@pretense/cli 0.3.0 → 0.3.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/index.js +1417 -183
- package/dist/index.js.map +1 -1
- package/package.json +6 -22
- package/bin/pretense +0 -2
package/dist/index.js
CHANGED
|
@@ -2,15 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import
|
|
6
|
-
import { readFileSync as
|
|
7
|
-
import {
|
|
5
|
+
import chalk7 from "chalk";
|
|
6
|
+
import { readFileSync as readFileSync6, existsSync as existsSync6, readdirSync, statSync, openSync, readSync as readSync2, closeSync } from "fs";
|
|
7
|
+
import { readFile, stat as fsStat, open as fsOpen } from "fs/promises";
|
|
8
|
+
import { resolve as resolve2, extname, join as join5, basename } from "path";
|
|
9
|
+
import os from "os";
|
|
8
10
|
|
|
9
11
|
// src/types.ts
|
|
10
12
|
var DEFAULT_CONFIG = {
|
|
11
13
|
port: 9339,
|
|
12
14
|
targetApis: ["https://api.anthropic.com", "https://api.openai.com"],
|
|
13
|
-
languages: ["typescript", "javascript", "python", "go", "java"],
|
|
15
|
+
languages: ["typescript", "javascript", "python", "go", "java", "csharp", "ruby", "rust"],
|
|
14
16
|
mutationRules: [
|
|
15
17
|
{ tokenType: "Variable" /* Variable */, prefix: "_v" },
|
|
16
18
|
{ tokenType: "Function" /* Function */, prefix: "_fn" },
|
|
@@ -262,24 +264,186 @@ var JAVA_KEYWORDS = /* @__PURE__ */ new Set([
|
|
|
262
264
|
"permits",
|
|
263
265
|
"yield"
|
|
264
266
|
]);
|
|
267
|
+
var CSHARP_KEYWORDS = /* @__PURE__ */ new Set([
|
|
268
|
+
"class",
|
|
269
|
+
"interface",
|
|
270
|
+
"namespace",
|
|
271
|
+
"public",
|
|
272
|
+
"private",
|
|
273
|
+
"protected",
|
|
274
|
+
"internal",
|
|
275
|
+
"static",
|
|
276
|
+
"void",
|
|
277
|
+
"return",
|
|
278
|
+
"new",
|
|
279
|
+
"using",
|
|
280
|
+
"override",
|
|
281
|
+
"sealed",
|
|
282
|
+
"abstract",
|
|
283
|
+
"virtual",
|
|
284
|
+
"readonly",
|
|
285
|
+
"const",
|
|
286
|
+
"var",
|
|
287
|
+
"int",
|
|
288
|
+
"string",
|
|
289
|
+
"bool",
|
|
290
|
+
"double",
|
|
291
|
+
"float",
|
|
292
|
+
"object",
|
|
293
|
+
"null",
|
|
294
|
+
"true",
|
|
295
|
+
"false",
|
|
296
|
+
"if",
|
|
297
|
+
"else",
|
|
298
|
+
"for",
|
|
299
|
+
"foreach",
|
|
300
|
+
"while",
|
|
301
|
+
"do",
|
|
302
|
+
"switch",
|
|
303
|
+
"case",
|
|
304
|
+
"break",
|
|
305
|
+
"continue",
|
|
306
|
+
"try",
|
|
307
|
+
"catch",
|
|
308
|
+
"finally",
|
|
309
|
+
"throw",
|
|
310
|
+
"async",
|
|
311
|
+
"await",
|
|
312
|
+
"get",
|
|
313
|
+
"set",
|
|
314
|
+
"partial",
|
|
315
|
+
"enum",
|
|
316
|
+
"struct",
|
|
317
|
+
"delegate",
|
|
318
|
+
"event"
|
|
319
|
+
]);
|
|
320
|
+
var RUBY_KEYWORDS = /* @__PURE__ */ new Set([
|
|
321
|
+
"def",
|
|
322
|
+
"class",
|
|
323
|
+
"module",
|
|
324
|
+
"end",
|
|
325
|
+
"do",
|
|
326
|
+
"if",
|
|
327
|
+
"else",
|
|
328
|
+
"elsif",
|
|
329
|
+
"unless",
|
|
330
|
+
"while",
|
|
331
|
+
"until",
|
|
332
|
+
"for",
|
|
333
|
+
"begin",
|
|
334
|
+
"rescue",
|
|
335
|
+
"ensure",
|
|
336
|
+
"raise",
|
|
337
|
+
"return",
|
|
338
|
+
"yield",
|
|
339
|
+
"self",
|
|
340
|
+
"super",
|
|
341
|
+
"true",
|
|
342
|
+
"false",
|
|
343
|
+
"nil",
|
|
344
|
+
"and",
|
|
345
|
+
"or",
|
|
346
|
+
"not",
|
|
347
|
+
"in",
|
|
348
|
+
"then",
|
|
349
|
+
"case",
|
|
350
|
+
"when",
|
|
351
|
+
"puts",
|
|
352
|
+
"print",
|
|
353
|
+
"require",
|
|
354
|
+
"require_relative",
|
|
355
|
+
"include",
|
|
356
|
+
"extend",
|
|
357
|
+
"attr_accessor",
|
|
358
|
+
"attr_reader",
|
|
359
|
+
"attr_writer",
|
|
360
|
+
"initialize",
|
|
361
|
+
"new",
|
|
362
|
+
"public",
|
|
363
|
+
"private",
|
|
364
|
+
"protected"
|
|
365
|
+
]);
|
|
366
|
+
var RUST_KEYWORDS = /* @__PURE__ */ new Set([
|
|
367
|
+
"fn",
|
|
368
|
+
"struct",
|
|
369
|
+
"enum",
|
|
370
|
+
"trait",
|
|
371
|
+
"impl",
|
|
372
|
+
"mod",
|
|
373
|
+
"pub",
|
|
374
|
+
"let",
|
|
375
|
+
"mut",
|
|
376
|
+
"const",
|
|
377
|
+
"type",
|
|
378
|
+
"use",
|
|
379
|
+
"crate",
|
|
380
|
+
"self",
|
|
381
|
+
"super",
|
|
382
|
+
"move",
|
|
383
|
+
"ref",
|
|
384
|
+
"match",
|
|
385
|
+
"if",
|
|
386
|
+
"else",
|
|
387
|
+
"loop",
|
|
388
|
+
"while",
|
|
389
|
+
"for",
|
|
390
|
+
"return",
|
|
391
|
+
"break",
|
|
392
|
+
"continue",
|
|
393
|
+
"where",
|
|
394
|
+
"as",
|
|
395
|
+
"in",
|
|
396
|
+
"true",
|
|
397
|
+
"false",
|
|
398
|
+
"unsafe",
|
|
399
|
+
"extern",
|
|
400
|
+
"dyn",
|
|
401
|
+
"box",
|
|
402
|
+
"static",
|
|
403
|
+
"async",
|
|
404
|
+
"await",
|
|
405
|
+
"Box",
|
|
406
|
+
"String",
|
|
407
|
+
"Vec",
|
|
408
|
+
"Option",
|
|
409
|
+
"Result",
|
|
410
|
+
"Ok",
|
|
411
|
+
"Err",
|
|
412
|
+
"Some",
|
|
413
|
+
"None"
|
|
414
|
+
]);
|
|
265
415
|
function detectLanguage(code) {
|
|
266
416
|
if (/:[\s]*\w+[\s]*[;,{)]/.test(code) || /import.*from\s+['"]/.test(code) || /interface\s+\w+/.test(code)) return "typescript";
|
|
267
417
|
if (/\bdef\s+\w+\s*\(/.test(code) || /\bimport\s+\w+/.test(code) && /:$/.test(code)) return "python";
|
|
268
418
|
if (/\bfunc\s+\w+\s*\(/.test(code) || /\bpackage\s+\w+/.test(code)) return "go";
|
|
269
419
|
if (/\bpublic\s+class\s+\w+/.test(code) || /\bimport\s+java\./.test(code)) return "java";
|
|
420
|
+
if (/\bnamespace\s+\w+/.test(code) || /\busing\s+System/.test(code)) return "csharp";
|
|
421
|
+
if (/\bfn\s+\w+\s*\(/.test(code) || /\blet\s+mut\s+\w+/.test(code)) return "rust";
|
|
422
|
+
if (/\bdef\s+\w+/.test(code) && /\bend\b/.test(code)) return "ruby";
|
|
270
423
|
if (/\bconst\s+\w+\s*=/.test(code) || /\bfunction\s+\w+\s*\(/.test(code)) return "javascript";
|
|
271
424
|
return "typescript";
|
|
272
425
|
}
|
|
273
|
-
function
|
|
274
|
-
|
|
275
|
-
for (let i = 0; i <
|
|
276
|
-
if (code
|
|
426
|
+
function buildLineIndex(code) {
|
|
427
|
+
const offsets = [];
|
|
428
|
+
for (let i = 0; i < code.length; i++) {
|
|
429
|
+
if (code.charCodeAt(i) === 10) offsets.push(i);
|
|
277
430
|
}
|
|
278
|
-
return
|
|
431
|
+
return offsets;
|
|
432
|
+
}
|
|
433
|
+
function lineFromIndex(lineOffsets, index) {
|
|
434
|
+
let lo = 0;
|
|
435
|
+
let hi = lineOffsets.length;
|
|
436
|
+
while (lo < hi) {
|
|
437
|
+
const mid = lo + hi >> 1;
|
|
438
|
+
if (lineOffsets[mid] < index) lo = mid + 1;
|
|
439
|
+
else hi = mid;
|
|
440
|
+
}
|
|
441
|
+
return lo + 1;
|
|
279
442
|
}
|
|
280
443
|
function scanTypeScript(code) {
|
|
281
444
|
const tokens = [];
|
|
282
445
|
if (!code.trim()) return tokens;
|
|
446
|
+
const lineOffsets = buildLineIndex(code);
|
|
283
447
|
const patterns = [
|
|
284
448
|
// Single-line comment
|
|
285
449
|
{ re: /\/\/[^\n]*/g, type: "Comment" /* Comment */ },
|
|
@@ -322,7 +486,7 @@ function scanTypeScript(code) {
|
|
|
322
486
|
value: captureValue,
|
|
323
487
|
start: captureStart,
|
|
324
488
|
end: captureStart + captureValue.length,
|
|
325
|
-
line:
|
|
489
|
+
line: lineFromIndex(lineOffsets, captureStart)
|
|
326
490
|
});
|
|
327
491
|
}
|
|
328
492
|
}
|
|
@@ -331,6 +495,7 @@ function scanTypeScript(code) {
|
|
|
331
495
|
function scanPython(code) {
|
|
332
496
|
const tokens = [];
|
|
333
497
|
if (!code.trim()) return tokens;
|
|
498
|
+
const lineOffsets = buildLineIndex(code);
|
|
334
499
|
const patterns = [
|
|
335
500
|
// Triple-quoted docstrings
|
|
336
501
|
{ re: /"""[\s\S]*?"""|'''[\s\S]*?'''/g, type: "Comment" /* Comment */ },
|
|
@@ -368,7 +533,7 @@ function scanPython(code) {
|
|
|
368
533
|
value: captureValue,
|
|
369
534
|
start: captureStart,
|
|
370
535
|
end: captureStart + captureValue.length,
|
|
371
|
-
line:
|
|
536
|
+
line: lineFromIndex(lineOffsets, captureStart)
|
|
372
537
|
});
|
|
373
538
|
}
|
|
374
539
|
}
|
|
@@ -377,6 +542,7 @@ function scanPython(code) {
|
|
|
377
542
|
function scanGo(code) {
|
|
378
543
|
const tokens = [];
|
|
379
544
|
if (!code.trim()) return tokens;
|
|
545
|
+
const lineOffsets = buildLineIndex(code);
|
|
380
546
|
const patterns = [
|
|
381
547
|
{ re: /\/\/[^\n]*/g, type: "Comment" /* Comment */ },
|
|
382
548
|
{ re: /\/\*[\s\S]*?\*\//g, type: "Comment" /* Comment */ },
|
|
@@ -413,7 +579,7 @@ function scanGo(code) {
|
|
|
413
579
|
value: captureValue,
|
|
414
580
|
start: captureStart,
|
|
415
581
|
end: captureStart + captureValue.length,
|
|
416
|
-
line:
|
|
582
|
+
line: lineFromIndex(lineOffsets, captureStart)
|
|
417
583
|
});
|
|
418
584
|
}
|
|
419
585
|
}
|
|
@@ -422,6 +588,7 @@ function scanGo(code) {
|
|
|
422
588
|
function scanJava(code) {
|
|
423
589
|
const tokens = [];
|
|
424
590
|
if (!code.trim()) return tokens;
|
|
591
|
+
const lineOffsets = buildLineIndex(code);
|
|
425
592
|
const patterns = [
|
|
426
593
|
{ re: /\/\/[^\n]*/g, type: "Comment" /* Comment */ },
|
|
427
594
|
{ re: /\/\*[\s\S]*?\*\//g, type: "Comment" /* Comment */ },
|
|
@@ -454,7 +621,137 @@ function scanJava(code) {
|
|
|
454
621
|
value: captureValue,
|
|
455
622
|
start: captureStart,
|
|
456
623
|
end: captureStart + captureValue.length,
|
|
457
|
-
line:
|
|
624
|
+
line: lineFromIndex(lineOffsets, captureStart)
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return deduplicate(tokens);
|
|
629
|
+
}
|
|
630
|
+
function scanCSharp(code) {
|
|
631
|
+
const tokens = [];
|
|
632
|
+
if (!code.trim()) return tokens;
|
|
633
|
+
const lineOffsets = buildLineIndex(code);
|
|
634
|
+
const patterns = [
|
|
635
|
+
{ re: /\/\/[^\n]*/g, type: "Comment" /* Comment */ },
|
|
636
|
+
{ re: /\/\*[\s\S]*?\*\//g, type: "Comment" /* Comment */ },
|
|
637
|
+
// Namespace/using import
|
|
638
|
+
{ re: /\busing\s+([\w.]+);/g, type: "Import" /* Import */, groupIndex: 1 },
|
|
639
|
+
// Class/interface/struct/enum names
|
|
640
|
+
{ re: /\b(?:class|interface|struct|enum)\s+([A-Za-z_][A-Za-z0-9_]*)/g, type: "Class" /* Class */, groupIndex: 1 },
|
|
641
|
+
// Method/property declaration
|
|
642
|
+
{ re: /\b(?:public|private|protected|internal|static|virtual|override|async)\s+(?:\w+\s+)+([a-zA-Z_]\w*)\s*\(/g, type: "Method" /* Method */, groupIndex: 1 },
|
|
643
|
+
// Local variable declarations
|
|
644
|
+
{ re: /\b(?:var|int|string|bool|double|float|object)\s+([a-zA-Z_]\w*)\b/g, type: "Variable" /* Variable */, groupIndex: 1 },
|
|
645
|
+
// Double-quoted strings
|
|
646
|
+
{ re: /"(?:[^"\\]|\\.)*"/g, type: "String" /* String */ },
|
|
647
|
+
// Verbatim strings
|
|
648
|
+
{ re: /@"(?:[^"]|"")*"/g, type: "String" /* String */ }
|
|
649
|
+
];
|
|
650
|
+
for (const { re, type, groupIndex } of patterns) {
|
|
651
|
+
re.lastIndex = 0;
|
|
652
|
+
let m;
|
|
653
|
+
while ((m = re.exec(code)) !== null) {
|
|
654
|
+
const fullMatch = m[0];
|
|
655
|
+
const captureValue = groupIndex !== void 0 ? m[groupIndex] ?? fullMatch : fullMatch;
|
|
656
|
+
const captureStart = groupIndex !== void 0 ? m.index + fullMatch.indexOf(captureValue) : m.index;
|
|
657
|
+
if (!captureValue || captureValue.trim() === "") continue;
|
|
658
|
+
if (type === "Variable" /* Variable */ || type === "Method" /* Method */ || type === "Class" /* Class */) {
|
|
659
|
+
if (CSHARP_KEYWORDS.has(captureValue)) continue;
|
|
660
|
+
if (captureValue.length <= 1) continue;
|
|
661
|
+
if (/^(String|Object|Integer|Boolean|Double|Float|List|Dictionary|Array|Exception|Task|Console|Math|Enum|Delegate)$/.test(captureValue)) continue;
|
|
662
|
+
}
|
|
663
|
+
tokens.push({
|
|
664
|
+
type,
|
|
665
|
+
value: captureValue,
|
|
666
|
+
start: captureStart,
|
|
667
|
+
end: captureStart + captureValue.length,
|
|
668
|
+
line: lineFromIndex(lineOffsets, captureStart)
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return deduplicate(tokens);
|
|
673
|
+
}
|
|
674
|
+
function scanRuby(code) {
|
|
675
|
+
const tokens = [];
|
|
676
|
+
if (!code.trim()) return tokens;
|
|
677
|
+
const lineOffsets = buildLineIndex(code);
|
|
678
|
+
const patterns = [
|
|
679
|
+
// Single-line comment
|
|
680
|
+
{ re: /#[^\n]*/g, type: "Comment" /* Comment */ },
|
|
681
|
+
// Method names
|
|
682
|
+
{ re: /\bdef\s+([a-zA-Z_]\w*[!?]?)/g, type: "Function" /* Function */, groupIndex: 1 },
|
|
683
|
+
// Class and module names
|
|
684
|
+
{ re: /\b(?:class|module)\s+([A-Z][a-zA-Z0-9_]*)/g, type: "Class" /* Class */, groupIndex: 1 },
|
|
685
|
+
// Instance variables
|
|
686
|
+
{ re: /@([a-zA-Z_]\w*)/g, type: "Property" /* Property */, groupIndex: 1 },
|
|
687
|
+
// All-caps constants (3+ chars to avoid single-letter constants)
|
|
688
|
+
{ re: /\b([A-Z][A-Z0-9_]{2,})\b/g, type: "Variable" /* Variable */, groupIndex: 1 },
|
|
689
|
+
// Double-quoted strings
|
|
690
|
+
{ re: /"(?:[^"\\]|\\.)*"/g, type: "String" /* String */ },
|
|
691
|
+
// Single-quoted strings
|
|
692
|
+
{ re: /'(?:[^'\\]|\\.)*'/g, type: "String" /* String */ }
|
|
693
|
+
];
|
|
694
|
+
for (const { re, type, groupIndex } of patterns) {
|
|
695
|
+
re.lastIndex = 0;
|
|
696
|
+
let m;
|
|
697
|
+
while ((m = re.exec(code)) !== null) {
|
|
698
|
+
const fullMatch = m[0];
|
|
699
|
+
const captureValue = groupIndex !== void 0 ? m[groupIndex] ?? fullMatch : fullMatch;
|
|
700
|
+
const captureStart = groupIndex !== void 0 ? m.index + fullMatch.indexOf(captureValue) : m.index;
|
|
701
|
+
if (!captureValue || captureValue.trim() === "") continue;
|
|
702
|
+
if (type === "Variable" /* Variable */ || type === "Function" /* Function */ || type === "Class" /* Class */ || type === "Property" /* Property */) {
|
|
703
|
+
if (RUBY_KEYWORDS.has(captureValue)) continue;
|
|
704
|
+
if (captureValue.length <= 1) continue;
|
|
705
|
+
}
|
|
706
|
+
tokens.push({
|
|
707
|
+
type,
|
|
708
|
+
value: captureValue,
|
|
709
|
+
start: captureStart,
|
|
710
|
+
end: captureStart + captureValue.length,
|
|
711
|
+
line: lineFromIndex(lineOffsets, captureStart)
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
return deduplicate(tokens);
|
|
716
|
+
}
|
|
717
|
+
function scanRust(code) {
|
|
718
|
+
const tokens = [];
|
|
719
|
+
if (!code.trim()) return tokens;
|
|
720
|
+
const lineOffsets = buildLineIndex(code);
|
|
721
|
+
const patterns = [
|
|
722
|
+
{ re: /\/\/[^\n]*/g, type: "Comment" /* Comment */ },
|
|
723
|
+
{ re: /\/\*[\s\S]*?\*\//g, type: "Comment" /* Comment */ },
|
|
724
|
+
// Use declarations
|
|
725
|
+
{ re: /\buse\s+([\w:]+)/g, type: "Import" /* Import */, groupIndex: 1 },
|
|
726
|
+
// Named type declarations (struct, enum, trait)
|
|
727
|
+
{ re: /\b(?:struct|enum|trait)\s+([A-Z][a-zA-Z0-9_]*)/g, type: "Class" /* Class */, groupIndex: 1 },
|
|
728
|
+
// Function names
|
|
729
|
+
{ re: /\bfn\s+([a-zA-Z_]\w*)/g, type: "Function" /* Function */, groupIndex: 1 },
|
|
730
|
+
// Constants (ALL_CAPS)
|
|
731
|
+
{ re: /\bconst\s+([A-Z_][A-Z0-9_]*)\s*:/g, type: "Variable" /* Variable */, groupIndex: 1 },
|
|
732
|
+
// Variable bindings
|
|
733
|
+
{ re: /\blet\s+(?:mut\s+)?([a-zA-Z_]\w*)/g, type: "Variable" /* Variable */, groupIndex: 1 },
|
|
734
|
+
// Double-quoted strings
|
|
735
|
+
{ re: /"(?:[^"\\]|\\.)*"/g, type: "String" /* String */ }
|
|
736
|
+
];
|
|
737
|
+
for (const { re, type, groupIndex } of patterns) {
|
|
738
|
+
re.lastIndex = 0;
|
|
739
|
+
let m;
|
|
740
|
+
while ((m = re.exec(code)) !== null) {
|
|
741
|
+
const fullMatch = m[0];
|
|
742
|
+
const captureValue = groupIndex !== void 0 ? m[groupIndex] ?? fullMatch : fullMatch;
|
|
743
|
+
const captureStart = groupIndex !== void 0 ? m.index + fullMatch.indexOf(captureValue) : m.index;
|
|
744
|
+
if (!captureValue || captureValue.trim() === "") continue;
|
|
745
|
+
if (type === "Variable" /* Variable */ || type === "Function" /* Function */ || type === "Class" /* Class */) {
|
|
746
|
+
if (RUST_KEYWORDS.has(captureValue)) continue;
|
|
747
|
+
if (captureValue.length <= 1) continue;
|
|
748
|
+
}
|
|
749
|
+
tokens.push({
|
|
750
|
+
type,
|
|
751
|
+
value: captureValue,
|
|
752
|
+
start: captureStart,
|
|
753
|
+
end: captureStart + captureValue.length,
|
|
754
|
+
line: lineFromIndex(lineOffsets, captureStart)
|
|
458
755
|
});
|
|
459
756
|
}
|
|
460
757
|
}
|
|
@@ -498,12 +795,61 @@ function scan(code, language) {
|
|
|
498
795
|
case "java":
|
|
499
796
|
tokens = scanJava(code);
|
|
500
797
|
break;
|
|
798
|
+
case "csharp":
|
|
799
|
+
case "cs":
|
|
800
|
+
tokens = scanCSharp(code);
|
|
801
|
+
break;
|
|
802
|
+
case "ruby":
|
|
803
|
+
case "rb":
|
|
804
|
+
tokens = scanRuby(code);
|
|
805
|
+
break;
|
|
806
|
+
case "rust":
|
|
807
|
+
case "rs":
|
|
808
|
+
tokens = scanRust(code);
|
|
809
|
+
break;
|
|
501
810
|
default:
|
|
502
811
|
tokens = scanTypeScript(code);
|
|
503
812
|
}
|
|
504
813
|
return { tokens, language: lang };
|
|
505
814
|
}
|
|
506
815
|
|
|
816
|
+
// src/salt.ts
|
|
817
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
818
|
+
import { join } from "path";
|
|
819
|
+
import { homedir } from "os";
|
|
820
|
+
import { randomBytes } from "crypto";
|
|
821
|
+
var SALT_BYTES = 32;
|
|
822
|
+
function getSaltPath() {
|
|
823
|
+
const dir = join(homedir(), ".pretense");
|
|
824
|
+
return { dir, path: join(dir, "mutation.salt") };
|
|
825
|
+
}
|
|
826
|
+
var _cachedSalt = null;
|
|
827
|
+
function getMutationSalt() {
|
|
828
|
+
if (_cachedSalt) return _cachedSalt;
|
|
829
|
+
const { dir, path } = getSaltPath();
|
|
830
|
+
if (existsSync(path)) {
|
|
831
|
+
try {
|
|
832
|
+
const raw = readFileSync(path, "utf-8").trim();
|
|
833
|
+
if (/^[0-9a-f]{64}$/i.test(raw)) {
|
|
834
|
+
_cachedSalt = raw.toLowerCase();
|
|
835
|
+
return _cachedSalt;
|
|
836
|
+
}
|
|
837
|
+
} catch {
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
const salt = randomBytes(SALT_BYTES).toString("hex");
|
|
841
|
+
try {
|
|
842
|
+
mkdirSync(dir, { recursive: true });
|
|
843
|
+
writeFileSync(path, salt, { mode: 384 });
|
|
844
|
+
} catch {
|
|
845
|
+
}
|
|
846
|
+
_cachedSalt = salt;
|
|
847
|
+
return salt;
|
|
848
|
+
}
|
|
849
|
+
function buildSaltedSeed(baseSeed) {
|
|
850
|
+
return `${baseSeed}:${getMutationSalt()}`;
|
|
851
|
+
}
|
|
852
|
+
|
|
507
853
|
// src/mutator.ts
|
|
508
854
|
function hash32(str) {
|
|
509
855
|
let h = 5381;
|
|
@@ -553,6 +899,7 @@ function mutate(code, language, seed = "pretense") {
|
|
|
553
899
|
stats: { tokensScanned: 0, tokensMutated: 0, durationMs: 0, language: lang }
|
|
554
900
|
};
|
|
555
901
|
}
|
|
902
|
+
const effectiveSeed = seed === "pretense" ? buildSaltedSeed(seed) : seed;
|
|
556
903
|
const { tokens } = scan(code, lang);
|
|
557
904
|
const map = /* @__PURE__ */ new Map();
|
|
558
905
|
for (const token of tokens) {
|
|
@@ -567,7 +914,7 @@ function mutate(code, language, seed = "pretense") {
|
|
|
567
914
|
continue;
|
|
568
915
|
}
|
|
569
916
|
const prefix = prefixForType(token.type);
|
|
570
|
-
const synthetic = deterministicId(token.value,
|
|
917
|
+
const synthetic = deterministicId(token.value, effectiveSeed, prefix);
|
|
571
918
|
map.set(token.value, synthetic);
|
|
572
919
|
}
|
|
573
920
|
const entries = [...map.entries()].sort((a, b) => b[0].length - a[0].length);
|
|
@@ -654,7 +1001,15 @@ var SECRET_PATTERNS = [
|
|
|
654
1001
|
{ name: "hashicorp-vault-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /hvs\.[A-Za-z0-9_-]{24,}/g },
|
|
655
1002
|
{ name: "fastly-api-key", category: "secret", severity: "high", defaultAction: "block", pattern: /fastly_[A-Za-z0-9]{32}/g },
|
|
656
1003
|
{ name: "algolia-api-key", category: "secret", severity: "high", defaultAction: "block", pattern: /[a-f0-9]{32}/g, validate: (m) => /ALGOLIA|algolia/.test(m) },
|
|
657
|
-
{ name: "pulumi-token", category: "secret", severity: "high", defaultAction: "block", pattern: /pul-[a-f0-9]{40}/g }
|
|
1004
|
+
{ name: "pulumi-token", category: "secret", severity: "high", defaultAction: "block", pattern: /pul-[a-f0-9]{40}/g },
|
|
1005
|
+
// ─── Extended patterns (v0.6 unit 8) ─────────────────────────────────────────
|
|
1006
|
+
{ name: "google-oauth-refresh", category: "secret", severity: "critical", defaultAction: "block", pattern: /\bGOCSPX-[A-Za-z0-9_-]{20,}\b/g },
|
|
1007
|
+
{ name: "slack-webhook-url", category: "secret", severity: "critical", defaultAction: "block", pattern: /https:\/\/hooks\.slack\.com\/services\/T[A-Z0-9]+\/B[A-Z0-9]+\/[A-Za-z0-9]{20,}/g },
|
|
1008
|
+
{ name: "sentry-dsn", category: "secret", severity: "high", defaultAction: "redact", pattern: /https:\/\/[a-f0-9]{32}@o\d+\.ingest\.sentry\.io\/\d+/g },
|
|
1009
|
+
{ name: "x509-certificate", category: "secret", severity: "high", defaultAction: "redact", pattern: /-----BEGIN CERTIFICATE-----/g },
|
|
1010
|
+
{ name: "launchdarkly-sdk-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /\bsdk-[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\b/g },
|
|
1011
|
+
{ name: "launchdarkly-mobile-key", category: "secret", severity: "high", defaultAction: "block", pattern: /\bmob-[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\b/g },
|
|
1012
|
+
{ name: "stripe-webhook-secret", category: "secret", severity: "critical", defaultAction: "block", pattern: /\bwhsec_[A-Za-z0-9]{32,}\b/g }
|
|
658
1013
|
];
|
|
659
1014
|
var PII_PATTERNS = [
|
|
660
1015
|
{ name: "ssn", category: "pii", severity: "critical", defaultAction: "redact", pattern: /\b\d{3}-\d{2}-\d{4}\b/g },
|
|
@@ -712,6 +1067,7 @@ function maskValue(value, type) {
|
|
|
712
1067
|
return "***";
|
|
713
1068
|
}
|
|
714
1069
|
var matchCounter = 0;
|
|
1070
|
+
var ENTROPY_SCAN_MAX_BYTES = 512 * 1024;
|
|
715
1071
|
function scan2(text, actionOverrides) {
|
|
716
1072
|
const start = performance.now();
|
|
717
1073
|
const matches = [];
|
|
@@ -737,28 +1093,44 @@ function scan2(text, actionOverrides) {
|
|
|
737
1093
|
});
|
|
738
1094
|
}
|
|
739
1095
|
}
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
(
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
1096
|
+
if (text.length <= ENTROPY_SCAN_MAX_BYTES) {
|
|
1097
|
+
const sortedByStart = [...matches].sort((a, b) => a.start - b.start);
|
|
1098
|
+
const coveredEnd = (idx) => {
|
|
1099
|
+
let lo = 0;
|
|
1100
|
+
let hi = sortedByStart.length - 1;
|
|
1101
|
+
let best = -1;
|
|
1102
|
+
while (lo <= hi) {
|
|
1103
|
+
const mid = lo + hi >> 1;
|
|
1104
|
+
if (sortedByStart[mid].start <= idx) {
|
|
1105
|
+
best = mid;
|
|
1106
|
+
lo = mid + 1;
|
|
1107
|
+
} else {
|
|
1108
|
+
hi = mid - 1;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
return best >= 0 ? sortedByStart[best].end : -1;
|
|
1112
|
+
};
|
|
1113
|
+
const highEntropyPattern = /\b[A-Za-z0-9_\-/.+=$]{21,}\b/g;
|
|
1114
|
+
highEntropyPattern.lastIndex = 0;
|
|
1115
|
+
let hem;
|
|
1116
|
+
while ((hem = highEntropyPattern.exec(text)) !== null) {
|
|
1117
|
+
const value = hem[0];
|
|
1118
|
+
const end = hem.index + value.length;
|
|
1119
|
+
if (coveredEnd(hem.index) >= end) continue;
|
|
1120
|
+
if (isHighEntropy(value)) {
|
|
1121
|
+
matchCounter++;
|
|
1122
|
+
matches.push({
|
|
1123
|
+
id: `scan_${matchCounter}`,
|
|
1124
|
+
category: "secret",
|
|
1125
|
+
type: "high-entropy-string",
|
|
1126
|
+
value,
|
|
1127
|
+
masked: value.slice(0, 6) + "..." + value.slice(-4),
|
|
1128
|
+
start: hem.index,
|
|
1129
|
+
end,
|
|
1130
|
+
severity: "medium",
|
|
1131
|
+
action: actionOverrides?.["high-entropy-string"] ?? "warn"
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
762
1134
|
}
|
|
763
1135
|
}
|
|
764
1136
|
const severityRank = { critical: 4, high: 3, medium: 2, low: 1 };
|
|
@@ -790,7 +1162,7 @@ function applyRedactions(text, matches) {
|
|
|
790
1162
|
}
|
|
791
1163
|
|
|
792
1164
|
// src/store.ts
|
|
793
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
1165
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2 } from "fs";
|
|
794
1166
|
import { dirname } from "path";
|
|
795
1167
|
var MutationStore = class {
|
|
796
1168
|
entries = /* @__PURE__ */ new Map();
|
|
@@ -855,20 +1227,20 @@ var MutationStore = class {
|
|
|
855
1227
|
*/
|
|
856
1228
|
persist() {
|
|
857
1229
|
const dir = dirname(this.filePath);
|
|
858
|
-
if (!
|
|
859
|
-
|
|
1230
|
+
if (!existsSync2(dir)) {
|
|
1231
|
+
mkdirSync2(dir, { recursive: true });
|
|
860
1232
|
}
|
|
861
1233
|
const data = JSON.stringify([...this.entries.values()], null, 2);
|
|
862
|
-
|
|
1234
|
+
writeFileSync2(this.filePath, data, "utf-8");
|
|
863
1235
|
}
|
|
864
1236
|
/**
|
|
865
1237
|
* Load entries from the JSON file into memory.
|
|
866
1238
|
* No-ops if the file doesn't exist.
|
|
867
1239
|
*/
|
|
868
1240
|
load() {
|
|
869
|
-
if (!
|
|
1241
|
+
if (!existsSync2(this.filePath)) return;
|
|
870
1242
|
try {
|
|
871
|
-
const raw =
|
|
1243
|
+
const raw = readFileSync2(this.filePath, "utf-8");
|
|
872
1244
|
const parsed = JSON.parse(raw);
|
|
873
1245
|
this.entries.clear();
|
|
874
1246
|
for (const entry of parsed) {
|
|
@@ -901,8 +1273,9 @@ var MutationStore = class {
|
|
|
901
1273
|
};
|
|
902
1274
|
|
|
903
1275
|
// src/config.ts
|
|
904
|
-
import { existsSync as
|
|
905
|
-
import { join, resolve } from "path";
|
|
1276
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
1277
|
+
import { join as join2, resolve } from "path";
|
|
1278
|
+
import { createHmac } from "crypto";
|
|
906
1279
|
var CONFIG_DIR_NAME = ".pretense";
|
|
907
1280
|
var CONFIG_FILE = "config.json";
|
|
908
1281
|
var MUTATION_MAP_FILE = "mutation-map.json";
|
|
@@ -914,30 +1287,33 @@ function getConfigDir(baseDir) {
|
|
|
914
1287
|
}
|
|
915
1288
|
function initConfig(dir) {
|
|
916
1289
|
const configDir = getConfigDir(dir);
|
|
917
|
-
if (!
|
|
918
|
-
|
|
1290
|
+
if (!existsSync3(configDir)) {
|
|
1291
|
+
mkdirSync3(configDir, { recursive: true });
|
|
919
1292
|
}
|
|
920
|
-
const configPath =
|
|
921
|
-
if (!
|
|
1293
|
+
const configPath = join2(configDir, CONFIG_FILE);
|
|
1294
|
+
if (!existsSync3(configPath)) {
|
|
922
1295
|
const config = {
|
|
923
1296
|
...DEFAULT_CONFIG,
|
|
924
1297
|
storePath: configDir
|
|
925
1298
|
};
|
|
926
|
-
|
|
927
|
-
}
|
|
928
|
-
const mapPath = join(configDir, MUTATION_MAP_FILE);
|
|
929
|
-
if (!existsSync2(mapPath)) {
|
|
930
|
-
writeFileSync2(mapPath, "[]", "utf-8");
|
|
1299
|
+
writeFileSync3(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
931
1300
|
}
|
|
932
|
-
const
|
|
933
|
-
if (!
|
|
934
|
-
|
|
1301
|
+
const mapPath = join2(configDir, MUTATION_MAP_FILE);
|
|
1302
|
+
if (!existsSync3(mapPath)) {
|
|
1303
|
+
writeFileSync3(mapPath, "[]", "utf-8");
|
|
935
1304
|
}
|
|
936
|
-
const
|
|
937
|
-
if (!
|
|
938
|
-
|
|
1305
|
+
const auditPath = join2(configDir, AUDIT_LOG_FILE);
|
|
1306
|
+
if (!existsSync3(auditPath)) {
|
|
1307
|
+
writeFileSync3(auditPath, "", "utf-8");
|
|
1308
|
+
}
|
|
1309
|
+
const usagePath = join2(configDir, USAGE_FILE);
|
|
1310
|
+
if (!existsSync3(usagePath)) {
|
|
1311
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1312
|
+
const month = today.slice(0, 7);
|
|
1313
|
+
const usageData = { month, mutations: 0, firstUseDate: today };
|
|
1314
|
+
writeFileSync3(
|
|
939
1315
|
usagePath,
|
|
940
|
-
JSON.stringify({
|
|
1316
|
+
JSON.stringify({ ...usageData, date: today, checksum: computeUsageChecksum(usageData) }, null, 2),
|
|
941
1317
|
"utf-8"
|
|
942
1318
|
);
|
|
943
1319
|
}
|
|
@@ -945,71 +1321,111 @@ function initConfig(dir) {
|
|
|
945
1321
|
}
|
|
946
1322
|
function loadConfig(dir) {
|
|
947
1323
|
const configDir = getConfigDir(dir);
|
|
948
|
-
const configPath =
|
|
949
|
-
if (!
|
|
1324
|
+
const configPath = join2(configDir, CONFIG_FILE);
|
|
1325
|
+
if (!existsSync3(configPath)) {
|
|
950
1326
|
return { ...DEFAULT_CONFIG };
|
|
951
1327
|
}
|
|
952
1328
|
try {
|
|
953
|
-
const raw =
|
|
1329
|
+
const raw = readFileSync3(configPath, "utf-8");
|
|
954
1330
|
const parsed = JSON.parse(raw);
|
|
955
1331
|
return { ...DEFAULT_CONFIG, ...parsed };
|
|
956
1332
|
} catch {
|
|
957
1333
|
return { ...DEFAULT_CONFIG };
|
|
958
1334
|
}
|
|
959
1335
|
}
|
|
1336
|
+
var USAGE_HMAC_SECRET = "pretense_usage_integrity_v1";
|
|
1337
|
+
function getUsageSecret() {
|
|
1338
|
+
return process.env["PRETENSE_USAGE_SECRET"] ?? USAGE_HMAC_SECRET;
|
|
1339
|
+
}
|
|
1340
|
+
function computeUsageChecksum(data) {
|
|
1341
|
+
const payload = JSON.stringify({ month: data.month, mutations: data.mutations, firstUseDate: data.firstUseDate });
|
|
1342
|
+
return createHmac("sha256", getUsageSecret()).update(payload).digest("hex");
|
|
1343
|
+
}
|
|
1344
|
+
function verifyUsageChecksum(data) {
|
|
1345
|
+
if (!data.checksum) return false;
|
|
1346
|
+
return data.checksum === computeUsageChecksum(data);
|
|
1347
|
+
}
|
|
960
1348
|
function loadUsage(dir) {
|
|
961
1349
|
const configDir = getConfigDir(dir);
|
|
962
|
-
const usagePath =
|
|
1350
|
+
const usagePath = join2(configDir, USAGE_FILE);
|
|
963
1351
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
964
|
-
|
|
965
|
-
|
|
1352
|
+
const currentMonth = today.slice(0, 7);
|
|
1353
|
+
if (!existsSync3(usagePath)) {
|
|
1354
|
+
return { month: currentMonth, mutations: 0, firstUseDate: today };
|
|
966
1355
|
}
|
|
967
1356
|
try {
|
|
968
|
-
const raw =
|
|
1357
|
+
const raw = readFileSync3(usagePath, "utf-8");
|
|
969
1358
|
const data = JSON.parse(raw);
|
|
970
|
-
if (data.date
|
|
971
|
-
|
|
1359
|
+
if (data.date && !data.month) {
|
|
1360
|
+
const legacyDate = data.date;
|
|
1361
|
+
return { month: currentMonth, mutations: 0, firstUseDate: data.firstUseDate ?? legacyDate };
|
|
1362
|
+
}
|
|
1363
|
+
if (!verifyUsageChecksum({ month: data.month, mutations: data.mutations, firstUseDate: data.firstUseDate, checksum: data.checksum })) {
|
|
1364
|
+
process.stderr.write("[PRETENSE] Warning: usage.json integrity check failed. Resetting usage counter.\n");
|
|
1365
|
+
return { month: currentMonth, mutations: 0, firstUseDate: data.firstUseDate ?? today };
|
|
972
1366
|
}
|
|
973
|
-
|
|
1367
|
+
if (data.month !== currentMonth) {
|
|
1368
|
+
return { month: currentMonth, mutations: 0, firstUseDate: data.firstUseDate ?? today };
|
|
1369
|
+
}
|
|
1370
|
+
return { month: data.month, mutations: data.mutations, firstUseDate: data.firstUseDate };
|
|
974
1371
|
} catch {
|
|
975
|
-
return {
|
|
1372
|
+
return { month: currentMonth, mutations: 0, firstUseDate: today };
|
|
976
1373
|
}
|
|
977
1374
|
}
|
|
978
1375
|
function saveUsage(usage, dir) {
|
|
979
1376
|
const configDir = getConfigDir(dir);
|
|
980
|
-
if (!
|
|
981
|
-
|
|
1377
|
+
if (!existsSync3(configDir)) {
|
|
1378
|
+
mkdirSync3(configDir, { recursive: true });
|
|
982
1379
|
}
|
|
983
|
-
const usagePath =
|
|
984
|
-
|
|
1380
|
+
const usagePath = join2(configDir, USAGE_FILE);
|
|
1381
|
+
const checksum = computeUsageChecksum(usage);
|
|
1382
|
+
writeFileSync3(usagePath, JSON.stringify({ ...usage, checksum }, null, 2), "utf-8");
|
|
1383
|
+
}
|
|
1384
|
+
var MIN_LICENSE_KEY_LENGTH = 40;
|
|
1385
|
+
var LICENSE_PAYLOAD_REGEX = /^[a-zA-Z0-9]+$/;
|
|
1386
|
+
function isValidLicenseKey(key, prefix) {
|
|
1387
|
+
if (key.length < MIN_LICENSE_KEY_LENGTH) return false;
|
|
1388
|
+
const afterPrefix = key.slice(prefix.length);
|
|
1389
|
+
const hmacSecret = process.env["PRETENSE_LICENSE_SECRET"];
|
|
1390
|
+
if (hmacSecret) {
|
|
1391
|
+
const lastUnderscore = afterPrefix.lastIndexOf("_");
|
|
1392
|
+
if (lastUnderscore === -1) return false;
|
|
1393
|
+
const payload = afterPrefix.slice(0, lastUnderscore);
|
|
1394
|
+
const providedHmac = afterPrefix.slice(lastUnderscore + 1);
|
|
1395
|
+
if (!payload || !providedHmac) return false;
|
|
1396
|
+
if (!LICENSE_PAYLOAD_REGEX.test(payload)) return false;
|
|
1397
|
+
const expectedHmac = createHmac("sha256", hmacSecret).update(payload).digest("hex").slice(0, 16);
|
|
1398
|
+
return providedHmac === expectedHmac;
|
|
1399
|
+
}
|
|
1400
|
+
return LICENSE_PAYLOAD_REGEX.test(afterPrefix);
|
|
985
1401
|
}
|
|
986
1402
|
function detectTier() {
|
|
987
1403
|
const key = process.env["PRETENSE_LICENSE_KEY"] ?? "";
|
|
988
|
-
if (key.startsWith("pre_ent_")) return "enterprise";
|
|
989
|
-
if (key.startsWith("pre_pro_")) return "pro";
|
|
1404
|
+
if (key.startsWith("pre_ent_") && isValidLicenseKey(key, "pre_ent_")) return "enterprise";
|
|
1405
|
+
if (key.startsWith("pre_pro_") && isValidLicenseKey(key, "pre_pro_")) return "pro";
|
|
990
1406
|
return "free";
|
|
991
1407
|
}
|
|
992
1408
|
function getTierLimits(tier) {
|
|
993
1409
|
switch (tier) {
|
|
994
1410
|
case "enterprise":
|
|
995
|
-
return {
|
|
1411
|
+
return { monthlyMutations: Infinity, auditRetentionDays: Infinity };
|
|
996
1412
|
case "pro":
|
|
997
|
-
return {
|
|
1413
|
+
return { monthlyMutations: Infinity, auditRetentionDays: 90 };
|
|
998
1414
|
case "free":
|
|
999
1415
|
default:
|
|
1000
|
-
return {
|
|
1416
|
+
return { monthlyMutations: 1e3, auditRetentionDays: 30 };
|
|
1001
1417
|
}
|
|
1002
1418
|
}
|
|
1003
1419
|
|
|
1004
1420
|
// src/audit.ts
|
|
1005
|
-
import { appendFileSync, existsSync as
|
|
1006
|
-
import { join as
|
|
1421
|
+
import { appendFileSync, existsSync as existsSync4, readFileSync as readFileSync4, mkdirSync as mkdirSync4 } from "fs";
|
|
1422
|
+
import { join as join3, dirname as dirname2 } from "path";
|
|
1007
1423
|
function writeAuditEntry(entry, dir) {
|
|
1008
1424
|
const configDir = getConfigDir(dir);
|
|
1009
|
-
const auditPath =
|
|
1425
|
+
const auditPath = join3(configDir, "audit.log");
|
|
1010
1426
|
const logDir = dirname2(auditPath);
|
|
1011
|
-
if (!
|
|
1012
|
-
|
|
1427
|
+
if (!existsSync4(logDir)) {
|
|
1428
|
+
mkdirSync4(logDir, { recursive: true });
|
|
1013
1429
|
}
|
|
1014
1430
|
const line = JSON.stringify(entry) + "\n";
|
|
1015
1431
|
appendFileSync(auditPath, line, "utf-8");
|
|
@@ -1028,11 +1444,11 @@ function createAuditEntry(sessionId, file, identifiersMutated, secretsBlocked, l
|
|
|
1028
1444
|
function readAuditLog(options = {}) {
|
|
1029
1445
|
const { limit, dir } = options;
|
|
1030
1446
|
const configDir = getConfigDir(dir);
|
|
1031
|
-
const auditPath =
|
|
1032
|
-
if (!
|
|
1447
|
+
const auditPath = join3(configDir, "audit.log");
|
|
1448
|
+
if (!existsSync4(auditPath)) {
|
|
1033
1449
|
return [];
|
|
1034
1450
|
}
|
|
1035
|
-
const raw =
|
|
1451
|
+
const raw = readFileSync4(auditPath, "utf-8");
|
|
1036
1452
|
const lines = raw.trim().split("\n").filter(Boolean);
|
|
1037
1453
|
let entries = [];
|
|
1038
1454
|
for (const line of lines) {
|
|
@@ -1095,17 +1511,100 @@ function formatAudit(entries, format = "text") {
|
|
|
1095
1511
|
import { Hono } from "hono";
|
|
1096
1512
|
import { serve } from "@hono/node-server";
|
|
1097
1513
|
import { nanoid } from "nanoid";
|
|
1514
|
+
import { createServer } from "net";
|
|
1515
|
+
|
|
1516
|
+
// src/auth.ts
|
|
1517
|
+
import { createHash } from "crypto";
|
|
1518
|
+
var AUTH_HEADERS = ["authorization", "x-api-key", "x-goog-api-key"];
|
|
1519
|
+
var PUBLIC_PATHS = /* @__PURE__ */ new Set([
|
|
1520
|
+
"/",
|
|
1521
|
+
"/health",
|
|
1522
|
+
"/stats",
|
|
1523
|
+
"/audit",
|
|
1524
|
+
"/tokens"
|
|
1525
|
+
]);
|
|
1526
|
+
function extractApiKey(c) {
|
|
1527
|
+
for (const h of AUTH_HEADERS) {
|
|
1528
|
+
const v = c.req.header(h);
|
|
1529
|
+
if (v && v.length > 0) {
|
|
1530
|
+
return v.replace(/^Bearer\s+/i, "").trim();
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
return null;
|
|
1534
|
+
}
|
|
1535
|
+
function sessionHash(apiKey) {
|
|
1536
|
+
return createHash("sha256").update(apiKey).digest("hex").slice(0, 16);
|
|
1537
|
+
}
|
|
1538
|
+
var requireApiKey = async (c, next) => {
|
|
1539
|
+
if (PUBLIC_PATHS.has(c.req.path)) {
|
|
1540
|
+
return next();
|
|
1541
|
+
}
|
|
1542
|
+
const key = extractApiKey(c);
|
|
1543
|
+
if (!key) {
|
|
1544
|
+
return c.json(
|
|
1545
|
+
{
|
|
1546
|
+
error: {
|
|
1547
|
+
type: "missing_api_key",
|
|
1548
|
+
message: "Authorization header required (Authorization, x-api-key, or x-goog-api-key)"
|
|
1549
|
+
}
|
|
1550
|
+
},
|
|
1551
|
+
401
|
|
1552
|
+
);
|
|
1553
|
+
}
|
|
1554
|
+
c.set("sessionHash", sessionHash(key));
|
|
1555
|
+
return next();
|
|
1556
|
+
};
|
|
1557
|
+
|
|
1558
|
+
// src/session-store.ts
|
|
1559
|
+
var sessions = /* @__PURE__ */ new Map();
|
|
1560
|
+
function getSession(hash) {
|
|
1561
|
+
let s = sessions.get(hash);
|
|
1562
|
+
if (!s) {
|
|
1563
|
+
s = {
|
|
1564
|
+
mutationMap: /* @__PURE__ */ new Map(),
|
|
1565
|
+
reverseMap: /* @__PURE__ */ new Map(),
|
|
1566
|
+
createdAt: Date.now(),
|
|
1567
|
+
lastUsed: Date.now()
|
|
1568
|
+
};
|
|
1569
|
+
sessions.set(hash, s);
|
|
1570
|
+
}
|
|
1571
|
+
s.lastUsed = Date.now();
|
|
1572
|
+
return s;
|
|
1573
|
+
}
|
|
1574
|
+
function sessionCount() {
|
|
1575
|
+
return sessions.size;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
// src/proxy.ts
|
|
1579
|
+
function anthropicUpstream() {
|
|
1580
|
+
return process.env["ANTHROPIC_UPSTREAM_URL"] ?? "https://api.anthropic.com";
|
|
1581
|
+
}
|
|
1582
|
+
function openaiUpstream() {
|
|
1583
|
+
return process.env["OPENAI_UPSTREAM_URL"] ?? "https://api.openai.com";
|
|
1584
|
+
}
|
|
1585
|
+
function geminiUpstream() {
|
|
1586
|
+
return process.env["GEMINI_UPSTREAM_URL"] ?? "https://generativelanguage.googleapis.com";
|
|
1587
|
+
}
|
|
1588
|
+
function ollamaUpstream() {
|
|
1589
|
+
return process.env["OLLAMA_UPSTREAM_URL"] ?? "http://localhost:11434";
|
|
1590
|
+
}
|
|
1098
1591
|
function detectUpstream(path) {
|
|
1099
|
-
if (path.
|
|
1100
|
-
if (path.
|
|
1101
|
-
|
|
1102
|
-
|
|
1592
|
+
if (path.startsWith("/v1/messages")) return anthropicUpstream();
|
|
1593
|
+
if (path.startsWith("/v1/chat/completions") || path.startsWith("/v1/completions") || path.startsWith("/v1/responses")) {
|
|
1594
|
+
return openaiUpstream();
|
|
1595
|
+
}
|
|
1596
|
+
if (path.startsWith("/v1beta/") || path.startsWith("/v1/models")) return geminiUpstream();
|
|
1597
|
+
if (path.startsWith("/api/chat") || path.startsWith("/api/generate")) return ollamaUpstream();
|
|
1598
|
+
return anthropicUpstream();
|
|
1103
1599
|
}
|
|
1104
1600
|
function detectProvider(path) {
|
|
1105
|
-
if (path.
|
|
1106
|
-
if (path.
|
|
1107
|
-
|
|
1108
|
-
|
|
1601
|
+
if (path.startsWith("/v1/messages")) return "anthropic";
|
|
1602
|
+
if (path.startsWith("/v1/chat/completions") || path.startsWith("/v1/completions") || path.startsWith("/v1/responses")) {
|
|
1603
|
+
return "openai";
|
|
1604
|
+
}
|
|
1605
|
+
if (path.startsWith("/v1beta/") || path.startsWith("/v1/models")) return "google";
|
|
1606
|
+
if (path.startsWith("/api/chat") || path.startsWith("/api/generate")) return "ollama";
|
|
1607
|
+
return "anthropic";
|
|
1109
1608
|
}
|
|
1110
1609
|
function extractText(body) {
|
|
1111
1610
|
const parts = [];
|
|
@@ -1184,6 +1683,24 @@ function createProxy(opts = {}) {
|
|
|
1184
1683
|
const store = opts.store ?? new MutationStore(`${config.storePath}/proxy-store.json`);
|
|
1185
1684
|
const verbose = opts.verbose ?? false;
|
|
1186
1685
|
const app = new Hono();
|
|
1686
|
+
app.use("*", requireApiKey);
|
|
1687
|
+
app.get(
|
|
1688
|
+
"/",
|
|
1689
|
+
(c) => c.json({
|
|
1690
|
+
name: "pretense",
|
|
1691
|
+
version: "0.3.0",
|
|
1692
|
+
status: "running",
|
|
1693
|
+
routes: {
|
|
1694
|
+
"GET /": "This welcome page",
|
|
1695
|
+
"GET /health": "Health check with uptime",
|
|
1696
|
+
"GET /stats": "Mutation statistics",
|
|
1697
|
+
"GET /audit": "Audit log (?limit=50)",
|
|
1698
|
+
"POST /*": "Proxy to upstream LLM API (Anthropic, OpenAI, Google auto-detected)"
|
|
1699
|
+
},
|
|
1700
|
+
languages: ["TypeScript", "JavaScript", "Python", "Go", "Java", "C#", "Ruby", "Rust"],
|
|
1701
|
+
docs: "https://pretense.ai/docs"
|
|
1702
|
+
})
|
|
1703
|
+
);
|
|
1187
1704
|
app.get(
|
|
1188
1705
|
"/health",
|
|
1189
1706
|
(c) => c.json({
|
|
@@ -1200,7 +1717,8 @@ function createProxy(opts = {}) {
|
|
|
1200
1717
|
"/stats",
|
|
1201
1718
|
(c) => c.json({
|
|
1202
1719
|
...stats,
|
|
1203
|
-
storeSize: store.size
|
|
1720
|
+
storeSize: store.size,
|
|
1721
|
+
activeSessions: sessionCount()
|
|
1204
1722
|
})
|
|
1205
1723
|
);
|
|
1206
1724
|
app.get("/audit", (c) => {
|
|
@@ -1233,15 +1751,17 @@ function createProxy(opts = {}) {
|
|
|
1233
1751
|
const requestId = nanoid(12);
|
|
1234
1752
|
const upstream = c.req.header("x-pretense-upstream") ?? detectUpstream(c.req.path);
|
|
1235
1753
|
const provider = detectProvider(c.req.path);
|
|
1754
|
+
const sessHash = c.get("sessionHash");
|
|
1755
|
+
const session = getSession(sessHash);
|
|
1236
1756
|
const tier = detectTier();
|
|
1237
|
-
const {
|
|
1757
|
+
const { monthlyMutations } = getTierLimits(tier);
|
|
1238
1758
|
const usage = loadUsage();
|
|
1239
|
-
if (tier === "free" && usage.mutations >=
|
|
1759
|
+
if (tier === "free" && usage.mutations >= monthlyMutations) {
|
|
1240
1760
|
return c.json(
|
|
1241
1761
|
{
|
|
1242
1762
|
error: {
|
|
1243
1763
|
type: "pretense_rate_limit",
|
|
1244
|
-
message: `
|
|
1764
|
+
message: `Monthly mutation limit reached (${monthlyMutations}/month). Upgrade to Pro for unlimited: https://pretense.ai/pricing`
|
|
1245
1765
|
}
|
|
1246
1766
|
},
|
|
1247
1767
|
429
|
|
@@ -1274,7 +1794,11 @@ function createProxy(opts = {}) {
|
|
|
1274
1794
|
const mutatedBlocks = [];
|
|
1275
1795
|
for (const block of blocks) {
|
|
1276
1796
|
const result = mutate(block.code, block.language);
|
|
1277
|
-
for (const [k, v] of result.map
|
|
1797
|
+
for (const [k, v] of result.map) {
|
|
1798
|
+
mutationMap.set(k, v);
|
|
1799
|
+
session.mutationMap.set(k, v);
|
|
1800
|
+
session.reverseMap.set(v, k);
|
|
1801
|
+
}
|
|
1278
1802
|
tokensMutated += result.stats.tokensMutated;
|
|
1279
1803
|
mutatedBlocks.push(result.mutatedCode);
|
|
1280
1804
|
}
|
|
@@ -1282,8 +1806,9 @@ function createProxy(opts = {}) {
|
|
|
1282
1806
|
});
|
|
1283
1807
|
usage.mutations += tokensMutated;
|
|
1284
1808
|
saveUsage(usage);
|
|
1285
|
-
|
|
1286
|
-
|
|
1809
|
+
const warningThreshold = Math.floor(monthlyMutations * 0.8);
|
|
1810
|
+
if (tier === "free" && usage.mutations >= warningThreshold && usage.mutations - tokensMutated < warningThreshold) {
|
|
1811
|
+
printUpgradeNudge(`${usage.mutations}/${monthlyMutations} monthly mutations used. Running low!`);
|
|
1287
1812
|
}
|
|
1288
1813
|
if (tier === "free" && daysSinceFirstUse() >= 7) {
|
|
1289
1814
|
printUpgradeNudge("You've been using Pretense for 7+ days. Unlock Pro features: unlimited mutations, 90-day audit, Slack alerts.");
|
|
@@ -1326,14 +1851,59 @@ function createProxy(opts = {}) {
|
|
|
1326
1851
|
return c.json({ error: { type: "pretense_upstream_error", message: "Failed to reach upstream provider" } }, 502);
|
|
1327
1852
|
}
|
|
1328
1853
|
if (body["stream"] === true) {
|
|
1329
|
-
|
|
1854
|
+
const streamReverseMap = new Map(mutationMap);
|
|
1855
|
+
const upstream2 = upstreamResp.body;
|
|
1856
|
+
if (!upstream2 || tokensMutated === 0) {
|
|
1857
|
+
return new Response(upstream2, {
|
|
1858
|
+
status: upstreamResp.status,
|
|
1859
|
+
headers: {
|
|
1860
|
+
"content-type": upstreamResp.headers.get("content-type") ?? "text/event-stream",
|
|
1861
|
+
"cache-control": "no-cache",
|
|
1862
|
+
"x-pretense-request-id": requestId,
|
|
1863
|
+
"x-pretense-protected": "true",
|
|
1864
|
+
"x-pretense-mutations": String(tokensMutated)
|
|
1865
|
+
}
|
|
1866
|
+
});
|
|
1867
|
+
}
|
|
1868
|
+
const decoder = new TextDecoder();
|
|
1869
|
+
const encoder = new TextEncoder();
|
|
1870
|
+
let buffer = "";
|
|
1871
|
+
const transform = new TransformStream({
|
|
1872
|
+
transform(chunk, controller) {
|
|
1873
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
1874
|
+
const lines = buffer.split("\n");
|
|
1875
|
+
buffer = lines.pop() ?? "";
|
|
1876
|
+
for (const line of lines) {
|
|
1877
|
+
if (line.startsWith("data: ") && line !== "data: [DONE]") {
|
|
1878
|
+
try {
|
|
1879
|
+
const json = JSON.parse(line.slice(6));
|
|
1880
|
+
const reversed = applyToBody(json, (text) => reverse(text, streamReverseMap));
|
|
1881
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(reversed)}
|
|
1882
|
+
`));
|
|
1883
|
+
} catch {
|
|
1884
|
+
controller.enqueue(encoder.encode(line + "\n"));
|
|
1885
|
+
}
|
|
1886
|
+
} else {
|
|
1887
|
+
controller.enqueue(encoder.encode(line + "\n"));
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
},
|
|
1891
|
+
flush(controller) {
|
|
1892
|
+
if (buffer.length > 0) {
|
|
1893
|
+
controller.enqueue(encoder.encode(buffer));
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
});
|
|
1897
|
+
const reversedStream = upstream2.pipeThrough(transform);
|
|
1898
|
+
return new Response(reversedStream, {
|
|
1330
1899
|
status: upstreamResp.status,
|
|
1331
1900
|
headers: {
|
|
1332
1901
|
"content-type": upstreamResp.headers.get("content-type") ?? "text/event-stream",
|
|
1333
1902
|
"cache-control": "no-cache",
|
|
1334
1903
|
"x-pretense-request-id": requestId,
|
|
1335
1904
|
"x-pretense-protected": "true",
|
|
1336
|
-
"x-pretense-mutations": String(tokensMutated)
|
|
1905
|
+
"x-pretense-mutations": String(tokensMutated),
|
|
1906
|
+
"x-pretense-stream-reversal": "active"
|
|
1337
1907
|
}
|
|
1338
1908
|
});
|
|
1339
1909
|
}
|
|
@@ -1363,31 +1933,435 @@ function createProxy(opts = {}) {
|
|
|
1363
1933
|
});
|
|
1364
1934
|
return app;
|
|
1365
1935
|
}
|
|
1936
|
+
function isPortAvailable(port) {
|
|
1937
|
+
return new Promise((resolve3) => {
|
|
1938
|
+
const tester = createServer().once("error", () => resolve3(false)).once("listening", () => {
|
|
1939
|
+
tester.once("close", () => resolve3(true)).close();
|
|
1940
|
+
}).listen(port, "127.0.0.1");
|
|
1941
|
+
});
|
|
1942
|
+
}
|
|
1943
|
+
async function findAvailablePort(startPort, maxAttempts = 10) {
|
|
1944
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
1945
|
+
const candidate = startPort + i;
|
|
1946
|
+
if (candidate < 1 || candidate > 65535) break;
|
|
1947
|
+
if (await isPortAvailable(candidate)) return candidate;
|
|
1948
|
+
}
|
|
1949
|
+
throw new Error(
|
|
1950
|
+
`No available port found in range ${startPort}-${startPort + maxAttempts - 1}. Free a port or pass --port <number>.`
|
|
1951
|
+
);
|
|
1952
|
+
}
|
|
1366
1953
|
function startProxy(opts = {}) {
|
|
1367
|
-
const
|
|
1954
|
+
const requestedPort = opts.port ?? opts.config?.port ?? DEFAULT_CONFIG.port;
|
|
1368
1955
|
const app = createProxy(opts);
|
|
1369
1956
|
const tier = detectTier();
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
\u2502 \u2502
|
|
1377
|
-
\u2502 Proxy: http://localhost:${port} \u2502
|
|
1378
|
-
\u2502 Health: http://localhost:${port}/health \u2502
|
|
1379
|
-
\u2502 Stats: http://localhost:${port}/stats \u2502
|
|
1380
|
-
\u2502 \u2502
|
|
1381
|
-
\u2502 ANTHROPIC_BASE_URL=http://localhost:${port} \u2502
|
|
1382
|
-
\u2502 OPENAI_BASE_URL=http://localhost:${port}/v1 \u2502
|
|
1383
|
-
\u2502 \u2502
|
|
1384
|
-
\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\x1B[0m
|
|
1957
|
+
void (async () => {
|
|
1958
|
+
let port;
|
|
1959
|
+
try {
|
|
1960
|
+
port = await findAvailablePort(requestedPort, 10);
|
|
1961
|
+
} catch (err) {
|
|
1962
|
+
process.stderr.write(`\x1B[31m[PRETENSE] ${err.message}\x1B[0m
|
|
1385
1963
|
`);
|
|
1386
|
-
|
|
1964
|
+
process.exit(1);
|
|
1965
|
+
}
|
|
1966
|
+
if (port !== requestedPort) {
|
|
1967
|
+
process.stderr.write(
|
|
1968
|
+
`\x1B[33m[PRETENSE] Port ${requestedPort} in use, falling back to ${port}.\x1B[0m
|
|
1969
|
+
`
|
|
1970
|
+
);
|
|
1971
|
+
}
|
|
1972
|
+
serve({ fetch: app.fetch, port }, () => {
|
|
1973
|
+
const portStr = String(port);
|
|
1974
|
+
process.stdout.write(`
|
|
1975
|
+
\x1B[36mPretense v0.3.0 [${tier.toUpperCase()}]\x1B[0m
|
|
1976
|
+
Your code hits AI naked. We fix that.
|
|
1977
|
+
|
|
1978
|
+
Proxy: http://localhost:${portStr}
|
|
1979
|
+
Health: http://localhost:${portStr}/health
|
|
1980
|
+
Stats: http://localhost:${portStr}/stats
|
|
1981
|
+
|
|
1982
|
+
Drop-in env vars:
|
|
1983
|
+
ANTHROPIC_BASE_URL=http://localhost:${portStr}
|
|
1984
|
+
OPENAI_BASE_URL=http://localhost:${portStr}/v1
|
|
1985
|
+
GEMINI_BASE_URL=http://localhost:${portStr}/v1beta
|
|
1986
|
+
OLLAMA_HOST=http://localhost:${portStr}
|
|
1987
|
+
|
|
1988
|
+
Routes intercepted:
|
|
1989
|
+
/v1/messages -> Anthropic (Claude Code, SDK)
|
|
1990
|
+
/v1/chat/completions -> OpenAI (ChatGPT, Cursor)
|
|
1991
|
+
/v1/responses -> OpenAI (Codex CLI)
|
|
1992
|
+
/v1beta/... -> Google Gemini
|
|
1993
|
+
/api/chat, /api/generate -> Ollama (local)
|
|
1994
|
+
`);
|
|
1995
|
+
});
|
|
1996
|
+
})();
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
// src/token-footer.ts
|
|
2000
|
+
import chalk from "chalk";
|
|
2001
|
+
var PLAN_LIMITS = {
|
|
2002
|
+
free: { monthlyMutations: 1e3, monthlyScans: 5e3, seats: 3, pricePerMonth: "$0/month" },
|
|
2003
|
+
pro: { monthlyMutations: 1e5, monthlyScans: 5e5, seats: 25, pricePerMonth: "$29/seat/month" },
|
|
2004
|
+
enterprise: {
|
|
2005
|
+
monthlyMutations: Number.POSITIVE_INFINITY,
|
|
2006
|
+
monthlyScans: Number.POSITIVE_INFINITY,
|
|
2007
|
+
seats: Number.POSITIVE_INFINITY,
|
|
2008
|
+
pricePerMonth: "$99/seat/month"
|
|
2009
|
+
}
|
|
2010
|
+
};
|
|
2011
|
+
function getPlanLimits(tier) {
|
|
2012
|
+
return PLAN_LIMITS[tier];
|
|
2013
|
+
}
|
|
2014
|
+
function fmt(n) {
|
|
2015
|
+
if (!Number.isFinite(n)) return "unlimited";
|
|
2016
|
+
return n.toLocaleString("en-US");
|
|
2017
|
+
}
|
|
2018
|
+
function printTokenFooter(filesScanned, mutationsMade) {
|
|
2019
|
+
const tier = detectTier();
|
|
2020
|
+
const limits = getPlanLimits(tier);
|
|
2021
|
+
const usage = loadUsage();
|
|
2022
|
+
if (tier === "enterprise") {
|
|
2023
|
+
process.stderr.write(
|
|
2024
|
+
chalk.gray(`
|
|
2025
|
+
${filesScanned} files, ${mutationsMade} mutations \xB7 Plan: Enterprise (unlimited)
|
|
2026
|
+
|
|
2027
|
+
`)
|
|
2028
|
+
);
|
|
2029
|
+
return;
|
|
2030
|
+
}
|
|
2031
|
+
const used = usage.mutations;
|
|
2032
|
+
const cap = limits.monthlyMutations;
|
|
2033
|
+
const pct = cap > 0 ? Math.min(100, Math.round(used / cap * 100)) : 0;
|
|
2034
|
+
const usageStr = pct >= 80 ? chalk.yellow(`${fmt(used)} / ${fmt(cap)}`) : chalk.gray(`${fmt(used)} / ${fmt(cap)}`);
|
|
2035
|
+
process.stderr.write(
|
|
2036
|
+
`
|
|
2037
|
+
${chalk.cyan(filesScanned + " files")}, ${chalk.cyan(mutationsMade + " mutations")} \xB7 Plan: ${chalk.bold(tier.toUpperCase())} \xB7 Mutations this month: ${usageStr} (${pct}%)
|
|
2038
|
+
`
|
|
2039
|
+
);
|
|
2040
|
+
if (tier === "free") {
|
|
2041
|
+
if (pct >= 80) {
|
|
2042
|
+
process.stderr.write(
|
|
2043
|
+
chalk.yellow(` \u26A0 Approaching free-tier limit. Upgrade: ${chalk.bold("pretense upgrade")}
|
|
2044
|
+
|
|
2045
|
+
`)
|
|
2046
|
+
);
|
|
2047
|
+
} else {
|
|
2048
|
+
process.stderr.write(
|
|
2049
|
+
chalk.gray(` Run ${chalk.bold("pretense status")} for details, ${chalk.bold("pretense upgrade")} to remove limits.
|
|
2050
|
+
|
|
2051
|
+
`)
|
|
2052
|
+
);
|
|
2053
|
+
}
|
|
2054
|
+
} else {
|
|
2055
|
+
process.stderr.write("\n");
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
// src/banner.ts
|
|
2060
|
+
import chalk2 from "chalk";
|
|
2061
|
+
var BANNER_RAW = `\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 pretense
|
|
2062
|
+
\u2588\u2588 \u2588\u2588
|
|
2063
|
+
\u2588\u2588 \u2588\u2588 Stop your code from
|
|
2064
|
+
\u2588\u2588 \u2588\u2588 leaking to any LLM.
|
|
2065
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
|
|
2066
|
+
\u2588\u2588 pretense.ai
|
|
2067
|
+
\u2588\u2588
|
|
2068
|
+
\u2588\u2588
|
|
2069
|
+
\u2588\u2588
|
|
2070
|
+
\u2588\u2588`;
|
|
2071
|
+
var ICE_BLUE = "#7DD3FC";
|
|
2072
|
+
function shouldPrint() {
|
|
2073
|
+
if (process.env.PRETENSE_NO_BANNER === "1") return false;
|
|
2074
|
+
if (!process.stdout.isTTY) return false;
|
|
2075
|
+
if (process.env.CI) return false;
|
|
2076
|
+
return true;
|
|
2077
|
+
}
|
|
2078
|
+
function tint(line) {
|
|
2079
|
+
try {
|
|
2080
|
+
return chalk2.hex(ICE_BLUE)(line);
|
|
2081
|
+
} catch {
|
|
2082
|
+
return line;
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
function printBanner() {
|
|
2086
|
+
if (!shouldPrint()) return;
|
|
2087
|
+
const lines = BANNER_RAW.split("\n");
|
|
2088
|
+
for (const line of lines) {
|
|
2089
|
+
process.stderr.write(tint(line) + "\n");
|
|
2090
|
+
}
|
|
2091
|
+
process.stderr.write("\n");
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
// src/commands/status.ts
|
|
2095
|
+
import chalk3 from "chalk";
|
|
2096
|
+
var PLAN_LABEL = {
|
|
2097
|
+
free: "Free",
|
|
2098
|
+
pro: "Pro",
|
|
2099
|
+
enterprise: "Enterprise"
|
|
2100
|
+
};
|
|
2101
|
+
function fmt2(n) {
|
|
2102
|
+
if (!Number.isFinite(n)) return "unlimited";
|
|
2103
|
+
return n.toLocaleString("en-US");
|
|
2104
|
+
}
|
|
2105
|
+
function runStatus() {
|
|
2106
|
+
const tier = detectTier();
|
|
2107
|
+
const planLimits = getPlanLimits(tier);
|
|
2108
|
+
const tierLimits = getTierLimits(tier);
|
|
2109
|
+
const usage = loadUsage();
|
|
2110
|
+
const tierBadge = tier === "enterprise" ? chalk3.bgMagenta.white.bold(" ENTERPRISE ") : tier === "pro" ? chalk3.bgBlue.white.bold(" PRO ") : chalk3.bgGray.white.bold(" FREE ");
|
|
2111
|
+
const lines = [];
|
|
2112
|
+
lines.push("");
|
|
2113
|
+
lines.push(`${chalk3.cyan("Pretense")} ${chalk3.gray("v0.3.0")} ${tierBadge}`);
|
|
2114
|
+
lines.push("");
|
|
2115
|
+
lines.push(` Plan: ${chalk3.bold(PLAN_LABEL[tier])} ${chalk3.gray("(" + planLimits.pricePerMonth + ")")}`);
|
|
2116
|
+
lines.push(` Mutations: ${fmt2(usage.mutations)} / ${fmt2(planLimits.monthlyMutations)} this month`);
|
|
2117
|
+
lines.push(` Scans: 0 / ${fmt2(planLimits.monthlyScans)} this month`);
|
|
2118
|
+
lines.push(` Seats: 1 / ${fmt2(planLimits.seats)}`);
|
|
2119
|
+
lines.push(` Audit log: ${Number.isFinite(tierLimits.auditRetentionDays) ? `${tierLimits.auditRetentionDays} days` : "unlimited"}`);
|
|
2120
|
+
lines.push("");
|
|
2121
|
+
if (tier === "free") {
|
|
2122
|
+
lines.push(` ${chalk3.gray("Upgrade:")} ${chalk3.bold("pretense upgrade")}`);
|
|
2123
|
+
lines.push(` ${chalk3.gray("Credits:")} ${chalk3.bold("pretense credits")}`);
|
|
2124
|
+
lines.push("");
|
|
2125
|
+
}
|
|
2126
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
// src/commands/upgrade.ts
|
|
2130
|
+
import chalk4 from "chalk";
|
|
2131
|
+
var PRICING_URL = "https://pretense.ai/pricing";
|
|
2132
|
+
function runUpgrade() {
|
|
2133
|
+
const tier = detectTier();
|
|
2134
|
+
const lines = [];
|
|
2135
|
+
lines.push("");
|
|
2136
|
+
lines.push(chalk4.cyan.bold(" Pretense Plans"));
|
|
2137
|
+
lines.push("");
|
|
2138
|
+
lines.push(chalk4.gray(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"));
|
|
2139
|
+
lines.push(chalk4.gray(" \u2502 \u2502 Free \u2502 Pro \u2502 Enterprise \u2502"));
|
|
2140
|
+
lines.push(chalk4.gray(" \u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524"));
|
|
2141
|
+
lines.push(` \u2502 Price/seat \u2502 ${chalk4.bold("$0")} \u2502 ${chalk4.bold("$29")} \u2502 ${chalk4.bold("$99")} \u2502`);
|
|
2142
|
+
lines.push(" \u2502 Mutations/mo \u2502 1,000 \u2502 100,000 \u2502 unlimited \u2502");
|
|
2143
|
+
lines.push(" \u2502 Scans/mo \u2502 5,000 \u2502 500,000 \u2502 unlimited \u2502");
|
|
2144
|
+
lines.push(" \u2502 Seats \u2502 3 \u2502 25 \u2502 unlimited \u2502");
|
|
2145
|
+
lines.push(" \u2502 Audit log \u2502 30 days \u2502 90 days \u2502 unlimited \u2502");
|
|
2146
|
+
lines.push(" \u2502 SSO/SAML \u2502 - \u2502 - \u2502 yes \u2502");
|
|
2147
|
+
lines.push(" \u2502 On-prem \u2502 - \u2502 - \u2502 yes \u2502");
|
|
2148
|
+
lines.push(" \u2502 SLA \u2502 - \u2502 - \u2502 24/7 \u2502");
|
|
2149
|
+
lines.push(chalk4.gray(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"));
|
|
2150
|
+
lines.push("");
|
|
2151
|
+
lines.push(` ${chalk4.gray("Current plan:")} ${chalk4.bold(tier.toUpperCase())}`);
|
|
2152
|
+
lines.push(` ${chalk4.gray("Upgrade at:")} ${chalk4.cyan.underline(PRICING_URL)}`);
|
|
2153
|
+
lines.push("");
|
|
2154
|
+
lines.push(chalk4.gray(" After purchase, set:"));
|
|
2155
|
+
lines.push(chalk4.gray(" export PRETENSE_LICENSE_KEY=pre_pro_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"));
|
|
2156
|
+
lines.push("");
|
|
2157
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
// src/commands/credits.ts
|
|
2161
|
+
import chalk5 from "chalk";
|
|
2162
|
+
var PLAN_LABEL2 = {
|
|
2163
|
+
free: "Free",
|
|
2164
|
+
pro: "Pro",
|
|
2165
|
+
enterprise: "Enterprise"
|
|
2166
|
+
};
|
|
2167
|
+
function fmt3(n) {
|
|
2168
|
+
if (!Number.isFinite(n)) return "unlimited";
|
|
2169
|
+
return n.toLocaleString("en-US");
|
|
2170
|
+
}
|
|
2171
|
+
function progressBar(used, cap, width = 24) {
|
|
2172
|
+
if (!Number.isFinite(cap) || cap <= 0) return chalk5.green("[" + "=".repeat(width) + "]");
|
|
2173
|
+
const pct = Math.min(1, used / cap);
|
|
2174
|
+
const filled = Math.round(pct * width);
|
|
2175
|
+
const bar = "=".repeat(filled) + "-".repeat(width - filled);
|
|
2176
|
+
const colored = pct >= 0.8 ? chalk5.yellow(bar) : pct >= 1 ? chalk5.red(bar) : chalk5.green(bar);
|
|
2177
|
+
return "[" + colored + "]";
|
|
2178
|
+
}
|
|
2179
|
+
function runCredits() {
|
|
2180
|
+
const tier = detectTier();
|
|
2181
|
+
const limits = getPlanLimits(tier);
|
|
2182
|
+
const usage = loadUsage();
|
|
2183
|
+
const used = usage.mutations;
|
|
2184
|
+
const cap = limits.monthlyMutations;
|
|
2185
|
+
const remaining = Number.isFinite(cap) ? Math.max(0, cap - used) : Number.POSITIVE_INFINITY;
|
|
2186
|
+
const pct = Number.isFinite(cap) && cap > 0 ? Math.round(used / cap * 100) : 0;
|
|
2187
|
+
const lines = [];
|
|
2188
|
+
lines.push("");
|
|
2189
|
+
lines.push(chalk5.cyan.bold(" Pretense Credits"));
|
|
2190
|
+
lines.push("");
|
|
2191
|
+
lines.push(` Plan: ${chalk5.bold(PLAN_LABEL2[tier])}`);
|
|
2192
|
+
lines.push(` Month: ${usage.month}`);
|
|
2193
|
+
lines.push("");
|
|
2194
|
+
lines.push(` Mutations: ${progressBar(used, cap)} ${fmt3(used)} / ${fmt3(cap)}`);
|
|
2195
|
+
lines.push(` Used: ${pct}%`);
|
|
2196
|
+
lines.push(` Remaining: ${fmt3(remaining)}`);
|
|
2197
|
+
lines.push("");
|
|
2198
|
+
if (tier === "free" && Number.isFinite(cap) && used >= cap * 0.8) {
|
|
2199
|
+
lines.push(chalk5.yellow(" \u26A0 Approaching free-tier limit. Run 'pretense upgrade' for unlimited."));
|
|
2200
|
+
lines.push("");
|
|
2201
|
+
} else if (tier === "free") {
|
|
2202
|
+
lines.push(chalk5.gray(" Tip: Run 'pretense upgrade' to compare plans."));
|
|
2203
|
+
lines.push("");
|
|
2204
|
+
}
|
|
2205
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
// src/commands/login.ts
|
|
2209
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4, readFileSync as readFileSync5, chmodSync, readSync } from "fs";
|
|
2210
|
+
import { join as join4 } from "path";
|
|
2211
|
+
import { homedir as homedir2 } from "os";
|
|
2212
|
+
import chalk6 from "chalk";
|
|
2213
|
+
var CONFIG_DIR_NAME2 = ".pretense";
|
|
2214
|
+
var CONFIG_FILE2 = "config.json";
|
|
2215
|
+
function getUserConfigDir() {
|
|
2216
|
+
return join4(homedir2(), CONFIG_DIR_NAME2);
|
|
2217
|
+
}
|
|
2218
|
+
function isValidKeyShape(key) {
|
|
2219
|
+
const trimmed = key.trim();
|
|
2220
|
+
if (trimmed.length < 24) return false;
|
|
2221
|
+
if (!/^[A-Za-z0-9_]+$/.test(trimmed)) return false;
|
|
2222
|
+
return trimmed.startsWith("pk_live_") || trimmed.startsWith("ptns_live_");
|
|
2223
|
+
}
|
|
2224
|
+
function readStdinIfPiped() {
|
|
2225
|
+
if (process.stdin.isTTY) return "";
|
|
2226
|
+
try {
|
|
2227
|
+
const chunks = [];
|
|
2228
|
+
const buf = Buffer.alloc(4096);
|
|
2229
|
+
let bytesRead = 0;
|
|
2230
|
+
do {
|
|
2231
|
+
try {
|
|
2232
|
+
bytesRead = readSync(0, buf, 0, buf.length, null);
|
|
2233
|
+
} catch {
|
|
2234
|
+
bytesRead = 0;
|
|
2235
|
+
}
|
|
2236
|
+
if (bytesRead > 0) chunks.push(Buffer.from(buf.subarray(0, bytesRead)));
|
|
2237
|
+
} while (bytesRead === buf.length);
|
|
2238
|
+
return Buffer.concat(chunks).toString("utf-8").trim();
|
|
2239
|
+
} catch {
|
|
2240
|
+
return "";
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
function maskKey(key) {
|
|
2244
|
+
const trimmed = key.trim();
|
|
2245
|
+
if (trimmed.length <= 12) return trimmed;
|
|
2246
|
+
return `${trimmed.slice(0, 12)}${"\u2022".repeat(8)}${trimmed.slice(-4)}`;
|
|
2247
|
+
}
|
|
2248
|
+
function saveApiKey(key, configDir = getUserConfigDir()) {
|
|
2249
|
+
if (!existsSync5(configDir)) {
|
|
2250
|
+
mkdirSync5(configDir, { recursive: true });
|
|
2251
|
+
}
|
|
2252
|
+
const path = join4(configDir, CONFIG_FILE2);
|
|
2253
|
+
let existing = {};
|
|
2254
|
+
if (existsSync5(path)) {
|
|
2255
|
+
try {
|
|
2256
|
+
existing = JSON.parse(readFileSync5(path, "utf-8"));
|
|
2257
|
+
} catch {
|
|
2258
|
+
existing = {};
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
const next = {
|
|
2262
|
+
...existing,
|
|
2263
|
+
api_key: key.trim(),
|
|
2264
|
+
saved_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2265
|
+
};
|
|
2266
|
+
writeFileSync4(path, JSON.stringify(next, null, 2), { encoding: "utf-8", mode: 384 });
|
|
2267
|
+
try {
|
|
2268
|
+
chmodSync(path, 384);
|
|
2269
|
+
} catch {
|
|
2270
|
+
}
|
|
2271
|
+
return path;
|
|
2272
|
+
}
|
|
2273
|
+
function runLogin(opts = {}) {
|
|
2274
|
+
const candidate = opts.key?.trim() || (process.env["PRETENSE_API_KEY"] ?? "").trim() || readStdinIfPiped();
|
|
2275
|
+
if (!candidate) {
|
|
2276
|
+
console.error(chalk6.red("Error: no API key provided."));
|
|
2277
|
+
console.error(
|
|
2278
|
+
chalk6.gray("Try: pretense login --key pk_live_xxxxx")
|
|
2279
|
+
);
|
|
2280
|
+
console.error(
|
|
2281
|
+
chalk6.gray(" or: echo $PRETENSE_API_KEY | pretense login")
|
|
2282
|
+
);
|
|
2283
|
+
return 1;
|
|
2284
|
+
}
|
|
2285
|
+
if (!isValidKeyShape(candidate)) {
|
|
2286
|
+
console.error(chalk6.red("Error: that does not look like a Pretense API key."));
|
|
2287
|
+
console.error(
|
|
2288
|
+
chalk6.gray("Keys start with pk_live_ and are at least 24 characters long.")
|
|
2289
|
+
);
|
|
2290
|
+
console.error(
|
|
2291
|
+
chalk6.gray("Get one at https://pretense.ai/dashboard/settings/api-keys")
|
|
2292
|
+
);
|
|
2293
|
+
return 1;
|
|
2294
|
+
}
|
|
2295
|
+
const path = saveApiKey(candidate, opts.configDir);
|
|
2296
|
+
console.log(chalk6.green("Saved API key to"), chalk6.bold(path));
|
|
2297
|
+
console.log(chalk6.gray(` ${maskKey(candidate)}`));
|
|
2298
|
+
console.log(chalk6.gray(" File mode 600 \u2014 readable only by you."));
|
|
2299
|
+
return 0;
|
|
1387
2300
|
}
|
|
1388
2301
|
|
|
1389
2302
|
// src/index.ts
|
|
2303
|
+
{
|
|
2304
|
+
const major = parseInt(process.versions.node.split(".")[0], 10);
|
|
2305
|
+
if (Number.isNaN(major) || major < 18) {
|
|
2306
|
+
process.stderr.write(
|
|
2307
|
+
`pretense requires Node.js 18 or newer (you have v${process.versions.node}).
|
|
2308
|
+
`
|
|
2309
|
+
);
|
|
2310
|
+
process.stderr.write(`Upgrade via nvm: nvm install 20 && nvm use 20
|
|
2311
|
+
`);
|
|
2312
|
+
process.exit(1);
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
1390
2315
|
var VERSION = "0.3.0";
|
|
2316
|
+
var SCAN_CONCURRENCY = Math.max(2, os.cpus().length);
|
|
2317
|
+
var PROGRESS_INTERVAL_MS = 500;
|
|
2318
|
+
var SUPPORTED_LANGUAGES = ["typescript", "javascript", "python", "go", "java", "csharp", "ruby", "rust"];
|
|
2319
|
+
var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".java", ".cs", ".rb", ".rs"]);
|
|
2320
|
+
var SUPPORTED_CONFIG_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
2321
|
+
".json",
|
|
2322
|
+
".yaml",
|
|
2323
|
+
".yml",
|
|
2324
|
+
".toml",
|
|
2325
|
+
".env",
|
|
2326
|
+
".cfg",
|
|
2327
|
+
".ini",
|
|
2328
|
+
".tf",
|
|
2329
|
+
".tfstate",
|
|
2330
|
+
".tfvars",
|
|
2331
|
+
".hcl",
|
|
2332
|
+
".properties",
|
|
2333
|
+
".conf",
|
|
2334
|
+
".xml",
|
|
2335
|
+
".plist",
|
|
2336
|
+
".pem",
|
|
2337
|
+
".key",
|
|
2338
|
+
".crt",
|
|
2339
|
+
".pub",
|
|
2340
|
+
".pfx",
|
|
2341
|
+
".p12",
|
|
2342
|
+
".htpasswd"
|
|
2343
|
+
]);
|
|
2344
|
+
var SUPPORTED_FILENAMES = /* @__PURE__ */ new Set([
|
|
2345
|
+
".env",
|
|
2346
|
+
".env.local",
|
|
2347
|
+
".env.production",
|
|
2348
|
+
".env.development",
|
|
2349
|
+
".env.staging",
|
|
2350
|
+
".env.test",
|
|
2351
|
+
".npmrc",
|
|
2352
|
+
".netrc",
|
|
2353
|
+
".pypirc",
|
|
2354
|
+
"Dockerfile",
|
|
2355
|
+
"docker-compose.yml",
|
|
2356
|
+
"docker-compose.yaml"
|
|
2357
|
+
]);
|
|
2358
|
+
function isScannableFile(filePath) {
|
|
2359
|
+
const ext = extname(filePath).toLowerCase();
|
|
2360
|
+
return SUPPORTED_EXTENSIONS.has(ext) || SUPPORTED_CONFIG_EXTENSIONS.has(ext) || SUPPORTED_FILENAMES.has(basename(filePath));
|
|
2361
|
+
}
|
|
2362
|
+
function isSourceCodeFile(filePath) {
|
|
2363
|
+
return SUPPORTED_EXTENSIONS.has(extname(filePath).toLowerCase());
|
|
2364
|
+
}
|
|
1391
2365
|
function langFromExt(filePath) {
|
|
1392
2366
|
const ext = extname(filePath).toLowerCase();
|
|
1393
2367
|
const map = {
|
|
@@ -1399,26 +2373,82 @@ function langFromExt(filePath) {
|
|
|
1399
2373
|
".cjs": "javascript",
|
|
1400
2374
|
".py": "python",
|
|
1401
2375
|
".go": "go",
|
|
1402
|
-
".java": "java"
|
|
2376
|
+
".java": "java",
|
|
2377
|
+
".cs": "csharp",
|
|
2378
|
+
".rb": "ruby",
|
|
2379
|
+
".rs": "rust"
|
|
1403
2380
|
};
|
|
1404
2381
|
return map[ext] ?? "typescript";
|
|
1405
2382
|
}
|
|
2383
|
+
function validatePort(value) {
|
|
2384
|
+
const port = parseInt(value, 10);
|
|
2385
|
+
if (isNaN(port) || port.toString() !== value.trim()) return null;
|
|
2386
|
+
if (port < 1 || port > 65535) return null;
|
|
2387
|
+
return port;
|
|
2388
|
+
}
|
|
2389
|
+
function isBinaryFile(filePath) {
|
|
2390
|
+
let fd;
|
|
2391
|
+
try {
|
|
2392
|
+
fd = openSync(filePath, "r");
|
|
2393
|
+
const buf = Buffer.alloc(8192);
|
|
2394
|
+
const bytesRead = readSync2(fd, buf, 0, 8192, 0);
|
|
2395
|
+
for (let i = 0; i < bytesRead; i++) {
|
|
2396
|
+
if (buf[i] === 0) return true;
|
|
2397
|
+
}
|
|
2398
|
+
return false;
|
|
2399
|
+
} catch {
|
|
2400
|
+
return false;
|
|
2401
|
+
} finally {
|
|
2402
|
+
if (fd !== void 0) closeSync(fd);
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
async function isBinaryFileAsync(filePath) {
|
|
2406
|
+
let handle;
|
|
2407
|
+
try {
|
|
2408
|
+
handle = await fsOpen(filePath, "r");
|
|
2409
|
+
const buf = Buffer.alloc(8192);
|
|
2410
|
+
const { bytesRead } = await handle.read(buf, 0, 8192, 0);
|
|
2411
|
+
for (let i = 0; i < bytesRead; i++) {
|
|
2412
|
+
if (buf[i] === 0) return true;
|
|
2413
|
+
}
|
|
2414
|
+
return false;
|
|
2415
|
+
} catch {
|
|
2416
|
+
return false;
|
|
2417
|
+
} finally {
|
|
2418
|
+
if (handle) await handle.close().catch(() => {
|
|
2419
|
+
});
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
function validateLanguage(lang) {
|
|
2423
|
+
const normalized = lang.toLowerCase();
|
|
2424
|
+
const aliases = {
|
|
2425
|
+
ts: "typescript",
|
|
2426
|
+
tsx: "typescript",
|
|
2427
|
+
js: "javascript",
|
|
2428
|
+
jsx: "javascript",
|
|
2429
|
+
py: "python",
|
|
2430
|
+
cs: "csharp",
|
|
2431
|
+
rb: "ruby",
|
|
2432
|
+
rs: "rust"
|
|
2433
|
+
};
|
|
2434
|
+
const resolved = aliases[normalized] ?? normalized;
|
|
2435
|
+
return SUPPORTED_LANGUAGES.includes(resolved) ? resolved : null;
|
|
2436
|
+
}
|
|
1406
2437
|
function collectFiles(target) {
|
|
1407
2438
|
const abs = resolve2(target);
|
|
1408
|
-
if (!
|
|
2439
|
+
if (!existsSync6(abs)) {
|
|
1409
2440
|
return [];
|
|
1410
2441
|
}
|
|
1411
2442
|
const stat = statSync(abs);
|
|
1412
2443
|
if (stat.isFile()) return [abs];
|
|
1413
2444
|
const files = [];
|
|
1414
|
-
const supportedExts = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".java"]);
|
|
1415
2445
|
function walk(dir) {
|
|
1416
2446
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
1417
|
-
const fullPath =
|
|
2447
|
+
const fullPath = join5(dir, entry.name);
|
|
1418
2448
|
if (entry.isDirectory()) {
|
|
1419
2449
|
if (["node_modules", ".git", "dist", "build", ".pretense", ".next", "__pycache__"].includes(entry.name)) continue;
|
|
1420
2450
|
walk(fullPath);
|
|
1421
|
-
} else if (entry.isFile() &&
|
|
2451
|
+
} else if (entry.isFile() && isScannableFile(entry.name)) {
|
|
1422
2452
|
files.push(fullPath);
|
|
1423
2453
|
}
|
|
1424
2454
|
}
|
|
@@ -1427,10 +2457,12 @@ function collectFiles(target) {
|
|
|
1427
2457
|
return files;
|
|
1428
2458
|
}
|
|
1429
2459
|
var program = new Command();
|
|
1430
|
-
program.name("pretense").description("AI firewall CLI \u2014 mutates proprietary code before LLM API calls").version(VERSION)
|
|
2460
|
+
program.name("pretense").description("AI firewall CLI \u2014 mutates proprietary code before LLM API calls").version(VERSION).hook("preAction", () => {
|
|
2461
|
+
printBanner();
|
|
2462
|
+
});
|
|
1431
2463
|
program.command("init").description("Initialize .pretense/ config in current directory").option("-d, --dir <path>", "Target directory", process.cwd()).action((opts) => {
|
|
1432
2464
|
const dir = resolve2(opts.dir);
|
|
1433
|
-
console.log(
|
|
2465
|
+
console.log(chalk7.cyan("Initializing Pretense in"), chalk7.bold(dir));
|
|
1434
2466
|
const configDir = initConfig(dir);
|
|
1435
2467
|
const files = collectFiles(dir);
|
|
1436
2468
|
const langCounts = {};
|
|
@@ -1438,83 +2470,246 @@ program.command("init").description("Initialize .pretense/ config in current dir
|
|
|
1438
2470
|
const lang = langFromExt(f);
|
|
1439
2471
|
langCounts[lang] = (langCounts[lang] ?? 0) + 1;
|
|
1440
2472
|
}
|
|
1441
|
-
console.log(
|
|
1442
|
-
console.log(
|
|
2473
|
+
console.log(chalk7.green("\n .pretense/ created at"), chalk7.bold(configDir));
|
|
2474
|
+
console.log(chalk7.gray(`
|
|
1443
2475
|
Scanned ${files.length} source files:`));
|
|
1444
2476
|
for (const [lang, count] of Object.entries(langCounts)) {
|
|
1445
|
-
console.log(
|
|
2477
|
+
console.log(chalk7.gray(` ${lang}: ${count} files`));
|
|
1446
2478
|
}
|
|
1447
|
-
console.log(
|
|
2479
|
+
console.log(chalk7.cyan("\n Next: run"), chalk7.bold("pretense start"), chalk7.cyan("to launch the proxy"));
|
|
1448
2480
|
console.log();
|
|
1449
2481
|
});
|
|
1450
2482
|
program.command("start").description("Start the Pretense proxy server").option("-p, --port <number>", "Port number", "9339").option("-v, --verbose", "Enable verbose logging", false).action((opts) => {
|
|
1451
2483
|
const config = loadConfig();
|
|
1452
|
-
const port =
|
|
2484
|
+
const port = validatePort(opts.port);
|
|
2485
|
+
if (port === null) {
|
|
2486
|
+
console.error(chalk7.red("Error: Invalid port number:"), chalk7.bold(opts.port));
|
|
2487
|
+
console.error(chalk7.gray("Port must be an integer between 1 and 65535."));
|
|
2488
|
+
process.exit(1);
|
|
2489
|
+
}
|
|
1453
2490
|
startProxy({ config, port, verbose: opts.verbose });
|
|
1454
2491
|
});
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
2492
|
+
async function scanOneFile(file, opts) {
|
|
2493
|
+
let size = 0;
|
|
2494
|
+
try {
|
|
2495
|
+
const st = await fsStat(file);
|
|
2496
|
+
size = st.size;
|
|
2497
|
+
if (st.size > opts.maxFileSize) {
|
|
2498
|
+
return { file, identifiers: 0, secrets: 0, secretTypes: [], skipped: "too-large", sizeBytes: size };
|
|
2499
|
+
}
|
|
2500
|
+
} catch {
|
|
2501
|
+
return { file, identifiers: 0, secrets: 0, secretTypes: [], skipped: "read-error" };
|
|
2502
|
+
}
|
|
2503
|
+
if (!isSourceCodeFile(file)) {
|
|
2504
|
+
try {
|
|
2505
|
+
if (await isBinaryFileAsync(file)) {
|
|
2506
|
+
return { file, identifiers: 0, secrets: 0, secretTypes: [], skipped: "binary", sizeBytes: size };
|
|
2507
|
+
}
|
|
2508
|
+
} catch {
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
let code;
|
|
2512
|
+
try {
|
|
2513
|
+
code = await readFile(file, "utf-8");
|
|
2514
|
+
} catch {
|
|
2515
|
+
return { file, identifiers: 0, secrets: 0, secretTypes: [], skipped: "read-error", sizeBytes: size };
|
|
1460
2516
|
}
|
|
1461
|
-
|
|
1462
|
-
let
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
const code = readFileSync4(file, "utf-8");
|
|
1466
|
-
const lang = langFromExt(file);
|
|
1467
|
-
let identifiers = 0;
|
|
1468
|
-
if (!opts.secretsOnly) {
|
|
2517
|
+
const lang = langFromExt(file);
|
|
2518
|
+
let identifiers = 0;
|
|
2519
|
+
if (!opts.secretsOnly && isSourceCodeFile(file)) {
|
|
2520
|
+
try {
|
|
1469
2521
|
const scanResult = scan(code, lang);
|
|
1470
2522
|
identifiers = scanResult.tokens.length;
|
|
1471
|
-
|
|
2523
|
+
} catch {
|
|
1472
2524
|
}
|
|
2525
|
+
}
|
|
2526
|
+
let blocked = [];
|
|
2527
|
+
try {
|
|
1473
2528
|
const secretResult = scan2(code);
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
2529
|
+
blocked = secretResult.matches.filter((m) => m.action === "block" || m.action === "redact");
|
|
2530
|
+
} catch {
|
|
2531
|
+
}
|
|
2532
|
+
return {
|
|
2533
|
+
file,
|
|
2534
|
+
identifiers,
|
|
2535
|
+
secrets: blocked.length,
|
|
2536
|
+
secretTypes: blocked.map((m) => m.type),
|
|
2537
|
+
sizeBytes: size
|
|
2538
|
+
};
|
|
2539
|
+
}
|
|
2540
|
+
async function scanFilesParallel(files, opts) {
|
|
2541
|
+
const results = [];
|
|
2542
|
+
let cursor = 0;
|
|
2543
|
+
let done = 0;
|
|
2544
|
+
let lastPrint = 0;
|
|
2545
|
+
let interrupted = false;
|
|
2546
|
+
const stopHandler = () => {
|
|
2547
|
+
interrupted = true;
|
|
2548
|
+
};
|
|
2549
|
+
process.on("SIGINT", stopHandler);
|
|
2550
|
+
process.on("SIGTERM", stopHandler);
|
|
2551
|
+
const isTTY = process.stderr.isTTY === true;
|
|
2552
|
+
const printProgress = (force = false) => {
|
|
2553
|
+
if (opts.quiet) return;
|
|
2554
|
+
const now = Date.now();
|
|
2555
|
+
if (!force && now - lastPrint < PROGRESS_INTERVAL_MS) return;
|
|
2556
|
+
lastPrint = now;
|
|
2557
|
+
const found = results.reduce((acc, r) => acc + r.secrets, 0);
|
|
2558
|
+
const msg = `Scanning ${done}/${files.length} files (${found} secrets so far)...`;
|
|
2559
|
+
if (isTTY) {
|
|
2560
|
+
process.stderr.write(`\r\x1B[K${msg}`);
|
|
2561
|
+
} else {
|
|
2562
|
+
process.stderr.write(`${msg}
|
|
2563
|
+
`);
|
|
2564
|
+
}
|
|
2565
|
+
};
|
|
2566
|
+
async function worker() {
|
|
2567
|
+
while (!interrupted) {
|
|
2568
|
+
const i = cursor++;
|
|
2569
|
+
if (i >= files.length) return;
|
|
2570
|
+
const file = files[i];
|
|
2571
|
+
const row = await scanOneFile(file, { secretsOnly: opts.secretsOnly, maxFileSize: opts.maxFileSize });
|
|
2572
|
+
if (row) results.push(row);
|
|
2573
|
+
done++;
|
|
2574
|
+
printProgress();
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
printProgress(true);
|
|
2578
|
+
const workers = Array.from({ length: Math.min(opts.concurrency, files.length || 1) }, () => worker());
|
|
2579
|
+
await Promise.all(workers);
|
|
2580
|
+
process.removeListener("SIGINT", stopHandler);
|
|
2581
|
+
process.removeListener("SIGTERM", stopHandler);
|
|
2582
|
+
if (!opts.quiet && isTTY) {
|
|
2583
|
+
process.stderr.write(`\r\x1B[K`);
|
|
2584
|
+
}
|
|
2585
|
+
return { results, interrupted };
|
|
2586
|
+
}
|
|
2587
|
+
function parseSizeOption(value) {
|
|
2588
|
+
const trimmed = value.trim().toLowerCase();
|
|
2589
|
+
const m = /^(\d+(?:\.\d+)?)\s*(b|k|kb|m|mb|g|gb)?$/.exec(trimmed);
|
|
2590
|
+
if (!m) return null;
|
|
2591
|
+
const n = parseFloat(m[1]);
|
|
2592
|
+
if (!isFinite(n) || n <= 0) return null;
|
|
2593
|
+
const unit = m[2] ?? "b";
|
|
2594
|
+
const mult = unit.startsWith("g") ? 1024 ** 3 : unit.startsWith("m") ? 1024 ** 2 : unit.startsWith("k") ? 1024 : 1;
|
|
2595
|
+
return Math.floor(n * mult);
|
|
2596
|
+
}
|
|
2597
|
+
program.command("scan <target>").description("Scan file or directory for identifiers and secrets (no mutation)").option("--secrets-only", "Only scan for secrets/credentials", false).option("--json", "Output as JSON", false).option("--max-file-size <size>", "Skip files larger than this (e.g. 10mb, 500k)", "10mb").option("--concurrency <n>", "Max files scanned in parallel", String(SCAN_CONCURRENCY)).option("--quiet", "Suppress progress output", false).action(async (target, opts) => {
|
|
2598
|
+
const absTarget = resolve2(target);
|
|
2599
|
+
if (!existsSync6(absTarget)) {
|
|
2600
|
+
console.error(chalk7.red("Error: Path not found:"), chalk7.bold(absTarget));
|
|
2601
|
+
process.exit(1);
|
|
2602
|
+
}
|
|
2603
|
+
const maxFileSize = parseSizeOption(opts.maxFileSize);
|
|
2604
|
+
if (maxFileSize === null) {
|
|
2605
|
+
console.error(chalk7.red("Error: Invalid --max-file-size value:"), chalk7.bold(opts.maxFileSize));
|
|
2606
|
+
console.error(chalk7.gray("Use bytes or a unit suffix, e.g. 10mb, 500k, 1048576"));
|
|
2607
|
+
process.exit(1);
|
|
2608
|
+
}
|
|
2609
|
+
const concurrency = Math.max(1, parseInt(opts.concurrency, 10) || SCAN_CONCURRENCY);
|
|
2610
|
+
if (!opts.quiet && !opts.json) {
|
|
2611
|
+
process.stderr.write(chalk7.gray(`Discovering files in ${absTarget}...
|
|
2612
|
+
`));
|
|
2613
|
+
}
|
|
2614
|
+
const files = collectFiles(target);
|
|
2615
|
+
if (files.length === 0) {
|
|
2616
|
+
console.error(chalk7.red("Error: No source files found at"), chalk7.bold(absTarget));
|
|
2617
|
+
console.error(chalk7.gray("Supported extensions: " + [...SUPPORTED_EXTENSIONS].join(", ")));
|
|
2618
|
+
process.exit(1);
|
|
2619
|
+
}
|
|
2620
|
+
if (!opts.quiet && !opts.json) {
|
|
2621
|
+
process.stderr.write(chalk7.gray(`Found ${files.length} candidate files. Scanning with concurrency=${concurrency}...
|
|
2622
|
+
`));
|
|
1482
2623
|
}
|
|
2624
|
+
const startedAt = Date.now();
|
|
2625
|
+
const { results: allResults, interrupted } = await scanFilesParallel(files, {
|
|
2626
|
+
secretsOnly: opts.secretsOnly,
|
|
2627
|
+
maxFileSize,
|
|
2628
|
+
quiet: opts.quiet || opts.json,
|
|
2629
|
+
concurrency
|
|
2630
|
+
});
|
|
2631
|
+
const elapsedMs = Date.now() - startedAt;
|
|
2632
|
+
const totalIdentifiers = allResults.reduce((acc, r) => acc + r.identifiers, 0);
|
|
2633
|
+
const totalSecrets = allResults.reduce((acc, r) => acc + r.secrets, 0);
|
|
2634
|
+
const skippedLarge = allResults.filter((r) => r.skipped === "too-large").length;
|
|
2635
|
+
const skippedBinary = allResults.filter((r) => r.skipped === "binary").length;
|
|
1483
2636
|
if (opts.json) {
|
|
1484
|
-
console.log(JSON.stringify({
|
|
2637
|
+
console.log(JSON.stringify({
|
|
2638
|
+
files: allResults,
|
|
2639
|
+
totalIdentifiers,
|
|
2640
|
+
totalSecrets,
|
|
2641
|
+
skippedLarge,
|
|
2642
|
+
skippedBinary,
|
|
2643
|
+
elapsedMs,
|
|
2644
|
+
interrupted
|
|
2645
|
+
}, null, 2));
|
|
1485
2646
|
} else {
|
|
1486
|
-
console.log(
|
|
2647
|
+
console.log(chalk7.cyan("\nPretense Scan Results\n"));
|
|
1487
2648
|
for (const r of allResults) {
|
|
1488
|
-
|
|
2649
|
+
if (r.skipped) {
|
|
2650
|
+
if (r.skipped === "too-large") {
|
|
2651
|
+
const mb = ((r.sizeBytes ?? 0) / 1024 / 1024).toFixed(1);
|
|
2652
|
+
console.log(chalk7.gray(` ${r.file} \u2014 skipped (${mb} MB > limit)`));
|
|
2653
|
+
}
|
|
2654
|
+
continue;
|
|
2655
|
+
}
|
|
2656
|
+
const secretBadge = r.secrets > 0 ? chalk7.red(` [${r.secrets} SECRET${r.secrets > 1 ? "S" : ""}]`) : "";
|
|
2657
|
+
const showRow = r.secrets > 0 || !opts.secretsOnly && r.identifiers > 0;
|
|
2658
|
+
if (!showRow) continue;
|
|
1489
2659
|
console.log(
|
|
1490
|
-
|
|
2660
|
+
chalk7.gray(" ") + chalk7.white(r.file) + chalk7.gray(` \u2014 ${r.identifiers} identifiers`) + secretBadge
|
|
1491
2661
|
);
|
|
1492
2662
|
if (r.secrets > 0) {
|
|
1493
2663
|
for (const t of r.secretTypes) {
|
|
1494
|
-
console.log(
|
|
2664
|
+
console.log(chalk7.red(` ! ${t}`));
|
|
1495
2665
|
}
|
|
1496
2666
|
}
|
|
1497
2667
|
}
|
|
1498
|
-
|
|
1499
|
-
|
|
2668
|
+
const skipNote = skippedLarge + skippedBinary > 0 ? chalk7.gray(` (skipped ${skippedLarge} large, ${skippedBinary} binary)`) : "";
|
|
2669
|
+
console.log(
|
|
2670
|
+
chalk7.gray(`
|
|
2671
|
+
Total: ${allResults.length} files in ${(elapsedMs / 1e3).toFixed(2)}s, ${totalIdentifiers} identifiers, `) + (totalSecrets > 0 ? chalk7.red(`${totalSecrets} secrets found`) : chalk7.green("0 secrets")) + skipNote
|
|
2672
|
+
);
|
|
2673
|
+
if (interrupted) {
|
|
2674
|
+
console.log(chalk7.yellow("\n Interrupted \u2014 partial results shown above."));
|
|
2675
|
+
}
|
|
1500
2676
|
console.log();
|
|
1501
2677
|
}
|
|
2678
|
+
if (!opts.json) {
|
|
2679
|
+
printTokenFooter(allResults.length, totalIdentifiers);
|
|
2680
|
+
}
|
|
2681
|
+
if (interrupted) {
|
|
2682
|
+
process.exit(130);
|
|
2683
|
+
}
|
|
1502
2684
|
if (totalSecrets > 0) {
|
|
1503
2685
|
process.exit(2);
|
|
1504
2686
|
}
|
|
1505
2687
|
});
|
|
1506
|
-
program.command("mutate <file>").description("Mutate a file and print result to stdout").option("-s, --seed <seed>", "Mutation seed", "pretense").option("--save", "Save mutation map to .pretense/", false).option("--json", "Output as JSON (includes map + stats)", false).action((file, opts) => {
|
|
2688
|
+
program.command("mutate <file>").description("Mutate a file and print result to stdout").option("-s, --seed <seed>", "Mutation seed", "pretense").option("-l, --language <lang>", "Source language (typescript, javascript, python, go, java, csharp, ruby, rust)").option("--save", "Save mutation map to .pretense/", false).option("--json", "Output as JSON (includes map + stats)", false).action((file, opts) => {
|
|
1507
2689
|
const absPath = resolve2(file);
|
|
1508
|
-
if (!
|
|
1509
|
-
console.error(
|
|
2690
|
+
if (!existsSync6(absPath)) {
|
|
2691
|
+
console.error(chalk7.red("Error: File not found:"), chalk7.bold(absPath));
|
|
2692
|
+
process.exit(1);
|
|
2693
|
+
}
|
|
2694
|
+
if (opts.language) {
|
|
2695
|
+
const validLang = validateLanguage(opts.language);
|
|
2696
|
+
if (!validLang) {
|
|
2697
|
+
console.error(chalk7.red("Error: Unsupported language:"), chalk7.bold(opts.language));
|
|
2698
|
+
console.error(chalk7.gray("Supported languages: " + SUPPORTED_LANGUAGES.join(", ")));
|
|
2699
|
+
process.exit(1);
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
if (!SUPPORTED_EXTENSIONS.has(extname(absPath).toLowerCase()) && isBinaryFile(absPath)) {
|
|
2703
|
+
console.error(chalk7.red("Error: Not a supported source file:"), chalk7.bold(absPath));
|
|
2704
|
+
console.error(chalk7.gray("Supported extensions: " + [...SUPPORTED_EXTENSIONS].join(", ")));
|
|
1510
2705
|
process.exit(1);
|
|
1511
2706
|
}
|
|
1512
|
-
const code =
|
|
1513
|
-
const lang = langFromExt(absPath);
|
|
2707
|
+
const code = readFileSync6(absPath, "utf-8");
|
|
2708
|
+
const lang = opts.language ? validateLanguage(opts.language) : langFromExt(absPath);
|
|
1514
2709
|
const result = mutate(code, lang, opts.seed);
|
|
1515
2710
|
if (opts.save) {
|
|
1516
2711
|
const configDir = getConfigDir();
|
|
1517
|
-
const store = new MutationStore(
|
|
2712
|
+
const store = new MutationStore(join5(configDir, "mutation-map.json"));
|
|
1518
2713
|
store.load();
|
|
1519
2714
|
store.save({
|
|
1520
2715
|
id: absPath,
|
|
@@ -1524,7 +2719,7 @@ program.command("mutate <file>").description("Mutate a file and print result to
|
|
|
1524
2719
|
language: lang
|
|
1525
2720
|
});
|
|
1526
2721
|
store.persist();
|
|
1527
|
-
process.stderr.write(
|
|
2722
|
+
process.stderr.write(chalk7.gray(`Mutation map saved to ${configDir}/mutation-map.json
|
|
1528
2723
|
`));
|
|
1529
2724
|
}
|
|
1530
2725
|
if (opts.json) {
|
|
@@ -1535,35 +2730,44 @@ program.command("mutate <file>").description("Mutate a file and print result to
|
|
|
1535
2730
|
}, null, 2));
|
|
1536
2731
|
} else {
|
|
1537
2732
|
process.stdout.write(result.mutatedCode);
|
|
1538
|
-
process.stderr.write(
|
|
2733
|
+
process.stderr.write(chalk7.gray(`
|
|
1539
2734
|
--- ${result.stats.tokensMutated} identifiers mutated in ${result.stats.durationMs}ms ---
|
|
1540
2735
|
`));
|
|
2736
|
+
printTokenFooter(1, result.stats.tokensMutated);
|
|
1541
2737
|
}
|
|
1542
2738
|
});
|
|
1543
2739
|
program.command("reverse <file>").description("Reverse mutations using stored map").option("-m, --map <path>", "Path to mutation map JSON file").action((file, opts) => {
|
|
1544
2740
|
const absPath = resolve2(file);
|
|
1545
|
-
if (!
|
|
1546
|
-
console.error(
|
|
2741
|
+
if (!existsSync6(absPath)) {
|
|
2742
|
+
console.error(chalk7.red("Error: File not found:"), chalk7.bold(absPath));
|
|
1547
2743
|
process.exit(1);
|
|
1548
2744
|
}
|
|
1549
|
-
const mutatedCode =
|
|
1550
|
-
const mapPath = opts.map ? resolve2(opts.map) :
|
|
1551
|
-
if (!
|
|
1552
|
-
console.error(
|
|
1553
|
-
console.error(
|
|
2745
|
+
const mutatedCode = readFileSync6(absPath, "utf-8");
|
|
2746
|
+
const mapPath = opts.map ? resolve2(opts.map) : join5(getConfigDir(), "mutation-map.json");
|
|
2747
|
+
if (!existsSync6(mapPath)) {
|
|
2748
|
+
console.error(chalk7.red("Error: Mutation map not found:"), chalk7.bold(mapPath));
|
|
2749
|
+
console.error(chalk7.gray("Run 'pretense mutate --save' first, or specify --map <path>"));
|
|
2750
|
+
process.exit(1);
|
|
2751
|
+
}
|
|
2752
|
+
let store;
|
|
2753
|
+
try {
|
|
2754
|
+
store = new MutationStore(mapPath);
|
|
2755
|
+
store.load();
|
|
2756
|
+
} catch {
|
|
2757
|
+
console.error(chalk7.red("Error: Failed to load mutation map:"), chalk7.bold(mapPath));
|
|
2758
|
+
console.error(chalk7.gray("The file may be corrupted. Run 'pretense mutate --save' to regenerate."));
|
|
1554
2759
|
process.exit(1);
|
|
1555
2760
|
}
|
|
1556
|
-
const store = new MutationStore(mapPath);
|
|
1557
|
-
store.load();
|
|
1558
2761
|
const entry = store.get(absPath) ?? store.getByHash(absPath) ?? store.list(1)[0];
|
|
1559
2762
|
if (!entry) {
|
|
1560
|
-
console.error(
|
|
2763
|
+
console.error(chalk7.red("Error: No mutation map entries found."));
|
|
2764
|
+
console.error(chalk7.gray("Run 'pretense mutate --save <file>' first to generate a map."));
|
|
1561
2765
|
process.exit(1);
|
|
1562
2766
|
}
|
|
1563
2767
|
const map = MutationStore.toMap(entry);
|
|
1564
2768
|
const restored = reverse(mutatedCode, map);
|
|
1565
2769
|
process.stdout.write(restored);
|
|
1566
|
-
process.stderr.write(
|
|
2770
|
+
process.stderr.write(chalk7.gray(`
|
|
1567
2771
|
--- Reversed ${map.size} mutations ---
|
|
1568
2772
|
`));
|
|
1569
2773
|
});
|
|
@@ -1577,5 +2781,35 @@ program.command("audit").description("View the audit log").option("--json", "Out
|
|
|
1577
2781
|
program.command("version").description("Print Pretense CLI version").action(() => {
|
|
1578
2782
|
console.log(`@pretense/cli v${VERSION}`);
|
|
1579
2783
|
});
|
|
2784
|
+
program.command("status").description("Show current plan, monthly quota, seat usage").action(() => {
|
|
2785
|
+
runStatus();
|
|
2786
|
+
});
|
|
2787
|
+
program.command("usage").description("Alias for `pretense status` \u2014 show monthly quota usage").action(() => {
|
|
2788
|
+
runStatus();
|
|
2789
|
+
});
|
|
2790
|
+
program.command("tokens").description("Alias for `pretense credits` \u2014 show remaining mutation budget").action(() => {
|
|
2791
|
+
runCredits();
|
|
2792
|
+
});
|
|
2793
|
+
program.command("upgrade").description("Compare plans and upgrade to Pro or Enterprise").action(() => {
|
|
2794
|
+
runUpgrade();
|
|
2795
|
+
});
|
|
2796
|
+
program.command("credits").description("Show remaining mutation/scan budget for the current month").action(() => {
|
|
2797
|
+
runCredits();
|
|
2798
|
+
});
|
|
2799
|
+
program.command("login").description("Save a Pretense API key to ~/.pretense/config.json for future CLI runs").option("-k, --key <key>", "API key (pk_live_...). If omitted, read from $PRETENSE_API_KEY or stdin.").action((opts) => {
|
|
2800
|
+
const code = runLogin({ key: opts.key });
|
|
2801
|
+
if (code !== 0) process.exit(code);
|
|
2802
|
+
});
|
|
2803
|
+
function handleFatalError(err) {
|
|
2804
|
+
if (err instanceof Error && err.message) {
|
|
2805
|
+
console.error(chalk7.red("Error:"), err.message);
|
|
2806
|
+
} else {
|
|
2807
|
+
console.error(chalk7.red("An unexpected error occurred."));
|
|
2808
|
+
}
|
|
2809
|
+
process.exit(1);
|
|
2810
|
+
}
|
|
2811
|
+
process.on("uncaughtException", handleFatalError);
|
|
2812
|
+
process.on("unhandledRejection", handleFatalError);
|
|
2813
|
+
program.showSuggestionAfterError(true).showHelpAfterError("(use --help for available commands)");
|
|
1580
2814
|
program.parse();
|
|
1581
2815
|
//# sourceMappingURL=index.js.map
|