shrinker-ai 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +449 -363
- package/dist/src/cli.js +144 -25
- package/dist/src/config.js +42 -0
- package/dist/src/execution/run-command.js +2 -2
- package/dist/src/filters/select-filter.js +26 -18
- package/dist/src/metrics/coverage.js +67 -0
- package/dist/src/metrics/dashboard-template.generated.js +4 -0
- package/dist/src/metrics/dashboard.js +131 -116
- package/dist/src/metrics/stats-store.js +211 -51
- package/integrations/macos/install.sh +54 -1
- package/integrations/macos/shrinker-profile.zsh +165 -98
- package/integrations/macos/uninstall.sh +50 -0
- package/integrations/windows/install.ps1 +161 -113
- package/integrations/windows/shrinker-profile.ps1 +206 -136
- package/integrations/windows/uninstall.ps1 +101 -57
- package/package.json +46 -39
- package/templates/agent-rules.md +18 -18
- package/.copilot-instructions.md +0 -28
- package/CLAUDE.md +0 -28
package/dist/src/cli.js
CHANGED
|
@@ -1,32 +1,37 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import process from "node:process";
|
|
3
2
|
import path from "node:path";
|
|
3
|
+
import process from "node:process";
|
|
4
4
|
import { applyFilter } from "./filters/select-filter.js";
|
|
5
5
|
import { runCommand } from "./execution/run-command.js";
|
|
6
6
|
import { getLatestRawOutput, getRawOutput, saveRawOutput } from "./execution/raw-output-store.js";
|
|
7
7
|
import { cleanText } from "./formatting/ansi.js";
|
|
8
8
|
import { formatMeasurements, measure } from "./metrics/measure.js";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { serveStatsDashboard, startStatsDashboard, writeStatsDashboard } from "./metrics/dashboard.js";
|
|
10
|
+
import { classifyWrappedRun, commandSignature, isCoverageTrackingEnabled } from "./metrics/coverage.js";
|
|
11
|
+
import { defaultStatsPath, formatCoverage, formatStats, formatStatsChart, getStats, recordRun, recordUncovered, } from "./metrics/stats-store.js";
|
|
11
12
|
function usage() {
|
|
12
|
-
return `Usage:
|
|
13
|
-
shrinker <command> [args...]
|
|
14
|
-
shrinker exec [options] [--] <command> [args...]
|
|
15
|
-
shrinker pipe [options]
|
|
16
|
-
shrinker stats [--json] [--chart] [--dashboard]
|
|
17
|
-
shrinker last [--path]
|
|
18
|
-
shrinker raw <capture-id> [--path]
|
|
19
|
-
shrinker
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
--
|
|
24
|
-
--
|
|
25
|
-
--
|
|
26
|
-
--
|
|
27
|
-
--
|
|
28
|
-
--no-
|
|
29
|
-
--
|
|
13
|
+
return `Usage:
|
|
14
|
+
shrinker <command> [args...]
|
|
15
|
+
shrinker exec [options] [--] <command> [args...]
|
|
16
|
+
shrinker pipe [options]
|
|
17
|
+
shrinker stats [--json] [--chart] [--coverage] [--dashboard] [--restart] [--port <number>]
|
|
18
|
+
shrinker last [--path]
|
|
19
|
+
shrinker raw <capture-id> [--path]
|
|
20
|
+
shrinker track --executable <name> [--subcommand <name>] [--bytes <number>] [--exit-code <number>]
|
|
21
|
+
shrinker help
|
|
22
|
+
|
|
23
|
+
Options:
|
|
24
|
+
--kind <auto|git-status|git-diff|git-log|git-list|npm|tail|find|rg|docker|kubectl|cat|gh|test|log>
|
|
25
|
+
--max-lines <number> default: 120
|
|
26
|
+
--per-file-lines <number> default: 40
|
|
27
|
+
--raw bypass filtering
|
|
28
|
+
--metrics print per-run savings and duration
|
|
29
|
+
--no-save do not save omitted raw output
|
|
30
|
+
--no-stats do not record this run
|
|
31
|
+
--coverage list commands shrinker does not cover yet
|
|
32
|
+
--dashboard serve and open the local dashboard at http://127.0.0.1:4317
|
|
33
|
+
--restart restart the local dashboard server
|
|
34
|
+
--port <number> dashboard server port (default: 4317)
|
|
30
35
|
--help`;
|
|
31
36
|
}
|
|
32
37
|
function parsePositiveInteger(value, option) {
|
|
@@ -36,6 +41,20 @@ function parsePositiveInteger(value, option) {
|
|
|
36
41
|
}
|
|
37
42
|
return parsed;
|
|
38
43
|
}
|
|
44
|
+
function parseNonNegativeInteger(value, option) {
|
|
45
|
+
const parsed = Number(value);
|
|
46
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
47
|
+
throw new Error(`${option} requires a non-negative integer`);
|
|
48
|
+
}
|
|
49
|
+
return parsed;
|
|
50
|
+
}
|
|
51
|
+
function parseInteger(value, option) {
|
|
52
|
+
const parsed = Number(value);
|
|
53
|
+
if (!Number.isInteger(parsed)) {
|
|
54
|
+
throw new Error(`${option} requires an integer`);
|
|
55
|
+
}
|
|
56
|
+
return parsed;
|
|
57
|
+
}
|
|
39
58
|
function parseArgs(args) {
|
|
40
59
|
const first = args[0];
|
|
41
60
|
let mode;
|
|
@@ -48,6 +67,7 @@ function parseArgs(args) {
|
|
|
48
67
|
first === "pipe" ||
|
|
49
68
|
first === "stats" ||
|
|
50
69
|
first === "last" ||
|
|
70
|
+
first === "track" ||
|
|
51
71
|
first === "raw") {
|
|
52
72
|
const reserved = args.shift();
|
|
53
73
|
mode = reserved === "raw" ? "raw-output" : reserved;
|
|
@@ -62,9 +82,17 @@ function parseArgs(args) {
|
|
|
62
82
|
let showMetrics = false;
|
|
63
83
|
let json = false;
|
|
64
84
|
let chart = false;
|
|
85
|
+
let coverage = false;
|
|
65
86
|
let dashboard = false;
|
|
87
|
+
let dashboardServer = false;
|
|
88
|
+
let dashboardRestart = false;
|
|
89
|
+
let dashboardPort = 4317;
|
|
66
90
|
let showPath = false;
|
|
67
91
|
let captureId;
|
|
92
|
+
let trackExecutable;
|
|
93
|
+
let trackSubcommand;
|
|
94
|
+
let trackBytes;
|
|
95
|
+
let trackExitCode;
|
|
68
96
|
let maxLines = 120;
|
|
69
97
|
let perFileLines = 40;
|
|
70
98
|
while (args.length > 0 && args[0] !== "--") {
|
|
@@ -85,8 +113,24 @@ function parseArgs(args) {
|
|
|
85
113
|
json = true;
|
|
86
114
|
else if (option === "--chart" && mode === "stats")
|
|
87
115
|
chart = true;
|
|
116
|
+
else if (option === "--coverage" && mode === "stats")
|
|
117
|
+
coverage = true;
|
|
118
|
+
else if (option === "--executable" && mode === "track")
|
|
119
|
+
trackExecutable = args.shift();
|
|
120
|
+
else if (option === "--subcommand" && mode === "track")
|
|
121
|
+
trackSubcommand = args.shift();
|
|
122
|
+
else if (option === "--bytes" && mode === "track")
|
|
123
|
+
trackBytes = parseNonNegativeInteger(args.shift(), "--bytes");
|
|
124
|
+
else if (option === "--exit-code" && mode === "track")
|
|
125
|
+
trackExitCode = parseInteger(args.shift(), "--exit-code");
|
|
88
126
|
else if (option === "--dashboard" && mode === "stats")
|
|
89
127
|
dashboard = true;
|
|
128
|
+
else if (option === "--dashboard-server" && mode === "stats")
|
|
129
|
+
dashboardServer = true;
|
|
130
|
+
else if (option === "--restart" && mode === "stats")
|
|
131
|
+
dashboardRestart = true;
|
|
132
|
+
else if (option === "--port" && mode === "stats")
|
|
133
|
+
dashboardPort = parsePositiveInteger(args.shift(), "--port");
|
|
90
134
|
else if (option === "--path" && mode === "last")
|
|
91
135
|
showPath = true;
|
|
92
136
|
else if (option === "--path" && mode === "raw-output")
|
|
@@ -138,10 +182,16 @@ function parseArgs(args) {
|
|
|
138
182
|
throw new Error("exec requires a command");
|
|
139
183
|
if (mode === "stats" && args.length > 0)
|
|
140
184
|
throw new Error("stats does not accept command arguments");
|
|
185
|
+
if (dashboardRestart && !dashboard)
|
|
186
|
+
throw new Error("--restart requires stats --dashboard");
|
|
141
187
|
if (mode === "last" && args.length > 0)
|
|
142
188
|
throw new Error("last does not accept command arguments");
|
|
143
189
|
if (mode === "raw-output" && !captureId)
|
|
144
190
|
throw new Error("raw requires a capture ID");
|
|
191
|
+
if (mode === "track" && !trackExecutable)
|
|
192
|
+
throw new Error("track requires --executable");
|
|
193
|
+
if (mode === "track" && args.length > 0)
|
|
194
|
+
throw new Error("track does not accept command arguments");
|
|
145
195
|
return {
|
|
146
196
|
mode,
|
|
147
197
|
kind,
|
|
@@ -151,9 +201,17 @@ function parseArgs(args) {
|
|
|
151
201
|
showMetrics,
|
|
152
202
|
json,
|
|
153
203
|
chart,
|
|
204
|
+
coverage,
|
|
154
205
|
dashboard,
|
|
206
|
+
dashboardServer,
|
|
207
|
+
dashboardRestart,
|
|
208
|
+
dashboardPort,
|
|
155
209
|
showPath,
|
|
156
210
|
...(captureId ? { captureId } : {}),
|
|
211
|
+
...(trackExecutable ? { trackExecutable } : {}),
|
|
212
|
+
...(trackSubcommand ? { trackSubcommand } : {}),
|
|
213
|
+
...(trackBytes === undefined ? {} : { trackBytes }),
|
|
214
|
+
...(trackExitCode === undefined ? {} : { trackExitCode }),
|
|
157
215
|
maxLines,
|
|
158
216
|
perFileLines,
|
|
159
217
|
command: args,
|
|
@@ -187,11 +245,13 @@ async function render(rawOutput, options, durationMs, exitCode) {
|
|
|
187
245
|
process.stderr.write(`${formatMeasurements(measurements, durationMs)}\n`);
|
|
188
246
|
}
|
|
189
247
|
if (options.trackStats) {
|
|
248
|
+
const signature = options.mode === "pipe" ? undefined : commandSignature(options.command);
|
|
190
249
|
try {
|
|
191
250
|
recordRun({
|
|
192
251
|
mode: options.mode === "pipe" ? "pipe" : "exec",
|
|
193
252
|
filterKind: result.kind,
|
|
194
253
|
commandName: options.mode === "pipe" ? "stdin" : path.basename(options.command[0] ?? "unknown"),
|
|
254
|
+
...(signature?.subcommand ? { commandSubcommand: signature.subcommand } : {}),
|
|
195
255
|
measurements,
|
|
196
256
|
...(durationMs === undefined ? {} : { durationMs }),
|
|
197
257
|
omitted: result.omitted,
|
|
@@ -201,6 +261,29 @@ async function render(rawOutput, options, durationMs, exitCode) {
|
|
|
201
261
|
catch (error) {
|
|
202
262
|
process.stderr.write(`[shrinker] could not record stats: ${String(error)}\n`);
|
|
203
263
|
}
|
|
264
|
+
if (options.mode !== "pipe" && isCoverageTrackingEnabled()) {
|
|
265
|
+
try {
|
|
266
|
+
const reason = classifyWrappedRun({
|
|
267
|
+
matched: result.matched,
|
|
268
|
+
kind: result.kind,
|
|
269
|
+
measurements,
|
|
270
|
+
});
|
|
271
|
+
if (reason && signature) {
|
|
272
|
+
recordUncovered({
|
|
273
|
+
source: "wrapped",
|
|
274
|
+
reason,
|
|
275
|
+
executable: signature.executable,
|
|
276
|
+
...(signature.subcommand ? { subcommand: signature.subcommand } : {}),
|
|
277
|
+
rawBytes: measurements.rawBytes,
|
|
278
|
+
rawEstimatedTokens: measurements.rawEstimatedTokens,
|
|
279
|
+
...(exitCode === undefined ? {} : { exitCode }),
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
process.stderr.write(`[shrinker] could not record coverage: ${String(error)}\n`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
204
287
|
}
|
|
205
288
|
const isWrappedGitLog = options.mode === "exec" && result.kind === "git-log";
|
|
206
289
|
const shouldSave = result.recovery !== "threshold" && !isWrappedGitLog;
|
|
@@ -223,15 +306,51 @@ async function main() {
|
|
|
223
306
|
if (options.mode === "stats") {
|
|
224
307
|
const summary = getStats(defaultStatsPath());
|
|
225
308
|
if (options.dashboard) {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
309
|
+
if (options.dashboardServer) {
|
|
310
|
+
await serveStatsDashboard(() => getStats(defaultStatsPath()), options.dashboardPort);
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
writeStatsDashboard(summary);
|
|
314
|
+
const dashboard = await startStatsDashboard(options.dashboardPort, options.dashboardRestart);
|
|
315
|
+
if (dashboard.reused) {
|
|
316
|
+
process.stdout.write(`Dashboard server already running at http://127.0.0.1:${options.dashboardPort}\n`);
|
|
317
|
+
}
|
|
318
|
+
else if (dashboard.restarted) {
|
|
319
|
+
process.stdout.write(`Dashboard server restarted at http://127.0.0.1:${options.dashboardPort} (PID ${dashboard.pid})\n`);
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
process.stdout.write(`Dashboard server started at http://127.0.0.1:${options.dashboardPort} (PID ${dashboard.pid})\n`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
229
325
|
return;
|
|
230
326
|
}
|
|
231
|
-
const output = options.json
|
|
327
|
+
const output = options.json
|
|
328
|
+
? JSON.stringify(summary, null, 2)
|
|
329
|
+
: options.coverage
|
|
330
|
+
? formatCoverage(summary)
|
|
331
|
+
: options.chart
|
|
332
|
+
? formatStatsChart(summary)
|
|
333
|
+
: formatStats(summary);
|
|
232
334
|
process.stdout.write(`${output}\n`);
|
|
233
335
|
return;
|
|
234
336
|
}
|
|
337
|
+
if (options.mode === "track") {
|
|
338
|
+
try {
|
|
339
|
+
recordUncovered({
|
|
340
|
+
source: "shell",
|
|
341
|
+
reason: "unlisted-subcommand",
|
|
342
|
+
executable: options.trackExecutable ?? "",
|
|
343
|
+
...(options.trackSubcommand ? { subcommand: options.trackSubcommand } : {}),
|
|
344
|
+
...(options.trackBytes === undefined ? {} : { rawBytes: options.trackBytes }),
|
|
345
|
+
...(options.trackBytes === undefined
|
|
346
|
+
? {}
|
|
347
|
+
: { rawEstimatedTokens: Math.ceil(options.trackBytes / 4) }),
|
|
348
|
+
...(options.trackExitCode === undefined ? {} : { exitCode: options.trackExitCode }),
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
catch { }
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
235
354
|
if (options.mode === "last") {
|
|
236
355
|
const latest = await getLatestRawOutput();
|
|
237
356
|
if (!latest)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
export function defaultConfigPath() {
|
|
5
|
+
return process.env['SHRINKER_CONFIG_PATH'] ?? path.join(os.homedir(), ".shrinker", "config");
|
|
6
|
+
}
|
|
7
|
+
// `KEY=value` lines; `#` starts a comment. Unknown keys are ignored so older CLIs tolerate newer files.
|
|
8
|
+
export function readConfig(configPath = defaultConfigPath()) {
|
|
9
|
+
const settings = new Map();
|
|
10
|
+
let contents;
|
|
11
|
+
try {
|
|
12
|
+
contents = readFileSync(configPath, "utf8");
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return settings;
|
|
16
|
+
}
|
|
17
|
+
for (const line of contents.split(/\r?\n/)) {
|
|
18
|
+
const withoutComment = line.split("#")[0]?.trim();
|
|
19
|
+
if (!withoutComment)
|
|
20
|
+
continue;
|
|
21
|
+
const separator = withoutComment.indexOf("=");
|
|
22
|
+
if (separator <= 0)
|
|
23
|
+
continue;
|
|
24
|
+
const key = withoutComment.slice(0, separator).trim();
|
|
25
|
+
const value = withoutComment.slice(separator + 1).trim();
|
|
26
|
+
if (key)
|
|
27
|
+
settings.set(key, value);
|
|
28
|
+
}
|
|
29
|
+
return settings;
|
|
30
|
+
}
|
|
31
|
+
// Environment wins so a single command can override the persisted choice.
|
|
32
|
+
export function resolveSetting(key, configPath) {
|
|
33
|
+
const fromEnvironment = process.env[key];
|
|
34
|
+
if (fromEnvironment !== undefined && fromEnvironment.trim() !== "")
|
|
35
|
+
return fromEnvironment;
|
|
36
|
+
return readConfig(configPath).get(key);
|
|
37
|
+
}
|
|
38
|
+
export function isTruthy(value) {
|
|
39
|
+
const normalized = value?.trim().toLowerCase();
|
|
40
|
+
return normalized === "1" || normalized === "true" || normalized === "yes";
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -12,9 +12,9 @@ function quoteForCmd(argument) {
|
|
|
12
12
|
async function resolveWindowsCommand(command) {
|
|
13
13
|
if (process.platform !== "win32" || path.extname(command))
|
|
14
14
|
return command;
|
|
15
|
-
const pathEntries = (process.env
|
|
15
|
+
const pathEntries = (process.env['PATH'] ?? "").split(path.delimiter);
|
|
16
16
|
const executableExtensions = new Set([".com", ".exe", ".bat", ".cmd"]);
|
|
17
|
-
const extensions = (process.env
|
|
17
|
+
const extensions = (process.env['PATHEXT'] ?? ".COM;.EXE;.BAT;.CMD")
|
|
18
18
|
.split(";")
|
|
19
19
|
.map((extension) => extension.trim().toLowerCase())
|
|
20
20
|
.filter((extension) => executableExtensions.has(extension));
|
|
@@ -31,41 +31,45 @@ const FILTERS = {
|
|
|
31
31
|
log: filterGenericLog,
|
|
32
32
|
};
|
|
33
33
|
export function detectFilter(command) {
|
|
34
|
+
return detectFilterMatch(command).kind;
|
|
35
|
+
}
|
|
36
|
+
export function detectFilterMatch(command) {
|
|
34
37
|
const executable = detectExecutable(command);
|
|
38
|
+
const matched = (kind) => ({ kind, matched: true });
|
|
35
39
|
const gitSubcommand = detectGitSubcommand(command);
|
|
36
40
|
if (gitSubcommand === "status")
|
|
37
|
-
return "git-status";
|
|
41
|
+
return matched("git-status");
|
|
38
42
|
if (gitSubcommand === "log")
|
|
39
|
-
return "git-log";
|
|
43
|
+
return matched("git-log");
|
|
40
44
|
if (gitSubcommand === "reflog")
|
|
41
|
-
return "git-log";
|
|
45
|
+
return matched("git-log");
|
|
42
46
|
if (gitSubcommand === "diff" || gitSubcommand === "show")
|
|
43
|
-
return "git-diff";
|
|
47
|
+
return matched("git-diff");
|
|
44
48
|
if (gitSubcommand)
|
|
45
|
-
return "git-list";
|
|
49
|
+
return matched("git-list");
|
|
46
50
|
if (executable === "npm" || executable === "pnpm" || executable === "yarn") {
|
|
47
51
|
if (isTestRunnerCommand(command))
|
|
48
|
-
return "test";
|
|
49
|
-
return "npm";
|
|
52
|
+
return matched("test");
|
|
53
|
+
return matched("npm");
|
|
50
54
|
}
|
|
51
55
|
if (executable === "tail")
|
|
52
|
-
return "tail";
|
|
56
|
+
return matched("tail");
|
|
53
57
|
if (executable === "find")
|
|
54
|
-
return "find";
|
|
58
|
+
return matched("find");
|
|
55
59
|
if (executable === "rg" || executable === "ripgrep")
|
|
56
|
-
return "rg";
|
|
60
|
+
return matched("rg");
|
|
57
61
|
if (executable === "docker")
|
|
58
|
-
return "docker";
|
|
62
|
+
return matched("docker");
|
|
59
63
|
if (executable === "kubectl")
|
|
60
|
-
return "kubectl";
|
|
64
|
+
return matched("kubectl");
|
|
61
65
|
if (executable === "cat")
|
|
62
|
-
return "cat";
|
|
66
|
+
return matched("cat");
|
|
63
67
|
if (executable === "gh")
|
|
64
|
-
return "gh";
|
|
68
|
+
return matched("gh");
|
|
65
69
|
if (isTestRunnerCommand(command)) {
|
|
66
|
-
return "test";
|
|
70
|
+
return matched("test");
|
|
67
71
|
}
|
|
68
|
-
return "log";
|
|
72
|
+
return { kind: "log", matched: false };
|
|
69
73
|
}
|
|
70
74
|
function detectExecutable(command) {
|
|
71
75
|
const first = command[0];
|
|
@@ -101,7 +105,10 @@ function detectGitSubcommand(command) {
|
|
|
101
105
|
return undefined;
|
|
102
106
|
}
|
|
103
107
|
export function applyFilter(input, requestedKind, command, options) {
|
|
104
|
-
const
|
|
108
|
+
const detection = requestedKind === "auto"
|
|
109
|
+
? detectFilterMatch(command)
|
|
110
|
+
: { kind: requestedKind, matched: true };
|
|
111
|
+
const kind = detection.kind;
|
|
105
112
|
const result = FILTERS[kind](input, { ...options, command });
|
|
106
113
|
const cleanedRaw = cleanText(input);
|
|
107
114
|
const comparison = measure(cleanedRaw, result.output);
|
|
@@ -110,10 +117,11 @@ export function applyFilter(input, requestedKind, command, options) {
|
|
|
110
117
|
return {
|
|
111
118
|
output: cleanedRaw,
|
|
112
119
|
kind,
|
|
120
|
+
matched: detection.matched,
|
|
113
121
|
omitted: false,
|
|
114
122
|
notes: [...result.notes, "compact output was not smaller; returned cleaned raw output"],
|
|
115
123
|
};
|
|
116
124
|
}
|
|
117
|
-
return result;
|
|
125
|
+
return { ...result, matched: detection.matched };
|
|
118
126
|
}
|
|
119
127
|
//# sourceMappingURL=select-filter.js.map
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { isTruthy, resolveSetting } from "../config.js";
|
|
2
|
+
const DEFAULT_LOW_REDUCTION_PERCENT = 10;
|
|
3
|
+
const MINIMUM_TRACKED_RAW_TOKENS = 200;
|
|
4
|
+
const TOKEN_PATTERN = /^[a-z0-9][a-z0-9._+-]{0,63}$/;
|
|
5
|
+
const OPTION_VALUE_FLAGS = {
|
|
6
|
+
git: ["-C", "-c", "--git-dir", "--work-tree", "--namespace"],
|
|
7
|
+
npm: ["--prefix", "--cache", "--registry", "--workspace", "--userconfig", "-w", "-C"],
|
|
8
|
+
pnpm: ["--prefix", "--registry", "--workspace", "-w", "-C", "--dir"],
|
|
9
|
+
yarn: ["--cwd", "--registry"],
|
|
10
|
+
docker: ["-H", "--host", "--context", "--config"],
|
|
11
|
+
kubectl: ["-n", "--namespace", "-o", "--output", "--context", "--kubeconfig", "--cluster", "--user"],
|
|
12
|
+
gh: ["-R", "--repo"],
|
|
13
|
+
};
|
|
14
|
+
export function isCoverageTrackingEnabled() {
|
|
15
|
+
return isTruthy(resolveSetting("SHRINKER_TRACK_UNCOVERED"));
|
|
16
|
+
}
|
|
17
|
+
function lowReductionPercent() {
|
|
18
|
+
const configured = Number(process.env['SHRINKER_LOW_REDUCTION_PERCENT']);
|
|
19
|
+
return Number.isFinite(configured) && configured >= 0 && configured <= 100
|
|
20
|
+
? configured
|
|
21
|
+
: DEFAULT_LOW_REDUCTION_PERCENT;
|
|
22
|
+
}
|
|
23
|
+
export function sanitizeToken(value) {
|
|
24
|
+
if (!value)
|
|
25
|
+
return undefined;
|
|
26
|
+
const normalized = value.trim().toLowerCase();
|
|
27
|
+
return TOKEN_PATTERN.test(normalized) ? normalized : undefined;
|
|
28
|
+
}
|
|
29
|
+
function normalizeExecutable(value) {
|
|
30
|
+
if (!value)
|
|
31
|
+
return undefined;
|
|
32
|
+
const base = value
|
|
33
|
+
.split(/[\\/]/)
|
|
34
|
+
.pop()
|
|
35
|
+
?.replace(/\.(exe|cmd|bat|ps1)$/i, "");
|
|
36
|
+
return sanitizeToken(base);
|
|
37
|
+
}
|
|
38
|
+
export function commandSignature(command) {
|
|
39
|
+
const executable = normalizeExecutable(command[0]);
|
|
40
|
+
if (!executable)
|
|
41
|
+
return undefined;
|
|
42
|
+
const valueFlags = new Set(OPTION_VALUE_FLAGS[executable] ?? []);
|
|
43
|
+
for (let index = 1; index < command.length; index += 1) {
|
|
44
|
+
const part = command[index];
|
|
45
|
+
if (!part)
|
|
46
|
+
continue;
|
|
47
|
+
if (valueFlags.has(part)) {
|
|
48
|
+
index += 1;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (part.startsWith("-"))
|
|
52
|
+
continue;
|
|
53
|
+
const subcommand = sanitizeToken(part);
|
|
54
|
+
return subcommand ? { executable, subcommand } : { executable };
|
|
55
|
+
}
|
|
56
|
+
return { executable };
|
|
57
|
+
}
|
|
58
|
+
export function classifyWrappedRun(run) {
|
|
59
|
+
if (run.measurements.rawEstimatedTokens < MINIMUM_TRACKED_RAW_TOKENS)
|
|
60
|
+
return undefined;
|
|
61
|
+
if (!run.matched)
|
|
62
|
+
return "no-filter";
|
|
63
|
+
if (run.measurements.reductionPercent < lowReductionPercent())
|
|
64
|
+
return "low-reduction";
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=coverage.js.map
|