@skillsmith/cli 0.6.2 → 0.6.4

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 (210) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/src/commands/__meta__/telemetry-coverage.test.d.ts +20 -0
  4. package/dist/src/commands/__meta__/telemetry-coverage.test.d.ts.map +1 -0
  5. package/dist/src/commands/__meta__/telemetry-coverage.test.js +118 -0
  6. package/dist/src/commands/__meta__/telemetry-coverage.test.js.map +1 -0
  7. package/dist/src/commands/ab-test.d.ts +1 -0
  8. package/dist/src/commands/ab-test.d.ts.map +1 -1
  9. package/dist/src/commands/ab-test.js +17 -8
  10. package/dist/src/commands/ab-test.js.map +1 -1
  11. package/dist/src/commands/analyze.d.ts +1 -0
  12. package/dist/src/commands/analyze.d.ts.map +1 -1
  13. package/dist/src/commands/analyze.js +32 -23
  14. package/dist/src/commands/analyze.js.map +1 -1
  15. package/dist/src/commands/audit-collisions.d.ts +1 -4
  16. package/dist/src/commands/audit-collisions.d.ts.map +1 -1
  17. package/dist/src/commands/audit-collisions.js +32 -23
  18. package/dist/src/commands/audit-collisions.js.map +1 -1
  19. package/dist/src/commands/audit-sources.action.d.ts +37 -0
  20. package/dist/src/commands/audit-sources.action.d.ts.map +1 -0
  21. package/dist/src/commands/audit-sources.action.js +300 -0
  22. package/dist/src/commands/audit-sources.action.js.map +1 -0
  23. package/dist/src/commands/audit-sources.d.ts +37 -0
  24. package/dist/src/commands/audit-sources.d.ts.map +1 -0
  25. package/dist/src/commands/audit-sources.js +55 -0
  26. package/dist/src/commands/audit-sources.js.map +1 -0
  27. package/dist/src/commands/audit-sources.test.d.ts +9 -0
  28. package/dist/src/commands/audit-sources.test.d.ts.map +1 -0
  29. package/dist/src/commands/audit-sources.test.js +228 -0
  30. package/dist/src/commands/audit-sources.test.js.map +1 -0
  31. package/dist/src/commands/audit.d.ts +2 -6
  32. package/dist/src/commands/audit.d.ts.map +1 -1
  33. package/dist/src/commands/audit.js +52 -31
  34. package/dist/src/commands/audit.js.map +1 -1
  35. package/dist/src/commands/author/index.d.ts +2 -1
  36. package/dist/src/commands/author/index.d.ts.map +1 -1
  37. package/dist/src/commands/author/index.js +4 -1
  38. package/dist/src/commands/author/index.js.map +1 -1
  39. package/dist/src/commands/author/init.action.d.ts +29 -0
  40. package/dist/src/commands/author/init.action.d.ts.map +1 -0
  41. package/dist/src/commands/author/init.action.js +131 -0
  42. package/dist/src/commands/author/init.action.js.map +1 -0
  43. package/dist/src/commands/author/init.d.ts +0 -14
  44. package/dist/src/commands/author/init.d.ts.map +1 -1
  45. package/dist/src/commands/author/init.js +3 -87
  46. package/dist/src/commands/author/init.js.map +1 -1
  47. package/dist/src/commands/author/mcp-init.d.ts +1 -0
  48. package/dist/src/commands/author/mcp-init.d.ts.map +1 -1
  49. package/dist/src/commands/author/mcp-init.js +21 -13
  50. package/dist/src/commands/author/mcp-init.js.map +1 -1
  51. package/dist/src/commands/author/subagent.d.ts +1 -0
  52. package/dist/src/commands/author/subagent.d.ts.map +1 -1
  53. package/dist/src/commands/author/subagent.js +23 -15
  54. package/dist/src/commands/author/subagent.js.map +1 -1
  55. package/dist/src/commands/author/transform.d.ts +1 -0
  56. package/dist/src/commands/author/transform.d.ts.map +1 -1
  57. package/dist/src/commands/author/transform.js +23 -15
  58. package/dist/src/commands/author/transform.js.map +1 -1
  59. package/dist/src/commands/config.d.ts +2 -0
  60. package/dist/src/commands/config.d.ts.map +1 -1
  61. package/dist/src/commands/config.js +43 -28
  62. package/dist/src/commands/config.js.map +1 -1
  63. package/dist/src/commands/create.d.ts +1 -0
  64. package/dist/src/commands/create.d.ts.map +1 -1
  65. package/dist/src/commands/create.js +28 -19
  66. package/dist/src/commands/create.js.map +1 -1
  67. package/dist/src/commands/diff.d.ts +4 -0
  68. package/dist/src/commands/diff.d.ts.map +1 -1
  69. package/dist/src/commands/diff.js +57 -42
  70. package/dist/src/commands/diff.js.map +1 -1
  71. package/dist/src/commands/diff.test.js +30 -0
  72. package/dist/src/commands/diff.test.js.map +1 -1
  73. package/dist/src/commands/import-local.d.ts +7 -0
  74. package/dist/src/commands/import-local.d.ts.map +1 -1
  75. package/dist/src/commands/import-local.js +43 -34
  76. package/dist/src/commands/import-local.js.map +1 -1
  77. package/dist/src/commands/import.d.ts +6 -0
  78. package/dist/src/commands/import.d.ts.map +1 -1
  79. package/dist/src/commands/import.js +23 -14
  80. package/dist/src/commands/import.js.map +1 -1
  81. package/dist/src/commands/index.d.ts +1 -0
  82. package/dist/src/commands/index.d.ts.map +1 -1
  83. package/dist/src/commands/index.js +2 -0
  84. package/dist/src/commands/index.js.map +1 -1
  85. package/dist/src/commands/info.d.ts +7 -0
  86. package/dist/src/commands/info.d.ts.map +1 -1
  87. package/dist/src/commands/info.js +77 -68
  88. package/dist/src/commands/info.js.map +1 -1
  89. package/dist/src/commands/install-skill.d.ts +3 -0
  90. package/dist/src/commands/install-skill.d.ts.map +1 -1
  91. package/dist/src/commands/install-skill.js +23 -15
  92. package/dist/src/commands/install-skill.js.map +1 -1
  93. package/dist/src/commands/install.d.ts +21 -0
  94. package/dist/src/commands/install.d.ts.map +1 -1
  95. package/dist/src/commands/install.js +154 -139
  96. package/dist/src/commands/install.js.map +1 -1
  97. package/dist/src/commands/login.d.ts +4 -0
  98. package/dist/src/commands/login.d.ts.map +1 -1
  99. package/dist/src/commands/login.js +56 -47
  100. package/dist/src/commands/login.js.map +1 -1
  101. package/dist/src/commands/logout.d.ts +1 -0
  102. package/dist/src/commands/logout.d.ts.map +1 -1
  103. package/dist/src/commands/logout.js +36 -27
  104. package/dist/src/commands/logout.js.map +1 -1
  105. package/dist/src/commands/manage.d.ts +3 -9
  106. package/dist/src/commands/manage.d.ts.map +1 -1
  107. package/dist/src/commands/manage.js +69 -45
  108. package/dist/src/commands/manage.js.map +1 -1
  109. package/dist/src/commands/merge.d.ts +7 -0
  110. package/dist/src/commands/merge.d.ts.map +1 -1
  111. package/dist/src/commands/merge.js +117 -108
  112. package/dist/src/commands/merge.js.map +1 -1
  113. package/dist/src/commands/pin.d.ts +2 -0
  114. package/dist/src/commands/pin.d.ts.map +1 -1
  115. package/dist/src/commands/pin.js +88 -73
  116. package/dist/src/commands/pin.js.map +1 -1
  117. package/dist/src/commands/recommend.d.ts +1 -0
  118. package/dist/src/commands/recommend.d.ts.map +1 -1
  119. package/dist/src/commands/recommend.js +35 -27
  120. package/dist/src/commands/recommend.js.map +1 -1
  121. package/dist/src/commands/search-formatters.d.ts +1 -0
  122. package/dist/src/commands/search-formatters.d.ts.map +1 -1
  123. package/dist/src/commands/search-formatters.js +3 -0
  124. package/dist/src/commands/search-formatters.js.map +1 -1
  125. package/dist/src/commands/search.action.d.ts +16 -0
  126. package/dist/src/commands/search.action.d.ts.map +1 -0
  127. package/dist/src/commands/search.action.js +384 -0
  128. package/dist/src/commands/search.action.js.map +1 -0
  129. package/dist/src/commands/search.d.ts +3 -0
  130. package/dist/src/commands/search.d.ts.map +1 -1
  131. package/dist/src/commands/search.js +5 -343
  132. package/dist/src/commands/search.js.map +1 -1
  133. package/dist/src/commands/sync.action.d.ts +39 -0
  134. package/dist/src/commands/sync.action.d.ts.map +1 -0
  135. package/dist/src/commands/sync.action.js +353 -0
  136. package/dist/src/commands/sync.action.js.map +1 -0
  137. package/dist/src/commands/sync.d.ts +3 -0
  138. package/dist/src/commands/sync.d.ts.map +1 -1
  139. package/dist/src/commands/sync.js +8 -310
  140. package/dist/src/commands/sync.js.map +1 -1
  141. package/dist/src/commands/telemetry.action.d.ts +38 -0
  142. package/dist/src/commands/telemetry.action.d.ts.map +1 -0
  143. package/dist/src/commands/telemetry.action.js +350 -0
  144. package/dist/src/commands/telemetry.action.js.map +1 -0
  145. package/dist/src/commands/telemetry.d.ts +30 -0
  146. package/dist/src/commands/telemetry.d.ts.map +1 -0
  147. package/dist/src/commands/telemetry.helpers.d.ts +58 -0
  148. package/dist/src/commands/telemetry.helpers.d.ts.map +1 -0
  149. package/dist/src/commands/telemetry.helpers.js +173 -0
  150. package/dist/src/commands/telemetry.helpers.js.map +1 -0
  151. package/dist/src/commands/telemetry.helpers.test.d.ts +15 -0
  152. package/dist/src/commands/telemetry.helpers.test.d.ts.map +1 -0
  153. package/dist/src/commands/telemetry.helpers.test.js +240 -0
  154. package/dist/src/commands/telemetry.helpers.test.js.map +1 -0
  155. package/dist/src/commands/telemetry.js +65 -0
  156. package/dist/src/commands/telemetry.js.map +1 -0
  157. package/dist/src/commands/telemetry.test.d.ts +18 -0
  158. package/dist/src/commands/telemetry.test.d.ts.map +1 -0
  159. package/dist/src/commands/telemetry.test.js +401 -0
  160. package/dist/src/commands/telemetry.test.js.map +1 -0
  161. package/dist/src/commands/whoami.d.ts +1 -0
  162. package/dist/src/commands/whoami.d.ts.map +1 -1
  163. package/dist/src/commands/whoami.js +29 -21
  164. package/dist/src/commands/whoami.js.map +1 -1
  165. package/dist/src/index.js +3 -1
  166. package/dist/src/index.js.map +1 -1
  167. package/dist/src/utils/manifest.d.ts +75 -0
  168. package/dist/src/utils/manifest.d.ts.map +1 -1
  169. package/dist/src/utils/manifest.js +78 -0
  170. package/dist/src/utils/manifest.js.map +1 -1
  171. package/dist/src/utils/manifest.test.d.ts +20 -0
  172. package/dist/src/utils/manifest.test.d.ts.map +1 -0
  173. package/dist/src/utils/manifest.test.js +344 -0
  174. package/dist/src/utils/manifest.test.js.map +1 -0
  175. package/dist/src/utils/open-database.d.ts +10 -2
  176. package/dist/src/utils/open-database.d.ts.map +1 -1
  177. package/dist/src/utils/open-database.js +15 -2
  178. package/dist/src/utils/open-database.js.map +1 -1
  179. package/dist/src/utils/skills-directory.d.ts.map +1 -1
  180. package/dist/src/utils/skills-directory.js +5 -1
  181. package/dist/src/utils/skills-directory.js.map +1 -1
  182. package/dist/tests/e2e/audit-sources-apply.test.d.ts +13 -0
  183. package/dist/tests/e2e/audit-sources-apply.test.d.ts.map +1 -0
  184. package/dist/tests/e2e/audit-sources-apply.test.js +166 -0
  185. package/dist/tests/e2e/audit-sources-apply.test.js.map +1 -0
  186. package/dist/tests/e2e/audit-sources-recovery.test.d.ts +15 -0
  187. package/dist/tests/e2e/audit-sources-recovery.test.d.ts.map +1 -0
  188. package/dist/tests/e2e/audit-sources-recovery.test.js +179 -0
  189. package/dist/tests/e2e/audit-sources-recovery.test.js.map +1 -0
  190. package/dist/tests/e2e/audit-sources-roundtrip.test.d.ts +23 -0
  191. package/dist/tests/e2e/audit-sources-roundtrip.test.d.ts.map +1 -0
  192. package/dist/tests/e2e/audit-sources-roundtrip.test.js +107 -0
  193. package/dist/tests/e2e/audit-sources-roundtrip.test.js.map +1 -0
  194. package/dist/tests/e2e/utils/source-recovery-fixture.d.ts +49 -0
  195. package/dist/tests/e2e/utils/source-recovery-fixture.d.ts.map +1 -0
  196. package/dist/tests/e2e/utils/source-recovery-fixture.js +106 -0
  197. package/dist/tests/e2e/utils/source-recovery-fixture.js.map +1 -0
  198. package/dist/tests/embedding-probe-stdio.test.d.ts +11 -0
  199. package/dist/tests/embedding-probe-stdio.test.d.ts.map +1 -0
  200. package/dist/tests/embedding-probe-stdio.test.js +58 -0
  201. package/dist/tests/embedding-probe-stdio.test.js.map +1 -0
  202. package/dist/tests/init.test.js +2 -1
  203. package/dist/tests/init.test.js.map +1 -1
  204. package/dist/tests/install-quarantine.test.d.ts +10 -0
  205. package/dist/tests/install-quarantine.test.d.ts.map +1 -0
  206. package/dist/tests/install-quarantine.test.js +86 -0
  207. package/dist/tests/install-quarantine.test.js.map +1 -0
  208. package/package.json +5 -5
  209. package/templates/skill-telemetry.cmd +16 -0
  210. package/templates/skill-telemetry.sh +199 -0
