playwright-archaeologist 0.1.1
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/LICENSE +21 -0
- package/README.md +392 -0
- package/bin/cli.js +2 -0
- package/dist/chunk-7ZQGW5OV.js +255 -0
- package/dist/chunk-7ZQGW5OV.js.map +1 -0
- package/dist/chunk-F5WCXM7I.js +4469 -0
- package/dist/chunk-F5WCXM7I.js.map +1 -0
- package/dist/chunk-RWPEKZOW.js +118 -0
- package/dist/chunk-RWPEKZOW.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +310 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +1948 -0
- package/dist/index.js +789 -0
- package/dist/index.js.map +1 -0
- package/dist/page-scanner-Q76HROEW.js +8 -0
- package/dist/page-scanner-Q76HROEW.js.map +1 -0
- package/package.json +83 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// src/crawl/url-utils.ts
|
|
2
|
+
var ALLOWED_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
|
|
3
|
+
function normalizeUrl(url, options = {}) {
|
|
4
|
+
let parsed;
|
|
5
|
+
try {
|
|
6
|
+
parsed = new URL(url);
|
|
7
|
+
} catch {
|
|
8
|
+
return url;
|
|
9
|
+
}
|
|
10
|
+
const params = Array.from(parsed.searchParams.entries());
|
|
11
|
+
params.sort((a, b) => {
|
|
12
|
+
const keyCompare = a[0].localeCompare(b[0]);
|
|
13
|
+
if (keyCompare !== 0) return keyCompare;
|
|
14
|
+
return a[1].localeCompare(b[1]);
|
|
15
|
+
});
|
|
16
|
+
parsed.search = "";
|
|
17
|
+
if (params.length > 0) {
|
|
18
|
+
const sp = new URLSearchParams();
|
|
19
|
+
for (const [key, value] of params) {
|
|
20
|
+
sp.append(key, value);
|
|
21
|
+
}
|
|
22
|
+
parsed.search = sp.toString();
|
|
23
|
+
}
|
|
24
|
+
if (!options.preserveHash) {
|
|
25
|
+
parsed.hash = "";
|
|
26
|
+
}
|
|
27
|
+
let pathname = parsed.pathname;
|
|
28
|
+
while (pathname.length > 1 && pathname.endsWith("/")) {
|
|
29
|
+
pathname = pathname.slice(0, -1);
|
|
30
|
+
}
|
|
31
|
+
parsed.pathname = pathname;
|
|
32
|
+
return parsed.toString();
|
|
33
|
+
}
|
|
34
|
+
function isSameOrigin(url, baseUrl) {
|
|
35
|
+
try {
|
|
36
|
+
const a = new URL(url);
|
|
37
|
+
const b = new URL(baseUrl);
|
|
38
|
+
return a.origin === b.origin;
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function globToRegExp(pattern) {
|
|
44
|
+
let regexStr = "";
|
|
45
|
+
let i = 0;
|
|
46
|
+
while (i < pattern.length) {
|
|
47
|
+
const ch = pattern[i];
|
|
48
|
+
if (ch === "*") {
|
|
49
|
+
if (i + 1 < pattern.length && pattern[i + 1] === "*") {
|
|
50
|
+
regexStr += ".*";
|
|
51
|
+
i += 2;
|
|
52
|
+
if (i < pattern.length && pattern[i] === "/") {
|
|
53
|
+
regexStr += "\\/?";
|
|
54
|
+
i++;
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
regexStr += "[^/]*";
|
|
58
|
+
i++;
|
|
59
|
+
}
|
|
60
|
+
} else if (ch === "?") {
|
|
61
|
+
regexStr += "[^/]";
|
|
62
|
+
i++;
|
|
63
|
+
} else {
|
|
64
|
+
regexStr += ch.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
65
|
+
i++;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return new RegExp(`^${regexStr}$`);
|
|
69
|
+
}
|
|
70
|
+
function matchesGlob(url, patterns) {
|
|
71
|
+
if (patterns.length === 0) return false;
|
|
72
|
+
for (const pattern of patterns) {
|
|
73
|
+
const regex = globToRegExp(pattern);
|
|
74
|
+
if (regex.test(url)) return true;
|
|
75
|
+
if (pattern.startsWith("/")) {
|
|
76
|
+
try {
|
|
77
|
+
const parsed = new URL(url);
|
|
78
|
+
if (regex.test(parsed.pathname)) return true;
|
|
79
|
+
} catch {
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
function isAllowedProtocol(url) {
|
|
86
|
+
try {
|
|
87
|
+
const parsed = new URL(url);
|
|
88
|
+
return ALLOWED_PROTOCOLS.has(parsed.protocol);
|
|
89
|
+
} catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function resolveUrl(relative, base) {
|
|
94
|
+
try {
|
|
95
|
+
return new URL(relative, base).toString();
|
|
96
|
+
} catch {
|
|
97
|
+
return relative;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function shouldCrawl(url, baseUrl, options) {
|
|
101
|
+
if (!isAllowedProtocol(url)) return false;
|
|
102
|
+
if (!options.followExternal && !isSameOrigin(url, baseUrl)) return false;
|
|
103
|
+
if (options.exclude && options.exclude.length > 0 && matchesGlob(url, options.exclude)) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
if (options.include && options.include.length > 0 && !matchesGlob(url, options.include)) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export {
|
|
113
|
+
normalizeUrl,
|
|
114
|
+
isSameOrigin,
|
|
115
|
+
resolveUrl,
|
|
116
|
+
shouldCrawl
|
|
117
|
+
};
|
|
118
|
+
//# sourceMappingURL=chunk-RWPEKZOW.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/crawl/url-utils.ts"],"sourcesContent":["/**\n * URL normalization, scope checking, and utility functions.\n *\n * Responsibilities:\n * - Normalize URLs for deduplication (trailing slashes, query param ordering, hash stripping)\n * - Detect and handle hash-based routing\n * - Same-origin checking\n * - Include/exclude glob matching\n * - Protocol whitelist enforcement\n * - Relative URL resolution\n */\n\nconst ALLOWED_PROTOCOLS = new Set(['http:', 'https:']);\n\nexport interface NormalizeOptions {\n preserveHash?: boolean;\n}\n\n/**\n * Normalize a URL for deduplication.\n * - Sorts query parameters alphabetically\n * - Removes trailing slashes (except root path)\n * - Strips hash fragments (unless preserveHash is true)\n * - Lowercases scheme and host\n */\nexport function normalizeUrl(url: string, options: NormalizeOptions = {}): string {\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n return url;\n }\n\n // URL constructor already lowercases scheme and host.\n\n // Sort query parameters alphabetically\n const params = Array.from(parsed.searchParams.entries());\n params.sort((a, b) => {\n const keyCompare = a[0].localeCompare(b[0]);\n if (keyCompare !== 0) return keyCompare;\n return a[1].localeCompare(b[1]);\n });\n\n // Rebuild search string from sorted params\n parsed.search = '';\n if (params.length > 0) {\n const sp = new URLSearchParams();\n for (const [key, value] of params) {\n sp.append(key, value);\n }\n parsed.search = sp.toString();\n }\n\n // Handle hash\n if (!options.preserveHash) {\n parsed.hash = '';\n }\n\n // Remove trailing slashes from path (except root \"/\")\n let pathname = parsed.pathname;\n // Collapse multiple trailing slashes and remove them (but keep root)\n while (pathname.length > 1 && pathname.endsWith('/')) {\n pathname = pathname.slice(0, -1);\n }\n parsed.pathname = pathname;\n\n return parsed.toString();\n}\n\n/**\n * Check if a URL is same-origin as the base URL.\n */\nexport function isSameOrigin(url: string, baseUrl: string): boolean {\n try {\n const a = new URL(url);\n const b = new URL(baseUrl);\n return a.origin === b.origin;\n } catch {\n return false;\n }\n}\n\n/**\n * Convert a simple glob pattern to a RegExp.\n * Supports * (any chars except nothing special) and ** (any chars including path separators).\n */\nfunction globToRegExp(pattern: string): RegExp {\n let regexStr = '';\n let i = 0;\n while (i < pattern.length) {\n const ch = pattern[i];\n if (ch === '*') {\n if (i + 1 < pattern.length && pattern[i + 1] === '*') {\n // ** matches anything including /\n regexStr += '.*';\n i += 2;\n // Skip a following / after ** (e.g. **/ )\n if (i < pattern.length && pattern[i] === '/') {\n // The .* already covers the slash, but we still allow it\n regexStr += '\\\\/?';\n i++;\n }\n } else {\n // * matches anything except /\n regexStr += '[^/]*';\n i++;\n }\n } else if (ch === '?') {\n regexStr += '[^/]';\n i++;\n } else {\n // Escape regex special chars\n regexStr += ch.replace(/[.+^${}()|[\\]\\\\]/g, '\\\\$&');\n i++;\n }\n }\n return new RegExp(`^${regexStr}$`);\n}\n\n/**\n * Check if a URL matches any of the provided glob patterns.\n */\nexport function matchesGlob(url: string, patterns: string[]): boolean {\n if (patterns.length === 0) return false;\n\n for (const pattern of patterns) {\n const regex = globToRegExp(pattern);\n\n // Test against full URL\n if (regex.test(url)) return true;\n\n // If pattern starts with /, test against just the path\n if (pattern.startsWith('/')) {\n try {\n const parsed = new URL(url);\n if (regex.test(parsed.pathname)) return true;\n } catch {\n // ignore\n }\n }\n }\n return false;\n}\n\n/**\n * Check if a URL uses an allowed protocol.\n * Only http: and https: are allowed by default.\n */\nexport function isAllowedProtocol(url: string): boolean {\n try {\n const parsed = new URL(url);\n return ALLOWED_PROTOCOLS.has(parsed.protocol);\n } catch {\n return false;\n }\n}\n\n/**\n * Detect if a set of URLs suggest hash-based routing.\n * Returns true if 3+ URLs have hash fragments starting with #/ or #!/.\n */\nexport function detectHashRouting(urls: string[]): boolean {\n let count = 0;\n for (const url of urls) {\n try {\n const parsed = new URL(url);\n const hash = parsed.hash;\n if (hash.startsWith('#/') || hash.startsWith('#!/')) {\n count++;\n }\n } catch {\n // ignore malformed URLs\n }\n }\n return count >= 3;\n}\n\n/**\n * Resolve a relative URL against a base URL.\n */\nexport function resolveUrl(relative: string, base: string): string {\n try {\n return new URL(relative, base).toString();\n } catch {\n return relative;\n }\n}\n\n/**\n * Strip authentication info (user:pass@) from a URL.\n */\nexport function stripAuthInfo(url: string): string {\n try {\n const parsed = new URL(url);\n parsed.username = '';\n parsed.password = '';\n return parsed.toString();\n } catch {\n return url;\n }\n}\n\n/**\n * Check if URL should be crawled based on include/exclude patterns and protocol.\n */\nexport function shouldCrawl(\n url: string,\n baseUrl: string,\n options: {\n include?: string[];\n exclude?: string[];\n followExternal?: boolean;\n }\n): boolean {\n // Must be an allowed protocol\n if (!isAllowedProtocol(url)) return false;\n\n // Same-origin check (unless followExternal)\n if (!options.followExternal && !isSameOrigin(url, baseUrl)) return false;\n\n // Exclude takes precedence over include\n if (options.exclude && options.exclude.length > 0 && matchesGlob(url, options.exclude)) {\n return false;\n }\n\n // If include patterns are specified, URL must match at least one\n if (options.include && options.include.length > 0 && !matchesGlob(url, options.include)) {\n return false;\n }\n\n return true;\n}\n"],"mappings":"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","names":[]}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ArchaeologistError,
|
|
3
|
+
AuthError,
|
|
4
|
+
ConfigError,
|
|
5
|
+
CrawlConfigSchema,
|
|
6
|
+
DiffConfigSchema,
|
|
7
|
+
DiffError,
|
|
8
|
+
createBundle,
|
|
9
|
+
diffBundles,
|
|
10
|
+
dig,
|
|
11
|
+
generateDiffReportHtml,
|
|
12
|
+
logger,
|
|
13
|
+
normalizeEntryUrl,
|
|
14
|
+
parseViewport
|
|
15
|
+
} from "./chunk-F5WCXM7I.js";
|
|
16
|
+
import "./chunk-RWPEKZOW.js";
|
|
17
|
+
|
|
18
|
+
// src/cli.ts
|
|
19
|
+
import { Command } from "commander";
|
|
20
|
+
import { createRequire } from "module";
|
|
21
|
+
import { resolve, dirname } from "path";
|
|
22
|
+
import { execSync } from "child_process";
|
|
23
|
+
import { accessSync, constants } from "fs";
|
|
24
|
+
var require2 = createRequire(import.meta.url);
|
|
25
|
+
var pkg = require2("../package.json");
|
|
26
|
+
function parseViewportStrict(input, fieldName) {
|
|
27
|
+
const vp = parseViewport(input);
|
|
28
|
+
if (!vp) {
|
|
29
|
+
throw new ConfigError(
|
|
30
|
+
fieldName,
|
|
31
|
+
`Invalid viewport format "${input}". Expected WxH (e.g. 1280x720). Width must be 320-7680, height 240-4320.`,
|
|
32
|
+
input
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
return vp;
|
|
36
|
+
}
|
|
37
|
+
function parseViewportList(input) {
|
|
38
|
+
const parts = input.split(",").map((s) => s.trim()).filter(Boolean);
|
|
39
|
+
if (parts.length === 0) {
|
|
40
|
+
throw new ConfigError("viewports", "No viewports specified in --viewports.", input);
|
|
41
|
+
}
|
|
42
|
+
return parts.map((part) => parseViewportStrict(part, "viewports"));
|
|
43
|
+
}
|
|
44
|
+
function collectArray(value, previous) {
|
|
45
|
+
return previous.concat(value);
|
|
46
|
+
}
|
|
47
|
+
function parseIntOption(value, name) {
|
|
48
|
+
const n = parseInt(value, 10);
|
|
49
|
+
if (!Number.isFinite(n)) {
|
|
50
|
+
throw new ConfigError(name, `"${value}" is not a valid integer for --${name}.`, value);
|
|
51
|
+
}
|
|
52
|
+
return n;
|
|
53
|
+
}
|
|
54
|
+
var program = new Command();
|
|
55
|
+
program.name("playwright-archaeologist").description("Generate a complete behavioral specification of any web app").version(pkg.version, "-V, --version");
|
|
56
|
+
program.command("dig").description("Crawl a website and generate a behavioral specification").argument("<url>", "URL to crawl (https:// is prepended if no protocol)").option("-d, --depth <n>", "Maximum crawl depth from entry URL (default: 5)", (v) => parseIntOption(v, "depth")).option("--max-pages <n>", "Maximum total pages to visit (default: 1000)", (v) => parseIntOption(v, "max-pages")).option("--include <pattern>", "URL glob patterns to include (repeatable)", collectArray, []).option("--exclude <pattern>", "URL glob patterns to exclude (repeatable)", collectArray, []).option("--follow-external", "Follow links to external origins", false).option("--deep-click", "Click non-link interactive elements to discover SPA states", false).option("--include-iframes", "Crawl cross-origin iframe content", false).option("-c, --concurrency <n>", "Number of parallel browser contexts (default: 3)", (v) => parseIntOption(v, "concurrency")).option("--delay <ms>", "Delay between page visits per context in ms (default: 0)", (v) => parseIntOption(v, "delay")).option("--timeout <ms>", "Per-page navigation timeout in ms (default: 30000)", (v) => parseIntOption(v, "timeout")).option("--max-time <s>", "Global crawl timeout in seconds (default: 3600)", (v) => parseIntOption(v, "max-time")).option("--auth <script>", "Path to auth script (.ts or .js)").option("--cookies <file>", "Path to cookies JSON file for session injection").option("--include-cookies", "Include cookies/auth headers in output (default: scrubbed)", false).option("-o, --output <dir>", "Output directory (default: .archaeologist)").option("-f, --format <fmt>", "Output format: html, json, openapi, both (default: both)").option("--no-screenshots", "Skip screenshot capture").option("--no-har", "Skip HAR recording").option("--viewport <WxH>", "Primary viewport dimensions (default: 1280x720)").option("--viewports <list>", "Additional viewports for responsive screenshots (comma-separated, e.g. 375x667,768x1024)").option("--resume", "Resume from last checkpoint in the output directory", false).option("-y, --yes", "Skip confirmation prompts", false).option("--allow-private", "Allow crawling private/internal IP ranges", false).option("-v, --verbose", "Enable debug logging", false).action(async (url, opts) => {
|
|
57
|
+
if (opts.verbose) {
|
|
58
|
+
logger.setLevel("debug");
|
|
59
|
+
}
|
|
60
|
+
logger.debug("Raw CLI options: %o", opts);
|
|
61
|
+
try {
|
|
62
|
+
const { url: normalizedUrl, warnings } = normalizeEntryUrl(url);
|
|
63
|
+
for (const w of warnings) {
|
|
64
|
+
logger.warn(w);
|
|
65
|
+
}
|
|
66
|
+
let authScript;
|
|
67
|
+
if (typeof opts.auth === "string") {
|
|
68
|
+
authScript = resolve(opts.auth);
|
|
69
|
+
try {
|
|
70
|
+
accessSync(authScript, constants.R_OK);
|
|
71
|
+
} catch {
|
|
72
|
+
throw new AuthError(
|
|
73
|
+
"script_not_found",
|
|
74
|
+
`Auth script not found: ${authScript}`,
|
|
75
|
+
authScript
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
let cookiesFile;
|
|
80
|
+
if (typeof opts.cookies === "string") {
|
|
81
|
+
cookiesFile = resolve(opts.cookies);
|
|
82
|
+
try {
|
|
83
|
+
accessSync(cookiesFile, constants.R_OK);
|
|
84
|
+
} catch {
|
|
85
|
+
throw new AuthError(
|
|
86
|
+
"cookie_file_not_found",
|
|
87
|
+
`Cookies file not found: ${cookiesFile}`,
|
|
88
|
+
cookiesFile
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
let viewport;
|
|
93
|
+
if (typeof opts.viewport === "string") {
|
|
94
|
+
viewport = parseViewportStrict(opts.viewport, "viewport");
|
|
95
|
+
}
|
|
96
|
+
let additionalViewports;
|
|
97
|
+
if (typeof opts.viewports === "string") {
|
|
98
|
+
additionalViewports = parseViewportList(opts.viewports);
|
|
99
|
+
}
|
|
100
|
+
const outputDir = resolve(typeof opts.output === "string" ? opts.output : ".archaeologist");
|
|
101
|
+
const parentDir = dirname(outputDir);
|
|
102
|
+
try {
|
|
103
|
+
accessSync(parentDir, constants.W_OK);
|
|
104
|
+
} catch {
|
|
105
|
+
throw new ConfigError(
|
|
106
|
+
"outputDir",
|
|
107
|
+
`Output directory parent does not exist or is not writable: ${parentDir}`,
|
|
108
|
+
outputDir
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
const rawConfig = {
|
|
112
|
+
targetUrl: normalizedUrl,
|
|
113
|
+
outputDir
|
|
114
|
+
};
|
|
115
|
+
if (opts.depth !== void 0) rawConfig.depth = opts.depth;
|
|
116
|
+
if (opts.maxPages !== void 0) rawConfig.maxPages = opts.maxPages;
|
|
117
|
+
if (opts.include && opts.include.length > 0) rawConfig.include = opts.include;
|
|
118
|
+
if (opts.exclude && opts.exclude.length > 0) rawConfig.exclude = opts.exclude;
|
|
119
|
+
if (opts.followExternal) rawConfig.followExternal = true;
|
|
120
|
+
if (opts.deepClick) rawConfig.deepClick = true;
|
|
121
|
+
if (opts.includeIframes) rawConfig.includeIframes = true;
|
|
122
|
+
if (opts.concurrency !== void 0) rawConfig.concurrency = opts.concurrency;
|
|
123
|
+
if (opts.delay !== void 0) rawConfig.delay = opts.delay;
|
|
124
|
+
if (opts.timeout !== void 0) rawConfig.timeout = opts.timeout;
|
|
125
|
+
if (opts.maxTime !== void 0) rawConfig.maxTime = opts.maxTime;
|
|
126
|
+
if (authScript) rawConfig.authScript = authScript;
|
|
127
|
+
if (cookiesFile) rawConfig.cookiesFile = cookiesFile;
|
|
128
|
+
if (opts.includeCookies) rawConfig.includeCookies = true;
|
|
129
|
+
if (typeof opts.format === "string") rawConfig.format = opts.format;
|
|
130
|
+
if (opts.screenshots === false) rawConfig.noScreenshots = true;
|
|
131
|
+
if (opts.har === false) rawConfig.noHar = true;
|
|
132
|
+
if (viewport) rawConfig.viewport = viewport;
|
|
133
|
+
if (additionalViewports) rawConfig.additionalViewports = additionalViewports;
|
|
134
|
+
if (opts.resume) rawConfig.resume = true;
|
|
135
|
+
if (opts.yes) rawConfig.yes = true;
|
|
136
|
+
if (opts.allowPrivate) rawConfig.allowPrivate = true;
|
|
137
|
+
const parseResult = CrawlConfigSchema.safeParse(rawConfig);
|
|
138
|
+
if (!parseResult.success) {
|
|
139
|
+
const issues = parseResult.error.issues;
|
|
140
|
+
const messages = issues.map((issue) => {
|
|
141
|
+
const path = issue.path.join(".");
|
|
142
|
+
return ` --${path}: ${issue.message}`;
|
|
143
|
+
});
|
|
144
|
+
throw new ConfigError(
|
|
145
|
+
issues[0].path.join(".") || "config",
|
|
146
|
+
`Invalid configuration:
|
|
147
|
+
${messages.join("\n")}`,
|
|
148
|
+
rawConfig
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
const validatedConfig = parseResult.data;
|
|
152
|
+
const resolvedConfig = {
|
|
153
|
+
...validatedConfig,
|
|
154
|
+
outputDir: resolve(validatedConfig.outputDir),
|
|
155
|
+
authScript: validatedConfig.authScript ? resolve(validatedConfig.authScript) : void 0,
|
|
156
|
+
cookiesFile: validatedConfig.cookiesFile ? resolve(validatedConfig.cookiesFile) : void 0,
|
|
157
|
+
warnings
|
|
158
|
+
};
|
|
159
|
+
logger.debug("Resolved config: %o", resolvedConfig);
|
|
160
|
+
const result = await dig(resolvedConfig);
|
|
161
|
+
logger.success(
|
|
162
|
+
`Crawl complete. ${result.artifacts.meta.pagesVisited} pages visited in ${result.artifacts.meta.duration}ms.`
|
|
163
|
+
);
|
|
164
|
+
if (result.errors.length > 0) {
|
|
165
|
+
logger.warn(`${result.errors.length} error(s) encountered during crawl.`);
|
|
166
|
+
}
|
|
167
|
+
const bundlePath = resolve(outputDir, "bundle");
|
|
168
|
+
try {
|
|
169
|
+
const manifest = await createBundle(outputDir, bundlePath);
|
|
170
|
+
logger.success(`Bundle created: ${bundlePath} (${manifest.files.length} files)`);
|
|
171
|
+
} catch (bundleErr) {
|
|
172
|
+
logger.warn(`Bundle creation failed: ${bundleErr instanceof Error ? bundleErr.message : String(bundleErr)}`);
|
|
173
|
+
}
|
|
174
|
+
process.exit(0);
|
|
175
|
+
} catch (err) {
|
|
176
|
+
handleError(err);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
program.command("install").description("Install Playwright Chromium browser").action(() => {
|
|
180
|
+
try {
|
|
181
|
+
logger.info("Installing Playwright Chromium browser...");
|
|
182
|
+
execSync("npx playwright install chromium", {
|
|
183
|
+
stdio: "inherit",
|
|
184
|
+
timeout: 3e5
|
|
185
|
+
// 5 minute timeout
|
|
186
|
+
});
|
|
187
|
+
logger.success("Chromium browser installed successfully.");
|
|
188
|
+
} catch (err) {
|
|
189
|
+
logger.error("Failed to install Chromium browser.");
|
|
190
|
+
if (err instanceof Error) {
|
|
191
|
+
logger.debug(err.message);
|
|
192
|
+
}
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
program.command("diff").description("Compare two .archaeologist bundles and generate a diff report").argument("<old>", "Path to the older .archaeologist bundle").argument("<new>", "Path to the newer .archaeologist bundle").option("-o, --output <dir>", "Output directory for the diff report").option("--threshold <n>", "Pixel diff threshold 0-1 (default: 0.1)", parseFloat).option("--max-ratio <n>", "Max diff ratio 0-100 before marking changed (default: 0.5)", parseFloat).option("--ignore-fields <fields>", "Comma-separated fields to ignore in API diff").option("--normalize-dynamic", "Normalize dynamic values (UUIDs, timestamps) before diffing", false).option("--detailed", "Show detailed value-level diffs (default: schema-level only)", false).option("--format-html <path>", "Output HTML diff report to this path").option("--format-json <path>", "Output JSON diff report to this path").option("--format-junit <path>", "Output JUnit XML diff report to this path").option("--format-markdown <path>", "Output Markdown diff report to this path").option("-v, --verbose", "Enable debug logging", false).action(async (oldBundle, newBundle, opts) => {
|
|
197
|
+
if (opts.verbose) {
|
|
198
|
+
logger.setLevel("debug");
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
const oldPath = resolve(oldBundle);
|
|
202
|
+
const newPath = resolve(newBundle);
|
|
203
|
+
try {
|
|
204
|
+
accessSync(oldPath, constants.R_OK);
|
|
205
|
+
} catch {
|
|
206
|
+
throw new ConfigError("oldBundle", `Old bundle not found: ${oldPath}`, oldPath);
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
accessSync(newPath, constants.R_OK);
|
|
210
|
+
} catch {
|
|
211
|
+
throw new ConfigError("newBundle", `New bundle not found: ${newPath}`, newPath);
|
|
212
|
+
}
|
|
213
|
+
const rawDiffConfig = {
|
|
214
|
+
oldBundle: oldPath,
|
|
215
|
+
newBundle: newPath
|
|
216
|
+
};
|
|
217
|
+
if (typeof opts.output === "string") rawDiffConfig.outputDir = opts.output;
|
|
218
|
+
if (opts.threshold !== void 0) rawDiffConfig.diffThreshold = opts.threshold;
|
|
219
|
+
if (opts.maxRatio !== void 0) rawDiffConfig.diffMaxRatio = opts.maxRatio;
|
|
220
|
+
if (typeof opts.ignoreFields === "string") {
|
|
221
|
+
rawDiffConfig.diffIgnoreFields = opts.ignoreFields.split(",").map((s2) => s2.trim()).filter(Boolean);
|
|
222
|
+
}
|
|
223
|
+
if (opts.normalizeDynamic) rawDiffConfig.normalizeDynamicValues = true;
|
|
224
|
+
if (opts.detailed) rawDiffConfig.detailed = true;
|
|
225
|
+
const outputFormats = {};
|
|
226
|
+
if (typeof opts.formatHtml === "string") outputFormats.html = opts.formatHtml;
|
|
227
|
+
if (typeof opts.formatJson === "string") outputFormats.json = opts.formatJson;
|
|
228
|
+
if (typeof opts.formatJunit === "string") outputFormats.junit = opts.formatJunit;
|
|
229
|
+
if (typeof opts.formatMarkdown === "string") outputFormats.markdown = opts.formatMarkdown;
|
|
230
|
+
if (Object.keys(outputFormats).length > 0) rawDiffConfig.outputFormats = outputFormats;
|
|
231
|
+
const parseResult = DiffConfigSchema.safeParse(rawDiffConfig);
|
|
232
|
+
if (!parseResult.success) {
|
|
233
|
+
const issues = parseResult.error.issues;
|
|
234
|
+
const messages = issues.map((issue) => {
|
|
235
|
+
const path = issue.path.join(".");
|
|
236
|
+
return ` --${path}: ${issue.message}`;
|
|
237
|
+
});
|
|
238
|
+
throw new ConfigError(
|
|
239
|
+
issues[0].path.join(".") || "config",
|
|
240
|
+
`Invalid diff configuration:
|
|
241
|
+
${messages.join("\n")}`,
|
|
242
|
+
rawDiffConfig
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
logger.info(`Comparing bundles:
|
|
246
|
+
old: ${oldPath}
|
|
247
|
+
new: ${newPath}`);
|
|
248
|
+
const diffResult = await diffBundles(oldPath, newPath);
|
|
249
|
+
const s = diffResult.summary;
|
|
250
|
+
const totalChanges = s.routes.added + s.routes.removed + s.routes.changed + s.forms.added + s.forms.removed + s.forms.changed + s.api.added + s.api.removed + s.api.changed + s.screenshots.changed + s.screenshots.added + s.screenshots.removed;
|
|
251
|
+
logger.info(`Diff complete: ${totalChanges} change(s) found`);
|
|
252
|
+
if (s.routes.added > 0) logger.info(` Routes added: ${s.routes.added}`);
|
|
253
|
+
if (s.routes.removed > 0) logger.info(` Routes removed: ${s.routes.removed}`);
|
|
254
|
+
if (s.routes.changed > 0) logger.info(` Routes modified: ${s.routes.changed}`);
|
|
255
|
+
if (s.forms.changed > 0) logger.info(` Forms changed: ${s.forms.changed}`);
|
|
256
|
+
if (s.api.changed > 0) logger.info(` API endpoints changed: ${s.api.changed}`);
|
|
257
|
+
if (s.screenshots.changed > 0) logger.info(` Screenshots changed: ${s.screenshots.changed}`);
|
|
258
|
+
const { writeFile: writeFileAsync, mkdir: mkdirAsync } = await import("fs/promises");
|
|
259
|
+
if (typeof opts.formatHtml === "string") {
|
|
260
|
+
const html = generateDiffReportHtml(diffResult);
|
|
261
|
+
await writeFileAsync(resolve(opts.formatHtml), html, "utf-8");
|
|
262
|
+
logger.success(`HTML diff report: ${opts.formatHtml}`);
|
|
263
|
+
}
|
|
264
|
+
if (typeof opts.formatJson === "string") {
|
|
265
|
+
await writeFileAsync(resolve(opts.formatJson), JSON.stringify(diffResult, null, 2), "utf-8");
|
|
266
|
+
logger.success(`JSON diff report: ${opts.formatJson}`);
|
|
267
|
+
}
|
|
268
|
+
if (typeof opts.output === "string") {
|
|
269
|
+
const outDir = resolve(opts.output);
|
|
270
|
+
await mkdirAsync(outDir, { recursive: true });
|
|
271
|
+
const html = generateDiffReportHtml(diffResult);
|
|
272
|
+
await writeFileAsync(resolve(outDir, "diff-report.html"), html, "utf-8");
|
|
273
|
+
await writeFileAsync(resolve(outDir, "diff.json"), JSON.stringify(diffResult, null, 2), "utf-8");
|
|
274
|
+
logger.success(`Diff reports written to: ${outDir}`);
|
|
275
|
+
}
|
|
276
|
+
process.exit(totalChanges > 0 ? 1 : 0);
|
|
277
|
+
} catch (err) {
|
|
278
|
+
handleError(err);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
function handleError(err) {
|
|
282
|
+
if (err instanceof ConfigError) {
|
|
283
|
+
logger.error(`Configuration error: ${err.message}`);
|
|
284
|
+
process.exit(1);
|
|
285
|
+
}
|
|
286
|
+
if (err instanceof AuthError) {
|
|
287
|
+
logger.error(`Authentication error: ${err.message}`);
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
if (err instanceof DiffError) {
|
|
291
|
+
logger.error(`Diff error: ${err.message}`);
|
|
292
|
+
process.exit(2);
|
|
293
|
+
}
|
|
294
|
+
if (err instanceof ArchaeologistError) {
|
|
295
|
+
logger.error(`${err.name}: ${err.message}`);
|
|
296
|
+
if (err.cause instanceof Error) {
|
|
297
|
+
logger.debug(`Caused by: ${err.cause.message}`);
|
|
298
|
+
}
|
|
299
|
+
process.exit(2);
|
|
300
|
+
}
|
|
301
|
+
if (err instanceof Error) {
|
|
302
|
+
logger.error(err.message);
|
|
303
|
+
logger.debug(err.stack ?? "");
|
|
304
|
+
process.exit(1);
|
|
305
|
+
}
|
|
306
|
+
logger.error(`Unexpected error: ${String(err)}`);
|
|
307
|
+
process.exit(1);
|
|
308
|
+
}
|
|
309
|
+
program.parse();
|
|
310
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["/**\n * playwright-archaeologist CLI entry point.\n *\n * Parses command-line arguments with Commander.js, validates configuration\n * with Zod, and dispatches to the appropriate handler (dig, diff, install).\n */\n\nimport { Command, Option } from 'commander';\nimport { createRequire } from 'node:module';\nimport { resolve, dirname } from 'node:path';\nimport { execSync } from 'node:child_process';\nimport { accessSync, constants } from 'node:fs';\nimport {\n CrawlConfigSchema,\n DiffConfigSchema,\n normalizeEntryUrl,\n parseViewport,\n} from './types/config.js';\nimport type { ResolvedConfig, Viewport } from './types/config.js';\nimport { ConfigError, AuthError, ArchaeologistError, DiffError } from './types/errors.js';\nimport { logger } from './utils/logger.js';\nimport { dig } from './crawl/orchestrator.js';\nimport { diffBundles } from './diff/diff-engine.js';\nimport { generateDiffReportHtml } from './diff/diff-report.js';\nimport { createBundle } from './bundle/bundle-creator.js';\n\n// ---------------------------------------------------------------------------\n// Load package.json for version\n// ---------------------------------------------------------------------------\n\nconst require = createRequire(import.meta.url);\nconst pkg = require('../package.json') as { version: string };\n\n// ---------------------------------------------------------------------------\n// Viewport parsing helper\n// ---------------------------------------------------------------------------\n\n/**\n * Parse a viewport string \"WxH\" or throw a ConfigError.\n */\nfunction parseViewportStrict(input: string, fieldName: string): Viewport {\n const vp = parseViewport(input);\n if (!vp) {\n throw new ConfigError(\n fieldName,\n `Invalid viewport format \"${input}\". Expected WxH (e.g. 1280x720). Width must be 320-7680, height 240-4320.`,\n input,\n );\n }\n return vp;\n}\n\n/**\n * Parse a comma-separated list of viewport strings.\n */\nfunction parseViewportList(input: string): Viewport[] {\n const parts = input.split(',').map((s) => s.trim()).filter(Boolean);\n if (parts.length === 0) {\n throw new ConfigError('viewports', 'No viewports specified in --viewports.', input);\n }\n return parts.map((part) => parseViewportStrict(part, 'viewports'));\n}\n\n/**\n * Collect repeatable --include / --exclude options into arrays.\n */\nfunction collectArray(value: string, previous: string[]): string[] {\n return previous.concat(value);\n}\n\n/**\n * Parse an integer option with a descriptive name for errors.\n */\nfunction parseIntOption(value: string, name: string): number {\n const n = parseInt(value, 10);\n if (!Number.isFinite(n)) {\n throw new ConfigError(name, `\"${value}\" is not a valid integer for --${name}.`, value);\n }\n return n;\n}\n\n// ---------------------------------------------------------------------------\n// Program\n// ---------------------------------------------------------------------------\n\nconst program = new Command();\n\nprogram\n .name('playwright-archaeologist')\n .description('Generate a complete behavioral specification of any web app')\n .version(pkg.version, '-V, --version');\n\n// ---------------------------------------------------------------------------\n// `dig` command\n// ---------------------------------------------------------------------------\n\nprogram\n .command('dig')\n .description('Crawl a website and generate a behavioral specification')\n .argument('<url>', 'URL to crawl (https:// is prepended if no protocol)')\n\n // -- Discovery options --\n .option('-d, --depth <n>', 'Maximum crawl depth from entry URL (default: 5)', (v) => parseIntOption(v, 'depth'))\n .option('--max-pages <n>', 'Maximum total pages to visit (default: 1000)', (v) => parseIntOption(v, 'max-pages'))\n .option('--include <pattern>', 'URL glob patterns to include (repeatable)', collectArray, [])\n .option('--exclude <pattern>', 'URL glob patterns to exclude (repeatable)', collectArray, [])\n .option('--follow-external', 'Follow links to external origins', false)\n .option('--deep-click', 'Click non-link interactive elements to discover SPA states', false)\n .option('--include-iframes', 'Crawl cross-origin iframe content', false)\n\n // -- Performance options --\n .option('-c, --concurrency <n>', 'Number of parallel browser contexts (default: 3)', (v) => parseIntOption(v, 'concurrency'))\n .option('--delay <ms>', 'Delay between page visits per context in ms (default: 0)', (v) => parseIntOption(v, 'delay'))\n .option('--timeout <ms>', 'Per-page navigation timeout in ms (default: 30000)', (v) => parseIntOption(v, 'timeout'))\n .option('--max-time <s>', 'Global crawl timeout in seconds (default: 3600)', (v) => parseIntOption(v, 'max-time'))\n\n // -- Auth options --\n .option('--auth <script>', 'Path to auth script (.ts or .js)')\n .option('--cookies <file>', 'Path to cookies JSON file for session injection')\n .option('--include-cookies', 'Include cookies/auth headers in output (default: scrubbed)', false)\n\n // -- Output options --\n .option('-o, --output <dir>', 'Output directory (default: .archaeologist)')\n .option('-f, --format <fmt>', 'Output format: html, json, openapi, both (default: both)')\n .option('--no-screenshots', 'Skip screenshot capture')\n .option('--no-har', 'Skip HAR recording')\n\n // -- Display options --\n .option('--viewport <WxH>', 'Primary viewport dimensions (default: 1280x720)')\n .option('--viewports <list>', 'Additional viewports for responsive screenshots (comma-separated, e.g. 375x667,768x1024)')\n\n // -- Resume options --\n .option('--resume', 'Resume from last checkpoint in the output directory', false)\n .option('-y, --yes', 'Skip confirmation prompts', false)\n\n // -- Security options --\n .option('--allow-private', 'Allow crawling private/internal IP ranges', false)\n\n // -- Debug options --\n .option('-v, --verbose', 'Enable debug logging', false)\n\n .action(async (url: string, opts: Record<string, unknown>) => {\n // Enable debug logging first so all subsequent logs are visible\n if (opts.verbose) {\n logger.setLevel('debug');\n }\n\n logger.debug('Raw CLI options: %o', opts);\n\n try {\n // --- Normalize the entry URL ---\n const { url: normalizedUrl, warnings } = normalizeEntryUrl(url);\n for (const w of warnings) {\n logger.warn(w);\n }\n\n // --- Validate auth script path ---\n let authScript: string | undefined;\n if (typeof opts.auth === 'string') {\n authScript = resolve(opts.auth);\n try {\n accessSync(authScript, constants.R_OK);\n } catch {\n throw new AuthError(\n 'script_not_found',\n `Auth script not found: ${authScript}`,\n authScript,\n );\n }\n }\n\n // --- Validate cookies file path ---\n let cookiesFile: string | undefined;\n if (typeof opts.cookies === 'string') {\n cookiesFile = resolve(opts.cookies);\n try {\n accessSync(cookiesFile, constants.R_OK);\n } catch {\n throw new AuthError(\n 'cookie_file_not_found' as 'cookie_file_malformed',\n `Cookies file not found: ${cookiesFile}`,\n cookiesFile,\n );\n }\n }\n\n // --- Parse viewport ---\n let viewport: Viewport | undefined;\n if (typeof opts.viewport === 'string') {\n viewport = parseViewportStrict(opts.viewport, 'viewport');\n }\n\n // --- Parse additional viewports ---\n let additionalViewports: Viewport[] | undefined;\n if (typeof opts.viewports === 'string') {\n additionalViewports = parseViewportList(opts.viewports);\n }\n\n // --- Resolve output directory ---\n const outputDir = resolve(typeof opts.output === 'string' ? opts.output : '.archaeologist');\n\n // Validate parent directory exists\n const parentDir = dirname(outputDir);\n try {\n accessSync(parentDir, constants.W_OK);\n } catch {\n throw new ConfigError(\n 'outputDir',\n `Output directory parent does not exist or is not writable: ${parentDir}`,\n outputDir,\n );\n }\n\n // --- Build the raw config object for Zod validation ---\n const rawConfig: Record<string, unknown> = {\n targetUrl: normalizedUrl,\n outputDir,\n };\n\n if (opts.depth !== undefined) rawConfig.depth = opts.depth;\n if (opts.maxPages !== undefined) rawConfig.maxPages = opts.maxPages;\n if (opts.include && (opts.include as string[]).length > 0) rawConfig.include = opts.include;\n if (opts.exclude && (opts.exclude as string[]).length > 0) rawConfig.exclude = opts.exclude;\n if (opts.followExternal) rawConfig.followExternal = true;\n if (opts.deepClick) rawConfig.deepClick = true;\n if (opts.includeIframes) rawConfig.includeIframes = true;\n if (opts.concurrency !== undefined) rawConfig.concurrency = opts.concurrency;\n if (opts.delay !== undefined) rawConfig.delay = opts.delay;\n if (opts.timeout !== undefined) rawConfig.timeout = opts.timeout;\n if (opts.maxTime !== undefined) rawConfig.maxTime = opts.maxTime;\n if (authScript) rawConfig.authScript = authScript;\n if (cookiesFile) rawConfig.cookiesFile = cookiesFile;\n if (opts.includeCookies) rawConfig.includeCookies = true;\n if (typeof opts.format === 'string') rawConfig.format = opts.format;\n // Commander uses --no-screenshots to set opts.screenshots = false\n if (opts.screenshots === false) rawConfig.noScreenshots = true;\n if (opts.har === false) rawConfig.noHar = true;\n if (viewport) rawConfig.viewport = viewport;\n if (additionalViewports) rawConfig.additionalViewports = additionalViewports;\n if (opts.resume) rawConfig.resume = true;\n if (opts.yes) rawConfig.yes = true;\n if (opts.allowPrivate) rawConfig.allowPrivate = true;\n\n // --- Validate with Zod ---\n const parseResult = CrawlConfigSchema.safeParse(rawConfig);\n\n if (!parseResult.success) {\n const issues = parseResult.error.issues;\n const messages = issues.map((issue) => {\n const path = issue.path.join('.');\n return ` --${path}: ${issue.message}`;\n });\n throw new ConfigError(\n issues[0].path.join('.') || 'config',\n `Invalid configuration:\\n${messages.join('\\n')}`,\n rawConfig,\n );\n }\n\n const validatedConfig = parseResult.data;\n\n // --- Build ResolvedConfig ---\n const resolvedConfig: ResolvedConfig = {\n ...validatedConfig,\n outputDir: resolve(validatedConfig.outputDir),\n authScript: validatedConfig.authScript ? resolve(validatedConfig.authScript) : undefined,\n cookiesFile: validatedConfig.cookiesFile ? resolve(validatedConfig.cookiesFile) : undefined,\n warnings,\n };\n\n logger.debug('Resolved config: %o', resolvedConfig);\n\n // --- Run the crawl ---\n const result = await dig(resolvedConfig);\n\n logger.success(\n `Crawl complete. ${result.artifacts.meta.pagesVisited} pages visited in ${result.artifacts.meta.duration}ms.`,\n );\n\n if (result.errors.length > 0) {\n logger.warn(`${result.errors.length} error(s) encountered during crawl.`);\n }\n\n // Create regression baseline bundle\n const bundlePath = resolve(outputDir, 'bundle');\n try {\n const manifest = await createBundle(outputDir, bundlePath);\n logger.success(`Bundle created: ${bundlePath} (${manifest.files.length} files)`);\n } catch (bundleErr) {\n logger.warn(`Bundle creation failed: ${bundleErr instanceof Error ? bundleErr.message : String(bundleErr)}`);\n }\n\n process.exit(0);\n } catch (err) {\n handleError(err);\n }\n });\n\n// ---------------------------------------------------------------------------\n// `install` command\n// ---------------------------------------------------------------------------\n\nprogram\n .command('install')\n .description('Install Playwright Chromium browser')\n .action(() => {\n try {\n logger.info('Installing Playwright Chromium browser...');\n execSync('npx playwright install chromium', {\n stdio: 'inherit',\n timeout: 300_000, // 5 minute timeout\n });\n logger.success('Chromium browser installed successfully.');\n } catch (err) {\n logger.error('Failed to install Chromium browser.');\n if (err instanceof Error) {\n logger.debug(err.message);\n }\n process.exit(1);\n }\n });\n\n// ---------------------------------------------------------------------------\n// `diff` command\n// ---------------------------------------------------------------------------\n\nprogram\n .command('diff')\n .description('Compare two .archaeologist bundles and generate a diff report')\n .argument('<old>', 'Path to the older .archaeologist bundle')\n .argument('<new>', 'Path to the newer .archaeologist bundle')\n .option('-o, --output <dir>', 'Output directory for the diff report')\n .option('--threshold <n>', 'Pixel diff threshold 0-1 (default: 0.1)', parseFloat)\n .option('--max-ratio <n>', 'Max diff ratio 0-100 before marking changed (default: 0.5)', parseFloat)\n .option('--ignore-fields <fields>', 'Comma-separated fields to ignore in API diff')\n .option('--normalize-dynamic', 'Normalize dynamic values (UUIDs, timestamps) before diffing', false)\n .option('--detailed', 'Show detailed value-level diffs (default: schema-level only)', false)\n .option('--format-html <path>', 'Output HTML diff report to this path')\n .option('--format-json <path>', 'Output JSON diff report to this path')\n .option('--format-junit <path>', 'Output JUnit XML diff report to this path')\n .option('--format-markdown <path>', 'Output Markdown diff report to this path')\n .option('-v, --verbose', 'Enable debug logging', false)\n .action(async (oldBundle: string, newBundle: string, opts: Record<string, unknown>) => {\n if (opts.verbose) {\n logger.setLevel('debug');\n }\n\n try {\n // Validate bundle paths exist\n const oldPath = resolve(oldBundle);\n const newPath = resolve(newBundle);\n\n try {\n accessSync(oldPath, constants.R_OK);\n } catch {\n throw new ConfigError('oldBundle', `Old bundle not found: ${oldPath}`, oldPath);\n }\n\n try {\n accessSync(newPath, constants.R_OK);\n } catch {\n throw new ConfigError('newBundle', `New bundle not found: ${newPath}`, newPath);\n }\n\n // Build diff config\n const rawDiffConfig: Record<string, unknown> = {\n oldBundle: oldPath,\n newBundle: newPath,\n };\n\n if (typeof opts.output === 'string') rawDiffConfig.outputDir = opts.output;\n if (opts.threshold !== undefined) rawDiffConfig.diffThreshold = opts.threshold;\n if (opts.maxRatio !== undefined) rawDiffConfig.diffMaxRatio = opts.maxRatio;\n if (typeof opts.ignoreFields === 'string') {\n rawDiffConfig.diffIgnoreFields = opts.ignoreFields.split(',').map((s: string) => s.trim()).filter(Boolean);\n }\n if (opts.normalizeDynamic) rawDiffConfig.normalizeDynamicValues = true;\n if (opts.detailed) rawDiffConfig.detailed = true;\n\n // Build output formats\n const outputFormats: Record<string, string> = {};\n if (typeof opts.formatHtml === 'string') outputFormats.html = opts.formatHtml;\n if (typeof opts.formatJson === 'string') outputFormats.json = opts.formatJson;\n if (typeof opts.formatJunit === 'string') outputFormats.junit = opts.formatJunit;\n if (typeof opts.formatMarkdown === 'string') outputFormats.markdown = opts.formatMarkdown;\n if (Object.keys(outputFormats).length > 0) rawDiffConfig.outputFormats = outputFormats;\n\n // Validate with Zod\n const parseResult = DiffConfigSchema.safeParse(rawDiffConfig);\n\n if (!parseResult.success) {\n const issues = parseResult.error.issues;\n const messages = issues.map((issue) => {\n const path = issue.path.join('.');\n return ` --${path}: ${issue.message}`;\n });\n throw new ConfigError(\n issues[0].path.join('.') || 'config',\n `Invalid diff configuration:\\n${messages.join('\\n')}`,\n rawDiffConfig,\n );\n }\n\n logger.info(`Comparing bundles:\\n old: ${oldPath}\\n new: ${newPath}`);\n\n // Run diff engine\n const diffResult = await diffBundles(oldPath, newPath);\n\n // Terminal summary\n const s = diffResult.summary;\n const totalChanges =\n s.routes.added + s.routes.removed + s.routes.changed +\n s.forms.added + s.forms.removed + s.forms.changed +\n s.api.added + s.api.removed + s.api.changed +\n s.screenshots.changed + s.screenshots.added + s.screenshots.removed;\n logger.info(`Diff complete: ${totalChanges} change(s) found`);\n if (s.routes.added > 0) logger.info(` Routes added: ${s.routes.added}`);\n if (s.routes.removed > 0) logger.info(` Routes removed: ${s.routes.removed}`);\n if (s.routes.changed > 0) logger.info(` Routes modified: ${s.routes.changed}`);\n if (s.forms.changed > 0) logger.info(` Forms changed: ${s.forms.changed}`);\n if (s.api.changed > 0) logger.info(` API endpoints changed: ${s.api.changed}`);\n if (s.screenshots.changed > 0) logger.info(` Screenshots changed: ${s.screenshots.changed}`);\n\n // Write outputs\n const { writeFile: writeFileAsync, mkdir: mkdirAsync } = await import('node:fs/promises');\n\n if (typeof opts.formatHtml === 'string') {\n const html = generateDiffReportHtml(diffResult);\n await writeFileAsync(resolve(opts.formatHtml), html, 'utf-8');\n logger.success(`HTML diff report: ${opts.formatHtml}`);\n }\n\n if (typeof opts.formatJson === 'string') {\n await writeFileAsync(resolve(opts.formatJson), JSON.stringify(diffResult, null, 2), 'utf-8');\n logger.success(`JSON diff report: ${opts.formatJson}`);\n }\n\n if (typeof opts.output === 'string') {\n const outDir = resolve(opts.output as string);\n await mkdirAsync(outDir, { recursive: true });\n const html = generateDiffReportHtml(diffResult);\n await writeFileAsync(resolve(outDir, 'diff-report.html'), html, 'utf-8');\n await writeFileAsync(resolve(outDir, 'diff.json'), JSON.stringify(diffResult, null, 2), 'utf-8');\n logger.success(`Diff reports written to: ${outDir}`);\n }\n\n // Exit codes: 0 = identical, 1 = changes detected\n process.exit(totalChanges > 0 ? 1 : 0);\n } catch (err) {\n handleError(err);\n }\n });\n\n// ---------------------------------------------------------------------------\n// Error handler\n// ---------------------------------------------------------------------------\n\nfunction handleError(err: unknown): never {\n if (err instanceof ConfigError) {\n logger.error(`Configuration error: ${err.message}`);\n process.exit(1);\n }\n\n if (err instanceof AuthError) {\n logger.error(`Authentication error: ${err.message}`);\n process.exit(1);\n }\n\n if (err instanceof DiffError) {\n logger.error(`Diff error: ${err.message}`);\n process.exit(2);\n }\n\n if (err instanceof ArchaeologistError) {\n logger.error(`${err.name}: ${err.message}`);\n if (err.cause instanceof Error) {\n logger.debug(`Caused by: ${err.cause.message}`);\n }\n process.exit(2);\n }\n\n if (err instanceof Error) {\n logger.error(err.message);\n logger.debug(err.stack ?? '');\n process.exit(1);\n }\n\n logger.error(`Unexpected error: ${String(err)}`);\n process.exit(1);\n}\n\n// ---------------------------------------------------------------------------\n// Parse and run\n// ---------------------------------------------------------------------------\n\nprogram.parse();\n"],"mappings":"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","names":["require","s"]}
|