create-oke 0.10.3 → 0.11.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.
Files changed (57) hide show
  1. package/README.md +2 -2
  2. package/package.json +2 -2
  3. package/src/ai-setup/apply.ts +113 -17
  4. package/src/ai-setup/catalog.ts +3 -3
  5. package/src/ai-setup/from-pref.ts +1 -1
  6. package/src/ai-setup/prompts.ts +1 -1
  7. package/src/cli.test.ts +183 -142
  8. package/src/cli.ts +190 -32
  9. package/src/create-defaults.test.ts +21 -15
  10. package/src/create-defaults.ts +33 -13
  11. package/src/customize-flow.test.ts +30 -35
  12. package/src/customize-flow.ts +68 -229
  13. package/src/drivers-catalog.ts +42 -48
  14. package/src/local-okengine.test.ts +50 -41
  15. package/src/local-okengine.ts +28 -14
  16. package/src/locales.test.ts +128 -0
  17. package/src/locales.ts +321 -0
  18. package/src/scaffold.ts +277 -33
  19. package/src/templates.ts +2 -8
  20. package/src/transform.test.ts +96 -36
  21. package/src/transform.ts +285 -110
  22. package/templates/advanced/.env.example +23 -3
  23. package/templates/advanced/README.md +1 -1
  24. package/templates/advanced/drizzle.config.ts +10 -13
  25. package/templates/advanced/oke.config.ts +12 -57
  26. package/templates/advanced/package.json +3 -1
  27. package/templates/advanced/src/app.ts +3 -10
  28. package/templates/advanced/src/core.ts +58 -0
  29. package/templates/advanced/src/db/schema.decl.ts +1 -1
  30. package/templates/advanced/src/db/seed/index.ts +2 -2
  31. package/templates/advanced/src/flows/notes/index.ts +1 -13
  32. package/templates/advanced/src/locales/index.ts +6 -0
  33. package/templates/advanced/tests/advanced.test.ts +1 -1
  34. package/templates/standard/.env.example +23 -3
  35. package/templates/standard/README.md +1 -1
  36. package/templates/standard/drizzle.config.ts +10 -13
  37. package/templates/standard/oke.config.ts +10 -56
  38. package/templates/standard/package.json +3 -1
  39. package/templates/standard/src/app.ts +3 -10
  40. package/templates/standard/src/core.ts +55 -0
  41. package/templates/standard/src/db/schema.decl.ts +1 -1
  42. package/templates/standard/src/db/seed/index.ts +2 -2
  43. package/templates/standard/src/flows/notes/index.ts +1 -10
  44. package/templates/standard/src/locales/index.ts +6 -0
  45. package/templates/standard/tests/standard.test.ts +1 -1
  46. package/templates/advanced/src/core/channels.ts +0 -13
  47. package/templates/advanced/src/core/gates.ts +0 -12
  48. package/templates/advanced/src/core/index.ts +0 -12
  49. package/templates/advanced/src/core/store.ts +0 -8
  50. package/templates/advanced/src/core/vault.ts +0 -7
  51. package/templates/advanced/src/locales/ar.ts +0 -18
  52. package/templates/standard/src/core/channels.ts +0 -13
  53. package/templates/standard/src/core/gates.ts +0 -12
  54. package/templates/standard/src/core/index.ts +0 -12
  55. package/templates/standard/src/core/store.ts +0 -5
  56. package/templates/standard/src/core/vault.ts +0 -7
  57. package/templates/standard/src/locales/ar.ts +0 -18
package/README.md CHANGED
@@ -50,8 +50,8 @@ model selection runs in the wizard **before** `bun install` (writes `.env.local`
50
50
 
51
51
  Store-bearing scaffolds ship `drizzle.config.ts` and `.env.example`. Local
52
52
  `oke dev` auto-runs `oke db push` on schema change and asks once whether to
53
- seed; use `oke mode docker` / `oke dev --docker` for compose infra (OpenBao
54
- tokens and stack passwords are generated and preserved per project). Both
53
+ seed; compose stack passwords are generated and preserved per project
54
+ (built-in vault needs no extra container). Both
55
55
  templates include `.github/workflows/ci.yml` (typecheck + `bun test` on push/PR).
56
56
 
57
57
  Every scaffold writes an `AGENTS.md` so coding agents know the OKE contract
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-oke",
3
- "version": "0.10.3",
3
+ "version": "0.11.0",
4
4
  "description": "Scaffold an okengine app — bunx create-oke@latest <name>",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -22,7 +22,7 @@
22
22
  },
23
23
  "scripts": {
24
24
  "typecheck": "tsc --noEmit",
25
- "test": "bun test"
25
+ "test": "bun test src"
26
26
  },
