acstack 0.49.0 → 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/package.json +1 -1
- package/package.json +1 -1
- package/skills/handoff/SKILL.md +14 -14
- package/skills/handoff/launch-herdr.md +15 -4
- package/skills/implement/SKILL.md +5 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
acstack_tw_main() {
|
|
2
|
+
ACTION="${1:-status}"
|
|
3
|
+
case "$ACTION" in
|
|
4
|
+
status)
|
|
5
|
+
echo "config: $CFG $([ -f "$CFG" ] && echo '(found)' || echo '(missing, using defaults)')"
|
|
6
|
+
find_pi
|
|
7
|
+
for t in herdr delta diffnav fzf lazygit tuicr yazi glow mermaid-ascii harlequin gh nvim pi; do
|
|
8
|
+
case "$t" in
|
|
9
|
+
gh) k=gh_dash ;; glow|mermaid-ascii) k=markdown ;; fzf) k=diffnav ;;
|
|
10
|
+
nvim) k=neovim ;;
|
|
11
|
+
*) k="$t" ;;
|
|
12
|
+
esac
|
|
13
|
+
# neovim and pi default to off, so reading them with the default every
|
|
14
|
+
# other one takes would report them enabled when the config says nothing.
|
|
15
|
+
printf ' %-13s %-14s enabled=%s\n' "$t" "$(have "$t" && echo installed || echo 'not installed')" \
|
|
16
|
+
"$(cfg_get "$k.enabled" "$(enabled_default "$k")")"
|
|
17
|
+
done
|
|
18
|
+
grep -q "$BEGIN" "$HERDR_CFG" 2>/dev/null && say "herdr managed block: present" || say "herdr managed block: absent"
|
|
19
|
+
echo
|
|
20
|
+
# Where a copy and an open actually land, which is the first thing to
|
|
21
|
+
# check when o or y misbehaves on a machine reached over SSH.
|
|
22
|
+
if [ -n "${DISPLAY:-}${WAYLAND_DISPLAY:-}" ]; then
|
|
23
|
+
say "clipboard: this machine has a display, using its own"
|
|
24
|
+
elif [ -x "$BIN/xclip" ]; then
|
|
25
|
+
say "clipboard: xclip stand-in, copying to your terminal over OSC 52"
|
|
26
|
+
else
|
|
27
|
+
say "clipboard: none - run apply, or copy will fail in gh-dash and friends"
|
|
28
|
+
fi
|
|
29
|
+
if grep -q 'vim\.g\.clipboard' "$NVIM_PLUGIN" 2>/dev/null; then
|
|
30
|
+
say "neovim clipboard: OSC 52, written by this setup"
|
|
31
|
+
elif mine="$(nvim_user_sets 'g\.clipboard')"; then
|
|
32
|
+
say "neovim clipboard: set by $mine"
|
|
33
|
+
elif [ "$(cfg_get neovim.enabled false)" = "true" ]; then
|
|
34
|
+
say "neovim clipboard: none - run apply, or :%y+ fails over SSH"
|
|
35
|
+
fi
|
|
36
|
+
# Ask the opener itself rather than repeating its rules here. Two copies
|
|
37
|
+
# of this decision drifting apart is exactly how o came to claim it had
|
|
38
|
+
# opened a page it had not.
|
|
39
|
+
case "$(acstack-open --why 2>/dev/null)" in
|
|
40
|
+
command) say "browser: \$ACSTACK_OPEN_CMD = $ACSTACK_OPEN_CMD" ;;
|
|
41
|
+
xdg-open) say "browser: xdg-open on this machine" ;;
|
|
42
|
+
wslview|wsl-open|explorer.exe)
|
|
43
|
+
say "browser: the Windows side of this machine" ;;
|
|
44
|
+
opener) say "browser: your default browser on your laptop, through the opener" ;;
|
|
45
|
+
bridge) say "browser: Chrome on your laptop, through the Playwriter bridge" ;;
|
|
46
|
+
link) say "browser: none - o hands you a ctrl+clickable link and copies it" ;;
|
|
47
|
+
clipboard) say "browser: none, and no terminal either - o copies and reports failure" ;;
|
|
48
|
+
*) say "browser: unknown - run apply" ;;
|
|
49
|
+
esac
|
|
50
|
+
[ -f "$HOME/.cache/acstack/open.log" ] \
|
|
51
|
+
&& say "last open: $(tail -1 "$HOME/.cache/acstack/open.log")"
|
|
52
|
+
# What the pi component set up, read from pi's own files. Shown whenever
|
|
53
|
+
# pi is here or asked for, because a pi installed by hand is still one the
|
|
54
|
+
# user may want to know about.
|
|
55
|
+
if have pi || [ "$(cfg_get pi.enabled false)" = "true" ]; then
|
|
56
|
+
echo
|
|
57
|
+
have pi && say "pi: $(installed_version pi)" || say "pi: not installed"
|
|
58
|
+
for id in "$PI_ACSTACK" $(version_table | awk -F'\t' '$2=="pi" && $3=="npm" && $1!="pi"{print $1}' | while read -r name; do pi_npm_package "$name"; done); do
|
|
59
|
+
src=$(pi_state pkg "$id") && say "pi package: $src" || say "pi package: $id not installed"
|
|
60
|
+
done
|
|
61
|
+
[ -e "$PI_RETRO_EXT" ] && say "pi agentic-retro extension: present" \
|
|
62
|
+
|| say "pi agentic-retro extension: absent"
|
|
63
|
+
grep -qsF "$PI_MD_BEGIN" "$PI_DIR"/AGENTS.override.md "$PI_DIR"/AGENTS.md "$PI_DIR"/AGENTS.MD \
|
|
64
|
+
"$PI_DIR"/CLAUDE.md "$PI_DIR"/CLAUDE.MD \
|
|
65
|
+
&& say "pi delegation line: present" || say "pi delegation line: absent"
|
|
66
|
+
ta=$(json_field "$PI_SUBAGENT_CFG" toolActivation)
|
|
67
|
+
say "pi toolActivation: ${ta:-not set}"
|
|
68
|
+
[ -f "$PI_DIR/auth.json" ] && say "pi login: logged in" \
|
|
69
|
+
|| say "pi login: not logged in - run pi, then /login"
|
|
70
|
+
fi
|
|
71
|
+
if [ "$(cfg_get tuicr.build_from_fork false)" = "true" ]; then
|
|
72
|
+
echo
|
|
73
|
+
say "tuicr: local fork build - this is meant to be temporary"
|
|
74
|
+
if have gh; then
|
|
75
|
+
for pr in 607 633; do
|
|
76
|
+
state=$(gh pr view "$pr" --repo agavra/tuicr --json state -q .state 2>/dev/null || echo "unknown")
|
|
77
|
+
printf ' agavra/tuicr#%s %s\n' "$pr" "$state"
|
|
78
|
+
done
|
|
79
|
+
say "both MERGED? set tuicr.build_from_fork: false and re-run apply"
|
|
80
|
+
say "check the merged key names first - review can rename them"
|
|
81
|
+
else
|
|
82
|
+
say "install gh to have this check agavra/tuicr#607 and #633 for you"
|
|
83
|
+
fi
|
|
84
|
+
fi
|
|
85
|
+
;;
|
|
86
|
+
apply)
|
|
87
|
+
require_yaml
|
|
88
|
+
arch_supported || exit 1
|
|
89
|
+
# Every setting has a default, so a missing config is a working setup and
|
|
90
|
+
# not an error. Say so anyway: the alternative is a run that looks like it
|
|
91
|
+
# read your settings and did not.
|
|
92
|
+
[ -f "$CFG" ] || say "no config at $CFG, so every setting takes its default"
|
|
93
|
+
[ "$(cfg_get enabled true)" = "true" ] || { echo "terminal workspace disabled in config"; exit 0; }
|
|
94
|
+
if [ "$(cfg_get herdr.enabled true)" = "true" ] && ! have herdr; then
|
|
95
|
+
# herdr ships an installer that picks the right build and verifies a
|
|
96
|
+
# checksum. Prefer it over matching release asset names ourselves.
|
|
97
|
+
curl -fsSL https://herdr.dev/install.sh | sh >/dev/null 2>&1 \
|
|
98
|
+
&& say "herdr installed" || say "herdr install failed, see herdr.dev"
|
|
99
|
+
fi
|
|
100
|
+
[ "$(cfg_get delta.enabled true)" = "true" ] && install_binary delta dandavison/delta x86_64-unknown-linux-gnu
|
|
101
|
+
[ "$(cfg_get diffnav.enabled true)" = "true" ] && install_binary diffnav dlvhdr/diffnav Linux_x86_64
|
|
102
|
+
# fzf drives the comparison picker behind acstack-diff pick.
|
|
103
|
+
[ "$(cfg_get diffnav.enabled true)" = "true" ] && install_binary fzf junegunn/fzf linux_amd64
|
|
104
|
+
[ "$(cfg_get lazygit.enabled true)" = "true" ] && install_binary lazygit jesseduffield/lazygit linux_x86_64
|
|
105
|
+
write_helpers
|
|
106
|
+
[ "$(cfg_get tuicr.enabled true)" = "true" ] && apply_tuicr
|
|
107
|
+
apply_yazi
|
|
108
|
+
apply_markdown
|
|
109
|
+
apply_neovim
|
|
110
|
+
[ "$(cfg_get herdr.enabled true)" = "true" ] && apply_herdr
|
|
111
|
+
[ "$(cfg_get herdr.enabled true)" = "true" ] && apply_herdr_numbers
|
|
112
|
+
[ "$(cfg_get delta.enabled true)" = "true" ] && apply_delta
|
|
113
|
+
[ "$(cfg_get remote.enabled true)" = "true" ] && apply_remote
|
|
114
|
+
[ "$(cfg_get gh_dash.enabled true)" = "true" ] && apply_ghdash
|
|
115
|
+
[ "$(cfg_get harlequin.enabled true)" = "true" ] && apply_harlequin
|
|
116
|
+
[ "$(cfg_get pi.enabled false)" = "true" ] && apply_pi
|
|
117
|
+
echo "done. open a herdr session and press prefix+? to see the keymap"
|
|
118
|
+
# Last, so a run that died half way leaves no stamp claiming success.
|
|
119
|
+
write_stamp
|
|
120
|
+
;;
|
|
121
|
+
outdated)
|
|
122
|
+
# Exit 2 means "cannot tell", and a missing PyYAML is exactly that.
|
|
123
|
+
# require_yaml exits 1, which every caller reads as "there is something to
|
|
124
|
+
# fetch" and acts on findings this run was never able to produce.
|
|
125
|
+
python3 -c 'import yaml' 2>/dev/null || {
|
|
126
|
+
echo "PyYAML is required to read $CFG, so nothing here can be compared" >&2
|
|
127
|
+
exit 2; }
|
|
128
|
+
[ -f "$EXAMPLE" ] || { echo "no config.example.yaml beside $0" >&2; exit 2; }
|
|
129
|
+
out=$(outdated_report)
|
|
130
|
+
[ -n "$out" ] || exit 0
|
|
131
|
+
printf '%s\n' "$out"
|
|
132
|
+
# An unknown is not a finding: nothing can be done about a tool with no
|
|
133
|
+
# version flag or a GitHub that did not answer. Only a real one exits 1.
|
|
134
|
+
printf '%s\n' "$out" | grep -qE '^(missing|new|stale)' && exit 1
|
|
135
|
+
exit 0
|
|
136
|
+
;;
|
|
137
|
+
upgrade)
|
|
138
|
+
require_yaml
|
|
139
|
+
arch_supported || exit 1
|
|
140
|
+
shift
|
|
141
|
+
names="$*"
|
|
142
|
+
report=""
|
|
143
|
+
if [ -z "$names" ]; then
|
|
144
|
+
report=$(outdated_report)
|
|
145
|
+
names=$(printf '%s\n' "$report" | awk -F'\t' '$1=="stale"{print $2}')
|
|
146
|
+
fi
|
|
147
|
+
for n in $names; do upgrade_one "$n"; done
|
|
148
|
+
|
|
149
|
+
# A new component is a config edit, not a download. Append the block the
|
|
150
|
+
# example ships, comments and all, rather than rewriting the file through
|
|
151
|
+
# yaml - a round trip through the parser drops every comment the user has.
|
|
152
|
+
printf '%s\n' "$report" | awk -F'\t' '$1=="new"{print $2}' | while read -r comp; do
|
|
153
|
+
[ -n "$comp" ] || continue
|
|
154
|
+
ACSTACK_COMP="$comp" py "$EXAMPLE" "$CFG" <<'PY'
|
|
155
|
+
import os, re, sys
|
|
156
|
+
comp = os.environ["ACSTACK_COMP"]
|
|
157
|
+
ship = open(sys.argv[1]).read()
|
|
158
|
+
# The block runs from the component's own key to the next top-level key. A
|
|
159
|
+
# top-level key is the only thing that starts in column zero and ends in a
|
|
160
|
+
# colon, which is what makes a plain text slice safe here.
|
|
161
|
+
m = re.search(r"(?ms)^%s:.*?(?=^\S+:|\Z)" % re.escape(comp), ship)
|
|
162
|
+
if not m:
|
|
163
|
+
raise SystemExit
|
|
164
|
+
block = m.group(0).rstrip() + "\n"
|
|
165
|
+
mine = open(sys.argv[2]).read() if os.path.isfile(sys.argv[2]) else ""
|
|
166
|
+
if re.search(r"(?m)^%s:" % re.escape(comp), mine):
|
|
167
|
+
raise SystemExit
|
|
168
|
+
open(sys.argv[2], "a").write(("" if mine.endswith("\n\n") else "\n") + block)
|
|
169
|
+
print(" %s added to your config, with its comments" % comp)
|
|
170
|
+
PY
|
|
171
|
+
done
|
|
172
|
+
|
|
173
|
+
say "run apply to install anything new"
|
|
174
|
+
;;
|
|
175
|
+
disable)
|
|
176
|
+
DISABLE_USAGE="usage: setup.sh disable <herdr|delta|diffnav|gh_dash|harlequin|lazygit|neovim|pi|tuicr>"
|
|
177
|
+
comp="${2:?$DISABLE_USAGE}"
|
|
178
|
+
case "$comp" in
|
|
179
|
+
delta) for k in core.pager interactive.diffFilter delta.navigate delta.line-numbers; do
|
|
180
|
+
git config --global --unset "$k" 2>/dev/null
|
|
181
|
+
done
|
|
182
|
+
say "delta unset as git pager"
|
|
183
|
+
say "now set delta.enabled: false in $CFG so apply does not restore it" ;;
|
|
184
|
+
herdr)
|
|
185
|
+
py "$HERDR_CFG" "$BEGIN" "$END" <<'PY'
|
|
186
|
+
import re, sys, os
|
|
187
|
+
p, b, e = sys.argv[1:4]
|
|
188
|
+
if os.path.isfile(p):
|
|
189
|
+
t = open(p).read()
|
|
190
|
+
open(p, "w").write(re.sub(re.escape(b) + r".*?" + re.escape(e) + r"\n?", "", t, flags=re.S))
|
|
191
|
+
PY
|
|
192
|
+
say "managed block removed from herdr config"
|
|
193
|
+
say "now set herdr.enabled: false in $CFG so apply does not restore it" ;;
|
|
194
|
+
neovim)
|
|
195
|
+
# A whole file of ours, so turning it off is a deletion. Anything the
|
|
196
|
+
# user wrote by hand under that name is theirs and stays.
|
|
197
|
+
if nvim_ours; then
|
|
198
|
+
rm -f "$NVIM_PLUGIN"
|
|
199
|
+
say "neovim plugin file removed"
|
|
200
|
+
fi
|
|
201
|
+
say "now set neovim.enabled: false in $CFG so apply does not restore it" ;;
|
|
202
|
+
pi)
|
|
203
|
+
# Only what apply added. The pi binary, its logins and its sessions
|
|
204
|
+
# stay, and so does anything the user installed into pi themselves.
|
|
205
|
+
remove_pi
|
|
206
|
+
say "now set pi.enabled: false in $CFG so apply does not restore it" ;;
|
|
207
|
+
diffnav|gh_dash|harlequin|lazygit|tuicr)
|
|
208
|
+
# Only this component's popup goes away. Rebuild the managed block so
|
|
209
|
+
# the rest of the keymap survives.
|
|
210
|
+
require_yaml
|
|
211
|
+
ACSTACK_CFG="$CFG" py "$comp" <<'PY'
|
|
212
|
+
import os, sys, yaml
|
|
213
|
+
p = os.environ["ACSTACK_CFG"]
|
|
214
|
+
c = yaml.safe_load(open(p)) or {}
|
|
215
|
+
c.setdefault(sys.argv[1], {})["enabled"] = False
|
|
216
|
+
yaml.safe_dump(c, open(p, "w"), sort_keys=False)
|
|
217
|
+
PY
|
|
218
|
+
apply_herdr
|
|
219
|
+
say "$comp disabled, rest of the keymap left in place" ;;
|
|
220
|
+
yazi|markdown|remote)
|
|
221
|
+
# These three have no keys to remove and no git settings to unset, so
|
|
222
|
+
# there is nothing here to carry out. Refusing says so; the arm that
|
|
223
|
+
# was missing let the script exit 0 in silence, which reads as done.
|
|
224
|
+
say "$comp has no disable step"
|
|
225
|
+
say "set $comp.enabled: false in $CFG and run apply instead"
|
|
226
|
+
exit 1 ;;
|
|
227
|
+
*) echo "$comp is not a component. $DISABLE_USAGE" >&2; exit 1 ;;
|
|
228
|
+
esac
|
|
229
|
+
;;
|
|
230
|
+
uninstall)
|
|
231
|
+
"$0" disable herdr; "$0" disable delta; "$0" disable neovim; "$0" disable pi
|
|
232
|
+
# By prefix, which is why helpers are named acstack-*: anything this
|
|
233
|
+
# script ever wrote goes, including helpers retired in an older version.
|
|
234
|
+
herdr plugin unlink acstack.herdr-numbers >/dev/null 2>&1 || true
|
|
235
|
+
rm -rf "$HOME/.config/acstack/herdr-numbers"
|
|
236
|
+
rm -f "$BIN"/acstack-*
|
|
237
|
+
# The xclip stand-in is the one managed file without the prefix.
|
|
238
|
+
grep -q "Installed by acstack" "$BIN/xclip" 2>/dev/null && rm -f "$BIN/xclip"
|
|
239
|
+
# Every file apply leaves a marked region in. Removing the region is the
|
|
240
|
+
# same edit `disable herdr` makes, so whatever the user wrote around it
|
|
241
|
+
# comes through untouched. Left behind, the yazi regions keep routing
|
|
242
|
+
# markdown at acstack-md, a helper this command has just deleted.
|
|
243
|
+
ACSTACK_ARGS="$BEGIN|$END" py \
|
|
244
|
+
"$HOME/.config/tuicr/config.toml" \
|
|
245
|
+
"$HOME/.config/yazi/yazi.toml" \
|
|
246
|
+
"$HOME/.config/yazi/keymap.toml" <<'PY'
|
|
247
|
+
import os, re, sys
|
|
248
|
+
b, e = os.environ["ACSTACK_ARGS"].split("|")
|
|
249
|
+
for p in sys.argv[1:]:
|
|
250
|
+
if os.path.isfile(p):
|
|
251
|
+
t = open(p).read()
|
|
252
|
+
open(p, "w").write(re.sub(re.escape(b) + r".*?" + re.escape(e) + r"\n?", "", t, flags=re.S))
|
|
253
|
+
PY
|
|
254
|
+
# gh-dash reads YAML that python rewrites whole, so a marker comment never
|
|
255
|
+
# survives the round trip. What identifies acstack's work there is the name
|
|
256
|
+
# on each keybinding, the same handle apply_ghdash uses to replace its own
|
|
257
|
+
# entries, plus the two page keys it binds. Sections, repo paths and every
|
|
258
|
+
# other keybinding belong to the user and stay.
|
|
259
|
+
if [ -f "$GHDASH_CFG" ] && python3 -c 'import yaml' 2>/dev/null; then
|
|
260
|
+
py "$GHDASH_CFG" <<'PY'
|
|
261
|
+
import sys, yaml
|
|
262
|
+
p = sys.argv[1]
|
|
263
|
+
cfg = yaml.safe_load(open(p)) or {}
|
|
264
|
+
MANAGED = ("review (tuicr)", "tree diff", "agent review", "open in browser")
|
|
265
|
+
kbs = cfg.get("keybindings") or {}
|
|
266
|
+
for scope in ("prs", "issues"):
|
|
267
|
+
if scope in kbs:
|
|
268
|
+
kbs[scope] = [k for k in kbs[scope] or [] if k.get("name") not in MANAGED]
|
|
269
|
+
if not kbs[scope]:
|
|
270
|
+
del kbs[scope]
|
|
271
|
+
if "universal" in kbs:
|
|
272
|
+
kbs["universal"] = [k for k in kbs["universal"] or []
|
|
273
|
+
if k.get("builtin") not in ("pageUp", "pageDown")]
|
|
274
|
+
if not kbs["universal"]:
|
|
275
|
+
del kbs["universal"]
|
|
276
|
+
if kbs:
|
|
277
|
+
cfg["keybindings"] = kbs
|
|
278
|
+
else:
|
|
279
|
+
cfg.pop("keybindings", None)
|
|
280
|
+
yaml.safe_dump(cfg, open(p, "w"), sort_keys=False, default_flow_style=False, width=200)
|
|
281
|
+
PY
|
|
282
|
+
say "gh-dash keybindings removed"
|
|
283
|
+
fi
|
|
284
|
+
# The one tool uv owns rather than this script. Leaving it behind leaves an
|
|
285
|
+
# environment nothing on the machine points at any more.
|
|
286
|
+
if have uv && uv tool list 2>/dev/null | grep -q '^harlequin '; then
|
|
287
|
+
uv tool uninstall harlequin >/dev/null 2>&1 && say "harlequin removed"
|
|
288
|
+
fi
|
|
289
|
+
say "managed config and helpers removed. binaries left in place, except"
|
|
290
|
+
say "harlequin, which uv owns whole rather than as a file in $BIN"
|
|
291
|
+
say "pi stays too, with its logins and sessions: npm uninstall -g @earendil-works/pi-coding-agent"
|
|
292
|
+
;;
|
|
293
|
+
*) echo "usage: setup.sh [status|apply|outdated|upgrade|disable <component>|uninstall]"; exit 1 ;;
|
|
294
|
+
esac
|
|
295
|
+
|
|
296
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
have() { command -v "$1" >/dev/null 2>&1; }
|
|
2
|
+
|
|
3
|
+
require_yaml() {
|
|
4
|
+
python3 -c 'import yaml' 2>/dev/null && return 0
|
|
5
|
+
echo "PyYAML is required to read $CFG" >&2
|
|
6
|
+
echo "Install it (pip install --user pyyaml) and run again. Refusing to" >&2
|
|
7
|
+
echo "continue, because without it every setting falls back to a default" >&2
|
|
8
|
+
echo "and your config is silently ignored." >&2
|
|
9
|
+
exit 1
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
arch_supported() {
|
|
13
|
+
case "$(uname -m)" in
|
|
14
|
+
x86_64|amd64) return 0 ;;
|
|
15
|
+
*) say "prebuilt binaries are x86_64 only, and this is $(uname -m)"
|
|
16
|
+
say "install herdr, delta, diffnav, and lazygit yourself, then run apply again"
|
|
17
|
+
return 1 ;;
|
|
18
|
+
esac
|
|
19
|
+
}
|
|
20
|
+
say() { printf ' %s\n' "$*"; }
|
|
21
|
+
|
|
22
|
+
py() { python3 - "$@"; }
|
|
23
|
+
|
|
24
|
+
# Reads one dotted path. require_yaml runs first, so a fallback here means
|
|
25
|
+
# the key is absent, never that yaml failed to import.
|
|
26
|
+
cfg_get() { # cfg_get <dotted.path> <default>
|
|
27
|
+
ACSTACK_CFG="$CFG" py "$1" "${2:-}" <<'PY'
|
|
28
|
+
import os, sys
|
|
29
|
+
import yaml
|
|
30
|
+
p = os.environ["ACSTACK_CFG"]
|
|
31
|
+
if not os.path.isfile(p):
|
|
32
|
+
print(sys.argv[2]); raise SystemExit
|
|
33
|
+
d = yaml.safe_load(open(p)) or {}
|
|
34
|
+
for k in sys.argv[1].split("."):
|
|
35
|
+
d = d.get(k) if isinstance(d, dict) else None
|
|
36
|
+
if d is None: break
|
|
37
|
+
if isinstance(d, bool): d = "true" if d else "false"
|
|
38
|
+
print(d if d is not None else sys.argv[2])
|
|
39
|
+
PY
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# neovim and pi write into a config the user built, so they start off. Every
|
|
43
|
+
# other component owns its files outright and starts on.
|
|
44
|
+
enabled_default() { # enabled_default <component>
|
|
45
|
+
case "$1" in neovim|pi) echo false ;; *) echo true ;; esac
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# The example config that shipped beside this script. Both `outdated` and the
|
|
49
|
+
# stamp read it, so resolve it once.
|
|
50
|
+
EXAMPLE="$SCRIPT_DIR/config.example.yaml"
|
|
51
|
+
STAMP="$(dirname "$CFG")/terminal-workspace.applied"
|
|
52
|
+
# What the pi component changed, so disable undoes that and nothing else. A
|
|
53
|
+
# package or a setting the user already had is theirs, and stays.
|
|
54
|
+
PI_OWNED="$(dirname "$CFG")/terminal-workspace.pi.json"
|
|
55
|
+
|
|
56
|
+
# Reads a yaml list into one item per line. cfg_get prints a python list repr
|
|
57
|
+
# for a list value, which no caller can use.
|
|
58
|
+
cfg_get_list() { # cfg_get_list <dotted.path> <space-separated default>
|
|
59
|
+
ACSTACK_CFG="$CFG" py "$1" "${2:-}" <<'PY'
|
|
60
|
+
import os, sys
|
|
61
|
+
import yaml
|
|
62
|
+
p = os.environ["ACSTACK_CFG"]
|
|
63
|
+
d = (yaml.safe_load(open(p)) or {}) if os.path.isfile(p) else {}
|
|
64
|
+
for k in sys.argv[1].split("."):
|
|
65
|
+
d = d.get(k) if isinstance(d, dict) else None
|
|
66
|
+
if d is None: break
|
|
67
|
+
print("\n".join(str(x) for x in d) if isinstance(d, list) else sys.argv[2].replace(" ", "\n"))
|
|
68
|
+
PY
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# What `apply` had to offer, recorded so a later run can tell what arrived
|
|
72
|
+
# since. Without it, a component added by an upgrade is invisible: cfg_get
|
|
73
|
+
# returns its default, apply installs it, and nobody is ever told it is there.
|
|
74
|
+
write_stamp() {
|
|
75
|
+
[ -f "$EXAMPLE" ] || return 0
|
|
76
|
+
mkdir -p "$(dirname "$STAMP")"
|
|
77
|
+
{ echo "# written by acstack terminal-workspace apply, do not edit"
|
|
78
|
+
py "$EXAMPLE" <<'PY'
|
|
79
|
+
import sys
|
|
80
|
+
import yaml
|
|
81
|
+
for k in (yaml.safe_load(open(sys.argv[1])) or {}):
|
|
82
|
+
print(k)
|
|
83
|
+
PY
|
|
84
|
+
} > "$STAMP"
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
install_binary() { # install_binary <name> <repo> <asset-match>
|
|
88
|
+
local name="$1" repo="$2" match="$3"
|
|
89
|
+
# `upgrade` reinstalls over a binary that is already here. `apply` never
|
|
90
|
+
# does: it installs what is missing and leaves what is not.
|
|
91
|
+
if [ "${INSTALL_FORCE:-0}" != "1" ] && have "$name"; then
|
|
92
|
+
say "$name already installed"; return 0
|
|
93
|
+
fi
|
|
94
|
+
local url json
|
|
95
|
+
# py reads its script from stdin, so the release JSON has to arrive by
|
|
96
|
+
# environment rather than by pipe: a pipe here is silently swallowed by
|
|
97
|
+
# the heredoc and python then reads an empty stdin.
|
|
98
|
+
json=$(curl -sSf "https://api.github.com/repos/$repo/releases/latest" 2>/dev/null)
|
|
99
|
+
[ -z "$json" ] && { say "$name: could not reach the GitHub release API"; return 1; }
|
|
100
|
+
url=$(ACSTACK_JSON="$json" py "$match" <<'PY'
|
|
101
|
+
import json, os, sys
|
|
102
|
+
try:
|
|
103
|
+
d = json.loads(os.environ["ACSTACK_JSON"])
|
|
104
|
+
except ValueError:
|
|
105
|
+
raise SystemExit
|
|
106
|
+
for a in d.get("assets", []):
|
|
107
|
+
# yazi publishes .zip only; delta and glow publish .tar.gz. Take either.
|
|
108
|
+
if sys.argv[1] in a["name"] and a["name"].endswith((".tar.gz", ".tgz", ".zip")):
|
|
109
|
+
print(a["browser_download_url"]); break
|
|
110
|
+
PY
|
|
111
|
+
)
|
|
112
|
+
[ -z "$url" ] && { say "$name: no matching release asset, install manually"; return 1; }
|
|
113
|
+
# Explicit cleanup rather than `trap ... RETURN`: a RETURN trap stays
|
|
114
|
+
# registered for every later function return in this shell, where $tmp is
|
|
115
|
+
# gone and `set -u` then aborts the run.
|
|
116
|
+
local tmp found ar; tmp=$(mktemp -d)
|
|
117
|
+
case "$url" in *.zip) ar="$tmp/a.zip" ;; *) ar="$tmp/a.tgz" ;; esac
|
|
118
|
+
if ! curl -sL "$url" -o "$ar"; then
|
|
119
|
+
rm -rf "$tmp"; say "$name: download failed"; return 1
|
|
120
|
+
fi
|
|
121
|
+
case "$ar" in
|
|
122
|
+
*.zip) have unzip || { rm -rf "$tmp"; say "$name: needs unzip"; return 1; }
|
|
123
|
+
unzip -qo "$ar" -d "$tmp" || { rm -rf "$tmp"; say "$name: unzip failed"; return 1; } ;;
|
|
124
|
+
*) tar xzf "$ar" -C "$tmp" || { rm -rf "$tmp"; say "$name: untar failed"; return 1; } ;;
|
|
125
|
+
esac
|
|
126
|
+
found=$(find "$tmp" -type f -name "$name" | head -1)
|
|
127
|
+
if [ -z "$found" ]; then
|
|
128
|
+
rm -rf "$tmp"; say "$name: binary not found in archive"; return 1
|
|
129
|
+
fi
|
|
130
|
+
mkdir -p "$BIN"; cp "$found" "$BIN/$name"; chmod +x "$BIN/$name"
|
|
131
|
+
rm -rf "$tmp"
|
|
132
|
+
say "$name installed to $BIN/$name"
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
install_plain_github_binary() { # install_plain_github_binary <name> <repo> <asset-match>
|
|
136
|
+
local name="$1" repo="$2" match="$3"
|
|
137
|
+
if [ "${INSTALL_FORCE:-0}" != "1" ] && have "$name"; then
|
|
138
|
+
say "$name already installed"; return 0
|
|
139
|
+
fi
|
|
140
|
+
local json url tmp
|
|
141
|
+
json=$(curl -sSf "https://api.github.com/repos/$repo/releases/latest" 2>/dev/null)
|
|
142
|
+
[ -z "$json" ] && { say "$name: could not reach the GitHub release API"; return 1; }
|
|
143
|
+
url=$(ACSTACK_JSON="$json" py "$match" <<'PY'
|
|
144
|
+
import json, os, sys
|
|
145
|
+
try:
|
|
146
|
+
d = json.loads(os.environ["ACSTACK_JSON"])
|
|
147
|
+
except ValueError:
|
|
148
|
+
raise SystemExit
|
|
149
|
+
for a in d.get("assets", []):
|
|
150
|
+
if sys.argv[1] in a["name"]:
|
|
151
|
+
print(a["browser_download_url"]); break
|
|
152
|
+
PY
|
|
153
|
+
)
|
|
154
|
+
[ -z "$url" ] && { say "$name: no matching release asset, install manually"; return 1; }
|
|
155
|
+
tmp=$(mktemp)
|
|
156
|
+
if ! curl -fsSL "$url" -o "$tmp"; then
|
|
157
|
+
rm -f "$tmp"; say "$name: download failed"; return 1
|
|
158
|
+
fi
|
|
159
|
+
mkdir -p "$BIN"; cp "$tmp" "$BIN/$name"; chmod +x "$BIN/$name"
|
|
160
|
+
rm -f "$tmp"
|
|
161
|
+
say "$name installed to $BIN/$name"
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
install_npm_tool() { # install_npm_tool <command> <package>
|
|
165
|
+
local command="$1" package="$2"
|
|
166
|
+
if have "$command"; then say "$command already installed"; return 0; fi
|
|
167
|
+
have npm || { say "$command: npm not installed, skipping"; return 0; }
|
|
168
|
+
if npm install -g "$package" >/dev/null 2>&1; then
|
|
169
|
+
say "$command installed with npm"
|
|
170
|
+
else
|
|
171
|
+
say "$command install failed, run: npm install -g $package"
|
|
172
|
+
fi
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
install_uv_tool() { # install_uv_tool <command> <package>
|
|
176
|
+
local command="$1" package="$2"
|
|
177
|
+
if have "$command"; then say "$command already installed"; return 0; fi
|
|
178
|
+
have uv || { say "$command: uv not installed, skipping"; return 0; }
|
|
179
|
+
if uv tool install "$package" >/dev/null 2>&1; then
|
|
180
|
+
say "$command installed with uv"
|
|
181
|
+
else
|
|
182
|
+
say "$command install failed, run: uv tool install $package"
|
|
183
|
+
fi
|
|
184
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
write_helpers() {
|
|
2
|
+
mkdir -p "$BIN"
|
|
3
|
+
# Helpers this script used to write and no longer does. Upgrading otherwise
|
|
4
|
+
# leaves them on PATH, shadowing nothing but confusing everything.
|
|
5
|
+
rm -f "$BIN"/acstack-files "$BIN"/acstack-files-tab \
|
|
6
|
+
"$BIN"/acstack-yazi-tab "$BIN"/acstack-tab "$BIN"/acstack-renumber
|
|
7
|
+
|
|
8
|
+
local src name
|
|
9
|
+
for src in "$SCRIPT_DIR"/helpers/acstack-*; do
|
|
10
|
+
[ -f "$src" ] || continue
|
|
11
|
+
name=${src##*/}
|
|
12
|
+
cp "$src" "$BIN/$name"
|
|
13
|
+
chmod +x "$BIN/$name"
|
|
14
|
+
done
|
|
15
|
+
|
|
16
|
+
# The one helper with a path in the config. The source holds the default, so
|
|
17
|
+
# this carries nothing but a value the user chose.
|
|
18
|
+
ACSTACK_DB_CFG="$(cfg_get harlequin.config_path "~/.config/acstack/harlequin.toml")" \
|
|
19
|
+
py "$BIN/acstack-db" <<'PY'
|
|
20
|
+
import os, re, sys
|
|
21
|
+
# A bash single-quoted literal, so a path with a space or a quote in it reaches
|
|
22
|
+
# the helper as one word rather than as shell syntax.
|
|
23
|
+
v = "'" + os.environ["ACSTACK_DB_CFG"].replace("'", "'\\''") + "'"
|
|
24
|
+
p = sys.argv[1]
|
|
25
|
+
t = open(p).read()
|
|
26
|
+
t, n = re.subn(r"(?m)^DB_CFG_DEFAULT=.*# acstack:config_path$",
|
|
27
|
+
lambda m: "DB_CFG_DEFAULT=" + v + " # acstack:config_path", t)
|
|
28
|
+
if n:
|
|
29
|
+
open(p, "w").write(t)
|
|
30
|
+
PY
|
|
31
|
+
|
|
32
|
+
say "helpers written: acstack-diff, acstack-dash, acstack-db, acstack-md, acstack-view"
|
|
33
|
+
say "remote helpers written: acstack-clip, acstack-open, acstack-bridge"
|
|
34
|
+
say "herdr helpers written: acstack-popup, acstack-herdr-tab, acstack-herdr-renumber, acstack-edit"
|
|
35
|
+
}
|