Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
node_modules
dist
.git
.claude
coverage
*.db
*.db-*
19 changes: 19 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: ci
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm typecheck
- run: pnpm test:run
- run: pnpm build
60 changes: 60 additions & 0 deletions .github/workflows/deploy-dev2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Ship every merge to dev2, where submitfleet.com runs (born there 2026-10-14,
# never on Railway). The box pulls the merged commit, rebuilds the
# image(s) from this repo and restarts the compose stack under
# /home/anthony/www/submitfleet.com. Generated by cli-tools/dev2/dev2-site scaffold.
name: Deploy to dev2

on:
push:
branches: [main]
workflow_dispatch:
inputs:
ref:
description: Git ref to deploy (defaults to the pushed commit)
required: false
type: string

concurrency:
group: deploy-dev2
cancel-in-progress: false

jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
# ${{ }} values go through env, never straight into a run: body.
- name: Resolve target revision
id: rev
env:
REF: ${{ inputs.ref || github.sha }}
run: echo "sha=$REF" >> "$GITHUB_OUTPUT"

- name: Set up ssh
env:
SSH_KEY: ${{ secrets.DEV2_SSH_KEY }}
KNOWN_HOSTS: ${{ secrets.DEV2_KNOWN_HOSTS }}
run: |
install -d -m 700 ~/.ssh
printf '%s\n' "$SSH_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
printf '%s\n' "$KNOWN_HOSTS" > ~/.ssh/known_hosts
chmod 644 ~/.ssh/known_hosts

- name: Deploy
env:
DEV2_USER: ${{ secrets.DEV2_USER }}
DEV2_HOST: ${{ secrets.DEV2_HOST }}
SHA: ${{ steps.rev.outputs.sha }}
run: |
ssh -o BatchMode=yes "$DEV2_USER@$DEV2_HOST" \
/home/anthony/www/submitfleet.com/deploy-app.sh "$SHA"

- name: Verify the site answers
run: |
for i in $(seq 1 10); do
code=$(curl -s -o /dev/null -w '%{http_code}' "https://submitfleet.com/healthz" || true)
case "$code" in 2*|3*|401|403) echo "submitfleet.com $code"; exit 0;; esac
echo "attempt $i: $code"; sleep 10
done
echo "submitfleet.com never answered"; exit 1
30 changes: 30 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# SubmitFleet: API, MCP (/mcp), dashboard (/app) and the worker in one process.
FROM node:24-alpine AS build
WORKDIR /app
RUN corepack enable
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
RUN pnpm install --frozen-lockfile
COPY tsconfig.json tsconfig.build.json ./
COPY src ./src
RUN pnpm build

FROM node:24-alpine AS deps
WORKDIR /app
RUN corepack enable
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
RUN pnpm install --frozen-lockfile --prod --ignore-scripts

FROM node:24-alpine
WORKDIR /app
# SQLite lives in /data (a bind-mounted volume on dev2); SUBMITFLEET_SECRET_KEY comes from the vault.
ENV NODE_ENV=production HOST=0.0.0.0 PORT=3000 SUBMITFLEET_HOME=/data
# --chown: the deploy account's umask 007 leaves the checkout unreadable to other users.
COPY --chown=node:node package.json ./
COPY --chown=node:node catalog ./catalog
COPY --from=deps --chown=node:node /app/node_modules ./node_modules
COPY --from=build --chown=node:node /app/dist ./dist
RUN mkdir -p /data && chown node:node /data
USER node
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s CMD wget -qO- http://127.0.0.1:3000/healthz || exit 1
CMD ["node", "dist/cli.js", "serve", "--port", "3000"]
115 changes: 115 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,118 @@
# SubmitFleet

One product profile. Coordinated submissions. Proof of what went live.

SubmitFleet is an open-source, self-hostable engine for submitting legitimate products to launch
sites and directories. It keeps one approved product profile, plans a bounded campaign, asks the
owner to approve exactly what will be sent, runs durable workers, and records receipts and verified
listings. The same core serves a CLI, a TUI, an HTTP API, an MCP server and a mobile dashboard.

## Status: early engine, below the release gate

| | |
| --- | --- |
| Release-qualifying destinations | **0 of 100 required** (`submitfleet coverage report`) |
| Adapters shipped | 1 (TinyLaunch, contract-tested against its published agent guide; no live submission yet) |
| Deployed | No. Nothing runs at submitfleet.com yet. |
| Submissions made | None |

