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.
- package/README.md +7 -5
- package/package.json +2 -3
- package/src/agents-md.ts +1 -1
- package/src/ai-setup/apply.ts +66 -23
- package/src/ai-setup/catalog.ts +1316 -35
- package/src/ai-setup/detect-ollama.ts +48 -0
- package/src/ai-setup/from-pref.ts +32 -0
- package/src/ai-setup/prompts.ts +430 -486
- package/src/ai-setup/recommend.ts +118 -101
- package/src/cli.test.ts +36 -7
- package/src/cli.ts +21 -16
- package/src/customize-flow.test.ts +43 -13
- package/src/customize-flow.ts +136 -61
- package/src/drivers-catalog.ts +29 -12
- package/src/local-okengine.test.ts +59 -0
- package/src/local-okengine.ts +137 -0
- package/src/scaffold.ts +1 -1
- package/src/transform.test.ts +54 -32
- package/src/transform.ts +46 -9
- package/src/wizard-select.ts +5 -6
- package/templates/advanced/.github/workflows/ci.yml +24 -0
- package/templates/advanced/.vscode/settings.json +15 -0
- package/templates/advanced/README.md +15 -4
- package/templates/advanced/drizzle.config.ts +6 -4
- package/templates/advanced/oke.config.ts +6 -2
- package/templates/advanced/package.json +5 -2
- package/templates/advanced/src/app.ts +2 -14
- package/templates/advanced/src/core/index.ts +12 -0
- package/templates/advanced/src/{core.ts → core/store.ts} +1 -1
- package/templates/advanced/src/db/migrations/.gitkeep +0 -0
- package/templates/{standard/src → advanced/src/db}/schema.decl.ts +1 -1
- package/templates/advanced/src/{seed → db/seed}/index.ts +2 -2
- package/templates/advanced/src/flows/notes/index.ts +2 -4
- package/templates/advanced/tests/advanced.test.ts +10 -9
- package/templates/advanced/tsconfig.json +23 -0
- package/templates/standard/.github/workflows/ci.yml +24 -0
- package/templates/standard/.vscode/settings.json +15 -0
- package/templates/standard/README.md +20 -9
- package/templates/standard/drizzle.config.ts +6 -4
- package/templates/standard/oke.config.ts +4 -0
- package/templates/standard/package.json +5 -2
- package/templates/standard/src/app.ts +2 -14
- package/templates/standard/src/core/index.ts +12 -0
- package/templates/standard/src/{core.ts → core/store.ts} +1 -1
- package/templates/standard/src/db/migrations/.gitkeep +0 -0
- package/templates/{advanced/src → standard/src/db}/schema.decl.ts +1 -1
- package/templates/standard/src/{seed → db/seed}/index.ts +2 -2
- package/templates/standard/src/flows/notes/index.ts +2 -4
- package/templates/standard/tests/standard.test.ts +13 -10
- package/templates/standard/tsconfig.json +23 -0
- /package/templates/advanced/src/{channels.ts → core/channels.ts} +0 -0
- /package/templates/advanced/src/{gates.ts → core/gates.ts} +0 -0
- /package/templates/advanced/src/{vault.ts → core/vault.ts} +0 -0
- /package/templates/standard/src/{channels.ts → core/channels.ts} +0 -0
- /package/templates/standard/src/{gates.ts → core/gates.ts} +0 -0
- /package/templates/standard/src/{vault.ts → core/vault.ts} +0 -0
package/src/customize-flow.ts
CHANGED
|
@@ -18,6 +18,10 @@ import {
|
|
|
18
18
|
TEMPLATE_TEST,
|
|
19
19
|
VAULT_CHOICES,
|
|
20
20
|
AI_PROVIDERS,
|
|
21
|
+
LLAMA_CPP_IMAGE,
|
|
22
|
+
OLLAMA_IMAGE,
|
|
23
|
+
SGLANG_IMAGE,
|
|
24
|
+
VLLM_IMAGE,
|
|
21
25
|
aiDriverForProvider,
|
|
22
26
|
customizeFacetsFor,
|
|
23
27
|
pinsDockerReady,
|
|
@@ -43,12 +47,12 @@ export type SidePins = {
|
|
|
43
47
|
sql?: string;
|
|
44
48
|
kv?: string;
|
|
45
49
|
files?: string;
|
|
50
|
+
/** Driver id, or `"none"` / unset for no store.index. */
|
|
46
51
|
index?: string | null;
|
|
47
52
|
signal?: string;
|
|
48
53
|
clock?: string;
|
|
49
54
|
vault?: string;
|
|
50
55
|
email?: string;
|
|
51
|
-
enableIndex?: boolean;
|
|
52
56
|
};
|
|
53
57
|
|
|
54
58
|
/**
|
|
@@ -62,6 +66,15 @@ export function pinsFromSides(local: string, docker: string, test: string): EnvD
|
|
|
62
66
|
return { local, docker, test, prod: docker };
|
|
63
67
|
}
|
|
64
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Whether a side pin enables store.index.
|
|
71
|
+
*
|
|
72
|
+
* @param value - Picked driver or none
|
|
73
|
+
*/
|
|
74
|
+
function indexEnabled(value: string | null | undefined): value is string {
|
|
75
|
+
return typeof value === "string" && value !== "none";
|
|
76
|
+
}
|
|
77
|
+
|
|
65
78
|
/**
|
|
66
79
|
* Fill missing side from template defaults.
|
|
67
80
|
*
|
|
@@ -92,17 +105,13 @@ export function assembleDriverDefaults(
|
|
|
92
105
|
dockerSrc.files ?? TEMPLATE_DOCKER_PROD.files,
|
|
93
106
|
TEMPLATE_TEST.files,
|
|
94
107
|
);
|
|
95
|
-
const enableIndex = Boolean(
|
|
96
|
-
(side === "local" ? picked.enableIndex : other?.enableIndex) ||
|
|
97
|
-
(side === "docker" ? picked.enableIndex : other?.enableIndex),
|
|
98
|
-
);
|
|
99
108
|
const indexLocal = localSrc.index;
|
|
100
109
|
const indexDocker = dockerSrc.index;
|
|
101
110
|
const index =
|
|
102
|
-
|
|
111
|
+
indexEnabled(indexLocal) || indexEnabled(indexDocker)
|
|
103
112
|
? pinsFromSides(
|
|
104
|
-
|
|
105
|
-
|
|
113
|
+
indexEnabled(indexLocal) ? indexLocal : "memory",
|
|
114
|
+
indexEnabled(indexDocker) ? indexDocker : "meilisearch",
|
|
106
115
|
"memory",
|
|
107
116
|
)
|
|
108
117
|
: null;
|
|
@@ -135,6 +144,55 @@ export function assembleDriverDefaults(
|
|
|
135
144
|
};
|
|
136
145
|
}
|
|
137
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Balanced llama.cpp defaults for "AI setup → Recommended" (no quiz).
|
|
149
|
+
* Lightest local footprint; curated Docker Hub `ai/` model via docker-repo.
|
|
150
|
+
*/
|
|
151
|
+
export function recommendedAiApply(): AiSetupApplyInput {
|
|
152
|
+
return {
|
|
153
|
+
driver: "openai-compatible",
|
|
154
|
+
baseUrl: process.env.OKE_AI_URL ?? "http://127.0.0.1:8080/v1",
|
|
155
|
+
chatModel: "smollm2",
|
|
156
|
+
visionModel: null,
|
|
157
|
+
embedModel: null,
|
|
158
|
+
image: LLAMA_CPP_IMAGE,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Attach the compose image pin for a local/self-hosted AI provider menu id.
|
|
164
|
+
*
|
|
165
|
+
* @param input - Apply input from prompts
|
|
166
|
+
* @param provider - Menu provider id
|
|
167
|
+
*/
|
|
168
|
+
export function withLocalAiImage(input: AiSetupApplyInput, provider: string): AiSetupApplyInput {
|
|
169
|
+
if (provider === "llama-cpp") {
|
|
170
|
+
return {
|
|
171
|
+
...input,
|
|
172
|
+
image: LLAMA_CPP_IMAGE,
|
|
173
|
+
baseUrl: input.baseUrl ?? "http://127.0.0.1:8080/v1",
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
if (provider === "ollama") {
|
|
177
|
+
return { ...input, image: OLLAMA_IMAGE };
|
|
178
|
+
}
|
|
179
|
+
if (provider === "vllm") {
|
|
180
|
+
return {
|
|
181
|
+
...input,
|
|
182
|
+
image: VLLM_IMAGE,
|
|
183
|
+
baseUrl: input.baseUrl ?? "http://127.0.0.1:8000/v1",
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
if (provider === "sglang") {
|
|
187
|
+
return {
|
|
188
|
+
...input,
|
|
189
|
+
image: SGLANG_IMAGE,
|
|
190
|
+
baseUrl: input.baseUrl ?? "http://127.0.0.1:30000/v1",
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
return input;
|
|
194
|
+
}
|
|
195
|
+
|
|
138
196
|
/**
|
|
139
197
|
* Customize drivers for one template.
|
|
140
198
|
*
|
|
@@ -148,12 +206,12 @@ export async function askCustomizeFlow(
|
|
|
148
206
|
[
|
|
149
207
|
{
|
|
150
208
|
value: "local",
|
|
151
|
-
label: "
|
|
209
|
+
label: "Local",
|
|
152
210
|
hint: "oke mode local — sqlite / memory / fs …",
|
|
153
211
|
},
|
|
154
212
|
{
|
|
155
213
|
value: "docker",
|
|
156
|
-
label: "
|
|
214
|
+
label: "Docker",
|
|
157
215
|
hint: "oke mode docker — postgres / redis / s3 …",
|
|
158
216
|
},
|
|
159
217
|
],
|
|
@@ -197,44 +255,82 @@ export async function askCustomizeFlow(
|
|
|
197
255
|
let aiPref: CreateDefaults["ai"] = { enabled: false, provider: null, driver: null };
|
|
198
256
|
let aiApply: AiSetupApplyInput | null = null;
|
|
199
257
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
258
|
+
aiSetup: for (;;) {
|
|
259
|
+
const aiSetup = await selectWithBack(
|
|
260
|
+
"AI setup",
|
|
261
|
+
[
|
|
262
|
+
{
|
|
263
|
+
value: "recommended",
|
|
264
|
+
label: "Recommended",
|
|
265
|
+
hint: "llama.cpp · lightest local footprint",
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
value: "customize",
|
|
269
|
+
label: "Customize",
|
|
270
|
+
hint: "pick provider & models",
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
value: "off",
|
|
274
|
+
label: "Off",
|
|
275
|
+
hint: "no AI drivers",
|
|
276
|
+
},
|
|
277
|
+
],
|
|
278
|
+
"recommended",
|
|
216
279
|
{ allowBack: true },
|
|
217
280
|
);
|
|
218
|
-
if (
|
|
219
|
-
if (
|
|
281
|
+
if (aiSetup === null) return null;
|
|
282
|
+
if (aiSetup === WIZARD_BACK) {
|
|
283
|
+
// Back from AI → leave AI off (same as Off) and finish customize.
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
if (aiSetup === "off") break;
|
|
287
|
+
if (aiSetup === "recommended") {
|
|
288
|
+
aiPins =
|
|
289
|
+
primary === "local" && !alsoOther
|
|
290
|
+
? pinsLocalOnly("openai-compatible", "openai-compatible", "mock")
|
|
291
|
+
: pinsDockerReady("openai-compatible", "openai-compatible", "mock");
|
|
292
|
+
aiApply = recommendedAiApply();
|
|
293
|
+
aiPref = aiPrefWithModels(
|
|
294
|
+
{
|
|
295
|
+
enabled: true,
|
|
296
|
+
provider: "llama-cpp",
|
|
297
|
+
driver: "openai-compatible",
|
|
298
|
+
},
|
|
299
|
+
aiApply,
|
|
300
|
+
);
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// customize — local provider, optional docker provider, then model wizard
|
|
305
|
+
const options = AI_PROVIDERS.map((p) => ({ value: p.value, label: p.label }));
|
|
306
|
+
providers: for (;;) {
|
|
307
|
+
const askDockerAi = primary === "docker" || alsoOther;
|
|
308
|
+
const localProviderValue = await selectWithBack(
|
|
309
|
+
askDockerAi || primary === "local" ? "AI Provider — local" : "AI Provider",
|
|
310
|
+
options,
|
|
311
|
+
"llama-cpp",
|
|
312
|
+
{ allowBack: true },
|
|
313
|
+
);
|
|
314
|
+
if (localProviderValue === null) return null;
|
|
315
|
+
if (localProviderValue === WIZARD_BACK) continue aiSetup;
|
|
316
|
+
|
|
220
317
|
const localProvider = localProviderValue as AiProviderId;
|
|
221
318
|
const localDriver = aiDriverForProvider(localProvider);
|
|
222
319
|
let dockerDriver = localDriver === "mock" ? "mock" : localDriver;
|
|
320
|
+
let dockerProvider: AiProviderId = localProvider;
|
|
223
321
|
|
|
224
|
-
if (
|
|
322
|
+
if (askDockerAi) {
|
|
225
323
|
const dockerProviderValue = await selectWithBack(
|
|
226
324
|
"AI Provider — docker",
|
|
227
325
|
options,
|
|
228
|
-
"
|
|
229
|
-
{
|
|
230
|
-
allowBack: true,
|
|
231
|
-
},
|
|
326
|
+
"llama-cpp",
|
|
327
|
+
{ allowBack: true },
|
|
232
328
|
);
|
|
233
329
|
if (dockerProviderValue === null) return null;
|
|
234
|
-
if (dockerProviderValue
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
330
|
+
if (dockerProviderValue === WIZARD_BACK) continue providers;
|
|
331
|
+
dockerProvider = dockerProviderValue as AiProviderId;
|
|
332
|
+
dockerDriver = aiDriverForProvider(dockerProvider);
|
|
333
|
+
if (dockerDriver === "mock") dockerDriver = "mock";
|
|
238
334
|
}
|
|
239
335
|
|
|
240
336
|
aiPins =
|
|
@@ -250,7 +346,7 @@ export async function askCustomizeFlow(
|
|
|
250
346
|
if (setupProvider !== "mock") {
|
|
251
347
|
const picked = await askAiSetup({ provider: setupProvider });
|
|
252
348
|
if (picked === null) return null;
|
|
253
|
-
aiApply = picked;
|
|
349
|
+
aiApply = withLocalAiImage(picked, askDockerAi ? dockerProvider : localProvider);
|
|
254
350
|
} else {
|
|
255
351
|
aiApply = { driver: "mock" };
|
|
256
352
|
}
|
|
@@ -263,6 +359,7 @@ export async function askCustomizeFlow(
|
|
|
263
359
|
},
|
|
264
360
|
aiApply,
|
|
265
361
|
);
|
|
362
|
+
break aiSetup;
|
|
266
363
|
}
|
|
267
364
|
}
|
|
268
365
|
|
|
@@ -288,17 +385,11 @@ async function walkFacetsForSide(
|
|
|
288
385
|
let i = 0;
|
|
289
386
|
while (i < facets.length) {
|
|
290
387
|
const facet = facets[i]!;
|
|
291
|
-
if (facet === "index" && !out.enableIndex) {
|
|
292
|
-
out.index = null;
|
|
293
|
-
i++;
|
|
294
|
-
continue;
|
|
295
|
-
}
|
|
296
388
|
const result = await askOneFacet(facet, side, out);
|
|
297
389
|
if (result === null) return null;
|
|
298
390
|
if (result === WIZARD_BACK) {
|
|
299
391
|
if (i === 0) return WIZARD_BACK;
|
|
300
392
|
i--;
|
|
301
|
-
while (i > 0 && facets[i] === "index" && !out.enableIndex) i--;
|
|
302
393
|
continue;
|
|
303
394
|
}
|
|
304
395
|
i++;
|
|
@@ -343,27 +434,11 @@ async function askOneFacet(
|
|
|
343
434
|
state.files = v;
|
|
344
435
|
return true;
|
|
345
436
|
}
|
|
346
|
-
case "enableIndex": {
|
|
347
|
-
const value = await selectWithBack(
|
|
348
|
-
`Enable store.index (search)? — ${suffix}`,
|
|
349
|
-
[
|
|
350
|
-
{ value: "yes", label: "✓ Yes" },
|
|
351
|
-
{ value: "no", label: "✗ No" },
|
|
352
|
-
],
|
|
353
|
-
state.enableIndex ? "yes" : "no",
|
|
354
|
-
{ allowBack: true },
|
|
355
|
-
);
|
|
356
|
-
if (value === null) return null;
|
|
357
|
-
if (value === WIZARD_BACK) return WIZARD_BACK;
|
|
358
|
-
state.enableIndex = value === "yes";
|
|
359
|
-
if (!state.enableIndex) state.index = null;
|
|
360
|
-
return true;
|
|
361
|
-
}
|
|
362
437
|
case "index": {
|
|
363
438
|
const v = await askSideChoice(
|
|
364
439
|
`store.index — ${suffix}`,
|
|
365
440
|
INDEX_CHOICES,
|
|
366
|
-
side === "local" ? "
|
|
441
|
+
side === "local" ? "none" : "meilisearch",
|
|
367
442
|
);
|
|
368
443
|
if (v === null || v === WIZARD_BACK) return v;
|
|
369
444
|
state.index = v;
|
package/src/drivers-catalog.ts
CHANGED
|
@@ -68,6 +68,7 @@ export const FILES_CHOICES: readonly DriverChoice[] = [
|
|
|
68
68
|
];
|
|
69
69
|
|
|
70
70
|
export const INDEX_CHOICES: readonly DriverChoice[] = [
|
|
71
|
+
{ value: "none", label: "none" },
|
|
71
72
|
{ value: "memory", label: "memory" },
|
|
72
73
|
{ value: "pgvector", label: "pgvector" },
|
|
73
74
|
{ value: "libsql", label: "libsql" },
|
|
@@ -99,24 +100,41 @@ export const VAULT_CHOICES: readonly DriverChoice[] = [
|
|
|
99
100
|
{ value: "memory", label: "memory" },
|
|
100
101
|
];
|
|
101
102
|
|
|
103
|
+
/** Email driver ids for `drivers.channel.email` (SMS/WhatsApp/push are other mediums). */
|
|
102
104
|
export const EMAIL_CHOICES: readonly DriverChoice[] = [
|
|
103
105
|
{ value: "console", label: "console" },
|
|
104
106
|
{ value: "smtp", label: "smtp" },
|
|
105
107
|
{ value: "resend", label: "resend" },
|
|
106
108
|
{ value: "sndr", label: "sndr" },
|
|
107
|
-
|
|
109
|
+
/** Protocol id is `taqnyat-mail`; menu label stays short in the email facet. */
|
|
110
|
+
{ value: "taqnyat-mail", label: "taqnyat" },
|
|
108
111
|
];
|
|
109
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Pinned local-inference images — must match `src/docker/recipes/*` pins.
|
|
115
|
+
* Never `latest` (GGUF-parser CVE floors: llama.cpp ≥ b8146, Ollama ≥ 0.17.1).
|
|
116
|
+
*/
|
|
117
|
+
export const LLAMA_CPP_IMAGE = "ghcr.io/ggml-org/llama.cpp:server-b10290";
|
|
118
|
+
/** @see LLAMA_CPP_IMAGE */
|
|
119
|
+
export const OLLAMA_IMAGE = "ollama/ollama:0.32.6";
|
|
120
|
+
/** @see LLAMA_CPP_IMAGE */
|
|
121
|
+
export const VLLM_IMAGE = "vllm/vllm-openai:v0.26.0";
|
|
122
|
+
/** @see LLAMA_CPP_IMAGE */
|
|
123
|
+
export const SGLANG_IMAGE = "lmsysorg/sglang:v0.5.16-runtime";
|
|
124
|
+
|
|
110
125
|
/** AI menu providers → protocol driver. */
|
|
111
126
|
export const AI_PROVIDERS = [
|
|
112
|
-
{ value: "
|
|
113
|
-
{ value: "
|
|
114
|
-
{ value: "
|
|
115
|
-
{ value: "
|
|
116
|
-
{ value: "
|
|
117
|
-
{ value: "
|
|
118
|
-
{ value: "
|
|
119
|
-
{ value: "
|
|
127
|
+
{ value: "llama-cpp", label: "llama.cpp (Local)", driver: "openai-compatible" },
|
|
128
|
+
{ value: "ollama", label: "Ollama (Local)", driver: "ollama" },
|
|
129
|
+
{ value: "vllm", label: "vLLM (self-hosted GPU)", driver: "openai-compatible" },
|
|
130
|
+
{ value: "sglang", label: "SGLang (self-hosted GPU)", driver: "openai-compatible" },
|
|
131
|
+
{ value: "openai", label: "OpenAI", driver: "openai-compatible" },
|
|
132
|
+
{ value: "anthropic", label: "Anthropic", driver: "anthropic" },
|
|
133
|
+
{ value: "gemini", label: "Gemini", driver: "openai-compatible" },
|
|
134
|
+
{ value: "lmstudio", label: "LM Studio", driver: "openai-compatible" },
|
|
135
|
+
{ value: "openrouter", label: "OpenRouter", driver: "openai-compatible" },
|
|
136
|
+
{ value: "custom", label: "Custom OpenAI Compatible", driver: "openai-compatible" },
|
|
137
|
+
{ value: "mock", label: "Mock (dev only)", driver: "mock" },
|
|
120
138
|
] as const;
|
|
121
139
|
|
|
122
140
|
export type AiProviderId = (typeof AI_PROVIDERS)[number]["value"];
|
|
@@ -187,7 +205,6 @@ export type CustomizeFacetId =
|
|
|
187
205
|
| "sql"
|
|
188
206
|
| "kv"
|
|
189
207
|
| "files"
|
|
190
|
-
| "enableIndex"
|
|
191
208
|
| "index"
|
|
192
209
|
| "signal"
|
|
193
210
|
| "clock"
|
|
@@ -201,7 +218,7 @@ export type CustomizeFacetId =
|
|
|
201
218
|
*/
|
|
202
219
|
export function customizeFacetsFor(template: TemplateId): readonly CustomizeFacetId[] {
|
|
203
220
|
if (template === "standard") return ["sql"];
|
|
204
|
-
return ["sql", "kv", "files", "
|
|
221
|
+
return ["sql", "kv", "files", "index", "signal", "clock", "vault", "email"];
|
|
205
222
|
}
|
|
206
223
|
|
|
207
224
|
/**
|
|
@@ -223,5 +240,5 @@ export const DEFAULT_IMAGES: Readonly<Record<string, string>> = {
|
|
|
223
240
|
"channel.email": "axllent/mailpit:v1.22.3",
|
|
224
241
|
vault: "openbao/openbao:2.6.1",
|
|
225
242
|
"store.index": "getmeili/meilisearch:v1.37",
|
|
226
|
-
ai:
|
|
243
|
+
ai: LLAMA_CPP_IMAGE,
|
|
227
244
|
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local okengine staging for monorepo create-oke.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import { existsSync, lstatSync, readFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { localOkengineStageDir, materializeLocalOkengineDependency } from "./local-okengine.ts";
|
|
9
|
+
import { resolveLocalOkengineRoot } from "./templates.ts";
|
|
10
|
+
import { resolveOkengineDependency } from "./transform.ts";
|
|
11
|
+
|
|
12
|
+
describe("materializeLocalOkengineDependency", () => {
|
|
13
|
+
test("stages publish-shaped package without workspaces or devDependencies", () => {
|
|
14
|
+
const root = resolveLocalOkengineRoot();
|
|
15
|
+
expect(root).not.toBeNull();
|
|
16
|
+
if (!root) return;
|
|
17
|
+
|
|
18
|
+
const dep = materializeLocalOkengineDependency(root);
|
|
19
|
+
const stage = localOkengineStageDir();
|
|
20
|
+
expect(dep).toBe(`file:${stage}`);
|
|
21
|
+
|
|
22
|
+
const pkg = JSON.parse(readFileSync(join(stage, "package.json"), "utf8")) as {
|
|
23
|
+
name: string;
|
|
24
|
+
dependencies?: Record<string, string>;
|
|
25
|
+
devDependencies?: unknown;
|
|
26
|
+
workspaces?: unknown;
|
|
27
|
+
peerDependencies?: Record<string, string>;
|
|
28
|
+
exports?: Record<string, string>;
|
|
29
|
+
};
|
|
30
|
+
expect(pkg.name).toBe("okengine");
|
|
31
|
+
expect(pkg.devDependencies).toBeUndefined();
|
|
32
|
+
expect(pkg.workspaces).toBeUndefined();
|
|
33
|
+
expect(pkg.peerDependencies).toBeUndefined();
|
|
34
|
+
expect(pkg.dependencies?.["oxc-parser"]).toBeDefined();
|
|
35
|
+
// Peers folded into dependencies so the out-of-tree stage can resolve them.
|
|
36
|
+
expect(pkg.dependencies?.["drizzle-orm"]).toBe("1.0.0-rc.4");
|
|
37
|
+
expect(pkg.dependencies?.["drizzle-kit"]).toBe("1.0.0-rc.4");
|
|
38
|
+
expect(pkg.dependencies?.["zod"]).toBeDefined();
|
|
39
|
+
expect(pkg.exports?.["./config"]).toBe("./src/config/index.ts");
|
|
40
|
+
|
|
41
|
+
// Real copies — Bun file: install drops directory symlinks.
|
|
42
|
+
const src = join(stage, "src");
|
|
43
|
+
expect(existsSync(src)).toBe(true);
|
|
44
|
+
expect(lstatSync(src).isSymbolicLink()).toBe(false);
|
|
45
|
+
expect(existsSync(join(stage, "src/config/index.ts"))).toBe(true);
|
|
46
|
+
expect(existsSync(join(stage, "node_modules", "zod"))).toBe(true);
|
|
47
|
+
expect(existsSync(join(stage, "node_modules", "drizzle-kit", "cli.mjs"))).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("resolveOkengineDependency uses the staged file: path locally", () => {
|
|
51
|
+
const root = resolveLocalOkengineRoot();
|
|
52
|
+
expect(root).not.toBeNull();
|
|
53
|
+
if (!root) return;
|
|
54
|
+
|
|
55
|
+
const dep = resolveOkengineDependency(root);
|
|
56
|
+
expect(dep).toBe(`file:${localOkengineStageDir()}`);
|
|
57
|
+
expect(dep).not.toBe(`file:${root}`);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consumer-facing `file:` package for local / monorepo create-oke.
|
|
3
|
+
*
|
|
4
|
+
* Linking the monorepo root pulls `workspaces` + `devDependencies` into the
|
|
5
|
+
* scaffold (Console UI, drizzle-zod, …). Bun then warns that RC
|
|
6
|
+
* `drizzle-orm` fails drizzle-zod’s `>=0.36` peer. Stage a publish-shaped
|
|
7
|
+
* tree instead:
|
|
8
|
+
*
|
|
9
|
+
* 1. Copy `files` (Bun’s `file:` install drops directory symlinks).
|
|
10
|
+
* 2. Install dependencies **in the stage** — Bun keeps `package.json` as a
|
|
11
|
+
* symlink into `~/.oke/...`, so the package root is outside the app and
|
|
12
|
+
* resolution never sees the app’s `node_modules` (peers like `zod` must
|
|
13
|
+
* live next to the stage too).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { homedir } from "node:os";
|
|
18
|
+
import { dirname, join, resolve } from "node:path";
|
|
19
|
+
|
|
20
|
+
/** Fields kept on the staged consumer package.json. */
|
|
21
|
+
type ConsumerOkenginePackageJson = {
|
|
22
|
+
readonly name: string;
|
|
23
|
+
readonly version: string;
|
|
24
|
+
readonly description?: string;
|
|
25
|
+
readonly license?: string;
|
|
26
|
+
readonly repository?: unknown;
|
|
27
|
+
readonly bin?: unknown;
|
|
28
|
+
readonly files?: readonly string[];
|
|
29
|
+
readonly type?: string;
|
|
30
|
+
readonly sideEffects?: boolean | string[];
|
|
31
|
+
readonly exports?: unknown;
|
|
32
|
+
readonly dependencies?: Record<string, string>;
|
|
33
|
+
readonly peerDependencies?: Record<string, string>;
|
|
34
|
+
readonly peerDependenciesMeta?: Record<string, unknown>;
|
|
35
|
+
readonly engines?: unknown;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Directory under `~/.oke` where the consumer-facing local package is staged.
|
|
40
|
+
*
|
|
41
|
+
* @returns Absolute stage root
|
|
42
|
+
*/
|
|
43
|
+
export function localOkengineStageDir(): string {
|
|
44
|
+
return join(homedir(), ".oke", "create-oke", "okengine");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Build (or refresh) a publish-shaped okengine package and return its
|
|
49
|
+
* installable `file:` dependency string.
|
|
50
|
+
*
|
|
51
|
+
* @param localOkengineRoot - Absolute monorepo / package root
|
|
52
|
+
*/
|
|
53
|
+
export function materializeLocalOkengineDependency(localOkengineRoot: string): string {
|
|
54
|
+
const root = resolve(localOkengineRoot);
|
|
55
|
+
const pkgPath = join(root, "package.json");
|
|
56
|
+
if (!existsSync(pkgPath)) {
|
|
57
|
+
throw new Error(`create-oke: okengine package.json missing at ${pkgPath}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const raw = JSON.parse(readFileSync(pkgPath, "utf8")) as Record<string, unknown>;
|
|
61
|
+
if (raw["name"] !== "okengine") {
|
|
62
|
+
throw new Error(`create-oke: expected package name "okengine" at ${pkgPath}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const deps = isStringRecord(raw["dependencies"]) ? { ...raw["dependencies"] } : {};
|
|
66
|
+
const peers = isStringRecord(raw["peerDependencies"]) ? raw["peerDependencies"] : {};
|
|
67
|
+
const pins = {
|
|
68
|
+
...(isStringRecord(raw["devDependencies"]) ? raw["devDependencies"] : {}),
|
|
69
|
+
...deps,
|
|
70
|
+
};
|
|
71
|
+
// Peers are provided by the app at publish time; for a file: stage whose
|
|
72
|
+
// package root sits outside the app, install them beside the stage too.
|
|
73
|
+
// Prefer monorepo pin versions over peer ranges (`>=1.0.0-rc.0` can resolve
|
|
74
|
+
// a broken drizzle-kit channel build without `drizzle-kit/cli`).
|
|
75
|
+
const stagePeers = Object.fromEntries(
|
|
76
|
+
Object.entries(peers).map(([name, range]) => [name, pins[name] ?? range]),
|
|
77
|
+
);
|
|
78
|
+
const stageDependencies = { ...deps, ...stagePeers };
|
|
79
|
+
|
|
80
|
+
const consumer: ConsumerOkenginePackageJson = {
|
|
81
|
+
name: "okengine",
|
|
82
|
+
version: String(raw["version"] ?? "0.0.0"),
|
|
83
|
+
...(typeof raw["description"] === "string" ? { description: raw["description"] } : {}),
|
|
84
|
+
...(typeof raw["license"] === "string" ? { license: raw["license"] } : {}),
|
|
85
|
+
...(raw["repository"] !== undefined ? { repository: raw["repository"] } : {}),
|
|
86
|
+
...(raw["bin"] !== undefined ? { bin: raw["bin"] } : {}),
|
|
87
|
+
...(Array.isArray(raw["files"]) ? { files: raw["files"] as string[] } : {}),
|
|
88
|
+
...(typeof raw["type"] === "string" ? { type: raw["type"] } : {}),
|
|
89
|
+
...(raw["sideEffects"] !== undefined
|
|
90
|
+
? { sideEffects: raw["sideEffects"] as boolean | string[] }
|
|
91
|
+
: {}),
|
|
92
|
+
...(raw["exports"] !== undefined ? { exports: raw["exports"] } : {}),
|
|
93
|
+
...(Object.keys(stageDependencies).length > 0 ? { dependencies: stageDependencies } : {}),
|
|
94
|
+
...(raw["engines"] !== undefined ? { engines: raw["engines"] } : {}),
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const stage = localOkengineStageDir();
|
|
98
|
+
rmSync(stage, { recursive: true, force: true });
|
|
99
|
+
mkdirSync(stage, { recursive: true });
|
|
100
|
+
writeFileSync(join(stage, "package.json"), `${JSON.stringify(consumer, null, 2)}\n`, "utf8");
|
|
101
|
+
|
|
102
|
+
const entries =
|
|
103
|
+
consumer.files && consumer.files.length > 0
|
|
104
|
+
? consumer.files
|
|
105
|
+
: ["src", "manifest.v1.schema.json", "AGENTS.md", "README.md"];
|
|
106
|
+
|
|
107
|
+
for (const rel of entries) {
|
|
108
|
+
const from = join(root, rel);
|
|
109
|
+
if (!existsSync(from)) continue;
|
|
110
|
+
const to = join(stage, rel);
|
|
111
|
+
mkdirSync(dirname(to), { recursive: true });
|
|
112
|
+
cpSync(from, to, { recursive: true, dereference: true });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const install = Bun.spawnSync({
|
|
116
|
+
cmd: ["bun", "install"],
|
|
117
|
+
cwd: stage,
|
|
118
|
+
stdout: "pipe",
|
|
119
|
+
stderr: "pipe",
|
|
120
|
+
});
|
|
121
|
+
if (install.exitCode !== 0) {
|
|
122
|
+
const err = new TextDecoder().decode(install.stderr).trim();
|
|
123
|
+
throw new Error(
|
|
124
|
+
`create-oke: bun install failed in local okengine stage${err ? `\n${err}` : ""}`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return `file:${stage}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @param value - Unknown record candidate
|
|
133
|
+
*/
|
|
134
|
+
function isStringRecord(value: unknown): value is Record<string, string> {
|
|
135
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
136
|
+
return Object.values(value).every((v) => typeof v === "string");
|
|
137
|
+
}
|
package/src/scaffold.ts
CHANGED
|
@@ -175,7 +175,7 @@ export function scaffold(options: ScaffoldOptions): ScaffoldResult {
|
|
|
175
175
|
/**
|
|
176
176
|
* For postgres, pin `store.sql` local/docker/prod in `oke.config.ts`.
|
|
177
177
|
*
|
|
178
|
-
* The default template ships abstract `src/schema.decl.ts` — dialect is
|
|
178
|
+
* The default template ships abstract `src/db/schema.decl.ts` — dialect is
|
|
179
179
|
* emitted from the active `store.sql` driver, so there is no hand-written
|
|
180
180
|
* `sqliteTable`/`pgTable` source to rewrite. `sqlite` keeps the template
|
|
181
181
|
* dual-mode config (`local: sqlite` · `docker`/`prod: postgres`) untouched.
|