acstack 0.48.5 → 0.49.1
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/.claude-plugin/plugin.json +1 -1
- package/assets/terminal-workspace/helpers/acstack-bridge +185 -0
- package/assets/terminal-workspace/helpers/acstack-clip +88 -0
- package/assets/terminal-workspace/helpers/acstack-dash +25 -0
- package/assets/terminal-workspace/helpers/acstack-db +86 -0
- package/assets/terminal-workspace/helpers/acstack-diff +268 -0
- package/assets/terminal-workspace/helpers/acstack-edit +25 -0
- package/assets/terminal-workspace/helpers/acstack-gh +47 -0
- package/assets/terminal-workspace/helpers/acstack-herdr-renumber +93 -0
- package/assets/terminal-workspace/helpers/acstack-herdr-tab +49 -0
- package/assets/terminal-workspace/helpers/acstack-md +401 -0
- package/assets/terminal-workspace/helpers/acstack-md-lib +219 -0
- package/assets/terminal-workspace/helpers/acstack-open +217 -0
- package/assets/terminal-workspace/helpers/acstack-popup +13 -0
- package/assets/terminal-workspace/helpers/acstack-view +33 -0
- package/assets/terminal-workspace/helpers/xclip +31 -0
- package/assets/terminal-workspace/lib/apply-ghdash.sh +78 -0
- package/assets/terminal-workspace/lib/apply-herdr-numbers.sh +49 -0
- package/assets/terminal-workspace/lib/apply-herdr.sh +308 -0
- package/assets/terminal-workspace/lib/apply-tools.sh +293 -0
- package/assets/terminal-workspace/lib/apply-tuicr.sh +133 -0
- package/assets/terminal-workspace/lib/apply-yazi.sh +389 -0
- package/assets/terminal-workspace/lib/commands.sh +296 -0
- package/assets/terminal-workspace/lib/core.sh +184 -0
- package/assets/terminal-workspace/lib/helper-install.sh +35 -0
- package/assets/terminal-workspace/lib/pi-packages.sh +331 -0
- package/assets/terminal-workspace/lib/pi-state.sh +479 -0
- package/assets/terminal-workspace/lib/versions.sh +254 -0
- package/assets/terminal-workspace/setup.sh +24 -4637
- package/cli/dist/index.js +8 -4
- package/cli/dist/index.js.map +2 -2
- package/cli/package.json +1 -1
- package/package.json +1 -1
- package/skills/archive/SKILL.md +3 -0
- package/skills/grilling/SKILL.md +2 -1
- package/skills/handoff/SKILL.md +14 -14
- package/skills/handoff/launch-herdr.md +15 -4
- package/skills/implement/SKILL.md +19 -3
- package/skills/map/SKILL.md +24 -24
- package/skills/map/alignment.md +38 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
acstack_version() {
|
|
2
|
+
json_field "$SCRIPT_DIR/../../.claude-plugin/plugin.json" version
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
# Installs one pi package, or moves its pin. Ownership is taken only when the
|
|
6
|
+
# package was absent: one the user installed stays theirs, pin and all.
|
|
7
|
+
pi_add() { # pi_add <source> <identity>
|
|
8
|
+
local cur
|
|
9
|
+
cur=$(pi_state pkg "$2")
|
|
10
|
+
if [ -n "$cur" ]; then
|
|
11
|
+
# Unpinned, presence is enough: a pin the user chose stays.
|
|
12
|
+
case "$1" in "$2"@*) ;; *) say "pi: $2 already installed"; return 0 ;; esac
|
|
13
|
+
[ "$cur" = "$1" ] && { say "pi: $1 already installed"; return 0; }
|
|
14
|
+
fi
|
|
15
|
+
if ! pi install "$1" >/dev/null 2>&1; then
|
|
16
|
+
say "pi: install of $1 failed, run: pi install $1"
|
|
17
|
+
return 1
|
|
18
|
+
fi
|
|
19
|
+
if [ -n "$cur" ]; then
|
|
20
|
+
pi_state pin "$2" "$cur" "$1"
|
|
21
|
+
say "pi: $cur moved to $1"
|
|
22
|
+
else
|
|
23
|
+
pi_state own pkg "$2"
|
|
24
|
+
say "pi: $1 installed"
|
|
25
|
+
fi
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# Puts back a pin apply moved, while the pin is still the one apply set.
|
|
29
|
+
pi_unpin() { # pi_unpin <identity>
|
|
30
|
+
local rec prior ours cur
|
|
31
|
+
rec=$(pi_state pin-get "$1") || return 0
|
|
32
|
+
prior="${rec%%$'\t'*}" ours="${rec#*$'\t'}"
|
|
33
|
+
cur=$(pi_state pkg "$1")
|
|
34
|
+
if [ "$cur" = "$ours" ]; then
|
|
35
|
+
have pi || { say "pi: not on PATH, so $cur stays pinned"; return 0; }
|
|
36
|
+
pi install "$prior" >/dev/null 2>&1 \
|
|
37
|
+
|| { say "pi: could not put back $prior, run: pi install $prior"; return 0; }
|
|
38
|
+
say "pi: $cur put back to $prior"
|
|
39
|
+
fi
|
|
40
|
+
pi_state pin-forget "$1"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# Removes a package this setup installed. One the user installed is left.
|
|
44
|
+
pi_drop() { # pi_drop <identity>
|
|
45
|
+
local cur
|
|
46
|
+
pi_state owns pkg "$1" || return 0
|
|
47
|
+
cur=$(pi_state pkg "$1")
|
|
48
|
+
if [ -n "$cur" ]; then
|
|
49
|
+
have pi || { say "pi: not on PATH, so $cur stays installed"; return 0; }
|
|
50
|
+
pi remove "$cur" >/dev/null 2>&1 || { say "pi: remove of $cur failed, run: pi remove $cur"; return 0; }
|
|
51
|
+
say "pi: $cur removed"
|
|
52
|
+
fi
|
|
53
|
+
pi_state disown pkg "$1"
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
pi_npm_package() { # pi_npm_package <name> -> npm package identity, or nothing
|
|
57
|
+
version_table | awk -F'\t' -v n="$1" '$1==n && $2=="pi" && $3=="npm"{print "npm:" $4; found=1; exit} END{exit !found}'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# A package pi holds at a fixed version, such as npm:pi-subagents@1.0.0.
|
|
61
|
+
pi_npm_pin() { # pi_npm_pin <name>
|
|
62
|
+
local pkg src
|
|
63
|
+
pkg=$(pi_npm_package "$1") || return 1
|
|
64
|
+
src=$(pi_state pkg "$pkg") || return 1
|
|
65
|
+
[ "$src" != "$pkg" ]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
pi_suggest_todo_discipline() {
|
|
69
|
+
local needle="When using a todo list, keep it current as work completes."
|
|
70
|
+
grep -qsF "$needle" "$PI_DIR"/AGENTS.override.md "$PI_DIR"/AGENTS.md "$PI_DIR"/AGENTS.MD \
|
|
71
|
+
"$PI_DIR"/CLAUDE.md "$PI_DIR"/CLAUDE.MD && return 0
|
|
72
|
+
say "pi: todo tool installed. For stricter todo discipline, ask the agent"
|
|
73
|
+
say "pi: running this setup to hand-edit $PI_DIR/AGENTS.md and add:"
|
|
74
|
+
say "pi: $needle Before reporting a task as done, check the todo list"
|
|
75
|
+
say "pi: and mark every completed step completed. Do not leave stale"
|
|
76
|
+
say "pi: in-progress or pending todos after a commit, PR, merge or workflow gate."
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# The pin apply would move to, or nothing when the installed one is current
|
|
80
|
+
# or newer. A newer pin is one the user chose, so it is never moved back.
|
|
81
|
+
pi_acstack_behind() {
|
|
82
|
+
local cur ver pin
|
|
83
|
+
ver=$(acstack_version); [ -n "$ver" ] || return 0
|
|
84
|
+
cur=$(pi_state pkg "$PI_ACSTACK") || return 0
|
|
85
|
+
case "$cur" in "$PI_ACSTACK"@*) pin="${cur##*@}" ;; *) return 0 ;; esac
|
|
86
|
+
[ "$pin" = "$ver" ] && return 0
|
|
87
|
+
[ "$(printf '%s\n%s\n' "$pin" "$ver" | sort -V | tail -1)" = "$ver" ] || return 0
|
|
88
|
+
printf '%s\t%s' "$pin" "$ver"
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
bili_bin() {
|
|
92
|
+
if [ -x "$PI_DIR/npm/node_modules/.bin/bili" ]; then
|
|
93
|
+
printf '%s' "$PI_DIR/npm/node_modules/.bin/bili"
|
|
94
|
+
elif have bili; then
|
|
95
|
+
command -v bili
|
|
96
|
+
else
|
|
97
|
+
return 1
|
|
98
|
+
fi
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
bili_status() { # bili_status <bin> <client> -> installed|absent|unknown
|
|
102
|
+
local line
|
|
103
|
+
line=$("$1" plugin list 2>/dev/null | awk -v c="$2" '$1==c{print; exit}') || true
|
|
104
|
+
[ -n "$line" ] || { echo unknown; return 0; }
|
|
105
|
+
case "$line" in *"not installed"*) echo absent ;; *) echo installed ;; esac
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
bili_client() { # bili_client install|remove <client>
|
|
109
|
+
local op="$1" client="$2" bin marker status
|
|
110
|
+
marker="bili:$client"
|
|
111
|
+
if [ "$op" = "install" ]; then
|
|
112
|
+
[ "$client" = "pi" ] || have "$client" || return 0
|
|
113
|
+
pi_state owns file "$marker" && return 0
|
|
114
|
+
if bin=$(bili_bin); then
|
|
115
|
+
status=$(bili_status "$bin" "$client")
|
|
116
|
+
if [ "$status" = "installed" ]; then
|
|
117
|
+
say "pi: billion-context already enabled for $client"
|
|
118
|
+
return 0
|
|
119
|
+
fi
|
|
120
|
+
if "$bin" plugin install "$client" >/dev/null 2>&1; then
|
|
121
|
+
pi_state own file "$marker"
|
|
122
|
+
say "pi: billion-context enabled for $client"
|
|
123
|
+
else
|
|
124
|
+
say "pi: billion-context $client setup failed, run: bili plugin install $client"
|
|
125
|
+
fi
|
|
126
|
+
else
|
|
127
|
+
say "pi: bili command not found, so billion-context $client setup was skipped"
|
|
128
|
+
fi
|
|
129
|
+
elif pi_state owns file "$marker"; then
|
|
130
|
+
if bin=$(bili_bin); then
|
|
131
|
+
if "$bin" plugin remove "$client" >/dev/null 2>&1; then
|
|
132
|
+
pi_state disown file "$marker"
|
|
133
|
+
say "pi: billion-context disabled for $client"
|
|
134
|
+
else
|
|
135
|
+
say "pi: billion-context $client removal failed, run: bili plugin remove $client"
|
|
136
|
+
fi
|
|
137
|
+
else
|
|
138
|
+
say "pi: bili command not found, so billion-context $client setup was left installed"
|
|
139
|
+
fi
|
|
140
|
+
fi
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
apply_pi() {
|
|
144
|
+
find_pi
|
|
145
|
+
if ! have pi; then
|
|
146
|
+
have npm || {
|
|
147
|
+
say "npm not installed, skipping pi"
|
|
148
|
+
say "install Node.js, then run apply again"
|
|
149
|
+
return 0
|
|
150
|
+
}
|
|
151
|
+
npm install -g @earendil-works/pi-coding-agent >/dev/null 2>&1 \
|
|
152
|
+
|| { say "pi install failed, run: npm install -g @earendil-works/pi-coding-agent"; return 0; }
|
|
153
|
+
have pi || { say "pi installed, but npm's global bin is not on PATH"; return 0; }
|
|
154
|
+
say "pi installed"
|
|
155
|
+
fi
|
|
156
|
+
# pi rewrites settings.json on every install. Handing it a file it cannot
|
|
157
|
+
# parse risks the user's settings, so concede the whole component instead.
|
|
158
|
+
if ! pi_state parses "$PI_SETTINGS"; then
|
|
159
|
+
say "pi: $PI_SETTINGS does not parse, so nothing was installed into pi"
|
|
160
|
+
say "fix that file, then run apply again"
|
|
161
|
+
return 0
|
|
162
|
+
fi
|
|
163
|
+
|
|
164
|
+
patch_pi_win32_paste
|
|
165
|
+
|
|
166
|
+
# ACStack cannot launch a subagent on pi without pi-subagents.
|
|
167
|
+
pi_add npm:pi-subagents npm:pi-subagents
|
|
168
|
+
if [ "$(cfg_get pi.todo true)" = "true" ]; then
|
|
169
|
+
pi_add npm:@juicesharp/rpiv-todo npm:@juicesharp/rpiv-todo
|
|
170
|
+
pi_suggest_todo_discipline
|
|
171
|
+
else
|
|
172
|
+
pi_drop npm:@juicesharp/rpiv-todo
|
|
173
|
+
fi
|
|
174
|
+
if [ "$(cfg_get pi.plugin_manager true)" = "true" ]; then
|
|
175
|
+
pi_add npm:pi-plugin-manager npm:pi-plugin-manager
|
|
176
|
+
else
|
|
177
|
+
pi_drop npm:pi-plugin-manager
|
|
178
|
+
fi
|
|
179
|
+
# bili's proxy dies with the session that started it, cutting off every
|
|
180
|
+
# other Claude Code or Codex session. Keep it in pi; undo older installs.
|
|
181
|
+
bili_client remove codex
|
|
182
|
+
bili_client remove claude
|
|
183
|
+
if [ "$(cfg_get pi.billion_context true)" = "true" ]; then
|
|
184
|
+
pi_state merge "$BILLION_CONFIG" billion '{"pi.subagents": false}'
|
|
185
|
+
if [ "$(json_field "$BILLION_CONFIG" pi.subagents)" = "False" ]; then
|
|
186
|
+
pi_add "$PI_BILLION" "$PI_BILLION"
|
|
187
|
+
bili_client install pi
|
|
188
|
+
else
|
|
189
|
+
say "pi: billion-context acp_delegate could not be disabled, so billion-context setup was skipped"
|
|
190
|
+
fi
|
|
191
|
+
else
|
|
192
|
+
bili_client remove pi
|
|
193
|
+
pi_state merge "$BILLION_CONFIG" billion '{"pi.subagents": null}'
|
|
194
|
+
pi_drop "$PI_BILLION"
|
|
195
|
+
fi
|
|
196
|
+
if [ "$(cfg_get pi.question_tool true)" = "true" ]; then
|
|
197
|
+
pi_add npm:@juicesharp/rpiv-ask-user-question npm:@juicesharp/rpiv-ask-user-question
|
|
198
|
+
else
|
|
199
|
+
pi_drop npm:@juicesharp/rpiv-ask-user-question
|
|
200
|
+
fi
|
|
201
|
+
if [ "$(cfg_get pi.web_access true)" = "true" ]; then
|
|
202
|
+
pi_add npm:pi-web-access npm:pi-web-access
|
|
203
|
+
else
|
|
204
|
+
pi_drop npm:pi-web-access
|
|
205
|
+
fi
|
|
206
|
+
if [ "$(cfg_get pi.lens false)" = "true" ]; then
|
|
207
|
+
pi_add npm:pi-lens npm:pi-lens
|
|
208
|
+
else
|
|
209
|
+
pi_drop npm:pi-lens
|
|
210
|
+
fi
|
|
211
|
+
if [ "$(cfg_get pi.slash_anywhere true)" = "true" ]; then
|
|
212
|
+
pi_add "$PI_SLASH_ANYWHERE" "$PI_SLASH_ANYWHERE"
|
|
213
|
+
else
|
|
214
|
+
pi_drop "$PI_SLASH_ANYWHERE"
|
|
215
|
+
fi
|
|
216
|
+
local old
|
|
217
|
+
if old=$(pi_state pkg "$PI_ACSTACK_GIT"); then
|
|
218
|
+
say "pi: $old serves old ACStack skills, run: pi remove $old"
|
|
219
|
+
fi
|
|
220
|
+
local ver cur; ver=$(acstack_version)
|
|
221
|
+
if [ -n "$ver" ]; then
|
|
222
|
+
if cur=$(pi_state pkg "$PI_ACSTACK") && [ -z "$(pi_acstack_behind)" ]; then
|
|
223
|
+
say "pi: $cur already installed"
|
|
224
|
+
else
|
|
225
|
+
pi_add "$PI_ACSTACK@$ver" "$PI_ACSTACK"
|
|
226
|
+
fi
|
|
227
|
+
else
|
|
228
|
+
say "pi: no plugin version beside this script, so ACStack was not installed into pi"
|
|
229
|
+
fi
|
|
230
|
+
if [ "$(cfg_get pi.compact_tools true)" = "true" ]; then
|
|
231
|
+
pi_add npm:pi-compact-tools npm:pi-compact-tools
|
|
232
|
+
else
|
|
233
|
+
pi_drop npm:pi-compact-tools
|
|
234
|
+
fi
|
|
235
|
+
[ "$(cfg_get pi.settings.theme alex-mid-grey)" = "alex-mid-grey" ] && pi_write_theme
|
|
236
|
+
if [ "$(cfg_get pi.zentui true)" = "true" ]; then
|
|
237
|
+
pi_add "$PI_ZENTUI" "$PI_ZENTUI"
|
|
238
|
+
pi_state merge "$PI_ZENTUI_CFG" zentui "$(pi_zentui_wanted)"
|
|
239
|
+
else
|
|
240
|
+
pi_state merge "$PI_ZENTUI_CFG" zentui "$(pi_zentui_release)"
|
|
241
|
+
pi_drop "$PI_ZENTUI"
|
|
242
|
+
fi
|
|
243
|
+
if [ "$(cfg_get pi.hide_lens_lsp_status true)" = "true" ]; then
|
|
244
|
+
pi_state merge "$PI_LENS_CFG" pi_lens '{"ui.hideLspStatus": true, "widget.visible": false}'
|
|
245
|
+
else
|
|
246
|
+
pi_state merge "$PI_LENS_CFG" pi_lens '{"ui.hideLspStatus": null, "widget.visible": null}'
|
|
247
|
+
fi
|
|
248
|
+
apply_lens_extra_tools
|
|
249
|
+
|
|
250
|
+
# agentic-retro writes its own extension file. This setup only asks it to,
|
|
251
|
+
# and owns the file only when it was not there before.
|
|
252
|
+
if [ "$(cfg_get pi.agentic_retro true)" = "true" ]; then
|
|
253
|
+
if have agentic-retro; then
|
|
254
|
+
local fresh=0; [ -e "$PI_RETRO_EXT" ] || fresh=1
|
|
255
|
+
if agentic-retro install-pi >/dev/null 2>&1; then
|
|
256
|
+
[ "$fresh" = "1" ] && pi_state own file "$PI_RETRO_EXT"
|
|
257
|
+
say "pi: agentic-retro extension written"
|
|
258
|
+
else
|
|
259
|
+
say "pi: agentic-retro install-pi failed, run it yourself"
|
|
260
|
+
fi
|
|
261
|
+
else
|
|
262
|
+
say "pi: agentic-retro not on PATH, so its pi extension was skipped"
|
|
263
|
+
say "install the agentic-retro plugin first, then run apply again"
|
|
264
|
+
fi
|
|
265
|
+
elif pi_state owns file "$PI_RETRO_EXT"; then
|
|
266
|
+
rm -f "$PI_RETRO_EXT"
|
|
267
|
+
pi_state disown file "$PI_RETRO_EXT"
|
|
268
|
+
say "pi: agentic-retro extension removed"
|
|
269
|
+
fi
|
|
270
|
+
|
|
271
|
+
pi_state merge "$PI_SETTINGS" settings "$(pi_settings_wanted)"
|
|
272
|
+
pi_state merge "$PI_KEYBINDINGS" keybindings '{"app.message.dequeue": "ctrl+enter"}'
|
|
273
|
+
pi_state merge "$PI_SUBAGENT_CFG" subagent '{"toolActivation": "eager"}'
|
|
274
|
+
pi_state md write
|
|
275
|
+
patch_pi_win32_paste
|
|
276
|
+
|
|
277
|
+
# Logging in is interactive and stores a credential, so it stays the user's.
|
|
278
|
+
[ -f "$PI_DIR/auth.json" ] || say "pi: not logged in yet. Run pi, then /login"
|
|
279
|
+
return 0
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
# disable pi and uninstall both come here. It undoes what apply_pi recorded
|
|
283
|
+
# and leaves the pi binary, its logins and its sessions alone.
|
|
284
|
+
remove_pi() {
|
|
285
|
+
find_pi
|
|
286
|
+
if pi_state parses "$PI_SETTINGS"; then
|
|
287
|
+
pi_unpin "$PI_ACSTACK"
|
|
288
|
+
pi_drop "$PI_ACSTACK"
|
|
289
|
+
bili_client remove codex
|
|
290
|
+
bili_client remove claude
|
|
291
|
+
bili_client remove pi
|
|
292
|
+
pi_drop "$PI_BILLION"
|
|
293
|
+
pi_drop npm:pi-plugin-manager
|
|
294
|
+
pi_drop npm:@juicesharp/rpiv-todo
|
|
295
|
+
pi_drop npm:pi-compact-tools
|
|
296
|
+
pi_drop "$PI_ZENTUI"
|
|
297
|
+
pi_drop npm:@juicesharp/rpiv-ask-user-question
|
|
298
|
+
pi_drop npm:pi-web-access
|
|
299
|
+
pi_drop npm:pi-lens
|
|
300
|
+
pi_drop "$PI_SLASH_ANYWHERE"
|
|
301
|
+
pi_drop npm:pi-subagents
|
|
302
|
+
else
|
|
303
|
+
say "pi: $PI_SETTINGS does not parse, so its packages were left installed"
|
|
304
|
+
fi
|
|
305
|
+
pi_state merge "$BILLION_CONFIG" billion '{"pi.subagents": null}'
|
|
306
|
+
if pi_state owns file "$PI_RETRO_EXT"; then
|
|
307
|
+
rm -f "$PI_RETRO_EXT"
|
|
308
|
+
pi_state disown file "$PI_RETRO_EXT"
|
|
309
|
+
say "pi: agentic-retro extension removed"
|
|
310
|
+
fi
|
|
311
|
+
pi_state merge "$PI_SETTINGS" settings \
|
|
312
|
+
'{"hideThinkingBlock": null, "markdown.mermaid": null, "enableInstallTelemetry": null, "theme": null}'
|
|
313
|
+
pi_state merge "$PI_ZENTUI_CFG" zentui "$(pi_zentui_release)"
|
|
314
|
+
pi_state merge "$PI_LENS_CFG" pi_lens '{"ui.hideLspStatus": null, "widget.visible": null}'
|
|
315
|
+
if pi_state owns file "$PI_THEME"; then
|
|
316
|
+
rm -f "$PI_THEME"
|
|
317
|
+
pi_state disown file "$PI_THEME"
|
|
318
|
+
say "pi: ACStack theme removed"
|
|
319
|
+
fi
|
|
320
|
+
pi_state merge "$PI_KEYBINDINGS" keybindings '{"app.message.dequeue": null}'
|
|
321
|
+
pi_state merge "$PI_SUBAGENT_CFG" subagent '{"toolActivation": null}'
|
|
322
|
+
pi_state md strip
|
|
323
|
+
return 0
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
# Every tool with a version worth comparing.
|
|
327
|
+
# name | component key | source | reference | release asset match
|
|
328
|
+
# mermaid-ascii is deliberately absent: it has no version flag at all, so
|
|
329
|
+
# nothing here can read what is installed. `upgrade mermaid-ascii` still works,
|
|
330
|
+
# because upgrade_one carries an arm of its own for it. A tool that is in
|
|
331
|
+
# neither place is one upgrade cannot reach, whatever the docs promise.
|