youmd 0.6.27 → 0.7.0
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/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 +24 -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,141 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# bootstrap-new-mac.sh — set up a new Mac with Houston's agent skill/stack layer
|
|
3
|
+
#
|
|
4
|
+
# Safe to re-run (idempotent). Prints all steps before executing them.
|
|
5
|
+
# Does NOT run launchctl, does NOT push git, does NOT install anything
|
|
6
|
+
# beyond youmd CLI and git clones.
|
|
7
|
+
#
|
|
8
|
+
# After this script: run install-daemons.sh to activate the sync daemons.
|
|
9
|
+
#
|
|
10
|
+
# Usage: bash bootstrap-new-mac.sh
|
|
11
|
+
|
|
12
|
+
set -euo pipefail
|
|
13
|
+
|
|
14
|
+
AGENT_SHARED="${HOME}/.agent-shared"
|
|
15
|
+
SCISTACK="${HOME}/.claude/scistack"
|
|
16
|
+
CLAUDE_SKILLS="${HOME}/.claude/skills"
|
|
17
|
+
|
|
18
|
+
LOOSE_SKILLS="agent-runtime-guard agent-stack-sync continue skill-governor"
|
|
19
|
+
|
|
20
|
+
AGENT_SHARED_REPO="https://github.com/houstongolden/agent-shared.git"
|
|
21
|
+
SCISTACK_REPO="https://github.com/Hubify-Projects/scistack.git"
|
|
22
|
+
|
|
23
|
+
step() {
|
|
24
|
+
echo ""
|
|
25
|
+
echo "===> $*"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
info() {
|
|
29
|
+
echo " $*"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
warn() {
|
|
33
|
+
echo " WARN: $*"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
step "1. Install youmd CLI"
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
if command -v youmd >/dev/null 2>&1; then
|
|
40
|
+
info "youmd already installed: $(youmd --version 2>/dev/null || echo '(version unknown)')"
|
|
41
|
+
else
|
|
42
|
+
info "Installing youmd via official install script..."
|
|
43
|
+
curl -fsSL https://you.md/install.sh | bash
|
|
44
|
+
info "youmd installed."
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
step "2. Clone agent-shared repo → ${AGENT_SHARED}"
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
if [ -d "${AGENT_SHARED}/.git" ]; then
|
|
51
|
+
info "Already present: ${AGENT_SHARED} — skipping clone."
|
|
52
|
+
else
|
|
53
|
+
info "Cloning ${AGENT_SHARED_REPO} → ${AGENT_SHARED}"
|
|
54
|
+
mkdir -p "$(dirname "${AGENT_SHARED}")"
|
|
55
|
+
# Use gh if available for credential handling; fall back to git
|
|
56
|
+
if command -v gh >/dev/null 2>&1; then
|
|
57
|
+
gh repo clone houstongolden/agent-shared "${AGENT_SHARED}"
|
|
58
|
+
else
|
|
59
|
+
git clone "${AGENT_SHARED_REPO}" "${AGENT_SHARED}"
|
|
60
|
+
fi
|
|
61
|
+
info "Cloned agent-shared."
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
step "3. Clone scistack repo → ${SCISTACK}"
|
|
66
|
+
# ---------------------------------------------------------------------------
|
|
67
|
+
if [ -d "${SCISTACK}/.git" ]; then
|
|
68
|
+
info "Already present: ${SCISTACK} — skipping clone."
|
|
69
|
+
else
|
|
70
|
+
info "Cloning ${SCISTACK_REPO} → ${SCISTACK}"
|
|
71
|
+
mkdir -p "$(dirname "${SCISTACK}")"
|
|
72
|
+
if command -v gh >/dev/null 2>&1; then
|
|
73
|
+
gh repo clone Hubify-Projects/scistack "${SCISTACK}"
|
|
74
|
+
else
|
|
75
|
+
git clone "${SCISTACK_REPO}" "${SCISTACK}"
|
|
76
|
+
fi
|
|
77
|
+
info "Cloned scistack."
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
# ---------------------------------------------------------------------------
|
|
81
|
+
step "4. Restore loose skills from agent-shared/claude-skills/ → ~/.claude/skills/"
|
|
82
|
+
# ---------------------------------------------------------------------------
|
|
83
|
+
mkdir -p "${CLAUDE_SKILLS}"
|
|
84
|
+
|
|
85
|
+
for skill in ${LOOSE_SKILLS}; do
|
|
86
|
+
src="${AGENT_SHARED}/claude-skills/${skill}"
|
|
87
|
+
dst="${CLAUDE_SKILLS}/${skill}"
|
|
88
|
+
|
|
89
|
+
if [ ! -d "${src}" ]; then
|
|
90
|
+
warn "Loose skill '${skill}' not found in agent-shared yet — will be populated on first sync."
|
|
91
|
+
continue
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
if [ -d "${dst}" ]; then
|
|
95
|
+
info "Skill '${skill}' already exists at ${dst} — skipping (run sync.sh to update)."
|
|
96
|
+
else
|
|
97
|
+
info "Restoring skill '${skill}' → ${dst}"
|
|
98
|
+
rsync -a "${src}/" "${dst}/"
|
|
99
|
+
info "Restored ${skill}."
|
|
100
|
+
fi
|
|
101
|
+
done
|
|
102
|
+
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
step "5. Reminder: restore secrets"
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
echo ""
|
|
107
|
+
echo " !! IMPORTANT: Secrets are NOT in git. Restore them separately:"
|
|
108
|
+
echo ""
|
|
109
|
+
echo " If you use the env-vault (~/Desktop/CODE_2025/youmd/cli/scripts/env-vault/):"
|
|
110
|
+
echo " youmd env restore <vault-file> # CLI wrapper (preferred)"
|
|
111
|
+
echo " bash ~/Desktop/CODE_2025/youmd/cli/scripts/env-vault/restore.sh <vault-file>"
|
|
112
|
+
echo ""
|
|
113
|
+
echo " Secrets to restore (set in your shell profile or .env files):"
|
|
114
|
+
echo " OPENROUTER_API_KEY"
|
|
115
|
+
echo " CONVEX_DEPLOY_KEY"
|
|
116
|
+
echo " YOUMD_API_KEY"
|
|
117
|
+
echo " GH_TOKEN (or: gh auth login)"
|
|
118
|
+
echo " ANTHROPIC_API_KEY"
|
|
119
|
+
echo ""
|
|
120
|
+
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
step "6. Next steps"
|
|
123
|
+
# ---------------------------------------------------------------------------
|
|
124
|
+
echo ""
|
|
125
|
+
echo " Bootstrap complete. To activate the auto-sync daemons, run:"
|
|
126
|
+
echo ""
|
|
127
|
+
echo " youmd stack daemon install # preferred (CLI-native)"
|
|
128
|
+
echo ""
|
|
129
|
+
echo " Or run the raw script directly:"
|
|
130
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
131
|
+
echo " bash ${SCRIPT_DIR}/install-daemons.sh"
|
|
132
|
+
echo ""
|
|
133
|
+
echo " To do a manual sync now:"
|
|
134
|
+
echo " youmd stack sync # preferred (CLI-native)"
|
|
135
|
+
echo ""
|
|
136
|
+
echo " To do a dry-run first (recommended):"
|
|
137
|
+
echo " youmd stack sync --dry-run"
|
|
138
|
+
echo ""
|
|
139
|
+
echo "================================================================"
|
|
140
|
+
echo "Bootstrap done. Welcome to the new machine."
|
|
141
|
+
echo "================================================================"
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>Label</key>
|
|
6
|
+
<string>com.youmd.identity-sync</string>
|
|
7
|
+
|
|
8
|
+
<key>ProgramArguments</key>
|
|
9
|
+
<array>
|
|
10
|
+
<string>/bin/zsh</string>
|
|
11
|
+
<string>-lc</string>
|
|
12
|
+
<string>youmd sync</string>
|
|
13
|
+
</array>
|
|
14
|
+
|
|
15
|
+
<key>StartInterval</key>
|
|
16
|
+
<integer>300</integer>
|
|
17
|
+
|
|
18
|
+
<key>RunAtLoad</key>
|
|
19
|
+
<true/>
|
|
20
|
+
|
|
21
|
+
<key>KeepAlive</key>
|
|
22
|
+
<false/>
|
|
23
|
+
|
|
24
|
+
<key>StandardOutPath</key>
|
|
25
|
+
<string>__HOME__/.youmd/logs/identity-sync.out.log</string>
|
|
26
|
+
|
|
27
|
+
<key>StandardErrorPath</key>
|
|
28
|
+
<string>__HOME__/.youmd/logs/identity-sync.err.log</string>
|
|
29
|
+
|
|
30
|
+
<key>EnvironmentVariables</key>
|
|
31
|
+
<dict>
|
|
32
|
+
<key>HOME</key>
|
|
33
|
+
<string>__HOME__</string>
|
|
34
|
+
</dict>
|
|
35
|
+
</dict>
|
|
36
|
+
</plist>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>Label</key>
|
|
6
|
+
<string>com.youmd.skillstack-sync</string>
|
|
7
|
+
|
|
8
|
+
<key>ProgramArguments</key>
|
|
9
|
+
<array>
|
|
10
|
+
<string>/bin/zsh</string>
|
|
11
|
+
<string>-lc</string>
|
|
12
|
+
<string>youmd stack sync</string>
|
|
13
|
+
</array>
|
|
14
|
+
|
|
15
|
+
<key>StartInterval</key>
|
|
16
|
+
<integer>300</integer>
|
|
17
|
+
|
|
18
|
+
<key>RunAtLoad</key>
|
|
19
|
+
<true/>
|
|
20
|
+
|
|
21
|
+
<key>KeepAlive</key>
|
|
22
|
+
<false/>
|
|
23
|
+
|
|
24
|
+
<key>StandardOutPath</key>
|
|
25
|
+
<string>__HOME__/.youmd/logs/skillstack-sync.out.log</string>
|
|
26
|
+
|
|
27
|
+
<key>StandardErrorPath</key>
|
|
28
|
+
<string>__HOME__/.youmd/logs/skillstack-sync.err.log</string>
|
|
29
|
+
|
|
30
|
+
<key>EnvironmentVariables</key>
|
|
31
|
+
<dict>
|
|
32
|
+
<key>HOME</key>
|
|
33
|
+
<string>__HOME__</string>
|
|
34
|
+
<key>PATH</key>
|
|
35
|
+
<string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
|
36
|
+
</dict>
|
|
37
|
+
</dict>
|
|
38
|
+
</plist>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# install-daemons.sh — install skillstack-sync and identity-sync LaunchAgents
|
|
3
|
+
#
|
|
4
|
+
# Idempotent: safe to re-run. Unloads before reloading.
|
|
5
|
+
# Does NOT need sudo — LaunchAgents run as the current user.
|
|
6
|
+
#
|
|
7
|
+
# Usage: ./install-daemons.sh
|
|
8
|
+
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
|
|
11
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
12
|
+
LAUNCH_AGENTS_DIR="${HOME}/Library/LaunchAgents"
|
|
13
|
+
LOG_DIR="${HOME}/.youmd/logs"
|
|
14
|
+
|
|
15
|
+
PLISTS="com.youmd.skillstack-sync com.youmd.identity-sync"
|
|
16
|
+
|
|
17
|
+
echo "==> Creating log directory: ${LOG_DIR}"
|
|
18
|
+
mkdir -p "${LOG_DIR}"
|
|
19
|
+
|
|
20
|
+
echo "==> Creating LaunchAgents directory: ${LAUNCH_AGENTS_DIR}"
|
|
21
|
+
mkdir -p "${LAUNCH_AGENTS_DIR}"
|
|
22
|
+
|
|
23
|
+
for label in ${PLISTS}; do
|
|
24
|
+
src="${SCRIPT_DIR}/${label}.plist"
|
|
25
|
+
dst="${LAUNCH_AGENTS_DIR}/${label}.plist"
|
|
26
|
+
|
|
27
|
+
if [ ! -f "${src}" ]; then
|
|
28
|
+
echo "ERROR: Source plist not found: ${src}" >&2
|
|
29
|
+
exit 1
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
echo ""
|
|
33
|
+
echo "--- Installing ${label} ---"
|
|
34
|
+
|
|
35
|
+
# Substitute __HOME__ placeholder with actual home dir
|
|
36
|
+
# BSD sed: use a temp file, no -i '' in-place on stdin
|
|
37
|
+
local_tmp="$(mktemp -t youmd_plist)"
|
|
38
|
+
sed "s|__HOME__|${HOME}|g" "${src}" > "${local_tmp}"
|
|
39
|
+
|
|
40
|
+
# Unload existing daemon if loaded (ignore errors — it may not be loaded)
|
|
41
|
+
if launchctl list "${label}" >/dev/null 2>&1; then
|
|
42
|
+
echo " Unloading existing ${label}..."
|
|
43
|
+
launchctl unload "${dst}" 2>/dev/null || true
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
echo " Copying ${label}.plist → ${LAUNCH_AGENTS_DIR}/"
|
|
47
|
+
cp "${local_tmp}" "${dst}"
|
|
48
|
+
rm -f "${local_tmp}"
|
|
49
|
+
|
|
50
|
+
echo " Loading ${label}..."
|
|
51
|
+
launchctl load -w "${dst}"
|
|
52
|
+
|
|
53
|
+
echo " Loaded: ${label}"
|
|
54
|
+
done
|
|
55
|
+
|
|
56
|
+
echo ""
|
|
57
|
+
echo "================================================================"
|
|
58
|
+
echo "Both daemons installed and running."
|
|
59
|
+
echo ""
|
|
60
|
+
echo "Log files:"
|
|
61
|
+
echo " ${LOG_DIR}/skillstack-sync.log (combined sync log)"
|
|
62
|
+
echo " ${LOG_DIR}/skillstack-sync.out.log (daemon stdout)"
|
|
63
|
+
echo " ${LOG_DIR}/skillstack-sync.err.log (daemon stderr)"
|
|
64
|
+
echo " ${LOG_DIR}/identity-sync.out.log (youmd sync stdout)"
|
|
65
|
+
echo " ${LOG_DIR}/identity-sync.err.log (youmd sync stderr)"
|
|
66
|
+
echo ""
|
|
67
|
+
echo "Check daemon status:"
|
|
68
|
+
echo " launchctl list com.youmd.skillstack-sync"
|
|
69
|
+
echo " launchctl list com.youmd.identity-sync"
|
|
70
|
+
echo ""
|
|
71
|
+
echo "To uninstall:"
|
|
72
|
+
for label in ${PLISTS}; do
|
|
73
|
+
echo " launchctl unload ${LAUNCH_AGENTS_DIR}/${label}.plist && rm ${LAUNCH_AGENTS_DIR}/${label}.plist"
|
|
74
|
+
done
|
|
75
|
+
echo "================================================================"
|
|
@@ -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 "$@"
|