@@ -0,0 +1,199 @@
1
+ #!/usr/bin/env sh
2
+ # Skillsmith skill-invocation telemetry hook (SMI-5012 / SMI-5020 W3.S1).
3
+ #
4
+ # Invoked from ~/.claude/settings.json PreToolUse + PostToolUse matchers on
5
+ # the `Skill` tool. Captures skill-invocation latency telemetry for the
6
+ # Skillsmith /functions/v1/events endpoint.
7
+ #
8
+ # PRIVACY INVARIANTS (DO NOT WEAKEN):
9
+ # - NEVER reads `.tool_input.args` from the hook stdin payload. EVER.
10
+ # Only `.session_id` and `.tool_input.skill` are extracted.
11
+ # - NEVER captures absolute paths, cwd, env vars, or file contents.
12
+ # - NEVER blocks the user. Exits 0 in every code path. No `set -e`.
13
+ # - Curl is fire-and-forget (backgrounded + output redirected).
14
+ #
15
+ # CROSS-PLATFORM:
16
+ # - POSIX `sh` only (NOT bash). Runs on macOS /bin/sh (dash-equiv),
17
+ # Linux /bin/sh (dash/bash), and Git Bash on Windows (via .cmd shim).
18
+ # - `date +%s%3N` (GNU) falls back to `date +%s` (BSD/macOS) for second-
19
+ # precision when millisecond precision is unavailable.
20
+ #
21
+ # STATE:
22
+ # - Start files: $HOME/.skillsmith/run/skill-$SESSION-$SKILL.start
23
+ # Per M8: lives under ~/.skillsmith/run (NOT /tmp) so files survive
24
+ # reboot; orphans GC'd by next pre-call's 1h sweep.
25
+ #
26
+ # OPT-IN GATES (both must pass; checked before parsing stdin):
27
+ # 1. SKILLSMITH_TELEMETRY_DISABLE != "1" (panic switch env var)
28
+ # 2. ~/.skillsmith/config.json `.telemetry.enabled == true`
29
+ #
30
+ # VERIFICATION (1000-iteration micro-bench):
31
+ # PAYLOAD='{"session_id":"abc","tool_input":{"skill":"search"}}'
32
+ # for i in $(seq 1 1000); do
33
+ # printf '%s' "$PAYLOAD" | time \
34
+ # sh packages/cli/templates/skill-telemetry.sh pre
35
+ # done
36
+ # # Target: <25ms p95 EXCLUDING the backgrounded curl in `post`.
37
+ #
38
+ # Claude Code hook stdin contract:
39
+ # https://code.claude.com/docs/en/hooks.md
40
+
41
+ set -u
42
+
43
+ MODE="${1:-}"
44
+
45
+ # ---- Gate 1: panic switch ----
46
+ if [ "${SKILLSMITH_TELEMETRY_DISABLE:-}" = "1" ]; then
47
+ exit 0
48
+ fi
49
+
50
+ # ---- Gate 2: jq availability (defensive; required by other Skillsmith hooks) ----
51
+ command -v jq >/dev/null 2>&1 || exit 0
52
+
53
+ # ---- Gate 3: opt-in config (single jq invocation for all three reads) ----
54
+ CONFIG="${HOME}/.skillsmith/config.json"
55
+ if [ ! -f "$CONFIG" ]; then
56
+ exit 0
57
+ fi
58
+ # Output is newline-separated: enabled-bool, endpoint, anonymousId.
59
+ # `jq -e` here would require a separate call; we instead check the first
60
+ # line for "true" via `case`. One jq fork instead of three.
61
+ CFG_OUT="$(jq -r '
62
+ (.telemetry.enabled // false),
63
+ (.telemetry.endpoint // "https://vrcnzpmndtroqxxoqkzy.supabase.co/functions/v1/events"),
64
+ (.telemetry.anonymousId // "")
65
+ ' "$CONFIG" 2>/dev/null)"
66
+ # POSIX line-extraction without spawning awk/sed.
67
+ CFG_ENABLED="${CFG_OUT%%
68
+ *}"
69
+ CFG_REST="${CFG_OUT#*
70
+ }"
71
+ ENDPOINT="${CFG_REST%%
72
+ *}"
73
+ ANON_ID="${CFG_REST#*
74
+ }"
75
+ case "$CFG_ENABLED" in
76
+ true) ;;
77
+ *) exit 0 ;;
78
+ esac
79
+
80
+ # ---- Parse Claude Code hook stdin payload (single jq for both reads) ----
81
+ # IMPORTANT: only `.session_id` and `.tool_input.skill` are extracted.
82
+ # `.tool_input.args` is NEVER read. See privacy invariants above.
83
+ PAYLOAD="$(cat 2>/dev/null || printf '%s' '{}')"
84
+ STDIN_OUT="$(printf '%s' "$PAYLOAD" | jq -r '(.session_id // ""), (.tool_input.skill // "")' 2>/dev/null)"
85
+ SESSION_ID="${STDIN_OUT%%
86
+ *}"
87
+ SKILL_NAME="${STDIN_OUT#*
88
+ }"
89
+
90
+ RUN_DIR="${HOME}/.skillsmith/run"
91
+ mkdir -p "$RUN_DIR" 2>/dev/null || true
92
+
93
+ # ---- Quarantine path: unparseable payload ----
94
+ # Per risk #13 (line 803): tool_input.skill shape changes ship a
95
+ # `skill_invoke_unparsed` event. Emit ONLY from `pre` so we don't
96
+ # double-count (post has no matching start file to read anyway).
97
+ if [ -z "$SESSION_ID" ] || [ -z "$SKILL_NAME" ]; then
98
+ if [ "$MODE" = "pre" ] && [ -n "$ANON_ID" ]; then
99
+ QUARANTINE_PAYLOAD="$(jq -n \
100
+ --arg event 'skill_invoke_unparsed' \
101
+ --arg anon "$ANON_ID" \
102
+ --arg source 'claude-code-hook' \
103
+ --arg framework 'claude-code' \
104
+ --arg platform "$(uname -s 2>/dev/null | tr '[:upper:]' '[:lower:]')" \
105
+ '{event:$event, anonymous_id:$anon, metadata:{source:$source, framework:$framework, platform:$platform, success:false}}' \
106
+ 2>/dev/null)"
107
+ if [ -n "$QUARANTINE_PAYLOAD" ]; then
108
+ nohup curl -s -m 5 -X POST \
109
+ -H 'Content-Type: application/json' \
110
+ -d "$QUARANTINE_PAYLOAD" \
111
+ "$ENDPOINT" >/dev/null 2>&1 &
112
+ fi
113
+ fi
114
+ exit 0
115
+ fi
116
+
117
+ START_FILE="$RUN_DIR/skill-${SESSION_ID}-${SKILL_NAME}.start"
118
+
119
+ case "$MODE" in
120
+ pre)
121
+ # GC orphans older than 1h (mtime), then record start time (ms epoch).
122
+ # `find -mmin` is portable across BSD (macOS) and GNU (Linux/Git Bash).
123
+ find "$RUN_DIR" -maxdepth 1 -name 'skill-*.start' -type f -mmin +60 -delete 2>/dev/null || true
124
+ NOW_MS="$(date +%s%3N 2>/dev/null)"
125
+ # BSD date doesn't support %3N — falls back to seconds with 000 suffix.
126
+ case "$NOW_MS" in
127
+ *N|'')
128
+ NOW_MS="$(date +%s 2>/dev/null)000"
129
+ ;;
130
+ esac
131
+ printf '%s' "$NOW_MS" > "$START_FILE" 2>/dev/null || true
132
+ ;;
133
+
134
+ post)
135
+ if [ ! -f "$START_FILE" ]; then
136
+ exit 0
137
+ fi
138
+ START_MS="$(cat "$START_FILE" 2>/dev/null)"
139
+ rm -f "$START_FILE" 2>/dev/null || true
140
+ if [ -z "$START_MS" ]; then
141
+ exit 0
142
+ fi
143
+
144
+ NOW_MS="$(date +%s%3N 2>/dev/null)"
145
+ case "$NOW_MS" in
146
+ *N|'')
147
+ NOW_MS="$(date +%s 2>/dev/null)000"
148
+ ;;
149
+ esac
150
+
151
+ # Pure-sh integer arithmetic; both operands are integers above.
152
+ DURATION_MS=$((NOW_MS - START_MS))
153
+ if [ "$DURATION_MS" -lt 0 ]; then
154
+ DURATION_MS=0
155
+ fi
156
+
157
+ # If no anonymous id has been provisioned yet, we have nothing to send.
158
+ if [ -z "$ANON_ID" ]; then
159
+ exit 0
160
+ fi
161
+
162
+ PLATFORM="$(uname -s 2>/dev/null | tr '[:upper:]' '[:lower:]')"
163
+
164
+ # Build payload (see wire format at skill-invoke-telemetry.md:614-643).
165
+ # NOTE: only $SKILL_NAME and $SESSION_ID flow from stdin → payload.
166
+ # No args, no paths, no env vars.
167
+ PAYLOAD_JSON="$(jq -n \
168
+ --arg event 'skill_invoke' \
169
+ --arg anon "$ANON_ID" \
170
+ --arg skill "$SKILL_NAME" \
171
+ --arg session "$SESSION_ID" \
172
+ --argjson dur "$DURATION_MS" \
173
+ --arg source 'claude-code-hook' \
174
+ --arg framework 'claude-code' \
175
+ --arg platform "$PLATFORM" \
176
+ '{event:$event, anonymous_id:$anon, metadata:{skill_name:$skill, session_id:$session, duration_ms:$dur, source:$source, framework:$framework, platform:$platform, success:true, is_subagent:false}}' \
177
+ 2>/dev/null)"
178
+
179
+ if [ -z "$PAYLOAD_JSON" ]; then
180
+ exit 0
181
+ fi
182
+
183
+ # Fire-and-forget. `nohup` + `&` + redirected fd's mean the parent
184
+ # shell does not wait on the curl process. `-m 5` caps the curl at
185
+ # 5s in case the endpoint is unreachable (no impact on hook latency
186
+ # because the curl is already detached).
187
+ nohup curl -s -m 5 -X POST \
188
+ -H 'Content-Type: application/json' \
189
+ -d "$PAYLOAD_JSON" \
190
+ "$ENDPOINT" >/dev/null 2>&1 &
191
+ ;;
192
+
193
+ *)
194
+ # Unknown mode — silently exit. Belt-and-suspenders.
195
+ :
196
+ ;;
197
+ esac
198
+
199
+ exit 0