@webmaster-droid/cli 0.1.0-alpha.0 → 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 +121 -96
  2. package/package.json +15 -3
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 {
@@ -58,6 +141,7 @@ program.command("init").description("Initialize webmaster-droid config in curren
58
141
  "NEXT_PUBLIC_AGENT_API_BASE_URL=http://localhost:8787",
59
142
  "CMS_S3_BUCKET=",
60
143
  "CMS_S3_REGION=",
144
+ "CMS_PUBLIC_BASE_URL=https://your-domain.example",
61
145
  "SUPABASE_JWKS_URL=",
62
146
  "MODEL_OPENAI_ENABLED=true",
63
147
  "MODEL_GEMINI_ENABLED=true",
@@ -68,9 +152,9 @@ program.command("init").description("Initialize webmaster-droid config in curren
68
152
  console.log(`Created: ${envExample}`);
69
153
  }
70
154
  });
71
- var schema = program.command("schema").description("Schema helpers");
155
+ var schema = program.command("schema").description("Optional schema helpers");
72
156
  schema.command("init").description("Create starter schema file").option("--out <file>", "schema output", "cms/schema.webmaster.ts").action(async (opts) => {
73
- const outFile = path.resolve(process.cwd(), opts.out);
157
+ const outFile = path2.resolve(process.cwd(), opts.out);
74
158
  await ensureDir(outFile);
75
159
  const template = `export default {
76
160
  name: "webmaster-droid-schema",
@@ -88,8 +172,8 @@ schema.command("init").description("Create starter schema file").option("--out <
88
172
  }
89
173
  });
90
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) => {
91
- const input = path.resolve(process.cwd(), opts.input);
92
- 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);
93
177
  let manifest;
94
178
  if (input.endsWith(".json")) {
95
179
  manifest = await readJson(input);
@@ -110,7 +194,7 @@ schema.command("build").description("Compile schema file to runtime manifest JSO
110
194
  console.log(`Wrote manifest: ${output}`);
111
195
  });
112
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) => {
113
- const root = path.resolve(process.cwd(), srcDir);
197
+ const root = path2.resolve(process.cwd(), srcDir);
114
198
  const files = await glob("**/*.{ts,tsx,js,jsx}", {
115
199
  cwd: root,
116
200
  absolute: true,
@@ -119,11 +203,11 @@ program.command("scan").description("Scan source files for static content candid
119
203
  const findings = [];
120
204
  for (const file of files) {
121
205
  const code = await fs.readFile(file, "utf8");
122
- const ast = parse(code, {
206
+ const ast = parse2(code, {
123
207
  sourceType: "module",
124
208
  plugins: ["typescript", "jsx"]
125
209
  });
126
- traverse(ast, {
210
+ traverse2(ast, {
127
211
  JSXText(pathNode) {
128
212
  const text = normalizeText(pathNode.node.value);
129
213
  if (!text || text.length < 3) {
@@ -131,25 +215,25 @@ program.command("scan").description("Scan source files for static content candid
131
215
  }
132
216
  findings.push({
133
217
  type: "jsx-text",
134
- file: path.relative(process.cwd(), file),
218
+ file: path2.relative(process.cwd(), file),
135
219
  line: pathNode.node.loc?.start.line,
136
220
  column: pathNode.node.loc?.start.column,
137
221
  text
138
222
  });
139
223
  },
140
224
  JSXAttribute(pathNode) {
141
- const name = t.isJSXIdentifier(pathNode.node.name) ? pathNode.node.name.name : "";
225
+ const name = t2.isJSXIdentifier(pathNode.node.name) ? pathNode.node.name.name : "";
142
226
  if (!["src", "href", "alt", "title"].includes(name)) {
143
227
  return;
144
228
  }
145
229
  const valueNode = pathNode.node.value;
146
- if (!valueNode || !t.isStringLiteral(valueNode)) {
230
+ if (!valueNode || !t2.isStringLiteral(valueNode)) {
147
231
  return;
148
232
  }
149
233
  findings.push({
150
234
  type: "jsx-attr",
151
235
  attr: name,
152
- file: path.relative(process.cwd(), file),
236
+ file: path2.relative(process.cwd(), file),
153
237
  line: valueNode.loc?.start.line,
154
238
  column: valueNode.loc?.start.column,
155
239
  text: valueNode.value
@@ -157,7 +241,7 @@ program.command("scan").description("Scan source files for static content candid
157
241
  }
158
242
  });
159
243
  }
160
- const output = path.resolve(process.cwd(), opts.out);
244
+ const output = path2.resolve(process.cwd(), opts.out);
161
245
  await ensureDir(output);
162
246
  await fs.writeFile(
163
247
  output,
@@ -177,7 +261,7 @@ program.command("scan").description("Scan source files for static content candid
177
261
  console.log(`Scan complete. Findings: ${findings.length}. Report: ${output}`);
178
262
  });
179
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) => {
180
- const root = path.resolve(process.cwd(), srcDir);
264
+ const root = path2.resolve(process.cwd(), srcDir);
181
265
  const files = await glob("**/*.{tsx,jsx}", {
182
266
  cwd: root,
183
267
  absolute: true,
@@ -186,71 +270,12 @@ program.command("codemod").description("Apply deterministic JSX codemods to Edit
186
270
  const changed = [];
187
271
  for (const file of files) {
188
272
  const source = await fs.readFile(file, "utf8");
189
- const ast = parse(source, {
190
- sourceType: "module",
191
- plugins: ["typescript", "jsx"]
192
- });
193
- let touched = false;
194
- let needsEditableTextImport = false;
195
- traverse(ast, {
196
- JSXElement(pathNode) {
197
- const children = pathNode.node.children;
198
- const nonWhitespace = children.filter(
199
- (child) => !(t.isJSXText(child) && normalizeText(child.value) === "")
200
- );
201
- if (nonWhitespace.length !== 1 || !t.isJSXText(nonWhitespace[0])) {
202
- return;
203
- }
204
- const text = normalizeText(nonWhitespace[0].value);
205
- if (!text || text.length < 3) {
206
- return;
207
- }
208
- const loc = nonWhitespace[0].loc?.start.line ?? 0;
209
- const rel = path.relative(process.cwd(), file);
210
- const pathHint = defaultPathFor(rel, loc, "text");
211
- const editableEl = t.jsxElement(
212
- t.jsxOpeningElement(t.jsxIdentifier("EditableText"), [
213
- t.jsxAttribute(t.jsxIdentifier("path"), t.stringLiteral(pathHint)),
214
- t.jsxAttribute(t.jsxIdentifier("fallback"), t.stringLiteral(text))
215
- ], true),
216
- null,
217
- [],
218
- true
219
- );
220
- pathNode.node.children = [t.jsxExpressionContainer(editableEl)];
221
- touched = true;
222
- needsEditableTextImport = true;
223
- }
224
- });
225
- if (!touched) {
226
- continue;
227
- }
228
- if (needsEditableTextImport) {
229
- const body = ast.program.body;
230
- const hasImport = body.some(
231
- (node) => t.isImportDeclaration(node) && node.source.value === "@webmaster-droid/react" && node.specifiers.some(
232
- (specifier) => t.isImportSpecifier(specifier) && t.isIdentifier(specifier.imported) && specifier.imported.name === "EditableText"
233
- )
234
- );
235
- if (!hasImport) {
236
- body.unshift(
237
- t.importDeclaration(
238
- [
239
- t.importSpecifier(
240
- t.identifier("EditableText"),
241
- t.identifier("EditableText")
242
- )
243
- ],
244
- t.stringLiteral("@webmaster-droid/react")
245
- )
246
- );
247
- }
248
- }
249
- const next = generate(ast, { retainLines: true }, source).code;
250
- if (next === source) {
273
+ const transformed = transformEditableTextCodemod(source, file, process.cwd());
274
+ if (!transformed.changed) {
251
275
  continue;
252
276
  }
253
- const relFile = path.relative(process.cwd(), file);
277
+ const next = transformed.next;
278
+ const relFile = path2.relative(process.cwd(), file);
254
279
  changed.push({
255
280
  file: relFile,
256
281
  patch: createTwoFilesPatch(relFile, relFile, source, next)
@@ -259,7 +284,7 @@ program.command("codemod").description("Apply deterministic JSX codemods to Edit
259
284
  await fs.writeFile(file, next, "utf8");
260
285
  }
261
286
  }
262
- const output = path.resolve(process.cwd(), opts.out);
287
+ const output = path2.resolve(process.cwd(), opts.out);
263
288
  await ensureDir(output);
264
289
  await fs.writeFile(
265
290
  output,
@@ -285,7 +310,7 @@ program.command("doctor").description("Validate local environment for webmaster-
285
310
  issues.push(`Node.js 20+ required, found ${process.versions.node}`);
286
311
  }
287
312
  try {
288
- await fs.access(path.resolve(process.cwd(), "package.json"));
313
+ await fs.access(path2.resolve(process.cwd(), "package.json"));
289
314
  } catch {
290
315
  issues.push("package.json missing in current working directory");
291
316
  }
@@ -315,7 +340,7 @@ program.command("dev").description("Start project dev command (pass-through)").o
315
340
  var deploy = program.command("deploy").description("Deployment helpers");
316
341
  var aws = deploy.command("aws").description("Deploy AWS lambda bundle");
317
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) => {
318
- const entry = path.resolve(process.cwd(), opts.entry);
343
+ const entry = path2.resolve(process.cwd(), opts.entry);
319
344
  const tmpDir = opts.tmpDir;
320
345
  const functions = String(opts.functions).split(",").map((item) => item.trim()).filter(Boolean);
321
346
  const run = (cmd) => new Promise((resolve, reject) => {
@@ -338,15 +363,15 @@ aws.requiredOption("--entry <file>", "entry TypeScript file").requiredOption("--
338
363
  });
339
364
  var skill = program.command("skill").description("Skill helpers");
340
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) => {
341
- const cliDir = path.dirname(fileURLToPath(import.meta.url));
342
- const repoRoot = path.resolve(cliDir, "../../..");
343
- 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");
344
369
  const codexHome = opts.codexHome || process.env.CODEX_HOME;
345
370
  if (!codexHome) {
346
371
  throw new Error("CODEX_HOME is not set. Provide --codex-home.");
347
372
  }
348
- const destination = path.join(codexHome, "skills", "webmaster-droid-convert");
349
- 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"));
350
375
  try {
351
376
  await fs.access(destination);
352
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.0",
3
+ "version": "0.1.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "webmaster-droid": "dist/index.js"
@@ -31,6 +31,18 @@
31
31
  },
32
32
  "scripts": {
33
33
  "build": "tsup src/index.ts --format esm --dts --clean",
34
- "typecheck": "tsc --noEmit"
35
- }
34
+ "typecheck": "tsc --noEmit",
35
+ "test": "node ../../scripts/run-workspace-tests.mjs"
36
+ },
37
+ "description": "CLI for project bootstrap, schema, scan/codemod, deploy, and skill install.",
38
+ "license": "MIT",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/Slava-AV/webmaster-droid.git",
42
+ "directory": "packages/cli"
43
+ },
44
+ "bugs": {
45
+ "url": "https://github.com/Slava-AV/webmaster-droid/issues"
46
+ },
47
+ "homepage": "https://github.com/Slava-AV/webmaster-droid#readme"
36
48
  }