@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.
- package/dist/index.js +352 -184
- 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
|
|
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
|
|
13
|
-
import
|
|
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
|
-
|
|
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(
|
|
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(
|
|
33
|
-
program.command("init").description("Initialize webmaster-droid config in current project").option("--framework <framework>", "framework", "next").option("--backend <backend>", "backend", "
|
|
34
|
-
const
|
|
35
|
-
|
|
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: "${
|
|
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 =
|
|
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
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
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("
|
|
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 =
|
|
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 =
|
|
93
|
-
const 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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
308
|
+
ok: false,
|
|
309
|
+
command: "scan",
|
|
310
|
+
version: CLI_VERSION,
|
|
311
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
312
|
+
errors: [errorToMessage(error)]
|
|
172
313
|
},
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
378
|
+
ok: false,
|
|
379
|
+
command: "codemod",
|
|
380
|
+
version: CLI_VERSION,
|
|
381
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
382
|
+
errors: [errorToMessage(error)]
|
|
274
383
|
},
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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(
|
|
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 =
|
|
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 =
|
|
343
|
-
const repoRoot =
|
|
344
|
-
const sourceSkill =
|
|
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 =
|
|
350
|
-
await ensureDir(
|
|
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.
|
|
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",
|