superacli 1.1.0 → 1.1.2

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 (261) hide show
  1. package/.beads/.br_history/issues.20260308_235202_180577215.jsonl +57 -0
  2. package/.beads/.br_history/issues.20260308_235202_387414163.jsonl +57 -0
  3. package/.beads/.br_history/issues.20260308_235202_564422794.jsonl +57 -0
  4. package/.beads/.br_history/issues.20260308_235202_742600597.jsonl +57 -0
  5. package/.beads/.br_history/issues.20260308_235208_133360069.jsonl +57 -0
  6. package/.beads/.br_history/issues.20260308_235505_473406307.jsonl +57 -0
  7. package/.beads/.br_history/issues.20260308_235505_662360489.jsonl +57 -0
  8. package/.beads/.br_history/issues.20260308_235505_843935624.jsonl +57 -0
  9. package/.beads/.br_history/issues.20260308_235506_044530221.jsonl +57 -0
  10. package/.beads/.br_history/issues.20260309_002618_115728731.jsonl +57 -0
  11. package/.beads/.br_history/issues.20260309_003748_878174586.jsonl +57 -0
  12. package/.beads/.br_history/issues.20260309_004057_868755623.jsonl +57 -0
  13. package/.beads/.br_history/issues.20260309_004058_512842163.jsonl +57 -0
  14. package/.beads/.br_history/issues.20260309_004058_994445226.jsonl +57 -0
  15. package/.beads/.br_history/issues.20260309_004059_475988596.jsonl +57 -0
  16. package/.beads/.br_history/issues.20260309_161902_566857851.jsonl +57 -0
  17. package/.beads/.br_history/issues.20260309_170512_277017739.jsonl +57 -0
  18. package/.beads/.br_history/issues.20260309_170512_477876921.jsonl +57 -0
  19. package/.beads/.br_history/issues.20260309_170512_664382701.jsonl +57 -0
  20. package/.beads/.br_history/issues.20260309_170512_859400333.jsonl +57 -0
  21. package/.beads/.br_history/issues.20260309_212326_082771164.jsonl +57 -0
  22. package/.beads/.br_history/issues.20260309_212326_245619716.jsonl +58 -0
  23. package/.beads/.br_history/issues.20260309_212326_403198317.jsonl +59 -0
  24. package/.beads/.br_history/issues.20260309_212332_539197678.jsonl +60 -0
  25. package/.beads/.br_history/issues.20260309_212332_731373599.jsonl +60 -0
  26. package/.beads/.br_history/issues.20260309_212332_928710953.jsonl +60 -0
  27. package/.beads/.br_history/issues.20260309_213021_341505240.jsonl +60 -0
  28. package/.beads/.br_history/issues.20260309_213022_023136934.jsonl +60 -0
  29. package/.beads/.br_history/issues.20260309_213022_400050719.jsonl +60 -0
  30. package/.beads/issues.jsonl +20 -17
  31. package/README.md +1 -1
  32. package/__tests__/adapter-schema.test.js +3 -0
  33. package/__tests__/aws-plugin.test.js +84 -0
  34. package/__tests__/az-plugin.test.js +84 -0
  35. package/__tests__/builtin-adapter.test.js +29 -0
  36. package/__tests__/cline-plugin.test.js +109 -0
  37. package/__tests__/cline-skill.test.js +49 -0
  38. package/__tests__/docker-plugin.test.js +3 -1
  39. package/__tests__/eza-plugin.test.js +81 -0
  40. package/__tests__/gcloud-plugin.test.js +86 -0
  41. package/__tests__/gh-plugin.test.js +86 -0
  42. package/__tests__/helm-plugin.test.js +81 -0
  43. package/__tests__/http-adapter.test.js +118 -0
  44. package/__tests__/just-plugin.test.js +82 -0
  45. package/__tests__/kubectl-plugin.test.js +83 -0
  46. package/__tests__/linear-plugin.test.js +81 -0
  47. package/__tests__/mcp-adapter.test.js +187 -0
  48. package/__tests__/nextest-plugin.test.js +82 -0
  49. package/__tests__/npm-plugin.test.js +81 -0
  50. package/__tests__/nullclaw-plugin.test.js +157 -0
  51. package/__tests__/openapi-adapter.test.js +199 -0
  52. package/__tests__/plugin-agency-agents.test.js +6 -6
  53. package/__tests__/plugin-nullclaw.test.js +78 -0
  54. package/__tests__/plugin-visual-explainer.test.js +62 -0
  55. package/__tests__/plugins-manager.test.js +59 -10
  56. package/__tests__/pnpm-plugin.test.js +81 -0
  57. package/__tests__/poetry-plugin.test.js +83 -0
  58. package/__tests__/process-adapter.test.js +124 -90
  59. package/__tests__/pulumi-plugin.test.js +81 -0
  60. package/__tests__/railway-plugin.test.js +84 -0
  61. package/__tests__/server-app.test.js +67 -0
  62. package/__tests__/server-config-service.test.js +79 -0
  63. package/__tests__/server-routes-ask.test.js +89 -0
  64. package/__tests__/server-routes-commands.test.js +55 -0
  65. package/__tests__/server-routes-config.test.js +87 -0
  66. package/__tests__/server-routes-jobs.test.js +53 -0
  67. package/__tests__/server-routes-misc.test.js +112 -0
  68. package/__tests__/server-storage-adapter.test.js +40 -0
  69. package/__tests__/server-storage-file.test.js +73 -0
  70. package/__tests__/server-storage-mongo.test.js +74 -0
  71. package/__tests__/shell-adapter.test.js +81 -22
  72. package/__tests__/stripe-plugin.test.js +3 -1
  73. package/__tests__/supabase-plugin.test.js +86 -0
  74. package/__tests__/terraform-plugin.test.js +83 -0
  75. package/__tests__/uv-plugin.test.js +81 -0
  76. package/__tests__/vercel-plugin.test.js +81 -0
  77. package/__tests__/watchexec-plugin.test.js +80 -0
  78. package/cli/adapter-schema.js +5 -0
  79. package/cli/adapters/process.js +53 -2
  80. package/cli/plugin-install-guidance.js +320 -0
  81. package/cli/plugins-manager.js +272 -212
  82. package/cli/skills.js +16 -5
  83. package/cli/supercli.js +26 -2
  84. package/docs/plugins.md +2 -0
  85. package/docs/skills/cline-non-interactive/SKILL.md +59 -0
  86. package/docs/skills-catalog.md +35 -0
  87. package/docs/visual-overview.md +21 -0
  88. package/jest.config.js +4 -1
  89. package/package.json +4 -3
  90. package/plugins/agency-agents/plugin.json +5 -0
  91. package/{cli/plugin-agency-agents.js → plugins/agency-agents/scripts/post-install.js} +13 -3
  92. package/plugins/aws/README.md +46 -0
  93. package/plugins/aws/plugin.json +42 -0
  94. package/plugins/az/README.md +46 -0
  95. package/plugins/az/plugin.json +42 -0
  96. package/plugins/clickup/plugin.json +38 -0
  97. package/plugins/clickup/scripts/post-install.js +107 -0
  98. package/plugins/clickup/scripts/post-uninstall.js +30 -0
  99. package/plugins/cline/README.md +48 -0
  100. package/plugins/cline/plugin.json +92 -0
  101. package/plugins/eza/README.md +40 -0
  102. package/plugins/eza/plugin.json +42 -0
  103. package/plugins/gcloud/README.md +46 -0
  104. package/plugins/gcloud/plugin.json +42 -0
  105. package/plugins/gh/README.md +46 -0
  106. package/plugins/gh/plugin.json +43 -0
  107. package/plugins/helm/README.md +42 -0
  108. package/plugins/helm/plugin.json +42 -0
  109. package/plugins/just/README.md +42 -0
  110. package/plugins/just/plugin.json +42 -0
  111. package/plugins/kubectl/README.md +46 -0
  112. package/plugins/kubectl/plugin.json +42 -0
  113. package/plugins/linear/README.md +60 -0
  114. package/plugins/linear/plugin.json +42 -0
  115. package/plugins/nextest/README.md +42 -0
  116. package/plugins/nextest/plugin.json +42 -0
  117. package/plugins/npm/README.md +46 -0
  118. package/plugins/npm/plugin.json +42 -0
  119. package/plugins/nullclaw/README.md +45 -0
  120. package/plugins/nullclaw/plugin.json +64 -0
  121. package/plugins/nullclaw/scripts/post-install.js +189 -0
  122. package/plugins/nullclaw/scripts/post-uninstall.js +25 -0
  123. package/plugins/openfang/plugin.json +37 -0
  124. package/plugins/openfang/scripts/post-install.js +163 -0
  125. package/plugins/openfang/scripts/post-uninstall.js +30 -0
  126. package/plugins/plugins.json +234 -0
  127. package/plugins/pnpm/README.md +46 -0
  128. package/plugins/pnpm/plugin.json +42 -0
  129. package/plugins/poetry/README.md +46 -0
  130. package/plugins/poetry/plugin.json +42 -0
  131. package/plugins/pulumi/README.md +46 -0
  132. package/plugins/pulumi/plugin.json +42 -0
  133. package/plugins/railway/README.md +58 -0
  134. package/plugins/railway/plugin.json +43 -0
  135. package/plugins/supabase/README.md +55 -0
  136. package/plugins/supabase/plugin.json +42 -0
  137. package/plugins/superpowers/plugin.json +22 -0
  138. package/plugins/superpowers/scripts/post-install.js +124 -0
  139. package/plugins/superpowers/scripts/post-uninstall.js +30 -0
  140. package/plugins/terraform/README.md +46 -0
  141. package/plugins/terraform/plugin.json +42 -0
  142. package/plugins/uv/README.md +46 -0
  143. package/plugins/uv/plugin.json +42 -0
  144. package/plugins/vercel/README.md +47 -0
  145. package/plugins/vercel/plugin.json +42 -0
  146. package/plugins/visual-explainer/plugin.json +15 -0
  147. package/plugins/visual-explainer/scripts/post-install.js +111 -0
  148. package/plugins/watchexec/README.md +40 -0
  149. package/plugins/watchexec/plugin.json +42 -0
  150. package/tests/test-aws-smoke.sh +56 -0
  151. package/tests/test-az-smoke.sh +56 -0
  152. package/tests/test-cline-smoke.sh +37 -0
  153. package/tests/test-eza-smoke.sh +33 -0
  154. package/tests/test-gcloud-smoke.sh +56 -0
  155. package/tests/test-gh-smoke.sh +56 -0
  156. package/tests/test-helm-smoke.sh +33 -0
  157. package/tests/test-just-smoke.sh +40 -0
  158. package/tests/test-kubectl-smoke.sh +37 -0
  159. package/tests/test-linear-smoke.sh +97 -0
  160. package/tests/test-nextest-smoke.sh +33 -0
  161. package/tests/test-npm-smoke.sh +32 -0
  162. package/tests/test-nullclaw-smoke.sh +51 -0
  163. package/tests/test-plugins-registry.js +110 -0
  164. package/tests/test-pnpm-smoke.sh +33 -0
  165. package/tests/test-poetry-smoke.sh +33 -0
  166. package/tests/test-pulumi-smoke.sh +33 -0
  167. package/tests/test-railway-smoke.sh +95 -0
  168. package/tests/test-supabase-smoke.sh +95 -0
  169. package/tests/test-terraform-smoke.sh +33 -0
  170. package/tests/test-uv-smoke.sh +33 -0
  171. package/tests/test-vercel-smoke.sh +55 -0
  172. package/tests/test-watchexec-smoke.sh +33 -0
  173. package/.beads/.br_history/issues.20260308_180927_477542428.jsonl +0 -12
  174. package/.beads/.br_history/issues.20260308_181032_020230108.jsonl +0 -12
  175. package/.beads/.br_history/issues.20260308_181032_180539413.jsonl +0 -12
  176. package/.beads/.br_history/issues.20260308_181032_372621506.jsonl +0 -12
  177. package/.beads/.br_history/issues.20260308_181032_565142225.jsonl +0 -12
  178. package/.beads/.br_history/issues.20260308_181311_336346464.jsonl +0 -12
  179. package/.beads/.br_history/issues.20260308_181444_039234498.jsonl +0 -13
  180. package/.beads/.br_history/issues.20260308_181503_794764403.jsonl +0 -13
  181. package/.beads/.br_history/issues.20260308_181503_950163105.jsonl +0 -13
  182. package/.beads/.br_history/issues.20260308_192031_852553505.jsonl +0 -13
  183. package/.beads/.br_history/issues.20260308_193552_846920518.jsonl +0 -14
  184. package/.beads/.br_history/issues.20260308_194054_394884833.jsonl +0 -14
  185. package/.beads/.br_history/issues.20260308_194209_440472460.jsonl +0 -15
  186. package/.beads/.br_history/issues.20260308_195319_099391899.jsonl +0 -15
  187. package/.beads/.br_history/issues.20260308_195324_176987204.jsonl +0 -16
  188. package/.beads/.br_history/issues.20260308_195436_929114019.jsonl +0 -16
  189. package/.beads/.br_history/issues.20260308_195437_055808298.jsonl +0 -17
  190. package/.beads/.br_history/issues.20260308_195437_304297399.jsonl +0 -18
  191. package/.beads/.br_history/issues.20260308_195437_556007332.jsonl +0 -19
  192. package/.beads/.br_history/issues.20260308_195444_987209695.jsonl +0 -20
  193. package/.beads/.br_history/issues.20260308_195445_133350193.jsonl +0 -20
  194. package/.beads/.br_history/issues.20260308_195445_400185615.jsonl +0 -20
  195. package/.beads/.br_history/issues.20260308_195445_689886334.jsonl +0 -20
  196. package/.beads/.br_history/issues.20260308_195445_949947727.jsonl +0 -20
  197. package/.beads/.br_history/issues.20260308_195745_580473297.jsonl +0 -20
  198. package/.beads/.br_history/issues.20260308_195745_725920532.jsonl +0 -20
  199. package/.beads/.br_history/issues.20260308_195745_968227911.jsonl +0 -20
  200. package/.beads/.br_history/issues.20260308_195746_224276322.jsonl +0 -20
  201. package/.beads/.br_history/issues.20260308_200018_386890807.jsonl +0 -20
  202. package/ref-btcbot/.env.example +0 -19
  203. package/ref-btcbot/README.md +0 -3
  204. package/ref-btcbot/docs/bot.md +0 -72
  205. package/ref-btcbot/docs/skills/btcbot.backtest/SKILL.md +0 -70
  206. package/ref-btcbot/docs/skills/btcbot.config/SKILL.md +0 -79
  207. package/ref-btcbot/docs/skills/btcbot.orders/SKILL.md +0 -60
  208. package/ref-btcbot/docs/skills/btcbot.positions/SKILL.md +0 -54
  209. package/ref-btcbot/docs/skills/btcbot.risk/SKILL.md +0 -69
  210. package/ref-btcbot/docs/skills/btcbot.run-loop/SKILL.md +0 -63
  211. package/ref-btcbot/docs/skills/btcbot.run-once/SKILL.md +0 -63
  212. package/ref-btcbot/docs/skills/btcbot.status/SKILL.md +0 -59
  213. package/ref-btcbot/examples/sample-candles.json +0 -52
  214. package/ref-btcbot/package.json +0 -18
  215. package/ref-btcbot/plugin/plugin.json +0 -146
  216. package/ref-btcbot/src/cli.js +0 -104
  217. package/ref-btcbot/src/config.js +0 -80
  218. package/ref-btcbot/src/core/bot-runner.js +0 -78
  219. package/ref-btcbot/src/core/exchange-factory.js +0 -19
  220. package/ref-btcbot/src/core/live-exchange.js +0 -12
  221. package/ref-btcbot/src/core/paper-exchange.js +0 -82
  222. package/ref-btcbot/src/core/risk-engine.js +0 -42
  223. package/ref-btcbot/src/core/state-repository.js +0 -47
  224. package/ref-btcbot/src/services/backtest-service.js +0 -44
  225. package/ref-btcbot/src/services/market-data.js +0 -32
  226. package/ref-btcbot/src/storage/json-store.js +0 -45
  227. package/ref-btcbot/src/strategy/hedge-strategy.js +0 -65
  228. package/ref-btcbot/src/strategy/indicators.js +0 -56
  229. package/ref-btcbot/src/web/api.js +0 -50
  230. package/ref-btcbot/src/web/app.js +0 -51
  231. package/ref-btcbot/src/web/index.html +0 -70
  232. package/ref-btcbot/src/web/server.js +0 -33
  233. /package/.beads/.br_history/{issues.20260308_180927_477542428.jsonl.meta.json → issues.20260308_235202_180577215.jsonl.meta.json} +0 -0
  234. /package/.beads/.br_history/{issues.20260308_181032_020230108.jsonl.meta.json → issues.20260308_235202_387414163.jsonl.meta.json} +0 -0
  235. /package/.beads/.br_history/{issues.20260308_181032_180539413.jsonl.meta.json → issues.20260308_235202_564422794.jsonl.meta.json} +0 -0
  236. /package/.beads/.br_history/{issues.20260308_181032_372621506.jsonl.meta.json → issues.20260308_235202_742600597.jsonl.meta.json} +0 -0
  237. /package/.beads/.br_history/{issues.20260308_181032_565142225.jsonl.meta.json → issues.20260308_235208_133360069.jsonl.meta.json} +0 -0
  238. /package/.beads/.br_history/{issues.20260308_181311_336346464.jsonl.meta.json → issues.20260308_235505_473406307.jsonl.meta.json} +0 -0
  239. /package/.beads/.br_history/{issues.20260308_181444_039234498.jsonl.meta.json → issues.20260308_235505_662360489.jsonl.meta.json} +0 -0
  240. /package/.beads/.br_history/{issues.20260308_181503_794764403.jsonl.meta.json → issues.20260308_235505_843935624.jsonl.meta.json} +0 -0
  241. /package/.beads/.br_history/{issues.20260308_181503_950163105.jsonl.meta.json → issues.20260308_235506_044530221.jsonl.meta.json} +0 -0
  242. /package/.beads/.br_history/{issues.20260308_192031_852553505.jsonl.meta.json → issues.20260309_002618_115728731.jsonl.meta.json} +0 -0
  243. /package/.beads/.br_history/{issues.20260308_193552_846920518.jsonl.meta.json → issues.20260309_003748_878174586.jsonl.meta.json} +0 -0
  244. /package/.beads/.br_history/{issues.20260308_194054_394884833.jsonl.meta.json → issues.20260309_004057_868755623.jsonl.meta.json} +0 -0
  245. /package/.beads/.br_history/{issues.20260308_194209_440472460.jsonl.meta.json → issues.20260309_004058_512842163.jsonl.meta.json} +0 -0
  246. /package/.beads/.br_history/{issues.20260308_195319_099391899.jsonl.meta.json → issues.20260309_004058_994445226.jsonl.meta.json} +0 -0
  247. /package/.beads/.br_history/{issues.20260308_195324_176987204.jsonl.meta.json → issues.20260309_004059_475988596.jsonl.meta.json} +0 -0
  248. /package/.beads/.br_history/{issues.20260308_195436_929114019.jsonl.meta.json → issues.20260309_161902_566857851.jsonl.meta.json} +0 -0
  249. /package/.beads/.br_history/{issues.20260308_195437_055808298.jsonl.meta.json → issues.20260309_170512_277017739.jsonl.meta.json} +0 -0
  250. /package/.beads/.br_history/{issues.20260308_195437_304297399.jsonl.meta.json → issues.20260309_170512_477876921.jsonl.meta.json} +0 -0
  251. /package/.beads/.br_history/{issues.20260308_195437_556007332.jsonl.meta.json → issues.20260309_170512_664382701.jsonl.meta.json} +0 -0
  252. /package/.beads/.br_history/{issues.20260308_195444_987209695.jsonl.meta.json → issues.20260309_170512_859400333.jsonl.meta.json} +0 -0
  253. /package/.beads/.br_history/{issues.20260308_195445_133350193.jsonl.meta.json → issues.20260309_212326_082771164.jsonl.meta.json} +0 -0
  254. /package/.beads/.br_history/{issues.20260308_195445_400185615.jsonl.meta.json → issues.20260309_212326_245619716.jsonl.meta.json} +0 -0
  255. /package/.beads/.br_history/{issues.20260308_195445_689886334.jsonl.meta.json → issues.20260309_212326_403198317.jsonl.meta.json} +0 -0
  256. /package/.beads/.br_history/{issues.20260308_195445_949947727.jsonl.meta.json → issues.20260309_212332_539197678.jsonl.meta.json} +0 -0
  257. /package/.beads/.br_history/{issues.20260308_195745_580473297.jsonl.meta.json → issues.20260309_212332_731373599.jsonl.meta.json} +0 -0
  258. /package/.beads/.br_history/{issues.20260308_195745_725920532.jsonl.meta.json → issues.20260309_212332_928710953.jsonl.meta.json} +0 -0
  259. /package/.beads/.br_history/{issues.20260308_195745_968227911.jsonl.meta.json → issues.20260309_213021_341505240.jsonl.meta.json} +0 -0
  260. /package/.beads/.br_history/{issues.20260308_195746_224276322.jsonl.meta.json → issues.20260309_213022_023136934.jsonl.meta.json} +0 -0
  261. /package/.beads/.br_history/{issues.20260308_200018_386890807.jsonl.meta.json → issues.20260309_213022_400050719.jsonl.meta.json} +0 -0
