diff --git a/examples/framework-demo/README.md b/examples/framework-demo/README.md new file mode 100644 index 0000000..994b3f7 --- /dev/null +++ b/examples/framework-demo/README.md @@ -0,0 +1,72 @@ +# The Framework — end-to-end demo + +One prompt, taken all the way to a **running, deployed app** — offline and +deterministic, in a couple of seconds. + +This runs the real product (`@gemstack/framework`'s `runFramework`) with the +built-in **fake driver**: no Claude Code, no model, no API keys. Same code a live +run executes — preset detection, architect decisions, the full-fledged +production-grade loop, deploy — just with scripted agent turns so it is instant +and repeatable. Two things are genuinely real, not narrated: + +- **the app boots and serves** — the serve gate starts a real HTTP server and the + run leaves it running, so the demo `fetch`es it and prints what it served; +- **deploy runs the real `cloudflareTarget` adapter** (over a simulated wrangler), + so it ends at a real-looking `workers.dev` URL. + +## Run it + +```bash +pnpm --filter @gemstack/example-framework-demo start +``` + +## What you see + +``` +Prompt: "A paginated orders page backed by an orders table, with sign-in." + +--- live narration --- +◆ fake in /tmp/framework-demo-xxxx + Detected Vike (confidence 2); framing with 3 persona(s) +▶ scope: full — "A paginated orders page backed by an orders table, with sign-in." +▶ architect: Vike + universal-orm on Postgres, with vike-auth + · universal-orm on Postgres — the orders catalog is relational and needs typed queries + · SSR over SPA — orders need per-request data and auth on the server + Building on Vike + universal-orm on Postgres, with vike-auth + Checking the app is production-grade + serve: start: node server.js + serve: fetch http://localhost:50106/ -> 200 + ✗ checklist pass 1: No authentication on the orders page yet + → improving: No authentication on the orders page yet + serve: start: node server.js + serve: fetch http://localhost:50106/ -> 200 + ✓ checklist pass 2: production-grade +▶ deploy: SSR → cloudflare (per-request orders data + server-side auth) +✓ production-grade in 2 pass(es) +▶ your app is running at http://localhost:50106 +✓ done + +--- outcome --- + preset detected: Vike + production-grade: true (in 2 pass(es)) + deployed to: cloudflare → https://orders-app.gemstack.workers.dev + running locally: http://localhost:50106 + it served: Orders

Orders

