howone 0.1.50 → 0.1.52

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 (52) hide show
  1. package/package.json +1 -1
  2. package/templates/vite/.howone/skills/howone/03-ai-capabilities/01-ai-capability-architecture.md +16 -2
  3. package/templates/vite/.howone/skills/howone/03-ai-capabilities/02-workflow-contract-rules.md +107 -0
  4. package/templates/vite/.howone/skills/howone/03-ai-capabilities/03-service-capability-catalog.md +98 -22
  5. package/templates/vite/.howone/skills/howone/03-ai-capabilities/04-workflow-operations.md +8 -0
  6. package/templates/vite/.howone/skills/howone/03-ai-capabilities/05-ai-feature-playbooks.md +66 -1
  7. package/templates/vite/.howone/skills/web-clone/LICENSE +0 -21
  8. package/templates/vite/.howone/skills/web-clone/README.md +0 -179
  9. package/templates/vite/.howone/skills/web-clone/SKILL.md +0 -243
  10. package/templates/vite/.howone/skills/web-clone/references/assessment.md +0 -77
  11. package/templates/vite/.howone/skills/web-clone/references/complex-playbooks.md +0 -46
  12. package/templates/vite/.howone/skills/web-clone/references/deliverables.md +0 -144
  13. package/templates/vite/.howone/skills/web-clone/references/design-dna.md +0 -125
  14. package/templates/vite/.howone/skills/web-clone/references/effect-extraction.md +0 -73
  15. package/templates/vite/.howone/skills/web-clone/references/marbles-case.md +0 -31
  16. package/templates/vite/.howone/skills/web-clone/references/reverse-engineering.md +0 -34
  17. package/templates/vite/.howone/skills/web-clone/references/static-mirror.md +0 -72
  18. package/templates/vite/.howone/skills/web-clone/scripts/asset-harvest.mjs +0 -101
  19. package/templates/vite/.howone/skills/web-clone/scripts/audit-clone.mjs +0 -151
  20. package/templates/vite/.howone/skills/web-clone/scripts/compare-recon.mjs +0 -265
  21. package/templates/vite/.howone/skills/web-clone/scripts/dna-scaffold.mjs +0 -214
  22. package/templates/vite/.howone/skills/web-clone/scripts/init-clone.mjs +0 -136
  23. package/templates/vite/.howone/skills/web-clone/scripts/interaction-probe.mjs +0 -314
  24. package/templates/vite/.howone/skills/web-clone/scripts/lib/playwright-loader.mjs +0 -39
  25. package/templates/vite/.howone/skills/web-clone/scripts/mirror-site.mjs +0 -121
  26. package/templates/vite/.howone/skills/web-clone/scripts/network-capture.mjs +0 -127
  27. package/templates/vite/.howone/skills/web-clone/scripts/recon-site.mjs +0 -235
  28. package/templates/vite/.howone/skills/web-clone/scripts/route-crawl.mjs +0 -228
  29. package/templates/vite/.howone/skills/web-clone/scripts/sourcemap-hunt.mjs +0 -112
  30. package/templates/vite/.howone/skills/web-clone/scripts/visual-diff.mjs +0 -161
  31. package/templates/vite/.howone/skills/web-shader-extractor/SKILL.md +0 -123
  32. package/templates/vite/.howone/skills/web-shader-extractor/references/capture-backends.md +0 -201
  33. package/templates/vite/.howone/skills/web-shader-extractor/references/evidence-policy.md +0 -93
  34. package/templates/vite/.howone/skills/web-shader-extractor/references/operating-contract.md +0 -82
  35. package/templates/vite/.howone/skills/web-shader-extractor/references/qa-failure-policy.md +0 -100
  36. package/templates/vite/.howone/skills/web-shader-extractor/references/recon-kernel.md +0 -206
  37. package/templates/vite/.howone/skills/web-shader-extractor/references/replay-policy.md +0 -145
  38. package/templates/vite/.howone/skills/web-shader-extractor/references/shaders-com.md +0 -212
  39. package/templates/vite/.howone/skills/web-shader-extractor/references/source-analysis.md +0 -112
  40. package/templates/vite/.howone/skills/web-shader-extractor/references/surface-discovery.md +0 -113
  41. package/templates/vite/.howone/skills/web-shader-extractor/references/target-lock.md +0 -205
  42. package/templates/vite/.howone/skills/web-shader-extractor/references/three-shader-reconstruction.md +0 -155
  43. package/templates/vite/.howone/skills/web-shader-extractor/references/tool-capability-matrix.md +0 -57
  44. package/templates/vite/.howone/skills/web-shader-extractor/references/unicorn-studio.md +0 -387
  45. package/templates/vite/.howone/skills/web-shader-extractor/scripts/fetch-rendered-dom.mjs +0 -178
  46. package/templates/vite/.howone/skills/web-shader-extractor/scripts/scan-bundle.sh +0 -80
  47. package/templates/vite/.howone/skills/web-shader-extractor/templates/extraction-report.md +0 -41
  48. package/templates/vite/.howone/skills/web-shader-extractor/templates/known-gaps.md +0 -14
  49. package/templates/vite/.howone/skills/web-shader-extractor/templates/qa-report.md +0 -74
  50. package/templates/vite/.howone/skills/web-shader-extractor/templates/replay-manifest.json +0 -147
  51. package/templates/vite/.howone/skills/web-shader-extractor/templates/run-state.json +0 -75
  52. package/templates/vite/.howone/skills/web-shader-extractor/templates/scout-card.json +0 -106
