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 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 (requires OPENAI_API_KEY)
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 (requires OPENAI_API_KEY)',
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":"AAEA,wBAAgB,cAAc,IAAI,IAAI,CAKrC"}
1
+ {"version":3,"file":"postinstall.d.ts","sourceRoot":"","sources":["../src/postinstall.ts"],"names":[],"mappings":"AAIA,wBAAgB,cAAc,IAAI,IAAI,CAYrC"}
@@ -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 MESSAGE = "SapperAI installed. Run 'npx sapper-ai scan' and follow the prompts to harden your setup.";
7
+ const package_json_1 = __importDefault(require("../package.json"));
8
+ const format_1 = require("./utils/format");
5
9
  function runPostinstall() {
6
10
  try {
7
- console.log(MESSAGE);
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
@@ -8,6 +8,8 @@ export interface ScanOptions {
8
8
  ai?: boolean;
9
9
  noSave?: boolean;
10
10
  noOpen?: boolean;
11
+ noPrompt?: boolean;
12
+ noColor?: boolean;
11
13
  }
12
14
  export interface ScanResult {
13
15
  version: '1.0';
@@ -1 +1 @@
1
- {"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../src/scan.ts"],"names":[],"mappings":"AAqBA,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;CACjB;AAgBD,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;AAgcD,wBAAsB,OAAO,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,CAkRxE"}
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, ...rows.map((r) => r.pattern.length)));
200
- const baseWidth = 2 + idxWidth + 2 + 2 + riskWidth + 2 + 2 + patternWidth + 2;
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 top = ` ┌${'─'.repeat(idxWidth + 2)}┬${''.repeat(fileWidth + 2)}┬${''.repeat(riskWidth + 2)}┬${''.repeat(patternWidth + 2)}┐`;
204
- const header = ` │ ${padRight('#', idxWidth)} │ ${padRight('File', fileWidth)} ${padRight('Risk', riskWidth)} │ ${padRight('Pattern', patternWidth)} │`;
205
- const sep = ` ├${'─'.repeat(idxWidth + 2)}┼${''.repeat(fileWidth + 2)}┼${'─'.repeat(riskWidth + 2)}┼${'─'.repeat(patternWidth + 2)}┤`;
206
- const lines = [top, header, sep];
207
- for (const r of rows) {
208
- const file = truncateToWidth(r.file, fileWidth);
209
- const pattern = truncateToWidth(r.pattern, patternWidth);
210
- lines.push(` │ ${padRight(r.idx, idxWidth)} │ ${padRight(file, fileWidth)} │ ${padRightVisual(r.risk, riskWidth)} │ ${padRight(pattern, patternWidth)} │`);
211
- }
212
- const bottom = ` └${'─'.repeat(idxWidth + 2)}┴${'─'.repeat(fileWidth + 2)}┴${'─'.repeat(riskWidth + 2)}┴${'─'.repeat(patternWidth + 2)}┘`;
213
- lines.push(bottom);
214
- return lines;
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
- const apiKey = process.env.OPENAI_API_KEY;
351
+ let apiKey = await (0, auth_1.loadOpenAiApiKey)();
399
352
  if (!apiKey) {
400
- console.log('\n Error: OPENAI_API_KEY environment variable is required for --ai mode.\n');
401
- return 1;
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
- console.log(` Collecting files... ${files.length} files found`);
435
- const eligibleByName = files.filter((f) => (0, core_1.isConfigLikeFile)(f)).length;
436
- console.log(` Filter: config-like only (${eligibleByName} eligible / ${files.length} total)`);
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 total = files.length;
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({ filePath, decision: result.decision, quarantinedId: result.quarantinedId });
409
+ scannedFindings.push({
410
+ filePath,
411
+ decision: result.decision,
412
+ quarantinedId: result.quarantinedId,
413
+ source: aiEnabled ? 'rules' : undefined,
414
+ });
476
415
  }
477
416
  }
478
- if (isTTY && total > 0) {
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
- if (suspiciousFindings.length > maxAiFiles) {
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 (let i = 0; i < aiTargets.length; i += 1) {
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 mergedReasons = uniq([...finding.decision.reasons, ...aiDecision.reasons]);
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 (aiDecision.risk > finding.decision.risk) {
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: total,
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(` Saved to ${jsonPath}`);
585
- console.log(` Report: ${htmlPath}`);
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
- const msg = ` All clear — ${scannedFiles}/${eligibleFiles} eligible files scanned, 0 threats detected (${total} total files)`;
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 threats.length > 0 ? 1 : 0;
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
+ }
@@ -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): Promise<void>;
4
+ export declare function atomicWriteFile(filePath: string, content: string, options?: {
5
+ mode?: number;
6
+ }): Promise<void>;
5
7
  //# sourceMappingURL=fs.d.ts.map
@@ -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,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAStF"}
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.0",
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.0",
45
+ "@sapper-ai/mcp": "0.3.1",
45
46
  "@sapper-ai/types": "0.2.1"
46
47
  },
47
48
  "peerDependencies": {