@mozole/cli 1.4.0 → 1.4.2

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 (38) hide show
  1. package/CHANGELOG.md +18 -1
  2. package/README.md +7 -1
  3. package/dist/commands/adopt.d.ts +1 -1
  4. package/dist/commands/adopt.d.ts.map +1 -1
  5. package/dist/commands/adopt.js +312 -102
  6. package/dist/commands/adopt.js.map +1 -1
  7. package/dist/platform/process.d.ts.map +1 -1
  8. package/dist/platform/process.js +4 -0
  9. package/dist/platform/process.js.map +1 -1
  10. package/dist/scaffold/adoption.d.ts +17 -0
  11. package/dist/scaffold/adoption.d.ts.map +1 -0
  12. package/dist/scaffold/adoption.js +279 -0
  13. package/dist/scaffold/adoption.js.map +1 -0
  14. package/dist/scaffold/design.d.ts +4 -0
  15. package/dist/scaffold/design.d.ts.map +1 -0
  16. package/dist/scaffold/design.js +51 -0
  17. package/dist/scaffold/design.js.map +1 -0
  18. package/dist/scaffold/legacy.d.ts +4 -0
  19. package/dist/scaffold/legacy.d.ts.map +1 -0
  20. package/dist/scaffold/legacy.js +139 -0
  21. package/dist/scaffold/legacy.js.map +1 -0
  22. package/dist/scaffold/routes.d.ts +3 -0
  23. package/dist/scaffold/routes.d.ts.map +1 -0
  24. package/dist/scaffold/routes.js +37 -0
  25. package/dist/scaffold/routes.js.map +1 -0
  26. package/dist/scaffold/standalone.d.ts +2 -1
  27. package/dist/scaffold/standalone.d.ts.map +1 -1
  28. package/dist/scaffold/standalone.js +43 -19
  29. package/dist/scaffold/standalone.js.map +1 -1
  30. package/dist/scaffold/templates.d.ts.map +1 -1
  31. package/dist/scaffold/templates.js +26 -5
  32. package/dist/scaffold/templates.js.map +1 -1
  33. package/dist/version.d.ts +4 -2
  34. package/dist/version.d.ts.map +1 -1
  35. package/dist/version.js +3 -1
  36. package/dist/version.js.map +1 -1
  37. package/docs/agent-audit.md +2 -2
  38. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { access, copyFile, mkdir, readFile, rename, rm, writeFile, } from "node:fs/promises";
1
+ import { access, copyFile, lstat, mkdir, mkdtemp, readdir, readFile, rename, rm, rmdir, writeFile, } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { readConfig } from "../config/project.js";
4
4
  import { cceCommand, initializeCce, requireCceRuntime, } from "../context/cce.js";
@@ -6,6 +6,9 @@ import { assertAntigravityMcpMergeable, cceMcpServer, configureAntigravityMcp, m
6
6
  import { npmExecutable } from "../platform/executable.js";
7
7
  import { run } from "../platform/process.js";
8
8
  import { TaskProgress } from "../progress.js";
9
+ import { canonicalPlan, safeRead, } from "../scaffold/adoption.js";
10
+ import { detectRoutes } from "../scaffold/routes.js";
11
+ import { basicAgentGuide } from "../scaffold/standalone.js";
9
12
  import { projectConfig, projectTemplates } from "../scaffold/templates.js";
10
13
  import { writeFiles } from "../scaffold/write.js";
11
14
  import { TOOLCHAIN } from "../version.js";
@@ -28,7 +31,9 @@ async function restoreFiles(snapshots) {
28
31
  continue;
29
32
  }
30
33
  await mkdir(path.dirname(file), { recursive: true });
31
- await writeFiles(path.dirname(file), { [path.basename(file)]: contents.toString("utf8") }, true);
34
+ const temporary = `${file}.${process.pid}.tmp`;
35
+ await writeFile(temporary, contents);
36
+ await rename(temporary, file);
32
37
  }
33
38
  }
