@brickhouse-tech/sync-agents 0.1.10 → 0.1.12

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 CHANGED
@@ -31,13 +31,16 @@ chmod +x /usr/local/bin/sync-agents
31
31
 
32
32
  ```
33
33
  .agents/
34
+ ├── config # sync targets (claude, windsurf, cursor, copilot)
34
35
  ├── rules/
35
36
  │ ├── rule1.md
36
37
  │ ├── rule2.md
37
38
  │ └── ...
38
39
  ├── skills/
39
- │ ├── skill1.md
40
- ├── skill2.md
40
+ │ ├── skill1/
41
+ │ └── SKILL.md
42
+ │ ├── skill2/
43
+ │ │ └── SKILL.md
41
44
  │ └── ...
42
45
  ├── workflows/
43
46
  │ ├── workflow1.md
@@ -46,6 +49,8 @@ chmod +x /usr/local/bin/sync-agents
46
49
  └── STATE.md
47
50
  ```
48
51
 
52
+ > **Note:** Skills use a directory layout (`skills/name/SKILL.md`) rather than flat files. This allows skills to include supporting files alongside their definition. The `fix` command can convert legacy flat skill files to the directory layout automatically.
53
+
49
54
  Running `sync-agents sync` creates symlinks from `.agents/` subdirectories into `.claude/`, `.windsurf/`, `.cursor/`, and `.github/copilot/`. Any changes to `.agents/` are automatically reflected in the target directories because they are symlinks, not copies.
50
55
 
51
56
  AGENTS.md is also symlinked to CLAUDE.md so that Claude reads the index natively.
@@ -70,6 +75,7 @@ AGENTS.md is also symlinked to CLAUDE.md so that Claude reads the index natively
70
75
  | `add <type> <name>` | Add a new rule, skill, or workflow from a template (type is `rule`, `skill`, or `workflow`) |
71
76
  | `index` | Regenerate `AGENTS.md` by scanning the contents of `.agents/` |
72
77
  | `clean` | Remove all synced symlinks and empty target directories (does not remove `.agents/`) |
78
+ | `fix [type]` | Migrate legacy dirs into `.agents/`, convert flat skill files to directory layout, and repair broken symlinks. Type: `skills`, `rules`, `workflows`, or `all` (default) |
73
79
 
74
80
  ## Options
75
81
 
@@ -81,6 +87,47 @@ AGENTS.md is also symlinked to CLAUDE.md so that Claude reads the index natively
81
87
  | `--targets <list>` | Comma-separated list of sync targets (default: `claude,windsurf,cursor,copilot`) |
82
88
  | `--dry-run` | Show what would be done without making changes |
83
89
  | `--force` | Overwrite existing files and symlinks |
90
+ | `--no-clobber` | (fix only) Skip items that already exist in `.agents/` instead of merging |
91
+
92
+ ## Configuration
93
+
94
+ `sync-agents init` creates `.agents/config` with default sync targets:
95
+
96
+ ```
97
+ # sync-agents configuration
98
+ # Comma-separated list of sync targets (available: claude, windsurf, cursor, copilot)
99
+ targets = claude,windsurf,cursor,copilot
100
+ ```
101
+
102
+ Edit this file to limit which targets `sync` writes to by default. The `--targets` flag on any command overrides the config.
103
+
104
+ ## Fix
105
+
106
+ The `fix` command handles three scenarios:
107
+
108
+ 1. **Legacy directory migration** — Moves top-level `skills/`, `rules/`, or `workflows/` directories into `.agents/` and replaces them with symlinks.
109
+ 2. **Flat skill conversion** — Converts `.agents/skills/name.md` flat files to the directory layout `.agents/skills/name/SKILL.md`.
110
+ 3. **Symlink repair** — Recreates missing or broken symlinks in target directories (`.claude/`, `.windsurf/`, etc.) and the `CLAUDE.md` symlink.
111
+
112
+ ```bash
113
+ # Fix everything (all types)
114
+ sync-agents fix
115
+
116
+ # Fix only skills
117
+ sync-agents fix skills
118
+
119
+ # Preview without changing anything
120
+ sync-agents fix --dry-run
121
+
122
+ # Don't overwrite items already in .agents/
123
+ sync-agents fix --no-clobber skills
124
+ ```
125
+
126
+ A reproducible demo is available in [`examples/fix/`](examples/fix/):
127
+
128
+ ```bash
129
+ bash examples/fix/run-demo.sh
130
+ ```
84
131
 
