@brickhouse-tech/sync-agents 0.1.9 → 0.1.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brickhouse-tech/sync-agents",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
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,6 +75,9 @@ ${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 + repair broken symlinks
79
+ type: skills, rules, workflows, or all
80
+ --no-clobber: skip items that already exist in .agents/
78
81
  inherit <label> <path> Add an inheritance link to AGENTS.md (convention-based)
79
82
  inherit --list List current inheritance links
80
83
  inherit --remove <label> Remove an inheritance link by label
@@ -249,7 +252,14 @@ cmd_add() {
249
252
 
250
253
  ensure_agents_dir
251
254
 
252
- local filepath="$PROJECT_ROOT/$AGENTS_DIR/$type/$name.md"
255
+ # Skills use directory layout: skills/name/SKILL.md
256
+ # Rules and workflows use flat files: rules/name.md, workflows/name.md
257
+ local filepath
258
+ if [[ "$type" == "skills" ]]; then
259
+ filepath="$PROJECT_ROOT/$AGENTS_DIR/$type/$name/SKILL.md"
260
+ else
261
+ filepath="$PROJECT_ROOT/$AGENTS_DIR/$type/$name.md"
262
+ fi
253
263
 
254
264
  if [[ -f "$filepath" ]] && [[ "$FORCE" != "true" ]]; then
255
265
  error "File already exists: $filepath (use --force to overwrite)"
@@ -265,6 +275,9 @@ cmd_add() {
265
275
  *) template_name="RULE_TEMPLATE.md" ;;
266
276
  esac
267
277
 
278
+ # Create parent directory for skills
279
+ mkdir -p "$(dirname "$filepath")"
280
+
268
281
  if [[ -f "$TEMPLATES_DIR/$template_name" ]]; then
269
282
  sed "s/\${NAME}/$name/g" "$TEMPLATES_DIR/$template_name" > "$filepath"
270
283
  elif [[ -f "$TEMPLATES_DIR/RULE_TEMPLATE.md" ]]; then
@@ -287,6 +300,308 @@ TMPL_EOF
287
300
  info "Updated $AGENTS_MD index"
288
301
  }
289
302
 
303
+ cmd_fix() {
304
+ ensure_agents_dir
305
+
306
+ # Parse fix-specific flags
307
+ local no_clobber="false"
308
+ local fix_args=()
309
+ for arg in "$@"; do
310
+ case "$arg" in
311
+ --no-clobber) no_clobber="true" ;;
312
+ *) fix_args+=("$arg") ;;
313
+ esac
314
+ done
315
+
316
+ local fix_type="${fix_args[0]:-all}"
317
+ local subdirs=()
318
+
319
+ case "$fix_type" in
320
+ skills|rules|workflows)
321
+ subdirs=("$fix_type")
322
+ ;;
323
+ all)
324
+ subdirs=(skills rules workflows)
325
+ ;;
326
+ *)
327
+ error "Unknown type: $fix_type (expected: skills, rules, workflows, or all)"
328
+ exit 1
329
+ ;;
330
+ esac
331
+
332
+ local agents_abs
333
+ agents_abs="$(cd "$PROJECT_ROOT/$AGENTS_DIR" && pwd)"
334
+ local fixed=0
335
+ local skipped=0
336
+ local merged=0
337
+
338
+ # Helper: compare inodes of two directories (portable across macOS/Linux)
339
+ same_inode() {
340
+ local inode_a inode_b
341
+ if stat --version >/dev/null 2>&1; then
342
+ # GNU stat (Linux)
343
+ inode_a="$(stat -c '%i' "$1" 2>/dev/null)"
344
+ inode_b="$(stat -c '%i' "$2" 2>/dev/null)"
345
+ else
346
+ # BSD stat (macOS)
347
+ inode_a="$(stat -f '%i' "$1" 2>/dev/null)"
348
+ inode_b="$(stat -f '%i' "$2" 2>/dev/null)"
349
+ fi
350
+ [[ -n "$inode_a" ]] && [[ "$inode_a" == "$inode_b" ]]
351
+ }
352
+
353
+ for subdir in "${subdirs[@]}"; do
354
+ local legacy_dir="$PROJECT_ROOT/$subdir"
355
+ local agents_subdir="$agents_abs/$subdir"
356
+
357
+ # Skip if legacy dir doesn't exist or is already a symlink
358
+ if [[ ! -d "$legacy_dir" ]]; then
359
+ continue
360
+ fi
361
+ if [[ -L "$legacy_dir" ]]; then
362
+ info "$subdir/ is already a symlink — nothing to do."
363
+ continue
364
+ fi
365
+
366
+ # Detect same-inode (legacy dir IS .agents/subdir — e.g. hardlink or bind mount)
367
+ if [[ -d "$agents_subdir" ]] && same_inode "$legacy_dir" "$agents_subdir"; then
368
+ warn "$subdir/ and $AGENTS_DIR/$subdir/ are the same directory (same inode)."
369
+ warn "Replacing $subdir/ with a symlink to $AGENTS_DIR/$subdir/."
370
+ if [[ "$DRY_RUN" == "true" ]]; then
371
+ echo " would remove $subdir/ (same inode as $AGENTS_DIR/$subdir/)"
372
+ echo " would create symlink $subdir/ -> $AGENTS_DIR/$subdir"
373
+ else
374
+ rm -rf "$legacy_dir"
375
+ ln -s "$AGENTS_DIR/$subdir" "$legacy_dir"
376
+ info "Replaced $subdir/ with symlink -> $AGENTS_DIR/$subdir"
377
+ fi
378
+ fixed=$((fixed + 1))
379
+ continue
380
+ fi
381
+
382
+ info "Found legacy directory: $subdir/"
383
+ mkdir -p "$agents_subdir"
384
+
385
+ # Move each item from legacy dir into .agents/subdir
386
+ for item in "$legacy_dir"/*/; do
387
+ [[ -d "$item" ]] || continue
388
+ local name
389
+ name="$(basename "$item")"
390
+
391
+ if [[ -d "$agents_subdir/$name" ]]; then
392
+ if [[ "$no_clobber" == "true" ]]; then
393
+ warn "Skipping $subdir/$name — already exists in $AGENTS_DIR/$subdir/ (--no-clobber)"
394
+ skipped=$((skipped + 1))
395
+ continue
396
+ fi
397
+ # Merge: legacy content wins (overwrite into .agents/)
398
+ if [[ "$DRY_RUN" == "true" ]]; then
399
+ echo " would merge: $subdir/$name -> $AGENTS_DIR/$subdir/$name (overwrite)"
400
+ else
401
+ rm -rf "${agents_subdir:?}/${name:?}"
402
+ mv "$item" "$agents_subdir/$name"
403
+ info "Merged: $subdir/$name -> $AGENTS_DIR/$subdir/$name (overwrote existing)"
404
+ fi
405
+ merged=$((merged + 1))
406
+ fixed=$((fixed + 1))
407
+ continue
408
+ fi
409
+
410
+ if [[ "$DRY_RUN" == "true" ]]; then
411
+ echo " would move: $subdir/$name -> $AGENTS_DIR/$subdir/$name"
412
+ else
413
+ mv "$item" "$agents_subdir/$name"
414
+ info "Moved: $subdir/$name -> $AGENTS_DIR/$subdir/$name"
415
+ fi
416
+ fixed=$((fixed + 1))
417
+ done
418
+
419
+ # Also move any top-level files (e.g. loose .md rules)
420
+ for item in "$legacy_dir"/*; do
421
+ [[ -f "$item" ]] || continue
422
+ local name
423
+ name="$(basename "$item")"
424
+
425
+ if [[ -f "$agents_subdir/$name" ]]; then
426
+ if [[ "$no_clobber" == "true" ]]; then
427
+ warn "Skipping $subdir/$name — already exists in $AGENTS_DIR/$subdir/ (--no-clobber)"
428
+ skipped=$((skipped + 1))
429
+ continue
430
+ fi
431
+ # Merge: legacy content wins
432
+ if [[ "$DRY_RUN" == "true" ]]; then
433
+ echo " would merge: $subdir/$name -> $AGENTS_DIR/$subdir/$name (overwrite)"
434
+ else
435
+ mv "$item" "$agents_subdir/$name"
436
+ info "Merged: $subdir/$name -> $AGENTS_DIR/$subdir/$name (overwrote existing)"
437
+ fi
438
+ merged=$((merged + 1))
439
+ fixed=$((fixed + 1))
440
+ continue
441
+ fi
442
+
443
+ if [[ "$DRY_RUN" == "true" ]]; then
444
+ echo " would move: $subdir/$name -> $AGENTS_DIR/$subdir/$name"
445
+ else
446
+ mv "$item" "$agents_subdir/$name"
447
+ info "Moved: $subdir/$name -> $AGENTS_DIR/$subdir/$name"
448
+ fi
449
+ fixed=$((fixed + 1))
450
+ done
451
+
452
+ # Remove the now-empty legacy dir and replace with symlink
453
+ if [[ "$DRY_RUN" == "true" ]]; then
454
+ echo " would replace $subdir/ with symlink -> $AGENTS_DIR/$subdir"
455
+ else
456
+ # Check if dir is empty (only . and .. remain)
457
+ if [[ -z "$(ls -A "$legacy_dir" 2>/dev/null)" ]]; then
458
+ rmdir "$legacy_dir"
459
+ ln -s "$AGENTS_DIR/$subdir" "$legacy_dir"
460
+ info "Replaced $subdir/ with symlink -> $AGENTS_DIR/$subdir"
461
+ else
462
+ warn "$subdir/ is not empty after migration — skipping symlink replacement"
463
+ warn "Remaining items:"
464
+ find "$legacy_dir" -mindepth 1 -maxdepth 1 -exec basename {} \; | sed 's/^/ /'
465
+ fi
466
+ fi
467
+ done
468
+
469
+ # --- Phase 1b: Convert flat skill files to directory layout ---
470
+ # e.g. .agents/skills/foo.md -> .agents/skills/foo/SKILL.md
471
+ for subdir in "${subdirs[@]}"; do
472
+ [[ "$subdir" == "skills" ]] || continue
473
+ local skills_dir="$agents_abs/skills"
474
+ [[ -d "$skills_dir" ]] || continue
475
+
476
+ for flat_file in "$skills_dir"/*.md; do
477
+ [[ -f "$flat_file" ]] || continue
478
+ local name
479
+ name="$(basename "$flat_file" .md)"
480
+ local target_dir="$skills_dir/$name"
481
+ local target_file="$target_dir/SKILL.md"
482
+
483
+ if [[ -d "$target_dir" ]] && [[ -f "$target_file" ]]; then
484
+ if [[ "$no_clobber" == "true" ]]; then
485
+ warn "Skipping flat skill $name.md — $name/SKILL.md already exists (--no-clobber)"
486
+ skipped=$((skipped + 1))
487
+ continue
488
+ fi
489
+ # Flat file wins (same merge behavior as legacy migration)
490
+ if [[ "$DRY_RUN" == "true" ]]; then
491
+ echo " would convert: skills/$name.md -> skills/$name/SKILL.md (overwrite)"
492
+ else
493
+ mv "$flat_file" "$target_file"
494
+ info "Converted: skills/$name.md -> skills/$name/SKILL.md (overwrote existing)"
495
+ fi
496
+ merged=$((merged + 1))
497
+ fixed=$((fixed + 1))
498
+ continue
499
+ fi
500
+
501
+ if [[ "$DRY_RUN" == "true" ]]; then
502
+ echo " would convert: skills/$name.md -> skills/$name/SKILL.md"
503
+ else
504
+ mkdir -p "$target_dir"
505
+ mv "$flat_file" "$target_file"
506
+ info "Converted: skills/$name.md -> skills/$name/SKILL.md"
507
+ fi
508
+ fixed=$((fixed + 1))
509
+ done
510
+ done
511
+
512
+ # --- Phase 2: Repair broken/missing symlinks ---
513
+ local repaired=0
514
+
515
+ for target in "${ACTIVE_TARGETS[@]}"; do
516
+ local target_dir
517
+ target_dir="$(resolve_target_dir "$target" "$PROJECT_ROOT")"
518
+ local agents_rel
519
+ agents_rel="$(resolve_agents_rel "$target")"
520
+
521
+ for subdir in "${subdirs[@]}"; do
522
+ if [[ ! -d "$agents_abs/$subdir" ]]; then
523
+ continue
524
+ fi
525
+ local expected_link="$target_dir/$subdir"
526
+ local expected_source="$agents_rel/$subdir"
527
+
528
+ if [[ -L "$expected_link" ]]; then
529
+ local current_target
530
+ current_target="$(readlink "$expected_link")"
531
+ if [[ "$current_target" == "$expected_source" ]]; then
532
+ continue # Already correct
533
+ fi
534
+ # Symlink exists but points to wrong target
535
+ if [[ "$DRY_RUN" == "true" ]]; then
536
+ echo " would relink: $expected_link -> $expected_source (was $current_target)"
537
+ else
538
+ rm "$expected_link"
539
+ create_symlink "$expected_source" "$expected_link" "false"
540
+ info "Repaired: $expected_link -> $expected_source (was $current_target)"
541
+ fi
542
+ repaired=$((repaired + 1))
543
+ elif [[ -e "$expected_link" ]]; then
544
+ # Something exists but isn't a symlink — skip unless --force
545
+ if [[ "$FORCE" == "true" ]]; then
546
+ if [[ "$DRY_RUN" == "true" ]]; then
547
+ echo " would replace: $expected_link with symlink -> $expected_source"
548
+ else
549
+ rm -rf "$expected_link"
550
+ create_symlink "$expected_source" "$expected_link" "false"
551
+ info "Repaired: replaced $expected_link with symlink -> $expected_source"
552
+ fi
553
+ repaired=$((repaired + 1))
554
+ else
555
+ warn "$expected_link exists but is not a symlink (use --force to replace)"
556
+ fi
557
+ else
558
+ # Missing entirely
559
+ if [[ "$DRY_RUN" == "true" ]]; then
560
+ echo " would create: $expected_link -> $expected_source"
561
+ else
562
+ create_symlink "$expected_source" "$expected_link" "false"
563
+ fi
564
+ repaired=$((repaired + 1))
565
+ fi
566
+ done
567
+ done
568
+
569
+ # Repair CLAUDE.md symlink
570
+ if [[ -f "$PROJECT_ROOT/$AGENTS_MD" ]]; then
571
+ if [[ -L "$PROJECT_ROOT/CLAUDE.md" ]]; then
572
+ local current_target
573
+ current_target="$(readlink "$PROJECT_ROOT/CLAUDE.md")"
574
+ if [[ "$current_target" != "$AGENTS_MD" ]]; then
575
+ if [[ "$DRY_RUN" == "true" ]]; then
576
+ echo " would relink: CLAUDE.md -> $AGENTS_MD (was $current_target)"
577
+ else
578
+ rm "$PROJECT_ROOT/CLAUDE.md"
579
+ create_symlink "$AGENTS_MD" "$PROJECT_ROOT/CLAUDE.md" "false"
580
+ fi
581
+ repaired=$((repaired + 1))
582
+ fi
583
+ elif [[ ! -e "$PROJECT_ROOT/CLAUDE.md" ]]; then
584
+ if [[ "$DRY_RUN" == "true" ]]; then
585
+ echo " would create: CLAUDE.md -> $AGENTS_MD"
586
+ else
587
+ create_symlink "$AGENTS_MD" "$PROJECT_ROOT/CLAUDE.md" "false"
588
+ fi
589
+ repaired=$((repaired + 1))
590
+ fi
591
+ fi
592
+
593
+ # Summary
594
+ if [[ "$fixed" -eq 0 ]] && [[ "$skipped" -eq 0 ]] && [[ "$repaired" -eq 0 ]]; then
595
+ info "Nothing to fix — all directories and symlinks are correct."
596
+ else
597
+ if [[ "$fixed" -gt 0 ]]; then info "Fixed $fixed item(s)."; fi
598
+ if [[ "$merged" -gt 0 ]]; then info "Merged $merged item(s) (legacy overwrote existing)."; fi
599
+ if [[ "$skipped" -gt 0 ]]; then warn "Skipped $skipped item(s) (use without --no-clobber to merge)."; fi
600
+ if [[ "$repaired" -gt 0 ]]; then info "Repaired $repaired symlink(s)."; fi
601
+ if [[ "$fixed" -gt 0 ]]; then info "Run 'sync-agents sync' to update agent target symlinks."; fi
602
+ fi
603
+ }
604
+
290
605
  cmd_sync() {
291
606
  ensure_agents_dir
292
607
 
@@ -936,16 +1251,30 @@ HEADER
936
1251
  fi
937
1252
  echo ""
938
1253
 
939
- # Skills
1254
+ # Skills (directory layout: skills/name/SKILL.md, or legacy flat: skills/name.md)
940
1255
  echo "## Skills"
941
1256
  echo ""
1257
+ local has_skills="false"
1258
+ # Directory skills: skills/name/SKILL.md
1259
+ for d in "$agents_dir/skills/"*/; do
1260
+ [[ -d "$d" ]] || continue
1261
+ local name
1262
+ name="$(basename "$d")"
1263
+ if [[ -f "$d/SKILL.md" ]]; then
1264
+ echo "- [$name](.agents/skills/$name/SKILL.md)"
1265
+ has_skills="true"
1266
+ fi
1267
+ done
1268
+ # Legacy flat skills: skills/name.md
942
1269
  if compgen -G "$agents_dir/skills/*.md" > /dev/null 2>&1; then
943
1270
  for f in "$agents_dir/skills/"*.md; do
944
1271
  local name
945
1272
  name="$(basename "$f" .md)"
946
1273
  echo "- [$name](.agents/skills/$name.md)"
1274
+ has_skills="true"
947
1275
  done
948
- else
1276
+ fi
1277
+ if [[ "$has_skills" == "false" ]]; then
949
1278
  echo "_No skills defined yet. Add one with \`sync-agents add skill <name>\`._"
950
1279
  fi
951
1280
  echo ""
@@ -1124,6 +1453,9 @@ main() {
1124
1453
  import)
1125
1454
  cmd_import "$@"
1126
1455
  ;;
1456
+ fix)
1457
+ cmd_fix "$@"
1458
+ ;;
1127
1459
  hook)
1128
1460
  cmd_hook
1129
1461
  ;;