@soleri/cli 9.0.2 → 9.3.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 +116 -3
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/create.js +6 -2
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/hooks.js +36 -13
- package/dist/commands/hooks.js.map +1 -1
- package/dist/commands/install.d.ts +1 -0
- package/dist/commands/install.js +61 -12
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/pack.js +0 -1
- package/dist/commands/pack.js.map +1 -1
- package/dist/commands/staging.d.ts +2 -0
- package/dist/commands/staging.js +175 -0
- package/dist/commands/staging.js.map +1 -0
- package/dist/hook-packs/full/manifest.json +2 -2
- package/dist/hook-packs/installer.d.ts +4 -11
- package/dist/hook-packs/installer.js +197 -23
- package/dist/hook-packs/installer.js.map +1 -1
- package/dist/hook-packs/installer.ts +223 -38
- package/dist/hook-packs/registry.d.ts +16 -13
- package/dist/hook-packs/registry.js +11 -18
- package/dist/hook-packs/registry.js.map +1 -1
- package/dist/hook-packs/registry.ts +31 -30
- package/dist/hook-packs/yolo-safety/manifest.json +23 -0
- package/dist/hook-packs/yolo-safety/scripts/anti-deletion.sh +214 -0
- package/dist/hooks/templates.js +1 -1
- package/dist/hooks/templates.js.map +1 -1
- package/dist/main.js +2 -0
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/create.test.ts +6 -2
- package/src/__tests__/hook-packs.test.ts +67 -25
- package/src/__tests__/wizard-e2e.mjs +153 -58
- package/src/commands/agent.ts +146 -3
- package/src/commands/create.ts +8 -2
- package/src/commands/hooks.ts +36 -31
- package/src/commands/install.ts +65 -22
- package/src/commands/pack.ts +0 -1
- package/src/commands/staging.ts +208 -0
- package/src/hook-packs/full/manifest.json +2 -2
- package/src/hook-packs/installer.ts +223 -38
- package/src/hook-packs/registry.ts +31 -30
- package/src/hook-packs/yolo-safety/manifest.json +23 -0
- package/src/hook-packs/yolo-safety/scripts/anti-deletion.sh +214 -0
- package/src/hooks/templates.ts +1 -1
- package/src/main.ts +2 -0
- package/dist/commands/cognee.d.ts +0 -10
- package/dist/commands/cognee.js +0 -364
- package/dist/commands/cognee.js.map +0 -1
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "yolo-safety",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Anti-deletion guardrail for YOLO mode — intercepts destructive commands, stages files for review",
|
|
5
|
+
"hooks": [],
|
|
6
|
+
"scripts": [
|
|
7
|
+
{
|
|
8
|
+
"name": "anti-deletion",
|
|
9
|
+
"file": "anti-deletion.sh",
|
|
10
|
+
"targetDir": "hooks"
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"lifecycleHooks": [
|
|
14
|
+
{
|
|
15
|
+
"event": "PreToolUse",
|
|
16
|
+
"matcher": "Bash",
|
|
17
|
+
"type": "command",
|
|
18
|
+
"command": "bash ~/.claude/hooks/anti-deletion.sh",
|
|
19
|
+
"timeout": 10,
|
|
20
|
+
"statusMessage": "Checking for destructive commands..."
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Anti-Deletion Staging Hook for Claude Code (Soleri Hook Pack: yolo-safety)
|
|
3
|
+
# PreToolUse -> Bash: intercepts rm, rmdir, mv (of project dirs), git clean, reset --hard
|
|
4
|
+
# Copies target files to ~/.soleri/staging/<timestamp>/ then blocks the command.
|
|
5
|
+
#
|
|
6
|
+
# Catastrophic commands (rm -rf /, rm -rf ~) should stay in deny rules —
|
|
7
|
+
# this hook handles targeted deletes only.
|
|
8
|
+
#
|
|
9
|
+
# Dependencies: jq (required), perl (optional, for heredoc stripping)
|
|
10
|
+
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
|
|
13
|
+
STAGING_ROOT="$HOME/.soleri/staging"
|
|
14
|
+
PROJECTS_DIR="$HOME/projects"
|
|
15
|
+
INPUT=$(cat)
|
|
16
|
+
|
|
17
|
+
# Extract the command from stdin JSON
|
|
18
|
+
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
19
|
+
|
|
20
|
+
# No command found — let it through
|
|
21
|
+
if [ -z "$CMD" ]; then
|
|
22
|
+
exit 0
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
# --- Strip heredocs and quoted strings to avoid false positives ---
|
|
26
|
+
# Commands like: gh issue comment --body "$(cat <<'EOF' ... rmdir ... EOF)"
|
|
27
|
+
# contain destructive keywords in text, not as actual commands.
|
|
28
|
+
|
|
29
|
+
# Remove heredoc blocks: <<'EOF'...EOF and <<EOF...EOF (multiline)
|
|
30
|
+
STRIPPED=$(echo "$CMD" | perl -0777 -pe "s/<<'?\\w+'?.*?^\\w+$//gms" 2>/dev/null || echo "$CMD")
|
|
31
|
+
# Remove double-quoted strings (greedy but good enough for this check)
|
|
32
|
+
STRIPPED=$(echo "$STRIPPED" | sed -E 's/"[^"]*"//g' 2>/dev/null || echo "$STRIPPED")
|
|
33
|
+
# Remove single-quoted strings
|
|
34
|
+
STRIPPED=$(echo "$STRIPPED" | sed -E "s/'[^']*'//g" 2>/dev/null || echo "$STRIPPED")
|
|
35
|
+
|
|
36
|
+
# --- Detect destructive commands (on stripped command only) ---
|
|
37
|
+
|
|
38
|
+
IS_RM=false
|
|
39
|
+
IS_RMDIR=false
|
|
40
|
+
IS_MV_PROJECT=false
|
|
41
|
+
IS_GIT_CLEAN=false
|
|
42
|
+
IS_RESET_HARD=false
|
|
43
|
+
IS_GIT_CHECKOUT_DOT=false
|
|
44
|
+
IS_GIT_RESTORE_DOT=false
|
|
45
|
+
|
|
46
|
+
# Check for rm commands (but not git rm which is safe — it stages, doesn't destroy)
|
|
47
|
+
if echo "$STRIPPED" | grep -qE '(^|\s|;|&&|\|\|)rm\s'; then
|
|
48
|
+
if ! echo "$STRIPPED" | grep -qE '(^|\s)git\s+rm\s'; then
|
|
49
|
+
IS_RM=true
|
|
50
|
+
fi
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
# Check for rmdir commands
|
|
54
|
+
if echo "$STRIPPED" | grep -qE '(^|\s|;|&&|\|\|)rmdir\s'; then
|
|
55
|
+
IS_RMDIR=true
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
# Check for mv commands that move project directories or git repos
|
|
59
|
+
if echo "$STRIPPED" | grep -qE '(^|\s|;|&&|\|\|)mv\s'; then
|
|
60
|
+
MV_SOURCES=$(echo "$STRIPPED" | sed -E 's/^.*\bmv\s+//' | sed -E 's/-(f|i|n|v)\s+//g')
|
|
61
|
+
if echo "$MV_SOURCES" | grep -qE "(~/projects|$HOME/projects|\\\$HOME/projects|\\.git)"; then
|
|
62
|
+
IS_MV_PROJECT=true
|
|
63
|
+
fi
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
# Check for git clean
|
|
67
|
+
if echo "$STRIPPED" | grep -qE '(^|\s|;|&&|\|\|)git\s+clean\b'; then
|
|
68
|
+
IS_GIT_CLEAN=true
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
# Check for git reset --hard
|
|
72
|
+
if echo "$STRIPPED" | grep -qE '(^|\s|;|&&|\|\|)git\s+reset\s+--hard'; then
|
|
73
|
+
IS_RESET_HARD=true
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
# Check for git checkout -- . (restores all files, discards changes)
|
|
77
|
+
if echo "$STRIPPED" | grep -qE '(^|\s|;|&&|\|\|)git\s+checkout\s+--\s+\.'; then
|
|
78
|
+
IS_GIT_CHECKOUT_DOT=true
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
# Check for git restore . (restores all files, discards changes)
|
|
82
|
+
if echo "$STRIPPED" | grep -qE '(^|\s|;|&&|\|\|)git\s+restore\s+\.'; then
|
|
83
|
+
IS_GIT_RESTORE_DOT=true
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
# Not a destructive command — let it through
|
|
87
|
+
if [ "$IS_RM" = false ] && [ "$IS_RMDIR" = false ] && [ "$IS_MV_PROJECT" = false ] && \
|
|
88
|
+
[ "$IS_GIT_CLEAN" = false ] && [ "$IS_RESET_HARD" = false ] && \
|
|
89
|
+
[ "$IS_GIT_CHECKOUT_DOT" = false ] && [ "$IS_GIT_RESTORE_DOT" = false ]; then
|
|
90
|
+
exit 0
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
# --- Handle git clean (block outright) ---
|
|
94
|
+
|
|
95
|
+
if [ "$IS_GIT_CLEAN" = true ]; then
|
|
96
|
+
jq -n '{
|
|
97
|
+
continue: false,
|
|
98
|
+
stopReason: "BLOCKED: git clean would remove untracked files. Use git stash --include-untracked to save them first, or ask the user to run git clean manually."
|
|
99
|
+
}'
|
|
100
|
+
exit 0
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
# --- Handle git reset --hard (block outright) ---
|
|
104
|
+
|
|
105
|
+
if [ "$IS_RESET_HARD" = true ]; then
|
|
106
|
+
jq -n '{
|
|
107
|
+
continue: false,
|
|
108
|
+
stopReason: "BLOCKED: git reset --hard would discard uncommitted changes. Use git stash to save them first, or ask the user to run this manually."
|
|
109
|
+
}'
|
|
110
|
+
exit 0
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
# --- Handle git checkout -- . (block outright) ---
|
|
114
|
+
|
|
115
|
+
if [ "$IS_GIT_CHECKOUT_DOT" = true ]; then
|
|
116
|
+
jq -n '{
|
|
117
|
+
continue: false,
|
|
118
|
+
stopReason: "BLOCKED: git checkout -- . would discard all uncommitted changes. Use git stash to save them first, or ask the user to run this manually."
|
|
119
|
+
}'
|
|
120
|
+
exit 0
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
# --- Handle git restore . (block outright) ---
|
|
124
|
+
|
|
125
|
+
if [ "$IS_GIT_RESTORE_DOT" = true ]; then
|
|
126
|
+
jq -n '{
|
|
127
|
+
continue: false,
|
|
128
|
+
stopReason: "BLOCKED: git restore . would discard all uncommitted changes. Use git stash to save them first, or ask the user to run this manually."
|
|
129
|
+
}'
|
|
130
|
+
exit 0
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
# --- Handle mv of project directories (block outright) ---
|
|
134
|
+
|
|
135
|
+
if [ "$IS_MV_PROJECT" = true ]; then
|
|
136
|
+
jq -n '{
|
|
137
|
+
continue: false,
|
|
138
|
+
stopReason: "BLOCKED: mv of a project directory or git repo detected. Moving project directories can cause data loss if the operation fails midway. Ask the user to run this manually, or use cp + verify + rm instead."
|
|
139
|
+
}'
|
|
140
|
+
exit 0
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
# --- Handle rmdir (block outright) ---
|
|
144
|
+
|
|
145
|
+
if [ "$IS_RMDIR" = true ]; then
|
|
146
|
+
jq -n '{
|
|
147
|
+
continue: false,
|
|
148
|
+
stopReason: "BLOCKED: rmdir detected. Removing directories can break project structure. Ask the user to confirm this operation manually."
|
|
149
|
+
}'
|
|
150
|
+
exit 0
|
|
151
|
+
fi
|
|
152
|
+
|
|
153
|
+
# --- Handle rm commands — copy to staging, then block ---
|
|
154
|
+
|
|
155
|
+
# Create timestamped staging directory
|
|
156
|
+
TIMESTAMP=$(date +%Y-%m-%d_%H%M%S)
|
|
157
|
+
STAGE_DIR="$STAGING_ROOT/$TIMESTAMP"
|
|
158
|
+
|
|
159
|
+
# Extract file paths from the rm command
|
|
160
|
+
# Strip rm and its flags, keeping only the file arguments
|
|
161
|
+
FILES=$(echo "$CMD" | sed -E 's/^.*\brm\s+//' | sed -E 's/-(r|f|rf|fr|v|i|rv|fv|rfv|frv)\s+//g' | tr ' ' '\n' | grep -v '^-' | grep -v '^$')
|
|
162
|
+
|
|
163
|
+
if [ -z "$FILES" ]; then
|
|
164
|
+
jq -n '{
|
|
165
|
+
continue: false,
|
|
166
|
+
stopReason: "BLOCKED: rm command detected but could not parse file targets. Please specify files explicitly."
|
|
167
|
+
}'
|
|
168
|
+
exit 0
|
|
169
|
+
fi
|
|
170
|
+
|
|
171
|
+
STAGED=()
|
|
172
|
+
MISSING=()
|
|
173
|
+
|
|
174
|
+
mkdir -p "$STAGE_DIR"
|
|
175
|
+
|
|
176
|
+
while IFS= read -r filepath; do
|
|
177
|
+
# Expand path (handle ~, relative paths)
|
|
178
|
+
expanded=$(eval echo "$filepath" 2>/dev/null || echo "$filepath")
|
|
179
|
+
|
|
180
|
+
if [ -e "$expanded" ]; then
|
|
181
|
+
# Preserve directory structure in staging
|
|
182
|
+
target_dir="$STAGE_DIR/$(dirname "$expanded")"
|
|
183
|
+
mkdir -p "$target_dir"
|
|
184
|
+
# COPY instead of MOVE — originals stay intact, staging is a backup
|
|
185
|
+
if [ -d "$expanded" ]; then
|
|
186
|
+
cp -R "$expanded" "$target_dir/" 2>/dev/null && STAGED+=("$expanded") || MISSING+=("$expanded")
|
|
187
|
+
else
|
|
188
|
+
cp "$expanded" "$target_dir/" 2>/dev/null && STAGED+=("$expanded") || MISSING+=("$expanded")
|
|
189
|
+
fi
|
|
190
|
+
else
|
|
191
|
+
MISSING+=("$expanded")
|
|
192
|
+
fi
|
|
193
|
+
done <<< "$FILES"
|
|
194
|
+
|
|
195
|
+
# Build response
|
|
196
|
+
STAGED_COUNT=${#STAGED[@]}
|
|
197
|
+
MISSING_COUNT=${#MISSING[@]}
|
|
198
|
+
|
|
199
|
+
if [ "$STAGED_COUNT" -eq 0 ] && [ "$MISSING_COUNT" -gt 0 ]; then
|
|
200
|
+
# All files were missing — let the rm fail naturally
|
|
201
|
+
rmdir "$STAGE_DIR" 2>/dev/null || true
|
|
202
|
+
exit 0
|
|
203
|
+
fi
|
|
204
|
+
|
|
205
|
+
STAGED_LIST=$(printf '%s, ' "${STAGED[@]}" | sed 's/, $//')
|
|
206
|
+
|
|
207
|
+
jq -n \
|
|
208
|
+
--arg staged "$STAGED_LIST" \
|
|
209
|
+
--arg dir "$STAGE_DIR" \
|
|
210
|
+
--argjson count "$STAGED_COUNT" \
|
|
211
|
+
'{
|
|
212
|
+
continue: false,
|
|
213
|
+
stopReason: ("BLOCKED & BACKED UP: " + ($count | tostring) + " item(s) copied to " + $dir + " — files: " + $staged + ". The originals are untouched. To proceed with deletion, ask the user to run the rm command manually.")
|
|
214
|
+
}'
|
package/dist/hooks/templates.js
CHANGED
|
@@ -105,7 +105,7 @@ function generateClaudeCodeSettings(dir) {
|
|
|
105
105
|
hooks: [
|
|
106
106
|
{
|
|
107
107
|
type: 'command',
|
|
108
|
-
command: `echo "[${agentId}] session started — register project: ${agentId}_core op:
|
|
108
|
+
command: `echo "[${agentId}] session started — register project: ${agentId}_core op:session_start params:{ projectPath: \\".\\" }" && echo "Check for active plans: ${agentId}_core op:get_plan"`,
|
|
109
109
|
},
|
|
110
110
|
],
|
|
111
111
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"templates.js","sourceRoot":"","sources":["../../src/hooks/templates.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAIxD,MAAM,CAAC,MAAM,iBAAiB,GAAe,CAAC,aAAa,EAAE,QAAQ,EAAE,UAAU,EAAE,SAAS,CAAC,CAAC;AAO9F,kFAAkF;AAClF,SAAS,UAAU,CAAC,EAAU;IAC5B,OAAO,EAAE,CAAC,OAAO,CAAC,aAAa,EAAE,EAAE,CAAC,CAAC;AACvC,CAAC;AAED,SAAS,YAAY,CAAC,GAAY;IAChC,MAAM,GAAG,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;IAC7B,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAC;IACtB,OAAO,EAAE,OAAO,EAAE,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,WAAW,EAAE,GAAG,CAAC,WAAW,EAAE,CAAC;AAC5E,CAAC;AAED,oBAAoB;AAEpB;;;;;;;;;;;;;;;GAeG;AACH,SAAS,oBAAoB,CAAC,OAAe;IAC3C,mEAAmE;IACnE,mCAAmC;IACnC,oEAAoE;IACpE,MAAM,KAAK,GAAG;QACZ,8BAA8B;QAC9B,2CAA2C;QAC3C,kIAAkI;QAClI,0FAA0F;QAC1F,yJAAyJ;QACzJ,2FAA2F;QAC3F,wEAAwE;QACxE,2DAA2D;QAC3D;YACE,iKAAiK;YACjK,oMAAoM;YACpM,qMAAqM;YACrM,mKAAmK;YACnK,mKAAmK;YACnK,6JAA6J;YAC7J,+KAA+K;YAC/K,gLAAgL;YAChL,qLAAqL;YACrL,IAAI;SACL,CAAC,IAAI,CAAC,IAAI,CAAC;QACZ,oDAAoD;QACpD,cAAc,OAAO,uFAAuF;KAC7G,CAAC;IAEF,OAAO,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAC5B,CAAC;AAED,SAAS,0BAA0B,CAAC,GAAY;IAC9C,MAAM,IAAI,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;IAC/B,MAAM,OAAO,GAAG,IAAI,EAAE,OAAO,IAAI,UAAU,CAAC;IAC5C,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAC7B;QACE,KAAK,EAAE;YACL,gBAAgB,EAAE;gBAChB;oBACE,KAAK,EAAE;wBACL;4BACE,IAAI,EAAE,SAAS;4BACf,OAAO,EAAE,oBAAoB,CAAC,OAAO,CAAC;yBACvC;qBACF;iBACF;aACF;YACD,UAAU,EAAE;gBACV;oBACE,OAAO,EAAE,GAAG;oBACZ,KAAK,EAAE;wBACL;4BACE,IAAI,EAAE,SAAS;4BACf,OAAO,EAAE,UAAU,OAAO,qBAAqB;yBAChD;qBACF;iBACF;aACF;YACD,WAAW,EAAE;gBACX;oBACE,OAAO,EAAE,YAAY;oBACrB,KAAK,EAAE;wBACL;4BACE,IAAI,EAAE,SAAS;4BACf,OAAO,EAAE,UAAU,OAAO,iBAAiB;yBAC5C;qBACF;iBACF;aACF;YACD,YAAY,EAAE;gBACZ;oBACE,KAAK,EAAE;wBACL;4BACE,IAAI,EAAE,SAAS;4BACf,OAAO,EAAE,UAAU,OAAO,yCAAyC,OAAO,
|
|
1
|
+
{"version":3,"file":"templates.js","sourceRoot":"","sources":["../../src/hooks/templates.ts"],"names":[],"mappings":"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"}
|
package/dist/main.js
CHANGED
|
@@ -19,6 +19,7 @@ import { registerPack } from './commands/pack.js';
|
|
|
19
19
|
import { registerSkills } from './commands/skills.js';
|
|
20
20
|
import { registerAgent } from './commands/agent.js';
|
|
21
21
|
import { registerTelegram } from './commands/telegram.js';
|
|
22
|
+
import { registerStaging } from './commands/staging.js';
|
|
22
23
|
const require = createRequire(import.meta.url);
|
|
23
24
|
const { version } = require('../package.json');
|
|
24
25
|
const RESET = '\x1b[0m';
|
|
@@ -73,5 +74,6 @@ registerPack(program);
|
|
|
73
74
|
registerSkills(program);
|
|
74
75
|
registerAgent(program);
|
|
75
76
|
registerTelegram(program);
|
|
77
|
+
registerStaging(program);
|
|
76
78
|
program.parse();
|
|
77
79
|
//# 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;
|
|
1
|
+
{"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@ import { previewScaffold, scaffold } from '@soleri/forge/lib';
|
|
|
6
6
|
import type { AgentConfig } from '@soleri/forge/lib';
|
|
7
7
|
import { installPack } from '../hook-packs/installer.js';
|
|
8
8
|
|
|
9
|
-
describe('create command', () => {
|
|
9
|
+
describe('create command', { timeout: 30_000 }, () => {
|
|
10
10
|
let tempDir: string;
|
|
11
11
|
|
|
12
12
|
const testConfig: AgentConfig = {
|
|
@@ -81,7 +81,11 @@ describe('create command', () => {
|
|
|
81
81
|
),
|
|
82
82
|
);
|
|
83
83
|
expect(testingBundle.domain).toBe('testing');
|
|
84
|
-
expect(testingBundle.entries).
|
|
84
|
+
expect(testingBundle.entries.length).toBeGreaterThanOrEqual(0);
|
|
85
|
+
if (testingBundle.entries.length > 0) {
|
|
86
|
+
expect(testingBundle.entries[0].id).toBe('testing-seed');
|
|
87
|
+
expect(testingBundle.entries[0].tags).toContain('seed');
|
|
88
|
+
}
|
|
85
89
|
});
|
|
86
90
|
|
|
87
91
|
it('should read config from file for non-interactive mode', () => {
|
|
@@ -3,7 +3,6 @@ import { mkdirSync, rmSync, existsSync, readFileSync, writeFileSync } from 'node
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
|
|
6
|
-
// Mock homedir to use a temp directory instead of real ~/.claude/
|
|
7
6
|
const tempHome = join(tmpdir(), `cli-hookpacks-test-${Date.now()}`);
|
|
8
7
|
|
|
9
8
|
vi.mock('node:os', async () => {
|
|
@@ -24,10 +23,9 @@ describe('hook-packs', () => {
|
|
|
24
23
|
});
|
|
25
24
|
|
|
26
25
|
describe('registry', () => {
|
|
27
|
-
it('should list all
|
|
26
|
+
it('should list all 6 built-in packs', () => {
|
|
28
27
|
const packs = listPacks();
|
|
29
|
-
expect(packs.length).toBe(
|
|
30
|
-
|
|
28
|
+
expect(packs.length).toBe(6);
|
|
31
29
|
const names = packs.map((p) => p.name).sort();
|
|
32
30
|
expect(names).toEqual([
|
|
33
31
|
'a11y',
|
|
@@ -35,6 +33,7 @@ describe('hook-packs', () => {
|
|
|
35
33
|
'css-discipline',
|
|
36
34
|
'full',
|
|
37
35
|
'typescript-safety',
|
|
36
|
+
'yolo-safety',
|
|
38
37
|
]);
|
|
39
38
|
});
|
|
40
39
|
|
|
@@ -50,7 +49,7 @@ describe('hook-packs', () => {
|
|
|
50
49
|
expect(getPack('nonexistent')).toBeNull();
|
|
51
50
|
});
|
|
52
51
|
|
|
53
|
-
it('should return full pack with composedFrom', () => {
|
|
52
|
+
it('should return full pack with composedFrom including yolo-safety', () => {
|
|
54
53
|
const pack = getPack('full');
|
|
55
54
|
expect(pack).not.toBeNull();
|
|
56
55
|
expect(pack!.manifest.composedFrom).toEqual([
|
|
@@ -58,27 +57,36 @@ describe('hook-packs', () => {
|
|
|
58
57
|
'a11y',
|
|
59
58
|
'css-discipline',
|
|
60
59
|
'clean-commits',
|
|
60
|
+
'yolo-safety',
|
|
61
61
|
]);
|
|
62
62
|
expect(pack!.manifest.hooks).toHaveLength(8);
|
|
63
63
|
});
|
|
64
64
|
|
|
65
65
|
it('should return empty installed packs when none installed', () => {
|
|
66
|
-
|
|
66
|
+
const installed = getInstalledPacks();
|
|
67
|
+
expect(installed.filter((p) => p !== 'yolo-safety')).toEqual([]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should get yolo-safety pack with scripts and lifecycleHooks', () => {
|
|
71
|
+
const pack = getPack('yolo-safety');
|
|
72
|
+
expect(pack).not.toBeNull();
|
|
73
|
+
expect(pack!.manifest.name).toBe('yolo-safety');
|
|
74
|
+
expect(pack!.manifest.hooks).toEqual([]);
|
|
75
|
+
expect(pack!.manifest.scripts).toHaveLength(1);
|
|
76
|
+
expect(pack!.manifest.scripts![0].name).toBe('anti-deletion');
|
|
77
|
+
expect(pack!.manifest.lifecycleHooks).toHaveLength(1);
|
|
78
|
+
expect(pack!.manifest.lifecycleHooks![0].event).toBe('PreToolUse');
|
|
67
79
|
});
|
|
68
80
|
});
|
|
69
81
|
|
|
70
82
|
describe('installer', () => {
|
|
71
83
|
it('should install a simple pack', () => {
|
|
72
84
|
const result = installPack('typescript-safety');
|
|
73
|
-
|
|
74
85
|
expect(result.installed).toEqual(['no-any-types', 'no-console-log']);
|
|
75
86
|
expect(result.skipped).toEqual([]);
|
|
76
|
-
|
|
77
87
|
const claudeDir = join(tempHome, '.claude');
|
|
78
88
|
expect(existsSync(join(claudeDir, 'hookify.no-any-types.local.md'))).toBe(true);
|
|
79
89
|
expect(existsSync(join(claudeDir, 'hookify.no-console-log.local.md'))).toBe(true);
|
|
80
|
-
|
|
81
|
-
// Verify content was copied correctly
|
|
82
90
|
const content = readFileSync(join(claudeDir, 'hookify.no-any-types.local.md'), 'utf-8');
|
|
83
91
|
expect(content).toContain('name: no-any-types');
|
|
84
92
|
expect(content).toContain('Soleri Hook Pack: typescript-safety');
|
|
@@ -87,19 +95,16 @@ describe('hook-packs', () => {
|
|
|
87
95
|
it('should be idempotent — skip existing files', () => {
|
|
88
96
|
installPack('typescript-safety');
|
|
89
97
|
const result = installPack('typescript-safety');
|
|
90
|
-
|
|
91
98
|
expect(result.installed).toEqual([]);
|
|
92
99
|
expect(result.skipped).toEqual(['no-any-types', 'no-console-log']);
|
|
93
100
|
});
|
|
94
101
|
|
|
95
102
|
it('should install composed pack (full)', () => {
|
|
96
103
|
const result = installPack('full');
|
|
97
|
-
|
|
98
104
|
expect(result.installed).toHaveLength(8);
|
|
99
105
|
expect(result.skipped).toEqual([]);
|
|
100
|
-
|
|
101
106
|
const claudeDir = join(tempHome, '.claude');
|
|
102
|
-
const
|
|
107
|
+
for (const hook of [
|
|
103
108
|
'no-any-types',
|
|
104
109
|
'no-console-log',
|
|
105
110
|
'no-important',
|
|
@@ -108,19 +113,20 @@ describe('hook-packs', () => {
|
|
|
108
113
|
'focus-ring-required',
|
|
109
114
|
'ux-touch-targets',
|
|
110
115
|
'no-ai-attribution',
|
|
111
|
-
]
|
|
112
|
-
for (const hook of expectedHooks) {
|
|
116
|
+
]) {
|
|
113
117
|
expect(existsSync(join(claudeDir, `hookify.${hook}.local.md`))).toBe(true);
|
|
114
118
|
}
|
|
119
|
+
expect(result.scripts).toHaveLength(1);
|
|
120
|
+
expect(result.scripts[0]).toBe('hooks/anti-deletion.sh');
|
|
121
|
+
expect(existsSync(join(claudeDir, 'hooks', 'anti-deletion.sh'))).toBe(true);
|
|
115
122
|
});
|
|
116
123
|
|
|
117
124
|
it('should skip already-installed hooks when installing full after partial', () => {
|
|
118
125
|
installPack('typescript-safety');
|
|
119
126
|
const result = installPack('full');
|
|
120
|
-
|
|
121
127
|
expect(result.skipped).toContain('no-any-types');
|
|
122
128
|
expect(result.skipped).toContain('no-console-log');
|
|
123
|
-
expect(result.installed).toHaveLength(6);
|
|
129
|
+
expect(result.installed).toHaveLength(6);
|
|
124
130
|
});
|
|
125
131
|
|
|
126
132
|
it('should throw for unknown pack', () => {
|
|
@@ -130,9 +136,7 @@ describe('hook-packs', () => {
|
|
|
130
136
|
it('should remove a pack', () => {
|
|
131
137
|
installPack('a11y');
|
|
132
138
|
const result = removePack('a11y');
|
|
133
|
-
|
|
134
139
|
expect(result.removed).toEqual(['semantic-html', 'focus-ring-required', 'ux-touch-targets']);
|
|
135
|
-
|
|
136
140
|
const claudeDir = join(tempHome, '.claude');
|
|
137
141
|
expect(existsSync(join(claudeDir, 'hookify.semantic-html.local.md'))).toBe(false);
|
|
138
142
|
});
|
|
@@ -145,6 +149,42 @@ describe('hook-packs', () => {
|
|
|
145
149
|
it('should throw for unknown pack on remove', () => {
|
|
146
150
|
expect(() => removePack('nonexistent')).toThrow('Unknown hook pack: "nonexistent"');
|
|
147
151
|
});
|
|
152
|
+
|
|
153
|
+
it('should install yolo-safety pack with scripts and lifecycle hooks', () => {
|
|
154
|
+
const result = installPack('yolo-safety');
|
|
155
|
+
expect(result.installed).toEqual([]);
|
|
156
|
+
expect(result.scripts).toHaveLength(1);
|
|
157
|
+
expect(result.scripts[0]).toBe('hooks/anti-deletion.sh');
|
|
158
|
+
expect(result.lifecycleHooks).toHaveLength(1);
|
|
159
|
+
expect(result.lifecycleHooks[0]).toBe('PreToolUse:Bash');
|
|
160
|
+
const claudeDir = join(tempHome, '.claude');
|
|
161
|
+
expect(existsSync(join(claudeDir, 'hooks', 'anti-deletion.sh'))).toBe(true);
|
|
162
|
+
const settings = JSON.parse(readFileSync(join(claudeDir, 'settings.json'), 'utf-8'));
|
|
163
|
+
expect(settings.hooks).toBeDefined();
|
|
164
|
+
expect(settings.hooks.PreToolUse).toHaveLength(1);
|
|
165
|
+
expect(settings.hooks.PreToolUse[0].command).toBe('bash ~/.claude/hooks/anti-deletion.sh');
|
|
166
|
+
expect(settings.hooks.PreToolUse[0]._soleriPack).toBe('yolo-safety');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should remove yolo-safety pack including scripts and lifecycle hooks', () => {
|
|
170
|
+
installPack('yolo-safety');
|
|
171
|
+
const result = removePack('yolo-safety');
|
|
172
|
+
expect(result.scripts).toHaveLength(1);
|
|
173
|
+
expect(result.lifecycleHooks).toHaveLength(1);
|
|
174
|
+
const claudeDir = join(tempHome, '.claude');
|
|
175
|
+
expect(existsSync(join(claudeDir, 'hooks', 'anti-deletion.sh'))).toBe(false);
|
|
176
|
+
const settings = JSON.parse(readFileSync(join(claudeDir, 'settings.json'), 'utf-8'));
|
|
177
|
+
expect(settings.hooks.PreToolUse).toBeUndefined();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should be idempotent for yolo-safety lifecycle hooks', () => {
|
|
181
|
+
installPack('yolo-safety');
|
|
182
|
+
const result2 = installPack('yolo-safety');
|
|
183
|
+
expect(result2.lifecycleHooks).toEqual([]);
|
|
184
|
+
const claudeDir = join(tempHome, '.claude');
|
|
185
|
+
const settings = JSON.parse(readFileSync(join(claudeDir, 'settings.json'), 'utf-8'));
|
|
186
|
+
expect(settings.hooks.PreToolUse).toHaveLength(1);
|
|
187
|
+
});
|
|
148
188
|
});
|
|
149
189
|
|
|
150
190
|
describe('isPackInstalled', () => {
|
|
@@ -158,7 +198,6 @@ describe('hook-packs', () => {
|
|
|
158
198
|
});
|
|
159
199
|
|
|
160
200
|
it('should return partial when some hooks present', () => {
|
|
161
|
-
// Install just one of the two hooks
|
|
162
201
|
const claudeDir = join(tempHome, '.claude');
|
|
163
202
|
writeFileSync(join(claudeDir, 'hookify.no-any-types.local.md'), 'test');
|
|
164
203
|
expect(isPackInstalled('typescript-safety')).toBe('partial');
|
|
@@ -167,29 +206,32 @@ describe('hook-packs', () => {
|
|
|
167
206
|
it('should return false for unknown pack', () => {
|
|
168
207
|
expect(isPackInstalled('nonexistent')).toBe(false);
|
|
169
208
|
});
|
|
209
|
+
|
|
210
|
+
it('should detect yolo-safety as installed when script is present', () => {
|
|
211
|
+
installPack('yolo-safety');
|
|
212
|
+
expect(isPackInstalled('yolo-safety')).toBe(true);
|
|
213
|
+
});
|
|
170
214
|
});
|
|
171
215
|
|
|
172
216
|
describe('getInstalledPacks', () => {
|
|
173
217
|
it('should list installed packs', () => {
|
|
174
218
|
installPack('typescript-safety');
|
|
175
219
|
installPack('a11y');
|
|
176
|
-
|
|
177
220
|
const installed = getInstalledPacks();
|
|
178
221
|
expect(installed).toContain('typescript-safety');
|
|
179
222
|
expect(installed).toContain('a11y');
|
|
180
223
|
expect(installed).not.toContain('css-discipline');
|
|
181
224
|
});
|
|
182
225
|
|
|
183
|
-
it('should include full when all
|
|
226
|
+
it('should include full when all hooks and scripts are present', () => {
|
|
184
227
|
installPack('full');
|
|
185
|
-
|
|
186
228
|
const installed = getInstalledPacks();
|
|
187
|
-
// All packs should show as installed since full installs all hooks
|
|
188
229
|
expect(installed).toContain('full');
|
|
189
230
|
expect(installed).toContain('typescript-safety');
|
|
190
231
|
expect(installed).toContain('a11y');
|
|
191
232
|
expect(installed).toContain('css-discipline');
|
|
192
233
|
expect(installed).toContain('clean-commits');
|
|
234
|
+
expect(installed).toContain('yolo-safety');
|
|
193
235
|
});
|
|
194
236
|
});
|
|
195
237
|
});
|