@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.
- package/CHANGELOG.md +22 -0
- package/README.md +54 -156
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +6 -3
- package/dist/cli.js.map +1 -1
- package/dist/commands/adopt.d.ts +2 -1
- package/dist/commands/adopt.d.ts.map +1 -1
- package/dist/commands/adopt.js +460 -81
- package/dist/commands/adopt.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +39 -16
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/new.d.ts.map +1 -1
- package/dist/commands/new.js +22 -11
- package/dist/commands/new.js.map +1 -1
- package/dist/commands/qa.d.ts.map +1 -1
- package/dist/commands/qa.js +18 -12
- package/dist/commands/qa.js.map +1 -1
- package/dist/commands/stats.d.ts.map +1 -1
- package/dist/commands/stats.js +6 -1
- package/dist/commands/stats.js.map +1 -1
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +12 -8
- package/dist/commands/verify.js.map +1 -1
- package/dist/config/project.d.ts +3 -3
- package/dist/config/project.d.ts.map +1 -1
- package/dist/config/project.js +11 -12
- package/dist/config/project.js.map +1 -1
- package/dist/governor/fingerprint.d.ts.map +1 -1
- package/dist/governor/fingerprint.js +2 -0
- package/dist/governor/fingerprint.js.map +1 -1
- package/dist/governor/hook.d.ts +1 -0
- package/dist/governor/hook.d.ts.map +1 -1
- package/dist/governor/hook.js +18 -9
- package/dist/governor/hook.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/qa/runner.js +1 -1
- package/dist/qa/runner.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/backend.d.ts.map +1 -1
- package/dist/scaffold/backend.js +57 -11
- package/dist/scaffold/backend.js.map +1 -1
- 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 +7 -0
- package/dist/scaffold/standalone.d.ts.map +1 -0
- package/dist/scaffold/standalone.js +294 -0
- package/dist/scaffold/standalone.js.map +1 -0
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +82 -26
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/scaffold/write.d.ts.map +1 -1
- package/dist/scaffold/write.js +19 -5
- package/dist/scaffold/write.js.map +1 -1
- package/dist/verify/runner.d.ts.map +1 -1
- package/dist/verify/runner.js +2 -4
- package/dist/verify/runner.js.map +1 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.js +1 -1
- package/docs/agent-audit.md +24 -0
- package/package.json +4 -2
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",
|
|
@@ -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
|
|
78
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
197
|
-
|
|
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
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
|
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(
|
|
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
|
|
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 (
|
|
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
|
|
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.${
|
|
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
|