create-oke 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,11 +3,15 @@
3
3
  Scaffold an [okengine](https://www.npmjs.com/package/okengine) app.
4
4
 
5
5
  ```bash
6
- bunx create-oke@latest my-app --template hello
6
+ bunx create-oke@latest my-app
7
7
  cd my-app
8
8
  oke dev
9
9
  ```
10
10
 
11
+ On a TTY, create-oke opens a Clack wizard: confirm name, pick a template, and
12
+ optionally install + start `oke dev`. Pass `--yes` or `--template` /
13
+ `--from-example` for non-interactive (CI / agents).
14
+
11
15
  | Port | What |
12
16
  |---|---|
13
17
  | `:6530` | your app |
@@ -21,6 +25,7 @@ Pick another with `--template`, or start from a teaching example with `--from-ex
21
25
  bunx create-oke@latest my-app --template hello
22
26
  bunx create-oke@latest my-app --template minimal
23
27
  bunx create-oke@latest my-app --template full
28
+ bunx create-oke@latest my-app --yes
24
29
  bunx create-oke@latest my-notes --from-example notes
25
30
  ```
26
31
 
@@ -31,6 +36,9 @@ bunx create-oke@latest my-notes --from-example notes
31
36
  | `standard` | Full recommended file layout, empty scaffolding (default) |
32
37
  | `full` | Every element present and wired, no business logic |
33
38
 
39
+ Every scaffold writes an `AGENTS.md` so coding agents know the OKE contract
40
+ (`--no-agents-md` to skip).
41
+
34
42
  `--from-example` copies a teaching example (including its business logic and
35
43
  comments) — most new projects want `--template` instead.
36
44
 
@@ -41,10 +49,6 @@ comments) — most new projects want `--template` instead.
41
49
  | `provisions` | durable flows · `vault` · `channel` · plugins |
42
50
  | `skyport` | `ai` · multi-tenancy · distributed topology |
43
51
 
44
- With no arguments in a real terminal, create-oke asks for a project name and
45
- template interactively. Non-TTY / CI / any explicit flag stays fully scriptable
46
- — zero prompts.
47
-
48
52
  ## Registries
49
53
 
50
54
  | Registry | Package |
