create-oke 0.9.1 → 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.
Files changed (56) hide show
  1. package/README.md +7 -5
  2. package/package.json +2 -3
  3. package/src/agents-md.ts +1 -1
  4. package/src/ai-setup/apply.ts +66 -23
  5. package/src/ai-setup/catalog.ts +1316 -35
  6. package/src/ai-setup/detect-ollama.ts +48 -0
  7. package/src/ai-setup/from-pref.ts +32 -0
  8. package/src/ai-setup/prompts.ts +430 -486
  9. package/src/ai-setup/recommend.ts +118 -101
  10. package/src/cli.test.ts +36 -7
  11. package/src/cli.ts +21 -16
  12. package/src/customize-flow.test.ts +43 -13
  13. package/src/customize-flow.ts +136 -61
  14. package/src/drivers-catalog.ts +29 -12
  15. package/src/local-okengine.test.ts +59 -0
  16. package/src/local-okengine.ts +137 -0
  17. package/src/scaffold.ts +1 -1
  18. package/src/transform.test.ts +54 -32
  19. package/src/transform.ts +46 -9
  20. package/src/wizard-select.ts +5 -6
  21. package/templates/advanced/.github/workflows/ci.yml +24 -0
  22. package/templates/advanced/.vscode/settings.json +15 -0
  23. package/templates/advanced/README.md +15 -4
  24. package/templates/advanced/drizzle.config.ts +6 -4
  25. package/templates/advanced/oke.config.ts +6 -2
  26. package/templates/advanced/package.json +5 -2
  27. package/templates/advanced/src/app.ts +2 -14
  28. package/templates/advanced/src/core/index.ts +12 -0
  29. package/templates/advanced/src/{core.ts → core/store.ts} +1 -1
  30. package/templates/advanced/src/db/migrations/.gitkeep +0 -0
  31. package/templates/{standard/src → advanced/src/db}/schema.decl.ts +1 -1
  32. package/templates/advanced/src/{seed → db/seed}/index.ts +2 -2
  33. package/templates/advanced/src/flows/notes/index.ts +2 -4
  34. package/templates/advanced/tests/advanced.test.ts +10 -9
  35. package/templates/advanced/tsconfig.json +23 -0
  36. package/templates/standard/.github/workflows/ci.yml +24 -0
  37. package/templates/standard/.vscode/settings.json +15 -0
  38. package/templates/standard/README.md +20 -9
  39. package/templates/standard/drizzle.config.ts +6 -4
  40. package/templates/standard/oke.config.ts +4 -0
  41. package/templates/standard/package.json +5 -2
  42. package/templates/standard/src/app.ts +2 -14
  43. package/templates/standard/src/core/index.ts +12 -0
  44. package/templates/standard/src/{core.ts → core/store.ts} +1 -1
  45. package/templates/standard/src/db/migrations/.gitkeep +0 -0
  46. package/templates/{advanced/src → standard/src/db}/schema.decl.ts +1 -1
  47. package/templates/standard/src/{seed → db/seed}/index.ts +2 -2
  48. package/templates/standard/src/flows/notes/index.ts +2 -4
  49. package/templates/standard/tests/standard.test.ts +13 -10
  50. package/templates/standard/tsconfig.json +23 -0
  51. /package/templates/advanced/src/{channels.ts → core/channels.ts} +0 -0
  52. /package/templates/advanced/src/{gates.ts → core/gates.ts} +0 -0
  53. /package/templates/advanced/src/{vault.ts → core/vault.ts} +0 -0
  54. /package/templates/standard/src/{channels.ts → core/channels.ts} +0 -0
  55. /package/templates/standard/src/{gates.ts → core/gates.ts} +0 -0
  56. /package/templates/standard/src/{vault.ts → core/vault.ts} +0 -0
package/README.md CHANGED
@@ -18,7 +18,7 @@ On a TTY, create-oke opens a Clack wizard:
18
18
  user-global `~/.oke/create-defaults.json`) · **Reuse previous settings**
19
19
  (only when saved settings exist for the selected template).
20
20
  The project-name step rejects a path that already exists and is not empty.
21
- Customize steps include **← Back**. Scaffold writes `.oke/mode` from the
21
+ Customize steps include **Back**. Scaffold writes `.oke/mode` from the
22
22
  primary side (recommended: standard → `local`, advanced → `docker`).
23
23
  4. Install dependencies and start `oke dev`?
24
24
 
@@ -41,16 +41,18 @@ bunx create-oke@latest my-app --no-ai
41
41
 
42
42
  `--sql sqlite|postgres` is opt-in. The default keeps the dual-mode Store
43
43
  config (`local: sqlite` · `docker`/`prod: postgres`) and abstract
44
- `src/schema.decl.ts` (dialect emitted from the active driver). Pass
44
+ `src/db/schema.decl.ts` (dialect emitted from the active driver). Pass
45
45
  `--sql postgres` to pin `oke.config.ts` `store.sql` local/docker/prod to
46
46
  `postgres`. Customize can set drivers the template exposes; when AI is enabled,
