seaworthycode 1.1.1 → 1.1.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/index.js CHANGED
@@ -9,8 +9,10 @@ import { cac } from "cac";
9
9
  import { createRequire as createRequire2 } from "module";
10
10
 
11
11
  // src/cli-action.ts
12
- import { stat } from "fs/promises";
12
+ import { stat, access as access2 } from "fs/promises";
13
13
  import { resolve as resolve3 } from "path";
14
+ import { homedir as homedir3 } from "os";
15
+ import { join as join15 } from "path";
14
16
 
15
17
  // ../core/dist/index.js
16
18
  import { readFileSync } from "fs";
@@ -296,8 +298,8 @@ async function peekFileHead(filePath) {
296
298
  break;
297
299
  }
298
300
  }
299
- const text2 = buf.toString("utf-8", 0, bytesRead).toLowerCase();
300
- const isGenerated = GENERATED_MARKERS.some((marker) => text2.includes(marker));
301
+ const text22 = buf.toString("utf-8", 0, bytesRead).toLowerCase();
302
+ const isGenerated = GENERATED_MARKERS.some((marker) => text22.includes(marker));
301
303
  return { isBinary, isGenerated };
302
304
  } finally {
303
305
  await handle.close();
@@ -769,9 +771,13 @@ var copy = {
769
771
  "report.scannedFiles": (count) => `Scanned ${count} files.`,
770
772
  "report.summary": "Summary",
771
773
  "report.bySeverity": "By severity",
772
- "report.tierWatermark": (tier, licensedTo) => `Tier: ${tier} \u2014 Licensed to ${licensedTo}`,
773
- "report.preScanNotice": (count) => `${count} additional check(s) available on Pro. See ${SITE_URL} to upgrade.`,
774
- "report.postScanCta": (url) => `Learn more: ${url}`,
774
+ "report.tierWatermark": (licensedTo) => `\u2693 Pro \xB7 ${licensedTo}`,
775
+ "report.scanProgress.starting": (targetDir) => `\u2693 Scanning ${targetDir}...`,
776
+ "report.scanProgress.category": (label, count) => count > 0 ? ` \u2693 ${label} \u2014 ${count} issue(s)` : ` \u2693 ${label} \u2713`,
777
+ "report.cta.clean": `\u2713 All clear \u2014 your project is shipshape!`,
778
+ "report.cta.hasFindings": `Fix these before you ship. ${SITE_URL}/docs`,
779
+ "report.cta.critical": `\u26A0 Critical issues found \u2014 patch before you deploy. ${SITE_URL}/docs`,
780
+ "report.proUpsellNamed": (names, count) => `\u2693 Pro unlocks ${count} more check${count === 1 ? "" : "s"} (${names}). ${SITE_URL}`,
775
781
  "report.llmPrivacyWarning": "Pro checks will send code context to your LLM provider via your API key. No data is sent to Seaworthy servers.",
776
782
  "report.confidenceHigh": "high confidence",
777
783
  "report.confidenceMedium": "medium confidence",
@@ -1540,9 +1546,6 @@ var ScanRunner = class {
1540
1546
  const { files, meta } = await crawl({ targetDir: options.targetDir, maxFileSize: options.maxFileSize, debug: options.debug });
1541
1547
  const checks = registry.getForTier(options.tier);
1542
1548
  const skippedChecks = registry.getSkippedChecks(options.tier);
1543
- if (skippedChecks.length > 0) {
1544
- console.error(getCopy("report.preScanNotice", skippedChecks.length));
1545
- }
1546
1549
  if (options.llm) {
1547
1550
  console.error(getCopy("report.llmPrivacyWarning"));
1548
1551
  }
@@ -1557,14 +1560,30 @@ var ScanRunner = class {
1557
1560
  };
1558
1561
  const results = [];
1559
1562
  const mutex = [];
1563
+ const inFlight = /* @__PURE__ */ new Map();
1564
+ const findingsPerCategory = /* @__PURE__ */ new Map();
1565
+ for (const check2 of checks) {
1566
+ inFlight.set(check2.category, (inFlight.get(check2.category) ?? 0) + 1);
1567
+ }
1560
1568
  await runWithConcurrency(
1561
1569
  checks,
1562
1570
  async (check2) => {
1563
1571
  try {
1564
1572
  const findings2 = await check2.run(ctx);
1565
1573
  mutex.push(...findings2);
1574
+ if (options.onCategoryComplete) {
1575
+ findingsPerCategory.set(check2.category, (findingsPerCategory.get(check2.category) ?? 0) + findings2.length);
1576
+ }
1566
1577
  } catch (err) {
1567
1578
  console.error(`Check "${check2.id}" failed: ${String(err)}`);
1579
+ } finally {
1580
+ if (options.onCategoryComplete) {
1581
+ const remaining = (inFlight.get(check2.category) ?? 1) - 1;
1582
+ inFlight.set(check2.category, remaining);
1583
+ if (remaining === 0) {
1584
+ options.onCategoryComplete(check2.category, findingsPerCategory.get(check2.category) ?? 0);
1585
+ }
1586
+ }
1568
1587
  }
1569
1588
  },
1570
1589
  concurrency
@@ -1601,7 +1620,7 @@ var ScanRunner = class {
1601
1620
  return {
1602
1621
  findings,
1603
1622
  scannedFileCount: files.length,
1604
- skippedCheckCount: skippedChecks.length
1623
+ skippedChecks
1605
1624
  };
1606
1625
  }
1607
1626
  };
@@ -2351,15 +2370,31 @@ var text = {
2351
2370
  success: (s) => source_default.hex(tokens.success)(s)
2352
2371
  };
2353
2372
  var SEVERITY_ORDER2 = ["critical", "high", "medium", "low", "info"];
2373
+ function checkNameOf(checkId) {
2374
+ const short = checkId.replace(/^[^.]+\./, "");
2375
+ try {
2376
+ return getCopy(`checks.${short}.name`);
2377
+ } catch {
2378
+ return short;
2379
+ }
2380
+ }
2354
2381
  var TerminalReporter = class {
2355
2382
  generate(input) {
2356
- const { findings: rawFindings, targetDir, tier, licensedTo, scannedFileCount } = input;
2383
+ const { findings: rawFindings, targetDir, tier, licensedTo, scannedFileCount, skippedChecks = [], showIds = false } = input;
2357
2384
  const findings = dedupeDegradedFindings(rawFindings);
2358
2385
  if (findings.length === 0) {
2359
- return `${text.success(getCopy("report.noIssues"))}
2360
- ${text.meta(getCopy("report.scannedFiles", scannedFileCount))}
2361
-
2362
- ${text.link(getCopy("report.postScanCta", SITE_URL))}`;
2386
+ const lines2 = [];
2387
+ lines2.push(text.success(getCopy("report.cta.clean")));
2388
+ lines2.push(text.meta(getCopy("report.scannedFiles", scannedFileCount)));
2389
+ if (tier === "pro") {
2390
+ lines2.push("");
2391
+ lines2.push(text.meta(getCopy("report.tierWatermark", licensedTo)));
2392
+ }
2393
+ if (skippedChecks.length > 0) {
2394
+ lines2.push("");
2395
+ lines2.push(text.link(buildProUpsell(skippedChecks)));
2396
+ }
2397
+ return lines2.join("\n");
2363
2398
  }
2364
2399
  const lines = [];
2365
2400
  lines.push(text.heading(getCopy("report.header", targetDir, findings.length)));
@@ -2372,7 +2407,9 @@ ${text.link(getCopy("report.postScanCta", SITE_URL))}`;
2372
2407
  const rawAnalysisType = f.properties?.analysisType;
2373
2408
  const analysisTypeValue = rawAnalysisType === "pattern" || rawAnalysisType === "semantic" ? rawAnalysisType : void 0;
2374
2409
  const analysisType = analysisTypeValue ? text.meta(` [${getCopy(`reporting.analysisType.${analysisTypeValue}`)}]`) : "";
2375
- lines.push(`${severityLabel}${baselineLabel} ${f.checkId}${analysisType} \u2014 ${f.message}${loc}${confidenceLabel}`);
2410
+ const name = checkNameOf(f.checkId);
2411
+ const idSuffix = showIds ? text.meta(` \xB7 ${f.checkId}`) : "";
2412
+ lines.push(`${severityLabel}${baselineLabel} ${name}${idSuffix}${analysisType} \u2014 ${f.message}${loc}${confidenceLabel}`);
2376
2413
  if (f.remediation) {
2377
2414
  lines.push(` ${text.meta(`\u2192 ${f.remediation}`)}`);
2378
2415
  }
@@ -2390,11 +2427,23 @@ ${text.link(getCopy("report.postScanCta", SITE_URL))}`;
2390
2427
  }
2391
2428
  }
2392
2429
  lines.push("");
2393
- lines.push(text.meta(getCopy("report.tierWatermark", tier, licensedTo)));
2394
- lines.push(text.link(getCopy("report.postScanCta", SITE_URL)));
2430
+ const hasCriticalOrHigh = findings.some((f) => f.severity === "critical" || f.severity === "high");
2431
+ lines.push(text.link(hasCriticalOrHigh ? getCopy("report.cta.critical") : getCopy("report.cta.hasFindings")));
2432
+ if (tier === "pro") {
2433
+ lines.push(text.meta(getCopy("report.tierWatermark", licensedTo)));
2434
+ }
2435
+ if (skippedChecks.length > 0) {
2436
+ lines.push(text.link(buildProUpsell(skippedChecks)));
2437
+ }
2395
2438
  return lines.join("\n");
2396
2439
  }
2397
2440
  };
2441
+ function buildProUpsell(skippedChecks) {
2442
+ const count = skippedChecks.length;
2443
+ const names = skippedChecks.slice(0, 2).map((c) => c.name.toLowerCase()).join(", ");
2444
+ const suffix = count > 2 ? ", \u2026" : "";
2445
+ return getCopy("report.proUpsellNamed", names + suffix, count);
2446
+ }
2398
2447
  function textForSeverity(severity) {
2399
2448
  switch (severity) {
2400
2449
  case "critical":
@@ -5640,8 +5689,8 @@ function downgradeSeverity(severity) {
5640
5689
  if (idx < 0 || idx >= SEVERITY_ORDER3.length - 1) return severity;
5641
5690
  return SEVERITY_ORDER3[idx + 1];
5642
5691
  }
5643
- function estimateTokens(text2) {
5644
- return Math.ceil(text2.length / 4);
5692
+ function estimateTokens(text22) {
5693
+ return Math.ceil(text22.length / 4);
5645
5694
  }
5646
5695
  async function filterFilesByBudget(files, budget) {
5647
5696
  if (budget === void 0 || budget === Infinity) {
@@ -7087,8 +7136,8 @@ function unquoteString(value) {
7087
7136
  }
7088
7137
  return trimmed;
7089
7138
  }
7090
- function isDefaultCredential(password) {
7091
- const normalized = unquoteString(password).toLowerCase();
7139
+ function isDefaultCredential(password2) {
7140
+ const normalized = unquoteString(password2).toLowerCase();
7092
7141
  const defaults = /* @__PURE__ */ new Set([
7093
7142
  "admin",
7094
7143
  "password",
@@ -7190,8 +7239,8 @@ async function scanSqlDefaultCredentials(ctx) {
7190
7239
  let roleMatch = createRoleRegex.exec(content);
7191
7240
  while (roleMatch) {
7192
7241
  const roleName = roleMatch[1].replace(/["`[\]]/g, "");
7193
- const password = roleMatch[2];
7194
- if (!looksLikeHash(password) && isDefaultCredential(password)) {
7242
+ const password2 = roleMatch[2];
7243
+ if (!looksLikeHash(password2) && isDefaultCredential(password2)) {
7195
7244
  const beforeMatch = content.slice(0, roleMatch.index);
7196
7245
  const lineNum = beforeMatch.split("\n").length;
7197
7246
  findings.push({
@@ -7757,8 +7806,8 @@ registry.register(
7757
7806
  })
7758
7807
  );
7759
7808
  var CHECK_ID11 = "security.os-command-injection";
7760
- function isStaticShellLikeLiteral(text2) {
7761
- const t = text2.trim();
7809
+ function isStaticShellLikeLiteral(text22) {
7810
+ const t = text22.trim();
7762
7811
  if (t.length < 2) return false;
7763
7812
  if (t.startsWith('"') && t.endsWith('"') || t.startsWith("'") && t.endsWith("'")) return true;
7764
7813
  if (t.startsWith("`") && t.endsWith("`") && !/(?<!\\)\${/.test(t)) return true;
@@ -9176,10 +9225,10 @@ var CSS_COMMENT = /\/\*([\s\S]*?)\*\//g;
9176
9225
  var JSON_SCRIPT = /<script\b[^>]*type\s*=\s*["']application\/json["'][^>]*>([\s\S]*?)<\/script>/gi;
9177
9226
  function scanPattern(content, filePath, checkPrefix, pattern, messageText, findings) {
9178
9227
  for (const match of content.matchAll(pattern)) {
9179
- const text2 = match[1] ?? match[0] ?? "";
9180
- if (!SECRET_MARKER.test(text2)) continue;
9181
- const valueMatch = text2.match(SECRET_VALUE);
9182
- const value = valueMatch?.[1] ?? text2;
9228
+ const text22 = match[1] ?? match[0] ?? "";
9229
+ if (!SECRET_MARKER.test(text22)) continue;
9230
+ const valueMatch = text22.match(SECRET_VALUE);
9231
+ const value = valueMatch?.[1] ?? text22;
9183
9232
  const { line, column } = lineAndColumn5(content, match.index ?? 0);
9184
9233
  findings.push({
9185
9234
  id: `${CHECK_ID23}:${filePath}:${line}:${column}:${checkPrefix}`,
@@ -9830,11 +9879,11 @@ function pushFinding6(findings, filePath, content, index, matchText) {
9830
9879
  }
9831
9880
  function scanCss(findings, filePath, scanContent, originalContent, offset = 0) {
9832
9881
  for (const match of scanContent.matchAll(BRACE_RULE_PATTERN)) {
9833
- const text2 = match[0] ?? "";
9834
- if (!ATTR_SELECTOR_PATTERN.test(text2)) continue;
9835
- if (!EXTERNAL_HTTP_URL_PATTERN.test(text2)) continue;
9836
- const leadingWhitespace = text2.match(/^\s*/)?.[0] ?? "";
9837
- pushFinding6(findings, filePath, originalContent, (match.index ?? 0) + offset + leadingWhitespace.length, text2);
9882
+ const text22 = match[0] ?? "";
9883
+ if (!ATTR_SELECTOR_PATTERN.test(text22)) continue;
9884
+ if (!EXTERNAL_HTTP_URL_PATTERN.test(text22)) continue;
9885
+ const leadingWhitespace = text22.match(/^\s*/)?.[0] ?? "";
9886
+ pushFinding6(findings, filePath, originalContent, (match.index ?? 0) + offset + leadingWhitespace.length, text22);
9838
9887
  }
9839
9888
  }
9840
9889
  registry.register({
@@ -10491,11 +10540,11 @@ function applyTaintToBashReadTarget(command, scopeRoot, s, cleanseFromId) {
10491
10540
  for (let i = 0; i < command.namedChildCount; i++) {
10492
10541
  const child = command.namedChild(i);
10493
10542
  if (child.type !== "word") continue;
10494
- const text2 = child.text;
10495
- if (text2 === "read" || text2.startsWith("-")) continue;
10543
+ const text22 = child.text;
10544
+ if (text22 === "read" || text22.startsWith("-")) continue;
10496
10545
  markTaint(s, child.id);
10497
10546
  if (cleanseFromId !== void 0) mergeCleanse(s, cleanseFromId, child.id);
10498
- taintVarUses(text2, scopeRoot, s);
10547
+ taintVarUses(text22, scopeRoot, s);
10499
10548
  }
10500
10549
  }
10501
10550
  function collectEntries(table) {
@@ -15092,7 +15141,23 @@ var copy3 = {
15092
15141
  "cli.help.usage": "Usage: seaworthy [path] [options]",
15093
15142
  "cli.help.options": "Options",
15094
15143
  "cli.help.learnMore": `Documentation: ${SITE_URL}/docs`,
15095
- "cli.scanning": "Scanning...",
15144
+ "cli.setup.intro": "Welcome to Seaworthy! Let's get you set up.",
15145
+ "cli.setup.licensePrompt": "License key (press Enter to skip and scan free)",
15146
+ "cli.setup.licenseHint": "Buy a key at seaworthycode.com \xB7 Enter skips to free tier",
15147
+ "cli.setup.providerPrompt": "LLM provider for Pro checks",
15148
+ "cli.setup.providerSkip": "Skip \u2014 no LLM checks",
15149
+ "cli.setup.apiKeyPrompt": "API key for",
15150
+ "cli.setup.modelPrompt": "Model (press Enter for default)",
15151
+ "cli.setup.baseUrlPrompt": "Custom API base URL (press Enter to use default)",
15152
+ "cli.setup.done": "Config saved. Run `seaworthy .` to scan your project.",
15153
+ "cli.setup.skipped": "Setup skipped. Running free-tier scan.",
15154
+ "cli.firstRun": "First time? Let's get you set up.",
15155
+ "cli.category.security": "Security",
15156
+ "cli.category.resilience": "Resilience",
15157
+ "cli.category.ops": "Ops Basics",
15158
+ "cli.category.dependencies": "Dependencies",
15159
+ "cli.category.configuration": "Configuration",
15160
+ "cli.category.data-exposure": "Data Exposure",
15096
15161
  "cli.reportWritten": (path) => `Report written to ${path}`,
15097
15162
  "cli.error.notADirectory": (path) => `Path is not a directory: ${path}`,
15098
15163
  "cli.error.notFound": (path) => `Path not found: ${path}`,
@@ -15154,6 +15219,14 @@ function resolveScanTimeoutMs(tier) {
15154
15219
  }
15155
15220
  return tier === "free" ? FREE_SCAN_TIMEOUT_MS : PRO_SCAN_TIMEOUT_MS;
15156
15221
  }
15222
+ var CATEGORY_LABELS = {
15223
+ security: getCopy2("cli.category.security"),
15224
+ resilience: getCopy2("cli.category.resilience"),
15225
+ ops: getCopy2("cli.category.ops"),
15226
+ dependencies: getCopy2("cli.category.dependencies"),
15227
+ configuration: getCopy2("cli.category.configuration"),
15228
+ "data-exposure": getCopy2("cli.category.data-exposure")
15229
+ };
15157
15230
  function withTimeout(promise, ms, message) {
15158
15231
  return new Promise((resolve5, reject) => {
15159
15232
  const timer = setTimeout(() => reject(new Error(message)), ms);
@@ -15186,6 +15259,10 @@ async function executeScan(args, ctx) {
15186
15259
  if (args.llmApiKey && args.llmProvider && !process.env.SEAWORTHY_SKIP_LLM_CHECK) {
15187
15260
  llm = createProvider({ provider: args.llmProvider, apiKey: args.llmApiKey, model: args.llmModel, baseUrl: args.llmApiUrl });
15188
15261
  }
15262
+ const showProgress = args.format === "terminal" && !args.sarifStdout;
15263
+ if (showProgress) {
15264
+ process.stderr.write(getCopy("report.scanProgress.starting", args.targetDir) + "\n");
15265
+ }
15189
15266
  const runner = new ScanRunner();
15190
15267
  const scanTimeoutMs = resolveScanTimeoutMs(ctx.tier);
15191
15268
  const result = await withTimeout(
@@ -15196,7 +15273,11 @@ async function executeScan(args, ctx) {
15196
15273
  maxFileSize: args.maxFileSize,
15197
15274
  debug: args.debug,
15198
15275
  baseline: args.baseline,
15199
- minConfidence: args.minConfidence
15276
+ minConfidence: args.minConfidence,
15277
+ onCategoryComplete: showProgress ? (category, count) => {
15278
+ const label = CATEGORY_LABELS[category] ?? category;
15279
+ process.stderr.write(getCopy("report.scanProgress.category", label, count) + "\n");
15280
+ } : void 0
15200
15281
  }),
15201
15282
  scanTimeoutMs,
15202
15283
  getCopy("errors.scan.timed_out", Math.round(scanTimeoutMs / 1e3))
@@ -15206,7 +15287,9 @@ async function executeScan(args, ctx) {
15206
15287
  targetDir: args.targetDir,
15207
15288
  tier: ctx.tier,
15208
15289
  licensedTo: ctx.licensedTo ?? "free tier",
15209
- scannedFileCount: result.scannedFileCount
15290
+ scannedFileCount: result.scannedFileCount,
15291
+ skippedChecks: result.skippedChecks,
15292
+ showIds: args.showIds ?? args.debug
15210
15293
  };
15211
15294
  let primaryOutput;
15212
15295
  switch (args.format) {
@@ -15257,6 +15340,9 @@ var scanCommand = createCommand({
15257
15340
  handler: executeScan
15258
15341
  });
15259
15342
 
15343
+ // src/commands/setup.ts
15344
+ import { intro, outro, text as text2, password, select, isCancel } from "@clack/prompts";
15345
+
15260
15346
  // src/config/store.ts
15261
15347
  import { promises as fs9 } from "fs";
15262
15348
  import { homedir as homedir2 } from "os";
@@ -15283,6 +15369,91 @@ async function writeConfig(config) {
15283
15369
  await fs9.chmod(getConfigFile(), 384);
15284
15370
  }
15285
15371
 
15372
+ // src/commands/setup.ts
15373
+ var PROVIDERS_WITH_CUSTOM_URL = /* @__PURE__ */ new Set(["ollama", "openrouter"]);
15374
+ async function executeSetup() {
15375
+ intro(getCopy2("cli.setup.intro"));
15376
+ const config = await readConfig();
15377
+ const licenseKeyResult = await text2({
15378
+ message: getCopy2("cli.setup.licensePrompt"),
15379
+ placeholder: "SW-XXXXXX",
15380
+ defaultValue: config.licenseKey ?? "",
15381
+ validate: () => void 0
15382
+ });
15383
+ if (isCancel(licenseKeyResult)) {
15384
+ outro(getCopy2("cli.setup.skipped"));
15385
+ return { ok: true };
15386
+ }
15387
+ const licenseKey = String(licenseKeyResult).trim() || void 0;
15388
+ const providers = listPresetProviders();
15389
+ const providerOptions = [
15390
+ { value: "", label: getCopy2("cli.setup.providerSkip") },
15391
+ ...providers.map((p) => ({ value: p, label: p }))
15392
+ ];
15393
+ const providerResult = await select({
15394
+ message: getCopy2("cli.setup.providerPrompt"),
15395
+ options: providerOptions,
15396
+ initialValue: config.llmProvider ?? ""
15397
+ });
15398
+ if (isCancel(providerResult)) {
15399
+ outro(getCopy2("cli.setup.skipped"));
15400
+ return { ok: true };
15401
+ }
15402
+ const llmProvider = String(providerResult) || void 0;
15403
+ let llmApiKey = config.llmApiKey;
15404
+ let llmModel = config.llmModel;
15405
+ let llmApiUrl = config.llmApiUrl;
15406
+ if (llmProvider) {
15407
+ const apiKeyResult = await password({
15408
+ message: `${getCopy2("cli.setup.apiKeyPrompt")} ${llmProvider}`,
15409
+ validate: () => void 0
15410
+ });
15411
+ if (isCancel(apiKeyResult)) {
15412
+ outro(getCopy2("cli.setup.skipped"));
15413
+ return { ok: true };
15414
+ }
15415
+ llmApiKey = String(apiKeyResult).trim() || void 0;
15416
+ const preset = resolvePreset(llmProvider);
15417
+ const modelResult = await text2({
15418
+ message: getCopy2("cli.setup.modelPrompt"),
15419
+ placeholder: preset?.defaultModel ?? "",
15420
+ defaultValue: config.llmModel ?? "",
15421
+ validate: () => void 0
15422
+ });
15423
+ if (isCancel(modelResult)) {
15424
+ outro(getCopy2("cli.setup.skipped"));
15425
+ return { ok: true };
15426
+ }
15427
+ llmModel = String(modelResult).trim() || void 0;
15428
+ if (PROVIDERS_WITH_CUSTOM_URL.has(llmProvider)) {
15429
+ const urlResult = await text2({
15430
+ message: getCopy2("cli.setup.baseUrlPrompt"),
15431
+ placeholder: preset?.baseUrl ?? "",
15432
+ defaultValue: config.llmApiUrl ?? "",
15433
+ validate: () => void 0
15434
+ });
15435
+ if (isCancel(urlResult)) {
15436
+ outro(getCopy2("cli.setup.skipped"));
15437
+ return { ok: true };
15438
+ }
15439
+ llmApiUrl = String(urlResult).trim() || void 0;
15440
+ } else {
15441
+ llmApiUrl = void 0;
15442
+ }
15443
+ }
15444
+ await writeConfig({ licenseKey, llmProvider, llmApiKey, llmModel, llmApiUrl });
15445
+ outro(getCopy2("cli.setup.done"));
15446
+ return { ok: true };
15447
+ }
15448
+ var setupCommand = createCommand(
15449
+ {
15450
+ name: "setup",
15451
+ description: "Interactive first-run setup for license key and LLM provider",
15452
+ handler: async () => executeSetup()
15453
+ },
15454
+ { requireLicense: false }
15455
+ );
15456
+
15286
15457
  // src/auth/fs-credential-provider.ts
15287
15458
  var FsCredentialProvider = class {
15288
15459
  async getCredentials() {
@@ -15344,6 +15515,20 @@ async function checkUpdate() {
15344
15515
  }
15345
15516
 
15346
15517
  // src/cli-action.ts
15518
+ function getConfigFile2() {
15519
+ return join15(process.env.SEAWORTHY_CONFIG_DIR ?? join15(homedir3(), ".seaworthy"), "config");
15520
+ }
15521
+ async function isFirstRun(options) {
15522
+ if (options.json || options.output || options.sarif || options.sarifStdout || process.env.CI || process.env.SEAWORTHY_LICENSE_KEY || process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY || !process.stdin.isTTY || !process.stdout.isTTY) {
15523
+ return false;
15524
+ }
15525
+ try {
15526
+ await access2(getConfigFile2());
15527
+ return false;
15528
+ } catch {
15529
+ return true;
15530
+ }
15531
+ }
15347
15532
  function resolveExitCode(code) {
15348
15533
  const normalized = code.replace(/^errors\./, "");
15349
15534
  if (["license.invalid", "license.expired"].includes(normalized)) return 1;
@@ -15401,14 +15586,22 @@ async function runCli(options) {
15401
15586
  error: getCopy2("cli.error.notFound", dir)
15402
15587
  };
15403
15588
  }
15589
+ if (await isFirstRun(options)) {
15590
+ console.error(getCopy2("cli.firstRun"));
15591
+ await setupCommand.handler({ action: "run" }, { tier: "free", licensedTo: "free tier" });
15592
+ }
15404
15593
  const provider = new FsCredentialProvider();
15405
15594
  const license = new LicenseClient({ baseUrl: LICENSE_SERVER_URL });
15406
15595
  const authResult = await resolveAuth(provider, license);
15407
15596
  if (!authResult.ok) {
15408
- const message = resolveErrorMessage(authResult.code);
15409
- return { code: resolveExitCode(authResult.code), error: message };
15597
+ const normalized = authResult.code.replace(/^errors\./, "");
15598
+ const softCodes = ["license.invalid", "license.expired"];
15599
+ if (!softCodes.includes(normalized)) {
15600
+ const message = resolveErrorMessage(authResult.code);
15601
+ return { code: resolveExitCode(authResult.code), error: message };
15602
+ }
15410
15603
  }
15411
- const auth = authResult.value;
15604
+ const auth = authResult.ok ? authResult.value : { tier: "free", token: "", licensedTo: "free tier" };
15412
15605
  const allowedTiers = {
15413
15606
  free: ["free"],
15414
15607
  pro: ["free", "pro"]
@@ -15437,7 +15630,8 @@ async function runCli(options) {
15437
15630
  maxFileSize: options.maxFileSize,
15438
15631
  debug: options.debug,
15439
15632
  baseline: options.baseline,
15440
- minConfidence: options.minConfidence
15633
+ minConfidence: options.minConfidence,
15634
+ showIds: options.showIds
15441
15635
  };
15442
15636
  const updateStart = Date.now();
15443
15637
  const updatePromise = checkUpdate().catch(() => void 0);
@@ -15524,12 +15718,12 @@ var require5 = createRequire2(import.meta.url);
15524
15718
  var { version } = require5("../package.json");
15525
15719
  var cli = cac("seaworthy");
15526
15720
  cli.version(version);
15527
- cli.command("[path]", "Scan a directory for issues").option("--json", "Output JSON to stdout").option("--output <path>", "Write HTML report to file").option("--sarif <path>", "Write SARIF report to file").option("--sarif-stdout", "Write SARIF report to stdout").option("--fail-on-findings", "Exit with code 5 when findings are found").option("--tier <tier>", "Select tier (free, pro)").option("--provider <provider>", "LLM provider (anthropic, openai, kimi, deepseek, gemini, minimax, ollama, openrouter)").option("--max-file-size <bytes>", "Skip files larger than this (default: 1 MB)").option("--debug", "Enable debug logging").option("--baseline [ref]", "Compare against a baseline file or save current findings as baseline (--baseline save)").option("--min-confidence <level>", "Hide findings below this confidence (high, medium, low)").action(async (targetDir, options) => {
15721
+ cli.command("[path]", "Scan a directory for issues").option("--json", "Output JSON to stdout").option("--output <path>", "Write HTML report to file").option("--sarif <path>", "Write SARIF report to file").option("--sarif-stdout", "Write SARIF report to stdout").option("--fail-on-findings", "Exit with code 5 when findings are found").option("--tier <tier>", "Select tier (free, pro)").option("--provider <provider>", "LLM provider (anthropic, openai, kimi, moonshot, deepseek, gemini, minimax, ollama, openrouter)").option("--max-file-size <bytes>", "Skip files larger than this (default: 1 MB)").option("--show-ids", "Show check IDs alongside names in terminal output").option("--debug", "Enable debug logging").option("--baseline [ref]", "Compare against a baseline file or save current findings as baseline (--baseline save)").option("--min-confidence <level>", "Hide findings below this confidence (high, medium, low)").action(async (targetDir, options) => {
15528
15722
  const maxFileSize = options.maxFileSize ? Number(options.maxFileSize) : void 0;
15529
15723
  const tier = options.tier;
15530
15724
  const baseline = options.baseline === "save" ? "save" : options.baseline;
15531
15725
  const minConfidence = options.minConfidence;
15532
- const result = await runCli({ targetDir, json: options.json, output: options.output, sarif: options.sarif, sarifStdout: options.sarifStdout, failOnFindings: options.failOnFindings, tier, provider: options.provider, maxFileSize, debug: options.debug, baseline, minConfidence });
15726
+ const result = await runCli({ targetDir, json: options.json, output: options.output, sarif: options.sarif, sarifStdout: options.sarifStdout, failOnFindings: options.failOnFindings, tier, provider: options.provider, maxFileSize, showIds: options.showIds, debug: options.debug, baseline, minConfidence });
15533
15727
  if (result.updateMessage) {
15534
15728
  console.error(result.updateMessage);
15535
15729
  }
@@ -15553,15 +15747,22 @@ cli.command("config <action> [key] [value]", "Manage configuration").action(asyn
15553
15747
  console.log(result.output);
15554
15748
  }
15555
15749
  });
15750
+ cli.command("setup", "Interactive setup for license key and LLM provider").action(async () => {
15751
+ const result = await setupCommand.handler({ action: "run" }, { tier: "free", licensedTo: "free tier" });
15752
+ if (!result.ok) {
15753
+ console.error(resolveErrorMessage(result.code));
15754
+ process.exit(2);
15755
+ }
15756
+ });
15556
15757
  cli.help(() => {
15557
15758
  return [
15558
15759
  {
15559
15760
  title: "Examples",
15560
- body: ` $ npx seaworthy ./my-project
15561
- $ npx seaworthy ./my-project --json
15562
- $ npx seaworthy ./my-project --output report.html
15563
- $ npx seaworthy ./my-project --sarif report.sarif
15564
- $ npx seaworthy --provider kimi ./my-project`
15761
+ body: ` $ npx seaworthycode ./my-project
15762
+ $ npx seaworthycode ./my-project --json
15763
+ $ npx seaworthycode ./my-project --output report.html
15764
+ $ npx seaworthycode ./my-project --sarif report.sarif
15765
+ $ npx seaworthycode --provider kimi ./my-project`
15565
15766
  },
15566
15767
  {
15567
15768
  title: "Options",
@@ -15571,12 +15772,17 @@ cli.help(() => {
15571
15772
  --sarif-stdout ${getCopy2("cli.help.flag.sarifStdout")}
15572
15773
  --fail-on-findings Exit with code 5 when findings are found
15573
15774
  --tier <tier> Override tier (free, pro)
15574
- --provider <name> LLM provider (anthropic, openai, kimi, deepseek, gemini, minimax, ollama, openrouter)
15775
+ --provider <name> LLM provider (anthropic, openai, kimi, moonshot, deepseek, gemini, minimax, ollama, openrouter)
15575
15776
  --max-file-size <n> Skip files larger than n bytes (default: 1 MB)
15576
15777
  --baseline [ref] Compare against baseline file or save (--baseline save)
15577
15778
  --min-confidence <level> Hide findings below confidence (high, medium, low)
15779
+ --show-ids Show check IDs alongside names
15578
15780
  --debug Enable debug logging`
15579
15781
  },
15782
+ {
15783
+ title: "Commands",
15784
+ body: ` seaworthy setup Interactive first-run setup`
15785
+ },
15580
15786
  {
15581
15787
  title: "Configuration",
15582
15788
  body: ` ~/.seaworthy/config # license key, LLM provider, API key, model, url`