tanship-cli
v0.0.2
Published
Bootstrap TANSHIP template projects: Cloudflare D1/R2 resources, identity rewrite, .env generation, migrations, first production deploy, and a private GitHub repo.
Readme
tanship-cli
npx tanship-cli create — one guided run that turns a fresh TANSHIP template clone into a deployed, privately-owned project:
- preflight toolcheck (node ≥22, pnpm, git, gh auth, Cloudflare credentials)
- guided questionnaire + single overview confirm
- clone
TANSHIP-app/tanship_template_1(remote renamed toupstream) pnpm install+cloudflare.vars.json{}placeholder (fresh-clone guard)- Cloudflare D1 + R2 creation with read-back (reuses existing names; a globally-taken R2 name forces a rename)
- identity rewrite across 13 files + residue scan (must be 0 matches)
.envgeneration (3 generated secrets, pasted keys, neutral placeholders for inactive providers / Turnstile / Chatwoot) +pnpm cloudflare:sync-env- D1 migrations (
--local, then--remote) — the command that never exits is handled: completion markers, quiet-window settle, SIGTERM/SIGKILL, exit-0-without-marker is treated as failure - first
pnpm deploy:production+ origin health poll - commit + private GitHub repo (
gh repo create --private --source . --remote origin --push) - final report (names only — secret values are never echoed)
Requirements
- Node ≥22, pnpm, git
- GitHub CLI authenticated (
gh auth login) with access to the private template repo - Shell exports:
CLOUDFLARE_ACCOUNT_IDCLOUDFLARE_API_TOKEN(template: “Edit Cloudflare Workers”)
Every spawned cf command gets these variables injected explicitly — cf does not read .env by itself. The token intentionally lives only in the shell + .env (never written by the CLI into state).
Usage
npx tanship-cli create # interactive from scratch
npx tanship-cli create --name my-app # pre-fill the project name
npx tanship-cli create --dry-run # Steps 0–1 only, zero creation
npx tanship-cli create --resume # continue an interrupted runProgress lives in <project>/.tanship/state.json — non-secret answers and created-resource ids only. Pasted credentials go into <project>/.env (gitignored) and are never persisted to state.
Replayable automation mode (--answers)
For E2E reproducibility (“one command, from zero to deployed, rerunnable”):
npx tanship-cli create --answers ./e2e-answers.jsonAutomation replays still pause at the overview confirm (one Enter / one expect send) before anything is created — a human stays on the trigger. The JSON file shape (validated strictly):
{
"answers": {
"projectName": "acme", // also dir, worker, db, repo defaults
"dirName": "acme",
"workerName": "acme",
"dbName": "acme",
"bucketName": "acme-assets",
"ghOwner": "acme-org",
"repoName": "acme",
"provider": "creem", // creem | stripe | paypal
"googleEnabled": false,
"contactToEmail": "[email protected]",
"resendFromEmail": "Acme <[email protected]>"
},
"providerInput": { /* every key for the chosen provider */ },
"resendApiKey": "re_...",
"googleInput": { "GOOGLE_CLIENT_ID": "", "GOOGLE_CLIENT_SECRET": "" }
}The overview confirm step stays interactive on purpose: automation replays are explicitly confirmed by a human (or expect) before anything is created.
Contract points (template ↔ CLI)
Changing any of these in the template means a CLI release:
- Template repo:
TANSHIP-app/tanship_template_1(gh repo clone, HTTPS) - Script names:
cloudflare:sync-env,deploy:check,deploy:production,db:migrate:local,db:migrate:remote,build - Env names (full list from
.env.example):- public:
VITE_*,PAYMENT_PROVIDER,CREEM_MODE,CREEM_PRODUCT_*_ID,STRIPE_PRICE_*_ID,PAYPAL_MODE,PAYPAL_CLIENT_ID,PAYPAL_WEBHOOK_ID,PAYPAL_PLAN_*_ID,GOOGLE_CLIENT_ID,VITE_GOOGLE_CLIENT_ID,RESEND_FROM_EMAIL,CONTACT_TO_EMAIL,VITE_TURNSTILE_SITE_KEY,VITE_ENABLE_TWITTER_TESTIMONIALS,VITE_TESTIMONIAL_TWEET_IDS,ADMIN_EMAILS - secrets:
BETTER_AUTH_SECRET,NEWSLETTER_TOKEN_SECRET,CONTACT_FORM_SECRET,TURNSTILE_SECRET_KEY,RESEND_API_KEY,GOOGLE_CLIENT_SECRET,CREEM_API_KEY,CREEM_WEBHOOK_SECRET,STRIPE_SECRET_KEY,STRIPE_WEBHOOK_SECRET,PAYPAL_CLIENT_SECRET - tooling:
CLOUDFLARE_ACCOUNT_ID,CLOUDFLARE_DATABASE_ID
- public:
- Config structure:
cloudflare.vars.json(local-only, imported bycloudflare.config.ts), D1 binding underworker.env.DB(name,id), R2 bindingASSETS_R2(name) - Identity rewrite checklist (plan §3 Step 5 — 13 files):
| # | File | Rewritten |
|---|------|-----------|
| 1 | cloudflare.config.ts | worker name, DB name+id, R2 bucket |
| 2 | scripts/cloudflare-env.mjs | PRODUCTION_BASE_URL |
| 3 | scripts/build-cloudflare.mjs | PRODUCTION_BASE_URL |
| 4 | scripts/deploy-check.mjs | worker/db name constants, db id constant |
| 5 | package.json | db:migrate:* D1 ids |
| 6 | .env | generated in Step 6 — not rewritten in Step 5 |
| 7 | src/tests/cloudflare-build.test.ts | origin assertions |
| 8 | src/tests/cloudflare-env-sync.test.ts | origin fixture |
| 9 | src/tests/deploy-preflight.test.ts | worker/db name + id fixtures |
| 10 | README.md | worker/db name, origin host, tail name |
| 11 | src/server/AGENTS.md | bucket example, tail name |
| 12 | src/tests/analytics-host.test.tsx, src/tests/auth-handler-identity.test.ts | origin fixtures |
| 13 | scripts/deploy-production.mjs | secrets tmpdir prefix |
Residue scan patterns are precise literals: template worker name, template D1 id, template workers.dev origin host. Bare account handles are deliberately not scanned (they would mis-hit the dependency name @pang3fan-creator/analytics-kit).
Development
pnpm install
pnpm exec tsc --noEmit # typecheck
pnpm build # emit dist/
pnpm exec vitest run test/<name>.test.ts # targeted suitesTest suites: identity (rewrite engine + residue scan + resume mode), migrate (hang → marker → SIGTERM handling), envfile (rendering + placeholder rules), state (no secrets persisted), proc (credential injection), resources (D1/R2 parse + reuse logic + subdomain), preflight (Step 0 gates), report (names-only guarantee).
E2E (stays out of unit runs): real Cloudflare resources, strictly serial, one round-trip per round; artifacts under test/e2e/.
