@mozole/cli 1.3.2 → 1.4.1

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 (76) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +54 -156
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +6 -3
  5. package/dist/cli.js.map +1 -1
  6. package/dist/commands/adopt.d.ts +2 -1
  7. package/dist/commands/adopt.d.ts.map +1 -1
  8. package/dist/commands/adopt.js +460 -81
  9. package/dist/commands/adopt.js.map +1 -1
  10. package/dist/commands/doctor.d.ts.map +1 -1
  11. package/dist/commands/doctor.js +39 -16
  12. package/dist/commands/doctor.js.map +1 -1
  13. package/dist/commands/new.d.ts.map +1 -1
  14. package/dist/commands/new.js +22 -11
  15. package/dist/commands/new.js.map +1 -1
  16. package/dist/commands/qa.d.ts.map +1 -1
  17. package/dist/commands/qa.js +18 -12
  18. package/dist/commands/qa.js.map +1 -1
  19. package/dist/commands/stats.d.ts.map +1 -1
  20. package/dist/commands/stats.js +6 -1
  21. package/dist/commands/stats.js.map +1 -1
  22. package/dist/commands/verify.d.ts.map +1 -1
  23. package/dist/commands/verify.js +12 -8
  24. package/dist/commands/verify.js.map +1 -1
  25. package/dist/config/project.d.ts +3 -3
  26. package/dist/config/project.d.ts.map +1 -1
  27. package/dist/config/project.js +11 -12
  28. package/dist/config/project.js.map +1 -1
  29. package/dist/governor/fingerprint.d.ts.map +1 -1
  30. package/dist/governor/fingerprint.js +2 -0
  31. package/dist/governor/fingerprint.js.map +1 -1
  32. package/dist/governor/hook.d.ts +1 -0
  33. package/dist/governor/hook.d.ts.map +1 -1
  34. package/dist/governor/hook.js +18 -9
  35. package/dist/governor/hook.js.map +1 -1
  36. package/dist/platform/process.d.ts.map +1 -1
  37. package/dist/platform/process.js +4 -0
  38. package/dist/platform/process.js.map +1 -1
  39. package/dist/qa/runner.js +1 -1
  40. package/dist/qa/runner.js.map +1 -1
  41. package/dist/scaffold/adoption.d.ts +17 -0
  42. package/dist/scaffold/adoption.d.ts.map +1 -0
  43. package/dist/scaffold/adoption.js +279 -0
  44. package/dist/scaffold/adoption.js.map +1 -0
  45. package/dist/scaffold/backend.d.ts.map +1 -1
  46. package/dist/scaffold/backend.js +57 -11
  47. package/dist/scaffold/backend.js.map +1 -1
  48. package/dist/scaffold/design.d.ts +4 -0
  49. package/dist/scaffold/design.d.ts.map +1 -0
  50. package/dist/scaffold/design.js +50 -0
  51. package/dist/scaffold/design.js.map +1 -0
  52. package/dist/scaffold/legacy.d.ts +4 -0
  53. package/dist/scaffold/legacy.d.ts.map +1 -0
  54. package/dist/scaffold/legacy.js +139 -0
  55. package/dist/scaffold/legacy.js.map +1 -0
  56. package/dist/scaffold/routes.d.ts +3 -0
  57. package/dist/scaffold/routes.d.ts.map +1 -0
  58. package/dist/scaffold/routes.js +37 -0
  59. package/dist/scaffold/routes.js.map +1 -0
  60. package/dist/scaffold/standalone.d.ts +7 -0
  61. package/dist/scaffold/standalone.d.ts.map +1 -0
  62. package/dist/scaffold/standalone.js +294 -0
  63. package/dist/scaffold/standalone.js.map +1 -0
  64. package/dist/scaffold/templates.d.ts.map +1 -1
  65. package/dist/scaffold/templates.js +82 -26
  66. package/dist/scaffold/templates.js.map +1 -1
  67. package/dist/scaffold/write.d.ts.map +1 -1
  68. package/dist/scaffold/write.js +19 -5
  69. package/dist/scaffold/write.js.map +1 -1
  70. package/dist/verify/runner.d.ts.map +1 -1
  71. package/dist/verify/runner.js +2 -4
  72. package/dist/verify/runner.js.map +1 -1
  73. package/dist/version.d.ts +2 -2
  74. package/dist/version.js +1 -1
  75. package/docs/agent-audit.md +24 -0
  76. package/package.json +4 -2
