create-oke 0.1.6 → 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.
Files changed (122) hide show
  1. package/README.md +26 -6
  2. package/{templates → examples}/linkly/package.json +1 -1
  3. package/{templates → examples}/notes/package.json +1 -1
  4. package/{templates → examples}/provisions/package.json +1 -1
  5. package/{templates → examples}/skyport/package.json +1 -1
  6. package/package.json +6 -2
  7. package/src/agents-md.ts +99 -0
  8. package/src/cli.test.ts +318 -19
  9. package/src/cli.ts +561 -29
  10. package/src/docs-origin.ts +19 -0
  11. package/src/index.ts +3 -1
  12. package/src/scaffold.ts +59 -32
  13. package/src/sync-templates.ts +28 -7
  14. package/src/templates.ts +85 -12
  15. package/src/transform.ts +5 -2
  16. package/templates/full/README.md +49 -0
  17. package/templates/full/oke.config.ts +23 -0
  18. package/templates/full/package.json +15 -0
  19. package/templates/full/src/ai.ts +11 -0
  20. package/templates/full/src/app.ts +34 -0
  21. package/templates/full/src/channels.ts +9 -0
  22. package/templates/full/src/core.ts +4 -0
  23. package/templates/full/src/flows/main/index.ts +45 -0
  24. package/templates/full/src/flows/main/shapes.ts +3 -0
  25. package/templates/full/src/flows/main/signals.ts +8 -0
  26. package/templates/full/src/gates.ts +4 -0
  27. package/templates/full/src/locales/ar.ts +3 -0
  28. package/templates/full/src/locales/en.ts +3 -0
  29. package/templates/full/src/schema.ts +8 -0
  30. package/templates/full/src/vault.ts +9 -0
  31. package/templates/full/tests/full.test.ts +11 -0
  32. package/templates/hello/README.md +38 -0
  33. package/templates/hello/oke.config.ts +3 -0
  34. package/templates/hello/package.json +14 -0
  35. package/templates/hello/src/app.ts +10 -0
  36. package/templates/hello/src/flows/hello/index.ts +27 -0
  37. package/templates/hello/tests/hello.test.ts +10 -0
  38. package/templates/minimal/README.md +41 -0
  39. package/templates/minimal/oke.config.ts +7 -0
  40. package/templates/minimal/package.json +15 -0
  41. package/templates/minimal/src/app.ts +13 -0
  42. package/templates/minimal/src/core.ts +4 -0
  43. package/templates/minimal/src/flows/main/index.ts +49 -0
  44. package/templates/minimal/src/schema.ts +8 -0
  45. package/templates/minimal/tests/minimal.test.ts +13 -0
  46. package/templates/standard/README.md +47 -0
  47. package/templates/standard/oke.config.ts +19 -0
  48. package/templates/standard/package.json +15 -0
  49. package/templates/standard/src/app.ts +18 -0
  50. package/templates/standard/src/channels.ts +4 -0
  51. package/templates/standard/src/core.ts +4 -0
  52. package/templates/standard/src/flows/main/index.ts +31 -0
  53. package/templates/standard/src/flows/main/shapes.ts +2 -0
  54. package/templates/standard/src/flows/main/signals.ts +4 -0
  55. package/templates/standard/src/gates.ts +4 -0
  56. package/templates/standard/src/locales/ar.ts +2 -0
  57. package/templates/standard/src/locales/en.ts +2 -0
  58. package/templates/standard/src/schema.ts +9 -0
  59. package/templates/standard/src/vault.ts +4 -0
  60. package/templates/standard/tests/standard.test.ts +10 -0
  61. /package/{templates → examples}/linkly/oke.config.ts +0 -0
  62. /package/{templates → examples}/linkly/src/app.ts +0 -0
  63. /package/{templates → examples}/linkly/src/core.ts +0 -0
  64. /package/{templates → examples}/linkly/src/flows/analytics/index.ts +0 -0
  65. /package/{templates → examples}/linkly/src/flows/links/index.ts +0 -0
  66. /package/{templates → examples}/linkly/src/flows/links/shapes.ts +0 -0
  67. /package/{templates → examples}/linkly/src/flows/links/signals.ts +0 -0
  68. /package/{templates → examples}/linkly/src/gates.ts +0 -0
  69. /package/{templates → examples}/linkly/src/schema.ts +0 -0
  70. /package/{templates → examples}/linkly/tests/linkly.test.ts +0 -0
  71. /package/{templates → examples}/notes/oke.config.ts +0 -0
  72. /package/{templates → examples}/notes/src/app.ts +0 -0
  73. /package/{templates → examples}/notes/src/core.ts +0 -0
  74. /package/{templates → examples}/notes/src/flows/notes/index.ts +0 -0
  75. /package/{templates → examples}/notes/src/schema.ts +0 -0
  76. /package/{templates → examples}/notes/tests/notes.test.ts +0 -0
  77. /package/{templates → examples}/provisions/oke.config.ts +0 -0
  78. /package/{templates → examples}/provisions/src/app.ts +0 -0
  79. /package/{templates → examples}/provisions/src/channels.ts +0 -0
  80. /package/{templates → examples}/provisions/src/core.ts +0 -0
  81. /package/{templates → examples}/provisions/src/flows/notifications/index.ts +0 -0
  82. /package/{templates → examples}/provisions/src/flows/orders/index.ts +0 -0
  83. /package/{templates → examples}/provisions/src/flows/orders/shapes.ts +0 -0
  84. /package/{templates → examples}/provisions/src/flows/orders/signals.ts +0 -0
  85. /package/{templates → examples}/provisions/src/flows/payments/index.ts +0 -0
  86. /package/{templates → examples}/provisions/src/flows/payments/shapes.ts +0 -0
  87. /package/{templates → examples}/provisions/src/flows/payments/stripe.ts +0 -0
  88. /package/{templates → examples}/provisions/src/gates.ts +0 -0
  89. /package/{templates → examples}/provisions/src/locales/ar.ts +0 -0
  90. /package/{templates → examples}/provisions/src/locales/en.ts +0 -0
  91. /package/{templates → examples}/provisions/src/plugins/audit-schema.ts +0 -0
  92. /package/{templates → examples}/provisions/src/plugins/audit.ts +0 -0
  93. /package/{templates → examples}/provisions/src/schema.ts +0 -0
  94. /package/{templates → examples}/provisions/src/vault.ts +0 -0
  95. /package/{templates → examples}/provisions/tests/orders.test.ts +0 -0
  96. /package/{templates → examples}/skyport/evals/triage.jsonl +0 -0
  97. /package/{templates → examples}/skyport/oke.config.ts +0 -0
  98. /package/{templates → examples}/skyport/oke.images.lock +0 -0
  99. /package/{templates → examples}/skyport/src/ai.ts +0 -0
  100. /package/{templates → examples}/skyport/src/app.ts +0 -0
  101. /package/{templates → examples}/skyport/src/channels.ts +0 -0
  102. /package/{templates → examples}/skyport/src/core.ts +0 -0
  103. /package/{templates → examples}/skyport/src/flows/bookings/index.ts +0 -0
  104. /package/{templates → examples}/skyport/src/flows/bookings/shapes.ts +0 -0
  105. /package/{templates → examples}/skyport/src/flows/bookings/signals.ts +0 -0
  106. /package/{templates → examples}/skyport/src/flows/notifications/index.ts +0 -0
  107. /package/{templates → examples}/skyport/src/flows/payments/index.ts +0 -0
  108. /package/{templates → examples}/skyport/src/flows/payments/shapes.ts +0 -0
  109. /package/{templates → examples}/skyport/src/flows/support/index.ts +0 -0
  110. /package/{templates → examples}/skyport/src/flows/users/elements.ts +0 -0
  111. /package/{templates → examples}/skyport/src/flows/users/index.ts +0 -0
  112. /package/{templates → examples}/skyport/src/flows/users/shapes.ts +0 -0
  113. /package/{templates → examples}/skyport/src/gates.ts +0 -0
  114. /package/{templates → examples}/skyport/src/journeys.ts +0 -0
  115. /package/{templates → examples}/skyport/src/locales/ar.ts +0 -0
  116. /package/{templates → examples}/skyport/src/locales/en.ts +0 -0
  117. /package/{templates → examples}/skyport/src/plugins/audit-schema.ts +0 -0
  118. /package/{templates → examples}/skyport/src/plugins/audit.ts +0 -0
  119. /package/{templates → examples}/skyport/src/plugins/panel.tsx +0 -0
  120. /package/{templates → examples}/skyport/src/schema.ts +0 -0
  121. /package/{templates → examples}/skyport/src/vault.ts +0 -0
  122. /package/{templates → examples}/skyport/tests/support.test.ts +0 -0
