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 +1 -1
- package/dist/cli.js +0 -42
- package/dist/scan.d.ts.map +1 -1
- package/dist/scan.js +87 -56
- package/dist/utils/progress.d.ts +40 -0
- package/dist/utils/progress.d.ts.map +1 -0
- package/dist/utils/progress.js +113 -0
- package/package.json +1 -9
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";
|
|
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));
|
package/dist/scan.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../src/scan.ts"],"names":[],"mappings":"
|
|
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
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
filePath,
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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.
|
|
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",
|