specweave 1.0.43 → 1.0.46
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.md +22 -22
- package/package.json +1 -1
- package/plugins/specweave/hooks/lib/hook-errors.sh +400 -0
- package/plugins/specweave/hooks/universal/fail-fast-wrapper.sh +128 -22
- package/plugins/specweave/hooks/v2/dispatchers/post-tool-use.sh +198 -51
- package/plugins/specweave/hooks/v2/guards/task-ac-sync-guard.sh +340 -259
- package/plugins/specweave/hooks/v2/handlers/github-sync-handler.sh +51 -15
- package/plugins/specweave/hooks/v2/queue/processor.sh +10 -2
- package/plugins/specweave-ado/hooks/post-living-docs-update.sh +10 -3
- package/src/templates/CLAUDE.md.template +4 -4
package/CLAUDE.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
<!-- SW:META template="claude" version="1.0.
|
|
1
|
+
<!-- SW:META template="claude" version="1.0.44" sections="header,start,autodetect,metarule,rules,workflow,structure,taskformat,secrets,syncing,mapping,testing,limits,troubleshooting,principles,linking,docs" -->
|
|
2
2
|
|
|
3
|
-
<!-- SW:SECTION:header version="1.0.
|
|
3
|
+
<!-- SW:SECTION:header version="1.0.44" -->
|
|
4
4
|
**Framework**: SpecWeave | **Truth**: `spec.md` + `tasks.md`
|
|
5
5
|
<!-- SW:END:header -->
|
|
6
6
|
|
|
7
|
-
<!-- SW:SECTION:start version="1.0.
|
|
7
|
+
<!-- SW:SECTION:start version="1.0.44" -->
|
|
8
8
|
## Getting Started
|
|
9
9
|
|
|
10
10
|
**Initial increment**: `0001-project-setup` (auto-created by `specweave init`)
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
2. **Customize**: Edit spec.md and use for setup tasks
|
|
15
15
|
<!-- SW:END:start -->
|
|
16
16
|
|
|
17
|
-
<!-- SW:SECTION:autodetect version="1.0.
|
|
17
|
+
<!-- SW:SECTION:autodetect version="1.0.44" -->
|
|
18
18
|
## Auto-Detection
|
|
19
19
|
|
|
20
20
|
SpecWeave auto-detects product descriptions and routes to `/sw:increment`:
|
|
@@ -24,7 +24,7 @@ SpecWeave auto-detects product descriptions and routes to `/sw:increment`:
|
|
|
24
24
|
**Opt-out phrases**: "Just brainstorm first" | "Don't plan yet" | "Quick discussion" | "Let's explore ideas"
|
|
25
25
|
<!-- SW:END:autodetect -->
|
|
26
26
|
|
|
27
|
-
<!-- SW:SECTION:metarule version="1.0.
|
|
27
|
+
<!-- SW:SECTION:metarule version="1.0.44" -->
|
|
28
28
|
## Meta-Rule: Think-Before-Act
|
|
29
29
|
|
|
30
30
|
**Satisfy dependencies BEFORE dependent operations.**
|
|
@@ -35,7 +35,7 @@ SpecWeave auto-detects product descriptions and routes to `/sw:increment`:
|
|
|
35
35
|
```
|
|
36
36
|
<!-- SW:END:metarule -->
|
|
37
37
|
|
|
38
|
-
<!-- SW:SECTION:rules version="1.0.
|
|
38
|
+
<!-- SW:SECTION:rules version="1.0.44" -->
|
|
39
39
|
## Rules
|
|
40
40
|
|
|
41
41
|
1. **Files** → `.specweave/increments/####-name/` (spec.md, plan.md, tasks.md at root; reports/, scripts/, logs/ subfolders)
|
|
@@ -45,7 +45,7 @@ SpecWeave auto-detects product descriptions and routes to `/sw:increment`:
|
|
|
45
45
|
5. **Root clean**: NEVER create .md/reports/scripts in project root → use increment folders
|
|
46
46
|
<!-- SW:END:rules -->
|
|
47
47
|
|
|
48
|
-
<!-- SW:SECTION:workflow version="1.0.
|
|
48
|
+
<!-- SW:SECTION:workflow version="1.0.44" -->
|
|
49
49
|
## Workflow
|
|
50
50
|
|
|
51
51
|
`/sw:increment "X"` → `/sw:do` → `/sw:progress` → `/sw:done 0001`
|
|
@@ -62,7 +62,7 @@ SpecWeave auto-detects product descriptions and routes to `/sw:increment`:
|
|
|
62
62
|
**Natural language**: "Let's build X" → `/sw:increment` | "What's status?" → `/sw:progress` | "We're done" → `/sw:done`
|
|
63
63
|
<!-- SW:END:workflow -->
|
|
64
64
|
|
|
65
|
-
<!-- SW:SECTION:structure version="1.0.
|
|
65
|
+
<!-- SW:SECTION:structure version="1.0.44" -->
|
|
66
66
|
## Structure
|
|
67
67
|
|
|
68
68
|
```
|
|
@@ -79,12 +79,12 @@ SpecWeave auto-detects product descriptions and routes to `/sw:increment`:
|
|
|
79
79
|
|
|
80
80
|
**Multi-repo permissions**: In `.claude/settings.json`:
|
|
81
81
|
```json
|
|
82
|
-
{"permissions":{"allow":["Write(
|
|
82
|
+
{"permissions":{"allow":["Write(//**)","Edit(//**)"],"additionalDirectories":["repositories"],"defaultMode":"bypassPermissions"}}
|
|
83
83
|
```
|
|
84
|
-
**Path syntax**:
|
|
84
|
+
**Path syntax**: `//path` = absolute | `/path` = relative to settings file | `**` = recursive | `additionalDirectories` = explicit working dirs
|
|
85
85
|
<!-- SW:END:structure -->
|
|
86
86
|
|
|
87
|
-
<!-- SW:SECTION:taskformat version="1.0.
|
|
87
|
+
<!-- SW:SECTION:taskformat version="1.0.44" -->
|
|
88
88
|
## Task Format
|
|
89
89
|
|
|
90
90
|
```markdown
|
|
@@ -94,7 +94,7 @@ SpecWeave auto-detects product descriptions and routes to `/sw:increment`:
|
|
|
94
94
|
```
|
|
95
95
|
<!-- SW:END:taskformat -->
|
|
96
96
|
|
|
97
|
-
<!-- SW:SECTION:secrets version="1.0.
|
|
97
|
+
<!-- SW:SECTION:secrets version="1.0.44" -->
|
|
98
98
|
## Secrets Check
|
|
99
99
|
|
|
100
100
|
**BEFORE CLI tools**: Check existing config first!
|
|
@@ -105,7 +105,7 @@ gh auth status
|
|
|
105
105
|
```
|
|
106
106
|
<!-- SW:END:secrets -->
|
|
107
107
|
|
|
108
|
-
<!-- SW:SECTION:syncing version="1.0.
|
|
108
|
+
<!-- SW:SECTION:syncing version="1.0.44" -->
|
|
109
109
|
## External Sync (GitHub/JIRA/ADO)
|
|
110
110
|
|
|
111
111
|
**After increment creation**: Run `/sw-github:sync {id}` to create issues!
|
|
@@ -124,7 +124,7 @@ Living docs sync ≠ External sync. They are separate:
|
|
|
124
124
|
**Verify tokens**: `grep GITHUB_TOKEN .env` | `gh auth status`
|
|
125
125
|
<!-- SW:END:syncing -->
|
|
126
126
|
|
|
127
|
-
<!-- SW:SECTION:mapping version="1.0.
|
|
127
|
+
<!-- SW:SECTION:mapping version="1.0.44" -->
|
|
128
128
|
## GitHub Mapping
|
|
129
129
|
|
|
130
130
|
| SpecWeave | GitHub |
|
|
@@ -134,7 +134,7 @@ Living docs sync ≠ External sync. They are separate:
|
|
|
134
134
|
| Task T-XXX | Checkbox |
|
|
135
135
|
<!-- SW:END:mapping -->
|
|
136
136
|
|
|
137
|
-
<!-- SW:SECTION:testing version="1.0.
|
|
137
|
+
<!-- SW:SECTION:testing version="1.0.44" -->
|
|
138
138
|
## Testing
|
|
139
139
|
|
|
140
140
|
BDD in tasks.md | Unit >80% | `.test.ts` (Vitest)
|
|
@@ -146,13 +146,13 @@ vi.mock('fs', () => ({ readFile: vi.fn() }));
|
|
|
146
146
|
```
|
|
147
147
|
<!-- SW:END:testing -->
|
|
148
148
|
|
|
149
|
-
<!-- SW:SECTION:limits version="1.0.
|
|
149
|
+
<!-- SW:SECTION:limits version="1.0.44" -->
|
|
150
150
|
## Limits
|
|
151
151
|
|
|
152
152
|
**Max 1500 lines/file** — extract before adding
|
|
153
153
|
<!-- SW:END:limits -->
|
|
154
154
|
|
|
155
|
-
<!-- SW:SECTION:troubleshooting version="1.0.
|
|
155
|
+
<!-- SW:SECTION:troubleshooting version="1.0.44" -->
|
|
156
156
|
## Troubleshooting
|
|
157
157
|
|
|
158
158
|
| Issue | Fix |
|
|
@@ -166,11 +166,11 @@ vi.mock('fs', () => ({ readFile: vi.fn() }));
|
|
|
166
166
|
| GitHub not syncing | Run `/sw-github:sync {id}` explicitly |
|
|
167
167
|
| Permission denied | Set `canUpsertInternalItems: true` in config.json |
|
|
168
168
|
| No GITHUB_TOKEN | Check `.env` file or run `gh auth login` |
|
|
169
|
-
| Edits blocked in repositories/ | Add `Write(
|
|
170
|
-
| Path patterns not working |
|
|
169
|
+
| Edits blocked in repositories/ | Add `"additionalDirectories":["repositories"]` + `Write(//**)`, `Edit(//**)` to `.claude/settings.json` |
|
|
170
|
+
| Path patterns not working | `//path` = absolute, `/path` = relative to settings file, `additionalDirectories` for explicit working dirs |
|
|
171
171
|
<!-- SW:END:troubleshooting -->
|
|
172
172
|
|
|
173
|
-
<!-- SW:SECTION:principles version="1.0.
|
|
173
|
+
<!-- SW:SECTION:principles version="1.0.44" -->
|
|
174
174
|
## Principles
|
|
175
175
|
|
|
176
176
|
1. **Spec-first**: `/sw:increment` before coding
|
|
@@ -180,7 +180,7 @@ vi.mock('fs', () => ({ readFile: vi.fn() }));
|
|
|
180
180
|
5. **Clean**: All files in increment folders
|
|
181
181
|
<!-- SW:END:principles -->
|
|
182
182
|
|
|
183
|
-
<!-- SW:SECTION:linking version="1.0.
|
|
183
|
+
<!-- SW:SECTION:linking version="1.0.44" -->
|
|
184
184
|
## Bidirectional Linking
|
|
185
185
|
|
|
186
186
|
Tasks ↔ User Stories auto-linked via AC-IDs: `AC-US1-01` → `US-001`
|
|
@@ -188,7 +188,7 @@ Tasks ↔ User Stories auto-linked via AC-IDs: `AC-US1-01` → `US-001`
|
|
|
188
188
|
Task format: `**AC**: AC-US1-01, AC-US1-02` (CRITICAL for linking)
|
|
189
189
|
<!-- SW:END:linking -->
|
|
190
190
|
|
|
191
|
-
<!-- SW:SECTION:docs version="1.0.
|
|
191
|
+
<!-- SW:SECTION:docs version="1.0.44" -->
|
|
192
192
|
## Docs
|
|
193
193
|
|
|
194
194
|
[spec-weave.com](https://spec-weave.com) | `.specweave/docs/internal/`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specweave",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.46",
|
|
4
4
|
"description": "Spec-driven development framework for Claude Code. AI-native workflow with living documentation, intelligent agents, and multilingual support (9 languages). Enterprise-grade traceability with permanent specs and temporary increments.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# hook-errors.sh - Centralized hook error logging and warning output
|
|
3
|
+
#
|
|
4
|
+
# PRINCIPLE: Hooks MUST NEVER block operations. All errors become warnings.
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# source "$HOOK_DIR/lib/hook-errors.sh"
|
|
8
|
+
# log_hook_warning "task-ac-sync" "Pattern not found in spec.md"
|
|
9
|
+
# log_hook_error "metadata-guard" "JSON parse failed" "$details"
|
|
10
|
+
#
|
|
11
|
+
# Environment:
|
|
12
|
+
# SPECWEAVE_HOOK_VERBOSE=1 - Show all warnings to user
|
|
13
|
+
# SPECWEAVE_HOOK_DEBUG=1 - Log debug info to files
|
|
14
|
+
#
|
|
15
|
+
# All functions are non-blocking and always return 0
|
|
16
|
+
|
|
17
|
+
set +e
|
|
18
|
+
|
|
19
|
+
# ============================================================================
|
|
20
|
+
# Configuration
|
|
21
|
+
# ============================================================================
|
|
22
|
+
|
|
23
|
+
HOOK_ERRORS_VERSION="1.0.0"
|
|
24
|
+
|
|
25
|
+
# Find project root
|
|
26
|
+
_find_hook_project_root() {
|
|
27
|
+
local dir="${1:-$PWD}"
|
|
28
|
+
while [[ "$dir" != "/" ]]; do
|
|
29
|
+
if [[ -d "$dir/.specweave" ]]; then
|
|
30
|
+
echo "$dir"
|
|
31
|
+
return 0
|
|
32
|
+
fi
|
|
33
|
+
dir=$(dirname "$dir")
|
|
34
|
+
done
|
|
35
|
+
echo "$PWD"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
_HOOK_PROJECT_ROOT=$(_find_hook_project_root)
|
|
39
|
+
_HOOK_LOGS_DIR="$_HOOK_PROJECT_ROOT/.specweave/logs"
|
|
40
|
+
_HOOK_STATE_DIR="$_HOOK_PROJECT_ROOT/.specweave/state"
|
|
41
|
+
|
|
42
|
+
# Ensure directories exist
|
|
43
|
+
mkdir -p "$_HOOK_LOGS_DIR" 2>/dev/null || true
|
|
44
|
+
mkdir -p "$_HOOK_STATE_DIR" 2>/dev/null || true
|
|
45
|
+
|
|
46
|
+
# Log files
|
|
47
|
+
_HOOK_ERROR_LOG="$_HOOK_LOGS_DIR/hook-errors.log"
|
|
48
|
+
_HOOK_WARNING_LOG="$_HOOK_LOGS_DIR/hook-warnings.log"
|
|
49
|
+
_HOOK_DEBUG_LOG="$_HOOK_LOGS_DIR/hook-debug.log"
|
|
50
|
+
|
|
51
|
+
# ============================================================================
|
|
52
|
+
# Internal Helpers
|
|
53
|
+
# ============================================================================
|
|
54
|
+
|
|
55
|
+
_timestamp() {
|
|
56
|
+
date '+%Y-%m-%d %H:%M:%S'
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
_rotate_log_if_needed() {
|
|
60
|
+
local log_file="$1"
|
|
61
|
+
local max_size="${2:-1048576}" # 1MB default
|
|
62
|
+
|
|
63
|
+
if [[ -f "$log_file" ]]; then
|
|
64
|
+
local size=$(stat -f%z "$log_file" 2>/dev/null || stat -c%s "$log_file" 2>/dev/null || echo 0)
|
|
65
|
+
if [[ "$size" -gt "$max_size" ]]; then
|
|
66
|
+
mv "$log_file" "${log_file}.old" 2>/dev/null || true
|
|
67
|
+
fi
|
|
68
|
+
fi
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# ============================================================================
|
|
72
|
+
# Public API: Logging Functions
|
|
73
|
+
# ============================================================================
|
|
74
|
+
|
|
75
|
+
# Log a warning - operation continues, user sees message if VERBOSE=1
|
|
76
|
+
# Usage: log_hook_warning "hook-name" "message"
|
|
77
|
+
log_hook_warning() {
|
|
78
|
+
local hook_name="${1:-unknown}"
|
|
79
|
+
local message="${2:-No message}"
|
|
80
|
+
local timestamp=$(_timestamp)
|
|
81
|
+
|
|
82
|
+
# Always log to file
|
|
83
|
+
echo "[$timestamp] WARNING [$hook_name]: $message" >> "$_HOOK_WARNING_LOG" 2>/dev/null || true
|
|
84
|
+
|
|
85
|
+
# Show to user if verbose mode
|
|
86
|
+
if [[ "${SPECWEAVE_HOOK_VERBOSE:-0}" == "1" ]]; then
|
|
87
|
+
echo ""
|
|
88
|
+
echo " WARNING [$hook_name]: $message"
|
|
89
|
+
echo ""
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
return 0
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# Log an error - operation continues, but this is significant
|
|
96
|
+
# Usage: log_hook_error "hook-name" "message" ["details"]
|
|
97
|
+
log_hook_error() {
|
|
98
|
+
local hook_name="${1:-unknown}"
|
|
99
|
+
local message="${2:-No message}"
|
|
100
|
+
local details="${3:-}"
|
|
101
|
+
local timestamp=$(_timestamp)
|
|
102
|
+
|
|
103
|
+
# Rotate log if too large
|
|
104
|
+
_rotate_log_if_needed "$_HOOK_ERROR_LOG"
|
|
105
|
+
|
|
106
|
+
# Always log to file with details
|
|
107
|
+
{
|
|
108
|
+
echo "============================================================"
|
|
109
|
+
echo "[$timestamp] ERROR [$hook_name]"
|
|
110
|
+
echo "Message: $message"
|
|
111
|
+
if [[ -n "$details" ]]; then
|
|
112
|
+
echo "Details: $details"
|
|
113
|
+
fi
|
|
114
|
+
echo "============================================================"
|
|
115
|
+
} >> "$_HOOK_ERROR_LOG" 2>/dev/null || true
|
|
116
|
+
|
|
117
|
+
# Always show errors to user (they're significant)
|
|
118
|
+
echo ""
|
|
119
|
+
echo " [Hook Warning] $hook_name: $message"
|
|
120
|
+
if [[ -n "$details" ]] && [[ "${SPECWEAVE_HOOK_DEBUG:-0}" == "1" ]]; then
|
|
121
|
+
echo " Details: $details"
|
|
122
|
+
fi
|
|
123
|
+
echo " (Operation continues - hook errors are non-blocking)"
|
|
124
|
+
echo ""
|
|
125
|
+
|
|
126
|
+
return 0
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# Log debug info - only to file if DEBUG=1
|
|
130
|
+
# Usage: log_hook_debug "hook-name" "message"
|
|
131
|
+
log_hook_debug() {
|
|
132
|
+
local hook_name="${1:-unknown}"
|
|
133
|
+
local message="${2:-}"
|
|
134
|
+
|
|
135
|
+
if [[ "${SPECWEAVE_HOOK_DEBUG:-0}" == "1" ]]; then
|
|
136
|
+
local timestamp=$(_timestamp)
|
|
137
|
+
_rotate_log_if_needed "$_HOOK_DEBUG_LOG"
|
|
138
|
+
echo "[$timestamp] DEBUG [$hook_name]: $message" >> "$_HOOK_DEBUG_LOG" 2>/dev/null || true
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
return 0
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
# Log hook start - for tracking which hooks ran
|
|
145
|
+
# Usage: log_hook_start "hook-name" ["context"]
|
|
146
|
+
log_hook_start() {
|
|
147
|
+
local hook_name="${1:-unknown}"
|
|
148
|
+
local context="${2:-}"
|
|
149
|
+
|
|
150
|
+
log_hook_debug "$hook_name" "START ${context:+- $context}"
|
|
151
|
+
return 0
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
# Log hook end - with success/failure status
|
|
155
|
+
# Usage: log_hook_end "hook-name" "success|failure" ["message"]
|
|
156
|
+
log_hook_end() {
|
|
157
|
+
local hook_name="${1:-unknown}"
|
|
158
|
+
local status="${2:-success}"
|
|
159
|
+
local message="${3:-}"
|
|
160
|
+
|
|
161
|
+
log_hook_debug "$hook_name" "END ($status) ${message:+- $message}"
|
|
162
|
+
return 0
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
# ============================================================================
|
|
166
|
+
# Public API: Safe Execution Helpers
|
|
167
|
+
# ============================================================================
|
|
168
|
+
|
|
169
|
+
# Run a command safely, logging errors but never failing
|
|
170
|
+
# Usage: safe_run "hook-name" command arg1 arg2...
|
|
171
|
+
safe_run() {
|
|
172
|
+
local hook_name="${1:-unknown}"
|
|
173
|
+
shift
|
|
174
|
+
|
|
175
|
+
local output
|
|
176
|
+
local exit_code
|
|
177
|
+
|
|
178
|
+
output=$("$@" 2>&1)
|
|
179
|
+
exit_code=$?
|
|
180
|
+
|
|
181
|
+
if [[ $exit_code -ne 0 ]]; then
|
|
182
|
+
log_hook_warning "$hook_name" "Command failed (exit $exit_code): $*"
|
|
183
|
+
log_hook_debug "$hook_name" "Output: $output"
|
|
184
|
+
fi
|
|
185
|
+
|
|
186
|
+
return 0 # Always succeed
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
# Run a file modification safely with backup
|
|
190
|
+
# Usage: safe_file_modify "hook-name" "file" "command with \$file placeholder"
|
|
191
|
+
safe_file_modify() {
|
|
192
|
+
local hook_name="${1:-unknown}"
|
|
193
|
+
local file="$2"
|
|
194
|
+
local command="$3"
|
|
195
|
+
|
|
196
|
+
if [[ ! -f "$file" ]]; then
|
|
197
|
+
log_hook_warning "$hook_name" "File not found: $file"
|
|
198
|
+
return 0
|
|
199
|
+
fi
|
|
200
|
+
|
|
201
|
+
# Create backup
|
|
202
|
+
local backup="${file}.hook-backup"
|
|
203
|
+
cp "$file" "$backup" 2>/dev/null || {
|
|
204
|
+
log_hook_warning "$hook_name" "Failed to create backup of $file"
|
|
205
|
+
return 0
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
# Run command (replace $file placeholder)
|
|
209
|
+
local actual_command="${command//\$file/$file}"
|
|
210
|
+
if ! eval "$actual_command" 2>/dev/null; then
|
|
211
|
+
log_hook_warning "$hook_name" "Modification failed, restoring backup"
|
|
212
|
+
mv "$backup" "$file" 2>/dev/null || true
|
|
213
|
+
return 0
|
|
214
|
+
fi
|
|
215
|
+
|
|
216
|
+
# Verify file is valid (not empty, not truncated)
|
|
217
|
+
if [[ ! -s "$file" ]]; then
|
|
218
|
+
log_hook_error "$hook_name" "File became empty after modification, restoring"
|
|
219
|
+
mv "$backup" "$file" 2>/dev/null || true
|
|
220
|
+
return 0
|
|
221
|
+
fi
|
|
222
|
+
|
|
223
|
+
# Success - remove backup
|
|
224
|
+
rm -f "$backup" 2>/dev/null || true
|
|
225
|
+
return 0
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
# ============================================================================
|
|
229
|
+
# Public API: Pattern Matching Helpers
|
|
230
|
+
# ============================================================================
|
|
231
|
+
|
|
232
|
+
# Check if a pattern exists in file (with logging)
|
|
233
|
+
# Usage: if pattern_exists "hook-name" "file" "pattern"; then ...
|
|
234
|
+
pattern_exists() {
|
|
235
|
+
local hook_name="${1:-unknown}"
|
|
236
|
+
local file="$2"
|
|
237
|
+
local pattern="$3"
|
|
238
|
+
|
|
239
|
+
if [[ ! -f "$file" ]]; then
|
|
240
|
+
log_hook_debug "$hook_name" "pattern_exists: file not found: $file"
|
|
241
|
+
return 1
|
|
242
|
+
fi
|
|
243
|
+
|
|
244
|
+
if grep -qE "$pattern" "$file" 2>/dev/null; then
|
|
245
|
+
log_hook_debug "$hook_name" "pattern_exists: found '$pattern' in $file"
|
|
246
|
+
return 0
|
|
247
|
+
else
|
|
248
|
+
log_hook_debug "$hook_name" "pattern_exists: NOT found '$pattern' in $file"
|
|
249
|
+
return 1
|
|
250
|
+
fi
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
# Safe sed replacement with pattern existence check
|
|
254
|
+
# Usage: safe_sed_replace "hook-name" "file" "pattern" "replacement"
|
|
255
|
+
safe_sed_replace() {
|
|
256
|
+
local hook_name="${1:-unknown}"
|
|
257
|
+
local file="$2"
|
|
258
|
+
local pattern="$3"
|
|
259
|
+
local replacement="$4"
|
|
260
|
+
|
|
261
|
+
if [[ ! -f "$file" ]]; then
|
|
262
|
+
log_hook_warning "$hook_name" "Cannot replace: file not found: $file"
|
|
263
|
+
return 0
|
|
264
|
+
fi
|
|
265
|
+
|
|
266
|
+
# Check if pattern exists first
|
|
267
|
+
if ! grep -qE "$pattern" "$file" 2>/dev/null; then
|
|
268
|
+
log_hook_debug "$hook_name" "Pattern not found (may already be updated): $pattern"
|
|
269
|
+
return 0 # Not an error - pattern may have been updated already
|
|
270
|
+
fi
|
|
271
|
+
|
|
272
|
+
# Try macOS sed first, then Linux
|
|
273
|
+
if sed -i '' "s|$pattern|$replacement|g" "$file" 2>/dev/null; then
|
|
274
|
+
log_hook_debug "$hook_name" "Replaced pattern (macOS sed)"
|
|
275
|
+
return 0
|
|
276
|
+
elif sed -i "s|$pattern|$replacement|g" "$file" 2>/dev/null; then
|
|
277
|
+
log_hook_debug "$hook_name" "Replaced pattern (Linux sed)"
|
|
278
|
+
return 0
|
|
279
|
+
else
|
|
280
|
+
log_hook_warning "$hook_name" "sed replacement failed for pattern: $pattern"
|
|
281
|
+
return 0 # Still don't fail
|
|
282
|
+
fi
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
# ============================================================================
|
|
286
|
+
# Public API: JSON Helpers
|
|
287
|
+
# ============================================================================
|
|
288
|
+
|
|
289
|
+
# Safe JSON read with fallback
|
|
290
|
+
# Usage: value=$(safe_json_get "hook-name" "file.json" ".key")
|
|
291
|
+
safe_json_get() {
|
|
292
|
+
local hook_name="${1:-unknown}"
|
|
293
|
+
local file="$2"
|
|
294
|
+
local key="$3"
|
|
295
|
+
local default="${4:-}"
|
|
296
|
+
|
|
297
|
+
if [[ ! -f "$file" ]]; then
|
|
298
|
+
echo "$default"
|
|
299
|
+
return 0
|
|
300
|
+
fi
|
|
301
|
+
|
|
302
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
303
|
+
log_hook_debug "$hook_name" "jq not available, returning default"
|
|
304
|
+
echo "$default"
|
|
305
|
+
return 0
|
|
306
|
+
fi
|
|
307
|
+
|
|
308
|
+
local value
|
|
309
|
+
value=$(jq -r "$key // empty" "$file" 2>/dev/null)
|
|
310
|
+
|
|
311
|
+
if [[ -z "$value" ]] || [[ "$value" == "null" ]]; then
|
|
312
|
+
echo "$default"
|
|
313
|
+
else
|
|
314
|
+
echo "$value"
|
|
315
|
+
fi
|
|
316
|
+
|
|
317
|
+
return 0
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
# Safe JSON update
|
|
321
|
+
# Usage: safe_json_set "hook-name" "file.json" ".key" "value"
|
|
322
|
+
safe_json_set() {
|
|
323
|
+
local hook_name="${1:-unknown}"
|
|
324
|
+
local file="$2"
|
|
325
|
+
local key="$3"
|
|
326
|
+
local value="$4"
|
|
327
|
+
|
|
328
|
+
if [[ ! -f "$file" ]]; then
|
|
329
|
+
log_hook_warning "$hook_name" "Cannot update JSON: file not found: $file"
|
|
330
|
+
return 0
|
|
331
|
+
fi
|
|
332
|
+
|
|
333
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
334
|
+
log_hook_warning "$hook_name" "jq not available, skipping JSON update"
|
|
335
|
+
return 0
|
|
336
|
+
fi
|
|
337
|
+
|
|
338
|
+
local tmp_file
|
|
339
|
+
tmp_file=$(mktemp)
|
|
340
|
+
|
|
341
|
+
if jq "$key = $value" "$file" > "$tmp_file" 2>/dev/null && [[ -s "$tmp_file" ]]; then
|
|
342
|
+
mv "$tmp_file" "$file" 2>/dev/null || {
|
|
343
|
+
log_hook_warning "$hook_name" "Failed to write JSON update"
|
|
344
|
+
rm -f "$tmp_file"
|
|
345
|
+
}
|
|
346
|
+
else
|
|
347
|
+
log_hook_warning "$hook_name" "jq update failed for $key"
|
|
348
|
+
rm -f "$tmp_file"
|
|
349
|
+
fi
|
|
350
|
+
|
|
351
|
+
return 0
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
# ============================================================================
|
|
355
|
+
# Public API: Output Helpers
|
|
356
|
+
# ============================================================================
|
|
357
|
+
|
|
358
|
+
# Output a visible warning banner to user
|
|
359
|
+
# Usage: show_hook_warning_banner "title" "message line 1" "message line 2"
|
|
360
|
+
show_hook_warning_banner() {
|
|
361
|
+
local title="$1"
|
|
362
|
+
shift
|
|
363
|
+
|
|
364
|
+
echo ""
|
|
365
|
+
echo " WARNING: $title"
|
|
366
|
+
echo " ----------------------------------------"
|
|
367
|
+
for line in "$@"; do
|
|
368
|
+
echo " $line"
|
|
369
|
+
done
|
|
370
|
+
echo " ----------------------------------------"
|
|
371
|
+
echo ""
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
# Output a non-blocking info message
|
|
375
|
+
# Usage: show_hook_info "Operation completed with warnings"
|
|
376
|
+
show_hook_info() {
|
|
377
|
+
local message="$1"
|
|
378
|
+
echo ""
|
|
379
|
+
echo " [Info] $message"
|
|
380
|
+
echo ""
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
# ============================================================================
|
|
384
|
+
# Initialization
|
|
385
|
+
# ============================================================================
|
|
386
|
+
|
|
387
|
+
# Export all functions for use in subshells
|
|
388
|
+
export -f log_hook_warning 2>/dev/null || true
|
|
389
|
+
export -f log_hook_error 2>/dev/null || true
|
|
390
|
+
export -f log_hook_debug 2>/dev/null || true
|
|
391
|
+
export -f log_hook_start 2>/dev/null || true
|
|
392
|
+
export -f log_hook_end 2>/dev/null || true
|
|
393
|
+
export -f safe_run 2>/dev/null || true
|
|
394
|
+
export -f safe_file_modify 2>/dev/null || true
|
|
395
|
+
export -f pattern_exists 2>/dev/null || true
|
|
396
|
+
export -f safe_sed_replace 2>/dev/null || true
|
|
397
|
+
export -f safe_json_get 2>/dev/null || true
|
|
398
|
+
export -f safe_json_set 2>/dev/null || true
|
|
399
|
+
export -f show_hook_warning_banner 2>/dev/null || true
|
|
400
|
+
export -f show_hook_info 2>/dev/null || true
|