sapper-ai 0.6.0 → 0.6.1
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.js +17 -3
- 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 +99 -167
- 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/package.json +3 -2
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.js
CHANGED
|
@@ -133,7 +133,8 @@ Usage:
|
|
|
133
133
|
sapper-ai scan ./path Scan a specific file/directory
|
|
134
134
|
sapper-ai scan --policy ./sapperai.config.yaml Use explicit policy path (fatal if invalid)
|
|
135
135
|
sapper-ai scan --fix Quarantine blocked files
|
|
136
|
-
sapper-ai scan --ai Deep scan with AI analysis (
|
|
136
|
+
sapper-ai scan --ai Deep scan with AI analysis (OpenAI; prompts for key in a TTY)
|
|
137
|
+
sapper-ai scan --no-color Disable ANSI colors
|
|
137
138
|
sapper-ai scan --no-prompt Disable all prompts (CI-safe)
|
|
138
139
|
sapper-ai scan --harden After scan, offer to apply recommended hardening
|
|
139
140
|
sapper-ai scan --no-open Skip opening report in browser
|
|
@@ -161,6 +162,7 @@ function parseScanArgs(argv) {
|
|
|
161
162
|
let ai = false;
|
|
162
163
|
let noSave = false;
|
|
163
164
|
let noOpen = false;
|
|
165
|
+
let noColor = false;
|
|
164
166
|
let noPrompt = false;
|
|
165
167
|
let harden = false;
|
|
166
168
|
for (let index = 0; index < argv.length; index += 1) {
|
|
@@ -190,6 +192,10 @@ function parseScanArgs(argv) {
|
|
|
190
192
|
ai = true;
|
|
191
193
|
continue;
|
|
192
194
|
}
|
|
195
|
+
if (arg === '--no-color') {
|
|
196
|
+
noColor = true;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
193
199
|
if (arg === '--no-prompt') {
|
|
194
200
|
noPrompt = true;
|
|
195
201
|
continue;
|
|
@@ -211,12 +217,13 @@ function parseScanArgs(argv) {
|
|
|
211
217
|
}
|
|
212
218
|
targets.push(arg);
|
|
213
219
|
}
|
|
214
|
-
return { targets, policyPath, fix, deep, system, ai, noSave, noOpen, noPrompt, harden };
|
|
220
|
+
return { targets, policyPath, fix, deep, system, ai, noSave, noOpen, noColor, noPrompt, harden };
|
|
215
221
|
}
|
|
216
222
|
function parseHardenArgs(argv) {
|
|
217
223
|
let apply = false;
|
|
218
224
|
let includeSystem = false;
|
|
219
225
|
let yes = false;
|
|
226
|
+
let noColor = false;
|
|
220
227
|
let noPrompt = false;
|
|
221
228
|
let force = false;
|
|
222
229
|
let workflowVersion;
|
|
@@ -240,6 +247,10 @@ function parseHardenArgs(argv) {
|
|
|
240
247
|
yes = true;
|
|
241
248
|
continue;
|
|
242
249
|
}
|
|
250
|
+
if (arg === '--no-color') {
|
|
251
|
+
noColor = true;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
243
254
|
if (arg === '--no-prompt') {
|
|
244
255
|
noPrompt = true;
|
|
245
256
|
continue;
|
|
@@ -268,6 +279,7 @@ function parseHardenArgs(argv) {
|
|
|
268
279
|
apply,
|
|
269
280
|
includeSystem,
|
|
270
281
|
yes,
|
|
282
|
+
noColor,
|
|
271
283
|
noPrompt,
|
|
272
284
|
force,
|
|
273
285
|
workflowVersion,
|
|
@@ -454,7 +466,7 @@ async function promptScanDepth() {
|
|
|
454
466
|
choices: [
|
|
455
467
|
{ name: 'Quick scan (rules only) Fast regex pattern matching', value: false },
|
|
456
468
|
{
|
|
457
|
-
name: 'Deep scan (rules + AI) AI-powered analysis (
|
|
469
|
+
name: 'Deep scan (rules + AI) AI-powered analysis (OpenAI)',
|
|
458
470
|
value: true,
|
|
459
471
|
},
|
|
460
472
|
],
|
|
@@ -468,6 +480,8 @@ async function resolveScanOptions(args) {
|
|
|
468
480
|
fix: args.fix,
|
|
469
481
|
noSave: args.noSave,
|
|
470
482
|
noOpen: args.noOpen,
|
|
483
|
+
noColor: args.noColor,
|
|
484
|
+
noPrompt: args.noPrompt,
|
|
471
485
|
policyPath: args.policyPath,
|
|
472
486
|
};
|
|
473
487
|
if (args.system) {
|
|
@@ -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":"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"}
|
package/dist/scan.js
CHANGED
|
@@ -38,12 +38,10 @@ 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 format_1 = require("./utils/format");
|
|
42
44
|
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
45
|
const SYSTEM_SCAN_PATHS = (() => {
|
|
48
46
|
const home = (0, node_os_1.homedir)();
|
|
49
47
|
return [
|
|
@@ -134,84 +132,37 @@ async function collectFiles(targetPath, deep) {
|
|
|
134
132
|
}
|
|
135
133
|
return results;
|
|
136
134
|
}
|
|
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
135
|
function extractPatternLabel(decision) {
|
|
167
136
|
const reason = decision.reasons[0];
|
|
168
137
|
if (!reason)
|
|
169
138
|
return 'threat';
|
|
170
139
|
return reason.startsWith('Detected pattern: ') ? reason.slice('Detected pattern: '.length) : reason;
|
|
171
140
|
}
|
|
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
141
|
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
142
|
const riskWidth = 4;
|
|
199
|
-
const patternWidth = Math.min(20, Math.max('Pattern'.length, ...
|
|
200
|
-
const
|
|
143
|
+
const patternWidth = Math.min(20, Math.max('Pattern'.length, ...findings.map((f) => extractPatternLabel(f.decision).length)));
|
|
144
|
+
const sourceWidth = opts.includeSource ? Math.max('Source'.length, 5) : 0;
|
|
201
145
|
const maxTableWidth = Math.max(60, Math.min(opts.columns || 80, 120));
|
|
146
|
+
const sepWidth = 2;
|
|
147
|
+
const baseWidth = 'File'.length + sepWidth + riskWidth + sepWidth + patternWidth + (opts.includeSource ? sepWidth + sourceWidth : 0);
|
|
202
148
|
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
|
-
|
|
149
|
+
const headers = opts.includeSource ? ['File', 'Risk', 'Pattern', 'Source'] : ['File', 'Risk', 'Pattern'];
|
|
150
|
+
const rows = findings.map((f) => {
|
|
151
|
+
const relative = f.filePath.startsWith(opts.cwd + '/') ? f.filePath.slice(opts.cwd.length + 1) : f.filePath;
|
|
152
|
+
const file = (0, format_1.truncateToWidth)(relative, fileWidth);
|
|
153
|
+
const label = extractPatternLabel(f.decision);
|
|
154
|
+
const patternPlain = (0, format_1.truncateToWidth)(label, patternWidth);
|
|
155
|
+
const pattern = `${opts.colors.dim}${patternPlain}${opts.colors.reset}`;
|
|
156
|
+
const riskValue = f.decision.risk.toFixed(2);
|
|
157
|
+
const riskPlain = (0, format_1.padLeft)(riskValue, riskWidth);
|
|
158
|
+
const risk = `${(0, format_1.riskColor)(f.decision.risk, opts.colors)}${riskPlain}${opts.colors.reset}`;
|
|
159
|
+
if (!opts.includeSource) {
|
|
160
|
+
return [file, risk, pattern];
|
|
161
|
+
}
|
|
162
|
+
const src = f.source === 'ai' ? `${opts.colors.olive}ai${opts.colors.reset}` : `${opts.colors.dim}rules${opts.colors.reset}`;
|
|
163
|
+
return [file, risk, pattern, src];
|
|
164
|
+
});
|
|
165
|
+
return (0, format_1.table)(headers, rows, opts.colors);
|
|
215
166
|
}
|
|
216
167
|
function isThreat(decision, policy) {
|
|
217
168
|
const { riskThreshold, blockMinConfidence } = getThresholds(policy);
|
|
@@ -390,15 +341,34 @@ async function buildScanResult(params) {
|
|
|
390
341
|
}
|
|
391
342
|
async function runScan(options = {}) {
|
|
392
343
|
const cwd = process.cwd();
|
|
344
|
+
const colors = (0, format_1.createColors)({ noColor: options.noColor });
|
|
393
345
|
const policy = resolvePolicy(cwd, { policyPath: options.policyPath });
|
|
394
346
|
const fix = options.fix === true;
|
|
347
|
+
console.log(`\n${(0, format_1.header)('scan', colors)}\n`);
|
|
395
348
|
const aiEnabled = options.ai === true;
|
|
396
349
|
let llmConfig = null;
|
|
397
350
|
if (aiEnabled) {
|
|
398
|
-
|
|
351
|
+
let apiKey = await (0, auth_1.loadOpenAiApiKey)();
|
|
399
352
|
if (!apiKey) {
|
|
400
|
-
|
|
401
|
-
|
|
353
|
+
const canPrompt = options.noPrompt !== true && process.stdout.isTTY === true && process.stdin.isTTY === true;
|
|
354
|
+
if (!canPrompt) {
|
|
355
|
+
console.log(' Error: OPENAI_API_KEY environment variable is required for --ai mode.\n');
|
|
356
|
+
return 1;
|
|
357
|
+
}
|
|
358
|
+
console.log(' No OpenAI API key found.\n');
|
|
359
|
+
console.log(` ${colors.olive}Get one at https://platform.openai.com/api-keys${colors.reset}`);
|
|
360
|
+
console.log();
|
|
361
|
+
apiKey = await (0, auth_1.promptAndSaveOpenAiApiKey)();
|
|
362
|
+
if (!apiKey) {
|
|
363
|
+
console.log('\n Error: API key is required for --ai mode.\n');
|
|
364
|
+
return 1;
|
|
365
|
+
}
|
|
366
|
+
const authPath = (0, auth_1.getAuthPath)();
|
|
367
|
+
const home = (0, node_os_1.homedir)();
|
|
368
|
+
const displayAuthPath = authPath === home ? '~' : authPath.startsWith(home + '/') ? `~/${authPath.slice(home.length + 1)}` : authPath;
|
|
369
|
+
console.log();
|
|
370
|
+
console.log(`${colors.dim} Key saved to ${displayAuthPath}${colors.reset}`);
|
|
371
|
+
console.log();
|
|
402
372
|
}
|
|
403
373
|
llmConfig = { provider: 'openai', apiKey, model: 'gpt-4.1-mini' };
|
|
404
374
|
}
|
|
@@ -412,17 +382,6 @@ async function runScan(options = {}) {
|
|
|
412
382
|
const detectors = (0, core_1.createDetectors)({ policy, preferredDetectors: ['rules'] });
|
|
413
383
|
const quarantineDir = process.env.SAPPERAI_QUARANTINE_DIR;
|
|
414
384
|
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
385
|
const fileSet = new Set();
|
|
427
386
|
for (const target of targets) {
|
|
428
387
|
const files = await collectFiles(target, deep);
|
|
@@ -431,84 +390,44 @@ async function runScan(options = {}) {
|
|
|
431
390
|
}
|
|
432
391
|
}
|
|
433
392
|
const files = Array.from(fileSet).sort();
|
|
434
|
-
|
|
435
|
-
const eligibleByName =
|
|
436
|
-
|
|
393
|
+
const eligibleFiles = files.filter((f) => (0, core_1.isConfigLikeFile)(f));
|
|
394
|
+
const eligibleByName = eligibleFiles.length;
|
|
395
|
+
const skippedNotEligible = Math.max(0, files.length - eligibleByName);
|
|
396
|
+
console.log(`${colors.dim} Scanning ${eligibleByName} files...${colors.reset}`);
|
|
437
397
|
console.log();
|
|
438
|
-
if (aiEnabled) {
|
|
439
|
-
console.log(' Phase 1/2: Rules scan');
|
|
440
|
-
console.log();
|
|
441
|
-
}
|
|
442
398
|
const scannedFindings = [];
|
|
443
399
|
let scannedFiles = 0;
|
|
444
|
-
let eligibleFiles = 0;
|
|
445
|
-
let skippedNotEligible = 0;
|
|
446
400
|
let skippedEmptyOrUnreadable = 0;
|
|
447
|
-
const
|
|
448
|
-
const progressWidth = Math.max(10, Math.min(30, (process.stdout.columns ?? 80) - 30));
|
|
449
|
-
for (let i = 0; i < files.length; i += 1) {
|
|
450
|
-
const filePath = files[i];
|
|
451
|
-
if (isTTY && total > 0) {
|
|
452
|
-
const bar = renderProgressBar(i + 1, total, progressWidth);
|
|
453
|
-
const label = ' Scanning: ';
|
|
454
|
-
const maxPath = Math.max(10, (process.stdout.columns ?? 80) - stripAnsi(bar).length - label.length);
|
|
455
|
-
const scanning = `${label}${truncateToWidth(filePath, maxPath)}`;
|
|
456
|
-
if (i === 0) {
|
|
457
|
-
process.stdout.write(`${bar}\n${scanning}\n`);
|
|
458
|
-
}
|
|
459
|
-
else {
|
|
460
|
-
process.stdout.write(`\x1b[2A\x1b[2K\r${bar}\n\x1b[2K\r${scanning}\n`);
|
|
461
|
-
}
|
|
462
|
-
}
|
|
401
|
+
for (const filePath of eligibleFiles) {
|
|
463
402
|
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
403
|
if (result.skipReason === 'empty_or_unreadable') {
|
|
470
404
|
skippedEmptyOrUnreadable += 1;
|
|
471
405
|
continue;
|
|
472
406
|
}
|
|
473
407
|
if (result.scanned && result.decision) {
|
|
474
408
|
scannedFiles += 1;
|
|
475
|
-
scannedFindings.push({
|
|
409
|
+
scannedFindings.push({
|
|
410
|
+
filePath,
|
|
411
|
+
decision: result.decision,
|
|
412
|
+
quarantinedId: result.quarantinedId,
|
|
413
|
+
source: aiEnabled ? 'rules' : undefined,
|
|
414
|
+
});
|
|
476
415
|
}
|
|
477
416
|
}
|
|
478
|
-
|
|
479
|
-
process.stdout.write('\x1b[2A\x1b[2K\r\x1b[1B\x1b[2K\r');
|
|
480
|
-
}
|
|
417
|
+
let aiTargetsCount = 0;
|
|
481
418
|
if (aiEnabled && llmConfig) {
|
|
482
419
|
const suspiciousFindings = scannedFindings.filter((f) => f.decision.risk >= 0.5);
|
|
483
420
|
const maxAiFiles = 50;
|
|
484
421
|
if (suspiciousFindings.length > 0) {
|
|
485
422
|
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();
|
|
423
|
+
aiTargetsCount = aiTargets.length;
|
|
492
424
|
const detectorsList = (policy.detectors ?? ['rules']).slice();
|
|
493
425
|
if (!detectorsList.includes('llm')) {
|
|
494
426
|
detectorsList.push('llm');
|
|
495
427
|
}
|
|
496
428
|
const aiPolicy = { ...policy, llm: llmConfig, detectors: detectorsList };
|
|
497
429
|
const aiDetectors = (0, core_1.createDetectors)({ policy: aiPolicy, preferredDetectors: ['rules', 'llm'] });
|
|
498
|
-
for (
|
|
499
|
-
const finding = aiTargets[i];
|
|
500
|
-
if (isTTY) {
|
|
501
|
-
const bar = renderProgressBar(i + 1, aiTargets.length, progressWidth);
|
|
502
|
-
const label = ' Analyzing: ';
|
|
503
|
-
const maxPath = Math.max(10, (process.stdout.columns ?? 80) - stripAnsi(bar).length - label.length);
|
|
504
|
-
const scanning = `${label}${truncateToWidth(finding.filePath, maxPath)}`;
|
|
505
|
-
if (i === 0) {
|
|
506
|
-
process.stdout.write(`${bar}\n${scanning}\n`);
|
|
507
|
-
}
|
|
508
|
-
else {
|
|
509
|
-
process.stdout.write(`\x1b[2A\x1b[2K\r${bar}\n\x1b[2K\r${scanning}\n`);
|
|
510
|
-
}
|
|
511
|
-
}
|
|
430
|
+
for (const finding of aiTargets) {
|
|
512
431
|
try {
|
|
513
432
|
const raw = await readFileIfPresent(finding.filePath);
|
|
514
433
|
if (!raw)
|
|
@@ -521,7 +440,10 @@ async function runScan(options = {}) {
|
|
|
521
440
|
sourcePath: finding.filePath,
|
|
522
441
|
sourceType: targetType,
|
|
523
442
|
});
|
|
524
|
-
const
|
|
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]);
|
|
525
447
|
const existingEvidence = finding.decision.evidence;
|
|
526
448
|
const mergedEvidence = [...existingEvidence];
|
|
527
449
|
for (const ev of aiDecision.evidence) {
|
|
@@ -534,7 +456,8 @@ async function runScan(options = {}) {
|
|
|
534
456
|
reasons: mergedReasons,
|
|
535
457
|
evidence: mergedEvidence,
|
|
536
458
|
};
|
|
537
|
-
if (
|
|
459
|
+
if (aiDominates) {
|
|
460
|
+
finding.source = 'ai';
|
|
538
461
|
finding.decision = {
|
|
539
462
|
...nextDecision,
|
|
540
463
|
action: aiDecision.action,
|
|
@@ -543,6 +466,7 @@ async function runScan(options = {}) {
|
|
|
543
466
|
};
|
|
544
467
|
}
|
|
545
468
|
else {
|
|
469
|
+
finding.source = finding.source ?? 'rules';
|
|
546
470
|
finding.decision = nextDecision;
|
|
547
471
|
}
|
|
548
472
|
finding.aiAnalysis =
|
|
@@ -551,19 +475,22 @@ async function runScan(options = {}) {
|
|
|
551
475
|
catch {
|
|
552
476
|
}
|
|
553
477
|
}
|
|
554
|
-
if (isTTY && aiTargets.length > 0) {
|
|
555
|
-
process.stdout.write('\x1b[2A\x1b[2K\r\x1b[1B\x1b[2K\r');
|
|
556
|
-
}
|
|
557
478
|
}
|
|
558
479
|
}
|
|
480
|
+
const scopeLabel = options.scopeLabel ??
|
|
481
|
+
(options.system
|
|
482
|
+
? 'AI system scan'
|
|
483
|
+
: deep
|
|
484
|
+
? 'Current + subdirectories'
|
|
485
|
+
: 'Current directory only');
|
|
559
486
|
const skippedFiles = skippedNotEligible + skippedEmptyOrUnreadable;
|
|
560
487
|
const threats = scannedFindings.filter((f) => isThreat(f.decision, policy));
|
|
561
488
|
const scanResult = await buildScanResult({
|
|
562
489
|
scope: scopeLabel,
|
|
563
490
|
target: targets.join(', '),
|
|
564
491
|
ai: aiEnabled,
|
|
565
|
-
totalFiles:
|
|
566
|
-
eligibleFiles,
|
|
492
|
+
totalFiles: files.length,
|
|
493
|
+
eligibleFiles: eligibleByName,
|
|
567
494
|
scannedFiles,
|
|
568
495
|
skippedFiles,
|
|
569
496
|
skippedNotEligible,
|
|
@@ -581,8 +508,8 @@ async function runScan(options = {}) {
|
|
|
581
508
|
const html = generateHtmlReport(scanResult);
|
|
582
509
|
const htmlPath = (0, node_path_1.join)(scanDir, `${ts}.html`);
|
|
583
510
|
await (0, promises_1.writeFile)(htmlPath, html, 'utf8');
|
|
584
|
-
console.log(
|
|
585
|
-
console.log(
|
|
511
|
+
console.log(`${colors.dim} Saved to ${jsonPath}${colors.reset}`);
|
|
512
|
+
console.log(`${colors.dim} Report: ${htmlPath}${colors.reset}`);
|
|
586
513
|
console.log();
|
|
587
514
|
if (options.noOpen !== true) {
|
|
588
515
|
try {
|
|
@@ -599,28 +526,33 @@ async function runScan(options = {}) {
|
|
|
599
526
|
}
|
|
600
527
|
}
|
|
601
528
|
}
|
|
529
|
+
if (aiEnabled) {
|
|
530
|
+
const countWidth = Math.max(String(eligibleByName).length, String(aiTargetsCount).length);
|
|
531
|
+
const rulesName = `${colors.dim}rules${colors.reset}`;
|
|
532
|
+
const aiName = `${colors.olive}ai${colors.reset}`;
|
|
533
|
+
console.log(` Phase 1 ${(0, format_1.padRightVisual)(rulesName, 5)} ${(0, format_1.padLeft)(String(eligibleByName), countWidth)} files`);
|
|
534
|
+
console.log(` Phase 2 ${(0, format_1.padRightVisual)(aiName, 5)} ${(0, format_1.padLeft)(String(aiTargetsCount), countWidth)} files`);
|
|
535
|
+
console.log();
|
|
536
|
+
}
|
|
602
537
|
if (threats.length === 0) {
|
|
603
|
-
|
|
604
|
-
console.log(color ? `${GREEN}${msg}${RESET}` : msg);
|
|
538
|
+
console.log(` ${colors.olive}All clear — 0 threats in ${eligibleByName} files${colors.reset}`);
|
|
605
539
|
console.log();
|
|
540
|
+
return 0;
|
|
541
|
+
}
|
|
542
|
+
console.log(renderFindingsTable(threats, {
|
|
543
|
+
cwd,
|
|
544
|
+
columns: process.stdout.columns ?? 80,
|
|
545
|
+
colors,
|
|
546
|
+
includeSource: aiEnabled,
|
|
547
|
+
}));
|
|
548
|
+
console.log();
|
|
549
|
+
console.log(` ${threats.length} threats found in ${eligibleByName} files (${files.length} total)`);
|
|
550
|
+
console.log();
|
|
551
|
+
if (!fix) {
|
|
552
|
+
console.log(' Run npx sapper-ai scan --fix to quarantine.\n');
|
|
606
553
|
}
|
|
607
554
|
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
555
|
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
556
|
}
|
|
625
|
-
return
|
|
557
|
+
return 1;
|
|
626
558
|
}
|
|
@@ -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
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sapper-ai",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "AI security guardrails - single install, sensible defaults",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"security",
|
|
@@ -39,9 +39,10 @@
|
|
|
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
48
|
"peerDependencies": {
|