sapper-ai 0.6.1 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAkBA,wBAAsB,MAAM,CAAC,IAAI,GAAE,MAAM,EAA0B,GAAG,OAAO,CAAC,MAAM,CAAC,CAuFpF"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAiBA,wBAAsB,MAAM,CAAC,IAAI,GAAE,MAAM,EAA0B,GAAG,OAAO,CAAC,MAAM,CAAC,CAmFpF"}
package/dist/cli.js CHANGED
@@ -39,7 +39,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
39
39
  Object.defineProperty(exports, "__esModule", { value: true });
40
40
  exports.runCli = runCli;
41
41
  const node_fs_1 = require("node:fs");
42
- const node_child_process_1 = require("node:child_process");
43
42
  const node_os_1 = require("node:os");
44
43
  const node_path_1 = require("node:path");
45
44
  const readline = __importStar(require("node:readline"));
@@ -111,9 +110,6 @@ async function runCli(argv = process.argv.slice(2)) {
111
110
  }
112
111
  return (0, quarantine_1.runQuarantineRestore)({ id: parsed.id, quarantineDir: parsed.quarantineDir, force: parsed.force });
113
112
  }
114
- if (argv[0] === 'dashboard') {
115
- return runDashboard();
116
- }
117
113
  if (argv[0] !== 'init') {
118
114
  printUsage();
119
115
  return 1;
@@ -147,7 +143,6 @@ Usage:
147
143
  sapper-ai quarantine list List quarantined files
148
144
  sapper-ai quarantine restore <id> [--force] Restore quarantined file by id
149
145
  sapper-ai init Interactive setup wizard
150
- sapper-ai dashboard Launch web dashboard
151
146
  sapper-ai --help Show this help
152
147
 
153
148
  Learn more: https://github.com/sapper-ai/sapperai
@@ -524,43 +519,6 @@ async function resolveScanOptions(args) {
524
519
  }
525
520
  return { ...common, targets: [cwd], deep: true, ai, scopeLabel: 'Current + subdirectories' };
526
521
  }
