ai-engineering-kit 0.1.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 (37) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +33 -0
  3. package/README.md +65 -0
  4. package/dist/cli.js +675 -0
  5. package/package.json +56 -0
  6. package/template/agents/claude-code.CLAUDE.md +88 -0
  7. package/template/agents/codex.AGENTS.md +82 -0
  8. package/template/ai-workspace/brainstorms/.gitkeep +0 -0
  9. package/template/ai-workspace/plans/.gitkeep +0 -0
  10. package/template/ai-workspace/prds/.gitkeep +0 -0
  11. package/template/ai-workspace/research/.gitkeep +0 -0
  12. package/template/ai-workspace/reviews/.gitkeep +0 -0
  13. package/template/ai-workspace/runbooks/.gitkeep +0 -0
  14. package/template/ai-workspace/templates/.gitkeep +0 -0
  15. package/template/docs/foundations/product-vision.md +21 -0
  16. package/template/docs/foundations/project-guidelines.md +19 -0
  17. package/template/docs/foundations/technical-decisions.md +50 -0
  18. package/template/docs/foundations/testing-principles.md +13 -0
  19. package/template/skills/core-workflow/audit-architecture/REFERENCE.md +78 -0
  20. package/template/skills/core-workflow/audit-architecture/SKILL.md +76 -0
  21. package/template/skills/core-workflow/explore-design/SKILL.md +94 -0
  22. package/template/skills/core-workflow/implement/SKILL.md +107 -0
  23. package/template/skills/core-workflow/implement/deep-modules.md +33 -0
  24. package/template/skills/core-workflow/implement/interface-design.md +31 -0
  25. package/template/skills/core-workflow/implement/mocking.md +59 -0
  26. package/template/skills/core-workflow/implement/refactoring.md +10 -0
  27. package/template/skills/core-workflow/implement/tests.md +61 -0
  28. package/template/skills/core-workflow/investigate-bug/SKILL.md +104 -0
  29. package/template/skills/core-workflow/kickoff/SKILL.md +10 -0
  30. package/template/skills/core-workflow/load-context/SKILL.md +26 -0
  31. package/template/skills/core-workflow/plan-feature/SKILL.md +107 -0
  32. package/template/skills/core-workflow/plan-refactor/SKILL.md +68 -0
  33. package/template/skills/core-workflow/review/SKILL.md +127 -0
  34. package/template/skills/core-workflow/setup-foundations/SKILL.md +58 -0
  35. package/template/skills/core-workflow/verify-completion/SKILL.md +20 -0
  36. package/template/skills/core-workflow/write-prd/SKILL.md +74 -0
  37. package/template/skills/core-workflow/write-skill/SKILL.md +117 -0
