create-oke 0.1.6 → 0.2.1

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 (117) hide show
  1. package/README.md +22 -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/cli.test.ts +246 -18
  8. package/src/cli.ts +358 -32
  9. package/src/index.ts +2 -1
  10. package/src/scaffold.ts +50 -32
  11. package/src/sync-templates.ts +28 -7
  12. package/src/templates.ts +85 -12
  13. package/src/transform.ts +3 -2
  14. package/templates/full/oke.config.ts +23 -0
  15. package/templates/full/package.json +15 -0
  16. package/templates/full/src/ai.ts +11 -0
  17. package/templates/full/src/app.ts +34 -0
  18. package/templates/full/src/channels.ts +9 -0
  19. package/templates/full/src/core.ts +4 -0
  20. package/templates/full/src/flows/main/index.ts +27 -0
  21. package/templates/full/src/flows/main/shapes.ts +3 -0
  22. package/templates/full/src/flows/main/signals.ts +8 -0
  23. package/templates/full/src/gates.ts +4 -0
  24. package/templates/full/src/locales/ar.ts +3 -0
  25. package/templates/full/src/locales/en.ts +3 -0
  26. package/templates/full/src/schema.ts +8 -0
  27. package/templates/full/src/vault.ts +9 -0
  28. package/templates/full/tests/full.test.ts +11 -0
  29. package/templates/hello/README.md +13 -0
  30. package/templates/hello/oke.config.ts +3 -0
  31. package/templates/hello/package.json +14 -0
  32. package/templates/hello/src/app.ts +10 -0
  33. package/templates/hello/src/flows/hello/index.ts +10 -0
  34. package/templates/hello/tests/hello.test.ts +10 -0
  35. package/templates/minimal/oke.config.ts +7 -0
  36. package/templates/minimal/package.json +15 -0
  37. package/templates/minimal/src/app.ts +13 -0
  38. package/templates/minimal/src/core.ts +4 -0
  39. package/templates/minimal/src/flows/main/index.ts +32 -0
  40. package/templates/minimal/src/schema.ts +8 -0
  41. package/templates/minimal/tests/minimal.test.ts +13 -0
  42. package/templates/standard/oke.config.ts +19 -0
  43. package/templates/standard/package.json +15 -0
  44. package/templates/standard/src/app.ts +18 -0
  45. package/templates/standard/src/channels.ts +4 -0
  46. package/templates/standard/src/core.ts +4 -0
  47. package/templates/standard/src/flows/main/index.ts +14 -0
  48. package/templates/standard/src/flows/main/shapes.ts +2 -0
  49. package/templates/standard/src/flows/main/signals.ts +4 -0
  50. package/templates/standard/src/gates.ts +4 -0
  51. package/templates/standard/src/locales/ar.ts +2 -0
  52. package/templates/standard/src/locales/en.ts +2 -0
  53. package/templates/standard/src/schema.ts +9 -0
  54. package/templates/standard/src/vault.ts +4 -0
  55. package/templates/standard/tests/standard.test.ts +10 -0
  56. /package/{templates → examples}/linkly/oke.config.ts +0 -0
  57. /package/{templates → examples}/linkly/src/app.ts +0 -0
  58. /package/{templates → examples}/linkly/src/core.ts +0 -0
  59. /package/{templates → examples}/linkly/src/flows/analytics/index.ts +0 -0
  60. /package/{templates → examples}/linkly/src/flows/links/index.ts +0 -0
  61. /package/{templates → examples}/linkly/src/flows/links/shapes.ts +0 -0
  62. /package/{templates → examples}/linkly/src/flows/links/signals.ts +0 -0
  63. /package/{templates → examples}/linkly/src/gates.ts +0 -0
  64. /package/{templates → examples}/linkly/src/schema.ts +0 -0
  65. /package/{templates → examples}/linkly/tests/linkly.test.ts +0 -0
  66. /package/{templates → examples}/notes/oke.config.ts +0 -0
  67. /package/{templates → examples}/notes/src/app.ts +0 -0
  68. /package/{templates → examples}/notes/src/core.ts +0 -0
  69. /package/{templates → examples}/notes/src/flows/notes/index.ts +0 -0
  70. /package/{templates → examples}/notes/src/schema.ts +0 -0
  71. /package/{templates → examples}/notes/tests/notes.test.ts +0 -0
  72. /package/{templates → examples}/provisions/oke.config.ts +0 -0
  73. /package/{templates → examples}/provisions/src/app.ts +0 -0
  74. /package/{templates → examples}/provisions/src/channels.ts +0 -0
  75. /package/{templates → examples}/provisions/src/core.ts +0 -0
  76. /package/{templates → examples}/provisions/src/flows/notifications/index.ts +0 -0
  77. /package/{templates → examples}/provisions/src/flows/orders/index.ts +0 -0
  78. /package/{templates → examples}/provisions/src/flows/orders/shapes.ts +0 -0
  79. /package/{templates → examples}/provisions/src/flows/orders/signals.ts +0 -0
  80. /package/{templates → examples}/provisions/src/flows/payments/index.ts +0 -0
  81. /package/{templates → examples}/provisions/src/flows/payments/shapes.ts +0 -0
  82. /package/{templates → examples}/provisions/src/flows/payments/stripe.ts +0 -0
  83. /package/{templates → examples}/provisions/src/gates.ts +0 -0
  84. /package/{templates → examples}/provisions/src/locales/ar.ts +0 -0
  85. /package/{templates → examples}/provisions/src/locales/en.ts +0 -0
  86. /package/{templates → examples}/provisions/src/plugins/audit-schema.ts +0 -0
  87. /package/{templates → examples}/provisions/src/plugins/audit.ts +0 -0
  88. /package/{templates → examples}/provisions/src/schema.ts +0 -0
  89. /package/{templates → examples}/provisions/src/vault.ts +0 -0
  90. /package/{templates → examples}/provisions/tests/orders.test.ts +0 -0
  91. /package/{templates → examples}/skyport/evals/triage.jsonl +0 -0
  92. /package/{templates → examples}/skyport/oke.config.ts +0 -0
  93. /package/{templates → examples}/skyport/oke.images.lock +0 -0
  94. /package/{templates → examples}/skyport/src/ai.ts +0 -0
  95. /package/{templates → examples}/skyport/src/app.ts +0 -0
  96. /package/{templates → examples}/skyport/src/channels.ts +0 -0
  97. /package/{templates → examples}/skyport/src/core.ts +0 -0
  98. /package/{templates → examples}/skyport/src/flows/bookings/index.ts +0 -0
  99. /package/{templates → examples}/skyport/src/flows/bookings/shapes.ts +0 -0
  100. /package/{templates → examples}/skyport/src/flows/bookings/signals.ts +0 -0
  101. /package/{templates → examples}/skyport/src/flows/notifications/index.ts +0 -0
  102. /package/{templates → examples}/skyport/src/flows/payments/index.ts +0 -0
  103. /package/{templates → examples}/skyport/src/flows/payments/shapes.ts +0 -0
  104. /package/{templates → examples}/skyport/src/flows/support/index.ts +0 -0
  105. /package/{templates → examples}/skyport/src/flows/users/elements.ts +0 -0
  106. /package/{templates → examples}/skyport/src/flows/users/index.ts +0 -0
  107. /package/{templates → examples}/skyport/src/flows/users/shapes.ts +0 -0
  108. /package/{templates → examples}/skyport/src/gates.ts +0 -0
  109. /package/{templates → examples}/skyport/src/journeys.ts +0 -0
  110. /package/{templates → examples}/skyport/src/locales/ar.ts +0 -0
  111. /package/{templates → examples}/skyport/src/locales/en.ts +0 -0
  112. /package/{templates → examples}/skyport/src/plugins/audit-schema.ts +0 -0
  113. /package/{templates → examples}/skyport/src/plugins/audit.ts +0 -0
  114. /package/{templates → examples}/skyport/src/plugins/panel.tsx +0 -0
  115. /package/{templates → examples}/skyport/src/schema.ts +0 -0
  116. /package/{templates → examples}/skyport/src/vault.ts +0 -0
  117. /package/{templates → examples}/skyport/tests/support.test.ts +0 -0
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  Scaffold an [okengine](https://www.npmjs.com/package/okengine) app.
4
4
 
5
5
  ```bash
6
- bunx create-oke@latest my-app
6
+ bunx create-oke@latest my-app --template hello
7
7
  cd my-app
8
8
  oke dev
9
9
  ```
@@ -14,21 +14,37 @@ oke dev
14
14
  | `:6533` | Console |
15
15
  | `:6535` | MCP |
16
16
 
17
- Default template is **Notes** (same tree as [`examples/notes`](https://github.com/omqkhafi/okengine/tree/main/examples/notes)). Pick another with `--template`:
17
+ Default template is **standard** (full recommended file layout, empty scaffolding).
18
+ Pick another with `--template`, or start from a teaching example with `--from-example`:
18
19
 
19
20
  ```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
21
+ bunx create-oke@latest my-app --template hello
22
+ bunx create-oke@latest my-app --template minimal
23
+ bunx create-oke@latest my-app --template full
24
+ bunx create-oke@latest my-notes --from-example notes
23
25
  ```
24
26
 
25
- | Template | Adds |
27
+ | Template | Purpose |
28
+ |---|---|
29
+ | `hello` | Fastest possible "it works" — one flow, no Store |
30
+ | `minimal` | Smallest shape you'd actually ship — Store + 1–2 flows |
31
+ | `standard` | Full recommended file layout, empty scaffolding (default) |
32
+ | `full` | Every element present and wired, no business logic |
33
+
34
+ `--from-example` copies a teaching example (including its business logic and
35
+ comments) — most new projects want `--template` instead.
36
+
37
+ | Example | Adds |
26
38
  |---|---|
27
39
  | `notes` | `oke` · `flow` · `http` · `store.sql` · typed client |
28
40
  | `linkly` | `signal` · `clock` · `gate` |
29
41
  | `provisions` | durable flows · `vault` · `channel` · plugins |
30
42
  | `skyport` | `ai` · multi-tenancy · distributed topology |
31
43
 
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
+
32
48
  ## Registries
33
49
 
34
50
  | Registry | Package |
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oke/example-linkly",
3
- "version": "0.1.6",
3
+ "version": "0.2.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.2.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.2.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.2.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.1",
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"
package/src/cli.test.ts CHANGED
@@ -1,14 +1,32 @@
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 { mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs";
7
7
  import { tmpdir } from "node:os";
8
8
  import { join } from "node:path";
9
- import { formatCdPath, parseArgs } from "./cli.ts";
9
+ import {
10
+ FROM_EXAMPLE_CHOICE,
11
+ formatCdPath,
12
+ nextStepsText,
13
+ parseArgs,
14
+ run,
15
+ scaffoldArgsFromAnswers,
16
+ scaffoldArgsFromCli,
17
+ shouldPrompt,
18
+ sourceFromArgs,
19
+ type InteractiveAnswers,
20
+ } from "./cli.ts";
10
21
  import { listTemplateFiles, scaffold } from "./scaffold.ts";
11
- import { resolveTemplateDir, TEMPLATES } from "./templates.ts";
22
+ import {
23
+ DEFAULT_TEMPLATE,
24
+ EXAMPLES,
25
+ TEMPLATES,
26
+ packageRoot,
27
+ resolveExampleDir,
28
+ resolveTemplateDir,
29
+ } from "./templates.ts";
12
30
  import {
13
31
  sanitizeProjectName,
14
32
  shouldSkipTemplatePath,
@@ -16,22 +34,98 @@ import {
16
34
  } from "./transform.ts";
17
35
 
18
36
  describe("parseArgs", () => {
19
- test("defaults template to notes", () => {
37
+ test("defaults template to standard", () => {
20
38
  const a = parseArgs(["my-app"]);
21
39
  expect(a.name).toBe("my-app");
22
- expect(a.template).toBe("notes");
40
+ expect(a.template).toBe("standard");
41
+ expect(a.templateExplicit).toBe(false);
42
+ expect(a.fromExample).toBeUndefined();
23
43
  });
24
44
 
25
45
  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",
46
+ expect(parseArgs(["x", "--template", "hello"]).template).toBe("hello");
47
+ expect(parseArgs(["x", "-t", "full"]).template).toBe("full");
48
+ expect(parseArgs(["x", "--template=minimal"]).template).toBe("minimal");
49
+ expect(parseArgs(["x", "--template", "hello"]).templateExplicit).toBe(true);
50
+ });
51
+
52
+ test("accepts --from-example", () => {
53
+ expect(parseArgs(["x", "--from-example", "notes"]).fromExample).toBe(
54
+ "notes",
55
+ );
56
+ expect(parseArgs(["x", "--from-example=skyport"]).fromExample).toBe(
57
+ "skyport",
30
58
  );
31
59
  });
32
60
 
33
- test("rejects unknown template", () => {
61
+ test("rejects unknown template / example / both", () => {
34
62
  expect(() => parseArgs(["x", "--template", "nope"])).toThrow(/template/);
63
+ expect(() => parseArgs(["x", "--from-example", "nope"])).toThrow(/example/);
64
+ expect(() =>
65
+ parseArgs(["x", "--template", "hello", "--from-example", "notes"]),
66
+ ).toThrow(/either/);
67
+ });
68
+ });
69
+
70
+ describe("shouldPrompt", () => {
71
+ test("TTY + no args → interactive", () => {
72
+ expect(shouldPrompt(parseArgs([]), true)).toBe(true);
73
+ });
74
+
75
+ test("non-TTY or any explicit input → no prompts", () => {
76
+ expect(shouldPrompt(parseArgs([]), false)).toBe(false);
77
+ expect(shouldPrompt(parseArgs(["my-app"]), true)).toBe(false);
78
+ expect(shouldPrompt(parseArgs(["--template", "hello"]), true)).toBe(false);
79
+ expect(shouldPrompt(parseArgs(["--from-example", "notes"]), true)).toBe(
80
+ false,
81
+ );
82
+ expect(shouldPrompt(parseArgs(["--help"]), true)).toBe(false);
83
+ });
84
+ });
85
+
86
+ describe("sourceFromArgs", () => {
87
+ test("prefers example when set", () => {
88
+ expect(sourceFromArgs(parseArgs(["x", "--from-example", "linkly"]))).toEqual(
89
+ { kind: "example", id: "linkly" },
90
+ );
91
+ expect(sourceFromArgs(parseArgs(["x"]))).toEqual({
92
+ kind: "template",
93
+ id: DEFAULT_TEMPLATE,
94
+ });
95
+ });
96
+ });
97
+
98
+ describe("scaffoldArgsFromAnswers ≡ flag-driven", () => {
99
+ test("each --template path matches interactive choice", () => {
100
+ for (const id of TEMPLATES) {
101
+ const answers: InteractiveAnswers = { name: "x", choice: id };
102
+ const fromAnswers = scaffoldArgsFromAnswers(answers);
103
+ const fromFlags = scaffoldArgsFromCli(
104
+ parseArgs(["x", "--template", id]),
105
+ );
106
+ expect(fromAnswers).toEqual(fromFlags);
107
+ expect(fromAnswers.source).toEqual({ kind: "template", id });
108
+ }
109
+ });
110
+
111
+ test("each --from-example path matches interactive from-example choice", () => {
112
+ for (const id of EXAMPLES) {
113
+ const answers: InteractiveAnswers = {
114
+ name: "x",
115
+ choice: FROM_EXAMPLE_CHOICE,
116
+ example: id,
117
+ };
118
+ const fromAnswers = scaffoldArgsFromAnswers(answers);
119
+ const fromFlags = scaffoldArgsFromCli(
120
+ parseArgs(["x", "--from-example", id]),
121
+ );
122
+ expect(fromAnswers).toEqual(fromFlags);
123
+ expect(fromAnswers.source).toEqual({ kind: "example", id });
124
+ }
125
+ });
126
+
127
+ test("8 paths cover every template and every example", () => {
128
+ expect(TEMPLATES.length + EXAMPLES.length).toBe(8);
35
129
  });
36
130
  });
37
131
 
@@ -39,7 +133,7 @@ describe("transformPackageJson", () => {
39
133
  test("rewrites name and okengine file:../.. to installable ref", () => {
40
134
  const next = transformPackageJson(
41
135
  {
42
- name: "@oke/example-notes",
136
+ name: "@oke/template-hello",
43
137
  private: true,
44
138
  dependencies: {
45
139
  okengine: "file:../..",
@@ -81,7 +175,7 @@ describe("formatCdPath", () => {
81
175
  });
82
176
 
83
177
  describe("scaffold structure", () => {
84
- test("each template produces the example tree (minus skips)", () => {
178
+ test("each clean template produces its source tree (minus skips)", () => {
85
179
  for (const id of TEMPLATES) {
86
180
  const dir = mkdtempSync(join(tmpdir(), `create-oke-${id}-`));
87
181
  try {
@@ -90,7 +184,7 @@ describe("scaffold structure", () => {
90
184
  const result = scaffold({
91
185
  targetDir: join(dir, id),
92
186
  name: `app-${id}`,
93
- template: id,
187
+ source: { kind: "template", id },
94
188
  });
95
189
  expect([...result.files].sort()).toEqual(expected);
96
190
  const pkg = JSON.parse(
@@ -98,13 +192,147 @@ describe("scaffold structure", () => {
98
192
  ) as { name: string; dependencies: { okengine: string } };
99
193
  expect(pkg.name).toBe(`app-${id}`);
100
194
  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
195
  } finally {
106
196
  rmSync(dir, { recursive: true, force: true });
107
197
  }
108
198
  }
109
199
  });
200
+
201
+ test("hello has exactly one flow and no Store", () => {
202
+ const dir = mkdtempSync(join(tmpdir(), "create-oke-hello-assert-"));
203
+ try {
204
+ const result = scaffold({
205
+ targetDir: join(dir, "hello"),
206
+ name: "hello-app",
207
+ source: { kind: "template", id: "hello" },
208
+ });
209
+ const flowFiles = result.files.filter(
210
+ (f) => f.startsWith("src/flows/") && f.endsWith(".ts"),
211
+ );
212
+ expect(flowFiles).toEqual(["src/flows/hello/index.ts"]);
213
+ const flowSrc = readFileSync(
214
+ join(result.targetDir, "src/flows/hello/index.ts"),
215
+ "utf8",
216
+ );
217
+ expect(flowSrc).toMatch(/export const hello/);
218
+ expect(flowSrc).not.toMatch(/\bstore\b/);
219
+ expect(result.files.some((f) => f.includes("core.ts"))).toBe(false);
220
+ expect(result.files.some((f) => f.includes("schema.ts"))).toBe(false);
221
+ const all = result.files
222
+ .filter((f) => f.endsWith(".ts"))
223
+ .map((f) => readFileSync(join(result.targetDir, f), "utf8"))
224
+ .join("\n");
225
+ expect(all).not.toMatch(/store\.sql/);
226
+ expect(all).not.toMatch(/\bbookings\b|\borders\b|\blinks\b|\bnotes\b/);
227
+ } finally {
228
+ rmSync(dir, { recursive: true, force: true });
229
+ }
230
+ });
231
+
232
+ test("standard has full four-applications layout and no business domain", () => {
233
+ const dir = mkdtempSync(join(tmpdir(), "create-oke-standard-assert-"));
234
+ try {
235
+ const result = scaffold({
236
+ targetDir: join(dir, "standard"),
237
+ name: "standard-app",
238
+ source: { kind: "template", id: "standard" },
239
+ });
240
+ for (const path of [
241
+ "src/gates.ts",
242
+ "src/vault.ts",
243
+ "src/channels.ts",
244
+ "src/locales/en.ts",
245
+ "src/locales/ar.ts",
246
+ "src/flows/main/shapes.ts",
247
+ "src/flows/main/signals.ts",
248
+ "src/core.ts",
249
+ "src/schema.ts",
250
+ "src/app.ts",
251
+ ]) {
252
+ expect(result.files).toContain(path);
253
+ }
254
+ const all = result.files
255
+ .filter((f) => f.endsWith(".ts") || f.endsWith(".md"))
256
+ .map((f) => readFileSync(join(result.targetDir, f), "utf8"))
257
+ .join("\n");
258
+ expect(all).not.toMatch(/\bbookings\b|\borders\b|\blinks\b|\bstripe\b/i);
259
+ } finally {
260
+ rmSync(dir, { recursive: true, force: true });
261
+ }
262
+ });
263
+
264
+ test("--from-example notes matches today's example tree", () => {
265
+ const dir = mkdtempSync(join(tmpdir(), "create-oke-from-example-"));
266
+ try {
267
+ const exampleDir = resolveExampleDir("notes");
268
+ const expected = listTemplateFiles(exampleDir);
269
+ const result = scaffold({
270
+ targetDir: join(dir, "notes"),
271
+ name: "from-notes",
272
+ source: { kind: "example", id: "notes" },
273
+ });
274
+ expect([...result.files].sort()).toEqual(expected);
275
+ expect(result.files).toContain("src/flows/notes/index.ts");
276
+ } finally {
277
+ rmSync(dir, { recursive: true, force: true });
278
+ }
279
+ });
280
+ });
281
+
282
+ describe("non-TTY CLI", () => {
283
+ test("no args + non-TTY → zero prompts, exit 1", async () => {
284
+ const code = await run([], { stdinIsTTY: false });
285
+ expect(code).toBe(1);
286
+ });
287
+
288
+ test("spawned with piped stdin shows no clack prompts", async () => {
289
+ const proc = Bun.spawn(["bun", "run", join(packageRoot(), "src/index.ts")], {
290
+ cwd: packageRoot(),
291
+ stdin: "pipe",
292
+ stdout: "pipe",
293
+ stderr: "pipe",
294
+ });
295
+ proc.stdin.end();
296
+ const code = await proc.exited;
297
+ const out = await new Response(proc.stdout).text();
298
+ const err = await new Response(proc.stderr).text();
299
+ expect(code).toBe(1);
300
+ expect(err).toMatch(/missing <name>/);
301
+ // Clack intro / select chrome must not appear.
302
+ expect(out + err).not.toMatch(/Project name|Start from a worked example/);
303
+ expect(out + err).not.toMatch(/│/);
304
+ });
305
+
306
+ test("flag-driven scaffold prints the shared next-steps text", async () => {
307
+ const root = mkdtempSync(join(tmpdir(), "create-oke-flag-"));
308
+ const target = join(root, "flag-app");
309
+ try {
310
+ const code = await run(
311
+ [target, "--template", "hello"],
312
+ { stdinIsTTY: false },
313
+ );
314
+ expect(code).toBe(0);
315
+ expect(readdirSync(target).length).toBeGreaterThan(0);
316
+ const result = {
317
+ targetDir: target,
318
+ name: "flag-app",
319
+ source: { kind: "template" as const, id: "hello" as const },
320
+ label: "hello",
321
+ okengineDependency: "x",
322
+ files: [],
323
+ };
324
+ expect(nextStepsText(result)).toContain("oke dev");
325
+ expect(nextStepsText(result)).toContain("bun install");
326
+ } finally {
327
+ rmSync(root, { recursive: true, force: true });
328
+ }
329
+ });
330
+ });
331
+
332
+ describe("examples catalogue", () => {
333
+ test("all four teaching examples resolve", () => {
334
+ for (const id of EXAMPLES) {
335
+ expect(resolveExampleDir(id)).toBeTruthy();
336
+ }
337
+ });
110
338
  });