create-oke 0.18.4 → 0.19.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 +1 -1
- package/package.json +3 -3
- package/src/agents-md.ts +6 -4
- package/src/ai-setup/apply.test.ts +278 -0
- package/src/ai-setup/apply.ts +430 -52
- package/src/ai-setup/catalog.ts +250 -1343
- package/src/ai-setup/from-pref.ts +3 -71
- package/src/ai-setup/prompts.ts +60 -443
- package/src/cli.test.ts +21 -15
- package/src/cli.ts +20 -11
- package/src/create-defaults.test.ts +4 -4
- package/src/create-defaults.ts +3 -3
- package/src/customize-flow.test.ts +9 -15
- package/src/customize-flow.ts +59 -66
- package/src/drivers-catalog.ts +18 -23
- package/src/transform.test.ts +69 -10
- package/src/transform.ts +10 -35
- package/templates/advanced/.env.example +28 -11
- package/templates/advanced/.github/workflows/ci.yml +1 -1
- package/templates/advanced/oke.config.ts +3 -4
- package/templates/advanced/package.json +11 -10
- package/templates/advanced/src/app.ts +36 -1
- package/templates/advanced/src/core.ts +12 -11
- package/templates/advanced/src/db/schema.decl.ts +6 -2
- package/templates/advanced/src/db/seed/index.ts +4 -4
- package/templates/advanced/src/flows/main/route.ts +3 -2
- package/templates/advanced/src/flows/notes/[id]/archive.ts +5 -8
- package/templates/advanced/src/flows/notes/[id]/attach.ts +1 -4
- package/templates/advanced/src/flows/notes/[id]/get.ts +4 -7
- package/templates/advanced/src/flows/notes/[id]/summarize.ts +3 -4
- package/templates/advanced/src/flows/notes/create.ts +4 -6
- package/templates/advanced/src/flows/notes/digest.ts +6 -5
- package/templates/advanced/src/flows/notes/list.ts +4 -5
- package/templates/advanced/src/flows/notes/shapes.ts +13 -3
- package/templates/advanced/src/flows/notes/signals.ts +1 -2
- package/templates/advanced/src/vault.ts +138 -0
- package/templates/advanced/tests/advanced.test.ts +11 -8
- package/templates/advanced/web/src/App.css +7 -0
- package/templates/advanced/web/src/App.tsx +101 -1
- package/templates/advanced/web/src/client.ts +16 -3
- package/templates/advanced/web/vite.config.ts +1 -0
- package/templates/standard/.env.example +22 -11
- package/templates/standard/.github/workflows/ci.yml +1 -1
- package/templates/standard/oke.config.ts +3 -3
- package/templates/standard/package.json +11 -10
- package/templates/standard/src/app.ts +6 -1
- package/templates/standard/src/core.ts +10 -11
- package/templates/standard/src/db/schema.decl.ts +6 -2
- package/templates/standard/src/db/seed/index.ts +3 -3
- package/templates/standard/src/flows/main/route.ts +3 -2
- package/templates/standard/src/flows/notes/[id]/archive.ts +5 -8
- package/templates/standard/src/flows/notes/[id]/get.ts +4 -7
- package/templates/standard/src/flows/notes/create.ts +4 -6
- package/templates/standard/src/flows/notes/list.ts +4 -5
- package/templates/standard/src/flows/notes/shapes.ts +12 -2
- package/templates/standard/src/flows/notes/signals.ts +1 -2
- package/templates/standard/src/vault.ts +123 -0
- package/templates/standard/tests/standard.test.ts +3 -1
- package/templates/standard/web/src/client.ts +2 -2
- package/templates/standard/web/vite.config.ts +1 -0
- package/src/ai-setup/detect-ollama.ts +0 -228
- package/src/ai-setup/recommend.ts +0 -220
package/src/cli.test.ts
CHANGED
|
@@ -282,24 +282,24 @@ describe("wizard ← Back", () => {
|
|
|
282
282
|
});
|
|
283
283
|
|
|
284
284
|
describe("aiSetupProviderFor", () => {
|
|
285
|
-
test("
|
|
285
|
+
test("maps mock menu to custom when openai-compatible is pinned", () => {
|
|
286
286
|
expect(
|
|
287
287
|
aiSetupProviderFor("mock", {
|
|
288
|
-
dev: "
|
|
288
|
+
dev: "openai-compatible",
|
|
289
289
|
test: "mock",
|
|
290
|
-
prod: "
|
|
290
|
+
prod: "openai-compatible",
|
|
291
291
|
}),
|
|
292
|
-
).toBe("
|
|
292
|
+
).toBe("custom");
|
|
293
293
|
});
|
|
294
294
|
|
|
295
|
-
test("keeps menu
|
|
295
|
+
test("keeps menu lmstudio", () => {
|
|
296
296
|
expect(
|
|
297
|
-
aiSetupProviderFor("
|
|
298
|
-
dev: "
|
|
297
|
+
aiSetupProviderFor("lmstudio", {
|
|
298
|
+
dev: "openai-compatible",
|
|
299
299
|
test: "mock",
|
|
300
|
-
prod: "
|
|
300
|
+
prod: "openai-compatible",
|
|
301
301
|
}),
|
|
302
|
-
).toBe("
|
|
302
|
+
).toBe("lmstudio");
|
|
303
303
|
});
|
|
304
304
|
});
|
|
305
305
|
|
|
@@ -339,7 +339,7 @@ describe("interactive branches", () => {
|
|
|
339
339
|
expect(existsSync(join(dir, "src", "flows", "notes", "digest.ts"))).toBe(true);
|
|
340
340
|
expect(existsSync(join(dir, "src", "flows", "notes", "[id]", "attach.ts"))).toBe(true);
|
|
341
341
|
const digest = readFileSync(join(dir, "src", "flows", "notes", "digest.ts"), "utf8");
|
|
342
|
-
expect(digest).toContain('
|
|
342
|
+
expect(digest).toContain('clock("notes.digest", { every: "1d" })');
|
|
343
343
|
const attach = readFileSync(join(dir, "src", "flows", "notes", "[id]", "attach.ts"), "utf8");
|
|
344
344
|
expect(attach).toContain("export const attach");
|
|
345
345
|
expect(readFileSync(join(dir, "oke.config.ts"), "utf8")).toMatch(
|
|
@@ -616,7 +616,11 @@ describe("scaffold structure", () => {
|
|
|
616
616
|
expect(appTs).toMatch(/oke\(\{\s*name:\s*["']notes["']/);
|
|
617
617
|
const pkg = JSON.parse(readFileSync(join(result.targetDir, "package.json"), "utf8")) as {
|
|
618
618
|
name: string;
|
|
619
|
-
dependencies: {
|
|
619
|
+
dependencies: {
|
|
620
|
+
okengine: string;
|
|
621
|
+
"@duckdb/node-api"?: string;
|
|
622
|
+
"oxc-parser"?: string;
|
|
623
|
+
};
|
|
620
624
|
trustedDependencies?: readonly string[];
|
|
621
625
|
scripts: { typecheck?: string; test?: string; web?: string; "web:build"?: string };
|
|
622
626
|
devDependencies: {
|
|
@@ -628,7 +632,8 @@ describe("scaffold structure", () => {
|
|
|
628
632
|
};
|
|
629
633
|
expect(pkg.name).toBe(`app-${id}`);
|
|
630
634
|
expect(pkg.dependencies.okengine).not.toMatch(/^file:\.\./);
|
|
631
|
-
expect(pkg.dependencies["@duckdb/node-api"]).toBe("^1.5.5-r.
|
|
635
|
+
expect(pkg.dependencies["@duckdb/node-api"]).toBe("^1.5.5-r.4");
|
|
636
|
+
expect(pkg.dependencies["oxc-parser"]).toBe("^0.149.0");
|
|
632
637
|
expect(pkg.trustedDependencies).toContain("@duckdb/node-api");
|
|
633
638
|
expect(pkg.scripts.typecheck).toContain("tsc --noEmit");
|
|
634
639
|
expect(pkg.scripts.typecheck).toContain("tsc -b -p web/tsconfig.json");
|
|
@@ -638,9 +643,9 @@ describe("scaffold structure", () => {
|
|
|
638
643
|
expect(result.files).toContain("web/vite.config.ts");
|
|
639
644
|
expect(result.files).toContain("web/src/client.ts");
|
|
640
645
|
expect(pkg.devDependencies.typescript).toBeTruthy();
|
|
641
|
-
expect(pkg.devDependencies.vite).toBe("^8.
|
|
642
|
-
expect(pkg.devDependencies["@electric-sql/pglite"]).toBe("^0.5.
|
|
643
|
-
expect(pkg.devDependencies["@electric-sql/pglite-pgvector"]).toBe("^0.0.
|
|
646
|
+
expect(pkg.devDependencies.vite).toBe("^8.3.0");
|
|
647
|
+
expect(pkg.devDependencies["@electric-sql/pglite"]).toBe("^0.5.8");
|
|
648
|
+
expect(pkg.devDependencies["@electric-sql/pglite-pgvector"]).toBe("^0.0.9");
|
|
644
649
|
const drizzle = readFileSync(join(result.targetDir, "drizzle.config.ts"), "utf8");
|
|
645
650
|
expect(drizzle).toContain('dialect: "postgresql"');
|
|
646
651
|
expect(drizzle).toContain("DATABASE_URL");
|
|
@@ -667,6 +672,7 @@ describe("scaffold structure", () => {
|
|
|
667
672
|
});
|
|
668
673
|
for (const path of [
|
|
669
674
|
"src/core.ts",
|
|
675
|
+
"src/vault.ts",
|
|
670
676
|
"src/locales/en.ts",
|
|
671
677
|
"src/locales/index.ts",
|
|
672
678
|
"src/flows/main/shapes.ts",
|
package/src/cli.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
log,
|
|
17
17
|
note,
|
|
18
18
|
outro,
|
|
19
|
+
password,
|
|
19
20
|
select,
|
|
20
21
|
spinner,
|
|
21
22
|
text,
|
|
@@ -494,7 +495,7 @@ export function scaffoldArgsFromCli(args: CliArgs): ScaffoldCallArgs {
|
|
|
494
495
|
source: sourceFromArgs(args),
|
|
495
496
|
agentsMd: args.agentsMd,
|
|
496
497
|
sqlDriver: args.sqlDriver,
|
|
497
|
-
aiApply: args.ai === "force" ? nonInteractiveAiApply("
|
|
498
|
+
aiApply: args.ai === "force" ? nonInteractiveAiApply("openrouter") : null,
|
|
498
499
|
locales: args.locales,
|
|
499
500
|
pgdog: args.pgdog ?? false,
|
|
500
501
|
proxy: args.proxy ?? "none",
|
|
@@ -715,6 +716,17 @@ export async function askInteractiveAnswers(
|
|
|
715
716
|
ai: aiPrefWithModels(prev.ai, picked),
|
|
716
717
|
updatedAt: new Date().toISOString(),
|
|
717
718
|
});
|
|
719
|
+
} else if (aiApply?.apiKeyEnv && !aiApply.apiKey) {
|
|
720
|
+
// Prefs never store the token — ask again so `.env.local` + Vault get it.
|
|
721
|
+
const token = await password({
|
|
722
|
+
message: `API token (${aiApply.apiKeyEnv})`,
|
|
723
|
+
validate: (v) => {
|
|
724
|
+
if (!v?.trim()) return "API token is required";
|
|
725
|
+
return undefined;
|
|
726
|
+
},
|
|
727
|
+
});
|
|
728
|
+
if (isCancel(token)) return null;
|
|
729
|
+
aiApply = { ...aiApply, apiKey: String(token).trim() };
|
|
718
730
|
}
|
|
719
731
|
}
|
|
720
732
|
note(`Reusing settings from ${io.path}`, "Previous settings");
|
|
@@ -724,10 +736,12 @@ export async function askInteractiveAnswers(
|
|
|
724
736
|
const customized = await askCustomizeFlow(template);
|
|
725
737
|
if (customized === null) return null;
|
|
726
738
|
if (customized === WIZARD_BACK) continue;
|
|
727
|
-
createDefaults = customized;
|
|
739
|
+
createDefaults = customized.defaults;
|
|
728
740
|
persistDefaults = true;
|
|
729
|
-
|
|
730
|
-
|
|
741
|
+
// Use the live apply payload (includes apiKey). Never rebuild from prefs
|
|
742
|
+
// alone — that drops the token and leaves `# OPENROUTER_API_KEY=` empty.
|
|
743
|
+
if (partial.ai !== "skip") {
|
|
744
|
+
aiApply = customized.aiApply;
|
|
731
745
|
}
|
|
732
746
|
break;
|
|
733
747
|
}
|
|
@@ -893,21 +907,16 @@ async function askProxy(initial: CreateProxyId = "none"): Promise<CreateProxyId
|
|
|
893
907
|
}
|
|
894
908
|
|
|
895
909
|
/**
|
|
896
|
-
* Provider passed to `oke ai setup` —
|
|
910
|
+
* Provider passed to `oke ai setup` — map mock pins to a concrete menu id.
|
|
897
911
|
*
|
|
898
912
|
* @param menuProvider - Menu id chosen in the wizard
|
|
899
913
|
* @param pins - Resolved driver pins
|
|
900
914
|
*/
|
|
901
915
|
export function aiSetupProviderFor(menuProvider: string, pins: EnvDriverPins): string {
|
|
902
|
-
if (menuProvider === "ollama" || pins.dev === "ollama" || pins.prod === "ollama") {
|
|
903
|
-
return "ollama";
|
|
904
|
-
}
|
|
905
|
-
if (menuProvider === "llama-cpp") return "llama-cpp";
|
|
906
|
-
if (menuProvider === "vllm" || menuProvider === "sglang") return menuProvider;
|
|
907
916
|
if (menuProvider === "mock") {
|
|
908
917
|
if (pins.dev === "anthropic" || pins.prod === "anthropic") return "anthropic";
|
|
909
918
|
if (pins.dev === "openai-compatible" || pins.prod === "openai-compatible") {
|
|
910
|
-
return "
|
|
919
|
+
return "custom";
|
|
911
920
|
}
|
|
912
921
|
}
|
|
913
922
|
return menuProvider;
|
|
@@ -73,9 +73,9 @@ describe("read/write round-trip", () => {
|
|
|
73
73
|
clock: pinsDockerReady("file", "frozen"),
|
|
74
74
|
vault: pinsDockerReady("vault", "memory"),
|
|
75
75
|
channel: { email: pinsDockerReady("smtp", "console") },
|
|
76
|
-
ai: pinsDockerReady("
|
|
76
|
+
ai: pinsDockerReady("openai-compatible", "mock"),
|
|
77
77
|
},
|
|
78
|
-
ai: { enabled: true, provider: "
|
|
78
|
+
ai: { enabled: true, provider: "openrouter", driver: "openai-compatible" },
|
|
79
79
|
locales: ["ar"],
|
|
80
80
|
pgdog: true,
|
|
81
81
|
proxy: "caddy",
|
|
@@ -85,8 +85,8 @@ describe("read/write round-trip", () => {
|
|
|
85
85
|
expect(loaded).not.toBeNull();
|
|
86
86
|
expect(loaded!.template).toBe("advanced");
|
|
87
87
|
expect(loaded!.drivers.store.sql.dev).toBe("postgres");
|
|
88
|
-
expect(loaded!.drivers.ai?.dev).toBe("
|
|
89
|
-
expect(loaded!.ai.provider).toBe("
|
|
88
|
+
expect(loaded!.drivers.ai?.dev).toBe("openai-compatible");
|
|
89
|
+
expect(loaded!.ai.provider).toBe("openrouter");
|
|
90
90
|
expect(loaded!.profile).toBe("docker-ready");
|
|
91
91
|
expect(loaded!.locales).toEqual(["ar"]);
|
|
92
92
|
expect(loaded!.pgdog).toBe(true);
|
package/src/create-defaults.ts
CHANGED
|
@@ -26,15 +26,15 @@ export type EnvDriverPins = {
|
|
|
26
26
|
/** Optional AI preference saved with create defaults. */
|
|
27
27
|
export type CreateAiPref = {
|
|
28
28
|
readonly enabled: boolean;
|
|
29
|
-
/** Menu id:
|
|
29
|
+
/** Menu id: openrouter · openai · anthropic · gemini · lmstudio · custom · mock · … */
|
|
30
30
|
readonly provider: string | null;
|
|
31
31
|
/** Protocol driver id written to `drivers.ai`. */
|
|
32
32
|
readonly driver: string | null;
|
|
33
33
|
/** Chat model id (from wizard / `oke ai setup`). */
|
|
34
34
|
readonly chatModel?: string | null;
|
|
35
|
-
/** Vision model id
|
|
35
|
+
/** Vision model id. */
|
|
36
36
|
readonly visionModel?: string | null;
|
|
37
|
-
/** Embedding model id
|
|
37
|
+
/** Embedding model id. */
|
|
38
38
|
readonly embedModel?: string | null;
|
|
39
39
|
/** Optional OpenAI-compatible base URL. */
|
|
40
40
|
readonly baseUrl?: string | null;
|
|
@@ -47,21 +47,15 @@ describe("catalog labels", () => {
|
|
|
47
47
|
});
|
|
48
48
|
|
|
49
49
|
describe("recommendedAiApply", () => {
|
|
50
|
-
test("returns
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
expect(apply.image).not.toContain("latest");
|
|
60
|
-
expect(apply.visionModel).toBeNull();
|
|
61
|
-
} finally {
|
|
62
|
-
if (prev !== undefined) process.env.OKE_AI_URL = prev;
|
|
63
|
-
else delete process.env.OKE_AI_URL;
|
|
64
|
-
}
|
|
50
|
+
test("returns OpenRouter with openrouter/free (no Docker image)", () => {
|
|
51
|
+
const apply = recommendedAiApply();
|
|
52
|
+
expect(apply.driver).toBe("openai-compatible");
|
|
53
|
+
expect(apply.provider).toBe("openrouter");
|
|
54
|
+
expect(apply.chatModel).toBe("openrouter/free");
|
|
55
|
+
expect(apply.baseUrl).toBeUndefined();
|
|
56
|
+
expect(apply.apiKeyEnv).toBe("OPENROUTER_API_KEY");
|
|
57
|
+
expect(apply.image).toBeUndefined();
|
|
58
|
+
expect(apply.visionModel).toBeNull();
|
|
65
59
|
});
|
|
66
60
|
});
|
|
67
61
|
|
package/src/customize-flow.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Customize wizard — walk Docker-first facets once, assemble `{ dev, test, prod }`.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { isCancel, password } from "@clack/prompts";
|
|
5
6
|
import { toCreateDefaults, type CreateDefaults, type EnvDriverPins } from "./create-defaults.ts";
|
|
6
7
|
import {
|
|
7
8
|
CLOCK_CHOICES,
|
|
@@ -14,20 +15,18 @@ import {
|
|
|
14
15
|
TEMPLATE_DEV,
|
|
15
16
|
TEMPLATE_TEST,
|
|
16
17
|
VAULT_CHOICES,
|
|
17
|
-
AI_PROVIDERS,
|
|
18
|
-
LLAMA_CPP_IMAGE,
|
|
19
|
-
OLLAMA_IMAGE,
|
|
20
|
-
SGLANG_IMAGE,
|
|
21
|
-
VLLM_IMAGE,
|
|
22
|
-
aiDriverForProvider,
|
|
23
18
|
customizeFacetsFor,
|
|
24
19
|
pinsDockerReady,
|
|
25
20
|
pinsEnv,
|
|
26
|
-
type AiProviderId,
|
|
27
21
|
type CustomizeFacetId,
|
|
28
22
|
type DriverChoice,
|
|
29
23
|
} from "./drivers-catalog.ts";
|
|
30
24
|
import type { AiSetupApplyInput } from "./ai-setup/apply.ts";
|
|
25
|
+
import {
|
|
26
|
+
aiDriverForMenuProvider,
|
|
27
|
+
aiProviderSelectOptions,
|
|
28
|
+
cloudApplyDefaults,
|
|
29
|
+
} from "./ai-setup/catalog.ts";
|
|
31
30
|
import { aiPrefWithModels } from "./ai-setup/from-pref.ts";
|
|
32
31
|
import { askAiSetup } from "./ai-setup/prompts.ts";
|
|
33
32
|
import type { TemplateId } from "./templates.ts";
|
|
@@ -95,54 +94,42 @@ export function assembleDriverDefaults(picked: SidePins): CreateDefaults["driver
|
|
|
95
94
|
}
|
|
96
95
|
|
|
97
96
|
/**
|
|
98
|
-
*
|
|
99
|
-
*
|
|
97
|
+
* Cloud defaults for "AI setup → Recommended" (no quiz).
|
|
98
|
+
* OpenRouter registry provider + `openrouter/free` (zero Docker).
|
|
100
99
|
*/
|
|
101
100
|
export function recommendedAiApply(): AiSetupApplyInput {
|
|
102
|
-
return
|
|
103
|
-
driver: "openai-compatible",
|
|
104
|
-
baseUrl: process.env.OKE_AI_URL ?? "http://127.0.0.1:8080/v1",
|
|
105
|
-
chatModel: "granite3.3:2b",
|
|
106
|
-
visionModel: null,
|
|
107
|
-
embedModel: null,
|
|
108
|
-
image: LLAMA_CPP_IMAGE,
|
|
109
|
-
};
|
|
101
|
+
return cloudApplyDefaults("openrouter");
|
|
110
102
|
}
|
|
111
103
|
|
|
112
104
|
/**
|
|
113
|
-
*
|
|
105
|
+
* Recommended path — OpenRouter defaults, then prompt for `OPENROUTER_API_KEY`.
|
|
114
106
|
*
|
|
115
|
-
* @
|
|
116
|
-
* @param provider - Menu provider id
|
|
107
|
+
* @returns Apply input with token, or null on cancel
|
|
117
108
|
*/
|
|
118
|
-
export function
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
return {
|
|
131
|
-
...input,
|
|
132
|
-
image: VLLM_IMAGE,
|
|
133
|
-
baseUrl: input.baseUrl ?? "http://127.0.0.1:8000/v1",
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
if (provider === "sglang") {
|
|
137
|
-
return {
|
|
138
|
-
...input,
|
|
139
|
-
image: SGLANG_IMAGE,
|
|
140
|
-
baseUrl: input.baseUrl ?? "http://127.0.0.1:30000/v1",
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
return input;
|
|
109
|
+
export async function askRecommendedAiApply(): Promise<AiSetupApplyInput | null> {
|
|
110
|
+
const base = recommendedAiApply();
|
|
111
|
+
const apiKeyEnv = base.apiKeyEnv ?? "OPENROUTER_API_KEY";
|
|
112
|
+
const token = await password({
|
|
113
|
+
message: `API token (${apiKeyEnv})`,
|
|
114
|
+
validate: (v) => {
|
|
115
|
+
if (!v?.trim()) return "API token is required";
|
|
116
|
+
return undefined;
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
if (isCancel(token)) return null;
|
|
120
|
+
return { ...base, apiKey: String(token).trim() };
|
|
144
121
|
}
|
|
145
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Result of a completed Customize pass — defaults for persistence plus the
|
|
125
|
+
* live {@link AiSetupApplyInput} (includes the API token; never write the
|
|
126
|
+
* token into `create-defaults.json`).
|
|
127
|
+
*/
|
|
128
|
+
export type CustomizeFlowResult = {
|
|
129
|
+
readonly defaults: CreateDefaults;
|
|
130
|
+
readonly aiApply: AiSetupApplyInput | null;
|
|
131
|
+
};
|
|
132
|
+
|
|
146
133
|
/**
|
|
147
134
|
* Customize drivers for one template.
|
|
148
135
|
*
|
|
@@ -150,7 +137,7 @@ export function withLocalAiImage(input: AiSetupApplyInput, provider: string): Ai
|
|
|
150
137
|
*/
|
|
151
138
|
export async function askCustomizeFlow(
|
|
152
139
|
template: TemplateId,
|
|
153
|
-
): Promise<
|
|
140
|
+
): Promise<CustomizeFlowResult | WizardBack | null> {
|
|
154
141
|
const facets = customizeFacetsFor(template);
|
|
155
142
|
const primaryPins = await walkFacets(facets);
|
|
156
143
|
if (primaryPins === null) return null;
|
|
@@ -163,14 +150,14 @@ export async function askCustomizeFlow(
|
|
|
163
150
|
let aiPref: CreateDefaults["ai"] = { enabled: false, provider: null, driver: null };
|
|
164
151
|
let aiApply: AiSetupApplyInput | null = null;
|
|
165
152
|
|
|
166
|
-
|
|
153
|
+
for (;;) {
|
|
167
154
|
const aiSetup = await selectWithBack(
|
|
168
155
|
"AI setup",
|
|
169
156
|
[
|
|
170
157
|
{
|
|
171
158
|
value: "recommended",
|
|
172
159
|
label: "Recommended",
|
|
173
|
-
hint: "
|
|
160
|
+
hint: "openrouter · openrouter/free",
|
|
174
161
|
},
|
|
175
162
|
{
|
|
176
163
|
value: "customize",
|
|
@@ -193,11 +180,13 @@ export async function askCustomizeFlow(
|
|
|
193
180
|
if (aiSetup === "off") break;
|
|
194
181
|
if (aiSetup === "recommended") {
|
|
195
182
|
aiPins = pinsDockerReady("openai-compatible", "mock");
|
|
196
|
-
|
|
183
|
+
const picked = await askRecommendedAiApply();
|
|
184
|
+
if (picked === null) return null;
|
|
185
|
+
aiApply = picked;
|
|
197
186
|
aiPref = aiPrefWithModels(
|
|
198
187
|
{
|
|
199
188
|
enabled: true,
|
|
200
|
-
provider: "
|
|
189
|
+
provider: "openrouter",
|
|
201
190
|
driver: "openai-compatible",
|
|
202
191
|
},
|
|
203
192
|
aiApply,
|
|
@@ -205,21 +194,21 @@ export async function askCustomizeFlow(
|
|
|
205
194
|
break;
|
|
206
195
|
}
|
|
207
196
|
|
|
208
|
-
const options =
|
|
209
|
-
const providerValue = await selectWithBack("AI Provider", options, "
|
|
197
|
+
const options = aiProviderSelectOptions({ includeMock: true });
|
|
198
|
+
const providerValue = await selectWithBack("AI Provider", options, "openrouter", {
|
|
210
199
|
allowBack: true,
|
|
211
200
|
});
|
|
212
201
|
if (providerValue === null) return null;
|
|
213
202
|
if (providerValue === WIZARD_BACK) continue;
|
|
214
203
|
|
|
215
|
-
const provider = providerValue
|
|
216
|
-
const driver =
|
|
204
|
+
const provider = providerValue;
|
|
205
|
+
const driver = aiDriverForMenuProvider(provider);
|
|
217
206
|
aiPins = pinsDockerReady(driver, "mock");
|
|
218
207
|
|
|
219
208
|
if (provider !== "mock") {
|
|
220
209
|
const picked = await askAiSetup({ provider });
|
|
221
210
|
if (picked === null) return null;
|
|
222
|
-
aiApply =
|
|
211
|
+
aiApply = picked;
|
|
223
212
|
} else {
|
|
224
213
|
aiApply = { driver: "mock" };
|
|
225
214
|
}
|
|
@@ -235,16 +224,20 @@ export async function askCustomizeFlow(
|
|
|
235
224
|
break;
|
|
236
225
|
}
|
|
237
226
|
|
|
238
|
-
return
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
227
|
+
return {
|
|
228
|
+
defaults: toCreateDefaults({
|
|
229
|
+
template,
|
|
230
|
+
profile: "docker-ready",
|
|
231
|
+
drivers: { ...drivers, ai: aiPins },
|
|
232
|
+
ai: aiPref,
|
|
233
|
+
// Locales / PgDog / proxy are asked once in the main wizard (not per customize pass).
|
|
234
|
+
locales: [],
|
|
235
|
+
pgdog: false,
|
|
236
|
+
proxy: "none",
|
|
237
|
+
}),
|
|
238
|
+
// Keep the live token here — prefs persist only `apiKeyEnv`, never the value.
|
|
239
|
+
aiApply,
|
|
240
|
+
};
|
|
248
241
|
}
|
|
249
242
|
|
|
250
243
|
/**
|
package/src/drivers-catalog.ts
CHANGED
|
@@ -108,29 +108,25 @@ export const EMAIL_CHOICES: readonly DriverChoice[] = [
|
|
|
108
108
|
{ value: "taqnyat-mail", label: "taqnyat" },
|
|
109
109
|
];
|
|
110
110
|
|
|
111
|
-
/**
|
|
112
|
-
* Pinned local-inference images — must match `src/docker/recipes/*` pins.
|
|
113
|
-
* Never `latest` (GGUF-parser CVE floors: llama.cpp ≥ b8146, Ollama ≥ 0.17.1).
|
|
114
|
-
*/
|
|
115
|
-
export const LLAMA_CPP_IMAGE = "ghcr.io/ggml-org/llama.cpp:server-b10450";
|
|
116
|
-
/** @see LLAMA_CPP_IMAGE */
|
|
117
|
-
export const OLLAMA_IMAGE = "ollama/ollama:0.32.13";
|
|
118
|
-
/** @see LLAMA_CPP_IMAGE */
|
|
119
|
-
export const VLLM_IMAGE = "vllm/vllm-openai:v0.27.1";
|
|
120
|
-
/** @see LLAMA_CPP_IMAGE */
|
|
121
|
-
export const SGLANG_IMAGE = "lmsysorg/sglang:v0.5.17-runtime";
|
|
122
|
-
|
|
123
|
-
/** AI menu providers → protocol driver. */
|
|
111
|
+
/** AI menu providers → protocol driver (kept for call-site compat). */
|
|
124
112
|
export const AI_PROVIDERS = [
|
|
125
|
-
{
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
113
|
+
{
|
|
114
|
+
value: "openrouter",
|
|
115
|
+
label: "OpenRouter",
|
|
116
|
+
driver: "openai-compatible",
|
|
117
|
+
hint: "recommended · zero Docker · openrouter/free",
|
|
118
|
+
},
|
|
129
119
|
{ value: "openai", label: "OpenAI", driver: "openai-compatible" },
|
|
130
120
|
{ value: "anthropic", label: "Anthropic", driver: "anthropic" },
|
|
131
|
-
{ value: "
|
|
121
|
+
{ value: "groq", label: "Groq", driver: "openai-compatible" },
|
|
122
|
+
{ value: "together", label: "Together AI", driver: "openai-compatible" },
|
|
123
|
+
{ value: "deepseek", label: "DeepSeek", driver: "openai-compatible" },
|
|
124
|
+
{ value: "mistral", label: "Mistral", driver: "openai-compatible" },
|
|
125
|
+
{ value: "xai", label: "xAI (Grok)", driver: "openai-compatible" },
|
|
126
|
+
{ value: "deepinfra", label: "DeepInfra", driver: "openai-compatible" },
|
|
127
|
+
{ value: "vercel", label: "Vercel AI Gateway", driver: "openai-compatible" },
|
|
128
|
+
{ value: "gemini", label: "Google Gemini", driver: "openai-compatible" },
|
|
132
129
|
{ value: "lmstudio", label: "LM Studio", driver: "openai-compatible" },
|
|
133
|
-
{ value: "openrouter", label: "OpenRouter", driver: "openai-compatible" },
|
|
134
130
|
{ value: "custom", label: "Custom OpenAI Compatible", driver: "openai-compatible" },
|
|
135
131
|
{ value: "mock", label: "Mock (dev only)", driver: "mock" },
|
|
136
132
|
] as const;
|
|
@@ -230,12 +226,11 @@ export function aiDriverForProvider(provider: string): string {
|
|
|
230
226
|
/** Default image pins keyed by role (standard template). */
|
|
231
227
|
export const DEFAULT_IMAGES: Readonly<Record<string, string>> = {
|
|
232
228
|
"store.sql": "postgres:18-alpine",
|
|
233
|
-
pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.
|
|
229
|
+
pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.57",
|
|
234
230
|
"store.kv": "redis:8-alpine",
|
|
235
|
-
"store.files": "rustfs/rustfs:1.0.0-rc.
|
|
236
|
-
"channel.email": "axllent/mailpit:v1.
|
|
231
|
+
"store.files": "rustfs/rustfs:1.0.0-rc.5",
|
|
232
|
+
"channel.email": "axllent/mailpit:v1.31.1",
|
|
237
233
|
"store.index": "getmeili/meilisearch:v1.53",
|
|
238
|
-
ai: LLAMA_CPP_IMAGE,
|
|
239
234
|
proxy: "caddy:2-alpine",
|
|
240
235
|
};
|
|
241
236
|
|
package/src/transform.test.ts
CHANGED
|
@@ -104,6 +104,23 @@ describe("vault backend defaults", () => {
|
|
|
104
104
|
const env = readFileSync(join(resolveTemplateDir(id), ".env.example"), "utf8");
|
|
105
105
|
expect(env, id).toContain("OKE_VAULT_MASTER_KEY");
|
|
106
106
|
expect(env, id).toContain("oke vault init");
|
|
107
|
+
expect(env, id).toContain("src/vault.ts");
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("both templates declare stack vault.secret / vault.config contracts", () => {
|
|
112
|
+
for (const id of ["standard", "advanced"] as const) {
|
|
113
|
+
const vault = readFileSync(join(resolveTemplateDir(id), "src/vault.ts"), "utf8");
|
|
114
|
+
expect(vault, id).toContain('vault.secret("APP_WEBHOOK_SECRET"');
|
|
115
|
+
expect(vault, id).toContain('vault.secret("DATABASE_URL"');
|
|
116
|
+
expect(vault, id).toContain('vault.config("OKE_APP_URL"');
|
|
117
|
+
expect(vault, id).toContain('vault.config("PUBLIC_API_URL"');
|
|
118
|
+
expect(vault, id).toContain('dev: "http://127.0.0.1:6530"');
|
|
119
|
+
expect(vault, id).not.toMatch(/PUBLIC_API_URL[\s\S]*?dev:\s*""/);
|
|
120
|
+
expect(vault, id).toContain("export const NOTES_VAULT");
|
|
121
|
+
expect(vault, id).toContain("vault.fromDocker");
|
|
122
|
+
const app = readFileSync(join(resolveTemplateDir(id), "src/app.ts"), "utf8");
|
|
123
|
+
expect(app, id).toContain("secrets: NOTES_VAULT");
|
|
107
124
|
}
|
|
108
125
|
});
|
|
109
126
|
|
|
@@ -138,24 +155,24 @@ describe("vault backend defaults", () => {
|
|
|
138
155
|
});
|
|
139
156
|
|
|
140
157
|
describe("upsertAiDrivers", () => {
|
|
141
|
-
const
|
|
142
|
-
dev: "
|
|
158
|
+
const localAiPins: EnvDriverPins = {
|
|
159
|
+
dev: "openai-compatible",
|
|
143
160
|
test: "mock",
|
|
144
|
-
prod: "
|
|
161
|
+
prod: "openai-compatible",
|
|
145
162
|
};
|
|
146
163
|
|
|
147
164
|
test("inserts drivers.ai inside drivers (not images / channel)", () => {
|
|
148
|
-
const next = upsertAiDrivers(templateConfig("advanced"),
|
|
165
|
+
const next = upsertAiDrivers(templateConfig("advanced"), localAiPins);
|
|
149
166
|
const config = evalConfig(next);
|
|
150
|
-
expect(config.drivers?.ai).toEqual(
|
|
167
|
+
expect(config.drivers?.ai).toEqual(localAiPins);
|
|
151
168
|
expect(config.drivers?.channel?.ai).toBeUndefined();
|
|
152
169
|
// Sparse templates omit drivers.channel — images.channel stays a string pin.
|
|
153
|
-
expect(config.images?.channel?.email).toBe("axllent/mailpit:v1.
|
|
170
|
+
expect(config.images?.channel?.email).toBe("axllent/mailpit:v1.31.1");
|
|
154
171
|
expect(config.images?.ai).toBeUndefined();
|
|
155
172
|
});
|
|
156
173
|
|
|
157
174
|
test("applyCreateAnswers with ai pins keeps top-level drivers.ai", () => {
|
|
158
|
-
const
|
|
175
|
+
const aiPins = pinsDockerReady("openai-compatible", "mock");
|
|
159
176
|
for (const id of ["standard", "advanced"] as const) {
|
|
160
177
|
const next = applyCreateAnswers(
|
|
161
178
|
templateConfig(id),
|
|
@@ -173,9 +190,9 @@ describe("upsertAiDrivers", () => {
|
|
|
173
190
|
clock: pinsDockerReady("postgres", "frozen"),
|
|
174
191
|
vault: pinsDockerReady("vault", "memory"),
|
|
175
192
|
channel: { email: pinsDockerReady("smtp", "console") },
|
|
176
|
-
ai:
|
|
193
|
+
ai: aiPins,
|
|
177
194
|
},
|
|
178
|
-
ai: { enabled: true, provider: "
|
|
195
|
+
ai: { enabled: true, provider: "openrouter", driver: "openai-compatible" },
|
|
179
196
|
locales: [],
|
|
180
197
|
pgdog: false,
|
|
181
198
|
proxy: "none",
|
|
@@ -188,7 +205,49 @@ describe("upsertAiDrivers", () => {
|
|
|
188
205
|
prod: "openai-compatible",
|
|
189
206
|
});
|
|
190
207
|
expect(config.drivers?.channel?.ai, id).toBeUndefined();
|
|
191
|
-
expect(config.images?.ai, id).
|
|
208
|
+
expect(config.images?.ai, id).toBeUndefined();
|
|
192
209
|
}
|
|
193
210
|
});
|
|
211
|
+
|
|
212
|
+
test("applyCreateAnswers with OpenRouter does not pin images.ai", () => {
|
|
213
|
+
const cloudPins = pinsDockerReady("openai-compatible", "mock");
|
|
214
|
+
const next = applyCreateAnswers(
|
|
215
|
+
templateConfig("advanced"),
|
|
216
|
+
toCreateDefaults({
|
|
217
|
+
template: "advanced",
|
|
218
|
+
profile: "docker-ready",
|
|
219
|
+
drivers: {
|
|
220
|
+
store: {
|
|
221
|
+
sql: pinsDockerReady("postgres", "pglite"),
|
|
222
|
+
kv: pinsDockerReady("redis", "memory"),
|
|
223
|
+
files: pinsDockerReady("s3", "memory"),
|
|
224
|
+
index: null,
|
|
225
|
+
},
|
|
226
|
+
signal: pinsDockerReady("redis", "memory"),
|
|
227
|
+
clock: pinsDockerReady("postgres", "frozen"),
|
|
228
|
+
vault: pinsDockerReady("vault", "memory"),
|
|
229
|
+
channel: { email: pinsDockerReady("smtp", "console") },
|
|
230
|
+
ai: cloudPins,
|
|
231
|
+
},
|
|
232
|
+
ai: {
|
|
233
|
+
enabled: true,
|
|
234
|
+
provider: "openrouter",
|
|
235
|
+
driver: "openai-compatible",
|
|
236
|
+
chatModel: "openrouter/free",
|
|
237
|
+
apiKeyEnv: "OPENROUTER_API_KEY",
|
|
238
|
+
},
|
|
239
|
+
locales: [],
|
|
240
|
+
pgdog: false,
|
|
241
|
+
proxy: "none",
|
|
242
|
+
}),
|
|
243
|
+
);
|
|
244
|
+
const config = evalConfig(next);
|
|
245
|
+
expect(config.drivers?.ai).toEqual({
|
|
246
|
+
dev: "openai-compatible",
|
|
247
|
+
test: "mock",
|
|
248
|
+
prod: "openai-compatible",
|
|
249
|
+
});
|
|
250
|
+
expect(config.images?.ai).toBeUndefined();
|
|
251
|
+
expect(extractImages(next).ai).toBeUndefined();
|
|
252
|
+
});
|
|
194
253
|
});
|