acstack 0.0.0-stage → 0.47.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 +19 -0
- package/LICENSE +21 -0
- package/README.md +389 -2
- package/THIRD_PARTY_NOTICES +14 -0
- package/TROUBLESHOOTING.md +225 -0
- package/agents/codex/frontend-verifier.toml +352 -0
- package/agents/codex/task-checker.toml +345 -0
- package/agents/codex/task-executor.toml +157 -0
- package/agents/codex/test-writer.toml +149 -0
- package/agents/frontend-verifier.md +351 -0
- package/agents/task-checker.md +344 -0
- package/agents/task-executor.md +156 -0
- package/agents/test-writer.md +148 -0
- package/assets/playwriter-bridge/build-launcher.ps1 +32 -0
- package/assets/playwriter-bridge/doctor.ps1 +450 -0
- package/assets/playwriter-bridge/launcher-src.cs +131 -0
- package/assets/playwriter-bridge/opener.js +330 -0
- package/assets/playwriter-bridge/opener.ps1 +63 -0
- package/assets/playwriter-bridge/register-tasks.ps1 +328 -0
- package/assets/playwriter-bridge/relay.ps1 +50 -0
- package/assets/playwriter-bridge/stop.ps1 +125 -0
- package/assets/playwriter-bridge/sync.ps1 +271 -0
- package/assets/playwriter-bridge/tunnel.ps1 +325 -0
- package/assets/playwriter-bridge/vm-free-port.sh +166 -0
- package/assets/terminal-workspace/client-keybindings.md +87 -0
- package/assets/terminal-workspace/config.example.yaml +334 -0
- package/assets/terminal-workspace/setup.sh +4404 -0
- package/cli/README.md +47 -0
- package/cli/bin/acstack.js +3 -0
- package/cli/dist/index.js +25984 -0
- package/cli/dist/index.js.map +7 -0
- package/cli/package.json +59 -0
- package/docs/adr/0001-ste-writing-standard.md +16 -0
- package/docs/adr/0002-version-gate-not-auto-bump.md +35 -0
- package/docs/adr/0003-handoff-ack-command.md +19 -0
- package/docs/adr/0004-dev-setup-per-host.md +13 -0
- package/docs/adr/0005-two-orchestrators-per-host.md +25 -0
- package/docs/adr/0006-document-opener-service.md +89 -0
- package/docs/adr/0007-lead-check-reads-the-transcript.md +87 -0
- package/docs/adr/0008-cli-audits-skill-interviews.md +37 -0
- package/docs/adr/0009-verify-the-write-not-the-writer.md +51 -0
- package/docs/adr/0010-promote-dev-to-main-by-fast-forward.md +115 -0
- package/docs/adr/0011-node-header-is-the-edge-encoding.md +20 -0
- package/docs/adr/0012-node-kind-is-a-closed-set-of-five.md +31 -0
- package/docs/adr/0013-the-diagram-renderer-is-pure.md +23 -0
- package/docs/adr/0014-reading-a-node-hides-its-generated-diagram.md +29 -0
- package/docs/adr/0015-a-node-id-links-to-its-issue-through-an-anchor.md +37 -0
- package/docs/adr/0016-the-map-root-carries-no-status-of-its-own.md +21 -0
- package/docs/adr/0017-call-impeccable-copy-only-its-questions.md +13 -0
- package/docs/adr/index.md +17 -0
- package/docs/config-reference.md +572 -0
- package/docs/dev-setups.md +164 -0
- package/docs/migrate-0.8.md +28 -0
- package/docs/playwriter-bridge-windows.md +15 -0
- package/docs/terminal-workspace-keys.md +137 -0
- package/docs/terminal-workspace.md +1780 -0
- package/docs/workflows.md +309 -0
- package/hooks/ac-writing-style-reminder.sh +86 -0
- package/hooks/context-pressure.sh +583 -0
- package/hooks/hooks.json +50 -0
- package/hooks/session-start-handoff.sh +165 -0
- package/hooks/session-start.sh +214 -0
- package/orchestrator/AGENTS.md +532 -0
- package/output-styles/ac-writing-style.md +286 -0
- package/package.json +50 -4
- package/pi/agents/frontend-verifier.md +357 -0
- package/pi/agents/task-checker.md +348 -0
- package/pi/agents/task-executor.md +160 -0
- package/pi/agents/test-writer.md +154 -0
- package/pi/extension.ts +723 -0
- package/profiles/fullstack-python.yaml +29 -0
- package/profiles/node-typescript.yaml +23 -0
- package/profiles/python.yaml +24 -0
- package/skills/archive/SKILL.md +160 -0
- package/skills/bootstrap/SKILL.md +109 -0
- package/skills/bridge/HANDOFF.md +93 -0
- package/skills/bridge/SKILL-VM.md +292 -0
- package/skills/bridge/SKILL-WINDOWS.md +526 -0
- package/skills/bridge/SKILL.md +121 -0
- package/skills/browser-verify/SKILL.md +229 -0
- package/skills/code-review/SKILL.md +44 -0
- package/skills/commit/SKILL.md +106 -0
- package/skills/compact-and-continue/SKILL.md +241 -0
- package/skills/explore/SKILL.md +149 -0
- package/skills/grilling/SKILL.md +134 -0
- package/skills/handoff/SKILL.md +250 -0
- package/skills/handoff/launch-herdr.md +197 -0
- package/skills/handoff/launch-without-herdr.md +30 -0
- package/skills/handoff/lead-check.md +59 -0
- package/skills/handoff/outcomes.md +162 -0
- package/skills/handoff/running-agent.md +50 -0
- package/skills/handoff/watch.md +115 -0
- package/skills/host/SKILL.md +247 -0
- package/skills/host/detect-host.sh +297 -0
- package/skills/host/herdr.md +79 -0
- package/skills/host/install-herdr-block.sh +467 -0
- package/skills/host/install-orca.sh +512 -0
- package/skills/host/notes.md +97 -0
- package/skills/host/orca.md +160 -0
- package/skills/host/questions.md +163 -0
- package/skills/host/report.md +42 -0
- package/skills/implement/SKILL.md +230 -0
- package/skills/implement/decision-log.md +62 -0
- package/skills/implement/design-pass.md +32 -0
- package/skills/implement/relaxed-tdd.md +28 -0
- package/skills/map/SKILL.md +250 -0
- package/skills/map/frontend.md +227 -0
- package/skills/map/trackers/files.md +74 -0
- package/skills/map/trackers/github.md +180 -0
- package/skills/map/visuals.md +387 -0
- package/skills/new-worktree/SKILL.md +155 -0
- package/skills/new-worktree/detect-orchestrator.sh +328 -0
- package/skills/new-worktree/detect-orchestrator.test.sh +894 -0
- package/skills/new-worktree/herdr.md +84 -0
- package/skills/new-worktree/orca.md +101 -0
- package/skills/pre-commit-review/SKILL.md +242 -0
- package/skills/pre-commit-review/auto-fix.md +19 -0
- package/skills/pre-commit-review/interactive.md +35 -0
- package/skills/quick-fix/SKILL.md +125 -0
- package/skills/record-context/SKILL.md +106 -0
- package/skills/setup/SKILL.md +250 -0
- package/skills/setup/browser.md +123 -0
- package/skills/setup/design-review.md +97 -0
- package/skills/setup/first-run.md +114 -0
- package/skills/setup/report.md +68 -0
- package/skills/setup/row-fixes.md +196 -0
- package/skills/setup/terminal-workspace.md +153 -0
- package/skills/sync/SKILL.md +171 -0
- package/skills/teardown-worktree/SKILL.md +245 -0
- package/skills/teardown-worktree/detector.md +39 -0
- package/skills/teardown-worktree/herdr.md +113 -0
- package/skills/teardown-worktree/orca.md +61 -0
- package/skills/terminal-workspace/SKILL.md +249 -0
- package/skills/terminal-workspace/components.md +36 -0
- package/skills/terminal-workspace/config-choices.md +69 -0
- package/skills/terminal-workspace/emulator-keys.md +48 -0
- package/skills/terminal-workspace/headless-clipboard.md +175 -0
- package/skills/terminal-workspace/turn-off-and-update.md +104 -0
- package/skills/test-conventions/SKILL.md +59 -0
- package/skills/visual-docs/SKILL.md +246 -0
- package/skills/writing/SKILL.md +231 -0
- package/skills/writing/examples.md +153 -0
- package/skills/writing/vocabulary.md +96 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ACStack host setup: tell herdr where to put its worktrees.
|
|
3
|
+
#
|
|
4
|
+
# herdr is the terminal workspace this project drives agents from. It makes a
|
|
5
|
+
# git worktree per unit of work, and where those worktrees land is one setting
|
|
6
|
+
# in ~/.config/herdr/config.toml. This script writes that one setting and
|
|
7
|
+
# nothing else, inside a fenced block it owns:
|
|
8
|
+
#
|
|
9
|
+
# # >>> acstack terminal-workspace >>>
|
|
10
|
+
# [worktrees]
|
|
11
|
+
# directory = "~/.herdr/worktrees"
|
|
12
|
+
# # <<< acstack terminal-workspace <<<
|
|
13
|
+
#
|
|
14
|
+
# The fence is what makes the script safe to run twice, and safe to run against
|
|
15
|
+
# a file somebody has been editing by hand. Every line outside the block
|
|
16
|
+
# survives, in its original order, with its content unchanged, and only the
|
|
17
|
+
# block's own lines are ever rewritten. Two edits outside the block are possible
|
|
18
|
+
# and both are deliberate: a blank separator line goes in ahead of a block being
|
|
19
|
+
# appended to a file that already had content, and a file that ended without a
|
|
20
|
+
# newline gains one.
|
|
21
|
+
#
|
|
22
|
+
# It refuses, and writes nothing, in three cases. Two of them are files that no
|
|
23
|
+
# edit leaves readable: a file holding more than one such block, and a file
|
|
24
|
+
# defining [worktrees] outside the block. TOML rejects a table defined twice,
|
|
25
|
+
# and guessing which copy the user meant to keep would be the wrong kind of
|
|
26
|
+
# helpful. The third is a block this script did not write.
|
|
27
|
+
# assets/terminal-workspace/setup.sh fences a far wider block between these same
|
|
28
|
+
# two markers, holding the keymap and the popup key bindings as well as
|
|
29
|
+
# [worktrees], and replacing that with these four lines would delete all of it.
|
|
30
|
+
# A wider block that already names the wanted directory is left alone and
|
|
31
|
+
# reported as nothing to do.
|
|
32
|
+
#
|
|
33
|
+
# There are two modes and no default. --plan prints what would happen and writes
|
|
34
|
+
# nothing at all; --apply does it. Plan mode cannot write by accident, because
|
|
35
|
+
# the one function in this script that writes is only ever called on the apply
|
|
36
|
+
# path.
|
|
37
|
+
#
|
|
38
|
+
# Every path derives from $HOME or from --config, and nothing is hardcoded to a
|
|
39
|
+
# real location. That is what makes it testable: tests/test-host-install.sh runs
|
|
40
|
+
# it under `env -i` with a PATH of fake executables and a temporary HOME.
|
|
41
|
+
|
|
42
|
+
set -u
|
|
43
|
+
|
|
44
|
+
PROG="install-herdr-block.sh"
|
|
45
|
+
TOTAL=1
|
|
46
|
+
|
|
47
|
+
# --- the managed block -----------------------------------------------------
|
|
48
|
+
#
|
|
49
|
+
# These four strings are the contract. The markers are matched on whole lines,
|
|
50
|
+
# exactly, so a line that merely mentions one of them is ordinary content.
|
|
51
|
+
|
|
52
|
+
BLOCK_START='# >>> acstack terminal-workspace >>>'
|
|
53
|
+
BLOCK_END='# <<< acstack terminal-workspace <<<'
|
|
54
|
+
BLOCK_TABLE='[worktrees]'
|
|
55
|
+
# shellcheck disable=SC2088 # the literal "~/" is what gets written into config.toml; herdr expands it
|
|
56
|
+
DEFAULT_WORKTREE_DIR='~/.herdr/worktrees'
|
|
57
|
+
# Where the kernel release string lives on Linux. An option overrides it, which
|
|
58
|
+
# is what lets a test hand the Windows Subsystem for Linux check its own file.
|
|
59
|
+
DEFAULT_OSRELEASE_FILE="/proc/sys/kernel/osrelease"
|
|
60
|
+
|
|
61
|
+
usage() {
|
|
62
|
+
cat <<'USAGE'
|
|
63
|
+
install-herdr-block.sh - point herdr's worktrees at one directory.
|
|
64
|
+
|
|
65
|
+
Usage:
|
|
66
|
+
install-herdr-block.sh --plan [options] Print what would happen. Writes nothing.
|
|
67
|
+
install-herdr-block.sh --apply [options] Do it. Running it twice is a no-op.
|
|
68
|
+
install-herdr-block.sh --help Print this help and exit.
|
|
69
|
+
|
|
70
|
+
Options:
|
|
71
|
+
--worktree-dir DIR Directory herdr puts worktrees in.
|
|
72
|
+
Default: ~/.herdr/worktrees, written literally, with the
|
|
73
|
+
tilde left for herdr to expand.
|
|
74
|
+
--config PATH Config file to edit.
|
|
75
|
+
Default: ~/.config/herdr/config.toml
|
|
76
|
+
--osrelease-file PATH
|
|
77
|
+
Kernel release file the Windows Subsystem for Linux
|
|
78
|
+
check reads. Default: /proc/sys/kernel/osrelease
|
|
79
|
+
|
|
80
|
+
Exactly one of --plan and --apply is required; neither is the default, because
|
|
81
|
+
this script edits a file you may have written by hand.
|
|
82
|
+
|
|
83
|
+
Only the fenced block between the markers
|
|
84
|
+
|
|
85
|
+
# >>> acstack terminal-workspace >>>
|
|
86
|
+
# <<< acstack terminal-workspace <<<
|
|
87
|
+
|
|
88
|
+
is ever written. Everything else in the file is preserved exactly, in order.
|
|
89
|
+
|
|
90
|
+
Exit codes: 0 all good, 1 the write failed, 3 this machine is not Linux,
|
|
91
|
+
4 the file is not safe to edit, 64 the command line was wrong.
|
|
92
|
+
USAGE
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# --- small helpers ---------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
err() { printf '%s: %s\n' "$PROG" "$1" >&2; }
|
|
98
|
+
|
|
99
|
+
fail() { err "$1"; exit 1; }
|
|
100
|
+
|
|
101
|
+
die_usage() {
|
|
102
|
+
err "$1"
|
|
103
|
+
err "run with --help for usage."
|
|
104
|
+
exit 64
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
# The file cannot be edited without corrupting it. Say why, in full, and stop
|
|
108
|
+
# without having written anything.
|
|
109
|
+
refuse() {
|
|
110
|
+
err "$1"
|
|
111
|
+
exit 4
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# --- argument parsing ------------------------------------------------------
|
|
115
|
+
#
|
|
116
|
+
# Parsed in full first, so --help and a bad command line cost nothing: no
|
|
117
|
+
# platform detection and no file so much as opened.
|
|
118
|
+
|
|
119
|
+
MODE=""
|
|
120
|
+
WORKTREE_DIR="$DEFAULT_WORKTREE_DIR"
|
|
121
|
+
CONFIG=""
|
|
122
|
+
OSRELEASE_FILE="$DEFAULT_OSRELEASE_FILE"
|
|
123
|
+
|
|
124
|
+
need_value() { # $1=option name, $2=arguments left including the option itself
|
|
125
|
+
[ "$2" -ge 2 ] || die_usage "$1 needs a value."
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
set_mode() {
|
|
129
|
+
[ -z "$MODE" ] || die_usage "choose exactly one of --plan and --apply."
|
|
130
|
+
MODE="$1"
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
while [ "$#" -gt 0 ]; do
|
|
134
|
+
case "$1" in
|
|
135
|
+
--plan) set_mode "plan" ;;
|
|
136
|
+
--apply) set_mode "apply" ;;
|
|
137
|
+
--worktree-dir) need_value "$1" "$#"; WORKTREE_DIR="$2"; shift ;;
|
|
138
|
+
--config) need_value "$1" "$#"; CONFIG="$2"; shift ;;
|
|
139
|
+
--osrelease-file) need_value "$1" "$#"; OSRELEASE_FILE="$2"; shift ;;
|
|
140
|
+
--help|-h) usage; exit 0 ;;
|
|
141
|
+
*) die_usage "unknown argument: $1" ;;
|
|
142
|
+
esac
|
|
143
|
+
shift
|
|
144
|
+
done
|
|
145
|
+
|
|
146
|
+
[ -n "$MODE" ] || die_usage "one of --plan and --apply is required."
|
|
147
|
+
|
|
148
|
+
[ -n "$WORKTREE_DIR" ] || die_usage "--worktree-dir needs a directory."
|
|
149
|
+
|
|
150
|
+
# The directory goes into a basic TOML string. A double quote would end that
|
|
151
|
+
# string early and a backslash starts an escape, so neither can be let through:
|
|
152
|
+
# the result would be a config file herdr cannot parse.
|
|
153
|
+
case "$WORKTREE_DIR" in
|
|
154
|
+
*'"'*|*'\'*) die_usage "--worktree-dir may not hold a double quote or a backslash, and this one does: $WORKTREE_DIR" ;;
|
|
155
|
+
esac
|
|
156
|
+
|
|
157
|
+
[ -n "${HOME:-}" ] || fail "HOME is not set, so there is no default config path."
|
|
158
|
+
[ -n "$CONFIG" ] || CONFIG="$HOME/.config/herdr/config.toml"
|
|
159
|
+
|
|
160
|
+
# --- platform gate ---------------------------------------------------------
|
|
161
|
+
#
|
|
162
|
+
# herdr is a Linux terminal workspace. Elsewhere there is no config file worth
|
|
163
|
+
# writing, so the script stops before touching anything.
|
|
164
|
+
|
|
165
|
+
PLATFORM="$(uname -s 2>/dev/null || true)"
|
|
166
|
+
if [ "$PLATFORM" != "Linux" ]; then
|
|
167
|
+
printf '%s: this installer supports Linux only. This machine reports: %s\n' \
|
|
168
|
+
"$PROG" "${PLATFORM:-unknown}"
|
|
169
|
+
printf '%s: nothing has been changed.\n' "$PROG"
|
|
170
|
+
exit 3
|
|
171
|
+
fi
|
|
172
|
+
|
|
173
|
+
# --- the Windows Subsystem for Linux is not supported ----------------------
|
|
174
|
+
#
|
|
175
|
+
# The Windows Subsystem for Linux is a Linux environment running on Windows. It
|
|
176
|
+
# answers `Linux` to `uname -s`, so the gate above lets it straight through, and
|
|
177
|
+
# herdr is not supported there. Two markers give it away and either one on its
|
|
178
|
+
# own is enough.
|
|
179
|
+
|
|
180
|
+
WSL_EVIDENCE=""
|
|
181
|
+
if [ -n "${WSL_DISTRO_NAME:-}" ]; then
|
|
182
|
+
WSL_EVIDENCE="the WSL_DISTRO_NAME environment variable is set to: $WSL_DISTRO_NAME"
|
|
183
|
+
elif [ -f "$OSRELEASE_FILE" ] && [ -r "$OSRELEASE_FILE" ] &&
|
|
184
|
+
grep -qi microsoft "$OSRELEASE_FILE" 2>/dev/null; then
|
|
185
|
+
# Matched without regard to case: the kernel release reads `microsoft` on WSL2
|
|
186
|
+
# and `Microsoft` on WSL1.
|
|
187
|
+
WSL_EVIDENCE="the kernel release in $OSRELEASE_FILE names microsoft: $(head -n 1 "$OSRELEASE_FILE" 2>/dev/null)"
|
|
188
|
+
fi
|
|
189
|
+
|
|
190
|
+
# An osrelease file that is not there says nothing about the environment, so a
|
|
191
|
+
# machine without one carries on rather than being refused on a missing file.
|
|
192
|
+
if [ -n "$WSL_EVIDENCE" ]; then
|
|
193
|
+
printf '%s: this is the Windows Subsystem for Linux (WSL), which is not supported.\n' "$PROG"
|
|
194
|
+
printf '%s: %s\n' "$PROG" "$WSL_EVIDENCE"
|
|
195
|
+
printf '%s: nothing has been changed.\n' "$PROG"
|
|
196
|
+
exit 3
|
|
197
|
+
fi
|
|
198
|
+
|
|
199
|
+
# --- the block -------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
DIR_LINE="directory = \"$WORKTREE_DIR\""
|
|
202
|
+
|
|
203
|
+
render_block() {
|
|
204
|
+
printf '%s\n' "$BLOCK_START"
|
|
205
|
+
printf '%s\n' "$BLOCK_TABLE"
|
|
206
|
+
printf '%s\n' "$DIR_LINE"
|
|
207
|
+
printf '%s\n' "$BLOCK_END"
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
BLOCK_WANTED="$(render_block)"
|
|
211
|
+
|
|
212
|
+
# True when $1 is a line that opens the [worktrees] table, allowing for the
|
|
213
|
+
# surrounding whitespace TOML permits.
|
|
214
|
+
is_worktrees_table() {
|
|
215
|
+
local s="$1"
|
|
216
|
+
s="${s#"${s%%[![:space:]]*}"}"
|
|
217
|
+
s="${s%"${s##*[![:space:]]}"}"
|
|
218
|
+
[ "$s" = "$BLOCK_TABLE" ]
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
# The block's own lines, markers stripped off.
|
|
222
|
+
block_body() {
|
|
223
|
+
printf '%s\n' "$BLOCK_FOUND" | sed '1d;$d'
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
# TOML tables the block defines besides [worktrees], space separated, and empty
|
|
227
|
+
# when it defines none.
|
|
228
|
+
#
|
|
229
|
+
# This script owns the [worktrees] table and nothing else, so a block holding a
|
|
230
|
+
# second table was written by somebody else. That somebody is
|
|
231
|
+
# assets/terminal-workspace/setup.sh, which fences a far wider block - [keys], a
|
|
232
|
+
# [[keys.command]] per binding, and [worktrees] - between these same two
|
|
233
|
+
# markers. Replacing that block with these four lines deletes a keymap, so the
|
|
234
|
+
# shape of the block is what tells the two writers apart.
|
|
235
|
+
foreign_tables() {
|
|
236
|
+
local line s out=""
|
|
237
|
+
while IFS= read -r line; do
|
|
238
|
+
s="${line#"${line%%[![:space:]]*}"}"
|
|
239
|
+
s="${s%"${s##*[![:space:]]}"}"
|
|
240
|
+
case "$s" in
|
|
241
|
+
\[*\]) ;;
|
|
242
|
+
*) continue ;;
|
|
243
|
+
esac
|
|
244
|
+
is_worktrees_table "$s" && continue
|
|
245
|
+
case " $out " in *" $s "*) continue ;; esac
|
|
246
|
+
out="${out:+$out }$s"
|
|
247
|
+
done <<BODY
|
|
248
|
+
$(block_body)
|
|
249
|
+
BODY
|
|
250
|
+
printf '%s' "$out"
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
# Whether the block already carries the exact directory line this script would
|
|
254
|
+
# write. A wider block that already says the right thing needs no edit at all.
|
|
255
|
+
block_sets_wanted_dir() {
|
|
256
|
+
printf '%s\n' "$BLOCK_FOUND" | grep -qxF -- "$DIR_LINE"
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
# Line numbers of every whole line equal to $1 in $2, space separated.
|
|
260
|
+
marker_lines() {
|
|
261
|
+
grep -nxF -- "$1" "$2" 2>/dev/null | cut -d: -f1 | tr '\n' ' '
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
count_words() {
|
|
265
|
+
printf '%s' "$1" | wc -w
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
# --- read the file and decide ----------------------------------------------
|
|
269
|
+
#
|
|
270
|
+
# ACTION is one of: create, append, replace, none. Working it out is entirely
|
|
271
|
+
# read-only, and both modes reach it the same way.
|
|
272
|
+
|
|
273
|
+
BLOCK_FOUND="" # the block exactly as the file holds it now
|
|
274
|
+
UNMANAGED_LINES="" # line numbers of [worktrees] tables outside the block
|
|
275
|
+
UNTERMINATED=0 # a start marker the file never closes
|
|
276
|
+
|
|
277
|
+
# One pass, tracking whether we are inside the block, because a [worktrees] line
|
|
278
|
+
# inside the block is ours and a [worktrees] line outside it is somebody else's.
|
|
279
|
+
scan_config() {
|
|
280
|
+
local n=0 inside=0 line
|
|
281
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
282
|
+
n=$((n + 1))
|
|
283
|
+
if [ "$inside" -eq 0 ] && [ "$line" = "$BLOCK_START" ]; then
|
|
284
|
+
inside=1
|
|
285
|
+
BLOCK_FOUND="$line"
|
|
286
|
+
continue
|
|
287
|
+
fi
|
|
288
|
+
if [ "$inside" -eq 1 ]; then
|
|
289
|
+
BLOCK_FOUND="$BLOCK_FOUND
|
|
290
|
+
$line"
|
|
291
|
+
[ "$line" = "$BLOCK_END" ] && inside=0
|
|
292
|
+
continue
|
|
293
|
+
fi
|
|
294
|
+
if is_worktrees_table "$line"; then
|
|
295
|
+
UNMANAGED_LINES="${UNMANAGED_LINES:+$UNMANAGED_LINES }$n"
|
|
296
|
+
fi
|
|
297
|
+
done < "$1"
|
|
298
|
+
UNTERMINATED="$inside"
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
ACTION="create"
|
|
302
|
+
SKIP_REASON=""
|
|
303
|
+
|
|
304
|
+
if [ -e "$CONFIG" ] && [ ! -f "$CONFIG" ]; then
|
|
305
|
+
refuse "$CONFIG exists but is not a regular file, so it will not be edited."
|
|
306
|
+
fi
|
|
307
|
+
|
|
308
|
+
if [ -f "$CONFIG" ]; then
|
|
309
|
+
START_LINES="$(marker_lines "$BLOCK_START" "$CONFIG")"
|
|
310
|
+
END_LINES="$(marker_lines "$BLOCK_END" "$CONFIG")"
|
|
311
|
+
N_START="$(count_words "$START_LINES")"
|
|
312
|
+
N_END="$(count_words "$END_LINES")"
|
|
313
|
+
|
|
314
|
+
if [ "$N_START" -gt 1 ] || [ "$N_END" -gt 1 ]; then
|
|
315
|
+
err "$CONFIG holds more than one acstack terminal-workspace block."
|
|
316
|
+
err "start markers on line(s): ${START_LINES:-none}"
|
|
317
|
+
err "end markers on line(s): ${END_LINES:-none}"
|
|
318
|
+
refuse "leave exactly one block in place and run this again. Nothing has been changed."
|
|
319
|
+
fi
|
|
320
|
+
if [ "$N_START" -ne "$N_END" ]; then
|
|
321
|
+
err "$CONFIG has an acstack terminal-workspace fence that does not pair up:"
|
|
322
|
+
err "start markers on line(s): ${START_LINES:-none}"
|
|
323
|
+
err "end markers on line(s): ${END_LINES:-none}"
|
|
324
|
+
refuse "repair the fence by hand and run this again. Nothing has been changed."
|
|
325
|
+
fi
|
|
326
|
+
|
|
327
|
+
scan_config "$CONFIG"
|
|
328
|
+
|
|
329
|
+
if [ "$UNTERMINATED" -eq 1 ]; then
|
|
330
|
+
refuse "$CONFIG opens an acstack terminal-workspace block on line ${START_LINES:-?}and never closes it. Repair it by hand and run this again. Nothing has been changed."
|
|
331
|
+
fi
|
|
332
|
+
if [ -n "$UNMANAGED_LINES" ]; then
|
|
333
|
+
err "$CONFIG already defines a $BLOCK_TABLE table on line(s): $UNMANAGED_LINES"
|
|
334
|
+
err "That table is outside the managed block, so writing the block would define $BLOCK_TABLE twice and herdr would no longer be able to read the file."
|
|
335
|
+
refuse "move that table inside the block, or delete it, and run this again. Nothing has been changed."
|
|
336
|
+
fi
|
|
337
|
+
|
|
338
|
+
if [ "$N_START" -eq 1 ]; then
|
|
339
|
+
FOREIGN_TABLES="$(foreign_tables)"
|
|
340
|
+
if [ "$BLOCK_FOUND" = "$BLOCK_WANTED" ]; then
|
|
341
|
+
ACTION="none"
|
|
342
|
+
SKIP_REASON="the block is already exactly right"
|
|
343
|
+
elif [ -n "$FOREIGN_TABLES" ]; then
|
|
344
|
+
if block_sets_wanted_dir; then
|
|
345
|
+
ACTION="none"
|
|
346
|
+
SKIP_REASON="the terminal-workspace block already sets this directory"
|
|
347
|
+
else
|
|
348
|
+
err "$CONFIG holds a block between these markers that this script did not write."
|
|
349
|
+
err "Besides $BLOCK_TABLE that block defines: $FOREIGN_TABLES"
|
|
350
|
+
err "terminal-workspace setup.sh writes that block, and it carries the keymap and the popup key bindings. Replacing it with the four lines this script writes would delete all of it."
|
|
351
|
+
err "setup.sh writes $BLOCK_TABLE itself, from herdr.worktrees_directory in ~/.config/acstack/terminal-workspace.yaml."
|
|
352
|
+
refuse "set the directory there and run terminal-workspace setup.sh apply instead. Nothing has been changed."
|
|
353
|
+
fi
|
|
354
|
+
else
|
|
355
|
+
ACTION="replace"
|
|
356
|
+
fi
|
|
357
|
+
else
|
|
358
|
+
ACTION="append"
|
|
359
|
+
fi
|
|
360
|
+
fi
|
|
361
|
+
|
|
362
|
+
case "$ACTION" in
|
|
363
|
+
create) DETAIL="the file does not exist yet, so it is created holding just the block" ;;
|
|
364
|
+
append) DETAIL="the file exists without the block, so the block is added at the end" ;;
|
|
365
|
+
replace) DETAIL="the file holds an out-of-date block, so those lines are replaced in place" ;;
|
|
366
|
+
*) DETAIL="" ;;
|
|
367
|
+
esac
|
|
368
|
+
|
|
369
|
+
# --- the write -------------------------------------------------------------
|
|
370
|
+
|
|
371
|
+
# The whole new file, on stdout. Lines outside the block are reproduced exactly
|
|
372
|
+
# and in order; only the block's four lines come from us.
|
|
373
|
+
render_new_config() {
|
|
374
|
+
local line inside=0 had_content=0
|
|
375
|
+
case "$ACTION" in
|
|
376
|
+
create)
|
|
377
|
+
render_block
|
|
378
|
+
;;
|
|
379
|
+
append)
|
|
380
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
381
|
+
printf '%s\n' "$line"
|
|
382
|
+
had_content=1
|
|
383
|
+
done < "$CONFIG"
|
|
384
|
+
[ "$had_content" -eq 1 ] && printf '\n'
|
|
385
|
+
render_block
|
|
386
|
+
;;
|
|
387
|
+
replace)
|
|
388
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
389
|
+
if [ "$inside" -eq 0 ] && [ "$line" = "$BLOCK_START" ]; then
|
|
390
|
+
inside=1
|
|
391
|
+
render_block
|
|
392
|
+
continue
|
|
393
|
+
fi
|
|
394
|
+
if [ "$inside" -eq 1 ]; then
|
|
395
|
+
[ "$line" = "$BLOCK_END" ] && inside=0
|
|
396
|
+
continue
|
|
397
|
+
fi
|
|
398
|
+
printf '%s\n' "$line"
|
|
399
|
+
done < "$CONFIG"
|
|
400
|
+
;;
|
|
401
|
+
esac
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
# The one function in this script that writes anything. It stages the new file
|
|
405
|
+
# beside the old one and moves it into place, so a config file is either the old
|
|
406
|
+
# one or the new one and never a half-written mixture.
|
|
407
|
+
write_config() {
|
|
408
|
+
local dir tmp mode=""
|
|
409
|
+
|
|
410
|
+
dir="$(dirname "$CONFIG")"
|
|
411
|
+
mkdir -p "$dir" || { err "could not create $dir"; return 1; }
|
|
412
|
+
|
|
413
|
+
tmp="$(mktemp "$dir/.herdr-config.XXXXXX")" || {
|
|
414
|
+
err "could not create a temporary file in $dir"; return 1; }
|
|
415
|
+
|
|
416
|
+
if ! render_new_config > "$tmp"; then
|
|
417
|
+
rm -f "$tmp"; err "could not write the new config to $tmp"; return 1
|
|
418
|
+
fi
|
|
419
|
+
|
|
420
|
+
[ -f "$CONFIG" ] && mode="$(stat -c '%a' "$CONFIG" 2>/dev/null || true)"
|
|
421
|
+
|
|
422
|
+
if ! mv -f "$tmp" "$CONFIG"; then
|
|
423
|
+
rm -f "$tmp"; err "could not move the new config into $CONFIG"; return 1
|
|
424
|
+
fi
|
|
425
|
+
|
|
426
|
+
# mktemp makes the staged file private; give the config back the permissions
|
|
427
|
+
# it had, or the ordinary ones if it is new.
|
|
428
|
+
chmod "${mode:-644}" "$CONFIG" || { err "could not restore the mode of $CONFIG"; return 1; }
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
# --- reporting -------------------------------------------------------------
|
|
432
|
+
|
|
433
|
+
step_line() { # $1=number $2=description $3=needed $4=reason
|
|
434
|
+
local status
|
|
435
|
+
if [ "$3" -eq 1 ]; then
|
|
436
|
+
if [ "$MODE" = "plan" ]; then status="[todo]"; else status="[done]"; fi
|
|
437
|
+
else
|
|
438
|
+
if [ "$MODE" = "plan" ]; then status="[skip: $4]"; else status="[skipped: $4]"; fi
|
|
439
|
+
fi
|
|
440
|
+
printf '[%s/%s] %s %s\n' "$1" "$TOTAL" "$2" "$status"
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
# --- run -------------------------------------------------------------------
|
|
444
|
+
|
|
445
|
+
if [ "$MODE" = "plan" ]; then
|
|
446
|
+
printf '%s: plan only. Nothing on this machine is changed.\n\n' "$PROG"
|
|
447
|
+
else
|
|
448
|
+
printf '%s: applying.\n\n' "$PROG"
|
|
449
|
+
fi
|
|
450
|
+
|
|
451
|
+
NEEDED=1
|
|
452
|
+
[ "$ACTION" = "none" ] && NEEDED=0
|
|
453
|
+
|
|
454
|
+
# The work comes first, so that a failure never gets to print [done].
|
|
455
|
+
if [ "$NEEDED" -eq 1 ] && [ "$MODE" = "apply" ]; then
|
|
456
|
+
write_config || fail "could not write the worktrees block into $CONFIG"
|
|
457
|
+
fi
|
|
458
|
+
|
|
459
|
+
step_line 1 "worktrees block in $CONFIG" "$NEEDED" "$SKIP_REASON"
|
|
460
|
+
|
|
461
|
+
if [ "$NEEDED" -eq 1 ]; then
|
|
462
|
+
printf ' %s:\n' "$DETAIL"
|
|
463
|
+
render_block | sed 's/^/ /'
|
|
464
|
+
fi
|
|
465
|
+
|
|
466
|
+
printf '\n'
|
|
467
|
+
printf 'Nothing here needs root: the only file touched is %s.\n' "$CONFIG"
|