@vibecheckai/cli 3.4.0 → 3.5.1

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 (228) hide show
  1. package/bin/registry.js +154 -338
  2. package/bin/runners/context/generators/mcp.js +13 -15
  3. package/bin/runners/context/proof-context.js +1 -248
  4. package/bin/runners/lib/analysis-core.js +180 -198
  5. package/bin/runners/lib/analyzers.js +223 -1669
  6. package/bin/runners/lib/cli-output.js +210 -242
  7. package/bin/runners/lib/detectors-v2.js +785 -547
  8. package/bin/runners/lib/entitlements-v2.js +458 -96
  9. package/bin/runners/lib/error-handler.js +9 -16
  10. package/bin/runners/lib/global-flags.js +0 -37
  11. package/bin/runners/lib/route-truth.js +322 -1167
  12. package/bin/runners/lib/scan-output.js +469 -448
  13. package/bin/runners/lib/ship-output.js +27 -280
  14. package/bin/runners/lib/terminal-ui.js +733 -231
  15. package/bin/runners/lib/truth.js +321 -1004
  16. package/bin/runners/lib/unified-output.js +158 -162
  17. package/bin/runners/lib/upsell.js +204 -104
  18. package/bin/runners/runAllowlist.js +324 -0
  19. package/bin/runners/runAuth.js +95 -324
  20. package/bin/runners/runCheckpoint.js +21 -39
  21. package/bin/runners/runContext.js +24 -136
  22. package/bin/runners/runDoctor.js +67 -115
  23. package/bin/runners/runEvidencePack.js +219 -0
  24. package/bin/runners/runFix.js +5 -6
  25. package/bin/runners/runGuard.js +118 -212
  26. package/bin/runners/runInit.js +2 -14
  27. package/bin/runners/runInstall.js +281 -0
  28. package/bin/runners/runLabs.js +341 -0
  29. package/bin/runners/runMcp.js +52 -130
  30. package/bin/runners/runPolish.js +20 -43
  31. package/bin/runners/runProve.js +3 -13
  32. package/bin/runners/runReality.js +0 -14
  33. package/bin/runners/runReport.js +2 -3
  34. package/bin/runners/runScan.js +44 -511
  35. package/bin/runners/runShip.js +14 -28
  36. package/bin/runners/runValidate.js +2 -19
  37. package/bin/runners/runWatch.js +54 -118
  38. package/bin/vibecheck.js +41 -148
  39. package/mcp-server/ARCHITECTURE.md +339 -0
  40. package/mcp-server/__tests__/cache.test.ts +313 -0
  41. package/mcp-server/__tests__/executor.test.ts +239 -0
  42. package/mcp-server/__tests__/fixtures/exclusion-test/.cache/webpack/cache.pack +1 -0
  43. package/mcp-server/__tests__/fixtures/exclusion-test/.next/server/chunk.js +3 -0
  44. package/mcp-server/__tests__/fixtures/exclusion-test/.turbo/cache.json +3 -0
  45. package/mcp-server/__tests__/fixtures/exclusion-test/.venv/lib/env.py +3 -0
  46. package/mcp-server/__tests__/fixtures/exclusion-test/dist/bundle.js +3 -0
  47. package/mcp-server/__tests__/fixtures/exclusion-test/package.json +5 -0
  48. package/mcp-server/__tests__/fixtures/exclusion-test/src/app.ts +5 -0
  49. package/mcp-server/__tests__/fixtures/exclusion-test/venv/lib/config.py +4 -0
  50. package/mcp-server/__tests__/ids.test.ts +345 -0
  51. package/mcp-server/__tests__/integration/tools.test.ts +410 -0
  52. package/mcp-server/__tests__/registry.test.ts +365 -0
  53. package/mcp-server/__tests__/sandbox.test.ts +323 -0
  54. package/mcp-server/__tests__/schemas.test.ts +372 -0
  55. package/mcp-server/benchmarks/run-benchmarks.ts +304 -0
  56. package/mcp-server/examples/doctor.request.json +14 -0
  57. package/mcp-server/examples/doctor.response.json +53 -0
  58. package/mcp-server/examples/error.response.json +15 -0
  59. package/mcp-server/examples/scan.request.json +14 -0
  60. package/mcp-server/examples/scan.response.json +108 -0
  61. package/mcp-server/handlers/tool-handler.ts +671 -0
  62. package/mcp-server/index-v3.ts +293 -0
  63. package/mcp-server/index.js +1072 -1573
  64. package/mcp-server/index.old.js +4137 -0
  65. package/mcp-server/lib/cache.ts +341 -0
  66. package/mcp-server/lib/errors.ts +346 -0
  67. package/mcp-server/lib/executor.ts +792 -0
  68. package/mcp-server/lib/ids.ts +238 -0
  69. package/mcp-server/lib/logger.ts +368 -0
  70. package/mcp-server/lib/metrics.ts +365 -0
  71. package/mcp-server/lib/sandbox.ts +337 -0
  72. package/mcp-server/lib/validator.ts +229 -0
  73. package/mcp-server/package-lock.json +165 -0
  74. package/mcp-server/package.json +32 -7
  75. package/mcp-server/premium-tools.js +2 -2
  76. package/mcp-server/registry/tools.json +476 -0
  77. package/mcp-server/schemas/error-envelope.schema.json +125 -0
  78. package/mcp-server/schemas/finding.schema.json +167 -0
  79. package/mcp-server/schemas/report-artifact.schema.json +88 -0
  80. package/mcp-server/schemas/run-request.schema.json +75 -0
  81. package/mcp-server/schemas/verdict.schema.json +168 -0
  82. package/mcp-server/tier-auth.d.ts +71 -0
  83. package/mcp-server/tier-auth.js +371 -183
  84. package/mcp-server/truth-context.js +90 -131
  85. package/mcp-server/truth-firewall-tools.js +1000 -1611
  86. package/mcp-server/tsconfig.json +34 -0
  87. package/mcp-server/vibecheck-tools.js +2 -2
  88. package/mcp-server/vitest.config.ts +16 -0
  89. package/package.json +3 -4
  90. package/bin/runners/lib/agent-firewall/ai/false-positive-analyzer.js +0 -474
  91. package/bin/runners/lib/agent-firewall/change-packet/builder.js +0 -488
  92. package/bin/runners/lib/agent-firewall/change-packet/schema.json +0 -228
  93. package/bin/runners/lib/agent-firewall/change-packet/store.js +0 -200
  94. package/bin/runners/lib/agent-firewall/claims/claim-types.js +0 -21
  95. package/bin/runners/lib/agent-firewall/claims/extractor.js +0 -303
  96. package/bin/runners/lib/agent-firewall/claims/patterns.js +0 -24
  97. package/bin/runners/lib/agent-firewall/critic/index.js +0 -151
  98. package/bin/runners/lib/agent-firewall/critic/judge.js +0 -432
  99. package/bin/runners/lib/agent-firewall/critic/prompts.js +0 -305
  100. package/bin/runners/lib/agent-firewall/evidence/auth-evidence.js +0 -88
  101. package/bin/runners/lib/agent-firewall/evidence/contract-evidence.js +0 -75
  102. package/bin/runners/lib/agent-firewall/evidence/env-evidence.js +0 -127
  103. package/bin/runners/lib/agent-firewall/evidence/resolver.js +0 -102
  104. package/bin/runners/lib/agent-firewall/evidence/route-evidence.js +0 -213
  105. package/bin/runners/lib/agent-firewall/evidence/side-effect-evidence.js +0 -145
  106. package/bin/runners/lib/agent-firewall/fs-hook/daemon.js +0 -19
  107. package/bin/runners/lib/agent-firewall/fs-hook/installer.js +0 -87
  108. package/bin/runners/lib/agent-firewall/fs-hook/watcher.js +0 -184
  109. package/bin/runners/lib/agent-firewall/git-hook/pre-commit.js +0 -163
  110. package/bin/runners/lib/agent-firewall/ide-extension/cursor.js +0 -107
  111. package/bin/runners/lib/agent-firewall/ide-extension/vscode.js +0 -68
  112. package/bin/runners/lib/agent-firewall/ide-extension/windsurf.js +0 -66
  113. package/bin/runners/lib/agent-firewall/interceptor/base.js +0 -304
  114. package/bin/runners/lib/agent-firewall/interceptor/cursor.js +0 -35
  115. package/bin/runners/lib/agent-firewall/interceptor/vscode.js +0 -35
  116. package/bin/runners/lib/agent-firewall/interceptor/windsurf.js +0 -34
  117. package/bin/runners/lib/agent-firewall/lawbook/distributor.js +0 -465
  118. package/bin/runners/lib/agent-firewall/lawbook/evaluator.js +0 -604
  119. package/bin/runners/lib/agent-firewall/lawbook/index.js +0 -304
  120. package/bin/runners/lib/agent-firewall/lawbook/registry.js +0 -514
  121. package/bin/runners/lib/agent-firewall/lawbook/schema.js +0 -420
  122. package/bin/runners/lib/agent-firewall/logger.js +0 -141
  123. package/bin/runners/lib/agent-firewall/policy/default-policy.json +0 -90
  124. package/bin/runners/lib/agent-firewall/policy/engine.js +0 -103
  125. package/bin/runners/lib/agent-firewall/policy/loader.js +0 -451
  126. package/bin/runners/lib/agent-firewall/policy/rules/auth-drift.js +0 -50
  127. package/bin/runners/lib/agent-firewall/policy/rules/contract-drift.js +0 -50
  128. package/bin/runners/lib/agent-firewall/policy/rules/fake-success.js +0 -86
  129. package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +0 -162
  130. package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +0 -189
  131. package/bin/runners/lib/agent-firewall/policy/rules/scope.js +0 -93
  132. package/bin/runners/lib/agent-firewall/policy/rules/unsafe-side-effect.js +0 -57
  133. package/bin/runners/lib/agent-firewall/policy/schema.json +0 -183
  134. package/bin/runners/lib/agent-firewall/policy/verdict.js +0 -54
  135. package/bin/runners/lib/agent-firewall/proposal/extractor.js +0 -394
  136. package/bin/runners/lib/agent-firewall/proposal/index.js +0 -212
  137. package/bin/runners/lib/agent-firewall/proposal/schema.js +0 -251
  138. package/bin/runners/lib/agent-firewall/proposal/validator.js +0 -386
  139. package/bin/runners/lib/agent-firewall/reality/index.js +0 -332
  140. package/bin/runners/lib/agent-firewall/reality/state.js +0 -625
  141. package/bin/runners/lib/agent-firewall/reality/watcher.js +0 -322
  142. package/bin/runners/lib/agent-firewall/risk/index.js +0 -173
  143. package/bin/runners/lib/agent-firewall/risk/scorer.js +0 -328
  144. package/bin/runners/lib/agent-firewall/risk/thresholds.js +0 -321
  145. package/bin/runners/lib/agent-firewall/risk/vectors.js +0 -421
  146. package/bin/runners/lib/agent-firewall/simulator/diff-simulator.js +0 -472
  147. package/bin/runners/lib/agent-firewall/simulator/import-resolver.js +0 -346
  148. package/bin/runners/lib/agent-firewall/simulator/index.js +0 -181
  149. package/bin/runners/lib/agent-firewall/simulator/route-validator.js +0 -380
  150. package/bin/runners/lib/agent-firewall/time-machine/incident-correlator.js +0 -661
  151. package/bin/runners/lib/agent-firewall/time-machine/index.js +0 -267
  152. package/bin/runners/lib/agent-firewall/time-machine/replay-engine.js +0 -436
  153. package/bin/runners/lib/agent-firewall/time-machine/state-reconstructor.js +0 -490
  154. package/bin/runners/lib/agent-firewall/time-machine/timeline-builder.js +0 -530
  155. package/bin/runners/lib/agent-firewall/truthpack/index.js +0 -67
  156. package/bin/runners/lib/agent-firewall/truthpack/loader.js +0 -137
  157. package/bin/runners/lib/agent-firewall/unblock/planner.js +0 -337
  158. package/bin/runners/lib/agent-firewall/utils/ignore-checker.js +0 -118
  159. package/bin/runners/lib/api-client.js +0 -269
  160. package/bin/runners/lib/authority-badge.js +0 -425
  161. package/bin/runners/lib/engines/accessibility-engine.js +0 -190
  162. package/bin/runners/lib/engines/api-consistency-engine.js +0 -162
  163. package/bin/runners/lib/engines/ast-cache.js +0 -99
  164. package/bin/runners/lib/engines/code-quality-engine.js +0 -255
  165. package/bin/runners/lib/engines/console-logs-engine.js +0 -115
  166. package/bin/runners/lib/engines/cross-file-analysis-engine.js +0 -268
  167. package/bin/runners/lib/engines/dead-code-engine.js +0 -198
  168. package/bin/runners/lib/engines/deprecated-api-engine.js +0 -226
  169. package/bin/runners/lib/engines/empty-catch-engine.js +0 -150
  170. package/bin/runners/lib/engines/file-filter.js +0 -131
  171. package/bin/runners/lib/engines/hardcoded-secrets-engine.js +0 -251
  172. package/bin/runners/lib/engines/mock-data-engine.js +0 -272
  173. package/bin/runners/lib/engines/parallel-processor.js +0 -71
  174. package/bin/runners/lib/engines/performance-issues-engine.js +0 -265
  175. package/bin/runners/lib/engines/security-vulnerabilities-engine.js +0 -243
  176. package/bin/runners/lib/engines/todo-fixme-engine.js +0 -115
  177. package/bin/runners/lib/engines/type-aware-engine.js +0 -152
  178. package/bin/runners/lib/engines/unsafe-regex-engine.js +0 -225
  179. package/bin/runners/lib/engines/vibecheck-engines/README.md +0 -53
  180. package/bin/runners/lib/engines/vibecheck-engines/index.js +0 -15
  181. package/bin/runners/lib/engines/vibecheck-engines/lib/ast-cache.js +0 -164
  182. package/bin/runners/lib/engines/vibecheck-engines/lib/code-quality-engine.js +0 -291
  183. package/bin/runners/lib/engines/vibecheck-engines/lib/console-logs-engine.js +0 -83
  184. package/bin/runners/lib/engines/vibecheck-engines/lib/dead-code-engine.js +0 -198
  185. package/bin/runners/lib/engines/vibecheck-engines/lib/deprecated-api-engine.js +0 -275
  186. package/bin/runners/lib/engines/vibecheck-engines/lib/empty-catch-engine.js +0 -167
  187. package/bin/runners/lib/engines/vibecheck-engines/lib/file-filter.js +0 -217
  188. package/bin/runners/lib/engines/vibecheck-engines/lib/hardcoded-secrets-engine.js +0 -139
  189. package/bin/runners/lib/engines/vibecheck-engines/lib/mock-data-engine.js +0 -140
  190. package/bin/runners/lib/engines/vibecheck-engines/lib/parallel-processor.js +0 -164
  191. package/bin/runners/lib/engines/vibecheck-engines/lib/performance-issues-engine.js +0 -234
  192. package/bin/runners/lib/engines/vibecheck-engines/lib/type-aware-engine.js +0 -217
  193. package/bin/runners/lib/engines/vibecheck-engines/lib/unsafe-regex-engine.js +0 -78
  194. package/bin/runners/lib/engines/vibecheck-engines/package.json +0 -13
  195. package/bin/runners/lib/exit-codes.js +0 -275
  196. package/bin/runners/lib/fingerprint.js +0 -377
  197. package/bin/runners/lib/help-formatter.js +0 -413
  198. package/bin/runners/lib/logger.js +0 -38
  199. package/bin/runners/lib/ship-output-enterprise.js +0 -239
  200. package/bin/runners/lib/unified-cli-output.js +0 -604
  201. package/bin/runners/runAgent.d.ts +0 -5
  202. package/bin/runners/runAgent.js +0 -161
  203. package/bin/runners/runApprove.js +0 -1200
  204. package/bin/runners/runClassify.js +0 -859
  205. package/bin/runners/runContext.d.ts +0 -4
  206. package/bin/runners/runFirewall.d.ts +0 -5
  207. package/bin/runners/runFirewall.js +0 -134
  208. package/bin/runners/runFirewallHook.d.ts +0 -5
  209. package/bin/runners/runFirewallHook.js +0 -56
  210. package/bin/runners/runPolish.d.ts +0 -4
  211. package/bin/runners/runProof.zip +0 -0
  212. package/bin/runners/runTruth.d.ts +0 -5
  213. package/bin/runners/runTruth.js +0 -101
  214. package/mcp-server/HARDENING_SUMMARY.md +0 -299
  215. package/mcp-server/agent-firewall-interceptor.js +0 -500
  216. package/mcp-server/authority-tools.js +0 -569
  217. package/mcp-server/conductor/conflict-resolver.js +0 -588
  218. package/mcp-server/conductor/execution-planner.js +0 -544
  219. package/mcp-server/conductor/index.js +0 -377
  220. package/mcp-server/conductor/lock-manager.js +0 -615
  221. package/mcp-server/conductor/request-queue.js +0 -550
  222. package/mcp-server/conductor/session-manager.js +0 -500
  223. package/mcp-server/conductor/tools.js +0 -510
  224. package/mcp-server/lib/api-client.cjs +0 -13
  225. package/mcp-server/lib/logger.cjs +0 -30
  226. package/mcp-server/logger.js +0 -173
  227. package/mcp-server/tools-v3.js +0 -706
  228. package/mcp-server/vibecheck-mcp-server-3.2.0.tgz +0 -0
