create-oke 0.6.1 → 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.
- package/README.md +23 -8
- package/package.json +3 -3
- package/src/ai-setup/apply.ts +214 -0
- package/src/ai-setup/catalog.ts +263 -0
- package/src/ai-setup/detect-ollama.ts +180 -0
- package/src/ai-setup/from-pref.ts +99 -0
- package/src/ai-setup/prompts.ts +649 -0
- package/src/ai-setup/recommend.ts +203 -0
- package/src/cli.test.ts +385 -28
- package/src/cli.ts +332 -29
- package/src/create-defaults.test.ts +92 -0
- package/src/create-defaults.ts +246 -0
- package/src/customize-flow.test.ts +82 -0
- package/src/customize-flow.ts +432 -0
- package/src/drivers-catalog.ts +217 -0
- package/src/scaffold.ts +97 -10
- package/src/templates.ts +14 -9
- package/src/transform.test.ts +105 -0
- package/src/transform.ts +227 -0
- package/src/wizard-select.ts +50 -0
- package/{template → templates/advanced}/.env.example +1 -0
- package/templates/advanced/README.md +32 -0
- package/templates/advanced/oke.config.ts +74 -0
- package/templates/advanced/package.json +18 -0
- package/templates/advanced/src/app.ts +30 -0
- package/templates/advanced/src/channels.ts +13 -0
- package/templates/advanced/src/core.ts +8 -0
- package/{template → templates/advanced}/src/flows/main/index.ts +9 -6
- package/templates/advanced/src/flows/main/shapes.ts +1 -0
- package/templates/advanced/src/flows/main/signals.ts +1 -0
- package/templates/advanced/src/flows/notes/index.ts +224 -0
- package/templates/advanced/src/flows/notes/shapes.ts +52 -0
- package/templates/advanced/src/flows/notes/signals.ts +13 -0
- package/templates/advanced/src/gates.ts +12 -0
- package/templates/advanced/src/locales/ar.ts +18 -0
- package/templates/advanced/src/locales/en.ts +22 -0
- package/templates/advanced/src/schema.decl.ts +13 -0
- package/templates/advanced/src/vault.ts +7 -0
- package/templates/advanced/tests/advanced.test.ts +42 -0
- package/templates/standard/.env.example +94 -0
- package/templates/standard/README.md +46 -0
- package/templates/standard/drizzle.config.ts +25 -0
- package/{template → templates/standard}/oke.config.ts +9 -9
- package/{template → templates/standard}/package.json +1 -1
- package/templates/standard/src/app.ts +30 -0
- package/templates/standard/src/channels.ts +13 -0
- package/{template → templates/standard}/src/core.ts +1 -0
- package/templates/standard/src/flows/main/index.ts +34 -0
- package/templates/standard/src/flows/main/shapes.ts +1 -0
- package/templates/standard/src/flows/main/signals.ts +1 -0
- package/templates/standard/src/flows/notes/index.ts +143 -0
- package/templates/standard/src/flows/notes/shapes.ts +26 -0
- package/templates/standard/src/flows/notes/signals.ts +13 -0
- package/templates/standard/src/gates.ts +12 -0
- package/templates/standard/src/locales/ar.ts +18 -0
- package/templates/standard/src/locales/en.ts +22 -0
- package/templates/standard/src/schema.decl.ts +13 -0
- package/templates/standard/src/vault.ts +7 -0
- package/templates/standard/tests/standard.test.ts +51 -0
- package/template/README.md +0 -77
- package/template/bun.lock +0 -845
- package/template/src/app.ts +0 -18
- package/template/src/channels.ts +0 -4
- package/template/src/flows/main/shapes.ts +0 -2
- package/template/src/flows/main/signals.ts +0 -4
- package/template/src/gates.ts +0 -4
- package/template/src/locales/ar.ts +0 -20
- package/template/src/locales/en.ts +0 -26
- package/template/src/schema.decl.ts +0 -12
- package/template/src/vault.ts +0 -4
- package/template/tests/standard.test.ts +0 -18
- /package/{template → templates/advanced}/drizzle.config.ts +0 -0
package/src/cli.test.ts
CHANGED
|
@@ -3,10 +3,21 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { describe, expect, test } from "bun:test";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
existsSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
mkdtempSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
readdirSync,
|
|
12
|
+
rmSync,
|
|
13
|
+
writeFileSync,
|
|
14
|
+
} from "node:fs";
|
|
7
15
|
import { tmpdir } from "node:os";
|
|
8
16
|
import { join } from "node:path";
|
|
9
17
|
import {
|
|
18
|
+
WIZARD_BACK,
|
|
19
|
+
aiSetupProviderFor,
|
|
20
|
+
defaultsBranchOptions,
|
|
10
21
|
formatCdPath,
|
|
11
22
|
nextStepsText,
|
|
12
23
|
parseArgs,
|
|
@@ -15,11 +26,26 @@ import {
|
|
|
15
26
|
scaffoldArgsFromCli,
|
|
16
27
|
shouldPrompt,
|
|
17
28
|
sourceFromArgs,
|
|
29
|
+
withBackOption,
|
|
18
30
|
type InteractiveAnswers,
|
|
19
31
|
} from "./cli.ts";
|
|
20
|
-
import { listTemplateFiles, scaffold } from "./scaffold.ts";
|
|
21
|
-
import { DEFAULT_TEMPLATE, TEMPLATES, packageRoot, resolveTemplateDir } from "./templates.ts";
|
|
22
32
|
import {
|
|
33
|
+
createDefaultsPath,
|
|
34
|
+
readCreateDefaults,
|
|
35
|
+
toCreateDefaults,
|
|
36
|
+
writeCreateDefaults,
|
|
37
|
+
} from "./create-defaults.ts";
|
|
38
|
+
import { pinsDockerReady, pinsLocalOnly, recommendedDefaults } from "./drivers-catalog.ts";
|
|
39
|
+
import { listTemplateFiles, scaffold, targetDirectoryBlockReason } from "./scaffold.ts";
|
|
40
|
+
import {
|
|
41
|
+
DEFAULT_TEMPLATE,
|
|
42
|
+
TEMPLATE_DEFAULT_MODE,
|
|
43
|
+
TEMPLATES,
|
|
44
|
+
packageRoot,
|
|
45
|
+
resolveTemplateDir,
|
|
46
|
+
} from "./templates.ts";
|
|
47
|
+
import {
|
|
48
|
+
applyCreateAnswers,
|
|
23
49
|
sanitizeProjectName,
|
|
24
50
|
shouldSkipTemplatePath,
|
|
25
51
|
transformConfigForSqlDriver,
|
|
@@ -27,6 +53,18 @@ import {
|
|
|
27
53
|
transformSchemaForSqlDriver,
|
|
28
54
|
} from "./transform.ts";
|
|
29
55
|
|
|
56
|
+
describe("defaultsBranchOptions", () => {
|
|
57
|
+
test("hides reuse when no previous settings exist", () => {
|
|
58
|
+
const values = defaultsBranchOptions(false).map((o) => o.value);
|
|
59
|
+
expect(values).toEqual(["recommended", "customize"]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("offers reuse when previous settings exist", () => {
|
|
63
|
+
const values = defaultsBranchOptions(true).map((o) => o.value);
|
|
64
|
+
expect(values).toEqual(["recommended", "reuse", "customize"]);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
30
68
|
describe("parseArgs", () => {
|
|
31
69
|
test("defaults template to standard", () => {
|
|
32
70
|
const a = parseArgs(["my-app"]);
|
|
@@ -37,7 +75,7 @@ describe("parseArgs", () => {
|
|
|
37
75
|
|
|
38
76
|
test("accepts --template and -t", () => {
|
|
39
77
|
expect(parseArgs(["x", "--template", "standard"]).template).toBe("standard");
|
|
40
|
-
expect(parseArgs(["x", "-t", "
|
|
78
|
+
expect(parseArgs(["x", "-t", "advanced"]).template).toBe("advanced");
|
|
41
79
|
expect(parseArgs(["x", "--template=standard"]).template).toBe("standard");
|
|
42
80
|
expect(parseArgs(["x", "--template", "standard"]).templateExplicit).toBe(true);
|
|
43
81
|
});
|
|
@@ -100,39 +138,295 @@ describe("sourceFromArgs", () => {
|
|
|
100
138
|
});
|
|
101
139
|
});
|
|
102
140
|
|
|
141
|
+
function recommendedAnswers(overrides: Partial<InteractiveAnswers> = {}): InteractiveAnswers {
|
|
142
|
+
return {
|
|
143
|
+
name: "x",
|
|
144
|
+
choice: "standard",
|
|
145
|
+
installAndRun: false,
|
|
146
|
+
agentsMd: true,
|
|
147
|
+
createDefaults: undefined,
|
|
148
|
+
aiApply: null,
|
|
149
|
+
...overrides,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
103
153
|
describe("scaffoldArgsFromAnswers ≡ flag-driven", () => {
|
|
104
|
-
test("each --template path matches interactive choice", () => {
|
|
154
|
+
test("each --template path matches interactive choice (recommended)", () => {
|
|
105
155
|
for (const id of TEMPLATES) {
|
|
106
|
-
const answers
|
|
107
|
-
name: "x",
|
|
108
|
-
choice: id,
|
|
109
|
-
installAndRun: false,
|
|
110
|
-
agentsMd: true,
|
|
111
|
-
};
|
|
156
|
+
const answers = recommendedAnswers({ choice: id });
|
|
112
157
|
const fromAnswers = scaffoldArgsFromAnswers(answers);
|
|
113
158
|
const fromFlags = scaffoldArgsFromCli(parseArgs(["x", "--template", id]));
|
|
114
|
-
expect(fromAnswers).
|
|
115
|
-
expect(fromAnswers.
|
|
116
|
-
expect(fromAnswers.
|
|
159
|
+
expect(fromAnswers.name).toBe(fromFlags.name);
|
|
160
|
+
expect(fromAnswers.targetDir).toBe(fromFlags.targetDir);
|
|
161
|
+
expect(fromAnswers.source).toEqual(fromFlags.source);
|
|
162
|
+
expect(fromAnswers.agentsMd).toBe(fromFlags.agentsMd);
|
|
163
|
+
expect(fromAnswers.sqlDriver).toBe(fromFlags.sqlDriver);
|
|
164
|
+
expect(fromAnswers.createDefaults).toBeUndefined();
|
|
117
165
|
}
|
|
118
166
|
});
|
|
119
167
|
|
|
120
|
-
test("interactive never opts into --sql postgres
|
|
121
|
-
|
|
122
|
-
name: "x",
|
|
123
|
-
choice: "standard",
|
|
124
|
-
installAndRun: false,
|
|
125
|
-
agentsMd: true,
|
|
126
|
-
};
|
|
127
|
-
expect(scaffoldArgsFromAnswers(answers).sqlDriver).toBe("sqlite");
|
|
168
|
+
test("interactive never opts into --sql postgres without override", () => {
|
|
169
|
+
expect(scaffoldArgsFromAnswers(recommendedAnswers()).sqlDriver).toBe("sqlite");
|
|
128
170
|
expect(
|
|
129
171
|
scaffoldArgsFromCli(parseArgs(["x", "--template", "standard", "--sql", "postgres"]))
|
|
130
172
|
.sqlDriver,
|
|
131
173
|
).toBe("postgres");
|
|
132
174
|
});
|
|
133
175
|
|
|
134
|
-
test("
|
|
135
|
-
expect(TEMPLATES).toEqual(["standard"]);
|
|
176
|
+
test("standard and advanced are available", () => {
|
|
177
|
+
expect(TEMPLATES).toEqual(["standard", "advanced"]);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe("defaultsBranchOptions template hints", () => {
|
|
182
|
+
test("reuse hint names the selected template", () => {
|
|
183
|
+
const reuse = defaultsBranchOptions(true, "advanced").find((o) => o.value === "reuse");
|
|
184
|
+
expect(reuse?.hint).toContain("advanced");
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe("parseArgs AI flags", () => {
|
|
189
|
+
test("accepts --ai / --no-ai / --ai skip", () => {
|
|
190
|
+
expect(parseArgs(["x"]).ai).toBe("prompt");
|
|
191
|
+
expect(parseArgs(["x", "--ai"]).ai).toBe("force");
|
|
192
|
+
expect(parseArgs(["x", "--no-ai"]).ai).toBe("skip");
|
|
193
|
+
expect(parseArgs(["x", "--ai", "skip"]).ai).toBe("skip");
|
|
194
|
+
expect(parseArgs(["x", "--ai=skip"]).ai).toBe("skip");
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe("wizard ← Back", () => {
|
|
199
|
+
test("withBackOption appends sentinel when allowed", () => {
|
|
200
|
+
const base = [{ value: "memory", label: "memory" }];
|
|
201
|
+
expect(withBackOption(base, false)).toEqual(base);
|
|
202
|
+
const withBack = withBackOption(base, true);
|
|
203
|
+
expect(withBack.at(-1)).toEqual({ value: WIZARD_BACK, label: "← Back" });
|
|
204
|
+
expect(WIZARD_BACK).toBe("__back__");
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe("aiSetupProviderFor", () => {
|
|
209
|
+
test("prefers ollama when docker is ollama even if local is mock", () => {
|
|
210
|
+
expect(
|
|
211
|
+
aiSetupProviderFor("mock", {
|
|
212
|
+
local: "mock",
|
|
213
|
+
docker: "ollama",
|
|
214
|
+
test: "mock",
|
|
215
|
+
prod: "ollama",
|
|
216
|
+
}),
|
|
217
|
+
).toBe("ollama");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("keeps local ollama", () => {
|
|
221
|
+
expect(
|
|
222
|
+
aiSetupProviderFor("ollama", {
|
|
223
|
+
local: "ollama",
|
|
224
|
+
docker: "anthropic",
|
|
225
|
+
test: "mock",
|
|
226
|
+
prod: "anthropic",
|
|
227
|
+
}),
|
|
228
|
+
).toBe("ollama");
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe("interactive branches", () => {
|
|
233
|
+
test("recommended → no createDefaults, no AI setup", async () => {
|
|
234
|
+
const dir = mkdtempSync(join(tmpdir(), "oke-rec-"));
|
|
235
|
+
rmSync(dir, { recursive: true, force: true });
|
|
236
|
+
try {
|
|
237
|
+
const code = await run([dir], {
|
|
238
|
+
stdinIsTTY: true,
|
|
239
|
+
runPostScaffold: false,
|
|
240
|
+
ask: async () => recommendedAnswers({ name: dir }),
|
|
241
|
+
});
|
|
242
|
+
expect(code).toBe(0);
|
|
243
|
+
const config = readFileSync(join(dir, "oke.config.ts"), "utf8");
|
|
244
|
+
expect(config).toContain('local: "sqlite"');
|
|
245
|
+
expect(readFileSync(join(dir, ".oke", "mode"), "utf8").trim()).toBe("local");
|
|
246
|
+
expect(existsSync(join(dir, "src", "flows", "notes", "index.ts"))).toBe(true);
|
|
247
|
+
} finally {
|
|
248
|
+
rmSync(dir, { recursive: true, force: true });
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("recommended advanced → docker mode + advanced Notes flows", async () => {
|
|
253
|
+
const dir = mkdtempSync(join(tmpdir(), "oke-rec-adv-"));
|
|
254
|
+
rmSync(dir, { recursive: true, force: true });
|
|
255
|
+
try {
|
|
256
|
+
const code = await run([dir], {
|
|
257
|
+
stdinIsTTY: true,
|
|
258
|
+
runPostScaffold: false,
|
|
259
|
+
ask: async () => recommendedAnswers({ name: dir, choice: "advanced" }),
|
|
260
|
+
});
|
|
261
|
+
expect(code).toBe(0);
|
|
262
|
+
expect(readFileSync(join(dir, ".oke", "mode"), "utf8").trim()).toBe("docker");
|
|
263
|
+
const notes = readFileSync(join(dir, "src", "flows", "notes", "index.ts"), "utf8");
|
|
264
|
+
expect(notes).toContain('name: "notes.digest"');
|
|
265
|
+
expect(notes).toContain('name: "notes.attach"');
|
|
266
|
+
} finally {
|
|
267
|
+
rmSync(dir, { recursive: true, force: true });
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("reuse previous settings round-trip", async () => {
|
|
272
|
+
const home = mkdtempSync(join(tmpdir(), "oke-reuse-"));
|
|
273
|
+
const path = createDefaultsPath(home);
|
|
274
|
+
const base = recommendedDefaults("local-only");
|
|
275
|
+
const saved = {
|
|
276
|
+
...base,
|
|
277
|
+
drivers: {
|
|
278
|
+
...base.drivers,
|
|
279
|
+
store: {
|
|
280
|
+
...base.drivers.store,
|
|
281
|
+
sql: {
|
|
282
|
+
local: "libsql",
|
|
283
|
+
docker: "postgres",
|
|
284
|
+
test: "memory",
|
|
285
|
+
prod: "postgres",
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
writeCreateDefaults(saved, path);
|
|
291
|
+
|
|
292
|
+
const dir = mkdtempSync(join(tmpdir(), "oke-reuse-app-"));
|
|
293
|
+
rmSync(dir, { recursive: true, force: true });
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
const code = await run([dir], {
|
|
297
|
+
stdinIsTTY: true,
|
|
298
|
+
runPostScaffold: false,
|
|
299
|
+
ask: async () =>
|
|
300
|
+
recommendedAnswers({
|
|
301
|
+
name: dir,
|
|
302
|
+
createDefaults: saved,
|
|
303
|
+
aiApply: null,
|
|
304
|
+
}),
|
|
305
|
+
});
|
|
306
|
+
expect(code).toBe(0);
|
|
307
|
+
const config = readFileSync(join(dir, "oke.config.ts"), "utf8");
|
|
308
|
+
expect(config).toContain('local: "libsql"');
|
|
309
|
+
const templateConfig = readFileSync(
|
|
310
|
+
join(resolveTemplateDir("standard"), "oke.config.ts"),
|
|
311
|
+
"utf8",
|
|
312
|
+
);
|
|
313
|
+
expect(() => applyCreateAnswers(templateConfig, saved)).not.toThrow();
|
|
314
|
+
} finally {
|
|
315
|
+
rmSync(home, { recursive: true, force: true });
|
|
316
|
+
rmSync(dir, { recursive: true, force: true });
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test("customize answers applied + persistence write", async () => {
|
|
321
|
+
const home = mkdtempSync(join(tmpdir(), "oke-custom-"));
|
|
322
|
+
const path = createDefaultsPath(home);
|
|
323
|
+
const customized = toCreateDefaults({
|
|
324
|
+
template: "standard",
|
|
325
|
+
profile: "local-only",
|
|
326
|
+
drivers: {
|
|
327
|
+
store: {
|
|
328
|
+
sql: pinsLocalOnly("pglite", "postgres", "memory"),
|
|
329
|
+
kv: pinsLocalOnly("memory", "redis", "memory"),
|
|
330
|
+
files: pinsLocalOnly("fs", "s3", "memory"),
|
|
331
|
+
index: null,
|
|
332
|
+
},
|
|
333
|
+
signal: pinsLocalOnly("memory", "redis", "memory"),
|
|
334
|
+
clock: pinsLocalOnly("memory", "file", "frozen"),
|
|
335
|
+
vault: pinsLocalOnly("env", "openbao", "memory"),
|
|
336
|
+
channel: { email: pinsLocalOnly("console", "smtp", "console") },
|
|
337
|
+
ai: null,
|
|
338
|
+
},
|
|
339
|
+
ai: { enabled: false, provider: null, driver: null },
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
const dir = mkdtempSync(join(tmpdir(), "oke-custom-app-"));
|
|
343
|
+
rmSync(dir, { recursive: true, force: true });
|
|
344
|
+
|
|
345
|
+
try {
|
|
346
|
+
const code = await run([dir], {
|
|
347
|
+
stdinIsTTY: true,
|
|
348
|
+
runPostScaffold: false,
|
|
349
|
+
ask: async (partial) => {
|
|
350
|
+
writeCreateDefaults(customized, path);
|
|
351
|
+
return recommendedAnswers({
|
|
352
|
+
name: dir,
|
|
353
|
+
createDefaults: customized,
|
|
354
|
+
agentsMd: partial.agentsMd ?? true,
|
|
355
|
+
});
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
expect(code).toBe(0);
|
|
359
|
+
expect(existsSync(path)).toBe(true);
|
|
360
|
+
expect(readCreateDefaults(path)?.drivers.store.sql.local).toBe("pglite");
|
|
361
|
+
expect(readFileSync(join(dir, "oke.config.ts"), "utf8")).toContain('local: "pglite"');
|
|
362
|
+
// local-only profile → seed oke dev mode so the prompt is not repeated.
|
|
363
|
+
expect(readFileSync(join(dir, ".oke", "mode"), "utf8").trim()).toBe("local");
|
|
364
|
+
} finally {
|
|
365
|
+
rmSync(home, { recursive: true, force: true });
|
|
366
|
+
rmSync(dir, { recursive: true, force: true });
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test("docker-ready profile seeds .oke/mode as docker", async () => {
|
|
371
|
+
const customized = toCreateDefaults({
|
|
372
|
+
template: "advanced",
|
|
373
|
+
profile: "docker-ready",
|
|
374
|
+
drivers: {
|
|
375
|
+
store: {
|
|
376
|
+
sql: pinsDockerReady("sqlite", "postgres", "memory"),
|
|
377
|
+
kv: pinsLocalOnly("memory", "redis", "memory"),
|
|
378
|
+
files: pinsLocalOnly("fs", "s3", "memory"),
|
|
379
|
+
index: null,
|
|
380
|
+
},
|
|
381
|
+
signal: pinsLocalOnly("memory", "redis", "memory"),
|
|
382
|
+
clock: pinsLocalOnly("memory", "file", "frozen"),
|
|
383
|
+
vault: pinsLocalOnly("env", "openbao", "memory"),
|
|
384
|
+
channel: { email: pinsLocalOnly("console", "smtp", "console") },
|
|
385
|
+
ai: null,
|
|
386
|
+
},
|
|
387
|
+
ai: { enabled: false, provider: null, driver: null },
|
|
388
|
+
});
|
|
389
|
+
const dir = mkdtempSync(join(tmpdir(), "oke-docker-mode-"));
|
|
390
|
+
rmSync(dir, { recursive: true, force: true });
|
|
391
|
+
try {
|
|
392
|
+
const code = await run([dir], {
|
|
393
|
+
stdinIsTTY: true,
|
|
394
|
+
runPostScaffold: false,
|
|
395
|
+
ask: async () =>
|
|
396
|
+
recommendedAnswers({
|
|
397
|
+
name: dir,
|
|
398
|
+
createDefaults: customized,
|
|
399
|
+
aiApply: null,
|
|
400
|
+
}),
|
|
401
|
+
});
|
|
402
|
+
expect(code).toBe(0);
|
|
403
|
+
expect(readFileSync(join(dir, ".oke", "mode"), "utf8").trim()).toBe("docker");
|
|
404
|
+
} finally {
|
|
405
|
+
rmSync(dir, { recursive: true, force: true });
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
test("non-TTY --yes stays zero-prompt recommended", async () => {
|
|
410
|
+
const dir = mkdtempSync(join(tmpdir(), "oke-yes-"));
|
|
411
|
+
rmSync(dir, { recursive: true, force: true });
|
|
412
|
+
try {
|
|
413
|
+
let askCalled = false;
|
|
414
|
+
const code = await run([dir, "--yes", "--no-install", "--no-ai"], {
|
|
415
|
+
stdinIsTTY: false,
|
|
416
|
+
runPostScaffold: false,
|
|
417
|
+
ask: async () => {
|
|
418
|
+
askCalled = true;
|
|
419
|
+
return recommendedAnswers({ name: dir });
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
expect(code).toBe(0);
|
|
423
|
+
expect(askCalled).toBe(false);
|
|
424
|
+
const config = readFileSync(join(dir, "oke.config.ts"), "utf8");
|
|
425
|
+
expect(config).toContain('local: "sqlite"');
|
|
426
|
+
expect(config).not.toMatch(/\bai:\s*\{/);
|
|
427
|
+
} finally {
|
|
428
|
+
rmSync(dir, { recursive: true, force: true });
|
|
429
|
+
}
|
|
136
430
|
});
|
|
137
431
|
});
|
|
138
432
|
|
|
@@ -234,9 +528,12 @@ describe("scaffold structure", () => {
|
|
|
234
528
|
name: `app-${id}`,
|
|
235
529
|
source: { kind: "template", id },
|
|
236
530
|
});
|
|
237
|
-
const extras = ["AGENTS.md"];
|
|
531
|
+
const extras = ["AGENTS.md", ".oke/mode"];
|
|
238
532
|
if (expected.includes(".env.example")) extras.push(".env.local");
|
|
239
533
|
expect([...result.files].sort()).toEqual([...expected, ...extras].sort());
|
|
534
|
+
expect(readFileSync(join(result.targetDir, ".oke", "mode"), "utf8").trim()).toBe(
|
|
535
|
+
TEMPLATE_DEFAULT_MODE[id],
|
|
536
|
+
);
|
|
240
537
|
expect(result.files).toContain(".gitignore");
|
|
241
538
|
expect(result.files).toContain("README.md");
|
|
242
539
|
expect(result.sqlDriver).toBe("sqlite");
|
|
@@ -245,21 +542,22 @@ describe("scaffold structure", () => {
|
|
|
245
542
|
);
|
|
246
543
|
const readme = readFileSync(join(result.targetDir, "README.md"), "utf8");
|
|
247
544
|
expect(readme).toMatch(/oke dev/);
|
|
248
|
-
expect(readme).toMatch(new RegExp(
|
|
545
|
+
expect(readme).toMatch(new RegExp(`Notes \\(${id}\\)`, "i"));
|
|
546
|
+
expect(readme).toMatch(/notes\.(create|attach|digest)|main\.health/);
|
|
249
547
|
expect(readFileSync(join(result.targetDir, ".gitignore"), "utf8")).toMatch(/node_modules/);
|
|
250
548
|
const pkg = JSON.parse(readFileSync(join(result.targetDir, "package.json"), "utf8")) as {
|
|
251
549
|
name: string;
|
|
252
550
|
dependencies: { okengine: string };
|
|
253
551
|
};
|
|
254
552
|
expect(pkg.name).toBe(`app-${id}`);
|
|
255
|
-
expect(pkg.dependencies.okengine).not.
|
|
553
|
+
expect(pkg.dependencies.okengine).not.toMatch(/^file:\.\./);
|
|
256
554
|
} finally {
|
|
257
555
|
rmSync(dir, { recursive: true, force: true });
|
|
258
556
|
}
|
|
259
557
|
}
|
|
260
558
|
});
|
|
261
559
|
|
|
262
|
-
test("standard has
|
|
560
|
+
test("standard has Notes layout (gates/vault/channels + notes flows)", () => {
|
|
263
561
|
const dir = mkdtempSync(join(tmpdir(), "create-oke-standard-assert-"));
|
|
264
562
|
try {
|
|
265
563
|
const result = scaffold({
|
|
@@ -275,12 +573,16 @@ describe("scaffold structure", () => {
|
|
|
275
573
|
"src/locales/ar.ts",
|
|
276
574
|
"src/flows/main/shapes.ts",
|
|
277
575
|
"src/flows/main/signals.ts",
|
|
576
|
+
"src/flows/notes/index.ts",
|
|
278
577
|
"src/core.ts",
|
|
279
578
|
"src/schema.decl.ts",
|
|
280
579
|
"src/app.ts",
|
|
281
580
|
]) {
|
|
282
581
|
expect(result.files).toContain(path);
|
|
283
582
|
}
|
|
583
|
+
const notes = readFileSync(join(result.targetDir, "src/flows/notes/index.ts"), "utf8");
|
|
584
|
+
expect(notes).toContain('name: "notes.create"');
|
|
585
|
+
expect(notes).not.toContain('name: "notes.digest"');
|
|
284
586
|
const all = result.files
|
|
285
587
|
.filter((f) => f.endsWith(".ts") || f.endsWith(".md"))
|
|
286
588
|
.map((f) => readFileSync(join(result.targetDir, f), "utf8"))
|
|
@@ -335,12 +637,67 @@ describe("scaffold structure", () => {
|
|
|
335
637
|
});
|
|
336
638
|
});
|
|
337
639
|
|
|
640
|
+
describe("targetDirectoryBlockReason", () => {
|
|
641
|
+
test("null when missing or empty", () => {
|
|
642
|
+
const root = mkdtempSync(join(tmpdir(), "oke-target-ok-"));
|
|
643
|
+
const missing = join(root, "fresh");
|
|
644
|
+
const empty = join(root, "empty");
|
|
645
|
+
mkdirSync(empty);
|
|
646
|
+
try {
|
|
647
|
+
expect(targetDirectoryBlockReason(missing)).toBeNull();
|
|
648
|
+
expect(targetDirectoryBlockReason(empty)).toBeNull();
|
|
649
|
+
} finally {
|
|
650
|
+
rmSync(root, { recursive: true, force: true });
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
test("names the conflicting folder when not empty", () => {
|
|
655
|
+
const root = mkdtempSync(join(tmpdir(), "oke-target-busy-"));
|
|
656
|
+
const busy = join(root, "oke-1");
|
|
657
|
+
mkdirSync(busy);
|
|
658
|
+
writeFileSync(join(busy, "package.json"), "{}\n");
|
|
659
|
+
try {
|
|
660
|
+
const reason = targetDirectoryBlockReason(busy);
|
|
661
|
+
expect(reason).toMatch(/"oke-1" already exists and is not empty/);
|
|
662
|
+
expect(reason).toContain(busy);
|
|
663
|
+
} finally {
|
|
664
|
+
rmSync(root, { recursive: true, force: true });
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
});
|
|
668
|
+
|
|
338
669
|
describe("non-TTY CLI", () => {
|
|
339
670
|
test("no args + non-TTY → zero prompts, exit 1", async () => {
|
|
340
671
|
const code = await run([], { stdinIsTTY: false, runPostScaffold: false });
|
|
341
672
|
expect(code).toBe(1);
|
|
342
673
|
});
|
|
343
674
|
|
|
675
|
+
test("existing non-empty dir fails before scaffold", async () => {
|
|
676
|
+
const root = mkdtempSync(join(tmpdir(), "create-oke-exists-"));
|
|
677
|
+
const target = join(root, "oke-1");
|
|
678
|
+
mkdirSync(target);
|
|
679
|
+
writeFileSync(join(target, "README.md"), "already here\n");
|
|
680
|
+
try {
|
|
681
|
+
const proc = Bun.spawn(
|
|
682
|
+
["bun", "run", join(packageRoot(), "src/index.ts"), target, "--yes", "--no-install"],
|
|
683
|
+
{
|
|
684
|
+
cwd: root,
|
|
685
|
+
stdin: "pipe",
|
|
686
|
+
stdout: "pipe",
|
|
687
|
+
stderr: "pipe",
|
|
688
|
+
},
|
|
689
|
+
);
|
|
690
|
+
proc.stdin.end();
|
|
691
|
+
const code = await proc.exited;
|
|
692
|
+
const err = await new Response(proc.stderr).text();
|
|
693
|
+
expect(code).toBe(1);
|
|
694
|
+
expect(err).toMatch(/"oke-1" already exists and is not empty/);
|
|
695
|
+
expect(readFileSync(join(target, "README.md"), "utf8")).toBe("already here\n");
|
|
696
|
+
} finally {
|
|
697
|
+
rmSync(root, { recursive: true, force: true });
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
|
|
344
701
|
test("spawned with piped stdin shows no clack prompts", async () => {
|
|
345
702
|
const proc = Bun.spawn(["bun", "run", join(packageRoot(), "src/index.ts")], {
|
|
346
703
|
cwd: packageRoot(),
|