This build is the foundation the 100-destination release is measured against, not that release.
It does not claim traffic, ranking or customer outcomes. The PRD's value pilot, design-partner
evidence and the adapter build-out to 100+ are still ahead.

## What the engine enforces

- **The release gate.** A destination counts only if it is a launch site or directory, verified free
with fresh evidence, permission-reviewed, unattended (not assisted, not always-captcha), has an
installed adapter with contract tests, owner-authorized live-submission evidence, reconciliation
(or a documented limitation), healthy status and an accepted quality review. Counts are of
independent listing groups, so aliases, mirrors and language paths never multiply coverage.
Imports from public lists are discovery input only and can never enable execution.
- **Strict campaigns.** `--min-executable-destinations` defaults to 100. A campaign with fewer
executable targets returns `coverage_insufficient` with the real count and every blocker, and
cannot be approved or run. The owner may plan a smaller campaign explicitly; the engine gate is
unchanged.
- **Approvals bound to scope.** The owner approves a hash of the exact targets, accounts, payloads,
product facts, dates and spend limits. Any material change invalidates it. Agents (and the MCP
server) can request approval but there is no agent path to grant it; the CLI only approves from
an interactive terminal.
- **Free only by default.** Listing spend is fixed at $0. Paid, conditional-free and
backlink-required routes are excluded. An adapter that meets an unexpected payment stops for a
human.
- **Durable, honest execution.** Intents are persisted before any external write. Workers claim
atomically with leases and fencing; per-account queue limits and cooldowns are enforced inside the
claim. A timeout after a possible write is `uncertain` and is reconciled before anything is
retried. Submitted, scheduled, under review and published are separate states. The event history
is append-only.
- **Bounded spend.** Operating costs are reserved atomically before dispatch and settled after, so
parallel workers cannot overspend.
- **Stops.** Global pause, campaign pause, account disconnect and destination disable all stop new
writes. Cancel stops unsent work and never claims remote deletion.
- **Secrets.** Account credentials are sealed with AES-256-GCM and never appear in model context,
reports or logs. Workspaces are isolated.
- **Reports.** Original target set, exclusions, receipts, listing URLs, unresolved jobs, effort and
itemized cost, in Markdown, JSON or CSV. Missing analytics or effort stay unknown, never zero.

## Quick start (self-hosted)

```sh
pnpm install && pnpm build
node dist/cli.js init # workspace, secret key, owner API key (shown once)
node dist/cli.js catalog seed
node dist/cli.js coverage report # exit 3 while below 100
node dist/cli.js products add https://your-product.example --name "Name" --tagline "..." \
--description "..." --logo logo.png --field tinylaunch.category_id=9
node dist/cli.js products approve PRODUCT_ID
node dist/cli.js accounts connect tinylaunch --email you@example.com
node dist/cli.js campaign plan --product PRODUCT_ID --free-only --min-executable-destinations 100
node dist/cli.js campaign preview CAMPAIGN_ID
node dist/cli.js campaign approve CAMPAIGN_ID
node dist/cli.js campaign run CAMPAIGN_ID
node dist/cli.js worker # or: serve (API + dashboard + worker)
node dist/cli.js report export CAMPAIGN_ID --format markdown
```

Assets (logos) are read only from `$SUBMITFLEET_HOME/assets`. Run `submitfleet help` for every
command. Exit codes: 0 ok, 1 error, 2 usage, 3 gate not met, 4 jobs waiting for a person.

### Configuration

| Variable | Default |
| --- | --- |
| `SUBMITFLEET_HOME` | `~/.local/share/submitfleet` |
| `SUBMITFLEET_DB_URL` / `SUBMITFLEET_DB_AUTH_TOKEN` | local SQLite file; set a `libsql://` URL for Turso |
| `SUBMITFLEET_SECRET_KEY` | generated key file in `SUBMITFLEET_HOME` (hosted: inject from the vault) |
| `SUBMITFLEET_ASSETS_DIR` | `$SUBMITFLEET_HOME/assets` |
| `PORT`, `HOST` | `8787`, `127.0.0.1` |
| `SUBMITFLEET_ALLOWED_ORIGINS` | none (same-origin only) |

## Surfaces

- **CLI** `submitfleet ...` and **TUI** `submitfleet tui`
- **HTTP API** under `/api/v1` with bearer API keys (`owner` or `agent` role)
- **MCP**: `submitfleet mcp --stdio` locally, or authenticated Streamable HTTP at `/mcp`. All tools
use the `submitfleet_` prefix.
- **Dashboard** at `/app`: coverage, portfolio, campaigns (preview, approve, run, pause, cancel),
human-action queue, accounts, reports with Copy Markdown and downloads.

