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/src/cli.ts CHANGED
@@ -1,17 +1,39 @@
1
1
  /**
2
- * `create-oke` CLI — one command, sensible default, done.
2
+ * `create-oke` CLI — clean templates by default; teaching examples opt-in.
3
3
  *
4
4
  * ```bash
5
- * bunx create-oke@latest <name> [--template notes|linkly|provisions|skyport]
5
+ * bunx create-oke@latest <name> [--template hello|minimal|standard|full]
6
+ * bunx create-oke@latest <name> --from-example notes|linkly|provisions|skyport
7
+ * bunx create-oke@latest <name> --yes
8
+ * bunx create-oke@latest # interactive when stdin is a TTY
6
9
  * ```
7
10
  */
8
11
 
12
+ import {
13
+ cancel,
14
+ confirm,
15
+ intro,
16
+ isCancel,
17
+ note,
18
+ outro,
19
+ select,
20
+ spinner,
21
+ text,
22
+ } from "@clack/prompts";
9
23
  import { basename, relative, resolve } from "node:path";
10
- import { scaffold } from "./scaffold.ts";
24
+ import { existsSync, rmSync } from "node:fs";
25
+ import { agentsMdContent } from "./agents-md.ts";
26
+ import { docsUrl } from "./docs-origin.ts";
27
+ import { scaffold, type ScaffoldResult, type ScaffoldSource } from "./scaffold.ts";
11
28
  import {
12
29
  DEFAULT_TEMPLATE,
30
+ EXAMPLE_NEW_IDEAS,
31
+ EXAMPLES,
32
+ TEMPLATE_PURPOSES,
13
33
  TEMPLATES,
34
+ isExampleId,
14
35
  isTemplateId,
36
+ type ExampleId,
15
37
  type TemplateId,
16
38
  } from "./templates.ts";
17
39
 
