seaworthycode 1.2.12 → 1.2.13

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/index.js CHANGED
@@ -407,10 +407,11 @@ async function crawl(options) {
407
407
  const { isBinary, isGenerated } = await peekFileHead(fullPath);
408
408
  if (isBinary || isGenerated) continue;
409
409
  if (await shouldSkipMinifiedJs(fullPath, entry.name)) continue;
410
+ let cachedContent;
410
411
  files.push({
411
412
  path: fullPath,
412
413
  relativePath: relPath,
413
- content: () => fs.readFile(fullPath, "utf-8"),
414
+ content: () => cachedContent ??= fs.readFile(fullPath, "utf-8"),
414
415
  language: inferLanguage(entry.name)
415
416
  });
416
417
  } catch {
@@ -427,10 +428,11 @@ async function crawl(options) {
427
428
  const { isBinary, isGenerated } = await peekFileHead(fullPath);
428
429
  if (isBinary || isGenerated) continue;
429
430
  if (await shouldSkipMinifiedJs(fullPath, entry.name)) continue;
431
+ let cachedContent;
430
432
  files.push({
431
433
  path: fullPath,
432
434
  relativePath: relPath,
433
- content: () => fs.readFile(fullPath, "utf-8"),
435
+ content: () => cachedContent ??= fs.readFile(fullPath, "utf-8"),
434
436
  language: inferLanguage(entry.name)
435
437
  });
436
438
  } catch {
@@ -789,6 +791,7 @@ var copy = {
789
791
  "report.cta.hasFindings": `Fix these before you ship. ${SITE_URL}/docs`,
790
792
  "report.cta.critical": `\u26A0 Critical issues found \u2014 patch before you deploy. ${SITE_URL}/docs`,
791
793
  "report.proUpsellNamed": (names, count) => `\u2693 Pro unlocks ${count} more check${count === 1 ? "" : "s"} (${names}). ${SITE_URL}`,
794
+ "report.timedOutChecks": (names, count) => `\u26A0 ${count} check${count === 1 ? "" : "s"} timed out and were skipped (${names}). Use SEAWORTHY_CHECK_TIMEOUT_MS to extend the limit.`,
792
795
  "report.preScanNotice": (count) => `\u2693 Pro unlocks ${count} additional checks. Upgrade at ${SITE_URL}`,
793
796
  "report.llmPrivacyWarning": "Pro checks will send code context to your LLM provider via your API key. No data is sent to Seaworthy servers.",
794
797
  "report.confidenceHigh": "high confidence",
@@ -1601,6 +1604,16 @@ async function runWithConcurrencyCollect(items, fn, concurrency) {
1601
1604
  await Promise.all(workers);
1602
1605
  return results;
1603
1606
  }
1607
+ var DEFAULT_CHECK_TIMEOUT_MS = 5 * 60 * 1e3;
1608
+ var SLOW_CHECK_THRESHOLD_RATIO = 0.5;
1609
+ function getCheckTimeoutMs() {
1610
+ const env2 = process.env.SEAWORTHY_CHECK_TIMEOUT_MS;
1611
+ if (env2 !== void 0) {
1612
+ const n = parseInt(env2, 10);
1613
+ if (!isNaN(n) && n > 0) return n;
1614
+ }
1615
+ return DEFAULT_CHECK_TIMEOUT_MS;
1616
+ }
1604
1617
  var ScanRunner = class {
1605
1618
  async run(options) {
1606
1619
  const { concurrency = 4 } = options;
@@ -1628,15 +1641,54 @@ var ScanRunner = class {
1628
1641
  }
1629
1642
  const categoryCompleted = /* @__PURE__ */ new Map();
1630
1643
  let completedCount = 0;
1644
+ const timedOutChecks = [];
1645
+ const checkTimeoutMs = getCheckTimeoutMs();
1631
1646
  await runWithConcurrency(
1632
1647
  checks,
1633
1648
  async (check2) => {
1649
+ if (options.debug) {
1650
+ process.stderr.write(`[seaworthy] check.start ${check2.id}
1651
+ `);
1652
+ }
1653
+ const startMs = Date.now();
1654
+ let timedOut = false;
1655
+ let softTimer;
1656
+ let hardTimer;
1634
1657
  try {
1635
- const findings2 = await check2.run(ctx);
1636
- checkResults.set(check2.id, findings2);
1658
+ const findings2 = await new Promise((resolve22, reject) => {
1659
+ softTimer = setTimeout(() => {
1660
+ process.stderr.write(
1661
+ `[seaworthy] check.slow ${check2.id} \u2014 still running after ${Math.round(checkTimeoutMs * SLOW_CHECK_THRESHOLD_RATIO / 1e3)}s
1662
+ `
1663
+ );
1664
+ }, checkTimeoutMs * SLOW_CHECK_THRESHOLD_RATIO);
1665
+ hardTimer = setTimeout(() => {
1666
+ timedOut = true;
1667
+ process.stderr.write(
1668
+ `[seaworthy] check.timeout ${check2.id} \u2014 aborting after ${Math.round(checkTimeoutMs / 1e3)}s
1669
+ `
1670
+ );
1671
+ resolve22([]);
1672
+ }, checkTimeoutMs);
1673
+ check2.run(ctx).then(resolve22, reject);
1674
+ });
1675
+ clearTimeout(softTimer);
1676
+ clearTimeout(hardTimer);
1677
+ if (timedOut) {
1678
+ timedOutChecks.push({ id: check2.id, name: check2.name, category: check2.category });
1679
+ checkResults.set(check2.id, []);
1680
+ } else {
1681
+ checkResults.set(check2.id, findings2);
1682
+ }
1637
1683
  } catch (err) {
1684
+ clearTimeout(softTimer);
1685
+ clearTimeout(hardTimer);
1638
1686
  console.error(`Check "${check2.id}" failed: ${String(err)}`);
1639
1687
  }
1688
+ if (options.debug) {
1689
+ process.stderr.write(`[seaworthy] check.done ${check2.id} ${Date.now() - startMs}ms
1690
+ `);
1691
+ }
1640
1692
  options.onCheckComplete?.(++completedCount, checks.length, check2.name);
1641
1693
  if (options.onCategoryComplete) {
1642
1694
  const catDone = (categoryCompleted.get(check2.category) ?? 0) + 1;
@@ -1685,7 +1737,8 @@ var ScanRunner = class {
1685
1737
  return {
1686
1738
  findings,
1687
1739
  scannedFileCount: files.length,
1688
- skippedChecks
1740
+ skippedChecks,
1741
+ timedOutChecks
1689
1742
  };
1690
1743
  }
1691
1744
  };
@@ -2486,7 +2539,7 @@ function renderCollapsedGroup(group, showIds) {
2486
2539
  }
2487
2540
  var TerminalReporter = class {
2488
2541
  generate(input) {
2489
- const { findings: rawFindings, targetDir, tier, licensedTo, scannedFileCount, skippedChecks = [], showIds = false, all = false } = input;
2542
+ const { findings: rawFindings, targetDir, tier, licensedTo, scannedFileCount, skippedChecks = [], timedOutChecks = [], showIds = false, all = false } = input;
2490
2543
  const findings = dedupeDegradedFindings(rawFindings);
2491
2544
  if (findings.length === 0) {
2492
2545
  const lines2 = [];
@@ -2500,6 +2553,10 @@ var TerminalReporter = class {
2500
2553
  lines2.push("");
2501
2554
  lines2.push(text.link(buildProUpsell(skippedChecks)));
2502
2555
  }
2556
+ if (timedOutChecks.length > 0) {
2557
+ lines2.push("");
2558
+ lines2.push(text.warning(buildTimedOutNote(timedOutChecks)));
2559
+ }
2503
2560
  return lines2.join("\n");
2504
2561
  }
2505
2562
  const lines = [];
@@ -2549,9 +2606,18 @@ var TerminalReporter = class {
2549
2606
  if (skippedChecks.length > 0) {
2550
2607
  lines.push(text.link(buildProUpsell(skippedChecks)));
2551
2608
  }
2609
+ if (timedOutChecks.length > 0) {
2610
+ lines.push(text.warning(buildTimedOutNote(timedOutChecks)));
2611
+ }
2552
2612
  return lines.join("\n");
2553
2613
  }
2554
2614
  };
2615
+ function buildTimedOutNote(timedOutChecks) {
2616
+ const count = timedOutChecks.length;
2617
+ const names = timedOutChecks.slice(0, 2).map((c) => c.name.toLowerCase()).join(", ");
2618
+ const suffix = count > 2 ? ", \u2026" : "";
2619
+ return getCopy("report.timedOutChecks", names + suffix, count);
2620
+ }
2555
2621
  function buildProUpsell(skippedChecks) {
2556
2622
  const count = skippedChecks.length;
2557
2623
  const names = skippedChecks.slice(0, 2).map((c) => c.name.toLowerCase()).join(", ");
@@ -5908,6 +5974,14 @@ function classifyError(err) {
5908
5974
  return "llm.unexpected";
5909
5975
  }
5910
5976
  var LLM_TIMEOUT_MS = 9e4;
5977
+ function getMaxFilesPerLlmCheck() {
5978
+ const env2 = process.env.SEAWORTHY_LLM_MAX_FILES;
5979
+ if (env2 !== void 0) {
5980
+ const n = parseInt(env2, 10);
5981
+ if (!isNaN(n) && n > 0) return n;
5982
+ }
5983
+ return 150;
5984
+ }
5911
5985
  function withLLMTimeout(promise, ms) {
5912
5986
  return new Promise((resolve22, reject) => {
5913
5987
  const timerId = setTimeout(() => {
@@ -6149,7 +6223,7 @@ async function runLLMCheck(ctx, template) {
6149
6223
  return cached;
6150
6224
  }
6151
6225
  }
6152
- const eligibleFiles = await filterFilesByBudget(proEligibleFiles, template.tokenBudget);
6226
+ let eligibleFiles = await filterFilesByBudget(proEligibleFiles, template.tokenBudget);
6153
6227
  if (eligibleFiles.length === 0) {
6154
6228
  let mechFindings = await runMechanicalCheck(ctx, template);
6155
6229
  if (template.projectScoped) {
@@ -6165,6 +6239,13 @@ async function runLLMCheck(ctx, template) {
6165
6239
  }
6166
6240
  return withRemediation;
6167
6241
  }
6242
+ const maxFiles = getMaxFilesPerLlmCheck();
6243
+ if (eligibleFiles.length > maxFiles) {
6244
+ console.error(
6245
+ `[seaworthy] ${template.checkId}: ${eligibleFiles.length} eligible files \u2014 capping LLM prompt to ${maxFiles}`
6246
+ );
6247
+ eligibleFiles = eligibleFiles.slice(0, maxFiles);
6248
+ }
6168
6249
  const prompt = `${PROMPT_PREAMBLE}
6169
6250
 
6170
6251
  ${await template.buildPrompt(eligibleFiles)}`;
@@ -16142,6 +16223,7 @@ async function executeScan(args, ctx) {
16142
16223
  licensedTo: ctx.licensedTo ?? "free tier",
16143
16224
  scannedFileCount: result.scannedFileCount,
16144
16225
  skippedChecks: result.skippedChecks,
16226
+ timedOutChecks: result.timedOutChecks,
16145
16227
  showIds: args.showIds ?? args.debug,
16146
16228
  all: args.all
16147
16229
  };