@blockyfy/stg-cli 0.4.0

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 ADDED
@@ -0,0 +1,3987 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+ import chalk9 from "chalk";
6
+ import {
7
+ readFileSync as readFileSync7,
8
+ existsSync as existsSync8,
9
+ readdirSync,
10
+ statSync,
11
+ openSync,
12
+ readSync as readSync2,
13
+ closeSync
14
+ } from "fs";
15
+ import { readFile, stat as fsStat, open as fsOpen } from "fs/promises";
16
+ import { resolve as resolve2, extname, join as join6, basename } from "path";
17
+ import os from "os";
18
+
19
+ // src/types.ts
20
+ var DEFAULT_CONFIG = {
21
+ port: 9339,
22
+ targetApis: ["https://api.anthropic.com", "https://api.openai.com"],
23
+ languages: ["typescript", "javascript", "python", "go", "java", "csharp", "ruby", "rust"],
24
+ mutationRules: [
25
+ { tokenType: "Variable" /* Variable */, prefix: "_v" },
26
+ { tokenType: "Function" /* Function */, prefix: "_fn" },
27
+ { tokenType: "Class" /* Class */, prefix: "_cls" },
28
+ { tokenType: "String" /* String */ },
29
+ { tokenType: "Comment" /* Comment */, strip: true },
30
+ { tokenType: "Import" /* Import */ },
31
+ { tokenType: "Property" /* Property */, prefix: "_prop" },
32
+ { tokenType: "Method" /* Method */, prefix: "_fn" }
33
+ ],
34
+ storePath: `${process.env["HOME"] ?? "/tmp"}/.pretest`
35
+ };
36
+
37
+ // src/scanner.ts
38
+ var TS_JS_KEYWORDS = /* @__PURE__ */ new Set([
39
+ "abstract",
40
+ "any",
41
+ "as",
42
+ "async",
43
+ "await",
44
+ "boolean",
45
+ "break",
46
+ "case",
47
+ "catch",
48
+ "class",
49
+ "const",
50
+ "constructor",
51
+ "continue",
52
+ "debugger",
53
+ "declare",
54
+ "default",
55
+ "delete",
56
+ "do",
57
+ "else",
58
+ "enum",
59
+ "export",
60
+ "extends",
61
+ "false",
62
+ "finally",
63
+ "for",
64
+ "from",
65
+ "function",
66
+ "get",
67
+ "if",
68
+ "implements",
69
+ "import",
70
+ "in",
71
+ "instanceof",
72
+ "interface",
73
+ "keyof",
74
+ "let",
75
+ "module",
76
+ "namespace",
77
+ "never",
78
+ "new",
79
+ "null",
80
+ "number",
81
+ "object",
82
+ "of",
83
+ "package",
84
+ "private",
85
+ "protected",
86
+ "public",
87
+ "readonly",
88
+ "require",
89
+ "return",
90
+ "set",
91
+ "static",
92
+ "string",
93
+ "super",
94
+ "switch",
95
+ "symbol",
96
+ "this",
97
+ "throw",
98
+ "true",
99
+ "try",
100
+ "type",
101
+ "typeof",
102
+ "undefined",
103
+ "unknown",
104
+ "var",
105
+ "void",
106
+ "while",
107
+ "with",
108
+ "yield",
109
+ "satisfies",
110
+ "using",
111
+ "infer",
112
+ "asserts"
113
+ ]);
114
+ var PYTHON_KEYWORDS = /* @__PURE__ */ new Set([
115
+ "False",
116
+ "None",
117
+ "True",
118
+ "and",
119
+ "as",
120
+ "assert",
121
+ "async",
122
+ "await",
123
+ "break",
124
+ "class",
125
+ "continue",
126
+ "def",
127
+ "del",
128
+ "elif",
129
+ "else",
130
+ "except",
131
+ "finally",
132
+ "for",
133
+ "from",
134
+ "global",
135
+ "if",
136
+ "import",
137
+ "in",
138
+ "is",
139
+ "lambda",
140
+ "nonlocal",
141
+ "not",
142
+ "or",
143
+ "pass",
144
+ "raise",
145
+ "return",
146
+ "try",
147
+ "while",
148
+ "with",
149
+ "yield",
150
+ "self",
151
+ "cls"
152
+ ]);
153
+ var GO_KEYWORDS = /* @__PURE__ */ new Set([
154
+ "break",
155
+ "case",
156
+ "chan",
157
+ "const",
158
+ "continue",
159
+ "default",
160
+ "defer",
161
+ "else",
162
+ "fallthrough",
163
+ "for",
164
+ "func",
165
+ "go",
166
+ "goto",
167
+ "if",
168
+ "import",
169
+ "interface",
170
+ "map",
171
+ "package",
172
+ "range",
173
+ "return",
174
+ "select",
175
+ "struct",
176
+ "switch",
177
+ "type",
178
+ "var",
179
+ "nil",
180
+ "true",
181
+ "false",
182
+ "error",
183
+ "string",
184
+ "int",
185
+ "int8",
186
+ "int16",
187
+ "int32",
188
+ "int64",
189
+ "uint",
190
+ "uint8",
191
+ "uint16",
192
+ "uint32",
193
+ "uint64",
194
+ "float32",
195
+ "float64",
196
+ "complex64",
197
+ "complex128",
198
+ "bool",
199
+ "byte",
200
+ "rune",
201
+ "uintptr",
202
+ "make",
203
+ "new",
204
+ "len",
205
+ "cap",
206
+ "append",
207
+ "copy",
208
+ "close",
209
+ "delete",
210
+ "panic",
211
+ "recover",
212
+ "print",
213
+ "println"
214
+ ]);
215
+ var JAVA_KEYWORDS = /* @__PURE__ */ new Set([
216
+ "abstract",
217
+ "assert",
218
+ "boolean",
219
+ "break",
220
+ "byte",
221
+ "case",
222
+ "catch",
223
+ "char",
224
+ "class",
225
+ "const",
226
+ "continue",
227
+ "default",
228
+ "do",
229
+ "double",
230
+ "else",
231
+ "enum",
232
+ "extends",
233
+ "final",
234
+ "finally",
235
+ "float",
236
+ "for",
237
+ "goto",
238
+ "if",
239
+ "implements",
240
+ "import",
241
+ "instanceof",
242
+ "int",
243
+ "interface",
244
+ "long",
245
+ "native",
246
+ "new",
247
+ "package",
248
+ "private",
249
+ "protected",
250
+ "public",
251
+ "return",
252
+ "short",
253
+ "static",
254
+ "strictfp",
255
+ "super",
256
+ "switch",
257
+ "synchronized",
258
+ "this",
259
+ "throw",
260
+ "throws",
261
+ "transient",
262
+ "try",
263
+ "void",
264
+ "volatile",
265
+ "while",
266
+ "true",
267
+ "false",
268
+ "null",
269
+ "var",
270
+ "record",
271
+ "sealed",
272
+ "permits",
273
+ "yield"
274
+ ]);
275
+ var CSHARP_KEYWORDS = /* @__PURE__ */ new Set([
276
+ "class",
277
+ "interface",
278
+ "namespace",
279
+ "public",
280
+ "private",
281
+ "protected",
282
+ "internal",
283
+ "static",
284
+ "void",
285
+ "return",
286
+ "new",
287
+ "using",
288
+ "override",
289
+ "sealed",
290
+ "abstract",
291
+ "virtual",
292
+ "readonly",
293
+ "const",
294
+ "var",
295
+ "int",
296
+ "string",
297
+ "bool",
298
+ "double",
299
+ "float",
300
+ "object",
301
+ "null",
302
+ "true",
303
+ "false",
304
+ "if",
305
+ "else",
306
+ "for",
307
+ "foreach",
308
+ "while",
309
+ "do",
310
+ "switch",
311
+ "case",
312
+ "break",
313
+ "continue",
314
+ "try",
315
+ "catch",
316
+ "finally",
317
+ "throw",
318
+ "async",
319
+ "await",
320
+ "get",
321
+ "set",
322
+ "partial",
323
+ "enum",
324
+ "struct",
325
+ "delegate",
326
+ "event"
327
+ ]);
328
+ var RUBY_KEYWORDS = /* @__PURE__ */ new Set([
329
+ "def",
330
+ "class",
331
+ "module",
332
+ "end",
333
+ "do",
334
+ "if",
335
+ "else",
336
+ "elsif",
337
+ "unless",
338
+ "while",
339
+ "until",
340
+ "for",
341
+ "begin",
342
+ "rescue",
343
+ "ensure",
344
+ "raise",
345
+ "return",
346
+ "yield",
347
+ "self",
348
+ "super",
349
+ "true",
350
+ "false",
351
+ "nil",
352
+ "and",
353
+ "or",
354
+ "not",
355
+ "in",
356
+ "then",
357
+ "case",
358
+ "when",
359
+ "puts",
360
+ "print",
361
+ "require",
362
+ "require_relative",
363
+ "include",
364
+ "extend",
365
+ "attr_accessor",
366
+ "attr_reader",
367
+ "attr_writer",
368
+ "initialize",
369
+ "new",
370
+ "public",
371
+ "private",
372
+ "protected"
373
+ ]);
374
+ var RUST_KEYWORDS = /* @__PURE__ */ new Set([
375
+ "fn",
376
+ "struct",
377
+ "enum",
378
+ "trait",
379
+ "impl",
380
+ "mod",
381
+ "pub",
382
+ "let",
383
+ "mut",
384
+ "const",
385
+ "type",
386
+ "use",
387
+ "crate",
388
+ "self",
389
+ "super",
390
+ "move",
391
+ "ref",
392
+ "match",
393
+ "if",
394
+ "else",
395
+ "loop",
396
+ "while",
397
+ "for",
398
+ "return",
399
+ "break",
400
+ "continue",
401
+ "where",
402
+ "as",
403
+ "in",
404
+ "true",
405
+ "false",
406
+ "unsafe",
407
+ "extern",
408
+ "dyn",
409
+ "box",
410
+ "static",
411
+ "async",
412
+ "await",
413
+ "Box",
414
+ "String",
415
+ "Vec",
416
+ "Option",
417
+ "Result",
418
+ "Ok",
419
+ "Err",
420
+ "Some",
421
+ "None"
422
+ ]);
423
+ function detectLanguage(code) {
424
+ if (/:[\s]*\w+[\s]*[;,{)]/.test(code) || /import.*from\s+['"]/.test(code) || /interface\s+\w+/.test(code)) return "typescript";
425
+ if (/\bdef\s+\w+\s*\(/.test(code) || /\bimport\s+\w+/.test(code) && /:$/.test(code)) return "python";
426
+ if (/\bfunc\s+\w+\s*\(/.test(code) || /\bpackage\s+\w+/.test(code)) return "go";
427
+ if (/\bpublic\s+class\s+\w+/.test(code) || /\bimport\s+java\./.test(code)) return "java";
428
+ if (/\bnamespace\s+\w+/.test(code) || /\busing\s+System/.test(code)) return "csharp";
429
+ if (/\bfn\s+\w+\s*\(/.test(code) || /\blet\s+mut\s+\w+/.test(code)) return "rust";
430
+ if (/\bdef\s+\w+/.test(code) && /\bend\b/.test(code)) return "ruby";
431
+ if (/\bconst\s+\w+\s*=/.test(code) || /\bfunction\s+\w+\s*\(/.test(code)) return "javascript";
432
+ return "typescript";
433
+ }
434
+ function buildLineIndex(code) {
435
+ const offsets = [];
436
+ for (let i = 0; i < code.length; i++) {
437
+ if (code.charCodeAt(i) === 10) offsets.push(i);
438
+ }
439
+ return offsets;
440
+ }
441
+ function lineFromIndex(lineOffsets, index) {
442
+ let lo = 0;
443
+ let hi = lineOffsets.length;
444
+ while (lo < hi) {
445
+ const mid = lo + hi >> 1;
446
+ if (lineOffsets[mid] < index) lo = mid + 1;
447
+ else hi = mid;
448
+ }
449
+ return lo + 1;
450
+ }
451
+ function scanTypeScript(code) {
452
+ const tokens = [];
453
+ if (!code.trim()) return tokens;
454
+ const lineOffsets = buildLineIndex(code);
455
+ const patterns = [
456
+ // Single-line comment
457
+ { re: /\/\/[^\n]*/g, type: "Comment" /* Comment */ },
458
+ // Multi-line comment
459
+ { re: /\/\*[\s\S]*?\*\//g, type: "Comment" /* Comment */ },
460
+ // Quoted HTTP header names (fetch/axios headers objects) — mutated, not vendor-identifying literals
461
+ { re: /"(?:x-[a-z0-9][a-z0-9-]*|authorization|api-key)"/gi, type: "HttpHeaderString" /* HttpHeaderString */ },
462
+ { re: /'(?:x-[a-z0-9][a-z0-9-]*|authorization|api-key)'/gi, type: "HttpHeaderString" /* HttpHeaderString */ },
463
+ // Unquoted `Authorization` key — value must start with a string/template (skips interface/type shapes)
464
+ {
465
+ re: /(?:\{|\,)\s*(Authorization)\s*:\s*(?=[`'"])/g,
466
+ type: "HttpHeaderString" /* HttpHeaderString */,
467
+ groupIndex: 1
468
+ },
469
+ // Import source (the module path string)
470
+ { re: /\bimport\b[^'"]*['"]([^'"]+)['"]/g, type: "Import" /* Import */, groupIndex: 1 },
471
+ // Class name
472
+ { re: /\bclass\s+([A-Za-z_$][A-Za-z0-9_$]*)/g, type: "Class" /* Class */, groupIndex: 1 },
473
+ // Function/method name (function keyword)
474
+ { re: /\bfunction\s+([A-Za-z_$][A-Za-z0-9_$]*)/g, type: "Function" /* Function */, groupIndex: 1 },
475
+ // Arrow function assigned to const/let/var
476
+ { re: /\b(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\b(?!\s*[-.][A-Za-z0-9_$])\s*=\s*(?:async\s*)?\(/g, type: "Function" /* Function */, groupIndex: 1 },
477
+ // Method: identifier followed by (
478
+ { re: /\b([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/g, type: "Method" /* Method */, groupIndex: 1 },
479
+ // Env-style binding names with hyphens or dots (e.g. AWS-SECRET-KEY, AWS.SECRET.KEY) — not valid JS identifiers but common in pasted config
480
+ { re: /\b(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*(?:(?:-|\.)(?:[A-Za-z0-9_$]+))+)(?=\s*=)/g, type: "Variable" /* Variable */, groupIndex: 1 },
481
+ // Variable declarations (exclude names continued with -/. so AWS-SECRET-KEY is captured only by the rule above)
482
+ { re: /\b(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\b(?!\s*[-.][A-Za-z0-9_$])/g, type: "Variable" /* Variable */, groupIndex: 1 },
483
+ // Member receiver: matches in matches.push, row in row.patientId, def in def.name
484
+ { re: /\b([A-Za-z_$][A-Za-z0-9_$]*)\s*(?:\.|\?\.)[^0-9]/g, type: "Variable" /* Variable */, groupIndex: 1 },
485
+ // Property after . or ?. when not immediately followed by ( — method calls use Method rule
486
+ { re: /\.([A-Za-z_$][A-Za-z0-9_$]*)\b(?!\s*\()/g, type: "Property" /* Property */, groupIndex: 1 },
487
+ { re: /\?\.([A-Za-z_$][A-Za-z0-9_$]*)\b(?!\s*\()/g, type: "Property" /* Property */, groupIndex: 1 },
488
+ // Template literals (preserve backtick wrapper)
489
+ { re: /`[^`]*`/g, type: "String" /* String */ },
490
+ // Double-quoted strings
491
+ { re: /"(?:[^"\\]|\\.)*"/g, type: "String" /* String */ },
492
+ // Single-quoted strings
493
+ { re: /'(?:[^'\\]|\\.)*'/g, type: "String" /* String */ }
494
+ ];
495
+ for (const { re, type, groupIndex } of patterns) {
496
+ re.lastIndex = 0;
497
+ let m;
498
+ while ((m = re.exec(code)) !== null) {
499
+ const fullMatch = m[0];
500
+ const captureValue = groupIndex !== void 0 ? m[groupIndex] ?? fullMatch : fullMatch;
501
+ const captureStart = groupIndex !== void 0 ? m.index + fullMatch.indexOf(captureValue) : m.index;
502
+ if (!captureValue || captureValue.trim() === "") continue;
503
+ if (type === "Variable" /* Variable */ || type === "Function" /* Function */ || type === "Class" /* Class */ || type === "Method" /* Method */ || type === "Property" /* Property */) {
504
+ if (TS_JS_KEYWORDS.has(captureValue)) continue;
505
+ if (/^(console|process|Object|Array|Math|JSON|Promise|Error|Map|Set|Date|RegExp|Symbol|Proxy|Reflect|global|globalThis|window|document|module|exports|require|__dirname|__filename|Intl|WebAssembly)$/.test(captureValue)) continue;
506
+ if (captureValue.length <= 1) continue;
507
+ }
508
+ if (type === "Property" /* Property */ && captureValue === "meta" && code.slice(Math.max(0, captureStart - 7), captureStart) === "import.") {
509
+ continue;
510
+ }
511
+ tokens.push({
512
+ type,
513
+ value: captureValue,
514
+ start: captureStart,
515
+ end: captureStart + captureValue.length,
516
+ line: lineFromIndex(lineOffsets, captureStart)
517
+ });
518
+ }
519
+ }
520
+ const commentSpans = tokens.filter((t) => t.type === "Comment" /* Comment */);
521
+ const stringLikeSpans = tokens.filter(
522
+ (t) => t.type === "String" /* String */ || t.type === "HttpHeaderString" /* HttpHeaderString */
523
+ );
524
+ const dropsSpanOverlap = (t) => {
525
+ if (t.type === "Variable" /* Variable */ || t.type === "Function" /* Function */ || t.type === "Class" /* Class */ || t.type === "Method" /* Method */ || t.type === "Property" /* Property */) {
526
+ if (commentSpans.some((c) => t.start < c.end && t.end > c.start)) return false;
527
+ if (stringLikeSpans.some((s) => t.start < s.end && t.end > s.start)) return false;
528
+ }
529
+ return true;
530
+ };
531
+ return deduplicate(tokens.filter(dropsSpanOverlap));
532
+ }
533
+ function scanPython(code) {
534
+ const tokens = [];
535
+ if (!code.trim()) return tokens;
536
+ const lineOffsets = buildLineIndex(code);
537
+ const patterns = [
538
+ // Triple-quoted docstrings
539
+ { re: /"""[\s\S]*?"""|'''[\s\S]*?'''/g, type: "Comment" /* Comment */ },
540
+ // Line comments
541
+ { re: /#[^\n]*/g, type: "Comment" /* Comment */ },
542
+ // Import module name
543
+ { re: /\bimport\s+([A-Za-z_][A-Za-z0-9_.]*)/g, type: "Import" /* Import */, groupIndex: 1 },
544
+ { re: /\bfrom\s+([A-Za-z_.][A-Za-z0-9_.]*)\s+import/g, type: "Import" /* Import */, groupIndex: 1 },
545
+ // Class name
546
+ { re: /\bclass\s+([A-Za-z_][A-Za-z0-9_]*)/g, type: "Class" /* Class */, groupIndex: 1 },
547
+ // Function/method def
548
+ { re: /\bdef\s+([A-Za-z_][A-Za-z0-9_]*)/g, type: "Function" /* Function */, groupIndex: 1 },
549
+ // Call site: method name in obj.method(
550
+ { re: /\b([A-Za-z_][A-Za-z0-9_]*)\s*\(/g, type: "Method" /* Method */, groupIndex: 1 },
551
+ // obj.attr receiver and attribute (matches.push-style)
552
+ { re: /\b([A-Za-z_][A-Za-z0-9_]*)\s*\.\s*(?=[a-zA-Z_])/g, type: "Variable" /* Variable */, groupIndex: 1 },
553
+ { re: /\.([A-Za-z_][A-Za-z0-9_]*)\b(?!\s*\()/g, type: "Property" /* Property */, groupIndex: 1 },
554
+ // Variable assignment (simple name = ...)
555
+ { re: /^([A-Za-z_][A-Za-z0-9_]*)\s*=/gm, type: "Variable" /* Variable */, groupIndex: 1 },
556
+ // Double-quoted strings
557
+ { re: /"(?:[^"\\]|\\.)*"/g, type: "String" /* String */ },
558
+ // Single-quoted strings
559
+ { re: /'(?:[^'\\]|\\.)*'/g, type: "String" /* String */ }
560
+ ];
561
+ for (const { re, type, groupIndex } of patterns) {
562
+ re.lastIndex = 0;
563
+ let m;
564
+ while ((m = re.exec(code)) !== null) {
565
+ const fullMatch = m[0];
566
+ const captureValue = groupIndex !== void 0 ? m[groupIndex] ?? fullMatch : fullMatch;
567
+ const captureStart = groupIndex !== void 0 ? m.index + fullMatch.indexOf(captureValue) : m.index;
568
+ if (!captureValue || captureValue.trim() === "") continue;
569
+ if (type === "Variable" /* Variable */ || type === "Function" /* Function */ || type === "Class" /* Class */ || type === "Method" /* Method */ || type === "Property" /* Property */) {
570
+ if (PYTHON_KEYWORDS.has(captureValue)) continue;
571
+ if (captureValue.length <= 1) continue;
572
+ if (captureValue.startsWith("__") && captureValue.endsWith("__")) continue;
573
+ }
574
+ tokens.push({
575
+ type,
576
+ value: captureValue,
577
+ start: captureStart,
578
+ end: captureStart + captureValue.length,
579
+ line: lineFromIndex(lineOffsets, captureStart)
580
+ });
581
+ }
582
+ }
583
+ const pyCommentSpans = tokens.filter((t) => t.type === "Comment" /* Comment */);
584
+ const pyStringSpans = tokens.filter((t) => t.type === "String" /* String */);
585
+ const dropsPyOverlap = (t) => {
586
+ if (t.type === "Variable" /* Variable */ || t.type === "Function" /* Function */ || t.type === "Class" /* Class */ || t.type === "Method" /* Method */ || t.type === "Property" /* Property */) {
587
+ if (pyCommentSpans.some((c) => t.start < c.end && t.end > c.start)) return false;
588
+ if (pyStringSpans.some((s) => t.start < s.end && t.end > s.start)) return false;
589
+ }
590
+ return true;
591
+ };
592
+ return deduplicate(tokens.filter(dropsPyOverlap));
593
+ }
594
+ function scanGo(code) {
595
+ const tokens = [];
596
+ if (!code.trim()) return tokens;
597
+ const lineOffsets = buildLineIndex(code);
598
+ const patterns = [
599
+ { re: /\/\/[^\n]*/g, type: "Comment" /* Comment */ },
600
+ { re: /\/\*[\s\S]*?\*\//g, type: "Comment" /* Comment */ },
601
+ // Import path
602
+ { re: /import\s*\(\s*([\s\S]*?)\s*\)/g, type: "Import" /* Import */ },
603
+ { re: /import\s+"([^"]+)"/g, type: "Import" /* Import */, groupIndex: 1 },
604
+ // Type declaration
605
+ { re: /\btype\s+([A-Za-z_][A-Za-z0-9_]*)\s+(?:struct|interface)/g, type: "Class" /* Class */, groupIndex: 1 },
606
+ // Func name
607
+ { re: /\bfunc\s+(?:\([^)]*\)\s*)?([A-Za-z_][A-Za-z0-9_]*)\s*\(/g, type: "Function" /* Function */, groupIndex: 1 },
608
+ // Var/const declaration
609
+ { re: /\b(?:var|const)\s+([A-Za-z_][A-Za-z0-9_]*)\b/g, type: "Variable" /* Variable */, groupIndex: 1 },
610
+ // Short variable declaration
611
+ { re: /\b([A-Za-z_][A-Za-z0-9_]*)\s*:=/g, type: "Variable" /* Variable */, groupIndex: 1 },
612
+ // Double-quoted strings
613
+ { re: /"(?:[^"\\]|\\.)*"/g, type: "String" /* String */ },
614
+ // Backtick raw strings
615
+ { re: /`[^`]*`/g, type: "String" /* String */ }
616
+ ];
617
+ for (const { re, type, groupIndex } of patterns) {
618
+ re.lastIndex = 0;
619
+ let m;
620
+ while ((m = re.exec(code)) !== null) {
621
+ const fullMatch = m[0];
622
+ const captureValue = groupIndex !== void 0 ? m[groupIndex] ?? fullMatch : fullMatch;
623
+ const captureStart = groupIndex !== void 0 ? m.index + fullMatch.indexOf(captureValue) : m.index;
624
+ if (!captureValue || captureValue.trim() === "") continue;
625
+ if (type === "Variable" /* Variable */ || type === "Function" /* Function */ || type === "Class" /* Class */) {
626
+ if (GO_KEYWORDS.has(captureValue)) continue;
627
+ if (captureValue.length <= 1) continue;
628
+ }
629
+ tokens.push({
630
+ type,
631
+ value: captureValue,
632
+ start: captureStart,
633
+ end: captureStart + captureValue.length,
634
+ line: lineFromIndex(lineOffsets, captureStart)
635
+ });
636
+ }
637
+ }
638
+ return deduplicate(tokens);
639
+ }
640
+ function scanJava(code) {
641
+ const tokens = [];
642
+ if (!code.trim()) return tokens;
643
+ const lineOffsets = buildLineIndex(code);
644
+ const patterns = [
645
+ { re: /\/\/[^\n]*/g, type: "Comment" /* Comment */ },
646
+ { re: /\/\*[\s\S]*?\*\//g, type: "Comment" /* Comment */ },
647
+ // Import statement
648
+ { re: /\bimport\s+([\w.]+);/g, type: "Import" /* Import */, groupIndex: 1 },
649
+ // Class/interface/enum
650
+ { re: /\b(?:class|interface|enum|record)\s+([A-Za-z_$][A-Za-z0-9_$]*)/g, type: "Class" /* Class */, groupIndex: 1 },
651
+ // Method declaration
652
+ { re: /\b(?:public|private|protected|static|final|abstract|synchronized|native|default)[\w\s<>\[\],]*\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/g, type: "Method" /* Method */, groupIndex: 1 },
653
+ // Variable declaration
654
+ { re: /\b(?:int|long|double|float|boolean|char|byte|short|String|Object|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*[=;,)]/g, type: "Variable" /* Variable */, groupIndex: 1 },
655
+ // Double-quoted strings
656
+ { re: /"(?:[^"\\]|\\.)*"/g, type: "String" /* String */ }
657
+ ];
658
+ for (const { re, type, groupIndex } of patterns) {
659
+ re.lastIndex = 0;
660
+ let m;
661
+ while ((m = re.exec(code)) !== null) {
662
+ const fullMatch = m[0];
663
+ const captureValue = groupIndex !== void 0 ? m[groupIndex] ?? fullMatch : fullMatch;
664
+ const captureStart = groupIndex !== void 0 ? m.index + fullMatch.indexOf(captureValue) : m.index;
665
+ if (!captureValue || captureValue.trim() === "") continue;
666
+ if (type === "Variable" /* Variable */ || type === "Method" /* Method */ || type === "Class" /* Class */) {
667
+ if (JAVA_KEYWORDS.has(captureValue)) continue;
668
+ if (captureValue.length <= 1) continue;
669
+ if (/^(String|Object|Integer|Long|Double|Boolean|List|Map|Set|Array|Exception|Error|System|Math|Thread|Class|Enum)$/.test(captureValue)) continue;
670
+ }
671
+ tokens.push({
672
+ type,
673
+ value: captureValue,
674
+ start: captureStart,
675
+ end: captureStart + captureValue.length,
676
+ line: lineFromIndex(lineOffsets, captureStart)
677
+ });
678
+ }
679
+ }
680
+ return deduplicate(tokens);
681
+ }
682
+ function scanCSharp(code) {
683
+ const tokens = [];
684
+ if (!code.trim()) return tokens;
685
+ const lineOffsets = buildLineIndex(code);
686
+ const patterns = [
687
+ { re: /\/\/[^\n]*/g, type: "Comment" /* Comment */ },
688
+ { re: /\/\*[\s\S]*?\*\//g, type: "Comment" /* Comment */ },
689
+ // Namespace/using import
690
+ { re: /\busing\s+([\w.]+);/g, type: "Import" /* Import */, groupIndex: 1 },
691
+ // Class/interface/struct/enum names
692
+ { re: /\b(?:class|interface|struct|enum)\s+([A-Za-z_][A-Za-z0-9_]*)/g, type: "Class" /* Class */, groupIndex: 1 },
693
+ // Method/property declaration
694
+ { re: /\b(?:public|private|protected|internal|static|virtual|override|async)\s+(?:\w+\s+)+([a-zA-Z_]\w*)\s*\(/g, type: "Method" /* Method */, groupIndex: 1 },
695
+ // Local variable declarations
696
+ { re: /\b(?:var|int|string|bool|double|float|object)\s+([a-zA-Z_]\w*)\b/g, type: "Variable" /* Variable */, groupIndex: 1 },
697
+ // Double-quoted strings
698
+ { re: /"(?:[^"\\]|\\.)*"/g, type: "String" /* String */ },
699
+ // Verbatim strings
700
+ { re: /@"(?:[^"]|"")*"/g, type: "String" /* String */ }
701
+ ];
702
+ for (const { re, type, groupIndex } of patterns) {
703
+ re.lastIndex = 0;
704
+ let m;
705
+ while ((m = re.exec(code)) !== null) {
706
+ const fullMatch = m[0];
707
+ const captureValue = groupIndex !== void 0 ? m[groupIndex] ?? fullMatch : fullMatch;
708
+ const captureStart = groupIndex !== void 0 ? m.index + fullMatch.indexOf(captureValue) : m.index;
709
+ if (!captureValue || captureValue.trim() === "") continue;
710
+ if (type === "Variable" /* Variable */ || type === "Method" /* Method */ || type === "Class" /* Class */) {
711
+ if (CSHARP_KEYWORDS.has(captureValue)) continue;
712
+ if (captureValue.length <= 1) continue;
713
+ if (/^(String|Object|Integer|Boolean|Double|Float|List|Dictionary|Array|Exception|Task|Console|Math|Enum|Delegate)$/.test(captureValue)) continue;
714
+ }
715
+ tokens.push({
716
+ type,
717
+ value: captureValue,
718
+ start: captureStart,
719
+ end: captureStart + captureValue.length,
720
+ line: lineFromIndex(lineOffsets, captureStart)
721
+ });
722
+ }
723
+ }
724
+ return deduplicate(tokens);
725
+ }
726
+ function scanRuby(code) {
727
+ const tokens = [];
728
+ if (!code.trim()) return tokens;
729
+ const lineOffsets = buildLineIndex(code);
730
+ const patterns = [
731
+ // Single-line comment
732
+ { re: /#[^\n]*/g, type: "Comment" /* Comment */ },
733
+ // Method names
734
+ { re: /\bdef\s+([a-zA-Z_]\w*[!?]?)/g, type: "Function" /* Function */, groupIndex: 1 },
735
+ // Class and module names
736
+ { re: /\b(?:class|module)\s+([A-Z][a-zA-Z0-9_]*)/g, type: "Class" /* Class */, groupIndex: 1 },
737
+ // Instance variables
738
+ { re: /@([a-zA-Z_]\w*)/g, type: "Property" /* Property */, groupIndex: 1 },
739
+ // All-caps constants (3+ chars to avoid single-letter constants)
740
+ { re: /\b([A-Z][A-Z0-9_]{2,})\b/g, type: "Variable" /* Variable */, groupIndex: 1 },
741
+ // Double-quoted strings
742
+ { re: /"(?:[^"\\]|\\.)*"/g, type: "String" /* String */ },
743
+ // Single-quoted strings
744
+ { re: /'(?:[^'\\]|\\.)*'/g, type: "String" /* String */ }
745
+ ];
746
+ for (const { re, type, groupIndex } of patterns) {
747
+ re.lastIndex = 0;
748
+ let m;
749
+ while ((m = re.exec(code)) !== null) {
750
+ const fullMatch = m[0];
751
+ const captureValue = groupIndex !== void 0 ? m[groupIndex] ?? fullMatch : fullMatch;
752
+ const captureStart = groupIndex !== void 0 ? m.index + fullMatch.indexOf(captureValue) : m.index;
753
+ if (!captureValue || captureValue.trim() === "") continue;
754
+ if (type === "Variable" /* Variable */ || type === "Function" /* Function */ || type === "Class" /* Class */ || type === "Property" /* Property */) {
755
+ if (RUBY_KEYWORDS.has(captureValue)) continue;
756
+ if (captureValue.length <= 1) continue;
757
+ }
758
+ tokens.push({
759
+ type,
760
+ value: captureValue,
761
+ start: captureStart,
762
+ end: captureStart + captureValue.length,
763
+ line: lineFromIndex(lineOffsets, captureStart)
764
+ });
765
+ }
766
+ }
767
+ return deduplicate(tokens);
768
+ }
769
+ function scanRust(code) {
770
+ const tokens = [];
771
+ if (!code.trim()) return tokens;
772
+ const lineOffsets = buildLineIndex(code);
773
+ const patterns = [
774
+ { re: /\/\/[^\n]*/g, type: "Comment" /* Comment */ },
775
+ { re: /\/\*[\s\S]*?\*\//g, type: "Comment" /* Comment */ },
776
+ // Use declarations
777
+ { re: /\buse\s+([\w:]+)/g, type: "Import" /* Import */, groupIndex: 1 },
778
+ // Named type declarations (struct, enum, trait)
779
+ { re: /\b(?:struct|enum|trait)\s+([A-Z][a-zA-Z0-9_]*)/g, type: "Class" /* Class */, groupIndex: 1 },
780
+ // Function names
781
+ { re: /\bfn\s+([a-zA-Z_]\w*)/g, type: "Function" /* Function */, groupIndex: 1 },
782
+ // Constants (ALL_CAPS)
783
+ { re: /\bconst\s+([A-Z_][A-Z0-9_]*)\s*:/g, type: "Variable" /* Variable */, groupIndex: 1 },
784
+ // Variable bindings
785
+ { re: /\blet\s+(?:mut\s+)?([a-zA-Z_]\w*)/g, type: "Variable" /* Variable */, groupIndex: 1 },
786
+ // Double-quoted strings
787
+ { re: /"(?:[^"\\]|\\.)*"/g, type: "String" /* String */ }
788
+ ];
789
+ for (const { re, type, groupIndex } of patterns) {
790
+ re.lastIndex = 0;
791
+ let m;
792
+ while ((m = re.exec(code)) !== null) {
793
+ const fullMatch = m[0];
794
+ const captureValue = groupIndex !== void 0 ? m[groupIndex] ?? fullMatch : fullMatch;
795
+ const captureStart = groupIndex !== void 0 ? m.index + fullMatch.indexOf(captureValue) : m.index;
796
+ if (!captureValue || captureValue.trim() === "") continue;
797
+ if (type === "Variable" /* Variable */ || type === "Function" /* Function */ || type === "Class" /* Class */) {
798
+ if (RUST_KEYWORDS.has(captureValue)) continue;
799
+ if (captureValue.length <= 1) continue;
800
+ }
801
+ tokens.push({
802
+ type,
803
+ value: captureValue,
804
+ start: captureStart,
805
+ end: captureStart + captureValue.length,
806
+ line: lineFromIndex(lineOffsets, captureStart)
807
+ });
808
+ }
809
+ }
810
+ return deduplicate(tokens);
811
+ }
812
+ function deduplicate(tokens) {
813
+ tokens.sort((a, b) => a.start - b.start);
814
+ const seen = /* @__PURE__ */ new Set();
815
+ const result = [];
816
+ for (const token of tokens) {
817
+ const key = `${token.start}:${token.end}`;
818
+ if (!seen.has(key)) {
819
+ seen.add(key);
820
+ result.push(token);
821
+ }
822
+ }
823
+ return result;
824
+ }
825
+ function scan(code, language) {
826
+ if (!code || !code.trim()) {
827
+ return { tokens: [], language };
828
+ }
829
+ const lang = language.toLowerCase();
830
+ let tokens;
831
+ switch (lang) {
832
+ case "typescript":
833
+ case "javascript":
834
+ case "ts":
835
+ case "js":
836
+ case "tsx":
837
+ case "jsx":
838
+ tokens = scanTypeScript(code);
839
+ break;
840
+ case "python":
841
+ case "py":
842
+ tokens = scanPython(code);
843
+ break;
844
+ case "go":
845
+ tokens = scanGo(code);
846
+ break;
847
+ case "java":
848
+ tokens = scanJava(code);
849
+ break;
850
+ case "csharp":
851
+ case "cs":
852
+ tokens = scanCSharp(code);
853
+ break;
854
+ case "ruby":
855
+ case "rb":
856
+ tokens = scanRuby(code);
857
+ break;
858
+ case "rust":
859
+ case "rs":
860
+ tokens = scanRust(code);
861
+ break;
862
+ default:
863
+ tokens = scanTypeScript(code);
864
+ }
865
+ return { tokens, language: lang };
866
+ }
867
+
868
+ // src/salt.ts
869
+ import { readFileSync, writeFileSync, mkdirSync, existsSync as existsSync2, renameSync as renameSync2 } from "fs";
870
+ import { join as join2 } from "path";
871
+ import { homedir } from "os";
872
+ import { randomBytes } from "crypto";
873
+
874
+ // src/config-dir.ts
875
+ import { existsSync, renameSync } from "fs";
876
+ import { join } from "path";
877
+ var PROJECT_CONFIG_DIR_NAME = ".pretest";
878
+ var LEGACY_PROJECT_CONFIG_DIR_NAME = ".pretense";
879
+ function migrateLegacyProjectConfigDir(parentDir) {
880
+ const next = join(parentDir, PROJECT_CONFIG_DIR_NAME);
881
+ const prev = join(parentDir, LEGACY_PROJECT_CONFIG_DIR_NAME);
882
+ if (existsSync(next)) return;
883
+ if (!existsSync(prev)) return;
884
+ try {
885
+ renameSync(prev, next);
886
+ } catch {
887
+ }
888
+ }
889
+
890
+ // src/salt.ts
891
+ var SALT_BYTES = 32;
892
+ function getSaltPath() {
893
+ const home = homedir();
894
+ migrateLegacyProjectConfigDir(home);
895
+ const dir = join2(home, PROJECT_CONFIG_DIR_NAME);
896
+ const legacyDir = join2(home, LEGACY_PROJECT_CONFIG_DIR_NAME);
897
+ const path = join2(dir, "mutation.salt");
898
+ const legacySalt = join2(legacyDir, "mutation.salt");
899
+ if (!existsSync2(path) && existsSync2(legacySalt)) {
900
+ try {
901
+ if (!existsSync2(dir)) mkdirSync(dir, { recursive: true });
902
+ renameSync2(legacySalt, path);
903
+ } catch {
904
+ }
905
+ }
906
+ return { dir, path };
907
+ }
908
+ var _cachedSalt = null;
909
+ function getMutationSalt() {
910
+ if (_cachedSalt) return _cachedSalt;
911
+ const { dir, path } = getSaltPath();
912
+ if (existsSync2(path)) {
913
+ try {
914
+ const raw = readFileSync(path, "utf-8").trim();
915
+ if (/^[0-9a-f]{64}$/i.test(raw)) {
916
+ _cachedSalt = raw.toLowerCase();
917
+ return _cachedSalt;
918
+ }
919
+ } catch {
920
+ }
921
+ }
922
+ const salt = randomBytes(SALT_BYTES).toString("hex");
923
+ try {
924
+ mkdirSync(dir, { recursive: true });
925
+ writeFileSync(path, salt, { mode: 384 });
926
+ } catch {
927
+ }
928
+ _cachedSalt = salt;
929
+ return salt;
930
+ }
931
+ function buildSaltedSeed(baseSeed) {
932
+ return `${baseSeed}:${getMutationSalt()}`;
933
+ }
934
+ function effectiveSeedForMutation(userSeed) {
935
+ if (userSeed === "pretense" || userSeed === "pretest") {
936
+ return buildSaltedSeed("pretense");
937
+ }
938
+ return userSeed;
939
+ }
940
+
941
+ // src/deterministic-id.ts
942
+ function hash32(str) {
943
+ let h = 5381;
944
+ for (let i = 0; i < str.length; i++) {
945
+ h = (h << 5) + h ^ str.charCodeAt(i);
946
+ }
947
+ return h >>> 0;
948
+ }
949
+ function toAlphaNum(n, len) {
950
+ const CHARS = "abcdefghijklmnopqrstuvwxyz0123456789";
951
+ let result = "";
952
+ let v = n;
953
+ for (let i = 0; i < len; i++) {
954
+ result = CHARS[v % CHARS.length] + result;
955
+ v = Math.floor(v / CHARS.length);
956
+ }
957
+ return result;
958
+ }
959
+ function deterministicSyntheticId(original, seed, prefix, len = 4) {
960
+ const combined = `${seed}:${prefix}:${original}`;
961
+ const h = hash32(combined);
962
+ return prefix + toAlphaNum(h, len);
963
+ }
964
+
965
+ // src/mutator.ts
966
+ function prefixForType(type) {
967
+ switch (type) {
968
+ case "Class" /* Class */:
969
+ return "_cls";
970
+ case "Function" /* Function */:
971
+ case "Method" /* Method */:
972
+ return "_fn";
973
+ case "Variable" /* Variable */:
974
+ return "_v";
975
+ case "Property" /* Property */:
976
+ return "_prop";
977
+ case "HttpHeaderString" /* HttpHeaderString */:
978
+ return "_hk";
979
+ default:
980
+ return "_tok";
981
+ }
982
+ }
983
+ function mutate(code, language, seed = "pretest") {
984
+ const startMs = performance.now();
985
+ const lang = language ?? detectLanguage(code);
986
+ if (!code || !code.trim()) {
987
+ const emptyMap = /* @__PURE__ */ new Map();
988
+ return {
989
+ mutatedCode: code,
990
+ map: emptyMap,
991
+ stats: { tokensScanned: 0, tokensMutated: 0, durationMs: 0, language: lang }
992
+ };
993
+ }
994
+ const effectiveSeed = effectiveSeedForMutation(seed);
995
+ const { tokens } = scan(code, lang);
996
+ const map = /* @__PURE__ */ new Map();
997
+ for (const token of tokens) {
998
+ if (map.has(token.value)) continue;
999
+ if (token.type === "Comment" /* Comment */) {
1000
+ continue;
1001
+ }
1002
+ if (token.type === "String" /* String */) {
1003
+ continue;
1004
+ }
1005
+ if (token.type === "HttpHeaderString" /* HttpHeaderString */) {
1006
+ const qm = /^("|')(.+)\1$/.exec(token.value);
1007
+ if (qm) {
1008
+ const inner = qm[2];
1009
+ const q = qm[1];
1010
+ const syntheticInner = deterministicSyntheticId(inner, effectiveSeed, prefixForType("HttpHeaderString" /* HttpHeaderString */));
1011
+ map.set(token.value, `${q}${syntheticInner}${q}`);
1012
+ } else {
1013
+ const syntheticInner = deterministicSyntheticId(token.value, effectiveSeed, prefixForType("HttpHeaderString" /* HttpHeaderString */));
1014
+ map.set(token.value, syntheticInner);
1015
+ }
1016
+ continue;
1017
+ }
1018
+ if (token.type === "Import" /* Import */) {
1019
+ continue;
1020
+ }
1021
+ const prefix = prefixForType(token.type);
1022
+ const synthetic = deterministicSyntheticId(token.value, effectiveSeed, prefix);
1023
+ map.set(token.value, synthetic);
1024
+ }
1025
+ const entries = [...map.entries()].sort((a, b) => b[0].length - a[0].length);
1026
+ let mutatedCode = code;
1027
+ for (const [original, synthetic] of entries) {
1028
+ if (!synthetic) continue;
1029
+ const escaped = original.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1030
+ if (original.match(/^[A-Za-z_$][A-Za-z0-9_$]*$/)) {
1031
+ mutatedCode = mutatedCode.replace(new RegExp(`\\b${escaped}\\b`, "g"), synthetic);
1032
+ } else {
1033
+ mutatedCode = mutatedCode.split(original).join(synthetic);
1034
+ }
1035
+ }
1036
+ const durationMs = Math.round((performance.now() - startMs) * 100) / 100;
1037
+ return {
1038
+ mutatedCode,
1039
+ map,
1040
+ stats: {
1041
+ tokensScanned: tokens.length,
1042
+ tokensMutated: map.size,
1043
+ durationMs,
1044
+ language: lang
1045
+ }
1046
+ };
1047
+ }
1048
+
1049
+ // src/reverser.ts
1050
+ function reverse(mutatedCode, map, secretsMap) {
1051
+ if (!mutatedCode) return mutatedCode;
1052
+ let result = mutatedCode;
1053
+ if (map.size > 0) {
1054
+ const reverseMap = /* @__PURE__ */ new Map();
1055
+ for (const [original, synthetic] of map.entries()) {
1056
+ if (synthetic === "" || !synthetic) continue;
1057
+ reverseMap.set(synthetic, original);
1058
+ }
1059
+ if (reverseMap.size > 0) {
1060
+ const sortedEntries = [...reverseMap.entries()].sort((a, b) => b[0].length - a[0].length);
1061
+ for (const [synthetic, original] of sortedEntries) {
1062
+ const escaped = synthetic.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1063
+ if (synthetic.match(/^[A-Za-z_$][A-Za-z0-9_$]*$/) || synthetic.match(/^_[a-z]+[a-z0-9]{4,}$/)) {
1064
+ result = result.replace(new RegExp(`\\b${escaped}\\b`, "g"), original);
1065
+ } else {
1066
+ result = result.split(synthetic).join(original);
1067
+ }
1068
+ }
1069
+ }
1070
+ }
1071
+ if (secretsMap && secretsMap.size > 0) {
1072
+ const sortedSecrets = [...secretsMap.entries()].sort((a, b) => b[0].length - a[0].length);
1073
+ for (const [placeholder, original] of sortedSecrets) {
1074
+ result = result.split(placeholder).join(original);
1075
+ }
1076
+ }
1077
+ return result;
1078
+ }
1079
+
1080
+ // src/secrets.ts
1081
+ var SECRET_PATTERNS = [
1082
+ { name: "anthropic-api-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /sk-ant-api\d{2}-[A-Za-z0-9_-]{40,}/g },
1083
+ /** Legacy `sk-…` and modern `sk-proj-…` (hyphenated body); aligned with upstream scanner heuristics */
1084
+ { name: "openai-api-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /sk-(?:proj-)?[A-Za-z0-9_-]{20,}/g },
1085
+ { name: "aws-access-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /AKIA[0-9A-Z]{16}/g },
1086
+ /** `AWS_SECRET = '…40 chars…'` (not `AWS_SECRET_ACCESS_KEY`, which is covered below). */
1087
+ { name: "aws-secret-assignment", category: "secret", severity: "critical", defaultAction: "block", pattern: /\bAWS_SECRET\s*=\s*['"]([A-Za-z0-9/+=]{40})['"]/gd, valueGroup: 1 },
1088
+ /**
1089
+ * Env-style labels with `-` or `.` (e.g. `AWS-SECRET-KEY`, `AWS.SECRET.KEY`, long `ACCESS` forms).
1090
+ * Case-insensitive `AWS` / `SECRET` / `KEY`. Value: 40-char secret; quotes optional (matches aws-secret-key).
1091
+ */
1092
+ {
1093
+ name: "aws-secret-key-delimited",
1094
+ category: "secret",
1095
+ severity: "critical",
1096
+ defaultAction: "block",
1097
+ pattern: /\bAWS[-.]SECRET(?:[-.]ACCESS)?[-.]KEY\s*[=:]\s*['"]?([A-Za-z0-9/+=]{40})['"]?/gdi,
1098
+ valueGroup: 1
1099
+ },
1100
+ { name: "aws-secret-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /(?:aws_secret_access_key|AWS_SECRET_ACCESS_KEY)\s*[=:]\s*['"]?([A-Za-z0-9/+=]{40})['"]?/gd, valueGroup: 1 },
1101
+ { name: "github-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /gh[ps]_[A-Za-z0-9_]{36,}/g },
1102
+ { name: "github-fine-grained", category: "secret", severity: "critical", defaultAction: "block", pattern: /github_pat_[A-Za-z0-9_]{22,}/g },
1103
+ { name: "stripe-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /(?:sk|pk|rk)_(?:test|live)_[A-Za-z0-9]{20,}/g },
1104
+ { name: "private-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g },
1105
+ /** Three base64url JWT segments; payload segment is not required to start with `eyJ` (Supabase and many issuers use compact payloads). */
1106
+ { name: "jwt-token", category: "secret", severity: "high", defaultAction: "block", pattern: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{4,}/g },
1107
+ { name: "database-url", category: "credential", severity: "critical", defaultAction: "block", pattern: /(?:postgres|mysql|mongodb|redis|amqp):\/\/[^\s'"\\)]+:[^\s'"\\)]+@[^\s'"\\)]+/g },
1108
+ {
1109
+ name: "generic-password",
1110
+ category: "credential",
1111
+ severity: "high",
1112
+ defaultAction: "redact",
1113
+ // Do not match `token` / `secret` inside identifiers (e.g. GITHUB_TOKEN, AWS_SECRET): require no word/underscore before keyword.
1114
+ pattern: /(?<![A-Za-z0-9_])(?:password|passwd|pwd|secret|token|api_key|apikey|api-key)\s*[=:]\s*['"]([^\s'"]{8,})['"]/gid,
1115
+ valueGroup: 1
1116
+ },
1117
+ { name: "slack-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /xox[bpors]-[A-Za-z0-9-]{10,}/g },
1118
+ { name: "google-api-key", category: "secret", severity: "high", defaultAction: "block", pattern: /AIza[A-Za-z0-9_-]{35}/g },
1119
+ { name: "npm-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /npm_[A-Za-z0-9]{36}/g },
1120
+ { name: "sendgrid-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}/g },
1121
+ { name: "twilio-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /SK[a-f0-9]{32}/g },
1122
+ // ─── Additional patterns (v0.3) ──────────────────────────────────────────────
1123
+ { name: "azure-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /(?:AccountKey|SharedAccessKey)=[A-Za-z0-9+/=]{40,}/g },
1124
+ { name: "mailgun-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /key-[a-f0-9]{32}/g },
1125
+ { name: "twilio-sid", category: "secret", severity: "high", defaultAction: "block", pattern: /AC[a-f0-9]{32}/g },
1126
+ { name: "heroku-api-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /[hH]eroku.*[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/g },
1127
+ { name: "shopify-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /shpat_[a-fA-F0-9]{32}/g },
1128
+ { name: "datadog-key", category: "secret", severity: "high", defaultAction: "block", pattern: /dd[a-z]{1,2}_[a-zA-Z0-9]{32,}/g },
1129
+ { name: "vercel-token", category: "secret", severity: "high", defaultAction: "block", pattern: /vc_[a-zA-Z0-9]{24,}/g },
1130
+ { name: "supabase-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /sbp_[a-f0-9]{40}/g },
1131
+ { name: "linear-api-key", category: "secret", severity: "high", defaultAction: "block", pattern: /lin_api_[A-Za-z0-9]{40}/g },
1132
+ { name: "cloudflare-api-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /v1\.0-[a-f0-9]{24}-[a-f0-9]{146}/g },
1133
+ { name: "discord-bot-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /[MN][A-Za-z\d]{23,}\.[\w-]{6}\.[\w-]{27}/g },
1134
+ { name: "grafana-api-key", category: "secret", severity: "high", defaultAction: "block", pattern: /eyJrIjoi[A-Za-z0-9_-]{40,}/g },
1135
+ { name: "confluent-key", category: "secret", severity: "high", defaultAction: "block", pattern: /CONFLUENT[A-Z_]*\s*[=:]\s*['"]?[A-Za-z0-9/+=]{16,}['"]?/g },
1136
+ { name: "digitalocean-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /dop_v1_[a-f0-9]{64}/g },
1137
+ { name: "doppler-token", category: "secret", severity: "high", defaultAction: "block", pattern: /dp\.st\.[a-z0-9_-]{2,}\.[A-Za-z0-9]{40,}/g },
1138
+ { name: "planetscale-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /pscale_tkn_[A-Za-z0-9_-]{32,}/g },
1139
+ { name: "hashicorp-vault-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /hvs\.[A-Za-z0-9_-]{24,}/g },
1140
+ { name: "fastly-api-key", category: "secret", severity: "high", defaultAction: "block", pattern: /fastly_[A-Za-z0-9]{32}/g },
1141
+ { name: "algolia-api-key", category: "secret", severity: "high", defaultAction: "block", pattern: /[a-f0-9]{32}/g, validate: (m) => /ALGOLIA|algolia/.test(m) },
1142
+ { name: "pulumi-token", category: "secret", severity: "high", defaultAction: "block", pattern: /pul-[a-f0-9]{40}/g },
1143
+ // ─── Extended patterns (v0.6 unit 8) ─────────────────────────────────────────
1144
+ { name: "google-oauth-refresh", category: "secret", severity: "critical", defaultAction: "block", pattern: /\bGOCSPX-[A-Za-z0-9_-]{20,}\b/g },
1145
+ { 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 },
1146
+ { name: "sentry-dsn", category: "secret", severity: "high", defaultAction: "redact", pattern: /https:\/\/[a-f0-9]{32}@o\d+\.ingest\.sentry\.io\/\d+/g },
1147
+ { name: "x509-certificate", category: "secret", severity: "high", defaultAction: "redact", pattern: /-----BEGIN CERTIFICATE-----/g },
1148
+ { 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 },
1149
+ { 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 },
1150
+ /** Stripe signing secrets are often 32+ chars; allow shorter synthetic/test tails so redaction still applies. */
1151
+ { name: "stripe-webhook-secret", category: "secret", severity: "critical", defaultAction: "block", pattern: /\bwhsec_[A-Za-z0-9]{16,}\b/g },
1152
+ /** Stripe Price / product IDs (`price_…`) — identifiers, not card data, but leak billing context. */
1153
+ { name: "stripe-price-id", category: "secret", severity: "high", defaultAction: "block", pattern: /\bprice_[A-Za-z0-9_]{14,}\b/g },
1154
+ /** `FOO_PASSWORD = '…'` / `NEXT_PUBLIC_*_PASSWORD` — generic-password skips when `password` is preceded by `_`. */
1155
+ {
1156
+ name: "env-password-assignment",
1157
+ category: "credential",
1158
+ severity: "high",
1159
+ defaultAction: "block",
1160
+ pattern: /\b[A-Z][A-Z0-9_]*PASSWORD\s*=\s*['"]([^'"]{6,})['"]/gd,
1161
+ valueGroup: 1
1162
+ },
1163
+ /** Quoted common DB dialect names — stack hints when mutating env-style config. */
1164
+ {
1165
+ name: "quoted-db-dialect",
1166
+ category: "credential",
1167
+ severity: "medium",
1168
+ defaultAction: "block",
1169
+ pattern: /(['"])(postgres|mysql|redis)\1/gd,
1170
+ valueGroup: 2
1171
+ }
1172
+ ];
1173
+ var PII_PATTERNS = [
1174
+ { name: "ssn", category: "pii", severity: "critical", defaultAction: "redact", pattern: /\b\d{3}-\d{2}-\d{4}\b/g },
1175
+ { name: "credit-card", category: "pii", severity: "critical", defaultAction: "redact", pattern: /\b(?:\d[ -]*?){13,19}\b/g, validate: luhnCheck },
1176
+ { name: "email", category: "pii", severity: "medium", defaultAction: "warn", pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g },
1177
+ { name: "phone-us", category: "pii", severity: "medium", defaultAction: "warn", pattern: /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g },
1178
+ { name: "ip-address", category: "pii", severity: "low", defaultAction: "warn", pattern: /\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\b/g, validate: (ip) => !ip.startsWith("127.") && !ip.startsWith("0.") && ip !== "255.255.255.255" }
1179
+ ];
1180
+ var ENV_PATTERNS = [
1181
+ { name: "env-secret", category: "credential", severity: "high", defaultAction: "block", pattern: /(?:process\.env\.|os\.environ\[|ENV\[|getenv\()['"]?((?:SECRET|TOKEN|PASSWORD|API_KEY|PRIVATE|AUTH)[A-Z_0-9]*)['"]?\]?\)?/gi }
1182
+ ];
1183
+ function luhnCheck(num) {
1184
+ const digits = num.replace(/\D/g, "");
1185
+ if (digits.length < 13 || digits.length > 19) return false;
1186
+ let sum = 0;
1187
+ let alternate = false;
1188
+ for (let i = digits.length - 1; i >= 0; i--) {
1189
+ let n = parseInt(digits[i], 10);
1190
+ if (alternate) {
1191
+ n *= 2;
1192
+ if (n > 9) n -= 9;
1193
+ }
1194
+ sum += n;
1195
+ alternate = !alternate;
1196
+ }
1197
+ return sum % 10 === 0;
1198
+ }
1199
+ function shannonEntropy(str) {
1200
+ if (str.length === 0) return 0;
1201
+ const freq = /* @__PURE__ */ new Map();
1202
+ for (const ch of str) {
1203
+ freq.set(ch, (freq.get(ch) ?? 0) + 1);
1204
+ }
1205
+ let entropy = 0;
1206
+ const len = str.length;
1207
+ for (const count of freq.values()) {
1208
+ const p = count / len;
1209
+ if (p > 0) entropy -= p * Math.log2(p);
1210
+ }
1211
+ return entropy;
1212
+ }
1213
+ function isHighEntropy(str, minLength = 20, threshold = 4.5) {
1214
+ if (str.length <= minLength) return false;
1215
+ return shannonEntropy(str) > threshold;
1216
+ }
1217
+ function maskValue(value, type) {
1218
+ if (type.includes("key") || type.includes("token") || type.includes("secret")) return value.slice(0, 6) + "..." + value.slice(-4);
1219
+ if (type === "ssn") return "***-**-" + value.slice(-4);
1220
+ if (type === "credit-card") return "****-****-****-" + value.replace(/\D/g, "").slice(-4);
1221
+ if (type === "email") {
1222
+ const [local, domain] = value.split("@");
1223
+ return (local?.[0] ?? "*") + "***@" + (domain ?? "");
1224
+ }
1225
+ if (value.length > 12) return value.slice(0, 4) + "..." + value.slice(-4);
1226
+ return "***";
1227
+ }
1228
+ var matchCounter = 0;
1229
+ var ENTROPY_SCAN_MAX_BYTES = 512 * 1024;
1230
+ function scan2(text, actionOverrides) {
1231
+ const start = performance.now();
1232
+ const matches = [];
1233
+ const allPatterns = [...SECRET_PATTERNS, ...PII_PATTERNS, ...ENV_PATTERNS];
1234
+ for (const def of allPatterns) {
1235
+ def.pattern.lastIndex = 0;
1236
+ let m;
1237
+ while ((m = def.pattern.exec(text)) !== null) {
1238
+ let value = m[0];
1239
+ let start2 = m.index;
1240
+ let end = start2 + value.length;
1241
+ if (def.valueGroup !== void 0) {
1242
+ const span = m.indices?.[def.valueGroup];
1243
+ if (!span) continue;
1244
+ const [gs, ge] = span;
1245
+ value = text.slice(gs, ge);
1246
+ start2 = gs;
1247
+ end = ge;
1248
+ }
1249
+ if (def.validate && !def.validate(value)) continue;
1250
+ const action = actionOverrides?.[def.name] ?? def.defaultAction;
1251
+ matchCounter++;
1252
+ matches.push({
1253
+ id: `scan_${matchCounter}`,
1254
+ category: def.category,
1255
+ type: def.name,
1256
+ value,
1257
+ masked: maskValue(value, def.name),
1258
+ start: start2,
1259
+ end,
1260
+ severity: def.severity,
1261
+ action
1262
+ });
1263
+ }
1264
+ }
1265
+ if (text.length <= ENTROPY_SCAN_MAX_BYTES) {
1266
+ const sortedByStart = [...matches].sort((a, b) => a.start - b.start);
1267
+ const coveredEnd = (idx) => {
1268
+ let lo = 0;
1269
+ let hi = sortedByStart.length - 1;
1270
+ let best = -1;
1271
+ while (lo <= hi) {
1272
+ const mid = lo + hi >> 1;
1273
+ if (sortedByStart[mid].start <= idx) {
1274
+ best = mid;
1275
+ lo = mid + 1;
1276
+ } else {
1277
+ hi = mid - 1;
1278
+ }
1279
+ }
1280
+ return best >= 0 ? sortedByStart[best].end : -1;
1281
+ };
1282
+ const highEntropyPattern = /\b[A-Za-z0-9_\-/.+=$]{21,}\b/g;
1283
+ highEntropyPattern.lastIndex = 0;
1284
+ let hem;
1285
+ while ((hem = highEntropyPattern.exec(text)) !== null) {
1286
+ const value = hem[0];
1287
+ const end = hem.index + value.length;
1288
+ if (coveredEnd(hem.index) >= end) continue;
1289
+ if (isHighEntropy(value)) {
1290
+ matchCounter++;
1291
+ matches.push({
1292
+ id: `scan_${matchCounter}`,
1293
+ category: "secret",
1294
+ type: "high-entropy-string",
1295
+ value,
1296
+ masked: value.slice(0, 6) + "..." + value.slice(-4),
1297
+ start: hem.index,
1298
+ end,
1299
+ severity: "medium",
1300
+ action: actionOverrides?.["high-entropy-string"] ?? "warn"
1301
+ });
1302
+ }
1303
+ }
1304
+ }
1305
+ const severityRank = { critical: 4, high: 3, medium: 2, low: 1 };
1306
+ matches.sort((a, b) => a.start - b.start || severityRank[b.severity] - severityRank[a.severity]);
1307
+ const deduped = [];
1308
+ let lastEnd = -1;
1309
+ for (const match of matches) {
1310
+ if (match.start >= lastEnd) {
1311
+ deduped.push(match);
1312
+ lastEnd = match.end;
1313
+ }
1314
+ }
1315
+ return {
1316
+ clean: deduped.filter((m) => m.action === "block" || m.action === "redact").length === 0,
1317
+ matches: deduped,
1318
+ scannedAt: Date.now(),
1319
+ durationMs: Math.round((performance.now() - start) * 100) / 100
1320
+ };
1321
+ }
1322
+ function applyRedactions(text, matches) {
1323
+ return applyRedactionsTracked(text, matches).redactedCode;
1324
+ }
1325
+ var SECRET_LITERAL_PREFIX = "_sl";
1326
+ function applyRedactionsTracked(text, matches, mutationSeed = buildSaltedSeed("pretense")) {
1327
+ const actionable = matches.filter((m) => m.action === "block" || m.action === "redact");
1328
+ const sorted = [...actionable].sort((a, b) => b.start - a.start);
1329
+ const secretsMap = /* @__PURE__ */ new Map();
1330
+ const placeholders = /* @__PURE__ */ new Map();
1331
+ const forward = [...actionable].sort((a, b) => a.start - b.start);
1332
+ for (const match of forward) {
1333
+ const tag = deterministicSyntheticId(`${match.id}:${match.start}`, mutationSeed, SECRET_LITERAL_PREFIX);
1334
+ placeholders.set(match.id, tag);
1335
+ secretsMap.set(tag, match.value);
1336
+ }
1337
+ let result = text;
1338
+ for (const match of sorted) {
1339
+ const tag = placeholders.get(match.id);
1340
+ result = result.slice(0, match.start) + tag + result.slice(match.end);
1341
+ }
1342
+ return { redactedCode: result, secretsMap };
1343
+ }
1344
+
1345
+ // src/store.ts
1346
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync3 } from "fs";
1347
+ import { dirname } from "path";
1348
+ var MutationStore = class {
1349
+ entries = /* @__PURE__ */ new Map();
1350
+ filePath;
1351
+ constructor(filePath) {
1352
+ this.filePath = filePath;
1353
+ }
1354
+ /**
1355
+ * Persist a mutation map with associated metadata.
1356
+ *
1357
+ * @example
1358
+ * store.save({ id: "req-1", originalHash: "abc", mapEntries: [...], timestamp: Date.now(), language: "typescript" });
1359
+ */
1360
+ save(entry) {
1361
+ this.entries.set(entry.id, { ...entry });
1362
+ }
1363
+ /**
1364
+ * Retrieve a stored entry by request ID.
1365
+ *
1366
+ * @example
1367
+ * const entry = store.get("req-1");
1368
+ */
1369
+ get(id) {
1370
+ return this.entries.get(id) ?? null;
1371
+ }
1372
+ /**
1373
+ * Find the most recent entry whose originalHash matches.
1374
+ *
1375
+ * @example
1376
+ * const entry = store.getByHash("sha256-abc123");
1377
+ */
1378
+ getByHash(hash) {
1379
+ for (const entry of [...this.entries.values()].reverse()) {
1380
+ if (entry.originalHash === hash) return entry;
1381
+ }
1382
+ return null;
1383
+ }
1384
+ /**
1385
+ * Return all stored entries, newest first, up to `limit`.
1386
+ *
1387
+ * @example
1388
+ * store.list(10) // last 10 entries
1389
+ */
1390
+ list(limit = 50) {
1391
+ const all = [...this.entries.values()].reverse();
1392
+ return all.slice(0, limit);
1393
+ }
1394
+ /**
1395
+ * Delete an entry by ID. Returns true if it existed.
1396
+ */
1397
+ delete(id) {
1398
+ return this.entries.delete(id);
1399
+ }
1400
+ /**
1401
+ * Remove all entries from memory.
1402
+ */
1403
+ clear() {
1404
+ this.entries.clear();
1405
+ }
1406
+ /**
1407
+ * Write current in-memory state to the JSON file.
1408
+ */
1409
+ persist() {
1410
+ const dir = dirname(this.filePath);
1411
+ if (!existsSync3(dir)) {
1412
+ mkdirSync2(dir, { recursive: true });
1413
+ }
1414
+ const data = JSON.stringify([...this.entries.values()], null, 2);
1415
+ writeFileSync2(this.filePath, data, "utf-8");
1416
+ }
1417
+ /**
1418
+ * Load entries from the JSON file into memory.
1419
+ * No-ops if the file doesn't exist.
1420
+ */
1421
+ load() {
1422
+ if (!existsSync3(this.filePath)) return;
1423
+ try {
1424
+ const raw = readFileSync2(this.filePath, "utf-8");
1425
+ const parsed = JSON.parse(raw);
1426
+ this.entries.clear();
1427
+ for (const entry of parsed) {
1428
+ this.entries.set(entry.id, entry);
1429
+ }
1430
+ } catch {
1431
+ this.entries.clear();
1432
+ }
1433
+ }
1434
+ /** Total entries in memory */
1435
+ get size() {
1436
+ return this.entries.size;
1437
+ }
1438
+ /**
1439
+ * Reconstruct a MutationMap from a stored entry.
1440
+ *
1441
+ * @example
1442
+ * const map = store.toMap(entry);
1443
+ * reverse(mutatedCode, map);
1444
+ */
1445
+ static toMap(entry) {
1446
+ return new Map(entry.mapEntries);
1447
+ }
1448
+ /**
1449
+ * Serialize a MutationMap to the format stored in StoreEntry.
1450
+ */
1451
+ static fromMap(map) {
1452
+ return [...map.entries()];
1453
+ }
1454
+ };
1455
+
1456
+ // src/config.ts
1457
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
1458
+ import { join as join3, resolve } from "path";
1459
+ import { createHmac } from "crypto";
1460
+ var CONFIG_DIR_NAME = PROJECT_CONFIG_DIR_NAME;
1461
+ var CONFIG_FILE = "config.json";
1462
+ var MUTATION_MAP_FILE = "mutation-map.json";
1463
+ var AUDIT_LOG_FILE = "audit.log";
1464
+ var USAGE_FILE = "usage.json";
1465
+ function getConfigDir(baseDir) {
1466
+ const base = baseDir ?? process.cwd();
1467
+ migrateLegacyProjectConfigDir(base);
1468
+ return resolve(base, CONFIG_DIR_NAME);
1469
+ }
1470
+ function initConfig(dir) {
1471
+ const configDir = getConfigDir(dir);
1472
+ if (!existsSync4(configDir)) {
1473
+ mkdirSync3(configDir, { recursive: true });
1474
+ }
1475
+ const configPath = join3(configDir, CONFIG_FILE);
1476
+ if (!existsSync4(configPath)) {
1477
+ const config = {
1478
+ ...DEFAULT_CONFIG,
1479
+ storePath: configDir
1480
+ };
1481
+ writeFileSync3(configPath, JSON.stringify(config, null, 2), "utf-8");
1482
+ }
1483
+ const mapPath = join3(configDir, MUTATION_MAP_FILE);
1484
+ if (!existsSync4(mapPath)) {
1485
+ writeFileSync3(mapPath, "[]", "utf-8");
1486
+ }
1487
+ const auditPath = join3(configDir, AUDIT_LOG_FILE);
1488
+ if (!existsSync4(auditPath)) {
1489
+ writeFileSync3(auditPath, "", "utf-8");
1490
+ }
1491
+ const usagePath = join3(configDir, USAGE_FILE);
1492
+ if (!existsSync4(usagePath)) {
1493
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1494
+ const month = today.slice(0, 7);
1495
+ const usageData = { month, mutations: 0, firstUseDate: today };
1496
+ writeFileSync3(
1497
+ usagePath,
1498
+ JSON.stringify({ ...usageData, date: today, checksum: computeUsageChecksum(usageData) }, null, 2),
1499
+ "utf-8"
1500
+ );
1501
+ }
1502
+ return configDir;
1503
+ }
1504
+ function loadConfig(dir) {
1505
+ const configDir = getConfigDir(dir);
1506
+ const configPath = join3(configDir, CONFIG_FILE);
1507
+ if (!existsSync4(configPath)) {
1508
+ return { ...DEFAULT_CONFIG };
1509
+ }
1510
+ try {
1511
+ const raw = readFileSync3(configPath, "utf-8");
1512
+ const parsed = JSON.parse(raw);
1513
+ return { ...DEFAULT_CONFIG, ...parsed };
1514
+ } catch {
1515
+ return { ...DEFAULT_CONFIG };
1516
+ }
1517
+ }
1518
+ var USAGE_HMAC_SECRET = "pretense_usage_integrity_v1";
1519
+ function getUsageSecret() {
1520
+ return process.env["PRETEST_USAGE_SECRET"]?.trim() || process.env["PRETENSE_USAGE_SECRET"]?.trim() || USAGE_HMAC_SECRET;
1521
+ }
1522
+ function computeUsageChecksum(data) {
1523
+ const payload = JSON.stringify({ month: data.month, mutations: data.mutations, firstUseDate: data.firstUseDate });
1524
+ return createHmac("sha256", getUsageSecret()).update(payload).digest("hex");
1525
+ }
1526
+ function verifyUsageChecksum(data) {
1527
+ if (!data.checksum) return false;
1528
+ return data.checksum === computeUsageChecksum(data);
1529
+ }
1530
+ function loadUsage(dir) {
1531
+ const configDir = getConfigDir(dir);
1532
+ const usagePath = join3(configDir, USAGE_FILE);
1533
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1534
+ const currentMonth = today.slice(0, 7);
1535
+ if (!existsSync4(usagePath)) {
1536
+ return { month: currentMonth, mutations: 0, firstUseDate: today };
1537
+ }
1538
+ try {
1539
+ const raw = readFileSync3(usagePath, "utf-8");
1540
+ const data = JSON.parse(raw);
1541
+ if (data.date && !data.month) {
1542
+ const legacyDate = data.date;
1543
+ return { month: currentMonth, mutations: 0, firstUseDate: data.firstUseDate ?? legacyDate };
1544
+ }
1545
+ if (!verifyUsageChecksum({ month: data.month, mutations: data.mutations, firstUseDate: data.firstUseDate, checksum: data.checksum })) {
1546
+ process.stderr.write("[PRETEST] Warning: usage.json integrity check failed. Resetting usage counter.\n");
1547
+ return { month: currentMonth, mutations: 0, firstUseDate: data.firstUseDate ?? today };
1548
+ }
1549
+ if (data.month !== currentMonth) {
1550
+ return { month: currentMonth, mutations: 0, firstUseDate: data.firstUseDate ?? today };
1551
+ }
1552
+ return { month: data.month, mutations: data.mutations, firstUseDate: data.firstUseDate };
1553
+ } catch {
1554
+ return { month: currentMonth, mutations: 0, firstUseDate: today };
1555
+ }
1556
+ }
1557
+ function saveUsage(usage, dir) {
1558
+ const configDir = getConfigDir(dir);
1559
+ if (!existsSync4(configDir)) {
1560
+ mkdirSync3(configDir, { recursive: true });
1561
+ }
1562
+ const usagePath = join3(configDir, USAGE_FILE);
1563
+ const checksum = computeUsageChecksum(usage);
1564
+ writeFileSync3(usagePath, JSON.stringify({ ...usage, checksum }, null, 2), "utf-8");
1565
+ }
1566
+ var MIN_LICENSE_KEY_LENGTH = 40;
1567
+ var LICENSE_PAYLOAD_REGEX = /^[a-zA-Z0-9]+$/;
1568
+ function isValidLicenseKey(key, prefix) {
1569
+ if (key.length < MIN_LICENSE_KEY_LENGTH) return false;
1570
+ const afterPrefix = key.slice(prefix.length);
1571
+ const hmacSecret = process.env["PRETEST_LICENSE_SECRET"]?.trim() || process.env["PRETENSE_LICENSE_SECRET"]?.trim();
1572
+ if (hmacSecret) {
1573
+ const lastUnderscore = afterPrefix.lastIndexOf("_");
1574
+ if (lastUnderscore === -1) return false;
1575
+ const payload = afterPrefix.slice(0, lastUnderscore);
1576
+ const providedHmac = afterPrefix.slice(lastUnderscore + 1);
1577
+ if (!payload || !providedHmac) return false;
1578
+ if (!LICENSE_PAYLOAD_REGEX.test(payload)) return false;
1579
+ const expectedHmac = createHmac("sha256", hmacSecret).update(payload).digest("hex").slice(0, 16);
1580
+ return providedHmac === expectedHmac;
1581
+ }
1582
+ return LICENSE_PAYLOAD_REGEX.test(afterPrefix);
1583
+ }
1584
+ function detectTier() {
1585
+ const key = process.env["PRETEST_LICENSE_KEY"]?.trim() || process.env["PRETENSE_LICENSE_KEY"]?.trim() || "";
1586
+ if (key.startsWith("pre_ent_") && isValidLicenseKey(key, "pre_ent_")) return "enterprise";
1587
+ if (key.startsWith("pre_pro_") && isValidLicenseKey(key, "pre_pro_")) return "pro";
1588
+ return "free";
1589
+ }
1590
+ function tierFromDashboardPlan(plan, fallback) {
1591
+ if (plan === "ENTERPRISE") return "enterprise";
1592
+ if (plan === "PRO") return "pro";
1593
+ return fallback;
1594
+ }
1595
+ function getTierLimits(tier) {
1596
+ switch (tier) {
1597
+ case "enterprise":
1598
+ return { monthlyMutations: Infinity, auditRetentionDays: Infinity };
1599
+ case "pro":
1600
+ return { monthlyMutations: Infinity, auditRetentionDays: 90 };
1601
+ case "free":
1602
+ default:
1603
+ return { monthlyMutations: 1e3, auditRetentionDays: 30 };
1604
+ }
1605
+ }
1606
+
1607
+ // src/audit.ts
1608
+ import { appendFileSync, existsSync as existsSync5, readFileSync as readFileSync4, mkdirSync as mkdirSync4 } from "fs";
1609
+ import { join as join4, dirname as dirname2 } from "path";
1610
+ function writeAuditEntry(entry, dir) {
1611
+ const configDir = getConfigDir(dir);
1612
+ const auditPath = join4(configDir, "audit.log");
1613
+ const logDir = dirname2(auditPath);
1614
+ if (!existsSync5(logDir)) {
1615
+ mkdirSync4(logDir, { recursive: true });
1616
+ }
1617
+ const line = JSON.stringify(entry) + "\n";
1618
+ appendFileSync(auditPath, line, "utf-8");
1619
+ }
1620
+ function createAuditEntry(sessionId, file, identifiersMutated, secretsBlocked, llmProvider, latencyMs) {
1621
+ return {
1622
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1623
+ sessionId,
1624
+ file,
1625
+ identifiersMutated,
1626
+ secretsBlocked,
1627
+ llmProvider,
1628
+ latencyMs
1629
+ };
1630
+ }
1631
+ function readAuditLog(options = {}) {
1632
+ const { limit, dir } = options;
1633
+ const configDir = getConfigDir(dir);
1634
+ const auditPath = join4(configDir, "audit.log");
1635
+ if (!existsSync5(auditPath)) {
1636
+ return [];
1637
+ }
1638
+ const raw = readFileSync4(auditPath, "utf-8");
1639
+ const lines = raw.trim().split("\n").filter(Boolean);
1640
+ let entries = [];
1641
+ for (const line of lines) {
1642
+ try {
1643
+ entries.push(JSON.parse(line));
1644
+ } catch {
1645
+ }
1646
+ }
1647
+ const tier = detectTier();
1648
+ const { auditRetentionDays } = getTierLimits(tier);
1649
+ if (auditRetentionDays !== Infinity) {
1650
+ const cutoff = Date.now() - auditRetentionDays * 24 * 60 * 60 * 1e3;
1651
+ entries = entries.filter((e) => new Date(e.timestamp).getTime() >= cutoff);
1652
+ }
1653
+ entries.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
1654
+ if (limit && limit > 0) {
1655
+ entries = entries.slice(0, limit);
1656
+ }
1657
+ return entries;
1658
+ }
1659
+ function formatJson(entries) {
1660
+ return JSON.stringify(entries, null, 2);
1661
+ }
1662
+ function formatCsv(entries) {
1663
+ const headers = "timestamp,sessionId,file,identifiersMutated,secretsBlocked,llmProvider,latencyMs";
1664
+ const rows = entries.map(
1665
+ (e) => [
1666
+ e.timestamp,
1667
+ e.sessionId,
1668
+ `"${e.file.replace(/"/g, '""')}"`,
1669
+ e.identifiersMutated,
1670
+ e.secretsBlocked,
1671
+ e.llmProvider,
1672
+ e.latencyMs
1673
+ ].join(",")
1674
+ );
1675
+ return [headers, ...rows].join("\n");
1676
+ }
1677
+ function formatText(entries) {
1678
+ if (entries.length === 0) return "No audit entries found.";
1679
+ const lines = entries.map((e) => {
1680
+ const date = new Date(e.timestamp).toLocaleString();
1681
+ return `[${date}] ${e.file} | ${e.identifiersMutated} mutated | ${e.secretsBlocked} secrets blocked | ${e.llmProvider} | ${e.latencyMs}ms`;
1682
+ });
1683
+ return lines.join("\n");
1684
+ }
1685
+ function formatAudit(entries, format = "text") {
1686
+ switch (format) {
1687
+ case "json":
1688
+ return formatJson(entries);
1689
+ case "csv":
1690
+ return formatCsv(entries);
1691
+ case "text":
1692
+ default:
1693
+ return formatText(entries);
1694
+ }
1695
+ }
1696
+
1697
+ // src/proxy.ts
1698
+ import { Hono } from "hono";
1699
+ import { serve } from "@hono/node-server";
1700
+ import { nanoid } from "nanoid";
1701
+ import { createServer } from "net";
1702
+ import { existsSync as existsSync7, watch } from "fs";
1703
+
1704
+ // src/auth.ts
1705
+ import { createHash } from "crypto";
1706
+ var AUTH_HEADERS = ["authorization", "x-api-key", "x-goog-api-key"];
1707
+ var PUBLIC_PATHS = /* @__PURE__ */ new Set([
1708
+ "/",
1709
+ "/health",
1710
+ "/stats",
1711
+ "/audit",
1712
+ "/tokens"
1713
+ ]);
1714
+ function extractApiKey(c) {
1715
+ for (const h of AUTH_HEADERS) {
1716
+ const v = c.req.header(h);
1717
+ if (v && v.length > 0) {
1718
+ return v.replace(/^Bearer\s+/i, "").trim();
1719
+ }
1720
+ }
1721
+ return null;
1722
+ }
1723
+ function sessionHash(apiKey) {
1724
+ return createHash("sha256").update(apiKey).digest("hex").slice(0, 16);
1725
+ }
1726
+ var requireApiKey = async (c, next) => {
1727
+ if (PUBLIC_PATHS.has(c.req.path)) {
1728
+ return next();
1729
+ }
1730
+ const key = extractApiKey(c);
1731
+ if (!key) {
1732
+ return c.json(
1733
+ {
1734
+ error: {
1735
+ type: "missing_api_key",
1736
+ message: "Authorization header required (Authorization, x-api-key, or x-goog-api-key)"
1737
+ }
1738
+ },
1739
+ 401
1740
+ );
1741
+ }
1742
+ c.set("sessionHash", sessionHash(key));
1743
+ return next();
1744
+ };
1745
+
1746
+ // src/session-store.ts
1747
+ var sessions = /* @__PURE__ */ new Map();
1748
+ function getSession(hash) {
1749
+ let s = sessions.get(hash);
1750
+ if (!s) {
1751
+ s = {
1752
+ mutationMap: /* @__PURE__ */ new Map(),
1753
+ reverseMap: /* @__PURE__ */ new Map(),
1754
+ createdAt: Date.now(),
1755
+ lastUsed: Date.now()
1756
+ };
1757
+ sessions.set(hash, s);
1758
+ }
1759
+ s.lastUsed = Date.now();
1760
+ return s;
1761
+ }
1762
+ function sessionCount() {
1763
+ return sessions.size;
1764
+ }
1765
+
1766
+ // src/git-context.ts
1767
+ import { execFile } from "child_process";
1768
+ import { promisify } from "util";
1769
+ var execFileAsync = promisify(execFile);
1770
+ var GIT_TIMEOUT_MS = 2e3;
1771
+ async function runGit(args, cwd) {
1772
+ try {
1773
+ const { stdout } = await execFileAsync("git", args, {
1774
+ cwd,
1775
+ timeout: GIT_TIMEOUT_MS,
1776
+ windowsHide: true
1777
+ });
1778
+ const trimmed = stdout.trim();
1779
+ return trimmed.length > 0 ? trimmed : void 0;
1780
+ } catch {
1781
+ return void 0;
1782
+ }
1783
+ }
1784
+ function sanitizeRemote(raw) {
1785
+ if (/^[\w.-]+@[^:]+:/.test(raw) && !raw.includes("://")) {
1786
+ return raw;
1787
+ }
1788
+ try {
1789
+ const url = new URL(raw);
1790
+ url.username = "";
1791
+ url.password = "";
1792
+ return url.toString();
1793
+ } catch {
1794
+ return raw;
1795
+ }
1796
+ }
1797
+ async function collectGitContext(cwd = process.cwd()) {
1798
+ const insideRepo = await runGit(["rev-parse", "--is-inside-work-tree"], cwd);
1799
+ if (insideRepo !== "true") return {};
1800
+ const [remote, branch, sha] = await Promise.all([
1801
+ runGit(["remote", "get-url", "origin"], cwd),
1802
+ runGit(["rev-parse", "--abbrev-ref", "HEAD"], cwd),
1803
+ runGit(["rev-parse", "HEAD"], cwd)
1804
+ ]);
1805
+ const ctx = {};
1806
+ if (remote) ctx.git_remote = sanitizeRemote(remote);
1807
+ if (branch && branch !== "HEAD") ctx.git_branch = branch;
1808
+ if (sha) ctx.git_commit_sha = sha;
1809
+ return ctx;
1810
+ }
1811
+ var CACHE_TTL_MS = 3e4;
1812
+ var cache = /* @__PURE__ */ new Map();
1813
+ async function collectGitContextCached(cwd = process.cwd(), now = Date.now()) {
1814
+ const hit = cache.get(cwd);
1815
+ if (hit && hit.expiresAt > now) return hit.value;
1816
+ const value = await collectGitContext(cwd);
1817
+ cache.set(cwd, { value, expiresAt: now + CACHE_TTL_MS });
1818
+ return value;
1819
+ }
1820
+
1821
+ // src/commands/login.ts
1822
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4, readFileSync as readFileSync5, chmodSync, readSync } from "fs";
1823
+ import { join as join5 } from "path";
1824
+ import { homedir as homedir2 } from "os";
1825
+ import readline from "readline/promises";
1826
+ import chalk from "chalk";
1827
+
1828
+ // src/dashboard-env.ts
1829
+ var DASHBOARD_API_KEY_ENVS = ["PRETEST_API_KEY", "PRETENSE_API_KEY"];
1830
+ var DASHBOARD_API_URL_ENVS = ["PRETEST_API_URL", "PRETENSE_API_URL"];
1831
+ var DASHBOARD_NO_BANNER_ENVS = ["PRETEST_NO_BANNER", "PRETENSE_NO_BANNER"];
1832
+ function firstNonEmptyEnv(names) {
1833
+ for (const name of names) {
1834
+ const v = process.env[name]?.trim();
1835
+ if (v) return v;
1836
+ }
1837
+ return "";
1838
+ }
1839
+ function activeDashboardApiKeyEnvName() {
1840
+ for (const name of DASHBOARD_API_KEY_ENVS) {
1841
+ const v = process.env[name]?.trim();
1842
+ if (v) return name;
1843
+ }
1844
+ return null;
1845
+ }
1846
+ function assignDashboardApiKeyToProcess(key) {
1847
+ process.env["PRETEST_API_KEY"] = key;
1848
+ process.env["PRETENSE_API_KEY"] = key;
1849
+ }
1850
+ function clearDashboardApiKeyFromProcess() {
1851
+ delete process.env["PRETEST_API_KEY"];
1852
+ delete process.env["PRETENSE_API_KEY"];
1853
+ }
1854
+ function hasDashboardApiKeyInEnv() {
1855
+ return DASHBOARD_API_KEY_ENVS.some((n) => !!process.env[n]?.trim());
1856
+ }
1857
+
1858
+ // src/publish-info.ts
1859
+ var PUBLISH_PACKAGE_URL = "https://www.npmjs.com/package/@blockyfy/stg-cli";
1860
+
1861
+ // src/commands/login.ts
1862
+ var CONFIG_DIR_NAME2 = PROJECT_CONFIG_DIR_NAME;
1863
+ var CONFIG_FILE2 = "config.json";
1864
+ function getUserDashboardConfigDir() {
1865
+ const home = homedir2();
1866
+ migrateLegacyProjectConfigDir(home);
1867
+ return join5(home, CONFIG_DIR_NAME2);
1868
+ }
1869
+ function getUserDashboardConfigPath() {
1870
+ return join5(getUserDashboardConfigDir(), CONFIG_FILE2);
1871
+ }
1872
+ function isValidKeyShape(key) {
1873
+ const trimmed = key.trim();
1874
+ if (trimmed.length < 24) return false;
1875
+ if (!/^[A-Za-z0-9_]+$/.test(trimmed)) return false;
1876
+ return trimmed.startsWith("pk_live_") || trimmed.startsWith("ptns_live_") || trimmed.startsWith("prtns_live_");
1877
+ }
1878
+ function readStdinIfPiped() {
1879
+ if (process.stdin.isTTY) return "";
1880
+ try {
1881
+ const chunks = [];
1882
+ const buf = Buffer.alloc(4096);
1883
+ let bytesRead = 0;
1884
+ do {
1885
+ try {
1886
+ bytesRead = readSync(0, buf, 0, buf.length, null);
1887
+ } catch {
1888
+ bytesRead = 0;
1889
+ }
1890
+ if (bytesRead > 0) chunks.push(Buffer.from(buf.subarray(0, bytesRead)));
1891
+ } while (bytesRead === buf.length);
1892
+ return Buffer.concat(chunks).toString("utf-8").trim();
1893
+ } catch {
1894
+ return "";
1895
+ }
1896
+ }
1897
+ function maskKey(key) {
1898
+ const trimmed = key.trim();
1899
+ if (trimmed.length <= 12) return trimmed;
1900
+ return `${trimmed.slice(0, 12)}${"\u2022".repeat(8)}${trimmed.slice(-4)}`;
1901
+ }
1902
+ function logDashboardCredentialHint() {
1903
+ const envName = activeDashboardApiKeyEnvName();
1904
+ const env = envName ? process.env[envName]?.trim() ?? "" : "";
1905
+ const envOk = env.length > 0 && isValidKeyShape(env);
1906
+ const abs = getUserDashboardConfigPath();
1907
+ const fk = loadApiKey();
1908
+ if (envOk && envName) {
1909
+ console.error(
1910
+ chalk.gray(
1911
+ `[pretest] Dashboard key source: ${envName} in environment (${maskKey(env)}) \u2014 unset this variable after logout if you expect no key.`
1912
+ )
1913
+ );
1914
+ } else if (fk && isValidKeyShape(fk)) {
1915
+ console.error(chalk.gray(`[pretest] Dashboard key source: ${abs} (${maskKey(fk)})`));
1916
+ } else {
1917
+ console.error(chalk.gray("[pretest] Dashboard key source: none configured."));
1918
+ }
1919
+ }
1920
+ function saveApiKey(key, configDir = getUserDashboardConfigDir()) {
1921
+ if (!existsSync6(configDir)) {
1922
+ mkdirSync5(configDir, { recursive: true });
1923
+ }
1924
+ const path = join5(configDir, CONFIG_FILE2);
1925
+ let existing = {};
1926
+ if (existsSync6(path)) {
1927
+ try {
1928
+ existing = JSON.parse(readFileSync5(path, "utf-8"));
1929
+ } catch {
1930
+ existing = {};
1931
+ }
1932
+ }
1933
+ const next = {
1934
+ ...existing,
1935
+ api_key: key.trim(),
1936
+ saved_at: (/* @__PURE__ */ new Date()).toISOString()
1937
+ };
1938
+ writeFileSync4(path, JSON.stringify(next, null, 2), { encoding: "utf-8", mode: 384 });
1939
+ try {
1940
+ chmodSync(path, 384);
1941
+ } catch {
1942
+ }
1943
+ return path;
1944
+ }
1945
+ function removeSavedDashboardCredentials(configDir = getUserDashboardConfigDir()) {
1946
+ const path = join5(configDir, CONFIG_FILE2);
1947
+ if (!existsSync6(path)) return { removed: false, path };
1948
+ let existing = {};
1949
+ try {
1950
+ existing = JSON.parse(readFileSync5(path, "utf-8"));
1951
+ } catch {
1952
+ return { removed: false, path };
1953
+ }
1954
+ const ak = existing.api_key;
1955
+ const hadKey = typeof ak === "string" && ak.trim().length > 0;
1956
+ if (!hadKey) return { removed: false, path };
1957
+ delete existing.api_key;
1958
+ delete existing.saved_at;
1959
+ writeFileSync4(path, JSON.stringify(existing, null, 2), { encoding: "utf-8", mode: 384 });
1960
+ try {
1961
+ chmodSync(path, 384);
1962
+ } catch {
1963
+ }
1964
+ return { removed: true, path };
1965
+ }
1966
+ function loadApiKey(configDir = getUserDashboardConfigDir()) {
1967
+ const path = join5(configDir, CONFIG_FILE2);
1968
+ if (!existsSync6(path)) return null;
1969
+ try {
1970
+ const raw = readFileSync5(path, "utf-8");
1971
+ const parsed = JSON.parse(raw);
1972
+ if (typeof parsed.api_key === "string") {
1973
+ const t = parsed.api_key.trim();
1974
+ if (t.length > 0) return t;
1975
+ }
1976
+ } catch {
1977
+ }
1978
+ return null;
1979
+ }
1980
+ function resolveDashboardKeyCandidate() {
1981
+ const env = firstNonEmptyEnv(DASHBOARD_API_KEY_ENVS);
1982
+ if (env.length > 0) {
1983
+ if (!isValidKeyShape(env)) {
1984
+ return { key: null, fromFile: false, badEnv: true };
1985
+ }
1986
+ return { key: env, fromFile: false, badEnv: false };
1987
+ }
1988
+ const fileKey = loadApiKey();
1989
+ if (fileKey && isValidKeyShape(fileKey)) {
1990
+ return { key: fileKey.trim(), fromFile: true, badEnv: false };
1991
+ }
1992
+ return { key: null, fromFile: Boolean(fileKey), badEnv: false };
1993
+ }
1994
+ function installDashboardKeyForCurrentProcess(key) {
1995
+ const trimmed = key.trim();
1996
+ const path = saveApiKey(trimmed);
1997
+ assignDashboardApiKeyToProcess(trimmed);
1998
+ return path;
1999
+ }
2000
+ async function promptDashboardApiKeyAfterFailure() {
2001
+ if (!process.stdin.isTTY || !process.stdout.isTTY) return null;
2002
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
2003
+ try {
2004
+ const line = (await rl.question(chalk.bold("Dashboard API key: "))).trim();
2005
+ return line.length > 0 ? line : null;
2006
+ } finally {
2007
+ rl.close();
2008
+ }
2009
+ }
2010
+ async function ensureDashboardKeyForStart() {
2011
+ const { key, fromFile, badEnv } = resolveDashboardKeyCandidate();
2012
+ if (badEnv) {
2013
+ console.error(
2014
+ chalk.red(
2015
+ "Error: A dashboard API key is set in the environment but does not match the expected key shape."
2016
+ )
2017
+ );
2018
+ console.error(
2019
+ chalk.gray(
2020
+ "Keys look like prtns_live_\u2026 or pk_live_\u2026. Fix the variable or unset it to use ~/.pretest/config.json."
2021
+ )
2022
+ );
2023
+ process.exit(1);
2024
+ }
2025
+ if (key) {
2026
+ return;
2027
+ }
2028
+ if (fromFile) {
2029
+ console.warn(
2030
+ chalk.yellow(
2031
+ "Ignoring invalid `api_key` in ~/.pretest/config.json (wrong shape). Enter a new key or run `pretest login --key ...`."
2032
+ )
2033
+ );
2034
+ }
2035
+ const stdinOk = process.stdin.isTTY;
2036
+ const stdoutOk = process.stdout.isTTY;
2037
+ if (!stdinOk || !stdoutOk) {
2038
+ console.error(chalk.red("Error: Dashboard API key required to start the proxy."));
2039
+ console.error(
2040
+ chalk.gray("Save one with `pretest login --key prtns_live_...` or set PRETEST_API_KEY.")
2041
+ );
2042
+ process.exit(1);
2043
+ }
2044
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
2045
+ try {
2046
+ console.log(
2047
+ chalk.cyan(
2048
+ "No dashboard API key found in ~/.pretest/config.json. Set PRETEST_API_KEY or run pretest login."
2049
+ )
2050
+ );
2051
+ const entered = (await rl.question(chalk.bold("Enter your dashboard API key: "))).trim();
2052
+ if (!entered) {
2053
+ console.error(chalk.red("No key entered; exiting."));
2054
+ process.exit(1);
2055
+ }
2056
+ if (!isValidKeyShape(entered)) {
2057
+ console.error(chalk.red("That does not look like a dashboard API key (expect prtns_live_\u2026 or pk_live_\u2026)."));
2058
+ console.error(chalk.gray(`Get a key via the package page, then run \`pretest login --key ...\`: ${PUBLISH_PACKAGE_URL}`));
2059
+ process.exit(1);
2060
+ }
2061
+ const path = installDashboardKeyForCurrentProcess(entered);
2062
+ console.log(chalk.green("Saved API key to"), chalk.bold(path));
2063
+ console.log(chalk.gray(` ${maskKey(entered)}`));
2064
+ } finally {
2065
+ rl.close();
2066
+ }
2067
+ const persisted = resolveDashboardKeyCandidate();
2068
+ if (!persisted.key || persisted.badEnv) {
2069
+ console.error(chalk.red("Error: Dashboard API key could not be confirmed after login prompt."));
2070
+ process.exit(1);
2071
+ }
2072
+ }
2073
+ function runLogin(opts = {}) {
2074
+ const candidate = opts.key?.trim() || firstNonEmptyEnv(DASHBOARD_API_KEY_ENVS) || readStdinIfPiped();
2075
+ if (!candidate) {
2076
+ console.error(chalk.red("Error: no API key provided."));
2077
+ console.error(
2078
+ chalk.gray("Try: pretest login --key pk_live_xxxxx")
2079
+ );
2080
+ console.error(
2081
+ chalk.gray(" or: echo $PRETEST_API_KEY | pretest login")
2082
+ );
2083
+ return 1;
2084
+ }
2085
+ if (!isValidKeyShape(candidate)) {
2086
+ console.error(chalk.red("Error: that does not look like a dashboard API key."));
2087
+ console.error(
2088
+ chalk.gray("Keys start with prtns_live_ (or pk_live_) and are at least 24 characters long.")
2089
+ );
2090
+ console.error(
2091
+ chalk.gray(`See: ${PUBLISH_PACKAGE_URL}`)
2092
+ );
2093
+ return 1;
2094
+ }
2095
+ const path = saveApiKey(candidate, opts.configDir);
2096
+ console.log(chalk.green("Saved API key to"), chalk.bold(path));
2097
+ console.log(chalk.gray(` ${maskKey(candidate)}`));
2098
+ console.log(chalk.gray(" File mode 600 \u2014 readable only by you."));
2099
+ return 0;
2100
+ }
2101
+
2102
+ // src/backend-client.ts
2103
+ function usageSummaryFromValidate(v) {
2104
+ return {
2105
+ plan: v.plan,
2106
+ monthlyMutationLimit: v.monthlyMutationLimit,
2107
+ mutationsUsed: v.mutationsUsed,
2108
+ mutationsRemaining: v.mutationsRemaining,
2109
+ requestsThisPeriod: 0,
2110
+ secretsBlockedThisPeriod: 0,
2111
+ periodResetsAt: v.periodResetsAt,
2112
+ keyExpiresAt: null,
2113
+ keyStatus: "active"
2114
+ };
2115
+ }
2116
+ var DEFAULT_PRETENSE_API_URL = "https://pretense-stg-api.nxtsen.com";
2117
+ var DEFAULT_REQUEST_TIMEOUT_MS = 15e3;
2118
+ function getConfiguredApiRoot(rootOverride) {
2119
+ const raw = (rootOverride?.trim() || firstNonEmptyEnv(DASHBOARD_API_URL_ENVS) || DEFAULT_PRETENSE_API_URL).trim();
2120
+ if (!raw) {
2121
+ return DEFAULT_PRETENSE_API_URL.replace(/\/$/, "");
2122
+ }
2123
+ let normalized = raw;
2124
+ if (!/^https?:\/\//i.test(normalized)) {
2125
+ normalized = `https://${normalized}`;
2126
+ }
2127
+ try {
2128
+ const u = new URL(normalized);
2129
+ const origin = /^pretense\.ai$/i.test(u.hostname) ? DEFAULT_PRETENSE_API_URL.replace(/\/$/, "") : u.origin;
2130
+ const path = u.pathname === "/" ? "" : u.pathname.replace(/\/$/, "");
2131
+ return `${origin}${path}`;
2132
+ } catch {
2133
+ return DEFAULT_PRETENSE_API_URL.replace(/\/$/, "");
2134
+ }
2135
+ }
2136
+ function getCliApiUrl(pathWithinCli, rootOverride) {
2137
+ const root = getConfiguredApiRoot(rootOverride);
2138
+ const tail = pathWithinCli.replace(/^\/+/, "");
2139
+ return `${root}/api/cli/${tail}`;
2140
+ }
2141
+ function getApiRequestTimeoutMs() {
2142
+ const raw = process.env["PRETEST_API_TIMEOUT_MS"]?.trim() || process.env["PRETENSE_API_TIMEOUT_MS"]?.trim();
2143
+ if (!raw || !/^\d+$/.test(raw)) return DEFAULT_REQUEST_TIMEOUT_MS;
2144
+ const n = parseInt(raw, 10);
2145
+ return Math.min(Math.max(n, 3e3), 12e4);
2146
+ }
2147
+ function getApiKey() {
2148
+ const fromEnv = firstNonEmptyEnv(DASHBOARD_API_KEY_ENVS);
2149
+ if (fromEnv) return fromEnv;
2150
+ const fromFile = loadApiKey();
2151
+ if (!fromFile) return null;
2152
+ const t = fromFile.trim();
2153
+ return t.length > 0 ? t : null;
2154
+ }
2155
+ function getDashboardApiKey() {
2156
+ return getApiKey();
2157
+ }
2158
+ var cachedValidation = null;
2159
+ var lastValidationSuccessAt = 0;
2160
+ var VALIDATION_CACHE_FRESH_MS = 45e3;
2161
+ function getCachedValidation() {
2162
+ return cachedValidation;
2163
+ }
2164
+ function clearValidationCache() {
2165
+ cachedValidation = null;
2166
+ lastValidationSuccessAt = 0;
2167
+ }
2168
+ function bumpCachedUsageAfterLog(identifiersMutated) {
2169
+ if (!cachedValidation || identifiersMutated <= 0) return;
2170
+ cachedValidation.mutationsUsed += identifiersMutated;
2171
+ if (cachedValidation.mutationsRemaining !== -1) {
2172
+ cachedValidation.mutationsRemaining = Math.max(
2173
+ 0,
2174
+ cachedValidation.mutationsRemaining - identifiersMutated
2175
+ );
2176
+ }
2177
+ }
2178
+ function makeBackendReachabilityError(code, message) {
2179
+ const err = new Error(message);
2180
+ err.code = code;
2181
+ return err;
2182
+ }
2183
+ async function validateKey(opts = {}) {
2184
+ const enforceReachability = opts.enforceReachability === true;
2185
+ const now = Date.now();
2186
+ const cacheFresh = cachedValidation !== null && now - lastValidationSuccessAt < VALIDATION_CACHE_FRESH_MS;
2187
+ if (!opts.force && cacheFresh) {
2188
+ return cachedValidation;
2189
+ }
2190
+ const validateUrl = getCliApiUrl("auth/validate");
2191
+ const timeoutMs = getApiRequestTimeoutMs();
2192
+ const apiKey = getApiKey();
2193
+ if (!apiKey) {
2194
+ if (enforceReachability) {
2195
+ const err = new Error("Missing dashboard API key");
2196
+ err.code = "MISSING_API_KEY";
2197
+ cachedValidation = null;
2198
+ lastValidationSuccessAt = 0;
2199
+ throw err;
2200
+ }
2201
+ return null;
2202
+ }
2203
+ const controller = new AbortController();
2204
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
2205
+ try {
2206
+ const resp = await fetch(validateUrl, {
2207
+ method: "POST",
2208
+ headers: {
2209
+ "content-type": "application/json",
2210
+ authorization: `Bearer ${apiKey}`
2211
+ },
2212
+ body: "{}",
2213
+ signal: controller.signal
2214
+ });
2215
+ if (resp.status === 401 || resp.status === 403) {
2216
+ const raw = await resp.json().catch(() => ({}));
2217
+ const msg = typeof raw.message === "string" ? raw.message : "API key rejected by server";
2218
+ const err = new Error(msg);
2219
+ err.code = typeof raw.code === "string" ? raw.code : "INVALID_API_KEY";
2220
+ cachedValidation = null;
2221
+ lastValidationSuccessAt = 0;
2222
+ throw err;
2223
+ }
2224
+ if (!resp.ok) {
2225
+ const msg = `Dashboard API returned HTTP ${resp.status} from ${validateUrl}`;
2226
+ if (enforceReachability) {
2227
+ cachedValidation = null;
2228
+ lastValidationSuccessAt = 0;
2229
+ throw makeBackendReachabilityError("AUTH_BACKEND_REJECTED", msg);
2230
+ }
2231
+ return null;
2232
+ }
2233
+ let data;
2234
+ try {
2235
+ data = await resp.json();
2236
+ } catch {
2237
+ if (enforceReachability) {
2238
+ cachedValidation = null;
2239
+ lastValidationSuccessAt = 0;
2240
+ throw makeBackendReachabilityError(
2241
+ "AUTH_BACKEND_REJECTED",
2242
+ `Invalid JSON from ${validateUrl}`
2243
+ );
2244
+ }
2245
+ return null;
2246
+ }
2247
+ if (typeof data.valid === "boolean" && data.valid === false) {
2248
+ const err = new Error("API key is not valid");
2249
+ err.code = "INVALID_API_KEY";
2250
+ cachedValidation = null;
2251
+ lastValidationSuccessAt = 0;
2252
+ throw err;
2253
+ }
2254
+ cachedValidation = data;
2255
+ lastValidationSuccessAt = Date.now();
2256
+ return data;
2257
+ } catch (err) {
2258
+ if (err.code === "KEY_REVOKED" || err.code === "KEY_EXPIRED" || err.code === "INVALID_API_KEY" || err.code === "AUTH_ERROR" || err.code === "ORG_INACTIVE" || err.code === "MISSING_API_KEY" || err.code === "BACKEND_UNAVAILABLE" || err.code === "AUTH_BACKEND_REJECTED") {
2259
+ throw err;
2260
+ }
2261
+ if (enforceReachability) {
2262
+ const msg = err.name === "AbortError" ? `Timed out after ${timeoutMs}ms reaching ${validateUrl} (raise PRETEST_API_TIMEOUT_MS or PRETENSE_API_TIMEOUT_MS)` : `Cannot reach dashboard API at ${validateUrl}: ${err.message}`;
2263
+ cachedValidation = null;
2264
+ lastValidationSuccessAt = 0;
2265
+ throw makeBackendReachabilityError("BACKEND_UNAVAILABLE", msg);
2266
+ }
2267
+ if (opts.verbose) {
2268
+ process.stderr.write(
2269
+ `[PRETEST] Backend validation failed: ${err.message}
2270
+ `
2271
+ );
2272
+ }
2273
+ return null;
2274
+ } finally {
2275
+ clearTimeout(timer);
2276
+ }
2277
+ }
2278
+ async function fetchUsageSummary(opts = {}) {
2279
+ const summaryUrl = getCliApiUrl("usage/summary");
2280
+ const apiKey = getApiKey();
2281
+ if (!apiKey) return null;
2282
+ const controller = new AbortController();
2283
+ const timer = setTimeout(() => controller.abort(), getApiRequestTimeoutMs());
2284
+ try {
2285
+ const resp = await fetch(summaryUrl, {
2286
+ method: "GET",
2287
+ headers: { authorization: `Bearer ${apiKey}` },
2288
+ signal: controller.signal
2289
+ });
2290
+ if (!resp.ok) return null;
2291
+ return await resp.json();
2292
+ } catch (err) {
2293
+ if (opts.verbose) {
2294
+ process.stderr.write(
2295
+ `[PRETEST] Usage summary fetch failed: ${err.message}
2296
+ `
2297
+ );
2298
+ }
2299
+ return null;
2300
+ } finally {
2301
+ clearTimeout(timer);
2302
+ }
2303
+ }
2304
+ async function isWithinLimits() {
2305
+ const v = getCachedValidation();
2306
+ if (!v) return { allowed: true };
2307
+ if (v.mutationsRemaining !== -1 && v.mutationsRemaining <= 0) {
2308
+ return {
2309
+ allowed: false,
2310
+ reason: `Monthly mutation limit reached (${v.mutationsUsed}/${v.monthlyMutationLimit}). See ${PUBLISH_PACKAGE_URL}`
2311
+ };
2312
+ }
2313
+ return { allowed: true };
2314
+ }
2315
+
2316
+ // src/log-uploader.ts
2317
+ async function uploadLog(event, options) {
2318
+ const apiRoot = options.apiUrl?.trim() || void 0;
2319
+ const endpoint = getCliApiUrl("log", apiRoot);
2320
+ const { apiKey, verbose = false, timeoutMs = getApiRequestTimeoutMs() } = options;
2321
+ if (!apiKey) return;
2322
+ const gitCtx = options.gitContext ?? await collectGitContextCached(options.cwd);
2323
+ const body = {
2324
+ ...event,
2325
+ ...gitCtx,
2326
+ repo_remote_url: gitCtx.git_remote,
2327
+ git_branch: gitCtx.git_branch,
2328
+ commit_sha: gitCtx.git_commit_sha
2329
+ };
2330
+ const controller = new AbortController();
2331
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
2332
+ try {
2333
+ const resp = await fetch(endpoint, {
2334
+ method: "POST",
2335
+ headers: {
2336
+ "content-type": "application/json",
2337
+ authorization: `Bearer ${apiKey}`
2338
+ },
2339
+ body: JSON.stringify(body),
2340
+ signal: controller.signal
2341
+ });
2342
+ if (!resp.ok && verbose) {
2343
+ process.stderr.write(`[PRETEST] log upload HTTP ${resp.status}
2344
+ `);
2345
+ }
2346
+ } catch (err) {
2347
+ if (verbose) {
2348
+ const msg = err instanceof Error ? err.message : String(err);
2349
+ process.stderr.write(`[PRETEST] log upload failed: ${msg}
2350
+ `);
2351
+ }
2352
+ } finally {
2353
+ clearTimeout(timer);
2354
+ }
2355
+ }
2356
+
2357
+ // src/start-client-instructions.ts
2358
+ import chalk2 from "chalk";
2359
+ function printStartClientInstructions(port) {
2360
+ const host = `http://localhost:${port}`;
2361
+ console.log();
2362
+ console.log(chalk2.cyan.bold("\u2500\u2500 Client setup (environment variables) \u2500\u2500"));
2363
+ console.log(chalk2.gray("Point the CLI at the backend API (optional if using default):"));
2364
+ console.log(
2365
+ chalk2.white(`export PRETEST_API_URL='https://pretense-stg-api.nxtsen.com'`)
2366
+ );
2367
+ console.log();
2368
+ console.log(chalk2.gray("Log in with your dashboard API key:"));
2369
+ console.log(chalk2.white("pretest login --key your-dashboard-api-key"));
2370
+ console.log();
2371
+ console.log(
2372
+ chalk2.gray("Documentation:"),
2373
+ chalk2.cyan.underline(PUBLISH_PACKAGE_URL)
2374
+ );
2375
+ console.log();
2376
+ console.log(
2377
+ chalk2.gray("Set the LLM base URL for your provider (proxy below):")
2378
+ );
2379
+ console.log(chalk2.white(`export ANTHROPIC_BASE_URL="${host}"`));
2380
+ console.log(chalk2.white(`export ANTHROPIC_BASE_URL=${host}`));
2381
+ console.log(chalk2.white(`export OPENAI_BASE_URL=${host}/v1`));
2382
+ console.log(chalk2.white(`export GEMINI_BASE_URL=${host}/v1beta`));
2383
+ console.log(chalk2.white(`export OLLAMA_HOST=${host}`));
2384
+ console.log();
2385
+ console.log(chalk2.gray("Set your real upstream LLM API key (unchanged):"));
2386
+ console.log(chalk2.white('export ANTHROPIC_API_KEY="sk-ant-API-KEY"'));
2387
+ console.log();
2388
+ console.log(chalk2.cyan.bold("\u2500\u2500 Stop & log out \u2500\u2500"));
2389
+ console.log(
2390
+ chalk2.gray(
2391
+ "Stop the proxy: press Ctrl+C in the terminal where pretest start is running."
2392
+ )
2393
+ );
2394
+ console.log(
2395
+ chalk2.gray(
2396
+ "Remove the saved dashboard key from ~/.pretest/config.json:"
2397
+ )
2398
+ );
2399
+ console.log(chalk2.white("pretest logout"));
2400
+ console.log(
2401
+ chalk2.gray(
2402
+ "(confirm with y / cancel with n.) Mutations stop immediately unless this proxy was started with"
2403
+ )
2404
+ );
2405
+ console.log(
2406
+ chalk2.gray(
2407
+ "PRETEST_API_KEY in that same terminal \u2014 then press Ctrl+C and run pretest start again."
2408
+ )
2409
+ );
2410
+ console.log();
2411
+ }
2412
+
2413
+ // src/version.ts
2414
+ import { readFileSync as readFileSync6 } from "fs";
2415
+ import { fileURLToPath } from "url";
2416
+ var cached = null;
2417
+ function getCliSemver() {
2418
+ if (cached !== null) return cached;
2419
+ try {
2420
+ const url = new URL("../package.json", import.meta.url);
2421
+ const raw = readFileSync6(fileURLToPath(url), "utf-8");
2422
+ cached = JSON.parse(raw).version ?? "0.0.0";
2423
+ } catch {
2424
+ cached = "0.0.0";
2425
+ }
2426
+ return cached;
2427
+ }
2428
+
2429
+ // src/proxy.ts
2430
+ var dashboardSavedCredentialsRevoked = false;
2431
+ function attachDashboardLogoutWatcher(enabled) {
2432
+ if (!enabled) return;
2433
+ const dir = getUserDashboardConfigDir();
2434
+ let debounceTimer;
2435
+ const applyFilesystemCredentialState = () => {
2436
+ const fk = loadApiKey();
2437
+ if (!fk) {
2438
+ if (dashboardSavedCredentialsRevoked) return;
2439
+ dashboardSavedCredentialsRevoked = true;
2440
+ clearDashboardApiKeyFromProcess();
2441
+ clearValidationCache();
2442
+ try {
2443
+ process.stderr.write(
2444
+ `\x1B[33m[PRETEST] Saved dashboard API key removed (~/.pretest/config.json, e.g. pretest logout). LLM proxy POST requests are disabled until you run pretest login and restart pretest start.\x1B[0m
2445
+ `
2446
+ );
2447
+ } catch {
2448
+ }
2449
+ return;
2450
+ }
2451
+ if (dashboardSavedCredentialsRevoked) {
2452
+ dashboardSavedCredentialsRevoked = false;
2453
+ clearValidationCache();
2454
+ }
2455
+ };
2456
+ const schedule = () => {
2457
+ if (debounceTimer) clearTimeout(debounceTimer);
2458
+ debounceTimer = setTimeout(applyFilesystemCredentialState, 120);
2459
+ };
2460
+ try {
2461
+ if (!existsSync7(dir)) return;
2462
+ watch(dir, () => {
2463
+ schedule();
2464
+ });
2465
+ } catch {
2466
+ }
2467
+ }
2468
+ function anthropicUpstream() {
2469
+ return process.env["ANTHROPIC_UPSTREAM_URL"] ?? "https://api.anthropic.com";
2470
+ }
2471
+ function openaiUpstream() {
2472
+ return process.env["OPENAI_UPSTREAM_URL"] ?? "https://api.openai.com";
2473
+ }
2474
+ function geminiUpstream() {
2475
+ return process.env["GEMINI_UPSTREAM_URL"] ?? "https://generativelanguage.googleapis.com";
2476
+ }
2477
+ function ollamaUpstream() {
2478
+ return process.env["OLLAMA_UPSTREAM_URL"] ?? "http://localhost:11434";
2479
+ }
2480
+ function detectUpstream(path) {
2481
+ if (path.startsWith("/v1/messages")) return anthropicUpstream();
2482
+ if (path.startsWith("/v1/chat/completions") || path.startsWith("/v1/completions") || path.startsWith("/v1/responses")) {
2483
+ return openaiUpstream();
2484
+ }
2485
+ if (path.startsWith("/v1beta/") || path.startsWith("/v1/models")) return geminiUpstream();
2486
+ if (path.startsWith("/api/chat") || path.startsWith("/api/generate")) return ollamaUpstream();
2487
+ return anthropicUpstream();
2488
+ }
2489
+ function detectProvider(path) {
2490
+ if (path.startsWith("/v1/messages")) return "anthropic";
2491
+ if (path.startsWith("/v1/chat/completions") || path.startsWith("/v1/completions") || path.startsWith("/v1/responses")) {
2492
+ return "openai";
2493
+ }
2494
+ if (path.startsWith("/v1beta/") || path.startsWith("/v1/models")) return "google";
2495
+ if (path.startsWith("/api/chat") || path.startsWith("/api/generate")) return "ollama";
2496
+ return "anthropic";
2497
+ }
2498
+ function stripPretenseOnlyForwardingHeaders(headers) {
2499
+ const drop = /* @__PURE__ */ new Set(["x-pretense-api-key", "x-pretense-key"]);
2500
+ for (const k of [...Object.keys(headers)]) {
2501
+ if (drop.has(k.toLowerCase())) delete headers[k];
2502
+ }
2503
+ }
2504
+ function extractText(body) {
2505
+ const parts = [];
2506
+ if (typeof body["system"] === "string") parts.push(body["system"]);
2507
+ if (Array.isArray(body["messages"])) {
2508
+ for (const msg of body["messages"]) {
2509
+ if (typeof msg["content"] === "string") parts.push(msg["content"]);
2510
+ else if (Array.isArray(msg["content"])) {
2511
+ for (const block of msg["content"]) {
2512
+ if (block["type"] === "text" && typeof block["text"] === "string") parts.push(block["text"]);
2513
+ }
2514
+ }
2515
+ }
2516
+ }
2517
+ return parts.join("\n");
2518
+ }
2519
+ function extractCodeBlocks(text) {
2520
+ const blocks = [];
2521
+ const re = /```(\w*)\n([\s\S]*?)```/g;
2522
+ let m;
2523
+ while ((m = re.exec(text)) !== null) {
2524
+ blocks.push({
2525
+ code: m[2] ?? "",
2526
+ language: m[1] ?? "typescript",
2527
+ start: m.index,
2528
+ end: m.index + m[0].length
2529
+ });
2530
+ }
2531
+ return blocks;
2532
+ }
2533
+ function replaceCodeBlocks(text, blocks, replacements) {
2534
+ let result = text;
2535
+ for (let i = blocks.length - 1; i >= 0; i--) {
2536
+ const block = blocks[i];
2537
+ const replacement = replacements[i] ?? block.code;
2538
+ const lang = block.language ?? "typescript";
2539
+ result = result.slice(0, block.start) + "```" + lang + "\n" + replacement + "```" + result.slice(block.end);
2540
+ }
2541
+ return result;
2542
+ }
2543
+ function applyToBody(body, transform) {
2544
+ const mutateField = (val) => {
2545
+ if (typeof val === "string") return transform(val);
2546
+ if (Array.isArray(val)) return val.map(mutateField);
2547
+ if (val && typeof val === "object") {
2548
+ const obj = val;
2549
+ return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, mutateField(v)]));
2550
+ }
2551
+ return val;
2552
+ };
2553
+ return mutateField(body);
2554
+ }
2555
+ var stats = {
2556
+ requestsProcessed: 0,
2557
+ totalTokensMutated: 0,
2558
+ totalSecretsBlocked: 0,
2559
+ startedAt: Date.now()
2560
+ };
2561
+ function daysSinceFirstUse() {
2562
+ const usage = loadUsage();
2563
+ if (!usage.firstUseDate) return 0;
2564
+ const first = new Date(usage.firstUseDate).getTime();
2565
+ return Math.floor((Date.now() - first) / (24 * 60 * 60 * 1e3));
2566
+ }
2567
+ function printUpgradeNudge(reason) {
2568
+ process.stdout.write(
2569
+ `
2570
+ \x1B[33m[PRETEST] ${reason}\x1B[0m
2571
+ \x1B[33m[PRETEST] Upgrade to Pro: ${PUBLISH_PACKAGE_URL}\x1B[0m
2572
+
2573
+ `
2574
+ );
2575
+ }
2576
+ function createProxy(opts = {}) {
2577
+ const config = { ...DEFAULT_CONFIG, ...opts.config };
2578
+ const store = opts.store ?? new MutationStore(`${config.storePath}/proxy-store.json`);
2579
+ const verbose = opts.verbose ?? false;
2580
+ const app = new Hono();
2581
+ app.use("*", requireApiKey);
2582
+ app.get(
2583
+ "/",
2584
+ (c) => c.json({
2585
+ name: "pretest",
2586
+ version: getCliSemver(),
2587
+ status: "running",
2588
+ routes: {
2589
+ "GET /": "This welcome page",
2590
+ "GET /health": "Health check with uptime",
2591
+ "GET /stats": "Mutation statistics",
2592
+ "GET /audit": "Audit log (?limit=50)",
2593
+ "POST /*": "Proxy to upstream LLM API (Anthropic, OpenAI, Google auto-detected)"
2594
+ },
2595
+ languages: ["TypeScript", "JavaScript", "Python", "Go", "Java", "C#", "Ruby", "Rust"],
2596
+ docs: PUBLISH_PACKAGE_URL
2597
+ })
2598
+ );
2599
+ app.get(
2600
+ "/health",
2601
+ (c) => c.json({
2602
+ status: "ok",
2603
+ version: getCliSemver(),
2604
+ uptime: Math.floor((Date.now() - stats.startedAt) / 1e3),
2605
+ stats: {
2606
+ requestsProcessed: stats.requestsProcessed,
2607
+ totalTokensMutated: stats.totalTokensMutated
2608
+ }
2609
+ })
2610
+ );
2611
+ app.get(
2612
+ "/stats",
2613
+ (c) => c.json({
2614
+ ...stats,
2615
+ storeSize: store.size,
2616
+ activeSessions: sessionCount()
2617
+ })
2618
+ );
2619
+ app.get("/audit", (c) => {
2620
+ const limit = parseInt(c.req.query("limit") ?? "50");
2621
+ return c.json(store.list(limit));
2622
+ });
2623
+ app.all("/*", async (c) => {
2624
+ if (c.req.method !== "POST") {
2625
+ const upstream2 = c.req.header("x-pretense-upstream") ?? detectUpstream(c.req.path);
2626
+ const url = upstream2 + c.req.path;
2627
+ const headers2 = {};
2628
+ c.req.raw.headers.forEach((v, k) => {
2629
+ headers2[k] = v;
2630
+ });
2631
+ delete headers2["host"];
2632
+ headers2["host"] = new URL(upstream2).host;
2633
+ stripPretenseOnlyForwardingHeaders(headers2);
2634
+ const resp = await fetch(url, { method: c.req.method, headers: headers2 });
2635
+ return new Response(resp.body, { status: resp.status, headers: Object.fromEntries(resp.headers.entries()) });
2636
+ }
2637
+ const requestStart = performance.now();
2638
+ let body;
2639
+ try {
2640
+ body = await c.req.json();
2641
+ } catch {
2642
+ return c.json({ error: { type: "invalid_request", message: "Invalid JSON body" } }, 400);
2643
+ }
2644
+ if (!body || Object.keys(body).length === 0) {
2645
+ return c.json({ error: { type: "invalid_request", message: "Empty request body" } }, 400);
2646
+ }
2647
+ const requestId = nanoid(12);
2648
+ const upstream = c.req.header("x-pretense-upstream") ?? detectUpstream(c.req.path);
2649
+ const provider = detectProvider(c.req.path);
2650
+ if (dashboardSavedCredentialsRevoked) {
2651
+ return c.json(
2652
+ {
2653
+ error: {
2654
+ type: "pretense_session_logged_out",
2655
+ message: "Saved dashboard credentials were removed (for example pretest logout). Run pretest login, then restart pretest start. If you started the proxy with a dashboard API key in this shell, stop it (Ctrl+C) and start again after logout."
2656
+ }
2657
+ },
2658
+ 401
2659
+ );
2660
+ }
2661
+ const dashboardApiKey = getDashboardApiKey();
2662
+ if (!dashboardApiKey) {
2663
+ return c.json(
2664
+ {
2665
+ error: {
2666
+ type: "pretense_dashboard_key_required",
2667
+ message: "Configure your dashboard API key: run `pretest login --key prtns_live_...` or set PRETEST_API_KEY."
2668
+ }
2669
+ },
2670
+ 401
2671
+ );
2672
+ }
2673
+ try {
2674
+ await validateKey({ verbose, force: false, enforceReachability: true });
2675
+ } catch (err) {
2676
+ const code = err.code ?? "";
2677
+ const msg = err.message;
2678
+ const knownAuthCodes = /* @__PURE__ */ new Set([
2679
+ "KEY_REVOKED",
2680
+ "KEY_EXPIRED",
2681
+ "INVALID_API_KEY",
2682
+ "MISSING_API_KEY"
2683
+ ]);
2684
+ if (knownAuthCodes.has(code)) {
2685
+ return c.json(
2686
+ { error: { type: "pretense_auth_error", code, message: msg } },
2687
+ 401
2688
+ );
2689
+ }
2690
+ if (code === "BACKEND_UNAVAILABLE" || code === "AUTH_BACKEND_REJECTED") {
2691
+ return c.json(
2692
+ { error: { type: "pretense_auth_backend_error", code, message: msg } },
2693
+ 503
2694
+ );
2695
+ }
2696
+ if (code === "ORG_INACTIVE") {
2697
+ return c.json(
2698
+ { error: { type: "pretense_org_inactive", code, message: msg } },
2699
+ 403
2700
+ );
2701
+ }
2702
+ return c.json(
2703
+ { error: { type: "pretense_auth_error", message: msg } },
2704
+ 401
2705
+ );
2706
+ }
2707
+ const sessHash = c.get("sessionHash");
2708
+ const session = getSession(sessHash);
2709
+ const tier = tierFromDashboardPlan(
2710
+ getCachedValidation()?.plan,
2711
+ detectTier()
2712
+ );
2713
+ const { monthlyMutations } = getTierLimits(tier);
2714
+ const usage = loadUsage();
2715
+ if (tier === "free" && usage.mutations >= monthlyMutations) {
2716
+ return c.json(
2717
+ {
2718
+ error: {
2719
+ type: "pretense_rate_limit",
2720
+ message: `Monthly mutation limit reached (${monthlyMutations}/month). Upgrade to Pro for unlimited: ${PUBLISH_PACKAGE_URL}`
2721
+ }
2722
+ },
2723
+ 429
2724
+ );
2725
+ }
2726
+ const backendLimits = await isWithinLimits();
2727
+ if (!backendLimits.allowed) {
2728
+ return c.json(
2729
+ {
2730
+ error: {
2731
+ type: "pretense_rate_limit",
2732
+ message: backendLimits.reason ?? "Mutation limit exceeded."
2733
+ }
2734
+ },
2735
+ 429
2736
+ );
2737
+ }
2738
+ const cliAuth = getCachedValidation();
2739
+ if (cliAuth?.permission === "read_only") {
2740
+ return c.json(
2741
+ {
2742
+ error: {
2743
+ type: "pretense_forbidden",
2744
+ message: "This API key is read-only. Create a read_write key in the dashboard to use the Pretest proxy."
2745
+ }
2746
+ },
2747
+ 403
2748
+ );
2749
+ }
2750
+ const fullText = extractText(body);
2751
+ const secretScan = scan2(fullText);
2752
+ const blockedSecrets = secretScan.matches.filter((m) => m.action === "block");
2753
+ if (blockedSecrets.length > 0) {
2754
+ stats.totalSecretsBlocked += blockedSecrets.length;
2755
+ if (tier === "free") {
2756
+ printUpgradeNudge(`${blockedSecrets.length} secret(s) blocked. Pro tier includes real-time secret alerts via Slack/PagerDuty.`);
2757
+ }
2758
+ return c.json(
2759
+ {
2760
+ error: {
2761
+ type: "pretense_blocked",
2762
+ message: `Request blocked: ${blockedSecrets.length} secret(s) detected. Types: ${[...new Set(blockedSecrets.map((m) => m.type))].join(", ")}`
2763
+ }
2764
+ },
2765
+ 400
2766
+ );
2767
+ }
2768
+ const processedBody = applyToBody(body, (text) => applyRedactions(text, secretScan.matches));
2769
+ const mutationMap = /* @__PURE__ */ new Map();
2770
+ let tokensMutated = 0;
2771
+ const mutatedBody = applyToBody(processedBody, (text) => {
2772
+ const blocks = extractCodeBlocks(text);
2773
+ if (blocks.length === 0) {
2774
+ const lang = detectLanguage(text);
2775
+ const result = mutate(text, lang);
2776
+ for (const [k, v] of result.map) {
2777
+ mutationMap.set(k, v);
2778
+ session.mutationMap.set(k, v);
2779
+ session.reverseMap.set(v, k);
2780
+ }
2781
+ tokensMutated += result.stats.tokensMutated;
2782
+ return result.mutatedCode;
2783
+ }
2784
+ const mutatedBlocks = [];
2785
+ for (const block of blocks) {
2786
+ const result = mutate(block.code, block.language);
2787
+ for (const [k, v] of result.map) {
2788
+ mutationMap.set(k, v);
2789
+ session.mutationMap.set(k, v);
2790
+ session.reverseMap.set(v, k);
2791
+ }
2792
+ tokensMutated += result.stats.tokensMutated;
2793
+ mutatedBlocks.push(result.mutatedCode);
2794
+ }
2795
+ return replaceCodeBlocks(text, blocks, mutatedBlocks);
2796
+ });
2797
+ usage.mutations += tokensMutated;
2798
+ saveUsage(usage);
2799
+ const remoteLogDisabled = process.env["PRETENSE_DISABLE_REMOTE_LOG"] === "1";
2800
+ if (dashboardApiKey && !remoteLogDisabled) {
2801
+ void uploadLog(
2802
+ {
2803
+ file_count: 0,
2804
+ identifiers_mutated: tokensMutated,
2805
+ secrets_blocked: blockedSecrets.length,
2806
+ llm_provider: provider,
2807
+ cli_version: getCliSemver()
2808
+ },
2809
+ { apiKey: dashboardApiKey, verbose }
2810
+ );
2811
+ bumpCachedUsageAfterLog(tokensMutated);
2812
+ }
2813
+ const warningThreshold = Math.floor(monthlyMutations * 0.8);
2814
+ if (tier === "free" && usage.mutations >= warningThreshold && usage.mutations - tokensMutated < warningThreshold) {
2815
+ printUpgradeNudge(`${usage.mutations}/${monthlyMutations} monthly mutations used. Running low!`);
2816
+ }
2817
+ if (tier === "free" && daysSinceFirstUse() >= 7) {
2818
+ printUpgradeNudge("You've been using Pretest for 7+ days. Unlock Pro features: unlimited mutations, 90-day audit, Slack alerts.");
2819
+ }
2820
+ const entry = {
2821
+ id: requestId,
2822
+ originalHash: requestId,
2823
+ mapEntries: [...mutationMap.entries()],
2824
+ timestamp: Date.now(),
2825
+ language: "mixed"
2826
+ };
2827
+ store.save(entry);
2828
+ stats.requestsProcessed++;
2829
+ stats.totalTokensMutated += tokensMutated;
2830
+ const latencyMs = Math.round((performance.now() - requestStart) * 100) / 100;
2831
+ writeAuditEntry(
2832
+ createAuditEntry(requestId, "proxy-request", tokensMutated, blockedSecrets.length, provider, latencyMs)
2833
+ );
2834
+ if (verbose && tokensMutated > 0) {
2835
+ process.stdout.write(`[PRETEST] ${tokensMutated} tokens mutated for request ${requestId}
2836
+ `);
2837
+ }
2838
+ const headers = {};
2839
+ c.req.raw.headers.forEach((v, k) => {
2840
+ headers[k] = v;
2841
+ });
2842
+ delete headers["host"];
2843
+ delete headers["content-length"];
2844
+ headers["host"] = new URL(upstream).host;
2845
+ headers["x-pretense-request-id"] = requestId;
2846
+ stripPretenseOnlyForwardingHeaders(headers);
2847
+ const forwardBody = JSON.stringify(mutatedBody);
2848
+ let upstreamResp;
2849
+ try {
2850
+ upstreamResp = await fetch(upstream + c.req.path, {
2851
+ method: "POST",
2852
+ headers: { ...headers, "content-type": "application/json", "content-length": String(Buffer.byteLength(forwardBody)) },
2853
+ body: forwardBody
2854
+ });
2855
+ } catch {
2856
+ return c.json({ error: { type: "pretense_upstream_error", message: "Failed to reach upstream provider" } }, 502);
2857
+ }
2858
+ if (body["stream"] === true) {
2859
+ const streamReverseMap = new Map(mutationMap);
2860
+ const upstream2 = upstreamResp.body;
2861
+ if (!upstream2 || tokensMutated === 0) {
2862
+ return new Response(upstream2, {
2863
+ status: upstreamResp.status,
2864
+ headers: {
2865
+ "content-type": upstreamResp.headers.get("content-type") ?? "text/event-stream",
2866
+ "cache-control": "no-cache",
2867
+ "x-pretense-request-id": requestId,
2868
+ "x-pretense-protected": "true",
2869
+ "x-pretense-mutations": String(tokensMutated)
2870
+ }
2871
+ });
2872
+ }
2873
+ const decoder = new TextDecoder();
2874
+ const encoder = new TextEncoder();
2875
+ let buffer = "";
2876
+ const transform = new TransformStream({
2877
+ transform(chunk, controller) {
2878
+ buffer += decoder.decode(chunk, { stream: true });
2879
+ const lines = buffer.split("\n");
2880
+ buffer = lines.pop() ?? "";
2881
+ for (const line of lines) {
2882
+ if (line.startsWith("data: ") && line !== "data: [DONE]") {
2883
+ try {
2884
+ const json = JSON.parse(line.slice(6));
2885
+ const reversed = applyToBody(json, (text) => reverse(text, streamReverseMap));
2886
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(reversed)}
2887
+ `));
2888
+ } catch {
2889
+ controller.enqueue(encoder.encode(line + "\n"));
2890
+ }
2891
+ } else {
2892
+ controller.enqueue(encoder.encode(line + "\n"));
2893
+ }
2894
+ }
2895
+ },
2896
+ flush(controller) {
2897
+ if (buffer.length > 0) {
2898
+ controller.enqueue(encoder.encode(buffer));
2899
+ }
2900
+ }
2901
+ });
2902
+ const reversedStream = upstream2.pipeThrough(transform);
2903
+ return new Response(reversedStream, {
2904
+ status: upstreamResp.status,
2905
+ headers: {
2906
+ "content-type": upstreamResp.headers.get("content-type") ?? "text/event-stream",
2907
+ "cache-control": "no-cache",
2908
+ "x-pretense-request-id": requestId,
2909
+ "x-pretense-protected": "true",
2910
+ "x-pretense-mutations": String(tokensMutated),
2911
+ "x-pretense-stream-reversal": "active"
2912
+ }
2913
+ });
2914
+ }
2915
+ const reverseMap = new Map(mutationMap);
2916
+ let respBody;
2917
+ try {
2918
+ respBody = await upstreamResp.json();
2919
+ } catch {
2920
+ return new Response(null, {
2921
+ status: upstreamResp.status,
2922
+ headers: {
2923
+ "x-pretense-protected": "true",
2924
+ "x-pretense-mutations": String(tokensMutated)
2925
+ }
2926
+ });
2927
+ }
2928
+ const reversedBody = applyToBody(respBody, (text) => {
2929
+ const blocks = extractCodeBlocks(text);
2930
+ if (blocks.length === 0) return reverse(text, reverseMap);
2931
+ const reversedBlocks = blocks.map((b) => reverse(b.code, reverseMap));
2932
+ return replaceCodeBlocks(text, blocks, reversedBlocks);
2933
+ });
2934
+ c.header("x-pretense-request-id", requestId);
2935
+ c.header("x-pretense-protected", "true");
2936
+ c.header("x-pretense-mutations", String(tokensMutated));
2937
+ return c.json(reversedBody, upstreamResp.status);
2938
+ });
2939
+ return app;
2940
+ }
2941
+ function isPortAvailable(port) {
2942
+ return new Promise((resolve3) => {
2943
+ const tester = createServer().once("error", () => resolve3(false)).once("listening", () => {
2944
+ tester.once("close", () => resolve3(true)).close();
2945
+ }).listen(port, "127.0.0.1");
2946
+ });
2947
+ }
2948
+ async function findAvailablePort(startPort, maxAttempts = 10) {
2949
+ for (let i = 0; i < maxAttempts; i++) {
2950
+ const candidate = startPort + i;
2951
+ if (candidate < 1 || candidate > 65535) break;
2952
+ if (await isPortAvailable(candidate)) return candidate;
2953
+ }
2954
+ throw new Error(
2955
+ `No available port found in range ${startPort}-${startPort + maxAttempts - 1}. Free a port or pass --port <number>.`
2956
+ );
2957
+ }
2958
+ function startProxy(opts = {}) {
2959
+ const requestedPort = opts.port ?? opts.config?.port ?? DEFAULT_CONFIG.port;
2960
+ const app = createProxy(opts);
2961
+ void (async () => {
2962
+ const dashboardKeyFromEnvAtLaunch = hasDashboardApiKeyInEnv();
2963
+ const dashboardKey = getDashboardApiKey();
2964
+ if (!dashboardKey || !isValidKeyShape(dashboardKey)) {
2965
+ process.stderr.write(
2966
+ `\x1B[31m[PRETEST] API key required or invalid shape. Run \`pretest login --key prtns_live_...\`\x1B[0m
2967
+ \x1B[31m[PRETEST] or set PRETEST_API_KEY. See ${PUBLISH_PACKAGE_URL}\x1B[0m
2968
+ `
2969
+ );
2970
+ process.exit(1);
2971
+ }
2972
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
2973
+ for (; ; ) {
2974
+ try {
2975
+ const validation = await validateKey({
2976
+ force: true,
2977
+ verbose: opts.verbose,
2978
+ enforceReachability: true
2979
+ });
2980
+ if (!validation) {
2981
+ process.stderr.write(
2982
+ `\x1B[31m[PRETEST] Startup validation failed (no API key or unreachable backend). Cannot start proxy.\x1B[0m
2983
+ `
2984
+ );
2985
+ process.exit(1);
2986
+ }
2987
+ const plan = validation.plan ?? "FREE";
2988
+ const remaining = validation.mutationsRemaining === -1 ? "unlimited" : String(validation.mutationsRemaining);
2989
+ process.stdout.write(
2990
+ `\x1B[32m[PRETEST] Key validated: ${plan} plan` + (validation.organizationName ? ` (${validation.organizationName})` : "") + ` \u2014 ${remaining} mutations remaining\x1B[0m
2991
+ `
2992
+ );
2993
+ break;
2994
+ } catch (err) {
2995
+ const code = err.code;
2996
+ const msg = err.message;
2997
+ if (code === "BACKEND_UNAVAILABLE" || code === "AUTH_BACKEND_REJECTED") {
2998
+ process.stderr.write(`\x1B[31m[PRETEST] ${msg}\x1B[0m
2999
+ `);
3000
+ process.stderr.write(`\x1B[31m[PRETEST] Fix network or set PRETEST_API_URL (or PRETENSE_API_URL) if using a custom API.\x1B[0m
3001
+ `);
3002
+ process.exit(1);
3003
+ }
3004
+ if (code === "ORG_INACTIVE") {
3005
+ process.stderr.write(`\x1B[31m[PRETEST] ${msg}\x1B[0m
3006
+ `);
3007
+ process.stderr.write(`\x1B[31m[PRETEST] Your organization is inactive; contact support.\x1B[0m
3008
+ `);
3009
+ process.exit(1);
3010
+ }
3011
+ const authRetryable = code === "INVALID_API_KEY" || code === "KEY_REVOKED" || code === "KEY_EXPIRED" || code === "AUTH_ERROR" || code === "MISSING_API_KEY";
3012
+ if (authRetryable && interactive) {
3013
+ process.stderr.write(`\x1B[31m[PRETEST] ${msg}\x1B[0m
3014
+ `);
3015
+ process.stderr.write(
3016
+ `\x1B[33m[PRETEST] Enter your dashboard API key and press Enter (Ctrl+C to quit).\x1B[0m
3017
+ `
3018
+ );
3019
+ let entered = null;
3020
+ for (; ; ) {
3021
+ entered = await promptDashboardApiKeyAfterFailure();
3022
+ if (!entered) {
3023
+ process.stderr.write(`\x1B[31m[PRETEST] No key entered; exiting.\x1B[0m
3024
+ `);
3025
+ process.exit(1);
3026
+ }
3027
+ if (isValidKeyShape(entered)) break;
3028
+ process.stderr.write(
3029
+ `\x1B[31m[PRETEST] That does not look like a dashboard key (expect prtns_live_\u2026 or pk_live_\u2026).\x1B[0m
3030
+ `
3031
+ );
3032
+ }
3033
+ const savedPath = installDashboardKeyForCurrentProcess(entered);
3034
+ process.stdout.write(`\x1B[32m[PRETEST] Saved API key to ${savedPath}\x1B[0m
3035
+ `);
3036
+ clearValidationCache();
3037
+ continue;
3038
+ }
3039
+ if (code === "KEY_REVOKED" || code === "KEY_EXPIRED" || code === "INVALID_API_KEY" || code === "AUTH_ERROR" || code === "MISSING_API_KEY") {
3040
+ process.stderr.write(`\x1B[31m[PRETEST] ${msg}\x1B[0m
3041
+ `);
3042
+ process.stderr.write(`\x1B[31m[PRETEST] Run 'pretest login --key <new-key>' to fix.\x1B[0m
3043
+ `);
3044
+ process.exit(1);
3045
+ }
3046
+ process.stderr.write(`\x1B[31m[PRETEST] ${msg}\x1B[0m
3047
+ `);
3048
+ process.stderr.write(`\x1B[31m[PRETEST] Run 'pretest login --key <new-key>' or check connectivity.\x1B[0m
3049
+ `);
3050
+ process.exit(1);
3051
+ }
3052
+ }
3053
+ attachDashboardLogoutWatcher(!dashboardKeyFromEnvAtLaunch);
3054
+ const bannerTier = tierFromDashboardPlan(
3055
+ getCachedValidation()?.plan,
3056
+ detectTier()
3057
+ );
3058
+ let port;
3059
+ try {
3060
+ port = await findAvailablePort(requestedPort, 10);
3061
+ } catch (err) {
3062
+ process.stderr.write(`\x1B[31m[PRETEST] ${err.message}\x1B[0m
3063
+ `);
3064
+ process.exit(1);
3065
+ }
3066
+ if (port !== requestedPort) {
3067
+ process.stderr.write(
3068
+ `\x1B[33m[PRETEST] Port ${requestedPort} in use, falling back to ${port}.\x1B[0m
3069
+ `
3070
+ );
3071
+ }
3072
+ printStartClientInstructions(port);
3073
+ serve({ fetch: app.fetch, port }, () => {
3074
+ const portStr = String(port);
3075
+ process.stdout.write(`
3076
+ \x1B[36mPretest v${getCliSemver()} [${bannerTier.toUpperCase()}]\x1B[0m
3077
+ Your code hits AI naked. We fix that.
3078
+
3079
+ Proxy: http://localhost:${portStr}
3080
+ Health: http://localhost:${portStr}/health
3081
+ Stats: http://localhost:${portStr}/stats
3082
+
3083
+ Drop-in env vars:
3084
+ ANTHROPIC_BASE_URL=http://localhost:${portStr}
3085
+ OPENAI_BASE_URL=http://localhost:${portStr}/v1
3086
+ GEMINI_BASE_URL=http://localhost:${portStr}/v1beta
3087
+ OLLAMA_HOST=http://localhost:${portStr}
3088
+
3089
+ Routes intercepted:
3090
+ /v1/messages -> Anthropic (Claude Code, SDK)
3091
+ /v1/chat/completions -> OpenAI (ChatGPT, Cursor)
3092
+ /v1/responses -> OpenAI (Codex CLI)
3093
+ /v1beta/... -> Google Gemini
3094
+ /api/chat, /api/generate -> Ollama (local)
3095
+ `);
3096
+ setInterval(() => {
3097
+ if (dashboardSavedCredentialsRevoked) return;
3098
+ if (!getDashboardApiKey()) return;
3099
+ void validateKey({
3100
+ force: true,
3101
+ verbose: opts.verbose,
3102
+ enforceReachability: true
3103
+ }).catch(() => {
3104
+ });
3105
+ }, 6e4);
3106
+ });
3107
+ })();
3108
+ }
3109
+
3110
+ // src/token-footer.ts
3111
+ import chalk3 from "chalk";
3112
+ var PLAN_LIMITS = {
3113
+ free: { monthlyMutations: 1e3, monthlyScans: 5e3, seats: 3, pricePerMonth: "$0/month" },
3114
+ pro: { monthlyMutations: 1e5, monthlyScans: 5e5, seats: 25, pricePerMonth: "$29/seat/month" },
3115
+ enterprise: {
3116
+ monthlyMutations: Number.POSITIVE_INFINITY,
3117
+ monthlyScans: Number.POSITIVE_INFINITY,
3118
+ seats: Number.POSITIVE_INFINITY,
3119
+ pricePerMonth: "$99/seat/month"
3120
+ }
3121
+ };
3122
+ function getPlanLimits(tier) {
3123
+ return PLAN_LIMITS[tier];
3124
+ }
3125
+ function fmt(n) {
3126
+ if (!Number.isFinite(n)) return "unlimited";
3127
+ return n.toLocaleString("en-US");
3128
+ }
3129
+ function printTokenFooter(filesScanned, mutationsMade) {
3130
+ const tier = detectTier();
3131
+ const limits = getPlanLimits(tier);
3132
+ const usage = loadUsage();
3133
+ if (tier === "enterprise") {
3134
+ process.stderr.write(
3135
+ chalk3.gray(`
3136
+ ${filesScanned} files, ${mutationsMade} mutations \xB7 Plan: Enterprise (unlimited)
3137
+
3138
+ `)
3139
+ );
3140
+ return;
3141
+ }
3142
+ const used = usage.mutations;
3143
+ const cap = limits.monthlyMutations;
3144
+ const pct = cap > 0 ? Math.min(100, Math.round(used / cap * 100)) : 0;
3145
+ const usageStr = pct >= 80 ? chalk3.yellow(`${fmt(used)} / ${fmt(cap)}`) : chalk3.gray(`${fmt(used)} / ${fmt(cap)}`);
3146
+ process.stderr.write(
3147
+ `
3148
+ ${chalk3.cyan(filesScanned + " files")}, ${chalk3.cyan(mutationsMade + " mutations")} \xB7 Plan: ${chalk3.bold(tier.toUpperCase())} \xB7 Mutations this month: ${usageStr} (${pct}%)
3149
+ `
3150
+ );
3151
+ if (tier === "free") {
3152
+ if (pct >= 80) {
3153
+ process.stderr.write(
3154
+ chalk3.yellow(` \u26A0 Approaching free-tier limit. Upgrade: ${chalk3.bold("pretest upgrade")}
3155
+
3156
+ `)
3157
+ );
3158
+ } else {
3159
+ process.stderr.write(
3160
+ chalk3.gray(` Run ${chalk3.bold("pretest status")} for details, ${chalk3.bold("pretest upgrade")} to remove limits.
3161
+
3162
+ `)
3163
+ );
3164
+ }
3165
+ } else {
3166
+ process.stderr.write("\n");
3167
+ }
3168
+ }
3169
+
3170
+ // src/banner.ts
3171
+ import chalk4 from "chalk";
3172
+ var BANNER_RAW = `\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 pretest
3173
+ \u2588\u2588 \u2588\u2588
3174
+ \u2588\u2588 \u2588\u2588 Stop your code from
3175
+ \u2588\u2588 \u2588\u2588 leaking to any LLM.
3176
+ \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
3177
+ \u2588\u2588 @blockyfy/stg-cli
3178
+ \u2588\u2588
3179
+ \u2588\u2588
3180
+ \u2588\u2588
3181
+ \u2588\u2588`;
3182
+ var ICE_BLUE = "#7DD3FC";
3183
+ function shouldPrint() {
3184
+ if (DASHBOARD_NO_BANNER_ENVS.some((n) => process.env[n] === "1")) return false;
3185
+ if (!process.stdout.isTTY) return false;
3186
+ if (process.env.CI) return false;
3187
+ return true;
3188
+ }
3189
+ function tint(line) {
3190
+ try {
3191
+ return chalk4.hex(ICE_BLUE)(line);
3192
+ } catch {
3193
+ return line;
3194
+ }
3195
+ }
3196
+ function printBanner() {
3197
+ if (!shouldPrint()) return;
3198
+ const lines = BANNER_RAW.split("\n");
3199
+ for (const line of lines) {
3200
+ process.stderr.write(tint(line) + "\n");
3201
+ }
3202
+ process.stderr.write("\n");
3203
+ }
3204
+
3205
+ // src/commands/status.ts
3206
+ import chalk5 from "chalk";
3207
+ var PLAN_LABEL = {
3208
+ free: "Free",
3209
+ pro: "Pro",
3210
+ enterprise: "Enterprise"
3211
+ };
3212
+ function fmt2(n) {
3213
+ if (n === -1 || !Number.isFinite(n)) return "unlimited";
3214
+ return n.toLocaleString("en-US");
3215
+ }
3216
+ async function runStatus() {
3217
+ let remote = await fetchUsageSummary();
3218
+ if (!remote) {
3219
+ const v = await validateKey({ force: true });
3220
+ if (v?.valid) {
3221
+ remote = usageSummaryFromValidate(v);
3222
+ }
3223
+ }
3224
+ if (remote) {
3225
+ const plan = remote.plan;
3226
+ const tierBadge2 = plan === "ENTERPRISE" ? chalk5.bgMagenta.white.bold(" ENTERPRISE ") : plan === "PRO" ? chalk5.bgBlue.white.bold(" PRO ") : chalk5.bgGray.white.bold(" FREE ");
3227
+ const lines2 = [];
3228
+ lines2.push("");
3229
+ lines2.push(`${chalk5.cyan("Pretest")} ${chalk5.gray(`v${getCliSemver()}`)} ${tierBadge2} ${chalk5.green("(synced)")}`);
3230
+ lines2.push("");
3231
+ lines2.push(` Plan: ${chalk5.bold(plan)}`);
3232
+ lines2.push(` Mutations: ${fmt2(remote.mutationsUsed)} / ${fmt2(remote.monthlyMutationLimit)} this month`);
3233
+ lines2.push(` Remaining: ${fmt2(remote.mutationsRemaining)}`);
3234
+ lines2.push(` Requests: ${fmt2(remote.requestsThisPeriod)} this period`);
3235
+ lines2.push(` Secrets: ${fmt2(remote.secretsBlockedThisPeriod)} blocked`);
3236
+ lines2.push(` Key status: ${remote.keyStatus === "active" ? chalk5.green("active") : chalk5.red("revoked")}`);
3237
+ lines2.push(` Resets at: ${new Date(remote.periodResetsAt).toLocaleDateString()}`);
3238
+ if (remote.keyExpiresAt) {
3239
+ lines2.push(` Key expires: ${new Date(remote.keyExpiresAt).toLocaleDateString()}`);
3240
+ }
3241
+ lines2.push("");
3242
+ if (plan === "FREE") {
3243
+ lines2.push(` ${chalk5.gray("Upgrade:")} ${chalk5.bold("pretest upgrade")}`);
3244
+ lines2.push("");
3245
+ }
3246
+ process.stdout.write(lines2.join("\n") + "\n");
3247
+ return;
3248
+ }
3249
+ const tier = detectTier();
3250
+ const planLimits = getPlanLimits(tier);
3251
+ const tierLimits = getTierLimits(tier);
3252
+ const usage = loadUsage();
3253
+ const tierBadge = tier === "enterprise" ? chalk5.bgMagenta.white.bold(" ENTERPRISE ") : tier === "pro" ? chalk5.bgBlue.white.bold(" PRO ") : chalk5.bgGray.white.bold(" FREE ");
3254
+ const lines = [];
3255
+ lines.push("");
3256
+ lines.push(`${chalk5.cyan("Pretest")} ${chalk5.gray(`v${getCliSemver()}`)} ${tierBadge} ${chalk5.yellow("(offline)")}`);
3257
+ lines.push("");
3258
+ lines.push(` Plan: ${chalk5.bold(PLAN_LABEL[tier])} ${chalk5.gray("(" + planLimits.pricePerMonth + ")")}`);
3259
+ lines.push(` Mutations: ${fmt2(usage.mutations)} / ${fmt2(planLimits.monthlyMutations)} this month`);
3260
+ lines.push(` Scans: 0 / ${fmt2(planLimits.monthlyScans)} this month`);
3261
+ lines.push(` Seats: 1 / ${fmt2(planLimits.seats)}`);
3262
+ lines.push(` Audit log: ${Number.isFinite(tierLimits.auditRetentionDays) ? `${tierLimits.auditRetentionDays} days` : "unlimited"}`);
3263
+ lines.push("");
3264
+ if (tier === "free") {
3265
+ lines.push(` ${chalk5.gray("Upgrade:")} ${chalk5.bold("pretest upgrade")}`);
3266
+ lines.push(` ${chalk5.gray("Credits:")} ${chalk5.bold("pretest credits")}`);
3267
+ lines.push("");
3268
+ }
3269
+ process.stdout.write(lines.join("\n") + "\n");
3270
+ }
3271
+
3272
+ // src/commands/upgrade.ts
3273
+ import chalk6 from "chalk";
3274
+ var PRICING_URL = PUBLISH_PACKAGE_URL;
3275
+ function runUpgrade() {
3276
+ const tier = detectTier();
3277
+ const lines = [];
3278
+ lines.push("");
3279
+ lines.push(chalk6.cyan.bold(" Pretest Plans"));
3280
+ lines.push("");
3281
+ lines.push(chalk6.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"));
3282
+ lines.push(chalk6.gray(" \u2502 \u2502 Free \u2502 Pro \u2502 Enterprise \u2502"));
3283
+ lines.push(chalk6.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"));
3284
+ lines.push(` \u2502 Price/seat \u2502 ${chalk6.bold("$0")} \u2502 ${chalk6.bold("$29")} \u2502 ${chalk6.bold("$99")} \u2502`);
3285
+ lines.push(" \u2502 Mutations/mo \u2502 1,000 \u2502 100,000 \u2502 unlimited \u2502");
3286
+ lines.push(" \u2502 Scans/mo \u2502 5,000 \u2502 500,000 \u2502 unlimited \u2502");
3287
+ lines.push(" \u2502 Seats \u2502 3 \u2502 25 \u2502 unlimited \u2502");
3288
+ lines.push(" \u2502 Audit log \u2502 30 days \u2502 90 days \u2502 unlimited \u2502");
3289
+ lines.push(" \u2502 SSO/SAML \u2502 - \u2502 - \u2502 yes \u2502");
3290
+ lines.push(" \u2502 On-prem \u2502 - \u2502 - \u2502 yes \u2502");
3291
+ lines.push(" \u2502 SLA \u2502 - \u2502 - \u2502 24/7 \u2502");
3292
+ lines.push(chalk6.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"));
3293
+ lines.push("");
3294
+ lines.push(` ${chalk6.gray("Current plan:")} ${chalk6.bold(tier.toUpperCase())}`);
3295
+ lines.push(` ${chalk6.gray("Upgrade at:")} ${chalk6.cyan.underline(PRICING_URL)}`);
3296
+ lines.push("");
3297
+ lines.push(chalk6.gray(" After purchase, set:"));
3298
+ lines.push(chalk6.gray(" export PRETEST_LICENSE_KEY=pre_pro_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"));
3299
+ lines.push("");
3300
+ process.stdout.write(lines.join("\n") + "\n");
3301
+ }
3302
+
3303
+ // src/commands/credits.ts
3304
+ import chalk7 from "chalk";
3305
+ var PLAN_LABEL2 = {
3306
+ free: "Free",
3307
+ pro: "Pro",
3308
+ enterprise: "Enterprise"
3309
+ };
3310
+ function fmt3(n) {
3311
+ if (n === -1 || !Number.isFinite(n)) return "unlimited";
3312
+ return n.toLocaleString("en-US");
3313
+ }
3314
+ function progressBar(used, cap, width = 24) {
3315
+ if (cap === -1 || !Number.isFinite(cap) || cap <= 0) return chalk7.green("[" + "=".repeat(width) + "]");
3316
+ const pct = Math.min(1, used / cap);
3317
+ const filled = Math.round(pct * width);
3318
+ const bar = "=".repeat(filled) + "-".repeat(width - filled);
3319
+ const colored = pct >= 0.8 ? chalk7.yellow(bar) : pct >= 1 ? chalk7.red(bar) : chalk7.green(bar);
3320
+ return "[" + colored + "]";
3321
+ }
3322
+ async function runCredits() {
3323
+ let remote = await fetchUsageSummary();
3324
+ if (!remote) {
3325
+ const v = await validateKey({ force: true });
3326
+ if (v?.valid) {
3327
+ remote = usageSummaryFromValidate(v);
3328
+ }
3329
+ }
3330
+ if (remote) {
3331
+ const used2 = remote.mutationsUsed;
3332
+ const cap2 = remote.monthlyMutationLimit;
3333
+ const remaining2 = remote.mutationsRemaining;
3334
+ const pct2 = cap2 > 0 && cap2 !== -1 ? Math.round(used2 / cap2 * 100) : 0;
3335
+ const lines2 = [];
3336
+ lines2.push("");
3337
+ lines2.push(chalk7.cyan.bold(" Pretest Credits") + " " + chalk7.green("(synced)"));
3338
+ lines2.push("");
3339
+ lines2.push(` Plan: ${chalk7.bold(remote.plan)}`);
3340
+ lines2.push(` Resets: ${new Date(remote.periodResetsAt).toLocaleDateString()}`);
3341
+ lines2.push("");
3342
+ lines2.push(` Mutations: ${progressBar(used2, cap2)} ${fmt3(used2)} / ${fmt3(cap2)}`);
3343
+ lines2.push(` Used: ${pct2}%`);
3344
+ lines2.push(` Remaining: ${fmt3(remaining2)}`);
3345
+ lines2.push("");
3346
+ if (remote.plan === "FREE" && cap2 !== -1 && used2 >= cap2 * 0.8) {
3347
+ lines2.push(chalk7.yellow(" Warning: Approaching free-tier limit. Run 'pretest upgrade' for unlimited."));
3348
+ lines2.push("");
3349
+ } else if (remote.plan === "FREE") {
3350
+ lines2.push(chalk7.gray(" Tip: Run 'pretest upgrade' to compare plans."));
3351
+ lines2.push("");
3352
+ }
3353
+ process.stdout.write(lines2.join("\n") + "\n");
3354
+ return;
3355
+ }
3356
+ const tier = detectTier();
3357
+ const limits = getPlanLimits(tier);
3358
+ const usage = loadUsage();
3359
+ const used = usage.mutations;
3360
+ const cap = limits.monthlyMutations;
3361
+ const remaining = Number.isFinite(cap) ? Math.max(0, cap - used) : Number.POSITIVE_INFINITY;
3362
+ const pct = Number.isFinite(cap) && cap > 0 ? Math.round(used / cap * 100) : 0;
3363
+ const lines = [];
3364
+ lines.push("");
3365
+ lines.push(chalk7.cyan.bold(" Pretest Credits") + " " + chalk7.yellow("(offline)"));
3366
+ lines.push("");
3367
+ lines.push(` Plan: ${chalk7.bold(PLAN_LABEL2[tier])}`);
3368
+ lines.push(` Month: ${usage.month}`);
3369
+ lines.push("");
3370
+ lines.push(` Mutations: ${progressBar(used, cap)} ${fmt3(used)} / ${fmt3(cap)}`);
3371
+ lines.push(` Used: ${pct}%`);
3372
+ lines.push(` Remaining: ${fmt3(remaining)}`);
3373
+ lines.push("");
3374
+ if (tier === "free" && Number.isFinite(cap) && used >= cap * 0.8) {
3375
+ lines.push(chalk7.yellow(" Warning: Approaching free-tier limit. Run 'pretest upgrade' for unlimited."));
3376
+ lines.push("");
3377
+ } else if (tier === "free") {
3378
+ lines.push(chalk7.gray(" Tip: Run 'pretest upgrade' to compare plans."));
3379
+ lines.push("");
3380
+ }
3381
+ process.stdout.write(lines.join("\n") + "\n");
3382
+ }
3383
+
3384
+ // src/commands/logout.ts
3385
+ import readline2 from "readline/promises";
3386
+ import chalk8 from "chalk";
3387
+ async function runLogout(opts = {}) {
3388
+ if (!loadApiKey()) {
3389
+ console.log(chalk8.gray("No saved dashboard API key in ~/.pretest/config.json."));
3390
+ console.log(
3391
+ chalk8.gray("If you use PRETEST_API_KEY in your shell, run unset PRETEST_API_KEY.")
3392
+ );
3393
+ return 0;
3394
+ }
3395
+ if (!opts.assumeYes) {
3396
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
3397
+ console.error(chalk8.red("Error: confirmation requires an interactive terminal."));
3398
+ console.error(chalk8.gray("Run `pretest logout --yes` to skip the prompt."));
3399
+ return 1;
3400
+ }
3401
+ const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
3402
+ try {
3403
+ const raw = await rl.question(
3404
+ chalk8.bold("Log out and remove your saved API key? Type y for yes, n for no: ")
3405
+ );
3406
+ const answer = raw.trim().toLowerCase();
3407
+ if (answer !== "y" && answer !== "yes") {
3408
+ console.log(chalk8.yellow("Logout cancelled."));
3409
+ return 0;
3410
+ }
3411
+ } finally {
3412
+ rl.close();
3413
+ }
3414
+ }
3415
+ const hadEnvDashboardKey = hasDashboardApiKeyInEnv();
3416
+ const { removed, path } = removeSavedDashboardCredentials();
3417
+ clearValidationCache();
3418
+ clearDashboardApiKeyFromProcess();
3419
+ if (removed) {
3420
+ console.log(chalk8.green("Logged out. Removed saved API key from"), chalk8.bold(path));
3421
+ console.log(
3422
+ chalk8.gray("Other terminals: run unset PRETEST_API_KEY if that variable is still exported (e.g. in ~/.zshrc).")
3423
+ );
3424
+ if (hadEnvDashboardKey) {
3425
+ console.log(
3426
+ chalk8.yellow(
3427
+ "This shell had a dashboard key in the environment; it was cleared for this process only. Restart any running pretest proxy (Ctrl+C, then pretest start)."
3428
+ )
3429
+ );
3430
+ }
3431
+ }
3432
+ return 0;
3433
+ }
3434
+
3435
+ // src/index.ts
3436
+ {
3437
+ const major = parseInt(process.versions.node.split(".")[0], 10);
3438
+ if (Number.isNaN(major) || major < 18) {
3439
+ process.stderr.write(
3440
+ `pretest requires Node.js 18 or newer (you have v${process.versions.node}).
3441
+ `
3442
+ );
3443
+ process.stderr.write(`Upgrade via nvm: nvm install 20 && nvm use 20
3444
+ `);
3445
+ process.exit(1);
3446
+ }
3447
+ }
3448
+ var VERSION = getCliSemver();
3449
+ var SCAN_CONCURRENCY = Math.max(2, os.cpus().length);
3450
+ var PROGRESS_INTERVAL_MS = 500;
3451
+ var SUPPORTED_LANGUAGES = ["typescript", "javascript", "python", "go", "java", "csharp", "ruby", "rust"];
3452
+ var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".java", ".cs", ".rb", ".rs"]);
3453
+ var SUPPORTED_CONFIG_EXTENSIONS = /* @__PURE__ */ new Set([
3454
+ ".json",
3455
+ ".yaml",
3456
+ ".yml",
3457
+ ".toml",
3458
+ ".env",
3459
+ ".cfg",
3460
+ ".ini",
3461
+ ".tf",
3462
+ ".tfstate",
3463
+ ".tfvars",
3464
+ ".hcl",
3465
+ ".properties",
3466
+ ".conf",
3467
+ ".xml",
3468
+ ".plist",
3469
+ ".pem",
3470
+ ".key",
3471
+ ".crt",
3472
+ ".pub",
3473
+ ".pfx",
3474
+ ".p12",
3475
+ ".htpasswd"
3476
+ ]);
3477
+ var SUPPORTED_FILENAMES = /* @__PURE__ */ new Set([
3478
+ ".env",
3479
+ ".env.local",
3480
+ ".env.production",
3481
+ ".env.development",
3482
+ ".env.staging",
3483
+ ".env.test",
3484
+ ".npmrc",
3485
+ ".netrc",
3486
+ ".pypirc",
3487
+ "Dockerfile",
3488
+ "docker-compose.yml",
3489
+ "docker-compose.yaml"
3490
+ ]);
3491
+ function isScannableFile(filePath) {
3492
+ const ext = extname(filePath).toLowerCase();
3493
+ return SUPPORTED_EXTENSIONS.has(ext) || SUPPORTED_CONFIG_EXTENSIONS.has(ext) || SUPPORTED_FILENAMES.has(basename(filePath));
3494
+ }
3495
+ function isSourceCodeFile(filePath) {
3496
+ return SUPPORTED_EXTENSIONS.has(extname(filePath).toLowerCase());
3497
+ }
3498
+ function langFromExt(filePath) {
3499
+ const ext = extname(filePath).toLowerCase();
3500
+ const map = {
3501
+ ".ts": "typescript",
3502
+ ".tsx": "typescript",
3503
+ ".js": "javascript",
3504
+ ".jsx": "javascript",
3505
+ ".mjs": "javascript",
3506
+ ".cjs": "javascript",
3507
+ ".py": "python",
3508
+ ".go": "go",
3509
+ ".java": "java",
3510
+ ".cs": "csharp",
3511
+ ".rb": "ruby",
3512
+ ".rs": "rust"
3513
+ };
3514
+ return map[ext] ?? "typescript";
3515
+ }
3516
+ function validatePort(value) {
3517
+ const port = parseInt(value, 10);
3518
+ if (isNaN(port) || port.toString() !== value.trim()) return null;
3519
+ if (port < 1 || port > 65535) return null;
3520
+ return port;
3521
+ }
3522
+ function isBinaryFile(filePath) {
3523
+ let fd;
3524
+ try {
3525
+ fd = openSync(filePath, "r");
3526
+ const buf = Buffer.alloc(8192);
3527
+ const bytesRead = readSync2(fd, buf, 0, 8192, 0);
3528
+ for (let i = 0; i < bytesRead; i++) {
3529
+ if (buf[i] === 0) return true;
3530
+ }
3531
+ return false;
3532
+ } catch {
3533
+ return false;
3534
+ } finally {
3535
+ if (fd !== void 0) closeSync(fd);
3536
+ }
3537
+ }
3538
+ async function isBinaryFileAsync(filePath) {
3539
+ let handle;
3540
+ try {
3541
+ handle = await fsOpen(filePath, "r");
3542
+ const buf = Buffer.alloc(8192);
3543
+ const { bytesRead } = await handle.read(buf, 0, 8192, 0);
3544
+ for (let i = 0; i < bytesRead; i++) {
3545
+ if (buf[i] === 0) return true;
3546
+ }
3547
+ return false;
3548
+ } catch {
3549
+ return false;
3550
+ } finally {
3551
+ if (handle) await handle.close().catch(() => {
3552
+ });
3553
+ }
3554
+ }
3555
+ function validateLanguage(lang) {
3556
+ const normalized = lang.toLowerCase();
3557
+ const aliases = {
3558
+ ts: "typescript",
3559
+ tsx: "typescript",
3560
+ js: "javascript",
3561
+ jsx: "javascript",
3562
+ py: "python",
3563
+ cs: "csharp",
3564
+ rb: "ruby",
3565
+ rs: "rust"
3566
+ };
3567
+ const resolved = aliases[normalized] ?? normalized;
3568
+ return SUPPORTED_LANGUAGES.includes(resolved) ? resolved : null;
3569
+ }
3570
+ function collectFiles(target) {
3571
+ const abs = resolve2(target);
3572
+ if (!existsSync8(abs)) {
3573
+ return [];
3574
+ }
3575
+ const stat = statSync(abs);
3576
+ if (stat.isFile()) return [abs];
3577
+ const files = [];
3578
+ function walk(dir) {
3579
+ let entries;
3580
+ try {
3581
+ entries = readdirSync(dir, { withFileTypes: true });
3582
+ } catch {
3583
+ return;
3584
+ }
3585
+ const skipDirs = /* @__PURE__ */ new Set([
3586
+ "node_modules",
3587
+ ".git",
3588
+ "dist",
3589
+ "build",
3590
+ ".pretest",
3591
+ ".pretense",
3592
+ ".next",
3593
+ "__pycache__",
3594
+ ".Trash"
3595
+ ]);
3596
+ for (const entry of entries) {
3597
+ const fullPath = join6(dir, entry.name);
3598
+ if (entry.isDirectory()) {
3599
+ if (skipDirs.has(entry.name)) continue;
3600
+ walk(fullPath);
3601
+ } else if (entry.isFile() && isScannableFile(entry.name)) {
3602
+ files.push(fullPath);
3603
+ }
3604
+ }
3605
+ }
3606
+ walk(abs);
3607
+ return files;
3608
+ }
3609
+ var program = new Command();
3610
+ program.name("pretest").description("AI firewall CLI \u2014 mutates proprietary code before LLM API calls").version(VERSION).hook("preAction", () => {
3611
+ printBanner();
3612
+ });
3613
+ program.command("init").description("Initialize .pretest/ config in current directory").option("-d, --dir <path>", "Target directory", process.cwd()).action((opts) => {
3614
+ const dir = resolve2(opts.dir);
3615
+ console.log(chalk9.cyan("Initializing Pretest in"), chalk9.bold(dir));
3616
+ const configDir = initConfig(dir);
3617
+ const files = collectFiles(dir);
3618
+ const langCounts = {};
3619
+ for (const f of files) {
3620
+ const lang = langFromExt(f);
3621
+ langCounts[lang] = (langCounts[lang] ?? 0) + 1;
3622
+ }
3623
+ console.log(chalk9.green("\n .pretest/ created at"), chalk9.bold(configDir));
3624
+ console.log(chalk9.gray(`
3625
+ Scanned ${files.length} source files:`));
3626
+ for (const [lang, count] of Object.entries(langCounts)) {
3627
+ console.log(chalk9.gray(` ${lang}: ${count} files`));
3628
+ }
3629
+ console.log(chalk9.cyan("\n Next: run"), chalk9.bold("pretest start"), chalk9.cyan("to launch the proxy"));
3630
+ console.log();
3631
+ });
3632
+ program.command("start").description("Start the Pretest proxy server").option("-p, --port <number>", "Port number", "9339").option("-v, --verbose", "Enable verbose logging", false).action(async (opts) => {
3633
+ const config = loadConfig();
3634
+ const port = validatePort(opts.port);
3635
+ if (port === null) {
3636
+ console.error(chalk9.red("Error: Invalid port number:"), chalk9.bold(opts.port));
3637
+ console.error(chalk9.gray("Port must be an integer between 1 and 65535."));
3638
+ process.exit(1);
3639
+ }
3640
+ await ensureDashboardKeyForStart();
3641
+ logDashboardCredentialHint();
3642
+ startProxy({ config, port, verbose: opts.verbose });
3643
+ });
3644
+ async function scanOneFile(file, opts) {
3645
+ let size = 0;
3646
+ try {
3647
+ const st = await fsStat(file);
3648
+ size = st.size;
3649
+ if (st.size > opts.maxFileSize) {
3650
+ return { file, identifiers: 0, secrets: 0, secretTypes: [], skipped: "too-large", sizeBytes: size };
3651
+ }
3652
+ } catch {
3653
+ return { file, identifiers: 0, secrets: 0, secretTypes: [], skipped: "read-error" };
3654
+ }
3655
+ if (!isSourceCodeFile(file)) {
3656
+ try {
3657
+ if (await isBinaryFileAsync(file)) {
3658
+ return { file, identifiers: 0, secrets: 0, secretTypes: [], skipped: "binary", sizeBytes: size };
3659
+ }
3660
+ } catch {
3661
+ }
3662
+ }
3663
+ let code;
3664
+ try {
3665
+ code = await readFile(file, "utf-8");
3666
+ } catch {
3667
+ return { file, identifiers: 0, secrets: 0, secretTypes: [], skipped: "read-error", sizeBytes: size };
3668
+ }
3669
+ const lang = langFromExt(file);
3670
+ let identifiers = 0;
3671
+ if (!opts.secretsOnly && isSourceCodeFile(file)) {
3672
+ try {
3673
+ const scanResult = scan(code, lang);
3674
+ identifiers = scanResult.tokens.length;
3675
+ } catch {
3676
+ }
3677
+ }
3678
+ let blocked = [];
3679
+ try {
3680
+ const secretResult = scan2(code);
3681
+ blocked = secretResult.matches.filter((m) => m.action === "block" || m.action === "redact");
3682
+ } catch {
3683
+ }
3684
+ return {
3685
+ file,
3686
+ identifiers,
3687
+ secrets: blocked.length,
3688
+ secretTypes: blocked.map((m) => m.type),
3689
+ sizeBytes: size
3690
+ };
3691
+ }
3692
+ async function scanFilesParallel(files, opts) {
3693
+ const results = [];
3694
+ let cursor = 0;
3695
+ let done = 0;
3696
+ let lastPrint = 0;
3697
+ let interrupted = false;
3698
+ const stopHandler = () => {
3699
+ interrupted = true;
3700
+ };
3701
+ process.on("SIGINT", stopHandler);
3702
+ process.on("SIGTERM", stopHandler);
3703
+ const isTTY = process.stderr.isTTY === true;
3704
+ const printProgress = (force = false) => {
3705
+ if (opts.quiet) return;
3706
+ const now = Date.now();
3707
+ if (!force && now - lastPrint < PROGRESS_INTERVAL_MS) return;
3708
+ lastPrint = now;
3709
+ const found = results.reduce((acc, r) => acc + r.secrets, 0);
3710
+ const msg = `Scanning ${done}/${files.length} files (${found} secrets so far)...`;
3711
+ if (isTTY) {
3712
+ process.stderr.write(`\r\x1B[K${msg}`);
3713
+ } else {
3714
+ process.stderr.write(`${msg}
3715
+ `);
3716
+ }
3717
+ };
3718
+ async function worker() {
3719
+ while (!interrupted) {
3720
+ const i = cursor++;
3721
+ if (i >= files.length) return;
3722
+ const file = files[i];
3723
+ const row = await scanOneFile(file, { secretsOnly: opts.secretsOnly, maxFileSize: opts.maxFileSize });
3724
+ if (row) results.push(row);
3725
+ done++;
3726
+ printProgress();
3727
+ }
3728
+ }
3729
+ printProgress(true);
3730
+ const workers = Array.from({ length: Math.min(opts.concurrency, files.length || 1) }, () => worker());
3731
+ await Promise.all(workers);
3732
+ process.removeListener("SIGINT", stopHandler);
3733
+ process.removeListener("SIGTERM", stopHandler);
3734
+ if (!opts.quiet && isTTY) {
3735
+ process.stderr.write(`\r\x1B[K`);
3736
+ }
3737
+ return { results, interrupted };
3738
+ }
3739
+ function parseSizeOption(value) {
3740
+ const trimmed = value.trim().toLowerCase();
3741
+ const m = /^(\d+(?:\.\d+)?)\s*(b|k|kb|m|mb|g|gb)?$/.exec(trimmed);
3742
+ if (!m) return null;
3743
+ const n = parseFloat(m[1]);
3744
+ if (!isFinite(n) || n <= 0) return null;
3745
+ const unit = m[2] ?? "b";
3746
+ const mult = unit.startsWith("g") ? 1024 ** 3 : unit.startsWith("m") ? 1024 ** 2 : unit.startsWith("k") ? 1024 : 1;
3747
+ return Math.floor(n * mult);
3748
+ }
3749
+ 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) => {
3750
+ const absTarget = resolve2(target);
3751
+ if (!existsSync8(absTarget)) {
3752
+ console.error(chalk9.red("Error: Path not found:"), chalk9.bold(absTarget));
3753
+ process.exit(1);
3754
+ }
3755
+ const maxFileSize = parseSizeOption(opts.maxFileSize);
3756
+ if (maxFileSize === null) {
3757
+ console.error(chalk9.red("Error: Invalid --max-file-size value:"), chalk9.bold(opts.maxFileSize));
3758
+ console.error(chalk9.gray("Use bytes or a unit suffix, e.g. 10mb, 500k, 1048576"));
3759
+ process.exit(1);
3760
+ }
3761
+ const concurrency = Math.max(1, parseInt(opts.concurrency, 10) || SCAN_CONCURRENCY);
3762
+ if (!opts.quiet && !opts.json) {
3763
+ process.stderr.write(chalk9.gray(`Discovering files in ${absTarget}...
3764
+ `));
3765
+ }
3766
+ const files = collectFiles(target);
3767
+ if (files.length === 0) {
3768
+ console.error(chalk9.red("Error: No source files found at"), chalk9.bold(absTarget));
3769
+ console.error(chalk9.gray("Supported extensions: " + [...SUPPORTED_EXTENSIONS].join(", ")));
3770
+ process.exit(1);
3771
+ }
3772
+ if (!opts.quiet && !opts.json) {
3773
+ process.stderr.write(chalk9.gray(`Found ${files.length} candidate files. Scanning with concurrency=${concurrency}...
3774
+ `));
3775
+ }
3776
+ const startedAt = Date.now();
3777
+ const { results: allResults, interrupted } = await scanFilesParallel(files, {
3778
+ secretsOnly: opts.secretsOnly,
3779
+ maxFileSize,
3780
+ quiet: opts.quiet || opts.json,
3781
+ concurrency
3782
+ });
3783
+ const elapsedMs = Date.now() - startedAt;
3784
+ const totalIdentifiers = allResults.reduce((acc, r) => acc + r.identifiers, 0);
3785
+ const totalSecrets = allResults.reduce((acc, r) => acc + r.secrets, 0);
3786
+ const skippedLarge = allResults.filter((r) => r.skipped === "too-large").length;
3787
+ const skippedBinary = allResults.filter((r) => r.skipped === "binary").length;
3788
+ if (opts.json) {
3789
+ console.log(JSON.stringify({
3790
+ files: allResults,
3791
+ totalIdentifiers,
3792
+ totalSecrets,
3793
+ skippedLarge,
3794
+ skippedBinary,
3795
+ elapsedMs,
3796
+ interrupted
3797
+ }, null, 2));
3798
+ } else {
3799
+ console.log(chalk9.cyan("\nPretest Scan Results\n"));
3800
+ for (const r of allResults) {
3801
+ if (r.skipped) {
3802
+ if (r.skipped === "too-large") {
3803
+ const mb = ((r.sizeBytes ?? 0) / 1024 / 1024).toFixed(1);
3804
+ console.log(chalk9.gray(` ${r.file} \u2014 skipped (${mb} MB > limit)`));
3805
+ }
3806
+ continue;
3807
+ }
3808
+ const secretBadge = r.secrets > 0 ? chalk9.red(` [${r.secrets} SECRET${r.secrets > 1 ? "S" : ""}]`) : "";
3809
+ const showRow = r.secrets > 0 || !opts.secretsOnly && r.identifiers > 0;
3810
+ if (!showRow) continue;
3811
+ console.log(
3812
+ chalk9.gray(" ") + chalk9.white(r.file) + chalk9.gray(` \u2014 ${r.identifiers} identifiers`) + secretBadge
3813
+ );
3814
+ if (r.secrets > 0) {
3815
+ for (const t of r.secretTypes) {
3816
+ console.log(chalk9.red(` ! ${t}`));
3817
+ }
3818
+ }
3819
+ }
3820
+ const skipNote = skippedLarge + skippedBinary > 0 ? chalk9.gray(` (skipped ${skippedLarge} large, ${skippedBinary} binary)`) : "";
3821
+ console.log(
3822
+ chalk9.gray(`
3823
+ Total: ${allResults.length} files in ${(elapsedMs / 1e3).toFixed(2)}s, ${totalIdentifiers} identifiers, `) + (totalSecrets > 0 ? chalk9.red(`${totalSecrets} secrets found`) : chalk9.green("0 secrets")) + skipNote
3824
+ );
3825
+ if (interrupted) {
3826
+ console.log(chalk9.yellow("\n Interrupted \u2014 partial results shown above."));
3827
+ }
3828
+ console.log();
3829
+ }
3830
+ if (!opts.json) {
3831
+ printTokenFooter(allResults.length, totalIdentifiers);
3832
+ }
3833
+ if (interrupted) {
3834
+ process.exit(130);
3835
+ }
3836
+ if (totalSecrets > 0) {
3837
+ process.exit(2);
3838
+ }
3839
+ });
3840
+ program.command("mutate <file>").description("Mutate identifiers for stdout (scanner redacts secrets/PII in the source first)").option("-s, --seed <seed>", "Mutation seed", "pretest").option("-l, --language <lang>", "Source language (typescript, javascript, python, go, java, csharp, ruby, rust)").option("--save", "Save mutation map to .pretest/", false).option("--json", "Output as JSON (includes map + stats)", false).action((file, opts) => {
3841
+ const absPath = resolve2(file);
3842
+ if (!existsSync8(absPath)) {
3843
+ console.error(chalk9.red("Error: File not found:"), chalk9.bold(absPath));
3844
+ process.exit(1);
3845
+ }
3846
+ if (opts.language) {
3847
+ const validLang = validateLanguage(opts.language);
3848
+ if (!validLang) {
3849
+ console.error(chalk9.red("Error: Unsupported language:"), chalk9.bold(opts.language));
3850
+ console.error(chalk9.gray("Supported languages: " + SUPPORTED_LANGUAGES.join(", ")));
3851
+ process.exit(1);
3852
+ }
3853
+ }
3854
+ if (!SUPPORTED_EXTENSIONS.has(extname(absPath).toLowerCase()) && isBinaryFile(absPath)) {
3855
+ console.error(chalk9.red("Error: Not a supported source file:"), chalk9.bold(absPath));
3856
+ console.error(chalk9.gray("Supported extensions: " + [...SUPPORTED_EXTENSIONS].join(", ")));
3857
+ process.exit(1);
3858
+ }
3859
+ const code = readFileSync7(absPath, "utf-8");
3860
+ const lang = opts.language ? validateLanguage(opts.language) : langFromExt(absPath);
3861
+ const secretScan = scan2(code);
3862
+ const effectiveSeed = effectiveSeedForMutation(opts.seed);
3863
+ const { redactedCode: redacted, secretsMap } = applyRedactionsTracked(code, secretScan.matches, effectiveSeed);
3864
+ const secretsRedacted = secretsMap.size;
3865
+ const result = mutate(redacted, lang, opts.seed);
3866
+ if (opts.save) {
3867
+ const configDir = getConfigDir();
3868
+ const store = new MutationStore(join6(configDir, "mutation-map.json"));
3869
+ store.load();
3870
+ store.save({
3871
+ id: absPath,
3872
+ originalHash: absPath,
3873
+ mapEntries: MutationStore.fromMap(result.map),
3874
+ secretsMapEntries: [...secretsMap.entries()],
3875
+ timestamp: Date.now(),
3876
+ language: lang
3877
+ });
3878
+ store.persist();
3879
+ process.stderr.write(chalk9.gray(`Mutation map saved to ${configDir}/mutation-map.json
3880
+ `));
3881
+ }
3882
+ if (opts.json) {
3883
+ console.log(JSON.stringify({
3884
+ mutatedCode: result.mutatedCode,
3885
+ map: Object.fromEntries(result.map),
3886
+ stats: result.stats,
3887
+ secretsRedacted,
3888
+ secretsMap: Object.fromEntries(secretsMap)
3889
+ }, null, 2));
3890
+ } else {
3891
+ process.stdout.write(result.mutatedCode);
3892
+ if (secretsRedacted > 0) {
3893
+ process.stderr.write(
3894
+ chalk9.gray(`--- ${secretsRedacted} secret/PII span(s) redacted before identifier mutation ---
3895
+ `)
3896
+ );
3897
+ }
3898
+ process.stderr.write(chalk9.gray(`
3899
+ --- ${result.stats.tokensMutated} identifiers mutated in ${result.stats.durationMs}ms ---
3900
+ `));
3901
+ printTokenFooter(1, result.stats.tokensMutated);
3902
+ }
3903
+ });
3904
+ program.command("reverse <file>").description("Reverse mutations using stored map").option("-m, --map <path>", "Path to mutation map JSON file").action((file, opts) => {
3905
+ const absPath = resolve2(file);
3906
+ if (!existsSync8(absPath)) {
3907
+ console.error(chalk9.red("Error: File not found:"), chalk9.bold(absPath));
3908
+ process.exit(1);
3909
+ }
3910
+ const mutatedCode = readFileSync7(absPath, "utf-8");
3911
+ const mapPath = opts.map ? resolve2(opts.map) : join6(getConfigDir(), "mutation-map.json");
3912
+ if (!existsSync8(mapPath)) {
3913
+ console.error(chalk9.red("Error: Mutation map not found:"), chalk9.bold(mapPath));
3914
+ console.error(chalk9.gray("Run 'pretest mutate --save' first, or specify --map <path>"));
3915
+ process.exit(1);
3916
+ }
3917
+ let store;
3918
+ try {
3919
+ store = new MutationStore(mapPath);
3920
+ store.load();
3921
+ } catch {
3922
+ console.error(chalk9.red("Error: Failed to load mutation map:"), chalk9.bold(mapPath));
3923
+ console.error(chalk9.gray("The file may be corrupted. Run 'pretest mutate --save' to regenerate."));
3924
+ process.exit(1);
3925
+ }
3926
+ const entry = store.get(absPath) ?? store.getByHash(absPath) ?? store.list(1)[0];
3927
+ if (!entry) {
3928
+ console.error(chalk9.red("Error: No mutation map entries found."));
3929
+ console.error(chalk9.gray("Run 'pretest mutate --save <file>' first to generate a map."));
3930
+ process.exit(1);
3931
+ }
3932
+ const map = MutationStore.toMap(entry);
3933
+ const secretsMap = entry.secretsMapEntries ? new Map(entry.secretsMapEntries) : void 0;
3934
+ const restored = reverse(mutatedCode, map, secretsMap);
3935
+ process.stdout.write(restored);
3936
+ const secretsCount = secretsMap?.size ?? 0;
3937
+ const secretsSuffix = secretsCount > 0 ? ` + ${secretsCount} secret(s)` : "";
3938
+ process.stderr.write(chalk9.gray(`
3939
+ --- Reversed ${map.size} mutations${secretsSuffix} ---
3940
+ `));
3941
+ });
3942
+ program.command("audit").description("View the audit log").option("--json", "Output as JSON", false).option("--csv", "Output as CSV", false).option("-l, --limit <number>", "Limit entries", "50").action((opts) => {
3943
+ const format = opts.json ? "json" : opts.csv ? "csv" : "text";
3944
+ const limit = parseInt(opts.limit) || 50;
3945
+ const entries = readAuditLog({ limit, format });
3946
+ const output = formatAudit(entries, format);
3947
+ console.log(output);
3948
+ });
3949
+ program.command("version").description("Print Pretest CLI version").action(() => {
3950
+ console.log(`@blockyfy/stg-cli v${VERSION}`);
3951
+ });
3952
+ program.command("status").description("Show current plan, monthly quota, seat usage").action(async () => {
3953
+ await runStatus();
3954
+ });
3955
+ program.command("usage").description("Alias for `pretest status` \u2014 show monthly quota usage").action(async () => {
3956
+ await runStatus();
3957
+ });
3958
+ program.command("tokens").description("Alias for `pretest credits` \u2014 show remaining mutation budget").action(async () => {
3959
+ await runCredits();
3960
+ });
3961
+ program.command("upgrade").description("Compare plans and upgrade to Pro or Enterprise").action(() => {
3962
+ runUpgrade();
3963
+ });
3964
+ program.command("credits").description("Show remaining mutation/scan budget for the current month").action(async () => {
3965
+ await runCredits();
3966
+ });
3967
+ program.command("login").description("Save a dashboard API key to ~/.pretest/config.json for future CLI runs").option("-k, --key <key>", "API key (prtns_live_... or pk_live_...). If omitted, read from $PRETEST_API_KEY or stdin.").action((opts) => {
3968
+ const code = runLogin({ key: opts.key });
3969
+ if (code !== 0) process.exit(code);
3970
+ });
3971
+ program.command("logout").description("Remove the saved dashboard API key from ~/.pretest/config.json").option("-y, --yes", "Skip confirmation prompt", false).action(async (opts) => {
3972
+ const code = await runLogout({ assumeYes: opts.yes === true });
3973
+ if (code !== 0) process.exit(code);
3974
+ });
3975
+ function handleFatalError(err) {
3976
+ if (err instanceof Error && err.message) {
3977
+ console.error(chalk9.red("Error:"), err.message);
3978
+ } else {
3979
+ console.error(chalk9.red("An unexpected error occurred."));
3980
+ }
3981
+ process.exit(1);
3982
+ }
3983
+ process.on("uncaughtException", handleFatalError);
3984
+ process.on("unhandledRejection", handleFatalError);
3985
+ program.showSuggestionAfterError(true).showHelpAfterError("(use --help for available commands)");
3986
+ program.parse();
3987
+ //# sourceMappingURL=index.js.map