package/dist/cli.js ADDED
@@ -0,0 +1,675 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { basename as basename2 } from "path";
5
+ import { intro, outro, confirm, select, isCancel as isCancel2, cancel as cancel2, log } from "@clack/prompts";
6
+
7
+ // src/catalog/load.ts
8
+ import { readdirSync, readFileSync } from "fs";
9
+ import { join, posix } from "path";
10
+
11
+ // src/manifest/manifest.ts
12
+ import { createHash } from "crypto";
13
+ function hashContent(content) {
14
+ return createHash("sha256").update(content).digest("hex");
15
+ }
16
+ function serializeManifest(manifest) {
17
+ return JSON.stringify(manifest, null, 2) + "\n";
18
+ }
19
+ function parseManifest(json) {
20
+ return JSON.parse(json);
21
+ }
22
+
23
+ // src/catalog/definition.ts
24
+ var CATEGORIES = [
25
+ { id: "core-workflow", label: "Core workflow", from: "skills/core-workflow", to: "ai/skills" }
26
+ ];
27
+ var TREE_COMPONENTS = [
28
+ { id: "docs-foundations", label: "docs/foundations scaffold", from: "docs/foundations", to: "docs/foundations" },
29
+ { id: "ai-workspace", label: "ai/ workspace dirs", from: "ai-workspace", to: "ai" }
30
+ ];
31
+ var COMPONENTS = [
32
+ { id: "skills", label: "Skills", unlocks: "categories" },
33
+ ...TREE_COMPONENTS.map((t) => ({ id: t.id, label: t.label })),
34
+ { id: "entry-files", label: "Agent entry files", unlocks: "agents" }
35
+ ];
36
+ var AGENTS = [
37
+ {
38
+ id: "claude-code",
39
+ label: "Claude Code",
40
+ files: [{ from: "agents/claude-code.CLAUDE.md", to: "CLAUDE.md", substitute: true }]
41
+ },
42
+ {
43
+ id: "codex",
44
+ label: "Codex",
45
+ files: [{ from: "agents/codex.AGENTS.md", to: "AGENTS.md", substitute: true }]
46
+ }
47
+ ];
48
+
49
+ // src/catalog/load.ts
50
+ function listFiles(root, base) {
51
+ let out = [];
52
+ for (const entry of readdirSync(join(root, base), { withFileTypes: true })) {
53
+ const rel = base ? posix.join(base, entry.name) : entry.name;
54
+ if (entry.isDirectory()) out = out.concat(listFiles(root, rel));
55
+ else out.push(rel);
56
+ }
57
+ return out;
58
+ }
59
+ function catalogFile(templateDir, templatePath, targetPath, extra) {
60
+ const content = readFileSync(join(templateDir, templatePath), "utf8");
61
+ return {
62
+ targetPath,
63
+ templatePath,
64
+ substitute: false,
65
+ contentHash: hashContent(content),
66
+ ...extra
67
+ };
68
+ }
69
+ function loadCatalog(templateDir, version) {
70
+ const files = [];
71
+ for (const category of CATEGORIES) {
72
+ for (const rel of listFiles(templateDir, category.from)) {
73
+ const sub = rel.slice(category.from.length + 1);
74
+ files.push(
75
+ catalogFile(templateDir, rel, posix.join(category.to, sub), {
76
+ component: "skills",
77
+ category: category.id
78
+ })
79
+ );
80
+ }
81
+ }
82
+ for (const tree of TREE_COMPONENTS) {
83
+ for (const rel of listFiles(templateDir, tree.from)) {
84
+ const sub = rel.slice(tree.from.length + 1);
85
+ files.push(
86
+ catalogFile(templateDir, rel, posix.join(tree.to, sub), { component: tree.id })
87
+ );
88
+ }
89
+ }
90
+ for (const agent of AGENTS) {
91
+ for (const file of agent.files) {
92
+ files.push(
93
+ catalogFile(templateDir, file.from, file.to, {
94
+ component: "entry-files",
95
+ agent: agent.id,
96
+ substitute: file.substitute
97
+ })
98
+ );
99
+ }
100
+ }
101
+ return { version, files };
102
+ }
103
+
104
+ // src/catalog/catalog.ts
105
+ function resolveSelection(catalog, selection) {
106
+ return catalog.files.filter((f) => {
107
+ if (!selection.components.includes(f.component)) return false;
108
+ if (f.category !== void 0 && !selection.categories.includes(f.category)) return false;
109
+ if (f.agent !== void 0 && !selection.agents.includes(f.agent)) return false;
110
+ return true;
111
+ });
112
+ }
113
+
114
+ // src/planner/planner.ts
115
+ function planInstall(input) {
116
+ const files = resolveSelection(input.catalog, input.selection);
117
+ const byPath = new Map(
118
+ (input.manifest?.files ?? []).map((f) => [f.path, f])
119
+ );
120
+ const actions = files.map((f) => {
121
+ const tracked = byPath.get(f.targetPath);
122
+ return tracked ? { type: planTracked(f, tracked, input.onDisk.get(f.targetPath)), path: f.targetPath } : { type: planUntracked(f, input.onDisk.has(f.targetPath)), path: f.targetPath };
123
+ });
124
+ return { actions };
125
+ }
126
+ function planTracked(file, tracked, onDiskHash) {
127
+ if (tracked.status === "ejected") return "skip";
128
+ const edited = onDiskHash !== tracked.hash;
129
+ const hasNewVersion = file.contentHash !== tracked.hash;
130
+ if (!edited) return hasNewVersion ? "refresh" : "skip";
131
+ return hasNewVersion ? "conflict" : "skip";
132
+ }
133
+ function planUntracked(_file, onDisk) {
134
+ return onDisk ? "conflict" : "add";
135
+ }
136
+
137
+ // src/io/project.ts
138
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
139
+ import { join as join2 } from "path";
140
+ var MANIFEST_FILE = ".ai-kit.json";
141
+ function readManifest(projectDir) {
142
+ const path = join2(projectDir, MANIFEST_FILE);
143
+ if (!existsSync(path)) return null;
144
+ return parseManifest(readFileSync2(path, "utf8"));
145
+ }
146
+ function readOnDiskHashes(projectDir, files) {
147
+ const hashes = /* @__PURE__ */ new Map();
148
+ for (const file of files) {
149
+ const path = join2(projectDir, file.targetPath);
150
+ if (existsSync(path)) hashes.set(file.targetPath, hashContent(readFileSync2(path, "utf8")));
151
+ }
152
+ return hashes;
153
+ }
154
+
155
+ // src/applier/applier.ts
156
+ import { writeFileSync as writeFileSync2 } from "fs";
157
+ import { join as join4 } from "path";
158
+
159
+ // src/applier/write.ts
160
+ import { mkdirSync, readFileSync as readFileSync3, writeFileSync } from "fs";
161
+ import { dirname, join as join3 } from "path";
162
+ function writeTemplateFile(projectDir, templateDir, file, projectName, suffix = "") {
163
+ const raw = readFileSync3(join3(templateDir, file.templatePath), "utf8");
164
+ const content = file.substitute ? raw.replaceAll("{{PROJECT_NAME}}", projectName) : raw;
165
+ const targetPath = file.targetPath + suffix;
166
+ const dest = join3(projectDir, targetPath);
167
+ mkdirSync(dirname(dest), { recursive: true });
168
+ writeFileSync(dest, content);
169
+ return targetPath;
170
+ }
171
+
172
+ // src/applier/applier.ts
173
+ var ApplyConflictError = class extends Error {
174
+ constructor(paths) {
175
+ super(`Refusing to overwrite existing files:
176
+ ${paths.join("\n ")}`);
177
+ this.paths = paths;
178
+ this.name = "ApplyConflictError";
179
+ }
180
+ paths;
181
+ };
182
+ function applyPlan(input) {
183
+ const conflicts = input.plan.actions.filter((a) => a.type === "conflict").map((a) => a.path);
184
+ if (conflicts.length > 0) throw new ApplyConflictError(conflicts);
185
+ const byPath = new Map(input.files.map((f) => [f.targetPath, f]));
186
+ const written = [];
187
+ const entries = [];
188
+ for (const action of input.plan.actions) {
189
+ if (action.type !== "add" && action.type !== "refresh") continue;
190
+ const file = byPath.get(action.path);
191
+ if (!file) continue;
192
+ written.push(writeTemplateFile(input.projectDir, input.templateDir, file, input.projectName));
193
+ entries.push({
194
+ path: file.targetPath,
195
+ sourceVersion: input.version,
196
+ hash: file.contentHash,
197
+ status: "managed"
198
+ });
199
+ }
200
+ const manifest = {
201
+ version: input.version,
202
+ agents: input.selection.agents,
203
+ components: input.selection.components,
204
+ categories: input.selection.categories,
205
+ files: entries
206
+ };
207
+ writeFileSync2(join4(input.projectDir, MANIFEST_FILE), serializeManifest(manifest));
208
+ return { written, manifest };
209
+ }
210
+
211
+ // src/applier/links.ts
212
+ import { existsSync as existsSync2, lstatSync, mkdirSync as mkdirSync2, readlinkSync, symlinkSync } from "fs";
213
+ import { join as join5 } from "path";
214
+ var CLAUDE_SKILLS_LINK = ".claude/skills";
215
+ var LINK_TARGET = "../ai/skills";
216
+ function currentTarget(projectDir) {
217
+ const path = join5(projectDir, CLAUDE_SKILLS_LINK);
218
+ if (!existsSync2(path) && !isSymlink(path)) return null;
219
+ return isSymlink(path) ? readlinkSync(path) : "<not-a-symlink>";
220
+ }
221
+ function isSymlink(path) {
222
+ try {
223
+ return lstatSync(path).isSymbolicLink();
224
+ } catch {
225
+ return false;
226
+ }
227
+ }
228
+ function claudeSkillsLinkConflict(projectDir) {
229
+ const target = currentTarget(projectDir);
230
+ return target !== null && target !== LINK_TARGET;
231
+ }
232
+ function ensureClaudeSkillsLink(projectDir) {
233
+ if (currentTarget(projectDir) === LINK_TARGET) return "skipped";
234
+ mkdirSync2(join5(projectDir, ".claude"), { recursive: true });
235
+ try {
236
+ symlinkSync(LINK_TARGET, join5(projectDir, CLAUDE_SKILLS_LINK), "dir");
237
+ return "created";
238
+ } catch {
239
+ return "unsupported";
240
+ }
241
+ }
242
+
243
+ // src/paths.ts
244
+ import { readFileSync as readFileSync4 } from "fs";
245
+ import { dirname as dirname2, join as join6 } from "path";
246
+ import { fileURLToPath } from "url";
247
+ var packageRoot = join6(dirname2(fileURLToPath(import.meta.url)), "..");
248
+ var TEMPLATE_DIR = join6(packageRoot, "template");
249
+ function kitVersion() {
250
+ const pkg = JSON.parse(readFileSync4(join6(packageRoot, "package.json"), "utf8"));
251
+ return pkg.version;
252
+ }
253
+
254
+ // src/init.ts
255
+ function runInit(opts) {
256
+ const templateDir = opts.templateDir ?? TEMPLATE_DIR;
257
+ const version = opts.version ?? kitVersion();
258
+ const catalog = loadCatalog(templateDir, version);
259
+ const files = resolveSelection(catalog, opts.selection);
260
+ const plan = planInstall({
261
+ selection: opts.selection,
262
+ catalog,
263
+ onDisk: readOnDiskHashes(opts.projectDir, files),
264
+ manifest: readManifest(opts.projectDir)
265
+ });
266
+ const wantsSkillLink = opts.selection.agents.includes("claude-code") && files.some((f) => f.component === "skills");
267
+ if (wantsSkillLink && claudeSkillsLinkConflict(opts.projectDir)) {
268
+ throw new ApplyConflictError([CLAUDE_SKILLS_LINK]);
269
+ }
270
+ const result = applyPlan({
271
+ projectDir: opts.projectDir,
272
+ templateDir,
273
+ files,
274
+ plan,
275
+ selection: opts.selection,
276
+ version,
277
+ projectName: opts.projectName
278
+ });
279
+ if (wantsSkillLink && ensureClaudeSkillsLink(opts.projectDir) === "unsupported") {
280
+ console.warn(
281
+ `
282
+ Could not create the ${CLAUDE_SKILLS_LINK} symlink (your OS blocked it).
283
+ On Windows, enable Developer Mode (or run as admin) and re-run, or point Claude Code at ai/skills/ manually.`
284
+ );
285
+ }
286
+ return result;
287
+ }
288
+
289
+ // src/menu/prompt.ts
290
+ import { basename } from "path";
291
+ import { multiselect, text, isCancel, cancel } from "@clack/prompts";
292
+
293
+ // src/menu/menu.ts
294
+ function componentOptions() {
295
+ return COMPONENTS.map((c) => ({ value: c.id, label: c.label }));
296
+ }
297
+ function categoryOptions() {
298
+ return CATEGORIES.map((c) => ({ value: c.id, label: c.label }));
299
+ }
300
+ function agentOptions() {
301
+ return AGENTS.map((a) => ({ value: a.id, label: a.label }));
302
+ }
303
+ function buildSelection(answers) {
304
+ return {
305
+ components: answers.components,
306
+ categories: answers.components.includes("skills") ? answers.categories : [],
307
+ agents: answers.components.includes("entry-files") ? answers.agents : []
308
+ };
309
+ }
310
+
311
+ // src/menu/prompt.ts
312
+ function required(value) {
313
+ if (isCancel(value)) {
314
+ cancel("Cancelled \u2014 nothing written.");
315
+ process.exit(0);
316
+ }
317
+ return value;
318
+ }
319
+ async function promptForSelection(projectDir) {
320
+ const components = required(
321
+ await multiselect({
322
+ message: "Which parts do you want to install?",
323
+ options: componentOptions(),
324
+ required: true
325
+ })
326
+ );
327
+ let categories = [];
328
+ if (components.includes("skills")) {
329
+ categories = required(
330
+ await multiselect({
331
+ message: "Which skill categories?",
332
+ options: categoryOptions(),
333
+ required: true
334
+ })
335
+ );
336
+ }
337
+ let agents = [];
338
+ if (components.includes("entry-files")) {
339
+ agents = required(
340
+ await multiselect({
341
+ message: "Which coding agents?",
342
+ options: agentOptions(),
343
+ required: true
344
+ })
345
+ );
346
+ }
347
+ const projectName = required(
348
+ await text({
349
+ message: "Project name",
350
+ defaultValue: basename(projectDir),
351
+ placeholder: basename(projectDir)
352
+ })
353
+ );
354
+ return { selection: buildSelection({ components, categories, agents }), projectName };
355
+ }
356
+
357
+ // src/applier/update.ts
358
+ import { writeFileSync as writeFileSync3 } from "fs";
359
+ import { join as join7 } from "path";
360
+
361
+ // src/applier/resolve.ts
362
+ function conflictOps(choice) {
363
+ switch (choice) {
364
+ case "overwrite":
365
+ return { writeTarget: true, writeNew: false };
366
+ case "keep-mine":
367
+ return { writeTarget: false, writeNew: false };
368
+ case "keep-theirs":
369
+ return { writeTarget: false, writeNew: true };
370
+ }
371
+ }
372
+
373
+ // src/applier/update.ts
374
+ function installedSelection(manifest) {
375
+ return { components: manifest.components, categories: manifest.categories, agents: manifest.agents };
376
+ }
377
+ var union = (a, b) => [.../* @__PURE__ */ new Set([...a, ...b])];
378
+ function unionSelection(a, b) {
379
+ return {
380
+ components: union(a.components, b.components),
381
+ categories: union(a.categories, b.categories),
382
+ agents: union(a.agents, b.agents)
383
+ };
384
+ }
385
+ function applyTypesFor(mode) {
386
+ return mode === "add" ? /* @__PURE__ */ new Set(["add"]) : /* @__PURE__ */ new Set(["add", "refresh", "conflict"]);
387
+ }
388
+ function prepareReRun(input) {
389
+ const templateDir = input.templateDir ?? TEMPLATE_DIR;
390
+ const version = input.version ?? kitVersion();
391
+ const catalog = loadCatalog(templateDir, version);
392
+ const selection = unionSelection(installedSelection(input.manifest), input.addParts);
393
+ const files = resolveSelection(catalog, selection);
394
+ const plan = planInstall({
395
+ selection,
396
+ catalog,
397
+ onDisk: readOnDiskHashes(input.projectDir, files),
398
+ manifest: input.manifest
399
+ });
400
+ return { catalog, selection, files, plan, applyTypes: applyTypesFor(input.mode), version, templateDir };
401
+ }
402
+ var DEFAULT_APPLY = /* @__PURE__ */ new Set(["add", "refresh", "conflict"]);
403
+ function applyUpdate(input) {
404
+ const byPath = new Map(input.files.map((f) => [f.targetPath, f]));
405
+ const written = [];
406
+ const applyTypes = input.applyTypes ?? DEFAULT_APPLY;
407
+ const write = (file, suffix = "") => written.push(writeTemplateFile(input.projectDir, input.templateDir, file, input.projectName, suffix));
408
+ for (const action of input.plan.actions) {
409
+ if (!applyTypes.has(action.type)) continue;
410
+ const file = byPath.get(action.path);
411
+ if (!file) continue;
412
+ if (action.type === "add" || action.type === "refresh") {
413
+ write(file);
414
+ } else if (action.type === "conflict") {
415
+ const ops = conflictOps(input.resolutions.get(action.path) ?? "keep-mine");
416
+ if (ops.writeTarget) write(file);
417
+ if (ops.writeNew) write(file, ".new");
418
+ }
419
+ }
420
+ const priorByPath = new Map(input.priorManifest.files.map((f) => [f.path, f]));
421
+ const actionByPath = new Map(input.plan.actions.map((a) => [a.path, a.type]));
422
+ const entries = [];
423
+ for (const file of input.files) {
424
+ const prior = priorByPath.get(file.targetPath);
425
+ if (prior?.status === "ejected") {
426
+ entries.push(prior);
427
+ continue;
428
+ }
429
+ const type = actionByPath.get(file.targetPath);
430
+ const reconciled = type === "skip" || type !== void 0 && applyTypes.has(type);
431
+ if (reconciled) {
432
+ entries.push({ path: file.targetPath, sourceVersion: input.version, hash: file.contentHash, status: "managed" });
433
+ } else if (prior) {
434
+ entries.push(prior);
435
+ }
436
+ }
437
+ const manifest = {
438
+ version: input.version,
439
+ agents: input.selection.agents,
440
+ components: input.selection.components,
441
+ categories: input.selection.categories,
442
+ files: entries
443
+ };
444
+ writeFileSync3(join7(input.projectDir, MANIFEST_FILE), serializeManifest(manifest));
445
+ return { written, manifest };
446
+ }
447
+
448
+ // src/cli.ts
449
+ var EMPTY_SELECTION = { components: [], categories: [], agents: [] };
450
+ function fullSelection() {
451
+ return {
452
+ components: ["skills", ...TREE_COMPONENTS.map((t) => t.id), "entry-files"],
453
+ categories: CATEGORIES.map((c) => c.id),
454
+ agents: AGENTS.map((a) => a.id)
455
+ };
456
+ }
457
+ function parseValue(args, flag) {
458
+ const i = args.indexOf(flag);
459
+ return i === -1 ? void 0 : args[i + 1];
460
+ }
461
+ function parseList(args, flag) {
462
+ const value = parseValue(args, flag);
463
+ return value === void 0 ? void 0 : value.split(",").map((s) => s.trim()).filter(Boolean);
464
+ }
465
+ function selectionFromArgs(args) {
466
+ if (args.includes("--all")) return fullSelection();
467
+ const all = fullSelection();
468
+ return {
469
+ components: parseList(args, "--components") ?? all.components,
470
+ categories: parseList(args, "--categories") ?? all.categories,
471
+ agents: parseList(args, "--agents") ?? all.agents
472
+ };
473
+ }
474
+ var ORDER = ["conflict", "add", "refresh", "skip"];
475
+ var LABEL = {
476
+ add: "ADD",
477
+ refresh: "REFRESH",
478
+ conflict: "CONFLICT",
479
+ skip: "SKIP"
480
+ };
481
+ function formatPlan(plan) {
482
+ const lines = [];
483
+ for (const type of ORDER) {
484
+ const inGroup = plan.actions.filter((a) => a.type === type);
485
+ if (inGroup.length === 0) continue;
486
+ lines.push(`${LABEL[type]} (${inGroup.length})`);
487
+ for (const action of inGroup) lines.push(` ${action.path}`);
488
+ lines.push("");
489
+ }
490
+ const counts = ORDER.map((t) => `${plan.actions.filter((a) => a.type === t).length} ${t}`).join(" ");
491
+ lines.push(`${plan.actions.length} files: ${counts}`);
492
+ return lines.join("\n");
493
+ }
494
+ function printPlan(plan) {
495
+ console.log("\n" + formatPlan(plan));
496
+ }
497
+ function planFor(projectDir, selection) {
498
+ const catalog = loadCatalog(TEMPLATE_DIR, kitVersion());
499
+ const files = resolveSelection(catalog, selection);
500
+ const plan = planInstall({
501
+ selection,
502
+ catalog,
503
+ onDisk: readOnDiskHashes(projectDir, files),
504
+ manifest: readManifest(projectDir)
505
+ });
506
+ return { catalog, plan };
507
+ }
508
+ function reportConflict(error) {
509
+ if (!(error instanceof ApplyConflictError)) return false;
510
+ console.error("Refusing to overwrite existing files:");
511
+ for (const path of error.paths) console.error(` ${path}`);
512
+ console.error("\nMove or remove them, or select different parts.");
513
+ process.exitCode = 1;
514
+ return true;
515
+ }
516
+ function dryRun(args) {
517
+ const projectDir = process.cwd();
518
+ const selection = selectionFromArgs(args);
519
+ const { catalog, plan } = planFor(projectDir, selection);
520
+ console.log(`ai-engineering-kit ${catalog.version} \u2014 dry run for ${projectDir}`);
521
+ printPlan(plan);
522
+ }
523
+ function init(args) {
524
+ const projectDir = process.cwd();
525
+ const selection = selectionFromArgs(args);
526
+ const projectName = parseValue(args, "--name") ?? basename2(projectDir);
527
+ try {
528
+ const result = runInit({ projectDir, selection, projectName });
529
+ console.log(`ai-engineering-kit ${kitVersion()} installed into ${projectDir}`);
530
+ console.log(`${result.written.length} files written.`);
531
+ } catch (error) {
532
+ if (!reportConflict(error)) throw error;
533
+ }
534
+ }
535
+ function bail(value) {
536
+ if (isCancel2(value)) {
537
+ cancel2("Cancelled \u2014 nothing written.");
538
+ process.exit(0);
539
+ }
540
+ return value;
541
+ }
542
+ async function runReRun(projectDir) {
543
+ const manifest = readManifest(projectDir);
544
+ intro(`ai-engineering-kit ${kitVersion()} \u2014 already installed`);
545
+ const mode = bail(
546
+ await select({
547
+ message: "What would you like to do?",
548
+ options: [
549
+ { value: "update", label: "Update installed parts to latest" },
550
+ { value: "add", label: "Add more parts" },
551
+ { value: "both", label: "Both" }
552
+ ]
553
+ })
554
+ );
555
+ let addParts = EMPTY_SELECTION;
556
+ let projectName = basename2(projectDir);
557
+ if (mode === "add" || mode === "both") {
558
+ const picked = await promptForSelection(projectDir);
559
+ addParts = picked.selection;
560
+ projectName = picked.projectName;
561
+ }
562
+ const prepared = prepareReRun({ projectDir, manifest, mode, addParts });
563
+ const applicable = {
564
+ actions: prepared.plan.actions.filter((a) => prepared.applyTypes.has(a.type))
565
+ };
566
+ log.message(formatPlan(applicable));
567
+ const resolutions = /* @__PURE__ */ new Map();
568
+ if (prepared.applyTypes.has("conflict")) {
569
+ for (const action of applicable.actions.filter((a) => a.type === "conflict")) {
570
+ const choice = bail(
571
+ await select({
572
+ message: `You edited ${action.path}. A newer version exists.`,
573
+ options: [
574
+ { value: "keep-mine", label: "Keep mine" },
575
+ { value: "overwrite", label: "Overwrite with the new version" },
576
+ { value: "keep-theirs", label: "Keep mine, save theirs as .new" }
577
+ ]
578
+ })
579
+ );
580
+ resolutions.set(action.path, choice);
581
+ }
582
+ }
583
+ const pending = applicable.actions.filter((a) => a.type !== "skip").length;
584
+ const proceed = await confirm({ message: `Apply ${pending} changes in ${projectDir}?` });
585
+ if (isCancel2(proceed) || !proceed) {
586
+ outro("Nothing written.");
587
+ return;
588
+ }
589
+ const result = applyUpdate({
590
+ projectDir,
591
+ templateDir: prepared.templateDir,
592
+ files: prepared.files,
593
+ plan: prepared.plan,
594
+ resolutions,
595
+ priorManifest: manifest,
596
+ selection: prepared.selection,
597
+ version: prepared.version,
598
+ projectName,
599
+ applyTypes: prepared.applyTypes
600
+ });
601
+ outro(`Done. ${result.written.length} files written.`);
602
+ }
603
+ async function runInteractive() {
604
+ const projectDir = process.cwd();
605
+ if (readManifest(projectDir)) return runReRun(projectDir);
606
+ intro(`ai-engineering-kit ${kitVersion()}`);
607
+ const { selection, projectName } = await promptForSelection(projectDir);
608
+ const { plan } = planFor(projectDir, selection);
609
+ log.message(formatPlan(plan));
610
+ const pending = plan.actions.filter((a) => a.type !== "skip").length;
611
+ const proceed = await confirm({ message: `Scaffold ${pending} files into ${projectDir}?` });
612
+ if (isCancel2(proceed) || !proceed) {
613
+ outro("Nothing written.");
614
+ return;
615
+ }
616
+ try {
617
+ const result = runInit({ projectDir, selection, projectName });
618
+ outro(`Installed ${result.written.length} files into ${projectDir}.`);
619
+ } catch (error) {
620
+ if (!reportConflict(error)) throw error;
621
+ }
622
+ }
623
+ function performReRun(projectDir, mode, addParts) {
624
+ const manifest = readManifest(projectDir);
625
+ if (!manifest) {
626
+ console.error("Not an ai-engineering-kit project (no .ai-kit.json). Run `ai-engineering-kit init` first.");
627
+ process.exitCode = 1;
628
+ return;
629
+ }
630
+ const prepared = prepareReRun({ projectDir, manifest, mode, addParts });
631
+ const result = applyUpdate({
632
+ projectDir,
633
+ templateDir: prepared.templateDir,
634
+ files: prepared.files,
635
+ plan: prepared.plan,
636
+ resolutions: /* @__PURE__ */ new Map(),
637
+ // non-interactive: conflicts default to keep-mine
638
+ priorManifest: manifest,
639
+ selection: prepared.selection,
640
+ version: prepared.version,
641
+ projectName: basename2(projectDir),
642
+ applyTypes: prepared.applyTypes
643
+ });
644
+ console.log(`${result.written.length} files written. Edited files were kept (use the interactive flow to resolve).`);
645
+ }
646
+ function update() {
647
+ performReRun(process.cwd(), "update", EMPTY_SELECTION);
648
+ }
649
+ function add(args) {
650
+ const addParts = {
651
+ components: parseList(args, "--components") ?? [],
652
+ categories: parseList(args, "--categories") ?? [],
653
+ agents: parseList(args, "--agents") ?? []
654
+ };
655
+ performReRun(process.cwd(), "add", addParts);
656
+ }
657
+ async function main(argv) {
658
+ const args = argv.slice(2);
659
+ if (args.includes("--dry-run")) return dryRun(args);
660
+ if (args[0] === "init") return init(args.slice(1));
661
+ if (args[0] === "update") return update();
662
+ if (args[0] === "add") return add(args.slice(1));
663
+ if (args.length === 0) return runInteractive();
664
+ console.log("ai-engineering-kit");
665
+ console.log("\nUsage:");
666
+ console.log(" ai-engineering-kit # interactive install or update");
667
+ console.log(" ai-engineering-kit init [--name <project>] [--all | --components a,b --categories x --agents y]");
668
+ console.log(" ai-engineering-kit update # refresh installed parts to latest (keeps your edits)");
669
+ console.log(" ai-engineering-kit add --components a,b [--categories x] [--agents y]");
670
+ console.log(" ai-engineering-kit --dry-run [--all | --components a,b --categories x --agents y]");
671
+ }
672
+ main(process.argv).catch((error) => {
673
+ console.error(error);
674
+ process.exit(1);
675
+ });