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.
- package/README.md +26 -6
- package/{templates → examples}/linkly/package.json +1 -1
- package/{templates → examples}/notes/package.json +1 -1
- package/{templates → examples}/provisions/package.json +1 -1
- package/{templates → examples}/skyport/package.json +1 -1
- package/package.json +6 -2
- package/src/agents-md.ts +99 -0
- package/src/cli.test.ts +318 -19
- package/src/cli.ts +561 -29
- package/src/docs-origin.ts +19 -0
- package/src/index.ts +3 -1
- package/src/scaffold.ts +59 -32
- package/src/sync-templates.ts +28 -7
- package/src/templates.ts +85 -12
- package/src/transform.ts +5 -2
- package/templates/full/README.md +49 -0
- package/templates/full/oke.config.ts +23 -0
- package/templates/full/package.json +15 -0
- package/templates/full/src/ai.ts +11 -0
- package/templates/full/src/app.ts +34 -0
- package/templates/full/src/channels.ts +9 -0
- package/templates/full/src/core.ts +4 -0
- package/templates/full/src/flows/main/index.ts +45 -0
- package/templates/full/src/flows/main/shapes.ts +3 -0
- package/templates/full/src/flows/main/signals.ts +8 -0
- package/templates/full/src/gates.ts +4 -0
- package/templates/full/src/locales/ar.ts +3 -0
- package/templates/full/src/locales/en.ts +3 -0
- package/templates/full/src/schema.ts +8 -0
- package/templates/full/src/vault.ts +9 -0
- package/templates/full/tests/full.test.ts +11 -0
- package/templates/hello/README.md +38 -0
- package/templates/hello/oke.config.ts +3 -0
- package/templates/hello/package.json +14 -0
- package/templates/hello/src/app.ts +10 -0
- package/templates/hello/src/flows/hello/index.ts +27 -0
- package/templates/hello/tests/hello.test.ts +10 -0
- package/templates/minimal/README.md +41 -0
- package/templates/minimal/oke.config.ts +7 -0
- package/templates/minimal/package.json +15 -0
- package/templates/minimal/src/app.ts +13 -0
- package/templates/minimal/src/core.ts +4 -0
- package/templates/minimal/src/flows/main/index.ts +49 -0
- package/templates/minimal/src/schema.ts +8 -0
- package/templates/minimal/tests/minimal.test.ts +13 -0
- package/templates/standard/README.md +47 -0
- package/templates/standard/oke.config.ts +19 -0
- package/templates/standard/package.json +15 -0
- package/templates/standard/src/app.ts +18 -0
- package/templates/standard/src/channels.ts +4 -0
- package/templates/standard/src/core.ts +4 -0
- package/templates/standard/src/flows/main/index.ts +31 -0
- package/templates/standard/src/flows/main/shapes.ts +2 -0
- package/templates/standard/src/flows/main/signals.ts +4 -0
- package/templates/standard/src/gates.ts +4 -0
- package/templates/standard/src/locales/ar.ts +2 -0
- package/templates/standard/src/locales/en.ts +2 -0
- package/templates/standard/src/schema.ts +9 -0
- package/templates/standard/src/vault.ts +4 -0
- package/templates/standard/tests/standard.test.ts +10 -0
- /package/{templates → examples}/linkly/oke.config.ts +0 -0
- /package/{templates → examples}/linkly/src/app.ts +0 -0
- /package/{templates → examples}/linkly/src/core.ts +0 -0
- /package/{templates → examples}/linkly/src/flows/analytics/index.ts +0 -0
- /package/{templates → examples}/linkly/src/flows/links/index.ts +0 -0
- /package/{templates → examples}/linkly/src/flows/links/shapes.ts +0 -0
- /package/{templates → examples}/linkly/src/flows/links/signals.ts +0 -0
- /package/{templates → examples}/linkly/src/gates.ts +0 -0
- /package/{templates → examples}/linkly/src/schema.ts +0 -0
- /package/{templates → examples}/linkly/tests/linkly.test.ts +0 -0
- /package/{templates → examples}/notes/oke.config.ts +0 -0
- /package/{templates → examples}/notes/src/app.ts +0 -0
- /package/{templates → examples}/notes/src/core.ts +0 -0
- /package/{templates → examples}/notes/src/flows/notes/index.ts +0 -0
- /package/{templates → examples}/notes/src/schema.ts +0 -0
- /package/{templates → examples}/notes/tests/notes.test.ts +0 -0
- /package/{templates → examples}/provisions/oke.config.ts +0 -0
- /package/{templates → examples}/provisions/src/app.ts +0 -0
- /package/{templates → examples}/provisions/src/channels.ts +0 -0
- /package/{templates → examples}/provisions/src/core.ts +0 -0
- /package/{templates → examples}/provisions/src/flows/notifications/index.ts +0 -0
- /package/{templates → examples}/provisions/src/flows/orders/index.ts +0 -0
- /package/{templates → examples}/provisions/src/flows/orders/shapes.ts +0 -0
- /package/{templates → examples}/provisions/src/flows/orders/signals.ts +0 -0
- /package/{templates → examples}/provisions/src/flows/payments/index.ts +0 -0
- /package/{templates → examples}/provisions/src/flows/payments/shapes.ts +0 -0
- /package/{templates → examples}/provisions/src/flows/payments/stripe.ts +0 -0
- /package/{templates → examples}/provisions/src/gates.ts +0 -0
- /package/{templates → examples}/provisions/src/locales/ar.ts +0 -0
- /package/{templates → examples}/provisions/src/locales/en.ts +0 -0
- /package/{templates → examples}/provisions/src/plugins/audit-schema.ts +0 -0
- /package/{templates → examples}/provisions/src/plugins/audit.ts +0 -0
- /package/{templates → examples}/provisions/src/schema.ts +0 -0
- /package/{templates → examples}/provisions/src/vault.ts +0 -0
- /package/{templates → examples}/provisions/tests/orders.test.ts +0 -0
- /package/{templates → examples}/skyport/evals/triage.jsonl +0 -0
- /package/{templates → examples}/skyport/oke.config.ts +0 -0
- /package/{templates → examples}/skyport/oke.images.lock +0 -0
- /package/{templates → examples}/skyport/src/ai.ts +0 -0
- /package/{templates → examples}/skyport/src/app.ts +0 -0
- /package/{templates → examples}/skyport/src/channels.ts +0 -0
- /package/{templates → examples}/skyport/src/core.ts +0 -0
- /package/{templates → examples}/skyport/src/flows/bookings/index.ts +0 -0
- /package/{templates → examples}/skyport/src/flows/bookings/shapes.ts +0 -0
- /package/{templates → examples}/skyport/src/flows/bookings/signals.ts +0 -0
- /package/{templates → examples}/skyport/src/flows/notifications/index.ts +0 -0
- /package/{templates → examples}/skyport/src/flows/payments/index.ts +0 -0
- /package/{templates → examples}/skyport/src/flows/payments/shapes.ts +0 -0
- /package/{templates → examples}/skyport/src/flows/support/index.ts +0 -0
- /package/{templates → examples}/skyport/src/flows/users/elements.ts +0 -0
- /package/{templates → examples}/skyport/src/flows/users/index.ts +0 -0
- /package/{templates → examples}/skyport/src/flows/users/shapes.ts +0 -0
- /package/{templates → examples}/skyport/src/gates.ts +0 -0
- /package/{templates → examples}/skyport/src/journeys.ts +0 -0
- /package/{templates → examples}/skyport/src/locales/ar.ts +0 -0
- /package/{templates → examples}/skyport/src/locales/en.ts +0 -0
- /package/{templates → examples}/skyport/src/plugins/audit-schema.ts +0 -0
- /package/{templates → examples}/skyport/src/plugins/audit.ts +0 -0
- /package/{templates → examples}/skyport/src/plugins/panel.tsx +0 -0
- /package/{templates → examples}/skyport/src/schema.ts +0 -0
- /package/{templates → examples}/skyport/src/vault.ts +0 -0
- /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 —
|
|
2
|
+
* `create-oke` CLI — clean templates by default; teaching examples opt-in.
|
|
3
3
|
*
|
|
4
4
|
* ```bash
|
|
5
|
-
* bunx create-oke@latest <name> [--template
|
|
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 {
|
|
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 {
|
|
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
|
|
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
|
|
101
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
494
|
+
if (args.help) {
|
|
126
495
|
console.log(helpText());
|
|
127
|
-
|
|
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
|
|
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
|
|
138
|
-
name
|
|
139
|
-
|
|
604
|
+
targetDir,
|
|
605
|
+
name,
|
|
606
|
+
source,
|
|
607
|
+
writeAgentsMd: agentsMd,
|
|
140
608
|
});
|
|
609
|
+
if (spun) spun.stop("Scaffolded.");
|
|
141
610
|
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
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
|
+
}
|