@output.ai/cli 0.5.3 → 0.5.5
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/README.md +22 -5
- package/dist/api/generated/api.d.ts +86 -0
- package/dist/api/generated/api.js +17 -0
- package/dist/api/http_client.js +2 -1
- package/dist/assets/docker/docker-compose-dev.yml +14 -5
- package/dist/commands/dev/index.js +2 -6
- package/dist/commands/workflow/debug.d.ts +1 -0
- package/dist/commands/workflow/debug.js +9 -6
- package/dist/commands/workflow/debug.spec.js +0 -2
- package/dist/services/docker.js +5 -4
- package/dist/services/docker.spec.js +5 -0
- package/dist/services/messages.js +8 -8
- package/dist/services/trace_reader.d.ts +12 -10
- package/dist/services/trace_reader.js +36 -46
- package/dist/services/trace_reader.spec.js +57 -143
- package/dist/utils/env_loader.d.ts +0 -5
- package/dist/utils/env_loader.js +11 -35
- package/dist/utils/env_loader.spec.d.ts +1 -0
- package/dist/utils/env_loader.spec.js +63 -0
- package/dist/utils/error_handler.js +31 -7
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -11,14 +11,31 @@ Command-line interface for creating and running Output Framework workflows.
|
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
13
|
# Create a new project
|
|
14
|
-
output init
|
|
15
|
-
cd
|
|
14
|
+
npx @output.ai/cli init
|
|
15
|
+
cd <project-name>
|
|
16
16
|
|
|
17
17
|
# Start development services
|
|
18
|
-
output dev
|
|
18
|
+
npx output dev
|
|
19
19
|
|
|
20
20
|
# Run a workflow
|
|
21
|
-
output workflow run simple --input '{"question": "who is ada lovelace?"}'
|
|
21
|
+
npx output workflow run simple --input '{"question": "who is ada lovelace?"}'
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Environment Configuration
|
|
25
|
+
|
|
26
|
+
By default, the CLI loads environment variables from `.env` in the current directory.
|
|
27
|
+
|
|
28
|
+
To use a different env file, set the `OUTPUT_CLI_ENV` environment variable:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# Use production environment
|
|
32
|
+
OUTPUT_CLI_ENV=.env.prod npx output workflow list
|
|
33
|
+
|
|
34
|
+
# Use staging environment
|
|
35
|
+
OUTPUT_CLI_ENV=.env.staging npx output workflow run my-workflow
|
|
36
|
+
|
|
37
|
+
# Absolute path
|
|
38
|
+
OUTPUT_CLI_ENV=/etc/output/production.env npx output workflow status wf-123
|
|
22
39
|
```
|
|
23
40
|
|
|
24
41
|
## Command Reference
|
|
@@ -47,7 +64,7 @@ Running `output dev` starts:
|
|
|
47
64
|
|---------|-----|-------------|
|
|
48
65
|
| Temporal UI | http://localhost:8080 | Monitor and debug workflows |
|
|
49
66
|
| API Server | http://localhost:3001 | REST API for workflow execution |
|
|
50
|
-
| Worker |
|
|
67
|
+
| Worker | - | Processes workflows with auto-reload |
|
|
51
68
|
|
|
52
69
|
## Documentation
|
|
53
70
|
|
|
@@ -54,6 +54,60 @@ export interface TraceInfo {
|
|
|
54
54
|
/** File destinations for trace data */
|
|
55
55
|
destinations?: TraceInfoDestinations;
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* The workflow input
|
|
59
|
+
*/
|
|
60
|
+
export type TraceDataInput = {
|
|
61
|
+
[key: string]: unknown;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* The workflow output
|
|
65
|
+
*/
|
|
66
|
+
export type TraceDataOutput = {
|
|
67
|
+
[key: string]: unknown;
|
|
68
|
+
};
|
|
69
|
+
export type TraceDataStepsItem = {
|
|
70
|
+
[key: string]: unknown;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Trace data containing workflow execution details
|
|
74
|
+
*/
|
|
75
|
+
export interface TraceData {
|
|
76
|
+
/** The workflow execution ID */
|
|
77
|
+
workflowId?: string;
|
|
78
|
+
/** The workflow input */
|
|
79
|
+
input?: TraceDataInput;
|
|
80
|
+
/** The workflow output */
|
|
81
|
+
output?: TraceDataOutput;
|
|
82
|
+
/** The workflow execution steps */
|
|
83
|
+
steps?: TraceDataStepsItem[];
|
|
84
|
+
[key: string]: unknown;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Indicates trace was fetched from remote storage
|
|
88
|
+
*/
|
|
89
|
+
export type TraceLogRemoteResponseSource = typeof TraceLogRemoteResponseSource[keyof typeof TraceLogRemoteResponseSource];
|
|
90
|
+
export declare const TraceLogRemoteResponseSource: {
|
|
91
|
+
readonly remote: "remote";
|
|
92
|
+
};
|
|
93
|
+
export interface TraceLogRemoteResponse {
|
|
94
|
+
/** Indicates trace was fetched from remote storage */
|
|
95
|
+
source: TraceLogRemoteResponseSource;
|
|
96
|
+
data: TraceData;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Indicates trace is available locally
|
|
100
|
+
*/
|
|
101
|
+
export type TraceLogLocalResponseSource = typeof TraceLogLocalResponseSource[keyof typeof TraceLogLocalResponseSource];
|
|
102
|
+
export declare const TraceLogLocalResponseSource: {
|
|
103
|
+
readonly local: "local";
|
|
104
|
+
};
|
|
105
|
+
export interface TraceLogLocalResponse {
|
|
106
|
+
/** Indicates trace is available locally */
|
|
107
|
+
source: TraceLogLocalResponseSource;
|
|
108
|
+
/** Absolute path to local trace file */
|
|
109
|
+
localPath: string;
|
|
110
|
+
}
|
|
57
111
|
/**
|
|
58
112
|
* Current run status
|
|
59
113
|
*/
|
|
@@ -149,6 +203,13 @@ export type GetWorkflowIdResult200 = {
|
|
|
149
203
|
output?: unknown;
|
|
150
204
|
trace?: TraceInfo;
|
|
151
205
|
};
|
|
206
|
+
export type GetWorkflowIdTraceLog200 = TraceLogRemoteResponse | TraceLogLocalResponse;
|
|
207
|
+
export type GetWorkflowIdTraceLog404 = {
|
|
208
|
+
error?: string;
|
|
209
|
+
};
|
|
210
|
+
export type GetWorkflowIdTraceLog500 = {
|
|
211
|
+
error?: string;
|
|
212
|
+
};
|
|
152
213
|
export type GetWorkflowCatalogId200 = {
|
|
153
214
|
/** Each workflow available in this catalog */
|
|
154
215
|
workflows?: Workflow[];
|
|
@@ -273,6 +334,31 @@ export type getWorkflowIdResultResponseError = (getWorkflowIdResultResponse404)
|
|
|
273
334
|
export type getWorkflowIdResultResponse = (getWorkflowIdResultResponseSuccess | getWorkflowIdResultResponseError);
|
|
274
335
|
export declare const getGetWorkflowIdResultUrl: (id: string) => string;
|
|
275
336
|
export declare const getWorkflowIdResult: (id: string, options?: ApiRequestOptions) => Promise<getWorkflowIdResultResponse>;
|
|
337
|
+
/**
|
|
338
|
+
* Returns trace data for a completed workflow. If trace is stored remotely (S3), fetches and returns the data inline. If trace is local only, returns the local path.
|
|
339
|
+
* @summary Get workflow trace log data
|
|
340
|
+
*/
|
|
341
|
+
export type getWorkflowIdTraceLogResponse200 = {
|
|
342
|
+
data: GetWorkflowIdTraceLog200;
|
|
343
|
+
status: 200;
|
|
344
|
+
};
|
|
345
|
+
export type getWorkflowIdTraceLogResponse404 = {
|
|
346
|
+
data: GetWorkflowIdTraceLog404;
|
|
347
|
+
status: 404;
|
|
348
|
+
};
|
|
349
|
+
export type getWorkflowIdTraceLogResponse500 = {
|
|
350
|
+
data: GetWorkflowIdTraceLog500;
|
|
351
|
+
status: 500;
|
|
352
|
+
};
|
|
353
|
+
export type getWorkflowIdTraceLogResponseSuccess = (getWorkflowIdTraceLogResponse200) & {
|
|
354
|
+
headers: Headers;
|
|
355
|
+
};
|
|
356
|
+
export type getWorkflowIdTraceLogResponseError = (getWorkflowIdTraceLogResponse404 | getWorkflowIdTraceLogResponse500) & {
|
|
357
|
+
headers: Headers;
|
|
358
|
+
};
|
|
359
|
+
export type getWorkflowIdTraceLogResponse = (getWorkflowIdTraceLogResponseSuccess | getWorkflowIdTraceLogResponseError);
|
|
360
|
+
export declare const getGetWorkflowIdTraceLogUrl: (id: string) => string;
|
|
361
|
+
export declare const getWorkflowIdTraceLog: (id: string, options?: ApiRequestOptions) => Promise<getWorkflowIdTraceLogResponse>;
|
|
276
362
|
/**
|
|
277
363
|
* @summary Get a specific workflow catalog by ID
|
|
278
364
|
*/
|
|
@@ -7,6 +7,14 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { customFetchInstance } from '../http_client.js';
|
|
9
9
|
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
|
10
|
+
export const TraceLogRemoteResponseSource = {
|
|
11
|
+
remote: 'remote',
|
|
12
|
+
};
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
|
14
|
+
export const TraceLogLocalResponseSource = {
|
|
15
|
+
local: 'local',
|
|
16
|
+
};
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
|
10
18
|
export const WorkflowRunInfoStatus = {
|
|
11
19
|
running: 'running',
|
|
12
20
|
completed: 'completed',
|
|
@@ -88,6 +96,15 @@ export const getWorkflowIdResult = async (id, options) => {
|
|
|
88
96
|
method: 'GET'
|
|
89
97
|
});
|
|
90
98
|
};
|
|
99
|
+
export const getGetWorkflowIdTraceLogUrl = (id) => {
|
|
100
|
+
return `/workflow/${id}/trace-log`;
|
|
101
|
+
};
|
|
102
|
+
export const getWorkflowIdTraceLog = async (id, options) => {
|
|
103
|
+
return customFetchInstance(getGetWorkflowIdTraceLogUrl(id), {
|
|
104
|
+
...options,
|
|
105
|
+
method: 'GET'
|
|
106
|
+
});
|
|
107
|
+
};
|
|
91
108
|
;
|
|
92
109
|
export const getGetWorkflowCatalogIdUrl = (id) => {
|
|
93
110
|
return `/workflow/catalog/${id}`;
|
package/dist/api/http_client.js
CHANGED
|
@@ -9,8 +9,9 @@ const api = ky.create({
|
|
|
9
9
|
retry: {
|
|
10
10
|
limit: 2,
|
|
11
11
|
methods: ['get', 'put', 'head', 'delete', 'options', 'trace'],
|
|
12
|
-
statusCodes: [408, 413, 429,
|
|
12
|
+
statusCodes: [408, 413, 429, 502, 503, 504]
|
|
13
13
|
},
|
|
14
|
+
throwHttpErrors: false,
|
|
14
15
|
hooks: {
|
|
15
16
|
beforeRequest: [
|
|
16
17
|
request => {
|
|
@@ -82,11 +82,15 @@ services:
|
|
|
82
82
|
image: growthxteam/output-api:latest
|
|
83
83
|
networks:
|
|
84
84
|
- main
|
|
85
|
+
env_file: ./.env
|
|
85
86
|
environment:
|
|
86
87
|
- PORT=3001
|
|
87
|
-
- CATALOG_ID
|
|
88
|
+
- CATALOG_ID=${CATALOG_ID:-main}
|
|
88
89
|
- TEMPORAL_ADDRESS=temporal:7233
|
|
89
90
|
- NODE_ENV=development
|
|
91
|
+
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-}
|
|
92
|
+
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-}
|
|
93
|
+
- AWS_REGION=${AWS_REGION:-us-west-1}
|
|
90
94
|
ports:
|
|
91
95
|
- '3001:3001'
|
|
92
96
|
|
|
@@ -97,14 +101,19 @@ services:
|
|
|
97
101
|
image: node:24.3-slim
|
|
98
102
|
networks:
|
|
99
103
|
- main
|
|
100
|
-
env_file:
|
|
104
|
+
env_file: ./.env
|
|
101
105
|
environment:
|
|
102
|
-
- CATALOG_ID
|
|
103
|
-
- LOG_HTTP_VERBOSE
|
|
106
|
+
- CATALOG_ID=${CATALOG_ID:-main}
|
|
107
|
+
- LOG_HTTP_VERBOSE=${LOG_HTTP_VERBOSE:-true}
|
|
104
108
|
- REDIS_URL=redis://redis:6379
|
|
105
109
|
- TEMPORAL_ADDRESS=temporal:7233
|
|
106
|
-
- TRACE_LOCAL_ON
|
|
110
|
+
- TRACE_LOCAL_ON=${TRACE_LOCAL_ON:-true}
|
|
111
|
+
- TRACE_REMOTE_ON=${TRACE_REMOTE_ON:-}
|
|
112
|
+
- TRACE_REMOTE_S3_BUCKET=${TRACE_REMOTE_S3_BUCKET:-}
|
|
107
113
|
- HOST_TRACE_PATH=${PWD}/logs
|
|
114
|
+
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-}
|
|
115
|
+
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-}
|
|
116
|
+
- AWS_REGION=${AWS_REGION:-us-west-1}
|
|
108
117
|
command: >
|
|
109
118
|
sh -c "
|
|
110
119
|
npm run output:worker:install &&
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command, Flags } from '@oclif/core';
|
|
2
2
|
import fs from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import logUpdate from 'log-update';
|
|
4
5
|
import { validateDockerEnvironment, startDockerCompose, stopDockerCompose, getServiceStatus, DockerComposeConfigNotFoundError, getDefaultDockerComposePath, SERVICE_HEALTH, SERVICE_STATE } from '#services/docker.js';
|
|
5
6
|
import { getErrorMessage } from '#utils/error_utils.js';
|
|
6
7
|
import { getDevSuccessMessage } from '#services/messages.js';
|
|
@@ -109,7 +110,6 @@ export default class Dev extends Command {
|
|
|
109
110
|
}
|
|
110
111
|
}
|
|
111
112
|
async pollServiceStatus(dockerComposePath) {
|
|
112
|
-
const state = { lastLineCount: 0 };
|
|
113
113
|
const outputServiceStatus = async () => {
|
|
114
114
|
try {
|
|
115
115
|
const services = await getServiceStatus(dockerComposePath);
|
|
@@ -122,11 +122,7 @@ export default class Dev extends Command {
|
|
|
122
122
|
'',
|
|
123
123
|
`${ANSI.DIM}Press Ctrl+C to stop services${ANSI.RESET}`
|
|
124
124
|
];
|
|
125
|
-
|
|
126
|
-
process.stdout.write(`\x1b[${state.lastLineCount}A\x1b[J`);
|
|
127
|
-
}
|
|
128
|
-
process.stdout.write(lines.join('\n') + '\n');
|
|
129
|
-
state.lastLineCount = lines.length;
|
|
125
|
+
logUpdate(lines.join('\n'));
|
|
130
126
|
}
|
|
131
127
|
catch {
|
|
132
128
|
// silent retry on next poll
|
|
@@ -9,6 +9,7 @@ export default class WorkflowDebug extends Command {
|
|
|
9
9
|
format: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
10
10
|
};
|
|
11
11
|
run(): Promise<void>;
|
|
12
|
+
private conditionalLog;
|
|
12
13
|
private outputJson;
|
|
13
14
|
private displayTextTrace;
|
|
14
15
|
catch(error: Error): Promise<void>;
|
|
@@ -27,18 +27,21 @@ export default class WorkflowDebug extends Command {
|
|
|
27
27
|
async run() {
|
|
28
28
|
const { args, flags } = await this.parse(WorkflowDebug);
|
|
29
29
|
const isJsonFormat = flags.format === OUTPUT_FORMAT.JSON;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
// Output based on format
|
|
30
|
+
this.conditionalLog(`Fetching debug information for workflow: ${args.workflowId}...`, isJsonFormat);
|
|
31
|
+
const { data: traceData, location } = await getTrace(args.workflowId);
|
|
32
|
+
const source = location.isRemote ? 'remote' : 'local';
|
|
33
|
+
this.conditionalLog(`Trace source: ${source}${!location.isRemote ? ` (${location.path})` : ''}`, isJsonFormat);
|
|
35
34
|
if (isJsonFormat) {
|
|
36
35
|
this.outputJson(traceData);
|
|
37
36
|
return;
|
|
38
37
|
}
|
|
39
|
-
// Display text format
|
|
40
38
|
this.displayTextTrace(traceData);
|
|
41
39
|
}
|
|
40
|
+
conditionalLog(message, disabled) {
|
|
41
|
+
if (!disabled) {
|
|
42
|
+
this.log(message);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
42
45
|
outputJson(data) {
|
|
43
46
|
this.log(JSON.stringify(data, null, 2));
|
|
44
47
|
}
|
package/dist/services/docker.js
CHANGED
|
@@ -2,6 +2,7 @@ import { execFileSync, execSync, spawn } from 'node:child_process';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { ux } from '@oclif/core';
|
|
5
|
+
import logUpdate from 'log-update';
|
|
5
6
|
const DEFAULT_COMPOSE_PATH = '../assets/docker/docker-compose-dev.yml';
|
|
6
7
|
export const SERVICE_HEALTH = {
|
|
7
8
|
HEALTHY: 'healthy',
|
|
@@ -108,16 +109,16 @@ export async function waitForServicesHealthy(dockerComposePath, timeoutMs = 1200
|
|
|
108
109
|
const services = await getServiceStatus(dockerComposePath);
|
|
109
110
|
const allHealthy = services.every(s => s.health === SERVICE_HEALTH.HEALTHY || s.health === SERVICE_HEALTH.NONE);
|
|
110
111
|
if (services.length > 0) {
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
ux.stdout('⏳ Waiting for services to become healthy...\n');
|
|
114
|
-
ux.stdout(formatServiceStatus(services) + '\n');
|
|
112
|
+
const statusLines = formatServiceStatus(services);
|
|
113
|
+
logUpdate(`⏳ Waiting for services to become healthy...\n${statusLines}`);
|
|
115
114
|
}
|
|
116
115
|
if (allHealthy && services.length > 0) {
|
|
116
|
+
logUpdate.done();
|
|
117
117
|
return;
|
|
118
118
|
}
|
|
119
119
|
await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
|
|
120
120
|
}
|
|
121
|
+
logUpdate.done();
|
|
121
122
|
throw new Error('Timeout waiting for services to become healthy');
|
|
122
123
|
}
|
|
123
124
|
export async function startDockerCompose(dockerComposePath, enableWatch = false) {
|
|
@@ -6,6 +6,11 @@ vi.mock('node:child_process', () => ({
|
|
|
6
6
|
execFileSync: vi.fn(),
|
|
7
7
|
spawn: vi.fn()
|
|
8
8
|
}));
|
|
9
|
+
vi.mock('log-update', () => {
|
|
10
|
+
const fn = vi.fn();
|
|
11
|
+
fn.done = vi.fn();
|
|
12
|
+
return { default: fn };
|
|
13
|
+
});
|
|
9
14
|
describe('docker service', () => {
|
|
10
15
|
beforeEach(() => {
|
|
11
16
|
vi.clearAllMocks();
|
|
@@ -173,11 +173,11 @@ export const getProjectSuccessMessage = (folderName, installSuccess, envConfigur
|
|
|
173
173
|
}
|
|
174
174
|
steps.push({
|
|
175
175
|
step: 'Start development services',
|
|
176
|
-
command: 'output dev',
|
|
176
|
+
command: 'npx output dev',
|
|
177
177
|
note: 'Launches Temporal, Redis, PostgreSQL, API, Worker, and UI'
|
|
178
178
|
}, {
|
|
179
179
|
step: 'Run example workflow',
|
|
180
|
-
command: 'output workflow run simple --input src/simple/scenarios/question_ada_lovelace.json',
|
|
180
|
+
command: 'npx output workflow run simple --input src/simple/scenarios/question_ada_lovelace.json',
|
|
181
181
|
note: 'Execute in a new terminal after services are running'
|
|
182
182
|
}, {
|
|
183
183
|
step: 'Monitor workflows',
|
|
@@ -219,14 +219,14 @@ ${divider}
|
|
|
219
219
|
|
|
220
220
|
${createSectionHeader('QUICK START COMMANDS', '⚡')}
|
|
221
221
|
|
|
222
|
-
${bulletPoint} ${ux.colorize('white', 'Plan a workflow:')} ${formatCommand('output workflow plan')}
|
|
223
|
-
${bulletPoint} ${ux.colorize('white', 'Generate from plan:')} ${formatCommand('output workflow generate')}
|
|
224
|
-
${bulletPoint} ${ux.colorize('white', 'List workflows:')} ${formatCommand('output workflow list')}
|
|
225
|
-
${bulletPoint} ${ux.colorize('white', 'View help:')} ${formatCommand('output --help')}
|
|
222
|
+
${bulletPoint} ${ux.colorize('white', 'Plan a workflow:')} ${formatCommand('npx output workflow plan')}
|
|
223
|
+
${bulletPoint} ${ux.colorize('white', 'Generate from plan:')} ${formatCommand('npx output workflow generate')}
|
|
224
|
+
${bulletPoint} ${ux.colorize('white', 'List workflows:')} ${formatCommand('npx output workflow list')}
|
|
225
|
+
${bulletPoint} ${ux.colorize('white', 'View help:')} ${formatCommand('npx output --help')}
|
|
226
226
|
|
|
227
227
|
${divider}
|
|
228
228
|
|
|
229
|
-
${ux.colorize('dim', '💡 Tip: Use ')}${formatCommand('output workflow plan')}${ux.colorize('dim', ' to design your first custom workflow')}
|
|
229
|
+
${ux.colorize('dim', '💡 Tip: Use ')}${formatCommand('npx output workflow plan')}${ux.colorize('dim', ' to design your first custom workflow')}
|
|
230
230
|
${ux.colorize('dim', ' with AI assistance.')}
|
|
231
231
|
|
|
232
232
|
${ux.colorize('green', ux.colorize('bold', 'Happy building with Output SDK! 🚀'))}
|
|
@@ -321,7 +321,7 @@ ${createSectionHeader('RUN A WORKFLOW', '🚀')}
|
|
|
321
321
|
|
|
322
322
|
${ux.colorize('white', 'In a new terminal, execute:')}
|
|
323
323
|
|
|
324
|
-
${formatCommand('output workflow run simple --input \'{"question": "Hello!"}\'')}
|
|
324
|
+
${formatCommand('npx output workflow run simple --input \'{"question": "Hello!"}\'')}
|
|
325
325
|
|
|
326
326
|
${divider}
|
|
327
327
|
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import type { TraceData } from '#types/trace.js';
|
|
2
2
|
export type { TraceData };
|
|
3
|
+
export interface TraceLocation {
|
|
4
|
+
path: string;
|
|
5
|
+
isRemote: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface TraceResult {
|
|
8
|
+
data: TraceData;
|
|
9
|
+
location: TraceLocation;
|
|
10
|
+
}
|
|
3
11
|
/**
|
|
4
|
-
*
|
|
12
|
+
* Get trace data from workflow ID using the API
|
|
13
|
+
* The API handles S3 fetching - CLI only needs to read local files when necessary
|
|
14
|
+
* @returns Both the trace data and the location it was fetched from
|
|
5
15
|
*/
|
|
6
|
-
export declare function
|
|
7
|
-
/**
|
|
8
|
-
* Read and parse trace file
|
|
9
|
-
*/
|
|
10
|
-
export declare function readTraceFile(path: string): Promise<TraceData>;
|
|
11
|
-
/**
|
|
12
|
-
* Get trace data from workflow ID
|
|
13
|
-
*/
|
|
14
|
-
export declare function getTrace(workflowId: string): Promise<TraceData>;
|
|
16
|
+
export declare function getTrace(workflowId: string): Promise<TraceResult>;
|
|
@@ -1,49 +1,10 @@
|
|
|
1
|
-
import { readFile
|
|
2
|
-
import {
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { getWorkflowIdTraceLog } from '#api/generated/api.js';
|
|
3
3
|
import { getErrorCode } from '#utils/error_utils.js';
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Read and parse trace file from local path
|
|
6
6
|
*/
|
|
7
|
-
async function
|
|
8
|
-
try {
|
|
9
|
-
await stat(path);
|
|
10
|
-
return { exists: true };
|
|
11
|
-
}
|
|
12
|
-
catch (error) {
|
|
13
|
-
const code = getErrorCode(error);
|
|
14
|
-
if (code === 'ENOENT') {
|
|
15
|
-
return { exists: false };
|
|
16
|
-
}
|
|
17
|
-
if (code === 'EACCES') {
|
|
18
|
-
return { exists: false, error: `Permission denied: ${path}` };
|
|
19
|
-
}
|
|
20
|
-
return { exists: false, error: `Cannot access file: ${path}` };
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Find trace file from workflow metadata
|
|
25
|
-
*/
|
|
26
|
-
export async function findTraceFile(workflowId) {
|
|
27
|
-
const response = await getWorkflowIdResult(workflowId);
|
|
28
|
-
// Check if we got a successful response
|
|
29
|
-
if (response.status !== 200) {
|
|
30
|
-
throw new Error(`Failed to get workflow result for ${workflowId}`);
|
|
31
|
-
}
|
|
32
|
-
const tracePath = response.data.trace?.destinations?.local;
|
|
33
|
-
if (!tracePath) {
|
|
34
|
-
throw new Error(`No trace file path found for workflow ${workflowId}`);
|
|
35
|
-
}
|
|
36
|
-
const fileCheck = await fileExists(tracePath);
|
|
37
|
-
if (!fileCheck.exists) {
|
|
38
|
-
const errorDetail = fileCheck.error || `Trace file not found at path: ${tracePath}`;
|
|
39
|
-
throw new Error(errorDetail);
|
|
40
|
-
}
|
|
41
|
-
return tracePath;
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Read and parse trace file
|
|
45
|
-
*/
|
|
46
|
-
export async function readTraceFile(path) {
|
|
7
|
+
async function readLocalTraceFile(path) {
|
|
47
8
|
try {
|
|
48
9
|
const content = await readFile(path, 'utf-8');
|
|
49
10
|
return JSON.parse(content);
|
|
@@ -59,9 +20,38 @@ export async function readTraceFile(path) {
|
|
|
59
20
|
}
|
|
60
21
|
}
|
|
61
22
|
/**
|
|
62
|
-
* Get trace data from workflow ID
|
|
23
|
+
* Get trace data from workflow ID using the API
|
|
24
|
+
* The API handles S3 fetching - CLI only needs to read local files when necessary
|
|
25
|
+
* @returns Both the trace data and the location it was fetched from
|
|
63
26
|
*/
|
|
64
27
|
export async function getTrace(workflowId) {
|
|
65
|
-
const
|
|
66
|
-
|
|
28
|
+
const response = await getWorkflowIdTraceLog(workflowId);
|
|
29
|
+
if (response.status === 404) {
|
|
30
|
+
throw new Error(`Workflow not found or no trace available: ${workflowId}`);
|
|
31
|
+
}
|
|
32
|
+
if (response.status === 500) {
|
|
33
|
+
const errorData = response.data;
|
|
34
|
+
const errorMessage = errorData?.error || 'Failed to fetch trace from API';
|
|
35
|
+
throw new Error(`API error (500): ${errorMessage}`);
|
|
36
|
+
}
|
|
37
|
+
if (response.status !== 200) {
|
|
38
|
+
const errorResponse = response;
|
|
39
|
+
throw new Error(`Unexpected API response status: ${errorResponse.status}`);
|
|
40
|
+
}
|
|
41
|
+
const data = response.data;
|
|
42
|
+
if (data.source === 'remote') {
|
|
43
|
+
return {
|
|
44
|
+
data: data.data,
|
|
45
|
+
location: { path: 'remote', isRemote: true }
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (data.source === 'local') {
|
|
49
|
+
const localPath = data.localPath;
|
|
50
|
+
const traceData = await readLocalTraceFile(localPath);
|
|
51
|
+
return {
|
|
52
|
+
data: traceData,
|
|
53
|
+
location: { path: localPath, isRemote: false }
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
throw new Error('Invalid trace log response format');
|
|
67
57
|
}
|
|
@@ -1,164 +1,78 @@
|
|
|
1
|
-
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
readFile: vi.fn(),
|
|
6
|
-
stat: vi.fn()
|
|
1
|
+
import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
|
|
2
|
+
const mockGetWorkflowIdTraceLog = vi.fn();
|
|
3
|
+
vi.mock('#api/generated/api.js', () => ({
|
|
4
|
+
getWorkflowIdTraceLog: (...args) => mockGetWorkflowIdTraceLog(...args)
|
|
7
5
|
}));
|
|
8
|
-
|
|
9
|
-
vi.mock('
|
|
10
|
-
|
|
6
|
+
const mockReadFile = vi.fn();
|
|
7
|
+
vi.mock('node:fs/promises', () => ({
|
|
8
|
+
readFile: (...args) => mockReadFile(...args)
|
|
11
9
|
}));
|
|
12
|
-
describe('
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
18
|
-
mockReadFile: fsModule.readFile,
|
|
19
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
|
-
mockStat: fsModule.stat,
|
|
21
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
22
|
-
mockGetWorkflowIdResult: apiModule.getWorkflowIdResult
|
|
23
|
-
};
|
|
24
|
-
};
|
|
10
|
+
describe('trace_reader', () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
mockGetWorkflowIdTraceLog.mockReset();
|
|
13
|
+
mockReadFile.mockReset();
|
|
14
|
+
});
|
|
25
15
|
afterEach(() => {
|
|
26
16
|
vi.clearAllMocks();
|
|
27
17
|
});
|
|
28
|
-
describe('
|
|
29
|
-
it('should
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
data: {
|
|
36
|
-
workflowId,
|
|
37
|
-
output: { result: 'test result' },
|
|
38
|
-
trace: {
|
|
39
|
-
destinations: {
|
|
40
|
-
local: expectedPath,
|
|
41
|
-
remote: null
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
mockStat.mockResolvedValue({ isFile: () => true });
|
|
47
|
-
const result = await findTraceFile(workflowId);
|
|
48
|
-
expect(result).toBe(expectedPath);
|
|
49
|
-
expect(mockGetWorkflowIdResult).toHaveBeenCalledWith(workflowId);
|
|
50
|
-
expect(mockStat).toHaveBeenCalledWith(expectedPath);
|
|
51
|
-
});
|
|
52
|
-
it('should throw error when no trace path in metadata', async () => {
|
|
53
|
-
const { mockGetWorkflowIdResult } = await getMocks();
|
|
54
|
-
const workflowId = 'test-workflow-456';
|
|
55
|
-
mockGetWorkflowIdResult.mockResolvedValue({
|
|
56
|
-
status: 200,
|
|
57
|
-
data: {
|
|
58
|
-
workflowId,
|
|
59
|
-
output: { result: 'test result' },
|
|
60
|
-
trace: {
|
|
61
|
-
destinations: {
|
|
62
|
-
local: null,
|
|
63
|
-
remote: null
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
await expect(findTraceFile(workflowId))
|
|
69
|
-
.rejects.toThrow(`No trace file path found for workflow ${workflowId}`);
|
|
70
|
-
});
|
|
71
|
-
it('should throw error when trace file not on disk', async () => {
|
|
72
|
-
const { mockGetWorkflowIdResult, mockStat } = await getMocks();
|
|
73
|
-
const workflowId = 'test-workflow-789';
|
|
74
|
-
const expectedPath = '/app/logs/runs/test/2024-01-01_test-workflow-789.json';
|
|
75
|
-
mockGetWorkflowIdResult.mockResolvedValue({
|
|
18
|
+
describe('getTrace', () => {
|
|
19
|
+
it('should return trace data directly for remote source', async () => {
|
|
20
|
+
const mockTraceData = {
|
|
21
|
+
root: { workflowName: 'test', workflowId: 'wf-123', startTime: Date.now() },
|
|
22
|
+
children: []
|
|
23
|
+
};
|
|
24
|
+
mockGetWorkflowIdTraceLog.mockResolvedValue({
|
|
76
25
|
status: 200,
|
|
77
26
|
data: {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
trace: {
|
|
81
|
-
destinations: {
|
|
82
|
-
local: expectedPath,
|
|
83
|
-
remote: null
|
|
84
|
-
}
|
|
85
|
-
}
|
|
27
|
+
source: 'remote',
|
|
28
|
+
data: mockTraceData
|
|
86
29
|
}
|
|
87
30
|
});
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
31
|
+
const { getTrace } = await import('./trace_reader.js');
|
|
32
|
+
const result = await getTrace('wf-123');
|
|
33
|
+
expect(result.data).toEqual(mockTraceData);
|
|
34
|
+
expect(result.location.isRemote).toBe(true);
|
|
35
|
+
expect(mockReadFile).not.toHaveBeenCalled();
|
|
92
36
|
});
|
|
93
|
-
it('should
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
});
|
|
100
|
-
it('should handle missing trace property gracefully', async () => {
|
|
101
|
-
const { mockGetWorkflowIdResult } = await getMocks();
|
|
102
|
-
const workflowId = 'test-workflow-no-trace';
|
|
103
|
-
mockGetWorkflowIdResult.mockResolvedValue({
|
|
37
|
+
it('should read local file for local source', async () => {
|
|
38
|
+
const mockTraceData = {
|
|
39
|
+
root: { workflowName: 'test', workflowId: 'wf-123', startTime: Date.now() },
|
|
40
|
+
children: []
|
|
41
|
+
};
|
|
42
|
+
mockGetWorkflowIdTraceLog.mockResolvedValue({
|
|
104
43
|
status: 200,
|
|
105
44
|
data: {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
// No trace property at all
|
|
45
|
+
source: 'local',
|
|
46
|
+
localPath: '/path/to/trace.json'
|
|
109
47
|
}
|
|
110
48
|
});
|
|
111
|
-
|
|
112
|
-
|
|
49
|
+
mockReadFile.mockResolvedValue(JSON.stringify(mockTraceData));
|
|
50
|
+
const { getTrace } = await import('./trace_reader.js');
|
|
51
|
+
const result = await getTrace('wf-123');
|
|
52
|
+
expect(result.data).toEqual(mockTraceData);
|
|
53
|
+
expect(result.location.isRemote).toBe(false);
|
|
54
|
+
expect(result.location.path).toBe('/path/to/trace.json');
|
|
55
|
+
expect(mockReadFile).toHaveBeenCalledWith('/path/to/trace.json', 'utf-8');
|
|
113
56
|
});
|
|
114
|
-
it('should throw error when
|
|
115
|
-
|
|
116
|
-
const workflowId = 'non-existent-workflow';
|
|
117
|
-
mockGetWorkflowIdResult.mockResolvedValue({
|
|
57
|
+
it('should throw error when API returns 404', async () => {
|
|
58
|
+
mockGetWorkflowIdTraceLog.mockResolvedValue({
|
|
118
59
|
status: 404,
|
|
119
|
-
data:
|
|
60
|
+
data: { error: 'Not found' }
|
|
120
61
|
});
|
|
121
|
-
await
|
|
122
|
-
|
|
62
|
+
const { getTrace } = await import('./trace_reader.js');
|
|
63
|
+
await expect(getTrace('wf-123'))
|
|
64
|
+
.rejects
|
|
65
|
+
.toThrow('Workflow not found or no trace available: wf-123');
|
|
123
66
|
});
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
mockReadFile.mockResolvedValue(JSON.stringify(traceData));
|
|
134
|
-
const result = await readTraceFile(path);
|
|
135
|
-
expect(result).toEqual(traceData);
|
|
136
|
-
expect(mockReadFile).toHaveBeenCalledWith(path, 'utf-8');
|
|
137
|
-
});
|
|
138
|
-
it('should throw error for non-existent file', async () => {
|
|
139
|
-
const { mockReadFile } = await getMocks();
|
|
140
|
-
const path = '/logs/missing.json';
|
|
141
|
-
const error = new Error('ENOENT');
|
|
142
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
143
|
-
error.code = 'ENOENT';
|
|
144
|
-
mockReadFile.mockRejectedValue(error);
|
|
145
|
-
await expect(readTraceFile(path))
|
|
146
|
-
.rejects.toThrow(`Trace file not found at path: ${path}`);
|
|
147
|
-
});
|
|
148
|
-
it('should throw error for invalid JSON', async () => {
|
|
149
|
-
const { mockReadFile } = await getMocks();
|
|
150
|
-
const path = '/logs/invalid.json';
|
|
151
|
-
mockReadFile.mockResolvedValue('invalid json {');
|
|
152
|
-
await expect(readTraceFile(path))
|
|
153
|
-
.rejects.toThrow(`Invalid JSON in trace file: ${path}`);
|
|
154
|
-
});
|
|
155
|
-
it('should rethrow other errors', async () => {
|
|
156
|
-
const { mockReadFile } = await getMocks();
|
|
157
|
-
const path = '/logs/test.json';
|
|
158
|
-
const error = new Error('Permission denied');
|
|
159
|
-
mockReadFile.mockRejectedValue(error);
|
|
160
|
-
await expect(readTraceFile(path))
|
|
161
|
-
.rejects.toThrow('Permission denied');
|
|
67
|
+
it('should throw error when API returns 500', async () => {
|
|
68
|
+
mockGetWorkflowIdTraceLog.mockResolvedValue({
|
|
69
|
+
status: 500,
|
|
70
|
+
data: { error: 'S3 access denied' }
|
|
71
|
+
});
|
|
72
|
+
const { getTrace } = await import('./trace_reader.js');
|
|
73
|
+
await expect(getTrace('wf-123'))
|
|
74
|
+
.rejects
|
|
75
|
+
.toThrow('S3 access denied');
|
|
162
76
|
});
|
|
163
77
|
});
|
|
164
78
|
});
|
package/dist/utils/env_loader.js
CHANGED
|
@@ -1,43 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Environment loader utility
|
|
3
|
-
* Loads .env
|
|
3
|
+
* Loads .env file from the current working directory
|
|
4
|
+
* Set OUTPUT_CLI_ENV to specify a custom env file path
|
|
4
5
|
*/
|
|
5
|
-
import
|
|
6
|
-
import
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
7
|
+
import { resolve } from 'node:path';
|
|
7
8
|
import * as dotenv from 'dotenv';
|
|
8
|
-
/**
|
|
9
|
-
* Load environment variables from .env files in the current working directory
|
|
10
|
-
* Loads in order: .env, then .env.local (if exists)
|
|
11
|
-
* .env.local overrides values from .env
|
|
12
|
-
*/
|
|
13
9
|
export function loadEnvironment() {
|
|
14
10
|
const cwd = process.cwd();
|
|
15
|
-
|
|
16
|
-
const envPath =
|
|
17
|
-
if (
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (result.error) {
|
|
21
|
-
// Log warning but don't fail - malformed .env shouldn't break the CLI
|
|
22
|
-
console.warn(`Warning: Error parsing .env file: ${result.error.message}`);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
catch (error) {
|
|
26
|
-
// Silent failure - .env loading is optional
|
|
27
|
-
console.warn(`Warning: Could not load .env file: ${error}`);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
// Load .env.local file (overrides .env)
|
|
31
|
-
const envLocalPath = path.join(cwd, '.env.local');
|
|
32
|
-
if (fs.existsSync(envLocalPath)) {
|
|
33
|
-
try {
|
|
34
|
-
const result = dotenv.config({ path: envLocalPath });
|
|
35
|
-
if (result.error) {
|
|
36
|
-
console.warn(`Warning: Error parsing .env.local file: ${result.error.message}`);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
catch (error) {
|
|
40
|
-
console.warn(`Warning: Could not load .env.local file: ${error}`);
|
|
41
|
-
}
|
|
11
|
+
const envFile = process.env.OUTPUT_CLI_ENV || '.env';
|
|
12
|
+
const envPath = resolve(cwd, envFile);
|
|
13
|
+
if (!existsSync(envPath)) {
|
|
14
|
+
console.warn(`Warning: Env file not found: ${envPath}`);
|
|
15
|
+
return;
|
|
42
16
|
}
|
|
17
|
+
console.log(`Loading env from: ${envPath}`);
|
|
18
|
+
dotenv.config({ path: envPath });
|
|
43
19
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the env loader utility
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
5
|
+
import { existsSync } from 'node:fs';
|
|
6
|
+
import { resolve } from 'node:path';
|
|
7
|
+
import * as dotenv from 'dotenv';
|
|
8
|
+
vi.mock('node:fs');
|
|
9
|
+
vi.mock('dotenv');
|
|
10
|
+
describe('loadEnvironment', () => {
|
|
11
|
+
const originalEnv = { ...process.env };
|
|
12
|
+
const mockCwd = '/mock/project';
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
vi.resetModules();
|
|
15
|
+
vi.clearAllMocks();
|
|
16
|
+
vi.spyOn(process, 'cwd').mockReturnValue(mockCwd);
|
|
17
|
+
vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
18
|
+
vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
19
|
+
});
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
process.env = { ...originalEnv };
|
|
22
|
+
vi.restoreAllMocks();
|
|
23
|
+
});
|
|
24
|
+
it('should load from OUTPUT_CLI_ENV when set and file exists', async () => {
|
|
25
|
+
process.env.OUTPUT_CLI_ENV = '.env.prod';
|
|
26
|
+
const expectedPath = resolve(mockCwd, '.env.prod');
|
|
27
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
28
|
+
vi.mocked(dotenv.config).mockReturnValue({ parsed: { API_URL: 'https://prod.api.com' } });
|
|
29
|
+
const { loadEnvironment } = await import('./env_loader.js');
|
|
30
|
+
loadEnvironment();
|
|
31
|
+
expect(console.log).toHaveBeenCalledWith(`Loading env from: ${expectedPath}`);
|
|
32
|
+
expect(dotenv.config).toHaveBeenCalledWith({ path: expectedPath });
|
|
33
|
+
});
|
|
34
|
+
it('should warn when OUTPUT_CLI_ENV file does not exist', async () => {
|
|
35
|
+
process.env.OUTPUT_CLI_ENV = '.env.missing';
|
|
36
|
+
const expectedPath = resolve(mockCwd, '.env.missing');
|
|
37
|
+
vi.mocked(existsSync).mockReturnValue(false);
|
|
38
|
+
const { loadEnvironment } = await import('./env_loader.js');
|
|
39
|
+
loadEnvironment();
|
|
40
|
+
expect(console.warn).toHaveBeenCalledWith(`Warning: Env file not found: ${expectedPath}`);
|
|
41
|
+
expect(dotenv.config).not.toHaveBeenCalled();
|
|
42
|
+
});
|
|
43
|
+
it('should load .env by default and log', async () => {
|
|
44
|
+
delete process.env.OUTPUT_CLI_ENV;
|
|
45
|
+
const envPath = resolve(mockCwd, '.env');
|
|
46
|
+
vi.mocked(existsSync).mockImplementation(p => p === envPath);
|
|
47
|
+
vi.mocked(dotenv.config).mockReturnValue({ parsed: {} });
|
|
48
|
+
const { loadEnvironment } = await import('./env_loader.js');
|
|
49
|
+
loadEnvironment();
|
|
50
|
+
expect(console.log).toHaveBeenCalledWith(`Loading env from: ${envPath}`);
|
|
51
|
+
expect(dotenv.config).toHaveBeenCalledTimes(1);
|
|
52
|
+
expect(dotenv.config).toHaveBeenCalledWith({ path: envPath });
|
|
53
|
+
});
|
|
54
|
+
it('should warn when default .env does not exist', async () => {
|
|
55
|
+
delete process.env.OUTPUT_CLI_ENV;
|
|
56
|
+
const envPath = resolve(mockCwd, '.env');
|
|
57
|
+
vi.mocked(existsSync).mockReturnValue(false);
|
|
58
|
+
const { loadEnvironment } = await import('./env_loader.js');
|
|
59
|
+
loadEnvironment();
|
|
60
|
+
expect(console.warn).toHaveBeenCalledWith(`Warning: Env file not found: ${envPath}`);
|
|
61
|
+
expect(dotenv.config).not.toHaveBeenCalled();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -5,21 +5,45 @@ const DEFAULT_MESSAGES = {
|
|
|
5
5
|
404: 'Resource not found.',
|
|
6
6
|
UNKNOWN: 'An unknown error occurred.'
|
|
7
7
|
};
|
|
8
|
+
/**
|
|
9
|
+
* Extract detailed error information from fetch errors and their causes
|
|
10
|
+
*/
|
|
11
|
+
function getDetailedErrorMessage(error) {
|
|
12
|
+
const apiError = error;
|
|
13
|
+
const parts = [];
|
|
14
|
+
if (apiError.message) {
|
|
15
|
+
parts.push(apiError.message);
|
|
16
|
+
}
|
|
17
|
+
if (apiError.cause) {
|
|
18
|
+
const cause = apiError.cause;
|
|
19
|
+
if (cause.message && cause.message !== apiError.message) {
|
|
20
|
+
parts.push(`Cause: ${cause.message}`);
|
|
21
|
+
}
|
|
22
|
+
if (cause.code) {
|
|
23
|
+
parts.push(`Code: ${cause.code}`);
|
|
24
|
+
}
|
|
25
|
+
if (cause.hostname) {
|
|
26
|
+
parts.push(`Host: ${cause.hostname}${cause.port ? ':' + cause.port : ''}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (apiError.response?.status) {
|
|
30
|
+
parts.push(`HTTP Status: ${apiError.response.status}`);
|
|
31
|
+
}
|
|
32
|
+
return parts.length > 0 ? parts.join(' | ') : 'Unknown error';
|
|
33
|
+
}
|
|
8
34
|
export function handleApiError(error, errorFn, overrides = {}) {
|
|
9
35
|
const apiError = error;
|
|
10
36
|
const errorMessages = { ...DEFAULT_MESSAGES, ...overrides };
|
|
11
|
-
if (apiError.code === 'ECONNREFUSED') {
|
|
12
|
-
|
|
37
|
+
if (apiError.code === 'ECONNREFUSED' || apiError.cause?.code === 'ECONNREFUSED') {
|
|
38
|
+
errorFn(errorMessages.ECONNREFUSED, { exit: 1 });
|
|
13
39
|
}
|
|
14
40
|
if (apiError.response?.status) {
|
|
15
41
|
const status = apiError.response.status;
|
|
16
42
|
const message = errorMessages[status];
|
|
17
43
|
if (message) {
|
|
18
|
-
|
|
44
|
+
errorFn(message, { exit: 1 });
|
|
19
45
|
}
|
|
20
46
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
return errorFn(errorMessages.UNKNOWN, { exit: 1 });
|
|
47
|
+
const detailedMessage = getDetailedErrorMessage(error);
|
|
48
|
+
errorFn(detailedMessage, { exit: 1 });
|
|
25
49
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@output.ai/cli",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
4
4
|
"description": "CLI for Output.ai workflow generation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"handlebars": "4.7.8",
|
|
36
36
|
"json-schema-library": "10.3.0",
|
|
37
37
|
"ky": "1.12.0",
|
|
38
|
+
"log-update": "7.0.2",
|
|
38
39
|
"validator": "13.15.22"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|