create-oke 0.1.4 → 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.
- package/README.md +55 -0
- 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 +7 -3
- package/src/cli.test.ts +246 -18
- package/src/cli.ts +358 -32
- package/src/index.ts +9 -1
- package/src/scaffold.ts +50 -32
- package/src/sync-templates.ts +28 -7
- package/src/templates.ts +85 -12
- package/src/transform.ts +3 -2
- 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 +27 -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 +13 -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 +10 -0
- package/templates/hello/tests/hello.test.ts +10 -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 +32 -0
- package/templates/minimal/src/schema.ts +8 -0
- package/templates/minimal/tests/minimal.test.ts +13 -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 +14 -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,34 @@
|
|
|
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 # interactive when stdin is a TTY
|
|
6
8
|
* ```
|
|
7
9
|
*/
|
|
8
10
|
|
|
11
|
+
import {
|
|
12
|
+
cancel,
|
|
13
|
+
intro,
|
|
14
|
+
isCancel,
|
|
15
|
+
outro,
|
|
16
|
+
select,
|
|
17
|
+
spinner,
|
|
18
|
+
text,
|
|
19
|
+
} from "@clack/prompts";
|
|
9
20
|
import { basename, relative, resolve } from "node:path";
|
|
10
|
-
import {
|
|
21
|
+
import { existsSync, rmSync } from "node:fs";
|
|
22
|
+
import { scaffold, type ScaffoldResult, type ScaffoldSource } from "./scaffold.ts";
|
|
11
23
|
import {
|
|
12
24
|
DEFAULT_TEMPLATE,
|
|
25
|
+
EXAMPLE_NEW_IDEAS,
|
|
26
|
+
EXAMPLES,
|
|
27
|
+
TEMPLATE_PURPOSES,
|
|
13
28
|
TEMPLATES,
|
|
29
|
+
isExampleId,
|
|
14
30
|
isTemplateId,
|
|
31
|
+
type ExampleId,
|
|
15
32
|
type TemplateId,
|
|
16
33
|
} from "./templates.ts";
|
|
17
34
|
|
|
@@ -19,10 +36,40 @@ import {
|
|
|
19
36
|
export type CliArgs = {
|
|
20
37
|
readonly name: string | undefined;
|
|
21
38
|
readonly template: TemplateId;
|
|
39
|
+
readonly fromExample: ExampleId | undefined;
|
|
40
|
+
/** True when `--template` / `-t` was present on the argv. */
|
|
41
|
+
readonly templateExplicit: boolean;
|
|
22
42
|
readonly help: boolean;
|
|
23
43
|
readonly targetDir: string | undefined;
|
|
24
44
|
};
|
|
25
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Sentinel choice for "Start from a worked example" in interactive answers.
|
|
48
|
+
* Distinct from clack's internal select value (`__example__`).
|
|
49
|
+
*/
|
|
50
|
+
export const FROM_EXAMPLE_CHOICE = "from-example" as const;
|
|
51
|
+
|
|
52
|
+
/** Answers collected by the interactive ask step (no clack types). */
|
|
53
|
+
export type InteractiveAnswers =
|
|
54
|
+
| {
|
|
55
|
+
readonly name: string;
|
|
56
|
+
readonly choice: TemplateId;
|
|
57
|
+
}
|
|
58
|
+
| {
|
|
59
|
+
readonly name: string;
|
|
60
|
+
readonly choice: typeof FROM_EXAMPLE_CHOICE;
|
|
61
|
+
readonly example: ExampleId;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Canonical scaffold invocation — shared by interactive and flag-driven paths.
|
|
66
|
+
*/
|
|
67
|
+
export type ScaffoldCallArgs = {
|
|
68
|
+
readonly name: string;
|
|
69
|
+
readonly targetDir: string;
|
|
70
|
+
readonly source: ScaffoldSource;
|
|
71
|
+
};
|
|
72
|
+
|
|
26
73
|
/**
|
|
27
74
|
* Parse argv after the binary name.
|
|
28
75
|
*
|
|
@@ -31,6 +78,8 @@ export type CliArgs = {
|
|
|
31
78
|
export function parseArgs(argv: readonly string[]): CliArgs {
|
|
32
79
|
let name: string | undefined;
|
|
33
80
|
let template: TemplateId = DEFAULT_TEMPLATE;
|
|
81
|
+
let fromExample: ExampleId | undefined;
|
|
82
|
+
let templateExplicit = false;
|
|
34
83
|
let help = false;
|
|
35
84
|
let targetDir: string | undefined;
|
|
36
85
|
|
|
@@ -48,6 +97,7 @@ export function parseArgs(argv: readonly string[]): CliArgs {
|
|
|
48
97
|
);
|
|
49
98
|
}
|
|
50
99
|
template = next;
|
|
100
|
+
templateExplicit = true;
|
|
51
101
|
continue;
|
|
52
102
|
}
|
|
53
103
|
if (a.startsWith("--template=")) {
|
|
@@ -58,6 +108,27 @@ export function parseArgs(argv: readonly string[]): CliArgs {
|
|
|
58
108
|
);
|
|
59
109
|
}
|
|
60
110
|
template = value;
|
|
111
|
+
templateExplicit = true;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (a === "--from-example") {
|
|
115
|
+
const next = argv[++i];
|
|
116
|
+
if (!next || !isExampleId(next)) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`create-oke: --from-example must be one of ${EXAMPLES.join("|")}`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
fromExample = next;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (a.startsWith("--from-example=")) {
|
|
125
|
+
const value = a.slice("--from-example=".length);
|
|
126
|
+
if (!isExampleId(value)) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`create-oke: --from-example must be one of ${EXAMPLES.join("|")}`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
fromExample = value;
|
|
61
132
|
continue;
|
|
62
133
|
}
|
|
63
134
|
if (a.startsWith("-")) {
|
|
@@ -70,11 +141,17 @@ export function parseArgs(argv: readonly string[]): CliArgs {
|
|
|
70
141
|
throw new Error(`create-oke: unexpected argument ${a}`);
|
|
71
142
|
}
|
|
72
143
|
|
|
144
|
+
if (templateExplicit && fromExample !== undefined) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
"create-oke: use either --template or --from-example, not both",
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
73
150
|
if (name !== undefined) {
|
|
74
151
|
targetDir = resolve(name);
|
|
75
152
|
}
|
|
76
153
|
|
|
77
|
-
return { name, template, help, targetDir };
|
|
154
|
+
return { name, template, fromExample, templateExplicit, help, targetDir };
|
|
78
155
|
}
|
|
79
156
|
|
|
80
157
|
/**
|
|
@@ -88,32 +165,218 @@ export function formatCdPath(targetDir: string): string {
|
|
|
88
165
|
return rel;
|
|
89
166
|
}
|
|
90
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Post-scaffold next-steps block — shared by interactive and flag-driven paths.
|
|
170
|
+
*
|
|
171
|
+
* @param result - Successful scaffold result
|
|
172
|
+
*/
|
|
173
|
+
export function nextStepsText(result: ScaffoldResult): string {
|
|
174
|
+
return `
|
|
175
|
+
Scaffolded ${result.label} → ${result.targetDir}
|
|
176
|
+
|
|
177
|
+
Next steps:
|
|
178
|
+
|
|
179
|
+
cd ${formatCdPath(result.targetDir)}
|
|
180
|
+
bun install
|
|
181
|
+
oke dev # app :6530 · Console :6533 · MCP :6535
|
|
182
|
+
`;
|
|
183
|
+
}
|
|
184
|
+
|
|
91
185
|
/**
|
|
92
186
|
* Help text — next steps match four-applications.md (`bun install` · `oke dev`).
|
|
93
187
|
*/
|
|
94
188
|
export function helpText(): string {
|
|
189
|
+
const templateLines = TEMPLATES.map(
|
|
190
|
+
(id) =>
|
|
191
|
+
` ${id.padEnd(12)}${TEMPLATE_PURPOSES[id]}${id === DEFAULT_TEMPLATE ? " (default)" : ""}`,
|
|
192
|
+
).join("\n");
|
|
193
|
+
const exampleLines = EXAMPLES.map(
|
|
194
|
+
(id) => ` ${id.padEnd(12)}${EXAMPLE_NEW_IDEAS[id]}`,
|
|
195
|
+
).join("\n");
|
|
196
|
+
|
|
95
197
|
return `create-oke — scaffold an okengine app
|
|
96
198
|
|
|
97
199
|
Usage:
|
|
98
|
-
bunx create-oke@latest <name> [--template
|
|
200
|
+
bunx create-oke@latest <name> [--template hello|minimal|standard|full]
|
|
201
|
+
bunx create-oke@latest <name> --from-example notes|linkly|provisions|skyport
|
|
202
|
+
bunx create-oke@latest # interactive (TTY only)
|
|
203
|
+
|
|
204
|
+
Templates (clean starters from templates/):
|
|
205
|
+
${templateLines}
|
|
99
206
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
provisions advanced — + Vault · Channel · plugins
|
|
104
|
-
skyport complex — all eight elements · AI · tenancy
|
|
207
|
+
--from-example (copies a teaching example, including its business logic and
|
|
208
|
+
comments — most new projects want --template instead):
|
|
209
|
+
${exampleLines}
|
|
105
210
|
|
|
106
|
-
No telemetry. Bun only.
|
|
211
|
+
No telemetry. Bun only. Interactive prompts only when stdin is a TTY and no
|
|
212
|
+
name / --template / --from-example is given.
|
|
107
213
|
`;
|
|
108
214
|
}
|
|
109
215
|
|
|
216
|
+
/**
|
|
217
|
+
* Whether the CLI should open the two-question interactive flow.
|
|
218
|
+
*
|
|
219
|
+
* Mirrors oke / gflows: bare invocation is interactive only in a real terminal;
|
|
220
|
+
* any explicit flag or positional name stays fully scriptable.
|
|
221
|
+
*
|
|
222
|
+
* @param args - Parsed args
|
|
223
|
+
* @param stdinIsTTY - `process.stdin.isTTY`
|
|
224
|
+
*/
|
|
225
|
+
export function shouldPrompt(
|
|
226
|
+
args: CliArgs,
|
|
227
|
+
stdinIsTTY: boolean | undefined,
|
|
228
|
+
): boolean {
|
|
229
|
+
if (!stdinIsTTY) return false;
|
|
230
|
+
if (args.help) return false;
|
|
231
|
+
if (args.name !== undefined) return false;
|
|
232
|
+
if (args.templateExplicit) return false;
|
|
233
|
+
if (args.fromExample !== undefined) return false;
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Build the {@link ScaffoldSource} for a flag-driven invocation.
|
|
239
|
+
*
|
|
240
|
+
* @param args - Parsed args
|
|
241
|
+
*/
|
|
242
|
+
export function sourceFromArgs(args: CliArgs): ScaffoldSource {
|
|
243
|
+
if (args.fromExample !== undefined) {
|
|
244
|
+
return { kind: "example", id: args.fromExample };
|
|
245
|
+
}
|
|
246
|
+
return { kind: "template", id: args.template };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Map flag-driven {@link CliArgs} to scaffold call args.
|
|
251
|
+
*
|
|
252
|
+
* Requires a positional project name (and thus `targetDir`).
|
|
253
|
+
*
|
|
254
|
+
* @param args - Parsed CLI args with `name` set
|
|
255
|
+
*/
|
|
256
|
+
export function scaffoldArgsFromCli(args: CliArgs): ScaffoldCallArgs {
|
|
257
|
+
if (args.name === undefined || args.targetDir === undefined) {
|
|
258
|
+
throw new Error("create-oke: missing <name>");
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
name: basename(resolve(args.name)),
|
|
262
|
+
targetDir: args.targetDir,
|
|
263
|
+
source: sourceFromArgs(args),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Pure map from interactive answers → scaffold call args.
|
|
269
|
+
*
|
|
270
|
+
* Independent of clack — unit-tested against {@link scaffoldArgsFromCli}
|
|
271
|
+
* for every template and every `--from-example` choice.
|
|
272
|
+
*
|
|
273
|
+
* @param answers - Collected interactive answers
|
|
274
|
+
*/
|
|
275
|
+
export function scaffoldArgsFromAnswers(
|
|
276
|
+
answers: InteractiveAnswers,
|
|
277
|
+
): ScaffoldCallArgs {
|
|
278
|
+
const targetDir = resolve(answers.name.trim());
|
|
279
|
+
const name = basename(targetDir);
|
|
280
|
+
if (answers.choice === FROM_EXAMPLE_CHOICE) {
|
|
281
|
+
return {
|
|
282
|
+
name,
|
|
283
|
+
targetDir,
|
|
284
|
+
source: { kind: "example", id: answers.example },
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
name,
|
|
289
|
+
targetDir,
|
|
290
|
+
source: { kind: "template", id: answers.choice },
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Ask step — clack prompts only. Returns answers or `null` on cancel.
|
|
296
|
+
*
|
|
297
|
+
* Injectable for tests; production uses {@link askInteractiveAnswers}.
|
|
298
|
+
*
|
|
299
|
+
* @param partial - Name already known (skipped in the prompt)
|
|
300
|
+
*/
|
|
301
|
+
export type AskInteractiveFn = (
|
|
302
|
+
partial: { readonly name?: string },
|
|
303
|
+
) => Promise<InteractiveAnswers | null>;
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Collect interactive answers via `@clack/prompts`.
|
|
307
|
+
*
|
|
308
|
+
* @param partial - Optional pre-filled project name
|
|
309
|
+
* @returns Answers, or `null` if the user cancelled
|
|
310
|
+
*/
|
|
311
|
+
export async function askInteractiveAnswers(
|
|
312
|
+
partial: { readonly name?: string } = {},
|
|
313
|
+
): Promise<InteractiveAnswers | null> {
|
|
314
|
+
let name = partial.name;
|
|
315
|
+
if (name === undefined) {
|
|
316
|
+
const nameValue = await text({
|
|
317
|
+
message: "Project name",
|
|
318
|
+
placeholder: "my-app",
|
|
319
|
+
validate: (value) => {
|
|
320
|
+
if (!value?.trim()) return "Project name is required";
|
|
321
|
+
return undefined;
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
if (isCancel(nameValue)) return null;
|
|
325
|
+
name = String(nameValue).trim();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const templateValue = await select({
|
|
329
|
+
message: "Template",
|
|
330
|
+
options: [
|
|
331
|
+
...TEMPLATES.map((id) => ({
|
|
332
|
+
value: id as string,
|
|
333
|
+
label: id,
|
|
334
|
+
hint: TEMPLATE_PURPOSES[id],
|
|
335
|
+
})),
|
|
336
|
+
{
|
|
337
|
+
value: "__example__",
|
|
338
|
+
label: "Start from a worked example",
|
|
339
|
+
hint: "Teaching apps with business logic — most projects want a template",
|
|
340
|
+
},
|
|
341
|
+
],
|
|
342
|
+
initialValue: DEFAULT_TEMPLATE,
|
|
343
|
+
});
|
|
344
|
+
if (isCancel(templateValue)) return null;
|
|
345
|
+
|
|
346
|
+
if (templateValue === "__example__") {
|
|
347
|
+
const exampleValue = await select({
|
|
348
|
+
message: "Example",
|
|
349
|
+
options: EXAMPLES.map((id) => ({
|
|
350
|
+
value: id,
|
|
351
|
+
label: id,
|
|
352
|
+
hint: EXAMPLE_NEW_IDEAS[id],
|
|
353
|
+
})),
|
|
354
|
+
});
|
|
355
|
+
if (isCancel(exampleValue)) return null;
|
|
356
|
+
return {
|
|
357
|
+
name,
|
|
358
|
+
choice: FROM_EXAMPLE_CHOICE,
|
|
359
|
+
example: exampleValue as ExampleId,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return { name, choice: templateValue as TemplateId };
|
|
364
|
+
}
|
|
365
|
+
|
|
110
366
|
/**
|
|
111
367
|
* Run the CLI.
|
|
112
368
|
*
|
|
113
369
|
* @param argv - Args after the binary
|
|
370
|
+
* @param options - Test seams (stdin TTY, ask injection)
|
|
114
371
|
* @returns Exit code
|
|
115
372
|
*/
|
|
116
|
-
export async function run(
|
|
373
|
+
export async function run(
|
|
374
|
+
argv: readonly string[],
|
|
375
|
+
options: {
|
|
376
|
+
readonly stdinIsTTY?: boolean | undefined;
|
|
377
|
+
readonly ask?: AskInteractiveFn;
|
|
378
|
+
} = {},
|
|
379
|
+
): Promise<number> {
|
|
117
380
|
let args: CliArgs;
|
|
118
381
|
try {
|
|
119
382
|
args = parseArgs(argv);
|
|
@@ -122,35 +385,98 @@ export async function run(argv: readonly string[]): Promise<number> {
|
|
|
122
385
|
return 1;
|
|
123
386
|
}
|
|
124
387
|
|
|
125
|
-
if (args.help
|
|
388
|
+
if (args.help) {
|
|
126
389
|
console.log(helpText());
|
|
127
|
-
|
|
128
|
-
console.error("create-oke: missing <name>");
|
|
129
|
-
return 1;
|
|
130
|
-
}
|
|
131
|
-
return args.help ? 0 : 1;
|
|
390
|
+
return 0;
|
|
132
391
|
}
|
|
133
392
|
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
name: projectName,
|
|
139
|
-
template: args.template,
|
|
140
|
-
});
|
|
393
|
+
const stdinIsTTY = options.stdinIsTTY ?? process.stdin.isTTY;
|
|
394
|
+
if (shouldPrompt(args, stdinIsTTY)) {
|
|
395
|
+
return runInteractive(args, options.ask ?? askInteractiveAnswers);
|
|
396
|
+
}
|
|
141
397
|
|
|
142
|
-
|
|
143
|
-
|
|
398
|
+
if (args.name === undefined) {
|
|
399
|
+
console.log(helpText());
|
|
400
|
+
console.error("create-oke: missing <name>");
|
|
401
|
+
return 1;
|
|
402
|
+
}
|
|
144
403
|
|
|
145
|
-
|
|
404
|
+
return runScaffold({
|
|
405
|
+
...scaffoldArgsFromCli(args),
|
|
406
|
+
interactive: false,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
146
409
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
410
|
+
/**
|
|
411
|
+
* Interactive TTY flow — ask → map answers → scaffold.
|
|
412
|
+
*
|
|
413
|
+
* @param args - Parsed args (name may already be set if ever called that way)
|
|
414
|
+
* @param ask - Injectable ask step
|
|
415
|
+
*/
|
|
416
|
+
async function runInteractive(
|
|
417
|
+
args: CliArgs,
|
|
418
|
+
ask: AskInteractiveFn,
|
|
419
|
+
): Promise<number> {
|
|
420
|
+
intro("create-oke");
|
|
421
|
+
|
|
422
|
+
const answers = await ask({ name: args.name });
|
|
423
|
+
if (answers === null) {
|
|
424
|
+
cancel("Cancelled.");
|
|
425
|
+
return 1;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return runScaffold({
|
|
429
|
+
...scaffoldArgsFromAnswers(answers),
|
|
430
|
+
interactive: true,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Scaffold with optional spinner / outro, cleaning up on failure or cancel.
|
|
436
|
+
*
|
|
437
|
+
* @param options - Scaffold inputs + interactive flag
|
|
438
|
+
*/
|
|
439
|
+
async function runScaffold(
|
|
440
|
+
options: ScaffoldCallArgs & { readonly interactive: boolean },
|
|
441
|
+
): Promise<number> {
|
|
442
|
+
const { name, targetDir, source, interactive } = options;
|
|
443
|
+
const existed = existsSync(targetDir);
|
|
444
|
+
let spun: ReturnType<typeof spinner> | undefined;
|
|
445
|
+
|
|
446
|
+
const cleanup = (): void => {
|
|
447
|
+
if (!existed && existsSync(targetDir)) {
|
|
448
|
+
rmSync(targetDir, { recursive: true, force: true });
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
const onSigInt = (): void => {
|
|
453
|
+
spun?.stop("Cancelled.");
|
|
454
|
+
cleanup();
|
|
455
|
+
if (interactive) cancel("Cancelled.");
|
|
456
|
+
process.exit(1);
|
|
457
|
+
};
|
|
458
|
+
process.once("SIGINT", onSigInt);
|
|
459
|
+
|
|
460
|
+
try {
|
|
461
|
+
if (interactive) {
|
|
462
|
+
spun = spinner();
|
|
463
|
+
spun.start("Scaffolding…");
|
|
464
|
+
}
|
|
465
|
+
const result = scaffold({ targetDir, name, source });
|
|
466
|
+
if (spun) spun.stop("Scaffolded.");
|
|
467
|
+
const message = nextStepsText(result);
|
|
468
|
+
if (interactive) {
|
|
469
|
+
outro(message.trim());
|
|
470
|
+
} else {
|
|
471
|
+
console.log(message);
|
|
472
|
+
}
|
|
151
473
|
return 0;
|
|
152
474
|
} catch (e) {
|
|
475
|
+
spun?.stop("Failed.");
|
|
476
|
+
cleanup();
|
|
153
477
|
console.error(e instanceof Error ? e.message : e);
|
|
154
478
|
return 1;
|
|
479
|
+
} finally {
|
|
480
|
+
process.off("SIGINT", onSigInt);
|
|
155
481
|
}
|
|
156
482
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
/**
|
|
3
|
-
* `create-oke` binary entry.
|
|
3
|
+
* `create-oke` binary entry — scaffold an okengine app.
|
|
4
|
+
*
|
|
5
|
+
* ```bash
|
|
6
|
+
* bunx create-oke@latest my-app
|
|
7
|
+
* bunx create-oke@latest my-app --template hello
|
|
8
|
+
* bunx create-oke@latest my-app --from-example notes
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
4
12
|
*/
|
|
5
13
|
|
|
6
14
|
import { run } from "./cli.ts";
|
package/src/scaffold.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Copy
|
|
2
|
+
* Copy a template or example tree and apply the package.json transform.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import {
|
|
@@ -8,13 +8,16 @@ import {
|
|
|
8
8
|
mkdirSync,
|
|
9
9
|
readdirSync,
|
|
10
10
|
readFileSync,
|
|
11
|
+
rmSync,
|
|
11
12
|
statSync,
|
|
12
13
|
writeFileSync,
|
|
13
14
|
} from "node:fs";
|
|
14
15
|
import { join, relative, resolve } from "node:path";
|
|
15
16
|
import {
|
|
17
|
+
resolveExampleDir,
|
|
16
18
|
resolveLocalOkengineRoot,
|
|
17
19
|
resolveTemplateDir,
|
|
20
|
+
type ExampleId,
|
|
18
21
|
type TemplateId,
|
|
19
22
|
} from "./templates.ts";
|
|
20
23
|
import {
|
|
@@ -25,35 +28,46 @@ import {
|
|
|
25
28
|
type ScaffoldPackageJson,
|
|
26
29
|
} from "./transform.ts";
|
|
27
30
|
|
|
31
|
+
/** Where the scaffold copies from. */
|
|
32
|
+
export type ScaffoldSource =
|
|
33
|
+
| { readonly kind: "template"; readonly id: TemplateId }
|
|
34
|
+
| { readonly kind: "example"; readonly id: ExampleId };
|
|
35
|
+
|
|
28
36
|
/** Options for {@link scaffold}. */
|
|
29
37
|
export type ScaffoldOptions = {
|
|
30
38
|
/** Destination directory (created). Absolute or cwd-relative. */
|
|
31
39
|
readonly targetDir: string;
|
|
32
40
|
/** npm package / folder name. */
|
|
33
41
|
readonly name: string;
|
|
34
|
-
/** Template
|
|
35
|
-
readonly
|
|
42
|
+
/** Template or teaching-example source. */
|
|
43
|
+
readonly source: ScaffoldSource;
|
|
36
44
|
};
|
|
37
45
|
|
|
38
46
|
/** Result of a successful scaffold. */
|
|
39
47
|
export type ScaffoldResult = {
|
|
40
48
|
readonly targetDir: string;
|
|
41
49
|
readonly name: string;
|
|
42
|
-
readonly
|
|
50
|
+
readonly source: ScaffoldSource;
|
|
51
|
+
/** Display label (`standard`, `notes`, …). */
|
|
52
|
+
readonly label: string;
|
|
43
53
|
readonly okengineDependency: string;
|
|
44
54
|
/** Relative paths written (POSIX), sorted. */
|
|
45
55
|
readonly files: readonly string[];
|
|
46
56
|
};
|
|
47
57
|
|
|
48
58
|
/**
|
|
49
|
-
* Scaffold a new okengine project from
|
|
59
|
+
* Scaffold a new okengine project from a clean template or teaching example.
|
|
50
60
|
*
|
|
51
|
-
* @param options - Name,
|
|
61
|
+
* @param options - Name, source, destination
|
|
52
62
|
*/
|
|
53
63
|
export function scaffold(options: ScaffoldOptions): ScaffoldResult {
|
|
54
64
|
const name = sanitizeProjectName(options.name);
|
|
55
65
|
const targetDir = resolve(options.targetDir);
|
|
56
|
-
const
|
|
66
|
+
const sourceDir =
|
|
67
|
+
options.source.kind === "template"
|
|
68
|
+
? resolveTemplateDir(options.source.id)
|
|
69
|
+
: resolveExampleDir(options.source.id);
|
|
70
|
+
const label = options.source.id;
|
|
57
71
|
|
|
58
72
|
if (existsSync(targetDir)) {
|
|
59
73
|
const entries = readdirSync(targetDir);
|
|
@@ -66,33 +80,37 @@ export function scaffold(options: ScaffoldOptions): ScaffoldResult {
|
|
|
66
80
|
mkdirSync(targetDir, { recursive: true });
|
|
67
81
|
}
|
|
68
82
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
83
|
+
try {
|
|
84
|
+
const okengineDependency = resolveOkengineDependency(
|
|
85
|
+
resolveLocalOkengineRoot(),
|
|
86
|
+
);
|
|
87
|
+
const written: string[] = [];
|
|
73
88
|
|
|
74
|
-
|
|
89
|
+
copyTree(sourceDir, targetDir, "", written);
|
|
75
90
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const nextPkg = transformPackageJson(sourcePkg, name, okengineDependency);
|
|
86
|
-
writeFileSync(pkgPath, `${JSON.stringify(nextPkg, null, 2)}\n`, "utf8");
|
|
91
|
+
const pkgPath = join(targetDir, "package.json");
|
|
92
|
+
if (!existsSync(pkgPath)) {
|
|
93
|
+
throw new Error(`create-oke: source "${label}" has no package.json`);
|
|
94
|
+
}
|
|
95
|
+
const sourcePkg = JSON.parse(
|
|
96
|
+
readFileSync(pkgPath, "utf8"),
|
|
97
|
+
) as ScaffoldPackageJson;
|
|
98
|
+
const nextPkg = transformPackageJson(sourcePkg, name, okengineDependency);
|
|
99
|
+
writeFileSync(pkgPath, `${JSON.stringify(nextPkg, null, 2)}\n`, "utf8");
|
|
87
100
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
101
|
+
written.sort();
|
|
102
|
+
return {
|
|
103
|
+
targetDir,
|
|
104
|
+
name,
|
|
105
|
+
source: options.source,
|
|
106
|
+
label,
|
|
107
|
+
okengineDependency,
|
|
108
|
+
files: written,
|
|
109
|
+
};
|
|
110
|
+
} catch (e) {
|
|
111
|
+
rmSync(targetDir, { recursive: true, force: true });
|
|
112
|
+
throw e;
|
|
113
|
+
}
|
|
96
114
|
}
|
|
97
115
|
|
|
98
116
|
/**
|
|
@@ -129,7 +147,7 @@ function copyTree(
|
|
|
129
147
|
}
|
|
130
148
|
|
|
131
149
|
/**
|
|
132
|
-
* List relative file paths in
|
|
150
|
+
* List relative file paths in a source tree using the same skip rules as scaffold.
|
|
133
151
|
*
|
|
134
152
|
* @param templateDir - Absolute template directory
|
|
135
153
|
*/
|