sapper-ai 0.6.0 → 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/auth.d.ts +11 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +102 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +17 -45
- package/dist/postinstall.d.ts.map +1 -1
- package/dist/postinstall.js +11 -2
- package/dist/scan.d.ts +2 -0
- package/dist/scan.d.ts.map +1 -1
- package/dist/scan.js +167 -204
- package/dist/utils/format.d.ts +22 -0
- package/dist/utils/format.d.ts.map +1 -0
- package/dist/utils/format.js +97 -0
- package/dist/utils/fs.d.ts +3 -1
- package/dist/utils/fs.d.ts.map +1 -1
- package/dist/utils/fs.js +2 -2
- 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 +3 -10
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare function getAuthPath(): string;
|
|
2
|
+
export declare function loadOpenAiApiKey(options?: {
|
|
3
|
+
env?: NodeJS.ProcessEnv;
|
|
4
|
+
authPath?: string;
|
|
5
|
+
}): Promise<string | null>;
|
|
6
|
+
export declare function maskApiKey(apiKey: string): string;
|
|
7
|
+
export declare function promptAndSaveOpenAiApiKey(options?: {
|
|
8
|
+
authPath?: string;
|
|
9
|
+
mask?: string;
|
|
10
|
+
}): Promise<string | null>;
|
|
11
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAkBA,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED,wBAAsB,gBAAgB,CAAC,OAAO,GAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAkB3H;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAIjD;AAED,wBAAsB,yBAAyB,CAAC,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA2B1H"}
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.getAuthPath = getAuthPath;
|
|
40
|
+
exports.loadOpenAiApiKey = loadOpenAiApiKey;
|
|
41
|
+
exports.maskApiKey = maskApiKey;
|
|
42
|
+
exports.promptAndSaveOpenAiApiKey = promptAndSaveOpenAiApiKey;
|
|
43
|
+
const node_os_1 = require("node:os");
|
|
44
|
+
const node_path_1 = require("node:path");
|
|
45
|
+
const password_1 = __importDefault(require("@inquirer/password"));
|
|
46
|
+
const fs_1 = require("./utils/fs");
|
|
47
|
+
function isNonEmptyString(value) {
|
|
48
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
49
|
+
}
|
|
50
|
+
function getAuthPath() {
|
|
51
|
+
return (0, node_path_1.join)((0, node_os_1.homedir)(), '.sapperai', 'auth.json');
|
|
52
|
+
}
|
|
53
|
+
async function loadOpenAiApiKey(options = {}) {
|
|
54
|
+
const env = options.env ?? process.env;
|
|
55
|
+
const fromEnv = env.OPENAI_API_KEY;
|
|
56
|
+
if (isNonEmptyString(fromEnv)) {
|
|
57
|
+
return fromEnv.trim();
|
|
58
|
+
}
|
|
59
|
+
const authPath = options.authPath ?? getAuthPath();
|
|
60
|
+
const raw = await (0, fs_1.readFileIfExists)(authPath);
|
|
61
|
+
if (raw === null)
|
|
62
|
+
return null;
|
|
63
|
+
try {
|
|
64
|
+
const parsed = JSON.parse(raw);
|
|
65
|
+
const key = parsed.openai?.apiKey;
|
|
66
|
+
return isNonEmptyString(key) ? key.trim() : null;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function maskApiKey(apiKey) {
|
|
73
|
+
const trimmed = apiKey.trim();
|
|
74
|
+
if (trimmed.length <= 3)
|
|
75
|
+
return '***';
|
|
76
|
+
return `${trimmed.slice(0, 3)}${'█'.repeat(Math.min(24, Math.max(6, trimmed.length - 3)))}`;
|
|
77
|
+
}
|
|
78
|
+
async function promptAndSaveOpenAiApiKey(options = {}) {
|
|
79
|
+
const key = await (0, password_1.default)({
|
|
80
|
+
message: 'Enter your API key:',
|
|
81
|
+
mask: options.mask ?? '█',
|
|
82
|
+
});
|
|
83
|
+
if (!isNonEmptyString(key))
|
|
84
|
+
return null;
|
|
85
|
+
const authPath = options.authPath ?? getAuthPath();
|
|
86
|
+
const payload = {
|
|
87
|
+
openai: {
|
|
88
|
+
apiKey: key.trim(),
|
|
89
|
+
savedAt: new Date().toISOString(),
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
await (0, fs_1.atomicWriteFile)(authPath, `${JSON.stringify(payload, null, 2)}\n`, { mode: 0o600 });
|
|
93
|
+
// Best-effort: ensure perms even if the file already existed with broader mode.
|
|
94
|
+
// On Windows this is typically a no-op; prefer env vars there.
|
|
95
|
+
try {
|
|
96
|
+
const { chmod } = await Promise.resolve().then(() => __importStar(require('node:fs/promises')));
|
|
97
|
+
await chmod(authPath, 0o600);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
}
|
|
101
|
+
return payload.openai.apiKey;
|
|
102
|
+
}
|
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;
|
|
@@ -133,7 +129,8 @@ Usage:
|
|
|
133
129
|
sapper-ai scan ./path Scan a specific file/directory
|
|
134
130
|
sapper-ai scan --policy ./sapperai.config.yaml Use explicit policy path (fatal if invalid)
|
|
135
131
|
sapper-ai scan --fix Quarantine blocked files
|
|
136
|
-
sapper-ai scan --ai Deep scan with AI analysis (
|
|
132
|
+
sapper-ai scan --ai Deep scan with AI analysis (OpenAI; prompts for key in a TTY)
|
|
133
|
+
sapper-ai scan --no-color Disable ANSI colors
|
|
137
134
|
sapper-ai scan --no-prompt Disable all prompts (CI-safe)
|
|
138
135
|
sapper-ai scan --harden After scan, offer to apply recommended hardening
|
|
139
136
|
sapper-ai scan --no-open Skip opening report in browser
|
|
@@ -146,7 +143,6 @@ Usage:
|
|
|
146
143
|
sapper-ai quarantine list List quarantined files
|
|
147
144
|
sapper-ai quarantine restore <id> [--force] Restore quarantined file by id
|
|
148
145
|
sapper-ai init Interactive setup wizard
|
|
149
|
-
sapper-ai dashboard Launch web dashboard
|
|
150
146
|
sapper-ai --help Show this help
|
|
151
147
|
|
|
152
148
|
Learn more: https://github.com/sapper-ai/sapperai
|
|
@@ -161,6 +157,7 @@ function parseScanArgs(argv) {
|
|
|
161
157
|
let ai = false;
|
|
162
158
|
let noSave = false;
|
|
163
159
|
let noOpen = false;
|
|
160
|
+
let noColor = false;
|
|
164
161
|
let noPrompt = false;
|
|
165
162
|
let harden = false;
|
|
166
163
|
for (let index = 0; index < argv.length; index += 1) {
|
|
@@ -190,6 +187,10 @@ function parseScanArgs(argv) {
|
|
|
190
187
|
ai = true;
|
|
191
188
|
continue;
|
|
192
189
|
}
|
|
190
|
+
if (arg === '--no-color') {
|
|
191
|
+
noColor = true;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
193
194
|
if (arg === '--no-prompt') {
|
|
194
195
|
noPrompt = true;
|
|
195
196
|
continue;
|
|
@@ -211,12 +212,13 @@ function parseScanArgs(argv) {
|
|
|
211
212
|
}
|
|
212
213
|
targets.push(arg);
|
|
213
214
|
}
|
|
214
|
-
return { targets, policyPath, fix, deep, system, ai, noSave, noOpen, noPrompt, harden };
|
|
215
|
+
return { targets, policyPath, fix, deep, system, ai, noSave, noOpen, noColor, noPrompt, harden };
|
|
215
216
|
}
|
|
216
217
|
function parseHardenArgs(argv) {
|
|
217
218
|
let apply = false;
|
|
218
219
|
let includeSystem = false;
|
|
219
220
|
let yes = false;
|
|
221
|
+
let noColor = false;
|
|
220
222
|
let noPrompt = false;
|
|
221
223
|
let force = false;
|
|
222
224
|
let workflowVersion;
|
|
@@ -240,6 +242,10 @@ function parseHardenArgs(argv) {
|
|
|
240
242
|
yes = true;
|
|
241
243
|
continue;
|
|
242
244
|
}
|
|
245
|
+
if (arg === '--no-color') {
|
|
246
|
+
noColor = true;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
243
249
|
if (arg === '--no-prompt') {
|
|
244
250
|
noPrompt = true;
|
|
245
251
|
continue;
|
|
@@ -268,6 +274,7 @@ function parseHardenArgs(argv) {
|
|
|
268
274
|
apply,
|
|
269
275
|
includeSystem,
|
|
270
276
|
yes,
|
|
277
|
+
noColor,
|
|
271
278
|
noPrompt,
|
|
272
279
|
force,
|
|
273
280
|
workflowVersion,
|
|
@@ -454,7 +461,7 @@ async function promptScanDepth() {
|
|
|
454
461
|
choices: [
|
|
455
462
|
{ name: 'Quick scan (rules only) Fast regex pattern matching', value: false },
|
|
456
463
|
{
|
|
457
|
-
name: 'Deep scan (rules + AI) AI-powered analysis (
|
|
464
|
+
name: 'Deep scan (rules + AI) AI-powered analysis (OpenAI)',
|
|
458
465
|
value: true,
|
|
459
466
|
},
|
|
460
467
|
],
|
|
@@ -468,6 +475,8 @@ async function resolveScanOptions(args) {
|
|
|
468
475
|
fix: args.fix,
|
|
469
476
|
noSave: args.noSave,
|
|
470
477
|
noOpen: args.noOpen,
|
|
478
|
+
noColor: args.noColor,
|
|
479
|
+
noPrompt: args.noPrompt,
|
|
471
480
|
policyPath: args.policyPath,
|
|
472
481
|
};
|
|
473
482
|
if (args.system) {
|
|
@@ -510,43 +519,6 @@ async function resolveScanOptions(args) {
|
|
|
510
519
|
}
|
|
511
520
|
return { ...common, targets: [cwd], deep: true, ai, scopeLabel: 'Current + subdirectories' };
|
|
512
521
|
}
|
|
513
|
-
async function runDashboard() {
|
|
514
|
-
const configuredPort = process.env.PORT;
|
|
515
|
-
const standalonePort = configuredPort ?? '4100';
|
|
516
|
-
const devPort = configuredPort ?? '3000';
|
|
517
|
-
try {
|
|
518
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
519
|
-
const startPath = require.resolve('@sapper-ai/dashboard/bin/start');
|
|
520
|
-
process.env.PORT = standalonePort;
|
|
521
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
522
|
-
require(startPath);
|
|
523
|
-
return await new Promise((resolveExit) => {
|
|
524
|
-
const stop = () => resolveExit(0);
|
|
525
|
-
process.once('SIGINT', stop);
|
|
526
|
-
process.once('SIGTERM', stop);
|
|
527
|
-
});
|
|
528
|
-
}
|
|
529
|
-
catch {
|
|
530
|
-
}
|
|
531
|
-
const webDir = (0, node_path_1.resolve)(__dirname, '../../../apps/web');
|
|
532
|
-
if ((0, node_fs_1.existsSync)((0, node_path_1.resolve)(webDir, 'package.json'))) {
|
|
533
|
-
console.log(`\n SapperAI Dashboard (dev): http://localhost:${devPort}/dashboard\n`);
|
|
534
|
-
console.log(' Press Ctrl+C to stop\n');
|
|
535
|
-
const child = (0, node_child_process_1.spawn)('npx', ['next', 'dev', '--port', devPort], {
|
|
536
|
-
cwd: webDir,
|
|
537
|
-
stdio: 'inherit',
|
|
538
|
-
env: process.env,
|
|
539
|
-
});
|
|
540
|
-
process.on('SIGINT', () => child.kill('SIGINT'));
|
|
541
|
-
process.on('SIGTERM', () => child.kill('SIGTERM'));
|
|
542
|
-
return await new Promise((resolveExit) => {
|
|
543
|
-
child.on('close', (code) => resolveExit(code ?? 0));
|
|
544
|
-
});
|
|
545
|
-
}
|
|
546
|
-
console.error('\n Install @sapper-ai/dashboard for standalone mode:');
|
|
547
|
-
console.error(' pnpm add @sapper-ai/dashboard\n');
|
|
548
|
-
return 1;
|
|
549
|
-
}
|
|
550
522
|
async function runInitWizard() {
|
|
551
523
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
552
524
|
const ask = (q) => new Promise((res) => rl.question(q, res));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"postinstall.d.ts","sourceRoot":"","sources":["../src/postinstall.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"postinstall.d.ts","sourceRoot":"","sources":["../src/postinstall.ts"],"names":[],"mappings":"AAIA,wBAAgB,cAAc,IAAI,IAAI,CAYrC"}
|
package/dist/postinstall.js
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.runPostinstall = runPostinstall;
|
|
4
|
-
const
|
|
7
|
+
const package_json_1 = __importDefault(require("../package.json"));
|
|
8
|
+
const format_1 = require("./utils/format");
|
|
5
9
|
function runPostinstall() {
|
|
6
10
|
try {
|
|
7
|
-
|
|
11
|
+
const colors = (0, format_1.createColors)();
|
|
12
|
+
const version = typeof package_json_1.default.version === 'string' ? package_json_1.default.version : '';
|
|
13
|
+
const name = colors.olive ? `${colors.olive}sapper-ai${colors.reset}` : 'sapper-ai';
|
|
14
|
+
const ver = version ? `${colors.dim}v${version}${colors.reset}` : '';
|
|
15
|
+
console.log(`\n ${name} ${ver}\n`);
|
|
16
|
+
console.log(' Run npx sapper-ai scan to get started.\n');
|
|
8
17
|
}
|
|
9
18
|
catch {
|
|
10
19
|
}
|
package/dist/scan.d.ts
CHANGED
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
|
@@ -38,12 +38,11 @@ const promises_1 = require("node:fs/promises");
|
|
|
38
38
|
const node_os_1 = require("node:os");
|
|
39
39
|
const node_path_1 = require("node:path");
|
|
40
40
|
const core_1 = require("@sapper-ai/core");
|
|
41
|
+
const auth_1 = require("./auth");
|
|
41
42
|
const presets_1 = require("./presets");
|
|
43
|
+
const progress_1 = require("./utils/progress");
|
|
44
|
+
const format_1 = require("./utils/format");
|
|
42
45
|
const repoRoot_1 = require("./utils/repoRoot");
|
|
43
|
-
const GREEN = '\x1b[32m';
|
|
44
|
-
const YELLOW = '\x1b[33m';
|
|
45
|
-
const RED = '\x1b[31m';
|
|
46
|
-
const RESET = '\x1b[0m';
|
|
47
46
|
const SYSTEM_SCAN_PATHS = (() => {
|
|
48
47
|
const home = (0, node_os_1.homedir)();
|
|
49
48
|
return [
|
|
@@ -134,84 +133,37 @@ async function collectFiles(targetPath, deep) {
|
|
|
134
133
|
}
|
|
135
134
|
return results;
|
|
136
135
|
}
|
|
137
|
-
function riskColor(risk) {
|
|
138
|
-
if (risk >= 0.8)
|
|
139
|
-
return RED;
|
|
140
|
-
if (risk >= 0.5)
|
|
141
|
-
return YELLOW;
|
|
142
|
-
return GREEN;
|
|
143
|
-
}
|
|
144
|
-
function stripAnsi(text) {
|
|
145
|
-
return text.replace(/\x1b\[[0-9;]*m/g, '');
|
|
146
|
-
}
|
|
147
|
-
function truncateToWidth(text, maxWidth) {
|
|
148
|
-
if (maxWidth <= 0) {
|
|
149
|
-
return '';
|
|
150
|
-
}
|
|
151
|
-
if (text.length <= maxWidth) {
|
|
152
|
-
return text;
|
|
153
|
-
}
|
|
154
|
-
if (maxWidth <= 3) {
|
|
155
|
-
return '.'.repeat(maxWidth);
|
|
156
|
-
}
|
|
157
|
-
return `...${text.slice(text.length - (maxWidth - 3))}`;
|
|
158
|
-
}
|
|
159
|
-
function renderProgressBar(current, total, width) {
|
|
160
|
-
const safeTotal = Math.max(1, total);
|
|
161
|
-
const pct = Math.floor((current / safeTotal) * 100);
|
|
162
|
-
const filled = Math.floor((current / safeTotal) * width);
|
|
163
|
-
const bar = '█'.repeat(filled) + '░'.repeat(Math.max(0, width - filled));
|
|
164
|
-
return ` ${bar} ${pct}% │ ${current}/${total} files`;
|
|
165
|
-
}
|
|
166
136
|
function extractPatternLabel(decision) {
|
|
167
137
|
const reason = decision.reasons[0];
|
|
168
138
|
if (!reason)
|
|
169
139
|
return 'threat';
|
|
170
140
|
return reason.startsWith('Detected pattern: ') ? reason.slice('Detected pattern: '.length) : reason;
|
|
171
141
|
}
|
|
172
|
-
function padRight(text, width) {
|
|
173
|
-
if (text.length >= width)
|
|
174
|
-
return text;
|
|
175
|
-
return text + ' '.repeat(width - text.length);
|
|
176
|
-
}
|
|
177
|
-
function padRightVisual(text, width) {
|
|
178
|
-
const visLen = stripAnsi(text).length;
|
|
179
|
-
if (visLen >= width)
|
|
180
|
-
return text;
|
|
181
|
-
return text + ' '.repeat(width - visLen);
|
|
182
|
-
}
|
|
183
|
-
function padLeft(text, width) {
|
|
184
|
-
if (text.length >= width)
|
|
185
|
-
return text;
|
|
186
|
-
return ' '.repeat(width - text.length) + text;
|
|
187
|
-
}
|
|
188
142
|
function renderFindingsTable(findings, opts) {
|
|
189
|
-
const rows = findings.map((f, i) => {
|
|
190
|
-
const file = f.filePath.startsWith(opts.cwd + '/') ? f.filePath.slice(opts.cwd.length + 1) : f.filePath;
|
|
191
|
-
const pattern = extractPatternLabel(f.decision);
|
|
192
|
-
const riskValue = f.decision.risk.toFixed(2);
|
|
193
|
-
const riskPlain = padLeft(riskValue, 4);
|
|
194
|
-
const risk = opts.color ? `${riskColor(f.decision.risk)}${riskPlain}${RESET}` : riskPlain;
|
|
195
|
-
return { idx: String(i + 1), file, risk, pattern };
|
|
196
|
-
});
|
|
197
|
-
const idxWidth = Math.max(1, ...rows.map((r) => r.idx.length));
|
|
198
143
|
const riskWidth = 4;
|
|
199
|
-
const patternWidth = Math.min(20, Math.max('Pattern'.length, ...
|
|
200
|
-
const
|
|
144
|
+
const patternWidth = Math.min(20, Math.max('Pattern'.length, ...findings.map((f) => extractPatternLabel(f.decision).length)));
|
|
145
|
+
const sourceWidth = opts.includeSource ? Math.max('Source'.length, 5) : 0;
|
|
201
146
|
const maxTableWidth = Math.max(60, Math.min(opts.columns || 80, 120));
|
|
147
|
+
const sepWidth = 2;
|
|
148
|
+
const baseWidth = 'File'.length + sepWidth + riskWidth + sepWidth + patternWidth + (opts.includeSource ? sepWidth + sourceWidth : 0);
|
|
202
149
|
const fileWidth = Math.max(20, Math.min(50, maxTableWidth - baseWidth));
|
|
203
|
-
const
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const
|
|
209
|
-
const pattern =
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
150
|
+
const headers = opts.includeSource ? ['File', 'Risk', 'Pattern', 'Source'] : ['File', 'Risk', 'Pattern'];
|
|
151
|
+
const rows = findings.map((f) => {
|
|
152
|
+
const relative = f.filePath.startsWith(opts.cwd + '/') ? f.filePath.slice(opts.cwd.length + 1) : f.filePath;
|
|
153
|
+
const file = (0, format_1.truncateToWidth)(relative, fileWidth);
|
|
154
|
+
const label = extractPatternLabel(f.decision);
|
|
155
|
+
const patternPlain = (0, format_1.truncateToWidth)(label, patternWidth);
|
|
156
|
+
const pattern = `${opts.colors.dim}${patternPlain}${opts.colors.reset}`;
|
|
157
|
+
const riskValue = f.decision.risk.toFixed(2);
|
|
158
|
+
const riskPlain = (0, format_1.padLeft)(riskValue, riskWidth);
|
|
159
|
+
const risk = `${(0, format_1.riskColor)(f.decision.risk, opts.colors)}${riskPlain}${opts.colors.reset}`;
|
|
160
|
+
if (!opts.includeSource) {
|
|
161
|
+
return [file, risk, pattern];
|
|
162
|
+
}
|
|
163
|
+
const src = f.source === 'ai' ? `${opts.colors.olive}ai${opts.colors.reset}` : `${opts.colors.dim}rules${opts.colors.reset}`;
|
|
164
|
+
return [file, risk, pattern, src];
|
|
165
|
+
});
|
|
166
|
+
return (0, format_1.table)(headers, rows, opts.colors);
|
|
215
167
|
}
|
|
216
168
|
function isThreat(decision, policy) {
|
|
217
169
|
const { riskThreshold, blockMinConfidence } = getThresholds(policy);
|
|
@@ -390,15 +342,34 @@ async function buildScanResult(params) {
|
|
|
390
342
|
}
|
|
391
343
|
async function runScan(options = {}) {
|
|
392
344
|
const cwd = process.cwd();
|
|
345
|
+
const colors = (0, format_1.createColors)({ noColor: options.noColor });
|
|
393
346
|
const policy = resolvePolicy(cwd, { policyPath: options.policyPath });
|
|
394
347
|
const fix = options.fix === true;
|
|
348
|
+
console.log(`\n${(0, format_1.header)('scan', colors)}\n`);
|
|
395
349
|
const aiEnabled = options.ai === true;
|
|
396
350
|
let llmConfig = null;
|
|
397
351
|
if (aiEnabled) {
|
|
398
|
-
|
|
352
|
+
let apiKey = await (0, auth_1.loadOpenAiApiKey)();
|
|
399
353
|
if (!apiKey) {
|
|
400
|
-
|
|
401
|
-
|
|
354
|
+
const canPrompt = options.noPrompt !== true && process.stdout.isTTY === true && process.stdin.isTTY === true;
|
|
355
|
+
if (!canPrompt) {
|
|
356
|
+
console.log(' Error: OPENAI_API_KEY environment variable is required for --ai mode.\n');
|
|
357
|
+
return 1;
|
|
358
|
+
}
|
|
359
|
+
console.log(' No OpenAI API key found.\n');
|
|
360
|
+
console.log(` ${colors.olive}Get one at https://platform.openai.com/api-keys${colors.reset}`);
|
|
361
|
+
console.log();
|
|
362
|
+
apiKey = await (0, auth_1.promptAndSaveOpenAiApiKey)();
|
|
363
|
+
if (!apiKey) {
|
|
364
|
+
console.log('\n Error: API key is required for --ai mode.\n');
|
|
365
|
+
return 1;
|
|
366
|
+
}
|
|
367
|
+
const authPath = (0, auth_1.getAuthPath)();
|
|
368
|
+
const home = (0, node_os_1.homedir)();
|
|
369
|
+
const displayAuthPath = authPath === home ? '~' : authPath.startsWith(home + '/') ? `~/${authPath.slice(home.length + 1)}` : authPath;
|
|
370
|
+
console.log();
|
|
371
|
+
console.log(`${colors.dim} Key saved to ${displayAuthPath}${colors.reset}`);
|
|
372
|
+
console.log();
|
|
402
373
|
}
|
|
403
374
|
llmConfig = { provider: 'openai', apiKey, model: 'gpt-4.1-mini' };
|
|
404
375
|
}
|
|
@@ -412,17 +383,6 @@ async function runScan(options = {}) {
|
|
|
412
383
|
const detectors = (0, core_1.createDetectors)({ policy, preferredDetectors: ['rules'] });
|
|
413
384
|
const quarantineDir = process.env.SAPPERAI_QUARANTINE_DIR;
|
|
414
385
|
const quarantineManager = quarantineDir ? new core_1.QuarantineManager({ quarantineDir }) : new core_1.QuarantineManager();
|
|
415
|
-
const isTTY = process.stdout.isTTY === true;
|
|
416
|
-
const color = isTTY;
|
|
417
|
-
const scopeLabel = options.scopeLabel ??
|
|
418
|
-
(options.system
|
|
419
|
-
? 'AI system scan'
|
|
420
|
-
: deep
|
|
421
|
-
? 'Current + subdirectories'
|
|
422
|
-
: 'Current directory only');
|
|
423
|
-
console.log('\n SapperAI Security Scanner\n');
|
|
424
|
-
console.log(` Scope: ${scopeLabel}`);
|
|
425
|
-
console.log();
|
|
426
386
|
const fileSet = new Set();
|
|
427
387
|
for (const target of targets) {
|
|
428
388
|
const files = await collectFiles(target, deep);
|
|
@@ -431,139 +391,137 @@ async function runScan(options = {}) {
|
|
|
431
391
|
}
|
|
432
392
|
}
|
|
433
393
|
const files = Array.from(fileSet).sort();
|
|
434
|
-
|
|
435
|
-
const eligibleByName =
|
|
436
|
-
|
|
394
|
+
const eligibleFiles = files.filter((f) => (0, core_1.isConfigLikeFile)(f));
|
|
395
|
+
const eligibleByName = eligibleFiles.length;
|
|
396
|
+
const skippedNotEligible = Math.max(0, files.length - eligibleByName);
|
|
397
|
+
console.log(`${colors.dim} Scanning ${eligibleByName} files...${colors.reset}`);
|
|
437
398
|
console.log();
|
|
438
|
-
if (aiEnabled) {
|
|
439
|
-
console.log(' Phase 1/2: Rules scan');
|
|
440
|
-
console.log();
|
|
441
|
-
}
|
|
442
399
|
const scannedFindings = [];
|
|
443
400
|
let scannedFiles = 0;
|
|
444
|
-
let eligibleFiles = 0;
|
|
445
|
-
let skippedNotEligible = 0;
|
|
446
401
|
let skippedEmptyOrUnreadable = 0;
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
+
}
|
|
458
425
|
}
|
|
459
|
-
|
|
460
|
-
|
|
426
|
+
finally {
|
|
427
|
+
rulesProgress.tick(filePath);
|
|
461
428
|
}
|
|
462
429
|
}
|
|
463
|
-
const result = await scanFile(filePath, policy, scanner, detectors, fix, quarantineManager);
|
|
464
|
-
if (result.skipReason === 'not_eligible') {
|
|
465
|
-
skippedNotEligible += 1;
|
|
466
|
-
continue;
|
|
467
|
-
}
|
|
468
|
-
eligibleFiles += 1;
|
|
469
|
-
if (result.skipReason === 'empty_or_unreadable') {
|
|
470
|
-
skippedEmptyOrUnreadable += 1;
|
|
471
|
-
continue;
|
|
472
|
-
}
|
|
473
|
-
if (result.scanned && result.decision) {
|
|
474
|
-
scannedFiles += 1;
|
|
475
|
-
scannedFindings.push({ filePath, decision: result.decision, quarantinedId: result.quarantinedId });
|
|
476
|
-
}
|
|
477
430
|
}
|
|
478
|
-
|
|
479
|
-
|
|
431
|
+
finally {
|
|
432
|
+
rulesProgress.done();
|
|
480
433
|
}
|
|
434
|
+
let aiTargetsCount = 0;
|
|
481
435
|
if (aiEnabled && llmConfig) {
|
|
482
436
|
const suspiciousFindings = scannedFindings.filter((f) => f.decision.risk >= 0.5);
|
|
483
437
|
const maxAiFiles = 50;
|
|
484
438
|
if (suspiciousFindings.length > 0) {
|
|
485
439
|
const aiTargets = suspiciousFindings.slice(0, maxAiFiles);
|
|
486
|
-
|
|
487
|
-
console.log(` Note: AI scan limited to ${maxAiFiles} files (${suspiciousFindings.length} suspicious)`);
|
|
488
|
-
}
|
|
489
|
-
console.log();
|
|
490
|
-
console.log(` Phase 2/2: AI deep scan (${aiTargets.length} files)`);
|
|
491
|
-
console.log();
|
|
440
|
+
aiTargetsCount = aiTargets.length;
|
|
492
441
|
const detectorsList = (policy.detectors ?? ['rules']).slice();
|
|
493
442
|
if (!detectorsList.includes('llm')) {
|
|
494
443
|
detectorsList.push('llm');
|
|
495
444
|
}
|
|
496
445
|
const aiPolicy = { ...policy, llm: llmConfig, detectors: detectorsList };
|
|
497
446
|
const aiDetectors = (0, core_1.createDetectors)({ policy: aiPolicy, preferredDetectors: ['rules', 'llm'] });
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
if (!mergedEvidence.some((e) => e.detectorId === ev.detectorId)) {
|
|
529
|
-
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
|
+
}
|
|
530
477
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
evidence: mergedEvidence,
|
|
536
|
-
};
|
|
537
|
-
if (aiDecision.risk > finding.decision.risk) {
|
|
538
|
-
finding.decision = {
|
|
539
|
-
...nextDecision,
|
|
540
|
-
action: aiDecision.action,
|
|
541
|
-
risk: aiDecision.risk,
|
|
542
|
-
confidence: aiDecision.confidence,
|
|
478
|
+
const nextDecision = {
|
|
479
|
+
...finding.decision,
|
|
480
|
+
reasons: mergedReasons,
|
|
481
|
+
evidence: mergedEvidence,
|
|
543
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;
|
|
544
498
|
}
|
|
545
|
-
|
|
546
|
-
|
|
499
|
+
catch {
|
|
500
|
+
}
|
|
501
|
+
finally {
|
|
502
|
+
aiProgress.tick(finding.filePath);
|
|
547
503
|
}
|
|
548
|
-
finding.aiAnalysis =
|
|
549
|
-
aiDecision.reasons.find((r) => !r.startsWith('Detected pattern:')) ?? null;
|
|
550
|
-
}
|
|
551
|
-
catch {
|
|
552
504
|
}
|
|
553
505
|
}
|
|
554
|
-
|
|
555
|
-
|
|
506
|
+
finally {
|
|
507
|
+
aiProgress.done();
|
|
556
508
|
}
|
|
557
509
|
}
|
|
558
510
|
}
|
|
511
|
+
const scopeLabel = options.scopeLabel ??
|
|
512
|
+
(options.system
|
|
513
|
+
? 'AI system scan'
|
|
514
|
+
: deep
|
|
515
|
+
? 'Current + subdirectories'
|
|
516
|
+
: 'Current directory only');
|
|
559
517
|
const skippedFiles = skippedNotEligible + skippedEmptyOrUnreadable;
|
|
560
518
|
const threats = scannedFindings.filter((f) => isThreat(f.decision, policy));
|
|
561
519
|
const scanResult = await buildScanResult({
|
|
562
520
|
scope: scopeLabel,
|
|
563
521
|
target: targets.join(', '),
|
|
564
522
|
ai: aiEnabled,
|
|
565
|
-
totalFiles:
|
|
566
|
-
eligibleFiles,
|
|
523
|
+
totalFiles: files.length,
|
|
524
|
+
eligibleFiles: eligibleByName,
|
|
567
525
|
scannedFiles,
|
|
568
526
|
skippedFiles,
|
|
569
527
|
skippedNotEligible,
|
|
@@ -581,8 +539,8 @@ async function runScan(options = {}) {
|
|
|
581
539
|
const html = generateHtmlReport(scanResult);
|
|
582
540
|
const htmlPath = (0, node_path_1.join)(scanDir, `${ts}.html`);
|
|
583
541
|
await (0, promises_1.writeFile)(htmlPath, html, 'utf8');
|
|
584
|
-
console.log(
|
|
585
|
-
console.log(
|
|
542
|
+
console.log(`${colors.dim} Saved to ${jsonPath}${colors.reset}`);
|
|
543
|
+
console.log(`${colors.dim} Report: ${htmlPath}${colors.reset}`);
|
|
586
544
|
console.log();
|
|
587
545
|
if (options.noOpen !== true) {
|
|
588
546
|
try {
|
|
@@ -599,28 +557,33 @@ async function runScan(options = {}) {
|
|
|
599
557
|
}
|
|
600
558
|
}
|
|
601
559
|
}
|
|
560
|
+
if (aiEnabled) {
|
|
561
|
+
const countWidth = Math.max(String(eligibleByName).length, String(aiTargetsCount).length);
|
|
562
|
+
const rulesName = `${colors.dim}rules${colors.reset}`;
|
|
563
|
+
const aiName = `${colors.olive}ai${colors.reset}`;
|
|
564
|
+
console.log(` Phase 1 ${(0, format_1.padRightVisual)(rulesName, 5)} ${(0, format_1.padLeft)(String(eligibleByName), countWidth)} files`);
|
|
565
|
+
console.log(` Phase 2 ${(0, format_1.padRightVisual)(aiName, 5)} ${(0, format_1.padLeft)(String(aiTargetsCount), countWidth)} files`);
|
|
566
|
+
console.log();
|
|
567
|
+
}
|
|
602
568
|
if (threats.length === 0) {
|
|
603
|
-
|
|
604
|
-
console.log(color ? `${GREEN}${msg}${RESET}` : msg);
|
|
569
|
+
console.log(` ${colors.olive}All clear — 0 threats in ${eligibleByName} files${colors.reset}`);
|
|
605
570
|
console.log();
|
|
571
|
+
return 0;
|
|
572
|
+
}
|
|
573
|
+
console.log(renderFindingsTable(threats, {
|
|
574
|
+
cwd,
|
|
575
|
+
columns: process.stdout.columns ?? 80,
|
|
576
|
+
colors,
|
|
577
|
+
includeSource: aiEnabled,
|
|
578
|
+
}));
|
|
579
|
+
console.log();
|
|
580
|
+
console.log(` ${threats.length} threats found in ${eligibleByName} files (${files.length} total)`);
|
|
581
|
+
console.log();
|
|
582
|
+
if (!fix) {
|
|
583
|
+
console.log(' Run npx sapper-ai scan --fix to quarantine.\n');
|
|
606
584
|
}
|
|
607
585
|
else {
|
|
608
|
-
const warn = ` ⚠ ${scannedFiles}/${eligibleFiles} eligible files scanned, ${threats.length} threats detected (${total} total files)`;
|
|
609
|
-
console.log(color ? `${RED}${warn}${RESET}` : warn);
|
|
610
586
|
console.log();
|
|
611
|
-
const tableLines = renderFindingsTable(threats, {
|
|
612
|
-
cwd,
|
|
613
|
-
columns: process.stdout.columns ?? 80,
|
|
614
|
-
color,
|
|
615
|
-
});
|
|
616
|
-
for (const line of tableLines) {
|
|
617
|
-
console.log(line);
|
|
618
|
-
}
|
|
619
|
-
console.log();
|
|
620
|
-
if (!fix) {
|
|
621
|
-
console.log(" Run 'npx sapper-ai scan --fix' to quarantine blocked files.");
|
|
622
|
-
console.log();
|
|
623
|
-
}
|
|
624
587
|
}
|
|
625
|
-
return
|
|
588
|
+
return 1;
|
|
626
589
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface Colors {
|
|
2
|
+
olive: string;
|
|
3
|
+
dim: string;
|
|
4
|
+
bold: string;
|
|
5
|
+
red: string;
|
|
6
|
+
yellow: string;
|
|
7
|
+
reset: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function createColors(options?: {
|
|
10
|
+
noColor?: boolean;
|
|
11
|
+
env?: NodeJS.ProcessEnv;
|
|
12
|
+
stdoutIsTTY?: boolean;
|
|
13
|
+
}): Colors;
|
|
14
|
+
export declare function header(command: string, colors: Colors): string;
|
|
15
|
+
export declare function riskColor(risk: number, colors: Colors): string;
|
|
16
|
+
export declare function stripAnsi(text: string): string;
|
|
17
|
+
export declare function truncateToWidth(text: string, maxWidth: number): string;
|
|
18
|
+
export declare function padRight(text: string, width: number): string;
|
|
19
|
+
export declare function padRightVisual(text: string, width: number): string;
|
|
20
|
+
export declare function padLeft(text: string, width: number): string;
|
|
21
|
+
export declare function table(headers: string[], rows: string[][], colors: Colors): string;
|
|
22
|
+
//# sourceMappingURL=format.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/utils/format.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,MAAM;IACrB,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;CACd;AAQD,wBAAgB,YAAY,CAC1B,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,CAAA;CAAO,GAClF,MAAM,CAkBR;AAGD,wBAAgB,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAG9D;AAGD,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAI9D;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9C;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAStE;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAG5D;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAIlE;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAG3D;AAID,wBAAgB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAsBjF"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createColors = createColors;
|
|
4
|
+
exports.header = header;
|
|
5
|
+
exports.riskColor = riskColor;
|
|
6
|
+
exports.stripAnsi = stripAnsi;
|
|
7
|
+
exports.truncateToWidth = truncateToWidth;
|
|
8
|
+
exports.padRight = padRight;
|
|
9
|
+
exports.padRightVisual = padRightVisual;
|
|
10
|
+
exports.padLeft = padLeft;
|
|
11
|
+
exports.table = table;
|
|
12
|
+
function supportsTruecolor(env) {
|
|
13
|
+
const value = env.COLORTERM?.toLowerCase();
|
|
14
|
+
if (!value)
|
|
15
|
+
return false;
|
|
16
|
+
return value.includes('truecolor') || value.includes('24bit');
|
|
17
|
+
}
|
|
18
|
+
function createColors(options = {}) {
|
|
19
|
+
const env = options.env ?? process.env;
|
|
20
|
+
const stdoutIsTTY = options.stdoutIsTTY ?? process.stdout.isTTY;
|
|
21
|
+
const disabled = env.NO_COLOR !== undefined || options.noColor === true || stdoutIsTTY !== true;
|
|
22
|
+
if (disabled) {
|
|
23
|
+
return { olive: '', dim: '', bold: '', red: '', yellow: '', reset: '' };
|
|
24
|
+
}
|
|
25
|
+
const olive = supportsTruecolor(env) ? '\x1b[38;2;107;142;35m' : '\x1b[32m';
|
|
26
|
+
return {
|
|
27
|
+
olive,
|
|
28
|
+
dim: '\x1b[2m',
|
|
29
|
+
bold: '\x1b[1m',
|
|
30
|
+
red: '\x1b[31m',
|
|
31
|
+
yellow: '\x1b[33m',
|
|
32
|
+
reset: '\x1b[0m',
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
// Header: " sapper-ai <command>"
|
|
36
|
+
function header(command, colors) {
|
|
37
|
+
const name = colors.olive ? `${colors.olive}sapper-ai${colors.reset}` : 'sapper-ai';
|
|
38
|
+
return ` ${name} ${command}`;
|
|
39
|
+
}
|
|
40
|
+
// Risk color by value (>= 0.8 red+bold, 0.5~0.8 yellow, < 0.5 dim)
|
|
41
|
+
function riskColor(risk, colors) {
|
|
42
|
+
if (risk >= 0.8)
|
|
43
|
+
return `${colors.bold}${colors.red}`;
|
|
44
|
+
if (risk >= 0.5)
|
|
45
|
+
return colors.yellow;
|
|
46
|
+
return colors.dim;
|
|
47
|
+
}
|
|
48
|
+
function stripAnsi(text) {
|
|
49
|
+
return text.replace(/\x1b\[[0-9;]*m/g, '');
|
|
50
|
+
}
|
|
51
|
+
function truncateToWidth(text, maxWidth) {
|
|
52
|
+
if (maxWidth <= 0)
|
|
53
|
+
return '';
|
|
54
|
+
if (text.length <= maxWidth)
|
|
55
|
+
return text;
|
|
56
|
+
if (maxWidth <= 3) {
|
|
57
|
+
return '.'.repeat(maxWidth);
|
|
58
|
+
}
|
|
59
|
+
return `...${text.slice(text.length - (maxWidth - 3))}`;
|
|
60
|
+
}
|
|
61
|
+
function padRight(text, width) {
|
|
62
|
+
if (text.length >= width)
|
|
63
|
+
return text;
|
|
64
|
+
return text + ' '.repeat(width - text.length);
|
|
65
|
+
}
|
|
66
|
+
function padRightVisual(text, width) {
|
|
67
|
+
const visLen = stripAnsi(text).length;
|
|
68
|
+
if (visLen >= width)
|
|
69
|
+
return text;
|
|
70
|
+
return text + ' '.repeat(width - visLen);
|
|
71
|
+
}
|
|
72
|
+
function padLeft(text, width) {
|
|
73
|
+
if (text.length >= width)
|
|
74
|
+
return text;
|
|
75
|
+
return ' '.repeat(width - text.length) + text;
|
|
76
|
+
}
|
|
77
|
+
// Aligned table (Vercel style, no box borders).
|
|
78
|
+
// ANSI-aware: stripAnsi based visible width.
|
|
79
|
+
function table(headers, rows, colors) {
|
|
80
|
+
const columnCount = headers.length;
|
|
81
|
+
const normalizedRows = rows.map((row) => {
|
|
82
|
+
const out = row.slice(0, columnCount);
|
|
83
|
+
while (out.length < columnCount)
|
|
84
|
+
out.push('');
|
|
85
|
+
return out;
|
|
86
|
+
});
|
|
87
|
+
const headerRow = headers.map((h) => (colors.dim ? `${colors.dim}${h}${colors.reset}` : h));
|
|
88
|
+
const all = [headerRow, ...normalizedRows];
|
|
89
|
+
const widths = headers.map((_, col) => Math.max(0, ...all.map((r) => stripAnsi(r[col] ?? '').length)));
|
|
90
|
+
const sep = ' ';
|
|
91
|
+
const lines = [];
|
|
92
|
+
lines.push(` ${headerRow.map((cell, i) => padRightVisual(cell, widths[i])).join(sep)}`.trimEnd());
|
|
93
|
+
for (const row of normalizedRows) {
|
|
94
|
+
lines.push(` ${row.map((cell, i) => padRightVisual(cell, widths[i])).join(sep)}`.trimEnd());
|
|
95
|
+
}
|
|
96
|
+
return lines.join('\n');
|
|
97
|
+
}
|
package/dist/utils/fs.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export declare function readFileIfExists(filePath: string): Promise<string | null>;
|
|
2
2
|
export declare function ensureDir(dirPath: string): Promise<void>;
|
|
3
3
|
export declare function backupFile(originalPath: string): Promise<string>;
|
|
4
|
-
export declare function atomicWriteFile(filePath: string, content: string
|
|
4
|
+
export declare function atomicWriteFile(filePath: string, content: string, options?: {
|
|
5
|
+
mode?: number;
|
|
6
|
+
}): Promise<void>;
|
|
5
7
|
//# sourceMappingURL=fs.d.ts.map
|
package/dist/utils/fs.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fs.d.ts","sourceRoot":"","sources":["../../src/utils/fs.ts"],"names":[],"mappings":"AAIA,wBAAsB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAM/E;AAED,wBAAsB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE9D;AAkBD,wBAAsB,UAAU,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAKtE;AAED,wBAAsB,eAAe,
|
|
1
|
+
{"version":3,"file":"fs.d.ts","sourceRoot":"","sources":["../../src/utils/fs.ts"],"names":[],"mappings":"AAIA,wBAAsB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAM/E;AAED,wBAAsB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE9D;AAkBD,wBAAsB,UAAU,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAKtE;AAED,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAO,GAC9B,OAAO,CAAC,IAAI,CAAC,CASf"}
|
package/dist/utils/fs.js
CHANGED
|
@@ -37,11 +37,11 @@ async function backupFile(originalPath) {
|
|
|
37
37
|
await (0, promises_1.copyFile)(originalPath, backupPath);
|
|
38
38
|
return backupPath;
|
|
39
39
|
}
|
|
40
|
-
async function atomicWriteFile(filePath, content) {
|
|
40
|
+
async function atomicWriteFile(filePath, content, options = {}) {
|
|
41
41
|
const dir = (0, node_path_1.dirname)(filePath);
|
|
42
42
|
await ensureDir(dir);
|
|
43
43
|
const tmpName = `.${(0, node_path_1.basename)(filePath)}.tmp.${process.pid}.${Date.now()}`;
|
|
44
44
|
const tmpPath = (0, node_path_1.join)(dir, tmpName);
|
|
45
|
-
await (0, promises_1.writeFile)(tmpPath, content, 'utf8');
|
|
45
|
+
await (0, promises_1.writeFile)(tmpPath, content, { encoding: 'utf8', mode: options.mode });
|
|
46
46
|
await (0, promises_1.rename)(tmpPath, filePath);
|
|
47
47
|
}
|
|
@@ -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",
|
|
@@ -39,19 +39,12 @@
|
|
|
39
39
|
"access": "public"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
+
"@inquirer/password": "^4.0.0",
|
|
42
43
|
"@inquirer/select": "^4.0.0",
|
|
43
44
|
"@sapper-ai/core": "0.2.2",
|
|
44
|
-
"@sapper-ai/mcp": "0.3.
|
|
45
|
+
"@sapper-ai/mcp": "0.3.1",
|
|
45
46
|
"@sapper-ai/types": "0.2.1"
|
|
46
47
|
},
|
|
47
|
-
"peerDependencies": {
|
|
48
|
-
"@sapper-ai/openai": "^0.2.2"
|
|
49
|
-
},
|
|
50
|
-
"peerDependenciesMeta": {
|
|
51
|
-
"@sapper-ai/openai": {
|
|
52
|
-
"optional": true
|
|
53
|
-
}
|
|
54
|
-
},
|
|
55
48
|
"devDependencies": {
|
|
56
49
|
"@types/node": "^20.0.0",
|
|
57
50
|
"typescript": "^5.3.0",
|