@ship-safe/cli 1.0.2 → 1.1.1

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 -148
  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
@@ -6903,144 +6904,221 @@ function collectFiles(rootDir, options) {
6903
6904
  }
6904
6905
 
6905
6906
  // src/lib/output.ts
6907
+ import chalk2 from "chalk";
6908
+
6909
+ // src/lib/banner.ts
6906
6910
  import chalk from "chalk";
6907
- var SEVERITY_LABEL = {
6908
- critical: chalk.bgRed.white.bold(" CRITICAL "),
6909
- high: chalk.bgYellow.black.bold(" HIGH "),
6910
- medium: chalk.bgHex("#ca8a04").black.bold(" MEDIUM "),
6911
- low: chalk.bgBlue.white.bold(" LOW "),
6912
- info: chalk.bgGray.white(" INFO ")
6911
+ var PURPLE = chalk.hex("#A855F7");
6912
+ var PURPLE_BOLD = chalk.hex("#A855F7").bold;
6913
+ var DIM_PURPLE = chalk.hex("#C084FC");
6914
+ function printBanner() {
6915
+ console.log("");
6916
+ console.log(
6917
+ PURPLE(" \u250F\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513")
6918
+ );
6919
+ console.log(
6920
+ PURPLE(" \u2503") + chalk.bold.white(" ") + PURPLE("\u2503")
6921
+ );
6922
+ console.log(
6923
+ PURPLE(" \u2503") + chalk.bold.white(" ") + PURPLE_BOLD("S H I P S A F E") + chalk.bold.white(" ") + PURPLE("\u2503")
6924
+ );
6925
+ console.log(
6926
+ PURPLE(" \u2503") + chalk.dim(" Security Scanner for AI-gen code ") + PURPLE("\u2503")
6927
+ );
6928
+ console.log(
6929
+ PURPLE(" \u2503") + chalk.bold.white(" ") + PURPLE("\u2503")
6930
+ );
6931
+ console.log(
6932
+ PURPLE(" \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251B")
6933
+ );
6934
+ console.log("");
6935
+ }
6936
+ function printDivider(width = 50) {
6937
+ console.log(chalk.dim(" " + "\u2500".repeat(width)));
6938
+ }
6939
+ function printError(text) {
6940
+ console.log(chalk.red(` \u2717 ${text}`));
6941
+ }
6942
+ function printInfo(text) {
6943
+ console.log(chalk.dim(` ${text}`));
6944
+ }
6945
+ function printScanSummaryBox(stats) {
6946
+ const label = chalk.dim;
6947
+ const value = chalk.white.bold;
6948
+ console.log("");
6949
+ printDivider();
6950
+ console.log("");
6951
+ console.log(
6952
+ ` ${label("Files:")} ${value(String(stats.files))} ${chalk.dim("\xB7")} ${label("Lines:")} ${value(stats.lines.toLocaleString())} ${chalk.dim("\xB7")} ${label("Time:")} ${value(`${stats.duration}ms`)}`
6953
+ );
6954
+ console.log("");
6955
+ if (stats.findings === 0) {
6956
+ console.log(chalk.green.bold(" \u2713 No security issues found!"));
6957
+ } else {
6958
+ const parts = [];
6959
+ if (stats.critical > 0) parts.push(chalk.red.bold(`${stats.critical} critical`));
6960
+ if (stats.high > 0) parts.push(chalk.yellow.bold(`${stats.high} high`));
6961
+ if (stats.medium > 0) parts.push(chalk.hex("#ca8a04").bold(`${stats.medium} medium`));
6962
+ if (stats.low > 0) parts.push(chalk.blue(`${stats.low} low`));
6963
+ console.log(
6964
+ ` ${chalk.bold.white(`${stats.findings} issues found:`)} ${parts.join(chalk.dim(" \xB7 "))}`
6965
+ );
6966
+ }
6967
+ console.log("");
6968
+ printDivider();
6969
+ }
6970
+ function printUpsellBox(criticalHigh) {
6971
+ console.log("");
6972
+ console.log(DIM_PURPLE(" \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"));
6973
+ console.log(
6974
+ DIM_PURPLE(" \u2502") + PURPLE_BOLD(" \u{1F512} Unlock AI-Powered Deep Analysis ") + DIM_PURPLE("\u2502")
6975
+ );
6976
+ console.log(DIM_PURPLE(" \u2502 \u2502"));
6977
+ if (criticalHigh > 0) {
6978
+ console.log(
6979
+ DIM_PURPLE(" \u2502 ") + chalk.white(`Found ${criticalHigh} critical/high issues. AI finds`) + " ".repeat(Math.max(0, 46 - `Found ${criticalHigh} critical/high issues. AI finds`.length)) + DIM_PURPLE("\u2502")
6980
+ );
6981
+ console.log(
6982
+ DIM_PURPLE(" \u2502 ") + chalk.dim("auth logic flaws, business logic bugs & more.") + " ".repeat(3) + DIM_PURPLE("\u2502")
6983
+ );
6984
+ } else {
6985
+ console.log(
6986
+ DIM_PURPLE(" \u2502 ") + chalk.dim("Surface scan clean. AI finds auth logic,") + " ".repeat(8) + DIM_PURPLE("\u2502")
6987
+ );
6988
+ console.log(
6989
+ DIM_PURPLE(" \u2502 ") + chalk.dim("RLS policies & business logic bugs.") + " ".repeat(13) + DIM_PURPLE("\u2502")
6990
+ );
6991
+ }
6992
+ console.log(DIM_PURPLE(" \u2502 \u2502"));
6993
+ console.log(
6994
+ DIM_PURPLE(" \u2502 ") + chalk.white("\u2192 Run ") + PURPLE_BOLD("shipsafe login") + chalk.white(" to get started") + " ".repeat(13) + DIM_PURPLE("\u2502")
6995
+ );
6996
+ console.log(DIM_PURPLE(" \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"));
6997
+ }
6998
+
6999
+ // src/lib/output.ts
7000
+ var SEVERITY_BADGE = {
7001
+ critical: chalk2.bgRed.white.bold(" CRITICAL "),
7002
+ high: chalk2.bgYellow.black.bold(" HIGH "),
7003
+ medium: chalk2.bgHex("#ca8a04").black.bold(" MEDIUM "),
7004
+ low: chalk2.bgBlue.white.bold(" LOW "),
7005
+ info: chalk2.bgGray.white(" INFO ")
6913
7006
  };
6914
7007
  var SEVERITY_COLOR = {
6915
- critical: chalk.red,
6916
- high: chalk.yellow,
6917
- medium: chalk.hex("#ca8a04"),
6918
- low: chalk.blue,
6919
- info: chalk.gray
7008
+ critical: chalk2.red,
7009
+ high: chalk2.yellow,
7010
+ medium: chalk2.hex("#ca8a04"),
7011
+ low: chalk2.blue,
7012
+ info: chalk2.gray
6920
7013
  };
6921
- function formatFinding(finding, index) {
7014
+ function formatFinding(finding) {
6922
7015
  const lines = [];
6923
7016
  const color = SEVERITY_COLOR[finding.severity];
6924
7017
  lines.push(
6925
- `
6926
- ${SEVERITY_LABEL[finding.severity]} ${chalk.bold(finding.title)}`
7018
+ `${SEVERITY_BADGE[finding.severity]} ${chalk2.bold.white(finding.title)}`
6927
7019
  );
6928
7020
  lines.push(
6929
- chalk.gray(` ${finding.file}:${finding.line}`) + (finding.cwe ? chalk.gray(` (${finding.cwe})`) : "")
7021
+ chalk2.dim(" \u{1F4C4} ") + chalk2.cyan(finding.file) + chalk2.dim(`:${finding.line}`) + (finding.cwe ? chalk2.dim(` \xB7 ${finding.cwe}`) : "")
6930
7022
  );
6931
- lines.push(` ${finding.description}`);
7023
+ lines.push(` ${chalk2.dim(finding.description)}`);
6932
7024
  if (finding.snippet) {
6933
7025
  lines.push("");
6934
- for (const snippetLine of finding.snippet.split("\n")) {
7026
+ const snippetLines = finding.snippet.split("\n");
7027
+ for (const snippetLine of snippetLines) {
6935
7028
  if (snippetLine.startsWith(">")) {
6936
- lines.push(` ${color(snippetLine)}`);
7029
+ lines.push(` ${color.bold(snippetLine)}`);
6937
7030
  } else {
6938
- lines.push(` ${chalk.gray(snippetLine)}`);
7031
+ lines.push(` ${chalk2.gray(snippetLine)}`);
6939
7032
  }
6940
7033
  }
6941
7034
  }
6942
7035
  lines.push("");
6943
- lines.push(chalk.green(` Fix: ${finding.fix.description}`));
7036
+ lines.push(chalk2.green(` \u{1F4A1} Fix: ${finding.fix.description}`));
6944
7037
  if (finding.fix.suggestion) {
6945
- lines.push(chalk.green.dim(` Suggestion: ${finding.fix.suggestion}`));
7038
+ lines.push(chalk2.green.dim(` ${finding.fix.suggestion}`));
6946
7039
  }
6947
7040
  return lines.join("\n");
6948
7041
  }
6949
7042
  function formatTableOutput(result, options = {}) {
6950
7043
  const lines = [];
6951
- lines.push("");
6952
- lines.push(chalk.bold.cyan(" ShipSafe Security Scan Results"));
6953
- lines.push(chalk.gray(" " + "\u2500".repeat(50)));
6954
- lines.push("");
6955
7044
  const { summary } = result;
6956
- lines.push(
6957
- ` ${chalk.bold("Files scanned:")} ${summary.filesScanned} | ${chalk.bold("Lines:")} ${summary.linesScanned.toLocaleString()} | ${chalk.bold("Duration:")} ${result.durationMs}ms`
6958
- );
6959
- lines.push("");
7045
+ printScanSummaryBox({
7046
+ files: summary.filesScanned,
7047
+ lines: summary.linesScanned,
7048
+ duration: result.durationMs,
7049
+ findings: summary.total,
7050
+ critical: summary.bySeverity.critical,
7051
+ high: summary.bySeverity.high,
7052
+ medium: summary.bySeverity.medium,
7053
+ low: summary.bySeverity.low
7054
+ });
6960
7055
  if (summary.total === 0) {
6961
- lines.push(chalk.green.bold(" No security issues found!"));
6962
7056
  if (!options.isLoggedIn) {
6963
- lines.push("");
6964
7057
  lines.push(
6965
- chalk.gray(
6966
- ` No issues in surface scan. Run ${chalk.bold.white("shipsafe login")} to unlock deep AI analysis.`
7058
+ chalk2.dim(
7059
+ ` Run ${chalk2.bold.white("shipsafe login")} to unlock deep AI analysis.`
6967
7060
  )
6968
7061
  );
7062
+ lines.push("");
6969
7063
  }
6970
- lines.push("");
6971
7064
  return lines.join("\n");
6972
7065
  }
6973
- const severityLine = [
6974
- summary.bySeverity.critical > 0 ? chalk.red.bold(`${summary.bySeverity.critical} critical`) : null,
6975
- summary.bySeverity.high > 0 ? chalk.yellow.bold(`${summary.bySeverity.high} high`) : null,
6976
- summary.bySeverity.medium > 0 ? chalk.hex("#ca8a04").bold(`${summary.bySeverity.medium} medium`) : null,
6977
- summary.bySeverity.low > 0 ? chalk.blue(`${summary.bySeverity.low} low`) : null,
6978
- summary.bySeverity.info > 0 ? chalk.gray(`${summary.bySeverity.info} info`) : null
6979
- ].filter(Boolean).join(" | ");
6980
- lines.push(` ${chalk.bold("Findings:")} ${summary.total} (${severityLine})`);
6981
7066
  const sorted = [...result.findings].sort(
6982
7067
  (a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity]
6983
7068
  );
6984
- for (let i = 0; i < sorted.length; i++) {
6985
- lines.push(formatFinding(sorted[i], i));
7069
+ let currentSeverity = null;
7070
+ let countInSeverity = 0;
7071
+ for (const finding of sorted) {
7072
+ if (finding.severity !== currentSeverity) {
7073
+ if (currentSeverity !== null) {
7074
+ lines.push("");
7075
+ }
7076
+ currentSeverity = finding.severity;
7077
+ countInSeverity = sorted.filter(
7078
+ (f) => f.severity === currentSeverity
7079
+ ).length;
7080
+ lines.push(
7081
+ chalk2.dim(" \u2500\u2500\u2500\u2500 ") + SEVERITY_COLOR[currentSeverity].bold(
7082
+ `${currentSeverity.toUpperCase()} (${countInSeverity})`
7083
+ ) + chalk2.dim(" " + "\u2500".repeat(35))
7084
+ );
7085
+ lines.push("");
7086
+ }
7087
+ lines.push(formatFinding(finding));
7088
+ lines.push("");
6986
7089
  }
6987
7090
  if (options.diff) {
6988
7091
  const { newFindings, resolvedFindings } = options.diff;
6989
7092
  if (newFindings.length > 0 || resolvedFindings.length > 0) {
6990
- lines.push("");
6991
- lines.push(chalk.bold(" Changes since last scan:"));
7093
+ printDivider();
7094
+ lines.push(chalk2.bold.white(" \u{1F4CA} Changes since last scan:"));
6992
7095
  if (newFindings.length > 0) {
6993
7096
  lines.push(
6994
- chalk.red(` \u2B06 ${newFindings.length} new issue${newFindings.length === 1 ? "" : "s"} introduced`)
7097
+ chalk2.red(
7098
+ ` \u2191 ${newFindings.length} new issue${newFindings.length === 1 ? "" : "s"} introduced`
7099
+ )
6995
7100
  );
6996
7101
  }
6997
7102
  if (resolvedFindings.length > 0) {
6998
7103
  lines.push(
6999
- chalk.green(` \u2B07 ${resolvedFindings.length} issue${resolvedFindings.length === 1 ? "" : "s"} resolved`)
7104
+ chalk2.green(
7105
+ ` \u2193 ${resolvedFindings.length} issue${resolvedFindings.length === 1 ? "" : "s"} resolved`
7106
+ )
7000
7107
  );
7001
7108
  }
7002
7109
  }
7003
7110
  }
7004
7111
  if (options.suppressedCount && options.suppressedCount > 0) {
7005
7112
  lines.push(
7006
- chalk.gray(`
7007
- ${options.suppressedCount} finding${options.suppressedCount === 1 ? "" : "s"} suppressed via .shipsafeignore`)
7113
+ chalk2.dim(
7114
+ `
7115
+ ${options.suppressedCount} finding${options.suppressedCount === 1 ? "" : "s"} suppressed via .shipsafeignore`
7116
+ )
7008
7117
  );
7009
7118
  }
7010
- lines.push("");
7011
- lines.push(chalk.gray(" " + "\u2500".repeat(50)));
7012
7119
  if (!options.isLoggedIn && summary.total > 0) {
7013
7120
  const criticalHigh = summary.bySeverity.critical + summary.bySeverity.high;
7014
- lines.push("");
7015
- if (criticalHigh > 0) {
7016
- lines.push(
7017
- chalk.yellow(
7018
- ` Found ${criticalHigh} critical/high issue${criticalHigh === 1 ? "" : "s"}. Deep AI analysis can find auth logic flaws,`
7019
- )
7020
- );
7021
- lines.push(
7022
- chalk.yellow(
7023
- ` business logic bugs, and more \u2014 run ${chalk.bold("shipsafe login")} to unlock.`
7024
- )
7025
- );
7026
- } else {
7027
- lines.push(
7028
- chalk.gray(
7029
- ` Surface scan clean. Run ${chalk.bold.white("shipsafe login")} for deep AI analysis`
7030
- )
7031
- );
7032
- lines.push(
7033
- chalk.gray(
7034
- ` covering auth logic, RLS policies, and business logic review.`
7035
- )
7036
- );
7037
- }
7038
- } else if (!options.isLoggedIn && summary.total === 0) {
7039
- lines.push(
7040
- chalk.gray(
7041
- ` No issues in surface scan. Run ${chalk.bold.white("shipsafe login")} to unlock deep AI analysis.`
7042
- )
7043
- );
7121
+ printUpsellBox(criticalHigh);
7044
7122
  }
7045
7123
  lines.push("");
7046
7124
  return lines.join("\n");
@@ -7089,7 +7167,7 @@ function formatSarifOutput(result) {
7089
7167
  }
7090
7168
 
7091
7169
  // src/commands/login.ts
7092
- import chalk2 from "chalk";
7170
+ import chalk3 from "chalk";
7093
7171
  import ora from "ora";
7094
7172
  import { readFileSync as readFileSync2, writeFileSync, mkdirSync, existsSync as existsSync2, unlinkSync } from "fs";
7095
7173
  import { homedir } from "os";
@@ -7101,7 +7179,7 @@ function getStoredToken() {
7101
7179
  if (!existsSync2(TOKEN_FILE)) return null;
7102
7180
  const data = JSON.parse(readFileSync2(TOKEN_FILE, "utf-8"));
7103
7181
  if (data.expiresAt < Date.now()) {
7104
- console.log(chalk2.yellow("Session expired. Please login again."));
7182
+ console.log(chalk3.yellow("Session expired. Please login again."));
7105
7183
  return null;
7106
7184
  }
7107
7185
  return data;
@@ -7128,7 +7206,7 @@ async function fetchProfile(apiUrl, rawToken) {
7128
7206
  }
7129
7207
  async function loginCommand(options) {
7130
7208
  const apiUrl = options.apiUrl || "https://ship-safe.co";
7131
- console.log(chalk2.bold("\nShipSafe CLI Login\n"));
7209
+ console.log(chalk3.bold("\nShipSafe CLI Login\n"));
7132
7210
  const spinner = ora("Requesting device code...").start();
7133
7211
  try {
7134
7212
  const res = await fetch(`${apiUrl}/api/cli/device-code`, {
@@ -7138,7 +7216,7 @@ async function loginCommand(options) {
7138
7216
  if (!res.ok) {
7139
7217
  spinner.fail("Failed to request device code");
7140
7218
  console.log(
7141
- chalk2.red(
7219
+ chalk3.red(
7142
7220
  "Could not connect to ShipSafe. Make sure the server is running."
7143
7221
  )
7144
7222
  );
@@ -7146,10 +7224,10 @@ async function loginCommand(options) {
7146
7224
  }
7147
7225
  const { deviceCode, userCode, verificationUrl, expiresIn } = await res.json();
7148
7226
  spinner.stop();
7149
- console.log(chalk2.bold("To login, open this URL in your browser:\n"));
7150
- console.log(chalk2.cyan(` ${verificationUrl}
7227
+ console.log(chalk3.bold("To login, open this URL in your browser:\n"));
7228
+ console.log(chalk3.cyan(` ${verificationUrl}
7151
7229
  `));
7152
- console.log(`Enter this code: ${chalk2.bold.green(userCode)}
7230
+ console.log(`Enter this code: ${chalk3.bold.green(userCode)}
7153
7231
  `);
7154
7232
  const pollSpinner = ora("Waiting for authorization...").start();
7155
7233
  const startTime = Date.now();
@@ -7180,17 +7258,17 @@ async function loginCommand(options) {
7180
7258
  }
7181
7259
  storeToken(tokenData);
7182
7260
  pollSpinner.succeed(
7183
- chalk2.green(`Logged in as ${chalk2.bold(pollData.email)}`)
7261
+ chalk3.green(`Logged in as ${chalk3.bold(pollData.email)}`)
7184
7262
  );
7185
7263
  if (profile?.cliTier) {
7186
7264
  console.log(
7187
- chalk2.cyan(
7265
+ chalk3.cyan(
7188
7266
  ` CLI plan: ${profile.cliTier} (${profile.aiQuota.remaining} AI scans remaining)`
7189
7267
  )
7190
7268
  );
7191
7269
  } else {
7192
7270
  console.log(
7193
- chalk2.dim(
7271
+ chalk3.dim(
7194
7272
  " No CLI plan. Upgrade at ship-safe.co/pricing for AI-powered scanning."
7195
7273
  )
7196
7274
  );
@@ -7199,7 +7277,7 @@ async function loginCommand(options) {
7199
7277
  }
7200
7278
  if (pollData.status === "expired") {
7201
7279
  pollSpinner.fail(
7202
- chalk2.yellow(
7280
+ chalk3.yellow(
7203
7281
  "Your login link expired. Run `shipsafe login` again."
7204
7282
  )
7205
7283
  );
@@ -7209,7 +7287,7 @@ async function loginCommand(options) {
7209
7287
  networkErrors++;
7210
7288
  if (networkErrors >= 3) {
7211
7289
  pollSpinner.fail(
7212
- chalk2.red(
7290
+ chalk3.red(
7213
7291
  "Lost connection to ShipSafe. Check your internet and run `shipsafe login` again."
7214
7292
  )
7215
7293
  );
@@ -7219,7 +7297,7 @@ async function loginCommand(options) {
7219
7297
  }
7220
7298
  }
7221
7299
  pollSpinner.fail(
7222
- chalk2.yellow(
7300
+ chalk3.yellow(
7223
7301
  "Your login link expired. Run `shipsafe login` again."
7224
7302
  )
7225
7303
  );
@@ -7227,13 +7305,13 @@ async function loginCommand(options) {
7227
7305
  spinner.fail("Could not connect to ShipSafe");
7228
7306
  if (error instanceof Error && (error.message.includes("fetch") || error.message.includes("ECONNREFUSED"))) {
7229
7307
  console.log(
7230
- chalk2.red(
7308
+ chalk3.red(
7231
7309
  "Make sure you have an internet connection and try again."
7232
7310
  )
7233
7311
  );
7234
7312
  } else {
7235
7313
  console.log(
7236
- chalk2.red(
7314
+ chalk3.red(
7237
7315
  error instanceof Error ? error.message : "Unknown error \u2014 try again."
7238
7316
  )
7239
7317
  );
@@ -7244,18 +7322,18 @@ async function logoutCommand() {
7244
7322
  try {
7245
7323
  if (existsSync2(TOKEN_FILE)) {
7246
7324
  unlinkSync(TOKEN_FILE);
7247
- console.log(chalk2.green("Logged out successfully."));
7325
+ console.log(chalk3.green("Logged out successfully."));
7248
7326
  } else {
7249
- console.log(chalk2.yellow("Not currently logged in."));
7327
+ console.log(chalk3.yellow("Not currently logged in."));
7250
7328
  }
7251
7329
  } catch {
7252
- console.log(chalk2.red("Failed to logout."));
7330
+ console.log(chalk3.red("Failed to logout."));
7253
7331
  }
7254
7332
  }
7255
7333
  async function whoamiCommand(options) {
7256
7334
  const token = getStoredToken();
7257
7335
  if (!token) {
7258
- console.log(chalk2.yellow("Not logged in. Run `shipsafe login` to login."));
7336
+ console.log(chalk3.yellow("Not logged in. Run `shipsafe login` to login."));
7259
7337
  return;
7260
7338
  }
7261
7339
  const apiUrl = options.apiUrl || "https://ship-safe.co";
@@ -7269,26 +7347,26 @@ async function whoamiCommand(options) {
7269
7347
  aiQuota: profile.aiQuota
7270
7348
  });
7271
7349
  spinner.stop();
7272
- console.log(chalk2.bold("\nShipSafe Account\n"));
7273
- console.log(` Email: ${chalk2.cyan(profile.email)}`);
7274
- console.log(` Web tier: ${chalk2.cyan(profile.tier)}`);
7350
+ console.log(chalk3.bold("\nShipSafe Account\n"));
7351
+ console.log(` Email: ${chalk3.cyan(profile.email)}`);
7352
+ console.log(` Web tier: ${chalk3.cyan(profile.tier)}`);
7275
7353
  if (profile.cliTier) {
7276
- console.log(` CLI plan: ${chalk2.green(profile.cliTier)}`);
7354
+ console.log(` CLI plan: ${chalk3.green(profile.cliTier)}`);
7277
7355
  console.log(
7278
- ` AI scans: ${chalk2.cyan(`${profile.aiQuota.used}/${profile.aiQuota.limit}`)} used this month (${chalk2.green(`${profile.aiQuota.remaining}`)} remaining)`
7356
+ ` AI scans: ${chalk3.cyan(`${profile.aiQuota.used}/${profile.aiQuota.limit}`)} used this month (${chalk3.green(`${profile.aiQuota.remaining}`)} remaining)`
7279
7357
  );
7280
7358
  } else {
7281
- console.log(` CLI plan: ${chalk2.dim("none")}`);
7359
+ console.log(` CLI plan: ${chalk3.dim("none")}`);
7282
7360
  console.log(
7283
- chalk2.dim(" Upgrade at ship-safe.co/pricing for AI-powered scanning.")
7361
+ chalk3.dim(" Upgrade at ship-safe.co/pricing for AI-powered scanning.")
7284
7362
  );
7285
7363
  }
7286
7364
  console.log();
7287
7365
  } else {
7288
7366
  spinner.stop();
7289
- console.log(chalk2.green(`Logged in as ${chalk2.bold(token.email)}`));
7367
+ console.log(chalk3.green(`Logged in as ${chalk3.bold(token.email)}`));
7290
7368
  if (token.cliTier) {
7291
- console.log(chalk2.cyan(` CLI plan: ${token.cliTier}`));
7369
+ console.log(chalk3.cyan(` CLI plan: ${token.cliTier}`));
7292
7370
  }
7293
7371
  }
7294
7372
  }
@@ -7296,7 +7374,7 @@ async function whoamiCommand(options) {
7296
7374
  // src/commands/ignore.ts
7297
7375
  import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, appendFileSync } from "fs";
7298
7376
  import { resolve, join as join2 } from "path";
7299
- import chalk3 from "chalk";
7377
+ import chalk4 from "chalk";
7300
7378
  var IGNORE_FILE = ".shipsafeignore";
7301
7379
  function getIgnorePath(dir) {
7302
7380
  return resolve(dir || ".", IGNORE_FILE);
@@ -7320,7 +7398,7 @@ function ignoreCommand(ruleId, options) {
7320
7398
  if (existsSync3(ignorePath)) {
7321
7399
  const existing = loadIgnoredRules(".");
7322
7400
  if (existing.has(ruleId)) {
7323
- console.log(chalk3.yellow(`Rule "${ruleId}" is already in ${IGNORE_FILE}`));
7401
+ console.log(chalk4.yellow(`Rule "${ruleId}" is already in ${IGNORE_FILE}`));
7324
7402
  return;
7325
7403
  }
7326
7404
  }
@@ -7341,18 +7419,18 @@ ${line}
7341
7419
  `
7342
7420
  );
7343
7421
  }
7344
- console.log(chalk3.green(`Added "${ruleId}" to ${IGNORE_FILE}`));
7422
+ console.log(chalk4.green(`Added "${ruleId}" to ${IGNORE_FILE}`));
7345
7423
  if (options.reason) {
7346
- console.log(chalk3.gray(` Reason: ${options.reason}`));
7424
+ console.log(chalk4.gray(` Reason: ${options.reason}`));
7347
7425
  }
7348
7426
  console.log(
7349
- chalk3.gray(` This finding will be suppressed in future scans.`)
7427
+ chalk4.gray(` This finding will be suppressed in future scans.`)
7350
7428
  );
7351
7429
  }
7352
7430
  function unignoreCommand(ruleId) {
7353
7431
  const ignorePath = getIgnorePath();
7354
7432
  if (!existsSync3(ignorePath)) {
7355
- console.log(chalk3.yellow(`No ${IGNORE_FILE} found.`));
7433
+ console.log(chalk4.yellow(`No ${IGNORE_FILE} found.`));
7356
7434
  return;
7357
7435
  }
7358
7436
  const content = readFileSync3(ignorePath, "utf-8");
@@ -7364,11 +7442,11 @@ function unignoreCommand(ruleId) {
7364
7442
  return lineRuleId !== ruleId;
7365
7443
  });
7366
7444
  if (filtered.length === lines.length) {
7367
- console.log(chalk3.yellow(`Rule "${ruleId}" is not in ${IGNORE_FILE}`));
7445
+ console.log(chalk4.yellow(`Rule "${ruleId}" is not in ${IGNORE_FILE}`));
7368
7446
  return;
7369
7447
  }
7370
7448
  writeFileSync2(ignorePath, filtered.join("\n"));
7371
- console.log(chalk3.green(`Removed "${ruleId}" from ${IGNORE_FILE}`));
7449
+ console.log(chalk4.green(`Removed "${ruleId}" from ${IGNORE_FILE}`));
7372
7450
  }
7373
7451
 
7374
7452
  // src/lib/scan-history.ts
@@ -7453,28 +7531,81 @@ function loadConfig(dir) {
7453
7531
  async function scanCommand(targetPath, options) {
7454
7532
  const resolvedPath = resolve2(targetPath);
7455
7533
  if (!existsSync5(resolvedPath)) {
7456
- console.error(chalk4.red(`Error: Path "${targetPath}" does not exist.`));
7534
+ printError(`Path "${targetPath}" does not exist.`);
7457
7535
  process.exit(1);
7458
7536
  }
7537
+ if (options.output === "table") {
7538
+ printBanner();
7539
+ }
7540
+ let tokenData = getStoredToken();
7541
+ if (!tokenData && !options.ci && options.output === "table") {
7542
+ const choice = await select({
7543
+ message: chalk5.white("How would you like to scan?"),
7544
+ choices: [
7545
+ {
7546
+ name: `${chalk5.green("\u25B8")} ${chalk5.bold("Free scan")} ${chalk5.dim("\u2014 rule-based analysis, no account needed")}`,
7547
+ value: "free"
7548
+ },
7549
+ {
7550
+ name: `${chalk5.hex("#A855F7")("\u25B8")} ${chalk5.bold("Login first")} ${chalk5.dim("\u2014 unlock AI-powered deep analysis")}`,
7551
+ value: "login"
7552
+ }
7553
+ ],
7554
+ theme: {
7555
+ prefix: { idle: chalk5.hex("#A855F7")(" \u{1F6E1}\uFE0F"), done: chalk5.green(" \u2713") },
7556
+ style: {
7557
+ highlight: (text) => chalk5.hex("#A855F7")(text)
7558
+ }
7559
+ }
7560
+ });
7561
+ if (choice === "login") {
7562
+ await loginCommand({ apiUrl: options.apiUrl });
7563
+ tokenData = getStoredToken();
7564
+ if (!tokenData) {
7565
+ printInfo("Login skipped. Running free scan...\n");
7566
+ } else {
7567
+ console.log("");
7568
+ }
7569
+ } else {
7570
+ console.log("");
7571
+ }
7572
+ }
7459
7573
  const config = loadConfig(resolvedPath);
7460
7574
  const severity = options.severity || config.severity || "low";
7461
- const spinner = ora2("Collecting files...").start();
7575
+ const spinner = ora2({
7576
+ text: chalk5.dim("Collecting files..."),
7577
+ color: "yellow",
7578
+ spinner: "dots12"
7579
+ }).start();
7462
7580
  const files = collectFiles(resolvedPath, { exclude: config.exclude });
7463
7581
  if (files.length === 0) {
7464
- spinner.fail("No supported files found to scan.");
7582
+ spinner.fail(chalk5.red("No supported files found to scan."));
7465
7583
  process.exit(1);
7466
7584
  }
7467
- spinner.text = `Scanning ${files.length} files...`;
7585
+ const totalLines = files.reduce(
7586
+ (sum, f) => sum + f.content.split("\n").length,
7587
+ 0
7588
+ );
7589
+ spinner.text = chalk5.dim(
7590
+ `Scanning ${chalk5.white.bold(String(files.length))} files (${chalk5.white.bold(totalLines.toLocaleString())} lines)...`
7591
+ );
7468
7592
  const result = await scan(
7469
7593
  { files, tier: "free" },
7470
7594
  (progress) => {
7471
- spinner.text = `${progress.stage} (${progress.findingsCount} findings)`;
7595
+ spinner.text = chalk5.dim(
7596
+ `${progress.stage} ${chalk5.white(`(${progress.findingsCount} findings)`)}`
7597
+ );
7472
7598
  }
7473
7599
  );
7474
- spinner.stop();
7475
- const tokenData = getStoredToken();
7600
+ spinner.succeed(
7601
+ chalk5.green(`Scan complete \u2014 ${result.findings.length} findings in ${result.durationMs}ms`)
7602
+ );
7476
7603
  if (tokenData?.cliTier) {
7477
- const aiSpinner = ora2("Running AI-powered deep analysis...").start();
7604
+ const aiSpinner = ora2({
7605
+ text: chalk5.dim("Running AI-powered deep analysis..."),
7606
+ color: "yellow",
7607
+ spinner: "dots12"
7608
+ }).start();
7478
7609
  try {
7479
7610
  const aiRes = await fetch(`${options.apiUrl}/api/cli/ai-scan`, {
7480
7611
  method: "POST",
@@ -7515,31 +7646,29 @@ async function scanCommand(targetPath, options) {
7515
7646
  info: result.findings.filter((f) => f.severity === "info").length
7516
7647
  };
7517
7648
  aiSpinner.succeed(
7518
- chalk4.green(
7519
- `AI analysis complete: ${aiFindings.length} additional findings`
7649
+ chalk5.green(
7650
+ `AI analysis: ${aiFindings.length} additional findings`
7520
7651
  )
7521
7652
  );
7522
- console.log(
7523
- chalk4.dim(
7524
- ` AI scans: ${aiData.quota.used}/${aiData.quota.limit} used this month (${aiData.quota.remaining} remaining)`
7525
- )
7653
+ printInfo(
7654
+ `AI scans: ${aiData.quota.used}/${aiData.quota.limit} used this month (${aiData.quota.remaining} remaining)`
7526
7655
  );
7527
7656
  } else if (aiRes.status === 429) {
7528
7657
  aiSpinner.warn(
7529
- chalk4.yellow("AI scan quota reached. Showing rule-based results only.")
7658
+ chalk5.yellow("AI scan quota reached. Showing rule-based results only.")
7530
7659
  );
7531
7660
  } else if (aiRes.status === 401) {
7532
7661
  aiSpinner.warn(
7533
- chalk4.yellow("Session expired. Run `shipsafe login` to re-authenticate.")
7662
+ chalk5.yellow("Session expired. Run `shipsafe login` to re-authenticate.")
7534
7663
  );
7535
7664
  } else {
7536
7665
  aiSpinner.warn(
7537
- chalk4.yellow("AI analysis unavailable. Showing rule-based results only.")
7666
+ chalk5.yellow("AI analysis unavailable. Showing rule-based results only.")
7538
7667
  );
7539
7668
  }
7540
7669
  } catch {
7541
7670
  aiSpinner.warn(
7542
- chalk4.yellow("Could not reach AI scanning service. Showing rule-based results only.")
7671
+ chalk5.yellow("Could not reach AI scanning service. Showing rule-based results only.")
7543
7672
  );
7544
7673
  }
7545
7674
  }
@@ -7587,7 +7716,11 @@ async function scanCommand(targetPath, options) {
7587
7716
  }
7588
7717
  const token = getStoredToken();
7589
7718
  if (token && options.upload) {
7590
- const uploadSpinner = ora2("Syncing results to dashboard...").start();
7719
+ const uploadSpinner = ora2({
7720
+ text: chalk5.dim("Syncing results to dashboard..."),
7721
+ color: "yellow",
7722
+ spinner: "dots12"
7723
+ }).start();
7591
7724
  try {
7592
7725
  const apiFindings = result.findings.map((f) => ({
7593
7726
  ruleId: f.ruleId,
@@ -7620,14 +7753,10 @@ async function scanCommand(targetPath, options) {
7620
7753
  if (res.ok) {
7621
7754
  const data = await res.json();
7622
7755
  uploadSpinner.succeed(
7623
- chalk4.green(`Results synced: ${data.dashboardUrl || "view in dashboard"}`)
7756
+ chalk5.green(`Results synced: ${data.dashboardUrl || "view in dashboard"}`)
7624
7757
  );
7625
7758
  if (data.deepScanStatus === "running") {
7626
- console.log(
7627
- chalk4.cyan(
7628
- " Deep AI analysis running \u2014 check dashboard for updates."
7629
- )
7630
- );
7759
+ printInfo("Deep AI analysis running \u2014 check dashboard for updates.");
7631
7760
  }
7632
7761
  } else if (res.status === 401) {
7633
7762
  uploadSpinner.warn(
@@ -7643,7 +7772,7 @@ async function scanCommand(targetPath, options) {
7643
7772
  }
7644
7773
  } else if (options.upload) {
7645
7774
  console.log(
7646
- chalk4.yellow(
7775
+ chalk5.yellow(
7647
7776
  "\nNot logged in. Run `shipsafe login` first to sync results."
7648
7777
  )
7649
7778
  );
@@ -7661,7 +7790,7 @@ async function scanCommand(targetPath, options) {
7661
7790
  // src/commands/init.ts
7662
7791
  import { writeFileSync as writeFileSync4, existsSync as existsSync6 } from "fs";
7663
7792
  import { resolve as resolve3 } from "path";
7664
- import chalk5 from "chalk";
7793
+ import chalk6 from "chalk";
7665
7794
  var DEFAULT_CONFIG = `# ShipSafe Configuration
7666
7795
  # https://ship-safe.co/docs/config
7667
7796
 
@@ -7692,11 +7821,11 @@ severity: low
7692
7821
  function initCommand() {
7693
7822
  const configPath = resolve3(".shipsafe.yml");
7694
7823
  if (existsSync6(configPath)) {
7695
- console.log(chalk5.yellow("A .shipsafe.yml already exists in this directory."));
7824
+ console.log(chalk6.yellow("A .shipsafe.yml already exists in this directory."));
7696
7825
  return;
7697
7826
  }
7698
7827
  writeFileSync4(configPath, DEFAULT_CONFIG, "utf-8");
7699
- console.log(chalk5.green("Created .shipsafe.yml configuration file."));
7828
+ console.log(chalk6.green("Created .shipsafe.yml configuration file."));
7700
7829
  }
7701
7830
 
7702
7831
  // src/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ship-safe/cli",
3
- "version": "1.0.2",
3
+ "version": "1.1.1",
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",