@webmaster-droid/cli 0.3.0 → 0.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/dist/index.js +486 -56
- 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,177 @@ 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 EDITABLE_COMPONENT_PATHS = {
|
|
334
|
+
EditableText: [{ pathProp: "path", fallbackProp: "fallback" }],
|
|
335
|
+
EditableRichText: [{ pathProp: "path", fallbackProp: "fallback" }],
|
|
336
|
+
EditableImage: [
|
|
337
|
+
{ pathProp: "path", fallbackProp: "fallbackSrc" },
|
|
338
|
+
{ pathProp: "altPath", fallbackProp: "fallbackAlt" }
|
|
339
|
+
],
|
|
340
|
+
EditableLink: [
|
|
341
|
+
{ pathProp: "hrefPath", fallbackProp: "fallbackHref" },
|
|
342
|
+
{ pathProp: "labelPath", fallbackProp: "fallbackLabel" }
|
|
343
|
+
]
|
|
344
|
+
};
|
|
345
|
+
function isSeedableEditablePath(pathValue) {
|
|
346
|
+
return pathValue.startsWith("pages.") || pathValue.startsWith("layout.") || pathValue.startsWith("seo.") || pathValue.startsWith("themeTokens.");
|
|
347
|
+
}
|
|
129
348
|
program.name("webmaster-droid").description("Webmaster Droid CLI").version(CLI_VERSION);
|
|
130
349
|
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
350
|
const backendRaw = String(opts.backend ?? "supabase").trim().toLowerCase();
|
|
@@ -146,12 +365,17 @@ program.command("init").description("Initialize webmaster-droid environment temp
|
|
|
146
365
|
"NEXT_PUBLIC_AGENT_API_BASE_URL=http://localhost:8787",
|
|
147
366
|
"",
|
|
148
367
|
"# Supabase (default backend)",
|
|
368
|
+
"# Supabase Edge blocks user-defined secrets with SUPABASE_ prefix.",
|
|
369
|
+
"# Use CMS_* overrides for custom secrets and leave built-in SUPABASE_* values as provided by Supabase.",
|
|
149
370
|
"NEXT_PUBLIC_SUPABASE_URL=",
|
|
150
371
|
"NEXT_PUBLIC_SUPABASE_ANON_KEY=",
|
|
151
372
|
"SUPABASE_URL=",
|
|
152
373
|
"SUPABASE_ANON_KEY=",
|
|
153
374
|
"SUPABASE_SERVICE_ROLE_KEY=",
|
|
154
|
-
"
|
|
375
|
+
"CMS_SUPABASE_URL=",
|
|
376
|
+
"CMS_SUPABASE_JWKS_URL=",
|
|
377
|
+
"CMS_SUPABASE_AUTH_KEY=",
|
|
378
|
+
"CMS_SUPABASE_JWT_SECRET=",
|
|
155
379
|
"CMS_SUPABASE_BUCKET=webmaster-droid-cms",
|
|
156
380
|
"CMS_STORAGE_PREFIX=cms",
|
|
157
381
|
"",
|
|
@@ -217,6 +441,173 @@ schema.command("build").description("Compile schema file to runtime manifest JSO
|
|
|
217
441
|
await fs.writeFile(output, JSON.stringify(manifest, null, 2) + "\n", "utf8");
|
|
218
442
|
console.log(`Wrote manifest: ${output}`);
|
|
219
443
|
});
|
|
444
|
+
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) => {
|
|
445
|
+
try {
|
|
446
|
+
const root = path2.resolve(process.cwd(), srcDir);
|
|
447
|
+
let rootStat;
|
|
448
|
+
try {
|
|
449
|
+
rootStat = await fs.stat(root);
|
|
450
|
+
} catch {
|
|
451
|
+
throw new Error(`Source directory not found: ${root}`);
|
|
452
|
+
}
|
|
453
|
+
if (!rootStat.isDirectory()) {
|
|
454
|
+
throw new Error(`Source path is not a directory: ${root}`);
|
|
455
|
+
}
|
|
456
|
+
const files = await glob("**/*.{ts,tsx,js,jsx}", {
|
|
457
|
+
cwd: root,
|
|
458
|
+
absolute: true,
|
|
459
|
+
ignore: ["**/*.d.ts", "**/node_modules/**", "**/.next/**", "**/dist/**"]
|
|
460
|
+
});
|
|
461
|
+
const staticPaths = /* @__PURE__ */ new Map();
|
|
462
|
+
const dynamicPaths = [];
|
|
463
|
+
const invalidPaths = [];
|
|
464
|
+
for (const file of files) {
|
|
465
|
+
const code = await fs.readFile(file, "utf8");
|
|
466
|
+
const ast = parse2(code, {
|
|
467
|
+
sourceType: "module",
|
|
468
|
+
plugins: ["typescript", "jsx"]
|
|
469
|
+
});
|
|
470
|
+
traverse2(ast, {
|
|
471
|
+
JSXOpeningElement(pathNode) {
|
|
472
|
+
const componentName = extractIdentifierName(pathNode.node.name);
|
|
473
|
+
if (!componentName) {
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
const specs = EDITABLE_COMPONENT_PATHS[componentName];
|
|
477
|
+
if (!specs) {
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
for (const spec of specs) {
|
|
481
|
+
const pathAttr = findAttribute(pathNode.node, spec.pathProp);
|
|
482
|
+
const pathValue = resolveAttributeValue(pathAttr?.value);
|
|
483
|
+
if (pathValue.kind === "missing") {
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
const relFile = path2.relative(process.cwd(), file);
|
|
487
|
+
if (pathValue.kind === "dynamic") {
|
|
488
|
+
dynamicPaths.push({
|
|
489
|
+
file: relFile,
|
|
490
|
+
line: pathAttr?.loc?.start.line,
|
|
491
|
+
prop: spec.pathProp
|
|
492
|
+
});
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
const normalizedPath = pathValue.value.trim();
|
|
496
|
+
if (!normalizedPath) {
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
if (!isSeedableEditablePath(normalizedPath)) {
|
|
500
|
+
invalidPaths.push({
|
|
501
|
+
file: relFile,
|
|
502
|
+
line: pathAttr?.loc?.start.line,
|
|
503
|
+
path: normalizedPath
|
|
504
|
+
});
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
const fallbackAttr = findAttribute(pathNode.node, spec.fallbackProp);
|
|
508
|
+
const fallbackValue = resolveAttributeValue(fallbackAttr?.value);
|
|
509
|
+
const fallback = fallbackValue.kind === "static" ? fallbackValue.value : "";
|
|
510
|
+
const existing = staticPaths.get(normalizedPath);
|
|
511
|
+
if (!existing) {
|
|
512
|
+
staticPaths.set(normalizedPath, {
|
|
513
|
+
fallback,
|
|
514
|
+
source: relFile,
|
|
515
|
+
line: pathAttr?.loc?.start.line
|
|
516
|
+
});
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
if (!existing.fallback && fallback) {
|
|
520
|
+
staticPaths.set(normalizedPath, {
|
|
521
|
+
fallback,
|
|
522
|
+
source: relFile,
|
|
523
|
+
line: pathAttr?.loc?.start.line
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
const baseFile = opts.base ? path2.resolve(process.cwd(), String(opts.base)) : null;
|
|
531
|
+
const baseSeed = baseFile ? await readJson(baseFile) : createSeedDocument();
|
|
532
|
+
const seedDocument = normalizeSeedDocument(baseSeed);
|
|
533
|
+
let writtenPaths = 0;
|
|
534
|
+
let preservedPaths = 0;
|
|
535
|
+
let writeFailures = 0;
|
|
536
|
+
for (const [seedPath, entry] of staticPaths.entries()) {
|
|
537
|
+
const existingValue = readByPath(seedDocument, seedPath);
|
|
538
|
+
if (existingValue !== void 0) {
|
|
539
|
+
preservedPaths += 1;
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
const written = writeByPath(seedDocument, seedPath, entry.fallback);
|
|
543
|
+
if (written) {
|
|
544
|
+
writtenPaths += 1;
|
|
545
|
+
} else {
|
|
546
|
+
writeFailures += 1;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
const output = path2.resolve(process.cwd(), opts.out);
|
|
550
|
+
await ensureDir(output);
|
|
551
|
+
await fs.writeFile(output, JSON.stringify(seedDocument, null, 2) + "\n", "utf8");
|
|
552
|
+
const report = {
|
|
553
|
+
outputPath: output,
|
|
554
|
+
source: root,
|
|
555
|
+
baseFile,
|
|
556
|
+
totalFiles: files.length,
|
|
557
|
+
discoveredStaticPaths: staticPaths.size,
|
|
558
|
+
writtenPaths,
|
|
559
|
+
preservedPaths,
|
|
560
|
+
dynamicPathSkips: dynamicPaths.length,
|
|
561
|
+
invalidPathSkips: invalidPaths.length,
|
|
562
|
+
writeFailures
|
|
563
|
+
};
|
|
564
|
+
if (opts.json) {
|
|
565
|
+
emitCliEnvelope({
|
|
566
|
+
ok: writeFailures === 0,
|
|
567
|
+
command: "seed",
|
|
568
|
+
version: CLI_VERSION,
|
|
569
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
570
|
+
data: report,
|
|
571
|
+
errors: writeFailures > 0 ? [`Failed to write ${writeFailures} discovered path(s) into seed document.`] : void 0
|
|
572
|
+
}, writeFailures > 0);
|
|
573
|
+
if (writeFailures > 0) {
|
|
574
|
+
process.exitCode = 1;
|
|
575
|
+
}
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
console.log(
|
|
579
|
+
`Seed generated. Static paths: ${staticPaths.size}. Written: ${writtenPaths}. Preserved: ${preservedPaths}. Output: ${output}`
|
|
580
|
+
);
|
|
581
|
+
if (dynamicPaths.length > 0) {
|
|
582
|
+
console.log(
|
|
583
|
+
`Skipped ${dynamicPaths.length} dynamic path expression(s). Convert them manually or use concrete index paths.`
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
if (invalidPaths.length > 0) {
|
|
587
|
+
console.log(
|
|
588
|
+
`Skipped ${invalidPaths.length} non-editable path(s) outside pages/layout/seo/themeTokens.`
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
if (writeFailures > 0) {
|
|
592
|
+
throw new Error(`Failed to write ${writeFailures} discovered path(s) into seed document.`);
|
|
593
|
+
}
|
|
594
|
+
} catch (error) {
|
|
595
|
+
if (!opts.json) {
|
|
596
|
+
throw error;
|
|
597
|
+
}
|
|
598
|
+
emitCliEnvelope(
|
|
599
|
+
{
|
|
600
|
+
ok: false,
|
|
601
|
+
command: "seed",
|
|
602
|
+
version: CLI_VERSION,
|
|
603
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
604
|
+
errors: [errorToMessage(error)]
|
|
605
|
+
},
|
|
606
|
+
true
|
|
607
|
+
);
|
|
608
|
+
process.exitCode = 1;
|
|
609
|
+
}
|
|
610
|
+
});
|
|
220
611
|
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) => {
|
|
221
612
|
try {
|
|
222
613
|
const root = path2.resolve(process.cwd(), srcDir);
|
|
@@ -318,20 +709,28 @@ program.command("codemod").description("Apply deterministic JSX codemods to Edit
|
|
|
318
709
|
ignore: ["**/node_modules/**", "**/.next/**", "**/dist/**"]
|
|
319
710
|
});
|
|
320
711
|
const changed = [];
|
|
712
|
+
const failures = [];
|
|
321
713
|
for (const file of files) {
|
|
322
|
-
const source = await fs.readFile(file, "utf8");
|
|
323
|
-
const transformed = transformEditableTextCodemod(source, file, process.cwd());
|
|
324
|
-
if (!transformed.changed) {
|
|
325
|
-
continue;
|
|
326
|
-
}
|
|
327
|
-
const next = transformed.next;
|
|
328
714
|
const relFile = path2.relative(process.cwd(), file);
|
|
329
|
-
|
|
330
|
-
file
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
715
|
+
try {
|
|
716
|
+
const source = await fs.readFile(file, "utf8");
|
|
717
|
+
const transformed = transformEditableTextCodemod(source, file, root);
|
|
718
|
+
if (!transformed.changed) {
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
const next = transformed.next;
|
|
722
|
+
changed.push({
|
|
723
|
+
file: relFile,
|
|
724
|
+
patch: createTwoFilesPatch(relFile, relFile, source, next)
|
|
725
|
+
});
|
|
726
|
+
if (opts.apply) {
|
|
727
|
+
await fs.writeFile(file, next, "utf8");
|
|
728
|
+
}
|
|
729
|
+
} catch (error) {
|
|
730
|
+
failures.push({
|
|
731
|
+
file: relFile,
|
|
732
|
+
error: errorToMessage(error)
|
|
733
|
+
});
|
|
335
734
|
}
|
|
336
735
|
}
|
|
337
736
|
const output = path2.resolve(process.cwd(), opts.out);
|
|
@@ -340,10 +739,41 @@ program.command("codemod").description("Apply deterministic JSX codemods to Edit
|
|
|
340
739
|
source: root,
|
|
341
740
|
apply: Boolean(opts.apply),
|
|
342
741
|
changedFiles: changed.length,
|
|
742
|
+
failedFiles: failures.length,
|
|
743
|
+
failures,
|
|
343
744
|
changes: changed
|
|
344
745
|
};
|
|
345
746
|
await ensureDir(output);
|
|
346
747
|
await fs.writeFile(output, JSON.stringify(report, null, 2) + "\n", "utf8");
|
|
748
|
+
if (failures.length > 0) {
|
|
749
|
+
const errorMessage = failures.map((item) => `${item.file}: ${item.error}`).join("; ");
|
|
750
|
+
if (opts.json) {
|
|
751
|
+
emitCliEnvelope(
|
|
752
|
+
{
|
|
753
|
+
ok: false,
|
|
754
|
+
command: "codemod",
|
|
755
|
+
version: CLI_VERSION,
|
|
756
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
757
|
+
data: {
|
|
758
|
+
reportPath: output,
|
|
759
|
+
source: root,
|
|
760
|
+
apply: Boolean(opts.apply),
|
|
761
|
+
changedFiles: changed.length
|
|
762
|
+
},
|
|
763
|
+
errors: [errorMessage]
|
|
764
|
+
},
|
|
765
|
+
true
|
|
766
|
+
);
|
|
767
|
+
process.exitCode = 1;
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
console.error(`Codemod encountered ${failures.length} file error(s). Report: ${output}`);
|
|
771
|
+
for (const failure of failures) {
|
|
772
|
+
console.error(`- ${failure.file}: ${failure.error}`);
|
|
773
|
+
}
|
|
774
|
+
process.exitCode = 1;
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
347
777
|
if (opts.json) {
|
|
348
778
|
emitCliEnvelope({
|
|
349
779
|
ok: true,
|