@webmaster-droid/cli 0.4.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.
Files changed (2) hide show
  1. package/dist/index.js +480 -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,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();
@@ -222,6 +441,173 @@ schema.command("build").description("Compile schema file to runtime manifest JSO
222
441
  await fs.writeFile(output, JSON.stringify(manifest, null, 2) + "\n", "utf8");
223
442
  console.log(`Wrote manifest: ${output}`);
224
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
+ });
225
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) => {
226
612
  try {
227
613
  const root = path2.resolve(process.cwd(), srcDir);
@@ -323,20 +709,28 @@ program.command("codemod").description("Apply deterministic JSX codemods to Edit
323
709
  ignore: ["**/node_modules/**", "**/.next/**", "**/dist/**"]
324
710
  });
325
711
  const changed = [];
712
+ const failures = [];
326
713
  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
714
  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");
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
+ });
340
734
  }
341
735
  }
342
736
  const output = path2.resolve(process.cwd(), opts.out);
@@ -345,10 +739,41 @@ program.command("codemod").description("Apply deterministic JSX codemods to Edit
345
739
  source: root,
346
740
  apply: Boolean(opts.apply),
347
741
  changedFiles: changed.length,
742
+ failedFiles: failures.length,
743
+ failures,
348
744
  changes: changed
349
745
  };
350
746
  await ensureDir(output);
351
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
+ }
352
777
  if (opts.json) {
353
778
  emitCliEnvelope({
354
779
  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.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "webmaster-droid": "dist/index.js"