acstack 0.49.0 → 0.49.2

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.
Files changed (36) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/assets/terminal-workspace/helpers/acstack-bridge +185 -0
  3. package/assets/terminal-workspace/helpers/acstack-clip +88 -0
  4. package/assets/terminal-workspace/helpers/acstack-dash +25 -0
  5. package/assets/terminal-workspace/helpers/acstack-db +86 -0
  6. package/assets/terminal-workspace/helpers/acstack-diff +268 -0
  7. package/assets/terminal-workspace/helpers/acstack-edit +25 -0
  8. package/assets/terminal-workspace/helpers/acstack-gh +47 -0
  9. package/assets/terminal-workspace/helpers/acstack-herdr-renumber +93 -0
  10. package/assets/terminal-workspace/helpers/acstack-herdr-tab +49 -0
  11. package/assets/terminal-workspace/helpers/acstack-md +401 -0
  12. package/assets/terminal-workspace/helpers/acstack-md-lib +219 -0
  13. package/assets/terminal-workspace/helpers/acstack-open +217 -0
  14. package/assets/terminal-workspace/helpers/acstack-popup +13 -0
  15. package/assets/terminal-workspace/helpers/acstack-view +33 -0
  16. package/assets/terminal-workspace/helpers/xclip +31 -0
  17. package/assets/terminal-workspace/lib/apply-ghdash.sh +78 -0
  18. package/assets/terminal-workspace/lib/apply-herdr-numbers.sh +49 -0
  19. package/assets/terminal-workspace/lib/apply-herdr.sh +308 -0
  20. package/assets/terminal-workspace/lib/apply-tools.sh +293 -0
  21. package/assets/terminal-workspace/lib/apply-tuicr.sh +133 -0
  22. package/assets/terminal-workspace/lib/apply-yazi.sh +389 -0
  23. package/assets/terminal-workspace/lib/commands.sh +303 -0
  24. package/assets/terminal-workspace/lib/core.sh +184 -0
  25. package/assets/terminal-workspace/lib/helper-install.sh +35 -0
  26. package/assets/terminal-workspace/lib/pi-packages.sh +360 -0
  27. package/assets/terminal-workspace/lib/pi-state.sh +479 -0
  28. package/assets/terminal-workspace/lib/versions.sh +254 -0
  29. package/assets/terminal-workspace/pi/acstack-question-blocked.ts +31 -0
  30. package/assets/terminal-workspace/setup.sh +25 -4637
  31. package/cli/package.json +1 -1
  32. package/docs/terminal-workspace.md +10 -1
  33. package/package.json +1 -1
  34. package/skills/handoff/SKILL.md +14 -14
  35. package/skills/handoff/launch-herdr.md +15 -4
  36. package/skills/implement/SKILL.md +5 -0
