sandstream-kit 1.0.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 (519) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +617 -0
  3. package/dist/adapters/api-key-adapter.d.ts +35 -0
  4. package/dist/adapters/api-key-adapter.js +46 -0
  5. package/dist/adapters/api-key-adapter.js.map +1 -0
  6. package/dist/adapters/clerk-auth.d.ts +6 -0
  7. package/dist/adapters/clerk-auth.js +20 -0
  8. package/dist/adapters/clerk-auth.js.map +1 -0
  9. package/dist/adapters/cloudflare-r2.d.ts +6 -0
  10. package/dist/adapters/cloudflare-r2.js +136 -0
  11. package/dist/adapters/cloudflare-r2.js.map +1 -0
  12. package/dist/adapters/expo-eas.d.ts +6 -0
  13. package/dist/adapters/expo-eas.js +129 -0
  14. package/dist/adapters/expo-eas.js.map +1 -0
  15. package/dist/adapters/flagsmith-flags.d.ts +5 -0
  16. package/dist/adapters/flagsmith-flags.js +20 -0
  17. package/dist/adapters/flagsmith-flags.js.map +1 -0
  18. package/dist/adapters/flyio-hosting.d.ts +2 -0
  19. package/dist/adapters/flyio-hosting.js +143 -0
  20. package/dist/adapters/flyio-hosting.js.map +1 -0
  21. package/dist/adapters/index.d.ts +6 -0
  22. package/dist/adapters/index.js +48 -0
  23. package/dist/adapters/index.js.map +1 -0
  24. package/dist/adapters/inngest-background.d.ts +5 -0
  25. package/dist/adapters/inngest-background.js +19 -0
  26. package/dist/adapters/inngest-background.js.map +1 -0
  27. package/dist/adapters/liveblocks-realtime.d.ts +11 -0
  28. package/dist/adapters/liveblocks-realtime.js +62 -0
  29. package/dist/adapters/liveblocks-realtime.js.map +1 -0
  30. package/dist/adapters/loops-email.d.ts +6 -0
  31. package/dist/adapters/loops-email.js +18 -0
  32. package/dist/adapters/loops-email.js.map +1 -0
  33. package/dist/adapters/neon-db.d.ts +10 -0
  34. package/dist/adapters/neon-db.js +94 -0
  35. package/dist/adapters/neon-db.js.map +1 -0
  36. package/dist/adapters/planetscale-db.d.ts +11 -0
  37. package/dist/adapters/planetscale-db.js +134 -0
  38. package/dist/adapters/planetscale-db.js.map +1 -0
  39. package/dist/adapters/posthog-analytics.d.ts +6 -0
  40. package/dist/adapters/posthog-analytics.js +22 -0
  41. package/dist/adapters/posthog-analytics.js.map +1 -0
  42. package/dist/adapters/railway-hosting.d.ts +2 -0
  43. package/dist/adapters/railway-hosting.js +136 -0
  44. package/dist/adapters/railway-hosting.js.map +1 -0
  45. package/dist/adapters/resend-email.d.ts +35 -0
  46. package/dist/adapters/resend-email.js +109 -0
  47. package/dist/adapters/resend-email.js.map +1 -0
  48. package/dist/adapters/searxng-instance.d.ts +6 -0
  49. package/dist/adapters/searxng-instance.js +240 -0
  50. package/dist/adapters/searxng-instance.js.map +1 -0
  51. package/dist/adapters/sentry-monitoring.d.ts +7 -0
  52. package/dist/adapters/sentry-monitoring.js +27 -0
  53. package/dist/adapters/sentry-monitoring.js.map +1 -0
  54. package/dist/adapters/stripe-payments.d.ts +6 -0
  55. package/dist/adapters/stripe-payments.js +134 -0
  56. package/dist/adapters/stripe-payments.js.map +1 -0
  57. package/dist/adapters/supabase-db.d.ts +6 -0
  58. package/dist/adapters/supabase-db.js +130 -0
  59. package/dist/adapters/supabase-db.js.map +1 -0
  60. package/dist/adapters/tinybird-analytics.d.ts +5 -0
  61. package/dist/adapters/tinybird-analytics.js +20 -0
  62. package/dist/adapters/tinybird-analytics.js.map +1 -0
  63. package/dist/adapters/trigger-background.d.ts +6 -0
  64. package/dist/adapters/trigger-background.js +20 -0
  65. package/dist/adapters/trigger-background.js.map +1 -0
  66. package/dist/adapters/types.d.ts +7 -0
  67. package/dist/adapters/types.js +2 -0
  68. package/dist/adapters/types.js.map +1 -0
  69. package/dist/adapters/upstash-redis.d.ts +6 -0
  70. package/dist/adapters/upstash-redis.js +88 -0
  71. package/dist/adapters/upstash-redis.js.map +1 -0
  72. package/dist/adapters/vercel-hosting.d.ts +6 -0
  73. package/dist/adapters/vercel-hosting.js +112 -0
  74. package/dist/adapters/vercel-hosting.js.map +1 -0
  75. package/dist/agent-adapter-model.d.ts +108 -0
  76. package/dist/agent-adapter-model.js +6 -0
  77. package/dist/agent-adapter-model.js.map +1 -0
  78. package/dist/agent-adapter-service.d.ts +67 -0
  79. package/dist/agent-adapter-service.js +299 -0
  80. package/dist/agent-adapter-service.js.map +1 -0
  81. package/dist/agent-config.d.ts +56 -0
  82. package/dist/agent-config.js +129 -0
  83. package/dist/agent-config.js.map +1 -0
  84. package/dist/agent-governance-model.d.ts +128 -0
  85. package/dist/agent-governance-model.js +6 -0
  86. package/dist/agent-governance-model.js.map +1 -0
  87. package/dist/agent-governance-service.d.ts +101 -0
  88. package/dist/agent-governance-service.js +319 -0
  89. package/dist/agent-governance-service.js.map +1 -0
  90. package/dist/alert-rules-engine.d.ts +102 -0
  91. package/dist/alert-rules-engine.js +210 -0
  92. package/dist/alert-rules-engine.js.map +1 -0
  93. package/dist/analytics-service.d.ts +126 -0
  94. package/dist/analytics-service.js +318 -0
  95. package/dist/analytics-service.js.map +1 -0
  96. package/dist/analyze.d.ts +19 -0
  97. package/dist/analyze.js +311 -0
  98. package/dist/analyze.js.map +1 -0
  99. package/dist/apm-instrumentor.d.ts +119 -0
  100. package/dist/apm-instrumentor.js +225 -0
  101. package/dist/apm-instrumentor.js.map +1 -0
  102. package/dist/approval-model.d.ts +82 -0
  103. package/dist/approval-model.js +6 -0
  104. package/dist/approval-model.js.map +1 -0
  105. package/dist/approval-service.d.ts +39 -0
  106. package/dist/approval-service.js +236 -0
  107. package/dist/approval-service.js.map +1 -0
  108. package/dist/approval.d.ts +22 -0
  109. package/dist/approval.js +148 -0
  110. package/dist/approval.js.map +1 -0
  111. package/dist/audit-logging-model.d.ts +157 -0
  112. package/dist/audit-logging-model.js +6 -0
  113. package/dist/audit-logging-model.js.map +1 -0
  114. package/dist/audit-logging-service.d.ts +89 -0
  115. package/dist/audit-logging-service.js +367 -0
  116. package/dist/audit-logging-service.js.map +1 -0
  117. package/dist/audit-secrets.d.ts +42 -0
  118. package/dist/audit-secrets.js +126 -0
  119. package/dist/audit-secrets.js.map +1 -0
  120. package/dist/audit.d.ts +43 -0
  121. package/dist/audit.js +286 -0
  122. package/dist/audit.js.map +1 -0
  123. package/dist/author-dashboard.d.ts +84 -0
  124. package/dist/author-dashboard.js +204 -0
  125. package/dist/author-dashboard.js.map +1 -0
  126. package/dist/author-notifications.d.ts +130 -0
  127. package/dist/author-notifications.js +261 -0
  128. package/dist/author-notifications.js.map +1 -0
  129. package/dist/author-verification.d.ts +79 -0
  130. package/dist/author-verification.js +257 -0
  131. package/dist/author-verification.js.map +1 -0
  132. package/dist/autonomous-setup-model.d.ts +117 -0
  133. package/dist/autonomous-setup-model.js +6 -0
  134. package/dist/autonomous-setup-model.js.map +1 -0
  135. package/dist/autonomous-setup-service.d.ts +74 -0
  136. package/dist/autonomous-setup-service.js +325 -0
  137. package/dist/autonomous-setup-service.js.map +1 -0
  138. package/dist/badge-system.d.ts +70 -0
  139. package/dist/badge-system.js +210 -0
  140. package/dist/badge-system.js.map +1 -0
  141. package/dist/baseline.d.ts +34 -0
  142. package/dist/baseline.js +78 -0
  143. package/dist/baseline.js.map +1 -0
  144. package/dist/beta-program-service.d.ts +112 -0
  145. package/dist/beta-program-service.js +240 -0
  146. package/dist/beta-program-service.js.map +1 -0
  147. package/dist/budget.d.ts +34 -0
  148. package/dist/budget.js +159 -0
  149. package/dist/budget.js.map +1 -0
  150. package/dist/bumblebee.d.ts +143 -0
  151. package/dist/bumblebee.js +384 -0
  152. package/dist/bumblebee.js.map +1 -0
  153. package/dist/cache-manager.d.ts +97 -0
  154. package/dist/cache-manager.js +244 -0
  155. package/dist/cache-manager.js.map +1 -0
  156. package/dist/cdn-adapter.d.ts +64 -0
  157. package/dist/cdn-adapter.js +263 -0
  158. package/dist/cdn-adapter.js.map +1 -0
  159. package/dist/certification-workflow-model.d.ts +95 -0
  160. package/dist/certification-workflow-model.js +6 -0
  161. package/dist/certification-workflow-model.js.map +1 -0
  162. package/dist/certification-workflow-service.d.ts +72 -0
  163. package/dist/certification-workflow-service.js +305 -0
  164. package/dist/certification-workflow-service.js.map +1 -0
  165. package/dist/check-design.d.ts +38 -0
  166. package/dist/check-design.js +256 -0
  167. package/dist/check-design.js.map +1 -0
  168. package/dist/check-gitignore.d.ts +39 -0
  169. package/dist/check-gitignore.js +156 -0
  170. package/dist/check-gitignore.js.map +1 -0
  171. package/dist/check-hooks.d.ts +15 -0
  172. package/dist/check-hooks.js +72 -0
  173. package/dist/check-hooks.js.map +1 -0
  174. package/dist/check-lock.d.ts +16 -0
  175. package/dist/check-lock.js +94 -0
  176. package/dist/check-lock.js.map +1 -0
  177. package/dist/check-secrets.d.ts +11 -0
  178. package/dist/check-secrets.js +320 -0
  179. package/dist/check-secrets.js.map +1 -0
  180. package/dist/check-security.d.ts +13 -0
  181. package/dist/check-security.js +887 -0
  182. package/dist/check-security.js.map +1 -0
  183. package/dist/check-services.d.ts +10 -0
  184. package/dist/check-services.js +44 -0
  185. package/dist/check-services.js.map +1 -0
  186. package/dist/check-skills.d.ts +8 -0
  187. package/dist/check-skills.js +26 -0
  188. package/dist/check-skills.js.map +1 -0
  189. package/dist/check-tests.d.ts +43 -0
  190. package/dist/check-tests.js +175 -0
  191. package/dist/check-tests.js.map +1 -0
  192. package/dist/check-tools.d.ts +8 -0
  193. package/dist/check-tools.js +42 -0
  194. package/dist/check-tools.js.map +1 -0
  195. package/dist/check-web-search.d.ts +12 -0
  196. package/dist/check-web-search.js +168 -0
  197. package/dist/check-web-search.js.map +1 -0
  198. package/dist/ci-cd-publisher.d.ts +162 -0
  199. package/dist/ci-cd-publisher.js +319 -0
  200. package/dist/ci-cd-publisher.js.map +1 -0
  201. package/dist/cli.d.ts +2 -0
  202. package/dist/cli.js +4074 -0
  203. package/dist/cli.js.map +1 -0
  204. package/dist/clone.d.ts +25 -0
  205. package/dist/clone.js +73 -0
  206. package/dist/clone.js.map +1 -0
  207. package/dist/completions.d.ts +8 -0
  208. package/dist/completions.js +250 -0
  209. package/dist/completions.js.map +1 -0
  210. package/dist/compression-manager.d.ts +107 -0
  211. package/dist/compression-manager.js +250 -0
  212. package/dist/compression-manager.js.map +1 -0
  213. package/dist/config.d.ts +233 -0
  214. package/dist/config.js +255 -0
  215. package/dist/config.js.map +1 -0
  216. package/dist/context.d.ts +38 -0
  217. package/dist/context.js +86 -0
  218. package/dist/context.js.map +1 -0
  219. package/dist/cost-monitor.d.ts +72 -0
  220. package/dist/cost-monitor.js +218 -0
  221. package/dist/cost-monitor.js.map +1 -0
  222. package/dist/create-plugin.d.ts +22 -0
  223. package/dist/create-plugin.js +266 -0
  224. package/dist/create-plugin.js.map +1 -0
  225. package/dist/database.d.ts +123 -0
  226. package/dist/database.js +354 -0
  227. package/dist/database.js.map +1 -0
  228. package/dist/datadog-adapter.d.ts +60 -0
  229. package/dist/datadog-adapter.js +245 -0
  230. package/dist/datadog-adapter.js.map +1 -0
  231. package/dist/doctor.d.ts +15 -0
  232. package/dist/doctor.js +131 -0
  233. package/dist/doctor.js.map +1 -0
  234. package/dist/documentation-generator.d.ts +226 -0
  235. package/dist/documentation-generator.js +348 -0
  236. package/dist/documentation-generator.js.map +1 -0
  237. package/dist/elevation-scopes.d.ts +40 -0
  238. package/dist/elevation-scopes.js +110 -0
  239. package/dist/elevation-scopes.js.map +1 -0
  240. package/dist/elevation.d.ts +102 -0
  241. package/dist/elevation.js +449 -0
  242. package/dist/elevation.js.map +1 -0
  243. package/dist/env-diff.d.ts +27 -0
  244. package/dist/env-diff.js +104 -0
  245. package/dist/env-diff.js.map +1 -0
  246. package/dist/env-inspect.d.ts +28 -0
  247. package/dist/env-inspect.js +81 -0
  248. package/dist/env-inspect.js.map +1 -0
  249. package/dist/env-switch.d.ts +37 -0
  250. package/dist/env-switch.js +102 -0
  251. package/dist/env-switch.js.map +1 -0
  252. package/dist/environment.d.ts +27 -0
  253. package/dist/environment.js +148 -0
  254. package/dist/environment.js.map +1 -0
  255. package/dist/error-tracker.d.ts +92 -0
  256. package/dist/error-tracker.js +206 -0
  257. package/dist/error-tracker.js.map +1 -0
  258. package/dist/escalate.d.ts +11 -0
  259. package/dist/escalate.js +73 -0
  260. package/dist/escalate.js.map +1 -0
  261. package/dist/event-stream.d.ts +81 -0
  262. package/dist/event-stream.js +161 -0
  263. package/dist/event-stream.js.map +1 -0
  264. package/dist/fix.d.ts +42 -0
  265. package/dist/fix.js +419 -0
  266. package/dist/fix.js.map +1 -0
  267. package/dist/governance-middleware.d.ts +22 -0
  268. package/dist/governance-middleware.js +173 -0
  269. package/dist/governance-middleware.js.map +1 -0
  270. package/dist/governance.d.ts +44 -0
  271. package/dist/governance.js +236 -0
  272. package/dist/governance.js.map +1 -0
  273. package/dist/hooks.d.ts +25 -0
  274. package/dist/hooks.js +281 -0
  275. package/dist/hooks.js.map +1 -0
  276. package/dist/id-generator.d.ts +43 -0
  277. package/dist/id-generator.js +47 -0
  278. package/dist/id-generator.js.map +1 -0
  279. package/dist/image-optimizer.d.ts +92 -0
  280. package/dist/image-optimizer.js +202 -0
  281. package/dist/image-optimizer.js.map +1 -0
  282. package/dist/install.d.ts +15 -0
  283. package/dist/install.js +59 -0
  284. package/dist/install.js.map +1 -0
  285. package/dist/lock.d.ts +82 -0
  286. package/dist/lock.js +264 -0
  287. package/dist/lock.js.map +1 -0
  288. package/dist/login.d.ts +23 -0
  289. package/dist/login.js +132 -0
  290. package/dist/login.js.map +1 -0
  291. package/dist/mcp-kit-tools-model.d.ts +195 -0
  292. package/dist/mcp-kit-tools-model.js +6 -0
  293. package/dist/mcp-kit-tools-model.js.map +1 -0
  294. package/dist/mcp-kit-tools-service.d.ts +127 -0
  295. package/dist/mcp-kit-tools-service.js +943 -0
  296. package/dist/mcp-kit-tools-service.js.map +1 -0
  297. package/dist/mcp-orchestrator.d.ts +70 -0
  298. package/dist/mcp-orchestrator.js +175 -0
  299. package/dist/mcp-orchestrator.js.map +1 -0
  300. package/dist/mcp-server.d.ts +3 -0
  301. package/dist/mcp-server.js +722 -0
  302. package/dist/mcp-server.js.map +1 -0
  303. package/dist/middleware/rate-limiter.d.ts +74 -0
  304. package/dist/middleware/rate-limiter.js +342 -0
  305. package/dist/middleware/rate-limiter.js.map +1 -0
  306. package/dist/migration-runner.d.ts +66 -0
  307. package/dist/migration-runner.js +192 -0
  308. package/dist/migration-runner.js.map +1 -0
  309. package/dist/migrations.d.ts +25 -0
  310. package/dist/migrations.js +530 -0
  311. package/dist/migrations.js.map +1 -0
  312. package/dist/moderation-system.d.ts +153 -0
  313. package/dist/moderation-system.js +338 -0
  314. package/dist/moderation-system.js.map +1 -0
  315. package/dist/multi-agent-workflow-model.d.ts +125 -0
  316. package/dist/multi-agent-workflow-model.js +6 -0
  317. package/dist/multi-agent-workflow-model.js.map +1 -0
  318. package/dist/multi-agent-workflow-service.d.ts +102 -0
  319. package/dist/multi-agent-workflow-service.js +452 -0
  320. package/dist/multi-agent-workflow-service.js.map +1 -0
  321. package/dist/onepassword.d.ts +75 -0
  322. package/dist/onepassword.js +140 -0
  323. package/dist/onepassword.js.map +1 -0
  324. package/dist/open.d.ts +30 -0
  325. package/dist/open.js +166 -0
  326. package/dist/open.js.map +1 -0
  327. package/dist/output.d.ts +32 -0
  328. package/dist/output.js +295 -0
  329. package/dist/output.js.map +1 -0
  330. package/dist/partner-service.d.ts +101 -0
  331. package/dist/partner-service.js +191 -0
  332. package/dist/partner-service.js.map +1 -0
  333. package/dist/payout-service.d.ts +136 -0
  334. package/dist/payout-service.js +293 -0
  335. package/dist/payout-service.js.map +1 -0
  336. package/dist/pkg.d.ts +30 -0
  337. package/dist/pkg.js +162 -0
  338. package/dist/pkg.js.map +1 -0
  339. package/dist/plugin-loader.d.ts +16 -0
  340. package/dist/plugin-loader.js +124 -0
  341. package/dist/plugin-loader.js.map +1 -0
  342. package/dist/plugin-registry-model.d.ts +133 -0
  343. package/dist/plugin-registry-model.js +6 -0
  344. package/dist/plugin-registry-model.js.map +1 -0
  345. package/dist/plugin-registry-service.d.ts +109 -0
  346. package/dist/plugin-registry-service.js +361 -0
  347. package/dist/plugin-registry-service.js.map +1 -0
  348. package/dist/plugin-registry.d.ts +58 -0
  349. package/dist/plugin-registry.js +108 -0
  350. package/dist/plugin-registry.js.map +1 -0
  351. package/dist/plugin-updates.d.ts +135 -0
  352. package/dist/plugin-updates.js +326 -0
  353. package/dist/plugin-updates.js.map +1 -0
  354. package/dist/plugins-cli.d.ts +7 -0
  355. package/dist/plugins-cli.js +157 -0
  356. package/dist/plugins-cli.js.map +1 -0
  357. package/dist/plugins.d.ts +88 -0
  358. package/dist/plugins.js +251 -0
  359. package/dist/plugins.js.map +1 -0
  360. package/dist/policy.d.ts +66 -0
  361. package/dist/policy.js +160 -0
  362. package/dist/policy.js.map +1 -0
  363. package/dist/post-pull-audit.d.ts +39 -0
  364. package/dist/post-pull-audit.js +151 -0
  365. package/dist/post-pull-audit.js.map +1 -0
  366. package/dist/provision.d.ts +17 -0
  367. package/dist/provision.js +147 -0
  368. package/dist/provision.js.map +1 -0
  369. package/dist/query-optimizer.d.ts +102 -0
  370. package/dist/query-optimizer.js +199 -0
  371. package/dist/query-optimizer.js.map +1 -0
  372. package/dist/read-only-mode.d.ts +46 -0
  373. package/dist/read-only-mode.js +71 -0
  374. package/dist/read-only-mode.js.map +1 -0
  375. package/dist/redis-adapter.d.ts +71 -0
  376. package/dist/redis-adapter.js +278 -0
  377. package/dist/redis-adapter.js.map +1 -0
  378. package/dist/resilience-tests.d.ts +120 -0
  379. package/dist/resilience-tests.js +293 -0
  380. package/dist/resilience-tests.js.map +1 -0
  381. package/dist/revocation.d.ts +22 -0
  382. package/dist/revocation.js +100 -0
  383. package/dist/revocation.js.map +1 -0
  384. package/dist/run.d.ts +21 -0
  385. package/dist/run.js +80 -0
  386. package/dist/run.js.map +1 -0
  387. package/dist/scan-build.d.ts +18 -0
  388. package/dist/scan-build.js +100 -0
  389. package/dist/scan-build.js.map +1 -0
  390. package/dist/scan-plaintext.d.ts +24 -0
  391. package/dist/scan-plaintext.js +147 -0
  392. package/dist/scan-plaintext.js.map +1 -0
  393. package/dist/scan-staged.d.ts +15 -0
  394. package/dist/scan-staged.js +70 -0
  395. package/dist/scan-staged.js.map +1 -0
  396. package/dist/scan-transcripts.d.ts +23 -0
  397. package/dist/scan-transcripts.js +93 -0
  398. package/dist/scan-transcripts.js.map +1 -0
  399. package/dist/secret-backends.d.ts +50 -0
  400. package/dist/secret-backends.js +510 -0
  401. package/dist/secret-backends.js.map +1 -0
  402. package/dist/secret-expiration.d.ts +46 -0
  403. package/dist/secret-expiration.js +172 -0
  404. package/dist/secret-expiration.js.map +1 -0
  405. package/dist/secrets-migrate.d.ts +75 -0
  406. package/dist/secrets-migrate.js +185 -0
  407. package/dist/secrets-migrate.js.map +1 -0
  408. package/dist/secrets-model.d.ts +77 -0
  409. package/dist/secrets-model.js +6 -0
  410. package/dist/secrets-model.js.map +1 -0
  411. package/dist/secrets-onecli.d.ts +65 -0
  412. package/dist/secrets-onecli.js +113 -0
  413. package/dist/secrets-onecli.js.map +1 -0
  414. package/dist/secrets-propagate.d.ts +48 -0
  415. package/dist/secrets-propagate.js +201 -0
  416. package/dist/secrets-propagate.js.map +1 -0
  417. package/dist/secrets-pull.d.ts +34 -0
  418. package/dist/secrets-pull.js +118 -0
  419. package/dist/secrets-pull.js.map +1 -0
  420. package/dist/secrets-purge-history.d.ts +53 -0
  421. package/dist/secrets-purge-history.js +144 -0
  422. package/dist/secrets-purge-history.js.map +1 -0
  423. package/dist/secrets-rotate-cli.d.ts +54 -0
  424. package/dist/secrets-rotate-cli.js +438 -0
  425. package/dist/secrets-rotate-cli.js.map +1 -0
  426. package/dist/secrets-rotate.d.ts +38 -0
  427. package/dist/secrets-rotate.js +65 -0
  428. package/dist/secrets-rotate.js.map +1 -0
  429. package/dist/secrets-service.d.ts +73 -0
  430. package/dist/secrets-service.js +283 -0
  431. package/dist/secrets-service.js.map +1 -0
  432. package/dist/secrets-set.d.ts +25 -0
  433. package/dist/secrets-set.js +33 -0
  434. package/dist/secrets-set.js.map +1 -0
  435. package/dist/secrets-sync.d.ts +21 -0
  436. package/dist/secrets-sync.js +215 -0
  437. package/dist/secrets-sync.js.map +1 -0
  438. package/dist/secrets-validate.d.ts +41 -0
  439. package/dist/secrets-validate.js +126 -0
  440. package/dist/secrets-validate.js.map +1 -0
  441. package/dist/secrets-vault-migrate.d.ts +71 -0
  442. package/dist/secrets-vault-migrate.js +258 -0
  443. package/dist/secrets-vault-migrate.js.map +1 -0
  444. package/dist/secrets.d.ts +16 -0
  445. package/dist/secrets.js +72 -0
  446. package/dist/secrets.js.map +1 -0
  447. package/dist/security-hardening.d.ts +150 -0
  448. package/dist/security-hardening.js +275 -0
  449. package/dist/security-hardening.js.map +1 -0
  450. package/dist/security-policy.d.ts +89 -0
  451. package/dist/security-policy.js +174 -0
  452. package/dist/security-policy.js.map +1 -0
  453. package/dist/security-prescan.d.ts +117 -0
  454. package/dist/security-prescan.js +566 -0
  455. package/dist/security-prescan.js.map +1 -0
  456. package/dist/sentry-adapter.d.ts +49 -0
  457. package/dist/sentry-adapter.js +227 -0
  458. package/dist/sentry-adapter.js.map +1 -0
  459. package/dist/service-adapter.d.ts +94 -0
  460. package/dist/service-adapter.js +162 -0
  461. package/dist/service-adapter.js.map +1 -0
  462. package/dist/skills.d.ts +13 -0
  463. package/dist/skills.js +17 -0
  464. package/dist/skills.js.map +1 -0
  465. package/dist/sla-monitor.d.ts +107 -0
  466. package/dist/sla-monitor.js +233 -0
  467. package/dist/sla-monitor.js.map +1 -0
  468. package/dist/stack-detector.d.ts +12 -0
  469. package/dist/stack-detector.js +251 -0
  470. package/dist/stack-detector.js.map +1 -0
  471. package/dist/team-model.d.ts +58 -0
  472. package/dist/team-model.js +83 -0
  473. package/dist/team-model.js.map +1 -0
  474. package/dist/team-service.d.ts +54 -0
  475. package/dist/team-service.js +206 -0
  476. package/dist/team-service.js.map +1 -0
  477. package/dist/toml-generator.d.ts +8 -0
  478. package/dist/toml-generator.js +223 -0
  479. package/dist/toml-generator.js.map +1 -0
  480. package/dist/triage-sandbox.d.ts +34 -0
  481. package/dist/triage-sandbox.js +167 -0
  482. package/dist/triage-sandbox.js.map +1 -0
  483. package/dist/triage.d.ts +30 -0
  484. package/dist/triage.js +79 -0
  485. package/dist/triage.js.map +1 -0
  486. package/dist/update-check.d.ts +13 -0
  487. package/dist/update-check.js +91 -0
  488. package/dist/update-check.js.map +1 -0
  489. package/dist/utils/colors.d.ts +14 -0
  490. package/dist/utils/colors.js +15 -0
  491. package/dist/utils/colors.js.map +1 -0
  492. package/dist/utils/didYouMean.d.ts +15 -0
  493. package/dist/utils/didYouMean.js +47 -0
  494. package/dist/utils/didYouMean.js.map +1 -0
  495. package/dist/utils/exec.d.ts +21 -0
  496. package/dist/utils/exec.js +23 -0
  497. package/dist/utils/exec.js.map +1 -0
  498. package/dist/utils/execFileNoThrow.d.ts +14 -0
  499. package/dist/utils/execFileNoThrow.js +29 -0
  500. package/dist/utils/execFileNoThrow.js.map +1 -0
  501. package/dist/utils/flags.d.ts +19 -0
  502. package/dist/utils/flags.js +36 -0
  503. package/dist/utils/flags.js.map +1 -0
  504. package/dist/utils/parseCommand.d.ts +16 -0
  505. package/dist/utils/parseCommand.js +13 -0
  506. package/dist/utils/parseCommand.js.map +1 -0
  507. package/dist/utils/prompt.d.ts +13 -0
  508. package/dist/utils/prompt.js +35 -0
  509. package/dist/utils/prompt.js.map +1 -0
  510. package/dist/utils/promptSelect.d.ts +19 -0
  511. package/dist/utils/promptSelect.js +89 -0
  512. package/dist/utils/promptSelect.js.map +1 -0
  513. package/dist/utils/redactSecrets.d.ts +24 -0
  514. package/dist/utils/redactSecrets.js +134 -0
  515. package/dist/utils/redactSecrets.js.map +1 -0
  516. package/dist/validation/dynamic-schema.d.ts +29 -0
  517. package/dist/validation/dynamic-schema.js +76 -0
  518. package/dist/validation/dynamic-schema.js.map +1 -0
  519. package/package.json +52 -0
