rhachet-roles-ehmpathy 1.13.3 → 1.13.7
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.
|
@@ -107,29 +107,28 @@ mapfile -t ALLOWED_PATTERNS < <(
|
|
|
107
107
|
jq -r '.permissions.allow // [] | .[] | select(startswith("Bash(")) | sub("^Bash\\("; "") | sub("\\)$"; "")' "$SETTINGS_FILE" 2>/dev/null
|
|
108
108
|
)
|
|
109
109
|
|
|
110
|
-
# Check if command matches any allowed pattern
|
|
110
|
+
# Check if a single command matches any allowed pattern
|
|
111
111
|
match_pattern() {
|
|
112
112
|
local cmd="$1"
|
|
113
113
|
local pattern="$2"
|
|
114
114
|
|
|
115
115
|
# Handle Claude Code's :* suffix matcher
|
|
116
|
-
# :* means "
|
|
117
|
-
# e.g., "
|
|
116
|
+
# :* means "match anything after" (any suffix, including spaces)
|
|
117
|
+
# e.g., "mkdir:*" matches "mkdir", "mkdir /path", "mkdir -p /foo/bar"
|
|
118
|
+
# e.g., "npm run test:*" matches "npm run test", "npm run test:unit"
|
|
118
119
|
|
|
119
120
|
# First, escape regex special chars except * and :
|
|
120
121
|
local escaped_pattern
|
|
121
122
|
escaped_pattern=$(printf '%s' "$pattern" | sed 's/[.^$+?{}()[\]|\\]/\\&/g')
|
|
122
123
|
|
|
123
124
|
# Convert :* to placeholder first (to avoid * -> .* conversion interfering)
|
|
124
|
-
|
|
125
|
-
escaped_pattern="${escaped_pattern//:\*/__COLON_STAR_PLACEHOLDER__}"
|
|
125
|
+
escaped_pattern="${escaped_pattern//:\*/__ANY_SUFFIX_PLACEHOLDER__}"
|
|
126
126
|
|
|
127
127
|
# Convert remaining * to .* (glob-style wildcard)
|
|
128
128
|
escaped_pattern="${escaped_pattern//\*/.*}"
|
|
129
129
|
|
|
130
|
-
# Now replace placeholder with
|
|
131
|
-
|
|
132
|
-
escaped_pattern="${escaped_pattern//__COLON_STAR_PLACEHOLDER__/(:.*)?}"
|
|
130
|
+
# Now replace placeholder with .* for "any suffix" matching
|
|
131
|
+
escaped_pattern="${escaped_pattern//__ANY_SUFFIX_PLACEHOLDER__/.*}"
|
|
133
132
|
|
|
134
133
|
# Build final regex
|
|
135
134
|
local regex="^${escaped_pattern}$"
|
|
@@ -140,6 +139,120 @@ match_pattern() {
|
|
|
140
139
|
return 1
|
|
141
140
|
}
|
|
142
141
|
|
|
142
|
+
# Check if a single command matches ANY allowed pattern
|
|
143
|
+
command_is_allowed() {
|
|
144
|
+
local cmd="$1"
|
|
145
|
+
# Trim leading/trailing whitespace
|
|
146
|
+
cmd=$(echo "$cmd" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
147
|
+
|
|
148
|
+
# Empty commands are allowed (e.g., trailing &&)
|
|
149
|
+
if [[ -z "$cmd" ]]; then
|
|
150
|
+
return 0
|
|
151
|
+
fi
|
|
152
|
+
|
|
153
|
+
for pattern in "${ALLOWED_PATTERNS[@]}"; do
|
|
154
|
+
if match_pattern "$cmd" "$pattern"; then
|
|
155
|
+
return 0
|
|
156
|
+
fi
|
|
157
|
+
done
|
|
158
|
+
return 1
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
# Split compound command on &&, ||, ; (respecting quotes)
|
|
162
|
+
# Returns newline-separated list of commands
|
|
163
|
+
split_compound_command() {
|
|
164
|
+
local input="$1"
|
|
165
|
+
local result=""
|
|
166
|
+
local current=""
|
|
167
|
+
local in_single_quote=false
|
|
168
|
+
local in_double_quote=false
|
|
169
|
+
local i=0
|
|
170
|
+
local len=${#input}
|
|
171
|
+
|
|
172
|
+
while [[ $i -lt $len ]]; do
|
|
173
|
+
local char="${input:$i:1}"
|
|
174
|
+
local next_char="${input:$((i+1)):1}"
|
|
175
|
+
|
|
176
|
+
# Handle quotes
|
|
177
|
+
if [[ "$char" == "'" && "$in_double_quote" == false ]]; then
|
|
178
|
+
in_single_quote=$([[ "$in_single_quote" == true ]] && echo false || echo true)
|
|
179
|
+
current+="$char"
|
|
180
|
+
((i++))
|
|
181
|
+
continue
|
|
182
|
+
fi
|
|
183
|
+
|
|
184
|
+
if [[ "$char" == '"' && "$in_single_quote" == false ]]; then
|
|
185
|
+
in_double_quote=$([[ "$in_double_quote" == true ]] && echo false || echo true)
|
|
186
|
+
current+="$char"
|
|
187
|
+
((i++))
|
|
188
|
+
continue
|
|
189
|
+
fi
|
|
190
|
+
|
|
191
|
+
# Only split if not inside quotes
|
|
192
|
+
if [[ "$in_single_quote" == false && "$in_double_quote" == false ]]; then
|
|
193
|
+
# Check for && or ||
|
|
194
|
+
if [[ "$char" == "&" && "$next_char" == "&" ]] || [[ "$char" == "|" && "$next_char" == "|" ]]; then
|
|
195
|
+
if [[ -n "$result" ]]; then
|
|
196
|
+
result+=$'\n'
|
|
197
|
+
fi
|
|
198
|
+
result+="$current"
|
|
199
|
+
current=""
|
|
200
|
+
((i+=2))
|
|
201
|
+
continue
|
|
202
|
+
fi
|
|
203
|
+
|
|
204
|
+
# Check for ;
|
|
205
|
+
if [[ "$char" == ";" ]]; then
|
|
206
|
+
if [[ -n "$result" ]]; then
|
|
207
|
+
result+=$'\n'
|
|
208
|
+
fi
|
|
209
|
+
result+="$current"
|
|
210
|
+
current=""
|
|
211
|
+
((i++))
|
|
212
|
+
continue
|
|
213
|
+
fi
|
|
214
|
+
fi
|
|
215
|
+
|
|
216
|
+
current+="$char"
|
|
217
|
+
((i++))
|
|
218
|
+
done
|
|
219
|
+
|
|
220
|
+
# Add final command
|
|
221
|
+
if [[ -n "$result" ]]; then
|
|
222
|
+
result+=$'\n'
|
|
223
|
+
fi
|
|
224
|
+
result+="$current"
|
|
225
|
+
|
|
226
|
+
echo "$result"
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
# Check if ALL parts of a compound command are allowed
|
|
230
|
+
all_parts_allowed() {
|
|
231
|
+
local cmd="$1"
|
|
232
|
+
local parts
|
|
233
|
+
local failed_part=""
|
|
234
|
+
|
|
235
|
+
# Split command into parts
|
|
236
|
+
parts=$(split_compound_command "$cmd")
|
|
237
|
+
|
|
238
|
+
# Check each part
|
|
239
|
+
while IFS= read -r part; do
|
|
240
|
+
# Trim whitespace
|
|
241
|
+
part=$(echo "$part" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
242
|
+
|
|
243
|
+
# Skip empty parts
|
|
244
|
+
[[ -z "$part" ]] && continue
|
|
245
|
+
|
|
246
|
+
if ! command_is_allowed "$part"; then
|
|
247
|
+
# Store the failed part for error reporting
|
|
248
|
+
echo "$part"
|
|
249
|
+
return 1
|
|
250
|
+
fi
|
|
251
|
+
done <<< "$parts"
|
|
252
|
+
|
|
253
|
+
return 0
|
|
254
|
+
}
|
|
255
|
+
|
|
143
256
|
# Transform raw permission pattern to compact bracket notation for display
|
|
144
257
|
format_pattern() {
|
|
145
258
|
local pattern="$1"
|
|
@@ -155,13 +268,11 @@ format_pattern() {
|
|
|
155
268
|
fi
|
|
156
269
|
}
|
|
157
270
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
exit 0 # Command matches an allowed pattern
|
|
161
|
-
fi
|
|
162
|
-
done
|
|
271
|
+
# Check if all parts of the command (including compound commands) are allowed
|
|
272
|
+
FAILED_PART=$(all_parts_allowed "$COMMAND") && exit 0
|
|
163
273
|
|
|
164
274
|
# Command not matched - handle based on mode
|
|
275
|
+
# FAILED_PART contains the first disallowed part of the command
|
|
165
276
|
|
|
166
277
|
# SOFTNUDGE mode: provide guidance but don't block (early return)
|
|
167
278
|
# Output plain text - no hookSpecificOutput so normal permission flow continues
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
######################################################################
|
|
3
|
+
# .what = safe mv wrapper that constrains moves to within the repo
|
|
4
|
+
#
|
|
5
|
+
# .why = mv can move/overwrite files anywhere on the filesystem.
|
|
6
|
+
# this wrapper ensures both source and destination resolve
|
|
7
|
+
# to paths within the current working directory (repo root).
|
|
8
|
+
#
|
|
9
|
+
# .how = uses realpath to resolve absolute paths, then validates
|
|
10
|
+
# both are prefixed by $PWD before executing mv.
|
|
11
|
+
#
|
|
12
|
+
# usage:
|
|
13
|
+
# bash mvsafe.sh <source> <destination>
|
|
14
|
+
#
|
|
15
|
+
# guarantee:
|
|
16
|
+
# ✔ fails if source is outside repo
|
|
17
|
+
# ✔ fails if destination is outside repo
|
|
18
|
+
# ✔ fails if source doesn't exist
|
|
19
|
+
# ✔ passes all arguments to mv if validation passes
|
|
20
|
+
######################################################################
|
|
21
|
+
|
|
22
|
+
set -euo pipefail
|
|
23
|
+
|
|
24
|
+
if [[ $# -lt 2 ]]; then
|
|
25
|
+
echo "error: mvsafe requires at least 2 arguments" >&2
|
|
26
|
+
echo "usage: mvsafe.sh <source> <destination>" >&2
|
|
27
|
+
exit 1
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
REPO_ROOT="$PWD"
|
|
31
|
+
|
|
32
|
+
# get the last argument (destination)
|
|
33
|
+
DEST="${*: -1}"
|
|
34
|
+
|
|
35
|
+
# get all arguments except the last (sources, could be multiple)
|
|
36
|
+
SOURCES=("${@:1:$#-1}")
|
|
37
|
+
|
|
38
|
+
# resolve destination path
|
|
39
|
+
# if dest doesn't exist yet, resolve its parent directory
|
|
40
|
+
if [[ -e "$DEST" ]]; then
|
|
41
|
+
DEST_RESOLVED="$(realpath "$DEST")"
|
|
42
|
+
else
|
|
43
|
+
DEST_PARENT="$(dirname "$DEST")"
|
|
44
|
+
if [[ ! -d "$DEST_PARENT" ]]; then
|
|
45
|
+
echo "error: destination parent directory does not exist: $DEST_PARENT" >&2
|
|
46
|
+
exit 1
|
|
47
|
+
fi
|
|
48
|
+
DEST_RESOLVED="$(realpath "$DEST_PARENT")/$(basename "$DEST")"
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# validate destination is within repo
|
|
52
|
+
if [[ "$DEST_RESOLVED" != "$REPO_ROOT"* ]]; then
|
|
53
|
+
echo "error: destination is outside repo: $DEST_RESOLVED" >&2
|
|
54
|
+
echo " repo root: $REPO_ROOT" >&2
|
|
55
|
+
exit 1
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
# validate each source is within repo
|
|
59
|
+
for SRC in "${SOURCES[@]}"; do
|
|
60
|
+
if [[ ! -e "$SRC" ]]; then
|
|
61
|
+
echo "error: source does not exist: $SRC" >&2
|
|
62
|
+
exit 1
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
SRC_RESOLVED="$(realpath "$SRC")"
|
|
66
|
+
|
|
67
|
+
if [[ "$SRC_RESOLVED" != "$REPO_ROOT"* ]]; then
|
|
68
|
+
echo "error: source is outside repo: $SRC_RESOLVED" >&2
|
|
69
|
+
echo " repo root: $REPO_ROOT" >&2
|
|
70
|
+
exit 1
|
|
71
|
+
fi
|
|
72
|
+
done
|
|
73
|
+
|
|
74
|
+
# all validations passed, execute mv
|
|
75
|
+
exec mv "$@"
|
|
@@ -38,7 +38,10 @@ PERMISSIONS_CONFIG=$(cat <<'EOF'
|
|
|
38
38
|
"Bash(sed:*)",
|
|
39
39
|
"Bash(tee:*)",
|
|
40
40
|
"Bash(find:*)",
|
|
41
|
-
"Bash(echo:*)"
|
|
41
|
+
"Bash(echo:*)",
|
|
42
|
+
"Bash(mv:*)",
|
|
43
|
+
"Bash(npx biome:*)",
|
|
44
|
+
"Bash(npx jest:*)",
|
|
42
45
|
],
|
|
43
46
|
"ask": [
|
|
44
47
|
"Bash(bash:*)",
|
|
@@ -55,7 +58,6 @@ PERMISSIONS_CONFIG=$(cat <<'EOF'
|
|
|
55
58
|
"WebFetch(domain:raw.githubusercontent.com)",
|
|
56
59
|
"WebFetch(domain:biomejs.dev)",
|
|
57
60
|
|
|
58
|
-
|
|
59
61
|
"Bash(ls:*)",
|
|
60
62
|
"Bash(tree:*)",
|
|
61
63
|
"Bash(cat:*)",
|
|
@@ -68,15 +70,13 @@ PERMISSIONS_CONFIG=$(cat <<'EOF'
|
|
|
68
70
|
"Bash(file:*)",
|
|
69
71
|
"Bash(mkdir:*)",
|
|
70
72
|
"Bash(pwd)",
|
|
73
|
+
"Bash(bash src/logic/roles/mechanic/.skills/claude.tools/mvsafe.sh:*)",
|
|
71
74
|
"Bash(npm view:*)",
|
|
72
75
|
"Bash(npm list:*)",
|
|
73
76
|
"Bash(npm remove:*)",
|
|
74
77
|
|
|
75
78
|
"Bash(npx rhachet roles boot --repo ehmpathy --role mechanic)",
|
|
76
79
|
"Bash(npx tsx ./bin/run:*)",
|
|
77
|
-
"Bash(npx tsc:*)",
|
|
78
|
-
"Bash(npx biome:*)",
|
|
79
|
-
"Bash(npx jest:*)",
|
|
80
80
|
|
|
81
81
|
"Bash(npm run build:*)",
|
|
82
82
|
"Bash(npm run build:compile)",
|
|
@@ -92,6 +92,9 @@ PERMISSIONS_CONFIG=$(cat <<'EOF'
|
|
|
92
92
|
"Bash(npm run test:acceptance:*)",
|
|
93
93
|
|
|
94
94
|
"Bash(THOROUGH=true npm run test:*)",
|
|
95
|
+
"Bash(THOROUGH=true npm run test:types:*)",
|
|
96
|
+
"Bash(THOROUGH=true npm run test:format:*)",
|
|
97
|
+
"Bash(THOROUGH=true npm run test:lint:*)",
|
|
95
98
|
"Bash(THOROUGH=true npm run test:unit:*)",
|
|
96
99
|
"Bash(THOROUGH=true npm run test:integration:*)",
|
|
97
100
|
"Bash(THOROUGH=true npm run test:acceptance:*)",
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "rhachet-roles-ehmpathy",
|
|
3
3
|
"author": "ehmpathy",
|
|
4
4
|
"description": "empathetic software construction roles and skills, via rhachet",
|
|
5
|
-
"version": "1.13.
|
|
5
|
+
"version": "1.13.7",
|
|
6
6
|
"repository": "ehmpathy/rhachet-roles-ehmpathy",
|
|
7
7
|
"homepage": "https://github.com/ehmpathy/rhachet-roles-ehmpathy",
|
|
8
8
|
"keywords": [
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
"test:format": "npm run test:format:biome",
|
|
35
35
|
"test:lint:deps": "npx depcheck -c ./.depcheckrc.yml",
|
|
36
36
|
"test:integration:hold": "jest -c ./jest.integration.config.ts --forceExit --verbose --passWithNoTests $([ -z $THOROUGH ] && echo '--changedSince=main')",
|
|
37
|
-
"test:lint:biome": "biome check
|
|
38
|
-
"test:lint:biome:all": "biome check
|
|
37
|
+
"test:lint:biome": "biome check --diagnostic-level=error",
|
|
38
|
+
"test:lint:biome:all": "biome check",
|
|
39
39
|
"test:lint": "npm run test:lint:biome && npm run test:lint:deps",
|
|
40
40
|
"test:unit": "jest -c ./jest.unit.config.ts --forceExit --verbose --passWithNoTests $([ -z $THOROUGH ] && echo '--changedSince=main')",
|
|
41
41
|
"test:integration": "echo 'todo: release'",
|
|
@@ -48,13 +48,13 @@
|
|
|
48
48
|
"postversion": "git push origin HEAD --tags --no-verify",
|
|
49
49
|
"prepare:husky": "npx husky install && chmod ug+x .husky/*",
|
|
50
50
|
"prepare": "[ -e .git ] && npm run prepare:husky || exit 0",
|
|
51
|
-
"test:format:biome": "biome format
|
|
51
|
+
"test:format:biome": "biome format"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@ehmpathy/as-command": "1.0.3",
|
|
55
55
|
"@ehmpathy/uni-time": "1.8.1",
|
|
56
56
|
"as-procedure": "1.1.7",
|
|
57
|
-
"domain-objects": "0.31.
|
|
57
|
+
"domain-objects": "0.31.7",
|
|
58
58
|
"fast-glob": "3.3.3",
|
|
59
59
|
"helpful-errors": "1.5.3",
|
|
60
60
|
"inquirer": "12.7.0",
|
|
@@ -80,9 +80,9 @@
|
|
|
80
80
|
"@types/node": "22.15.21",
|
|
81
81
|
"cz-conventional-changelog": "3.3.0",
|
|
82
82
|
"declapract": "0.13.0",
|
|
83
|
-
"declapract-typescript-ehmpathy": "0.43.
|
|
84
|
-
"declastruct": "1.
|
|
85
|
-
"declastruct-github": "1.0.
|
|
83
|
+
"declapract-typescript-ehmpathy": "0.43.16",
|
|
84
|
+
"declastruct": "1.5.1",
|
|
85
|
+
"declastruct-github": "1.0.7",
|
|
86
86
|
"depcheck": "1.4.3",
|
|
87
87
|
"esbuild-register": "3.6.0",
|
|
88
88
|
"husky": "8.0.3",
|