@webmaster-droid/cli 0.1.0-alpha.1 → 0.1.0

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 +120 -96
  2. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -2,19 +2,26 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { promises as fs } from "fs";
5
- import path from "path";
5
+ import path2 from "path";
6
6
  import process from "process";
7
7
  import { spawn } from "child_process";
8
8
  import { fileURLToPath } from "url";
9
9
  import { Command } from "commander";
10
10
  import { glob } from "glob";
11
- import { parse } from "@babel/parser";
12
- import traverse from "@babel/traverse";
13
- import generate from "@babel/generator";
14
- import * as t from "@babel/types";
11
+ import { parse as parse2 } from "@babel/parser";
12
+ import traverse2 from "@babel/traverse";
13
+ import * as t2 from "@babel/types";
15
14
  import { createTwoFilesPatch } from "diff";
16
15
  import { createJiti } from "jiti";
17
- var program = new Command();
16
+
17
+ // src/codemod.ts
18
+ import path from "path";
19
+ import { parse } from "@babel/parser";
20
+ import traverseImport from "@babel/traverse";
21
+ import generateImport from "@babel/generator";
22
+ import * as t from "@babel/types";
23
+ var traverse = traverseImport.default ?? traverseImport;
24
+ var generate = generateImport.default ?? generateImport;
18
25
  function normalizeText(text) {
19
26
  return text.replace(/\s+/g, " ").trim();
20
27
  }
@@ -22,22 +29,98 @@ function defaultPathFor(file, line, kind) {
22
29
  const stem = file.replace(/\\/g, "/").replace(/^\//, "").replace(/\.[tj]sx?$/, "").replace(/[^a-zA-Z0-9/]+/g, "-").replace(/\//g, ".");
23
30
  return `pages.todo.${stem}.${kind}.${line}`;
24
31
  }
32
+ function transformEditableTextCodemod(source, filePath, cwd) {
33
+ const ast = parse(source, {
34
+ sourceType: "module",
35
+ plugins: ["typescript", "jsx"]
36
+ });
37
+ let touched = false;
38
+ let needsEditableTextImport = false;
39
+ traverse(ast, {
40
+ JSXElement(pathNode) {
41
+ const children = pathNode.node.children;
42
+ const nonWhitespace = children.filter(
43
+ (child) => !(t.isJSXText(child) && normalizeText(child.value) === "")
44
+ );
45
+ if (nonWhitespace.length !== 1 || !t.isJSXText(nonWhitespace[0])) {
46
+ return;
47
+ }
48
+ const text = normalizeText(nonWhitespace[0].value);
49
+ if (!text || text.length < 3) {
50
+ return;
51
+ }
52
+ const loc = nonWhitespace[0].loc?.start.line ?? 0;
53
+ const rel = path.relative(cwd, filePath);
54
+ 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)];
69
+ touched = true;
70
+ needsEditableTextImport = true;
71
+ }
72
+ });
73
+ if (!touched) {
74
+ return {
75
+ changed: false,
76
+ next: source
77
+ };
78
+ }
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
+ }
99
+ }
100
+ const next = generate(ast, { retainLines: true }, source).code;
101
+ return {
102
+ changed: next !== source,
103
+ next
104
+ };
105
+ }
106
+
107
+ // src/index.ts
108
+ var program = new Command();
25
109
  async function ensureDir(filePath) {
26
- await fs.mkdir(path.dirname(filePath), { recursive: true });
110
+ await fs.mkdir(path2.dirname(filePath), { recursive: true });
27
111
  }
28
112
  async function readJson(filePath) {
29
113
  const raw = await fs.readFile(filePath, "utf8");
30
114
  return JSON.parse(raw);
31
115
  }
32
116
  program.name("webmaster-droid").description("Webmaster Droid CLI").version("0.1.0-alpha.0");
