@blockyfy/stg-cli 0.4.1 → 0.4.3
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/README.md +57 -70
- package/dist/index.js +290 -197
- package/dist/index.js.map +1 -1
- package/package.json +56 -56
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { Command } from "commander";
|
|
|
5
5
|
import chalk9 from "chalk";
|
|
6
6
|
import {
|
|
7
7
|
readFileSync as readFileSync7,
|
|
8
|
-
existsSync as
|
|
8
|
+
existsSync as existsSync8,
|
|
9
9
|
readdirSync,
|
|
10
10
|
statSync,
|
|
11
11
|
openSync,
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
closeSync
|
|
14
14
|
} from "fs";
|
|
15
15
|
import { readFile, stat as fsStat, open as fsOpen } from "fs/promises";
|
|
16
|
-
import { resolve as resolve2, extname, join as
|
|
16
|
+
import { resolve as resolve2, extname, join as join6, basename } from "path";
|
|
17
17
|
import os from "os";
|
|
18
18
|
|
|
19
19
|
// src/types.ts
|
|
@@ -31,7 +31,7 @@ var DEFAULT_CONFIG = {
|
|
|
31
31
|
{ tokenType: "Property" /* Property */, prefix: "_prop" },
|
|
32
32
|
{ tokenType: "Method" /* Method */, prefix: "_fn" }
|
|
33
33
|
],
|
|
34
|
-
storePath: `${process.env["HOME"] ?? "/tmp"}/.
|
|
34
|
+
storePath: `${process.env["HOME"] ?? "/tmp"}/.pretest`
|
|
35
35
|
};
|
|
36
36
|
|
|
37
37
|
// src/scanner.ts
|
|
@@ -866,20 +866,50 @@ function scan(code, language) {
|
|
|
866
866
|
}
|
|
867
867
|
|
|
868
868
|
// src/salt.ts
|
|
869
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
870
|
-
import { join } from "path";
|
|
869
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync as existsSync2, renameSync as renameSync2 } from "fs";
|
|
870
|
+
import { join as join2 } from "path";
|
|
871
871
|
import { homedir } from "os";
|
|
872
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
|
|
873
891
|
var SALT_BYTES = 32;
|
|
874
892
|
function getSaltPath() {
|
|
875
|
-
const
|
|
876
|
-
|
|
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 };
|
|
877
907
|
}
|
|
878
908
|
var _cachedSalt = null;
|
|
879
909
|
function getMutationSalt() {
|
|
880
910
|
if (_cachedSalt) return _cachedSalt;
|
|
881
911
|
const { dir, path } = getSaltPath();
|
|
882
|
-
if (
|
|
912
|
+
if (existsSync2(path)) {
|
|
883
913
|
try {
|
|
884
914
|
const raw = readFileSync(path, "utf-8").trim();
|
|
885
915
|
if (/^[0-9a-f]{64}$/i.test(raw)) {
|
|
@@ -901,6 +931,12 @@ function getMutationSalt() {
|
|
|
901
931
|
function buildSaltedSeed(baseSeed) {
|
|
902
932
|
return `${baseSeed}:${getMutationSalt()}`;
|
|
903
933
|
}
|
|
934
|
+
function effectiveSeedForMutation(userSeed) {
|
|
935
|
+
if (userSeed === "pretense" || userSeed === "pretest") {
|
|
936
|
+
return buildSaltedSeed("pretense");
|
|
937
|
+
}
|
|
938
|
+
return userSeed;
|
|
939
|
+
}
|
|
904
940
|
|
|
905
941
|
// src/deterministic-id.ts
|
|
906
942
|
function hash32(str) {
|
|
@@ -944,7 +980,7 @@ function prefixForType(type) {
|
|
|
944
980
|
return "_tok";
|
|
945
981
|
}
|
|
946
982
|
}
|
|
947
|
-
function mutate(code, language, seed = "
|
|
983
|
+
function mutate(code, language, seed = "pretest") {
|
|
948
984
|
const startMs = performance.now();
|
|
949
985
|
const lang = language ?? detectLanguage(code);
|
|
950
986
|
if (!code || !code.trim()) {
|
|
@@ -955,7 +991,7 @@ function mutate(code, language, seed = "pretense") {
|
|
|
955
991
|
stats: { tokensScanned: 0, tokensMutated: 0, durationMs: 0, language: lang }
|
|
956
992
|
};
|
|
957
993
|
}
|
|
958
|
-
const effectiveSeed =
|
|
994
|
+
const effectiveSeed = effectiveSeedForMutation(seed);
|
|
959
995
|
const { tokens } = scan(code, lang);
|
|
960
996
|
const map = /* @__PURE__ */ new Map();
|
|
961
997
|
for (const token of tokens) {
|
|
@@ -1044,7 +1080,7 @@ function reverse(mutatedCode, map, secretsMap) {
|
|
|
1044
1080
|
// src/secrets.ts
|
|
1045
1081
|
var SECRET_PATTERNS = [
|
|
1046
1082
|
{ name: "anthropic-api-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /sk-ant-api\d{2}-[A-Za-z0-9_-]{40,}/g },
|
|
1047
|
-
/** Legacy `sk-…` and modern `sk-proj-…` (hyphenated body); aligned with
|
|
1083
|
+
/** Legacy `sk-…` and modern `sk-proj-…` (hyphenated body); aligned with upstream scanner heuristics */
|
|
1048
1084
|
{ name: "openai-api-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /sk-(?:proj-)?[A-Za-z0-9_-]{20,}/g },
|
|
1049
1085
|
{ name: "aws-access-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /AKIA[0-9A-Z]{16}/g },
|
|
1050
1086
|
/** `AWS_SECRET = '…40 chars…'` (not `AWS_SECRET_ACCESS_KEY`, which is covered below). */
|
|
@@ -1307,7 +1343,7 @@ function applyRedactionsTracked(text, matches, mutationSeed = buildSaltedSeed("p
|
|
|
1307
1343
|
}
|
|
1308
1344
|
|
|
1309
1345
|
// src/store.ts
|
|
1310
|
-
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as
|
|
1346
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync3 } from "fs";
|
|
1311
1347
|
import { dirname } from "path";
|
|
1312
1348
|
var MutationStore = class {
|
|
1313
1349
|
entries = /* @__PURE__ */ new Map();
|
|
@@ -1372,7 +1408,7 @@ var MutationStore = class {
|
|
|
1372
1408
|
*/
|
|
1373
1409
|
persist() {
|
|
1374
1410
|
const dir = dirname(this.filePath);
|
|
1375
|
-
if (!
|
|
1411
|
+
if (!existsSync3(dir)) {
|
|
1376
1412
|
mkdirSync2(dir, { recursive: true });
|
|
1377
1413
|
}
|
|
1378
1414
|
const data = JSON.stringify([...this.entries.values()], null, 2);
|
|
@@ -1383,7 +1419,7 @@ var MutationStore = class {
|
|
|
1383
1419
|
* No-ops if the file doesn't exist.
|
|
1384
1420
|
*/
|
|
1385
1421
|
load() {
|
|
1386
|
-
if (!
|
|
1422
|
+
if (!existsSync3(this.filePath)) return;
|
|
1387
1423
|
try {
|
|
1388
1424
|
const raw = readFileSync2(this.filePath, "utf-8");
|
|
1389
1425
|
const parsed = JSON.parse(raw);
|
|
@@ -1418,41 +1454,42 @@ var MutationStore = class {
|
|
|
1418
1454
|
};
|
|
1419
1455
|
|
|
1420
1456
|
// src/config.ts
|
|
1421
|
-
import { existsSync as
|
|
1422
|
-
import { join as
|
|
1457
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
1458
|
+
import { join as join3, resolve } from "path";
|
|
1423
1459
|
import { createHmac } from "crypto";
|
|
1424
|
-
var CONFIG_DIR_NAME =
|
|
1460
|
+
var CONFIG_DIR_NAME = PROJECT_CONFIG_DIR_NAME;
|
|
1425
1461
|
var CONFIG_FILE = "config.json";
|
|
1426
1462
|
var MUTATION_MAP_FILE = "mutation-map.json";
|
|
1427
1463
|
var AUDIT_LOG_FILE = "audit.log";
|
|
1428
1464
|
var USAGE_FILE = "usage.json";
|
|
1429
1465
|
function getConfigDir(baseDir) {
|
|
1430
1466
|
const base = baseDir ?? process.cwd();
|
|
1467
|
+
migrateLegacyProjectConfigDir(base);
|
|
1431
1468
|
return resolve(base, CONFIG_DIR_NAME);
|
|
1432
1469
|
}
|
|
1433
1470
|
function initConfig(dir) {
|
|
1434
1471
|
const configDir = getConfigDir(dir);
|
|
1435
|
-
if (!
|
|
1472
|
+
if (!existsSync4(configDir)) {
|
|
1436
1473
|
mkdirSync3(configDir, { recursive: true });
|
|
1437
1474
|
}
|
|
1438
|
-
const configPath =
|
|
1439
|
-
if (!
|
|
1475
|
+
const configPath = join3(configDir, CONFIG_FILE);
|
|
1476
|
+
if (!existsSync4(configPath)) {
|
|
1440
1477
|
const config = {
|
|
1441
1478
|
...DEFAULT_CONFIG,
|
|
1442
1479
|
storePath: configDir
|
|
1443
1480
|
};
|
|
1444
1481
|
writeFileSync3(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
1445
1482
|
}
|
|
1446
|
-
const mapPath =
|
|
1447
|
-
if (!
|
|
1483
|
+
const mapPath = join3(configDir, MUTATION_MAP_FILE);
|
|
1484
|
+
if (!existsSync4(mapPath)) {
|
|
1448
1485
|
writeFileSync3(mapPath, "[]", "utf-8");
|
|
1449
1486
|
}
|
|
1450
|
-
const auditPath =
|
|
1451
|
-
if (!
|
|
1487
|
+
const auditPath = join3(configDir, AUDIT_LOG_FILE);
|
|
1488
|
+
if (!existsSync4(auditPath)) {
|
|
1452
1489
|
writeFileSync3(auditPath, "", "utf-8");
|
|
1453
1490
|
}
|
|
1454
|
-
const usagePath =
|
|
1455
|
-
if (!
|
|
1491
|
+
const usagePath = join3(configDir, USAGE_FILE);
|
|
1492
|
+
if (!existsSync4(usagePath)) {
|
|
1456
1493
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1457
1494
|
const month = today.slice(0, 7);
|
|
1458
1495
|
const usageData = { month, mutations: 0, firstUseDate: today };
|
|
@@ -1466,8 +1503,8 @@ function initConfig(dir) {
|
|
|
1466
1503
|
}
|
|
1467
1504
|
function loadConfig(dir) {
|
|
1468
1505
|
const configDir = getConfigDir(dir);
|
|
1469
|
-
const configPath =
|
|
1470
|
-
if (!
|
|
1506
|
+
const configPath = join3(configDir, CONFIG_FILE);
|
|
1507
|
+
if (!existsSync4(configPath)) {
|
|
1471
1508
|
return { ...DEFAULT_CONFIG };
|
|
1472
1509
|
}
|
|
1473
1510
|
try {
|
|
@@ -1480,7 +1517,7 @@ function loadConfig(dir) {
|
|
|
1480
1517
|
}
|
|
1481
1518
|
var USAGE_HMAC_SECRET = "pretense_usage_integrity_v1";
|
|
1482
1519
|
function getUsageSecret() {
|
|
1483
|
-
return process.env["PRETENSE_USAGE_SECRET"]
|
|
1520
|
+
return process.env["PRETEST_USAGE_SECRET"]?.trim() || process.env["PRETENSE_USAGE_SECRET"]?.trim() || USAGE_HMAC_SECRET;
|
|
1484
1521
|
}
|
|
1485
1522
|
function computeUsageChecksum(data) {
|
|
1486
1523
|
const payload = JSON.stringify({ month: data.month, mutations: data.mutations, firstUseDate: data.firstUseDate });
|
|
@@ -1492,10 +1529,10 @@ function verifyUsageChecksum(data) {
|
|
|
1492
1529
|
}
|
|
1493
1530
|
function loadUsage(dir) {
|
|
1494
1531
|
const configDir = getConfigDir(dir);
|
|
1495
|
-
const usagePath =
|
|
1532
|
+
const usagePath = join3(configDir, USAGE_FILE);
|
|
1496
1533
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1497
1534
|
const currentMonth = today.slice(0, 7);
|
|
1498
|
-
if (!
|
|
1535
|
+
if (!existsSync4(usagePath)) {
|
|
1499
1536
|
return { month: currentMonth, mutations: 0, firstUseDate: today };
|
|
1500
1537
|
}
|
|
1501
1538
|
try {
|
|
@@ -1506,7 +1543,7 @@ function loadUsage(dir) {
|
|
|
1506
1543
|
return { month: currentMonth, mutations: 0, firstUseDate: data.firstUseDate ?? legacyDate };
|
|
1507
1544
|
}
|
|
1508
1545
|
if (!verifyUsageChecksum({ month: data.month, mutations: data.mutations, firstUseDate: data.firstUseDate, checksum: data.checksum })) {
|
|
1509
|
-
process.stderr.write("[
|
|
1546
|
+
process.stderr.write("[PRETEST] Warning: usage.json integrity check failed. Resetting usage counter.\n");
|
|
1510
1547
|
return { month: currentMonth, mutations: 0, firstUseDate: data.firstUseDate ?? today };
|
|
1511
1548
|
}
|
|
1512
1549
|
if (data.month !== currentMonth) {
|
|
@@ -1519,10 +1556,10 @@ function loadUsage(dir) {
|
|
|
1519
1556
|
}
|
|
1520
1557
|
function saveUsage(usage, dir) {
|
|
1521
1558
|
const configDir = getConfigDir(dir);
|
|
1522
|
-
if (!
|
|
1559
|
+
if (!existsSync4(configDir)) {
|
|
1523
1560
|
mkdirSync3(configDir, { recursive: true });
|
|
1524
1561
|
}
|
|
1525
|
-
const usagePath =
|
|
1562
|
+
const usagePath = join3(configDir, USAGE_FILE);
|
|
1526
1563
|
const checksum = computeUsageChecksum(usage);
|
|
1527
1564
|
writeFileSync3(usagePath, JSON.stringify({ ...usage, checksum }, null, 2), "utf-8");
|
|
1528
1565
|
}
|
|
@@ -1531,7 +1568,7 @@ var LICENSE_PAYLOAD_REGEX = /^[a-zA-Z0-9]+$/;
|
|
|
1531
1568
|
function isValidLicenseKey(key, prefix) {
|
|
1532
1569
|
if (key.length < MIN_LICENSE_KEY_LENGTH) return false;
|
|
1533
1570
|
const afterPrefix = key.slice(prefix.length);
|
|
1534
|
-
const hmacSecret = process.env["PRETENSE_LICENSE_SECRET"];
|
|
1571
|
+
const hmacSecret = process.env["PRETEST_LICENSE_SECRET"]?.trim() || process.env["PRETENSE_LICENSE_SECRET"]?.trim();
|
|
1535
1572
|
if (hmacSecret) {
|
|
1536
1573
|
const lastUnderscore = afterPrefix.lastIndexOf("_");
|
|
1537
1574
|
if (lastUnderscore === -1) return false;
|
|
@@ -1545,7 +1582,7 @@ function isValidLicenseKey(key, prefix) {
|
|
|
1545
1582
|
return LICENSE_PAYLOAD_REGEX.test(afterPrefix);
|
|
1546
1583
|
}
|
|
1547
1584
|
function detectTier() {
|
|
1548
|
-
const key = process.env["PRETENSE_LICENSE_KEY"]
|
|
1585
|
+
const key = process.env["PRETEST_LICENSE_KEY"]?.trim() || process.env["PRETENSE_LICENSE_KEY"]?.trim() || "";
|
|
1549
1586
|
if (key.startsWith("pre_ent_") && isValidLicenseKey(key, "pre_ent_")) return "enterprise";
|
|
1550
1587
|
if (key.startsWith("pre_pro_") && isValidLicenseKey(key, "pre_pro_")) return "pro";
|
|
1551
1588
|
return "free";
|
|
@@ -1568,13 +1605,13 @@ function getTierLimits(tier) {
|
|
|
1568
1605
|
}
|
|
1569
1606
|
|
|
1570
1607
|
// src/audit.ts
|
|
1571
|
-
import { appendFileSync, existsSync as
|
|
1572
|
-
import { join as
|
|
1608
|
+
import { appendFileSync, existsSync as existsSync5, readFileSync as readFileSync4, mkdirSync as mkdirSync4 } from "fs";
|
|
1609
|
+
import { join as join4, dirname as dirname2 } from "path";
|
|
1573
1610
|
function writeAuditEntry(entry, dir) {
|
|
1574
1611
|
const configDir = getConfigDir(dir);
|
|
1575
|
-
const auditPath =
|
|
1612
|
+
const auditPath = join4(configDir, "audit.log");
|
|
1576
1613
|
const logDir = dirname2(auditPath);
|
|
1577
|
-
if (!
|
|
1614
|
+
if (!existsSync5(logDir)) {
|
|
1578
1615
|
mkdirSync4(logDir, { recursive: true });
|
|
1579
1616
|
}
|
|
1580
1617
|
const line = JSON.stringify(entry) + "\n";
|
|
@@ -1594,8 +1631,8 @@ function createAuditEntry(sessionId, file, identifiersMutated, secretsBlocked, l
|
|
|
1594
1631
|
function readAuditLog(options = {}) {
|
|
1595
1632
|
const { limit, dir } = options;
|
|
1596
1633
|
const configDir = getConfigDir(dir);
|
|
1597
|
-
const auditPath =
|
|
1598
|
-
if (!
|
|
1634
|
+
const auditPath = join4(configDir, "audit.log");
|
|
1635
|
+
if (!existsSync5(auditPath)) {
|
|
1599
1636
|
return [];
|
|
1600
1637
|
}
|
|
1601
1638
|
const raw = readFileSync4(auditPath, "utf-8");
|
|
@@ -1662,7 +1699,7 @@ import { Hono } from "hono";
|
|
|
1662
1699
|
import { serve } from "@hono/node-server";
|
|
1663
1700
|
import { nanoid } from "nanoid";
|
|
1664
1701
|
import { createServer } from "net";
|
|
1665
|
-
import { existsSync as
|
|
1702
|
+
import { existsSync as existsSync7, watch } from "fs";
|
|
1666
1703
|
|
|
1667
1704
|
// src/auth.ts
|
|
1668
1705
|
import { createHash } from "crypto";
|
|
@@ -1782,18 +1819,55 @@ async function collectGitContextCached(cwd = process.cwd(), now = Date.now()) {
|
|
|
1782
1819
|
}
|
|
1783
1820
|
|
|
1784
1821
|
// src/commands/login.ts
|
|
1785
|
-
import { existsSync as
|
|
1786
|
-
import { join as
|
|
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";
|
|
1787
1824
|
import { homedir as homedir2 } from "os";
|
|
1788
1825
|
import readline from "readline/promises";
|
|
1789
1826
|
import chalk from "chalk";
|
|
1790
|
-
|
|
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;
|
|
1791
1863
|
var CONFIG_FILE2 = "config.json";
|
|
1792
1864
|
function getUserDashboardConfigDir() {
|
|
1793
|
-
|
|
1865
|
+
const home = homedir2();
|
|
1866
|
+
migrateLegacyProjectConfigDir(home);
|
|
1867
|
+
return join5(home, CONFIG_DIR_NAME2);
|
|
1794
1868
|
}
|
|
1795
1869
|
function getUserDashboardConfigPath() {
|
|
1796
|
-
return
|
|
1870
|
+
return join5(getUserDashboardConfigDir(), CONFIG_FILE2);
|
|
1797
1871
|
}
|
|
1798
1872
|
function isValidKeyShape(key) {
|
|
1799
1873
|
const trimmed = key.trim();
|
|
@@ -1826,29 +1900,30 @@ function maskKey(key) {
|
|
|
1826
1900
|
return `${trimmed.slice(0, 12)}${"\u2022".repeat(8)}${trimmed.slice(-4)}`;
|
|
1827
1901
|
}
|
|
1828
1902
|
function logDashboardCredentialHint() {
|
|
1829
|
-
const
|
|
1903
|
+
const envName = activeDashboardApiKeyEnvName();
|
|
1904
|
+
const env = envName ? process.env[envName]?.trim() ?? "" : "";
|
|
1830
1905
|
const envOk = env.length > 0 && isValidKeyShape(env);
|
|
1831
1906
|
const abs = getUserDashboardConfigPath();
|
|
1832
1907
|
const fk = loadApiKey();
|
|
1833
|
-
if (envOk) {
|
|
1908
|
+
if (envOk && envName) {
|
|
1834
1909
|
console.error(
|
|
1835
1910
|
chalk.gray(
|
|
1836
|
-
`[
|
|
1911
|
+
`[pretest] Dashboard key source: ${envName} in environment (${maskKey(env)}) \u2014 unset this variable after logout if you expect no key.`
|
|
1837
1912
|
)
|
|
1838
1913
|
);
|
|
1839
1914
|
} else if (fk && isValidKeyShape(fk)) {
|
|
1840
|
-
console.error(chalk.gray(`[
|
|
1915
|
+
console.error(chalk.gray(`[pretest] Dashboard key source: ${abs} (${maskKey(fk)})`));
|
|
1841
1916
|
} else {
|
|
1842
|
-
console.error(chalk.gray("[
|
|
1917
|
+
console.error(chalk.gray("[pretest] Dashboard key source: none configured."));
|
|
1843
1918
|
}
|
|
1844
1919
|
}
|
|
1845
1920
|
function saveApiKey(key, configDir = getUserDashboardConfigDir()) {
|
|
1846
|
-
if (!
|
|
1921
|
+
if (!existsSync6(configDir)) {
|
|
1847
1922
|
mkdirSync5(configDir, { recursive: true });
|
|
1848
1923
|
}
|
|
1849
|
-
const path =
|
|
1924
|
+
const path = join5(configDir, CONFIG_FILE2);
|
|
1850
1925
|
let existing = {};
|
|
1851
|
-
if (
|
|
1926
|
+
if (existsSync6(path)) {
|
|
1852
1927
|
try {
|
|
1853
1928
|
existing = JSON.parse(readFileSync5(path, "utf-8"));
|
|
1854
1929
|
} catch {
|
|
@@ -1868,8 +1943,8 @@ function saveApiKey(key, configDir = getUserDashboardConfigDir()) {
|
|
|
1868
1943
|
return path;
|
|
1869
1944
|
}
|
|
1870
1945
|
function removeSavedDashboardCredentials(configDir = getUserDashboardConfigDir()) {
|
|
1871
|
-
const path =
|
|
1872
|
-
if (!
|
|
1946
|
+
const path = join5(configDir, CONFIG_FILE2);
|
|
1947
|
+
if (!existsSync6(path)) return { removed: false, path };
|
|
1873
1948
|
let existing = {};
|
|
1874
1949
|
try {
|
|
1875
1950
|
existing = JSON.parse(readFileSync5(path, "utf-8"));
|
|
@@ -1889,8 +1964,8 @@ function removeSavedDashboardCredentials(configDir = getUserDashboardConfigDir()
|
|
|
1889
1964
|
return { removed: true, path };
|
|
1890
1965
|
}
|
|
1891
1966
|
function loadApiKey(configDir = getUserDashboardConfigDir()) {
|
|
1892
|
-
const path =
|
|
1893
|
-
if (!
|
|
1967
|
+
const path = join5(configDir, CONFIG_FILE2);
|
|
1968
|
+
if (!existsSync6(path)) return null;
|
|
1894
1969
|
try {
|
|
1895
1970
|
const raw = readFileSync5(path, "utf-8");
|
|
1896
1971
|
const parsed = JSON.parse(raw);
|
|
@@ -1903,7 +1978,7 @@ function loadApiKey(configDir = getUserDashboardConfigDir()) {
|
|
|
1903
1978
|
return null;
|
|
1904
1979
|
}
|
|
1905
1980
|
function resolveDashboardKeyCandidate() {
|
|
1906
|
-
const env = (
|
|
1981
|
+
const env = firstNonEmptyEnv(DASHBOARD_API_KEY_ENVS);
|
|
1907
1982
|
if (env.length > 0) {
|
|
1908
1983
|
if (!isValidKeyShape(env)) {
|
|
1909
1984
|
return { key: null, fromFile: false, badEnv: true };
|
|
@@ -1919,14 +1994,14 @@ function resolveDashboardKeyCandidate() {
|
|
|
1919
1994
|
function installDashboardKeyForCurrentProcess(key) {
|
|
1920
1995
|
const trimmed = key.trim();
|
|
1921
1996
|
const path = saveApiKey(trimmed);
|
|
1922
|
-
|
|
1997
|
+
assignDashboardApiKeyToProcess(trimmed);
|
|
1923
1998
|
return path;
|
|
1924
1999
|
}
|
|
1925
2000
|
async function promptDashboardApiKeyAfterFailure() {
|
|
1926
2001
|
if (!process.stdin.isTTY || !process.stdout.isTTY) return null;
|
|
1927
2002
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1928
2003
|
try {
|
|
1929
|
-
const line = (await rl.question(chalk.bold("
|
|
2004
|
+
const line = (await rl.question(chalk.bold("Dashboard API key: "))).trim();
|
|
1930
2005
|
return line.length > 0 ? line : null;
|
|
1931
2006
|
} finally {
|
|
1932
2007
|
rl.close();
|
|
@@ -1935,9 +2010,15 @@ async function promptDashboardApiKeyAfterFailure() {
|
|
|
1935
2010
|
async function ensureDashboardKeyForStart() {
|
|
1936
2011
|
const { key, fromFile, badEnv } = resolveDashboardKeyCandidate();
|
|
1937
2012
|
if (badEnv) {
|
|
1938
|
-
console.error(chalk.red("Error: PRETENSE_API_KEY is set but is not a valid Pretense dashboard API key shape."));
|
|
1939
2013
|
console.error(
|
|
1940
|
-
chalk.
|
|
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
|
+
)
|
|
1941
2022
|
);
|
|
1942
2023
|
process.exit(1);
|
|
1943
2024
|
}
|
|
@@ -1947,32 +2028,34 @@ async function ensureDashboardKeyForStart() {
|
|
|
1947
2028
|
if (fromFile) {
|
|
1948
2029
|
console.warn(
|
|
1949
2030
|
chalk.yellow(
|
|
1950
|
-
"Ignoring invalid `api_key` in ~/.
|
|
2031
|
+
"Ignoring invalid `api_key` in ~/.pretest/config.json (wrong shape). Enter a new key or run `pretest login --key ...`."
|
|
1951
2032
|
)
|
|
1952
2033
|
);
|
|
1953
2034
|
}
|
|
1954
2035
|
const stdinOk = process.stdin.isTTY;
|
|
1955
2036
|
const stdoutOk = process.stdout.isTTY;
|
|
1956
2037
|
if (!stdinOk || !stdoutOk) {
|
|
1957
|
-
console.error(chalk.red("Error:
|
|
2038
|
+
console.error(chalk.red("Error: Dashboard API key required to start the proxy."));
|
|
1958
2039
|
console.error(
|
|
1959
|
-
chalk.gray(
|
|
1960
|
-
"Save one with `pretense login --key prtns_live_...` or set PRETENSE_API_KEY."
|
|
1961
|
-
)
|
|
2040
|
+
chalk.gray("Save one with `pretest login --key prtns_live_...` or set PRETEST_API_KEY.")
|
|
1962
2041
|
);
|
|
1963
2042
|
process.exit(1);
|
|
1964
2043
|
}
|
|
1965
2044
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1966
2045
|
try {
|
|
1967
|
-
console.log(
|
|
1968
|
-
|
|
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();
|
|
1969
2052
|
if (!entered) {
|
|
1970
2053
|
console.error(chalk.red("No key entered; exiting."));
|
|
1971
2054
|
process.exit(1);
|
|
1972
2055
|
}
|
|
1973
2056
|
if (!isValidKeyShape(entered)) {
|
|
1974
|
-
console.error(chalk.red("That does not look like a
|
|
1975
|
-
console.error(chalk.gray(
|
|
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}`));
|
|
1976
2059
|
process.exit(1);
|
|
1977
2060
|
}
|
|
1978
2061
|
const path = installDashboardKeyForCurrentProcess(entered);
|
|
@@ -1983,29 +2066,29 @@ async function ensureDashboardKeyForStart() {
|
|
|
1983
2066
|
}
|
|
1984
2067
|
const persisted = resolveDashboardKeyCandidate();
|
|
1985
2068
|
if (!persisted.key || persisted.badEnv) {
|
|
1986
|
-
console.error(chalk.red("Error:
|
|
2069
|
+
console.error(chalk.red("Error: Dashboard API key could not be confirmed after login prompt."));
|
|
1987
2070
|
process.exit(1);
|
|
1988
2071
|
}
|
|
1989
2072
|
}
|
|
1990
2073
|
function runLogin(opts = {}) {
|
|
1991
|
-
const candidate = opts.key?.trim() || (
|
|
2074
|
+
const candidate = opts.key?.trim() || firstNonEmptyEnv(DASHBOARD_API_KEY_ENVS) || readStdinIfPiped();
|
|
1992
2075
|
if (!candidate) {
|
|
1993
2076
|
console.error(chalk.red("Error: no API key provided."));
|
|
1994
2077
|
console.error(
|
|
1995
|
-
chalk.gray("Try:
|
|
2078
|
+
chalk.gray("Try: pretest login --key pk_live_xxxxx")
|
|
1996
2079
|
);
|
|
1997
2080
|
console.error(
|
|
1998
|
-
chalk.gray(" or: echo $
|
|
2081
|
+
chalk.gray(" or: echo $PRETEST_API_KEY | pretest login")
|
|
1999
2082
|
);
|
|
2000
2083
|
return 1;
|
|
2001
2084
|
}
|
|
2002
2085
|
if (!isValidKeyShape(candidate)) {
|
|
2003
|
-
console.error(chalk.red("Error: that does not look like a
|
|
2086
|
+
console.error(chalk.red("Error: that does not look like a dashboard API key."));
|
|
2004
2087
|
console.error(
|
|
2005
2088
|
chalk.gray("Keys start with prtns_live_ (or pk_live_) and are at least 24 characters long.")
|
|
2006
2089
|
);
|
|
2007
2090
|
console.error(
|
|
2008
|
-
chalk.gray(
|
|
2091
|
+
chalk.gray(`See: ${PUBLISH_PACKAGE_URL}`)
|
|
2009
2092
|
);
|
|
2010
2093
|
return 1;
|
|
2011
2094
|
}
|
|
@@ -2030,10 +2113,10 @@ function usageSummaryFromValidate(v) {
|
|
|
2030
2113
|
keyStatus: "active"
|
|
2031
2114
|
};
|
|
2032
2115
|
}
|
|
2033
|
-
var DEFAULT_PRETENSE_API_URL = "https://api.
|
|
2116
|
+
var DEFAULT_PRETENSE_API_URL = "https://pretense-stg-api.nxtsen.com";
|
|
2034
2117
|
var DEFAULT_REQUEST_TIMEOUT_MS = 15e3;
|
|
2035
2118
|
function getConfiguredApiRoot(rootOverride) {
|
|
2036
|
-
const raw = (rootOverride
|
|
2119
|
+
const raw = (rootOverride?.trim() || firstNonEmptyEnv(DASHBOARD_API_URL_ENVS) || DEFAULT_PRETENSE_API_URL).trim();
|
|
2037
2120
|
if (!raw) {
|
|
2038
2121
|
return DEFAULT_PRETENSE_API_URL.replace(/\/$/, "");
|
|
2039
2122
|
}
|
|
@@ -2043,7 +2126,7 @@ function getConfiguredApiRoot(rootOverride) {
|
|
|
2043
2126
|
}
|
|
2044
2127
|
try {
|
|
2045
2128
|
const u = new URL(normalized);
|
|
2046
|
-
const origin = /^pretense\.ai$/i.test(u.hostname) ?
|
|
2129
|
+
const origin = /^pretense\.ai$/i.test(u.hostname) ? DEFAULT_PRETENSE_API_URL.replace(/\/$/, "") : u.origin;
|
|
2047
2130
|
const path = u.pathname === "/" ? "" : u.pathname.replace(/\/$/, "");
|
|
2048
2131
|
return `${origin}${path}`;
|
|
2049
2132
|
} catch {
|
|
@@ -2056,20 +2139,20 @@ function getCliApiUrl(pathWithinCli, rootOverride) {
|
|
|
2056
2139
|
return `${root}/api/cli/${tail}`;
|
|
2057
2140
|
}
|
|
2058
2141
|
function getApiRequestTimeoutMs() {
|
|
2059
|
-
const raw = process.env["PRETENSE_API_TIMEOUT_MS"]?.trim();
|
|
2142
|
+
const raw = process.env["PRETEST_API_TIMEOUT_MS"]?.trim() || process.env["PRETENSE_API_TIMEOUT_MS"]?.trim();
|
|
2060
2143
|
if (!raw || !/^\d+$/.test(raw)) return DEFAULT_REQUEST_TIMEOUT_MS;
|
|
2061
2144
|
const n = parseInt(raw, 10);
|
|
2062
2145
|
return Math.min(Math.max(n, 3e3), 12e4);
|
|
2063
2146
|
}
|
|
2064
2147
|
function getApiKey() {
|
|
2065
|
-
const fromEnv =
|
|
2148
|
+
const fromEnv = firstNonEmptyEnv(DASHBOARD_API_KEY_ENVS);
|
|
2066
2149
|
if (fromEnv) return fromEnv;
|
|
2067
2150
|
const fromFile = loadApiKey();
|
|
2068
2151
|
if (!fromFile) return null;
|
|
2069
2152
|
const t = fromFile.trim();
|
|
2070
2153
|
return t.length > 0 ? t : null;
|
|
2071
2154
|
}
|
|
2072
|
-
function
|
|
2155
|
+
function getDashboardApiKey() {
|
|
2073
2156
|
return getApiKey();
|
|
2074
2157
|
}
|
|
2075
2158
|
var cachedValidation = null;
|
|
@@ -2109,7 +2192,7 @@ async function validateKey(opts = {}) {
|
|
|
2109
2192
|
const apiKey = getApiKey();
|
|
2110
2193
|
if (!apiKey) {
|
|
2111
2194
|
if (enforceReachability) {
|
|
2112
|
-
const err = new Error("Missing
|
|
2195
|
+
const err = new Error("Missing dashboard API key");
|
|
2113
2196
|
err.code = "MISSING_API_KEY";
|
|
2114
2197
|
cachedValidation = null;
|
|
2115
2198
|
lastValidationSuccessAt = 0;
|
|
@@ -2139,7 +2222,7 @@ async function validateKey(opts = {}) {
|
|
|
2139
2222
|
throw err;
|
|
2140
2223
|
}
|
|
2141
2224
|
if (!resp.ok) {
|
|
2142
|
-
const msg = `
|
|
2225
|
+
const msg = `Dashboard API returned HTTP ${resp.status} from ${validateUrl}`;
|
|
2143
2226
|
if (enforceReachability) {
|
|
2144
2227
|
cachedValidation = null;
|
|
2145
2228
|
lastValidationSuccessAt = 0;
|
|
@@ -2176,14 +2259,14 @@ async function validateKey(opts = {}) {
|
|
|
2176
2259
|
throw err;
|
|
2177
2260
|
}
|
|
2178
2261
|
if (enforceReachability) {
|
|
2179
|
-
const msg = err.name === "AbortError" ? `Timed out after ${timeoutMs}ms reaching ${validateUrl} (raise PRETENSE_API_TIMEOUT_MS)` : `Cannot reach
|
|
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}`;
|
|
2180
2263
|
cachedValidation = null;
|
|
2181
2264
|
lastValidationSuccessAt = 0;
|
|
2182
2265
|
throw makeBackendReachabilityError("BACKEND_UNAVAILABLE", msg);
|
|
2183
2266
|
}
|
|
2184
2267
|
if (opts.verbose) {
|
|
2185
2268
|
process.stderr.write(
|
|
2186
|
-
`[
|
|
2269
|
+
`[PRETEST] Backend validation failed: ${err.message}
|
|
2187
2270
|
`
|
|
2188
2271
|
);
|
|
2189
2272
|
}
|
|
@@ -2209,7 +2292,7 @@ async function fetchUsageSummary(opts = {}) {
|
|
|
2209
2292
|
} catch (err) {
|
|
2210
2293
|
if (opts.verbose) {
|
|
2211
2294
|
process.stderr.write(
|
|
2212
|
-
`[
|
|
2295
|
+
`[PRETEST] Usage summary fetch failed: ${err.message}
|
|
2213
2296
|
`
|
|
2214
2297
|
);
|
|
2215
2298
|
}
|
|
@@ -2227,7 +2310,7 @@ async function isWithinLimits() {
|
|
|
2227
2310
|
if (check.mutationsRemaining !== -1 && check.mutationsRemaining <= 0) {
|
|
2228
2311
|
return {
|
|
2229
2312
|
allowed: false,
|
|
2230
|
-
reason: `Monthly mutation limit reached (${check.mutationsUsed}/${check.monthlyMutationLimit}).
|
|
2313
|
+
reason: `Monthly mutation limit reached (${check.mutationsUsed}/${check.monthlyMutationLimit}). See ${PUBLISH_PACKAGE_URL}`
|
|
2231
2314
|
};
|
|
2232
2315
|
}
|
|
2233
2316
|
return { allowed: true };
|
|
@@ -2262,13 +2345,13 @@ async function uploadLog(event, options) {
|
|
|
2262
2345
|
signal: controller.signal
|
|
2263
2346
|
});
|
|
2264
2347
|
if (!resp.ok && verbose) {
|
|
2265
|
-
process.stderr.write(`[
|
|
2348
|
+
process.stderr.write(`[PRETEST] log upload HTTP ${resp.status}
|
|
2266
2349
|
`);
|
|
2267
2350
|
}
|
|
2268
2351
|
} catch (err) {
|
|
2269
2352
|
if (verbose) {
|
|
2270
2353
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2271
|
-
process.stderr.write(`[
|
|
2354
|
+
process.stderr.write(`[PRETEST] log upload failed: ${msg}
|
|
2272
2355
|
`);
|
|
2273
2356
|
}
|
|
2274
2357
|
} finally {
|
|
@@ -2282,13 +2365,18 @@ function printStartClientInstructions(port) {
|
|
|
2282
2365
|
const host = `http://localhost:${port}`;
|
|
2283
2366
|
console.log();
|
|
2284
2367
|
console.log(chalk2.cyan.bold("\u2500\u2500 Client setup (environment variables) \u2500\u2500"));
|
|
2285
|
-
console.log(chalk2.gray("Point the CLI at the
|
|
2368
|
+
console.log(chalk2.gray("Point the CLI at the backend API (optional if using default):"));
|
|
2286
2369
|
console.log(
|
|
2287
|
-
chalk2.white(`export
|
|
2370
|
+
chalk2.white(`export PRETEST_API_URL='https://pretense-stg-api.nxtsen.com'`)
|
|
2288
2371
|
);
|
|
2289
2372
|
console.log();
|
|
2290
|
-
console.log(chalk2.gray("Log in with your
|
|
2291
|
-
console.log(chalk2.white("
|
|
2373
|
+
console.log(chalk2.gray("Log in with your dashboard API key:"));
|
|
2374
|
+
console.log(chalk2.white("pretest login --key your-dashboard-api-key"));
|
|
2375
|
+
console.log();
|
|
2376
|
+
console.log(
|
|
2377
|
+
chalk2.gray("Documentation:"),
|
|
2378
|
+
chalk2.cyan.underline(PUBLISH_PACKAGE_URL)
|
|
2379
|
+
);
|
|
2292
2380
|
console.log();
|
|
2293
2381
|
console.log(
|
|
2294
2382
|
chalk2.gray("Set the LLM base URL for your provider (proxy below):")
|
|
@@ -2305,15 +2393,15 @@ function printStartClientInstructions(port) {
|
|
|
2305
2393
|
console.log(chalk2.cyan.bold("\u2500\u2500 Stop & log out \u2500\u2500"));
|
|
2306
2394
|
console.log(
|
|
2307
2395
|
chalk2.gray(
|
|
2308
|
-
"Stop the proxy: press Ctrl+C in the terminal where
|
|
2396
|
+
"Stop the proxy: press Ctrl+C in the terminal where pretest start is running."
|
|
2309
2397
|
)
|
|
2310
2398
|
);
|
|
2311
2399
|
console.log(
|
|
2312
2400
|
chalk2.gray(
|
|
2313
|
-
"Remove the saved
|
|
2401
|
+
"Remove the saved dashboard key from ~/.pretest/config.json:"
|
|
2314
2402
|
)
|
|
2315
2403
|
);
|
|
2316
|
-
console.log(chalk2.white("
|
|
2404
|
+
console.log(chalk2.white("pretest logout"));
|
|
2317
2405
|
console.log(
|
|
2318
2406
|
chalk2.gray(
|
|
2319
2407
|
"(confirm with y / cancel with n.) Mutations stop immediately unless this proxy was started with"
|
|
@@ -2321,7 +2409,7 @@ function printStartClientInstructions(port) {
|
|
|
2321
2409
|
);
|
|
2322
2410
|
console.log(
|
|
2323
2411
|
chalk2.gray(
|
|
2324
|
-
"
|
|
2412
|
+
"PRETEST_API_KEY in that same terminal \u2014 then press Ctrl+C and run pretest start again."
|
|
2325
2413
|
)
|
|
2326
2414
|
);
|
|
2327
2415
|
console.log();
|
|
@@ -2354,11 +2442,11 @@ function attachDashboardLogoutWatcher(enabled) {
|
|
|
2354
2442
|
if (!fk) {
|
|
2355
2443
|
if (dashboardSavedCredentialsRevoked) return;
|
|
2356
2444
|
dashboardSavedCredentialsRevoked = true;
|
|
2357
|
-
|
|
2445
|
+
clearDashboardApiKeyFromProcess();
|
|
2358
2446
|
clearValidationCache();
|
|
2359
2447
|
try {
|
|
2360
2448
|
process.stderr.write(
|
|
2361
|
-
`\x1B[33m[
|
|
2449
|
+
`\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
|
|
2362
2450
|
`
|
|
2363
2451
|
);
|
|
2364
2452
|
} catch {
|
|
@@ -2375,7 +2463,7 @@ function attachDashboardLogoutWatcher(enabled) {
|
|
|
2375
2463
|
debounceTimer = setTimeout(applyFilesystemCredentialState, 120);
|
|
2376
2464
|
};
|
|
2377
2465
|
try {
|
|
2378
|
-
if (!
|
|
2466
|
+
if (!existsSync7(dir)) return;
|
|
2379
2467
|
watch(dir, () => {
|
|
2380
2468
|
schedule();
|
|
2381
2469
|
});
|
|
@@ -2484,8 +2572,8 @@ function daysSinceFirstUse() {
|
|
|
2484
2572
|
function printUpgradeNudge(reason) {
|
|
2485
2573
|
process.stdout.write(
|
|
2486
2574
|
`
|
|
2487
|
-
\x1B[33m[
|
|
2488
|
-
\x1B[33m[
|
|
2575
|
+
\x1B[33m[PRETEST] ${reason}\x1B[0m
|
|
2576
|
+
\x1B[33m[PRETEST] Upgrade to Pro: ${PUBLISH_PACKAGE_URL}\x1B[0m
|
|
2489
2577
|
|
|
2490
2578
|
`
|
|
2491
2579
|
);
|
|
@@ -2499,7 +2587,7 @@ function createProxy(opts = {}) {
|
|
|
2499
2587
|
app.get(
|
|
2500
2588
|
"/",
|
|
2501
2589
|
(c) => c.json({
|
|
2502
|
-
name: "
|
|
2590
|
+
name: "pretest",
|
|
2503
2591
|
version: getCliSemver(),
|
|
2504
2592
|
status: "running",
|
|
2505
2593
|
routes: {
|
|
@@ -2510,7 +2598,7 @@ function createProxy(opts = {}) {
|
|
|
2510
2598
|
"POST /*": "Proxy to upstream LLM API (Anthropic, OpenAI, Google auto-detected)"
|
|
2511
2599
|
},
|
|
2512
2600
|
languages: ["TypeScript", "JavaScript", "Python", "Go", "Java", "C#", "Ruby", "Rust"],
|
|
2513
|
-
docs:
|
|
2601
|
+
docs: PUBLISH_PACKAGE_URL
|
|
2514
2602
|
})
|
|
2515
2603
|
);
|
|
2516
2604
|
app.get(
|
|
@@ -2569,19 +2657,19 @@ function createProxy(opts = {}) {
|
|
|
2569
2657
|
{
|
|
2570
2658
|
error: {
|
|
2571
2659
|
type: "pretense_session_logged_out",
|
|
2572
|
-
message: "Saved
|
|
2660
|
+
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."
|
|
2573
2661
|
}
|
|
2574
2662
|
},
|
|
2575
2663
|
401
|
|
2576
2664
|
);
|
|
2577
2665
|
}
|
|
2578
|
-
const
|
|
2579
|
-
if (!
|
|
2666
|
+
const dashboardApiKey = getDashboardApiKey();
|
|
2667
|
+
if (!dashboardApiKey) {
|
|
2580
2668
|
return c.json(
|
|
2581
2669
|
{
|
|
2582
2670
|
error: {
|
|
2583
2671
|
type: "pretense_dashboard_key_required",
|
|
2584
|
-
message: "Configure your
|
|
2672
|
+
message: "Configure your dashboard API key: run `pretest login --key prtns_live_...` or set PRETEST_API_KEY."
|
|
2585
2673
|
}
|
|
2586
2674
|
},
|
|
2587
2675
|
401
|
|
@@ -2629,12 +2717,12 @@ function createProxy(opts = {}) {
|
|
|
2629
2717
|
);
|
|
2630
2718
|
const { monthlyMutations } = getTierLimits(tier);
|
|
2631
2719
|
const usage = loadUsage();
|
|
2632
|
-
if (tier === "free" && usage.mutations >= monthlyMutations) {
|
|
2720
|
+
if (!dashboardApiKey && tier === "free" && usage.mutations >= monthlyMutations) {
|
|
2633
2721
|
return c.json(
|
|
2634
2722
|
{
|
|
2635
2723
|
error: {
|
|
2636
2724
|
type: "pretense_rate_limit",
|
|
2637
|
-
message: `Monthly mutation limit reached (${monthlyMutations}/month). Upgrade to Pro for unlimited:
|
|
2725
|
+
message: `Monthly mutation limit reached (${monthlyMutations}/month). Upgrade to Pro for unlimited: ${PUBLISH_PACKAGE_URL}`
|
|
2638
2726
|
}
|
|
2639
2727
|
},
|
|
2640
2728
|
429
|
|
@@ -2658,7 +2746,7 @@ function createProxy(opts = {}) {
|
|
|
2658
2746
|
{
|
|
2659
2747
|
error: {
|
|
2660
2748
|
type: "pretense_forbidden",
|
|
2661
|
-
message: "This API key is read-only. Create a read_write key in the dashboard to use the
|
|
2749
|
+
message: "This API key is read-only. Create a read_write key in the dashboard to use the Pretest proxy."
|
|
2662
2750
|
}
|
|
2663
2751
|
},
|
|
2664
2752
|
403
|
|
@@ -2714,7 +2802,7 @@ function createProxy(opts = {}) {
|
|
|
2714
2802
|
usage.mutations += tokensMutated;
|
|
2715
2803
|
saveUsage(usage);
|
|
2716
2804
|
const remoteLogDisabled = process.env["PRETENSE_DISABLE_REMOTE_LOG"] === "1";
|
|
2717
|
-
if (
|
|
2805
|
+
if (dashboardApiKey && !remoteLogDisabled) {
|
|
2718
2806
|
void uploadLog(
|
|
2719
2807
|
{
|
|
2720
2808
|
file_count: 0,
|
|
@@ -2723,7 +2811,7 @@ function createProxy(opts = {}) {
|
|
|
2723
2811
|
llm_provider: provider,
|
|
2724
2812
|
cli_version: getCliSemver()
|
|
2725
2813
|
},
|
|
2726
|
-
{ apiKey:
|
|
2814
|
+
{ apiKey: dashboardApiKey, verbose }
|
|
2727
2815
|
);
|
|
2728
2816
|
bumpCachedUsageAfterLog(tokensMutated);
|
|
2729
2817
|
}
|
|
@@ -2732,7 +2820,7 @@ function createProxy(opts = {}) {
|
|
|
2732
2820
|
printUpgradeNudge(`${usage.mutations}/${monthlyMutations} monthly mutations used. Running low!`);
|
|
2733
2821
|
}
|
|
2734
2822
|
if (tier === "free" && daysSinceFirstUse() >= 7) {
|
|
2735
|
-
printUpgradeNudge("You've been using
|
|
2823
|
+
printUpgradeNudge("You've been using Pretest for 7+ days. Unlock Pro features: unlimited mutations, 90-day audit, Slack alerts.");
|
|
2736
2824
|
}
|
|
2737
2825
|
const entry = {
|
|
2738
2826
|
id: requestId,
|
|
@@ -2749,7 +2837,7 @@ function createProxy(opts = {}) {
|
|
|
2749
2837
|
createAuditEntry(requestId, "proxy-request", tokensMutated, blockedSecrets.length, provider, latencyMs)
|
|
2750
2838
|
);
|
|
2751
2839
|
if (verbose && tokensMutated > 0) {
|
|
2752
|
-
process.stdout.write(`[
|
|
2840
|
+
process.stdout.write(`[PRETEST] ${tokensMutated} tokens mutated for request ${requestId}
|
|
2753
2841
|
`);
|
|
2754
2842
|
}
|
|
2755
2843
|
const headers = {};
|
|
@@ -2876,12 +2964,12 @@ function startProxy(opts = {}) {
|
|
|
2876
2964
|
const requestedPort = opts.port ?? opts.config?.port ?? DEFAULT_CONFIG.port;
|
|
2877
2965
|
const app = createProxy(opts);
|
|
2878
2966
|
void (async () => {
|
|
2879
|
-
const
|
|
2880
|
-
const dashboardKey =
|
|
2967
|
+
const dashboardKeyFromEnvAtLaunch = hasDashboardApiKeyInEnv();
|
|
2968
|
+
const dashboardKey = getDashboardApiKey();
|
|
2881
2969
|
if (!dashboardKey || !isValidKeyShape(dashboardKey)) {
|
|
2882
2970
|
process.stderr.write(
|
|
2883
|
-
`\x1B[31m[
|
|
2884
|
-
\x1B[31m[
|
|
2971
|
+
`\x1B[31m[PRETEST] API key required or invalid shape. Run \`pretest login --key prtns_live_...\`\x1B[0m
|
|
2972
|
+
\x1B[31m[PRETEST] or set PRETEST_API_KEY. See ${PUBLISH_PACKAGE_URL}\x1B[0m
|
|
2885
2973
|
`
|
|
2886
2974
|
);
|
|
2887
2975
|
process.exit(1);
|
|
@@ -2896,7 +2984,7 @@ function startProxy(opts = {}) {
|
|
|
2896
2984
|
});
|
|
2897
2985
|
if (!validation) {
|
|
2898
2986
|
process.stderr.write(
|
|
2899
|
-
`\x1B[31m[
|
|
2987
|
+
`\x1B[31m[PRETEST] Startup validation failed (no API key or unreachable backend). Cannot start proxy.\x1B[0m
|
|
2900
2988
|
`
|
|
2901
2989
|
);
|
|
2902
2990
|
process.exit(1);
|
|
@@ -2904,7 +2992,7 @@ function startProxy(opts = {}) {
|
|
|
2904
2992
|
const plan = validation.plan ?? "FREE";
|
|
2905
2993
|
const remaining = validation.mutationsRemaining === -1 ? "unlimited" : String(validation.mutationsRemaining);
|
|
2906
2994
|
process.stdout.write(
|
|
2907
|
-
`\x1B[32m[
|
|
2995
|
+
`\x1B[32m[PRETEST] Key validated: ${plan} plan` + (validation.organizationName ? ` (${validation.organizationName})` : "") + ` \u2014 ${remaining} mutations remaining\x1B[0m
|
|
2908
2996
|
`
|
|
2909
2997
|
);
|
|
2910
2998
|
break;
|
|
@@ -2912,62 +3000,62 @@ function startProxy(opts = {}) {
|
|
|
2912
3000
|
const code = err.code;
|
|
2913
3001
|
const msg = err.message;
|
|
2914
3002
|
if (code === "BACKEND_UNAVAILABLE" || code === "AUTH_BACKEND_REJECTED") {
|
|
2915
|
-
process.stderr.write(`\x1B[31m[
|
|
3003
|
+
process.stderr.write(`\x1B[31m[PRETEST] ${msg}\x1B[0m
|
|
2916
3004
|
`);
|
|
2917
|
-
process.stderr.write(`\x1B[31m[
|
|
3005
|
+
process.stderr.write(`\x1B[31m[PRETEST] Fix network or set PRETEST_API_URL (or PRETENSE_API_URL) if using a custom API.\x1B[0m
|
|
2918
3006
|
`);
|
|
2919
3007
|
process.exit(1);
|
|
2920
3008
|
}
|
|
2921
3009
|
if (code === "ORG_INACTIVE") {
|
|
2922
|
-
process.stderr.write(`\x1B[31m[
|
|
3010
|
+
process.stderr.write(`\x1B[31m[PRETEST] ${msg}\x1B[0m
|
|
2923
3011
|
`);
|
|
2924
|
-
process.stderr.write(`\x1B[31m[
|
|
3012
|
+
process.stderr.write(`\x1B[31m[PRETEST] Your organization is inactive; contact support.\x1B[0m
|
|
2925
3013
|
`);
|
|
2926
3014
|
process.exit(1);
|
|
2927
3015
|
}
|
|
2928
3016
|
const authRetryable = code === "INVALID_API_KEY" || code === "KEY_REVOKED" || code === "KEY_EXPIRED" || code === "AUTH_ERROR" || code === "MISSING_API_KEY";
|
|
2929
3017
|
if (authRetryable && interactive) {
|
|
2930
|
-
process.stderr.write(`\x1B[31m[
|
|
3018
|
+
process.stderr.write(`\x1B[31m[PRETEST] ${msg}\x1B[0m
|
|
2931
3019
|
`);
|
|
2932
3020
|
process.stderr.write(
|
|
2933
|
-
`\x1B[33m[
|
|
3021
|
+
`\x1B[33m[PRETEST] Enter your dashboard API key and press Enter (Ctrl+C to quit).\x1B[0m
|
|
2934
3022
|
`
|
|
2935
3023
|
);
|
|
2936
3024
|
let entered = null;
|
|
2937
3025
|
for (; ; ) {
|
|
2938
3026
|
entered = await promptDashboardApiKeyAfterFailure();
|
|
2939
3027
|
if (!entered) {
|
|
2940
|
-
process.stderr.write(`\x1B[31m[
|
|
3028
|
+
process.stderr.write(`\x1B[31m[PRETEST] No key entered; exiting.\x1B[0m
|
|
2941
3029
|
`);
|
|
2942
3030
|
process.exit(1);
|
|
2943
3031
|
}
|
|
2944
3032
|
if (isValidKeyShape(entered)) break;
|
|
2945
3033
|
process.stderr.write(
|
|
2946
|
-
`\x1B[31m[
|
|
3034
|
+
`\x1B[31m[PRETEST] That does not look like a dashboard key (expect prtns_live_\u2026 or pk_live_\u2026).\x1B[0m
|
|
2947
3035
|
`
|
|
2948
3036
|
);
|
|
2949
3037
|
}
|
|
2950
3038
|
const savedPath = installDashboardKeyForCurrentProcess(entered);
|
|
2951
|
-
process.stdout.write(`\x1B[32m[
|
|
3039
|
+
process.stdout.write(`\x1B[32m[PRETEST] Saved API key to ${savedPath}\x1B[0m
|
|
2952
3040
|
`);
|
|
2953
3041
|
clearValidationCache();
|
|
2954
3042
|
continue;
|
|
2955
3043
|
}
|
|
2956
3044
|
if (code === "KEY_REVOKED" || code === "KEY_EXPIRED" || code === "INVALID_API_KEY" || code === "AUTH_ERROR" || code === "MISSING_API_KEY") {
|
|
2957
|
-
process.stderr.write(`\x1B[31m[
|
|
3045
|
+
process.stderr.write(`\x1B[31m[PRETEST] ${msg}\x1B[0m
|
|
2958
3046
|
`);
|
|
2959
|
-
process.stderr.write(`\x1B[31m[
|
|
3047
|
+
process.stderr.write(`\x1B[31m[PRETEST] Run 'pretest login --key <new-key>' to fix.\x1B[0m
|
|
2960
3048
|
`);
|
|
2961
3049
|
process.exit(1);
|
|
2962
3050
|
}
|
|
2963
|
-
process.stderr.write(`\x1B[31m[
|
|
3051
|
+
process.stderr.write(`\x1B[31m[PRETEST] ${msg}\x1B[0m
|
|
2964
3052
|
`);
|
|
2965
|
-
process.stderr.write(`\x1B[31m[
|
|
3053
|
+
process.stderr.write(`\x1B[31m[PRETEST] Run 'pretest login --key <new-key>' or check connectivity.\x1B[0m
|
|
2966
3054
|
`);
|
|
2967
3055
|
process.exit(1);
|
|
2968
3056
|
}
|
|
2969
3057
|
}
|
|
2970
|
-
attachDashboardLogoutWatcher(!
|
|
3058
|
+
attachDashboardLogoutWatcher(!dashboardKeyFromEnvAtLaunch);
|
|
2971
3059
|
const bannerTier = tierFromDashboardPlan(
|
|
2972
3060
|
getCachedValidation()?.plan,
|
|
2973
3061
|
detectTier()
|
|
@@ -2976,13 +3064,13 @@ function startProxy(opts = {}) {
|
|
|
2976
3064
|
try {
|
|
2977
3065
|
port = await findAvailablePort(requestedPort, 10);
|
|
2978
3066
|
} catch (err) {
|
|
2979
|
-
process.stderr.write(`\x1B[31m[
|
|
3067
|
+
process.stderr.write(`\x1B[31m[PRETEST] ${err.message}\x1B[0m
|
|
2980
3068
|
`);
|
|
2981
3069
|
process.exit(1);
|
|
2982
3070
|
}
|
|
2983
3071
|
if (port !== requestedPort) {
|
|
2984
3072
|
process.stderr.write(
|
|
2985
|
-
`\x1B[33m[
|
|
3073
|
+
`\x1B[33m[PRETEST] Port ${requestedPort} in use, falling back to ${port}.\x1B[0m
|
|
2986
3074
|
`
|
|
2987
3075
|
);
|
|
2988
3076
|
}
|
|
@@ -2990,7 +3078,7 @@ function startProxy(opts = {}) {
|
|
|
2990
3078
|
serve({ fetch: app.fetch, port }, () => {
|
|
2991
3079
|
const portStr = String(port);
|
|
2992
3080
|
process.stdout.write(`
|
|
2993
|
-
\x1B[
|
|
3081
|
+
\x1B[36mPretest v${getCliSemver()} [${bannerTier.toUpperCase()}]\x1B[0m
|
|
2994
3082
|
Your code hits AI naked. We fix that.
|
|
2995
3083
|
|
|
2996
3084
|
Proxy: http://localhost:${portStr}
|
|
@@ -3012,7 +3100,7 @@ function startProxy(opts = {}) {
|
|
|
3012
3100
|
`);
|
|
3013
3101
|
setInterval(() => {
|
|
3014
3102
|
if (dashboardSavedCredentialsRevoked) return;
|
|
3015
|
-
if (!
|
|
3103
|
+
if (!getDashboardApiKey()) return;
|
|
3016
3104
|
void validateKey({
|
|
3017
3105
|
force: true,
|
|
3018
3106
|
verbose: opts.verbose,
|
|
@@ -3068,13 +3156,13 @@ function printTokenFooter(filesScanned, mutationsMade) {
|
|
|
3068
3156
|
if (tier === "free") {
|
|
3069
3157
|
if (pct >= 80) {
|
|
3070
3158
|
process.stderr.write(
|
|
3071
|
-
chalk3.yellow(` \u26A0 Approaching free-tier limit. Upgrade: ${chalk3.bold("
|
|
3159
|
+
chalk3.yellow(` \u26A0 Approaching free-tier limit. Upgrade: ${chalk3.bold("pretest upgrade")}
|
|
3072
3160
|
|
|
3073
3161
|
`)
|
|
3074
3162
|
);
|
|
3075
3163
|
} else {
|
|
3076
3164
|
process.stderr.write(
|
|
3077
|
-
chalk3.gray(` Run ${chalk3.bold("
|
|
3165
|
+
chalk3.gray(` Run ${chalk3.bold("pretest status")} for details, ${chalk3.bold("pretest upgrade")} to remove limits.
|
|
3078
3166
|
|
|
3079
3167
|
`)
|
|
3080
3168
|
);
|
|
@@ -3086,19 +3174,19 @@ function printTokenFooter(filesScanned, mutationsMade) {
|
|
|
3086
3174
|
|
|
3087
3175
|
// src/banner.ts
|
|
3088
3176
|
import chalk4 from "chalk";
|
|
3089
|
-
var BANNER_RAW = `\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
|
|
3177
|
+
var BANNER_RAW = `\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 pretest
|
|
3090
3178
|
\u2588\u2588 \u2588\u2588
|
|
3091
3179
|
\u2588\u2588 \u2588\u2588 Stop your code from
|
|
3092
3180
|
\u2588\u2588 \u2588\u2588 leaking to any LLM.
|
|
3093
3181
|
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
|
|
3094
|
-
\u2588\u2588
|
|
3182
|
+
\u2588\u2588 @blockyfy/stg-cli
|
|
3095
3183
|
\u2588\u2588
|
|
3096
3184
|
\u2588\u2588
|
|
3097
3185
|
\u2588\u2588
|
|
3098
3186
|
\u2588\u2588`;
|
|
3099
3187
|
var ICE_BLUE = "#7DD3FC";
|
|
3100
3188
|
function shouldPrint() {
|
|
3101
|
-
if (process.env
|
|
3189
|
+
if (DASHBOARD_NO_BANNER_ENVS.some((n) => process.env[n] === "1")) return false;
|
|
3102
3190
|
if (!process.stdout.isTTY) return false;
|
|
3103
3191
|
if (process.env.CI) return false;
|
|
3104
3192
|
return true;
|
|
@@ -3143,7 +3231,7 @@ async function runStatus() {
|
|
|
3143
3231
|
const tierBadge2 = plan === "ENTERPRISE" ? chalk5.bgMagenta.white.bold(" ENTERPRISE ") : plan === "PRO" ? chalk5.bgBlue.white.bold(" PRO ") : chalk5.bgGray.white.bold(" FREE ");
|
|
3144
3232
|
const lines2 = [];
|
|
3145
3233
|
lines2.push("");
|
|
3146
|
-
lines2.push(`${chalk5.cyan("
|
|
3234
|
+
lines2.push(`${chalk5.cyan("Pretest")} ${chalk5.gray(`v${getCliSemver()}`)} ${tierBadge2} ${chalk5.green("(synced)")}`);
|
|
3147
3235
|
lines2.push("");
|
|
3148
3236
|
lines2.push(` Plan: ${chalk5.bold(plan)}`);
|
|
3149
3237
|
lines2.push(` Mutations: ${fmt2(remote.mutationsUsed)} / ${fmt2(remote.monthlyMutationLimit)} this month`);
|
|
@@ -3157,7 +3245,7 @@ async function runStatus() {
|
|
|
3157
3245
|
}
|
|
3158
3246
|
lines2.push("");
|
|
3159
3247
|
if (plan === "FREE") {
|
|
3160
|
-
lines2.push(` ${chalk5.gray("Upgrade:")} ${chalk5.bold("
|
|
3248
|
+
lines2.push(` ${chalk5.gray("Upgrade:")} ${chalk5.bold("pretest upgrade")}`);
|
|
3161
3249
|
lines2.push("");
|
|
3162
3250
|
}
|
|
3163
3251
|
process.stdout.write(lines2.join("\n") + "\n");
|
|
@@ -3170,7 +3258,7 @@ async function runStatus() {
|
|
|
3170
3258
|
const tierBadge = tier === "enterprise" ? chalk5.bgMagenta.white.bold(" ENTERPRISE ") : tier === "pro" ? chalk5.bgBlue.white.bold(" PRO ") : chalk5.bgGray.white.bold(" FREE ");
|
|
3171
3259
|
const lines = [];
|
|
3172
3260
|
lines.push("");
|
|
3173
|
-
lines.push(`${chalk5.cyan("
|
|
3261
|
+
lines.push(`${chalk5.cyan("Pretest")} ${chalk5.gray(`v${getCliSemver()}`)} ${tierBadge} ${chalk5.yellow("(offline)")}`);
|
|
3174
3262
|
lines.push("");
|
|
3175
3263
|
lines.push(` Plan: ${chalk5.bold(PLAN_LABEL[tier])} ${chalk5.gray("(" + planLimits.pricePerMonth + ")")}`);
|
|
3176
3264
|
lines.push(` Mutations: ${fmt2(usage.mutations)} / ${fmt2(planLimits.monthlyMutations)} this month`);
|
|
@@ -3179,8 +3267,8 @@ async function runStatus() {
|
|
|
3179
3267
|
lines.push(` Audit log: ${Number.isFinite(tierLimits.auditRetentionDays) ? `${tierLimits.auditRetentionDays} days` : "unlimited"}`);
|
|
3180
3268
|
lines.push("");
|
|
3181
3269
|
if (tier === "free") {
|
|
3182
|
-
lines.push(` ${chalk5.gray("Upgrade:")} ${chalk5.bold("
|
|
3183
|
-
lines.push(` ${chalk5.gray("Credits:")} ${chalk5.bold("
|
|
3270
|
+
lines.push(` ${chalk5.gray("Upgrade:")} ${chalk5.bold("pretest upgrade")}`);
|
|
3271
|
+
lines.push(` ${chalk5.gray("Credits:")} ${chalk5.bold("pretest credits")}`);
|
|
3184
3272
|
lines.push("");
|
|
3185
3273
|
}
|
|
3186
3274
|
process.stdout.write(lines.join("\n") + "\n");
|
|
@@ -3188,12 +3276,12 @@ async function runStatus() {
|
|
|
3188
3276
|
|
|
3189
3277
|
// src/commands/upgrade.ts
|
|
3190
3278
|
import chalk6 from "chalk";
|
|
3191
|
-
var PRICING_URL =
|
|
3279
|
+
var PRICING_URL = PUBLISH_PACKAGE_URL;
|
|
3192
3280
|
function runUpgrade() {
|
|
3193
3281
|
const tier = detectTier();
|
|
3194
3282
|
const lines = [];
|
|
3195
3283
|
lines.push("");
|
|
3196
|
-
lines.push(chalk6.cyan.bold("
|
|
3284
|
+
lines.push(chalk6.cyan.bold(" Pretest Plans"));
|
|
3197
3285
|
lines.push("");
|
|
3198
3286
|
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"));
|
|
3199
3287
|
lines.push(chalk6.gray(" \u2502 \u2502 Free \u2502 Pro \u2502 Enterprise \u2502"));
|
|
@@ -3212,7 +3300,7 @@ function runUpgrade() {
|
|
|
3212
3300
|
lines.push(` ${chalk6.gray("Upgrade at:")} ${chalk6.cyan.underline(PRICING_URL)}`);
|
|
3213
3301
|
lines.push("");
|
|
3214
3302
|
lines.push(chalk6.gray(" After purchase, set:"));
|
|
3215
|
-
lines.push(chalk6.gray(" export
|
|
3303
|
+
lines.push(chalk6.gray(" export PRETEST_LICENSE_KEY=pre_pro_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"));
|
|
3216
3304
|
lines.push("");
|
|
3217
3305
|
process.stdout.write(lines.join("\n") + "\n");
|
|
3218
3306
|
}
|
|
@@ -3251,7 +3339,7 @@ async function runCredits() {
|
|
|
3251
3339
|
const pct2 = cap2 > 0 && cap2 !== -1 ? Math.round(used2 / cap2 * 100) : 0;
|
|
3252
3340
|
const lines2 = [];
|
|
3253
3341
|
lines2.push("");
|
|
3254
|
-
lines2.push(chalk7.cyan.bold("
|
|
3342
|
+
lines2.push(chalk7.cyan.bold(" Pretest Credits") + " " + chalk7.green("(synced)"));
|
|
3255
3343
|
lines2.push("");
|
|
3256
3344
|
lines2.push(` Plan: ${chalk7.bold(remote.plan)}`);
|
|
3257
3345
|
lines2.push(` Resets: ${new Date(remote.periodResetsAt).toLocaleDateString()}`);
|
|
@@ -3261,10 +3349,10 @@ async function runCredits() {
|
|
|
3261
3349
|
lines2.push(` Remaining: ${fmt3(remaining2)}`);
|
|
3262
3350
|
lines2.push("");
|
|
3263
3351
|
if (remote.plan === "FREE" && cap2 !== -1 && used2 >= cap2 * 0.8) {
|
|
3264
|
-
lines2.push(chalk7.yellow(" Warning: Approaching free-tier limit. Run '
|
|
3352
|
+
lines2.push(chalk7.yellow(" Warning: Approaching free-tier limit. Run 'pretest upgrade' for unlimited."));
|
|
3265
3353
|
lines2.push("");
|
|
3266
3354
|
} else if (remote.plan === "FREE") {
|
|
3267
|
-
lines2.push(chalk7.gray(" Tip: Run '
|
|
3355
|
+
lines2.push(chalk7.gray(" Tip: Run 'pretest upgrade' to compare plans."));
|
|
3268
3356
|
lines2.push("");
|
|
3269
3357
|
}
|
|
3270
3358
|
process.stdout.write(lines2.join("\n") + "\n");
|
|
@@ -3279,7 +3367,7 @@ async function runCredits() {
|
|
|
3279
3367
|
const pct = Number.isFinite(cap) && cap > 0 ? Math.round(used / cap * 100) : 0;
|
|
3280
3368
|
const lines = [];
|
|
3281
3369
|
lines.push("");
|
|
3282
|
-
lines.push(chalk7.cyan.bold("
|
|
3370
|
+
lines.push(chalk7.cyan.bold(" Pretest Credits") + " " + chalk7.yellow("(offline)"));
|
|
3283
3371
|
lines.push("");
|
|
3284
3372
|
lines.push(` Plan: ${chalk7.bold(PLAN_LABEL2[tier])}`);
|
|
3285
3373
|
lines.push(` Month: ${usage.month}`);
|
|
@@ -3289,10 +3377,10 @@ async function runCredits() {
|
|
|
3289
3377
|
lines.push(` Remaining: ${fmt3(remaining)}`);
|
|
3290
3378
|
lines.push("");
|
|
3291
3379
|
if (tier === "free" && Number.isFinite(cap) && used >= cap * 0.8) {
|
|
3292
|
-
lines.push(chalk7.yellow(" Warning: Approaching free-tier limit. Run '
|
|
3380
|
+
lines.push(chalk7.yellow(" Warning: Approaching free-tier limit. Run 'pretest upgrade' for unlimited."));
|
|
3293
3381
|
lines.push("");
|
|
3294
3382
|
} else if (tier === "free") {
|
|
3295
|
-
lines.push(chalk7.gray(" Tip: Run '
|
|
3383
|
+
lines.push(chalk7.gray(" Tip: Run 'pretest upgrade' to compare plans."));
|
|
3296
3384
|
lines.push("");
|
|
3297
3385
|
}
|
|
3298
3386
|
process.stdout.write(lines.join("\n") + "\n");
|
|
@@ -3303,14 +3391,16 @@ import readline2 from "readline/promises";
|
|
|
3303
3391
|
import chalk8 from "chalk";
|
|
3304
3392
|
async function runLogout(opts = {}) {
|
|
3305
3393
|
if (!loadApiKey()) {
|
|
3306
|
-
console.log(chalk8.gray("No saved
|
|
3307
|
-
console.log(
|
|
3394
|
+
console.log(chalk8.gray("No saved dashboard API key in ~/.pretest/config.json."));
|
|
3395
|
+
console.log(
|
|
3396
|
+
chalk8.gray("If you use PRETEST_API_KEY in your shell, run unset PRETEST_API_KEY.")
|
|
3397
|
+
);
|
|
3308
3398
|
return 0;
|
|
3309
3399
|
}
|
|
3310
3400
|
if (!opts.assumeYes) {
|
|
3311
3401
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
3312
3402
|
console.error(chalk8.red("Error: confirmation requires an interactive terminal."));
|
|
3313
|
-
console.error(chalk8.gray("Run `
|
|
3403
|
+
console.error(chalk8.gray("Run `pretest logout --yes` to skip the prompt."));
|
|
3314
3404
|
return 1;
|
|
3315
3405
|
}
|
|
3316
3406
|
const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -3327,17 +3417,19 @@ async function runLogout(opts = {}) {
|
|
|
3327
3417
|
rl.close();
|
|
3328
3418
|
}
|
|
3329
3419
|
}
|
|
3330
|
-
const hadEnvDashboardKey =
|
|
3420
|
+
const hadEnvDashboardKey = hasDashboardApiKeyInEnv();
|
|
3331
3421
|
const { removed, path } = removeSavedDashboardCredentials();
|
|
3332
3422
|
clearValidationCache();
|
|
3333
|
-
|
|
3423
|
+
clearDashboardApiKeyFromProcess();
|
|
3334
3424
|
if (removed) {
|
|
3335
3425
|
console.log(chalk8.green("Logged out. Removed saved API key from"), chalk8.bold(path));
|
|
3336
|
-
console.log(
|
|
3426
|
+
console.log(
|
|
3427
|
+
chalk8.gray("Other terminals: run unset PRETEST_API_KEY if that variable is still exported (e.g. in ~/.zshrc).")
|
|
3428
|
+
);
|
|
3337
3429
|
if (hadEnvDashboardKey) {
|
|
3338
3430
|
console.log(
|
|
3339
3431
|
chalk8.yellow(
|
|
3340
|
-
"This shell had
|
|
3432
|
+
"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)."
|
|
3341
3433
|
)
|
|
3342
3434
|
);
|
|
3343
3435
|
}
|
|
@@ -3350,7 +3442,7 @@ async function runLogout(opts = {}) {
|
|
|
3350
3442
|
const major = parseInt(process.versions.node.split(".")[0], 10);
|
|
3351
3443
|
if (Number.isNaN(major) || major < 18) {
|
|
3352
3444
|
process.stderr.write(
|
|
3353
|
-
`
|
|
3445
|
+
`pretest requires Node.js 18 or newer (you have v${process.versions.node}).
|
|
3354
3446
|
`
|
|
3355
3447
|
);
|
|
3356
3448
|
process.stderr.write(`Upgrade via nvm: nvm install 20 && nvm use 20
|
|
@@ -3482,7 +3574,7 @@ function validateLanguage(lang) {
|
|
|
3482
3574
|
}
|
|
3483
3575
|
function collectFiles(target) {
|
|
3484
3576
|
const abs = resolve2(target);
|
|
3485
|
-
if (!
|
|
3577
|
+
if (!existsSync8(abs)) {
|
|
3486
3578
|
return [];
|
|
3487
3579
|
}
|
|
3488
3580
|
const stat = statSync(abs);
|
|
@@ -3500,13 +3592,14 @@ function collectFiles(target) {
|
|
|
3500
3592
|
".git",
|
|
3501
3593
|
"dist",
|
|
3502
3594
|
"build",
|
|
3595
|
+
".pretest",
|
|
3503
3596
|
".pretense",
|
|
3504
3597
|
".next",
|
|
3505
3598
|
"__pycache__",
|
|
3506
3599
|
".Trash"
|
|
3507
3600
|
]);
|
|
3508
3601
|
for (const entry of entries) {
|
|
3509
|
-
const fullPath =
|
|
3602
|
+
const fullPath = join6(dir, entry.name);
|
|
3510
3603
|
if (entry.isDirectory()) {
|
|
3511
3604
|
if (skipDirs.has(entry.name)) continue;
|
|
3512
3605
|
walk(fullPath);
|
|
@@ -3519,12 +3612,12 @@ function collectFiles(target) {
|
|
|
3519
3612
|
return files;
|
|
3520
3613
|
}
|
|
3521
3614
|
var program = new Command();
|
|
3522
|
-
program.name("
|
|
3615
|
+
program.name("pretest").description("AI firewall CLI \u2014 mutates proprietary code before LLM API calls").version(VERSION).hook("preAction", () => {
|
|
3523
3616
|
printBanner();
|
|
3524
3617
|
});
|
|
3525
|
-
program.command("init").description("Initialize .
|
|
3618
|
+
program.command("init").description("Initialize .pretest/ config in current directory").option("-d, --dir <path>", "Target directory", process.cwd()).action((opts) => {
|
|
3526
3619
|
const dir = resolve2(opts.dir);
|
|
3527
|
-
console.log(chalk9.cyan("Initializing
|
|
3620
|
+
console.log(chalk9.cyan("Initializing Pretest in"), chalk9.bold(dir));
|
|
3528
3621
|
const configDir = initConfig(dir);
|
|
3529
3622
|
const files = collectFiles(dir);
|
|
3530
3623
|
const langCounts = {};
|
|
@@ -3532,16 +3625,16 @@ program.command("init").description("Initialize .pretense/ config in current dir
|
|
|
3532
3625
|
const lang = langFromExt(f);
|
|
3533
3626
|
langCounts[lang] = (langCounts[lang] ?? 0) + 1;
|
|
3534
3627
|
}
|
|
3535
|
-
console.log(chalk9.green("\n .
|
|
3628
|
+
console.log(chalk9.green("\n .pretest/ created at"), chalk9.bold(configDir));
|
|
3536
3629
|
console.log(chalk9.gray(`
|
|
3537
3630
|
Scanned ${files.length} source files:`));
|
|
3538
3631
|
for (const [lang, count] of Object.entries(langCounts)) {
|
|
3539
3632
|
console.log(chalk9.gray(` ${lang}: ${count} files`));
|
|
3540
3633
|
}
|
|
3541
|
-
console.log(chalk9.cyan("\n Next: run"), chalk9.bold("
|
|
3634
|
+
console.log(chalk9.cyan("\n Next: run"), chalk9.bold("pretest start"), chalk9.cyan("to launch the proxy"));
|
|
3542
3635
|
console.log();
|
|
3543
3636
|
});
|
|
3544
|
-
program.command("start").description("Start the
|
|
3637
|
+
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) => {
|
|
3545
3638
|
const config = loadConfig();
|
|
3546
3639
|
const port = validatePort(opts.port);
|
|
3547
3640
|
if (port === null) {
|
|
@@ -3660,7 +3753,7 @@ function parseSizeOption(value) {
|
|
|
3660
3753
|
}
|
|
3661
3754
|
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) => {
|
|
3662
3755
|
const absTarget = resolve2(target);
|
|
3663
|
-
if (!
|
|
3756
|
+
if (!existsSync8(absTarget)) {
|
|
3664
3757
|
console.error(chalk9.red("Error: Path not found:"), chalk9.bold(absTarget));
|
|
3665
3758
|
process.exit(1);
|
|
3666
3759
|
}
|
|
@@ -3708,7 +3801,7 @@ program.command("scan <target>").description("Scan file or directory for identif
|
|
|
3708
3801
|
interrupted
|
|
3709
3802
|
}, null, 2));
|
|
3710
3803
|
} else {
|
|
3711
|
-
console.log(chalk9.cyan("\
|
|
3804
|
+
console.log(chalk9.cyan("\nPretest Scan Results\n"));
|
|
3712
3805
|
for (const r of allResults) {
|
|
3713
3806
|
if (r.skipped) {
|
|
3714
3807
|
if (r.skipped === "too-large") {
|
|
@@ -3749,9 +3842,9 @@ program.command("scan <target>").description("Scan file or directory for identif
|
|
|
3749
3842
|
process.exit(2);
|
|
3750
3843
|
}
|
|
3751
3844
|
});
|
|
3752
|
-
program.command("mutate <file>").description("Mutate identifiers for stdout (scanner redacts secrets/PII in the source first)").option("-s, --seed <seed>", "Mutation seed", "
|
|
3845
|
+
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) => {
|
|
3753
3846
|
const absPath = resolve2(file);
|
|
3754
|
-
if (!
|
|
3847
|
+
if (!existsSync8(absPath)) {
|
|
3755
3848
|
console.error(chalk9.red("Error: File not found:"), chalk9.bold(absPath));
|
|
3756
3849
|
process.exit(1);
|
|
3757
3850
|
}
|
|
@@ -3771,13 +3864,13 @@ program.command("mutate <file>").description("Mutate identifiers for stdout (sca
|
|
|
3771
3864
|
const code = readFileSync7(absPath, "utf-8");
|
|
3772
3865
|
const lang = opts.language ? validateLanguage(opts.language) : langFromExt(absPath);
|
|
3773
3866
|
const secretScan = scan2(code);
|
|
3774
|
-
const effectiveSeed =
|
|
3867
|
+
const effectiveSeed = effectiveSeedForMutation(opts.seed);
|
|
3775
3868
|
const { redactedCode: redacted, secretsMap } = applyRedactionsTracked(code, secretScan.matches, effectiveSeed);
|
|
3776
3869
|
const secretsRedacted = secretsMap.size;
|
|
3777
3870
|
const result = mutate(redacted, lang, opts.seed);
|
|
3778
3871
|
if (opts.save) {
|
|
3779
3872
|
const configDir = getConfigDir();
|
|
3780
|
-
const store = new MutationStore(
|
|
3873
|
+
const store = new MutationStore(join6(configDir, "mutation-map.json"));
|
|
3781
3874
|
store.load();
|
|
3782
3875
|
store.save({
|
|
3783
3876
|
id: absPath,
|
|
@@ -3815,15 +3908,15 @@ program.command("mutate <file>").description("Mutate identifiers for stdout (sca
|
|
|
3815
3908
|
});
|
|
3816
3909
|
program.command("reverse <file>").description("Reverse mutations using stored map").option("-m, --map <path>", "Path to mutation map JSON file").action((file, opts) => {
|
|
3817
3910
|
const absPath = resolve2(file);
|
|
3818
|
-
if (!
|
|
3911
|
+
if (!existsSync8(absPath)) {
|
|
3819
3912
|
console.error(chalk9.red("Error: File not found:"), chalk9.bold(absPath));
|
|
3820
3913
|
process.exit(1);
|
|
3821
3914
|
}
|
|
3822
3915
|
const mutatedCode = readFileSync7(absPath, "utf-8");
|
|
3823
|
-
const mapPath = opts.map ? resolve2(opts.map) :
|
|
3824
|
-
if (!
|
|
3916
|
+
const mapPath = opts.map ? resolve2(opts.map) : join6(getConfigDir(), "mutation-map.json");
|
|
3917
|
+
if (!existsSync8(mapPath)) {
|
|
3825
3918
|
console.error(chalk9.red("Error: Mutation map not found:"), chalk9.bold(mapPath));
|
|
3826
|
-
console.error(chalk9.gray("Run '
|
|
3919
|
+
console.error(chalk9.gray("Run 'pretest mutate --save' first, or specify --map <path>"));
|
|
3827
3920
|
process.exit(1);
|
|
3828
3921
|
}
|
|
3829
3922
|
let store;
|
|
@@ -3832,13 +3925,13 @@ program.command("reverse <file>").description("Reverse mutations using stored ma
|
|
|
3832
3925
|
store.load();
|
|
3833
3926
|
} catch {
|
|
3834
3927
|
console.error(chalk9.red("Error: Failed to load mutation map:"), chalk9.bold(mapPath));
|
|
3835
|
-
console.error(chalk9.gray("The file may be corrupted. Run '
|
|
3928
|
+
console.error(chalk9.gray("The file may be corrupted. Run 'pretest mutate --save' to regenerate."));
|
|
3836
3929
|
process.exit(1);
|
|
3837
3930
|
}
|
|
3838
3931
|
const entry = store.get(absPath) ?? store.getByHash(absPath) ?? store.list(1)[0];
|
|
3839
3932
|
if (!entry) {
|
|
3840
3933
|
console.error(chalk9.red("Error: No mutation map entries found."));
|
|
3841
|
-
console.error(chalk9.gray("Run '
|
|
3934
|
+
console.error(chalk9.gray("Run 'pretest mutate --save <file>' first to generate a map."));
|
|
3842
3935
|
process.exit(1);
|
|
3843
3936
|
}
|
|
3844
3937
|
const map = MutationStore.toMap(entry);
|
|
@@ -3858,16 +3951,16 @@ program.command("audit").description("View the audit log").option("--json", "Out
|
|
|
3858
3951
|
const output = formatAudit(entries, format);
|
|
3859
3952
|
console.log(output);
|
|
3860
3953
|
});
|
|
3861
|
-
program.command("version").description("Print
|
|
3862
|
-
console.log(`@
|
|
3954
|
+
program.command("version").description("Print Pretest CLI version").action(() => {
|
|
3955
|
+
console.log(`@blockyfy/stg-cli v${VERSION}`);
|
|
3863
3956
|
});
|
|
3864
3957
|
program.command("status").description("Show current plan, monthly quota, seat usage").action(async () => {
|
|
3865
3958
|
await runStatus();
|
|
3866
3959
|
});
|
|
3867
|
-
program.command("usage").description("Alias for `
|
|
3960
|
+
program.command("usage").description("Alias for `pretest status` \u2014 show monthly quota usage").action(async () => {
|
|
3868
3961
|
await runStatus();
|
|
3869
3962
|
});
|
|
3870
|
-
program.command("tokens").description("Alias for `
|
|
3963
|
+
program.command("tokens").description("Alias for `pretest credits` \u2014 show remaining mutation budget").action(async () => {
|
|
3871
3964
|
await runCredits();
|
|
3872
3965
|
});
|
|
3873
3966
|
program.command("upgrade").description("Compare plans and upgrade to Pro or Enterprise").action(() => {
|
|
@@ -3876,11 +3969,11 @@ program.command("upgrade").description("Compare plans and upgrade to Pro or Ente
|
|
|
3876
3969
|
program.command("credits").description("Show remaining mutation/scan budget for the current month").action(async () => {
|
|
3877
3970
|
await runCredits();
|
|
3878
3971
|
});
|
|
3879
|
-
program.command("login").description("Save a
|
|
3972
|
+
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) => {
|
|
3880
3973
|
const code = runLogin({ key: opts.key });
|
|
3881
3974
|
if (code !== 0) process.exit(code);
|
|
3882
3975
|
});
|
|
3883
|
-
program.command("logout").description("Remove the saved
|
|
3976
|
+
program.command("logout").description("Remove the saved dashboard API key from ~/.pretest/config.json").option("-y, --yes", "Skip confirmation prompt", false).action(async (opts) => {
|
|
3884
3977
|
const code = await runLogout({ assumeYes: opts.yes === true });
|
|
3885
3978
|
if (code !== 0) process.exit(code);
|
|
3886
3979
|
});
|