shrinker-ai 0.4.0 → 0.9.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/.gitattributes +1 -0
- package/README.md +64 -3
- package/dist/src/cli.js +22 -0
- package/dist/src/config.js +23 -1
- package/dist/src/metrics/dashboard.js +8 -1
- package/dist/src/updates/check.js +144 -0
- package/dist/src/version.js +66 -0
- package/integrations/macos/install.sh +96 -6
- package/integrations/macos/uninstall.sh +20 -3
- package/integrations/windows/install.ps1 +85 -1
- package/integrations/windows/uninstall.ps1 +26 -1
- package/package.json +4 -1
- package/scripts/package-release.mjs +129 -0
package/.gitattributes
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*.sh text eol=lf
|
package/README.md
CHANGED
|
@@ -16,7 +16,9 @@ this POC proves that a few conservative, deterministic filters can save useful c
|
|
|
16
16
|
|
|
17
17
|
## Quick start
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
The recommended customer install downloads a standalone binary from GitHub Releases. It does not require npm registry access or a local Node.js installation.
|
|
20
|
+
|
|
21
|
+
Contributor and npm-based installs still require Node.js 22.13 or newer. This is the first Node 22 release where the built-in SQLite module no longer requires an experimental flag.
|
|
20
22
|
|
|
21
23
|
### One-command install (macOS zsh)
|
|
22
24
|
|
|
@@ -24,17 +26,59 @@ Requires Node.js 22.13 or newer. This is the first Node 22 release where the bui
|
|
|
24
26
|
curl -fsSL https://raw.githubusercontent.com/ivanduplenskikh/shrinker/main/integrations/macos/install.sh | bash
|
|
25
27
|
```
|
|
26
28
|
|
|
29
|
+
To pin a version:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
curl -fsSL https://raw.githubusercontent.com/ivanduplenskikh/shrinker/main/integrations/macos/install.sh | bash -s -- --version 0.4.0
|
|
33
|
+
```
|
|
34
|
+
|
|
27
35
|
### One-command install (Windows PowerShell)
|
|
28
36
|
|
|
29
37
|
```powershell
|
|
30
38
|
irm https://raw.githubusercontent.com/ivanduplenskikh/shrinker/main/integrations/windows/install.ps1 | iex
|
|
31
39
|
```
|
|
32
40
|
|
|
33
|
-
|
|
41
|
+
To pin a version:
|
|
42
|
+
|
|
43
|
+
```powershell
|
|
44
|
+
& ([scriptblock]::Create((irm https://raw.githubusercontent.com/ivanduplenskikh/shrinker/main/integrations/windows/install.ps1))) -Version 0.4.0
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The installer downloads these anonymous GitHub Release assets by default:
|
|
48
|
+
|
|
49
|
+
- `shrinker-win-x64.zip`
|
|
50
|
+
- `shrinker-macos-arm64.tar.gz`
|
|
51
|
+
- `shrinker-macos-x64.tar.gz`
|
|
52
|
+
- `shrinker-linux-x64.tar.gz`
|
|
53
|
+
|
|
54
|
+
Network allowlists need access to `raw.githubusercontent.com` for the installer script and `github.com/ivanduplenskikh/shrinker/releases/download/...` for release assets.
|
|
55
|
+
|
|
56
|
+
### Update notices
|
|
57
|
+
|
|
58
|
+
Shrinker checks GitHub Releases at most once every 24 hours and prints an update notice to stderr when a newer version is available. The check only requests release metadata from GitHub; command output, paths, stats, and machine identifiers are not sent.
|
|
59
|
+
|
|
60
|
+
To disable update checks for one shell session:
|
|
61
|
+
|
|
62
|
+
```powershell
|
|
63
|
+
$env:SHRINKER_UPDATE_CHECK = "0"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Or set it in `~/.shrinker/config`:
|
|
67
|
+
|
|
68
|
+
```text
|
|
69
|
+
SHRINKER_UPDATE_CHECK=0
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Optional npm package install
|
|
73
|
+
|
|
74
|
+
Use this only when npm registry access is available or preferred.
|
|
34
75
|
|
|
35
76
|
Windows PowerShell:
|
|
36
77
|
|
|
37
78
|
```powershell
|
|
79
|
+
pwsh -ExecutionPolicy Bypass -File .\integrations\windows\install.ps1 -UseNpm
|
|
80
|
+
|
|
81
|
+
# Or manually:
|
|
38
82
|
npm install --global shrinker-ai --registry=https://registry.npmjs.org
|
|
39
83
|
$pkg = Join-Path ((npm root --global).Trim()) "shrinker-ai"
|
|
40
84
|
pwsh -ExecutionPolicy Bypass -File (Join-Path $pkg "integrations\\windows\\install.ps1") -Local -SkipNpmInstall -SkipBuild -SkipLink
|
|
@@ -45,6 +89,9 @@ To enable automatic PowerShell routing, add `-EnableProfileRouting` to the final
|
|
|
45
89
|
macOS zsh:
|
|
46
90
|
|
|
47
91
|
```bash
|
|
92
|
+
bash ./integrations/macos/install.sh --use-npm
|
|
93
|
+
|
|
94
|
+
# Or manually:
|
|
48
95
|
npm install --global shrinker-ai --registry=https://registry.npmjs.org
|
|
49
96
|
pkg="$(npm root --global)/shrinker-ai"
|
|
50
97
|
bash "$pkg/integrations/macos/install.sh" --local --skip-npm-install --skip-build --skip-link --enable-profile-routing
|
|
@@ -75,7 +122,21 @@ The rules tell agents to prefer `shrinker <command>` for high-volume commands wh
|
|
|
75
122
|
|
|
76
123
|
### Uninstall
|
|
77
124
|
|
|
78
|
-
|
|
125
|
+
GitHub Release binary install:
|
|
126
|
+
|
|
127
|
+
Windows PowerShell:
|
|
128
|
+
|
|
129
|
+
```powershell
|
|
130
|
+
pwsh -ExecutionPolicy Bypass -File .\integrations\windows\uninstall.ps1
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
macOS zsh:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
bash ./integrations/macos/uninstall.sh
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
If you installed from npm, use npm mode:
|
|
79
140
|
|
|
80
141
|
```bash
|
|
81
142
|
npm uninstall --global shrinker-ai --registry=https://registry.npmjs.org
|
package/dist/src/cli.js
CHANGED
|
@@ -9,6 +9,8 @@ import { formatMeasurements, measure } from "./metrics/measure.js";
|
|
|
9
9
|
import { serveStatsDashboard, startStatsDashboard, writeStatsDashboard } from "./metrics/dashboard.js";
|
|
10
10
|
import { classifyWrappedRun, commandSignature, isCoverageTrackingEnabled } from "./metrics/coverage.js";
|
|
11
11
|
import { defaultStatsPath, formatCoverage, formatStats, formatStatsChart, getStats, recordRun, recordUncovered, } from "./metrics/stats-store.js";
|
|
12
|
+
import { checkForUpdate, formatUpdateNotice, markUpdateNoticeShown, wasUpdateNoticeShown } from "./updates/check.js";
|
|
13
|
+
import { getCurrentVersion } from "./version.js";
|
|
12
14
|
function usage() {
|
|
13
15
|
return `Usage:
|
|
14
16
|
shrinker <command> [args...]
|
|
@@ -297,10 +299,25 @@ async function render(rawOutput, options, durationMs, exitCode) {
|
|
|
297
299
|
}
|
|
298
300
|
}
|
|
299
301
|
}
|
|
302
|
+
async function maybeShowUpdateNotice() {
|
|
303
|
+
try {
|
|
304
|
+
const currentVersion = getCurrentVersion();
|
|
305
|
+
const result = await checkForUpdate(currentVersion ? { currentVersion } : {});
|
|
306
|
+
if (result.latestVersion && wasUpdateNoticeShown(result.latestVersion))
|
|
307
|
+
return;
|
|
308
|
+
const notice = formatUpdateNotice(result);
|
|
309
|
+
if (notice)
|
|
310
|
+
process.stderr.write(`${notice}\n`);
|
|
311
|
+
if (result.latestVersion && notice)
|
|
312
|
+
markUpdateNoticeShown(result.latestVersion);
|
|
313
|
+
}
|
|
314
|
+
catch { }
|
|
315
|
+
}
|
|
300
316
|
async function main() {
|
|
301
317
|
const options = parseArgs(process.argv.slice(2));
|
|
302
318
|
if (options.mode === "help") {
|
|
303
319
|
process.stdout.write(`${usage()}\n`);
|
|
320
|
+
await maybeShowUpdateNotice();
|
|
304
321
|
return;
|
|
305
322
|
}
|
|
306
323
|
if (options.mode === "stats") {
|
|
@@ -321,6 +338,7 @@ async function main() {
|
|
|
321
338
|
else {
|
|
322
339
|
process.stdout.write(`Dashboard server started at http://127.0.0.1:${options.dashboardPort} (PID ${dashboard.pid})\n`);
|
|
323
340
|
}
|
|
341
|
+
await maybeShowUpdateNotice();
|
|
324
342
|
}
|
|
325
343
|
return;
|
|
326
344
|
}
|
|
@@ -332,6 +350,7 @@ async function main() {
|
|
|
332
350
|
? formatStatsChart(summary)
|
|
333
351
|
: formatStats(summary);
|
|
334
352
|
process.stdout.write(`${output}\n`);
|
|
353
|
+
await maybeShowUpdateNotice();
|
|
335
354
|
return;
|
|
336
355
|
}
|
|
337
356
|
if (options.mode === "track") {
|
|
@@ -368,6 +387,7 @@ async function main() {
|
|
|
368
387
|
if (options.mode === "pipe") {
|
|
369
388
|
const input = await readStdin();
|
|
370
389
|
await render(input, options);
|
|
390
|
+
await maybeShowUpdateNotice();
|
|
371
391
|
return;
|
|
372
392
|
}
|
|
373
393
|
const [command, ...args] = options.command;
|
|
@@ -376,6 +396,8 @@ async function main() {
|
|
|
376
396
|
const result = await runCommand(command, args);
|
|
377
397
|
await render(result.combined, options, result.durationMs, result.exitCode);
|
|
378
398
|
process.exitCode = result.exitCode;
|
|
399
|
+
if (result.exitCode === 0)
|
|
400
|
+
await maybeShowUpdateNotice();
|
|
379
401
|
}
|
|
380
402
|
main().catch((error) => {
|
|
381
403
|
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
package/dist/src/config.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
export function defaultConfigPath() {
|
|
@@ -35,6 +35,28 @@ export function resolveSetting(key, configPath) {
|
|
|
35
35
|
return fromEnvironment;
|
|
36
36
|
return readConfig(configPath).get(key);
|
|
37
37
|
}
|
|
38
|
+
export function setConfigValue(key, value, configPath = defaultConfigPath()) {
|
|
39
|
+
const directory = path.dirname(configPath);
|
|
40
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
41
|
+
let lines = [];
|
|
42
|
+
try {
|
|
43
|
+
lines = readFileSync(configPath, "utf8").split(/\r?\n/);
|
|
44
|
+
}
|
|
45
|
+
catch { }
|
|
46
|
+
let replaced = false;
|
|
47
|
+
const nextLines = lines.map((line) => {
|
|
48
|
+
const withoutComment = line.split("#")[0]?.trim() ?? "";
|
|
49
|
+
const separator = withoutComment.indexOf("=");
|
|
50
|
+
const existingKey = separator > 0 ? withoutComment.slice(0, separator).trim() : "";
|
|
51
|
+
if (existingKey !== key)
|
|
52
|
+
return line;
|
|
53
|
+
replaced = true;
|
|
54
|
+
return `${key}=${value}`;
|
|
55
|
+
}).filter((line, index, array) => line !== "" || index < array.length - 1);
|
|
56
|
+
if (!replaced)
|
|
57
|
+
nextLines.push(`${key}=${value}`);
|
|
58
|
+
writeFileSync(configPath, `${nextLines.join("\n")}\n`, "utf8");
|
|
59
|
+
}
|
|
38
60
|
export function isTruthy(value) {
|
|
39
61
|
const normalized = value?.trim().toLowerCase();
|
|
40
62
|
return normalized === "1" || normalized === "true" || normalized === "yes";
|
|
@@ -8,6 +8,12 @@ import { DASHBOARD_STATS_PLACEHOLDER, DASHBOARD_TEMPLATE_HTML } from "./dashboar
|
|
|
8
8
|
const execFileAsync = promisify(execFile);
|
|
9
9
|
// Identifies a running server as ours without depending on user-visible copy.
|
|
10
10
|
const DASHBOARD_MARKER = 'name="generator" content="shrinker-dashboard"';
|
|
11
|
+
function getCliRelaunchCommand(args) {
|
|
12
|
+
if (process.pkg || !process.argv[1]) {
|
|
13
|
+
return { command: process.execPath, args };
|
|
14
|
+
}
|
|
15
|
+
return { command: process.execPath, args: [process.argv[1], ...args] };
|
|
16
|
+
}
|
|
11
17
|
// Neutralizes `</script>` inside string fields so the payload cannot break out of the JSON block.
|
|
12
18
|
function serializePayload(summary) {
|
|
13
19
|
const payload = {
|
|
@@ -146,7 +152,8 @@ export async function startStatsDashboard(port = 4317, restart = false) {
|
|
|
146
152
|
openStatsDashboard(url);
|
|
147
153
|
return { pid: 0, reused: true, restarted: false };
|
|
148
154
|
}
|
|
149
|
-
const
|
|
155
|
+
const command = getCliRelaunchCommand(["stats", "--dashboard", "--dashboard-server", "--port", String(port)]);
|
|
156
|
+
const child = spawn(command.command, command.args, { detached: true, stdio: "ignore" });
|
|
150
157
|
child.unref();
|
|
151
158
|
return { pid: child.pid ?? 0, reused: false, restarted };
|
|
152
159
|
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { defaultConfigPath, isTruthy, readConfig, resolveSetting, setConfigValue } from "../config.js";
|
|
2
|
+
const DEFAULT_REPOSITORY = "ivanduplenskikh/shrinker";
|
|
3
|
+
const DEFAULT_INTERVAL_HOURS = 24;
|
|
4
|
+
const DEFAULT_TIMEOUT_MS = 750;
|
|
5
|
+
const LAST_CHECK_KEY = "SHRINKER_LAST_UPDATE_CHECK";
|
|
6
|
+
const LATEST_VERSION_KEY = "SHRINKER_LATEST_VERSION";
|
|
7
|
+
const NOTICE_SHOWN_KEY = "SHRINKER_UPDATE_NOTICE_SHOWN";
|
|
8
|
+
function normalizeVersion(value) {
|
|
9
|
+
const normalized = value?.trim().replace(/^v/, "");
|
|
10
|
+
return normalized && /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(normalized) ? normalized : undefined;
|
|
11
|
+
}
|
|
12
|
+
function parseVersion(value) {
|
|
13
|
+
const normalized = normalizeVersion(value);
|
|
14
|
+
if (!normalized)
|
|
15
|
+
return undefined;
|
|
16
|
+
const [core, suffix] = normalized.split(/[-+]/, 2);
|
|
17
|
+
const parts = core?.split(".").map((part) => Number(part)) ?? [];
|
|
18
|
+
const major = parts[0];
|
|
19
|
+
const minor = parts[1];
|
|
20
|
+
const patch = parts[2];
|
|
21
|
+
if (!Number.isInteger(major) || !Number.isInteger(minor) || !Number.isInteger(patch))
|
|
22
|
+
return undefined;
|
|
23
|
+
const parsedMajor = major;
|
|
24
|
+
const parsedMinor = minor;
|
|
25
|
+
const parsedPatch = patch;
|
|
26
|
+
return {
|
|
27
|
+
major: parsedMajor,
|
|
28
|
+
minor: parsedMinor,
|
|
29
|
+
patch: parsedPatch,
|
|
30
|
+
...(suffix ? { prerelease: suffix } : {}),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function compareVersions(current, latest) {
|
|
34
|
+
const left = parseVersion(current);
|
|
35
|
+
const right = parseVersion(latest);
|
|
36
|
+
if (!left || !right)
|
|
37
|
+
return 0;
|
|
38
|
+
for (const key of ["major", "minor", "patch"]) {
|
|
39
|
+
if (left[key] < right[key])
|
|
40
|
+
return -1;
|
|
41
|
+
if (left[key] > right[key])
|
|
42
|
+
return 1;
|
|
43
|
+
}
|
|
44
|
+
if (left.prerelease && !right.prerelease)
|
|
45
|
+
return -1;
|
|
46
|
+
if (!left.prerelease && right.prerelease)
|
|
47
|
+
return 1;
|
|
48
|
+
if ((left.prerelease ?? "") < (right.prerelease ?? ""))
|
|
49
|
+
return -1;
|
|
50
|
+
if ((left.prerelease ?? "") > (right.prerelease ?? ""))
|
|
51
|
+
return 1;
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
54
|
+
function updateChecksEnabled(configPath) {
|
|
55
|
+
const setting = resolveSetting("SHRINKER_UPDATE_CHECK", configPath);
|
|
56
|
+
if (setting === undefined)
|
|
57
|
+
return true;
|
|
58
|
+
return isTruthy(setting);
|
|
59
|
+
}
|
|
60
|
+
function intervalHours(configPath, fallback) {
|
|
61
|
+
const configured = Number(resolveSetting("SHRINKER_UPDATE_CHECK_INTERVAL_HOURS", configPath));
|
|
62
|
+
return Number.isFinite(configured) && configured >= 0 ? configured : fallback;
|
|
63
|
+
}
|
|
64
|
+
function cachedResult(currentVersion, latestVersion) {
|
|
65
|
+
const latest = normalizeVersion(latestVersion);
|
|
66
|
+
return {
|
|
67
|
+
updateAvailable: compareVersions(currentVersion, latest) < 0,
|
|
68
|
+
...(currentVersion ? { currentVersion } : {}),
|
|
69
|
+
...(latest ? { latestVersion: latest } : {}),
|
|
70
|
+
checked: false,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function isCacheFresh(config, now, maxAgeHours) {
|
|
74
|
+
const previous = Number(config.get(LAST_CHECK_KEY));
|
|
75
|
+
if (!Number.isFinite(previous) || previous <= 0)
|
|
76
|
+
return false;
|
|
77
|
+
return now - previous < maxAgeHours * 60 * 60 * 1000;
|
|
78
|
+
}
|
|
79
|
+
async function fetchLatestRelease(repository, timeoutMs, fetchImpl) {
|
|
80
|
+
const response = await fetchImpl(`https://api.github.com/repos/${repository}/releases/latest`, {
|
|
81
|
+
headers: { "Accept": "application/vnd.github+json", "User-Agent": "shrinker-update-check" },
|
|
82
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
83
|
+
});
|
|
84
|
+
if (!response.ok)
|
|
85
|
+
return undefined;
|
|
86
|
+
const body = await response.json();
|
|
87
|
+
const version = normalizeVersion(typeof body.tag_name === "string" ? body.tag_name : undefined);
|
|
88
|
+
if (!version)
|
|
89
|
+
return undefined;
|
|
90
|
+
return {
|
|
91
|
+
version,
|
|
92
|
+
...(typeof body.html_url === "string" ? { releaseUrl: body.html_url } : {}),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
export async function checkForUpdate(options = {}) {
|
|
96
|
+
const configPath = options.configPath ?? defaultConfigPath();
|
|
97
|
+
const currentVersion = normalizeVersion(options.currentVersion);
|
|
98
|
+
if (!currentVersion || !updateChecksEnabled(configPath)) {
|
|
99
|
+
return { updateAvailable: false, ...(currentVersion ? { currentVersion } : {}), checked: false };
|
|
100
|
+
}
|
|
101
|
+
const now = options.now ?? Date.now();
|
|
102
|
+
const config = readConfig(configPath);
|
|
103
|
+
const maxAgeHours = options.intervalHours ?? intervalHours(configPath, DEFAULT_INTERVAL_HOURS);
|
|
104
|
+
if (isCacheFresh(config, now, maxAgeHours)) {
|
|
105
|
+
return cachedResult(currentVersion, config.get(LATEST_VERSION_KEY));
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
const latest = await fetchLatestRelease(options.repository ?? DEFAULT_REPOSITORY, options.timeoutMs ?? DEFAULT_TIMEOUT_MS, options.fetchImpl ?? fetch);
|
|
109
|
+
if (!latest?.version)
|
|
110
|
+
return { updateAvailable: false, currentVersion, checked: true };
|
|
111
|
+
setConfigValue(LAST_CHECK_KEY, String(now), configPath);
|
|
112
|
+
setConfigValue(LATEST_VERSION_KEY, latest.version, configPath);
|
|
113
|
+
return {
|
|
114
|
+
updateAvailable: compareVersions(currentVersion, latest.version) < 0,
|
|
115
|
+
currentVersion,
|
|
116
|
+
latestVersion: latest.version,
|
|
117
|
+
...(latest.releaseUrl ? { releaseUrl: latest.releaseUrl } : {}),
|
|
118
|
+
checked: true,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return { updateAvailable: false, currentVersion, checked: true };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
export function formatUpdateNotice(result) {
|
|
126
|
+
if (!result.updateAvailable || !result.currentVersion || !result.latestVersion)
|
|
127
|
+
return undefined;
|
|
128
|
+
const releaseUrl = result.releaseUrl ?? `https://github.com/${DEFAULT_REPOSITORY}/releases/latest`;
|
|
129
|
+
const installCommand = process.platform === "win32"
|
|
130
|
+
? `& ([scriptblock]::Create((irm https://raw.githubusercontent.com/${DEFAULT_REPOSITORY}/main/integrations/windows/install.ps1))) -Version ${result.latestVersion}`
|
|
131
|
+
: `curl -fsSL https://raw.githubusercontent.com/${DEFAULT_REPOSITORY}/main/integrations/macos/install.sh | bash -s -- --version ${result.latestVersion}`;
|
|
132
|
+
return [
|
|
133
|
+
`[shrinker] Update available: ${result.currentVersion} -> ${result.latestVersion}`,
|
|
134
|
+
`[shrinker] Release: ${releaseUrl}`,
|
|
135
|
+
`[shrinker] Install: ${installCommand}`,
|
|
136
|
+
].join("\n");
|
|
137
|
+
}
|
|
138
|
+
export function markUpdateNoticeShown(latestVersion, configPath = defaultConfigPath()) {
|
|
139
|
+
setConfigValue(NOTICE_SHOWN_KEY, latestVersion, configPath);
|
|
140
|
+
}
|
|
141
|
+
export function wasUpdateNoticeShown(latestVersion, configPath = defaultConfigPath()) {
|
|
142
|
+
return readConfig(configPath).get(NOTICE_SHOWN_KEY) === latestVersion;
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=check.js.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
const VERSION_PATTERN = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
|
|
6
|
+
let cachedVersion;
|
|
7
|
+
export function isPackagedBinary() {
|
|
8
|
+
return Boolean(process.pkg);
|
|
9
|
+
}
|
|
10
|
+
function normalizeVersion(value) {
|
|
11
|
+
if (typeof value !== "string")
|
|
12
|
+
return undefined;
|
|
13
|
+
const normalized = value.trim().replace(/^v/, "");
|
|
14
|
+
return VERSION_PATTERN.test(normalized) ? normalized : undefined;
|
|
15
|
+
}
|
|
16
|
+
function readVersionFile(filePath) {
|
|
17
|
+
try {
|
|
18
|
+
const metadata = JSON.parse(readFileSync(filePath, "utf8"));
|
|
19
|
+
return normalizeVersion(metadata.version);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function findPackageJsonVersion(startDirectory) {
|
|
26
|
+
let directory = startDirectory;
|
|
27
|
+
while (true) {
|
|
28
|
+
const packagePath = path.join(directory, "package.json");
|
|
29
|
+
const version = readVersionFile(packagePath);
|
|
30
|
+
if (version)
|
|
31
|
+
return version;
|
|
32
|
+
const parent = path.dirname(directory);
|
|
33
|
+
if (parent === directory)
|
|
34
|
+
return undefined;
|
|
35
|
+
directory = parent;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function findManifestVersion() {
|
|
39
|
+
const executableDirectory = path.dirname(process.execPath);
|
|
40
|
+
for (const candidate of [
|
|
41
|
+
path.join(executableDirectory, "..", "manifest.json"),
|
|
42
|
+
path.join(executableDirectory, "manifest.json"),
|
|
43
|
+
path.join(process.cwd(), "manifest.json"),
|
|
44
|
+
]) {
|
|
45
|
+
const resolved = path.resolve(candidate);
|
|
46
|
+
if (!existsSync(resolved))
|
|
47
|
+
continue;
|
|
48
|
+
const version = readVersionFile(resolved);
|
|
49
|
+
if (version)
|
|
50
|
+
return version;
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
export function getCurrentVersion() {
|
|
55
|
+
if (cachedVersion)
|
|
56
|
+
return cachedVersion;
|
|
57
|
+
const manifestVersion = findManifestVersion();
|
|
58
|
+
if (manifestVersion) {
|
|
59
|
+
cachedVersion = manifestVersion;
|
|
60
|
+
return cachedVersion;
|
|
61
|
+
}
|
|
62
|
+
const moduleDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
63
|
+
cachedVersion = findPackageJsonVersion(moduleDirectory);
|
|
64
|
+
return cachedVersion;
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=version.js.map
|
|
@@ -4,6 +4,9 @@ set -euo pipefail
|
|
|
4
4
|
PACKAGE_NAME="shrinker-ai"
|
|
5
5
|
REGISTRY="https://registry.npmjs.org"
|
|
6
6
|
VERSION=""
|
|
7
|
+
USE_NPM=0
|
|
8
|
+
RELEASE_REPO="ivanduplenskikh/shrinker"
|
|
9
|
+
ASSET_BASE_URL=""
|
|
7
10
|
LOCAL=0
|
|
8
11
|
SKIP_NPM_INSTALL=0
|
|
9
12
|
SKIP_BUILD=0
|
|
@@ -16,6 +19,7 @@ CLAUDE_ONLY=0
|
|
|
16
19
|
TRACK_UNCOVERED=""
|
|
17
20
|
PROFILE_PATH="${PROFILE_PATH:-$HOME/.zshrc}"
|
|
18
21
|
CONFIG_PATH="${SHRINKER_CONFIG_PATH:-$HOME/.shrinker/config}"
|
|
22
|
+
INSTALL_DIR="${SHRINKER_INSTALL_DIR:-$HOME/.shrinker}"
|
|
19
23
|
STEP="${SHRINKER_STEP_OFFSET:-0}"
|
|
20
24
|
|
|
21
25
|
print_message() {
|
|
@@ -28,9 +32,13 @@ print_message() {
|
|
|
28
32
|
while [[ $# -gt 0 ]]; do
|
|
29
33
|
case "$1" in
|
|
30
34
|
--local) LOCAL=1 ;;
|
|
35
|
+
--use-npm) USE_NPM=1 ;;
|
|
31
36
|
--package-name) PACKAGE_NAME="${2:-}"; shift ;;
|
|
32
37
|
--registry) REGISTRY="${2:-}"; shift ;;
|
|
33
38
|
--version) VERSION="${2:-}"; shift ;;
|
|
39
|
+
--release-repo) RELEASE_REPO="${2:-}"; shift ;;
|
|
40
|
+
--asset-base-url) ASSET_BASE_URL="${2:-}"; shift ;;
|
|
41
|
+
--install-dir) INSTALL_DIR="${2:-}"; shift ;;
|
|
34
42
|
--skip-npm-install) SKIP_NPM_INSTALL=1 ;;
|
|
35
43
|
--skip-build) SKIP_BUILD=1 ;;
|
|
36
44
|
--skip-link) SKIP_LINK=1 ;;
|
|
@@ -83,7 +91,7 @@ if (( interactive && SKIP_PROFILE == 0 && ENABLE_PROFILE_ROUTING == 0 )); then
|
|
|
83
91
|
ENABLE_PROFILE_ROUTING="$(prompt_yes_no "Enable automatic shell routing?" 0)"
|
|
84
92
|
fi
|
|
85
93
|
|
|
86
|
-
if (( LOCAL == 0 )); then
|
|
94
|
+
if (( LOCAL == 0 && USE_NPM == 1 )); then
|
|
87
95
|
package_spec="$PACKAGE_NAME"
|
|
88
96
|
[[ -z "$VERSION" ]] || package_spec="$PACKAGE_NAME@$VERSION"
|
|
89
97
|
print_message "📦" "Installing $package_spec from $REGISTRY..."
|
|
@@ -112,11 +120,13 @@ INTEGRATION_PATH="$SCRIPT_DIR/shrinker-profile.zsh"
|
|
|
112
120
|
BLOCK_START="<!-- shrinker agent rules start -->"
|
|
113
121
|
BLOCK_END="<!-- shrinker agent rules end -->"
|
|
114
122
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
123
|
+
if (( LOCAL == 1 )); then
|
|
124
|
+
node_version_raw="$(node -v 2>/dev/null || true)"
|
|
125
|
+
[[ -n "$node_version_raw" ]] || { print_message "❌" "Node.js was not found on PATH. Install Node.js 22.13+ first." >&2; exit 1; }
|
|
126
|
+
node_version="${node_version_raw#v}"
|
|
127
|
+
IFS='.' read -r node_major node_minor _ <<< "$node_version"
|
|
128
|
+
(( node_major > 22 || (node_major == 22 && node_minor >= 13) )) || { print_message "❌" "Node.js 22.13+ is required. Found $node_version_raw" >&2; exit 1; }
|
|
129
|
+
fi
|
|
120
130
|
|
|
121
131
|
set_agent_rules() {
|
|
122
132
|
local target="$1"
|
|
@@ -161,6 +171,19 @@ add_profile_integration() {
|
|
|
161
171
|
fi
|
|
162
172
|
}
|
|
163
173
|
|
|
174
|
+
add_path_integration() {
|
|
175
|
+
local profile_file="$1"
|
|
176
|
+
mkdir -p "$(dirname "$profile_file")"; touch "$profile_file"
|
|
177
|
+
if ! grep -Fq '# >>> shrinker path >>>' "$profile_file"; then
|
|
178
|
+
printf '\n# >>> shrinker path >>>\nexport PATH="%s/bin:$PATH"\n# <<< shrinker path <<<\n\n' "$INSTALL_DIR" >> "$profile_file"
|
|
179
|
+
print_message "🧭" "Added shrinker to PATH in profile: $profile_file"
|
|
180
|
+
fi
|
|
181
|
+
case ":$PATH:" in
|
|
182
|
+
*":$INSTALL_DIR/bin:"*) ;;
|
|
183
|
+
*) export PATH="$INSTALL_DIR/bin:$PATH" ;;
|
|
184
|
+
esac
|
|
185
|
+
}
|
|
186
|
+
|
|
164
187
|
set_config_value() {
|
|
165
188
|
local key="$1"
|
|
166
189
|
local value="$2"
|
|
@@ -175,6 +198,73 @@ set_config_value() {
|
|
|
175
198
|
print_message "⚙️" "Set $key=$value in: $CONFIG_PATH"
|
|
176
199
|
}
|
|
177
200
|
|
|
201
|
+
release_target() {
|
|
202
|
+
case "$(uname -s)-$(uname -m)" in
|
|
203
|
+
Darwin-arm64) printf '%s' "macos-arm64" ;;
|
|
204
|
+
Darwin-x86_64) printf '%s' "macos-x64" ;;
|
|
205
|
+
Linux-x86_64) printf '%s' "linux-x64" ;;
|
|
206
|
+
*) print_message "❌" "Unsupported release platform: $(uname -s)-$(uname -m)" >&2; exit 1 ;;
|
|
207
|
+
esac
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
release_asset_url() {
|
|
211
|
+
local target asset tag
|
|
212
|
+
target="$(release_target)"
|
|
213
|
+
asset="shrinker-$target.tar.gz"
|
|
214
|
+
if [[ -n "$ASSET_BASE_URL" ]]; then
|
|
215
|
+
printf '%s/%s' "${ASSET_BASE_URL%/}" "$asset"
|
|
216
|
+
elif [[ -n "$VERSION" ]]; then
|
|
217
|
+
tag="$VERSION"
|
|
218
|
+
[[ "$tag" == v* ]] || tag="v$tag"
|
|
219
|
+
printf 'https://github.com/%s/releases/download/%s/%s' "$RELEASE_REPO" "$tag" "$asset"
|
|
220
|
+
else
|
|
221
|
+
printf 'https://github.com/%s/releases/latest/download/%s' "$RELEASE_REPO" "$asset"
|
|
222
|
+
fi
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
install_release_package() {
|
|
226
|
+
local asset_url archive extract_dir name
|
|
227
|
+
asset_url="$(release_asset_url)"
|
|
228
|
+
archive="$(mktemp -t shrinker-release.XXXXXX).tar.gz"
|
|
229
|
+
extract_dir="$(mktemp -d -t shrinker-install.XXXXXX)"
|
|
230
|
+
trap 'rm -f "$archive"; rm -rf "$extract_dir"' RETURN
|
|
231
|
+
|
|
232
|
+
print_message "📦" "Downloading shrinker from: $asset_url"
|
|
233
|
+
curl -fL "$asset_url" -o "$archive"
|
|
234
|
+
tar -xzf "$archive" -C "$extract_dir"
|
|
235
|
+
mkdir -p "$INSTALL_DIR"
|
|
236
|
+
for name in bin integrations templates; do
|
|
237
|
+
[[ -d "$extract_dir/$name" ]] || { print_message "❌" "Release archive is missing: $name" >&2; exit 1; }
|
|
238
|
+
rm -rf "$INSTALL_DIR/$name"
|
|
239
|
+
cp -R "$extract_dir/$name" "$INSTALL_DIR/"
|
|
240
|
+
done
|
|
241
|
+
[[ -f "$extract_dir/manifest.json" ]] && cp "$extract_dir/manifest.json" "$INSTALL_DIR/manifest.json"
|
|
242
|
+
[[ -x "$INSTALL_DIR/bin/shrinker" ]] || chmod +x "$INSTALL_DIR/bin/shrinker"
|
|
243
|
+
[[ -x "$INSTALL_DIR/bin/shrinker" ]] || { print_message "❌" "Installed executable not found: $INSTALL_DIR/bin/shrinker" >&2; exit 1; }
|
|
244
|
+
|
|
245
|
+
add_path_integration "$PROFILE_PATH"
|
|
246
|
+
set_config_value "SHRINKER_TRACK_UNCOVERED" "$TRACK_UNCOVERED"
|
|
247
|
+
if (( SKIP_PROFILE == 0 && ENABLE_PROFILE_ROUTING == 1 )); then add_profile_integration "$PROFILE_PATH"; fi
|
|
248
|
+
if (( SKIP_AGENT_RULES == 0 )); then
|
|
249
|
+
[[ -f "$INSTALL_DIR/templates/agent-rules.md" ]] || { print_message "❌" "Agent rules template not found: $INSTALL_DIR/templates/agent-rules.md" >&2; exit 1; }
|
|
250
|
+
rules_body="$(cat "$INSTALL_DIR/templates/agent-rules.md")"
|
|
251
|
+
(( CLAUDE_ONLY )) || set_agent_rules "$HOME/.copilot/copilot-instructions.md" "$rules_body"
|
|
252
|
+
(( COPILOT_ONLY )) || set_agent_rules "$HOME/.claude/CLAUDE.md" "$rules_body"
|
|
253
|
+
fi
|
|
254
|
+
|
|
255
|
+
print_message "✅" "Install complete."
|
|
256
|
+
echo ""
|
|
257
|
+
echo "💡 Try: shrinker help"
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (( LOCAL == 0 )); then
|
|
261
|
+
install_release_package
|
|
262
|
+
if [[ -n "${BASH_SOURCE[0]:-}" && "${BASH_SOURCE[0]}" != "$0" ]]; then
|
|
263
|
+
return 0
|
|
264
|
+
fi
|
|
265
|
+
exit 0
|
|
266
|
+
fi
|
|
267
|
+
|
|
178
268
|
print_message "📦" "Installing shrinker from: $REPO_ROOT"
|
|
179
269
|
pushd "$REPO_ROOT" >/dev/null
|
|
180
270
|
if (( SKIP_NPM_INSTALL == 0 )); then
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
set -euo pipefail
|
|
3
3
|
|
|
4
|
-
INSTALL_DIR="${HOME
|
|
4
|
+
INSTALL_DIR="${SHRINKER_INSTALL_DIR:-$HOME/.shrinker}"
|
|
5
5
|
REMOVE_INSTALL_DIR=0
|
|
6
|
+
USE_NPM=0
|
|
6
7
|
SKIP_UNLINK=0
|
|
7
8
|
SKIP_AGENT_RULES=0
|
|
8
9
|
COPILOT_ONLY=0
|
|
@@ -22,6 +23,7 @@ print_step() {
|
|
|
22
23
|
|
|
23
24
|
while [[ $# -gt 0 ]]; do
|
|
24
25
|
case "$1" in
|
|
26
|
+
--use-npm) USE_NPM=1 ;;
|
|
25
27
|
--skip-unlink) SKIP_UNLINK=1 ;;
|
|
26
28
|
--skip-agent-rules) SKIP_AGENT_RULES=1 ;;
|
|
27
29
|
--copilot-only) COPILOT_ONLY=1 ;;
|
|
@@ -54,6 +56,17 @@ remove_profile_integration() {
|
|
|
54
56
|
mv "$profile_file.tmp" "$profile_file"
|
|
55
57
|
}
|
|
56
58
|
|
|
59
|
+
remove_path_integration() {
|
|
60
|
+
local profile_file="$1"
|
|
61
|
+
[[ -f "$profile_file" ]] || return 0
|
|
62
|
+
awk -v start='# >>> shrinker path >>>' -v end='# <<< shrinker path <<<' 'BEGIN{inblock=0} {if(index($0,start)>0){inblock=1;next} if(inblock&&index($0,end)>0){inblock=0;next} if(!inblock)print}' "$profile_file" > "$profile_file.tmp"
|
|
63
|
+
mv "$profile_file.tmp" "$profile_file"
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
remove_release_install() {
|
|
67
|
+
rm -rf "$INSTALL_DIR/bin" "$INSTALL_DIR/integrations" "$INSTALL_DIR/templates" "$INSTALL_DIR/manifest.json"
|
|
68
|
+
}
|
|
69
|
+
|
|
57
70
|
# The dashboard runs as a detached daemon, so unlinking the package never stops it.
|
|
58
71
|
stop_dashboard_server() {
|
|
59
72
|
local url="http://127.0.0.1:$DASHBOARD_PORT"
|
|
@@ -86,14 +99,18 @@ remove_managed_config() {
|
|
|
86
99
|
print_step "🛑" "Stopping the dashboard server on port $DASHBOARD_PORT..."
|
|
87
100
|
stop_dashboard_server
|
|
88
101
|
|
|
89
|
-
if (( SKIP_UNLINK == 0 )); then
|
|
102
|
+
if (( SKIP_UNLINK == 0 && USE_NPM == 1 )); then
|
|
90
103
|
print_step "🔗" "Unlinking shrinker globally..."
|
|
91
104
|
npm unlink --silent --global shrinker-ai
|
|
105
|
+
elif (( SKIP_UNLINK == 0 )); then
|
|
106
|
+
print_step "🔗" "Removing release-installed shrinker files..."
|
|
107
|
+
remove_release_install
|
|
92
108
|
else
|
|
93
|
-
print_step "⏭️" "Skipped
|
|
109
|
+
print_step "⏭️" "Skipped unlink/removal."
|
|
94
110
|
fi
|
|
95
111
|
print_step "🔧" "Removing shell profile integration..."
|
|
96
112
|
remove_profile_integration "$PROFILE_PATH"
|
|
113
|
+
remove_path_integration "$PROFILE_PATH"
|
|
97
114
|
if (( SKIP_AGENT_RULES == 0 )); then
|
|
98
115
|
(( CLAUDE_ONLY )) || remove_agent_rules "$HOME/.copilot/copilot-instructions.md"
|
|
99
116
|
(( COPILOT_ONLY )) || remove_agent_rules "$HOME/.claude/CLAUDE.md"
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
param(
|
|
2
2
|
[switch]$Local,
|
|
3
|
+
[switch]$UseNpm,
|
|
3
4
|
[string]$PackageName = "shrinker-ai",
|
|
4
5
|
[string]$Registry = "https://registry.npmjs.org",
|
|
5
6
|
[string]$Version,
|
|
7
|
+
[string]$ReleaseRepo = "ivanduplenskikh/shrinker",
|
|
8
|
+
[string]$AssetBaseUrl,
|
|
9
|
+
[string]$InstallDir = $(Join-Path $HOME ".shrinker"),
|
|
6
10
|
[switch]$SkipNpmInstall,
|
|
7
11
|
[switch]$SkipBuild,
|
|
8
12
|
[switch]$SkipLink,
|
|
@@ -99,7 +103,82 @@ function Add-ProfileIntegration {
|
|
|
99
103
|
Write-InstallStep "🔧" "Added shrinker integration block to profile: $ProfileFile"
|
|
100
104
|
}
|
|
101
105
|
|
|
102
|
-
|
|
106
|
+
function Add-UserPathEntry {
|
|
107
|
+
param([string]$Directory)
|
|
108
|
+
$currentUserPath = [Environment]::GetEnvironmentVariable("Path", "User")
|
|
109
|
+
$entries = @($currentUserPath -split [IO.Path]::PathSeparator | Where-Object { $_ })
|
|
110
|
+
if ($entries -notcontains $Directory) {
|
|
111
|
+
$newPath = (@($entries) + $Directory) -join [IO.Path]::PathSeparator
|
|
112
|
+
[Environment]::SetEnvironmentVariable("Path", $newPath, "User")
|
|
113
|
+
Write-InstallStep "🧭" "Added shrinker to the user PATH: $Directory"
|
|
114
|
+
}
|
|
115
|
+
$processEntries = @($env:Path -split [IO.Path]::PathSeparator | Where-Object { $_ })
|
|
116
|
+
if ($processEntries -notcontains $Directory) {
|
|
117
|
+
$env:Path = (@($processEntries) + $Directory) -join [IO.Path]::PathSeparator
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function Get-ReleaseAssetUrl {
|
|
122
|
+
$assetName = "shrinker-win-x64.zip"
|
|
123
|
+
if ($AssetBaseUrl) { return "$($AssetBaseUrl.TrimEnd('/'))/$assetName" }
|
|
124
|
+
if ($Version) {
|
|
125
|
+
$tag = if ($Version.StartsWith("v")) { $Version } else { "v$Version" }
|
|
126
|
+
return "https://github.com/$ReleaseRepo/releases/download/$tag/$assetName"
|
|
127
|
+
}
|
|
128
|
+
return "https://github.com/$ReleaseRepo/releases/latest/download/$assetName"
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function Install-ReleasePackage {
|
|
132
|
+
$assetUrl = Get-ReleaseAssetUrl
|
|
133
|
+
$archivePath = Join-Path ([IO.Path]::GetTempPath()) "shrinker-win-x64.zip"
|
|
134
|
+
$extractPath = Join-Path ([IO.Path]::GetTempPath()) ("shrinker-install-" + [guid]::NewGuid().ToString("N"))
|
|
135
|
+
try {
|
|
136
|
+
Write-InstallStep "📦" "Downloading shrinker from: $assetUrl"
|
|
137
|
+
Invoke-WebRequest -Uri $assetUrl -OutFile $archivePath -UseBasicParsing
|
|
138
|
+
New-Item -ItemType Directory -Force -Path $extractPath | Out-Null
|
|
139
|
+
Expand-Archive -LiteralPath $archivePath -DestinationPath $extractPath -Force
|
|
140
|
+
New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null
|
|
141
|
+
|
|
142
|
+
foreach ($name in @("bin", "integrations", "templates")) {
|
|
143
|
+
$source = Join-Path $extractPath $name
|
|
144
|
+
if (-not (Test-Path -LiteralPath $source)) { throw "Release archive is missing: $name" }
|
|
145
|
+
Copy-Item -LiteralPath $source -Destination $InstallDir -Recurse -Force
|
|
146
|
+
}
|
|
147
|
+
if (Test-Path -LiteralPath (Join-Path $extractPath "manifest.json")) {
|
|
148
|
+
Copy-Item -LiteralPath (Join-Path $extractPath "manifest.json") -Destination $InstallDir -Force
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
Remove-Item -LiteralPath $archivePath -Force -ErrorAction SilentlyContinue
|
|
153
|
+
Remove-Item -LiteralPath $extractPath -Recurse -Force -ErrorAction SilentlyContinue
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
$binPath = Join-Path $InstallDir "bin"
|
|
157
|
+
$exePath = Join-Path $binPath "shrinker.exe"
|
|
158
|
+
$legacyExePath = Join-Path $InstallDir "shrinker.exe"
|
|
159
|
+
if (-not (Test-Path -LiteralPath $exePath) -and (Test-Path -LiteralPath $legacyExePath)) {
|
|
160
|
+
New-Item -ItemType Directory -Force -Path $binPath | Out-Null
|
|
161
|
+
Move-Item -LiteralPath $legacyExePath -Destination $exePath -Force
|
|
162
|
+
}
|
|
163
|
+
if (-not (Test-Path -LiteralPath $exePath)) { throw "Installed executable not found: $exePath" }
|
|
164
|
+
Add-UserPathEntry $binPath
|
|
165
|
+
|
|
166
|
+
$templatePath = Join-Path $InstallDir "templates\agent-rules.md"
|
|
167
|
+
$integrationPath = Join-Path $InstallDir "integrations\windows\shrinker-profile.ps1"
|
|
168
|
+
if (-not $SkipAgentRules) {
|
|
169
|
+
if (-not (Test-Path $templatePath)) { throw "Agent rules template not found: $templatePath" }
|
|
170
|
+
$rulesBody = Get-Content $templatePath -Raw
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
Set-ConfigValue "SHRINKER_TRACK_UNCOVERED" $(if ($TrackUncovered) { "1" } else { "0" })
|
|
174
|
+
if (-not $SkipProfile -and $EnableProfileRouting) { Add-ProfileIntegration $ProfilePath $integrationPath }
|
|
175
|
+
if (-not $SkipAgentRules) { Set-AgentRules $rulesBody }
|
|
176
|
+
Write-InstallStep "✅" "Install complete."
|
|
177
|
+
Write-Host " "
|
|
178
|
+
Write-Host "💡 Try: shrinker help"
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (-not $Local -and $UseNpm) {
|
|
103
182
|
$packageSpec = if ($Version) { "$PackageName@$Version" } else { $PackageName }
|
|
104
183
|
Write-InstallStep "📦" "Installing $packageSpec from $Registry..."
|
|
105
184
|
& npm install --silent --global $packageSpec "--registry=$Registry"
|
|
@@ -116,6 +195,11 @@ if (-not $Local) {
|
|
|
116
195
|
return
|
|
117
196
|
}
|
|
118
197
|
|
|
198
|
+
if (-not $Local) {
|
|
199
|
+
Install-ReleasePackage
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
|
|
119
203
|
$scriptDir = $PSScriptRoot
|
|
120
204
|
$repoRoot = (Resolve-Path (Join-Path $scriptDir "..\..")).Path
|
|
121
205
|
$templatePath = Join-Path $repoRoot "templates\agent-rules.md"
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
param(
|
|
2
|
+
[switch]$UseNpm,
|
|
2
3
|
[switch]$SkipUnlink,
|
|
3
4
|
[switch]$SkipAgentRules,
|
|
4
5
|
[switch]$CopilotOnly,
|
|
5
6
|
[switch]$ClaudeOnly,
|
|
6
7
|
[switch]$PurgeData,
|
|
7
8
|
[int]$Port = 4317,
|
|
9
|
+
[string]$InstallDir = $(Join-Path $HOME ".shrinker"),
|
|
8
10
|
[string]$ProfilePath = $PROFILE,
|
|
9
11
|
[string]$ConfigPath = $(if ($env:SHRINKER_CONFIG_PATH) { $env:SHRINKER_CONFIG_PATH } else { Join-Path $HOME ".shrinker/config" })
|
|
10
12
|
)
|
|
@@ -45,6 +47,25 @@ function Remove-ProfileIntegration {
|
|
|
45
47
|
}
|
|
46
48
|
}
|
|
47
49
|
|
|
50
|
+
function Remove-UserPathEntry {
|
|
51
|
+
param([string]$Directory)
|
|
52
|
+
$currentUserPath = [Environment]::GetEnvironmentVariable("Path", "User")
|
|
53
|
+
if (-not $currentUserPath) { return }
|
|
54
|
+
$entries = @($currentUserPath -split [IO.Path]::PathSeparator | Where-Object { $_ -and $_ -ne $Directory })
|
|
55
|
+
$newPath = $entries -join [IO.Path]::PathSeparator
|
|
56
|
+
if ($newPath -ne $currentUserPath) {
|
|
57
|
+
[Environment]::SetEnvironmentVariable("Path", $newPath, "User")
|
|
58
|
+
Write-UninstallStep "🧭" "Removed shrinker from the user PATH: $Directory"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function Remove-ReleaseInstall {
|
|
63
|
+
foreach ($name in @("bin", "integrations", "templates", "manifest.json")) {
|
|
64
|
+
Remove-Item -LiteralPath (Join-Path $InstallDir $name) -Recurse -Force -ErrorAction SilentlyContinue
|
|
65
|
+
}
|
|
66
|
+
Remove-UserPathEntry (Join-Path $InstallDir "bin")
|
|
67
|
+
}
|
|
68
|
+
|
|
48
69
|
# The dashboard runs as a detached daemon, so unlinking the package never stops it.
|
|
49
70
|
function Stop-DashboardServer {
|
|
50
71
|
param([int]$DashboardPort)
|
|
@@ -72,11 +93,15 @@ function Remove-ManagedConfig {
|
|
|
72
93
|
Write-UninstallStep "🛑" "Stopping the dashboard server on port $Port..."
|
|
73
94
|
Stop-DashboardServer $Port
|
|
74
95
|
|
|
75
|
-
if (-not $SkipUnlink) {
|
|
96
|
+
if (-not $SkipUnlink -and $UseNpm) {
|
|
76
97
|
Write-UninstallStep "🔗" "Unlinking shrinker globally..."
|
|
77
98
|
& npm unlink --silent --global shrinker-ai
|
|
78
99
|
if ($LASTEXITCODE -ne 0) { throw "npm unlink failed." }
|
|
79
100
|
}
|
|
101
|
+
elseif (-not $SkipUnlink) {
|
|
102
|
+
Write-UninstallStep "🔗" "Removing release-installed shrinker files..."
|
|
103
|
+
Remove-ReleaseInstall
|
|
104
|
+
}
|
|
80
105
|
else { Write-UninstallStep "⏭️" "Skipped global npm unlink." }
|
|
81
106
|
Write-UninstallStep "🔧" "Removing PowerShell profile integration..."
|
|
82
107
|
Remove-ProfileIntegration $ProfilePath
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shrinker-ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Local deterministic command-output shrinker for coding agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
"dashboard": "npm run build --silent && node dist/src/cli.js stats --dashboard",
|
|
18
18
|
"dashboard:restart": "npm run build --silent && node dist/src/cli.js stats --dashboard --restart",
|
|
19
19
|
"pack": "npm run build --silent && npm pack",
|
|
20
|
+
"package:release": "npm run build --silent && node scripts/package-release.mjs",
|
|
21
|
+
"package:release:current": "node scripts/package-release.mjs",
|
|
20
22
|
"test": "npm run build --silent && node --test \"dist/tests/*.test.js\"",
|
|
21
23
|
"demo": "npm run build --silent && node dist/demo/run-demo.js",
|
|
22
24
|
"install:local": "pwsh -ExecutionPolicy Bypass -File ./integrations/windows/install.ps1 -Local",
|
|
@@ -30,6 +32,7 @@
|
|
|
30
32
|
},
|
|
31
33
|
"devDependencies": {
|
|
32
34
|
"@types/node": "^24.0.0",
|
|
35
|
+
"@yao-pkg/pkg": "^6.22.0",
|
|
33
36
|
"typescript": "^5.9.0"
|
|
34
37
|
},
|
|
35
38
|
"engines": {
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import process from "node:process";
|
|
8
|
+
|
|
9
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
|
+
const packageJson = JSON.parse(await readFile(path.join(repoRoot, "package.json"), "utf8"));
|
|
11
|
+
|
|
12
|
+
const targets = {
|
|
13
|
+
"win-x64": { pkg: "node22-win-x64", archive: "zip", binary: "shrinker.exe" },
|
|
14
|
+
"macos-arm64": { pkg: "node22-macos-arm64", archive: "tar.gz", binary: "shrinker" },
|
|
15
|
+
"macos-x64": { pkg: "node22-macos-x64", archive: "tar.gz", binary: "shrinker" },
|
|
16
|
+
"linux-x64": { pkg: "node22-linux-x64", archive: "tar.gz", binary: "shrinker" },
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function currentTarget() {
|
|
20
|
+
if (process.platform === "win32" && process.arch === "x64") return "win-x64";
|
|
21
|
+
if (process.platform === "darwin" && process.arch === "arm64") return "macos-arm64";
|
|
22
|
+
if (process.platform === "darwin" && process.arch === "x64") return "macos-x64";
|
|
23
|
+
if (process.platform === "linux" && process.arch === "x64") return "linux-x64";
|
|
24
|
+
throw new Error(`No default release target for ${process.platform}-${process.arch}. Pass --target explicitly.`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readOption(name) {
|
|
28
|
+
const index = process.argv.indexOf(name);
|
|
29
|
+
if (index === -1) return undefined;
|
|
30
|
+
const value = process.argv[index + 1];
|
|
31
|
+
if (!value || value.startsWith("--")) throw new Error(`${name} requires a value`);
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readTarget() {
|
|
36
|
+
const explicit = readOption("--target");
|
|
37
|
+
if (explicit) return explicit;
|
|
38
|
+
const positional = process.argv.slice(2).find((arg) => !arg.startsWith("--"));
|
|
39
|
+
return positional ?? currentTarget();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function run(command, args, options = {}) {
|
|
43
|
+
const result = spawnSync(command, args, {
|
|
44
|
+
cwd: repoRoot,
|
|
45
|
+
stdio: "inherit",
|
|
46
|
+
shell: false,
|
|
47
|
+
...options,
|
|
48
|
+
});
|
|
49
|
+
if (result.status !== 0) {
|
|
50
|
+
throw new Error(`${command} ${args.join(" ")} failed with exit code ${result.status ?? "unknown"}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function copySupportFiles(stageDir) {
|
|
55
|
+
await cp(
|
|
56
|
+
path.join(repoRoot, "integrations", "windows", "shrinker-profile.ps1"),
|
|
57
|
+
path.join(stageDir, "integrations", "windows", "shrinker-profile.ps1"),
|
|
58
|
+
{ recursive: true },
|
|
59
|
+
);
|
|
60
|
+
await cp(
|
|
61
|
+
path.join(repoRoot, "integrations", "macos", "shrinker-profile.zsh"),
|
|
62
|
+
path.join(stageDir, "integrations", "macos", "shrinker-profile.zsh"),
|
|
63
|
+
{ recursive: true },
|
|
64
|
+
);
|
|
65
|
+
await cp(
|
|
66
|
+
path.join(repoRoot, "templates", "agent-rules.md"),
|
|
67
|
+
path.join(stageDir, "templates", "agent-rules.md"),
|
|
68
|
+
{ recursive: true },
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function createArchive(stageDir, archivePath, archiveType) {
|
|
73
|
+
await rm(archivePath, { force: true });
|
|
74
|
+
if (archiveType === "zip") {
|
|
75
|
+
const expression = [
|
|
76
|
+
"$ErrorActionPreference = 'Stop'",
|
|
77
|
+
`$source = Join-Path '${stageDir.replaceAll("'", "''")}' '*'`,
|
|
78
|
+
`Compress-Archive -Path $source -DestinationPath '${archivePath.replaceAll("'", "''")}' -Force`,
|
|
79
|
+
].join("; ");
|
|
80
|
+
run("pwsh", ["-NoProfile", "-Command", expression]);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
run("tar", ["-czf", archivePath, "-C", stageDir, "."]);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const targetName = readTarget();
|
|
88
|
+
const version = readOption("--version") ?? packageJson.version;
|
|
89
|
+
const target = targets[targetName];
|
|
90
|
+
if (!target) {
|
|
91
|
+
throw new Error(`Unsupported target '${targetName}'. Supported targets: ${Object.keys(targets).join(", ")}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const entrypoint = path.join(repoRoot, "dist", "src", "cli.js");
|
|
95
|
+
if (!existsSync(entrypoint)) {
|
|
96
|
+
throw new Error("Missing dist/src/cli.js. Run npm run build before packaging.");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const releaseDir = path.join(repoRoot, "release");
|
|
100
|
+
const stageDir = path.join(repoRoot, ".shrinker", "package", targetName);
|
|
101
|
+
const binaryPath = path.join(stageDir, "bin", target.binary);
|
|
102
|
+
const archiveName = `shrinker-${targetName}.${target.archive === "zip" ? "zip" : "tar.gz"}`;
|
|
103
|
+
const archivePath = path.join(releaseDir, archiveName);
|
|
104
|
+
|
|
105
|
+
await rm(stageDir, { recursive: true, force: true });
|
|
106
|
+
await mkdir(path.dirname(binaryPath), { recursive: true });
|
|
107
|
+
await mkdir(releaseDir, { recursive: true });
|
|
108
|
+
|
|
109
|
+
run(process.execPath, [
|
|
110
|
+
path.join(repoRoot, "node_modules", "@yao-pkg", "pkg", "lib-es5", "bin.js"),
|
|
111
|
+
entrypoint,
|
|
112
|
+
"--targets",
|
|
113
|
+
target.pkg,
|
|
114
|
+
"--output",
|
|
115
|
+
binaryPath,
|
|
116
|
+
"--no-bytecode",
|
|
117
|
+
"--public",
|
|
118
|
+
"--public-packages",
|
|
119
|
+
"*",
|
|
120
|
+
]);
|
|
121
|
+
await copySupportFiles(stageDir);
|
|
122
|
+
await writeFile(
|
|
123
|
+
path.join(stageDir, "manifest.json"),
|
|
124
|
+
`${JSON.stringify({ name: packageJson.name, version, target: targetName, binary: `bin/${target.binary}` }, null, 2)}\n`,
|
|
125
|
+
"utf8",
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
await createArchive(stageDir, archivePath, target.archive);
|
|
129
|
+
console.log(`Created ${path.relative(repoRoot, archivePath)}`);
|