34
39
  function adoptionMutationFiles(root) {
@@ -40,6 +45,7 @@ function adoptionMutationFiles(root) {
40
45
  ".gitignore",
41
46
  "AGENTS.md",
42
47
  "CLAUDE.md",
48
+ "docs/design/README.md",
43
49
  ".mcp.json",
44
50
  ".context-engine.yaml",
45
51
  "vitest.config.ts",
@@ -75,39 +81,21 @@ async function atomicJson(file, value) {
75
81
  await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, "utf8");
76
82
  await rename(temporary, file);
77
83
  }
78
- export function detectRoutes(source) {
79
- const routes = ["/"];
80
- for (const match of source.matchAll(/route\(\s*["']([^"']+)["']/g))
81
- routes.push(`/${match[1]}`.replaceAll("//", "/"));
82
- return [...new Set(routes)];
83
- }
84
- async function ensureAgentBlock(file, block) {
85
- const begin = "<!-- mozole:begin -->";
86
- if (!(await exists(file))) {
87
- await writeFiles(path.dirname(file), { [path.basename(file)]: `${block.trim()}\n` }, true);
88
- return true;
89
- }
90
- const current = await readFile(file, "utf8");
91
- if (current.includes(begin)) {
92
- const end = "<!-- mozole:end -->";
93
- const start = current.indexOf(begin);
94
- const finish = current.indexOf(end, start);
95
- if (finish < 0)
96
- throw new Error(`Unclosed Mozole instruction block in ${file}`);
97
- const next = current.slice(0, start) +
98
- block.trim() +
99
- current.slice(finish + end.length);
100
- if (next === current)
101
- return false;
102
- await writeFiles(path.dirname(file), { [path.basename(file)]: next }, true);
103
- return true;
104
- }
105
- await writeFiles(path.dirname(file), { [path.basename(file)]: `${current.trimEnd()}\n\n${block.trim()}\n` }, true);
106
- return true;
107
- }
84
+ export { detectRoutes } from "../scaffold/routes.js";
108
85
  async function ensureGitignore(root, optimized = true) {
109
86
  const file = path.join(root, ".gitignore");
110
- const current = (await exists(file)) ? await readFile(file, "utf8") : "";
87
+ const original = (await exists(file)) ? await readFile(file, "utf8") : "";
88
+ const current = optimized
89
+ ? original
90
+ : original
91
+ .split(/\r?\n/)
92
+ .filter((line) => ![
93
+ ".cce/",
94
+ "/.cce/",
95
+ ".mozole/state/",
96
+ ".mozole/telemetry.json",
97
+ ].includes(line.trim()))
98
+ .join("\n");
111
99
  const required = [
112
100
  "dist/",
113
101
  "build/",
@@ -118,10 +106,10 @@ async function ensureGitignore(root, optimized = true) {
118
106
  ];
119
107
  const existing = new Set(current.split(/\r?\n/).map((line) => line.trim()));
120
108
  const missing = required.filter((line) => !existing.has(line));
121
- if (!missing.length)
109
+ if (!missing.length && current === original)
122
110
  return;
123
111
  await writeFiles(root, {
124
- ".gitignore": `${current.trimEnd()}${current.trim() ? "\n\n" : ""}# Mozole local files\n${missing.join("\n")}\n`,
112
+ ".gitignore": `${current.trimEnd()}${missing.length ? `${current.trim() ? "\n\n" : ""}# Mozole local files\n${missing.join("\n")}` : ""}\n`,
125
113
  }, true);
126
114
  }
127
115
  async function ensureBiomeConfig(root, defaultTemplate, optimized = true) {
@@ -147,7 +135,22 @@ async function ensureBiomeConfig(root, defaultTemplate, optimized = true) {
147
135
  const content = await readFile(file, "utf8");
148
136
  const config = JSON.parse(content);
149
137
  if (!config || typeof config !== "object")
150
- return;
138
+ throw new Error("biome.json must contain an object");
139
+ config.$schema = JSON.parse(defaultTemplate).$schema;
140
+ if (Array.isArray(config.files?.ignore)) {
141
+ config.files.includes = [
142
+ ...(config.files.includes ?? ["**"]),
143
+ ...config.files.ignore.map((pattern) => pattern.startsWith("!") ? pattern : `!${pattern}`),
144
+ ];
145
+ delete config.files.ignore;
146
+ }
147
+ if (!optimized && Array.isArray(config.files?.includes))
148
+ config.files.includes = config.files.includes.filter((pattern) => ![
149
+ "!.cce",
150
+ "!.cce/**",
151
+ "!.mozole/state",
152
+ "!.mozole/telemetry.json",
153
+ ].includes(pattern));
151
154
  if (!config.files || typeof config.files !== "object") {
152
155
  config.files = {
153
156
  includes: ["**", ...requiredIgnores],
@@ -161,34 +164,13 @@ async function ensureBiomeConfig(root, defaultTemplate, optimized = true) {
161
164
  }
162
165
  }
163
166
  }
164
- else if (Array.isArray(config.files.ignore)) {
165
- const cleanPatterns = [
166
- "build",
167
- "dist",
168
- "deploy",
169
- ".react-router",
170
- "node_modules",
171
- "coverage",
172
- ".mozole",
173
- ...(optimized ? [".cce"] : []),
174
- "public",
175
- "assets",
176
- "qa-screenshots",
177
- ];
178
- const existing = new Set(config.files.ignore);
179
- for (const pattern of cleanPatterns) {
180
- if (!existing.has(pattern) && !existing.has(`!${pattern}`)) {
181
- config.files.ignore.push(pattern);
182
- }
183
- }
184
- }
185
167
  else {
186
168
  config.files.includes = ["**", ...requiredIgnores];
187
169
  }
188
170
  await atomicJson(file, config);
189
171
  }
190
- catch {
191
- // If parsing fails, preserve existing file
172
+ catch (error) {
173
+ throw new Error(`Cannot migrate biome.json: ${String(error)}`);
192
174
  }
193
175
  }
194
176
  async function validateJsonObject(file) {
@@ -234,7 +216,7 @@ export async function adoptCommand(input, cwd, output = console, options = {}) {
234
216
  }
235
217
  const root = path.resolve(cwd, input);
236
218
  if (!options.cceInstall)
237
- return adoptPlain(root, output);
219
+ return adoptPlain(root, output, options);
238
220
  const packagePath = path.join(root, "package.json");
239
221
  const progress = new TaskProgress({
240
222
  total: 5,
@@ -248,9 +230,11 @@ export async function adoptCommand(input, cwd, output = console, options = {}) {
248
230
  process.once("SIGINT", abort);
249
231
  process.once("SIGTERM", abort);
250
232
  let snapshots;
233
+ let movedDirectories = [];
251
234
  try {
252
235
  progress.step("Analyzing project structure and package metadata");
253
236
  progress.logVerbose(`Reading package.json from ${packagePath}`);
237
+ await validateManagedPaths(root);
254
238
  const original = await readFile(packagePath, "utf8");
255
239
  const packageJson = JSON.parse(original);
256
240
  await assertAntigravityMcpMergeable(root);
@@ -262,7 +246,7 @@ export async function adoptCommand(input, cwd, output = console, options = {}) {
262
246
  (!allDependencies.vite && !allDependencies["@react-router/dev"]))
263
247
  throw new Error("Expected a React project using Vite or React Router Framework Mode.");
264
248
  const name = packageJson.name ?? path.basename(root);
265
- const templates = projectTemplates(name, { scaffold: false });
249
+ let templates = projectTemplates(name, { scaffold: false });
266
250
  const codexHooksPath = path.join(root, ".codex", "hooks.json");
267
251
  const claudeSettingsPath = path.join(root, ".claude", "settings.json");
268
252
  const hadCodexHooks = await validateJsonObject(codexHooksPath);
@@ -278,18 +262,41 @@ export async function adoptCommand(input, cwd, output = console, options = {}) {
278
262
  }
279
263
  })()
280
264
  : projectConfig(name, { scaffold: false });
265
+ const wasManaged = await exists(configPath);
266
+ if (!config.backend && (await exists(path.join(root, "api/index.php"))))
267
+ config.backend = projectConfig(name, { backend: true }).backend;
268
+ templates = projectTemplates(name, {
269
+ scaffold: false,
270
+ backend: Boolean(config.backend),
271
+ });
272
+ adaptFrameworkGuide(templates, Boolean(allDependencies["@react-router/dev"]));
273
+ const migration = await canonicalPlan(root, templates, true, wasManaged, name, knownGuides(name));
281
274
  config.name = name;
275
+ for (const key of Object.keys(config))
276
+ if (![
277
+ "schemaVersion",
278
+ "name",
279
+ "mode",
280
+ "framework",
281
+ "routes",
282
+ "backend",
283
+ "qa",
284
+ "cce",
285
+ "governor",
286
+ "telemetry",
287
+ "verifiers",
288
+ ].includes(key))
289
+ delete config[key];
282
290
  const optimized = projectConfig(name, { scaffold: false });
283
291
  config.mode = "optimized";
284
292
  if (!optimized.cce || !optimized.governor || !optimized.telemetry)
285
293
  throw new Error("Missing optimized defaults");
286
- config.cce ??= optimized.cce;
287
- config.governor ??= optimized.governor;
288
- config.telemetry ??= optimized.telemetry;
289
- if (config.governor?.enabled) {
290
- config.governor.mode = "enforce";
291
- config.governor.hooks = { codex: true, claude: true };
292
- }
294
+ config.cce = {
295
+ ...optimized.cce,
296
+ initialized: config.cce?.initialized ?? false,
297
+ };
298
+ config.governor = optimized.governor;
299
+ config.telemetry = optimized.telemetry;
293
300
  progress.step("Verifying CCE runtime availability");
294
301
  const cceRequired = process.env.MOZOLE_SKIP_CCE !== "1" && config.cce?.enabled;
295
302
  const cce = cceRequired
@@ -297,14 +304,25 @@ export async function adoptCommand(input, cwd, output = console, options = {}) {
297
304
  : await cceCommand();
298
305
  const cceLauncher = cce?.prefix.length ? "uvx" : "cce";
299
306
  progress.logVerbose(`CCE runtime: launcher=${cceLauncher}`);
300
- snapshots = await snapshotFiles(adoptionMutationFiles(root));
307
+ snapshots = await snapshotFiles([
308
+ ...new Set([
309
+ ...adoptionMutationFiles(root),
310
+ ...Object.keys(migration.writes).map((f) => path.join(root, f)),
311
+ ...migration.removes.map((f) => path.join(root, f)),
312
+ ]),
313
+ ]);
301
314
  const routeSourcePath = path.join(root, "app", "routes.ts");
302
315
  if (await exists(routeSourcePath))
303
316
  config.routes = detectRoutes(await readFile(routeSourcePath, "utf8"));
304
317
  const desiredScripts = {
305
318
  lint: "biome check .",
306
319
  format: "biome format --write .",
307
- typecheck: packageJson.scripts?.typecheck ?? "tsc --noEmit",
320
+ typecheck: !packageJson.scripts?.typecheck ||
321
+ packageJson.scripts.typecheck === "tsc --noEmit"
322
+ ? allDependencies["@react-router/dev"]
323
+ ? "react-router typegen && tsc --noEmit"
324
+ : "tsc --noEmit"
325
+ : packageJson.scripts.typecheck,
308
326
  "test:run": packageJson.scripts?.["test:run"] ?? "vitest run",
309
327
  verify: "mozole verify",
310
328
  "verify:force": "mozole verify --force",
@@ -324,6 +342,10 @@ export async function adoptCommand(input, cwd, output = console, options = {}) {
324
342
  "qa:interactive": "mozole qa --interactive",
325
343
  "qa:visual": "mozole qa --visual",
326
344
  };
345
+ next.scripts.typecheck = desiredScripts.typecheck;
346
+ if (config.backend)
347
+ for (const key of ["check:php", "build:deploy", "dev:api"])
348
+ next.scripts[key] = JSON.parse(required(templates, "package.json")).scripts[key];
327
349
  next.devDependencies = {
328
350
  "@mozole/cli": process.env.MOZOLE_CLI_SPEC ?? TOOLCHAIN.mozoleCli,
329
351
  "@playwright/test": packageJson.devDependencies?.["@playwright/test"] ??
@@ -342,6 +364,21 @@ export async function adoptCommand(input, cwd, output = console, options = {}) {
342
364
  vitest: packageJson.devDependencies?.vitest ?? TOOLCHAIN.vitest,
343
365
  ...packageJson.devDependencies,
344
366
  };
367
+ const generatedPackage = JSON.parse(required(templates, "package.json"));
368
+ for (const key of [
369
+ "@mozole/cli",
370
+ "@biomejs/biome",
371
+ "@testing-library/jest-dom",
372
+ "@testing-library/react",
373
+ "@testing-library/user-event",
374
+ "@vitejs/plugin-react",
375
+ "jsdom",
376
+ "typescript",
377
+ "vitest",
378
+ ])
379
+ next.devDependencies[key] = required(generatedPackage.devDependencies, key);
380
+ if (next.dependencies)
381
+ delete next.dependencies["@mozole/cli"];
345
382
  const nextText = `${JSON.stringify(next, null, 2)}\n`;
346
383
  const packageChanged = nextText !== original;
347
384
  if (packageChanged) {
@@ -361,6 +398,7 @@ export async function adoptCommand(input, cwd, output = console, options = {}) {
361
398
  await atomicJson(configPath, config);
362
399
  await writeFiles(root, {
363
400
  ".mozole/toolchain.json": template(".mozole/toolchain.json"),
401
+ "docs/design/README.md": template("docs/design/README.md"),
364
402
  ".context-engine.yaml": template(".context-engine.yaml"),
365
403
  "vitest.config.ts": template("vitest.config.ts"),
366
404
  "tests/setup.ts": template("tests/setup.ts"),
@@ -389,12 +427,10 @@ export async function adoptCommand(input, cwd, output = console, options = {}) {
389
427
  await mergeHookConfig(codexHooksPath, template(".codex/hooks.json"), path.join(backupDir, "codex-hooks.json.before-adopt"));
390
428
  if (hadClaudeSettings)
391
429
  await mergeHookConfig(claudeSettingsPath, template(".claude/settings.json"), path.join(backupDir, "claude-settings.json.before-adopt"));
392
- const block = "<!-- mozole:begin -->\n## Mandatory Mozole CCE contract\n\nRepository reads are forbidden by default. Allowed retrieval order: (1) CCE `context_search`; (2) additional CCE search, `expand_chunk`, or `related_context`; (3) bounded line-range read only when CCE output is insufficient; (4) full-file read only after explicit escalation. Editing a file is NOT sufficient justification for reading it in full. If CCE is unavailable, stop and report the blocker instead of falling back to repository reads. Routine browser validation uses screenshot-free `mozole qa`; visual QA requires an explicit user request or confirmed deterministic failure. Stop after requested work, `mozole verify`, and relevant routine QA pass.\n<!-- mozole:end -->";
393
- await ensureAgentBlock(path.join(root, "AGENTS.md"), block);
394
- await ensureAgentBlock(path.join(root, "CLAUDE.md"), `<!-- mozole:begin -->\n@AGENTS.md\n\n## Claude Code CCE contract\n\nRepository reads are forbidden by default. Use \`context_search\`, then additional CCE retrieval. Read may use a bounded line range only when CCE is insufficient; a full-file Read requires explicit escalation. Editing is not justification. If CCE is unavailable, report the blocker; do not fall back.\n<!-- mozole:end -->`);
395
430
  await ensureGitignore(root);
431
+ movedDirectories = await applyCanonicalPlan(root, migration, snapshots);
396
432
  progress.step("Installing missing dependencies & toolchain packages");
397
- if (packageChanged && process.env.MOZOLE_SKIP_INSTALL !== "1") {
433
+ if (process.env.MOZOLE_SKIP_INSTALL !== "1") {
398
434
  progress.logVerbose("Running npm install for adopted project");
399
435
  const installed = await run(npmExecutable(), ["install", "--no-fund", "--no-audit"], {
400
436
  cwd: root,
@@ -415,16 +451,6 @@ export async function adoptCommand(input, cwd, output = console, options = {}) {
415
451
  if (browser.exitCode !== 0)
416
452
  throw new Error("Chromium installation failed; package.json backup is in .mozole/backups/.");
417
453
  }
418
- try {
419
- progress.logVerbose("Formatting adopted project with Biome");
420
- await run(npmExecutable(), ["run", "format"], {
421
- cwd: root,
422
- timeoutMs: 60_000,
423
- signal: controller.signal,
424
- logFile: path.join(root, ".mozole", "logs", "biome-format.log"),
425
- });
426
- }
427
- catch { }
428
454
  }
429
455
  progress.step("Initializing CCE index and project configuration");
430
456
  if (cceRequired && !config.cce?.initialized) {
@@ -446,7 +472,7 @@ export async function adoptCommand(input, cwd, output = console, options = {}) {
446
472
  if (controller.signal.aborted)
447
473
  throw new Error("Adoption cancelled.");
448
474
  progress.done(`Adopted ${name}`);
449
- output.log(`ADOPT PASS\n\n${root}\nApplication code preserved.${packageChanged ? "\npackage.json backup: .mozole/backups/package.json.before-adopt" : "\nAlready up to date."}`);
475
+ output.log(`ADOPT PASS\n\n${root}\nApplication code preserved.${migrationSummary(migration)}`);
450
476
  return 0;
451
477
  }
452
478
  catch (error) {
@@ -454,6 +480,7 @@ export async function adoptCommand(input, cwd, output = console, options = {}) {
454
480
  let rollbackFailure;
455
481
  if (snapshots)
456
482
  try {
483
+ await restoreDirectories(movedDirectories);
457
484
  await restoreFiles(snapshots);
458
485
  }
459
486
  catch (rollbackError) {
@@ -471,9 +498,23 @@ export async function adoptCommand(input, cwd, output = console, options = {}) {
471
498
  }
472
499
  }
473
500
  /** Plain adoption never probes or installs agent runtimes. Existing user integrations are preserved. */
474
- async function adoptPlain(root, output) {
501
+ async function adoptPlain(root, output, options) {
475
502
  let snapshots;
503
+ let movedDirectories = [];
504
+ const controller = new AbortController();
505
+ const abort = () => controller.abort();
506
+ process.once("SIGINT", abort);
507
+ process.once("SIGTERM", abort);
508
+ const progress = new TaskProgress({
509
+ total: 3,
510
+ title: "Adopt",
511
+ output,
512
+ verbose: options.verbose,
513
+ onUpdate: options.onProgress,
514
+ });
476
515
  try {
516
+ progress.step("Inspecting project and planning canonical migration");
517
+ await validateManagedPaths(root);
477
518
  const packagePath = path.join(root, "package.json");
478
519
  const pkg = JSON.parse(await readFile(packagePath, "utf8"));
479
520
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
@@ -484,26 +525,45 @@ async function adoptPlain(root, output) {
484
525
  const config = (await exists(configPath))
485
526
  ? await readConfig(root)
486
527
  : projectConfig(name);
487
- if (config.cce?.enabled || config.governor?.enabled)
488
- throw new Error("This project already has explicitly enabled integrations. Use --cce-install to update them; plain adoption will not silently remove existing integrations.");
528
+ const wasManaged = await exists(configPath);
529
+ if (!config.backend && (await exists(path.join(root, "api/index.php"))))
530
+ config.backend = projectConfig(name, { backend: true }).backend;
489
531
  delete config.cce;
490
532
  delete config.governor;
491
533
  delete config.telemetry;
492
534
  config.mode = "scaffold";
493
535
  config.name = name;
536
+ for (const key of Object.keys(config))
537
+ if (![
538
+ "schemaVersion",
539
+ "name",
540
+ "mode",
541
+ "framework",
542
+ "routes",
543
+ "backend",
544
+ "qa",
545
+ "cce",
546
+ "governor",
547
+ "telemetry",
548
+ "verifiers",
549
+ ].includes(key))
550
+ delete config[key];
494
551
  const templates = projectTemplates(name, {
495
552
  backend: Boolean(config.backend?.enabled),
496
553
  });
554
+ adaptFrameworkGuide(templates, Boolean(deps["@react-router/dev"]));
555
+ const migration = await canonicalPlan(root, templates, false, wasManaged, name, knownGuides(name));
497
556
  const generated = JSON.parse(templates["package.json"] ?? "{}");
498
557
  const routeFile = path.join(root, "app", "routes.ts");
499
558
  if (await exists(routeFile))
500
559
  config.routes = detectRoutes(await readFile(routeFile, "utf8"));
501
- else
560
+ else if (!wasManaged)
502
561
  config.routes = ["/"];
503
562
  const files = {
504
563
  ".mozole/project.json": `${JSON.stringify(config, null, 2)}\n`,
505
564
  ".mozole/toolchain.json": required(templates, ".mozole/toolchain.json"),
506
565
  "scripts/qa.mjs": required(templates, "scripts/qa.mjs"),
566
+ "docs/design/README.md": required(templates, "docs/design/README.md"),
507
567
  "vitest.config.ts": required(templates, "vitest.config.ts"),
508
568
  "tests/setup.ts": required(templates, "tests/setup.ts"),
509
569
  ...(config.backend?.enabled
@@ -516,9 +576,12 @@ async function adoptPlain(root, output) {
516
576
  snapshots = await snapshotFiles([
517
577
  ...new Set([
518
578
  ...adoptionMutationFiles(root),
579
+ ...Object.keys(migration.writes).map((file) => path.join(root, file)),
580
+ ...migration.removes.map((file) => path.join(root, file)),
519
581
  ...Object.keys(files).map((file) => path.join(root, file)),
520
582
  ]),
521
583
  ]);
584
+ progress.step("Updating project scripts and managed files");
522
585
  const next = structuredClone(pkg);
523
586
  next.scripts = {
524
587
  lint: required(generated.scripts, "lint"),
@@ -527,6 +590,10 @@ async function adoptPlain(root, output) {
527
590
  "test:run": "vitest run",
528
591
  ...pkg.scripts,
529
592
  };
593
+ if (!pkg.scripts?.typecheck || pkg.scripts.typecheck === "tsc --noEmit")
594
+ next.scripts.typecheck = deps["@react-router/dev"]
595
+ ? "react-router typegen && tsc --noEmit"
596
+ : "tsc --noEmit";
530
597
  for (const key of [
531
598
  "verify",
532
599
  "verify:force",
@@ -538,7 +605,11 @@ async function adoptPlain(root, output) {
538
605
  next.scripts[key] = required(generated.scripts, key);
539
606
  if (config.backend?.enabled)
540
607
  for (const key of ["check:php", "build:deploy", "dev:api"])
541
- next.scripts[key] ??= required(generated.scripts, key);
608
+ next.scripts[key] = required(generated.scripts, key);
609
+ for (const [key, command] of Object.entries(next.scripts)) {
610
+ if (/^(?:npx(?: --no-install)? )?mozole\s/.test(command))
611
+ delete next.scripts[key];
612
+ }
542
613
  next.devDependencies = { ...pkg.devDependencies };
543
614
  delete next.devDependencies["@mozole/cli"];
544
615
  if (next.dependencies)
@@ -554,7 +625,7 @@ async function adoptPlain(root, output) {
554
625
  "vitest",
555
626
  "playwright",
556
627
  ])
557
- next.devDependencies[key] ??= required(generated.devDependencies, key);
628
+ next.devDependencies[key] = required(generated.devDependencies, key);
558
629
  const changed = JSON.stringify(next) !== JSON.stringify(pkg);
559
630
  if (changed) {
560
631
  await atomicJson(path.join(root, ".mozole", "backups", "package.json.before-adopt"), pkg);
@@ -564,13 +635,10 @@ async function adoptPlain(root, output) {
564
635
  await atomicJson(configPath, config);
565
636
  await atomicJson(path.join(root, ".mozole/toolchain.json"), JSON.parse(required(templates, ".mozole/toolchain.json")));
566
637
  await ensureBiomeConfig(root, required(templates, "biome.json"), false);
567
- let guide = required(templates, "AGENTS.md").trim();
568
- if (!deps["@react-router/dev"])
569
- guide = guide.replace("React Router and Vite source is in `app/`; routes are declared in `app/routes.ts`. Production HTML and assets are in `build/client/`.", "This is an existing React + Vite project. Preserve its source layout and routing conventions. The project's build script defines production output.");
570
- await ensureAgentBlock(path.join(root, "AGENTS.md"), `<!-- mozole:begin -->\n${guide}\n<!-- mozole:end -->`);
571
- await ensureAgentBlock(path.join(root, "CLAUDE.md"), "<!-- mozole:begin -->\n@AGENTS.md\n<!-- mozole:end -->");
572
638
  await ensureGitignore(root, false);
573
- if (changed && process.env.MOZOLE_SKIP_INSTALL !== "1") {
639
+ movedDirectories = await applyCanonicalPlan(root, migration, snapshots);
640
+ progress.step("Synchronizing installed dependencies and finalizing migration");
641
+ if (process.env.MOZOLE_SKIP_INSTALL !== "1") {
574
642
  for (const args of [
575
643
  ["install", "--no-fund", "--no-audit"],
576
644
  ...(process.env.MOZOLE_SKIP_BROWSER === "1"
@@ -580,20 +648,35 @@ async function adoptPlain(root, output) {
580
648
  const result = await run(npmExecutable(), args, {
581
649
  cwd: root,
582
650
  timeoutMs: 600_000,
651
+ signal: controller.signal,
583
652
  logFile: path.join(root, ".mozole", "logs", "adopt-install.log"),
584
653
  });
585
654
  if (result.exitCode !== 0)
586
655
  throw new Error("Dependency setup failed. See .mozole/logs/adopt-install.log");
587
656
  }
588
657
  }
589
- output.log(`ADOPT PASS\n\n${root}\nApplication code preserved.`);
658
+ controller.signal.throwIfAborted();
659
+ progress.done();
660
+ output.log(`ADOPT PASS\n\n${root}\nCurrent scaffold system installed. Application code preserved.${migrationSummary(migration)}`);
590
661
  return 0;
591
662
  }
592
663
  catch (error) {
593
- if (snapshots)
594
- await restoreFiles(snapshots);
595
- output.error(`ADOPT FAIL\n\n${error instanceof Error ? error.message : String(error)}\nManaged project files restored.`);
596
- return 1;
664
+ progress.done();
665
+ let rollbackFailure = "";
666
+ try {
667
+ await restoreDirectories(movedDirectories);
668
+ if (snapshots)
669
+ await restoreFiles(snapshots);
670
+ }
671
+ catch (failure) {
672
+ rollbackFailure = `\nRollback failed: ${String(failure)}`;
673
+ }
674
+ output.error(`ADOPT FAIL\n\n${error instanceof Error ? error.message : String(error)}${rollbackFailure || "\nManaged project files restored."}\nIf installation started, rerun npm install to reconcile node_modules.`);
675
+ return controller.signal.aborted ? 130 : 1;
676
+ }
677
+ finally {
678
+ process.removeListener("SIGINT", abort);
679
+ process.removeListener("SIGTERM", abort);
597
680
  }
598
681
  }
599
682
  function required(values, key) {
@@ -602,4 +685,131 @@ function required(values, key) {
602
685
  throw new Error(`Missing template value: ${key}`);
603
686
  return value;
604
687
  }
688
+ function knownGuides(name) {
689
+ const known = { "AGENTS.md": [], "CLAUDE.md": [] };
690
+ for (const scaffold of [true, false])
691
+ for (const backend of [true, false]) {
692
+ const files = projectTemplates(name, { scaffold, backend });
693
+ for (const file of Object.keys(known))
694
+ known[file]?.push(required(files, file));
695
+ }
696
+ for (const backend of [true, false])
697
+ known["AGENTS.md"]?.push(basicAgentGuide(backend).split("## Design implementation guardrails")[0] ?? "");
698
+ return known;
699
+ }
700
+ function adaptFrameworkGuide(templates, reactRouter) {
701
+ if (reactRouter)
702
+ return;
703
+ const replacement = "This is an existing React + Vite project. Preserve its source layout and routing conventions. The project's build script defines production output.";
704
+ templates["AGENTS.md"] = required(templates, "AGENTS.md")
705
+ .replace("React Router and Vite source is in `app/`; routes are declared in `app/routes.ts`. Production HTML and assets are in `build/client/`.", replacement)
706
+ .replace("React Router Framework Mode lives in `app/`; routes are in `app/routes.ts`; static output is `build/client/`.", replacement);
707
+ templates["docs/architecture.md"] = `# Architecture\n\n${replacement}\n`;
708
+ }
709
+ async function validateManagedPaths(root) {
710
+ for (const absolute of adoptionMutationFiles(root))
711
+ await safeRead(root, path.relative(root, absolute).replaceAll("\\", "/"));
712
+ for (const relative of [
713
+ ".codex/config.toml",
714
+ "scripts/qa.mjs",
715
+ "scripts/verify-extra.mjs",
716
+ "scripts/check-php.mjs",
717
+ "scripts/deploy.mjs",
718
+ "api/index.php",
719
+ "api/config.php",
720
+ ".htaccess",
721
+ "docs/architecture.md",
722
+ "docs/backend-php.md",
723
+ "docs/design-tokens.md",
724
+ ".mozole/telemetry.json",
725
+ ])
726
+ await safeRead(root, relative);
727
+ for (const relative of [".cce", ".mozole/state"]) {
728
+ try {
729
+ if ((await lstat(path.join(root, relative))).isSymbolicLink())
730
+ throw new Error(`Cannot adopt symlinked managed path: ${relative}`);
731
+ }
732
+ catch (error) {
733
+ if (error.code !== "ENOENT")
734
+ throw error;
735
+ }
736
+ }
737
+ }
738
+ async function restoreDirectories(moved) {
739
+ for (const { from, to } of [...moved].reverse()) {
740
+ await mkdir(path.dirname(from), { recursive: true });
741
+ await rm(from, { recursive: true, force: true });
742
+ await rename(to, from);
743
+ }
744
+ }
745
+ async function applyCanonicalPlan(root, plan, snapshots) {
746
+ const moved = [];
747
+ let backup;
748
+ async function backupDirectory() {
749
+ if (!backup) {
750
+ const parent = path.join(root, ".mozole/backups");
751
+ await mkdir(parent, { recursive: true });
752
+ backup = await mkdtemp(path.join(parent, "adopt-"));
753
+ plan.backup = backup;
754
+ }
755
+ return backup;
756
+ }
757
+ try {
758
+ for (const snapshot of snapshots) {
759
+ if (!snapshot.contents ||
760
+ snapshot.file.includes(`${path.sep}backups${path.sep}`))
761
+ continue;
762
+ const relative = path.relative(root, snapshot.file).replaceAll("\\", "/");
763
+ const desired = plan.removes.includes(relative)
764
+ ? undefined
765
+ : (plan.writes[relative] ?? (await safeRead(root, relative)));
766
+ if (desired !== undefined &&
767
+ snapshot.contents.equals(Buffer.from(desired)))
768
+ continue;
769
+ const file = path.join(await backupDirectory(), relative);
770
+ await mkdir(path.dirname(file), { recursive: true });
771
+ const temporary = `${file}.${process.pid}.tmp`;
772
+ await writeFile(temporary, snapshot.contents);
773
+ await rename(temporary, file);
774
+ }
775
+ await writeFiles(root, plan.writes, true);
776
+ for (const relative of plan.removes)
777
+ await rm(path.join(root, relative), { force: true });
778
+ for (const relative of plan.directories) {
779
+ const from = path.join(root, relative);
780
+ if (!(await exists(from)))
781
+ continue;
782
+ const to = path.join(await backupDirectory(), relative);
783
+ await mkdir(path.dirname(to), { recursive: true });
784
+ await rename(from, to);
785
+ moved.push({ from, to });
786
+ }
787
+ // Remove only empty integration directories; user-owned files prevent removal.
788
+ for (const relative of [
789
+ ".agents/workflows",
790
+ ".agents/rules",
791
+ ".agents",
792
+ ".codex",
793
+ ".claude",
794
+ ]) {
795
+ const directory = path.join(root, relative);
796
+ try {
797
+ if (!(await readdir(directory)).length)
798
+ await rmdir(directory);
799
+ }
800
+ catch (error) {
801
+ if (!["ENOENT", "ENOTEMPTY"].includes(error.code ?? ""))
802
+ throw error;
803
+ }
804
+ }
805
+ return moved;
806
+ }
807
+ catch (error) {
808
+ await restoreDirectories(moved);
809
+ throw error;
810
+ }
811
+ }
812
+ function migrationSummary(plan) {
813
+ return `${plan.backup ? `\nMigration backup: ${plan.backup}` : ""}${plan.notes.length ? `\n${plan.notes.join("\n")}` : ""}`;
814
+ }
605
815
  //# sourceMappingURL=adopt.js.map