@oculum/cli 1.0.9 → 1.0.11

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 +494 -139
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -40915,8 +40915,8 @@ var {
40915
40915
  } = import_index.default;
40916
40916
 
40917
40917
  // src/commands/scan.ts
40918
- var import_path3 = require("path");
40919
- var import_fs4 = require("fs");
40918
+ var import_path4 = require("path");
40919
+ var import_fs5 = require("fs");
40920
40920
  init_esm7();
40921
40921
 
40922
40922
  // node_modules/chalk/source/vendor/ansi-styles/index.js
@@ -43313,6 +43313,16 @@ function getApiBaseUrl() {
43313
43313
  function setAuthCredentials(apiKey, email, tier) {
43314
43314
  updateConfig({ apiKey, email, tier });
43315
43315
  }
43316
+ function syncAuthFromVerification(verifyResponse) {
43317
+ if (!verifyResponse.valid) return;
43318
+ const config = getConfig();
43319
+ if (!config.apiKey) return;
43320
+ const email = verifyResponse.email || config.email;
43321
+ const tier = verifyResponse.tier || config.tier || "free";
43322
+ if (tier !== config.tier || email !== config.email) {
43323
+ setAuthCredentials(config.apiKey, email, tier);
43324
+ }
43325
+ }
43316
43326
 
43317
43327
  // src/utils/api.ts
43318
43328
  var APIError = class extends Error {
@@ -43872,8 +43882,83 @@ function validateConfig(config) {
43872
43882
  validated.watch.clear = config.watch.clear;
43873
43883
  }
43874
43884
  }
43885
+ if (config.profiles && typeof config.profiles === "object") {
43886
+ validated.profiles = config.profiles;
43887
+ }
43888
+ if (typeof config.defaultProfile === "string") {
43889
+ validated.defaultProfile = config.defaultProfile;
43890
+ }
43875
43891
  return validated;
43876
43892
  }
43893
+ function getProfileConfig(projectConfig, profileName) {
43894
+ if (!projectConfig) return null;
43895
+ const name = profileName || projectConfig.defaultProfile;
43896
+ if (!name) return null;
43897
+ const profile = projectConfig.profiles?.[name];
43898
+ if (!profile) {
43899
+ if (profileName) {
43900
+ console.warn(source_default.yellow(`Warning: Profile "${profileName}" not found in config`));
43901
+ }
43902
+ return null;
43903
+ }
43904
+ return profile;
43905
+ }
43906
+
43907
+ // src/utils/history.ts
43908
+ var import_fs4 = require("fs");
43909
+ var import_path3 = require("path");
43910
+ var import_os2 = require("os");
43911
+ var import_crypto = require("crypto");
43912
+ var CONFIG_DIR2 = (0, import_path3.join)((0, import_os2.homedir)(), ".oculum");
43913
+ var HISTORY_FILE = (0, import_path3.join)(CONFIG_DIR2, "history.json");
43914
+ var MAX_ENTRIES = 25;
43915
+ function ensureConfigDir2() {
43916
+ if (!(0, import_fs4.existsSync)(CONFIG_DIR2)) {
43917
+ (0, import_fs4.mkdirSync)(CONFIG_DIR2, { recursive: true });
43918
+ }
43919
+ }
43920
+ function readHistoryFile() {
43921
+ try {
43922
+ if (!(0, import_fs4.existsSync)(HISTORY_FILE)) return [];
43923
+ const content = (0, import_fs4.readFileSync)(HISTORY_FILE, "utf-8");
43924
+ const parsed = JSON.parse(content);
43925
+ if (!Array.isArray(parsed)) return [];
43926
+ return parsed;
43927
+ } catch {
43928
+ return [];
43929
+ }
43930
+ }
43931
+ function writeHistoryFile(entries) {
43932
+ ensureConfigDir2();
43933
+ (0, import_fs4.writeFileSync)(HISTORY_FILE, JSON.stringify(entries, null, 2));
43934
+ }
43935
+ function listScanHistory() {
43936
+ return readHistoryFile();
43937
+ }
43938
+ function getScanHistoryEntry(id) {
43939
+ return readHistoryFile().find((e2) => e2.id === id);
43940
+ }
43941
+ function addScanHistoryEntry(input) {
43942
+ const entry = {
43943
+ id: input.id ?? (0, import_crypto.randomUUID)(),
43944
+ createdAt: input.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
43945
+ targetPath: input.targetPath,
43946
+ options: input.options,
43947
+ result: input.result
43948
+ };
43949
+ const current = readHistoryFile();
43950
+ const updated = [entry, ...current].slice(0, MAX_ENTRIES);
43951
+ writeHistoryFile(updated);
43952
+ return entry;
43953
+ }
43954
+ function deleteScanHistoryEntry(id) {
43955
+ const current = readHistoryFile();
43956
+ const updated = current.filter((e2) => e2.id !== id);
43957
+ writeHistoryFile(updated);
43958
+ }
43959
+ function clearScanHistory() {
43960
+ writeHistoryFile([]);
43961
+ }
43877
43962
 
43878
43963
  // src/commands/scan.ts
43879
43964
  function getChangedFiles(absolutePath, diff) {
@@ -43890,22 +43975,22 @@ function getChangedFiles(absolutePath, diff) {
43890
43975
  args = ["diff", "--name-only", "HEAD"];
43891
43976
  }
43892
43977
  const out = (0, import_child_process.execFileSync)("git", args, { cwd: gitRoot, stdio: ["ignore", "pipe", "ignore"] }).toString();
43893
- const files = out.split("\n").map((s) => s.trim()).filter(Boolean).map((p2) => (0, import_path3.resolve)(gitRoot, p2)).filter((p2) => p2.startsWith(absolutePath));
43978
+ const files = out.split("\n").map((s) => s.trim()).filter(Boolean).map((p2) => (0, import_path4.resolve)(gitRoot, p2)).filter((p2) => p2.startsWith(absolutePath));
43894
43979
  return files;
43895
43980
  } catch {
43896
43981
  return [];
43897
43982
  }
43898
43983
  }
43899
43984
  function isScannableFile(filePath) {
43900
- const ext2 = (0, import_path3.extname)(filePath).toLowerCase();
43901
- const fileName = (0, import_path3.basename)(filePath);
43985
+ const ext2 = (0, import_path4.extname)(filePath).toLowerCase();
43986
+ const fileName = (0, import_path4.basename)(filePath);
43902
43987
  if (import_scanner.SPECIAL_FILES.includes(fileName)) {
43903
43988
  return true;
43904
43989
  }
43905
43990
  return import_scanner.SCANNABLE_EXTENSIONS.includes(ext2);
43906
43991
  }
