elasticdash-sdk 0.2.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 (349) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +775 -0
  3. package/dist/browser-ui.d.ts +43 -0
  4. package/dist/browser-ui.d.ts.map +1 -0
  5. package/dist/browser-ui.js +246 -0
  6. package/dist/browser-ui.js.map +1 -0
  7. package/dist/capture/event.d.ts +33 -0
  8. package/dist/capture/event.d.ts.map +1 -0
  9. package/dist/capture/event.js +2 -0
  10. package/dist/capture/event.js.map +1 -0
  11. package/dist/capture/index.d.ts +4 -0
  12. package/dist/capture/index.d.ts.map +1 -0
  13. package/dist/capture/index.js +4 -0
  14. package/dist/capture/index.js.map +1 -0
  15. package/dist/capture/recorder.d.ts +24 -0
  16. package/dist/capture/recorder.d.ts.map +1 -0
  17. package/dist/capture/recorder.js +46 -0
  18. package/dist/capture/recorder.js.map +1 -0
  19. package/dist/capture/replay.d.ts +20 -0
  20. package/dist/capture/replay.d.ts.map +1 -0
  21. package/dist/capture/replay.js +47 -0
  22. package/dist/capture/replay.js.map +1 -0
  23. package/dist/ci/api-client.d.ts +38 -0
  24. package/dist/ci/api-client.d.ts.map +1 -0
  25. package/dist/ci/api-client.js +96 -0
  26. package/dist/ci/api-client.js.map +1 -0
  27. package/dist/ci/benchmark.d.ts +33 -0
  28. package/dist/ci/benchmark.d.ts.map +1 -0
  29. package/dist/ci/benchmark.js +213 -0
  30. package/dist/ci/benchmark.js.map +1 -0
  31. package/dist/ci/ed-runner.d.ts +48 -0
  32. package/dist/ci/ed-runner.d.ts.map +1 -0
  33. package/dist/ci/ed-runner.js +260 -0
  34. package/dist/ci/ed-runner.js.map +1 -0
  35. package/dist/ci/executor.d.ts +13 -0
  36. package/dist/ci/executor.d.ts.map +1 -0
  37. package/dist/ci/executor.js +542 -0
  38. package/dist/ci/executor.js.map +1 -0
  39. package/dist/ci/git-info.d.ts +17 -0
  40. package/dist/ci/git-info.d.ts.map +1 -0
  41. package/dist/ci/git-info.js +102 -0
  42. package/dist/ci/git-info.js.map +1 -0
  43. package/dist/ci/index.d.ts +6 -0
  44. package/dist/ci/index.d.ts.map +1 -0
  45. package/dist/ci/index.js +4 -0
  46. package/dist/ci/index.js.map +1 -0
  47. package/dist/ci/measurement.d.ts +9 -0
  48. package/dist/ci/measurement.d.ts.map +1 -0
  49. package/dist/ci/measurement.js +15 -0
  50. package/dist/ci/measurement.js.map +1 -0
  51. package/dist/ci/replay.d.ts +31 -0
  52. package/dist/ci/replay.d.ts.map +1 -0
  53. package/dist/ci/replay.js +96 -0
  54. package/dist/ci/replay.js.map +1 -0
  55. package/dist/ci/reporters/default.d.ts +8 -0
  56. package/dist/ci/reporters/default.d.ts.map +1 -0
  57. package/dist/ci/reporters/default.js +46 -0
  58. package/dist/ci/reporters/default.js.map +1 -0
  59. package/dist/ci/reporters/index.d.ts +8 -0
  60. package/dist/ci/reporters/index.d.ts.map +1 -0
  61. package/dist/ci/reporters/index.js +14 -0
  62. package/dist/ci/reporters/index.js.map +1 -0
  63. package/dist/ci/reporters/json.d.ts +8 -0
  64. package/dist/ci/reporters/json.d.ts.map +1 -0
  65. package/dist/ci/reporters/json.js +14 -0
  66. package/dist/ci/reporters/json.js.map +1 -0
  67. package/dist/ci/reporters/junit.d.ts +8 -0
  68. package/dist/ci/reporters/junit.d.ts.map +1 -0
  69. package/dist/ci/reporters/junit.js +48 -0
  70. package/dist/ci/reporters/junit.js.map +1 -0
  71. package/dist/ci/runner.d.ts +3 -0
  72. package/dist/ci/runner.d.ts.map +1 -0
  73. package/dist/ci/runner.js +187 -0
  74. package/dist/ci/runner.js.map +1 -0
  75. package/dist/ci/test-discovery.d.ts +5 -0
  76. package/dist/ci/test-discovery.d.ts.map +1 -0
  77. package/dist/ci/test-discovery.js +11 -0
  78. package/dist/ci/test-discovery.js.map +1 -0
  79. package/dist/ci/test-loader.d.ts +19 -0
  80. package/dist/ci/test-loader.d.ts.map +1 -0
  81. package/dist/ci/test-loader.js +149 -0
  82. package/dist/ci/test-loader.js.map +1 -0
  83. package/dist/ci/test-registry.d.ts +42 -0
  84. package/dist/ci/test-registry.d.ts.map +1 -0
  85. package/dist/ci/test-registry.js +18 -0
  86. package/dist/ci/test-registry.js.map +1 -0
  87. package/dist/ci/trace-schema.d.ts +30 -0
  88. package/dist/ci/trace-schema.d.ts.map +1 -0
  89. package/dist/ci/trace-schema.js +66 -0
  90. package/dist/ci/trace-schema.js.map +1 -0
  91. package/dist/ci/trace-writer.d.ts +16 -0
  92. package/dist/ci/trace-writer.d.ts.map +1 -0
  93. package/dist/ci/trace-writer.js +108 -0
  94. package/dist/ci/trace-writer.js.map +1 -0
  95. package/dist/ci/types.d.ts +108 -0
  96. package/dist/ci/types.d.ts.map +1 -0
  97. package/dist/ci/types.js +3 -0
  98. package/dist/ci/types.js.map +1 -0
  99. package/dist/ci/upload-client.d.ts +74 -0
  100. package/dist/ci/upload-client.d.ts.map +1 -0
  101. package/dist/ci/upload-client.js +195 -0
  102. package/dist/ci/upload-client.js.map +1 -0
  103. package/dist/cli.d.ts +3 -0
  104. package/dist/cli.d.ts.map +1 -0
  105. package/dist/cli.js +716 -0
  106. package/dist/cli.js.map +1 -0
  107. package/dist/core/agent-state.d.ts +47 -0
  108. package/dist/core/agent-state.d.ts.map +1 -0
  109. package/dist/core/agent-state.js +137 -0
  110. package/dist/core/agent-state.js.map +1 -0
  111. package/dist/core/judge-utils.d.ts +22 -0
  112. package/dist/core/judge-utils.d.ts.map +1 -0
  113. package/dist/core/judge-utils.js +211 -0
  114. package/dist/core/judge-utils.js.map +1 -0
  115. package/dist/core/registry.d.ts +28 -0
  116. package/dist/core/registry.d.ts.map +1 -0
  117. package/dist/core/registry.js +52 -0
  118. package/dist/core/registry.js.map +1 -0
  119. package/dist/dashboard-server.d.ts +65 -0
  120. package/dist/dashboard-server.d.ts.map +1 -0
  121. package/dist/dashboard-server.js +3940 -0
  122. package/dist/dashboard-server.js.map +1 -0
  123. package/dist/execution/tool-runner.d.ts +26 -0
  124. package/dist/execution/tool-runner.d.ts.map +1 -0
  125. package/dist/execution/tool-runner.js +316 -0
  126. package/dist/execution/tool-runner.js.map +1 -0
  127. package/dist/html/dashboard.html +2218 -0
  128. package/dist/http.d.ts +14 -0
  129. package/dist/http.d.ts.map +1 -0
  130. package/dist/http.js +13 -0
  131. package/dist/http.js.map +1 -0
  132. package/dist/index.cjs +8102 -0
  133. package/dist/index.d.ts +61 -0
  134. package/dist/index.d.ts.map +1 -0
  135. package/dist/index.js +67 -0
  136. package/dist/index.js.map +1 -0
  137. package/dist/interceptors/ai-interceptor.d.ts +26 -0
  138. package/dist/interceptors/ai-interceptor.d.ts.map +1 -0
  139. package/dist/interceptors/ai-interceptor.js +756 -0
  140. package/dist/interceptors/ai-interceptor.js.map +1 -0
  141. package/dist/interceptors/db-auto.d.ts +8 -0
  142. package/dist/interceptors/db-auto.d.ts.map +1 -0
  143. package/dist/interceptors/db-auto.js +217 -0
  144. package/dist/interceptors/db-auto.js.map +1 -0
  145. package/dist/interceptors/db.d.ts +23 -0
  146. package/dist/interceptors/db.d.ts.map +1 -0
  147. package/dist/interceptors/db.js +137 -0
  148. package/dist/interceptors/db.js.map +1 -0
  149. package/dist/interceptors/http.d.ts +28 -0
  150. package/dist/interceptors/http.d.ts.map +1 -0
  151. package/dist/interceptors/http.js +356 -0
  152. package/dist/interceptors/http.js.map +1 -0
  153. package/dist/interceptors/side-effects.d.ts +7 -0
  154. package/dist/interceptors/side-effects.d.ts.map +1 -0
  155. package/dist/interceptors/side-effects.js +72 -0
  156. package/dist/interceptors/side-effects.js.map +1 -0
  157. package/dist/interceptors/telemetry-push.d.ts +142 -0
  158. package/dist/interceptors/telemetry-push.d.ts.map +1 -0
  159. package/dist/interceptors/telemetry-push.js +463 -0
  160. package/dist/interceptors/telemetry-push.js.map +1 -0
  161. package/dist/interceptors/tool.d.ts +2 -0
  162. package/dist/interceptors/tool.d.ts.map +1 -0
  163. package/dist/interceptors/tool.js +274 -0
  164. package/dist/interceptors/tool.js.map +1 -0
  165. package/dist/interceptors/workflow-ai.d.ts +5 -0
  166. package/dist/interceptors/workflow-ai.d.ts.map +1 -0
  167. package/dist/interceptors/workflow-ai.js +382 -0
  168. package/dist/interceptors/workflow-ai.js.map +1 -0
  169. package/dist/internals/conditional-recorder.d.ts +21 -0
  170. package/dist/internals/conditional-recorder.d.ts.map +1 -0
  171. package/dist/internals/conditional-recorder.js +54 -0
  172. package/dist/internals/conditional-recorder.js.map +1 -0
  173. package/dist/internals/mock-resolver.d.ts +146 -0
  174. package/dist/internals/mock-resolver.d.ts.map +1 -0
  175. package/dist/internals/mock-resolver.js +427 -0
  176. package/dist/internals/mock-resolver.js.map +1 -0
  177. package/dist/matchers/index.d.ts +96 -0
  178. package/dist/matchers/index.d.ts.map +1 -0
  179. package/dist/matchers/index.js +668 -0
  180. package/dist/matchers/index.js.map +1 -0
  181. package/dist/observability.d.ts +82 -0
  182. package/dist/observability.d.ts.map +1 -0
  183. package/dist/observability.js +471 -0
  184. package/dist/observability.js.map +1 -0
  185. package/dist/portal-executor.d.ts +30 -0
  186. package/dist/portal-executor.d.ts.map +1 -0
  187. package/dist/portal-executor.js +324 -0
  188. package/dist/portal-executor.js.map +1 -0
  189. package/dist/portal-server.d.ts +3 -0
  190. package/dist/portal-server.d.ts.map +1 -0
  191. package/dist/portal-server.js +279 -0
  192. package/dist/portal-server.js.map +1 -0
  193. package/dist/proxy/llm-capture.d.ts +14 -0
  194. package/dist/proxy/llm-capture.d.ts.map +1 -0
  195. package/dist/proxy/llm-capture.js +264 -0
  196. package/dist/proxy/llm-capture.js.map +1 -0
  197. package/dist/reporter.d.ts +3 -0
  198. package/dist/reporter.d.ts.map +1 -0
  199. package/dist/reporter.js +72 -0
  200. package/dist/reporter.js.map +1 -0
  201. package/dist/runWorkflowSubprocess.d.ts +14 -0
  202. package/dist/runWorkflowSubprocess.d.ts.map +1 -0
  203. package/dist/runWorkflowSubprocess.js +66 -0
  204. package/dist/runWorkflowSubprocess.js.map +1 -0
  205. package/dist/runner.d.ts +16 -0
  206. package/dist/runner.d.ts.map +1 -0
  207. package/dist/runner.js +138 -0
  208. package/dist/runner.js.map +1 -0
  209. package/dist/socket-connector.d.ts +22 -0
  210. package/dist/socket-connector.d.ts.map +1 -0
  211. package/dist/socket-connector.js +104 -0
  212. package/dist/socket-connector.js.map +1 -0
  213. package/dist/telemetry-batcher.d.ts +56 -0
  214. package/dist/telemetry-batcher.d.ts.map +1 -0
  215. package/dist/telemetry-batcher.js +143 -0
  216. package/dist/telemetry-batcher.js.map +1 -0
  217. package/dist/test-setup.d.ts +12 -0
  218. package/dist/test-setup.d.ts.map +1 -0
  219. package/dist/test-setup.js +13 -0
  220. package/dist/test-setup.js.map +1 -0
  221. package/dist/tool-registry.d.ts +31 -0
  222. package/dist/tool-registry.d.ts.map +1 -0
  223. package/dist/tool-registry.js +73 -0
  224. package/dist/tool-registry.js.map +1 -0
  225. package/dist/tool-runner-worker.d.ts +2 -0
  226. package/dist/tool-runner-worker.d.ts.map +1 -0
  227. package/dist/tool-runner-worker.js +215 -0
  228. package/dist/tool-runner-worker.js.map +1 -0
  229. package/dist/trace-adapter/context.d.ts +72 -0
  230. package/dist/trace-adapter/context.d.ts.map +1 -0
  231. package/dist/trace-adapter/context.js +80 -0
  232. package/dist/trace-adapter/context.js.map +1 -0
  233. package/dist/tracing.d.ts +2 -0
  234. package/dist/tracing.d.ts.map +1 -0
  235. package/dist/tracing.js +59 -0
  236. package/dist/tracing.js.map +1 -0
  237. package/dist/trigger-executor.d.ts +12 -0
  238. package/dist/trigger-executor.d.ts.map +1 -0
  239. package/dist/trigger-executor.js +130 -0
  240. package/dist/trigger-executor.js.map +1 -0
  241. package/dist/types/portal.d.ts +76 -0
  242. package/dist/types/portal.d.ts.map +1 -0
  243. package/dist/types/portal.js +2 -0
  244. package/dist/types/portal.js.map +1 -0
  245. package/dist/utils/debug.d.ts +3 -0
  246. package/dist/utils/debug.d.ts.map +1 -0
  247. package/dist/utils/debug.js +8 -0
  248. package/dist/utils/debug.js.map +1 -0
  249. package/dist/utils/license-error.d.ts +23 -0
  250. package/dist/utils/license-error.d.ts.map +1 -0
  251. package/dist/utils/license-error.js +42 -0
  252. package/dist/utils/license-error.js.map +1 -0
  253. package/dist/utils/redact.d.ts +7 -0
  254. package/dist/utils/redact.d.ts.map +1 -0
  255. package/dist/utils/redact.js +26 -0
  256. package/dist/utils/redact.js.map +1 -0
  257. package/dist/workflow-runner-worker.d.ts +2 -0
  258. package/dist/workflow-runner-worker.d.ts.map +1 -0
  259. package/dist/workflow-runner-worker.js +329 -0
  260. package/dist/workflow-runner-worker.js.map +1 -0
  261. package/dist/workflow-runner.d.ts +14 -0
  262. package/dist/workflow-runner.d.ts.map +1 -0
  263. package/dist/workflow-runner.js +34 -0
  264. package/dist/workflow-runner.js.map +1 -0
  265. package/docs/agent-coding-instructions.md +138 -0
  266. package/docs/agent-integration-guide.md +564 -0
  267. package/docs/agents.md +140 -0
  268. package/docs/dashboard.md +394 -0
  269. package/docs/deno.md +69 -0
  270. package/docs/instrumentation.md +424 -0
  271. package/docs/langfuse-trace-structure.md +145 -0
  272. package/docs/matchers.md +173 -0
  273. package/docs/observability_contract.md +192 -0
  274. package/docs/observability_mode.md +195 -0
  275. package/docs/quickstart.md +621 -0
  276. package/docs/security-compliance.md +566 -0
  277. package/docs/test-writing-guidelines.md +444 -0
  278. package/docs/tools.md +165 -0
  279. package/docs/workflow-modes.md +253 -0
  280. package/package.json +76 -0
  281. package/src/browser-ui.ts +281 -0
  282. package/src/capture/event.ts +30 -0
  283. package/src/capture/index.ts +3 -0
  284. package/src/capture/recorder.ts +62 -0
  285. package/src/capture/replay.ts +55 -0
  286. package/src/ci/api-client.ts +136 -0
  287. package/src/ci/benchmark.ts +257 -0
  288. package/src/ci/ed-runner.ts +351 -0
  289. package/src/ci/executor.ts +671 -0
  290. package/src/ci/git-info.ts +127 -0
  291. package/src/ci/index.ts +5 -0
  292. package/src/ci/measurement.ts +25 -0
  293. package/src/ci/replay.ts +127 -0
  294. package/src/ci/reporters/default.ts +50 -0
  295. package/src/ci/reporters/index.ts +21 -0
  296. package/src/ci/reporters/json.ts +18 -0
  297. package/src/ci/reporters/junit.ts +61 -0
  298. package/src/ci/runner.ts +208 -0
  299. package/src/ci/test-discovery.ts +16 -0
  300. package/src/ci/test-loader.ts +187 -0
  301. package/src/ci/test-registry.ts +62 -0
  302. package/src/ci/trace-schema.ts +96 -0
  303. package/src/ci/trace-writer.ts +107 -0
  304. package/src/ci/types.ts +115 -0
  305. package/src/ci/upload-client.ts +300 -0
  306. package/src/cli.ts +811 -0
  307. package/src/core/agent-state.ts +162 -0
  308. package/src/core/judge-utils.ts +232 -0
  309. package/src/core/registry.ts +92 -0
  310. package/src/dashboard-server.ts +2047 -0
  311. package/src/execution/tool-runner.ts +352 -0
  312. package/src/html/dashboard.html +2218 -0
  313. package/src/http.ts +13 -0
  314. package/src/index.ts +138 -0
  315. package/src/interceptors/ai-interceptor.ts +798 -0
  316. package/src/interceptors/db-auto.ts +243 -0
  317. package/src/interceptors/db.ts +156 -0
  318. package/src/interceptors/http.ts +393 -0
  319. package/src/interceptors/side-effects.ts +83 -0
  320. package/src/interceptors/telemetry-push.ts +537 -0
  321. package/src/interceptors/tool.ts +287 -0
  322. package/src/interceptors/workflow-ai.ts +419 -0
  323. package/src/internals/conditional-recorder.ts +63 -0
  324. package/src/internals/mock-resolver.ts +492 -0
  325. package/src/matchers/index.ts +824 -0
  326. package/src/observability.ts +501 -0
  327. package/src/portal-executor.ts +355 -0
  328. package/src/portal-server.ts +304 -0
  329. package/src/proxy/llm-capture.ts +301 -0
  330. package/src/reporter.ts +81 -0
  331. package/src/runWorkflowSubprocess.ts +74 -0
  332. package/src/runner.ts +178 -0
  333. package/src/socket-connector.ts +117 -0
  334. package/src/telemetry-batcher.ts +191 -0
  335. package/src/test-setup.ts +16 -0
  336. package/src/tool-registry.ts +94 -0
  337. package/src/tool-runner-worker.ts +244 -0
  338. package/src/trace-adapter/context.ts +156 -0
  339. package/src/tracing.ts +62 -0
  340. package/src/trigger-executor.ts +171 -0
  341. package/src/types/agent.d.ts +63 -0
  342. package/src/types/expect.d.ts +81 -0
  343. package/src/types/modules.d.ts +2 -0
  344. package/src/types/portal.ts +69 -0
  345. package/src/utils/debug.ts +8 -0
  346. package/src/utils/license-error.ts +43 -0
  347. package/src/utils/redact.ts +25 -0
  348. package/src/workflow-runner-worker.ts +386 -0
  349. package/src/workflow-runner.ts +58 -0
