@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.
- package/dist/index.js +121 -96
- 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
|
|
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 traverse2 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,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(
|
|
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 =
|
|
35
|
-
const configPath =
|
|
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 =
|
|
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("
|
|
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 =
|
|
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 =
|
|
92
|
-
const 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 =
|
|
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 =
|
|
206
|
+
const ast = parse2(code, {
|
|
123
207
|
sourceType: "module",
|
|
124
208
|
plugins: ["typescript", "jsx"]
|
|
125
209
|
});
|
|
126
|
-
|
|
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:
|
|
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 =
|
|
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 || !
|
|
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:
|
|
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 =
|
|
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 =
|
|
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
|
|
190
|
-
|
|
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
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
342
|
-
const repoRoot =
|
|
343
|
-
const sourceSkill =
|
|
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 =
|
|
349
|
-
await ensureDir(
|
|
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
|
|
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
|
}
|