@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.
Files changed (2) hide show
  1. package/dist/index.js +532 -55
  2. 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 stem = file.replace(/\\/g, "/").replace(/^\//, "").replace(/\.[tj]sx?$/, "").replace(/[^a-zA-Z0-9/]+/g, "-").replace(/\//g, ".");
30
- return `pages.todo.${stem}.${kind}.${line}`;
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 = parse(source, {
34
- sourceType: "module",
35
- plugins: ["typescript", "jsx"]
36
- });
105
+ const ast = parseModule(source);
37
106
  let touched = false;
38
- let needsEditableTextImport = false;
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 editableEl = t.jsxElement(
56
- t.jsxOpeningElement(
57
- t.jsxIdentifier("EditableText"),
58
- [
59
- t.jsxAttribute(t.jsxIdentifier("path"), t.stringLiteral(pathHint)),
60
- t.jsxAttribute(t.jsxIdentifier("fallback"), t.stringLiteral(text))
61
- ],
62
- true
63
- ),
64
- null,
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
- if (needsEditableTextImport) {
80
- const body = ast.program.body;
81
- const hasImport = body.some(
82
- (node) => t.isImportDeclaration(node) && node.source.value === "@webmaster-droid/web" && node.specifiers.some(
83
- (specifier) => t.isImportSpecifier(specifier) && t.isIdentifier(specifier.imported) && specifier.imported.name === "EditableText"
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
- changed.push({
335
- file: relFile,
336
- patch: createTwoFilesPatch(relFile, relFile, source, next)
337
- });
338
- if (opts.apply) {
339
- await fs.writeFile(file, next, "utf8");
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webmaster-droid/cli",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "webmaster-droid": "dist/index.js"