package/README.md CHANGED
@@ -8,21 +8,41 @@ 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 |
14
18
  | `:6533` | Console |
15
19
  | `:6535` | MCP |
16
20
 
17
- Default template is **Notes** (same tree as [`examples/notes`](https://github.com/omqkhafi/okengine/tree/main/examples/notes)). Pick another with `--template`:
21
+ Default template is **standard** (full recommended file layout, empty scaffolding).
22
+ Pick another with `--template`, or start from a teaching example with `--from-example`:
18
23
 
19
24
  ```bash
20
- bunx create-oke@latest my-links --template linkly
21
- bunx create-oke@latest my-shop --template provisions
22
- bunx create-oke@latest my-sky --template skyport
25
+ bunx create-oke@latest my-app --template hello
26
+ bunx create-oke@latest my-app --template minimal
27
+ bunx create-oke@latest my-app --template full
28
+ bunx create-oke@latest my-app --yes
29
+ bunx create-oke@latest my-notes --from-example notes
23
30
  ```
24
31
 
25
- | Template | Adds |
32
+ | Template | Purpose |
33
+ |---|---|
34
+ | `hello` | Fastest possible "it works" — one flow, no Store |
35
+ | `minimal` | Smallest shape you'd actually ship — Store + 1–2 flows |
36
+ | `standard` | Full recommended file layout, empty scaffolding (default) |
37
+ | `full` | Every element present and wired, no business logic |
38
+
39
+ Every scaffold writes an `AGENTS.md` so coding agents know the OKE contract
40
+ (`--no-agents-md` to skip).
41
+
42
+ `--from-example` copies a teaching example (including its business logic and
43
+ comments) — most new projects want `--template` instead.
44
+
45
+ | Example | Adds |
26
46
  |---|---|
27
47
  | `notes` | `oke` · `flow` · `http` · `store.sql` · typed client |
28
48
  | `linkly` | `signal` · `clock` · `gate` |
@@ -36,4 +56,4 @@ bunx create-oke@latest my-sky --template skyport
36
56
  | npm (CLI) | [`create-oke`](https://www.npmjs.com/package/create-oke) — use this with `bunx` |
37
57
  | JSR | [`@omqkhafi/create-oke`](https://jsr.io/@omqkhafi/create-oke) — same sources; prefer npm for the binary |
38
58
 
39
- 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.1.6",
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.1.6",
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.1.6",
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.1.6",
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.1.6",
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",
@@ -18,7 +18,8 @@
18
18
  },
19
19
  "files": [
20
20
  "src",
21
- "templates"
21
+ "templates",
22
+ "examples"
22
23
  ],
23
24
  "scripts": {
24
25
  "prepack": "bun ./src/sync-templates.ts",
@@ -29,6 +30,9 @@
29
30
  "engines": {
30
31
  "bun": ">=1.3"
31
32
  },
33
+ "dependencies": {
34
+ "@clack/prompts": "^1.7.0"
35
+ },
32
36
  "devDependencies": {
33
37
  "@types/bun": "latest",
34
38
  "typescript": "^7.0.2"
@@ -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
@@ -1,14 +1,38 @@
1
1
  /**
2
- * Unit tests for create-oke argument parsing and transforms.
2
+ * Unit tests for create-oke argument parsing, transforms, and non-TTY behavior.
3
3
  */
4
4
 
5
5
  import { describe, expect, test } from "bun:test";
6
- import { mkdtempSync, readFileSync, 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
- import { formatCdPath, parseArgs } from "./cli.ts";
15
+ import {
16
+ FROM_EXAMPLE_CHOICE,
17
+ formatCdPath,
18
+ nextStepsText,
19
+ parseArgs,
20
+ run,
21
+ scaffoldArgsFromAnswers,
22
+ scaffoldArgsFromCli,
23
+ shouldPrompt,
24
+ sourceFromArgs,
25
+ type InteractiveAnswers,
26
+ } from "./cli.ts";
10
27
  import { listTemplateFiles, scaffold } from "./scaffold.ts";
11
- import { resolveTemplateDir, TEMPLATES } from "./templates.ts";
28
+ import {
29
+ DEFAULT_TEMPLATE,
30
+ EXAMPLES,
31
+ TEMPLATES,
32
+ packageRoot,
33
+ resolveExampleDir,
34
+ resolveTemplateDir,
35
+ } from "./templates.ts";
12
36
  import {
13
37
  sanitizeProjectName,
14
38
  shouldSkipTemplatePath,
@@ -16,22 +40,130 @@ import {
16
40
  } from "./transform.ts";
17
41
 
18
42
  describe("parseArgs", () => {
19
- test("defaults template to notes", () => {
43
+ test("defaults template to standard", () => {
20
44
  const a = parseArgs(["my-app"]);
21
45
  expect(a.name).toBe("my-app");
22
- expect(a.template).toBe("notes");
46
+ expect(a.template).toBe("standard");
47
+ expect(a.templateExplicit).toBe(false);
48
+ expect(a.fromExample).toBeUndefined();
23
49
  });
24
50
 
25
51
  test("accepts --template and -t", () => {
26
- expect(parseArgs(["x", "--template", "linkly"]).template).toBe("linkly");
27
- expect(parseArgs(["x", "-t", "skyport"]).template).toBe("skyport");
28
- expect(parseArgs(["x", "--template=provisions"]).template).toBe(
29
- "provisions",
52
+ expect(parseArgs(["x", "--template", "hello"]).template).toBe("hello");
53
+ expect(parseArgs(["x", "-t", "full"]).template).toBe("full");
54
+ expect(parseArgs(["x", "--template=minimal"]).template).toBe("minimal");
55
+ expect(parseArgs(["x", "--template", "hello"]).templateExplicit).toBe(true);
56
+ });
57
+
58
+ test("accepts --from-example", () => {
59
+ expect(parseArgs(["x", "--from-example", "notes"]).fromExample).toBe(
60
+ "notes",
61
+ );
62
+ expect(parseArgs(["x", "--from-example=skyport"]).fromExample).toBe(
63
+ "skyport",
30
64
  );
31
65
  });
32
66
 
33
- test("rejects unknown template", () => {
67
+ test("rejects unknown template / example / both", () => {
34
68
  expect(() => parseArgs(["x", "--template", "nope"])).toThrow(/template/);
69
+ expect(() => parseArgs(["x", "--from-example", "nope"])).toThrow(/example/);
70
+ expect(() =>
71
+ parseArgs(["x", "--template", "hello", "--from-example", "notes"]),
72
+ ).toThrow(/either/);
73
+ });
74
+ });
75
+
76
+ describe("shouldPrompt", () => {
77
+ test("TTY + no args → interactive", () => {
78
+ expect(shouldPrompt(parseArgs([]), true)).toBe(true);
79
+ });
80
+
81
+ test("TTY + name alone → still 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", () => {
86
+ expect(shouldPrompt(parseArgs([]), false)).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
+ );
92
+ expect(shouldPrompt(parseArgs(["--template", "hello"]), true)).toBe(false);
93
+ expect(shouldPrompt(parseArgs(["--from-example", "notes"]), true)).toBe(
94
+ false,
95
+ );
96
+ expect(shouldPrompt(parseArgs(["--help"]), true)).toBe(false);
97
+ });
98
+ });
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
+
117
+ describe("sourceFromArgs", () => {
118
+ test("prefers example when set", () => {
119
+ expect(sourceFromArgs(parseArgs(["x", "--from-example", "linkly"]))).toEqual(
120
+ { kind: "example", id: "linkly" },
121
+ );
122
+ expect(sourceFromArgs(parseArgs(["x"]))).toEqual({
123
+ kind: "template",
124
+ id: DEFAULT_TEMPLATE,
125
+ });
126
+ });
127
+ });
128
+
129
+ describe("scaffoldArgsFromAnswers ≡ flag-driven", () => {
130
+ test("each --template path matches interactive choice", () => {
131
+ for (const id of TEMPLATES) {
132
+ const answers: InteractiveAnswers = {
133
+ name: "x",
134
+ choice: id,
135
+ installAndRun: false,
136
+ agentsMd: true,
137
+ };
138
+ const fromAnswers = scaffoldArgsFromAnswers(answers);
139
+ const fromFlags = scaffoldArgsFromCli(
140
+ parseArgs(["x", "--template", id]),
141
+ );
142
+ expect(fromAnswers).toEqual(fromFlags);
143
+ expect(fromAnswers.source).toEqual({ kind: "template", id });
144
+ }
145
+ });
146
+
147
+ test("each --from-example path matches interactive from-example choice", () => {
148
+ for (const id of EXAMPLES) {
149
+ const answers: InteractiveAnswers = {
150
+ name: "x",
151
+ choice: FROM_EXAMPLE_CHOICE,
152
+ example: id,
153
+ installAndRun: false,
154
+ agentsMd: true,
155
+ };
156
+ const fromAnswers = scaffoldArgsFromAnswers(answers);
157
+ const fromFlags = scaffoldArgsFromCli(
158
+ parseArgs(["x", "--from-example", id]),
159
+ );
160
+ expect(fromAnswers).toEqual(fromFlags);
161
+ expect(fromAnswers.source).toEqual({ kind: "example", id });
162
+ }
163
+ });
164
+
165
+ test("8 paths cover every template and every example", () => {
166
+ expect(TEMPLATES.length + EXAMPLES.length).toBe(8);
35
167
  });
36
168
  });
37
169
 
@@ -39,7 +171,7 @@ describe("transformPackageJson", () => {
39
171
  test("rewrites name and okengine file:../.. to installable ref", () => {
40
172
  const next = transformPackageJson(
41
173
  {
42
- name: "@oke/example-notes",
174
+ name: "@oke/template-hello",
43
175
  private: true,
44
176
  dependencies: {
45
177
  okengine: "file:../..",
@@ -50,6 +182,7 @@ describe("transformPackageJson", () => {
50
182
  "0.0.26",
51
183
  );
52
184
  expect(next.name).toBe("my-app");
185
+ expect(next.version).toBe("0.0.1");
53
186
  expect(next.dependencies?.["okengine"]).toBe("0.0.26");
54
187
  expect(next.dependencies?.["okengine"]).not.toBe("file:../..");
55
188
  expect(next.dependencies?.["zod"]).toBe("^4.4.3");
@@ -81,7 +214,7 @@ describe("formatCdPath", () => {
81
214
  });
82
215
 
83
216
  describe("scaffold structure", () => {
84
- test("each template produces the example tree (minus skips)", () => {
217
+ test("each clean template produces its source tree (minus skips)", () => {
85
218
  for (const id of TEMPLATES) {
86
219
  const dir = mkdtempSync(join(tmpdir(), `create-oke-${id}-`));
87
220
  try {
@@ -90,21 +223,187 @@ describe("scaffold structure", () => {
90
223
  const result = scaffold({
91
224
  targetDir: join(dir, id),
92
225
  name: `app-${id}`,
93
- template: id,
226
+ source: { kind: "template", id },
94
227
  });
95
- 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/);
96
242
  const pkg = JSON.parse(
97
243
  readFileSync(join(result.targetDir, "package.json"), "utf8"),
98
244
  ) as { name: string; dependencies: { okengine: string } };
99
245
  expect(pkg.name).toBe(`app-${id}`);
100
246
  expect(pkg.dependencies.okengine).not.toBe("file:../..");
101
- expect(
102
- pkg.dependencies.okengine.startsWith("file:") ||
103
- /^\d+\.\d+\.\d+/.test(pkg.dependencies.okengine),
104
- ).toBe(true);
105
247
  } finally {
106
248
  rmSync(dir, { recursive: true, force: true });
107
249
  }
108
250
  }
109
251
  });
252
+
253
+ test("hello has exactly one flow and no Store", () => {
254
+ const dir = mkdtempSync(join(tmpdir(), "create-oke-hello-assert-"));
255
+ try {
256
+ const result = scaffold({
257
+ targetDir: join(dir, "hello"),
258
+ name: "hello-app",
259
+ source: { kind: "template", id: "hello" },
260
+ });
261
+ const flowFiles = result.files.filter(
262
+ (f) => f.startsWith("src/flows/") && f.endsWith(".ts"),
263
+ );
264
+ expect(flowFiles).toEqual(["src/flows/hello/index.ts"]);
265
+ const flowSrc = readFileSync(
266
+ join(result.targetDir, "src/flows/hello/index.ts"),
267
+ "utf8",
268
+ );
269
+ expect(flowSrc).toMatch(/export const hello/);
270
+ expect(flowSrc).not.toMatch(/\bstore\b/);
271
+ expect(result.files.some((f) => f.includes("core.ts"))).toBe(false);
272
+ expect(result.files.some((f) => f.includes("schema.ts"))).toBe(false);
273
+ const all = result.files
274
+ .filter((f) => f.endsWith(".ts"))
275
+ .map((f) => readFileSync(join(result.targetDir, f), "utf8"))
276
+ .join("\n");
277
+ expect(all).not.toMatch(/store\.sql/);
278
+ expect(all).not.toMatch(/\bbookings\b|\borders\b|\blinks\b|\bnotes\b/);
279
+ } finally {
280
+ rmSync(dir, { recursive: true, force: true });
281
+ }
282
+ });
283
+
284
+ test("standard has full four-applications layout and no business domain", () => {
285
+ const dir = mkdtempSync(join(tmpdir(), "create-oke-standard-assert-"));
286
+ try {
287
+ const result = scaffold({
288
+ targetDir: join(dir, "standard"),
289
+ name: "standard-app",
290
+ source: { kind: "template", id: "standard" },
291
+ });
292
+ for (const path of [
293
+ "src/gates.ts",
294
+ "src/vault.ts",
295
+ "src/channels.ts",
296
+ "src/locales/en.ts",
297
+ "src/locales/ar.ts",
298
+ "src/flows/main/shapes.ts",
299
+ "src/flows/main/signals.ts",
300
+ "src/core.ts",
301
+ "src/schema.ts",
302
+ "src/app.ts",
303
+ ]) {
304
+ expect(result.files).toContain(path);
305
+ }
306
+ const all = result.files
307
+ .filter((f) => f.endsWith(".ts") || f.endsWith(".md"))
308
+ .map((f) => readFileSync(join(result.targetDir, f), "utf8"))
309
+ .join("\n");
310
+ expect(all).not.toMatch(/\bbookings\b|\borders\b|\blinks\b|\bstripe\b/i);
311
+ } finally {
312
+ rmSync(dir, { recursive: true, force: true });
313
+ }
314
+ });
315
+
316
+ test("--from-example notes matches today's example tree", () => {
317
+ const dir = mkdtempSync(join(tmpdir(), "create-oke-from-example-"));
318
+ try {
319
+ const exampleDir = resolveExampleDir("notes");
320
+ const expected = listTemplateFiles(exampleDir);
321
+ const result = scaffold({
322
+ targetDir: join(dir, "notes"),
323
+ name: "from-notes",
324
+ source: { kind: "example", id: "notes" },
325
+ });
326
+ expect([...result.files].sort()).toEqual(
327
+ [...expected, "AGENTS.md"].sort(),
328
+ );
329
+ expect(result.files).toContain("src/flows/notes/index.ts");
330
+ } finally {
331
+ rmSync(dir, { recursive: true, force: true });
332
+ }
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
+ });
349
+ });
350
+
351
+ describe("non-TTY CLI", () => {
352
+ test("no args + non-TTY → zero prompts, exit 1", async () => {
353
+ const code = await run([], { stdinIsTTY: false, runPostScaffold: false });
354
+ expect(code).toBe(1);
355
+ });
356
+
357
+ test("spawned with piped stdin shows no clack prompts", async () => {
358
+ const proc = Bun.spawn(["bun", "run", join(packageRoot(), "src/index.ts")], {
359
+ cwd: packageRoot(),
360
+ stdin: "pipe",
361
+ stdout: "pipe",
362
+ stderr: "pipe",
363
+ });
364
+ proc.stdin.end();
365
+ const code = await proc.exited;
366
+ const out = await new Response(proc.stdout).text();
367
+ const err = await new Response(proc.stderr).text();
368
+ expect(code).toBe(1);
369
+ expect(err).toMatch(/missing <name>/);
370
+ // Clack intro / select chrome must not appear.
371
+ expect(out + err).not.toMatch(/Project name|Start from a worked example/);
372
+ expect(out + err).not.toMatch(/│/);
373
+ });
374
+
375
+ test("flag-driven scaffold prints the shared next-steps text", async () => {
376
+ const root = mkdtempSync(join(tmpdir(), "create-oke-flag-"));
377
+ const target = join(root, "flag-app");
378
+ try {
379
+ const code = await run(
380
+ [target, "--template", "hello", "--no-install"],
381
+ { stdinIsTTY: false, runPostScaffold: false },
382
+ );
383
+ expect(code).toBe(0);
384
+ expect(readdirSync(target).length).toBeGreaterThan(0);
385
+ expect(existsSync(join(target, "AGENTS.md"))).toBe(true);
386
+ const result = {
387
+ targetDir: target,
388
+ name: "flag-app",
389
+ source: { kind: "template" as const, id: "hello" as const },
390
+ label: "hello",
391
+ okengineDependency: "x",
392
+ files: [],
393
+ };
394
+ expect(nextStepsText(result)).toContain("oke dev");
395
+ expect(nextStepsText(result)).toContain("bun install");
396
+ expect(nextStepsText(result)).toContain("okengine.vercel.app");
397
+ } finally {
398
+ rmSync(root, { recursive: true, force: true });
399
+ }
400
+ });
401
+ });
402
+
403
+ describe("examples catalogue", () => {
404
+ test("all four teaching examples resolve", () => {
405
+ for (const id of EXAMPLES) {
406
+ expect(resolveExampleDir(id)).toBeTruthy();
407
+ }
408
+ });
110
409
  });