@@ -0,0 +1,82 @@
1
+ const fs = require("fs")
2
+ const os = require("os")
3
+ const path = require("path")
4
+ const { execSync } = require("child_process")
5
+
6
+ const CLI = path.join(__dirname, "..", "cli", "supercli.js")
7
+
8
+ function runNoServer(args, options = {}) {
9
+ try {
10
+ const env = { ...process.env }
11
+ delete env.SUPERCLI_SERVER
12
+ const out = execSync(`node ${CLI} ${args}`, {
13
+ encoding: "utf-8",
14
+ timeout: 15000,
15
+ env: { ...env, ...(options.env || {}) }
16
+ })
17
+ return { ok: true, output: out.trim(), code: 0 }
18
+ } catch (err) {
19
+ return {
20
+ ok: false,
21
+ output: (err.stdout || "").trim(),
22
+ stderr: (err.stderr || "").trim(),
23
+ code: err.status
24
+ }
25
+ }
26
+ }
27
+
28
+ function writeFakeNextestBinary(dir) {
29
+ const bin = path.join(dir, "cargo-nextest")
30
+ fs.writeFileSync(bin, [
31
+ "#!/usr/bin/env node",
32
+ "const args = process.argv.slice(2);",
33
+ "if (args[0] === '--version') { console.log('cargo-nextest 0.9.129-test'); process.exit(0); }",
34
+ "console.log(JSON.stringify({ ok: true, args }));"
35
+ ].join("\n"), "utf-8")
36
+ fs.chmodSync(bin, 0o755)
37
+ return bin
38
+ }
39
+
40
+ describe("nextest plugin", () => {
41
+ const fakeDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcli-nextest-"))
42
+ const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "dcli-home-nextest-"))
43
+ writeFakeNextestBinary(fakeDir)
44
+ const env = { ...process.env, PATH: `${fakeDir}:${process.env.PATH || ""}`, SUPERCLI_HOME: tempHome }
45
+
46
+ beforeAll(() => {
47
+ runNoServer("plugins install ./plugins/nextest --on-conflict replace --json", { env })
48
+ })
49
+
50
+ afterAll(() => {
51
+ runNoServer("plugins remove nextest --json", { env })
52
+ fs.rmSync(fakeDir, { recursive: true, force: true })
53
+ fs.rmSync(tempHome, { recursive: true, force: true })
54
+ })
55
+
56
+ test("routes cli version wrapped command", () => {
57
+ const r = runNoServer("nextest cli version --json", { env })
58
+ expect(r.ok).toBe(true)
59
+ const data = JSON.parse(r.output)
60
+ expect(data.command).toBe("nextest.cli.version")
61
+ expect(data.data.raw).toBe("cargo-nextest 0.9.129-test")
62
+ })
63
+
64
+ test("supports namespace passthrough", () => {
65
+ const r = runNoServer("nextest list --message-format json --json", { env })
66
+ expect(r.ok).toBe(true)
67
+ const data = JSON.parse(r.output)
68
+ expect(data.command).toBe("nextest.passthrough")
69
+ expect(data.data.args).toContain("list")
70
+ expect(data.data.args).toContain("--message-format")
71
+ expect(data.data.args).toContain("json")
72
+ expect(data.data.args).toContain("--json")
73
+ })
74
+
75
+ test("doctor reports cargo-nextest dependency as healthy", () => {
76
+ const r = runNoServer("plugins doctor nextest --json", { env })
77
+ expect(r.ok).toBe(true)
78
+ const data = JSON.parse(r.output)
79
+ expect(data.ok).toBe(true)
80
+ expect(data.checks.some(c => c.type === "binary" && c.binary === "cargo-nextest" && c.ok === true)).toBe(true)
81
+ })
82
+ })
@@ -0,0 +1,81 @@
1
+ const fs = require("fs")
2
+ const os = require("os")
3
+ const path = require("path")
4
+ const { execSync } = require("child_process")
5
+
6
+ const CLI = path.join(__dirname, "..", "cli", "supercli.js")
7
+
8
+ function runNoServer(args, options = {}) {
9
+ try {
10
+ const env = { ...process.env }
11
+ delete env.SUPERCLI_SERVER
12
+ const out = execSync(`node ${CLI} ${args}`, {
13
+ encoding: "utf-8",
14
+ timeout: 15000,
15
+ env: { ...env, ...(options.env || {}) }
16
+ })
17
+ return { ok: true, output: out.trim(), code: 0 }
18
+ } catch (err) {
19
+ return {
20
+ ok: false,
21
+ output: (err.stdout || "").trim(),
22
+ stderr: (err.stderr || "").trim(),
23
+ code: err.status
24
+ }
25
+ }
26
+ }
27
+
28
+ function writeFakeNpmBinary(dir) {
29
+ const bin = path.join(dir, "npm")
30
+ fs.writeFileSync(bin, [
31
+ "#!/usr/bin/env node",
32
+ "const args = process.argv.slice(2);",
33
+ "if (args[0] === '--version') { console.log('10.9.5-test'); process.exit(0); }",
34
+ "console.log(JSON.stringify({ ok: true, args }));"
35
+ ].join("\n"), "utf-8")
36
+ fs.chmodSync(bin, 0o755)
37
+ return bin
38
+ }
39
+
40
+ describe("npm plugin", () => {
41
+ const fakeDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcli-npm-"))
42
+ const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "dcli-home-npm-"))
43
+ writeFakeNpmBinary(fakeDir)
44
+ const env = { ...process.env, PATH: `${fakeDir}:${process.env.PATH || ""}`, SUPERCLI_HOME: tempHome }
45
+
46
+ beforeAll(() => {
47
+ runNoServer("plugins install ./plugins/npm --on-conflict replace --json", { env })
48
+ })
49
+
50
+ afterAll(() => {
51
+ runNoServer("plugins remove npm --json", { env })
52
+ fs.rmSync(fakeDir, { recursive: true, force: true })
53
+ fs.rmSync(tempHome, { recursive: true, force: true })
54
+ })
55
+
56
+ test("routes cli version wrapped command", () => {
57
+ const r = runNoServer("npm cli version --json", { env })
58
+ expect(r.ok).toBe(true)
59
+ const data = JSON.parse(r.output)
60
+ expect(data.command).toBe("npm.cli.version")
61
+ expect(data.data.raw).toBe("10.9.5-test")
62
+ })
63
+
64
+ test("supports namespace passthrough", () => {
65
+ const r = runNoServer("npm search react --json", { env })
66
+ expect(r.ok).toBe(true)
67
+ const data = JSON.parse(r.output)
68
+ expect(data.command).toBe("npm.passthrough")
69
+ expect(data.data.args[0]).toBe("search")
70
+ expect(data.data.args).toContain("react")
71
+ expect(data.data.args).toContain("--json")
72
+ })
73
+
74
+ test("doctor reports npm dependency as healthy", () => {
75
+ const r = runNoServer("plugins doctor npm --json", { env })
76
+ expect(r.ok).toBe(true)
77
+ const data = JSON.parse(r.output)
78
+ expect(data.ok).toBe(true)
79
+ expect(data.checks.some(c => c.type === "binary" && c.binary === "npm" && c.ok === true)).toBe(true)
80
+ })
81
+ })
@@ -0,0 +1,157 @@
1
+ const fs = require("fs")
2
+ const os = require("os")
3
+ const path = require("path")
4
+ const { execSync } = require("child_process")
5
+
6
+ const CLI = path.join(__dirname, "..", "cli", "supercli.js")
7
+
8
+ function runNoServer(args, options = {}) {
9
+ try {
10
+ const env = { ...process.env }
11
+ delete env.SUPERCLI_SERVER
12
+ const out = execSync(`node ${CLI} ${args}`, {
13
+ encoding: "utf-8",
14
+ timeout: 15000,
15
+ env: { ...env, ...(options.env || {}) }
16
+ })
17
+ return { ok: true, output: out.trim(), code: 0 }
18
+ } catch (err) {
19
+ return {
20
+ ok: false,
21
+ output: (err.stdout || "").trim(),
22
+ stderr: (err.stderr || "").trim(),
23
+ code: err.status
24
+ }
25
+ }
26
+ }
27
+
28
+ function writeFakeCurlBinary(dir) {
29
+ const bin = path.join(dir, "curl")
30
+ fs.writeFileSync(bin, [
31
+ "#!/usr/bin/env node",
32
+ "const args = process.argv.slice(2);",
33
+ "const url = args[args.length - 1] || '';",
34
+ "if (args.includes('--version')) { console.log('curl 8.0.0-test'); process.exit(0); }",
35
+ "if (url.includes('/git/trees/main?recursive=1')) {",
36
+ " console.log(JSON.stringify({ tree: [",
37
+ " { type: 'blob', path: 'README.md' },",
38
+ " { type: 'blob', path: 'AGENTS.md' },",
39
+ " { type: 'blob', path: 'CLAUDE.md' },",
40
+ " { type: 'blob', path: 'CONTRIBUTING.md' },",
41
+ " { type: 'blob', path: 'docs/en/README.md' },",
42
+ " { type: 'blob', path: 'docs/en/installation.md' },",
43
+ " { type: 'blob', path: 'docs/en/configuration.md' },",
44
+ " { type: 'blob', path: 'docs/en/commands.md' },",
45
+ " { type: 'blob', path: 'docs/en/usage.md' },",
46
+ " { type: 'blob', path: 'docs/en/architecture.md' },",
47
+ " { type: 'blob', path: 'docs/en/security.md' },",
48
+ " { type: 'blob', path: 'docs/en/gateway-api.md' },",
49
+ " { type: 'blob', path: 'docs/en/development.md' }",
50
+ " ] }));",
51
+ " process.exit(0);",
52
+ "}",
53
+ "if (url.includes('raw.githubusercontent.com/nullclaw/nullclaw/main/')) {",
54
+ " const docPath = url.split('/main/')[1] || 'README.md';",
55
+ " console.log(`# ${docPath}\\n\\nTest markdown for ${docPath}.`);",
56
+ " process.exit(0);",
57
+ "}",
58
+ "console.error(`unsupported url: ${url}`);",
59
+ "process.exit(22);"
60
+ ].join("\n"), "utf-8")
61
+ fs.chmodSync(bin, 0o755)
62
+ return bin
63
+ }
64
+
65
+ function writeFakeNullclawBinary(dir) {
66
+ const bin = path.join(dir, "nullclaw")
67
+ fs.writeFileSync(bin, [
68
+ "#!/usr/bin/env node",
69
+ "const args = process.argv.slice(2);",
70
+ "if (args[0] === '--version' || args[0] === 'version') { console.log('nullclaw v2026.3.8-test'); process.exit(0); }",
71
+ "if (args[0] === 'status') { console.log('NullClaw status: healthy'); process.exit(0); }",
72
+ "console.log(JSON.stringify({ ok: true, args }));"
73
+ ].join("\n"), "utf-8")
74
+ fs.chmodSync(bin, 0o755)
75
+ return bin
76
+ }
77
+
78
+ describe("nullclaw hybrid plugin", () => {
79
+ const fakeDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcli-nullclaw-"))
80
+ const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "dcli-home-nullclaw-"))
81
+ writeFakeCurlBinary(fakeDir)
82
+ writeFakeNullclawBinary(fakeDir)
83
+ const env = { ...process.env, PATH: `${fakeDir}:${process.env.PATH || ""}`, SUPERCLI_HOME: tempHome }
84
+ let removed = false
85
+
86
+ beforeAll(() => {
87
+ const install = runNoServer("plugins install ./plugins/nullclaw --on-conflict replace --json", { env })
88
+ expect(install.ok).toBe(true)
89
+ })
90
+
91
+ afterAll(() => {
92
+ if (!removed) runNoServer("plugins remove nullclaw --json", { env })
93
+ fs.rmSync(fakeDir, { recursive: true, force: true })
94
+ fs.rmSync(tempHome, { recursive: true, force: true })
95
+ })
96
+
97
+ test("indexes nullclaw skills provider", () => {
98
+ const provider = runNoServer("skills providers show --name nullclaw --json", { env })
99
+ expect(provider.ok).toBe(true)
100
+ const providerData = JSON.parse(provider.output)
101
+ expect(providerData.provider.name).toBe("nullclaw")
102
+
103
+ const list = runNoServer("skills list --catalog --provider nullclaw --json", { env })
104
+ expect(list.ok).toBe(true)
105
+ const listData = JSON.parse(list.output)
106
+ expect(listData.skills.some(skill => skill.id === "nullclaw:root.agents")).toBe(true)
107
+ })
108
+
109
+ test("routes wrapped version command", () => {
110
+ const r = runNoServer("nullclaw cli version --json", { env })
111
+ expect(r.ok).toBe(true)
112
+ const data = JSON.parse(r.output)
113
+ expect(data.command).toBe("nullclaw.cli.version")
114
+ expect(data.data.raw).toBe("nullclaw v2026.3.8-test")
115
+ })
116
+
117
+ test("routes wrapped system status command", () => {
118
+ const r = runNoServer("nullclaw system status --json", { env })
119
+ expect(r.ok).toBe(true)
120
+ const data = JSON.parse(r.output)
121
+ expect(data.command).toBe("nullclaw.system.status")
122
+ expect(data.data.raw).toBe("NullClaw status: healthy")
123
+ })
124
+
125
+ test("supports nullclaw namespace passthrough", () => {
126
+ const r = runNoServer("nullclaw models list --json", { env })
127
+ expect(r.ok).toBe(true)
128
+ const data = JSON.parse(r.output)
129
+ expect(data.command).toBe("nullclaw.passthrough")
130
+ expect(data.data.args[0]).toBe("models")
131
+ expect(data.data.args[1]).toBe("list")
132
+ expect(data.data.args).toContain("--json")
133
+ })
134
+
135
+ test("doctor reports curl and nullclaw dependencies as healthy", () => {
136
+ const r = runNoServer("plugins doctor nullclaw --json", { env })
137
+ expect(r.ok).toBe(true)
138
+ const data = JSON.parse(r.output)
139
+ expect(data.ok).toBe(true)
140
+ expect(data.checks.some(c => c.type === "binary" && c.binary === "curl" && c.ok === true)).toBe(true)
141
+ expect(data.checks.some(c => c.type === "binary" && c.binary === "nullclaw" && c.ok === true)).toBe(true)
142
+ })
143
+
144
+ test("removal cleans up the skills provider", () => {
145
+ const remove = runNoServer("plugins remove nullclaw --json", { env })
146
+ expect(remove.ok).toBe(true)
147
+ removed = true
148
+
149
+ const provider = runNoServer("skills providers show --name nullclaw --json", { env })
150
+ expect(provider.ok).toBe(false)
151
+
152
+ const list = runNoServer("skills list --catalog --provider nullclaw --json", { env })
153
+ expect(list.ok).toBe(true)
154
+ const listData = JSON.parse(list.output)
155
+ expect(listData.skills).toEqual([])
156
+ })
157
+ })
@@ -0,0 +1,199 @@
1
+ describe("openapi adapter", () => {
2
+ const mockSpec = {
3
+ servers: [{ url: "https://api.test/v1" }],
4
+ paths: {
5
+ "/users/{id}": {
6
+ get: {
7
+ operationId: "getUser",
8
+ parameters: [
9
+ { name: "id", in: "path" },
10
+ { name: "q", in: "query" }
11
+ ]
12
+ }
13
+ },
14
+ "/users": {
15
+ post: {
16
+ operationId: "createUser"
17
+ }
18
+ }
19
+ }
20
+ }
21
+
22
+ let execute;
23
+
24
+ beforeEach(() => {
25
+ jest.clearAllMocks()
26
+ jest.resetModules()
27
+ global.fetch = jest.fn()
28
+ execute = require("../cli/adapters/openapi").execute
29
+ })
30
+
31
+ test("throws if spec or operationId missing", async () => {
32
+ await expect(execute({ adapterConfig: {} }, {}, {})).rejects.toThrow(/requires 'spec' and 'operationId'/)
33
+ })
34
+
35
+ test("successfully executes GET operation with path and query params", async () => {
36
+ global.fetch
37
+ .mockResolvedValueOnce({
38
+ ok: true,
39
+ json: () => Promise.resolve(mockSpec)
40
+ })
41
+ .mockResolvedValueOnce({
42
+ ok: true,
43
+ headers: { get: () => "application/json" },
44
+ json: () => Promise.resolve({ user: "alice" })
45
+ })
46
+
47
+ const context = {
48
+ config: {
49
+ specs: [{ name: "my-spec", url: "http://specs/my.json" }]
50
+ }
51
+ }
52
+
53
+ const result = await execute({
54
+ adapterConfig: { spec: "my-spec", operationId: "getUser" }
55
+ }, { id: "123", q: "query-val" }, context)
56
+
57
+ expect(global.fetch).toHaveBeenCalledWith("http://specs/my.json")
58
+ expect(global.fetch).toHaveBeenCalledWith(
59
+ "https://api.test/v1/users/123?q=query-val",
60
+ expect.objectContaining({ method: "GET" })
61
+ )
62
+ expect(result).toEqual({ user: "alice" })
63
+ })
64
+
65
+ test("successfully executes POST operation with body", async () => {
66
+ global.fetch
67
+ .mockResolvedValueOnce({
68
+ ok: true,
69
+ json: () => Promise.resolve(mockSpec)
70
+ })
71
+ .mockResolvedValueOnce({
72
+ ok: true,
73
+ headers: { get: () => "text/plain" },
74
+ text: () => Promise.resolve("done")
75
+ })
76
+
77
+ const context = {
78
+ config: {
79
+ specs: [{ name: "my-spec", url: "u" }]
80
+ }
81
+ }
82
+
83
+ const result = await execute({
84
+ adapterConfig: { spec: "my-spec", operationId: "createUser" }
85
+ }, { name: "bob" }, context)
86
+
87
+ expect(global.fetch).toHaveBeenCalledWith(
88
+ "https://api.test/v1/users",
89
+ expect.objectContaining({
90
+ method: "POST",
91
+ body: JSON.stringify({ name: "bob" })
92
+ })
93
+ )
94
+ expect(result).toEqual({ raw: "done" })
95
+ })
96
+
97
+ test("resolves spec from remote server if not in local config", async () => {
98
+ global.fetch
99
+ .mockResolvedValueOnce({
100
+ ok: true,
101
+ json: () => Promise.resolve([{ name: "remote-spec", url: "http://remote/spec.json" }])
102
+ })
103
+ .mockResolvedValueOnce({
104
+ ok: true,
105
+ json: () => Promise.resolve(mockSpec)
106
+ })
107
+ .mockResolvedValueOnce({
108
+ ok: true,
109
+ headers: { get: () => "json" },
110
+ json: () => Promise.resolve({ ok: true })
111
+ })
112
+
113
+ const context = { server: "http://api.test", config: { specs: [] } }
114
+
115
+ await execute({
116
+ adapterConfig: { spec: "remote-spec", operationId: "getUser" }
117
+ }, { id: "1" }, context)
118
+
119
+ expect(global.fetch).toHaveBeenCalledWith("http://api.test/api/specs?format=json")
120
+ expect(global.fetch).toHaveBeenCalledWith("http://remote/spec.json")
121
+ })
122
+
123
+ test("throws if spec not found and no server", async () => {
124
+ const context = { config: { specs: [] } }
125
+ await expect(execute({
126
+ adapterConfig: { spec: "missing", operationId: "op" }
127
+ }, {}, context)).rejects.toThrow(/not found in local config/)
128
+ })
129
+
130
+ test("throws if remote spec list fetch fails", async () => {
131
+ global.fetch.mockResolvedValue({ ok: false, status: 500 })
132
+ const context = { server: "http://api.test", config: { specs: [] } }
133
+ await expect(execute({
134
+ adapterConfig: { spec: "any", operationId: "op" }
135
+ }, {}, context)).rejects.toThrow(/Failed to fetch specs list: 500/)
136
+ })
137
+
138
+ test("throws if remote spec not found in list", async () => {
139
+ global.fetch.mockResolvedValue({
140
+ ok: true,
141
+ json: () => Promise.resolve([])
142
+ })
143
+ const context = { server: "http://api.test", config: { specs: [] } }
144
+ await expect(execute({
145
+ adapterConfig: { spec: "missing", operationId: "op" }
146
+ }, {}, context)).rejects.toThrow(/OpenAPI spec 'missing' not found/)
147
+ })
148
+
149
+ test("throws if actual spec fetch fails", async () => {
150
+ global.fetch
151
+ .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve([{ name: "s", url: "u" }]) })
152
+ .mockResolvedValueOnce({ ok: false, status: 404 })
153
+ const context = { server: "http://api.test", config: { specs: [] } }
154
+ await expect(execute({
155
+ adapterConfig: { spec: "s", operationId: "op" }
156
+ }, {}, context)).rejects.toThrow(/Failed to fetch OpenAPI spec from u: 404/)
157
+ })
158
+
159
+ test("throws if operationId not found in spec", async () => {
160
+ global.fetch.mockResolvedValue({ ok: true, json: () => Promise.resolve(mockSpec) })
161
+ const context = { config: { specs: [{ name: "s", url: "u" }] } }
162
+ await expect(execute({
163
+ adapterConfig: { spec: "s", operationId: "missingOp" }
164
+ }, {}, context)).rejects.toThrow(/Operation 'missingOp' not found in spec/)
165
+ })
166
+
167
+ test("handles API call failure (500)", async () => {
168
+ global.fetch
169
+ .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockSpec) })
170
+ .mockResolvedValueOnce({
171
+ ok: false,
172
+ status: 500,
173
+ statusText: "Err",
174
+ text: () => Promise.resolve("body"),
175
+ headers: { get: () => "text/plain" }
176
+ })
177
+
178
+ const context = { config: { specs: [{ name: "s", url: "u" }] } }
179
+ await expect(execute({
180
+ adapterConfig: { spec: "s", operationId: "getUser" }
181
+ }, { id: "1" }, context)).rejects.toMatchObject({ code: 105 })
182
+ })
183
+
184
+ test("uses spec cache", async () => {
185
+ global.fetch
186
+ .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockSpec) })
187
+ .mockResolvedValue({ ok: true, headers: { get: () => "json" }, json: () => Promise.resolve({}) })
188
+
189
+ const context = { config: { specs: [{ name: "cached", url: "u" }] } }
190
+
191
+ // First call: spec fetch + API call
192
+ await execute({ adapterConfig: { spec: "cached", operationId: "getUser" } }, { id: "1" }, context)
193
+ // Second call: only API call (spec is cached)
194
+ await execute({ adapterConfig: { spec: "cached", operationId: "getUser" } }, { id: "2" }, context)
195
+
196
+ expect(global.fetch).toHaveBeenCalledWith("u")
197
+ expect(global.fetch).toHaveBeenCalledTimes(3) // 1 spec fetch + 2 API calls
198
+ })
199
+ })
@@ -1,9 +1,9 @@
1
1
  const { spawnSync } = require("child_process")
