@brickhouse-tech/sync-agents 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +108 -0
- package/package.json +36 -0
- package/src/md/RULE_TEMPLATE.md +6 -0
- package/src/md/STATE_TEMPLATE.md +42 -0
- package/src/sh/sync-agents.sh +569 -0
package/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# sync-agents
|
|
2
|
+
|
|
3
|
+
One set of agent rules to rule them all. `sync-agents` keeps your AI coding agent configurations in a single `.agents/` directory and syncs them to agent-specific directories (`.claude/`, `.windsurf/`) via symlinks. This ensures all agents follow the same rules, skills, and workflows without duplicating files.
|
|
4
|
+
|
|
5
|
+
AGENTS.md serves as an auto-generated index of everything in `.agents/` and is symlinked to CLAUDE.md for Claude compatibility.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @brickhouse-tech/sync-agents
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
or globally:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install -g @brickhouse-tech/sync-agents
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Topology
|
|
20
|
+
|
|
21
|
+
`.agents/` is the source of truth. It contains all rules, skills, workflows, and state for your agents:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
.agents/
|
|
25
|
+
├── rules/
|
|
26
|
+
│ ├── rule1.md
|
|
27
|
+
│ ├── rule2.md
|
|
28
|
+
│ └── ...
|
|
29
|
+
├── skills/
|
|
30
|
+
│ ├── skill1.md
|
|
31
|
+
│ ├── skill2.md
|
|
32
|
+
│ └── ...
|
|
33
|
+
├── workflows/
|
|
34
|
+
│ ├── workflow1.md
|
|
35
|
+
│ ├── workflow2.md
|
|
36
|
+
│ └── ...
|
|
37
|
+
└── STATE.md
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Running `sync-agents sync` creates symlinks from `.agents/` subdirectories into `.claude/` and `.windsurf/`. Any changes to `.agents/` are automatically reflected in the target directories because they are symlinks, not copies.
|
|
41
|
+
|
|
42
|
+
AGENTS.md is also symlinked to CLAUDE.md so that Claude reads the index natively.
|
|
43
|
+
|
|
44
|
+
## STATE.md
|
|
45
|
+
|
|
46
|
+
`.agents/STATE.md` tracks the current state of your project from the agent's perspective. It serves as a resumption point after failures or interruptions -- the agent can read STATE.md to determine where it left off and what tasks remain. Update it regularly to keep agents in sync with progress.
|
|
47
|
+
|
|
48
|
+
## Commands
|
|
49
|
+
|
|
50
|
+
| Command | Description |
|
|
51
|
+
|---|---|
|
|
52
|
+
| `init` | Initialize the `.agents/` directory structure with `rules/`, `skills/`, `workflows/`, `STATE.md`, and generate `AGENTS.md` |
|
|
53
|
+
| `sync` | Create symlinks from `.agents/` into `.claude/` and `.windsurf/`, and symlink `AGENTS.md` to `CLAUDE.md` |
|
|
54
|
+
| `status` | Show the current sync status of all targets and symlinks |
|
|
55
|
+
| `add <type> <name>` | Add a new rule, skill, or workflow from a template (type is `rule`, `skill`, or `workflow`) |
|
|
56
|
+
| `index` | Regenerate `AGENTS.md` by scanning the contents of `.agents/` |
|
|
57
|
+
| `clean` | Remove all synced symlinks and empty target directories (does not remove `.agents/`) |
|
|
58
|
+
|
|
59
|
+
## Options
|
|
60
|
+
|
|
61
|
+
| Option | Description |
|
|
62
|
+
|---|---|
|
|
63
|
+
| `-h`, `--help` | Show help message |
|
|
64
|
+
| `-v`, `--version` | Show version |
|
|
65
|
+
| `-d`, `--dir <path>` | Set project root directory (default: current directory) |
|
|
66
|
+
| `--targets <list>` | Comma-separated list of sync targets (default: `claude,windsurf`) |
|
|
67
|
+
| `--dry-run` | Show what would be done without making changes |
|
|
68
|
+
| `--force` | Overwrite existing files and symlinks |
|
|
69
|
+
|
|
70
|
+
## Usage
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# Initialize .agents/ structure in the current project
|
|
74
|
+
sync-agents init
|
|
75
|
+
|
|
76
|
+
# Add a new rule
|
|
77
|
+
sync-agents add rule no-eval
|
|
78
|
+
|
|
79
|
+
# Add a new skill
|
|
80
|
+
sync-agents add skill debugging
|
|
81
|
+
|
|
82
|
+
# Add a new workflow
|
|
83
|
+
sync-agents add workflow deploy
|
|
84
|
+
|
|
85
|
+
# Sync to all targets (.claude/ and .windsurf/)
|
|
86
|
+
sync-agents sync
|
|
87
|
+
|
|
88
|
+
# Sync to a specific target only
|
|
89
|
+
sync-agents sync --targets claude
|
|
90
|
+
|
|
91
|
+
# Preview sync without making changes
|
|
92
|
+
sync-agents sync --dry-run
|
|
93
|
+
|
|
94
|
+
# Force overwrite existing symlinks
|
|
95
|
+
sync-agents sync --force
|
|
96
|
+
|
|
97
|
+
# Check sync status
|
|
98
|
+
sync-agents status
|
|
99
|
+
|
|
100
|
+
# Regenerate the AGENTS.md index
|
|
101
|
+
sync-agents index
|
|
102
|
+
|
|
103
|
+
# Remove all synced symlinks
|
|
104
|
+
sync-agents clean
|
|
105
|
+
|
|
106
|
+
# Work in a different directory
|
|
107
|
+
sync-agents sync --dir /path/to/project
|
|
108
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@brickhouse-tech/sync-agents",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "AWS CFT Utilities to fill the gaps for Cloud Formation",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cft",
|
|
7
|
+
"aws",
|
|
8
|
+
"cloudformation"
|
|
9
|
+
],
|
|
10
|
+
"homepage": "https://github.com/brickhouse-tech/sync-agents#readme",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/brickhouse-tech/sync-agents/issues"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/brickhouse-tech/sync-agents.git"
|
|
17
|
+
},
|
|
18
|
+
"author": "Nicholas McCready",
|
|
19
|
+
"bin": {
|
|
20
|
+
"sync-agents": "src/sh/sync-agents.sh"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"src/**/*"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"test": "npx bats test/"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@commitlint/cli": "^20",
|
|
32
|
+
"@commitlint/config-conventional": "^20",
|
|
33
|
+
"commitlint": "20",
|
|
34
|
+
"sort-package-json": "3.6"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
|
|
2
|
+
---
|
|
3
|
+
trigger: always_on
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# State
|
|
7
|
+
|
|
8
|
+
This is the state template for sync-agents. It is used to define the state of the agent and its environment. The state markdown file itself is a human readble source for the agent to read and understand its current situation. It can be used to track the progress of the agent, identify any issues or challenges it may be facing, and provide a clear overview of its goals and objectives. The state is an important tool for the agent to use in order to make informed decisions and take appropriate actions to achieve its goals. By maintaining an up-to-date state, the agent can continuously learn and evolve, becoming more effective and efficient in its tasks.
|
|
9
|
+
|
|
10
|
+
The goal is to improve the performance of the agent by providing it with a clear and structured representation of its current situation. This allows the agent to make informed decisions and take appropriate actions to achieve its goals. The state is also used to track the progress of the agent and identify any issues or areas for improvement. By maintaining an up-to-date state, the agent can continuously learn and evolve, becoming more effective and efficient in its xtasks.
|
|
11
|
+
|
|
12
|
+
## TRACKING Agent State
|
|
13
|
+
|
|
14
|
+
Every state tracking will begin with a header of `### YYYYMMDDHHMMSS STATE: <STATE_NAME or OBJECTIVE` followed by a description of the state, any relevant information about the agent's performance, issues, or updates, and any actions taken or planned to address the current state. This format allows for easy tracking and monitoring of the agent's progress over time, as well as providing a clear record of the agent's history and development. By maintaining a detailed and organized state history, the agent can learn from past experiences and make informed decisions to improve its performance in the future.>
|
|
15
|
+
|
|
16
|
+
The format above will be written below the `## STATE HISTORY BELOW` header in the STATE.md file. This allows for easy tracking and monitoring of the agent's progress over time, as well as providing a clear record of the agent's history and development. By maintaining a detailed and organized state history, the agent can learn from past experiences and make informed decisions to improve its performance in the future.
|
|
17
|
+
|
|
18
|
+
## Formatted Agent State
|
|
19
|
+
|
|
20
|
+
The formatted agent state will be a structured representation of the agent's current situation, including its goals, objectives, performance metrics, and any relevant information about its environment. This formatted state will be used by the agent to make informed decisions and take appropriate actions to achieve its goals. By maintaining an up-to-date and well-structured formatted state, the agent can continuously learn and evolve, becoming more effective and efficient in its tasks. The formatted state will also be used to track the progress of the agent and identify any issues or areas for improvement, allowing for continuous optimization of the agent's performance.
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
|
|
24
|
+
The state is structured as follows:
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"agent_name": "string",
|
|
29
|
+
"goals": ["string"],
|
|
30
|
+
"skills": ["string"],
|
|
31
|
+
"workflows": ["string"],
|
|
32
|
+
"issues": ["string"],
|
|
33
|
+
"last_updated": "timestamp"
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
to `.agents/state.json`
|
|
37
|
+
|
|
38
|
+
## STATE HISTORY BELOW
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
AGENTS_DIR=".agents"
|
|
5
|
+
AGENTS_MD="AGENTS.md"
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
+
PACKAGE_JSON="${SCRIPT_DIR}/../../package.json"
|
|
8
|
+
TEMPLATES_DIR="${SCRIPT_DIR}/../md"
|
|
9
|
+
|
|
10
|
+
# Pull version from package.json
|
|
11
|
+
if [[ -f "$PACKAGE_JSON" ]]; then
|
|
12
|
+
VERSION="$(sed -n 's/.*"version": *"\([^"]*\)".*/\1/p' "$PACKAGE_JSON" | head -1)"
|
|
13
|
+
else
|
|
14
|
+
VERSION="unknown"
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
# Agent target directories
|
|
18
|
+
TARGETS=("claude" "windsurf")
|
|
19
|
+
|
|
20
|
+
# Colors (disabled if not a terminal)
|
|
21
|
+
if [[ -t 1 ]]; then
|
|
22
|
+
BOLD='\033[1m'
|
|
23
|
+
GREEN='\033[0;32m'
|
|
24
|
+
YELLOW='\033[0;33m'
|
|
25
|
+
RED='\033[0;31m'
|
|
26
|
+
CYAN='\033[0;36m'
|
|
27
|
+
RESET='\033[0m'
|
|
28
|
+
else
|
|
29
|
+
BOLD='' GREEN='' YELLOW='' RED='' CYAN='' RESET=''
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
info() { echo -e "${GREEN}[info]${RESET} $*"; }
|
|
33
|
+
warn() { echo -e "${YELLOW}[warn]${RESET} $*"; }
|
|
34
|
+
error() { echo -e "${RED}[error]${RESET} $*" >&2; }
|
|
35
|
+
|
|
36
|
+
usage() {
|
|
37
|
+
cat <<EOF
|
|
38
|
+
${BOLD}sync-agents${RESET} v${VERSION} - One set of agent rules to rule them all.
|
|
39
|
+
|
|
40
|
+
${BOLD}USAGE${RESET}
|
|
41
|
+
sync-agents <command> [options]
|
|
42
|
+
|
|
43
|
+
${BOLD}COMMANDS${RESET}
|
|
44
|
+
init Initialize .agents/ directory structure and AGENTS.md
|
|
45
|
+
sync Sync .agents/ to .claude/ and .windsurf/ via symlinks
|
|
46
|
+
status Show current sync status
|
|
47
|
+
add <type> <name> Add a new rule, skill, or workflow from template
|
|
48
|
+
index Regenerate AGENTS.md index from .agents/ contents
|
|
49
|
+
clean Remove all synced symlinks (does not remove .agents/)
|
|
50
|
+
|
|
51
|
+
${BOLD}OPTIONS${RESET}
|
|
52
|
+
-h, --help Show this help message
|
|
53
|
+
-v, --version Show version
|
|
54
|
+
-d, --dir <path> Set project root directory (default: current directory)
|
|
55
|
+
--targets <list> Comma-separated targets to sync (default: claude,windsurf)
|
|
56
|
+
--dry-run Show what would be done without making changes
|
|
57
|
+
--force Overwrite existing files/symlinks
|
|
58
|
+
|
|
59
|
+
${BOLD}EXAMPLES${RESET}
|
|
60
|
+
sync-agents init # Initialize .agents/ structure
|
|
61
|
+
sync-agents add rule no-eval # Add a new rule called "no-eval"
|
|
62
|
+
sync-agents sync # Sync to .claude/ and .windsurf/
|
|
63
|
+
sync-agents sync --targets claude
|
|
64
|
+
sync-agents status # Show current state
|
|
65
|
+
sync-agents clean # Remove synced symlinks
|
|
66
|
+
|
|
67
|
+
EOF
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# --------------------------------------------------------------------------
|
|
71
|
+
# Helpers
|
|
72
|
+
# --------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
find_project_root() {
|
|
75
|
+
local dir="${1:-.}"
|
|
76
|
+
dir="$(cd "$dir" && pwd)"
|
|
77
|
+
|
|
78
|
+
# Walk up to find a directory with .agents or .git
|
|
79
|
+
while [[ "$dir" != "/" ]]; do
|
|
80
|
+
if [[ -d "$dir/$AGENTS_DIR" ]] || [[ -d "$dir/.git" ]]; then
|
|
81
|
+
echo "$dir"
|
|
82
|
+
return 0
|
|
83
|
+
fi
|
|
84
|
+
dir="$(dirname "$dir")"
|
|
85
|
+
done
|
|
86
|
+
|
|
87
|
+
# Fallback to current directory
|
|
88
|
+
pwd
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
ensure_agents_dir() {
|
|
92
|
+
if [[ ! -d "$PROJECT_ROOT/$AGENTS_DIR" ]]; then
|
|
93
|
+
error ".agents/ directory not found. Run 'sync-agents init' first."
|
|
94
|
+
exit 1
|
|
95
|
+
fi
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
create_symlink() {
|
|
99
|
+
local source="$1"
|
|
100
|
+
local target="$2"
|
|
101
|
+
local dry_run="${3:-false}"
|
|
102
|
+
|
|
103
|
+
if [[ "$dry_run" == "true" ]]; then
|
|
104
|
+
echo " would link: $target -> $source"
|
|
105
|
+
return 0
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
local target_dir
|
|
109
|
+
target_dir="$(dirname "$target")"
|
|
110
|
+
mkdir -p "$target_dir"
|
|
111
|
+
|
|
112
|
+
if [[ -L "$target" ]]; then
|
|
113
|
+
local existing
|
|
114
|
+
existing="$(readlink "$target")"
|
|
115
|
+
if [[ "$existing" == "$source" ]]; then
|
|
116
|
+
return 0 # Already correct
|
|
117
|
+
fi
|
|
118
|
+
if [[ "$FORCE" == "true" ]]; then
|
|
119
|
+
rm "$target"
|
|
120
|
+
else
|
|
121
|
+
warn "Symlink already exists: $target -> $existing (use --force to overwrite)"
|
|
122
|
+
return 1
|
|
123
|
+
fi
|
|
124
|
+
elif [[ -e "$target" ]]; then
|
|
125
|
+
if [[ "$FORCE" == "true" ]]; then
|
|
126
|
+
rm -rf "$target"
|
|
127
|
+
else
|
|
128
|
+
warn "File already exists: $target (use --force to overwrite)"
|
|
129
|
+
return 1
|
|
130
|
+
fi
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
ln -sf "$source" "$target"
|
|
134
|
+
info "Linked: $target -> $source"
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
# --------------------------------------------------------------------------
|
|
138
|
+
# Commands
|
|
139
|
+
# --------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
cmd_init() {
|
|
142
|
+
info "Initializing $AGENTS_DIR/ directory structure..."
|
|
143
|
+
|
|
144
|
+
mkdir -p "$PROJECT_ROOT/$AGENTS_DIR/rules"
|
|
145
|
+
mkdir -p "$PROJECT_ROOT/$AGENTS_DIR/skills"
|
|
146
|
+
mkdir -p "$PROJECT_ROOT/$AGENTS_DIR/workflows"
|
|
147
|
+
|
|
148
|
+
# Copy STATE.md template if STATE.md doesn't exist
|
|
149
|
+
if [[ ! -f "$PROJECT_ROOT/$AGENTS_DIR/STATE.md" ]]; then
|
|
150
|
+
if [[ -f "$TEMPLATES_DIR/STATE_TEMPLATE.md" ]]; then
|
|
151
|
+
cp "$TEMPLATES_DIR/STATE_TEMPLATE.md" "$PROJECT_ROOT/$AGENTS_DIR/STATE.md"
|
|
152
|
+
info "Created $AGENTS_DIR/STATE.md from template"
|
|
153
|
+
else
|
|
154
|
+
# Inline fallback if template not found
|
|
155
|
+
cat > "$PROJECT_ROOT/$AGENTS_DIR/STATE.md" <<'STATE_EOF'
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
trigger: always_on
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
# State
|
|
162
|
+
|
|
163
|
+
## STATE HISTORY BELOW
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
STATE_EOF
|
|
167
|
+
info "Created $AGENTS_DIR/STATE.md"
|
|
168
|
+
fi
|
|
169
|
+
else
|
|
170
|
+
warn "$AGENTS_DIR/STATE.md already exists, skipping"
|
|
171
|
+
fi
|
|
172
|
+
|
|
173
|
+
# Generate AGENTS.md if it doesn't exist
|
|
174
|
+
if [[ ! -f "$PROJECT_ROOT/$AGENTS_MD" ]]; then
|
|
175
|
+
generate_agents_md
|
|
176
|
+
info "Created $AGENTS_MD"
|
|
177
|
+
else
|
|
178
|
+
warn "$AGENTS_MD already exists, skipping (run 'sync-agents index' to regenerate)"
|
|
179
|
+
fi
|
|
180
|
+
|
|
181
|
+
info "Initialization complete. Directory structure:"
|
|
182
|
+
print_tree "$PROJECT_ROOT/$AGENTS_DIR"
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
cmd_add() {
|
|
186
|
+
local type="${1:-}"
|
|
187
|
+
local name="${2:-}"
|
|
188
|
+
|
|
189
|
+
if [[ -z "$type" ]] || [[ -z "$name" ]]; then
|
|
190
|
+
error "Usage: sync-agents add <rule|skill|workflow> <name>"
|
|
191
|
+
exit 1
|
|
192
|
+
fi
|
|
193
|
+
|
|
194
|
+
case "$type" in
|
|
195
|
+
rule|rules) type="rules" ;;
|
|
196
|
+
skill|skills) type="skills" ;;
|
|
197
|
+
workflow|workflows) type="workflows" ;;
|
|
198
|
+
*)
|
|
199
|
+
error "Unknown type: $type. Must be one of: rule, skill, workflow"
|
|
200
|
+
exit 1
|
|
201
|
+
;;
|
|
202
|
+
esac
|
|
203
|
+
|
|
204
|
+
ensure_agents_dir
|
|
205
|
+
|
|
206
|
+
local filepath="$PROJECT_ROOT/$AGENTS_DIR/$type/$name.md"
|
|
207
|
+
|
|
208
|
+
if [[ -f "$filepath" ]] && [[ "$FORCE" != "true" ]]; then
|
|
209
|
+
error "File already exists: $filepath (use --force to overwrite)"
|
|
210
|
+
exit 1
|
|
211
|
+
fi
|
|
212
|
+
|
|
213
|
+
# Use RULE_TEMPLATE for all types, substituting name
|
|
214
|
+
if [[ -f "$TEMPLATES_DIR/RULE_TEMPLATE.md" ]]; then
|
|
215
|
+
sed "s/\${NAME}/$name/g" "$TEMPLATES_DIR/RULE_TEMPLATE.md" > "$filepath"
|
|
216
|
+
else
|
|
217
|
+
cat > "$filepath" <<TMPL_EOF
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
trigger: always_on
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
# $name
|
|
224
|
+
TMPL_EOF
|
|
225
|
+
fi
|
|
226
|
+
|
|
227
|
+
info "Created $type: $filepath"
|
|
228
|
+
|
|
229
|
+
# Regenerate index
|
|
230
|
+
generate_agents_md
|
|
231
|
+
info "Updated $AGENTS_MD index"
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
cmd_sync() {
|
|
235
|
+
ensure_agents_dir
|
|
236
|
+
|
|
237
|
+
local agents_abs
|
|
238
|
+
agents_abs="$(cd "$PROJECT_ROOT/$AGENTS_DIR" && pwd)"
|
|
239
|
+
|
|
240
|
+
info "Syncing $AGENTS_DIR/ to agent directories..."
|
|
241
|
+
|
|
242
|
+
for target in "${ACTIVE_TARGETS[@]}"; do
|
|
243
|
+
local target_dir="$PROJECT_ROOT/.$target"
|
|
244
|
+
info "Syncing to .${target}/"
|
|
245
|
+
|
|
246
|
+
# Sync subdirectories: rules, skills, workflows
|
|
247
|
+
for subdir in rules skills workflows; do
|
|
248
|
+
if [[ -d "$agents_abs/$subdir" ]]; then
|
|
249
|
+
local source_rel
|
|
250
|
+
source_rel="$(python3 -c "import os.path; print(os.path.relpath('$agents_abs/$subdir', '$(dirname "$target_dir/$subdir")'))" 2>/dev/null || echo "../$AGENTS_DIR/$subdir")"
|
|
251
|
+
create_symlink "$source_rel" "$target_dir/$subdir" "$DRY_RUN"
|
|
252
|
+
fi
|
|
253
|
+
done
|
|
254
|
+
done
|
|
255
|
+
|
|
256
|
+
# Symlink AGENTS.md -> CLAUDE.md
|
|
257
|
+
if [[ -f "$PROJECT_ROOT/$AGENTS_MD" ]]; then
|
|
258
|
+
create_symlink "$AGENTS_MD" "$PROJECT_ROOT/CLAUDE.md" "$DRY_RUN"
|
|
259
|
+
fi
|
|
260
|
+
|
|
261
|
+
info "Sync complete."
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
cmd_status() {
|
|
265
|
+
echo -e "${BOLD}sync-agents${RESET} v${VERSION}"
|
|
266
|
+
echo ""
|
|
267
|
+
|
|
268
|
+
# Check .agents/ directory
|
|
269
|
+
if [[ -d "$PROJECT_ROOT/$AGENTS_DIR" ]]; then
|
|
270
|
+
echo -e "${GREEN}[ok]${RESET} $AGENTS_DIR/ exists"
|
|
271
|
+
print_tree "$PROJECT_ROOT/$AGENTS_DIR"
|
|
272
|
+
else
|
|
273
|
+
echo -e "${RED}[missing]${RESET} $AGENTS_DIR/ not found"
|
|
274
|
+
fi
|
|
275
|
+
|
|
276
|
+
echo ""
|
|
277
|
+
|
|
278
|
+
# Check AGENTS.md
|
|
279
|
+
if [[ -f "$PROJECT_ROOT/$AGENTS_MD" ]]; then
|
|
280
|
+
echo -e "${GREEN}[ok]${RESET} $AGENTS_MD exists"
|
|
281
|
+
else
|
|
282
|
+
echo -e "${RED}[missing]${RESET} $AGENTS_MD not found"
|
|
283
|
+
fi
|
|
284
|
+
|
|
285
|
+
# Check CLAUDE.md symlink
|
|
286
|
+
if [[ -L "$PROJECT_ROOT/CLAUDE.md" ]]; then
|
|
287
|
+
local link_target
|
|
288
|
+
link_target="$(readlink "$PROJECT_ROOT/CLAUDE.md")"
|
|
289
|
+
echo -e "${GREEN}[ok]${RESET} CLAUDE.md -> $link_target"
|
|
290
|
+
elif [[ -f "$PROJECT_ROOT/CLAUDE.md" ]]; then
|
|
291
|
+
echo -e "${YELLOW}[warn]${RESET} CLAUDE.md exists but is not a symlink"
|
|
292
|
+
else
|
|
293
|
+
echo -e "${RED}[missing]${RESET} CLAUDE.md not found"
|
|
294
|
+
fi
|
|
295
|
+
|
|
296
|
+
echo ""
|
|
297
|
+
|
|
298
|
+
# Check each target
|
|
299
|
+
for target in "${TARGETS[@]}"; do
|
|
300
|
+
local target_dir="$PROJECT_ROOT/.$target"
|
|
301
|
+
if [[ -d "$target_dir" ]] || [[ -L "$target_dir/rules" ]]; then
|
|
302
|
+
echo -e "${CYAN}.$target/${RESET}"
|
|
303
|
+
for subdir in rules skills workflows; do
|
|
304
|
+
if [[ -L "$target_dir/$subdir" ]]; then
|
|
305
|
+
local link_target
|
|
306
|
+
link_target="$(readlink "$target_dir/$subdir")"
|
|
307
|
+
echo -e " ${GREEN}[synced]${RESET} $subdir -> $link_target"
|
|
308
|
+
elif [[ -d "$target_dir/$subdir" ]]; then
|
|
309
|
+
echo -e " ${YELLOW}[local]${RESET} $subdir (not symlinked)"
|
|
310
|
+
else
|
|
311
|
+
echo -e " ${RED}[missing]${RESET} $subdir"
|
|
312
|
+
fi
|
|
313
|
+
done
|
|
314
|
+
else
|
|
315
|
+
echo -e "${RED}[not synced]${RESET} .$target/"
|
|
316
|
+
fi
|
|
317
|
+
done
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
cmd_index() {
|
|
321
|
+
ensure_agents_dir
|
|
322
|
+
generate_agents_md
|
|
323
|
+
info "Regenerated $AGENTS_MD"
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
cmd_clean() {
|
|
327
|
+
info "Removing synced symlinks..."
|
|
328
|
+
|
|
329
|
+
for target in "${ACTIVE_TARGETS[@]}"; do
|
|
330
|
+
local target_dir="$PROJECT_ROOT/.$target"
|
|
331
|
+
for subdir in rules skills workflows; do
|
|
332
|
+
if [[ -L "$target_dir/$subdir" ]]; then
|
|
333
|
+
rm "$target_dir/$subdir"
|
|
334
|
+
info "Removed: .$target/$subdir"
|
|
335
|
+
fi
|
|
336
|
+
done
|
|
337
|
+
|
|
338
|
+
# Remove target dir if empty
|
|
339
|
+
if [[ -d "$target_dir" ]] && [[ -z "$(ls -A "$target_dir" 2>/dev/null)" ]]; then
|
|
340
|
+
rmdir "$target_dir"
|
|
341
|
+
info "Removed empty directory: .$target/"
|
|
342
|
+
fi
|
|
343
|
+
done
|
|
344
|
+
|
|
345
|
+
# Remove CLAUDE.md symlink
|
|
346
|
+
if [[ -L "$PROJECT_ROOT/CLAUDE.md" ]]; then
|
|
347
|
+
rm "$PROJECT_ROOT/CLAUDE.md"
|
|
348
|
+
info "Removed: CLAUDE.md symlink"
|
|
349
|
+
fi
|
|
350
|
+
|
|
351
|
+
info "Clean complete."
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
# --------------------------------------------------------------------------
|
|
355
|
+
# Index generator
|
|
356
|
+
# --------------------------------------------------------------------------
|
|
357
|
+
|
|
358
|
+
generate_agents_md() {
|
|
359
|
+
local outfile="$PROJECT_ROOT/$AGENTS_MD"
|
|
360
|
+
local agents_dir="$PROJECT_ROOT/$AGENTS_DIR"
|
|
361
|
+
|
|
362
|
+
cat > "$outfile" <<'HEADER'
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
trigger: always_on
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
# AGENTS
|
|
369
|
+
|
|
370
|
+
> Auto-generated by [sync-agents](https://github.com/brickhouse-tech/sync-agents). Do not edit manually.
|
|
371
|
+
> Run `sync-agents index` to regenerate.
|
|
372
|
+
|
|
373
|
+
This file indexes all rules, skills, and workflows defined in `.agents/`.
|
|
374
|
+
|
|
375
|
+
HEADER
|
|
376
|
+
|
|
377
|
+
# Rules
|
|
378
|
+
echo "## Rules" >> "$outfile"
|
|
379
|
+
echo "" >> "$outfile"
|
|
380
|
+
if compgen -G "$agents_dir/rules/*.md" > /dev/null 2>&1; then
|
|
381
|
+
for f in "$agents_dir/rules/"*.md; do
|
|
382
|
+
local name
|
|
383
|
+
name="$(basename "$f" .md)"
|
|
384
|
+
echo "- [$name](.agents/rules/$name.md)" >> "$outfile"
|
|
385
|
+
done
|
|
386
|
+
else
|
|
387
|
+
echo "_No rules defined yet. Add one with \`sync-agents add rule <name>\`._" >> "$outfile"
|
|
388
|
+
fi
|
|
389
|
+
echo "" >> "$outfile"
|
|
390
|
+
|
|
391
|
+
# Skills
|
|
392
|
+
echo "## Skills" >> "$outfile"
|
|
393
|
+
echo "" >> "$outfile"
|
|
394
|
+
if compgen -G "$agents_dir/skills/*.md" > /dev/null 2>&1; then
|
|
395
|
+
for f in "$agents_dir/skills/"*.md; do
|
|
396
|
+
local name
|
|
397
|
+
name="$(basename "$f" .md)"
|
|
398
|
+
echo "- [$name](.agents/skills/$name.md)" >> "$outfile"
|
|
399
|
+
done
|
|
400
|
+
else
|
|
401
|
+
echo "_No skills defined yet. Add one with \`sync-agents add skill <name>\`._" >> "$outfile"
|
|
402
|
+
fi
|
|
403
|
+
echo "" >> "$outfile"
|
|
404
|
+
|
|
405
|
+
# Workflows
|
|
406
|
+
echo "## Workflows" >> "$outfile"
|
|
407
|
+
echo "" >> "$outfile"
|
|
408
|
+
if compgen -G "$agents_dir/workflows/*.md" > /dev/null 2>&1; then
|
|
409
|
+
for f in "$agents_dir/workflows/"*.md; do
|
|
410
|
+
local name
|
|
411
|
+
name="$(basename "$f" .md)"
|
|
412
|
+
echo "- [$name](.agents/workflows/$name.md)" >> "$outfile"
|
|
413
|
+
done
|
|
414
|
+
else
|
|
415
|
+
echo "_No workflows defined yet. Add one with \`sync-agents add workflow <name>\`._" >> "$outfile"
|
|
416
|
+
fi
|
|
417
|
+
echo "" >> "$outfile"
|
|
418
|
+
|
|
419
|
+
# State reference
|
|
420
|
+
echo "## State" >> "$outfile"
|
|
421
|
+
echo "" >> "$outfile"
|
|
422
|
+
echo "- [STATE.md](.agents/STATE.md)" >> "$outfile"
|
|
423
|
+
echo "" >> "$outfile"
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
# --------------------------------------------------------------------------
|
|
427
|
+
# Tree printer (lightweight, no dependency on `tree`)
|
|
428
|
+
# --------------------------------------------------------------------------
|
|
429
|
+
|
|
430
|
+
print_tree() {
|
|
431
|
+
local dir="$1"
|
|
432
|
+
local prefix="${2:-}"
|
|
433
|
+
local entries=()
|
|
434
|
+
|
|
435
|
+
# Collect entries
|
|
436
|
+
while IFS= read -r entry; do
|
|
437
|
+
entries+=("$entry")
|
|
438
|
+
done < <(ls -1A "$dir" 2>/dev/null | sort)
|
|
439
|
+
|
|
440
|
+
local count=${#entries[@]}
|
|
441
|
+
if [[ $count -eq 0 ]]; then
|
|
442
|
+
return 0
|
|
443
|
+
fi
|
|
444
|
+
local i=0
|
|
445
|
+
|
|
446
|
+
for entry in "${entries[@]}"; do
|
|
447
|
+
i=$((i + 1))
|
|
448
|
+
local connector="├── "
|
|
449
|
+
local child_prefix="│ "
|
|
450
|
+
if [[ $i -eq $count ]]; then
|
|
451
|
+
connector="└── "
|
|
452
|
+
child_prefix=" "
|
|
453
|
+
fi
|
|
454
|
+
|
|
455
|
+
if [[ -d "$dir/$entry" ]]; then
|
|
456
|
+
echo "${prefix}${connector}${entry}/"
|
|
457
|
+
print_tree "$dir/$entry" "${prefix}${child_prefix}"
|
|
458
|
+
elif [[ -L "$dir/$entry" ]]; then
|
|
459
|
+
local link_target
|
|
460
|
+
link_target="$(readlink "$dir/$entry")"
|
|
461
|
+
echo "${prefix}${connector}${entry} -> ${link_target}"
|
|
462
|
+
else
|
|
463
|
+
echo "${prefix}${connector}${entry}"
|
|
464
|
+
fi
|
|
465
|
+
done
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
# --------------------------------------------------------------------------
|
|
469
|
+
# Main
|
|
470
|
+
# --------------------------------------------------------------------------
|
|
471
|
+
|
|
472
|
+
main() {
|
|
473
|
+
local command=""
|
|
474
|
+
local custom_dir=""
|
|
475
|
+
local custom_targets=""
|
|
476
|
+
DRY_RUN="false"
|
|
477
|
+
FORCE="false"
|
|
478
|
+
|
|
479
|
+
# Parse arguments
|
|
480
|
+
while [[ $# -gt 0 ]]; do
|
|
481
|
+
case "$1" in
|
|
482
|
+
-h|--help)
|
|
483
|
+
usage
|
|
484
|
+
exit 0
|
|
485
|
+
;;
|
|
486
|
+
-v|--version)
|
|
487
|
+
echo "sync-agents v${VERSION}"
|
|
488
|
+
exit 0
|
|
489
|
+
;;
|
|
490
|
+
-d|--dir)
|
|
491
|
+
custom_dir="$2"
|
|
492
|
+
shift 2
|
|
493
|
+
;;
|
|
494
|
+
--targets)
|
|
495
|
+
custom_targets="$2"
|
|
496
|
+
shift 2
|
|
497
|
+
;;
|
|
498
|
+
--dry-run)
|
|
499
|
+
DRY_RUN="true"
|
|
500
|
+
shift
|
|
501
|
+
;;
|
|
502
|
+
--force)
|
|
503
|
+
FORCE="true"
|
|
504
|
+
shift
|
|
505
|
+
;;
|
|
506
|
+
-*)
|
|
507
|
+
error "Unknown option: $1"
|
|
508
|
+
usage
|
|
509
|
+
exit 1
|
|
510
|
+
;;
|
|
511
|
+
*)
|
|
512
|
+
if [[ -z "$command" ]]; then
|
|
513
|
+
command="$1"
|
|
514
|
+
else
|
|
515
|
+
# Collect remaining args for subcommands
|
|
516
|
+
break
|
|
517
|
+
fi
|
|
518
|
+
shift
|
|
519
|
+
;;
|
|
520
|
+
esac
|
|
521
|
+
done
|
|
522
|
+
|
|
523
|
+
# Resolve project root
|
|
524
|
+
if [[ -n "$custom_dir" ]]; then
|
|
525
|
+
PROJECT_ROOT="$(cd "$custom_dir" && pwd)"
|
|
526
|
+
else
|
|
527
|
+
PROJECT_ROOT="$(find_project_root)"
|
|
528
|
+
fi
|
|
529
|
+
|
|
530
|
+
# Resolve active targets
|
|
531
|
+
if [[ -n "$custom_targets" ]]; then
|
|
532
|
+
IFS=',' read -ra ACTIVE_TARGETS <<< "$custom_targets"
|
|
533
|
+
else
|
|
534
|
+
ACTIVE_TARGETS=("${TARGETS[@]}")
|
|
535
|
+
fi
|
|
536
|
+
|
|
537
|
+
# Dispatch command
|
|
538
|
+
case "${command:-}" in
|
|
539
|
+
init)
|
|
540
|
+
cmd_init
|
|
541
|
+
;;
|
|
542
|
+
sync)
|
|
543
|
+
cmd_sync
|
|
544
|
+
;;
|
|
545
|
+
status)
|
|
546
|
+
cmd_status
|
|
547
|
+
;;
|
|
548
|
+
add)
|
|
549
|
+
cmd_add "$@"
|
|
550
|
+
;;
|
|
551
|
+
index)
|
|
552
|
+
cmd_index
|
|
553
|
+
;;
|
|
554
|
+
clean)
|
|
555
|
+
cmd_clean
|
|
556
|
+
;;
|
|
557
|
+
"")
|
|
558
|
+
usage
|
|
559
|
+
exit 0
|
|
560
|
+
;;
|
|
561
|
+
*)
|
|
562
|
+
error "Unknown command: $command"
|
|
563
|
+
usage
|
|
564
|
+
exit 1
|
|
565
|
+
;;
|
|
566
|
+
esac
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
main "$@"
|