qapture2 0.9.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 +118 -248
- package/dist/bin/init.cjs +323 -60
- package/dist/{chunk-HEHVZCSV.js → chunk-5W5FS7JY.js} +255 -58
- package/dist/{chunk-J77SGE5E.cjs → chunk-R6WVHVXO.cjs} +255 -58
- package/dist/index.cjs +5 -5
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +1 -1
- package/dist/next.cjs +4 -4
- package/dist/next.js +1 -1
- package/dist/standalone.cjs +2 -2
- package/dist/standalone.js +1 -1
- package/package.json +4 -3
package/dist/bin/init.cjs
CHANGED
|
@@ -24,8 +24,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
));
|
|
25
25
|
|
|
26
26
|
// src/bin/init.ts
|
|
27
|
-
var
|
|
28
|
-
var
|
|
27
|
+
var path7 = __toESM(require("path"), 1);
|
|
28
|
+
var fs7 = __toESM(require("fs"), 1);
|
|
29
29
|
var process2 = __toESM(require("process"), 1);
|
|
30
30
|
|
|
31
31
|
// src/artifacts/SKILL.md
|
|
@@ -35,13 +35,20 @@ var SKILL_default = "---\nname: qapture\ndescription: >\n Activated when the us
|
|
|
35
35
|
var AGENTS_SECTION_default = "<!-- qa-studio-section -->\n\n## Qapture\n\nThis project uses **Qapture** \u2014 an in-browser QA capture widget that ships\n**zero AI** (no model, no API keys, no network calls). **You** are the AI.\n\n### When you receive a `qa-notes-*.zip`\n\n1. **Unzip** the file.\n2. **Read `notes.md` top-to-bottom**, starting with everything above the\n `---NOTES---` separator:\n - **Project context** \u2014 name, stack, run commands, conventions.\n - **Login Context** \u2014 dev/test/seed credentials for the relevant roles.\n _(DEV/TEST/SEED only \u2014 never commit, log, or forward these values.)_\n - **Coverage Report** \u2014 red/amber/green zone checklist.\n - **Invariants** \u2014 rules you must never violate (e.g. \"prices \u2265 0\",\n \"checkout requires auth\").\n3. **Flag uncovered RED zones** before acting. RED = money / auth / irreversible\n state. If any red zone has no annotation in this ZIP, report it and ask the\n developer whether to proceed.\n4. **Triage the whole batch before touching code.** Read every point first \u2014\n points on different pages can share one root cause (check their runtime\n context for a repeated failing URL or error message) and deserve one fix,\n not N. Write the plan down before editing anything.\n5. **Orchestrate, don't grind through it yourself.** For anything past a\n single trivial point: you're the brain, not the muscle. Spawn one Sonnet\n subagent per point/cluster (model pinned explicitly on every call, never\n inherited; effort is your judgment call per task), parallelized across\n points that touch disjoint files \u2014 never two agents on the same file. You\n supervise by checking what each subagent's report actually claims and by\n running the project's own verify/test command yourself afterward, not by\n rereading every diff or trusting a subagent's \"done.\"\n6. **Each point/cluster, before fixing:**\n - **Page** + **Selector** + **Note** \u2192 locate the element in the source\n (priority: `#id` \u2192 `[data-testid]` \u2192 `aria-label` \u2192 `name` \u2192 visual match\n via the screenshot named in the point's **Screenshot** line \u2014\n `screenshots/point-N.webp`, or `.png` on browsers without WebP).\n - **Severity** (`bug`/`question`/`polish`) and **Status**\n (`open`/`fixed`/`verified` \u2014 `fixed` means someone says it is done but\n nobody has re-tested it yet)\n tell you how to treat the point \u2014 a `question` may not need a code\n change; a `verified` point was already re-checked once.\n - **Runtime context** (when present, in a collapsed `<details>` block) \u2014\n recent console errors/warnings and failed network calls captured right\n before the tester clicked capture, plus an environment snapshot, plus\n **forensics** (contrast/accessibility flags on the captured element) when\n present \u2014 treat these as objective acceptance criteria, not just the\n tester's prose. Read it before assuming a UI-only cause. Query strings in\n any URL there are already redacted; bodies, headers, cookies, and storage\n were never captured at all.\n - **Reproduce it live first** \u2014 run the app, log in as the relevant role,\n navigate to the page, actually trigger the failure \u2014 before writing a\n fix. A static screenshot can hide a stale report or an interaction-only\n bug.\n - Make the change following the project conventions and invariants, then\n re-verify the same repro.\n7. **Report** a summary table of changes, risk levels, and coverage status,\n plus a `### Suggestions` section for anything noticed beyond the reported\n points. Whether you may *implement* an extra idea (not just suggest it)\n follows the same red/amber/green gating as everything else: green \u2014 do it\n inline; amber \u2014 do it and disclose it; red \u2014 propose only, never touch it.\n\n### A single point, no ZIP\n\nA tester can also send you **one point directly**, pasted via Qapture's\n\"Copy as agent prompt\" button, with no ZIP and no preamble. Treat it exactly\nlike one point from step 6 above \u2014 there's nothing to cluster or triage with\nonly one point, and there's no Coverage Report to check or RED-zone flag to\nraise, since there's no journey context at all. Just fix it yourself if it's\ntrivial, or spawn a single subagent if it isn't.\n\n### Full protocol\n\n`.claude/skills/qapture/SKILL.md` (always kept current by `qapture init`).\n\n### Rules\n\n- Never read `.env`, `.env.local`, `.env.production`, or any `secrets/` path.\n- Never edit `qa.config.ts`, `qa.preamble.md`, or any qapture plugin files.\n- Never push/publish/deploy without explicit human approval.\n- Dev/test/seed credentials only \u2014 never use or request production credentials.\n\n_Qapture \u2014 https://github.com/mohammed-farhood/qapture_\n\n<!-- /qa-studio-section -->\n";
|
|
36
36
|
|
|
37
37
|
// src/bin/utils/args.ts
|
|
38
|
+
var DEFAULT_SHOT_PORT = 7017;
|
|
38
39
|
function parseArgs(argv2) {
|
|
39
40
|
const [cmd, ...rest] = argv2;
|
|
41
|
+
const base = {
|
|
42
|
+
dir: process.cwd(),
|
|
43
|
+
force: false,
|
|
44
|
+
port: DEFAULT_SHOT_PORT,
|
|
45
|
+
allow: []
|
|
46
|
+
};
|
|
40
47
|
if (cmd === "version" || cmd === "--version" || cmd === "-v" || cmd === "-V") {
|
|
41
|
-
return { command: "version"
|
|
48
|
+
return { ...base, command: "version" };
|
|
42
49
|
}
|
|
43
50
|
if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") {
|
|
44
|
-
return { command: "help"
|
|
51
|
+
return { ...base, command: "help" };
|
|
45
52
|
}
|
|
46
53
|
if (cmd === "init") {
|
|
47
54
|
let dir = process.cwd();
|
|
@@ -55,54 +62,229 @@ function parseArgs(argv2) {
|
|
|
55
62
|
dir = arg;
|
|
56
63
|
}
|
|
57
64
|
}
|
|
58
|
-
return { command: "init", dir, force };
|
|
65
|
+
return { ...base, command: "init", dir, force };
|
|
66
|
+
}
|
|
67
|
+
if (cmd === "shots") {
|
|
68
|
+
let port = DEFAULT_SHOT_PORT;
|
|
69
|
+
const allow = [];
|
|
70
|
+
for (let i = 0; i < rest.length; i++) {
|
|
71
|
+
const arg = rest[i];
|
|
72
|
+
if (arg === "--port" || arg === "-p") {
|
|
73
|
+
const n = Number(rest[++i]);
|
|
74
|
+
if (Number.isInteger(n) && n > 0 && n < 65536) port = n;
|
|
75
|
+
} else if (arg.startsWith("--port=")) {
|
|
76
|
+
const n = Number(arg.slice("--port=".length));
|
|
77
|
+
if (Number.isInteger(n) && n > 0 && n < 65536) port = n;
|
|
78
|
+
} else if (arg === "--allow") {
|
|
79
|
+
const origin = rest[++i];
|
|
80
|
+
if (origin) allow.push(origin.replace(/\/$/, ""));
|
|
81
|
+
} else if (arg.startsWith("--allow=")) {
|
|
82
|
+
const origin = arg.slice("--allow=".length);
|
|
83
|
+
if (origin) allow.push(origin.replace(/\/$/, ""));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return { ...base, command: "shots", port, allow };
|
|
59
87
|
}
|
|
60
|
-
return { command: "help"
|
|
88
|
+
return { ...base, command: "help" };
|
|
61
89
|
}
|
|
62
90
|
|
|
63
|
-
// src/bin/
|
|
91
|
+
// src/bin/shotServer.ts
|
|
92
|
+
var http = __toESM(require("http"), 1);
|
|
93
|
+
var os = __toESM(require("os"), 1);
|
|
64
94
|
var fs = __toESM(require("fs"), 1);
|
|
65
95
|
var path = __toESM(require("path"), 1);
|
|
96
|
+
var import_node_child_process = require("child_process");
|
|
97
|
+
var MAX_SIDE = 2e4;
|
|
98
|
+
var CAPTURE_TIMEOUT_MS = 8e3;
|
|
99
|
+
function isLoopbackOrigin(origin) {
|
|
100
|
+
try {
|
|
101
|
+
const u = new URL(origin);
|
|
102
|
+
return u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "[::1]" || u.hostname === "::1";
|
|
103
|
+
} catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function originAllowed(origin, allow) {
|
|
108
|
+
if (!origin) return false;
|
|
109
|
+
if (isLoopbackOrigin(origin)) return true;
|
|
110
|
+
return allow.includes(origin);
|
|
111
|
+
}
|
|
112
|
+
function corsHeaders(origin) {
|
|
113
|
+
return {
|
|
114
|
+
"Access-Control-Allow-Origin": origin,
|
|
115
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
116
|
+
"Access-Control-Allow-Headers": "content-type",
|
|
117
|
+
// Chrome's Private Network Access check: an https page reaching a loopback
|
|
118
|
+
// address is asked for explicitly, and refused unless we say yes here.
|
|
119
|
+
"Access-Control-Allow-Private-Network": "true",
|
|
120
|
+
"Vary": "Origin"
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function sendJson(res, status, body, extra = {}) {
|
|
124
|
+
const text = JSON.stringify(body);
|
|
125
|
+
res.writeHead(status, {
|
|
126
|
+
"Content-Type": "application/json",
|
|
127
|
+
"Content-Length": Buffer.byteLength(text),
|
|
128
|
+
"Cache-Control": "no-store",
|
|
129
|
+
...extra
|
|
130
|
+
});
|
|
131
|
+
res.end(text);
|
|
132
|
+
}
|
|
133
|
+
function readBody(req, limit = 4096) {
|
|
134
|
+
return new Promise((resolve2, reject) => {
|
|
135
|
+
let size = 0;
|
|
136
|
+
const chunks = [];
|
|
137
|
+
req.on("data", (c) => {
|
|
138
|
+
size += c.length;
|
|
139
|
+
if (size > limit) {
|
|
140
|
+
reject(new Error("body too large"));
|
|
141
|
+
req.destroy();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
chunks.push(c);
|
|
145
|
+
});
|
|
146
|
+
req.on("end", () => resolve2(Buffer.concat(chunks).toString("utf8")));
|
|
147
|
+
req.on("error", reject);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
function captureRect(rect) {
|
|
151
|
+
const file = path.join(
|
|
152
|
+
os.tmpdir(),
|
|
153
|
+
`qapture-shot-${process.pid}-${Date.now()}.png`
|
|
154
|
+
);
|
|
155
|
+
const region = `${Math.round(rect.x)},${Math.round(rect.y)},${Math.round(rect.w)},${Math.round(rect.h)}`;
|
|
156
|
+
return new Promise((resolve2, reject) => {
|
|
157
|
+
(0, import_node_child_process.execFile)(
|
|
158
|
+
"/usr/sbin/screencapture",
|
|
159
|
+
["-x", "-o", "-r", "-t", "png", "-R", region, file],
|
|
160
|
+
{ timeout: CAPTURE_TIMEOUT_MS },
|
|
161
|
+
(err) => {
|
|
162
|
+
if (err) {
|
|
163
|
+
fs.promises.unlink(file).catch(() => {
|
|
164
|
+
});
|
|
165
|
+
reject(err);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
fs.promises.readFile(file).then((buf) => {
|
|
169
|
+
fs.promises.unlink(file).catch(() => {
|
|
170
|
+
});
|
|
171
|
+
if (!buf.length) {
|
|
172
|
+
reject(new Error("empty capture"));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
resolve2(buf);
|
|
176
|
+
}).catch((e) => {
|
|
177
|
+
fs.promises.unlink(file).catch(() => {
|
|
178
|
+
});
|
|
179
|
+
reject(e);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
function parseRect(raw) {
|
|
186
|
+
let parsed;
|
|
187
|
+
try {
|
|
188
|
+
parsed = JSON.parse(raw);
|
|
189
|
+
} catch {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
193
|
+
const o = parsed;
|
|
194
|
+
const nums = ["x", "y", "w", "h"].map((k) => Number(o[k]));
|
|
195
|
+
if (nums.some((n) => !Number.isFinite(n))) return null;
|
|
196
|
+
const [x, y, w, h] = nums;
|
|
197
|
+
if (w < 1 || h < 1 || w > MAX_SIDE || h > MAX_SIDE) return null;
|
|
198
|
+
return { x, y, w, h };
|
|
199
|
+
}
|
|
200
|
+
function createShotServer(opts) {
|
|
201
|
+
return http.createServer((req, res) => {
|
|
202
|
+
const origin = req.headers.origin;
|
|
203
|
+
const url = (req.url ?? "").split("?")[0];
|
|
204
|
+
if (!originAllowed(origin, opts.allow)) {
|
|
205
|
+
sendJson(res, 403, { error: "origin not allowed" });
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const cors = corsHeaders(origin);
|
|
209
|
+
if (req.method === "OPTIONS") {
|
|
210
|
+
res.writeHead(204, cors);
|
|
211
|
+
res.end();
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (req.method === "GET" && url === "/qapture/health") {
|
|
215
|
+
sendJson(res, 200, { ok: true, engine: "screencapture", platform: process.platform }, cors);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (req.method === "POST" && url === "/qapture/shot") {
|
|
219
|
+
readBody(req).then((body) => {
|
|
220
|
+
const rect = parseRect(body);
|
|
221
|
+
if (!rect) {
|
|
222
|
+
sendJson(res, 400, { error: "bad rect" }, cors);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
return captureRect(rect).then((png) => {
|
|
226
|
+
if (opts.verbose) {
|
|
227
|
+
process.stdout.write(
|
|
228
|
+
` shot ${Math.round(rect.w)}\xD7${Math.round(rect.h)} @ ${Math.round(rect.x)},${Math.round(rect.y)} \u2192 ${(png.length / 1024).toFixed(0)} KB
|
|
229
|
+
`
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
sendJson(res, 200, {
|
|
233
|
+
png: `data:image/png;base64,${png.toString("base64")}`
|
|
234
|
+
}, cors);
|
|
235
|
+
});
|
|
236
|
+
}).catch((err) => {
|
|
237
|
+
sendJson(res, 500, { error: err.message || "capture failed" }, cors);
|
|
238
|
+
});
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
sendJson(res, 404, { error: "not found" }, cors);
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// src/bin/utils/writeIdempotent.ts
|
|
246
|
+
var fs2 = __toESM(require("fs"), 1);
|
|
247
|
+
var path2 = __toESM(require("path"), 1);
|
|
66
248
|
function writeIfAbsent(filePath, content, force) {
|
|
67
|
-
if (!force &&
|
|
249
|
+
if (!force && fs2.existsSync(filePath)) {
|
|
68
250
|
return "skipped";
|
|
69
251
|
}
|
|
70
252
|
ensureParentDir(filePath);
|
|
71
|
-
|
|
253
|
+
fs2.writeFileSync(filePath, content, "utf8");
|
|
72
254
|
return "written";
|
|
73
255
|
}
|
|
74
256
|
function writeAlways(filePath, content) {
|
|
75
257
|
ensureParentDir(filePath);
|
|
76
|
-
|
|
258
|
+
fs2.writeFileSync(filePath, content, "utf8");
|
|
77
259
|
}
|
|
78
260
|
function ensureParentDir(filePath) {
|
|
79
|
-
const dir =
|
|
80
|
-
if (!
|
|
81
|
-
|
|
261
|
+
const dir = path2.dirname(filePath);
|
|
262
|
+
if (!fs2.existsSync(dir)) {
|
|
263
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
82
264
|
}
|
|
83
265
|
}
|
|
84
266
|
|
|
85
267
|
// src/bin/utils/mergeAgentsMd.ts
|
|
86
|
-
var
|
|
268
|
+
var fs3 = __toESM(require("fs"), 1);
|
|
87
269
|
var SENTINEL_OPEN = "<!-- qa-studio-section -->";
|
|
88
270
|
var SENTINEL_CLOSE = "<!-- /qa-studio-section -->";
|
|
89
271
|
function mergeAgentsMd(agentsMdPath, sectionContent) {
|
|
90
|
-
if (!
|
|
272
|
+
if (!fs3.existsSync(agentsMdPath)) {
|
|
91
273
|
writeAlways(agentsMdPath, sectionContent.trimEnd() + "\n");
|
|
92
274
|
return "created";
|
|
93
275
|
}
|
|
94
|
-
const existing =
|
|
276
|
+
const existing = fs3.readFileSync(agentsMdPath, "utf8");
|
|
95
277
|
const openIdx = existing.indexOf(SENTINEL_OPEN);
|
|
96
278
|
const closeIdx = existing.indexOf(SENTINEL_CLOSE);
|
|
97
279
|
if (openIdx !== -1 && closeIdx !== -1 && closeIdx > openIdx) {
|
|
98
280
|
const before = existing.slice(0, openIdx);
|
|
99
281
|
const after = existing.slice(closeIdx + SENTINEL_CLOSE.length);
|
|
100
282
|
const updated = before.replace(/\n*$/, "\n") + sectionContent.trimEnd() + "\n" + after.replace(/^\n*/, "\n");
|
|
101
|
-
|
|
283
|
+
fs3.writeFileSync(agentsMdPath, updated, "utf8");
|
|
102
284
|
return "replaced";
|
|
103
285
|
}
|
|
104
286
|
const separator = existing.trimEnd().length === 0 ? "" : "\n\n";
|
|
105
|
-
|
|
287
|
+
fs3.writeFileSync(
|
|
106
288
|
agentsMdPath,
|
|
107
289
|
existing.trimEnd() + separator + sectionContent.trimEnd() + "\n",
|
|
108
290
|
"utf8"
|
|
@@ -111,12 +293,12 @@ function mergeAgentsMd(agentsMdPath, sectionContent) {
|
|
|
111
293
|
}
|
|
112
294
|
|
|
113
295
|
// src/bin/detectors/detectRoutes.ts
|
|
114
|
-
var
|
|
115
|
-
var
|
|
296
|
+
var path5 = __toESM(require("path"), 1);
|
|
297
|
+
var fs5 = __toESM(require("fs"), 1);
|
|
116
298
|
|
|
117
299
|
// src/bin/utils/walk.ts
|
|
118
|
-
var
|
|
119
|
-
var
|
|
300
|
+
var fs4 = __toESM(require("fs"), 1);
|
|
301
|
+
var path3 = __toESM(require("path"), 1);
|
|
120
302
|
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
121
303
|
"node_modules",
|
|
122
304
|
".git",
|
|
@@ -136,7 +318,7 @@ function walk(dir) {
|
|
|
136
318
|
const results = [];
|
|
137
319
|
let entries;
|
|
138
320
|
try {
|
|
139
|
-
entries =
|
|
321
|
+
entries = fs4.readdirSync(dir, { withFileTypes: true });
|
|
140
322
|
} catch {
|
|
141
323
|
return results;
|
|
142
324
|
}
|
|
@@ -144,30 +326,30 @@ function walk(dir) {
|
|
|
144
326
|
if (entry.isSymbolicLink()) continue;
|
|
145
327
|
if (entry.isDirectory()) {
|
|
146
328
|
if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
147
|
-
results.push(...walk(
|
|
329
|
+
results.push(...walk(path3.join(dir, entry.name)));
|
|
148
330
|
} else if (entry.isFile()) {
|
|
149
|
-
results.push(
|
|
331
|
+
results.push(path3.join(dir, entry.name));
|
|
150
332
|
}
|
|
151
333
|
}
|
|
152
334
|
return results;
|
|
153
335
|
}
|
|
154
336
|
function readFileSafe(filePath) {
|
|
155
337
|
try {
|
|
156
|
-
return
|
|
338
|
+
return fs4.readFileSync(filePath, "utf8");
|
|
157
339
|
} catch {
|
|
158
340
|
return "";
|
|
159
341
|
}
|
|
160
342
|
}
|
|
161
343
|
function dirExists(dirPath) {
|
|
162
344
|
try {
|
|
163
|
-
return
|
|
345
|
+
return fs4.statSync(dirPath).isDirectory();
|
|
164
346
|
} catch {
|
|
165
347
|
return false;
|
|
166
348
|
}
|
|
167
349
|
}
|
|
168
350
|
|
|
169
351
|
// src/bin/utils/secretGuard.ts
|
|
170
|
-
var
|
|
352
|
+
var path4 = __toESM(require("path"), 1);
|
|
171
353
|
var BLOCKED_EXACT_BASENAMES = /* @__PURE__ */ new Set([
|
|
172
354
|
".env",
|
|
173
355
|
".env.local",
|
|
@@ -223,8 +405,8 @@ var BLOCKED_PATH_SEGMENTS = [
|
|
|
223
405
|
];
|
|
224
406
|
function assertSafeToRead(filePath) {
|
|
225
407
|
const normalized = filePath.replace(/\\/g, "/");
|
|
226
|
-
const basename3 =
|
|
227
|
-
const ext =
|
|
408
|
+
const basename3 = path4.basename(filePath);
|
|
409
|
+
const ext = path4.extname(filePath).toLowerCase();
|
|
228
410
|
if (basename3 === ".env.example") return true;
|
|
229
411
|
if (/^\.env\.example(\.\w+)?$/.test(basename3)) return true;
|
|
230
412
|
if (BLOCKED_EXACT_BASENAMES.has(basename3.toLowerCase())) return false;
|
|
@@ -285,8 +467,8 @@ function fileToNextPagesRoute(filePath, pagesDir) {
|
|
|
285
467
|
}
|
|
286
468
|
var APP_PAGE_BASENAME = /^page\.(tsx?|jsx?)$/;
|
|
287
469
|
function fileToAppRoute(filePath, appDir) {
|
|
288
|
-
if (!APP_PAGE_BASENAME.test(
|
|
289
|
-
let rel =
|
|
470
|
+
if (!APP_PAGE_BASENAME.test(path5.basename(filePath))) return "";
|
|
471
|
+
let rel = path5.dirname(filePath).slice(appDir.length).replace(/\\/g, "/");
|
|
290
472
|
if (rel.startsWith("/")) rel = rel.slice(1);
|
|
291
473
|
rel = rel.replace(/\([^)]*\)\//g, "").replace(/\([^)]*\)$/, "");
|
|
292
474
|
rel = rel.replace(/\[\.\.\.([^\]]+)\]/g, "*").replace(/\[([^\]]+)\]/g, ":$1");
|
|
@@ -298,7 +480,7 @@ var RE_REACT_OBJ = /\bpath:\s*["'](\/?[^"']+)["']/g;
|
|
|
298
480
|
var RE_VUE_ROUTE = /\{\s*path:\s*["']([^"']+)["']/g;
|
|
299
481
|
var RE_HTTP_ROUTE = /\bapp\.(?:get|post|put|patch|delete|all)\(["']([/][^"']+)["']/g;
|
|
300
482
|
function grepSourceFile(filePath, addRoute) {
|
|
301
|
-
if (!SOURCE_EXTS.has(
|
|
483
|
+
if (!SOURCE_EXTS.has(path5.extname(filePath).toLowerCase())) return;
|
|
302
484
|
if (!assertSafeToRead(filePath)) return;
|
|
303
485
|
const content = readFileSafe(filePath);
|
|
304
486
|
if (!content) return;
|
|
@@ -321,8 +503,8 @@ function detectRoutes(targetDir) {
|
|
|
321
503
|
if (trimmed && trimmed !== "*") routes.add(trimmed);
|
|
322
504
|
};
|
|
323
505
|
for (const dir of [
|
|
324
|
-
|
|
325
|
-
|
|
506
|
+
path5.join(targetDir, "pages"),
|
|
507
|
+
path5.join(targetDir, "src", "pages")
|
|
326
508
|
]) {
|
|
327
509
|
if (!dirExists(dir)) continue;
|
|
328
510
|
for (const f of walk(dir)) {
|
|
@@ -333,8 +515,8 @@ function detectRoutes(targetDir) {
|
|
|
333
515
|
}
|
|
334
516
|
}
|
|
335
517
|
for (const dir of [
|
|
336
|
-
|
|
337
|
-
|
|
518
|
+
path5.join(targetDir, "app"),
|
|
519
|
+
path5.join(targetDir, "src", "app")
|
|
338
520
|
]) {
|
|
339
521
|
if (!dirExists(dir)) continue;
|
|
340
522
|
for (const f of walk(dir)) {
|
|
@@ -344,7 +526,7 @@ function detectRoutes(targetDir) {
|
|
|
344
526
|
}
|
|
345
527
|
}
|
|
346
528
|
}
|
|
347
|
-
const srcDir =
|
|
529
|
+
const srcDir = path5.join(targetDir, "src");
|
|
348
530
|
if (dirExists(srcDir)) {
|
|
349
531
|
for (const f of walk(srcDir)) {
|
|
350
532
|
grepSourceFile(f, addRoute);
|
|
@@ -352,8 +534,8 @@ function detectRoutes(targetDir) {
|
|
|
352
534
|
}
|
|
353
535
|
const rootCandidates = ["routes.ts", "routes.js", "router.ts", "router.js", "app.ts", "app.js"];
|
|
354
536
|
for (const name of rootCandidates) {
|
|
355
|
-
const f =
|
|
356
|
-
if (
|
|
537
|
+
const f = path5.join(targetDir, name);
|
|
538
|
+
if (fs5.existsSync(f)) grepSourceFile(f, addRoute);
|
|
357
539
|
}
|
|
358
540
|
const buckets = { buyer: [], seller: [], admin: [] };
|
|
359
541
|
for (const route of Array.from(routes).sort()) {
|
|
@@ -384,8 +566,8 @@ function detectRoutes(targetDir) {
|
|
|
384
566
|
}
|
|
385
567
|
|
|
386
568
|
// src/bin/detectors/detectCredentials.ts
|
|
387
|
-
var
|
|
388
|
-
var
|
|
569
|
+
var path6 = __toESM(require("path"), 1);
|
|
570
|
+
var fs6 = __toESM(require("fs"), 1);
|
|
389
571
|
var CREDENTIALS_BANNER = '// DEV/TEST/SEED ONLY \u2014 never production, never commit real passwords.\n// Extracted from .env.example and seeder files only.\n// Replace any "TODO: set from env \u2026" values with your actual dev/test credentials.';
|
|
390
572
|
var SEEDER_PATH_PATTERNS = [
|
|
391
573
|
/[\\/]seeders?[\\/]/i,
|
|
@@ -515,16 +697,16 @@ var SEEDER_SEARCH_DIRS = [
|
|
|
515
697
|
];
|
|
516
698
|
function detectCredentials(targetDir) {
|
|
517
699
|
const allMatches = [];
|
|
518
|
-
const envExample =
|
|
519
|
-
if (
|
|
700
|
+
const envExample = path6.join(targetDir, ".env.example");
|
|
701
|
+
if (fs6.existsSync(envExample) && assertSafeToRead(envExample)) {
|
|
520
702
|
const content = readFileSafe(envExample);
|
|
521
703
|
if (content) allMatches.push(...extractMatches(content, envExample));
|
|
522
704
|
}
|
|
523
705
|
for (const rel of SEEDER_SEARCH_DIRS) {
|
|
524
|
-
const dirPath =
|
|
706
|
+
const dirPath = path6.join(targetDir, rel);
|
|
525
707
|
if (!dirExists(dirPath)) continue;
|
|
526
708
|
const files = walk(dirPath).filter((f) => {
|
|
527
|
-
const ext =
|
|
709
|
+
const ext = path6.extname(f);
|
|
528
710
|
return [".js", ".ts", ".mjs", ".cjs", ".json"].includes(ext) && isSeederFile(f) && assertSafeToRead(f);
|
|
529
711
|
});
|
|
530
712
|
for (const f of files) {
|
|
@@ -775,13 +957,13 @@ var REPO_URL = "https://github.com/mohammed-farhood/qapture";
|
|
|
775
957
|
var PKG_VERSION = (() => {
|
|
776
958
|
try {
|
|
777
959
|
const candidates = [
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
960
|
+
path7.join(__dirname, "..", "..", "package.json"),
|
|
961
|
+
path7.join(__dirname, "..", "package.json"),
|
|
962
|
+
path7.join(__dirname, "package.json")
|
|
781
963
|
];
|
|
782
964
|
for (const p of candidates) {
|
|
783
|
-
if (
|
|
784
|
-
const pkg = JSON.parse(
|
|
965
|
+
if (fs7.existsSync(p)) {
|
|
966
|
+
const pkg = JSON.parse(fs7.readFileSync(p, "utf8"));
|
|
785
967
|
if (pkg.version) return pkg.version;
|
|
786
968
|
}
|
|
787
969
|
}
|
|
@@ -791,14 +973,14 @@ var PKG_VERSION = (() => {
|
|
|
791
973
|
})();
|
|
792
974
|
function readTargetPkg(targetDir) {
|
|
793
975
|
try {
|
|
794
|
-
const raw =
|
|
976
|
+
const raw = fs7.readFileSync(path7.join(targetDir, "package.json"), "utf8");
|
|
795
977
|
return JSON.parse(raw);
|
|
796
978
|
} catch {
|
|
797
979
|
return {};
|
|
798
980
|
}
|
|
799
981
|
}
|
|
800
982
|
function hasFile(targetDir, ...names) {
|
|
801
|
-
return names.some((n) =>
|
|
983
|
+
return names.some((n) => fs7.existsSync(path7.join(targetDir, n)));
|
|
802
984
|
}
|
|
803
985
|
function detectFrameworkHints(targetDir, pkg) {
|
|
804
986
|
const hints = [];
|
|
@@ -835,11 +1017,15 @@ qapture CLI v${PKG_VERSION}
|
|
|
835
1017
|
|
|
836
1018
|
Usage:
|
|
837
1019
|
qapture init [target-dir] [--force] Scaffold config + artifacts into target-dir
|
|
1020
|
+
qapture shots [--port N] [--allow O] Real screenshots without the browser prompt (macOS)
|
|
838
1021
|
qapture version Print version
|
|
839
1022
|
|
|
840
1023
|
Options:
|
|
841
1024
|
--force Overwrite qa.config.* and qa.preamble.md if they already exist
|
|
842
1025
|
(SKILL.md is always refreshed regardless of --force)
|
|
1026
|
+
--port Port for \`shots\` to listen on (default 7017)
|
|
1027
|
+
--allow Extra origin \`shots\` will answer, repeatable.
|
|
1028
|
+
Loopback (http://localhost:*) is always allowed.
|
|
843
1029
|
|
|
844
1030
|
Docs: ${REPO_URL}
|
|
845
1031
|
|
|
@@ -916,6 +1102,79 @@ ${DIVIDER}
|
|
|
916
1102
|
`
|
|
917
1103
|
);
|
|
918
1104
|
}
|
|
1105
|
+
function startShotServer(port, allow) {
|
|
1106
|
+
if (process2.platform !== "darwin") {
|
|
1107
|
+
process2.stderr.write(
|
|
1108
|
+
`
|
|
1109
|
+
qapture shots needs macOS \u2014 it drives /usr/sbin/screencapture,
|
|
1110
|
+
the binary behind Cmd+Shift+4. On ${process2.platform}, Qapture falls back
|
|
1111
|
+
to the browser's own screen capture.
|
|
1112
|
+
|
|
1113
|
+
`
|
|
1114
|
+
);
|
|
1115
|
+
process2.exit(1);
|
|
1116
|
+
}
|
|
1117
|
+
const server = createShotServer({ port, allow, verbose: true });
|
|
1118
|
+
server.on("error", (err) => {
|
|
1119
|
+
if (err.code === "EADDRINUSE") {
|
|
1120
|
+
process2.stderr.write(
|
|
1121
|
+
`
|
|
1122
|
+
Port ${port} is already in use.
|
|
1123
|
+
|
|
1124
|
+
If that is another copy of this helper, you are already running one.
|
|
1125
|
+
Otherwise pick a different port: npx qapture2 shots --port 7018
|
|
1126
|
+
(and set shotPort: 7018 in your Qapture config, so the widget looks there)
|
|
1127
|
+
|
|
1128
|
+
`
|
|
1129
|
+
);
|
|
1130
|
+
} else {
|
|
1131
|
+
process2.stderr.write(`
|
|
1132
|
+
qapture shots failed to start: ${err.message}
|
|
1133
|
+
|
|
1134
|
+
`);
|
|
1135
|
+
}
|
|
1136
|
+
process2.exit(1);
|
|
1137
|
+
});
|
|
1138
|
+
server.listen(port, "127.0.0.1", () => {
|
|
1139
|
+
process2.stdout.write(
|
|
1140
|
+
`
|
|
1141
|
+
${DIVIDER}
|
|
1142
|
+
qapture shots \u2014 real screenshots, no prompt, no heat
|
|
1143
|
+
${DIVIDER}
|
|
1144
|
+
|
|
1145
|
+
Listening 127.0.0.1:${port} (loopback only \u2014 not reachable from the network)
|
|
1146
|
+
Engine /usr/sbin/screencapture \u2014 the same one Cmd+Shift+4 uses
|
|
1147
|
+
Answering http://localhost:* and http://127.0.0.1:*
|
|
1148
|
+
` + (allow.length ? ` ${allow.join("\n ")}
|
|
1149
|
+
` : ` (add others with --allow https://your-site.example)
|
|
1150
|
+
`) + `
|
|
1151
|
+
First run: macOS will ask this terminal for Screen Recording permission.
|
|
1152
|
+
Grant it once. It is never asked again, and no capture pipeline is left
|
|
1153
|
+
running between shots \u2014 that is the whole point of moving off the
|
|
1154
|
+
browser's getDisplayMedia.
|
|
1155
|
+
|
|
1156
|
+
If captures come back as wallpaper with no windows in them, the
|
|
1157
|
+
permission was not granted: System Settings \u2192 Privacy & Security \u2192
|
|
1158
|
+
Screen Recording, tick your terminal, then restart it.
|
|
1159
|
+
|
|
1160
|
+
Leave this running while you test. Ctrl+C to stop.
|
|
1161
|
+
${DIVIDER}
|
|
1162
|
+
|
|
1163
|
+
`
|
|
1164
|
+
);
|
|
1165
|
+
});
|
|
1166
|
+
const shutdown = () => {
|
|
1167
|
+
process2.stdout.write(`
|
|
1168
|
+
qapture shots stopped.
|
|
1169
|
+
|
|
1170
|
+
`);
|
|
1171
|
+
server.close(() => process2.exit(0));
|
|
1172
|
+
setTimeout(() => process2.exit(0), 500).unref();
|
|
1173
|
+
};
|
|
1174
|
+
const proc = globalThis.process;
|
|
1175
|
+
proc.on("SIGINT", shutdown);
|
|
1176
|
+
proc.on("SIGTERM", shutdown);
|
|
1177
|
+
}
|
|
919
1178
|
function main(argv2) {
|
|
920
1179
|
const args = parseArgs(argv2);
|
|
921
1180
|
if (args.command === "version") {
|
|
@@ -926,16 +1185,20 @@ function main(argv2) {
|
|
|
926
1185
|
printUsage();
|
|
927
1186
|
process2.exit(0);
|
|
928
1187
|
}
|
|
929
|
-
|
|
1188
|
+
if (args.command === "shots") {
|
|
1189
|
+
startShotServer(args.port, args.allow);
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
const targetDir = path7.resolve(args.dir);
|
|
930
1193
|
const { force } = args;
|
|
931
|
-
if (!
|
|
1194
|
+
if (!fs7.existsSync(targetDir)) {
|
|
932
1195
|
process2.stderr.write(`
|
|
933
1196
|
Error: target directory does not exist: ${targetDir}
|
|
934
1197
|
|
|
935
1198
|
`);
|
|
936
1199
|
process2.exit(1);
|
|
937
1200
|
}
|
|
938
|
-
if (!
|
|
1201
|
+
if (!fs7.statSync(targetDir).isDirectory()) {
|
|
939
1202
|
process2.stderr.write(`
|
|
940
1203
|
Error: ${targetDir} is not a directory
|
|
941
1204
|
|
|
@@ -967,10 +1230,10 @@ qapture init \u2014 scanning ${targetDir} ...
|
|
|
967
1230
|
frameworkHints
|
|
968
1231
|
});
|
|
969
1232
|
const preambleText = genPreambleText({ projectName, frameworkHints });
|
|
970
|
-
const configPath =
|
|
971
|
-
const preamblePath =
|
|
972
|
-
const skillPath =
|
|
973
|
-
const agentsMdPath =
|
|
1233
|
+
const configPath = path7.join(targetDir, configFilename);
|
|
1234
|
+
const preamblePath = path7.join(targetDir, "qa.preamble.md");
|
|
1235
|
+
const skillPath = path7.join(targetDir, ".claude", "skills", "qapture", "SKILL.md");
|
|
1236
|
+
const agentsMdPath = path7.join(targetDir, "AGENTS.md");
|
|
974
1237
|
const configResult = writeIfAbsent(configPath, configText, force);
|
|
975
1238
|
const preambleResult = writeIfAbsent(preamblePath, preambleText, force);
|
|
976
1239
|
writeAlways(skillPath, SKILL_default);
|