create-oke 0.9.0 → 0.10.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 +7 -5
- package/package.json +2 -3
- package/src/agents-md.ts +1 -1
- package/src/ai-setup/apply.ts +66 -23
- package/src/ai-setup/catalog.ts +1316 -35
- package/src/ai-setup/detect-ollama.ts +48 -0
- package/src/ai-setup/from-pref.ts +32 -0
- package/src/ai-setup/prompts.ts +430 -486
- package/src/ai-setup/recommend.ts +118 -101
- package/src/cli.test.ts +36 -7
- package/src/cli.ts +21 -16
- package/src/customize-flow.test.ts +43 -13
- package/src/customize-flow.ts +136 -61
- package/src/drivers-catalog.ts +29 -12
- package/src/local-okengine.test.ts +59 -0
- package/src/local-okengine.ts +137 -0
- package/src/scaffold.ts +1 -1
- package/src/transform.test.ts +54 -32
- package/src/transform.ts +46 -9
- package/src/wizard-select.ts +5 -6
- package/templates/advanced/.github/workflows/ci.yml +24 -0
- package/templates/advanced/.vscode/settings.json +15 -0
- package/templates/advanced/README.md +15 -4
- package/templates/advanced/drizzle.config.ts +6 -4
- package/templates/advanced/oke.config.ts +6 -2
- package/templates/advanced/package.json +5 -2
- package/templates/advanced/src/app.ts +2 -14
- package/templates/advanced/src/core/index.ts +12 -0
- package/templates/advanced/src/{core.ts → core/store.ts} +1 -1
- package/templates/advanced/src/db/migrations/.gitkeep +0 -0
- package/templates/{standard/src → advanced/src/db}/schema.decl.ts +1 -1
- package/templates/advanced/src/{seed → db/seed}/index.ts +2 -2
- package/templates/advanced/src/flows/notes/index.ts +2 -4
- package/templates/advanced/tests/advanced.test.ts +10 -9
- package/templates/advanced/tsconfig.json +23 -0
- package/templates/standard/.github/workflows/ci.yml +24 -0
- package/templates/standard/.vscode/settings.json +15 -0
- package/templates/standard/README.md +20 -9
- package/templates/standard/drizzle.config.ts +6 -4
- package/templates/standard/oke.config.ts +4 -0
- package/templates/standard/package.json +5 -2
- package/templates/standard/src/app.ts +2 -14
- package/templates/standard/src/core/index.ts +12 -0
- package/templates/standard/src/{core.ts → core/store.ts} +1 -1
- package/templates/standard/src/db/migrations/.gitkeep +0 -0
- package/templates/{advanced/src → standard/src/db}/schema.decl.ts +1 -1
- package/templates/standard/src/{seed → db/seed}/index.ts +2 -2
- package/templates/standard/src/flows/notes/index.ts +2 -4
- package/templates/standard/tests/standard.test.ts +13 -10
- package/templates/standard/tsconfig.json +23 -0
- /package/templates/advanced/src/{channels.ts → core/channels.ts} +0 -0
- /package/templates/advanced/src/{gates.ts → core/gates.ts} +0 -0
- /package/templates/advanced/src/{vault.ts → core/vault.ts} +0 -0
- /package/templates/standard/src/{channels.ts → core/channels.ts} +0 -0
- /package/templates/standard/src/{gates.ts → core/gates.ts} +0 -0
- /package/templates/standard/src/{vault.ts → core/vault.ts} +0 -0
|
@@ -1,32 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Machine fit helpers for local Ollama model picks.
|
|
3
3
|
*
|
|
4
4
|
* Catalog `ramGb` is the **recommended machine tier** for that model (not the
|
|
5
|
-
* download size).
|
|
6
|
-
* model" — that number was machine RAM shown in the wrong place.
|
|
7
|
-
*
|
|
8
|
-
* Most dev machines are 8–16GB. Prefer picks that leave ~4GB for OS / IDE /
|
|
9
|
-
* browser when several models fit.
|
|
5
|
+
* download size). Prefer picks that leave ~4GB for OS / IDE / browser.
|
|
10
6
|
*/
|
|
11
7
|
|
|
12
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
CHAT_MODELS,
|
|
10
|
+
MODEL_TIERS,
|
|
11
|
+
type CatalogModel,
|
|
12
|
+
type ModelTier,
|
|
13
|
+
recommendForTier,
|
|
14
|
+
} from "./catalog.ts";
|
|
13
15
|
|
|
14
16
|
/** Reserved for OS + IDE + browser while the model runs. */
|
|
15
17
|
export const OS_HEADROOM_GB = 4;
|
|
16
18
|
|
|
17
|
-
/** What the developer mainly wants from the local model. */
|
|
18
|
-
export type AiUseCase = "coding" | "general" | "reasoning" | "balanced";
|
|
19
|
-
|
|
20
|
-
/** Speed vs quality preference. */
|
|
21
|
-
export type AiPriority = "speed" | "balanced" | "quality";
|
|
22
|
-
|
|
23
|
-
/** Answers from the short Ollama questionnaire. */
|
|
24
|
-
export type AiNeeds = {
|
|
25
|
-
readonly useCase: AiUseCase;
|
|
26
|
-
readonly priority: AiPriority;
|
|
27
|
-
readonly wantVision: boolean;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
19
|
/**
|
|
31
20
|
* RAM left after reserving OS/IDE headroom.
|
|
32
21
|
*
|
|
@@ -60,7 +49,6 @@ export function modelFitsComfortably(model: CatalogModel, totalRamGb: number | n
|
|
|
60
49
|
return Boolean(model.recommended);
|
|
61
50
|
}
|
|
62
51
|
const smallest = [...CHAT_MODELS].sort((a, b) => a.ramGb - b.ramGb)[0]!;
|
|
63
|
-
// Always allow the entry-level model on machines that meet its tier.
|
|
64
52
|
if (model.id === smallest.id) return model.ramGb <= totalRamGb;
|
|
65
53
|
return model.ramGb + OS_HEADROOM_GB <= totalRamGb;
|
|
66
54
|
}
|
|
@@ -101,103 +89,132 @@ export function comfortableChatModels(totalRamGb: number | null): readonly Catal
|
|
|
101
89
|
}
|
|
102
90
|
|
|
103
91
|
/**
|
|
104
|
-
*
|
|
92
|
+
* Suggest a tier for the machine's fit RAM budget.
|
|
105
93
|
*
|
|
106
|
-
* @param
|
|
107
|
-
* @param needs - Questionnaire answers
|
|
108
|
-
* @param totalRamGb - Machine RAM (tight-fit penalty)
|
|
94
|
+
* @param totalRamGb - Machine RAM
|
|
109
95
|
*/
|
|
110
|
-
export function
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
else if (needs.priority === "quality") score += model.ramGb * 4;
|
|
118
|
-
else score += model.ramGb * 2;
|
|
119
|
-
|
|
120
|
-
if (needs.useCase === "coding") {
|
|
121
|
-
if (model.id.startsWith("qwen")) score += 40;
|
|
122
|
-
if (model.id.includes("deepseek")) score += 15;
|
|
123
|
-
} else if (needs.useCase === "reasoning") {
|
|
124
|
-
if (model.id.includes("deepseek") || model.id.includes("r1")) score += 45;
|
|
125
|
-
if (model.id.startsWith("qwen")) score += 20;
|
|
126
|
-
} else if (needs.useCase === "general") {
|
|
127
|
-
if (model.id.includes("llama") || model.id.includes("gemma")) score += 35;
|
|
128
|
-
if (model.id.startsWith("qwen")) score += 20;
|
|
129
|
-
} else {
|
|
130
|
-
if (model.recommended) score += 25;
|
|
131
|
-
if (model.id.startsWith("qwen3.5:9b")) score += 30;
|
|
132
|
-
if (model.id.startsWith("gemma")) score += 20;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Prefer leaving OS/IDE headroom unless the user asked for max quality.
|
|
136
|
-
if (isTightFit(model, totalRamGb) && needs.priority !== "quality") {
|
|
137
|
-
score -= 35;
|
|
138
|
-
}
|
|
139
|
-
return score;
|
|
96
|
+
export function suggestTierForRam(totalRamGb: number | null): ModelTier {
|
|
97
|
+
if (totalRamGb === null || !Number.isFinite(totalRamGb)) return "fast";
|
|
98
|
+
const budget = usableRamGb(totalRamGb);
|
|
99
|
+
if (budget >= 24) return "smart";
|
|
100
|
+
if (budget >= 8) return "balanced";
|
|
101
|
+
if (budget >= 4) return "fast";
|
|
102
|
+
return "ultra-fast";
|
|
140
103
|
}
|
|
141
104
|
|
|
142
105
|
/**
|
|
143
|
-
*
|
|
106
|
+
* Recommend a chat model for host RAM (balanced default — no quiz).
|
|
144
107
|
*
|
|
145
108
|
* @param totalRamGb - Machine RAM
|
|
146
|
-
* @param needs - Questionnaire (defaults to balanced/speed-friendly)
|
|
147
109
|
*/
|
|
148
|
-
export function recommendChatForNeeds(
|
|
149
|
-
totalRamGb
|
|
150
|
-
needs: AiNeeds = { useCase: "balanced", priority: "balanced", wantVision: false },
|
|
151
|
-
): CatalogModel {
|
|
152
|
-
// Prefer comfortable pool; quality may still score a tight tier-fit higher.
|
|
153
|
-
const pool =
|
|
154
|
-
needs.priority === "quality"
|
|
155
|
-
? fittingChatModels(totalRamGb)
|
|
156
|
-
: comfortableChatModels(totalRamGb);
|
|
157
|
-
let best = pool[0]!;
|
|
158
|
-
let bestScore = Number.NEGATIVE_INFINITY;
|
|
159
|
-
for (const m of pool) {
|
|
160
|
-
const s = scoreChatModel(m, needs, totalRamGb);
|
|
161
|
-
if (s > bestScore) {
|
|
162
|
-
best = m;
|
|
163
|
-
bestScore = s;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
return best;
|
|
110
|
+
export function recommendChatForNeeds(totalRamGb: number | null): CatalogModel {
|
|
111
|
+
return recommendForTier(suggestTierForRam(totalRamGb), totalRamGb);
|
|
167
112
|
}
|
|
168
113
|
|
|
169
114
|
/**
|
|
170
|
-
*
|
|
115
|
+
* Shared hardware lines for local AI banners.
|
|
171
116
|
*
|
|
172
|
-
* @param
|
|
173
|
-
* @param wantVision - From questionnaire
|
|
117
|
+
* @param machine - Detected hardware
|
|
174
118
|
*/
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const
|
|
181
|
-
|
|
119
|
+
function formatMachineBannerLines(machine: {
|
|
120
|
+
readonly osName: string;
|
|
121
|
+
readonly cpuCount: number | null;
|
|
122
|
+
readonly ramGb: number | null;
|
|
123
|
+
}): readonly string[] {
|
|
124
|
+
const ram =
|
|
125
|
+
machine.ramGb !== null && Number.isFinite(machine.ramGb)
|
|
126
|
+
? `~${machine.ramGb}GB RAM`
|
|
127
|
+
: "RAM unknown";
|
|
128
|
+
const cpu =
|
|
129
|
+
machine.cpuCount !== null && Number.isFinite(machine.cpuCount) ? String(machine.cpuCount) : "?";
|
|
130
|
+
const fit =
|
|
131
|
+
machine.ramGb !== null && Number.isFinite(machine.ramGb)
|
|
132
|
+
? `~${usableRamGb(machine.ramGb)}GB RAM`
|
|
133
|
+
: "unknown";
|
|
134
|
+
const sep = " · ";
|
|
135
|
+
return [[`OS ${machine.osName}`, `CPU ${cpu}`, `RAM ${ram}`].join(sep), `Fit RAM ${fit}`];
|
|
182
136
|
}
|
|
183
137
|
|
|
184
138
|
/**
|
|
185
|
-
*
|
|
139
|
+
* Ollama banner lines — OS · CPU · RAM · fit · detected · tiers.
|
|
186
140
|
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
141
|
+
* @param machine - Detected hardware
|
|
142
|
+
* @param detectedIds - Installed model ids
|
|
143
|
+
*/
|
|
144
|
+
export function formatOllamaBanner(
|
|
145
|
+
machine: {
|
|
146
|
+
readonly osName: string;
|
|
147
|
+
readonly cpuCount: number | null;
|
|
148
|
+
readonly ramGb: number | null;
|
|
149
|
+
},
|
|
150
|
+
detectedIds: readonly string[],
|
|
151
|
+
): string {
|
|
152
|
+
const sep = " · ";
|
|
153
|
+
const lines = [
|
|
154
|
+
...formatMachineBannerLines(machine),
|
|
155
|
+
"",
|
|
156
|
+
"Detected local models",
|
|
157
|
+
...(detectedIds.length > 0 ? detectedIds.map((id) => id) : ["(none detected)"]),
|
|
158
|
+
"",
|
|
159
|
+
`Tiers${sep}${MODEL_TIERS.map((t) => t.label).join(sep)}`,
|
|
160
|
+
];
|
|
161
|
+
return lines.join("\n");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* llama.cpp banner — OS · CPU · RAM · fit · Docker Hub `ai/` source · tiers.
|
|
189
166
|
*
|
|
190
|
-
* @param
|
|
191
|
-
* @param chat - Chosen chat model
|
|
167
|
+
* @param machine - Detected hardware
|
|
192
168
|
*/
|
|
193
|
-
export function
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
169
|
+
export function formatLlamaCppBanner(machine: {
|
|
170
|
+
readonly osName: string;
|
|
171
|
+
readonly cpuCount: number | null;
|
|
172
|
+
readonly ramGb: number | null;
|
|
173
|
+
}): string {
|
|
174
|
+
const sep = " · ";
|
|
175
|
+
const lines = [
|
|
176
|
+
...formatMachineBannerLines(machine),
|
|
177
|
+
"",
|
|
178
|
+
"Model source",
|
|
179
|
+
"Docker Hub ai/ (curated) — pulled on first container start",
|
|
180
|
+
"",
|
|
181
|
+
`Tiers${sep}${MODEL_TIERS.map((t) => t.label).join(sep)}`,
|
|
182
|
+
];
|
|
183
|
+
return lines.join("\n");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Model name column width (monospace CLI table). */
|
|
187
|
+
const MODEL_COL_NAME = 26;
|
|
188
|
+
/** RAM column width, right-aligned (`≈32GB`). */
|
|
189
|
+
const MODEL_COL_RAM = 7;
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Clip or pad a cell for aligned CLI columns.
|
|
193
|
+
*
|
|
194
|
+
* @param value - Cell text
|
|
195
|
+
* @param width - Fixed width
|
|
196
|
+
*/
|
|
197
|
+
function clipPad(value: string, width: number): string {
|
|
198
|
+
if (value.length === width) return value;
|
|
199
|
+
if (value.length < width) return value.padEnd(width);
|
|
200
|
+
return `${value.slice(0, Math.max(0, width - 1))}…`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Column header for the model pick list (aligns with {@link formatModelRow}).
|
|
205
|
+
*/
|
|
206
|
+
export function formatModelTableHeader(): string {
|
|
207
|
+
return `${clipPad("Model", MODEL_COL_NAME)} ${"RAM".padStart(MODEL_COL_RAM)} Caps`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* One model row as aligned columns: Model | RAM | Caps.
|
|
212
|
+
*
|
|
213
|
+
* @param model - Catalog entry
|
|
214
|
+
*/
|
|
215
|
+
export function formatModelRow(model: CatalogModel): string {
|
|
216
|
+
const name = clipPad(model.label, MODEL_COL_NAME);
|
|
217
|
+
const ram = `≈${model.ramGb}GB`.padStart(MODEL_COL_RAM);
|
|
218
|
+
const caps = model.modalities.join(" · ");
|
|
219
|
+
return `${name} ${ram} ${caps}`;
|
|
203
220
|
}
|
package/src/cli.test.ts
CHANGED
|
@@ -200,7 +200,7 @@ describe("wizard ← Back", () => {
|
|
|
200
200
|
const base = [{ value: "memory", label: "memory" }];
|
|
201
201
|
expect(withBackOption(base, false)).toEqual(base);
|
|
202
202
|
const withBack = withBackOption(base, true);
|
|
203
|
-
expect(withBack.at(-1)).toEqual({ value: WIZARD_BACK, label: "
|
|
203
|
+
expect(withBack.at(-1)).toEqual({ value: WIZARD_BACK, label: "Back" });
|
|
204
204
|
expect(WIZARD_BACK).toBe("__back__");
|
|
205
205
|
});
|
|
206
206
|
});
|
|
@@ -536,6 +536,8 @@ describe("scaffold structure", () => {
|
|
|
536
536
|
);
|
|
537
537
|
expect(result.files).toContain(".gitignore");
|
|
538
538
|
expect(result.files).toContain("README.md");
|
|
539
|
+
expect(result.files).toContain(".github/workflows/ci.yml");
|
|
540
|
+
expect(result.files).toContain("tsconfig.json");
|
|
539
541
|
expect(result.sqlDriver).toBe("sqlite");
|
|
540
542
|
expect(readFileSync(join(result.targetDir, "AGENTS.md"), "utf8")).toMatch(
|
|
541
543
|
/one law|on\(Trigger\)/i,
|
|
@@ -544,13 +546,37 @@ describe("scaffold structure", () => {
|
|
|
544
546
|
expect(readme).toMatch(/oke dev/);
|
|
545
547
|
expect(readme).toMatch(new RegExp(`Notes \\(${id}\\)`, "i"));
|
|
546
548
|
expect(readme).toMatch(/notes\.(create|attach|digest)|main\.health/);
|
|
549
|
+
expect(readme).toMatch(/scaffold|Included vs you build/i);
|
|
550
|
+
expect(readme).toMatch(/\.github\/workflows\/ci\.yml/);
|
|
547
551
|
expect(readFileSync(join(result.targetDir, ".gitignore"), "utf8")).toMatch(/node_modules/);
|
|
552
|
+
const ciYml = readFileSync(join(result.targetDir, ".github/workflows/ci.yml"), "utf8");
|
|
553
|
+
expect(() => Bun.YAML.parse(ciYml)).not.toThrow();
|
|
554
|
+
const ci = Bun.YAML.parse(ciYml) as {
|
|
555
|
+
name?: string;
|
|
556
|
+
on?: unknown;
|
|
557
|
+
jobs?: { check?: { steps?: unknown[] } };
|
|
558
|
+
};
|
|
559
|
+
expect(ci.name).toBe("CI");
|
|
560
|
+
expect(ci.on).toBeTruthy();
|
|
561
|
+
expect(ci.jobs?.check).toBeTruthy();
|
|
562
|
+
expect(ciYml).toMatch(/bun run typecheck/);
|
|
563
|
+
expect(ciYml).toMatch(/bun test/);
|
|
564
|
+
const appTs = readFileSync(join(result.targetDir, "src/app.ts"), "utf8");
|
|
565
|
+
expect(appTs).not.toMatch(/Object\.assign/);
|
|
566
|
+
expect(appTs).not.toMatch(/env:\s*["']test["']/);
|
|
567
|
+
expect(appTs).toMatch(/stores:\s*\[/);
|
|
568
|
+
expect(appTs).toMatch(/oke\(\{[\s\S]*stores:/);
|
|
548
569
|
const pkg = JSON.parse(readFileSync(join(result.targetDir, "package.json"), "utf8")) as {
|
|
549
570
|
name: string;
|
|
550
571
|
dependencies: { okengine: string };
|
|
572
|
+
scripts: { typecheck?: string; test?: string };
|
|
573
|
+
devDependencies: { typescript?: string };
|
|
551
574
|
};
|
|
552
575
|
expect(pkg.name).toBe(`app-${id}`);
|
|
553
576
|
expect(pkg.dependencies.okengine).not.toMatch(/^file:\.\./);
|
|
577
|
+
expect(pkg.scripts.typecheck).toBe("tsc --noEmit");
|
|
578
|
+
expect(pkg.scripts.test).toBe("bun test");
|
|
579
|
+
expect(pkg.devDependencies.typescript).toBeTruthy();
|
|
554
580
|
} finally {
|
|
555
581
|
rmSync(dir, { recursive: true, force: true });
|
|
556
582
|
}
|
|
@@ -566,17 +592,20 @@ describe("scaffold structure", () => {
|
|
|
566
592
|
source: { kind: "template", id: "standard" },
|
|
567
593
|
});
|
|
568
594
|
for (const path of [
|
|
569
|
-
"src/gates.ts",
|
|
570
|
-
"src/vault.ts",
|
|
571
|
-
"src/channels.ts",
|
|
595
|
+
"src/core/gates.ts",
|
|
596
|
+
"src/core/vault.ts",
|
|
597
|
+
"src/core/channels.ts",
|
|
598
|
+
"src/core/store.ts",
|
|
599
|
+
"src/core/index.ts",
|
|
572
600
|
"src/locales/en.ts",
|
|
573
601
|
"src/locales/ar.ts",
|
|
574
602
|
"src/flows/main/shapes.ts",
|
|
575
603
|
"src/flows/main/signals.ts",
|
|
576
604
|
"src/flows/notes/index.ts",
|
|
577
|
-
"src/
|
|
578
|
-
"src/
|
|
605
|
+
"src/db/schema.decl.ts",
|
|
606
|
+
"src/db/seed/index.ts",
|
|
579
607
|
"src/app.ts",
|
|
608
|
+
".vscode/settings.json",
|
|
580
609
|
]) {
|
|
581
610
|
expect(result.files).toContain(path);
|
|
582
611
|
}
|
|
@@ -604,7 +633,7 @@ describe("scaffold structure", () => {
|
|
|
604
633
|
});
|
|
605
634
|
expect(result.sqlDriver).toBe("postgres");
|
|
606
635
|
// Abstract decl is dialect-agnostic — emit picks pgTable at sync time.
|
|
607
|
-
const decl = readFileSync(join(result.targetDir, "src/schema.decl.ts"), "utf8");
|
|
636
|
+
const decl = readFileSync(join(result.targetDir, "src/db/schema.decl.ts"), "utf8");
|
|
608
637
|
expect(decl).toContain("store.schema.table(");
|
|
609
638
|
expect(decl).not.toContain("sqliteTable");
|
|
610
639
|
expect(decl).not.toContain("pgTable");
|
package/src/cli.ts
CHANGED
|
@@ -108,7 +108,7 @@ export function defaultsBranchOptions(
|
|
|
108
108
|
const options: DefaultsBranchOption[] = [
|
|
109
109
|
{
|
|
110
110
|
value: "recommended",
|
|
111
|
-
label: "
|
|
111
|
+
label: "Yes, use recommended defaults",
|
|
112
112
|
hint:
|
|
113
113
|
template === "advanced"
|
|
114
114
|
? "Notes · docker-ready pins · .oke/mode docker"
|
|
@@ -118,13 +118,13 @@ export function defaultsBranchOptions(
|
|
|
118
118
|
if (hasPreviousForTemplate) {
|
|
119
119
|
options.push({
|
|
120
120
|
value: "reuse",
|
|
121
|
-
label: "
|
|
121
|
+
label: "No, reuse previous settings",
|
|
122
122
|
hint: `~/.oke/create-defaults.json (${template})`,
|
|
123
123
|
});
|
|
124
124
|
}
|
|
125
125
|
options.push({
|
|
126
126
|
value: "customize",
|
|
127
|
-
label: "
|
|
127
|
+
label: "No, customize settings",
|
|
128
128
|
hint:
|
|
129
129
|
template === "standard"
|
|
130
130
|
? "local|docker · SQL · optional AI"
|
|
@@ -155,7 +155,7 @@ export type ScaffoldCallArgs = {
|
|
|
155
155
|
readonly agentsMd: boolean;
|
|
156
156
|
readonly sqlDriver: SqlDriverId;
|
|
157
157
|
readonly createDefaults?: CreateDefaults;
|
|
158
|
-
/** Apply `src/ai.ts` + `.env.local` after scaffold (before install). */
|
|
158
|
+
/** Apply `src/core/ai.ts` + `.env.local` after scaffold (before install). */
|
|
159
159
|
readonly aiApply?: AiSetupApplyInput | null;
|
|
160
160
|
};
|
|
161
161
|
|
|
@@ -355,7 +355,8 @@ Template:
|
|
|
355
355
|
${templateLines}
|
|
356
356
|
|
|
357
357
|
On a TTY: pick standard|advanced, then recommended defaults, customize
|
|
358
|
-
(local or docker first;
|
|
358
|
+
(local or docker first; store.index with none; AI setup Recommended /
|
|
359
|
+
Customize / Off; optional other side; saved to ~/.oke/create-defaults.json),
|
|
359
360
|
or reuse when saved for that template. Non-TTY / --yes stay zero-prompt.
|
|
360
361
|
`;
|
|
361
362
|
}
|
|
@@ -398,7 +399,7 @@ export function scaffoldArgsFromCli(args: CliArgs): ScaffoldCallArgs {
|
|
|
398
399
|
source: sourceFromArgs(args),
|
|
399
400
|
agentsMd: args.agentsMd,
|
|
400
401
|
sqlDriver: args.sqlDriver,
|
|
401
|
-
aiApply: args.ai === "force" ? nonInteractiveAiApply("
|
|
402
|
+
aiApply: args.ai === "force" ? nonInteractiveAiApply("llama-cpp") : null,
|
|
402
403
|
};
|
|
403
404
|
}
|
|
404
405
|
|
|
@@ -512,7 +513,7 @@ export async function askInteractiveAnswers(
|
|
|
512
513
|
message: "Starter template",
|
|
513
514
|
options: TEMPLATES.map((id) => ({
|
|
514
515
|
value: id,
|
|
515
|
-
label: id === "standard" ? "
|
|
516
|
+
label: id === "standard" ? "standard" : "advanced",
|
|
516
517
|
hint: TEMPLATE_PURPOSES[id],
|
|
517
518
|
})),
|
|
518
519
|
initialValue: partial.template ?? DEFAULT_TEMPLATE,
|
|
@@ -604,7 +605,7 @@ export async function askInteractiveAnswers(
|
|
|
604
605
|
}
|
|
605
606
|
|
|
606
607
|
/**
|
|
607
|
-
* Provider passed to `oke ai setup` — prefer Ollama when either env uses it.
|
|
608
|
+
* Provider passed to `oke ai setup` — prefer native Ollama when either env uses it.
|
|
608
609
|
*
|
|
609
610
|
* @param localProvider - Menu id chosen for local
|
|
610
611
|
* @param pins - Resolved driver pins
|
|
@@ -612,9 +613,11 @@ export async function askInteractiveAnswers(
|
|
|
612
613
|
export function aiSetupProviderFor(localProvider: string, pins: EnvDriverPins): string {
|
|
613
614
|
if (localProvider === "ollama" || pins.local === "ollama") return "ollama";
|
|
614
615
|
if (pins.docker === "ollama") return "ollama";
|
|
616
|
+
if (localProvider === "llama-cpp") return "llama-cpp";
|
|
617
|
+
if (localProvider === "vllm" || localProvider === "sglang") return localProvider;
|
|
615
618
|
if (localProvider === "mock") {
|
|
616
619
|
if (pins.docker === "anthropic") return "anthropic";
|
|
617
|
-
if (pins.docker === "openai-compatible") return "
|
|
620
|
+
if (pins.docker === "openai-compatible") return "llama-cpp";
|
|
618
621
|
}
|
|
619
622
|
return localProvider;
|
|
620
623
|
}
|
|
@@ -798,7 +801,7 @@ async function runScaffold(
|
|
|
798
801
|
});
|
|
799
802
|
if (spun) spun.stop("Scaffolded.");
|
|
800
803
|
|
|
801
|
-
// Models were chosen in the wizard — write env + src/ai.ts before install
|
|
804
|
+
// Models were chosen in the wizard — write env + src/core/ai.ts before install
|
|
802
805
|
// so `--no-install` still gets a complete AI project. Preserve per-env
|
|
803
806
|
// `drivers.ai` pins from customize; flatten only when pins were never written.
|
|
804
807
|
if (runPostScaffold && aiApply) {
|
|
@@ -814,23 +817,25 @@ async function runScaffold(
|
|
|
814
817
|
}
|
|
815
818
|
|
|
816
819
|
if (runPostScaffold && install) {
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
const installOk = await runCommand(["bun", "install"], targetDir
|
|
820
|
+
// Inherit bun's progress — a silent spinner feels stuck on cold caches.
|
|
821
|
+
if (interactive) log.step("Installing dependencies…");
|
|
822
|
+
const installOk = await runCommand(["bun", "install"], targetDir, {
|
|
823
|
+
inherit: interactive,
|
|
824
|
+
});
|
|
820
825
|
if (!installOk) {
|
|
821
|
-
|
|
826
|
+
if (interactive) log.error("Install failed.");
|
|
822
827
|
cleanup();
|
|
823
828
|
console.error("create-oke: bun install failed");
|
|
824
829
|
return 1;
|
|
825
830
|
}
|
|
826
|
-
|
|
831
|
+
if (interactive) log.success("Installed.");
|
|
827
832
|
}
|
|
828
833
|
|
|
829
834
|
const message = nextStepsText(result);
|
|
830
835
|
if (interactive) {
|
|
831
836
|
note(
|
|
832
837
|
[
|
|
833
|
-
`
|
|
838
|
+
`Backend http://127.0.0.1:6530`,
|
|
834
839
|
`Console http://127.0.0.1:6533`,
|
|
835
840
|
`MCP http://127.0.0.1:6535`,
|
|
836
841
|
`Docs ${docsUrl("/docs")}`,
|
|
@@ -3,8 +3,14 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { describe, expect, test } from "bun:test";
|
|
6
|
-
import {
|
|
7
|
-
|
|
6
|
+
import {
|
|
7
|
+
EMAIL_CHOICES,
|
|
8
|
+
INDEX_CHOICES,
|
|
9
|
+
TEMPLATE_DOCKER_PROD,
|
|
10
|
+
TEMPLATE_LOCAL,
|
|
11
|
+
customizeFacetsFor,
|
|
12
|
+
} from "./drivers-catalog.ts";
|
|
13
|
+
import { assembleDriverDefaults, pinsFromSides, recommendedAiApply } from "./customize-flow.ts";
|
|
8
14
|
|
|
9
15
|
describe("customizeFacetsFor", () => {
|
|
10
16
|
test("standard is lean (sql only)", () => {
|
|
@@ -16,7 +22,6 @@ describe("customizeFacetsFor", () => {
|
|
|
16
22
|
"sql",
|
|
17
23
|
"kv",
|
|
18
24
|
"files",
|
|
19
|
-
"enableIndex",
|
|
20
25
|
"index",
|
|
21
26
|
"signal",
|
|
22
27
|
"clock",
|
|
@@ -26,6 +31,35 @@ describe("customizeFacetsFor", () => {
|
|
|
26
31
|
});
|
|
27
32
|
});
|
|
28
33
|
|
|
34
|
+
describe("catalog labels", () => {
|
|
35
|
+
test("store.index offers none before drivers", () => {
|
|
36
|
+
expect(INDEX_CHOICES.map((c) => c.value)).toEqual([
|
|
37
|
+
"none",
|
|
38
|
+
"memory",
|
|
39
|
+
"pgvector",
|
|
40
|
+
"libsql",
|
|
41
|
+
"meilisearch",
|
|
42
|
+
]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("channel.email labels taqnyat-mail as taqnyat", () => {
|
|
46
|
+
const row = EMAIL_CHOICES.find((c) => c.value === "taqnyat-mail");
|
|
47
|
+
expect(row?.label).toBe("taqnyat");
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("recommendedAiApply", () => {
|
|
52
|
+
test("returns llama.cpp (openai-compatible) with curated ai/ model", () => {
|
|
53
|
+
const apply = recommendedAiApply();
|
|
54
|
+
expect(apply.driver).toBe("openai-compatible");
|
|
55
|
+
expect(apply.chatModel).toBe("smollm2");
|
|
56
|
+
expect(apply.baseUrl).toContain("8080");
|
|
57
|
+
expect(apply.image).toContain("llama.cpp");
|
|
58
|
+
expect(apply.image).not.toContain("latest");
|
|
59
|
+
expect(apply.visionModel).toBeNull();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
29
63
|
describe("pinsFromSides", () => {
|
|
30
64
|
test("prod copies docker", () => {
|
|
31
65
|
expect(pinsFromSides("sqlite", "postgres", "memory")).toEqual({
|
|
@@ -38,7 +72,7 @@ describe("pinsFromSides", () => {
|
|
|
38
72
|
});
|
|
39
73
|
|
|
40
74
|
describe("assembleDriverDefaults", () => {
|
|
41
|
-
test("primary docker +
|
|
75
|
+
test("primary docker + skip other → local pins === template defaults", () => {
|
|
42
76
|
const drivers = assembleDriverDefaults(
|
|
43
77
|
"docker",
|
|
44
78
|
{ sql: "libsql", kv: "redis", files: "s3", signal: "nats" },
|
|
@@ -61,17 +95,13 @@ describe("assembleDriverDefaults", () => {
|
|
|
61
95
|
expect(drivers.store.kv.docker).toBe(TEMPLATE_DOCKER_PROD.kv);
|
|
62
96
|
});
|
|
63
97
|
|
|
64
|
-
test("index stays null
|
|
65
|
-
|
|
66
|
-
expect(
|
|
98
|
+
test("index stays null for none / unset", () => {
|
|
99
|
+
expect(assembleDriverDefaults("local", { sql: "sqlite" }, null).store.index).toBeNull();
|
|
100
|
+
expect(assembleDriverDefaults("docker", { index: "none" }, null).store.index).toBeNull();
|
|
67
101
|
});
|
|
68
102
|
|
|
69
|
-
test("
|
|
70
|
-
const drivers = assembleDriverDefaults(
|
|
71
|
-
"docker",
|
|
72
|
-
{ enableIndex: true, index: "meilisearch" },
|
|
73
|
-
null,
|
|
74
|
-
);
|
|
103
|
+
test("index driver on primary fills both columns", () => {
|
|
104
|
+
const drivers = assembleDriverDefaults("docker", { index: "meilisearch" }, null);
|
|
75
105
|
expect(drivers.store.index).toEqual({
|
|
76
106
|
local: "memory",
|
|
77
107
|
docker: "meilisearch",
|