527
- async function runDashboard() {
528
- const configuredPort = process.env.PORT;
529
- const standalonePort = configuredPort ?? '4100';
530
- const devPort = configuredPort ?? '3000';
531
- try {
532
- // eslint-disable-next-line @typescript-eslint/no-var-requires
533
- const startPath = require.resolve('@sapper-ai/dashboard/bin/start');
534
- process.env.PORT = standalonePort;
535
- // eslint-disable-next-line @typescript-eslint/no-var-requires
536
- require(startPath);
537
- return await new Promise((resolveExit) => {
538
- const stop = () => resolveExit(0);
539
- process.once('SIGINT', stop);
540
- process.once('SIGTERM', stop);
541
- });
542
- }
543
- catch {
544
- }
545
- const webDir = (0, node_path_1.resolve)(__dirname, '../../../apps/web');
546
- if ((0, node_fs_1.existsSync)((0, node_path_1.resolve)(webDir, 'package.json'))) {
547
- console.log(`\n SapperAI Dashboard (dev): http://localhost:${devPort}/dashboard\n`);
548
- console.log(' Press Ctrl+C to stop\n');
549
- const child = (0, node_child_process_1.spawn)('npx', ['next', 'dev', '--port', devPort], {
550
- cwd: webDir,
551
- stdio: 'inherit',
552
- env: process.env,
553
- });
554
- process.on('SIGINT', () => child.kill('SIGINT'));
555
- process.on('SIGTERM', () => child.kill('SIGTERM'));
556
- return await new Promise((resolveExit) => {
557
- child.on('close', (code) => resolveExit(code ?? 0));
558
- });
559
- }
560
- console.error('\n Install @sapper-ai/dashboard for standalone mode:');
561
- console.error(' pnpm add @sapper-ai/dashboard\n');
562
- return 1;
563
- }
564
522
  async function runInitWizard() {
565
523
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
566
524
  const ask = (q) => new Promise((res) => rl.question(q, res));
@@ -1 +1 @@
1
- {"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../src/scan.ts"],"names":[],"mappings":"AAuBA,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,GAAG,CAAC,EAAE,OAAO,CAAA;IACb,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,EAAE,CAAC,EAAE,OAAO,CAAA;IACZ,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAiBD,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,KAAK,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,EAAE,EAAE,OAAO,CAAA;IACX,OAAO,EAAE;QACP,cAAc,EAAE,OAAO,CAAA;KACxB,CAAA;IACD,OAAO,EAAE;QACP,UAAU,EAAE,MAAM,CAAA;QAClB,aAAa,EAAE,MAAM,CAAA;QACrB,YAAY,EAAE,MAAM,CAAA;QACpB,YAAY,EAAE,MAAM,CAAA;QACpB,kBAAkB,EAAE,MAAM,CAAA;QAC1B,wBAAwB,EAAE,MAAM,CAAA;QAChC,OAAO,EAAE,MAAM,CAAA;KAChB,CAAA;IACD,QAAQ,EAAE,KAAK,CAAC;QACd,QAAQ,EAAE,MAAM,CAAA;QAChB,IAAI,EAAE,MAAM,CAAA;QACZ,UAAU,EAAE,MAAM,CAAA;QAClB,MAAM,EAAE,MAAM,CAAA;QACd,QAAQ,EAAE,MAAM,EAAE,CAAA;QAClB,OAAO,EAAE,MAAM,EAAE,CAAA;QACjB,OAAO,EAAE,MAAM,CAAA;QACf,SAAS,EAAE,MAAM,EAAE,CAAA;QACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;QACzB,WAAW,EAAE,KAAK,CAAC;YACjB,KAAK,EAAE,MAAM,CAAA;YACb,QAAQ,EAAE,MAAM,GAAG,QAAQ,CAAA;YAC3B,SAAS,EAAE,MAAM,CAAA;YACjB,OAAO,EAAE,MAAM,CAAA;SAChB,CAAC,CAAA;KACH,CAAC,CAAA;CACH;AA8XD,wBAAsB,OAAO,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,CAiQxE"}
1
+ {"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../src/scan.ts"],"names":[],"mappings":"AAwBA,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,GAAG,CAAC,EAAE,OAAO,CAAA;IACb,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,EAAE,CAAC,EAAE,OAAO,CAAA;IACZ,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAiBD,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,KAAK,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,EAAE,EAAE,OAAO,CAAA;IACX,OAAO,EAAE;QACP,cAAc,EAAE,OAAO,CAAA;KACxB,CAAA;IACD,OAAO,EAAE;QACP,UAAU,EAAE,MAAM,CAAA;QAClB,aAAa,EAAE,MAAM,CAAA;QACrB,YAAY,EAAE,MAAM,CAAA;QACpB,YAAY,EAAE,MAAM,CAAA;QACpB,kBAAkB,EAAE,MAAM,CAAA;QAC1B,wBAAwB,EAAE,MAAM,CAAA;QAChC,OAAO,EAAE,MAAM,CAAA;KAChB,CAAA;IACD,QAAQ,EAAE,KAAK,CAAC;QACd,QAAQ,EAAE,MAAM,CAAA;QAChB,IAAI,EAAE,MAAM,CAAA;QACZ,UAAU,EAAE,MAAM,CAAA;QAClB,MAAM,EAAE,MAAM,CAAA;QACd,QAAQ,EAAE,MAAM,EAAE,CAAA;QAClB,OAAO,EAAE,MAAM,EAAE,CAAA;QACjB,OAAO,EAAE,MAAM,CAAA;QACf,SAAS,EAAE,MAAM,EAAE,CAAA;QACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;QACzB,WAAW,EAAE,KAAK,CAAC;YACjB,KAAK,EAAE,MAAM,CAAA;YACb,QAAQ,EAAE,MAAM,GAAG,QAAQ,CAAA;YAC3B,SAAS,EAAE,MAAM,CAAA;YACjB,OAAO,EAAE,MAAM,CAAA;SAChB,CAAC,CAAA;KACH,CAAC,CAAA;CACH;AA8XD,wBAAsB,OAAO,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,CA6RxE"}
package/dist/scan.js CHANGED
@@ -40,6 +40,7 @@ const node_path_1 = require("node:path");
40
40
  const core_1 = require("@sapper-ai/core");
