Repository navigation
Expand file tree
/
Copy pathdeployAgentSystem.sh
More file actions
executable file
·262 lines (236 loc) · 7.12 KB
/
Copy pathdeployAgentSystem.sh
File metadata and controls
executable file
·262 lines (236 loc) · 7.12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
#!/usr/bin/env bash
set -euo pipefail
# ---------------------------------------------------------------------------
# CQ01/CQ06: robust, transactional deployment of the whole agent system.
#
# Two-variant install (CQ06): the default deploys the orchestrated opencode
# agents (opencode_agentic) + opencode.jsonc; pi deploys the pi package.
# Skills always deploy to .agents/skills.
#
# - Runs under set -euo pipefail.
# - Validates CLI args before any work.
# - Snapshots every affected destination BEFORE writing, and restores on any
# verification failure (destination returns to its initial state).
# - Reports only *verified* copy counts.
# - Supports --dry-run (preview, writes/snapshots nothing) and
# --project <dir> (deploy into a project following runtime conventions).
# ---------------------------------------------------------------------------
# Step 1 — constants & snapshot root
SNAPSHOT_ROOT="${TMPDIR:-/tmp}/deployAgentSystem.$$"
SUCCESS=0
RESTORE_DONE=0
# Snapshot bookkeeping (parallel arrays, indexed by target)
SNAP_KEY=()
EXISTED=()
DST_FINAL_ARR=()
# Defaults
DRY_RUN=0
PROJECT_DIR=""
VARIANT=""
print_help() {
cat <<'EOF'
Usage: deployAgentSystem.sh [--dry-run] [--project <dir>] [pi]
Deploys this repo's agent system to XDG user dirs (or, with --project, into <dir>).
Variants (positional, optional):
(default) deploy the orchestrated opencode agents (opencode_agentic) + opencode.jsonc
pi deploy the pi package + shared skills (no opencode.jsonc)
Options:
--project <dir> deploy into project <dir> instead of user config
--dry-run preview the copy plan; write and snapshot nothing
--help, -h show this help and exit
Skills (./skills) are always deployed to .agents/skills — the cross-harness
Agent Skills location shared by opencode, pi, Codex, Cursor, and Copilot.
Deployment is atomic: on any verification failure the previous state is restored.
EOF
}
# --help/-h short-circuit: scan ALL args before parsing, so help wins
# regardless of position — even after invalid args (e.g.
# `--project /nonexistent --help` must print usage and exit 0).
for _arg in "$@"; do
if [[ "$_arg" == "--help" || "$_arg" == "-h" ]]; then
print_help
exit 0
fi
done
# Step 2 — argument parsing & validation (runs BEFORE the dry-run short-circuit)
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run)
DRY_RUN=1
shift
;;
--project)
if [[ $# -lt 2 ]]; then
echo "error: --project requires an existing directory" >&2
exit 2
fi
PROJECT_DIR="$2"
if [[ ! -d "$PROJECT_DIR" ]]; then
echo "error: --project requires an existing directory: $PROJECT_DIR" >&2
exit 2
fi
shift 2
;;
pi)
if [[ -n "$VARIANT" ]]; then
echo "error: only one variant may be specified: $1" >&2
exit 2
fi
VARIANT="$1"
shift
;;
*)
echo "error: unknown argument: $1" >&2
exit 2
;;
esac
done
# Step 3 — build the target list (src, dst_dir, kind)
SRC=()
DST_DIR=()
KIND=()
add_target() {
SRC+=("$1")
DST_DIR+=("$2")
KIND+=("$3")
}
# Skills are always deployed (user + project, all variants)
if [[ -n "$PROJECT_DIR" ]]; then
add_target ./skills "$PROJECT_DIR/.agents/skills" dir
else
add_target ./skills "$HOME/.agents/skills" dir
fi
# Config file: default (orchestrated) only (NOT pi — pi is not opencode)
if [[ "$VARIANT" != "pi" ]]; then
if [[ -n "$PROJECT_DIR" ]]; then
add_target ./opencode.jsonc "$PROJECT_DIR/.opencode" file
else
add_target ./opencode.jsonc "$HOME/.config/opencode" file
fi
fi
# Agents / variant source
if [[ "$VARIANT" == "pi" ]]; then
if [[ -n "$PROJECT_DIR" ]]; then
add_target ./pi "$PROJECT_DIR/.pi" dir
else
add_target ./pi "$HOME/.pi" dir
fi
else
if [[ -n "$PROJECT_DIR" ]]; then
add_target ./opencode_agentic "$PROJECT_DIR/.opencode/agents" dir
else
add_target ./opencode_agentic "$HOME/.config/opencode/agents" dir
fi
fi
# Resolve the final destination path for target index $1 into _DST
dst_final_for() {
local i="$1"
if [[ "${KIND[i]}" == "file" ]]; then
_DST="${DST_DIR[i]}/$(basename "${SRC[i]}")"
else
_DST="${DST_DIR[i]}"
fi
}
# Step 4 — --dry-run short-circuit (no snapshot, no writes, exit 0)
if [[ "$DRY_RUN" -eq 1 ]]; then
if [[ -n "$PROJECT_DIR" ]]; then
echo "dry-run: variant=${VARIANT:-default} target=project ($PROJECT_DIR)"
else
echo "dry-run: variant=${VARIANT:-default} target=user"
fi
for i in "${!SRC[@]}"; do
dst_final_for "$i"
echo "dry-run: would copy ${SRC[i]} -> $_DST (kind=${KIND[i]})"
done
exit 0
fi
# Step 5 — snapshot phase (BEFORE any write)
mkdir -p "$SNAPSHOT_ROOT"
# Register the trap now (after snapshot root exists), so the root is never
# left behind even if snapshotting itself fails.
cleanup() {
if [[ "$SUCCESS" -ne 1 && "$RESTORE_DONE" -ne 1 ]]; then
RESTORE_DONE=1
for i in "${!SNAP_KEY[@]}"; do
local dst="${DST_FINAL_ARR[i]}"
local snap="$SNAPSHOT_ROOT/${SNAP_KEY[i]}"
if [[ "${EXISTED[i]}" -eq 1 ]]; then
# Existed initially -> restore from snapshot.
rm -rf "$dst"
if [[ -d "$snap" ]]; then
mkdir -p "$dst"
cp -r "$snap/." "$dst/"
else
cp -r "$snap" "$dst"
fi
else
# Did not exist initially -> return to non-existent state.
rm -rf "$dst"
fi
done
fi
# Never leave the snapshot dir behind (success or failure).
rm -rf "$SNAPSHOT_ROOT"
}
trap cleanup EXIT
for i in "${!SRC[@]}"; do
dst_final_for "$i"
key="t$i"
if [[ -e "$_DST" ]]; then
existed=1
snap="$SNAPSHOT_ROOT/$key"
if [[ -d "$_DST" ]]; then
mkdir -p "$snap"
cp -r "$_DST/." "$snap/"
else
mkdir -p "$(dirname "$snap")"
cp -r "$_DST" "$snap"
fi
else
existed=0
fi
# Snapshot for this target is fully complete -> record it in the
# bookkeeping arrays. If a cp above had failed, the script exits here
# and cleanup sees only fully-snapshotted (unmodified) destinations,
# so a restore is a harmless no-op-equivalent — never a partial one.
DST_FINAL_ARR+=("$_DST")
SNAP_KEY+=("$key")
EXISTED+=("$existed")
done
# Step 6 — copy_to (file vs directory)
copy_to() {
local src="$1" dst="$2" kind="$3"
mkdir -p "$dst"
if [[ "$kind" == "file" ]]; then
# File source: cp -r "$src" "$dst/" preserves the basename and avoids the
# ENOTDIR failure that "$src/." would cause for a file.
cp -r "$src" "$dst/"
else
cp -r "$src/." "$dst/"
fi
}
# Step 7 — copy loop + per-target verification + truthful counts
for i in "${!SRC[@]}"; do
dst_final_for "$i"
copy_to "${SRC[i]}" "${DST_DIR[i]}" "${KIND[i]}"
if [[ "${KIND[i]}" == "file" ]]; then
if test -f "$_DST"; then
count=1
else
count=0
fi
else
if test -d "$_DST"; then
count=$(find "$_DST" -type f | wc -l)
else
count=0
fi
fi
# The printed count is derived from post-copy verification only.
echo "Copied $count files from ${SRC[i]} to $_DST"
if [[ "$count" -eq 0 ]]; then
echo "error: verification failed for $_DST; restoring snapshot" >&2
exit 1
fi
done
SUCCESS=1