85
132
  ## Inheritance
86
133
 
@@ -262,6 +309,12 @@ sync-agents index
262
309
  # Remove all synced symlinks
263
310
  sync-agents clean
264
311
 
312
+ # Fix legacy layouts and broken symlinks
313
+ sync-agents fix
314
+
315
+ # Fix only skills (migrate + convert flat files + repair symlinks)
316
+ sync-agents fix skills
317
+
265
318
  # Work in a different directory
266
319
  sync-agents sync --dir /path/to/project
267
320
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brickhouse-tech/sync-agents",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "Simple scripts to DRY up common agent interactions across multiple LLM providers.",
5
5
  "keywords": [
6
6
  "agents",
@@ -25,6 +25,10 @@
25
25
  "files": [
26
26
  "src/**/*"
27
27
  ],
28
+ "overrides": {
29
+ "file-type": ">=22",
30
+ "picomatch": ">=4.0.4"
31
+ },
28
32
  "scripts": {
29
33
  "lint": "shellcheck src/sh/*.sh",
30
34
  "test": "npx bats test/"
@@ -34,6 +38,6 @@
34
38
  "@commitlint/config-conventional": "^20",
35
39
  "commitlint": "20",
36
40
  "shellcheck": "^4.1.0",
37
- "sort-package-json": "3.6"
41
+ "sort-package-json": ">=3"
38
42
  }
39
43
  }
@@ -75,10 +75,13 @@ ${BOLD}COMMANDS${RESET}
75
75
  watch Watch .agents/ for changes and auto-regenerate index
76
76
  import <url> Import a rule/skill/workflow from a URL
77
77
  hook Install a pre-commit git hook for auto-sync
78
- fix [type] Migrate legacy dirs into .agents/ (type: skills, rules, workflows, or all)
78
+ fix [type] Migrate legacy dirs + repair broken symlinks
79
+ type: skills, rules, workflows, or all
80
+ --no-clobber: skip items that already exist in .agents/
79
81
  inherit <label> <path> Add an inheritance link to AGENTS.md (convention-based)
80
82
  inherit --list List current inheritance links
81
83
  inherit --remove <label> Remove an inheritance link by label
84
+ version Show version (same as --version)
82
85
 
83
86
  ${BOLD}OPTIONS${RESET}
84
87
  -h, --help Show this help message
@@ -301,7 +304,17 @@ TMPL_EOF
301
304
  cmd_fix() {
302
305
  ensure_agents_dir
303
306
 
304
- local fix_type="${1:-all}"
307
+ # Parse fix-specific flags
308
+ local no_clobber="false"
309
+ local fix_args=()
310
+ for arg in "$@"; do
311
+ case "$arg" in
312
+ --no-clobber) no_clobber="true" ;;
313
+ *) fix_args+=("$arg") ;;
314
+ esac
315
+ done
316
+
317
+ local fix_type="${fix_args[0]:-all}"
305
318
  local subdirs=()
306
319
 
307
320
  case "$fix_type" in
