@soleri/cli 9.3.1 → 9.5.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/dist/commands/agent.js +51 -2
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/hooks.js +126 -0
- package/dist/commands/hooks.js.map +1 -1
- package/dist/commands/install.js +5 -0
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/pack.js +62 -13
- package/dist/commands/pack.js.map +1 -1
- package/dist/commands/staging.d.ts +49 -0
- package/dist/commands/staging.js +108 -18
- package/dist/commands/staging.js.map +1 -1
- package/dist/commands/yolo.d.ts +2 -0
- package/dist/commands/yolo.js +86 -0
- package/dist/commands/yolo.js.map +1 -0
- package/dist/hook-packs/converter/README.md +99 -0
- package/dist/hook-packs/converter/template.d.ts +36 -0
- package/dist/hook-packs/converter/template.js +127 -0
- package/dist/hook-packs/converter/template.js.map +1 -0
- package/dist/hook-packs/converter/template.test.ts +133 -0
- package/dist/hook-packs/converter/template.ts +163 -0
- package/dist/hook-packs/flock-guard/README.md +65 -0
- package/dist/hook-packs/flock-guard/manifest.json +36 -0
- package/dist/hook-packs/flock-guard/scripts/flock-guard-post.sh +48 -0
- package/dist/hook-packs/flock-guard/scripts/flock-guard-pre.sh +85 -0
- package/dist/hook-packs/full/manifest.json +8 -1
- package/dist/hook-packs/graduation.d.ts +11 -0
- package/dist/hook-packs/graduation.js +48 -0
- package/dist/hook-packs/graduation.js.map +1 -0
- package/dist/hook-packs/graduation.ts +65 -0
- package/dist/hook-packs/installer.js +3 -1
- package/dist/hook-packs/installer.js.map +1 -1
- package/dist/hook-packs/installer.ts +3 -1
- package/dist/hook-packs/marketing-research/README.md +37 -0
- package/dist/hook-packs/marketing-research/manifest.json +24 -0
- package/dist/hook-packs/marketing-research/scripts/marketing-research.sh +70 -0
- package/dist/hook-packs/registry.d.ts +1 -0
- package/dist/hook-packs/registry.js +14 -4
- package/dist/hook-packs/registry.js.map +1 -1
- package/dist/hook-packs/registry.ts +18 -4
- package/dist/hook-packs/safety/README.md +50 -0
- package/dist/hook-packs/safety/manifest.json +23 -0
- package/dist/hook-packs/safety/scripts/anti-deletion.sh +280 -0
- package/dist/hook-packs/validator.d.ts +32 -0
- package/dist/hook-packs/validator.js +126 -0
- package/dist/hook-packs/validator.js.map +1 -0
- package/dist/hook-packs/validator.ts +158 -0
- package/dist/hook-packs/yolo-safety/manifest.json +3 -19
- package/dist/hook-packs/yolo-safety/scripts/anti-deletion.sh +121 -61
- package/dist/main.js +2 -0
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/flock-guard.test.ts +225 -0
- package/src/__tests__/graduation.test.ts +199 -0
- package/src/__tests__/hook-packs.test.ts +45 -20
- package/src/__tests__/hooks-convert.test.ts +342 -0
- package/src/__tests__/validator.test.ts +265 -0
- package/src/__tests__/wizard-e2e.mjs +1 -1
- package/src/commands/agent.ts +65 -2
- package/src/commands/hooks.ts +172 -0
- package/src/commands/install.ts +6 -0
- package/src/commands/pack.ts +80 -14
- package/src/commands/staging.ts +143 -20
- package/src/commands/yolo.ts +103 -0
- package/src/hook-packs/converter/README.md +99 -0
- package/src/hook-packs/converter/template.test.ts +133 -0
- package/src/hook-packs/converter/template.ts +163 -0
- package/src/hook-packs/flock-guard/README.md +65 -0
- package/src/hook-packs/flock-guard/manifest.json +36 -0
- package/src/hook-packs/flock-guard/scripts/flock-guard-post.sh +48 -0
- package/src/hook-packs/flock-guard/scripts/flock-guard-pre.sh +85 -0
- package/src/hook-packs/full/manifest.json +8 -1
- package/src/hook-packs/graduation.ts +65 -0
- package/src/hook-packs/installer.ts +3 -1
- package/src/hook-packs/marketing-research/README.md +37 -0
- package/src/hook-packs/marketing-research/manifest.json +24 -0
- package/src/hook-packs/marketing-research/scripts/marketing-research.sh +70 -0
- package/src/hook-packs/registry.ts +18 -4
- package/src/hook-packs/safety/README.md +50 -0
- package/src/hook-packs/safety/manifest.json +23 -0
- package/src/hook-packs/safety/scripts/anti-deletion.sh +280 -0
- package/src/hook-packs/validator.ts +158 -0
- package/src/hook-packs/yolo-safety/manifest.json +3 -19
- package/src/main.ts +2 -0
- package/vitest.config.ts +1 -0
- package/src/__tests__/archetypes.test.ts +0 -84
- package/src/__tests__/create.test.ts +0 -207
- package/src/hook-packs/yolo-safety/scripts/anti-deletion.sh +0 -214
- package/src/prompts/archetypes.ts +0 -343
|
@@ -1,21 +1,31 @@
|
|
|
1
|
-
#!/
|
|
1
|
+
#!/bin/sh
|
|
2
2
|
# Anti-Deletion Staging Hook for Claude Code (Soleri Hook Pack: yolo-safety)
|
|
3
|
-
# PreToolUse -> Bash: intercepts
|
|
4
|
-
#
|
|
3
|
+
# PreToolUse -> Bash: intercepts destructive commands, stages files, blocks execution.
|
|
4
|
+
#
|
|
5
|
+
# Intercepted patterns:
|
|
6
|
+
# - rm / rmdir (files/dirs — stages first, then blocks)
|
|
7
|
+
# - git push --force (blocks outright)
|
|
8
|
+
# - git reset --hard (blocks outright)
|
|
9
|
+
# - git clean (blocks outright)
|
|
10
|
+
# - git checkout -- . (blocks outright)
|
|
11
|
+
# - git restore . (blocks outright)
|
|
12
|
+
# - mv ~/projects/... (blocks outright)
|
|
13
|
+
# - drop table (SQL — blocks outright)
|
|
14
|
+
# - docker rm / rmi (blocks outright)
|
|
5
15
|
#
|
|
6
16
|
# Catastrophic commands (rm -rf /, rm -rf ~) should stay in deny rules —
|
|
7
17
|
# this hook handles targeted deletes only.
|
|
8
18
|
#
|
|
9
|
-
# Dependencies: jq (required)
|
|
19
|
+
# Dependencies: jq (required)
|
|
20
|
+
# POSIX sh compatible — no bash-specific features.
|
|
10
21
|
|
|
11
|
-
set -
|
|
22
|
+
set -eu
|
|
12
23
|
|
|
13
24
|
STAGING_ROOT="$HOME/.soleri/staging"
|
|
14
|
-
PROJECTS_DIR="$HOME/projects"
|
|
15
25
|
INPUT=$(cat)
|
|
16
26
|
|
|
17
27
|
# Extract the command from stdin JSON
|
|
18
|
-
CMD=$(
|
|
28
|
+
CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
19
29
|
|
|
20
30
|
# No command found — let it through
|
|
21
31
|
if [ -z "$CMD" ]; then
|
|
@@ -26,12 +36,17 @@ fi
|
|
|
26
36
|
# Commands like: gh issue comment --body "$(cat <<'EOF' ... rmdir ... EOF)"
|
|
27
37
|
# contain destructive keywords in text, not as actual commands.
|
|
28
38
|
|
|
29
|
-
# Remove heredoc blocks
|
|
30
|
-
STRIPPED=$(
|
|
31
|
-
# Remove double-quoted strings
|
|
32
|
-
STRIPPED=$(
|
|
39
|
+
# Remove heredoc blocks (best-effort with sed)
|
|
40
|
+
STRIPPED=$(printf '%s' "$CMD" | sed -e "s/<<'[A-Za-z_]*'.*//g" -e 's/<<[A-Za-z_]*.*//g' 2>/dev/null || printf '%s' "$CMD")
|
|
41
|
+
# Remove double-quoted strings
|
|
42
|
+
STRIPPED=$(printf '%s' "$STRIPPED" | sed 's/"[^"]*"//g' 2>/dev/null || printf '%s' "$STRIPPED")
|
|
33
43
|
# Remove single-quoted strings
|
|
34
|
-
STRIPPED=$(
|
|
44
|
+
STRIPPED=$(printf '%s' "$STRIPPED" | sed "s/'[^']*'//g" 2>/dev/null || printf '%s' "$STRIPPED")
|
|
45
|
+
|
|
46
|
+
# --- Helper: check if pattern matches stripped command ---
|
|
47
|
+
matches() {
|
|
48
|
+
printf '%s' "$STRIPPED" | grep -qE "$1"
|
|
49
|
+
}
|
|
35
50
|
|
|
36
51
|
# --- Detect destructive commands (on stripped command only) ---
|
|
37
52
|
|
|
@@ -42,56 +57,78 @@ IS_GIT_CLEAN=false
|
|
|
42
57
|
IS_RESET_HARD=false
|
|
43
58
|
IS_GIT_CHECKOUT_DOT=false
|
|
44
59
|
IS_GIT_RESTORE_DOT=false
|
|
60
|
+
IS_GIT_PUSH_FORCE=false
|
|
61
|
+
IS_DROP_TABLE=false
|
|
62
|
+
IS_DOCKER_RM=false
|
|
45
63
|
|
|
46
|
-
#
|
|
47
|
-
if
|
|
48
|
-
if !
|
|
64
|
+
# rm (but not git rm which stages, doesn't destroy)
|
|
65
|
+
if matches '(^|\s|;|&&|\|\|)rm\s'; then
|
|
66
|
+
if ! matches '(^|\s)git\s+rm\s'; then
|
|
49
67
|
IS_RM=true
|
|
50
68
|
fi
|
|
51
69
|
fi
|
|
52
70
|
|
|
53
|
-
#
|
|
54
|
-
if
|
|
71
|
+
# rmdir
|
|
72
|
+
if matches '(^|\s|;|&&|\|\|)rmdir\s'; then
|
|
55
73
|
IS_RMDIR=true
|
|
56
74
|
fi
|
|
57
75
|
|
|
58
|
-
#
|
|
59
|
-
if
|
|
60
|
-
|
|
61
|
-
if
|
|
76
|
+
# mv of project directories or git repos
|
|
77
|
+
if matches '(^|\s|;|&&|\|\|)mv\s'; then
|
|
78
|
+
MV_TAIL=$(printf '%s' "$STRIPPED" | sed 's/^.*\bmv //' | sed 's/-[finv] //g')
|
|
79
|
+
if printf '%s' "$MV_TAIL" | grep -qE '(~/projects|\.git)'; then
|
|
62
80
|
IS_MV_PROJECT=true
|
|
63
81
|
fi
|
|
64
82
|
fi
|
|
65
83
|
|
|
66
|
-
#
|
|
67
|
-
if
|
|
84
|
+
# git clean
|
|
85
|
+
if matches '(^|\s|;|&&|\|\|)git\s+clean\b'; then
|
|
68
86
|
IS_GIT_CLEAN=true
|
|
69
87
|
fi
|
|
70
88
|
|
|
71
|
-
#
|
|
72
|
-
if
|
|
89
|
+
# git reset --hard
|
|
90
|
+
if matches '(^|\s|;|&&|\|\|)git\s+reset\s+--hard'; then
|
|
73
91
|
IS_RESET_HARD=true
|
|
74
92
|
fi
|
|
75
93
|
|
|
76
|
-
#
|
|
77
|
-
if
|
|
94
|
+
# git checkout -- .
|
|
95
|
+
if matches '(^|\s|;|&&|\|\|)git\s+checkout\s+--\s+\.'; then
|
|
78
96
|
IS_GIT_CHECKOUT_DOT=true
|
|
79
97
|
fi
|
|
80
98
|
|
|
81
|
-
#
|
|
82
|
-
if
|
|
99
|
+
# git restore .
|
|
100
|
+
if matches '(^|\s|;|&&|\|\|)git\s+restore\s+\.'; then
|
|
83
101
|
IS_GIT_RESTORE_DOT=true
|
|
84
102
|
fi
|
|
85
103
|
|
|
86
|
-
#
|
|
104
|
+
# git push --force / -f (but not --force-with-lease which is safer)
|
|
105
|
+
if matches '(^|\s|;|&&|\|\|)git\s+push\s'; then
|
|
106
|
+
if matches 'git\s+push\s.*--force([^-]|$)' || matches 'git\s+push\s+-f(\s|$)' || matches 'git\s+push\s.*\s-f(\s|$)'; then
|
|
107
|
+
IS_GIT_PUSH_FORCE=true
|
|
108
|
+
fi
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
# SQL drop table (case-insensitive)
|
|
112
|
+
if printf '%s' "$STRIPPED" | grep -qiE '(^|\s|;)drop\s+table'; then
|
|
113
|
+
IS_DROP_TABLE=true
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
# docker rm / docker rmi
|
|
117
|
+
if matches '(^|\s|;|&&|\|\|)docker\s+(rm|rmi)\b'; then
|
|
118
|
+
IS_DOCKER_RM=true
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
# --- Not a destructive command — let it through ---
|
|
122
|
+
|
|
87
123
|
if [ "$IS_RM" = false ] && [ "$IS_RMDIR" = false ] && [ "$IS_MV_PROJECT" = false ] && \
|
|
88
124
|
[ "$IS_GIT_CLEAN" = false ] && [ "$IS_RESET_HARD" = false ] && \
|
|
89
|
-
[ "$IS_GIT_CHECKOUT_DOT" = false ] && [ "$IS_GIT_RESTORE_DOT" = false ]
|
|
125
|
+
[ "$IS_GIT_CHECKOUT_DOT" = false ] && [ "$IS_GIT_RESTORE_DOT" = false ] && \
|
|
126
|
+
[ "$IS_GIT_PUSH_FORCE" = false ] && [ "$IS_DROP_TABLE" = false ] && \
|
|
127
|
+
[ "$IS_DOCKER_RM" = false ]; then
|
|
90
128
|
exit 0
|
|
91
129
|
fi
|
|
92
130
|
|
|
93
|
-
# ---
|
|
94
|
-
|
|
131
|
+
# --- Block: git clean ---
|
|
95
132
|
if [ "$IS_GIT_CLEAN" = true ]; then
|
|
96
133
|
jq -n '{
|
|
97
134
|
continue: false,
|
|
@@ -100,8 +137,7 @@ if [ "$IS_GIT_CLEAN" = true ]; then
|
|
|
100
137
|
exit 0
|
|
101
138
|
fi
|
|
102
139
|
|
|
103
|
-
# ---
|
|
104
|
-
|
|
140
|
+
# --- Block: git reset --hard ---
|
|
105
141
|
if [ "$IS_RESET_HARD" = true ]; then
|
|
106
142
|
jq -n '{
|
|
107
143
|
continue: false,
|
|
@@ -110,8 +146,7 @@ if [ "$IS_RESET_HARD" = true ]; then
|
|
|
110
146
|
exit 0
|
|
111
147
|
fi
|
|
112
148
|
|
|
113
|
-
# ---
|
|
114
|
-
|
|
149
|
+
# --- Block: git checkout -- . ---
|
|
115
150
|
if [ "$IS_GIT_CHECKOUT_DOT" = true ]; then
|
|
116
151
|
jq -n '{
|
|
117
152
|
continue: false,
|
|
@@ -120,8 +155,7 @@ if [ "$IS_GIT_CHECKOUT_DOT" = true ]; then
|
|
|
120
155
|
exit 0
|
|
121
156
|
fi
|
|
122
157
|
|
|
123
|
-
# ---
|
|
124
|
-
|
|
158
|
+
# --- Block: git restore . ---
|
|
125
159
|
if [ "$IS_GIT_RESTORE_DOT" = true ]; then
|
|
126
160
|
jq -n '{
|
|
127
161
|
continue: false,
|
|
@@ -130,8 +164,16 @@ if [ "$IS_GIT_RESTORE_DOT" = true ]; then
|
|
|
130
164
|
exit 0
|
|
131
165
|
fi
|
|
132
166
|
|
|
133
|
-
# ---
|
|
167
|
+
# --- Block: git push --force ---
|
|
168
|
+
if [ "$IS_GIT_PUSH_FORCE" = true ]; then
|
|
169
|
+
jq -n '{
|
|
170
|
+
continue: false,
|
|
171
|
+
stopReason: "BLOCKED: git push --force can overwrite remote history and cause data loss for collaborators. Use --force-with-lease instead, or ask the user to run this manually."
|
|
172
|
+
}'
|
|
173
|
+
exit 0
|
|
174
|
+
fi
|
|
134
175
|
|
|
176
|
+
# --- Block: mv of project directories ---
|
|
135
177
|
if [ "$IS_MV_PROJECT" = true ]; then
|
|
136
178
|
jq -n '{
|
|
137
179
|
continue: false,
|
|
@@ -140,8 +182,7 @@ if [ "$IS_MV_PROJECT" = true ]; then
|
|
|
140
182
|
exit 0
|
|
141
183
|
fi
|
|
142
184
|
|
|
143
|
-
# ---
|
|
144
|
-
|
|
185
|
+
# --- Block: rmdir ---
|
|
145
186
|
if [ "$IS_RMDIR" = true ]; then
|
|
146
187
|
jq -n '{
|
|
147
188
|
continue: false,
|
|
@@ -150,6 +191,24 @@ if [ "$IS_RMDIR" = true ]; then
|
|
|
150
191
|
exit 0
|
|
151
192
|
fi
|
|
152
193
|
|
|
194
|
+
# --- Block: drop table ---
|
|
195
|
+
if [ "$IS_DROP_TABLE" = true ]; then
|
|
196
|
+
jq -n '{
|
|
197
|
+
continue: false,
|
|
198
|
+
stopReason: "BLOCKED: DROP TABLE detected. This would permanently destroy database data. Ask the user to run this SQL statement manually after confirming intent."
|
|
199
|
+
}'
|
|
200
|
+
exit 0
|
|
201
|
+
fi
|
|
202
|
+
|
|
203
|
+
# --- Block: docker rm / rmi ---
|
|
204
|
+
if [ "$IS_DOCKER_RM" = true ]; then
|
|
205
|
+
jq -n '{
|
|
206
|
+
continue: false,
|
|
207
|
+
stopReason: "BLOCKED: docker rm/rmi detected. Removing containers or images can cause data loss. Ask the user to run this manually."
|
|
208
|
+
}'
|
|
209
|
+
exit 0
|
|
210
|
+
fi
|
|
211
|
+
|
|
153
212
|
# --- Handle rm commands — copy to staging, then block ---
|
|
154
213
|
|
|
155
214
|
# Create timestamped staging directory
|
|
@@ -158,7 +217,7 @@ STAGE_DIR="$STAGING_ROOT/$TIMESTAMP"
|
|
|
158
217
|
|
|
159
218
|
# Extract file paths from the rm command
|
|
160
219
|
# Strip rm and its flags, keeping only the file arguments
|
|
161
|
-
FILES=$(
|
|
220
|
+
FILES=$(printf '%s' "$CMD" | sed 's/^.*\brm //' | sed 's/-[rRfivd]* //g' | tr ' ' '\n' | grep -v '^-' | grep -v '^$' || true)
|
|
162
221
|
|
|
163
222
|
if [ -z "$FILES" ]; then
|
|
164
223
|
jq -n '{
|
|
@@ -168,14 +227,15 @@ if [ -z "$FILES" ]; then
|
|
|
168
227
|
exit 0
|
|
169
228
|
fi
|
|
170
229
|
|
|
171
|
-
|
|
172
|
-
|
|
230
|
+
STAGED_COUNT=0
|
|
231
|
+
STAGED_LIST=""
|
|
232
|
+
MISSING_COUNT=0
|
|
173
233
|
|
|
174
234
|
mkdir -p "$STAGE_DIR"
|
|
175
235
|
|
|
176
|
-
while IFS= read -r filepath; do
|
|
236
|
+
printf '%s\n' "$FILES" | while IFS= read -r filepath; do
|
|
177
237
|
# Expand path (handle ~, relative paths)
|
|
178
|
-
expanded=$(eval
|
|
238
|
+
expanded=$(eval printf '%s' "$filepath" 2>/dev/null || printf '%s' "$filepath")
|
|
179
239
|
|
|
180
240
|
if [ -e "$expanded" ]; then
|
|
181
241
|
# Preserve directory structure in staging
|
|
@@ -183,32 +243,32 @@ while IFS= read -r filepath; do
|
|
|
183
243
|
mkdir -p "$target_dir"
|
|
184
244
|
# COPY instead of MOVE — originals stay intact, staging is a backup
|
|
185
245
|
if [ -d "$expanded" ]; then
|
|
186
|
-
|
|
246
|
+
# Use rsync if available (excludes node_modules/dist/.git), fall back to cp
|
|
247
|
+
if command -v rsync >/dev/null 2>&1; then
|
|
248
|
+
rsync -a --exclude='node_modules' --exclude='dist' --exclude='.git' "$expanded/" "$target_dir/$(basename "$expanded")/" 2>/dev/null
|
|
249
|
+
else
|
|
250
|
+
cp -R "$expanded" "$target_dir/" 2>/dev/null
|
|
251
|
+
fi
|
|
187
252
|
else
|
|
188
|
-
cp "$expanded" "$target_dir/" 2>/dev/null
|
|
253
|
+
cp "$expanded" "$target_dir/" 2>/dev/null
|
|
189
254
|
fi
|
|
190
|
-
else
|
|
191
|
-
MISSING+=("$expanded")
|
|
192
255
|
fi
|
|
193
|
-
done
|
|
256
|
+
done
|
|
194
257
|
|
|
195
|
-
#
|
|
196
|
-
|
|
197
|
-
|
|
258
|
+
# Count what was staged (check if staging dir has content)
|
|
259
|
+
if [ -d "$STAGE_DIR" ] && [ "$(ls -A "$STAGE_DIR" 2>/dev/null)" ]; then
|
|
260
|
+
STAGED_COUNT=$(find "$STAGE_DIR" -mindepth 1 -maxdepth 1 | wc -l | tr -d ' ')
|
|
261
|
+
fi
|
|
198
262
|
|
|
199
|
-
if [ "$STAGED_COUNT" -eq 0 ]
|
|
263
|
+
if [ "$STAGED_COUNT" -eq 0 ]; then
|
|
200
264
|
# All files were missing — let the rm fail naturally
|
|
201
265
|
rmdir "$STAGE_DIR" 2>/dev/null || true
|
|
202
266
|
exit 0
|
|
203
267
|
fi
|
|
204
268
|
|
|
205
|
-
STAGED_LIST=$(printf '%s, ' "${STAGED[@]}" | sed 's/, $//')
|
|
206
|
-
|
|
207
269
|
jq -n \
|
|
208
|
-
--arg staged "$STAGED_LIST" \
|
|
209
270
|
--arg dir "$STAGE_DIR" \
|
|
210
|
-
--argjson count "$STAGED_COUNT" \
|
|
211
271
|
'{
|
|
212
272
|
continue: false,
|
|
213
|
-
stopReason: ("BLOCKED & BACKED UP:
|
|
273
|
+
stopReason: ("BLOCKED & BACKED UP: Files copied to " + $dir + ". The originals are untouched. To proceed with deletion, ask the user to run the rm command manually.")
|
|
214
274
|
}'
|
package/dist/main.js
CHANGED
|
@@ -20,6 +20,7 @@ import { registerSkills } from './commands/skills.js';
|
|
|
20
20
|
import { registerAgent } from './commands/agent.js';
|
|
21
21
|
import { registerTelegram } from './commands/telegram.js';
|
|
22
22
|
import { registerStaging } from './commands/staging.js';
|
|
23
|
+
import { registerYolo } from './commands/yolo.js';
|
|
23
24
|
const require = createRequire(import.meta.url);
|
|
24
25
|
const { version } = require('../package.json');
|
|
25
26
|
const RESET = '\x1b[0m';
|
|
@@ -75,5 +76,6 @@ registerSkills(program);
|
|
|
75
76
|
registerAgent(program);
|
|
76
77
|
registerTelegram(program);
|
|
77
78
|
registerStaging(program);
|
|
79
|
+
registerYolo(program);
|
|
78
80
|
program.parse();
|
|
79
81
|
//# sourceMappingURL=main.js.map
|
package/dist/main.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,wBAAwB,EAAE,MAAM,iCAAiC,CAAC;AAC3E,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
|
+
import { execSync } from 'node:child_process';
|
|
3
|
+
import { mkdirSync, rmSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
|
|
8
|
+
const SCRIPTS_DIR = join(__dirname, '..', 'hook-packs', 'flock-guard', 'scripts');
|
|
9
|
+
const PRE_SCRIPT = join(SCRIPTS_DIR, 'flock-guard-pre.sh');
|
|
10
|
+
const POST_SCRIPT = join(SCRIPTS_DIR, 'flock-guard-post.sh');
|
|
11
|
+
|
|
12
|
+
// The scripts use `git rev-parse --show-toplevel` which resolves to the repo root.
|
|
13
|
+
// Compute the same hash the scripts will produce.
|
|
14
|
+
const PROJECT_ROOT = execSync('git rev-parse --show-toplevel', {
|
|
15
|
+
cwd: join(__dirname, '..'),
|
|
16
|
+
encoding: 'utf-8',
|
|
17
|
+
}).trim();
|
|
18
|
+
const PROJECT_HASH = execSync(`printf '%s' '${PROJECT_ROOT}' | shasum | cut -c1-8`, {
|
|
19
|
+
encoding: 'utf-8',
|
|
20
|
+
}).trim();
|
|
21
|
+
// Scripts use ${TMPDIR:-${TEMP:-/tmp}} — match that resolution for the test environment
|
|
22
|
+
const LOCK_DIR = `${process.env.TMPDIR || process.env.TEMP || tmpdir()}/soleri-guard-${PROJECT_HASH}.lock`;
|
|
23
|
+
|
|
24
|
+
function makePayload(command: string): string {
|
|
25
|
+
return JSON.stringify({ tool_name: 'Bash', tool_input: { command } });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function runPre(
|
|
29
|
+
command: string,
|
|
30
|
+
env?: Record<string, string>,
|
|
31
|
+
): { stdout: string; exitCode: number } {
|
|
32
|
+
try {
|
|
33
|
+
const stdout = execSync(
|
|
34
|
+
`printf '%s' '${escapeShell(makePayload(command))}' | sh '${PRE_SCRIPT}'`,
|
|
35
|
+
{
|
|
36
|
+
encoding: 'utf-8',
|
|
37
|
+
stdio: 'pipe',
|
|
38
|
+
cwd: PROJECT_ROOT,
|
|
39
|
+
env: { ...process.env, ...env },
|
|
40
|
+
},
|
|
41
|
+
);
|
|
42
|
+
return { stdout, exitCode: 0 };
|
|
43
|
+
} catch (err: any) {
|
|
44
|
+
return { stdout: err.stdout ?? '', exitCode: err.status ?? 1 };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function runPost(
|
|
49
|
+
command: string,
|
|
50
|
+
env?: Record<string, string>,
|
|
51
|
+
): { stdout: string; exitCode: number } {
|
|
52
|
+
try {
|
|
53
|
+
const stdout = execSync(
|
|
54
|
+
`printf '%s' '${escapeShell(makePayload(command))}' | sh '${POST_SCRIPT}'`,
|
|
55
|
+
{
|
|
56
|
+
encoding: 'utf-8',
|
|
57
|
+
stdio: 'pipe',
|
|
58
|
+
cwd: PROJECT_ROOT,
|
|
59
|
+
env: { ...process.env, ...env },
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
return { stdout, exitCode: 0 };
|
|
63
|
+
} catch (err: any) {
|
|
64
|
+
return { stdout: err.stdout ?? '', exitCode: err.status ?? 1 };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function escapeShell(s: string): string {
|
|
69
|
+
// Escape single quotes for use inside single-quoted shell string
|
|
70
|
+
return s.replace(/'/g, "'\\''");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function cleanLock(): void {
|
|
74
|
+
if (existsSync(LOCK_DIR)) {
|
|
75
|
+
rmSync(LOCK_DIR, { recursive: true, force: true });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
describe('flock-guard hook pack', () => {
|
|
80
|
+
afterEach(() => {
|
|
81
|
+
cleanLock();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// 1. Pre: allows non-lockfile commands
|
|
85
|
+
it('pre: allows non-lockfile commands (exit 0, no output)', () => {
|
|
86
|
+
const { stdout, exitCode } = runPre('echo hello');
|
|
87
|
+
expect(exitCode).toBe(0);
|
|
88
|
+
expect(stdout.trim()).toBe('');
|
|
89
|
+
expect(existsSync(LOCK_DIR)).toBe(false);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// 2. Pre: acquires lock on npm install
|
|
93
|
+
it('pre: acquires lock on npm install', () => {
|
|
94
|
+
const sessionId = `test-acquire-${Date.now()}`;
|
|
95
|
+
const { exitCode } = runPre('npm install', { CLAUDE_SESSION_ID: sessionId });
|
|
96
|
+
expect(exitCode).toBe(0);
|
|
97
|
+
expect(existsSync(LOCK_DIR)).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// 3. Pre: lock dir contains valid JSON with agentId and timestamp
|
|
101
|
+
it('pre: lock dir contains valid JSON with agentId and timestamp', () => {
|
|
102
|
+
const sessionId = `test-json-${Date.now()}`;
|
|
103
|
+
runPre('npm install', { CLAUDE_SESSION_ID: sessionId });
|
|
104
|
+
|
|
105
|
+
const lockJson = JSON.parse(readFileSync(join(LOCK_DIR, 'lock.json'), 'utf-8'));
|
|
106
|
+
expect(lockJson).toHaveProperty('agentId', sessionId);
|
|
107
|
+
expect(lockJson).toHaveProperty('timestamp');
|
|
108
|
+
expect(typeof lockJson.timestamp).toBe('number');
|
|
109
|
+
expect(lockJson.timestamp).toBeGreaterThan(0);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// 4. Post: releases lock after npm install
|
|
113
|
+
it('post: releases lock after npm install', () => {
|
|
114
|
+
const sessionId = `test-release-${Date.now()}`;
|
|
115
|
+
runPre('npm install', { CLAUDE_SESSION_ID: sessionId });
|
|
116
|
+
expect(existsSync(LOCK_DIR)).toBe(true);
|
|
117
|
+
|
|
118
|
+
const { exitCode } = runPost('npm install', { CLAUDE_SESSION_ID: sessionId });
|
|
119
|
+
expect(exitCode).toBe(0);
|
|
120
|
+
expect(existsSync(LOCK_DIR)).toBe(false);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// 5. Pre: blocks when lock held by another agent
|
|
124
|
+
it('pre: blocks when lock held by another agent', () => {
|
|
125
|
+
// Manually create lock with a different agentId
|
|
126
|
+
mkdirSync(LOCK_DIR, { recursive: true });
|
|
127
|
+
const now = Math.floor(Date.now() / 1000);
|
|
128
|
+
writeFileSync(
|
|
129
|
+
join(LOCK_DIR, 'lock.json'),
|
|
130
|
+
JSON.stringify({ agentId: 'other-agent-999', timestamp: now, command: 'npm install' }),
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const mySession = `test-blocked-${Date.now()}`;
|
|
134
|
+
const { stdout, exitCode } = runPre('npm install', { CLAUDE_SESSION_ID: mySession });
|
|
135
|
+
|
|
136
|
+
// Script exits 0 but outputs JSON with continue: false
|
|
137
|
+
expect(exitCode).toBe(0);
|
|
138
|
+
const output = JSON.parse(stdout.trim());
|
|
139
|
+
expect(output.continue).toBe(false);
|
|
140
|
+
expect(output.stopReason).toContain('BLOCKED');
|
|
141
|
+
expect(output.stopReason).toContain('other-agent-999');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// 6. Pre: cleans stale lock (timestamp older than 30s)
|
|
145
|
+
it('pre: cleans stale lock and acquires', () => {
|
|
146
|
+
mkdirSync(LOCK_DIR, { recursive: true });
|
|
147
|
+
const staleTime = Math.floor(Date.now() / 1000) - 60; // 60s ago
|
|
148
|
+
writeFileSync(
|
|
149
|
+
join(LOCK_DIR, 'lock.json'),
|
|
150
|
+
JSON.stringify({ agentId: 'stale-agent', timestamp: staleTime, command: 'npm install' }),
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const mySession = `test-stale-${Date.now()}`;
|
|
154
|
+
const { stdout, exitCode } = runPre('npm install', { CLAUDE_SESSION_ID: mySession });
|
|
155
|
+
expect(exitCode).toBe(0);
|
|
156
|
+
// Should not contain "continue: false" — lock was stale and cleaned
|
|
157
|
+
if (stdout.trim()) {
|
|
158
|
+
const output = JSON.parse(stdout.trim());
|
|
159
|
+
expect(output.continue).not.toBe(false);
|
|
160
|
+
}
|
|
161
|
+
// Lock should now be held by our session
|
|
162
|
+
expect(existsSync(LOCK_DIR)).toBe(true);
|
|
163
|
+
const lockJson = JSON.parse(readFileSync(join(LOCK_DIR, 'lock.json'), 'utf-8'));
|
|
164
|
+
expect(lockJson.agentId).toBe(mySession);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// 7. Pre: allows same agent reentry
|
|
168
|
+
it('pre: allows same agent reentry', () => {
|
|
169
|
+
const sessionId = `test-reentry-${Date.now()}`;
|
|
170
|
+
const env = { CLAUDE_SESSION_ID: sessionId };
|
|
171
|
+
|
|
172
|
+
// First acquisition
|
|
173
|
+
const first = runPre('npm install', env);
|
|
174
|
+
expect(first.exitCode).toBe(0);
|
|
175
|
+
expect(existsSync(LOCK_DIR)).toBe(true);
|
|
176
|
+
|
|
177
|
+
// Second acquisition with same session — should succeed (reentry)
|
|
178
|
+
const second = runPre('npm install', env);
|
|
179
|
+
expect(second.exitCode).toBe(0);
|
|
180
|
+
// No "continue: false" in output
|
|
181
|
+
if (second.stdout.trim()) {
|
|
182
|
+
const output = JSON.parse(second.stdout.trim());
|
|
183
|
+
expect(output.continue).not.toBe(false);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// 8. Post: only releases own lock (does not release lock held by other agent)
|
|
188
|
+
it('post: only releases own lock — does not remove lock held by another agent', () => {
|
|
189
|
+
// Create lock with a different agent
|
|
190
|
+
mkdirSync(LOCK_DIR, { recursive: true });
|
|
191
|
+
const now = Math.floor(Date.now() / 1000);
|
|
192
|
+
writeFileSync(
|
|
193
|
+
join(LOCK_DIR, 'lock.json'),
|
|
194
|
+
JSON.stringify({ agentId: 'other-agent-777', timestamp: now, command: 'npm install' }),
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
const mySession = `test-norelease-${Date.now()}`;
|
|
198
|
+
const { exitCode } = runPost('npm install', { CLAUDE_SESSION_ID: mySession });
|
|
199
|
+
expect(exitCode).toBe(0);
|
|
200
|
+
// Lock dir should still exist — we don't own it
|
|
201
|
+
expect(existsSync(LOCK_DIR)).toBe(true);
|
|
202
|
+
const lockJson = JSON.parse(readFileSync(join(LOCK_DIR, 'lock.json'), 'utf-8'));
|
|
203
|
+
expect(lockJson.agentId).toBe('other-agent-777');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// 9. Pre: detects other lockfile commands (yarn, pnpm, cargo, pip)
|
|
207
|
+
it('pre: detects yarn, pnpm install, cargo build, pip install', () => {
|
|
208
|
+
const commands = ['yarn', 'yarn install', 'pnpm install', 'cargo build', 'pip install'];
|
|
209
|
+
for (const cmd of commands) {
|
|
210
|
+
cleanLock();
|
|
211
|
+
const sessionId = `test-detect-${Date.now()}`;
|
|
212
|
+
const { exitCode } = runPre(cmd, { CLAUDE_SESSION_ID: sessionId });
|
|
213
|
+
expect(exitCode).toBe(0);
|
|
214
|
+
expect(existsSync(LOCK_DIR)).toBe(true);
|
|
215
|
+
cleanLock();
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// 10. Post: ignores non-lockfile commands
|
|
220
|
+
it('post: ignores non-lockfile commands (no crash, no lock interaction)', () => {
|
|
221
|
+
const { exitCode, stdout } = runPost('echo hello');
|
|
222
|
+
expect(exitCode).toBe(0);
|
|
223
|
+
expect(stdout.trim()).toBe('');
|
|
224
|
+
});
|
|
225
|
+
});
|