@webmaster-droid/cli 0.1.0-alpha.1 → 0.2.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 +352 -184
  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 traverseModule 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,116 @@ 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();
109
+ var CLI_VERSION = "0.1.0-alpha.0";
110
+ var traverse2 = traverseModule.default ?? traverseModule;
111
+ function emitCliEnvelope(payload, isError = false) {
112
+ const out = JSON.stringify(payload, null, 2);
113
+ if (isError) {
114
+ console.error(out);
115
+ return;
116
+ }
117
+ console.log(out);
118
+ }
119
+ function errorToMessage(error) {
120
+ return error instanceof Error ? error.message : String(error);
121
+ }
25
122
  async function ensureDir(filePath) {
26
- await fs.mkdir(path.dirname(filePath), { recursive: true });
123
+ await fs.mkdir(path2.dirname(filePath), { recursive: true });
27
124
  }
28
125
  async function readJson(filePath) {
29
126
  const raw = await fs.readFile(filePath, "utf8");
30
127
  return JSON.parse(raw);
31
128
  }
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");
129
+ program.name("webmaster-droid").description("Webmaster Droid CLI").version(CLI_VERSION);
130
+ program.command("init").description("Initialize optional webmaster-droid config in current project").option("--framework <framework>", "framework", "next").option("--backend <backend>", "backend (supabase|aws)", "supabase").option("--out <dir>", "output dir", ".").action(async (opts) => {
131
+ const backendRaw = String(opts.backend ?? "supabase").trim().toLowerCase();
132
+ if (backendRaw !== "supabase" && backendRaw !== "aws") {
133
+ throw new Error(`Unsupported backend '${opts.backend}'. Expected 'supabase' or 'aws'.`);
134
+ }
135
+ const backend = backendRaw;
136
+ const outDir = path2.resolve(process.cwd(), opts.out);
137
+ const configPath = path2.join(outDir, "webmaster-droid.config.ts");
36
138
  await ensureDir(configPath);
37
139
  const config = `export default {
38
140
  framework: "${opts.framework}",
39
- backend: "${opts.backend}",
40
- schema: "./cms/schema.webmaster.ts",
141
+ backend: "${backend}",
41
142
  apiBaseUrlEnv: "NEXT_PUBLIC_AGENT_API_BASE_URL"
42
143
  };
43
144
  `;
@@ -48,7 +149,7 @@ program.command("init").description("Initialize webmaster-droid config in curren
48
149
  await fs.writeFile(configPath, config, "utf8");
49
150
  console.log(`Created: ${configPath}`);
50
151
  }
51
- const envExample = path.join(outDir, ".env.webmaster-droid.example");
152
+ const envExample = path2.join(outDir, ".env.webmaster-droid.example");
52
153
  try {
53
154
  await fs.access(envExample);
54
155
  } catch {
@@ -56,22 +157,35 @@ program.command("init").description("Initialize webmaster-droid config in curren
56
157
  envExample,
57
158
  [
58
159
  "NEXT_PUBLIC_AGENT_API_BASE_URL=http://localhost:8787",
59
- "CMS_S3_BUCKET=",
60
- "CMS_S3_REGION=",
61
- "CMS_PUBLIC_BASE_URL=https://your-domain.example",
160
+ "",
161
+ "# Supabase (default backend)",
162
+ "NEXT_PUBLIC_SUPABASE_URL=",
163
+ "NEXT_PUBLIC_SUPABASE_ANON_KEY=",
164
+ "SUPABASE_URL=",
165
+ "SUPABASE_ANON_KEY=",
166
+ "SUPABASE_SERVICE_ROLE_KEY=",
62
167
  "SUPABASE_JWKS_URL=",
168
+ "CMS_SUPABASE_BUCKET=webmaster-droid-cms",
169
+ "CMS_STORAGE_PREFIX=cms",
170
+ "",
171
+ "# Shared runtime",
172
+ "CMS_PUBLIC_BASE_URL=https://your-domain.example",
63
173
  "MODEL_OPENAI_ENABLED=true",
64
174
  "MODEL_GEMINI_ENABLED=true",
65
- "DEFAULT_MODEL_ID=openai:gpt-5.2"
175
+ "DEFAULT_MODEL_ID=openai:gpt-5.2",
176
+ "",
177
+ "# AWS (optional backend)",
178
+ "CMS_S3_BUCKET=",
179
+ "CMS_S3_REGION="
66
180
  ].join("\n") + "\n",
67
181
  "utf8"
68
182
  );
69
183
  console.log(`Created: ${envExample}`);
70
184
  }
71
185
  });