@@ -0,0 +1,792 @@
1
+ /**
2
+ * CLI/Core Execution Layer
3
+ *
4
+ * Thin wrapper around CLI commands with:
5
+ * - Timeout handling
6
+ * - Cancellation support
7
+ * - Output parsing
8
+ * - Error normalization
9
+ *
10
+ * CRITICAL: Windows-safe. No shell=true. Args array only.
11
+ * Zero Unix shell assumptions (no head/grep/sed/awk).
12
+ */
13
+
14
+ import { spawn, ChildProcess, spawnSync } from 'child_process';
15
+ import { join, dirname, resolve } from 'path';
16
+ import { fileURLToPath } from 'url';
17
+ import { existsSync } from 'fs';
18
+ import { Errors, ErrorCode, VibecheckError, createErrorEnvelope, createSuccessEnvelope } from './errors.js';
19
+ import { createSandbox } from './sandbox.js';
20
+ import { getLogger } from './logger.js';
21
+ import { getGlobalCache } from './cache.js';
22
+ import { getMetricsCollector } from './metrics.js';
23
+ import { generateRequestId } from './ids.js';
24
+
25
+ const __dirname = dirname(fileURLToPath(import.meta.url));
26
+
27
+ /**
28
+ * CLI subcommands allowed - must be in registry
29
+ */
30
+ const ALLOWED_SUBCOMMANDS = new Set([
31
+ 'doctor', 'init', 'scan', 'ship', 'prove', 'reality',
32
+ 'fix', 'guard', 'ctx', 'report', 'polish', 'status',
33
+ 'share', 'badge', 'context', 'verify', 'gate',
34
+ ]);
35
+
36
+ /**
37
+ * Default timeouts per tool (in ms)
38
+ */
39
+ const DEFAULT_TIMEOUTS: Record<string, number> = {
40
+ doctor: 30000,
41
+ init: 60000,
42
+ scan: 120000,
43
+ ship: 90000,
44
+ prove: 600000,
45
+ reality: 300000,
46
+ fix: 300000,
47
+ guard: 60000,
48
+ ctx: 90000,
49
+ report: 60000,
50
+ polish: 60000,
51
+ status: 10000,
52
+ share: 30000,
53
+ badge: 10000,
54
+ };
55
+
56
+ /**
57
+ * Execution options
58
+ */
59
+ export interface ExecuteOptions {
60
+ projectPath: string;
61
+ timeout?: number;
62
+ cacheMode?: 'auto' | 'force' | 'skip';
63
+ cacheMaxAge?: number;
64
+ requestId?: string;
65
+ signal?: AbortSignal;
66
+ }
67
+
68
+ /**
69
+ * Execution result
70
+ */
71
+ export interface ExecuteResult<T> {
72
+ ok: boolean;
73
+ data?: T;
74
+ error?: {
75
+ code: ErrorCode;
76
+ message: string;
77
+ };
78
+ cached?: boolean;
79
+ durationMs: number;
80
+ exitCode?: number;
81
+ stdout?: string;
82
+ stderr?: string;
83
+ }
84
+
85
+ /**
86
+ * Active processes for cancellation
87
+ */
88
+ const activeProcesses = new Map<string, ChildProcess>();
89
+
90
+ /**
91
+ * Concurrency control
92
+ */
93
+ const workspaceLocks = new Map<string, number>();
94
+ const MAX_CONCURRENT_PER_WORKSPACE = 2;
95
+ const MAX_CONCURRENT_GLOBAL = 6;
96
+ let globalConcurrentCount = 0;
97
+
98
+ /**
99
+ * Get CLI binary path
100
+ *
101
+ * Searches multiple locations for the vibecheck CLI.
102
+ * Windows-safe: uses spawnSync with args array, no shell interpolation.
103
+ */
104
+ function getCliBinPath(): string {
105
+ // Try multiple locations
106
+ const possiblePaths = [
107
+ join(__dirname, '..', '..', 'bin', 'vibecheck.js'),
108
+ join(__dirname, '..', '..', 'packages', 'cli', 'bin', 'vibecheck.js'),
109
+ join(__dirname, '..', '..', 'packages', 'cli', 'dist', 'standalone.js'),
110
+ join(__dirname, '..', '..', 'node_modules', '@vibecheckai', 'cli', 'bin', 'vibecheck.js'),
111
+ join(__dirname, '..', 'node_modules', '.bin', 'vibecheck'),
112
+ ];
113
+
114
+ for (const binPath of possiblePaths) {
115
+ const resolvedPath = resolve(binPath);
116
+ if (existsSync(resolvedPath)) {
117
+ // Verify it works - use args array, NOT shell string
118
+ try {
119
+ const result = spawnSync('node', [resolvedPath, '--version'], {
120
+ encoding: 'utf-8',
121
+ timeout: 5000,
122
+ shell: false, // CRITICAL: No shell
123
+ windowsHide: true,
124
+ });
125
+ if (result.status === 0) {
126
+ return resolvedPath;
127
+ }
128
+ } catch {
129
+ continue;
130
+ }
131
+ }
132
+ }
133
+
134
+ // Try global vibecheck
135
+ try {
136
+ const result = spawnSync(process.platform === 'win32' ? 'where' : 'which', ['vibecheck'], {
137
+ encoding: 'utf-8',
138
+ timeout: 5000,
139
+ shell: false,
140
+ windowsHide: true,
141
+ });
142
+ if (result.status === 0 && result.stdout) {
143
+ return result.stdout.trim().split('\n')[0];
144
+ }
145
+ } catch {
146
+ // Not found globally
147
+ }
148
+
149
+ throw new VibecheckError(
150
+ ErrorCode.DEPENDENCY_MISSING,
151
+ 'vibecheck CLI not found. Run: npm install -g @vibecheckai/cli',
152
+ { userAction: 'Install the vibecheck CLI globally or in your project' }
153
+ );
154
+ }
155
+
156
+ /**
157
+ * Check if subcommand is allowed
158
+ */
159
+ function validateSubcommand(command: string): void {
160
+ if (!ALLOWED_SUBCOMMANDS.has(command)) {
161
+ throw new VibecheckError(
162
+ ErrorCode.INVALID_INPUT,
163
+ `Unknown CLI subcommand: ${command}. Allowed: ${[...ALLOWED_SUBCOMMANDS].join(', ')}`,
164
+ { userAction: 'Use a valid vibecheck subcommand' }
165
+ );
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Acquire concurrency slot
171
+ */
172
+ async function acquireConcurrencySlot(workspace: string): Promise<void> {
173
+ // Check global limit
174
+ if (globalConcurrentCount >= MAX_CONCURRENT_GLOBAL) {
175
+ throw new VibecheckError(
176
+ ErrorCode.RATE_LIMITED,
177
+ `Global concurrency limit reached (${MAX_CONCURRENT_GLOBAL})`,
178
+ { retryable: true, retryAfterMs: 1000 }
179
+ );
180
+ }
181
+
182
+ // Check workspace limit
183
+ const workspaceCount = workspaceLocks.get(workspace) ?? 0;
184
+ if (workspaceCount >= MAX_CONCURRENT_PER_WORKSPACE) {
185
+ throw new VibecheckError(
186
+ ErrorCode.RATE_LIMITED,
187
+ `Workspace concurrency limit reached (${MAX_CONCURRENT_PER_WORKSPACE})`,
188
+ { retryable: true, retryAfterMs: 1000 }
189
+ );
190
+ }
191
+
192
+ // Acquire slot
193
+ globalConcurrentCount++;
194
+ workspaceLocks.set(workspace, workspaceCount + 1);
195
+ }
196
+
197
+ /**
198
+ * Release concurrency slot
199
+ */
200
+ function releaseConcurrencySlot(workspace: string): void {
201
+ globalConcurrentCount = Math.max(0, globalConcurrentCount - 1);
202
+ const workspaceCount = workspaceLocks.get(workspace) ?? 0;
203
+ if (workspaceCount <= 1) {
204
+ workspaceLocks.delete(workspace);
205
+ } else {
206
+ workspaceLocks.set(workspace, workspaceCount - 1);
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Parse JSON from CLI output
212
+ *
213
+ * CLI output may contain ANSI codes, warnings, or other non-JSON lines.
214
+ * We extract the JSON object/array from the output.
215
+ */
216
+ function parseJsonOutput<T>(stdout: string): T | null {
217
+ // Try direct parse first
218
+ try {
219
+ return JSON.parse(stdout) as T;
220
+ } catch {
221
+ // Try to extract JSON from output
222
+ }
223
+
224
+ // Find JSON object or array
225
+ const jsonMatch = stdout.match(/(\{[\s\S]*\}|\[[\s\S]*\])/);
226
+ if (jsonMatch) {
227
+ try {
228
+ return JSON.parse(jsonMatch[1]) as T;
229
+ } catch {
230
+ // Parse failed
231
+ }
232
+ }
233
+
234
+ return null;
235
+ }
236
+
237
+ /**
238
+ * Execute a CLI command
239
+ *
240
+ * CRITICAL: Uses spawn with args array. No shell=true. Windows-safe.
241
+ */
242
+ export async function executeCli<T>(
243
+ command: string,
244
+ args: string[],
245
+ options: ExecuteOptions
246
+ ): Promise<ExecuteResult<T>> {
247
+ const logger = getLogger();
248
+ const metrics = getMetricsCollector();
249
+ const startTime = Date.now();
250
+ const requestId = options.requestId ?? generateRequestId();
251
+
252
+ // Validate subcommand
253
+ try {
254
+ validateSubcommand(command);
255
+ } catch (e) {
256
+ return {
257
+ ok: false,
258
+ error: { code: ErrorCode.INVALID_INPUT, message: (e as Error).message },
259
+ durationMs: Date.now() - startTime,
260
+ };
261
+ }
262
+
263
+ // Validate project path
264
+ const sandbox = createSandbox({ workspace: options.projectPath });
265
+ const validation = sandbox.validate(options.projectPath);
266
+ if (!validation.allowed) {
267
+ return {
268
+ ok: false,
269
+ error: { code: ErrorCode.PATH_OUTSIDE_SANDBOX, message: validation.reason! },
270
+ durationMs: Date.now() - startTime,
271
+ };
272
+ }
273
+
274
+ // Check cache
275
+ const cache = getGlobalCache();
276
+ const cacheKey = cache.generateKey(command, options.projectPath, { args });
277
+
278
+ if (options.cacheMode !== 'skip') {
279
+ const cached = cache.get<T>(cacheKey);
280
+ if (cached.cached) {
281
+ const maxAge = (options.cacheMaxAge ?? 300) * 1000;
282
+ if (cached.age < maxAge) {
283
+ logger.debug('Cache hit', { command, cacheAge: cached.age });
284
+ metrics.recordToolExecution(`vibecheck.${command}`, Date.now() - startTime, true, true);
285
+ return {
286
+ ok: true,
287
+ data: cached.data,
288
+ cached: true,
289
+ durationMs: Date.now() - startTime,
290
+ };
291
+ }
292
+ }
293
+ }
294
+
295
+ // Acquire concurrency slot
296
+ try {
297
+ await acquireConcurrencySlot(options.projectPath);
298
+ } catch (e) {
299
+ return {
300
+ ok: false,
301
+ error: { code: ErrorCode.RATE_LIMITED, message: (e as Error).message },
302
+ durationMs: Date.now() - startTime,
303
+ };
304
+ }
305
+
306
+ // Build command
307
+ let binPath: string;
308
+ try {
309
+ binPath = getCliBinPath();
310
+ } catch (e) {
311
+ releaseConcurrencySlot(options.projectPath);
312
+ return {
313
+ ok: false,
314
+ error: { code: ErrorCode.DEPENDENCY_MISSING, message: (e as Error).message },
315
+ durationMs: Date.now() - startTime,
316
+ };
317
+ }
318
+
319
+ // Build full args array - args array ONLY, no shell string interpolation
320
+ const fullArgs = [binPath, command, '--json', ...args];
321
+ const timeout = options.timeout ?? DEFAULT_TIMEOUTS[command] ?? 120000;
322
+
323
+ logger.info('Executing CLI', { command, args, timeout, requestId });
324
+
325
+ return new Promise((resolve) => {
326
+ let stdout = '';
327
+ let stderr = '';
328
+ let completed = false;
329
+ let timeoutId: NodeJS.Timeout | undefined;
330
+
331
+ // Spawn process - CRITICAL: shell: false (default, but explicit)
332
+ const child = spawn('node', fullArgs, {
333
+ cwd: options.projectPath,
334
+ env: {
335
+ ...process.env,
336
+ VIBECHECK_SKIP_AUTH: '1',
337
+ VIBECHECK_JSON_OUTPUT: '1',
338
+ VIBECHECK_REQUEST_ID: requestId,
339
+ // Don't pass through potentially dangerous vars
340
+ VIBECHECK_DANGEROUS: undefined,
341
+ },
342
+ shell: false, // CRITICAL: No shell
343
+ windowsHide: true,
344
+ stdio: ['ignore', 'pipe', 'pipe'],
345
+ });
346
+
347
+ // Track for cancellation
348
+ activeProcesses.set(requestId, child);
349
+
350
+ // Handle abort signal
351
+ if (options.signal) {
352
+ options.signal.addEventListener('abort', () => {
353
+ if (!completed) {
354
+ child.kill('SIGTERM');
355
+ completed = true;
356
+ activeProcesses.delete(requestId);
357
+ releaseConcurrencySlot(options.projectPath);
358
+ clearTimeout(timeoutId);
359
+
360
+ metrics.recordToolExecution(`vibecheck.${command}`, Date.now() - startTime, false, false);
361
+ resolve({
362
+ ok: false,
363
+ error: { code: ErrorCode.CANCELLED, message: 'Operation cancelled' },
364
+ durationMs: Date.now() - startTime,
365
+ });
366
+ }
367
+ });
368
+ }
369
+
370
+ // Collect output
371
+ child.stdout?.on('data', (data: Buffer) => {
372
+ stdout += data.toString();
373
+ });
374
+
375
+ child.stderr?.on('data', (data: Buffer) => {
376
+ stderr += data.toString();
377
+ });
378
+
379
+ // Handle timeout
380
+ timeoutId = setTimeout(() => {
381
+ if (!completed) {
382
+ child.kill('SIGTERM');
383
+ // Give process time to cleanup
384
+ setTimeout(() => {
385
+ if (!completed) {
386
+ child.kill('SIGKILL');
387
+ }
388
+ }, 1000);
389
+
390
+ completed = true;
391
+ activeProcesses.delete(requestId);
392
+ releaseConcurrencySlot(options.projectPath);
393
+
394
+ metrics.recordToolExecution(`vibecheck.${command}`, Date.now() - startTime, false, false);
395
+ resolve({
396
+ ok: false,
397
+ error: { code: ErrorCode.TIMEOUT, message: `Timeout after ${timeout}ms` },
398
+ durationMs: Date.now() - startTime,
399
+ });
400
+ }
401
+ }, timeout);
402
+
403
+ // Handle completion
404
+ child.on('close', (code: number | null) => {
405
+ clearTimeout(timeoutId);
406
+ if (completed) return;
407
+ completed = true;
408
+ activeProcesses.delete(requestId);
409
+ releaseConcurrencySlot(options.projectPath);
410
+
411
+ const durationMs = Date.now() - startTime;
412
+
413
+ // Try to parse JSON output
414
+ const data = parseJsonOutput<T>(stdout);
415
+
416
+ if (code === 0 && data !== null) {
417
+ // Success
418
+ if (options.cacheMode !== 'skip') {
419
+ cache.set(cacheKey, data, { ttl: options.cacheMaxAge ?? 300 });
420
+ }
421
+
422
+ logger.info('CLI completed', { command, code, durationMs });
423
+ metrics.recordToolExecution(`vibecheck.${command}`, durationMs, true, false);
424
+
425
+ resolve({
426
+ ok: true,
427
+ data,
428
+ durationMs,
429
+ cached: false,
430
+ exitCode: code,
431
+ stdout,
432
+ stderr,
433
+ });
434
+ return;
435
+ }
436
+
437
+ if (code === 0 && data === null) {
438
+ // Success but no JSON output - return raw output
439
+ logger.info('CLI completed (no JSON)', { command, code, durationMs });
440
+ metrics.recordToolExecution(`vibecheck.${command}`, durationMs, true, false);
441
+
442
+ resolve({
443
+ ok: true,
444
+ data: { output: stdout, exitCode: code } as unknown as T,
445
+ durationMs,
446
+ cached: false,
447
+ exitCode: code ?? undefined,
448
+ stdout,
449
+ stderr,
450
+ });
451
+ return;
452
+ }
453
+
454
+ // Non-zero exit
455
+ logger.warn('CLI failed', { command, code, stderr, durationMs });
456
+ metrics.recordToolExecution(`vibecheck.${command}`, durationMs, false, false);
457
+
458
+ resolve({
459
+ ok: false,
460
+ error: {
461
+ code: ErrorCode.CLI_ERROR,
462
+ message: stderr || stdout || `CLI exited with code ${code}`,
463
+ },
464
+ durationMs,
465
+ exitCode: code ?? undefined,
466
+ stdout,
467
+ stderr,
468
+ });
469
+ });
470
+
471
+ child.on('error', (err: Error) => {
472
+ clearTimeout(timeoutId);
473
+ if (completed) return;
474
+ completed = true;
475
+ activeProcesses.delete(requestId);
476
+ releaseConcurrencySlot(options.projectPath);
477
+
478
+ logger.error('CLI error', err);
479
+ metrics.recordToolExecution(`vibecheck.${command}`, Date.now() - startTime, false, false);
480
+
481
+ resolve({
482
+ ok: false,
483
+ error: { code: ErrorCode.CLI_ERROR, message: err.message },
484
+ durationMs: Date.now() - startTime,
485
+ });
486
+ });
487
+ });
488
+ }
489
+
490
+ /**
491
+ * Execute CLI synchronously (for simple commands)
492
+ *
493
+ * CRITICAL: Uses spawnSync with args array. No shell. Windows-safe.
494
+ */
495
+ export function executeCliSync<T>(
496
+ command: string,
497
+ args: string[],
498
+ options: ExecuteOptions
499
+ ): ExecuteResult<T> {
500
+ const startTime = Date.now();
501
+
502
+ // Validate subcommand
503
+ try {
504
+ validateSubcommand(command);
505
+ } catch (e) {
506
+ return {
507
+ ok: false,
508
+ error: { code: ErrorCode.INVALID_INPUT, message: (e as Error).message },
509
+ durationMs: Date.now() - startTime,
510
+ };
511
+ }
512
+
513
+ // Validate project path
514
+ const sandbox = createSandbox({ workspace: options.projectPath });
515
+ const validation = sandbox.validate(options.projectPath);
516
+ if (!validation.allowed) {
517
+ return {
518
+ ok: false,
519
+ error: { code: ErrorCode.PATH_OUTSIDE_SANDBOX, message: validation.reason! },
520
+ durationMs: Date.now() - startTime,
521
+ };
522
+ }
523
+
524
+ let binPath: string;
525
+ try {
526
+ binPath = getCliBinPath();
527
+ } catch (e) {
528
+ return {
529
+ ok: false,
530
+ error: { code: ErrorCode.DEPENDENCY_MISSING, message: (e as Error).message },
531
+ durationMs: Date.now() - startTime,
532
+ };
533
+ }
534
+
535
+ const fullArgs = [binPath, command, '--json', ...args];
536
+
537
+ try {
538
+ // CRITICAL: shell: false, args array
539
+ const result = spawnSync('node', fullArgs, {
540
+ cwd: options.projectPath,
541
+ encoding: 'utf-8',
542
+ timeout: options.timeout ?? DEFAULT_TIMEOUTS[command] ?? 120000,
543
+ maxBuffer: 10 * 1024 * 1024,
544
+ shell: false, // CRITICAL: No shell
545
+ windowsHide: true,
546
+ env: {
547
+ ...process.env,
548
+ VIBECHECK_SKIP_AUTH: '1',
549
+ VIBECHECK_JSON_OUTPUT: '1',
550
+ },
551
+ });
552
+
553
+ const durationMs = Date.now() - startTime;
554
+
555
+ if (result.error) {
556
+ return {
557
+ ok: false,
558
+ error: { code: ErrorCode.CLI_ERROR, message: result.error.message },
559
+ durationMs,
560
+ };
561
+ }
562
+
563
+ const stdout = result.stdout ?? '';
564
+ const data = parseJsonOutput<T>(stdout);
565
+
566
+ if (result.status === 0 && data !== null) {
567
+ return { ok: true, data, durationMs, exitCode: result.status };
568
+ }
569
+
570
+ if (result.status === 0) {
571
+ return {
572
+ ok: true,
573
+ data: { output: stdout } as unknown as T,
574
+ durationMs,
575
+ exitCode: result.status,
576
+ };
577
+ }
578
+
579
+ return {
580
+ ok: false,
581
+ error: {
582
+ code: ErrorCode.CLI_ERROR,
583
+ message: result.stderr || stdout || `CLI exited with code ${result.status}`,
584
+ },
585
+ durationMs,
586
+ exitCode: result.status ?? undefined,
587
+ };
588
+ } catch (err) {
589
+ const error = err as { message?: string; status?: number };
590
+ return {
591
+ ok: false,
592
+ error: {
593
+ code: ErrorCode.CLI_ERROR,
594
+ message: error.message || 'CLI execution failed',
595
+ },
596
+ durationMs: Date.now() - startTime,
597
+ };
598
+ }
599
+ }
600
+
601
+ /**
602
+ * Cancel a running command
603
+ */
604
+ export function cancelExecution(requestId: string): boolean {
605
+ const child = activeProcesses.get(requestId);
606
+ if (child) {
607
+ child.kill('SIGTERM');
608
+ activeProcesses.delete(requestId);
609
+ return true;
610
+ }
611
+ return false;
612
+ }
613
+
614
+ /**
615
+ * Get count of active executions
616
+ */
617
+ export function getActiveExecutionCount(): number {
618
+ return activeProcesses.size;
619
+ }
620
+
621
+ /**
622
+ * Get concurrency stats
623
+ */
624
+ export function getConcurrencyStats(): {
625
+ global: number;
626
+ maxGlobal: number;
627
+ workspaces: Record<string, number>;
628
+ maxPerWorkspace: number;
629
+ } {
630
+ const workspaces: Record<string, number> = {};
631
+ for (const [key, value] of workspaceLocks.entries()) {
632
+ workspaces[key] = value;
633
+ }
634
+ return {
635
+ global: globalConcurrentCount,
636
+ maxGlobal: MAX_CONCURRENT_GLOBAL,
637
+ workspaces,
638
+ maxPerWorkspace: MAX_CONCURRENT_PER_WORKSPACE,
639
+ };
640
+ }
641
+
642
+ /**
643
+ * Tool-specific executors
644
+ */
645
+ export const ToolExecutors = {
646
+ async doctor(options: ExecuteOptions) {
647
+ return executeCli('doctor', [], options);
648
+ },
649
+
650
+ async scan(options: ExecuteOptions & { profile?: string; since?: string; baseline?: string }) {
651
+ const args: string[] = [];
652
+ if (options.profile) args.push(`--profile=${options.profile}`);
653
+ if (options.since) args.push(`--since=${options.since}`);
654
+ if (options.baseline) args.push(`--baseline=${options.baseline}`);
655
+ return executeCli('scan', args, options);
656
+ },
657
+
658
+ async ship(options: ExecuteOptions & { strict?: boolean; mockproof?: boolean; ci?: boolean }) {
659
+ const args: string[] = [];
660
+ if (options.strict) args.push('--strict');
661
+ if (options.mockproof !== false) args.push('--mockproof');
662
+ if (options.ci) args.push('--ci');
663
+ return executeCli('ship', args, options);
664
+ },
665
+
666
+ async prove(options: ExecuteOptions & {
667
+ url?: string;
668
+ auth?: string;
669
+ storageState?: string;
670
+ maxFixRounds?: number;
671
+ skipReality?: boolean;
672
+ skipFix?: boolean;
673
+ }) {
674
+ const args: string[] = [];
675
+ if (options.url) args.push(`--url=${options.url}`);
676
+ if (options.auth) args.push(`--auth=${options.auth}`);
677
+ if (options.storageState) args.push(`--storage-state=${options.storageState}`);
678
+ if (options.maxFixRounds) args.push(`--max-fix-rounds=${options.maxFixRounds}`);
679
+ if (options.skipReality) args.push('--skip-reality');
680
+ if (options.skipFix) args.push('--skip-fix');
681
+ return executeCli('prove', args, { ...options, timeout: options.timeout ?? 600000 });
682
+ },
683
+
684
+ async reality(options: ExecuteOptions & {
685
+ url: string;
686
+ auth?: string;
687
+ verifyAuth?: boolean;
688
+ storageState?: string;
689
+ maxPages?: number;
690
+ headed?: boolean;
691
+ recordVideo?: boolean;
692
+ recordTrace?: boolean;
693
+ }) {
694
+ const args: string[] = [`--url=${options.url}`];
695
+ if (options.auth) args.push(`--auth=${options.auth}`);
696
+ if (options.verifyAuth) args.push('--verify-auth');
697
+ if (options.storageState) args.push(`--storage-state=${options.storageState}`);
698
+ if (options.maxPages) args.push(`--max-pages=${options.maxPages}`);
699
+ if (options.headed) args.push('--headed');
700
+ if (options.recordVideo) args.push('--record-video');
701
+ if (options.recordTrace) args.push('--record-trace');
702
+ return executeCli('reality', args, { ...options, timeout: options.timeout ?? 300000 });
703
+ },
704
+
705
+ async fix(options: ExecuteOptions & {
706
+ apply?: boolean;
707
+ promptOnly?: boolean;
708
+ autopilot?: boolean;
709
+ maxMissions?: number;
710
+ }) {
711
+ const args: string[] = [];
712
+ if (options.apply) args.push('--apply');
713
+ if (options.promptOnly) args.push('--prompt-only');
714
+ if (options.autopilot) args.push('--autopilot');
715
+ if (options.maxMissions) args.push(`--max-missions=${options.maxMissions}`);
716
+ return executeCli('fix', args, { ...options, timeout: options.timeout ?? 300000 });
717
+ },
718
+
719
+ async guard(options: ExecuteOptions & {
720
+ claims?: boolean;
721
+ hallucinations?: boolean;
722
+ prompts?: boolean;
723
+ strict?: boolean;
724
+ }) {
725
+ const args: string[] = [];
726
+ if (options.claims) args.push('--claims');
727
+ if (options.hallucinations) args.push('--hallucinations');
728
+ if (options.prompts) args.push('--prompts');
729
+ if (options.strict) args.push('--strict');
730
+ return executeCli('guard', args, options);
731
+ },
732
+
733
+ async ctx(options: ExecuteOptions & { snapshot?: boolean; sync?: boolean }) {
734
+ const args: string[] = [];
735
+ if (options.snapshot) args.push('--snapshot');
736
+ if (options.sync) args.push('--sync');
737
+ return executeCli('ctx', args, options);
738
+ },
739
+
740
+ async report(options: ExecuteOptions & {
741
+ type?: string;
742
+ format?: string;
743
+ output?: string;
744
+ maxFindings?: number;
745
+ }) {
746
+ const args: string[] = [];
747
+ if (options.type) args.push(`--type=${options.type}`);
748
+ if (options.format) args.push(`--format=${options.format}`);
749
+ if (options.output) args.push(`--output=${options.output}`);
750
+ if (options.maxFindings) args.push(`--max-findings=${options.maxFindings}`);
751
+ return executeCli('report', args, options);
752
+ },
753
+
754
+ async polish(options: ExecuteOptions & { category?: string; fix?: boolean }) {
755
+ const args: string[] = [];
756
+ if (options.category) args.push(`--category=${options.category}`);
757
+ if (options.fix) args.push('--fix');
758
+ return executeCli('polish', args, options);
759
+ },
760
+
761
+ async status(options: ExecuteOptions) {
762
+ return executeCli('status', [], { ...options, timeout: options.timeout ?? 10000 });
763
+ },
764
+
765
+ async share(options: ExecuteOptions & { missionDir?: string }) {
766
+ const args: string[] = [];
767
+ if (options.missionDir) args.push(`--mission-dir=${options.missionDir}`);
768
+ return executeCli('share', args, options);
769
+ },
770
+
771
+ async badge(options: ExecuteOptions & { format?: string; style?: string }) {
772
+ const args: string[] = [];
773
+ if (options.format) args.push(`--format=${options.format}`);
774
+ if (options.style) args.push(`--style=${options.style}`);
775
+ return executeCli('badge', args, { ...options, timeout: options.timeout ?? 10000 });
776
+ },
777
+
778
+ async init(options: ExecuteOptions & { force?: boolean }) {
779
+ const args: string[] = [];
780
+ if (options.force) args.push('--force');
781
+ return executeCli('init', args, options);
782
+ },
783
+ };
784
+
785
+ export default {
786
+ executeCli,
787
+ executeCliSync,
788
+ cancelExecution,
789
+ getActiveExecutionCount,
790
+ getConcurrencyStats,
791
+ ToolExecutors,
792
+ };