shrinker-ai 0.1.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.
Files changed (35) hide show
  1. package/.copilot-instructions.md +2 -0
  2. package/CLAUDE.md +2 -0
  3. package/README.md +295 -0
  4. package/dist/src/cli.js +265 -0
  5. package/dist/src/execution/raw-output-store.js +132 -0
  6. package/dist/src/execution/run-command.js +172 -0
  7. package/dist/src/filters/cat.js +13 -0
  8. package/dist/src/filters/docker.js +42 -0
  9. package/dist/src/filters/find.js +63 -0
  10. package/dist/src/filters/generic-log.js +38 -0
  11. package/dist/src/filters/gh.js +38 -0
  12. package/dist/src/filters/git-diff.js +56 -0
  13. package/dist/src/filters/git-list.js +25 -0
  14. package/dist/src/filters/git-log.js +256 -0
  15. package/dist/src/filters/git-status.js +67 -0
  16. package/dist/src/filters/kubectl.js +76 -0
  17. package/dist/src/filters/npm.js +44 -0
  18. package/dist/src/filters/rg.js +64 -0
  19. package/dist/src/filters/select-filter.js +119 -0
  20. package/dist/src/filters/table.js +27 -0
  21. package/dist/src/filters/tail.js +6 -0
  22. package/dist/src/filters/test-output.js +54 -0
  23. package/dist/src/filters/types.js +2 -0
  24. package/dist/src/formatting/ansi.js +13 -0
  25. package/dist/src/formatting/limits.js +17 -0
  26. package/dist/src/metrics/dashboard.js +138 -0
  27. package/dist/src/metrics/measure.js +29 -0
  28. package/dist/src/metrics/stats-store.js +166 -0
  29. package/integrations/install-shrinker.ps1 +166 -0
  30. package/integrations/install.ps1 +46 -0
  31. package/integrations/shrinker-profile.ps1 +136 -0
  32. package/integrations/uninstall-shrinker.ps1 +79 -0
  33. package/integrations/uninstall.ps1 +35 -0
  34. package/package.json +31 -0
  35. package/templates/agent-rules.md +19 -0