## Adapters

Every adapter implements `capabilities`, `validate`, `prepare` (no remote writes), `preflight`,
`execute` and `reconcile`; `update` and `withdraw` are optional and otherwise return
`unsupported_operation`. See `src/adapters/types.ts` and the TinyLaunch adapter for the pattern.
An adapter in the code is necessary, never sufficient: a destination counts only after its catalog
record carries contract-test and live-submission evidence through a reviewed registry edit.

## Known gaps against the PRD

- 99+ more qualifying adapters, and the first live TinyLaunch submission.
- Fact extraction from a product URL (products are entered, then owner-approved).
- Dashboard sign-in is an API key; the house magic-link and passkey sign-in is not wired yet.
- The dashboard is server-rendered HTML rather than Next.js.
- Browser-adapter runtime (isolated profiles, selectors, receipts) is not built.
- Analytics integration: outcomes can be recorded but no provider is connected.
- Hosting, DNS and TLS for submitfleet.com are not configured.

## License

MIT
36 changes: 36 additions & 0 deletions catalog/seed.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"note": "Reviewed seed records. A record here is research, not coverage: none is live-tested, so none counts toward the 100-destination release gate.",
"destinations": [
{
"slug": "tinylaunch",
"name": "TinyLaunch",
"canonical_domain": "tinylaunch.com",
"listing_group": "tinylaunch.com",
"kind": "launch",
"submission_url": "https://www.tinylaunch.com/api/v1/launches",
"required_fields": ["name", "tagline", "description", "url", "category_id", "logo"],
"account_requirement": "email_otp_maker_profile",
"fee_status": "verified_free",
"fee_conditions": "Free launch uses a daily free-slot pool; premium ($39) and a 110-directory service ($279) are separate paid options that SubmitFleet never selects.",
"fee_evidence_url": "https://www.tinylaunch.com/llms.txt",
"fee_checked_at": "2026-10-14T00:00:00.000Z",
"automation_method": "api",
"automation_permission": "permitted",
"permission_evidence": "Official agent guide documents the API for agents: https://www.tinylaunch.com/agents and https://www.tinylaunch.com/llms.txt",
"permission_checked_at": "2026-10-14T00:00:00.000Z",
"challenge_policy": "none",
"backlink_required": false,
"quality_decision": "unreviewed",
"quality_basis": null,
"adapter_id": "tinylaunch",
"adapter_version": "0.1.0",
"adapter_health": "unknown",
"reconcile_support": "limited",
"reconcile_limitation": "Launches are not listable via the documented API; confirmation comes from the POST receipt and the public launch page.",
"queue_limit_per_account": 1,
"queue_window_hours": 24,
"source": "https://www.tinylaunch.com/llms.txt",
"source_license": "facts only"
}
]
}
62 changes: 62 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{
"name": "@profullstack/submitfleet",
"version": "0.1.0",
"description": "SubmitFleet: one product profile, coordinated launch and directory submissions, proof of what went live. Self-hostable engine with CLI, TUI, HTTP API, MCP server and a mobile dashboard.",
"type": "module",
"license": "MIT",
"homepage": "https://submitfleet.com",
"repository": {
"type": "git",
"url": "git+https://github.com/profullstack/submitfleet.git"
},
"bugs": {
"url": "https://github.com/profullstack/submitfleet/issues"
},
"keywords": [
"launch",
"directory",
"submission",
"mcp",
"cli",
"startup"
],
"bin": {
"submitfleet": "dist/cli.js"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": "./dist/index.js"
},
"files": [
"dist",
"catalog",
"README.md",
"LICENSE"
],
"engines": {
"node": ">=22.5.0"
},
"packageManager": "pnpm@11.18.0",
"scripts": {
"build": "tsc -p tsconfig.build.json",
"dev": "tsx src/cli.ts",
"start": "node dist/cli.js serve",
"typecheck": "tsc --noEmit",
"test": "vitest",
"test:run": "vitest run"
},
"dependencies": {
"@hono/node-server": "2.1.5",
"@libsql/client": "0.18.0",
"@modelcontextprotocol/sdk": "1.32.1",
"hono": "4.13.13",
"zod": "4.6.7"
},
"devDependencies": {
"@types/node": "^26.6.5",
"tsx": "^4.23.15",
"typescript": "^7.0.2",
"vitest": "5.0.4"
}
}
Loading
Loading