@m2farhood-2/qapture 0.2.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,1186 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/bin/init.ts
27
+ var path7 = __toESM(require("path"), 1);
28
+ var fs7 = __toESM(require("fs"), 1);
29
+ var process2 = __toESM(require("process"), 1);
30
+
31
+ // src/artifacts/SKILL.md
32
+ var SKILL_default = "---\nname: qapture\ndescription: >\n Activated when the user provides a `qa-notes-*.zip` file exported from\n Qapture. Reads the preamble block in `notes.md` (project context, stack, run\n commands, theme tokens, dev/test login credentials, red-zone coverage report,\n and invariants), flags any uncovered RED risk zones before acting, then works\n through each `## Point N` annotation (page, element selector, screenshot \u2192\n locate code \u2192 make the change \u2192 verify by running the app). Finally grades\n coverage against the red zones and reports.\n\n **No AI is bundled in Qapture \u2014 YOU are the AI reading these artifacts.**\n Qapture is a 100% client-side, keyless, network-free capture widget.\ntriggers:\n - qa-notes-*.zip\n---\n\n# Qapture \u2014 Agent Skill\n\n> **Core principle:** Qapture ships zero AI. No model, no API keys, no network\n> calls. The CLI is a plain deterministic scaffolder. **You** \u2014 the coding agent\n> reading this skill \u2014 are the AI. The developer used Qapture to capture\n> annotated screenshots + notes from their live app; your job is to act on them.\n\n---\n\n## What Is Qapture?\n\nQapture is a drop-in in-browser widget (Shadow DOM, keyless, no telemetry).\nTesters annotate the live app: click an element or draw a region, add a note,\nand the widget captures a screenshot automatically. When done, they export a\n`qa-notes-*.zip`. That ZIP is the hand-off to you.\n\n---\n\n## ZIP Layout\n\n```\nqa-notes-<timestamp>.zip\n\u251C\u2500\u2500 notes.md \u2190 ALWAYS read this first (see Step 1)\n\u2514\u2500\u2500 screenshots/\n \u251C\u2500\u2500 point-1.png\n \u251C\u2500\u2500 point-2.png\n \u2514\u2500\u2500 ...\n```\n\n### `notes.md` structure\n\n```\n[PREAMBLE BLOCK]\n Project name, one-liner, stack, run commands, theme tokens,\n Login Context (dev/test credentials \u2014 see security note below),\n Coverage Report (red/amber/green zone checklist),\n Invariants, Additional Context.\n\n---NOTES---\n\n## Point 1\nPage: /some/path\nSelector: #some-element (or [data-testid=\"foo\"] etc.)\nNote: the tester's free-text description of the issue / request\n\n## Point 2\n...\n```\n\n---\n\n## Step 1 \u2014 Read the Preamble First\n\nBefore touching any code, open `notes.md` and parse everything **above** the\n`---NOTES---` separator. Extract and internalize:\n\n| Section | What to do |\n| ------------------ | --------------------------------------------------------------------- |\n| **Project / Stack** | Understand the framework, router, ORM, and any unusual constraints. |\n| **Run Commands** | Know how to start the dev server and seed the database. |\n| **Theme Tokens** | Understand the colour palette so you don't introduce style regressions.|\n| **Login Context** | DEV/TEST/SEED credentials only. Use these to log in during verification. **Never log, forward, or commit these values.** |\n| **Coverage Report**| List of RED / AMBER / GREEN zones and whether they are covered. |\n| **Invariants** | Absolute rules you must never violate (e.g. \"prices \u2265 0\", \"checkout requires auth\"). |\n| **Conventions** | Codebase naming, file organisation, import rules, validation approach. |\n\n---\n\n## Step 2 \u2014 Flag Uncovered RED Zones Before Acting\n\nAfter reading the preamble, check the Coverage Report for any RED zones that\nare **not yet covered** by an annotation in this ZIP.\n\nIf uncovered RED zones exist, **report them to the developer first**:\n\n```\n\u26A0\uFE0F Uncovered RED zones detected:\n \u2022 /checkout/payment \u2014 no annotation in this export\n \u2022 /seller/payouts \u2014 no annotation in this export\n\nThese are money/auth/irreversible flows. Do you want me to proceed with the\ncovered points only, or will you add annotations for the red zones first?\n```\n\nWait for developer confirmation before proceeding if any RED zone is uncovered.\n\n---\n\n## Step 3 \u2014 Act on Each Point\n\nFor each `## Point N` section in `notes.md`:\n\n### 3a. Read the annotation\n\n- **Page** \u2014 the route/URL where the issue was captured.\n- **Selector** \u2014 the CSS selector or aria identifier for the element.\n- **Note** \u2014 the tester's description of the problem or change request.\n\n### 3b. Open the screenshot\n\nLoad `screenshots/point-N.png` to visually confirm what the tester saw.\nThe screenshot is truth \u2014 if the selector doesn't resolve, the screenshot tells\nyou what element they meant.\n\n### 3c. Locate the code\n\nUse the selector priority chain below to find the relevant source:\n\n| Priority | Selector type | Action |\n| -------- | -------------------------------------------------- | --------------------------------------------------- |\n| 1 | `#some-id` | `grep -r 'some-id'` in `src/` |\n| 2 | `[data-testid=\"foo\"]` / `[data-test]` / `[data-cy]` | grep for the attribute value |\n| 3 | `aria-label` on interactive elements | grep for the label string |\n| 4 | `name` attribute on form fields | grep for `name=\"...\"` in the relevant form file |\n| 5 | Structural (e.g. `.card:nth-of-type(2) > button`) | narrow by page route \u2192 component file \u2192 visual match with screenshot |\n| Fallback | Selector didn't resolve | Use the screenshot: identify the element visually, search by text content or component name |\n\nNarrow your search by the **Page** field to avoid editing the wrong route's code.\n\n### 3d. Make the change\n\n- Follow the project's **Conventions** (from the preamble).\n- Respect all **Invariants** \u2014 never violate them even if the annotation implies it.\n- If the change touches a RED zone (money / auth / irreversible state), add an\n explicit comment: `// QA: red-zone change \u2014 reviewed <date>`.\n- Do **not** edit `qa.config.ts`, `qa.preamble.md`, or any qapture plugin files.\n\n---\n\n## Step 4 \u2014 Verify the Fix\n\n1. Run the app using the **Run Commands** from the preamble.\n2. Log in as the relevant role using **Login Context** credentials.\n (These are DEV/TEST/SEED only \u2014 never use production credentials.)\n3. Navigate to the **Page** listed in the annotation.\n4. **Reproduce** the original issue to confirm it existed, then verify it is fixed.\n5. In the browser console, run `document.querySelector('<selector>')` to confirm\n the element resolves as expected.\n6. Check adjacent paths for regressions, especially if the change is in a shared\n component.\n\n---\n\n## Step 5 \u2014 Grade and Report\n\nAfter acting on all points, produce a short report:\n\n```markdown\n## Qapture \u2014 Changes Summary\n\n| Point | Page | Change made | Verified | Risk |\n| ----- | --------------- | ----------------------------- | -------- | ----- |\n| 1 | /products | Fixed button label | \u2713 | green |\n| 2 | /checkout | Corrected total calculation | \u2713 | red |\n\n### Coverage vs Red Zones\n- [x] /checkout/payment \u2014 covered by Point 2\n- [ ] /seller/payouts \u2014 NOT covered (flagged in Step 2)\n\n### Uncovered items\nNone (all annotated points addressed).\n```\n\n---\n\n## Risk Zone Reference\n\n| Risk | Examples | Rule |\n| ------- | ----------------------------------------------------------------- | ------------------------------------------------ |\n| **red** | Payment, checkout, auth, order state, refunds, payouts, user data | Never change without developer review + comment |\n| **amber** | Cart, product listings, search, seller dashboard, inventory | Change carefully; verify end-to-end |\n| **green** | Static content, labels, colours, copy, tooltips, layout | Change freely; quick smoke-test |\n\n---\n\n## Security Note\n\n- **Dev/test/seed credentials only.** The Login Context in `notes.md` contains\n credentials from `.env.example` or seeder files \u2014 never from production.\n- **Never log, forward, store, or commit** Login Context values outside the\n development environment.\n- **Never read** `.env`, `.env.local`, `.env.production`, or any `secrets/`\n path. Qapture's CLI enforces this; you must too.\n- Qapture is **100% client-side** \u2014 it makes no network calls, holds no API\n keys, and sends no data anywhere.\n- **Never push, publish, or deploy** changes without explicit human approval,\n regardless of risk level.\n\n---\n\n## Common Pitfalls\n\n- **Don't assume selectors always resolve.** Selector strings may be stale if\n the DOM changed after annotation. When in doubt, use the screenshot.\n- **Don't skip the preamble.** Acting without reading the invariants or run\n commands is the most common source of broken fixes.\n- **Don't edit qapture config or plugin files** (`qa.config.ts`,\n `qa.preamble.md`, `.claude/skills/qapture/`, `src/components/qa-overlay/`).\n- **Don't use production credentials** \u2014 ever.\n- **Don't push/publish without human approval** \u2014 always present the changes\n for review first.\n- **Don't violate invariants** even if the annotation seems to imply it.\n Surface the conflict to the developer instead.\n\n---\n\n_Qapture \u2014 https://github.com/mohammed-farhood/qapture_\n";
33
+
34
+ // src/artifacts/AGENTS_SECTION.md
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 - **Theme tokens** \u2014 colour palette (respect these in any UI changes).\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. **Act on each `## Point N`** annotation:\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 `screenshots/point-N.png`).\n - Make the change following the project conventions and invariants.\n - **Verify**: run the app, log in as the relevant role, navigate to the page,\n confirm the fix.\n5. **Report** a summary table of changes, risk levels, and coverage status.\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
+
37
+ // src/bin/utils/args.ts
38
+ function parseArgs(argv2) {
39
+ const [cmd, ...rest] = argv2;
40
+ if (cmd === "version" || cmd === "--version" || cmd === "-v" || cmd === "-V") {
41
+ return { command: "version", dir: process.cwd(), force: false };
42
+ }
43
+ if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") {
44
+ return { command: "help", dir: process.cwd(), force: false };
45
+ }
46
+ if (cmd === "init") {
47
+ let dir = process.cwd();
48
+ let force = false;
49
+ for (const arg of rest) {
50
+ if (arg === "--force" || arg === "-f") {
51
+ force = true;
52
+ } else if (arg === "--no-force") {
53
+ force = false;
54
+ } else if (!arg.startsWith("-")) {
55
+ dir = arg;
56
+ }
57
+ }
58
+ return { command: "init", dir, force };
59
+ }
60
+ return { command: "help", dir: process.cwd(), force: false };
61
+ }
62
+
63
+ // src/bin/utils/writeIdempotent.ts
64
+ var fs = __toESM(require("fs"), 1);
65
+ var path = __toESM(require("path"), 1);
66
+ function writeIfAbsent(filePath, content, force) {
67
+ if (!force && fs.existsSync(filePath)) {
68
+ return "skipped";
69
+ }
70
+ ensureParentDir(filePath);
71
+ fs.writeFileSync(filePath, content, "utf8");
72
+ return "written";
73
+ }
74
+ function writeAlways(filePath, content) {
75
+ ensureParentDir(filePath);
76
+ fs.writeFileSync(filePath, content, "utf8");
77
+ }
78
+ function ensureParentDir(filePath) {
79
+ const dir = path.dirname(filePath);
80
+ if (!fs.existsSync(dir)) {
81
+ fs.mkdirSync(dir, { recursive: true });
82
+ }
83
+ }
84
+
85
+ // src/bin/utils/mergeAgentsMd.ts
86
+ var fs2 = __toESM(require("fs"), 1);
87
+ var SENTINEL_OPEN = "<!-- qa-studio-section -->";
88
+ var SENTINEL_CLOSE = "<!-- /qa-studio-section -->";
89
+ function mergeAgentsMd(agentsMdPath, sectionContent) {
90
+ if (!fs2.existsSync(agentsMdPath)) {
91
+ writeAlways(agentsMdPath, sectionContent.trimEnd() + "\n");
92
+ return "created";
93
+ }
94
+ const existing = fs2.readFileSync(agentsMdPath, "utf8");
95
+ const openIdx = existing.indexOf(SENTINEL_OPEN);
96
+ const closeIdx = existing.indexOf(SENTINEL_CLOSE);
97
+ if (openIdx !== -1 && closeIdx !== -1 && closeIdx > openIdx) {
98
+ const before = existing.slice(0, openIdx);
99
+ const after = existing.slice(closeIdx + SENTINEL_CLOSE.length);
100
+ const updated = before.replace(/\n*$/, "\n") + sectionContent.trimEnd() + "\n" + after.replace(/^\n*/, "\n");
101
+ fs2.writeFileSync(agentsMdPath, updated, "utf8");
102
+ return "replaced";
103
+ }
104
+ const separator = existing.trimEnd().length === 0 ? "" : "\n\n";
105
+ fs2.writeFileSync(
106
+ agentsMdPath,
107
+ existing.trimEnd() + separator + sectionContent.trimEnd() + "\n",
108
+ "utf8"
109
+ );
110
+ return "appended";
111
+ }
112
+
113
+ // src/bin/detectors/detectRoutes.ts
114
+ var path4 = __toESM(require("path"), 1);
115
+ var fs4 = __toESM(require("fs"), 1);
116
+
117
+ // src/bin/utils/walk.ts
118
+ var fs3 = __toESM(require("fs"), 1);
119
+ var path2 = __toESM(require("path"), 1);
120
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
121
+ "node_modules",
122
+ ".git",
123
+ "dist",
124
+ "build",
125
+ ".next",
126
+ ".turbo",
127
+ ".cache",
128
+ "coverage",
129
+ ".nyc_output",
130
+ ".parcel-cache",
131
+ "__pycache__",
132
+ ".venv",
133
+ "vendor"
134
+ ]);
135
+ function walk(dir) {
136
+ const results = [];
137
+ let entries;
138
+ try {
139
+ entries = fs3.readdirSync(dir, { withFileTypes: true });
140
+ } catch {
141
+ return results;
142
+ }
143
+ for (const entry of entries) {
144
+ if (entry.isSymbolicLink()) continue;
145
+ if (entry.isDirectory()) {
146
+ if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
147
+ results.push(...walk(path2.join(dir, entry.name)));
148
+ } else if (entry.isFile()) {
149
+ results.push(path2.join(dir, entry.name));
150
+ }
151
+ }
152
+ return results;
153
+ }
154
+ function readFileSafe(filePath) {
155
+ try {
156
+ return fs3.readFileSync(filePath, "utf8");
157
+ } catch {
158
+ return "";
159
+ }
160
+ }
161
+ function dirExists(dirPath) {
162
+ try {
163
+ return fs3.statSync(dirPath).isDirectory();
164
+ } catch {
165
+ return false;
166
+ }
167
+ }
168
+
169
+ // src/bin/utils/secretGuard.ts
170
+ var path3 = __toESM(require("path"), 1);
171
+ var BLOCKED_EXACT_BASENAMES = /* @__PURE__ */ new Set([
172
+ ".env",
173
+ ".env.local",
174
+ ".env.development",
175
+ ".env.test",
176
+ ".env.production",
177
+ ".env.staging",
178
+ ".env.ci",
179
+ ".env.preview",
180
+ ".env.override"
181
+ ]);
182
+ var BLOCKED_EXTENSIONS = /* @__PURE__ */ new Set([
183
+ ".pem",
184
+ ".key",
185
+ ".pfx",
186
+ ".p12",
187
+ ".crt",
188
+ ".der",
189
+ ".p8",
190
+ ".jks",
191
+ ".keystore",
192
+ ".secret"
193
+ ]);
194
+ var BLOCKED_BASENAME_PATTERNS = [
195
+ /^credentials?\./i,
196
+ // credentials.json, credential.yml, …
197
+ /^secrets?\./i,
198
+ // secrets.json, secret.yaml, …
199
+ /\.secret$/i,
200
+ // foo.secret
201
+ /^private_key/i,
202
+ // private_key.json (GCP service account)
203
+ /^service[-_]?account/i,
204
+ // service-account.json
205
+ /^keyfile/i,
206
+ // keyfile.json
207
+ /^\.netrc$/i,
208
+ /^\.pgpass$/i,
209
+ /^id_rsa/i,
210
+ // SSH private keys
211
+ /^id_ed25519/i,
212
+ /^id_ecdsa/i,
213
+ /^id_dsa/i
214
+ ];
215
+ var BLOCKED_PATH_SEGMENTS = [
216
+ /[\\/]secrets?[\\/]/i,
217
+ /[\\/]\.secrets?[\\/]/i,
218
+ /[^\\/][\\/]private[\\/]/i,
219
+ /[\\/]certs?[\\/]/i,
220
+ /[\\/]certificates?[\\/]/i,
221
+ /[\\/]keys?[\\/]/i,
222
+ /[\\/]credentials?[\\/]/i
223
+ ];
224
+ function assertSafeToRead(filePath) {
225
+ const normalized = filePath.replace(/\\/g, "/");
226
+ const basename3 = path3.basename(filePath);
227
+ const ext = path3.extname(filePath).toLowerCase();
228
+ if (basename3 === ".env.example") return true;
229
+ if (/^\.env\.example(\.\w+)?$/.test(basename3)) return true;
230
+ if (BLOCKED_EXACT_BASENAMES.has(basename3)) return false;
231
+ if (/^\.env\./i.test(basename3) && !/^\.env\.example/i.test(basename3)) {
232
+ return false;
233
+ }
234
+ if (BLOCKED_EXTENSIONS.has(ext)) return false;
235
+ for (const pat of BLOCKED_BASENAME_PATTERNS) {
236
+ if (pat.test(basename3)) return false;
237
+ }
238
+ for (const pat of BLOCKED_PATH_SEGMENTS) {
239
+ if (pat.test(normalized)) return false;
240
+ }
241
+ return true;
242
+ }
243
+
244
+ // src/bin/detectors/detectRoutes.ts
245
+ var LANE_COLORS = {
246
+ buyer: "#4f46e5",
247
+ // indigo
248
+ seller: "#7c3aed",
249
+ // violet
250
+ admin: "#dc2626"
251
+ // red
252
+ };
253
+ var LANE_META = [
254
+ { id: "buyer", en: "Buyer / Public", ar: "\u0627\u0644\u0645\u0634\u062A\u0631\u064A / \u0639\u0627\u0645" },
255
+ { id: "seller", en: "Seller", ar: "\u0627\u0644\u0628\u0627\u0626\u0639" },
256
+ { id: "admin", en: "Admin", ar: "\u0627\u0644\u0645\u0633\u0624\u0648\u0644" }
257
+ ];
258
+ function makeStep(routePath) {
259
+ return {
260
+ path: routePath,
261
+ risk: "green",
262
+ what: { en: "TODO: describe what to test here", ar: "" }
263
+ };
264
+ }
265
+ function classifyPath(routePath) {
266
+ const p = routePath.toLowerCase();
267
+ if (/^\/(seller|store-owner|vendor)/.test(p)) return "seller";
268
+ if (/^\/(admin|dashboard|control-panel|backoffice|back-office|management|cms)/.test(p)) {
269
+ return "admin";
270
+ }
271
+ if (/^\/(login|signin|sign-in|signup|sign-up|register|auth|forgot-password|reset-password|verify|email-verification|oauth)/.test(p)) {
272
+ return "auth";
273
+ }
274
+ return "buyer";
275
+ }
276
+ function fileToNextPagesRoute(filePath, pagesDir) {
277
+ let rel = filePath.slice(pagesDir.length).replace(/\\/g, "/");
278
+ if (rel.startsWith("/")) rel = rel.slice(1);
279
+ rel = rel.replace(/\.(tsx?|jsx?|mdx?)$/, "");
280
+ if (rel === "index") return "/";
281
+ rel = rel.replace(/\/index$/, "");
282
+ if (/^_/.test(rel) || /^api(\/|$)/.test(rel)) return "";
283
+ rel = rel.replace(/\[\.\.\.([^\]]+)\]/g, "*").replace(/\[([^\]]+)\]/g, ":$1");
284
+ return "/" + rel;
285
+ }
286
+ var APP_PAGE_BASENAME = /^page\.(tsx?|jsx?)$/;
287
+ function fileToAppRoute(filePath, appDir) {
288
+ if (!APP_PAGE_BASENAME.test(path4.basename(filePath))) return "";
289
+ let rel = path4.dirname(filePath).slice(appDir.length).replace(/\\/g, "/");
290
+ if (rel.startsWith("/")) rel = rel.slice(1);
291
+ rel = rel.replace(/\([^)]*\)\//g, "").replace(/\([^)]*\)$/, "");
292
+ rel = rel.replace(/\[\.\.\.([^\]]+)\]/g, "*").replace(/\[([^\]]+)\]/g, ":$1");
293
+ return "/" + (rel || "");
294
+ }
295
+ var SOURCE_EXTS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".vue", ".mjs", ".cjs"]);
296
+ var RE_REACT_JSX = /<Route\b[^>]*\bpath=["']([^"']+)["']/g;
297
+ var RE_REACT_OBJ = /\bpath:\s*["'](\/?[^"']+)["']/g;
298
+ var RE_VUE_ROUTE = /\{\s*path:\s*["']([^"']+)["']/g;
299
+ var RE_HTTP_ROUTE = /\bapp\.(?:get|post|put|patch|delete|all)\(["']([/][^"']+)["']/g;
300
+ function grepSourceFile(filePath, addRoute) {
301
+ if (!SOURCE_EXTS.has(path4.extname(filePath).toLowerCase())) return;
302
+ if (!assertSafeToRead(filePath)) return;
303
+ const content = readFileSafe(filePath);
304
+ if (!content) return;
305
+ let m;
306
+ RE_REACT_JSX.lastIndex = 0;
307
+ while ((m = RE_REACT_JSX.exec(content)) !== null) addRoute(m[1]);
308
+ RE_REACT_OBJ.lastIndex = 0;
309
+ while ((m = RE_REACT_OBJ.exec(content)) !== null) {
310
+ if (m[1].startsWith("/") || m[1] === "*") addRoute(m[1]);
311
+ }
312
+ RE_VUE_ROUTE.lastIndex = 0;
313
+ while ((m = RE_VUE_ROUTE.exec(content)) !== null) addRoute(m[1]);
314
+ RE_HTTP_ROUTE.lastIndex = 0;
315
+ while ((m = RE_HTTP_ROUTE.exec(content)) !== null) addRoute(m[1]);
316
+ }
317
+ function detectRoutes(targetDir) {
318
+ const routes = /* @__PURE__ */ new Set();
319
+ const addRoute = (r) => {
320
+ const trimmed = r.trim();
321
+ if (trimmed && trimmed !== "*") routes.add(trimmed);
322
+ };
323
+ for (const dir of [
324
+ path4.join(targetDir, "pages"),
325
+ path4.join(targetDir, "src", "pages")
326
+ ]) {
327
+ if (!dirExists(dir)) continue;
328
+ for (const f of walk(dir)) {
329
+ if (assertSafeToRead(f)) {
330
+ const r = fileToNextPagesRoute(f, dir);
331
+ if (r) addRoute(r);
332
+ }
333
+ }
334
+ }
335
+ for (const dir of [
336
+ path4.join(targetDir, "app"),
337
+ path4.join(targetDir, "src", "app")
338
+ ]) {
339
+ if (!dirExists(dir)) continue;
340
+ for (const f of walk(dir)) {
341
+ if (assertSafeToRead(f)) {
342
+ const r = fileToAppRoute(f, dir);
343
+ if (r) addRoute(r);
344
+ }
345
+ }
346
+ }
347
+ const srcDir = path4.join(targetDir, "src");
348
+ if (dirExists(srcDir)) {
349
+ for (const f of walk(srcDir)) {
350
+ grepSourceFile(f, addRoute);
351
+ }
352
+ }
353
+ const rootCandidates = ["routes.ts", "routes.js", "router.ts", "router.js", "app.ts", "app.js"];
354
+ for (const name of rootCandidates) {
355
+ const f = path4.join(targetDir, name);
356
+ if (fs4.existsSync(f)) grepSourceFile(f, addRoute);
357
+ }
358
+ const buckets = { buyer: [], seller: [], admin: [] };
359
+ for (const route of Array.from(routes).sort()) {
360
+ const lane = classifyPath(route);
361
+ if (lane === "auth") continue;
362
+ buckets[lane].push(route);
363
+ }
364
+ const lanes = [];
365
+ for (const { id, en, ar } of LANE_META) {
366
+ const paths = buckets[id];
367
+ if (!paths || paths.length === 0) continue;
368
+ lanes.push({
369
+ id,
370
+ color: LANE_COLORS[id] ?? "#4f46e5",
371
+ role: { en, ar },
372
+ steps: paths.map(makeStep)
373
+ });
374
+ }
375
+ if (lanes.length === 0) {
376
+ lanes.push({
377
+ id: "buyer",
378
+ color: LANE_COLORS["buyer"],
379
+ role: { en: "Buyer / Public", ar: "\u0627\u0644\u0645\u0634\u062A\u0631\u064A / \u0639\u0627\u0645" },
380
+ steps: [makeStep("/")]
381
+ });
382
+ }
383
+ return lanes;
384
+ }
385
+
386
+ // src/bin/detectors/detectTheme.ts
387
+ var path5 = __toESM(require("path"), 1);
388
+ var fs5 = __toESM(require("fs"), 1);
389
+ var PLACEHOLDER = "#REPLACE_ME";
390
+ var KEY_ALIASES = [
391
+ {
392
+ key: "primaryDark",
393
+ patterns: [
394
+ /primary[-_]?dark/i,
395
+ /primary[-_]?(?:800|900|700|deep)/i,
396
+ /brand[-_]?dark/i
397
+ ]
398
+ },
399
+ {
400
+ key: "primary",
401
+ patterns: [
402
+ /^primary$/i,
403
+ /primary[-_]?(?:base|default|main|500|600)?$/i,
404
+ /^brand$/i,
405
+ /brand[-_]?(?:main|primary|base|default)?$/i
406
+ ]
407
+ },
408
+ {
409
+ key: "accentDark",
410
+ patterns: [
411
+ /accent[-_]?dark/i,
412
+ /accent[-_]?(?:700|800|900|deep)/i,
413
+ /secondary[-_]?dark/i
414
+ ]
415
+ },
416
+ {
417
+ key: "accent",
418
+ patterns: [
419
+ /^accent$/i,
420
+ /accent[-_]?(?:base|default|main|500|600)?$/i,
421
+ /^secondary$/i,
422
+ /secondary[-_]?(?:main|base|default)?$/i,
423
+ /^highlight$/i
424
+ ]
425
+ },
426
+ {
427
+ key: "sage",
428
+ patterns: [
429
+ /^sage$/i,
430
+ /^muted$/i,
431
+ /^neutral$/i,
432
+ /^subdued$/i,
433
+ /gray[-_]?500/i
434
+ ]
435
+ },
436
+ {
437
+ key: "cream",
438
+ patterns: [
439
+ /^cream$/i,
440
+ /^background[-_]?light$/i,
441
+ /^bg[-_]?light$/i,
442
+ /^off[-_]?white$/i,
443
+ /^paper$/i,
444
+ /^canvas$/i
445
+ ]
446
+ },
447
+ {
448
+ key: "mauve",
449
+ patterns: [
450
+ /^mauve$/i,
451
+ /^lavender$/i,
452
+ /^purple[-_]?light$/i,
453
+ /^lilac$/i,
454
+ /^periwinkle$/i
455
+ ]
456
+ },
457
+ {
458
+ key: "surface",
459
+ patterns: [
460
+ /^surface$/i,
461
+ /^card$/i,
462
+ /^panel$/i,
463
+ /^background$/i,
464
+ /^bg$/i
465
+ ]
466
+ },
467
+ {
468
+ key: "ink",
469
+ patterns: [
470
+ /^ink$/i,
471
+ /^text[-_]?(?:default|primary|base|main)?$/i,
472
+ /^foreground$/i,
473
+ /^content$/i,
474
+ /^copy$/i
475
+ ]
476
+ }
477
+ ];
478
+ function resolveThemeKey(name) {
479
+ for (const { key, patterns } of KEY_ALIASES) {
480
+ for (const pat of patterns) {
481
+ if (pat.test(name)) return key;
482
+ }
483
+ }
484
+ return null;
485
+ }
486
+ var HEX_COLOR = /#[0-9a-fA-F]{3,8}\b/;
487
+ function extractTailwindColors(content) {
488
+ const colors = /* @__PURE__ */ new Map();
489
+ const RE = /['"]?([\w-]+)['"]?\s*:\s*['"]?(#[0-9a-fA-F]{3,8})['"]?/g;
490
+ let m;
491
+ while ((m = RE.exec(content)) !== null) {
492
+ const [, name, hex] = m;
493
+ if (HEX_COLOR.test(hex)) {
494
+ colors.set(name.toLowerCase(), hex);
495
+ }
496
+ }
497
+ return colors;
498
+ }
499
+ function extractCssCustomProps(content) {
500
+ const colors = /* @__PURE__ */ new Map();
501
+ const RE = /--([\w-]+)\s*:\s*(#[0-9a-fA-F]{3,8})\b/g;
502
+ let m;
503
+ while ((m = RE.exec(content)) !== null) {
504
+ const [, varName, hex] = m;
505
+ if (!HEX_COLOR.test(hex)) continue;
506
+ const stripped = varName.replace(/^(?:color|colour|clr|c|qs|qa)[-_]/, "").toLowerCase();
507
+ if (!colors.has(stripped)) colors.set(stripped, hex);
508
+ if (!colors.has(varName.toLowerCase())) colors.set(varName.toLowerCase(), hex);
509
+ }
510
+ return colors;
511
+ }
512
+ var CSS_SEARCH_DIRS = [
513
+ "",
514
+ // root
515
+ "src",
516
+ "styles",
517
+ "css",
518
+ "src/styles",
519
+ "src/css",
520
+ "src/app",
521
+ "app",
522
+ "assets",
523
+ "assets/css",
524
+ "assets/styles",
525
+ "public"
526
+ ];
527
+ function detectTheme(targetDir) {
528
+ const draft = {
529
+ primary: PLACEHOLDER,
530
+ primaryDark: PLACEHOLDER,
531
+ accent: PLACEHOLDER,
532
+ accentDark: PLACEHOLDER,
533
+ sage: PLACEHOLDER,
534
+ cream: PLACEHOLDER,
535
+ mauve: PLACEHOLDER,
536
+ surface: PLACEHOLDER,
537
+ ink: PLACEHOLDER
538
+ };
539
+ const allColors = /* @__PURE__ */ new Map();
540
+ const mergeColors = (extracted) => {
541
+ for (const [k, v] of extracted) {
542
+ if (!allColors.has(k)) allColors.set(k, v);
543
+ }
544
+ };
545
+ const tailwindNames = [
546
+ "tailwind.config.ts",
547
+ "tailwind.config.js",
548
+ "tailwind.config.cjs",
549
+ "tailwind.config.mjs"
550
+ ];
551
+ for (const name of tailwindNames) {
552
+ const filePath = path5.join(targetDir, name);
553
+ if (fs5.existsSync(filePath) && assertSafeToRead(filePath)) {
554
+ const content = readFileSafe(filePath);
555
+ if (content) mergeColors(extractTailwindColors(content));
556
+ break;
557
+ }
558
+ }
559
+ const CSS_EXTS = /\.(css|scss|sass|less|styl)$/i;
560
+ for (const rel of CSS_SEARCH_DIRS) {
561
+ const dirPath = rel ? path5.join(targetDir, rel) : targetDir;
562
+ if (!dirExists(dirPath)) continue;
563
+ const files = rel.includes("style") || rel.includes("css") ? walk(dirPath).filter((f) => CSS_EXTS.test(f)) : fs5.readdirSync(dirPath).filter((f) => CSS_EXTS.test(f)).map((f) => path5.join(dirPath, f));
564
+ for (const filePath of files) {
565
+ if (!assertSafeToRead(filePath)) continue;
566
+ const content = readFileSafe(filePath);
567
+ if (content) mergeColors(extractCssCustomProps(content));
568
+ }
569
+ }
570
+ for (const [name, hex] of allColors) {
571
+ const key = resolveThemeKey(name);
572
+ if (key && draft[key] === PLACEHOLDER) {
573
+ draft[key] = hex;
574
+ }
575
+ }
576
+ return draft;
577
+ }
578
+ function hasDetectedColors(draft) {
579
+ return Object.values(draft).some((v) => v !== PLACEHOLDER);
580
+ }
581
+
582
+ // src/bin/detectors/detectCredentials.ts
583
+ var path6 = __toESM(require("path"), 1);
584
+ var fs6 = __toESM(require("fs"), 1);
585
+ 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.';
586
+ var SEEDER_PATH_PATTERNS = [
587
+ /[\\/]seeders?[\\/]/i,
588
+ /[\\/]seeds?[\\/]/i,
589
+ /prisma[\\/]seed\.[jt]s/i,
590
+ /db[\\/]seeds?[\\/]/i,
591
+ /[\\/]seed\.[jt]sx?$/i,
592
+ /devseed/i,
593
+ /seed\.(?:js|ts|mjs|cjs)$/i
594
+ ];
595
+ function isSeederFile(filePath) {
596
+ const norm = filePath.replace(/\\/g, "/");
597
+ return SEEDER_PATH_PATTERNS.some((p) => p.test(norm));
598
+ }
599
+ function extractMatches(content) {
600
+ const out = [];
601
+ const lines = content.split("\n");
602
+ const FIELD_RE = /\b(email|login|username|password|phone|role)\s*[:=]\s*(?:["'`]([^"'`\r\n]+)["'`]|(process\.env\.(\w+)))/gi;
603
+ for (let i = 0; i < lines.length; i++) {
604
+ const line = lines[i];
605
+ let m;
606
+ FIELD_RE.lastIndex = 0;
607
+ while ((m = FIELD_RE.exec(line)) !== null) {
608
+ const type = m[1].toLowerCase();
609
+ let value;
610
+ if (m[3] !== void 0) {
611
+ value = `TODO: set from env ${m[4]} (use .env.example)`;
612
+ } else {
613
+ value = m[2].trim();
614
+ if (/^[<{]/.test(value) || /^(change[_-]?me|your[_-])/i.test(value)) continue;
615
+ }
616
+ out.push({ type, value, lineIdx: i, context: line });
617
+ }
618
+ }
619
+ return out;
620
+ }
621
+ var ROLE_HINTS = [
622
+ { role: "admin", patterns: [/admin/i, /superuser/i, /root/i, /operator/i] },
623
+ { role: "seller", patterns: [/seller/i, /vendor/i, /merchant/i, /store[_-]?owner/i] },
624
+ { role: "buyer", patterns: [/buyer/i, /customer/i, /client/i, /shopper/i] },
625
+ { role: "manager", patterns: [/manager/i, /supervisor/i] },
626
+ { role: "user", patterns: [/\buser\b/i, /\btest\b/i, /\bdemo\b/i] },
627
+ { role: "guest", patterns: [/guest/i, /anon/i, /public/i] }
628
+ ];
629
+ function inferRole(context, login) {
630
+ const haystack = (context + " " + login).toLowerCase();
631
+ for (const { role, patterns } of ROLE_HINTS) {
632
+ if (patterns.some((p) => p.test(haystack))) return role;
633
+ }
634
+ return "user";
635
+ }
636
+ function groupMatches(matches) {
637
+ if (matches.length === 0) return [];
638
+ const clusters = [];
639
+ let current = [matches[0]];
640
+ for (let i = 1; i < matches.length; i++) {
641
+ const m = matches[i];
642
+ const prev = current[current.length - 1];
643
+ const isIdentifier = m.type === "email" || m.type === "login" || m.type === "username";
644
+ const clusterHasIdentifier = current.some(
645
+ (x) => x.type === "email" || x.type === "login" || x.type === "username"
646
+ );
647
+ if (m.lineIdx - prev.lineIdx > 20 || isIdentifier && clusterHasIdentifier) {
648
+ clusters.push(current);
649
+ current = [m];
650
+ } else {
651
+ current.push(m);
652
+ }
653
+ }
654
+ clusters.push(current);
655
+ const creds = [];
656
+ for (const cluster of clusters) {
657
+ const emailMatch = cluster.find((m) => m.type === "email");
658
+ const loginMatch = cluster.find((m) => m.type === "login" || m.type === "username");
659
+ const passMatch = cluster.find((m) => m.type === "password");
660
+ const roleMatch = cluster.find((m) => m.type === "role");
661
+ const login = (emailMatch ?? loginMatch)?.value;
662
+ const password = passMatch?.value;
663
+ if (!login && !password) continue;
664
+ const contextStr = cluster.map((m) => m.context).join(" ");
665
+ const role = roleMatch?.value ?? inferRole(contextStr, login ?? "");
666
+ creds.push({
667
+ role,
668
+ login: login ?? "TODO: set login",
669
+ password: password ?? "TODO: set password",
670
+ seeded: true
671
+ });
672
+ }
673
+ const seen = /* @__PURE__ */ new Set();
674
+ return creds.filter((c) => {
675
+ const key = c.login + "|" + c.role;
676
+ if (seen.has(key)) return false;
677
+ seen.add(key);
678
+ return true;
679
+ });
680
+ }
681
+ var SEEDER_SEARCH_DIRS = [
682
+ "seeders",
683
+ "seeds",
684
+ "prisma",
685
+ "db",
686
+ "database",
687
+ "src/database",
688
+ "src/db",
689
+ "scripts",
690
+ "src/scripts",
691
+ "src"
692
+ ];
693
+ function detectCredentials(targetDir) {
694
+ const allMatches = [];
695
+ const envExample = path6.join(targetDir, ".env.example");
696
+ if (fs6.existsSync(envExample) && assertSafeToRead(envExample)) {
697
+ const content = readFileSafe(envExample);
698
+ if (content) allMatches.push(...extractMatches(content));
699
+ }
700
+ for (const rel of SEEDER_SEARCH_DIRS) {
701
+ const dirPath = path6.join(targetDir, rel);
702
+ if (!dirExists(dirPath)) continue;
703
+ const files = walk(dirPath).filter((f) => {
704
+ const ext = path6.extname(f);
705
+ return [".js", ".ts", ".mjs", ".cjs", ".json"].includes(ext) && isSeederFile(f) && assertSafeToRead(f);
706
+ });
707
+ for (const f of files) {
708
+ const content = readFileSafe(f);
709
+ if (content) allMatches.push(...extractMatches(content));
710
+ }
711
+ }
712
+ return groupMatches(allMatches);
713
+ }
714
+
715
+ // src/bin/generators/genConfig.ts
716
+ function singleQuote(s) {
717
+ return `'${s.replace(/'/g, "\\'")}'`;
718
+ }
719
+ function serializeTheme(theme) {
720
+ const lines = [];
721
+ const keys = Object.keys(theme);
722
+ for (const key of keys) {
723
+ const val = theme[key];
724
+ const isPlaceholder = val === PLACEHOLDER;
725
+ if (isPlaceholder) {
726
+ lines.push(` ${key}: ${singleQuote(val)}, // TODO: replace with your brand colour`);
727
+ } else {
728
+ lines.push(` ${key}: ${singleQuote(val)},`);
729
+ }
730
+ }
731
+ return ` theme: {
732
+ ${lines.join("\n")}
733
+ }`;
734
+ }
735
+ function serializeCredentials(creds) {
736
+ if (creds.length === 0) {
737
+ return ` // ${CREDENTIALS_BANNER.replace(/\n/g, "\n // ")}
738
+ credentials: [
739
+ // TODO: add dev/test credentials here (seeder/seed data)
740
+ // { role: 'buyer', login: 'buyer@test.com', password: 'test123', seeded: true },
741
+ ]`;
742
+ }
743
+ const banner = CREDENTIALS_BANNER.split("\n").map((l) => ` ${l}`).join("\n");
744
+ const rows = creds.map((c) => {
745
+ const lines = [
746
+ ` {`,
747
+ ` role: ${singleQuote(c.role)},`,
748
+ ` login: ${singleQuote(c.login)},`,
749
+ ` password: ${singleQuote(c.password)},`,
750
+ ` seeded: true,`,
751
+ ` }`
752
+ ];
753
+ return lines.join("\n");
754
+ });
755
+ return `${banner}
756
+ credentials: [
757
+ ${rows.join(",\n")},
758
+ ]`;
759
+ }
760
+ function serializeStep(step) {
761
+ const what = `{ en: ${singleQuote(step.what.en)}, ar: ${singleQuote(step.what.ar)} }`;
762
+ return [
763
+ ` {`,
764
+ ` path: ${singleQuote(step.path)},`,
765
+ ` // TODO: grade \u2014 risk 'red' (money/auth/irreversible) | 'amber' (important) | 'green' (informational)`,
766
+ ` risk: 'green',`,
767
+ ` what: ${what},`,
768
+ ` }`
769
+ ].join("\n");
770
+ }
771
+ function serializeLane(lane) {
772
+ const role = `{ en: ${singleQuote(lane.role.en)}, ar: ${singleQuote(lane.role.ar)} }`;
773
+ const steps = lane.steps.map((s) => serializeStep(s)).join(",\n");
774
+ return [
775
+ ` {`,
776
+ ` id: ${singleQuote(lane.id)},`,
777
+ ` color: ${singleQuote(lane.color ?? "#4f46e5")},`,
778
+ ` role: ${role},`,
779
+ ` steps: [`,
780
+ steps,
781
+ ` ],`,
782
+ ` }`
783
+ ].join("\n");
784
+ }
785
+ function serializeJourney(journey) {
786
+ if (journey.length === 0) {
787
+ return ` journey: [
788
+ // TODO: add journey lanes here. See qa.preamble.md for guidance.
789
+ // { id: 'buyer', color: '#4f46e5', role: { en: 'Buyer', ar: '\u0645\u0634\u062A\u0631\u064A' }, steps: [...] },
790
+ ]`;
791
+ }
792
+ const lanes = journey.map((l) => serializeLane(l)).join(",\n");
793
+ return ` journey: [
794
+ ${lanes},
795
+ ]`;
796
+ }
797
+ function serializePreamble() {
798
+ return [
799
+ ` /**`,
800
+ ` * preamble \u2014 read by your AI coding agent (Claude Code, Cursor, Windsurf, etc.)`,
801
+ ` * when it processes a qa-notes-*.zip export. Fill every TODO field.`,
802
+ ` * Alternatively, fill qa.preamble.md and copy the values here.`,
803
+ ` */`,
804
+ ` preamble: {`,
805
+ ` projectName: 'TODO: Your Project Name',`,
806
+ ` oneLiner: 'TODO: one sentence describing what this project does',`,
807
+ ` stack: 'TODO: e.g. Next.js 14, React 18, Prisma, PostgreSQL, Tailwind',`,
808
+ ` runCommands: 'TODO: e.g. npm run dev (starts on http://localhost:3000)',`,
809
+ ` conventions: 'TODO: coding conventions, naming patterns, file organisation',`,
810
+ ` invariants: 'TODO: things that must ALWAYS be true (e.g. prices \u2265 0, auth required for checkout)',`,
811
+ ` verifySteps: 'TODO: how to confirm a fix worked (e.g. reload + complete the flow)',`,
812
+ ` additionalContext: 'TODO: anything else the AI agent should know',`,
813
+ ` }`
814
+ ].join("\n");
815
+ }
816
+ function genConfigText(opts) {
817
+ const { namespace, isTypeScript, theme, journey, credentials, frameworkHints = [] } = opts;
818
+ const filename = isTypeScript ? "qa.config.ts" : "qa.config.js";
819
+ const hintsComment = frameworkHints.length > 0 ? ` *
820
+ * Auto-detected stack:
821
+ ${frameworkHints.map((h) => ` * \u2022 ${h}`).join("\n")}` : "";
822
+ const typeImport = isTypeScript ? `import type { QaConfig } from 'qapture';
823
+
824
+ ` : `// @ts-check
825
+ /** @type {import('qapture').QaConfig} */
826
+ `;
827
+ const typeAnnotation = isTypeScript ? ": QaConfig" : "";
828
+ const exportStatement = `export default config;
829
+ `;
830
+ const header = [
831
+ `/**`,
832
+ ` * qapture config \u2014 generated by \`qapture init\``,
833
+ ` *`,
834
+ ` * Fill every TODO field before mounting <Qapture config={config} />.`,
835
+ ` * Re-run \`qapture init --force\` only if you want to regenerate from scratch`,
836
+ ` * (your edits WILL be overwritten with --force).`,
837
+ ` *`,
838
+ ` * Schema reference: https://github.com/mohammed-farhood/qapture#qaconfig`,
839
+ hintsComment,
840
+ ` */`
841
+ ].filter((l) => l !== "").join("\n");
842
+ const themeBlock = serializeTheme(theme);
843
+ const credentialsBlock = serializeCredentials(credentials);
844
+ const journeyBlock = serializeJourney(journey);
845
+ const preambleBlock = serializePreamble();
846
+ const body = [
847
+ `const config${typeAnnotation} = {`,
848
+ ` namespace: ${singleQuote(namespace)},`,
849
+ ``,
850
+ themeBlock + ",",
851
+ ``,
852
+ ` brand: {`,
853
+ ` label: 'TODO: Your Project Name', // displayed in the QA panel header`,
854
+ ` },`,
855
+ ``,
856
+ ` loginField: {`,
857
+ ` en: 'TODO: e.g. Email or Username',`,
858
+ ` ar: 'TODO: e.g. \u0627\u0644\u0628\u0631\u064A\u062F \u0627\u0644\u0625\u0644\u0643\u062A\u0631\u0648\u0646\u064A',`,
859
+ ` },`,
860
+ ``,
861
+ credentialsBlock + ",",
862
+ ``,
863
+ journeyBlock + ",",
864
+ ``,
865
+ preambleBlock + ",",
866
+ `};`
867
+ ].join("\n");
868
+ const text = [header, "", typeImport + body, "", exportStatement].join("\n");
869
+ return { filename, text };
870
+ }
871
+
872
+ // src/bin/generators/genPreamble.ts
873
+ function genPreambleText(opts = {}) {
874
+ const { projectName, frameworkHints = [] } = opts;
875
+ const detectedName = projectName ? `_${projectName}_` : "_TODO: Your Project Name_";
876
+ const stackHint = frameworkHints.length > 0 ? frameworkHints.join(", ") : "TODO: e.g. Next.js 14, React 18, Prisma, PostgreSQL, Tailwind CSS";
877
+ return `# Qapture \u2014 Project Preamble
878
+
879
+ > **What is this file?**
880
+ > Fill in the sections below so your AI coding agent (Claude Code, Cursor,
881
+ > Windsurf, etc.) understands the project before it acts on a \`qa-notes-*.zip\`
882
+ > export. When you're happy with the content, copy the values into the \`preamble\`
883
+ > block of \`qa.config.ts\`.
884
+ >
885
+ > You can fill this manually OR paste it into your terminal agent and ask it to
886
+ > auto-populate from the codebase.
887
+
888
+ ---
889
+
890
+ ## Project
891
+
892
+ **Name:** ${detectedName}
893
+
894
+ **One-liner:**
895
+ TODO: one sentence \u2014 what does this project do for the user?
896
+
897
+ **Stack:**
898
+ ${stackHint}
899
+
900
+ ---
901
+
902
+ ## Run Commands
903
+
904
+ <!-- How to start the dev server, seed the database, run tests, etc. -->
905
+
906
+ \`\`\`bash
907
+ # TODO: fill in your start command(s)
908
+ # e.g.:
909
+ # npm run dev # starts on http://localhost:3000
910
+ # npm run db:seed # seed dev database
911
+ # npm run test # run unit/integration tests
912
+ \`\`\`
913
+
914
+ ---
915
+
916
+ ## Conventions
917
+
918
+ <!-- Coding conventions, naming patterns, file organisation rules. -->
919
+ <!-- Example: "All server actions live in src/actions/*.ts" -->
920
+
921
+ TODO: describe your codebase conventions, naming patterns, and any structural
922
+ rules the AI should respect when making changes (e.g. "always use Zod for
923
+ input validation", "mutations go through tRPC", "never import from ../db directly").
924
+
925
+ ---
926
+
927
+ ## Invariants
928
+
929
+ <!-- Things that MUST always be true \u2014 the AI must never violate these. -->
930
+ <!-- Example: "Cart total is always \u2265 0" or "Checkout requires authentication" -->
931
+
932
+ TODO: list your key business invariants, one per line:
933
+ - TODO: invariant 1
934
+ - TODO: invariant 2
935
+ - TODO: ...
936
+
937
+ ---
938
+
939
+ ## Verify Steps
940
+
941
+ <!-- How to confirm a fix worked \u2014 the AI follows these steps after making a change. -->
942
+
943
+ TODO: describe your verification workflow, e.g.:
944
+ 1. Run \`npm run dev\`
945
+ 2. Log in as the relevant role (see credentials in \`qa.config.ts\`)
946
+ 3. Navigate to the affected page
947
+ 4. Reproduce the original issue to confirm it's fixed
948
+ 5. Check no regressions on adjacent paths
949
+
950
+ ---
951
+
952
+ ## Additional Context
953
+
954
+ <!-- Anything else the AI agent should know: third-party integrations, known
955
+ quirks, pending migrations, feature flags, etc. -->
956
+
957
+ TODO: add any extra context here.
958
+
959
+ ---
960
+
961
+ _Generated by \`qapture init\`. See https://github.com/mohammed-farhood/qapture for docs._
962
+ `;
963
+ }
964
+
965
+ // src/bin/init.ts
966
+ var REPO_URL = "https://github.com/mohammed-farhood/qapture";
967
+ var PKG_VERSION = (() => {
968
+ try {
969
+ const candidates = [
970
+ path7.join(__dirname, "..", "..", "package.json"),
971
+ path7.join(__dirname, "..", "package.json"),
972
+ path7.join(__dirname, "package.json")
973
+ ];
974
+ for (const p of candidates) {
975
+ if (fs7.existsSync(p)) {
976
+ const pkg = JSON.parse(fs7.readFileSync(p, "utf8"));
977
+ if (pkg.version) return pkg.version;
978
+ }
979
+ }
980
+ } catch {
981
+ }
982
+ return "0.x";
983
+ })();
984
+ function readTargetPkg(targetDir) {
985
+ try {
986
+ const raw = fs7.readFileSync(path7.join(targetDir, "package.json"), "utf8");
987
+ return JSON.parse(raw);
988
+ } catch {
989
+ return {};
990
+ }
991
+ }
992
+ function hasFile(targetDir, ...names) {
993
+ return names.some((n) => fs7.existsSync(path7.join(targetDir, n)));
994
+ }
995
+ function detectFrameworkHints(targetDir, pkg) {
996
+ const hints = [];
997
+ const deps = {
998
+ ...pkg["dependencies"] ?? {},
999
+ ...pkg["devDependencies"] ?? {}
1000
+ };
1001
+ const hasDep = (name) => name in deps;
1002
+ if (hasDep("next")) hints.push("Next.js");
1003
+ if (hasDep("nuxt")) hints.push("Nuxt");
1004
+ if (hasDep("astro")) hints.push("Astro");
1005
+ if (hasDep("vite") && !hasDep("next")) hints.push("Vite");
1006
+ if (hasDep("remix")) hints.push("Remix");
1007
+ if (hasDep("gatsby")) hints.push("Gatsby");
1008
+ if (hasDep("svelte")) hints.push("Svelte");
1009
+ if (hasDep("react")) hints.push("React");
1010
+ if (hasDep("vue")) hints.push("Vue");
1011
+ if (hasDep("tailwindcss")) hints.push("Tailwind CSS");
1012
+ if (hasDep("styled-components")) hints.push("styled-components");
1013
+ if (hasDep("@prisma/client") || hasDep("prisma")) hints.push("Prisma");
1014
+ if (hasDep("drizzle-orm")) hints.push("Drizzle ORM");
1015
+ if (hasDep("mongoose")) hints.push("MongoDB/Mongoose");
1016
+ if (hasDep("typeorm")) hints.push("TypeORM");
1017
+ if (hasDep("next-auth") || hasDep("@auth/core")) hints.push("NextAuth");
1018
+ if (hasDep("lucia")) hints.push("Lucia");
1019
+ if (hasDep("clerk")) hints.push("Clerk");
1020
+ if (hasFile(targetDir, "tsconfig.json")) hints.push("TypeScript");
1021
+ return hints;
1022
+ }
1023
+ function printUsage() {
1024
+ process2.stdout.write(
1025
+ `
1026
+ qapture CLI v${PKG_VERSION}
1027
+
1028
+ Usage:
1029
+ qapture init [target-dir] [--force] Scaffold config + artifacts into target-dir
1030
+ qapture version Print version
1031
+
1032
+ Options:
1033
+ --force Overwrite qa.config.* and qa.preamble.md if they already exist
1034
+ (SKILL.md is always refreshed regardless of --force)
1035
+
1036
+ Docs: ${REPO_URL}
1037
+
1038
+ `
1039
+ );
1040
+ }
1041
+ function printVersion() {
1042
+ process2.stdout.write(`qapture ${PKG_VERSION}
1043
+ `);
1044
+ }
1045
+ var DIVIDER = "\u2500".repeat(60);
1046
+ function printSummary(targetDir, configFile, results, routeCount, credCount, colorsDetected) {
1047
+ const icon = (r) => r === "skipped" ? " (skip)" : " \u2713";
1048
+ const configLabel = configFile;
1049
+ const preambleLabel = "qa.preamble.md";
1050
+ const skillLabel = ".claude/skills/qapture/SKILL.md";
1051
+ const agentsLabel = "AGENTS.md";
1052
+ const configNote = results.config === "skipped" ? " (already exists \u2014 use --force to overwrite)" : " (review & fill TODOs)";
1053
+ const preambleNote = results.preamble === "skipped" ? " (already exists \u2014 use --force to overwrite)" : " (fill project context)";
1054
+ const agentsNote = results.agents === "created" ? " (created)" : results.agents === "replaced" ? " (section updated)" : " (section appended)";
1055
+ process2.stdout.write(
1056
+ `
1057
+ ${DIVIDER}
1058
+ qapture init \u2014 done!
1059
+ ${DIVIDER}
1060
+
1061
+ Files:
1062
+ ${icon(results.config)} ${configLabel}${configNote}
1063
+ ${icon(results.preamble)} ${preambleLabel}${preambleNote}
1064
+ \u2713 ${skillLabel} (always refreshed)
1065
+ \u2713 ${agentsLabel}${agentsNote}
1066
+
1067
+ Detected:
1068
+ \u2022 Routes/steps : ${routeCount > 0 ? routeCount : "none (fallback placeholder added)"}
1069
+ \u2022 Brand colours: ${colorsDetected ? "partial palette detected" : "none (all #REPLACE_ME)"}
1070
+ \u2022 Credentials : ${credCount > 0 ? credCount + " row(s) from .env.example/seeders" : "none (add manually)"}
1071
+
1072
+ ${DIVIDER}
1073
+ Mount the widget near your app root:
1074
+ ${DIVIDER}
1075
+
1076
+ import { Qapture } from 'qapture';
1077
+ import config from './${configFile.replace(/\.[jt]s$/, "")}';
1078
+
1079
+ // Render once near your app root:
1080
+ <Qapture config={config} />
1081
+
1082
+ ${DIVIDER}
1083
+ Next steps:
1084
+ ${DIVIDER}
1085
+
1086
+ 1. Fill in ${preambleLabel} with your project context
1087
+ (or ask your terminal agent to auto-populate it from the codebase)
1088
+ 2. Sync the preamble into the preamble block of ${configLabel}
1089
+ 3. Grade JOURNEY risk levels \u2014 change risk: 'green' to:
1090
+ 'red' \u2192 money / auth / irreversible flows
1091
+ 'amber' \u2192 important but recoverable flows
1092
+ 'green' \u2192 informational / display only (current default)
1093
+ 4. Fill remaining TODO: fields in ${configLabel}
1094
+ 5. Run your app and open Qapture (shortcut: Shift+Alt+Q)
1095
+
1096
+ ${DIVIDER}
1097
+ IDE advisory:
1098
+ ${DIVIDER}
1099
+
1100
+ Cursor \u2192 copy the qapture block from AGENTS.md
1101
+ into .cursor/rules/qapture.md
1102
+
1103
+ Windsurf \u2192 append the qapture block from AGENTS.md
1104
+ to .windsurf/rules.md
1105
+
1106
+ Docs: ${REPO_URL}
1107
+ ${DIVIDER}
1108
+
1109
+ `
1110
+ );
1111
+ }
1112
+ function main(argv2) {
1113
+ const args = parseArgs(argv2);
1114
+ if (args.command === "version") {
1115
+ printVersion();
1116
+ process2.exit(0);
1117
+ }
1118
+ if (args.command === "help") {
1119
+ printUsage();
1120
+ process2.exit(0);
1121
+ }
1122
+ const targetDir = path7.resolve(args.dir);
1123
+ const { force } = args;
1124
+ if (!fs7.existsSync(targetDir)) {
1125
+ process2.stderr.write(`
1126
+ Error: target directory does not exist: ${targetDir}
1127
+
1128
+ `);
1129
+ process2.exit(1);
1130
+ }
1131
+ if (!fs7.statSync(targetDir).isDirectory()) {
1132
+ process2.stderr.write(`
1133
+ Error: ${targetDir} is not a directory
1134
+
1135
+ `);
1136
+ process2.exit(1);
1137
+ }
1138
+ process2.stdout.write(`
1139
+ qapture init \u2014 scanning ${targetDir} ...
1140
+ `);
1141
+ const pkg = readTargetPkg(targetDir);
1142
+ const frameworkHints = detectFrameworkHints(targetDir, pkg);
1143
+ const isTypeScript = hasFile(targetDir, "tsconfig.json");
1144
+ process2.stdout.write(` Detecting routes ...
1145
+ `);
1146
+ const journey = detectRoutes(targetDir);
1147
+ const routeCount = journey.reduce((n, lane) => n + lane.steps.length, 0);
1148
+ process2.stdout.write(` Detecting theme ...
1149
+ `);
1150
+ const theme = detectTheme(targetDir);
1151
+ const colorsDetected = hasDetectedColors(theme);
1152
+ process2.stdout.write(` Detecting credentials (safe sources only) ...
1153
+ `);
1154
+ const credentials = detectCredentials(targetDir);
1155
+ const namespace = typeof pkg["name"] === "string" && pkg["name"].trim() ? pkg["name"].trim().replace(/^@[^/]+\//, "") : "qapture";
1156
+ const projectName = typeof pkg["name"] === "string" && pkg["name"].trim() ? pkg["name"].trim() : void 0;
1157
+ process2.stdout.write(` Generating files ...
1158
+ `);
1159
+ const { filename: configFilename, text: configText } = genConfigText({
1160
+ namespace,
1161
+ isTypeScript,
1162
+ theme,
1163
+ journey,
1164
+ credentials,
1165
+ frameworkHints
1166
+ });
1167
+ const preambleText = genPreambleText({ projectName, frameworkHints });
1168
+ const configPath = path7.join(targetDir, configFilename);
1169
+ const preamblePath = path7.join(targetDir, "qa.preamble.md");
1170
+ const skillPath = path7.join(targetDir, ".claude", "skills", "qapture", "SKILL.md");
1171
+ const agentsMdPath = path7.join(targetDir, "AGENTS.md");
1172
+ const configResult = writeIfAbsent(configPath, configText, force);
1173
+ const preambleResult = writeIfAbsent(preamblePath, preambleText, force);
1174
+ writeAlways(skillPath, SKILL_default);
1175
+ const skillResult = "written";
1176
+ const agentsResult = mergeAgentsMd(agentsMdPath, AGENTS_SECTION_default);
1177
+ printSummary(
1178
+ targetDir,
1179
+ configFilename,
1180
+ { config: configResult, preamble: preambleResult, skill: skillResult, agents: agentsResult },
1181
+ routeCount,
1182
+ credentials.length,
1183
+ colorsDetected
1184
+ );
1185
+ }
1186
+ main(process2.argv.slice(2));