@soleri/cli 9.3.0 → 9.4.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/.github/workflows/ci.yml +116 -0
- package/dist/commands/agent.js +51 -2
- package/dist/commands/agent.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/yolo-safety/manifest.json +2 -2
- 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__/hook-packs.test.ts +1 -1
- package/src/__tests__/wizard-e2e.mjs +1 -1
- package/src/commands/agent.ts +65 -2
- 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/yolo-safety/manifest.json +2 -2
- package/src/hook-packs/yolo-safety/scripts/anti-deletion.sh +121 -61
- package/src/main.ts +2 -0
- package/vitest.config.ts +3 -0
- package/src/__tests__/archetypes.test.ts +0 -84
- package/src/__tests__/create.test.ts +0 -207
- 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
|
@@ -162,7 +162,7 @@ describe('hook-packs', () => {
|
|
|
162
162
|
const settings = JSON.parse(readFileSync(join(claudeDir, 'settings.json'), 'utf-8'));
|
|
163
163
|
expect(settings.hooks).toBeDefined();
|
|
164
164
|
expect(settings.hooks.PreToolUse).toHaveLength(1);
|
|
165
|
-
expect(settings.hooks.PreToolUse[0].command).toBe('
|
|
165
|
+
expect(settings.hooks.PreToolUse[0].command).toBe('sh ~/.claude/hooks/anti-deletion.sh');
|
|
166
166
|
expect(settings.hooks.PreToolUse[0]._soleriPack).toBe('yolo-safety');
|
|
167
167
|
});
|
|
168
168
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Comprehensive E2E tests for the interactive create wizard.
|
|
4
4
|
*
|
|
5
|
-
* Tests
|
|
5
|
+
* Tests custom path, custom greeting, custom domains/principles,
|
|
6
6
|
* hook pack selection, cancel flows, and decline flows.
|
|
7
7
|
*
|
|
8
8
|
* Run: node packages/cli/src/__tests__/wizard-e2e.mjs
|
package/src/commands/agent.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
generateInjectClaudeMd,
|
|
27
27
|
generateSkills,
|
|
28
28
|
} from '@soleri/forge/lib';
|
|
29
|
+
import { composeClaudeMd, getEngineRulesContent } from '@soleri/forge/lib';
|
|
29
30
|
import type { AgentConfig } from '@soleri/forge/lib';
|
|
30
31
|
import { detectAgent } from '../utils/agent-context.js';
|
|
31
32
|
import { installClaude } from './install.js';
|
|
@@ -194,7 +195,70 @@ export function registerAgent(program: Command): void {
|
|
|
194
195
|
return;
|
|
195
196
|
}
|
|
196
197
|
|
|
197
|
-
//
|
|
198
|
+
// ─── File-tree agent (v7+) ────────────────────────────────
|
|
199
|
+
if (ctx.format === 'filetree') {
|
|
200
|
+
const enginePath = join(ctx.agentPath, 'instructions', '_engine.md');
|
|
201
|
+
const claudeMdPath = join(ctx.agentPath, 'CLAUDE.md');
|
|
202
|
+
|
|
203
|
+
// Generate skills from latest forge templates
|
|
204
|
+
const skillFiles = opts.skipSkills
|
|
205
|
+
? []
|
|
206
|
+
: generateSkills({ id: ctx.agentId } as AgentConfig);
|
|
207
|
+
|
|
208
|
+
if (opts.dryRun) {
|
|
209
|
+
p.log.info(`Would regenerate: ${enginePath}`);
|
|
210
|
+
p.log.info(`Would regenerate: ${claudeMdPath}`);
|
|
211
|
+
if (skillFiles.length > 0) {
|
|
212
|
+
const newSkills = skillFiles.filter(
|
|
213
|
+
([relPath]) => !existsSync(join(ctx.agentPath, relPath)),
|
|
214
|
+
);
|
|
215
|
+
const updatedSkills = skillFiles.filter(([relPath]) =>
|
|
216
|
+
existsSync(join(ctx.agentPath, relPath)),
|
|
217
|
+
);
|
|
218
|
+
p.log.info(
|
|
219
|
+
`Skills: ${skillFiles.length} total (${newSkills.length} new, ${updatedSkills.length} updated)`,
|
|
220
|
+
);
|
|
221
|
+
for (const [relPath] of newSkills) {
|
|
222
|
+
p.log.info(` + ${relPath}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
p.log.info(`Agent: ${ctx.agentId} (file-tree format)`);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 1. Sync skills from forge templates
|
|
230
|
+
if (skillFiles.length > 0) {
|
|
231
|
+
let newCount = 0;
|
|
232
|
+
let updatedCount = 0;
|
|
233
|
+
for (const [relPath, content] of skillFiles) {
|
|
234
|
+
const fullPath = join(ctx.agentPath, relPath);
|
|
235
|
+
const dirPath = dirname(fullPath);
|
|
236
|
+
const isNew = !existsSync(fullPath);
|
|
237
|
+
mkdirSync(dirPath, { recursive: true });
|
|
238
|
+
writeFileSync(fullPath, content, 'utf-8');
|
|
239
|
+
if (isNew) newCount++;
|
|
240
|
+
else updatedCount++;
|
|
241
|
+
}
|
|
242
|
+
p.log.success(
|
|
243
|
+
`Synced ${skillFiles.length} skills (${newCount} new, ${updatedCount} updated)`,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// 2. Regenerate _engine.md from latest shared-rules
|
|
248
|
+
mkdirSync(join(ctx.agentPath, 'instructions'), { recursive: true });
|
|
249
|
+
writeFileSync(enginePath, getEngineRulesContent(), 'utf-8');
|
|
250
|
+
p.log.success(`Regenerated ${enginePath}`);
|
|
251
|
+
|
|
252
|
+
// 3. Recompose CLAUDE.md from agent.yaml + instructions + workflows + skills
|
|
253
|
+
const result = composeClaudeMd(ctx.agentPath);
|
|
254
|
+
writeFileSync(claudeMdPath, result.content, 'utf-8');
|
|
255
|
+
p.log.success(
|
|
256
|
+
`Regenerated ${claudeMdPath} (${result.sources.length} sources, ${result.content.length} bytes)`,
|
|
257
|
+
);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ─── Legacy TypeScript agent ──────────────────────────────
|
|
198
262
|
const config = readAgentConfig(ctx.agentPath, ctx.agentId);
|
|
199
263
|
if (!config) {
|
|
200
264
|
p.log.error('Could not read agent config from persona.ts and entry point.');
|
|
@@ -216,7 +280,6 @@ export function registerAgent(program: Command): void {
|
|
|
216
280
|
p.log.info(`Agent: ${config.name} (${config.domains.length} domains)`);
|
|
217
281
|
p.log.info(`Domains: ${config.domains.join(', ')}`);
|
|
218
282
|
if (skillFiles.length > 0) {
|
|
219
|
-
// Check which skills are new vs existing
|
|
220
283
|
const newSkills = skillFiles.filter(
|
|
221
284
|
([relPath]) => !existsSync(join(ctx.agentPath, relPath)),
|
|
222
285
|
);
|
package/src/commands/pack.ts
CHANGED
|
@@ -13,6 +13,18 @@ import type { Command } from 'commander';
|
|
|
13
13
|
import * as p from '@clack/prompts';
|
|
14
14
|
import { PackLockfile, inferPackType, resolvePack, checkNpmVersion } from '@soleri/core';
|
|
15
15
|
import type { LockEntry, PackSource } from '@soleri/core';
|
|
16
|
+
|
|
17
|
+
// ─── Tier display helpers ────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
const TIER_BADGES: Record<string, string> = {
|
|
20
|
+
default: '[default]',
|
|
21
|
+
community: '[community]',
|
|
22
|
+
premium: '[premium]',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function tierBadge(tier?: string): string {
|
|
26
|
+
return TIER_BADGES[tier ?? 'community'] ?? '[community]';
|
|
27
|
+
}
|
|
16
28
|
import { detectAgent } from '../utils/agent-context.js';
|
|
17
29
|
|
|
18
30
|
const LOCKFILE_NAME = 'soleri.lock';
|
|
@@ -45,14 +57,18 @@ export function registerPack(program: Command): void {
|
|
|
45
57
|
pack
|
|
46
58
|
.command('list')
|
|
47
59
|
.option('--type <type>', 'Filter by pack type (hooks, skills, knowledge, domain, bundle)')
|
|
60
|
+
.option('--tier <tier>', 'Filter by tier (default, community, premium)')
|
|
48
61
|
.description('List installed packs')
|
|
49
|
-
.action((opts: { type?: string }) => {
|
|
62
|
+
.action((opts: { type?: string; tier?: string }) => {
|
|
50
63
|
const lockfile = new PackLockfile(getLockfilePath());
|
|
51
64
|
let entries = lockfile.list();
|
|
52
65
|
|
|
53
66
|
if (opts.type) {
|
|
54
67
|
entries = entries.filter((e) => e.type === opts.type);
|
|
55
68
|
}
|
|
69
|
+
if (opts.tier) {
|
|
70
|
+
entries = entries.filter((e) => (e.tier ?? 'community') === opts.tier);
|
|
71
|
+
}
|
|
56
72
|
|
|
57
73
|
if (entries.length === 0) {
|
|
58
74
|
p.log.info('No packs installed.');
|
|
@@ -61,9 +77,10 @@ export function registerPack(program: Command): void {
|
|
|
61
77
|
|
|
62
78
|
p.log.info(`${entries.length} pack(s) installed:\n`);
|
|
63
79
|
for (const entry of entries) {
|
|
64
|
-
const
|
|
80
|
+
const source =
|
|
65
81
|
entry.source === 'built-in' ? ' [built-in]' : entry.source === 'npm' ? ' [npm]' : '';
|
|
66
|
-
|
|
82
|
+
const tier = ` ${tierBadge(entry.tier)}`;
|
|
83
|
+
console.log(` ${entry.id}@${entry.version} ${entry.type}${tier}${source}`);
|
|
67
84
|
if (entry.vaultEntries > 0) console.log(` vault: ${entry.vaultEntries} entries`);
|
|
68
85
|
if (entry.skills.length > 0) console.log(` skills: ${entry.skills.join(', ')}`);
|
|
69
86
|
if (entry.hooks.length > 0) console.log(` hooks: ${entry.hooks.join(', ')}`);
|
|
@@ -163,6 +180,7 @@ export function registerPack(program: Command): void {
|
|
|
163
180
|
skills,
|
|
164
181
|
hooks,
|
|
165
182
|
facadesRegistered: (manifest.facades?.length ?? 0) > 0,
|
|
183
|
+
tier: manifest.tier ?? 'community',
|
|
166
184
|
};
|
|
167
185
|
|
|
168
186
|
lockfile.set(entry);
|
|
@@ -219,9 +237,14 @@ export function registerPack(program: Command): void {
|
|
|
219
237
|
return;
|
|
220
238
|
}
|
|
221
239
|
|
|
240
|
+
const tierLabel = entry.tier ?? 'community';
|
|
241
|
+
const tierNote =
|
|
242
|
+
tierLabel === 'premium' ? ' (currently unlocked — premium platform coming soon)' : '';
|
|
243
|
+
|
|
222
244
|
console.log(`\n Pack: ${entry.id}`);
|
|
223
245
|
console.log(` Version: ${entry.version}`);
|
|
224
246
|
console.log(` Type: ${entry.type}`);
|
|
247
|
+
console.log(` Tier: ${tierLabel}${tierNote}`);
|
|
225
248
|
console.log(` Source: ${entry.source}`);
|
|
226
249
|
console.log(` Directory: ${entry.directory}`);
|
|
227
250
|
console.log(` Installed: ${entry.installedAt}`);
|
|
@@ -378,7 +401,16 @@ export function registerPack(program: Command): void {
|
|
|
378
401
|
return;
|
|
379
402
|
}
|
|
380
403
|
|
|
381
|
-
|
|
404
|
+
// Collect all packs with their tier
|
|
405
|
+
const allPacks: Array<{
|
|
406
|
+
id: string;
|
|
407
|
+
version: string;
|
|
408
|
+
description: string;
|
|
409
|
+
domains: string;
|
|
410
|
+
tier: string;
|
|
411
|
+
category: string;
|
|
412
|
+
}> = [];
|
|
413
|
+
|
|
382
414
|
for (const baseDir of searchDirs) {
|
|
383
415
|
const categories = readdirSync(baseDir, { withFileTypes: true })
|
|
384
416
|
.filter((d) => d.isDirectory())
|
|
@@ -390,18 +422,19 @@ export function registerPack(program: Command): void {
|
|
|
390
422
|
(d) => d.isDirectory() && existsSync(join(categoryDir, d.name, 'soleri-pack.json')),
|
|
391
423
|
);
|
|
392
424
|
|
|
393
|
-
if (packs.length === 0) continue;
|
|
394
|
-
|
|
395
|
-
console.log(`\n ${category}/`);
|
|
396
425
|
for (const pk of packs) {
|
|
397
426
|
try {
|
|
398
427
|
const manifest = JSON.parse(
|
|
399
428
|
readFileSync(join(categoryDir, pk.name, 'soleri-pack.json'), 'utf-8'),
|
|
400
429
|
);
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
430
|
+
allPacks.push({
|
|
431
|
+
id: manifest.id,
|
|
432
|
+
version: manifest.version,
|
|
433
|
+
description: manifest.description || '',
|
|
434
|
+
domains: (manifest.domains as string[])?.join(', ') || '—',
|
|
435
|
+
tier: manifest.tier ?? 'community',
|
|
436
|
+
category,
|
|
437
|
+
});
|
|
405
438
|
} catch {
|
|
406
439
|
// skip malformed packs
|
|
407
440
|
}
|
|
@@ -409,11 +442,31 @@ export function registerPack(program: Command): void {
|
|
|
409
442
|
}
|
|
410
443
|
}
|
|
411
444
|
|
|
412
|
-
if (
|
|
445
|
+
if (allPacks.length === 0) {
|
|
413
446
|
p.log.info('No packs found.');
|
|
414
|
-
|
|
415
|
-
console.log(`\n ${total} pack(s) available.\n`);
|
|
447
|
+
return;
|
|
416
448
|
}
|
|
449
|
+
|
|
450
|
+
// Group by tier and display
|
|
451
|
+
const tierOrder: Array<{ key: string; label: string }> = [
|
|
452
|
+
{ key: 'default', label: 'Default (included with Soleri)' },
|
|
453
|
+
{ key: 'community', label: 'Community (free)' },
|
|
454
|
+
{ key: 'premium', label: 'Premium (included — premium platform coming soon)' },
|
|
455
|
+
];
|
|
456
|
+
|
|
457
|
+
for (const { key, label } of tierOrder) {
|
|
458
|
+
const tierPacks = allPacks.filter((pk) => pk.tier === key);
|
|
459
|
+
if (tierPacks.length === 0) continue;
|
|
460
|
+
|
|
461
|
+
console.log(`\n ${label}`);
|
|
462
|
+
console.log(` ${'─'.repeat(label.length)}`);
|
|
463
|
+
for (const pk of tierPacks) {
|
|
464
|
+
console.log(` ${pk.id}@${pk.version} ${pk.description}`);
|
|
465
|
+
console.log(` domains: ${pk.domains}`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
console.log(`\n ${allPacks.length} pack(s) available.\n`);
|
|
417
470
|
});
|
|
418
471
|
|
|
419
472
|
// ─── create ─────────────────────────────────────────────────
|
|
@@ -441,6 +494,18 @@ export function registerPack(program: Command): void {
|
|
|
441
494
|
});
|
|
442
495
|
if (p.isCancel(description)) return;
|
|
443
496
|
|
|
497
|
+
const tier = await p.select({
|
|
498
|
+
message: 'Pack tier:',
|
|
499
|
+
options: [
|
|
500
|
+
{ value: 'community', label: 'Community — free, published to npm' },
|
|
501
|
+
{
|
|
502
|
+
value: 'premium',
|
|
503
|
+
label: 'Premium — requires Soleri platform account (coming soon)',
|
|
504
|
+
},
|
|
505
|
+
],
|
|
506
|
+
});
|
|
507
|
+
if (p.isCancel(tier)) return;
|
|
508
|
+
|
|
444
509
|
const author = await p.text({ message: 'Author:', placeholder: '@username' });
|
|
445
510
|
if (p.isCancel(author)) return;
|
|
446
511
|
|
|
@@ -454,6 +519,7 @@ export function registerPack(program: Command): void {
|
|
|
454
519
|
id: name,
|
|
455
520
|
version: '1.0.0',
|
|
456
521
|
description: description || '',
|
|
522
|
+
tier: tier || 'community',
|
|
457
523
|
author: author || '',
|
|
458
524
|
license: 'MIT',
|
|
459
525
|
soleri: '>=2.0.0',
|