@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.
Files changed (88) hide show
  1. package/dist/commands/agent.js +51 -2
  2. package/dist/commands/agent.js.map +1 -1
  3. package/dist/commands/hooks.js +126 -0
  4. package/dist/commands/hooks.js.map +1 -1
  5. package/dist/commands/install.js +5 -0
  6. package/dist/commands/install.js.map +1 -1
  7. package/dist/commands/pack.js +62 -13
  8. package/dist/commands/pack.js.map +1 -1
  9. package/dist/commands/staging.d.ts +49 -0
  10. package/dist/commands/staging.js +108 -18
  11. package/dist/commands/staging.js.map +1 -1
  12. package/dist/commands/yolo.d.ts +2 -0
  13. package/dist/commands/yolo.js +86 -0
  14. package/dist/commands/yolo.js.map +1 -0
  15. package/dist/hook-packs/converter/README.md +99 -0
  16. package/dist/hook-packs/converter/template.d.ts +36 -0
  17. package/dist/hook-packs/converter/template.js +127 -0
  18. package/dist/hook-packs/converter/template.js.map +1 -0
  19. package/dist/hook-packs/converter/template.test.ts +133 -0
  20. package/dist/hook-packs/converter/template.ts +163 -0
  21. package/dist/hook-packs/flock-guard/README.md +65 -0
  22. package/dist/hook-packs/flock-guard/manifest.json +36 -0
  23. package/dist/hook-packs/flock-guard/scripts/flock-guard-post.sh +48 -0
  24. package/dist/hook-packs/flock-guard/scripts/flock-guard-pre.sh +85 -0
  25. package/dist/hook-packs/full/manifest.json +8 -1
  26. package/dist/hook-packs/graduation.d.ts +11 -0
  27. package/dist/hook-packs/graduation.js +48 -0
  28. package/dist/hook-packs/graduation.js.map +1 -0
  29. package/dist/hook-packs/graduation.ts +65 -0
  30. package/dist/hook-packs/installer.js +3 -1
  31. package/dist/hook-packs/installer.js.map +1 -1
  32. package/dist/hook-packs/installer.ts +3 -1
  33. package/dist/hook-packs/marketing-research/README.md +37 -0
  34. package/dist/hook-packs/marketing-research/manifest.json +24 -0
  35. package/dist/hook-packs/marketing-research/scripts/marketing-research.sh +70 -0
  36. package/dist/hook-packs/registry.d.ts +1 -0
  37. package/dist/hook-packs/registry.js +14 -4
  38. package/dist/hook-packs/registry.js.map +1 -1
  39. package/dist/hook-packs/registry.ts +18 -4
  40. package/dist/hook-packs/safety/README.md +50 -0
  41. package/dist/hook-packs/safety/manifest.json +23 -0
  42. package/dist/hook-packs/safety/scripts/anti-deletion.sh +280 -0
  43. package/dist/hook-packs/validator.d.ts +32 -0
  44. package/dist/hook-packs/validator.js +126 -0
  45. package/dist/hook-packs/validator.js.map +1 -0
  46. package/dist/hook-packs/validator.ts +158 -0
  47. package/dist/hook-packs/yolo-safety/manifest.json +3 -19
  48. package/dist/hook-packs/yolo-safety/scripts/anti-deletion.sh +121 -61
  49. package/dist/main.js +2 -0
  50. package/dist/main.js.map +1 -1
  51. package/package.json +1 -1
  52. package/src/__tests__/flock-guard.test.ts +225 -0
  53. package/src/__tests__/graduation.test.ts +199 -0
  54. package/src/__tests__/hook-packs.test.ts +45 -20
  55. package/src/__tests__/hooks-convert.test.ts +342 -0
  56. package/src/__tests__/validator.test.ts +265 -0
  57. package/src/__tests__/wizard-e2e.mjs +1 -1
  58. package/src/commands/agent.ts +65 -2
  59. package/src/commands/hooks.ts +172 -0
  60. package/src/commands/install.ts +6 -0
  61. package/src/commands/pack.ts +80 -14
  62. package/src/commands/staging.ts +143 -20
  63. package/src/commands/yolo.ts +103 -0
  64. package/src/hook-packs/converter/README.md +99 -0
  65. package/src/hook-packs/converter/template.test.ts +133 -0
  66. package/src/hook-packs/converter/template.ts +163 -0
  67. package/src/hook-packs/flock-guard/README.md +65 -0
  68. package/src/hook-packs/flock-guard/manifest.json +36 -0
  69. package/src/hook-packs/flock-guard/scripts/flock-guard-post.sh +48 -0
  70. package/src/hook-packs/flock-guard/scripts/flock-guard-pre.sh +85 -0
  71. package/src/hook-packs/full/manifest.json +8 -1
  72. package/src/hook-packs/graduation.ts +65 -0
  73. package/src/hook-packs/installer.ts +3 -1
  74. package/src/hook-packs/marketing-research/README.md +37 -0
  75. package/src/hook-packs/marketing-research/manifest.json +24 -0
  76. package/src/hook-packs/marketing-research/scripts/marketing-research.sh +70 -0
  77. package/src/hook-packs/registry.ts +18 -4
  78. package/src/hook-packs/safety/README.md +50 -0
  79. package/src/hook-packs/safety/manifest.json +23 -0
  80. package/src/hook-packs/safety/scripts/anti-deletion.sh +280 -0
  81. package/src/hook-packs/validator.ts +158 -0
  82. package/src/hook-packs/yolo-safety/manifest.json +3 -19
  83. package/src/main.ts +2 -0
  84. package/vitest.config.ts +1 -0
  85. package/src/__tests__/archetypes.test.ts +0 -84
  86. package/src/__tests__/create.test.ts +0 -207
  87. package/src/hook-packs/yolo-safety/scripts/anti-deletion.sh +0 -214
  88. package/src/prompts/archetypes.ts +0 -343
