@webmaster-droid/cli 0.1.0-alpha.1 → 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 +120 -96
- 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 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 {
|
|
@@ -69,9 +152,9 @@ program.command("init").description("Initialize webmaster-droid config in curren
|
|
|
69
152
|
console.log(`Created: ${envExample}`);
|
|
70
153
|
}
|
|
71
154
|
});
|
|
72
|
-
var schema = program.command("schema").description("
|
|
155
|
+
var schema = program.command("schema").description("Optional schema helpers");
|
|
73
156
|
schema.command("init").description("Create starter schema file").option("--out <file>", "schema output", "cms/schema.webmaster.ts").action(async (opts) => {
|
|
74
|
-
const outFile =
|
|
157
|
+
const outFile = path2.resolve(process.cwd(), opts.out);
|
|
75
158
|
await ensureDir(outFile);
|
|
76
159
|
const template = `export default {
|
|
77
160
|
name: "webmaster-droid-schema",
|
|
@@ -89,8 +172,8 @@ schema.command("init").description("Create starter schema file").option("--out <
|
|
|
89
172
|
}
|
|
90
173
|
});
|
|
91
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) => {
|
|
92
|
-
const input =
|
|
93
|
-
const output =
|
|
175
|
+
const input = path2.resolve(process.cwd(), opts.input);
|
|
176
|
+
const output = path2.resolve(process.cwd(), opts.output);
|
|
94
177
|
let manifest;
|
|
95
178
|
if (input.endsWith(".json")) {
|
|
96
179
|
manifest = await readJson(input);
|
|
@@ -111,7 +194,7 @@ schema.command("build").description("Compile schema file to runtime manifest JSO
|
|
|
111
194
|
console.log(`Wrote manifest: ${output}`);
|
|
112
195
|
});
|
|
113
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) => {
|
|
114
|
-
const root =
|
|
197
|
+
const root = path2.resolve(process.cwd(), srcDir);
|
|
115
198
|
const files = await glob("**/*.{ts,tsx,js,jsx}", {
|
|
116
199
|
cwd: root,
|
|
117
200
|
absolute: true,
|
|
@@ -120,11 +203,11 @@ program.command("scan").description("Scan source files for static content candid
|
|
|
120
203
|
const findings = [];
|
|
121
204
|
for (const file of files) {
|
|
122
205
|
const code = await fs.readFile(file, "utf8");
|
|
123
|
-
const ast =
|
|
206
|
+
const ast = parse2(code, {
|
|
124
207
|
sourceType: "module",
|
|
125
208
|
plugins: ["typescript", "jsx"]
|
|
126
209
|
});
|
|
127
|
-
|
|
210
|
+
traverse2(ast, {
|
|
128
211
|
JSXText(pathNode) {
|
|
129
212
|
const text = normalizeText(pathNode.node.value);
|
|
130
213
|
if (!text || text.length < 3) {
|
|
@@ -132,25 +215,25 @@ program.command("scan").description("Scan source files for static content candid
|
|
|
132
215
|
}
|
|
133
216
|
findings.push({
|
|
134
217
|
type: "jsx-text",
|
|
135
|
-
file:
|
|
218
|
+
file: path2.relative(process.cwd(), file),
|
|
136
219
|
line: pathNode.node.loc?.start.line,
|
|
137
220
|
column: pathNode.node.loc?.start.column,
|
|
138
221
|
text
|
|
139
222
|
});
|
|
140
223
|
},
|
|
141
224
|
JSXAttribute(pathNode) {
|
|
142
|
-
const name =
|
|
225
|
+
const name = t2.isJSXIdentifier(pathNode.node.name) ? pathNode.node.name.name : "";
|
|
143
226
|
if (!["src", "href", "alt", "title"].includes(name)) {
|
|
144
227
|
return;
|
|
145
228
|
}
|
|
146
229
|
const valueNode = pathNode.node.value;
|
|
147
|
-
if (!valueNode || !
|
|
230
|
+
if (!valueNode || !t2.isStringLiteral(valueNode)) {
|
|
148
231
|
return;
|
|
149
232
|
}
|
|
150
233
|
findings.push({
|
|
151
234
|
type: "jsx-attr",
|
|
152
235
|
attr: name,
|
|
153
|
-
file:
|
|
236
|
+
file: path2.relative(process.cwd(), file),
|
|
154
237
|
line: valueNode.loc?.start.line,
|
|
155
238
|
column: valueNode.loc?.start.column,
|
|
156
239
|
text: valueNode.value
|
|
@@ -158,7 +241,7 @@ program.command("scan").description("Scan source files for static content candid
|
|
|
158
241
|
}
|
|
159
242
|
});
|
|
160
243
|
}
|
|
161
|
-
const output =
|
|
244
|
+
const output = path2.resolve(process.cwd(), opts.out);
|
|
162
245
|
await ensureDir(output);
|
|
163
246
|
await fs.writeFile(
|
|
164
247
|
output,
|
|
@@ -178,7 +261,7 @@ program.command("scan").description("Scan source files for static content candid
|
|
|
178
261
|
console.log(`Scan complete. Findings: ${findings.length}. Report: ${output}`);
|
|
179
262
|
});
|
|
180
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) => {
|
|
181
|
-
const root =
|
|
264
|
+
const root = path2.resolve(process.cwd(), srcDir);
|
|
182
265
|
const files = await glob("**/*.{tsx,jsx}", {
|
|
183
266
|
cwd: root,
|
|
184
267
|
absolute: true,
|
|
@@ -187,71 +270,12 @@ program.command("codemod").description("Apply deterministic JSX codemods to Edit
|
|
|
187
270
|
const changed = [];
|
|
188
271
|
for (const file of files) {
|
|
189
272
|
const source = await fs.readFile(file, "utf8");
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
plugins: ["typescript", "jsx"]
|
|
193
|
-
});
|
|
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;
|
|
224
|
-
}
|
|
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
|
-
);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
const next = generate(ast, { retainLines: true }, source).code;
|
|
251
|
-
if (next === source) {
|
|
273
|
+
const transformed = transformEditableTextCodemod(source, file, process.cwd());
|
|
274
|
+
if (!transformed.changed) {
|
|
252
275
|
continue;
|
|
253
276
|
}
|
|
254
|
-
const
|
|
277
|
+
const next = transformed.next;
|
|
278
|
+
const relFile = path2.relative(process.cwd(), file);
|
|
255
279
|
changed.push({
|
|
256
280
|
file: relFile,
|
|
257
281
|
patch: createTwoFilesPatch(relFile, relFile, source, next)
|
|
@@ -260,7 +284,7 @@ program.command("codemod").description("Apply deterministic JSX codemods to Edit
|
|
|
260
284
|
await fs.writeFile(file, next, "utf8");
|
|
261
285
|
}
|
|
262
286
|
}
|
|
263
|
-
const output =
|
|
287
|
+
const output = path2.resolve(process.cwd(), opts.out);
|
|
264
288
|
await ensureDir(output);
|
|
265
289
|
await fs.writeFile(
|
|
266
290
|
output,
|
|
@@ -286,7 +310,7 @@ program.command("doctor").description("Validate local environment for webmaster-
|
|
|
286
310
|
issues.push(`Node.js 20+ required, found ${process.versions.node}`);
|
|
287
311
|
}
|
|
288
312
|
try {
|
|
289
|
-
await fs.access(
|
|
313
|
+
await fs.access(path2.resolve(process.cwd(), "package.json"));
|
|
290
314
|
} catch {
|
|
291
315
|
issues.push("package.json missing in current working directory");
|
|
292
316
|
}
|
|
@@ -316,7 +340,7 @@ program.command("dev").description("Start project dev command (pass-through)").o
|
|
|
316
340
|
var deploy = program.command("deploy").description("Deployment helpers");
|
|
317
341
|
var aws = deploy.command("aws").description("Deploy AWS lambda bundle");
|
|
318
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) => {
|
|
319
|
-
const entry =
|
|
343
|
+
const entry = path2.resolve(process.cwd(), opts.entry);
|
|
320
344
|
const tmpDir = opts.tmpDir;
|
|
321
345
|
const functions = String(opts.functions).split(",").map((item) => item.trim()).filter(Boolean);
|
|
322
346
|
const run = (cmd) => new Promise((resolve, reject) => {
|
|
@@ -339,15 +363,15 @@ aws.requiredOption("--entry <file>", "entry TypeScript file").requiredOption("--
|
|
|
339
363
|
});
|
|
340
364
|
var skill = program.command("skill").description("Skill helpers");
|
|
341
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) => {
|
|
342
|
-
const cliDir =
|
|
343
|
-
const repoRoot =
|
|
344
|
-
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");
|
|
345
369
|
const codexHome = opts.codexHome || process.env.CODEX_HOME;
|
|
346
370
|
if (!codexHome) {
|
|
347
371
|
throw new Error("CODEX_HOME is not set. Provide --codex-home.");
|
|
348
372
|
}
|
|
349
|
-
const destination =
|
|
350
|
-
await ensureDir(
|
|
373
|
+
const destination = path2.join(codexHome, "skills", "webmaster-droid-convert");
|
|
374
|
+
await ensureDir(path2.join(destination, "SKILL.md"));
|
|
351
375
|
try {
|
|
352
376
|
await fs.access(destination);
|
|
353
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,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",
|