From 6ee6232838886d8623bd1a066edfd53e99b0b8d4 Mon Sep 17 00:00:00 2001 From: Suleiman Shahbari Date: Fri, 3 Jul 2026 01:38:14 +0300 Subject: [PATCH] feat(example): bootstrap demo ships to Cloudflare end-to-end Wire the real cloudflareTarget adapter into the bootstrap-quickstart capstone. Offline: run it over a simulated wrangler (FakeRunner onExec) so the whole flow ends at a live-looking Workers URL with no credentials. Live: env-gate a real Cloudflare deploy when CLOUDFLARE_API_TOKEN is set, else planOnlyTarget. Closes #158 --- examples/bootstrap-quickstart/README.md | 35 +++++++++++++++---- .../src/bootstrap.test.ts | 8 +++-- .../bootstrap-quickstart/src/bootstrap.ts | 26 ++++++++++---- examples/bootstrap-quickstart/src/live.ts | 23 ++++++++++-- 4 files changed, 73 insertions(+), 19 deletions(-) diff --git a/examples/bootstrap-quickstart/README.md b/examples/bootstrap-quickstart/README.md index 3648406..0fa870f 100644 --- a/examples/bootstrap-quickstart/README.md +++ b/examples/bootstrap-quickstart/README.md @@ -17,8 +17,10 @@ pnpm --filter @gemstack/example-bootstrap-quickstart start No API key: `AiFake` scripts the model and `FakeRunner` is an in-memory sandbox, so the run is deterministic. You'll see the narration stream live, the files the -persona workers wrote, the checklist blocking once and then clearing, the deploy -decision, and the generated `CODE-OVERVIEW.md`. +persona workers wrote, the checklist blocking once and then clearing, the app +shipped to a Cloudflare URL, and the generated `CODE-OVERVIEW.md`. The deploy runs +the real `cloudflareTarget` adapter over a simulated `wrangler`, so the whole flow +ends at a live-looking URL with no credentials. ## What it shows @@ -28,7 +30,8 @@ decision, and the generated `CODE-OVERVIEW.md`. and records its choices to the **decisions ledger**, a **build** scaffolds the app with the persona workers inside a **runner**, the **full-fledged loop** repeats the production-grade checklist until its `{ blockers }` verdict is empty, and a - **deploy** is decided behind the `DeployTarget` seam. + **deploy** is decided and shipped through the `DeployTarget` seam — here the real + `cloudflareTarget` (SSR → Workers), run over a simulated `wrangler` offline. - **Surfaces (#100/#120)** — every phase streams as narration over the generic `launchAutopilot` handle. - **Scale mode (#114)** — `CODE-OVERVIEW.md` is generated from the scaffold. @@ -50,11 +53,29 @@ Swapping the fakes (`AiFake` + `FakeRunner`) for a real model + `LocalRunner` is only difference from `main.ts`; the orchestration is identical. A sample live run scaffolded a 9-file Vike + universal-orm orders app (schema + migration, `pages/orders/` with `+Page`/`+data`/`+config`, a UI-intent renderer) from the intent, blocked the -checklist once on missing auth, then decided SSR → dockploy. +checklist once on missing auth, then decided SSR → Cloudflare. -Scoped for a first, bounded proof: the production-grade **loop** keeps the scripted -verdict (so the run stays deterministic and cheap), and `deploy` uses `planOnlyTarget` -— it decides + narrates but does not actually ship. Real deploy adapters remain (#109). +### Real deploy to Cloudflare + +Add a Cloudflare token and the live run ships for real — `cloudflareTarget` +installs, builds, and deploys the scaffold (Workers for SSR, Pages for SSG/SPA) +and reports the live URL: + +```bash +ANTHROPIC_API_KEY=sk-... \ +CLOUDFLARE_API_TOKEN=... CLOUDFLARE_ACCOUNT_ID=... CLOUDFLARE_PROJECT=my-app \ +pnpm --filter @gemstack/example-bootstrap-quickstart start:live +``` + +Without `CLOUDFLARE_API_TOKEN` the deploy falls back to `planOnlyTarget` (decide + +narrate, no ship), so the live run works with only a model key. Note the deploy +runs `npm install && npm run build` against the scaffold, so a real ship needs the +generated app to actually build — that is what the full-fledged loop (and an +opt-in `serveCheck`, below) are for. + +Scoped for a bounded proof, the production-grade **loop** keeps the scripted verdict +so the run stays deterministic and cheap; making the checklist a real reviewer agent +is the natural follow-up. ### Giving the loop teeth: `serveCheck` diff --git a/examples/bootstrap-quickstart/src/bootstrap.test.ts b/examples/bootstrap-quickstart/src/bootstrap.test.ts index 688a578..ea05a9b 100644 --- a/examples/bootstrap-quickstart/src/bootstrap.test.ts +++ b/examples/bootstrap-quickstart/src/bootstrap.test.ts @@ -28,10 +28,12 @@ describe('bootstrap capstone: the whole epic composes end-to-end (offline)', () assert.deepEqual(result.blockers, []) assert.equal(result.productionGrade, true) - // Deploy: decided SSR + a target and reported the (fake) shipped URL. + // Deploy: decided SSR → Cloudflare and shipped via the real cloudflareTarget + // adapter (over a simulated wrangler), reporting the live URL it printed. assert.equal(result.deploy?.plan.render, 'ssr') - assert.equal(result.deploy?.plan.target, 'dockploy') - assert.equal(result.deploy?.result.url, 'https://orders.example.app') + assert.equal(result.deploy?.plan.target, 'cloudflare') + assert.equal(result.deploy?.result.deployed, true) + assert.equal(result.deploy?.result.url, 'https://orders-app.gemstack.workers.dev') // Surface: the stream ran scope-first, done-last, and the terminal printed it. assert.equal(events[0]?.type, 'scope') diff --git a/examples/bootstrap-quickstart/src/bootstrap.ts b/examples/bootstrap-quickstart/src/bootstrap.ts index 0caeaa4..cbadc75 100644 --- a/examples/bootstrap-quickstart/src/bootstrap.ts +++ b/examples/bootstrap-quickstart/src/bootstrap.ts @@ -6,7 +6,7 @@ import { loopChecklist, loopImprove, agentDeploy, - FakeDeployTarget, + cloudflareTarget, Loop, definePrompt, defineRule, @@ -88,8 +88,11 @@ const ARCHITECT_PLAN = { ], } -/** The deploy decision (what `agentDeploy` parses). */ -const DEPLOY_DECISION = { render: 'ssr', target: 'dockploy', reason: 'per-request orders data + server-side auth' } +/** The deploy decision (what `agentDeploy` parses). SSR → Cloudflare Workers. */ +const DEPLOY_DECISION = { render: 'ssr', target: 'cloudflare', reason: 'per-request orders data + server-side auth' } + +/** The URL the simulated `wrangler` prints, so the offline demo ends at a live-looking URL. */ +const DEPLOY_URL = 'https://orders-app.gemstack.workers.dev' /** * Script the fake provider. Order (concurrency 1) is: the architect's plan, then @@ -189,15 +192,26 @@ export async function runCapstone(write: (line: string) => void = () => {}): Pro const { preset, detection } = builtinPresetRegistry().select({ dependencies: PROJECT_DEPS }) const personas = presetPersonas(preset) - // Runner: an in-memory sandbox seeded with a minimal project. + // Runner: an in-memory sandbox seeded with a minimal project. `wrangler` is + // simulated (prints a live-looking URL) so the real cloudflareTarget adapter + // runs its full path — install → build → deploy → parse URL — offline. const runner = new FakeRunner({ - onExec: cmd => (cmd.includes('build') ? { stdout: 'built', stderr: '', exitCode: 0 } : { stdout: '', stderr: '', exitCode: 0 }), + onExec: cmd => { + if (cmd.includes('wrangler')) return { stdout: `Published orders-app\n${DEPLOY_URL}`, stderr: '', exitCode: 0 } + return { stdout: cmd.includes('build') ? 'built' : '', stderr: '', exitCode: 0 } + }, }) const session = await runner.boot({ files: { 'package.json': JSON.stringify({ name: 'orders-app' }) + '\n' } }) const loop = buildLoop() const ledger = new DecisionLedger() - const deployTarget = new FakeDeployTarget({ result: { deployed: true, url: 'https://orders.example.app' } }) + // The real Cloudflare adapter, run over the simulated wrangler above. A fake + // token lets it proceed offline; the live capstone passes a real one. + const deployTarget = cloudflareTarget({ + session, + apiToken: process.env['CLOUDFLARE_API_TOKEN'] ?? 'demo-token', + projectName: 'orders-app', + }) // Surfaces: run bootstrap detached; the terminal prints as events stream. const handle = launchAutopilot(onEvent => diff --git a/examples/bootstrap-quickstart/src/live.ts b/examples/bootstrap-quickstart/src/live.ts index 982d936..aed4cc4 100644 --- a/examples/bootstrap-quickstart/src/live.ts +++ b/examples/bootstrap-quickstart/src/live.ts @@ -6,6 +6,7 @@ import { loopChecklist, loopImprove, agentDeploy, + cloudflareTarget, Loop, definePrompt, defineRule, @@ -95,6 +96,23 @@ function buildLoop(): Loop { }) } +/** + * The deploy step. With `CLOUDFLARE_API_TOKEN` set, it ships for real: the model + * decides SSR/SSG/SPA and `cloudflareTarget` installs → builds → deploys the app + * to Cloudflare (Workers for SSR, Pages for SSG/SPA) and reports the live URL. + * Without a token it falls back to `planOnlyTarget` (decide + narrate, no ship), + * so the run works with only a model key. + */ +function deployStep(session: RunnerSession, deployer: ReturnType) { + if (process.env['CLOUDFLARE_API_TOKEN']) { + return agentDeploy(deployer, { + targets: ['cloudflare'], + target: cloudflareTarget({ session, projectName: process.env['CLOUDFLARE_PROJECT'] ?? 'gemstack-orders-demo' }), + }) + } + return agentDeploy(deployer) +} + /** Snapshot the real workspace into a { path: contents } map. */ async function snapshot(session: RunnerSession): Promise> { const files: Record = {} @@ -136,9 +154,8 @@ export async function runLiveCapstone(write: (line: string) => void = () => {}): build: supervisorBuild({ plan: livePlanner, workers: presetWorkers(session, personas), concurrency: 1 }), checklist: loopChecklist({ loop }), improve: loopImprove({ loop }), - // planOnlyTarget default: the model DECIDES render + target and narrates; actually - // shipping to Dockploy/Cloudflare stays behind the real DeployTarget adapters (#109). - deploy: agentDeploy(agent({ model: MODEL, instructions: 'deployer' })), + // Real Cloudflare deploy when CLOUDFLARE_API_TOKEN is set, else plan-only. + deploy: deployStep(session, agent({ model: MODEL, instructions: 'deployer' })), }, }).run(), )