@@ -19,10 +41,54 @@ import {
19
41
  export type CliArgs = {
20
42
  readonly name: string | undefined;
21
43
  readonly template: TemplateId;
44
+ readonly fromExample: ExampleId | undefined;
45
+ /** True when `--template` / `-t` was present on the argv. */
46
+ readonly templateExplicit: boolean;
22
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;
23
57
  readonly targetDir: string | undefined;
24
58
  };
25
59
 
60
+ /**
61
+ * Sentinel choice for "Start from a worked example" in interactive answers.
62
+ * Distinct from clack's internal select value (`__example__`).
63
+ */
64
+ export const FROM_EXAMPLE_CHOICE = "from-example" as const;
65
+
66
+ /** Answers collected by the interactive ask step (no clack types). */
67
+ export type InteractiveAnswers =
68
+ | {
69
+ readonly name: string;
70
+ readonly choice: TemplateId;
71
+ readonly installAndRun: boolean;
72
+ readonly agentsMd: boolean;
73
+ }
74
+ | {
75
+ readonly name: string;
76
+ readonly choice: typeof FROM_EXAMPLE_CHOICE;
77
+ readonly example: ExampleId;
78
+ readonly installAndRun: boolean;
79
+ readonly agentsMd: boolean;
80
+ };
81
+
82
+ /**
83
+ * Canonical scaffold invocation — shared by interactive and flag-driven paths.
84
+ */
85
+ export type ScaffoldCallArgs = {
86
+ readonly name: string;
87
+ readonly targetDir: string;
88
+ readonly source: ScaffoldSource;
89
+ readonly agentsMd: boolean;
90
+ };
91
+
26
92
  /**
27
93
  * Parse argv after the binary name.
28
94
  *
@@ -31,7 +97,12 @@ export type CliArgs = {
31
97
  export function parseArgs(argv: readonly string[]): CliArgs {
32
98
  let name: string | undefined;
33
99
  let template: TemplateId = DEFAULT_TEMPLATE;
100
+ let fromExample: ExampleId | undefined;
101
+ let templateExplicit = false;
34
102
  let help = false;
103
+ let yes = false;
104
+ let install: boolean | undefined;
105
+ let agentsMd = true;
35
106
  let targetDir: string | undefined;
36
107
 
37
108
  for (let i = 0; i < argv.length; i++) {
@@ -40,6 +111,26 @@ export function parseArgs(argv: readonly string[]): CliArgs {
40
111
  help = true;
41
112
  continue;
42
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
+ }
43
134
  if (a === "--template" || a === "-t") {
44
135
  const next = argv[++i];
45
136
  if (!next || !isTemplateId(next)) {
@@ -48,6 +139,7 @@ export function parseArgs(argv: readonly string[]): CliArgs {
48
139
  );
49
140
  }
50
141
  template = next;
142
+ templateExplicit = true;
51
143
  continue;
52
144
  }
53
145
  if (a.startsWith("--template=")) {
@@ -58,6 +150,27 @@ export function parseArgs(argv: readonly string[]): CliArgs {
58
150
  );
59
151
  }
60
152
  template = value;
153
+ templateExplicit = true;
154
+ continue;
155
+ }
156
+ if (a === "--from-example") {
157
+ const next = argv[++i];
158
+ if (!next || !isExampleId(next)) {
159
+ throw new Error(
160
+ `create-oke: --from-example must be one of ${EXAMPLES.join("|")}`,
161
+ );
162
+ }
163
+ fromExample = next;
164
+ continue;
165
+ }
166
+ if (a.startsWith("--from-example=")) {
167
+ const value = a.slice("--from-example=".length);
168
+ if (!isExampleId(value)) {
169
+ throw new Error(
170
+ `create-oke: --from-example must be one of ${EXAMPLES.join("|")}`,
171
+ );
172
+ }
173
+ fromExample = value;
61
174
  continue;
62
175
  }
63
176
  if (a.startsWith("-")) {
@@ -70,11 +183,31 @@ export function parseArgs(argv: readonly string[]): CliArgs {
70
183
  throw new Error(`create-oke: unexpected argument ${a}`);
71
184
  }
72
185
 
186
+ if (templateExplicit && fromExample !== undefined) {
187
+ throw new Error(
188
+ "create-oke: use either --template or --from-example, not both",
189
+ );
190
+ }
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
+
73
196
  if (name !== undefined) {
74
197
  targetDir = resolve(name);
75
198
  }
76
199
 
77
- return { name, template, help, targetDir };
200
+ return {
201
+ name,
202
+ template,
203
+ fromExample,
204
+ templateExplicit,
205
+ help,
206
+ yes,
207
+ install,
208
+ agentsMd,
209
+ targetDir,
210
+ };
78
211
  }
79
212
 
80
213
  /**
@@ -88,32 +221,268 @@ export function formatCdPath(targetDir: string): string {
88
221
  return rel;
89
222
  }
90
223
 
224
+ /**
225
+ * Post-scaffold next-steps block — shared by interactive and flag-driven paths.
226
+ *
227
+ * @param result - Successful scaffold result
228
+ */
229
+ export function nextStepsText(result: ScaffoldResult): string {
230
+ return `
231
+ Scaffolded ${result.label} → ${result.targetDir}
232
+
233
+ Next steps:
234
+
235
+ cd ${formatCdPath(result.targetDir)}
236
+ bun install
237
+ oke dev # app :6530 · Console :6533 · MCP :6535
238
+
239
+ Docs: ${docsUrl("/docs")}
240
+ `;
241
+ }
242
+
91
243
  /**
92
244
  * Help text — next steps match four-applications.md (`bun install` · `oke dev`).
93
245
  */
94
246
  export function helpText(): string {
247
+ const templateLines = TEMPLATES.map(
248
+ (id) =>
249
+ ` ${id.padEnd(12)}${TEMPLATE_PURPOSES[id]}${id === DEFAULT_TEMPLATE ? " (default)" : ""}`,
250
+ ).join("\n");
251
+ const exampleLines = EXAMPLES.map(
252
+ (id) => ` ${id.padEnd(12)}${EXAMPLE_NEW_IDEAS[id]}`,
253
+ ).join("\n");
254
+
95
255
  return `create-oke — scaffold an okengine app
96
256
 
97
257
  Usage:
98
- bunx create-oke@latest <name> [--template notes|linkly|provisions|skyport]
258
+ bunx create-oke@latest <name> [--template hello|minimal|standard|full]
259
+ bunx create-oke@latest <name> --from-example notes|linkly|provisions|skyport
260
+ bunx create-oke@latest <name> --yes
261
+ bunx create-oke@latest # interactive (TTY only)
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
99
272
 
100
- Templates (from examples/):
101
- notes basic — Flow · Store (default)
102
- linkly intermediate — + Signal · Clock · Gate
103
- provisions advanced — + Vault · Channel · plugins
104
- skyport complex — all eight elements · AI · tenancy
273
+ Templates (clean starters from templates/):
274
+ ${templateLines}
105
275
 
106
- No telemetry. Bun only. No interactive wizard.
276
+ --from-example (copies a teaching example, including its business logic and
277
+ comments — most new projects want --template instead):
278
+ ${exampleLines}
279
+
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.
107
283
  `;
108
284
  }
109
285
 
286
+ /**
287
+ * Whether the CLI should open the interactive Clack flow.
288
+ *
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.
291
+ *
292
+ * @param args - Parsed args
293
+ * @param stdinIsTTY - `process.stdin.isTTY`
294
+ */
295
+ export function shouldPrompt(
296
+ args: CliArgs,
297
+ stdinIsTTY: boolean | undefined,
298
+ ): boolean {
299
+ if (!stdinIsTTY) return false;
300
+ if (args.help) return false;
301
+ if (args.yes) return false;
302
+ if (args.templateExplicit) return false;
303
+ if (args.fromExample !== undefined) return false;
304
+ return true;
305
+ }
306
+
307
+ /**
308
+ * Build the {@link ScaffoldSource} for a flag-driven invocation.
309
+ *
310
+ * @param args - Parsed args
311
+ */
312
+ export function sourceFromArgs(args: CliArgs): ScaffoldSource {
313
+ if (args.fromExample !== undefined) {
314
+ return { kind: "example", id: args.fromExample };
315
+ }
316
+ return { kind: "template", id: args.template };
317
+ }
318
+
319
+ /**
320
+ * Map flag-driven {@link CliArgs} to scaffold call args.
321
+ *
322
+ * Requires a positional project name (and thus `targetDir`).
323
+ *
324
+ * @param args - Parsed CLI args with `name` set
325
+ */
326
+ export function scaffoldArgsFromCli(args: CliArgs): ScaffoldCallArgs {
327
+ if (args.name === undefined || args.targetDir === undefined) {
328
+ throw new Error("create-oke: missing <name>");
329
+ }
330
+ return {
331
+ name: basename(resolve(args.name)),
332
+ targetDir: args.targetDir,
333
+ source: sourceFromArgs(args),
334
+ agentsMd: args.agentsMd,
335
+ };
336
+ }
337
+
338
+ /**
339
+ * Pure map from interactive answers → scaffold call args.
340
+ *
341
+ * Independent of clack — unit-tested against {@link scaffoldArgsFromCli}
342
+ * for every template and every `--from-example` choice.
343
+ *
344
+ * @param answers - Collected interactive answers
345
+ */
346
+ export function scaffoldArgsFromAnswers(
347
+ answers: InteractiveAnswers,
348
+ ): ScaffoldCallArgs {
349
+ const targetDir = resolve(answers.name.trim());
350
+ const name = basename(targetDir);
351
+ if (answers.choice === FROM_EXAMPLE_CHOICE) {
352
+ return {
353
+ name,
354
+ targetDir,
355
+ source: { kind: "example", id: answers.example },
356
+ agentsMd: answers.agentsMd,
357
+ };
358
+ }
359
+ return {
360
+ name,
361
+ targetDir,
362
+ source: { kind: "template", id: answers.choice },
363
+ agentsMd: answers.agentsMd,
364
+ };
365
+ }
366
+
367
+ /**
368
+ * Ask step — clack prompts only. Returns answers or `null` on cancel.
369
+ *
370
+ * Injectable for tests; production uses {@link askInteractiveAnswers}.
371
+ *
372
+ * @param partial - Name already known (pre-filled in the prompt)
373
+ */
374
+ export type AskInteractiveFn = (
375
+ partial: { readonly name?: string; readonly agentsMd?: boolean },
376
+ ) => Promise<InteractiveAnswers | null>;
377
+
378
+ /**
379
+ * Collect interactive answers via `@clack/prompts`.
380
+ *
381
+ * @param partial - Optional pre-filled project name / agents-md default
382
+ * @returns Answers, or `null` if the user cancelled
383
+ */
384
+ export async function askInteractiveAnswers(
385
+ partial: { readonly name?: string; readonly agentsMd?: boolean } = {},
386
+ ): Promise<InteractiveAnswers | null> {
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();
398
+
399
+ const templateValue = await select({
400
+ message: "Template",
401
+ options: [
402
+ ...TEMPLATES.map((id) => ({
403
+ value: id as string,
404
+ label: id,
405
+ hint: TEMPLATE_PURPOSES[id],
406
+ })),
407
+ {
408
+ value: "__example__",
409
+ label: "Start from a worked example",
410
+ hint: "Teaching apps with business logic — most projects want a template",
411
+ },
412
+ ],
413
+ initialValue: DEFAULT_TEMPLATE,
414
+ });
415
+ if (isCancel(templateValue)) return null;
416
+
417
+ const agentsMd = partial.agentsMd ?? true;
418
+
419
+ if (templateValue === "__example__") {
420
+ const exampleValue = await select({
421
+ message: "Example",
422
+ options: EXAMPLES.map((id) => ({
423
+ value: id,
424
+ label: id,
425
+ hint: EXAMPLE_NEW_IDEAS[id],
426
+ })),
427
+ });
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
+
436
+ return {
437
+ name,
438
+ choice: FROM_EXAMPLE_CHOICE,
439
+ example: exampleValue as ExampleId,
440
+ installAndRun: Boolean(installAndRunValue),
441
+ agentsMd,
442
+ };
443
+ }
444
+
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;
468
+ }
469
+
110
470
  /**
111
471
  * Run the CLI.
112
472
  *
113
473
  * @param argv - Args after the binary
474
+ * @param options - Test seams (stdin TTY, ask injection, skip real install/dev)
114
475
  * @returns Exit code
115
476
  */
116
- export async function run(argv: readonly string[]): Promise<number> {
477
+ export async function run(
478
+ argv: readonly string[],
479
+ options: {
480
+ readonly stdinIsTTY?: boolean | undefined;
481
+ readonly ask?: AskInteractiveFn;
482
+ /** When false, skip spawning bun install / oke dev (tests). Default true. */
483
+ readonly runPostScaffold?: boolean;
484
+ } = {},
485
+ ): Promise<number> {
117
486
  let args: CliArgs;
118
487
  try {
119
488
  args = parseArgs(argv);
@@ -122,35 +491,198 @@ export async function run(argv: readonly string[]): Promise<number> {
122
491
  return 1;
123
492
  }
124
493
 
125
- if (args.help || args.name === undefined) {
494
+ if (args.help) {
126
495
  console.log(helpText());
127
- if (args.name === undefined && !args.help) {
128
- console.error("create-oke: missing <name>");
129
- return 1;
130
- }
131
- return args.help ? 0 : 1;
496
+ return 0;
132
497
  }
133
498
 
134
- const projectName = basename(resolve(args.name));
499
+ const stdinIsTTY = options.stdinIsTTY ?? process.stdin.isTTY;
500
+ if (shouldPrompt(args, stdinIsTTY)) {
501
+ return runInteractive(args, options.ask ?? askInteractiveAnswers, options);
502
+ }
503
+
504
+ if (args.name === undefined) {
505
+ console.log(helpText());
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");
510
+ return 1;
511
+ }
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
+
520
+ return runScaffold({
521
+ ...scaffoldArgsFromCli(args),
522
+ interactive: false,
523
+ install: shouldInstall(args),
524
+ startDev: false,
525
+ runPostScaffold: options.runPostScaffold ?? true,
526
+ });
527
+ }
528
+
529
+ /**
530
+ * Interactive TTY flow — ask → map answers → scaffold.
531
+ *
532
+ * @param args - Parsed args (name may already be set)
533
+ * @param ask - Injectable ask step
534
+ * @param options - Post-scaffold seams
535
+ */
536
+ async function runInteractive(
537
+ args: CliArgs,
538
+ ask: AskInteractiveFn,
539
+ options: { readonly runPostScaffold?: boolean },
540
+ ): Promise<number> {
541
+ intro("create-oke");
542
+
543
+ const answers = await ask({ name: args.name, agentsMd: args.agentsMd });
544
+ if (answers === null) {
545
+ cancel("Cancelled.");
546
+ return 1;
547
+ }
548
+
549
+ return runScaffold({
550
+ ...scaffoldArgsFromAnswers(answers),
551
+ interactive: true,
552
+ install: answers.installAndRun,
553
+ startDev: answers.installAndRun,
554
+ runPostScaffold: options.runPostScaffold ?? true,
555
+ });
556
+ }
557
+
558
+ /**
559
+ * Scaffold with optional spinner / outro, cleaning up on failure or cancel.
560
+ *
561
+ * @param options - Scaffold inputs + interactive / install flags
562
+ */
563
+ async function runScaffold(
564
+ options: ScaffoldCallArgs & {
565
+ readonly interactive: boolean;
566
+ readonly install: boolean;
567
+ readonly startDev: boolean;
568
+ readonly runPostScaffold: boolean;
569
+ },
570
+ ): Promise<number> {
571
+ const {
572
+ name,
573
+ targetDir,
574
+ source,
575
+ agentsMd,
576
+ interactive,
577
+ install,
578
+ startDev,
579
+ runPostScaffold,
580
+ } = options;
581
+ const existed = existsSync(targetDir);
582
+ let spun: ReturnType<typeof spinner> | undefined;
583
+
584
+ const cleanup = (): void => {
585
+ if (!existed && existsSync(targetDir)) {
586
+ rmSync(targetDir, { recursive: true, force: true });
587
+ }
588
+ };
589
+
590
+ const onSigInt = (): void => {
591
+ spun?.stop("Cancelled.");
592
+ cleanup();
593
+ if (interactive) cancel("Cancelled.");
594
+ process.exit(1);
595
+ };
596
+ process.once("SIGINT", onSigInt);
597
+
135
598
  try {
599
+ if (interactive) {
600
+ spun = spinner();
601
+ spun.start("Scaffolding…");
602
+ }
136
603
  const result = scaffold({
137
- targetDir: args.targetDir!,
138
- name: projectName,
139
- template: args.template,
604
+ targetDir,
605
+ name,
606
+ source,
607
+ writeAgentsMd: agentsMd,
140
608
  });
609
+ if (spun) spun.stop("Scaffolded.");
141
610
 
142
- console.log(`
143
- Scaffolded ${result.template} ${result.targetDir}
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
+ }
144
623
 
145
- Next steps:
624
+ const message = nextStepsText(result);
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
+ );
638
+ outro(message.trim());
639
+ } else {
640
+ console.log(message);
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
+ }
146
649
 
147
- cd ${formatCdPath(result.targetDir)}
148
- bun install
149
- oke dev # app :6530 · Console :6533 · MCP :6535
150
- `);
151
650
  return 0;
152
651
  } catch (e) {
652
+ spun?.stop("Failed.");
653
+ cleanup();
153
654
  console.error(e instanceof Error ? e.message : e);
154
655
  return 1;
656
+ } finally {
657
+ process.off("SIGINT", onSigInt);
155
658
  }
156
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
+ }