@webmaster-droid/cli 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +532 -55
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -22,20 +22,89 @@ import generateImport from "@babel/generator";
|
|
|
22
22
|
import * as t from "@babel/types";
|
|
23
23
|
var traverse = traverseImport.default ?? traverseImport;
|
|
24
24
|
var generate = generateImport.default ?? generateImport;
|
|
25
|
+
var PARSER_PLUGINS = ["typescript", "jsx"];
|
|
26
|
+
function parseModule(source) {
|
|
27
|
+
return parse(source, {
|
|
28
|
+
sourceType: "module",
|
|
29
|
+
plugins: PARSER_PLUGINS
|
|
30
|
+
});
|
|
31
|
+
}
|
|
25
32
|
function normalizeText(text) {
|
|
26
33
|
return text.replace(/\s+/g, " ").trim();
|
|
27
34
|
}
|
|
28
35
|
function defaultPathFor(file, line, kind) {
|
|
29
|
-
const
|
|
30
|
-
|
|
36
|
+
const normalized = file.replace(/\\/g, "/").replace(/^\//, "").replace(/\.[tj]sx?$/, "");
|
|
37
|
+
const stem = normalized.split("/").filter((segment) => segment && segment !== "." && segment !== "..").map(
|
|
38
|
+
(segment) => segment.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+/, "").replace(/-+$/, "")
|
|
39
|
+
).filter(Boolean).join(".");
|
|
40
|
+
return `pages.todo.${stem || "file"}.${kind}.${line}`;
|
|
41
|
+
}
|
|
42
|
+
function escapeJsxString(value) {
|
|
43
|
+
return JSON.stringify(value);
|
|
44
|
+
}
|
|
45
|
+
function applyReplacements(source, replacements) {
|
|
46
|
+
if (replacements.length === 0) {
|
|
47
|
+
return source;
|
|
48
|
+
}
|
|
49
|
+
const sorted = [...replacements].sort((a, b) => b.start - a.start);
|
|
50
|
+
let out = source;
|
|
51
|
+
for (const replacement of sorted) {
|
|
52
|
+
out = out.slice(0, replacement.start) + replacement.value + out.slice(replacement.end);
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
function hasEditableTextImportDeclaration(node) {
|
|
57
|
+
return node.source.value === "@webmaster-droid/web" && node.specifiers.some(
|
|
58
|
+
(specifier) => t.isImportSpecifier(specifier) && t.isIdentifier(specifier.imported) && specifier.imported.name === "EditableText"
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
function ensureEditableTextImport(source) {
|
|
62
|
+
const ast = parseModule(source);
|
|
63
|
+
const body = ast.program.body;
|
|
64
|
+
let lastImportEnd = 0;
|
|
65
|
+
let moduleImport = null;
|
|
66
|
+
for (const node of body) {
|
|
67
|
+
if (!t.isImportDeclaration(node)) {
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
lastImportEnd = node.end ?? lastImportEnd;
|
|
71
|
+
if (node.source.value === "@webmaster-droid/web" && node.importKind !== "type" && !moduleImport) {
|
|
72
|
+
moduleImport = node;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (moduleImport && hasEditableTextImportDeclaration(moduleImport)) {
|
|
76
|
+
return source;
|
|
77
|
+
}
|
|
78
|
+
if (moduleImport) {
|
|
79
|
+
const hasNamespaceSpecifier = moduleImport.specifiers.some(
|
|
80
|
+
(specifier) => t.isImportNamespaceSpecifier(specifier)
|
|
81
|
+
);
|
|
82
|
+
if (!hasNamespaceSpecifier && moduleImport.start !== null && moduleImport.end !== null) {
|
|
83
|
+
const updatedImport = t.cloneNode(moduleImport);
|
|
84
|
+
updatedImport.specifiers.push(
|
|
85
|
+
t.importSpecifier(t.identifier("EditableText"), t.identifier("EditableText"))
|
|
86
|
+
);
|
|
87
|
+
const importSource = generate(updatedImport, { compact: false }).code;
|
|
88
|
+
return source.slice(0, moduleImport.start) + importSource + source.slice(moduleImport.end);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const importLine = `import { EditableText } from "@webmaster-droid/web";
|
|
92
|
+
`;
|
|
93
|
+
if (lastImportEnd > 0) {
|
|
94
|
+
let insertionPoint = lastImportEnd;
|
|
95
|
+
if (source.slice(insertionPoint, insertionPoint + 2) === "\r\n") {
|
|
96
|
+
insertionPoint += 2;
|
|
97
|
+
} else if (source[insertionPoint] === "\n") {
|
|
98
|
+
insertionPoint += 1;
|
|
99
|
+
}
|
|
100
|
+
return source.slice(0, insertionPoint) + importLine + source.slice(insertionPoint);
|
|
101
|
+
}
|
|
102
|
+
return `${importLine}${source}`;
|
|
31
103
|
}
|
|
32
104
|
function transformEditableTextCodemod(source, filePath, cwd) {
|
|
33
|
-
const ast =
|
|
34
|
-
sourceType: "module",
|
|
35
|
-
plugins: ["typescript", "jsx"]
|
|
36
|
-
});
|
|
105
|
+
const ast = parseModule(source);
|
|
37
106
|
let touched = false;
|
|
38
|
-
|
|
107
|
+
const replacements = [];
|
|
39
108
|
traverse(ast, {
|
|
40
109
|
JSXElement(pathNode) {
|
|
41
110
|
const children = pathNode.node.children;
|
|
@@ -52,22 +121,17 @@ function transformEditableTextCodemod(source, filePath, cwd) {
|
|
|
52
121
|
const loc = nonWhitespace[0].loc?.start.line ?? 0;
|
|
53
122
|
const rel = path.relative(cwd, filePath);
|
|
54
123
|
const pathHint = defaultPathFor(rel, loc, "text");
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
[],
|
|
66
|
-
true
|
|
67
|
-
);
|
|
68
|
-
pathNode.node.children = [t.jsxExpressionContainer(editableEl)];
|
|
124
|
+
const targetNode = nonWhitespace[0];
|
|
125
|
+
if (typeof targetNode.start !== "number" || typeof targetNode.end !== "number") {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const replacement = `<EditableText path=${escapeJsxString(pathHint)} fallback=${escapeJsxString(text)} />`;
|
|
129
|
+
replacements.push({
|
|
130
|
+
start: targetNode.start,
|
|
131
|
+
end: targetNode.end,
|
|
132
|
+
value: replacement
|
|
133
|
+
});
|
|
69
134
|
touched = true;
|
|
70
|
-
needsEditableTextImport = true;
|
|
71
135
|
}
|
|
72
136
|
});
|
|
73
137
|
if (!touched) {
|
|
@@ -76,28 +140,12 @@ function transformEditableTextCodemod(source, filePath, cwd) {
|
|
|
76
140
|
next: source
|
|
77
141
|
};
|
|
78
142
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
)
|
|
85
|
-
);
|
|
86
|
-
if (!hasImport) {
|
|
87
|
-
body.unshift(
|
|
88
|
-
t.importDeclaration(
|
|
89
|
-
[
|
|
90
|
-
t.importSpecifier(
|
|
91
|
-
t.identifier("EditableText"),
|
|
92
|
-
t.identifier("EditableText")
|
|
93
|
-
)
|
|
94
|
-
],
|
|
95
|
-
t.stringLiteral("@webmaster-droid/web")
|
|
96
|
-
)
|
|
97
|
-
);
|
|
98
|
-
}
|
|
143
|
+
let next = applyReplacements(source, replacements);
|
|
144
|
+
next = ensureEditableTextImport(next);
|
|
145
|
+
parseModule(next);
|
|
146
|
+
if (source.endsWith("\n") && !next.endsWith("\n")) {
|
|
147
|
+
next += "\n";
|
|
99
148
|
}
|
|
100
|
-
const next = generate(ast, { retainLines: true }, source).code;
|
|
101
149
|
return {
|
|
102
150
|
changed: next !== source,
|
|
103
151
|
next
|
|
@@ -126,6 +174,195 @@ async function readJson(filePath) {
|
|
|
126
174
|
const raw = await fs.readFile(filePath, "utf8");
|
|
127
175
|
return JSON.parse(raw);
|
|
128
176
|
}
|
|
177
|
+
function isRecord(value) {
|
|
178
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
179
|
+
}
|
|
180
|
+
function splitPath(pathValue) {
|
|
181
|
+
return pathValue.replace(/\[(\d+)\]/g, ".$1").split(".").map((segment) => segment.trim()).filter(Boolean);
|
|
182
|
+
}
|
|
183
|
+
function readByPath(input, pathValue) {
|
|
184
|
+
const segments = splitPath(pathValue);
|
|
185
|
+
let current = input;
|
|
186
|
+
for (const segment of segments) {
|
|
187
|
+
if (Array.isArray(current)) {
|
|
188
|
+
const index = Number(segment);
|
|
189
|
+
if (Number.isNaN(index)) {
|
|
190
|
+
return void 0;
|
|
191
|
+
}
|
|
192
|
+
current = current[index];
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (!isRecord(current)) {
|
|
196
|
+
return void 0;
|
|
197
|
+
}
|
|
198
|
+
current = current[segment];
|
|
199
|
+
}
|
|
200
|
+
return current;
|
|
201
|
+
}
|
|
202
|
+
function writeByPath(input, pathValue, value) {
|
|
203
|
+
const segments = splitPath(pathValue);
|
|
204
|
+
if (segments.length === 0) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
let current = input;
|
|
208
|
+
for (let index = 0; index < segments.length - 1; index += 1) {
|
|
209
|
+
const segment = segments[index];
|
|
210
|
+
if (Array.isArray(current)) {
|
|
211
|
+
const itemIndex = Number(segment);
|
|
212
|
+
if (Number.isNaN(itemIndex)) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
if (current[itemIndex] === void 0) {
|
|
216
|
+
const next = segments[index + 1];
|
|
217
|
+
current[itemIndex] = /^\d+$/.test(next) ? [] : {};
|
|
218
|
+
}
|
|
219
|
+
current = current[itemIndex];
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (!isRecord(current)) {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
if (current[segment] === void 0) {
|
|
226
|
+
const next = segments[index + 1];
|
|
227
|
+
current[segment] = /^\d+$/.test(next) ? [] : {};
|
|
228
|
+
}
|
|
229
|
+
current = current[segment];
|
|
230
|
+
}
|
|
231
|
+
const leaf = segments.at(-1);
|
|
232
|
+
if (!leaf) {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
if (Array.isArray(current)) {
|
|
236
|
+
const itemIndex = Number(leaf);
|
|
237
|
+
if (Number.isNaN(itemIndex)) {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
current[itemIndex] = value;
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
if (!isRecord(current)) {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
current[leaf] = value;
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
function createSeedDocument() {
|
|
250
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
251
|
+
return {
|
|
252
|
+
meta: {
|
|
253
|
+
schemaVersion: 1,
|
|
254
|
+
contentVersion: "seed_v1",
|
|
255
|
+
updatedAt: now,
|
|
256
|
+
updatedBy: "seed-generator"
|
|
257
|
+
},
|
|
258
|
+
themeTokens: {},
|
|
259
|
+
layout: {},
|
|
260
|
+
pages: {},
|
|
261
|
+
seo: {}
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function normalizeSeedDocument(input) {
|
|
265
|
+
const defaults = createSeedDocument();
|
|
266
|
+
if (!isRecord(input)) {
|
|
267
|
+
return defaults;
|
|
268
|
+
}
|
|
269
|
+
const output = {
|
|
270
|
+
...defaults,
|
|
271
|
+
...input
|
|
272
|
+
};
|
|
273
|
+
const metaValue = isRecord(output.meta) ? output.meta : {};
|
|
274
|
+
output.meta = {
|
|
275
|
+
...defaults.meta,
|
|
276
|
+
...metaValue
|
|
277
|
+
};
|
|
278
|
+
for (const key of ["themeTokens", "layout", "pages", "seo"]) {
|
|
279
|
+
if (!isRecord(output[key])) {
|
|
280
|
+
output[key] = {};
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return output;
|
|
284
|
+
}
|
|
285
|
+
function extractIdentifierName(value) {
|
|
286
|
+
if (t2.isJSXIdentifier(value)) {
|
|
287
|
+
return value.name;
|
|
288
|
+
}
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
function staticTemplateLiteralValue(template) {
|
|
292
|
+
if (template.expressions.length > 0) {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
return template.quasis.map((part) => part.value.cooked ?? part.value.raw).join("");
|
|
296
|
+
}
|
|
297
|
+
function resolveAttributeValue(value) {
|
|
298
|
+
if (!value) {
|
|
299
|
+
return { kind: "missing" };
|
|
300
|
+
}
|
|
301
|
+
if (t2.isStringLiteral(value)) {
|
|
302
|
+
return { kind: "static", value: value.value };
|
|
303
|
+
}
|
|
304
|
+
if (!t2.isJSXExpressionContainer(value)) {
|
|
305
|
+
return { kind: "dynamic" };
|
|
306
|
+
}
|
|
307
|
+
const expression = value.expression;
|
|
308
|
+
if (t2.isStringLiteral(expression)) {
|
|
309
|
+
return { kind: "static", value: expression.value };
|
|
310
|
+
}
|
|
311
|
+
if (t2.isTemplateLiteral(expression)) {
|
|
312
|
+
const staticValue = staticTemplateLiteralValue(expression);
|
|
313
|
+
if (staticValue !== null) {
|
|
314
|
+
return { kind: "static", value: staticValue };
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return { kind: "dynamic" };
|
|
318
|
+
}
|
|
319
|
+
function findAttribute(node, name) {
|
|
320
|
+
for (const attribute of node.attributes) {
|
|
321
|
+
if (!t2.isJSXAttribute(attribute)) {
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (!t2.isJSXIdentifier(attribute.name)) {
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
if (attribute.name.name === name) {
|
|
328
|
+
return attribute;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
var SEEDABLE_ROOT_PREFIXES = ["pages.", "layout.", "seo.", "themeTokens."];
|
|
334
|
+
var EDITABLE_COMPONENT_PATHS = {
|
|
335
|
+
EditableText: [{ pathProp: "path", fallbackProp: "fallback" }],
|
|
336
|
+
EditableRichText: [{ pathProp: "path", fallbackProp: "fallback" }],
|
|
337
|
+
EditableImage: [
|
|
338
|
+
{ pathProp: "path", fallbackProp: "fallbackSrc" },
|
|
339
|
+
{ pathProp: "altPath", fallbackProp: "fallbackAlt" }
|
|
340
|
+
],
|
|
341
|
+
EditableLink: [
|
|
342
|
+
{ pathProp: "hrefPath", fallbackProp: "fallbackHref" },
|
|
343
|
+
{ pathProp: "labelPath", fallbackProp: "fallbackLabel" }
|
|
344
|
+
]
|
|
345
|
+
};
|
|
346
|
+
function isSeedableEditablePath(pathValue) {
|
|
347
|
+
return SEEDABLE_ROOT_PREFIXES.some((prefix) => pathValue.startsWith(prefix));
|
|
348
|
+
}
|
|
349
|
+
function formatSourceLocation(file, line) {
|
|
350
|
+
return line ? `${file}:${line}` : file;
|
|
351
|
+
}
|
|
352
|
+
function printSkipDetails(heading, entries) {
|
|
353
|
+
if (entries.length === 0) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
console.log(heading);
|
|
357
|
+
const MAX_PREVIEW = 25;
|
|
358
|
+
const shown = entries.slice(0, MAX_PREVIEW);
|
|
359
|
+
for (const entry of shown) {
|
|
360
|
+
console.log(` - ${entry}`);
|
|
361
|
+
}
|
|
362
|
+
if (entries.length > shown.length) {
|
|
363
|
+
console.log(` - ... ${entries.length - shown.length} more`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
129
366
|
program.name("webmaster-droid").description("Webmaster Droid CLI").version(CLI_VERSION);
|
|
130
367
|
program.command("init").description("Initialize webmaster-droid environment template in current project").option("--backend <backend>", "backend (supabase|aws)", "supabase").option("--out <dir>", "output dir", ".").action(async (opts) => {
|
|
131
368
|
const backendRaw = String(opts.backend ?? "supabase").trim().toLowerCase();
|
|
@@ -165,6 +402,8 @@ program.command("init").description("Initialize webmaster-droid environment temp
|
|
|
165
402
|
"MODEL_OPENAI_ENABLED=true",
|
|
166
403
|
"MODEL_GEMINI_ENABLED=true",
|
|
167
404
|
"DEFAULT_MODEL_ID=openai:gpt-5.2",
|
|
405
|
+
"OPENAI_API_KEY=",
|
|
406
|
+
"GOOGLE_GENERATIVE_AI_API_KEY=",
|
|
168
407
|
"",
|
|
169
408
|
"# AWS (optional backend)",
|
|
170
409
|
"CMS_S3_BUCKET=",
|
|
@@ -222,6 +461,205 @@ schema.command("build").description("Compile schema file to runtime manifest JSO
|
|
|
222
461
|
await fs.writeFile(output, JSON.stringify(manifest, null, 2) + "\n", "utf8");
|
|
223
462
|
console.log(`Wrote manifest: ${output}`);
|
|
224
463
|
});
|
|
464
|
+
program.command("seed").description("Generate CMS seed document from Editable component paths").argument("<srcDir>", "source directory").option("--out <file>", "seed output file", "cms/seed.from-editables.json").option("--base <file>", "merge into an existing seed document").option("--json", "emit machine-readable JSON output", false).action(async (srcDir, opts) => {
|
|
465
|
+
try {
|
|
466
|
+
const root = path2.resolve(process.cwd(), srcDir);
|
|
467
|
+
let rootStat;
|
|
468
|
+
try {
|
|
469
|
+
rootStat = await fs.stat(root);
|
|
470
|
+
} catch {
|
|
471
|
+
throw new Error(`Source directory not found: ${root}`);
|
|
472
|
+
}
|
|
473
|
+
if (!rootStat.isDirectory()) {
|
|
474
|
+
throw new Error(`Source path is not a directory: ${root}`);
|
|
475
|
+
}
|
|
476
|
+
const files = await glob("**/*.{ts,tsx,js,jsx}", {
|
|
477
|
+
cwd: root,
|
|
478
|
+
absolute: true,
|
|
479
|
+
ignore: ["**/*.d.ts", "**/node_modules/**", "**/.next/**", "**/dist/**"]
|
|
480
|
+
});
|
|
481
|
+
const staticPaths = /* @__PURE__ */ new Map();
|
|
482
|
+
const dynamicPaths = [];
|
|
483
|
+
const invalidPaths = [];
|
|
484
|
+
for (const file of files) {
|
|
485
|
+
const code = await fs.readFile(file, "utf8");
|
|
486
|
+
const ast = parse2(code, {
|
|
487
|
+
sourceType: "module",
|
|
488
|
+
plugins: ["typescript", "jsx"]
|
|
489
|
+
});
|
|
490
|
+
traverse2(ast, {
|
|
491
|
+
JSXOpeningElement(pathNode) {
|
|
492
|
+
const componentName = extractIdentifierName(pathNode.node.name);
|
|
493
|
+
if (!componentName) {
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
const specs = EDITABLE_COMPONENT_PATHS[componentName];
|
|
497
|
+
if (!specs) {
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
for (const spec of specs) {
|
|
501
|
+
const pathAttr = findAttribute(pathNode.node, spec.pathProp);
|
|
502
|
+
const pathValue = resolveAttributeValue(pathAttr?.value);
|
|
503
|
+
if (pathValue.kind === "missing") {
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
const relFile = path2.relative(process.cwd(), file);
|
|
507
|
+
if (pathValue.kind === "dynamic") {
|
|
508
|
+
dynamicPaths.push({
|
|
509
|
+
file: relFile,
|
|
510
|
+
line: pathAttr?.loc?.start.line,
|
|
511
|
+
component: componentName,
|
|
512
|
+
prop: spec.pathProp
|
|
513
|
+
});
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
const normalizedPath = pathValue.value.trim();
|
|
517
|
+
if (!normalizedPath) {
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
if (!isSeedableEditablePath(normalizedPath)) {
|
|
521
|
+
invalidPaths.push({
|
|
522
|
+
file: relFile,
|
|
523
|
+
line: pathAttr?.loc?.start.line,
|
|
524
|
+
component: componentName,
|
|
525
|
+
prop: spec.pathProp,
|
|
526
|
+
path: normalizedPath
|
|
527
|
+
});
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
const fallbackAttr = findAttribute(pathNode.node, spec.fallbackProp);
|
|
531
|
+
const fallbackValue = resolveAttributeValue(fallbackAttr?.value);
|
|
532
|
+
const fallback = fallbackValue.kind === "static" ? fallbackValue.value : "";
|
|
533
|
+
const existing = staticPaths.get(normalizedPath);
|
|
534
|
+
if (!existing) {
|
|
535
|
+
staticPaths.set(normalizedPath, {
|
|
536
|
+
fallback,
|
|
537
|
+
source: relFile,
|
|
538
|
+
line: pathAttr?.loc?.start.line
|
|
539
|
+
});
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
if (!existing.fallback && fallback) {
|
|
543
|
+
staticPaths.set(normalizedPath, {
|
|
544
|
+
fallback,
|
|
545
|
+
source: relFile,
|
|
546
|
+
line: pathAttr?.loc?.start.line
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
const baseFile = opts.base ? path2.resolve(process.cwd(), String(opts.base)) : null;
|
|
554
|
+
const baseSeed = baseFile ? await readJson(baseFile) : createSeedDocument();
|
|
555
|
+
const seedDocument = normalizeSeedDocument(baseSeed);
|
|
556
|
+
let writtenPaths = 0;
|
|
557
|
+
let preservedPaths = 0;
|
|
558
|
+
let writeFailures = 0;
|
|
559
|
+
for (const [seedPath, entry] of staticPaths.entries()) {
|
|
560
|
+
const existingValue = readByPath(seedDocument, seedPath);
|
|
561
|
+
if (existingValue !== void 0) {
|
|
562
|
+
preservedPaths += 1;
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
const written = writeByPath(seedDocument, seedPath, entry.fallback);
|
|
566
|
+
if (written) {
|
|
567
|
+
writtenPaths += 1;
|
|
568
|
+
} else {
|
|
569
|
+
writeFailures += 1;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
const output = path2.resolve(process.cwd(), opts.out);
|
|
573
|
+
await ensureDir(output);
|
|
574
|
+
await fs.writeFile(output, JSON.stringify(seedDocument, null, 2) + "\n", "utf8");
|
|
575
|
+
const report = {
|
|
576
|
+
outputPath: output,
|
|
577
|
+
source: root,
|
|
578
|
+
baseFile,
|
|
579
|
+
totalFiles: files.length,
|
|
580
|
+
discoveredStaticPaths: staticPaths.size,
|
|
581
|
+
writtenPaths,
|
|
582
|
+
preservedPaths,
|
|
583
|
+
dynamicPathSkips: dynamicPaths.length,
|
|
584
|
+
invalidPathSkips: invalidPaths.length,
|
|
585
|
+
writeFailures,
|
|
586
|
+
allowedRootPrefixes: [...SEEDABLE_ROOT_PREFIXES],
|
|
587
|
+
dynamicPathDetails: dynamicPaths.map((entry) => ({
|
|
588
|
+
...entry,
|
|
589
|
+
location: formatSourceLocation(entry.file, entry.line),
|
|
590
|
+
reason: "Path prop uses a dynamic expression and cannot be seeded automatically. Convert to concrete indexed paths."
|
|
591
|
+
})),
|
|
592
|
+
invalidPathDetails: invalidPaths.map((entry) => ({
|
|
593
|
+
...entry,
|
|
594
|
+
location: formatSourceLocation(entry.file, entry.line),
|
|
595
|
+
reason: `Path root is outside supported prefixes (${SEEDABLE_ROOT_PREFIXES.join(", ")}).`
|
|
596
|
+
})),
|
|
597
|
+
manualMigrationRequired: dynamicPaths.length > 0 || invalidPaths.length > 0
|
|
598
|
+
};
|
|
599
|
+
if (opts.json) {
|
|
600
|
+
emitCliEnvelope({
|
|
601
|
+
ok: writeFailures === 0,
|
|
602
|
+
command: "seed",
|
|
603
|
+
version: CLI_VERSION,
|
|
604
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
605
|
+
data: report,
|
|
606
|
+
errors: writeFailures > 0 ? [`Failed to write ${writeFailures} discovered path(s) into seed document.`] : void 0
|
|
607
|
+
}, writeFailures > 0);
|
|
608
|
+
if (writeFailures > 0) {
|
|
609
|
+
process.exitCode = 1;
|
|
610
|
+
}
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
console.log(
|
|
614
|
+
`Seed generated. Static paths: ${staticPaths.size}. Written: ${writtenPaths}. Preserved: ${preservedPaths}. Output: ${output}`
|
|
615
|
+
);
|
|
616
|
+
if (dynamicPaths.length > 0) {
|
|
617
|
+
console.log(
|
|
618
|
+
`Skipped ${dynamicPaths.length} dynamic path expression(s). Manual migration required before first edit.`
|
|
619
|
+
);
|
|
620
|
+
printSkipDetails(
|
|
621
|
+
"Dynamic path locations:",
|
|
622
|
+
dynamicPaths.map(
|
|
623
|
+
(entry) => `${formatSourceLocation(entry.file, entry.line)} <${entry.component} ${entry.prop}=...>`
|
|
624
|
+
)
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
if (invalidPaths.length > 0) {
|
|
628
|
+
console.log(
|
|
629
|
+
`Skipped ${invalidPaths.length} non-editable path(s) outside allowed roots (${SEEDABLE_ROOT_PREFIXES.join(", ")}).`
|
|
630
|
+
);
|
|
631
|
+
printSkipDetails(
|
|
632
|
+
"Invalid root path locations:",
|
|
633
|
+
invalidPaths.map(
|
|
634
|
+
(entry) => `${formatSourceLocation(entry.file, entry.line)} <${entry.component} ${entry.prop}="${entry.path}">`
|
|
635
|
+
)
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
if (dynamicPaths.length > 0 || invalidPaths.length > 0) {
|
|
639
|
+
console.log(
|
|
640
|
+
"Seed report includes all skipped entries. Resolve these paths manually, then rerun `webmaster-droid seed`."
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
if (writeFailures > 0) {
|
|
644
|
+
throw new Error(`Failed to write ${writeFailures} discovered path(s) into seed document.`);
|
|
645
|
+
}
|
|
646
|
+
} catch (error) {
|
|
647
|
+
if (!opts.json) {
|
|
648
|
+
throw error;
|
|
649
|
+
}
|
|
650
|
+
emitCliEnvelope(
|
|
651
|
+
{
|
|
652
|
+
ok: false,
|
|
653
|
+
command: "seed",
|
|
654
|
+
version: CLI_VERSION,
|
|
655
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
656
|
+
errors: [errorToMessage(error)]
|
|
657
|
+
},
|
|
658
|
+
true
|
|
659
|
+
);
|
|
660
|
+
process.exitCode = 1;
|
|
661
|
+
}
|
|
662
|
+
});
|
|
225
663
|
program.command("scan").description("Scan source files for static content candidates").argument("<srcDir>", "source directory").option("--out <file>", "report output", ".webmaster-droid/scan-report.json").option("--json", "emit machine-readable JSON output", false).action(async (srcDir, opts) => {
|
|
226
664
|
try {
|
|
227
665
|
const root = path2.resolve(process.cwd(), srcDir);
|
|
@@ -323,20 +761,28 @@ program.command("codemod").description("Apply deterministic JSX codemods to Edit
|
|
|
323
761
|
ignore: ["**/node_modules/**", "**/.next/**", "**/dist/**"]
|
|
324
762
|
});
|
|
325
763
|
const changed = [];
|
|
764
|
+
const failures = [];
|
|
326
765
|
for (const file of files) {
|
|
327
|
-
const source = await fs.readFile(file, "utf8");
|
|
328
|
-
const transformed = transformEditableTextCodemod(source, file, process.cwd());
|
|
329
|
-
if (!transformed.changed) {
|
|
330
|
-
continue;
|
|
331
|
-
}
|
|
332
|
-
const next = transformed.next;
|
|
333
766
|
const relFile = path2.relative(process.cwd(), file);
|
|
334
|
-
|
|
335
|
-
file
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
767
|
+
try {
|
|
768
|
+
const source = await fs.readFile(file, "utf8");
|
|
769
|
+
const transformed = transformEditableTextCodemod(source, file, root);
|
|
770
|
+
if (!transformed.changed) {
|
|
771
|
+
continue;
|
|
772
|
+
}
|
|
773
|
+
const next = transformed.next;
|
|
774
|
+
changed.push({
|
|
775
|
+
file: relFile,
|
|
776
|
+
patch: createTwoFilesPatch(relFile, relFile, source, next)
|
|
777
|
+
});
|
|
778
|
+
if (opts.apply) {
|
|
779
|
+
await fs.writeFile(file, next, "utf8");
|
|
780
|
+
}
|
|
781
|
+
} catch (error) {
|
|
782
|
+
failures.push({
|
|
783
|
+
file: relFile,
|
|
784
|
+
error: errorToMessage(error)
|
|
785
|
+
});
|
|
340
786
|
}
|
|
341
787
|
}
|
|
342
788
|
const output = path2.resolve(process.cwd(), opts.out);
|
|
@@ -345,10 +791,41 @@ program.command("codemod").description("Apply deterministic JSX codemods to Edit
|
|
|
345
791
|
source: root,
|
|
346
792
|
apply: Boolean(opts.apply),
|
|
347
793
|
changedFiles: changed.length,
|
|
794
|
+
failedFiles: failures.length,
|
|
795
|
+
failures,
|
|
348
796
|
changes: changed
|
|
349
797
|
};
|
|
350
798
|
await ensureDir(output);
|
|
351
799
|
await fs.writeFile(output, JSON.stringify(report, null, 2) + "\n", "utf8");
|
|
800
|
+
if (failures.length > 0) {
|
|
801
|
+
const errorMessage = failures.map((item) => `${item.file}: ${item.error}`).join("; ");
|
|
802
|
+
if (opts.json) {
|
|
803
|
+
emitCliEnvelope(
|
|
804
|
+
{
|
|
805
|
+
ok: false,
|
|
806
|
+
command: "codemod",
|
|
807
|
+
version: CLI_VERSION,
|
|
808
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
809
|
+
data: {
|
|
810
|
+
reportPath: output,
|
|
811
|
+
source: root,
|
|
812
|
+
apply: Boolean(opts.apply),
|
|
813
|
+
changedFiles: changed.length
|
|
814
|
+
},
|
|
815
|
+
errors: [errorMessage]
|
|
816
|
+
},
|
|
817
|
+
true
|
|
818
|
+
);
|
|
819
|
+
process.exitCode = 1;
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
console.error(`Codemod encountered ${failures.length} file error(s). Report: ${output}`);
|
|
823
|
+
for (const failure of failures) {
|
|
824
|
+
console.error(`- ${failure.file}: ${failure.error}`);
|
|
825
|
+
}
|
|
826
|
+
process.exitCode = 1;
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
352
829
|
if (opts.json) {
|
|
353
830
|
emitCliEnvelope({
|
|
354
831
|
ok: true,
|