@ship-safe/cli 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +277 -149
  2. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -11,8 +11,9 @@ import { Command, InvalidArgumentError, Option } from "commander";
11
11
  // src/commands/scan.ts
12
12
  import { resolve as resolve2, join as join4 } from "path";
13
13
  import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
14
- import chalk4 from "chalk";
14
+ import chalk5 from "chalk";
15
15
  import ora2 from "ora";
16
+ import { select } from "@inquirer/prompts";
16
17
  import { parse as parseYaml } from "yaml";
17
18
 
18
19
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
@@ -6873,7 +6874,12 @@ function collectFiles(rootDir, options) {
6873
6874
  ig.add(options.exclude);
6874
6875
  }
6875
6876
  function walk(dir) {
6876
- const entries = readdirSync(dir, { withFileTypes: true });
6877
+ let entries;
6878
+ try {
6879
+ entries = readdirSync(dir, { withFileTypes: true });
6880
+ } catch {
6881
+ return;
6882
+ }
6877
6883
  for (const entry of entries) {
6878
6884
  if (entry.isSymbolicLink()) continue;
6879
6885
  const fullPath = path2.join(dir, entry.name);
@@ -6898,144 +6904,215 @@ function collectFiles(rootDir, options) {
6898
6904
  }
6899
6905
 
6900
6906
  // src/lib/output.ts
6907
+ import chalk2 from "chalk";
6908
+
6909
+ // src/lib/banner.ts
6901
6910
  import chalk from "chalk";
6902
- var SEVERITY_LABEL = {
6903
- critical: chalk.bgRed.white.bold(" CRITICAL "),
6904
- high: chalk.bgYellow.black.bold(" HIGH "),
6905
- medium: chalk.bgHex("#ca8a04").black.bold(" MEDIUM "),
6906
- low: chalk.bgBlue.white.bold(" LOW "),
6907
- info: chalk.bgGray.white(" INFO ")
6911
+ var SHIELD = chalk.hex("#FF6B2B");
6912
+ var DIM_ORANGE = chalk.hex("#FF8C42");
6913
+ var BRAND2 = chalk.hex("#FF6B2B").bold;
6914
+ function printBanner() {
6915
+ console.log("");
6916
+ console.log(
6917
+ SHIELD(" \u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E")
6918
+ );
6919
+ console.log(
6920
+ SHIELD(" \u2502") + chalk.bold.white(" \u{1F6E1}\uFE0F ShipSafe Security Scanner ") + SHIELD("\u2502")
6921
+ );
6922
+ console.log(
6923
+ SHIELD(" \u2502") + chalk.dim(" Ship fast. Ship safe. ") + SHIELD("\u2502")
6924
+ );
6925
+ console.log(
6926
+ SHIELD(" \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F")
6927
+ );
6928
+ console.log("");
6929
+ }
6930
+ function printDivider(width = 50) {
6931
+ console.log(chalk.dim(" " + "\u2500".repeat(width)));
6932
+ }
6933
+ function printError(text) {
6934
+ console.log(chalk.red(` \u2717 ${text}`));
6935
+ }
6936
+ function printInfo(text) {
6937
+ console.log(chalk.dim(` ${text}`));
6938
+ }
6939
+ function printScanSummaryBox(stats) {
6940
+ const label = chalk.dim;
6941
+ const value = chalk.white.bold;
6942
+ console.log("");
6943
+ printDivider();
6944
+ console.log("");
6945
+ console.log(
6946
+ ` ${label("Files:")} ${value(String(stats.files))} ${chalk.dim("\xB7")} ${label("Lines:")} ${value(stats.lines.toLocaleString())} ${chalk.dim("\xB7")} ${label("Time:")} ${value(`${stats.duration}ms`)}`
6947
+ );
6948
+ console.log("");
6949
+ if (stats.findings === 0) {
6950
+ console.log(chalk.green.bold(" \u2713 No security issues found!"));
6951
+ } else {
6952
+ const parts = [];
6953
+ if (stats.critical > 0) parts.push(chalk.red.bold(`${stats.critical} critical`));
6954
+ if (stats.high > 0) parts.push(chalk.yellow.bold(`${stats.high} high`));
6955
+ if (stats.medium > 0) parts.push(chalk.hex("#ca8a04").bold(`${stats.medium} medium`));
6956
+ if (stats.low > 0) parts.push(chalk.blue(`${stats.low} low`));
6957
+ console.log(
6958
+ ` ${chalk.bold.white(`${stats.findings} issues found:`)} ${parts.join(chalk.dim(" \xB7 "))}`
6959
+ );
6960
+ }
6961
+ console.log("");
6962
+ printDivider();
6963
+ }
6964
+ function printUpsellBox(criticalHigh) {
6965
+ console.log("");
6966
+ console.log(DIM_ORANGE(" \u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E"));
6967
+ console.log(
6968
+ DIM_ORANGE(" \u2502") + BRAND2(" \u{1F512} Unlock AI-Powered Deep Analysis ") + DIM_ORANGE("\u2502")
6969
+ );
6970
+ console.log(DIM_ORANGE(" \u2502 \u2502"));
6971
+ if (criticalHigh > 0) {
6972
+ console.log(
6973
+ DIM_ORANGE(" \u2502 ") + chalk.white(`Found ${criticalHigh} critical/high issues. AI finds`) + " ".repeat(Math.max(0, 46 - `Found ${criticalHigh} critical/high issues. AI finds`.length)) + DIM_ORANGE("\u2502")
6974
+ );
6975
+ console.log(
6976
+ DIM_ORANGE(" \u2502 ") + chalk.dim("auth logic flaws, business logic bugs & more.") + " ".repeat(3) + DIM_ORANGE("\u2502")
6977
+ );
6978
+ } else {
6979
+ console.log(
6980
+ DIM_ORANGE(" \u2502 ") + chalk.dim("Surface scan clean. AI finds auth logic,") + " ".repeat(8) + DIM_ORANGE("\u2502")
6981
+ );
6982
+ console.log(
6983
+ DIM_ORANGE(" \u2502 ") + chalk.dim("RLS policies & business logic bugs.") + " ".repeat(13) + DIM_ORANGE("\u2502")
6984
+ );
6985
+ }
6986
+ console.log(DIM_ORANGE(" \u2502 \u2502"));
6987
+ console.log(
6988
+ DIM_ORANGE(" \u2502 ") + chalk.white("\u2192 Run ") + BRAND2("shipsafe login") + chalk.white(" to get started") + " ".repeat(13) + DIM_ORANGE("\u2502")
6989
+ );
6990
+ console.log(DIM_ORANGE(" \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F"));
6991
+ }
6992
+
6993
+ // src/lib/output.ts
6994
+ var SEVERITY_BADGE = {
6995
+ critical: chalk2.bgRed.white.bold(" CRITICAL "),
6996
+ high: chalk2.bgYellow.black.bold(" HIGH "),
6997
+ medium: chalk2.bgHex("#ca8a04").black.bold(" MEDIUM "),
6998
+ low: chalk2.bgBlue.white.bold(" LOW "),
6999
+ info: chalk2.bgGray.white(" INFO ")
6908
7000
  };
6909
7001
  var SEVERITY_COLOR = {
6910
- critical: chalk.red,
6911
- high: chalk.yellow,
6912
- medium: chalk.hex("#ca8a04"),
6913
- low: chalk.blue,
6914
- info: chalk.gray
7002
+ critical: chalk2.red,
7003
+ high: chalk2.yellow,
7004
+ medium: chalk2.hex("#ca8a04"),
7005
+ low: chalk2.blue,
7006
+ info: chalk2.gray
6915
7007
  };
6916
- function formatFinding(finding, index) {
7008
+ function formatFinding(finding) {
6917
7009
  const lines = [];
6918
7010
  const color = SEVERITY_COLOR[finding.severity];
6919
7011
  lines.push(
6920
- `
6921
- ${SEVERITY_LABEL[finding.severity]} ${chalk.bold(finding.title)}`
7012
+ `${SEVERITY_BADGE[finding.severity]} ${chalk2.bold.white(finding.title)}`
6922
7013
  );
6923
7014
  lines.push(
6924
- chalk.gray(` ${finding.file}:${finding.line}`) + (finding.cwe ? chalk.gray(` (${finding.cwe})`) : "")
7015
+ chalk2.dim(" \u{1F4C4} ") + chalk2.cyan(finding.file) + chalk2.dim(`:${finding.line}`) + (finding.cwe ? chalk2.dim(` \xB7 ${finding.cwe}`) : "")
6925
7016
  );
6926
- lines.push(` ${finding.description}`);
7017
+ lines.push(` ${chalk2.dim(finding.description)}`);
6927
7018
  if (finding.snippet) {
6928
7019
  lines.push("");
6929
- for (const snippetLine of finding.snippet.split("\n")) {
7020
+ const snippetLines = finding.snippet.split("\n");
7021
+ for (const snippetLine of snippetLines) {
6930
7022
  if (snippetLine.startsWith(">")) {
6931
- lines.push(` ${color(snippetLine)}`);
7023
+ lines.push(` ${color.bold(snippetLine)}`);
6932
7024
  } else {
6933
- lines.push(` ${chalk.gray(snippetLine)}`);
7025
+ lines.push(` ${chalk2.gray(snippetLine)}`);
6934
7026
  }
6935
7027
  }
6936
7028
  }
6937
7029
  lines.push("");
6938
- lines.push(chalk.green(` Fix: ${finding.fix.description}`));
7030
+ lines.push(chalk2.green(` \u{1F4A1} Fix: ${finding.fix.description}`));
6939
7031
  if (finding.fix.suggestion) {
6940
- lines.push(chalk.green.dim(` Suggestion: ${finding.fix.suggestion}`));
7032
+ lines.push(chalk2.green.dim(` ${finding.fix.suggestion}`));
6941
7033
  }
6942
7034
  return lines.join("\n");
6943
7035
  }
6944
7036
  function formatTableOutput(result, options = {}) {
6945
7037
  const lines = [];
6946
- lines.push("");
6947
- lines.push(chalk.bold.cyan(" ShipSafe Security Scan Results"));
6948
- lines.push(chalk.gray(" " + "\u2500".repeat(50)));
6949
- lines.push("");
6950
7038
  const { summary } = result;
6951
- lines.push(
6952
- ` ${chalk.bold("Files scanned:")} ${summary.filesScanned} | ${chalk.bold("Lines:")} ${summary.linesScanned.toLocaleString()} | ${chalk.bold("Duration:")} ${result.durationMs}ms`
6953
- );
6954
- lines.push("");
7039
+ printScanSummaryBox({
7040
+ files: summary.filesScanned,
7041
+ lines: summary.linesScanned,
7042
+ duration: result.durationMs,
7043
+ findings: summary.total,
7044
+ critical: summary.bySeverity.critical,
7045
+ high: summary.bySeverity.high,
7046
+ medium: summary.bySeverity.medium,
7047
+ low: summary.bySeverity.low
7048
+ });
6955
7049
  if (summary.total === 0) {
6956
- lines.push(chalk.green.bold(" No security issues found!"));
6957
7050
  if (!options.isLoggedIn) {
6958
- lines.push("");
6959
7051
  lines.push(
6960
- chalk.gray(
6961
- ` No issues in surface scan. Run ${chalk.bold.white("shipsafe login")} to unlock deep AI analysis.`
7052
+ chalk2.dim(
7053
+ ` Run ${chalk2.bold.white("shipsafe login")} to unlock deep AI analysis.`
6962
7054
  )
6963
7055
  );
7056
+ lines.push("");
6964
7057
  }
6965
- lines.push("");
6966
7058
  return lines.join("\n");
6967
7059
  }
6968
- const severityLine = [
6969
- summary.bySeverity.critical > 0 ? chalk.red.bold(`${summary.bySeverity.critical} critical`) : null,
6970
- summary.bySeverity.high > 0 ? chalk.yellow.bold(`${summary.bySeverity.high} high`) : null,
6971
- summary.bySeverity.medium > 0 ? chalk.hex("#ca8a04").bold(`${summary.bySeverity.medium} medium`) : null,
6972
- summary.bySeverity.low > 0 ? chalk.blue(`${summary.bySeverity.low} low`) : null,
6973
- summary.bySeverity.info > 0 ? chalk.gray(`${summary.bySeverity.info} info`) : null
6974
- ].filter(Boolean).join(" | ");
6975
- lines.push(` ${chalk.bold("Findings:")} ${summary.total} (${severityLine})`);
6976
7060
  const sorted = [...result.findings].sort(
6977
7061
  (a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity]
6978
7062
  );
6979
- for (let i = 0; i < sorted.length; i++) {
6980
- lines.push(formatFinding(sorted[i], i));
7063
+ let currentSeverity = null;
7064
+ let countInSeverity = 0;
7065
+ for (const finding of sorted) {
7066
+ if (finding.severity !== currentSeverity) {
7067
+ if (currentSeverity !== null) {
7068
+ lines.push("");
7069
+ }
7070
+ currentSeverity = finding.severity;
7071
+ countInSeverity = sorted.filter(
7072
+ (f) => f.severity === currentSeverity
7073
+ ).length;
7074
+ lines.push(
7075
+ chalk2.dim(" \u2500\u2500\u2500\u2500 ") + SEVERITY_COLOR[currentSeverity].bold(
7076
+ `${currentSeverity.toUpperCase()} (${countInSeverity})`
7077
+ ) + chalk2.dim(" " + "\u2500".repeat(35))
7078
+ );
7079
+ lines.push("");
7080
+ }
7081
+ lines.push(formatFinding(finding));
7082
+ lines.push("");
6981
7083
  }
6982
7084
  if (options.diff) {
6983
7085
  const { newFindings, resolvedFindings } = options.diff;
6984
7086
  if (newFindings.length > 0 || resolvedFindings.length > 0) {
6985
- lines.push("");
6986
- lines.push(chalk.bold(" Changes since last scan:"));
7087
+ printDivider();
7088
+ lines.push(chalk2.bold.white(" \u{1F4CA} Changes since last scan:"));
6987
7089
  if (newFindings.length > 0) {
6988
7090
  lines.push(
6989
- chalk.red(` \u2B06 ${newFindings.length} new issue${newFindings.length === 1 ? "" : "s"} introduced`)
7091
+ chalk2.red(
7092
+ ` \u2191 ${newFindings.length} new issue${newFindings.length === 1 ? "" : "s"} introduced`
7093
+ )
6990
7094
  );
6991
7095
  }
6992
7096
  if (resolvedFindings.length > 0) {
6993
7097
  lines.push(
6994
- chalk.green(` \u2B07 ${resolvedFindings.length} issue${resolvedFindings.length === 1 ? "" : "s"} resolved`)
7098
+ chalk2.green(
7099
+ ` \u2193 ${resolvedFindings.length} issue${resolvedFindings.length === 1 ? "" : "s"} resolved`
7100
+ )
6995
7101
  );
6996
7102
  }
6997
7103
  }
6998
7104
  }
6999
7105
  if (options.suppressedCount && options.suppressedCount > 0) {
7000
7106
  lines.push(
7001
- chalk.gray(`
7002
- ${options.suppressedCount} finding${options.suppressedCount === 1 ? "" : "s"} suppressed via .shipsafeignore`)
7107
+ chalk2.dim(
7108
+ `
7109
+ ${options.suppressedCount} finding${options.suppressedCount === 1 ? "" : "s"} suppressed via .shipsafeignore`
7110
+ )
7003
7111
  );
7004
7112
  }
7005
- lines.push("");
7006
- lines.push(chalk.gray(" " + "\u2500".repeat(50)));
7007
7113
  if (!options.isLoggedIn && summary.total > 0) {
7008
7114
  const criticalHigh = summary.bySeverity.critical + summary.bySeverity.high;
7009
- lines.push("");
7010
- if (criticalHigh > 0) {
7011
- lines.push(
7012
- chalk.yellow(
7013
- ` Found ${criticalHigh} critical/high issue${criticalHigh === 1 ? "" : "s"}. Deep AI analysis can find auth logic flaws,`
7014
- )
7015
- );
7016
- lines.push(
7017
- chalk.yellow(
7018
- ` business logic bugs, and more \u2014 run ${chalk.bold("shipsafe login")} to unlock.`
7019
- )
7020
- );
7021
- } else {
7022
- lines.push(
7023
- chalk.gray(
7024
- ` Surface scan clean. Run ${chalk.bold.white("shipsafe login")} for deep AI analysis`
7025
- )
7026
- );
7027
- lines.push(
7028
- chalk.gray(
7029
- ` covering auth logic, RLS policies, and business logic review.`
7030
- )
7031
- );
7032
- }
7033
- } else if (!options.isLoggedIn && summary.total === 0) {
7034
- lines.push(
7035
- chalk.gray(
7036
- ` No issues in surface scan. Run ${chalk.bold.white("shipsafe login")} to unlock deep AI analysis.`
7037
- )
7038
- );
7115
+ printUpsellBox(criticalHigh);
7039
7116
  }
7040
7117
  lines.push("");
7041
7118
  return lines.join("\n");
@@ -7084,7 +7161,7 @@ function formatSarifOutput(result) {
7084
7161
  }
7085
7162
 
7086
7163
  // src/commands/login.ts
7087
- import chalk2 from "chalk";
7164
+ import chalk3 from "chalk";
7088
7165
  import ora from "ora";
7089
7166
  import { readFileSync as readFileSync2, writeFileSync, mkdirSync, existsSync as existsSync2, unlinkSync } from "fs";
7090
7167
  import { homedir } from "os";
@@ -7096,7 +7173,7 @@ function getStoredToken() {
7096
7173
  if (!existsSync2(TOKEN_FILE)) return null;
7097
7174
  const data = JSON.parse(readFileSync2(TOKEN_FILE, "utf-8"));
7098
7175
  if (data.expiresAt < Date.now()) {
7099
- console.log(chalk2.yellow("Session expired. Please login again."));
7176
+ console.log(chalk3.yellow("Session expired. Please login again."));
7100
7177
  return null;
7101
7178
  }
7102
7179
  return data;
@@ -7123,7 +7200,7 @@ async function fetchProfile(apiUrl, rawToken) {
7123
7200
  }
7124
7201
  async function loginCommand(options) {
7125
7202
  const apiUrl = options.apiUrl || "https://ship-safe.co";
7126
- console.log(chalk2.bold("\nShipSafe CLI Login\n"));
7203
+ console.log(chalk3.bold("\nShipSafe CLI Login\n"));
7127
7204
  const spinner = ora("Requesting device code...").start();
7128
7205
  try {
7129
7206
  const res = await fetch(`${apiUrl}/api/cli/device-code`, {
@@ -7133,7 +7210,7 @@ async function loginCommand(options) {
7133
7210
  if (!res.ok) {
7134
7211
  spinner.fail("Failed to request device code");
7135
7212
  console.log(
7136
- chalk2.red(
7213
+ chalk3.red(
7137
7214
  "Could not connect to ShipSafe. Make sure the server is running."
7138
7215
  )
7139
7216
  );
@@ -7141,10 +7218,10 @@ async function loginCommand(options) {
7141
7218
  }
7142
7219
  const { deviceCode, userCode, verificationUrl, expiresIn } = await res.json();
7143
7220
  spinner.stop();
7144
- console.log(chalk2.bold("To login, open this URL in your browser:\n"));
7145
- console.log(chalk2.cyan(` ${verificationUrl}
7221
+ console.log(chalk3.bold("To login, open this URL in your browser:\n"));
7222
+ console.log(chalk3.cyan(` ${verificationUrl}
7146
7223
  `));
7147
- console.log(`Enter this code: ${chalk2.bold.green(userCode)}
7224
+ console.log(`Enter this code: ${chalk3.bold.green(userCode)}
7148
7225
  `);
7149
7226
  const pollSpinner = ora("Waiting for authorization...").start();
7150
7227
  const startTime = Date.now();
@@ -7175,17 +7252,17 @@ async function loginCommand(options) {
7175
7252
  }
7176
7253
  storeToken(tokenData);
7177
7254
  pollSpinner.succeed(
7178
- chalk2.green(`Logged in as ${chalk2.bold(pollData.email)}`)
7255
+ chalk3.green(`Logged in as ${chalk3.bold(pollData.email)}`)
7179
7256
  );
7180
7257
  if (profile?.cliTier) {
7181
7258
  console.log(
7182
- chalk2.cyan(
7259
+ chalk3.cyan(
7183
7260
  ` CLI plan: ${profile.cliTier} (${profile.aiQuota.remaining} AI scans remaining)`
7184
7261
  )
7185
7262
  );
7186
7263
  } else {
7187
7264
  console.log(
7188
- chalk2.dim(
7265
+ chalk3.dim(
7189
7266
  " No CLI plan. Upgrade at ship-safe.co/pricing for AI-powered scanning."
7190
7267
  )
7191
7268
  );
@@ -7194,7 +7271,7 @@ async function loginCommand(options) {
7194
7271
  }
7195
7272
  if (pollData.status === "expired") {
7196
7273
  pollSpinner.fail(
7197
- chalk2.yellow(
7274
+ chalk3.yellow(
7198
7275
  "Your login link expired. Run `shipsafe login` again."
7199
7276
  )
7200
7277
  );
@@ -7204,7 +7281,7 @@ async function loginCommand(options) {
7204
7281
  networkErrors++;
7205
7282
  if (networkErrors >= 3) {
7206
7283
  pollSpinner.fail(
7207
- chalk2.red(
7284
+ chalk3.red(
7208
7285
  "Lost connection to ShipSafe. Check your internet and run `shipsafe login` again."
7209
7286
  )
7210
7287
  );
@@ -7214,7 +7291,7 @@ async function loginCommand(options) {
7214
7291
  }
7215
7292
  }
7216
7293
  pollSpinner.fail(
7217
- chalk2.yellow(
7294
+ chalk3.yellow(
7218
7295
  "Your login link expired. Run `shipsafe login` again."
7219
7296
  )
7220
7297
  );
@@ -7222,13 +7299,13 @@ async function loginCommand(options) {
7222
7299
  spinner.fail("Could not connect to ShipSafe");
7223
7300
  if (error instanceof Error && (error.message.includes("fetch") || error.message.includes("ECONNREFUSED"))) {
7224
7301
  console.log(
7225
- chalk2.red(
7302
+ chalk3.red(
7226
7303
  "Make sure you have an internet connection and try again."
7227
7304
  )
7228
7305
  );
7229
7306
  } else {
7230
7307
  console.log(
7231
- chalk2.red(
7308
+ chalk3.red(
7232
7309
  error instanceof Error ? error.message : "Unknown error \u2014 try again."
7233
7310
  )
7234
7311
  );
@@ -7239,18 +7316,18 @@ async function logoutCommand() {
7239
7316
  try {
7240
7317
  if (existsSync2(TOKEN_FILE)) {
7241
7318
  unlinkSync(TOKEN_FILE);
7242
- console.log(chalk2.green("Logged out successfully."));
7319
+ console.log(chalk3.green("Logged out successfully."));
7243
7320
  } else {
7244
- console.log(chalk2.yellow("Not currently logged in."));
7321
+ console.log(chalk3.yellow("Not currently logged in."));
7245
7322
  }
7246
7323
  } catch {
7247
- console.log(chalk2.red("Failed to logout."));
7324
+ console.log(chalk3.red("Failed to logout."));
7248
7325
  }
7249
7326
  }
7250
7327
  async function whoamiCommand(options) {
7251
7328
  const token = getStoredToken();
7252
7329
  if (!token) {
7253
- console.log(chalk2.yellow("Not logged in. Run `shipsafe login` to login."));
7330
+ console.log(chalk3.yellow("Not logged in. Run `shipsafe login` to login."));
7254
7331
  return;
7255
7332
  }
7256
7333
  const apiUrl = options.apiUrl || "https://ship-safe.co";
@@ -7264,26 +7341,26 @@ async function whoamiCommand(options) {
7264
7341
  aiQuota: profile.aiQuota
7265
7342
  });
7266
7343
  spinner.stop();
7267
- console.log(chalk2.bold("\nShipSafe Account\n"));
7268
- console.log(` Email: ${chalk2.cyan(profile.email)}`);
7269
- console.log(` Web tier: ${chalk2.cyan(profile.tier)}`);
7344
+ console.log(chalk3.bold("\nShipSafe Account\n"));
7345
+ console.log(` Email: ${chalk3.cyan(profile.email)}`);
7346
+ console.log(` Web tier: ${chalk3.cyan(profile.tier)}`);
7270
7347
  if (profile.cliTier) {
7271
- console.log(` CLI plan: ${chalk2.green(profile.cliTier)}`);
7348
+ console.log(` CLI plan: ${chalk3.green(profile.cliTier)}`);
7272
7349
  console.log(
7273
- ` AI scans: ${chalk2.cyan(`${profile.aiQuota.used}/${profile.aiQuota.limit}`)} used this month (${chalk2.green(`${profile.aiQuota.remaining}`)} remaining)`
7350
+ ` AI scans: ${chalk3.cyan(`${profile.aiQuota.used}/${profile.aiQuota.limit}`)} used this month (${chalk3.green(`${profile.aiQuota.remaining}`)} remaining)`
7274
7351
  );
7275
7352
  } else {
7276
- console.log(` CLI plan: ${chalk2.dim("none")}`);
7353
+ console.log(` CLI plan: ${chalk3.dim("none")}`);
7277
7354
  console.log(
7278
- chalk2.dim(" Upgrade at ship-safe.co/pricing for AI-powered scanning.")
7355
+ chalk3.dim(" Upgrade at ship-safe.co/pricing for AI-powered scanning.")
7279
7356
  );
7280
7357
  }
7281
7358
  console.log();
7282
7359
  } else {
7283
7360
  spinner.stop();
7284
- console.log(chalk2.green(`Logged in as ${chalk2.bold(token.email)}`));
7361
+ console.log(chalk3.green(`Logged in as ${chalk3.bold(token.email)}`));
7285
7362
  if (token.cliTier) {
7286
- console.log(chalk2.cyan(` CLI plan: ${token.cliTier}`));
7363
+ console.log(chalk3.cyan(` CLI plan: ${token.cliTier}`));
7287
7364
  }
7288
7365
  }
7289
7366
  }
@@ -7291,7 +7368,7 @@ async function whoamiCommand(options) {
7291
7368
  // src/commands/ignore.ts
7292
7369
  import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, appendFileSync } from "fs";
7293
7370
  import { resolve, join as join2 } from "path";
7294
- import chalk3 from "chalk";
7371
+ import chalk4 from "chalk";
7295
7372
  var IGNORE_FILE = ".shipsafeignore";
7296
7373
  function getIgnorePath(dir) {
7297
7374
  return resolve(dir || ".", IGNORE_FILE);
@@ -7315,7 +7392,7 @@ function ignoreCommand(ruleId, options) {
7315
7392
  if (existsSync3(ignorePath)) {
7316
7393
  const existing = loadIgnoredRules(".");
7317
7394
  if (existing.has(ruleId)) {
7318
- console.log(chalk3.yellow(`Rule "${ruleId}" is already in ${IGNORE_FILE}`));
7395
+ console.log(chalk4.yellow(`Rule "${ruleId}" is already in ${IGNORE_FILE}`));
7319
7396
  return;
7320
7397
  }
7321
7398
  }
@@ -7336,18 +7413,18 @@ ${line}
7336
7413
  `
7337
7414
  );
7338
7415
  }
7339
- console.log(chalk3.green(`Added "${ruleId}" to ${IGNORE_FILE}`));
7416
+ console.log(chalk4.green(`Added "${ruleId}" to ${IGNORE_FILE}`));
7340
7417
  if (options.reason) {
7341
- console.log(chalk3.gray(` Reason: ${options.reason}`));
7418
+ console.log(chalk4.gray(` Reason: ${options.reason}`));
7342
7419
  }
7343
7420
  console.log(
7344
- chalk3.gray(` This finding will be suppressed in future scans.`)
7421
+ chalk4.gray(` This finding will be suppressed in future scans.`)
7345
7422
  );
7346
7423
  }
7347
7424
  function unignoreCommand(ruleId) {
7348
7425
  const ignorePath = getIgnorePath();
7349
7426
  if (!existsSync3(ignorePath)) {
7350
- console.log(chalk3.yellow(`No ${IGNORE_FILE} found.`));
7427
+ console.log(chalk4.yellow(`No ${IGNORE_FILE} found.`));
7351
7428
  return;
7352
7429
  }
7353
7430
  const content = readFileSync3(ignorePath, "utf-8");
@@ -7359,11 +7436,11 @@ function unignoreCommand(ruleId) {
7359
7436
  return lineRuleId !== ruleId;
7360
7437
  });
7361
7438
  if (filtered.length === lines.length) {
7362
- console.log(chalk3.yellow(`Rule "${ruleId}" is not in ${IGNORE_FILE}`));
7439
+ console.log(chalk4.yellow(`Rule "${ruleId}" is not in ${IGNORE_FILE}`));
7363
7440
  return;
7364
7441
  }
7365
7442
  writeFileSync2(ignorePath, filtered.join("\n"));
7366
- console.log(chalk3.green(`Removed "${ruleId}" from ${IGNORE_FILE}`));
7443
+ console.log(chalk4.green(`Removed "${ruleId}" from ${IGNORE_FILE}`));
7367
7444
  }
7368
7445
 
7369
7446
  // src/lib/scan-history.ts
@@ -7448,28 +7525,81 @@ function loadConfig(dir) {
7448
7525
  async function scanCommand(targetPath, options) {
7449
7526
  const resolvedPath = resolve2(targetPath);
7450
7527
  if (!existsSync5(resolvedPath)) {
7451
- console.error(chalk4.red(`Error: Path "${targetPath}" does not exist.`));
7528
+ printError(`Path "${targetPath}" does not exist.`);
7452
7529
  process.exit(1);
7453
7530
  }
7531
+ if (options.output === "table") {
7532
+ printBanner();
7533
+ }
7534
+ let tokenData = getStoredToken();
7535
+ if (!tokenData && !options.ci && options.output === "table") {
7536
+ const choice = await select({
7537
+ message: chalk5.white("How would you like to scan?"),
7538
+ choices: [
7539
+ {
7540
+ name: `${chalk5.green("\u25B8")} ${chalk5.bold("Free scan")} ${chalk5.dim("\u2014 rule-based analysis, no account needed")}`,
7541
+ value: "free"
7542
+ },
7543
+ {
7544
+ name: `${chalk5.hex("#FF6B2B")("\u25B8")} ${chalk5.bold("Login first")} ${chalk5.dim("\u2014 unlock AI-powered deep analysis")}`,
7545
+ value: "login"
7546
+ }
7547
+ ],
7548
+ theme: {
7549
+ prefix: { idle: chalk5.hex("#FF6B2B")(" \u{1F6E1}\uFE0F"), done: chalk5.green(" \u2713") },
7550
+ style: {
7551
+ highlight: (text) => chalk5.hex("#FF6B2B")(text)
7552
+ }
7553
+ }
7554
+ });
7555
+ if (choice === "login") {
7556
+ await loginCommand({ apiUrl: options.apiUrl });
7557
+ tokenData = getStoredToken();
7558
+ if (!tokenData) {
7559
+ printInfo("Login skipped. Running free scan...\n");
7560
+ } else {
7561
+ console.log("");
7562
+ }
7563
+ } else {
7564
+ console.log("");
7565
+ }
7566
+ }
7454
7567
  const config = loadConfig(resolvedPath);
7455
7568
  const severity = options.severity || config.severity || "low";
7456
- const spinner = ora2("Collecting files...").start();
7569
+ const spinner = ora2({
7570
+ text: chalk5.dim("Collecting files..."),
7571
+ color: "yellow",
7572
+ spinner: "dots12"
7573
+ }).start();
7457
7574
  const files = collectFiles(resolvedPath, { exclude: config.exclude });
7458
7575
  if (files.length === 0) {
7459
- spinner.fail("No supported files found to scan.");
7576
+ spinner.fail(chalk5.red("No supported files found to scan."));
7460
7577
  process.exit(1);
7461
7578
  }
7462
- spinner.text = `Scanning ${files.length} files...`;
7579
+ const totalLines = files.reduce(
7580
+ (sum, f) => sum + f.content.split("\n").length,
7581
+ 0
7582
+ );
7583
+ spinner.text = chalk5.dim(
7584
+ `Scanning ${chalk5.white.bold(String(files.length))} files (${chalk5.white.bold(totalLines.toLocaleString())} lines)...`
7585
+ );
7463
7586
  const result = await scan(
7464
7587
  { files, tier: "free" },
7465
7588
  (progress) => {
7466
- spinner.text = `${progress.stage} (${progress.findingsCount} findings)`;
7589
+ spinner.text = chalk5.dim(
7590
+ `${progress.stage} ${chalk5.white(`(${progress.findingsCount} findings)`)}`
7591
+ );
7467
7592
  }
7468
7593
  );
7469
- spinner.stop();
7470
- const tokenData = getStoredToken();
7594
+ spinner.succeed(
7595
+ chalk5.green(`Scan complete \u2014 ${result.findings.length} findings in ${result.durationMs}ms`)
7596
+ );
7471
7597
  if (tokenData?.cliTier) {
7472
- const aiSpinner = ora2("Running AI-powered deep analysis...").start();
7598
+ const aiSpinner = ora2({
7599
+ text: chalk5.dim("Running AI-powered deep analysis..."),
7600
+ color: "yellow",
7601
+ spinner: "dots12"
7602
+ }).start();
7473
7603
  try {
7474
7604
  const aiRes = await fetch(`${options.apiUrl}/api/cli/ai-scan`, {
7475
7605
  method: "POST",
@@ -7510,31 +7640,29 @@ async function scanCommand(targetPath, options) {
7510
7640
  info: result.findings.filter((f) => f.severity === "info").length
7511
7641
  };
7512
7642
  aiSpinner.succeed(
7513
- chalk4.green(
7514
- `AI analysis complete: ${aiFindings.length} additional findings`
7643
+ chalk5.green(
7644
+ `AI analysis: ${aiFindings.length} additional findings`
7515
7645
  )
7516
7646
  );
7517
- console.log(
7518
- chalk4.dim(
7519
- ` AI scans: ${aiData.quota.used}/${aiData.quota.limit} used this month (${aiData.quota.remaining} remaining)`
7520
- )
7647
+ printInfo(
7648
+ `AI scans: ${aiData.quota.used}/${aiData.quota.limit} used this month (${aiData.quota.remaining} remaining)`
7521
7649
  );
7522
7650
  } else if (aiRes.status === 429) {
7523
7651
  aiSpinner.warn(
7524
- chalk4.yellow("AI scan quota reached. Showing rule-based results only.")
7652
+ chalk5.yellow("AI scan quota reached. Showing rule-based results only.")
7525
7653
  );
7526
7654
  } else if (aiRes.status === 401) {
7527
7655
  aiSpinner.warn(
7528
- chalk4.yellow("Session expired. Run `shipsafe login` to re-authenticate.")
7656
+ chalk5.yellow("Session expired. Run `shipsafe login` to re-authenticate.")
7529
7657
  );
7530
7658
  } else {
7531
7659
  aiSpinner.warn(
7532
- chalk4.yellow("AI analysis unavailable. Showing rule-based results only.")
7660
+ chalk5.yellow("AI analysis unavailable. Showing rule-based results only.")
7533
7661
  );
7534
7662
  }
7535
7663
  } catch {
7536
7664
  aiSpinner.warn(
7537
- chalk4.yellow("Could not reach AI scanning service. Showing rule-based results only.")
7665
+ chalk5.yellow("Could not reach AI scanning service. Showing rule-based results only.")
7538
7666
  );
7539
7667
  }
7540
7668
  }
@@ -7582,7 +7710,11 @@ async function scanCommand(targetPath, options) {
7582
7710
  }
7583
7711
  const token = getStoredToken();
7584
7712
  if (token && options.upload) {
7585
- const uploadSpinner = ora2("Syncing results to dashboard...").start();
7713
+ const uploadSpinner = ora2({
7714
+ text: chalk5.dim("Syncing results to dashboard..."),
7715
+ color: "yellow",
7716
+ spinner: "dots12"
7717
+ }).start();
7586
7718
  try {
7587
7719
  const apiFindings = result.findings.map((f) => ({
7588
7720
  ruleId: f.ruleId,
@@ -7615,14 +7747,10 @@ async function scanCommand(targetPath, options) {
7615
7747
  if (res.ok) {
7616
7748
  const data = await res.json();
7617
7749
  uploadSpinner.succeed(
7618
- chalk4.green(`Results synced: ${data.dashboardUrl || "view in dashboard"}`)
7750
+ chalk5.green(`Results synced: ${data.dashboardUrl || "view in dashboard"}`)
7619
7751
  );
7620
7752
  if (data.deepScanStatus === "running") {
7621
- console.log(
7622
- chalk4.cyan(
7623
- " Deep AI analysis running \u2014 check dashboard for updates."
7624
- )
7625
- );
7753
+ printInfo("Deep AI analysis running \u2014 check dashboard for updates.");
7626
7754
  }
7627
7755
  } else if (res.status === 401) {
7628
7756
  uploadSpinner.warn(
@@ -7638,7 +7766,7 @@ async function scanCommand(targetPath, options) {
7638
7766
  }
7639
7767
  } else if (options.upload) {
7640
7768
  console.log(
7641
- chalk4.yellow(
7769
+ chalk5.yellow(
7642
7770
  "\nNot logged in. Run `shipsafe login` first to sync results."
7643
7771
  )
7644
7772
  );
@@ -7656,7 +7784,7 @@ async function scanCommand(targetPath, options) {
7656
7784
  // src/commands/init.ts
7657
7785
  import { writeFileSync as writeFileSync4, existsSync as existsSync6 } from "fs";
7658
7786
  import { resolve as resolve3 } from "path";
7659
- import chalk5 from "chalk";
7787
+ import chalk6 from "chalk";
7660
7788
  var DEFAULT_CONFIG = `# ShipSafe Configuration
7661
7789
  # https://ship-safe.co/docs/config
7662
7790
 
@@ -7687,11 +7815,11 @@ severity: low
7687
7815
  function initCommand() {
7688
7816
  const configPath = resolve3(".shipsafe.yml");
7689
7817
  if (existsSync6(configPath)) {
7690
- console.log(chalk5.yellow("A .shipsafe.yml already exists in this directory."));
7818
+ console.log(chalk6.yellow("A .shipsafe.yml already exists in this directory."));
7691
7819
  return;
7692
7820
  }
7693
7821
  writeFileSync4(configPath, DEFAULT_CONFIG, "utf-8");
7694
- console.log(chalk5.green("Created .shipsafe.yml configuration file."));
7822
+ console.log(chalk6.green("Created .shipsafe.yml configuration file."));
7695
7823
  }
7696
7824
 
7697
7825
  // src/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ship-safe/cli",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "Security scanner for AI-generated code — find vulnerabilities before you ship",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -40,6 +40,7 @@
40
40
  },
41
41
  "dependencies": {
42
42
  "@anthropic-ai/sdk": "^0.39",
43
+ "@inquirer/prompts": "^8.3.2",
43
44
  "chalk": "^5",
44
45
  "commander": "^13",
45
46
  "ignore": "^7",