package/dist/cli.js ADDED
@@ -0,0 +1,4074 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
3
+ import { writeFile, access, mkdir } from "node:fs/promises";
4
+ import { fileURLToPath } from "node:url";
5
+ import { resolve, dirname, join } from "node:path";
6
+ import { loadConfig, resolveActiveEnvironment } from "./config.js";
7
+ import { checkTools } from "./check-tools.js";
8
+ import { checkServices } from "./check-services.js";
9
+ import { checkSecrets } from "./check-secrets.js";
10
+ import { checkSecurity } from "./check-security.js";
11
+ import { checkWebSearch } from "./check-web-search.js";
12
+ import { installTools } from "./install.js";
13
+ import { loginServices } from "./login.js";
14
+ import { check1PasswordStatus, detect1PasswordMode } from "./onepassword.js";
15
+ import { isNonInteractive } from "./environment.js";
16
+ import { spawn as spawnChild } from "node:child_process";
17
+ import { promptSelect } from "./utils/promptSelect.js";
18
+ import { hasFlag, flagValue } from "./utils/flags.js";
19
+ import { scanPlaintextSecrets } from "./scan-plaintext.js";
20
+ import { planMigration, writeSecretToBackend, commentOutInFile, } from "./secrets-migrate.js";
21
+ import { analyzeRepo, renderClaudeMd, renderRulesMd } from "./analyze.js";
22
+ import { cmdSecretsRotate, pickBackendOpts } from "./secrets-rotate-cli.js";
23
+ import { detectTools as detectPurgeTools, previewMatches, purgeHistory, } from "./secrets-purge-history.js";
24
+ import { propagate, parseTargets, ALL_TARGETS, } from "./secrets-propagate.js";
25
+ import { initAllowlist, checkAllowlist, addToAllowlist, checkSecretPolicy, } from "./security-policy.js";
26
+ import { clearBumblebeeCache } from "./bumblebee.js";
27
+ import { KNOWN_ENVS, readActiveEnv, writeActiveEnv, } from "./env-switch.js";
28
+ import { scanStagedFiles } from "./scan-staged.js";
29
+ import { scanBuildArtifacts } from "./scan-build.js";
30
+ import { scanTranscripts } from "./scan-transcripts.js";
31
+ import { sampleCosts } from "./cost-monitor.js";
32
+ import { readSecretAuditEvents, groupBySecret, summarize, } from "./audit-secrets.js";
33
+ import { requireElevation, consumeElevation, grantElevation, clearElevation, readElevation, verifyTotp, elevationTtlMinutes, enrollTotp, resolveTotpSecret, } from "./elevation.js";
34
+ import { checkGitignore, patchGitignore, findCommittedSensitive, } from "./check-gitignore.js";
35
+ import { auditPull, reportSeverity } from "./post-pull-audit.js";
36
+ import { checkOneCliStatus, registerSecretInOneCli, generatePlaceholder, resolveOneCliConfig, } from "./secrets-onecli.js";
37
+ import { generateSecrets } from "./secrets.js";
38
+ import { syncSecrets } from "./secrets-sync.js";
39
+ import { generateCompletions } from "./completions.js";
40
+ import { checkForUpdate, printUpdateNotice } from "./update-check.js";
41
+ import { checkSkills } from "./check-skills.js";
42
+ import { checkLockFiles } from "./check-lock.js";
43
+ import { collectEscalations, formatEscalationMessage } from "./escalate.js";
44
+ import { printToolsTable, printServicesTable, printSecretsTable, printSkillsTable, printWebSearchStatus, printSecurityTable, printLockTable, printSummary, printAuditTable, runStep, stepHeader, } from "./output.js";
45
+ import { checkRevocationStatus } from "./revocation.js";
46
+ import { getBudgetStatus, formatBudgetStatus } from "./budget.js";
47
+ import { readAuditLog } from "./audit.js";
48
+ import { formatGovernanceStatus, mergeGovernanceConfigAsync } from "./governance.js";
49
+ import { withGovernance } from "./governance-middleware.js";
50
+ import { installHooks, SKIPPED_COMMITS_LOG } from "./hooks.js";
51
+ import { writeAgentConfig, detectAgentTargets } from "./agent-config.js";
52
+ import { checkHooks, isGitRepository } from "./check-hooks.js";
53
+ import { readkitMeta, readSkillsLock, readCliLock, updateSkillsLock, updateCliLock, } from "./lock.js";
54
+ import { provisionService, listAvailableServices, getServiceInfo } from "./provision.js";
55
+ import { cmdFix } from "./fix.js";
56
+ import { promptConfirm } from "./utils/prompt.js";
57
+ import { startMcpServer } from "./mcp-server.js";
58
+ import { c } from "./utils/colors.js";
59
+ import { runDoctor } from "./doctor.js";
60
+ import { inspectEnv } from "./env-inspect.js";
61
+ import { detectStack } from "./stack-detector.js";
62
+ import { generateToml } from "./toml-generator.js";
63
+ import { createPlugin } from "./create-plugin.js";
64
+ import { cmdPlugin } from "./plugins-cli.js";
65
+ import { cloneRepository } from "./clone.js";
66
+ import { executeCommand } from "./run.js";
67
+ import { listServices, openService } from "./open.js";
68
+ import { gatherProjectContext } from "./context.js";
69
+ import { runTriage, listTriageTools } from "./triage.js";
70
+ import { parsePkgSpec, installPkg } from "./pkg.js";
71
+ const KIT_FILE = ".kit.toml";
72
+ const __dirname = dirname(fileURLToPath(import.meta.url));
73
+ const KIT_VERSION = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8")).version;
74
+ function resolveConfigPath() {
75
+ return resolve(process.cwd(), KIT_FILE);
76
+ }
77
+ async function cmdCheck() {
78
+ const jsonMode = hasFlag(process.argv, "--json");
79
+ const enforceTests = hasFlag(process.argv, "--enforce-tests");
80
+ const config = await loadConfig(resolveConfigPath());
81
+ return await withGovernance(config, {
82
+ operation: "check",
83
+ operationType: "read",
84
+ metadata: {},
85
+ }, async () => {
86
+ const live = !jsonMode;
87
+ if (live)
88
+ stepHeader("Checks");
89
+ const step = (label, fn) => live ? runStep(label, fn) : fn();
90
+ const toolResults = config.tools
91
+ ? await step("tools", () => checkTools(config.tools))
92
+ : [];
93
+ const serviceResults = config.services
94
+ ? await step("services", () => checkServices(config.services))
95
+ : [];
96
+ const secretResults = config.secrets
97
+ ? await step("secrets", () => checkSecrets(config.secrets))
98
+ : { templateExists: null, keys: [] };
99
+ const skillResults = config.skills
100
+ ? await step("skills", () => checkSkills(config.skills))
101
+ : [];
102
+ const hookResults = config.hooks && isGitRepository()
103
+ ? await step("git hooks", () => checkHooks(config.hooks))
104
+ : [];
105
+ const webSearchResult = config.web?.search
106
+ ? await step("web search", () => checkWebSearch(config.web.search))
107
+ : null;
108
+ const securityResults = await step("security scan", () => checkSecurity());
109
+ const lockResults = await step("lock files", () => checkLockFiles(config));
110
+ // Test-coverage enforcement (--enforce-tests CLI flag overrides config).
111
+ // Baseline-aware: pre-existing untested files only warn; net-new fail.
112
+ const { checkTests } = await import("./check-tests.js");
113
+ const { loadBaseline, baselineGet } = await import("./baseline.js");
114
+ const baseline = await loadBaseline();
115
+ const testResults = await step("test coverage", () => checkTests({
116
+ enforce: enforceTests,
117
+ baseline: baselineGet(baseline, "tests", "untested_files"),
118
+ }));
119
+ const securityOk = securityResults.every((s) => s.status === "pass" || s.status === "skip");
120
+ const testsOk = testResults.every((t) => t.status !== "fail");
121
+ const lockOk = lockResults.every((l) => l.inSync);
122
+ const allOk = toolResults.every((t) => t.ok) &&
123
+ serviceResults.every((s) => s.authenticated) &&
124
+ secretResults.keys.every((s) => s.available) &&
125
+ skillResults.filter((s) => s.required).every((s) => s.installed) &&
126
+ hookResults.every((h) => h.installed && h.upToDate) &&
127
+ securityOk &&
128
+ testsOk &&
129
+ lockOk;
130
+ if (jsonMode) {
131
+ const checks = [
132
+ ...toolResults.map((t) => ({
133
+ name: t.name,
134
+ status: (t.ok ? "pass" : "fail"),
135
+ detail: t.installed ? `installed ${t.installed}` : "not installed",
136
+ category: "tools",
137
+ })),
138
+ ...serviceResults.map((s) => ({
139
+ name: s.name,
140
+ status: (s.authenticated ? "pass" : "fail"),
141
+ detail: s.output ?? (s.authenticated ? "authenticated" : "not authenticated"),
142
+ category: "services",
143
+ })),
144
+ ...secretResults.keys.map((s) => ({
145
+ name: s.name,
146
+ status: (s.available ? "pass" : "fail"),
147
+ detail: s.detail ?? (s.available ? "available" : "missing"),
148
+ category: "secrets",
149
+ })),
150
+ ...skillResults.map((s) => ({
151
+ name: s.name,
152
+ status: (s.installed ? "pass" : s.required ? "fail" : "warn"),
153
+ detail: s.installed ? "installed" : "not installed",
154
+ category: "skills",
155
+ })),
156
+ ...hookResults.map((h) => ({
157
+ name: h.hookName,
158
+ status: (!h.installed ? "fail" : !h.upToDate ? "warn" : "pass"),
159
+ detail: h.detail,
160
+ category: "hooks",
161
+ })),
162
+ ...(webSearchResult
163
+ ? [
164
+ {
165
+ name: webSearchResult.provider,
166
+ status: (webSearchResult.healthy ? "pass" : "fail"),
167
+ detail: webSearchResult.error ?? (webSearchResult.healthy ? "healthy" : "unhealthy"),
168
+ category: "web-search",
169
+ },
170
+ ]
171
+ : []),
172
+ ...lockResults.map((l) => ({
173
+ name: l.category === "skills-lock" ? "skills-lock.json" : "cli-lock.json",
174
+ status: (l.inSync ? "pass" : l.exists ? "warn" : "fail"),
175
+ detail: l.detail,
176
+ category: "lock",
177
+ })),
178
+ ...securityResults.map((s) => ({
179
+ name: s.name,
180
+ status: s.status,
181
+ detail: s.detail,
182
+ category: `security/${s.category}`,
183
+ })),
184
+ ...testResults.map((t) => ({
185
+ name: t.name,
186
+ status: t.status,
187
+ detail: t.detail,
188
+ category: "tests",
189
+ })),
190
+ ];
191
+ const summary = checks.reduce((acc, c) => {
192
+ if (c.status === "pass")
193
+ acc.passed++;
194
+ else if (c.status === "fail")
195
+ acc.failed++;
196
+ else if (c.status === "warn")
197
+ acc.warnings++;
198
+ else
199
+ acc.skipped++;
200
+ return acc;
201
+ }, { passed: 0, failed: 0, warnings: 0, skipped: 0 });
202
+ const output = { ok: allOk, checks, summary };
203
+ console.log(JSON.stringify(output, null, 2));
204
+ return allOk;
205
+ }
206
+ printToolsTable(toolResults);
207
+ printServicesTable(serviceResults);
208
+ printSecretsTable(secretResults.templateExists, secretResults.keys);
209
+ printSkillsTable(skillResults);
210
+ printWebSearchStatus(webSearchResult);
211
+ printLockTable(lockResults);
212
+ // Print hooks status if configured
213
+ if (hookResults.length > 0) {
214
+ console.log(`${c.cyan}Git Hooks${c.reset}`);
215
+ for (const r of hookResults) {
216
+ const icon = !r.installed
217
+ ? `${c.red}✗${c.reset}`
218
+ : !r.upToDate
219
+ ? `${c.yellow}!${c.reset}`
220
+ : `${c.green}✓${c.reset}`;
221
+ const status = !r.installed
222
+ ? `${c.red}not installed${c.reset}`
223
+ : !r.upToDate
224
+ ? `${c.yellow}outdated${c.reset}`
225
+ : `${c.green}up-to-date${c.reset}`;
226
+ console.log(` ${icon} ${r.hookName} ${status} ${c.dim}${r.detail}${c.reset}`);
227
+ }
228
+ console.log();
229
+ }
230
+ printSecurityTable(securityResults);
231
+ // Render test-coverage results in the same compact style.
232
+ if (testResults.length > 0) {
233
+ console.log(`\n${c.bold}Tests${c.reset}`);
234
+ for (const r of testResults) {
235
+ const icon = r.status === "pass" ? `${c.green}✓${c.reset}` :
236
+ r.status === "fail" ? `${c.red}✗${c.reset}` :
237
+ r.status === "warn" ? `${c.yellow}!${c.reset}` :
238
+ `${c.dim}-${c.reset}`;
239
+ console.log(` ${icon} ${r.name} ${c.dim}${r.detail}${c.reset}`);
240
+ if (r.files && r.files.length > 0) {
241
+ for (const f of r.files)
242
+ console.log(` ${c.dim}- ${f}${c.reset}`);
243
+ }
244
+ }
245
+ console.log();
246
+ }
247
+ printSummary(toolResults, serviceResults, secretResults.keys, securityResults);
248
+ return allOk;
249
+ });
250
+ }
251
+ /**
252
+ * `kit design` — a11y + design-token consistency, baseline-aware.
253
+ */
254
+ async function cmdDesign() {
255
+ const enforce = hasFlag(process.argv, "--enforce");
256
+ const jsonMode = hasFlag(process.argv, "--json");
257
+ const { checkDesign } = await import("./check-design.js");
258
+ const { loadBaseline, baselineGet } = await import("./baseline.js");
259
+ const baseline = await loadBaseline();
260
+ const results = await checkDesign({
261
+ enforce,
262
+ baseline: {
263
+ a11y: baselineGet(baseline, "design", "a11y"),
264
+ tokens: baselineGet(baseline, "design", "tokens"),
265
+ },
266
+ });
267
+ if (jsonMode) {
268
+ console.log(JSON.stringify({ ok: results.every((r) => r.status !== "fail"), checks: results }, null, 2));
269
+ return results.every((r) => r.status !== "fail");
270
+ }
271
+ console.log(`${c.bold}Design${c.reset}`);
272
+ for (const r of results) {
273
+ const icon = r.status === "pass" ? `${c.green}✓${c.reset}` :
274
+ r.status === "fail" ? `${c.red}✗${c.reset}` :
275
+ r.status === "warn" ? `${c.yellow}!${c.reset}` :
276
+ `${c.dim}-${c.reset}`;
277
+ console.log(` ${icon} ${r.name} ${c.dim}${r.detail}${c.reset}`);
278
+ if (r.files)
279
+ for (const f of r.files)
280
+ console.log(` ${c.dim}- ${f}${c.reset}`);
281
+ }
282
+ return results.every((r) => r.status !== "fail");
283
+ }
284
+ /**
285
+ * `kit review` — meta-runner: check + design + tests in one shot.
286
+ * Convenient single-command gate for AI agents and PR checks.
287
+ */
288
+ async function cmdReview() {
289
+ const jsonMode = hasFlag(process.argv, "--json");
290
+ let allOk = true;
291
+ if (!jsonMode)
292
+ console.log(`${c.bold}kit review${c.reset} — full repo audit\n`);
293
+ if (!jsonMode)
294
+ console.log(`${c.bold}=== check ===${c.reset}`);
295
+ const checkOk = await cmdCheck();
296
+ if (!checkOk)
297
+ allOk = false;
298
+ if (!jsonMode)
299
+ console.log(`\n${c.bold}=== design ===${c.reset}`);
300
+ const designOk = await cmdDesign();
301
+ if (!designOk)
302
+ allOk = false;
303
+ if (!jsonMode) {
304
+ console.log(`\n${c.bold}${allOk ? `${c.green}✓ review passed` : `${c.red}✗ review failed`}${c.reset}`);
305
+ }
306
+ return allOk;
307
+ }
308
+ /**
309
+ * `kit baseline freeze` — snapshot current warnings into .kit-baseline.json
310
+ * so future runs only gate on net-new findings. Currently freezes:
311
+ * - tests.untested_files
312
+ */
313
+ async function cmdBaseline() {
314
+ const sub = process.argv[3];
315
+ if (!sub || sub === "--help" || sub === "-h") {
316
+ console.log(`${c.bold}kit baseline${c.reset} — freeze current warnings`);
317
+ console.log("\nUsage:");
318
+ console.log(" kit baseline freeze Snapshot current findings as the new baseline");
319
+ console.log(" kit baseline show Print contents of .kit-baseline.json");
320
+ return true;
321
+ }
322
+ const { loadBaseline, saveBaseline, baselineSet, BASELINE_FILE } = await import("./baseline.js");
323
+ const baseline = await loadBaseline();
324
+ if (sub === "show") {
325
+ console.log(JSON.stringify(baseline, null, 2));
326
+ return true;
327
+ }
328
+ if (sub !== "freeze") {
329
+ console.error(`${c.red}Unknown subcommand: ${sub}${c.reset}`);
330
+ return false;
331
+ }
332
+ const { findUntestedSources } = await import("./check-tests.js");
333
+ const { collectDesignKeys } = await import("./check-design.js");
334
+ const untested = await findUntestedSources();
335
+ baselineSet(baseline, "tests", "untested_files", untested);
336
+ const design = await collectDesignKeys();
337
+ baselineSet(baseline, "design", "a11y", design.a11y);
338
+ baselineSet(baseline, "design", "tokens", design.tokens);
339
+ await saveBaseline(baseline);
340
+ console.log(`${c.green}✓${c.reset} Wrote ${BASELINE_FILE} — ${untested.length} untested file(s), ${design.a11y.length} a11y, ${design.tokens.length} design-token finding(s) frozen.`);
341
+ console.log(` Future runs will gate only on NEW findings.`);
342
+ return true;
343
+ }
344
+ async function cmdInstall() {
345
+ const config = await loadConfig(resolveConfigPath());
346
+ if (!config.tools || Object.keys(config.tools).length === 0) {
347
+ console.log(`${c.dim}No tools configured in ${KIT_FILE}${c.reset}`);
348
+ return true;
349
+ }
350
+ const toolsConfig = config.tools;
351
+ console.log(`${c.bold}${c.cyan}Installing tools via mise...${c.reset}\n`);
352
+ return await withGovernance(config, {
353
+ operation: "tools.install",
354
+ operationType: "write",
355
+ metadata: {
356
+ tools: Object.keys(toolsConfig),
357
+ },
358
+ }, async () => {
359
+ const results = await installTools(toolsConfig);
360
+ let allOk = true;
361
+ for (const r of results) {
362
+ const icon = r.action === "failed"
363
+ ? `${c.red}✗${c.reset}`
364
+ : `${c.green}✓${c.reset}`;
365
+ const label = r.action === "already_ok"
366
+ ? `${c.dim}already installed${c.reset}`
367
+ : r.action === "installed"
368
+ ? `${c.green}installed${c.reset}`
369
+ : `${c.red}failed${c.reset}`;
370
+ console.log(` ${icon} ${r.name} ${label} ${c.dim}${r.detail}${c.reset}`);
371
+ if (r.action === "failed")
372
+ allOk = false;
373
+ }
374
+ console.log();
375
+ return allOk;
376
+ });
377
+ }
378
+ async function ensureSecretsBackend(config) {
379
+ // Some secret backends (1Password) need a separate interactive signin before
380
+ // any of the per-key `op read` calls can succeed. Run it once up front so
381
+ // `kit setup` produces a usable .env.local in a single pass.
382
+ if (config.secrets?.store !== "1password")
383
+ return true;
384
+ const { mode, hint } = await detect1PasswordMode();
385
+ // Already authenticated via the desktop app or a service-account token —
386
+ // nothing else to do, the per-key `op read` calls will inherit auth.
387
+ if (mode === "service-account" || mode === "desktop-integration") {
388
+ return true;
389
+ }
390
+ if (mode === "not-installed") {
391
+ console.log(`${c.yellow}1Password CLI not installed.${c.reset}`);
392
+ console.log(`${c.dim}${hint}${c.reset}\n`);
393
+ return false;
394
+ }
395
+ if (mode === "no-account") {
396
+ console.log(`${c.yellow}No 1Password account configured.${c.reset}`);
397
+ console.log(`${c.dim}${hint}${c.reset}\n`);
398
+ return false;
399
+ }
400
+ // mode === "eval-signin": op exists, accounts exist, but no live session.
401
+ // We can't propagate OP_SESSION_<shorthand> from a child spawn back into
402
+ // the parent shell, so attempting `op signin` here would print eval-able
403
+ // text but leave the running kit invocation without auth. Explain the
404
+ // two viable paths to the user instead.
405
+ if (isNonInteractive()) {
406
+ console.log(`${c.yellow}1Password not signed in — non-interactive mode can't recover.${c.reset}`);
407
+ console.log(`${c.dim}${hint}${c.reset}\n`);
408
+ return false;
409
+ }
410
+ console.log(`${c.yellow}1Password not signed in.${c.reset}`);
411
+ console.log(`${c.dim}${hint}${c.reset}`);
412
+ console.log(`${c.dim}For headless / CI: set ${c.bold}OP_SERVICE_ACCOUNT_TOKEN${c.reset}${c.dim} instead.${c.reset}\n`);
413
+ // Last resort — try `op signin` interactively. With desktop-integration
414
+ // enabled mid-session this will succeed; with eval-only setups it will
415
+ // print export commands the user can copy.
416
+ const ok = await new Promise((resolve) => {
417
+ const child = spawnChild("op", ["signin"], {
418
+ stdio: "inherit",
419
+ env: { ...process.env },
420
+ });
421
+ child.on("close", (code) => resolve(code === 0));
422
+ child.on("error", () => resolve(false));
423
+ });
424
+ if (!ok)
425
+ return false;
426
+ const verify = await check1PasswordStatus();
427
+ if (!verify.authenticated) {
428
+ console.log(`${c.yellow}Still not authenticated — see hint above.${c.reset}\n`);
429
+ return false;
430
+ }
431
+ console.log(`${c.green}✓ 1Password authenticated${c.reset}\n`);
432
+ return true;
433
+ }
434
+ async function cmdLogin() {
435
+ const config = await loadConfig(resolveConfigPath());
436
+ // Per-service control: `--service <name>` narrows the login to a single
437
+ // configured service. Useful when one auth flakes and you don't want to
438
+ // re-run all 8 services again. `--retry-count N` retries the same login
439
+ // command on failure with exponential backoff. `--force-reauth` is
440
+ // accepted but currently a no-op flag the CLI layer surfaces — the
441
+ // underlying service-adapter is responsible for honoring it (most just
442
+ // re-run their login command idempotently anyway).
443
+ const args = process.argv.slice(3);
444
+ const serviceFilter = flagValue(args, "--service");
445
+ const retryIdx = args.indexOf("--retry-count");
446
+ const retryCount = retryIdx >= 0 && args[retryIdx + 1] ? Math.max(0, parseInt(args[retryIdx + 1], 10) || 0) : 0;
447
+ const backendOk = await ensureSecretsBackend(config);
448
+ if (!config.services || Object.keys(config.services).length === 0) {
449
+ console.log(`${c.dim}No services configured in ${KIT_FILE}${c.reset}`);
450
+ return backendOk;
451
+ }
452
+ // Narrow services config to the requested one, if any.
453
+ let servicesConfig = config.services;
454
+ if (serviceFilter) {
455
+ if (!servicesConfig[serviceFilter]) {
456
+ console.error(`${c.red}No service "${serviceFilter}" in .kit.toml. Available: ${Object.keys(servicesConfig).join(", ")}${c.reset}`);
457
+ return false;
458
+ }
459
+ servicesConfig = { [serviceFilter]: servicesConfig[serviceFilter] };
460
+ console.log(`${c.dim}Filtering to service "${serviceFilter}"${retryCount ? ` (retries=${retryCount})` : ""}${c.reset}`);
461
+ }
462
+ console.log(`${c.bold}${c.cyan}Authenticating services...${c.reset}`);
463
+ return await withGovernance(config, {
464
+ operation: "services.login",
465
+ operationType: "write",
466
+ metadata: {
467
+ services: Object.keys(servicesConfig),
468
+ },
469
+ }, async () => {
470
+ let results = await loginServices(servicesConfig);
471
+ // Retry-loop: re-run failing services up to retryCount times with
472
+ // exponential backoff (250ms, 500ms, 1s, ...). Each attempt only
473
+ // re-tries services that were marked failed/login_unverified.
474
+ for (let attempt = 1; attempt <= retryCount; attempt++) {
475
+ const failed = results.filter((r) => r.action === "failed" || r.action === "login_unverified");
476
+ if (failed.length === 0)
477
+ break;
478
+ const backoffMs = 250 * 2 ** (attempt - 1);
479
+ console.log(`${c.dim}Retrying ${failed.length} service(s) in ${backoffMs}ms (attempt ${attempt}/${retryCount})...${c.reset}`);
480
+ await new Promise((res) => setTimeout(res, backoffMs));
481
+ const retryConfig = {};
482
+ for (const f of failed) {
483
+ if (servicesConfig[f.name])
484
+ retryConfig[f.name] = servicesConfig[f.name];
485
+ }
486
+ const retryResults = await loginServices(retryConfig);
487
+ // Merge: replace failed entries with their retry outcome.
488
+ const updated = new Map(results.map((r) => [r.name, r]));
489
+ for (const r of retryResults)
490
+ updated.set(r.name, r);
491
+ results = Array.from(updated.values());
492
+ }
493
+ let allOk = true;
494
+ console.log();
495
+ for (const r of results) {
496
+ const icon = r.action === "failed"
497
+ ? `${c.red}✗${c.reset}`
498
+ : r.action === "login_unverified"
499
+ ? `${c.yellow}?${c.reset}`
500
+ : `${c.green}✓${c.reset}`;
501
+ const label = r.action === "already_authenticated"
502
+ ? `${c.dim}already authenticated${c.reset}`
503
+ : r.action === "logged_in"
504
+ ? `${c.green}logged in${c.reset}`
505
+ : r.action === "login_unverified"
506
+ ? `${c.yellow}login unverified${c.reset}`
507
+ : `${c.red}failed${c.reset}`;
508
+ console.log(` ${icon} ${r.name} ${label} ${c.dim}${r.detail}${c.reset}`);
509
+ if (r.action === "failed" || r.action === "login_unverified")
510
+ allOk = false;
511
+ }
512
+ console.log();
513
+ return allOk && backendOk;
514
+ });
515
+ }
516
+ async function cmdSecrets() {
517
+ // Route subcommand: kit secrets sync [--target=...] [--dry-run]
518
+ if (process.argv[3] === "sync") {
519
+ return cmdSecretsSync();
520
+ }
521
+ if (process.argv[3] === "migrate") {
522
+ return cmdSecretsMigrate();
523
+ }
524
+ if (process.argv[3] === "vault-migrate") {
525
+ return cmdSecretsVaultMigrate();
526
+ }
527
+ if (process.argv[3] === "rotate") {
528
+ return cmdSecretsRotate();
529
+ }
530
+ if (process.argv[3] === "onecli") {
531
+ return cmdSecretsOneCli();
532
+ }
533
+ if (process.argv[3] === "purge-history") {
534
+ return cmdSecretsPurgeHistory();
535
+ }
536
+ if (process.argv[3] === "propagate") {
537
+ return cmdSecretsPropagateStandalone();
538
+ }
539
+ if (process.argv[3] === "revoke-old") {
540
+ return cmdSecretsRevokeOld();
541
+ }
542
+ const config = await loadConfig(resolveConfigPath());
543
+ if (!config.secrets) {
544
+ console.log(`${c.dim}No secrets configured in ${KIT_FILE}${c.reset}`);
545
+ return true;
546
+ }
547
+ const secretsConfig = config.secrets;
548
+ // ── S9: refuse prod-scoped keys outside prod env ──────────────────────────
549
+ // Each key's source/ref/vault_path is checked against a "prod" marker;
550
+ // if any prod-scoped key is configured AND the active env is not "prod",
551
+ // require explicit KIT_PROD_OK=1 to proceed (CI deploy jobs set this).
552
+ const { looksLikeProdKey, getActiveEnv, prodReadAllowed } = await import("./env-switch.js");
553
+ const activeEnv = await getActiveEnv(process.cwd());
554
+ const prodKeys = Object.entries(secretsConfig.keys ?? {}).filter(([, v]) => {
555
+ return (looksLikeProdKey(v.ref) ||
556
+ looksLikeProdKey(v.name) ||
557
+ looksLikeProdKey(v.vault_path));
558
+ });
559
+ if (prodKeys.length > 0 && !prodReadAllowed(activeEnv)) {
560
+ console.error(`${c.red}✗ Refusing to materialize ${prodKeys.length} prod-scoped key(s) — active env is "${activeEnv}".${c.reset}`);
561
+ for (const [name, v] of prodKeys.slice(0, 5)) {
562
+ const ref = v.ref ?? v.name ?? v.vault_path ?? "?";
563
+ console.error(` ${c.dim}•${c.reset} ${name} ${c.dim}${ref}${c.reset}`);
564
+ }
565
+ if (prodKeys.length > 5) {
566
+ console.error(` ${c.dim}… and ${prodKeys.length - 5} more${c.reset}`);
567
+ }
568
+ console.error(`\n${c.dim}To proceed: ${c.bold}kit env switch prod${c.reset}${c.dim} (interactive), or set ${c.bold}KIT_PROD_OK=1${c.reset}${c.dim} in CI.${c.reset}\n`);
569
+ return false;
570
+ }
571
+ console.log(`${c.bold}${c.cyan}Generating secrets...${c.reset} ${c.dim}(env=${activeEnv})${c.reset}\n`);
572
+ return await withGovernance(config, {
573
+ operation: "secrets.generate",
574
+ operationType: "write",
575
+ metadata: {
576
+ store: secretsConfig.store,
577
+ template: secretsConfig.template,
578
+ },
579
+ }, async () => {
580
+ const { results, written, fromTemplate } = await generateSecrets(secretsConfig);
581
+ let allOk = true;
582
+ for (const r of results) {
583
+ const icon = r.resolved
584
+ ? `${c.green}✓${c.reset}`
585
+ : `${c.red}✗${c.reset}`;
586
+ const status = r.resolved
587
+ ? `${c.green}resolved${c.reset}`
588
+ : `${c.red}missing${c.reset}`;
589
+ console.log(` ${icon} ${r.name} ${status} ${c.dim}${r.detail}${c.reset}`);
590
+ if (!r.resolved)
591
+ allOk = false;
592
+ }
593
+ if (written) {
594
+ const source = fromTemplate
595
+ ? `from ${secretsConfig.template}`
596
+ : "from keys";
597
+ console.log(`\n ${c.green}✓${c.reset} Wrote .env.local ${c.dim}(${source})${c.reset}`);
598
+ }
599
+ console.log();
600
+ return allOk;
601
+ });
602
+ }
603
+ async function cmdSkills() {
604
+ const config = await loadConfig(resolveConfigPath());
605
+ if (!config.skills) {
606
+ console.log(`${c.dim}No skills configured in ${KIT_FILE}${c.reset}`);
607
+ return true;
608
+ }
609
+ const registry = config.skills.registry ?? "clawhub";
610
+ console.log(`${c.bold}${c.cyan}Skills${c.reset} ${c.dim}(registry: ${registry})${c.reset}\n`);
611
+ const results = await checkSkills(config.skills);
612
+ if (results.length === 0) {
613
+ console.log(`${c.dim}No skills listed in ${KIT_FILE}${c.reset}`);
614
+ return true;
615
+ }
616
+ const nameWidth = Math.max(12, ...results.map((r) => r.name.length)) + 2;
617
+ for (const r of results) {
618
+ const icon = r.installed
619
+ ? `${c.green}✓${c.reset}`
620
+ : r.required
621
+ ? `${c.red}✗${c.reset}`
622
+ : `${c.yellow}!${c.reset}`;
623
+ const name = r.name + " ".repeat(Math.max(0, nameWidth - r.name.length));
624
+ const tag = r.required
625
+ ? `${c.dim}[required]${c.reset}`
626
+ : `${c.dim}[optional]${c.reset}`;
627
+ const status = r.installed
628
+ ? `${c.green}installed${c.reset}`
629
+ : `${c.red}missing${c.reset}`;
630
+ console.log(` ${icon} ${name} ${tag} ${status} ${c.dim}${r.versionSpec}${c.reset}`);
631
+ }
632
+ const missing = results.filter((r) => !r.installed);
633
+ if (missing.length > 0) {
634
+ console.log();
635
+ console.log(`${c.bold}To install missing skills:${c.reset}`);
636
+ for (const m of missing) {
637
+ console.log(` ${c.cyan}openclaw install ${registry}/${m.name}${c.reset}`);
638
+ }
639
+ }
640
+ console.log();
641
+ const requiredMissing = results.filter((r) => r.required && !r.installed);
642
+ return requiredMissing.length === 0;
643
+ }
644
+ async function cmdEscalate() {
645
+ const config = await loadConfig(resolveConfigPath());
646
+ return await withGovernance(config, {
647
+ operation: "escalate",
648
+ operationType: "read",
649
+ metadata: {},
650
+ }, async () => {
651
+ const toolResults = config.tools ? await checkTools(config.tools) : [];
652
+ const serviceResults = config.services
653
+ ? await checkServices(config.services)
654
+ : [];
655
+ const secretResults = config.secrets
656
+ ? await checkSecrets(config.secrets)
657
+ : { templateExists: null, keys: [] };
658
+ const items = collectEscalations(toolResults, serviceResults, secretResults.keys);
659
+ if (items.length === 0) {
660
+ console.log(`${c.green}All checks passed — nothing to escalate.${c.reset}`);
661
+ return true;
662
+ }
663
+ const message = formatEscalationMessage(items, process.cwd());
664
+ console.log(`${c.bold}${c.yellow}Escalation summary${c.reset}\n`);
665
+ console.log(message);
666
+ // Write to file for easy copy/paste or piping
667
+ const { writeFile } = await import("node:fs/promises");
668
+ const escalationPath = resolve(process.cwd(), ".kit-escalation.txt");
669
+ await writeFile(escalationPath, message, "utf-8");
670
+ console.log(`${c.dim}Written to ${escalationPath}${c.reset}`);
671
+ console.log(`${c.dim}Send this to the project owner for manual resolution.${c.reset}\n`);
672
+ return false;
673
+ });
674
+ }
675
+ async function cmdSetup() {
676
+ console.log(`${c.bold}${c.cyan}kit setup${c.reset}`);
677
+ console.log(`${c.dim}${"─".repeat(50)}${c.reset}\n`);
678
+ const config = await loadConfig(resolveConfigPath());
679
+ // Step 1: Install
680
+ console.log(`${c.bold}[1/6] Install${c.reset}`);
681
+ const installOk = await cmdInstall();
682
+ if (!installOk) {
683
+ console.log(`${c.red}Install failed — stopping setup.${c.reset}`);
684
+ console.log(`${c.dim}Fix the issues above and run ${c.reset}${c.bold}kit setup${c.reset}${c.dim} again.${c.reset}`);
685
+ return false;
686
+ }
687
+ // Step 2: Git Hooks
688
+ if (config.hooks && Object.keys(config.hooks).length > 0 && isGitRepository()) {
689
+ console.log(`${c.bold}[2/6] Git Hooks${c.reset}`);
690
+ await cmdHooks();
691
+ console.log();
692
+ }
693
+ // Step 3: Login
694
+ console.log(`${c.bold}[3/6] Login${c.reset}`);
695
+ const loginOk = await cmdLogin();
696
+ if (!loginOk) {
697
+ console.log(`${c.yellow}Some logins failed — continuing with secrets + verify.${c.reset}\n`);
698
+ }
699
+ // Step 4: Secrets
700
+ console.log(`${c.bold}[4/6] Secrets${c.reset}`);
701
+ const secretsOk = await cmdSecrets();
702
+ // Step 5: Agent config — teach the present agent(s) to use kit. Idempotent;
703
+ // only writes a managed block, so re-running setup leaves it unchanged.
704
+ console.log(`${c.bold}[5/6] Agent config${c.reset}`);
705
+ const agentResults = await writeAgentConfig();
706
+ for (const r of agentResults) {
707
+ if (r.action === "failed") {
708
+ console.log(` ${c.yellow}!${c.reset} ${r.agent}: ${r.detail}`);
709
+ }
710
+ else {
711
+ const mark = r.action === "unchanged" ? `${c.dim}=` : `${c.green}✓`;
712
+ console.log(` ${mark}${c.reset} ${r.agent} ${c.dim}→ ${r.file} (${r.action})${c.reset}`);
713
+ }
714
+ }
715
+ console.log();
716
+ // Step 6: Verify
717
+ console.log(`${c.bold}[6/6] Verify${c.reset}`);
718
+ const verifyOk = await cmdCheck();
719
+ const allOk = installOk && loginOk && secretsOk && verifyOk;
720
+ if (allOk) {
721
+ console.log(`${c.bold}${c.green}Setup complete — you're ready to go! ✓${c.reset}\n`);
722
+ }
723
+ else {
724
+ console.log(`${c.bold}${c.yellow}Setup finished with issues. Run ${c.reset}${c.bold}kit check${c.reset}${c.yellow} to see what's left.${c.reset}\n`);
725
+ }
726
+ return allOk;
727
+ }
728
+ async function cmdAgentConfig() {
729
+ console.log(`${c.bold}${c.cyan}kit agent-config${c.reset}`);
730
+ console.log(`${c.dim}${"─".repeat(50)}${c.reset}\n`);
731
+ const targets = detectAgentTargets();
732
+ console.log(`${c.dim}Teaching ${targets.map((t) => `${c.reset}${c.bold}${t.agent}${c.reset}${c.dim}`).join(", ")} to use kit ` +
733
+ `(managed block in their rules file).${c.reset}\n`);
734
+ const results = await writeAgentConfig();
735
+ let failed = false;
736
+ for (const r of results) {
737
+ if (r.action === "failed") {
738
+ failed = true;
739
+ console.log(` ${c.red}✗${c.reset} ${r.agent} (${r.file}): ${r.detail}`);
740
+ }
741
+ else {
742
+ const mark = r.action === "unchanged" ? `${c.dim}=` : `${c.green}✓`;
743
+ console.log(` ${mark}${c.reset} ${r.agent} ${c.dim}→ ${r.file} (${r.action})${c.reset}`);
744
+ }
745
+ }
746
+ console.log();
747
+ console.log(`${c.dim}The block is regenerated in place on re-run; edit outside the markers freely.${c.reset}`);
748
+ return !failed;
749
+ }
750
+ async function cmdGovernance() {
751
+ const config = await loadConfig(resolveConfigPath());
752
+ const governanceConfig = await mergeGovernanceConfigAsync(config.governance);
753
+ console.log(`${c.bold}${c.cyan}Governance Status${c.reset}`);
754
+ console.log(`${c.dim}${"─".repeat(50)}${c.reset}\n`);
755
+ // Display governance configuration
756
+ console.log(formatGovernanceStatus(governanceConfig));
757
+ console.log();
758
+ // Check revocation status
759
+ if (governanceConfig.revocation.enabled) {
760
+ const revoked = await checkRevocationStatus(config.governance);
761
+ const icon = revoked ? `${c.red}✗${c.reset}` : `${c.green}✓${c.reset}`;
762
+ const status = revoked ? `${c.red}REVOKED${c.reset}` : `${c.green}Active${c.reset}`;
763
+ console.log(` ${icon} Access Status: ${status}`);
764
+ }
765
+ // Display budget status
766
+ if (governanceConfig.agent.max_tokens_per_day || governanceConfig.agent.max_operations_per_hour) {
767
+ console.log();
768
+ const budgetStatus = await getBudgetStatus(config.governance);
769
+ console.log(formatBudgetStatus(budgetStatus));
770
+ }
771
+ // Display agent info
772
+ if (governanceConfig.agent.id || governanceConfig.agent.name) {
773
+ console.log();
774
+ console.log("Agent Information");
775
+ console.log("─".repeat(50));
776
+ if (governanceConfig.agent.name) {
777
+ console.log(`Name: ${governanceConfig.agent.name}`);
778
+ }
779
+ if (governanceConfig.agent.id) {
780
+ console.log(`ID: ${governanceConfig.agent.id}`);
781
+ }
782
+ }
783
+ console.log();
784
+ return true;
785
+ }
786
+ async function cmdUpgrade() {
787
+ console.log(`${c.bold}${c.cyan}kit upgrade${c.reset}`);
788
+ console.log(`${c.dim}${"─".repeat(50)}${c.reset}\n`);
789
+ const config = await loadConfig(resolveConfigPath());
790
+ // Update skills lock
791
+ const skills = {};
792
+ if (config.skills?.required) {
793
+ Object.assign(skills, config.skills.required);
794
+ }
795
+ if (config.skills?.optional) {
796
+ Object.assign(skills, config.skills.optional);
797
+ }
798
+ const kitMeta = await readkitMeta();
799
+ await updateSkillsLock(skills, kitMeta?.name ? `${kitMeta.name}@${kitMeta.version}` : undefined);
800
+ // Update CLI lock
801
+ const tools = {};
802
+ if (config.tools) {
803
+ for (const [name, version] of Object.entries(config.tools)) {
804
+ tools[name] = { version, source: "mise" };
805
+ }
806
+ }
807
+ await updateCliLock(tools);
808
+ console.log(`${c.green}✓${c.reset} Updated lock files from .kit.toml\n`);
809
+ console.log(`${c.dim}Run ${c.reset}${c.bold}kit install${c.reset}${c.dim} to install updated tools${c.reset}`);
810
+ console.log(`${c.dim}Run ${c.reset}${c.bold}kit skills${c.reset}${c.dim} to check skill status${c.reset}\n`);
811
+ return true;
812
+ }
813
+ /**
814
+ * Generate a `.kit.toml` when none exists: detect stack, surface plaintext
815
+ * secrets, pick a secret backend, preview, and write. Returns "written" on
816
+ * success, "abort-error" when config can't be generated (low-confidence +
817
+ * non-interactive), or "abort-user" when the user declines the write.
818
+ */
819
+ async function generateConfigFile(configPath, nonInteractive) {
820
+ console.log(`${c.yellow}No .kit.toml found.${c.reset}\n`);
821
+ const stack = await runStep("detect project stack", () => detectStack(process.cwd()));
822
+ if (stack.confidence < 0.3 && nonInteractive) {
823
+ console.error(`${c.red}✗ Stack detection confidence too low (${(stack.confidence * 100).toFixed(0)}%) — cannot auto-generate config in non-interactive mode.${c.reset}`);
824
+ console.error(`${c.dim}Create a .kit.toml manually or run 'kit init' interactively.${c.reset}`);
825
+ return "abort-error";
826
+ }
827
+ const detectedLabel = [
828
+ stack.language !== "unknown" ? stack.language : null,
829
+ stack.framework ?? null,
830
+ stack.services.length ? stack.services.join(", ") : null,
831
+ ]
832
+ .filter(Boolean)
833
+ .join(" / ");
834
+ if (stack.language === "unknown") {
835
+ console.log(`${c.dim}Could not detect project type — generating minimal config.${c.reset}\n`);
836
+ }
837
+ else {
838
+ console.log(` ${c.green}✓${c.reset} Detected: ${c.bold}${detectedLabel}${c.reset} ${c.dim}(confidence: ${(stack.confidence * 100).toFixed(0)}%)${c.reset}\n`);
839
+ }
840
+ // ── Plaintext-secrets scan ─────────────────────────────────────────────
841
+ // Surface obvious credentials sitting in .env / package.json / scripts/
842
+ // BEFORE wiring up the vault, so the choice of backend is informed.
843
+ const plaintextHits = await scanPlaintextSecrets(process.cwd());
844
+ if (plaintextHits.length > 0) {
845
+ const totalFindings = plaintextHits.reduce((sum, h) => sum + h.findings.length, 0);
846
+ console.log(`${c.red}⚠ Found ${totalFindings} plaintext secret(s) across ${plaintextHits.length} file(s):${c.reset}`);
847
+ for (const hit of plaintextHits) {
848
+ const labels = hit.findings.map((f) => `${f.label}:${f.preview}`).join(", ");
849
+ console.log(` ${c.dim}•${c.reset} ${hit.file} ${c.dim}${labels}${c.reset}`);
850
+ }
851
+ console.log(`${c.yellow}These should be migrated to a vault. ` +
852
+ `Pick a secret backend below; we'll wire up the config now and you can run ` +
853
+ `${c.bold}kit secrets migrate${c.reset}${c.yellow} after.${c.reset}\n`);
854
+ }
855
+ // ── Secret-backend choice (interactive) ────────────────────────────────
856
+ let chosenStore = "1password";
857
+ if (!nonInteractive) {
858
+ chosenStore = (await promptSelect("Secret backend?", [
859
+ { value: "1password", label: "1Password", hint: "interactive signin via op CLI", recommended: true },
860
+ { value: "infisical", label: "Infisical", hint: "self-hosted or cloud, token-based" },
861
+ { value: "vault", label: "HashiCorp Vault", hint: "KV v2 paths" },
862
+ { value: "aws-sm", label: "AWS Secrets Manager", hint: "IAM credentials required" },
863
+ { value: "gcp-sm", label: "GCP Secret Manager", hint: "gcloud auth required" },
864
+ { value: "azure-kv", label: "Azure Key Vault", hint: "az login required" },
865
+ { value: "doppler", label: "Doppler", hint: "doppler login required" },
866
+ { value: "bitwarden", label: "Bitwarden", hint: "bw login + unlock required" },
867
+ { value: "env", label: "env (no vault)", hint: "not recommended — use only for local dev" },
868
+ ]));
869
+ console.log();
870
+ }
871
+ const tomlContent = generateToml(stack, { secretsStore: chosenStore });
872
+ // Show diff preview
873
+ console.log(`${c.bold}Preview — .kit.toml${c.reset}\n`);
874
+ for (const line of tomlContent.split("\n")) {
875
+ if (line.trim() === "") {
876
+ console.log();
877
+ }
878
+ else {
879
+ console.log(` ${c.green}+${c.reset} ${line}`);
880
+ }
881
+ }
882
+ console.log();
883
+ if (!nonInteractive) {
884
+ // Prompt with 5-second default-yes timeout
885
+ const accepted = await promptConfirm("Write this config? [Y/n] (auto-yes in 5s): ", 5000);
886
+ if (!accepted) {
887
+ console.log(`${c.dim}Aborted. Create .kit.toml manually when ready.${c.reset}`);
888
+ return "abort-user";
889
+ }
890
+ }
891
+ await writeFile(configPath, tomlContent, "utf-8");
892
+ console.log(` ${c.green}✓${c.reset} Generated ${c.bold}.kit.toml${c.reset}\n`);
893
+ return "written";
894
+ }
895
+ async function cmdInit() {
896
+ console.log(`${c.bold}${c.cyan}kit init${c.reset}`);
897
+ console.log(`${c.dim}${"─".repeat(50)}${c.reset}\n`);
898
+ const nonInteractive = hasFlag(process.argv, "--non-interactive") || hasFlag(process.argv, "--yes") || hasFlag(process.argv, "-y");
899
+ const configPath = resolveConfigPath();
900
+ // ── Auto-generate .kit.toml if absent ──────────────────────────────────
901
+ let configMissing = false;
902
+ try {
903
+ await access(configPath);
904
+ }
905
+ catch {
906
+ configMissing = true;
907
+ }
908
+ if (configMissing) {
909
+ const result = await generateConfigFile(configPath, nonInteractive);
910
+ if (result === "abort-error")
911
+ return false; // can't generate (low confidence + non-interactive)
912
+ if (result === "abort-user")
913
+ return true; // user declined — exit 0, not an error
914
+ }
915
+ const config = await loadConfig(configPath);
916
+ // Check if lock files exist
917
+ const kitMeta = await readkitMeta();
918
+ const skillsLock = await readSkillsLock();
919
+ const cliLock = await readCliLock();
920
+ if (kitMeta) {
921
+ console.log(`${c.dim}kit: ${kitMeta.name}@${kitMeta.version}${c.reset}\n`);
922
+ }
923
+ if (!skillsLock && !cliLock) {
924
+ console.log(`${c.yellow}No lock files found.${c.reset}`);
925
+ console.log(`${c.dim}Generating lock files from .kit.toml...${c.reset}\n`);
926
+ // Generate lock files from config
927
+ await runStep("generate lock files", async () => {
928
+ const skills = {};
929
+ if (config.skills?.required) {
930
+ Object.assign(skills, config.skills.required);
931
+ }
932
+ if (config.skills?.optional) {
933
+ Object.assign(skills, config.skills.optional);
934
+ }
935
+ await updateSkillsLock(skills, kitMeta?.name ? `${kitMeta.name}@${kitMeta.version}` : undefined);
936
+ const tools = {};
937
+ if (config.tools) {
938
+ for (const [name, version] of Object.entries(config.tools)) {
939
+ tools[name] = { version, source: "mise" };
940
+ }
941
+ }
942
+ await updateCliLock(tools);
943
+ });
944
+ console.log();
945
+ }
946
+ // Now run the setup process
947
+ const setupOk = await cmdSetup();
948
+ // First-install hook: offer a global prescan against an operator-chosen
949
+ // root (typically ~/projects). Asked once, marker written to
950
+ // ~/.kit/first-install-prompted so re-runs of `kit init` (per
951
+ // additional repos) don't re-ask. Honors --non-interactive/--yes/-y.
952
+ if (setupOk && !nonInteractive) {
953
+ await offerFirstInstallPrescan();
954
+ }
955
+ return setupOk;
956
+ }
957
+ async function offerFirstInstallPrescan() {
958
+ const homedir = (await import("node:os")).homedir();
959
+ const markerDir = join(homedir, ".kit");
960
+ const markerPath = join(markerDir, "first-install-prompted");
961
+ try {
962
+ await access(markerPath);
963
+ return; // Already prompted on this machine.
964
+ }
965
+ catch {
966
+ // Marker absent — proceed with prompt.
967
+ }
968
+ if (!process.stdin.isTTY)
969
+ return;
970
+ console.log();
971
+ console.log(`${c.bold}${c.cyan}First-install: multi-repo prescan${c.reset}`);
972
+ console.log(`${c.dim}${"─".repeat(50)}${c.reset}`);
973
+ console.log(`${c.dim}kit can scan every git repo under a directory for leaked secrets,`);
974
+ console.log(`${c.dim}.gitignore holes, public-repo + credential combos, and CVE-deps.${c.reset}\n`);
975
+ const readline = await import("node:readline/promises");
976
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
977
+ let answer = "";
978
+ let scanPath = "";
979
+ try {
980
+ answer = (await rl.question(`Run a prescan now? [y/N] `)).trim().toLowerCase();
981
+ if (answer === "y" || answer === "yes") {
982
+ const defaultPath = join(homedir, "projects");
983
+ scanPath = (await rl.question(`Path to scan [${defaultPath}]: `)).trim() || defaultPath;
984
+ }
985
+ }
986
+ finally {
987
+ rl.close();
988
+ }
989
+ // Always write marker (even if user declined) — don't re-pester.
990
+ await mkdir(markerDir, { recursive: true, mode: 0o700 });
991
+ await writeFile(markerPath, new Date().toISOString() + "\n", { encoding: "utf-8", mode: 0o600 });
992
+ if (answer !== "y" && answer !== "yes") {
993
+ console.log(`${c.dim}Skipped. Run later with ${c.bold}kit security prescan <path>${c.reset}${c.dim}.${c.reset}\n`);
994
+ return;
995
+ }
996
+ console.log(`${c.dim}Running prescan against ${scanPath}…${c.reset}\n`);
997
+ const { runPrescan } = await import("./security-prescan.js");
998
+ const report = await runPrescan({ root: resolve(scanPath), deep: false });
999
+ const bySev = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
1000
+ for (const f of report.findings)
1001
+ bySev[f.severity] = (bySev[f.severity] ?? 0) + 1;
1002
+ console.log(`${c.bold}Scanned${c.reset} ${report.repoCount} repo(s), ${report.findings.length} finding(s):`);
1003
+ if (bySev.critical)
1004
+ console.log(` ${c.red}critical${c.reset}: ${bySev.critical}`);
1005
+ if (bySev.high)
1006
+ console.log(` ${c.red}high ${c.reset}: ${bySev.high}`);
1007
+ if (bySev.medium)
1008
+ console.log(` ${c.yellow}medium ${c.reset}: ${bySev.medium}`);
1009
+ if (bySev.low)
1010
+ console.log(` ${c.dim}low ${c.reset}: ${bySev.low}`);
1011
+ if (report.summaryPath) {
1012
+ console.log(`\n${c.dim}Full report:${c.reset} ${report.summaryPath}`);
1013
+ }
1014
+ console.log();
1015
+ }
1016
+ /** Built-in pre-commit hook: scans staged files for known credential patterns. */
1017
+ const BUILTIN_HOOKS = {
1018
+ "secret-scan": {
1019
+ hookName: "pre-commit",
1020
+ commands: ["kit security scan-staged"],
1021
+ description: "Block commits that stage known credential patterns (Stripe, AWS, JWT, etc).",
1022
+ },
1023
+ "post-pull-audit": {
1024
+ hookName: "post-merge",
1025
+ commands: ["kit security verify-pull"],
1026
+ description: "After git pull/merge, audit new deps, gitignore drops, and introduced secrets.",
1027
+ },
1028
+ };
1029
+ async function cmdHooks() {
1030
+ const subcommand = process.argv[3];
1031
+ // Built-in hooks bypass the .kit.toml config path so they're available
1032
+ // on any repo, including ones that haven't run `kit init` yet.
1033
+ if (subcommand === "add") {
1034
+ return cmdHooksAdd();
1035
+ }
1036
+ const config = await loadConfig(resolveConfigPath());
1037
+ if (!config.hooks || Object.keys(config.hooks).length === 0) {
1038
+ console.log(`${c.dim}No hooks configured in ${KIT_FILE}${c.reset}`);
1039
+ return true;
1040
+ }
1041
+ if (!isGitRepository()) {
1042
+ console.log(`${c.red}Not a git repository${c.reset}`);
1043
+ return false;
1044
+ }
1045
+ if (subcommand === "install" || !subcommand) {
1046
+ console.log(`${c.bold}${c.cyan}Installing git hooks...${c.reset}\n`);
1047
+ const results = await installHooks(config.hooks);
1048
+ let allOk = true;
1049
+ for (const r of results) {
1050
+ const icon = r.action === "failed"
1051
+ ? `${c.red}✗${c.reset}`
1052
+ : `${c.green}✓${c.reset}`;
1053
+ const label = r.action === "installed"
1054
+ ? `${c.green}installed${c.reset}`
1055
+ : r.action === "updated"
1056
+ ? `${c.green}updated${c.reset}`
1057
+ : r.action === "skipped"
1058
+ ? `${c.dim}skipped${c.reset}`
1059
+ : `${c.red}failed${c.reset}`;
1060
+ console.log(` ${icon} ${r.hookName} ${label} ${c.dim}${r.detail}${c.reset}`);
1061
+ if (r.action === "failed")
1062
+ allOk = false;
1063
+ }
1064
+ console.log();
1065
+ return allOk;
1066
+ }
1067
+ else if (subcommand === "check") {
1068
+ console.log(`${c.bold}${c.cyan}Git Hooks${c.reset}\n`);
1069
+ const results = await checkHooks(config.hooks);
1070
+ let allOk = true;
1071
+ for (const r of results) {
1072
+ const icon = !r.installed
1073
+ ? `${c.red}✗${c.reset}`
1074
+ : !r.upToDate
1075
+ ? `${c.yellow}!${c.reset}`
1076
+ : `${c.green}✓${c.reset}`;
1077
+ const status = !r.installed
1078
+ ? `${c.red}not installed${c.reset}`
1079
+ : !r.upToDate
1080
+ ? `${c.yellow}outdated${c.reset}`
1081
+ : `${c.green}up-to-date${c.reset}`;
1082
+ console.log(` ${icon} ${r.hookName} ${status} ${c.dim}${r.detail}${c.reset}`);
1083
+ if (!r.installed || !r.upToDate)
1084
+ allOk = false;
1085
+ }
1086
+ if (!allOk) {
1087
+ console.log(`\n${c.dim}Run ${c.reset}${c.bold}kit hooks install${c.reset}${c.dim} to install/update hooks${c.reset}`);
1088
+ }
1089
+ console.log();
1090
+ return allOk;
1091
+ }
1092
+ else {
1093
+ console.error(`Unknown hooks subcommand: ${subcommand}`);
1094
+ console.error(`Usage: kit hooks [install|check|add <name>]`);
1095
+ return false;
1096
+ }
1097
+ }
1098
+ async function cmdHooksAdd() {
1099
+ const name = process.argv[4];
1100
+ if (!name) {
1101
+ console.error(`${c.red}Usage: kit hooks add <name>${c.reset}\n${c.dim}Available built-in hooks:${c.reset}`);
1102
+ for (const [k, v] of Object.entries(BUILTIN_HOOKS)) {
1103
+ console.error(` ${c.bold}${k}${c.reset} (git ${v.hookName}) — ${v.description}`);
1104
+ }
1105
+ return false;
1106
+ }
1107
+ const builtin = BUILTIN_HOOKS[name];
1108
+ if (!builtin) {
1109
+ console.error(`${c.red}No built-in hook named "${name}"${c.reset}`);
1110
+ console.error(`${c.dim}Available: ${Object.keys(BUILTIN_HOOKS).join(", ")}${c.reset}`);
1111
+ return false;
1112
+ }
1113
+ if (!isGitRepository()) {
1114
+ console.error(`${c.red}Not a git repository${c.reset}`);
1115
+ return false;
1116
+ }
1117
+ console.log(`${c.bold}${c.cyan}kit hooks add ${name}${c.reset} ${c.dim}(git ${builtin.hookName})${c.reset}`);
1118
+ console.log(`${c.dim}${"─".repeat(50)}${c.reset}\n`);
1119
+ // Check whether the target hook is already present and contains a kit-
1120
+ // managed step. If so, just confirm without re-writing.
1121
+ const { hooksDir } = await ensureHooksDir();
1122
+ const hookPath = `${hooksDir}/${builtin.hookName}`;
1123
+ const { existsSync, readFileSync } = await import("node:fs");
1124
+ if (existsSync(hookPath)) {
1125
+ const existing = readFileSync(hookPath, "utf-8");
1126
+ if (existing.includes(builtin.commands[0])) {
1127
+ console.log(` ${c.green}✓${c.reset} ${builtin.hookName} already has ${c.bold}${builtin.commands[0]}${c.reset}\n`);
1128
+ return true;
1129
+ }
1130
+ console.log(`${c.yellow}⚠ ${builtin.hookName} already exists at ${hookPath}.${c.reset}`);
1131
+ console.log(`${c.dim}kit will overwrite it with a managed version that calls back into kit.${c.reset}\n`);
1132
+ if (!isNonInteractive()) {
1133
+ const ok = await promptConfirm(`Overwrite? [Y/n] (auto-yes in 8s): `, 8000);
1134
+ if (!ok) {
1135
+ console.log(`${c.dim}Aborted.${c.reset}`);
1136
+ return false;
1137
+ }
1138
+ }
1139
+ }
1140
+ const results = await installHooks({ [builtin.hookName]: builtin.commands });
1141
+ const r = results[0];
1142
+ if (r.action === "failed") {
1143
+ console.error(`${c.red}✗ install failed: ${r.detail}${c.reset}`);
1144
+ return false;
1145
+ }
1146
+ console.log(` ${c.green}✓${c.reset} ${builtin.hookName} ${c.green}${r.action}${c.reset} ${c.dim}${r.detail}${c.reset}`);
1147
+ console.log(`\n${c.dim}Test by staging a file with a fake credential (e.g. ${c.bold}sk_${"test"}_${"A".repeat(20)}${c.reset}${c.dim}) and running ${c.bold}git commit${c.reset}${c.dim} — the commit should be blocked.${c.reset}\n`);
1148
+ return true;
1149
+ }
1150
+ async function ensureHooksDir() {
1151
+ const { resolve } = await import("node:path");
1152
+ return { hooksDir: resolve(process.cwd(), ".git", "hooks") };
1153
+ }
1154
+ /**
1155
+ * `kit mcp <list|status|auth|set-token|clear>` — declarative
1156
+ * MCP-server registry. The block in .kit.toml [mcp.<name>] declares
1157
+ * which MCPs the project intends to use; this command surfaces auth-state
1158
+ * and stages tokens for the kit-plugins to consume.
1159
+ *
1160
+ * Browser-OAuth flow itself is delegated — `kit mcp auth <name>`
1161
+ * prints the vendor's authorization URL and asks the operator to paste
1162
+ * back the callback URL (same pattern as the Sentry MCP auth flow this
1163
+ * session walked through). `kit mcp set-token <name>` is the
1164
+ * headless alternative for CI: read access-token from an env var.
1165
+ */
1166
+ async function cmdMcp() {
1167
+ const sub = process.argv[3];
1168
+ // An MCP client launches `kit mcp` and speaks the stdio transport — no
1169
+ // sub-command and a non-TTY stdin. Start the server in that case. Interactive
1170
+ // use (TTY) or any explicit sub falls through to the orchestrator below.
1171
+ if (!sub && !process.stdin.isTTY) {
1172
+ await startMcpServer();
1173
+ return true;
1174
+ }
1175
+ const config = await loadConfig(resolveConfigPath()).catch(() => null);
1176
+ const mcpConfig = config?.mcp;
1177
+ const { statusAll, statusForMcp, clearMcpToken, storeStaticToken, } = await import("./mcp-orchestrator.js");
1178
+ if (!sub || sub === "list" || sub === "status") {
1179
+ console.log(`${c.bold}${c.cyan}kit mcp${c.reset}`);
1180
+ console.log(`${c.dim}${"─".repeat(50)}${c.reset}\n`);
1181
+ const entries = await statusAll(mcpConfig);
1182
+ if (entries.length === 0) {
1183
+ console.log(`${c.dim}No [mcp.*] blocks declared in .kit.toml${c.reset}`);
1184
+ console.log(`${c.dim}Add e.g. [mcp.sentry] scopes = ["org:read", "project:write"]${c.reset}`);
1185
+ return true;
1186
+ }
1187
+ for (const e of entries) {
1188
+ const color = e.status === "ok"
1189
+ ? c.green
1190
+ : e.status === "missing" || e.status === "expired"
1191
+ ? c.yellow
1192
+ : c.red;
1193
+ const marker = e.status === "ok" ? "✓" : e.status === "missing" ? "?" : "✗";
1194
+ const scopeStr = e.declared?.scopes ? ` ${c.dim}[${e.declared.scopes.join(", ")}]${c.reset}` : "";
1195
+ console.log(` ${color}${marker}${c.reset} ${e.name.padEnd(14)} ${color}${e.status}${c.reset}${scopeStr}`);
1196
+ if (e.detail)
1197
+ console.log(` ${c.dim}${e.detail}${c.reset}`);
1198
+ }
1199
+ console.log();
1200
+ return entries.every((e) => e.status === "ok" || e.status === "missing");
1201
+ }
1202
+ if (sub === "auth") {
1203
+ const name = process.argv[4];
1204
+ if (!name) {
1205
+ console.error(`${c.red}Usage: kit mcp auth <name>${c.reset}`);
1206
+ return false;
1207
+ }
1208
+ const declared = mcpConfig?.[name];
1209
+ if (!declared) {
1210
+ console.error(`${c.red}No [mcp.${name}] block in .kit.toml${c.reset}`);
1211
+ return false;
1212
+ }
1213
+ // For now we surface vendor-specific guidance; full OAuth flow is
1214
+ // delegated to the operator (paste callback URL). When the vendor's
1215
+ // MCP server publishes a stable /authorize endpoint we can fully
1216
+ // automate.
1217
+ const authUrl = declared.url ?? `https://mcp.${name}.dev`;
1218
+ console.log(`${c.bold}Authorize kit for MCP server "${name}"${c.reset}`);
1219
+ console.log(`${c.dim}Vendor URL: ${authUrl}${c.reset}`);
1220
+ console.log(`${c.dim}Required scopes: ${(declared.scopes ?? ["(none declared)"]).join(", ")}${c.reset}\n`);
1221
+ console.log(`${c.yellow}OAuth-flow not yet automated for "${name}". Two options:${c.reset}`);
1222
+ console.log(` 1. Set env var: ${c.bold}kit mcp set-token ${name} --from-env <VAR>${c.reset}`);
1223
+ console.log(` 2. Paste token: ${c.bold}kit mcp set-token ${name} --paste${c.reset}`);
1224
+ return true;
1225
+ }
1226
+ if (sub === "set-token") {
1227
+ const name = process.argv[4];
1228
+ if (!name) {
1229
+ console.error(`${c.red}Usage: kit mcp set-token <name> [--from-env VAR | --paste]${c.reset}`);
1230
+ return false;
1231
+ }
1232
+ const args = process.argv.slice(5);
1233
+ const fromEnvIdx = args.indexOf("--from-env");
1234
+ let accessToken;
1235
+ if (fromEnvIdx >= 0 && args[fromEnvIdx + 1]) {
1236
+ const envVar = args[fromEnvIdx + 1];
1237
+ accessToken = process.env[envVar];
1238
+ if (!accessToken) {
1239
+ console.error(`${c.red}Env var ${envVar} is empty${c.reset}`);
1240
+ return false;
1241
+ }
1242
+ }
1243
+ else if (hasFlag(args, "--paste")) {
1244
+ const readline = await import("node:readline/promises");
1245
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1246
+ try {
1247
+ accessToken = (await rl.question(`Paste access token for "${name}": `)).trim();
1248
+ }
1249
+ finally {
1250
+ rl.close();
1251
+ }
1252
+ }
1253
+ else {
1254
+ console.error(`${c.red}Missing --from-env <VAR> or --paste${c.reset}`);
1255
+ return false;
1256
+ }
1257
+ const declared = mcpConfig?.[name];
1258
+ await storeStaticToken(name, accessToken, {
1259
+ scopes: declared?.scopes,
1260
+ });
1261
+ console.log(`${c.green}✓${c.reset} Token stored for ${name} in ~/.kit/mcp-tokens.json (chmod 0o600)`);
1262
+ const status = await statusForMcp(name, declared ?? null);
1263
+ console.log(`${c.dim}Status: ${status.status}${c.reset}`);
1264
+ return true;
1265
+ }
1266
+ if (sub === "clear") {
1267
+ const name = process.argv[4];
1268
+ if (!name) {
1269
+ console.error(`${c.red}Usage: kit mcp clear <name>${c.reset}`);
1270
+ return false;
1271
+ }
1272
+ await clearMcpToken(name);
1273
+ console.log(`${c.green}✓${c.reset} Cleared token for ${name}`);
1274
+ return true;
1275
+ }
1276
+ console.error(`${c.red}Usage: kit mcp [list | status | auth <name> | set-token <name> | clear <name>]${c.reset}`);
1277
+ return false;
1278
+ }
1279
+ async function cmdAuth() {
1280
+ const sub = process.argv[3];
1281
+ if (sub === "elevate")
1282
+ return cmdAuthElevate();
1283
+ if (sub === "status")
1284
+ return cmdAuthStatus();
1285
+ if (sub === "revoke")
1286
+ return cmdAuthRevoke();
1287
+ if (sub === "setup-totp")
1288
+ return cmdAuthSetupTotp();
1289
+ console.error(`${c.red}Usage: kit auth [elevate | status | revoke | setup-totp]${c.reset}`);
1290
+ return false;
1291
+ }
1292
+ async function cmdAuthSetupTotp() {
1293
+ // kit auth setup-totp [--issuer <name>] [--account <user@host>] [--overwrite]
1294
+ const args = process.argv.slice(4);
1295
+ const issuerIdx = args.indexOf("--issuer");
1296
+ const accountIdx = args.indexOf("--account");
1297
+ const overwrite = hasFlag(args, "--overwrite");
1298
+ const issuer = issuerIdx >= 0 ? args[issuerIdx + 1] : "kit";
1299
+ const defaultAccount = `${process.env.USER ?? "user"}@${process.env.HOSTNAME ?? "host"}`;
1300
+ const accountName = accountIdx >= 0 ? args[accountIdx + 1] : defaultAccount;
1301
+ console.log(`${c.bold}${c.cyan}kit auth setup-totp${c.reset}`);
1302
+ console.log(`${c.dim}${"─".repeat(50)}${c.reset}\n`);
1303
+ let result;
1304
+ try {
1305
+ result = await enrollTotp({ accountName, issuer, overwrite });
1306
+ }
1307
+ catch (err) {
1308
+ const msg = err instanceof Error ? err.message : String(err);
1309
+ console.error(`${c.red}✗ ${msg}${c.reset}`);
1310
+ console.error(`${c.dim}If you intentionally want to replace the existing secret, re-run with ${c.bold}--overwrite${c.reset}${c.dim} (your old authenticator entry will stop working).${c.reset}\n`);
1311
+ return false;
1312
+ }
1313
+ console.log(` ${c.green}✓${c.reset} secret written to ${result.filePath} ${c.dim}(chmod 600)${c.reset}\n`);
1314
+ console.log(`${c.bold}Provisioning URI:${c.reset}`);
1315
+ console.log(` ${c.dim}${result.uri}${c.reset}\n`);
1316
+ console.log(`${c.bold}Or enter the secret manually in your authenticator app:${c.reset}`);
1317
+ console.log(` ${c.bold}Secret:${c.reset} ${result.secret}`);
1318
+ console.log(` ${c.bold}Account:${c.reset} ${accountName}`);
1319
+ console.log(` ${c.bold}Issuer:${c.reset} ${issuer}\n`);
1320
+ console.log(`${c.bold}Verify enrollment — the next 6-digit code should be:${c.reset}`);
1321
+ console.log(` ${c.bold}${result.currentCode}${c.reset} ${c.dim}(or the one shown in your authenticator right now)${c.reset}\n`);
1322
+ console.log(`${c.dim}Future ${c.bold}kit auth elevate${c.reset}${c.dim} runs will prompt for the TOTP code automatically.${c.reset}`);
1323
+ console.log(`${c.dim}Override with ${c.bold}KIT_TOTP_SECRET=...${c.reset}${c.dim} in CI / scripted contexts.${c.reset}\n`);
1324
+ return true;
1325
+ }
1326
+ async function cmdAuthElevate() {
1327
+ const args = process.argv.slice(4);
1328
+ const scope = flagValue(args, "--scope") ?? "all";
1329
+ const ttlIdx = args.indexOf("--ttl-minutes");
1330
+ if (ttlIdx >= 0 && args[ttlIdx + 1]) {
1331
+ const n = parseInt(args[ttlIdx + 1], 10);
1332
+ if (Number.isFinite(n) && n > 0 && n <= 240) {
1333
+ process.env.KIT_ELEVATION_TTL_MINUTES = String(n);
1334
+ }
1335
+ }
1336
+ console.log(`${c.bold}${c.cyan}kit auth elevate${c.reset} ${c.dim}(scope=${scope})${c.reset}`);
1337
+ console.log(`${c.dim}${"─".repeat(50)}${c.reset}\n`);
1338
+ if (isNonInteractive()) {
1339
+ console.error(`${c.red}✗ Elevation requires an interactive TTY. Cannot run from agent / CI.${c.reset}`);
1340
+ console.error(`${c.dim}For CI deploy jobs that legitimately need a destructive op, set ${c.bold}KIT_ELEVATED=1${c.reset}${c.dim} (and audit-log the use).${c.reset}\n`);
1341
+ return false;
1342
+ }
1343
+ // Resolve from env first, then ~/.kit/totp-secret (created by
1344
+ // `kit auth setup-totp`).
1345
+ const totpSecret = await resolveTotpSecret();
1346
+ let method = "yes-prompt";
1347
+ if (totpSecret) {
1348
+ method = "totp";
1349
+ const readline = await import("node:readline/promises");
1350
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1351
+ try {
1352
+ const code = (await rl.question(`Enter 6-digit TOTP from your authenticator: `)).trim();
1353
+ if (!verifyTotp(code, totpSecret)) {
1354
+ console.error(`${c.red}✗ Invalid TOTP code.${c.reset}`);
1355
+ return false;
1356
+ }
1357
+ }
1358
+ finally {
1359
+ rl.close();
1360
+ }
1361
+ }
1362
+ else {
1363
+ const ok = await promptConfirm(`Confirm elevation [type YES, default no in 15s]: `, 15_000, false);
1364
+ if (!ok) {
1365
+ console.log(`${c.dim}Aborted.${c.reset}`);
1366
+ return false;
1367
+ }
1368
+ }
1369
+ const state = await grantElevation(scope, method);
1370
+ console.log(` ${c.green}✓${c.reset} elevated ${c.dim}scope=${state.scope} method=${state.method} expires=${state.expiresAt}${c.reset}`);
1371
+ console.log(`\n${c.dim}Destructive secret ops (rotate, migrate, onecli register, propagate) are unlocked for ${elevationTtlMinutes()}m. Run ${c.bold}kit auth revoke${c.reset}${c.dim} to drop early.${c.reset}\n`);
1372
+ return true;
1373
+ }
1374
+ async function cmdAuthStatus() {
1375
+ const state = await readElevation(process.cwd());
1376
+ if (!state) {
1377
+ console.log(`${c.dim}Not elevated.${c.reset}`);
1378
+ return true;
1379
+ }
1380
+ const expires = Date.parse(state.expiresAt);
1381
+ const valid = Number.isFinite(expires) && expires > Date.now();
1382
+ const status = valid
1383
+ ? `${c.green}active${c.reset} ${c.dim}(expires ${state.expiresAt})${c.reset}`
1384
+ : `${c.red}expired${c.reset} ${c.dim}(at ${state.expiresAt})${c.reset}`;
1385
+ console.log(`${status} scope=${state.scope} method=${state.method} granter=${state.granter}`);
1386
+ return true;
1387
+ }
1388
+ async function cmdAuthRevoke() {
1389
+ await clearElevation(process.cwd());
1390
+ console.log(`${c.green}✓${c.reset} elevation marker cleared.`);
1391
+ return true;
1392
+ }
1393
+ async function cmdAuditSecrets() {
1394
+ const args = process.argv.slice(4); // after "audit secrets"
1395
+ const sinceIdx = args.indexOf("--since-days");
1396
+ const sinceDays = sinceIdx >= 0 && args[sinceIdx + 1] ? parseInt(args[sinceIdx + 1], 10) : 30;
1397
+ const keyFilter = flagValue(args, "--key");
1398
+ const jsonMode = hasFlag(args, "--json");
1399
+ const events = await readSecretAuditEvents(process.cwd(), sinceDays);
1400
+ const { reports, unattributed } = groupBySecret(events);
1401
+ const filteredReports = keyFilter
1402
+ ? reports.filter((r) => r.key === keyFilter)
1403
+ : reports;
1404
+ const summary = summarize(filteredReports, sinceDays);
1405
+ if (jsonMode) {
1406
+ console.log(JSON.stringify({ summary, reports: filteredReports, unattributed }, null, 2));
1407
+ return true;
1408
+ }
1409
+ console.log(`${c.bold}${c.cyan}kit audit secrets${c.reset} ${c.dim}(last ${sinceDays}d)${c.reset}`);
1410
+ console.log(`${c.dim}${"─".repeat(50)}${c.reset}\n`);
1411
+ if (events.length === 0) {
1412
+ console.log(`${c.dim}No secret-related events in audit log (.kit-audit.jsonl).${c.reset}\n`);
1413
+ return true;
1414
+ }
1415
+ console.log(`${c.bold}${summary.totalEvents}${c.reset} event(s) across ${c.bold}${summary.keyCount}${c.reset} key(s)`);
1416
+ if (summary.topKey) {
1417
+ console.log(`${c.dim}Most touched: ${c.bold}${summary.topKey.key}${c.reset}${c.dim} (${summary.topKey.count} events)${c.reset}`);
1418
+ }
1419
+ console.log();
1420
+ for (const r of filteredReports.slice(0, 20)) {
1421
+ console.log(`${c.bold}${r.key}${c.reset} ${c.dim}(${r.events.length} events)${c.reset}`);
1422
+ for (const e of r.events.slice(-10)) {
1423
+ const icon = e.success ? `${c.green}✓${c.reset}` : `${c.red}✗${c.reset}`;
1424
+ const ts = e.timestamp.slice(0, 19);
1425
+ const agent = e.agent ? `[${e.agent}]` : "";
1426
+ const detail = e.detail ? ` ${c.dim}${e.detail}${c.reset}` : "";
1427
+ console.log(` ${icon} ${ts} ${e.operation} ${c.dim}${agent}${c.reset}${detail}`);
1428
+ }
1429
+ if (r.events.length > 10) {
1430
+ console.log(` ${c.dim}… ${r.events.length - 10} earlier events truncated${c.reset}`);
1431
+ }
1432
+ console.log();
1433
+ }
1434
+ if (!keyFilter && unattributed.length > 0) {
1435
+ console.log(`${c.dim}+ ${unattributed.length} event(s) couldn't be tied to a specific key. Use ${c.bold}--json${c.reset}${c.dim} for the full set.${c.reset}\n`);
1436
+ }
1437
+ return true;
1438
+ }
1439
+ async function cmdAudit() {
1440
+ const args = process.argv.slice(3);
1441
+ // Sub-sub: kit audit secrets [--since-days N] [--key NAME] [--json]
1442
+ if (args[0] === "secrets") {
1443
+ return cmdAuditSecrets();
1444
+ }
1445
+ // Parse --limit N
1446
+ let limit = 20;
1447
+ const limitIdx = args.indexOf("--limit");
1448
+ if (limitIdx !== -1 && args[limitIdx + 1]) {
1449
+ const parsed = parseInt(args[limitIdx + 1], 10);
1450
+ if (!isNaN(parsed) && parsed > 0)
1451
+ limit = parsed;
1452
+ }
1453
+ // Parse --operation <name>
1454
+ let operationFilter;
1455
+ const opIdx = args.indexOf("--operation");
1456
+ if (opIdx !== -1 && args[opIdx + 1]) {
1457
+ operationFilter = args[opIdx + 1];
1458
+ }
1459
+ // Determine log file path (use config if available, else default)
1460
+ let logFile = ".kit-audit.jsonl";
1461
+ try {
1462
+ const config = await loadConfig(resolveConfigPath());
1463
+ const govFile = config.governance?.audit?.log_file;
1464
+ if (govFile)
1465
+ logFile = govFile;
1466
+ }
1467
+ catch {
1468
+ // No .kit.toml — use default log file
1469
+ }
1470
+ let events = await readAuditLog(logFile, limit * 5); // read extra to allow filtering
1471
+ if (operationFilter) {
1472
+ events = events.filter((e) => e.operation.includes(operationFilter));
1473
+ }
1474
+ // Apply limit after filter
1475
+ events = events.slice(-limit);
1476
+ printAuditTable(events);
1477
+ if (events.length > 0) {
1478
+ console.log();
1479
+ console.log(`${c.dim}Showing ${events.length} entries from ${logFile}${c.reset}`);
1480
+ }
1481
+ return true;
1482
+ }
1483
+ async function cmdEnvList() {
1484
+ let config;
1485
+ try {
1486
+ // Load base config without env merge so we can inspect [env.*] sections
1487
+ config = await loadConfig(resolveConfigPath(), "dev");
1488
+ }
1489
+ catch {
1490
+ console.log(`${c.dim}No .kit.toml found — no environments configured.${c.reset}`);
1491
+ return true;
1492
+ }
1493
+ const activeEnv = resolveActiveEnvironment();
1494
+ const envSections = config.env ?? {};
1495
+ const envNames = ["dev", ...Object.keys(envSections).filter((k) => k !== "dev")];
1496
+ console.log(`${c.bold}${c.cyan}Environments${c.reset} ${c.dim}(active: ${activeEnv})${c.reset}\n`);
1497
+ for (const name of envNames) {
1498
+ const isActive = name === activeEnv;
1499
+ const marker = isActive ? `${c.green}●${c.reset} ` : " ";
1500
+ const override = envSections[name];
1501
+ if (!override) {
1502
+ console.log(` ${marker}${c.bold}${name}${c.reset} ${c.dim}base config (default)${c.reset}`);
1503
+ continue;
1504
+ }
1505
+ // Summarise which keys are overridden
1506
+ const overrideKeys = [];
1507
+ if (override.tools)
1508
+ overrideKeys.push(`tools(${Object.keys(override.tools).join(",")})`);
1509
+ if (override.secrets)
1510
+ overrideKeys.push(`secrets.store=${override.secrets.store ?? "?"}`);
1511
+ if (override.services)
1512
+ overrideKeys.push(`services(${Object.keys(override.services).join(",")})`);
1513
+ if (override.governance)
1514
+ overrideKeys.push("governance");
1515
+ if (override.skills)
1516
+ overrideKeys.push("skills");
1517
+ const summary = overrideKeys.length > 0 ? overrideKeys.join(", ") : "no overrides";
1518
+ console.log(` ${marker}${c.bold}${name}${c.reset} ${c.dim}${summary}${c.reset}`);
1519
+ }
1520
+ if (Object.keys(envSections).length === 0) {
1521
+ console.log(`${c.dim}\nNo [env.*] sections defined in .kit.toml.${c.reset}`);
1522
+ console.log(`${c.dim}Add [env.staging.*] or [env.production.*] to configure per-environment overrides.${c.reset}`);
1523
+ }
1524
+ console.log();
1525
+ return true;
1526
+ }
1527
+ async function cmdEnvSwitch() {
1528
+ // argv layout: [node, cli.js, "env", "switch", "<target>"]
1529
+ const target = process.argv[4];
1530
+ if (!target || !KNOWN_ENVS.includes(target)) {
1531
+ console.error(`${c.red}Usage: kit env switch <${KNOWN_ENVS.join("|")}>${c.reset}`);
1532
+ return false;
1533
+ }
1534
+ const env = target;
1535
+ console.log(`${c.bold}${c.cyan}kit env switch ${env}${c.reset}`);
1536
+ console.log(`${c.dim}${"─".repeat(50)}${c.reset}\n`);
1537
+ if (env === "prod" && !isNonInteractive()) {
1538
+ console.log(`${c.red}⚠ Switching to ${c.bold}prod${c.reset}${c.red} unlocks production credentials in this shell.${c.reset}`);
1539
+ console.log(`${c.dim}Anything that reads ${c.bold}.env.local${c.reset}${c.dim} after this point can call live services.${c.reset}\n`);
1540
+ const ok = await promptConfirm(`Continue? [y/N] (auto-no in 10s): `, 10_000, false);
1541
+ if (!ok) {
1542
+ console.log(`${c.dim}Aborted — active env unchanged.${c.reset}`);
1543
+ return false;
1544
+ }
1545
+ }
1546
+ const state = await writeActiveEnv(env, process.cwd());
1547
+ console.log(` ${c.green}✓${c.reset} active env is now ${c.bold}${state.env}${c.reset} ${c.dim}(by ${state.switchedBy} at ${state.switchedAt})${c.reset}`);
1548
+ if (env === "prod") {
1549
+ console.log(`\n${c.dim}Re-run ${c.bold}kit secrets${c.reset}${c.dim} to materialize prod values into .env.local. ${c.bold}kit env switch dev${c.reset}${c.dim} when done.${c.reset}\n`);
1550
+ }
1551
+ else {
1552
+ console.log(`\n${c.dim}Prod-scoped keys are now refused unless you switch back or set ${c.bold}KIT_PROD_OK=1${c.reset}${c.dim}.${c.reset}\n`);
1553
+ }
1554
+ return true;
1555
+ }
1556
+ async function cmdEnvCurrent() {
1557
+ const state = await readActiveEnv(process.cwd());
1558
+ if (!state) {
1559
+ console.log(`${c.dim}No active env set (defaulting to ${c.bold}dev${c.reset}${c.dim}). Run ${c.bold}kit env switch <env>${c.reset}${c.dim} to set one.${c.reset}`);
1560
+ console.log(`dev`);
1561
+ return true;
1562
+ }
1563
+ const color = state.env === "prod" ? c.red : state.env === "staging" ? c.yellow : c.green;
1564
+ console.log(`${color}${state.env}${c.reset} ${c.dim}(set ${state.switchedAt} by ${state.switchedBy})${c.reset}`);
1565
+ return true;
1566
+ }
1567
+ async function cmdEnv() {
1568
+ const args = process.argv.slice(2); // includes "env"
1569
+ // Route subcommand: kit env list / switch / current / validate
1570
+ if (args[1] === "list")
1571
+ return cmdEnvList();
1572
+ if (args[1] === "switch")
1573
+ return cmdEnvSwitch();
1574
+ if (args[1] === "current")
1575
+ return cmdEnvCurrent();
1576
+ const showValues = hasFlag(args, "--show-values");
1577
+ const missingOnly = hasFlag(args, "--missing");
1578
+ const jsonMode = hasFlag(args, "--json");
1579
+ let config = {};
1580
+ try {
1581
+ config = await loadConfig(resolveConfigPath());
1582
+ }
1583
+ catch {
1584
+ // Works without .kit.toml — shows .env.local contents only
1585
+ }
1586
+ const result = await inspectEnv(config, { showValues, missingOnly, cwd: process.cwd() });
1587
+ if (jsonMode) {
1588
+ console.log(JSON.stringify(result, null, 2));
1589
+ return result.ok;
1590
+ }
1591
+ console.log(`${c.bold}${c.cyan}Environment${c.reset}`);
1592
+ console.log(`${c.dim}${"─".repeat(50)}${c.reset}\n`);
1593
+ if (!result.envLocalExists) {
1594
+ console.log(`${c.yellow}⚠${c.reset} .env.local not found — run ${c.bold}kit secrets${c.reset} to generate it\n`);
1595
+ }
1596
+ if (result.keys.length === 0) {
1597
+ if (missingOnly) {
1598
+ console.log(`${c.green}✓${c.reset} All keys are set\n`);
1599
+ }
1600
+ else {
1601
+ console.log(`${c.dim}No keys configured or found${c.reset}\n`);
1602
+ }
1603
+ return result.ok;
1604
+ }
1605
+ for (const key of result.keys) {
1606
+ const icon = key.set ? `${c.green}✓${c.reset}` : `${c.red}✗${c.reset}`;
1607
+ const valueStr = key.set
1608
+ ? showValues
1609
+ ? `${c.dim}${key.value}${c.reset}`
1610
+ : `${c.dim}${key.redacted}${c.reset}`
1611
+ : `${c.red}not set${c.reset}`;
1612
+ const src = `${c.dim}[${key.source}]${c.reset}`;
1613
+ console.log(` ${icon} ${key.name} ${valueStr} ${src}`);
1614
+ }
1615
+ console.log();
1616
+ if (!result.ok && !missingOnly) {
1617
+ console.log(`${c.dim}Run ${c.reset}${c.bold}kit env --missing${c.reset}${c.dim} to see only unset keys${c.reset}`);
1618
+ console.log(`${c.dim}Run ${c.reset}${c.bold}kit secrets${c.reset}${c.dim} to generate .env.local${c.reset}\n`);
1619
+ }
1620
+ return result.ok;
1621
+ }
1622
+ async function cmdDoctor() {
1623
+ console.log(`${c.bold}${c.cyan}kit doctor${c.reset}`);
1624
+ console.log(`${c.dim}${"─".repeat(50)}${c.reset}\n`);
1625
+ let config = {};
1626
+ try {
1627
+ config = await loadConfig(resolveConfigPath());
1628
+ }
1629
+ catch {
1630
+ // Doctor works even without .kit.toml — skip config-dependent checks
1631
+ }
1632
+ const result = await runDoctor(config, process.cwd());
1633
+ for (const check of result.checks) {
1634
+ const icon = check.status === "pass"
1635
+ ? `${c.green}✓${c.reset}`
1636
+ : check.status === "warn"
1637
+ ? `${c.yellow}⚠${c.reset}`
1638
+ : check.status === "fail"
1639
+ ? `${c.red}✗${c.reset}`
1640
+ : `${c.dim}–${c.reset}`;
1641
+ console.log(` ${icon} ${check.name} ${c.dim}${check.detail}${c.reset}`);
1642
+ }
1643
+ console.log();
1644
+ console.log(`${c.dim}${"─".repeat(50)}${c.reset}`);
1645
+ const summaryParts = [];
1646
+ if (result.passed > 0)
1647
+ summaryParts.push(`${c.green}${result.passed} passed${c.reset}`);
1648
+ if (result.warnings > 0)
1649
+ summaryParts.push(`${c.yellow}${result.warnings} warning${result.warnings === 1 ? "" : "s"}${c.reset}`);
1650
+ if (result.failed > 0)
1651
+ summaryParts.push(`${c.red}${result.failed} failed${c.reset}`);
1652
+ if (summaryParts.length === 0)
1653
+ summaryParts.push(`${c.dim}no checks ran${c.reset}`);
1654
+ console.log(`\n ${summaryParts.join(" · ")}\n`);
1655
+ return result.failed === 0;
1656
+ }
1657
+ async function cmdAdd() {
1658
+ const serviceName = process.argv[3];
1659
+ if (!serviceName) {
1660
+ console.log(`${c.bold}${c.cyan}Available services:${c.reset}\n`);
1661
+ const services = listAvailableServices();
1662
+ for (const svc of services) {
1663
+ const info = getServiceInfo(svc);
1664
+ if (info) {
1665
+ console.log(` ${c.green}${svc}${c.reset} ${c.dim}${info.description}${c.reset}`);
1666
+ console.log(` ${c.dim}Requires: ${info.tools.join(", ")}${c.reset}`);
1667
+ }
1668
+ }
1669
+ console.log();
1670
+ console.log(`${c.dim}Usage: kit add <service>${c.reset}`);
1671
+ console.log(`${c.dim}Example: kit add stripe/payments${c.reset}`);
1672
+ console.log();
1673
+ return false;
1674
+ }
1675
+ console.log(`${c.bold}${c.cyan}Provisioning ${serviceName}...${c.reset}\n`);
1676
+ const projectPath = process.cwd();
1677
+ const projectName = process.cwd().split("/").pop();
1678
+ const result = await provisionService(serviceName, projectPath, projectName);
1679
+ if (result.success) {
1680
+ console.log(` ${c.green}✓${c.reset} ${result.message}`);
1681
+ if (result.secrets && Object.keys(result.secrets).length > 0) {
1682
+ console.log();
1683
+ console.log(` ${c.dim}Added secrets to .env.local:${c.reset}`);
1684
+ for (const key of Object.keys(result.secrets)) {
1685
+ console.log(` ${c.cyan}${key}${c.reset}`);
1686
+ }
1687
+ }
1688
+ if (result.config) {
1689
+ console.log();
1690
+ console.log(` ${c.dim}Updated skills-lock.json${c.reset}`);
1691
+ }
1692
+ console.log();
1693
+ return true;
1694
+ }
1695
+ else {
1696
+ console.log(` ${c.red}✗${c.reset} ${result.message}`);
1697
+ if (result.error) {
1698
+ console.log(` ${c.dim}Error: ${result.error}${c.reset}`);
1699
+ }
1700
+ console.log();
1701
+ return false;
1702
+ }
1703
+ }
1704
+ async function cmdSecretsOneCli() {
1705
+ // Sub-sub: kit secrets onecli [status|register <KEY> --host <pattern>]
1706
+ const sub = process.argv[4] ?? "status";
1707
+ if (sub === "status") {
1708
+ console.log(`${c.bold}${c.cyan}kit secrets onecli status${c.reset}`);
1709
+ console.log(`${c.dim}${"─".repeat(50)}${c.reset}\n`);
1710
+ const status = await checkOneCliStatus();
1711
+ const reach = status.reachable
1712
+ ? `${c.green}reachable${c.reset}`
1713
+ : `${c.red}unreachable${c.reset}`;
1714
+ const auth = status.authenticated
1715
+ ? `${c.green}authenticated${c.reset}`
1716
+ : `${c.red}not authenticated${c.reset}`;
1717
+ console.log(` Gateway API: ${status.apiUrl} ${reach}`);
1718
+ console.log(` Gateway proxy: ${status.gatewayUrl} ${c.dim}(set HTTPS_PROXY here for agent processes)${c.reset}`);
1719
+ console.log(` Auth: ${auth}`);
1720
+ if (status.version) {
1721
+ console.log(` Version: ${c.dim}${status.version}${c.reset}`);
1722
+ }
1723
+ if (status.error) {
1724
+ console.log(` ${c.yellow}→${c.reset} ${status.error}`);
1725
+ }
1726
+ console.log();
1727
+ if (!status.reachable) {
1728
+ console.log(`${c.dim}Start OneCLI: ${c.bold}cd /path/to/onecli && docker compose -f docker/docker-compose.yml up -d${c.reset}${c.dim}, or set ${c.bold}ONECLI_API_URL${c.reset}${c.dim} if it runs elsewhere.${c.reset}\n`);
1729
+ }
1730
+ return status.reachable && status.authenticated;
1731
+ }
1732
+ if (sub !== "register") {
1733
+ console.error(`${c.red}Usage: kit secrets onecli [status|register <KEY> --host <pattern> [--path <pattern>]]${c.reset}`);
1734
+ return false;
1735
+ }
1736
+ const args = process.argv.slice(5);
1737
+ const keyName = args[0];
1738
+ if (!keyName || keyName.startsWith("--")) {
1739
+ console.error(`${c.red}Usage: kit secrets onecli register <KEY> --host <pattern> [--path <pattern>]${c.reset}`);
1740
+ return false;
1741
+ }
1742
+ const hostIdx = args.indexOf("--host");
1743
+ const pathIdx = args.indexOf("--path");
1744
+ const hostPattern = hostIdx >= 0 ? args[hostIdx + 1] : undefined;
1745
+ const pathPattern = pathIdx >= 0 ? args[pathIdx + 1] : undefined;
1746
+ if (!hostPattern) {
1747
+ console.error(`${c.red}--host required (e.g. api.stripe.com, api.openai.com).${c.reset}`);
1748
+ return false;
1749
+ }
1750
+ console.log(`${c.bold}${c.cyan}kit secrets onecli register${c.reset}`);
1751
+ console.log(`${c.dim}${"─".repeat(50)}${c.reset}\n`);
1752
+ // S12: gate destructive op behind explicit elevation.
1753
+ // Register writes a fake placeholder into .env.local; one elevation =
1754
+ // one register so a stale TTL can't re-overwrite the file.
1755
+ const elev = await consumeElevation("onecli-register");
1756
+ if (!elev.ok) {
1757
+ console.error(`${c.red}✗ ${elev.reason}${c.reset}`);
1758
+ return false;
1759
+ }
1760
+ // 1. Verify OneCLI is reachable + authed before reading the secret.
1761
+ const status = await checkOneCliStatus();
1762
+ if (!status.reachable || !status.authenticated) {
1763
+ console.error(`${c.red}OneCLI not ready: ${status.error ?? "unreachable"}${c.reset}`);
1764
+ console.error(`${c.dim}Run ${c.bold}kit secrets onecli status${c.reset}${c.dim} for details.${c.reset}`);
1765
+ return false;
1766
+ }
1767
+ // 2. Read the real value from the configured upstream vault.
1768
+ const config = await loadConfig(resolveConfigPath());
1769
+ if (!config.secrets?.store || config.secrets.store === "env") {
1770
+ console.error(`${c.red}No upstream vault configured in .kit.toml.${c.reset}`);
1771
+ console.error(`${c.dim}Set ${c.bold}[secrets].store${c.reset}${c.dim} (run ${c.bold}kit init${c.reset}${c.dim}) first so kit knows where to read the real value from.${c.reset}`);
1772
+ return false;
1773
+ }
1774
+ const keyConfig = config.secrets.keys?.[keyName];
1775
+ if (!keyConfig) {
1776
+ console.error(`${c.red}Key "${keyName}" not in [secrets.keys] — run ${c.bold}kit init${c.reset}${c.red} or add it manually.${c.reset}`);
1777
+ return false;
1778
+ }
1779
+ const { generateSecrets: _unused } = await import("./secrets.js");
1780
+ void _unused;
1781
+ // resolveSecret isn't exported; reuse generateSecrets to pull the one value
1782
+ // we need. Cheaper: re-export resolveSecret, but for MVP we shell to op
1783
+ // /infisical/etc through the same code by asking generateSecrets to write
1784
+ // to a tmp file. Simpler: just use the env-resolution branch directly when
1785
+ // possible, and tell the user to use `kit secrets` for vault-resolution.
1786
+ // To avoid that complication for v1, require the value via stdin instead:
1787
+ console.log(`${c.dim}Reading real value from upstream vault (${config.secrets.store})...${c.reset}`);
1788
+ const realValue = await readSecretValueFromVault(keyName, config);
1789
+ if (!realValue) {
1790
+ console.error(`${c.red}Could not resolve "${keyName}" from upstream vault.${c.reset}`);
1791
+ console.error(`${c.dim}Check auth with ${c.bold}kit check${c.reset}${c.dim} and retry.${c.reset}`);
1792
+ return false;
1793
+ }
1794
+ // 3. Register with OneCLI.
1795
+ try {
1796
+ const result = await registerSecretInOneCli({
1797
+ name: keyName,
1798
+ value: realValue,
1799
+ hostPattern,
1800
+ pathPattern,
1801
+ });
1802
+ console.log(` ${c.green}✓${c.reset} registered ${c.bold}${keyName}${c.reset} in OneCLI ${c.dim}(id ${result.id.slice(0, 8)}…, host=${hostPattern})${c.reset}`);
1803
+ }
1804
+ catch (err) {
1805
+ const msg = err instanceof Error ? err.message : String(err);
1806
+ console.error(`${c.red}OneCLI register failed: ${msg}${c.reset}`);
1807
+ return false;
1808
+ }
1809
+ // 4. Write placeholder to .env.local so agents read a non-credential.
1810
+ const placeholder = generatePlaceholder();
1811
+ const { writeFile, readFile, access } = await import("node:fs/promises");
1812
+ const envPath = `${process.cwd()}/.env.local`;
1813
+ let envContent = "";
1814
+ try {
1815
+ await access(envPath);
1816
+ envContent = await readFile(envPath, "utf-8");
1817
+ }
1818
+ catch {
1819
+ envContent = "";
1820
+ }
1821
+ const lineRe = new RegExp(`^${keyName}=.*$`, "m");
1822
+ const newLine = `${keyName}=${placeholder} # placeholder — real value lives in OneCLI`;
1823
+ if (lineRe.test(envContent)) {
1824
+ envContent = envContent.replace(lineRe, newLine);
1825
+ }
1826
+ else {
1827
+ if (!envContent.endsWith("\n") && envContent.length > 0)
1828
+ envContent += "\n";
1829
+ envContent += newLine + "\n";
1830
+ }
1831
+ await writeFile(envPath, envContent, "utf-8");
1832
+ console.log(` ${c.green}✓${c.reset} wrote placeholder to .env.local ${c.dim}(${placeholder.slice(0, 14)}…)${c.reset}`);
1833
+ // 5. Surface the proxy setup the agent needs to honor.
1834
+ const cfg = resolveOneCliConfig();
1835
+ console.log();
1836
+ console.log(`${c.bold}Next: route agent traffic through OneCLI${c.reset}`);
1837
+ console.log(`${c.dim}For the agent process to use the placeholder + get the real value injected, point its HTTPS proxy at:${c.reset}`);
1838
+ console.log(` ${c.bold}HTTPS_PROXY=${cfg.gatewayUrl}${c.reset}`);
1839
+ console.log(` ${c.bold}HTTP_PROXY=${cfg.gatewayUrl}${c.reset}`);
1840
+ console.log(`${c.dim}OneCLI will intercept requests to ${c.bold}${hostPattern}${c.reset}${c.dim}, swap in the real value, and forward.${c.reset}\n`);
1841
+ return true;
1842
+ }
1843
+ async function readSecretValueFromVault(keyName, config) {
1844
+ // Reuse generateSecrets() to materialize values, then read the one we want.
1845
+ // Writes to a temp file then deletes it; the real value never touches the
1846
+ // working tree.
1847
+ if (!config.secrets)
1848
+ return null;
1849
+ const { generateSecrets } = await import("./secrets.js");
1850
+ const { mkdtempSync, writeFileSync, readFileSync, rmSync } = await import("node:fs");
1851
+ const { tmpdir } = await import("node:os");
1852
+ const { join } = await import("node:path");
1853
+ const dir = mkdtempSync(join(tmpdir(), "kit-onecli-"));
1854
+ const tmpEnv = join(dir, ".env");
1855
+ try {
1856
+ // We need generateSecrets to write to a path of our choosing; the current
1857
+ // signature takes outputPath. Use a no-template config so values land
1858
+ // verbatim as KEY=VALUE lines.
1859
+ const isolated = {
1860
+ ...config.secrets,
1861
+ template: undefined,
1862
+ keys: { [keyName]: config.secrets.keys[keyName] },
1863
+ };
1864
+ void writeFileSync; // keep import for future expansion
1865
+ await generateSecrets(isolated, tmpEnv);
1866
+ const content = readFileSync(tmpEnv, "utf-8");
1867
+ const match = content.match(new RegExp(`^${keyName}=(.*)$`, "m"));
1868
+ return match ? match[1] : null;
1869
+ }
1870
+ catch {
1871
+ return null;
1872
+ }
1873
+ finally {
1874
+ try {
1875
+ rmSync(dir, { recursive: true, force: true });
1876
+ }
1877
+ catch {
1878
+ /* ignore */
1879
+ }
1880
+ }
1881
+ }
1882
+ async function cmdSecretsRevokeOld() {
1883
+ // kit secrets revoke-old --via supabase-mgmt-api --project <ref> --key-id <id>
1884
+ const args = process.argv.slice(4);
1885
+ const viaIdx = args.indexOf("--via");
1886
+ const via = viaIdx >= 0 ? args[viaIdx + 1] : undefined;
1887
+ if (via !== "supabase-mgmt-api") {
1888
+ console.error(`${c.red}Usage: kit secrets revoke-old --via supabase-mgmt-api --project <ref> --key-id <id>${c.reset}`);
1889
+ return false;
1890
+ }
1891
+ const projectIdx = args.indexOf("--project");
1892
+ const projectRef = projectIdx >= 0 ? args[projectIdx + 1] : process.env.SUPABASE_PROJECT_REF;
1893
+ const keyIdIdx = args.indexOf("--key-id");
1894
+ const keyId = keyIdIdx >= 0 ? args[keyIdIdx + 1] : undefined;
1895
+ if (!projectRef) {
1896
+ console.error(`${c.red}--project <ref> required (or set SUPABASE_PROJECT_REF).${c.reset}`);
1897
+ return false;
1898
+ }
1899
+ if (!keyId) {
1900
+ console.error(`${c.red}--key-id <id> required. List candidates with ${c.bold}kit secrets onecli status${c.reset}${c.red} or via Supabase Dashboard.${c.reset}`);
1901
+ return false;
1902
+ }
1903
+ console.log(`${c.bold}${c.cyan}kit secrets revoke-old (supabase)${c.reset}`);
1904
+ console.log(`${c.dim}${"─".repeat(50)}${c.reset}\n`);
1905
+ const elev = await requireElevation("revoke-old");
1906
+ if (!elev.ok) {
1907
+ console.error(`${c.red}✗ ${elev.reason}${c.reset}`);
1908
+ return false;
1909
+ }
1910
+ const supabase = await import("sandstream-kit-plugin-supabase").catch(() => null);
1911
+ if (!supabase) {
1912
+ console.error(`${c.red}sandstream-kit-plugin-supabase not installed.${c.reset}`);
1913
+ return false;
1914
+ }
1915
+ const client = supabase.makeClient();
1916
+ const result = await supabase.revokeScopedKey(client, projectRef, keyId);
1917
+ if (!result.ok) {
1918
+ console.error(`${c.red}✗ ${result.detail}${c.reset}`);
1919
+ return false;
1920
+ }
1921
+ console.log(` ${c.green}✓${c.reset} ${result.detail}`);
1922
+ console.log(`\n${c.dim}Verify with ${c.bold}kit secrets onecli status${c.reset}${c.dim} or the Supabase Dashboard.${c.reset}\n`);
1923
+ return true;
1924
+ }
1925
+ async function cmdSecretsPropagateStandalone() {
1926
+ // kit secrets propagate <KEY> --value <v> | --stdin --to <targets> [opts]
1927
+ const args = process.argv.slice(4);
1928
+ const keyName = args[0];
1929
+ if (!keyName || keyName.startsWith("--")) {
1930
+ console.error(`${c.red}Usage: kit secrets propagate <KEY> [--value <v> | --stdin] --to <targets> [opts]${c.reset}`);
1931
+ console.error(`${c.dim}targets: ${ALL_TARGETS.join(",")}${c.reset}`);
1932
+ return false;
1933
+ }
1934
+ const toIdx = args.indexOf("--to");
1935
+ const targetSpec = toIdx >= 0 ? args[toIdx + 1] : undefined;
1936
+ if (!targetSpec) {
1937
+ console.error(`${c.red}--to <targets> required${c.reset}`);
1938
+ return false;
1939
+ }
1940
+ const targets = parseTargets(targetSpec);
1941
+ if (targets.length === 0) {
1942
+ console.error(`${c.red}--to: no valid targets in "${targetSpec}". Valid: ${ALL_TARGETS.join(", ")}${c.reset}`);
1943
+ return false;
1944
+ }
1945
+ // Read value: --value <v>, --stdin, or interactive masked prompt.
1946
+ let value;
1947
+ const valueIdx = args.indexOf("--value");
1948
+ if (valueIdx >= 0)
1949
+ value = args[valueIdx + 1];
1950
+ if (!value && hasFlag(args, "--stdin")) {
1951
+ value = await new Promise((resolve) => {
1952
+ let buf = "";
1953
+ process.stdin.setEncoding("utf-8");
1954
+ process.stdin.on("data", (chunk) => {
1955
+ buf += chunk;
1956
+ });
1957
+ process.stdin.on("end", () => resolve(buf.trim()));
1958
+ });
1959
+ }
1960
+ if (!value) {
1961
+ console.error(`${c.red}Provide value via --value <v> (visible in argv/ps) or --stdin (safer).${c.reset}`);
1962
+ return false;
1963
+ }
1964
+ // Same elevation gate as rotate.
1965
+ const elev = await requireElevation("propagate");
1966
+ if (!elev.ok) {
1967
+ console.error(`${c.red}✗ ${elev.reason}${c.reset}`);
1968
+ return false;
1969
+ }
1970
+ // Parse propagation options (same flag surface as rotate --propagate).
1971
+ const propOpts = {};
1972
+ const envIdx = args.indexOf("--target-env");
1973
+ if (envIdx >= 0)
1974
+ propOpts.env = args[envIdx + 1];
1975
+ const flyAppIdx = args.indexOf("--fly-app");
1976
+ if (flyAppIdx >= 0)
1977
+ propOpts.flyApp = args[flyAppIdx + 1];
1978
+ const cfWorkerIdx = args.indexOf("--cf-worker");
1979
+ if (cfWorkerIdx >= 0)
1980
+ propOpts.cfWorker = args[cfWorkerIdx + 1];
1981
+ const railwayServiceIdx = args.indexOf("--railway-service");
1982
+ if (railwayServiceIdx >= 0)
1983
+ propOpts.railwayService = args[railwayServiceIdx + 1];
1984
+ const awsRegionIdx = args.indexOf("--aws-region");
1985
+ if (awsRegionIdx >= 0)
1986
+ propOpts.awsRegion = args[awsRegionIdx + 1];
1987
+ const ghRepoIdx = args.indexOf("--github-repo");
1988
+ if (ghRepoIdx >= 0)
1989
+ propOpts.githubRepo = args[ghRepoIdx + 1];
1990
+ const vercelScopeIdx = args.indexOf("--vercel-scope");
1991
+ if (vercelScopeIdx >= 0)
1992
+ propOpts.vercelScope = args[vercelScopeIdx + 1];
1993
+ console.log(`${c.bold}${c.cyan}kit secrets propagate ${keyName}${c.reset}`);
1994
+ console.log(`${c.dim}${"─".repeat(50)}${c.reset}\n`);
1995
+ console.log(`${c.bold}Targets:${c.reset} ${c.dim}${targets.join(", ")}${c.reset}\n`);
1996
+ const results = await propagate(keyName, value, targets, propOpts);
1997
+ let allOk = true;
1998
+ for (const r of results) {
1999
+ const icon = r.ok ? `${c.green}✓${c.reset}` : `${c.red}✗${c.reset}`;
2000
+ const argvWarn = r.valueInArgv ? ` ${c.yellow}[value in argv]${c.reset}` : "";
2001
+ console.log(` ${icon} ${r.target.padEnd(10)} ${c.dim}${r.detail}${c.reset}${argvWarn}`);
2002
+ if (!r.ok)
2003
+ allOk = false;
2004
+ }
2005
+ console.log();
2006
+ return allOk;
2007
+ }
2008
+ async function cmdSecretsPurgeHistory() {
2009
+ // kit secrets purge-history <pattern> [<pattern>...] --force-history [--yes]
2010
+ const args = process.argv.slice(4);
2011
+ const force = hasFlag(args, "--force-history");
2012
+ const yes = hasFlag(args, "--yes");
2013
+ const patterns = args.filter((a) => !a.startsWith("--"));
2014
+ console.log(`${c.bold}${c.red}kit secrets purge-history${c.reset}`);
2015
+ console.log(`${c.dim}${"─".repeat(50)}${c.reset}\n`);
2016
+ if (patterns.length === 0) {
2017
+ console.error(`${c.red}Usage: kit secrets purge-history <pattern> [more...] --force-history [--yes]${c.reset}`);
2018
+ console.error(`${c.dim}Pattern can be a literal string (the leaked credential value) or a regex prefix.${c.reset}`);
2019
+ return false;
2020
+ }
2021
+ console.log(`${c.bold}Patterns to scrub:${c.reset}`);
2022
+ for (const p of patterns) {
2023
+ const masked = p.length > 12 ? `${p.slice(0, 6)}…${p.slice(-4)}` : p;
2024
+ console.log(` ${c.red}•${c.reset} ${masked} ${c.dim}(${p.length} chars)${c.reset}`);
2025
+ }
2026
+ console.log();
2027
+ console.log(`${c.bold}Impact preview:${c.reset}`);
2028
+ let totalCommits = 0;
2029
+ for (const p of patterns) {
2030
+ const pv = await previewMatches(p, process.cwd());
2031
+ totalCommits += pv.matchedCommits;
2032
+ if (pv.matchedCommits === 0) {
2033
+ console.log(` ${c.dim}•${c.reset} ${pv.pattern.slice(0, 14)}… ${c.dim}no matches${c.reset}`);
2034
+ }
2035
+ else {
2036
+ console.log(` ${c.yellow}•${c.reset} ${pv.pattern.slice(0, 14)}… ${c.yellow}${pv.matchedCommits} commit(s) touched${c.reset}`);
2037
+ for (const f of pv.matchedFiles.slice(0, 5)) {
2038
+ console.log(` ${c.dim}↳ ${f}${c.reset}`);
2039
+ }
2040
+ }
2041
+ }
2042
+ console.log();
2043
+ if (totalCommits === 0) {
2044
+ console.log(`${c.green}✓ No commits reference the supplied pattern(s). Nothing to do.${c.reset}\n`);
2045
+ return true;
2046
+ }
2047
+ console.warn(`${c.red}⚠ This rewrites git history.${c.reset}\n` +
2048
+ `${c.dim}Every commit hash from the first affected commit forward changes. Required follow-ups:${c.reset}\n` +
2049
+ ` ${c.dim}1. Force-push every branch + tag to remotes${c.reset}\n` +
2050
+ ` ${c.dim}2. All teammates must DELETE their local clone and re-clone${c.reset}\n` +
2051
+ ` ${c.dim}3. CI runners + deploy pipelines that pulled from this remote must re-clone${c.reset}\n` +
2052
+ ` ${c.dim}4. The leaked credential must ALSO be rotated — scrubbing history doesn't invalidate the value${c.reset}\n`);
2053
+ if (!force) {
2054
+ console.error(`${c.red}✗ Refusing to proceed without --force-history.${c.reset}`);
2055
+ console.error(`${c.dim}Re-run with ${c.bold}--force-history${c.reset}${c.dim} after confirming the impact list above.${c.reset}\n`);
2056
+ return false;
2057
+ }
2058
+ // Must be elevated AND must explicitly confirm in interactive mode.
2059
+ // History-rewrite is irreversible — one elevation = one purge.
2060
+ const elev = await consumeElevation("purge-history");
2061
+ if (!elev.ok) {
2062
+ console.error(`${c.red}✗ ${elev.reason}${c.reset}`);
2063
+ return false;
2064
+ }
2065
+ if (!isNonInteractive() && !yes) {
2066
+ const ok = await promptConfirm(`Confirm DESTRUCTIVE history rewrite [type YES, default no in 15s]: `, 15_000, false);
2067
+ if (!ok) {
2068
+ console.log(`${c.dim}Aborted.${c.reset}`);
2069
+ return false;
2070
+ }
2071
+ }
2072
+ const tools = await detectPurgeTools();
2073
+ if (!tools.filterRepoAvailable && !tools.bfgAvailable) {
2074
+ console.error(`${c.red}✗ Neither ${c.bold}git filter-repo${c.reset}${c.red} nor ${c.bold}bfg${c.reset}${c.red} installed.${c.reset}`);
2075
+ console.error(`${c.dim}Install: ${c.bold}pip install git-filter-repo${c.reset}${c.dim} or ${c.bold}brew install bfg${c.reset}${c.dim}.${c.reset}\n`);
2076
+ return false;
2077
+ }
2078
+ console.log(`${c.dim}Running ${tools.filterRepoAvailable ? "git filter-repo" : "bfg"}…${c.reset}`);
2079
+ const result = await purgeHistory(patterns, process.cwd());
2080
+ if (!result.ok) {
2081
+ console.error(`${c.red}✗ ${result.detail}${c.reset}`);
2082
+ return false;
2083
+ }
2084
+ console.log(` ${c.green}✓${c.reset} ${result.toolUsed} completed. ${c.dim}${result.detail}${c.reset}`);
2085
+ console.log(`\n${c.yellow}NEXT (manual):${c.reset}\n` +
2086
+ ` ${c.bold}git push origin --force --all${c.reset}\n` +
2087
+ ` ${c.bold}git push origin --force --tags${c.reset}\n` +
2088
+ `${c.dim}Then tell everyone with a clone to re-clone, and rotate the leaked credential if you haven't already.${c.reset}\n`);
2089
+ return true;
2090
+ }
2091
+ async function cmdSecretsMigrate() {
2092
+ console.log(`${c.bold}${c.cyan}kit secrets migrate${c.reset}`);
2093
+ console.log(`${c.dim}${"─".repeat(50)}${c.reset}\n`);
2094
+ const config = await loadConfig(resolveConfigPath());
2095
+ if (!config.secrets?.store || config.secrets.store === "env") {
2096
+ console.log(`${c.yellow}No vault configured in .kit.toml — set ${c.bold}[secrets].store${c.reset}${c.yellow} first (run ${c.bold}kit init${c.reset}${c.yellow}).${c.reset}\n`);
2097
+ return false;
2098
+ }
2099
+ const store = config.secrets.store;
2100
+ const dryRun = hasFlag(process.argv, "--dry-run");
2101
+ const noClean = hasFlag(process.argv, "--no-clean");
2102
+ console.log(`${c.dim}Target vault: ${c.bold}${store}${c.reset}${c.dim} (dry-run: ${dryRun})${c.reset}\n`);
2103
+ const secretsOnly = hasFlag(process.argv, "--secrets-only");
2104
+ const plan = await planMigration(process.cwd(), { secretsOnly });
2105
+ if (plan.keyValues.size === 0) {
2106
+ if (plan.hits.length > 0) {
2107
+ console.log(`${c.dim}Found ${plan.hits.length} suspicious file(s) but no migratable KEY=VALUE lines.${c.reset}`);
2108
+ for (const hit of plan.hits) {
2109
+ const labels = hit.findings.map((f) => `${f.label}:${f.preview}`).join(", ");
2110
+ console.log(` ${c.dim}•${c.reset} ${hit.file} ${c.dim}${labels}${c.reset}`);
2111
+ }
2112
+ console.log(`${c.dim}Embedded secrets in scripts / JSON need manual extraction.${c.reset}\n`);
2113
+ return false;
2114
+ }
2115
+ console.log(`${c.green}✓ No plaintext secrets found — nothing to migrate.${c.reset}\n`);
2116
+ return true;
2117
+ }
2118
+ console.log(`${c.bold}Plan${c.reset}`);
2119
+ console.log(`${c.dim}${"─".repeat(50)}${c.reset}\n`);
2120
+ for (const [key, { source }] of plan.keyValues) {
2121
+ console.log(` ${c.green}→${c.reset} ${key} ${c.dim}(from ${source})${c.reset}`);
2122
+ }
2123
+ console.log();
2124
+ if (dryRun) {
2125
+ console.log(`${c.dim}--dry-run set; not writing. Remove flag to perform the migration.${c.reset}\n`);
2126
+ return true;
2127
+ }
2128
+ const nonInteractive = isNonInteractive();
2129
+ if (!nonInteractive) {
2130
+ const ok = await promptConfirm(`Push ${plan.keyValues.size} key(s) to ${store}? [Y/n] (auto-yes in 10s): `, 10_000);
2131
+ if (!ok) {
2132
+ console.log(`${c.dim}Aborted.${c.reset}`);
2133
+ return false;
2134
+ }
2135
+ }
2136
+ // S12: gate destructive op behind explicit elevation.
2137
+ const elev = await requireElevation("migrate");
2138
+ if (!elev.ok) {
2139
+ console.error(`${c.red}✗ ${elev.reason}${c.reset}`);
2140
+ return false;
2141
+ }
2142
+ // Backend-specific options (region, project, vault name) from .kit.toml's
2143
+ // first key, with platform env-var fallbacks. Shared with the rotate flow.
2144
+ const backendOpts = pickBackendOpts(config.secrets, "", { envFallback: true });
2145
+ let fixed = 0;
2146
+ let failed = 0;
2147
+ const cleaned = new Map(); // source-file → keys
2148
+ console.log();
2149
+ for (const [key, { value, source }] of plan.keyValues) {
2150
+ const result = await writeSecretToBackend(store, key, value, backendOpts);
2151
+ if (result.ok) {
2152
+ console.log(` ${c.green}✓${c.reset} ${key} ${c.dim}${result.detail}${c.reset}`);
2153
+ fixed++;
2154
+ if (!noClean) {
2155
+ const arr = cleaned.get(source) ?? [];
2156
+ arr.push(key);
2157
+ cleaned.set(source, arr);
2158
+ }
2159
+ }
2160
+ else {
2161
+ console.log(` ${c.red}✗${c.reset} ${key} ${c.red}${result.detail}${c.reset}`);
2162
+ failed++;
2163
+ }
2164
+ }
2165
+ console.log();
2166
+ if (cleaned.size > 0 && !noClean) {
2167
+ // Post-migration mode. Default "blank" replaces value with empty so the
2168
+ // plaintext is gone but devs still see which env vars are required.
2169
+ // Override with --keep-commented (legacy behavior) or --purge (drop the
2170
+ // line entirely).
2171
+ const mode = hasFlag(process.argv, "--purge")
2172
+ ? "delete"
2173
+ : hasFlag(process.argv, "--keep-commented")
2174
+ ? "comment"
2175
+ : "blank";
2176
+ console.log(`${c.bold}Cleanup${c.reset} ${c.dim}(mode=${mode})${c.reset}`);
2177
+ console.log(`${c.dim}${"─".repeat(50)}${c.reset}\n`);
2178
+ for (const [file, keys] of cleaned) {
2179
+ const { changed } = await commentOutInFile(`${process.cwd()}/${file}`, keys, mode);
2180
+ const verb = mode === "delete" ? "deleted" : mode === "comment" ? "commented out" : "blanked";
2181
+ console.log(` ${c.green}✓${c.reset} ${file} ${c.dim}${changed} line(s) ${verb}${c.reset}`);
2182
+ }
2183
+ const explain = mode === "blank"
2184
+ ? "Values blanked (KEY= retained as required-var hint). Lines are safe to commit."
2185
+ : mode === "comment"
2186
+ ? "Lines commented (not deleted) — plaintext value remains; remove after verifying vault read works."
2187
+ : "Lines deleted entirely. Use .env.template / vault for required-var documentation.";
2188
+ console.log(`${c.dim}${explain}${c.reset}\n`);
2189
+ }
2190
+ console.log(`${fixed > 0 ? c.green : c.dim}${fixed} key(s) migrated${c.reset}${failed > 0 ? `, ${c.red}${failed} failed${c.reset}` : ""}.`);
2191
+ return failed === 0;
2192
+ }
2193
+ /**
2194
+ * Cross-vault migration. Reads every key whose source matches --from, writes
2195
+ * to --to, rewrites .kit.toml ref. One-shot elevation (consume on use).
2196
+ *
2197
+ * Example:
2198
+ * kit auth elevate --scope vault-migrate
2199
+ * kit secrets vault-migrate --from 1password --to infisical --dry-run
2200
+ * kit secrets vault-migrate --from 1password --to infisical
2201
+ */
2202
+ async function cmdSecretsVaultMigrate() {
2203
+ const args = process.argv.slice(4);
2204
+ const fromArg = args.find((a) => a.startsWith("--from="))?.split("=")[1] ??
2205
+ (args[args.indexOf("--from") + 1] ?? "");
2206
+ const toArg = args.find((a) => a.startsWith("--to="))?.split("=")[1] ??
2207
+ (args[args.indexOf("--to") + 1] ?? "");
2208
+ const dryRun = hasFlag(args, "--dry-run");
2209
+ if (hasFlag(args, "--help") || hasFlag(args, "-h") || !fromArg || !toArg) {
2210
+ console.log(`${c.bold}kit secrets vault-migrate${c.reset} — move keys between vault backends\n`);
2211
+ console.log("Usage:");
2212
+ console.log(" kit secrets vault-migrate --from <source> --to <target> [--dry-run]");
2213
+ console.log("");
2214
+ console.log("Supported backends: 1password, infisical, bitwarden, doppler, vault, aws-sm, gcp-sm, azure-kv");
2215
+ console.log("");
2216
+ console.log("Migration is gated by elevation (one-shot — consumed on use):");
2217
+ console.log(" kit auth elevate --scope vault-migrate");
2218
+ return !fromArg || !toArg ? false : true;
2219
+ }
2220
+ console.log(`${c.bold}${c.cyan}kit secrets vault-migrate${c.reset}`);
2221
+ console.log(`${c.dim}${"─".repeat(50)}${c.reset}\n`);
2222
+ console.log(`${c.dim}From: ${c.bold}${fromArg}${c.reset}${c.dim} → To: ${c.bold}${toArg}${c.reset}${c.dim} (dry-run: ${dryRun})${c.reset}\n`);
2223
+ const config = await loadConfig(resolveConfigPath());
2224
+ if (!config.secrets) {
2225
+ console.log(`${c.yellow}No [secrets] block in .kit.toml — nothing to migrate.${c.reset}\n`);
2226
+ return false;
2227
+ }
2228
+ const sourceKeys = Object.entries(config.secrets.keys ?? {}).filter(([, k]) => k.source === fromArg);
2229
+ if (sourceKeys.length === 0) {
2230
+ console.log(`${c.yellow}No keys with source="${fromArg}" found in .kit.toml.${c.reset}\n`);
2231
+ return false;
2232
+ }
2233
+ console.log(`${c.bold}Plan${c.reset}`);
2234
+ console.log(`${c.dim}${"─".repeat(50)}${c.reset}\n`);
2235
+ for (const [name] of sourceKeys) {
2236
+ console.log(` ${c.green}→${c.reset} ${name}`);
2237
+ }
2238
+ console.log();
2239
+ if (dryRun) {
2240
+ console.log(`${c.dim}--dry-run set; not reading values. Remove flag + elevate to perform.${c.reset}\n`);
2241
+ return true;
2242
+ }
2243
+ // One-shot elevation: migration is destructive (writes to new vault +
2244
+ // mutates .kit.toml). Same scope used for the other one-shot ops.
2245
+ const elev = await consumeElevation("vault-migrate");
2246
+ if (!elev.ok) {
2247
+ console.error(`${c.red}✗ ${elev.reason}${c.reset}`);
2248
+ console.error(`${c.dim}Run: kit auth elevate --scope vault-migrate${c.reset}`);
2249
+ return false;
2250
+ }
2251
+ const { vaultMigrate } = await import("./secrets-vault-migrate.js");
2252
+ const result = await vaultMigrate(config, {
2253
+ from: fromArg,
2254
+ to: toArg,
2255
+ dryRun: false,
2256
+ });
2257
+ console.log(`${c.bold}Results${c.reset}`);
2258
+ console.log(`${c.dim}${"─".repeat(50)}${c.reset}\n`);
2259
+ for (const item of result.items) {
2260
+ const marker = item.ok ? `${c.green}✓${c.reset}` : `${c.red}✗${c.reset}`;
2261
+ const refDetail = item.newRef ? ` ${c.dim}(${item.newRef})${c.reset}` : "";
2262
+ console.log(` ${marker} ${item.name} ${c.dim}${item.detail}${c.reset}${refDetail}`);
2263
+ }
2264
+ console.log();
2265
+ const failed = result.discovered - result.succeeded;
2266
+ console.log(`${result.succeeded > 0 ? c.green : c.dim}${result.succeeded} key(s) migrated${c.reset}${failed > 0 ? `, ${c.red}${failed} failed${c.reset}` : ""}.`);
2267
+ if (result.succeeded > 0) {
2268
+ console.log(`\n${c.dim}Next steps:${c.reset}`);
2269
+ console.log(` ${c.dim}1. Verify: kit check${c.reset}`);
2270
+ console.log(` ${c.dim}2. Rotate values in old vault (optional but recommended):${c.reset}`);
2271
+ console.log(` ${c.bold}kit secrets revoke-old --vault ${fromArg}${c.reset}`);
2272
+ }
2273
+ return failed === 0;
2274
+ }
2275
+ async function cmdSecretsSync() {
2276
+ const args = process.argv.slice(3); // after "secrets sync"
2277
+ const targetArg = args.find((a) => a.startsWith("--target="))?.split("=")[1];
2278
+ const dryRun = hasFlag(args, "--dry-run");
2279
+ const target = (targetArg ?? "stdout");
2280
+ const config = await loadConfig(resolveConfigPath());
2281
+ if (!config.secrets) {
2282
+ console.log(`${c.dim}No secrets configured in ${KIT_FILE}${c.reset}`);
2283
+ return true;
2284
+ }
2285
+ console.log(`${c.bold}${c.cyan}kit secrets sync${c.reset} → ${c.bold}${target}${c.reset}${dryRun ? ` ${c.yellow}(dry run)${c.reset}` : ""}\n`);
2286
+ const result = await syncSecrets(config.secrets, {
2287
+ target,
2288
+ dryRun,
2289
+ projectPath: process.cwd(),
2290
+ });
2291
+ if (result.synced.length > 0) {
2292
+ console.log(`${c.green}✓${c.reset} Synced:`);
2293
+ result.synced.forEach((k) => console.log(` ${c.green}+${c.reset} ${k}`));
2294
+ }
2295
+ if (result.skipped.length > 0 && dryRun) {
2296
+ console.log(`${c.dim}Would sync:${c.reset}`);
2297
+ result.skipped.forEach((k) => console.log(` ${c.dim}~ ${k}${c.reset}`));
2298
+ }
2299
+ if (result.failed.length > 0) {
2300
+ console.log(`${c.red}✗${c.reset} Failed:`);
2301
+ result.failed.forEach((k) => console.log(` ${c.red}✗${c.reset} ${k}`));
2302
+ }
2303
+ console.log(`\n${result.message}`);
2304
+ return result.failed.length === 0;
2305
+ }
2306
+ function detectCiFormat() {
2307
+ if (process.env.GITHUB_ACTIONS === "true")
2308
+ return "github";
2309
+ if (process.env.GITLAB_CI === "true")
2310
+ return "gitlab";
2311
+ if (process.env.CI === "true")
2312
+ return "text";
2313
+ return "text";
2314
+ }
2315
+ function emitGithubAnnotations(checks) {
2316
+ for (const ch of checks) {
2317
+ if (ch.status === "fail") {
2318
+ console.log(`::error::${ch.category}/${ch.name}: ${ch.detail}`);
2319
+ }
2320
+ else if (ch.status === "warn") {
2321
+ console.log(`::warning::${ch.category}/${ch.name}: ${ch.detail}`);
2322
+ }
2323
+ }
2324
+ }
2325
+ function emitGitlabJunit(checks, allOk) {
2326
+ const failures = checks.filter((c) => c.status === "fail");
2327
+ const warnings = checks.filter((c) => c.status === "warn");
2328
+ const lines = [
2329
+ `<?xml version="1.0" encoding="UTF-8"?>`,
2330
+ `<testsuites name="kit-ci" tests="${checks.length}" failures="${failures.length}" errors="0">`,
2331
+ ` <testsuite name="kit" tests="${checks.length}" failures="${failures.length}">`,
2332
+ ];
2333
+ for (const ch of checks) {
2334
+ lines.push(` <testcase name="${ch.name}" classname="${ch.category}">`);
2335
+ if (ch.status === "fail") {
2336
+ lines.push(` <failure message="${ch.detail}"/>`);
2337
+ }
2338
+ else if (ch.status === "warn") {
2339
+ lines.push(` <system-out>${ch.detail}</system-out>`);
2340
+ }
2341
+ lines.push(` </testcase>`);
2342
+ }
2343
+ lines.push(` </testsuite>`, `</testsuites>`);
2344
+ const xml = lines.join("\n");
2345
+ // Write to file for GitLab artifact collection
2346
+ import("node:fs/promises").then(({ writeFile }) => writeFile("kit-report.xml", xml, "utf8"));
2347
+ if (!allOk || warnings.length > 0) {
2348
+ console.log(`CI report written to kit-report.xml (${failures.length} failures, ${warnings.length} warnings)`);
2349
+ }
2350
+ }
2351
+ async function cmdCi() {
2352
+ const args = process.argv.slice(2);
2353
+ const formatArg = args.find((a) => a.startsWith("--format="))?.split("=")[1];
2354
+ const failOnWarning = hasFlag(args, "--fail-on-warning");
2355
+ const jsonMode = hasFlag(args, "--json");
2356
+ const format = formatArg ?? (jsonMode ? "json" : detectCiFormat());
2357
+ const config = await loadConfig(resolveConfigPath());
2358
+ return await withGovernance(config, { operation: "check", operationType: "read", metadata: {} }, async () => {
2359
+ const live = format === "text";
2360
+ if (live)
2361
+ stepHeader("CI checks");
2362
+ const step = (label, fn) => live ? runStep(label, fn) : fn();
2363
+ const toolResults = config.tools
2364
+ ? await step("tools", () => checkTools(config.tools))
2365
+ : [];
2366
+ const serviceResults = config.services
2367
+ ? await step("services", () => checkServices(config.services))
2368
+ : [];
2369
+ const secretResults = config.secrets
2370
+ ? await step("secrets", () => checkSecrets(config.secrets))
2371
+ : { templateExists: null, keys: [] };
2372
+ const skillResults = config.skills
2373
+ ? await step("skills", () => checkSkills(config.skills))
2374
+ : [];
2375
+ const securityResults = await step("security scan", () => checkSecurity());
2376
+ const lockResults = await step("lock files", () => checkLockFiles(config));
2377
+ const checks = [
2378
+ ...toolResults.map((t) => ({
2379
+ name: t.name,
2380
+ status: (t.ok ? "pass" : "fail"),
2381
+ detail: t.installed ? `installed ${t.installed}` : "not installed",
2382
+ category: "tools",
2383
+ })),
2384
+ ...serviceResults.map((s) => ({
2385
+ name: s.name,
2386
+ status: (s.authenticated ? "pass" : "fail"),
2387
+ detail: s.output ?? (s.authenticated ? "authenticated" : "not authenticated"),
2388
+ category: "services",
2389
+ })),
2390
+ ...secretResults.keys.map((s) => ({
2391
+ name: s.name,
2392
+ status: (s.available ? "pass" : "fail"),
2393
+ detail: s.detail ?? (s.available ? "available" : "missing"),
2394
+ category: "secrets",
2395
+ })),
2396
+ ...skillResults.map((s) => ({
2397
+ name: s.name,
2398
+ status: (s.installed ? "pass" : s.required ? "fail" : "warn"),
2399
+ detail: s.installed ? "installed" : "not installed",
2400
+ category: "skills",
2401
+ })),
2402
+ ...lockResults.map((l) => ({
2403
+ name: l.category === "skills-lock" ? "skills-lock.json" : "cli-lock.json",
2404
+ status: (l.inSync ? "pass" : l.exists ? "warn" : "fail"),
2405
+ detail: l.detail,
2406
+ category: "lock",
2407
+ })),
2408
+ ...securityResults.map((s) => ({
2409
+ name: s.name,
2410
+ status: s.status,
2411
+ detail: s.detail,
2412
+ category: `security/${s.category}`,
2413
+ })),
2414
+ ];
2415
+ const summary = checks.reduce((acc, c) => {
2416
+ if (c.status === "pass")
2417
+ acc.passed++;
2418
+ else if (c.status === "fail")
2419
+ acc.failed++;
2420
+ else if (c.status === "warn")
2421
+ acc.warnings++;
2422
+ else
2423
+ acc.skipped++;
2424
+ return acc;
2425
+ }, { passed: 0, failed: 0, warnings: 0, skipped: 0 });
2426
+ const allOk = summary.failed === 0 && (!failOnWarning || summary.warnings === 0);
2427
+ if (format === "github") {
2428
+ if (process.env.GITHUB_STEP_SUMMARY) {
2429
+ // Emit markdown summary to GitHub Actions step summary
2430
+ const lines = [
2431
+ "## kit CI Report",
2432
+ `| Status | Check | Detail |`,
2433
+ `|--------|-------|--------|`,
2434
+ ...checks.map((c) => `| ${c.status === "pass" ? "✅" : c.status === "warn" ? "⚠️" : "❌"} | \`${c.category}/${c.name}\` | ${c.detail} |`),
2435
+ ``,
2436
+ `**${summary.passed} passed, ${summary.failed} failed, ${summary.warnings} warnings**`,
2437
+ ];
2438
+ await import("node:fs/promises").then(({ appendFile }) => appendFile(process.env.GITHUB_STEP_SUMMARY, lines.join("\n") + "\n"));
2439
+ }
2440
+ emitGithubAnnotations(checks);
2441
+ console.log(`kit ci: ${summary.passed} passed, ${summary.failed} failed, ${summary.warnings} warnings`);
2442
+ }
2443
+ else if (format === "gitlab") {
2444
+ emitGitlabJunit(checks, allOk);
2445
+ console.log(`kit ci: ${summary.passed} passed, ${summary.failed} failed, ${summary.warnings} warnings`);
2446
+ }
2447
+ else if (format === "json") {
2448
+ const output = { ok: allOk, checks, summary };
2449
+ console.log(JSON.stringify(output, null, 2));
2450
+ }
2451
+ else {
2452
+ // text
2453
+ const failures = checks.filter((c) => c.status === "fail");
2454
+ const warnings = checks.filter((c) => c.status === "warn");
2455
+ if (failures.length > 0) {
2456
+ console.log(`FAILURES:`);
2457
+ failures.forEach((f) => console.log(` ✗ [${f.category}] ${f.name}: ${f.detail}`));
2458
+ }
2459
+ if (warnings.length > 0) {
2460
+ console.log(`WARNINGS:`);
2461
+ warnings.forEach((w) => console.log(` ! [${w.category}] ${w.name}: ${w.detail}`));
2462
+ }
2463
+ console.log(`kit ci: ${summary.passed} passed, ${summary.failed} failed, ${summary.warnings} warnings`);
2464
+ }
2465
+ return allOk;
2466
+ });
2467
+ }
2468
+ async function cmdAnalyze() {
2469
+ // Flags: --claude, --rules to emit one or the other; default is both.
2470
+ // --write <dir> persists the drafts (suffix .draft.md so the user reviews
2471
+ // before committing).
2472
+ const args = process.argv.slice(3);
2473
+ const wantClaude = hasFlag(args, "--claude") || (!hasFlag(args, "--rules") && !hasFlag(args, "--claude"));
2474
+ const wantRules = hasFlag(args, "--rules") || (!hasFlag(args, "--rules") && !hasFlag(args, "--claude"));
2475
+ const writeFlagIdx = args.indexOf("--write");
2476
+ const writeDir = writeFlagIdx >= 0 ? args[writeFlagIdx + 1] ?? process.cwd() : null;
2477
+ console.log(`${c.bold}${c.cyan}kit analyze${c.reset}`);
2478
+ console.log(`${c.dim}${"─".repeat(50)}${c.reset}\n`);
2479
+ const report = await analyzeRepo(process.cwd());
2480
+ // Summary line — terse, machine-readable enough to grep
2481
+ const stack = report.stack;
2482
+ console.log(`${c.bold}Detected:${c.reset} ${stack.language}${stack.framework ? ` / ${stack.framework}` : ""}${stack.services.length ? ` + ${stack.services.join(", ")}` : ""} ${c.dim}(confidence ${(stack.confidence * 100).toFixed(0)}%)${c.reset}`);
2483
+ if (report.testRunners.length)
2484
+ console.log(`${c.dim}Tests:${c.reset} ${report.testRunners.join(", ")}`);
2485
+ if (report.deployTargets.length)
2486
+ console.log(`${c.dim}Deploy:${c.reset} ${report.deployTargets.join(", ")}`);
2487
+ if (report.databaseClients.length)
2488
+ console.log(`${c.dim}DB clients:${c.reset} ${report.databaseClients.join(", ")}`);
2489
+ if (report.ciFiles.length)
2490
+ console.log(`${c.dim}CI:${c.reset} ${report.ciFiles.length} workflow(s)`);
2491
+ if (report.commitPrefixes.length)
2492
+ console.log(`${c.dim}Commit prefixes:${c.reset} ${report.commitPrefixes
2493
+ .slice(0, 5)
2494
+ .map((p) => `${p.prefix}(${p.count})`)
2495
+ .join(", ")}`);
2496
+ console.log();
2497
+ if (writeDir) {
2498
+ const { writeFile } = await import("node:fs/promises");
2499
+ if (wantClaude) {
2500
+ const path = `${writeDir}/CLAUDE.md.draft`;
2501
+ await writeFile(path, renderClaudeMd(report), "utf-8");
2502
+ console.log(` ${c.green}✓${c.reset} wrote ${path}`);
2503
+ if (report.hasClaudeMd) {
2504
+ console.log(` ${c.dim}(existing CLAUDE.md found — review draft before merging)${c.reset}`);
2505
+ }
2506
+ }
2507
+ if (wantRules) {
2508
+ const path = `${writeDir}/RULES.md.draft`;
2509
+ await writeFile(path, renderRulesMd(report), "utf-8");
2510
+ console.log(` ${c.green}✓${c.reset} wrote ${path}`);
2511
+ if (report.hasRulesMd) {
2512
+ console.log(` ${c.dim}(existing RULES.md found — review draft before merging)${c.reset}`);
2513
+ }
2514
+ }
2515
+ console.log();
2516
+ return true;
2517
+ }
2518
+ // No --write: print drafts to stdout for piping / inspection.
2519
+ if (wantClaude) {
2520
+ console.log(`${c.bold}── CLAUDE.md ${"─".repeat(40)}${c.reset}`);
2521
+ process.stdout.write(renderClaudeMd(report));
2522
+ console.log();
2523
+ }
2524
+ if (wantRules) {
2525
+ console.log(`${c.bold}── RULES.md ${"─".repeat(40)}${c.reset}`);
2526
+ process.stdout.write(renderRulesMd(report));
2527
+ console.log();
2528
+ }
2529
+ console.log(`${c.dim}Pipe to a file, or run with ${c.bold}--write .${c.reset}${c.dim} to materialize as CLAUDE.md.draft + RULES.md.draft${c.reset}`);
2530
+ return true;
2531
+ }
2532
+ /**
2533
+ * `kit security prescan <path> [--deep]` — multi-repo baseline sweep.
2534
+ *
2535
+ * Walks every git repo under <path> and emits a security-finding report
2536
+ * (default-bundle: secret-leak, gitignore-holes, tracked-secret-files,
2537
+ * branch-protection, public/private). `--deep` adds npm-audit-high,
2538
+ * workflow-drift, and kit-audit-gap checks.
2539
+ *
2540
+ * Report written to ~/.kit/prescans/<timestamp>.{jsonl,summary.md}.
2541
+ *
2542
+ * Designed for first-install onboarding: point kit at ~/projects/
2543
+ * and get a baseline of every actionable security gap across the
2544
+ * whole codebase. Read-only — no vendor state is modified.
2545
+ */
2546
+ async function cmdSecurityPrescan() {
2547
+ const root = process.argv[4];
2548
+ if (!root) {
2549
+ console.error(`${c.red}Usage: kit security prescan <path> [--deep] [--exclude=<substr>,<substr>] [--only=<check>,<check>] [--skip=<check>,<check>] [--vs-baseline=<path.jsonl>] [--format=text|json]${c.reset}`);
2550
+ console.error(`${c.dim}Example: kit security prescan ~/projects --deep --exclude=convex-backend,sentry-self-hosted${c.reset}`);
2551
+ return false;
2552
+ }
2553
+ const deep = hasFlag(process.argv, "--deep");
2554
+ const excludeArg = process.argv.find((a) => a.startsWith("--exclude="));
2555
+ const exclude = excludeArg
2556
+ ? excludeArg.slice("--exclude=".length).split(",").map((s) => s.trim()).filter(Boolean)
2557
+ : [];
2558
+ const onlyArg = process.argv.find((a) => a.startsWith("--only="));
2559
+ const onlyChecks = onlyArg
2560
+ ? onlyArg.slice("--only=".length).split(",").map((s) => s.trim()).filter(Boolean)
2561
+ : undefined;
2562
+ const skipArg = process.argv.find((a) => a.startsWith("--skip="));
2563
+ const skipChecks = skipArg
2564
+ ? skipArg.slice("--skip=".length).split(",").map((s) => s.trim()).filter(Boolean)
2565
+ : undefined;
2566
+ const formatArg = process.argv.find((a) => a.startsWith("--format="))?.split("=")[1];
2567
+ const format = formatArg === "json" ? "json" : "text";
2568
+ // --vs-baseline=<path> turns prescan into a drift-detector: run once,
2569
+ // diff against a baseline JSONL, output ONLY new regressions, exit 1 if any.
2570
+ // Designed for cron / systemd-timer / GitHub Actions schedule.
2571
+ const baselineArg = process.argv.find((a) => a.startsWith("--vs-baseline="));
2572
+ const vsBaseline = baselineArg ? baselineArg.slice("--vs-baseline=".length) : undefined;
2573
+ const { runPrescan } = await import("./security-prescan.js");
2574
+ if (format === "text") {
2575
+ console.log(`${c.bold}${c.cyan}kit security prescan${c.reset}`);
2576
+ console.log(`${c.dim}${"─".repeat(50)}${c.reset}\n`);
2577
+ console.log(` ${c.dim}root:${c.reset} ${root}`);
2578
+ console.log(` ${c.dim}mode:${c.reset} ${deep ? "deep (default + CVE + workflow-drift + audit-gap + bumblebee)" : "default-bundle"}`);
2579
+ if (exclude.length)
2580
+ console.log(` ${c.dim}exclude:${c.reset} ${exclude.join(", ")}`);
2581
+ console.log();
2582
+ }
2583
+ const startMs = Date.now();
2584
+ const report = await runPrescan({
2585
+ root: resolve(root),
2586
+ deep,
2587
+ exclude,
2588
+ onlyChecks,
2589
+ skipChecks,
2590
+ });
2591
+ const durSec = Math.round((Date.now() - startMs) / 1000);
2592
+ // --vs-baseline drift mode: diff current report against baseline JSONL,
2593
+ // emit ONLY added (regressions). Cron-friendly: exit 1 on regression.
2594
+ if (vsBaseline) {
2595
+ const { loadReport, diffReports } = await import("./security-prescan.js");
2596
+ const baseline = await loadReport(vsBaseline);
2597
+ const diff = diffReports(baseline, report);
2598
+ if (format === "json") {
2599
+ process.stdout.write(JSON.stringify({
2600
+ baseline: vsBaseline,
2601
+ addedCount: diff.added.length,
2602
+ removedCount: diff.removed.length,
2603
+ unchangedCount: diff.unchanged.length,
2604
+ added: diff.added,
2605
+ }, null, 2) + "\n");
2606
+ }
2607
+ else {
2608
+ console.log(`${c.bold}${c.cyan}drift report${c.reset}: ${diff.added.length} new finding(s) since baseline`);
2609
+ if (diff.added.length === 0) {
2610
+ console.log(`${c.green}✓ no regressions${c.reset}`);
2611
+ }
2612
+ else {
2613
+ for (const f of diff.added.slice(0, 30)) {
2614
+ const sevColor = f.severity === "critical" || f.severity === "high" ? c.red : c.yellow;
2615
+ console.log(` ${sevColor}•${c.reset} ${f.repo} — ${f.category} (${f.severity}): ${f.detail.slice(0, 80)}`);
2616
+ }
2617
+ if (diff.added.length > 30)
2618
+ console.log(` ${c.dim}…and ${diff.added.length - 30} more${c.reset}`);
2619
+ }
2620
+ }
2621
+ return diff.added.length === 0;
2622
+ }
2623
+ const bySev = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
2624
+ for (const f of report.findings)
2625
+ bySev[f.severity] = (bySev[f.severity] ?? 0) + 1;
2626
+ if (format === "json") {
2627
+ // Emit machine-readable summary; raw findings live in report.reportPath.
2628
+ process.stdout.write(JSON.stringify({
2629
+ startedAt: report.startedAt,
2630
+ finishedAt: report.finishedAt,
2631
+ durationSec: durSec,
2632
+ root: report.root,
2633
+ mode: deep ? "deep" : "default",
2634
+ exclude,
2635
+ repoCount: report.repoCount,
2636
+ findingCount: report.findings.length,
2637
+ bySeverity: bySev,
2638
+ reportPath: report.reportPath,
2639
+ summaryPath: report.summaryPath,
2640
+ findings: report.findings,
2641
+ }, null, 2) + "\n");
2642
+ return bySev.critical === 0 && bySev.high === 0;
2643
+ }
2644
+ console.log(`${c.bold}Scanned${c.reset} ${report.repoCount} repo(s) in ${durSec}s`);
2645
+ console.log(`${c.bold}Findings${c.reset}: ${report.findings.length} total`);
2646
+ if (bySev.critical)
2647
+ console.log(` ${c.red}critical${c.reset}: ${bySev.critical}`);
2648
+ if (bySev.high)
2649
+ console.log(` ${c.red}high ${c.reset}: ${bySev.high}`);
2650
+ if (bySev.medium)
2651
+ console.log(` ${c.yellow}medium ${c.reset}: ${bySev.medium}`);
2652
+ if (bySev.low)
2653
+ console.log(` ${c.dim}low ${c.reset}: ${bySev.low}`);
2654
+ if (bySev.info)
2655
+ console.log(` ${c.dim}info ${c.reset}: ${bySev.info}`);
2656
+ console.log();
2657
+ if (report.reportPath)
2658
+ console.log(`${c.dim} raw:${c.reset} ${report.reportPath}`);
2659
+ if (report.summaryPath)
2660
+ console.log(`${c.dim} summary:${c.reset} ${report.summaryPath}`);
2661
+ console.log();
2662
+ return bySev.critical === 0 && bySev.high === 0;
2663
+ }
2664
+ /**
2665
+ * `kit security prescan-diff <baseline.jsonl> <latest.jsonl>` — drift report.
2666
+ *
2667
+ * Reads two prescan-JSONL files (the raw report, NOT the summary.md), and
2668
+ * surfaces three buckets: added (regressions), removed (fixed), unchanged.
2669
+ *
2670
+ * Use case: schedule a baseline prescan at first-install, then re-run
2671
+ * weekly and diff against the baseline to surface NEW security gaps
2672
+ * introduced since.
2673
+ */
2674
+ async function cmdSecurityPrescanDiff() {
2675
+ const baseline = process.argv[4];
2676
+ const latest = process.argv[5];
2677
+ if (!baseline || !latest) {
2678
+ console.error(`${c.red}Usage: kit security prescan-diff <baseline.jsonl> <latest.jsonl> [--format=text|json]${c.reset}`);
2679
+ console.error(`${c.dim}Example: kit security prescan-diff ~/.kit/prescans/baseline.jsonl ~/.kit/prescans/latest.jsonl${c.reset}`);
2680
+ return false;
2681
+ }
2682
+ const formatArg = process.argv.find((a) => a.startsWith("--format="))?.split("=")[1];
2683
+ const format = formatArg === "json" ? "json" : "text";
2684
+ const { loadReport, diffReports } = await import("./security-prescan.js");
2685
+ const a = await loadReport(baseline);
2686
+ const b = await loadReport(latest);
2687
+ const diff = diffReports(a, b);
2688
+ if (format === "json") {
2689
+ process.stdout.write(JSON.stringify({
2690
+ baseline,
2691
+ latest,
2692
+ addedCount: diff.added.length,
2693
+ removedCount: diff.removed.length,
2694
+ unchangedCount: diff.unchanged.length,
2695
+ added: diff.added,
2696
+ removed: diff.removed,
2697
+ }, null, 2) + "\n");
2698
+ return diff.added.length === 0;
2699
+ }
2700
+ console.log(`${c.bold}${c.cyan}kit security prescan-diff${c.reset}`);
2701
+ console.log(`${c.dim}${"─".repeat(50)}${c.reset}\n`);
2702
+ console.log(` ${c.dim}baseline:${c.reset} ${baseline}`);
2703
+ console.log(` ${c.dim}latest: ${c.reset} ${latest}\n`);
2704
+ console.log(`${c.bold}Added (regressions)${c.reset}: ${diff.added.length > 0 ? c.red : c.green}${diff.added.length}${c.reset}`);
2705
+ console.log(`${c.bold}Removed (fixed)${c.reset}: ${c.green}${diff.removed.length}${c.reset}`);
2706
+ console.log(`${c.bold}Unchanged${c.reset}: ${c.dim}${diff.unchanged.length}${c.reset}\n`);
2707
+ if (diff.added.length) {
2708
+ console.log(`${c.red}New findings since baseline:${c.reset}`);
2709
+ for (const f of diff.added.slice(0, 20)) {
2710
+ console.log(` ${c.red}•${c.reset} ${f.repo} — ${f.category}: ${f.detail.slice(0, 80)}`);
2711
+ }
2712
+ if (diff.added.length > 20) {
2713
+ console.log(` ${c.dim}…and ${diff.added.length - 20} more${c.reset}`);
2714
+ }
2715
+ console.log();
2716
+ }
2717
+ if (diff.removed.length) {
2718
+ console.log(`${c.green}Resolved since baseline:${c.reset}`);
2719
+ for (const f of diff.removed.slice(0, 10)) {
2720
+ console.log(` ${c.green}✓${c.reset} ${f.repo} — ${f.category}: ${f.detail.slice(0, 80)}`);
2721
+ }
2722
+ if (diff.removed.length > 10) {
2723
+ console.log(` ${c.dim}…and ${diff.removed.length - 10} more${c.reset}`);
2724
+ }
2725
+ console.log();
2726
+ }
2727
+ // Exit 0 = no regressions; 1 = regressions present (CI-friendly).
2728
+ return diff.added.length === 0;
2729
+ }
2730
+ async function cmdSecurity() {
2731
+ // Subcommand routing
2732
+ const sub = process.argv[3];
2733
+ if (sub === "clear-cache") {
2734
+ return cmdSecurityClearCache();
2735
+ }
2736
+ if (sub === "scan-staged") {
2737
+ return cmdSecurityScanStaged();
2738
+ }
2739
+ if (sub === "scan-build") {
2740
+ return cmdSecurityScanBuild();
2741
+ }
2742
+ if (sub === "scan-transcripts") {
2743
+ return cmdSecurityScanTranscripts();
2744
+ }
2745
+ if (sub === "costs") {
2746
+ return cmdSecurityCosts();
2747
+ }
2748
+ if (sub === "check-gitignore") {
2749
+ return cmdSecurityCheckGitignore();
2750
+ }
2751
+ if (sub === "verify-pull") {
2752
+ return cmdSecurityVerifyPull();
2753
+ }
2754
+ if (sub === "prescan") {
2755
+ return cmdSecurityPrescan();
2756
+ }
2757
+ if (sub === "prescan-diff") {
2758
+ return cmdSecurityPrescanDiff();
2759
+ }
2760
+ if (sub !== "policy") {
2761
+ console.error(`${c.red}Usage: kit security policy [init|add <pkg>|check] | scan-staged | scan-build [dir...] | scan-transcripts | costs | check-gitignore [--fix] | verify-pull [--base <ref>] | prescan <path> [--deep] | prescan-diff <baseline.jsonl> <latest.jsonl> | clear-cache${c.reset}`);
2762
+ return false;
2763
+ }
2764
+ const action = process.argv[4] ?? "check";
2765
+ console.log(`${c.bold}${c.cyan}kit security policy${c.reset}`);
2766
+ console.log(`${c.dim}${"─".repeat(50)}${c.reset}\n`);
2767
+ if (action === "init") {
2768
+ const list = await initAllowlist(process.cwd());
2769
+ console.log(` ${c.green}✓${c.reset} wrote .kit-allowlist.json ${c.dim}(${list.packages.length} package(s) recorded)${c.reset}`);
2770
+ console.log(`\n${c.dim}Defaults: enforce_runtime=true, enforce_dev=false, allow_wildcards=false. Edit ${c.bold}.kit-allowlist.json${c.reset}${c.dim} to tighten or relax.${c.reset}\n`);
2771
+ return true;
2772
+ }
2773
+ if (action === "add") {
2774
+ const pkgName = process.argv[5];
2775
+ if (!pkgName) {
2776
+ console.error(`${c.red}Usage: kit security policy add <pkg>${c.reset}`);
2777
+ return false;
2778
+ }
2779
+ const { added, entry } = await addToAllowlist(pkgName, process.cwd());
2780
+ if (!entry) {
2781
+ console.error(`${c.red}Package "${pkgName}" not found in package.json — install it first.${c.reset}`);
2782
+ return false;
2783
+ }
2784
+ if (added) {
2785
+ console.log(` ${c.green}✓${c.reset} added ${c.bold}${entry.name}${c.reset} @ ${entry.range} ${c.dim}(${entry.reason})${c.reset}`);
2786
+ }
2787
+ else {
2788
+ console.log(` ${c.dim}${entry.name}${c.reset} already on the allowlist @ ${entry.range}`);
2789
+ }
2790
+ return true;
2791
+ }
2792
+ // check (default)
2793
+ const { list, violations } = await checkAllowlist(process.cwd());
2794
+ if (!list) {
2795
+ console.log(`${c.yellow}No .kit-allowlist.json found.${c.reset} ${c.dim}Run ${c.bold}kit security policy init${c.reset}${c.dim} to bootstrap.${c.reset}\n`);
2796
+ return false;
2797
+ }
2798
+ // Load .kit.toml to evaluate the secrets policy section against the
2799
+ // keys the project actually references. Missing config is non-fatal —
2800
+ // policy.secrets just doesn't apply.
2801
+ let secretViolations = [];
2802
+ try {
2803
+ const dkConfig = await loadConfig(resolveConfigPath());
2804
+ const keys = Object.keys(dkConfig.secrets?.keys ?? {});
2805
+ if (keys.length > 0) {
2806
+ secretViolations = checkSecretPolicy(list, keys);
2807
+ }
2808
+ }
2809
+ catch {
2810
+ /* no .kit.toml — skip secrets policy silently */
2811
+ }
2812
+ if (violations.length === 0 && secretViolations.length === 0) {
2813
+ console.log(`${c.green}✓${c.reset} All dependencies + secrets satisfy policy.\n`);
2814
+ return true;
2815
+ }
2816
+ const fmt = (kind) => kind === "runtime" ? `${c.red}runtime${c.reset}` : `${c.yellow}dev${c.reset}`;
2817
+ if (violations.length > 0) {
2818
+ console.log(`${c.red}✗ ${violations.length} dependency violation(s):${c.reset}\n`);
2819
+ for (const v of violations) {
2820
+ const why = v.reason === "not-on-allowlist"
2821
+ ? "not on allowlist"
2822
+ : "wildcard range blocked";
2823
+ console.log(` ${c.red}•${c.reset} ${v.name} @ ${v.range} ${c.dim}[${fmt(v.kind)}]${c.reset} ${c.dim}${why}${c.reset}`);
2824
+ }
2825
+ console.log();
2826
+ }
2827
+ if (secretViolations.length > 0) {
2828
+ console.log(`${c.red}✗ ${secretViolations.length} secrets policy violation(s):${c.reset}\n`);
2829
+ for (const sv of secretViolations) {
2830
+ console.log(` ${c.red}•${c.reset} ${sv.key} ${c.dim}[${sv.reason}]${c.reset} ${c.dim}${sv.detail}${c.reset}`);
2831
+ }
2832
+ console.log(`\n${c.dim}Edit ${c.bold}.kit-allowlist.json${c.reset}${c.dim} under ${c.bold}"secrets"${c.reset}${c.dim} to declare scope, TTL, and spend caps per key.${c.reset}`);
2833
+ }
2834
+ console.log(`\n${c.dim}Add a package: ${c.bold}kit security policy add <pkg>${c.reset}${c.dim}, or edit .kit-allowlist.json directly.${c.reset}\n`);
2835
+ return false;
2836
+ }
2837
+ async function cmdSecurityScanStaged() {
2838
+ // Designed to run from a git pre-commit hook. Prints nothing on success so
2839
+ // the hook output stays quiet for normal commits; exits non-zero with a
2840
+ // structured report when a credential pattern is staged.
2841
+ const hits = await scanStagedFiles();
2842
+ if (hits.length === 0)
2843
+ return true;
2844
+ const total = hits.reduce((sum, h) => sum + h.findings.length, 0);
2845
+ console.error(`${c.red}✗ kit secret-scan blocked the commit${c.reset}`);
2846
+ console.error(`${c.dim}Found ${total} potential secret(s) in ${hits.length} staged file(s):${c.reset}`);
2847
+ for (const hit of hits) {
2848
+ const labels = hit.findings
2849
+ .map((f) => `${f.label}:${f.preview}`)
2850
+ .join(", ");
2851
+ console.error(` ${c.red}•${c.reset} ${hit.file} ${c.dim}${labels}${c.reset}`);
2852
+ }
2853
+ console.error(`\n${c.dim}If a finding is a false positive, you can bypass with ${c.bold}git commit --no-verify${c.reset}${c.dim}, but prefer migrating the value to a vault first (${c.bold}kit secrets migrate${c.reset}${c.dim}).${c.reset}`);
2854
+ return false;
2855
+ }
2856
+ async function cmdSecurityScanBuild() {
2857
+ // Optional positional: extra dirs to scan beyond the defaults.
2858
+ const extras = process.argv.slice(4).filter((a) => !a.startsWith("--"));
2859
+ const hits = await scanBuildArtifacts(process.cwd(), extras.length > 0 ? extras : undefined);
2860
+ if (hits.length === 0) {
2861
+ console.log(`${c.green}✓ scan-build: no credential patterns in build artifacts.${c.reset}`);
2862
+ return true;
2863
+ }
2864
+ const total = hits.reduce((sum, h) => sum + h.findings.length, 0);
2865
+ console.error(`${c.red}✗ scan-build: ${total} potential secret(s) in ${hits.length} build file(s):${c.reset}`);
2866
+ for (const hit of hits.slice(0, 20)) {
2867
+ const labels = hit.findings.map((f) => `${f.label}:${f.preview}`).join(", ");
2868
+ console.error(` ${c.red}•${c.reset} ${hit.file} ${c.dim}${labels}${c.reset}`);
2869
+ }
2870
+ if (hits.length > 20) {
2871
+ console.error(` ${c.dim}… and ${hits.length - 20} more${c.reset}`);
2872
+ }
2873
+ console.error(`\n${c.dim}Typical cause: a server-only env var (e.g. STRIPE_SECRET_KEY) is referenced via NEXT_PUBLIC_* and got inlined into the client bundle. Rebuild after fixing.${c.reset}`);
2874
+ return false;
2875
+ }
2876
+ async function cmdSecurityScanTranscripts() {
2877
+ const hits = await scanTranscripts(process.cwd());
2878
+ if (hits.length === 0) {
2879
+ console.log(`${c.green}✓ scan-transcripts: no credentials found in agent state or prompt cache.${c.reset}`);
2880
+ return true;
2881
+ }
2882
+ const total = hits.reduce((sum, h) => sum + h.findings.length, 0);
2883
+ console.error(`${c.red}✗ scan-transcripts: ${total} potential secret(s) in ${hits.length} transcript/cache file(s):${c.reset}`);
2884
+ for (const hit of hits.slice(0, 20)) {
2885
+ const labels = hit.findings.map((f) => `${f.label}:${f.preview}`).join(", ");
2886
+ console.error(` ${c.red}•${c.reset} ${hit.file} ${c.dim}${labels}${c.reset}`);
2887
+ }
2888
+ if (hits.length > 20) {
2889
+ console.error(` ${c.dim}… and ${hits.length - 20} more${c.reset}`);
2890
+ }
2891
+ console.error(`\n${c.yellow}Rotate the leaked credential and purge the offending file(s) — they get replayed into every future agent prompt until cleared.${c.reset}\n`);
2892
+ return false;
2893
+ }
2894
+ async function cmdSecurityCosts() {
2895
+ console.log(`${c.bold}${c.cyan}kit security costs${c.reset}`);
2896
+ console.log(`${c.dim}${"─".repeat(50)}${c.reset}\n`);
2897
+ // Pull spend-caps from the allowlist (if present) so cost samples can be
2898
+ // compared against the declared policy.
2899
+ const { readAllowlist } = await import("./security-policy.js");
2900
+ const list = await readAllowlist(process.cwd());
2901
+ const caps = {};
2902
+ if (list?.secrets) {
2903
+ for (const [keyName, entry] of Object.entries(list.secrets)) {
2904
+ caps[keyName] = entry.spend_cap_usd ?? list.policy.default_spend_cap_usd;
2905
+ }
2906
+ }
2907
+ const samples = await sampleCosts({ caps });
2908
+ if (samples.length === 0) {
2909
+ console.log(`${c.dim}No supported provider keys found in env. Set STRIPE_SECRET_KEY (or other supported providers) to enable cost samples.${c.reset}\n`);
2910
+ return true;
2911
+ }
2912
+ let allOk = true;
2913
+ for (const s of samples) {
2914
+ const icon = s.status === "ok"
2915
+ ? `${c.green}✓${c.reset}`
2916
+ : s.status === "warn"
2917
+ ? `${c.yellow}!${c.reset}`
2918
+ : s.status === "over-cap" || s.status === "auth-failed"
2919
+ ? `${c.red}✗${c.reset}`
2920
+ : `${c.dim}-${c.reset}`;
2921
+ const capLabel = s.capUsd !== undefined
2922
+ ? `cap=${s.capUsd.toFixed(2)} USD`
2923
+ : `${c.yellow}no cap${c.reset}`;
2924
+ console.log(` ${icon} ${s.provider.padEnd(10)} current=${s.current.toFixed(2)} ${s.unit} ${capLabel} ${c.dim}${s.detail}${c.reset}`);
2925
+ if (s.status === "over-cap" || s.status === "auth-failed")
2926
+ allOk = false;
2927
+ }
2928
+ console.log();
2929
+ if (!allOk) {
2930
+ console.log(`${c.red}One or more keys are over cap or failed auth. Rotate / reduce / refresh.${c.reset}\n`);
2931
+ }
2932
+ return allOk;
2933
+ }
2934
+ async function cmdSecurityVerifyPull() {
2935
+ // kit security verify-pull [--base <ref>] [--head <ref>] [--json]
2936
+ const args = process.argv.slice(4);
2937
+ const baseIdx = args.indexOf("--base");
2938
+ const headIdx = args.indexOf("--head");
2939
+ const base = baseIdx >= 0 ? args[baseIdx + 1] : "HEAD~1";
2940
+ const head = headIdx >= 0 ? args[headIdx + 1] : "HEAD";
2941
+ const jsonMode = hasFlag(args, "--json");
2942
+ const report = await auditPull(process.cwd(), base, head);
2943
+ const severity = reportSeverity(report);
2944
+ if (jsonMode) {
2945
+ console.log(JSON.stringify({ severity, ...report }, null, 2));
2946
+ return severity !== "fail";
2947
+ }
2948
+ console.log(`${c.bold}${c.cyan}kit security verify-pull${c.reset} ${c.dim}(${base} → ${head})${c.reset}`);
2949
+ console.log(`${c.dim}${"─".repeat(50)}${c.reset}\n`);
2950
+ if (report.changedFiles.length === 0) {
2951
+ console.log(`${c.dim}No files changed in this range. Nothing to audit.${c.reset}\n`);
2952
+ return true;
2953
+ }
2954
+ console.log(`${c.dim}${report.changedFiles.length} file(s) changed${c.reset}\n`);
2955
+ if (report.newDependencies.length > 0) {
2956
+ console.log(`${c.yellow}⚠ ${report.newDependencies.length} new dependency/-ies:${c.reset}`);
2957
+ for (const dep of report.newDependencies) {
2958
+ console.log(` ${c.yellow}•${c.reset} ${dep} ${c.dim}→ run ${c.bold}kit triage npm ${dep}${c.reset}${c.dim} before installing${c.reset}`);
2959
+ }
2960
+ console.log();
2961
+ }
2962
+ if (report.removedGitignoreEntries.length > 0) {
2963
+ const sensitive = report.removedGitignoreEntries.filter((l) => /\.env|\.pem|\.key|id_rsa/.test(l));
2964
+ const severityIcon = sensitive.length > 0 ? `${c.red}✗` : `${c.yellow}⚠`;
2965
+ console.log(`${severityIcon}${c.reset} ${report.removedGitignoreEntries.length} .gitignore entry/-ies removed:`);
2966
+ for (const line of report.removedGitignoreEntries) {
2967
+ const hot = /\.env|\.pem|\.key|id_rsa/.test(line);
2968
+ const tag = hot ? `${c.red}[sensitive]${c.reset}` : `${c.dim}[low]${c.reset}`;
2969
+ console.log(` ${hot ? c.red : c.yellow}•${c.reset} ${line} ${tag}`);
2970
+ }
2971
+ console.log();
2972
+ }
2973
+ if (report.plaintextHits.length > 0) {
2974
+ const total = report.plaintextHits.reduce((sum, h) => sum + h.findings.length, 0);
2975
+ console.log(`${c.red}✗ ${total} plaintext secret(s) introduced in ${report.plaintextHits.length} file(s):${c.reset}`);
2976
+ for (const hit of report.plaintextHits.slice(0, 20)) {
2977
+ const labels = hit.findings
2978
+ .map((f) => `${f.label}:${f.preview}`)
2979
+ .join(", ");
2980
+ console.log(` ${c.red}•${c.reset} ${hit.file} ${c.dim}${labels}${c.reset}`);
2981
+ }
2982
+ if (report.plaintextHits.length > 20) {
2983
+ console.log(` ${c.dim}… ${report.plaintextHits.length - 20} more${c.reset}`);
2984
+ }
2985
+ console.log();
2986
+ }
2987
+ if (report.allowlistChanged || report.policyChanged || report.kitTomlChanged) {
2988
+ console.log(`${c.yellow}⚠ Security-policy files modified:${c.reset}`);
2989
+ if (report.allowlistChanged)
2990
+ console.log(` ${c.yellow}•${c.reset} .kit-allowlist.json`);
2991
+ if (report.policyChanged)
2992
+ console.log(` ${c.yellow}•${c.reset} .kit-policy.json`);
2993
+ if (report.kitTomlChanged)
2994
+ console.log(` ${c.yellow}•${c.reset} .kit.toml`);
2995
+ console.log(`${c.dim} → run ${c.bold}kit security policy check${c.reset}${c.dim} to verify the new state.${c.reset}\n`);
2996
+ }
2997
+ if (severity === "ok") {
2998
+ console.log(`${c.green}✓ No security concerns in this pull.${c.reset}\n`);
2999
+ return true;
3000
+ }
3001
+ if (severity === "warn") {
3002
+ console.log(`${c.yellow}⚠ Pull contains items worth a second look — address before running install/deploy.${c.reset}\n`);
3003
+ return true;
3004
+ }
3005
+ console.log(`${c.red}✗ Pull contains security regressions — review before continuing.${c.reset}\n`);
3006
+ return false;
3007
+ }
3008
+ async function cmdSecurityCheckGitignore() {
3009
+ const args = process.argv.slice(4);
3010
+ const fix = hasFlag(args, "--fix");
3011
+ console.log(`${c.bold}${c.cyan}kit security check-gitignore${c.reset}`);
3012
+ console.log(`${c.dim}${"─".repeat(50)}${c.reset}\n`);
3013
+ const result = await checkGitignore(process.cwd());
3014
+ const committed = await findCommittedSensitive(process.cwd());
3015
+ if (!result.exists) {
3016
+ console.log(`${c.yellow}⚠ No .gitignore file in this repo.${c.reset}\n`);
3017
+ }
3018
+ else {
3019
+ console.log(`${c.dim}.gitignore: ${result.presentPatterns.length}/${result.presentPatterns.length + result.missingPatterns.length} required patterns present${c.reset}\n`);
3020
+ }
3021
+ if (result.missingPatterns.length === 0) {
3022
+ console.log(`${c.green}✓ All required ignore patterns present.${c.reset}`);
3023
+ }
3024
+ else {
3025
+ console.log(`${c.red}✗ Missing ${result.missingPatterns.length} required pattern(s):${c.reset}\n`);
3026
+ for (const m of result.missingPatterns) {
3027
+ console.log(` ${c.red}•${c.reset} ${c.bold}${m.pattern.padEnd(28)}${c.reset} ${c.dim}${m.reason}${c.reset}`);
3028
+ }
3029
+ console.log();
3030
+ }
3031
+ if (committed.length > 0) {
3032
+ console.log(`${c.red}⚠ ${committed.length} sensitive file(s) already tracked in git:${c.reset}\n`);
3033
+ for (const path of committed.slice(0, 20)) {
3034
+ console.log(` ${c.red}•${c.reset} ${path}`);
3035
+ }
3036
+ if (committed.length > 20) {
3037
+ console.log(` ${c.dim}… ${committed.length - 20} more${c.reset}`);
3038
+ }
3039
+ console.log(`\n${c.yellow}Adding these to .gitignore does NOT untrack them. Use ${c.bold}git rm --cached <path>${c.reset}${c.yellow} + commit, then rotate any credentials they contained.${c.reset}\n`);
3040
+ }
3041
+ if (fix && result.missingPatterns.length > 0) {
3042
+ const patch = await patchGitignore(process.cwd());
3043
+ console.log(`${c.green}✓${c.reset} appended ${patch.added} pattern(s) to .gitignore`);
3044
+ console.log(`${c.dim}Review the new block, then ${c.bold}git add .gitignore && git commit${c.reset}${c.dim}.${c.reset}\n`);
3045
+ return true;
3046
+ }
3047
+ if (result.missingPatterns.length > 0 && !fix) {
3048
+ console.log(`${c.dim}Run with ${c.bold}--fix${c.reset}${c.dim} to append the missing patterns to .gitignore.${c.reset}\n`);
3049
+ }
3050
+ return result.missingPatterns.length === 0 && committed.length === 0;
3051
+ }
3052
+ async function cmdSecurityClearCache() {
3053
+ // Sub-sub: `kit security clear-cache [bumblebee]`. Defaults to bumblebee
3054
+ // since that's the only cached binary kit currently manages whose
3055
+ // checksum can mismatch in normal dev use.
3056
+ const target = process.argv[4] ?? "bumblebee";
3057
+ console.log(`${c.bold}${c.cyan}kit security clear-cache${c.reset}`);
3058
+ console.log(`${c.dim}${"─".repeat(50)}${c.reset}\n`);
3059
+ console.log(`${c.yellow}⚠ This will delete the cached ${target} binary.${c.reset}`);
3060
+ console.log(`${c.dim}Use when you have intentionally rebuilt the scanner locally (e.g. a feature branch) and the pinned checksum no longer matches.${c.reset}`);
3061
+ console.log(`${c.dim}If you did NOT rebuild and the checksum still mismatches, investigate for tampering — do not clear blindly.${c.reset}\n`);
3062
+ const nonInteractive = isNonInteractive();
3063
+ if (!nonInteractive) {
3064
+ const ok = await promptConfirm(`Continue? [Y/n] (auto-yes in 8s): `, 8000);
3065
+ if (!ok) {
3066
+ console.log(`${c.dim}Aborted.${c.reset}`);
3067
+ return false;
3068
+ }
3069
+ }
3070
+ if (target !== "bumblebee") {
3071
+ console.error(`${c.red}Unknown cache target: ${target} (only 'bumblebee' is supported)${c.reset}`);
3072
+ return false;
3073
+ }
3074
+ const result = await clearBumblebeeCache();
3075
+ if (result.removed) {
3076
+ console.log(` ${c.green}✓${c.reset} removed ${result.path}`);
3077
+ console.log(`${c.dim}Next ${c.bold}kit check${c.reset}${c.dim} will re-download and re-verify the scanner.${c.reset}\n`);
3078
+ }
3079
+ else {
3080
+ console.log(` ${c.dim}nothing to remove at ${result.path}${c.reset}\n`);
3081
+ }
3082
+ return true;
3083
+ }
3084
+ async function cmdCreatePlugin() {
3085
+ const pluginName = process.argv[3];
3086
+ if (!pluginName) {
3087
+ console.error(`${c.red}Usage: kit create-plugin <name>${c.reset}`);
3088
+ console.error(`${c.dim}Example: kit create-plugin aws-s3${c.reset}`);
3089
+ console.error(`${c.dim}Creates ./kit-plugin-aws-s3/ with a working TypeScript adapter.${c.reset}`);
3090
+ return false;
3091
+ }
3092
+ const skipInstall = hasFlag(process.argv, "--skip-install");
3093
+ console.log(`${c.bold}${c.cyan}Scaffolding kit plugin...${c.reset}\n`);
3094
+ const result = await createPlugin({
3095
+ name: pluginName,
3096
+ cwd: process.cwd(),
3097
+ skipInstall,
3098
+ });
3099
+ if (result.success) {
3100
+ console.log(` ${c.green}✓${c.reset} ${result.message}`);
3101
+ console.log();
3102
+ console.log(`${c.bold}Next steps:${c.reset}`);
3103
+ for (const step of result.nextSteps) {
3104
+ console.log(` ${c.dim}${step}${c.reset}`);
3105
+ }
3106
+ console.log();
3107
+ console.log(`${c.dim}See PLUGIN_AUTHORING.md for full documentation.${c.reset}`);
3108
+ return true;
3109
+ }
3110
+ return false;
3111
+ }
3112
+ async function cmdClone() {
3113
+ const args = process.argv.slice(2);
3114
+ const repoUrl = args[1];
3115
+ const targetDir = args[2];
3116
+ const noSetup = hasFlag(args, "--no-setup");
3117
+ const environment = hasFlag(args, "--env") ? args[args.indexOf("--env") + 1] : "default";
3118
+ if (!repoUrl) {
3119
+ console.error(`${c.red}Usage: kit clone <repo-url> [directory]${c.reset}`);
3120
+ console.error();
3121
+ console.error(`${c.dim}Options:${c.reset}`);
3122
+ console.error(`${c.dim} --no-setup Skip running kit setup after cloning${c.reset}`);
3123
+ console.error(`${c.dim} --env <name> Environment to use for setup (default: "default")${c.reset}`);
3124
+ console.error();
3125
+ console.error(`${c.dim}Example:${c.reset}`);
3126
+ console.error(`${c.dim} kit clone https://github.com/sandstream/example my-project${c.reset}`);
3127
+ console.error(`${c.dim} kit clone https://github.com/sandstream/example my-project --env production${c.reset}`);
3128
+ console.error(`${c.dim} kit clone https://github.com/sandstream/example my-project --no-setup${c.reset}`);
3129
+ return false;
3130
+ }
3131
+ console.log(`${c.bold}${c.cyan}kit clone${c.reset}`);
3132
+ console.log(`${c.dim}${"─".repeat(50)}${c.reset}\n`);
3133
+ const cloneResult = await cloneRepository({
3134
+ repoUrl,
3135
+ targetDir,
3136
+ noSetup,
3137
+ environment,
3138
+ cwd: process.cwd(),
3139
+ });
3140
+ if (!cloneResult.success) {
3141
+ console.error(`${c.red}✗ Clone failed: ${cloneResult.message}${c.reset}`);
3142
+ return false;
3143
+ }
3144
+ console.log(`${c.green}✓ Repository cloned to ${cloneResult.clonedPath}${c.reset}`);
3145
+ console.log();
3146
+ if (!cloneResult.haskitToml) {
3147
+ console.log(`${c.yellow}⚠ No .kit.toml found in repository.${c.reset}`);
3148
+ console.log(`${c.dim}Create one with 'cd ${cloneResult.clonedPath} && kit init' to set up kit.${c.reset}`);
3149
+ console.log();
3150
+ return true;
3151
+ }
3152
+ if (cloneResult.setupSkipped) {
3153
+ console.log(`${c.yellow}Setup skipped (--no-setup flag set).${c.reset}`);
3154
+ console.log(`${c.dim}Run: cd ${cloneResult.clonedPath} && kit setup${c.reset}`);
3155
+ console.log();
3156
+ return true;
3157
+ }
3158
+ // Run setup in the cloned directory
3159
+ console.log(`${c.bold}Running setup in cloned repository...${c.reset}\n`);
3160
+ const originalCwd = process.cwd();
3161
+ try {
3162
+ process.chdir(cloneResult.clonedPath);
3163
+ const setupOk = await cmdSetup();
3164
+ process.chdir(originalCwd);
3165
+ if (!setupOk) {
3166
+ console.log(`${c.yellow}Clone completed but setup failed. See above for details.${c.reset}`);
3167
+ }
3168
+ return setupOk;
3169
+ }
3170
+ catch (err) {
3171
+ process.chdir(originalCwd);
3172
+ console.error(`${c.red}Error during setup: ${err instanceof Error ? err.message : String(err)}${c.reset}`);
3173
+ return false;
3174
+ }
3175
+ }
3176
+ async function cmdRun() {
3177
+ const args = process.argv.slice(2);
3178
+ // Find the -- separator
3179
+ const doubleDashIndex = args.indexOf("--");
3180
+ if (doubleDashIndex === -1 || doubleDashIndex === args.length - 1) {
3181
+ console.error(`${c.red}Usage: kit run -- <command> [args...]${c.reset}`);
3182
+ console.error();
3183
+ console.error(`${c.dim}Options:${c.reset}`);
3184
+ console.error(`${c.dim} --env <name> Environment to use (dev, staging, production, etc.)${c.reset}`);
3185
+ console.error();
3186
+ console.error(`${c.dim}Example:${c.reset}`);
3187
+ console.error(`${c.dim} kit run -- pnpm test${c.reset}`);
3188
+ console.error(`${c.dim} kit run --env staging -- node scripts/migrate.js${c.reset}`);
3189
+ return false;
3190
+ }
3191
+ // Extract environment name if provided (reserved for future use)
3192
+ const envIndex = args.indexOf("--env");
3193
+ if (envIndex !== -1 && envIndex < doubleDashIndex) {
3194
+ // Environment flag parsed but not currently used in env var logic
3195
+ // Can be extended in future to select environment-specific config
3196
+ }
3197
+ // Extract command and args after --
3198
+ const commandArgs = args.slice(doubleDashIndex + 1);
3199
+ const result = await executeCommand({
3200
+ commandArgs,
3201
+ cwd: process.cwd(),
3202
+ inheritEnv: true,
3203
+ });
3204
+ process.exitCode = result.exitCode;
3205
+ return result.exitCode === 0;
3206
+ }
3207
+ async function cmdOpen() {
3208
+ const args = process.argv.slice(2);
3209
+ const serviceName = args[1];
3210
+ // Load env from .env.local for dashboard URL resolution
3211
+ let env = {};
3212
+ try {
3213
+ const envPath = resolve(process.cwd(), ".env.local");
3214
+ const { readFileSync } = await import("node:fs");
3215
+ const envContent = readFileSync(envPath, "utf-8");
3216
+ const lines = envContent.split("\n");
3217
+ for (const line of lines) {
3218
+ const trimmed = line.trim();
3219
+ if (!trimmed || trimmed.startsWith("#"))
3220
+ continue;
3221
+ const eqIndex = trimmed.indexOf("=");
3222
+ if (eqIndex === -1)
3223
+ continue;
3224
+ const key = trimmed.substring(0, eqIndex);
3225
+ const value = trimmed.substring(eqIndex + 1);
3226
+ env[key] = value;
3227
+ }
3228
+ }
3229
+ catch {
3230
+ // .env.local not found — continue with empty env
3231
+ }
3232
+ if (!serviceName || serviceName.startsWith("-")) {
3233
+ // No service specified — show menu
3234
+ console.log(`${c.bold}kit open${c.reset} — open service dashboards\n`);
3235
+ console.log(`${c.bold}Usage:${c.reset}`);
3236
+ console.log(` kit open <service>${c.dim} — open service dashboard${c.reset}`);
3237
+ console.log();
3238
+ const services = listServices();
3239
+ const maxLen = Math.max(...services.map((s) => s.name.length));
3240
+ console.log(`${c.bold}Available services:${c.reset}`);
3241
+ for (const service of services) {
3242
+ const pad = " ".repeat(maxLen - service.name.length + 2);
3243
+ console.log(` ${c.green}${service.name}${c.reset}${pad}${c.dim}${service.label}${c.reset}`);
3244
+ }
3245
+ console.log();
3246
+ console.log(`${c.dim}Example: kit open stripe${c.reset}`);
3247
+ return true;
3248
+ }
3249
+ const result = await openService(serviceName, env);
3250
+ if (result.success) {
3251
+ console.log(`${c.green}✓${c.reset} ${result.message}`);
3252
+ return true;
3253
+ }
3254
+ else {
3255
+ console.error(`${c.red}✗${c.reset} ${result.message}`);
3256
+ return false;
3257
+ }
3258
+ }
3259
+ async function cmdContext() {
3260
+ const jsonMode = hasFlag(process.argv, "--json");
3261
+ try {
3262
+ const config = await loadConfig(resolveConfigPath());
3263
+ const context = await gatherProjectContext(config, process.cwd());
3264
+ if (jsonMode) {
3265
+ console.log(JSON.stringify(context, null, 2));
3266
+ }
3267
+ else {
3268
+ // Human-readable format
3269
+ console.log(`${c.bold}Project Context${c.reset}\n`);
3270
+ console.log(`${c.bold}Project: ${c.reset}${context.projectName}`);
3271
+ console.log(`${c.bold}kit Version: ${c.reset}${context.kitVersion}`);
3272
+ console.log(`${c.bold}Active Environment: ${c.reset}${context.activeEnvironment}`);
3273
+ console.log();
3274
+ if (context.detectedStack && Object.keys(context.detectedStack).length > 0) {
3275
+ console.log(`${c.bold}Detected Stack:${c.reset}`);
3276
+ for (const [key, value] of Object.entries(context.detectedStack)) {
3277
+ console.log(` ${key}: ${value}`);
3278
+ }
3279
+ console.log();
3280
+ }
3281
+ if (context.tools.length > 0) {
3282
+ console.log(`${c.bold}Tools (${context.tools.filter((t) => t.ok).length}/${context.tools.length}):${c.reset}`);
3283
+ for (const tool of context.tools) {
3284
+ const status = tool.ok ? `${c.green}✓${c.reset}` : `${c.red}✗${c.reset}`;
3285
+ const version = tool.installed ? ` ${c.dim}(${tool.installed})${c.reset}` : " not installed";
3286
+ console.log(` ${status} ${tool.name}${version}`);
3287
+ }
3288
+ console.log();
3289
+ }
3290
+ if (context.services.length > 0) {
3291
+ console.log(`${c.bold}Services (${context.services.filter((s) => s.authenticated).length}/${context.services.length}):${c.reset}`);
3292
+ for (const service of context.services) {
3293
+ const status = service.authenticated ? `${c.green}✓${c.reset}` : `${c.red}✗${c.reset}`;
3294
+ console.log(` ${status} ${service.name}`);
3295
+ }
3296
+ console.log();
3297
+ }
3298
+ if (context.secrets.keys.length > 0) {
3299
+ console.log(`${c.bold}Secrets (${context.secrets.keys.filter((s) => s.available).length}/${context.secrets.keys.length}):${c.reset}`);
3300
+ for (const secret of context.secrets.keys) {
3301
+ const status = secret.available ? `${c.green}✓${c.reset}` : `${c.red}✗${c.reset}`;
3302
+ console.log(` ${status} ${secret.name}`);
3303
+ }
3304
+ console.log();
3305
+ }
3306
+ if (context.locks.length > 0) {
3307
+ const allInSync = context.locks.every((l) => l.inSync);
3308
+ console.log(`${c.bold}Lock Files${allInSync ? ` ${c.green}(in sync)` : ` ${c.yellow}(drift detected)`}:${c.reset}`);
3309
+ for (const lock of context.locks) {
3310
+ const status = lock.inSync ? `${c.green}✓${c.reset}` : `${c.yellow}⚠${c.reset}`;
3311
+ console.log(` ${status} ${lock.category}: ${lock.detail}`);
3312
+ }
3313
+ }
3314
+ }
3315
+ return true;
3316
+ }
3317
+ catch (err) {
3318
+ console.error(`${c.red}Error: ${err.message}${c.reset}`);
3319
+ return false;
3320
+ }
3321
+ }
3322
+ async function cmdWhoami() {
3323
+ const jsonMode = hasFlag(process.argv, "--json");
3324
+ let config = {};
3325
+ try {
3326
+ config = await loadConfig(resolveConfigPath());
3327
+ }
3328
+ catch {
3329
+ // Works without .kit.toml
3330
+ }
3331
+ const { detectEnvironment } = await import("./environment.js");
3332
+ const envInfo = detectEnvironment(config.governance);
3333
+ const agent = config.governance?.agent;
3334
+ const budgetEnabled = config.governance?.enabled && (agent?.max_tokens_per_day || agent?.max_operations_per_hour);
3335
+ let budget = null;
3336
+ if (budgetEnabled) {
3337
+ budget = await getBudgetStatus(config.governance);
3338
+ }
3339
+ if (jsonMode) {
3340
+ console.log(JSON.stringify({
3341
+ agent: agent
3342
+ ? { id: agent.id, name: agent.name }
3343
+ : null,
3344
+ environment: envInfo.environment,
3345
+ environment_source: envInfo.source,
3346
+ budget: budget
3347
+ ? {
3348
+ tokens_used: budget.tokens_used,
3349
+ tokens_limit: budget.tokens_limit,
3350
+ operations_used: budget.operations_used,
3351
+ operations_limit: budget.operations_limit,
3352
+ }
3353
+ : null,
3354
+ }, null, 2));
3355
+ return true;
3356
+ }
3357
+ console.log(`${c.bold}${c.cyan}kit whoami${c.reset}`);
3358
+ console.log(`${c.dim}${"─".repeat(50)}${c.reset}\n`);
3359
+ if (agent?.id || agent?.name) {
3360
+ if (agent.name)
3361
+ console.log(` ${c.bold}Agent:${c.reset} ${agent.name}`);
3362
+ if (agent.id)
3363
+ console.log(` ${c.bold}ID:${c.reset} ${c.dim}${agent.id}${c.reset}`);
3364
+ }
3365
+ else {
3366
+ console.log(` ${c.dim}No agent configured in [governance.agent]${c.reset}`);
3367
+ }
3368
+ const envColor = envInfo.environment === "prod"
3369
+ ? c.red
3370
+ : envInfo.environment === "staging"
3371
+ ? c.yellow
3372
+ : c.green;
3373
+ console.log(` ${c.bold}Env:${c.reset} ${envColor}${envInfo.environment}${c.reset} ${c.dim}(via ${envInfo.source})${c.reset}`);
3374
+ if (budget) {
3375
+ console.log();
3376
+ const tokensLine = budget.tokens_limit
3377
+ ? `${budget.tokens_used.toLocaleString()} / ${budget.tokens_limit.toLocaleString()} tokens today`
3378
+ : `${budget.tokens_used.toLocaleString()} tokens today`;
3379
+ const opsLine = budget.operations_limit
3380
+ ? `${budget.operations_used} / ${budget.operations_limit} operations this hour`
3381
+ : `${budget.operations_used} operations this hour`;
3382
+ console.log(` ${c.bold}Budget:${c.reset} ${c.dim}${tokensLine}${c.reset}`);
3383
+ console.log(` ${c.dim}${opsLine}${c.reset}`);
3384
+ }
3385
+ console.log();
3386
+ return true;
3387
+ }
3388
+ function cmdVersion() {
3389
+ console.log(KIT_VERSION);
3390
+ return true;
3391
+ }
3392
+ const COMMAND_HELP = {
3393
+ check: "Check status of all tools, services, secrets, and lock files",
3394
+ init: "Detect stack, generate .kit.toml, and run full setup",
3395
+ upgrade: "Update lock files from .kit.toml",
3396
+ install: "Install missing tools via mise",
3397
+ login: "Guided login to all configured services",
3398
+ secrets: "Generate .env.local from template + secret store",
3399
+ "secrets sync": "Push resolved secrets to GitHub Actions / .env.ci / stdout",
3400
+ "secrets migrate": "Migrate plaintext secrets in .env* → configured vault",
3401
+ "secrets rotate": "Rotate a key: write new value to vault (explicit / random)",
3402
+ "secrets onecli": "Register a key with OneCLI gateway so agent never sees the real value",
3403
+ "secrets purge-history": "Destructive: rewrite git history to remove a leaked value (--force-history)",
3404
+ "secrets propagate": "Push a value to deploy targets only (skips vault-write). --stdin safer than --value",
3405
+ "secrets revoke-old": "Revoke a previously-minted scoped key (Supabase Mgmt API)",
3406
+ "env switch": "Switch active environment (dev/staging/prod). Gates prod-key reads.",
3407
+ "env current": "Show active environment marker",
3408
+ analyze: "Analyze repo + emit draft CLAUDE.md / RULES.md",
3409
+ "agent-config": "Inject a managed 'use kit' block into CLAUDE.md / AGENTS.md / .cursorrules / .clinerules",
3410
+ "security policy": "Dependency allowlist enforcement (init|add|check)",
3411
+ "security clear-cache": "Clear cached scanner binary (after intentional rebuild)",
3412
+ "security scan-staged": "Pre-commit: scan staged files for credential patterns",
3413
+ "security scan-build": "Scan build artifacts (.next/, dist/) for inlined secrets",
3414
+ "security scan-transcripts": "Scan agent transcripts + prompt caches for leaked credentials",
3415
+ "security costs": "Snapshot per-key spend vs policy cap (Stripe live; others stubbed)",
3416
+ "security check-gitignore": "Verify .gitignore covers sensitive paths (--fix to auto-patch)",
3417
+ "security verify-pull": "After git pull: audit new deps, gitignore drops, introduced secrets",
3418
+ "security prescan": "Multi-repo baseline sweep (secrets, gitignore, branch-protect; --deep adds CVE/workflow-drift/bumblebee; --format=json + --vs-baseline=<path> for CI drift)",
3419
+ "security prescan-diff": "Diff two prescan reports — surface new regressions + fixed findings since baseline",
3420
+ "audit secrets": "Forensics: who/what touched each key + when (reads audit log)",
3421
+ "auth elevate": "Mint elevation marker for destructive secret ops (TOTP/yes-prompt)",
3422
+ "auth status": "Show active elevation",
3423
+ "auth revoke": "Drop the elevation marker",
3424
+ "auth setup-totp": "Enroll TOTP secret (writes ~/.kit/totp-secret 0600)",
3425
+ "hooks add": "Install a built-in hook (e.g. secret-scan)",
3426
+ setup: "Full pipeline: install → login → secrets → project setup",
3427
+ fix: "Auto-fix what is possible",
3428
+ escalate: "List what needs human action",
3429
+ governance: "View governance status and agent access controls",
3430
+ skills: "Check status of agent skills",
3431
+ hooks: "Manage git hooks",
3432
+ add: "Provision a service (kit add --list to see all adapters)",
3433
+ audit: "View audit log of kit operations",
3434
+ doctor: "Deep diagnostics — checks environment health in detail",
3435
+ env: "Show current environment info",
3436
+ ci: "CI-native check: GitHub Actions annotations, GitLab JUnit, JSON",
3437
+ clone: "Clone a Git repository and run kit setup",
3438
+ run: "Execute a command with project env vars loaded",
3439
+ open: "Open service dashboard in browser (stripe, vercel, railway, etc.)",
3440
+ context: "Show project context: tools, services, secrets, environment",
3441
+ mcp: "MCP server over stdio (Claude Code/Cursor/Codex); 'kit mcp list|auth|set-token|clear' manages declared servers",
3442
+ whoami: "Show current agent / user identity",
3443
+ version: "Print kit version",
3444
+ "create-plugin": "Scaffold a new kit plugin package",
3445
+ plugin: "Discover and manage kit plugins (search, list, scaffold, install)",
3446
+ triage: "Security evaluation before installing packages, images, or skills",
3447
+ pkg: "Install package with mandatory triage (kit pkg npm:express)",
3448
+ team: "Manage team members, roles, and permissions (RBAC, invitations, audit logs)",
3449
+ completions: "Output shell completion script (bash, zsh, fish)",
3450
+ help: "Show this help",
3451
+ };
3452
+ async function cmdPkg() {
3453
+ const args = process.argv.slice(3);
3454
+ const input = args[0];
3455
+ if (!input || input === "--help" || input === "-h") {
3456
+ console.log(`${c.bold}kit pkg${c.reset} — Install packages with mandatory triage\n`);
3457
+ console.log("Usage: kit pkg <ecosystem>:<package>[@version]\n");
3458
+ console.log("Ecosystems:");
3459
+ console.log(" npm:express npm install express");
3460
+ console.log(" npm-g:vercel npm install -g vercel");
3461
+ console.log(" pnpm:react pnpm add react");
3462
+ console.log(" pip:requests pip install requests");
3463
+ console.log(" brew:trivy brew install trivy");
3464
+ console.log(" docker:postgres docker pull postgres");
3465
+ console.log(" go:golang.org/x/tools/cmd/goimports@latest");
3466
+ console.log(" cargo:ripgrep cargo install ripgrep");
3467
+ console.log("");
3468
+ console.log("Examples:");
3469
+ console.log(" kit pkg npm:express@4.18.0");
3470
+ console.log(" kit pkg pip:requests");
3471
+ console.log(" kit pkg docker:stalwartlabs/stalwart");
3472
+ console.log(" kit pkg brew:semgrep");
3473
+ return true;
3474
+ }
3475
+ const spec = parsePkgSpec(input);
3476
+ if (!spec) {
3477
+ console.error(`${c.red}Invalid format: ${input}${c.reset}`);
3478
+ console.error("Expected: <ecosystem>:<package> (e.g. npm:express, pip:requests)");
3479
+ return false;
3480
+ }
3481
+ console.log(`${c.bold}Installing ${spec.ecosystem}:${spec.name}${spec.version ? `@${spec.version}` : ""}${c.reset}\n`);
3482
+ console.log(`${c.cyan}Step 1: Triage...${c.reset}`);
3483
+ const result = await installPkg(spec);
3484
+ console.log(result.output);
3485
+ return result.installed;
3486
+ }
3487
+ async function cmdTriage() {
3488
+ const args = process.argv.slice(3);
3489
+ const sandbox = hasFlag(args, "--sandbox");
3490
+ const filtered = args.filter((a) => a !== "--sandbox");
3491
+ const typeArg = filtered[0];
3492
+ // `check-deps` is a pre-commit subcommand handled below before we narrow
3493
+ // to TriageType; the rest of this function works against TriageType only.
3494
+ if (typeArg === "check-deps") {
3495
+ return cmdTriageCheckDeps();
3496
+ }
3497
+ const type = typeArg;
3498
+ const target = filtered[1];
3499
+ if (!typeArg || typeArg === "--help" || typeArg === "-h") {
3500
+ console.log(`${c.bold}kit triage${c.reset} — Security evaluation before installing packages\n`);
3501
+ console.log("Usage:");
3502
+ console.log(" kit triage docker <image> Evaluate Docker image (CVE + sandbox)");
3503
+ console.log(" kit triage npm <package> Evaluate npm package (registry + GitHub)");
3504
+ console.log(" kit triage npm <package> --sandbox + offline tarball inspection (install-script + path-traversal scan, no code executed)");
3505
+ console.log(" kit triage pip <package> Evaluate PyPI package");
3506
+ console.log(" kit triage repo <github-url> Evaluate GitHub repository");
3507
+ console.log(" kit triage skill <path|name> Evaluate Claude Code / agent skill");
3508
+ console.log(" kit triage all <target> Auto-detect and run all checks");
3509
+ console.log(" kit triage tools Show installed security tools");
3510
+ console.log(" kit triage check-deps Pre-commit gate: fail if staged deps lack triage entries");
3511
+ console.log("");
3512
+ console.log("Examples:");
3513
+ console.log(" kit triage npm express");
3514
+ console.log(" kit triage npm node-ipc --sandbox");
3515
+ console.log(" kit triage docker stalwartlabs/stalwart");
3516
+ console.log(" kit triage skill searxng");
3517
+ return true;
3518
+ }
3519
+ const validTypes = ["docker", "npm", "pip", "repo", "skill", "all", "tools"];
3520
+ if (!validTypes.includes(type)) {
3521
+ console.error(`${c.red}Unknown triage type: ${type}${c.reset}`);
3522
+ console.error(`Valid types: ${validTypes.join(", ")}`);
3523
+ return false;
3524
+ }
3525
+ if (type === "tools") {
3526
+ const result = await listTriageTools();
3527
+ console.log(result.output);
3528
+ return true;
3529
+ }
3530
+ if (!target) {
3531
+ console.error(`${c.red}Target required for triage ${type}${c.reset}`);
3532
+ return false;
3533
+ }
3534
+ console.log(`${c.bold}Running triage on ${type}: ${target}${c.reset}\n`);
3535
+ const result = await runTriage(type, target);
3536
+ console.log(result.output);
3537
+ // Sandbox is currently only meaningful for npm packages — extends the
3538
+ // registry-metadata check with offline tarball inspection.
3539
+ let sandboxClean = true;
3540
+ if (sandbox && type === "npm") {
3541
+ const { triageNpmSandbox } = await import("./triage-sandbox.js");
3542
+ console.log(`\n${c.bold}Sandbox inspection (offline):${c.reset}`);
3543
+ try {
3544
+ const sb = await triageNpmSandbox(target);
3545
+ console.log(` package: ${sb.package}${sb.version ? `@${sb.version}` : ""}`);
3546
+ console.log(` tarball: ${(sb.tarballSize / 1024).toFixed(1)} kB`);
3547
+ console.log(` install scripts: ${sb.hasInstallScripts ? "YES (review below)" : "none"}`);
3548
+ if (sb.findings.length === 0) {
3549
+ console.log(` ${c.green}✓ no risk signals${c.reset}`);
3550
+ }
3551
+ else {
3552
+ const critical = sb.findings.filter((f) => f.severity === "critical");
3553
+ if (critical.length > 0)
3554
+ sandboxClean = false;
3555
+ for (const f of sb.findings) {
3556
+ const tag = f.severity === "critical"
3557
+ ? `${c.red}[CRIT]${c.reset}`
3558
+ : f.severity === "warn"
3559
+ ? `${c.yellow}[WARN]${c.reset}`
3560
+ : `[info]`;
3561
+ console.log(` ${tag} ${f.signal} — ${f.detail}`);
3562
+ }
3563
+ }
3564
+ }
3565
+ catch (err) {
3566
+ console.error(` ${c.red}sandbox failed: ${err instanceof Error ? err.message : String(err)}${c.reset}`);
3567
+ sandboxClean = false;
3568
+ }
3569
+ }
3570
+ else if (sandbox && type !== "npm") {
3571
+ console.log(`\n${c.dim}(--sandbox currently only applies to npm; ignored for ${type})${c.reset}`);
3572
+ }
3573
+ const overall = result.passed && sandboxClean;
3574
+ if (!overall) {
3575
+ console.log(`\n${c.yellow}⚠️ Review warnings above before proceeding.${c.reset}`);
3576
+ }
3577
+ // Record the triage outcome so the pre-commit triage-check can verify
3578
+ // that every newly added dep was evaluated. Failed runs aren't logged —
3579
+ // a failed triage means the user explicitly chose not to install, so it
3580
+ // shouldn't unblock the commit.
3581
+ if (overall && target) {
3582
+ await recordTriageRun(type, target, sandbox);
3583
+ }
3584
+ return overall;
3585
+ }
3586
+ const TRIAGE_LOG_FILE = ".kit-triage.jsonl";
3587
+ const TRIAGE_MAX_AGE_DAYS = 7;
3588
+ async function recordTriageRun(type, target, sandbox) {
3589
+ const { appendFile } = await import("node:fs/promises");
3590
+ const { resolve } = await import("node:path");
3591
+ const entry = {
3592
+ timestamp: new Date().toISOString(),
3593
+ type,
3594
+ target,
3595
+ sandbox,
3596
+ granter: process.env.USER ?? "unknown",
3597
+ };
3598
+ try {
3599
+ await appendFile(resolve(process.cwd(), TRIAGE_LOG_FILE), JSON.stringify(entry) + "\n", "utf-8");
3600
+ }
3601
+ catch (err) {
3602
+ console.error(`${c.dim}(triage-log append failed: ${err instanceof Error ? err.message : err})${c.reset}`);
3603
+ }
3604
+ }
3605
+ /**
3606
+ * `kit triage check-deps` — pre-commit step. Diffs staged dep manifests
3607
+ * (package.json, requirements.txt, pyproject.toml) against the cached
3608
+ * versions in HEAD; any newly-added top-level package must have a triage
3609
+ * entry within the last `TRIAGE_MAX_AGE_DAYS` days. Exits non-zero on
3610
+ * missing entries so the commit fails.
3611
+ *
3612
+ * Intentionally does NOT walk transitive deps — that's the lockfile's
3613
+ * job. The pre-commit gate fires on what the developer is signing off on,
3614
+ * which is the manifest entries they wrote.
3615
+ */
3616
+ async function cmdTriageCheckDeps() {
3617
+ const { execFileSync } = await import("node:child_process");
3618
+ const { readFile } = await import("node:fs/promises");
3619
+ const { resolve } = await import("node:path");
3620
+ const gitShow = (ref) => {
3621
+ try {
3622
+ return execFileSync("git", ["show", ref], {
3623
+ encoding: "utf-8",
3624
+ stdio: ["pipe", "pipe", "ignore"],
3625
+ });
3626
+ }
3627
+ catch {
3628
+ return null;
3629
+ }
3630
+ };
3631
+ const newDeps = [];
3632
+ // package.json — compare staged vs HEAD top-level dependencies.
3633
+ const stagedPkg = gitShow(":package.json");
3634
+ if (stagedPkg) {
3635
+ try {
3636
+ const headPkgRaw = gitShow("HEAD:package.json");
3637
+ const staged = JSON.parse(stagedPkg);
3638
+ const head = headPkgRaw
3639
+ ? JSON.parse(headPkgRaw)
3640
+ : {};
3641
+ const stagedDeps = { ...staged.dependencies, ...staged.devDependencies };
3642
+ const headDeps = { ...head.dependencies, ...head.devDependencies };
3643
+ for (const name of Object.keys(stagedDeps)) {
3644
+ if (!(name in headDeps))
3645
+ newDeps.push({ ecosystem: "npm", name });
3646
+ }
3647
+ }
3648
+ catch { /* malformed package.json — skip */ }
3649
+ }
3650
+ // requirements.txt — line-oriented diff.
3651
+ const stagedReq = gitShow(":requirements.txt");
3652
+ if (stagedReq) {
3653
+ const headReq = gitShow("HEAD:requirements.txt") ?? "";
3654
+ const parseLines = (text) => new Set(text
3655
+ .split("\n")
3656
+ .map((l) => l.trim())
3657
+ .filter((l) => l && !l.startsWith("#"))
3658
+ .map((l) => l.split(/[<>=!~]/)[0].trim()));
3659
+ const stagedSet = parseLines(stagedReq);
3660
+ const headSet = parseLines(headReq);
3661
+ for (const name of stagedSet) {
3662
+ if (!headSet.has(name))
3663
+ newDeps.push({ ecosystem: "pip", name });
3664
+ }
3665
+ }
3666
+ if (newDeps.length === 0)
3667
+ return true;
3668
+ // Build a triage-log index of `<ecosystem>:<name>` → most-recent ISO ts.
3669
+ const latest = {};
3670
+ try {
3671
+ const text = await readFile(resolve(process.cwd(), TRIAGE_LOG_FILE), "utf-8");
3672
+ for (const line of text.split("\n")) {
3673
+ if (!line.trim())
3674
+ continue;
3675
+ try {
3676
+ const entry = JSON.parse(line);
3677
+ const key = `${entry.type}:${entry.target}`;
3678
+ const prev = latest[key];
3679
+ if (!prev || entry.timestamp > prev)
3680
+ latest[key] = entry.timestamp;
3681
+ }
3682
+ catch { /* skip malformed line */ }
3683
+ }
3684
+ }
3685
+ catch {
3686
+ /* no triage log yet */
3687
+ }
3688
+ const cutoff = Date.now() - TRIAGE_MAX_AGE_DAYS * 24 * 3600 * 1000;
3689
+ const missing = [];
3690
+ for (const dep of newDeps) {
3691
+ const key = `${dep.ecosystem}:${dep.name}`;
3692
+ const ts = latest[key];
3693
+ if (!ts || Date.parse(ts) < cutoff) {
3694
+ missing.push(` - ${dep.ecosystem}:${dep.name} (run: kit triage ${dep.ecosystem} ${dep.name})`);
3695
+ }
3696
+ }
3697
+ if (missing.length === 0)
3698
+ return true;
3699
+ console.error(`${c.red}✗ Triage missing for ${missing.length} newly-added dep(s):${c.reset}`);
3700
+ for (const line of missing)
3701
+ console.error(line);
3702
+ console.error("");
3703
+ console.error(`${c.dim}Triage policy lives in CLAUDE.md. Run the suggested triage commands above,`);
3704
+ console.error(`then re-stage and commit. Bypass with --no-verify is recorded to ${SKIPPED_COMMITS_LOG}.${c.reset}`);
3705
+ return false;
3706
+ }
3707
+ function cmdHelp(subcommand) {
3708
+ if (subcommand && COMMAND_HELP[subcommand]) {
3709
+ console.log(`kit ${subcommand} — ${COMMAND_HELP[subcommand]}`);
3710
+ return true;
3711
+ }
3712
+ const bold = c.bold, cyan = c.cyan, dim = c.dim, reset = c.reset, green = c.green;
3713
+ console.log(`${bold}kit${reset} ${dim}v${KIT_VERSION}${reset} — developer environment manager\n`);
3714
+ console.log(`${bold}Usage:${reset} kit ${cyan}<command>${reset} ${dim}[options]${reset}\n`);
3715
+ console.log(`${bold}Commands:${reset}`);
3716
+ const maxLen = Math.max(...Object.keys(COMMAND_HELP).map((k) => k.length));
3717
+ for (const [cmd, desc] of Object.entries(COMMAND_HELP)) {
3718
+ const pad = " ".repeat(maxLen - cmd.length + 2);
3719
+ console.log(` ${green}${cmd}${reset}${pad}${dim}${desc}${reset}`);
3720
+ }
3721
+ console.log(`\n${bold}Options:${reset}`);
3722
+ console.log(` ${green}--non-interactive${reset} ${dim}No prompts (for CI / agent use)${reset}`);
3723
+ console.log(` ${green}--json${reset} ${dim}Machine-readable output${reset}`);
3724
+ console.log(` ${green}--env=<name>${reset} ${dim}Environment override (dev/staging/production)${reset}`);
3725
+ console.log(` ${green}--dry-run${reset} ${dim}Preview without writing (where supported)${reset}`);
3726
+ console.log(` ${green}--help, -h${reset} ${dim}Show this help${reset}`);
3727
+ console.log(` ${green}--version, -v${reset} ${dim}Print version${reset}`);
3728
+ console.log(`\n${bold}Examples:${reset}`);
3729
+ console.log(` ${dim}kit init${reset} # Set up a new project`);
3730
+ console.log(` ${dim}kit check --json${reset} # Machine-readable status`);
3731
+ console.log(` ${dim}kit add neon/db${reset} # Provision Neon Postgres`);
3732
+ console.log(` ${dim}kit secrets sync --target=github${reset} # Push secrets to GitHub Actions`);
3733
+ console.log(` ${dim}kit ci --format=github${reset} # CI check with PR annotations`);
3734
+ console.log(` ${dim}kit add --list${reset} # List all available adapters`);
3735
+ return true;
3736
+ }
3737
+ async function cmdTeam() {
3738
+ const subcommand = process.argv[3];
3739
+ const args = process.argv.slice(4);
3740
+ if (!subcommand || subcommand === "--help" || subcommand === "-h") {
3741
+ console.log(`${c.bold}kit team${c.reset} — manage team members and permissions\n`);
3742
+ console.log(`${c.bold}Usage:${c.reset}`);
3743
+ console.log(` kit team create <name> Create new team`);
3744
+ console.log(` kit team invite <email> --role=<role> Invite user to team`);
3745
+ console.log(` kit team members list List team members`);
3746
+ console.log(` kit team member remove <email> Remove team member`);
3747
+ console.log(` kit team audit log [--limit=N] View audit logs\n`);
3748
+ console.log(`${c.dim}Roles: owner, admin, developer, guest${c.reset}`);
3749
+ return true;
3750
+ }
3751
+ try {
3752
+ switch (subcommand) {
3753
+ case "create": {
3754
+ const name = args[0];
3755
+ if (!name) {
3756
+ console.error(`${c.red}Error: team name required${c.reset}`);
3757
+ return false;
3758
+ }
3759
+ // Placeholder: would need user context from auth
3760
+ console.log(`${c.yellow}Note: Team creation requires authentication${c.reset}`);
3761
+ console.log(`${c.dim}Team management requires backend service integration${c.reset}`);
3762
+ return false;
3763
+ }
3764
+ case "invite": {
3765
+ const email = args[0];
3766
+ const roleArg = args.find((a) => a.startsWith("--role="));
3767
+ const role = roleArg?.split("=")[1] || "developer";
3768
+ if (!email) {
3769
+ console.error(`${c.red}Error: email required${c.reset}`);
3770
+ return false;
3771
+ }
3772
+ console.log(`${c.yellow}Invitation sent to ${email} as ${role}${c.reset}`);
3773
+ console.log(`${c.dim}Check your email for the invitation link${c.reset}`);
3774
+ return true;
3775
+ }
3776
+ case "members": {
3777
+ if (args[0] !== "list") {
3778
+ console.error(`${c.red}Unknown subcommand: ${args[0]}${c.reset}`);
3779
+ return false;
3780
+ }
3781
+ console.log(`${c.bold}Team Members${c.reset}\n`);
3782
+ console.log(`${c.dim}(Requires team context — run from project with .kit.toml)${c.reset}`);
3783
+ return false;
3784
+ }
3785
+ case "member": {
3786
+ if (args[0] !== "remove") {
3787
+ console.error(`${c.red}Unknown subcommand: ${args[0]}${c.reset}`);
3788
+ return false;
3789
+ }
3790
+ const email = args[1];
3791
+ if (!email) {
3792
+ console.error(`${c.red}Error: email required${c.reset}`);
3793
+ return false;
3794
+ }
3795
+ console.log(`${c.yellow}Member ${email} removed from team${c.reset}`);
3796
+ return true;
3797
+ }
3798
+ case "audit": {
3799
+ if (args[0] !== "log") {
3800
+ console.error(`${c.red}Unknown subcommand: ${args[0]}${c.reset}`);
3801
+ return false;
3802
+ }
3803
+ const limitArg = args.find((a) => a.startsWith("--limit="));
3804
+ const limit = limitArg ? parseInt(limitArg.split("=")[1]) : 50;
3805
+ console.log(`${c.bold}Audit Log${c.reset} ${c.dim}(limit: ${limit})${c.reset}\n`);
3806
+ console.log(`${c.dim}No audit events available${c.reset}`);
3807
+ return true;
3808
+ }
3809
+ default:
3810
+ console.error(`${c.red}Unknown subcommand: ${subcommand}${c.reset}`);
3811
+ console.error(`Run 'kit team --help' for usage information.`);
3812
+ return false;
3813
+ }
3814
+ }
3815
+ catch (err) {
3816
+ const error = err;
3817
+ console.error(`${c.red}Error: ${error.message}${c.reset}`);
3818
+ return false;
3819
+ }
3820
+ }
3821
+ /**
3822
+ * Surfaces commits that bypassed the pre-commit hook (`git commit
3823
+ * --no-verify`). The post-commit detector installed by `kit hooks
3824
+ * install` writes one JSONL line per skip to `.kit-skipped-commits.jsonl`;
3825
+ * we read the last few lines and print a red banner on stderr so the next
3826
+ * `kit` invocation makes the bypass visible. Suppressed when
3827
+ * `KIT_HIDE_HOOK_SKIP_BANNER=1` (useful for ephemeral CI).
3828
+ */
3829
+ async function showSkippedCommitBanner() {
3830
+ if (process.env.KIT_HIDE_HOOK_SKIP_BANNER === "1")
3831
+ return;
3832
+ try {
3833
+ const { readFile, stat } = await import("node:fs/promises");
3834
+ const { resolve } = await import("node:path");
3835
+ const logPath = resolve(process.cwd(), SKIPPED_COMMITS_LOG);
3836
+ const info = await stat(logPath).catch(() => null);
3837
+ if (!info)
3838
+ return;
3839
+ const content = await readFile(logPath, "utf-8");
3840
+ const lines = content.trim().split("\n").filter(Boolean);
3841
+ if (lines.length === 0)
3842
+ return;
3843
+ const recent = lines.slice(-3);
3844
+ process.stderr.write(`${c.red}[kit] ${lines.length} commit(s) bypassed pre-commit hook — most recent:${c.reset}\n`);
3845
+ for (const line of recent) {
3846
+ try {
3847
+ const entry = JSON.parse(line);
3848
+ process.stderr.write(` ${entry.timestamp} ${entry.sha.slice(0, 8)} (${entry.reason})\n`);
3849
+ }
3850
+ catch {
3851
+ /* malformed line — ignore */
3852
+ }
3853
+ }
3854
+ process.stderr.write(` Review: cat ${SKIPPED_COMMITS_LOG} | jq .\n` +
3855
+ ` Suppress: KIT_HIDE_HOOK_SKIP_BANNER=1\n`);
3856
+ }
3857
+ catch {
3858
+ /* banner is best-effort — never break the CLI */
3859
+ }
3860
+ }
3861
+ async function main() {
3862
+ const args = process.argv.slice(2);
3863
+ const command = args[0];
3864
+ const nonInteractive = hasFlag(args, "--non-interactive") ||
3865
+ process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true";
3866
+ // Activate read-only mode early so subsequent code paths see the env var.
3867
+ // Honored gates: writeSecretToBackend, grantElevation, installHooks, and
3868
+ // every kit-plugin write surface (vercel env-set, github secret-put,
3869
+ // stripe webhook-create, etc).
3870
+ if (hasFlag(args, "--read-only") || hasFlag(args, "--readonly")) {
3871
+ const { activateReadOnlyMode } = await import("./read-only-mode.js");
3872
+ activateReadOnlyMode("flag");
3873
+ }
3874
+ else if (process.env.KIT_READ_ONLY === "1") {
3875
+ const { activateReadOnlyMode } = await import("./read-only-mode.js");
3876
+ activateReadOnlyMode("env");
3877
+ }
3878
+ // Compute + export KIT_POLICY_HASH so classifiers / agents reading
3879
+ // the env see the same policy identity. Also honors
3880
+ // `[policy].default_mode = "read-only"` as a third source for the
3881
+ // read-only gate above (after flag + env-var).
3882
+ try {
3883
+ const cfgForPolicy = await loadConfig(resolveConfigPath()).catch(() => null);
3884
+ if (cfgForPolicy?.policy) {
3885
+ const { installPolicyHash } = await import("./policy.js");
3886
+ installPolicyHash(cfgForPolicy.policy);
3887
+ if (cfgForPolicy.policy.default_mode === "read-only" && !process.env.KIT_READ_ONLY) {
3888
+ const { activateReadOnlyMode } = await import("./read-only-mode.js");
3889
+ activateReadOnlyMode("policy");
3890
+ }
3891
+ }
3892
+ }
3893
+ catch {
3894
+ /* config not loadable at boot — non-fatal; subcommands re-load as needed */
3895
+ }
3896
+ await showSkippedCommitBanner();
3897
+ try {
3898
+ let ok;
3899
+ // --version / --help flags before the switch
3900
+ if (args[0] === "--version" || args[0] === "-v") {
3901
+ ok = cmdVersion();
3902
+ process.exitCode = ok ? 0 : 1;
3903
+ return;
3904
+ }
3905
+ if (args[0] === "--help" || args[0] === "-h") {
3906
+ ok = cmdHelp();
3907
+ process.exitCode = 0;
3908
+ return;
3909
+ }
3910
+ switch (command) {
3911
+ case "version":
3912
+ ok = cmdVersion();
3913
+ break;
3914
+ case "help":
3915
+ ok = cmdHelp(args[1]);
3916
+ break;
3917
+ case "completions": {
3918
+ const shell = args[1];
3919
+ const script = generateCompletions(shell);
3920
+ if (!script) {
3921
+ console.error(`Unknown shell: ${shell}. Use: bash, zsh, fish`);
3922
+ process.exitCode = 1;
3923
+ return;
3924
+ }
3925
+ process.stdout.write(script);
3926
+ ok = true;
3927
+ break;
3928
+ }
3929
+ case "whoami":
3930
+ ok = await cmdWhoami();
3931
+ break;
3932
+ case "check":
3933
+ case undefined:
3934
+ ok = await cmdCheck();
3935
+ break;
3936
+ case "init":
3937
+ ok = await cmdInit();
3938
+ break;
3939
+ case "upgrade":
3940
+ ok = await cmdUpgrade();
3941
+ break;
3942
+ case "install":
3943
+ ok = await cmdInstall();
3944
+ break;
3945
+ case "login":
3946
+ ok = await cmdLogin();
3947
+ break;
3948
+ case "secrets":
3949
+ ok = await cmdSecrets();
3950
+ break;
3951
+ case "setup":
3952
+ ok = await cmdSetup();
3953
+ break;
3954
+ case "skills":
3955
+ ok = await cmdSkills();
3956
+ break;
3957
+ case "fix":
3958
+ ok = await cmdFix();
3959
+ break;
3960
+ case "escalate":
3961
+ ok = await cmdEscalate();
3962
+ break;
3963
+ case "governance":
3964
+ ok = await cmdGovernance();
3965
+ break;
3966
+ case "agent-config":
3967
+ ok = await cmdAgentConfig();
3968
+ break;
3969
+ case "hooks":
3970
+ ok = await cmdHooks();
3971
+ break;
3972
+ case "add":
3973
+ ok = await cmdAdd();
3974
+ break;
3975
+ case "audit":
3976
+ ok = await cmdAudit();
3977
+ break;
3978
+ case "auth":
3979
+ ok = await cmdAuth();
3980
+ break;
3981
+ case "mcp":
3982
+ ok = await cmdMcp();
3983
+ break;
3984
+ case "env":
3985
+ ok = await cmdEnv();
3986
+ break;
3987
+ case "doctor":
3988
+ ok = await cmdDoctor();
3989
+ break;
3990
+ case "analyze":
3991
+ ok = await cmdAnalyze();
3992
+ break;
3993
+ case "security":
3994
+ ok = await cmdSecurity();
3995
+ break;
3996
+ case "create-plugin":
3997
+ ok = await cmdCreatePlugin();
3998
+ break;
3999
+ case "plugin":
4000
+ ok = await cmdPlugin();
4001
+ break;
4002
+ case "ci":
4003
+ ok = await cmdCi();
4004
+ break;
4005
+ case "clone":
4006
+ ok = await cmdClone();
4007
+ break;
4008
+ case "run":
4009
+ ok = await cmdRun();
4010
+ break;
4011
+ case "open":
4012
+ ok = await cmdOpen();
4013
+ break;
4014
+ case "context":
4015
+ ok = await cmdContext();
4016
+ break;
4017
+ case "triage":
4018
+ ok = await cmdTriage();
4019
+ break;
4020
+ case "baseline":
4021
+ ok = await cmdBaseline();
4022
+ break;
4023
+ case "design":
4024
+ ok = await cmdDesign();
4025
+ break;
4026
+ case "review":
4027
+ ok = await cmdReview();
4028
+ break;
4029
+ case "pkg":
4030
+ ok = await cmdPkg();
4031
+ break;
4032
+ case "team":
4033
+ ok = await cmdTeam();
4034
+ break;
4035
+ default: {
4036
+ console.error(`Unknown command: ${command}`);
4037
+ const { didYouMean } = await import("./utils/didYouMean.js");
4038
+ const knownCommands = [
4039
+ ...new Set(Object.keys(COMMAND_HELP).map((k) => k.split(" ")[0])),
4040
+ "help", "version", "completions",
4041
+ ];
4042
+ const suggestions = didYouMean(command, knownCommands);
4043
+ if (suggestions.length > 0) {
4044
+ console.error(`Did you mean: ${suggestions.map((s) => `'${s}'`).join(", ")}?`);
4045
+ }
4046
+ console.error(`Run 'kit help' for a list of available commands.`);
4047
+ process.exitCode = 1;
4048
+ return;
4049
+ }
4050
+ }
4051
+ process.exitCode = ok ? 0 : 1;
4052
+ // Non-interactive / CI: skip update check
4053
+ if (!nonInteractive) {
4054
+ checkForUpdate(KIT_VERSION).then((info) => {
4055
+ if (info)
4056
+ printUpdateNotice(info);
4057
+ }).catch(() => { }); // never fail
4058
+ }
4059
+ }
4060
+ catch (err) {
4061
+ if (err instanceof Error &&
4062
+ "code" in err &&
4063
+ err.code === "ENOENT") {
4064
+ console.error(`${c.red}Error: ${KIT_FILE} not found in ${process.cwd()}${c.reset}`);
4065
+ console.error(`${c.dim}Create a .kit.toml to define your project's tools, services, and secrets.${c.reset}`);
4066
+ process.exitCode = 1;
4067
+ }
4068
+ else {
4069
+ throw err;
4070
+ }
4071
+ }
4072
+ }
4073
+ main();
4074
+ //# sourceMappingURL=cli.js.map