33
- program.command("init").description("Initialize webmaster-droid config in current project").option("--framework <framework>", "framework", "next").option("--backend <backend>", "backend", "aws").option("--out <dir>", "output dir", ".").action(async (opts) => {
34
- const outDir = path.resolve(process.cwd(), opts.out);
35
- const configPath = path.join(outDir, "webmaster-droid.config.ts");
117
+ program.command("init").description("Initialize optional webmaster-droid config in current project").option("--framework <framework>", "framework", "next").option("--backend <backend>", "backend", "aws").option("--out <dir>", "output dir", ".").action(async (opts) => {
118
+ const outDir = path2.resolve(process.cwd(), opts.out);
119
+ const configPath = path2.join(outDir, "webmaster-droid.config.ts");
36
120
  await ensureDir(configPath);
37
121
  const config = `export default {
38
122
  framework: "${opts.framework}",
39
123
  backend: "${opts.backend}",
40
- schema: "./cms/schema.webmaster.ts",
41
124
  apiBaseUrlEnv: "NEXT_PUBLIC_AGENT_API_BASE_URL"
42
125
  };
43
126
  `;
@@ -48,7 +131,7 @@ program.command("init").description("Initialize webmaster-droid config in curren
48
131
  await fs.writeFile(configPath, config, "utf8");
49
132
  console.log(`Created: ${configPath}`);
50
133
  }
51
- const envExample = path.join(outDir, ".env.webmaster-droid.example");
134
+ const envExample = path2.join(outDir, ".env.webmaster-droid.example");
52
135
  try {
53
136
  await fs.access(envExample);
54
137
  } catch {
@@ -69,9 +152,9 @@ program.command("init").description("Initialize webmaster-droid config in curren
69
152
  console.log(`Created: ${envExample}`);
70
153
  }
71
154
  });
72
- var schema = program.command("schema").description("Schema helpers");
155
+ var schema = program.command("schema").description("Optional schema helpers");
73
156
  schema.command("init").description("Create starter schema file").option("--out <file>", "schema output", "cms/schema.webmaster.ts").action(async (opts) => {
74
- const outFile = path.resolve(process.cwd(), opts.out);
157
+ const outFile = path2.resolve(process.cwd(), opts.out);
75
158
  await ensureDir(outFile);
76
159
  const template = `export default {
77
160
  name: "webmaster-droid-schema",
@@ -89,8 +172,8 @@ schema.command("init").description("Create starter schema file").option("--out <
89
172
  }
90
173
  });
91
174
  schema.command("build").description("Compile schema file to runtime manifest JSON").requiredOption("--input <file>", "input schema file (.ts, .js, .json)").option("--output <file>", "output manifest", "cms/schema.manifest.json").action(async (opts) => {
92
- const input = path.resolve(process.cwd(), opts.input);
93
- const output = path.resolve(process.cwd(), opts.output);
175
+ const input = path2.resolve(process.cwd(), opts.input);
176
+ const output = path2.resolve(process.cwd(), opts.output);
94
177
  let manifest;
95
178
  if (input.endsWith(".json")) {
96
179
  manifest = await readJson(input);
@@ -111,7 +194,7 @@ schema.command("build").description("Compile schema file to runtime manifest JSO
111
194
  console.log(`Wrote manifest: ${output}`);
112
195
  });
113
196
  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").action(async (srcDir, opts) => {
114
- const root = path.resolve(process.cwd(), srcDir);
197
+ const root = path2.resolve(process.cwd(), srcDir);
115
198
  const files = await glob("**/*.{ts,tsx,js,jsx}", {
116
199
  cwd: root,
117
200
  absolute: true,
@@ -120,11 +203,11 @@ program.command("scan").description("Scan source files for static content candid
120
203
  const findings = [];
121
204
  for (const file of files) {
122
205
  const code = await fs.readFile(file, "utf8");
123
- const ast = parse(code, {
206
+ const ast = parse2(code, {
124
207
  sourceType: "module",
125
208
  plugins: ["typescript", "jsx"]
126
209
  });
127
- traverse(ast, {
210
+ traverse2(ast, {
128
211
  JSXText(pathNode) {
129
212
  const text = normalizeText(pathNode.node.value);
130
213
  if (!text || text.length < 3) {
@@ -132,25 +215,25 @@ program.command("scan").description("Scan source files for static content candid
132
215
  }
133
216
  findings.push({
134
217
  type: "jsx-text",
135
- file: path.relative(process.cwd(), file),
218
+ file: path2.relative(process.cwd(), file),
136
219
  line: pathNode.node.loc?.start.line,
137
220
  column: pathNode.node.loc?.start.column,
138
221
  text
139
222
  });
140
223
  },
141
224
  JSXAttribute(pathNode) {
142
- const name = t.isJSXIdentifier(pathNode.node.name) ? pathNode.node.name.name : "";
225
+ const name = t2.isJSXIdentifier(pathNode.node.name) ? pathNode.node.name.name : "";
143
226
  if (!["src", "href", "alt", "title"].includes(name)) {
144
227
  return;
145
228
  }
146
229
  const valueNode = pathNode.node.value;
147
- if (!valueNode || !t.isStringLiteral(valueNode)) {
230
+ if (!valueNode || !t2.isStringLiteral(valueNode)) {
148
231
  return;
149
232
  }
150
233
  findings.push({
151
234
  type: "jsx-attr",
152
235
  attr: name,
153
- file: path.relative(process.cwd(), file),
236
+ file: path2.relative(process.cwd(), file),
154
237
  line: valueNode.loc?.start.line,
155
238
  column: valueNode.loc?.start.column,
156
239
  text: valueNode.value
@@ -158,7 +241,7 @@ program.command("scan").description("Scan source files for static content candid
158
241
  }
159
242
  });
160
243
  }
161
- const output = path.resolve(process.cwd(), opts.out);
244
+ const output = path2.resolve(process.cwd(), opts.out);
162
245
  await ensureDir(output);
163
246
  await fs.writeFile(
164
247
  output,
@@ -178,7 +261,7 @@ program.command("scan").description("Scan source files for static content candid
178
261
  console.log(`Scan complete. Findings: ${findings.length}. Report: ${output}`);
179
262
  });
180
263
  program.command("codemod").description("Apply deterministic JSX codemods to Editable components").argument("<srcDir>", "source directory").option("--apply", "write file changes", false).option("--out <file>", "report output", ".webmaster-droid/codemod-report.json").action(async (srcDir, opts) => {
181
- const root = path.resolve(process.cwd(), srcDir);
264
+ const root = path2.resolve(process.cwd(), srcDir);
182
265
  const files = await glob("**/*.{tsx,jsx}", {
183
266
  cwd: root,
184
267
  absolute: true,
@@ -187,71 +270,12 @@ program.command("codemod").description("Apply deterministic JSX codemods to Edit
187
270
  const changed = [];
188
271
  for (const file of files) {
189
272
  const source = await fs.readFile(file, "utf8");
190
- const ast = parse(source, {
191
- sourceType: "module",
192
- plugins: ["typescript", "jsx"]
193
- });
194
- let touched = false;
195
- let needsEditableTextImport = false;
196
- traverse(ast, {
197
- JSXElement(pathNode) {
198
- const children = pathNode.node.children;
199
- const nonWhitespace = children.filter(
200
- (child) => !(t.isJSXText(child) && normalizeText(child.value) === "")
201
- );
202
- if (nonWhitespace.length !== 1 || !t.isJSXText(nonWhitespace[0])) {
203
- return;
204
- }
205
- const text = normalizeText(nonWhitespace[0].value);
206
- if (!text || text.length < 3) {
207
- return;
208
- }
209
- const loc = nonWhitespace[0].loc?.start.line ?? 0;
210
- const rel = path.relative(process.cwd(), file);
211
- const pathHint = defaultPathFor(rel, loc, "text");
212
- const editableEl = t.jsxElement(
213
- t.jsxOpeningElement(t.jsxIdentifier("EditableText"), [
214
- t.jsxAttribute(t.jsxIdentifier("path"), t.stringLiteral(pathHint)),
215
- t.jsxAttribute(t.jsxIdentifier("fallback"), t.stringLiteral(text))
216
- ], true),
217
- null,
218
- [],
219
- true
220
- );
221
- pathNode.node.children = [t.jsxExpressionContainer(editableEl)];
222
- touched = true;
223
- needsEditableTextImport = true;
224
- }
225
- });
226
- if (!touched) {
227
- continue;
228
- }
229
- if (needsEditableTextImport) {
230
- const body = ast.program.body;
231
- const hasImport = body.some(
232
- (node) => t.isImportDeclaration(node) && node.source.value === "@webmaster-droid/web" && node.specifiers.some(
233
- (specifier) => t.isImportSpecifier(specifier) && t.isIdentifier(specifier.imported) && specifier.imported.name === "EditableText"
234
- )
235
- );
236
- if (!hasImport) {
237
- body.unshift(
238
- t.importDeclaration(
239
- [
240
- t.importSpecifier(
241
- t.identifier("EditableText"),
242
- t.identifier("EditableText")
243
- )
244
- ],
245
- t.stringLiteral("@webmaster-droid/web")
246
- )
247
- );
248
- }
249
- }
250
- const next = generate(ast, { retainLines: true }, source).code;
251
- if (next === source) {
273
+ const transformed = transformEditableTextCodemod(source, file, process.cwd());
274
+ if (!transformed.changed) {
252
275
  continue;
253
276
  }
254
- const relFile = path.relative(process.cwd(), file);
277
+ const next = transformed.next;
278
+ const relFile = path2.relative(process.cwd(), file);
255
279
  changed.push({
256
280
  file: relFile,
257
281
  patch: createTwoFilesPatch(relFile, relFile, source, next)
@@ -260,7 +284,7 @@ program.command("codemod").description("Apply deterministic JSX codemods to Edit
260
284
  await fs.writeFile(file, next, "utf8");
261
285
  }
262
286
  }
263
- const output = path.resolve(process.cwd(), opts.out);
287
+ const output = path2.resolve(process.cwd(), opts.out);
264
288
  await ensureDir(output);
265
289
  await fs.writeFile(
266
290
  output,
@@ -286,7 +310,7 @@ program.command("doctor").description("Validate local environment for webmaster-
286
310
  issues.push(`Node.js 20+ required, found ${process.versions.node}`);
287
311
  }
288
312
  try {
289
- await fs.access(path.resolve(process.cwd(), "package.json"));
313
+ await fs.access(path2.resolve(process.cwd(), "package.json"));
290
314
  } catch {
291
315
  issues.push("package.json missing in current working directory");
292
316
  }
@@ -316,7 +340,7 @@ program.command("dev").description("Start project dev command (pass-through)").o
316
340
  var deploy = program.command("deploy").description("Deployment helpers");
317
341
  var aws = deploy.command("aws").description("Deploy AWS lambda bundle");
318
342
  aws.requiredOption("--entry <file>", "entry TypeScript file").requiredOption("--region <region>", "AWS region").requiredOption("--functions <names>", "comma-separated Lambda function names").option("--tmp-dir <dir>", "temp folder", "/tmp/webmaster-droid-deploy").action(async (opts) => {
319
- const entry = path.resolve(process.cwd(), opts.entry);
343
+ const entry = path2.resolve(process.cwd(), opts.entry);
320
344
  const tmpDir = opts.tmpDir;
321
345
  const functions = String(opts.functions).split(",").map((item) => item.trim()).filter(Boolean);
322
346
  const run = (cmd) => new Promise((resolve, reject) => {
@@ -339,15 +363,15 @@ aws.requiredOption("--entry <file>", "entry TypeScript file").requiredOption("--
339
363
  });
340
364
  var skill = program.command("skill").description("Skill helpers");
341
365
  skill.command("install").description("Install bundled conversion skill into CODEX_HOME").option("--codex-home <dir>", "CODEX_HOME path override").option("--force", "overwrite existing", false).action(async (opts) => {
342
- const cliDir = path.dirname(fileURLToPath(import.meta.url));
343
- const repoRoot = path.resolve(cliDir, "../../..");
344
- const sourceSkill = path.join(repoRoot, "skills", "webmaster-droid-convert");
366
+ const cliDir = path2.dirname(fileURLToPath(import.meta.url));
367
+ const repoRoot = path2.resolve(cliDir, "../../..");
368
+ const sourceSkill = path2.join(repoRoot, "skills", "webmaster-droid-convert");
345
369
  const codexHome = opts.codexHome || process.env.CODEX_HOME;
346
370
  if (!codexHome) {
347
371
  throw new Error("CODEX_HOME is not set. Provide --codex-home.");
348
372
  }
349
- const destination = path.join(codexHome, "skills", "webmaster-droid-convert");
350
- await ensureDir(path.join(destination, "SKILL.md"));
373
+ const destination = path2.join(codexHome, "skills", "webmaster-droid-convert");
374
+ await ensureDir(path2.join(destination, "SKILL.md"));
351
375
  try {
352
376
  await fs.access(destination);
353
377
  if (!opts.force) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webmaster-droid/cli",
3
- "version": "0.1.0-alpha.1",
3
+ "version": "0.1.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "webmaster-droid": "dist/index.js"
@@ -31,7 +31,8 @@
31
31
  },
32
32
  "scripts": {
33
33
  "build": "tsup src/index.ts --format esm --dts --clean",
34
- "typecheck": "tsc --noEmit"
34
+ "typecheck": "tsc --noEmit",
35
+ "test": "node ../../scripts/run-workspace-tests.mjs"
35
36
  },
36
37
  "description": "CLI for project bootstrap, schema, scan/codemod, deploy, and skill install.",
37
38
  "license": "MIT",