@viberaven/cli 1.5.0 → 1.5.2

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/dist/cli.js CHANGED
@@ -9474,7 +9474,52 @@ function buildDefinerFunctionCheck(replay) {
9474
9474
  findings
9475
9475
  };
9476
9476
  }
9477
- function buildPoolerCheck(files) {
9477
+ function directPostgresClients(files) {
9478
+ const found = /* @__PURE__ */ new Set();
9479
+ for (const file of files) {
9480
+ const path = file.path.replace(/\\/g, "/");
9481
+ if (PRISMA_SCHEMA.test(path)) {
9482
+ if (/provider\s*=\s*["'](?:postgresql|postgres)["']/i.test(file.content)) found.add(`prisma (${path})`);
9483
+ } else if (/(^|\/)package\.json$/.test(path)) {
9484
+ try {
9485
+ const pkg = JSON.parse(file.content);
9486
+ for (const key of ["dependencies", "devDependencies", "optionalDependencies"]) {
9487
+ const group = pkg[key];
9488
+ if (!group || typeof group !== "object") continue;
9489
+ for (const name of Object.keys(group)) if (DIRECT_POSTGRES_PACKAGES.has(name)) found.add(name);
9490
+ }
9491
+ } catch {
9492
+ }
9493
+ } else if (SOURCE_CODE_EXT.test(path)) {
9494
+ const match = file.content.match(DIRECT_POSTGRES_IMPORT);
9495
+ if (match) found.add(match[0].replace(/^(?:from\s+|import\s*\(\s*|require\s*\(\s*)/, "").replace(/['"]/g, ""));
9496
+ }
9497
+ }
9498
+ return [...found].sort();
9499
+ }
9500
+ function buildPoolerCheck(files, allFiles = files) {
9501
+ const clients = directPostgresClients(allFiles);
9502
+ if (clients.length === 0) {
9503
+ return {
9504
+ id: "vercel-supabase-pooler-port",
9505
+ title: "Vercel/Supabase pooler port",
9506
+ status: "pass",
9507
+ summary: "Not applicable. No direct Postgres driver or ORM was found in package.json or code, so the app reaches Supabase through supabase-js over HTTPS and has no database connection for the pooler to protect.",
9508
+ evidence: []
9509
+ };
9510
+ }
9511
+ const connectionEvidence = allFiles.filter((file) => isEnvFile(file.path.split(/[\\/]/).pop() ?? "") || SOURCE_CODE_EXT.test(file.path) || PRISMA_SCHEMA.test(file.path)).flatMap((file) => lineEvidence(file, CONNECTION_STRING));
9512
+ if (connectionEvidence.length === 0) {
9513
+ const reason = `a direct Postgres client is installed (${clients.join(", ")}) but no DATABASE_URL/POSTGRES_URL, env("...") or postgres:// connection string was found in env files, code or Prisma schemas`;
9514
+ return {
9515
+ id: "vercel-supabase-pooler-port",
9516
+ title: "Vercel/Supabase pooler port",
9517
+ status: "needs_work",
9518
+ summary: `Not checked: ${reason}, so the pooler port could not be checked.`,
9519
+ evidence: [],
9520
+ incomplete: [reason]
9521
+ };
9522
+ }
9478
9523
  const dbUrlPattern = /\b(?:DATABASE_URL|POSTGRES_URL)\b/i;
9479
9524
  const direct5432Evidence = files.flatMap(
9480
9525
  (file) => lineEvidence(file, /\b(?:DATABASE_URL|POSTGRES_URL)\b[^\n\r]*:5432\b/i)
@@ -9546,7 +9591,7 @@ function buildVercelSupabaseAudit(input) {
9546
9591
  const checks = [
9547
9592
  buildRlsCheck(replay),
9548
9593
  buildPermissivePolicyCheck(replay),
9549
- buildPoolerCheck(input.files),
9594
+ buildPoolerCheck(input.files, allFiles),
9550
9595
  buildServiceRoleCheck(allFiles, incomplete),
9551
9596
  buildDataApiGrantCheck(replay, allFiles, incomplete),
9552
9597
  buildDefinerFunctionCheck(replay)
@@ -9686,6 +9731,7 @@ async function collectSourceFiles(projectRoot, alreadyCollected) {
9686
9731
  else if (name === "package.json" || /^vite\.config\.[cm]?[jt]s$/.test(name)) candidates.push({ absolutePath, relativePath, rank: 0 });
9687
9732
  else if (name === "index.html" && !inTestDir) candidates.push({ absolutePath, relativePath, rank: 0 });
9688
9733
  else if (/(^|\/)supabase\/config\.toml$/.test(relativePath)) candidates.push({ absolutePath, relativePath, rank: 0 });
9734
+ else if (/\.prisma$/i.test(name) && !inTestDir) candidates.push({ absolutePath, relativePath, rank: 0 });
9689
9735
  else if (/\.sql$/i.test(name)) {
9690
9736
  if (!inTestDir) candidates.push({ absolutePath, relativePath, rank: 1 });
9691
9737
  } else if (SOURCE_CODE_EXT.test(name) && !inTestDir) candidates.push({ absolutePath, relativePath, rank: 2, filter: SOURCE_CODE_SIGNAL });
@@ -9776,7 +9822,7 @@ function renderVercelSupabaseAudit(result) {
9776
9822
  }
9777
9823
  return lines.join("\n").trimEnd();
9778
9824
  }
9779
- var import_promises3, import_node_path4, ENV_FILES, SQL_ROOTS, SQL_TEST_DIRS, SKIP_DIRS, SENSITIVE_ENV_KEY, SOURCE_SKIP_DIRS, SOURCE_TEST_DIRS, SOURCE_CODE_EXT, SOURCE_CODE_SIGNAL, SOURCE_LIMITS, CODE_DIR_FIRST, BULK_DIR_LAST;
9825
+ var import_promises3, import_node_path4, ENV_FILES, SQL_ROOTS, SQL_TEST_DIRS, SKIP_DIRS, SENSITIVE_ENV_KEY, DIRECT_POSTGRES_PACKAGES, DIRECT_POSTGRES_IMPORT, CONNECTION_STRING, PRISMA_SCHEMA, SOURCE_SKIP_DIRS, SOURCE_TEST_DIRS, SOURCE_CODE_EXT, SOURCE_CODE_SIGNAL, SOURCE_LIMITS, CODE_DIR_FIRST, BULK_DIR_LAST;
9780
9826
  var init_audit = __esm({
9781
9827
  "src/commands/audit.ts"() {
9782
9828
  "use strict";
@@ -9798,6 +9844,27 @@ var init_audit = __esm({
9798
9844
  SQL_TEST_DIRS = /* @__PURE__ */ new Set(["test", "tests", "__tests__", "spec"]);
9799
9845
  SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", ".next", "dist", "build", "coverage"]);
9800
9846
  SENSITIVE_ENV_KEY = /\b[A-Z0-9_]*(?:SECRET|TOKEN|KEY|PASSWORD|SERVICE_ROLE|DATABASE_URL|POSTGRES_URL)[A-Z0-9_]*\b/i;
9847
+ DIRECT_POSTGRES_PACKAGES = /* @__PURE__ */ new Set([
9848
+ "pg",
9849
+ "postgres",
9850
+ "pg-promise",
9851
+ "slonik",
9852
+ "@neondatabase/serverless",
9853
+ "@vercel/postgres",
9854
+ "prisma",
9855
+ "@prisma/client",
9856
+ "drizzle-orm",
9857
+ "kysely",
9858
+ "typeorm",
9859
+ "knex",
9860
+ "sequelize",
9861
+ "@mikro-orm/postgresql",
9862
+ "@databases/pg",
9863
+ "massive"
9864
+ ]);
9865
+ DIRECT_POSTGRES_IMPORT = /(?:from\s+|import\s*\(\s*|require\s*\(\s*)['"](?:npm:)?(?:pg|postgres|pg-promise|slonik|@neondatabase\/serverless|@vercel\/postgres|@prisma\/client|drizzle-orm(?:\/[\w-]+)?|kysely|typeorm|knex|sequelize)(?:@[^'"/]*)?(?:\/[^'"]*)?['"]|['"]jsr:@db\/postgres|deno\.land\/x\/postgres(?:js)?\b/;
9866
+ CONNECTION_STRING = /\b[A-Z0-9_]*(?:DATABASE_URL|POSTGRES_URL|POSTGRES_PRISMA_URL|DB_URL|DIRECT_URL)\b|\bpostgres(?:ql)?:\/\//;
9867
+ PRISMA_SCHEMA = /\.prisma$/i;
9801
9868
  SOURCE_SKIP_DIRS = /* @__PURE__ */ new Set([
9802
9869
  ...SKIP_DIRS,
9803
9870
  ".viberaven",
@@ -9817,7 +9884,7 @@ var init_audit = __esm({
9817
9884
  ]);
9818
9885
  SOURCE_TEST_DIRS = /* @__PURE__ */ new Set(["test", "tests", "__tests__", "__mocks__", "e2e", "cypress", "playwright", "fixtures"]);
9819
9886
  SOURCE_CODE_EXT = /\.(?:[cm]?[jt]sx?|vue|svelte|astro|html?)$/i;
9820
- SOURCE_CODE_SIGNAL = /supabase|service_?role|sb_secret_|eyJhbGci/i;
9887
+ SOURCE_CODE_SIGNAL = /supabase|service_?role|sb_secret_|eyJhbGci|DATABASE_URL|POSTGRES_URL|postgres(?:ql)?:\/\//i;
9821
9888
  SOURCE_LIMITS = {
9822
9889
  /** Directory entries listed, across the whole walk. */
9823
9890
  entries: 2e5,
@@ -10401,24 +10468,153 @@ function detectArchetype(signals, stack) {
10401
10468
  if (stack.length === 0) return "unknown";
10402
10469
  return "Web app";
10403
10470
  }
10404
- function collectEnvVarDrift(scan) {
10405
- const example = scan.files.find(
10406
- (file) => /(^|\/)\.env\.example$/.test(file.path) && typeof file.content === "string"
10407
- );
10408
- if (!example || typeof example.content !== "string") return [];
10409
- const declared = new Set(
10410
- example.content.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#") && line.includes("=")).map((line) => line.split("=")[0].trim())
10411
- );
10412
- const used = /* @__PURE__ */ new Set();
10413
- const pattern = /process\.env\.([A-Z][A-Z0-9_]{2,})/g;
10471
+ function isEnvTemplatePath(path) {
10472
+ return ENV_TEMPLATE_NAME.test(path.replace(/\\/g, "/").split("/").pop() ?? "");
10473
+ }
10474
+ function envTemplateNames(content) {
10475
+ const names = [];
10476
+ for (const raw of content.split(/\r?\n/)) {
10477
+ const match = raw.match(/^\s*(?:#\s*)?(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/);
10478
+ if (match) names.push(match[1]);
10479
+ }
10480
+ return names;
10481
+ }
10482
+ function analyzeEnvContract(scan, extra = {}) {
10483
+ const templates = /* @__PURE__ */ new Map();
10414
10484
  for (const file of scan.files) {
10415
- if (!/\.(ts|tsx|js|jsx|mjs|cjs)$/.test(file.path) || typeof file.content !== "string") continue;
10416
- let match;
10417
- while ((match = pattern.exec(file.content)) !== null) {
10418
- used.add(match[1]);
10485
+ if (typeof file.content === "string" && isEnvTemplatePath(file.path)) templates.set(file.path.replace(/\\/g, "/"), file.content);
10486
+ }
10487
+ for (const file of extra.envTemplates ?? []) {
10488
+ if (isEnvTemplatePath(file.path)) templates.set(file.path.replace(/\\/g, "/"), file.content);
10489
+ }
10490
+ const declared = new Set([...templates.values()].flatMap(envTemplateNames));
10491
+ const used = /* @__PURE__ */ new Set();
10492
+ for (const file of codeFilesFor(scan, extra.codeFiles)) {
10493
+ if (!ENV_CODE_FILE.test(file.path)) continue;
10494
+ const edgeFunction = /(^|\/)supabase\/functions\//.test(file.path);
10495
+ const add = (name) => {
10496
+ if (!(edgeFunction && SUPABASE_EDGE_ENV.has(name))) used.add(name);
10497
+ };
10498
+ const code = stripComments2(file.content);
10499
+ for (const pattern of ENV_REFERENCES) {
10500
+ for (const match of code.matchAll(pattern)) add(match[1]);
10501
+ }
10502
+ for (const match of code.matchAll(SVELTEKIT_STATIC_IMPORT)) {
10503
+ for (const part of match[1].split(",")) {
10504
+ const name = part.trim().split(/\s+as\s+/)[0].trim();
10505
+ if (new RegExp(`^${ENV_NAME}$`).test(name)) add(name);
10506
+ }
10507
+ }
10508
+ for (const match of code.matchAll(SVELTEKIT_DYNAMIC_IMPORT)) {
10509
+ for (const part of match[1].split(",")) {
10510
+ const [imported, local] = part.trim().split(/\s+as\s+/).map((x2) => x2.trim());
10511
+ if (imported !== "env") continue;
10512
+ const alias = (local ?? imported).replace(/\$/g, "\\$");
10513
+ const reads = new RegExp(`\\b${alias}\\.(${ENV_NAME})\\b|\\b${alias}\\[\\s*['"\`](${ENV_NAME})['"\`]\\s*\\]`, "g");
10514
+ for (const read of code.matchAll(reads)) add(read[1] ?? read[2]);
10515
+ }
10516
+ }
10517
+ for (const match of code.matchAll(ENV_DESTRUCTURE)) {
10518
+ for (const part of match[1].split(",")) {
10519
+ const name = part.trim().split(/[:=\s]/)[0];
10520
+ if (new RegExp(`^${ENV_NAME}$`).test(name)) add(name);
10521
+ }
10522
+ }
10523
+ }
10524
+ const missing = [...used].filter((name) => !declared.has(name) && !ENV_DRIFT_IGNORE.has(name) && !ENV_DRIFT_IGNORE_PREFIX.test(name)).sort();
10525
+ return { templates: [...templates.keys()].sort(), declared: [...declared].sort(), used: [...used].sort(), missing };
10526
+ }
10527
+ function stripComments2(source) {
10528
+ return source.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:'"\\])\/\/.*$/gm, "$1");
10529
+ }
10530
+ function exportedNames(code) {
10531
+ const names = [];
10532
+ const localVerifiers = /* @__PURE__ */ new Set();
10533
+ const directVerifier = (body) => /^\s*return\s+(?:await\s+)?[\w$.]+\.(?:webhooks\.constructEvent(?:Async)?|signature\.verifyHeader(?:Async)?)\s*\([^;{}]+\)\s*;?\s*$/.test(body) && !STRIPE_VERIFY_JSON_BODY.test(body);
10534
+ for (const fn of code.matchAll(/\b(export\s+)?(default\s+)?(?:async\s+)?function\s+([\w$]+)\s*\([^)]*\)\s*(?::[^{}]+)?\{([^{}]*)\}/g)) {
10535
+ if (!directVerifier(fn[4])) continue;
10536
+ localVerifiers.add(fn[3]);
10537
+ if (fn[1]) names.push(fn[2] ? "default" : fn[3]);
10538
+ }
10539
+ for (const list of code.matchAll(/export\s*\{([^}]+)\}/g)) {
10540
+ for (const entry of list[1].split(",")) {
10541
+ const name = entry.trim().match(/^([\w$]+)(?:\s+as\s+([\w$]+))?$/);
10542
+ if (name && localVerifiers.has(name[1])) names.push(name[2] ?? name[1]);
10543
+ }
10544
+ }
10545
+ for (const object of code.matchAll(/export\s+(?:const|let|var)\s+([\w$]+)\s*=\s*\{\s*(?:async\s+)?([\w$]+)\s*\([^)]*\)\s*\{([^{}]*)\}\s*,?\s*\}/g)) {
10546
+ if (directVerifier(object[3])) names.push(`${object[1]}.${object[2]}`);
10547
+ }
10548
+ return [...new Set(names)];
10549
+ }
10550
+ function assessStripeWebhooks(files) {
10551
+ const facts = [];
10552
+ for (const file of files) {
10553
+ const path = file.path.replace(/\\/g, "/");
10554
+ if (!CODE_FILE.test(path) || TEST_FILE.test(path)) continue;
10555
+ const code = stripComments2(file.content);
10556
+ const verify = STRIPE_VERIFY_CALL.test(code);
10557
+ const header = STRIPE_SIGNATURE_HEADER.test(code);
10558
+ const requestHandler = REQUEST_HANDLER.test(code);
10559
+ const handler = verify || requestHandler && (header || STRIPE_EVENT_TYPE.test(code) || /webhook/i.test(path) && /stripe/i.test(code));
10560
+ if (!handler) continue;
10561
+ facts.push({ path, code, verify, header, raw: RAW_BODY.test(code), jsonBody: STRIPE_VERIFY_JSON_BODY.test(code), names: exportedNames(code) });
10562
+ }
10563
+ const helpers = facts.filter((f) => f.verify && !f.jsonBody && f.names.length > 0);
10564
+ const paired = /* @__PURE__ */ new Set();
10565
+ for (const route of facts.filter((f) => !f.verify && f.header && f.raw)) {
10566
+ for (const helper of helpers) {
10567
+ const names = [];
10568
+ const withoutExtension = (path) => path.replace(/\.[cm]?[jt]sx?$/, "").replace(/\/index$/, "");
10569
+ for (const imported of route.code.matchAll(/import\s+([^;'"\n]+?)\s+from\s*['"]([^'"]+)['"]/g)) {
10570
+ const specifier = imported[2];
10571
+ const modulePath = specifier.startsWith(".") ? import_node_path7.posix.join(import_node_path7.posix.dirname(route.path), specifier) : specifier.replace(/^@\//, "");
10572
+ if (withoutExtension(modulePath) !== withoutExtension(helper.path)) continue;
10573
+ const binding = imported[1].trim();
10574
+ const defaultName = binding.match(/^([\w$]+)(?:\s*,|$)/)?.[1];
10575
+ if (defaultName && helper.names.includes("default")) names.push(defaultName);
10576
+ const named = binding.match(/\{([^}]+)\}/)?.[1];
10577
+ for (const entry of named?.split(",") ?? []) {
10578
+ const pair = entry.trim().match(/^([\w$]+)(?:\s+as\s+([\w$]+))?$/);
10579
+ if (!pair) continue;
10580
+ for (const name of helper.names) {
10581
+ if (name === pair[1] || name.startsWith(`${pair[1]}.`)) names.push((pair[2] ?? pair[1]) + name.slice(pair[1].length));
10582
+ }
10583
+ }
10584
+ }
10585
+ if (names.some((name) => new RegExp(`\\b${name.replace(/[.$]/g, "\\$&")}\\s*\\(`).test(route.code))) {
10586
+ paired.add(route.path);
10587
+ paired.add(helper.path);
10588
+ }
10589
+ }
10590
+ }
10591
+ const verified = [];
10592
+ const unverified = [];
10593
+ for (const f of facts) {
10594
+ const complete = f.verify && f.header && f.raw && !f.jsonBody;
10595
+ if (complete || paired.has(f.path)) {
10596
+ verified.push(f.path);
10597
+ continue;
10419
10598
  }
10599
+ const missing = [];
10600
+ if (!f.verify) missing.push(f.header && f.raw ? "no signature check (constructEvent), and no call to a helper that runs it" : "no signature check (constructEvent)");
10601
+ if (!f.header) missing.push("no stripe-signature header");
10602
+ if (f.jsonBody) missing.push("body parsed as JSON before verification");
10603
+ else if (!f.raw) missing.push("no raw body");
10604
+ unverified.push({ path: f.path, missing });
10420
10605
  }
10421
- return [...used].filter((name) => !declared.has(name) && !ENV_DRIFT_IGNORE.has(name) && !name.startsWith("npm_") && !name.startsWith("VERCEL_")).sort();
10606
+ return { verified: verified.sort(), unverified: unverified.sort((a, b3) => a.path < b3.path ? -1 : 1) };
10607
+ }
10608
+ function codeFilesFor(scan, fullCopies = []) {
10609
+ const full = new Map(fullCopies.map((f) => [f.path.replace(/\\/g, "/"), f.content]));
10610
+ const merged = /* @__PURE__ */ new Map();
10611
+ for (const file of scan.files) {
10612
+ if (typeof file.content !== "string") continue;
10613
+ const path = file.path.replace(/\\/g, "/");
10614
+ merged.set(path, full.get(path) ?? file.content);
10615
+ }
10616
+ for (const [path, content] of full) if (!merged.has(path)) merged.set(path, content);
10617
+ return [...merged].map(([path, content]) => ({ path, content }));
10422
10618
  }
10423
10619
  function findUnboundedQueryFiles(scan) {
10424
10620
  const flagged = [];
@@ -10525,7 +10721,7 @@ function scanIncompleteSeed(audit) {
10525
10721
  primaryMapCategory: "security"
10526
10722
  };
10527
10723
  }
10528
- function buildGapSeeds(scan, audit, databaseEvidence) {
10724
+ function buildGapSeeds(scan, audit, databaseEvidence, extra = {}) {
10529
10725
  const signals = scan.stackSignals;
10530
10726
  const usesDatabase = detectDatabaseUsage(scan, databaseEvidence);
10531
10727
  const seeds = [];
@@ -10572,6 +10768,16 @@ ${check.evidence.map((e2) => `- ${e2}`).join("\n")}` : "";
10572
10768
  copyPrompt: "Audit every use of SUPABASE_SERVICE_ROLE_KEY in this repo. It must only be read in server-only modules (route handlers, server actions, edge functions) \u2014 never in client components or files bundled for the browser. Move violations behind a server boundary.",
10573
10769
  primaryMapCategory: "security"
10574
10770
  });
10771
+ } else if (check.id === "vercel-supabase-pooler-port" && (check.incomplete ?? []).length > 0) {
10772
+ seeds.push({
10773
+ id: "pooler_port_unchecked",
10774
+ category: "DATABASE & DATA",
10775
+ severity: "info",
10776
+ title: "Supabase pooler port could not be checked",
10777
+ detail: `Not checked: ${(check.incomplete ?? []).join("; ")}.`,
10778
+ copyPrompt: "Find where the app builds its Postgres connection string (env files, Prisma schema, the ORM config) and make sure serverless deployments use Supabase's transaction pooler (port 6543) rather than the direct 5432 connection.",
10779
+ primaryMapCategory: "database"
10780
+ });
10575
10781
  } else if (check.id === "vercel-supabase-pooler-port") {
10576
10782
  seeds.push({
10577
10783
  id: "pooler_port_mismatch",
@@ -10685,8 +10891,19 @@ ${check.evidence.map((e2) => `- ${e2}`).join("\n")}` : "";
10685
10891
  });
10686
10892
  }
10687
10893
  if (signals.hasStripe) {
10688
- const webhookRoute = hasFilePath(scan, /webhook/i) || fileContentMatches(scan, /\.(ts|js|tsx)$/, /stripe.*webhook|constructEvent/i);
10689
- if (!webhookRoute) {
10894
+ const webhooks = assessStripeWebhooks(codeFilesFor(scan, extra.codeFiles));
10895
+ if (webhooks.unverified.length > 0) {
10896
+ const lines = webhooks.unverified.map((u2) => `${u2.path} (${u2.missing.join(", ")})`);
10897
+ seeds.push({
10898
+ id: "unverified_stripe_webhook",
10899
+ category: "SECURITY & AUTH",
10900
+ severity: "warning",
10901
+ title: "Stripe webhook does not verify the signature",
10902
+ detail: `${webhooks.unverified.length} Stripe webhook handler${webhooks.unverified.length === 1 ? "" : "s"} without signature verification over the raw body: ${capLines(lines).join("; ")}. Anyone who finds the URL can post a fake event, for example a paid checkout.`,
10903
+ copyPrompt: 'In each listed handler, read the raw request body (await req.text() in Next.js route handlers and Deno, express.raw({ type: "application/json" }) in Express), read the stripe-signature header, and pass both to stripe.webhooks.constructEvent (constructEventAsync on Deno or edge runtimes) with STRIPE_WEBHOOK_SECRET before trusting event.type. Return 400 when verification throws.',
10904
+ primaryMapCategory: "payments"
10905
+ });
10906
+ } else if (webhooks.verified.length === 0) {
10690
10907
  seeds.push({
10691
10908
  id: "missing_stripe_webhook",
10692
10909
  category: "MISSING FEATURES",
@@ -10698,19 +10915,35 @@ ${check.evidence.map((e2) => `- ${e2}`).join("\n")}` : "";
10698
10915
  });
10699
10916
  }
10700
10917
  }
10701
- if (signals.hasEnvExample) {
10702
- const drifted = collectEnvVarDrift(scan);
10918
+ {
10919
+ const contract = analyzeEnvContract(scan, extra);
10920
+ const drifted = contract.missing;
10703
10921
  if (drifted.length > 0) {
10704
10922
  const shown = drifted.slice(0, 5).join(", ");
10705
- seeds.push({
10706
- id: "env_var_drift",
10707
- category: "DEPLOYMENT",
10708
- severity: "warning",
10709
- title: "Env vars used in code but missing from .env.example",
10710
- detail: `Code reads ${drifted.length} environment variable${drifted.length === 1 ? "" : "s"} that .env.example does not document (${shown}${drifted.length > 5 ? ", \u2026" : ""}). Fresh clones, CI, and the production deploy can silently run without them.`,
10711
- copyPrompt: `Add placeholder entries for these variables to .env.example (never real values): ${shown}${drifted.length > 5 ? " and the rest" : ""}. Then confirm each one is set in the deployment environment (Vercel project settings).`,
10712
- primaryMapCategory: "deployment"
10713
- });
10923
+ const more = drifted.length > 5 ? ", \u2026" : "";
10924
+ const count = `${drifted.length} environment variable${drifted.length === 1 ? "" : "s"}`;
10925
+ if (contract.templates.length > 0) {
10926
+ const target = contract.templates.length === 1 ? contract.templates[0] : "the env templates";
10927
+ seeds.push({
10928
+ id: "env_var_drift",
10929
+ category: "DEPLOYMENT",
10930
+ severity: "warning",
10931
+ title: `Env vars used in code but missing from ${target}`,
10932
+ detail: `Code reads ${count} that ${contract.templates.length === 1 ? target : `no env template (${contract.templates.join(", ")})`} does not document (${shown}${more}). Fresh clones, CI, and the production deploy can silently run without them.`,
10933
+ copyPrompt: `Add placeholder entries for these variables to ${contract.templates.length === 1 ? target : contract.templates[0]} (never real values): ${shown}${drifted.length > 5 ? " and the rest" : ""}. Then confirm each one is set in the deployment environment (Vercel project settings).`,
10934
+ primaryMapCategory: "deployment"
10935
+ });
10936
+ } else {
10937
+ seeds.push({
10938
+ id: "env_var_drift",
10939
+ category: "DEPLOYMENT",
10940
+ severity: "info",
10941
+ title: "Env vars used in code but no env template documents them",
10942
+ detail: `Code reads ${count} (${shown}${more}) and the repo has no .env.example, .env.sample or similar template, so a fresh clone or CI has no list of what to set.`,
10943
+ copyPrompt: `Add a .env.example with placeholder entries (never real values) for: ${shown}${drifted.length > 5 ? " and the rest" : ""}. Commit it, and confirm each one is set in the deployment environment.`,
10944
+ primaryMapCategory: "deployment"
10945
+ });
10946
+ }
10714
10947
  }
10715
10948
  }
10716
10949
  if (hasDatabase) {
@@ -10799,15 +11032,52 @@ function checklistFrom(gaps) {
10799
11032
  }
10800
11033
  return base;
10801
11034
  }
11035
+ async function readPaymentCodeFiles(workspacePath, scan) {
11036
+ if (!scan.stackSignals.hasStripe) return [];
11037
+ const candidates = /* @__PURE__ */ new Set();
11038
+ for (const path of scan.fileTree.split("\n")) {
11039
+ if (CODE_FILE.test(path) && PAYMENT_PATH.test(path)) candidates.add(path);
11040
+ }
11041
+ for (const file of scan.files) {
11042
+ if (CODE_FILE.test(file.path) && typeof file.content === "string" && /stripe|constructEvent/i.test(file.content)) candidates.add(file.path);
11043
+ }
11044
+ const files = [];
11045
+ for (const path of [...candidates].slice(0, FULL_READ_LIMITS.files)) {
11046
+ try {
11047
+ const absolute = (0, import_node_path7.join)(workspacePath, path);
11048
+ if ((await (0, import_promises6.stat)(absolute)).size > FULL_READ_LIMITS.bytes) continue;
11049
+ files.push({ path, content: await (0, import_promises6.readFile)(absolute, "utf8") });
11050
+ } catch {
11051
+ }
11052
+ }
11053
+ return files;
11054
+ }
10802
11055
  async function buildLocalStationOutput(input) {
10803
11056
  const { scan, workspacePath } = input;
10804
11057
  const auditInput = await collectVercelSupabaseAuditInput(workspacePath);
10805
11058
  const audit = buildVercelSupabaseAudit(auditInput);
10806
11059
  const evidence = await collectDatabaseEvidence(workspacePath, scan);
10807
- const seeds = buildGapSeeds(scan, audit, {
10808
- files: [...auditInput.files, ...evidence.files],
10809
- incomplete: evidence.incomplete
10810
- });
11060
+ const auditFiles = [...auditInput.files, ...auditInput.sourceFiles ?? []];
11061
+ const envTemplates = auditFiles.filter((file) => isEnvTemplatePath(file.path));
11062
+ for (const name of ["example.env", "sample.env", "template.env", "env.example", "env.sample", "env.template"]) {
11063
+ try {
11064
+ envTemplates.push({ path: name, content: await (0, import_promises6.readFile)((0, import_node_path7.join)(workspacePath, name), "utf8") });
11065
+ } catch {
11066
+ }
11067
+ }
11068
+ const codeFiles = [
11069
+ ...await readPaymentCodeFiles(workspacePath, scan),
11070
+ ...auditFiles.filter((file) => ENV_CODE_FILE.test(file.path))
11071
+ ];
11072
+ const seeds = buildGapSeeds(
11073
+ scan,
11074
+ audit,
11075
+ {
11076
+ files: [...auditInput.files, ...evidence.files],
11077
+ incomplete: evidence.incomplete
11078
+ },
11079
+ { codeFiles, envTemplates }
11080
+ );
10811
11081
  const releaseTagSeed = await detectReleaseTagGap(workspacePath);
10812
11082
  if (releaseTagSeed) {
10813
11083
  seeds.push(releaseTagSeed);
@@ -10842,13 +11112,15 @@ async function buildLocalStationOutput(input) {
10842
11112
  productionChecklist: checklistFrom(seeds)
10843
11113
  };
10844
11114
  }
10845
- var import_node_child_process3, import_node_util3, execFileAsync3, ENV_DRIFT_IGNORE, AUDIT_FINDING_GAPS, FINDING_SEVERITY_RANK, DETAIL_MAX_FINDINGS, SEVERITY_ORDER;
11115
+ var import_node_child_process3, import_node_util3, import_promises6, import_node_path7, execFileAsync3, ENV_DRIFT_IGNORE, ENV_DRIFT_IGNORE_PREFIX, SUPABASE_EDGE_ENV, ENV_TEMPLATE_NAME, ENV_CODE_FILE, ENV_NAME, ENV_REFERENCES, SVELTEKIT_STATIC_IMPORT, SVELTEKIT_DYNAMIC_IMPORT, ENV_DESTRUCTURE, CODE_FILE, STRIPE_VERIFY_CALL, STRIPE_VERIFY_JSON_BODY, STRIPE_SIGNATURE_HEADER, RAW_BODY, STRIPE_EVENT_TYPE, REQUEST_HANDLER, TEST_FILE, AUDIT_FINDING_GAPS, FINDING_SEVERITY_RANK, DETAIL_MAX_FINDINGS, SEVERITY_ORDER, FULL_READ_LIMITS, PAYMENT_PATH;
10846
11116
  var init_localStation = __esm({
10847
11117
  "src/localStation.ts"() {
10848
11118
  "use strict";
10849
11119
  import_node_child_process3 = require("node:child_process");
10850
11120
  import_node_util3 = require("node:util");
10851
11121
  init_audit();
11122
+ import_promises6 = require("node:fs/promises");
11123
+ import_node_path7 = require("node:path");
10852
11124
  init_databaseEvidence();
10853
11125
  init_safeGit();
10854
11126
  execFileAsync3 = (0, import_node_util3.promisify)(import_node_child_process3.execFile);
@@ -10858,13 +11130,80 @@ var init_localStation = __esm({
10858
11130
  "PORT",
10859
11131
  "TZ",
10860
11132
  "ANALYZE",
11133
+ "HOST",
11134
+ "HOSTNAME",
11135
+ "HOME",
11136
+ "PATH",
11137
+ "PWD",
11138
+ "USER",
11139
+ "SHELL",
11140
+ "TMPDIR",
11141
+ "TEMP",
11142
+ "TMP",
11143
+ "LANG",
11144
+ "DEBUG",
10861
11145
  "VERCEL",
10862
- "VERCEL_ENV",
10863
- "VERCEL_URL",
10864
- "VERCEL_REGION",
10865
11146
  "NEXT_RUNTIME",
10866
- "NEXT_PHASE"
11147
+ "NEXT_PHASE",
11148
+ "NEXT_TELEMETRY_DISABLED",
11149
+ // Vite's built-in import.meta.env values.
11150
+ "MODE",
11151
+ "DEV",
11152
+ "PROD",
11153
+ "SSR",
11154
+ "BASE_URL",
11155
+ // Netlify, Render and other hosts.
11156
+ "NETLIFY",
11157
+ "CONTEXT",
11158
+ "URL",
11159
+ "DEPLOY_URL",
11160
+ "DEPLOY_PRIME_URL",
11161
+ "BUILD_ID",
11162
+ "SITE_ID",
11163
+ "SITE_NAME",
11164
+ "RENDER",
11165
+ "CF_PAGES",
11166
+ "AWS_REGION",
11167
+ "AWS_EXECUTION_ENV",
11168
+ "LAMBDA_TASK_ROOT"
11169
+ ]);
11170
+ ENV_DRIFT_IGNORE_PREFIX = /^(?:npm_|VERCEL_|NEXT_PUBLIC_VERCEL_|__NEXT|CF_PAGES_|RENDER_|RAILWAY_|FLY_|GITHUB_|RUNNER_|NETLIFY_|AWS_LAMBDA_)/;
11171
+ SUPABASE_EDGE_ENV = /* @__PURE__ */ new Set([
11172
+ "SUPABASE_URL",
11173
+ "SUPABASE_ANON_KEY",
11174
+ "SUPABASE_SERVICE_ROLE_KEY",
11175
+ "SUPABASE_DB_URL",
11176
+ "SUPABASE_PUBLISHABLE_KEYS",
11177
+ "SUPABASE_SECRET_KEYS",
11178
+ "SB_REGION",
11179
+ "SB_EXECUTION_ID",
11180
+ "DENO_DEPLOYMENT_ID",
11181
+ "DENO_REGION"
10867
11182
  ]);
11183
+ ENV_TEMPLATE_NAME = /^(?:\.env(?:\.[\w-]+)*\.(?:example|sample|template|tpl|dist|defaults)|(?:example|sample|template)\.env|env\.(?:example|sample|template))$/i;
11184
+ ENV_CODE_FILE = /\.(?:[cm]?[jt]sx?|vue|svelte|astro)$/i;
11185
+ ENV_NAME = "[A-Z][A-Z0-9_]{2,}";
11186
+ ENV_REFERENCES = [
11187
+ new RegExp(`process\\.env\\??\\.(${ENV_NAME})\\b`, "g"),
11188
+ new RegExp(`process\\.env\\??\\.?\\[\\s*['"\`](${ENV_NAME})['"\`]\\s*\\]`, "g"),
11189
+ new RegExp(`import\\.meta\\.env\\??\\.(${ENV_NAME})\\b`, "g"),
11190
+ new RegExp(`import\\.meta\\.env\\??\\.?\\[\\s*['"\`](${ENV_NAME})['"\`]\\s*\\]`, "g"),
11191
+ new RegExp(`Deno\\.env\\.get\\(\\s*['"\`](${ENV_NAME})['"\`]\\s*\\)`, "g"),
11192
+ new RegExp(`Bun\\.env\\.(${ENV_NAME})\\b`, "g"),
11193
+ // Hono and Cloudflare Workers bindings: c.env.X
11194
+ new RegExp(`\\b(?:c|ctx|context)\\.env\\.(${ENV_NAME})\\b`, "g")
11195
+ ];
11196
+ SVELTEKIT_STATIC_IMPORT = /import\s*\{([^}]*)\}\s*from\s*['"]\$env\/static\/(?:private|public)['"]/g;
11197
+ SVELTEKIT_DYNAMIC_IMPORT = /import\s*\{([^}]*)\}\s*from\s*['"]\$env\/dynamic\/(?:private|public)['"]/g;
11198
+ ENV_DESTRUCTURE = /(?:const|let|var)\s*\{([^}]*)\}\s*=\s*(?:process\.env|import\.meta\.env|Bun\.env)\b/g;
11199
+ CODE_FILE = /\.(?:[cm]?[jt]sx?)$/i;
11200
+ STRIPE_VERIFY_CALL = /\bconstructEvent(?:Async)?\s*\(|\.signature\.verifyHeader(?:Async)?\s*\(/;
11201
+ STRIPE_VERIFY_JSON_BODY = /\bconstructEvent(?:Async)?\s*\(\s*JSON\.stringify\s*\(/;
11202
+ STRIPE_SIGNATURE_HEADER = /stripe-signature/i;
11203
+ RAW_BODY = /\.text\(\s*\)|\.arrayBuffer\(\s*\)|express\.raw\s*\(|bodyParser\.raw\s*\(|\braw\(\s*\{|\brawBody\b|\bbuffer\(\s*req\b|getRawBody\s*\(|readRawBody\s*\(|\bevent\.body\b/;
11204
+ STRIPE_EVENT_TYPE = /['"`](?:checkout\.session|customer\.subscription|invoice|payment_intent|charge)\.[a-z_.]+['"`]/;
11205
+ REQUEST_HANDLER = /export\s+(?:async\s+)?function\s+POST\b|export\s+const\s+POST\b|export\s+default\s+(?:async\s+)?function\s*[\w$]*\s*\(\s*(?:req|request)\b|export\s+(?:async\s+function|function|const)\s+handler\b|\b[A-Za-z_$][\w$]*\.post\s*\(|Deno\.serve\s*\(|\bserve\s*\(|addEventListener\(\s*['"]fetch|export\s+default\s*\{\s*(?:async\s+)?fetch\b/;
11206
+ TEST_FILE = /(^|\/)(?:__tests__|__mocks__)\/|\.(?:test|spec)\.[cm]?[jt]sx?$/i;
10868
11207
  AUDIT_FINDING_GAPS = {
10869
11208
  rls_policy_allows_all_write: {
10870
11209
  title: "A policy lets anyone write every row",
@@ -10918,6 +11257,8 @@ var init_localStation = __esm({
10918
11257
  FINDING_SEVERITY_RANK = { critical: 0, high: 1, warning: 2, info: 3 };
10919
11258
  DETAIL_MAX_FINDINGS = 10;
10920
11259
  SEVERITY_ORDER = { critical: 0, warning: 1, info: 2 };
11260
+ FULL_READ_LIMITS = { files: 40, bytes: 512e3 };
11261
+ PAYMENT_PATH = /webhook|stripe|payment|billing|checkout/i;
10921
11262
  }
10922
11263
  });
10923
11264
 
@@ -11627,20 +11968,20 @@ var init_checkMap = __esm({
11627
11968
 
11628
11969
  // src/playbooks/playbooksRoot.ts
11629
11970
  function playbooksRoot() {
11630
- const candidates = [(0, import_node_path7.join)(__dirname, "playbooks"), (0, import_node_path7.join)(__dirname, "..", "..", "playbooks")];
11971
+ const candidates = [(0, import_node_path8.join)(__dirname, "playbooks"), (0, import_node_path8.join)(__dirname, "..", "..", "playbooks")];
11631
11972
  for (const dir of candidates) {
11632
- if ((0, import_node_fs4.existsSync)((0, import_node_path7.join)(dir, "vercel.json"))) {
11973
+ if ((0, import_node_fs4.existsSync)((0, import_node_path8.join)(dir, "vercel.json"))) {
11633
11974
  return dir;
11634
11975
  }
11635
11976
  }
11636
11977
  throw new Error("VibeRaven playbooks directory not found (expected vercel.json)");
11637
11978
  }
11638
- var import_node_fs4, import_node_path7;
11979
+ var import_node_fs4, import_node_path8;
11639
11980
  var init_playbooksRoot = __esm({
11640
11981
  "src/playbooks/playbooksRoot.ts"() {
11641
11982
  "use strict";
11642
11983
  import_node_fs4 = require("node:fs");
11643
- import_node_path7 = require("node:path");
11984
+ import_node_path8 = require("node:path");
11644
11985
  }
11645
11986
  });
11646
11987
 
@@ -11747,8 +12088,8 @@ async function loadPlaybook(provider2) {
11747
12088
  `Unknown provider "${provider2}". Available: ${PLAYBOOK_PROVIDERS.join(", ")}`
11748
12089
  );
11749
12090
  }
11750
- const path = (0, import_node_path8.join)(playbooksRoot(), `${normalized}.json`);
11751
- const raw = JSON.parse(await (0, import_promises6.readFile)(path, "utf-8"));
12091
+ const path = (0, import_node_path9.join)(playbooksRoot(), `${normalized}.json`);
12092
+ const raw = JSON.parse(await (0, import_promises7.readFile)(path, "utf-8"));
11752
12093
  const playbook = parsePlaybook(raw);
11753
12094
  if (playbook.provider !== normalized) {
11754
12095
  throw new Error(`Playbook file ${normalized}.json has mismatched provider field`);
@@ -11762,7 +12103,7 @@ function loadPlaybookSync(provider2) {
11762
12103
  `Unknown provider "${provider2}". Available: ${PLAYBOOK_PROVIDERS.join(", ")}`
11763
12104
  );
11764
12105
  }
11765
- const path = (0, import_node_path8.join)(playbooksRoot(), `${normalized}.json`);
12106
+ const path = (0, import_node_path9.join)(playbooksRoot(), `${normalized}.json`);
11766
12107
  const raw = JSON.parse((0, import_node_fs5.readFileSync)(path, "utf-8"));
11767
12108
  const playbook = parsePlaybook(raw);
11768
12109
  if (playbook.provider !== normalized) {
@@ -11770,13 +12111,13 @@ function loadPlaybookSync(provider2) {
11770
12111
  }
11771
12112
  return playbook;
11772
12113
  }
11773
- var import_node_fs5, import_promises6, import_node_path8;
12114
+ var import_node_fs5, import_promises7, import_node_path9;
11774
12115
  var init_loadPlaybook = __esm({
11775
12116
  "src/playbooks/loadPlaybook.ts"() {
11776
12117
  "use strict";
11777
12118
  import_node_fs5 = require("node:fs");
11778
- import_promises6 = require("node:fs/promises");
11779
- import_node_path8 = require("node:path");
12119
+ import_promises7 = require("node:fs/promises");
12120
+ import_node_path9 = require("node:path");
11780
12121
  init_playbooksRoot();
11781
12122
  init_types2();
11782
12123
  init_validate();
@@ -11861,20 +12202,20 @@ async function loadLastArtifact(startDir) {
11861
12202
  if (!workspace) {
11862
12203
  throw new ScanNotFoundError(needsScanMessage(startDir));
11863
12204
  }
11864
- const path = (0, import_node_path9.join)(getProjectArtifactsDir(workspace), "last-scan.json");
12205
+ const path = (0, import_node_path10.join)(getProjectArtifactsDir(workspace), "last-scan.json");
11865
12206
  try {
11866
- const raw = await (0, import_promises7.readFile)(path, "utf-8");
12207
+ const raw = await (0, import_promises8.readFile)(path, "utf-8");
11867
12208
  return JSON.parse(raw);
11868
12209
  } catch {
11869
12210
  throw new ScanNotFoundError(needsScanMessage(startDir));
11870
12211
  }
11871
12212
  }
11872
- var import_promises7, import_node_path9, SEVERITY_RANK2, ScanNotFoundError;
12213
+ var import_promises8, import_node_path10, SEVERITY_RANK2, ScanNotFoundError;
11873
12214
  var init_menu = __esm({
11874
12215
  "src/tui/menu.ts"() {
11875
12216
  "use strict";
11876
- import_promises7 = require("node:fs/promises");
11877
- import_node_path9 = require("node:path");
12217
+ import_promises8 = require("node:fs/promises");
12218
+ import_node_path10 = require("node:path");
11878
12219
  init_config();
11879
12220
  SEVERITY_RANK2 = {
11880
12221
  critical: 0,
@@ -12592,25 +12933,25 @@ var init_reportHtml = __esm({
12592
12933
  function getBundledReportAssetsDir() {
12593
12934
  const base = __dirname;
12594
12935
  const candidates = [
12595
- (0, import_node_path10.join)(base, "report"),
12596
- (0, import_node_path10.join)(base, "..", "assets", "report"),
12597
- (0, import_node_path10.join)(base, "..", "..", "assets", "report"),
12598
- (0, import_node_path10.join)(process.cwd(), "packages", "cli", "assets", "report"),
12599
- (0, import_node_path10.join)(process.cwd(), "assets", "report")
12936
+ (0, import_node_path11.join)(base, "report"),
12937
+ (0, import_node_path11.join)(base, "..", "assets", "report"),
12938
+ (0, import_node_path11.join)(base, "..", "..", "assets", "report"),
12939
+ (0, import_node_path11.join)(process.cwd(), "packages", "cli", "assets", "report"),
12940
+ (0, import_node_path11.join)(process.cwd(), "assets", "report")
12600
12941
  ];
12601
12942
  for (const dir of candidates) {
12602
- if ((0, import_node_fs6.existsSync)((0, import_node_path10.join)(dir, "station.css"))) {
12943
+ if ((0, import_node_fs6.existsSync)((0, import_node_path11.join)(dir, "station.css"))) {
12603
12944
  return dir;
12604
12945
  }
12605
12946
  }
12606
12947
  throw new Error("Report assets missing. Run `npm run sync-report-assets` and `npm run build` in packages/cli.");
12607
12948
  }
12608
- var import_node_fs6, import_node_path10, REPORT_ASSET_FILES;
12949
+ var import_node_fs6, import_node_path11, REPORT_ASSET_FILES;
12609
12950
  var init_reportAssets = __esm({
12610
12951
  "src/report/reportAssets.ts"() {
12611
12952
  "use strict";
12612
12953
  import_node_fs6 = require("node:fs");
12613
- import_node_path10 = require("node:path");
12954
+ import_node_path11 = require("node:path");
12614
12955
  REPORT_ASSET_FILES = [
12615
12956
  "station.css",
12616
12957
  "station.js",
@@ -12641,8 +12982,8 @@ function stripWindowsDrive(value) {
12641
12982
  }
12642
12983
  function stripAbsolutePrefix(value) {
12643
12984
  const normalized = slashPath(value);
12644
- const isAbsolute2 = /^[a-z]:\//i.test(normalized) || normalized.startsWith("/");
12645
- if (!isAbsolute2) {
12985
+ const isAbsolute3 = /^[a-z]:\//i.test(normalized) || normalized.startsWith("/");
12986
+ if (!isAbsolute3) {
12646
12987
  return normalized;
12647
12988
  }
12648
12989
  const lower = normalized.toLowerCase();
@@ -12838,11 +13179,11 @@ function emptyActionRegistry() {
12838
13179
  return { version: 1, nextId: 1, actions: {} };
12839
13180
  }
12840
13181
  function actionRegistryPath(cwd) {
12841
- return (0, import_node_path11.join)(getProjectArtifactsDir(cwd), "action-registry.json");
13182
+ return (0, import_node_path12.join)(getProjectArtifactsDir(cwd), "action-registry.json");
12842
13183
  }
12843
13184
  async function loadActionRegistry(cwd) {
12844
13185
  try {
12845
- const parsed = JSON.parse(await (0, import_promises8.readFile)(actionRegistryPath(cwd), "utf8"));
13186
+ const parsed = JSON.parse(await (0, import_promises9.readFile)(actionRegistryPath(cwd), "utf8"));
12846
13187
  if (parsed.version === 1 && Number.isInteger(parsed.nextId) && parsed.actions) {
12847
13188
  return parsed;
12848
13189
  }
@@ -12852,8 +13193,8 @@ async function loadActionRegistry(cwd) {
12852
13193
  }
12853
13194
  async function saveActionRegistry(cwd, registry) {
12854
13195
  const dir = getProjectArtifactsDir(cwd);
12855
- await (0, import_promises8.mkdir)(dir, { recursive: true });
12856
- await (0, import_promises8.writeFile)(actionRegistryPath(cwd), `${JSON.stringify(registry, null, 2)}
13196
+ await (0, import_promises9.mkdir)(dir, { recursive: true });
13197
+ await (0, import_promises9.writeFile)(actionRegistryPath(cwd), `${JSON.stringify(registry, null, 2)}
12857
13198
  `, "utf8");
12858
13199
  }
12859
13200
  function nextActionId(registry) {
@@ -12946,12 +13287,12 @@ async function resolveActionById(cwd, id) {
12946
13287
  const registry = await loadActionRegistry(cwd);
12947
13288
  return Object.values(registry.actions).find((entry) => entry.id === id);
12948
13289
  }
12949
- var import_promises8, import_node_path11;
13290
+ var import_promises9, import_node_path12;
12950
13291
  var init_registry2 = __esm({
12951
13292
  "src/actions/registry.ts"() {
12952
13293
  "use strict";
12953
- import_promises8 = require("node:fs/promises");
12954
- import_node_path11 = require("node:path");
13294
+ import_promises9 = require("node:fs/promises");
13295
+ import_node_path12 = require("node:path");
12955
13296
  init_config();
12956
13297
  init_canonical();
12957
13298
  }
@@ -12983,8 +13324,8 @@ async function writeActionArtifacts(input) {
12983
13324
  gateStatus: gateStatus2(input.artifact),
12984
13325
  actions: assigned.actions
12985
13326
  };
12986
- const actionsPath = (0, import_node_path12.join)(getProjectArtifactsDir(input.cwd), "actions.json");
12987
- await (0, import_promises9.writeFile)(actionsPath, `${JSON.stringify(manifest, null, 2)}
13327
+ const actionsPath = (0, import_node_path13.join)(getProjectArtifactsDir(input.cwd), "actions.json");
13328
+ await (0, import_promises10.writeFile)(actionsPath, `${JSON.stringify(manifest, null, 2)}
12988
13329
  `, "utf8");
12989
13330
  return {
12990
13331
  actionsPath,
@@ -12992,12 +13333,12 @@ async function writeActionArtifacts(input) {
12992
13333
  manifest
12993
13334
  };
12994
13335
  }
12995
- var import_promises9, import_node_path12;
13336
+ var import_promises10, import_node_path13;
12996
13337
  var init_artifacts = __esm({
12997
13338
  "src/actions/artifacts.ts"() {
12998
13339
  "use strict";
12999
- import_promises9 = require("node:fs/promises");
13000
- import_node_path12 = require("node:path");
13340
+ import_promises10 = require("node:fs/promises");
13341
+ import_node_path13 = require("node:path");
13001
13342
  init_config();
13002
13343
  init_derive();
13003
13344
  init_registry2();
@@ -13007,25 +13348,25 @@ var init_artifacts = __esm({
13007
13348
  // src/artifacts.ts
13008
13349
  async function copyReportAssets(reportAssetsDir) {
13009
13350
  const sourceDir = getBundledReportAssetsDir();
13010
- await (0, import_promises10.mkdir)((0, import_node_path13.join)(reportAssetsDir, "assets"), { recursive: true });
13351
+ await (0, import_promises11.mkdir)((0, import_node_path14.join)(reportAssetsDir, "assets"), { recursive: true });
13011
13352
  for (const rel of REPORT_ASSET_FILES) {
13012
- await (0, import_promises10.copyFile)((0, import_node_path13.join)(sourceDir, rel), (0, import_node_path13.join)(reportAssetsDir, rel));
13353
+ await (0, import_promises11.copyFile)((0, import_node_path14.join)(sourceDir, rel), (0, import_node_path14.join)(reportAssetsDir, rel));
13013
13354
  }
13014
13355
  }
13015
13356
  async function writeScanArtifacts(options) {
13016
13357
  const cwd = options.cwd ?? options.artifact.workspacePath;
13017
13358
  const dir = getProjectArtifactsDir(cwd);
13018
- await (0, import_promises10.mkdir)(dir, { recursive: true });
13019
- const jsonPath = (0, import_node_path13.join)(dir, "last-scan.json");
13020
- const gateResultPath = (0, import_node_path13.join)(dir, "gate-result.json");
13021
- const contextMapPath = (0, import_node_path13.join)(dir, "context-map.json");
13022
- const gapsDir = (0, import_node_path13.join)(dir, "gaps");
13023
- const tasklistPath = (0, import_node_path13.join)(dir, "agent-tasklist.md");
13024
- const summaryPath = (0, import_node_path13.join)(dir, "agent-summary.md");
13025
- const playbookPath = (0, import_node_path13.join)(dir, "launch-playbook.md");
13026
- const reportPath = (0, import_node_path13.join)(dir, "report.html");
13027
- const reportAssetsDir = (0, import_node_path13.join)(dir, "report");
13028
- await (0, import_promises10.mkdir)(gapsDir, { recursive: true });
13359
+ await (0, import_promises11.mkdir)(dir, { recursive: true });
13360
+ const jsonPath = (0, import_node_path14.join)(dir, "last-scan.json");
13361
+ const gateResultPath = (0, import_node_path14.join)(dir, "gate-result.json");
13362
+ const contextMapPath = (0, import_node_path14.join)(dir, "context-map.json");
13363
+ const gapsDir = (0, import_node_path14.join)(dir, "gaps");
13364
+ const tasklistPath = (0, import_node_path14.join)(dir, "agent-tasklist.md");
13365
+ const summaryPath = (0, import_node_path14.join)(dir, "agent-summary.md");
13366
+ const playbookPath = (0, import_node_path14.join)(dir, "launch-playbook.md");
13367
+ const reportPath = (0, import_node_path14.join)(dir, "report.html");
13368
+ const reportAssetsDir = (0, import_node_path14.join)(dir, "report");
13369
+ await (0, import_promises11.mkdir)(gapsDir, { recursive: true });
13029
13370
  const safe2 = sanitizeArtifactForDisk(options.artifact);
13030
13371
  const json = `${JSON.stringify(safe2, null, 2)}
13031
13372
  `;
@@ -13040,26 +13381,26 @@ async function writeScanArtifacts(options) {
13040
13381
  `;
13041
13382
  const gapEvidenceFiles = generateGapEvidenceFiles(safe2);
13042
13383
  await copyReportAssets(reportAssetsDir);
13043
- await (0, import_promises10.writeFile)(gateResultPath, gateResult, "utf-8");
13044
- await (0, import_promises10.writeFile)(contextMapPath, contextMap, "utf-8");
13384
+ await (0, import_promises11.writeFile)(gateResultPath, gateResult, "utf-8");
13385
+ await (0, import_promises11.writeFile)(contextMapPath, contextMap, "utf-8");
13045
13386
  for (const gap of gapEvidenceFiles) {
13046
- await (0, import_promises10.writeFile)((0, import_node_path13.join)(dir, "gaps", `${gap.content.id}.json`), `${JSON.stringify(gap.content, null, 2)}
13387
+ await (0, import_promises11.writeFile)((0, import_node_path14.join)(dir, "gaps", `${gap.content.id}.json`), `${JSON.stringify(gap.content, null, 2)}
13047
13388
  `, "utf-8");
13048
13389
  }
13049
- await (0, import_promises10.writeFile)(tasklistPath, tasklist, "utf-8");
13390
+ await (0, import_promises11.writeFile)(tasklistPath, tasklist, "utf-8");
13050
13391
  let architectureMapPath;
13051
13392
  if (typeof safe2.architectureMap === "string" && safe2.architectureMap.length > 0) {
13052
- architectureMapPath = (0, import_node_path13.join)(dir, "architecture-map.md");
13053
- await (0, import_promises10.writeFile)(architectureMapPath, safe2.architectureMap, "utf-8");
13393
+ architectureMapPath = (0, import_node_path14.join)(dir, "architecture-map.md");
13394
+ await (0, import_promises11.writeFile)(architectureMapPath, safe2.architectureMap, "utf-8");
13054
13395
  }
13055
13396
  if (safe2.architectureData) {
13056
- await (0, import_promises10.writeFile)((0, import_node_path13.join)(dir, "architecture-map.json"), `${JSON.stringify(safe2.architectureData, null, 2)}
13397
+ await (0, import_promises11.writeFile)((0, import_node_path14.join)(dir, "architecture-map.json"), `${JSON.stringify(safe2.architectureData, null, 2)}
13057
13398
  `, "utf-8");
13058
13399
  }
13059
- await (0, import_promises10.writeFile)(jsonPath, json, "utf-8");
13060
- await (0, import_promises10.writeFile)(summaryPath, summary, "utf-8");
13061
- await (0, import_promises10.writeFile)(playbookPath, playbook, "utf-8");
13062
- await (0, import_promises10.writeFile)(reportPath, html, "utf-8");
13400
+ await (0, import_promises11.writeFile)(jsonPath, json, "utf-8");
13401
+ await (0, import_promises11.writeFile)(summaryPath, summary, "utf-8");
13402
+ await (0, import_promises11.writeFile)(playbookPath, playbook, "utf-8");
13403
+ await (0, import_promises11.writeFile)(reportPath, html, "utf-8");
13063
13404
  const actionArtifacts = await writeActionArtifacts({
13064
13405
  cwd,
13065
13406
  artifact: safe2,
@@ -13082,12 +13423,12 @@ async function writeScanArtifacts(options) {
13082
13423
  ...architectureMapPath ? { architectureMapPath } : {}
13083
13424
  };
13084
13425
  }
13085
- var import_promises10, import_node_path13;
13426
+ var import_promises11, import_node_path14;
13086
13427
  var init_artifacts2 = __esm({
13087
13428
  "src/artifacts.ts"() {
13088
13429
  "use strict";
13089
- import_promises10 = require("node:fs/promises");
13090
- import_node_path13 = require("node:path");
13430
+ import_promises11 = require("node:fs/promises");
13431
+ import_node_path14 = require("node:path");
13091
13432
  init_contextMap();
13092
13433
  init_gateResult();
13093
13434
  init_gapEvidence();
@@ -13196,12 +13537,12 @@ function createAppWindowOpenCommands(target, platform = process.platform, env =
13196
13537
  ];
13197
13538
  if (platform === "win32") {
13198
13539
  const candidates = [
13199
- env.LOCALAPPDATA ? (0, import_node_path14.join)(env.LOCALAPPDATA, "Google", "Chrome", "Application", "chrome.exe") : "",
13200
- env.PROGRAMFILES ? (0, import_node_path14.join)(env.PROGRAMFILES, "Google", "Chrome", "Application", "chrome.exe") : "",
13201
- env["PROGRAMFILES(X86)"] ? (0, import_node_path14.join)(env["PROGRAMFILES(X86)"], "Google", "Chrome", "Application", "chrome.exe") : "",
13202
- env.LOCALAPPDATA ? (0, import_node_path14.join)(env.LOCALAPPDATA, "Microsoft", "Edge", "Application", "msedge.exe") : "",
13203
- env.PROGRAMFILES ? (0, import_node_path14.join)(env.PROGRAMFILES, "Microsoft", "Edge", "Application", "msedge.exe") : "",
13204
- env["PROGRAMFILES(X86)"] ? (0, import_node_path14.join)(env["PROGRAMFILES(X86)"], "Microsoft", "Edge", "Application", "msedge.exe") : "",
13540
+ env.LOCALAPPDATA ? (0, import_node_path15.join)(env.LOCALAPPDATA, "Google", "Chrome", "Application", "chrome.exe") : "",
13541
+ env.PROGRAMFILES ? (0, import_node_path15.join)(env.PROGRAMFILES, "Google", "Chrome", "Application", "chrome.exe") : "",
13542
+ env["PROGRAMFILES(X86)"] ? (0, import_node_path15.join)(env["PROGRAMFILES(X86)"], "Google", "Chrome", "Application", "chrome.exe") : "",
13543
+ env.LOCALAPPDATA ? (0, import_node_path15.join)(env.LOCALAPPDATA, "Microsoft", "Edge", "Application", "msedge.exe") : "",
13544
+ env.PROGRAMFILES ? (0, import_node_path15.join)(env.PROGRAMFILES, "Microsoft", "Edge", "Application", "msedge.exe") : "",
13545
+ env["PROGRAMFILES(X86)"] ? (0, import_node_path15.join)(env["PROGRAMFILES(X86)"], "Microsoft", "Edge", "Application", "msedge.exe") : "",
13205
13546
  "chrome",
13206
13547
  "msedge"
13207
13548
  ].filter(Boolean);
@@ -13222,12 +13563,12 @@ function createAppWindowOpenCommands(target, platform = process.platform, env =
13222
13563
  }
13223
13564
  async function openWithSystemDefault(target) {
13224
13565
  const { command, args, shell } = createOpenCommand(target);
13225
- await new Promise((resolve8, reject) => {
13566
+ await new Promise((resolve9, reject) => {
13226
13567
  const child = (0, import_node_child_process4.spawn)(command, args, { stdio: "ignore", shell });
13227
13568
  child.on("error", reject);
13228
13569
  child.on("exit", (code) => {
13229
13570
  if (code === 0) {
13230
- resolve8();
13571
+ resolve9();
13231
13572
  } else {
13232
13573
  reject(new Error(`Could not open browser (exit ${code ?? "unknown"}). Open manually: ${target}`));
13233
13574
  }
@@ -13238,7 +13579,7 @@ async function spawnDetached(command) {
13238
13579
  if (/^[A-Za-z]:[\\/]/.test(command.command) && !(0, import_node_fs7.existsSync)(command.command)) {
13239
13580
  throw new Error(`Browser not found: ${command.command}`);
13240
13581
  }
13241
- await new Promise((resolve8, reject) => {
13582
+ await new Promise((resolve9, reject) => {
13242
13583
  const child = (0, import_node_child_process4.spawn)(command.command, command.args, {
13243
13584
  detached: true,
13244
13585
  stdio: "ignore",
@@ -13246,7 +13587,7 @@ async function spawnDetached(command) {
13246
13587
  });
13247
13588
  child.once("spawn", () => {
13248
13589
  child.unref();
13249
- resolve8();
13590
+ resolve9();
13250
13591
  });
13251
13592
  child.once("error", reject);
13252
13593
  });
@@ -13278,13 +13619,13 @@ async function openUrlInBrowser(url) {
13278
13619
  }
13279
13620
  await openWithSystemDefault(trimmed);
13280
13621
  }
13281
- var import_node_child_process4, import_node_fs7, import_node_path14;
13622
+ var import_node_child_process4, import_node_fs7, import_node_path15;
13282
13623
  var init_openBrowser = __esm({
13283
13624
  "src/openBrowser.ts"() {
13284
13625
  "use strict";
13285
13626
  import_node_child_process4 = require("node:child_process");
13286
13627
  import_node_fs7 = require("node:fs");
13287
- import_node_path14 = require("node:path");
13628
+ import_node_path15 = require("node:path");
13288
13629
  }
13289
13630
  });
13290
13631
 
@@ -13314,12 +13655,12 @@ async function copyToClipboard(text) {
13314
13655
  }
13315
13656
  }
13316
13657
  function pipeToCommand(command, text, extraArgs = []) {
13317
- return new Promise((resolve8, reject) => {
13658
+ return new Promise((resolve9, reject) => {
13318
13659
  const child = (0, import_node_child_process5.spawn)(command, extraArgs, { stdio: ["pipe", "ignore", "ignore"] });
13319
13660
  child.on("error", reject);
13320
13661
  child.on("close", (code) => {
13321
13662
  if (code === 0) {
13322
- resolve8();
13663
+ resolve9();
13323
13664
  } else {
13324
13665
  reject(new Error(`${command} exited with code ${code ?? "unknown"}`));
13325
13666
  }
@@ -14226,7 +14567,7 @@ var VERSION;
14226
14567
  var init_version = __esm({
14227
14568
  "src/version.ts"() {
14228
14569
  "use strict";
14229
- VERSION = "1.5.0";
14570
+ VERSION = "1.5.2";
14230
14571
  }
14231
14572
  });
14232
14573
 
@@ -14621,25 +14962,25 @@ async function initCursorRulesPack(options) {
14621
14962
  const pack = buildCursorRulesPack();
14622
14963
  for (const rule of pack) {
14623
14964
  const file = `${CURSOR_RULES_DIR}/${rule.filename}`;
14624
- const path = (0, import_node_path15.join)(options.cwd, file);
14965
+ const path = (0, import_node_path16.join)(options.cwd, file);
14625
14966
  const existing = await readExistingFile(path);
14626
14967
  const changed = !existing.exists || existing.content !== rule.content;
14627
14968
  const action = !existing.exists ? "created" : changed ? "updated" : "unchanged";
14628
14969
  if (!options.dryRun && changed) {
14629
- await (0, import_promises11.mkdir)((0, import_node_path15.join)(options.cwd, CURSOR_RULES_DIR), { recursive: true });
14630
- await (0, import_promises11.writeFile)(path, rule.content, "utf-8");
14970
+ await (0, import_promises12.mkdir)((0, import_node_path16.join)(options.cwd, CURSOR_RULES_DIR), { recursive: true });
14971
+ await (0, import_promises12.writeFile)(path, rule.content, "utf-8");
14631
14972
  }
14632
14973
  results.push({ target: "cursor", file, path, action });
14633
14974
  }
14634
- const legacyPath = (0, import_node_path15.join)(options.cwd, LEGACY_CURSOR_RULE_FILE);
14975
+ const legacyPath = (0, import_node_path16.join)(options.cwd, LEGACY_CURSOR_RULE_FILE);
14635
14976
  if (!options.dryRun && await fileExists(legacyPath)) {
14636
- await (0, import_promises11.rm)(legacyPath, { force: true });
14977
+ await (0, import_promises12.rm)(legacyPath, { force: true });
14637
14978
  }
14638
14979
  return results;
14639
14980
  }
14640
14981
  async function readExistingFile(path) {
14641
14982
  try {
14642
- return { exists: true, content: await (0, import_promises11.readFile)(path, "utf-8") };
14983
+ return { exists: true, content: await (0, import_promises12.readFile)(path, "utf-8") };
14643
14984
  } catch (error) {
14644
14985
  if (isFileNotFoundError(error)) {
14645
14986
  return { exists: false, content: "" };
@@ -14649,7 +14990,7 @@ async function readExistingFile(path) {
14649
14990
  }
14650
14991
  async function fileExists(path) {
14651
14992
  try {
14652
- await (0, import_promises11.access)(path);
14993
+ await (0, import_promises12.access)(path);
14653
14994
  return true;
14654
14995
  } catch {
14655
14996
  return false;
@@ -14658,12 +14999,12 @@ async function fileExists(path) {
14658
14999
  function isFileNotFoundError(error) {
14659
15000
  return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
14660
15001
  }
14661
- var import_promises11, import_node_path15, CURSOR_RULES_DIR, LEGACY_CURSOR_RULE_FILE, DOMAIN_PATH_POINTER;
15002
+ var import_promises12, import_node_path16, CURSOR_RULES_DIR, LEGACY_CURSOR_RULE_FILE, DOMAIN_PATH_POINTER;
14662
15003
  var init_cursorRulesPack = __esm({
14663
15004
  "src/commands/cursorRulesPack.ts"() {
14664
15005
  "use strict";
14665
- import_promises11 = require("node:fs/promises");
14666
- import_node_path15 = require("node:path");
15006
+ import_promises12 = require("node:fs/promises");
15007
+ import_node_path16 = require("node:path");
14667
15008
  init_commands();
14668
15009
  CURSOR_RULES_DIR = ".cursor/rules";
14669
15010
  LEGACY_CURSOR_RULE_FILE = `${CURSOR_RULES_DIR}/viberaven.mdc`;
@@ -14763,11 +15104,11 @@ var init_agentTargets = __esm({
14763
15104
 
14764
15105
  // src/commands/seedPackageJsonScripts.ts
14765
15106
  async function seedPackageJsonScripts(options) {
14766
- const packageJsonPath = (0, import_node_path16.join)(options.cwd, "package.json");
15107
+ const packageJsonPath = (0, import_node_path17.join)(options.cwd, "package.json");
14767
15108
  if (!(0, import_node_fs8.existsSync)(packageJsonPath)) {
14768
15109
  return null;
14769
15110
  }
14770
- const raw = await (0, import_promises12.readFile)(packageJsonPath, "utf-8");
15111
+ const raw = await (0, import_promises13.readFile)(packageJsonPath, "utf-8");
14771
15112
  let pkg;
14772
15113
  try {
14773
15114
  pkg = JSON.parse(raw);
@@ -14797,17 +15138,17 @@ async function seedPackageJsonScripts(options) {
14797
15138
  pkg.scripts = scripts;
14798
15139
  const output = `${JSON.stringify(pkg, null, 2)}
14799
15140
  `;
14800
- await (0, import_promises12.writeFile)(packageJsonPath, output, "utf-8");
15141
+ await (0, import_promises13.writeFile)(packageJsonPath, output, "utf-8");
14801
15142
  }
14802
15143
  return { action: "updated", added, skipped, changed: true };
14803
15144
  }
14804
- var import_node_fs8, import_promises12, import_node_path16, VIBERAVEN_PACKAGE_JSON_SCRIPTS;
15145
+ var import_node_fs8, import_promises13, import_node_path17, VIBERAVEN_PACKAGE_JSON_SCRIPTS;
14805
15146
  var init_seedPackageJsonScripts = __esm({
14806
15147
  "src/commands/seedPackageJsonScripts.ts"() {
14807
15148
  "use strict";
14808
15149
  import_node_fs8 = require("node:fs");
14809
- import_promises12 = require("node:fs/promises");
14810
- import_node_path16 = require("node:path");
15150
+ import_promises13 = require("node:fs/promises");
15151
+ import_node_path17 = require("node:path");
14811
15152
  init_commands();
14812
15153
  VIBERAVEN_PACKAGE_JSON_SCRIPTS = {
14813
15154
  "viberaven:gate": PUBLIC_AGENT_MODE_COMMAND,
@@ -14827,13 +15168,13 @@ async function initAgentRules(options) {
14827
15168
  continue;
14828
15169
  }
14829
15170
  const file = AGENT_RULE_TARGETS[target].file;
14830
- const path = (0, import_node_path17.join)(options.cwd, file);
15171
+ const path = (0, import_node_path18.join)(options.cwd, file);
14831
15172
  const existing = await readExistingFile2(path);
14832
15173
  const injected = injectAgentRulesBlock(existing.content, renderAgentRulesForTarget(target));
14833
15174
  const action = !existing.exists ? "created" : injected.changed ? "updated" : "unchanged";
14834
15175
  if (!options.dryRun && injected.changed) {
14835
- await (0, import_promises13.mkdir)((0, import_node_path17.dirname)(path), { recursive: true });
14836
- await (0, import_promises13.writeFile)(path, injected.content, "utf-8");
15176
+ await (0, import_promises14.mkdir)((0, import_node_path18.dirname)(path), { recursive: true });
15177
+ await (0, import_promises14.writeFile)(path, injected.content, "utf-8");
14837
15178
  }
14838
15179
  results.push({ target, file, path, action });
14839
15180
  }
@@ -14901,7 +15242,7 @@ function formatAgentRulesInitSummary(output) {
14901
15242
  }
14902
15243
  async function readExistingFile2(path) {
14903
15244
  try {
14904
- return { exists: true, content: await (0, import_promises13.readFile)(path, "utf-8") };
15245
+ return { exists: true, content: await (0, import_promises14.readFile)(path, "utf-8") };
14905
15246
  } catch (error) {
14906
15247
  if (isFileNotFoundError2(error)) {
14907
15248
  return { exists: false, content: "" };
@@ -14912,12 +15253,12 @@ async function readExistingFile2(path) {
14912
15253
  function isFileNotFoundError2(error) {
14913
15254
  return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
14914
15255
  }
14915
- var import_promises13, import_node_path17;
15256
+ var import_promises14, import_node_path18;
14916
15257
  var init_initRules = __esm({
14917
15258
  "src/commands/initRules.ts"() {
14918
15259
  "use strict";
14919
- import_promises13 = require("node:fs/promises");
14920
- import_node_path17 = require("node:path");
15260
+ import_promises14 = require("node:fs/promises");
15261
+ import_node_path18 = require("node:path");
14921
15262
  init_agentRulesBlock();
14922
15263
  init_agentTargets();
14923
15264
  init_cursorRulesPack();
@@ -15180,7 +15521,7 @@ async function runInteractiveSession(startDir = process.cwd()) {
15180
15521
  Ie(`${import_picocolors4.default.bold("VibeRaven")} ${import_picocolors4.default.dim(VERSION)}`);
15181
15522
  const cwd = await resolveWorkspaceRoot(startDir);
15182
15523
  const artifactsAt = await findArtifactsWorkspace(startDir);
15183
- if (artifactsAt && (0, import_node_path18.resolve)(artifactsAt) !== (0, import_node_path18.resolve)(startDir)) {
15524
+ if (artifactsAt && (0, import_node_path19.resolve)(artifactsAt) !== (0, import_node_path19.resolve)(startDir)) {
15184
15525
  M2.message(import_picocolors4.default.dim(`Using scan from: ${artifactsAt}`));
15185
15526
  } else {
15186
15527
  M2.message(import_picocolors4.default.dim(`Project folder: ${cwd}`));
@@ -15239,11 +15580,11 @@ async function runInteractiveSession(startDir = process.cwd()) {
15239
15580
  }
15240
15581
  Se(import_picocolors4.default.dim("Run viberaven anytime for the interactive menu."));
15241
15582
  }
15242
- var import_node_path18, import_picocolors4;
15583
+ var import_node_path19, import_picocolors4;
15243
15584
  var init_runInteractive = __esm({
15244
15585
  "src/tui/runInteractive.ts"() {
15245
15586
  "use strict";
15246
- import_node_path18 = require("node:path");
15587
+ import_node_path19 = require("node:path");
15247
15588
  init_dist2();
15248
15589
  import_picocolors4 = __toESM(require_picocolors());
15249
15590
  init_config();
@@ -15268,18 +15609,18 @@ var init_runInteractive = __esm({
15268
15609
  // src/commands/condense.ts
15269
15610
  async function runCondenseCommand(options) {
15270
15611
  const dir = getProjectArtifactsDir(options.cwd);
15271
- const artifact = JSON.parse(await (0, import_promises14.readFile)((0, import_node_path19.join)(dir, "last-scan.json"), "utf8"));
15272
- const contextMapPath = (0, import_node_path19.join)(dir, "context-map.json");
15273
- await (0, import_promises14.writeFile)(contextMapPath, `${JSON.stringify(generateContextMap(artifact), null, 2)}
15612
+ const artifact = JSON.parse(await (0, import_promises15.readFile)((0, import_node_path20.join)(dir, "last-scan.json"), "utf8"));
15613
+ const contextMapPath = (0, import_node_path20.join)(dir, "context-map.json");
15614
+ await (0, import_promises15.writeFile)(contextMapPath, `${JSON.stringify(generateContextMap(artifact), null, 2)}
15274
15615
  `, "utf8");
15275
15616
  return { contextMapPath };
15276
15617
  }
15277
- var import_promises14, import_node_path19;
15618
+ var import_promises15, import_node_path20;
15278
15619
  var init_condense = __esm({
15279
15620
  "src/commands/condense.ts"() {
15280
15621
  "use strict";
15281
- import_promises14 = require("node:fs/promises");
15282
- import_node_path19 = require("node:path");
15622
+ import_promises15 = require("node:fs/promises");
15623
+ import_node_path20 = require("node:path");
15283
15624
  init_config();
15284
15625
  init_contextMap();
15285
15626
  }
@@ -15287,9 +15628,9 @@ var init_condense = __esm({
15287
15628
 
15288
15629
  // src/heal/pathSafety.ts
15289
15630
  function assertSafeHealTarget(cwd, target) {
15290
- const root = (0, import_node_path20.resolve)(cwd);
15291
- const absolute = (0, import_node_path20.resolve)(root, target);
15292
- const rel = (0, import_node_path20.relative)(root, absolute);
15631
+ const root = (0, import_node_path21.resolve)(cwd);
15632
+ const absolute = (0, import_node_path21.resolve)(root, target);
15633
+ const rel = (0, import_node_path21.relative)(root, absolute);
15293
15634
  if (rel.startsWith("..") || rel === "" || /^[A-Za-z]:/.test(rel)) {
15294
15635
  throw new Error("Heal target must stay inside the workspace");
15295
15636
  }
@@ -15299,11 +15640,11 @@ function assertSafeHealTarget(cwd, target) {
15299
15640
  }
15300
15641
  return absolute;
15301
15642
  }
15302
- var import_node_path20, BLOCKED_SEGMENTS;
15643
+ var import_node_path21, BLOCKED_SEGMENTS;
15303
15644
  var init_pathSafety = __esm({
15304
15645
  "src/heal/pathSafety.ts"() {
15305
15646
  "use strict";
15306
- import_node_path20 = require("node:path");
15647
+ import_node_path21 = require("node:path");
15307
15648
  BLOCKED_SEGMENTS = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", ".next", ".viberaven"]);
15308
15649
  }
15309
15650
  });
@@ -15765,7 +16106,7 @@ export const config = {
15765
16106
  // src/heal/recipes/eslintRestrictedImports.ts
15766
16107
  function detectEslintConfigFile(cwd) {
15767
16108
  for (const candidate of ESLINT_CONFIG_CANDIDATES) {
15768
- if ((0, import_node_fs9.existsSync)((0, import_node_path21.join)(cwd, candidate))) {
16109
+ if ((0, import_node_fs9.existsSync)((0, import_node_path22.join)(cwd, candidate))) {
15769
16110
  return candidate;
15770
16111
  }
15771
16112
  }
@@ -15920,12 +16261,12 @@ function applyEslintRestrictedImportsRecipe(source, configFile) {
15920
16261
  }
15921
16262
  return applyLegacyModuleRecipe(source);
15922
16263
  }
15923
- var import_node_fs9, import_node_path21, VIBERAVEN_ESLINT_MARKER, RESTRICTED_IMPORTS_MESSAGE, RESTRICTED_PATHS, RESTRICTED_PATTERNS, ESLINT_CONFIG_CANDIDATES;
16264
+ var import_node_fs9, import_node_path22, VIBERAVEN_ESLINT_MARKER, RESTRICTED_IMPORTS_MESSAGE, RESTRICTED_PATHS, RESTRICTED_PATTERNS, ESLINT_CONFIG_CANDIDATES;
15924
16265
  var init_eslintRestrictedImports = __esm({
15925
16266
  "src/heal/recipes/eslintRestrictedImports.ts"() {
15926
16267
  "use strict";
15927
16268
  import_node_fs9 = require("node:fs");
15928
- import_node_path21 = require("node:path");
16269
+ import_node_path22 = require("node:path");
15929
16270
  init_commands();
15930
16271
  VIBERAVEN_ESLINT_MARKER = "VibeRaven heal: eslint_restricted_imports";
15931
16272
  RESTRICTED_IMPORTS_MESSAGE = `Restricted import. Run ${PUBLIC_AGENT_MODE_COMMAND} before substituting packages.`;
@@ -15977,13 +16318,13 @@ function defaultTargetFile(gapId) {
15977
16318
  }
15978
16319
  async function readSourceOrEmpty(absolutePath) {
15979
16320
  if (!(0, import_node_fs10.existsSync)(absolutePath)) return "";
15980
- return (0, import_promises15.readFile)(absolutePath, "utf8");
16321
+ return (0, import_promises16.readFile)(absolutePath, "utf8");
15981
16322
  }
15982
16323
  async function detectUpstash(cwd) {
15983
16324
  try {
15984
- const pkgPath = (0, import_node_path22.join)(cwd, "package.json");
16325
+ const pkgPath = (0, import_node_path23.join)(cwd, "package.json");
15985
16326
  if (!(0, import_node_fs10.existsSync)(pkgPath)) return false;
15986
- const raw = await (0, import_promises15.readFile)(pkgPath, "utf8");
16327
+ const raw = await (0, import_promises16.readFile)(pkgPath, "utf8");
15987
16328
  const pkg = JSON.parse(raw);
15988
16329
  const deps = {
15989
16330
  ...pkg["dependencies"] ?? {},
@@ -15998,7 +16339,7 @@ async function dispatchRecipeByGapId(gapId, cwd, explicitTarget) {
15998
16339
  const targetFile = explicitTarget ?? defaultTargetFile(gapId);
15999
16340
  if (targetFile === void 0) return null;
16000
16341
  if (gapId === "auth_secret_missing" || gapId === "node_env_not_set" || gapId === "database_url_missing") {
16001
- const absolutePath = (0, import_node_path22.join)(cwd, targetFile);
16342
+ const absolutePath = (0, import_node_path23.join)(cwd, targetFile);
16002
16343
  const source = await readSourceOrEmpty(absolutePath);
16003
16344
  let result;
16004
16345
  if (gapId === "auth_secret_missing") result = applyAuthSecretRecipe(source);
@@ -16012,25 +16353,25 @@ async function dispatchRecipeByGapId(gapId, cwd, explicitTarget) {
16012
16353
  };
16013
16354
  }
16014
16355
  if (gapId === "missing_error_boundary") {
16015
- const absolutePath = (0, import_node_path22.join)(cwd, "app/error.tsx");
16356
+ const absolutePath = (0, import_node_path23.join)(cwd, "app/error.tsx");
16016
16357
  const source = await readSourceOrEmpty(absolutePath);
16017
16358
  const result = applyErrorBoundaryRecipe(source);
16018
16359
  return { ...result, canAutoApply: true, recipeName: gapId };
16019
16360
  }
16020
16361
  if (gapId === "missing_health_route") {
16021
- const absolutePath = (0, import_node_path22.join)(cwd, "app/api/health/route.ts");
16362
+ const absolutePath = (0, import_node_path23.join)(cwd, "app/api/health/route.ts");
16022
16363
  const source = await readSourceOrEmpty(absolutePath);
16023
16364
  const result = applyHealthRouteRecipe(source);
16024
16365
  return { ...result, canAutoApply: true, recipeName: gapId };
16025
16366
  }
16026
16367
  if (gapId === "missing_loading_state") {
16027
- const absolutePath = (0, import_node_path22.join)(cwd, "app/loading.tsx");
16368
+ const absolutePath = (0, import_node_path23.join)(cwd, "app/loading.tsx");
16028
16369
  const source = await readSourceOrEmpty(absolutePath);
16029
16370
  const result = applyLoadingStateRecipe(source);
16030
16371
  return { ...result, canAutoApply: true, recipeName: gapId };
16031
16372
  }
16032
16373
  if (gapId === "missing_404_page") {
16033
- const absolutePath = (0, import_node_path22.join)(cwd, "app/not-found.tsx");
16374
+ const absolutePath = (0, import_node_path23.join)(cwd, "app/not-found.tsx");
16034
16375
  const source = await readSourceOrEmpty(absolutePath);
16035
16376
  const result = applyNotFoundRecipe(source);
16036
16377
  return { ...result, canAutoApply: true, recipeName: gapId };
@@ -16048,10 +16389,10 @@ async function dispatchRecipeByGapId(gapId, cwd, explicitTarget) {
16048
16389
  }
16049
16390
  if (gapId === "missing_csp_header") {
16050
16391
  let configFile = "next.config.js";
16051
- let absolutePath = (0, import_node_path22.join)(cwd, configFile);
16392
+ let absolutePath = (0, import_node_path23.join)(cwd, configFile);
16052
16393
  if (!(0, import_node_fs10.existsSync)(absolutePath)) {
16053
16394
  configFile = "next.config.mjs";
16054
- absolutePath = (0, import_node_path22.join)(cwd, configFile);
16395
+ absolutePath = (0, import_node_path23.join)(cwd, configFile);
16055
16396
  }
16056
16397
  const source = await readSourceOrEmpty(absolutePath);
16057
16398
  const result = applyCspHeaderRecipe(source);
@@ -16063,7 +16404,7 @@ async function dispatchRecipeByGapId(gapId, cwd, explicitTarget) {
16063
16404
  }
16064
16405
  if (gapId === "missing_rate_limit") {
16065
16406
  const hasUpstash = await detectUpstash(cwd);
16066
- const middlewarePath = (0, import_node_path22.join)(cwd, "middleware.ts");
16407
+ const middlewarePath = (0, import_node_path23.join)(cwd, "middleware.ts");
16067
16408
  const source = await readSourceOrEmpty(middlewarePath);
16068
16409
  const result = applyRateLimitRecipe(source, hasUpstash);
16069
16410
  return {
@@ -16085,7 +16426,7 @@ async function dispatchRecipeByGapId(gapId, cwd, explicitTarget) {
16085
16426
  recipeName: gapId
16086
16427
  };
16087
16428
  }
16088
- const absolutePath = (0, import_node_path22.join)(cwd, configFile);
16429
+ const absolutePath = (0, import_node_path23.join)(cwd, configFile);
16089
16430
  const source = await readSourceOrEmpty(absolutePath);
16090
16431
  const result = applyEslintRestrictedImportsRecipe(source, configFile);
16091
16432
  return {
@@ -16099,13 +16440,13 @@ async function dispatchRecipeByGapId(gapId, cwd, explicitTarget) {
16099
16440
  }
16100
16441
  return null;
16101
16442
  }
16102
- var import_node_fs10, import_promises15, import_node_path22;
16443
+ var import_node_fs10, import_promises16, import_node_path23;
16103
16444
  var init_recipes = __esm({
16104
16445
  "src/heal/recipes/index.ts"() {
16105
16446
  "use strict";
16106
16447
  import_node_fs10 = require("node:fs");
16107
- import_promises15 = require("node:fs/promises");
16108
- import_node_path22 = require("node:path");
16448
+ import_promises16 = require("node:fs/promises");
16449
+ import_node_path23 = require("node:path");
16109
16450
  init_envAuthSecret();
16110
16451
  init_envNodeEnv();
16111
16452
  init_envDatabaseUrl();
@@ -16180,14 +16521,14 @@ async function applyHeal(options) {
16180
16521
  rollback: { available: false, instructions: "Recipe matched but no change was needed (already applied or file already exists)." }
16181
16522
  };
16182
16523
  }
16183
- const absoluteTarget = (0, import_node_path23.join)(options.cwd, dispatched.targetFile);
16524
+ const absoluteTarget = (0, import_node_path24.join)(options.cwd, dispatched.targetFile);
16184
16525
  assertSafeHealTarget(options.cwd, dispatched.targetFile);
16185
- await (0, import_promises16.mkdir)((0, import_node_path23.dirname)(absoluteTarget), { recursive: true });
16186
- const healDir2 = (0, import_node_path23.join)(options.cwd, ".viberaven", "heal", id);
16187
- await (0, import_promises16.mkdir)((0, import_node_path23.join)(healDir2, "before"), { recursive: true });
16188
- const beforeContent = (0, import_node_fs11.existsSync)(absoluteTarget) ? await (0, import_promises16.readFile)(absoluteTarget, "utf8") : "";
16189
- await (0, import_promises16.writeFile)((0, import_node_path23.join)(healDir2, "before", "target.txt"), beforeContent, "utf8");
16190
- await (0, import_promises16.writeFile)(absoluteTarget, dispatched.output, "utf8");
16526
+ await (0, import_promises17.mkdir)((0, import_node_path24.dirname)(absoluteTarget), { recursive: true });
16527
+ const healDir2 = (0, import_node_path24.join)(options.cwd, ".viberaven", "heal", id);
16528
+ await (0, import_promises17.mkdir)((0, import_node_path24.join)(healDir2, "before"), { recursive: true });
16529
+ const beforeContent = (0, import_node_fs11.existsSync)(absoluteTarget) ? await (0, import_promises17.readFile)(absoluteTarget, "utf8") : "";
16530
+ await (0, import_promises17.writeFile)((0, import_node_path24.join)(healDir2, "before", "target.txt"), beforeContent, "utf8");
16531
+ await (0, import_promises17.writeFile)(absoluteTarget, dispatched.output, "utf8");
16191
16532
  const patch2 = [
16192
16533
  `--- ${dispatched.targetFile}`,
16193
16534
  `+++ ${dispatched.targetFile}`,
@@ -16197,7 +16538,7 @@ async function applyHeal(options) {
16197
16538
  dispatched.output,
16198
16539
  ""
16199
16540
  ].join("\n");
16200
- await (0, import_promises16.writeFile)((0, import_node_path23.join)(healDir2, "patch.diff"), patch2, "utf8");
16541
+ await (0, import_promises17.writeFile)((0, import_node_path24.join)(healDir2, "patch.diff"), patch2, "utf8");
16201
16542
  const result2 = {
16202
16543
  $schema: "https://viberaven.dev/schemas/heal-result.schema.json",
16203
16544
  schemaVersion: "v1",
@@ -16208,7 +16549,7 @@ async function applyHeal(options) {
16208
16549
  gapId: options.gapId,
16209
16550
  recipe: dispatched.recipeName,
16210
16551
  target: dispatched.targetFile,
16211
- changedFiles: [(0, import_node_path23.relative)(options.cwd, absoluteTarget).replace(/\\/g, "/")],
16552
+ changedFiles: [(0, import_node_path24.relative)(options.cwd, absoluteTarget).replace(/\\/g, "/")],
16212
16553
  artifacts: {
16213
16554
  patch: `.viberaven/heal/${id}/patch.diff`,
16214
16555
  result: `.viberaven/heal/${id}/result.json`
@@ -16218,7 +16559,7 @@ async function applyHeal(options) {
16218
16559
  instructions: "Restore .viberaven/heal/<healId>/before/target.txt to the target file or apply the reverse patch."
16219
16560
  }
16220
16561
  };
16221
- await (0, import_promises16.writeFile)((0, import_node_path23.join)(healDir2, "result.json"), `${JSON.stringify(result2, null, 2)}
16562
+ await (0, import_promises17.writeFile)((0, import_node_path24.join)(healDir2, "result.json"), `${JSON.stringify(result2, null, 2)}
16222
16563
  `, "utf8");
16223
16564
  return result2;
16224
16565
  }
@@ -16238,7 +16579,7 @@ async function applyHeal(options) {
16238
16579
  };
16239
16580
  }
16240
16581
  const absolute = assertSafeHealTarget(options.cwd, options.target);
16241
- const before = await (0, import_promises16.readFile)(absolute, "utf8");
16582
+ const before = await (0, import_promises17.readFile)(absolute, "utf8");
16242
16583
  const recipe = applyEmptyCatchRecipe(before);
16243
16584
  if (!recipe.changed) {
16244
16585
  return {
@@ -16255,10 +16596,10 @@ async function applyHeal(options) {
16255
16596
  rollback: { available: false, instructions: "No supported heal recipe matched this file." }
16256
16597
  };
16257
16598
  }
16258
- const healDir = (0, import_node_path23.join)(options.cwd, ".viberaven", "heal", id);
16259
- await (0, import_promises16.mkdir)((0, import_node_path23.join)(healDir, "before"), { recursive: true });
16260
- await (0, import_promises16.writeFile)((0, import_node_path23.join)(healDir, "before", "target.txt"), before, "utf8");
16261
- await (0, import_promises16.writeFile)(absolute, recipe.output, "utf8");
16599
+ const healDir = (0, import_node_path24.join)(options.cwd, ".viberaven", "heal", id);
16600
+ await (0, import_promises17.mkdir)((0, import_node_path24.join)(healDir, "before"), { recursive: true });
16601
+ await (0, import_promises17.writeFile)((0, import_node_path24.join)(healDir, "before", "target.txt"), before, "utf8");
16602
+ await (0, import_promises17.writeFile)(absolute, recipe.output, "utf8");
16262
16603
  const patch = [
16263
16604
  `--- ${options.target}`,
16264
16605
  `+++ ${options.target}`,
@@ -16268,7 +16609,7 @@ async function applyHeal(options) {
16268
16609
  recipe.output,
16269
16610
  ""
16270
16611
  ].join("\n");
16271
- await (0, import_promises16.writeFile)((0, import_node_path23.join)(healDir, "patch.diff"), patch, "utf8");
16612
+ await (0, import_promises17.writeFile)((0, import_node_path24.join)(healDir, "patch.diff"), patch, "utf8");
16272
16613
  const result = {
16273
16614
  $schema: "https://viberaven.dev/schemas/heal-result.schema.json",
16274
16615
  schemaVersion: "v1",
@@ -16279,7 +16620,7 @@ async function applyHeal(options) {
16279
16620
  gapId: options.gapId,
16280
16621
  recipe: "empty-catch-safe-response",
16281
16622
  target: options.target,
16282
- changedFiles: [(0, import_node_path23.relative)(options.cwd, absolute).replace(/\\/g, "/")],
16623
+ changedFiles: [(0, import_node_path24.relative)(options.cwd, absolute).replace(/\\/g, "/")],
16283
16624
  artifacts: {
16284
16625
  patch: `.viberaven/heal/${id}/patch.diff`,
16285
16626
  result: `.viberaven/heal/${id}/result.json`
@@ -16289,17 +16630,17 @@ async function applyHeal(options) {
16289
16630
  instructions: "Restore .viberaven/heal/<healId>/before/target.txt to the target file or apply the reverse patch."
16290
16631
  }
16291
16632
  };
16292
- await (0, import_promises16.writeFile)((0, import_node_path23.join)(healDir, "result.json"), `${JSON.stringify(result, null, 2)}
16633
+ await (0, import_promises17.writeFile)((0, import_node_path24.join)(healDir, "result.json"), `${JSON.stringify(result, null, 2)}
16293
16634
  `, "utf8");
16294
16635
  return result;
16295
16636
  }
16296
- var import_promises16, import_node_fs11, import_node_path23;
16637
+ var import_promises17, import_node_fs11, import_node_path24;
16297
16638
  var init_apply = __esm({
16298
16639
  "src/heal/apply.ts"() {
16299
16640
  "use strict";
16300
- import_promises16 = require("node:fs/promises");
16641
+ import_promises17 = require("node:fs/promises");
16301
16642
  import_node_fs11 = require("node:fs");
16302
- import_node_path23 = require("node:path");
16643
+ import_node_path24 = require("node:path");
16303
16644
  init_pathSafety();
16304
16645
  init_emptyCatch();
16305
16646
  init_recipes();
@@ -16311,8 +16652,8 @@ function healId2() {
16311
16652
  return `heal_${(/* @__PURE__ */ new Date()).toISOString().replace(/\D/g, "").slice(0, 14)}`;
16312
16653
  }
16313
16654
  async function writeHealPlan(options) {
16314
- const dir = (0, import_node_path24.join)(options.cwd, ".viberaven");
16315
- await (0, import_promises17.mkdir)(dir, { recursive: true });
16655
+ const dir = (0, import_node_path25.join)(options.cwd, ".viberaven");
16656
+ await (0, import_promises18.mkdir)(dir, { recursive: true });
16316
16657
  const id = healId2();
16317
16658
  const target = options.target ?? `gap:${options.gapId}`;
16318
16659
  const markdown = [
@@ -16339,17 +16680,17 @@ async function writeHealPlan(options) {
16339
16680
  artifacts: { plan: ".viberaven/heal-plan.md" },
16340
16681
  rollback: { available: false, instructions: "No source files were changed." }
16341
16682
  };
16342
- await (0, import_promises17.writeFile)((0, import_node_path24.join)(dir, "heal-plan.md"), markdown, "utf8");
16343
- await (0, import_promises17.writeFile)((0, import_node_path24.join)(dir, "heal-plan.json"), `${JSON.stringify(result, null, 2)}
16683
+ await (0, import_promises18.writeFile)((0, import_node_path25.join)(dir, "heal-plan.md"), markdown, "utf8");
16684
+ await (0, import_promises18.writeFile)((0, import_node_path25.join)(dir, "heal-plan.json"), `${JSON.stringify(result, null, 2)}
16344
16685
  `, "utf8");
16345
16686
  return result;
16346
16687
  }
16347
- var import_promises17, import_node_path24;
16688
+ var import_promises18, import_node_path25;
16348
16689
  var init_plan = __esm({
16349
16690
  "src/heal/plan.ts"() {
16350
16691
  "use strict";
16351
- import_promises17 = require("node:fs/promises");
16352
- import_node_path24 = require("node:path");
16692
+ import_promises18 = require("node:fs/promises");
16693
+ import_node_path25 = require("node:path");
16353
16694
  init_commands();
16354
16695
  }
16355
16696
  });
@@ -16359,8 +16700,8 @@ function healId3() {
16359
16700
  return `heal_${(/* @__PURE__ */ new Date()).toISOString().replace(/\D/g, "").slice(0, 14)}`;
16360
16701
  }
16361
16702
  async function writeHealPrompt(options) {
16362
- const dir = (0, import_node_path25.join)(options.cwd, ".viberaven");
16363
- await (0, import_promises18.mkdir)(dir, { recursive: true });
16703
+ const dir = (0, import_node_path26.join)(options.cwd, ".viberaven");
16704
+ await (0, import_promises19.mkdir)(dir, { recursive: true });
16364
16705
  const id = healId3();
16365
16706
  const target = options.target ?? `gap:${options.gapId}`;
16366
16707
  const prompt = [
@@ -16387,15 +16728,15 @@ async function writeHealPrompt(options) {
16387
16728
  artifacts: { prompt: ".viberaven/heal-prompt.md" },
16388
16729
  rollback: { available: false, instructions: "No source files were changed." }
16389
16730
  };
16390
- await (0, import_promises18.writeFile)((0, import_node_path25.join)(dir, "heal-prompt.md"), prompt, "utf8");
16731
+ await (0, import_promises19.writeFile)((0, import_node_path26.join)(dir, "heal-prompt.md"), prompt, "utf8");
16391
16732
  return result;
16392
16733
  }
16393
- var import_promises18, import_node_path25;
16734
+ var import_promises19, import_node_path26;
16394
16735
  var init_prompt = __esm({
16395
16736
  "src/heal/prompt.ts"() {
16396
16737
  "use strict";
16397
- import_promises18 = require("node:fs/promises");
16398
- import_node_path25 = require("node:path");
16738
+ import_promises19 = require("node:fs/promises");
16739
+ import_node_path26 = require("node:path");
16399
16740
  init_commands();
16400
16741
  }
16401
16742
  });
@@ -16406,7 +16747,7 @@ function loopStatePath(workspaceRoot) {
16406
16747
  }
16407
16748
  async function loadLoopState(workspaceRoot) {
16408
16749
  try {
16409
- const raw = await (0, import_promises19.readFile)(loopStatePath(workspaceRoot), "utf8");
16750
+ const raw = await (0, import_promises20.readFile)(loopStatePath(workspaceRoot), "utf8");
16410
16751
  const parsed = JSON.parse(raw);
16411
16752
  if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) && typeof parsed.batchApplied === "number" && typeof parsed.lastGapCount === "number" && typeof parsed.stalledScans === "number") {
16412
16753
  const p2 = parsed;
@@ -16426,8 +16767,8 @@ async function loadLoopState(workspaceRoot) {
16426
16767
  async function saveLoopState(workspaceRoot, state) {
16427
16768
  try {
16428
16769
  const dir = (0, import_path.join)(workspaceRoot, ".viberaven");
16429
- await (0, import_promises19.mkdir)(dir, { recursive: true });
16430
- await (0, import_promises19.writeFile)(loopStatePath(workspaceRoot), JSON.stringify(state, null, 2) + "\n", "utf8");
16770
+ await (0, import_promises20.mkdir)(dir, { recursive: true });
16771
+ await (0, import_promises20.writeFile)(loopStatePath(workspaceRoot), JSON.stringify(state, null, 2) + "\n", "utf8");
16431
16772
  } catch (err) {
16432
16773
  console.warn("[VibeRaven] Could not save loop-state.json:", err instanceof Error ? err.message : String(err));
16433
16774
  }
@@ -16453,11 +16794,11 @@ function resetBatch(state, newGapCount) {
16453
16794
  appliedGapIdsSinceScan: []
16454
16795
  };
16455
16796
  }
16456
- var import_promises19, import_path, DEFAULT_LOOP_STATE;
16797
+ var import_promises20, import_path, DEFAULT_LOOP_STATE;
16457
16798
  var init_loopState = __esm({
16458
16799
  "src/loopState.ts"() {
16459
16800
  "use strict";
16460
- import_promises19 = require("fs/promises");
16801
+ import_promises20 = require("fs/promises");
16461
16802
  import_path = require("path");
16462
16803
  DEFAULT_LOOP_STATE = {
16463
16804
  batchApplied: 0,
@@ -16498,11 +16839,11 @@ var init_heal = __esm({
16498
16839
 
16499
16840
  // src/stackRecommend.ts
16500
16841
  async function recommendStack(cwd = process.cwd()) {
16501
- const pkgPath = (0, import_node_path26.join)(cwd, "package.json");
16842
+ const pkgPath = (0, import_node_path27.join)(cwd, "package.json");
16502
16843
  if (!(0, import_node_fs12.existsSync)(pkgPath)) {
16503
16844
  return DEFAULT_STACK;
16504
16845
  }
16505
- const pkg = JSON.parse(await (0, import_promises20.readFile)(pkgPath, "utf-8"));
16846
+ const pkg = JSON.parse(await (0, import_promises21.readFile)(pkgPath, "utf-8"));
16506
16847
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
16507
16848
  const names = Object.keys(deps).join(" ").toLowerCase();
16508
16849
  const rec = {
@@ -16530,13 +16871,13 @@ async function recommendStack(cwd = process.cwd()) {
16530
16871
  }
16531
16872
  return rec;
16532
16873
  }
16533
- var import_promises20, import_node_fs12, import_node_path26, DEFAULT_STACK;
16874
+ var import_promises21, import_node_fs12, import_node_path27, DEFAULT_STACK;
16534
16875
  var init_stackRecommend = __esm({
16535
16876
  "src/stackRecommend.ts"() {
16536
16877
  "use strict";
16537
- import_promises20 = require("node:fs/promises");
16878
+ import_promises21 = require("node:fs/promises");
16538
16879
  import_node_fs12 = require("node:fs");
16539
- import_node_path26 = require("node:path");
16880
+ import_node_path27 = require("node:path");
16540
16881
  DEFAULT_STACK = {
16541
16882
  frontend: "react",
16542
16883
  ui: "tailwind + shadcn/ui",
@@ -16581,7 +16922,7 @@ var init_runInit = __esm({
16581
16922
  async function checkAgentInjection(cwd) {
16582
16923
  const checks = [];
16583
16924
  for (const item4 of REQUIRED_EXISTENCE_CHECKS) {
16584
- const exists = await fileExists2((0, import_node_path27.join)(cwd, item4.file));
16925
+ const exists = await fileExists2((0, import_node_path28.join)(cwd, item4.file));
16585
16926
  checks.push({
16586
16927
  id: item4.id,
16587
16928
  status: exists ? "pass" : "fail",
@@ -16589,17 +16930,17 @@ async function checkAgentInjection(cwd) {
16589
16930
  });
16590
16931
  }
16591
16932
  for (const item4 of OPTIONAL_CURSOR_PACK_CHECKS) {
16592
- const exists = await fileExists2((0, import_node_path27.join)(cwd, item4.file));
16933
+ const exists = await fileExists2((0, import_node_path28.join)(cwd, item4.file));
16593
16934
  checks.push({
16594
16935
  id: item4.id,
16595
16936
  status: exists ? "pass" : "fail",
16596
16937
  message: exists ? `${item4.file} exists` : `Missing ${item4.file} \u2014 run npx -y viberaven init --agents all`
16597
16938
  });
16598
16939
  }
16599
- const legacyCursorPath = (0, import_node_path27.join)(cwd, ".cursor/rules/viberaven.mdc");
16940
+ const legacyCursorPath = (0, import_node_path28.join)(cwd, ".cursor/rules/viberaven.mdc");
16600
16941
  if (await fileExists2(legacyCursorPath)) {
16601
- const legacyContent = await (0, import_promises21.readFile)(legacyCursorPath, "utf-8");
16602
- const hasCoreSplit = await fileExists2((0, import_node_path27.join)(cwd, ".cursor/rules/viberaven-core.mdc"));
16942
+ const legacyContent = await (0, import_promises22.readFile)(legacyCursorPath, "utf-8");
16943
+ const hasCoreSplit = await fileExists2((0, import_node_path28.join)(cwd, ".cursor/rules/viberaven-core.mdc"));
16603
16944
  if (!hasCoreSplit || legacyContent.includes("alwaysApply: true")) {
16604
16945
  checks.push({
16605
16946
  id: "cursor-legacy-mdc",
@@ -16610,7 +16951,7 @@ async function checkAgentInjection(cwd) {
16610
16951
  }
16611
16952
  for (const target of CORE_AGENT_INJECTION_TARGETS) {
16612
16953
  const file = target === "cursor" ? ".cursor/rules/viberaven-core.mdc" : AGENT_RULE_TARGETS[target].file;
16613
- const path = (0, import_node_path27.join)(cwd, file);
16954
+ const path = (0, import_node_path28.join)(cwd, file);
16614
16955
  const exists = await fileExists2(path);
16615
16956
  if (!exists) {
16616
16957
  checks.push({
@@ -16620,7 +16961,7 @@ async function checkAgentInjection(cwd) {
16620
16961
  });
16621
16962
  continue;
16622
16963
  }
16623
- const content = await (0, import_promises21.readFile)(path, "utf-8");
16964
+ const content = await (0, import_promises22.readFile)(path, "utf-8");
16624
16965
  const hasCommand = content.includes(PUBLIC_CHECK_COMMAND);
16625
16966
  checks.push({
16626
16967
  id: `canonical-${target}`,
@@ -16654,18 +16995,18 @@ function formatDoctorAgentsReport(report) {
16654
16995
  }
16655
16996
  async function fileExists2(path) {
16656
16997
  try {
16657
- await (0, import_promises21.access)(path);
16998
+ await (0, import_promises22.access)(path);
16658
16999
  return true;
16659
17000
  } catch {
16660
17001
  return false;
16661
17002
  }
16662
17003
  }
16663
- var import_promises21, import_node_path27, REQUIRED_EXISTENCE_CHECKS, OPTIONAL_CURSOR_PACK_CHECKS, STALE_PATTERNS;
17004
+ var import_promises22, import_node_path28, REQUIRED_EXISTENCE_CHECKS, OPTIONAL_CURSOR_PACK_CHECKS, STALE_PATTERNS;
16664
17005
  var init_doctorAgents = __esm({
16665
17006
  "src/commands/doctorAgents.ts"() {
16666
17007
  "use strict";
16667
- import_promises21 = require("node:fs/promises");
16668
- import_node_path27 = require("node:path");
17008
+ import_promises22 = require("node:fs/promises");
17009
+ import_node_path28 = require("node:path");
16669
17010
  init_commands();
16670
17011
  init_agentTargets();
16671
17012
  REQUIRED_EXISTENCE_CHECKS = [
@@ -16925,39 +17266,164 @@ var init_runAudit = __esm({
16925
17266
  }
16926
17267
  });
16927
17268
 
16928
- // src/actions/render.ts
16929
- function redact(text) {
16930
- return redactString(text).replace(/\[REDACTED(?:_[A-Z_]+)?\]/g, "<redacted>").replace(
16931
- /\b([A-Z0-9_]*(?:SECRET|TOKEN|KEY|PASSWORD|DATABASE_URL|POSTGRES_URL|SERVICE_ROLE)[A-Z0-9_]*)=([^,\s]+)/gi,
16932
- "$1=<redacted>"
16933
- ).replace(/\bpostgres(?:ql)?:\/\/[^@\s]+@/gi, "postgresql://<redacted>@").replace(/[A-Za-z]:\\[^\s`"]+/g, "<repo-relative-path>").replace(/\b\/Users\/[^\s`"]+/g, "<repo-relative-path>").replace(/\b\/home\/[^\s`"]+/g, "<repo-relative-path>").replace(/\beyJ[A-Za-z0-9._-]*\b/g, "<redacted>");
16934
- }
16935
- function stringifyPayload(value) {
16936
- if (Array.isArray(value)) {
16937
- return value.map((entry) => String(entry)).join("\n");
16938
- }
16939
- if (value && typeof value === "object") {
16940
- return JSON.stringify(value, null, 2);
17269
+ // src/commands/cleanPlan.ts
17270
+ function buildContextCleanupPlan(input) {
17271
+ const items = [];
17272
+ const seen = /* @__PURE__ */ new Set();
17273
+ for (const file of input.files) {
17274
+ const path = normalizePath5((0, import_node_path29.relative)(input.projectRoot, file.path) || file.path);
17275
+ const fullPath = normalizePath5(file.path);
17276
+ const category = categorizeCleanupFile(path, fullPath, file);
17277
+ if (!category || seen.has(path)) {
17278
+ continue;
17279
+ }
17280
+ seen.add(path);
17281
+ items.push({
17282
+ path,
17283
+ category,
17284
+ action: "review-ignore-or-exclude-manually",
17285
+ reason: reasonForCategory(category)
17286
+ });
16941
17287
  }
16942
- return String(value ?? "");
17288
+ return {
17289
+ title: "VibeRaven context cleanup plan",
17290
+ warning: "VibeRaven only writes this review plan. Review each item manually before changing ignore rules or excluding artifacts from agent context.",
17291
+ items
17292
+ };
16943
17293
  }
16944
- function fenceFor(format) {
16945
- if (format === "bash" || format === "sql" || format === "json") {
16946
- return format;
16947
- }
16948
- return "txt";
17294
+ async function collectCleanupFiles(projectRoot) {
17295
+ const files = [];
17296
+ await walkCleanupFiles(projectRoot, projectRoot, files, 0);
17297
+ return files;
16949
17298
  }
16950
- function renderTarget(target) {
16951
- if (!target) {
16952
- return [];
17299
+ async function writeCleanupPlan(projectRoot, plan) {
17300
+ const outputDir = (0, import_node_path29.join)(projectRoot, ".viberaven");
17301
+ const outputPath = (0, import_node_path29.join)(outputDir, "context-cleanup.md");
17302
+ await (0, import_promises23.mkdir)(outputDir, { recursive: true });
17303
+ await (0, import_promises23.writeFile)(outputPath, renderCleanupPlan(plan), "utf8");
17304
+ return outputPath;
17305
+ }
17306
+ function categorizeCleanupFile(relativePath, fullPath, file) {
17307
+ if (relativePath.startsWith(".viberaven/") || fullPath.includes("/.viberaven/")) {
17308
+ return "generated-artifact";
16953
17309
  }
16954
- if (target.type === "url") {
16955
- return [`Open: ${redact(target.href || target.label)}`];
17310
+ if (isCachePath(relativePath)) {
17311
+ return "cache-directory";
16956
17312
  }
16957
- if (target.type === "file") {
16958
- return [`File: ${redact(target.path)}`];
17313
+ if (relativePath.endsWith(".log") && file.sizeBytes >= LARGE_LOG_BYTES) {
17314
+ return "large-log";
16959
17315
  }
16960
- if (target.type === "command") {
17316
+ return void 0;
17317
+ }
17318
+ function isCachePath(path) {
17319
+ return path === ".next/cache" || path.startsWith(".next/cache/") || path === ".turbo" || path.startsWith(".turbo/") || path === ".vite" || path.startsWith(".vite/") || path.includes("/node_modules/.cache/") || path.endsWith("/node_modules/.cache");
17320
+ }
17321
+ function reasonForCategory(category) {
17322
+ switch (category) {
17323
+ case "generated-artifact":
17324
+ return "Generated VibeRaven artifact. Keep current scan outputs, but avoid loading old reports into agent context.";
17325
+ case "large-log":
17326
+ return "Large log file that can add token noise. Review it before changing ignore rules or excluding it from agent context.";
17327
+ case "cache-directory":
17328
+ return "Build cache evidence. Do not load it into agent context unless debugging build internals.";
17329
+ }
17330
+ }
17331
+ function renderCleanupPlan(plan) {
17332
+ const lines = ["# VibeRaven context cleanup plan", "", plan.warning, ""];
17333
+ if (plan.items.length === 0) {
17334
+ lines.push("No noisy generated artifacts, large logs, or cache directories were found by the conservative scanner.");
17335
+ }
17336
+ for (const item4 of plan.items) {
17337
+ lines.push(`- ${item4.path}`);
17338
+ lines.push(` - Category: ${item4.category}`);
17339
+ lines.push(` - Action: ${item4.action}`);
17340
+ lines.push(` - Reason: ${item4.reason}`);
17341
+ }
17342
+ return `${lines.join("\n")}
17343
+ `;
17344
+ }
17345
+ async function walkCleanupFiles(root, dir, result, depth) {
17346
+ if (depth > MAX_WALK_DEPTH) {
17347
+ return;
17348
+ }
17349
+ let entries;
17350
+ try {
17351
+ entries = await (0, import_promises23.readdir)(dir, { withFileTypes: true });
17352
+ } catch {
17353
+ return;
17354
+ }
17355
+ for (const entry of entries) {
17356
+ if (SKIP_DIRECTORIES.has(entry.name)) {
17357
+ continue;
17358
+ }
17359
+ const absolute = (0, import_node_path29.join)(dir, entry.name);
17360
+ if (entry.isDirectory()) {
17361
+ if (isCachePath(normalizePath5((0, import_node_path29.relative)(root, absolute)))) {
17362
+ result.push({ path: absolute, sizeBytes: 0, kind: "directory" });
17363
+ continue;
17364
+ }
17365
+ await walkCleanupFiles(root, absolute, result, depth + 1);
17366
+ continue;
17367
+ }
17368
+ if (!entry.isFile()) {
17369
+ continue;
17370
+ }
17371
+ try {
17372
+ const fileStat = await (0, import_promises23.stat)(absolute);
17373
+ result.push({ path: absolute, sizeBytes: fileStat.size, kind: "file" });
17374
+ } catch {
17375
+ continue;
17376
+ }
17377
+ }
17378
+ }
17379
+ function normalizePath5(path) {
17380
+ return path.replace(/\\/g, "/");
17381
+ }
17382
+ var import_promises23, import_node_path29, MAX_WALK_DEPTH, LARGE_LOG_BYTES, SKIP_DIRECTORIES;
17383
+ var init_cleanPlan = __esm({
17384
+ "src/commands/cleanPlan.ts"() {
17385
+ "use strict";
17386
+ import_promises23 = require("node:fs/promises");
17387
+ import_node_path29 = require("node:path");
17388
+ MAX_WALK_DEPTH = 5;
17389
+ LARGE_LOG_BYTES = 8192;
17390
+ SKIP_DIRECTORIES = /* @__PURE__ */ new Set([".git", "node_modules"]);
17391
+ }
17392
+ });
17393
+
17394
+ // src/actions/render.ts
17395
+ function redact(text) {
17396
+ return redactString(text).replace(/\[REDACTED(?:_[A-Z_]+)?\]/g, "<redacted>").replace(
17397
+ /\b([A-Z0-9_]*(?:SECRET|TOKEN|KEY|PASSWORD|DATABASE_URL|POSTGRES_URL|SERVICE_ROLE)[A-Z0-9_]*)=([^,\s]+)/gi,
17398
+ "$1=<redacted>"
17399
+ ).replace(/\bpostgres(?:ql)?:\/\/[^@\s]+@/gi, "postgresql://<redacted>@").replace(/[A-Za-z]:\\[^\s`"]+/g, "<repo-relative-path>").replace(/\b\/Users\/[^\s`"]+/g, "<repo-relative-path>").replace(/\b\/home\/[^\s`"]+/g, "<repo-relative-path>").replace(/\beyJ[A-Za-z0-9._-]*\b/g, "<redacted>");
17400
+ }
17401
+ function stringifyPayload(value) {
17402
+ if (Array.isArray(value)) {
17403
+ return value.map((entry) => String(entry)).join("\n");
17404
+ }
17405
+ if (value && typeof value === "object") {
17406
+ return JSON.stringify(value, null, 2);
17407
+ }
17408
+ return String(value ?? "");
17409
+ }
17410
+ function fenceFor(format) {
17411
+ if (format === "bash" || format === "sql" || format === "json") {
17412
+ return format;
17413
+ }
17414
+ return "txt";
17415
+ }
17416
+ function renderTarget(target) {
17417
+ if (!target) {
17418
+ return [];
17419
+ }
17420
+ if (target.type === "url") {
17421
+ return [`Open: ${redact(target.href || target.label)}`];
17422
+ }
17423
+ if (target.type === "file") {
17424
+ return [`File: ${redact(target.path)}`];
17425
+ }
17426
+ if (target.type === "command") {
16961
17427
  return ["Run:", "```bash", redact(target.command), "```"];
16962
17428
  }
16963
17429
  if (target.type === "provider") {
@@ -17027,10 +17493,10 @@ var init_render = __esm({
17027
17493
 
17028
17494
  // src/commands/actions.ts
17029
17495
  async function runActionsCommand(input) {
17030
- const actionsPath = (0, import_node_path28.join)(getProjectArtifactsDir(input.cwd), "actions.json");
17496
+ const actionsPath = (0, import_node_path30.join)(getProjectArtifactsDir(input.cwd), "actions.json");
17031
17497
  let raw;
17032
17498
  try {
17033
- raw = await (0, import_promises22.readFile)(actionsPath, "utf8");
17499
+ raw = await (0, import_promises24.readFile)(actionsPath, "utf8");
17034
17500
  } catch {
17035
17501
  process.stderr.write("No VibeRaven actions found. Run `npx -y viberaven --agent-mode` first.\n");
17036
17502
  return 1;
@@ -17044,12 +17510,12 @@ async function runActionsCommand(input) {
17044
17510
  process.stdout.write(renderActionSurface(manifest));
17045
17511
  return 0;
17046
17512
  }
17047
- var import_promises22, import_node_path28;
17513
+ var import_promises24, import_node_path30;
17048
17514
  var init_actions = __esm({
17049
17515
  "src/commands/actions.ts"() {
17050
17516
  "use strict";
17051
- import_promises22 = require("node:fs/promises");
17052
- import_node_path28 = require("node:path");
17517
+ import_promises24 = require("node:fs/promises");
17518
+ import_node_path30 = require("node:path");
17053
17519
  init_render();
17054
17520
  init_config();
17055
17521
  }
@@ -17058,7 +17524,7 @@ var init_actions = __esm({
17058
17524
  // src/commands/verifyAction.ts
17059
17525
  async function currentActionExists(cwd, actionId) {
17060
17526
  try {
17061
- const raw = await (0, import_promises23.readFile)((0, import_node_path29.join)(getProjectArtifactsDir(cwd), "actions.json"), "utf8");
17527
+ const raw = await (0, import_promises25.readFile)((0, import_node_path31.join)(getProjectArtifactsDir(cwd), "actions.json"), "utf8");
17062
17528
  const manifest = JSON.parse(raw);
17063
17529
  return manifest.actions.some((action) => action.id === actionId);
17064
17530
  } catch {
@@ -17102,12 +17568,12 @@ Run \`npx -y viberaven --verify\` to refresh the production gate for this action
17102
17568
  );
17103
17569
  return 0;
17104
17570
  }
17105
- var import_promises23, import_node_path29;
17571
+ var import_promises25, import_node_path31;
17106
17572
  var init_verifyAction = __esm({
17107
17573
  "src/commands/verifyAction.ts"() {
17108
17574
  "use strict";
17109
- import_promises23 = require("node:fs/promises");
17110
- import_node_path29 = require("node:path");
17575
+ import_promises25 = require("node:fs/promises");
17576
+ import_node_path31 = require("node:path");
17111
17577
  init_registry2();
17112
17578
  init_config();
17113
17579
  }
@@ -17272,8 +17738,8 @@ async function runCheckCommand(options) {
17272
17738
  for (const gap of sorted) {
17273
17739
  console.log(`${SEVERITY_ICON[gap.severity]} ${gap.title} (${gap.id})`);
17274
17740
  if (!options.quiet && gap.detail) {
17275
- const firstLine = gap.detail.split("\n")[0];
17276
- console.log(` ${firstLine}`);
17741
+ const firstLine2 = gap.detail.split("\n")[0];
17742
+ console.log(` ${firstLine2}`);
17277
17743
  }
17278
17744
  }
17279
17745
  }
@@ -17304,128 +17770,1443 @@ var init_check = __esm({
17304
17770
  }
17305
17771
  });
17306
17772
 
17307
- // src/commands/cleanPlan.ts
17308
- function buildContextCleanupPlan(input) {
17309
- const items = [];
17310
- const seen = /* @__PURE__ */ new Set();
17311
- for (const file of input.files) {
17312
- const path = normalizePath5((0, import_node_path30.relative)(input.projectRoot, file.path) || file.path);
17313
- const fullPath = normalizePath5(file.path);
17314
- const category = categorizeCleanupFile(path, fullPath, file);
17315
- if (!category || seen.has(path)) {
17316
- continue;
17773
+ // src/rls/permissionsMatrix.ts
17774
+ function sha256(data) {
17775
+ return (0, import_node_crypto.createHash)("sha256").update(data).digest("hex");
17776
+ }
17777
+ function parsePermissionsMatrix(text) {
17778
+ let raw;
17779
+ try {
17780
+ raw = JSON.parse(text);
17781
+ } catch (error) {
17782
+ throw new MatrixError([`not valid JSON: ${error instanceof Error ? error.message : String(error)}`]);
17783
+ }
17784
+ const problems = [];
17785
+ if (!isObject2(raw)) throw new MatrixError(["the file must hold a JSON object"]);
17786
+ for (const key of Object.keys(raw)) {
17787
+ if (!TOP_KEYS.has(key)) problems.push(`${key}: unknown key`);
17788
+ }
17789
+ if (raw.version !== 1) problems.push(`version: must be 1 (got ${show(raw.version)})`);
17790
+ if (raw.description !== void 0 && typeof raw.description !== "string") problems.push("description: must be a string");
17791
+ let defaultGrants = "supabase-legacy";
17792
+ if (raw.defaultGrants !== void 0) {
17793
+ if (DEFAULT_GRANT_MODES.includes(raw.defaultGrants)) defaultGrants = raw.defaultGrants;
17794
+ else problems.push(`defaultGrants: must be ${quoteList(DEFAULT_GRANT_MODES)} (got ${show(raw.defaultGrants)})`);
17795
+ }
17796
+ const tables = [];
17797
+ if (!isObject2(raw.tables)) {
17798
+ problems.push('tables: must be an object keyed by table name, e.g. { "public.posts": { ... } }');
17799
+ } else if (Object.keys(raw.tables).length === 0) {
17800
+ problems.push("tables: list at least one table");
17801
+ } else {
17802
+ const seen = /* @__PURE__ */ new Set();
17803
+ for (const [key, value] of Object.entries(raw.tables)) {
17804
+ const table = parseTable(key, value, problems);
17805
+ if (!table) continue;
17806
+ const full = `${table.schema}.${table.name}`;
17807
+ if (seen.has(full)) problems.push(`tables[${show(key)}]: ${full} is listed twice`);
17808
+ seen.add(full);
17809
+ tables.push(table);
17317
17810
  }
17318
- seen.add(path);
17319
- items.push({
17320
- path,
17321
- category,
17322
- action: "review-ignore-or-exclude-manually",
17323
- reason: reasonForCategory(category)
17811
+ }
17812
+ if (problems.length > 0) throw new MatrixError(problems);
17813
+ return {
17814
+ version: 1,
17815
+ ...typeof raw.description === "string" ? { description: raw.description } : {},
17816
+ defaultGrants,
17817
+ tables,
17818
+ hash: sha256(text)
17819
+ };
17820
+ }
17821
+ function parseTable(key, value, problems) {
17822
+ const at2 = `tables[${show(key)}]`;
17823
+ const parts = key.split(".");
17824
+ const nameOk = (parts.length === 1 || parts.length === 2) && parts.every((p2) => IDENT.test(p2));
17825
+ if (!nameOk) {
17826
+ problems.push(`${at2}: table must be "name" or "schema.name" using letters, digits, _ and $`);
17827
+ return void 0;
17828
+ }
17829
+ if (!isObject2(value)) {
17830
+ problems.push(`${at2}: must be an object with "owner" and "expect"`);
17831
+ return void 0;
17832
+ }
17833
+ const before = problems.length;
17834
+ for (const k3 of Object.keys(value)) {
17835
+ if (!TABLE_KEYS.has(k3)) problems.push(`${at2}.${k3}: unknown key`);
17836
+ }
17837
+ let ownerColumn = "";
17838
+ if (value.owner === void 0) {
17839
+ problems.push(`${at2}.owner: missing; say which column holds the owner, e.g. { "column": "user_id", "is": "auth.uid()" }`);
17840
+ } else if (!isObject2(value.owner)) {
17841
+ problems.push(`${at2}.owner: must be an object like { "column": "user_id", "is": "auth.uid()" }`);
17842
+ } else {
17843
+ for (const k3 of Object.keys(value.owner)) {
17844
+ if (!OWNER_KEYS.has(k3)) problems.push(`${at2}.owner.${k3}: unknown key`);
17845
+ }
17846
+ if (typeof value.owner.column !== "string" || !IDENT.test(value.owner.column)) {
17847
+ problems.push(`${at2}.owner.column: must be a column name (got ${show(value.owner.column)})`);
17848
+ } else {
17849
+ ownerColumn = value.owner.column;
17850
+ }
17851
+ if (value.owner.is !== "auth.uid()") {
17852
+ problems.push(`${at2}.owner.is: only "auth.uid()" is supported (got ${show(value.owner.is)})`);
17853
+ }
17854
+ }
17855
+ const expect = {};
17856
+ let serviceRoleExcluded = false;
17857
+ if (!isObject2(value.expect)) {
17858
+ problems.push(`${at2}.expect: missing; list owner, other_user and anon, each with select, insert, update and delete`);
17859
+ } else {
17860
+ for (const actor of Object.keys(value.expect)) {
17861
+ if (!MATRIX_ACTORS.includes(actor)) {
17862
+ problems.push(`${at2}.expect.${actor}: unknown actor; use one of ${MATRIX_ACTORS.join(", ")}`);
17863
+ }
17864
+ }
17865
+ for (const actor of TESTED_ACTORS) {
17866
+ const cells = value.expect[actor];
17867
+ const actorAt = `${at2}.expect.${actor}`;
17868
+ if (cells === void 0) {
17869
+ problems.push(`${actorAt}: missing; list select, insert, update and delete for this actor`);
17870
+ continue;
17871
+ }
17872
+ if (!isObject2(cells)) {
17873
+ problems.push(`${actorAt}: must be an object like { "select": "deny", "insert": "deny", "update": "deny", "delete": "deny" }`);
17874
+ continue;
17875
+ }
17876
+ for (const action of Object.keys(cells)) {
17877
+ if (!ALL_ACTIONS.includes(action)) {
17878
+ problems.push(`${actorAt}.${action}: unknown action; use one of ${ALL_ACTIONS.join(", ")}`);
17879
+ }
17880
+ }
17881
+ const parsed = {};
17882
+ for (const action of MATRIX_ACTIONS) {
17883
+ const cell = cells[action];
17884
+ if (cell === void 0) problems.push(`${actorAt}.${action}: missing; write ${quoteList(CELL_VALUES)}`);
17885
+ else if (!CELL_VALUES.includes(cell)) problems.push(`${actorAt}.${action}: must be ${quoteList(CELL_VALUES)} (got ${show(cell)})`);
17886
+ else parsed[action] = cell;
17887
+ }
17888
+ if (cells.reassign !== void 0) {
17889
+ if (!CELL_VALUES.includes(cells.reassign)) problems.push(`${actorAt}.reassign: must be ${quoteList(CELL_VALUES)} (got ${show(cells.reassign)})`);
17890
+ else parsed.reassign = cells.reassign;
17891
+ }
17892
+ expect[actor] = parsed;
17893
+ }
17894
+ if (value.expect.service_role !== void 0) {
17895
+ if (value.expect.service_role === "excluded") serviceRoleExcluded = true;
17896
+ else problems.push(`${at2}.expect.service_role: must be "excluded"; the service role bypasses RLS, so it is never tested`);
17897
+ }
17898
+ }
17899
+ const seed = {};
17900
+ if (value.seed !== void 0) {
17901
+ if (!isObject2(value.seed)) {
17902
+ problems.push(`${at2}.seed: must be an object of column values, e.g. { "title": "hello" }`);
17903
+ } else {
17904
+ for (const [column, v] of Object.entries(value.seed)) {
17905
+ if (!IDENT.test(column)) problems.push(`${at2}.seed.${column}: not a column name`);
17906
+ else if (v !== null && !["string", "number", "boolean"].includes(typeof v)) problems.push(`${at2}.seed.${column}: must be a string, number, boolean or null`);
17907
+ else if (column === ownerColumn) problems.push(`${at2}.seed.${column}: the owner column is set by the test, leave it out`);
17908
+ else seed[column] = v;
17909
+ }
17910
+ }
17911
+ }
17912
+ let updateColumn;
17913
+ if (value.updateColumn !== void 0) {
17914
+ if (typeof value.updateColumn !== "string" || !IDENT.test(value.updateColumn)) problems.push(`${at2}.updateColumn: must be a column name`);
17915
+ else if (value.updateColumn === ownerColumn) problems.push(`${at2}.updateColumn: pick a column other than the owner column`);
17916
+ else updateColumn = value.updateColumn;
17917
+ }
17918
+ if (value.description !== void 0 && typeof value.description !== "string") problems.push(`${at2}.description: must be a string`);
17919
+ if (problems.length > before) return void 0;
17920
+ const [schema, name] = parts.length === 2 ? parts : ["public", parts[0]];
17921
+ return { key, schema, name, ownerColumn, expect, serviceRoleExcluded, seed, ...updateColumn ? { updateColumn } : {} };
17922
+ }
17923
+ var import_node_crypto, MATRIX_ACTORS, TESTED_ACTORS, MATRIX_ACTIONS, OPTIONAL_ACTIONS, ALL_ACTIONS, CELL_VALUES, DEFAULT_GRANT_MODES, DEFAULT_MATRIX_FILE, MatrixError, IDENT, TOP_KEYS, TABLE_KEYS, OWNER_KEYS, isObject2, show, quoteList;
17924
+ var init_permissionsMatrix = __esm({
17925
+ "src/rls/permissionsMatrix.ts"() {
17926
+ "use strict";
17927
+ import_node_crypto = require("node:crypto");
17928
+ MATRIX_ACTORS = ["owner", "other_user", "anon", "service_role"];
17929
+ TESTED_ACTORS = ["owner", "other_user", "anon"];
17930
+ MATRIX_ACTIONS = ["select", "insert", "update", "delete"];
17931
+ OPTIONAL_ACTIONS = ["reassign"];
17932
+ ALL_ACTIONS = [...MATRIX_ACTIONS, ...OPTIONAL_ACTIONS];
17933
+ CELL_VALUES = ["allow", "deny", "skip"];
17934
+ DEFAULT_GRANT_MODES = ["supabase-legacy", "none"];
17935
+ DEFAULT_MATRIX_FILE = "viberaven.permissions.json";
17936
+ MatrixError = class extends Error {
17937
+ constructor(problems) {
17938
+ super(`The permissions matrix is not valid:
17939
+ ${problems.map((p2) => ` - ${p2}`).join("\n")}`);
17940
+ this.problems = problems;
17941
+ }
17942
+ name = "MatrixError";
17943
+ };
17944
+ IDENT = /^[A-Za-z_][A-Za-z0-9_$]*$/;
17945
+ TOP_KEYS = /* @__PURE__ */ new Set(["$schema", "version", "description", "defaultGrants", "tables"]);
17946
+ TABLE_KEYS = /* @__PURE__ */ new Set(["owner", "expect", "seed", "updateColumn", "description"]);
17947
+ OWNER_KEYS = /* @__PURE__ */ new Set(["column", "is"]);
17948
+ isObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
17949
+ show = (value) => JSON.stringify(value);
17950
+ quoteList = (items) => items.length === 2 ? `"${items[0]}" or "${items[1]}"` : `${items.slice(0, -1).map((i) => `"${i}"`).join(", ")} or "${items[items.length - 1]}"`;
17951
+ }
17952
+ });
17953
+
17954
+ // src/rls/supabaseImitation.ts
17955
+ function assumptionsFor(defaultGrants) {
17956
+ return [
17957
+ defaultGrants === "supabase-legacy" ? "defaultGrants supabase-legacy: new public tables are granted to anon and authenticated, as on existing Supabase projects. Newer projects may not grant by default, so a table without RLS may be less exposed there." : "defaultGrants none: new public tables get no grants beyond what the migrations write. Existing Supabase projects do grant by default.",
17958
+ "Migrations run in file-name order as the postgres superuser, like `supabase db reset`.",
17959
+ "Each probe runs in its own transaction and is rolled back, so every cell starts from the same seeded rows."
17960
+ ];
17961
+ }
17962
+ function imitationSetupSql(defaultGrants) {
17963
+ const grants = defaultGrants === "supabase-legacy" ? `
17964
+ alter default privileges in schema public grant all on tables to anon, authenticated, service_role;
17965
+ alter default privileges in schema public grant all on sequences to anon, authenticated, service_role;
17966
+ alter default privileges in schema public grant all on functions to anon, authenticated, service_role;` : "";
17967
+ return `
17968
+ create role anon nologin noinherit;
17969
+ create role authenticated nologin noinherit;
17970
+ create role service_role nologin noinherit bypassrls;
17971
+ create role authenticator nologin noinherit;
17972
+ create role supabase_admin nologin;
17973
+ create role supabase_auth_admin nologin;
17974
+ create role supabase_storage_admin nologin;
17975
+ create role dashboard_user nologin;
17976
+ grant anon, authenticated, service_role to authenticator;
17977
+
17978
+ create schema if not exists extensions;
17979
+ grant usage on schema extensions to anon, authenticated, service_role;
17980
+
17981
+ create schema auth;
17982
+ grant usage on schema auth to anon, authenticated, service_role;
17983
+ create table auth.users (
17984
+ instance_id uuid,
17985
+ id uuid primary key,
17986
+ aud varchar(255),
17987
+ role varchar(255),
17988
+ email varchar(255) unique,
17989
+ encrypted_password varchar(255),
17990
+ email_confirmed_at timestamptz,
17991
+ phone text unique,
17992
+ raw_app_meta_data jsonb default '{}'::jsonb,
17993
+ raw_user_meta_data jsonb default '{}'::jsonb,
17994
+ is_anonymous boolean not null default false,
17995
+ created_at timestamptz default now(),
17996
+ updated_at timestamptz default now()
17997
+ );
17998
+ create function auth.jwt() returns jsonb language sql stable as $$
17999
+ select coalesce(nullif(current_setting('request.jwt.claims', true), ''), '{}')::jsonb
18000
+ $$;
18001
+ create function auth.uid() returns uuid language sql stable as $$
18002
+ select nullif(coalesce(nullif(current_setting('request.jwt.claim.sub', true), ''), auth.jwt() ->> 'sub'), '')::uuid
18003
+ $$;
18004
+ create function auth.role() returns text language sql stable as $$
18005
+ select coalesce(nullif(current_setting('request.jwt.claim.role', true), ''), auth.jwt() ->> 'role')
18006
+ $$;
18007
+ create function auth.email() returns text language sql stable as $$
18008
+ select coalesce(nullif(current_setting('request.jwt.claim.email', true), ''), auth.jwt() ->> 'email')
18009
+ $$;
18010
+ grant execute on all functions in schema auth to anon, authenticated, service_role;
18011
+
18012
+ create schema storage;
18013
+ grant usage on schema storage to anon, authenticated, service_role;
18014
+ create table storage.buckets (
18015
+ id text primary key,
18016
+ name text not null unique,
18017
+ owner uuid,
18018
+ public boolean default false,
18019
+ file_size_limit bigint,
18020
+ allowed_mime_types text[],
18021
+ created_at timestamptz default now(),
18022
+ updated_at timestamptz default now()
18023
+ );
18024
+ create table storage.objects (
18025
+ id uuid primary key default gen_random_uuid(),
18026
+ bucket_id text references storage.buckets (id),
18027
+ name text,
18028
+ owner uuid,
18029
+ owner_id text,
18030
+ metadata jsonb,
18031
+ path_tokens text[] generated always as (string_to_array(name, '/')) stored,
18032
+ created_at timestamptz default now(),
18033
+ updated_at timestamptz default now()
18034
+ );
18035
+ alter table storage.objects enable row level security;
18036
+ alter table storage.buckets enable row level security;
18037
+ create function storage.foldername(name text) returns text[] language sql immutable as $$
18038
+ select (string_to_array(name, '/'))[1:array_length(string_to_array(name, '/'), 1) - 1]
18039
+ $$;
18040
+ create function storage.filename(name text) returns text language sql immutable as $$
18041
+ select (string_to_array(name, '/'))[array_length(string_to_array(name, '/'), 1)]
18042
+ $$;
18043
+ create function storage.extension(name text) returns text language sql immutable as $$
18044
+ select reverse(split_part(reverse(storage.filename(name)), '.', 1))
18045
+ $$;
18046
+
18047
+ grant usage on schema public to anon, authenticated, service_role;
18048
+ ${grants}
18049
+ `;
18050
+ }
18051
+ function seedUsersSql() {
18052
+ return `
18053
+ insert into auth.users (id, aud, role, email) values
18054
+ ('${OWNER_ID}', 'authenticated', 'authenticated', 'owner@example.test'),
18055
+ ('${OTHER_USER_ID}', 'authenticated', 'authenticated', 'other-user@example.test');
18056
+ `;
18057
+ }
18058
+ var OWNER_ID, OTHER_USER_ID, IMITATION_EXCLUSIONS;
18059
+ var init_supabaseImitation = __esm({
18060
+ "src/rls/supabaseImitation.ts"() {
18061
+ "use strict";
18062
+ OWNER_ID = "11111111-1111-4111-8111-111111111111";
18063
+ OTHER_USER_ID = "22222222-2222-4222-8222-222222222222";
18064
+ IMITATION_EXCLUSIONS = [
18065
+ "Supabase Auth is not run: auth.uid() reads a JWT sub claim the test sets, no sign-in or token signing happens.",
18066
+ "PostgREST (the Data API) is not run: probes go straight to Postgres as the anon and authenticated roles, so API settings, exposed schemas and column selection are not tested.",
18067
+ "Storage, Realtime and Edge Functions are not modelled; storage.objects and storage.buckets exist only so migrations that mention them can apply.",
18068
+ "The live project is not read: its applied migrations, actual grants and Auth settings may differ from the repo.",
18069
+ "supabase/seed.sql is not applied.",
18070
+ "Views, functions and RPC calls are only exercised through the listed tables."
18071
+ ];
18072
+ }
18073
+ });
18074
+
18075
+ // src/rls/workerDatabase.ts
18076
+ async function createWorkerDatabase(options) {
18077
+ const worker = new import_node_worker_threads.Worker(WORKER_SOURCE, {
18078
+ eval: true,
18079
+ workerData: { pglitePath: options.pglitePath, contrib: options.contrib },
18080
+ env: { ...options.env }
18081
+ });
18082
+ const pending = /* @__PURE__ */ new Map();
18083
+ let nextId = 0;
18084
+ let dead;
18085
+ const failAll = (error) => {
18086
+ dead = dead ?? error;
18087
+ for (const [, p2] of pending) {
18088
+ clearTimeout(p2.timer);
18089
+ p2.reject(error);
18090
+ }
18091
+ pending.clear();
18092
+ };
18093
+ worker.on("message", (m2) => {
18094
+ const p2 = pending.get(m2.id);
18095
+ if (!p2) return;
18096
+ pending.delete(m2.id);
18097
+ clearTimeout(p2.timer);
18098
+ if (m2.ok) p2.resolve(m2.result);
18099
+ else p2.reject(new SqlError(m2.error?.message ?? "unknown error", m2.error?.code));
18100
+ });
18101
+ worker.on("error", (error) => failAll(error instanceof Error ? error : new Error(String(error))));
18102
+ worker.on("exit", (code) => failAll(new Error(`the PGlite worker stopped (exit ${code})`)));
18103
+ const call = (message, timeoutMs) => {
18104
+ if (dead) return Promise.reject(dead);
18105
+ const id = ++nextId;
18106
+ return new Promise((resolve9, reject) => {
18107
+ const timer = setTimeout(() => {
18108
+ const error = new RlsTimeoutError(`timed out after ${Math.round(timeoutMs / 1e3)} s`);
18109
+ failAll(error);
18110
+ void worker.terminate();
18111
+ }, timeoutMs);
18112
+ pending.set(id, { resolve: resolve9, reject, timer });
18113
+ worker.postMessage({ ...message, id });
17324
18114
  });
18115
+ };
18116
+ try {
18117
+ await call({ op: "init" }, options.startTimeoutMs ?? 6e4);
18118
+ } catch (error) {
18119
+ failAll(error instanceof Error ? error : new Error(String(error)));
18120
+ await worker.terminate();
18121
+ throw error;
17325
18122
  }
17326
18123
  return {
17327
- title: "VibeRaven context cleanup plan",
17328
- warning: "VibeRaven only writes this review plan. Review each item manually before changing ignore rules or excluding artifacts from agent context.",
17329
- items
18124
+ query: (sql, params = [], opts = {}) => call({ op: "query", sql, params }, opts.timeoutMs ?? options.statementTimeoutMs),
18125
+ exec: (sql, opts = {}) => call({ op: "exec", sql }, opts.timeoutMs ?? options.statementTimeoutMs),
18126
+ isolationReport: () => call({ op: "isolation" }, 1e4),
18127
+ close: async () => {
18128
+ if (!dead) await call({ op: "close" }, 1e4).catch(() => void 0);
18129
+ await worker.terminate();
18130
+ }
17330
18131
  };
17331
18132
  }
17332
- async function collectCleanupFiles(projectRoot) {
17333
- const files = [];
17334
- await walkCleanupFiles(projectRoot, projectRoot, files, 0);
17335
- return files;
18133
+ var import_node_module, import_node_worker_threads, NETWORK_DENIED, RlsTimeoutError, WORKER_SOURCE, SqlError, resolveFromCli, defaultPgliteLoader;
18134
+ var init_workerDatabase = __esm({
18135
+ "src/rls/workerDatabase.ts"() {
18136
+ "use strict";
18137
+ import_node_module = require("node:module");
18138
+ import_node_worker_threads = require("node:worker_threads");
18139
+ NETWORK_DENIED = "rls-test: network is denied while the runner is active";
18140
+ RlsTimeoutError = class extends Error {
18141
+ name = "RlsTimeoutError";
18142
+ };
18143
+ WORKER_SOURCE = `
18144
+ const { parentPort, workerData } = require('node:worker_threads');
18145
+ const DENIED = ${JSON.stringify(NETWORK_DENIED)};
18146
+ globalThis.fetch = async () => { throw new Error(DENIED); };
18147
+ const net = require('node:net');
18148
+ net.Socket.prototype.connect = function () { throw new Error(DENIED); };
18149
+ const { PGlite } = require(workerData.pglitePath);
18150
+ const extensions = {};
18151
+ for (const [name, path] of Object.entries(workerData.contrib)) {
18152
+ try { const mod = require(path); if (mod[name]) extensions[name] = mod[name]; } catch {}
18153
+ }
18154
+ let db;
18155
+ const reply = (id, result) => parentPort.postMessage({ id, ok: true, result });
18156
+ parentPort.on('message', async (m) => {
18157
+ try {
18158
+ if (m.op === 'init') { db = new PGlite({ extensions }); await db.waitReady; return reply(m.id, null); }
18159
+ if (m.op === 'exec') { await db.exec(m.sql); return reply(m.id, null); }
18160
+ if (m.op === 'query') { const r = await db.query(m.sql, m.params); return reply(m.id, { rows: r.rows, affectedRows: r.affectedRows }); }
18161
+ if (m.op === 'close') { await db.close(); return reply(m.id, null); }
18162
+ if (m.op === 'isolation') {
18163
+ let f = 'fetch allowed', s = 'socket allowed';
18164
+ try { await fetch('http://127.0.0.1:9/'); } catch (e) { f = String(e && e.message); }
18165
+ try { net.connect(9, '127.0.0.1'); } catch (e) { s = String(e && e.message); }
18166
+ return reply(m.id, { envKeys: Object.keys(process.env).sort(), fetch: f, socket: s });
18167
+ }
18168
+ } catch (e) {
18169
+ parentPort.postMessage({ id: m.id, ok: false, error: { code: e && typeof e.code === 'string' ? e.code : undefined, message: String((e && e.message) || e) } });
18170
+ }
18171
+ });
18172
+ `;
18173
+ SqlError = class extends Error {
18174
+ constructor(message, code) {
18175
+ super(message);
18176
+ this.code = code;
18177
+ }
18178
+ };
18179
+ resolveFromCli = (specifier) => (0, import_node_module.createRequire)(__filename).resolve(specifier);
18180
+ defaultPgliteLoader = async () => {
18181
+ const pglitePath = resolveFromCli("@electric-sql/pglite");
18182
+ const contrib = {};
18183
+ for (const name of ["uuid_ossp", "pgcrypto", "citext", "pg_trgm"]) {
18184
+ try {
18185
+ contrib[name] = resolveFromCli(`@electric-sql/pglite/contrib/${name}`);
18186
+ } catch {
18187
+ }
18188
+ }
18189
+ return { create: (options) => createWorkerDatabase({ pglitePath, contrib, ...options }) };
18190
+ };
18191
+ }
18192
+ });
18193
+
18194
+ // src/rls/rlsRunner.ts
18195
+ function sqlError(error) {
18196
+ const e2 = error;
18197
+ return { code: typeof e2?.code === "string" ? e2.code : "unknown", message: firstLine(typeof e2?.message === "string" ? e2.message : String(error)) };
18198
+ }
18199
+ function isSqlError(error) {
18200
+ return !(error instanceof RlsTimeoutError) && typeof error?.code === "string";
18201
+ }
18202
+ function displayPath(projectDir, path) {
18203
+ for (const base of [projectDir, process.cwd()]) {
18204
+ const rel = (0, import_node_path32.relative)(base, path);
18205
+ if (rel && !rel.startsWith("..") && !(0, import_node_path32.isAbsolute)(rel)) return rel.split("\\").join("/");
18206
+ }
18207
+ return path;
18208
+ }
18209
+ async function gitCommit(projectDir, paths) {
18210
+ const run = async (args) => {
18211
+ const call = await safeGitInvocation(projectDir, args);
18212
+ const { stdout } = await (0, import_node_util4.promisify)(import_node_child_process6.execFile)("git", call.args, { cwd: projectDir, env: call.env, windowsHide: true, timeout: 5e3, encoding: "utf8" });
18213
+ return String(stdout);
18214
+ };
18215
+ let sha;
18216
+ try {
18217
+ sha = (await run(["rev-parse", "HEAD"])).trim();
18218
+ if (!/^[0-9a-f]{40}$/.test(sha)) return null;
18219
+ } catch {
18220
+ return null;
18221
+ }
18222
+ let dirty = null;
18223
+ try {
18224
+ dirty = (await run(["status", "--porcelain", "--untracked-files=all", "--", ...paths])).trim().length > 0;
18225
+ } catch {
18226
+ dirty = null;
18227
+ }
18228
+ return { sha, dirty };
18229
+ }
18230
+ function allowlistedEnv(source) {
18231
+ const allowed = {};
18232
+ for (const key of ENV_ALLOWLIST) if (source[key] !== void 0) allowed[key] = source[key];
18233
+ return allowed;
18234
+ }
18235
+ async function withIsolation(fn) {
18236
+ const savedEnv = process.env;
18237
+ const savedFetch = globalThis.fetch;
18238
+ const savedConnect = import_node_net.default.Socket.prototype.connect;
18239
+ process.env = allowlistedEnv(savedEnv);
18240
+ globalThis.fetch = (async () => {
18241
+ throw new Error(NETWORK_DENIED);
18242
+ });
18243
+ import_node_net.default.Socket.prototype.connect = function denied() {
18244
+ throw new Error(NETWORK_DENIED);
18245
+ };
18246
+ try {
18247
+ return await fn();
18248
+ } finally {
18249
+ process.env = savedEnv;
18250
+ globalThis.fetch = savedFetch;
18251
+ import_node_net.default.Socket.prototype.connect = savedConnect;
18252
+ }
18253
+ }
18254
+ function valueCandidates(column, label) {
18255
+ if (column.enumLabels && column.enumLabels.length > 0) return label === "u" ? [...column.enumLabels] : [column.enumLabels[0]];
18256
+ const u2 = label === "u";
18257
+ switch (column.typname) {
18258
+ case "uuid":
18259
+ return [(0, import_node_crypto2.randomUUID)()];
18260
+ case "json":
18261
+ case "jsonb":
18262
+ return u2 ? ['{"vr":"u"}', "{}"] : ["{}"];
18263
+ case "date":
18264
+ return u2 ? ["2002-02-02", "2003-03-03"] : ["2001-01-01"];
18265
+ case "timestamp":
18266
+ case "timestamptz":
18267
+ return u2 ? ["2002-02-02 00:00:00+00", "2003-03-03 00:00:00+00"] : ["2001-01-01 00:00:00+00"];
18268
+ case "time":
18269
+ case "timetz":
18270
+ return u2 ? ["13:00:00", "14:00:00"] : ["12:00:00"];
18271
+ case "interval":
18272
+ return u2 ? ["2 days", "3 days"] : ["1 day"];
18273
+ default:
18274
+ break;
18275
+ }
18276
+ if (column.category === "S") {
18277
+ const values = [`vr-${label} rls-test value`, `vr-${label}`, label];
18278
+ return values.filter((v) => column.maxLength === null || v.length <= column.maxLength);
18279
+ }
18280
+ if (column.category === "N") return NUMBERS[label];
18281
+ if (column.category === "B") return u2 ? ["true", "false"] : ["false"];
18282
+ if (column.category === "A") return u2 ? [] : ["{}"];
18283
+ return [];
18284
+ }
18285
+ async function describeTable(db, table) {
18286
+ const display = `${table.schema}.${table.name}`;
18287
+ const rel = await db.query(
18288
+ `select c.oid::int as oid, c.relkind::text as kind from pg_class c join pg_namespace n on n.oid = c.relnamespace where n.nspname = $1 and c.relname = $2`,
18289
+ [table.schema, table.name]
18290
+ );
18291
+ if (rel.rows.length === 0) return { reason: `${display} does not exist after the migrations; check the table name in the matrix.` };
18292
+ const kind = String(rel.rows[0].kind);
18293
+ if (kind !== "r" && kind !== "p") return { reason: `${display} is not a plain table (relkind ${kind}); views and other relations are not tested yet.` };
18294
+ const oid = Number(rel.rows[0].oid);
18295
+ const cols = await db.query(
18296
+ `select a.attname::text as name, a.attnotnull as not_null, a.atthasdef as has_default, a.attidentity::text as identity,
18297
+ a.attgenerated::text as generated, t.typname::text as typname, t.typcategory::text as category,
18298
+ format_type(a.atttypid, a.atttypmod) as type,
18299
+ case when t.typname in ('varchar', 'bpchar') and a.atttypmod > 4 then a.atttypmod - 4 end as max_length,
18300
+ (select array_agg(e.enumlabel::text order by e.enumsortorder) from pg_enum e where e.enumtypid = a.atttypid) as enum_labels,
18301
+ exists (select 1 from pg_constraint k where k.conrelid = a.attrelid and k.contype = 'f' and a.attnum = any (k.conkey)) as is_fk,
18302
+ exists (select 1 from pg_index i where i.indrelid = a.attrelid and i.indisprimary and a.attnum = any (i.indkey)) as is_pk,
18303
+ exists (select 1 from pg_index i where i.indrelid = a.attrelid and i.indisunique and i.indnkeyatts = 1 and i.indkey[0] = a.attnum) as is_unique
18304
+ from pg_attribute a join pg_type t on t.oid = a.atttypid
18305
+ where a.attrelid = $1 and a.attnum > 0 and not a.attisdropped
18306
+ order by a.attnum`,
18307
+ [oid]
18308
+ );
18309
+ const unique3 = /* @__PURE__ */ new Set();
18310
+ const columns = cols.rows.map((r) => {
18311
+ if (r.is_unique) unique3.add(String(r.name));
18312
+ return {
18313
+ name: String(r.name),
18314
+ notNull: Boolean(r.not_null),
18315
+ hasDefault: Boolean(r.has_default),
18316
+ identity: String(r.identity ?? ""),
18317
+ generated: String(r.generated ?? ""),
18318
+ typname: String(r.typname),
18319
+ category: String(r.category),
18320
+ type: String(r.type),
18321
+ maxLength: r.max_length === null || r.max_length === void 0 ? null : Number(r.max_length),
18322
+ enumLabels: Array.isArray(r.enum_labels) ? r.enum_labels : null,
18323
+ isFk: Boolean(r.is_fk),
18324
+ isPk: Boolean(r.is_pk)
18325
+ };
18326
+ });
18327
+ const byName = new Map(columns.map((c) => [c.name, c]));
18328
+ const owner = byName.get(table.ownerColumn);
18329
+ if (!owner) return { reason: `${display} has no column ${table.ownerColumn} (the matrix owner column).` };
18330
+ if (owner.typname !== "uuid" && owner.category !== "S") {
18331
+ return { reason: `${display}.${table.ownerColumn} is ${owner.type}; the owner column must hold auth.uid() (uuid or text).` };
18332
+ }
18333
+ const pk = columns.filter((c) => c.isPk);
18334
+ if (pk.length === 0) return { reason: `${display} has no primary key; rls-test targets the owner's row by primary key.` };
18335
+ for (const column of Object.keys(table.seed)) {
18336
+ if (!byName.has(column)) return { reason: `the matrix seeds ${display}.${column}, which does not exist.` };
18337
+ }
18338
+ if (table.updateColumn) {
18339
+ const column = byName.get(table.updateColumn);
18340
+ if (!column) return { reason: `the matrix updateColumn ${display}.${table.updateColumn} does not exist.` };
18341
+ if (valueCandidates(column, "u").length === 0) return { reason: `the test cannot invent a new value for ${display}.${column.name} (${column.type}); pick another updateColumn.` };
18342
+ }
18343
+ const writable = (c) => c.identity === "" && c.generated === "";
18344
+ const required = columns.filter((c) => c.name !== owner.name && c.notNull && !c.hasDefault && writable(c) && !(c.name in table.seed));
18345
+ for (const column of required) {
18346
+ if (column.isFk) return { reason: `${display}.${column.name} is a NOT NULL foreign key the test cannot invent; give it a value under "seed" in the matrix.` };
18347
+ if (valueCandidates(column, "a").length === 0) {
18348
+ return { reason: `${display}.${column.name} (${column.type}) is NOT NULL and the test cannot invent a value; give it one under "seed" in the matrix.` };
18349
+ }
18350
+ }
18351
+ const seedColumns = Object.keys(table.seed);
18352
+ return {
18353
+ shape: {
18354
+ table,
18355
+ oid,
18356
+ qualified: `${quoteIdent2(table.schema)}.${quoteIdent2(table.name)}`,
18357
+ display,
18358
+ columns,
18359
+ pk,
18360
+ owner,
18361
+ ownerUnique: unique3.has(owner.name),
18362
+ ownerInPk: pk.some((c) => c.name === owner.name),
18363
+ insertColumns: [owner.name, ...seedColumns, ...required.map((c) => c.name)],
18364
+ rowValues: (label, ownerId) => [ownerId, ...seedColumns.map((c) => table.seed[c]), ...required.map((c) => valueCandidates(c, label)[0])]
18365
+ }
18366
+ };
18367
+ }
18368
+ function pkCondition(shape, firstParam) {
18369
+ return `(${shape.pk.map((c) => quoteIdent2(c.name)).join(", ")}) = (${shape.pk.map((_3, i) => `$${firstParam + i}`).join(", ")})`;
18370
+ }
18371
+ async function privilegesFor(db, shape, role) {
18372
+ const t = (await db.query(
18373
+ `select has_table_privilege($1, $2::oid, 'SELECT') as ts, has_table_privilege($1, $2::oid, 'INSERT') as ti,
18374
+ has_table_privilege($1, $2::oid, 'UPDATE') as tu, has_table_privilege($1, $2::oid, 'DELETE') as td,
18375
+ has_any_column_privilege($1, $2::oid, 'SELECT') as cs, has_any_column_privilege($1, $2::oid, 'INSERT') as ci,
18376
+ has_any_column_privilege($1, $2::oid, 'UPDATE') as cu`,
18377
+ [role, shape.oid]
18378
+ )).rows[0];
18379
+ const cols = await db.query(
18380
+ `select a.attname::text as name, has_column_privilege($1, $2::oid, a.attnum, 'SELECT') as s,
18381
+ has_column_privilege($1, $2::oid, a.attnum, 'INSERT') as i, has_column_privilege($1, $2::oid, a.attnum, 'UPDATE') as u
18382
+ from pg_attribute a where a.attrelid = $2 and a.attnum > 0 and not a.attisdropped`,
18383
+ [role, shape.oid]
18384
+ );
18385
+ return {
18386
+ table: { select: Boolean(t.ts), insert: Boolean(t.ti), update: Boolean(t.tu), delete: Boolean(t.td) },
18387
+ any: { select: Boolean(t.cs), insert: Boolean(t.ci), update: Boolean(t.cu) },
18388
+ column: new Map(cols.rows.map((r) => [String(r.name), { select: Boolean(r.s), insert: Boolean(r.i), update: Boolean(r.u) }]))
18389
+ };
18390
+ }
18391
+ async function setClaims(db, claims) {
18392
+ await db.query(
18393
+ `select set_config('request.jwt.claims', $1, true), set_config('request.jwt.claim.sub', $2, true), set_config('request.jwt.claim.role', $3, true), set_config('request.jwt.claim.email', $4, true)`,
18394
+ [JSON.stringify(claims), claims.sub ?? "", claims.role ?? "", claims.email ?? ""]
18395
+ );
18396
+ }
18397
+ async function measure(db, shape, target) {
18398
+ const owner = quoteIdent2(shape.owner.name);
18399
+ const all = (await db.query(
18400
+ `select count(*)::int as n, coalesce(md5(string_agg(row_to_json(t)::text, E'\\n' order by row_to_json(t)::text)), '') as digest,
18401
+ (count(*) filter (where ${owner}::text = $1))::int as owned
18402
+ from ${shape.qualified} t`,
18403
+ [OWNER_ID]
18404
+ )).rows[0];
18405
+ const row = (await db.query(`select row_to_json(t)::text as j, ${owner}::text as o from ${shape.qualified} t where ${pkCondition(shape, 1)}`, target)).rows[0];
18406
+ return {
18407
+ count: Number(all.n),
18408
+ digest: String(all.digest),
18409
+ owned: Number(all.owned),
18410
+ target: row ? String(row.j) : null,
18411
+ targetOwner: row && row.o !== null ? String(row.o) : null
18412
+ };
18413
+ }
18414
+ async function runStatement(db, shape, target, stmt, session, prep) {
18415
+ await db.exec("begin");
18416
+ try {
18417
+ if (prep) await prep();
18418
+ const before = await measure(db, shape, target);
18419
+ await db.exec("savepoint vr_probe");
18420
+ let identity;
18421
+ if (session) {
18422
+ await db.exec(`set local role ${session.role}`);
18423
+ await setClaims(db, session.claims);
18424
+ const who = (await db.query(`select current_user::text as u, current_setting('is_superuser') as su, (select rolbypassrls from pg_roles where rolname = current_user) as b`)).rows[0];
18425
+ identity = { user: String(who.u), superuser: who.su === "on", bypass: Boolean(who.b) };
18426
+ }
18427
+ const run = { identity };
18428
+ try {
18429
+ const r = await db.query(stmt.sql, stmt.params);
18430
+ run.rows = r.rows;
18431
+ run.affected = r.affectedRows ?? 0;
18432
+ } catch (error) {
18433
+ if (!isSqlError(error)) throw error;
18434
+ run.error = sqlError(error);
18435
+ }
18436
+ if (!run.error && stmt.select) {
18437
+ await db.exec("savepoint vr_count");
18438
+ try {
18439
+ run.visible = Number((await db.query(`select count(*)::int as n from ${shape.qualified}`)).rows[0].n);
18440
+ await db.exec("release savepoint vr_count");
18441
+ } catch (error) {
18442
+ if (!isSqlError(error)) throw error;
18443
+ await db.exec("rollback to savepoint vr_count");
18444
+ }
18445
+ }
18446
+ if (run.error) await db.exec("rollback to savepoint vr_probe");
18447
+ else await db.exec("reset role");
18448
+ const after = await measure(db, shape, target);
18449
+ return { ...run, before, after };
18450
+ } finally {
18451
+ await db.exec("rollback");
18452
+ }
18453
+ }
18454
+ function writeControl(filtered, changed) {
18455
+ return (run) => {
18456
+ if (run.error) return { ok: false, detail: `as the superuser the same statement failed: ${run.error.message} (${run.error.code})` };
18457
+ if (!changed(run)) {
18458
+ return {
18459
+ ok: false,
18460
+ detail: filtered ? `as the superuser the same statement changed ${plural2(run.affected ?? 0, "row")}, expected 1` : "as the superuser the same statement did not change the owner row"
18461
+ };
18462
+ }
18463
+ return { ok: true, detail: `as the superuser the same statement changed ${plural2(filtered ? run.affected ?? 0 : 1, "row")}` };
18464
+ };
18465
+ }
18466
+ async function differingValue(db, shape, target, column) {
18467
+ for (const candidate of valueCandidates(column, "u")) {
18468
+ try {
18469
+ const r = await db.query(
18470
+ `select ($${target.length + 1}::${column.type}) is distinct from ${quoteIdent2(column.name)} as d from ${shape.qualified} where ${pkCondition(shape, 1)}`,
18471
+ [...target, candidate]
18472
+ );
18473
+ if (r.rows[0]?.d === true) return candidate;
18474
+ } catch (error) {
18475
+ if (!isSqlError(error)) throw error;
18476
+ }
18477
+ }
18478
+ return void 0;
18479
+ }
18480
+ async function selectPlans(c) {
18481
+ const { shape, target, privileges } = c;
18482
+ const pkSelectable = shape.pk.every((col) => privileges.column.get(col.name)?.select);
18483
+ const noPrivilege = !privileges.table.select && !privileges.any.select;
18484
+ const privilegeDenied = (run) => isPrivilegeRefusal(run) && noPrivilege ? { verdict: "denied", detail: `refused: ${run.error.message} (42501), no SELECT on any column` } : void 0;
18485
+ if (pkSelectable || noPrivilege) {
18486
+ const pkList = shape.pk.map((col) => `${quoteIdent2(col.name)}::text`).join(", ");
18487
+ return [
18488
+ {
18489
+ stmt: { name: "select", sql: `select ${pkList} from ${shape.qualified} where ${pkCondition(shape, 1)}`, params: target, display: `select the owner's row from ${shape.display} by primary key`, filtered: true, select: "pk" },
18490
+ controlOk: (run) => run.error ? { ok: false, detail: `as the superuser the same statement failed: ${run.error.message} (${run.error.code})` } : run.rows?.length === 1 ? { ok: true, detail: "as the superuser the same statement returned 1 row" } : { ok: false, detail: `as the superuser the same statement returned ${plural2(run.rows?.length ?? 0, "row")}, expected 1` },
18491
+ judge: (run) => {
18492
+ const denied = privilegeDenied(run);
18493
+ if (denied) return denied;
18494
+ if (run.error) return { verdict: "unknown", detail: `error ${run.error.code}: ${run.error.message}` };
18495
+ const sees = run.visible === void 0 ? "" : ` (sees ${run.visible} of ${run.before.count} rows)`;
18496
+ if (run.rows?.length === 1) return { verdict: "allowed", detail: `returned the owner row${sees}` };
18497
+ if (run.rows?.length === 0) return { verdict: "denied", detail: `returned no row${sees}` };
18498
+ return { verdict: "unknown", detail: `returned ${run.rows?.length} rows for one primary key` };
18499
+ }
18500
+ }
18501
+ ];
18502
+ }
18503
+ const granted = shape.columns.filter((col) => privileges.column.get(col.name)?.select);
18504
+ const cols = granted.map((col) => `${quoteIdent2(col.name)}::text as ${quoteIdent2(col.name)}`).join(", ");
18505
+ const all = (await c.db.query(`select ${cols}, (${pkCondition(shape, 1)}) as is_target from ${shape.qualified}`, target)).rows;
18506
+ const key = (row) => JSON.stringify(granted.map((col) => row[col.name] ?? null));
18507
+ const targetRow = all.find((r) => r.is_target === true);
18508
+ const targetKey = targetRow ? key(targetRow) : void 0;
18509
+ const unique3 = targetKey !== void 0 && all.filter((r) => key(r) === targetKey).length === 1;
18510
+ const found = (run) => Boolean(targetKey && run.rows?.some((r) => key(r) === targetKey));
18511
+ const names = granted.map((col) => col.name).join(", ");
18512
+ return [
18513
+ {
18514
+ stmt: { name: "select columns", sql: `select ${cols} from ${shape.qualified}`, params: [], display: `select the granted columns (${names}) of ${shape.display}, no row filter`, filtered: false, select: "columns" },
18515
+ controlOk: (run) => found(run) ? { ok: true, detail: "as the superuser the same statement returned the owner row" } : { ok: false, detail: "as the superuser the same statement did not return the owner row" },
18516
+ judge: (run) => {
18517
+ if (run.error) return { verdict: "unknown", detail: `error ${run.error.code}: ${run.error.message}` };
18518
+ if (!unique3) return { verdict: "unknown", detail: `the granted columns (${names}) do not uniquely identify the owner row` };
18519
+ if (found(run)) return { verdict: "allowed", detail: `read the owner row through the granted columns (${names})` };
18520
+ return { verdict: "denied", detail: `the owner row is not among the rows returned through the granted columns (${names})` };
18521
+ }
18522
+ }
18523
+ ];
18524
+ }
18525
+ function insertPlans(c) {
18526
+ const { shape, privileges, db } = c;
18527
+ const values = shape.rowValues("i", OWNER_ID);
18528
+ const placeholders = values.map((_3, i) => `$${i + 1}`).join(", ");
18529
+ const noPrivilege = !privileges.table.insert && !privileges.any.insert;
18530
+ const owner = quoteIdent2(shape.owner.name);
18531
+ const prep = shape.ownerUnique ? async () => void await db.query(`delete from ${shape.qualified} where ${owner}::text = $1`, [OWNER_ID]) : void 0;
18532
+ const added = (run) => run.after.owned === run.before.owned + 1 && run.after.count === run.before.count + 1;
18533
+ return [
18534
+ {
18535
+ stmt: {
18536
+ name: "insert",
18537
+ sql: `insert into ${shape.qualified} (${shape.insertColumns.map(quoteIdent2).join(", ")}) values (${placeholders})`,
18538
+ params: values,
18539
+ display: `insert into ${shape.display} a row whose ${shape.owner.name} is the owner`,
18540
+ filtered: true
18541
+ },
18542
+ prep,
18543
+ controlOk: (run) => {
18544
+ if (run.error) return { ok: false, detail: `as the superuser the same statement failed: ${run.error.message} (${run.error.code})` };
18545
+ return added(run) ? { ok: true, detail: "as the superuser the same statement changed 1 row" } : { ok: false, detail: `as the superuser the same statement changed ${plural2(run.affected ?? 0, "row")}, expected 1` };
18546
+ },
18547
+ judge: (run) => {
18548
+ if (isRlsRefusal(run)) return { verdict: "unknown", detail: `refused by WITH CHECK: ${run.error.message} (42501). Only the generated row was tried; other values may allow insertion` };
18549
+ if (isPrivilegeRefusal(run)) {
18550
+ return noPrivilege ? { verdict: "denied", detail: `refused: ${run.error.message} (42501), no INSERT on any column` } : { verdict: "unknown", detail: `refused for missing column privileges (${run.error.message}), but the role can insert some columns` };
18551
+ }
18552
+ if (run.error) return { verdict: "unknown", detail: `error ${run.error.code}: ${run.error.message}` };
18553
+ if (added(run)) return { verdict: "allowed", detail: `1 row added for the owner (${run.before.count} to ${run.after.count} rows)` };
18554
+ if ((run.affected ?? 0) === 0 && run.after.digest === run.before.digest) return { verdict: "denied", detail: "0 rows added, table unchanged" };
18555
+ return {
18556
+ verdict: "unknown",
18557
+ detail: `reported ${plural2(run.affected ?? 0, "row")}, but the table went from ${run.before.count} to ${run.after.count} rows and the owner's rows from ${run.before.owned} to ${run.after.owned}`
18558
+ };
18559
+ }
18560
+ }
18561
+ ];
18562
+ }
18563
+ function freeOtherUserSlot(db, shape) {
18564
+ if (!shape.ownerUnique) return void 0;
18565
+ return async () => void await db.query(`delete from ${shape.qualified} where ${quoteIdent2(shape.owner.name)}::text = $1`, [OTHER_USER_ID]);
18566
+ }
18567
+ function pickUpdateColumn(shape, privileges) {
18568
+ if (shape.table.updateColumn) return shape.columns.find((col) => col.name === shape.table.updateColumn);
18569
+ const candidates = shape.columns.filter(
18570
+ (col) => col.name !== shape.owner.name && !col.isPk && !col.isFk && col.identity === "" && col.generated === "" && col.category !== "A" && valueCandidates(col, "u").length > 0
18571
+ );
18572
+ if (!privileges.any.update) return candidates[0];
18573
+ return candidates.find((col) => privileges.column.get(col.name)?.update);
18574
+ }
18575
+ async function updatePlans(c) {
18576
+ const { db, shape, target, privileges, actor } = c;
18577
+ const pkSelectable = shape.pk.every((col) => privileges.column.get(col.name)?.select);
18578
+ const noPrivilege = !privileges.table.update && !privileges.any.update;
18579
+ const filtered = pkSelectable || noPrivilege;
18580
+ const where = filtered ? ` where ${pkCondition(shape, 1)}` : "";
18581
+ const baseParams = filtered ? target : [];
18582
+ const ownerUpdatable = Boolean(privileges.column.get(shape.owner.name)?.update) || noPrivilege;
18583
+ const column = pickUpdateColumn(shape, privileges);
18584
+ const value = column ? await differingValue(db, shape, target, column) : void 0;
18585
+ const plans = [];
18586
+ const add = (name, sets, display, noop = false, prep) => {
18587
+ let n = baseParams.length;
18588
+ const assignments = sets.map((s) => `${quoteIdent2(s.column)} = ${s.value === SELF ? quoteIdent2(s.column) : `$${++n}`}`).join(", ");
18589
+ const sql = `update ${shape.qualified} set ${assignments}${where}`;
18590
+ const params = [...baseParams, ...sets.filter((s) => s.value !== SELF).map((s) => s.value)];
18591
+ const changed = (run) => noop ? (run.affected ?? 0) >= 1 && run.after.target !== null : run.after.target !== null && run.after.target !== run.before.target;
18592
+ plans.push({
18593
+ stmt: { name, sql, params, display: `${display}${filtered ? "" : ", no row filter (the role cannot read the primary key)"}`, filtered },
18594
+ prep,
18595
+ controlOk: writeControl(filtered, changed),
18596
+ judge: (run) => {
18597
+ if (isRlsRefusal(run)) return { verdict: "unknown", detail: `refused by WITH CHECK: ${run.error.message}. USING matched the owner row, so this is not a denial` };
18598
+ if (isPrivilegeRefusal(run)) {
18599
+ return noPrivilege ? { verdict: "denied", detail: `refused: ${run.error.message} (42501), no UPDATE on any column` } : { verdict: "unknown", detail: `refused for column privileges (${run.error.message}), but the role can update some columns` };
18600
+ }
18601
+ if (run.error) return { verdict: "unknown", detail: `error ${run.error.code}: ${run.error.message}` };
18602
+ if (changed(run)) return { verdict: "allowed", detail: noop ? "1 row updated (rewrote its own value)" : `${changedRows(filtered ? run.affected : 1)} (${sets.map((s) => s.column).join(" and ")} rewritten)` };
18603
+ if (!filtered) return { verdict: "denied", detail: "the owner row is unchanged" };
18604
+ if ((run.affected ?? 0) === 0 && run.after.digest === run.before.digest) return { verdict: "denied", detail: "0 rows changed, the row is still there and unchanged" };
18605
+ return { verdict: "unknown", detail: `reported ${plural2(run.affected ?? 0, "row")}, but the owner row ${run.after.target === null ? "is gone" : "did not change"} and the table ${run.after.digest === run.before.digest ? "did not" : "did"} change` };
18606
+ }
18607
+ });
18608
+ };
18609
+ if (column && value !== void 0) add("update value", [{ column: column.name, value }], `update ${column.name} on the owner's row in ${shape.display}`);
18610
+ if (actor === "other_user" && ownerUpdatable && !shape.ownerInPk) {
18611
+ const sets = [{ column: shape.owner.name, value: OTHER_USER_ID }];
18612
+ if (column && value !== void 0) sets.push({ column: column.name, value });
18613
+ add(
18614
+ "update claiming the row",
18615
+ sets,
18616
+ `update the owner's row setting ${shape.owner.name} to the other user${column && value !== void 0 ? ` and ${column.name}` : ""}`,
18617
+ false,
18618
+ freeOtherUserSlot(db, shape)
18619
+ );
18620
+ }
18621
+ if (plans.length === 0) {
18622
+ add("update noop", [{ column: shape.owner.name, value: SELF }], `update the owner's row in ${shape.display} (${shape.owner.name} = itself)`, true);
18623
+ }
18624
+ return plans;
18625
+ }
18626
+ function reassignPlans(c) {
18627
+ const { db, shape, target, privileges, actor } = c;
18628
+ const pkSelectable = shape.pk.every((col) => privileges.column.get(col.name)?.select);
18629
+ const ownerUpdatable = Boolean(privileges.column.get(shape.owner.name)?.update);
18630
+ const noPrivilege = !ownerUpdatable;
18631
+ const filtered = pkSelectable || noPrivilege;
18632
+ const where = filtered ? ` where ${pkCondition(shape, 1)}` : "";
18633
+ const baseParams = filtered ? target : [];
18634
+ const owner = quoteIdent2(shape.owner.name);
18635
+ const moved = (run) => run.after.targetOwner === OTHER_USER_ID;
18636
+ const stmt = {
18637
+ name: "reassign",
18638
+ sql: `update ${shape.qualified} set ${owner} = $${baseParams.length + 1}${where}`,
18639
+ params: [...baseParams, OTHER_USER_ID],
18640
+ display: `set ${shape.owner.name} on the owner's row in ${shape.display} to the other user${filtered ? "" : ", no row filter"}`,
18641
+ filtered
18642
+ };
18643
+ return [
18644
+ {
18645
+ stmt,
18646
+ prep: freeOtherUserSlot(db, shape),
18647
+ controlOk: writeControl(filtered, moved),
18648
+ judge: (run) => {
18649
+ if (isRlsRefusal(run)) return { verdict: "unknown", detail: `refused by WITH CHECK: ${run.error.message} (42501). A transfer combined with other values was not tested, so this may not be about the owner change alone` };
18650
+ if (isPrivilegeRefusal(run)) {
18651
+ return noPrivilege ? { verdict: "denied", detail: `refused: ${run.error.message} (42501), no UPDATE on ${shape.owner.name}` } : { verdict: "unknown", detail: `refused for privileges (${run.error.message}), but the role can update ${shape.owner.name}` };
18652
+ }
18653
+ if (run.error) return { verdict: "unknown", detail: `error ${run.error.code}: ${run.error.message}` };
18654
+ if (moved(run)) return { verdict: "allowed", detail: `the owner row now belongs to the other user` };
18655
+ if (!filtered && run.after.target === run.before.target) return { verdict: "denied", detail: "the owner row is unchanged" };
18656
+ if ((run.affected ?? 0) === 0 && run.after.digest === run.before.digest) return { verdict: "denied", detail: "0 rows changed, the row is still there and unchanged" };
18657
+ return { verdict: "unknown", detail: `reported ${plural2(run.affected ?? 0, "row")}, but the owner column did not move` };
18658
+ }
18659
+ }
18660
+ ];
17336
18661
  }
17337
- async function writeCleanupPlan(projectRoot, plan) {
17338
- const outputDir = (0, import_node_path30.join)(projectRoot, ".viberaven");
17339
- const outputPath = (0, import_node_path30.join)(outputDir, "context-cleanup.md");
17340
- await (0, import_promises24.mkdir)(outputDir, { recursive: true });
17341
- await (0, import_promises24.writeFile)(outputPath, renderCleanupPlan(plan), "utf8");
17342
- return outputPath;
18662
+ function deletePlans(c) {
18663
+ const { shape, target, privileges } = c;
18664
+ const pkSelectable = shape.pk.every((col) => privileges.column.get(col.name)?.select);
18665
+ const noPrivilege = !privileges.table.delete;
18666
+ const filtered = pkSelectable || noPrivilege;
18667
+ const gone = (run) => run.after.target === null && run.before.target !== null;
18668
+ return [
18669
+ {
18670
+ stmt: {
18671
+ name: "delete",
18672
+ sql: `delete from ${shape.qualified}${filtered ? ` where ${pkCondition(shape, 1)}` : ""}`,
18673
+ params: filtered ? target : [],
18674
+ display: `delete the owner's row from ${shape.display}${filtered ? "" : ", no row filter (the role cannot read the primary key)"}`,
18675
+ filtered
18676
+ },
18677
+ controlOk: writeControl(filtered, gone),
18678
+ judge: (run) => {
18679
+ if (isPrivilegeRefusal(run) && noPrivilege) return { verdict: "denied", detail: `refused: ${run.error.message} (42501), no DELETE privilege` };
18680
+ if (run.error) return { verdict: "unknown", detail: `error ${run.error.code}: ${run.error.message}` };
18681
+ if (gone(run)) return { verdict: "allowed", detail: `${filtered ? "1 row deleted" : "deleted"}, the owner row is gone (${run.before.count} to ${run.after.count} rows)` };
18682
+ if (!filtered) return { verdict: "denied", detail: "the owner row is still there" };
18683
+ if ((run.affected ?? 0) === 0 && run.after.digest === run.before.digest) return { verdict: "denied", detail: "0 rows changed, the row is still there and unchanged" };
18684
+ return {
18685
+ verdict: "unknown",
18686
+ detail: `reported ${plural2(run.affected ?? 0, "row")}, but the owner row is ${run.after.target === null ? "gone" : "still there"} and the table ${run.after.digest === run.before.digest ? "did not" : "did"} change`
18687
+ };
18688
+ }
18689
+ }
18690
+ ];
17343
18691
  }
17344
- function categorizeCleanupFile(relativePath, fullPath, file) {
17345
- if (relativePath.startsWith(".viberaven/") || fullPath.includes("/.viberaven/")) {
17346
- return "generated-artifact";
17347
- }
17348
- if (isCachePath(relativePath)) {
17349
- return "cache-directory";
18692
+ async function runCell(ctx, privilegesByRole, actor, action, expected) {
18693
+ const base = { table: ctx.shape.display, actor, action, expected };
18694
+ if (expected === "skip") return { ...base, outcome: "N/A", observed: { summary: "not tested (skip in the matrix)" }, reason: "The matrix marks this cell skip." };
18695
+ const session = ACTOR_SESSION[actor];
18696
+ if (action === "reassign" && ctx.shape.ownerInPk) {
18697
+ const reason2 = `${ctx.shape.owner.name} is both the owner column and the primary key, so moving a row to another user changes which row it is; reassign is not tested on this table.`;
18698
+ return { ...base, outcome: "Unknown", observed: { summary: "not tested: the owner column is the primary key" }, reason: reason2 };
18699
+ }
18700
+ const c = { ...ctx, actor, privileges: privilegesByRole.get(session.role) };
18701
+ const plans = action === "select" ? await selectPlans(c) : action === "insert" ? insertPlans(c) : action === "update" ? await updatePlans(c) : action === "reassign" ? reassignPlans(c) : deletePlans(c);
18702
+ const records = [];
18703
+ let decisive;
18704
+ let firstRun;
18705
+ for (const plan of plans) {
18706
+ const control = plan.controlOk(await runStatement(ctx.db, ctx.shape, ctx.target, plan.stmt, null, plan.prep));
18707
+ const run = await runStatement(ctx.db, ctx.shape, ctx.target, plan.stmt, session, plan.prep);
18708
+ firstRun = firstRun ?? run;
18709
+ let judged;
18710
+ const id = run.identity;
18711
+ if (!id || id.superuser || id.bypass || id.user !== session.role) {
18712
+ judged = { verdict: "unknown", detail: `the probe ran as ${id?.user} (superuser ${id?.superuser}, BYPASSRLS ${id?.bypass}), so RLS may not have applied` };
18713
+ } else if (!control.ok) {
18714
+ judged = { verdict: "unknown", detail: `positive control failed: ${control.detail}` };
18715
+ } else {
18716
+ judged = plan.judge(run);
18717
+ }
18718
+ const record = { name: plan.stmt.name, statement: plan.stmt.display, verdict: judged.verdict, detail: judged.detail, affectedRows: run.affected, ...run.error ? { error: run.error } : {}, control };
18719
+ records.push(record);
18720
+ if (judged.verdict === "allowed" && !decisive) decisive = { run, record };
18721
+ }
18722
+ let verdict;
18723
+ if (decisive) verdict = "allowed";
18724
+ else if (records.length > 0 && records.every((r2) => r2.verdict === "denied")) verdict = "denied";
18725
+ else verdict = "unknown";
18726
+ const shown = decisive ?? { run: firstRun, record: records.find((r2) => r2.verdict !== "denied") ?? records[0] };
18727
+ const r = shown.run;
18728
+ const observed = {
18729
+ statement: shown.record.statement,
18730
+ role: session.role,
18731
+ currentUser: r.identity?.user,
18732
+ superuser: r.identity?.superuser,
18733
+ bypassRls: r.identity?.bypass,
18734
+ rowsReturned: r.rows && plans[0].stmt.select === "pk" ? r.rows.length : void 0,
18735
+ visibleRows: r.visible,
18736
+ affectedRows: plans[0].stmt.select ? void 0 : r.affected,
18737
+ ...r.error ? { error: r.error } : {},
18738
+ rowCountBefore: r.before.count,
18739
+ rowCountAfter: r.after.count,
18740
+ stateChanged: r.after.digest !== r.before.digest,
18741
+ targetPresentAfter: r.after.target !== null,
18742
+ control: shown.record.control,
18743
+ probes: records,
18744
+ ...action === "insert" && ctx.shape.ownerUnique ? { note: `${ctx.shape.owner.name} allows one row per user, so the owner's existing row was removed inside the rolled-back probe transaction first` } : {},
18745
+ summary: verdict === "unknown" ? records.filter((x2) => x2.verdict !== "denied").map((x2) => x2.detail).join("; ") : shown.record.detail
18746
+ };
18747
+ if (verdict === "unknown") {
18748
+ return { ...base, outcome: "Unknown", observed, reason: `Not settled by the evidence: ${observed.summary}.` };
17350
18749
  }
17351
- if (relativePath.endsWith(".log") && file.sizeBytes >= LARGE_LOG_BYTES) {
17352
- return "large-log";
18750
+ const allowed = verdict === "allowed";
18751
+ const want = expected === "allow";
18752
+ const pass = allowed === want;
18753
+ const actorText = actor === "other_user" ? "another signed-in user" : actor === "anon" ? "anon" : "the owner";
18754
+ const reason = pass ? `Expected ${expected}, observed ${allowed ? "allowed" : "denied"}.` : want ? `Expected ${actorText} to be allowed to ${action}, but ${observed.summary}.${action !== "select" && !r.error ? " Postgres reports rows no policy allows as 0 rows, not as an error, so the app sees no failure." : ""}` : `Expected ${actorText} to be denied ${action}, but ${observed.summary}.`;
18755
+ return { ...base, outcome: pass ? "Pass" : "Fail", observed, reason };
18756
+ }
18757
+ function cellsFor(table) {
18758
+ const out = [];
18759
+ for (const actor of TESTED_ACTORS) {
18760
+ for (const action of MATRIX_ACTIONS) out.push({ actor, action, expected: table.expect[actor][action] });
18761
+ const reassign = table.expect[actor].reassign;
18762
+ if (reassign) out.push({ actor, action: "reassign", expected: reassign });
17353
18763
  }
17354
- return void 0;
18764
+ return out;
17355
18765
  }
17356
- function isCachePath(path) {
17357
- return path === ".next/cache" || path.startsWith(".next/cache/") || path === ".turbo" || path.startsWith(".turbo/") || path === ".vite" || path.startsWith(".vite/") || path.includes("/node_modules/.cache/") || path.endsWith("/node_modules/.cache");
18766
+ function unknownCells(table, reason, summary) {
18767
+ const display = `${table.schema}.${table.name}`;
18768
+ const cells = cellsFor(table).map(
18769
+ ({ actor, action, expected }) => expected === "skip" ? { table: display, actor, action, expected, outcome: "N/A", observed: { summary: "not tested (skip in the matrix)" }, reason: "The matrix marks this cell skip." } : { table: display, actor, action, expected, outcome: "Unknown", observed: { summary }, reason }
18770
+ );
18771
+ return [...cells, ...serviceRoleCells(table)];
18772
+ }
18773
+ function serviceRoleCells(table) {
18774
+ if (!table.serviceRoleExcluded) return [];
18775
+ const display = `${table.schema}.${table.name}`;
18776
+ return MATRIX_ACTIONS.map((action) => ({
18777
+ table: display,
18778
+ actor: "service_role",
18779
+ action,
18780
+ expected: "excluded",
18781
+ outcome: "N/A",
18782
+ observed: { summary: "excluded: the service role bypasses RLS by design" },
18783
+ reason: "The matrix excludes the service role."
18784
+ }));
17358
18785
  }
17359
- function reasonForCategory(category) {
17360
- switch (category) {
17361
- case "generated-artifact":
17362
- return "Generated VibeRaven artifact. Keep current scan outputs, but avoid loading old reports into agent context.";
17363
- case "large-log":
17364
- return "Large log file that can add token noise. Review it before changing ignore rules or excluding it from agent context.";
17365
- case "cache-directory":
17366
- return "Build cache evidence. Do not load it into agent context unless debugging build internals.";
17367
- }
18786
+ function summarise(tables) {
18787
+ const cells = tables.flatMap((t) => t.cells);
18788
+ const count = (o2) => cells.filter((c) => c.outcome === o2).length;
18789
+ const notApplicable = count("N/A");
18790
+ return { pass: count("Pass"), fail: count("Fail"), unknown: count("Unknown"), notApplicable, tested: cells.length - notApplicable, total: cells.length };
17368
18791
  }
17369
- function renderCleanupPlan(plan) {
17370
- const lines = ["# VibeRaven context cleanup plan", "", plan.warning, ""];
17371
- if (plan.items.length === 0) {
17372
- lines.push("No noisy generated artifacts, large logs, or cache directories were found by the conservative scanner.");
18792
+ function verdictFor(summary) {
18793
+ if (summary.fail > 0) {
18794
+ return {
18795
+ verdict: `Not ready within tested scope: ${plural2(summary.fail, "cell does", "cells do")} not match the matrix; ${plural2(summary.unknown, "unknown")}.`,
18796
+ exitCode: 1
18797
+ };
17373
18798
  }
17374
- for (const item4 of plan.items) {
17375
- lines.push(`- ${item4.path}`);
17376
- lines.push(` - Category: ${item4.category}`);
17377
- lines.push(` - Action: ${item4.action}`);
17378
- lines.push(` - Reason: ${item4.reason}`);
18799
+ if (summary.pass === 0) {
18800
+ return { verdict: `Nothing was observed: ${plural2(summary.unknown, "unknown")}, ${summary.notApplicable} N/A.`, exitCode: 2 };
17379
18801
  }
17380
- return `${lines.join("\n")}
17381
- `;
18802
+ if (summary.unknown > 0) {
18803
+ return { verdict: `No blockers found within tested scope; ${plural2(summary.unknown, "unknown")}.`, exitCode: 2 };
18804
+ }
18805
+ return { verdict: "No blockers found within tested scope; 0 unknowns. Scenario passed locally.", exitCode: 0 };
17382
18806
  }
17383
- async function walkCleanupFiles(root, dir, result, depth) {
17384
- if (depth > MAX_WALK_DEPTH) {
17385
- return;
18807
+ async function listMigrations(dir) {
18808
+ try {
18809
+ const entries = await (0, import_promises26.readdir)(dir, { withFileTypes: true });
18810
+ return entries.filter((e2) => e2.isFile() && /\.sql$/i.test(e2.name) && !e2.name.startsWith(".")).map((e2) => e2.name).sort((a, b3) => a < b3 ? -1 : a > b3 ? 1 : 0);
18811
+ } catch {
18812
+ return [];
17386
18813
  }
17387
- let entries;
18814
+ }
18815
+ async function runRlsTest(options) {
18816
+ const projectDir = (0, import_node_path32.resolve)(options.projectDir);
18817
+ const matrixPath = (0, import_node_path32.resolve)(projectDir, options.matrixPath ?? DEFAULT_MATRIX_FILE);
18818
+ const migrationsDir = (0, import_node_path32.resolve)(projectDir, options.migrationsDir ?? (0, import_node_path32.join)("supabase", "migrations"));
18819
+ const timeouts = { ...DEFAULT_TIMEOUTS, ...options.timeouts };
18820
+ if ((0, import_node_path32.basename)(matrixPath).toLowerCase().startsWith(".env")) {
18821
+ throw new RlsInputError(`Refusing to read ${(0, import_node_path32.basename)(matrixPath)}: rls-test never reads .env files. Point --matrix at a viberaven.permissions.json.`);
18822
+ }
18823
+ const filesRead = [];
18824
+ let matrixText;
17388
18825
  try {
17389
- entries = await (0, import_promises24.readdir)(dir, { withFileTypes: true });
18826
+ const info = await (0, import_promises26.stat)(matrixPath);
18827
+ if (!info.isFile()) throw new Error("not a file");
18828
+ matrixText = normalise(await (0, import_promises26.readFile)(matrixPath, "utf8"));
17390
18829
  } catch {
17391
- return;
18830
+ throw new RlsInputError(
18831
+ `No permissions matrix at ${matrixPath}. Write one by hand (schema: schemas/permissions-matrix.schema.json in the @viberaven/cli package); rls-test never infers it from your policies.`
18832
+ );
17392
18833
  }
17393
- for (const entry of entries) {
17394
- if (SKIP_DIRECTORIES.has(entry.name)) {
17395
- continue;
18834
+ filesRead.push(displayPath(projectDir, matrixPath));
18835
+ const matrix = parsePermissionsMatrix(matrixText);
18836
+ const names = await listMigrations(migrationsDir);
18837
+ const migrationFiles = [];
18838
+ for (const name of names) {
18839
+ const sql = normalise(await (0, import_promises26.readFile)((0, import_node_path32.join)(migrationsDir, name), "utf8"));
18840
+ filesRead.push(displayPath(projectDir, (0, import_node_path32.join)(migrationsDir, name)));
18841
+ migrationFiles.push({ name, sha256: sha256(sql), sql });
18842
+ }
18843
+ const migrationsHash = sha256(migrationFiles.map((f) => `${f.name}\0${f.sha256}`).join("\n"));
18844
+ const commit = await gitCommit(projectDir, [migrationsDir, matrixPath]);
18845
+ const noReassign = matrix.tables.filter((t) => TESTED_ACTORS.every((a) => t.expect[a].reassign === void 0)).map((t) => `${t.schema}.${t.name}`);
18846
+ const result = {
18847
+ label: NON_AUTHORITATIVE_LABEL,
18848
+ experimental: true,
18849
+ tool: options.tool ?? "viberaven rls-test",
18850
+ project: projectDir,
18851
+ commit,
18852
+ engine: { name: "PGlite", available: false },
18853
+ migrations: { dir: displayPath(projectDir, migrationsDir), files: migrationFiles.map(({ name, sha256: hash }) => ({ name, sha256: hash })), hash: migrationsHash, applied: false },
18854
+ matrix: { path: displayPath(projectDir, matrixPath), hash: matrix.hash, defaultGrants: matrix.defaultGrants },
18855
+ filesRead,
18856
+ assumptions: assumptionsFor(matrix.defaultGrants),
18857
+ exclusions: [
18858
+ ...IMITATION_EXCLUSIONS,
18859
+ ...PROBE_EXCLUSIONS,
18860
+ ...noReassign.length > 0 ? [`Ownership transfer (reassign) is not in the matrix for ${noReassign.join(", ")}, so it was not tested.`] : [],
18861
+ ...matrix.tables.some((t) => t.serviceRoleExcluded) ? ["The service role bypasses RLS and is excluded by the matrix."] : []
18862
+ ],
18863
+ roleChecks: [],
18864
+ tables: [],
18865
+ summary: { pass: 0, fail: 0, unknown: 0, notApplicable: 0, tested: 0, total: 0 },
18866
+ verdict: "",
18867
+ exitCode: 2
18868
+ };
18869
+ const finish = () => {
18870
+ result.summary = summarise(result.tables);
18871
+ const { verdict, exitCode } = verdictFor(result.summary);
18872
+ result.verdict = verdict;
18873
+ result.exitCode = exitCode;
18874
+ return result;
18875
+ };
18876
+ const allUnknown = (reason, summary) => {
18877
+ result.tables = matrix.tables.map((t) => ({ name: `${t.schema}.${t.name}`, unknownReason: reason, cells: unknownCells(t, reason, summary) }));
18878
+ result.roleChecks = result.roleChecks.filter((c) => !c.role.includes(" on "));
18879
+ return finish();
18880
+ };
18881
+ if (migrationFiles.length === 0) {
18882
+ return allUnknown(`No migrations found in ${result.migrations.dir}; there is nothing to test.`, "no migrations");
18883
+ }
18884
+ return withIsolation(async () => {
18885
+ let db;
18886
+ let engineLoaded = false;
18887
+ try {
18888
+ const engine = await (options.loadPglite ?? defaultPgliteLoader)();
18889
+ engineLoaded = true;
18890
+ db = await engine.create({ env: allowlistedEnv(process.env), statementTimeoutMs: timeouts.statementMs });
18891
+ } catch (error) {
18892
+ result.engine.error = firstLine(error instanceof Error ? error.message : String(error));
18893
+ if (engineLoaded) return allUnknown(`PGlite could not start: ${result.engine.error}. Nothing was tested.`, "not run: PGlite startup failed");
18894
+ const missing = error?.code === "MODULE_NOT_FOUND" || error?.code === "ERR_MODULE_NOT_FOUND";
18895
+ return missing ? allUnknown(PGLITE_INSTALL_HINT, "not run: PGlite is not installed") : allUnknown(`PGlite could not load: ${result.engine.error}. Nothing was tested.`, "not run: PGlite loading failed");
17396
18896
  }
17397
- const absolute = (0, import_node_path30.join)(dir, entry.name);
17398
- if (entry.isDirectory()) {
17399
- if (isCachePath(normalizePath5((0, import_node_path30.relative)(root, absolute)))) {
17400
- result.push({ path: absolute, sizeBytes: 0, kind: "directory" });
17401
- continue;
18897
+ try {
18898
+ result.engine.available = true;
18899
+ const version = String((await db.query("select version() as v")).rows[0]?.v ?? "");
18900
+ result.engine.postgres = version.match(/PostgreSQL [0-9.]+/)?.[0];
18901
+ result.engine.version = version.match(/PGlite ([0-9.]+)/)?.[1];
18902
+ if (db.isolationReport) result.engine.isolation = await db.isolationReport();
18903
+ await options.onIsolated?.();
18904
+ const me2 = await db.query(`select current_user::text as u, current_setting('is_superuser') as su`);
18905
+ if (me2.rows[0]?.su !== "on") {
18906
+ return allUnknown(`The PGlite session user ${me2.rows[0]?.u} is not a superuser, so migrations and state checks cannot run as intended.`, "harness problem");
17402
18907
  }
17403
- await walkCleanupFiles(root, absolute, result, depth + 1);
17404
- continue;
18908
+ await db.exec(imitationSetupSql(matrix.defaultGrants));
18909
+ await db.exec("set search_path to public, extensions");
18910
+ const deadline = Date.now() + timeouts.migrationsMs;
18911
+ for (const file of migrationFiles) {
18912
+ try {
18913
+ const remaining = deadline - Date.now();
18914
+ if (remaining <= 0) throw new RlsTimeoutError(`timed out after ${Math.round(timeouts.migrationsMs / 1e3)} s`);
18915
+ await db.exec(file.sql, { timeoutMs: remaining });
18916
+ } catch (error) {
18917
+ if (error instanceof RlsTimeoutError) {
18918
+ result.migrations.failure = { file: file.name, message: error.message };
18919
+ return allUnknown(
18920
+ `Applying the migrations did not finish within ${Math.round(timeouts.migrationsMs / 1e3)} s (stopped at ${file.name}). PGlite ignores statement_timeout, so the worker was stopped and nothing was tested.`,
18921
+ `not run: migration ${file.name} timed out`
18922
+ );
18923
+ }
18924
+ if (!isSqlError(error)) throw error;
18925
+ const e2 = sqlError(error);
18926
+ result.migrations.failure = { file: file.name, code: e2.code, message: e2.message };
18927
+ return allUnknown(
18928
+ `Migration ${file.name} failed in the PGlite imitation (${e2.code}: ${e2.message}). The imitation cannot model something it needs, so nothing after it was tested.`,
18929
+ `not run: migration ${file.name} failed`
18930
+ );
18931
+ }
18932
+ }
18933
+ result.migrations.applied = true;
18934
+ await db.exec(`reset role; set search_path to public, extensions`);
18935
+ try {
18936
+ await db.exec(seedUsersSql());
18937
+ } catch (error) {
18938
+ if (!isSqlError(error)) throw error;
18939
+ const e2 = sqlError(error);
18940
+ return allUnknown(
18941
+ `Creating the two test users in auth.users failed (${e2.code}: ${e2.message}), probably in a trigger the migrations add. Nothing was tested.`,
18942
+ "not run: test users could not be created"
18943
+ );
18944
+ }
18945
+ const roles = await db.query(`select rolname::text as r, rolsuper as su, rolbypassrls as bypass from pg_roles where rolname in ('anon', 'authenticated') order by rolname`);
18946
+ for (const role of ["anon", "authenticated"]) {
18947
+ const row = roles.rows.find((r) => r.r === role);
18948
+ const ok = Boolean(row) && !row?.su && !row?.bypass;
18949
+ result.roleChecks.push({ role, ok, detail: row ? `superuser ${Boolean(row.su)}, BYPASSRLS ${Boolean(row.bypass)}` : "the role does not exist after the migrations" });
18950
+ }
18951
+ const badRoles = result.roleChecks.filter((c) => !c.ok);
18952
+ for (const table of matrix.tables) {
18953
+ result.tables.push(await runTable(db, table, badRoles, result.roleChecks));
18954
+ }
18955
+ return finish();
18956
+ } catch (error) {
18957
+ if (error instanceof RlsTimeoutError || !isSqlError(error)) {
18958
+ const message = firstLine(error instanceof Error ? error.message : String(error));
18959
+ return allUnknown(
18960
+ error instanceof RlsTimeoutError ? `A statement did not finish within ${Math.round(timeouts.statementMs / 1e3)} s, so the PGlite worker was stopped and no cell is reported as passed.` : `The PGlite worker stopped (${message}), so no cell is reported as passed.`,
18961
+ "not run: the database stopped"
18962
+ );
18963
+ }
18964
+ throw error;
18965
+ } finally {
18966
+ await db.close().catch(() => void 0);
17405
18967
  }
17406
- if (!entry.isFile()) {
17407
- continue;
18968
+ });
18969
+ }
18970
+ async function seedRow(db, shape, label, userId) {
18971
+ const owner = quoteIdent2(shape.owner.name);
18972
+ const pkText = shape.pk.map((c, i) => `${quoteIdent2(c.name)}::text as k${i}`).join(", ");
18973
+ const existing = await db.query(`select ${pkText} from ${shape.qualified} where ${owner}::text = $1 order by 1 limit 1`, [userId]);
18974
+ const keyOf2 = (row) => shape.pk.map((_3, i) => String(row[`k${i}`]));
18975
+ if (existing.rows.length === 1) return { key: keyOf2(existing.rows[0]), reused: true };
18976
+ const values = shape.rowValues(label, userId);
18977
+ const r = await db.query(
18978
+ `insert into ${shape.qualified} (${shape.insertColumns.map(quoteIdent2).join(", ")}) values (${values.map((_3, i) => `$${i + 1}`).join(", ")}) returning ${shape.pk.map((c, i) => `${quoteIdent2(c.name)}::text as k${i}`).join(", ")}`,
18979
+ values
18980
+ );
18981
+ if (r.rows.length !== 1) throw Object.assign(new Error("the insert returned no row (a trigger or rule may drop it)"), { code: "vr000" });
18982
+ return { key: keyOf2(r.rows[0]), reused: false };
18983
+ }
18984
+ async function runTable(db, table, badRoles, roleChecks) {
18985
+ const display = `${table.schema}.${table.name}`;
18986
+ const unknownTable = (reason, extra = {}) => ({ name: display, ...extra, unknownReason: reason, cells: unknownCells(table, reason, "not run") });
18987
+ if (badRoles.length > 0) {
18988
+ return unknownTable(`Role check failed (${badRoles.map((c) => `${c.role}: ${c.detail}`).join("; ")}), so RLS would not apply to the probes the way it does for the Data API.`);
18989
+ }
18990
+ const described = await describeTable(db, table);
18991
+ if (!described.shape) return unknownTable(described.reason ?? "unknown table problem");
18992
+ const shape = described.shape;
18993
+ const rel = await db.query(
18994
+ `select c.relrowsecurity as enabled, c.relforcerowsecurity as forced, pg_get_userbyid(c.relowner)::text as owner,
18995
+ pg_has_role('anon', c.relowner, 'USAGE') as anon_owner, pg_has_role('authenticated', c.relowner, 'USAGE') as auth_owner
18996
+ from pg_class c where c.oid = $1`,
18997
+ [shape.oid]
18998
+ );
18999
+ const info = rel.rows[0];
19000
+ const rls = { enabled: Boolean(info.enabled), forced: Boolean(info.forced), owner: String(info.owner) };
19001
+ for (const [role, isOwner] of [["anon", info.anon_owner], ["authenticated", info.auth_owner]]) {
19002
+ roleChecks.push({ role: `${role} on ${display}`, ok: !isOwner, detail: isOwner ? `${role} owns ${display} (owner ${rls.owner}), so RLS does not apply to it` : `not the owner (owner ${rls.owner})` });
19003
+ }
19004
+ if (info.anon_owner || info.auth_owner) {
19005
+ return unknownTable(`anon or authenticated owns ${display} (owner ${rls.owner}); table owners skip RLS, so the probes would prove nothing.`, { rls });
19006
+ }
19007
+ let ownerSeed;
19008
+ let otherSeed;
19009
+ try {
19010
+ ownerSeed = await seedRow(db, shape, "a", OWNER_ID);
19011
+ otherSeed = await seedRow(db, shape, "b", OTHER_USER_ID);
19012
+ } catch (error) {
19013
+ if (!isSqlError(error)) throw error;
19014
+ const e2 = sqlError(error);
19015
+ return unknownTable(`Could not seed ${display} (${e2.code}: ${e2.message}).${SEED_HINTS[e2.code] ? ` ${SEED_HINTS[e2.code]}` : ""}`, { rls });
19016
+ }
19017
+ const seeded = await measure(db, shape, ownerSeed.key);
19018
+ if (seeded.target === null || seeded.targetOwner !== OWNER_ID) {
19019
+ return unknownTable(`After seeding, the owner's row in ${display} is not owned by the test owner; a trigger or default may rewrite ${shape.owner.name}.`, { rls });
19020
+ }
19021
+ const privilegesByRole = /* @__PURE__ */ new Map();
19022
+ for (const role of ["anon", "authenticated"]) privilegesByRole.set(role, await privilegesFor(db, shape, role));
19023
+ const cells = [];
19024
+ for (const { actor, action, expected } of cellsFor(table)) {
19025
+ cells.push(await runCell({ db, shape, target: ownerSeed.key }, privilegesByRole, actor, action, expected));
19026
+ }
19027
+ cells.push(...serviceRoleCells(table));
19028
+ const columns = {};
19029
+ shape.insertColumns.forEach((c) => {
19030
+ columns[c] = c === shape.owner.name ? "the test user id" : "generated test value";
19031
+ });
19032
+ for (const c of Object.keys(table.seed)) columns[c] = "from the matrix seed";
19033
+ return {
19034
+ name: display,
19035
+ rls,
19036
+ seed: { rowsBefore: seeded.count, ownerRow: ownerSeed.key.join(","), otherUserRow: otherSeed.key.join(","), reused: { owner: ownerSeed.reused, otherUser: otherSeed.reused }, columns },
19037
+ cells
19038
+ };
19039
+ }
19040
+ var import_node_child_process6, import_node_crypto2, import_promises26, import_node_net, import_node_path32, import_node_util4, NON_AUTHORITATIVE_LABEL, PGLITE_INSTALL_HINT, ENV_ALLOWLIST, PERMISSION_DENIED, RLS_REFUSAL, DEFAULT_TIMEOUTS, RlsInputError, normalise, quoteIdent2, firstLine, plural2, NUMBERS, ACTOR_SESSION, changedRows, isRlsRefusal, isPrivilegeRefusal, SELF, PROBE_EXCLUSIONS, SEED_HINTS;
19041
+ var init_rlsRunner = __esm({
19042
+ "src/rls/rlsRunner.ts"() {
19043
+ "use strict";
19044
+ import_node_child_process6 = require("node:child_process");
19045
+ import_node_crypto2 = require("node:crypto");
19046
+ import_promises26 = require("node:fs/promises");
19047
+ import_node_net = __toESM(require("node:net"));
19048
+ import_node_path32 = require("node:path");
19049
+ import_node_util4 = require("node:util");
19050
+ init_safeGit();
19051
+ init_permissionsMatrix();
19052
+ init_supabaseImitation();
19053
+ init_workerDatabase();
19054
+ NON_AUTHORITATIVE_LABEL = "PGlite imitation of Supabase, non-authoritative. Scenario passed locally does not prove production.";
19055
+ PGLITE_INSTALL_HINT = "PGlite (@electric-sql/pglite) is not installed next to VibeRaven, so nothing was run. It is optional (about 25 MB). Run: npx -y -p viberaven -p @electric-sql/pglite@0.5.8 viberaven rls-test, or add both to the project: npm i -D viberaven @electric-sql/pglite@0.5.8";
19056
+ ENV_ALLOWLIST = ["PATH", "Path", "SystemRoot", "SYSTEMROOT", "windir", "TEMP", "TMP", "TMPDIR", "HOME", "USERPROFILE", "LANG", "LC_ALL", "TZ"];
19057
+ PERMISSION_DENIED = "42501";
19058
+ RLS_REFUSAL = /row-level security/i;
19059
+ DEFAULT_TIMEOUTS = { migrationsMs: 12e4, statementMs: 3e4 };
19060
+ RlsInputError = class extends Error {
19061
+ name = "RlsInputError";
19062
+ };
19063
+ normalise = (text) => text.replace(/\r\n/g, "\n");
19064
+ quoteIdent2 = (name) => `"${name.replace(/"/g, '""')}"`;
19065
+ firstLine = (message) => message.split("\n")[0].trim();
19066
+ plural2 = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
19067
+ NUMBERS = { a: ["1001"], b: ["1002"], i: ["1003"], u: ["1004", "7", "1"] };
19068
+ ACTOR_SESSION = {
19069
+ owner: { role: "authenticated", uid: OWNER_ID, claims: { sub: OWNER_ID, role: "authenticated", aud: "authenticated", email: "owner@example.test" } },
19070
+ other_user: { role: "authenticated", uid: OTHER_USER_ID, claims: { sub: OTHER_USER_ID, role: "authenticated", aud: "authenticated", email: "other-user@example.test" } },
19071
+ anon: { role: "anon", uid: null, claims: { role: "anon" } }
19072
+ };
19073
+ changedRows = (n) => `${plural2(n ?? 0, "row")} changed`;
19074
+ isRlsRefusal = (run) => run.error?.code === PERMISSION_DENIED && RLS_REFUSAL.test(run.error.message);
19075
+ isPrivilegeRefusal = (run) => run.error?.code === PERMISSION_DENIED && !RLS_REFUSAL.test(run.error.message);
19076
+ SELF = Symbol("self");
19077
+ PROBE_EXCLUSIONS = [
19078
+ "Policies that look up other tables (teams, organisations, memberships) are only exercised with the rows the migrations create: no membership rows are seeded.",
19079
+ "WITH CHECK expressions are tried with the generated values only; when every value tried is refused, the cell is Unknown, not a pass.",
19080
+ "When a role can use some columns but cannot read the primary key, probes run without a row filter and are judged by the owner row alone.",
19081
+ "When the primary key is readable, writes are filtered by it; unfiltered UPDATE and DELETE statements are not also tested.",
19082
+ "One row per test user is used; a row the migrations create for the user (an auth.users trigger) is reused.",
19083
+ "Side effects of triggers on other tables are not checked."
19084
+ ];
19085
+ SEED_HINTS = {
19086
+ "23502": 'A NOT NULL column has no value: give it one under "seed" in the matrix.',
19087
+ "23503": 'A foreign key points at a row that does not exist: give the column a value under "seed" in the matrix.',
19088
+ "23505": 'A unique constraint clashed with the test row: give the unique column a distinct value under "seed", or check whether a trigger already creates the row.',
19089
+ "23514": 'A CHECK constraint refused the generated values: give the column a valid value under "seed" in the matrix.'
19090
+ };
19091
+ }
19092
+ });
19093
+
19094
+ // src/rls/render.ts
19095
+ function renderRlsText(result) {
19096
+ const lines = [];
19097
+ lines.push(`viberaven rls-test (experimental) \xB7 ${result.project}`);
19098
+ lines.push(NON_AUTHORITATIVE_LABEL);
19099
+ lines.push("");
19100
+ const commit = result.commit ? `${result.commit.sha.slice(0, 12)}${result.commit.dirty === true ? " (migrations or matrix changed since this commit)" : result.commit.dirty === false ? " (migrations and matrix match this commit)" : ""}` : "not a git repository";
19101
+ lines.push(`Commit ${commit}`);
19102
+ const files = result.migrations.files.map((f) => f.name);
19103
+ lines.push(
19104
+ `Migrations ${result.migrations.dir}: ${files.length} file${files.length === 1 ? "" : "s"}${files.length > 0 ? ` (${files.slice(0, 4).join(", ")}${files.length > 4 ? ", ..." : ""})` : ""}, sha256 ${result.migrations.hash.slice(0, 16)}, ${result.migrations.applied ? "applied" : result.migrations.failure ? `failed at ${result.migrations.failure.file}` : "not applied"}`
19105
+ );
19106
+ lines.push(`Matrix ${result.matrix.path}, sha256 ${result.matrix.hash.slice(0, 16)}, defaultGrants ${result.matrix.defaultGrants}`);
19107
+ lines.push(
19108
+ `Engine ${result.engine.available ? `PGlite ${result.engine.version ?? "?"} (${result.engine.postgres ?? "PostgreSQL ?"}), in memory, network denied` : "PGlite not available"}`
19109
+ );
19110
+ if (result.roleChecks.length > 0) {
19111
+ const bad = result.roleChecks.filter((c) => !c.ok);
19112
+ lines.push(
19113
+ `Roles ${bad.length === 0 ? "anon and authenticated: not superuser, not BYPASSRLS, not the table owner" : bad.map((c) => `${c.role}: ${c.detail}`).join("; ")}`
19114
+ );
19115
+ }
19116
+ if (!result.engine.available) {
19117
+ lines.push("");
19118
+ if (result.engine.error) lines.push(`(${result.engine.error})`);
19119
+ }
19120
+ for (const table of result.tables) {
19121
+ lines.push("");
19122
+ const rls = table.rls ? `RLS ${table.rls.enabled ? "on" : "off"}${table.rls.forced ? ", forced" : ""}, owner ${table.rls.owner}` : "not inspected";
19123
+ lines.push(`${table.name} (${rls})`);
19124
+ if (table.seed) {
19125
+ const reused = [table.seed.reused.owner ? "the owner" : "", table.seed.reused.otherUser ? "the other user" : ""].filter(Boolean);
19126
+ lines.push(
19127
+ ` One row for the owner and one for another user (${table.seed.rowsBefore} rows in the table)${reused.length > 0 ? `; reused the rows the migrations created for ${reused.join(" and ")}` : ""}.`
19128
+ );
17408
19129
  }
17409
- try {
17410
- const fileStat = await (0, import_promises24.stat)(absolute);
17411
- result.push({ path: absolute, sizeBytes: fileStat.size, kind: "file" });
17412
- } catch {
17413
- continue;
19130
+ if (table.unknownReason) lines.push(` Unknown: ${table.unknownReason}`);
19131
+ const tested = table.cells.filter((c) => c.actor !== "service_role");
19132
+ const actorWidth = 14;
19133
+ const actionWidth = 10;
19134
+ lines.push(` ${"actor".padEnd(actorWidth)}${"action".padEnd(actionWidth)}${"expected".padEnd(10)}${"result".padEnd(9)}observed`);
19135
+ for (const cell of tested) {
19136
+ lines.push(` ${cell.actor.padEnd(actorWidth)}${cell.action.padEnd(actionWidth)}${cell.expected.padEnd(10)}${cell.outcome.padEnd(9)}${cell.observed.summary}`);
17414
19137
  }
19138
+ const service = table.cells.filter((c) => c.actor === "service_role");
19139
+ if (service.length > 0) lines.push(` ${"service_role".padEnd(actorWidth)}${"all".padEnd(actionWidth)}${"excluded".padEnd(10)}${"N/A".padEnd(9)}bypasses RLS by design, not tested`);
17415
19140
  }
19141
+ const s = result.summary;
19142
+ lines.push("");
19143
+ lines.push(`Result: ${s.pass} Pass, ${s.fail} Fail, ${s.unknown} Unknown, ${s.notApplicable} N/A \xB7 ${s.pass + s.fail} of ${s.tested} tested cells observed (${s.total} cells in the matrix)`);
19144
+ lines.push(result.verdict);
19145
+ const failed = result.tables.flatMap((t) => t.cells).filter((c) => c.outcome === "Fail");
19146
+ for (const cell of failed) {
19147
+ lines.push(` Fail ${cell.actor} ${cell.action} on ${cell.table}: ${cell.reason}`);
19148
+ }
19149
+ const unknown = result.tables.flatMap((t) => t.cells).filter((c) => c.outcome === "Unknown");
19150
+ const unknownReasons = [...new Set(unknown.map((c) => c.reason))];
19151
+ for (const reason of unknownReasons.slice(0, 5)) lines.push(` Unknown ${reason}`);
19152
+ if (unknownReasons.length > 5) lines.push(` Unknown and ${unknownReasons.length - 5} more reasons (see --json)`);
19153
+ lines.push("");
19154
+ lines.push("Assumptions:");
19155
+ for (const a of result.assumptions) lines.push(` - ${a}`);
19156
+ lines.push("Not tested:");
19157
+ for (const e2 of result.exclusions) lines.push(` - ${e2}`);
19158
+ lines.push("");
19159
+ lines.push(NON_AUTHORITATIVE_LABEL);
19160
+ return lines.join("\n");
17416
19161
  }
17417
- function normalizePath5(path) {
17418
- return path.replace(/\\/g, "/");
19162
+ function renderRlsJson(result) {
19163
+ return `${JSON.stringify(result, null, 2)}
19164
+ `;
17419
19165
  }
17420
- var import_promises24, import_node_path30, MAX_WALK_DEPTH, LARGE_LOG_BYTES, SKIP_DIRECTORIES;
17421
- var init_cleanPlan = __esm({
17422
- "src/commands/cleanPlan.ts"() {
19166
+ var init_render2 = __esm({
19167
+ "src/rls/render.ts"() {
17423
19168
  "use strict";
17424
- import_promises24 = require("node:fs/promises");
17425
- import_node_path30 = require("node:path");
17426
- MAX_WALK_DEPTH = 5;
17427
- LARGE_LOG_BYTES = 8192;
17428
- SKIP_DIRECTORIES = /* @__PURE__ */ new Set([".git", "node_modules"]);
19169
+ init_rlsRunner();
19170
+ }
19171
+ });
19172
+
19173
+ // src/commands/rlsTest.ts
19174
+ async function runRlsTestCommand(input) {
19175
+ const write = input.write ?? ((text) => process.stdout.write(text));
19176
+ const writeError = input.writeError ?? ((text) => process.stderr.write(text));
19177
+ try {
19178
+ const result = await (input.run ?? runRlsTest)({ projectDir: input.cwd, matrixPath: input.matrix, migrationsDir: input.migrations });
19179
+ write(input.json ? renderRlsJson(result) : `${renderRlsText(result)}
19180
+ `);
19181
+ return result.exitCode;
19182
+ } catch (error) {
19183
+ const expected = error instanceof MatrixError || error instanceof RlsInputError;
19184
+ const message = expected ? error.message : `rls-test stopped on an unexpected error, so nothing is reported as passed: ${error instanceof Error ? error.message : String(error)}`;
19185
+ if (input.json) {
19186
+ write(
19187
+ `${JSON.stringify(
19188
+ { label: NON_AUTHORITATIVE_LABEL, experimental: true, error: message, ...error instanceof MatrixError ? { problems: error.problems } : {}, exitCode: 2 },
19189
+ null,
19190
+ 2
19191
+ )}
19192
+ `
19193
+ );
19194
+ } else {
19195
+ writeError(`viberaven rls-test (experimental)
19196
+ ${NON_AUTHORITATIVE_LABEL}
19197
+
19198
+ ${message}
19199
+ `);
19200
+ }
19201
+ return 2;
19202
+ }
19203
+ }
19204
+ var init_rlsTest = __esm({
19205
+ "src/commands/rlsTest.ts"() {
19206
+ "use strict";
19207
+ init_permissionsMatrix();
19208
+ init_render2();
19209
+ init_rlsRunner();
17429
19210
  }
17430
19211
  });
17431
19212
 
@@ -18049,15 +19830,17 @@ function buildConnectProviders(providers) {
18049
19830
  }));
18050
19831
  }
18051
19832
  function buildMissionCards(artifact, providers) {
18052
- const blockers = artifact.gaps.slice(0, 6).map((gap) => ({
19833
+ const blockers = artifact.gaps.map((gap) => ({
18053
19834
  id: gap.id,
18054
19835
  title: gap.title,
18055
19836
  detail: gap.detail || void 0,
18056
19837
  severity: gap.severity,
18057
19838
  category: blockerCategoryLabel(gap),
18058
- tone: blockerTone(gap.severity)
19839
+ tone: blockerTone(gap.severity),
19840
+ prompt: gap.copyPrompt || void 0,
19841
+ requiresUserAction: gapRequiresUserAction(gap)
18059
19842
  }));
18060
- const planSteps = artifact.gaps.slice(0, 8).map((gap) => ({
19843
+ const planSteps = artifact.gaps.map((gap) => ({
18061
19844
  id: gap.id,
18062
19845
  title: gap.title,
18063
19846
  category: planStepCategoryLabel(gap),
@@ -18076,7 +19859,7 @@ function buildMissionCards(artifact, providers) {
18076
19859
  };
18077
19860
  }
18078
19861
  function projectNameFromPath(pathValue) {
18079
- return (0, import_node_path31.basename)(pathValue.replace(/[\\/]$/, "")) || "current repo";
19862
+ return (0, import_node_path33.basename)(pathValue.replace(/[\\/]$/, "")) || "current repo";
18080
19863
  }
18081
19864
  function gapSearchText(gap) {
18082
19865
  return [
@@ -18495,19 +20278,19 @@ function mapArtifactToLocalUiState(artifact, presentProviderIds) {
18495
20278
  selectedProviderId: selectedProvider.id,
18496
20279
  currentPrompt: selectedProvider.nextFix?.prompt ?? "",
18497
20280
  artifacts: {
18498
- tasklist: (0, import_node_path31.join)(".viberaven", "agent-tasklist.md"),
18499
- contextMap: (0, import_node_path31.join)(".viberaven", "context-map.json"),
18500
- gateResult: (0, import_node_path31.join)(".viberaven", "gate-result.json"),
18501
- lastScan: (0, import_node_path31.join)(".viberaven", "last-scan.json")
20281
+ tasklist: (0, import_node_path33.join)(".viberaven", "agent-tasklist.md"),
20282
+ contextMap: (0, import_node_path33.join)(".viberaven", "context-map.json"),
20283
+ gateResult: (0, import_node_path33.join)(".viberaven", "gate-result.json"),
20284
+ lastScan: (0, import_node_path33.join)(".viberaven", "last-scan.json")
18502
20285
  },
18503
20286
  empty: false
18504
20287
  };
18505
20288
  }
18506
- var import_node_path31, CONNECT_PROVIDER_PRIORITY, providerDashboardUrls, providerHints, PROVIDER_OWNED_GAP;
20289
+ var import_node_path33, CONNECT_PROVIDER_PRIORITY, providerDashboardUrls, providerHints, PROVIDER_OWNED_GAP;
18507
20290
  var init_artifactAdapter = __esm({
18508
20291
  "src/local-ui/artifactAdapter.ts"() {
18509
20292
  "use strict";
18510
- import_node_path31 = require("node:path");
20293
+ import_node_path33 = require("node:path");
18511
20294
  init_providerLogos();
18512
20295
  init_prompts();
18513
20296
  init_providerLaunchPaths();
@@ -18549,19 +20332,20 @@ function escapeJsonForHtml(value) {
18549
20332
  return JSON.stringify(value).replace(/</g, "\\u003c");
18550
20333
  }
18551
20334
  function renderStackHtml(state) {
18552
- const title = "VibeRaven";
20335
+ const title = `VibeRaven | ${state.project?.name ? `Studio for ${state.project.name}` : "Studio"}`;
18553
20336
  return `<!doctype html>
18554
20337
  <html lang="en">
18555
20338
  <head>
18556
20339
  <meta charset="utf-8" />
18557
20340
  <meta name="viewport" content="width=device-width, initial-scale=1" />
18558
- <meta name="theme-color" content="#0a0a0a" />
20341
+ <meta name="theme-color" content="#0a0a0b" />
18559
20342
  <meta name="color-scheme" content="dark" />
18560
20343
  <title>${title.replace(/</g, "&lt;")}</title>
18561
20344
  <link rel="icon" type="image/png" href="/manga/favicon.png?v=stack1" />
18562
20345
  <link rel="preconnect" href="https://fonts.googleapis.com" />
18563
20346
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
18564
- <link href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=Instrument+Serif&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
20347
+ <!-- Same faces as viberaven.dev. Offline, the Studio falls back to the system sans and mono. -->
20348
+ <link href="https://fonts.googleapis.com/css2?family=Schibsted+Grotesk:wght@400..700&family=IBM+Plex+Mono:wght@400;600&display=swap" rel="stylesheet" />
18565
20349
  <link rel="stylesheet" href="/stack/app.css" />
18566
20350
  </head>
18567
20351
  <body>
@@ -18808,27 +20592,39 @@ var init_planLimits = __esm({
18808
20592
  // src/hosted/credentials.ts
18809
20593
  function defaultConfigDir(env = process.env, platform = process.platform) {
18810
20594
  const override = env.VIBERAVEN_CONFIG_DIR?.trim();
18811
- if (override) return (0, import_node_path32.resolve)(override);
20595
+ if (override) return (0, import_node_path34.resolve)(override);
18812
20596
  if (platform === "win32") {
18813
- return (0, import_node_path32.join)(env.APPDATA?.trim() || (0, import_node_path32.join)((0, import_node_os.homedir)(), "AppData", "Roaming"), "viberaven");
20597
+ return (0, import_node_path34.join)(env.APPDATA?.trim() || (0, import_node_path34.join)((0, import_node_os.homedir)(), "AppData", "Roaming"), "viberaven");
18814
20598
  }
18815
20599
  if (platform === "darwin") {
18816
- return (0, import_node_path32.join)((0, import_node_os.homedir)(), "Library", "Application Support", "viberaven");
20600
+ return (0, import_node_path34.join)((0, import_node_os.homedir)(), "Library", "Application Support", "viberaven");
18817
20601
  }
18818
- return (0, import_node_path32.join)(env.XDG_CONFIG_HOME?.trim() || (0, import_node_path32.join)((0, import_node_os.homedir)(), ".config"), "viberaven");
20602
+ return (0, import_node_path34.join)(env.XDG_CONFIG_HOME?.trim() || (0, import_node_path34.join)((0, import_node_os.homedir)(), ".config"), "viberaven");
18819
20603
  }
18820
20604
  function isInside(child, parent) {
18821
- const rel = (0, import_node_path32.relative)((0, import_node_path32.resolve)(parent), (0, import_node_path32.resolve)(child));
18822
- return rel === "" || !rel.startsWith("..") && !(0, import_node_path32.isAbsolute)(rel);
20605
+ const rel = (0, import_node_path34.relative)((0, import_node_path34.resolve)(parent), (0, import_node_path34.resolve)(child));
20606
+ return rel === "" || !rel.startsWith("..") && !(0, import_node_path34.isAbsolute)(rel);
20607
+ }
20608
+ async function retryTransient(work) {
20609
+ for (let attempt = 1; ; attempt += 1) {
20610
+ try {
20611
+ return await work();
20612
+ } catch (error) {
20613
+ const code = error?.code;
20614
+ if (attempt >= FS_ATTEMPTS || !code || !TRANSIENT_FS_CODES.has(code)) throw error;
20615
+ await new Promise((resolveWait) => setTimeout(resolveWait, FS_BACKOFF_MS * attempt));
20616
+ }
20617
+ }
18823
20618
  }
18824
- var import_node_child_process6, import_promises25, import_node_os, import_node_path32, CREDENTIALS_FILE_NAME, CredentialLocationError, CredentialPermissionError, restrictWithIcacls, CredentialStore;
20619
+ var import_node_child_process7, import_node_crypto3, import_promises27, import_node_os, import_node_path34, CREDENTIALS_FILE_NAME, CredentialLocationError, CredentialPermissionError, restrictWithIcacls, TRANSIENT_FS_CODES, FS_ATTEMPTS, FS_BACKOFF_MS, CredentialStore;
18825
20620
  var init_credentials = __esm({
18826
20621
  "src/hosted/credentials.ts"() {
18827
20622
  "use strict";
18828
- import_node_child_process6 = require("node:child_process");
18829
- import_promises25 = require("node:fs/promises");
20623
+ import_node_child_process7 = require("node:child_process");
20624
+ import_node_crypto3 = require("node:crypto");
20625
+ import_promises27 = require("node:fs/promises");
18830
20626
  import_node_os = require("node:os");
18831
- import_node_path32 = require("node:path");
20627
+ import_node_path34 = require("node:path");
18832
20628
  CREDENTIALS_FILE_NAME = "credentials.json";
18833
20629
  CredentialLocationError = class extends Error {
18834
20630
  };
@@ -18842,25 +20638,32 @@ var init_credentials = __esm({
18842
20638
  const name = (0, import_node_os.userInfo)().username;
18843
20639
  const user = process.env.USERDOMAIN ? `${process.env.USERDOMAIN}\\${name}` : name;
18844
20640
  return new Promise((resolveRestrict, reject) => {
18845
- (0, import_node_child_process6.execFile)("icacls", [path, "/inheritance:r", "/grant:r", `${user}:F`], { windowsHide: true, timeout: 1e4 }, (error) => {
20641
+ (0, import_node_child_process7.execFile)("icacls", [path, "/inheritance:r", "/grant:r", `${user}:F`], { windowsHide: true, timeout: 1e4 }, (error) => {
18846
20642
  if (error) reject(error);
18847
20643
  else resolveRestrict();
18848
20644
  });
18849
20645
  });
18850
20646
  };
20647
+ TRANSIENT_FS_CODES = /* @__PURE__ */ new Set(["EPERM", "EACCES", "EBUSY"]);
20648
+ FS_ATTEMPTS = 8;
20649
+ FS_BACKOFF_MS = 10;
18851
20650
  CredentialStore = class {
18852
20651
  constructor(configDir, workspacePath, options = {}) {
18853
20652
  this.configDir = configDir;
18854
20653
  this.workspacePath = workspacePath;
18855
- this.path = (0, import_node_path32.join)(configDir, CREDENTIALS_FILE_NAME);
20654
+ this.path = (0, import_node_path34.join)(configDir, CREDENTIALS_FILE_NAME);
18856
20655
  this.restrict = options.restrict !== void 0 ? options.restrict : process.platform === "win32" ? restrictWithIcacls : null;
20656
+ this.fs = { readFile: (path, encoding) => (0, import_promises27.readFile)(path, encoding), rename: (from, to) => (0, import_promises27.rename)(from, to), rm: (path) => (0, import_promises27.rm)(path, { force: true }), ...options.fs };
18857
20657
  }
18858
20658
  path;
18859
20659
  restrict;
20660
+ fs;
20661
+ /** Every change to the file runs one at a time, in the order it was asked for. */
20662
+ queue = Promise.resolve();
18860
20663
  async read() {
18861
20664
  let raw;
18862
20665
  try {
18863
- raw = await (0, import_promises25.readFile)(this.path, "utf8");
20666
+ raw = await retryTransient(() => this.fs.readFile(this.path, "utf8"));
18864
20667
  } catch {
18865
20668
  return null;
18866
20669
  }
@@ -18878,22 +20681,36 @@ var init_credentials = __esm({
18878
20681
  const stored = await this.read();
18879
20682
  return stored && stored.apiBaseUrl === apiBaseUrl ? stored : null;
18880
20683
  }
18881
- async write(credentials) {
20684
+ /** Resolves once every change queued so far has landed (or failed). */
20685
+ idle() {
20686
+ return this.queue;
20687
+ }
20688
+ serial(work) {
20689
+ const run = this.queue.then(work, work);
20690
+ this.queue = run.then(() => void 0, () => void 0);
20691
+ return run;
20692
+ }
20693
+ write(credentials) {
20694
+ return this.serial(async () => {
20695
+ await this.writeNow({ version: 1, ...credentials, savedAt: (/* @__PURE__ */ new Date()).toISOString() });
20696
+ });
20697
+ }
20698
+ /** Must run inside `serial`. Returns false when `guard` stopped the swap and the file was left alone. */
20699
+ async writeNow(body, guard) {
18882
20700
  if (isInside(this.configDir, this.workspacePath)) {
18883
20701
  throw new CredentialLocationError(
18884
20702
  `Refusing to store the VibeRaven sign-in inside this project (${this.configDir}). Set VIBERAVEN_CONFIG_DIR to a folder outside the repo.`
18885
20703
  );
18886
20704
  }
18887
- await (0, import_promises25.mkdir)(this.configDir, { recursive: true, mode: 448 });
18888
- const body = { version: 1, ...credentials, savedAt: (/* @__PURE__ */ new Date()).toISOString() };
18889
- const temp = `${this.path}.${process.pid}.tmp`;
18890
- await (0, import_promises25.writeFile)(temp, `${JSON.stringify(body, null, 2)}
20705
+ await (0, import_promises27.mkdir)(this.configDir, { recursive: true, mode: 448 });
20706
+ const temp = `${this.path}.${(0, import_node_crypto3.randomUUID)()}.tmp`;
20707
+ await (0, import_promises27.writeFile)(temp, `${JSON.stringify(body, null, 2)}
18891
20708
  `, { encoding: "utf8", mode: 384 });
18892
- await (0, import_promises25.chmod)(temp, 384).catch(() => void 0);
20709
+ await (0, import_promises27.chmod)(temp, 384).catch(() => void 0);
18893
20710
  try {
18894
20711
  if (this.restrict) await this.restrict(temp);
18895
20712
  } catch (error) {
18896
- await (0, import_promises25.rm)(temp, { force: true });
20713
+ await (0, import_promises27.rm)(temp, { force: true });
18897
20714
  const code = error?.code;
18898
20715
  const toolMissing = code === "ENOENT";
18899
20716
  throw new CredentialPermissionError(
@@ -18901,22 +20718,53 @@ var init_credentials = __esm({
18901
20718
  toolMissing
18902
20719
  );
18903
20720
  }
20721
+ if (guard) {
20722
+ const onDisk = await this.readFor(guard.apiBaseUrl);
20723
+ if (onDisk?.accessToken !== guard.accessToken || guard.isCurrent && !guard.isCurrent()) {
20724
+ await (0, import_promises27.rm)(temp, { force: true });
20725
+ return false;
20726
+ }
20727
+ }
18904
20728
  try {
18905
- await (0, import_promises25.rename)(temp, this.path);
20729
+ await retryTransient(() => this.fs.rename(temp, this.path));
18906
20730
  } catch (error) {
18907
- await (0, import_promises25.rm)(temp, { force: true });
20731
+ await (0, import_promises27.rm)(temp, { force: true });
18908
20732
  throw error;
18909
20733
  }
18910
- await (0, import_promises25.chmod)(this.path, 384).catch(() => void 0);
20734
+ await (0, import_promises27.chmod)(this.path, 384).catch(() => void 0);
20735
+ return true;
18911
20736
  }
18912
20737
  /** Keeps the stored plan label in step with what the API last said. */
18913
- async updatePlan(apiBaseUrl, plan) {
18914
- const stored = await this.readFor(apiBaseUrl);
18915
- if (!stored || stored.account.plan === plan) return;
18916
- await this.write({ apiBaseUrl: stored.apiBaseUrl, accessToken: stored.accessToken, account: { ...stored.account, plan } });
20738
+ updatePlan(apiBaseUrl, plan) {
20739
+ return this.serial(async () => {
20740
+ const stored = await this.readFor(apiBaseUrl);
20741
+ if (!stored || stored.account.plan === plan) return;
20742
+ await this.writeNow(
20743
+ { version: 1, apiBaseUrl: stored.apiBaseUrl, accessToken: stored.accessToken, account: { ...stored.account, plan }, savedAt: (/* @__PURE__ */ new Date()).toISOString() },
20744
+ { apiBaseUrl: stored.apiBaseUrl, accessToken: stored.accessToken }
20745
+ );
20746
+ });
18917
20747
  }
18918
- async clear() {
18919
- await (0, import_promises25.rm)(this.path, { force: true });
20748
+ /**
20749
+ * Replaces the saved account label with what the API holds now. Only touches the file while it
20750
+ * still holds the sign-in `accessToken` belongs to and `isCurrent()` still says yes, so a
20751
+ * sign-out or a new sign-in made while the API was answering is never overwritten. `savedAt`
20752
+ * is the sign-in time and is kept.
20753
+ */
20754
+ updateAccount(apiBaseUrl, accessToken, account, isCurrent) {
20755
+ return this.serial(async () => {
20756
+ const stored = await this.readFor(apiBaseUrl);
20757
+ if (!stored || stored.accessToken !== accessToken) return;
20758
+ if (stored.account.email === account.email && stored.account.plan === account.plan) return;
20759
+ if (isCurrent && !isCurrent()) return;
20760
+ await this.writeNow(
20761
+ { version: 1, apiBaseUrl: stored.apiBaseUrl, accessToken, account: { ...stored.account, ...account }, savedAt: stored.savedAt },
20762
+ { apiBaseUrl: stored.apiBaseUrl, accessToken, isCurrent }
20763
+ );
20764
+ });
20765
+ }
20766
+ clear() {
20767
+ return this.serial(() => retryTransient(() => this.fs.rm(this.path)));
18920
20768
  }
18921
20769
  };
18922
20770
  }
@@ -18942,17 +20790,22 @@ function assertHttpBaseUrl(baseUrl) {
18942
20790
  }
18943
20791
  }
18944
20792
  async function postJson(url, payload, options) {
20793
+ return requestJson(url, { ...options, method: "POST", body: JSON.stringify(payload === void 0 ? {} : payload) });
20794
+ }
20795
+ async function getJson(url, options) {
20796
+ return requestJson(url, { ...options, method: "GET" });
20797
+ }
20798
+ async function requestJson(url, options) {
18945
20799
  let response;
18946
20800
  try {
18947
20801
  response = await fetch(url, {
18948
- method: "POST",
20802
+ method: options.method,
18949
20803
  headers: {
18950
- "Content-Type": "application/json",
20804
+ ...options.body !== void 0 ? { "Content-Type": "application/json" } : {},
18951
20805
  ...options.headers ?? {},
18952
20806
  ...options.accessToken ? { Authorization: `Bearer ${options.accessToken}` } : {}
18953
20807
  },
18954
- // The Node API rejects `Content-Type: application/json` with an empty body.
18955
- body: JSON.stringify(payload === void 0 ? {} : payload),
20808
+ body: options.body,
18956
20809
  // Never follow a redirect with the sign-in token and the project attached.
18957
20810
  redirect: "error",
18958
20811
  signal: AbortSignal.timeout(options.timeoutMs)
@@ -19029,6 +20882,13 @@ async function runHostedCheck(baseUrl, accessToken, payload, timeoutMs, idempote
19029
20882
  }
19030
20883
  return response;
19031
20884
  }
20885
+ async function fetchAccount(baseUrl, accessToken, timeoutMs) {
20886
+ const payload = await getJson(`${normalizeBaseUrl(baseUrl)}/v1/account/me`, { accessToken, timeoutMs, label: "Account check" });
20887
+ if (!isAccount(payload)) {
20888
+ throw new HostedCallError("invalid_response", "Account response did not match the expected shape.");
20889
+ }
20890
+ return { email: payload.email, plan: payload.plan, usage: payload.usage };
20891
+ }
19032
20892
  function isRecord4(value) {
19033
20893
  return typeof value === "object" && value !== null && !Array.isArray(value);
19034
20894
  }
@@ -19053,6 +20913,9 @@ function isUsage(value) {
19053
20913
  function isStationResponse(value) {
19054
20914
  return isRecord4(value) && (value.status === "stable" || value.status === "drifting" || value.status === "chaos") && isNonEmptyString(value.reason) && isNonEmptyString(value.impact) && (value.confidence === "low" || value.confidence === "medium" || value.confidence === "high") && typeof value.output === "string" && isUsage(value.usage);
19055
20915
  }
20916
+ function isAccount(value) {
20917
+ return isRecord4(value) && isNonEmptyString(value.email) && (value.plan === "free" || value.plan === "pro") && isUsage(value.usage);
20918
+ }
19056
20919
  var DEFAULT_API_BASE_URL, HostedCallError;
19057
20920
  var init_hostedClient = __esm({
19058
20921
  "src/hosted/hostedClient.ts"() {
@@ -19237,7 +21100,7 @@ async function buildHostedCheckPayload(workspacePath) {
19237
21100
  throw new Error("Could not assemble the full check request.");
19238
21101
  }
19239
21102
  function finalizePayload(payload) {
19240
- const folder = (0, import_node_path33.basename)(payload.workspacePath.split("\\").join("/").replace(/\/+$/, "")) || "project";
21103
+ const folder = (0, import_node_path35.basename)(payload.workspacePath.split("\\").join("/").replace(/\/+$/, "")) || "project";
19241
21104
  const redacted = redactPayload({ ...payload, workspacePath: folder });
19242
21105
  return { payload: redacted.payload, redactions: redacted.count };
19243
21106
  }
@@ -19268,13 +21131,13 @@ function splitSections(prompt) {
19268
21131
  chars: section.length
19269
21132
  }));
19270
21133
  }
19271
- var import_node_path33, PayloadCaptured;
21134
+ var import_node_path35, PayloadCaptured;
19272
21135
  var init_payload = __esm({
19273
21136
  "src/hosted/payload.ts"() {
19274
21137
  "use strict";
19275
21138
  init_fileScanner();
19276
21139
  init_orchestrator();
19277
- import_node_path33 = require("node:path");
21140
+ import_node_path35 = require("node:path");
19278
21141
  init_config();
19279
21142
  init_redact();
19280
21143
  PayloadCaptured = class extends Error {
@@ -19350,11 +21213,11 @@ function readServerMessage(body) {
19350
21213
  return body.slice(0, 300);
19351
21214
  }
19352
21215
  }
19353
- var import_node_crypto, PRICING_URL, FREE_LIMIT_MESSAGE, PREVIEW_TTL_MS, MAX_PREVIEWS, TRANSIENT_REASONS, SEVERITY_RANK3, FullCheckService;
21216
+ var import_node_crypto4, PRICING_URL, FREE_LIMIT_MESSAGE, PREVIEW_TTL_MS, STATUS_TIMEOUT_MS, STATUS_CACHE_MS, MAX_PREVIEWS, TRANSIENT_REASONS, SEVERITY_RANK3, FullCheckService;
19354
21217
  var init_fullCheck = __esm({
19355
21218
  "src/hosted/fullCheck.ts"() {
19356
21219
  "use strict";
19357
- import_node_crypto = require("node:crypto");
21220
+ import_node_crypto4 = require("node:crypto");
19358
21221
  init_engines();
19359
21222
  init_planLimits();
19360
21223
  init_productionMapLanes();
@@ -19365,6 +21228,8 @@ var init_fullCheck = __esm({
19365
21228
  PRICING_URL = "https://viberaven.dev/pricing";
19366
21229
  FREE_LIMIT_MESSAGE = `You've used your free full checks. Pro gives ${PRO_MONTHLY_STATION_RUNS} a month.`;
19367
21230
  PREVIEW_TTL_MS = 15 * 6e4;
21231
+ STATUS_TIMEOUT_MS = 5e3;
21232
+ STATUS_CACHE_MS = 5e3;
19368
21233
  MAX_PREVIEWS = 5;
19369
21234
  TRANSIENT_REASONS = /* @__PURE__ */ new Set(["network", "timeout", "server", "rate_limited", "busy"]);
19370
21235
  SEVERITY_RANK3 = { critical: 0, warning: 1, info: 2 };
@@ -19372,10 +21237,12 @@ var init_fullCheck = __esm({
19372
21237
  constructor(workspacePath, options = {}) {
19373
21238
  this.workspacePath = workspacePath;
19374
21239
  this.apiBaseUrl = normalizeBaseUrl(options.apiBaseUrl ?? resolveApiBaseUrl());
19375
- this.store = new CredentialStore(options.configDir ?? defaultConfigDir(), workspacePath, { restrict: options.restrictCredentialsFile });
21240
+ this.store = new CredentialStore(options.configDir ?? defaultConfigDir(), workspacePath, { restrict: options.restrictCredentialsFile, fs: options.credentialsFs });
19376
21241
  this.openUrl = options.openUrl ?? openUrlInBrowser;
19377
21242
  this.requestTimeoutMs = options.requestTimeoutMs ?? 15e3;
19378
21243
  this.runTimeoutMs = options.runTimeoutMs ?? 18e4;
21244
+ this.statusTimeoutMs = options.statusTimeoutMs ?? STATUS_TIMEOUT_MS;
21245
+ this.statusCacheMs = options.statusCacheMs ?? STATUS_CACHE_MS;
19379
21246
  this.buildPayload = options.buildPayload ?? buildHostedCheckPayload;
19380
21247
  }
19381
21248
  apiBaseUrl;
@@ -19383,6 +21250,8 @@ var init_fullCheck = __esm({
19383
21250
  openUrl;
19384
21251
  requestTimeoutMs;
19385
21252
  runTimeoutMs;
21253
+ statusTimeoutMs;
21254
+ statusCacheMs;
19386
21255
  buildPayload;
19387
21256
  pending = null;
19388
21257
  previews = /* @__PURE__ */ new Map();
@@ -19390,16 +21259,54 @@ var init_fullCheck = __esm({
19390
21259
  running = false;
19391
21260
  /** Used only when the token could not be written to disk with owner-only access. */
19392
21261
  sessionCredentials = null;
21262
+ /** The last live plan check, so a burst of `status()` calls asks the API once. */
21263
+ lastCheck = null;
21264
+ checkInFlight = null;
21265
+ /**
21266
+ * Bumped whenever the sign-in or what the API holds may have changed (sign-out, sign-in, a run,
21267
+ * a plan limit). A live check started before the bump is superseded: its answer is not cached,
21268
+ * not shown as live, and not written to the credentials file.
21269
+ */
21270
+ generation = 0;
21271
+ /** The last time the API confirmed the plan, kept when a later check fails. */
21272
+ lastLive = null;
21273
+ /** A saved sign-in this Studio signed out of but could not delete. Ignored until the file changes. */
21274
+ unremovedToken = null;
21275
+ /** Failed sign-out could not identify the disk token; only a new explicit sign-in can trust it again. */
21276
+ ignoreStoredCredentials = false;
19393
21277
  /** The sign-in to use: the saved one, else a session-only one, and only for this API. */
19394
21278
  async credentials() {
21279
+ if (this.sessionCredentials?.apiBaseUrl === this.apiBaseUrl) return this.sessionCredentials;
21280
+ if (this.ignoreStoredCredentials) return null;
19395
21281
  const stored = await this.store.readFor(this.apiBaseUrl);
19396
- if (stored) return stored;
19397
- return this.sessionCredentials?.apiBaseUrl === this.apiBaseUrl ? this.sessionCredentials : null;
21282
+ return stored && stored.accessToken !== this.unremovedToken ? stored : null;
19398
21283
  }
21284
+ /**
21285
+ * Signs this Studio out. Never throws: if the saved sign-in cannot be deleted (Windows still
21286
+ * refusing after the retries), this Studio ignores it from now on and it returns false.
21287
+ */
19399
21288
  async clearCredentials() {
19400
21289
  this.sessionCredentials = null;
19401
21290
  this.lastUsage = null;
19402
- await this.store.clear();
21291
+ this.lastLive = null;
21292
+ this.forgetLiveCheck();
21293
+ const saved = await this.store.readFor(this.apiBaseUrl).catch(() => null);
21294
+ try {
21295
+ await this.store.clear();
21296
+ this.ignoreStoredCredentials = false;
21297
+ return true;
21298
+ } catch (error) {
21299
+ if (saved) this.unremovedToken = saved.accessToken;
21300
+ else this.ignoreStoredCredentials = true;
21301
+ console.warn(`VibeRaven: the saved sign-in at ${this.store.path} could not be removed (${error instanceof Error ? error.message : String(error)}). This Studio treats it as signed out.`);
21302
+ return false;
21303
+ }
21304
+ }
21305
+ /** The next `status()` asks the API again, and any check still in flight is superseded. */
21306
+ forgetLiveCheck() {
21307
+ this.generation += 1;
21308
+ this.lastCheck = null;
21309
+ this.checkInFlight = null;
19403
21310
  }
19404
21311
  /** Keep the plan label in step with what the API last said (upgrade, downgrade, lapse). */
19405
21312
  /**
@@ -19409,6 +21316,8 @@ var init_fullCheck = __esm({
19409
21316
  * failure the old saved sign-in (with its old plan label) is still there.
19410
21317
  */
19411
21318
  async refreshPlan(plan) {
21319
+ this.forgetLiveCheck();
21320
+ if (this.lastLive) this.lastLive = { ...this.lastLive, account: { ...this.lastLive.account, plan } };
19412
21321
  if (this.sessionCredentials) this.sessionCredentials = { ...this.sessionCredentials, account: { ...this.sessionCredentials.account, plan } };
19413
21322
  try {
19414
21323
  await this.store.updatePlan(this.apiBaseUrl, plan);
@@ -19416,19 +21325,87 @@ var init_fullCheck = __esm({
19416
21325
  console.warn(`VibeRaven: could not update the saved plan label to "${plan}"; keeping the saved sign-in as it was. ${error instanceof Error ? error.message : String(error)}`);
19417
21326
  }
19418
21327
  }
21328
+ /**
21329
+ * What the "Run full check" sheet shows. When signed in, the plan and usage come from the API
21330
+ * (`GET /v1/account/me`), not from the label saved at sign-in, so a Pro subscription that is no
21331
+ * longer valid shows as Free. If the API cannot be asked, the saved values are returned with `stale: true`.
21332
+ * A 401 means the sign-in is gone: it is cleared and the status reads signed out.
21333
+ */
19419
21334
  async status() {
21335
+ const before = await this.credentials();
21336
+ const check = before ? await this.liveCheck(before) : null;
19420
21337
  const creds = await this.credentials();
21338
+ const live = creds && typeof check === "object" && check?.accessToken === creds.accessToken ? check : null;
21339
+ const known = creds && this.lastLive?.accessToken === creds.accessToken ? this.lastLive : null;
19421
21340
  const pending = this.pending && Date.parse(this.pending.expiresAt) > Date.now() ? this.pending : null;
19422
21341
  return {
19423
21342
  apiBaseUrl: this.apiBaseUrl,
19424
21343
  usingDefaultApi: this.apiBaseUrl === DEFAULT_API_BASE_URL,
19425
21344
  signedIn: creds !== null,
19426
- account: creds ? creds.account : null,
19427
- usage: creds ? this.lastUsage : null,
21345
+ // When stale, the last live answer beats the file: its rewrite may not have landed yet.
21346
+ account: creds ? live?.account ?? known?.account ?? creds.account : null,
21347
+ usage: creds ? live?.usage ?? this.lastUsage : null,
19428
21348
  signIn: pending ? this.signInView(pending) : null,
19429
21349
  credentialsPath: this.store.path,
19430
- sessionOnly: creds !== null && creds === this.sessionCredentials
21350
+ sessionOnly: creds !== null && creds === this.sessionCredentials,
21351
+ stale: creds !== null && live === null,
21352
+ checkedAt: known?.checkedAt ?? null
21353
+ };
21354
+ }
21355
+ /** One live check per sign-in, shared by concurrent callers and reused for `statusCacheMs`. */
21356
+ liveCheck(creds) {
21357
+ const last = this.lastCheck;
21358
+ if (last && last.accessToken === creds.accessToken && Date.now() - last.at < this.statusCacheMs) return Promise.resolve(last.result);
21359
+ const inFlight = this.checkInFlight;
21360
+ if (inFlight && inFlight.accessToken === creds.accessToken) return inFlight.promise;
21361
+ const generation = this.generation;
21362
+ const entry = {
21363
+ accessToken: creds.accessToken,
21364
+ promise: this.askAccount(creds, generation).then((result) => {
21365
+ if (this.checkInFlight === entry) this.checkInFlight = null;
21366
+ if (this.generation === generation) this.lastCheck = { accessToken: creds.accessToken, at: Date.now(), result };
21367
+ return result;
21368
+ })
19431
21369
  };
21370
+ this.checkInFlight = entry;
21371
+ return entry.promise;
21372
+ }
21373
+ /** Never throws: any failure other than a 401 leaves the saved sign-in as it is. */
21374
+ async askAccount(creds, generation) {
21375
+ try {
21376
+ const answer = await fetchAccount(this.apiBaseUrl, creds.accessToken, this.statusTimeoutMs);
21377
+ const live = {
21378
+ accessToken: creds.accessToken,
21379
+ account: { email: answer.email, plan: answer.plan },
21380
+ usage: toUsage(answer.usage),
21381
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString()
21382
+ };
21383
+ const current = await this.credentials();
21384
+ if (current?.accessToken !== creds.accessToken) return "unavailable";
21385
+ if (this.generation !== generation) return "unavailable";
21386
+ this.lastLive = live;
21387
+ this.lastUsage = live.usage;
21388
+ this.saveAccount(creds.accessToken, live.account, generation);
21389
+ return live;
21390
+ } catch (error) {
21391
+ if (error instanceof HostedCallError && error.kind === "http" && error.details.status === 401) {
21392
+ const current = await this.credentials().catch(() => null);
21393
+ if (current?.accessToken === creds.accessToken) await this.clearCredentials();
21394
+ return "signed_out";
21395
+ }
21396
+ return "unavailable";
21397
+ }
21398
+ }
21399
+ /**
21400
+ * Fire and forget: `status()` never waits on the file (locking it can take seconds on Windows).
21401
+ * The rewrite is queued behind every other change to the file, skipped if this check has been
21402
+ * superseded by the time it runs, and a failure is logged, never thrown.
21403
+ */
21404
+ saveAccount(accessToken, account, generation) {
21405
+ if (this.sessionCredentials?.accessToken === accessToken) this.sessionCredentials = { ...this.sessionCredentials, account };
21406
+ this.store.updateAccount(this.apiBaseUrl, accessToken, account, () => this.generation === generation).catch((error) => {
21407
+ console.warn(`VibeRaven: could not update the saved plan label to "${account.plan}"; keeping the saved sign-in as it was. ${error instanceof Error ? error.message : String(error)}`);
21408
+ });
19432
21409
  }
19433
21410
  async startSignIn() {
19434
21411
  return this.guard(async () => {
@@ -19466,27 +21443,37 @@ var init_fullCheck = __esm({
19466
21443
  const account = { email: polled.account.email, plan: polled.account.plan };
19467
21444
  this.pending = null;
19468
21445
  this.lastUsage = null;
21446
+ this.lastLive = null;
19469
21447
  this.sessionCredentials = null;
21448
+ this.unremovedToken = null;
21449
+ this.forgetLiveCheck();
19470
21450
  try {
19471
21451
  await this.store.write({ apiBaseUrl: this.apiBaseUrl, accessToken: polled.accessToken, account });
19472
21452
  } catch (error) {
19473
- if (!(error instanceof CredentialPermissionError)) throw error;
19474
- await this.store.clear();
21453
+ if (error instanceof CredentialLocationError) throw error;
21454
+ const reason = error instanceof CredentialPermissionError ? error.message : `The sign-in file could not be saved (${error instanceof Error ? error.message : String(error)})`;
21455
+ if (!(error instanceof CredentialPermissionError)) console.warn(`VibeRaven: ${reason}; keeping this sign-in for this Studio session only.`);
21456
+ await this.store.clear().catch(() => void 0);
19475
21457
  this.sessionCredentials = { version: 1, apiBaseUrl: this.apiBaseUrl, accessToken: polled.accessToken, account, savedAt: (/* @__PURE__ */ new Date()).toISOString() };
21458
+ this.ignoreStoredCredentials = false;
19476
21459
  return {
19477
21460
  kind: "signed_in",
19478
21461
  account,
19479
- notice: `Signed in for this Studio session only. ${error.message}, so it was not saved to disk. You will need to sign in again next time you open the Studio.`
21462
+ notice: `Signed in for this Studio session only. ${reason}, so it was not saved to disk. You will need to sign in again next time you open the Studio.`
19480
21463
  };
19481
21464
  }
21465
+ this.ignoreStoredCredentials = false;
19482
21466
  return { kind: "signed_in", account };
19483
21467
  });
19484
21468
  }
19485
21469
  async signOut() {
19486
21470
  return this.guard(async () => {
19487
21471
  this.pending = null;
19488
- await this.clearCredentials();
19489
- return { kind: "signed_out" };
21472
+ if (await this.clearCredentials()) return { kind: "signed_out" };
21473
+ return {
21474
+ kind: "signed_out",
21475
+ notice: `Signed out in this Studio, but the saved sign-in at ${this.store.path} could not be removed. Delete that file, or sign out again once nothing else has it open.`
21476
+ };
19490
21477
  });
19491
21478
  }
19492
21479
  async prepare() {
@@ -19495,8 +21482,8 @@ var init_fullCheck = __esm({
19495
21482
  const built = await this.buildPayload(this.workspacePath);
19496
21483
  const final = finalizePayload(built.payload);
19497
21484
  this.prunePreviews();
19498
- const id = (0, import_node_crypto.randomUUID)();
19499
- this.previews.set(id, { payload: final.payload, idempotencyKey: (0, import_node_crypto.randomUUID)(), createdAt: Date.now() });
21485
+ const id = (0, import_node_crypto4.randomUUID)();
21486
+ this.previews.set(id, { payload: final.payload, idempotencyKey: (0, import_node_crypto4.randomUUID)(), createdAt: Date.now() });
19500
21487
  return { kind: "preview", preview: describePayload(id, `${this.apiBaseUrl}/v1/station/run`, final.payload, built.scan, final.redactions) };
19501
21488
  } catch (error) {
19502
21489
  if (error instanceof HostedCallError) return this.fromCallError(error);
@@ -19616,10 +21603,10 @@ async function connectProviderMcp(cwd, providerId) {
19616
21603
  if (!recipe) {
19617
21604
  return { ok: false, message: `No known MCP server for ${providerId} yet. Add one manually to .mcp.json.` };
19618
21605
  }
19619
- const configPath = (0, import_node_path34.join)(cwd, ".mcp.json");
21606
+ const configPath = (0, import_node_path36.join)(cwd, ".mcp.json");
19620
21607
  let config = {};
19621
21608
  try {
19622
- const raw = await (0, import_promises26.readFile)(configPath, "utf8");
21609
+ const raw = await (0, import_promises28.readFile)(configPath, "utf8");
19623
21610
  const parsed = JSON.parse(raw.replace(/^\uFEFF/, ""));
19624
21611
  if (!isRecord5(parsed) || parsed.mcpServers !== void 0 && !isRecord5(parsed.mcpServers)) {
19625
21612
  return { ok: false, message: "Existing .mcp.json has an unsupported structure. It was left unchanged." };
@@ -19636,7 +21623,7 @@ async function connectProviderMcp(cwd, providerId) {
19636
21623
  }
19637
21624
  servers[providerId] = recipe.entry;
19638
21625
  const next = { ...config, mcpServers: servers };
19639
- await (0, import_promises26.writeFile)(configPath, `${JSON.stringify(next, null, 2)}
21626
+ await (0, import_promises28.writeFile)(configPath, `${JSON.stringify(next, null, 2)}
19640
21627
  `, "utf8");
19641
21628
  const suffix = recipe.note ? ` ${recipe.note}` : "";
19642
21629
  return { ok: true, message: `Added ${providerId} MCP server to .mcp.json.${suffix}`, path: ".mcp.json" };
@@ -19648,7 +21635,7 @@ function headerValue(request, name) {
19648
21635
  function tokensMatch(given, expected) {
19649
21636
  const a = Buffer.from(given, "utf8");
19650
21637
  const b3 = Buffer.from(expected, "utf8");
19651
- return a.length === b3.length && (0, import_node_crypto2.timingSafeEqual)(a, b3);
21638
+ return a.length === b3.length && (0, import_node_crypto5.timingSafeEqual)(a, b3);
19652
21639
  }
19653
21640
  function guardRequest(request, response, pathname, guard) {
19654
21641
  const allowedHosts = [`127.0.0.1:${guard.port}`, `localhost:${guard.port}`];
@@ -19689,7 +21676,7 @@ async function defaultScanRunner(flags, positional) {
19689
21676
  }
19690
21677
  async function readPackageJson(cwd) {
19691
21678
  try {
19692
- const raw = await (0, import_promises26.readFile)((0, import_node_path34.join)(cwd, "package.json"), "utf8");
21679
+ const raw = await (0, import_promises28.readFile)((0, import_node_path36.join)(cwd, "package.json"), "utf8");
19693
21680
  const parsed = JSON.parse(raw.replace(/^\uFEFF/, ""));
19694
21681
  return isRecord5(parsed) ? parsed : {};
19695
21682
  } catch {
@@ -19712,7 +21699,7 @@ async function listProjectFiles(cwd, maxFiles = 1800) {
19712
21699
  if (files.length >= maxFiles) return;
19713
21700
  let entries;
19714
21701
  try {
19715
- entries = await (0, import_promises26.readdir)(dir, { withFileTypes: true });
21702
+ entries = await (0, import_promises28.readdir)(dir, { withFileTypes: true });
19716
21703
  } catch {
19717
21704
  return;
19718
21705
  }
@@ -19720,7 +21707,7 @@ async function listProjectFiles(cwd, maxFiles = 1800) {
19720
21707
  if (files.length >= maxFiles) return;
19721
21708
  const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
19722
21709
  if (entry.isDirectory()) {
19723
- if (!skipped.has(entry.name)) await walk((0, import_node_path34.join)(dir, entry.name), rel);
21710
+ if (!skipped.has(entry.name)) await walk((0, import_node_path36.join)(dir, entry.name), rel);
19724
21711
  continue;
19725
21712
  }
19726
21713
  if (entry.isFile()) files.push(rel.toLowerCase());
@@ -19739,7 +21726,7 @@ async function readEnvKeys(cwd) {
19739
21726
  const keys = /* @__PURE__ */ new Set();
19740
21727
  for (const name of [".env.example", ".env"]) {
19741
21728
  try {
19742
- const raw = await (0, import_promises26.readFile)((0, import_node_path34.join)(cwd, name), "utf8");
21729
+ const raw = await (0, import_promises28.readFile)((0, import_node_path36.join)(cwd, name), "utf8");
19743
21730
  for (const line of raw.split(/\r?\n/)) {
19744
21731
  const trimmed = line.trim();
19745
21732
  if (!trimmed || trimmed.startsWith("#")) continue;
@@ -19842,7 +21829,7 @@ function runGit(cwd, args, timeoutMs = 1500) {
19842
21829
  }
19843
21830
  function spawnGit(cwd, args, env, timeoutMs) {
19844
21831
  return new Promise((resolveGit) => {
19845
- const child = (0, import_node_child_process7.spawn)("git", args, {
21832
+ const child = (0, import_node_child_process8.spawn)("git", args, {
19846
21833
  cwd,
19847
21834
  env,
19848
21835
  windowsHide: true,
@@ -19915,7 +21902,7 @@ function runGitCommand(cwd, args, timeoutMs = 5e3) {
19915
21902
  }
19916
21903
  function spawnGitCommand(cwd, args, env, timeoutMs) {
19917
21904
  return new Promise((resolveCommand) => {
19918
- const child = (0, import_node_child_process7.spawn)("git", args, {
21905
+ const child = (0, import_node_child_process8.spawn)("git", args, {
19919
21906
  cwd,
19920
21907
  env,
19921
21908
  windowsHide: true,
@@ -20057,7 +22044,7 @@ async function codexModelOptions() {
20057
22044
  { id: "gpt-5.4-mini", label: "GPT-5.4 Mini" }
20058
22045
  ];
20059
22046
  try {
20060
- const raw = await (0, import_promises26.readFile)((0, import_node_path34.join)((0, import_node_os2.homedir)(), ".codex", "models_cache.json"), "utf8");
22047
+ const raw = await (0, import_promises28.readFile)((0, import_node_path36.join)((0, import_node_os2.homedir)(), ".codex", "models_cache.json"), "utf8");
20061
22048
  const parsed = JSON.parse(raw);
20062
22049
  const list = Array.isArray(parsed) ? parsed : parsed.models ?? [];
20063
22050
  const options = list.filter((model) => typeof model?.slug === "string").filter((model) => !/auto-review|reserve/i.test(model.slug)).map((model) => ({ id: model.slug, label: model.display_name?.trim() || model.slug }));
@@ -20081,7 +22068,7 @@ function cliModelDefaults(id) {
20081
22068
  }
20082
22069
  function detectCommand(command, cwd) {
20083
22070
  return new Promise((resolveDetect) => {
20084
- const child = (0, import_node_child_process7.spawn)(process.platform === "win32" ? "where.exe" : "sh", process.platform === "win32" ? [command] : ["-lc", `command -v ${command}`], {
22071
+ const child = (0, import_node_child_process8.spawn)(process.platform === "win32" ? "where.exe" : "sh", process.platform === "win32" ? [command] : ["-lc", `command -v ${command}`], {
20085
22072
  cwd,
20086
22073
  windowsHide: true,
20087
22074
  stdio: ["ignore", "pipe", "pipe"]
@@ -20168,7 +22155,7 @@ function setupCommandFor(cliId, step) {
20168
22155
  function runShellCommand(command, cwd, timeoutMs = 18e4) {
20169
22156
  return new Promise((resolveRun) => {
20170
22157
  const useCmd = process.platform === "win32";
20171
- const child = (0, import_node_child_process7.spawn)(useCmd ? process.env.ComSpec || "cmd.exe" : "sh", useCmd ? ["/d", "/s", "/c", command] : ["-lc", command], {
22158
+ const child = (0, import_node_child_process8.spawn)(useCmd ? process.env.ComSpec || "cmd.exe" : "sh", useCmd ? ["/d", "/s", "/c", command] : ["-lc", command], {
20172
22159
  cwd,
20173
22160
  windowsHide: true,
20174
22161
  stdio: ["ignore", "pipe", "pipe"]
@@ -20202,7 +22189,7 @@ function openSystemTerminal(command, cwd) {
20202
22189
  const fullCommand = process.platform === "win32" ? `cd /d ${quotedCwd} && ${command}` : `cd ${quotedCwd} && ${command}`;
20203
22190
  let child;
20204
22191
  if (process.platform === "win32") {
20205
- child = (0, import_node_child_process7.spawn)("cmd.exe", ["/c", "start", "cmd.exe", "/k", fullCommand], {
22192
+ child = (0, import_node_child_process8.spawn)("cmd.exe", ["/c", "start", "cmd.exe", "/k", fullCommand], {
20206
22193
  cwd,
20207
22194
  windowsHide: true,
20208
22195
  stdio: "ignore",
@@ -20210,9 +22197,9 @@ function openSystemTerminal(command, cwd) {
20210
22197
  });
20211
22198
  } else if (process.platform === "darwin") {
20212
22199
  const script = `tell application "Terminal" to do script "${fullCommand.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
20213
- child = (0, import_node_child_process7.spawn)("osascript", ["-e", script], { cwd, windowsHide: true, stdio: "ignore", detached: true });
22200
+ child = (0, import_node_child_process8.spawn)("osascript", ["-e", script], { cwd, windowsHide: true, stdio: "ignore", detached: true });
20214
22201
  } else {
20215
- child = (0, import_node_child_process7.spawn)("x-terminal-emulator", ["-e", `bash -lc ${JSON.stringify(fullCommand)}`], {
22202
+ child = (0, import_node_child_process8.spawn)("x-terminal-emulator", ["-e", `bash -lc ${JSON.stringify(fullCommand)}`], {
20216
22203
  cwd,
20217
22204
  windowsHide: true,
20218
22205
  stdio: "ignore",
@@ -20260,19 +22247,19 @@ function isPlaceholderProjectName(value) {
20260
22247
  async function readMcpEvidence(cwd) {
20261
22248
  const candidates = MCP_CONFIG_PATHS.map((relativePath) => ({
20262
22249
  source: relativePath.replace(/\\/g, "/"),
20263
- path: (0, import_node_path34.resolve)(cwd, relativePath)
22250
+ path: (0, import_node_path36.resolve)(cwd, relativePath)
20264
22251
  }));
20265
22252
  const home = process.env.USERPROFILE || process.env.HOME;
20266
22253
  if (home) {
20267
22254
  candidates.push({
20268
22255
  source: "~/.codex/config.toml",
20269
- path: (0, import_node_path34.resolve)(home, ".codex", "config.toml")
22256
+ path: (0, import_node_path36.resolve)(home, ".codex", "config.toml")
20270
22257
  });
20271
22258
  }
20272
22259
  const evidence = [];
20273
22260
  for (const candidate of candidates) {
20274
22261
  try {
20275
- const text = await (0, import_promises26.readFile)(candidate.path, "utf8");
22262
+ const text = await (0, import_promises28.readFile)(candidate.path, "utf8");
20276
22263
  if (/mcp/i.test(text)) evidence.push({ source: candidate.source, text });
20277
22264
  } catch {
20278
22265
  }
@@ -20363,7 +22350,7 @@ async function enrichProjectMetadata(state, cwd) {
20363
22350
  async function readState(cwd) {
20364
22351
  let state;
20365
22352
  try {
20366
- const raw = await (0, import_promises26.readFile)((0, import_node_path34.join)(cwd, ".viberaven", "last-scan.json"), "utf8");
22353
+ const raw = await (0, import_promises28.readFile)((0, import_node_path36.join)(cwd, ".viberaven", "last-scan.json"), "utf8");
20367
22354
  const packageJson = await readPackageJson(cwd);
20368
22355
  const deps = dependencyNames(packageJson);
20369
22356
  const files = await listProjectFiles(cwd);
@@ -20609,7 +22596,7 @@ function quoteWindowsCmdArg(value) {
20609
22596
  }
20610
22597
  async function whereExe(command, cwd) {
20611
22598
  return new Promise((resolvePath) => {
20612
- const child = (0, import_node_child_process7.spawn)("where.exe", [command], {
22599
+ const child = (0, import_node_child_process8.spawn)("where.exe", [command], {
20613
22600
  cwd,
20614
22601
  windowsHide: true,
20615
22602
  stdio: ["ignore", "pipe", "ignore"]
@@ -20629,8 +22616,8 @@ async function whereExe(command, cwd) {
20629
22616
  });
20630
22617
  }
20631
22618
  function expandShimPath(value, base) {
20632
- const relative7 = value.replace(/^%(?:dp0%\\+|~dp0\\*)/i, "");
20633
- return relative7 === value ? value : (0, import_node_path34.join)(base, ...relative7.split(/[\\/]+/));
22619
+ const relative8 = value.replace(/^%(?:dp0%\\+|~dp0\\*)/i, "");
22620
+ return relative8 === value ? value : (0, import_node_path36.join)(base, ...relative8.split(/[\\/]+/));
20634
22621
  }
20635
22622
  function findShimTarget(lines, pattern, accept = () => true) {
20636
22623
  for (const line of lines) {
@@ -20641,21 +22628,21 @@ function findShimTarget(lines, pattern, accept = () => true) {
20641
22628
  }
20642
22629
  async function resolveWindowsShimTarget(shimPath) {
20643
22630
  try {
20644
- const content = await (0, import_promises26.readFile)(shimPath, "utf8");
20645
- const base = (0, import_node_path34.dirname)(shimPath);
22631
+ const content = await (0, import_promises28.readFile)(shimPath, "utf8");
22632
+ const base = (0, import_node_path36.dirname)(shimPath);
20646
22633
  const lines = content.split(/\r?\n/);
20647
22634
  const trailingArgs = (line, after) => [...line.slice(after).split("%*")[0].matchAll(/"([^"]*)"|([^\s"&|<>]+)/g)].map((token) => expandShimPath(token[1] ?? token[2], base)).filter((value) => value.length > 0 && !value.includes("%"));
20648
22635
  const script = findShimTarget(lines, /"%(?:dp0%\\+|~dp0\\*)([^"]+\.js)"/i);
20649
22636
  if (script) {
20650
22637
  return {
20651
22638
  command: process.execPath,
20652
- args: [(0, import_node_path34.join)(base, ...script.match[1].split(/[\\/]+/)), ...trailingArgs(script.line, script.match.index + script.match[0].length)]
22639
+ args: [(0, import_node_path36.join)(base, ...script.match[1].split(/[\\/]+/)), ...trailingArgs(script.line, script.match.index + script.match[0].length)]
20653
22640
  };
20654
22641
  }
20655
- const binary = findShimTarget(lines, /"%(?:dp0%\\+|~dp0\\*)([^"]+\.exe)"/i, (target) => (0, import_node_path34.basename)(target).toLowerCase() !== "node.exe");
22642
+ const binary = findShimTarget(lines, /"%(?:dp0%\\+|~dp0\\*)([^"]+\.exe)"/i, (target) => (0, import_node_path36.basename)(target).toLowerCase() !== "node.exe");
20656
22643
  if (binary) {
20657
22644
  return {
20658
- command: (0, import_node_path34.join)(base, ...binary.match[1].split(/[\\/]+/)),
22645
+ command: (0, import_node_path36.join)(base, ...binary.match[1].split(/[\\/]+/)),
20659
22646
  args: trailingArgs(binary.line, binary.match.index + binary.match[0].length)
20660
22647
  };
20661
22648
  }
@@ -20676,7 +22663,7 @@ function killProcessTree(pid, fallback) {
20676
22663
  return;
20677
22664
  }
20678
22665
  if (process.platform === "win32") {
20679
- const killer = (0, import_node_child_process7.spawn)("taskkill", ["/pid", String(pid), "/T", "/F"], { windowsHide: true, stdio: "ignore" });
22666
+ const killer = (0, import_node_child_process8.spawn)("taskkill", ["/pid", String(pid), "/T", "/F"], { windowsHide: true, stdio: "ignore" });
20680
22667
  killer.once("error", fallback);
20681
22668
  killer.once("close", (code) => {
20682
22669
  if (code !== 0) fallback();
@@ -20701,7 +22688,7 @@ async function runAgentCommand(command, args, cwd, stdin, runId) {
20701
22688
  const spawnCommand = shim?.command ?? (useCmdShim ? process.env.ComSpec || "cmd.exe" : command);
20702
22689
  const spawnArgs = shim ? [...shim.args, ...args] : useCmdShim ? ["/d", "/s", "/c", [command, ...args.map(quoteWindowsCmdArg)].join(" ")] : args;
20703
22690
  return new Promise((resolveRun) => {
20704
- const child = (0, import_node_child_process7.spawn)(spawnCommand, spawnArgs, {
22691
+ const child = (0, import_node_child_process8.spawn)(spawnCommand, spawnArgs, {
20705
22692
  cwd,
20706
22693
  windowsHide: true,
20707
22694
  // A separate process group lets Stop and timeout terminate descendants on Unix too.
@@ -20780,8 +22767,8 @@ async function writeChatContextArtifact(cwd, request, prompt) {
20780
22767
  "",
20781
22768
  prompt
20782
22769
  ].filter((line) => typeof line === "string");
20783
- await (0, import_promises26.mkdir)((0, import_node_path34.join)(cwd, ".viberaven"), { recursive: true });
20784
- await (0, import_promises26.writeFile)((0, import_node_path34.join)(cwd, ".viberaven", "chat-context.md"), `${lines.join("\n")}
22770
+ await (0, import_promises28.mkdir)((0, import_node_path36.join)(cwd, ".viberaven"), { recursive: true });
22771
+ await (0, import_promises28.writeFile)((0, import_node_path36.join)(cwd, ".viberaven", "chat-context.md"), `${lines.join("\n")}
20785
22772
  `, "utf8");
20786
22773
  } catch {
20787
22774
  }
@@ -20801,13 +22788,13 @@ async function runAgentChat(cwd, request) {
20801
22788
  }
20802
22789
  const prompt = await buildAgentChatPrompt(request, cwd);
20803
22790
  await writeChatContextArtifact(cwd, request, prompt);
20804
- const outputPath = request.cliId === "codex" ? (0, import_node_path34.join)(cwd, ".viberaven", `studio-agent-${Date.now()}-${Math.random().toString(36).slice(2)}.md`) : void 0;
20805
- if (outputPath) await (0, import_promises26.mkdir)((0, import_node_path34.dirname)(outputPath), { recursive: true });
22791
+ const outputPath = request.cliId === "codex" ? (0, import_node_path36.join)(cwd, ".viberaven", `studio-agent-${Date.now()}-${Math.random().toString(36).slice(2)}.md`) : void 0;
22792
+ if (outputPath) await (0, import_promises28.mkdir)((0, import_node_path36.dirname)(outputPath), { recursive: true });
20806
22793
  const { command, args, stdin } = commandForAgentChat(request, prompt, cwd, outputPath);
20807
22794
  const result = await runAgentCommand(command, args, cwd, stdin, request.runId);
20808
22795
  if (outputPath) {
20809
22796
  try {
20810
- const finalMessage = (await (0, import_promises26.readFile)(outputPath, "utf8")).trim();
22797
+ const finalMessage = (await (0, import_promises28.readFile)(outputPath, "utf8")).trim();
20811
22798
  if (finalMessage) return { ...result, output: finalMessage };
20812
22799
  } catch {
20813
22800
  }
@@ -20819,8 +22806,8 @@ function firstGapId(state) {
20819
22806
  return state.providers.find((provider2) => provider2.nextFix?.id)?.nextFix?.id;
20820
22807
  }
20821
22808
  async function runLocalAction(cwd, intent) {
20822
- const artifactDir = (0, import_node_path34.join)(cwd, ".viberaven");
20823
- await (0, import_promises26.mkdir)(artifactDir, { recursive: true });
22809
+ const artifactDir = (0, import_node_path36.join)(cwd, ".viberaven");
22810
+ await (0, import_promises28.mkdir)(artifactDir, { recursive: true });
20824
22811
  if (intent === "preview_rehearsal") {
20825
22812
  const state2 = await readState(cwd);
20826
22813
  const topProvider = state2.providers[0];
@@ -20861,7 +22848,7 @@ async function runLocalAction(cwd, intent) {
20861
22848
  proof,
20862
22849
  ""
20863
22850
  ].join("\n");
20864
- await (0, import_promises26.writeFile)((0, import_node_path34.join)(artifactDir, "preview-rehearsal.md"), markdown, "utf8");
22851
+ await (0, import_promises28.writeFile)((0, import_node_path36.join)(artifactDir, "preview-rehearsal.md"), markdown, "utf8");
20865
22852
  return {
20866
22853
  title: "Preview rehearsal ready",
20867
22854
  summary: `${projectName}: ${gateLabel} with ${state2.providers.length} provider beat${state2.providers.length === 1 ? "" : "s"}`,
@@ -20871,7 +22858,7 @@ async function runLocalAction(cwd, intent) {
20871
22858
  if (intent === "audit_vercel_supabase") {
20872
22859
  const auditInput = await collectVercelSupabaseAuditInput(cwd);
20873
22860
  const result2 = buildVercelSupabaseAudit(auditInput);
20874
- await (0, import_promises26.writeFile)((0, import_node_path34.join)(artifactDir, "audit-vercel-supabase.txt"), `${renderVercelSupabaseAudit(result2)}
22861
+ await (0, import_promises28.writeFile)((0, import_node_path36.join)(artifactDir, "audit-vercel-supabase.txt"), `${renderVercelSupabaseAudit(result2)}
20875
22862
  `, "utf8");
20876
22863
  return {
20877
22864
  title: result2.status === "pass" ? "Audit passed" : "Audit needs work",
@@ -20899,7 +22886,7 @@ async function runLocalAction(cwd, intent) {
20899
22886
  const mode = intent === "heal_prompt" ? "prompt" : intent === "heal_apply" ? "apply" : "plan";
20900
22887
  const result = await runHealCommand({ cwd, mode, gapId, yes: mode === "apply", noVerify: true });
20901
22888
  if (mode === "apply") {
20902
- await (0, import_promises26.writeFile)((0, import_node_path34.join)(artifactDir, "heal-apply-result.json"), `${JSON.stringify(result, null, 2)}
22889
+ await (0, import_promises28.writeFile)((0, import_node_path36.join)(artifactDir, "heal-apply-result.json"), `${JSON.stringify(result, null, 2)}
20903
22890
  `, "utf8");
20904
22891
  }
20905
22892
  return {
@@ -20909,14 +22896,14 @@ async function runLocalAction(cwd, intent) {
20909
22896
  };
20910
22897
  }
20911
22898
  async function sendArtifact(cwd, rel, response, contentType = "text/plain; charset=utf-8") {
20912
- const absolute = (0, import_node_path34.resolve)(cwd, rel);
20913
- const root = (0, import_node_path34.resolve)(cwd);
22899
+ const absolute = (0, import_node_path36.resolve)(cwd, rel);
22900
+ const root = (0, import_node_path36.resolve)(cwd);
20914
22901
  if (!absolute.startsWith(root)) {
20915
22902
  sendText(response, 403, "Forbidden");
20916
22903
  return;
20917
22904
  }
20918
22905
  try {
20919
- const text = await (0, import_promises26.readFile)(absolute, "utf8");
22906
+ const text = await (0, import_promises28.readFile)(absolute, "utf8");
20920
22907
  response.writeHead(200, { "content-type": contentType });
20921
22908
  response.end(text);
20922
22909
  } catch {
@@ -20925,7 +22912,7 @@ async function sendArtifact(cwd, rel, response, contentType = "text/plain; chars
20925
22912
  }
20926
22913
  async function sendReportArtifact(cwd, response) {
20927
22914
  try {
20928
- const text = await (0, import_promises26.readFile)((0, import_node_path34.join)(cwd, ".viberaven", "report.html"), "utf8");
22915
+ const text = await (0, import_promises28.readFile)((0, import_node_path36.join)(cwd, ".viberaven", "report.html"), "utf8");
20929
22916
  response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
20930
22917
  response.end(text);
20931
22918
  return;
@@ -21052,7 +23039,7 @@ async function sendReportArtifact(cwd, response) {
21052
23039
  }
21053
23040
  async function sendExtensionIcon(cwd, response) {
21054
23041
  try {
21055
- const body = await (0, import_promises26.readFile)((0, import_node_path34.join)(cwd, "media", "extension-icon.png"));
23042
+ const body = await (0, import_promises28.readFile)((0, import_node_path36.join)(cwd, "media", "extension-icon.png"));
21056
23043
  sendBinary(response, 200, "image/png", body);
21057
23044
  } catch {
21058
23045
  sendText(response, 404, "Icon not found");
@@ -21065,15 +23052,15 @@ async function sendMangaAsset(pathname, response) {
21065
23052
  return;
21066
23053
  }
21067
23054
  const candidates = [
21068
- (0, import_node_path34.join)(__dirname, "manga", name),
23055
+ (0, import_node_path36.join)(__dirname, "manga", name),
21069
23056
  // installed + dev: <pkg>/dist/manga
21070
- (0, import_node_path34.join)(__dirname, "..", "assets", "manga", name),
23057
+ (0, import_node_path36.join)(__dirname, "..", "assets", "manga", name),
21071
23058
  // fallback: <pkg>/assets/manga (source)
21072
- (0, import_node_path34.join)(__dirname, "..", "..", "assets", "manga", name)
23059
+ (0, import_node_path36.join)(__dirname, "..", "..", "assets", "manga", name)
21073
23060
  ];
21074
23061
  for (const candidate of candidates) {
21075
23062
  try {
21076
- const body = await (0, import_promises26.readFile)(candidate);
23063
+ const body = await (0, import_promises28.readFile)(candidate);
21077
23064
  response.setHeader("cache-control", "no-cache");
21078
23065
  sendBinary(response, 200, contentTypeForAsset(candidate), body);
21079
23066
  return;
@@ -21083,7 +23070,7 @@ async function sendMangaAsset(pathname, response) {
21083
23070
  sendText(response, 404, "Asset not found");
21084
23071
  }
21085
23072
  function contentTypeForAsset(pathname) {
21086
- switch ((0, import_node_path34.extname)(pathname).toLowerCase()) {
23073
+ switch ((0, import_node_path36.extname)(pathname).toLowerCase()) {
21087
23074
  case ".svg":
21088
23075
  return "image/svg+xml; charset=utf-8";
21089
23076
  case ".png":
@@ -21100,14 +23087,14 @@ function contentTypeForAsset(pathname) {
21100
23087
  }
21101
23088
  async function sendBundledReportAsset(pathname, response) {
21102
23089
  const rel = decodeURIComponent(pathname.replace(/^\/report\//, ""));
21103
- const assetRoot = (0, import_node_path34.resolve)(getBundledReportAssetsDir());
21104
- const absolute = (0, import_node_path34.resolve)(assetRoot, rel);
23090
+ const assetRoot = (0, import_node_path36.resolve)(getBundledReportAssetsDir());
23091
+ const absolute = (0, import_node_path36.resolve)(assetRoot, rel);
21105
23092
  if (!absolute.startsWith(assetRoot)) {
21106
23093
  sendText(response, 403, "Forbidden");
21107
23094
  return;
21108
23095
  }
21109
23096
  try {
21110
- const body = await (0, import_promises26.readFile)(absolute);
23097
+ const body = await (0, import_promises28.readFile)(absolute);
21111
23098
  sendBinary(response, 200, contentTypeForAsset(absolute), body);
21112
23099
  } catch {
21113
23100
  sendText(response, 404, "Asset not found");
@@ -21129,18 +23116,18 @@ async function bundleStackClient(entryPath) {
21129
23116
  return output.text;
21130
23117
  }
21131
23118
  async function sendStackAppJs(response) {
21132
- const distPath = (0, import_node_path34.join)(__dirname, "stack", "app.js");
23119
+ const distPath = (0, import_node_path36.join)(__dirname, "stack", "app.js");
21133
23120
  try {
21134
- const body = await (0, import_promises26.readFile)(distPath);
23121
+ const body = await (0, import_promises28.readFile)(distPath);
21135
23122
  response.writeHead(200, { "content-type": "application/javascript; charset=utf-8", "cache-control": "no-store" });
21136
23123
  response.end(body);
21137
23124
  return;
21138
23125
  } catch {
21139
23126
  }
21140
- const entryPath = (0, import_node_path34.join)(__dirname, "stack", "main.ts");
23127
+ const entryPath = (0, import_node_path36.join)(__dirname, "stack", "main.ts");
21141
23128
  let mtimeMs;
21142
23129
  try {
21143
- mtimeMs = (await (0, import_promises26.stat)(entryPath)).mtimeMs;
23130
+ mtimeMs = (await (0, import_promises28.stat)(entryPath)).mtimeMs;
21144
23131
  } catch {
21145
23132
  sendText(response, 404, "Stack client entry not found: src/local-ui/stack/main.ts is missing.");
21146
23133
  return;
@@ -21159,7 +23146,7 @@ async function sendStackAppJs(response) {
21159
23146
  }
21160
23147
  async function sendStackAppCss(response) {
21161
23148
  try {
21162
- const body = await (0, import_promises26.readFile)((0, import_node_path34.join)(__dirname, "stack", "stack.css"));
23149
+ const body = await (0, import_promises28.readFile)((0, import_node_path36.join)(__dirname, "stack", "stack.css"));
21163
23150
  response.writeHead(200, { "content-type": "text/css; charset=utf-8", "cache-control": "no-store" });
21164
23151
  response.end(body);
21165
23152
  } catch {
@@ -21168,7 +23155,7 @@ async function sendStackAppCss(response) {
21168
23155
  }
21169
23156
  async function readProviderVerifyLedger(cwd) {
21170
23157
  try {
21171
- const raw = await (0, import_promises26.readFile)((0, import_node_path34.join)(cwd, ".viberaven", "provider-verify.json"), "utf8");
23158
+ const raw = await (0, import_promises28.readFile)((0, import_node_path36.join)(cwd, ".viberaven", "provider-verify.json"), "utf8");
21172
23159
  const parsed = JSON.parse(raw.replace(/^\uFEFF/, ""));
21173
23160
  return isRecord5(parsed) ? parsed : {};
21174
23161
  } catch {
@@ -21178,8 +23165,8 @@ async function readProviderVerifyLedger(cwd) {
21178
23165
  async function writeProviderVerifyEntry(cwd, providerId, entry) {
21179
23166
  const ledger = await readProviderVerifyLedger(cwd);
21180
23167
  ledger[providerId] = entry;
21181
- await (0, import_promises26.mkdir)((0, import_node_path34.join)(cwd, ".viberaven"), { recursive: true });
21182
- await (0, import_promises26.writeFile)((0, import_node_path34.join)(cwd, ".viberaven", "provider-verify.json"), `${JSON.stringify(ledger, null, 2)}
23168
+ await (0, import_promises28.mkdir)((0, import_node_path36.join)(cwd, ".viberaven"), { recursive: true });
23169
+ await (0, import_promises28.writeFile)((0, import_node_path36.join)(cwd, ".viberaven", "provider-verify.json"), `${JSON.stringify(ledger, null, 2)}
21183
23170
  `, "utf8");
21184
23171
  }
21185
23172
  async function handleRequest(request, response, options) {
@@ -21240,13 +23227,13 @@ async function handleRequest(request, response, options) {
21240
23227
  return;
21241
23228
  }
21242
23229
  if (request.method === "GET" && url.pathname === "/api/architecture-map") {
21243
- const mapPath = (0, import_node_path34.join)(options.cwd, ".viberaven", "architecture-map.md");
21244
- const markdown = await (0, import_promises26.readFile)(mapPath, "utf8").catch(() => null);
23230
+ const mapPath = (0, import_node_path36.join)(options.cwd, ".viberaven", "architecture-map.md");
23231
+ const markdown = await (0, import_promises28.readFile)(mapPath, "utf8").catch(() => null);
21245
23232
  if (markdown === null) {
21246
23233
  sendJsonStatus(response, 404, { error: "No architecture map yet. Run a scan first (it is written on every scan)." });
21247
23234
  return;
21248
23235
  }
21249
- const dataRaw = await (0, import_promises26.readFile)((0, import_node_path34.join)(options.cwd, ".viberaven", "architecture-map.json"), "utf8").catch(() => null);
23236
+ const dataRaw = await (0, import_promises28.readFile)((0, import_node_path36.join)(options.cwd, ".viberaven", "architecture-map.json"), "utf8").catch(() => null);
21250
23237
  let data = null;
21251
23238
  if (dataRaw) {
21252
23239
  try {
@@ -21255,7 +23242,7 @@ async function handleRequest(request, response, options) {
21255
23242
  data = null;
21256
23243
  }
21257
23244
  }
21258
- const contextRaw = await (0, import_promises26.readFile)((0, import_node_path34.join)(options.cwd, ".viberaven", "context-map.json"), "utf8").catch(() => null);
23245
+ const contextRaw = await (0, import_promises28.readFile)((0, import_node_path36.join)(options.cwd, ".viberaven", "context-map.json"), "utf8").catch(() => null);
21259
23246
  let gaps = [];
21260
23247
  if (contextRaw) {
21261
23248
  try {
@@ -21521,35 +23508,35 @@ async function handleRequest(request, response, options) {
21521
23508
  return;
21522
23509
  }
21523
23510
  if (request.method === "GET" && url.pathname === "/api/artifacts/tasklist") {
21524
- await sendArtifact(options.cwd, (0, import_node_path34.join)(".viberaven", "agent-tasklist.md"), response);
23511
+ await sendArtifact(options.cwd, (0, import_node_path36.join)(".viberaven", "agent-tasklist.md"), response);
21525
23512
  return;
21526
23513
  }
21527
23514
  if (request.method === "GET" && url.pathname === "/api/artifacts/playbook") {
21528
- await sendArtifact(options.cwd, (0, import_node_path34.join)(".viberaven", "launch-playbook.md"), response);
23515
+ await sendArtifact(options.cwd, (0, import_node_path36.join)(".viberaven", "launch-playbook.md"), response);
21529
23516
  return;
21530
23517
  }
21531
23518
  if (request.method === "GET" && url.pathname === "/api/artifacts/audit") {
21532
- await sendArtifact(options.cwd, (0, import_node_path34.join)(".viberaven", "audit-vercel-supabase.txt"), response);
23519
+ await sendArtifact(options.cwd, (0, import_node_path36.join)(".viberaven", "audit-vercel-supabase.txt"), response);
21533
23520
  return;
21534
23521
  }
21535
23522
  if (request.method === "GET" && url.pathname === "/api/artifacts/cleanup") {
21536
- await sendArtifact(options.cwd, (0, import_node_path34.join)(".viberaven", "context-cleanup.md"), response);
23523
+ await sendArtifact(options.cwd, (0, import_node_path36.join)(".viberaven", "context-cleanup.md"), response);
21537
23524
  return;
21538
23525
  }
21539
23526
  if (request.method === "GET" && url.pathname === "/api/artifacts/preview") {
21540
- await sendArtifact(options.cwd, (0, import_node_path34.join)(".viberaven", "preview-rehearsal.md"), response);
23527
+ await sendArtifact(options.cwd, (0, import_node_path36.join)(".viberaven", "preview-rehearsal.md"), response);
21541
23528
  return;
21542
23529
  }
21543
23530
  if (request.method === "GET" && url.pathname === "/api/artifacts/heal-plan") {
21544
- await sendArtifact(options.cwd, (0, import_node_path34.join)(".viberaven", "heal-plan.md"), response);
23531
+ await sendArtifact(options.cwd, (0, import_node_path36.join)(".viberaven", "heal-plan.md"), response);
21545
23532
  return;
21546
23533
  }
21547
23534
  if (request.method === "GET" && url.pathname === "/api/artifacts/heal-prompt") {
21548
- await sendArtifact(options.cwd, (0, import_node_path34.join)(".viberaven", "heal-prompt.md"), response);
23535
+ await sendArtifact(options.cwd, (0, import_node_path36.join)(".viberaven", "heal-prompt.md"), response);
21549
23536
  return;
21550
23537
  }
21551
23538
  if (request.method === "GET" && url.pathname === "/api/artifacts/heal-apply") {
21552
- await sendArtifact(options.cwd, (0, import_node_path34.join)(".viberaven", "heal-apply-result.json"), response, "application/json; charset=utf-8");
23539
+ await sendArtifact(options.cwd, (0, import_node_path36.join)(".viberaven", "heal-apply-result.json"), response, "application/json; charset=utf-8");
21553
23540
  return;
21554
23541
  }
21555
23542
  if (request.method === "GET" && url.pathname === "/api/artifacts/report") {
@@ -21575,12 +23562,12 @@ async function handleRequest(request, response, options) {
21575
23562
  }
21576
23563
  if (request.method === "GET" && url.pathname === "/vendor/anime.umd.min.js") {
21577
23564
  for (const candidate of [
21578
- (0, import_node_path34.join)(__dirname, "vendor", "anime.umd.min.js"),
21579
- (0, import_node_path34.join)(__dirname, "..", "assets", "vendor", "anime.umd.min.js"),
21580
- (0, import_node_path34.join)(__dirname, "..", "..", "assets", "vendor", "anime.umd.min.js")
23565
+ (0, import_node_path36.join)(__dirname, "vendor", "anime.umd.min.js"),
23566
+ (0, import_node_path36.join)(__dirname, "..", "assets", "vendor", "anime.umd.min.js"),
23567
+ (0, import_node_path36.join)(__dirname, "..", "..", "assets", "vendor", "anime.umd.min.js")
21581
23568
  ]) {
21582
23569
  try {
21583
- const body = await (0, import_promises26.readFile)(candidate);
23570
+ const body = await (0, import_promises28.readFile)(candidate);
21584
23571
  response.setHeader("cache-control", "public, max-age=604800, immutable");
21585
23572
  sendBinary(response, 200, "text/javascript; charset=utf-8", body);
21586
23573
  return;
@@ -21797,11 +23784,11 @@ function listen(server, startPort) {
21797
23784
  });
21798
23785
  }
21799
23786
  async function startLocalUiServer(options) {
21800
- const cwd = (0, import_node_path34.resolve)(options.cwd);
23787
+ const cwd = (0, import_node_path36.resolve)(options.cwd);
21801
23788
  const scanRunner = options.scanRunner ?? defaultScanRunner;
21802
23789
  const autoScan = { state: "idle" };
21803
23790
  const fullCheck = new FullCheckService(cwd, options.fullCheck);
21804
- const guard = { token: (0, import_node_crypto2.randomBytes)(32).toString("base64url"), port: 0 };
23791
+ const guard = { token: (0, import_node_crypto5.randomBytes)(32).toString("base64url"), port: 0 };
21805
23792
  const server = (0, import_node_http.createServer)((request, response) => {
21806
23793
  const pathname = new URL(request.url ?? "/", "http://127.0.0.1").pathname;
21807
23794
  if (!guardRequest(request, response, pathname, guard)) {
@@ -21814,7 +23801,7 @@ async function startLocalUiServer(options) {
21814
23801
  });
21815
23802
  const port = await listen(server, options.port ?? 4177);
21816
23803
  guard.port = port;
21817
- const hasArtifact = await (0, import_promises26.readFile)((0, import_node_path34.join)(cwd, ".viberaven", "last-scan.json"), "utf8").then(
23804
+ const hasArtifact = await (0, import_promises28.readFile)((0, import_node_path36.join)(cwd, ".viberaven", "last-scan.json"), "utf8").then(
21818
23805
  () => true,
21819
23806
  () => false
21820
23807
  );
@@ -21853,16 +23840,16 @@ async function waitForServerShutdown() {
21853
23840
  process.once("SIGTERM", done);
21854
23841
  });
21855
23842
  }
21856
- var import_node_http, import_node_child_process7, import_node_crypto2, import_promises26, import_node_os2, import_node_path34, CLI_AGENT_SETUP, MCP_CONFIG_PATHS, MCP_PROVIDER_NEEDLES, MCP_CONNECT_COMMANDS, MCP_SERVER_RECIPES, STUDIO_TOKEN_HEADER, PROVIDER_IDS, STUDIO_STATUS_ARGS, CLI_AUTH_PROBE, AGENT_RUN_TIMEOUT_MS, activeAgentRuns, stackBundleCache;
23843
+ var import_node_http, import_node_child_process8, import_node_crypto5, import_promises28, import_node_os2, import_node_path36, CLI_AGENT_SETUP, MCP_CONFIG_PATHS, MCP_PROVIDER_NEEDLES, MCP_CONNECT_COMMANDS, MCP_SERVER_RECIPES, STUDIO_TOKEN_HEADER, PROVIDER_IDS, STUDIO_STATUS_ARGS, CLI_AUTH_PROBE, AGENT_RUN_TIMEOUT_MS, activeAgentRuns, stackBundleCache;
21857
23844
  var init_server = __esm({
21858
23845
  "src/local-ui/server.ts"() {
21859
23846
  "use strict";
21860
23847
  import_node_http = require("node:http");
21861
- import_node_child_process7 = require("node:child_process");
21862
- import_node_crypto2 = require("node:crypto");
21863
- import_promises26 = require("node:fs/promises");
23848
+ import_node_child_process8 = require("node:child_process");
23849
+ import_node_crypto5 = require("node:crypto");
23850
+ import_promises28 = require("node:fs/promises");
21864
23851
  import_node_os2 = require("node:os");
21865
- import_node_path34 = require("node:path");
23852
+ import_node_path36 = require("node:path");
21866
23853
  init_cleanPlan();
21867
23854
  init_audit();
21868
23855
  init_heal();
@@ -21903,8 +23890,8 @@ var init_server = __esm({
21903
23890
  MCP_CONFIG_PATHS = [
21904
23891
  ".mcp.json",
21905
23892
  "mcp.json",
21906
- (0, import_node_path34.join)(".cursor", "mcp.json"),
21907
- (0, import_node_path34.join)(".vscode", "mcp.json")
23893
+ (0, import_node_path36.join)(".cursor", "mcp.json"),
23894
+ (0, import_node_path36.join)(".vscode", "mcp.json")
21908
23895
  ];
21909
23896
  MCP_PROVIDER_NEEDLES = {
21910
23897
  supabase: ["supabase", "@supabase/mcp", "mcp-server-supabase"],
@@ -22063,9 +24050,9 @@ var init_nextActionBlock = __esm({
22063
24050
  function defaultMcpConfigPaths() {
22064
24051
  const home = (0, import_node_os3.homedir)();
22065
24052
  return [
22066
- (0, import_node_path35.join)(home, ".config", "claude", "claude_desktop_config.json"),
22067
- (0, import_node_path35.join)(home, ".cursor", "mcp.json"),
22068
- (0, import_node_path35.join)(home, ".gemini", "antigravity", "mcp_config.json")
24053
+ (0, import_node_path37.join)(home, ".config", "claude", "claude_desktop_config.json"),
24054
+ (0, import_node_path37.join)(home, ".cursor", "mcp.json"),
24055
+ (0, import_node_path37.join)(home, ".gemini", "antigravity", "mcp_config.json")
22069
24056
  ];
22070
24057
  }
22071
24058
  function resolveConfigPaths() {
@@ -22142,13 +24129,13 @@ async function verifyProviderGap(options) {
22142
24129
  fallbackCommand: FALLBACK_COMMAND
22143
24130
  };
22144
24131
  }
22145
- var import_node_fs13, import_node_os3, import_node_path35, FALLBACK_COMMAND, SUPPORTED_PROVIDERS, configPathsOverride;
24132
+ var import_node_fs13, import_node_os3, import_node_path37, FALLBACK_COMMAND, SUPPORTED_PROVIDERS, configPathsOverride;
22146
24133
  var init_providerMcpBridge = __esm({
22147
24134
  "src/providerMcpBridge.ts"() {
22148
24135
  "use strict";
22149
24136
  import_node_fs13 = require("node:fs");
22150
24137
  import_node_os3 = require("node:os");
22151
- import_node_path35 = require("node:path");
24138
+ import_node_path37 = require("node:path");
22152
24139
  FALLBACK_COMMAND = "npx -y viberaven audit --vercel-supabase --json";
22153
24140
  SUPPORTED_PROVIDERS = /* @__PURE__ */ new Set(["supabase", "vercel"]);
22154
24141
  }
@@ -22232,6 +24219,13 @@ Usage:
22232
24219
  viberaven audit --vercel-supabase [--json] [path]
22233
24220
  Local Vercel/Supabase repo evidence audit (RLS, pooler, secrets)
22234
24221
 
24222
+ viberaven clean --plan [path]
24223
+ Write a non-destructive context cleanup review plan (.viberaven/context-cleanup.md)
24224
+
24225
+ viberaven rls-test [--matrix viberaven.permissions.json] [--migrations dir] [--json] [path]
24226
+ Experimental: run the Supabase migrations in a PGlite imitation and test who can
24227
+ read, change and delete rows against your permissions matrix (non-authoritative)
24228
+
22235
24229
 
22236
24230
 
22237
24231
  Agent workflow (Claude Code / Codex / Gemini):
@@ -22352,7 +24346,7 @@ async function cmdProviderVerify(flags, positional) {
22352
24346
  return result.verified ? 0 : 1;
22353
24347
  }
22354
24348
  async function cmdStatus(flags, positional) {
22355
- const startDir = positional[0] ? (0, import_node_path36.resolve)(process.cwd(), positional[0]) : process.cwd();
24349
+ const startDir = positional[0] ? (0, import_node_path38.resolve)(process.cwd(), positional[0]) : process.cwd();
22356
24350
  let artifact;
22357
24351
  try {
22358
24352
  artifact = await loadLastArtifact(startDir);
@@ -22391,7 +24385,7 @@ async function cmdStatus(flags, positional) {
22391
24385
  return 0;
22392
24386
  }
22393
24387
  async function runScanCommand(flags, positional, options) {
22394
- const workspacePath = positional[0] ? (0, import_node_path36.resolve)(process.cwd(), positional[0]) : await resolveWorkspaceRoot(process.cwd());
24388
+ const workspacePath = positional[0] ? (0, import_node_path38.resolve)(process.cwd(), positional[0]) : await resolveWorkspaceRoot(process.cwd());
22395
24389
  if (!options?.deferMachineOutput) {
22396
24390
  console.log(`Scanning ${workspacePath}\u2026 (local, offline)`);
22397
24391
  }
@@ -22418,7 +24412,7 @@ async function runScanCommand(flags, positional, options) {
22418
24412
  printNextActionBlock(block);
22419
24413
  printProviderActionBlock(tasks);
22420
24414
  try {
22421
- const manifest = JSON.parse(await (0, import_promises27.readFile)(paths.actionsPath, "utf8"));
24415
+ const manifest = JSON.parse(await (0, import_promises29.readFile)(paths.actionsPath, "utf8"));
22422
24416
  console.log(renderActionSurface(manifest, { limit: 5 }).trimEnd());
22423
24417
  } catch (error) {
22424
24418
  console.warn(error instanceof Error ? error.message : String(error));
@@ -22435,7 +24429,7 @@ async function runScanCommand(flags, positional, options) {
22435
24429
  return { exitCode: 0, artifacts: paths };
22436
24430
  }
22437
24431
  async function cmdReport(flags, positional) {
22438
- const startDir = positional[0] ? (0, import_node_path36.resolve)(process.cwd(), positional[0]) : process.cwd();
24432
+ const startDir = positional[0] ? (0, import_node_path38.resolve)(process.cwd(), positional[0]) : process.cwd();
22439
24433
  try {
22440
24434
  const paths = await refreshReportFromDisk(startDir);
22441
24435
  console.log(`Report refreshed: ${paths.reportPath}`);
@@ -22457,7 +24451,7 @@ async function cmdReport(flags, positional) {
22457
24451
  }
22458
24452
  }
22459
24453
  async function cmdPrompt(flags, positional) {
22460
- const startDir = positional[0] ? (0, import_node_path36.resolve)(process.cwd(), positional[0]) : process.cwd();
24454
+ const startDir = positional[0] ? (0, import_node_path38.resolve)(process.cwd(), positional[0]) : process.cwd();
22461
24455
  let artifact;
22462
24456
  try {
22463
24457
  artifact = await loadLastArtifact(startDir);
@@ -22554,7 +24548,7 @@ async function main() {
22554
24548
  const wantsJsonl = hasFlag(flags, "jsonl");
22555
24549
  const wantsStrict = hasFlag(flags, "strict");
22556
24550
  if (flags.condense) {
22557
- const cwd = positional[0] ? (0, import_node_path36.resolve)(process.cwd(), positional[0]) : process.cwd();
24551
+ const cwd = positional[0] ? (0, import_node_path38.resolve)(process.cwd(), positional[0]) : process.cwd();
22558
24552
  const result = await runCondenseCommand({ cwd });
22559
24553
  console.log(`VibeRaven context map refreshed: ${result.contextMapPath}`);
22560
24554
  return 0;
@@ -22574,7 +24568,7 @@ async function main() {
22574
24568
  }
22575
24569
  if (!command && flags.verify === true && typeof flags.action === "string") {
22576
24570
  return runVerifyActionCommand({
22577
- cwd: positional[0] ? (0, import_node_path36.resolve)(process.cwd(), positional[0]) : process.cwd(),
24571
+ cwd: positional[0] ? (0, import_node_path38.resolve)(process.cwd(), positional[0]) : process.cwd(),
22578
24572
  actionId: flags.action
22579
24573
  });
22580
24574
  }
@@ -22585,7 +24579,7 @@ async function main() {
22585
24579
  console.error("VibeRaven could not produce machine output because scan artifacts were not written.");
22586
24580
  return 3;
22587
24581
  }
22588
- const gateResult = scanResult.artifacts && (wantsJson || wantsJsonl || wantsStrict) ? JSON.parse(await (0, import_promises27.readFile)(scanResult.artifacts.gateResultPath, "utf8")) : void 0;
24582
+ const gateResult = scanResult.artifacts && (wantsJson || wantsJsonl || wantsStrict) ? JSON.parse(await (0, import_promises29.readFile)(scanResult.artifacts.gateResultPath, "utf8")) : void 0;
22589
24583
  const strictExitCode = wantsStrict && gateResult ? exitCodeForStrictGate(gateResult, { failOnWarnings: flags.strict === "warning" }) : scanResult.exitCode;
22590
24584
  if (wantsJson && gateResult) {
22591
24585
  process.stdout.write(renderGateResultJson(gateResult));
@@ -22607,7 +24601,7 @@ async function main() {
22607
24601
  env: process.env
22608
24602
  });
22609
24603
  if (mode === "local-ui") {
22610
- const cwd = positional[0] ? (0, import_node_path36.resolve)(process.cwd(), positional[0]) : process.cwd();
24604
+ const cwd = positional[0] ? (0, import_node_path38.resolve)(process.cwd(), positional[0]) : process.cwd();
22611
24605
  const handle = await startLocalUiServer({ cwd });
22612
24606
  console.log(`VibeRaven local UI: ${handle.url}`);
22613
24607
  await waitForServerShutdown();
@@ -22627,7 +24621,7 @@ async function main() {
22627
24621
  await runInteractiveSession();
22628
24622
  return 0;
22629
24623
  case "ui": {
22630
- const cwd = positional[0] ? (0, import_node_path36.resolve)(process.cwd(), positional[0]) : process.cwd();
24624
+ const cwd = positional[0] ? (0, import_node_path38.resolve)(process.cwd(), positional[0]) : process.cwd();
22631
24625
  const envPort = Number(process.env.PORT);
22632
24626
  const handle = await startLocalUiServer({ cwd, port: Number.isFinite(envPort) && envPort > 0 ? envPort : void 0 });
22633
24627
  console.log(`VibeRaven local UI: ${handle.url}`);
@@ -22639,12 +24633,12 @@ async function main() {
22639
24633
  return cmdStatus(flags, positional);
22640
24634
  case "check":
22641
24635
  return runCheckCommand({
22642
- cwd: positional[0] ? (0, import_node_path36.resolve)(process.cwd(), positional[0]) : process.cwd(),
24636
+ cwd: positional[0] ? (0, import_node_path38.resolve)(process.cwd(), positional[0]) : process.cwd(),
22643
24637
  json: Boolean(flags.json),
22644
24638
  quiet: Boolean(flags.quiet)
22645
24639
  });
22646
24640
  case "fix": {
22647
- const fixCwd = positional[0] ? (0, import_node_path36.resolve)(process.cwd(), positional[0]) : process.cwd();
24641
+ const fixCwd = positional[0] ? (0, import_node_path38.resolve)(process.cwd(), positional[0]) : process.cwd();
22648
24642
  const gapId = typeof flags.gap === "string" ? flags.gap : void 0;
22649
24643
  if (!gapId) {
22650
24644
  const artifact = await loadLastArtifact(fixCwd).catch(() => void 0);
@@ -22671,19 +24665,19 @@ async function main() {
22671
24665
  }
22672
24666
  case "actions":
22673
24667
  return runActionsCommand({
22674
- cwd: positional[0] ? (0, import_node_path36.resolve)(process.cwd(), positional[0]) : process.cwd(),
24668
+ cwd: positional[0] ? (0, import_node_path38.resolve)(process.cwd(), positional[0]) : process.cwd(),
22675
24669
  json: Boolean(flags.json)
22676
24670
  });
22677
24671
  case "preview":
22678
24672
  return runPreviewCommand({
22679
- cwd: positional[0] ? (0, import_node_path36.resolve)(process.cwd(), positional[0]) : process.cwd(),
24673
+ cwd: positional[0] ? (0, import_node_path38.resolve)(process.cwd(), positional[0]) : process.cwd(),
22680
24674
  agentMode: flags["agent-mode"] === true,
22681
24675
  json: Boolean(flags.json)
22682
24676
  });
22683
24677
  case "next":
22684
24678
  return runNextCommand({
22685
24679
  json: Boolean(flags.json),
22686
- cwd: positional[0] ? (0, import_node_path36.resolve)(process.cwd(), positional[0]) : process.cwd()
24680
+ cwd: positional[0] ? (0, import_node_path38.resolve)(process.cwd(), positional[0]) : process.cwd()
22687
24681
  });
22688
24682
  case "guide": {
22689
24683
  const provider2 = positional[0];
@@ -22717,7 +24711,7 @@ async function main() {
22717
24711
  case "provider-verify":
22718
24712
  return cmdProviderVerify(flags, positional);
22719
24713
  case "init": {
22720
- const cwd = positional[0] ? (0, import_node_path36.resolve)(process.cwd(), positional[0]) : process.cwd();
24714
+ const cwd = positional[0] ? (0, import_node_path38.resolve)(process.cwd(), positional[0]) : process.cwd();
22721
24715
  const agents = typeof flags.agents === "string" ? flags.agents : void 0;
22722
24716
  return runInitCommand({
22723
24717
  cwd,
@@ -22731,7 +24725,7 @@ async function main() {
22731
24725
  return 1;
22732
24726
  }
22733
24727
  return runDoctorAgentsCommand({
22734
- cwd: positional[0] ? (0, import_node_path36.resolve)(process.cwd(), positional[0]) : process.cwd()
24728
+ cwd: positional[0] ? (0, import_node_path38.resolve)(process.cwd(), positional[0]) : process.cwd()
22735
24729
  });
22736
24730
  case "validate-npm-package":
22737
24731
  return runValidateNpmPackageCommand({
@@ -22744,7 +24738,27 @@ async function main() {
22744
24738
  return 1;
22745
24739
  }
22746
24740
  return runAuditCommand({
22747
- cwd: positional[0] ? (0, import_node_path36.resolve)(process.cwd(), positional[0]) : process.cwd(),
24741
+ cwd: positional[0] ? (0, import_node_path38.resolve)(process.cwd(), positional[0]) : process.cwd(),
24742
+ json: Boolean(flags.json)
24743
+ });
24744
+ case "clean": {
24745
+ if (flags.plan !== true) {
24746
+ console.error("Usage: viberaven clean --plan [path]");
24747
+ return 1;
24748
+ }
24749
+ const cwd = positional[0] ? (0, import_node_path38.resolve)(process.cwd(), positional[0]) : process.cwd();
24750
+ const plan = buildContextCleanupPlan({ projectRoot: cwd, files: await collectCleanupFiles(cwd) });
24751
+ const outputPath = await writeCleanupPlan(cwd, plan);
24752
+ console.log(`Wrote ${outputPath}
24753
+ `);
24754
+ console.log(renderCleanupPlan(plan).trimEnd());
24755
+ return 0;
24756
+ }
24757
+ case "rls-test":
24758
+ return runRlsTestCommand({
24759
+ cwd: positional[0] ? (0, import_node_path38.resolve)(process.cwd(), positional[0]) : process.cwd(),
24760
+ matrix: typeof flags.matrix === "string" ? (0, import_node_path38.resolve)(process.cwd(), flags.matrix) : void 0,
24761
+ migrations: typeof flags.migrations === "string" ? (0, import_node_path38.resolve)(process.cwd(), flags.migrations) : void 0,
22748
24762
  json: Boolean(flags.json)
22749
24763
  });
22750
24764
  default:
@@ -22753,11 +24767,11 @@ async function main() {
22753
24767
  return 1;
22754
24768
  }
22755
24769
  }
22756
- var import_promises27, import_node_path36, STACK_AREAS;
24770
+ var import_promises29, import_node_path38, STACK_AREAS;
22757
24771
  var init_cli = __esm({
22758
24772
  "src/cli.ts"() {
22759
- import_promises27 = require("node:fs/promises");
22760
- import_node_path36 = require("node:path");
24773
+ import_promises29 = require("node:fs/promises");
24774
+ import_node_path38 = require("node:path");
22761
24775
  init_config();
22762
24776
  init_runScan();
22763
24777
  init_artifacts2();
@@ -22784,10 +24798,12 @@ var init_cli = __esm({
22784
24798
  init_runDoctor();
22785
24799
  init_runValidateNpmPackage();
22786
24800
  init_runAudit();
24801
+ init_cleanPlan();
22787
24802
  init_actions();
22788
24803
  init_verifyAction();
22789
24804
  init_preview();
22790
24805
  init_check();
24806
+ init_rlsTest();
22791
24807
  init_server();
22792
24808
  init_version();
22793
24809
  init_loopState();