package/src/cli.ts ADDED
@@ -0,0 +1,811 @@
1
+ #!/usr/bin/env node
2
+ import 'dotenv/config'
3
+ import { Command } from 'commander'
4
+ import fg from 'fast-glob'
5
+ import path from 'node:path'
6
+ import { pathToFileURL, fileURLToPath } from 'node:url'
7
+ import { existsSync, copyFileSync, mkdirSync, readFileSync, appendFileSync } from 'node:fs'
8
+
9
+ import { registerMatchers } from './matchers/index.js'
10
+ import { installAIInterceptor } from './interceptors/ai-interceptor.js'
11
+ import { runFiles } from './runner.js'
12
+ import { reportResults } from './reporter.js'
13
+ import { startBrowserUiServer, type UiEvent } from './browser-ui.js'
14
+ import { startDashboardServer } from './dashboard-server.js'
15
+ import { initObservability, shutdownObservability } from './observability.js'
16
+ import { startPortalServer } from './portal-server.js'
17
+ import { resolveRuntimeModule, scanTools, buildToolArgs, runToolInSubprocess } from './execution/tool-runner.js'
18
+
19
+ /** Brief, single-line preview of any value for trace event tables. */
20
+ function previewify(value: unknown, maxLen = 200): string {
21
+ if (value === null || value === undefined) return ''
22
+ let str: string
23
+ try {
24
+ str = typeof value === 'string' ? value : JSON.stringify(value)
25
+ } catch {
26
+ str = String(value)
27
+ }
28
+ str = str.replace(/\n/g, ' ').trim()
29
+ return str.length > maxLen ? str.slice(0, maxLen) + '...' : str
30
+ }
31
+
32
+ function stripAnsi(input?: string): string | undefined {
33
+ if (!input) return input
34
+ return input.replace(/\u001b\[[0-9;]*m/g, '')
35
+ }
36
+
37
+ // --- Config loading (optional) ---
38
+ interface ElasticDashConfig {
39
+ testMatch?: string[]
40
+ traceMode?: 'local' | 'remote'
41
+ }
42
+
43
+ async function loadConfig(cwd: string): Promise<ElasticDashConfig> {
44
+ const configPath = path.join(cwd, 'elasticdash.config.ts')
45
+ const configPathJs = path.join(cwd, 'elasticdash.config.js')
46
+
47
+ for (const p of [configPath, configPathJs]) {
48
+ if (existsSync(p)) {
49
+ try {
50
+ const mod = await import(pathToFileURL(p).href)
51
+ return (mod.default ?? {}) as ElasticDashConfig
52
+ } catch (error) {
53
+ // Skip this config file if it can't be imported (e.g., .ts when running from built dist)
54
+ continue
55
+ }
56
+ }
57
+ }
58
+ return {}
59
+ }
60
+
61
+ // --- File discovery ---
62
+ async function discoverTestFiles(patterns: string[], cwd: string): Promise<string[]> {
63
+ const files = await fg(patterns, { cwd, absolute: true })
64
+ return files.sort()
65
+ }
66
+
67
+ // --- Validate repository directory ---
68
+ function validateRepoDirectory(cwd: string): boolean {
69
+ const validFiles = [
70
+ 'elasticdash.config.ts',
71
+ 'elasticdash.config.js',
72
+ 'ed_workflows.ts',
73
+ 'ed_workflows.js',
74
+ 'ed_tools.ts',
75
+ 'ed_tools.js',
76
+ ]
77
+ return validFiles.some(file => existsSync(path.join(cwd, file)))
78
+ }
79
+
80
+ // --- Bootstrap ---
81
+ async function bootstrap(): Promise<void> {
82
+
83
+ const cwd = process.cwd()
84
+ const isInitGuide = process.argv.includes('init-guide')
85
+
86
+ // Skip matchers/interceptors for commands that don't need them
87
+ if (!isInitGuide) {
88
+ registerMatchers()
89
+ installAIInterceptor()
90
+ }
91
+ if (!isInitGuide && !validateRepoDirectory(cwd)) {
92
+ console.error(
93
+ `[elasticdash] Error: elasticdash command must be run from the elasticdash-sdk SDK repository directory.\n` +
94
+ `Current directory: ${cwd}\n` +
95
+ `Expected: A directory containing one of: elasticdash.config.ts, elasticdash.config.js, ed_workflows.ts, ed_workflows.js`
96
+ )
97
+ process.exit(1)
98
+ }
99
+ const config = await loadConfig(cwd)
100
+ const defaultPattern = config.testMatch ?? ['**/*.ai.test.ts', '**/*.ai.test.js']
101
+
102
+ // Read version from package.json
103
+ // Use require for CJS compatibility, fallback to import if needed
104
+ // This path is relative to the compiled dist directory
105
+ let version = 'unknown'
106
+ try {
107
+ // @ts-ignore
108
+ version = (await import(pathToFileURL(path.join(cwd, 'package.json')).href, { with: { type: 'json' } })).default.version
109
+ } catch (e) {
110
+ try {
111
+ version = require(path.join(cwd, 'package.json')).version
112
+ } catch {}
113
+ }
114
+
115
+
116
+ const program = new Command()
117
+
118
+ program
119
+ .name('elasticdash')
120
+ .description('AI-native test runner for ElasticDash workflow testing')
121
+ .version(version)
122
+
123
+ // elasticdash test [dir]
124
+ program
125
+ .command('test [dir]')
126
+ .description('Discover and run all AI test files')
127
+ .option('--no-browser-ui', 'Disable browser progress UI')
128
+ .option('--browser-ui-port <port>', 'Port for browser UI', (v) => Number(v), undefined)
129
+ .action(async (dir?: string, cmd?: any) => {
130
+ const searchBase = dir ? path.resolve(cwd, dir) : cwd
131
+ console.log('[elasticdash] Test discovery pattern:', defaultPattern)
132
+ console.log('[elasticdash] Test search base:', searchBase)
133
+ const files = await discoverTestFiles(defaultPattern, searchBase)
134
+ console.log('[elasticdash] Discovered test files:', files)
135
+
136
+ if (files.length === 0) {
137
+ console.error(`No test files found matching: ${defaultPattern.join(', ')}`)
138
+ process.exit(1)
139
+ }
140
+
141
+ const useBrowserUiEnv = process.env.ELASTICDASH_BROWSER_UI !== '0'
142
+ const useBrowserUiFlag = cmd?.browserUi !== false
143
+ const enableBrowserUi = useBrowserUiEnv && useBrowserUiFlag
144
+
145
+ const ui = enableBrowserUi
146
+ ? await startBrowserUiServer({ port: cmd?.browserUiPort, autoOpen: true })
147
+ : undefined
148
+
149
+ if (ui) {
150
+ ui.send({ type: 'run-start', payload: { files } })
151
+ }
152
+
153
+ const startedAt = Date.now()
154
+
155
+ const results = await runFiles(files, {
156
+ hooks: {
157
+ onTestStart(name) {
158
+ ui?.send({ type: 'test-start', payload: { name } })
159
+ },
160
+ onTestFinish(name, passed, durationMs, error) {
161
+ ui?.send({
162
+ type: 'test-finish',
163
+ payload: { name, passed, durationMs, errorMessage: stripAnsi(error?.message) },
164
+ })
165
+ },
166
+ },
167
+ })
168
+
169
+ // Log registered tests
170
+ const { getRegistry } = await import('./core/registry.js')
171
+ const registry = getRegistry()
172
+ console.log('[elasticdash] Tests registered:', registry.tests.map(t => t.name))
173
+ reportResults(results)
174
+
175
+ const anyFailed = results.some((fr) => fr.results.some((r) => !r.passed))
176
+
177
+ let uiDelayMs = 0
178
+ if (ui) {
179
+ const durationMs = Date.now() - startedAt
180
+ const failures: Array<{ name: string; errorMessage?: string }> = []
181
+ let totalTests = 0
182
+ let passedCount = 0
183
+ for (const fr of results) {
184
+ for (const r of fr.results) {
185
+ totalTests += 1
186
+ if (r.passed) passedCount += 1
187
+ else failures.push({ name: r.name, errorMessage: stripAnsi(r.error?.message) })
188
+ }
189
+ }
190
+ ui.send({
191
+ type: 'run-summary',
192
+ payload: {
193
+ passed: passedCount,
194
+ failed: failures.length,
195
+ total: totalTests,
196
+ durationMs,
197
+ failures,
198
+ },
199
+ })
200
+ uiDelayMs = 60000
201
+ }
202
+
203
+ if (uiDelayMs > 0) {
204
+ await new Promise((resolve) => setTimeout(resolve, uiDelayMs))
205
+ ui?.close()
206
+ }
207
+
208
+ process.exit(anyFailed ? 1 : 0)
209
+ })
210
+
211
+ // elasticdash run <file>
212
+ program
213
+ .command('run <file>')
214
+ .description('Run a single AI test file')
215
+ .option('--no-browser-ui', 'Disable browser progress UI')
216
+ .option('--browser-ui-port <port>', 'Port for browser UI', (v) => Number(v), undefined)
217
+ .action(async (file: string, cmd?: any) => {
218
+ const absFile = pathToFileURL(path.resolve(cwd, file)).href
219
+
220
+ const useBrowserUiEnv = process.env.ELASTICDASH_BROWSER_UI !== '0'
221
+ const useBrowserUiFlag = cmd?.browserUi !== false
222
+ const enableBrowserUi = useBrowserUiEnv && useBrowserUiFlag
223
+ const ui = enableBrowserUi
224
+ ? await startBrowserUiServer({ port: cmd?.browserUiPort, autoOpen: true })
225
+ : undefined
226
+
227
+ if (ui) {
228
+ ui.send({ type: 'run-start', payload: { files: [absFile] } })
229
+ }
230
+
231
+ const startedAt = Date.now()
232
+
233
+ const results = await runFiles([absFile], {
234
+ hooks: {
235
+ onTestStart(name) {
236
+ ui?.send({ type: 'test-start', payload: { name } })
237
+ },
238
+ onTestFinish(name, passed, durationMs, error) {
239
+ ui?.send({
240
+ type: 'test-finish',
241
+ payload: { name, passed, durationMs, errorMessage: stripAnsi(error?.message) },
242
+ })
243
+ },
244
+ },
245
+ })
246
+ reportResults(results)
247
+
248
+ const anyFailed = results.some((fr) => fr.results.some((r) => !r.passed))
249
+ let uiDelayMs = 0
250
+ if (ui) {
251
+ const durationMs = Date.now() - startedAt
252
+ const failures: Array<{ name: string; errorMessage?: string }> = []
253
+ let totalTests = 0
254
+ let passedCount = 0
255
+ for (const fr of results) {
256
+ for (const r of fr.results) {
257
+ totalTests += 1
258
+ if (r.passed) passedCount += 1
259
+ else failures.push({ name: r.name, errorMessage: stripAnsi(r.error?.message) })
260
+ }
261
+ }
262
+ ui.send({
263
+ type: 'run-summary',
264
+ payload: {
265
+ passed: passedCount,
266
+ failed: failures.length,
267
+ total: totalTests,
268
+ durationMs,
269
+ failures,
270
+ },
271
+ })
272
+ uiDelayMs = 60000
273
+ }
274
+
275
+ if (uiDelayMs > 0) {
276
+ await new Promise((resolve) => setTimeout(resolve, uiDelayMs))
277
+ ui?.close()
278
+ }
279
+
280
+ process.exit(anyFailed ? 1 : 0)
281
+ })
282
+
283
+ // elasticdash dashboard
284
+ program
285
+ .command('dashboard')
286
+ .description('Browse and search workflow functions')
287
+ .option('--port <port>', 'Dashboard server port', (v) => Number(v), process.env.ELASTICDASH_PORT ? Number(process.env.ELASTICDASH_PORT) : 4573)
288
+ .option('--no-open', 'Skip auto-opening browser')
289
+ .action(async (options: { port: number; open: boolean }) => {
290
+ console.log('[elasticdash] Starting dashboard server...')
291
+
292
+ const server = await startDashboardServer(cwd, {
293
+ port: options.port,
294
+ autoOpen: options.open,
295
+ })
296
+
297
+ console.log(`[elasticdash] Dashboard running at ${server.url}`)
298
+ console.log('[elasticdash] Press Ctrl+C to stop')
299
+
300
+ // Keep the process running with proper cleanup
301
+ let isShuttingDown = false
302
+
303
+ const cleanup = async () => {
304
+ if (isShuttingDown) {
305
+ // Force exit on second Ctrl+C
306
+ console.log('\n[elasticdash] Force exiting...')
307
+ process.exit(1)
308
+ }
309
+
310
+ isShuttingDown = true
311
+ console.log('\n[elasticdash] Shutting down dashboard server...')
312
+
313
+ try {
314
+ await Promise.race([
315
+ server.close(),
316
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 5000))
317
+ ])
318
+ process.exit(0)
319
+ } catch (error) {
320
+ console.error('[elasticdash] Error during shutdown:', error)
321
+ process.exit(1)
322
+ }
323
+ }
324
+
325
+ process.once('SIGINT', cleanup)
326
+ process.once('SIGTERM', cleanup)
327
+ })
328
+
329
+ // elasticdash observe
330
+ program
331
+ .command('observe')
332
+ .description('Start observability mode — stream trace events to ElasticDash backend')
333
+ .option('--server <url>', 'ElasticDash backend API URL', process.env.ELASTICDASH_API_URL)
334
+ .option('--api-key <key>', 'Project API key', process.env.ELASTICDASH_API_KEY)
335
+ .action(async (options: { server?: string; apiKey?: string }) => {
336
+ const serverUrl = options.server
337
+ if (!serverUrl) {
338
+ console.error('[elasticdash] Error: --server or ELASTICDASH_API_URL is required')
339
+ process.exit(1)
340
+ }
341
+
342
+ const handle = initObservability({
343
+ serverUrl,
344
+ apiKey: options.apiKey,
345
+ })
346
+
347
+ console.log(`[elasticdash] Observability active`)
348
+ console.log(` Session ID : ${handle.sessionId}`)
349
+ console.log(` Server : ${serverUrl}`)
350
+ console.log(`[elasticdash] Press Ctrl+C to stop`)
351
+
352
+ let isShuttingDown = false
353
+ const cleanup = async () => {
354
+ if (isShuttingDown) {
355
+ process.exit(1)
356
+ }
357
+ isShuttingDown = true
358
+ console.log('\n[elasticdash] Shutting down observability...')
359
+ await shutdownObservability()
360
+ process.exit(0)
361
+ }
362
+
363
+ process.once('SIGINT', cleanup)
364
+ process.once('SIGTERM', cleanup)
365
+ })
366
+
367
+ // elasticdash ed-test — Phase 3 fixture-based CI testing
368
+ program
369
+ .command('ed-test')
370
+ .description('Run ed_tests benchmarks against recorded fixtures')
371
+ .option('--cwd <path>', 'Root for test discovery')
372
+ .option('--no-upload', 'Skip uploading results to backend')
373
+ .option('--filter <pattern>', 'Only run tests matching glob pattern')
374
+ .option('--reporter <name>', 'Output format: default, json, junit', 'default')
375
+ .option('--fail-fast', 'Stop after first failing test')
376
+ .option('--runs <count>', 'Number of times to run each test (passes if any run succeeds)', (v) => Number(v), 1)
377
+ .action(async (options: {
378
+ cwd?: string
379
+ upload?: boolean
380
+ filter?: string
381
+ reporter?: string
382
+ failFast?: boolean
383
+ runs?: number
384
+ }) => {
385
+ // Auto-register tsx loader so .ts test files can be imported.
386
+ // tsx exposes a register() API for programmatic use.
387
+ try {
388
+ const { register } = await import('node:module')
389
+ register('tsx/esm', import.meta.url)
390
+ } catch {
391
+ // tsx or module.register not available — .ts files will fail with a clear error
392
+ }
393
+
394
+ const { runEdTests } = await import('./ci/ed-runner.js')
395
+ const { createReporter } = await import('./ci/reporters/index.js')
396
+ const { buildUploadPayload, uploadResults, persistFailedUpload } = await import('./ci/upload-client.js')
397
+
398
+ const reporter = createReporter((options.reporter as 'default' | 'json' | 'junit') || 'default')
399
+ const testCwd = options.cwd ? path.resolve(cwd, options.cwd) : cwd
400
+
401
+ let runResult
402
+ try {
403
+ runResult = await runEdTests({
404
+ cwd: testCwd,
405
+ filter: options.filter,
406
+ failFast: options.failFast,
407
+ noUpload: options.upload === false,
408
+ reporter: (options.reporter as 'default' | 'json' | 'junit') || 'default',
409
+ runs: options.runs,
410
+ })
411
+ } catch (err) {
412
+ console.error(`[elasticdash] Configuration error: ${err instanceof Error ? err.message : String(err)}`)
413
+ process.exit(3)
414
+ }
415
+
416
+ if (runResult.results.length === 0) {
417
+ console.error('[elasticdash] No tests found.')
418
+ process.exit(3)
419
+ }
420
+
421
+ // Report results
422
+ for (const r of runResult.results) {
423
+ reporter.onTestStart(r.testName)
424
+ reporter.onTestResult(r)
425
+ }
426
+
427
+ // Upload if enabled
428
+ let uploadUrl: string | undefined
429
+ const shouldUpload = options.upload !== false
430
+ const apiKey = process.env.ELASTICDASH_API_KEY
431
+ const serverUrl = process.env.ELASTICDASH_API_URL
432
+
433
+ if (shouldUpload && apiKey && serverUrl) {
434
+ const payload = buildUploadPayload(runResult)
435
+ try {
436
+ const response = await uploadResults(payload, { serverUrl, apiKey })
437
+ uploadUrl = `${serverUrl.replace(/\/+$/, '')}/runs/${response.runId}`
438
+ } catch (err) {
439
+ const errorMsg = err instanceof Error ? err.message : String(err)
440
+ console.warn(`[elasticdash] Upload failed: ${errorMsg}`)
441
+ await persistFailedUpload(buildUploadPayload(runResult), errorMsg, testCwd)
442
+
443
+ // Exit 4 only if tests themselves passed
444
+ const hasFailed = runResult.results.some(r => r.status === 'fail')
445
+ if (!hasFailed) {
446
+ reporter.onRunComplete(runResult)
447
+ process.exit(4)
448
+ }
449
+ }
450
+ } else if (shouldUpload && !apiKey) {
451
+ // In CI, warn about missing API key
452
+ const isCI = !!(process.env.CI || process.env.GITHUB_ACTIONS || process.env.GITLAB_CI || process.env.CIRCLECI || process.env.BUILDKITE)
453
+ if (isCI) {
454
+ console.warn('[elasticdash] Warning: ELASTICDASH_API_KEY not set — skipping upload')
455
+ }
456
+ }
457
+
458
+ reporter.onRunComplete(runResult, uploadUrl)
459
+
460
+ const hasFailed = runResult.results.some(r => r.status === 'fail')
461
+ process.exit(hasFailed ? 1 : 0)
462
+ })
463
+
464
+ // elasticdash ci
465
+ program
466
+ .command('ci')
467
+ .description('Run CI/CD tests — fetch test groups from backend, execute tests, submit results')
468
+ .option('--server <url>', 'ElasticDash backend API URL', process.env.ELASTICDASH_API_URL)
469
+ .option('--api-key <key>', 'Project API key', process.env.ELASTICDASH_API_KEY)
470
+ .option('--workflow <name>', 'Filter test groups by workflow name')
471
+ .option('--tags <tags>', 'Filter test groups by tags (comma-separated)')
472
+ .option('--triggered-by <source>', 'Trigger source', 'ci')
473
+ .option('--git-branch <branch>', 'Git branch name')
474
+ .option('--git-commit <sha>', 'Git commit SHA')
475
+ .option('--git-commit-message <msg>', 'Git commit message')
476
+ .option('--git-pr-number <number>', 'Pull request number', (v) => Number(v))
477
+ .option('--git-pr-url <url>', 'Pull request URL')
478
+ .action(async (options: {
479
+ server?: string
480
+ apiKey?: string
481
+ workflow?: string
482
+ tags?: string
483
+ triggeredBy?: string
484
+ gitBranch?: string
485
+ gitCommit?: string
486
+ gitCommitMessage?: string
487
+ gitPrNumber?: number
488
+ gitPrUrl?: string
489
+ }) => {
490
+ const serverUrl = options.server
491
+ if (!serverUrl) {
492
+ console.error('[elasticdash] Error: --server or ELASTICDASH_API_URL is required')
493
+ process.exit(1)
494
+ }
495
+
496
+ const apiKey = options.apiKey
497
+ if (!apiKey) {
498
+ console.error('[elasticdash] Error: --api-key or ELASTICDASH_API_KEY is required')
499
+ process.exit(1)
500
+ }
501
+
502
+ const { runCI } = await import('./ci/runner.js')
503
+ const summary = await runCI({
504
+ serverUrl,
505
+ apiKey,
506
+ workflowName: options.workflow,
507
+ tags: options.tags ? options.tags.split(',').map(s => s.trim()).filter(Boolean) : undefined,
508
+ triggeredBy: (options.triggeredBy as 'ci' | 'api') || 'ci',
509
+ gitBranch: options.gitBranch,
510
+ gitCommit: options.gitCommit,
511
+ gitCommitMessage: options.gitCommitMessage,
512
+ gitPrNumber: options.gitPrNumber,
513
+ gitPrUrl: options.gitPrUrl,
514
+ })
515
+
516
+ process.exit(summary.failed > 0 ? 1 : 0)
517
+ })
518
+
519
+ // elasticdash portal
520
+ program
521
+ .command('portal')
522
+ .description('Start a portal server to receive and execute rerun tasks from ElasticDash backend')
523
+ .option('--server <url>', 'ElasticDash backend API URL to POST results to', process.env.ELASTICDASH_API_URL)
524
+ .option('--api-key <key>', 'Project API key', process.env.ELASTICDASH_API_KEY)
525
+ .option('--port <port>', 'Portal server port', (v) => Number(v), process.env.ELASTICDASH_PORTAL_PORT ? Number(process.env.ELASTICDASH_PORTAL_PORT) : 4574)
526
+ .option('--allowed-origins <origins>', 'Comma-separated list of additional allowed origin domains', process.env.ELASTICDASH_ALLOWED_ORIGINS)
527
+ .action(async (options: { server?: string; apiKey?: string; port: number; allowedOrigins?: string }) => {
528
+ const backendUrl = options.server
529
+ if (!backendUrl) {
530
+ console.error('[elasticdash] Error: --server or ELASTICDASH_API_URL is required')
531
+ process.exit(1)
532
+ }
533
+
534
+ const allowedOrigins = options.allowedOrigins
535
+ ? options.allowedOrigins.split(',').map(s => s.trim()).filter(Boolean)
536
+ : undefined
537
+
538
+ const handle = await startPortalServer({
539
+ port: options.port,
540
+ backendUrl,
541
+ apiKey: options.apiKey,
542
+ cwd,
543
+ allowedOrigins,
544
+ })
545
+
546
+ console.log(`[elasticdash] Portal server running`)
547
+ console.log(` URL : ${handle.url}`)
548
+ console.log(` Backend : ${backendUrl}`)
549
+ console.log(` Port : ${handle.port}`)
550
+ console.log(`[elasticdash] Waiting for tasks from backend... Press Ctrl+C to stop`)
551
+
552
+ // Register with backend
553
+ try {
554
+ const headers: Record<string, string> = { 'Content-Type': 'application/json' }
555
+ if (options.apiKey) headers['Authorization'] = `Bearer ${options.apiKey}`
556
+ const res = await fetch(`${backendUrl}/api/portal/register`, {
557
+ method: 'POST',
558
+ headers,
559
+ body: JSON.stringify({ portalUrl: handle.url }),
560
+ })
561
+ if (res.ok) {
562
+ console.log(`[elasticdash] Registered with backend`)
563
+ } else {
564
+ console.warn(`[elasticdash] Backend registration returned ${res.status} — portal will still accept tasks directly`)
565
+ }
566
+ } catch {
567
+ console.warn(`[elasticdash] Could not register with backend — portal will still accept tasks directly`)
568
+ }
569
+
570
+ let isShuttingDown = false
571
+ const cleanup = async () => {
572
+ if (isShuttingDown) {
573
+ process.exit(1)
574
+ }
575
+ isShuttingDown = true
576
+ console.log('\n[elasticdash] Shutting down portal...')
577
+ await handle.close()
578
+ process.exit(0)
579
+ }
580
+
581
+ process.once('SIGINT', cleanup)
582
+ process.once('SIGTERM', cleanup)
583
+ })
584
+
585
+ // elasticdash run-tool <name>
586
+ program
587
+ .command('run-tool <name>')
588
+ .description('Run a single tool by name with given input. Used for rerun validation (e.g. ElasticDash MCP).')
589
+ .option('--input <json>', 'JSON input to pass to the tool')
590
+ .option('--input-file <path>', 'Path to JSON file with input')
591
+ .action(async (name: string, options: { input?: string; inputFile?: string }) => {
592
+ const toolsModulePath = resolveRuntimeModule(cwd, 'ed_tools')
593
+ if (!toolsModulePath) {
594
+ console.error(`[elasticdash] Error: Could not find ed_tools.ts or ed_tools.js in ${cwd}`)
595
+ process.exit(1)
596
+ }
597
+
598
+ let toolInput: unknown = undefined
599
+ try {
600
+ if (options.inputFile) {
601
+ toolInput = JSON.parse(readFileSync(path.resolve(cwd, options.inputFile), 'utf-8'))
602
+ } else if (options.input) {
603
+ toolInput = JSON.parse(options.input)
604
+ }
605
+ } catch (err) {
606
+ console.error(`[elasticdash] Error: Failed to parse input JSON: ${err instanceof Error ? err.message : String(err)}`)
607
+ process.exit(1)
608
+ }
609
+
610
+ // File-scan is used to compute a positional-arg signature when present,
611
+ // but a missing scan hit is not fatal: tools defined via edTool() live
612
+ // in the runtime registry and only resolve once the worker imports the
613
+ // module. The worker handles "not found" with a clear error.
614
+ const tools = scanTools(cwd)
615
+ const tool = tools.find(t => t.name === name)
616
+
617
+ const args = buildToolArgs(toolInput, tool)
618
+ const result = await runToolInSubprocess(toolsModulePath, name, args)
619
+
620
+ if (!result.ok) {
621
+ console.error(`[elasticdash] ${result.error ?? 'Tool execution failed'}`)
622
+ process.exit(1)
623
+ }
624
+
625
+ const payload = {
626
+ tool: name,
627
+ output: result.currentOutput,
628
+ duration_ms: result.currentDurationMs,
629
+ }
630
+ process.stdout.write(JSON.stringify(payload, null, 2) + '\n')
631
+ // Sentinel line so machine consumers (e.g. ElasticDash MCP) can extract the
632
+ // result envelope even when project code prints to stdout before us.
633
+ process.stdout.write('__ED_RUN_TOOL_RESULT__:' + JSON.stringify(payload) + '\n')
634
+ process.exit(0)
635
+ })
636
+
637
+ // elasticdash run-workflow <name>
638
+ program
639
+ .command('run-workflow <name>')
640
+ .description('Run a single workflow by name with given input. Used for trace-less rerun (e.g. ElasticDash MCP).')
641
+ .option('--input <json>', 'JSON input to pass to the workflow')
642
+ .option('--input-file <path>', 'Path to JSON file with input')
643
+ .option('--timeout-seconds <n>', 'Hard cap on a single run in seconds', (v) => Number(v), 300)
644
+ .action(async (name: string, options: { input?: string; inputFile?: string; timeoutSeconds: number }) => {
645
+ const workflowsModulePath = resolveRuntimeModule(cwd, 'ed_workflows')
646
+ if (!workflowsModulePath) {
647
+ console.error(`[elasticdash] Error: Could not find ed_workflows.ts or ed_workflows.js in ${cwd}`)
648
+ process.exit(1)
649
+ }
650
+
651
+ // tsx's in-process `register('tsx/esm', ...)` produces a data:text/javascript URL
652
+ // that newer Node versions reject as unresolvable. To load TS reliably, re-spawn
653
+ // ourselves once with `--import tsx` in NODE_OPTIONS so tsx is installed at
654
+ // process startup (the same approach `runToolInSubprocess` uses for ed_tools.ts).
655
+ const isTs = workflowsModulePath.endsWith('.ts') || workflowsModulePath.endsWith('.tsx')
656
+ const nodeOptions = process.env.NODE_OPTIONS ?? ''
657
+ const tsxAlreadyLoaded = nodeOptions.includes('tsx') || nodeOptions.includes('--import')
658
+ if (isTs && !tsxAlreadyLoaded) {
659
+ const { spawn } = await import('node:child_process')
660
+ const child = spawn(process.execPath, process.argv.slice(1), {
661
+ env: { ...process.env, NODE_OPTIONS: `${nodeOptions} --import tsx`.trim() },
662
+ cwd,
663
+ stdio: 'inherit',
664
+ })
665
+ const exitCode: number = await new Promise(resolve => {
666
+ child.on('exit', code => resolve(code ?? 0))
667
+ child.on('error', () => resolve(1))
668
+ })
669
+ process.exit(exitCode)
670
+ }
671
+
672
+ let workflowInput: unknown = undefined
673
+ try {
674
+ if (options.inputFile) {
675
+ workflowInput = JSON.parse(readFileSync(path.resolve(cwd, options.inputFile), 'utf-8'))
676
+ } else if (options.input) {
677
+ workflowInput = JSON.parse(options.input)
678
+ }
679
+ } catch (err) {
680
+ console.error(`[elasticdash] Error: Failed to parse input JSON: ${err instanceof Error ? err.message : String(err)}`)
681
+ process.exit(1)
682
+ }
683
+
684
+ let mod: Record<string, unknown>
685
+ try {
686
+ mod = await import(pathToFileURL(workflowsModulePath).href) as Record<string, unknown>
687
+ } catch (err) {
688
+ console.error(`[elasticdash] Error: Failed to import ${workflowsModulePath}: ${err instanceof Error ? err.message : String(err)}`)
689
+ process.exit(1)
690
+ }
691
+
692
+ const workflowFn = mod[name] as ((input?: unknown) => unknown) | undefined
693
+ if (typeof workflowFn !== 'function') {
694
+ console.error(`[elasticdash] Error: Workflow '${name}' not found or not a function in ${workflowsModulePath}`)
695
+ process.exit(1)
696
+ }
697
+
698
+ const { runWorkflow } = await import('./workflow-runner.js')
699
+
700
+ const startedAt = Date.now()
701
+ let runError: string | undefined
702
+ let timedOut = false
703
+ let traceId: string | null = null
704
+ let output: unknown = null
705
+ let events: Array<Record<string, unknown>> = []
706
+
707
+ try {
708
+ const runPromise = runWorkflow(async () => workflowFn(workflowInput))
709
+ const timeoutPromise = new Promise((_, reject) => {
710
+ setTimeout(() => {
711
+ timedOut = true
712
+ reject(new Error(`workflow '${name}' timed out after ${options.timeoutSeconds}s`))
713
+ }, options.timeoutSeconds * 1000)
714
+ })
715
+ const runResult = await Promise.race([runPromise, timeoutPromise]) as { result: unknown; trace: { traceId: string; events: Array<{ id: number; type: string; name: string; input: unknown; output: unknown; durationMs: number }> } }
716
+ output = runResult.result
717
+ traceId = runResult.trace?.traceId ?? null
718
+ events = (runResult.trace?.events ?? []).map((e) => {
719
+ const out = e.output as Record<string, unknown> | null | undefined
720
+ const hasError = out !== null && typeof out === 'object' && out !== null && 'error' in out
721
+ return {
722
+ id: e.id,
723
+ type: e.type,
724
+ name: e.name,
725
+ duration_ms: e.durationMs,
726
+ has_error: hasError,
727
+ input_preview: previewify(e.input),
728
+ output_preview: previewify(e.output),
729
+ }
730
+ })
731
+ } catch (err) {
732
+ runError = err instanceof Error ? err.message : String(err)
733
+ }
734
+ const endedAt = Date.now()
735
+
736
+ const envelope = {
737
+ workflow: name,
738
+ trace_id: traceId,
739
+ output,
740
+ duration_ms: endedAt - startedAt,
741
+ started_at: startedAt,
742
+ ended_at: endedAt,
743
+ events,
744
+ status: runError ? (timedOut ? 'timed_out' : 'failed') : 'completed',
745
+ ...(runError ? { error: runError } : {}),
746
+ }
747
+
748
+ process.stdout.write(JSON.stringify(envelope, null, 2) + '\n')
749
+ process.stdout.write('__ED_RUN_WORKFLOW_RESULT__:' + JSON.stringify(envelope) + '\n')
750
+ process.exit(runError ? 1 : 0)
751
+ })
752
+
753
+ // elasticdash init-guide
754
+ program
755
+ .command('init-guide')
756
+ .description('Copy the ElasticDash agent coding instructions into your project')
757
+ .option('--target <path>', 'Destination file path', 'AGENTS.md')
758
+ .option('--force', 'Overwrite the file instead of appending')
759
+ .action(async (cmd: { target: string; force?: boolean }) => {
760
+ const targetPath = path.resolve(cwd, cmd.target)
761
+
762
+ // Resolve docs from the SDK's own docs/ directory
763
+ const thisFile = fileURLToPath(import.meta.url)
764
+ const docsDir = path.resolve(path.dirname(thisFile), '..', 'docs')
765
+ const instructionsSrc = path.resolve(docsDir, 'agent-coding-instructions.md')
766
+
767
+ if (!existsSync(instructionsSrc)) {
768
+ console.error(`[elasticdash] Could not find agent coding instructions at ${instructionsSrc}`)
769
+ process.exit(1)
770
+ }
771
+
772
+ // Ensure target directory exists
773
+ const targetDir = path.dirname(targetPath)
774
+ if (!existsSync(targetDir)) {
775
+ mkdirSync(targetDir, { recursive: true })
776
+ }
777
+
778
+ const guideContent = readFileSync(instructionsSrc, 'utf-8')
779
+
780
+ if (existsSync(targetPath) && !cmd.force) {
781
+ // Check if instructions are already appended
782
+ const existing = readFileSync(targetPath, 'utf-8')
783
+ if (existing.includes('ElasticDash SDK — AI Coding Agent Instructions')) {
784
+ console.log(`[elasticdash] ${cmd.target} already contains the ElasticDash agent instructions. Use --force to replace the file.`)
785
+ process.exit(0)
786
+ }
787
+
788
+ // Append to existing file
789
+ appendFileSync(targetPath, '\n\n---\n\n' + guideContent)
790
+ console.log(`[elasticdash] ElasticDash agent instructions appended to ${cmd.target}`)
791
+ } else {
792
+ // Create new file or overwrite with --force
793
+ copyFileSync(instructionsSrc, targetPath)
794
+ console.log(`[elasticdash] Agent coding instructions written to ${cmd.target}`)
795
+ }
796
+
797
+ console.log()
798
+ console.log(` Tell your coding agent:`)
799
+ console.log()
800
+ console.log(` Read ${cmd.target} and follow it to integrate elasticdash-sdk into this project.`)
801
+ console.log()
802
+ process.exit(0)
803
+ })
804
+
805
+ await program.parseAsync(process.argv)
806
+ }
807
+
808
+ bootstrap().catch((err) => {
809
+ console.error(err)
810
+ process.exit(1)
811
+ })