@webmaster-droid/cli 0.1.0-alpha.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.
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,364 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { promises as fs } from "fs";
5
+ import path from "path";
6
+ import process from "process";
7
+ import { spawn } from "child_process";
8
+ import { fileURLToPath } from "url";
9
+ import { Command } from "commander";
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";
15
+ import { createTwoFilesPatch } from "diff";
16
+ import { createJiti } from "jiti";
17
+ var program = new Command();
18
+ function normalizeText(text) {
19
+ return text.replace(/\s+/g, " ").trim();
20
+ }
21
+ function defaultPathFor(file, line, kind) {
22
+ const stem = file.replace(/\\/g, "/").replace(/^\//, "").replace(/\.[tj]sx?$/, "").replace(/[^a-zA-Z0-9/]+/g, "-").replace(/\//g, ".");
23
+ return `pages.todo.${stem}.${kind}.${line}`;
24
+ }
25
+ async function ensureDir(filePath) {
26
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
27
+ }
28
+ async function readJson(filePath) {
29
+ const raw = await fs.readFile(filePath, "utf8");
30
+ return JSON.parse(raw);
31
+ }
32
+ 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");
36
+ await ensureDir(configPath);
37
+ const config = `export default {
38
+ framework: "${opts.framework}",
39
+ backend: "${opts.backend}",
40
+ schema: "./cms/schema.webmaster.ts",
41
+ apiBaseUrlEnv: "NEXT_PUBLIC_AGENT_API_BASE_URL"
42
+ };
43
+ `;
44
+ try {
45
+ await fs.access(configPath);
46
+ console.log(`Config already exists: ${configPath}`);
47
+ } catch {
48
+ await fs.writeFile(configPath, config, "utf8");
49
+ console.log(`Created: ${configPath}`);
50
+ }
51
+ const envExample = path.join(outDir, ".env.webmaster-droid.example");
52
+ try {
53
+ await fs.access(envExample);
54
+ } catch {
55
+ await fs.writeFile(
56
+ envExample,
57
+ [
58
+ "NEXT_PUBLIC_AGENT_API_BASE_URL=http://localhost:8787",
59
+ "CMS_S3_BUCKET=",
60
+ "CMS_S3_REGION=",
61
+ "SUPABASE_JWKS_URL=",
62
+ "MODEL_OPENAI_ENABLED=true",
63
+ "MODEL_GEMINI_ENABLED=true",
64
+ "DEFAULT_MODEL_ID=openai:gpt-5.2"
65
+ ].join("\n") + "\n",
66
+ "utf8"
67
+ );
68
+ console.log(`Created: ${envExample}`);
69
+ }
70
+ });
71
+ var schema = program.command("schema").description("Schema helpers");
72
+ 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);
74
+ await ensureDir(outFile);
75
+ const template = `export default {
76
+ name: "webmaster-droid-schema",
77
+ version: 1,
78
+ editablePathPrefixes: ["pages.", "layout.", "seo.", "themeTokens."],
79
+ notes: "Adjust this schema to your website model"
80
+ };
81
+ `;
82
+ try {
83
+ await fs.access(outFile);
84
+ console.log(`Schema already exists: ${outFile}`);
85
+ } catch {
86
+ await fs.writeFile(outFile, template, "utf8");
87
+ console.log(`Created: ${outFile}`);
88
+ }
89
+ });
90
+ 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);
93
+ let manifest;
94
+ if (input.endsWith(".json")) {
95
+ manifest = await readJson(input);
96
+ } else {
97
+ const jiti = createJiti(import.meta.url);
98
+ const loaded = await jiti.import(input);
99
+ manifest = loaded.default ?? loaded.schema ?? loaded;
100
+ }
101
+ if (!manifest || typeof manifest !== "object") {
102
+ throw new Error("Schema manifest must be an object.");
103
+ }
104
+ const prefixes = manifest.editablePathPrefixes ?? [];
105
+ if (!Array.isArray(prefixes) || prefixes.some((value) => typeof value !== "string")) {
106
+ throw new Error("manifest.editablePathPrefixes must be a string array.");
107
+ }
108
+ await ensureDir(output);
109
+ await fs.writeFile(output, JSON.stringify(manifest, null, 2) + "\n", "utf8");
110
+ console.log(`Wrote manifest: ${output}`);
111
+ });
112
+ 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);
114
+ const files = await glob("**/*.{ts,tsx,js,jsx}", {
115
+ cwd: root,
116
+ absolute: true,
117
+ ignore: ["**/*.d.ts", "**/node_modules/**", "**/.next/**", "**/dist/**"]
118
+ });
119
+ const findings = [];
120
+ for (const file of files) {
121
+ const code = await fs.readFile(file, "utf8");
122
+ const ast = parse(code, {
123
+ sourceType: "module",
124
+ plugins: ["typescript", "jsx"]
125
+ });
126
+ traverse(ast, {
127
+ JSXText(pathNode) {
128
+ const text = normalizeText(pathNode.node.value);
129
+ if (!text || text.length < 3) {
130
+ return;
131
+ }
132
+ findings.push({
133
+ type: "jsx-text",
134
+ file: path.relative(process.cwd(), file),
135
+ line: pathNode.node.loc?.start.line,
136
+ column: pathNode.node.loc?.start.column,
137
+ text
138
+ });
139
+ },
140
+ JSXAttribute(pathNode) {
141
+ const name = t.isJSXIdentifier(pathNode.node.name) ? pathNode.node.name.name : "";
142
+ if (!["src", "href", "alt", "title"].includes(name)) {
143
+ return;
144
+ }
145
+ const valueNode = pathNode.node.value;
146
+ if (!valueNode || !t.isStringLiteral(valueNode)) {
147
+ return;
148
+ }
149
+ findings.push({
150
+ type: "jsx-attr",
151
+ attr: name,
152
+ file: path.relative(process.cwd(), file),
153
+ line: valueNode.loc?.start.line,
154
+ column: valueNode.loc?.start.column,
155
+ text: valueNode.value
156
+ });
157
+ }
158
+ });
159
+ }
160
+ const output = path.resolve(process.cwd(), opts.out);
161
+ await ensureDir(output);
162
+ await fs.writeFile(
163
+ output,
164
+ JSON.stringify(
165
+ {
166
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
167
+ source: root,
168
+ totalFiles: files.length,
169
+ totalFindings: findings.length,
170
+ findings
171
+ },
172
+ null,
173
+ 2
174
+ ) + "\n",
175
+ "utf8"
176
+ );
177
+ console.log(`Scan complete. Findings: ${findings.length}. Report: ${output}`);
178
+ });
179
+ 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);
181
+ const files = await glob("**/*.{tsx,jsx}", {
182
+ cwd: root,
183
+ absolute: true,
184
+ ignore: ["**/node_modules/**", "**/.next/**", "**/dist/**"]
185
+ });
186
+ const changed = [];
187
+ for (const file of files) {
188
+ 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) {
251
+ continue;
252
+ }
253
+ const relFile = path.relative(process.cwd(), file);
254
+ changed.push({
255
+ file: relFile,
256
+ patch: createTwoFilesPatch(relFile, relFile, source, next)
257
+ });
258
+ if (opts.apply) {
259
+ await fs.writeFile(file, next, "utf8");
260
+ }
261
+ }
262
+ const output = path.resolve(process.cwd(), opts.out);
263
+ await ensureDir(output);
264
+ await fs.writeFile(
265
+ output,
266
+ JSON.stringify(
267
+ {
268
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
269
+ source: root,
270
+ apply: Boolean(opts.apply),
271
+ changedFiles: changed.length,
272
+ changes: changed
273
+ },
274
+ null,
275
+ 2
276
+ ) + "\n",
277
+ "utf8"
278
+ );
279
+ console.log(`${opts.apply ? "Applied" : "Previewed"} codemod changes: ${changed.length}. Report: ${output}`);
280
+ });
281
+ program.command("doctor").description("Validate local environment for webmaster-droid").action(async () => {
282
+ const issues = [];
283
+ const major = Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
284
+ if (!Number.isFinite(major) || major < 20) {
285
+ issues.push(`Node.js 20+ required, found ${process.versions.node}`);
286
+ }
287
+ try {
288
+ await fs.access(path.resolve(process.cwd(), "package.json"));
289
+ } catch {
290
+ issues.push("package.json missing in current working directory");
291
+ }
292
+ if (issues.length > 0) {
293
+ console.error("Doctor found issues:");
294
+ for (const issue of issues) {
295
+ console.error(`- ${issue}`);
296
+ }
297
+ process.exitCode = 1;
298
+ return;
299
+ }
300
+ console.log("Doctor checks passed.");
301
+ });
302
+ program.command("dev").description("Start project dev command (pass-through)").option("--cmd <command>", "command to run", "npm run dev").action(async (opts) => {
303
+ const [bin, ...args] = opts.cmd.split(" ");
304
+ await new Promise((resolve, reject) => {
305
+ const child = spawn(bin, args, { stdio: "inherit", shell: true });
306
+ child.on("exit", (code) => {
307
+ if (code === 0) {
308
+ resolve();
309
+ return;
310
+ }
311
+ reject(new Error(`Command failed with code ${code ?? "unknown"}`));
312
+ });
313
+ });
314
+ });
315
+ var deploy = program.command("deploy").description("Deployment helpers");
316
+ var aws = deploy.command("aws").description("Deploy AWS lambda bundle");
317
+ 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);
319
+ const tmpDir = opts.tmpDir;
320
+ const functions = String(opts.functions).split(",").map((item) => item.trim()).filter(Boolean);
321
+ const run = (cmd) => new Promise((resolve, reject) => {
322
+ const child = spawn(cmd, { stdio: "inherit", shell: true });
323
+ child.on("exit", (code) => {
324
+ if (code === 0) {
325
+ resolve();
326
+ } else {
327
+ reject(new Error(`Command failed: ${cmd}`));
328
+ }
329
+ });
330
+ });
331
+ await run(`rm -rf ${tmpDir} && mkdir -p ${tmpDir}`);
332
+ await run(`npx esbuild ${entry} --bundle --platform=node --target=node20 --format=cjs --outfile=${tmpDir}/index.js`);
333
+ await run(`cd ${tmpDir} && zip -q lambda.zip index.js`);
334
+ for (const fn of functions) {
335
+ await run(`aws lambda update-function-code --region ${opts.region} --function-name ${fn} --zip-file fileb://${tmpDir}/lambda.zip >/dev/null`);
336
+ await run(`aws lambda wait function-updated --region ${opts.region} --function-name ${fn}`);
337
+ }
338
+ });
339
+ var skill = program.command("skill").description("Skill helpers");
340
+ 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");
344
+ const codexHome = opts.codexHome || process.env.CODEX_HOME;
345
+ if (!codexHome) {
346
+ throw new Error("CODEX_HOME is not set. Provide --codex-home.");
347
+ }
348
+ const destination = path.join(codexHome, "skills", "webmaster-droid-convert");
349
+ await ensureDir(path.join(destination, "SKILL.md"));
350
+ try {
351
+ await fs.access(destination);
352
+ if (!opts.force) {
353
+ throw new Error(`Skill already exists at ${destination}. Use --force to overwrite.`);
354
+ }
355
+ await fs.rm(destination, { recursive: true, force: true });
356
+ } catch {
357
+ }
358
+ await fs.cp(sourceSkill, destination, { recursive: true });
359
+ console.log(`Installed skill to ${destination}`);
360
+ });
361
+ program.parseAsync(process.argv).catch((error) => {
362
+ console.error(error instanceof Error ? error.message : String(error));
363
+ process.exit(1);
364
+ });
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@webmaster-droid/cli",
3
+ "version": "0.1.0-alpha.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "webmaster-droid": "dist/index.js"
7
+ },
8
+ "main": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "dependencies": {
23
+ "@babel/generator": "^7.28.0",
24
+ "@babel/parser": "^7.28.0",
25
+ "@babel/traverse": "^7.28.0",
26
+ "@babel/types": "^7.28.0",
27
+ "commander": "^14.0.1",
28
+ "diff": "^8.0.2",
29
+ "glob": "^11.0.3",
30
+ "jiti": "^2.6.1"
31
+ },
32
+ "scripts": {
33
+ "build": "tsup src/index.ts --format esm --dts --clean",
34
+ "typecheck": "tsc --noEmit"
35
+ }
36
+ }