youmd 0.6.27 → 0.7.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/dist/commands/env.d.ts +9 -0
- package/dist/commands/env.d.ts.map +1 -0
- package/dist/commands/env.js +105 -0
- package/dist/commands/env.js.map +1 -0
- package/dist/commands/machine.d.ts +2 -0
- package/dist/commands/machine.d.ts.map +1 -0
- package/dist/commands/machine.js +71 -0
- package/dist/commands/machine.js.map +1 -0
- package/dist/commands/stack.d.ts.map +1 -1
- package/dist/commands/stack.js +98 -0
- package/dist/commands/stack.js.map +1 -1
- package/dist/index.js +53 -0
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/scripts/check-version-exists.mjs +40 -0
- package/scripts/env-vault/README.md +123 -0
- package/scripts/env-vault/backup.sh +167 -0
- package/scripts/env-vault/restore.sh +149 -0
- package/scripts/skillstack-sync/README.md +166 -0
- package/scripts/skillstack-sync/bootstrap-new-mac.sh +141 -0
- package/scripts/skillstack-sync/com.youmd.identity-sync.plist +36 -0
- package/scripts/skillstack-sync/com.youmd.skillstack-sync.plist +38 -0
- package/scripts/skillstack-sync/install-daemons.sh +75 -0
- package/scripts/skillstack-sync/sync.sh +320 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# sync.sh — cross-machine agent skill/stack syncer
|
|
3
|
+
# Compatible: bash 3.2+, macOS BSD coreutils (NO mapfile, NO GNU mktemp flags)
|
|
4
|
+
#
|
|
5
|
+
# Usage:
|
|
6
|
+
# ./sync.sh # sync all repos + loose skills
|
|
7
|
+
# ./sync.sh --dry-run # log intended actions only, write nothing
|
|
8
|
+
#
|
|
9
|
+
# Env overrides:
|
|
10
|
+
# SKILLSTACK_REPOS space-separated list of absolute repo paths (default: ~/.agent-shared ~/.claude/scistack)
|
|
11
|
+
# GIT_USER_NAME commit author name (default: Houston Golden)
|
|
12
|
+
# GIT_USER_EMAIL commit author email (default: houston@bamf.ai)
|
|
13
|
+
|
|
14
|
+
set -euo pipefail
|
|
15
|
+
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
# Config
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
20
|
+
LOG_DIR="${HOME}/.youmd/logs"
|
|
21
|
+
LOG_FILE="${LOG_DIR}/skillstack-sync.log"
|
|
22
|
+
|
|
23
|
+
GIT_USER_NAME="${GIT_USER_NAME:-Houston Golden}"
|
|
24
|
+
GIT_USER_EMAIL="${GIT_USER_EMAIL:-houston@bamf.ai}"
|
|
25
|
+
|
|
26
|
+
AGENT_SHARED="${HOME}/.agent-shared"
|
|
27
|
+
SCISTACK="${HOME}/.claude/scistack"
|
|
28
|
+
|
|
29
|
+
# 4 loose skills that live in ~/.claude/skills/ but aren't in any repo
|
|
30
|
+
LOOSE_SKILLS="agent-runtime-guard agent-stack-sync continue skill-governor"
|
|
31
|
+
|
|
32
|
+
# Repos to sync (space-separated absolute paths; override via env).
|
|
33
|
+
# v1 default is agent-shared ONLY — it's purpose-built for sync and safe to
|
|
34
|
+
# auto-commit. scistack is intentionally NOT auto-synced by default: it holds
|
|
35
|
+
# protected learning-loop IP where auto-committing WIP on a timer is undesirable.
|
|
36
|
+
# To opt scistack (or other repos) in, set:
|
|
37
|
+
# SKILLSTACK_REPOS="$HOME/.agent-shared $HOME/.claude/scistack"
|
|
38
|
+
DEFAULT_REPOS="${AGENT_SHARED}"
|
|
39
|
+
SKILLSTACK_REPOS="${SKILLSTACK_REPOS:-${DEFAULT_REPOS}}"
|
|
40
|
+
|
|
41
|
+
DRY_RUN=0
|
|
42
|
+
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
# Parse flags
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
for arg in "$@"; do
|
|
47
|
+
case "$arg" in
|
|
48
|
+
--dry-run) DRY_RUN=1 ;;
|
|
49
|
+
--once) : ;; # default — accepted for explicit invocation
|
|
50
|
+
*) echo "Unknown flag: $arg" >&2; exit 1 ;;
|
|
51
|
+
esac
|
|
52
|
+
done
|
|
53
|
+
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
# Logging
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
mkdir -p "${LOG_DIR}"
|
|
58
|
+
|
|
59
|
+
log() {
|
|
60
|
+
local ts
|
|
61
|
+
ts="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
|
62
|
+
local msg="[${ts}] $*"
|
|
63
|
+
echo "${msg}"
|
|
64
|
+
echo "${msg}" >> "${LOG_FILE}"
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
log_warn() {
|
|
68
|
+
log "WARN $*"
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
log_error() {
|
|
72
|
+
log "ERROR $*"
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
log_dry() {
|
|
76
|
+
log "DRY $*"
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
# Helpers
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
hostname_short() {
|
|
84
|
+
# BSD hostname (no --short flag)
|
|
85
|
+
hostname | cut -d. -f1
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
is_dry() {
|
|
89
|
+
[ "${DRY_RUN}" -eq 1 ]
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
# Check if repo is in a mid-merge/rebase state
|
|
93
|
+
repo_is_in_progress() {
|
|
94
|
+
local repo="$1"
|
|
95
|
+
if [ -f "${repo}/.git/MERGE_HEAD" ] \
|
|
96
|
+
|| [ -d "${repo}/.git/rebase-merge" ] \
|
|
97
|
+
|| [ -d "${repo}/.git/rebase-apply" ]; then
|
|
98
|
+
return 0
|
|
99
|
+
fi
|
|
100
|
+
return 1
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# Diff two directories; returns 0 if they differ, 1 if identical
|
|
104
|
+
dirs_differ() {
|
|
105
|
+
local src="$1" dst="$2"
|
|
106
|
+
if diff -rq "${src}" "${dst}" >/dev/null 2>&1; then
|
|
107
|
+
return 1 # identical
|
|
108
|
+
fi
|
|
109
|
+
return 0 # different
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# ---------------------------------------------------------------------------
|
|
113
|
+
# LOOSE SKILL MIRROR (local → agent-shared repo)
|
|
114
|
+
# ---------------------------------------------------------------------------
|
|
115
|
+
mirror_loose_skills_to_repo() {
|
|
116
|
+
local skill name src dst
|
|
117
|
+
log "--- Loose-skill mirror: ~/.claude/skills/ → ${AGENT_SHARED}/claude-skills/ ---"
|
|
118
|
+
|
|
119
|
+
for skill in ${LOOSE_SKILLS}; do
|
|
120
|
+
src="${HOME}/.claude/skills/${skill}"
|
|
121
|
+
dst="${AGENT_SHARED}/claude-skills/${skill}"
|
|
122
|
+
|
|
123
|
+
if [ ! -d "${src}" ]; then
|
|
124
|
+
log_warn "Loose skill '${skill}' not found at ${src}, skipping."
|
|
125
|
+
continue
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
if is_dry; then
|
|
129
|
+
log_dry "Would rsync ${src}/ → ${dst}/"
|
|
130
|
+
else
|
|
131
|
+
mkdir -p "${dst}"
|
|
132
|
+
rsync -a "${src}/" "${dst}/"
|
|
133
|
+
log "Mirrored ${skill} → ${AGENT_SHARED}/claude-skills/${skill}"
|
|
134
|
+
fi
|
|
135
|
+
done
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
# ---------------------------------------------------------------------------
|
|
139
|
+
# REVERSE LOOSE SKILL MIRROR (agent-shared repo → local ~/.claude/skills/)
|
|
140
|
+
# Called AFTER agent-shared is synced, so we pick up remote edits.
|
|
141
|
+
# ---------------------------------------------------------------------------
|
|
142
|
+
mirror_loose_skills_from_repo() {
|
|
143
|
+
local skill src dst bak ts
|
|
144
|
+
log "--- Reverse loose-skill mirror: ${AGENT_SHARED}/claude-skills/ → ~/.claude/skills/ ---"
|
|
145
|
+
|
|
146
|
+
for skill in ${LOOSE_SKILLS}; do
|
|
147
|
+
src="${AGENT_SHARED}/claude-skills/${skill}"
|
|
148
|
+
dst="${HOME}/.claude/skills/${skill}"
|
|
149
|
+
|
|
150
|
+
if [ ! -d "${src}" ]; then
|
|
151
|
+
log_warn "Repo copy of loose skill '${skill}' not found at ${src}, skipping reverse mirror."
|
|
152
|
+
continue
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
# Detect if there are actually changes to apply
|
|
156
|
+
if [ -d "${dst}" ] && ! dirs_differ "${src}" "${dst}"; then
|
|
157
|
+
log "Loose skill '${skill}': no changes, skipping reverse mirror."
|
|
158
|
+
continue
|
|
159
|
+
fi
|
|
160
|
+
|
|
161
|
+
if is_dry; then
|
|
162
|
+
log_dry "Would backup ${dst} → ${dst}.bak.<timestamp> and rsync ${src}/ → ${dst}/"
|
|
163
|
+
continue
|
|
164
|
+
fi
|
|
165
|
+
|
|
166
|
+
# Backup existing local copy before overwriting
|
|
167
|
+
if [ -d "${dst}" ]; then
|
|
168
|
+
ts="$(date -u +"%Y-%m-%dT%H-%M-%SZ")"
|
|
169
|
+
bak="${dst}.bak.${ts}"
|
|
170
|
+
log "Backing up ${dst} → ${bak}"
|
|
171
|
+
rsync -a "${dst}/" "${bak}/"
|
|
172
|
+
fi
|
|
173
|
+
|
|
174
|
+
mkdir -p "${dst}"
|
|
175
|
+
rsync -a "${src}/" "${dst}/"
|
|
176
|
+
log "Reverse-mirrored ${skill} → ${dst}"
|
|
177
|
+
done
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
# ---------------------------------------------------------------------------
|
|
181
|
+
# SYNC ONE REPO
|
|
182
|
+
# Returns 0 on success, 1 on non-fatal failure.
|
|
183
|
+
# ---------------------------------------------------------------------------
|
|
184
|
+
sync_repo() {
|
|
185
|
+
local repo="$1"
|
|
186
|
+
local repo_name
|
|
187
|
+
repo_name="$(basename "${repo}")"
|
|
188
|
+
|
|
189
|
+
log "=== Syncing repo: ${repo} ==="
|
|
190
|
+
|
|
191
|
+
# Guard: must be a git repo
|
|
192
|
+
if [ ! -d "${repo}/.git" ]; then
|
|
193
|
+
log_warn "Skipping ${repo}: not a git repository."
|
|
194
|
+
return 1
|
|
195
|
+
fi
|
|
196
|
+
|
|
197
|
+
# Guard: must not be mid-merge/rebase
|
|
198
|
+
if repo_is_in_progress "${repo}"; then
|
|
199
|
+
log_warn "Skipping ${repo}: repository is mid-merge or mid-rebase. Resolve manually first."
|
|
200
|
+
return 1
|
|
201
|
+
fi
|
|
202
|
+
|
|
203
|
+
# Stage and commit local changes if any
|
|
204
|
+
local status_out
|
|
205
|
+
status_out="$(git -C "${repo}" status --porcelain 2>&1)"
|
|
206
|
+
if [ -n "${status_out}" ]; then
|
|
207
|
+
local ts host commit_msg
|
|
208
|
+
ts="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
|
209
|
+
host="$(hostname_short)"
|
|
210
|
+
commit_msg="auto-sync: ${host} ${ts}"
|
|
211
|
+
|
|
212
|
+
if is_dry; then
|
|
213
|
+
log_dry "Would commit in ${repo}: '${commit_msg}'"
|
|
214
|
+
log_dry "Changed files:\n${status_out}"
|
|
215
|
+
else
|
|
216
|
+
git -C "${repo}" add -A
|
|
217
|
+
git -C "${repo}" \
|
|
218
|
+
-c "user.name=${GIT_USER_NAME}" \
|
|
219
|
+
-c "user.email=${GIT_USER_EMAIL}" \
|
|
220
|
+
commit -m "${commit_msg}"
|
|
221
|
+
log "Committed local changes in ${repo_name}: '${commit_msg}'"
|
|
222
|
+
fi
|
|
223
|
+
else
|
|
224
|
+
log "No local changes in ${repo_name}."
|
|
225
|
+
fi
|
|
226
|
+
|
|
227
|
+
# Pull (merge, no rebase)
|
|
228
|
+
if is_dry; then
|
|
229
|
+
log_dry "Would git pull --no-rebase --no-edit in ${repo}"
|
|
230
|
+
else
|
|
231
|
+
local pull_log
|
|
232
|
+
pull_log="$(mktemp -t skillstack_pull)"
|
|
233
|
+
local pull_ok=0
|
|
234
|
+
git -C "${repo}" pull --no-rebase --no-edit >"${pull_log}" 2>&1 || pull_ok=$?
|
|
235
|
+
# Echo pull output line by line into the log
|
|
236
|
+
while IFS= read -r line; do
|
|
237
|
+
log "pull|${repo_name}: ${line}"
|
|
238
|
+
done < "${pull_log}"
|
|
239
|
+
rm -f "${pull_log}"
|
|
240
|
+
|
|
241
|
+
if [ "${pull_ok}" -ne 0 ]; then
|
|
242
|
+
log_error "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
|
|
243
|
+
log_error "CONFLICT in ${repo_name} — pull failed."
|
|
244
|
+
log_error "Aborting merge/rebase and skipping push."
|
|
245
|
+
log_error "Manual resolution needed: cd ${repo} && git status"
|
|
246
|
+
log_error "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
|
|
247
|
+
# Abort any in-progress merge or rebase, leave working tree clean
|
|
248
|
+
git -C "${repo}" merge --abort 2>/dev/null || true
|
|
249
|
+
git -C "${repo}" rebase --abort 2>/dev/null || true
|
|
250
|
+
return 1
|
|
251
|
+
fi
|
|
252
|
+
log "Pull succeeded for ${repo_name}."
|
|
253
|
+
fi
|
|
254
|
+
|
|
255
|
+
# Push
|
|
256
|
+
if is_dry; then
|
|
257
|
+
log_dry "Would git push in ${repo}"
|
|
258
|
+
else
|
|
259
|
+
local push_log
|
|
260
|
+
push_log="$(mktemp -t skillstack_push)"
|
|
261
|
+
local push_ok=0
|
|
262
|
+
git -C "${repo}" push >"${push_log}" 2>&1 || push_ok=$?
|
|
263
|
+
while IFS= read -r line; do
|
|
264
|
+
log "push|${repo_name}: ${line}"
|
|
265
|
+
done < "${push_log}"
|
|
266
|
+
rm -f "${push_log}"
|
|
267
|
+
|
|
268
|
+
if [ "${push_ok}" -ne 0 ]; then
|
|
269
|
+
log_error "Push FAILED for ${repo_name}. Check connectivity/auth."
|
|
270
|
+
return 1
|
|
271
|
+
fi
|
|
272
|
+
log "Push succeeded for ${repo_name}."
|
|
273
|
+
fi
|
|
274
|
+
|
|
275
|
+
log "=== Done: ${repo_name} ==="
|
|
276
|
+
return 0
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
# ---------------------------------------------------------------------------
|
|
280
|
+
# MAIN
|
|
281
|
+
# ---------------------------------------------------------------------------
|
|
282
|
+
main() {
|
|
283
|
+
local mode
|
|
284
|
+
if is_dry; then
|
|
285
|
+
mode="DRY-RUN"
|
|
286
|
+
else
|
|
287
|
+
mode="LIVE"
|
|
288
|
+
fi
|
|
289
|
+
|
|
290
|
+
log "============================================================"
|
|
291
|
+
log "skillstack-sync START [${mode}] on $(hostname_short)"
|
|
292
|
+
log "============================================================"
|
|
293
|
+
|
|
294
|
+
# Step 1: Mirror loose skills into agent-shared BEFORE syncing it
|
|
295
|
+
mirror_loose_skills_to_repo
|
|
296
|
+
|
|
297
|
+
# Step 2: Sync each repo
|
|
298
|
+
local agent_shared_synced=0
|
|
299
|
+
for repo in ${SKILLSTACK_REPOS}; do
|
|
300
|
+
if sync_repo "${repo}"; then
|
|
301
|
+
if [ "${repo}" = "${AGENT_SHARED}" ]; then
|
|
302
|
+
agent_shared_synced=1
|
|
303
|
+
fi
|
|
304
|
+
fi
|
|
305
|
+
done
|
|
306
|
+
|
|
307
|
+
# Step 3: Reverse-mirror loose skills from agent-shared back to ~/.claude/skills/
|
|
308
|
+
# Only do this if agent-shared was successfully synced (or in dry-run mode)
|
|
309
|
+
if is_dry || [ "${agent_shared_synced}" -eq 1 ]; then
|
|
310
|
+
mirror_loose_skills_from_repo
|
|
311
|
+
else
|
|
312
|
+
log_warn "Skipping reverse loose-skill mirror because agent-shared sync did not complete cleanly."
|
|
313
|
+
fi
|
|
314
|
+
|
|
315
|
+
log "============================================================"
|
|
316
|
+
log "skillstack-sync DONE [${mode}]"
|
|
317
|
+
log "============================================================"
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
main "$@"
|