@oculum/cli 1.0.10 → 1.0.12

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 +515 -146
  2. package/package.json +1 -1
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
@@ -43882,8 +43882,83 @@ function validateConfig(config) {
43882
43882
  validated.watch.clear = config.watch.clear;
43883
43883
  }
43884
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
+ }
43885
43891
  return validated;
43886
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
+ }
43887
43962
 
43888
43963
  // src/commands/scan.ts
43889
43964
  function getChangedFiles(absolutePath, diff) {
@@ -43900,22 +43975,22 @@ function getChangedFiles(absolutePath, diff) {
43900
43975
  args = ["diff", "--name-only", "HEAD"];
43901
43976
  }
43902
43977
  const out = (0, import_child_process.execFileSync)("git", args, { cwd: gitRoot, stdio: ["ignore", "pipe", "ignore"] }).toString();
43903
- 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));
43904
43979
  return files;
43905
43980
  } catch {
43906
43981
  return [];
43907
43982
  }
43908
43983
  }
43909
43984
  function isScannableFile(filePath) {
43910
- const ext2 = (0, import_path3.extname)(filePath).toLowerCase();
43911
- const fileName = (0, import_path3.basename)(filePath);
43985
+ const ext2 = (0, import_path4.extname)(filePath).toLowerCase();
43986
+ const fileName = (0, import_path4.basename)(filePath);
43912
43987
  if (import_scanner.SPECIAL_FILES.includes(fileName)) {
43913
43988
  return true;
43914
43989
  }
43915
43990
  return import_scanner.SCANNABLE_EXTENSIONS.includes(ext2);
43916
43991
  }
