specrails 0.2.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/.claude/skills/openspec-apply-change/SKILL.md +156 -0
- package/.claude/skills/openspec-archive-change/SKILL.md +114 -0
- package/.claude/skills/openspec-bulk-archive-change/SKILL.md +246 -0
- package/.claude/skills/openspec-continue-change/SKILL.md +118 -0
- package/.claude/skills/openspec-explore/SKILL.md +290 -0
- package/.claude/skills/openspec-ff-change/SKILL.md +101 -0
- package/.claude/skills/openspec-new-change/SKILL.md +74 -0
- package/.claude/skills/openspec-onboard/SKILL.md +529 -0
- package/.claude/skills/openspec-sync-specs/SKILL.md +138 -0
- package/.claude/skills/openspec-verify-change/SKILL.md +168 -0
- package/README.md +226 -0
- package/VERSION +1 -0
- package/bin/specrails.js +41 -0
- package/commands/setup.md +851 -0
- package/install.sh +488 -0
- package/package.json +34 -0
- package/prompts/analyze-codebase.md +87 -0
- package/prompts/generate-personas.md +61 -0
- package/prompts/infer-conventions.md +72 -0
- package/templates/agents/sr-architect.md +194 -0
- package/templates/agents/sr-backend-developer.md +54 -0
- package/templates/agents/sr-backend-reviewer.md +139 -0
- package/templates/agents/sr-developer.md +146 -0
- package/templates/agents/sr-doc-sync.md +167 -0
- package/templates/agents/sr-frontend-developer.md +48 -0
- package/templates/agents/sr-frontend-reviewer.md +132 -0
- package/templates/agents/sr-product-analyst.md +36 -0
- package/templates/agents/sr-product-manager.md +148 -0
- package/templates/agents/sr-reviewer.md +265 -0
- package/templates/agents/sr-security-reviewer.md +178 -0
- package/templates/agents/sr-test-writer.md +163 -0
- package/templates/claude-md/root.md +50 -0
- package/templates/commands/sr/batch-implement.md +282 -0
- package/templates/commands/sr/compat-check.md +271 -0
- package/templates/commands/sr/health-check.md +396 -0
- package/templates/commands/sr/implement.md +972 -0
- package/templates/commands/sr/product-backlog.md +195 -0
- package/templates/commands/sr/refactor-recommender.md +169 -0
- package/templates/commands/sr/update-product-driven-backlog.md +272 -0
- package/templates/commands/sr/why.md +96 -0
- package/templates/personas/persona.md +43 -0
- package/templates/personas/the-maintainer.md +78 -0
- package/templates/rules/layer.md +8 -0
- package/templates/security/security-exemptions.yaml +20 -0
- package/templates/settings/confidence-config.json +17 -0
- package/templates/settings/settings.json +15 -0
- package/templates/web-manager/README.md +107 -0
- package/templates/web-manager/client/index.html +12 -0
- package/templates/web-manager/client/package-lock.json +1727 -0
- package/templates/web-manager/client/package.json +20 -0
- package/templates/web-manager/client/src/App.tsx +83 -0
- package/templates/web-manager/client/src/components/AgentActivity.tsx +19 -0
- package/templates/web-manager/client/src/components/CommandInput.tsx +81 -0
- package/templates/web-manager/client/src/components/LogStream.tsx +57 -0
- package/templates/web-manager/client/src/components/PipelineSidebar.tsx +65 -0
- package/templates/web-manager/client/src/components/SearchBox.tsx +34 -0
- package/templates/web-manager/client/src/hooks/usePipeline.ts +62 -0
- package/templates/web-manager/client/src/hooks/useWebSocket.ts +59 -0
- package/templates/web-manager/client/src/main.tsx +9 -0
- package/templates/web-manager/client/tsconfig.json +21 -0
- package/templates/web-manager/client/tsconfig.node.json +11 -0
- package/templates/web-manager/client/vite.config.ts +13 -0
- package/templates/web-manager/package-lock.json +3327 -0
- package/templates/web-manager/package.json +30 -0
- package/templates/web-manager/server/hooks.test.ts +196 -0
- package/templates/web-manager/server/hooks.ts +71 -0
- package/templates/web-manager/server/index.test.ts +186 -0
- package/templates/web-manager/server/index.ts +99 -0
- package/templates/web-manager/server/spawner.test.ts +319 -0
- package/templates/web-manager/server/spawner.ts +89 -0
- package/templates/web-manager/server/types.ts +46 -0
- package/templates/web-manager/tsconfig.json +14 -0
- package/templates/web-manager/vitest.config.ts +8 -0
- package/update.sh +877 -0
package/update.sh
ADDED
|
@@ -0,0 +1,877 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# specrails updater
|
|
5
|
+
# Updates an existing specrails installation in a target repository.
|
|
6
|
+
# Preserves project-specific customizations (agents, personas, rules).
|
|
7
|
+
|
|
8
|
+
# Detect pipe mode (curl | bash) vs local execution
|
|
9
|
+
if [[ -z "${BASH_SOURCE[0]:-}" || "${BASH_SOURCE[0]:-}" == "bash" ]]; then
|
|
10
|
+
SPECRAILS_TMPDIR="$(mktemp -d)"
|
|
11
|
+
trap 'rm -rf "$SPECRAILS_TMPDIR"' EXIT
|
|
12
|
+
git clone --depth 1 https://github.com/fjpulidop/specrails.git "$SPECRAILS_TMPDIR/specrails" 2>/dev/null || {
|
|
13
|
+
echo "Error: failed to clone specrails repository." >&2
|
|
14
|
+
exit 1
|
|
15
|
+
}
|
|
16
|
+
SCRIPT_DIR="$SPECRAILS_TMPDIR/specrails"
|
|
17
|
+
else
|
|
18
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
19
|
+
fi
|
|
20
|
+
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || echo "")"
|
|
21
|
+
|
|
22
|
+
# Colors
|
|
23
|
+
RED='\033[0;31m'
|
|
24
|
+
GREEN='\033[0;32m'
|
|
25
|
+
YELLOW='\033[1;33m'
|
|
26
|
+
BLUE='\033[0;34m'
|
|
27
|
+
CYAN='\033[0;36m'
|
|
28
|
+
BOLD='\033[1m'
|
|
29
|
+
NC='\033[0m'
|
|
30
|
+
|
|
31
|
+
# ─────────────────────────────────────────────
|
|
32
|
+
# Argument parsing
|
|
33
|
+
# ─────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
CUSTOM_ROOT_DIR=""
|
|
36
|
+
UPDATE_COMPONENT="all"
|
|
37
|
+
FORCE_UPDATE=false
|
|
38
|
+
|
|
39
|
+
while [[ $# -gt 0 ]]; do
|
|
40
|
+
case "$1" in
|
|
41
|
+
--root-dir)
|
|
42
|
+
if [[ -z "${2:-}" ]]; then
|
|
43
|
+
echo "Error: --root-dir requires a path argument." >&2
|
|
44
|
+
exit 1
|
|
45
|
+
fi
|
|
46
|
+
CUSTOM_ROOT_DIR="$2"
|
|
47
|
+
shift 2
|
|
48
|
+
;;
|
|
49
|
+
--only)
|
|
50
|
+
if [[ -z "${2:-}" ]]; then
|
|
51
|
+
echo "Error: --only requires a component argument." >&2
|
|
52
|
+
echo "Usage: update.sh [--root-dir <path>] [--only <web-manager|commands|agents|core|all>] [--force]" >&2
|
|
53
|
+
exit 1
|
|
54
|
+
fi
|
|
55
|
+
UPDATE_COMPONENT="$2"
|
|
56
|
+
case "$UPDATE_COMPONENT" in
|
|
57
|
+
web-manager|commands|agents|core|all) ;;
|
|
58
|
+
*)
|
|
59
|
+
echo "Error: unknown component '$UPDATE_COMPONENT'." >&2
|
|
60
|
+
echo "Valid values: web-manager, commands, agents, core, all" >&2
|
|
61
|
+
exit 1
|
|
62
|
+
;;
|
|
63
|
+
esac
|
|
64
|
+
shift 2
|
|
65
|
+
;;
|
|
66
|
+
--force)
|
|
67
|
+
FORCE_UPDATE=true
|
|
68
|
+
shift
|
|
69
|
+
;;
|
|
70
|
+
*)
|
|
71
|
+
echo "Unknown argument: $1" >&2
|
|
72
|
+
echo "Usage: update.sh [--root-dir <path>] [--only <web-manager|commands|agents|core|all>] [--force]" >&2
|
|
73
|
+
exit 1
|
|
74
|
+
;;
|
|
75
|
+
esac
|
|
76
|
+
done
|
|
77
|
+
|
|
78
|
+
# Override REPO_ROOT if --root-dir was provided
|
|
79
|
+
if [[ -n "$CUSTOM_ROOT_DIR" ]]; then
|
|
80
|
+
REPO_ROOT="$(cd "$CUSTOM_ROOT_DIR" 2>/dev/null && pwd)" || {
|
|
81
|
+
echo "Error: --root-dir path does not exist or is not accessible: $CUSTOM_ROOT_DIR" >&2
|
|
82
|
+
exit 1
|
|
83
|
+
}
|
|
84
|
+
if [[ ! -d "$REPO_ROOT" ]]; then
|
|
85
|
+
echo "Error: --root-dir path is not a directory: $CUSTOM_ROOT_DIR" >&2
|
|
86
|
+
exit 1
|
|
87
|
+
fi
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
# Detect if running from within the specrails source repo itself
|
|
91
|
+
if [[ -z "$CUSTOM_ROOT_DIR" && -f "$SCRIPT_DIR/install.sh" && -d "$SCRIPT_DIR/templates" && "$SCRIPT_DIR" == "$REPO_ROOT"* ]]; then
|
|
92
|
+
# We're inside the specrails source — ask for target repo
|
|
93
|
+
echo ""
|
|
94
|
+
echo -e "${YELLOW}⚠${NC} You're running the updater from inside the specrails source repo."
|
|
95
|
+
echo -e " specrails updates a ${BOLD}target${NC} repository, not itself."
|
|
96
|
+
echo ""
|
|
97
|
+
read -p " Enter the path to the target repo (or 'q' to quit): " TARGET_PATH
|
|
98
|
+
if [[ "$TARGET_PATH" == "q" || -z "$TARGET_PATH" ]]; then
|
|
99
|
+
echo " Aborted. No changes made."
|
|
100
|
+
exit 0
|
|
101
|
+
fi
|
|
102
|
+
# Expand ~ and resolve path
|
|
103
|
+
TARGET_PATH="${TARGET_PATH/#\~/$HOME}"
|
|
104
|
+
REPO_ROOT="$(cd "$TARGET_PATH" 2>/dev/null && pwd)" || {
|
|
105
|
+
echo "Error: path does not exist or is not accessible: $TARGET_PATH" >&2
|
|
106
|
+
exit 1
|
|
107
|
+
}
|
|
108
|
+
if [[ ! -d "$REPO_ROOT/.git" ]]; then
|
|
109
|
+
echo -e "${YELLOW}⚠${NC} Warning: $REPO_ROOT does not appear to be a git repository."
|
|
110
|
+
read -p " Continue anyway? (y/n): " CONTINUE_NOGIT
|
|
111
|
+
if [[ "$CONTINUE_NOGIT" != "y" && "$CONTINUE_NOGIT" != "Y" ]]; then
|
|
112
|
+
echo " Aborted. No changes made."
|
|
113
|
+
exit 0
|
|
114
|
+
fi
|
|
115
|
+
fi
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
# ─────────────────────────────────────────────
|
|
119
|
+
# Helpers
|
|
120
|
+
# ─────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
ok() { echo -e " ${GREEN}✓${NC} $1"; }
|
|
123
|
+
warn() { echo -e " ${YELLOW}⚠${NC} $1"; }
|
|
124
|
+
fail() { echo -e " ${RED}✗${NC} $1"; }
|
|
125
|
+
info() { echo -e " ${BLUE}→${NC} $1"; }
|
|
126
|
+
step() { echo -e "\n${BOLD}$1${NC}"; }
|
|
127
|
+
|
|
128
|
+
AVAILABLE_VERSION="$(cat "$SCRIPT_DIR/VERSION")"
|
|
129
|
+
|
|
130
|
+
print_header() {
|
|
131
|
+
local installed_ver="${1:-unknown}"
|
|
132
|
+
echo ""
|
|
133
|
+
echo -e "${BOLD}${CYAN}╔══════════════════════════════════════════════╗${NC}"
|
|
134
|
+
echo -e "${BOLD}${CYAN}║ specrails update v${AVAILABLE_VERSION} ║${NC}"
|
|
135
|
+
echo -e "${BOLD}${CYAN}║ Agent Workflow System for Claude Code ║${NC}"
|
|
136
|
+
echo -e "${BOLD}${CYAN}╚══════════════════════════════════════════════╝${NC}"
|
|
137
|
+
echo ""
|
|
138
|
+
if [[ "$installed_ver" != "$AVAILABLE_VERSION" ]]; then
|
|
139
|
+
info "Installed: v${installed_ver} → Available: v${AVAILABLE_VERSION}"
|
|
140
|
+
fi
|
|
141
|
+
echo ""
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
generate_manifest() {
|
|
145
|
+
local version
|
|
146
|
+
version="$(cat "$SCRIPT_DIR/VERSION")"
|
|
147
|
+
|
|
148
|
+
local updated_at
|
|
149
|
+
updated_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
|
150
|
+
|
|
151
|
+
# Write version file
|
|
152
|
+
printf '%s\n' "$version" > "$REPO_ROOT/.specrails-version"
|
|
153
|
+
|
|
154
|
+
# Build artifact checksums for all files under templates/
|
|
155
|
+
local artifacts_json=""
|
|
156
|
+
local first=true
|
|
157
|
+
while IFS= read -r -d '' filepath; do
|
|
158
|
+
local relpath
|
|
159
|
+
relpath="templates/${filepath#"$SCRIPT_DIR/templates/"}"
|
|
160
|
+
local checksum
|
|
161
|
+
checksum="sha256:$(shasum -a 256 "$filepath" | awk '{print $1}')"
|
|
162
|
+
if [ "$first" = true ]; then
|
|
163
|
+
first=false
|
|
164
|
+
else
|
|
165
|
+
artifacts_json="${artifacts_json},"
|
|
166
|
+
fi
|
|
167
|
+
artifacts_json="${artifacts_json}
|
|
168
|
+
\"${relpath}\": \"${checksum}\""
|
|
169
|
+
done < <(find "$SCRIPT_DIR/templates" -type f -not -path '*/node_modules/*' -not -name 'package-lock.json' -print0 | sort -z)
|
|
170
|
+
|
|
171
|
+
# Include commands/setup.md
|
|
172
|
+
local setup_checksum
|
|
173
|
+
setup_checksum="sha256:$(shasum -a 256 "$SCRIPT_DIR/commands/setup.md" | awk '{print $1}')"
|
|
174
|
+
if [ -n "$artifacts_json" ]; then
|
|
175
|
+
artifacts_json="${artifacts_json},"
|
|
176
|
+
fi
|
|
177
|
+
artifacts_json="${artifacts_json}
|
|
178
|
+
\"commands/setup.md\": \"${setup_checksum}\""
|
|
179
|
+
|
|
180
|
+
cat > "$REPO_ROOT/.specrails-manifest.json" << EOF
|
|
181
|
+
{
|
|
182
|
+
"version": "${version}",
|
|
183
|
+
"installed_at": "${updated_at}",
|
|
184
|
+
"artifacts": {${artifacts_json}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
EOF
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
# ─────────────────────────────────────────────
|
|
191
|
+
# Phase 1: Prerequisites + version check
|
|
192
|
+
# ─────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
# Resolve REPO_ROOT before printing header
|
|
195
|
+
if [[ -z "$REPO_ROOT" ]]; then
|
|
196
|
+
echo ""
|
|
197
|
+
fail "Not inside a git repository and no --root-dir provided."
|
|
198
|
+
echo " Usage: update.sh [--root-dir <path>]"
|
|
199
|
+
exit 1
|
|
200
|
+
fi
|
|
201
|
+
|
|
202
|
+
VERSION_FILE="$REPO_ROOT/.specrails-version"
|
|
203
|
+
AGENTS_DIR="$REPO_ROOT/.claude/agents"
|
|
204
|
+
|
|
205
|
+
# Detect installation state
|
|
206
|
+
INSTALLED_VERSION=""
|
|
207
|
+
IS_LEGACY=false
|
|
208
|
+
|
|
209
|
+
if [[ -f "$VERSION_FILE" ]]; then
|
|
210
|
+
INSTALLED_VERSION="$(cat "$VERSION_FILE" | tr -d '[:space:]')"
|
|
211
|
+
elif [[ -d "$AGENTS_DIR" ]] && [[ -n "$(ls -A "$AGENTS_DIR" 2>/dev/null)" ]]; then
|
|
212
|
+
IS_LEGACY=true
|
|
213
|
+
INSTALLED_VERSION="0.1.0"
|
|
214
|
+
else
|
|
215
|
+
echo ""
|
|
216
|
+
fail "No specrails installation found. Run install.sh first."
|
|
217
|
+
echo ""
|
|
218
|
+
exit 1
|
|
219
|
+
fi
|
|
220
|
+
|
|
221
|
+
print_header "$INSTALLED_VERSION"
|
|
222
|
+
|
|
223
|
+
if [[ -n "$CUSTOM_ROOT_DIR" ]]; then
|
|
224
|
+
ok "Update root (--root-dir): $REPO_ROOT"
|
|
225
|
+
else
|
|
226
|
+
ok "Git repository root: $REPO_ROOT"
|
|
227
|
+
fi
|
|
228
|
+
|
|
229
|
+
# Content-aware up-to-date check (skip for legacy migrations and agent-only runs)
|
|
230
|
+
if [[ "$INSTALLED_VERSION" == "$AVAILABLE_VERSION" ]] && [[ "$IS_LEGACY" == false ]] && [[ "$UPDATE_COMPONENT" != "agents" ]] && [[ "$FORCE_UPDATE" == false ]]; then
|
|
231
|
+
# Same version — check if any template content has actually changed
|
|
232
|
+
local_manifest="$REPO_ROOT/.specrails-manifest.json"
|
|
233
|
+
HAS_CHANGES=false
|
|
234
|
+
|
|
235
|
+
if [[ -f "$local_manifest" ]]; then
|
|
236
|
+
while IFS= read -r -d '' filepath; do
|
|
237
|
+
relpath="templates/${filepath#"$SCRIPT_DIR/templates/"}"
|
|
238
|
+
current_checksum="sha256:$(shasum -a 256 "$filepath" | awk '{print $1}')"
|
|
239
|
+
manifest_checksum="$(python3 -c "
|
|
240
|
+
import json, sys
|
|
241
|
+
try:
|
|
242
|
+
data = json.load(open(sys.argv[1]))
|
|
243
|
+
print(data['artifacts'].get(sys.argv[2], ''))
|
|
244
|
+
except Exception:
|
|
245
|
+
print('')
|
|
246
|
+
" "$local_manifest" "$relpath" 2>/dev/null || echo "")"
|
|
247
|
+
|
|
248
|
+
if [[ -z "$manifest_checksum" ]] || [[ "$current_checksum" != "$manifest_checksum" ]]; then
|
|
249
|
+
HAS_CHANGES=true
|
|
250
|
+
break
|
|
251
|
+
fi
|
|
252
|
+
done < <(find "$SCRIPT_DIR/templates" -type f -not -path '*/node_modules/*' -not -name 'package-lock.json' -print0 | sort -z)
|
|
253
|
+
|
|
254
|
+
# Also check commands/setup.md
|
|
255
|
+
if [[ "$HAS_CHANGES" == false ]] && [[ -f "$SCRIPT_DIR/commands/setup.md" ]]; then
|
|
256
|
+
setup_checksum="sha256:$(shasum -a 256 "$SCRIPT_DIR/commands/setup.md" | awk '{print $1}')"
|
|
257
|
+
manifest_setup="$(python3 -c "
|
|
258
|
+
import json, sys
|
|
259
|
+
try:
|
|
260
|
+
data = json.load(open(sys.argv[1]))
|
|
261
|
+
print(data['artifacts'].get('commands/setup.md', ''))
|
|
262
|
+
except Exception:
|
|
263
|
+
print('')
|
|
264
|
+
" "$local_manifest" 2>/dev/null || echo "")"
|
|
265
|
+
if [[ "$setup_checksum" != "$manifest_setup" ]]; then
|
|
266
|
+
HAS_CHANGES=true
|
|
267
|
+
fi
|
|
268
|
+
fi
|
|
269
|
+
else
|
|
270
|
+
# No manifest — can't verify, assume changes exist
|
|
271
|
+
HAS_CHANGES=true
|
|
272
|
+
fi
|
|
273
|
+
|
|
274
|
+
if [[ "$HAS_CHANGES" == false ]]; then
|
|
275
|
+
ok "Already up to date (v${AVAILABLE_VERSION}) — all templates match"
|
|
276
|
+
echo ""
|
|
277
|
+
exit 0
|
|
278
|
+
else
|
|
279
|
+
info "Same version (v${AVAILABLE_VERSION}) but template content has changed — updating"
|
|
280
|
+
fi
|
|
281
|
+
fi
|
|
282
|
+
|
|
283
|
+
# ─────────────────────────────────────────────
|
|
284
|
+
# Phase 2: Legacy migration
|
|
285
|
+
# ─────────────────────────────────────────────
|
|
286
|
+
|
|
287
|
+
if [[ "$IS_LEGACY" == true ]]; then
|
|
288
|
+
step "Phase 2: Legacy migration"
|
|
289
|
+
warn "No .specrails-version found — assuming v0.1.0 (pre-versioning install)"
|
|
290
|
+
info "Generating baseline manifest from current specrails templates..."
|
|
291
|
+
generate_manifest
|
|
292
|
+
# Overwrite with legacy version so the update flow sees "0.1.0 → current"
|
|
293
|
+
printf '0.1.0\n' > "$VERSION_FILE"
|
|
294
|
+
ok "Written .specrails-version as 0.1.0"
|
|
295
|
+
ok "Written .specrails-manifest.json"
|
|
296
|
+
fi
|
|
297
|
+
|
|
298
|
+
# ─────────────────────────────────────────────
|
|
299
|
+
# Phase 3: Backup
|
|
300
|
+
# ─────────────────────────────────────────────
|
|
301
|
+
|
|
302
|
+
step "Phase 3: Creating backup"
|
|
303
|
+
|
|
304
|
+
BACKUP_DIR="$REPO_ROOT/.claude.specrails.backup"
|
|
305
|
+
UPDATE_SUCCESS=false
|
|
306
|
+
|
|
307
|
+
# Trap: on exit, if update did not succeed, warn about backup
|
|
308
|
+
cleanup_on_exit() {
|
|
309
|
+
if [[ "$UPDATE_SUCCESS" != true ]] && [[ -d "$BACKUP_DIR" ]]; then
|
|
310
|
+
echo ""
|
|
311
|
+
warn "Update did not complete successfully."
|
|
312
|
+
warn "Your previous .claude/ is backed up at: $BACKUP_DIR"
|
|
313
|
+
warn "To restore: rm -rf \"$REPO_ROOT/.claude\" && mv \"$BACKUP_DIR\" \"$REPO_ROOT/.claude\""
|
|
314
|
+
echo ""
|
|
315
|
+
fi
|
|
316
|
+
}
|
|
317
|
+
trap cleanup_on_exit EXIT
|
|
318
|
+
|
|
319
|
+
rsync -a --exclude='node_modules' "$REPO_ROOT/.claude/" "$BACKUP_DIR/"
|
|
320
|
+
ok "Backed up .claude/ to .claude.specrails.backup/ (excluding node_modules)"
|
|
321
|
+
|
|
322
|
+
# ─────────────────────────────────────────────
|
|
323
|
+
# Update functions
|
|
324
|
+
# ─────────────────────────────────────────────
|
|
325
|
+
|
|
326
|
+
NEEDS_SETUP_UPDATE=false
|
|
327
|
+
FORCE_AGENTS=false
|
|
328
|
+
|
|
329
|
+
do_migrate_sr_prefix() {
|
|
330
|
+
# Detect and migrate legacy installations that use unprefixed agent/command names.
|
|
331
|
+
# A legacy installation is one where .claude/agents/architect.md exists (without sr- prefix).
|
|
332
|
+
local agents_dir="$REPO_ROOT/.claude/agents"
|
|
333
|
+
local commands_dir="$REPO_ROOT/.claude/commands"
|
|
334
|
+
local memory_dir="$REPO_ROOT/.claude/agent-memory"
|
|
335
|
+
|
|
336
|
+
if [[ ! -f "$agents_dir/architect.md" ]]; then
|
|
337
|
+
return # Nothing to migrate
|
|
338
|
+
fi
|
|
339
|
+
|
|
340
|
+
step "Migration: adding sr- prefix namespace"
|
|
341
|
+
info "Legacy installation detected (unprefixed agent names). Migrating to sr- prefix..."
|
|
342
|
+
|
|
343
|
+
local migrated_agents=0
|
|
344
|
+
local migrated_commands=0
|
|
345
|
+
local migrated_memory=0
|
|
346
|
+
|
|
347
|
+
# Migrate agent files
|
|
348
|
+
local known_agents=(
|
|
349
|
+
"architect"
|
|
350
|
+
"developer"
|
|
351
|
+
"reviewer"
|
|
352
|
+
"product-manager"
|
|
353
|
+
"product-analyst"
|
|
354
|
+
"test-writer"
|
|
355
|
+
"doc-sync"
|
|
356
|
+
"frontend-developer"
|
|
357
|
+
"backend-developer"
|
|
358
|
+
"frontend-reviewer"
|
|
359
|
+
"backend-reviewer"
|
|
360
|
+
"security-reviewer"
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
for agent in "${known_agents[@]}"; do
|
|
364
|
+
local src="$agents_dir/${agent}.md"
|
|
365
|
+
local dst="$agents_dir/sr-${agent}.md"
|
|
366
|
+
if [[ -f "$src" ]] && [[ ! -f "$dst" ]]; then
|
|
367
|
+
mv "$src" "$dst"
|
|
368
|
+
info "Renamed: agents/${agent}.md → agents/sr-${agent}.md"
|
|
369
|
+
((migrated_agents++))
|
|
370
|
+
fi
|
|
371
|
+
done
|
|
372
|
+
|
|
373
|
+
# Migrate persona files in .claude/agents/personas/
|
|
374
|
+
local personas_dir="$agents_dir/personas"
|
|
375
|
+
if [[ -d "$personas_dir" ]]; then
|
|
376
|
+
while IFS= read -r -d '' persona_file; do
|
|
377
|
+
local persona_basename
|
|
378
|
+
persona_basename="$(basename "$persona_file")"
|
|
379
|
+
# Skip files already prefixed with sr-
|
|
380
|
+
if [[ "$persona_basename" == sr-* ]]; then
|
|
381
|
+
continue
|
|
382
|
+
fi
|
|
383
|
+
local persona_dst="$personas_dir/sr-${persona_basename}"
|
|
384
|
+
if [[ ! -f "$persona_dst" ]]; then
|
|
385
|
+
mv "$persona_file" "$persona_dst"
|
|
386
|
+
info "Renamed: personas/${persona_basename} → personas/sr-${persona_basename}"
|
|
387
|
+
((migrated_agents++))
|
|
388
|
+
fi
|
|
389
|
+
done < <(find "$personas_dir" -maxdepth 1 -name "*.md" -not -name "sr-*.md" -print0 2>/dev/null)
|
|
390
|
+
fi
|
|
391
|
+
|
|
392
|
+
# Create .claude/commands/sr/ and migrate workflow commands
|
|
393
|
+
local workflow_commands=(
|
|
394
|
+
"implement"
|
|
395
|
+
"batch-implement"
|
|
396
|
+
"product-backlog"
|
|
397
|
+
"update-product-driven-backlog"
|
|
398
|
+
"health-check"
|
|
399
|
+
"compat-check"
|
|
400
|
+
"refactor-recommender"
|
|
401
|
+
"why"
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
if [[ -d "$commands_dir" ]]; then
|
|
405
|
+
mkdir -p "$commands_dir/sr"
|
|
406
|
+
for cmd in "${workflow_commands[@]}"; do
|
|
407
|
+
local src="$commands_dir/${cmd}.md"
|
|
408
|
+
local dst="$commands_dir/sr/${cmd}.md"
|
|
409
|
+
if [[ -f "$src" ]] && [[ ! -f "$dst" ]]; then
|
|
410
|
+
mv "$src" "$dst"
|
|
411
|
+
info "Moved: commands/${cmd}.md → commands/sr/${cmd}.md"
|
|
412
|
+
((migrated_commands++))
|
|
413
|
+
fi
|
|
414
|
+
done
|
|
415
|
+
fi
|
|
416
|
+
|
|
417
|
+
# Migrate agent memory directories (only known agent dirs, not failures/ or explanations/)
|
|
418
|
+
if [[ -d "$memory_dir" ]]; then
|
|
419
|
+
for agent in "${known_agents[@]}"; do
|
|
420
|
+
local src="$memory_dir/${agent}"
|
|
421
|
+
local dst="$memory_dir/sr-${agent}"
|
|
422
|
+
if [[ -d "$src" ]] && [[ ! -d "$dst" ]]; then
|
|
423
|
+
mv "$src" "$dst"
|
|
424
|
+
info "Renamed: agent-memory/${agent}/ → agent-memory/sr-${agent}/"
|
|
425
|
+
((migrated_memory++))
|
|
426
|
+
fi
|
|
427
|
+
done
|
|
428
|
+
fi
|
|
429
|
+
|
|
430
|
+
# Summary
|
|
431
|
+
if [[ "$migrated_agents" -gt 0 ]] || [[ "$migrated_commands" -gt 0 ]] || [[ "$migrated_memory" -gt 0 ]]; then
|
|
432
|
+
ok "Migration complete: ${migrated_agents} agents/personas, ${migrated_commands} commands, ${migrated_memory} memory dirs"
|
|
433
|
+
else
|
|
434
|
+
ok "Migration check complete — nothing to migrate"
|
|
435
|
+
fi
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
do_core() {
|
|
439
|
+
step "Updating core artifacts (commands, skills, setup-templates)"
|
|
440
|
+
|
|
441
|
+
local manifest_file="$REPO_ROOT/.specrails-manifest.json"
|
|
442
|
+
local updated_count=0
|
|
443
|
+
local added_count=0
|
|
444
|
+
|
|
445
|
+
# Helper: check if a source file differs from its manifest checksum
|
|
446
|
+
# Returns 0 (true) if file is new or changed, 1 if unchanged
|
|
447
|
+
_file_changed() {
|
|
448
|
+
local source_file="$1"
|
|
449
|
+
local manifest_key="$2"
|
|
450
|
+
|
|
451
|
+
if [[ ! -f "$manifest_file" ]]; then
|
|
452
|
+
return 0 # No manifest — assume changed
|
|
453
|
+
fi
|
|
454
|
+
|
|
455
|
+
local current_checksum
|
|
456
|
+
current_checksum="sha256:$(shasum -a 256 "$source_file" | awk '{print $1}')"
|
|
457
|
+
local manifest_checksum
|
|
458
|
+
manifest_checksum="$(python3 -c "
|
|
459
|
+
import json, sys
|
|
460
|
+
try:
|
|
461
|
+
data = json.load(open(sys.argv[1]))
|
|
462
|
+
print(data['artifacts'].get(sys.argv[2], ''))
|
|
463
|
+
except Exception:
|
|
464
|
+
print('')
|
|
465
|
+
" "$manifest_file" "$manifest_key" 2>/dev/null || echo "")"
|
|
466
|
+
|
|
467
|
+
if [[ -z "$manifest_checksum" ]]; then
|
|
468
|
+
return 0 # New file
|
|
469
|
+
elif [[ "$current_checksum" != "$manifest_checksum" ]]; then
|
|
470
|
+
return 0 # Changed
|
|
471
|
+
fi
|
|
472
|
+
return 1 # Unchanged
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
# Update /setup command (selective)
|
|
476
|
+
mkdir -p "$REPO_ROOT/.claude/commands"
|
|
477
|
+
if _file_changed "$SCRIPT_DIR/commands/setup.md" "commands/setup.md"; then
|
|
478
|
+
cp "$SCRIPT_DIR/commands/setup.md" "$REPO_ROOT/.claude/commands/setup.md"
|
|
479
|
+
ok "Updated /setup command"
|
|
480
|
+
((updated_count++))
|
|
481
|
+
fi
|
|
482
|
+
|
|
483
|
+
# Update setup templates (selective — only copy changed/new files)
|
|
484
|
+
while IFS= read -r -d '' filepath; do
|
|
485
|
+
local relpath
|
|
486
|
+
relpath="templates/${filepath#"$SCRIPT_DIR/templates/"}"
|
|
487
|
+
|
|
488
|
+
if _file_changed "$filepath" "$relpath"; then
|
|
489
|
+
local dest="$REPO_ROOT/.claude/setup-templates/${filepath#"$SCRIPT_DIR/templates/"}"
|
|
490
|
+
mkdir -p "$(dirname "$dest")"
|
|
491
|
+
cp "$filepath" "$dest"
|
|
492
|
+
|
|
493
|
+
# Determine if new or changed
|
|
494
|
+
local manifest_checksum
|
|
495
|
+
manifest_checksum="$(python3 -c "
|
|
496
|
+
import json, sys
|
|
497
|
+
try:
|
|
498
|
+
data = json.load(open(sys.argv[1]))
|
|
499
|
+
print(data['artifacts'].get(sys.argv[2], ''))
|
|
500
|
+
except Exception:
|
|
501
|
+
print('')
|
|
502
|
+
" "$manifest_file" "$relpath" 2>/dev/null || echo "")"
|
|
503
|
+
if [[ -z "$manifest_checksum" ]]; then
|
|
504
|
+
info "New: $relpath"
|
|
505
|
+
((added_count++))
|
|
506
|
+
else
|
|
507
|
+
info "Changed: $relpath"
|
|
508
|
+
((updated_count++))
|
|
509
|
+
fi
|
|
510
|
+
fi
|
|
511
|
+
done < <(find "$SCRIPT_DIR/templates" -type f -not -path '*/node_modules/*' -not -name 'package-lock.json' -print0 | sort -z)
|
|
512
|
+
|
|
513
|
+
# Update prompts (selective)
|
|
514
|
+
if [[ -d "$SCRIPT_DIR/prompts" ]] && [[ -n "$(ls -A "$SCRIPT_DIR/prompts" 2>/dev/null)" ]]; then
|
|
515
|
+
while IFS= read -r -d '' filepath; do
|
|
516
|
+
local relpath
|
|
517
|
+
relpath="prompts/${filepath#"$SCRIPT_DIR/prompts/"}"
|
|
518
|
+
local dest="$REPO_ROOT/.claude/setup-templates/prompts/${filepath#"$SCRIPT_DIR/prompts/"}"
|
|
519
|
+
|
|
520
|
+
# Prompts aren't in manifest yet — compare directly with destination
|
|
521
|
+
if [[ ! -f "$dest" ]] || ! diff -q "$filepath" "$dest" &>/dev/null; then
|
|
522
|
+
mkdir -p "$(dirname "$dest")"
|
|
523
|
+
cp "$filepath" "$dest"
|
|
524
|
+
if [[ ! -f "$dest" ]]; then
|
|
525
|
+
info "New prompt: $relpath"
|
|
526
|
+
((added_count++))
|
|
527
|
+
else
|
|
528
|
+
info "Changed prompt: $relpath"
|
|
529
|
+
((updated_count++))
|
|
530
|
+
fi
|
|
531
|
+
fi
|
|
532
|
+
done < <(find "$SCRIPT_DIR/prompts" -type f -print0 | sort -z)
|
|
533
|
+
fi
|
|
534
|
+
|
|
535
|
+
# Update skills (selective)
|
|
536
|
+
if [[ -d "$SCRIPT_DIR/.claude/skills" ]] && [[ -n "$(ls -A "$SCRIPT_DIR/.claude/skills" 2>/dev/null)" ]]; then
|
|
537
|
+
while IFS= read -r -d '' filepath; do
|
|
538
|
+
local relpath
|
|
539
|
+
relpath=".claude/skills/${filepath#"$SCRIPT_DIR/.claude/skills/"}"
|
|
540
|
+
local dest="$REPO_ROOT/$relpath"
|
|
541
|
+
|
|
542
|
+
if [[ ! -f "$dest" ]] || ! diff -q "$filepath" "$dest" &>/dev/null; then
|
|
543
|
+
mkdir -p "$(dirname "$dest")"
|
|
544
|
+
cp "$filepath" "$dest"
|
|
545
|
+
if [[ ! -f "$dest" ]]; then
|
|
546
|
+
info "New skill: $relpath"
|
|
547
|
+
((added_count++))
|
|
548
|
+
else
|
|
549
|
+
info "Changed skill: $relpath"
|
|
550
|
+
((updated_count++))
|
|
551
|
+
fi
|
|
552
|
+
fi
|
|
553
|
+
done < <(find "$SCRIPT_DIR/.claude/skills" -type f -print0 | sort -z)
|
|
554
|
+
fi
|
|
555
|
+
|
|
556
|
+
if [[ "$updated_count" -eq 0 ]] && [[ "$added_count" -eq 0 ]]; then
|
|
557
|
+
ok "All core artifacts unchanged"
|
|
558
|
+
else
|
|
559
|
+
ok "Core update: ${updated_count} changed, ${added_count} new"
|
|
560
|
+
fi
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
do_web_manager() {
|
|
564
|
+
step "Updating web manager (Pipeline Monitor)"
|
|
565
|
+
|
|
566
|
+
local web_manager_dir="$REPO_ROOT/.claude/web-manager"
|
|
567
|
+
local source_dir="$SCRIPT_DIR/templates/web-manager"
|
|
568
|
+
local has_npm=false
|
|
569
|
+
if command -v npm &>/dev/null; then
|
|
570
|
+
has_npm=true
|
|
571
|
+
fi
|
|
572
|
+
|
|
573
|
+
if [[ ! -d "$source_dir" ]]; then
|
|
574
|
+
ok "No web manager template found — skipping"
|
|
575
|
+
return
|
|
576
|
+
fi
|
|
577
|
+
|
|
578
|
+
if [[ -d "$web_manager_dir" ]]; then
|
|
579
|
+
# Already installed — check for actual changes (excluding node_modules)
|
|
580
|
+
local wm_changes
|
|
581
|
+
wm_changes="$(diff -rq --exclude='node_modules' --exclude='.DS_Store' "$source_dir" "$web_manager_dir" 2>/dev/null || true)"
|
|
582
|
+
|
|
583
|
+
if [[ -z "$wm_changes" ]]; then
|
|
584
|
+
ok "Web manager unchanged — skipping"
|
|
585
|
+
return
|
|
586
|
+
fi
|
|
587
|
+
|
|
588
|
+
local wm_changed_count
|
|
589
|
+
wm_changed_count="$(echo "$wm_changes" | wc -l | tr -d ' ')"
|
|
590
|
+
info "${wm_changed_count} web manager file(s) changed — syncing"
|
|
591
|
+
|
|
592
|
+
rsync -a --delete --exclude='node_modules' \
|
|
593
|
+
"$source_dir/" "$web_manager_dir/"
|
|
594
|
+
ok "Synced web manager files (node_modules preserved)"
|
|
595
|
+
|
|
596
|
+
# Only re-run npm install if package.json changed
|
|
597
|
+
local needs_server_install=false
|
|
598
|
+
local needs_client_install=false
|
|
599
|
+
if echo "$wm_changes" | grep -q "package.json" 2>/dev/null; then
|
|
600
|
+
if echo "$wm_changes" | grep -q "client/package.json" 2>/dev/null; then
|
|
601
|
+
needs_client_install=true
|
|
602
|
+
fi
|
|
603
|
+
# Check for root package.json (not client/)
|
|
604
|
+
if echo "$wm_changes" | grep -v "client/" | grep -q "package.json" 2>/dev/null; then
|
|
605
|
+
needs_server_install=true
|
|
606
|
+
fi
|
|
607
|
+
fi
|
|
608
|
+
|
|
609
|
+
if [[ "$has_npm" == true ]]; then
|
|
610
|
+
if [[ "$needs_server_install" == true ]]; then
|
|
611
|
+
info "Re-running npm install for server (package.json changed)..."
|
|
612
|
+
(cd "$web_manager_dir" && npm install --silent 2>/dev/null) && {
|
|
613
|
+
ok "Server dependencies updated"
|
|
614
|
+
} || {
|
|
615
|
+
warn "Server dependency install failed — run 'cd .claude/web-manager && npm install' manually"
|
|
616
|
+
}
|
|
617
|
+
fi
|
|
618
|
+
if [[ "$needs_client_install" == true ]]; then
|
|
619
|
+
info "Re-running npm install for client (package.json changed)..."
|
|
620
|
+
(cd "$web_manager_dir/client" && npm install --silent 2>/dev/null) && {
|
|
621
|
+
ok "Client dependencies updated"
|
|
622
|
+
} || {
|
|
623
|
+
warn "Client dependency install failed — run 'cd .claude/web-manager/client && npm install' manually"
|
|
624
|
+
}
|
|
625
|
+
fi
|
|
626
|
+
elif [[ "$needs_server_install" == true ]] || [[ "$needs_client_install" == true ]]; then
|
|
627
|
+
warn "npm not found — package.json changed but cannot install. Run 'cd .claude/web-manager && npm install' manually."
|
|
628
|
+
fi
|
|
629
|
+
else
|
|
630
|
+
# Not installed — full install
|
|
631
|
+
mkdir -p "$web_manager_dir"
|
|
632
|
+
cp -r "$source_dir/"* "$web_manager_dir/"
|
|
633
|
+
ok "Installed web manager to .claude/web-manager/"
|
|
634
|
+
|
|
635
|
+
if [[ "$has_npm" == true ]]; then
|
|
636
|
+
info "Installing web manager dependencies..."
|
|
637
|
+
(cd "$web_manager_dir" && npm install --silent 2>/dev/null) && {
|
|
638
|
+
ok "Server dependencies installed"
|
|
639
|
+
} || {
|
|
640
|
+
warn "Server dependency install failed — run 'cd .claude/web-manager && npm install' manually"
|
|
641
|
+
}
|
|
642
|
+
(cd "$web_manager_dir/client" && npm install --silent 2>/dev/null) && {
|
|
643
|
+
ok "Client dependencies installed"
|
|
644
|
+
} || {
|
|
645
|
+
warn "Client dependency install failed — run 'cd .claude/web-manager/client && npm install' manually"
|
|
646
|
+
}
|
|
647
|
+
else
|
|
648
|
+
warn "npm not available — skipping dependency install. Run 'cd .claude/web-manager && npm install' later."
|
|
649
|
+
fi
|
|
650
|
+
fi
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
do_agents() {
|
|
654
|
+
step "Checking adapted artifacts (agents, rules)"
|
|
655
|
+
|
|
656
|
+
local manifest_file="$REPO_ROOT/.specrails-manifest.json"
|
|
657
|
+
|
|
658
|
+
if [[ ! -f "$manifest_file" ]]; then
|
|
659
|
+
warn "No .specrails-manifest.json found — cannot detect template changes."
|
|
660
|
+
warn "Run update.sh without --only to regenerate the manifest."
|
|
661
|
+
return
|
|
662
|
+
fi
|
|
663
|
+
|
|
664
|
+
local changed_templates=()
|
|
665
|
+
local new_templates=()
|
|
666
|
+
|
|
667
|
+
# Check templates/agents/ and templates/rules/ for changes
|
|
668
|
+
while IFS= read -r -d '' filepath; do
|
|
669
|
+
local relpath
|
|
670
|
+
relpath="templates/${filepath#"$SCRIPT_DIR/templates/"}"
|
|
671
|
+
local current_checksum
|
|
672
|
+
current_checksum="sha256:$(shasum -a 256 "$filepath" | awk '{print $1}')"
|
|
673
|
+
|
|
674
|
+
# Look up this path in the manifest
|
|
675
|
+
local manifest_checksum
|
|
676
|
+
manifest_checksum="$(python3 -c "
|
|
677
|
+
import json, sys
|
|
678
|
+
manifest_file = sys.argv[1]
|
|
679
|
+
relpath = sys.argv[2]
|
|
680
|
+
try:
|
|
681
|
+
data = json.load(open(manifest_file))
|
|
682
|
+
print(data['artifacts'].get(relpath, ''))
|
|
683
|
+
except Exception:
|
|
684
|
+
print('')
|
|
685
|
+
" "$manifest_file" "$relpath" 2>/dev/null || echo "")"
|
|
686
|
+
|
|
687
|
+
if [[ -z "$manifest_checksum" ]]; then
|
|
688
|
+
new_templates+=("$relpath")
|
|
689
|
+
elif [[ "$current_checksum" != "$manifest_checksum" ]]; then
|
|
690
|
+
changed_templates+=("$relpath")
|
|
691
|
+
fi
|
|
692
|
+
done < <(find "$SCRIPT_DIR/templates/agents" "$SCRIPT_DIR/templates/rules" -type f -print0 2>/dev/null | sort -z)
|
|
693
|
+
|
|
694
|
+
# Handle changed templates
|
|
695
|
+
if [[ "${#changed_templates[@]}" -gt 0 ]] || [[ "$FORCE_AGENTS" == true ]]; then
|
|
696
|
+
if [[ "$FORCE_AGENTS" == true ]]; then
|
|
697
|
+
info "Agent regeneration forced via --only agents."
|
|
698
|
+
else
|
|
699
|
+
echo ""
|
|
700
|
+
warn "The following agent/rule templates have changed:"
|
|
701
|
+
for t in "${changed_templates[@]}"; do
|
|
702
|
+
echo " $t"
|
|
703
|
+
done
|
|
704
|
+
echo ""
|
|
705
|
+
fi
|
|
706
|
+
|
|
707
|
+
local answer
|
|
708
|
+
read -p " Regenerate agents? (y/N): " answer
|
|
709
|
+
if [[ "$answer" == "y" ]] || [[ "$answer" == "Y" ]]; then
|
|
710
|
+
NEEDS_SETUP_UPDATE=true
|
|
711
|
+
ok "Will regenerate agents via /setup --update"
|
|
712
|
+
else
|
|
713
|
+
warn "Workflow may break with outdated agents. Run '/setup --update' inside Claude Code when ready."
|
|
714
|
+
fi
|
|
715
|
+
else
|
|
716
|
+
ok "All agent/rule templates unchanged — no regeneration needed"
|
|
717
|
+
fi
|
|
718
|
+
|
|
719
|
+
# Handle new templates
|
|
720
|
+
if [[ "${#new_templates[@]}" -gt 0 ]]; then
|
|
721
|
+
echo ""
|
|
722
|
+
info "New agent/rule templates are available:"
|
|
723
|
+
for t in "${new_templates[@]}"; do
|
|
724
|
+
echo " $t"
|
|
725
|
+
done
|
|
726
|
+
info "These will be evaluated during /setup --update"
|
|
727
|
+
NEEDS_SETUP_UPDATE=true
|
|
728
|
+
fi
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
do_settings() {
|
|
732
|
+
step "Merging settings"
|
|
733
|
+
|
|
734
|
+
local user_settings="$REPO_ROOT/.claude/settings.json"
|
|
735
|
+
local template_settings="$SCRIPT_DIR/templates/settings/settings.json"
|
|
736
|
+
|
|
737
|
+
if [[ -f "$user_settings" ]] && [[ -f "$template_settings" ]]; then
|
|
738
|
+
if command -v python3 &>/dev/null; then
|
|
739
|
+
python3 -c "
|
|
740
|
+
import json, sys
|
|
741
|
+
|
|
742
|
+
template_path = sys.argv[1]
|
|
743
|
+
user_path = sys.argv[2]
|
|
744
|
+
|
|
745
|
+
with open(template_path) as f:
|
|
746
|
+
template = json.load(f)
|
|
747
|
+
|
|
748
|
+
with open(user_path) as f:
|
|
749
|
+
user = json.load(f)
|
|
750
|
+
|
|
751
|
+
def merge_additive(base, overlay):
|
|
752
|
+
for key, value in overlay.items():
|
|
753
|
+
if key not in base:
|
|
754
|
+
base[key] = value
|
|
755
|
+
elif isinstance(base[key], dict) and isinstance(value, dict):
|
|
756
|
+
merge_additive(base[key], value)
|
|
757
|
+
elif isinstance(base[key], list) and isinstance(value, list):
|
|
758
|
+
existing = set(str(i) for i in base[key])
|
|
759
|
+
for item in value:
|
|
760
|
+
if isinstance(item, str) and '{{' in item:
|
|
761
|
+
continue
|
|
762
|
+
if str(item) not in existing:
|
|
763
|
+
base[key].append(item)
|
|
764
|
+
existing.add(str(item))
|
|
765
|
+
return base
|
|
766
|
+
|
|
767
|
+
merged = merge_additive(user, template)
|
|
768
|
+
|
|
769
|
+
with open(user_path, 'w') as f:
|
|
770
|
+
json.dump(merged, f, indent=2)
|
|
771
|
+
f.write('\n')
|
|
772
|
+
|
|
773
|
+
" "$template_settings" "$user_settings" >/dev/null 2>&1 && ok "Merged settings.json (new keys added, existing preserved)" || {
|
|
774
|
+
warn "settings.json merge failed — skipping. Inspect manually."
|
|
775
|
+
}
|
|
776
|
+
else
|
|
777
|
+
warn "python3 not found — skipping settings.json merge."
|
|
778
|
+
fi
|
|
779
|
+
elif [[ ! -f "$user_settings" ]] && [[ -f "$template_settings" ]]; then
|
|
780
|
+
mkdir -p "$REPO_ROOT/.claude"
|
|
781
|
+
cp "$template_settings" "$user_settings"
|
|
782
|
+
ok "Installed settings.json (was missing)"
|
|
783
|
+
fi
|
|
784
|
+
|
|
785
|
+
# security-exemptions.yaml: skip if already exists (preserve user exemptions)
|
|
786
|
+
local user_exemptions="$REPO_ROOT/.claude/security-exemptions.yaml"
|
|
787
|
+
local template_exemptions="$SCRIPT_DIR/templates/security/security-exemptions.yaml"
|
|
788
|
+
if [[ ! -f "$user_exemptions" ]] && [[ -f "$template_exemptions" ]]; then
|
|
789
|
+
cp "$template_exemptions" "$user_exemptions"
|
|
790
|
+
ok "Installed security-exemptions.yaml (was missing)"
|
|
791
|
+
else
|
|
792
|
+
ok "security-exemptions.yaml preserved (user customizations kept)"
|
|
793
|
+
fi
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
do_stamp() {
|
|
797
|
+
step "Writing version stamp and manifest"
|
|
798
|
+
generate_manifest
|
|
799
|
+
ok "Updated .specrails-version to v${AVAILABLE_VERSION}"
|
|
800
|
+
ok "Updated .specrails-manifest.json"
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
# ─────────────────────────────────────────────
|
|
804
|
+
# Phase 4: Run selected components
|
|
805
|
+
# ─────────────────────────────────────────────
|
|
806
|
+
|
|
807
|
+
step "Phase 4: Running update (component: ${UPDATE_COMPONENT})"
|
|
808
|
+
|
|
809
|
+
case "$UPDATE_COMPONENT" in
|
|
810
|
+
all)
|
|
811
|
+
do_migrate_sr_prefix
|
|
812
|
+
do_core
|
|
813
|
+
do_web_manager
|
|
814
|
+
do_agents
|
|
815
|
+
do_settings
|
|
816
|
+
do_stamp
|
|
817
|
+
;;
|
|
818
|
+
commands)
|
|
819
|
+
do_migrate_sr_prefix
|
|
820
|
+
do_core
|
|
821
|
+
do_stamp
|
|
822
|
+
;;
|
|
823
|
+
web-manager)
|
|
824
|
+
do_web_manager
|
|
825
|
+
do_stamp
|
|
826
|
+
;;
|
|
827
|
+
agents)
|
|
828
|
+
do_migrate_sr_prefix
|
|
829
|
+
FORCE_AGENTS=true
|
|
830
|
+
do_agents
|
|
831
|
+
do_stamp
|
|
832
|
+
;;
|
|
833
|
+
core)
|
|
834
|
+
do_migrate_sr_prefix
|
|
835
|
+
do_core
|
|
836
|
+
do_stamp
|
|
837
|
+
;;
|
|
838
|
+
esac
|
|
839
|
+
|
|
840
|
+
# ─────────────────────────────────────────────
|
|
841
|
+
# Phase 5: Cleanup and summary
|
|
842
|
+
# ─────────────────────────────────────────────
|
|
843
|
+
|
|
844
|
+
step "Phase 5: Cleanup"
|
|
845
|
+
|
|
846
|
+
UPDATE_SUCCESS=true
|
|
847
|
+
rm -rf "$BACKUP_DIR"
|
|
848
|
+
ok "Backup removed"
|
|
849
|
+
|
|
850
|
+
# Clean up setup-templates if no /setup re-run is needed
|
|
851
|
+
if [[ "$NEEDS_SETUP_UPDATE" != true ]] && [[ -d "$REPO_ROOT/.claude/setup-templates" ]]; then
|
|
852
|
+
rm -rf "$REPO_ROOT/.claude/setup-templates"
|
|
853
|
+
ok "Cleaned up setup-templates (no /setup re-run needed)"
|
|
854
|
+
fi
|
|
855
|
+
|
|
856
|
+
echo ""
|
|
857
|
+
echo -e "${BOLD}${GREEN}Update complete — v${INSTALLED_VERSION} → v${AVAILABLE_VERSION}${NC}"
|
|
858
|
+
echo ""
|
|
859
|
+
echo " Component updated: ${UPDATE_COMPONENT}"
|
|
860
|
+
echo ""
|
|
861
|
+
|
|
862
|
+
if [[ "$NEEDS_SETUP_UPDATE" == true ]]; then
|
|
863
|
+
echo -e "${BOLD}${CYAN}Next step: regenerate adapted agents${NC}"
|
|
864
|
+
echo ""
|
|
865
|
+
echo " Open Claude Code in this repo and run:"
|
|
866
|
+
echo ""
|
|
867
|
+
echo -e " ${BOLD}/setup --update${NC}"
|
|
868
|
+
echo ""
|
|
869
|
+
echo " Claude will re-analyze your codebase and regenerate only the"
|
|
870
|
+
echo " agents and rules whose templates have changed."
|
|
871
|
+
echo ""
|
|
872
|
+
else
|
|
873
|
+
echo -e "${BOLD}${CYAN}No agent regeneration needed.${NC}"
|
|
874
|
+
echo ""
|
|
875
|
+
echo " Open Claude Code and continue working normally."
|
|
876
|
+
echo ""
|
|
877
|
+
fi
|