@brickhouse-tech/sync-agents 0.2.6 → 0.3.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/README.md +29 -10
- package/bin/sync-agents.js +16 -14
- package/package.json +14 -13
- package/src/md/RULE_TEMPLATE.md +0 -6
- package/src/md/SKILL_TEMPLATE.md +0 -23
- package/src/md/STATE_TEMPLATE.md +0 -24
- package/src/md/WORKFLOW_TEMPLATE.md +0 -24
- package/src/sh/sync-agents.sh +0 -1596
package/src/sh/sync-agents.sh
DELETED
|
@@ -1,1596 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# ----------------------------------------------------------------------------
|
|
3
|
-
# DEPRECATED — bash implementation of sync-agents.
|
|
4
|
-
#
|
|
5
|
-
# The Go binary shipped via per-platform npm packages is the supported
|
|
6
|
-
# implementation. This shell script remains only as:
|
|
7
|
-
# - the fallback path in bin/sync-agents.js for unsupported triples
|
|
8
|
-
# - the standalone curl install in README (also deprecated)
|
|
9
|
-
# Scheduled for removal in a future major after platform-package install
|
|
10
|
-
# data shows the fallback is unused. Bug fixes go into go/, not here.
|
|
11
|
-
# ----------------------------------------------------------------------------
|
|
12
|
-
set -euo pipefail
|
|
13
|
-
|
|
14
|
-
AGENTS_DIR=".agents"
|
|
15
|
-
AGENTS_MD="AGENTS.md"
|
|
16
|
-
|
|
17
|
-
# Resolve the real script directory, following symlinks.
|
|
18
|
-
# Needed for `npm i -g` installs where the executable is a symlink in PATH
|
|
19
|
-
# (e.g. /usr/local/bin/sync-agents -> .../node_modules/.../src/sh/sync-agents.sh).
|
|
20
|
-
# BSD readlink (macOS default) lacks -f, so we walk the chain ourselves.
|
|
21
|
-
resolve_script_dir() {
|
|
22
|
-
local path="${BASH_SOURCE[0]}"
|
|
23
|
-
while [[ -L "$path" ]]; do
|
|
24
|
-
local dir
|
|
25
|
-
dir="$(cd -P "$(dirname "$path")" && pwd)"
|
|
26
|
-
path="$(readlink "$path")"
|
|
27
|
-
[[ "$path" != /* ]] && path="$dir/$path"
|
|
28
|
-
done
|
|
29
|
-
cd -P "$(dirname "$path")" && pwd
|
|
30
|
-
}
|
|
31
|
-
SCRIPT_DIR="$(resolve_script_dir)"
|
|
32
|
-
PACKAGE_JSON="${SCRIPT_DIR}/../../package.json"
|
|
33
|
-
TEMPLATES_DIR="${SCRIPT_DIR}/../md"
|
|
34
|
-
|
|
35
|
-
# Pull version from package.json via node (we're an npm package, node is present).
|
|
36
|
-
# Force CommonJS so this works regardless of the caller's ESM/CJS context
|
|
37
|
-
# (e.g. NODE_OPTIONS=--input-type=module). Path is passed via argv to avoid
|
|
38
|
-
# shell-quoting issues. Requires Node 20+.
|
|
39
|
-
if [[ -f "$PACKAGE_JSON" ]] && command -v node >/dev/null 2>&1; then
|
|
40
|
-
VERSION="$(node --input-type=commonjs -e 'process.stdout.write(require(process.argv[1]).version)' "$PACKAGE_JSON" 2>/dev/null || echo unknown)"
|
|
41
|
-
else
|
|
42
|
-
VERSION="unknown"
|
|
43
|
-
fi
|
|
44
|
-
|
|
45
|
-
# Agent target directories
|
|
46
|
-
TARGETS=("claude" "windsurf" "cursor" "copilot")
|
|
47
|
-
|
|
48
|
-
# Colors (disabled if not a terminal)
|
|
49
|
-
if [[ -t 1 ]]; then
|
|
50
|
-
BOLD='\033[1m'
|
|
51
|
-
GREEN='\033[0;32m'
|
|
52
|
-
YELLOW='\033[0;33m'
|
|
53
|
-
RED='\033[0;31m'
|
|
54
|
-
CYAN='\033[0;36m'
|
|
55
|
-
RESET='\033[0m'
|
|
56
|
-
else
|
|
57
|
-
BOLD='' GREEN='' YELLOW='' RED='' CYAN='' RESET=''
|
|
58
|
-
fi
|
|
59
|
-
|
|
60
|
-
info() { echo -e "${GREEN}[info]${RESET} $*"; }
|
|
61
|
-
warn() { echo -e "${YELLOW}[warn]${RESET} $*"; }
|
|
62
|
-
error() { echo -e "${RED}[error]${RESET} $*" >&2; }
|
|
63
|
-
|
|
64
|
-
# Resolve target directory path (copilot uses .github/copilot/ instead of .copilot/)
|
|
65
|
-
resolve_target_dir() {
|
|
66
|
-
local target="$1"
|
|
67
|
-
local root="$2"
|
|
68
|
-
if [[ "$target" == "copilot" ]]; then
|
|
69
|
-
echo "$root/.github/copilot"
|
|
70
|
-
else
|
|
71
|
-
echo "$root/.$target"
|
|
72
|
-
fi
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
# Resolve relative path from target dir back to .agents/ (accounts for depth)
|
|
76
|
-
resolve_agents_rel() {
|
|
77
|
-
local target="$1"
|
|
78
|
-
if [[ "$target" == "copilot" ]]; then
|
|
79
|
-
echo "../../$AGENTS_DIR"
|
|
80
|
-
else
|
|
81
|
-
echo "../$AGENTS_DIR"
|
|
82
|
-
fi
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
usage() {
|
|
86
|
-
cat <<EOF
|
|
87
|
-
${BOLD}sync-agents${RESET} v${VERSION} - One set of agent rules to rule them all.
|
|
88
|
-
|
|
89
|
-
${BOLD}USAGE${RESET}
|
|
90
|
-
sync-agents <command> [options]
|
|
91
|
-
|
|
92
|
-
${BOLD}COMMANDS${RESET}
|
|
93
|
-
init Initialize .agents/ directory structure and AGENTS.md
|
|
94
|
-
sync Sync .agents/ to agent directories via symlinks
|
|
95
|
-
status Show current sync status
|
|
96
|
-
add <type> <name> Add a new rule, skill, or workflow from template
|
|
97
|
-
index Regenerate AGENTS.md index from .agents/ contents
|
|
98
|
-
clean Remove all synced symlinks (does not remove .agents/)
|
|
99
|
-
watch Watch .agents/ for changes and auto-regenerate index
|
|
100
|
-
import <url> Import a rule/skill/workflow from a URL
|
|
101
|
-
hook Install a pre-commit git hook for auto-sync
|
|
102
|
-
fix [type] Migrate legacy dirs + repair broken symlinks
|
|
103
|
-
type: skills, rules, workflows, or all
|
|
104
|
-
--no-clobber: skip items that already exist in .agents/
|
|
105
|
-
inherit <label> <path> Add an inheritance link to AGENTS.md (convention-based)
|
|
106
|
-
inherit --list List current inheritance links
|
|
107
|
-
inherit --remove <label> Remove an inheritance link by label
|
|
108
|
-
version Show version (same as --version)
|
|
109
|
-
|
|
110
|
-
${BOLD}OPTIONS${RESET}
|
|
111
|
-
-h, --help Show this help message
|
|
112
|
-
-v, --version Show version
|
|
113
|
-
-d, --dir <path> Set project root directory (default: current directory)
|
|
114
|
-
--targets <list> Comma-separated targets (overrides .agents/config)
|
|
115
|
-
--dry-run Show what would be done without making changes
|
|
116
|
-
--force Overwrite existing files/symlinks
|
|
117
|
-
|
|
118
|
-
${BOLD}EXAMPLES${RESET}
|
|
119
|
-
sync-agents init # Initialize .agents/ structure
|
|
120
|
-
sync-agents add rule no-eval # Add a new rule called "no-eval"
|
|
121
|
-
sync-agents sync # Sync to .claude/ and .windsurf/
|
|
122
|
-
sync-agents sync --targets claude
|
|
123
|
-
sync-agents status # Show current state
|
|
124
|
-
sync-agents clean # Remove synced symlinks
|
|
125
|
-
|
|
126
|
-
EOF
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
# --------------------------------------------------------------------------
|
|
130
|
-
# Helpers
|
|
131
|
-
# --------------------------------------------------------------------------
|
|
132
|
-
|
|
133
|
-
find_project_root() {
|
|
134
|
-
local dir="${1:-.}"
|
|
135
|
-
dir="$(cd "$dir" && pwd)"
|
|
136
|
-
|
|
137
|
-
# Walk up to find a directory with .agents or .git
|
|
138
|
-
while [[ "$dir" != "/" ]]; do
|
|
139
|
-
if [[ -d "$dir/$AGENTS_DIR" ]] || [[ -d "$dir/.git" ]]; then
|
|
140
|
-
echo "$dir"
|
|
141
|
-
return 0
|
|
142
|
-
fi
|
|
143
|
-
dir="$(dirname "$dir")"
|
|
144
|
-
done
|
|
145
|
-
|
|
146
|
-
# Fallback to current directory
|
|
147
|
-
pwd
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
ensure_agents_dir() {
|
|
151
|
-
if [[ ! -d "$PROJECT_ROOT/$AGENTS_DIR" ]]; then
|
|
152
|
-
error ".agents/ directory not found. Run 'sync-agents init' first."
|
|
153
|
-
exit 1
|
|
154
|
-
fi
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
create_symlink() {
|
|
158
|
-
local source="$1"
|
|
159
|
-
local target="$2"
|
|
160
|
-
local dry_run="${3:-false}"
|
|
161
|
-
|
|
162
|
-
if [[ "$dry_run" == "true" ]]; then
|
|
163
|
-
echo " would link: $target -> $source"
|
|
164
|
-
return 0
|
|
165
|
-
fi
|
|
166
|
-
|
|
167
|
-
local target_dir
|
|
168
|
-
target_dir="$(dirname "$target")"
|
|
169
|
-
mkdir -p "$target_dir"
|
|
170
|
-
|
|
171
|
-
if [[ -L "$target" ]]; then
|
|
172
|
-
local existing
|
|
173
|
-
existing="$(readlink "$target")"
|
|
174
|
-
if [[ "$existing" == "$source" ]]; then
|
|
175
|
-
return 0 # Already correct
|
|
176
|
-
fi
|
|
177
|
-
if [[ "$FORCE" == "true" ]]; then
|
|
178
|
-
rm "$target"
|
|
179
|
-
else
|
|
180
|
-
warn "Symlink already exists: $target -> $existing (use --force to overwrite)"
|
|
181
|
-
return 1
|
|
182
|
-
fi
|
|
183
|
-
elif [[ -e "$target" ]]; then
|
|
184
|
-
if [[ "$FORCE" == "true" ]]; then
|
|
185
|
-
rm -rf "$target"
|
|
186
|
-
else
|
|
187
|
-
warn "File already exists: $target (use --force to overwrite)"
|
|
188
|
-
return 1
|
|
189
|
-
fi
|
|
190
|
-
fi
|
|
191
|
-
|
|
192
|
-
ln -sf "$source" "$target"
|
|
193
|
-
info "Linked: $target -> $source"
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
# --------------------------------------------------------------------------
|
|
197
|
-
# Commands
|
|
198
|
-
# --------------------------------------------------------------------------
|
|
199
|
-
|
|
200
|
-
# Migrate legacy .agents/STATE.md to the new per-file state pattern.
|
|
201
|
-
# - Ensures rules/state.md exists (the convention rule).
|
|
202
|
-
# - If legacy STATE.md has inline history entries, extracts them into a
|
|
203
|
-
# timestamped STATE_legacy-history_YYYYMMDDHHMMSS.md file.
|
|
204
|
-
# - Removes the legacy STATE.md.
|
|
205
|
-
migrate_legacy_state() {
|
|
206
|
-
local agents_dir="$1"
|
|
207
|
-
local legacy="$agents_dir/STATE.md"
|
|
208
|
-
|
|
209
|
-
[[ -f "$legacy" ]] || return 0
|
|
210
|
-
|
|
211
|
-
# Check if legacy file has meaningful content (beyond template boilerplate)
|
|
212
|
-
local content_lines
|
|
213
|
-
content_lines="$(grep -cvE '^(---|trigger:|#|$|Track project|Update this|Be sure|Description of|Save both|A new file|STATE HISTORY)' "$legacy" 2>/dev/null | tail -1 || echo 0)"
|
|
214
|
-
content_lines="${content_lines//[^0-9]/}"
|
|
215
|
-
: "${content_lines:=0}"
|
|
216
|
-
|
|
217
|
-
if [[ "$content_lines" -gt 0 ]]; then
|
|
218
|
-
# Extract history into a timestamped state file
|
|
219
|
-
local timestamp
|
|
220
|
-
timestamp="$(date +%Y%m%d%H%M%S)"
|
|
221
|
-
local migrated="$agents_dir/STATE_legacy-history_${timestamp}.md"
|
|
222
|
-
cp "$legacy" "$migrated"
|
|
223
|
-
info "Migrated legacy STATE.md history → $(basename "$migrated")"
|
|
224
|
-
fi
|
|
225
|
-
|
|
226
|
-
rm "$legacy"
|
|
227
|
-
info "Removed legacy $AGENTS_DIR/STATE.md (replaced by rules/state.md pattern)"
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
cmd_init() {
|
|
231
|
-
info "Initializing $AGENTS_DIR/ directory structure..."
|
|
232
|
-
|
|
233
|
-
mkdir -p "$PROJECT_ROOT/$AGENTS_DIR/rules"
|
|
234
|
-
mkdir -p "$PROJECT_ROOT/$AGENTS_DIR/skills"
|
|
235
|
-
mkdir -p "$PROJECT_ROOT/$AGENTS_DIR/workflows"
|
|
236
|
-
|
|
237
|
-
# Create rules/state.md (the state convention rule) if it doesn't exist
|
|
238
|
-
local state_rule="$PROJECT_ROOT/$AGENTS_DIR/rules/state.md"
|
|
239
|
-
if [[ ! -f "$state_rule" ]]; then
|
|
240
|
-
if [[ -f "$TEMPLATES_DIR/STATE_TEMPLATE.md" ]]; then
|
|
241
|
-
cp "$TEMPLATES_DIR/STATE_TEMPLATE.md" "$state_rule"
|
|
242
|
-
info "Created $AGENTS_DIR/rules/state.md from template"
|
|
243
|
-
else
|
|
244
|
-
# Inline fallback if template not found
|
|
245
|
-
cat > "$state_rule" <<'STATE_EOF'
|
|
246
|
-
---
|
|
247
|
-
trigger: always_on
|
|
248
|
-
---
|
|
249
|
-
|
|
250
|
-
# State
|
|
251
|
-
|
|
252
|
-
Track project progress, current objectives, and resumption context.
|
|
253
|
-
Update this file regularly so agents can pick up where they left off.
|
|
254
|
-
|
|
255
|
-
## Save location
|
|
256
|
-
|
|
257
|
-
../STATE_${CONTEXT_DESCRIPTION}_YYYYMMDDHHMMSS.md
|
|
258
|
-
|
|
259
|
-
A new file will be created each time during agent executions as a short summary of the current state, progress, blockers, and next steps.
|
|
260
|
-
|
|
261
|
-
## Format
|
|
262
|
-
|
|
263
|
-
### YYYYMMDDHHMMSS STATE: <objective>
|
|
264
|
-
|
|
265
|
-
Description of current state, progress, blockers, and next steps.
|
|
266
|
-
|
|
267
|
-
Be sure to indicate whats left and what done via - [ ] checkboxes in state file
|
|
268
|
-
STATE_EOF
|
|
269
|
-
info "Created $AGENTS_DIR/rules/state.md"
|
|
270
|
-
fi
|
|
271
|
-
else
|
|
272
|
-
warn "$AGENTS_DIR/rules/state.md already exists, skipping"
|
|
273
|
-
fi
|
|
274
|
-
|
|
275
|
-
# Migrate legacy STATE.md if it exists (from older sync-agents versions)
|
|
276
|
-
if [[ -f "$PROJECT_ROOT/$AGENTS_DIR/STATE.md" ]]; then
|
|
277
|
-
migrate_legacy_state "$PROJECT_ROOT/$AGENTS_DIR"
|
|
278
|
-
fi
|
|
279
|
-
|
|
280
|
-
# Create default config if it doesn't exist
|
|
281
|
-
if [[ ! -f "$PROJECT_ROOT/$AGENTS_DIR/config" ]]; then
|
|
282
|
-
cat > "$PROJECT_ROOT/$AGENTS_DIR/config" <<CONFIG_EOF
|
|
283
|
-
# sync-agents configuration
|
|
284
|
-
# Comma-separated list of sync targets (available: claude, windsurf, cursor, copilot)
|
|
285
|
-
# Override per-command with: sync-agents sync --targets claude,cursor
|
|
286
|
-
targets = claude,windsurf,cursor,copilot
|
|
287
|
-
CONFIG_EOF
|
|
288
|
-
info "Created $AGENTS_DIR/config"
|
|
289
|
-
else
|
|
290
|
-
warn "$AGENTS_DIR/config already exists, skipping"
|
|
291
|
-
fi
|
|
292
|
-
|
|
293
|
-
# Generate AGENTS.md if it doesn't exist
|
|
294
|
-
if [[ ! -f "$PROJECT_ROOT/$AGENTS_MD" ]]; then
|
|
295
|
-
generate_agents_md
|
|
296
|
-
info "Created $AGENTS_MD"
|
|
297
|
-
else
|
|
298
|
-
warn "$AGENTS_MD already exists, skipping (run 'sync-agents index' to regenerate)"
|
|
299
|
-
fi
|
|
300
|
-
|
|
301
|
-
# Add default .gitignore entries for agent tool directories
|
|
302
|
-
add_default_gitignore_entries
|
|
303
|
-
|
|
304
|
-
info "Initialization complete. Directory structure:"
|
|
305
|
-
print_tree "$PROJECT_ROOT/$AGENTS_DIR"
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
cmd_add() {
|
|
309
|
-
local type="${1:-}"
|
|
310
|
-
local name="${2:-}"
|
|
311
|
-
|
|
312
|
-
if [[ -z "$type" ]] || [[ -z "$name" ]]; then
|
|
313
|
-
error "Usage: sync-agents add <rule|skill|workflow> <name>"
|
|
314
|
-
exit 1
|
|
315
|
-
fi
|
|
316
|
-
|
|
317
|
-
case "$type" in
|
|
318
|
-
rule|rules) type="rules" ;;
|
|
319
|
-
skill|skills) type="skills" ;;
|
|
320
|
-
workflow|workflows) type="workflows" ;;
|
|
321
|
-
*)
|
|
322
|
-
error "Unknown type: $type. Must be one of: rule, skill, workflow"
|
|
323
|
-
exit 1
|
|
324
|
-
;;
|
|
325
|
-
esac
|
|
326
|
-
|
|
327
|
-
ensure_agents_dir
|
|
328
|
-
|
|
329
|
-
# Skills use directory layout: skills/name/SKILL.md
|
|
330
|
-
# Rules and workflows use flat files: rules/name.md, workflows/name.md
|
|
331
|
-
local filepath
|
|
332
|
-
if [[ "$type" == "skills" ]]; then
|
|
333
|
-
filepath="$PROJECT_ROOT/$AGENTS_DIR/$type/$name/SKILL.md"
|
|
334
|
-
else
|
|
335
|
-
filepath="$PROJECT_ROOT/$AGENTS_DIR/$type/$name.md"
|
|
336
|
-
fi
|
|
337
|
-
|
|
338
|
-
if [[ -f "$filepath" ]] && [[ "$FORCE" != "true" ]]; then
|
|
339
|
-
error "File already exists: $filepath (use --force to overwrite)"
|
|
340
|
-
exit 1
|
|
341
|
-
fi
|
|
342
|
-
|
|
343
|
-
# Use type-specific template (RULE_TEMPLATE, SKILL_TEMPLATE, WORKFLOW_TEMPLATE)
|
|
344
|
-
local template_name
|
|
345
|
-
case "$type" in
|
|
346
|
-
rules) template_name="RULE_TEMPLATE.md" ;;
|
|
347
|
-
skills) template_name="SKILL_TEMPLATE.md" ;;
|
|
348
|
-
workflows) template_name="WORKFLOW_TEMPLATE.md" ;;
|
|
349
|
-
*) template_name="RULE_TEMPLATE.md" ;;
|
|
350
|
-
esac
|
|
351
|
-
|
|
352
|
-
# Create parent directory for skills
|
|
353
|
-
mkdir -p "$(dirname "$filepath")"
|
|
354
|
-
|
|
355
|
-
if [[ -f "$TEMPLATES_DIR/$template_name" ]]; then
|
|
356
|
-
sed "s/\${NAME}/$name/g" "$TEMPLATES_DIR/$template_name" > "$filepath"
|
|
357
|
-
elif [[ -f "$TEMPLATES_DIR/RULE_TEMPLATE.md" ]]; then
|
|
358
|
-
# Fallback to rule template if type-specific template missing
|
|
359
|
-
sed "s/\${NAME}/$name/g" "$TEMPLATES_DIR/RULE_TEMPLATE.md" > "$filepath"
|
|
360
|
-
else
|
|
361
|
-
cat > "$filepath" <<TMPL_EOF
|
|
362
|
-
---
|
|
363
|
-
trigger: always_on
|
|
364
|
-
---
|
|
365
|
-
|
|
366
|
-
# $name
|
|
367
|
-
TMPL_EOF
|
|
368
|
-
fi
|
|
369
|
-
|
|
370
|
-
info "Created $type: $filepath"
|
|
371
|
-
|
|
372
|
-
# Regenerate index
|
|
373
|
-
generate_agents_md
|
|
374
|
-
info "Updated $AGENTS_MD index"
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
cmd_fix() {
|
|
378
|
-
ensure_agents_dir
|
|
379
|
-
|
|
380
|
-
# Parse fix-specific flags
|
|
381
|
-
local no_clobber="false"
|
|
382
|
-
local fix_args=()
|
|
383
|
-
for arg in "$@"; do
|
|
384
|
-
case "$arg" in
|
|
385
|
-
--no-clobber) no_clobber="true" ;;
|
|
386
|
-
*) fix_args+=("$arg") ;;
|
|
387
|
-
esac
|
|
388
|
-
done
|
|
389
|
-
|
|
390
|
-
local fix_type="${fix_args[0]:-all}"
|
|
391
|
-
local subdirs=()
|
|
392
|
-
|
|
393
|
-
case "$fix_type" in
|
|
394
|
-
skills|rules|workflows)
|
|
395
|
-
subdirs=("$fix_type")
|
|
396
|
-
;;
|
|
397
|
-
all)
|
|
398
|
-
subdirs=(skills rules workflows)
|
|
399
|
-
;;
|
|
400
|
-
*)
|
|
401
|
-
error "Unknown type: $fix_type (expected: skills, rules, workflows, or all)"
|
|
402
|
-
exit 1
|
|
403
|
-
;;
|
|
404
|
-
esac
|
|
405
|
-
|
|
406
|
-
local agents_abs
|
|
407
|
-
agents_abs="$(cd "$PROJECT_ROOT/$AGENTS_DIR" && pwd)"
|
|
408
|
-
local fixed=0
|
|
409
|
-
local skipped=0
|
|
410
|
-
local merged=0
|
|
411
|
-
|
|
412
|
-
# Helper: compare inodes of two directories (portable across macOS/Linux)
|
|
413
|
-
same_inode() {
|
|
414
|
-
local inode_a inode_b
|
|
415
|
-
if stat --version >/dev/null 2>&1; then
|
|
416
|
-
# GNU stat (Linux)
|
|
417
|
-
inode_a="$(stat -c '%i' "$1" 2>/dev/null)"
|
|
418
|
-
inode_b="$(stat -c '%i' "$2" 2>/dev/null)"
|
|
419
|
-
else
|
|
420
|
-
# BSD stat (macOS)
|
|
421
|
-
inode_a="$(stat -f '%i' "$1" 2>/dev/null)"
|
|
422
|
-
inode_b="$(stat -f '%i' "$2" 2>/dev/null)"
|
|
423
|
-
fi
|
|
424
|
-
[[ -n "$inode_a" ]] && [[ "$inode_a" == "$inode_b" ]]
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
for subdir in "${subdirs[@]}"; do
|
|
428
|
-
local legacy_dir="$PROJECT_ROOT/$subdir"
|
|
429
|
-
local agents_subdir="$agents_abs/$subdir"
|
|
430
|
-
|
|
431
|
-
# Skip if legacy dir doesn't exist or is already a symlink
|
|
432
|
-
if [[ ! -d "$legacy_dir" ]]; then
|
|
433
|
-
continue
|
|
434
|
-
fi
|
|
435
|
-
if [[ -L "$legacy_dir" ]]; then
|
|
436
|
-
info "$subdir/ is already a symlink — nothing to do."
|
|
437
|
-
continue
|
|
438
|
-
fi
|
|
439
|
-
|
|
440
|
-
# Detect same-inode (legacy dir IS .agents/subdir — e.g. hardlink or bind mount)
|
|
441
|
-
if [[ -d "$agents_subdir" ]] && same_inode "$legacy_dir" "$agents_subdir"; then
|
|
442
|
-
warn "$subdir/ and $AGENTS_DIR/$subdir/ are the same directory (same inode)."
|
|
443
|
-
warn "Replacing $subdir/ with a symlink to $AGENTS_DIR/$subdir/."
|
|
444
|
-
if [[ "$DRY_RUN" == "true" ]]; then
|
|
445
|
-
echo " would remove $subdir/ (same inode as $AGENTS_DIR/$subdir/)"
|
|
446
|
-
echo " would create symlink $subdir/ -> $AGENTS_DIR/$subdir"
|
|
447
|
-
else
|
|
448
|
-
rm -rf "$legacy_dir"
|
|
449
|
-
ln -s "$AGENTS_DIR/$subdir" "$legacy_dir"
|
|
450
|
-
info "Replaced $subdir/ with symlink -> $AGENTS_DIR/$subdir"
|
|
451
|
-
fi
|
|
452
|
-
fixed=$((fixed + 1))
|
|
453
|
-
continue
|
|
454
|
-
fi
|
|
455
|
-
|
|
456
|
-
info "Found legacy directory: $subdir/"
|
|
457
|
-
mkdir -p "$agents_subdir"
|
|
458
|
-
|
|
459
|
-
# Move each item from legacy dir into .agents/subdir
|
|
460
|
-
for item in "$legacy_dir"/*/; do
|
|
461
|
-
[[ -d "$item" ]] || continue
|
|
462
|
-
local name
|
|
463
|
-
name="$(basename "$item")"
|
|
464
|
-
|
|
465
|
-
if [[ -d "$agents_subdir/$name" ]]; then
|
|
466
|
-
if [[ "$no_clobber" == "true" ]]; then
|
|
467
|
-
warn "Skipping $subdir/$name — already exists in $AGENTS_DIR/$subdir/ (--no-clobber)"
|
|
468
|
-
skipped=$((skipped + 1))
|
|
469
|
-
continue
|
|
470
|
-
fi
|
|
471
|
-
# Merge: legacy content wins (overwrite into .agents/)
|
|
472
|
-
if [[ "$DRY_RUN" == "true" ]]; then
|
|
473
|
-
echo " would merge: $subdir/$name -> $AGENTS_DIR/$subdir/$name (overwrite)"
|
|
474
|
-
else
|
|
475
|
-
rm -rf "${agents_subdir:?}/${name:?}"
|
|
476
|
-
mv "$item" "$agents_subdir/$name"
|
|
477
|
-
info "Merged: $subdir/$name -> $AGENTS_DIR/$subdir/$name (overwrote existing)"
|
|
478
|
-
fi
|
|
479
|
-
merged=$((merged + 1))
|
|
480
|
-
fixed=$((fixed + 1))
|
|
481
|
-
continue
|
|
482
|
-
fi
|
|
483
|
-
|
|
484
|
-
if [[ "$DRY_RUN" == "true" ]]; then
|
|
485
|
-
echo " would move: $subdir/$name -> $AGENTS_DIR/$subdir/$name"
|
|
486
|
-
else
|
|
487
|
-
mv "$item" "$agents_subdir/$name"
|
|
488
|
-
info "Moved: $subdir/$name -> $AGENTS_DIR/$subdir/$name"
|
|
489
|
-
fi
|
|
490
|
-
fixed=$((fixed + 1))
|
|
491
|
-
done
|
|
492
|
-
|
|
493
|
-
# Also move any top-level files (e.g. loose .md rules)
|
|
494
|
-
for item in "$legacy_dir"/*; do
|
|
495
|
-
[[ -f "$item" ]] || continue
|
|
496
|
-
local name
|
|
497
|
-
name="$(basename "$item")"
|
|
498
|
-
|
|
499
|
-
if [[ -f "$agents_subdir/$name" ]]; then
|
|
500
|
-
if [[ "$no_clobber" == "true" ]]; then
|
|
501
|
-
warn "Skipping $subdir/$name — already exists in $AGENTS_DIR/$subdir/ (--no-clobber)"
|
|
502
|
-
skipped=$((skipped + 1))
|
|
503
|
-
continue
|
|
504
|
-
fi
|
|
505
|
-
# Merge: legacy content wins
|
|
506
|
-
if [[ "$DRY_RUN" == "true" ]]; then
|
|
507
|
-
echo " would merge: $subdir/$name -> $AGENTS_DIR/$subdir/$name (overwrite)"
|
|
508
|
-
else
|
|
509
|
-
mv "$item" "$agents_subdir/$name"
|
|
510
|
-
info "Merged: $subdir/$name -> $AGENTS_DIR/$subdir/$name (overwrote existing)"
|
|
511
|
-
fi
|
|
512
|
-
merged=$((merged + 1))
|
|
513
|
-
fixed=$((fixed + 1))
|
|
514
|
-
continue
|
|
515
|
-
fi
|
|
516
|
-
|
|
517
|
-
if [[ "$DRY_RUN" == "true" ]]; then
|
|
518
|
-
echo " would move: $subdir/$name -> $AGENTS_DIR/$subdir/$name"
|
|
519
|
-
else
|
|
520
|
-
mv "$item" "$agents_subdir/$name"
|
|
521
|
-
info "Moved: $subdir/$name -> $AGENTS_DIR/$subdir/$name"
|
|
522
|
-
fi
|
|
523
|
-
fixed=$((fixed + 1))
|
|
524
|
-
done
|
|
525
|
-
|
|
526
|
-
# Remove the now-empty legacy dir and replace with symlink
|
|
527
|
-
if [[ "$DRY_RUN" == "true" ]]; then
|
|
528
|
-
echo " would replace $subdir/ with symlink -> $AGENTS_DIR/$subdir"
|
|
529
|
-
else
|
|
530
|
-
# Check if dir is empty (only . and .. remain)
|
|
531
|
-
if [[ -z "$(ls -A "$legacy_dir" 2>/dev/null)" ]]; then
|
|
532
|
-
rmdir "$legacy_dir"
|
|
533
|
-
ln -s "$AGENTS_DIR/$subdir" "$legacy_dir"
|
|
534
|
-
info "Replaced $subdir/ with symlink -> $AGENTS_DIR/$subdir"
|
|
535
|
-
else
|
|
536
|
-
warn "$subdir/ is not empty after migration — skipping symlink replacement"
|
|
537
|
-
warn "Remaining items:"
|
|
538
|
-
find "$legacy_dir" -mindepth 1 -maxdepth 1 -exec basename {} \; | sed 's/^/ /'
|
|
539
|
-
fi
|
|
540
|
-
fi
|
|
541
|
-
done
|
|
542
|
-
|
|
543
|
-
# --- Phase 1b: Convert flat skill files to directory layout ---
|
|
544
|
-
# e.g. .agents/skills/foo.md -> .agents/skills/foo/SKILL.md
|
|
545
|
-
for subdir in "${subdirs[@]}"; do
|
|
546
|
-
[[ "$subdir" == "skills" ]] || continue
|
|
547
|
-
local skills_dir="$agents_abs/skills"
|
|
548
|
-
[[ -d "$skills_dir" ]] || continue
|
|
549
|
-
|
|
550
|
-
for flat_file in "$skills_dir"/*.md; do
|
|
551
|
-
[[ -f "$flat_file" ]] || continue
|
|
552
|
-
local name
|
|
553
|
-
name="$(basename "$flat_file" .md)"
|
|
554
|
-
local target_dir="$skills_dir/$name"
|
|
555
|
-
local target_file="$target_dir/SKILL.md"
|
|
556
|
-
|
|
557
|
-
if [[ -d "$target_dir" ]] && [[ -f "$target_file" ]]; then
|
|
558
|
-
if [[ "$no_clobber" == "true" ]]; then
|
|
559
|
-
warn "Skipping flat skill $name.md — $name/SKILL.md already exists (--no-clobber)"
|
|
560
|
-
skipped=$((skipped + 1))
|
|
561
|
-
continue
|
|
562
|
-
fi
|
|
563
|
-
# Flat file wins (same merge behavior as legacy migration)
|
|
564
|
-
if [[ "$DRY_RUN" == "true" ]]; then
|
|
565
|
-
echo " would convert: skills/$name.md -> skills/$name/SKILL.md (overwrite)"
|
|
566
|
-
else
|
|
567
|
-
mv "$flat_file" "$target_file"
|
|
568
|
-
info "Converted: skills/$name.md -> skills/$name/SKILL.md (overwrote existing)"
|
|
569
|
-
fi
|
|
570
|
-
merged=$((merged + 1))
|
|
571
|
-
fixed=$((fixed + 1))
|
|
572
|
-
continue
|
|
573
|
-
fi
|
|
574
|
-
|
|
575
|
-
if [[ "$DRY_RUN" == "true" ]]; then
|
|
576
|
-
echo " would convert: skills/$name.md -> skills/$name/SKILL.md"
|
|
577
|
-
else
|
|
578
|
-
mkdir -p "$target_dir"
|
|
579
|
-
mv "$flat_file" "$target_file"
|
|
580
|
-
info "Converted: skills/$name.md -> skills/$name/SKILL.md"
|
|
581
|
-
fi
|
|
582
|
-
fixed=$((fixed + 1))
|
|
583
|
-
done
|
|
584
|
-
done
|
|
585
|
-
|
|
586
|
-
# --- Phase 2: Repair broken/missing symlinks ---
|
|
587
|
-
local repaired=0
|
|
588
|
-
|
|
589
|
-
for target in "${ACTIVE_TARGETS[@]}"; do
|
|
590
|
-
local target_dir
|
|
591
|
-
target_dir="$(resolve_target_dir "$target" "$PROJECT_ROOT")"
|
|
592
|
-
local agents_rel
|
|
593
|
-
agents_rel="$(resolve_agents_rel "$target")"
|
|
594
|
-
|
|
595
|
-
for subdir in "${subdirs[@]}"; do
|
|
596
|
-
if [[ ! -d "$agents_abs/$subdir" ]]; then
|
|
597
|
-
continue
|
|
598
|
-
fi
|
|
599
|
-
local expected_link="$target_dir/$subdir"
|
|
600
|
-
local expected_source="$agents_rel/$subdir"
|
|
601
|
-
|
|
602
|
-
if [[ -L "$expected_link" ]]; then
|
|
603
|
-
local current_target
|
|
604
|
-
current_target="$(readlink "$expected_link")"
|
|
605
|
-
if [[ "$current_target" == "$expected_source" ]]; then
|
|
606
|
-
continue # Already correct
|
|
607
|
-
fi
|
|
608
|
-
# Symlink exists but points to wrong target
|
|
609
|
-
if [[ "$DRY_RUN" == "true" ]]; then
|
|
610
|
-
echo " would relink: $expected_link -> $expected_source (was $current_target)"
|
|
611
|
-
else
|
|
612
|
-
rm "$expected_link"
|
|
613
|
-
create_symlink "$expected_source" "$expected_link" "false"
|
|
614
|
-
info "Repaired: $expected_link -> $expected_source (was $current_target)"
|
|
615
|
-
fi
|
|
616
|
-
repaired=$((repaired + 1))
|
|
617
|
-
elif [[ -e "$expected_link" ]]; then
|
|
618
|
-
# Something exists but isn't a symlink — skip unless --force
|
|
619
|
-
if [[ "$FORCE" == "true" ]]; then
|
|
620
|
-
if [[ "$DRY_RUN" == "true" ]]; then
|
|
621
|
-
echo " would replace: $expected_link with symlink -> $expected_source"
|
|
622
|
-
else
|
|
623
|
-
rm -rf "$expected_link"
|
|
624
|
-
create_symlink "$expected_source" "$expected_link" "false"
|
|
625
|
-
info "Repaired: replaced $expected_link with symlink -> $expected_source"
|
|
626
|
-
fi
|
|
627
|
-
repaired=$((repaired + 1))
|
|
628
|
-
else
|
|
629
|
-
warn "$expected_link exists but is not a symlink (use --force to replace)"
|
|
630
|
-
fi
|
|
631
|
-
else
|
|
632
|
-
# Missing entirely
|
|
633
|
-
if [[ "$DRY_RUN" == "true" ]]; then
|
|
634
|
-
echo " would create: $expected_link -> $expected_source"
|
|
635
|
-
else
|
|
636
|
-
create_symlink "$expected_source" "$expected_link" "false"
|
|
637
|
-
fi
|
|
638
|
-
repaired=$((repaired + 1))
|
|
639
|
-
fi
|
|
640
|
-
done
|
|
641
|
-
done
|
|
642
|
-
|
|
643
|
-
# Repair CLAUDE.md symlink
|
|
644
|
-
if [[ -f "$PROJECT_ROOT/$AGENTS_MD" ]]; then
|
|
645
|
-
if [[ -L "$PROJECT_ROOT/CLAUDE.md" ]]; then
|
|
646
|
-
local current_target
|
|
647
|
-
current_target="$(readlink "$PROJECT_ROOT/CLAUDE.md")"
|
|
648
|
-
if [[ "$current_target" != "$AGENTS_MD" ]]; then
|
|
649
|
-
if [[ "$DRY_RUN" == "true" ]]; then
|
|
650
|
-
echo " would relink: CLAUDE.md -> $AGENTS_MD (was $current_target)"
|
|
651
|
-
else
|
|
652
|
-
rm "$PROJECT_ROOT/CLAUDE.md"
|
|
653
|
-
create_symlink "$AGENTS_MD" "$PROJECT_ROOT/CLAUDE.md" "false"
|
|
654
|
-
fi
|
|
655
|
-
repaired=$((repaired + 1))
|
|
656
|
-
fi
|
|
657
|
-
elif [[ ! -e "$PROJECT_ROOT/CLAUDE.md" ]]; then
|
|
658
|
-
if [[ "$DRY_RUN" == "true" ]]; then
|
|
659
|
-
echo " would create: CLAUDE.md -> $AGENTS_MD"
|
|
660
|
-
else
|
|
661
|
-
create_symlink "$AGENTS_MD" "$PROJECT_ROOT/CLAUDE.md" "false"
|
|
662
|
-
fi
|
|
663
|
-
repaired=$((repaired + 1))
|
|
664
|
-
fi
|
|
665
|
-
fi
|
|
666
|
-
|
|
667
|
-
# --- Phase 3: Migrate legacy STATE.md to per-file state pattern ---
|
|
668
|
-
local state_migrated=0
|
|
669
|
-
if [[ -f "$agents_abs/STATE.md" ]]; then
|
|
670
|
-
# Ensure the state rule exists first
|
|
671
|
-
if [[ ! -f "$agents_abs/rules/state.md" ]]; then
|
|
672
|
-
if [[ -f "$TEMPLATES_DIR/STATE_TEMPLATE.md" ]]; then
|
|
673
|
-
if [[ "$DRY_RUN" == "true" ]]; then
|
|
674
|
-
echo " would create: $AGENTS_DIR/rules/state.md from template"
|
|
675
|
-
else
|
|
676
|
-
mkdir -p "$agents_abs/rules"
|
|
677
|
-
cp "$TEMPLATES_DIR/STATE_TEMPLATE.md" "$agents_abs/rules/state.md"
|
|
678
|
-
info "Created $AGENTS_DIR/rules/state.md (state convention rule)"
|
|
679
|
-
fi
|
|
680
|
-
fi
|
|
681
|
-
fi
|
|
682
|
-
|
|
683
|
-
if [[ "$DRY_RUN" == "true" ]]; then
|
|
684
|
-
echo " would migrate: $AGENTS_DIR/STATE.md → per-file state pattern"
|
|
685
|
-
else
|
|
686
|
-
migrate_legacy_state "$agents_abs"
|
|
687
|
-
fi
|
|
688
|
-
state_migrated=1
|
|
689
|
-
fi
|
|
690
|
-
|
|
691
|
-
# Summary
|
|
692
|
-
if [[ "$fixed" -eq 0 ]] && [[ "$skipped" -eq 0 ]] && [[ "$repaired" -eq 0 ]] && [[ "$state_migrated" -eq 0 ]]; then
|
|
693
|
-
info "Nothing to fix — all directories and symlinks are correct."
|
|
694
|
-
else
|
|
695
|
-
if [[ "$fixed" -gt 0 ]]; then info "Fixed $fixed item(s)."; fi
|
|
696
|
-
if [[ "$merged" -gt 0 ]]; then info "Merged $merged item(s) (legacy overwrote existing)."; fi
|
|
697
|
-
if [[ "$skipped" -gt 0 ]]; then warn "Skipped $skipped item(s) (use without --no-clobber to merge)."; fi
|
|
698
|
-
if [[ "$repaired" -gt 0 ]]; then info "Repaired $repaired symlink(s)."; fi
|
|
699
|
-
if [[ "$state_migrated" -gt 0 ]]; then info "Migrated legacy STATE.md to per-file state pattern."; fi
|
|
700
|
-
if [[ "$fixed" -gt 0 ]]; then info "Run 'sync-agents sync' to update agent target symlinks."; fi
|
|
701
|
-
fi
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
cmd_sync() {
|
|
705
|
-
ensure_agents_dir
|
|
706
|
-
|
|
707
|
-
local agents_abs
|
|
708
|
-
agents_abs="$(cd "$PROJECT_ROOT/$AGENTS_DIR" && pwd)"
|
|
709
|
-
|
|
710
|
-
info "Syncing $AGENTS_DIR/ to agent directories..."
|
|
711
|
-
|
|
712
|
-
for target in "${ACTIVE_TARGETS[@]}"; do
|
|
713
|
-
local target_dir
|
|
714
|
-
target_dir="$(resolve_target_dir "$target" "$PROJECT_ROOT")"
|
|
715
|
-
local agents_rel
|
|
716
|
-
agents_rel="$(resolve_agents_rel "$target")"
|
|
717
|
-
info "Syncing to ${target_dir#"$PROJECT_ROOT"/}/"
|
|
718
|
-
|
|
719
|
-
# Sync subdirectories: rules, skills, workflows
|
|
720
|
-
for subdir in rules skills workflows; do
|
|
721
|
-
if [[ -d "$agents_abs/$subdir" ]]; then
|
|
722
|
-
local source_rel="$agents_rel/$subdir"
|
|
723
|
-
create_symlink "$source_rel" "$target_dir/$subdir" "$DRY_RUN"
|
|
724
|
-
fi
|
|
725
|
-
done
|
|
726
|
-
done
|
|
727
|
-
|
|
728
|
-
# Symlink AGENTS.md -> CLAUDE.md
|
|
729
|
-
if [[ -f "$PROJECT_ROOT/$AGENTS_MD" ]]; then
|
|
730
|
-
create_symlink "$AGENTS_MD" "$PROJECT_ROOT/CLAUDE.md" "$DRY_RUN"
|
|
731
|
-
fi
|
|
732
|
-
|
|
733
|
-
# Update .gitignore with synced symlink entries
|
|
734
|
-
update_gitignore
|
|
735
|
-
|
|
736
|
-
info "Sync complete."
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
# --------------------------------------------------------------------------
|
|
740
|
-
# .gitignore management
|
|
741
|
-
# --------------------------------------------------------------------------
|
|
742
|
-
|
|
743
|
-
# Add default .gitignore entries for agent tool directories (called during init)
|
|
744
|
-
add_default_gitignore_entries() {
|
|
745
|
-
local gitignore="$PROJECT_ROOT/.gitignore"
|
|
746
|
-
|
|
747
|
-
# Create .gitignore if it doesn't exist
|
|
748
|
-
if [[ ! -f "$gitignore" ]]; then
|
|
749
|
-
touch "$gitignore"
|
|
750
|
-
info "Created .gitignore"
|
|
751
|
-
fi
|
|
752
|
-
|
|
753
|
-
# Check if .DS_Store is already present (case-insensitive check)
|
|
754
|
-
if ! grep -qiE "^\.DS_Store$" "$gitignore" 2>/dev/null; then
|
|
755
|
-
# Add .DS_Store if not present
|
|
756
|
-
if [[ -s "$gitignore" ]] && ! tail -c1 "$gitignore" | grep -q '^$'; then
|
|
757
|
-
echo "" >> "$gitignore"
|
|
758
|
-
fi
|
|
759
|
-
echo ".DS_Store" >> "$gitignore"
|
|
760
|
-
info "Added .DS_Store to .gitignore"
|
|
761
|
-
fi
|
|
762
|
-
|
|
763
|
-
# Define default entries (tool artifacts, not symlinks)
|
|
764
|
-
# Using pattern: ignore everything in dir, except specific files we want to track
|
|
765
|
-
local marker="# sync-agents — ignore tool artifacts, keep symlinks"
|
|
766
|
-
|
|
767
|
-
# Check if sync-agents section already exists
|
|
768
|
-
if grep -qF "$marker" "$gitignore"; then
|
|
769
|
-
# Section exists - check if we need to add any missing entries
|
|
770
|
-
local needs_update=false
|
|
771
|
-
|
|
772
|
-
# Check for each pattern
|
|
773
|
-
if ! grep -qF ".cursor/*" "$gitignore"; then needs_update=true; fi
|
|
774
|
-
if ! grep -qF "!.cursor/rules" "$gitignore"; then needs_update=true; fi
|
|
775
|
-
if ! grep -qF ".codex/*" "$gitignore"; then needs_update=true; fi
|
|
776
|
-
if ! grep -qF "!.codex/instructions.md" "$gitignore"; then needs_update=true; fi
|
|
777
|
-
if ! grep -qF ".github/copilot/*" "$gitignore"; then needs_update=true; fi
|
|
778
|
-
if ! grep -qF "!.github/copilot/instructions.md" "$gitignore"; then needs_update=true; fi
|
|
779
|
-
|
|
780
|
-
if [[ "$needs_update" == "true" ]]; then
|
|
781
|
-
# Rebuild section by reading the file, preserving everything else
|
|
782
|
-
local tmp
|
|
783
|
-
tmp="$(mktemp)"
|
|
784
|
-
local in_section=false
|
|
785
|
-
|
|
786
|
-
while IFS= read -r line; do
|
|
787
|
-
if [[ "$line" == "$marker" ]]; then
|
|
788
|
-
in_section=true
|
|
789
|
-
# Output the marker
|
|
790
|
-
{
|
|
791
|
-
echo "$line"
|
|
792
|
-
echo ".cursor/*"
|
|
793
|
-
echo "!.cursor/rules"
|
|
794
|
-
echo ".codex/*"
|
|
795
|
-
echo "!.codex/instructions.md"
|
|
796
|
-
echo ".github/copilot/*"
|
|
797
|
-
echo "!.github/copilot/instructions.md"
|
|
798
|
-
} >> "$tmp"
|
|
799
|
-
continue
|
|
800
|
-
fi
|
|
801
|
-
|
|
802
|
-
# Skip old entries in the sync-agents section (until we hit empty line or new section)
|
|
803
|
-
if [[ "$in_section" == "true" ]]; then
|
|
804
|
-
if [[ -z "$line" ]] || [[ "$line" == "#"* ]]; then
|
|
805
|
-
in_section=false
|
|
806
|
-
echo "$line" >> "$tmp"
|
|
807
|
-
fi
|
|
808
|
-
# Skip old entry lines (they're replaced above)
|
|
809
|
-
continue
|
|
810
|
-
fi
|
|
811
|
-
|
|
812
|
-
echo "$line" >> "$tmp"
|
|
813
|
-
done < "$gitignore"
|
|
814
|
-
|
|
815
|
-
mv "$tmp" "$gitignore"
|
|
816
|
-
info "Updated sync-agents section in .gitignore"
|
|
817
|
-
fi
|
|
818
|
-
else
|
|
819
|
-
# Section doesn't exist, add entire block
|
|
820
|
-
# Add separator if file is non-empty
|
|
821
|
-
if [[ -s "$gitignore" ]] && ! tail -c1 "$gitignore" | grep -q '^$'; then
|
|
822
|
-
echo "" >> "$gitignore"
|
|
823
|
-
fi
|
|
824
|
-
|
|
825
|
-
# Add all entries
|
|
826
|
-
{
|
|
827
|
-
echo "$marker"
|
|
828
|
-
echo ".cursor/*"
|
|
829
|
-
echo "!.cursor/rules"
|
|
830
|
-
echo ".codex/*"
|
|
831
|
-
echo "!.codex/instructions.md"
|
|
832
|
-
echo ".github/copilot/*"
|
|
833
|
-
echo "!.github/copilot/instructions.md"
|
|
834
|
-
} >> "$gitignore"
|
|
835
|
-
|
|
836
|
-
info "Added sync-agents section to .gitignore with 7 entries"
|
|
837
|
-
fi
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
update_gitignore() {
|
|
841
|
-
local gitignore="$PROJECT_ROOT/.gitignore"
|
|
842
|
-
|
|
843
|
-
# Build list of entries that should be ignored (synced symlinks)
|
|
844
|
-
local entries=()
|
|
845
|
-
for target in "${ACTIVE_TARGETS[@]}"; do
|
|
846
|
-
local target_dir
|
|
847
|
-
target_dir="$(resolve_target_dir "$target" "$PROJECT_ROOT")"
|
|
848
|
-
local rel_path="${target_dir#"$PROJECT_ROOT"/}/"
|
|
849
|
-
entries+=("$rel_path")
|
|
850
|
-
done
|
|
851
|
-
entries+=("CLAUDE.md")
|
|
852
|
-
|
|
853
|
-
if [[ "$DRY_RUN" == "true" ]]; then
|
|
854
|
-
for entry in "${entries[@]}"; do
|
|
855
|
-
if [[ ! -f "$gitignore" ]] || ! grep -qxF "$entry" "$gitignore"; then
|
|
856
|
-
echo " would add to .gitignore: $entry"
|
|
857
|
-
fi
|
|
858
|
-
done
|
|
859
|
-
return 0
|
|
860
|
-
fi
|
|
861
|
-
|
|
862
|
-
# Create .gitignore if it doesn't exist
|
|
863
|
-
[[ -f "$gitignore" ]] || touch "$gitignore"
|
|
864
|
-
|
|
865
|
-
local added=0
|
|
866
|
-
for entry in "${entries[@]}"; do
|
|
867
|
-
if ! grep -qxF "$entry" "$gitignore"; then
|
|
868
|
-
# Add sync-agents header on first addition
|
|
869
|
-
if [[ "$added" -eq 0 ]]; then
|
|
870
|
-
# Check if header already exists
|
|
871
|
-
if ! grep -qF "# sync-agents" "$gitignore"; then
|
|
872
|
-
# Add a blank line separator if file is non-empty
|
|
873
|
-
if [[ -s "$gitignore" ]]; then
|
|
874
|
-
echo "" >> "$gitignore"
|
|
875
|
-
fi
|
|
876
|
-
echo "# sync-agents (generated symlinks)" >> "$gitignore"
|
|
877
|
-
fi
|
|
878
|
-
fi
|
|
879
|
-
echo "$entry" >> "$gitignore"
|
|
880
|
-
added=$((added + 1))
|
|
881
|
-
fi
|
|
882
|
-
done
|
|
883
|
-
|
|
884
|
-
if [[ "$added" -gt 0 ]]; then
|
|
885
|
-
info "Added $added entries to .gitignore"
|
|
886
|
-
fi
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
cmd_status() {
|
|
890
|
-
echo -e "${BOLD}sync-agents${RESET} v${VERSION}"
|
|
891
|
-
echo ""
|
|
892
|
-
|
|
893
|
-
# Check .agents/ directory
|
|
894
|
-
if [[ -d "$PROJECT_ROOT/$AGENTS_DIR" ]]; then
|
|
895
|
-
echo -e "${GREEN}[ok]${RESET} $AGENTS_DIR/ exists"
|
|
896
|
-
print_tree "$PROJECT_ROOT/$AGENTS_DIR"
|
|
897
|
-
else
|
|
898
|
-
echo -e "${RED}[missing]${RESET} $AGENTS_DIR/ not found"
|
|
899
|
-
fi
|
|
900
|
-
|
|
901
|
-
echo ""
|
|
902
|
-
|
|
903
|
-
# Check AGENTS.md
|
|
904
|
-
if [[ -f "$PROJECT_ROOT/$AGENTS_MD" ]]; then
|
|
905
|
-
echo -e "${GREEN}[ok]${RESET} $AGENTS_MD exists"
|
|
906
|
-
else
|
|
907
|
-
echo -e "${RED}[missing]${RESET} $AGENTS_MD not found"
|
|
908
|
-
fi
|
|
909
|
-
|
|
910
|
-
# Check CLAUDE.md symlink
|
|
911
|
-
if [[ -L "$PROJECT_ROOT/CLAUDE.md" ]]; then
|
|
912
|
-
local link_target
|
|
913
|
-
link_target="$(readlink "$PROJECT_ROOT/CLAUDE.md")"
|
|
914
|
-
echo -e "${GREEN}[ok]${RESET} CLAUDE.md -> $link_target"
|
|
915
|
-
elif [[ -f "$PROJECT_ROOT/CLAUDE.md" ]]; then
|
|
916
|
-
echo -e "${YELLOW}[warn]${RESET} CLAUDE.md exists but is not a symlink"
|
|
917
|
-
else
|
|
918
|
-
echo -e "${RED}[missing]${RESET} CLAUDE.md not found"
|
|
919
|
-
fi
|
|
920
|
-
|
|
921
|
-
echo ""
|
|
922
|
-
|
|
923
|
-
# Check each target
|
|
924
|
-
for target in "${TARGETS[@]}"; do
|
|
925
|
-
local target_dir
|
|
926
|
-
target_dir="$(resolve_target_dir "$target" "$PROJECT_ROOT")"
|
|
927
|
-
local display_dir="${target_dir#"$PROJECT_ROOT"/}"
|
|
928
|
-
if [[ -d "$target_dir" ]] || [[ -L "$target_dir/rules" ]]; then
|
|
929
|
-
echo -e "${CYAN}${display_dir}/${RESET}"
|
|
930
|
-
for subdir in rules skills workflows; do
|
|
931
|
-
if [[ -L "$target_dir/$subdir" ]]; then
|
|
932
|
-
local link_target
|
|
933
|
-
link_target="$(readlink "$target_dir/$subdir")"
|
|
934
|
-
echo -e " ${GREEN}[synced]${RESET} $subdir -> $link_target"
|
|
935
|
-
elif [[ -d "$target_dir/$subdir" ]]; then
|
|
936
|
-
echo -e " ${YELLOW}[local]${RESET} $subdir (not symlinked)"
|
|
937
|
-
else
|
|
938
|
-
echo -e " ${RED}[missing]${RESET} $subdir"
|
|
939
|
-
fi
|
|
940
|
-
done
|
|
941
|
-
else
|
|
942
|
-
echo -e "${RED}[not synced]${RESET} ${display_dir}/"
|
|
943
|
-
fi
|
|
944
|
-
done
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
cmd_index() {
|
|
948
|
-
ensure_agents_dir
|
|
949
|
-
generate_agents_md
|
|
950
|
-
info "Regenerated $AGENTS_MD"
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
cmd_watch() {
|
|
954
|
-
ensure_agents_dir
|
|
955
|
-
|
|
956
|
-
local watch_dir="$PROJECT_ROOT/$AGENTS_DIR"
|
|
957
|
-
|
|
958
|
-
if command -v fswatch >/dev/null 2>&1; then
|
|
959
|
-
info "Watching $AGENTS_DIR/ for changes... (Ctrl+C to stop)"
|
|
960
|
-
cmd_index
|
|
961
|
-
fswatch -o "$watch_dir" | while read -r _; do
|
|
962
|
-
info "Change detected, regenerating index..."
|
|
963
|
-
cmd_index
|
|
964
|
-
done
|
|
965
|
-
elif command -v inotifywait >/dev/null 2>&1; then
|
|
966
|
-
info "Watching $AGENTS_DIR/ for changes... (Ctrl+C to stop)"
|
|
967
|
-
cmd_index
|
|
968
|
-
inotifywait -m -r -e modify,create,delete,move --format '%w%f' "$watch_dir" | while read -r _; do
|
|
969
|
-
info "Change detected, regenerating index..."
|
|
970
|
-
cmd_index
|
|
971
|
-
done
|
|
972
|
-
else
|
|
973
|
-
error "Neither fswatch (macOS) nor inotifywait (Linux) found."
|
|
974
|
-
error "Install with: brew install fswatch OR apt install inotify-tools"
|
|
975
|
-
exit 1
|
|
976
|
-
fi
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
cmd_import() {
|
|
980
|
-
local url="${1:-}"
|
|
981
|
-
if [[ -z "$url" ]]; then
|
|
982
|
-
error "Usage: sync-agents import <url>"
|
|
983
|
-
exit 1
|
|
984
|
-
fi
|
|
985
|
-
|
|
986
|
-
ensure_agents_dir
|
|
987
|
-
|
|
988
|
-
local filename
|
|
989
|
-
filename="$(basename "$url")"
|
|
990
|
-
if [[ "$filename" != *.md ]]; then
|
|
991
|
-
filename="${filename}.md"
|
|
992
|
-
fi
|
|
993
|
-
|
|
994
|
-
# Auto-detect type from URL path
|
|
995
|
-
local type=""
|
|
996
|
-
case "$url" in
|
|
997
|
-
*/rules/*) type="rules" ;;
|
|
998
|
-
*/skills/*) type="skills" ;;
|
|
999
|
-
*/workflows/*) type="workflows" ;;
|
|
1000
|
-
esac
|
|
1001
|
-
|
|
1002
|
-
if [[ -z "$type" ]]; then
|
|
1003
|
-
echo "Could not detect type from URL. Choose:"
|
|
1004
|
-
echo " 1) rule"
|
|
1005
|
-
echo " 2) skill"
|
|
1006
|
-
echo " 3) workflow"
|
|
1007
|
-
read -rp "Selection (1-3): " choice
|
|
1008
|
-
case "$choice" in
|
|
1009
|
-
1) type="rules" ;;
|
|
1010
|
-
2) type="skills" ;;
|
|
1011
|
-
3) type="workflows" ;;
|
|
1012
|
-
*) error "Invalid selection"; exit 1 ;;
|
|
1013
|
-
esac
|
|
1014
|
-
fi
|
|
1015
|
-
|
|
1016
|
-
mkdir -p "$PROJECT_ROOT/$AGENTS_DIR/$type"
|
|
1017
|
-
local dest="$PROJECT_ROOT/$AGENTS_DIR/$type/$filename"
|
|
1018
|
-
|
|
1019
|
-
info "Importing $url → $AGENTS_DIR/$type/$filename"
|
|
1020
|
-
|
|
1021
|
-
if ! curl -fsSL "$url" -o "$dest"; then
|
|
1022
|
-
error "Failed to download: $url"
|
|
1023
|
-
exit 1
|
|
1024
|
-
fi
|
|
1025
|
-
|
|
1026
|
-
info "Imported successfully."
|
|
1027
|
-
cmd_index
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
cmd_hook() {
|
|
1031
|
-
if [[ ! -d "$PROJECT_ROOT/.git" ]]; then
|
|
1032
|
-
error "Not a git repository (no .git/ found)."
|
|
1033
|
-
exit 1
|
|
1034
|
-
fi
|
|
1035
|
-
|
|
1036
|
-
local hook_dir="$PROJECT_ROOT/.git/hooks"
|
|
1037
|
-
local hook_file="$hook_dir/pre-commit"
|
|
1038
|
-
mkdir -p "$hook_dir"
|
|
1039
|
-
|
|
1040
|
-
local marker="sync-agents start"
|
|
1041
|
-
|
|
1042
|
-
if [[ -f "$hook_file" ]] && grep -q "$marker" "$hook_file"; then
|
|
1043
|
-
info "Git hook already installed in $hook_file"
|
|
1044
|
-
return 0
|
|
1045
|
-
fi
|
|
1046
|
-
|
|
1047
|
-
local hook_block
|
|
1048
|
-
hook_block="$(cat <<'HOOK'
|
|
1049
|
-
|
|
1050
|
-
# --- sync-agents start ---
|
|
1051
|
-
if command -v sync-agents >/dev/null 2>&1; then
|
|
1052
|
-
sync-agents sync 2>/dev/null
|
|
1053
|
-
sync-agents index 2>/dev/null
|
|
1054
|
-
git add AGENTS.md CLAUDE.md .claude/ .windsurf/ .cursor/ .github/copilot/ 2>/dev/null || true
|
|
1055
|
-
fi
|
|
1056
|
-
# --- sync-agents end ---
|
|
1057
|
-
HOOK
|
|
1058
|
-
)"
|
|
1059
|
-
|
|
1060
|
-
if [[ -f "$hook_file" ]]; then
|
|
1061
|
-
echo "$hook_block" >> "$hook_file"
|
|
1062
|
-
info "Appended sync-agents hook to existing $hook_file"
|
|
1063
|
-
else
|
|
1064
|
-
printf '#!/bin/sh\n%s\n' "$hook_block" > "$hook_file"
|
|
1065
|
-
chmod +x "$hook_file"
|
|
1066
|
-
info "Created git hook: $hook_file"
|
|
1067
|
-
fi
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
cmd_clean() {
|
|
1071
|
-
info "Removing synced symlinks..."
|
|
1072
|
-
|
|
1073
|
-
for target in "${ACTIVE_TARGETS[@]}"; do
|
|
1074
|
-
local target_dir
|
|
1075
|
-
target_dir="$(resolve_target_dir "$target" "$PROJECT_ROOT")"
|
|
1076
|
-
local display_dir="${target_dir#"$PROJECT_ROOT"/}"
|
|
1077
|
-
for subdir in rules skills workflows; do
|
|
1078
|
-
if [[ -L "$target_dir/$subdir" ]]; then
|
|
1079
|
-
rm "$target_dir/$subdir"
|
|
1080
|
-
info "Removed: ${display_dir}/$subdir"
|
|
1081
|
-
fi
|
|
1082
|
-
done
|
|
1083
|
-
|
|
1084
|
-
# Remove target dir if empty
|
|
1085
|
-
if [[ -d "$target_dir" ]] && [[ -z "$(ls -A "$target_dir" 2>/dev/null)" ]]; then
|
|
1086
|
-
rmdir "$target_dir"
|
|
1087
|
-
info "Removed empty directory: ${display_dir}/"
|
|
1088
|
-
fi
|
|
1089
|
-
done
|
|
1090
|
-
|
|
1091
|
-
# Remove CLAUDE.md symlink
|
|
1092
|
-
if [[ -L "$PROJECT_ROOT/CLAUDE.md" ]]; then
|
|
1093
|
-
rm "$PROJECT_ROOT/CLAUDE.md"
|
|
1094
|
-
info "Removed: CLAUDE.md symlink"
|
|
1095
|
-
fi
|
|
1096
|
-
|
|
1097
|
-
info "Clean complete."
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
# --------------------------------------------------------------------------
|
|
1101
|
-
# Inherit
|
|
1102
|
-
# --------------------------------------------------------------------------
|
|
1103
|
-
|
|
1104
|
-
cmd_inherit() {
|
|
1105
|
-
local action="${1:-}"
|
|
1106
|
-
|
|
1107
|
-
# --list: show current inherits
|
|
1108
|
-
if [[ "$action" == "--list" ]]; then
|
|
1109
|
-
if [[ ! -f "$PROJECT_ROOT/$AGENTS_MD" ]]; then
|
|
1110
|
-
info "No AGENTS.md found."
|
|
1111
|
-
return 0
|
|
1112
|
-
fi
|
|
1113
|
-
local in_section="false"
|
|
1114
|
-
while IFS= read -r line; do
|
|
1115
|
-
if [[ "$line" =~ ^##[[:space:]]+Inherits ]]; then
|
|
1116
|
-
in_section="true"
|
|
1117
|
-
continue
|
|
1118
|
-
fi
|
|
1119
|
-
if [[ "$in_section" == "true" ]] && [[ "$line" =~ ^## ]]; then
|
|
1120
|
-
break
|
|
1121
|
-
fi
|
|
1122
|
-
if [[ "$in_section" == "true" ]] && [[ "$line" =~ ^-[[:space:]]+\[ ]]; then
|
|
1123
|
-
echo "$line"
|
|
1124
|
-
fi
|
|
1125
|
-
done < "$PROJECT_ROOT/$AGENTS_MD"
|
|
1126
|
-
return 0
|
|
1127
|
-
fi
|
|
1128
|
-
|
|
1129
|
-
# --remove <label>: remove an inherit entry
|
|
1130
|
-
if [[ "$action" == "--remove" ]]; then
|
|
1131
|
-
local label="${2:-}"
|
|
1132
|
-
if [[ -z "$label" ]]; then
|
|
1133
|
-
error "Usage: sync-agents inherit --remove <label>"
|
|
1134
|
-
exit 1
|
|
1135
|
-
fi
|
|
1136
|
-
if [[ ! -f "$PROJECT_ROOT/$AGENTS_MD" ]]; then
|
|
1137
|
-
error "No AGENTS.md found."
|
|
1138
|
-
exit 1
|
|
1139
|
-
fi
|
|
1140
|
-
# Remove the line matching [label](...) from the Inherits section
|
|
1141
|
-
local tmp
|
|
1142
|
-
tmp="$(mktemp)"
|
|
1143
|
-
local in_section="false"
|
|
1144
|
-
local removed="false"
|
|
1145
|
-
while IFS= read -r line; do
|
|
1146
|
-
if [[ "$line" =~ ^##[[:space:]]+Inherits ]]; then
|
|
1147
|
-
in_section="true"
|
|
1148
|
-
echo "$line" >> "$tmp"
|
|
1149
|
-
continue
|
|
1150
|
-
fi
|
|
1151
|
-
if [[ "$in_section" == "true" ]] && [[ "$line" =~ ^## ]]; then
|
|
1152
|
-
in_section="false"
|
|
1153
|
-
fi
|
|
1154
|
-
if [[ "$in_section" == "true" ]] && [[ "$line" == *"[$label]("* ]]; then
|
|
1155
|
-
removed="true"
|
|
1156
|
-
continue
|
|
1157
|
-
fi
|
|
1158
|
-
echo "$line" >> "$tmp"
|
|
1159
|
-
done < "$PROJECT_ROOT/$AGENTS_MD"
|
|
1160
|
-
mv "$tmp" "$PROJECT_ROOT/$AGENTS_MD"
|
|
1161
|
-
if [[ "$removed" == "true" ]]; then
|
|
1162
|
-
info "Removed inherit: $label"
|
|
1163
|
-
else
|
|
1164
|
-
warn "No inherit found with label: $label"
|
|
1165
|
-
fi
|
|
1166
|
-
return 0
|
|
1167
|
-
fi
|
|
1168
|
-
|
|
1169
|
-
# Default: add <label> <path>
|
|
1170
|
-
local label="$action"
|
|
1171
|
-
local path="${2:-}"
|
|
1172
|
-
|
|
1173
|
-
if [[ -z "$label" ]] || [[ -z "$path" ]]; then
|
|
1174
|
-
error "Usage: sync-agents inherit <label> <path>"
|
|
1175
|
-
error " sync-agents inherit --list"
|
|
1176
|
-
error " sync-agents inherit --remove <label>"
|
|
1177
|
-
exit 1
|
|
1178
|
-
fi
|
|
1179
|
-
|
|
1180
|
-
# Validate the path exists (resolve relative to PROJECT_ROOT)
|
|
1181
|
-
local resolved_path
|
|
1182
|
-
if [[ "$path" == /* ]] || [[ "$path" == ~* ]]; then
|
|
1183
|
-
resolved_path="${path/#\~/$HOME}"
|
|
1184
|
-
else
|
|
1185
|
-
resolved_path="$PROJECT_ROOT/$path"
|
|
1186
|
-
fi
|
|
1187
|
-
|
|
1188
|
-
if [[ ! -f "$resolved_path" ]] && [[ ! -d "$resolved_path" ]]; then
|
|
1189
|
-
warn "Path does not exist: $path (link will be added anyway)"
|
|
1190
|
-
fi
|
|
1191
|
-
|
|
1192
|
-
# Check if AGENTS.md exists
|
|
1193
|
-
if [[ ! -f "$PROJECT_ROOT/$AGENTS_MD" ]]; then
|
|
1194
|
-
error "No AGENTS.md found. Run 'sync-agents init' first."
|
|
1195
|
-
exit 1
|
|
1196
|
-
fi
|
|
1197
|
-
|
|
1198
|
-
# Check if Inherits section exists; if not, add it after the header
|
|
1199
|
-
if ! grep -q "^## Inherits" "$PROJECT_ROOT/$AGENTS_MD"; then
|
|
1200
|
-
# Insert Inherits section right after the header block (after first blank line following description)
|
|
1201
|
-
local tmp
|
|
1202
|
-
tmp="$(mktemp)"
|
|
1203
|
-
local header_done="false"
|
|
1204
|
-
local inherits_written="false"
|
|
1205
|
-
while IFS= read -r line; do
|
|
1206
|
-
echo "$line" >> "$tmp"
|
|
1207
|
-
# Write inherits section after the description paragraph (first line starting with "This file")
|
|
1208
|
-
if [[ "$header_done" == "false" ]] && [[ "$line" == "This file indexes"* ]]; then
|
|
1209
|
-
header_done="true"
|
|
1210
|
-
{
|
|
1211
|
-
echo ""
|
|
1212
|
-
echo "## Inherits"
|
|
1213
|
-
echo ""
|
|
1214
|
-
echo "- [$label]($path)"
|
|
1215
|
-
} >> "$tmp"
|
|
1216
|
-
inherits_written="true"
|
|
1217
|
-
fi
|
|
1218
|
-
done < "$PROJECT_ROOT/$AGENTS_MD"
|
|
1219
|
-
# Fallback: if header pattern wasn't found, append at the end before ## Rules
|
|
1220
|
-
if [[ "$inherits_written" == "false" ]]; then
|
|
1221
|
-
rm "$tmp"
|
|
1222
|
-
tmp="$(mktemp)"
|
|
1223
|
-
while IFS= read -r line; do
|
|
1224
|
-
if [[ "$line" == "## Rules" ]] && [[ "$inherits_written" == "false" ]]; then
|
|
1225
|
-
{
|
|
1226
|
-
echo "## Inherits"
|
|
1227
|
-
echo ""
|
|
1228
|
-
echo "- [$label]($path)"
|
|
1229
|
-
echo ""
|
|
1230
|
-
} >> "$tmp"
|
|
1231
|
-
inherits_written="true"
|
|
1232
|
-
fi
|
|
1233
|
-
echo "$line" >> "$tmp"
|
|
1234
|
-
done < "$PROJECT_ROOT/$AGENTS_MD"
|
|
1235
|
-
fi
|
|
1236
|
-
mv "$tmp" "$PROJECT_ROOT/$AGENTS_MD"
|
|
1237
|
-
else
|
|
1238
|
-
# Inherits section exists — check for duplicate label
|
|
1239
|
-
if grep -q "\[$label\](" "$PROJECT_ROOT/$AGENTS_MD"; then
|
|
1240
|
-
warn "Inherit with label '$label' already exists. Use --remove first to update."
|
|
1241
|
-
return 1
|
|
1242
|
-
fi
|
|
1243
|
-
# Append to existing Inherits section (after last inherit entry or section header)
|
|
1244
|
-
local tmp
|
|
1245
|
-
tmp="$(mktemp)"
|
|
1246
|
-
local in_section="false"
|
|
1247
|
-
local added="false"
|
|
1248
|
-
while IFS= read -r line; do
|
|
1249
|
-
if [[ "$line" =~ ^##[[:space:]]+Inherits ]]; then
|
|
1250
|
-
in_section="true"
|
|
1251
|
-
echo "$line" >> "$tmp"
|
|
1252
|
-
continue
|
|
1253
|
-
fi
|
|
1254
|
-
# When we hit the next section or blank line after entries, insert
|
|
1255
|
-
if [[ "$in_section" == "true" ]] && [[ "$added" == "false" ]]; then
|
|
1256
|
-
if [[ "$line" =~ ^## ]] || [[ -z "$line" ]]; then
|
|
1257
|
-
# Check if previous content had entries; add after them
|
|
1258
|
-
if [[ "$line" =~ ^## ]]; then
|
|
1259
|
-
{
|
|
1260
|
-
echo "- [$label]($path)"
|
|
1261
|
-
echo ""
|
|
1262
|
-
} >> "$tmp"
|
|
1263
|
-
added="true"
|
|
1264
|
-
in_section="false"
|
|
1265
|
-
fi
|
|
1266
|
-
fi
|
|
1267
|
-
if [[ "$line" =~ ^-[[:space:]]+\[ ]]; then
|
|
1268
|
-
echo "$line" >> "$tmp"
|
|
1269
|
-
continue
|
|
1270
|
-
fi
|
|
1271
|
-
if [[ -z "$line" ]] && [[ "$added" == "false" ]]; then
|
|
1272
|
-
echo "- [$label]($path)" >> "$tmp"
|
|
1273
|
-
added="true"
|
|
1274
|
-
in_section="false"
|
|
1275
|
-
echo "$line" >> "$tmp"
|
|
1276
|
-
continue
|
|
1277
|
-
fi
|
|
1278
|
-
fi
|
|
1279
|
-
echo "$line" >> "$tmp"
|
|
1280
|
-
done < "$PROJECT_ROOT/$AGENTS_MD"
|
|
1281
|
-
# If we never added (section was at end of file)
|
|
1282
|
-
if [[ "$added" == "false" ]]; then
|
|
1283
|
-
echo "- [$label]($path)" >> "$tmp"
|
|
1284
|
-
echo "" >> "$tmp"
|
|
1285
|
-
fi
|
|
1286
|
-
mv "$tmp" "$PROJECT_ROOT/$AGENTS_MD"
|
|
1287
|
-
fi
|
|
1288
|
-
|
|
1289
|
-
info "Added inherit: [$label]($path)"
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1292
|
-
# --------------------------------------------------------------------------
|
|
1293
|
-
# Index generator
|
|
1294
|
-
# --------------------------------------------------------------------------
|
|
1295
|
-
|
|
1296
|
-
generate_agents_md() {
|
|
1297
|
-
local outfile="$PROJECT_ROOT/$AGENTS_MD"
|
|
1298
|
-
local agents_dir="$PROJECT_ROOT/$AGENTS_DIR"
|
|
1299
|
-
|
|
1300
|
-
# Preserve existing Inherits section before regenerating
|
|
1301
|
-
local inherits_block=""
|
|
1302
|
-
if [[ -f "$outfile" ]]; then
|
|
1303
|
-
local in_section="false"
|
|
1304
|
-
while IFS= read -r line; do
|
|
1305
|
-
if [[ "$line" =~ ^##[[:space:]]+Inherits ]]; then
|
|
1306
|
-
in_section="true"
|
|
1307
|
-
inherits_block+="$line"$'\n'
|
|
1308
|
-
continue
|
|
1309
|
-
fi
|
|
1310
|
-
if [[ "$in_section" == "true" ]] && [[ "$line" =~ ^## ]]; then
|
|
1311
|
-
break
|
|
1312
|
-
fi
|
|
1313
|
-
if [[ "$in_section" == "true" ]]; then
|
|
1314
|
-
inherits_block+="$line"$'\n'
|
|
1315
|
-
fi
|
|
1316
|
-
done < "$outfile"
|
|
1317
|
-
fi
|
|
1318
|
-
|
|
1319
|
-
cat > "$outfile" <<'HEADER'
|
|
1320
|
-
---
|
|
1321
|
-
trigger: always_on
|
|
1322
|
-
---
|
|
1323
|
-
|
|
1324
|
-
# AGENTS
|
|
1325
|
-
|
|
1326
|
-
> Auto-generated by [sync-agents](https://github.com/brickhouse-tech/sync-agents). Do not edit manually.
|
|
1327
|
-
> Run `sync-agents index` to regenerate.
|
|
1328
|
-
|
|
1329
|
-
This file indexes all rules, skills, and workflows defined in `.agents/`.
|
|
1330
|
-
|
|
1331
|
-
HEADER
|
|
1332
|
-
|
|
1333
|
-
{
|
|
1334
|
-
# Inherits (preserved from previous AGENTS.md)
|
|
1335
|
-
if [[ -n "$inherits_block" ]]; then
|
|
1336
|
-
printf '%s\n' "$inherits_block"
|
|
1337
|
-
fi
|
|
1338
|
-
|
|
1339
|
-
# Rules
|
|
1340
|
-
echo "## Rules"
|
|
1341
|
-
echo ""
|
|
1342
|
-
if compgen -G "$agents_dir/rules/*.md" > /dev/null 2>&1; then
|
|
1343
|
-
for f in "$agents_dir/rules/"*.md; do
|
|
1344
|
-
local name
|
|
1345
|
-
name="$(basename "$f" .md)"
|
|
1346
|
-
echo "- [$name](.agents/rules/$name.md)"
|
|
1347
|
-
done
|
|
1348
|
-
else
|
|
1349
|
-
echo "_No rules defined yet. Add one with \`sync-agents add rule <name>\`._"
|
|
1350
|
-
fi
|
|
1351
|
-
echo ""
|
|
1352
|
-
|
|
1353
|
-
# Skills (directory layout: skills/name/SKILL.md, or legacy flat: skills/name.md)
|
|
1354
|
-
echo "## Skills"
|
|
1355
|
-
echo ""
|
|
1356
|
-
local has_skills="false"
|
|
1357
|
-
# Directory skills: skills/name/SKILL.md
|
|
1358
|
-
for d in "$agents_dir/skills/"*/; do
|
|
1359
|
-
[[ -d "$d" ]] || continue
|
|
1360
|
-
local name
|
|
1361
|
-
name="$(basename "$d")"
|
|
1362
|
-
if [[ -f "$d/SKILL.md" ]]; then
|
|
1363
|
-
echo "- [$name](.agents/skills/$name/SKILL.md)"
|
|
1364
|
-
has_skills="true"
|
|
1365
|
-
fi
|
|
1366
|
-
done
|
|
1367
|
-
# Legacy flat skills: skills/name.md
|
|
1368
|
-
if compgen -G "$agents_dir/skills/*.md" > /dev/null 2>&1; then
|
|
1369
|
-
for f in "$agents_dir/skills/"*.md; do
|
|
1370
|
-
local name
|
|
1371
|
-
name="$(basename "$f" .md)"
|
|
1372
|
-
echo "- [$name](.agents/skills/$name.md)"
|
|
1373
|
-
has_skills="true"
|
|
1374
|
-
done
|
|
1375
|
-
fi
|
|
1376
|
-
if [[ "$has_skills" == "false" ]]; then
|
|
1377
|
-
echo "_No skills defined yet. Add one with \`sync-agents add skill <name>\`._"
|
|
1378
|
-
fi
|
|
1379
|
-
echo ""
|
|
1380
|
-
|
|
1381
|
-
# Workflows
|
|
1382
|
-
echo "## Workflows"
|
|
1383
|
-
echo ""
|
|
1384
|
-
if compgen -G "$agents_dir/workflows/*.md" > /dev/null 2>&1; then
|
|
1385
|
-
for f in "$agents_dir/workflows/"*.md; do
|
|
1386
|
-
local name
|
|
1387
|
-
name="$(basename "$f" .md)"
|
|
1388
|
-
echo "- [$name](.agents/workflows/$name.md)"
|
|
1389
|
-
done
|
|
1390
|
-
else
|
|
1391
|
-
echo "_No workflows defined yet. Add one with \`sync-agents add workflow <name>\`._"
|
|
1392
|
-
fi
|
|
1393
|
-
echo ""
|
|
1394
|
-
|
|
1395
|
-
# State (list individual state snapshot files)
|
|
1396
|
-
echo "## State"
|
|
1397
|
-
echo ""
|
|
1398
|
-
local has_state="false"
|
|
1399
|
-
if compgen -G "$agents_dir/STATE_*.md" > /dev/null 2>&1; then
|
|
1400
|
-
for f in "$agents_dir"/STATE_*.md; do
|
|
1401
|
-
local name
|
|
1402
|
-
name="$(basename "$f" .md)"
|
|
1403
|
-
echo "- [$name](.agents/$(basename "$f"))"
|
|
1404
|
-
has_state="true"
|
|
1405
|
-
done
|
|
1406
|
-
fi
|
|
1407
|
-
# Legacy fallback: still list STATE.md if it exists (pre-migration)
|
|
1408
|
-
if [[ -f "$agents_dir/STATE.md" ]]; then
|
|
1409
|
-
echo "- [STATE.md](.agents/STATE.md)"
|
|
1410
|
-
has_state="true"
|
|
1411
|
-
fi
|
|
1412
|
-
if [[ "$has_state" == "false" ]]; then
|
|
1413
|
-
echo "_No state snapshots yet. Agents will create STATE_*context*_*timestamp*.md files as they work._"
|
|
1414
|
-
fi
|
|
1415
|
-
echo ""
|
|
1416
|
-
} >> "$outfile"
|
|
1417
|
-
}
|
|
1418
|
-
|
|
1419
|
-
# --------------------------------------------------------------------------
|
|
1420
|
-
# Tree printer (lightweight, no dependency on `tree`)
|
|
1421
|
-
# --------------------------------------------------------------------------
|
|
1422
|
-
|
|
1423
|
-
print_tree() {
|
|
1424
|
-
local dir="$1"
|
|
1425
|
-
local prefix="${2:-}"
|
|
1426
|
-
local entries=()
|
|
1427
|
-
|
|
1428
|
-
# Collect entries
|
|
1429
|
-
while IFS= read -r entry; do
|
|
1430
|
-
entries+=("$entry")
|
|
1431
|
-
done < <(find "$dir" -maxdepth 1 -mindepth 1 -exec basename {} \; 2>/dev/null | sort)
|
|
1432
|
-
|
|
1433
|
-
local count=${#entries[@]}
|
|
1434
|
-
if [[ $count -eq 0 ]]; then
|
|
1435
|
-
return 0
|
|
1436
|
-
fi
|
|
1437
|
-
local i=0
|
|
1438
|
-
|
|
1439
|
-
for entry in "${entries[@]}"; do
|
|
1440
|
-
i=$((i + 1))
|
|
1441
|
-
local connector="├── "
|
|
1442
|
-
local child_prefix="│ "
|
|
1443
|
-
if [[ $i -eq $count ]]; then
|
|
1444
|
-
connector="└── "
|
|
1445
|
-
child_prefix=" "
|
|
1446
|
-
fi
|
|
1447
|
-
|
|
1448
|
-
if [[ -d "$dir/$entry" ]]; then
|
|
1449
|
-
echo "${prefix}${connector}${entry}/"
|
|
1450
|
-
print_tree "$dir/$entry" "${prefix}${child_prefix}"
|
|
1451
|
-
elif [[ -L "$dir/$entry" ]]; then
|
|
1452
|
-
local link_target
|
|
1453
|
-
link_target="$(readlink "$dir/$entry")"
|
|
1454
|
-
echo "${prefix}${connector}${entry} -> ${link_target}"
|
|
1455
|
-
else
|
|
1456
|
-
echo "${prefix}${connector}${entry}"
|
|
1457
|
-
fi
|
|
1458
|
-
done
|
|
1459
|
-
}
|
|
1460
|
-
|
|
1461
|
-
# --------------------------------------------------------------------------
|
|
1462
|
-
# Main
|
|
1463
|
-
# --------------------------------------------------------------------------
|
|
1464
|
-
|
|
1465
|
-
main() {
|
|
1466
|
-
local command=""
|
|
1467
|
-
local custom_dir=""
|
|
1468
|
-
local custom_targets=""
|
|
1469
|
-
DRY_RUN="false"
|
|
1470
|
-
FORCE="false"
|
|
1471
|
-
|
|
1472
|
-
# Parse arguments
|
|
1473
|
-
while [[ $# -gt 0 ]]; do
|
|
1474
|
-
case "$1" in
|
|
1475
|
-
-h|--help)
|
|
1476
|
-
usage
|
|
1477
|
-
exit 0
|
|
1478
|
-
;;
|
|
1479
|
-
-v|--version)
|
|
1480
|
-
echo "sync-agents v${VERSION}"
|
|
1481
|
-
exit 0
|
|
1482
|
-
;;
|
|
1483
|
-
-d|--dir)
|
|
1484
|
-
custom_dir="$2"
|
|
1485
|
-
shift 2
|
|
1486
|
-
;;
|
|
1487
|
-
--targets)
|
|
1488
|
-
custom_targets="$2"
|
|
1489
|
-
shift 2
|
|
1490
|
-
;;
|
|
1491
|
-
--dry-run)
|
|
1492
|
-
DRY_RUN="true"
|
|
1493
|
-
shift
|
|
1494
|
-
;;
|
|
1495
|
-
--force)
|
|
1496
|
-
FORCE="true"
|
|
1497
|
-
shift
|
|
1498
|
-
;;
|
|
1499
|
-
-*)
|
|
1500
|
-
if [[ -n "$command" ]]; then
|
|
1501
|
-
# Unknown flag after command — pass to subcommand
|
|
1502
|
-
break
|
|
1503
|
-
fi
|
|
1504
|
-
error "Unknown option: $1"
|
|
1505
|
-
usage
|
|
1506
|
-
exit 1
|
|
1507
|
-
;;
|
|
1508
|
-
*)
|
|
1509
|
-
if [[ -z "$command" ]]; then
|
|
1510
|
-
command="$1"
|
|
1511
|
-
else
|
|
1512
|
-
# Collect remaining args for subcommands
|
|
1513
|
-
break
|
|
1514
|
-
fi
|
|
1515
|
-
shift
|
|
1516
|
-
;;
|
|
1517
|
-
esac
|
|
1518
|
-
done
|
|
1519
|
-
|
|
1520
|
-
# Resolve project root
|
|
1521
|
-
if [[ -n "$custom_dir" ]]; then
|
|
1522
|
-
PROJECT_ROOT="$(cd "$custom_dir" && pwd)"
|
|
1523
|
-
else
|
|
1524
|
-
PROJECT_ROOT="$(find_project_root)"
|
|
1525
|
-
fi
|
|
1526
|
-
|
|
1527
|
-
# Resolve active targets (priority: --targets flag > .agents/config > built-in defaults)
|
|
1528
|
-
if [[ -n "$custom_targets" ]]; then
|
|
1529
|
-
IFS=',' read -ra ACTIVE_TARGETS <<< "$custom_targets"
|
|
1530
|
-
else
|
|
1531
|
-
local config_file="$PROJECT_ROOT/$AGENTS_DIR/config"
|
|
1532
|
-
if [[ -f "$config_file" ]]; then
|
|
1533
|
-
local config_targets
|
|
1534
|
-
config_targets="$(sed -n 's/^targets *= *//p' "$config_file" | tr -d ' ')"
|
|
1535
|
-
if [[ -n "$config_targets" ]]; then
|
|
1536
|
-
IFS=',' read -ra ACTIVE_TARGETS <<< "$config_targets"
|
|
1537
|
-
else
|
|
1538
|
-
ACTIVE_TARGETS=("${TARGETS[@]}")
|
|
1539
|
-
fi
|
|
1540
|
-
else
|
|
1541
|
-
ACTIVE_TARGETS=("${TARGETS[@]}")
|
|
1542
|
-
fi
|
|
1543
|
-
fi
|
|
1544
|
-
|
|
1545
|
-
# Dispatch command
|
|
1546
|
-
case "${command:-}" in
|
|
1547
|
-
init)
|
|
1548
|
-
cmd_init
|
|
1549
|
-
;;
|
|
1550
|
-
sync)
|
|
1551
|
-
cmd_sync
|
|
1552
|
-
;;
|
|
1553
|
-
status)
|
|
1554
|
-
cmd_status
|
|
1555
|
-
;;
|
|
1556
|
-
add)
|
|
1557
|
-
cmd_add "$@"
|
|
1558
|
-
;;
|
|
1559
|
-
index)
|
|
1560
|
-
cmd_index
|
|
1561
|
-
;;
|
|
1562
|
-
clean)
|
|
1563
|
-
cmd_clean
|
|
1564
|
-
;;
|
|
1565
|
-
watch)
|
|
1566
|
-
cmd_watch
|
|
1567
|
-
;;
|
|
1568
|
-
import)
|
|
1569
|
-
cmd_import "$@"
|
|
1570
|
-
;;
|
|
1571
|
-
fix)
|
|
1572
|
-
cmd_fix "$@"
|
|
1573
|
-
;;
|
|
1574
|
-
hook)
|
|
1575
|
-
cmd_hook
|
|
1576
|
-
;;
|
|
1577
|
-
inherit)
|
|
1578
|
-
cmd_inherit "$@"
|
|
1579
|
-
;;
|
|
1580
|
-
version)
|
|
1581
|
-
echo "sync-agents v${VERSION}"
|
|
1582
|
-
exit 0
|
|
1583
|
-
;;
|
|
1584
|
-
"")
|
|
1585
|
-
usage
|
|
1586
|
-
exit 0
|
|
1587
|
-
;;
|
|
1588
|
-
*)
|
|
1589
|
-
error "Unknown command: $command"
|
|
1590
|
-
usage
|
|
1591
|
-
exit 1
|
|
1592
|
-
;;
|
|
1593
|
-
esac
|
|
1594
|
-
}
|
|
1595
|
-
|
|
1596
|
-
main "$@"
|