zefiro 0.8.1 → 0.10.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.
@@ -0,0 +1,264 @@
1
+ import {
2
+ __commonJS,
3
+ __require,
4
+ __toESM
5
+ } from "./cli-wckvcay0.js";
6
+
7
+ // ../../node_modules/.bun/picocolors@1.1.1/node_modules/picocolors/picocolors.js
8
+ var require_picocolors = __commonJS((exports, module) => {
9
+ var p = process || {};
10
+ var argv = p.argv || [];
11
+ var env = p.env || {};
12
+ var isColorSupported = !(!!env.NO_COLOR || argv.includes("--no-color")) && (!!env.FORCE_COLOR || argv.includes("--color") || p.platform === "win32" || (p.stdout || {}).isTTY && env.TERM !== "dumb" || !!env.CI);
13
+ var formatter = (open, close, replace = open) => (input) => {
14
+ let string = "" + input, index = string.indexOf(close, open.length);
15
+ return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close;
16
+ };
17
+ var replaceClose = (string, close, replace, index) => {
18
+ let result = "", cursor = 0;
19
+ do {
20
+ result += string.substring(cursor, index) + replace;
21
+ cursor = index + close.length;
22
+ index = string.indexOf(close, cursor);
23
+ } while (~index);
24
+ return result + string.substring(cursor);
25
+ };
26
+ var createColors = (enabled = isColorSupported) => {
27
+ let f = enabled ? formatter : () => String;
28
+ return {
29
+ isColorSupported: enabled,
30
+ reset: f("\x1B[0m", "\x1B[0m"),
31
+ bold: f("\x1B[1m", "\x1B[22m", "\x1B[22m\x1B[1m"),
32
+ dim: f("\x1B[2m", "\x1B[22m", "\x1B[22m\x1B[2m"),
33
+ italic: f("\x1B[3m", "\x1B[23m"),
34
+ underline: f("\x1B[4m", "\x1B[24m"),
35
+ inverse: f("\x1B[7m", "\x1B[27m"),
36
+ hidden: f("\x1B[8m", "\x1B[28m"),
37
+ strikethrough: f("\x1B[9m", "\x1B[29m"),
38
+ black: f("\x1B[30m", "\x1B[39m"),
39
+ red: f("\x1B[31m", "\x1B[39m"),
40
+ green: f("\x1B[32m", "\x1B[39m"),
41
+ yellow: f("\x1B[33m", "\x1B[39m"),
42
+ blue: f("\x1B[34m", "\x1B[39m"),
43
+ magenta: f("\x1B[35m", "\x1B[39m"),
44
+ cyan: f("\x1B[36m", "\x1B[39m"),
45
+ white: f("\x1B[37m", "\x1B[39m"),
46
+ gray: f("\x1B[90m", "\x1B[39m"),
47
+ bgBlack: f("\x1B[40m", "\x1B[49m"),
48
+ bgRed: f("\x1B[41m", "\x1B[49m"),
49
+ bgGreen: f("\x1B[42m", "\x1B[49m"),
50
+ bgYellow: f("\x1B[43m", "\x1B[49m"),
51
+ bgBlue: f("\x1B[44m", "\x1B[49m"),
52
+ bgMagenta: f("\x1B[45m", "\x1B[49m"),
53
+ bgCyan: f("\x1B[46m", "\x1B[49m"),
54
+ bgWhite: f("\x1B[47m", "\x1B[49m"),
55
+ blackBright: f("\x1B[90m", "\x1B[39m"),
56
+ redBright: f("\x1B[91m", "\x1B[39m"),
57
+ greenBright: f("\x1B[92m", "\x1B[39m"),
58
+ yellowBright: f("\x1B[93m", "\x1B[39m"),
59
+ blueBright: f("\x1B[94m", "\x1B[39m"),
60
+ magentaBright: f("\x1B[95m", "\x1B[39m"),
61
+ cyanBright: f("\x1B[96m", "\x1B[39m"),
62
+ whiteBright: f("\x1B[97m", "\x1B[39m"),
63
+ bgBlackBright: f("\x1B[100m", "\x1B[49m"),
64
+ bgRedBright: f("\x1B[101m", "\x1B[49m"),
65
+ bgGreenBright: f("\x1B[102m", "\x1B[49m"),
66
+ bgYellowBright: f("\x1B[103m", "\x1B[49m"),
67
+ bgBlueBright: f("\x1B[104m", "\x1B[49m"),
68
+ bgMagentaBright: f("\x1B[105m", "\x1B[49m"),
69
+ bgCyanBright: f("\x1B[106m", "\x1B[49m"),
70
+ bgWhiteBright: f("\x1B[107m", "\x1B[49m")
71
+ };
72
+ };
73
+ module.exports = createColors();
74
+ module.exports.createColors = createColors;
75
+ });
76
+
77
+ // src/utils/logger.ts
78
+ var import_picocolors = __toESM(require_picocolors(), 1);
79
+ var verboseEnabled = false;
80
+ function setVerbose(enabled) {
81
+ verboseEnabled = enabled;
82
+ }
83
+ function info(msg) {
84
+ console.log(import_picocolors.default.blue("i") + " " + msg);
85
+ }
86
+ function success(msg) {
87
+ console.log(import_picocolors.default.green("✓") + " " + msg);
88
+ }
89
+ function warn(msg) {
90
+ console.log(import_picocolors.default.yellow("!") + " " + msg);
91
+ }
92
+ function error(msg) {
93
+ console.error(import_picocolors.default.red("✗") + " " + msg);
94
+ }
95
+ function verbose(msg) {
96
+ if (verboseEnabled) {
97
+ console.log(import_picocolors.default.gray(" " + msg));
98
+ }
99
+ }
100
+ function header(title) {
101
+ console.log(`
102
+ ` + import_picocolors.default.bold(import_picocolors.default.magenta(title)));
103
+ console.log(import_picocolors.default.gray("─".repeat(title.length + 4)));
104
+ }
105
+ function banner(version) {
106
+ const title = ` zefiro v${version}`;
107
+ const subtitle = " AI-powered application explorer";
108
+ const width = Math.max(title.length, subtitle.length) + 2;
109
+ const top = import_picocolors.default.cyan("┌" + "─".repeat(width) + "┐");
110
+ const mid1 = import_picocolors.default.cyan("│") + import_picocolors.default.bold(title) + " ".repeat(width - title.length) + import_picocolors.default.cyan("│");
111
+ const mid2 = import_picocolors.default.cyan("│") + import_picocolors.default.gray(subtitle) + " ".repeat(width - subtitle.length) + import_picocolors.default.cyan("│");
112
+ const bottom = import_picocolors.default.cyan("└" + "─".repeat(width) + "┘");
113
+ console.log(top);
114
+ console.log(mid1);
115
+ console.log(mid2);
116
+ console.log(bottom);
117
+ }
118
+
119
+ // src/browser/agent-browser.ts
120
+ import { execSync } from "node:child_process";
121
+ class AgentBrowser {
122
+ session;
123
+ headed;
124
+ constructor(opts = {}) {
125
+ this.session = opts.session ?? "zefiro";
126
+ this.headed = opts.headed ?? false;
127
+ }
128
+ exec(args, opts) {
129
+ const sessionArgs = ["--session", this.session];
130
+ const headedArgs = this.headed ? ["--headed"] : [];
131
+ const cmd = ["agent-browser", ...headedArgs, ...sessionArgs, ...args].join(" ");
132
+ verbose(`> ${cmd}`);
133
+ try {
134
+ return execSync(cmd, {
135
+ encoding: "utf-8",
136
+ timeout: opts?.timeout ?? 60000,
137
+ stdio: ["pipe", "pipe", "pipe"]
138
+ }).trim();
139
+ } catch (err) {
140
+ const stderr = err.stderr?.toString().trim() ?? "";
141
+ const stdout = err.stdout?.toString().trim() ?? "";
142
+ throw new Error(`agent-browser failed: ${stderr || stdout || err.message}`);
143
+ }
144
+ }
145
+ async open(url) {
146
+ this.exec(["open", url]);
147
+ }
148
+ async waitForLoad(strategy = "networkidle", delayMs = 1500) {
149
+ try {
150
+ this.exec(["wait", "--load", strategy], { timeout: 30000 });
151
+ } catch {
152
+ verbose("Wait for load timed out, continuing");
153
+ }
154
+ if (delayMs > 0) {
155
+ this.exec(["wait", String(delayMs)]);
156
+ }
157
+ }
158
+ async snapshot() {
159
+ const output = this.exec(["snapshot", "-i"]);
160
+ return parseSnapshot(output);
161
+ }
162
+ async screenshot(filePath) {
163
+ this.exec(["screenshot", filePath]);
164
+ }
165
+ async click(ref) {
166
+ this.exec(["click", ref]);
167
+ }
168
+ async fill(ref, text) {
169
+ this.exec(["fill", ref, `"${text}"`]);
170
+ }
171
+ async getAttribute(ref, attr) {
172
+ return this.exec(["get", "attr", ref, attr]);
173
+ }
174
+ async getUrl() {
175
+ return this.exec(["get", "url"]);
176
+ }
177
+ async getTitle() {
178
+ return this.exec(["get", "title"]);
179
+ }
180
+ async stateSave(filePath) {
181
+ this.exec(["state", "save", filePath]);
182
+ }
183
+ async stateLoad(filePath) {
184
+ this.exec(["state", "load", filePath]);
185
+ }
186
+ async close() {
187
+ try {
188
+ this.exec(["close"]);
189
+ } catch {}
190
+ }
191
+ async setViewport(width, height) {
192
+ this.exec(["set", "viewport", String(width), String(height)]);
193
+ }
194
+ }
195
+ function parseSnapshot(output) {
196
+ const elements = [];
197
+ const lines = output.split(`
198
+ `);
199
+ for (const line of lines) {
200
+ const match = line.match(/@(e\d+)\s+\[(\w+)\]\s+"([^"]*)"/);
201
+ if (match) {
202
+ elements.push({
203
+ ref: `@${match[1]}`,
204
+ role: match[2],
205
+ label: match[3]
206
+ });
207
+ continue;
208
+ }
209
+ const altMatch = line.match(/@(e\d+)\s+\[(\w+)\]\s+(.*)/);
210
+ if (altMatch) {
211
+ elements.push({
212
+ ref: `@${altMatch[1]}`,
213
+ role: altMatch[2],
214
+ label: altMatch[3].trim().replace(/^"|"$/g, "")
215
+ });
216
+ }
217
+ }
218
+ return elements;
219
+ }
220
+
221
+ // src/browser/auth.ts
222
+ import { existsSync } from "node:fs";
223
+ import { join } from "node:path";
224
+ import { createInterface } from "node:readline";
225
+ var AUTH_STATE_FILE = ".qai/zefiro/auth-state.json";
226
+ async function authenticate(browser, config) {
227
+ const { method, stateFile } = config.auth;
228
+ if (method === "state-file") {
229
+ const path = stateFile ?? AUTH_STATE_FILE;
230
+ if (!existsSync(path)) {
231
+ throw new Error(`Auth state file not found: ${path}`);
232
+ }
233
+ info(`Loading auth state from ${path}`);
234
+ await browser.stateLoad(path);
235
+ return path;
236
+ }
237
+ info("Opening browser for manual authentication...");
238
+ info("Please log in to the application, then press Enter to continue.");
239
+ await browser.open(config.baseUrl);
240
+ await browser.waitForLoad(config.waitStrategy, config.waitDelay);
241
+ await waitForEnter();
242
+ const savePath = join(process.cwd(), AUTH_STATE_FILE);
243
+ try {
244
+ const { mkdirSync } = await import("node:fs");
245
+ const { dirname } = await import("node:path");
246
+ mkdirSync(dirname(savePath), { recursive: true });
247
+ await browser.stateSave(savePath);
248
+ success(`Auth state saved to ${AUTH_STATE_FILE} (reuse with --auth-state)`);
249
+ } catch (err) {
250
+ warn(`Could not save auth state: ${err.message}`);
251
+ }
252
+ return savePath;
253
+ }
254
+ function waitForEnter() {
255
+ return new Promise((resolve) => {
256
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
257
+ rl.question("", () => {
258
+ rl.close();
259
+ resolve();
260
+ });
261
+ });
262
+ }
263
+
264
+ export { require_picocolors, setVerbose, info, success, warn, error, verbose, header, banner, AgentBrowser, authenticate };
@@ -0,0 +1,145 @@
1
+ import {
2
+ classifySectionType,
3
+ generateEdgeId,
4
+ generateSectionId
5
+ } from "./cli-5w708rbb.js";
6
+
7
+ // src/browser/graph-builder.ts
8
+ function toReportNodeType(sectionType) {
9
+ const classified = classifySectionType(sectionType);
10
+ switch (classified) {
11
+ case "form":
12
+ return "page";
13
+ case "table":
14
+ return "page";
15
+ case "tab":
16
+ return "page";
17
+ default:
18
+ return classified;
19
+ }
20
+ }
21
+ var SECTION_ICONS = {
22
+ home: "home",
23
+ dashboard: "layout-dashboard",
24
+ settings: "settings",
25
+ auth: "shield",
26
+ login: "log-in",
27
+ users: "users",
28
+ admin: "shield-check",
29
+ reports: "bar-chart",
30
+ analytics: "trending-up"
31
+ };
32
+ function guessIcon(slug) {
33
+ for (const [key, icon] of Object.entries(SECTION_ICONS)) {
34
+ if (slug.includes(key))
35
+ return icon;
36
+ }
37
+ return;
38
+ }
39
+ function buildReportGraph(state) {
40
+ const nodes = [];
41
+ const edges = [];
42
+ const edgeSet = new Set;
43
+ const visitedPages = Array.from(state.pages.values()).filter((p) => p.status === "visited");
44
+ const slugToStableId = new Map;
45
+ for (const page of visitedPages) {
46
+ slugToStableId.set(page.slug, page.stableId);
47
+ nodes.push({
48
+ id: page.stableId,
49
+ sectionSlug: page.slug,
50
+ type: "page",
51
+ label: page.analysis?.pageName ?? page.title ?? page.slug,
52
+ description: page.analysis?.description ?? `Page at ${page.path}`,
53
+ route: page.path,
54
+ screenshotFilename: "01-default.png"
55
+ });
56
+ if (page.analysis?.sections) {
57
+ for (const section of page.analysis.sections) {
58
+ const sectionId = section.stableId ?? generateSectionId(page.slug, section.name);
59
+ const nodeType = toReportNodeType(section.type);
60
+ nodes.push({
61
+ id: sectionId,
62
+ sectionSlug: page.slug,
63
+ type: nodeType,
64
+ label: section.name,
65
+ description: `${section.type} section with ${section.elements.length} elements`
66
+ });
67
+ const containEdgeId = generateEdgeId(page.stableId, sectionId);
68
+ if (!edgeSet.has(containEdgeId)) {
69
+ edgeSet.add(containEdgeId);
70
+ edges.push({
71
+ id: containEdgeId,
72
+ source: page.stableId,
73
+ target: sectionId,
74
+ label: "contains"
75
+ });
76
+ }
77
+ }
78
+ }
79
+ }
80
+ const urlToStableId = new Map;
81
+ for (const page of visitedPages) {
82
+ urlToStableId.set(page.url, page.stableId);
83
+ }
84
+ for (const page of visitedPages) {
85
+ for (const linkUrl of page.outgoingLinks) {
86
+ const targetId = urlToStableId.get(linkUrl);
87
+ if (targetId && targetId !== page.stableId) {
88
+ const navEdgeId = generateEdgeId(page.stableId, targetId);
89
+ if (!edgeSet.has(navEdgeId)) {
90
+ edgeSet.add(navEdgeId);
91
+ edges.push({
92
+ id: navEdgeId,
93
+ source: page.stableId,
94
+ target: targetId,
95
+ label: "navigates"
96
+ });
97
+ }
98
+ }
99
+ }
100
+ }
101
+ const sectionGroups = buildSectionGroups(state.topology, visitedPages);
102
+ return { nodes, edges, sectionGroups };
103
+ }
104
+ function buildSectionGroups(topology, visitedPages) {
105
+ const groups = [];
106
+ const assignedSlugs = new Set;
107
+ for (const branch of topology) {
108
+ const descendantSlugs = collectSlugs(branch);
109
+ for (const s of descendantSlugs)
110
+ assignedSlugs.add(s);
111
+ groups.push({
112
+ sectionSlug: branch.slug,
113
+ name: branch.title || branch.slug,
114
+ icon: guessIcon(branch.slug)
115
+ });
116
+ }
117
+ for (const page of visitedPages) {
118
+ if (!assignedSlugs.has(page.slug)) {
119
+ groups.push({
120
+ sectionSlug: page.slug,
121
+ name: page.analysis?.pageName ?? page.title ?? page.slug,
122
+ icon: guessIcon(page.slug)
123
+ });
124
+ }
125
+ }
126
+ return groups;
127
+ }
128
+ function collectSlugs(node) {
129
+ const slugs = [node.slug];
130
+ for (const child of node.children) {
131
+ slugs.push(...collectSlugs(child));
132
+ }
133
+ return slugs;
134
+ }
135
+ function buildReportSections(visitedPages, sectionMarkdowns) {
136
+ return visitedPages.map((page, i) => ({
137
+ slug: page.slug,
138
+ name: page.analysis?.pageName ?? page.title ?? page.slug,
139
+ description: page.analysis?.description,
140
+ markdown: sectionMarkdowns.get(page.slug) ?? "",
141
+ sortOrder: i
142
+ }));
143
+ }
144
+
145
+ export { buildReportGraph, buildReportSections };
@@ -0,0 +1,133 @@
1
+ import {
2
+ AgentBrowser,
3
+ authenticate,
4
+ error,
5
+ info,
6
+ success,
7
+ verbose
8
+ } from "./cli-b26q1e27.js";
9
+ import {
10
+ ensureDir,
11
+ writeFile
12
+ } from "./cli-zvk8gwe4.js";
13
+ import {
14
+ buildTopology,
15
+ extractHrefsFromElements,
16
+ generateSectionMarkdown,
17
+ normalizeUrl,
18
+ shouldVisit,
19
+ urlToSlug
20
+ } from "./cli-6qr9gvkp.js";
21
+
22
+ // src/browser/explorer.ts
23
+ import { join } from "node:path";
24
+ async function runExploration(config) {
25
+ const state = {
26
+ baseUrl: config.baseUrl,
27
+ startedAt: new Date().toISOString(),
28
+ pages: new Map,
29
+ queue: [],
30
+ topology: []
31
+ };
32
+ const browser = new AgentBrowser({
33
+ session: config.sessionName,
34
+ headed: config.headed || config.auth.method === "manual"
35
+ });
36
+ const visited = new Set;
37
+ const screenshotsDir = join(config.outputDir, "screenshots");
38
+ const sectionsDir = join(config.outputDir, "sections");
39
+ ensureDir(screenshotsDir);
40
+ ensureDir(sectionsDir);
41
+ try {
42
+ await authenticate(browser, config);
43
+ await browser.setViewport(1440, 900);
44
+ const startUrl = normalizeUrl(config.baseUrl, config.baseUrl);
45
+ state.queue.push(startUrl);
46
+ let pageCount = 0;
47
+ while (state.queue.length > 0 && pageCount < config.maxPages) {
48
+ const url = state.queue.shift();
49
+ const normalized = normalizeUrl(url, config.baseUrl);
50
+ if (visited.has(normalized))
51
+ continue;
52
+ if (!shouldVisit(url, config.baseUrl, config.excludePatterns, visited)) {
53
+ continue;
54
+ }
55
+ visited.add(normalized);
56
+ const slug = urlToSlug(url, config.baseUrl);
57
+ info(`[${pageCount + 1}/${config.maxPages}] Exploring: ${url}`);
58
+ const page = {
59
+ url: normalized,
60
+ path: new URL(normalized).pathname,
61
+ title: "",
62
+ slug,
63
+ depth: 0,
64
+ screenshot: "",
65
+ elements: [],
66
+ outgoingLinks: [],
67
+ status: "pending"
68
+ };
69
+ try {
70
+ await browser.open(url);
71
+ await browser.waitForLoad(config.waitStrategy, config.waitDelay);
72
+ const actualUrl = await browser.getUrl();
73
+ const actualNormalized = normalizeUrl(actualUrl, config.baseUrl);
74
+ if (actualNormalized !== normalized) {
75
+ page.url = actualNormalized;
76
+ page.path = new URL(actualNormalized).pathname;
77
+ page.slug = urlToSlug(actualUrl, config.baseUrl);
78
+ if (visited.has(actualNormalized)) {
79
+ verbose(` Redirected to already-visited ${actualUrl}, skipping`);
80
+ continue;
81
+ }
82
+ visited.add(actualNormalized);
83
+ }
84
+ page.title = await browser.getTitle();
85
+ const slugDir = join(screenshotsDir, page.slug);
86
+ ensureDir(slugDir);
87
+ const screenshotPath = join(slugDir, "01-default.png");
88
+ await browser.screenshot(screenshotPath);
89
+ page.screenshot = `screenshots/${page.slug}/01-default.png`;
90
+ page.elements = await browser.snapshot();
91
+ const linkRefs = extractHrefsFromElements(page.elements);
92
+ for (const ref of linkRefs) {
93
+ try {
94
+ const href = await browser.getAttribute(ref, "href");
95
+ if (href) {
96
+ const linkUrl = normalizeUrl(href, actualUrl);
97
+ page.outgoingLinks.push(linkUrl);
98
+ if (shouldVisit(linkUrl, config.baseUrl, config.excludePatterns, visited)) {
99
+ state.queue.push(linkUrl);
100
+ }
101
+ }
102
+ } catch {}
103
+ }
104
+ page.depth = calculateDepth(page.path);
105
+ const relatedPages = Array.from(state.pages.values()).filter((p) => p.status === "visited").map((p) => ({ slug: p.slug, name: p.analysis?.pageName ?? p.title, path: p.path }));
106
+ const markdown = generateSectionMarkdown(page, relatedPages);
107
+ writeFile(join(sectionsDir, `${page.slug}.md`), markdown);
108
+ page.status = "visited";
109
+ page.visitedAt = new Date().toISOString();
110
+ pageCount++;
111
+ success(` ${page.title || page.slug} — ${page.elements.length} elements, ${page.outgoingLinks.length} links`);
112
+ } catch (err) {
113
+ page.status = "error";
114
+ page.error = err.message;
115
+ error(` Error: ${err.message}`);
116
+ }
117
+ state.pages.set(page.url, page);
118
+ }
119
+ state.topology = buildTopology(state.pages);
120
+ state.completedAt = new Date().toISOString();
121
+ await browser.close();
122
+ } catch (err) {
123
+ error(`Exploration failed: ${err.message}`);
124
+ await browser.close();
125
+ throw err;
126
+ }
127
+ return state;
128
+ }
129
+ function calculateDepth(path) {
130
+ const segments = path.replace(/^\/|\/$/g, "").split("/").filter(Boolean);
131
+ return segments.length;
132
+ }
133
+ export { runExploration };