2
2
  const { addProvider, syncCatalog } = require("../cli/skills-catalog")
3
3
  const {
4
- installAgencyAgentsSkillProvider,
4
+ run,
5
5
  buildRemoteEntriesFromTree
6
- } = require("../cli/plugin-agency-agents")
6
+ } = require("../plugins/agency-agents/scripts/post-install")
7
7
 
8
8
  jest.mock("child_process")
9
9
  jest.mock("../cli/skills-catalog")
@@ -28,7 +28,7 @@ describe("plugin-agency-agents", () => {
28
28
  expect(entries[1].id).toBe("marketing.marketing-growth-hacker")
29
29
  })
30
30
 
31
- test("installAgencyAgentsSkillProvider stores provider and syncs catalog", () => {
31
+ test("run stores provider and syncs catalog", () => {
32
32
  spawnSync.mockReturnValue({
33
33
  status: 0,
34
34
  stdout: JSON.stringify({
@@ -40,7 +40,7 @@ describe("plugin-agency-agents", () => {
40
40
  })
41
41
  syncCatalog.mockReturnValue({ skills: [1, 2, 3] })
42
42
 
43
- const result = installAgencyAgentsSkillProvider()
43
+ const result = run()
44
44
 
45
45
  expect(addProvider).toHaveBeenCalledWith(expect.objectContaining({
46
46
  name: "agency-agents",
@@ -55,8 +55,8 @@ describe("plugin-agency-agents", () => {
55
55
  })
56
56
  })
57
57
 
58
- test("installAgencyAgentsSkillProvider throws on curl failure", () => {
58
+ test("run throws on curl failure", () => {
59
59
  spawnSync.mockReturnValue({ status: 22, stderr: "404" })
60
- expect(() => installAgencyAgentsSkillProvider()).toThrow(/Failed to fetch agency-agents metadata/)
60
+ expect(() => run()).toThrow(/Failed to fetch agency-agents metadata/)
61
61
  })
62
62
  })
@@ -0,0 +1,78 @@
1
+ const { spawnSync } = require("child_process")
2
+ const { addProvider, removeProvider, syncCatalog } = require("../cli/skills-catalog")
3
+ const {
4
+ CATALOG_FILES,
5
+ run,
6
+ buildRemoteEntriesFromTree
7
+ } = require("../plugins/nullclaw/scripts/post-install")
8
+ const { run: runUninstall } = require("../plugins/nullclaw/scripts/post-uninstall")
9
+
10
+ jest.mock("child_process")
11
+ jest.mock("../cli/skills-catalog")
12
+
13
+ describe("plugin-nullclaw", () => {
14
+ beforeEach(() => {
15
+ jest.clearAllMocks()
16
+ })
17
+
18
+ test("buildRemoteEntriesFromTree keeps only curated nullclaw docs", () => {
19
+ const entries = buildRemoteEntriesFromTree({
20
+ tree: [
21
+ { type: "blob", path: "README.md" },
22
+ { type: "blob", path: "AGENTS.md" },
23
+ { type: "blob", path: "docs/en/commands.md" },
24
+ { type: "blob", path: "docs/zh/commands.md" },
25
+ { type: "blob", path: "spec/webchannel_v1.json" }
26
+ ]
27
+ })
28
+
29
+ expect(entries.map(entry => entry.id)).toEqual([
30
+ "root.readme",
31
+ "root.agents",
32
+ "docs.en.commands"
33
+ ])
34
+ expect(entries.every(entry => entry.source_url.includes("raw.githubusercontent.com/nullclaw/nullclaw/main/"))).toBe(true)
35
+ })
36
+
37
+ test("run stores provider and syncs catalog", () => {
38
+ spawnSync.mockReturnValue({
39
+ status: 0,
40
+ stdout: JSON.stringify({
41
+ tree: CATALOG_FILES.map(file => ({ type: "blob", path: file.path }))
42
+ })
43
+ })
44
+ syncCatalog.mockReturnValue({ skills: [1, 2, 3, 4] })
45
+
46
+ const result = run()
47
+
48
+ expect(addProvider).toHaveBeenCalledWith(expect.objectContaining({
49
+ name: "nullclaw",
50
+ type: "remote_static",
51
+ entries: expect.arrayContaining([
52
+ expect.objectContaining({ id: "root.agents" }),
53
+ expect.objectContaining({ id: "docs.en.architecture" })
54
+ ])
55
+ }))
56
+ expect(syncCatalog).toHaveBeenCalled()
57
+ expect(result).toEqual({
58
+ provider: "nullclaw",
59
+ entries: CATALOG_FILES.length,
60
+ synced_skills: 4
61
+ })
62
+ })
63
+
64
+ test("run throws on curl failure", () => {
65
+ spawnSync.mockReturnValue({ status: 22, stderr: "404" })
66
+ expect(() => run()).toThrow(/Failed to fetch nullclaw metadata/)
67
+ })
68
+
69
+ test("post-uninstall removes provider and syncs catalog", () => {
70
+ removeProvider.mockReturnValue(true)
71
+ syncCatalog.mockReturnValue({ skills: [1] })
72
+ const result = runUninstall()
73
+ expect(removeProvider).toHaveBeenCalledWith("nullclaw")
74
+ expect(addProvider).not.toHaveBeenCalled()
75
+ expect(syncCatalog).toHaveBeenCalled()
76
+ expect(result).toEqual({ provider: "nullclaw", removed: true, synced_skills: 1 })
77
+ })
78
+ })