… +``` + +The loop does not take the agent's word for "production-grade": it **boots the app +and fetches it** every pass (the `serve:` lines), and blocks until both the review +and the real server pass. When it finishes, the app is left running so you can open +it — that is the localhost link, live until the run stops. + +## The real thing + +Same flow, driven against Claude Code instead of the fake driver: + +```bash +npx @gemstack/framework "a paginated orders page with sign-in" +``` + +That opens the localhost dashboard, wraps Claude Code as the coding agent, and runs +the identical scope → architect → build → production-grade loop → deploy — writing +real files and, with `--serve`, leaving the real app running behind a preview link. diff --git a/examples/framework-demo/package.json b/examples/framework-demo/package.json new file mode 100644 index 0000000..03eebc8 --- /dev/null +++ b/examples/framework-demo/package.json @@ -0,0 +1,22 @@ +{ + "name": "@gemstack/example-framework-demo", + "version": "0.0.0", + "private": true, + "description": "Showable end-to-end demo of @gemstack/framework: one prompt → scope → architect → build → full-fledged loop → deploy → a real running app, offline and deterministic via the fake driver.", + "type": "module", + "scripts": { + "typecheck": "tsc --noEmit", + "test": "tsc -p tsconfig.test.json && cd dist-test && node --test", + "clean": "rm -rf dist-test", + "start": "tsx src/main.ts" + }, + "dependencies": { + "@gemstack/framework": "workspace:^", + "@gemstack/ai-autopilot": "workspace:^" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "tsx": "^4.19.0", + "typescript": "^5.4.0" + } +} diff --git a/examples/framework-demo/src/demo.test.ts b/examples/framework-demo/src/demo.test.ts new file mode 100644 index 0000000..13f3a7b --- /dev/null +++ b/examples/framework-demo/src/demo.test.ts @@ -0,0 +1,31 @@ +import { describe, it } from 'node:test' +import assert from 'node:assert/strict' +import { DEMO_INTENT, runDemo } from './demo.js' + +describe('framework demo: one prompt → a running, deployed app (offline)', () => { + it('runs the whole product flow and ends at a real serving app', async () => { + const lines: string[] = [] + const out = await runDemo(line => lines.push(line)) + + // Preset detection picked Vike from the demo's deps signals. + assert.equal(out.framework, 'Vike') + + // The full-fledged loop blocked once (no auth) then cleared → production-grade. + assert.equal(out.productionGrade, true) + assert.equal(out.passes, 2) + + // Deploy ran the real cloudflareTarget adapter → a live workers.dev URL. + assert.equal(out.deployTarget, 'cloudflare') + assert.equal(out.deployUrl, 'https://orders-app.gemstack.workers.dev') + + // The app was actually booted and served (not just narrated). + assert.match(out.previewUrl ?? '', /^http:\/\/localhost:\d+$/) + assert.match(out.served, /Orders/) + assert.match(out.served, /Ada Lovelace/) + + // The narration told the whole story: the prompt, then a production-grade end. + assert.ok(lines.some(l => l.includes(DEMO_INTENT))) + assert.ok(lines.some(l => l.includes('production-grade'))) + assert.ok(lines.some(l => l.includes('your app is running at'))) + }) +}) diff --git a/examples/framework-demo/src/demo.ts b/examples/framework-demo/src/demo.ts new file mode 100644 index 0000000..4fe5ea6 --- /dev/null +++ b/examples/framework-demo/src/demo.ts @@ -0,0 +1,139 @@ +import { mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { createServer } from 'node:net' +import { cloudflareTarget } from '@gemstack/ai-autopilot' +import { + FAKE_DEPLOY, + FAKE_INTENT, + FAKE_SIGNALS, + fakeDriver, + formatFrameworkEvent, + runFramework, + type AppPreview, +} from '@gemstack/framework' + +/** + * The showable end-to-end demo for `@gemstack/framework`: one prompt taken all + * the way to a *running, deployed* app, offline and deterministic. + * + * It drives the real product (`runFramework`) with the built-in **fake driver** + * (no Claude Code, no model, no keys) so the whole flow — preset detection, + * architect decisions, build, the full-fledged production-grade loop, and deploy + * — runs the same code a live run does, just with scripted agent turns. Two + * things are genuinely real, not narrated: + * + * - the app **boots and serves**: the serve gate starts a real HTTP server and + * the run leaves it running, so the demo can `fetch` it and prove it works; + * - the **deploy** runs the real `cloudflareTarget` adapter over a simulated + * wrangler, so it ends at a real-looking `workers.dev` URL deterministically. + * + * This is the artifact to point people at (README / the-framework.ai / Discord): + * a single command that goes from an idea to an app you can open. + */ + +/** The one prompt the demo builds from. */ +export const DEMO_INTENT = FAKE_INTENT + +/** What {@link runDemo} reports back after the flow completes. */ +export interface DemoOutcome { + /** The detected framework preset (Vike, from the demo's deps signals). */ + framework: string | undefined + /** Whether the loop reached production-grade, and in how many passes. */ + productionGrade: boolean + passes: number + /** The deploy result: the target and the (simulated) live URL. */ + deployTarget: string | undefined + deployUrl: string | undefined + /** The localhost URL the app was left running at. */ + previewUrl: string | undefined + /** The first bytes the running app actually served (proof it works). */ + served: string +} + +/** An ephemeral free port so the demo never collides with something already bound. */ +function freePort(): Promise { + return new Promise((resolvePromise, rejectPromise) => { + const srv = createServer() + srv.on('error', rejectPromise) + srv.listen(0, '127.0.0.1', () => { + const addr = srv.address() + const port = typeof addr === 'object' && addr ? addr.port : 0 + srv.close(() => resolvePromise(port)) + }) + }) +} + +/** The tiny but real "orders app" the serve gate boots — a stand-in for what the agent builds. */ +function ordersAppSource(port: number): string { + return [ + `const http = require('http')`, + `const page = \`Orders`, + `

Orders

`, + ``, + `
#CustomerTotal
1001Ada Lovelace$42.00
1002Alan Turing$17.50
\``, + `http.createServer((_, res) => res.end(page)).listen(${port})`, + ].join('\n') +} + +/** The simulated Cloudflare: every command succeeds; the deploy prints a workers.dev URL. */ +const simulatedCloudflare = { + exec: async (command: string) => ({ + stdout: /wrangler|deploy/.test(command) + ? 'Published framework-demo\nhttps://orders-app.gemstack.workers.dev' + : '', + stderr: '', + exitCode: 0, + }), +} + +/** + * Run the whole demo and stream one narration line per phase to `onLine`. + * Deterministic and offline; leaves no processes or temp files behind. + */ +export async function runDemo(onLine: (line: string) => void): Promise { + const dir = await mkdtemp(join(tmpdir(), 'framework-demo-')) + const port = await freePort() + await writeFile(join(dir, 'server.js'), ordersAppSource(port) + '\n') + await writeFile(join(dir, 'package.json'), JSON.stringify({ name: 'orders-app', private: true }) + '\n') + + let preview: AppPreview | undefined + try { + const run = await runFramework({ + intent: DEMO_INTENT, + driver: fakeDriver(), + cwd: dir, + signals: FAKE_SIGNALS, + serve: { command: 'node server.js', port, waitMs: 8000, keepAlive: true }, + deploy: FAKE_DEPLOY, + deployTarget: cloudflareTarget({ + session: simulatedCloudflare, + apiToken: 'demo-token', + accountId: 'demo-account', + projectName: 'framework-demo', + }), + onEvent: event => onLine(formatFrameworkEvent(event)), + }) + preview = run.preview + + // Prove the app the loop signed off on is actually serving right now. + let served = '' + if (preview) { + const res = await fetch(preview.url) + served = (await res.text()).replace(/\s+/g, ' ').trim() + } + + return { + framework: run.detection.framework, + productionGrade: run.result.productionGrade, + passes: run.result.passes, + deployTarget: run.result.deploy?.plan.target, + deployUrl: run.result.deploy?.result.url, + previewUrl: preview?.url, + served, + } + } finally { + if (preview) await preview.stop() + await rm(dir, { recursive: true, force: true }) + } +} diff --git a/examples/framework-demo/src/main.ts b/examples/framework-demo/src/main.ts new file mode 100644 index 0000000..80d15af --- /dev/null +++ b/examples/framework-demo/src/main.ts @@ -0,0 +1,24 @@ +import { DEMO_INTENT, runDemo } from './demo.js' + +/** Run the demo and print the whole story: the live narration, then the outcome. */ +async function main(): Promise { + console.log('The Framework — end-to-end demo (offline, deterministic)\n') + console.log(`Prompt: "${DEMO_INTENT}"\n`) + console.log('--- live narration ---') + const out = await runDemo(line => console.log(line)) + + console.log('\n--- outcome ---') + console.log(` preset detected: ${out.framework}`) + console.log(` production-grade: ${out.productionGrade} (in ${out.passes} pass(es))`) + console.log(` deployed to: ${out.deployTarget} → ${out.deployUrl}`) + console.log(` running locally: ${out.previewUrl}`) + console.log(` it served: ${out.served.slice(0, 72)}${out.served.length > 72 ? '…' : ''}`) + + console.log('\nThat is the fake driver. To do it for real against Claude Code:') + console.log(' npx @gemstack/framework "a paginated orders page with sign-in"') +} + +main().catch(err => { + console.error(err) + process.exitCode = 1 +}) diff --git a/examples/framework-demo/tsconfig.json b/examples/framework-demo/tsconfig.json new file mode 100644 index 0000000..404aab4 --- /dev/null +++ b/examples/framework-demo/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { "noEmit": true, "rootDir": "src" }, + "include": ["src"] +} diff --git a/examples/framework-demo/tsconfig.test.json b/examples/framework-demo/tsconfig.test.json new file mode 100644 index 0000000..eebda2f --- /dev/null +++ b/examples/framework-demo/tsconfig.test.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { "outDir": "dist-test", "rootDir": "src" }, + "include": ["src"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f28338e..367f377 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -96,6 +96,25 @@ importers: specifier: ^5.4.0 version: 5.9.3 + examples/framework-demo: + dependencies: + '@gemstack/ai-autopilot': + specifier: workspace:^ + version: link:../../packages/ai-autopilot + '@gemstack/framework': + specifier: workspace:^ + version: link:../../packages/framework + devDependencies: + '@types/node': + specifier: ^20.0.0 + version: 20.19.43 + tsx: + specifier: ^4.19.0 + version: 4.22.4 + typescript: + specifier: ^5.4.0 + version: 5.9.3 + examples/mcp-quickstart: dependencies: '@gemstack/mcp':