shrinker-ai 0.3.3 → 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 +494 -363
- package/dist/src/cli.js +144 -25
- package/dist/src/config.js +42 -0
- package/dist/src/execution/run-command.js +2 -2
- package/dist/src/filters/select-filter.js +26 -18
- package/dist/src/metrics/coverage.js +67 -0
- package/dist/src/metrics/dashboard-template.generated.js +4 -0
- package/dist/src/metrics/dashboard.js +137 -115
- package/dist/src/metrics/stats-store.js +211 -51
- package/integrations/macos/install.sh +150 -7
- package/integrations/macos/shrinker-profile.zsh +165 -98
- package/integrations/macos/uninstall.sh +70 -3
- package/integrations/windows/install.ps1 +239 -113
- package/integrations/windows/shrinker-profile.ps1 +206 -136
- package/integrations/windows/uninstall.ps1 +126 -57
- package/package.json +49 -39
- package/scripts/package-release.mjs +129 -0
- package/templates/agent-rules.md +18 -18
- package/.copilot-instructions.md +0 -28
- package/CLAUDE.md +0 -28
|
@@ -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
|
|
@@ -13,7 +16,10 @@ SKIP_PROFILE=0
|
|
|
13
16
|
SKIP_AGENT_RULES=0
|
|
14
17
|
COPILOT_ONLY=0
|
|
15
18
|
CLAUDE_ONLY=0
|
|
19
|
+
TRACK_UNCOVERED=""
|
|
16
20
|
PROFILE_PATH="${PROFILE_PATH:-$HOME/.zshrc}"
|
|
21
|
+
CONFIG_PATH="${SHRINKER_CONFIG_PATH:-$HOME/.shrinker/config}"
|
|
22
|
+
INSTALL_DIR="${SHRINKER_INSTALL_DIR:-$HOME/.shrinker}"
|
|
17
23
|
STEP="${SHRINKER_STEP_OFFSET:-0}"
|
|
18
24
|
|
|
19
25
|
print_message() {
|
|
@@ -26,14 +32,20 @@ print_message() {
|
|
|
26
32
|
while [[ $# -gt 0 ]]; do
|
|
27
33
|
case "$1" in
|
|
28
34
|
--local) LOCAL=1 ;;
|
|
35
|
+
--use-npm) USE_NPM=1 ;;
|
|
29
36
|
--package-name) PACKAGE_NAME="${2:-}"; shift ;;
|
|
30
37
|
--registry) REGISTRY="${2:-}"; shift ;;
|
|
31
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 ;;
|
|
32
42
|
--skip-npm-install) SKIP_NPM_INSTALL=1 ;;
|
|
33
43
|
--skip-build) SKIP_BUILD=1 ;;
|
|
34
44
|
--skip-link) SKIP_LINK=1 ;;
|
|
35
45
|
--enable-profile-routing) ENABLE_PROFILE_ROUTING=1 ;;
|
|
36
46
|
--skip-profile) SKIP_PROFILE=1 ;;
|
|
47
|
+
--enable-uncovered-tracking) TRACK_UNCOVERED=1 ;;
|
|
48
|
+
--disable-uncovered-tracking) TRACK_UNCOVERED=0 ;;
|
|
37
49
|
--skip-agent-rules) SKIP_AGENT_RULES=1 ;;
|
|
38
50
|
--copilot-only) COPILOT_ONLY=1 ;;
|
|
39
51
|
--claude-only) CLAUDE_ONLY=1 ;;
|
|
@@ -46,7 +58,40 @@ done
|
|
|
46
58
|
if (( ENABLE_PROFILE_ROUTING == 1 && SKIP_PROFILE == 1 )); then print_message "❌" "Use either --enable-profile-routing or --skip-profile, not both." >&2; exit 1; fi
|
|
47
59
|
if (( COPILOT_ONLY == 1 && CLAUDE_ONLY == 1 )); then print_message "❌" "Use either --copilot-only or --claude-only, not both." >&2; exit 1; fi
|
|
48
60
|
|
|
49
|
-
|
|
61
|
+
# Prompt only on a TTY; non-interactive installs keep tracking on and leave the shell profile alone.
|
|
62
|
+
prompt_yes_no() {
|
|
63
|
+
local question="$1"
|
|
64
|
+
local default_answer="$2"
|
|
65
|
+
local reply
|
|
66
|
+
local hint="[y/N]"
|
|
67
|
+
[[ "$default_answer" == "1" ]] && hint="[Y/n]"
|
|
68
|
+
printf ' %s %s ' "$question" "$hint" > /dev/tty
|
|
69
|
+
IFS= read -r reply < /dev/tty || reply=""
|
|
70
|
+
case "$(printf '%s' "$reply" | tr '[:upper:]' '[:lower:]')" in
|
|
71
|
+
y|yes) echo 1 ;;
|
|
72
|
+
n|no) echo 0 ;;
|
|
73
|
+
*) echo "$default_answer" ;;
|
|
74
|
+
esac
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interactive=0
|
|
78
|
+
[[ -t 0 && -r /dev/tty ]] && interactive=1
|
|
79
|
+
|
|
80
|
+
if [[ -z "$TRACK_UNCOVERED" ]]; then
|
|
81
|
+
if (( interactive )); then
|
|
82
|
+
print_message "❓" "Track uncovered commands? Records which unfiltered commands cost the most tokens."
|
|
83
|
+
TRACK_UNCOVERED="$(prompt_yes_no "Enable uncovered-command tracking?" 1)"
|
|
84
|
+
else
|
|
85
|
+
TRACK_UNCOVERED=1
|
|
86
|
+
fi
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
if (( interactive && SKIP_PROFILE == 0 && ENABLE_PROFILE_ROUTING == 0 )); then
|
|
90
|
+
print_message "❓" "Route wrapped commands through shrinker automatically? Adds a source line to $PROFILE_PATH."
|
|
91
|
+
ENABLE_PROFILE_ROUTING="$(prompt_yes_no "Enable automatic shell routing?" 0)"
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
if (( LOCAL == 0 && USE_NPM == 1 )); then
|
|
50
95
|
package_spec="$PACKAGE_NAME"
|
|
51
96
|
[[ -z "$VERSION" ]] || package_spec="$PACKAGE_NAME@$VERSION"
|
|
52
97
|
print_message "📦" "Installing $package_spec from $REGISTRY..."
|
|
@@ -60,6 +105,7 @@ if (( LOCAL == 0 )); then
|
|
|
60
105
|
$( (( SKIP_AGENT_RULES )) && echo --skip-agent-rules ) \
|
|
61
106
|
$( (( COPILOT_ONLY )) && echo --copilot-only ) \
|
|
62
107
|
$( (( CLAUDE_ONLY )) && echo --claude-only ) \
|
|
108
|
+
$( (( TRACK_UNCOVERED )) && echo --enable-uncovered-tracking || echo --disable-uncovered-tracking ) \
|
|
63
109
|
--profile-path "$PROFILE_PATH"
|
|
64
110
|
if [[ -n "${BASH_SOURCE[0]:-}" && "${BASH_SOURCE[0]}" != "$0" ]]; then
|
|
65
111
|
return 0
|
|
@@ -74,11 +120,13 @@ INTEGRATION_PATH="$SCRIPT_DIR/shrinker-profile.zsh"
|
|
|
74
120
|
BLOCK_START="<!-- shrinker agent rules start -->"
|
|
75
121
|
BLOCK_END="<!-- shrinker agent rules end -->"
|
|
76
122
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
82
130
|
|
|
83
131
|
set_agent_rules() {
|
|
84
132
|
local target="$1"
|
|
@@ -123,6 +171,100 @@ add_profile_integration() {
|
|
|
123
171
|
fi
|
|
124
172
|
}
|
|
125
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
|
+
|
|
187
|
+
set_config_value() {
|
|
188
|
+
local key="$1"
|
|
189
|
+
local value="$2"
|
|
190
|
+
mkdir -p "$(dirname "$CONFIG_PATH")"
|
|
191
|
+
chmod 700 "$(dirname "$CONFIG_PATH")" 2>/dev/null || true
|
|
192
|
+
touch "$CONFIG_PATH"
|
|
193
|
+
if grep -Eq "^[[:space:]]*$key[[:space:]]*=" "$CONFIG_PATH"; then
|
|
194
|
+
grep -Ev "^[[:space:]]*$key[[:space:]]*=" "$CONFIG_PATH" > "$CONFIG_PATH.tmp" || true
|
|
195
|
+
mv "$CONFIG_PATH.tmp" "$CONFIG_PATH"
|
|
196
|
+
fi
|
|
197
|
+
printf '%s=%s\n' "$key" "$value" >> "$CONFIG_PATH"
|
|
198
|
+
print_message "⚙️" "Set $key=$value in: $CONFIG_PATH"
|
|
199
|
+
}
|
|
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
|
+
|
|
126
268
|
print_message "📦" "Installing shrinker from: $REPO_ROOT"
|
|
127
269
|
pushd "$REPO_ROOT" >/dev/null
|
|
128
270
|
if (( SKIP_NPM_INSTALL == 0 )); then
|
|
@@ -131,7 +273,7 @@ if (( SKIP_NPM_INSTALL == 0 )); then
|
|
|
131
273
|
fi
|
|
132
274
|
if (( SKIP_BUILD == 0 )); then
|
|
133
275
|
print_message "🏗️" "Building shrinker..."
|
|
134
|
-
npm run build --silent
|
|
276
|
+
SHRINKER_BUILD_QUIET=1 npm run build --silent
|
|
135
277
|
fi
|
|
136
278
|
if (( SKIP_LINK == 0 )); then
|
|
137
279
|
print_message "🔗" "Linking shrinker globally..."
|
|
@@ -139,6 +281,7 @@ if (( SKIP_LINK == 0 )); then
|
|
|
139
281
|
fi
|
|
140
282
|
popd >/dev/null
|
|
141
283
|
|
|
284
|
+
set_config_value "SHRINKER_TRACK_UNCOVERED" "$TRACK_UNCOVERED"
|
|
142
285
|
if (( SKIP_PROFILE == 0 && ENABLE_PROFILE_ROUTING == 1 )); then add_profile_integration "$PROFILE_PATH"; fi
|
|
143
286
|
if (( SKIP_AGENT_RULES == 0 )); then
|
|
144
287
|
[[ -f "$TEMPLATE_PATH" ]] || { print_message "❌" "Agent rules template not found: $TEMPLATE_PATH" >&2; exit 1; }
|
|
@@ -1,98 +1,165 @@
|
|
|
1
|
-
typeset -gA SHRINK_RULES
|
|
2
|
-
SHRINK_RULES=(
|
|
3
|
-
git "status diff log show reflog branch tag stash"
|
|
4
|
-
npm "test t install i ci ls list"
|
|
5
|
-
docker "ps logs images compose"
|
|
6
|
-
kubectl "get describe logs"
|
|
7
|
-
gh "pr issue run"
|
|
8
|
-
rg "*"
|
|
9
|
-
find "*"
|
|
10
|
-
tail "*"
|
|
11
|
-
cat "*"
|
|
12
|
-
ls "*"
|
|
13
|
-
)
|
|
14
|
-
|
|
15
|
-
typeset -gA SHRINK_OPTION_VALUE_FLAGS
|
|
16
|
-
SHRINK_OPTION_VALUE_FLAGS=(
|
|
17
|
-
git "-C -c --git-dir --work-tree --namespace"
|
|
18
|
-
npm "--prefix --cache --registry --workspace --userconfig -w -C"
|
|
19
|
-
docker "-H --host --context --config"
|
|
20
|
-
kubectl "-n --namespace -o --output --context --kubeconfig --cluster --user"
|
|
21
|
-
gh "-R --repo"
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
1
|
+
typeset -gA SHRINK_RULES
|
|
2
|
+
SHRINK_RULES=(
|
|
3
|
+
git "status diff log show reflog branch tag stash"
|
|
4
|
+
npm "test t install i ci ls list"
|
|
5
|
+
docker "ps logs images compose"
|
|
6
|
+
kubectl "get describe logs"
|
|
7
|
+
gh "pr issue run"
|
|
8
|
+
rg "*"
|
|
9
|
+
find "*"
|
|
10
|
+
tail "*"
|
|
11
|
+
cat "*"
|
|
12
|
+
ls "*"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
typeset -gA SHRINK_OPTION_VALUE_FLAGS
|
|
16
|
+
SHRINK_OPTION_VALUE_FLAGS=(
|
|
17
|
+
git "-C -c --git-dir --work-tree --namespace"
|
|
18
|
+
npm "--prefix --cache --registry --workspace --userconfig -w -C"
|
|
19
|
+
docker "-H --host --context --config"
|
|
20
|
+
kubectl "-n --namespace -o --output --context --kubeconfig --cluster --user"
|
|
21
|
+
gh "-R --repo"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# Resolved once at load so wrapped commands never pay for reading the config file.
|
|
25
|
+
typeset -g SHRINK_TRACK_UNCOVERED_DEFAULT=""
|
|
26
|
+
_shrink_load_config() {
|
|
27
|
+
local config="${SHRINKER_CONFIG_PATH:-$HOME/.shrinker/config}"
|
|
28
|
+
[[ -r "$config" ]] || return 0
|
|
29
|
+
local value
|
|
30
|
+
value="$(sed -n 's/^[[:space:]]*SHRINKER_TRACK_UNCOVERED[[:space:]]*=[[:space:]]*\([^#[:space:]]*\).*/\1/p' "$config" | tail -n 1)"
|
|
31
|
+
SHRINK_TRACK_UNCOVERED_DEFAULT="$value"
|
|
32
|
+
}
|
|
33
|
+
_shrink_load_config
|
|
34
|
+
|
|
35
|
+
_shrink_uncovered_enabled() {
|
|
36
|
+
local value="${SHRINKER_TRACK_UNCOVERED:-$SHRINK_TRACK_UNCOVERED_DEFAULT}"
|
|
37
|
+
case "${value:l}" in
|
|
38
|
+
1|true|yes) return 0 ;;
|
|
39
|
+
*) return 1 ;;
|
|
40
|
+
esac
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
_shrink_get_subcommand() {
|
|
44
|
+
local cmd="$1"
|
|
45
|
+
shift
|
|
46
|
+
|
|
47
|
+
local -a args
|
|
48
|
+
args=("$@")
|
|
49
|
+
|
|
50
|
+
local -a value_flags
|
|
51
|
+
value_flags=(${=SHRINK_OPTION_VALUE_FLAGS[$cmd]})
|
|
52
|
+
|
|
53
|
+
local i part
|
|
54
|
+
i=1
|
|
55
|
+
while (( i <= ${#args[@]} )); do
|
|
56
|
+
part="${args[$i]}"
|
|
57
|
+
if [[ -z "$part" ]]; then
|
|
58
|
+
(( i++ ))
|
|
59
|
+
continue
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
if (( ${value_flags[(Ie)$part]} > 0 )); then
|
|
63
|
+
(( i += 2 ))
|
|
64
|
+
continue
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
if [[ "$part" == -* ]]; then
|
|
68
|
+
(( i++ ))
|
|
69
|
+
continue
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
print -r -- "${part:l}"
|
|
73
|
+
return 0
|
|
74
|
+
done
|
|
75
|
+
|
|
76
|
+
print -r -- ""
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
_shrink_should_route() {
|
|
80
|
+
local cmd="$1"
|
|
81
|
+
shift
|
|
82
|
+
|
|
83
|
+
local rules="${SHRINK_RULES[$cmd]}"
|
|
84
|
+
[[ -z "$rules" ]] && return 1
|
|
85
|
+
[[ "$rules" == *"*"* ]] && return 0
|
|
86
|
+
|
|
87
|
+
local subcommand
|
|
88
|
+
subcommand="$(_shrink_get_subcommand "$cmd" "$@")"
|
|
89
|
+
[[ -z "$subcommand" ]] && return 1
|
|
90
|
+
|
|
91
|
+
local -a allowlist
|
|
92
|
+
allowlist=(${=rules})
|
|
93
|
+
(( ${allowlist[(Ie)$subcommand]} > 0 ))
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
_shrink_track_uncovered() {
|
|
97
|
+
local cmd="$1"
|
|
98
|
+
local bytes="$2"
|
|
99
|
+
local status_code="$3"
|
|
100
|
+
shift 3
|
|
101
|
+
|
|
102
|
+
_shrink_uncovered_enabled || return 0
|
|
103
|
+
command -v shrinker >/dev/null 2>&1 || return 0
|
|
104
|
+
|
|
105
|
+
local subcommand
|
|
106
|
+
subcommand="$(_shrink_get_subcommand "$cmd" "$@")"
|
|
107
|
+
|
|
108
|
+
local -a track_args
|
|
109
|
+
track_args=(track --executable "$cmd" --bytes "$bytes" --exit-code "$status_code")
|
|
110
|
+
[[ -n "$subcommand" ]] && track_args+=(--subcommand "$subcommand")
|
|
111
|
+
|
|
112
|
+
(command shrinker "${track_args[@]}" >/dev/null 2>&1 &)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
_shrink_run_native_tracked() {
|
|
116
|
+
local cmd="$1"
|
|
117
|
+
shift
|
|
118
|
+
|
|
119
|
+
if ! _shrink_uncovered_enabled; then
|
|
120
|
+
command "$cmd" "$@"
|
|
121
|
+
return $?
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
# Only measure output volume when stdout is redirected (the agent case). Interactive
|
|
125
|
+
# terminals keep the native command untouched so pagers and prompts still work.
|
|
126
|
+
if [[ -t 1 ]]; then
|
|
127
|
+
command "$cmd" "$@"
|
|
128
|
+
local status_code=$?
|
|
129
|
+
_shrink_track_uncovered "$cmd" 0 "$status_code" "$@"
|
|
130
|
+
return $status_code
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
local capture
|
|
134
|
+
capture="$(mktemp -t shrinker-track)" || { command "$cmd" "$@"; return $?; }
|
|
135
|
+
command "$cmd" "$@" | tee "$capture"
|
|
136
|
+
local status_code=${pipestatus[1]}
|
|
137
|
+
local bytes
|
|
138
|
+
bytes="$(wc -c < "$capture" | tr -d ' ')"
|
|
139
|
+
rm -f "$capture"
|
|
140
|
+
_shrink_track_uncovered "$cmd" "${bytes:-0}" "$status_code" "$@"
|
|
141
|
+
return $status_code
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
_shrink_invoke_or_native() {
|
|
145
|
+
local cmd="$1"
|
|
146
|
+
shift
|
|
147
|
+
|
|
148
|
+
if _shrink_should_route "$cmd" "$@" && command -v shrinker >/dev/null 2>&1; then
|
|
149
|
+
command shrinker "$cmd" "$@"
|
|
150
|
+
return $?
|
|
151
|
+
fi
|
|
152
|
+
|
|
153
|
+
_shrink_run_native_tracked "$cmd" "$@"
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
git() { _shrink_invoke_or_native git "$@"; }
|
|
157
|
+
npm() { _shrink_invoke_or_native npm "$@"; }
|
|
158
|
+
docker() { _shrink_invoke_or_native docker "$@"; }
|
|
159
|
+
kubectl() { _shrink_invoke_or_native kubectl "$@"; }
|
|
160
|
+
gh() { _shrink_invoke_or_native gh "$@"; }
|
|
161
|
+
rg() { _shrink_invoke_or_native rg "$@"; }
|
|
162
|
+
find() { _shrink_invoke_or_native find "$@"; }
|
|
163
|
+
tail() { _shrink_invoke_or_native tail "$@"; }
|
|
164
|
+
cat() { _shrink_invoke_or_native cat "$@"; }
|
|
165
|
+
ls() { _shrink_invoke_or_native ls "$@"; }
|
|
@@ -1,13 +1,18 @@
|
|
|
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
|
|
9
10
|
CLAUDE_ONLY=0
|
|
11
|
+
PURGE_DATA=0
|
|
12
|
+
DASHBOARD_PORT=4317
|
|
10
13
|
PROFILE_PATH="${PROFILE_PATH:-$HOME/.zshrc}"
|
|
14
|
+
CONFIG_PATH="${SHRINKER_CONFIG_PATH:-$HOME/.shrinker/config}"
|
|
15
|
+
DATA_DIR="$(dirname "$CONFIG_PATH")"
|
|
11
16
|
UNINSTALL_STEP=0
|
|
12
17
|
|
|
13
18
|
print_step() {
|
|
@@ -18,6 +23,7 @@ print_step() {
|
|
|
18
23
|
|
|
19
24
|
while [[ $# -gt 0 ]]; do
|
|
20
25
|
case "$1" in
|
|
26
|
+
--use-npm) USE_NPM=1 ;;
|
|
21
27
|
--skip-unlink) SKIP_UNLINK=1 ;;
|
|
22
28
|
--skip-agent-rules) SKIP_AGENT_RULES=1 ;;
|
|
23
29
|
--copilot-only) COPILOT_ONLY=1 ;;
|
|
@@ -25,6 +31,8 @@ while [[ $# -gt 0 ]]; do
|
|
|
25
31
|
--profile-path) PROFILE_PATH="${2:-}"; shift ;;
|
|
26
32
|
--install-dir) INSTALL_DIR="${2:-}"; shift ;;
|
|
27
33
|
--remove-install-dir) REMOVE_INSTALL_DIR=1 ;;
|
|
34
|
+
--purge-data) PURGE_DATA=1 ;;
|
|
35
|
+
--port) DASHBOARD_PORT="${2:-}"; shift ;;
|
|
28
36
|
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
|
29
37
|
esac
|
|
30
38
|
shift
|
|
@@ -48,20 +56,79 @@ remove_profile_integration() {
|
|
|
48
56
|
mv "$profile_file.tmp" "$profile_file"
|
|
49
57
|
}
|
|
50
58
|
|
|
51
|
-
|
|
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
|
+
|
|
70
|
+
# The dashboard runs as a detached daemon, so unlinking the package never stops it.
|
|
71
|
+
stop_dashboard_server() {
|
|
72
|
+
local url="http://127.0.0.1:$DASHBOARD_PORT"
|
|
73
|
+
if command -v curl >/dev/null 2>&1; then
|
|
74
|
+
curl -fsS -m 3 -X POST "$url/__shrinker_shutdown" -o /dev/null 2>/dev/null || true
|
|
75
|
+
sleep 1
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
local pid
|
|
79
|
+
pid="$(lsof -nP -iTCP:"$DASHBOARD_PORT" -sTCP:LISTEN -t 2>/dev/null | head -n 1 || true)"
|
|
80
|
+
if [[ -n "$pid" ]]; then
|
|
81
|
+
kill "$pid" 2>/dev/null || true
|
|
82
|
+
sleep 1
|
|
83
|
+
pid="$(lsof -nP -iTCP:"$DASHBOARD_PORT" -sTCP:LISTEN -t 2>/dev/null | head -n 1 || true)"
|
|
84
|
+
[[ -n "$pid" ]] && kill -9 "$pid" 2>/dev/null || true
|
|
85
|
+
fi
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
# Only drop installer-managed keys so hand-written settings survive.
|
|
89
|
+
remove_managed_config() {
|
|
90
|
+
[[ -f "$CONFIG_PATH" ]] || return 0
|
|
91
|
+
grep -Ev '^[[:space:]]*SHRINKER_TRACK_UNCOVERED[[:space:]]*=' "$CONFIG_PATH" > "$CONFIG_PATH.tmp" || true
|
|
92
|
+
if [[ -s "$CONFIG_PATH.tmp" ]]; then
|
|
93
|
+
mv "$CONFIG_PATH.tmp" "$CONFIG_PATH"
|
|
94
|
+
else
|
|
95
|
+
rm -f "$CONFIG_PATH.tmp" "$CONFIG_PATH"
|
|
96
|
+
fi
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
print_step "🛑" "Stopping the dashboard server on port $DASHBOARD_PORT..."
|
|
100
|
+
stop_dashboard_server
|
|
101
|
+
|
|
102
|
+
if (( SKIP_UNLINK == 0 && USE_NPM == 1 )); then
|
|
52
103
|
print_step "🔗" "Unlinking shrinker globally..."
|
|
53
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
|
|
54
108
|
else
|
|
55
|
-
print_step "⏭️" "Skipped
|
|
109
|
+
print_step "⏭️" "Skipped unlink/removal."
|
|
56
110
|
fi
|
|
57
111
|
print_step "🔧" "Removing shell profile integration..."
|
|
58
112
|
remove_profile_integration "$PROFILE_PATH"
|
|
113
|
+
remove_path_integration "$PROFILE_PATH"
|
|
59
114
|
if (( SKIP_AGENT_RULES == 0 )); then
|
|
60
115
|
(( CLAUDE_ONLY )) || remove_agent_rules "$HOME/.copilot/copilot-instructions.md"
|
|
61
116
|
(( COPILOT_ONLY )) || remove_agent_rules "$HOME/.claude/CLAUDE.md"
|
|
62
117
|
else
|
|
63
118
|
print_step "⏭️" "Skipped managed agent rules."
|
|
64
119
|
fi
|
|
120
|
+
|
|
121
|
+
print_step "🔧" "Removing managed settings..."
|
|
122
|
+
remove_managed_config
|
|
123
|
+
rm -f "$DATA_DIR/dashboard.html"
|
|
124
|
+
|
|
125
|
+
if (( PURGE_DATA == 1 )); then
|
|
126
|
+
rm -rf "$DATA_DIR"
|
|
127
|
+
print_step "🧹" "Removed local data: $DATA_DIR"
|
|
128
|
+
elif [[ -d "$DATA_DIR" ]]; then
|
|
129
|
+
print_step "💾" "Kept saved stats in $DATA_DIR (use --purge-data to delete)."
|
|
130
|
+
fi
|
|
131
|
+
|
|
65
132
|
print_step "✅" "Uninstall complete."
|
|
66
133
|
|
|
67
134
|
if (( REMOVE_INSTALL_DIR == 1 )) && [[ -d "$INSTALL_DIR" ]]; then
|