@@ -320,13 +333,50 @@ cmd_fix() {
320
333
  local agents_abs
321
334
  agents_abs="$(cd "$PROJECT_ROOT/$AGENTS_DIR" && pwd)"
322
335
  local fixed=0
336
+ local skipped=0
337
+ local merged=0
338
+
339
+ # Helper: compare inodes of two directories (portable across macOS/Linux)
340
+ same_inode() {
341
+ local inode_a inode_b
342
+ if stat --version >/dev/null 2>&1; then
343
+ # GNU stat (Linux)
344
+ inode_a="$(stat -c '%i' "$1" 2>/dev/null)"
345
+ inode_b="$(stat -c '%i' "$2" 2>/dev/null)"
346
+ else
347
+ # BSD stat (macOS)
348
+ inode_a="$(stat -f '%i' "$1" 2>/dev/null)"
349
+ inode_b="$(stat -f '%i' "$2" 2>/dev/null)"
350
+ fi
351
+ [[ -n "$inode_a" ]] && [[ "$inode_a" == "$inode_b" ]]
352
+ }
323
353
 
324
354
  for subdir in "${subdirs[@]}"; do
325
355
  local legacy_dir="$PROJECT_ROOT/$subdir"
326
356
  local agents_subdir="$agents_abs/$subdir"
327
357
 
328
358
  # Skip if legacy dir doesn't exist or is already a symlink
329
- if [[ ! -d "$legacy_dir" ]] || [[ -L "$legacy_dir" ]]; then
359
+ if [[ ! -d "$legacy_dir" ]]; then
360
+ continue
361
+ fi
362
+ if [[ -L "$legacy_dir" ]]; then
363
+ info "$subdir/ is already a symlink — nothing to do."
364
+ continue
365
+ fi
366
+
367
+ # Detect same-inode (legacy dir IS .agents/subdir — e.g. hardlink or bind mount)
368
+ if [[ -d "$agents_subdir" ]] && same_inode "$legacy_dir" "$agents_subdir"; then
369
+ warn "$subdir/ and $AGENTS_DIR/$subdir/ are the same directory (same inode)."
370
+ warn "Replacing $subdir/ with a symlink to $AGENTS_DIR/$subdir/."
371
+ if [[ "$DRY_RUN" == "true" ]]; then
372
+ echo " would remove $subdir/ (same inode as $AGENTS_DIR/$subdir/)"
373
+ echo " would create symlink $subdir/ -> $AGENTS_DIR/$subdir"
374
+ else
375
+ rm -rf "$legacy_dir"
376
+ ln -s "$AGENTS_DIR/$subdir" "$legacy_dir"
377
+ info "Replaced $subdir/ with symlink -> $AGENTS_DIR/$subdir"
378
+ fi
379
+ fixed=$((fixed + 1))
330
380
  continue
331
381
  fi
332
382
 
@@ -340,7 +390,21 @@ cmd_fix() {
340
390
  name="$(basename "$item")"
341
391
 
342
392
  if [[ -d "$agents_subdir/$name" ]]; then
343
- warn "Skipping $subdir/$name already exists in $AGENTS_DIR/$subdir/"
393
+ if [[ "$no_clobber" == "true" ]]; then
394
+ warn "Skipping $subdir/$name — already exists in $AGENTS_DIR/$subdir/ (--no-clobber)"
395
+ skipped=$((skipped + 1))
396
+ continue
397
+ fi
398
+ # Merge: legacy content wins (overwrite into .agents/)
399
+ if [[ "$DRY_RUN" == "true" ]]; then
400
+ echo " would merge: $subdir/$name -> $AGENTS_DIR/$subdir/$name (overwrite)"
401
+ else
402
+ rm -rf "${agents_subdir:?}/${name:?}"
403
+ mv "$item" "$agents_subdir/$name"
404
+ info "Merged: $subdir/$name -> $AGENTS_DIR/$subdir/$name (overwrote existing)"
405
+ fi
406
+ merged=$((merged + 1))
407
+ fixed=$((fixed + 1))
344
408
  continue
345
409
  fi
346
410
 
@@ -360,7 +424,20 @@ cmd_fix() {
360
424
  name="$(basename "$item")"
361
425
 
362
426
  if [[ -f "$agents_subdir/$name" ]]; then
363
- warn "Skipping $subdir/$name already exists in $AGENTS_DIR/$subdir/"
427
+ if [[ "$no_clobber" == "true" ]]; then
428
+ warn "Skipping $subdir/$name — already exists in $AGENTS_DIR/$subdir/ (--no-clobber)"
429
+ skipped=$((skipped + 1))
430
+ continue
431
+ fi
432
+ # Merge: legacy content wins
433
+ if [[ "$DRY_RUN" == "true" ]]; then
434
+ echo " would merge: $subdir/$name -> $AGENTS_DIR/$subdir/$name (overwrite)"
435
+ else
436
+ mv "$item" "$agents_subdir/$name"
437
+ info "Merged: $subdir/$name -> $AGENTS_DIR/$subdir/$name (overwrote existing)"
438
+ fi
439
+ merged=$((merged + 1))
440
+ fixed=$((fixed + 1))
364
441
  continue
365
442
  fi
366
443
 
@@ -390,10 +467,139 @@ cmd_fix() {
390
467
  fi
391
468
  done
392
469
 
393
- if [[ "$fixed" -eq 0 ]]; then
394
- info "Nothing to fix — all directories are already in $AGENTS_DIR/ or symlinked."
470
+ # --- Phase 1b: Convert flat skill files to directory layout ---
471
+ # e.g. .agents/skills/foo.md -> .agents/skills/foo/SKILL.md
472
+ for subdir in "${subdirs[@]}"; do
473
+ [[ "$subdir" == "skills" ]] || continue
474
+ local skills_dir="$agents_abs/skills"
475
+ [[ -d "$skills_dir" ]] || continue
476
+
477
+ for flat_file in "$skills_dir"/*.md; do
478
+ [[ -f "$flat_file" ]] || continue
479
+ local name
480
+ name="$(basename "$flat_file" .md)"
481
+ local target_dir="$skills_dir/$name"
482
+ local target_file="$target_dir/SKILL.md"
483
+
484
+ if [[ -d "$target_dir" ]] && [[ -f "$target_file" ]]; then
485
+ if [[ "$no_clobber" == "true" ]]; then
486
+ warn "Skipping flat skill $name.md — $name/SKILL.md already exists (--no-clobber)"
487
+ skipped=$((skipped + 1))
488
+ continue
489
+ fi
490
+ # Flat file wins (same merge behavior as legacy migration)
491
+ if [[ "$DRY_RUN" == "true" ]]; then
492
+ echo " would convert: skills/$name.md -> skills/$name/SKILL.md (overwrite)"
493
+ else
494
+ mv "$flat_file" "$target_file"
495
+ info "Converted: skills/$name.md -> skills/$name/SKILL.md (overwrote existing)"
496
+ fi
497
+ merged=$((merged + 1))
498
+ fixed=$((fixed + 1))
499
+ continue
500
+ fi
501
+
502
+ if [[ "$DRY_RUN" == "true" ]]; then
503
+ echo " would convert: skills/$name.md -> skills/$name/SKILL.md"
504
+ else
505
+ mkdir -p "$target_dir"
506
+ mv "$flat_file" "$target_file"
507
+ info "Converted: skills/$name.md -> skills/$name/SKILL.md"
508
+ fi
509
+ fixed=$((fixed + 1))
510
+ done
511
+ done
512
+
513
+ # --- Phase 2: Repair broken/missing symlinks ---
514
+ local repaired=0
515
+
516
+ for target in "${ACTIVE_TARGETS[@]}"; do
517
+ local target_dir
518
+ target_dir="$(resolve_target_dir "$target" "$PROJECT_ROOT")"
519
+ local agents_rel
520
+ agents_rel="$(resolve_agents_rel "$target")"
521
+
522
+ for subdir in "${subdirs[@]}"; do
523
+ if [[ ! -d "$agents_abs/$subdir" ]]; then
524
+ continue
525
+ fi
526
+ local expected_link="$target_dir/$subdir"
527
+ local expected_source="$agents_rel/$subdir"
528
+
529
+ if [[ -L "$expected_link" ]]; then
530
+ local current_target
531
+ current_target="$(readlink "$expected_link")"
532
+ if [[ "$current_target" == "$expected_source" ]]; then
533
+ continue # Already correct
534
+ fi
535
+ # Symlink exists but points to wrong target
536
+ if [[ "$DRY_RUN" == "true" ]]; then
537
+ echo " would relink: $expected_link -> $expected_source (was $current_target)"
538
+ else
539
+ rm "$expected_link"
540
+ create_symlink "$expected_source" "$expected_link" "false"
541
+ info "Repaired: $expected_link -> $expected_source (was $current_target)"
542
+ fi
543
+ repaired=$((repaired + 1))
544
+ elif [[ -e "$expected_link" ]]; then
545
+ # Something exists but isn't a symlink — skip unless --force
546
+ if [[ "$FORCE" == "true" ]]; then
547
+ if [[ "$DRY_RUN" == "true" ]]; then
548
+ echo " would replace: $expected_link with symlink -> $expected_source"
549
+ else
550
+ rm -rf "$expected_link"
551
+ create_symlink "$expected_source" "$expected_link" "false"
552
+ info "Repaired: replaced $expected_link with symlink -> $expected_source"
553
+ fi
554
+ repaired=$((repaired + 1))
555
+ else
556
+ warn "$expected_link exists but is not a symlink (use --force to replace)"
557
+ fi
558
+ else
559
+ # Missing entirely
560
+ if [[ "$DRY_RUN" == "true" ]]; then
561
+ echo " would create: $expected_link -> $expected_source"
562
+ else
563
+ create_symlink "$expected_source" "$expected_link" "false"
564
+ fi
565
+ repaired=$((repaired + 1))
566
+ fi
567
+ done
568
+ done
569
+
570
+ # Repair CLAUDE.md symlink
571
+ if [[ -f "$PROJECT_ROOT/$AGENTS_MD" ]]; then
572
+ if [[ -L "$PROJECT_ROOT/CLAUDE.md" ]]; then
573
+ local current_target
574
+ current_target="$(readlink "$PROJECT_ROOT/CLAUDE.md")"
575
+ if [[ "$current_target" != "$AGENTS_MD" ]]; then
576
+ if [[ "$DRY_RUN" == "true" ]]; then
577
+ echo " would relink: CLAUDE.md -> $AGENTS_MD (was $current_target)"
578
+ else
579
+ rm "$PROJECT_ROOT/CLAUDE.md"
580
+ create_symlink "$AGENTS_MD" "$PROJECT_ROOT/CLAUDE.md" "false"
581
+ fi
582
+ repaired=$((repaired + 1))
583
+ fi
584
+ elif [[ ! -e "$PROJECT_ROOT/CLAUDE.md" ]]; then
585
+ if [[ "$DRY_RUN" == "true" ]]; then
586
+ echo " would create: CLAUDE.md -> $AGENTS_MD"
587
+ else
588
+ create_symlink "$AGENTS_MD" "$PROJECT_ROOT/CLAUDE.md" "false"
589
+ fi
590
+ repaired=$((repaired + 1))
591
+ fi
592
+ fi
593
+
594
+ # Summary
595
+ if [[ "$fixed" -eq 0 ]] && [[ "$skipped" -eq 0 ]] && [[ "$repaired" -eq 0 ]]; then
596
+ info "Nothing to fix — all directories and symlinks are correct."
395
597
  else
396
- info "Fixed $fixed item(s). Run 'sync-agents sync' to update agent target symlinks."
598
+ if [[ "$fixed" -gt 0 ]]; then info "Fixed $fixed item(s)."; fi
599
+ if [[ "$merged" -gt 0 ]]; then info "Merged $merged item(s) (legacy overwrote existing)."; fi
600
+ if [[ "$skipped" -gt 0 ]]; then warn "Skipped $skipped item(s) (use without --no-clobber to merge)."; fi
601
+ if [[ "$repaired" -gt 0 ]]; then info "Repaired $repaired symlink(s)."; fi
602
+ if [[ "$fixed" -gt 0 ]]; then info "Run 'sync-agents sync' to update agent target symlinks."; fi
397
603
  fi
398
604
  }
399
605
 
@@ -1257,6 +1463,10 @@ main() {
1257
1463
  inherit)
1258
1464
  cmd_inherit "$@"
1259
1465
  ;;
1466
+ version)
1467
+ echo "sync-agents v${VERSION}"
1468
+ exit 0
1469
+ ;;
1260
1470
  "")
1261
1471
  usage
1262
1472
  exit 0