zefiro 0.8.0 → 0.9.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 +8 -76
- package/dist/App-bh752tgg.js +24972 -0
- package/dist/cli-6qr9gvkp.js +508 -0
- package/dist/cli-b26q1e27.js +264 -0
- package/dist/cli-cymqw41r.js +119 -0
- package/dist/cli-g7n2me6z.js +133 -0
- package/dist/cli.js +51 -45
- package/dist/explorer-g6bmbak1.js +10 -0
- package/dist/index.js +1 -1
- package/dist/mcp.js +3836 -26
- package/dist/report-sdtah1f4.js +11 -0
- package/package.json +4 -2
- package/skills/zefiro-copilot/SKILL.md +103 -0
|
@@ -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,119 @@
|
|
|
1
|
+
import {
|
|
2
|
+
exports_external
|
|
3
|
+
} from "./cli-z2krvkcq.js";
|
|
4
|
+
|
|
5
|
+
// src/config/schema.ts
|
|
6
|
+
function nested(shape) {
|
|
7
|
+
const inner = exports_external.object(shape);
|
|
8
|
+
return exports_external.preprocess((val) => val ?? {}, inner);
|
|
9
|
+
}
|
|
10
|
+
var PathsSchema = exports_external.object({
|
|
11
|
+
workingDir: exports_external.string().default(".qai/zefiro"),
|
|
12
|
+
outputDir: exports_external.string().default("app-report")
|
|
13
|
+
});
|
|
14
|
+
var BrowserSchema = exports_external.object({
|
|
15
|
+
sessionName: exports_external.string().default("zefiro"),
|
|
16
|
+
headed: exports_external.boolean().default(false),
|
|
17
|
+
viewport: exports_external.object({
|
|
18
|
+
width: exports_external.number().default(1440),
|
|
19
|
+
height: exports_external.number().default(900)
|
|
20
|
+
}).default({ width: 1440, height: 900 }),
|
|
21
|
+
waitStrategy: exports_external.enum(["networkidle", "load", "domcontentloaded"]).default("networkidle"),
|
|
22
|
+
waitDelay: exports_external.number().default(1500),
|
|
23
|
+
maxPages: exports_external.number().default(100),
|
|
24
|
+
pageTimeout: exports_external.number().default(30000),
|
|
25
|
+
excludePatterns: exports_external.array(exports_external.string()).default([])
|
|
26
|
+
});
|
|
27
|
+
var AuthSchema = exports_external.object({
|
|
28
|
+
method: exports_external.enum(["state-file", "manual"]).default("manual"),
|
|
29
|
+
stateFile: exports_external.string().nullable().default(null)
|
|
30
|
+
});
|
|
31
|
+
var QaiConfigSchema = exports_external.object({
|
|
32
|
+
baseUrl: exports_external.string().nullable().default(null),
|
|
33
|
+
paths: nested(PathsSchema.shape),
|
|
34
|
+
browser: nested(BrowserSchema.shape),
|
|
35
|
+
auth: nested(AuthSchema.shape),
|
|
36
|
+
contextFile: exports_external.string().default(".qai/zefiro/context.md")
|
|
37
|
+
});
|
|
38
|
+
function defineConfig(config) {
|
|
39
|
+
return config;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/config/loader.ts
|
|
43
|
+
import { existsSync } from "node:fs";
|
|
44
|
+
import { dirname, join, resolve } from "node:path";
|
|
45
|
+
import { pathToFileURL } from "node:url";
|
|
46
|
+
var CONFIG_DIR = ".qai/zefiro";
|
|
47
|
+
var LEGACY_CONFIG_DIR = ".e2e-ai";
|
|
48
|
+
var CONFIG_FILENAMES = ["config.ts", "config.js", "config.mjs"];
|
|
49
|
+
var cachedConfig = null;
|
|
50
|
+
var cachedProjectRoot = null;
|
|
51
|
+
function findConfigDir(startDir) {
|
|
52
|
+
let dir = resolve(startDir);
|
|
53
|
+
const root = dirname(dir) === dir ? dir : undefined;
|
|
54
|
+
while (true) {
|
|
55
|
+
for (const configDir of [CONFIG_DIR, LEGACY_CONFIG_DIR]) {
|
|
56
|
+
const candidate = join(dir, configDir);
|
|
57
|
+
if (existsSync(join(candidate, "agents")) || existsSync(join(candidate, "context.md"))) {
|
|
58
|
+
return dir;
|
|
59
|
+
}
|
|
60
|
+
if (configDir === CONFIG_DIR && existsSync(candidate)) {
|
|
61
|
+
return dir;
|
|
62
|
+
}
|
|
63
|
+
for (const name of CONFIG_FILENAMES) {
|
|
64
|
+
if (existsSync(join(candidate, name))) {
|
|
65
|
+
return dir;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const parent = dirname(dir);
|
|
70
|
+
if (parent === dir || dir === root)
|
|
71
|
+
return null;
|
|
72
|
+
dir = parent;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function getProjectRoot() {
|
|
76
|
+
if (cachedProjectRoot)
|
|
77
|
+
return cachedProjectRoot;
|
|
78
|
+
const found = findConfigDir(process.cwd());
|
|
79
|
+
cachedProjectRoot = found ?? process.cwd();
|
|
80
|
+
return cachedProjectRoot;
|
|
81
|
+
}
|
|
82
|
+
function getPackageRoot() {
|
|
83
|
+
let dir = import.meta.dirname;
|
|
84
|
+
while (!existsSync(join(dir, "package.json"))) {
|
|
85
|
+
const parent = dirname(dir);
|
|
86
|
+
if (parent === dir)
|
|
87
|
+
return dir;
|
|
88
|
+
dir = parent;
|
|
89
|
+
}
|
|
90
|
+
return dir;
|
|
91
|
+
}
|
|
92
|
+
async function loadConfig() {
|
|
93
|
+
if (cachedConfig)
|
|
94
|
+
return cachedConfig;
|
|
95
|
+
const projectRoot = getProjectRoot();
|
|
96
|
+
let userConfig = {};
|
|
97
|
+
for (const configDir of [CONFIG_DIR, LEGACY_CONFIG_DIR]) {
|
|
98
|
+
const dir = join(projectRoot, configDir);
|
|
99
|
+
let found = false;
|
|
100
|
+
for (const name of CONFIG_FILENAMES) {
|
|
101
|
+
const configPath = join(dir, name);
|
|
102
|
+
if (existsSync(configPath)) {
|
|
103
|
+
try {
|
|
104
|
+
const fileUrl = pathToFileURL(configPath).href;
|
|
105
|
+
const mod = await import(fileUrl);
|
|
106
|
+
userConfig = mod.default ?? mod;
|
|
107
|
+
found = true;
|
|
108
|
+
break;
|
|
109
|
+
} catch {}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (found)
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
cachedConfig = QaiConfigSchema.parse(userConfig);
|
|
116
|
+
return cachedConfig;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export { defineConfig, CONFIG_DIR, getProjectRoot, getPackageRoot, loadConfig };
|
|
@@ -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 };
|
package/dist/cli.js
CHANGED
|
@@ -4,24 +4,26 @@ import {
|
|
|
4
4
|
getPackageRoot,
|
|
5
5
|
getProjectRoot,
|
|
6
6
|
loadConfig
|
|
7
|
-
} from "./cli-
|
|
7
|
+
} from "./cli-cymqw41r.js";
|
|
8
8
|
import"./cli-z2krvkcq.js";
|
|
9
9
|
import {
|
|
10
10
|
dist_default2 as dist_default,
|
|
11
11
|
dist_default3 as dist_default2,
|
|
12
12
|
dist_default9 as dist_default3
|
|
13
13
|
} from "./cli-eyd8t0cw.js";
|
|
14
|
+
import {
|
|
15
|
+
runExploration
|
|
16
|
+
} from "./cli-g7n2me6z.js";
|
|
14
17
|
import {
|
|
15
18
|
banner,
|
|
16
19
|
error,
|
|
17
20
|
header,
|
|
18
21
|
info,
|
|
19
22
|
require_picocolors,
|
|
20
|
-
runExploration,
|
|
21
23
|
setVerbose,
|
|
22
24
|
success,
|
|
23
25
|
warn
|
|
24
|
-
} from "./cli-
|
|
26
|
+
} from "./cli-b26q1e27.js";
|
|
25
27
|
import {
|
|
26
28
|
ensureDir,
|
|
27
29
|
writeFile
|
|
@@ -29,7 +31,7 @@ import {
|
|
|
29
31
|
import {
|
|
30
32
|
generateIndexHtml,
|
|
31
33
|
generateReadme
|
|
32
|
-
} from "./cli-
|
|
34
|
+
} from "./cli-6qr9gvkp.js";
|
|
33
35
|
import {
|
|
34
36
|
__commonJS,
|
|
35
37
|
__require,
|
|
@@ -3117,13 +3119,9 @@ function registerAuth(program2) {
|
|
|
3117
3119
|
// src/commands/mcp.ts
|
|
3118
3120
|
var import_picocolors3 = __toESM(require_picocolors(), 1);
|
|
3119
3121
|
var TOOLS = [
|
|
3120
|
-
"
|
|
3121
|
-
"
|
|
3122
|
-
"
|
|
3123
|
-
"zefiro_extract_state_variables — V4: extract state variables from AST",
|
|
3124
|
-
"zefiro_build_v4_graph — V4: build formal model from AST + state",
|
|
3125
|
-
"zefiro_build_qa_map — validate + write QA map (V2/V3/V4)",
|
|
3126
|
-
"zefiro_read_qa_map — read existing QA map file"
|
|
3122
|
+
"zefiro_explore — explore a web application via BFS",
|
|
3123
|
+
"zefiro_read_docs — read generated documentation",
|
|
3124
|
+
"zefiro_scan_codebase — scan project test infrastructure"
|
|
3127
3125
|
];
|
|
3128
3126
|
var DESKTOP_CONFIG = JSON.stringify({
|
|
3129
3127
|
mcpServers: {
|
|
@@ -3216,11 +3214,6 @@ async function migrateFromLegacy(projectRoot, nonInteractive) {
|
|
|
3216
3214
|
cpSync(astSrc, join4(newPath, "ast-scan.json"));
|
|
3217
3215
|
migrated++;
|
|
3218
3216
|
}
|
|
3219
|
-
const qamapSrc = join4(legacyPath, "qa-map.json");
|
|
3220
|
-
if (existsSync2(qamapSrc)) {
|
|
3221
|
-
cpSync(qamapSrc, join4(newPath, "qa-map.json"));
|
|
3222
|
-
migrated++;
|
|
3223
|
-
}
|
|
3224
3217
|
success(`Migrated ${migrated} item(s) from ${LEGACY_DIR}/ to ${CONFIG_DIR}/`);
|
|
3225
3218
|
if (!nonInteractive) {
|
|
3226
3219
|
const remove = await dist_default({
|
|
@@ -3242,7 +3235,8 @@ async function migrateFromLegacy(projectRoot, nonInteractive) {
|
|
|
3242
3235
|
}
|
|
3243
3236
|
|
|
3244
3237
|
// src/commands/init.ts
|
|
3245
|
-
var
|
|
3238
|
+
var SKILLS_TARGET_BASE = ".claude/skills";
|
|
3239
|
+
var SKILL_DIRS = ["zefiro", "zefiro-copilot"];
|
|
3246
3240
|
function registerInit(program2) {
|
|
3247
3241
|
program2.command("init").description("Initialize zefiro configuration for your project").option("--non-interactive", "Skip interactive prompts, use defaults").action(async (cmdOpts) => {
|
|
3248
3242
|
try {
|
|
@@ -3340,40 +3334,52 @@ ${qaiBlock}
|
|
|
3340
3334
|
success(`Environment variables written to .env`);
|
|
3341
3335
|
}
|
|
3342
3336
|
async function copySkillsToLocal(projectRoot, packageRoot, nonInteractive) {
|
|
3343
|
-
const
|
|
3344
|
-
const
|
|
3345
|
-
if (!existsSync3(
|
|
3337
|
+
const sourceBase = join5(packageRoot, "skills");
|
|
3338
|
+
const targetBase = join5(projectRoot, SKILLS_TARGET_BASE);
|
|
3339
|
+
if (!existsSync3(sourceBase)) {
|
|
3346
3340
|
warn("Could not read package skills directory");
|
|
3347
3341
|
return 0;
|
|
3348
3342
|
}
|
|
3349
|
-
|
|
3350
|
-
|
|
3343
|
+
let totalSourceFiles = [];
|
|
3344
|
+
let totalExistingFiles = 0;
|
|
3345
|
+
for (const skillDir of SKILL_DIRS) {
|
|
3346
|
+
const sourceDir = join5(sourceBase, skillDir);
|
|
3347
|
+
if (!existsSync3(sourceDir))
|
|
3348
|
+
continue;
|
|
3349
|
+
const files = collectMarkdownFiles(sourceDir, sourceDir);
|
|
3350
|
+
for (const f of files)
|
|
3351
|
+
totalSourceFiles.push({ dir: skillDir, relPath: f });
|
|
3352
|
+
}
|
|
3353
|
+
if (totalSourceFiles.length === 0)
|
|
3351
3354
|
return 0;
|
|
3352
|
-
const
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3355
|
+
for (const skillDir of SKILL_DIRS) {
|
|
3356
|
+
const targetDir = join5(targetBase, skillDir);
|
|
3357
|
+
if (existsSync3(targetDir)) {
|
|
3358
|
+
totalExistingFiles += collectMarkdownFiles(targetDir, targetDir).length;
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
if (totalExistingFiles > 0) {
|
|
3362
|
+
if (nonInteractive) {
|
|
3363
|
+
info(`Skill files already exist in ${SKILLS_TARGET_BASE}/, skipping`);
|
|
3364
|
+
return 0;
|
|
3365
|
+
}
|
|
3366
|
+
const { confirm } = await import("./index-0yaea7f8.js");
|
|
3367
|
+
const overwrite = await confirm({
|
|
3368
|
+
message: `Update skills to latest version? (${totalSourceFiles.length} files, currently ${totalExistingFiles} in ${SKILLS_TARGET_BASE}/)`,
|
|
3369
|
+
default: true
|
|
3370
|
+
});
|
|
3371
|
+
if (!overwrite) {
|
|
3372
|
+
info("Skipping skill copy");
|
|
3373
|
+
return 0;
|
|
3369
3374
|
}
|
|
3370
3375
|
}
|
|
3371
|
-
for (const relPath of
|
|
3372
|
-
const content = readFileSync3(join5(
|
|
3373
|
-
writeFile(join5(
|
|
3376
|
+
for (const { dir, relPath } of totalSourceFiles) {
|
|
3377
|
+
const content = readFileSync3(join5(sourceBase, dir, relPath), "utf-8");
|
|
3378
|
+
writeFile(join5(targetBase, dir, relPath), content);
|
|
3374
3379
|
}
|
|
3375
|
-
|
|
3376
|
-
|
|
3380
|
+
const skillNames = SKILL_DIRS.join(", ");
|
|
3381
|
+
success(`Skills copied to ${SKILLS_TARGET_BASE}/ [${skillNames}] (${totalSourceFiles.length} files)`);
|
|
3382
|
+
return totalSourceFiles.length;
|
|
3377
3383
|
}
|
|
3378
3384
|
function collectMarkdownFiles(dir, baseDir) {
|
|
3379
3385
|
const results = [];
|
|
@@ -3411,7 +3417,7 @@ var hasFlags = userArgs.some((a) => a.startsWith("-"));
|
|
|
3411
3417
|
var cols = process.stdout.columns || 80;
|
|
3412
3418
|
var rows = process.stdout.rows || 24;
|
|
3413
3419
|
if (!hasCommand && !hasFlags && process.stdout.isTTY && cols >= 60 && rows >= 20) {
|
|
3414
|
-
const { launchTui } = await import("./App-
|
|
3420
|
+
const { launchTui } = await import("./App-bh752tgg.js");
|
|
3415
3421
|
await launchTui(program2);
|
|
3416
3422
|
} else {
|
|
3417
3423
|
program2.parse();
|