@@ -52,4 +56,4 @@ template interactively. Non-TTY / CI / any explicit flag stays fully scriptable
52
56
  | npm (CLI) | [`create-oke`](https://www.npmjs.com/package/create-oke) — use this with `bunx` |
53
57
  | JSR | [`@omqkhafi/create-oke`](https://jsr.io/@omqkhafi/create-oke) — same sources; prefer npm for the binary |
54
58
 
55
- Requires **Bun ≥ 1.3**. Framework docs: [okengine README](https://github.com/omqkhafi/okengine#readme) · [unified theory](https://github.com/omqkhafi/okengine/blob/main/docs/spec/unified-theory.md).
59
+ Requires **Bun ≥ 1.3**. Docs: [okengine.vercel.app](https://okengine.vercel.app) · [handbook](https://okengine.vercel.app/docs) · [unified theory](https://github.com/omqkhafi/okengine/blob/main/docs/spec/unified-theory.md).
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oke/example-linkly",
3
- "version": "0.2.1",
3
+ "version": "0.0.1",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "dependencies": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oke/example-notes",
3
- "version": "0.2.1",
3
+ "version": "0.0.1",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "dependencies": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oke/example-provisions",
3
- "version": "0.2.1",
3
+ "version": "0.0.1",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "dependencies": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oke/example-skyport",
3
- "version": "0.2.1",
3
+ "version": "0.0.1",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "dependencies": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-oke",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Scaffold an okengine app — bunx create-oke@latest <name>",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Slim `AGENTS.md` written into every scaffolded app for coding agents.
3
+ */
4
+
5
+ import { docsUrl } from "./docs-origin.ts";
6
+
7
+ /**
8
+ * Render the app-facing agent contract for a new project.
9
+ *
10
+ * @param projectName - Sanitized package / folder name
11
+ */
12
+ export function agentsMdContent(projectName: string): string {
13
+ const handbook = docsUrl("/docs");
14
+ return `# ${projectName} — OKE Agent Contract
15
+
16
+ This file is for coding agents (Cursor, Claude Code, Codex, …). Read it before
17
+ changing backend code. Handbook: ${handbook}
18
+
19
+ **If the docs are silent, stop and ask.**
20
+
21
+ ## The one law
22
+
23
+ Every backend behavior is a Flow:
24
+
25
+ \`\`\`text
26
+ on(Trigger) → Effects
27
+ \`\`\`
28
+
29
+ There are no separate species called endpoints, handlers, consumers, jobs,
30
+ subscribers, or workflows. There is one species — the **Flow** — and triggers
31
+ are typed values.
32
+
33
+ ## The eight elements
34
+
35
+ | Element | Essence |
36
+ |---|---|
37
+ | **Flow** | behavior |
38
+ | **Signal** | data in motion |
39
+ | **Store** | data at rest (\`sql\` · \`kv\` · \`files\` · \`index\`) |
40
+ | **Clock** | time |
41
+ | **Gate** | permission to act |
42
+ | **Vault** | protected knowledge |
43
+ | **Channel** | reaching humans |
44
+ | **AI** | reaching machine intelligence |
45
+
46
+ An element earns its place only if it has irreducible physics. New
47
+ infrastructure becomes a new **driver** for an existing element — never a
48
+ ninth element.
49
+
50
+ ## The ten exports
51
+
52
+ \`\`\`typescript
53
+ import { on, flow, signal, store, clock, gate, vault, channel, ai, plugin } from "okengine";
54
+ \`\`\`
55
+
56
+ That is the entire public vocabulary. Everything else is derived.
57
+
58
+ ## The fx rule
59
+
60
+ **All world access goes through \`fx\`. No exceptions.**
61
+
62
+ A direct \`node:\` import (or any other side-channel I/O) in element or flow
63
+ code is a **defect**. \`fx\` is the single door to the outside world; effects
64
+ are inferred from what a Flow touches through it.
65
+
66
+ ## Ports
67
+
68
+ | Port | Surface |
69
+ |---|---|
70
+ | **6530** | App |
71
+ | **6533** | Console |
72
+ | **6535** | MCP |
73
+
74
+ Mnemonic: O·K·E = 6·5·3.
75
+
76
+ ## How to run
77
+
78
+ \`\`\`bash
79
+ bun install
80
+ oke dev # or: bun run dev
81
+ \`\`\`
82
+
83
+ App \`:6530\` · Console \`:6533\` · MCP \`:6535\`.
84
+
85
+ ## Common mistakes
86
+
87
+ - ❌ I/O outside \`fx\` (raw \`fetch\`, \`node:fs\`, vendor SDKs in flow bodies)
88
+ - ✅ \`fx.store\` / \`fx.emit\` / \`fx.ask\` / channel sends through \`fx\`
89
+ - ❌ Inventing a ninth “element” or a parallel handler stack beside Flows
90
+ - ✅ New capability = new **driver** on an existing element, or a new Flow
91
+ - ❌ Untyped HTTP handlers that skip \`on\` / \`flow\` / contracts
92
+ - ✅ \`on(http.get("/…"), flow({ in, out, do }))\`
93
+
94
+ ## Learn more
95
+
96
+ - Handbook: ${handbook}
97
+ - Get started: ${docsUrl("/docs/get-started/introduction")}
98
+ `;
99
+ }
package/src/cli.test.ts CHANGED
@@ -3,7 +3,13 @@
3
3
  */
4
4
 
5
5
  import { describe, expect, test } from "bun:test";
6
- import { mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs";
6
+ import {
7
+ existsSync,
8
+ mkdtempSync,
9
+ readFileSync,
10
+ readdirSync,
11
+ rmSync,
12
+ } from "node:fs";
7
13
  import { tmpdir } from "node:os";
8
14
  import { join } from "node:path";
9
15
  import {
@@ -72,9 +78,17 @@ describe("shouldPrompt", () => {
72
78
  expect(shouldPrompt(parseArgs([]), true)).toBe(true);
73
79
  });
74
80
 
75
- test("non-TTY or any explicit input no prompts", () => {
81
+ test("TTY + name alonestill interactive (confirm name / template)", () => {
82
+ expect(shouldPrompt(parseArgs(["my-app"]), true)).toBe(true);
83
+ });
84
+
85
+ test("non-TTY, --yes, or config flags → no prompts", () => {
76
86
  expect(shouldPrompt(parseArgs([]), false)).toBe(false);
77
- expect(shouldPrompt(parseArgs(["my-app"]), true)).toBe(false);
87
+ expect(shouldPrompt(parseArgs(["my-app"]), false)).toBe(false);
88
+ expect(shouldPrompt(parseArgs(["my-app", "--yes"]), true)).toBe(false);
89
+ expect(shouldPrompt(parseArgs(["my-app", "--template", "hello"]), true)).toBe(
90
+ false,
91
+ );
78
92
  expect(shouldPrompt(parseArgs(["--template", "hello"]), true)).toBe(false);
79
93
  expect(shouldPrompt(parseArgs(["--from-example", "notes"]), true)).toBe(
80
94
  false,
@@ -83,6 +97,23 @@ describe("shouldPrompt", () => {
83
97
  });
84
98
  });
85
99
 
100
+ describe("parseArgs flags", () => {
101
+ test("accepts --yes / --install / --no-install / --no-agents-md", () => {
102
+ expect(parseArgs(["x", "--yes"]).yes).toBe(true);
103
+ expect(parseArgs(["x", "-y"]).yes).toBe(true);
104
+ expect(parseArgs(["x", "--install"]).install).toBe(true);
105
+ expect(parseArgs(["x", "--no-install"]).install).toBe(false);
106
+ expect(parseArgs(["x", "--no-agents-md"]).agentsMd).toBe(false);
107
+ expect(parseArgs(["x"]).agentsMd).toBe(true);
108
+ });
109
+
110
+ test("rejects --install with --no-install", () => {
111
+ expect(() => parseArgs(["x", "--install", "--no-install"])).toThrow(
112
+ /install/,
113
+ );
114
+ });
115
+ });
116
+
86
117
  describe("sourceFromArgs", () => {
87
118
  test("prefers example when set", () => {
88
119
  expect(sourceFromArgs(parseArgs(["x", "--from-example", "linkly"]))).toEqual(
@@ -98,7 +129,12 @@ describe("sourceFromArgs", () => {
98
129
  describe("scaffoldArgsFromAnswers ≡ flag-driven", () => {
99
130
  test("each --template path matches interactive choice", () => {
100
131
  for (const id of TEMPLATES) {
101
- const answers: InteractiveAnswers = { name: "x", choice: id };
132
+ const answers: InteractiveAnswers = {
133
+ name: "x",
134
+ choice: id,
135
+ installAndRun: false,
136
+ agentsMd: true,
137
+ };
102
138
  const fromAnswers = scaffoldArgsFromAnswers(answers);
103
139
  const fromFlags = scaffoldArgsFromCli(
104
140
  parseArgs(["x", "--template", id]),
@@ -114,6 +150,8 @@ describe("scaffoldArgsFromAnswers ≡ flag-driven", () => {
114
150
  name: "x",
115
151
  choice: FROM_EXAMPLE_CHOICE,
116
152
  example: id,
153
+ installAndRun: false,
154
+ agentsMd: true,
117
155
  };
118
156
  const fromAnswers = scaffoldArgsFromAnswers(answers);
119
157
  const fromFlags = scaffoldArgsFromCli(
@@ -144,6 +182,7 @@ describe("transformPackageJson", () => {
144
182
  "0.0.26",
145
183
  );
146
184
  expect(next.name).toBe("my-app");
185
+ expect(next.version).toBe("0.0.1");
147
186
  expect(next.dependencies?.["okengine"]).toBe("0.0.26");
148
187
  expect(next.dependencies?.["okengine"]).not.toBe("file:../..");
149
188
  expect(next.dependencies?.["zod"]).toBe("^4.4.3");
@@ -186,7 +225,20 @@ describe("scaffold structure", () => {
186
225
  name: `app-${id}`,
187
226
  source: { kind: "template", id },
188
227
  });
189
- expect([...result.files].sort()).toEqual(expected);
228
+ expect([...result.files].sort()).toEqual(
229
+ [...expected, "AGENTS.md"].sort(),
230
+ );
231
+ expect(result.files).toContain(".gitignore");
232
+ expect(result.files).toContain("README.md");
233
+ expect(
234
+ readFileSync(join(result.targetDir, "AGENTS.md"), "utf8"),
235
+ ).toMatch(/one law|on\(Trigger\)/i);
236
+ const readme = readFileSync(join(result.targetDir, "README.md"), "utf8");
237
+ expect(readme).toMatch(/oke dev/);
238
+ expect(readme).toMatch(new RegExp(`^# ${id}`, "m"));
239
+ expect(
240
+ readFileSync(join(result.targetDir, ".gitignore"), "utf8"),
241
+ ).toMatch(/node_modules/);
190
242
  const pkg = JSON.parse(
191
243
  readFileSync(join(result.targetDir, "package.json"), "utf8"),
192
244
  ) as { name: string; dependencies: { okengine: string } };
@@ -271,17 +323,34 @@ describe("scaffold structure", () => {
271
323
  name: "from-notes",
272
324
  source: { kind: "example", id: "notes" },
273
325
  });
274
- expect([...result.files].sort()).toEqual(expected);
326
+ expect([...result.files].sort()).toEqual(
327
+ [...expected, "AGENTS.md"].sort(),
328
+ );
275
329
  expect(result.files).toContain("src/flows/notes/index.ts");
276
330
  } finally {
277
331
  rmSync(dir, { recursive: true, force: true });
278
332
  }
279
333
  });
334
+
335
+ test("--no-agents-md skips AGENTS.md", () => {
336
+ const dir = mkdtempSync(join(tmpdir(), "create-oke-no-agents-"));
337
+ try {
338
+ const result = scaffold({
339
+ targetDir: join(dir, "hello"),
340
+ name: "no-agents",
341
+ source: { kind: "template", id: "hello" },
342
+ writeAgentsMd: false,
343
+ });
344
+ expect(result.files).not.toContain("AGENTS.md");
345
+ } finally {
346
+ rmSync(dir, { recursive: true, force: true });
347
+ }
348
+ });
280
349
  });
281
350
 
282
351
  describe("non-TTY CLI", () => {
283
352
  test("no args + non-TTY → zero prompts, exit 1", async () => {
284
- const code = await run([], { stdinIsTTY: false });
353
+ const code = await run([], { stdinIsTTY: false, runPostScaffold: false });
285
354
  expect(code).toBe(1);
286
355
  });
287
356
 
@@ -308,11 +377,12 @@ describe("non-TTY CLI", () => {
308
377
  const target = join(root, "flag-app");
309
378
  try {
310
379
  const code = await run(
311
- [target, "--template", "hello"],
312
- { stdinIsTTY: false },
380
+ [target, "--template", "hello", "--no-install"],
381
+ { stdinIsTTY: false, runPostScaffold: false },
313
382
  );
314
383
  expect(code).toBe(0);
315
384
  expect(readdirSync(target).length).toBeGreaterThan(0);
385
+ expect(existsSync(join(target, "AGENTS.md"))).toBe(true);
316
386
  const result = {
317
387
  targetDir: target,
318
388
  name: "flag-app",
@@ -323,6 +393,7 @@ describe("non-TTY CLI", () => {
323
393
  };
324
394
  expect(nextStepsText(result)).toContain("oke dev");
325
395
  expect(nextStepsText(result)).toContain("bun install");
396
+ expect(nextStepsText(result)).toContain("okengine.vercel.app");
326
397
  } finally {
327
398
  rmSync(root, { recursive: true, force: true });
328
399
  }
package/src/cli.ts CHANGED
@@ -4,14 +4,17 @@
4
4
  * ```bash
5
5
  * bunx create-oke@latest <name> [--template hello|minimal|standard|full]
6
6
  * bunx create-oke@latest <name> --from-example notes|linkly|provisions|skyport
7
+ * bunx create-oke@latest <name> --yes
7
8
  * bunx create-oke@latest # interactive when stdin is a TTY
8
9
  * ```
9
10
  */
10
11
 
11
12
  import {
12
13
  cancel,
14
+ confirm,
13
15
  intro,
14
16
  isCancel,
17
+ note,
15
18
  outro,
16
19
  select,
17
20
  spinner,
@@ -19,6 +22,8 @@ import {
19
22
  } from "@clack/prompts";
20
23
  import { basename, relative, resolve } from "node:path";
21
24
  import { existsSync, rmSync } from "node:fs";
25
+ import { agentsMdContent } from "./agents-md.ts";
26
+ import { docsUrl } from "./docs-origin.ts";
22
27
  import { scaffold, type ScaffoldResult, type ScaffoldSource } from "./scaffold.ts";
23
28
  import {
24
29
  DEFAULT_TEMPLATE,
@@ -40,6 +45,15 @@ export type CliArgs = {
40
45
  /** True when `--template` / `-t` was present on the argv. */
41
46
  readonly templateExplicit: boolean;
42
47
  readonly help: boolean;
48
+ /** Skip all prompts; use defaults. */
49
+ readonly yes: boolean;
50
+ /**
51
+ * Install after scaffold.
52
+ * `undefined` = default (interactive asks; `--yes` installs; else skip).
53
+ */
54
+ readonly install: boolean | undefined;
55
+ /** Write `AGENTS.md` (default true). */
56
+ readonly agentsMd: boolean;
43
57
  readonly targetDir: string | undefined;
44
58
  };
45
59
 
@@ -54,11 +68,15 @@ export type InteractiveAnswers =
54
68
  | {
55
69
  readonly name: string;
56
70
  readonly choice: TemplateId;
71
+ readonly installAndRun: boolean;
72
+ readonly agentsMd: boolean;
57
73
  }
58
74
  | {
59
75
  readonly name: string;
60
76
  readonly choice: typeof FROM_EXAMPLE_CHOICE;
61
77
  readonly example: ExampleId;
78
+ readonly installAndRun: boolean;
79
+ readonly agentsMd: boolean;
62
80
  };
63
81
 
64
82
  /**
@@ -68,6 +86,7 @@ export type ScaffoldCallArgs = {
68
86
  readonly name: string;
69
87
  readonly targetDir: string;
70
88
  readonly source: ScaffoldSource;
89
+ readonly agentsMd: boolean;
71
90
  };
72
91
 
73
92
  /**
@@ -81,6 +100,9 @@ export function parseArgs(argv: readonly string[]): CliArgs {
81
100
  let fromExample: ExampleId | undefined;
82
101
  let templateExplicit = false;
83
102
  let help = false;
103
+ let yes = false;
104
+ let install: boolean | undefined;
105
+ let agentsMd = true;
84
106
  let targetDir: string | undefined;
85
107
 
86
108
  for (let i = 0; i < argv.length; i++) {
@@ -89,6 +111,26 @@ export function parseArgs(argv: readonly string[]): CliArgs {
89
111
  help = true;
90
112
  continue;
91
113
  }
114
+ if (a === "--yes" || a === "-y") {
115
+ yes = true;
116
+ continue;
117
+ }
118
+ if (a === "--install") {
119
+ install = true;
120
+ continue;
121
+ }
122
+ if (a === "--no-install") {
123
+ install = false;
124
+ continue;
125
+ }
126
+ if (a === "--no-agents-md") {
127
+ agentsMd = false;
128
+ continue;
129
+ }
130
+ if (a === "--agents-md") {
131
+ agentsMd = true;
132
+ continue;
133
+ }
92
134
  if (a === "--template" || a === "-t") {
93
135
  const next = argv[++i];
94
136
  if (!next || !isTemplateId(next)) {
@@ -147,11 +189,25 @@ export function parseArgs(argv: readonly string[]): CliArgs {
147
189
  );
148
190
  }
149
191
 
192
+ if (argv.includes("--install") && argv.includes("--no-install")) {
193
+ throw new Error("create-oke: use either --install or --no-install, not both");
194
+ }
195
+
150
196
  if (name !== undefined) {
151
197
  targetDir = resolve(name);
152
198
  }
153
199
 
154
- return { name, template, fromExample, templateExplicit, help, targetDir };
200
+ return {
201
+ name,
202
+ template,
203
+ fromExample,
204
+ templateExplicit,
205
+ help,
206
+ yes,
207
+ install,
208
+ agentsMd,
209
+ targetDir,
210
+ };
155
211
  }
156
212
 
157
213
  /**
@@ -179,6 +235,8 @@ Next steps:
179
235
  cd ${formatCdPath(result.targetDir)}
180
236
  bun install
181
237
  oke dev # app :6530 · Console :6533 · MCP :6535
238
+
239
+ Docs: ${docsUrl("/docs")}
182
240
  `;
183
241
  }
184
242
 
@@ -199,8 +257,19 @@ export function helpText(): string {
199
257
  Usage:
200
258
  bunx create-oke@latest <name> [--template hello|minimal|standard|full]
201
259
  bunx create-oke@latest <name> --from-example notes|linkly|provisions|skyport
260
+ bunx create-oke@latest <name> --yes
202
261
  bunx create-oke@latest # interactive (TTY only)
203
262
 
263
+ Options:
264
+ -t, --template <id> Clean starter (default: ${DEFAULT_TEMPLATE})
265
+ --from-example <id> Teaching example (non-interactive)
266
+ -y, --yes No prompts; defaults + bun install (no oke dev)
267
+ --install Run bun install after scaffold
268
+ --no-install Skip bun install
269
+ --agents-md Write AGENTS.md (default)
270
+ --no-agents-md Skip AGENTS.md
271
+ -h, --help Show this help
272
+
204
273
  Templates (clean starters from templates/):
205
274
  ${templateLines}
206
275
 
@@ -208,16 +277,17 @@ ${templateLines}
208
277
  comments — most new projects want --template instead):
209
278
  ${exampleLines}
210
279
 
211
- No telemetry. Bun only. Interactive prompts only when stdin is a TTY and no
212
- name / --template / --from-example is given.
280
+ No telemetry. Bun only. On a TTY, a project name alone still opens the wizard
281
+ (confirm name, template, install). Non-TTY / --yes / --template /
282
+ --from-example stay fully scriptable.
213
283
  `;
214
284
  }
215
285
 
216
286
  /**
217
- * Whether the CLI should open the two-question interactive flow.
287
+ * Whether the CLI should open the interactive Clack flow.
218
288
  *
219
- * Mirrors oke / gflows: bare invocation is interactive only in a real terminal;
220
- * any explicit flag or positional name stays fully scriptable.
289
+ * TTY humans get the wizard even when a name is pre-filled. Config flags
290
+ * (`--template`, `--from-example`) or `--yes` skip prompts for CI/agents.
221
291
  *
222
292
  * @param args - Parsed args
223
293
  * @param stdinIsTTY - `process.stdin.isTTY`
@@ -228,7 +298,7 @@ export function shouldPrompt(
228
298
  ): boolean {
229
299
  if (!stdinIsTTY) return false;
230
300
  if (args.help) return false;
231
- if (args.name !== undefined) return false;
301
+ if (args.yes) return false;
232
302
  if (args.templateExplicit) return false;
233
303
  if (args.fromExample !== undefined) return false;
234
304
  return true;
@@ -261,6 +331,7 @@ export function scaffoldArgsFromCli(args: CliArgs): ScaffoldCallArgs {
261
331
  name: basename(resolve(args.name)),
262
332
  targetDir: args.targetDir,
263
333
  source: sourceFromArgs(args),
334
+ agentsMd: args.agentsMd,
264
335
  };
265
336
  }
266
337
 
@@ -282,12 +353,14 @@ export function scaffoldArgsFromAnswers(
282
353
  name,
283
354
  targetDir,
284
355
  source: { kind: "example", id: answers.example },
356
+ agentsMd: answers.agentsMd,
285
357
  };
286
358
  }
287
359
  return {
288
360
  name,
289
361
  targetDir,
290
362
  source: { kind: "template", id: answers.choice },
363
+ agentsMd: answers.agentsMd,
291
364
  };
292
365
  }
293
366
 
@@ -296,34 +369,32 @@ export function scaffoldArgsFromAnswers(
296
369
  *
297
370
  * Injectable for tests; production uses {@link askInteractiveAnswers}.
298
371
  *
299
- * @param partial - Name already known (skipped in the prompt)
372
+ * @param partial - Name already known (pre-filled in the prompt)
300
373
  */
301
374
  export type AskInteractiveFn = (
302
- partial: { readonly name?: string },
375
+ partial: { readonly name?: string; readonly agentsMd?: boolean },
303
376
  ) => Promise<InteractiveAnswers | null>;
304
377
 
305
378
  /**
306
379
  * Collect interactive answers via `@clack/prompts`.
307
380
  *
308
- * @param partial - Optional pre-filled project name
381
+ * @param partial - Optional pre-filled project name / agents-md default
309
382
  * @returns Answers, or `null` if the user cancelled
310
383
  */
311
384
  export async function askInteractiveAnswers(
312
- partial: { readonly name?: string } = {},
385
+ partial: { readonly name?: string; readonly agentsMd?: boolean } = {},
313
386
  ): Promise<InteractiveAnswers | null> {
314
- let name = partial.name;
315
- if (name === undefined) {
316
- const nameValue = await text({
317
- message: "Project name",
318
- placeholder: "my-app",
319
- validate: (value) => {
320
- if (!value?.trim()) return "Project name is required";
321
- return undefined;
322
- },
323
- });
324
- if (isCancel(nameValue)) return null;
325
- name = String(nameValue).trim();
326
- }
387
+ const nameValue = await text({
388
+ message: "Project name",
389
+ placeholder: "my-app",
390
+ initialValue: partial.name ?? "my-app",
391
+ validate: (value) => {
392
+ if (!value?.trim()) return "Project name is required";
393
+ return undefined;
394
+ },
395
+ });
396
+ if (isCancel(nameValue)) return null;
397
+ const name = String(nameValue).trim();
327
398
 
328
399
  const templateValue = await select({
329
400
  message: "Template",
@@ -343,6 +414,8 @@ export async function askInteractiveAnswers(
343
414
  });
344
415
  if (isCancel(templateValue)) return null;
345
416
 
417
+ const agentsMd = partial.agentsMd ?? true;
418
+
346
419
  if (templateValue === "__example__") {
347
420
  const exampleValue = await select({
348
421
  message: "Example",
@@ -353,21 +426,52 @@ export async function askInteractiveAnswers(
353
426
  })),
354
427
  });
355
428
  if (isCancel(exampleValue)) return null;
429
+
430
+ const installAndRunValue = await confirm({
431
+ message: "Install dependencies and start oke dev?",
432
+ initialValue: true,
433
+ });
434
+ if (isCancel(installAndRunValue)) return null;
435
+
356
436
  return {
357
437
  name,
358
438
  choice: FROM_EXAMPLE_CHOICE,
359
439
  example: exampleValue as ExampleId,
440
+ installAndRun: Boolean(installAndRunValue),
441
+ agentsMd,
360
442
  };
361
443
  }
362
444
 
363
- return { name, choice: templateValue as TemplateId };
445
+ const installAndRunValue = await confirm({
446
+ message: "Install dependencies and start oke dev?",
447
+ initialValue: true,
448
+ });
449
+ if (isCancel(installAndRunValue)) return null;
450
+
451
+ return {
452
+ name,
453
+ choice: templateValue as TemplateId,
454
+ installAndRun: Boolean(installAndRunValue),
455
+ agentsMd,
456
+ };
457
+ }
458
+
459
+ /**
460
+ * Resolve whether to run `bun install` after scaffold (non-interactive).
461
+ *
462
+ * @param args - Parsed args
463
+ */
464
+ export function shouldInstall(args: CliArgs): boolean {
465
+ if (args.install === true) return true;
466
+ if (args.install === false) return false;
467
+ return args.yes;
364
468
  }
365
469
 
366
470
  /**
367
471
  * Run the CLI.
368
472
  *
369
473
  * @param argv - Args after the binary
370
- * @param options - Test seams (stdin TTY, ask injection)
474
+ * @param options - Test seams (stdin TTY, ask injection, skip real install/dev)
371
475
  * @returns Exit code
372
476
  */
373
477
  export async function run(
@@ -375,6 +479,8 @@ export async function run(
375
479
  options: {
376
480
  readonly stdinIsTTY?: boolean | undefined;
377
481
  readonly ask?: AskInteractiveFn;
482
+ /** When false, skip spawning bun install / oke dev (tests). Default true. */
483
+ readonly runPostScaffold?: boolean;
378
484
  } = {},
379
485
  ): Promise<number> {
380
486
  let args: CliArgs;
@@ -392,34 +498,49 @@ export async function run(
392
498
 
393
499
  const stdinIsTTY = options.stdinIsTTY ?? process.stdin.isTTY;
394
500
  if (shouldPrompt(args, stdinIsTTY)) {
395
- return runInteractive(args, options.ask ?? askInteractiveAnswers);
501
+ return runInteractive(args, options.ask ?? askInteractiveAnswers, options);
396
502
  }
397
503
 
398
504
  if (args.name === undefined) {
399
505
  console.log(helpText());
400
- console.error("create-oke: missing <name>");
506
+ console.error(
507
+ "create-oke: missing <name>. Pass a name, or run in a TTY for the wizard.",
508
+ );
509
+ console.error(" Example: bunx create-oke@latest my-app --yes");
401
510
  return 1;
402
511
  }
403
512
 
513
+ if (args.yes) {
514
+ const source = sourceFromArgs(args);
515
+ console.log(
516
+ `Using defaults: ${source.kind}=${source.id} agents-md=${args.agentsMd} install=${shouldInstall(args)}`,
517
+ );
518
+ }
519
+
404
520
  return runScaffold({
405
521
  ...scaffoldArgsFromCli(args),
406
522
  interactive: false,
523
+ install: shouldInstall(args),
524
+ startDev: false,
525
+ runPostScaffold: options.runPostScaffold ?? true,
407
526
  });
408
527
  }
409
528
 
410
529
  /**
411
530
  * Interactive TTY flow — ask → map answers → scaffold.
412
531
  *
413
- * @param args - Parsed args (name may already be set if ever called that way)
532
+ * @param args - Parsed args (name may already be set)
414
533
  * @param ask - Injectable ask step
534
+ * @param options - Post-scaffold seams
415
535
  */
416
536
  async function runInteractive(
417
537
  args: CliArgs,
418
538
  ask: AskInteractiveFn,
539
+ options: { readonly runPostScaffold?: boolean },
419
540
  ): Promise<number> {
420
541
  intro("create-oke");
421
542
 
422
- const answers = await ask({ name: args.name });
543
+ const answers = await ask({ name: args.name, agentsMd: args.agentsMd });
423
544
  if (answers === null) {
424
545
  cancel("Cancelled.");
425
546
  return 1;
@@ -428,18 +549,35 @@ async function runInteractive(
428
549
  return runScaffold({
429
550
  ...scaffoldArgsFromAnswers(answers),
430
551
  interactive: true,
552
+ install: answers.installAndRun,
553
+ startDev: answers.installAndRun,
554
+ runPostScaffold: options.runPostScaffold ?? true,
431
555
  });
432
556
  }
433
557
 
434
558
  /**
435
559
  * Scaffold with optional spinner / outro, cleaning up on failure or cancel.
436
560
  *
437
- * @param options - Scaffold inputs + interactive flag
561
+ * @param options - Scaffold inputs + interactive / install flags
438
562
  */
439
563
  async function runScaffold(
440
- options: ScaffoldCallArgs & { readonly interactive: boolean },
564
+ options: ScaffoldCallArgs & {
565
+ readonly interactive: boolean;
566
+ readonly install: boolean;
567
+ readonly startDev: boolean;
568
+ readonly runPostScaffold: boolean;
569
+ },
441
570
  ): Promise<number> {
442
- const { name, targetDir, source, interactive } = options;
571
+ const {
572
+ name,
573
+ targetDir,
574
+ source,
575
+ agentsMd,
576
+ interactive,
577
+ install,
578
+ startDev,
579
+ runPostScaffold,
580
+ } = options;
443
581
  const existed = existsSync(targetDir);
444
582
  let spun: ReturnType<typeof spinner> | undefined;
445
583
 
@@ -462,14 +600,53 @@ async function runScaffold(
462
600
  spun = spinner();
463
601
  spun.start("Scaffolding…");
464
602
  }
465
- const result = scaffold({ targetDir, name, source });
603
+ const result = scaffold({
604
+ targetDir,
605
+ name,
606
+ source,
607
+ writeAgentsMd: agentsMd,
608
+ });
466
609
  if (spun) spun.stop("Scaffolded.");
610
+
611
+ if (runPostScaffold && install) {
612
+ const installSpun = interactive ? spinner() : undefined;
613
+ installSpun?.start("Installing dependencies…");
614
+ const installOk = await runCommand(["bun", "install"], targetDir);
615
+ if (!installOk) {
616
+ installSpun?.stop("Install failed.");
617
+ cleanup();
618
+ console.error("create-oke: bun install failed");
619
+ return 1;
620
+ }
621
+ installSpun?.stop("Installed.");
622
+ }
623
+
467
624
  const message = nextStepsText(result);
468
625
  if (interactive) {
626
+ note(
627
+ [
628
+ `App http://127.0.0.1:6530`,
629
+ `Console http://127.0.0.1:6533`,
630
+ `MCP http://127.0.0.1:6535`,
631
+ `Docs ${docsUrl("/docs")}`,
632
+ agentsMd ? `Agents AGENTS.md` : undefined,
633
+ ]
634
+ .filter(Boolean)
635
+ .join("\n"),
636
+ "Ports",
637
+ );
469
638
  outro(message.trim());
470
639
  } else {
471
640
  console.log(message);
472
641
  }
642
+
643
+ if (runPostScaffold && startDev) {
644
+ const ok = await runCommand(["bun", "run", "dev"], targetDir, {
645
+ inherit: true,
646
+ });
647
+ return ok ? 0 : 1;
648
+ }
649
+
473
650
  return 0;
474
651
  } catch (e) {
475
652
  spun?.stop("Failed.");
@@ -480,3 +657,32 @@ async function runScaffold(
480
657
  process.off("SIGINT", onSigInt);
481
658
  }
482
659
  }
660
+
661
+ /**
662
+ * Spawn a command in `cwd`.
663
+ *
664
+ * @param cmd - Argv
665
+ * @param cwd - Working directory
666
+ * @param options - Inherit stdio for long-running processes
667
+ */
668
+ async function runCommand(
669
+ cmd: readonly string[],
670
+ cwd: string,
671
+ options: { readonly inherit?: boolean } = {},
672
+ ): Promise<boolean> {
673
+ const proc = Bun.spawn([...cmd], {
674
+ cwd,
675
+ stdout: options.inherit ? "inherit" : "pipe",
676
+ stderr: options.inherit ? "inherit" : "pipe",
677
+ stdin: options.inherit ? "inherit" : undefined,
678
+ });
679
+ const code = await proc.exited;
680
+ if (code !== 0 && !options.inherit) {
681
+ const err = await new Response(proc.stderr).text();
682
+ if (err.trim()) console.error(err);
683
+ }
684
+ return code === 0;
685
+ }
686
+
687
+ /** Re-export for tests that assert agents content shape. */
688
+ export { agentsMdContent };
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Canonical public docs origin for scaffolded AGENTS.md and next-steps.
3
+ *
4
+ * Keep in lockstep with `src/docs-origin.ts` in the okengine package.
5
+ * Flip both when the custom domain is attached: `https://oke.omqkhafi.dev`.
6
+ */
7
+
8
+ /** Live docs site (Vercel). Planned cutover: `https://oke.omqkhafi.dev`. */
9
+ export const DOCS_ORIGIN = "https://okengine.vercel.app" as const;
10
+
11
+ /**
12
+ * Absolute docs URL for a path under the handbook / site.
13
+ *
14
+ * @param path - Path beginning with `/` (e.g. `/docs`)
15
+ */
16
+ export function docsUrl(path: string): string {
17
+ const normalized = path.startsWith("/") ? path : `/${path}`;
18
+ return `${DOCS_ORIGIN}${normalized}`;
19
+ }
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  *
5
5
  * ```bash
6
6
  * bunx create-oke@latest my-app
7
+ * bunx create-oke@latest my-app --yes
7
8
  * bunx create-oke@latest my-app --template hello
8
9
  * bunx create-oke@latest my-app --from-example notes
9
10
  * ```
package/src/scaffold.ts CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  type ExampleId,
21
21
  type TemplateId,
22
22
  } from "./templates.ts";
23
+ import { agentsMdContent } from "./agents-md.ts";
23
24
  import {
24
25
  sanitizeProjectName,
25
26
  shouldSkipTemplatePath,
@@ -41,6 +42,8 @@ export type ScaffoldOptions = {
41
42
  readonly name: string;
42
43
  /** Template or teaching-example source. */
43
44
  readonly source: ScaffoldSource;
45
+ /** Write root `AGENTS.md` (default true). */
46
+ readonly writeAgentsMd?: boolean;
44
47
  };
45
48
 
46
49
  /** Result of a successful scaffold. */
@@ -98,6 +101,12 @@ export function scaffold(options: ScaffoldOptions): ScaffoldResult {
98
101
  const nextPkg = transformPackageJson(sourcePkg, name, okengineDependency);
99
102
  writeFileSync(pkgPath, `${JSON.stringify(nextPkg, null, 2)}\n`, "utf8");
100
103
 
104
+ if (options.writeAgentsMd !== false) {
105
+ const agentsPath = join(targetDir, "AGENTS.md");
106
+ writeFileSync(agentsPath, agentsMdContent(name), "utf8");
107
+ if (!written.includes("AGENTS.md")) written.push("AGENTS.md");
108
+ }
109
+
101
110
  written.sort();
102
111
  return {
103
112
  targetDir,
package/src/transform.ts CHANGED
@@ -62,6 +62,8 @@ export function transformPackageJson(
62
62
  return {
63
63
  ...source,
64
64
  name: projectName,
65
+ // Starter apps always begin at 0.0.1 — never the framework lockstep version.
66
+ version: "0.0.1",
65
67
  dependencies,
66
68
  };
67
69
  }
@@ -0,0 +1,49 @@
1
+ # full
2
+
3
+ **Every element present and wired** — no business logic. Use when you want the
4
+ complete surface ready to fill in.
5
+
6
+ ## Run
7
+
8
+ ```bash
9
+ bun install
10
+ oke dev # app :6530 · Console :6533 · MCP :6535
11
+ ```
12
+
13
+ | Port | Try |
14
+ |---|---|
15
+ | `:6530` | `GET /` · `GET /health` |
16
+ | `:6533` | Console |
17
+ | `:6535` | MCP |
18
+
19
+ ## What’s in this template
20
+
21
+ ```text
22
+ full/
23
+ ├── oke.config.ts
24
+ ├── src/
25
+ │ ├── app.ts # gates, secrets, signals, stores, channel, ai
26
+ │ ├── core.ts / schema.ts # Store
27
+ │ ├── gates.ts # open gate (used on HTTP)
28
+ │ ├── vault.ts # appSecret
29
+ │ ├── channels.ts # welcome template
30
+ │ ├── ai.ts # stub model + echo prompt
31
+ │ ├── locales/en.ts · ar.ts
32
+ │ └── flows/main/
33
+ │ ├── index.ts # GET / · GET /health · every("1d")
34
+ │ ├── shapes.ts
35
+ │ └── signals.ts # pinged
36
+ └── tests/full.test.ts
37
+ ```
38
+
39
+ - **Elements wired:** Flow · Store · Signal · Clock (`every`) · Gate · Vault · Channel · AI
40
+ - **Unit:** `main`
41
+ - **App `$options`:** `gates`, `secrets`, `signals`, `stores`, `channel`, `ai`
42
+ - **Flows:** `root`, `health` (emits `pinged`), plus a trivial daily cron
43
+
44
+ Replace stubs with real models, prompts, channels, and domain flows. Prefer
45
+ `standard` if you do not need AI / clock / vault on day one.
46
+
47
+ ## Agent contract
48
+
49
+ See [`AGENTS.md`](./AGENTS.md). Handbook: [okengine.vercel.app/docs](https://okengine.vercel.app/docs)
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oke/template-full",
3
- "version": "0.2.1",
3
+ "version": "0.0.1",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "dependencies": {
@@ -1,8 +1,26 @@
1
1
  import { on, flow, http, every } from "okengine";
2
+ import { z } from "zod";
2
3
  import { open } from "../../gates";
3
4
  import { Health } from "./shapes";
4
5
  import { pinged } from "./signals";
5
6
 
7
+ /** First-run welcome — visit :6530/ after `oke dev`. */
8
+ export const root = on(
9
+ http.get("/").gate(open),
10
+ flow({
11
+ out: z.object({
12
+ ok: z.literal(true),
13
+ console: z.string(),
14
+ try: z.string(),
15
+ }),
16
+ do: () => ({
17
+ ok: true as const,
18
+ console: "http://127.0.0.1:6533",
19
+ try: "/health",
20
+ }),
21
+ }),
22
+ );
23
+
6
24
  export const health = on(
7
25
  http.get("/health").gate(open),
8
26
  flow({
@@ -1,13 +1,38 @@
1
1
  # hello
2
2
 
3
- Fastest possible okengine scaffold — one flow, no Store. This is an "it works"
4
- starting point, not a lesson.
3
+ Fastest possible “it works” — one unit, two HTTP flows, **no Store**.
4
+
5
+ ## Run
5
6
 
6
7
  ```bash
7
8
  bun install
8
9
  oke dev # app :6530 · Console :6533 · MCP :6535
9
10
  ```
10
11
 
11
- Try `GET /hello`. For the teaching path (comment-heavy apps with real domains),
12
- see the `examples/` apps and `docs/spec/four-applications.md` in the okengine
13
- repo or scaffold with `bunx create-oke@latest <name> --from-example notes`.
12
+ | Port | Try |
13
+ |---|---|
14
+ | `:6530` | `GET /` · `GET /hello` |
15
+ | `:6533` | Console |
16
+ | `:6535` | MCP |
17
+
18
+ ## What’s in this template
19
+
20
+ ```text
21
+ hello/
22
+ ├── oke.config.ts
23
+ ├── src/
24
+ │ ├── app.ts # oke({ name: "hello" }).adopt({ hello })
25
+ │ └── flows/hello/index.ts # GET / · GET /hello
26
+ └── tests/hello.test.ts
27
+ ```
28
+
29
+ - **Elements:** Flow only (no `store`, `gate`, `vault`, `channel`, or `ai`)
30
+ - **Unit:** `hello`
31
+ - **Flows:** `root` (`GET /`), `hello` (`GET /hello` → `{ message: "ok" }`)
32
+
33
+ Replace `/hello` with your first real route. When you need persistence, move up to
34
+ `minimal` or `standard` — or keep growing this tree.
35
+
36
+ ## Agent contract
37
+
38
+ See [`AGENTS.md`](./AGENTS.md). Handbook: [okengine.vercel.app/docs](https://okengine.vercel.app/docs)
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oke/template-hello",
3
- "version": "0.2.1",
3
+ "version": "0.0.1",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "dependencies": {
@@ -1,6 +1,23 @@
1
1
  import { on, flow, http } from "okengine";
2
2
  import { z } from "zod";
3
3
 
4
+ /** First-run welcome — visit :6530/ after `oke dev`. */
5
+ export const root = on(
6
+ http.get("/"),
7
+ flow({
8
+ out: z.object({
9
+ ok: z.literal(true),
10
+ console: z.string(),
11
+ try: z.string(),
12
+ }),
13
+ do: () => ({
14
+ ok: true as const,
15
+ console: "http://127.0.0.1:6533",
16
+ try: "/hello",
17
+ }),
18
+ }),
19
+ );
20
+
4
21
  export const hello = on(
5
22
  http.get("/hello"),
6
23
  flow({
@@ -0,0 +1,41 @@
1
+ # minimal
2
+
3
+ Smallest shape you’d actually ship — **Store + HTTP flows** over a single table.
4
+
5
+ ## Run
6
+
7
+ ```bash
8
+ bun install
9
+ oke dev # app :6530 · Console :6533 · MCP :6535
10
+ ```
11
+
12
+ | Port | Try |
13
+ |---|---|
14
+ | `:6530` | `GET /` · `GET /entries` · `POST /entries` |
15
+ | `:6533` | Console |
16
+ | `:6535` | MCP |
17
+
18
+ ## What’s in this template
19
+
20
+ ```text
21
+ minimal/
22
+ ├── oke.config.ts
23
+ ├── src/
24
+ │ ├── app.ts # adopt({ main }), stores: [db]
25
+ │ ├── core.ts # store.sql("…")
26
+ │ ├── schema.ts # entries table (Drizzle)
27
+ │ └── flows/main/index.ts # GET / · GET|POST /entries
28
+ └── tests/minimal.test.ts
29
+ ```
30
+
31
+ - **Elements:** Flow · Store (`sql`)
32
+ - **Unit:** `main`
33
+ - **Flows:** `root`, `list` (`GET /entries`), `create` (`POST /entries` with `{ body }`)
34
+ - **Schema:** `entries` — `id`, `body`, `createdAt`
35
+
36
+ Swap `entries` for your domain table and grow flows beside it. Need gates,
37
+ channels, or locales? Use `standard`.
38
+
39
+ ## Agent contract
40
+
41
+ See [`AGENTS.md`](./AGENTS.md). Handbook: [okengine.vercel.app/docs](https://okengine.vercel.app/docs)
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oke/template-minimal",
3
- "version": "0.2.1",
3
+ "version": "0.0.1",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "dependencies": {
@@ -11,6 +11,23 @@ const Entry = z.object({
11
11
  createdAt: z.number(),
12
12
  });
13
13
 
14
+ /** First-run welcome — visit :6530/ after `oke dev`. */
15
+ export const root = on(
16
+ http.get("/"),
17
+ flow({
18
+ out: z.object({
19
+ ok: z.literal(true),
20
+ console: z.string(),
21
+ try: z.string(),
22
+ }),
23
+ do: () => ({
24
+ ok: true as const,
25
+ console: "http://127.0.0.1:6533",
26
+ try: "GET /entries",
27
+ }),
28
+ }),
29
+ );
30
+
14
31
  export const create = on(
15
32
  http.post("/entries"),
16
33
  flow({
@@ -0,0 +1,47 @@
1
+ # standard
2
+
3
+ Full recommended file layout with **empty scaffolding** — structure without
4
+ business logic. Default `create-oke` template.
5
+
6
+ ## Run
7
+
8
+ ```bash
9
+ bun install
10
+ oke dev # app :6530 · Console :6533 · MCP :6535
11
+ ```
12
+
13
+ | Port | Try |
14
+ |---|---|
15
+ | `:6530` | `GET /` · `GET /health` |
16
+ | `:6533` | Console |
17
+ | `:6535` | MCP |
18
+
19
+ ## What’s in this template
20
+
21
+ ```text
22
+ standard/
23
+ ├── oke.config.ts
24
+ ├── src/
25
+ │ ├── app.ts # adopt({ main }); wires stores + side modules
26
+ │ ├── core.ts / schema.ts # Store (replace the placeholder table)
27
+ │ ├── gates.ts # Gate stubs
28
+ │ ├── vault.ts # Vault stubs
29
+ │ ├── channels.ts # Channel stubs
30
+ │ ├── locales/en.ts · ar.ts # i18n stubs
31
+ │ └── flows/main/
32
+ │ ├── index.ts # GET / · GET /health
33
+ │ ├── shapes.ts # contracts live next to flows
34
+ │ └── signals.ts # Signal stubs
35
+ └── tests/standard.test.ts
36
+ ```
37
+
38
+ - **Elements stubbed:** Flow · Store · Signal · Gate · Vault · Channel (+ locales)
39
+ - **Unit:** `main`
40
+ - **Flows to replace:** `root`, `health` — delete or rename once your domain exists
41
+
42
+ Fill `schema.ts`, then add real flows under `src/flows/<unit>/`. Prefer this
43
+ layout for new products; use `hello` / `minimal` only when you want less surface.
44
+
45
+ ## Agent contract
46
+
47
+ See [`AGENTS.md`](./AGENTS.md). Handbook: [okengine.vercel.app/docs](https://okengine.vercel.app/docs)
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oke/template-standard",
3
- "version": "0.2.1",
3
+ "version": "0.0.1",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "dependencies": {
@@ -4,6 +4,23 @@ import { z } from "zod";
4
4
  import "./shapes";
5
5
  import "./signals";
6
6
 
7
+ /** First-run welcome — visit :6530/ after `oke dev`. */
8
+ export const root = on(
9
+ http.get("/"),
10
+ flow({
11
+ out: z.object({
12
+ ok: z.literal(true),
13
+ console: z.string(),
14
+ try: z.string(),
15
+ }),
16
+ do: () => ({
17
+ ok: true as const,
18
+ console: "http://127.0.0.1:6533",
19
+ try: "/health",
20
+ }),
21
+ }),
22
+ );
23
+
7
24
  /** Replace with your unit's flows. */
8
25
  export const health = on(
9
26
  http.get("/health"),