@@ -0,0 +1,172 @@
1
+ import { access } from "node:fs/promises";
2
+ import { readFile, readdir } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { spawn } from "node:child_process";
5
+ function quoteForCmd(argument) {
6
+ if (argument.length === 0)
7
+ return "\"\"";
8
+ if (!/[\s"^&|<>]/.test(argument))
9
+ return argument;
10
+ return `"${argument.replace(/"/g, '""')}"`;
11
+ }
12
+ async function resolveWindowsCommand(command) {
13
+ if (process.platform !== "win32" || path.extname(command))
14
+ return command;
15
+ const pathEntries = (process.env.PATH ?? "").split(path.delimiter);
16
+ const executableExtensions = new Set([".com", ".exe", ".bat", ".cmd"]);
17
+ const extensions = (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD")
18
+ .split(";")
19
+ .map((extension) => extension.trim().toLowerCase())
20
+ .filter((extension) => executableExtensions.has(extension));
21
+ for (const directory of pathEntries) {
22
+ if (!directory)
23
+ continue;
24
+ for (const extension of extensions) {
25
+ const candidate = path.join(directory, `${command}${extension}`);
26
+ try {
27
+ await access(candidate);
28
+ return candidate;
29
+ }
30
+ catch {
31
+ // Continue searching PATH.
32
+ }
33
+ }
34
+ }
35
+ return command;
36
+ }
37
+ function parseAliasArgs(args) {
38
+ const paths = [];
39
+ for (const arg of args) {
40
+ if (arg.startsWith("-"))
41
+ return { paths, unsupportedOption: arg };
42
+ paths.push(arg);
43
+ }
44
+ return { paths };
45
+ }
46
+ async function runWindowsAlias(command, args) {
47
+ if (process.platform !== "win32")
48
+ return undefined;
49
+ const alias = command.toLowerCase();
50
+ if (alias !== "cat" && alias !== "ls" && alias !== "dir")
51
+ return undefined;
52
+ const started = performance.now();
53
+ const parsed = parseAliasArgs(args);
54
+ if (parsed.unsupportedOption) {
55
+ return {
56
+ stdout: "",
57
+ stderr: `Unsupported ${alias} option in shrinker alias mode: ${parsed.unsupportedOption}`,
58
+ combined: `Unsupported ${alias} option in shrinker alias mode: ${parsed.unsupportedOption}`,
59
+ exitCode: 2,
60
+ durationMs: Math.round(performance.now() - started),
61
+ };
62
+ }
63
+ if (alias === "cat") {
64
+ const targets = parsed.paths.length > 0 ? parsed.paths : ["-"];
65
+ if (targets.includes("-")) {
66
+ return {
67
+ stdout: "",
68
+ stderr: "cat alias requires at least one file path",
69
+ combined: "cat alias requires at least one file path",
70
+ exitCode: 2,
71
+ durationMs: Math.round(performance.now() - started),
72
+ };
73
+ }
74
+ const parts = [];
75
+ for (const target of targets) {
76
+ try {
77
+ parts.push(await readFile(target, "utf8"));
78
+ }
79
+ catch (error) {
80
+ const message = error.message;
81
+ return {
82
+ stdout: "",
83
+ stderr: message,
84
+ combined: message,
85
+ exitCode: 1,
86
+ durationMs: Math.round(performance.now() - started),
87
+ };
88
+ }
89
+ }
90
+ const stdout = parts.join(parts.length > 1 ? "\n" : "");
91
+ return {
92
+ stdout,
93
+ stderr: "",
94
+ combined: stdout,
95
+ exitCode: 0,
96
+ durationMs: Math.round(performance.now() - started),
97
+ };
98
+ }
99
+ const targetPath = parsed.paths[0] ?? ".";
100
+ try {
101
+ const entries = await readdir(targetPath, { withFileTypes: true });
102
+ const lines = entries
103
+ .map((entry) => (entry.isDirectory() ? `${entry.name}/` : entry.name))
104
+ .sort((left, right) => left.localeCompare(right));
105
+ const stdout = lines.join("\n");
106
+ return {
107
+ stdout,
108
+ stderr: "",
109
+ combined: stdout,
110
+ exitCode: 0,
111
+ durationMs: Math.round(performance.now() - started),
112
+ };
113
+ }
114
+ catch (error) {
115
+ const message = error.message;
116
+ return {
117
+ stdout: "",
118
+ stderr: message,
119
+ combined: message,
120
+ exitCode: 1,
121
+ durationMs: Math.round(performance.now() - started),
122
+ };
123
+ }
124
+ }
125
+ async function spawnAndCapture(executable, args, viaCmdProxy) {
126
+ const started = performance.now();
127
+ return await new Promise((resolve, reject) => {
128
+ const spawnCommand = viaCmdProxy ? "cmd.exe" : executable;
129
+ const spawnArgs = viaCmdProxy
130
+ ? ["/d", "/s", "/c", `${quoteForCmd(executable)} ${args.map(quoteForCmd).join(" ")}`]
131
+ : args;
132
+ const child = spawn(spawnCommand, spawnArgs, {
133
+ cwd: process.cwd(),
134
+ env: process.env,
135
+ shell: false,
136
+ windowsHide: true,
137
+ });
138
+ const stdout = [];
139
+ const stderr = [];
140
+ child.stdout.on("data", (chunk) => stdout.push(chunk));
141
+ child.stderr.on("data", (chunk) => stderr.push(chunk));
142
+ child.on("error", reject);
143
+ child.on("close", (code) => {
144
+ const stdoutText = Buffer.concat(stdout).toString("utf8");
145
+ const stderrText = Buffer.concat(stderr).toString("utf8");
146
+ resolve({
147
+ stdout: stdoutText,
148
+ stderr: stderrText,
149
+ combined: [stdoutText, stderrText].filter(Boolean).join("\n"),
150
+ exitCode: code ?? 1,
151
+ durationMs: Math.round(performance.now() - started),
152
+ });
153
+ });
154
+ });
155
+ }
156
+ export async function runCommand(command, args) {
157
+ const aliasResult = await runWindowsAlias(command, args);
158
+ if (aliasResult)
159
+ return aliasResult;
160
+ const executable = await resolveWindowsCommand(command);
161
+ try {
162
+ return await spawnAndCapture(executable, args, false);
163
+ }
164
+ catch (error) {
165
+ const code = error.code;
166
+ if (process.platform === "win32" && (code === "EFTYPE" || code === "EINVAL")) {
167
+ return await spawnAndCapture(executable, args, true);
168
+ }
169
+ throw error;
170
+ }
171
+ }
172
+ //# sourceMappingURL=run-command.js.map
@@ -0,0 +1,13 @@
1
+ import { cleanText } from "../formatting/ansi.js";
2
+ import { limitLines } from "../formatting/limits.js";
3
+ export function filterCatOutput(input, options) {
4
+ const lines = cleanText(input).split("\n");
5
+ const limited = limitLines(lines, options.maxLines);
6
+ return {
7
+ output: limited.lines.join("\n"),
8
+ kind: "cat",
9
+ omitted: limited.omitted > 0,
10
+ notes: limited.omitted > 0 ? [`omitted ${limited.omitted} lines`] : [],
11
+ };
12
+ }
13
+ //# sourceMappingURL=cat.js.map
@@ -0,0 +1,42 @@
1
+ import { cleanText } from "../formatting/ansi.js";
2
+ import { filterGenericLog } from "./generic-log.js";
3
+ import { compactTable } from "./table.js";
4
+ function hasOption(command, ...names) {
5
+ return command.some((part) => names.includes(part));
6
+ }
7
+ function detectSubcommand(command) {
8
+ const index = command.findIndex((part) => /(?:^|[\\/])docker(?:\.exe)?$/i.test(part));
9
+ if (index < 0)
10
+ return undefined;
11
+ return command[index + 1]?.toLowerCase();
12
+ }
13
+ export function filterDockerOutput(input, options) {
14
+ const command = options.command ?? [];
15
+ const subcommand = detectSubcommand(command);
16
+ if (hasOption(command, "--format", "-f", "--quiet", "-q")) {
17
+ return {
18
+ output: cleanText(input),
19
+ kind: "docker",
20
+ omitted: false,
21
+ notes: ["explicit docker format preserved"],
22
+ };
23
+ }
24
+ if (subcommand === "logs" || subcommand === "attach") {
25
+ const result = filterGenericLog(input, options);
26
+ return { ...result, kind: "docker" };
27
+ }
28
+ if (subcommand === "ps" || subcommand === "images" || subcommand === "container" || subcommand === "volume" || subcommand === "network") {
29
+ const table = compactTable(input, options);
30
+ if (table.parsed) {
31
+ return {
32
+ output: table.output,
33
+ kind: "docker",
34
+ omitted: table.omitted,
35
+ notes: table.notes,
36
+ };
37
+ }
38
+ }
39
+ const fallback = filterGenericLog(input, options);
40
+ return { ...fallback, kind: "docker" };
41
+ }
42
+ //# sourceMappingURL=docker.js.map
@@ -0,0 +1,63 @@
1
+ import path from "node:path";
2
+ import { cleanText } from "../formatting/ansi.js";
3
+ import { limitLines } from "../formatting/limits.js";
4
+ const ERROR_PATTERN = /permission denied|no such file|cannot access|find:/i;
5
+ export function filterFindOutput(input, options) {
6
+ const lines = cleanText(input).split("\n").filter((line) => line.trim());
7
+ const paths = [];
8
+ const errors = [];
9
+ for (const line of lines) {
10
+ if (ERROR_PATTERN.test(line)) {
11
+ errors.push(line);
12
+ }
13
+ else {
14
+ paths.push(line);
15
+ }
16
+ }
17
+ if (paths.length === 0 && errors.length === 0) {
18
+ return {
19
+ output: "",
20
+ kind: "find",
21
+ omitted: false,
22
+ notes: [],
23
+ };
24
+ }
25
+ const directoryCounts = new Map();
26
+ for (const filePath of paths) {
27
+ const directory = path.dirname(filePath).replace(/\\/g, "/") || ".";
28
+ directoryCounts.set(directory, (directoryCounts.get(directory) ?? 0) + 1);
29
+ }
30
+ const topDirectories = [...directoryCounts.entries()]
31
+ .sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]))
32
+ .slice(0, 8);
33
+ const output = [`paths: ${paths.length}`];
34
+ if (topDirectories.length > 0) {
35
+ output.push("top directories:");
36
+ for (const [directory, count] of topDirectories) {
37
+ output.push(` ${directory} (${count})`);
38
+ }
39
+ }
40
+ if (errors.length > 0) {
41
+ output.push(`errors: ${errors.length}`);
42
+ output.push(...errors.slice(0, Math.max(1, options.perFileLines)).map((line) => ` ${line}`));
43
+ if (errors.length > options.perFileLines) {
44
+ output.push(` ... ${errors.length - options.perFileLines} error lines omitted ...`);
45
+ }
46
+ }
47
+ output.push("sample paths:");
48
+ output.push(...paths.slice(0, Math.max(1, options.perFileLines)).map((filePath) => ` ${filePath}`));
49
+ if (paths.length > options.perFileLines) {
50
+ output.push(` ... ${paths.length - options.perFileLines} paths omitted ...`);
51
+ }
52
+ const limited = limitLines(output, options.maxLines);
53
+ const omitted = paths.length > options.perFileLines ||
54
+ errors.length > options.perFileLines ||
55
+ limited.omitted > 0;
56
+ return {
57
+ output: limited.lines.join("\n"),
58
+ kind: "find",
59
+ omitted,
60
+ notes: omitted ? ["collapsed path listing"] : [],
61
+ };
62
+ }
63
+ //# sourceMappingURL=find.js.map
@@ -0,0 +1,38 @@
1
+ import { cleanText } from "../formatting/ansi.js";
2
+ import { limitLines } from "../formatting/limits.js";
3
+ const PROGRESS_PATTERN = /^(?:\s*\d{1,3}%|progress\b|downloading\b|uploading\b|building\b|compiling\b|waiting\b|\.{3,}|[=\-#>.]{8,})/i;
4
+ const IMPORTANT_PATTERN = /\b(error|failed|failure|fatal|warn(?:ing)?|exception|panic)\b/i;
5
+ export function filterGenericLog(input, options) {
6
+ const lines = cleanText(input).split("\n");
7
+ const compact = [];
8
+ let progressLines = 0;
9
+ for (let index = 0; index < lines.length;) {
10
+ const line = lines[index] ?? "";
11
+ let count = 1;
12
+ while (lines[index + count] === line) {
13
+ count += 1;
14
+ }
15
+ if (PROGRESS_PATTERN.test(line) && !IMPORTANT_PATTERN.test(line)) {
16
+ progressLines += count;
17
+ }
18
+ else if (count >= 3) {
19
+ compact.push(`${line} [repeated ${count}x]`);
20
+ }
21
+ else {
22
+ compact.push(...lines.slice(index, index + count));
23
+ }
24
+ index += count;
25
+ }
26
+ if (progressLines > 0) {
27
+ compact.unshift(`[${progressLines} progress lines collapsed]`);
28
+ }
29
+ const limited = limitLines(compact, options.maxLines);
30
+ const omitted = progressLines > 0 || limited.omitted > 0 || compact.length < lines.length;
31
+ return {
32
+ output: limited.lines.join("\n"),
33
+ kind: "log",
34
+ omitted,
35
+ notes: progressLines > 0 ? [`collapsed ${progressLines} progress lines`] : [],
36
+ };
37
+ }
38
+ //# sourceMappingURL=generic-log.js.map
@@ -0,0 +1,38 @@
1
+ import { cleanText } from "../formatting/ansi.js";
2
+ import { filterGenericLog } from "./generic-log.js";
3
+ import { compactTable } from "./table.js";
4
+ function hasStructuredOutput(command) {
5
+ for (let index = 0; index < command.length; index += 1) {
6
+ const part = command[index];
7
+ if (!part)
8
+ continue;
9
+ if (part === "--json" || part === "--template" || part === "--jq")
10
+ return true;
11
+ if (part.startsWith("--json=") || part.startsWith("--template=") || part.startsWith("--jq=")) {
12
+ return true;
13
+ }
14
+ }
15
+ return false;
16
+ }
17
+ export function filterGhOutput(input, options) {
18
+ if (hasStructuredOutput(options.command ?? [])) {
19
+ return {
20
+ output: cleanText(input),
21
+ kind: "gh",
22
+ omitted: false,
23
+ notes: ["explicit gh structured output preserved"],
24
+ };
25
+ }
26
+ const table = compactTable(input, options);
27
+ if (table.parsed) {
28
+ return {
29
+ output: table.output,
30
+ kind: "gh",
31
+ omitted: table.omitted,
32
+ notes: table.notes,
33
+ };
34
+ }
35
+ const fallback = filterGenericLog(input, options);
36
+ return { ...fallback, kind: "gh" };
37
+ }
38
+ //# sourceMappingURL=gh.js.map
@@ -0,0 +1,56 @@
1
+ import { cleanText } from "../formatting/ansi.js";
2
+ import { limitLines } from "../formatting/limits.js";
3
+ export function filterGitDiff(input, options) {
4
+ const lines = cleanText(input).split("\n");
5
+ const files = [];
6
+ let current;
7
+ for (const line of lines) {
8
+ const header = line.match(/^diff --git a\/(.+?) b\/(.+)$/);
9
+ if (header) {
10
+ current = {
11
+ name: header[2] ?? header[1] ?? "unknown",
12
+ lines: [],
13
+ additions: 0,
14
+ deletions: 0,
15
+ };
16
+ files.push(current);
17
+ continue;
18
+ }
19
+ if (!current)
20
+ continue;
21
+ if (line.startsWith("+") && !line.startsWith("+++"))
22
+ current.additions += 1;
23
+ if (line.startsWith("-") && !line.startsWith("---"))
24
+ current.deletions += 1;
25
+ if (line.startsWith("@@") ||
26
+ (line.startsWith("+") && !line.startsWith("+++")) ||
27
+ (line.startsWith("-") && !line.startsWith("---"))) {
28
+ current.lines.push(line);
29
+ }
30
+ }
31
+ if (files.length === 0) {
32
+ return {
33
+ output: lines.join("\n"),
34
+ kind: "git-diff",
35
+ omitted: false,
36
+ notes: ["unrecognized diff format; returned cleaned output"],
37
+ };
38
+ }
39
+ const output = [];
40
+ let perFileOmitted = 0;
41
+ for (const file of files) {
42
+ output.push(`${file.name} (+${file.additions} -${file.deletions})`);
43
+ const limited = limitLines(file.lines, options.perFileLines);
44
+ perFileOmitted += limited.omitted;
45
+ output.push(...limited.lines.map((line) => ` ${line}`));
46
+ }
47
+ const limited = limitLines(output, options.maxLines);
48
+ const omitted = perFileOmitted + limited.omitted;
49
+ return {
50
+ output: limited.lines.join("\n"),
51
+ kind: "git-diff",
52
+ omitted: true,
53
+ notes: omitted > 0 ? [`omitted ${omitted} diff lines`] : ["removed diff metadata and context"],
54
+ };
55
+ }
56
+ //# sourceMappingURL=git-diff.js.map
@@ -0,0 +1,25 @@
1
+ import { cleanText } from "../formatting/ansi.js";
2
+ import { limitLines } from "../formatting/limits.js";
3
+ export function filterGitList(input, options) {
4
+ const lines = cleanText(input)
5
+ .split("\n")
6
+ .map((line) => line.trim())
7
+ .filter(Boolean)
8
+ .map((line) => line.replace(/^\*\s+/, "current: "));
9
+ if (lines.length === 0) {
10
+ return {
11
+ output: cleanText(input),
12
+ kind: "git-list",
13
+ omitted: false,
14
+ notes: ["unrecognized git list format; returned cleaned output"],
15
+ };
16
+ }
17
+ const limited = limitLines(lines, options.maxLines);
18
+ return {
19
+ output: limited.lines.join("\n"),
20
+ kind: "git-list",
21
+ omitted: limited.omitted > 0,
22
+ notes: limited.omitted > 0 ? [`omitted ${limited.omitted} lines`] : [],
23
+ };
24
+ }
25
+ //# sourceMappingURL=git-list.js.map