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.
Files changed (72) hide show
  1. package/README.md +23 -8
  2. package/package.json +3 -3
  3. package/src/ai-setup/apply.ts +214 -0
  4. package/src/ai-setup/catalog.ts +263 -0
  5. package/src/ai-setup/detect-ollama.ts +180 -0
  6. package/src/ai-setup/from-pref.ts +99 -0
  7. package/src/ai-setup/prompts.ts +649 -0
  8. package/src/ai-setup/recommend.ts +203 -0
  9. package/src/cli.test.ts +385 -28
  10. package/src/cli.ts +332 -29
  11. package/src/create-defaults.test.ts +92 -0
  12. package/src/create-defaults.ts +246 -0
  13. package/src/customize-flow.test.ts +82 -0
  14. package/src/customize-flow.ts +432 -0
  15. package/src/drivers-catalog.ts +217 -0
  16. package/src/scaffold.ts +97 -10
  17. package/src/templates.ts +14 -9
  18. package/src/transform.test.ts +105 -0
  19. package/src/transform.ts +227 -0
  20. package/src/wizard-select.ts +50 -0
  21. package/{template → templates/advanced}/.env.example +40 -2
  22. package/templates/advanced/README.md +32 -0
  23. package/templates/advanced/oke.config.ts +74 -0
  24. package/templates/advanced/package.json +18 -0
  25. package/templates/advanced/src/app.ts +30 -0
  26. package/templates/advanced/src/channels.ts +13 -0
  27. package/templates/advanced/src/core.ts +8 -0
  28. package/{template → templates/advanced}/src/flows/main/index.ts +9 -6
  29. package/templates/advanced/src/flows/main/shapes.ts +1 -0
  30. package/templates/advanced/src/flows/main/signals.ts +1 -0
  31. package/templates/advanced/src/flows/notes/index.ts +224 -0
  32. package/templates/advanced/src/flows/notes/shapes.ts +52 -0
  33. package/templates/advanced/src/flows/notes/signals.ts +13 -0
  34. package/templates/advanced/src/gates.ts +12 -0
  35. package/templates/advanced/src/locales/ar.ts +18 -0
  36. package/templates/advanced/src/locales/en.ts +22 -0
  37. package/templates/advanced/src/schema.decl.ts +13 -0
  38. package/templates/advanced/src/vault.ts +7 -0
  39. package/templates/advanced/tests/advanced.test.ts +42 -0
  40. package/templates/standard/.env.example +94 -0
  41. package/templates/standard/README.md +46 -0
  42. package/templates/standard/drizzle.config.ts +25 -0
  43. package/{template → templates/standard}/oke.config.ts +9 -9
  44. package/{template → templates/standard}/package.json +1 -1
  45. package/templates/standard/src/app.ts +30 -0
  46. package/templates/standard/src/channels.ts +13 -0
  47. package/{template → templates/standard}/src/core.ts +1 -0
  48. package/templates/standard/src/flows/main/index.ts +34 -0
  49. package/templates/standard/src/flows/main/shapes.ts +1 -0
  50. package/templates/standard/src/flows/main/signals.ts +1 -0
  51. package/templates/standard/src/flows/notes/index.ts +143 -0
  52. package/templates/standard/src/flows/notes/shapes.ts +26 -0
  53. package/templates/standard/src/flows/notes/signals.ts +13 -0
  54. package/templates/standard/src/gates.ts +12 -0
  55. package/templates/standard/src/locales/ar.ts +18 -0
  56. package/templates/standard/src/locales/en.ts +22 -0
  57. package/templates/standard/src/schema.decl.ts +13 -0
  58. package/templates/standard/src/vault.ts +7 -0
  59. package/templates/standard/tests/standard.test.ts +51 -0
  60. package/template/README.md +0 -77
  61. package/template/bun.lock +0 -845
  62. package/template/src/app.ts +0 -18
  63. package/template/src/channels.ts +0 -4
  64. package/template/src/flows/main/shapes.ts +0 -2
  65. package/template/src/flows/main/signals.ts +0 -4
  66. package/template/src/gates.ts +0 -4
  67. package/template/src/locales/ar.ts +0 -20
  68. package/template/src/locales/en.ts +0 -26
  69. package/template/src/schema.decl.ts +0 -12
  70. package/template/src/vault.ts +0 -4
  71. package/template/tests/standard.test.ts +0 -18
  72. /package/{template → templates/advanced}/drizzle.config.ts +0 -0