43907
43992
  function getLanguage(filePath) {
43908
- const ext2 = (0, import_path3.extname)(filePath).toLowerCase();
43993
+ const ext2 = (0, import_path4.extname)(filePath).toLowerCase();
43909
43994
  const langMap = {
43910
43995
  ".js": "javascript",
43911
43996
  ".jsx": "javascript",
@@ -43930,15 +44015,15 @@ function getLanguage(filePath) {
43930
44015
  return langMap[ext2] || "text";
43931
44016
  }
43932
44017
  async function collectFiles(targetPath) {
43933
- const absolutePath = (0, import_path3.resolve)(targetPath);
43934
- const stats = (0, import_fs4.statSync)(absolutePath);
44018
+ const absolutePath = (0, import_path4.resolve)(targetPath);
44019
+ const stats = (0, import_fs5.statSync)(absolutePath);
43935
44020
  const files = [];
43936
44021
  if (stats.isFile()) {
43937
44022
  if (isScannableFile(absolutePath)) {
43938
- const content = (0, import_fs4.readFileSync)(absolutePath, "utf-8");
44023
+ const content = (0, import_fs5.readFileSync)(absolutePath, "utf-8");
43939
44024
  if (content.length <= import_scanner.MAX_FILE_SIZE) {
43940
44025
  files.push({
43941
- path: (0, import_path3.relative)(process.cwd(), absolutePath),
44026
+ path: (0, import_path4.relative)(process.cwd(), absolutePath),
43942
44027
  content,
43943
44028
  language: getLanguage(absolutePath),
43944
44029
  size: content.length
@@ -44007,8 +44092,8 @@ async function collectFiles(targetPath) {
44007
44092
  "**/.env.*.local",
44008
44093
  "**/**.env"
44009
44094
  ];
44010
- const gitignorePath = (0, import_path3.join)(absolutePath, ".gitignore");
44011
- const hasGitignore = (0, import_fs4.existsSync)(gitignorePath);
44095
+ const gitignorePath = (0, import_path4.join)(absolutePath, ".gitignore");
44096
+ const hasGitignore = (0, import_fs5.existsSync)(gitignorePath);
44012
44097
  const globOptions = {
44013
44098
  cwd: absolutePath,
44014
44099
  ignore: ignorePatterns,
@@ -44022,11 +44107,11 @@ async function collectFiles(targetPath) {
44022
44107
  for (const file of foundFiles) {
44023
44108
  const filePath = String(file);
44024
44109
  try {
44025
- const fileStats = (0, import_fs4.statSync)(filePath);
44110
+ const fileStats = (0, import_fs5.statSync)(filePath);
44026
44111
  if (fileStats.size > import_scanner.MAX_FILE_SIZE) continue;
44027
- const content = (0, import_fs4.readFileSync)(filePath, "utf-8");
44112
+ const content = (0, import_fs5.readFileSync)(filePath, "utf-8");
44028
44113
  files.push({
44029
- path: (0, import_path3.relative)(process.cwd(), filePath),
44114
+ path: (0, import_path4.relative)(process.cwd(), filePath),
44030
44115
  content,
44031
44116
  language: getLanguage(filePath),
44032
44117
  size: content.length
@@ -44038,8 +44123,8 @@ async function collectFiles(targetPath) {
44038
44123
  return files;
44039
44124
  }
44040
44125
  async function collectFilesForScan(targetPath, options) {
44041
- const absolutePath = (0, import_path3.resolve)(targetPath);
44042
- const stats = (0, import_fs4.statSync)(absolutePath);
44126
+ const absolutePath = (0, import_path4.resolve)(targetPath);
44127
+ const stats = (0, import_fs5.statSync)(absolutePath);
44043
44128
  if (stats.isFile()) {
44044
44129
  return collectFiles(targetPath);
44045
44130
  }
@@ -44048,13 +44133,13 @@ async function collectFilesForScan(targetPath, options) {
44048
44133
  const files = [];
44049
44134
  for (const filePath of changed) {
44050
44135
  try {
44051
- if (!(0, import_fs4.existsSync)(filePath)) continue;
44136
+ if (!(0, import_fs5.existsSync)(filePath)) continue;
44052
44137
  if (!isScannableFile(filePath)) continue;
44053
- const fileStats = (0, import_fs4.statSync)(filePath);
44138
+ const fileStats = (0, import_fs5.statSync)(filePath);
44054
44139
  if (fileStats.size > import_scanner.MAX_FILE_SIZE) continue;
44055
- const content = (0, import_fs4.readFileSync)(filePath, "utf-8");
44140
+ const content = (0, import_fs5.readFileSync)(filePath, "utf-8");
44056
44141
  files.push({
44057
- path: (0, import_path3.relative)(process.cwd(), filePath),
44142
+ path: (0, import_path4.relative)(process.cwd(), filePath),
44058
44143
  content,
44059
44144
  language: getLanguage(filePath),
44060
44145
  size: content.length
@@ -44068,7 +44153,7 @@ async function collectFilesForScan(targetPath, options) {
44068
44153
  }
44069
44154
  function createEmptyResult(targetPath) {
44070
44155
  return {
44071
- repoName: (0, import_path3.basename)((0, import_path3.resolve)(targetPath)),
44156
+ repoName: (0, import_path4.basename)((0, import_path4.resolve)(targetPath)),
44072
44157
  repoUrl: "",
44073
44158
  branch: "local",
44074
44159
  filesScanned: 0,
@@ -44192,6 +44277,13 @@ async function runScanOnce(targetPath, options) {
44192
44277
  const config = getConfig();
44193
44278
  const noColor = options.color === false;
44194
44279
  const cancellationToken = (0, import_scanner.createCancellationToken)();
44280
+ if ((options.depth === "validated" || options.depth === "deep") && isAuthenticated()) {
44281
+ try {
44282
+ const verified = await verifyApiKey(config.apiKey);
44283
+ syncAuthFromVerification(verified);
44284
+ } catch {
44285
+ }
44286
+ }
44195
44287
  if ((options.depth === "validated" || options.depth === "deep") && !isAuthenticated()) {
44196
44288
  if (!options.quiet) {
44197
44289
  console.log("");
@@ -44283,7 +44375,7 @@ async function runScanOnce(targetPath, options) {
44283
44375
  options.depth,
44284
44376
  config.apiKey,
44285
44377
  {
44286
- name: (0, import_path3.basename)((0, import_path3.resolve)(targetPath)),
44378
+ name: (0, import_path4.basename)((0, import_path4.resolve)(targetPath)),
44287
44379
  url: "",
44288
44380
  branch: "local"
44289
44381
  }
@@ -44295,7 +44387,7 @@ async function runScanOnce(targetPath, options) {
44295
44387
  result = await (0, import_scanner.runScan)(
44296
44388
  files,
44297
44389
  {
44298
- name: (0, import_path3.basename)((0, import_path3.resolve)(targetPath)),
44390
+ name: (0, import_path4.basename)((0, import_path4.resolve)(targetPath)),
44299
44391
  url: "",
44300
44392
  branch: "local"
44301
44393
  },
@@ -44358,32 +44450,33 @@ Results written to ${options.output}`));
44358
44450
  }
44359
44451
  async function runScan(targetPath, cliOptions) {
44360
44452
  const projectConfig = loadProjectConfig(targetPath, cliOptions.quiet);
44361
- let scanDepth = cliOptions.mode || cliOptions.depth || "cheap";
44453
+ const profileConfig = getProfileConfig(projectConfig, cliOptions.profile);
44454
+ let scanDepth = cliOptions.mode || cliOptions.depth;
44362
44455
  if (scanDepth === "quick") {
44363
44456
  scanDepth = "cheap";
44364
44457
  }
44365
- const baseOptions = {
44366
- depth: scanDepth,
44367
- format: cliOptions.format ?? "terminal",
44368
- failOn: cliOptions.failOn ?? "high",
44458
+ const options = {
44459
+ depth: scanDepth ?? profileConfig?.depth ?? projectConfig?.depth ?? "cheap",
44460
+ format: cliOptions.format ?? profileConfig?.format ?? projectConfig?.format ?? "terminal",
44461
+ failOn: cliOptions.failOn ?? profileConfig?.failOn ?? projectConfig?.failOn ?? "high",
44369
44462
  color: cliOptions.color,
44370
- quiet: cliOptions.quiet,
44463
+ quiet: cliOptions.quiet ?? profileConfig?.quiet ?? projectConfig?.quiet,
44371
44464
  verbose: cliOptions.verbose,
44372
44465
  incremental: cliOptions.incremental,
44373
44466
  diff: cliOptions.diff,
44374
- output: cliOptions.output
44375
- };
44376
- const options = {
44377
- ...baseOptions,
44378
- depth: baseOptions.depth ?? projectConfig?.depth ?? "cheap",
44379
- format: baseOptions.format ?? projectConfig?.format ?? "terminal",
44380
- failOn: baseOptions.failOn ?? projectConfig?.failOn ?? "high",
44381
- quiet: baseOptions.quiet ?? projectConfig?.quiet,
44382
- verbose: baseOptions.verbose,
44383
- output: baseOptions.output ?? projectConfig?.output
44467
+ output: cliOptions.output ?? profileConfig?.output ?? projectConfig?.output,
44468
+ profile: cliOptions.profile
44384
44469
  };
44385
44470
  try {
44386
- const { output, exitCode } = await runScanOnce(targetPath, options);
44471
+ const { result, output, exitCode } = await runScanOnce(targetPath, options);
44472
+ try {
44473
+ addScanHistoryEntry({
44474
+ targetPath,
44475
+ options: { depth: options.depth, format: options.format, failOn: options.failOn },
44476
+ result
44477
+ });
44478
+ } catch {
44479
+ }
44387
44480
  console.log(output);
44388
44481
  if (exitCode !== 0) {
44389
44482
  process.exit(exitCode);
@@ -44394,7 +44487,7 @@ async function runScan(targetPath, cliOptions) {
44394
44487
  process.exit(1);
44395
44488
  }
44396
44489
  }
44397
- var scanCommand = new Command("scan").description("Scan a directory or file for security vulnerabilities").argument("[path]", "path to scan", ".").option("-d, --depth <depth>", "scan depth: cheap (free), validated, deep", "cheap").option("-m, --mode <mode>", "alias for --depth (quick, validated, deep)").option("-f, --format <format>", "output format: terminal, json, sarif, markdown", "terminal").option("--fail-on <severity>", "exit with error code if findings at severity", "high").option("--no-color", "disable colored output").option("-q, --quiet", "minimal output for CI (suppress spinners and decorations)").option("-v, --verbose", "show detailed scanner logs (debug mode)").option("-o, --output <file>", "write output to file").option("--incremental", "only scan changed files (requires git)").option("--diff <ref>", "diff against branch/commit for incremental scan").addHelpText("after", `
44490
+ var scanCommand = new Command("scan").description("Scan a directory or file for security vulnerabilities").argument("[path]", "path to scan", ".").option("-d, --depth <depth>", "scan depth: cheap (free), validated, deep").option("-m, --mode <mode>", "alias for --depth (quick, validated, deep)").option("-f, --format <format>", "output format: terminal, json, sarif, markdown").option("--fail-on <severity>", "exit with error code if findings at severity").option("--no-color", "disable colored output").option("-q, --quiet", "minimal output for CI (suppress spinners and decorations)").option("-v, --verbose", "show detailed scanner logs (debug mode)").option("-o, --output <file>", "write output to file").option("-p, --profile <name>", "use named profile from oculum.config.json").option("--incremental", "only scan changed files (requires git)").option("--diff <ref>", "diff against branch/commit for incremental scan").addHelpText("after", `
44398
44491
  Scan Modes:
44399
44492
  cheap Free Fast pattern matching, runs locally
44400
44493
  Best for: Quick checks, CI/CD pipelines
@@ -44419,9 +44512,14 @@ Configuration:
44419
44512
  {
44420
44513
  "depth": "validated",
44421
44514
  "failOn": "high",
44422
- "ignore": ["**/test/**"]
44515
+ "profiles": {
44516
+ "ci": { "depth": "validated", "quiet": true, "format": "sarif" },
44517
+ "strict": { "depth": "validated", "failOn": "medium" }
44518
+ }
44423
44519
  }
44424
44520
 
44521
+ Use profiles: oculum scan . --profile ci
44522
+
44425
44523
  More Help:
44426
44524
  $ oculum help scan-modes Detailed mode comparison
44427
44525
  $ oculum help ci-setup CI/CD integration examples
@@ -44551,7 +44649,10 @@ async function status() {
44551
44649
  spinner.succeed(" Authenticated");
44552
44650
  console.log("");
44553
44651
  const email = result.email || config.email || "unknown";
44554
- const tier = result.tier || config.tier || "free";
44652
+ const tier = result.tier || "free";
44653
+ if (tier !== config.tier || email !== config.email) {
44654
+ setAuthCredentials(config.apiKey, email, tier);
44655
+ }
44555
44656
  const tierBadge = tier === "pro" ? source_default.bgBlue.white(" PRO ") : tier === "enterprise" ? source_default.bgMagenta.white(" ENTERPRISE ") : source_default.bgGray.white(" FREE ");
44556
44657
  console.log(source_default.dim(" Email: ") + source_default.white(email));
44557
44658
  console.log(source_default.dim(" Plan: ") + tierBadge);
@@ -44595,11 +44696,12 @@ var statusCommand = new Command("status").description("Show current authenticati
44595
44696
  var upgradeCommand = new Command("upgrade").description("Upgrade your subscription").action(upgrade);
44596
44697
 
44597
44698
  // src/commands/watch.ts
44598
- var import_path4 = require("path");
44599
- var import_fs7 = require("fs");
44699
+ var import_path5 = require("path");
44700
+ var import_fs8 = require("fs");
44701
+ var readline = __toESM(require("readline"));
44600
44702
 
44601
44703
  // ../../node_modules/chokidar/esm/index.js
44602
- var import_fs6 = require("fs");
44704
+ var import_fs7 = require("fs");
44603
44705
  var import_promises4 = require("fs/promises");
44604
44706
  var import_events = require("events");
44605
44707
  var sysPath2 = __toESM(require("path"), 1);
@@ -44824,10 +44926,10 @@ function readdirp(root, options = {}) {
44824
44926
  }
44825
44927
 
44826
44928
  // ../../node_modules/chokidar/esm/handler.js
44827
- var import_fs5 = require("fs");
44929
+ var import_fs6 = require("fs");
44828
44930
  var import_promises3 = require("fs/promises");
44829
44931
  var sysPath = __toESM(require("path"), 1);
44830
- var import_os2 = require("os");
44932
+ var import_os3 = require("os");
44831
44933
  var STR_DATA = "data";
44832
44934
  var STR_END = "end";
44833
44935
  var STR_CLOSE = "close";
@@ -44838,7 +44940,7 @@ var isWindows = pl === "win32";
44838
44940
  var isMacos = pl === "darwin";
44839
44941
  var isLinux = pl === "linux";
44840
44942
  var isFreeBSD = pl === "freebsd";
44841
- var isIBMi = (0, import_os2.type)() === "OS400";
44943
+ var isIBMi = (0, import_os3.type)() === "OS400";
44842
44944
  var EVENTS = {
44843
44945
  ALL: "all",
44844
44946
  READY: "ready",
@@ -45162,7 +45264,7 @@ function createFsWatchInstance(path2, options, listener, errHandler, emitRaw) {
45162
45264
  }
45163
45265
  };
45164
45266
  try {
45165
- return (0, import_fs5.watch)(path2, {
45267
+ return (0, import_fs6.watch)(path2, {
45166
45268
  persistent: options.persistent
45167
45269
  }, handleEvent);
45168
45270
  } catch (error) {
@@ -45245,7 +45347,7 @@ var setFsWatchFileListener = (path2, fullPath, options, handlers) => {
45245
45347
  let cont = FsWatchFileInstances.get(fullPath);
45246
45348
  const copts = cont && cont.options;
45247
45349
  if (copts && (copts.persistent < options.persistent || copts.interval > options.interval)) {
45248
- (0, import_fs5.unwatchFile)(fullPath);
45350
+ (0, import_fs6.unwatchFile)(fullPath);
45249
45351
  cont = void 0;
45250
45352
  }
45251
45353
  if (cont) {
@@ -45256,7 +45358,7 @@ var setFsWatchFileListener = (path2, fullPath, options, handlers) => {
45256
45358
  listeners: listener,
45257
45359
  rawEmitters: rawEmitter,
45258
45360
  options,
45259
- watcher: (0, import_fs5.watchFile)(fullPath, options, (curr, prev) => {
45361
+ watcher: (0, import_fs6.watchFile)(fullPath, options, (curr, prev) => {
45260
45362
  foreach(cont.rawEmitters, (rawEmitter2) => {
45261
45363
  rawEmitter2(EV.CHANGE, fullPath, { curr, prev });
45262
45364
  });
@@ -45273,7 +45375,7 @@ var setFsWatchFileListener = (path2, fullPath, options, handlers) => {
45273
45375
  delFromSet(cont, KEY_RAW, rawEmitter);
45274
45376
  if (isEmptySet(cont.listeners)) {
45275
45377
  FsWatchFileInstances.delete(fullPath);
45276
- (0, import_fs5.unwatchFile)(fullPath);
45378
+ (0, import_fs6.unwatchFile)(fullPath);
45277
45379
  cont.options = cont.watcher = void 0;
45278
45380
  Object.freeze(cont);
45279
45381
  }
@@ -46116,7 +46218,7 @@ var FSWatcher = class extends import_events.EventEmitter {
46116
46218
  const now = /* @__PURE__ */ new Date();
46117
46219
  const writes = this._pendingWrites;
46118
46220
  function awaitWriteFinishFn(prevStat) {
46119
- (0, import_fs6.stat)(fullPath, (err, curStat) => {
46221
+ (0, import_fs7.stat)(fullPath, (err, curStat) => {
46120
46222
  if (err || !writes.has(path2)) {
46121
46223
  if (err && err.code !== "ENOENT")
46122
46224
  awfEmit(err);
@@ -46293,13 +46395,13 @@ var esm_default = { watch, FSWatcher };
46293
46395
  var import_scanner2 = __toESM(require_dist());
46294
46396
  var import_formatters2 = __toESM(require_formatters());
46295
46397
  function isScannableFile2(filePath) {
46296
- const ext2 = (0, import_path4.extname)(filePath).toLowerCase();
46297
- const fileName = (0, import_path4.basename)(filePath);
46398
+ const ext2 = (0, import_path5.extname)(filePath).toLowerCase();
46399
+ const fileName = (0, import_path5.basename)(filePath);
46298
46400
  if (import_scanner2.SPECIAL_FILES.includes(fileName)) return true;
46299
46401
  return import_scanner2.SCANNABLE_EXTENSIONS.includes(ext2);
46300
46402
  }
46301
46403
  function getLanguage2(filePath) {
46302
- const ext2 = (0, import_path4.extname)(filePath).toLowerCase();
46404
+ const ext2 = (0, import_path5.extname)(filePath).toLowerCase();
46303
46405
  const langMap = {
46304
46406
  ".js": "javascript",
46305
46407
  ".jsx": "javascript",
@@ -46318,11 +46420,11 @@ function getLanguage2(filePath) {
46318
46420
  }
46319
46421
  function readScanFile(filePath) {
46320
46422
  try {
46321
- const stats = (0, import_fs7.statSync)(filePath);
46423
+ const stats = (0, import_fs8.statSync)(filePath);
46322
46424
  if (stats.size > import_scanner2.MAX_FILE_SIZE) return null;
46323
- const content = (0, import_fs7.readFileSync)(filePath, "utf-8");
46425
+ const content = (0, import_fs8.readFileSync)(filePath, "utf-8");
46324
46426
  return {
46325
- path: (0, import_path4.relative)(process.cwd(), filePath),
46427
+ path: (0, import_path5.relative)(process.cwd(), filePath),
46326
46428
  content,
46327
46429
  language: getLanguage2(filePath),
46328
46430
  size: content.length
@@ -46339,7 +46441,7 @@ function debounce(fn, delay) {
46339
46441
  };
46340
46442
  }
46341
46443
  async function watch2(targetPath, options) {
46342
- const absolutePath = (0, import_path4.resolve)(targetPath);
46444
+ const absolutePath = (0, import_path5.resolve)(targetPath);
46343
46445
  const config = getConfig();
46344
46446
  if ((options.depth === "validated" || options.depth === "deep") && !isAuthenticated()) {
46345
46447
  if (!options.quiet) {
@@ -46349,31 +46451,40 @@ async function watch2(targetPath, options) {
46349
46451
  }
46350
46452
  options.depth = "cheap";
46351
46453
  }
46352
- if (!options.quiet) {
46454
+ const changedFiles = /* @__PURE__ */ new Set();
46455
+ let isPaused = false;
46456
+ let scanCount = 0;
46457
+ let totalIssues = 0;
46458
+ const showBanner = () => {
46459
+ if (options.quiet) return;
46353
46460
  console.log("");
46354
46461
  console.log(source_default.bold(" \u{1F441}\uFE0F Oculum Watch Mode"));
46355
- console.log(source_default.dim(" " + "\u2500".repeat(38)));
46462
+ console.log(source_default.dim(" " + "\u2500".repeat(50)));
46356
46463
  console.log("");
46357
46464
  console.log(source_default.dim(" Watching: ") + source_default.white(absolutePath));
46358
46465
  console.log(source_default.dim(" Depth: ") + source_default.white(options.depth === "cheap" ? "Quick (pattern matching)" : options.depth));
46359
- console.log(source_default.dim(" Debounce: ") + source_default.white(`${options.debounce}ms`));
46466
+ console.log(source_default.dim(" Status: ") + (isPaused ? source_default.yellow("Paused") : source_default.green("Active")));
46467
+ if (scanCount > 0) {
46468
+ console.log(source_default.dim(" Scans: ") + source_default.white(`${scanCount} (${totalIssues} issues found)`));
46469
+ }
46360
46470
  console.log("");
46361
- console.log(source_default.dim(" Press ") + source_default.white("Ctrl+C") + source_default.dim(" to stop watching"));
46471
+ console.log(source_default.dim(" Keyboard: ") + source_default.white("[r]") + source_default.dim(" rescan ") + source_default.white("[c]") + source_default.dim(" clear ") + source_default.white("[p]") + source_default.dim(" pause ") + source_default.white("[q]") + source_default.dim(" quit"));
46472
+ console.log(source_default.dim(" " + "\u2500".repeat(50)));
46362
46473
  console.log("");
46363
- }
46364
- const changedFiles = /* @__PURE__ */ new Set();
46474
+ };
46475
+ showBanner();
46365
46476
  let isScanning = false;
46366
46477
  const runScanOnChanges = debounce(async () => {
46367
- if (isScanning || changedFiles.size === 0) return;
46478
+ if (isScanning || changedFiles.size === 0 || isPaused) return;
46368
46479
  isScanning = true;
46369
46480
  const filesToScan = Array.from(changedFiles);
46370
46481
  changedFiles.clear();
46371
46482
  if (options.clearOnScan && !options.quiet) {
46372
46483
  console.clear();
46484
+ showBanner();
46373
46485
  }
46374
46486
  if (!options.quiet) {
46375
46487
  const fileText = filesToScan.length === 1 ? "file" : "files";
46376
- console.log("");
46377
46488
  console.log(source_default.cyan(` \u27F3 [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Scanning ${filesToScan.length} changed ${fileText}...`));
46378
46489
  }
46379
46490
  const scanFiles = [];
@@ -46383,7 +46494,7 @@ async function watch2(targetPath, options) {
46383
46494
  }
46384
46495
  if (scanFiles.length === 0) {
46385
46496
  if (!options.quiet) {
46386
- console.log(source_default.dim("No scannable files found."));
46497
+ console.log(source_default.dim(" No scannable files found."));
46387
46498
  }
46388
46499
  isScanning = false;
46389
46500
  return;
@@ -46392,24 +46503,36 @@ async function watch2(targetPath, options) {
46392
46503
  const result = await (0, import_scanner2.runScan)(
46393
46504
  scanFiles,
46394
46505
  {
46395
- name: (0, import_path4.basename)(absolutePath),
46506
+ name: (0, import_path5.basename)(absolutePath),
46396
46507
  url: "",
46397
46508
  branch: "watch"
46398
46509
  },
46399
46510
  {
46400
46511
  enableAI: options.depth !== "cheap" && isAuthenticated(),
46401
46512
  scanDepth: options.depth,
46402
- scanMode: "incremental"
46513
+ scanMode: "incremental",
46514
+ quiet: true
46515
+ // Suppress internal scanner logs
46403
46516
  }
46404
46517
  );
46518
+ scanCount++;
46519
+ totalIssues += result.vulnerabilities.length;
46520
+ try {
46521
+ addScanHistoryEntry({
46522
+ targetPath: absolutePath,
46523
+ options: { depth: options.depth, format: "terminal", failOn: "high" },
46524
+ result
46525
+ });
46526
+ } catch {
46527
+ }
46405
46528
  if (result.vulnerabilities.length === 0) {
46406
46529
  if (!options.quiet) {
46407
- console.log(source_default.green(" \u2713 No issues found"));
46530
+ console.log(source_default.green(" \u2713 No issues found"));
46408
46531
  }
46409
46532
  } else {
46410
46533
  const issueCount = result.vulnerabilities.length;
46411
46534
  const issueText = issueCount === 1 ? "issue" : "issues";
46412
- console.log(source_default.yellow(` \u26A0 Found ${issueCount} ${issueText}:`));
46535
+ console.log(source_default.yellow(` \u26A0 Found ${issueCount} ${issueText}:`));
46413
46536
  console.log((0, import_formatters2.formatTerminalOutput)(result, {
46414
46537
  maxFindingsPerGroup: 5
46415
46538
  }));
@@ -46420,6 +46543,47 @@ async function watch2(targetPath, options) {
46420
46543
  }
46421
46544
  isScanning = false;
46422
46545
  }, options.debounce);
46546
+ const triggerFullRescan = async () => {
46547
+ if (isScanning) return;
46548
+ if (!options.quiet) {
46549
+ console.log(source_default.cyan(`
46550
+ \u27F3 [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Triggering full rescan...`));
46551
+ }
46552
+ const { glob: glob3 } = await Promise.resolve().then(() => (init_esm7(), esm_exports));
46553
+ const patterns2 = [
46554
+ "**/*.js",
46555
+ "**/*.jsx",
46556
+ "**/*.ts",
46557
+ "**/*.tsx",
46558
+ "**/*.py",
46559
+ "**/*.go",
46560
+ "**/*.java",
46561
+ "**/*.rb",
46562
+ "**/*.php",
46563
+ "**/*.cs",
46564
+ "**/package.json"
46565
+ ];
46566
+ const ignorePatterns2 = [
46567
+ "**/node_modules/**",
46568
+ "**/dist/**",
46569
+ "**/build/**",
46570
+ "**/.git/**",
46571
+ "**/vendor/**",
46572
+ "**/__pycache__/**"
46573
+ ];
46574
+ const allFiles2 = await glob3(patterns2, {
46575
+ cwd: absolutePath,
46576
+ ignore: ignorePatterns2,
46577
+ nodir: true,
46578
+ absolute: true
46579
+ });
46580
+ changedFiles.clear();
46581
+ for (const filePath of allFiles2) {
46582
+ changedFiles.add(filePath);
46583
+ }
46584
+ isScanning = false;
46585
+ runScanOnChanges();
46586
+ };
46423
46587
  const watcher = esm_default.watch(absolutePath, {
46424
46588
  ignored: [
46425
46589
  "**/node_modules/**",
@@ -46439,17 +46603,17 @@ async function watch2(targetPath, options) {
46439
46603
  });
46440
46604
  watcher.on("change", (filePath) => {
46441
46605
  if (!isScannableFile2(filePath)) return;
46442
- changedFiles.add((0, import_path4.resolve)(filePath));
46606
+ changedFiles.add((0, import_path5.resolve)(filePath));
46443
46607
  if (!options.quiet) {
46444
- console.log(source_default.dim(`Changed: ${(0, import_path4.relative)(process.cwd(), filePath)}`));
46608
+ console.log(source_default.dim(`Changed: ${(0, import_path5.relative)(process.cwd(), filePath)}`));
46445
46609
  }
46446
46610
  runScanOnChanges();
46447
46611
  });
46448
46612
  watcher.on("add", (filePath) => {
46449
46613
  if (!isScannableFile2(filePath)) return;
46450
- changedFiles.add((0, import_path4.resolve)(filePath));
46614
+ changedFiles.add((0, import_path5.resolve)(filePath));
46451
46615
  if (!options.quiet) {
46452
- console.log(source_default.dim(`Added: ${(0, import_path4.relative)(process.cwd(), filePath)}`));
46616
+ console.log(source_default.dim(`Added: ${(0, import_path5.relative)(process.cwd(), filePath)}`));
46453
46617
  }
46454
46618
  runScanOnChanges();
46455
46619
  });
@@ -46457,13 +46621,47 @@ async function watch2(targetPath, options) {
46457
46621
  const enhanced = enhanceError(error);
46458
46622
  console.error(formatError(enhanced));
46459
46623
  });
46460
- process.on("SIGINT", () => {
46624
+ const cleanup = () => {
46461
46625
  if (!options.quiet) {
46462
- console.log(source_default.dim("\n\nStopping watch mode..."));
46626
+ console.log(source_default.dim("\n\n Stopping watch mode..."));
46627
+ if (scanCount > 0) {
46628
+ console.log(source_default.dim(` Completed ${scanCount} scans, found ${totalIssues} total issues.`));
46629
+ }
46630
+ console.log("");
46631
+ }
46632
+ if (process.stdin.isTTY) {
46633
+ process.stdin.setRawMode(false);
46463
46634
  }
46464
46635
  watcher.close();
46465
46636
  process.exit(0);
46466
- });
46637
+ };
46638
+ if (process.stdin.isTTY && !options.quiet) {
46639
+ readline.emitKeypressEvents(process.stdin);
46640
+ process.stdin.setRawMode(true);
46641
+ process.stdin.on("keypress", (ch, key) => {
46642
+ if (!key) return;
46643
+ if (key.name === "r") {
46644
+ triggerFullRescan();
46645
+ } else if (key.name === "c") {
46646
+ console.clear();
46647
+ showBanner();
46648
+ } else if (key.name === "p") {
46649
+ isPaused = !isPaused;
46650
+ if (isPaused) {
46651
+ console.log(source_default.yellow("\n \u23F8 Watch paused. Press [p] to resume.\n"));
46652
+ } else {
46653
+ console.log(source_default.green("\n \u25B6 Watch resumed.\n"));
46654
+ if (changedFiles.size > 0) {
46655
+ runScanOnChanges();
46656
+ }
46657
+ }
46658
+ } else if (key.name === "q" || key.ctrl && key.name === "c") {
46659
+ cleanup();
46660
+ }
46661
+ });
46662
+ } else {
46663
+ process.on("SIGINT", cleanup);
46664
+ }
46467
46665
  if (!options.quiet) {
46468
46666
  console.log(source_default.dim("Performing initial scan..."));
46469
46667
  }
@@ -46507,9 +46705,15 @@ Examples:
46507
46705
  $ oculum watch . --clear # Clear console between scans
46508
46706
  $ oculum watch . --debounce 1000 # Wait 1s after changes
46509
46707
 
46708
+ Keyboard Controls:
46709
+ [r] Rescan all files
46710
+ [c] Clear console
46711
+ [p] Pause/resume watching
46712
+ [q] Quit watch mode
46713
+
46510
46714
  Tips:
46511
46715
  \u2022 Watch mode uses 'cheap' depth by default for fast feedback
46512
- \u2022 Press Ctrl+C to stop watching
46716
+ \u2022 Scans are recorded to history (view with: oculum history)
46513
46717
  \u2022 Combine with oculum.config.json for project-specific settings
46514
46718
  `).action((path2, cliOptions) => {
46515
46719
  const projectConfig = loadProjectConfig(path2, cliOptions.quiet);
@@ -47132,59 +47336,6 @@ var Y2 = ({ indicator: t = "dots" } = {}) => {
47132
47336
  // src/commands/ui.ts
47133
47337
  var import_fs11 = require("fs");
47134
47338
 
47135
- // src/utils/history.ts
47136
- var import_fs8 = require("fs");
47137
- var import_path5 = require("path");
47138
- var import_os3 = require("os");
47139
- var import_crypto = require("crypto");
47140
- var CONFIG_DIR2 = (0, import_path5.join)((0, import_os3.homedir)(), ".oculum");
47141
- var HISTORY_FILE = (0, import_path5.join)(CONFIG_DIR2, "history.json");
47142
- var MAX_ENTRIES = 25;
47143
- function ensureConfigDir2() {
47144
- if (!(0, import_fs8.existsSync)(CONFIG_DIR2)) {
47145
- (0, import_fs8.mkdirSync)(CONFIG_DIR2, { recursive: true });
47146
- }
47147
- }
47148
- function readHistoryFile() {
47149
- try {
47150
- if (!(0, import_fs8.existsSync)(HISTORY_FILE)) return [];
47151
- const content = (0, import_fs8.readFileSync)(HISTORY_FILE, "utf-8");
47152
- const parsed = JSON.parse(content);
47153
- if (!Array.isArray(parsed)) return [];
47154
- return parsed;
47155
- } catch {
47156
- return [];
47157
- }
47158
- }
47159
- function writeHistoryFile(entries) {
47160
- ensureConfigDir2();
47161
- (0, import_fs8.writeFileSync)(HISTORY_FILE, JSON.stringify(entries, null, 2));
47162
- }
47163
- function listScanHistory() {
47164
- return readHistoryFile();
47165
- }
47166
- function addScanHistoryEntry(input) {
47167
- const entry = {
47168
- id: input.id ?? (0, import_crypto.randomUUID)(),
47169
- createdAt: input.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
47170
- targetPath: input.targetPath,
47171
- options: input.options,
47172
- result: input.result
47173
- };
47174
- const current = readHistoryFile();
47175
- const updated = [entry, ...current].slice(0, MAX_ENTRIES);
47176
- writeHistoryFile(updated);
47177
- return entry;
47178
- }
47179
- function deleteScanHistoryEntry(id) {
47180
- const current = readHistoryFile();
47181
- const updated = current.filter((e2) => e2.id !== id);
47182
- writeHistoryFile(updated);
47183
- }
47184
- function clearScanHistory() {
47185
- writeHistoryFile([]);
47186
- }
47187
-
47188
47339
  // src/utils/first-run.ts
47189
47340
  var import_fs9 = require("fs");
47190
47341
  var import_path6 = require("path");
@@ -48138,6 +48289,9 @@ async function runAuthFlow() {
48138
48289
  const verified = await verifyApiKey(config.apiKey);
48139
48290
  if (verified.valid && verified.tier) {
48140
48291
  currentTier = verified.tier;
48292
+ if (currentTier !== config.tier || verified.email && verified.email !== config.email) {
48293
+ setAuthCredentials(config.apiKey, verified.email || config.email, currentTier);
48294
+ }
48141
48295
  }
48142
48296
  } catch {
48143
48297
  }
@@ -48167,9 +48321,14 @@ async function runAuthFlow() {
48167
48321
  s.stop("Stored credentials are invalid or expired.");
48168
48322
  continue;
48169
48323
  }
48324
+ const email = verified.email || getConfig().email || "unknown";
48325
+ const tier = verified.tier || "free";
48326
+ if (tier !== getConfig().tier || email !== getConfig().email) {
48327
+ setAuthCredentials(getConfig().apiKey, email, tier);
48328
+ }
48170
48329
  s.stop("Authenticated");
48171
- M2.info(`Email: ${verified.email || getConfig().email || "unknown"}`);
48172
- M2.info(`Tier: ${verified.tier || getConfig().tier || "unknown"}`);
48330
+ M2.info(`Email: ${email}`);
48331
+ M2.info(`Tier: ${tier}`);
48173
48332
  } catch (err) {
48174
48333
  s.stop("Verification failed");
48175
48334
  M2.error(String(err));
@@ -48921,6 +49080,200 @@ function showTroubleshooting() {
48921
49080
  console.log(source_default.white(" - Get support: ") + source_default.cyan("support@oculum.dev\n"));
48922
49081
  }
48923
49082
 
49083
+ // src/commands/history.ts
49084
+ function formatDate3(isoString) {
49085
+ const date = new Date(isoString);
49086
+ return date.toLocaleString("en-US", {
49087
+ month: "short",
49088
+ day: "numeric",
49089
+ hour: "2-digit",
49090
+ minute: "2-digit"
49091
+ });
49092
+ }
49093
+ function truncate(str, maxLen) {
49094
+ if (str.length <= maxLen) return str;
49095
+ return str.slice(0, maxLen - 2) + "..";
49096
+ }
49097
+ function getStatusBadge(entry) {
49098
+ const { severityCounts } = entry.result;
49099
+ if (severityCounts.critical > 0 || severityCounts.high > 0) {
49100
+ return source_default.red("FAIL");
49101
+ }
49102
+ return source_default.green("PASS");
49103
+ }
49104
+ function getTotalIssues(entry) {
49105
+ const { severityCounts } = entry.result;
49106
+ return severityCounts.critical + severityCounts.high + severityCounts.medium + severityCounts.low + severityCounts.info;
49107
+ }
49108
+ function showHistoryList() {
49109
+ const entries = listScanHistory();
49110
+ if (entries.length === 0) {
49111
+ console.log(source_default.dim("\nNo scan history found."));
49112
+ console.log(source_default.dim("Run a scan with: oculum scan .\n"));
49113
+ return;
49114
+ }
49115
+ console.log(source_default.bold("\nRecent Scans"));
49116
+ console.log(source_default.dim("\u2500".repeat(70)));
49117
+ console.log();
49118
+ console.log(
49119
+ source_default.dim(" ID ") + source_default.dim("Date ") + source_default.dim("Path ") + source_default.dim("Issues ") + source_default.dim("Status")
49120
+ );
49121
+ console.log(source_default.dim(" " + "\u2500".repeat(66)));
49122
+ for (const entry of entries) {
49123
+ const id = entry.id.slice(0, 8);
49124
+ const date = formatDate3(entry.createdAt);
49125
+ const path2 = truncate(entry.targetPath, 22);
49126
+ const issues = getTotalIssues(entry);
49127
+ const status2 = getStatusBadge(entry);
49128
+ console.log(
49129
+ ` ${source_default.cyan(id)} ${source_default.white(date.padEnd(16))} ${source_default.dim(path2.padEnd(22))} ${String(issues).padEnd(6)} ` + status2
49130
+ );
49131
+ }
49132
+ console.log();
49133
+ console.log(source_default.dim("\u2500".repeat(70)));
49134
+ console.log(source_default.dim("Run 'oculum history show <id>' for details"));
49135
+ console.log(source_default.dim("Run 'oculum history clear' to clear all history\n"));
49136
+ }
49137
+ function showScanDetails(id) {
49138
+ const entry = getScanHistoryEntry(id);
49139
+ if (!entry) {
49140
+ const entries = listScanHistory();
49141
+ const partial = entries.find((e2) => e2.id.startsWith(id));
49142
+ if (partial) {
49143
+ showScanDetailsForEntry(partial);
49144
+ return;
49145
+ }
49146
+ console.log(source_default.red(`
49147
+ Scan not found: ${id}`));
49148
+ console.log(source_default.dim("Run 'oculum history' to see available scans\n"));
49149
+ return;
49150
+ }
49151
+ showScanDetailsForEntry(entry);
49152
+ }
49153
+ function showScanDetailsForEntry(entry) {
49154
+ const { result, options, targetPath, createdAt, id } = entry;
49155
+ const { severityCounts, vulnerabilities, filesScanned, scanDuration } = result;
49156
+ console.log(source_default.bold("\nScan Details"));
49157
+ console.log(source_default.dim("\u2500".repeat(50)));
49158
+ console.log();
49159
+ console.log(source_default.white(" ID: ") + source_default.cyan(id.slice(0, 8)));
49160
+ console.log(source_default.white(" Date: ") + formatDate3(createdAt));
49161
+ console.log(source_default.white(" Path: ") + targetPath);
49162
+ console.log(source_default.white(" Depth: ") + options.depth);
49163
+ console.log(source_default.white(" Files: ") + filesScanned);
49164
+ console.log(source_default.white(" Duration: ") + `${(scanDuration / 1e3).toFixed(1)}s`);
49165
+ console.log();
49166
+ console.log(source_default.bold(" Findings"));
49167
+ console.log(source_default.dim(" " + "\u2500".repeat(30)));
49168
+ const severities = [
49169
+ { name: "Critical", count: severityCounts.critical, color: source_default.red },
49170
+ { name: "High", count: severityCounts.high, color: source_default.yellow },
49171
+ { name: "Medium", count: severityCounts.medium, color: source_default.magenta },
49172
+ { name: "Low", count: severityCounts.low, color: source_default.blue },
49173
+ { name: "Info", count: severityCounts.info, color: source_default.gray }
49174
+ ];
49175
+ for (const { name, count, color } of severities) {
49176
+ if (count > 0) {
49177
+ console.log(` ${color(name.padEnd(10))} ${count}`);
49178
+ }
49179
+ }
49180
+ const total = getTotalIssues(entry);
49181
+ if (total === 0) {
49182
+ console.log(source_default.green(" No issues found"));
49183
+ }
49184
+ if (vulnerabilities.length > 0) {
49185
+ console.log();
49186
+ console.log(source_default.bold(" Top Findings"));
49187
+ console.log(source_default.dim(" " + "\u2500".repeat(30)));
49188
+ const topFindings = vulnerabilities.slice(0, 5);
49189
+ for (const finding of topFindings) {
49190
+ const severityColor = finding.severity === "critical" ? source_default.red : finding.severity === "high" ? source_default.yellow : finding.severity === "medium" ? source_default.magenta : finding.severity === "low" ? source_default.blue : source_default.gray;
49191
+ console.log(
49192
+ ` ${severityColor(finding.severity.toUpperCase().padEnd(8))} ` + source_default.white(truncate(finding.title, 40))
49193
+ );
49194
+ console.log(source_default.dim(` ${finding.filePath}:${finding.lineNumber}`));
49195
+ }
49196
+ if (vulnerabilities.length > 5) {
49197
+ console.log(source_default.dim(` ... and ${vulnerabilities.length - 5} more`));
49198
+ }
49199
+ }
49200
+ console.log();
49201
+ console.log(source_default.dim("\u2500".repeat(50)));
49202
+ console.log(source_default.dim(`Run 'oculum history delete ${id.slice(0, 8)}' to remove this scan
49203
+ `));
49204
+ }
49205
+ function clearHistory() {
49206
+ const entries = listScanHistory();
49207
+ if (entries.length === 0) {
49208
+ console.log(source_default.dim("\nNo history to clear.\n"));
49209
+ return;
49210
+ }
49211
+ clearScanHistory();
49212
+ console.log(source_default.green(`
49213
+ Cleared ${entries.length} scan entries.
49214
+ `));
49215
+ }
49216
+ function deleteEntry(id) {
49217
+ const entry = getScanHistoryEntry(id);
49218
+ if (!entry) {
49219
+ const entries = listScanHistory();
49220
+ const partial = entries.find((e2) => e2.id.startsWith(id));
49221
+ if (partial) {
49222
+ deleteScanHistoryEntry(partial.id);
49223
+ console.log(source_default.green(`
49224
+ Deleted scan ${partial.id.slice(0, 8)}
49225
+ `));
49226
+ return;
49227
+ }
49228
+ console.log(source_default.red(`
49229
+ Scan not found: ${id}
49230
+ `));
49231
+ return;
49232
+ }
49233
+ deleteScanHistoryEntry(entry.id);
49234
+ console.log(source_default.green(`
49235
+ Deleted scan ${entry.id.slice(0, 8)}
49236
+ `));
49237
+ }
49238
+ var historyCommand = new Command("history").description("View and manage scan history").argument("[subcommand]", "show, clear, or delete").argument("[id]", "scan ID for show/delete").action((subcommand, id) => {
49239
+ if (!subcommand) {
49240
+ showHistoryList();
49241
+ return;
49242
+ }
49243
+ switch (subcommand) {
49244
+ case "show":
49245
+ if (!id) {
49246
+ console.log(source_default.red("\nPlease provide a scan ID"));
49247
+ console.log(source_default.dim("Usage: oculum history show <id>\n"));
49248
+ return;
49249
+ }
49250
+ showScanDetails(id);
49251
+ break;
49252
+ case "clear":
49253
+ clearHistory();
49254
+ break;
49255
+ case "delete":
49256
+ if (!id) {
49257
+ console.log(source_default.red("\nPlease provide a scan ID"));
49258
+ console.log(source_default.dim("Usage: oculum history delete <id>\n"));
49259
+ return;
49260
+ }
49261
+ deleteEntry(id);
49262
+ break;
49263
+ default:
49264
+ showScanDetails(subcommand);
49265
+ }
49266
+ }).addHelpText("after", `
49267
+ Examples:
49268
+ $ oculum history List recent scans
49269
+ $ oculum history show abc123 View scan details
49270
+ $ oculum history abc123 Quick access (same as show)
49271
+ $ oculum history delete abc Delete a scan by ID
49272
+ $ oculum history clear Clear all history
49273
+
49274
+ Note: History stores up to 25 recent scans locally.
49275
+ `);
49276
+
48924
49277
  // src/utils/ci-detect.ts
48925
49278
  var CI_ENV_VARS = [
48926
49279
  "CI",
@@ -48982,7 +49335,7 @@ function shouldRunUI() {
48982
49335
  return isOcAlias || isUICommand;
48983
49336
  }
48984
49337
  var program2 = new Command();
48985
- program2.name("oculum").description("AI-native security scanner for detecting vulnerabilities in LLM-generated code").version("1.0.8").addHelpText("after", `
49338
+ program2.name("oculum").description("AI-native security scanner for detecting vulnerabilities in LLM-generated code").version("1.0.9").addHelpText("after", `
48986
49339
  Quick Start:
48987
49340
  $ oculum scan . Scan current directory (free)
48988
49341
  $ oculum ui Interactive mode with guided setup
@@ -48991,6 +49344,7 @@ Quick Start:
48991
49344
  Common Commands:
48992
49345
  scan [path] Scan files for security vulnerabilities
48993
49346
  watch [path] Watch files and scan on changes
49347
+ history View and manage scan history
48994
49348
  ui Interactive terminal UI
48995
49349
  login Authenticate with Oculum
48996
49350
  status Check authentication status
@@ -49013,6 +49367,7 @@ program2.addCommand(usageCommand);
49013
49367
  program2.addCommand(watchCommand);
49014
49368
  program2.addCommand(uiCommand);
49015
49369
  program2.addCommand(helpCommand);
49370
+ program2.addCommand(historyCommand);
49016
49371
  async function main2() {
49017
49372
  const interactive = isInteractiveTerminal();
49018
49373
  if (interactive && shouldRunUI()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oculum/cli",
3
- "version": "1.0.9",
3
+ "version": "1.0.11",
4
4
  "description": "AI-native security scanner CLI for detecting vulnerabilities in AI-generated code, BYOK patterns, and modern web applications",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -19,7 +19,7 @@
19
19
  "url": "https://github.com/flexipie/oculum/issues"
20
20
  },
21
21
  "scripts": {
22
- "build": "esbuild src/index.ts --bundle --platform=node --target=node18 --outfile=dist/index.js --banner:js=\"#!/usr/bin/env node\" --define:process.env.OCULUM_API_URL='undefined' --define:VERSION='\"1.0.8\"'",
22
+ "build": "esbuild src/index.ts --bundle --platform=node --target=node18 --outfile=dist/index.js --banner:js=\"#!/usr/bin/env node\" --define:process.env.OCULUM_API_URL='undefined' --define:VERSION='\"1.0.9\"'",
23
23
  "dev": "npm run build -- --watch",
24
24
  "test": "echo \"No tests configured yet\"",
25
25
  "lint": "eslint src/"