41
41
  const auth_1 = require("./auth");
42
42
  const presets_1 = require("./presets");
43
+ const progress_1 = require("./utils/progress");
43
44
  const format_1 = require("./utils/format");
44
45
  const repoRoot_1 = require("./utils/repoRoot");
45
46
  const SYSTEM_SCAN_PATHS = (() => {
@@ -398,22 +399,38 @@ async function runScan(options = {}) {
398
399
  const scannedFindings = [];
399
400
  let scannedFiles = 0;
400
401
  let skippedEmptyOrUnreadable = 0;
401
- for (const filePath of eligibleFiles) {
402
- const result = await scanFile(filePath, policy, scanner, detectors, fix, quarantineManager);
403
- if (result.skipReason === 'empty_or_unreadable') {
404
- skippedEmptyOrUnreadable += 1;
405
- continue;
406
- }
407
- if (result.scanned && result.decision) {
408
- scannedFiles += 1;
409
- scannedFindings.push({
410
- filePath,
411
- decision: result.decision,
412
- quarantinedId: result.quarantinedId,
413
- source: aiEnabled ? 'rules' : undefined,
414
- });
402
+ const rulesProgress = (0, progress_1.createProgressBar)({
403
+ label: aiEnabled ? 'Phase 1 rules' : 'Scan',
404
+ total: eligibleFiles.length,
405
+ colors,
406
+ });
407
+ rulesProgress.start();
408
+ try {
409
+ for (const filePath of eligibleFiles) {
410
+ try {
411
+ const result = await scanFile(filePath, policy, scanner, detectors, fix, quarantineManager);
412
+ if (result.skipReason === 'empty_or_unreadable') {
413
+ skippedEmptyOrUnreadable += 1;
414
+ continue;
415
+ }
416
+ if (result.scanned && result.decision) {
417
+ scannedFiles += 1;
418
+ scannedFindings.push({
419
+ filePath,
420
+ decision: result.decision,
421
+ quarantinedId: result.quarantinedId,
422
+ source: aiEnabled ? 'rules' : undefined,
423
+ });
424
+ }
425
+ }
426
+ finally {
427
+ rulesProgress.tick(filePath);
428
+ }
415
429
  }
416
430
  }
431
+ finally {
432
+ rulesProgress.done();
433
+ }
417
434
  let aiTargetsCount = 0;
418
435
  if (aiEnabled && llmConfig) {
419
436
  const suspiciousFindings = scannedFindings.filter((f) => f.decision.risk >= 0.5);
@@ -427,54 +444,68 @@ async function runScan(options = {}) {
427
444
  }
428
445
  const aiPolicy = { ...policy, llm: llmConfig, detectors: detectorsList };
429
446
  const aiDetectors = (0, core_1.createDetectors)({ policy: aiPolicy, preferredDetectors: ['rules', 'llm'] });
430
- for (const finding of aiTargets) {
431
- try {
432
- const raw = await readFileIfPresent(finding.filePath);
433
- if (!raw)
434
- continue;
435
- const surface = (0, core_1.normalizeSurfaceText)(raw);
436
- const targetType = (0, core_1.classifyTargetType)(finding.filePath);
437
- const id = `${targetType}:${(0, core_1.buildEntryName)(finding.filePath)}`;
438
- const aiDecision = await scanner.scanTool(id, surface, aiPolicy, aiDetectors, {
439
- scanSource: 'file_surface',
440
- sourcePath: finding.filePath,
441
- sourceType: targetType,
442
- });
443
- const aiDominates = aiDecision.risk > finding.decision.risk;
444
- const mergedReasons = aiDominates
445
- ? uniq([...aiDecision.reasons, ...finding.decision.reasons])
446
- : uniq([...finding.decision.reasons, ...aiDecision.reasons]);
447
- const existingEvidence = finding.decision.evidence;
448
- const mergedEvidence = [...existingEvidence];
449
- for (const ev of aiDecision.evidence) {
450
- if (!mergedEvidence.some((e) => e.detectorId === ev.detectorId)) {
451
- mergedEvidence.push(ev);
447
+ const aiProgress = (0, progress_1.createProgressBar)({
448
+ label: 'Phase 2 ai',
449
+ total: aiTargets.length,
450
+ colors,
451
+ });
452
+ aiProgress.start();
453
+ try {
454
+ for (const finding of aiTargets) {
455
+ try {
456
+ const raw = await readFileIfPresent(finding.filePath);
457
+ if (!raw)
458
+ continue;
459
+ const surface = (0, core_1.normalizeSurfaceText)(raw);
460
+ const targetType = (0, core_1.classifyTargetType)(finding.filePath);
461
+ const id = `${targetType}:${(0, core_1.buildEntryName)(finding.filePath)}`;
462
+ const aiDecision = await scanner.scanTool(id, surface, aiPolicy, aiDetectors, {
463
+ scanSource: 'file_surface',
464
+ sourcePath: finding.filePath,
465
+ sourceType: targetType,
466
+ });
467
+ const aiDominates = aiDecision.risk > finding.decision.risk;
468
+ const mergedReasons = aiDominates
469
+ ? uniq([...aiDecision.reasons, ...finding.decision.reasons])
470
+ : uniq([...finding.decision.reasons, ...aiDecision.reasons]);
471
+ const existingEvidence = finding.decision.evidence;
472
+ const mergedEvidence = [...existingEvidence];
473
+ for (const ev of aiDecision.evidence) {
474
+ if (!mergedEvidence.some((e) => e.detectorId === ev.detectorId)) {
475
+ mergedEvidence.push(ev);
476
+ }
452
477
  }
453
- }
454
- const nextDecision = {
455
- ...finding.decision,
456
- reasons: mergedReasons,
457
- evidence: mergedEvidence,
458
- };
459
- if (aiDominates) {
460
- finding.source = 'ai';
461
- finding.decision = {
462
- ...nextDecision,
463
- action: aiDecision.action,
464
- risk: aiDecision.risk,
465
- confidence: aiDecision.confidence,
478
+ const nextDecision = {
479
+ ...finding.decision,
480
+ reasons: mergedReasons,
481
+ evidence: mergedEvidence,
466
482
  };
483
+ if (aiDominates) {
484
+ finding.source = 'ai';
485
+ finding.decision = {
486
+ ...nextDecision,
487
+ action: aiDecision.action,
488
+ risk: aiDecision.risk,
489
+ confidence: aiDecision.confidence,
490
+ };
491
+ }
492
+ else {
493
+ finding.source = finding.source ?? 'rules';
494
+ finding.decision = nextDecision;
495
+ }
496
+ finding.aiAnalysis =
497
+ aiDecision.reasons.find((r) => !r.startsWith('Detected pattern:')) ?? null;
467
498
  }
468
- else {
469
- finding.source = finding.source ?? 'rules';
470
- finding.decision = nextDecision;
499
+ catch {
500
+ }
501
+ finally {
502
+ aiProgress.tick(finding.filePath);
471
503
  }
472
- finding.aiAnalysis =
473
- aiDecision.reasons.find((r) => !r.startsWith('Detected pattern:')) ?? null;
474
- }
475
- catch {
476
504
  }
477
505
  }
506
+ finally {
507
+ aiProgress.done();
508
+ }
478
509
  }
479
510
  }
480
511
  const scopeLabel = options.scopeLabel ??
@@ -0,0 +1,40 @@
1
+ import type { Colors } from './format';
2
+ export interface ProgressStream {
3
+ isTTY?: boolean;
4
+ columns?: number;
5
+ write(text: string): boolean;
6
+ }
7
+ export interface ProgressBarOptions {
8
+ label: string;
9
+ total: number;
10
+ colors: Colors;
11
+ stream?: ProgressStream;
12
+ now?: () => number;
13
+ minIntervalMs?: number;
14
+ minBarWidth?: number;
15
+ maxBarWidth?: number;
16
+ }
17
+ export declare class ProgressBar {
18
+ private readonly options;
19
+ private readonly stream;
20
+ private readonly now;
21
+ private readonly minIntervalMs;
22
+ private readonly minBarWidth;
23
+ private readonly maxBarWidth;
24
+ private readonly enabled;
25
+ private current;
26
+ private detail;
27
+ private lastRenderAt;
28
+ private rendered;
29
+ private finished;
30
+ constructor(options: ProgressBarOptions);
31
+ start(detail?: string): void;
32
+ tick(detail?: string): void;
33
+ done(detail?: string): void;
34
+ private render;
35
+ private isComplete;
36
+ private renderLine;
37
+ private renderBar;
38
+ }
39
+ export declare function createProgressBar(options: ProgressBarOptions): ProgressBar;
40
+ //# sourceMappingURL=progress.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"progress.d.ts","sourceRoot":"","sources":["../../src/utils/progress.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AAGtC,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAA;CAC7B;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,cAAc,CAAA;IACvB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAClB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAUD,qBAAa,WAAW;IAaV,OAAO,CAAC,QAAQ,CAAC,OAAO;IAZpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgB;IACvC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAc;IAClC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAQ;IACtC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAQ;IACpC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAQ;IACpC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,OAAO,CAAI;IACnB,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,YAAY,CAAI;IACxB,OAAO,CAAC,QAAQ,CAAQ;IACxB,OAAO,CAAC,QAAQ,CAAQ;gBAEK,OAAO,EAAE,kBAAkB;IASxD,KAAK,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI;IAM5B,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI;IAU3B,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI;IAW3B,OAAO,CAAC,MAAM;IAYd,OAAO,CAAC,UAAU;IAOlB,OAAO,CAAC,UAAU;IA4BlB,OAAO,CAAC,SAAS;CAoBlB;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,GAAG,WAAW,CAE1E"}
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ProgressBar = void 0;
4
+ exports.createProgressBar = createProgressBar;
5
+ const format_1 = require("./format");
6
+ const DEFAULT_MIN_INTERVAL_MS = 100;
7
+ const DEFAULT_MIN_BAR_WIDTH = 10;
8
+ const DEFAULT_MAX_BAR_WIDTH = 40;
9
+ function clamp(value, min, max) {
10
+ return Math.min(max, Math.max(min, value));
11
+ }
12
+ class ProgressBar {
13
+ constructor(options) {
14
+ this.options = options;
15
+ this.current = 0;
16
+ this.lastRenderAt = 0;
17
+ this.rendered = false;
18
+ this.finished = false;
19
+ this.stream = options.stream ?? process.stdout;
20
+ this.now = options.now ?? Date.now;
21
+ this.minIntervalMs = options.minIntervalMs ?? DEFAULT_MIN_INTERVAL_MS;
22
+ this.minBarWidth = options.minBarWidth ?? DEFAULT_MIN_BAR_WIDTH;
23
+ this.maxBarWidth = options.maxBarWidth ?? DEFAULT_MAX_BAR_WIDTH;
24
+ this.enabled = this.stream.isTTY === true;
25
+ }
26
+ start(detail) {
27
+ if (!this.enabled)
28
+ return;
29
+ this.detail = detail;
30
+ this.render(true);
31
+ }
32
+ tick(detail) {
33
+ if (!this.enabled || this.finished)
34
+ return;
35
+ this.current += 1;
36
+ if (this.options.total > 0 && this.current > this.options.total) {
37
+ this.current = this.options.total;
38
+ }
39
+ this.detail = detail;
40
+ this.render(false);
41
+ }
42
+ done(detail) {
43
+ if (!this.enabled || this.finished)
44
+ return;
45
+ if (this.options.total > 0) {
46
+ this.current = this.options.total;
47
+ }
48
+ this.detail = detail;
49
+ this.render(true);
50
+ this.stream.write('\n');
51
+ this.finished = true;
52
+ }
53
+ render(force) {
54
+ const now = this.now();
55
+ if (!force && this.rendered && now - this.lastRenderAt < this.minIntervalMs && !this.isComplete()) {
56
+ return;
57
+ }
58
+ const line = this.renderLine();
59
+ this.stream.write(`\r\x1b[2K${line}`);
60
+ this.lastRenderAt = now;
61
+ this.rendered = true;
62
+ }
63
+ isComplete() {
64
+ if (this.options.total <= 0) {
65
+ return true;
66
+ }
67
+ return this.current >= this.options.total;
68
+ }
69
+ renderLine() {
70
+ const total = this.options.total;
71
+ const safeCurrent = total > 0 ? clamp(this.current, 0, total) : 0;
72
+ const ratio = total > 0 ? safeCurrent / total : 1;
73
+ const percent = Math.round(ratio * 100);
74
+ const countText = `${safeCurrent}/${total}`;
75
+ const percentText = `${String(percent).padStart(3, ' ')}%`;
76
+ const suffix = `${countText} ${percentText}`;
77
+ const columns = this.stream.columns ?? 80;
78
+ const label = this.options.label;
79
+ const baseReserved = 2 + label.length + 1 + 2 + 2 + 1 + suffix.length;
80
+ const maxFitWidth = Math.max(1, columns - baseReserved);
81
+ const availableForBar = maxFitWidth >= this.minBarWidth ? Math.min(this.maxBarWidth, maxFitWidth) : maxFitWidth;
82
+ const bar = this.renderBar(availableForBar, ratio);
83
+ const detailPrefix = ' ';
84
+ const detailMaxWidth = Math.max(0, columns - (2 + label.length + 1 + 2 + availableForBar + 2 + suffix.length + detailPrefix.length));
85
+ const rawDetail = this.detail ? (0, format_1.truncateToWidth)(this.detail, detailMaxWidth) : '';
86
+ const detail = rawDetail
87
+ ? `${detailPrefix}${this.options.colors.dim}${rawDetail}${this.options.colors.reset}`
88
+ : '';
89
+ return ` ${label} [${bar}] ${suffix}${detail}`;
90
+ }
91
+ renderBar(width, ratio) {
92
+ if (width <= 0)
93
+ return '';
94
+ const safeRatio = clamp(ratio, 0, 1);
95
+ const filled = Math.floor(safeRatio * width);
96
+ if (filled >= width) {
97
+ const body = '='.repeat(width);
98
+ return this.options.colors.olive ? `${this.options.colors.olive}${body}${this.options.colors.reset}` : body;
99
+ }
100
+ const visibleFilled = Math.max(1, filled);
101
+ const leadCount = Math.max(0, visibleFilled - 1);
102
+ const lead = '='.repeat(leadCount);
103
+ const head = '>';
104
+ const tail = '-'.repeat(Math.max(0, width - visibleFilled));
105
+ const filledPart = this.options.colors.olive ? `${this.options.colors.olive}${lead}${head}${this.options.colors.reset}` : `${lead}${head}`;
106
+ const emptyPart = this.options.colors.dim ? `${this.options.colors.dim}${tail}${this.options.colors.reset}` : tail;
107
+ return `${filledPart}${emptyPart}`;
108
+ }
109
+ }
110
+ exports.ProgressBar = ProgressBar;
111
+ function createProgressBar(options) {
112
+ return new ProgressBar(options);
113
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sapper-ai",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "AI security guardrails - single install, sensible defaults",
5
5
  "keywords": [
6
6
  "security",
@@ -45,14 +45,6 @@
45
45
  "@sapper-ai/mcp": "0.3.1",
46
46
  "@sapper-ai/types": "0.2.1"
47
47
  },
48
- "peerDependencies": {
49
- "@sapper-ai/openai": "^0.2.2"
50
- },
51
- "peerDependenciesMeta": {
52
- "@sapper-ai/openai": {
53
- "optional": true
54
- }
55
- },
56
48
  "devDependencies": {
57
49
  "@types/node": "^20.0.0",
58
50
  "typescript": "^5.3.0",