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 +1 -0
- package/README.md +48 -3
- package/dist/src/metrics/dashboard.js +8 -1
- package/integrations/macos/install.sh +96 -6
- package/integrations/macos/uninstall.sh +20 -3
- package/integrations/windows/install.ps1 +79 -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,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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,76 @@ 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
|
+
|
|
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.
|
|
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)}`);
|