@@ -1,21 +1,31 @@
1
- #!/usr/bin/env bash
1
+ #!/bin/sh
2
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.
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), perl (optional, for heredoc stripping)
19
+ # Dependencies: jq (required)
20
+ # POSIX sh compatible — no bash-specific features.
10
21
 
11
- set -euo pipefail
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=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
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: <<'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")
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=$(echo "$STRIPPED" | sed -E "s/'[^']*'//g" 2>/dev/null || echo "$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
- # 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
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
- # Check for rmdir commands
54
- if echo "$STRIPPED" | grep -qE '(^|\s|;|&&|\|\|)rmdir\s'; then
71
+ # rmdir
72
+ if matches '(^|\s|;|&&|\|\|)rmdir\s'; then
55
73
  IS_RMDIR=true
56
74
  fi
57
75
 
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
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
- # Check for git clean
67
- if echo "$STRIPPED" | grep -qE '(^|\s|;|&&|\|\|)git\s+clean\b'; then
84
+ # git clean
85
+ if matches '(^|\s|;|&&|\|\|)git\s+clean\b'; then
68
86
  IS_GIT_CLEAN=true
69
87
  fi
70
88
 
71
- # Check for git reset --hard
72
- if echo "$STRIPPED" | grep -qE '(^|\s|;|&&|\|\|)git\s+reset\s+--hard'; then
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
- # Check for git checkout -- . (restores all files, discards changes)
77
- if echo "$STRIPPED" | grep -qE '(^|\s|;|&&|\|\|)git\s+checkout\s+--\s+\.'; then
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
- # Check for git restore . (restores all files, discards changes)
82
- if echo "$STRIPPED" | grep -qE '(^|\s|;|&&|\|\|)git\s+restore\s+\.'; then
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
- # Not a destructive command let it through
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 ]; then
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
- # --- Handle git clean (block outright) ---
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
- # --- Handle git reset --hard (block outright) ---
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
- # --- Handle git checkout -- . (block outright) ---
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
- # --- Handle git restore . (block outright) ---
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
- # --- Handle mv of project directories (block outright) ---
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
- # --- Handle rmdir (block outright) ---
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=$(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 '^$')
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
- STAGED=()
172
- MISSING=()
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 echo "$filepath" 2>/dev/null || echo "$filepath")
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
- cp -R "$expanded" "$target_dir/" 2>/dev/null && STAGED+=("$expanded") || MISSING+=("$expanded")
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 && STAGED+=("$expanded") || MISSING+=("$expanded")
253
+ cp "$expanded" "$target_dir/" 2>/dev/null
189
254
  fi
190
- else
191
- MISSING+=("$expanded")
192
255
  fi
193
- done <<< "$FILES"
256
+ done
194
257
 
195
- # Build response
196
- STAGED_COUNT=${#STAGED[@]}
197
- MISSING_COUNT=${#MISSING[@]}
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 ] && [ "$MISSING_COUNT" -gt 0 ]; then
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: " + ($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.")
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":"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"}
1
+ {"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soleri/cli",
3
- "version": "9.3.1",
3
+ "version": "9.5.0",
4
4
  "description": "Developer CLI for creating and managing Soleri AI agents.",
5
5
  "keywords": [
6
6
  "agent",
@@ -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
+ });