72
- var schema = program.command("schema").description("Schema helpers");
186
+ var schema = program.command("schema").description("Optional schema helpers");
73
187
  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);
188
+ const outFile = path2.resolve(process.cwd(), opts.out);
75
189
  await ensureDir(outFile);
76
190
  const template = `export default {
77
191
  name: "webmaster-droid-schema",
@@ -89,8 +203,8 @@ schema.command("init").description("Create starter schema file").option("--out <
89
203
  }
90
204
  });
91
205
  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);
206
+ const input = path2.resolve(process.cwd(), opts.input);
207
+ const output = path2.resolve(process.cwd(), opts.output);
94
208
  let manifest;
95
209
  if (input.endsWith(".json")) {
96
210
  manifest = await readJson(input);
@@ -110,187 +224,197 @@ schema.command("build").description("Compile schema file to runtime manifest JSO
110
224
  await fs.writeFile(output, JSON.stringify(manifest, null, 2) + "\n", "utf8");
111
225
  console.log(`Wrote manifest: ${output}`);
112
226
  });
113
- 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);
115
- const files = await glob("**/*.{ts,tsx,js,jsx}", {
116
- cwd: root,
117
- absolute: true,
118
- ignore: ["**/*.d.ts", "**/node_modules/**", "**/.next/**", "**/dist/**"]
119
- });
120
- const findings = [];
121
- for (const file of files) {
122
- const code = await fs.readFile(file, "utf8");
123
- const ast = parse(code, {
124
- sourceType: "module",
125
- plugins: ["typescript", "jsx"]
227
+ 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) => {
228
+ try {
229
+ const root = path2.resolve(process.cwd(), srcDir);
230
+ const files = await glob("**/*.{ts,tsx,js,jsx}", {
231
+ cwd: root,
232
+ absolute: true,
233
+ ignore: ["**/*.d.ts", "**/node_modules/**", "**/.next/**", "**/dist/**"]
126
234
  });
127
- traverse(ast, {
128
- JSXText(pathNode) {
129
- const text = normalizeText(pathNode.node.value);
130
- if (!text || text.length < 3) {
131
- return;
132
- }
133
- findings.push({
134
- type: "jsx-text",
135
- file: path.relative(process.cwd(), file),
136
- line: pathNode.node.loc?.start.line,
137
- column: pathNode.node.loc?.start.column,
138
- text
139
- });
140
- },
141
- JSXAttribute(pathNode) {
142
- const name = t.isJSXIdentifier(pathNode.node.name) ? pathNode.node.name.name : "";
143
- if (!["src", "href", "alt", "title"].includes(name)) {
144
- return;
235
+ const findings = [];
236
+ for (const file of files) {
237
+ const code = await fs.readFile(file, "utf8");
238
+ const ast = parse2(code, {
239
+ sourceType: "module",
240
+ plugins: ["typescript", "jsx"]
241
+ });
242
+ traverse2(ast, {
243
+ JSXText(pathNode) {
244
+ const text = normalizeText(pathNode.node.value);
245
+ if (!text || text.length < 3) {
246
+ return;
247
+ }
248
+ findings.push({
249
+ type: "jsx-text",
250
+ file: path2.relative(process.cwd(), file),
251
+ line: pathNode.node.loc?.start.line,
252
+ column: pathNode.node.loc?.start.column,
253
+ text
254
+ });
255
+ },
256
+ JSXAttribute(pathNode) {
257
+ const name = t2.isJSXIdentifier(pathNode.node.name) ? pathNode.node.name.name : "";
258
+ if (!["src", "href", "alt", "title"].includes(name)) {
259
+ return;
260
+ }
261
+ const valueNode = pathNode.node.value;
262
+ if (!valueNode || !t2.isStringLiteral(valueNode)) {
263
+ return;
264
+ }
265
+ findings.push({
266
+ type: "jsx-attr",
267
+ attr: name,
268
+ file: path2.relative(process.cwd(), file),
269
+ line: valueNode.loc?.start.line,
270
+ column: valueNode.loc?.start.column,
271
+ text: valueNode.value
272
+ });
145
273
  }
146
- const valueNode = pathNode.node.value;
147
- if (!valueNode || !t.isStringLiteral(valueNode)) {
148
- return;
274
+ });
275
+ }
276
+ const output = path2.resolve(process.cwd(), opts.out);
277
+ const report = {
278
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
279
+ source: root,
280
+ totalFiles: files.length,
281
+ totalFindings: findings.length,
282
+ findings
283
+ };
284
+ await ensureDir(output);
285
+ await fs.writeFile(output, JSON.stringify(report, null, 2) + "\n", "utf8");
286
+ if (opts.json) {
287
+ emitCliEnvelope({
288
+ ok: true,
289
+ command: "scan",
290
+ version: CLI_VERSION,
291
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
292
+ data: {
293
+ reportPath: output,
294
+ source: root,
295
+ totalFiles: files.length,
296
+ totalFindings: findings.length
149
297
  }
150
- findings.push({
151
- type: "jsx-attr",
152
- attr: name,
153
- file: path.relative(process.cwd(), file),
154
- line: valueNode.loc?.start.line,
155
- column: valueNode.loc?.start.column,
156
- text: valueNode.value
157
- });
158
- }
159
- });
160
- }
161
- const output = path.resolve(process.cwd(), opts.out);
162
- await ensureDir(output);
163
- await fs.writeFile(
164
- output,
165
- JSON.stringify(
298
+ });
299
+ return;
300
+ }
301
+ console.log(`Scan complete. Findings: ${findings.length}. Report: ${output}`);
302
+ } catch (error) {
303
+ if (!opts.json) {
304
+ throw error;
305
+ }
306
+ emitCliEnvelope(
166
307
  {
167
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
168
- source: root,
169
- totalFiles: files.length,
170
- totalFindings: findings.length,
171
- findings
308
+ ok: false,
309
+ command: "scan",
310
+ version: CLI_VERSION,
311
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
312
+ errors: [errorToMessage(error)]
172
313
  },
173
- null,
174
- 2
175
- ) + "\n",
176
- "utf8"
177
- );
178
- console.log(`Scan complete. Findings: ${findings.length}. Report: ${output}`);
314
+ true
315
+ );
316
+ process.exitCode = 1;
317
+ }
179
318
  });
180
- 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);
182
- const files = await glob("**/*.{tsx,jsx}", {
183
- cwd: root,
184
- absolute: true,
185
- ignore: ["**/node_modules/**", "**/.next/**", "**/dist/**"]
186
- });
187
- const changed = [];
188
- for (const file of files) {
189
- const source = await fs.readFile(file, "utf8");
190
- const ast = parse(source, {
191
- sourceType: "module",
192
- plugins: ["typescript", "jsx"]
319
+ 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").option("--json", "emit machine-readable JSON output", false).action(async (srcDir, opts) => {
320
+ try {
321
+ const root = path2.resolve(process.cwd(), srcDir);
322
+ const files = await glob("**/*.{tsx,jsx}", {
323
+ cwd: root,
324
+ absolute: true,
325
+ ignore: ["**/node_modules/**", "**/.next/**", "**/dist/**"]
193
326
  });
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;
327
+ const changed = [];
328
+ for (const file of files) {
329
+ const source = await fs.readFile(file, "utf8");
330
+ const transformed = transformEditableTextCodemod(source, file, process.cwd());
331
+ if (!transformed.changed) {
332
+ continue;
224
333
  }
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
- );
334
+ const next = transformed.next;
335
+ const relFile = path2.relative(process.cwd(), file);
336
+ changed.push({
337
+ file: relFile,
338
+ patch: createTwoFilesPatch(relFile, relFile, source, next)
339
+ });
340
+ if (opts.apply) {
341
+ await fs.writeFile(file, next, "utf8");
248
342
  }
249
343
  }
250
- const next = generate(ast, { retainLines: true }, source).code;
251
- if (next === source) {
252
- continue;
344
+ const output = path2.resolve(process.cwd(), opts.out);
345
+ const report = {
346
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
347
+ source: root,
348
+ apply: Boolean(opts.apply),
349
+ changedFiles: changed.length,
350
+ changes: changed
351
+ };
352
+ await ensureDir(output);
353
+ await fs.writeFile(output, JSON.stringify(report, null, 2) + "\n", "utf8");
354
+ if (opts.json) {
355
+ emitCliEnvelope({
356
+ ok: true,
357
+ command: "codemod",
358
+ version: CLI_VERSION,
359
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
360
+ data: {
361
+ reportPath: output,
362
+ source: root,
363
+ apply: Boolean(opts.apply),
364
+ changedFiles: changed.length
365
+ }
366
+ });
367
+ return;
253
368
  }
254
- const relFile = path.relative(process.cwd(), file);
255
- changed.push({
256
- file: relFile,
257
- patch: createTwoFilesPatch(relFile, relFile, source, next)
258
- });
259
- if (opts.apply) {
260
- await fs.writeFile(file, next, "utf8");
369
+ console.log(
370
+ `${opts.apply ? "Applied" : "Previewed"} codemod changes: ${changed.length}. Report: ${output}`
371
+ );
372
+ } catch (error) {
373
+ if (!opts.json) {
374
+ throw error;
261
375
  }
262
- }
263
- const output = path.resolve(process.cwd(), opts.out);
264
- await ensureDir(output);
265
- await fs.writeFile(
266
- output,
267
- JSON.stringify(
376
+ emitCliEnvelope(
268
377
  {
269
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
270
- source: root,
271
- apply: Boolean(opts.apply),
272
- changedFiles: changed.length,
273
- changes: changed
378
+ ok: false,
379
+ command: "codemod",
380
+ version: CLI_VERSION,
381
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
382
+ errors: [errorToMessage(error)]
274
383
  },
275
- null,
276
- 2
277
- ) + "\n",
278
- "utf8"
279
- );
280
- console.log(`${opts.apply ? "Applied" : "Previewed"} codemod changes: ${changed.length}. Report: ${output}`);
384
+ true
385
+ );
386
+ process.exitCode = 1;
387
+ }
281
388
  });
282
- program.command("doctor").description("Validate local environment for webmaster-droid").action(async () => {
389
+ program.command("doctor").description("Validate local environment for webmaster-droid").option("--json", "emit machine-readable JSON output", false).action(async (opts) => {
283
390
  const issues = [];
284
391
  const major = Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
285
392
  if (!Number.isFinite(major) || major < 20) {
286
393
  issues.push(`Node.js 20+ required, found ${process.versions.node}`);
287
394
  }
288
395
  try {
289
- await fs.access(path.resolve(process.cwd(), "package.json"));
396
+ await fs.access(path2.resolve(process.cwd(), "package.json"));
290
397
  } catch {
291
398
  issues.push("package.json missing in current working directory");
292
399
  }
293
400
  if (issues.length > 0) {
401
+ if (opts.json) {
402
+ emitCliEnvelope(
403
+ {
404
+ ok: false,
405
+ command: "doctor",
406
+ version: CLI_VERSION,
407
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
408
+ data: {
409
+ checksPassed: false
410
+ },
411
+ errors: issues
412
+ },
413
+ true
414
+ );
415
+ process.exitCode = 1;
416
+ return;
417
+ }
294
418
  console.error("Doctor found issues:");
295
419
  for (const issue of issues) {
296
420
  console.error(`- ${issue}`);
@@ -298,6 +422,18 @@ program.command("doctor").description("Validate local environment for webmaster-
298
422
  process.exitCode = 1;
299
423
  return;
300
424
  }
425
+ if (opts.json) {
426
+ emitCliEnvelope({
427
+ ok: true,
428
+ command: "doctor",
429
+ version: CLI_VERSION,
430
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
431
+ data: {
432
+ checksPassed: true
433
+ }
434
+ });
435
+ return;
436
+ }
301
437
  console.log("Doctor checks passed.");
302
438
  });
303
439
  program.command("dev").description("Start project dev command (pass-through)").option("--cmd <command>", "command to run", "npm run dev").action(async (opts) => {
@@ -316,7 +452,7 @@ program.command("dev").description("Start project dev command (pass-through)").o
316
452
  var deploy = program.command("deploy").description("Deployment helpers");
317
453
  var aws = deploy.command("aws").description("Deploy AWS lambda bundle");
318
454
  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);
455
+ const entry = path2.resolve(process.cwd(), opts.entry);
320
456
  const tmpDir = opts.tmpDir;
321
457
  const functions = String(opts.functions).split(",").map((item) => item.trim()).filter(Boolean);
322
458
  const run = (cmd) => new Promise((resolve, reject) => {
@@ -337,17 +473,49 @@ aws.requiredOption("--entry <file>", "entry TypeScript file").requiredOption("--
337
473
  await run(`aws lambda wait function-updated --region ${opts.region} --function-name ${fn}`);
338
474
  }
339
475
  });
476
+ var supabase = deploy.command("supabase").description("Deploy Supabase edge functions");
477
+ supabase.requiredOption("--project-ref <ref>", "Supabase project reference").requiredOption("--functions <names>", "comma-separated function names").option("--env-file <path>", "path to env file for function deployment").option("--no-verify-jwt", "disable JWT verification for deployed functions").action(async (opts) => {
478
+ const functions = String(opts.functions).split(",").map((item) => item.trim()).filter(Boolean);
479
+ if (functions.length === 0) {
480
+ throw new Error("No function names provided.");
481
+ }
482
+ const run = (cmd) => new Promise((resolve, reject) => {
483
+ const child = spawn(cmd, { stdio: "inherit", shell: true });
484
+ child.on("exit", (code) => {
485
+ if (code === 0) {
486
+ resolve();
487
+ } else {
488
+ reject(new Error(`Command failed: ${cmd}`));
489
+ }
490
+ });
491
+ });
492
+ for (const fn of functions) {
493
+ const parts = [
494
+ "supabase functions deploy",
495
+ fn,
496
+ "--project-ref",
497
+ opts.projectRef
498
+ ];
499
+ if (opts.envFile) {
500
+ parts.push("--env-file", opts.envFile);
501
+ }
502
+ if (opts.verifyJwt === false) {
503
+ parts.push("--no-verify-jwt");
504
+ }
505
+ await run(parts.join(" "));
506
+ }
507
+ });
340
508
  var skill = program.command("skill").description("Skill helpers");
341
509
  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");
510
+ const cliDir = path2.dirname(fileURLToPath(import.meta.url));
511
+ const repoRoot = path2.resolve(cliDir, "../../..");
512
+ const sourceSkill = path2.join(repoRoot, "skills", "webmaster-droid-convert");
345
513
  const codexHome = opts.codexHome || process.env.CODEX_HOME;
346
514
  if (!codexHome) {
347
515
  throw new Error("CODEX_HOME is not set. Provide --codex-home.");
348
516
  }
349
- const destination = path.join(codexHome, "skills", "webmaster-droid-convert");
350
- await ensureDir(path.join(destination, "SKILL.md"));
517
+ const destination = path2.join(codexHome, "skills", "webmaster-droid-convert");
518
+ await ensureDir(path2.join(destination, "SKILL.md"));
351
519
  try {
352
520
  await fs.access(destination);
353
521
  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.2.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",