@@ -0,0 +1,246 @@
1
+ /**
2
+ * User-global create-oke defaults — `~/.oke/create-defaults.json`.
3
+ *
4
+ * Project-local `.oke/` (mode, sqlite, …) stays under the app cwd.
5
+ * This file is the first home-directory preference for the scaffold wizard.
6
+ */
7
+
8
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
9
+ import { homedir } from "node:os";
10
+ import { dirname, join } from "node:path";
11
+ import { isTemplateId, type TemplateId } from "./templates.ts";
12
+
13
+ /** Create profile assumption — mirrors `oke dev` local vs docker-ready. */
14
+ export type CreateProfile = "local-only" | "docker-ready";
15
+
16
+ /** Env columns written into `oke.config.ts` driver maps. */
17
+ export type EnvDriverPins = {
18
+ readonly local: string;
19
+ readonly docker: string;
20
+ readonly test: string;
21
+ readonly prod: string;
22
+ };
23
+
24
+ /** Optional AI preference saved with create defaults. */
25
+ export type CreateAiPref = {
26
+ readonly enabled: boolean;
27
+ /** Menu id: ollama · openai · anthropic · gemini · lmstudio · openrouter · custom · mock */
28
+ readonly provider: string | null;
29
+ /** Protocol driver id written to `drivers.ai`. */
30
+ readonly driver: string | null;
31
+ /** Chat model id (from wizard / `oke ai setup`). */
32
+ readonly chatModel?: string | null;
33
+ /** Vision model id (ollama). */
34
+ readonly visionModel?: string | null;
35
+ /** Embedding model id (ollama). */
36
+ readonly embedModel?: string | null;
37
+ /** Optional OpenAI-compatible base URL. */
38
+ readonly baseUrl?: string | null;
39
+ /** Env var name for the API key (cloud providers). */
40
+ readonly apiKeyEnv?: string | null;
41
+ };
42
+
43
+ /** Full customize answers persisted for "reuse previous settings". */
44
+ export type CreateDefaults = {
45
+ readonly version: 1;
46
+ /** Starter that produced these pins — reuse only when it matches. */
47
+ readonly template: TemplateId;
48
+ readonly profile: CreateProfile;
49
+ readonly drivers: {
50
+ readonly store: {
51
+ readonly sql: EnvDriverPins;
52
+ readonly kv: EnvDriverPins;
53
+ readonly files: EnvDriverPins;
54
+ /** `null` = leave index unset (template default). */
55
+ readonly index: EnvDriverPins | null;
56
+ };
57
+ readonly signal: EnvDriverPins;
58
+ readonly clock: EnvDriverPins;
59
+ readonly vault: EnvDriverPins;
60
+ readonly channel: { readonly email: EnvDriverPins };
61
+ /** `null` = leave `drivers.ai` unset. */
62
+ readonly ai: EnvDriverPins | null;
63
+ };
64
+ readonly ai: CreateAiPref;
65
+ readonly updatedAt: string;
66
+ };
67
+
68
+ /** Relative path under the user home directory. */
69
+ export const CREATE_DEFAULTS_RELATIVE = ".oke/create-defaults.json";
70
+
71
+ /**
72
+ * Absolute path to the create-defaults file.
73
+ *
74
+ * @param home - Override home (tests)
75
+ */
76
+ export function createDefaultsPath(home: string = homedir()): string {
77
+ return join(home, CREATE_DEFAULTS_RELATIVE);
78
+ }
79
+
80
+ /**
81
+ * Read and validate create-defaults, or `null` when missing/corrupt.
82
+ *
83
+ * @param path - Absolute file path
84
+ */
85
+ export function readCreateDefaults(path: string = createDefaultsPath()): CreateDefaults | null {
86
+ if (!existsSync(path)) return null;
87
+ try {
88
+ const raw = JSON.parse(readFileSync(path, "utf8")) as unknown;
89
+ return parseCreateDefaults(raw);
90
+ } catch {
91
+ return null;
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Persist create-defaults (creates `~/.oke` as needed).
97
+ *
98
+ * @param defaults - Validated payload
99
+ * @param path - Absolute file path
100
+ */
101
+ export function writeCreateDefaults(
102
+ defaults: CreateDefaults,
103
+ path: string = createDefaultsPath(),
104
+ ): void {
105
+ mkdirSync(dirname(path), { recursive: true });
106
+ writeFileSync(path, `${JSON.stringify(defaults, null, 2)}\n`, "utf8");
107
+ }
108
+
109
+ /**
110
+ * Build a {@link CreateDefaults} document from customize answers.
111
+ *
112
+ * @param answers - Customize payload (without metadata)
113
+ */
114
+ export function toCreateDefaults(
115
+ answers: Omit<CreateDefaults, "version" | "updatedAt">,
116
+ ): CreateDefaults {
117
+ return {
118
+ version: 1,
119
+ ...answers,
120
+ updatedAt: new Date().toISOString(),
121
+ };
122
+ }
123
+
124
+ /**
125
+ * Validate a parsed JSON value as {@link CreateDefaults}.
126
+ *
127
+ * @param raw - Unknown JSON
128
+ */
129
+ export function parseCreateDefaults(raw: unknown): CreateDefaults | null {
130
+ if (!raw || typeof raw !== "object") return null;
131
+ const o = raw as Record<string, unknown>;
132
+ if (o.version !== 1) return null;
133
+ if (o.profile !== "local-only" && o.profile !== "docker-ready") return null;
134
+ // Older files omit template — treat as standard so reuse stays usable.
135
+ const templateRaw = o.template === undefined || o.template === null ? "standard" : o.template;
136
+ if (typeof templateRaw !== "string" || !isTemplateId(templateRaw)) return null;
137
+ const template = templateRaw;
138
+ if (!o.drivers || typeof o.drivers !== "object") return null;
139
+ const drivers = o.drivers as Record<string, unknown>;
140
+ if (!drivers.store || typeof drivers.store !== "object") return null;
141
+ const store = drivers.store as Record<string, unknown>;
142
+ const sql = parsePins(store.sql);
143
+ const kv = parsePins(store.kv);
144
+ const files = parsePins(store.files);
145
+ if (!sql || !kv || !files) return null;
146
+ const index = store.index === null ? null : parsePins(store.index);
147
+ if (store.index !== null && index === null) return null;
148
+ const signal = parsePins(drivers.signal);
149
+ const clock = parsePins(drivers.clock);
150
+ const vault = parsePins(drivers.vault);
151
+ if (!signal || !clock || !vault) return null;
152
+ if (!drivers.channel || typeof drivers.channel !== "object") return null;
153
+ const email = parsePins((drivers.channel as Record<string, unknown>).email);
154
+ if (!email) return null;
155
+ const aiPins = drivers.ai === null || drivers.ai === undefined ? null : parsePins(drivers.ai);
156
+ if (drivers.ai != null && aiPins === null) return null;
157
+ if (!o.ai || typeof o.ai !== "object") return null;
158
+ const ai = o.ai as Record<string, unknown>;
159
+ if (typeof ai.enabled !== "boolean") return null;
160
+ const provider =
161
+ ai.provider === null || ai.provider === undefined
162
+ ? null
163
+ : typeof ai.provider === "string"
164
+ ? ai.provider
165
+ : null;
166
+ if (ai.provider !== null && ai.provider !== undefined && typeof ai.provider !== "string") {
167
+ return null;
168
+ }
169
+ const driver =
170
+ ai.driver === null || ai.driver === undefined
171
+ ? null
172
+ : typeof ai.driver === "string"
173
+ ? ai.driver
174
+ : null;
175
+ if (ai.driver !== null && ai.driver !== undefined && typeof ai.driver !== "string") {
176
+ return null;
177
+ }
178
+ const chatModel = optionalStringOrNull(ai.chatModel);
179
+ const visionModel = optionalStringOrNull(ai.visionModel);
180
+ const embedModel = optionalStringOrNull(ai.embedModel);
181
+ const baseUrl = optionalStringOrNull(ai.baseUrl);
182
+ const apiKeyEnv = optionalStringOrNull(ai.apiKeyEnv);
183
+ if (
184
+ chatModel === false ||
185
+ visionModel === false ||
186
+ embedModel === false ||
187
+ baseUrl === false ||
188
+ apiKeyEnv === false
189
+ ) {
190
+ return null;
191
+ }
192
+ if (typeof o.updatedAt !== "string") return null;
193
+ return {
194
+ version: 1,
195
+ template,
196
+ profile: o.profile,
197
+ drivers: {
198
+ store: { sql, kv, files, index },
199
+ signal,
200
+ clock,
201
+ vault,
202
+ channel: { email },
203
+ ai: aiPins,
204
+ },
205
+ ai: {
206
+ enabled: ai.enabled,
207
+ provider,
208
+ driver,
209
+ ...(chatModel !== undefined ? { chatModel } : {}),
210
+ ...(visionModel !== undefined ? { visionModel } : {}),
211
+ ...(embedModel !== undefined ? { embedModel } : {}),
212
+ ...(baseUrl !== undefined ? { baseUrl } : {}),
213
+ ...(apiKeyEnv !== undefined ? { apiKeyEnv } : {}),
214
+ },
215
+ updatedAt: o.updatedAt,
216
+ };
217
+ }
218
+
219
+ /**
220
+ * Parse optional string|null fields. `false` = invalid type.
221
+ *
222
+ * @param value - Raw JSON value
223
+ */
224
+ function optionalStringOrNull(value: unknown): string | null | undefined | false {
225
+ if (value === undefined) return undefined;
226
+ if (value === null) return null;
227
+ if (typeof value === "string") return value;
228
+ return false;
229
+ }
230
+
231
+ /**
232
+ * @param raw - Candidate pins object
233
+ */
234
+ function parsePins(raw: unknown): EnvDriverPins | null {
235
+ if (!raw || typeof raw !== "object") return null;
236
+ const o = raw as Record<string, unknown>;
237
+ if (
238
+ typeof o.local !== "string" ||
239
+ typeof o.docker !== "string" ||
240
+ typeof o.test !== "string" ||
241
+ typeof o.prod !== "string"
242
+ ) {
243
+ return null;
244
+ }
245
+ return { local: o.local, docker: o.docker, test: o.test, prod: o.prod };
246
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Unit tests for local|docker customize assembly (no Clack prompts).
3
+ */
4
+
5
+ import { describe, expect, test } from "bun:test";
6
+ import { TEMPLATE_DOCKER_PROD, TEMPLATE_LOCAL, customizeFacetsFor } from "./drivers-catalog.ts";
7
+ import { assembleDriverDefaults, pinsFromSides } from "./customize-flow.ts";
8
+
9
+ describe("customizeFacetsFor", () => {
10
+ test("standard is lean (sql only)", () => {
11
+ expect(customizeFacetsFor("standard")).toEqual(["sql"]);
12
+ });
13
+
14
+ test("advanced walks the full facet list", () => {
15
+ expect(customizeFacetsFor("advanced")).toEqual([
16
+ "sql",
17
+ "kv",
18
+ "files",
19
+ "enableIndex",
20
+ "index",
21
+ "signal",
22
+ "clock",
23
+ "vault",
24
+ "email",
25
+ ]);
26
+ });
27
+ });
28
+
29
+ describe("pinsFromSides", () => {
30
+ test("prod copies docker", () => {
31
+ expect(pinsFromSides("sqlite", "postgres", "memory")).toEqual({
32
+ local: "sqlite",
33
+ docker: "postgres",
34
+ test: "memory",
35
+ prod: "postgres",
36
+ });
37
+ });
38
+ });
39
+
40
+ describe("assembleDriverDefaults", () => {
41
+ test("primary docker + decline other → local pins === template defaults", () => {
42
+ const drivers = assembleDriverDefaults(
43
+ "docker",
44
+ { sql: "libsql", kv: "redis", files: "s3", signal: "nats" },
45
+ null,
46
+ );
47
+ expect(drivers.store.sql.docker).toBe("libsql");
48
+ expect(drivers.store.sql.local).toBe(TEMPLATE_LOCAL.sql);
49
+ expect(drivers.store.kv.local).toBe(TEMPLATE_LOCAL.kv);
50
+ expect(drivers.signal.local).toBe(TEMPLATE_LOCAL.signal);
51
+ expect(drivers.store.sql.prod).toBe("libsql");
52
+ });
53
+
54
+ test("primary local + customize docker → both sides set", () => {
55
+ const drivers = assembleDriverDefaults("local", { sql: "pglite" }, { sql: "postgres" });
56
+ expect(drivers.store.sql.local).toBe("pglite");
57
+ expect(drivers.store.sql.docker).toBe("postgres");
58
+ expect(drivers.store.sql.prod).toBe("postgres");
59
+ // Unpicked facets fall back to catalog defaults.
60
+ expect(drivers.store.kv.local).toBe(TEMPLATE_LOCAL.kv);
61
+ expect(drivers.store.kv.docker).toBe(TEMPLATE_DOCKER_PROD.kv);
62
+ });
63
+
64
+ test("index stays null unless enableIndex", () => {
65
+ const drivers = assembleDriverDefaults("local", { sql: "sqlite" }, null);
66
+ expect(drivers.store.index).toBeNull();
67
+ });
68
+
69
+ test("enableIndex on primary fills both columns", () => {
70
+ const drivers = assembleDriverDefaults(
71
+ "docker",
72
+ { enableIndex: true, index: "meilisearch" },
73
+ null,
74
+ );
75
+ expect(drivers.store.index).toEqual({
76
+ local: "memory",
77
+ docker: "meilisearch",
78
+ test: "memory",
79
+ prod: "meilisearch",
80
+ });
81
+ });
82
+ });