43917
43992
  function getLanguage(filePath) {
43918
- const ext2 = (0, import_path3.extname)(filePath).toLowerCase();
43993
+ const ext2 = (0, import_path4.extname)(filePath).toLowerCase();
43919
43994
  const langMap = {
43920
43995
  ".js": "javascript",
43921
43996
  ".jsx": "javascript",
@@ -43940,15 +44015,15 @@ function getLanguage(filePath) {
43940
44015
  return langMap[ext2] || "text";
43941
44016
  }
43942
44017
  async function collectFiles(targetPath) {
43943
- const absolutePath = (0, import_path3.resolve)(targetPath);
43944
- const stats = (0, import_fs4.statSync)(absolutePath);
44018
+ const absolutePath = (0, import_path4.resolve)(targetPath);
44019
+ const stats = (0, import_fs5.statSync)(absolutePath);
43945
44020
  const files = [];
43946
44021
  if (stats.isFile()) {
43947
44022
  if (isScannableFile(absolutePath)) {
43948
- const content = (0, import_fs4.readFileSync)(absolutePath, "utf-8");
44023
+ const content = (0, import_fs5.readFileSync)(absolutePath, "utf-8");
43949
44024
  if (content.length <= import_scanner.MAX_FILE_SIZE) {
43950
44025
  files.push({
43951
- path: (0, import_path3.relative)(process.cwd(), absolutePath),
44026
+ path: (0, import_path4.relative)(process.cwd(), absolutePath),
43952
44027
  content,
43953
44028
  language: getLanguage(absolutePath),
43954
44029
  size: content.length
@@ -44017,8 +44092,8 @@ async function collectFiles(targetPath) {
44017
44092
  "**/.env.*.local",
44018
44093
  "**/**.env"
44019
44094
  ];
44020
- const gitignorePath = (0, import_path3.join)(absolutePath, ".gitignore");
44021
- const hasGitignore = (0, import_fs4.existsSync)(gitignorePath);
44095
+ const gitignorePath = (0, import_path4.join)(absolutePath, ".gitignore");
44096
+ const hasGitignore = (0, import_fs5.existsSync)(gitignorePath);
44022
44097
  const globOptions = {
44023
44098
  cwd: absolutePath,
44024
44099
  ignore: ignorePatterns,
@@ -44032,11 +44107,11 @@ async function collectFiles(targetPath) {
44032
44107
  for (const file of foundFiles) {
44033
44108
  const filePath = String(file);
44034
44109
  try {
44035
- const fileStats = (0, import_fs4.statSync)(filePath);
44110
+ const fileStats = (0, import_fs5.statSync)(filePath);
44036
44111
  if (fileStats.size > import_scanner.MAX_FILE_SIZE) continue;
44037
- const content = (0, import_fs4.readFileSync)(filePath, "utf-8");
44112
+ const content = (0, import_fs5.readFileSync)(filePath, "utf-8");
44038
44113
  files.push({
44039
- path: (0, import_path3.relative)(process.cwd(), filePath),
44114
+ path: (0, import_path4.relative)(process.cwd(), filePath),
44040
44115
  content,
44041
44116
  language: getLanguage(filePath),
44042
44117
  size: content.length
@@ -44048,8 +44123,8 @@ async function collectFiles(targetPath) {
44048
44123
  return files;
44049
44124
  }
44050
44125
  async function collectFilesForScan(targetPath, options) {
44051
- const absolutePath = (0, import_path3.resolve)(targetPath);
44052
- const stats = (0, import_fs4.statSync)(absolutePath);
44126
+ const absolutePath = (0, import_path4.resolve)(targetPath);
44127
+ const stats = (0, import_fs5.statSync)(absolutePath);
44053
44128
  if (stats.isFile()) {
44054
44129
  return collectFiles(targetPath);
44055
44130
  }
@@ -44058,13 +44133,13 @@ async function collectFilesForScan(targetPath, options) {
44058
44133
  const files = [];
44059
44134
  for (const filePath of changed) {
44060
44135
  try {
44061
- if (!(0, import_fs4.existsSync)(filePath)) continue;
44136
+ if (!(0, import_fs5.existsSync)(filePath)) continue;
44062
44137
  if (!isScannableFile(filePath)) continue;
44063
- const fileStats = (0, import_fs4.statSync)(filePath);
44138
+ const fileStats = (0, import_fs5.statSync)(filePath);
44064
44139
  if (fileStats.size > import_scanner.MAX_FILE_SIZE) continue;
44065
- const content = (0, import_fs4.readFileSync)(filePath, "utf-8");
44140
+ const content = (0, import_fs5.readFileSync)(filePath, "utf-8");
44066
44141
  files.push({
44067
- path: (0, import_path3.relative)(process.cwd(), filePath),
44142
+ path: (0, import_path4.relative)(process.cwd(), filePath),
44068
44143
  content,
44069
44144
  language: getLanguage(filePath),
44070
44145
  size: content.length
@@ -44078,7 +44153,7 @@ async function collectFilesForScan(targetPath, options) {
44078
44153
  }
44079
44154
  function createEmptyResult(targetPath) {
44080
44155
  return {
44081
- repoName: (0, import_path3.basename)((0, import_path3.resolve)(targetPath)),
44156
+ repoName: (0, import_path4.basename)((0, import_path4.resolve)(targetPath)),
44082
44157
  repoUrl: "",
44083
44158
  branch: "local",
44084
44159
  filesScanned: 0,
@@ -44300,7 +44375,7 @@ async function runScanOnce(targetPath, options) {
44300
44375
  options.depth,
44301
44376
  config.apiKey,
44302
44377
  {
44303
- name: (0, import_path3.basename)((0, import_path3.resolve)(targetPath)),
44378
+ name: (0, import_path4.basename)((0, import_path4.resolve)(targetPath)),
44304
44379
  url: "",
44305
44380
  branch: "local"
44306
44381
  }
@@ -44312,7 +44387,7 @@ async function runScanOnce(targetPath, options) {
44312
44387
  result = await (0, import_scanner.runScan)(
44313
44388
  files,
44314
44389
  {
44315
- name: (0, import_path3.basename)((0, import_path3.resolve)(targetPath)),
44390
+ name: (0, import_path4.basename)((0, import_path4.resolve)(targetPath)),
44316
44391
  url: "",
44317
44392
  branch: "local"
44318
44393
  },
@@ -44375,32 +44450,33 @@ Results written to ${options.output}`));
44375
44450
  }
44376
44451
  async function runScan(targetPath, cliOptions) {
44377
44452
  const projectConfig = loadProjectConfig(targetPath, cliOptions.quiet);
44378
- let scanDepth = cliOptions.mode || cliOptions.depth || "cheap";
44453
+ const profileConfig = getProfileConfig(projectConfig, cliOptions.profile);
44454
+ let scanDepth = cliOptions.mode || cliOptions.depth;
44379
44455
  if (scanDepth === "quick") {
44380
44456
  scanDepth = "cheap";
44381
44457
  }
44382
- const baseOptions = {
44383
- depth: scanDepth,
44384
- format: cliOptions.format ?? "terminal",
44385
- 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",
44386
44462
  color: cliOptions.color,
44387
- quiet: cliOptions.quiet,
44463
+ quiet: cliOptions.quiet ?? profileConfig?.quiet ?? projectConfig?.quiet,
44388
44464
  verbose: cliOptions.verbose,
44389
44465
  incremental: cliOptions.incremental,
44390
44466
  diff: cliOptions.diff,
44391
- output: cliOptions.output
44392
- };
44393
- const options = {
44394
- ...baseOptions,
44395
- depth: baseOptions.depth ?? projectConfig?.depth ?? "cheap",
44396
- format: baseOptions.format ?? projectConfig?.format ?? "terminal",
44397
- failOn: baseOptions.failOn ?? projectConfig?.failOn ?? "high",
44398
- quiet: baseOptions.quiet ?? projectConfig?.quiet,
44399
- verbose: baseOptions.verbose,
44400
- output: baseOptions.output ?? projectConfig?.output
44467
+ output: cliOptions.output ?? profileConfig?.output ?? projectConfig?.output,
44468
+ profile: cliOptions.profile
44401
44469
  };
44402
44470
  try {
44403
- 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
+ }
44404
44480
  console.log(output);
44405
44481
  if (exitCode !== 0) {
44406
44482
  process.exit(exitCode);
@@ -44411,7 +44487,7 @@ async function runScan(targetPath, cliOptions) {
44411
44487
  process.exit(1);
44412
44488
  }
44413
44489
  }
44414
- 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", `
44415
44491
  Scan Modes:
44416
44492
  cheap Free Fast pattern matching, runs locally
44417
44493
  Best for: Quick checks, CI/CD pipelines
@@ -44436,9 +44512,14 @@ Configuration:
44436
44512
  {
44437
44513
  "depth": "validated",
44438
44514
  "failOn": "high",
44439
- "ignore": ["**/test/**"]
44515
+ "profiles": {
44516
+ "ci": { "depth": "validated", "quiet": true, "format": "sarif" },
44517
+ "strict": { "depth": "validated", "failOn": "medium" }
44518
+ }
44440
44519
  }
44441
44520
 
44521
+ Use profiles: oculum scan . --profile ci
44522
+
44442
44523
  More Help:
44443
44524
  $ oculum help scan-modes Detailed mode comparison
44444
44525
  $ oculum help ci-setup CI/CD integration examples
@@ -44615,11 +44696,12 @@ var statusCommand = new Command("status").description("Show current authenticati
44615
44696
  var upgradeCommand = new Command("upgrade").description("Upgrade your subscription").action(upgrade);
44616
44697
 
44617
44698
  // src/commands/watch.ts
44618
- var import_path4 = require("path");
44619
- var import_fs7 = require("fs");
44699
+ var import_path5 = require("path");
44700
+ var import_fs8 = require("fs");
44701
+ var readline = __toESM(require("readline"));
44620
44702
 
44621
44703
  // ../../node_modules/chokidar/esm/index.js
44622
- var import_fs6 = require("fs");
44704
+ var import_fs7 = require("fs");
44623
44705
  var import_promises4 = require("fs/promises");
44624
44706
  var import_events = require("events");
44625
44707
  var sysPath2 = __toESM(require("path"), 1);
@@ -44844,10 +44926,10 @@ function readdirp(root, options = {}) {
44844
44926
  }
44845
44927
 
44846
44928
  // ../../node_modules/chokidar/esm/handler.js
44847
- var import_fs5 = require("fs");
44929
+ var import_fs6 = require("fs");
44848
44930
  var import_promises3 = require("fs/promises");
44849
44931
  var sysPath = __toESM(require("path"), 1);
44850
- var import_os2 = require("os");
44932
+ var import_os3 = require("os");
44851
44933
  var STR_DATA = "data";
44852
44934
  var STR_END = "end";
44853
44935
  var STR_CLOSE = "close";
@@ -44858,7 +44940,7 @@ var isWindows = pl === "win32";
44858
44940
  var isMacos = pl === "darwin";
44859
44941
  var isLinux = pl === "linux";
44860
44942
  var isFreeBSD = pl === "freebsd";
44861
- var isIBMi = (0, import_os2.type)() === "OS400";
44943
+ var isIBMi = (0, import_os3.type)() === "OS400";
44862
44944
  var EVENTS = {
44863
44945
  ALL: "all",
44864
44946
  READY: "ready",
@@ -45182,7 +45264,7 @@ function createFsWatchInstance(path2, options, listener, errHandler, emitRaw) {
45182
45264
  }
45183
45265
  };
45184
45266
  try {
45185
- return (0, import_fs5.watch)(path2, {
45267
+ return (0, import_fs6.watch)(path2, {
45186
45268
  persistent: options.persistent
45187
45269
  }, handleEvent);
45188
45270
  } catch (error) {
@@ -45265,7 +45347,7 @@ var setFsWatchFileListener = (path2, fullPath, options, handlers) => {
45265
45347
  let cont = FsWatchFileInstances.get(fullPath);
45266
45348
  const copts = cont && cont.options;
45267
45349
  if (copts && (copts.persistent < options.persistent || copts.interval > options.interval)) {
45268
- (0, import_fs5.unwatchFile)(fullPath);
45350
+ (0, import_fs6.unwatchFile)(fullPath);
45269
45351
  cont = void 0;
45270
45352
  }
45271
45353
  if (cont) {
@@ -45276,7 +45358,7 @@ var setFsWatchFileListener = (path2, fullPath, options, handlers) => {
45276
45358
  listeners: listener,
45277
45359
  rawEmitters: rawEmitter,
45278
45360
  options,
45279
- watcher: (0, import_fs5.watchFile)(fullPath, options, (curr, prev) => {
45361
+ watcher: (0, import_fs6.watchFile)(fullPath, options, (curr, prev) => {
45280
45362
  foreach(cont.rawEmitters, (rawEmitter2) => {
45281
45363
  rawEmitter2(EV.CHANGE, fullPath, { curr, prev });
45282
45364
  });
@@ -45293,7 +45375,7 @@ var setFsWatchFileListener = (path2, fullPath, options, handlers) => {
45293
45375
  delFromSet(cont, KEY_RAW, rawEmitter);
45294
45376
  if (isEmptySet(cont.listeners)) {
45295
45377
  FsWatchFileInstances.delete(fullPath);
45296
- (0, import_fs5.unwatchFile)(fullPath);
45378
+ (0, import_fs6.unwatchFile)(fullPath);
45297
45379
  cont.options = cont.watcher = void 0;
45298
45380
  Object.freeze(cont);
45299
45381
  }
@@ -46136,7 +46218,7 @@ var FSWatcher = class extends import_events.EventEmitter {
46136
46218
  const now = /* @__PURE__ */ new Date();
46137
46219
  const writes = this._pendingWrites;
46138
46220
  function awaitWriteFinishFn(prevStat) {
46139
- (0, import_fs6.stat)(fullPath, (err, curStat) => {
46221
+ (0, import_fs7.stat)(fullPath, (err, curStat) => {
46140
46222
  if (err || !writes.has(path2)) {
46141
46223
  if (err && err.code !== "ENOENT")
46142
46224
  awfEmit(err);
@@ -46313,13 +46395,13 @@ var esm_default = { watch, FSWatcher };
46313
46395
  var import_scanner2 = __toESM(require_dist());
46314
46396
  var import_formatters2 = __toESM(require_formatters());
46315
46397
  function isScannableFile2(filePath) {
46316
- const ext2 = (0, import_path4.extname)(filePath).toLowerCase();
46317
- const fileName = (0, import_path4.basename)(filePath);
46398
+ const ext2 = (0, import_path5.extname)(filePath).toLowerCase();
46399
+ const fileName = (0, import_path5.basename)(filePath);
46318
46400
  if (import_scanner2.SPECIAL_FILES.includes(fileName)) return true;
46319
46401
  return import_scanner2.SCANNABLE_EXTENSIONS.includes(ext2);
46320
46402
  }
46321
46403
  function getLanguage2(filePath) {
46322
- const ext2 = (0, import_path4.extname)(filePath).toLowerCase();
46404
+ const ext2 = (0, import_path5.extname)(filePath).toLowerCase();
46323
46405
  const langMap = {
46324
46406
  ".js": "javascript",
46325
46407
  ".jsx": "javascript",
@@ -46338,11 +46420,11 @@ function getLanguage2(filePath) {
46338
46420
  }
46339
46421
  function readScanFile(filePath) {
46340
46422
  try {
46341
- const stats = (0, import_fs7.statSync)(filePath);
46423
+ const stats = (0, import_fs8.statSync)(filePath);
46342
46424
  if (stats.size > import_scanner2.MAX_FILE_SIZE) return null;
46343
- const content = (0, import_fs7.readFileSync)(filePath, "utf-8");
46425
+ const content = (0, import_fs8.readFileSync)(filePath, "utf-8");
46344
46426
  return {
46345
- path: (0, import_path4.relative)(process.cwd(), filePath),
46427
+ path: (0, import_path5.relative)(process.cwd(), filePath),
46346
46428
  content,
46347
46429
  language: getLanguage2(filePath),
46348
46430
  size: content.length
@@ -46359,7 +46441,7 @@ function debounce(fn, delay) {
46359
46441
  };
46360
46442
  }
46361
46443
  async function watch2(targetPath, options) {
46362
- const absolutePath = (0, import_path4.resolve)(targetPath);
46444
+ const absolutePath = (0, import_path5.resolve)(targetPath);
46363
46445
  const config = getConfig();
46364
46446
  if ((options.depth === "validated" || options.depth === "deep") && !isAuthenticated()) {
46365
46447
  if (!options.quiet) {
@@ -46369,31 +46451,40 @@ async function watch2(targetPath, options) {
46369
46451
  }
46370
46452
  options.depth = "cheap";
46371
46453
  }
46372
- 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;
46373
46460
  console.log("");
46374
46461
  console.log(source_default.bold(" \u{1F441}\uFE0F Oculum Watch Mode"));
46375
- console.log(source_default.dim(" " + "\u2500".repeat(38)));
46462
+ console.log(source_default.dim(" " + "\u2500".repeat(50)));
46376
46463
  console.log("");
46377
46464
  console.log(source_default.dim(" Watching: ") + source_default.white(absolutePath));
46378
46465
  console.log(source_default.dim(" Depth: ") + source_default.white(options.depth === "cheap" ? "Quick (pattern matching)" : options.depth));
46379
- 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
+ }
46380
46470
  console.log("");
46381
- 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)));
46382
46473
  console.log("");
46383
- }
46384
- const changedFiles = /* @__PURE__ */ new Set();
46474
+ };
46475
+ showBanner();
46385
46476
  let isScanning = false;
46386
46477
  const runScanOnChanges = debounce(async () => {
46387
- if (isScanning || changedFiles.size === 0) return;
46478
+ if (isScanning || changedFiles.size === 0 || isPaused) return;
46388
46479
  isScanning = true;
46389
46480
  const filesToScan = Array.from(changedFiles);
46390
46481
  changedFiles.clear();
46391
46482
  if (options.clearOnScan && !options.quiet) {
46392
46483
  console.clear();
46484
+ showBanner();
46393
46485
  }
46394
46486
  if (!options.quiet) {
46395
46487
  const fileText = filesToScan.length === 1 ? "file" : "files";
46396
- console.log("");
46397
46488
  console.log(source_default.cyan(` \u27F3 [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Scanning ${filesToScan.length} changed ${fileText}...`));
46398
46489
  }
46399
46490
  const scanFiles = [];
@@ -46403,7 +46494,7 @@ async function watch2(targetPath, options) {
46403
46494
  }
46404
46495
  if (scanFiles.length === 0) {
46405
46496
  if (!options.quiet) {
46406
- console.log(source_default.dim("No scannable files found."));
46497
+ console.log(source_default.dim(" No scannable files found."));
46407
46498
  }
46408
46499
  isScanning = false;
46409
46500
  return;
@@ -46412,24 +46503,36 @@ async function watch2(targetPath, options) {
46412
46503
  const result = await (0, import_scanner2.runScan)(
46413
46504
  scanFiles,
46414
46505
  {
46415
- name: (0, import_path4.basename)(absolutePath),
46506
+ name: (0, import_path5.basename)(absolutePath),
46416
46507
  url: "",
46417
46508
  branch: "watch"
46418
46509
  },
46419
46510
  {
46420
46511
  enableAI: options.depth !== "cheap" && isAuthenticated(),
46421
46512
  scanDepth: options.depth,
46422
- scanMode: "incremental"
46513
+ scanMode: "incremental",
46514
+ quiet: true
46515
+ // Suppress internal scanner logs
46423
46516
  }
46424
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
+ }
46425
46528
  if (result.vulnerabilities.length === 0) {
46426
46529
  if (!options.quiet) {
46427
- console.log(source_default.green(" \u2713 No issues found"));
46530
+ console.log(source_default.green(" \u2713 No issues found"));
46428
46531
  }
46429
46532
  } else {
46430
46533
  const issueCount = result.vulnerabilities.length;
46431
46534
  const issueText = issueCount === 1 ? "issue" : "issues";
46432
- console.log(source_default.yellow(` \u26A0 Found ${issueCount} ${issueText}:`));
46535
+ console.log(source_default.yellow(` \u26A0 Found ${issueCount} ${issueText}:`));
46433
46536
  console.log((0, import_formatters2.formatTerminalOutput)(result, {
46434
46537
  maxFindingsPerGroup: 5
46435
46538
  }));
@@ -46440,36 +46543,94 @@ async function watch2(targetPath, options) {
46440
46543
  }
46441
46544
  isScanning = false;
46442
46545
  }, options.debounce);
46443
- const watcher = esm_default.watch(absolutePath, {
46444
- ignored: [
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 = [
46445
46567
  "**/node_modules/**",
46446
46568
  "**/dist/**",
46447
46569
  "**/build/**",
46448
46570
  "**/.git/**",
46449
46571
  "**/vendor/**",
46450
- "**/__pycache__/**",
46451
- "**/venv/**",
46452
- "**/.venv/**",
46453
- "**/coverage/**",
46454
- "**/.next/**",
46455
- "**/.nuxt/**"
46456
- ],
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
+ };
46587
+ const watcherIgnore = [
46588
+ "**/node_modules/**",
46589
+ "**/dist/**",
46590
+ "**/build/**",
46591
+ "**/.git/**",
46592
+ "**/vendor/**",
46593
+ "**/__pycache__/**",
46594
+ "**/venv/**",
46595
+ "**/.venv/**",
46596
+ "**/coverage/**",
46597
+ "**/.next/**",
46598
+ "**/.nuxt/**",
46599
+ "**/.turbo/**",
46600
+ "**/out/**",
46601
+ "**/.cache/**",
46602
+ "**/.vercel/**",
46603
+ "**/.netlify/**",
46604
+ "**/target/**",
46605
+ "**/.gradle/**",
46606
+ "**/.mvn/**",
46607
+ "**/bower_components/**",
46608
+ "**/.yarn/**",
46609
+ "**/package-lock.json",
46610
+ "**/yarn.lock",
46611
+ "**/pnpm-lock.yaml",
46612
+ "**/*.min.js",
46613
+ "**/*.min.css",
46614
+ "**/*.bundle.js"
46615
+ ];
46616
+ const watcher = esm_default.watch(absolutePath, {
46617
+ ignored: watcherIgnore,
46457
46618
  persistent: true,
46458
46619
  ignoreInitial: true
46459
46620
  });
46460
46621
  watcher.on("change", (filePath) => {
46461
46622
  if (!isScannableFile2(filePath)) return;
46462
- changedFiles.add((0, import_path4.resolve)(filePath));
46623
+ changedFiles.add((0, import_path5.resolve)(filePath));
46463
46624
  if (!options.quiet) {
46464
- console.log(source_default.dim(`Changed: ${(0, import_path4.relative)(process.cwd(), filePath)}`));
46625
+ console.log(source_default.dim(`Changed: ${(0, import_path5.relative)(process.cwd(), filePath)}`));
46465
46626
  }
46466
46627
  runScanOnChanges();
46467
46628
  });
46468
46629
  watcher.on("add", (filePath) => {
46469
46630
  if (!isScannableFile2(filePath)) return;
46470
- changedFiles.add((0, import_path4.resolve)(filePath));
46631
+ changedFiles.add((0, import_path5.resolve)(filePath));
46471
46632
  if (!options.quiet) {
46472
- console.log(source_default.dim(`Added: ${(0, import_path4.relative)(process.cwd(), filePath)}`));
46633
+ console.log(source_default.dim(`Added: ${(0, import_path5.relative)(process.cwd(), filePath)}`));
46473
46634
  }
46474
46635
  runScanOnChanges();
46475
46636
  });
@@ -46477,13 +46638,47 @@ async function watch2(targetPath, options) {
46477
46638
  const enhanced = enhanceError(error);
46478
46639
  console.error(formatError(enhanced));
46479
46640
  });
46480
- process.on("SIGINT", () => {
46641
+ const cleanup = () => {
46481
46642
  if (!options.quiet) {
46482
- console.log(source_default.dim("\n\nStopping watch mode..."));
46643
+ console.log(source_default.dim("\n\n Stopping watch mode..."));
46644
+ if (scanCount > 0) {
46645
+ console.log(source_default.dim(` Completed ${scanCount} scans, found ${totalIssues} total issues.`));
46646
+ }
46647
+ console.log("");
46648
+ }
46649
+ if (process.stdin.isTTY) {
46650
+ process.stdin.setRawMode(false);
46483
46651
  }
46484
46652
  watcher.close();
46485
46653
  process.exit(0);
46486
- });
46654
+ };
46655
+ if (process.stdin.isTTY && !options.quiet) {
46656
+ readline.emitKeypressEvents(process.stdin);
46657
+ process.stdin.setRawMode(true);
46658
+ process.stdin.on("keypress", (ch, key) => {
46659
+ if (!key) return;
46660
+ if (key.name === "r") {
46661
+ triggerFullRescan();
46662
+ } else if (key.name === "c") {
46663
+ console.clear();
46664
+ showBanner();
46665
+ } else if (key.name === "p") {
46666
+ isPaused = !isPaused;
46667
+ if (isPaused) {
46668
+ console.log(source_default.yellow("\n \u23F8 Watch paused. Press [p] to resume.\n"));
46669
+ } else {
46670
+ console.log(source_default.green("\n \u25B6 Watch resumed.\n"));
46671
+ if (changedFiles.size > 0) {
46672
+ runScanOnChanges();
46673
+ }
46674
+ }
46675
+ } else if (key.name === "q" || key.ctrl && key.name === "c") {
46676
+ cleanup();
46677
+ }
46678
+ });
46679
+ } else {
46680
+ process.on("SIGINT", cleanup);
46681
+ }
46487
46682
  if (!options.quiet) {
46488
46683
  console.log(source_default.dim("Performing initial scan..."));
46489
46684
  }
@@ -46507,7 +46702,32 @@ async function watch2(targetPath, options) {
46507
46702
  "**/build/**",
46508
46703
  "**/.git/**",
46509
46704
  "**/vendor/**",
46510
- "**/__pycache__/**"
46705
+ "**/__pycache__/**",
46706
+ "**/venv/**",
46707
+ "**/.venv/**",
46708
+ "**/coverage/**",
46709
+ "**/.next/**",
46710
+ "**/.nuxt/**",
46711
+ "**/.turbo/**",
46712
+ "**/out/**",
46713
+ "**/.cache/**",
46714
+ "**/.vercel/**",
46715
+ "**/.netlify/**",
46716
+ "**/target/**",
46717
+ "**/bin/**",
46718
+ "**/obj/**",
46719
+ "**/.gradle/**",
46720
+ "**/.mvn/**",
46721
+ "**/bower_components/**",
46722
+ "**/jspm_packages/**",
46723
+ "**/.yarn/**",
46724
+ "**/.pnp.*",
46725
+ "**/package-lock.json",
46726
+ "**/yarn.lock",
46727
+ "**/pnpm-lock.yaml",
46728
+ "**/*.min.js",
46729
+ "**/*.min.css",
46730
+ "**/*.bundle.js"
46511
46731
  ];
46512
46732
  const allFiles = await glob2(patterns, {
46513
46733
  cwd: absolutePath,
@@ -46527,9 +46747,15 @@ Examples:
46527
46747
  $ oculum watch . --clear # Clear console between scans
46528
46748
  $ oculum watch . --debounce 1000 # Wait 1s after changes
46529
46749
 
46750
+ Keyboard Controls:
46751
+ [r] Rescan all files
46752
+ [c] Clear console
46753
+ [p] Pause/resume watching
46754
+ [q] Quit watch mode
46755
+
46530
46756
  Tips:
46531
46757
  \u2022 Watch mode uses 'cheap' depth by default for fast feedback
46532
- \u2022 Press Ctrl+C to stop watching
46758
+ \u2022 Scans are recorded to history (view with: oculum history)
46533
46759
  \u2022 Combine with oculum.config.json for project-specific settings
46534
46760
  `).action((path2, cliOptions) => {
46535
46761
  const projectConfig = loadProjectConfig(path2, cliOptions.quiet);
@@ -47152,59 +47378,6 @@ var Y2 = ({ indicator: t = "dots" } = {}) => {
47152
47378
  // src/commands/ui.ts
47153
47379
  var import_fs11 = require("fs");
47154
47380
 
47155
- // src/utils/history.ts
47156
- var import_fs8 = require("fs");
47157
- var import_path5 = require("path");
47158
- var import_os3 = require("os");
47159
- var import_crypto = require("crypto");
47160
- var CONFIG_DIR2 = (0, import_path5.join)((0, import_os3.homedir)(), ".oculum");
47161
- var HISTORY_FILE = (0, import_path5.join)(CONFIG_DIR2, "history.json");
47162
- var MAX_ENTRIES = 25;
47163
- function ensureConfigDir2() {
47164
- if (!(0, import_fs8.existsSync)(CONFIG_DIR2)) {
47165
- (0, import_fs8.mkdirSync)(CONFIG_DIR2, { recursive: true });
47166
- }
47167
- }
47168
- function readHistoryFile() {
47169
- try {
47170
- if (!(0, import_fs8.existsSync)(HISTORY_FILE)) return [];
47171
- const content = (0, import_fs8.readFileSync)(HISTORY_FILE, "utf-8");
47172
- const parsed = JSON.parse(content);
47173
- if (!Array.isArray(parsed)) return [];
47174
- return parsed;
47175
- } catch {
47176
- return [];
47177
- }
47178
- }
47179
- function writeHistoryFile(entries) {
47180
- ensureConfigDir2();
47181
- (0, import_fs8.writeFileSync)(HISTORY_FILE, JSON.stringify(entries, null, 2));
47182
- }
47183
- function listScanHistory() {
47184
- return readHistoryFile();
47185
- }
47186
- function addScanHistoryEntry(input) {
47187
- const entry = {
47188
- id: input.id ?? (0, import_crypto.randomUUID)(),
47189
- createdAt: input.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
47190
- targetPath: input.targetPath,
47191
- options: input.options,
47192
- result: input.result
47193
- };
47194
- const current = readHistoryFile();
47195
- const updated = [entry, ...current].slice(0, MAX_ENTRIES);
47196
- writeHistoryFile(updated);
47197
- return entry;
47198
- }
47199
- function deleteScanHistoryEntry(id) {
47200
- const current = readHistoryFile();
47201
- const updated = current.filter((e2) => e2.id !== id);
47202
- writeHistoryFile(updated);
47203
- }
47204
- function clearScanHistory() {
47205
- writeHistoryFile([]);
47206
- }
47207
-
47208
47381
  // src/utils/first-run.ts
47209
47382
  var import_fs9 = require("fs");
47210
47383
  var import_path6 = require("path");
@@ -47280,7 +47453,7 @@ function showLogo() {
47280
47453
  async function showWelcomeScreen() {
47281
47454
  console.clear();
47282
47455
  showLogo();
47283
- console.log(source_default.bold.white(" AI-Native Security Scanner for Modern Codebases\n"));
47456
+ console.log(source_default.bold.white(" AI-Native api key Security Scanner for Modern Codebases\n"));
47284
47457
  console.log(source_default.dim(" \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\u2500\u2500\u2500\u2500\n"));
47285
47458
  console.log(source_default.white(" Oculum detects security vulnerabilities in AI-generated"));
47286
47459
  console.log(source_default.white(" code and LLM-powered applications, including:\n"));
@@ -48949,6 +49122,200 @@ function showTroubleshooting() {
48949
49122
  console.log(source_default.white(" - Get support: ") + source_default.cyan("support@oculum.dev\n"));
48950
49123
  }
48951
49124
 
49125
+ // src/commands/history.ts
49126
+ function formatDate3(isoString) {
49127
+ const date = new Date(isoString);
49128
+ return date.toLocaleString("en-US", {
49129
+ month: "short",
49130
+ day: "numeric",
49131
+ hour: "2-digit",
49132
+ minute: "2-digit"
49133
+ });
49134
+ }
49135
+ function truncate(str, maxLen) {
49136
+ if (str.length <= maxLen) return str;
49137
+ return str.slice(0, maxLen - 2) + "..";
49138
+ }
49139
+ function getStatusBadge(entry) {
49140
+ const { severityCounts } = entry.result;
49141
+ if (severityCounts.critical > 0 || severityCounts.high > 0) {
49142
+ return source_default.red("FAIL");
49143
+ }
49144
+ return source_default.green("PASS");
49145
+ }
49146
+ function getTotalIssues(entry) {
49147
+ const { severityCounts } = entry.result;
49148
+ return severityCounts.critical + severityCounts.high + severityCounts.medium + severityCounts.low + severityCounts.info;
49149
+ }
49150
+ function showHistoryList() {
49151
+ const entries = listScanHistory();
49152
+ if (entries.length === 0) {
49153
+ console.log(source_default.dim("\nNo scan history found."));
49154
+ console.log(source_default.dim("Run a scan with: oculum scan .\n"));
49155
+ return;
49156
+ }
49157
+ console.log(source_default.bold("\nRecent Scans"));
49158
+ console.log(source_default.dim("\u2500".repeat(70)));
49159
+ console.log();
49160
+ console.log(
49161
+ source_default.dim(" ID ") + source_default.dim("Date ") + source_default.dim("Path ") + source_default.dim("Issues ") + source_default.dim("Status")
49162
+ );
49163
+ console.log(source_default.dim(" " + "\u2500".repeat(66)));
49164
+ for (const entry of entries) {
49165
+ const id = entry.id.slice(0, 8);
49166
+ const date = formatDate3(entry.createdAt);
49167
+ const path2 = truncate(entry.targetPath, 22);
49168
+ const issues = getTotalIssues(entry);
49169
+ const status2 = getStatusBadge(entry);
49170
+ console.log(
49171
+ ` ${source_default.cyan(id)} ${source_default.white(date.padEnd(16))} ${source_default.dim(path2.padEnd(22))} ${String(issues).padEnd(6)} ` + status2
49172
+ );
49173
+ }
49174
+ console.log();
49175
+ console.log(source_default.dim("\u2500".repeat(70)));
49176
+ console.log(source_default.dim("Run 'oculum history show <id>' for details"));
49177
+ console.log(source_default.dim("Run 'oculum history clear' to clear all history\n"));
49178
+ }
49179
+ function showScanDetails(id) {
49180
+ const entry = getScanHistoryEntry(id);
49181
+ if (!entry) {
49182
+ const entries = listScanHistory();
49183
+ const partial = entries.find((e2) => e2.id.startsWith(id));
49184
+ if (partial) {
49185
+ showScanDetailsForEntry(partial);
49186
+ return;
49187
+ }
49188
+ console.log(source_default.red(`
49189
+ Scan not found: ${id}`));
49190
+ console.log(source_default.dim("Run 'oculum history' to see available scans\n"));
49191
+ return;
49192
+ }
49193
+ showScanDetailsForEntry(entry);
49194
+ }
49195
+ function showScanDetailsForEntry(entry) {
49196
+ const { result, options, targetPath, createdAt, id } = entry;
49197
+ const { severityCounts, vulnerabilities, filesScanned, scanDuration } = result;
49198
+ console.log(source_default.bold("\nScan Details"));
49199
+ console.log(source_default.dim("\u2500".repeat(50)));
49200
+ console.log();
49201
+ console.log(source_default.white(" ID: ") + source_default.cyan(id.slice(0, 8)));
49202
+ console.log(source_default.white(" Date: ") + formatDate3(createdAt));
49203
+ console.log(source_default.white(" Path: ") + targetPath);
49204
+ console.log(source_default.white(" Depth: ") + options.depth);
49205
+ console.log(source_default.white(" Files: ") + filesScanned);
49206
+ console.log(source_default.white(" Duration: ") + `${(scanDuration / 1e3).toFixed(1)}s`);
49207
+ console.log();
49208
+ console.log(source_default.bold(" Findings"));
49209
+ console.log(source_default.dim(" " + "\u2500".repeat(30)));
49210
+ const severities = [
49211
+ { name: "Critical", count: severityCounts.critical, color: source_default.red },
49212
+ { name: "High", count: severityCounts.high, color: source_default.yellow },
49213
+ { name: "Medium", count: severityCounts.medium, color: source_default.magenta },
49214
+ { name: "Low", count: severityCounts.low, color: source_default.blue },
49215
+ { name: "Info", count: severityCounts.info, color: source_default.gray }
49216
+ ];
49217
+ for (const { name, count, color } of severities) {
49218
+ if (count > 0) {
49219
+ console.log(` ${color(name.padEnd(10))} ${count}`);
49220
+ }
49221
+ }
49222
+ const total = getTotalIssues(entry);
49223
+ if (total === 0) {
49224
+ console.log(source_default.green(" No issues found"));
49225
+ }
49226
+ if (vulnerabilities.length > 0) {
49227
+ console.log();
49228
+ console.log(source_default.bold(" Top Findings"));
49229
+ console.log(source_default.dim(" " + "\u2500".repeat(30)));
49230
+ const topFindings = vulnerabilities.slice(0, 5);
49231
+ for (const finding of topFindings) {
49232
+ 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;
49233
+ console.log(
49234
+ ` ${severityColor(finding.severity.toUpperCase().padEnd(8))} ` + source_default.white(truncate(finding.title, 40))
49235
+ );
49236
+ console.log(source_default.dim(` ${finding.filePath}:${finding.lineNumber}`));
49237
+ }
49238
+ if (vulnerabilities.length > 5) {
49239
+ console.log(source_default.dim(` ... and ${vulnerabilities.length - 5} more`));
49240
+ }
49241
+ }
49242
+ console.log();
49243
+ console.log(source_default.dim("\u2500".repeat(50)));
49244
+ console.log(source_default.dim(`Run 'oculum history delete ${id.slice(0, 8)}' to remove this scan
49245
+ `));
49246
+ }
49247
+ function clearHistory() {
49248
+ const entries = listScanHistory();
49249
+ if (entries.length === 0) {
49250
+ console.log(source_default.dim("\nNo history to clear.\n"));
49251
+ return;
49252
+ }
49253
+ clearScanHistory();
49254
+ console.log(source_default.green(`
49255
+ Cleared ${entries.length} scan entries.
49256
+ `));
49257
+ }
49258
+ function deleteEntry(id) {
49259
+ const entry = getScanHistoryEntry(id);
49260
+ if (!entry) {
49261
+ const entries = listScanHistory();
49262
+ const partial = entries.find((e2) => e2.id.startsWith(id));
49263
+ if (partial) {
49264
+ deleteScanHistoryEntry(partial.id);
49265
+ console.log(source_default.green(`
49266
+ Deleted scan ${partial.id.slice(0, 8)}
49267
+ `));
49268
+ return;
49269
+ }
49270
+ console.log(source_default.red(`
49271
+ Scan not found: ${id}
49272
+ `));
49273
+ return;
49274
+ }
49275
+ deleteScanHistoryEntry(entry.id);
49276
+ console.log(source_default.green(`
49277
+ Deleted scan ${entry.id.slice(0, 8)}
49278
+ `));
49279
+ }
49280
+ 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) => {
49281
+ if (!subcommand) {
49282
+ showHistoryList();
49283
+ return;
49284
+ }
49285
+ switch (subcommand) {
49286
+ case "show":
49287
+ if (!id) {
49288
+ console.log(source_default.red("\nPlease provide a scan ID"));
49289
+ console.log(source_default.dim("Usage: oculum history show <id>\n"));
49290
+ return;
49291
+ }
49292
+ showScanDetails(id);
49293
+ break;
49294
+ case "clear":
49295
+ clearHistory();
49296
+ break;
49297
+ case "delete":
49298
+ if (!id) {
49299
+ console.log(source_default.red("\nPlease provide a scan ID"));
49300
+ console.log(source_default.dim("Usage: oculum history delete <id>\n"));
49301
+ return;
49302
+ }
49303
+ deleteEntry(id);
49304
+ break;
49305
+ default:
49306
+ showScanDetails(subcommand);
49307
+ }
49308
+ }).addHelpText("after", `
49309
+ Examples:
49310
+ $ oculum history List recent scans
49311
+ $ oculum history show abc123 View scan details
49312
+ $ oculum history abc123 Quick access (same as show)
49313
+ $ oculum history delete abc Delete a scan by ID
49314
+ $ oculum history clear Clear all history
49315
+
49316
+ Note: History stores up to 25 recent scans locally.
49317
+ `);
49318
+
48952
49319
  // src/utils/ci-detect.ts
48953
49320
  var CI_ENV_VARS = [
48954
49321
  "CI",
@@ -49019,6 +49386,7 @@ Quick Start:
49019
49386
  Common Commands:
49020
49387
  scan [path] Scan files for security vulnerabilities
49021
49388
  watch [path] Watch files and scan on changes
49389
+ history View and manage scan history
49022
49390
  ui Interactive terminal UI
49023
49391
  login Authenticate with Oculum
49024
49392
  status Check authentication status
@@ -49041,6 +49409,7 @@ program2.addCommand(usageCommand);
49041
49409
  program2.addCommand(watchCommand);
49042
49410
  program2.addCommand(uiCommand);
49043
49411
  program2.addCommand(helpCommand);
49412
+ program2.addCommand(historyCommand);
49044
49413
  async function main2() {
49045
49414
  const interactive = isInteractiveTerminal();
49046
49415
  if (interactive && shouldRunUI()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oculum/cli",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
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": {