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.
package/README.md CHANGED
@@ -17,7 +17,7 @@ bun add -g zefiro
17
17
  zefiro init
18
18
 
19
19
  # 2. Explore a running application
20
- zefiro scan # static AST scan (optional)
20
+ zefiro explore
21
21
  ```
22
22
 
23
23
  ---
@@ -52,27 +52,18 @@ On re-run (`.qai/zefiro/agents/` already exists), preserves your existing setup
52
52
  **After init:**
53
53
  1. Generate `.qai/zefiro/context.md` using the `init-agent` prompt in your AI tool, or via MCP (`zefiro_scan_codebase`)
54
54
  2. Review the generated context
55
- 3. Run `zefiro scan`
55
+ 3. Run `zefiro explore`
56
56
 
57
57
  ---
58
58
 
59
- ### `zefiro scan`
59
+ ### `zefiro explore`
60
60
 
61
- Scan your codebase AST extracts routes, components, hooks, imports, and dependencies.
61
+ Explore a running web application via browser automation. Navigates pages, captures screenshots, and generates structured documentation.
62
62
 
63
63
  ```bash
64
- zefiro scan
65
- zefiro scan --scan-dir src/app
66
- zefiro scan --output custom-scan.json
67
- zefiro scan --no-cache
64
+ zefiro explore
68
65
  ```
69
66
 
70
- | Option | Description |
71
- |--------|-------------|
72
- | `--output <file>` | Output file (default: `.qai/zefiro/ast-scan.json`) |
73
- | `--scan-dir <dir>` | Directory to scan (overrides config) |
74
- | `--no-cache` | Disable file-level MD5 caching |
75
-
76
67
  ---
77
68
 
78
69
  ### `zefiro auth`
@@ -0,0 +1,167 @@
1
+ // src/browser/url-tracker.ts
2
+ import { createHash } from "node:crypto";
3
+ var SKIP_EXTENSIONS = new Set([
4
+ ".pdf",
5
+ ".zip",
6
+ ".csv",
7
+ ".xlsx",
8
+ ".xls",
9
+ ".doc",
10
+ ".docx",
11
+ ".png",
12
+ ".jpg",
13
+ ".jpeg",
14
+ ".gif",
15
+ ".svg",
16
+ ".ico",
17
+ ".webp",
18
+ ".mp3",
19
+ ".mp4",
20
+ ".wav",
21
+ ".avi",
22
+ ".mov"
23
+ ]);
24
+ var SKIP_PATTERNS = [
25
+ /\/logout/i,
26
+ /\/signout/i,
27
+ /\/api\//i,
28
+ /\/auth\/callback/i,
29
+ /\/oauth/i,
30
+ /javascript:/i,
31
+ /mailto:/i,
32
+ /tel:/i,
33
+ /#$/
34
+ ];
35
+ function normalizeUrl(url, baseUrl) {
36
+ try {
37
+ const resolved = new URL(url, baseUrl);
38
+ resolved.hash = "";
39
+ let normalized = resolved.href;
40
+ if (normalized.endsWith("/") && resolved.pathname !== "/") {
41
+ normalized = normalized.slice(0, -1);
42
+ }
43
+ return normalized;
44
+ } catch {
45
+ return url;
46
+ }
47
+ }
48
+ function shouldVisit(url, baseUrl, excludePatterns, visited) {
49
+ try {
50
+ const parsed = new URL(url);
51
+ const base = new URL(baseUrl);
52
+ if (parsed.origin !== base.origin)
53
+ return false;
54
+ const normalized = normalizeUrl(url, baseUrl);
55
+ if (visited.has(normalized))
56
+ return false;
57
+ const ext = parsed.pathname.match(/\.\w+$/)?.[0]?.toLowerCase();
58
+ if (ext && SKIP_EXTENSIONS.has(ext))
59
+ return false;
60
+ for (const pattern of SKIP_PATTERNS) {
61
+ if (pattern.test(url))
62
+ return false;
63
+ }
64
+ for (const pattern of excludePatterns) {
65
+ try {
66
+ if (new RegExp(pattern).test(url))
67
+ return false;
68
+ } catch {
69
+ if (url.includes(pattern))
70
+ return false;
71
+ }
72
+ }
73
+ return true;
74
+ } catch {
75
+ return false;
76
+ }
77
+ }
78
+ function extractHrefsFromElements(elements) {
79
+ return elements.filter((el) => el.role === "link").map((el) => el.ref);
80
+ }
81
+ function urlToSlug(url, baseUrl) {
82
+ try {
83
+ const parsed = new URL(url, baseUrl);
84
+ const path = parsed.pathname;
85
+ if (path === "/" || path === "")
86
+ return "home";
87
+ return path.replace(/^\/|\/$/g, "").replace(/\//g, "-").replace(/[^a-zA-Z0-9-]/g, "-").replace(/-+/g, "-").toLowerCase();
88
+ } catch {
89
+ return "unknown";
90
+ }
91
+ }
92
+ function buildTopology(pages) {
93
+ const root = { path: "/", slug: "root", stableId: generatePageId("root"), title: "Application", children: [] };
94
+ const nodeMap = new Map;
95
+ nodeMap.set("/", root);
96
+ const sortedPages = Array.from(pages.values()).filter((p) => p.status === "visited").sort((a, b) => a.depth - b.depth);
97
+ for (const page of sortedPages) {
98
+ const segments = page.path.replace(/^\/|\/$/g, "").split("/").filter(Boolean);
99
+ const node = {
100
+ path: page.path,
101
+ slug: page.slug,
102
+ stableId: page.stableId,
103
+ title: page.title || page.slug,
104
+ children: []
105
+ };
106
+ let parentPath = "/";
107
+ if (segments.length > 1) {
108
+ parentPath = "/" + segments.slice(0, -1).join("/");
109
+ }
110
+ const parent = nodeMap.get(parentPath) ?? root;
111
+ parent.children.push(node);
112
+ nodeMap.set(page.path, node);
113
+ }
114
+ return root.children;
115
+ }
116
+ function labelHash(label) {
117
+ return createHash("sha256").update(label.trim().toLowerCase()).digest("hex").slice(0, 8);
118
+ }
119
+ function toKebab(s) {
120
+ return s.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
121
+ }
122
+ function generatePageId(slug) {
123
+ return `page:${slug}`;
124
+ }
125
+ function generateSectionId(pageSlug, sectionName) {
126
+ return `section:${pageSlug}:${toKebab(sectionName)}`;
127
+ }
128
+ function generateElementId(pageSlug, role, label) {
129
+ return `element:${pageSlug}:${role}:${labelHash(label)}`;
130
+ }
131
+ function generateEdgeId(sourceId, targetId) {
132
+ return `edge:${sourceId}->${targetId}`;
133
+ }
134
+ function classifySectionType(sectionType) {
135
+ const t = sectionType.toLowerCase();
136
+ if (t.includes("modal") || t.includes("dialog") || t.includes("popup"))
137
+ return "dialog";
138
+ if (t.includes("form"))
139
+ return "form";
140
+ if (t.includes("table") || t.includes("list") || t.includes("grid"))
141
+ return "table";
142
+ if (t.includes("tab"))
143
+ return "tab";
144
+ if (t.includes("filter") || t.includes("search"))
145
+ return "filter";
146
+ if (t.includes("action") || t.includes("toolbar") || t.includes("button"))
147
+ return "action";
148
+ if (t.includes("panel") || t.includes("card") || t.includes("section"))
149
+ return "page";
150
+ return "page";
151
+ }
152
+ function topologyToAscii(nodes, prefix = "") {
153
+ const lines = [];
154
+ for (let i = 0;i < nodes.length; i++) {
155
+ const isLast = i === nodes.length - 1;
156
+ const connector = isLast ? "└── " : "├── ";
157
+ const childPrefix = isLast ? " " : "│ ";
158
+ lines.push(`${prefix}${connector}${nodes[i].path} — ${nodes[i].title}`);
159
+ if (nodes[i].children.length > 0) {
160
+ lines.push(topologyToAscii(nodes[i].children, prefix + childPrefix));
161
+ }
162
+ }
163
+ return lines.join(`
164
+ `);
165
+ }
166
+
167
+ export { normalizeUrl, shouldVisit, extractHrefsFromElements, urlToSlug, buildTopology, generatePageId, generateSectionId, generateElementId, generateEdgeId, classifySectionType, topologyToAscii };