@pretense/cli 0.4.0 → 0.5.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/README.md +70 -70
- package/dist/index.js +439 -264
- package/dist/index.js.map +1 -1
- package/package.json +57 -57
package/dist/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command } from "commander";
|
|
5
|
-
import
|
|
4
|
+
import { Command as Command2 } from "commander";
|
|
5
|
+
import chalk10 from "chalk";
|
|
6
6
|
import {
|
|
7
|
-
readFileSync as
|
|
8
|
-
existsSync as
|
|
7
|
+
readFileSync as readFileSync8,
|
|
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
|
|
16
|
+
import { resolve as resolve3, extname, join as join6, basename } from "path";
|
|
17
17
|
import os from "os";
|
|
18
18
|
|
|
19
19
|
// src/types.ts
|
|
@@ -901,6 +901,12 @@ function getMutationSalt() {
|
|
|
901
901
|
function buildSaltedSeed(baseSeed) {
|
|
902
902
|
return `${baseSeed}:${getMutationSalt()}`;
|
|
903
903
|
}
|
|
904
|
+
function effectiveSeedForMutation(userSeed) {
|
|
905
|
+
if (userSeed === "pretense" || userSeed === "pretest") {
|
|
906
|
+
return buildSaltedSeed("pretense");
|
|
907
|
+
}
|
|
908
|
+
return userSeed;
|
|
909
|
+
}
|
|
904
910
|
|
|
905
911
|
// src/deterministic-id.ts
|
|
906
912
|
function hash32(str) {
|
|
@@ -1517,15 +1523,6 @@ function loadUsage(dir) {
|
|
|
1517
1523
|
return { month: currentMonth, mutations: 0, firstUseDate: today };
|
|
1518
1524
|
}
|
|
1519
1525
|
}
|
|
1520
|
-
function saveUsage(usage, dir) {
|
|
1521
|
-
const configDir = getConfigDir(dir);
|
|
1522
|
-
if (!existsSync3(configDir)) {
|
|
1523
|
-
mkdirSync3(configDir, { recursive: true });
|
|
1524
|
-
}
|
|
1525
|
-
const usagePath = join2(configDir, USAGE_FILE);
|
|
1526
|
-
const checksum = computeUsageChecksum(usage);
|
|
1527
|
-
writeFileSync3(usagePath, JSON.stringify({ ...usage, checksum }, null, 2), "utf-8");
|
|
1528
|
-
}
|
|
1529
1526
|
var MIN_LICENSE_KEY_LENGTH = 40;
|
|
1530
1527
|
var LICENSE_PAYLOAD_REGEX = /^[a-zA-Z0-9]+$/;
|
|
1531
1528
|
function isValidLicenseKey(key, prefix) {
|
|
@@ -1641,7 +1638,7 @@ function formatText(entries) {
|
|
|
1641
1638
|
if (entries.length === 0) return "No audit entries found.";
|
|
1642
1639
|
const lines = entries.map((e) => {
|
|
1643
1640
|
const date = new Date(e.timestamp).toLocaleString();
|
|
1644
|
-
return `[${date}] ${e.file} | ${e.identifiersMutated}
|
|
1641
|
+
return `[${date}] ${e.file} | ${e.identifiersMutated} protected | ${e.secretsBlocked} secrets blocked | ${e.llmProvider} | ${e.latencyMs}ms`;
|
|
1645
1642
|
});
|
|
1646
1643
|
return lines.join("\n");
|
|
1647
1644
|
}
|
|
@@ -2016,6 +2013,27 @@ function runLogin(opts = {}) {
|
|
|
2016
2013
|
return 0;
|
|
2017
2014
|
}
|
|
2018
2015
|
|
|
2016
|
+
// src/dashboard-env.ts
|
|
2017
|
+
var DASHBOARD_API_KEY_ENVS = ["PRETENSE_API_KEY", "PRETEST_API_KEY"];
|
|
2018
|
+
var DASHBOARD_API_URL_ENVS = ["PRETENSE_API_URL", "PRETEST_API_URL"];
|
|
2019
|
+
function firstNonEmptyEnv(names) {
|
|
2020
|
+
for (const name of names) {
|
|
2021
|
+
const v = process.env[name]?.trim();
|
|
2022
|
+
if (v) return v;
|
|
2023
|
+
}
|
|
2024
|
+
return "";
|
|
2025
|
+
}
|
|
2026
|
+
function clearDashboardApiKeyFromProcess() {
|
|
2027
|
+
delete process.env["PRETEST_API_KEY"];
|
|
2028
|
+
delete process.env["PRETENSE_API_KEY"];
|
|
2029
|
+
}
|
|
2030
|
+
function hasDashboardApiKeyInEnv() {
|
|
2031
|
+
return DASHBOARD_API_KEY_ENVS.some((n) => !!process.env[n]?.trim());
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
// src/publish-info.ts
|
|
2035
|
+
var PUBLISH_PACKAGE_URL = "https://www.npmjs.com/package/@pretense/cli";
|
|
2036
|
+
|
|
2019
2037
|
// src/backend-client.ts
|
|
2020
2038
|
function usageSummaryFromValidate(v) {
|
|
2021
2039
|
return {
|
|
@@ -2031,9 +2049,9 @@ function usageSummaryFromValidate(v) {
|
|
|
2031
2049
|
};
|
|
2032
2050
|
}
|
|
2033
2051
|
var DEFAULT_PRETENSE_API_URL = "https://api.pretense.ai";
|
|
2034
|
-
var DEFAULT_REQUEST_TIMEOUT_MS =
|
|
2052
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 6e4;
|
|
2035
2053
|
function getConfiguredApiRoot(rootOverride) {
|
|
2036
|
-
const raw = (rootOverride
|
|
2054
|
+
const raw = (rootOverride?.trim() || firstNonEmptyEnv(DASHBOARD_API_URL_ENVS) || DEFAULT_PRETENSE_API_URL).trim();
|
|
2037
2055
|
if (!raw) {
|
|
2038
2056
|
return DEFAULT_PRETENSE_API_URL.replace(/\/$/, "");
|
|
2039
2057
|
}
|
|
@@ -2043,7 +2061,7 @@ function getConfiguredApiRoot(rootOverride) {
|
|
|
2043
2061
|
}
|
|
2044
2062
|
try {
|
|
2045
2063
|
const u = new URL(normalized);
|
|
2046
|
-
const origin = /^pretense\.ai$/i.test(u.hostname) ?
|
|
2064
|
+
const origin = /^pretense\.ai$/i.test(u.hostname) ? DEFAULT_PRETENSE_API_URL.replace(/\/$/, "") : u.origin;
|
|
2047
2065
|
const path = u.pathname === "/" ? "" : u.pathname.replace(/\/$/, "");
|
|
2048
2066
|
return `${origin}${path}`;
|
|
2049
2067
|
} catch {
|
|
@@ -2056,20 +2074,20 @@ function getCliApiUrl(pathWithinCli, rootOverride) {
|
|
|
2056
2074
|
return `${root}/api/cli/${tail}`;
|
|
2057
2075
|
}
|
|
2058
2076
|
function getApiRequestTimeoutMs() {
|
|
2059
|
-
const raw = process.env["PRETENSE_API_TIMEOUT_MS"]?.trim();
|
|
2077
|
+
const raw = process.env["PRETEST_API_TIMEOUT_MS"]?.trim() || process.env["PRETENSE_API_TIMEOUT_MS"]?.trim();
|
|
2060
2078
|
if (!raw || !/^\d+$/.test(raw)) return DEFAULT_REQUEST_TIMEOUT_MS;
|
|
2061
2079
|
const n = parseInt(raw, 10);
|
|
2062
2080
|
return Math.min(Math.max(n, 3e3), 12e4);
|
|
2063
2081
|
}
|
|
2064
2082
|
function getApiKey() {
|
|
2065
|
-
const fromEnv =
|
|
2083
|
+
const fromEnv = firstNonEmptyEnv(DASHBOARD_API_KEY_ENVS);
|
|
2066
2084
|
if (fromEnv) return fromEnv;
|
|
2067
2085
|
const fromFile = loadApiKey();
|
|
2068
2086
|
if (!fromFile) return null;
|
|
2069
2087
|
const t = fromFile.trim();
|
|
2070
2088
|
return t.length > 0 ? t : null;
|
|
2071
2089
|
}
|
|
2072
|
-
function
|
|
2090
|
+
function getDashboardApiKey() {
|
|
2073
2091
|
return getApiKey();
|
|
2074
2092
|
}
|
|
2075
2093
|
var cachedValidation = null;
|
|
@@ -2092,6 +2110,15 @@ function bumpCachedUsageAfterLog(identifiersMutated) {
|
|
|
2092
2110
|
);
|
|
2093
2111
|
}
|
|
2094
2112
|
}
|
|
2113
|
+
function syncCachedUsageFromServer(codeProtectionsUsed, limit) {
|
|
2114
|
+
if (!cachedValidation) return;
|
|
2115
|
+
if (codeProtectionsUsed < cachedValidation.mutationsUsed) return;
|
|
2116
|
+
cachedValidation.mutationsUsed = codeProtectionsUsed;
|
|
2117
|
+
cachedValidation.monthlyMutationLimit = limit;
|
|
2118
|
+
if (limit !== -1) {
|
|
2119
|
+
cachedValidation.mutationsRemaining = Math.max(0, limit - codeProtectionsUsed);
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2095
2122
|
function makeBackendReachabilityError(code, message) {
|
|
2096
2123
|
const err = new Error(message);
|
|
2097
2124
|
err.code = code;
|
|
@@ -2109,7 +2136,7 @@ async function validateKey(opts = {}) {
|
|
|
2109
2136
|
const apiKey = getApiKey();
|
|
2110
2137
|
if (!apiKey) {
|
|
2111
2138
|
if (enforceReachability) {
|
|
2112
|
-
const err = new Error("Missing
|
|
2139
|
+
const err = new Error("Missing dashboard API key");
|
|
2113
2140
|
err.code = "MISSING_API_KEY";
|
|
2114
2141
|
cachedValidation = null;
|
|
2115
2142
|
lastValidationSuccessAt = 0;
|
|
@@ -2130,16 +2157,16 @@ async function validateKey(opts = {}) {
|
|
|
2130
2157
|
signal: controller.signal
|
|
2131
2158
|
});
|
|
2132
2159
|
if (resp.status === 401 || resp.status === 403) {
|
|
2133
|
-
const
|
|
2134
|
-
const msg = typeof
|
|
2160
|
+
const raw2 = await resp.json().catch(() => ({}));
|
|
2161
|
+
const msg = typeof raw2.message === "string" ? raw2.message : "API key rejected by server";
|
|
2135
2162
|
const err = new Error(msg);
|
|
2136
|
-
err.code = typeof
|
|
2163
|
+
err.code = typeof raw2.code === "string" ? raw2.code : "INVALID_API_KEY";
|
|
2137
2164
|
cachedValidation = null;
|
|
2138
2165
|
lastValidationSuccessAt = 0;
|
|
2139
2166
|
throw err;
|
|
2140
2167
|
}
|
|
2141
2168
|
if (!resp.ok) {
|
|
2142
|
-
const msg = `
|
|
2169
|
+
const msg = `Dashboard API returned HTTP ${resp.status} from ${validateUrl}`;
|
|
2143
2170
|
if (enforceReachability) {
|
|
2144
2171
|
cachedValidation = null;
|
|
2145
2172
|
lastValidationSuccessAt = 0;
|
|
@@ -2147,9 +2174,9 @@ async function validateKey(opts = {}) {
|
|
|
2147
2174
|
}
|
|
2148
2175
|
return null;
|
|
2149
2176
|
}
|
|
2150
|
-
let
|
|
2177
|
+
let raw;
|
|
2151
2178
|
try {
|
|
2152
|
-
|
|
2179
|
+
raw = await resp.json();
|
|
2153
2180
|
} catch {
|
|
2154
2181
|
if (enforceReachability) {
|
|
2155
2182
|
cachedValidation = null;
|
|
@@ -2161,6 +2188,12 @@ async function validateKey(opts = {}) {
|
|
|
2161
2188
|
}
|
|
2162
2189
|
return null;
|
|
2163
2190
|
}
|
|
2191
|
+
const data = {
|
|
2192
|
+
...raw,
|
|
2193
|
+
mutationsRemaining: raw["mutationsRemaining"] ?? raw["codeProtectionsRemaining"] ?? -1,
|
|
2194
|
+
mutationsUsed: raw["mutationsUsed"] ?? raw["codeProtectionsUsed"] ?? 0,
|
|
2195
|
+
monthlyMutationLimit: raw["monthlyMutationLimit"] ?? raw["monthlyCodeProtectionLimit"] ?? -1
|
|
2196
|
+
};
|
|
2164
2197
|
if (typeof data.valid === "boolean" && data.valid === false) {
|
|
2165
2198
|
const err = new Error("API key is not valid");
|
|
2166
2199
|
err.code = "INVALID_API_KEY";
|
|
@@ -2176,7 +2209,7 @@ async function validateKey(opts = {}) {
|
|
|
2176
2209
|
throw err;
|
|
2177
2210
|
}
|
|
2178
2211
|
if (enforceReachability) {
|
|
2179
|
-
const msg = err.name === "AbortError" ? `Timed out after ${timeoutMs}ms reaching ${validateUrl} (raise PRETENSE_API_TIMEOUT_MS)` : `Cannot reach
|
|
2212
|
+
const msg = err.name === "AbortError" ? `Timed out after ${timeoutMs}ms reaching ${validateUrl} (raise PRETENSE_API_TIMEOUT_MS or PRETEST_API_TIMEOUT_MS)` : `Cannot reach dashboard API at ${validateUrl}: ${err.message}`;
|
|
2180
2213
|
cachedValidation = null;
|
|
2181
2214
|
lastValidationSuccessAt = 0;
|
|
2182
2215
|
throw makeBackendReachabilityError("BACKEND_UNAVAILABLE", msg);
|
|
@@ -2205,7 +2238,13 @@ async function fetchUsageSummary(opts = {}) {
|
|
|
2205
2238
|
signal: controller.signal
|
|
2206
2239
|
});
|
|
2207
2240
|
if (!resp.ok) return null;
|
|
2208
|
-
|
|
2241
|
+
const raw = await resp.json();
|
|
2242
|
+
return {
|
|
2243
|
+
...raw,
|
|
2244
|
+
mutationsUsed: raw["mutationsUsed"] ?? raw["codeProtectionsUsed"] ?? 0,
|
|
2245
|
+
mutationsRemaining: raw["mutationsRemaining"] ?? raw["codeProtectionsRemaining"] ?? -1,
|
|
2246
|
+
monthlyMutationLimit: raw["monthlyMutationLimit"] ?? raw["monthlyCodeProtectionLimit"] ?? -1
|
|
2247
|
+
};
|
|
2209
2248
|
} catch (err) {
|
|
2210
2249
|
if (opts.verbose) {
|
|
2211
2250
|
process.stderr.write(
|
|
@@ -2222,27 +2261,91 @@ async function isWithinLimits() {
|
|
|
2222
2261
|
const v = getCachedValidation();
|
|
2223
2262
|
if (!v) return { allowed: true };
|
|
2224
2263
|
if (v.mutationsRemaining !== -1 && v.mutationsRemaining <= 0) {
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2264
|
+
const fresh = await validateKey({ force: true }).catch(() => null);
|
|
2265
|
+
const check = fresh ?? v;
|
|
2266
|
+
if (check.mutationsRemaining !== -1 && check.mutationsRemaining <= 0) {
|
|
2267
|
+
return {
|
|
2268
|
+
allowed: false,
|
|
2269
|
+
reason: `Monthly security token limit reached (${check.mutationsUsed}/${check.monthlyMutationLimit}). See ${PUBLISH_PACKAGE_URL}`
|
|
2270
|
+
};
|
|
2271
|
+
}
|
|
2272
|
+
return { allowed: true };
|
|
2229
2273
|
}
|
|
2230
2274
|
return { allowed: true };
|
|
2231
2275
|
}
|
|
2232
2276
|
|
|
2233
2277
|
// src/log-uploader.ts
|
|
2234
|
-
|
|
2278
|
+
var FLUSH_INTERVAL_MS = 5e3;
|
|
2279
|
+
var buffer = [];
|
|
2280
|
+
var flushTimer = null;
|
|
2281
|
+
function startFlushTimer(verbose) {
|
|
2282
|
+
if (flushTimer !== null) return;
|
|
2283
|
+
flushTimer = setInterval(() => {
|
|
2284
|
+
void flushBuffer(verbose);
|
|
2285
|
+
}, FLUSH_INTERVAL_MS);
|
|
2286
|
+
if (typeof flushTimer.unref === "function") flushTimer.unref();
|
|
2287
|
+
}
|
|
2288
|
+
function attachExitFlush() {
|
|
2289
|
+
const flush = () => {
|
|
2290
|
+
void flushBuffer(false);
|
|
2291
|
+
};
|
|
2292
|
+
process.once("exit", flush);
|
|
2293
|
+
process.once("SIGINT", () => {
|
|
2294
|
+
flush();
|
|
2295
|
+
process.exit(0);
|
|
2296
|
+
});
|
|
2297
|
+
process.once("SIGTERM", () => {
|
|
2298
|
+
flush();
|
|
2299
|
+
process.exit(0);
|
|
2300
|
+
});
|
|
2301
|
+
}
|
|
2302
|
+
var exitFlushAttached = false;
|
|
2303
|
+
async function flushBuffer(verbose) {
|
|
2304
|
+
if (buffer.length === 0) return;
|
|
2305
|
+
const snapshot = buffer;
|
|
2306
|
+
buffer = [];
|
|
2307
|
+
const last = snapshot[snapshot.length - 1];
|
|
2308
|
+
const { options } = last;
|
|
2309
|
+
let file_count = 0;
|
|
2310
|
+
let identifiers_mutated = 0;
|
|
2311
|
+
let secrets_blocked = 0;
|
|
2312
|
+
let scan_tokens = 0;
|
|
2313
|
+
let mutation_tokens = 0;
|
|
2314
|
+
let read_tokens = 0;
|
|
2315
|
+
let llm_provider = "unknown";
|
|
2316
|
+
let cli_version;
|
|
2317
|
+
for (const { event } of snapshot) {
|
|
2318
|
+
file_count += event.file_count ?? 0;
|
|
2319
|
+
identifiers_mutated += event.identifiers_mutated ?? 0;
|
|
2320
|
+
secrets_blocked += event.secrets_blocked ?? 0;
|
|
2321
|
+
scan_tokens += event.scan_tokens ?? 0;
|
|
2322
|
+
mutation_tokens += event.mutation_tokens ?? 0;
|
|
2323
|
+
read_tokens += event.read_tokens ?? 0;
|
|
2324
|
+
if (event.llm_provider) llm_provider = event.llm_provider;
|
|
2325
|
+
if (event.cli_version) cli_version = event.cli_version;
|
|
2326
|
+
}
|
|
2235
2327
|
const apiRoot = options.apiUrl?.trim() || void 0;
|
|
2236
2328
|
const endpoint = getCliApiUrl("log", apiRoot);
|
|
2237
|
-
const { apiKey,
|
|
2329
|
+
const { apiKey, timeoutMs = getApiRequestTimeoutMs(), onResponse } = options;
|
|
2238
2330
|
if (!apiKey) return;
|
|
2239
2331
|
const gitCtx = options.gitContext ?? await collectGitContextCached(options.cwd);
|
|
2332
|
+
const idempotency_key = crypto.randomUUID();
|
|
2240
2333
|
const body = {
|
|
2241
|
-
|
|
2242
|
-
|
|
2334
|
+
file_count,
|
|
2335
|
+
identifiers_mutated,
|
|
2336
|
+
identifiers_code_protected: identifiers_mutated,
|
|
2337
|
+
secrets_blocked,
|
|
2338
|
+
llm_provider,
|
|
2339
|
+
scan_tokens,
|
|
2340
|
+
code_protection_tokens: mutation_tokens,
|
|
2341
|
+
read_tokens,
|
|
2342
|
+
idempotency_key,
|
|
2343
|
+
...cli_version ? { cli_version } : {},
|
|
2344
|
+
git_remote: gitCtx.git_remote,
|
|
2243
2345
|
repo_remote_url: gitCtx.git_remote,
|
|
2244
2346
|
git_branch: gitCtx.git_branch,
|
|
2245
|
-
commit_sha: gitCtx.git_commit_sha
|
|
2347
|
+
commit_sha: gitCtx.git_commit_sha,
|
|
2348
|
+
git_commit_sha: gitCtx.git_commit_sha
|
|
2246
2349
|
};
|
|
2247
2350
|
const controller = new AbortController();
|
|
2248
2351
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -2256,8 +2359,14 @@ async function uploadLog(event, options) {
|
|
|
2256
2359
|
body: JSON.stringify(body),
|
|
2257
2360
|
signal: controller.signal
|
|
2258
2361
|
});
|
|
2259
|
-
if (
|
|
2260
|
-
|
|
2362
|
+
if (resp.ok) {
|
|
2363
|
+
const data = await resp.json().catch(() => null);
|
|
2364
|
+
if (data && typeof data.codeProtectionsUsed === "number" && typeof data.limit === "number") {
|
|
2365
|
+
onResponse?.(data.codeProtectionsUsed, data.limit);
|
|
2366
|
+
}
|
|
2367
|
+
} else if (verbose) {
|
|
2368
|
+
const detail = await resp.text().catch(() => "");
|
|
2369
|
+
process.stderr.write(`[PRETENSE] log upload HTTP ${resp.status} (batch of ${snapshot.length}): ${detail}
|
|
2261
2370
|
`);
|
|
2262
2371
|
}
|
|
2263
2372
|
} catch (err) {
|
|
@@ -2270,6 +2379,15 @@ async function uploadLog(event, options) {
|
|
|
2270
2379
|
clearTimeout(timer);
|
|
2271
2380
|
}
|
|
2272
2381
|
}
|
|
2382
|
+
async function uploadLog(event, options) {
|
|
2383
|
+
if (!options.apiKey) return;
|
|
2384
|
+
buffer.push({ event, options });
|
|
2385
|
+
startFlushTimer(options.verbose ?? false);
|
|
2386
|
+
if (!exitFlushAttached) {
|
|
2387
|
+
exitFlushAttached = true;
|
|
2388
|
+
attachExitFlush();
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2273
2391
|
|
|
2274
2392
|
// src/start-client-instructions.ts
|
|
2275
2393
|
import chalk2 from "chalk";
|
|
@@ -2328,6 +2446,10 @@ import { fileURLToPath } from "url";
|
|
|
2328
2446
|
var cached = null;
|
|
2329
2447
|
function getCliSemver() {
|
|
2330
2448
|
if (cached !== null) return cached;
|
|
2449
|
+
if (typeof __BINARY_VERSION__ !== "undefined" && __BINARY_VERSION__) {
|
|
2450
|
+
cached = __BINARY_VERSION__;
|
|
2451
|
+
return cached;
|
|
2452
|
+
}
|
|
2331
2453
|
try {
|
|
2332
2454
|
const url = new URL("../package.json", import.meta.url);
|
|
2333
2455
|
const raw = readFileSync6(fileURLToPath(url), "utf-8");
|
|
@@ -2349,7 +2471,7 @@ function attachDashboardLogoutWatcher(enabled) {
|
|
|
2349
2471
|
if (!fk) {
|
|
2350
2472
|
if (dashboardSavedCredentialsRevoked) return;
|
|
2351
2473
|
dashboardSavedCredentialsRevoked = true;
|
|
2352
|
-
|
|
2474
|
+
clearDashboardApiKeyFromProcess();
|
|
2353
2475
|
clearValidationCache();
|
|
2354
2476
|
try {
|
|
2355
2477
|
process.stderr.write(
|
|
@@ -2480,7 +2602,7 @@ function printUpgradeNudge(reason) {
|
|
|
2480
2602
|
process.stdout.write(
|
|
2481
2603
|
`
|
|
2482
2604
|
\x1B[33m[PRETENSE] ${reason}\x1B[0m
|
|
2483
|
-
\x1B[33m[PRETENSE] Upgrade to Pro:
|
|
2605
|
+
\x1B[33m[PRETENSE] Upgrade to Pro: ${PUBLISH_PACKAGE_URL}\x1B[0m
|
|
2484
2606
|
|
|
2485
2607
|
`
|
|
2486
2608
|
);
|
|
@@ -2505,7 +2627,7 @@ function createProxy(opts = {}) {
|
|
|
2505
2627
|
"POST /*": "Proxy to upstream LLM API (Anthropic, OpenAI, Google auto-detected)"
|
|
2506
2628
|
},
|
|
2507
2629
|
languages: ["TypeScript", "JavaScript", "Python", "Go", "Java", "C#", "Ruby", "Rust"],
|
|
2508
|
-
docs:
|
|
2630
|
+
docs: PUBLISH_PACKAGE_URL
|
|
2509
2631
|
})
|
|
2510
2632
|
);
|
|
2511
2633
|
app.get(
|
|
@@ -2564,19 +2686,19 @@ function createProxy(opts = {}) {
|
|
|
2564
2686
|
{
|
|
2565
2687
|
error: {
|
|
2566
2688
|
type: "pretense_session_logged_out",
|
|
2567
|
-
message: "Saved
|
|
2689
|
+
message: "Saved dashboard credentials were removed (for example pretense logout). Run pretense login, then restart pretense start. If you started the proxy with a dashboard API key in this shell, stop it (Ctrl+C) and start again after logout."
|
|
2568
2690
|
}
|
|
2569
2691
|
},
|
|
2570
2692
|
401
|
|
2571
2693
|
);
|
|
2572
2694
|
}
|
|
2573
|
-
const
|
|
2574
|
-
if (!
|
|
2695
|
+
const dashboardApiKey = getDashboardApiKey();
|
|
2696
|
+
if (!dashboardApiKey) {
|
|
2575
2697
|
return c.json(
|
|
2576
2698
|
{
|
|
2577
2699
|
error: {
|
|
2578
2700
|
type: "pretense_dashboard_key_required",
|
|
2579
|
-
message: "Configure your
|
|
2701
|
+
message: "Configure your dashboard API key: run `pretense login --key prtns_live_...` or set PRETENSE_API_KEY."
|
|
2580
2702
|
}
|
|
2581
2703
|
},
|
|
2582
2704
|
401
|
|
@@ -2622,26 +2744,13 @@ function createProxy(opts = {}) {
|
|
|
2622
2744
|
getCachedValidation()?.plan,
|
|
2623
2745
|
detectTier()
|
|
2624
2746
|
);
|
|
2625
|
-
const { monthlyMutations } = getTierLimits(tier);
|
|
2626
|
-
const usage = loadUsage();
|
|
2627
|
-
if (tier === "free" && usage.mutations >= monthlyMutations) {
|
|
2628
|
-
return c.json(
|
|
2629
|
-
{
|
|
2630
|
-
error: {
|
|
2631
|
-
type: "pretense_rate_limit",
|
|
2632
|
-
message: `Monthly mutation limit reached (${monthlyMutations}/month). Upgrade to Pro for unlimited: https://pretense.ai/pricing`
|
|
2633
|
-
}
|
|
2634
|
-
},
|
|
2635
|
-
429
|
|
2636
|
-
);
|
|
2637
|
-
}
|
|
2638
2747
|
const backendLimits = await isWithinLimits();
|
|
2639
2748
|
if (!backendLimits.allowed) {
|
|
2640
2749
|
return c.json(
|
|
2641
2750
|
{
|
|
2642
2751
|
error: {
|
|
2643
2752
|
type: "pretense_rate_limit",
|
|
2644
|
-
message: backendLimits.reason ?? "
|
|
2753
|
+
message: backendLimits.reason ?? "Security token limit exceeded."
|
|
2645
2754
|
}
|
|
2646
2755
|
},
|
|
2647
2756
|
429
|
|
@@ -2706,10 +2815,9 @@ function createProxy(opts = {}) {
|
|
|
2706
2815
|
}
|
|
2707
2816
|
return replaceCodeBlocks(text, blocks, mutatedBlocks);
|
|
2708
2817
|
});
|
|
2709
|
-
usage.mutations += tokensMutated;
|
|
2710
|
-
saveUsage(usage);
|
|
2711
2818
|
const remoteLogDisabled = process.env["PRETENSE_DISABLE_REMOTE_LOG"] === "1";
|
|
2712
|
-
if (
|
|
2819
|
+
if (dashboardApiKey && !remoteLogDisabled) {
|
|
2820
|
+
bumpCachedUsageAfterLog(tokensMutated);
|
|
2713
2821
|
void uploadLog(
|
|
2714
2822
|
{
|
|
2715
2823
|
file_count: 0,
|
|
@@ -2718,16 +2826,26 @@ function createProxy(opts = {}) {
|
|
|
2718
2826
|
llm_provider: provider,
|
|
2719
2827
|
cli_version: getCliSemver()
|
|
2720
2828
|
},
|
|
2721
|
-
{
|
|
2829
|
+
{
|
|
2830
|
+
apiKey: dashboardApiKey,
|
|
2831
|
+
verbose,
|
|
2832
|
+
onResponse: (codeProtectionsUsed, limit) => {
|
|
2833
|
+
syncCachedUsageFromServer(codeProtectionsUsed, limit);
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2722
2836
|
);
|
|
2723
|
-
bumpCachedUsageAfterLog(tokensMutated);
|
|
2724
2837
|
}
|
|
2725
|
-
const
|
|
2726
|
-
if (
|
|
2727
|
-
|
|
2838
|
+
const warnCache = getCachedValidation();
|
|
2839
|
+
if (warnCache && warnCache.monthlyMutationLimit !== -1 && tier === "free") {
|
|
2840
|
+
const limit = warnCache.monthlyMutationLimit;
|
|
2841
|
+
const used = warnCache.mutationsUsed;
|
|
2842
|
+
const warningThreshold = Math.floor(limit * 0.8);
|
|
2843
|
+
if (used >= warningThreshold && used - tokensMutated < warningThreshold) {
|
|
2844
|
+
printUpgradeNudge(`${used}/${limit} monthly security tokens used. Running low!`);
|
|
2845
|
+
}
|
|
2728
2846
|
}
|
|
2729
2847
|
if (tier === "free" && daysSinceFirstUse() >= 7) {
|
|
2730
|
-
printUpgradeNudge("You've been using Pretense for 7+ days. Unlock Pro features: unlimited
|
|
2848
|
+
printUpgradeNudge("You've been using Pretense for 7+ days. Unlock Pro features: unlimited security tokens, 90-day audit, Slack alerts.");
|
|
2731
2849
|
}
|
|
2732
2850
|
const entry = {
|
|
2733
2851
|
id: requestId,
|
|
@@ -2744,7 +2862,7 @@ function createProxy(opts = {}) {
|
|
|
2744
2862
|
createAuditEntry(requestId, "proxy-request", tokensMutated, blockedSecrets.length, provider, latencyMs)
|
|
2745
2863
|
);
|
|
2746
2864
|
if (verbose && tokensMutated > 0) {
|
|
2747
|
-
process.stdout.write(`[PRETENSE] ${tokensMutated} tokens
|
|
2865
|
+
process.stdout.write(`[PRETENSE] ${tokensMutated} security tokens protected for request ${requestId}
|
|
2748
2866
|
`);
|
|
2749
2867
|
}
|
|
2750
2868
|
const headers = {};
|
|
@@ -2784,12 +2902,12 @@ function createProxy(opts = {}) {
|
|
|
2784
2902
|
}
|
|
2785
2903
|
const decoder = new TextDecoder();
|
|
2786
2904
|
const encoder = new TextEncoder();
|
|
2787
|
-
let
|
|
2905
|
+
let buffer2 = "";
|
|
2788
2906
|
const transform = new TransformStream({
|
|
2789
2907
|
transform(chunk, controller) {
|
|
2790
|
-
|
|
2791
|
-
const lines =
|
|
2792
|
-
|
|
2908
|
+
buffer2 += decoder.decode(chunk, { stream: true });
|
|
2909
|
+
const lines = buffer2.split("\n");
|
|
2910
|
+
buffer2 = lines.pop() ?? "";
|
|
2793
2911
|
for (const line of lines) {
|
|
2794
2912
|
if (line.startsWith("data: ") && line !== "data: [DONE]") {
|
|
2795
2913
|
try {
|
|
@@ -2806,8 +2924,8 @@ function createProxy(opts = {}) {
|
|
|
2806
2924
|
}
|
|
2807
2925
|
},
|
|
2808
2926
|
flush(controller) {
|
|
2809
|
-
if (
|
|
2810
|
-
controller.enqueue(encoder.encode(
|
|
2927
|
+
if (buffer2.length > 0) {
|
|
2928
|
+
controller.enqueue(encoder.encode(buffer2));
|
|
2811
2929
|
}
|
|
2812
2930
|
}
|
|
2813
2931
|
});
|
|
@@ -2851,9 +2969,9 @@ function createProxy(opts = {}) {
|
|
|
2851
2969
|
return app;
|
|
2852
2970
|
}
|
|
2853
2971
|
function isPortAvailable(port) {
|
|
2854
|
-
return new Promise((
|
|
2855
|
-
const tester = createServer().once("error", () =>
|
|
2856
|
-
tester.once("close", () =>
|
|
2972
|
+
return new Promise((resolve4) => {
|
|
2973
|
+
const tester = createServer().once("error", () => resolve4(false)).once("listening", () => {
|
|
2974
|
+
tester.once("close", () => resolve4(true)).close();
|
|
2857
2975
|
}).listen(port, "127.0.0.1");
|
|
2858
2976
|
});
|
|
2859
2977
|
}
|
|
@@ -2871,12 +2989,12 @@ function startProxy(opts = {}) {
|
|
|
2871
2989
|
const requestedPort = opts.port ?? opts.config?.port ?? DEFAULT_CONFIG.port;
|
|
2872
2990
|
const app = createProxy(opts);
|
|
2873
2991
|
void (async () => {
|
|
2874
|
-
const
|
|
2875
|
-
const dashboardKey =
|
|
2992
|
+
const dashboardKeyFromEnvAtLaunch = hasDashboardApiKeyInEnv();
|
|
2993
|
+
const dashboardKey = getDashboardApiKey();
|
|
2876
2994
|
if (!dashboardKey || !isValidKeyShape(dashboardKey)) {
|
|
2877
2995
|
process.stderr.write(
|
|
2878
2996
|
`\x1B[31m[PRETENSE] API key required or invalid shape. Run \`pretense login --key prtns_live_...\`\x1B[0m
|
|
2879
|
-
\x1B[31m[PRETENSE] or set PRETENSE_API_KEY.
|
|
2997
|
+
\x1B[31m[PRETENSE] or set PRETENSE_API_KEY. See ${PUBLISH_PACKAGE_URL}\x1B[0m
|
|
2880
2998
|
`
|
|
2881
2999
|
);
|
|
2882
3000
|
process.exit(1);
|
|
@@ -2899,7 +3017,7 @@ function startProxy(opts = {}) {
|
|
|
2899
3017
|
const plan = validation.plan ?? "FREE";
|
|
2900
3018
|
const remaining = validation.mutationsRemaining === -1 ? "unlimited" : String(validation.mutationsRemaining);
|
|
2901
3019
|
process.stdout.write(
|
|
2902
|
-
`\x1B[32m[PRETENSE] Key validated: ${plan} plan` + (validation.organizationName ? ` (${validation.organizationName})` : "") + ` \u2014 ${remaining}
|
|
3020
|
+
`\x1B[32m[PRETENSE] Key validated: ${plan} plan` + (validation.organizationName ? ` (${validation.organizationName})` : "") + ` \u2014 ${remaining} security tokens remaining\x1B[0m
|
|
2903
3021
|
`
|
|
2904
3022
|
);
|
|
2905
3023
|
break;
|
|
@@ -2909,7 +3027,7 @@ function startProxy(opts = {}) {
|
|
|
2909
3027
|
if (code === "BACKEND_UNAVAILABLE" || code === "AUTH_BACKEND_REJECTED") {
|
|
2910
3028
|
process.stderr.write(`\x1B[31m[PRETENSE] ${msg}\x1B[0m
|
|
2911
3029
|
`);
|
|
2912
|
-
process.stderr.write(`\x1B[31m[PRETENSE] Fix network or set PRETENSE_API_URL if using a custom API.\x1B[0m
|
|
3030
|
+
process.stderr.write(`\x1B[31m[PRETENSE] Fix network or set PRETENSE_API_URL (or PRETEST_API_URL) if using a custom API.\x1B[0m
|
|
2913
3031
|
`);
|
|
2914
3032
|
process.exit(1);
|
|
2915
3033
|
}
|
|
@@ -2925,7 +3043,7 @@ function startProxy(opts = {}) {
|
|
|
2925
3043
|
process.stderr.write(`\x1B[31m[PRETENSE] ${msg}\x1B[0m
|
|
2926
3044
|
`);
|
|
2927
3045
|
process.stderr.write(
|
|
2928
|
-
`\x1B[33m[PRETENSE] Enter your
|
|
3046
|
+
`\x1B[33m[PRETENSE] Enter your dashboard API key and press Enter (Ctrl+C to quit).\x1B[0m
|
|
2929
3047
|
`
|
|
2930
3048
|
);
|
|
2931
3049
|
let entered = null;
|
|
@@ -2938,7 +3056,7 @@ function startProxy(opts = {}) {
|
|
|
2938
3056
|
}
|
|
2939
3057
|
if (isValidKeyShape(entered)) break;
|
|
2940
3058
|
process.stderr.write(
|
|
2941
|
-
`\x1B[31m[PRETENSE] That does not look like a
|
|
3059
|
+
`\x1B[31m[PRETENSE] That does not look like a dashboard key (expect prtns_live_\u2026 or pk_live_\u2026).\x1B[0m
|
|
2942
3060
|
`
|
|
2943
3061
|
);
|
|
2944
3062
|
}
|
|
@@ -2962,7 +3080,7 @@ function startProxy(opts = {}) {
|
|
|
2962
3080
|
process.exit(1);
|
|
2963
3081
|
}
|
|
2964
3082
|
}
|
|
2965
|
-
attachDashboardLogoutWatcher(!
|
|
3083
|
+
attachDashboardLogoutWatcher(!dashboardKeyFromEnvAtLaunch);
|
|
2966
3084
|
const bannerTier = tierFromDashboardPlan(
|
|
2967
3085
|
getCachedValidation()?.plan,
|
|
2968
3086
|
detectTier()
|
|
@@ -3007,7 +3125,7 @@ function startProxy(opts = {}) {
|
|
|
3007
3125
|
`);
|
|
3008
3126
|
setInterval(() => {
|
|
3009
3127
|
if (dashboardSavedCredentialsRevoked) return;
|
|
3010
|
-
if (!
|
|
3128
|
+
if (!getDashboardApiKey()) return;
|
|
3011
3129
|
void validateKey({
|
|
3012
3130
|
force: true,
|
|
3013
3131
|
verbose: opts.verbose,
|
|
@@ -3031,39 +3149,43 @@ var PLAN_LIMITS = {
|
|
|
3031
3149
|
pricePerMonth: "$99/seat/month"
|
|
3032
3150
|
}
|
|
3033
3151
|
};
|
|
3034
|
-
function getPlanLimits(tier) {
|
|
3035
|
-
return PLAN_LIMITS[tier];
|
|
3036
|
-
}
|
|
3037
3152
|
function fmt(n) {
|
|
3038
|
-
if (!Number.isFinite(n)) return "unlimited";
|
|
3153
|
+
if (n === -1 || !Number.isFinite(n)) return "unlimited";
|
|
3039
3154
|
return n.toLocaleString("en-US");
|
|
3040
3155
|
}
|
|
3041
3156
|
function printTokenFooter(filesScanned, mutationsMade) {
|
|
3042
|
-
const
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3157
|
+
const cached2 = getCachedValidation();
|
|
3158
|
+
if (!cached2) {
|
|
3159
|
+
process.stderr.write(chalk3.gray(`
|
|
3160
|
+
${filesScanned} files, ${mutationsMade} security tokens
|
|
3161
|
+
|
|
3162
|
+
`));
|
|
3163
|
+
return;
|
|
3164
|
+
}
|
|
3165
|
+
const plan = cached2.plan;
|
|
3166
|
+
if (plan === "ENTERPRISE") {
|
|
3046
3167
|
process.stderr.write(
|
|
3047
3168
|
chalk3.gray(`
|
|
3048
|
-
${filesScanned} files, ${mutationsMade}
|
|
3169
|
+
${filesScanned} files, ${mutationsMade} security tokens \xB7 Plan: Enterprise (unlimited)
|
|
3049
3170
|
|
|
3050
3171
|
`)
|
|
3051
3172
|
);
|
|
3052
3173
|
return;
|
|
3053
3174
|
}
|
|
3054
|
-
const used =
|
|
3055
|
-
const cap =
|
|
3056
|
-
const
|
|
3057
|
-
const
|
|
3175
|
+
const used = cached2.mutationsUsed;
|
|
3176
|
+
const cap = cached2.monthlyMutationLimit;
|
|
3177
|
+
const unlimited = cap === -1 || !Number.isFinite(cap);
|
|
3178
|
+
const pct = !unlimited && cap > 0 ? Math.min(100, Math.round(used / cap * 100)) : 0;
|
|
3179
|
+
const usageStr = unlimited ? chalk3.gray(`${fmt(used)} / unlimited`) : pct >= 80 ? chalk3.yellow(`${fmt(used)} / ${fmt(cap)}`) : chalk3.gray(`${fmt(used)} / ${fmt(cap)}`);
|
|
3058
3180
|
process.stderr.write(
|
|
3059
3181
|
`
|
|
3060
|
-
${chalk3.cyan(filesScanned + " files")}, ${chalk3.cyan(mutationsMade + "
|
|
3182
|
+
${chalk3.cyan(filesScanned + " files")}, ${chalk3.cyan(mutationsMade + " security tokens")} \xB7 Plan: ${chalk3.bold(plan)} \xB7 Security tokens this month: ${usageStr}${!unlimited ? ` (${pct}%)` : ""}
|
|
3061
3183
|
`
|
|
3062
3184
|
);
|
|
3063
|
-
if (
|
|
3185
|
+
if (plan === "FREE") {
|
|
3064
3186
|
if (pct >= 80) {
|
|
3065
3187
|
process.stderr.write(
|
|
3066
|
-
chalk3.yellow(` \u26A0 Approaching free-tier limit. Upgrade: ${chalk3.bold(
|
|
3188
|
+
chalk3.yellow(` \u26A0 Approaching free-tier limit. Upgrade: ${chalk3.bold(PUBLISH_PACKAGE_URL)}
|
|
3067
3189
|
|
|
3068
3190
|
`)
|
|
3069
3191
|
);
|
|
@@ -3116,11 +3238,6 @@ function printBanner() {
|
|
|
3116
3238
|
|
|
3117
3239
|
// src/commands/status.ts
|
|
3118
3240
|
import chalk5 from "chalk";
|
|
3119
|
-
var PLAN_LABEL = {
|
|
3120
|
-
free: "Free",
|
|
3121
|
-
pro: "Pro",
|
|
3122
|
-
enterprise: "Enterprise"
|
|
3123
|
-
};
|
|
3124
3241
|
function fmt2(n) {
|
|
3125
3242
|
if (n === -1 || !Number.isFinite(n)) return "unlimited";
|
|
3126
3243
|
return n.toLocaleString("en-US");
|
|
@@ -3135,50 +3252,36 @@ async function runStatus() {
|
|
|
3135
3252
|
}
|
|
3136
3253
|
if (remote) {
|
|
3137
3254
|
const plan = remote.plan;
|
|
3138
|
-
const
|
|
3139
|
-
const
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3255
|
+
const tierBadge = plan === "ENTERPRISE" ? chalk5.bgMagenta.white.bold(" ENTERPRISE ") : plan === "PRO" ? chalk5.bgBlue.white.bold(" PRO ") : chalk5.bgGray.white.bold(" FREE ");
|
|
3256
|
+
const lines = [];
|
|
3257
|
+
lines.push("");
|
|
3258
|
+
lines.push(`${chalk5.cyan("Pretense")} ${chalk5.gray(`v${getCliSemver()}`)} ${tierBadge} ${chalk5.green("(synced)")}`);
|
|
3259
|
+
lines.push("");
|
|
3260
|
+
lines.push(` Plan: ${chalk5.bold(plan)}`);
|
|
3261
|
+
lines.push(` Security tokens:${fmt2(remote.mutationsUsed)} / ${fmt2(remote.monthlyMutationLimit)} this month`);
|
|
3262
|
+
lines.push(` Remaining: ${fmt2(remote.mutationsRemaining)}`);
|
|
3263
|
+
lines.push(` Requests: ${fmt2(remote.requestsThisPeriod)} this period`);
|
|
3264
|
+
lines.push(` Secrets: ${fmt2(remote.secretsBlockedThisPeriod)} blocked`);
|
|
3265
|
+
lines.push(` Key status: ${remote.keyStatus === "active" ? chalk5.green("active") : chalk5.red("revoked")}`);
|
|
3266
|
+
lines.push(` Resets at: ${new Date(remote.periodResetsAt).toLocaleDateString()}`);
|
|
3150
3267
|
if (remote.keyExpiresAt) {
|
|
3151
|
-
|
|
3268
|
+
lines.push(` Key expires: ${new Date(remote.keyExpiresAt).toLocaleDateString()}`);
|
|
3152
3269
|
}
|
|
3153
|
-
|
|
3270
|
+
lines.push("");
|
|
3154
3271
|
if (plan === "FREE") {
|
|
3155
|
-
|
|
3156
|
-
|
|
3272
|
+
lines.push(` ${chalk5.gray("Upgrade:")} ${chalk5.bold("pretense upgrade")}`);
|
|
3273
|
+
lines.push("");
|
|
3157
3274
|
}
|
|
3158
|
-
process.stdout.write(
|
|
3275
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
3159
3276
|
return;
|
|
3160
3277
|
}
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
lines.push(`${chalk5.cyan("Pretense")} ${chalk5.gray(`v${getCliSemver()}`)} ${tierBadge} ${chalk5.yellow("(offline)")}`);
|
|
3169
|
-
lines.push("");
|
|
3170
|
-
lines.push(` Plan: ${chalk5.bold(PLAN_LABEL[tier])} ${chalk5.gray("(" + planLimits.pricePerMonth + ")")}`);
|
|
3171
|
-
lines.push(` Mutations: ${fmt2(usage.mutations)} / ${fmt2(planLimits.monthlyMutations)} this month`);
|
|
3172
|
-
lines.push(` Scans: 0 / ${fmt2(planLimits.monthlyScans)} this month`);
|
|
3173
|
-
lines.push(` Seats: 1 / ${fmt2(planLimits.seats)}`);
|
|
3174
|
-
lines.push(` Audit log: ${Number.isFinite(tierLimits.auditRetentionDays) ? `${tierLimits.auditRetentionDays} days` : "unlimited"}`);
|
|
3175
|
-
lines.push("");
|
|
3176
|
-
if (tier === "free") {
|
|
3177
|
-
lines.push(` ${chalk5.gray("Upgrade:")} ${chalk5.bold("pretense upgrade")}`);
|
|
3178
|
-
lines.push(` ${chalk5.gray("Credits:")} ${chalk5.bold("pretense credits")}`);
|
|
3179
|
-
lines.push("");
|
|
3180
|
-
}
|
|
3181
|
-
process.stdout.write(lines.join("\n") + "\n");
|
|
3278
|
+
process.stderr.write(
|
|
3279
|
+
chalk5.yellow(`
|
|
3280
|
+
[pretense] Could not reach server \u2014 usage data unavailable.
|
|
3281
|
+
`) + chalk5.gray(` Check your connection or run \`pretense login\` if your key has changed.
|
|
3282
|
+
|
|
3283
|
+
`)
|
|
3284
|
+
);
|
|
3182
3285
|
}
|
|
3183
3286
|
|
|
3184
3287
|
// src/commands/upgrade.ts
|
|
@@ -3214,11 +3317,6 @@ function runUpgrade() {
|
|
|
3214
3317
|
|
|
3215
3318
|
// src/commands/credits.ts
|
|
3216
3319
|
import chalk7 from "chalk";
|
|
3217
|
-
var PLAN_LABEL2 = {
|
|
3218
|
-
free: "Free",
|
|
3219
|
-
pro: "Pro",
|
|
3220
|
-
enterprise: "Enterprise"
|
|
3221
|
-
};
|
|
3222
3320
|
function fmt3(n) {
|
|
3223
3321
|
if (n === -1 || !Number.isFinite(n)) return "unlimited";
|
|
3224
3322
|
return n.toLocaleString("en-US");
|
|
@@ -3240,57 +3338,38 @@ async function runCredits() {
|
|
|
3240
3338
|
}
|
|
3241
3339
|
}
|
|
3242
3340
|
if (remote) {
|
|
3243
|
-
const
|
|
3244
|
-
const
|
|
3245
|
-
const
|
|
3246
|
-
const
|
|
3247
|
-
const
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
if (remote.plan === "FREE" &&
|
|
3259
|
-
|
|
3260
|
-
|
|
3341
|
+
const used = remote.mutationsUsed;
|
|
3342
|
+
const cap = remote.monthlyMutationLimit;
|
|
3343
|
+
const remaining = remote.mutationsRemaining;
|
|
3344
|
+
const pct = cap > 0 && cap !== -1 ? Math.round(used / cap * 100) : 0;
|
|
3345
|
+
const lines = [];
|
|
3346
|
+
lines.push("");
|
|
3347
|
+
lines.push(chalk7.cyan.bold(" Pretense Credits") + " " + chalk7.green("(synced)"));
|
|
3348
|
+
lines.push("");
|
|
3349
|
+
lines.push(` Plan: ${chalk7.bold(remote.plan)}`);
|
|
3350
|
+
lines.push(` Resets: ${new Date(remote.periodResetsAt).toLocaleDateString()}`);
|
|
3351
|
+
lines.push("");
|
|
3352
|
+
lines.push(` Security tokens:${progressBar(used, cap)} ${fmt3(used)} / ${fmt3(cap)}`);
|
|
3353
|
+
lines.push(` Used: ${pct}%`);
|
|
3354
|
+
lines.push(` Remaining: ${fmt3(remaining)}`);
|
|
3355
|
+
lines.push("");
|
|
3356
|
+
if (remote.plan === "FREE" && cap !== -1 && used >= cap * 0.8) {
|
|
3357
|
+
lines.push(chalk7.yellow(" Warning: Approaching free-tier limit. Run 'pretense upgrade' for unlimited."));
|
|
3358
|
+
lines.push("");
|
|
3261
3359
|
} else if (remote.plan === "FREE") {
|
|
3262
|
-
|
|
3263
|
-
|
|
3360
|
+
lines.push(chalk7.gray(" Tip: Run 'pretense upgrade' to compare plans."));
|
|
3361
|
+
lines.push("");
|
|
3264
3362
|
}
|
|
3265
|
-
process.stdout.write(
|
|
3363
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
3266
3364
|
return;
|
|
3267
3365
|
}
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
const lines = [];
|
|
3276
|
-
lines.push("");
|
|
3277
|
-
lines.push(chalk7.cyan.bold(" Pretense Credits") + " " + chalk7.yellow("(offline)"));
|
|
3278
|
-
lines.push("");
|
|
3279
|
-
lines.push(` Plan: ${chalk7.bold(PLAN_LABEL2[tier])}`);
|
|
3280
|
-
lines.push(` Month: ${usage.month}`);
|
|
3281
|
-
lines.push("");
|
|
3282
|
-
lines.push(` Mutations: ${progressBar(used, cap)} ${fmt3(used)} / ${fmt3(cap)}`);
|
|
3283
|
-
lines.push(` Used: ${pct}%`);
|
|
3284
|
-
lines.push(` Remaining: ${fmt3(remaining)}`);
|
|
3285
|
-
lines.push("");
|
|
3286
|
-
if (tier === "free" && Number.isFinite(cap) && used >= cap * 0.8) {
|
|
3287
|
-
lines.push(chalk7.yellow(" Warning: Approaching free-tier limit. Run 'pretense upgrade' for unlimited."));
|
|
3288
|
-
lines.push("");
|
|
3289
|
-
} else if (tier === "free") {
|
|
3290
|
-
lines.push(chalk7.gray(" Tip: Run 'pretense upgrade' to compare plans."));
|
|
3291
|
-
lines.push("");
|
|
3292
|
-
}
|
|
3293
|
-
process.stdout.write(lines.join("\n") + "\n");
|
|
3366
|
+
process.stderr.write(
|
|
3367
|
+
chalk7.yellow(`
|
|
3368
|
+
[pretense] Could not reach server \u2014 usage data unavailable.
|
|
3369
|
+
`) + chalk7.gray(` Check your connection or run \`pretense login\` if your key has changed.
|
|
3370
|
+
|
|
3371
|
+
`)
|
|
3372
|
+
);
|
|
3294
3373
|
}
|
|
3295
3374
|
|
|
3296
3375
|
// src/commands/logout.ts
|
|
@@ -3340,6 +3419,100 @@ async function runLogout(opts = {}) {
|
|
|
3340
3419
|
return 0;
|
|
3341
3420
|
}
|
|
3342
3421
|
|
|
3422
|
+
// src/commands/install.ts
|
|
3423
|
+
import { Command } from "commander";
|
|
3424
|
+
import chalk9 from "chalk";
|
|
3425
|
+
import {
|
|
3426
|
+
existsSync as existsSync7,
|
|
3427
|
+
writeFileSync as writeFileSync5,
|
|
3428
|
+
chmodSync as chmodSync2,
|
|
3429
|
+
readFileSync as readFileSync7,
|
|
3430
|
+
mkdirSync as mkdirSync6
|
|
3431
|
+
} from "fs";
|
|
3432
|
+
import { join as join5, resolve as resolve2 } from "path";
|
|
3433
|
+
var HOOK_SCRIPTS = {
|
|
3434
|
+
"pre-commit": `#!/bin/sh
|
|
3435
|
+
# Pretense AI Firewall \u2014 pre-commit hook
|
|
3436
|
+
# Installed by: pretense install --mode pre-commit
|
|
3437
|
+
|
|
3438
|
+
pretense scan pre-commit
|
|
3439
|
+
exit $?
|
|
3440
|
+
`,
|
|
3441
|
+
"pre-push": `#!/bin/sh
|
|
3442
|
+
# Pretense AI Firewall \u2014 pre-push hook
|
|
3443
|
+
# Installed by: pretense install --mode pre-push
|
|
3444
|
+
|
|
3445
|
+
pretense scan pre-push
|
|
3446
|
+
exit $?
|
|
3447
|
+
`
|
|
3448
|
+
};
|
|
3449
|
+
var VALID_MODES = ["pre-commit", "pre-push"];
|
|
3450
|
+
function findGitDir(cwd) {
|
|
3451
|
+
const gitDir = join5(cwd, ".git");
|
|
3452
|
+
if (existsSync7(gitDir)) return gitDir;
|
|
3453
|
+
return null;
|
|
3454
|
+
}
|
|
3455
|
+
function installHook(hooksDir, mode, force) {
|
|
3456
|
+
const hookPath = join5(hooksDir, mode);
|
|
3457
|
+
const script = HOOK_SCRIPTS[mode];
|
|
3458
|
+
if (existsSync7(hookPath) && !force) {
|
|
3459
|
+
const existing = readFileSync7(hookPath, "utf-8");
|
|
3460
|
+
if (existing.includes("Pretense AI Firewall")) {
|
|
3461
|
+
return { installed: false, path: hookPath, skipped: true };
|
|
3462
|
+
}
|
|
3463
|
+
return { installed: false, path: hookPath };
|
|
3464
|
+
}
|
|
3465
|
+
writeFileSync5(hookPath, script, "utf-8");
|
|
3466
|
+
chmodSync2(hookPath, 493);
|
|
3467
|
+
return { installed: true, path: hookPath };
|
|
3468
|
+
}
|
|
3469
|
+
function installCommand() {
|
|
3470
|
+
return new Command("install").description("Install Pretense git hooks (pre-commit and/or pre-push)").option("-m, --mode <mode>", "Hook mode: pre-commit, pre-push (omit to install both)").option("-f, --force", "Overwrite existing hooks without prompting", false).option("-d, --dir <path>", "Project directory (defaults to cwd)", ".").action((opts) => {
|
|
3471
|
+
const dir = resolve2(opts.dir);
|
|
3472
|
+
const force = opts.force;
|
|
3473
|
+
const modeArg = opts.mode;
|
|
3474
|
+
if (modeArg && !VALID_MODES.includes(modeArg)) {
|
|
3475
|
+
console.error(
|
|
3476
|
+
chalk9.red(`
|
|
3477
|
+
Error: Invalid mode "${modeArg}". Use: ${VALID_MODES.join(", ")}
|
|
3478
|
+
`)
|
|
3479
|
+
);
|
|
3480
|
+
process.exit(1);
|
|
3481
|
+
}
|
|
3482
|
+
const modes = modeArg ? [modeArg] : [...VALID_MODES];
|
|
3483
|
+
const gitDir = findGitDir(dir);
|
|
3484
|
+
if (!gitDir) {
|
|
3485
|
+
console.error(
|
|
3486
|
+
chalk9.red(`
|
|
3487
|
+
Error: No .git directory found in ${dir}
|
|
3488
|
+
Run this command from a git repository root.
|
|
3489
|
+
`)
|
|
3490
|
+
);
|
|
3491
|
+
process.exit(1);
|
|
3492
|
+
}
|
|
3493
|
+
const hooksDir = join5(gitDir, "hooks");
|
|
3494
|
+
mkdirSync6(hooksDir, { recursive: true });
|
|
3495
|
+
console.log(chalk9.cyan("\n Pretense hook installer\n"));
|
|
3496
|
+
let hasError = false;
|
|
3497
|
+
for (const mode of modes) {
|
|
3498
|
+
const result = installHook(hooksDir, mode, force);
|
|
3499
|
+
if (result.installed) {
|
|
3500
|
+
console.log(chalk9.green(` Installed ${mode} hook at ${result.path}`));
|
|
3501
|
+
} else if (result.skipped) {
|
|
3502
|
+
console.log(chalk9.dim(` Pretense ${mode} hook already installed at ${result.path}`));
|
|
3503
|
+
} else {
|
|
3504
|
+
console.error(chalk9.yellow(` Hook already exists at ${result.path}. Use --force to overwrite.`));
|
|
3505
|
+
hasError = true;
|
|
3506
|
+
}
|
|
3507
|
+
}
|
|
3508
|
+
if (hasError) {
|
|
3509
|
+
console.log("");
|
|
3510
|
+
process.exit(1);
|
|
3511
|
+
}
|
|
3512
|
+
console.log(chalk9.bold.green("\n Git hooks installed successfully.\n"));
|
|
3513
|
+
});
|
|
3514
|
+
}
|
|
3515
|
+
|
|
3343
3516
|
// src/index.ts
|
|
3344
3517
|
{
|
|
3345
3518
|
const major = parseInt(process.versions.node.split(".")[0], 10);
|
|
@@ -3476,8 +3649,8 @@ function validateLanguage(lang) {
|
|
|
3476
3649
|
return SUPPORTED_LANGUAGES.includes(resolved) ? resolved : null;
|
|
3477
3650
|
}
|
|
3478
3651
|
function collectFiles(target) {
|
|
3479
|
-
const abs =
|
|
3480
|
-
if (!
|
|
3652
|
+
const abs = resolve3(target);
|
|
3653
|
+
if (!existsSync8(abs)) {
|
|
3481
3654
|
return [];
|
|
3482
3655
|
}
|
|
3483
3656
|
const stat = statSync(abs);
|
|
@@ -3495,13 +3668,14 @@ function collectFiles(target) {
|
|
|
3495
3668
|
".git",
|
|
3496
3669
|
"dist",
|
|
3497
3670
|
"build",
|
|
3671
|
+
".pretest",
|
|
3498
3672
|
".pretense",
|
|
3499
3673
|
".next",
|
|
3500
3674
|
"__pycache__",
|
|
3501
3675
|
".Trash"
|
|
3502
3676
|
]);
|
|
3503
3677
|
for (const entry of entries) {
|
|
3504
|
-
const fullPath =
|
|
3678
|
+
const fullPath = join6(dir, entry.name);
|
|
3505
3679
|
if (entry.isDirectory()) {
|
|
3506
3680
|
if (skipDirs.has(entry.name)) continue;
|
|
3507
3681
|
walk(fullPath);
|
|
@@ -3513,13 +3687,13 @@ function collectFiles(target) {
|
|
|
3513
3687
|
walk(abs);
|
|
3514
3688
|
return files;
|
|
3515
3689
|
}
|
|
3516
|
-
var program = new
|
|
3690
|
+
var program = new Command2();
|
|
3517
3691
|
program.name("pretense").description("AI firewall CLI \u2014 mutates proprietary code before LLM API calls").version(VERSION).hook("preAction", () => {
|
|
3518
3692
|
printBanner();
|
|
3519
3693
|
});
|
|
3520
3694
|
program.command("init").description("Initialize .pretense/ config in current directory").option("-d, --dir <path>", "Target directory", process.cwd()).action((opts) => {
|
|
3521
|
-
const dir =
|
|
3522
|
-
console.log(
|
|
3695
|
+
const dir = resolve3(opts.dir);
|
|
3696
|
+
console.log(chalk10.cyan("Initializing Pretense in"), chalk10.bold(dir));
|
|
3523
3697
|
const configDir = initConfig(dir);
|
|
3524
3698
|
const files = collectFiles(dir);
|
|
3525
3699
|
const langCounts = {};
|
|
@@ -3527,21 +3701,21 @@ program.command("init").description("Initialize .pretense/ config in current dir
|
|
|
3527
3701
|
const lang = langFromExt(f);
|
|
3528
3702
|
langCounts[lang] = (langCounts[lang] ?? 0) + 1;
|
|
3529
3703
|
}
|
|
3530
|
-
console.log(
|
|
3531
|
-
console.log(
|
|
3704
|
+
console.log(chalk10.green("\n .pretense/ created at"), chalk10.bold(configDir));
|
|
3705
|
+
console.log(chalk10.gray(`
|
|
3532
3706
|
Scanned ${files.length} source files:`));
|
|
3533
3707
|
for (const [lang, count] of Object.entries(langCounts)) {
|
|
3534
|
-
console.log(
|
|
3708
|
+
console.log(chalk10.gray(` ${lang}: ${count} files`));
|
|
3535
3709
|
}
|
|
3536
|
-
console.log(
|
|
3710
|
+
console.log(chalk10.cyan("\n Next: run"), chalk10.bold("pretense start"), chalk10.cyan("to launch the proxy"));
|
|
3537
3711
|
console.log();
|
|
3538
3712
|
});
|
|
3539
3713
|
program.command("start").description("Start the Pretense proxy server").option("-p, --port <number>", "Port number", "9339").option("-v, --verbose", "Enable verbose logging", false).action(async (opts) => {
|
|
3540
3714
|
const config = loadConfig();
|
|
3541
3715
|
const port = validatePort(opts.port);
|
|
3542
3716
|
if (port === null) {
|
|
3543
|
-
console.error(
|
|
3544
|
-
console.error(
|
|
3717
|
+
console.error(chalk10.red("Error: Invalid port number:"), chalk10.bold(opts.port));
|
|
3718
|
+
console.error(chalk10.gray("Port must be an integer between 1 and 65535."));
|
|
3545
3719
|
process.exit(1);
|
|
3546
3720
|
}
|
|
3547
3721
|
await ensureDashboardKeyForStart();
|
|
@@ -3654,30 +3828,30 @@ function parseSizeOption(value) {
|
|
|
3654
3828
|
return Math.floor(n * mult);
|
|
3655
3829
|
}
|
|
3656
3830
|
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) => {
|
|
3657
|
-
const absTarget =
|
|
3658
|
-
if (!
|
|
3659
|
-
console.error(
|
|
3831
|
+
const absTarget = resolve3(target);
|
|
3832
|
+
if (!existsSync8(absTarget)) {
|
|
3833
|
+
console.error(chalk10.red("Error: Path not found:"), chalk10.bold(absTarget));
|
|
3660
3834
|
process.exit(1);
|
|
3661
3835
|
}
|
|
3662
3836
|
const maxFileSize = parseSizeOption(opts.maxFileSize);
|
|
3663
3837
|
if (maxFileSize === null) {
|
|
3664
|
-
console.error(
|
|
3665
|
-
console.error(
|
|
3838
|
+
console.error(chalk10.red("Error: Invalid --max-file-size value:"), chalk10.bold(opts.maxFileSize));
|
|
3839
|
+
console.error(chalk10.gray("Use bytes or a unit suffix, e.g. 10mb, 500k, 1048576"));
|
|
3666
3840
|
process.exit(1);
|
|
3667
3841
|
}
|
|
3668
3842
|
const concurrency = Math.max(1, parseInt(opts.concurrency, 10) || SCAN_CONCURRENCY);
|
|
3669
3843
|
if (!opts.quiet && !opts.json) {
|
|
3670
|
-
process.stderr.write(
|
|
3844
|
+
process.stderr.write(chalk10.gray(`Discovering files in ${absTarget}...
|
|
3671
3845
|
`));
|
|
3672
3846
|
}
|
|
3673
3847
|
const files = collectFiles(target);
|
|
3674
3848
|
if (files.length === 0) {
|
|
3675
|
-
console.error(
|
|
3676
|
-
console.error(
|
|
3849
|
+
console.error(chalk10.red("Error: No source files found at"), chalk10.bold(absTarget));
|
|
3850
|
+
console.error(chalk10.gray("Supported extensions: " + [...SUPPORTED_EXTENSIONS].join(", ")));
|
|
3677
3851
|
process.exit(1);
|
|
3678
3852
|
}
|
|
3679
3853
|
if (!opts.quiet && !opts.json) {
|
|
3680
|
-
process.stderr.write(
|
|
3854
|
+
process.stderr.write(chalk10.gray(`Found ${files.length} candidate files. Scanning with concurrency=${concurrency}...
|
|
3681
3855
|
`));
|
|
3682
3856
|
}
|
|
3683
3857
|
const startedAt = Date.now();
|
|
@@ -3703,34 +3877,34 @@ program.command("scan <target>").description("Scan file or directory for identif
|
|
|
3703
3877
|
interrupted
|
|
3704
3878
|
}, null, 2));
|
|
3705
3879
|
} else {
|
|
3706
|
-
console.log(
|
|
3880
|
+
console.log(chalk10.cyan("\nPretense Scan Results\n"));
|
|
3707
3881
|
for (const r of allResults) {
|
|
3708
3882
|
if (r.skipped) {
|
|
3709
3883
|
if (r.skipped === "too-large") {
|
|
3710
3884
|
const mb = ((r.sizeBytes ?? 0) / 1024 / 1024).toFixed(1);
|
|
3711
|
-
console.log(
|
|
3885
|
+
console.log(chalk10.gray(` ${r.file} \u2014 skipped (${mb} MB > limit)`));
|
|
3712
3886
|
}
|
|
3713
3887
|
continue;
|
|
3714
3888
|
}
|
|
3715
|
-
const secretBadge = r.secrets > 0 ?
|
|
3889
|
+
const secretBadge = r.secrets > 0 ? chalk10.red(` [${r.secrets} SECRET${r.secrets > 1 ? "S" : ""}]`) : "";
|
|
3716
3890
|
const showRow = r.secrets > 0 || !opts.secretsOnly && r.identifiers > 0;
|
|
3717
3891
|
if (!showRow) continue;
|
|
3718
3892
|
console.log(
|
|
3719
|
-
|
|
3893
|
+
chalk10.gray(" ") + chalk10.white(r.file) + chalk10.gray(` \u2014 ${r.identifiers} identifiers`) + secretBadge
|
|
3720
3894
|
);
|
|
3721
3895
|
if (r.secrets > 0) {
|
|
3722
3896
|
for (const t of r.secretTypes) {
|
|
3723
|
-
console.log(
|
|
3897
|
+
console.log(chalk10.red(` ! ${t}`));
|
|
3724
3898
|
}
|
|
3725
3899
|
}
|
|
3726
3900
|
}
|
|
3727
|
-
const skipNote = skippedLarge + skippedBinary > 0 ?
|
|
3901
|
+
const skipNote = skippedLarge + skippedBinary > 0 ? chalk10.gray(` (skipped ${skippedLarge} large, ${skippedBinary} binary)`) : "";
|
|
3728
3902
|
console.log(
|
|
3729
|
-
|
|
3730
|
-
Total: ${allResults.length} files in ${(elapsedMs / 1e3).toFixed(2)}s, ${totalIdentifiers} identifiers, `) + (totalSecrets > 0 ?
|
|
3903
|
+
chalk10.gray(`
|
|
3904
|
+
Total: ${allResults.length} files in ${(elapsedMs / 1e3).toFixed(2)}s, ${totalIdentifiers} identifiers, `) + (totalSecrets > 0 ? chalk10.red(`${totalSecrets} secrets found`) : chalk10.green("0 secrets")) + skipNote
|
|
3731
3905
|
);
|
|
3732
3906
|
if (interrupted) {
|
|
3733
|
-
console.log(
|
|
3907
|
+
console.log(chalk10.yellow("\n Interrupted \u2014 partial results shown above."));
|
|
3734
3908
|
}
|
|
3735
3909
|
console.log();
|
|
3736
3910
|
}
|
|
@@ -3745,34 +3919,34 @@ program.command("scan <target>").description("Scan file or directory for identif
|
|
|
3745
3919
|
}
|
|
3746
3920
|
});
|
|
3747
3921
|
program.command("mutate <file>").description("Mutate identifiers for stdout (scanner redacts secrets/PII in the source first)").option("-s, --seed <seed>", "Mutation seed", "pretense").option("-l, --language <lang>", "Source language (typescript, javascript, python, go, java, csharp, ruby, rust)").option("--save", "Save mutation map to .pretense/", false).option("--json", "Output as JSON (includes map + stats)", false).action((file, opts) => {
|
|
3748
|
-
const absPath =
|
|
3749
|
-
if (!
|
|
3750
|
-
console.error(
|
|
3922
|
+
const absPath = resolve3(file);
|
|
3923
|
+
if (!existsSync8(absPath)) {
|
|
3924
|
+
console.error(chalk10.red("Error: File not found:"), chalk10.bold(absPath));
|
|
3751
3925
|
process.exit(1);
|
|
3752
3926
|
}
|
|
3753
3927
|
if (opts.language) {
|
|
3754
3928
|
const validLang = validateLanguage(opts.language);
|
|
3755
3929
|
if (!validLang) {
|
|
3756
|
-
console.error(
|
|
3757
|
-
console.error(
|
|
3930
|
+
console.error(chalk10.red("Error: Unsupported language:"), chalk10.bold(opts.language));
|
|
3931
|
+
console.error(chalk10.gray("Supported languages: " + SUPPORTED_LANGUAGES.join(", ")));
|
|
3758
3932
|
process.exit(1);
|
|
3759
3933
|
}
|
|
3760
3934
|
}
|
|
3761
3935
|
if (!SUPPORTED_EXTENSIONS.has(extname(absPath).toLowerCase()) && isBinaryFile(absPath)) {
|
|
3762
|
-
console.error(
|
|
3763
|
-
console.error(
|
|
3936
|
+
console.error(chalk10.red("Error: Not a supported source file:"), chalk10.bold(absPath));
|
|
3937
|
+
console.error(chalk10.gray("Supported extensions: " + [...SUPPORTED_EXTENSIONS].join(", ")));
|
|
3764
3938
|
process.exit(1);
|
|
3765
3939
|
}
|
|
3766
|
-
const code =
|
|
3940
|
+
const code = readFileSync8(absPath, "utf-8");
|
|
3767
3941
|
const lang = opts.language ? validateLanguage(opts.language) : langFromExt(absPath);
|
|
3768
3942
|
const secretScan = scan2(code);
|
|
3769
|
-
const effectiveSeed =
|
|
3943
|
+
const effectiveSeed = effectiveSeedForMutation(opts.seed);
|
|
3770
3944
|
const { redactedCode: redacted, secretsMap } = applyRedactionsTracked(code, secretScan.matches, effectiveSeed);
|
|
3771
3945
|
const secretsRedacted = secretsMap.size;
|
|
3772
3946
|
const result = mutate(redacted, lang, opts.seed);
|
|
3773
3947
|
if (opts.save) {
|
|
3774
3948
|
const configDir = getConfigDir();
|
|
3775
|
-
const store = new MutationStore(
|
|
3949
|
+
const store = new MutationStore(join6(configDir, "mutation-map.json"));
|
|
3776
3950
|
store.load();
|
|
3777
3951
|
store.save({
|
|
3778
3952
|
id: absPath,
|
|
@@ -3783,7 +3957,7 @@ program.command("mutate <file>").description("Mutate identifiers for stdout (sca
|
|
|
3783
3957
|
language: lang
|
|
3784
3958
|
});
|
|
3785
3959
|
store.persist();
|
|
3786
|
-
process.stderr.write(
|
|
3960
|
+
process.stderr.write(chalk10.gray(`Mutation map saved to ${configDir}/mutation-map.json
|
|
3787
3961
|
`));
|
|
3788
3962
|
}
|
|
3789
3963
|
if (opts.json) {
|
|
@@ -3798,27 +3972,27 @@ program.command("mutate <file>").description("Mutate identifiers for stdout (sca
|
|
|
3798
3972
|
process.stdout.write(result.mutatedCode);
|
|
3799
3973
|
if (secretsRedacted > 0) {
|
|
3800
3974
|
process.stderr.write(
|
|
3801
|
-
|
|
3975
|
+
chalk10.gray(`--- ${secretsRedacted} secret/PII span(s) redacted before code protection ---
|
|
3802
3976
|
`)
|
|
3803
3977
|
);
|
|
3804
3978
|
}
|
|
3805
|
-
process.stderr.write(
|
|
3806
|
-
--- ${result.stats.tokensMutated} identifiers
|
|
3979
|
+
process.stderr.write(chalk10.gray(`
|
|
3980
|
+
--- ${result.stats.tokensMutated} identifiers protected in ${result.stats.durationMs}ms ---
|
|
3807
3981
|
`));
|
|
3808
3982
|
printTokenFooter(1, result.stats.tokensMutated);
|
|
3809
3983
|
}
|
|
3810
3984
|
});
|
|
3811
3985
|
program.command("reverse <file>").description("Reverse mutations using stored map").option("-m, --map <path>", "Path to mutation map JSON file").action((file, opts) => {
|
|
3812
|
-
const absPath =
|
|
3813
|
-
if (!
|
|
3814
|
-
console.error(
|
|
3986
|
+
const absPath = resolve3(file);
|
|
3987
|
+
if (!existsSync8(absPath)) {
|
|
3988
|
+
console.error(chalk10.red("Error: File not found:"), chalk10.bold(absPath));
|
|
3815
3989
|
process.exit(1);
|
|
3816
3990
|
}
|
|
3817
|
-
const mutatedCode =
|
|
3818
|
-
const mapPath = opts.map ?
|
|
3819
|
-
if (!
|
|
3820
|
-
console.error(
|
|
3821
|
-
console.error(
|
|
3991
|
+
const mutatedCode = readFileSync8(absPath, "utf-8");
|
|
3992
|
+
const mapPath = opts.map ? resolve3(opts.map) : join6(getConfigDir(), "mutation-map.json");
|
|
3993
|
+
if (!existsSync8(mapPath)) {
|
|
3994
|
+
console.error(chalk10.red("Error: Mutation map not found:"), chalk10.bold(mapPath));
|
|
3995
|
+
console.error(chalk10.gray("Run 'pretense mutate --save' first, or specify --map <path>"));
|
|
3822
3996
|
process.exit(1);
|
|
3823
3997
|
}
|
|
3824
3998
|
let store;
|
|
@@ -3826,14 +4000,14 @@ program.command("reverse <file>").description("Reverse mutations using stored ma
|
|
|
3826
4000
|
store = new MutationStore(mapPath);
|
|
3827
4001
|
store.load();
|
|
3828
4002
|
} catch {
|
|
3829
|
-
console.error(
|
|
3830
|
-
console.error(
|
|
4003
|
+
console.error(chalk10.red("Error: Failed to load mutation map:"), chalk10.bold(mapPath));
|
|
4004
|
+
console.error(chalk10.gray("The file may be corrupted. Run 'pretense mutate --save' to regenerate."));
|
|
3831
4005
|
process.exit(1);
|
|
3832
4006
|
}
|
|
3833
4007
|
const entry = store.get(absPath) ?? store.getByHash(absPath) ?? store.list(1)[0];
|
|
3834
4008
|
if (!entry) {
|
|
3835
|
-
console.error(
|
|
3836
|
-
console.error(
|
|
4009
|
+
console.error(chalk10.red("Error: No mutation map entries found."));
|
|
4010
|
+
console.error(chalk10.gray("Run 'pretense mutate --save <file>' first to generate a map."));
|
|
3837
4011
|
process.exit(1);
|
|
3838
4012
|
}
|
|
3839
4013
|
const map = MutationStore.toMap(entry);
|
|
@@ -3842,8 +4016,8 @@ program.command("reverse <file>").description("Reverse mutations using stored ma
|
|
|
3842
4016
|
process.stdout.write(restored);
|
|
3843
4017
|
const secretsCount = secretsMap?.size ?? 0;
|
|
3844
4018
|
const secretsSuffix = secretsCount > 0 ? ` + ${secretsCount} secret(s)` : "";
|
|
3845
|
-
process.stderr.write(
|
|
3846
|
-
--- Reversed ${map.size}
|
|
4019
|
+
process.stderr.write(chalk10.gray(`
|
|
4020
|
+
--- Reversed ${map.size} security tokens${secretsSuffix} ---
|
|
3847
4021
|
`));
|
|
3848
4022
|
});
|
|
3849
4023
|
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) => {
|
|
@@ -3862,7 +4036,7 @@ program.command("status").description("Show current plan, monthly quota, seat us
|
|
|
3862
4036
|
program.command("usage").description("Alias for `pretense status` \u2014 show monthly quota usage").action(async () => {
|
|
3863
4037
|
await runStatus();
|
|
3864
4038
|
});
|
|
3865
|
-
program.command("tokens").description("Alias for `pretense credits` \u2014 show remaining
|
|
4039
|
+
program.command("tokens").description("Alias for `pretense credits` \u2014 show remaining security token budget").action(async () => {
|
|
3866
4040
|
await runCredits();
|
|
3867
4041
|
});
|
|
3868
4042
|
program.command("upgrade").description("Compare plans and upgrade to Pro or Enterprise").action(() => {
|
|
@@ -3871,19 +4045,20 @@ program.command("upgrade").description("Compare plans and upgrade to Pro or Ente
|
|
|
3871
4045
|
program.command("credits").description("Show remaining mutation/scan budget for the current month").action(async () => {
|
|
3872
4046
|
await runCredits();
|
|
3873
4047
|
});
|
|
3874
|
-
program.command("login").description("Save a
|
|
4048
|
+
program.command("login").description("Save a dashboard API key to ~/.pretense/config.json for future CLI runs").option("-k, --key <key>", "API key (prtns_live_... or pk_live_...). If omitted, read from $PRETENSE_API_KEY or stdin.").action((opts) => {
|
|
3875
4049
|
const code = runLogin({ key: opts.key });
|
|
3876
4050
|
if (code !== 0) process.exit(code);
|
|
3877
4051
|
});
|
|
3878
|
-
program.command("logout").description("Remove the saved
|
|
4052
|
+
program.command("logout").description("Remove the saved dashboard API key from ~/.pretense/config.json").option("-y, --yes", "Skip confirmation prompt", false).action(async (opts) => {
|
|
3879
4053
|
const code = await runLogout({ assumeYes: opts.yes === true });
|
|
3880
4054
|
if (code !== 0) process.exit(code);
|
|
3881
4055
|
});
|
|
4056
|
+
program.addCommand(installCommand());
|
|
3882
4057
|
function handleFatalError(err) {
|
|
3883
4058
|
if (err instanceof Error && err.message) {
|
|
3884
|
-
console.error(
|
|
4059
|
+
console.error(chalk10.red("Error:"), err.message);
|
|
3885
4060
|
} else {
|
|
3886
|
-
console.error(
|
|
4061
|
+
console.error(chalk10.red("An unexpected error occurred."));
|
|
3887
4062
|
}
|
|
3888
4063
|
process.exit(1);
|
|
3889
4064
|
}
|