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.
@@ -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
- if (( LOCAL == 0 )); then
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
- node_version_raw="$(node -v 2>/dev/null || true)"
78
- [[ -n "$node_version_raw" ]] || { print_message "❌" "Node.js was not found on PATH. Install Node.js 22.13+ first." >&2; exit 1; }
79
- node_version="${node_version_raw#v}"
80
- IFS='.' read -r node_major node_minor _ <<< "$node_version"
81
- (( 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
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
- _shrink_get_subcommand() {
25
- local cmd="$1"
26
- shift
27
-
28
- local -a args
29
- args=("$@")
30
-
31
- local -a value_flags
32
- value_flags=(${=SHRINK_OPTION_VALUE_FLAGS[$cmd]})
33
-
34
- local i part
35
- i=1
36
- while (( i <= ${#args[@]} )); do
37
- part="${args[$i]}"
38
- if [[ -z "$part" ]]; then
39
- (( i++ ))
40
- continue
41
- fi
42
-
43
- if (( ${value_flags[(Ie)$part]} > 0 )); then
44
- (( i += 2 ))
45
- continue
46
- fi
47
-
48
- if [[ "$part" == -* ]]; then
49
- (( i++ ))
50
- continue
51
- fi
52
-
53
- print -r -- "${part:l}"
54
- return 0
55
- done
56
-
57
- print -r -- ""
58
- }
59
-
60
- _shrink_should_route() {
61
- local cmd="$1"
62
- shift
63
-
64
- local rules="${SHRINK_RULES[$cmd]}"
65
- [[ -z "$rules" ]] && return 1
66
- [[ "$rules" == *"*"* ]] && return 0
67
-
68
- local subcommand
69
- subcommand="$(_shrink_get_subcommand "$cmd" "$@")"
70
- [[ -z "$subcommand" ]] && return 1
71
-
72
- local -a allowlist
73
- allowlist=(${=rules})
74
- (( ${allowlist[(Ie)$subcommand]} > 0 ))
75
- }
76
-
77
- _shrink_invoke_or_native() {
78
- local cmd="$1"
79
- shift
80
-
81
- if _shrink_should_route "$cmd" "$@" && command -v shrinker >/dev/null 2>&1; then
82
- command shrinker "$cmd" "$@"
83
- return $?
84
- fi
85
-
86
- command "$cmd" "$@"
87
- }
88
-
89
- git() { _shrink_invoke_or_native git "$@"; }
90
- npm() { _shrink_invoke_or_native npm "$@"; }
91
- docker() { _shrink_invoke_or_native docker "$@"; }
92
- kubectl() { _shrink_invoke_or_native kubectl "$@"; }
93
- gh() { _shrink_invoke_or_native gh "$@"; }
94
- rg() { _shrink_invoke_or_native rg "$@"; }
95
- find() { _shrink_invoke_or_native find "$@"; }
96
- tail() { _shrink_invoke_or_native tail "$@"; }
97
- cat() { _shrink_invoke_or_native cat "$@"; }
98
- ls() { _shrink_invoke_or_native ls "$@"; }
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}/.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
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
- if (( SKIP_UNLINK == 0 )); then
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 global npm unlink."
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