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