shrinker-ai 0.4.0 → 0.7.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 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
- Requires 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.
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,43 @@ 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
- ### Install from npm package
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
+ ### Optional npm package install
57
+
58
+ Use this only when npm registry access is available or preferred.
34
59
 
35
60
  Windows PowerShell:
36
61
 
37
62
  ```powershell
63
+ pwsh -ExecutionPolicy Bypass -File .\integrations\windows\install.ps1 -UseNpm
64
+
65
+ # Or manually:
38
66
  npm install --global shrinker-ai --registry=https://registry.npmjs.org
39
67
  $pkg = Join-Path ((npm root --global).Trim()) "shrinker-ai"
40
68
  pwsh -ExecutionPolicy Bypass -File (Join-Path $pkg "integrations\\windows\\install.ps1") -Local -SkipNpmInstall -SkipBuild -SkipLink
@@ -45,6 +73,9 @@ To enable automatic PowerShell routing, add `-EnableProfileRouting` to the final
45
73
  macOS zsh:
46
74
 
47
75
  ```bash
76
+ bash ./integrations/macos/install.sh --use-npm
77
+
78
+ # Or manually:
48
79
  npm install --global shrinker-ai --registry=https://registry.npmjs.org
49
80
  pkg="$(npm root --global)/shrinker-ai"
50
81
  bash "$pkg/integrations/macos/install.sh" --local --skip-npm-install --skip-build --skip-link --enable-profile-routing
@@ -75,7 +106,21 @@ The rules tell agents to prefer `shrinker <command>` for high-volume commands wh
75
106
 
76
107
  ### Uninstall
77
108
 
78
- If you installed from npm, remove the package first:
109
+ GitHub Release binary install:
110
+
111
+ Windows PowerShell:
112
+
113
+ ```powershell
114
+ pwsh -ExecutionPolicy Bypass -File .\integrations\windows\uninstall.ps1
115
+ ```
116
+
117
+ macOS zsh:
118
+
119
+ ```bash
120
+ bash ./integrations/macos/uninstall.sh
121
+ ```
122
+
123
+ If you installed from npm, use npm mode:
79
124
 
80
125
  ```bash
81
126
  npm uninstall --global shrinker-ai --registry=https://registry.npmjs.org
@@ -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 child = spawn(process.execPath, [process.argv[1] ?? "", "stats", "--dashboard", "--dashboard-server", "--port", String(port)], { detached: true, stdio: "ignore" });
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
  }
@@ -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
- node_version_raw="$(node -v 2>/dev/null || true)"
116
- [[ -n "$node_version_raw" ]] || { print_message "❌" "Node.js was not found on PATH. Install Node.js 22.13+ first." >&2; exit 1; }
117
- node_version="${node_version_raw#v}"
118
- IFS='.' read -r node_major node_minor _ <<< "$node_version"
119
- (( node_major > 22 || (node_major == 22 && node_minor >= 13) )) || { print_message "❌" "Node.js 22.13+ is required. Found $node_version_raw" >&2; exit 1; }
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}/.shrinker-src"
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 global npm unlink."
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,76 @@ function Add-ProfileIntegration {
99
103
  Write-InstallStep "🔧" "Added shrinker integration block to profile: $ProfileFile"
100
104
  }
101
105
 
102
- if (-not $Local) {
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
+
141
+ foreach ($name in @("bin", "integrations", "templates")) {
142
+ $source = Join-Path $extractPath $name
143
+ if (-not (Test-Path -LiteralPath $source)) { throw "Release archive is missing: $name" }
144
+ Copy-Item -LiteralPath $source -Destination $InstallDir -Recurse -Force
145
+ }
146
+ if (Test-Path -LiteralPath (Join-Path $extractPath "manifest.json")) {
147
+ Copy-Item -LiteralPath (Join-Path $extractPath "manifest.json") -Destination $InstallDir -Force
148
+ }
149
+ }
150
+ finally {
151
+ Remove-Item -LiteralPath $archivePath -Force -ErrorAction SilentlyContinue
152
+ Remove-Item -LiteralPath $extractPath -Recurse -Force -ErrorAction SilentlyContinue
153
+ }
154
+
155
+ $binPath = Join-Path $InstallDir "bin"
156
+ $exePath = Join-Path $binPath "shrinker.exe"
157
+ if (-not (Test-Path -LiteralPath $exePath)) { throw "Installed executable not found: $exePath" }
158
+ Add-UserPathEntry $binPath
159
+
160
+ $templatePath = Join-Path $InstallDir "templates\agent-rules.md"
161
+ $integrationPath = Join-Path $InstallDir "integrations\windows\shrinker-profile.ps1"
162
+ if (-not $SkipAgentRules) {
163
+ if (-not (Test-Path $templatePath)) { throw "Agent rules template not found: $templatePath" }
164
+ $rulesBody = Get-Content $templatePath -Raw
165
+ }
166
+
167
+ Set-ConfigValue "SHRINKER_TRACK_UNCOVERED" $(if ($TrackUncovered) { "1" } else { "0" })
168
+ if (-not $SkipProfile -and $EnableProfileRouting) { Add-ProfileIntegration $ProfilePath $integrationPath }
169
+ if (-not $SkipAgentRules) { Set-AgentRules $rulesBody }
170
+ Write-InstallStep "✅" "Install complete."
171
+ Write-Host " "
172
+ Write-Host "💡 Try: shrinker help"
173
+ }
174
+
175
+ if (-not $Local -and $UseNpm) {
103
176
  $packageSpec = if ($Version) { "$PackageName@$Version" } else { $PackageName }
104
177
  Write-InstallStep "📦" "Installing $packageSpec from $Registry..."
105
178
  & npm install --silent --global $packageSpec "--registry=$Registry"
@@ -116,6 +189,11 @@ if (-not $Local) {
116
189
  return
117
190
  }
118
191
 
192
+ if (-not $Local) {
193
+ Install-ReleasePackage
194
+ return
195
+ }
196
+
119
197
  $scriptDir = $PSScriptRoot
120
198
  $repoRoot = (Resolve-Path (Join-Path $scriptDir "..\..")).Path
121
199
  $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.4.0",
3
+ "version": "0.7.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)}`);