@@ -0,0 +1,254 @@
1
+ version_table() {
2
+ cat <<'T'
3
+ delta delta github dandavison/delta x86_64-unknown-linux-gnu
4
+ diffnav diffnav github dlvhdr/diffnav Linux_x86_64
5
+ fzf diffnav github junegunn/fzf linux_amd64
6
+ lazygit lazygit github jesseduffield/lazygit linux_x86_64
7
+ tuicr tuicr github agavra/tuicr x86_64-unknown-linux-gnu
8
+ yazi yazi github sxyazi/yazi x86_64-unknown-linux-gnu
9
+ glow markdown github charmbracelet/glow Linux_x86_64
10
+ herdr herdr herdr - -
11
+ gh-dash gh_dash ghext dlvhdr/gh-dash -
12
+ harlequin harlequin pypi harlequin -
13
+ pi pi npm @earendil-works/pi-coding-agent -
14
+ pi-subagents pi npm pi-subagents -
15
+ billion-context pi npm billion-context -
16
+ @juicesharp/rpiv-todo pi npm @juicesharp/rpiv-todo -
17
+ pi-plugin-manager pi npm pi-plugin-manager -
18
+ @juicesharp/rpiv-ask-user-question pi npm @juicesharp/rpiv-ask-user-question -
19
+ pi-web-access pi npm pi-web-access -
20
+ pi-lens pi npm pi-lens -
21
+ pi-slash-anywhere pi npm pi-slash-anywhere -
22
+ pi-compact-tools pi npm pi-compact-tools -
23
+ pi-zentui pi npm pi-zentui -
24
+ actionlint pi github rhysd/actionlint linux_amd64
25
+ hadolint pi github hadolint/hadolint Linux-x86_64
26
+ terraform-ls pi github hashicorp/terraform-ls linux_amd64
27
+ tflint pi github terraform-linters/tflint linux_amd64
28
+ terragrunt pi github gruntwork-io/terragrunt linux_amd64
29
+ T
30
+ }
31
+
32
+ installed_version() { # installed_version <name> -> a bare version, or nothing
33
+ local v=""
34
+ case "$1" in
35
+ # delta 0.19.2
36
+ delta) v=$(delta --version 2>/dev/null | awk '{print $2}') ;;
37
+ # an ASCII banner, then: version v0.12.0
38
+ diffnav) v=$(diffnav --version 2>/dev/null | sed -n 's/^version //p') ;;
39
+ # 0.74.3 (15f64c49)
40
+ fzf) v=$(fzf --version 2>/dev/null | awk '{print $1}') ;;
41
+ # commit=..., version=0.64.1, os=linux, arch=amd64, git version=2.43.0
42
+ # Split on the commas first: the line ends in git's own version, and one
43
+ # regex over the whole line reads that one instead of lazygit's.
44
+ lazygit) v=$(lazygit --version 2>/dev/null | tr ',' '\n' | sed -n 's/^ *version=//p') ;;
45
+ # glow version 3.0.0 (add5e7a)
46
+ glow) v=$(glow --version 2>/dev/null | awk '{print $3}') ;;
47
+ # tuicr 0.23.0
48
+ tuicr) v=$(tuicr --version 2>/dev/null | awk '{print $2}') ;;
49
+ # a block of lines, one of them: " Version: 26.8.15 (1f3588d 2026-08-15)"
50
+ yazi) v=$(yazi --version 2>/dev/null | sed -n 's/^ *Version: \([^ ]*\).*/\1/p') ;;
51
+ # herdr 0.8.2
52
+ herdr) v=$(herdr --version 2>/dev/null | awk '{print $2}') ;;
53
+ # gh dash<TAB>dlvhdr/gh-dash<TAB>v4.25.2
54
+ gh-dash) v=$(gh extension list 2>/dev/null | awk -F'\t' '$2=="dlvhdr/gh-dash"{print $3}') ;;
55
+ # harlequin v2.13.0, then an indented line per command it installed
56
+ harlequin) v=$(uv tool list 2>/dev/null | awk '$1=="harlequin"{print $2}') ;;
57
+ # 1.1.0
58
+ pi) find_pi; v=$(pi --version 2>/dev/null | awk '{print $NF}') ;;
59
+ # actionlint version 1.7.7
60
+ actionlint) v=$(actionlint -version 2>/dev/null | awk '{print $2}') ;;
61
+ # Haskell Dockerfile Linter 2.12.0
62
+ hadolint) v=$(hadolint --version 2>/dev/null | awk '{print $NF}') ;;
63
+ # terraform-ls version 0.36.5
64
+ terraform-ls) v=$(terraform-ls version 2>/dev/null | awk '{print $2}') ;;
65
+ # TFLint version 0.59.1
66
+ tflint) v=$(tflint --version 2>/dev/null | awk 'NR==1{print $3}') ;;
67
+ # terragrunt version v0.91.2
68
+ terragrunt) v=$(terragrunt --version 2>/dev/null | awk '{print $NF}') ;;
69
+ # pi installs npm packages under its own directory, not npm's global one.
70
+ *) if pkg=$(pi_npm_package "$1"); then
71
+ v=$(json_field "$PI_DIR/npm/node_modules/${pkg#npm:}/package.json" version)
72
+ fi ;;
73
+ esac
74
+ printf '%s' "${v#v}"
75
+ }
76
+
77
+ # One JSON field, by dotted path, read from a file. install_binary hands its
78
+ # release document to python through the environment; this one cannot, because
79
+ # herdr's manifest carries a whole changelog and a document that size overflows
80
+ # the environment an exec is allowed. A file has no such ceiling.
81
+ json_field() { # json_field <file> <dotted.path>
82
+ py "$1" "$2" <<'PY'
83
+ import json, sys
84
+ try:
85
+ d = json.load(open(sys.argv[1]))
86
+ except (ValueError, OSError):
87
+ raise SystemExit
88
+ for k in sys.argv[2].split("."):
89
+ d = d.get(k) if isinstance(d, dict) else None
90
+ if d is None: raise SystemExit
91
+ print(d)
92
+ PY
93
+ }
94
+
95
+ latest_version() { # latest_version <source> <reference> -> a version, or nothing
96
+ [ "${ACSTACK_TW_OFFLINE:-0}" = "1" ] && return 0
97
+ local url field tmp v="" tok="${GH_TOKEN:-${GITHUB_TOKEN:-}}"
98
+ local -a auth=()
99
+ case "$1" in
100
+ github|ghext)
101
+ url="https://api.github.com/repos/$2/releases/latest"; field=tag_name
102
+ # An unauthenticated GitHub call gets 60 an hour, shared with everything
103
+ # else on the machine. Ten tools in one run is a sixth of that. A token
104
+ # raises it to 5000 and costs nothing to pass. It goes to GitHub and
105
+ # nowhere else: the two hosts below never see it.
106
+ [ -n "$tok" ] && auth=(-H "Authorization: Bearer $tok") ;;
107
+ # herdr publishes its own manifest, and `herdr update` installs from it.
108
+ herdr) url="https://herdr.dev/latest.json"; field=version ;;
109
+ pypi) url="https://pypi.org/pypi/$2/json"; field=info.version ;;
110
+ # A scoped name keeps its @ and escapes its slash.
111
+ npm) url="https://registry.npmjs.org/${2/\//%2F}/latest"; field=version ;;
112
+ *) return 0 ;;
113
+ esac
114
+ tmp=$(mktemp)
115
+ curl -sSf "${auth[@]}" --max-time 10 "$url" -o "$tmp" 2>/dev/null \
116
+ && v=$(json_field "$tmp" "$field")
117
+ rm -f "$tmp"
118
+ printf '%s' "${v#v}"
119
+ }
120
+
121
+ outdated_report() { # tab-separated rows on stdout, one per finding
122
+ local name key src ref inst latest
123
+ # Components that arrived since the last apply. See write_stamp for why this
124
+ # reads the stamp rather than the user's own config: a config that overrides
125
+ # four keys and defaults the rest is normal, and diffing against it would
126
+ # report every defaulted component as new.
127
+ if [ ! -f "$STAMP" ]; then
128
+ # No stamp and no managed block is a machine apply has never touched.
129
+ # Comparing versions there would report a workspace as current when none
130
+ # of it is installed, so say the one true thing and stop.
131
+ if ! grep -q "$BEGIN" "$HERDR_CFG" 2>/dev/null; then
132
+ printf 'missing\tworkspace\tnever applied on this machine\n'
133
+ return 0
134
+ fi
135
+ printf 'unknown\tcomponents\tno stamp yet, run apply once\n'
136
+ elif [ -f "$EXAMPLE" ]; then
137
+ # The row stays until apply installs the component and writes a new stamp.
138
+ # Only the detail moves: once upgrade has appended the block, "not in your
139
+ # config" is false, and a report that says it is stops being worth reading.
140
+ ACSTACK_STAMP="$STAMP" py "$EXAMPLE" "$CFG" <<'PY'
141
+ import os, re, sys
142
+ import yaml
143
+ had = {l.strip() for l in open(os.environ["ACSTACK_STAMP"])
144
+ if l.strip() and not l.startswith("#")}
145
+ mine = open(sys.argv[2]).read() if os.path.isfile(sys.argv[2]) else ""
146
+ for k in (yaml.safe_load(open(sys.argv[1])) or {}):
147
+ if k in had:
148
+ continue
149
+ if re.search(r"(?m)^%s:" % re.escape(k), mine):
150
+ print("new\t%s\tnew component, run apply to install it" % k)
151
+ else:
152
+ print("new\t%s\tnew component, not in your config" % k)
153
+ PY
154
+ fi
155
+
156
+ while IFS=$'\t' read -r name key src ref _; do
157
+ [ "$(cfg_get "$key.enabled" "$(enabled_default "$key")")" = "true" ] || continue
158
+ # A fork build's version comes from a local checkout, so a release tag says
159
+ # nothing about it.
160
+ [ "$name" = "tuicr" ] && [ "$(cfg_get tuicr.build_from_fork false)" = "true" ] && continue
161
+ # pi update never moves a pinned npm source, so a newer release is nothing
162
+ # upgrade could fetch. Say so once instead of reporting it stale forever.
163
+ if pi_npm_pin "$name"; then
164
+ printf 'unknown\t%s\tpinned to %s in pi, which pi update never moves\n' "$name" "$(pi_state pkg "$(pi_npm_package "$name")")"
165
+ continue
166
+ fi
167
+ inst=$(installed_version "$name")
168
+ [ -n "$inst" ] || continue # not installed is apply's business, not this one
169
+ latest=$(latest_version "$src" "$ref")
170
+ if [ -z "$latest" ]; then
171
+ [ "${ACSTACK_TW_OFFLINE:-0}" = "1" ] \
172
+ || printf 'unknown\t%s\tcould not reach %s\n' "$name" "$src"
173
+ elif [ "$inst" != "$latest" ]; then
174
+ printf 'stale\t%s\t%s -> %s\n' "$name" "$inst" "$latest"
175
+ fi
176
+ done < <(version_table)
177
+
178
+ # The ACStack package on pi is pinned to a release, so it falls behind the
179
+ # moment this plugin updates. Both versions are local, so this needs no
180
+ # network and reports even offline.
181
+ if [ "$(cfg_get pi.enabled false)" = "true" ]; then
182
+ local behind; behind=$(pi_acstack_behind)
183
+ [ -n "$behind" ] && printf 'stale\tpi-acstack\t%s -> %s\n' "${behind%%$'\t'*}" "${behind#*$'\t'}"
184
+ fi
185
+ }
186
+
187
+ # Reinstalls over what is there, from the coordinates the table already holds.
188
+ # Split out so the tuicr arm can guard the fork build and still reach it,
189
+ # rather than carrying a second copy of the repository and the asset match.
190
+ upgrade_from_table() { # upgrade_from_table <name>
191
+ local repo match
192
+ read -r repo match < <(version_table | awk -F'\t' -v n="$1" '$1==n{print $4, $5}')
193
+ [ -n "${repo:-}" ] && [ "$repo" != "-" ] \
194
+ || { say "$1 is not a tool this script installs"; return 1; }
195
+ INSTALL_FORCE=1 install_binary "$1" "$repo" "$match"
196
+ # yazi's archive carries ya beside it, and the two must not drift.
197
+ if [ "$1" = "yazi" ]; then
198
+ INSTALL_FORCE=1 install_binary ya sxyazi/yazi "$match"
199
+ fi
200
+ }
201
+
202
+ upgrade_one() { # upgrade_one <name>
203
+ if [ "$1" = "pi-acstack" ] || pi_npm_package "$1" >/dev/null; then
204
+ find_pi
205
+ fi
206
+ case "$1" in
207
+ herdr) herdr update >/dev/null 2>&1 \
208
+ && say "herdr updated" || say "herdr update failed" ;;
209
+ gh-dash) gh extension upgrade dlvhdr/gh-dash >/dev/null 2>&1 \
210
+ && say "gh-dash upgraded" || say "gh-dash upgrade failed" ;;
211
+ harlequin) uv tool upgrade harlequin >/dev/null 2>&1 \
212
+ && say "harlequin upgraded" || say "harlequin upgrade failed" ;;
213
+ # With no target, pi update updates pi itself and no package.
214
+ pi) if pi update >/dev/null 2>&1; then
215
+ say "pi updated"
216
+ patch_pi_win32_paste
217
+ else
218
+ say "pi update failed"
219
+ fi ;;
220
+ # Only a pin behind this plugin. A newer pin is the user's choice, and an
221
+ # absent or unpinned ACStack is apply's business.
222
+ pi-acstack)
223
+ [ -n "$(pi_acstack_behind)" ] || { say "pi-acstack: nothing behind this plugin to move"; return 0; }
224
+ pi_add "$PI_ACSTACK@$(acstack_version)" "$PI_ACSTACK" ;;
225
+ # Absent from the table, because it has no version flag for outdated to
226
+ # read. Naming it is the only way to refresh it, so the arm it needs is
227
+ # here rather than behind a table lookup that will never find it.
228
+ mermaid-ascii)
229
+ INSTALL_FORCE=1 install_binary mermaid-ascii AlexanderGrooff/mermaid-ascii Linux_x86_64 ;;
230
+ hadolint)
231
+ INSTALL_FORCE=1 install_plain_github_binary hadolint hadolint/hadolint Linux-x86_64 ;;
232
+ terragrunt)
233
+ INSTALL_FORCE=1 install_plain_github_binary terragrunt gruntwork-io/terragrunt linux_amd64 ;;
234
+ # The fork build comes from a local checkout, so a release archive would
235
+ # replace it with the stock binary and silently drop the patches it carries.
236
+ # outdated already skips it; naming it here has to skip it too.
237
+ tuicr)
238
+ if [ "$(cfg_get tuicr.build_from_fork false)" = "true" ]; then
239
+ say "tuicr is built from a fork here, so run apply to rebuild it"
240
+ return 0
241
+ fi
242
+ upgrade_from_table tuicr ;;
243
+ *) if pkg=$(pi_npm_package "$1"); then
244
+ if pi_npm_pin "$1"; then
245
+ say "$1 is pinned to $(pi_state pkg "$pkg") in pi, which pi update never moves"
246
+ return 0
247
+ fi
248
+ pi update "$pkg" >/dev/null 2>&1 \
249
+ && say "$1 updated" || say "$1 update failed"
250
+ else
251
+ upgrade_from_table "$1"
252
+ fi ;;
253
+ esac
254
+ }
@@ -0,0 +1,31 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+
3
+ export default function (pi: ExtensionAPI) {
4
+ let pending: { active: boolean; native: boolean } | undefined;
5
+ let forwarding = false;
6
+
7
+ pi.events.on("herdr:blocked", (event: unknown) => {
8
+ if (!forwarding && pending && (event as { active?: unknown } | null)?.active === pending.active) {
9
+ pending.native = true;
10
+ pending = undefined;
11
+ }
12
+ });
13
+
14
+ pi.events.on("rpiv:ask-user:blocked", (event: unknown) => {
15
+ const active = (event as { active?: unknown } | null)?.active;
16
+ if (typeof active !== "boolean") return;
17
+ const transition = { active, native: false };
18
+ pending = transition;
19
+ // shortcut: dual producers emit public then herdr synchronously; async pairs need an upstream source ID
20
+ queueMicrotask(() => {
21
+ if (pending === transition) pending = undefined;
22
+ if (transition.native) return;
23
+ forwarding = true;
24
+ try {
25
+ pi.events.emit("herdr:blocked", { active });
26
+ } finally {
27
+ forwarding = false;
28
+ }
29
+ });
30
+ });
31
+ }