@@ -1,127 +0,0 @@
1
- #!/usr/bin/env node
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import crypto from "node:crypto";
5
- import { loadPlaywright, launchChromium } from "./lib/playwright-loader.mjs";
6
-
7
- function usage() {
8
- console.log(`Usage:
9
- node scripts/network-capture.mjs --url <url> --out RECON/network [--label original] [--wait 5000] [--max-bytes 1000000]
10
-
11
- Captures browser requests and saves JSON/text XHR/fetch responses as fixtures.
12
- `);
13
- }
14
-
15
- function parseArgs(argv) {
16
- const out = { url: "", outDir: "RECON/network", label: "site", waitMs: 5000, maxBytes: 1_000_000 };
17
- for (let i = 0; i < argv.length; i += 1) {
18
- const arg = argv[i];
19
- if (arg === "--help" || arg === "-h") out.help = true;
20
- else if (arg === "--url") out.url = argv[++i] || "";
21
- else if (arg === "--out") out.outDir = argv[++i] || "RECON/network";
22
- else if (arg === "--label") out.label = argv[++i] || "site";
23
- else if (arg === "--wait") out.waitMs = Number(argv[++i] || "5000");
24
- else if (arg === "--max-bytes") out.maxBytes = Number(argv[++i] || "1000000");
25
- else throw new Error(`Unexpected argument: ${arg}`);
26
- }
27
- return out;
28
- }
29
-
30
- function safeFixtureName(url, contentType) {
31
- const parsed = new URL(url);
32
- const cleanPath = parsed.pathname.replace(/[^a-z0-9._-]+/gi, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "response";
33
- const hash = crypto.createHash("sha1").update(url).digest("hex").slice(0, 10);
34
- const ext = contentType.includes("json") ? ".json" : ".txt";
35
- return `${cleanPath}-${hash}${ext}`;
36
- }
37
-
38
- function shouldSave(resourceType, contentType) {
39
- if (!["xhr", "fetch"].includes(resourceType)) return false;
40
- return /json|text|graphql|javascript/i.test(contentType || "");
41
- }
42
-
43
- try {
44
- const args = parseArgs(process.argv.slice(2));
45
- if (args.help || !args.url) {
46
- usage();
47
- process.exit(args.help ? 0 : 1);
48
- }
49
-
50
- const outDir = path.resolve(args.outDir);
51
- const fixturesDir = path.join(outDir, "fixtures");
52
- fs.mkdirSync(fixturesDir, { recursive: true });
53
-
54
- const { chromium } = loadPlaywright();
55
- const browser = await launchChromium(chromium);
56
- const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
57
-
58
- const requests = [];
59
- const responses = [];
60
-
61
- page.on("request", (request) => {
62
- requests.push({
63
- url: request.url(),
64
- method: request.method(),
65
- resourceType: request.resourceType(),
66
- headers: request.headers(),
67
- postData: request.postData() || "",
68
- });
69
- });
70
-
71
- page.on("response", async (response) => {
72
- const request = response.request();
73
- const headers = response.headers();
74
- const contentType = headers["content-type"] || "";
75
- const entry = {
76
- url: response.url(),
77
- status: response.status(),
78
- ok: response.ok(),
79
- method: request.method(),
80
- resourceType: request.resourceType(),
81
- contentType,
82
- fixture: "",
83
- error: "",
84
- };
85
-
86
- if (shouldSave(request.resourceType(), contentType)) {
87
- try {
88
- const body = await response.body();
89
- if (body.length <= args.maxBytes) {
90
- const file = path.join(fixturesDir, safeFixtureName(response.url(), contentType));
91
- fs.writeFileSync(file, body);
92
- entry.fixture = path.relative(outDir, file);
93
- entry.bytes = body.length;
94
- } else {
95
- entry.error = `body too large: ${body.length}`;
96
- }
97
- } catch (error) {
98
- entry.error = error.message;
99
- }
100
- }
101
-
102
- responses.push(entry);
103
- });
104
-
105
- await page.goto(args.url, { waitUntil: "domcontentloaded", timeout: 45000 });
106
- await page.waitForLoadState("networkidle", { timeout: args.waitMs }).catch(() => {});
107
- if (args.waitMs > 0) await page.waitForTimeout(args.waitMs);
108
- await browser.close();
109
-
110
- const manifest = {
111
- label: args.label,
112
- url: args.url,
113
- capturedAt: new Date().toISOString(),
114
- requestCount: requests.length,
115
- responseCount: responses.length,
116
- fixtureCount: responses.filter((response) => response.fixture).length,
117
- requests,
118
- responses,
119
- };
120
-
121
- const manifestFile = path.join(outDir, `${args.label}-network.json`);
122
- fs.writeFileSync(manifestFile, `${JSON.stringify(manifest, null, 2)}\n`);
123
- console.log(manifestFile);
124
- } catch (error) {
125
- console.error(`network-capture failed: ${error.message}`);
126
- process.exit(1);
127
- }
@@ -1,235 +0,0 @@
1
- #!/usr/bin/env node
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import { loadPlaywright, launchChromium } from "./lib/playwright-loader.mjs";
5
-
6
- function usage() {
7
- console.log(`Usage:
8
- node scripts/recon-site.mjs --url <url> --out <RECON dir> [--label original|clone] [--widths 1440,768,390] [--wait 1200]
9
-
10
- Outputs:
11
- <out>/<label>-recon.json
12
- <out>/<label>-summary.md
13
- <out>/screenshots/<label>-<width>.png
14
- `);
15
- }
16
-
17
- function parseArgs(argv) {
18
- const out = {
19
- url: "",
20
- outDir: "RECON",
21
- label: "site",
22
- widths: [1440, 768, 390],
23
- waitMs: 1200,
24
- };
25
- for (let i = 0; i < argv.length; i += 1) {
26
- const arg = argv[i];
27
- if (arg === "--help" || arg === "-h") out.help = true;
28
- else if (arg === "--url") out.url = argv[++i] || "";
29
- else if (arg === "--out") out.outDir = argv[++i] || "";
30
- else if (arg === "--label") out.label = argv[++i] || "site";
31
- else if (arg === "--widths") out.widths = (argv[++i] || "").split(",").map((n) => Number(n.trim())).filter(Boolean);
32
- else if (arg === "--wait") out.waitMs = Number(argv[++i] || "1200");
33
- else throw new Error(`Unexpected argument: ${arg}`);
34
- }
35
- return out;
36
- }
37
-
38
- function summarizeFlags(signals) {
39
- return Object.entries(signals.frameworks || {})
40
- .filter(([, value]) => value)
41
- .map(([key]) => key);
42
- }
43
-
44
- function writeSummary(file, data) {
45
- const first = data.captures[0]?.signals || {};
46
- const lines = [
47
- `# ${data.label} recon`,
48
- "",
49
- `- URL: ${data.url}`,
50
- `- Title: ${first.title || ""}`,
51
- `- Lang: ${first.lang || ""}`,
52
- `- Viewports: ${data.captures.map((c) => c.viewport.width).join(", ")}`,
53
- `- Framework signals: ${summarizeFlags(first).join(", ") || "none"}`,
54
- `- Canvas count: ${first.counts?.canvas ?? 0}`,
55
- `- Video count: ${first.counts?.video ?? 0}`,
56
- `- Image count: ${first.counts?.images ?? 0}`,
57
- `- Link count: ${first.counts?.links ?? 0}`,
58
- `- Console errors: ${data.console.errors.length}`,
59
- `- Page errors: ${data.console.pageErrors.length}`,
60
- "",
61
- "## Screenshots",
62
- ...data.captures.map((c) => `- ${c.viewport.width}: ${c.screenshot}`),
63
- "",
64
- ];
65
- fs.writeFileSync(file, lines.join("\n"));
66
- }
67
-
68
- async function collectSignals(page) {
69
- return page.evaluate(() => {
70
- const bySelector = (selector) => Array.from(document.querySelectorAll(selector));
71
- const text = (node) => (node?.textContent || "").trim().replace(/\s+/g, " ");
72
- const win = window;
73
- const scripts = bySelector("script[src]").map((s) => s.src);
74
- const stylesheets = bySelector("link[rel='stylesheet']").map((s) => s.href);
75
- const headings = bySelector("h1,h2,h3").slice(0, 60).map((h) => ({
76
- tag: h.tagName.toLowerCase(),
77
- text: text(h).slice(0, 160),
78
- }));
79
- const sections = bySelector("header,nav,main,section,article,aside,footer").slice(0, 80).map((node) => {
80
- const rect = node.getBoundingClientRect();
81
- const style = getComputedStyle(node);
82
- return {
83
- tag: node.tagName.toLowerCase(),
84
- id: node.id || "",
85
- className: String(node.className || "").slice(0, 160),
86
- text: text(node).slice(0, 240),
87
- rect: {
88
- x: Math.round(rect.x),
89
- y: Math.round(rect.y),
90
- width: Math.round(rect.width),
91
- height: Math.round(rect.height),
92
- },
93
- style: {
94
- display: style.display,
95
- position: style.position,
96
- backgroundColor: style.backgroundColor,
97
- color: style.color,
98
- fontFamily: style.fontFamily,
99
- fontSize: style.fontSize,
100
- },
101
- };
102
- });
103
- const cssVariables = Array.from(document.styleSheets).flatMap((sheet) => {
104
- try {
105
- return Array.from(sheet.cssRules || []);
106
- } catch {
107
- return [];
108
- }
109
- }).flatMap((rule) => {
110
- const style = rule.style;
111
- if (!style) return [];
112
- return Array.from(style)
113
- .filter((name) => name.startsWith("--"))
114
- .map((name) => [name, style.getPropertyValue(name).trim()]);
115
- }).slice(0, 200);
116
- const images = bySelector("img").slice(0, 80).map((img) => ({
117
- src: img.currentSrc || img.src,
118
- alt: img.alt || "",
119
- width: img.naturalWidth || img.width || 0,
120
- height: img.naturalHeight || img.height || 0,
121
- }));
122
- const canvases = bySelector("canvas").map((canvas) => ({
123
- width: canvas.width,
124
- height: canvas.height,
125
- cssWidth: canvas.getBoundingClientRect().width,
126
- cssHeight: canvas.getBoundingClientRect().height,
127
- }));
128
- return {
129
- href: location.href,
130
- title: document.title,
131
- lang: document.documentElement.lang || "",
132
- bodyTextChars: (document.body?.innerText || "").length,
133
- scrollHeight: document.documentElement.scrollHeight,
134
- viewport: {
135
- width: window.innerWidth,
136
- height: window.innerHeight,
137
- },
138
- h1: bySelector("h1").map((h) => text(h)).filter(Boolean).slice(0, 10),
139
- headings,
140
- metaDescription: document.querySelector("meta[name='description']")?.content || "",
141
- counts: {
142
- links: bySelector("a[href]").length,
143
- images: bySelector("img").length,
144
- video: bySelector("video").length,
145
- canvas: bySelector("canvas").length,
146
- sections: sections.length,
147
- forms: bySelector("form").length,
148
- buttons: bySelector("button").length,
149
- inputs: bySelector("input,textarea,select").length,
150
- interactive: bySelector("a[href],button,input,textarea,select,summary,[role='button'],[tabindex]").length,
151
- scripts: scripts.length,
152
- stylesheets: stylesheets.length,
153
- },
154
- frameworks: {
155
- react: Boolean(win.__REACT_DEVTOOLS_GLOBAL_HOOK__) || Boolean(document.querySelector("#__next,[data-reactroot],[data-reactid]")),
156
- next: Boolean(document.querySelector("#__next")) || scripts.some((src) => src.includes("/_next/")),
157
- vue: Boolean(win.__VUE__) || Boolean(document.querySelector("[data-v-app]")),
158
- nuxt: Boolean(win.__NUXT__) || scripts.some((src) => src.includes("/_nuxt/")),
159
- svelte: Boolean(document.querySelector("[data-svelte-h]")),
160
- astro: Boolean(document.querySelector("[data-astro-cid]")) || scripts.some((src) => src.includes("astro")),
161
- three: Boolean(win.THREE) || scripts.some((src) => /three(\.module)?(\.min)?\.js/i.test(src)),
162
- gsap: Boolean(win.gsap) || scripts.some((src) => src.toLowerCase().includes("gsap")),
163
- lenis: Boolean(win.Lenis) || scripts.some((src) => src.toLowerCase().includes("lenis")),
164
- },
165
- scripts: scripts.slice(0, 120),
166
- stylesheets: stylesheets.slice(0, 80),
167
- sections,
168
- cssVariables,
169
- fonts: Array.from(document.fonts || []).map((font) => font.family).filter(Boolean).slice(0, 40),
170
- images,
171
- canvases,
172
- };
173
- });
174
- }
175
-
176
- try {
177
- const args = parseArgs(process.argv.slice(2));
178
- if (args.help || !args.url) {
179
- usage();
180
- process.exit(args.help ? 0 : 1);
181
- }
182
-
183
- const { chromium } = loadPlaywright();
184
- const outDir = path.resolve(args.outDir);
185
- const screenshotsDir = path.join(outDir, "screenshots");
186
- fs.mkdirSync(screenshotsDir, { recursive: true });
187
-
188
- const consoleState = { errors: [], warnings: [], pageErrors: [] };
189
- const browser = await launchChromium(chromium);
190
- const captures = [];
191
-
192
- for (const width of args.widths) {
193
- const page = await browser.newPage({ viewport: { width, height: 900 }, deviceScaleFactor: 1 });
194
- page.on("console", (message) => {
195
- const entry = { type: message.type(), text: message.text(), viewport: width };
196
- if (message.type() === "error") consoleState.errors.push(entry);
197
- if (message.type() === "warning") consoleState.warnings.push(entry);
198
- });
199
- page.on("pageerror", (error) => {
200
- consoleState.pageErrors.push({ message: error.message, viewport: width });
201
- });
202
-
203
- await page.goto(args.url, { waitUntil: "domcontentloaded", timeout: 45000 });
204
- await page.waitForLoadState("networkidle", { timeout: 8000 }).catch(() => {});
205
- if (args.waitMs > 0) await page.waitForTimeout(args.waitMs);
206
-
207
- const signals = await collectSignals(page);
208
- const screenshotName = `${args.label}-${width}.png`;
209
- const screenshotPath = path.join(screenshotsDir, screenshotName);
210
- await page.screenshot({ path: screenshotPath, fullPage: true });
211
- captures.push({
212
- viewport: { width, height: 900 },
213
- screenshot: path.relative(outDir, screenshotPath),
214
- signals,
215
- });
216
- await page.close();
217
- }
218
-
219
- await browser.close();
220
-
221
- const result = {
222
- label: args.label,
223
- url: args.url,
224
- capturedAt: new Date().toISOString(),
225
- console: consoleState,
226
- captures,
227
- };
228
- const jsonFile = path.join(outDir, `${args.label}-recon.json`);
229
- fs.writeFileSync(jsonFile, `${JSON.stringify(result, null, 2)}\n`);
230
- writeSummary(path.join(outDir, `${args.label}-summary.md`), result);
231
- console.log(jsonFile);
232
- } catch (error) {
233
- console.error(`recon-site failed: ${error.message}`);
234
- process.exit(1);
235
- }
@@ -1,228 +0,0 @@
1
- #!/usr/bin/env node
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import crypto from "node:crypto";
5
- import { loadPlaywright, launchChromium } from "./lib/playwright-loader.mjs";
6
-
7
- function usage() {
8
- console.log(`Usage:
9
- node scripts/route-crawl.mjs --url <url> --out RECON/routes [--label original] [--max-pages 25] [--max-depth 2] [--width 1440] [--wait 800] [--allow-subdomains]
10
-
11
- Crawls same-site internal links, captures a screenshot per route, and writes a route map.
12
- `);
13
- }
14
-
15
- function parseArgs(argv) {
16
- const out = {
17
- url: "",
18
- outDir: "RECON/routes",
19
- label: "site",
20
- maxPages: 25,
21
- maxDepth: 2,
22
- width: 1440,
23
- waitMs: 800,
24
- allowSubdomains: false,
25
- };
26
- for (let i = 0; i < argv.length; i += 1) {
27
- const arg = argv[i];
28
- if (arg === "--help" || arg === "-h") out.help = true;
29
- else if (arg === "--url") out.url = argv[++i] || "";
30
- else if (arg === "--out") out.outDir = argv[++i] || "RECON/routes";
31
- else if (arg === "--label") out.label = argv[++i] || "site";
32
- else if (arg === "--max-pages") out.maxPages = Number(argv[++i] || "25");
33
- else if (arg === "--max-depth") out.maxDepth = Number(argv[++i] || "2");
34
- else if (arg === "--width") out.width = Number(argv[++i] || "1440");
35
- else if (arg === "--wait") out.waitMs = Number(argv[++i] || "800");
36
- else if (arg === "--allow-subdomains") out.allowSubdomains = true;
37
- else throw new Error(`Unexpected argument: ${arg}`);
38
- }
39
- return out;
40
- }
41
-
42
- function normalizeUrl(rawUrl, baseUrl) {
43
- try {
44
- const url = new URL(rawUrl, baseUrl);
45
- if (!["http:", "https:"].includes(url.protocol)) return "";
46
- url.hash = "";
47
- url.searchParams.sort();
48
- return url.toString().replace(/\/$/, "");
49
- } catch {
50
- return "";
51
- }
52
- }
53
-
54
- function sameSite(candidate, origin, allowSubdomains) {
55
- const url = new URL(candidate);
56
- const root = new URL(origin);
57
- if (url.origin === root.origin) return true;
58
- return allowSubdomains && url.hostname.endsWith(`.${root.hostname}`);
59
- }
60
-
61
- function routeFileName(url) {
62
- const parsed = new URL(url);
63
- const clean = `${parsed.hostname}${parsed.pathname}`.replace(/[^a-z0-9._-]+/gi, "-").replace(/^-+|-+$/g, "").slice(0, 90) || "route";
64
- const hash = crypto.createHash("sha1").update(url).digest("hex").slice(0, 8);
65
- return `${clean}-${hash}.png`;
66
- }
67
-
68
- function summarizeMarkdown(result) {
69
- const lines = [
70
- `# ${result.label} route map`,
71
- "",
72
- `- URL: ${result.url}`,
73
- `- Captured routes: ${result.routes.length}`,
74
- `- Max depth: ${result.maxDepth}`,
75
- `- Max pages: ${result.maxPages}`,
76
- "",
77
- "## Routes",
78
- "| Depth | Status | Path | Title | H1 | Links | Screenshot |",
79
- "|---:|---:|---|---|---|---:|---|",
80
- ];
81
- for (const route of result.routes) {
82
- const url = new URL(route.url);
83
- const pathLabel = `${url.pathname}${url.search}`;
84
- lines.push(`| ${route.depth} | ${route.status || ""} | ${pathLabel || "/"} | ${route.title.replaceAll("|", "\\|")} | ${route.h1.join(" / ").replaceAll("|", "\\|")} | ${route.linkCount} | ${route.screenshot} |`);
85
- }
86
- if (result.skipped.length) {
87
- lines.push("");
88
- lines.push("## Skipped / Failed");
89
- for (const item of result.skipped.slice(0, 80)) {
90
- lines.push(`- ${item.url} · ${item.reason}`);
91
- }
92
- }
93
- return `${lines.join("\n")}\n`;
94
- }
95
-
96
- async function collectPage(page) {
97
- return page.evaluate(() => {
98
- const text = (node) => (node?.textContent || "").trim().replace(/\s+/g, " ");
99
- const links = Array.from(document.querySelectorAll("a[href]")).map((a) => ({
100
- href: a.href,
101
- text: text(a).slice(0, 120),
102
- }));
103
- const headings = Array.from(document.querySelectorAll("h1,h2,h3")).slice(0, 40).map((node) => ({
104
- tag: node.tagName.toLowerCase(),
105
- text: text(node).slice(0, 160),
106
- }));
107
- return {
108
- href: location.href,
109
- title: document.title || "",
110
- lang: document.documentElement.lang || "",
111
- metaDescription: document.querySelector("meta[name='description']")?.content || "",
112
- h1: Array.from(document.querySelectorAll("h1")).map((node) => text(node)).filter(Boolean).slice(0, 8),
113
- headings,
114
- scrollHeight: document.documentElement.scrollHeight,
115
- counts: {
116
- links: links.length,
117
- images: document.images.length,
118
- canvas: document.querySelectorAll("canvas").length,
119
- forms: document.forms.length,
120
- buttons: document.querySelectorAll("button,[role='button']").length,
121
- },
122
- links,
123
- };
124
- });
125
- }
126
-
127
- try {
128
- const args = parseArgs(process.argv.slice(2));
129
- if (args.help || !args.url) {
130
- usage();
131
- process.exit(args.help ? 0 : 1);
132
- }
133
-
134
- const startUrl = normalizeUrl(args.url, args.url);
135
- if (!startUrl) throw new Error(`Invalid URL: ${args.url}`);
136
-
137
- const outDir = path.resolve(args.outDir);
138
- const screenshotsDir = path.join(outDir, "screenshots");
139
- fs.mkdirSync(screenshotsDir, { recursive: true });
140
-
141
- const { chromium } = loadPlaywright();
142
- const browser = await launchChromium(chromium);
143
- const page = await browser.newPage({ viewport: { width: args.width, height: 900 }, deviceScaleFactor: 1 });
144
-
145
- const queue = [{ url: startUrl, depth: 0, from: "" }];
146
- const seen = new Set();
147
- const routes = [];
148
- const skipped = [];
149
-
150
- while (queue.length && routes.length < args.maxPages) {
151
- const current = queue.shift();
152
- if (!current || seen.has(current.url)) continue;
153
- seen.add(current.url);
154
-
155
- if (current.depth > args.maxDepth) {
156
- skipped.push({ url: current.url, reason: `depth>${args.maxDepth}` });
157
- continue;
158
- }
159
- if (!sameSite(current.url, startUrl, args.allowSubdomains)) {
160
- skipped.push({ url: current.url, reason: "external" });
161
- continue;
162
- }
163
-
164
- const consoleErrors = [];
165
- const onConsole = (message) => {
166
- if (message.type() === "error") consoleErrors.push(message.text());
167
- };
168
- page.on("console", onConsole);
169
-
170
- try {
171
- const response = await page.goto(current.url, { waitUntil: "domcontentloaded", timeout: 45000 });
172
- await page.waitForLoadState("networkidle", { timeout: 6000 }).catch(() => {});
173
- if (args.waitMs > 0) await page.waitForTimeout(args.waitMs);
174
- const data = await collectPage(page);
175
- const screenshotName = routeFileName(current.url);
176
- const screenshotPath = path.join(screenshotsDir, screenshotName);
177
- await page.screenshot({ path: screenshotPath, fullPage: true });
178
-
179
- routes.push({
180
- url: current.url,
181
- from: current.from,
182
- depth: current.depth,
183
- status: response?.status() || 0,
184
- title: data.title,
185
- lang: data.lang,
186
- metaDescription: data.metaDescription,
187
- h1: data.h1,
188
- headings: data.headings,
189
- counts: data.counts,
190
- linkCount: data.links.length,
191
- screenshot: path.relative(outDir, screenshotPath),
192
- consoleErrors,
193
- });
194
-
195
- for (const link of data.links) {
196
- const nextUrl = normalizeUrl(link.href, current.url);
197
- if (!nextUrl || seen.has(nextUrl)) continue;
198
- if (!sameSite(nextUrl, startUrl, args.allowSubdomains)) continue;
199
- queue.push({ url: nextUrl, depth: current.depth + 1, from: current.url });
200
- }
201
- } catch (error) {
202
- skipped.push({ url: current.url, reason: error.message });
203
- } finally {
204
- page.off("console", onConsole);
205
- }
206
- }
207
-
208
- await browser.close();
209
-
210
- const result = {
211
- label: args.label,
212
- url: startUrl,
213
- capturedAt: new Date().toISOString(),
214
- maxPages: args.maxPages,
215
- maxDepth: args.maxDepth,
216
- allowSubdomains: args.allowSubdomains,
217
- routes,
218
- skipped,
219
- };
220
- const jsonFile = path.join(outDir, `${args.label}-route-map.json`);
221
- const mdFile = path.join(outDir, `${args.label}-route-map.md`);
222
- fs.writeFileSync(jsonFile, `${JSON.stringify(result, null, 2)}\n`);
223
- fs.writeFileSync(mdFile, summarizeMarkdown(result));
224
- console.log(jsonFile);
225
- } catch (error) {
226
- console.error(`route-crawl failed: ${error.message}`);
227
- process.exit(1);
228
- }
@@ -1,112 +0,0 @@
1
- #!/usr/bin/env node
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import crypto from "node:crypto";
5
-
6
- function usage() {
7
- console.log(`Usage:
8
- node scripts/sourcemap-hunt.mjs --recon original-recon.json --out RECON/sourcemaps [--all-external]
9
-
10
- Finds sourceMappingURL hints in discovered JavaScript bundles and tries to download source maps.
11
- `);
12
- }
13
-
14
- function parseArgs(argv) {
15
- const out = { recon: "", outDir: "RECON/sourcemaps", allExternal: false };
16
- for (let i = 0; i < argv.length; i += 1) {
17
- const arg = argv[i];
18
- if (arg === "--help" || arg === "-h") out.help = true;
19
- else if (arg === "--recon") out.recon = argv[++i] || "";
20
- else if (arg === "--out") out.outDir = argv[++i] || "RECON/sourcemaps";
21
- else if (arg === "--all-external") out.allExternal = true;
22
- else throw new Error(`Unexpected argument: ${arg}`);
23
- }
24
- return out;
25
- }
26
-
27
- function collectScripts(recon) {
28
- const scripts = new Set();
29
- for (const capture of recon.captures || []) {
30
- for (const script of capture.signals?.scripts || []) {
31
- if (/^https?:\/\//i.test(script)) scripts.add(script);
32
- }
33
- }
34
- return Array.from(scripts);
35
- }
36
-
37
- function fileNameFor(url, suffix = "") {
38
- const parsed = new URL(url);
39
- const base = path.basename(parsed.pathname).replace(/[^a-z0-9._-]+/gi, "-").slice(0, 90) || "bundle.js";
40
- const hash = crypto.createHash("sha1").update(url).digest("hex").slice(0, 10);
41
- return `${base}-${hash}${suffix}`;
42
- }
43
-
44
- function resolveMapUrl(scriptUrl, mapHint) {
45
- if (!mapHint) return `${scriptUrl}.map`;
46
- if (mapHint.startsWith("data:")) return "";
47
- return new URL(mapHint, scriptUrl).toString();
48
- }
49
-
50
- async function fetchText(url) {
51
- const response = await fetch(url, {
52
- headers: {
53
- "user-agent": "web-clone-skill/1.0 sourcemap-hunt",
54
- "accept": "*/*",
55
- },
56
- });
57
- if (!response.ok) throw new Error(`${response.status} ${response.statusText}`);
58
- return response.text();
59
- }
60
-
61
- try {
62
- const args = parseArgs(process.argv.slice(2));
63
- if (args.help || !args.recon) {
64
- usage();
65
- process.exit(args.help ? 0 : 1);
66
- }
67
-
68
- const recon = JSON.parse(fs.readFileSync(args.recon, "utf8"));
69
- const originHost = recon.url ? new URL(recon.url).hostname : "";
70
- const scripts = collectScripts(recon).filter((script) => args.allExternal || new URL(script).hostname === originHost);
71
- const outDir = path.resolve(args.outDir);
72
- fs.mkdirSync(outDir, { recursive: true });
73
-
74
- const results = [];
75
- for (const scriptUrl of scripts) {
76
- const entry = { scriptUrl, status: "unknown", mapUrl: "", mapFile: "", error: "" };
77
- try {
78
- const js = await fetchText(scriptUrl);
79
- const hint = js.match(/[#@]\s*sourceMappingURL=([^\s*]+)/)?.[1] || "";
80
- entry.mapUrl = resolveMapUrl(scriptUrl, hint);
81
- if (!entry.mapUrl) {
82
- entry.status = "inline-or-data-map";
83
- results.push(entry);
84
- continue;
85
- }
86
- const mapText = await fetchText(entry.mapUrl);
87
- const mapFile = path.join(outDir, fileNameFor(entry.mapUrl, ".map"));
88
- fs.writeFileSync(mapFile, mapText);
89
- entry.mapFile = mapFile;
90
- entry.status = "ok";
91
- } catch (error) {
92
- entry.status = "error";
93
- entry.error = error.message;
94
- }
95
- results.push(entry);
96
- }
97
-
98
- const manifest = {
99
- source: args.recon,
100
- url: recon.url,
101
- allExternal: args.allExternal,
102
- scriptCount: scripts.length,
103
- mapCount: results.filter((item) => item.status === "ok").length,
104
- results,
105
- };
106
- const manifestFile = path.join(outDir, "sourcemap-manifest.json");
107
- fs.writeFileSync(manifestFile, `${JSON.stringify(manifest, null, 2)}\n`);
108
- console.log(manifestFile);
109
- } catch (error) {
110
- console.error(`sourcemap-hunt failed: ${error.message}`);
111
- process.exit(1);
112
- }