@mozole/cli 1.4.0 → 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.
- package/CHANGELOG.md +10 -1
- package/README.md +5 -1
- package/dist/commands/adopt.d.ts +1 -1
- package/dist/commands/adopt.d.ts.map +1 -1
- package/dist/commands/adopt.js +312 -102
- package/dist/commands/adopt.js.map +1 -1
- package/dist/platform/process.d.ts.map +1 -1
- package/dist/platform/process.js +4 -0
- package/dist/platform/process.js.map +1 -1
- package/dist/scaffold/adoption.d.ts +17 -0
- package/dist/scaffold/adoption.d.ts.map +1 -0
- package/dist/scaffold/adoption.js +279 -0
- package/dist/scaffold/adoption.js.map +1 -0
- package/dist/scaffold/design.d.ts +4 -0
- package/dist/scaffold/design.d.ts.map +1 -0
- package/dist/scaffold/design.js +50 -0
- package/dist/scaffold/design.js.map +1 -0
- package/dist/scaffold/legacy.d.ts +4 -0
- package/dist/scaffold/legacy.d.ts.map +1 -0
- package/dist/scaffold/legacy.js +139 -0
- package/dist/scaffold/legacy.js.map +1 -0
- package/dist/scaffold/routes.d.ts +3 -0
- package/dist/scaffold/routes.d.ts.map +1 -0
- package/dist/scaffold/routes.js +37 -0
- package/dist/scaffold/routes.js.map +1 -0
- package/dist/scaffold/standalone.d.ts +2 -1
- package/dist/scaffold/standalone.d.ts.map +1 -1
- package/dist/scaffold/standalone.js +43 -19
- package/dist/scaffold/standalone.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +9 -4
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.js +1 -1
- package/docs/agent-audit.md +2 -2
- package/package.json +1 -1
package/dist/commands/adopt.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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(
|
|
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
|
|
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 (
|
|
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.${
|
|
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
|
-
|
|
488
|
-
|
|
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]
|
|
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]
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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
|