create-oke 0.6.0 → 0.7.0
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 +23 -8
- package/package.json +3 -3
- package/src/ai-setup/apply.ts +214 -0
- package/src/ai-setup/catalog.ts +263 -0
- package/src/ai-setup/detect-ollama.ts +180 -0
- package/src/ai-setup/from-pref.ts +99 -0
- package/src/ai-setup/prompts.ts +649 -0
- package/src/ai-setup/recommend.ts +203 -0
- package/src/cli.test.ts +385 -28
- package/src/cli.ts +332 -29
- package/src/create-defaults.test.ts +92 -0
- package/src/create-defaults.ts +246 -0
- package/src/customize-flow.test.ts +82 -0
- package/src/customize-flow.ts +432 -0
- package/src/drivers-catalog.ts +217 -0
- package/src/scaffold.ts +97 -10
- package/src/templates.ts +14 -9
- package/src/transform.test.ts +105 -0
- package/src/transform.ts +227 -0
- package/src/wizard-select.ts +50 -0
- package/{template → templates/advanced}/.env.example +40 -2
- package/templates/advanced/README.md +32 -0
- package/templates/advanced/oke.config.ts +74 -0
- package/templates/advanced/package.json +18 -0
- package/templates/advanced/src/app.ts +30 -0
- package/templates/advanced/src/channels.ts +13 -0
- package/templates/advanced/src/core.ts +8 -0
- package/{template → templates/advanced}/src/flows/main/index.ts +9 -6
- package/templates/advanced/src/flows/main/shapes.ts +1 -0
- package/templates/advanced/src/flows/main/signals.ts +1 -0
- package/templates/advanced/src/flows/notes/index.ts +224 -0
- package/templates/advanced/src/flows/notes/shapes.ts +52 -0
- package/templates/advanced/src/flows/notes/signals.ts +13 -0
- package/templates/advanced/src/gates.ts +12 -0
- package/templates/advanced/src/locales/ar.ts +18 -0
- package/templates/advanced/src/locales/en.ts +22 -0
- package/templates/advanced/src/schema.decl.ts +13 -0
- package/templates/advanced/src/vault.ts +7 -0
- package/templates/advanced/tests/advanced.test.ts +42 -0
- package/templates/standard/.env.example +94 -0
- package/templates/standard/README.md +46 -0
- package/templates/standard/drizzle.config.ts +25 -0
- package/{template → templates/standard}/oke.config.ts +9 -9
- package/{template → templates/standard}/package.json +1 -1
- package/templates/standard/src/app.ts +30 -0
- package/templates/standard/src/channels.ts +13 -0
- package/{template → templates/standard}/src/core.ts +1 -0
- package/templates/standard/src/flows/main/index.ts +34 -0
- package/templates/standard/src/flows/main/shapes.ts +1 -0
- package/templates/standard/src/flows/main/signals.ts +1 -0
- package/templates/standard/src/flows/notes/index.ts +143 -0
- package/templates/standard/src/flows/notes/shapes.ts +26 -0
- package/templates/standard/src/flows/notes/signals.ts +13 -0
- package/templates/standard/src/gates.ts +12 -0
- package/templates/standard/src/locales/ar.ts +18 -0
- package/templates/standard/src/locales/en.ts +22 -0
- package/templates/standard/src/schema.decl.ts +13 -0
- package/templates/standard/src/vault.ts +7 -0
- package/templates/standard/tests/standard.test.ts +51 -0
- package/template/README.md +0 -77
- package/template/bun.lock +0 -845
- package/template/src/app.ts +0 -18
- package/template/src/channels.ts +0 -4
- package/template/src/flows/main/shapes.ts +0 -2
- package/template/src/flows/main/signals.ts +0 -4
- package/template/src/gates.ts +0 -4
- package/template/src/locales/ar.ts +0 -20
- package/template/src/locales/en.ts +0 -26
- package/template/src/schema.decl.ts +0 -12
- package/template/src/vault.ts +0 -4
- package/template/tests/standard.test.ts +0 -18
- /package/{template → templates/advanced}/drizzle.config.ts +0 -0
package/src/cli.ts
CHANGED
|
@@ -1,19 +1,50 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `create-oke` CLI —
|
|
2
|
+
* `create-oke` CLI — standard|advanced Notes starters with recommended / reuse / customize.
|
|
3
3
|
*
|
|
4
4
|
* ```bash
|
|
5
|
-
* bunx create-oke@latest <name> [--template standard]
|
|
5
|
+
* bunx create-oke@latest <name> [--template standard|advanced]
|
|
6
6
|
* bunx create-oke@latest <name> --yes
|
|
7
7
|
* bunx create-oke@latest # interactive when stdin is a TTY
|
|
8
8
|
* ```
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
cancel,
|
|
13
|
+
confirm,
|
|
14
|
+
intro,
|
|
15
|
+
isCancel,
|
|
16
|
+
log,
|
|
17
|
+
note,
|
|
18
|
+
outro,
|
|
19
|
+
select,
|
|
20
|
+
spinner,
|
|
21
|
+
text,
|
|
22
|
+
} from "@clack/prompts";
|
|
12
23
|
import { basename, relative, resolve } from "node:path";
|
|
13
24
|
import { existsSync, rmSync } from "node:fs";
|
|
14
25
|
import { agentsMdContent } from "./agents-md.ts";
|
|
26
|
+
import {
|
|
27
|
+
createDefaultsPath,
|
|
28
|
+
readCreateDefaults,
|
|
29
|
+
writeCreateDefaults,
|
|
30
|
+
type CreateDefaults,
|
|
31
|
+
type EnvDriverPins,
|
|
32
|
+
} from "./create-defaults.ts";
|
|
15
33
|
import { docsUrl } from "./docs-origin.ts";
|
|
16
|
-
import {
|
|
34
|
+
import { applyAiSetup, type AiSetupApplyInput } from "./ai-setup/apply.ts";
|
|
35
|
+
import {
|
|
36
|
+
aiPrefWithModels,
|
|
37
|
+
applyInputFromAiPref,
|
|
38
|
+
nonInteractiveAiApply,
|
|
39
|
+
} from "./ai-setup/from-pref.ts";
|
|
40
|
+
import { askAiSetup } from "./ai-setup/prompts.ts";
|
|
41
|
+
import { askCustomizeFlow } from "./customize-flow.ts";
|
|
42
|
+
import {
|
|
43
|
+
scaffold,
|
|
44
|
+
targetDirectoryBlockReason,
|
|
45
|
+
type ScaffoldResult,
|
|
46
|
+
type ScaffoldSource,
|
|
47
|
+
} from "./scaffold.ts";
|
|
17
48
|
import {
|
|
18
49
|
DEFAULT_TEMPLATE,
|
|
19
50
|
TEMPLATE_PURPOSES,
|
|
@@ -22,6 +53,11 @@ import {
|
|
|
22
53
|
type TemplateId,
|
|
23
54
|
} from "./templates.ts";
|
|
24
55
|
import { DEFAULT_SQL_DRIVER, SQL_DRIVERS, isSqlDriverId, type SqlDriverId } from "./transform.ts";
|
|
56
|
+
import { WIZARD_BACK } from "./wizard-select.ts";
|
|
57
|
+
export { WIZARD_BACK, selectWithBack, withBackOption, type WizardBack } from "./wizard-select.ts";
|
|
58
|
+
|
|
59
|
+
/** How AI setup is requested on the CLI. */
|
|
60
|
+
export type AiCliMode = "prompt" | "skip" | "force";
|
|
25
61
|
|
|
26
62
|
/** Parsed CLI arguments. */
|
|
27
63
|
export type CliArgs = {
|
|
@@ -43,15 +79,70 @@ export type CliArgs = {
|
|
|
43
79
|
readonly install: boolean | undefined;
|
|
44
80
|
/** Write `AGENTS.md` (default true). */
|
|
45
81
|
readonly agentsMd: boolean;
|
|
82
|
+
/** AI setup after install. */
|
|
83
|
+
readonly ai: AiCliMode;
|
|
46
84
|
readonly targetDir: string | undefined;
|
|
47
85
|
};
|
|
48
86
|
|
|
87
|
+
/** Defaults-branch choice after project name. */
|
|
88
|
+
export type DefaultsBranch = "recommended" | "reuse" | "customize";
|
|
89
|
+
|
|
90
|
+
/** One Clack option for the recommended / reuse / customize branch. */
|
|
91
|
+
export type DefaultsBranchOption = {
|
|
92
|
+
readonly value: DefaultsBranch;
|
|
93
|
+
readonly label: string;
|
|
94
|
+
readonly hint: string;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Build the defaults-branch menu. **Reuse** appears only when previous
|
|
99
|
+
* settings exist for the selected template.
|
|
100
|
+
*
|
|
101
|
+
* @param hasPreviousForTemplate - Matching create-defaults on disk
|
|
102
|
+
* @param template - Selected starter
|
|
103
|
+
*/
|
|
104
|
+
export function defaultsBranchOptions(
|
|
105
|
+
hasPreviousForTemplate: boolean,
|
|
106
|
+
template: TemplateId = "standard",
|
|
107
|
+
): DefaultsBranchOption[] {
|
|
108
|
+
const options: DefaultsBranchOption[] = [
|
|
109
|
+
{
|
|
110
|
+
value: "recommended",
|
|
111
|
+
label: "★ Yes, use recommended defaults",
|
|
112
|
+
hint:
|
|
113
|
+
template === "advanced"
|
|
114
|
+
? "Notes · docker-ready pins · .oke/mode docker"
|
|
115
|
+
: "Notes · local-first pins · .oke/mode local",
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
if (hasPreviousForTemplate) {
|
|
119
|
+
options.push({
|
|
120
|
+
value: "reuse",
|
|
121
|
+
label: "↻ No, reuse previous settings",
|
|
122
|
+
hint: `~/.oke/create-defaults.json (${template})`,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
options.push({
|
|
126
|
+
value: "customize",
|
|
127
|
+
label: "◆ No, customize settings",
|
|
128
|
+
hint:
|
|
129
|
+
template === "standard"
|
|
130
|
+
? "local|docker · SQL · optional AI"
|
|
131
|
+
: "local|docker · all facets · optional AI",
|
|
132
|
+
});
|
|
133
|
+
return options;
|
|
134
|
+
}
|
|
135
|
+
|
|
49
136
|
/** Answers collected by the interactive ask step (no clack types). */
|
|
50
137
|
export type InteractiveAnswers = {
|
|
51
138
|
readonly name: string;
|
|
52
139
|
readonly choice: TemplateId;
|
|
53
140
|
readonly installAndRun: boolean;
|
|
54
141
|
readonly agentsMd: boolean;
|
|
142
|
+
/** `undefined` = recommended template defaults (no createDefaults apply). */
|
|
143
|
+
readonly createDefaults: CreateDefaults | undefined;
|
|
144
|
+
/** Model / env apply payload — written after scaffold, before install. */
|
|
145
|
+
readonly aiApply: AiSetupApplyInput | null;
|
|
55
146
|
};
|
|
56
147
|
|
|
57
148
|
/**
|
|
@@ -63,6 +154,9 @@ export type ScaffoldCallArgs = {
|
|
|
63
154
|
readonly source: ScaffoldSource;
|
|
64
155
|
readonly agentsMd: boolean;
|
|
65
156
|
readonly sqlDriver: SqlDriverId;
|
|
157
|
+
readonly createDefaults?: CreateDefaults;
|
|
158
|
+
/** Apply `src/ai.ts` + `.env.local` after scaffold (before install). */
|
|
159
|
+
readonly aiApply?: AiSetupApplyInput | null;
|
|
66
160
|
};
|
|
67
161
|
|
|
68
162
|
/**
|
|
@@ -80,6 +174,7 @@ export function parseArgs(argv: readonly string[]): CliArgs {
|
|
|
80
174
|
let yes = false;
|
|
81
175
|
let install: boolean | undefined;
|
|
82
176
|
let agentsMd = true;
|
|
177
|
+
let ai: AiCliMode = "prompt";
|
|
83
178
|
let targetDir: string | undefined;
|
|
84
179
|
|
|
85
180
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -108,6 +203,26 @@ export function parseArgs(argv: readonly string[]): CliArgs {
|
|
|
108
203
|
agentsMd = true;
|
|
109
204
|
continue;
|
|
110
205
|
}
|
|
206
|
+
if (a === "--no-ai" || a === "--ai=skip") {
|
|
207
|
+
ai = "skip";
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (a === "--ai") {
|
|
211
|
+
const next = argv[i + 1];
|
|
212
|
+
if (next === "skip") {
|
|
213
|
+
i++;
|
|
214
|
+
ai = "skip";
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
ai = "force";
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (a.startsWith("--ai=")) {
|
|
221
|
+
const value = a.slice("--ai=".length);
|
|
222
|
+
if (value === "skip") ai = "skip";
|
|
223
|
+
else ai = "force";
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
111
226
|
if (a === "--sql") {
|
|
112
227
|
const next = argv[++i];
|
|
113
228
|
if (!next || !isSqlDriverId(next)) {
|
|
@@ -172,6 +287,7 @@ export function parseArgs(argv: readonly string[]): CliArgs {
|
|
|
172
287
|
yes,
|
|
173
288
|
install,
|
|
174
289
|
agentsMd,
|
|
290
|
+
ai,
|
|
175
291
|
targetDir,
|
|
176
292
|
};
|
|
177
293
|
}
|
|
@@ -217,7 +333,7 @@ export function helpText(): string {
|
|
|
217
333
|
return `create-oke — scaffold an okengine app
|
|
218
334
|
|
|
219
335
|
Usage:
|
|
220
|
-
bunx create-oke@latest <name> [--template standard]
|
|
336
|
+
bunx create-oke@latest <name> [--template standard|advanced]
|
|
221
337
|
bunx create-oke@latest <name> --yes
|
|
222
338
|
bunx create-oke@latest # interactive (TTY only)
|
|
223
339
|
|
|
@@ -231,22 +347,22 @@ Options:
|
|
|
231
347
|
--no-install Skip bun install
|
|
232
348
|
--agents-md Write AGENTS.md (default)
|
|
233
349
|
--no-agents-md Skip AGENTS.md
|
|
350
|
+
--ai Configure AI in the wizard (models before install)
|
|
351
|
+
--no-ai, --ai skip Skip AI configuration
|
|
234
352
|
-h, --help Show this help
|
|
235
353
|
|
|
236
354
|
Template:
|
|
237
355
|
${templateLines}
|
|
238
356
|
|
|
239
|
-
|
|
240
|
-
(
|
|
357
|
+
On a TTY: pick standard|advanced, then recommended defaults, customize
|
|
358
|
+
(local or docker first; optional other side; saved to ~/.oke/create-defaults.json),
|
|
359
|
+
or reuse when saved for that template. Non-TTY / --yes stay zero-prompt.
|
|
241
360
|
`;
|
|
242
361
|
}
|
|
243
362
|
|
|
244
363
|
/**
|
|
245
364
|
* Whether the CLI should open the interactive Clack flow.
|
|
246
365
|
*
|
|
247
|
-
* TTY humans get the wizard even when a name is pre-filled. Config flags
|
|
248
|
-
* `--template` or `--yes` skips prompts for CI/agents.
|
|
249
|
-
*
|
|
250
366
|
* @param args - Parsed args
|
|
251
367
|
* @param stdinIsTTY - `process.stdin.isTTY`
|
|
252
368
|
*/
|
|
@@ -270,8 +386,6 @@ export function sourceFromArgs(args: CliArgs): ScaffoldSource {
|
|
|
270
386
|
/**
|
|
271
387
|
* Map flag-driven {@link CliArgs} to scaffold call args.
|
|
272
388
|
*
|
|
273
|
-
* Requires a positional project name (and thus `targetDir`).
|
|
274
|
-
*
|
|
275
389
|
* @param args - Parsed CLI args with `name` set
|
|
276
390
|
*/
|
|
277
391
|
export function scaffoldArgsFromCli(args: CliArgs): ScaffoldCallArgs {
|
|
@@ -284,51 +398,99 @@ export function scaffoldArgsFromCli(args: CliArgs): ScaffoldCallArgs {
|
|
|
284
398
|
source: sourceFromArgs(args),
|
|
285
399
|
agentsMd: args.agentsMd,
|
|
286
400
|
sqlDriver: args.sqlDriver,
|
|
401
|
+
aiApply: args.ai === "force" ? nonInteractiveAiApply("ollama") : null,
|
|
287
402
|
};
|
|
288
403
|
}
|
|
289
404
|
|
|
290
405
|
/**
|
|
291
406
|
* Pure map from interactive answers → scaffold call args.
|
|
292
407
|
*
|
|
293
|
-
* Independent of clack — unit-tested against {@link scaffoldArgsFromCli}
|
|
294
|
-
* for the standard template.
|
|
295
|
-
*
|
|
296
408
|
* @param answers - Collected interactive answers
|
|
409
|
+
* @param sqlDriverOverride - Optional `--sql` flag merge
|
|
297
410
|
*/
|
|
298
|
-
export function scaffoldArgsFromAnswers(
|
|
411
|
+
export function scaffoldArgsFromAnswers(
|
|
412
|
+
answers: InteractiveAnswers,
|
|
413
|
+
sqlDriverOverride?: SqlDriverId,
|
|
414
|
+
): ScaffoldCallArgs {
|
|
299
415
|
const targetDir = resolve(answers.name.trim());
|
|
300
416
|
const name = basename(targetDir);
|
|
301
|
-
|
|
302
|
-
|
|
417
|
+
let createDefaults = answers.createDefaults;
|
|
418
|
+
let sqlDriver: SqlDriverId = DEFAULT_SQL_DRIVER;
|
|
419
|
+
|
|
420
|
+
if (createDefaults && sqlDriverOverride === "postgres") {
|
|
421
|
+
createDefaults = {
|
|
422
|
+
...createDefaults,
|
|
423
|
+
drivers: {
|
|
424
|
+
...createDefaults.drivers,
|
|
425
|
+
store: {
|
|
426
|
+
...createDefaults.drivers.store,
|
|
427
|
+
sql: {
|
|
428
|
+
local: "postgres",
|
|
429
|
+
docker: "postgres",
|
|
430
|
+
test: createDefaults.drivers.store.sql.test,
|
|
431
|
+
prod: "postgres",
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
},
|
|
435
|
+
};
|
|
436
|
+
sqlDriver = "postgres";
|
|
437
|
+
} else if (!createDefaults && sqlDriverOverride) {
|
|
438
|
+
sqlDriver = sqlDriverOverride;
|
|
439
|
+
} else if (createDefaults?.drivers.store.sql.local === "postgres") {
|
|
440
|
+
sqlDriver = "postgres";
|
|
441
|
+
}
|
|
442
|
+
|
|
303
443
|
return {
|
|
304
444
|
name,
|
|
305
445
|
targetDir,
|
|
306
446
|
source: { kind: "template", id: answers.choice },
|
|
307
447
|
agentsMd: answers.agentsMd,
|
|
308
|
-
sqlDriver
|
|
448
|
+
sqlDriver,
|
|
449
|
+
...(createDefaults !== undefined ? { createDefaults } : {}),
|
|
450
|
+
aiApply: answers.aiApply,
|
|
309
451
|
};
|
|
310
452
|
}
|
|
311
453
|
|
|
312
454
|
/**
|
|
313
455
|
* Ask step — clack prompts only. Returns answers or `null` on cancel.
|
|
314
456
|
*
|
|
315
|
-
* Injectable for tests; production uses {@link askInteractiveAnswers}.
|
|
316
|
-
*
|
|
317
457
|
* @param partial - Name already known (pre-filled in the prompt)
|
|
318
458
|
*/
|
|
319
459
|
export type AskInteractiveFn = (partial: {
|
|
320
460
|
readonly name?: string;
|
|
321
461
|
readonly agentsMd?: boolean;
|
|
462
|
+
readonly sqlDriver?: SqlDriverId;
|
|
463
|
+
readonly ai?: AiCliMode;
|
|
464
|
+
readonly template?: TemplateId;
|
|
322
465
|
}) => Promise<InteractiveAnswers | null>;
|
|
323
466
|
|
|
467
|
+
/** Injectable persistence seams for tests. */
|
|
468
|
+
export type CreateDefaultsIo = {
|
|
469
|
+
readonly path: string;
|
|
470
|
+
readonly read: () => CreateDefaults | null;
|
|
471
|
+
readonly write: (defaults: CreateDefaults) => void;
|
|
472
|
+
};
|
|
473
|
+
|
|
324
474
|
/**
|
|
325
475
|
* Collect interactive answers via `@clack/prompts`.
|
|
326
476
|
*
|
|
327
477
|
* @param partial - Optional pre-filled project name / agents-md default
|
|
478
|
+
* @param io - Persistence seams (tests)
|
|
328
479
|
* @returns Answers, or `null` if the user cancelled
|
|
329
480
|
*/
|
|
330
481
|
export async function askInteractiveAnswers(
|
|
331
|
-
partial: {
|
|
482
|
+
partial: {
|
|
483
|
+
readonly name?: string;
|
|
484
|
+
readonly agentsMd?: boolean;
|
|
485
|
+
readonly sqlDriver?: SqlDriverId;
|
|
486
|
+
readonly ai?: AiCliMode;
|
|
487
|
+
readonly template?: TemplateId;
|
|
488
|
+
} = {},
|
|
489
|
+
io: CreateDefaultsIo = {
|
|
490
|
+
path: createDefaultsPath(),
|
|
491
|
+
read: () => readCreateDefaults(),
|
|
492
|
+
write: (d) => writeCreateDefaults(d),
|
|
493
|
+
},
|
|
332
494
|
): Promise<InteractiveAnswers | null> {
|
|
333
495
|
const nameValue = await text({
|
|
334
496
|
message: "Project name",
|
|
@@ -336,12 +498,93 @@ export async function askInteractiveAnswers(
|
|
|
336
498
|
initialValue: partial.name ?? "my-app",
|
|
337
499
|
validate: (value) => {
|
|
338
500
|
if (!value?.trim()) return "Project name is required";
|
|
501
|
+
const blocked = targetDirectoryBlockReason(resolve(value.trim()));
|
|
502
|
+
if (blocked) {
|
|
503
|
+
return blocked.replace(/^create-oke:\s*/, "");
|
|
504
|
+
}
|
|
339
505
|
return undefined;
|
|
340
506
|
},
|
|
341
507
|
});
|
|
342
508
|
if (isCancel(nameValue)) return null;
|
|
343
509
|
const name = String(nameValue).trim();
|
|
344
510
|
|
|
511
|
+
const templateValue = await select({
|
|
512
|
+
message: "Starter template",
|
|
513
|
+
options: TEMPLATES.map((id) => ({
|
|
514
|
+
value: id,
|
|
515
|
+
label: id === "standard" ? "★ standard" : "◆ advanced",
|
|
516
|
+
hint: TEMPLATE_PURPOSES[id],
|
|
517
|
+
})),
|
|
518
|
+
initialValue: partial.template ?? DEFAULT_TEMPLATE,
|
|
519
|
+
});
|
|
520
|
+
if (isCancel(templateValue)) return null;
|
|
521
|
+
const template = templateValue as TemplateId;
|
|
522
|
+
|
|
523
|
+
let createDefaults: CreateDefaults | undefined;
|
|
524
|
+
let aiApply: AiSetupApplyInput | null = null;
|
|
525
|
+
|
|
526
|
+
for (;;) {
|
|
527
|
+
const previous = io.read();
|
|
528
|
+
const canReuse = previous !== null && previous.template === template;
|
|
529
|
+
const branchValue = await select({
|
|
530
|
+
message: "Would you like to use the recommended oke defaults?",
|
|
531
|
+
options: defaultsBranchOptions(canReuse, template),
|
|
532
|
+
});
|
|
533
|
+
if (isCancel(branchValue)) return null;
|
|
534
|
+
const branch = branchValue as DefaultsBranch;
|
|
535
|
+
|
|
536
|
+
if (branch === "recommended") {
|
|
537
|
+
createDefaults = undefined;
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (branch === "reuse") {
|
|
542
|
+
const prev = io.read() ?? previous;
|
|
543
|
+
if (!prev || prev.template !== template) {
|
|
544
|
+
note(
|
|
545
|
+
"No saved settings for this template — customize or use recommended.",
|
|
546
|
+
"Previous settings",
|
|
547
|
+
);
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
createDefaults = prev;
|
|
551
|
+
if (prev.ai.enabled && partial.ai !== "skip") {
|
|
552
|
+
aiApply = applyInputFromAiPref(prev.ai);
|
|
553
|
+
if (!aiApply?.chatModel && prev.ai.provider && prev.ai.provider !== "mock") {
|
|
554
|
+
const picked = await askAiSetup({ provider: prev.ai.provider });
|
|
555
|
+
if (picked === null) return null;
|
|
556
|
+
aiApply = picked;
|
|
557
|
+
io.write({
|
|
558
|
+
...prev,
|
|
559
|
+
ai: aiPrefWithModels(prev.ai, picked),
|
|
560
|
+
updatedAt: new Date().toISOString(),
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
note(`Reusing settings from ${io.path}`, "Previous settings");
|
|
565
|
+
break;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const customized = await askCustomizeFlow(template);
|
|
569
|
+
if (customized === null) return null;
|
|
570
|
+
if (customized === WIZARD_BACK) continue;
|
|
571
|
+
createDefaults = customized;
|
|
572
|
+
io.write(customized);
|
|
573
|
+
note(`Saved globally for next projects → ${io.path}`, "Defaults");
|
|
574
|
+
if (customized.ai.enabled && partial.ai !== "skip") {
|
|
575
|
+
aiApply = applyInputFromAiPref(customized.ai);
|
|
576
|
+
}
|
|
577
|
+
break;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (partial.ai === "skip") {
|
|
581
|
+
aiApply = null;
|
|
582
|
+
} else if (partial.ai === "force" && !aiApply) {
|
|
583
|
+
const picked = await askAiSetup({});
|
|
584
|
+
if (picked === null) return null;
|
|
585
|
+
aiApply = picked;
|
|
586
|
+
}
|
|
587
|
+
|
|
345
588
|
const agentsMd = partial.agentsMd ?? true;
|
|
346
589
|
|
|
347
590
|
const installAndRunValue = await confirm({
|
|
@@ -352,12 +595,30 @@ export async function askInteractiveAnswers(
|
|
|
352
595
|
|
|
353
596
|
return {
|
|
354
597
|
name,
|
|
355
|
-
choice:
|
|
598
|
+
choice: template,
|
|
356
599
|
installAndRun: Boolean(installAndRunValue),
|
|
357
600
|
agentsMd,
|
|
601
|
+
createDefaults,
|
|
602
|
+
aiApply,
|
|
358
603
|
};
|
|
359
604
|
}
|
|
360
605
|
|
|
606
|
+
/**
|
|
607
|
+
* Provider passed to `oke ai setup` — prefer Ollama when either env uses it.
|
|
608
|
+
*
|
|
609
|
+
* @param localProvider - Menu id chosen for local
|
|
610
|
+
* @param pins - Resolved driver pins
|
|
611
|
+
*/
|
|
612
|
+
export function aiSetupProviderFor(localProvider: string, pins: EnvDriverPins): string {
|
|
613
|
+
if (localProvider === "ollama" || pins.local === "ollama") return "ollama";
|
|
614
|
+
if (pins.docker === "ollama") return "ollama";
|
|
615
|
+
if (localProvider === "mock") {
|
|
616
|
+
if (pins.docker === "anthropic") return "anthropic";
|
|
617
|
+
if (pins.docker === "openai-compatible") return "openai";
|
|
618
|
+
}
|
|
619
|
+
return localProvider;
|
|
620
|
+
}
|
|
621
|
+
|
|
361
622
|
/**
|
|
362
623
|
* Resolve whether to run `bun install` after scaffold (non-interactive).
|
|
363
624
|
*
|
|
@@ -373,7 +634,7 @@ export function shouldInstall(args: CliArgs): boolean {
|
|
|
373
634
|
* Run the CLI.
|
|
374
635
|
*
|
|
375
636
|
* @param argv - Args after the binary
|
|
376
|
-
* @param options - Test seams
|
|
637
|
+
* @param options - Test seams
|
|
377
638
|
* @returns Exit code
|
|
378
639
|
*/
|
|
379
640
|
export async function run(
|
|
@@ -383,6 +644,7 @@ export async function run(
|
|
|
383
644
|
readonly ask?: AskInteractiveFn;
|
|
384
645
|
/** When false, skip spawning bun install / oke dev (tests). Default true. */
|
|
385
646
|
readonly runPostScaffold?: boolean;
|
|
647
|
+
readonly createDefaultsIo?: CreateDefaultsIo;
|
|
386
648
|
} = {},
|
|
387
649
|
): Promise<number> {
|
|
388
650
|
let args: CliArgs;
|
|
@@ -400,7 +662,18 @@ export async function run(
|
|
|
400
662
|
|
|
401
663
|
const stdinIsTTY = options.stdinIsTTY ?? process.stdin.isTTY;
|
|
402
664
|
if (shouldPrompt(args, stdinIsTTY)) {
|
|
403
|
-
|
|
665
|
+
const ask =
|
|
666
|
+
options.ask ??
|
|
667
|
+
((partial) =>
|
|
668
|
+
askInteractiveAnswers(
|
|
669
|
+
partial,
|
|
670
|
+
options.createDefaultsIo ?? {
|
|
671
|
+
path: createDefaultsPath(),
|
|
672
|
+
read: () => readCreateDefaults(),
|
|
673
|
+
write: (d) => writeCreateDefaults(d),
|
|
674
|
+
},
|
|
675
|
+
));
|
|
676
|
+
return runInteractive(args, ask, options);
|
|
404
677
|
}
|
|
405
678
|
|
|
406
679
|
if (args.name === undefined) {
|
|
@@ -410,15 +683,22 @@ export async function run(
|
|
|
410
683
|
return 1;
|
|
411
684
|
}
|
|
412
685
|
|
|
686
|
+
const cliScaffold = scaffoldArgsFromCli(args);
|
|
687
|
+
const earlyBlock = targetDirectoryBlockReason(cliScaffold.targetDir);
|
|
688
|
+
if (earlyBlock) {
|
|
689
|
+
console.error(earlyBlock);
|
|
690
|
+
return 1;
|
|
691
|
+
}
|
|
692
|
+
|
|
413
693
|
if (args.yes) {
|
|
414
694
|
const source = sourceFromArgs(args);
|
|
415
695
|
console.log(
|
|
416
|
-
`Using defaults: ${source.kind}=${source.id} sql=${args.sqlDriver} agents-md=${args.agentsMd} install=${shouldInstall(args)}`,
|
|
696
|
+
`Using defaults: ${source.kind}=${source.id} sql=${args.sqlDriver} agents-md=${args.agentsMd} install=${shouldInstall(args)} ai=${args.ai}`,
|
|
417
697
|
);
|
|
418
698
|
}
|
|
419
699
|
|
|
420
700
|
return runScaffold({
|
|
421
|
-
...
|
|
701
|
+
...cliScaffold,
|
|
422
702
|
interactive: false,
|
|
423
703
|
install: shouldInstall(args),
|
|
424
704
|
startDev: false,
|
|
@@ -440,14 +720,19 @@ async function runInteractive(
|
|
|
440
720
|
): Promise<number> {
|
|
441
721
|
intro("create-oke");
|
|
442
722
|
|
|
443
|
-
const answers = await ask({
|
|
723
|
+
const answers = await ask({
|
|
724
|
+
name: args.name,
|
|
725
|
+
agentsMd: args.agentsMd,
|
|
726
|
+
sqlDriver: args.sqlDriverExplicit ? args.sqlDriver : undefined,
|
|
727
|
+
ai: args.ai,
|
|
728
|
+
});
|
|
444
729
|
if (answers === null) {
|
|
445
730
|
cancel("Cancelled.");
|
|
446
731
|
return 1;
|
|
447
732
|
}
|
|
448
733
|
|
|
449
734
|
return runScaffold({
|
|
450
|
-
...scaffoldArgsFromAnswers(answers),
|
|
735
|
+
...scaffoldArgsFromAnswers(answers, args.sqlDriverExplicit ? args.sqlDriver : undefined),
|
|
451
736
|
interactive: true,
|
|
452
737
|
install: answers.installAndRun,
|
|
453
738
|
startDev: answers.installAndRun,
|
|
@@ -474,6 +759,8 @@ async function runScaffold(
|
|
|
474
759
|
source,
|
|
475
760
|
agentsMd,
|
|
476
761
|
sqlDriver,
|
|
762
|
+
createDefaults,
|
|
763
|
+
aiApply,
|
|
477
764
|
interactive,
|
|
478
765
|
install,
|
|
479
766
|
startDev,
|
|
@@ -507,9 +794,25 @@ async function runScaffold(
|
|
|
507
794
|
source,
|
|
508
795
|
writeAgentsMd: agentsMd,
|
|
509
796
|
sqlDriver,
|
|
797
|
+
...(createDefaults !== undefined ? { createDefaults } : {}),
|
|
510
798
|
});
|
|
511
799
|
if (spun) spun.stop("Scaffolded.");
|
|
512
800
|
|
|
801
|
+
// Models were chosen in the wizard — write env + src/ai.ts before install
|
|
802
|
+
// so `--no-install` still gets a complete AI project. Preserve per-env
|
|
803
|
+
// `drivers.ai` pins from customize; flatten only when pins were never written.
|
|
804
|
+
if (runPostScaffold && aiApply) {
|
|
805
|
+
try {
|
|
806
|
+
applyAiSetup(targetDir, aiApply, {
|
|
807
|
+
updateDrivers: createDefaults?.drivers.ai == null,
|
|
808
|
+
});
|
|
809
|
+
if (interactive) log.success("AI configured.");
|
|
810
|
+
} catch (err) {
|
|
811
|
+
if (interactive) log.warn("AI config write failed.");
|
|
812
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
513
816
|
if (runPostScaffold && install) {
|
|
514
817
|
const installSpun = interactive ? spinner() : undefined;
|
|
515
818
|
installSpun?.start("Installing dependencies…");
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistence round-trip for ~/.oke/create-defaults.json
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import {
|
|
10
|
+
createDefaultsPath,
|
|
11
|
+
parseCreateDefaults,
|
|
12
|
+
readCreateDefaults,
|
|
13
|
+
toCreateDefaults,
|
|
14
|
+
writeCreateDefaults,
|
|
15
|
+
} from "./create-defaults.ts";
|
|
16
|
+
import { pinsLocalOnly, recommendedDefaults } from "./drivers-catalog.ts";
|
|
17
|
+
|
|
18
|
+
describe("createDefaultsPath", () => {
|
|
19
|
+
test("joins ~/.oke/create-defaults.json", () => {
|
|
20
|
+
expect(createDefaultsPath("/tmp/home")).toBe(join("/tmp/home", ".oke", "create-defaults.json"));
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("parseCreateDefaults", () => {
|
|
25
|
+
test("rejects corrupt / wrong version", () => {
|
|
26
|
+
expect(parseCreateDefaults(null)).toBeNull();
|
|
27
|
+
expect(parseCreateDefaults({ version: 2 })).toBeNull();
|
|
28
|
+
expect(parseCreateDefaults({ version: 1, profile: "nope" })).toBeNull();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("accepts recommended shape", () => {
|
|
32
|
+
const d = recommendedDefaults("local-only", "standard");
|
|
33
|
+
const parsed = parseCreateDefaults(d);
|
|
34
|
+
expect(parsed?.profile).toBe("local-only");
|
|
35
|
+
expect(parsed?.template).toBe("standard");
|
|
36
|
+
expect(parsed?.drivers.store.sql.local).toBe("sqlite");
|
|
37
|
+
expect(parsed?.ai.enabled).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("older files without template default to standard", () => {
|
|
41
|
+
const d = recommendedDefaults("local-only", "standard");
|
|
42
|
+
const { template: _t, ...rest } = d;
|
|
43
|
+
const parsed = parseCreateDefaults(rest);
|
|
44
|
+
expect(parsed?.template).toBe("standard");
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("read/write round-trip", () => {
|
|
49
|
+
test("write → read returns same drivers", () => {
|
|
50
|
+
const home = mkdtempSync(join(tmpdir(), "oke-create-defaults-"));
|
|
51
|
+
const path = createDefaultsPath(home);
|
|
52
|
+
try {
|
|
53
|
+
const original = toCreateDefaults({
|
|
54
|
+
template: "advanced",
|
|
55
|
+
profile: "docker-ready",
|
|
56
|
+
drivers: {
|
|
57
|
+
store: {
|
|
58
|
+
sql: pinsLocalOnly("libsql", "postgres", "memory"),
|
|
59
|
+
kv: pinsLocalOnly("redis", "redis", "memory"),
|
|
60
|
+
files: pinsLocalOnly("fs", "s3", "memory"),
|
|
61
|
+
index: null,
|
|
62
|
+
},
|
|
63
|
+
signal: pinsLocalOnly("memory", "redis", "memory"),
|
|
64
|
+
clock: pinsLocalOnly("file", "file", "frozen"),
|
|
65
|
+
vault: pinsLocalOnly("env", "openbao", "memory"),
|
|
66
|
+
channel: { email: pinsLocalOnly("console", "smtp", "console") },
|
|
67
|
+
ai: pinsLocalOnly("ollama", "ollama", "mock"),
|
|
68
|
+
},
|
|
69
|
+
ai: { enabled: true, provider: "ollama", driver: "ollama" },
|
|
70
|
+
});
|
|
71
|
+
writeCreateDefaults(original, path);
|
|
72
|
+
const loaded = readCreateDefaults(path);
|
|
73
|
+
expect(loaded).not.toBeNull();
|
|
74
|
+
expect(loaded!.template).toBe("advanced");
|
|
75
|
+
expect(loaded!.drivers.store.sql.local).toBe("libsql");
|
|
76
|
+
expect(loaded!.drivers.ai?.local).toBe("ollama");
|
|
77
|
+
expect(loaded!.ai.provider).toBe("ollama");
|
|
78
|
+
expect(loaded!.profile).toBe("docker-ready");
|
|
79
|
+
} finally {
|
|
80
|
+
rmSync(home, { recursive: true, force: true });
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("missing file → null", () => {
|
|
85
|
+
const home = mkdtempSync(join(tmpdir(), "oke-create-defaults-miss-"));
|
|
86
|
+
try {
|
|
87
|
+
expect(readCreateDefaults(createDefaultsPath(home))).toBeNull();
|
|
88
|
+
} finally {
|
|
89
|
+
rmSync(home, { recursive: true, force: true });
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
});
|