create-oke 0.6.1 → 0.8.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 (74) 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 +226 -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 +1 -0
  22. package/templates/advanced/README.md +39 -0
  23. package/templates/advanced/oke.config.ts +81 -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/seed/index.ts +67 -0
  39. package/templates/advanced/src/vault.ts +7 -0
  40. package/templates/advanced/tests/advanced.test.ts +42 -0
  41. package/templates/standard/.env.example +94 -0
  42. package/templates/standard/README.md +48 -0
  43. package/templates/standard/drizzle.config.ts +25 -0
  44. package/{template → templates/standard}/oke.config.ts +14 -7
  45. package/{template → templates/standard}/package.json +1 -1
  46. package/templates/standard/src/app.ts +30 -0
  47. package/templates/standard/src/channels.ts +13 -0
  48. package/{template → templates/standard}/src/core.ts +1 -0
  49. package/templates/standard/src/flows/main/index.ts +34 -0
  50. package/templates/standard/src/flows/main/shapes.ts +1 -0
  51. package/templates/standard/src/flows/main/signals.ts +1 -0
  52. package/templates/standard/src/flows/notes/index.ts +143 -0
  53. package/templates/standard/src/flows/notes/shapes.ts +26 -0
  54. package/templates/standard/src/flows/notes/signals.ts +13 -0
  55. package/templates/standard/src/gates.ts +12 -0
  56. package/templates/standard/src/locales/ar.ts +18 -0
  57. package/templates/standard/src/locales/en.ts +22 -0
  58. package/templates/standard/src/schema.decl.ts +13 -0
  59. package/templates/standard/src/seed/index.ts +56 -0
  60. package/templates/standard/src/vault.ts +7 -0
  61. package/templates/standard/tests/standard.test.ts +51 -0
  62. package/template/README.md +0 -77
  63. package/template/bun.lock +0 -845
  64. package/template/src/app.ts +0 -18
  65. package/template/src/channels.ts +0 -4
  66. package/template/src/flows/main/shapes.ts +0 -2
  67. package/template/src/flows/main/signals.ts +0 -4
  68. package/template/src/gates.ts +0 -4
  69. package/template/src/locales/ar.ts +0 -20
  70. package/template/src/locales/en.ts +0 -26
  71. package/template/src/schema.decl.ts +0 -12
  72. package/template/src/vault.ts +0 -4
  73. package/template/tests/standard.test.ts +0 -18
  74. /package/{template → templates/advanced}/drizzle.config.ts +0 -0
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Detect local Ollama — CLI (`ollama list` / `ps`) and HTTP `/api/tags`.
3
+ */
4
+
5
+ import { ALL_CURATED, type CatalogModel } from "./catalog.ts";
6
+
7
+ /** Default Ollama HTTP origin (create-oke copy — no okengine driver import). */
8
+ const OLLAMA_DEFAULT_BASE_URL = "http://127.0.0.1:11434";
9
+
10
+ /**
11
+ * Normalize an Ollama base URL (strip trailing slash / accidental `/v1`).
12
+ *
13
+ * @param raw - User or env value
14
+ */
15
+ function normalizeOllamaBaseUrl(raw: string): string {
16
+ const trimmed = raw.trim().replace(/\/$/, "");
17
+ if (!trimmed) return OLLAMA_DEFAULT_BASE_URL;
18
+ if (/^https?:\/\//i.test(trimmed)) return trimmed.replace(/\/v1\/?$/i, "");
19
+ return `http://${trimmed}`;
20
+ }
21
+
22
+ /** Result of probing the local Ollama install. */
23
+ export type OllamaDetectResult = {
24
+ readonly available: boolean;
25
+ readonly baseUrl: string;
26
+ readonly installed: readonly string[];
27
+ readonly running: readonly string[];
28
+ readonly curatedInstalled: readonly CatalogModel[];
29
+ readonly curatedMissing: readonly CatalogModel[];
30
+ };
31
+
32
+ /**
33
+ * Parse `ollama list` tabular output into model names.
34
+ *
35
+ * @param stdout - CLI stdout
36
+ */
37
+ export function parseOllamaList(stdout: string): string[] {
38
+ const lines = stdout
39
+ .split("\n")
40
+ .map((l) => l.trim())
41
+ .filter(Boolean);
42
+ const out: string[] = [];
43
+ for (const line of lines) {
44
+ if (/^NAME\b/i.test(line)) continue;
45
+ const name = line.split(/\s+/)[0];
46
+ if (name) out.push(name);
47
+ }
48
+ return out;
49
+ }
50
+
51
+ /**
52
+ * Parse `ollama ps` tabular output into running model names.
53
+ *
54
+ * @param stdout - CLI stdout
55
+ */
56
+ export function parseOllamaPs(stdout: string): string[] {
57
+ return parseOllamaList(stdout);
58
+ }
59
+
60
+ /**
61
+ * Normalize installed tag for comparison (`qwen3.5:9b` vs `qwen3.5:9b-mlx`).
62
+ *
63
+ * @param id - Catalog or installed id
64
+ * @param installed - Installed names
65
+ */
66
+ export function isInstalled(id: string, installed: readonly string[]): boolean {
67
+ const base = id.split(":")[0] ?? id;
68
+ return installed.some((name) => {
69
+ if (name === id) return true;
70
+ if (name.startsWith(`${id}-`)) return true;
71
+ if (name.startsWith(`${base}:`) && id.startsWith(`${base}:`)) {
72
+ // treat mlx / quant suffixes as matching the same family:tag when prefix matches
73
+ const instTag = name.slice(base.length + 1);
74
+ const wantTag = id.slice(base.length + 1);
75
+ return (
76
+ instTag === wantTag || instTag.startsWith(`${wantTag}-`) || wantTag.startsWith(instTag)
77
+ );
78
+ }
79
+ return false;
80
+ });
81
+ }
82
+
83
+ /**
84
+ * Detect Ollama via CLI then HTTP fallback.
85
+ *
86
+ * @param options - Base URL / spawn / fetch seams
87
+ */
88
+ export async function detectOllama(
89
+ options: {
90
+ readonly baseUrl?: string;
91
+ readonly fetch?: typeof globalThis.fetch;
92
+ readonly run?: (cmd: readonly string[]) => Promise<{ code: number; stdout: string }>;
93
+ } = {},
94
+ ): Promise<OllamaDetectResult> {
95
+ const baseUrl = normalizeOllamaBaseUrl(options.baseUrl ?? OLLAMA_DEFAULT_BASE_URL);
96
+ const run =
97
+ options.run ??
98
+ (async (cmd) => {
99
+ const proc = Bun.spawn([...cmd], { stdout: "pipe", stderr: "pipe" });
100
+ const stdout = await new Response(proc.stdout).text();
101
+ const code = await proc.exited;
102
+ return { code, stdout };
103
+ });
104
+
105
+ let installed: string[] = [];
106
+ let running: string[] = [];
107
+ let available = false;
108
+
109
+ try {
110
+ const list = await run(["ollama", "list"]);
111
+ if (list.code === 0) {
112
+ available = true;
113
+ installed = parseOllamaList(list.stdout);
114
+ }
115
+ const ps = await run(["ollama", "ps"]);
116
+ if (ps.code === 0) {
117
+ running = parseOllamaPs(ps.stdout);
118
+ }
119
+ } catch {
120
+ // CLI missing — try HTTP
121
+ }
122
+
123
+ if (!available) {
124
+ const fetchFn = options.fetch ?? globalThis.fetch;
125
+ try {
126
+ const res = await fetchFn(`${baseUrl}/api/tags`, {
127
+ method: "GET",
128
+ signal: AbortSignal.timeout(2_000),
129
+ });
130
+ if (res.ok) {
131
+ available = true;
132
+ const json = (await res.json()) as { models?: readonly { name?: string }[] };
133
+ installed = (json.models ?? [])
134
+ .map((m) => m.name)
135
+ .filter((n): n is string => typeof n === "string");
136
+ }
137
+ } catch {
138
+ // unavailable
139
+ }
140
+ }
141
+
142
+ const curatedInstalled = ALL_CURATED.filter((m) => isInstalled(m.id, installed));
143
+ const curatedMissing = ALL_CURATED.filter((m) => !isInstalled(m.id, installed));
144
+
145
+ return {
146
+ available,
147
+ baseUrl,
148
+ installed,
149
+ running,
150
+ curatedInstalled,
151
+ curatedMissing,
152
+ };
153
+ }
154
+
155
+ /**
156
+ * Best-effort total system RAM in GB.
157
+ */
158
+ export function detectTotalRamGb(): number | null {
159
+ try {
160
+ if (process.platform === "darwin") {
161
+ const proc = Bun.spawnSync(["sysctl", "-n", "hw.memsize"], { stdout: "pipe" });
162
+ if (proc.exitCode === 0) {
163
+ const bytes = Number(proc.stdout.toString().trim());
164
+ if (Number.isFinite(bytes) && bytes > 0) return Math.round(bytes / 1024 ** 3);
165
+ }
166
+ }
167
+ if (process.platform === "linux") {
168
+ const proc = Bun.spawnSync(["awk", "/MemTotal/ {print $2}", "/proc/meminfo"], {
169
+ stdout: "pipe",
170
+ });
171
+ if (proc.exitCode === 0) {
172
+ const kb = Number(proc.stdout.toString().trim());
173
+ if (Number.isFinite(kb) && kb > 0) return Math.round(kb / 1024 ** 2);
174
+ }
175
+ }
176
+ } catch {
177
+ // ignore
178
+ }
179
+ return null;
180
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Convert persisted create-defaults AI prefs ↔ apply input.
3
+ */
4
+
5
+ import type { CreateAiPref } from "../create-defaults.ts";
6
+ import type { AiSetupApplyInput } from "./apply.ts";
7
+
8
+ /**
9
+ * Build apply input from saved create-defaults (reuse path).
10
+ *
11
+ * @param pref - Saved AI preference
12
+ */
13
+ export function applyInputFromAiPref(pref: CreateAiPref): AiSetupApplyInput | null {
14
+ if (!pref.enabled || !pref.driver) return null;
15
+ const driver = pref.driver as AiSetupApplyInput["driver"];
16
+ if (
17
+ driver !== "ollama" &&
18
+ driver !== "anthropic" &&
19
+ driver !== "openai-compatible" &&
20
+ driver !== "mock"
21
+ ) {
22
+ return null;
23
+ }
24
+ return {
25
+ driver,
26
+ ...(pref.baseUrl ? { baseUrl: pref.baseUrl } : {}),
27
+ ...(pref.chatModel ? { chatModel: pref.chatModel } : {}),
28
+ ...(pref.visionModel !== undefined && pref.visionModel !== null
29
+ ? { visionModel: pref.visionModel }
30
+ : {}),
31
+ ...(pref.embedModel !== undefined && pref.embedModel !== null
32
+ ? { embedModel: pref.embedModel }
33
+ : {}),
34
+ ...(pref.apiKeyEnv ? { apiKeyEnv: pref.apiKeyEnv } : {}),
35
+ };
36
+ }
37
+
38
+ /**
39
+ * Merge model choices into a CreateAiPref for persistence.
40
+ *
41
+ * @param base - Provider / driver prefs
42
+ * @param apply - Model apply input
43
+ */
44
+ export function aiPrefWithModels(
45
+ base: CreateAiPref,
46
+ apply: AiSetupApplyInput | null,
47
+ ): CreateAiPref {
48
+ if (!apply) return base;
49
+ return {
50
+ ...base,
51
+ driver: apply.driver,
52
+ chatModel: apply.chatModel ?? null,
53
+ visionModel: apply.visionModel ?? null,
54
+ embedModel: apply.embedModel ?? null,
55
+ baseUrl: apply.baseUrl ?? null,
56
+ apiKeyEnv: apply.apiKeyEnv ?? null,
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Non-interactive defaults for `--yes --ai` (mirrors `oke ai setup --yes`).
62
+ *
63
+ * @param provider - Menu id
64
+ */
65
+ export function nonInteractiveAiApply(provider: string): AiSetupApplyInput {
66
+ if (provider === "ollama") {
67
+ return {
68
+ driver: "ollama",
69
+ baseUrl: process.env.OKE_AI_URL ?? "http://127.0.0.1:11434",
70
+ chatModel: "gemma4:e4b",
71
+ visionModel: "qwen3-vl:4b",
72
+ embedModel: "nomic-embed-text",
73
+ };
74
+ }
75
+ if (provider === "anthropic") {
76
+ return {
77
+ driver: "anthropic",
78
+ chatModel: "claude-sonnet-4-20250514",
79
+ visionModel: null,
80
+ embedModel: null,
81
+ apiKeyEnv: "ANTHROPIC_API_KEY",
82
+ };
83
+ }
84
+ const baseUrls: Record<string, string | undefined> = {
85
+ openai: "https://api.openai.com/v1",
86
+ openrouter: "https://openrouter.ai/api/v1",
87
+ lmstudio: "http://127.0.0.1:1234/v1",
88
+ gemini: undefined,
89
+ custom: undefined,
90
+ };
91
+ return {
92
+ driver: "openai-compatible",
93
+ ...(baseUrls[provider] !== undefined ? { baseUrl: baseUrls[provider] } : {}),
94
+ chatModel: "gpt-4o-mini",
95
+ visionModel: null,
96
+ embedModel: null,
97
+ apiKeyEnv: "OPENAI_API_KEY",
98
+ };
99
+ }