@@ -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 writeFile(file, contents);
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",
@@ -70,56 +76,53 @@ async function exists(file) {
70
76
  }
71
77
  }
72
78
  async function atomicJson(file, value) {
79
+ await mkdir(path.dirname(file), { recursive: true });
73
80
  const temporary = `${file}.${process.pid}.tmp`;
74
81
  await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, "utf8");
75
82
  await rename(temporary, file);
76
83
  }
77
- export function detectRoutes(source) {
78
- const routes = ["/"];
79
- for (const match of source.matchAll(/route\(\s*["']([^"']+)["']/g))
80
- routes.push(`/${match[1]}`.replaceAll("//", "/"));
81
- return [...new Set(routes)];
82
- }
83
- async function ensureAgentBlock(file, block) {
84
- const begin = "<!-- mozole:begin -->";
85
- if (!(await exists(file))) {
86
- await writeFile(file, `${block.trim()}\n`, "utf8");
87
- return true;
88
- }
89
- const current = await readFile(file, "utf8");
90
- if (current.includes(begin))
91
- return false;
92
- await writeFile(file, `${current.trimEnd()}\n\n${block.trim()}\n`, "utf8");
93
- return true;
94
- }
95
- async function ensureGitignore(root) {
84
+ export { detectRoutes } from "../scaffold/routes.js";
85
+ async function ensureGitignore(root, optimized = true) {
96
86
  const file = path.join(root, ".gitignore");
97
- 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");
98
99
  const required = [
99
100
  "dist/",
100
101
  "build/",
102
+ "deploy/",
101
103
  ".mozole/logs/",
102
104
  ".mozole/qa/",
103
- ".mozole/state/",
104
- ".mozole/telemetry.json",
105
- ".cce/",
105
+ ...(optimized ? [".mozole/state/", ".mozole/telemetry.json", ".cce/"] : []),
106
106
  ];
107
107
  const existing = new Set(current.split(/\r?\n/).map((line) => line.trim()));
108
108
  const missing = required.filter((line) => !existing.has(line));
109
- if (!missing.length)
109
+ if (!missing.length && current === original)
110
110
  return;
111
- await writeFile(file, `${current.trimEnd()}${current.trim() ? "\n\n" : ""}# Mozole local state\n${missing.join("\n")}\n`, "utf8");
111
+ await writeFiles(root, {
112
+ ".gitignore": `${current.trimEnd()}${missing.length ? `${current.trim() ? "\n\n" : ""}# Mozole local files\n${missing.join("\n")}` : ""}\n`,
113
+ }, true);
112
114
  }
113
- async function ensureBiomeConfig(root, defaultTemplate) {
115
+ async function ensureBiomeConfig(root, defaultTemplate, optimized = true) {
114
116
  const file = path.join(root, "biome.json");
115
117
  const requiredIgnores = [
116
118
  "!build",
117
119
  "!dist",
120
+ "!deploy",
118
121
  "!.react-router",
119
122
  "!node_modules",
120
123
  "!coverage",
121
124
  "!.mozole",
122
- "!.cce",
125
+ ...(optimized ? ["!.cce"] : []),
123
126
  "!public",
124
127
  "!assets",
125
128
  "!qa-screenshots",
@@ -132,7 +135,22 @@ async function ensureBiomeConfig(root, defaultTemplate) {
132
135
  const content = await readFile(file, "utf8");
133
136
  const config = JSON.parse(content);
134
137
  if (!config || typeof config !== "object")
135
- 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));
136
154
  if (!config.files || typeof config.files !== "object") {
137
155
  config.files = {
138
156
  includes: ["**", ...requiredIgnores],
@@ -146,33 +164,13 @@ async function ensureBiomeConfig(root, defaultTemplate) {
146
164
  }
147
165
  }
148
166
  }
149
- else if (Array.isArray(config.files.ignore)) {
150
- const cleanPatterns = [
151
- "build",
152
- "dist",
153
- ".react-router",
154
- "node_modules",
155
- "coverage",
156
- ".mozole",
157
- ".cce",
158
- "public",
159
- "assets",
160
- "qa-screenshots",
161
- ];
162
- const existing = new Set(config.files.ignore);
163
- for (const pattern of cleanPatterns) {
164
- if (!existing.has(pattern) && !existing.has(`!${pattern}`)) {
165
- config.files.ignore.push(pattern);
166
- }
167
- }
168
- }
169
167
  else {
170
168
  config.files.includes = ["**", ...requiredIgnores];
171
169
  }
172
170
  await atomicJson(file, config);
173
171
  }
174
- catch {
175
- // If parsing fails, preserve existing file
172
+ catch (error) {
173
+ throw new Error(`Cannot migrate biome.json: ${String(error)}`);
176
174
  }
177
175
  }
178
176
  async function validateJsonObject(file) {
@@ -185,16 +183,28 @@ async function validateJsonObject(file) {
185
183
  }
186
184
  async function mergeHookConfig(file, template, backupFile) {
187
185
  const currentText = await readFile(file, "utf8");
188
- if (currentText.includes("mozole governor hook"))
189
- return;
190
186
  const current = JSON.parse(currentText);
191
187
  const desired = JSON.parse(template);
192
188
  const hooks = current.hooks && typeof current.hooks === "object"
193
189
  ? current.hooks
194
190
  : {};
195
191
  current.hooks = hooks;
196
- const existing = Array.isArray(hooks.PreToolUse) ? hooks.PreToolUse : [];
197
- hooks.PreToolUse = [...existing, ...desired.hooks.PreToolUse];
192
+ for (const [event, groups] of Object.entries(desired.hooks)) {
193
+ const existing = Array.isArray(hooks[event])
194
+ ? hooks[event]
195
+ : [];
196
+ const preserved = existing.flatMap((group) => {
197
+ if (!Array.isArray(group.hooks))
198
+ return [group];
199
+ const remaining = group.hooks.filter((handler) => !JSON.stringify(handler).includes("mozole governor hook"));
200
+ return remaining.length || !group.hooks.length
201
+ ? [{ ...group, hooks: remaining }]
202
+ : [];
203
+ });
204
+ hooks[event] = [...preserved, ...groups];
205
+ }
206
+ if (JSON.stringify(current) === JSON.stringify(JSON.parse(currentText)))
207
+ return;
198
208
  await mkdir(path.dirname(backupFile), { recursive: true });
199
209
  await copyFile(file, backupFile);
200
210
  await atomicJson(file, current);
@@ -205,6 +215,8 @@ export async function adoptCommand(input, cwd, output = console, options = {}) {
205
215
  return 2;
206
216
  }
207
217
  const root = path.resolve(cwd, input);
218
+ if (!options.cceInstall)
219
+ return adoptPlain(root, output, options);
208
220
  const packagePath = path.join(root, "package.json");
209
221
  const progress = new TaskProgress({
210
222
  total: 5,
@@ -218,9 +230,11 @@ export async function adoptCommand(input, cwd, output = console, options = {}) {
218
230
  process.once("SIGINT", abort);
219
231
  process.once("SIGTERM", abort);
220
232
  let snapshots;
233
+ let movedDirectories = [];
221
234
  try {
222
235
  progress.step("Analyzing project structure and package metadata");
223
236
  progress.logVerbose(`Reading package.json from ${packagePath}`);
237
+ await validateManagedPaths(root);
224
238
  const original = await readFile(packagePath, "utf8");
225
239
  const packageJson = JSON.parse(original);
226
240
  await assertAntigravityMcpMergeable(root);
@@ -232,7 +246,7 @@ export async function adoptCommand(input, cwd, output = console, options = {}) {
232
246
  (!allDependencies.vite && !allDependencies["@react-router/dev"]))
233
247
  throw new Error("Expected a React project using Vite or React Router Framework Mode.");
234
248
  const name = packageJson.name ?? path.basename(root);
235
- const templates = projectTemplates(name);
249
+ let templates = projectTemplates(name, { scaffold: false });
236
250
  const codexHooksPath = path.join(root, ".codex", "hooks.json");
237
251
  const claudeSettingsPath = path.join(root, ".claude", "settings.json");
238
252
  const hadCodexHooks = await validateJsonObject(codexHooksPath);
@@ -247,27 +261,68 @@ export async function adoptCommand(input, cwd, output = console, options = {}) {
247
261
  throw new Error(`Existing .mozole/project.json is invalid: ${error instanceof Error ? error.message : String(error)}`);
248
262
  }
249
263
  })()
250
- : projectConfig(name);
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));
251
274
  config.name = name;
252
- if (config.governor.enabled) {
253
- config.governor.mode = "enforce";
254
- config.governor.hooks = { codex: true, claude: true };
255
- }
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];
290
+ const optimized = projectConfig(name, { scaffold: false });
291
+ config.mode = "optimized";
292
+ if (!optimized.cce || !optimized.governor || !optimized.telemetry)
293
+ throw new Error("Missing optimized defaults");
294
+ config.cce = {
295
+ ...optimized.cce,
296
+ initialized: config.cce?.initialized ?? false,
297
+ };
298
+ config.governor = optimized.governor;
299
+ config.telemetry = optimized.telemetry;
256
300
  progress.step("Verifying CCE runtime availability");
257
- const cceRequired = process.env.MOZOLE_SKIP_CCE !== "1" && config.cce.enabled;
301
+ const cceRequired = process.env.MOZOLE_SKIP_CCE !== "1" && config.cce?.enabled;
258
302
  const cce = cceRequired
259
303
  ? await requireCceRuntime(root, controller.signal)
260
304
  : await cceCommand();
261
305
  const cceLauncher = cce?.prefix.length ? "uvx" : "cce";
262
306
  progress.logVerbose(`CCE runtime: launcher=${cceLauncher}`);
263
- 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
+ ]);
264
314
  const routeSourcePath = path.join(root, "app", "routes.ts");
265
315
  if (await exists(routeSourcePath))
266
316
  config.routes = detectRoutes(await readFile(routeSourcePath, "utf8"));
267
317
  const desiredScripts = {
268
318
  lint: "biome check .",
269
319
  format: "biome format --write .",
270
- 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,
271
326
  "test:run": packageJson.scripts?.["test:run"] ?? "vitest run",
272
327
  verify: "mozole verify",
273
328
  "verify:force": "mozole verify --force",
@@ -287,6 +342,10 @@ export async function adoptCommand(input, cwd, output = console, options = {}) {
287
342
  "qa:interactive": "mozole qa --interactive",
288
343
  "qa:visual": "mozole qa --visual",
289
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];
290
349
  next.devDependencies = {
291
350
  "@mozole/cli": process.env.MOZOLE_CLI_SPEC ?? TOOLCHAIN.mozoleCli,
292
351
  "@playwright/test": packageJson.devDependencies?.["@playwright/test"] ??
@@ -305,6 +364,21 @@ export async function adoptCommand(input, cwd, output = console, options = {}) {
305
364
  vitest: packageJson.devDependencies?.vitest ?? TOOLCHAIN.vitest,
306
365
  ...packageJson.devDependencies,
307
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"];
308
382
  const nextText = `${JSON.stringify(next, null, 2)}\n`;
309
383
  const packageChanged = nextText !== original;
310
384
  if (packageChanged) {
@@ -324,6 +398,7 @@ export async function adoptCommand(input, cwd, output = console, options = {}) {
324
398
  await atomicJson(configPath, config);
325
399
  await writeFiles(root, {
326
400
  ".mozole/toolchain.json": template(".mozole/toolchain.json"),
401
+ "docs/design/README.md": template("docs/design/README.md"),
327
402
  ".context-engine.yaml": template(".context-engine.yaml"),
328
403
  "vitest.config.ts": template("vitest.config.ts"),
329
404
  "tests/setup.ts": template("tests/setup.ts"),
@@ -352,12 +427,10 @@ export async function adoptCommand(input, cwd, output = console, options = {}) {
352
427
  await mergeHookConfig(codexHooksPath, template(".codex/hooks.json"), path.join(backupDir, "codex-hooks.json.before-adopt"));
353
428
  if (hadClaudeSettings)
354
429
  await mergeHookConfig(claudeSettingsPath, template(".claude/settings.json"), path.join(backupDir, "claude-settings.json.before-adopt"));
355
- 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 -->";
356
- await ensureAgentBlock(path.join(root, "AGENTS.md"), block);
357
- 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 -->`);
358
430
  await ensureGitignore(root);
431
+ movedDirectories = await applyCanonicalPlan(root, migration, snapshots);
359
432
  progress.step("Installing missing dependencies & toolchain packages");
360
- if (packageChanged && process.env.MOZOLE_SKIP_INSTALL !== "1") {
433
+ if (process.env.MOZOLE_SKIP_INSTALL !== "1") {
361
434
  progress.logVerbose("Running npm install for adopted project");
362
435
  const installed = await run(npmExecutable(), ["install", "--no-fund", "--no-audit"], {
363
436
  cwd: root,
@@ -378,19 +451,9 @@ export async function adoptCommand(input, cwd, output = console, options = {}) {
378
451
  if (browser.exitCode !== 0)
379
452
  throw new Error("Chromium installation failed; package.json backup is in .mozole/backups/.");
380
453
  }
381
- try {
382
- progress.logVerbose("Formatting adopted project with Biome");
383
- await run(npmExecutable(), ["run", "format"], {
384
- cwd: root,
385
- timeoutMs: 60_000,
386
- signal: controller.signal,
387
- logFile: path.join(root, ".mozole", "logs", "biome-format.log"),
388
- });
389
- }
390
- catch { }
391
454
  }
392
455
  progress.step("Initializing CCE index and project configuration");
393
- if (cceRequired && !config.cce.initialized) {
456
+ if (cceRequired && !config.cce?.initialized) {
394
457
  progress.logVerbose("Initializing CCE for adopted project");
395
458
  const initialized = await initializeCce(root, controller.signal, cce);
396
459
  if (initialized?.exitCode !== 0) {
@@ -409,7 +472,7 @@ export async function adoptCommand(input, cwd, output = console, options = {}) {
409
472
  if (controller.signal.aborted)
410
473
  throw new Error("Adoption cancelled.");
411
474
  progress.done(`Adopted ${name}`);
412
- 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)}`);
413
476
  return 0;
414
477
  }
415
478
  catch (error) {
@@ -417,6 +480,7 @@ export async function adoptCommand(input, cwd, output = console, options = {}) {
417
480
  let rollbackFailure;
418
481
  if (snapshots)
419
482
  try {
483
+ await restoreDirectories(movedDirectories);
420
484
  await restoreFiles(snapshots);
421
485
  }
422
486
  catch (rollbackError) {
@@ -433,4 +497,319 @@ export async function adoptCommand(input, cwd, output = console, options = {}) {
433
497
  process.removeListener("SIGTERM", abort);
434
498
  }
435
499
  }
500
+ /** Plain adoption never probes or installs agent runtimes. Existing user integrations are preserved. */
501
+ async function adoptPlain(root, output, options) {
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
+ });
515
+ try {
516
+ progress.step("Inspecting project and planning canonical migration");
517
+ await validateManagedPaths(root);
518
+ const packagePath = path.join(root, "package.json");
519
+ const pkg = JSON.parse(await readFile(packagePath, "utf8"));
520
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
521
+ if (!deps.react || (!deps.vite && !deps["@react-router/dev"]))
522
+ throw new Error("Expected a React project using Vite or React Router Framework Mode.");
523
+ const name = pkg.name ?? path.basename(root);
524
+ const configPath = path.join(root, ".mozole", "project.json");
525
+ const config = (await exists(configPath))
526
+ ? await readConfig(root)
527
+ : projectConfig(name);
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;
531
+ delete config.cce;
532
+ delete config.governor;
533
+ delete config.telemetry;
534
+ config.mode = "scaffold";
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];
551
+ const templates = projectTemplates(name, {
552
+ backend: Boolean(config.backend?.enabled),
553
+ });
554
+ adaptFrameworkGuide(templates, Boolean(deps["@react-router/dev"]));
555
+ const migration = await canonicalPlan(root, templates, false, wasManaged, name, knownGuides(name));
556
+ const generated = JSON.parse(templates["package.json"] ?? "{}");
557
+ const routeFile = path.join(root, "app", "routes.ts");
558
+ if (await exists(routeFile))
559
+ config.routes = detectRoutes(await readFile(routeFile, "utf8"));
560
+ else if (!wasManaged)
561
+ config.routes = ["/"];
562
+ const files = {
563
+ ".mozole/project.json": `${JSON.stringify(config, null, 2)}\n`,
564
+ ".mozole/toolchain.json": required(templates, ".mozole/toolchain.json"),
565
+ "scripts/qa.mjs": required(templates, "scripts/qa.mjs"),
566
+ "docs/design/README.md": required(templates, "docs/design/README.md"),
567
+ "vitest.config.ts": required(templates, "vitest.config.ts"),
568
+ "tests/setup.ts": required(templates, "tests/setup.ts"),
569
+ ...(config.backend?.enabled
570
+ ? {
571
+ "scripts/check-php.mjs": required(templates, "scripts/check-php.mjs"),
572
+ "scripts/deploy.mjs": required(templates, "scripts/deploy.mjs"),
573
+ }
574
+ : {}),
575
+ };
576
+ snapshots = await snapshotFiles([
577
+ ...new Set([
578
+ ...adoptionMutationFiles(root),
579
+ ...Object.keys(migration.writes).map((file) => path.join(root, file)),
580
+ ...migration.removes.map((file) => path.join(root, file)),
581
+ ...Object.keys(files).map((file) => path.join(root, file)),
582
+ ]),
583
+ ]);
584
+ progress.step("Updating project scripts and managed files");
585
+ const next = structuredClone(pkg);
586
+ next.scripts = {
587
+ lint: required(generated.scripts, "lint"),
588
+ format: required(generated.scripts, "format"),
589
+ typecheck: "tsc --noEmit",
590
+ "test:run": "vitest run",
591
+ ...pkg.scripts,
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";
597
+ for (const key of [
598
+ "verify",
599
+ "verify:force",
600
+ "qa",
601
+ "qa:force",
602
+ "qa:interactive",
603
+ "qa:visual",
604
+ ])
605
+ next.scripts[key] = required(generated.scripts, key);
606
+ if (config.backend?.enabled)
607
+ for (const key of ["check:php", "build:deploy", "dev:api"])
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
+ }
613
+ next.devDependencies = { ...pkg.devDependencies };
614
+ delete next.devDependencies["@mozole/cli"];
615
+ if (next.dependencies)
616
+ delete next.dependencies["@mozole/cli"];
617
+ for (const key of [
618
+ "@biomejs/biome",
619
+ "@testing-library/jest-dom",
620
+ "@testing-library/react",
621
+ "@testing-library/user-event",
622
+ "@vitejs/plugin-react",
623
+ "jsdom",
624
+ "typescript",
625
+ "vitest",
626
+ "playwright",
627
+ ])
628
+ next.devDependencies[key] = required(generated.devDependencies, key);
629
+ const changed = JSON.stringify(next) !== JSON.stringify(pkg);
630
+ if (changed) {
631
+ await atomicJson(path.join(root, ".mozole", "backups", "package.json.before-adopt"), pkg);
632
+ await atomicJson(packagePath, next);
633
+ }
634
+ await writeFiles(root, files);
635
+ await atomicJson(configPath, config);
636
+ await atomicJson(path.join(root, ".mozole/toolchain.json"), JSON.parse(required(templates, ".mozole/toolchain.json")));
637
+ await ensureBiomeConfig(root, required(templates, "biome.json"), false);
638
+ await ensureGitignore(root, false);
639
+ movedDirectories = await applyCanonicalPlan(root, migration, snapshots);
640
+ progress.step("Synchronizing installed dependencies and finalizing migration");
641
+ if (process.env.MOZOLE_SKIP_INSTALL !== "1") {
642
+ for (const args of [
643
+ ["install", "--no-fund", "--no-audit"],
644
+ ...(process.env.MOZOLE_SKIP_BROWSER === "1"
645
+ ? []
646
+ : [["exec", "--", "playwright", "install", "chromium"]]),
647
+ ]) {
648
+ const result = await run(npmExecutable(), args, {
649
+ cwd: root,
650
+ timeoutMs: 600_000,
651
+ signal: controller.signal,
652
+ logFile: path.join(root, ".mozole", "logs", "adopt-install.log"),
653
+ });
654
+ if (result.exitCode !== 0)
655
+ throw new Error("Dependency setup failed. See .mozole/logs/adopt-install.log");
656
+ }
657
+ }
658
+ controller.signal.throwIfAborted();
659
+ progress.done();
660
+ output.log(`ADOPT PASS\n\n${root}\nCurrent scaffold system installed. Application code preserved.${migrationSummary(migration)}`);
661
+ return 0;
662
+ }
663
+ catch (error) {
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);
680
+ }
681
+ }
682
+ function required(values, key) {
683
+ const value = values[key];
684
+ if (value === undefined)
685
+ throw new Error(`Missing template value: ${key}`);
686
+ return value;
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
+ }
436
815
  //# sourceMappingURL=adopt.js.map