27
27
  "dependencies": {
28
28
  "@clack/prompts": "^1.7.0"
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Write AI driver config, env, and `src/core/ai.ts` for `oke ai setup`.
2
+ * Write AI driver config, env, and AI models into `src/core.ts` for `oke ai setup`.
3
3
  */
4
4
 
5
5
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
@@ -70,13 +70,15 @@ export function applyAiSetup(
70
70
  : existsSync(envExample)
71
71
  ? readFileSync(envExample, "utf8")
72
72
  : "";
73
- // Driver / URL stay commented so docker mode can hydrate `OKE_AI_URL` from
74
- // `docker/.env.docker` without being shadowed. The selected chat model is
75
- // written active `oke dev` seeds it into stack controls / `LLAMA_ARG_DOCKER_REPO`
76
- // (commented-only left the compose default `smollm2` forever).
73
+ // Driver / URL / model stay commented so docker mode can hydrate from
74
+ // `docker/.env.docker` without being shadowed. The chosen model is still
75
+ // written into `.env.local` as a hint (and into `.env.docker` when present);
76
+ // `loadExistingStackControls` seeds commented `OKE_AI_MODEL` on first boot.
77
77
  env = upsertEnv(env, "OKE_AI_DRIVER", input.driver, { comment: true });
78
78
  if (input.baseUrl) env = upsertEnv(env, "OKE_AI_URL", input.baseUrl, { comment: true });
79
- if (input.chatModel) env = upsertEnv(env, "OKE_AI_MODEL", input.chatModel);
79
+ if (input.chatModel) {
80
+ env = upsertEnv(env, "OKE_AI_MODEL", input.chatModel, { comment: true });
81
+ }
80
82
  if (input.visionModel) {
81
83
  env = upsertEnv(env, "OKE_AI_VISION_MODEL", input.visionModel, { comment: true });
82
84
  }
@@ -92,10 +94,7 @@ export function applyAiSetup(
92
94
  }
93
95
  writeFileSync(envPath, env.endsWith("\n") ? env : `${env}\n`, "utf8");
94
96
 
95
- const aiTsPath = join(cwd, "src", "core", "ai.ts");
96
- mkdirSync(dirname(aiTsPath), { recursive: true });
97
- writeFileSync(aiTsPath, renderAiTs(input), "utf8");
98
- ensureAiImported(cwd);
97
+ const aiTsPath = writeAiModels(cwd, input);
99
98
 
100
99
  return { configPath, envPath, aiTsPath };
101
100
  }
@@ -105,11 +104,11 @@ export function applyAiSetup(
105
104
  * @param driver - AI driver id
106
105
  */
107
106
  export function upsertAiDrivers(source: string, driver: string): string {
107
+ const runtime = driver === "mock" ? "mock" : driver;
108
108
  const block = `{
109
- local: "${driver}",
110
- docker: "${driver === "mock" ? "mock" : driver}",
109
+ dev: "${runtime}",
111
110
  test: "mock",
112
- prod: "${driver === "mock" ? "mock" : driver}",
111
+ prod: "${runtime}",
113
112
  }`;
114
113
  // Top-level drivers.ai only (4-space indent) — never nest under channel.email.
115
114
  const aiRe = /^( {4})ai:\s*\{[\s\S]*?\n\1\}/m;
@@ -226,16 +225,113 @@ export function renderAiTs(input: AiSetupApplyInput): string {
226
225
  }
227
226
 
228
227
  /**
229
- * Ensure `src/app.ts` or `src/core/index.ts` imports `./core/ai` / `./ai`.
228
+ * Write AI model declarations into `src/core.ts` (preferred) or legacy `src/core/ai.ts`.
229
+ *
230
+ * @param cwd - Project root
231
+ * @param input - Setup choices
232
+ * @returns Path written
233
+ */
234
+ function writeAiModels(cwd: string, input: AiSetupApplyInput): string {
235
+ const rendered = renderAiTs(input);
236
+ const coreTsPath = join(cwd, "src", "core.ts");
237
+ const legacyIndex = join(cwd, "src", "core", "index.ts");
238
+
239
+ // Folder layout still in the wild — keep writing a sidecar.
240
+ if (!existsSync(coreTsPath) && existsSync(legacyIndex)) {
241
+ const aiTsPath = join(cwd, "src", "core", "ai.ts");
242
+ mkdirSync(dirname(aiTsPath), { recursive: true });
243
+ writeFileSync(aiTsPath, rendered, "utf8");
244
+ ensureLegacyAiImported(cwd);
245
+ return aiTsPath;
246
+ }
247
+
248
+ mkdirSync(dirname(coreTsPath), { recursive: true });
249
+ if (existsSync(coreTsPath)) {
250
+ const existing = readFileSync(coreTsPath, "utf8");
251
+ if (hasAiModels(existing)) {
252
+ return coreTsPath;
253
+ }
254
+ writeFileSync(coreTsPath, mergeAiIntoCore(existing, rendered), "utf8");
255
+ } else {
256
+ writeFileSync(coreTsPath, rendered, "utf8");
257
+ }
258
+ ensureCoreImported(cwd);
259
+ return coreTsPath;
260
+ }
261
+
262
+ /**
263
+ * @param source - Existing TypeScript
264
+ */
265
+ function hasAiModels(source: string): boolean {
266
+ return (
267
+ /\bai\.model\s*\(/.test(source) ||
268
+ /from\s+["']\.\/(?:core\/)?ai["']/.test(source) ||
269
+ /import\s+["']\.\/(?:core\/)?ai["']/.test(source)
270
+ );
271
+ }
272
+
273
+ /**
274
+ * Merge rendered AI module into an existing `src/core.ts`.
275
+ *
276
+ * @param existing - Current core.ts
277
+ * @param rendered - Output of {@link renderAiTs}
278
+ */
279
+ export function mergeAiIntoCore(existing: string, rendered: string): string {
280
+ const body = rendered.replace(/^import\s*\{\s*ai\s*\}\s*from\s*["']okengine["'];\s*\n*/m, "");
281
+ const next = ensureNamedOkengineImport(existing, "ai");
282
+ return `${next.trimEnd()}\n\n${body.trimStart()}`;
283
+ }
284
+
285
+ /**
286
+ * Ensure a named binding is imported from `"okengine"`.
287
+ *
288
+ * @param source - Module source
289
+ * @param name - Binding to add
290
+ */
291
+ export function ensureNamedOkengineImport(source: string, name: string): string {
292
+ const re = /import\s*\{([^}]*)\}\s*from\s*["']okengine["']\s*;/;
293
+ const m = re.exec(source);
294
+ if (!m) {
295
+ return `import { ${name} } from "okengine";\n${source}`;
296
+ }
297
+ const names = m[1]!
298
+ .split(",")
299
+ .map((s) => s.trim())
300
+ .filter(Boolean);
301
+ if (names.includes(name)) return source;
302
+ const sorted = [...names, name].sort((a, b) => a.localeCompare(b));
303
+ return source.replace(re, `import { ${sorted.join(", ")} } from "okengine";`);
304
+ }
305
+
306
+ /**
307
+ * Ensure `src/app.ts` loads `@/core` / `./core` so merged AI registers.
308
+ *
309
+ * @param cwd - Project root
310
+ */
311
+ function ensureCoreImported(cwd: string): void {
312
+ const appPath = join(cwd, "src", "app.ts");
313
+ if (!existsSync(appPath)) return;
314
+ const src = readFileSync(appPath, "utf8");
315
+ if (
316
+ /from\s+["']@\/core["']/.test(src) ||
317
+ /import\s+["']@\/core["']/.test(src) ||
318
+ /from\s+["']\.\/core(?:\.ts)?["']/.test(src) ||
319
+ /import\s+["']\.\/core(?:\.ts)?["']/.test(src)
320
+ ) {
321
+ return;
322
+ }
323
+ writeFileSync(appPath, `import "@/core";\n${src}`, "utf8");
324
+ }
325
+
326
+ /**
327
+ * Legacy `src/core/*` layout — side-effect import the AI sidecar.
230
328
  *
231
329
  * @param cwd - Project root
232
330
  */
233
- function ensureAiImported(cwd: string): void {
331
+ function ensureLegacyAiImported(cwd: string): void {
234
332
  const candidates: ReadonlyArray<{ readonly rel: string; readonly importLine: string }> = [
235
333
  { rel: "src/app.ts", importLine: `import "./core/ai";\n` },
236
334
  { rel: "src/core/index.ts", importLine: `import "./ai";\n` },
237
- // Legacy layout
238
- { rel: "src/core.ts", importLine: `import "./ai";\n` },
239
335
  ];
240
336
  for (const { rel, importLine } of candidates) {
241
337
  const path = join(cwd, rel);
@@ -788,9 +788,8 @@ export const LLAMA_CPP_CHAT_MODELS: readonly CatalogModel[] = [
788
788
  {
789
789
  id: "smollm2",
790
790
  label: "SmolLM2",
791
- hint: "Default · lightest",
791
+ hint: "Lightest",
792
792
  role: "chat",
793
- recommended: true,
794
793
  ramGb: 1,
795
794
  tier: "ultra-fast",
796
795
  modalities: ["text"],
@@ -951,8 +950,9 @@ export const LLAMA_CPP_CHAT_MODELS: readonly CatalogModel[] = [
951
950
  {
952
951
  id: "granite3.3:2b",
953
952
  label: "Granite 3.3 2B",
954
- hint: "IBM 2B",
953
+ hint: "Default · IBM 2B",
955
954
  role: "chat",
955
+ recommended: true,
956
956
  ramGb: 2,
957
957
  tier: "ultra-fast",
958
958
  modalities: ["text", "code"],
@@ -68,7 +68,7 @@ export function nonInteractiveAiApply(provider: string): AiSetupApplyInput {
68
68
  return {
69
69
  driver: "openai-compatible",
70
70
  baseUrl: process.env.OKE_AI_URL ?? "http://127.0.0.1:8080/v1",
71
- chatModel: "smollm2",
71
+ chatModel: "granite3.3:2b",
72
72
  visionModel: null,
73
73
  embedModel: null,
74
74
  image: LLAMA_CPP_IMAGE,
@@ -154,7 +154,7 @@ async function askLlamaCppPath(options: {
154
154
  if (mode === null) return null;
155
155
 
156
156
  if (mode === "manual") {
157
- const id = await askOtherModelId("smollm2");
157
+ const id = await askOtherModelId("granite3.3:2b");
158
158
  if (id === null) return null;
159
159
  return finishLlamaCpp(id);
160
160
  }