47
47
  model selection runs in the wizard **before** `bun install` (writes `.env.local`
48
48
 
49
- - `src/ai.ts`).
49
+ - `src/core/ai.ts`).
50
50
 
51
51
  Store-bearing scaffolds ship `drizzle.config.ts` and `.env.example`. Local
52
- `oke dev` auto-runs `oke db push` on schema change; use `oke mode docker` /
53
- `oke dev --docker` for compose infra.
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
55
+ templates include `.github/workflows/ci.yml` (typecheck + `bun test` on push/PR).
54
56
 
55
57
  Every scaffold writes an `AGENTS.md` so coding agents know the OKE contract
56
58
  (`--no-agents-md` to skip).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-oke",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "description": "Scaffold an okengine app — bunx create-oke@latest <name>",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -22,8 +22,7 @@
22
22
  },
23
23
  "scripts": {
24
24
  "typecheck": "tsc --noEmit",
25
- "test": "bun test",
26
- "test:integration": "CREATE_OKE_INTEGRATION=1 bun test tests/scaffold.integration.test.ts tests/dev.integration.test.ts"
25
+ "test": "bun test"
27
26
  },
28
27
  "dependencies": {
29
28
  "@clack/prompts": "^1.7.0"
package/src/agents-md.ts CHANGED
@@ -67,7 +67,7 @@ are inferred from what a Flow touches through it.
67
67
 
68
68
  | Port | Surface |
69
69
  |---|---|
70
- | **6530** | App |
70
+ | **6530** | Backend |
71
71
  | **6533** | Console |
72
72
  | **6535** | MCP |
73
73
 
@@ -1,9 +1,10 @@
1
1
  /**
2
- * Write AI driver config, env, and `src/ai.ts` for `oke ai setup`.
2
+ * Write AI driver config, env, and `src/core/ai.ts` for `oke ai setup`.
3
3
  */
4
4
 
5
5
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
6
  import { dirname, join } from "node:path";
7
+ import { OLLAMA_IMAGE } from "../drivers-catalog.ts";
7
8
 
8
9
  /** Choices applied to the project. */
9
10
  export type AiSetupApplyInput = {
@@ -13,6 +14,10 @@ export type AiSetupApplyInput = {
13
14
  readonly visionModel?: string | null;
14
15
  readonly embedModel?: string | null;
15
16
  readonly apiKeyEnv?: string;
17
+ /** When set with {@link apiKeyEnv}, writes the token into `.env.local`. */
18
+ readonly apiKey?: string;
19
+ /** Optional `images.ai` pin (llama.cpp / Ollama / vLLM / SGLang). */
20
+ readonly image?: string;
16
21
  };
17
22
 
18
23
  /** Options for {@link applyAiSetup}. */
@@ -49,8 +54,10 @@ export function applyAiSetup(
49
54
  if (updateDrivers) {
50
55
  let config = readFileSync(configPath, "utf8");
51
56
  config = upsertAiDrivers(config, input.driver);
52
- if (input.driver === "ollama") {
53
- config = upsertImage(config, "ai", "ollama/ollama:latest");
57
+ if (input.image) {
58
+ config = upsertImage(config, "ai", input.image);
59
+ } else if (input.driver === "ollama") {
60
+ config = upsertImage(config, "ai", OLLAMA_IMAGE);
54
61
  }
55
62
  writeFileSync(configPath, config, "utf8");
56
63
  }
@@ -62,20 +69,29 @@ export function applyAiSetup(
62
69
  : existsSync(envExample)
63
70
  ? readFileSync(envExample, "utf8")
64
71
  : "";
65
- env = upsertEnv(env, "OKE_AI_DRIVER", input.driver);
66
- if (input.baseUrl) env = upsertEnv(env, "OKE_AI_URL", input.baseUrl);
72
+ // Driver / URL stay commented so docker mode can hydrate `OKE_AI_URL` from
73
+ // `docker/.env.docker` without being shadowed. The selected chat model is
74
+ // written active — `oke dev` seeds it into stack controls / `LLAMA_ARG_DOCKER_REPO`
75
+ // (commented-only left the compose default `smollm2` forever).
76
+ env = upsertEnv(env, "OKE_AI_DRIVER", input.driver, { comment: true });
77
+ if (input.baseUrl) env = upsertEnv(env, "OKE_AI_URL", input.baseUrl, { comment: true });
67
78
  if (input.chatModel) env = upsertEnv(env, "OKE_AI_MODEL", input.chatModel);
68
- if (input.visionModel) env = upsertEnv(env, "OKE_AI_VISION_MODEL", input.visionModel);
69
- if (input.embedModel) env = upsertEnv(env, "OKE_AI_EMBED_MODEL", input.embedModel);
79
+ if (input.visionModel) {
80
+ env = upsertEnv(env, "OKE_AI_VISION_MODEL", input.visionModel, { comment: true });
81
+ }
82
+ if (input.embedModel) {
83
+ env = upsertEnv(env, "OKE_AI_EMBED_MODEL", input.embedModel, { comment: true });
84
+ }
70
85
  if (input.apiKeyEnv) {
71
- // Ensure the key name is present (value left blank for the user)
72
- if (!new RegExp(`^${input.apiKeyEnv}=`, "m").test(env)) {
73
- env = `${env.trimEnd()}\n${input.apiKeyEnv}=\n`;
86
+ if (input.apiKey !== undefined && input.apiKey.length > 0) {
87
+ env = upsertEnv(env, input.apiKeyEnv, input.apiKey);
88
+ } else if (!new RegExp(`^#?\\s*${input.apiKeyEnv}=`, "m").test(env)) {
89
+ env = `${env.trimEnd()}\n# ${input.apiKeyEnv}=\n`;
74
90
  }
75
91
  }
76
92
  writeFileSync(envPath, env.endsWith("\n") ? env : `${env}\n`, "utf8");
77
93
 
78
- const aiTsPath = join(cwd, "src", "ai.ts");
94
+ const aiTsPath = join(cwd, "src", "core", "ai.ts");
79
95
  mkdirSync(dirname(aiTsPath), { recursive: true });
80
96
  writeFileSync(aiTsPath, renderAiTs(input), "utf8");
81
97
  ensureAiImported(cwd);
@@ -89,11 +105,11 @@ export function applyAiSetup(
89
105
  */
90
106
  export function upsertAiDrivers(source: string, driver: string): string {
91
107
  const block = `{
92
- local: "${driver}",
93
- docker: "${driver === "mock" ? "mock" : driver}",
94
- test: "mock",
95
- prod: "${driver === "mock" ? "mock" : driver}",
96
- }`;
108
+ local: "${driver}",
109
+ docker: "${driver === "mock" ? "mock" : driver}",
110
+ test: "mock",
111
+ prod: "${driver === "mock" ? "mock" : driver}",
112
+ }`;
97
113
  // Top-level drivers.ai only (4-space indent) — never nest under channel.email.
98
114
  const aiRe = /^( {4})ai:\s*\{[\s\S]*?\n\1\}/m;
99
115
  if (aiRe.test(source)) {
@@ -131,13 +147,30 @@ export function upsertImage(source: string, key: string, image: string): string
131
147
  return source.replace(imagesRe, `images: {${body}\n${line}\n }`);
132
148
  }
133
149
 
150
+ /** Options for {@link upsertEnv}. */
151
+ export type UpsertEnvOptions = {
152
+ /**
153
+ * When true, write `# KEY=value` (opt-in override). Used for AI stack keys
154
+ * so they do not shadow `docker/.env.docker`.
155
+ */
156
+ readonly comment?: boolean;
157
+ };
158
+
134
159
  /**
160
+ * Insert or replace a dotenv assignment (optionally commented).
161
+ *
135
162
  * @param env - dotenv text
136
163
  * @param key - Variable name
137
164
  * @param value - Value
165
+ * @param options - Comment vs active line
138
166
  */
139
- export function upsertEnv(env: string, key: string, value: string): string {
140
- const line = `${key}=${value}`;
167
+ export function upsertEnv(
168
+ env: string,
169
+ key: string,
170
+ value: string,
171
+ options: UpsertEnvOptions = {},
172
+ ): string {
173
+ const line = options.comment ? `# ${key}=${value}` : `${key}=${value}`;
141
174
  const re = new RegExp(`^#?\\s*${key}=.*$`, "m");
142
175
  if (re.test(env)) return env.replace(re, line);
143
176
  const aiSection = /(# ── AI[^\n]*\n)/;
@@ -197,18 +230,28 @@ export function renderAiTs(input: AiSetupApplyInput): string {
197
230
  }
198
231
 
199
232
  /**
200
- * Ensure `src/app.ts` (or `src/core.ts`) imports `./ai`.
233
+ * Ensure `src/app.ts` or `src/core/index.ts` imports `./core/ai` / `./ai`.
201
234
  *
202
235
  * @param cwd - Project root
203
236
  */
204
237
  function ensureAiImported(cwd: string): void {
205
- for (const rel of ["src/app.ts", "src/core.ts"]) {
238
+ const candidates: ReadonlyArray<{ readonly rel: string; readonly importLine: string }> = [
239
+ { rel: "src/app.ts", importLine: `import "./core/ai";\n` },
240
+ { rel: "src/core/index.ts", importLine: `import "./ai";\n` },
241
+ // Legacy layout
242
+ { rel: "src/core.ts", importLine: `import "./ai";\n` },
243
+ ];
244
+ for (const { rel, importLine } of candidates) {
206
245
  const path = join(cwd, rel);
207
246
  if (!existsSync(path)) continue;
208
247
  const src = readFileSync(path, "utf8");
209
- if (/from\s+["']\.\/ai["']/.test(src) || /import\s+["']\.\/ai["']/.test(src)) return;
210
- const next = `import "./ai";\n${src}`;
211
- writeFileSync(path, next, "utf8");
248
+ if (
249
+ /from\s+["']\.\/(?:core\/)?ai["']/.test(src) ||
250
+ /import\s+["']\.\/(?:core\/)?ai["']/.test(src)
251
+ ) {
252
+ return;
253
+ }
254
+ writeFileSync(path, `${importLine}${src}`, "utf8");
212
255
  return;
213
256
  }
214
257
  }