@outputai/cli 0.10.1-next.be4ec7f.0 → 0.10.1-next.bf95a6f.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/dist/api/generated/api.d.ts +81 -26
- package/dist/api/generated/api.js +7 -4
- package/dist/assets/docker/docker-compose-dev.yml +2 -2
- package/dist/commands/workflow/result.js +2 -2
- package/dist/commands/workflow/result.spec.js +65 -1
- package/dist/commands/workflow/run.js +10 -3
- package/dist/commands/workflow/run.spec.js +42 -5
- package/dist/commands/workflow/start.d.ts +3 -1
- package/dist/commands/workflow/start.js +12 -2
- package/dist/commands/workflow/start.spec.js +30 -5
- package/dist/commands/workflow/status.spec.js +1 -1
- package/dist/commands/workflow/{test_eval.d.ts → test.d.ts} +0 -1
- package/dist/commands/workflow/{test_eval.js → test.js} +0 -1
- package/dist/commands/workflow/{test_eval.spec.js → test.spec.js} +4 -4
- package/dist/generated/framework_version.json +1 -1
- package/dist/templates/agent_instructions/CLAUDE.md.template +4 -2
- package/dist/templates/project/README.md.template +3 -1
- package/dist/templates/project/package.json.template +2 -2
- package/dist/utils/env_loader.js +6 -2
- package/dist/utils/env_loader.spec.js +61 -32
- package/dist/utils/format_workflow_result.d.ts +3 -2
- package/dist/utils/format_workflow_result.js +6 -2
- package/dist/utils/format_workflow_result.spec.js +39 -6
- package/dist/utils/normalize_workflow_status.d.ts +4 -3
- package/dist/utils/normalize_workflow_status.js +12 -3
- package/dist/utils/normalize_workflow_status.spec.js +3 -0
- package/dist/utils/resolve_input.d.ts +9 -1
- package/dist/utils/resolve_input.js +8 -2
- package/dist/utils/resolve_input.spec.d.ts +1 -0
- package/dist/utils/resolve_input.spec.js +75 -0
- package/dist/views/dev/components/workflow_status.js +1 -1
- package/dist/views/dev/hooks/use_run_detail.js +1 -1
- package/dist/views/dev/hooks/use_run_detail.spec.js +1 -1
- package/dist/views/dev/panels/runs_panel.js +2 -2
- package/oclif.manifest.json +15 -9
- package/package.json +6 -8
- /package/dist/commands/workflow/{test_eval.spec.d.ts → test.spec.d.ts} +0 -0
|
@@ -66,22 +66,29 @@ export interface Workflow {
|
|
|
66
66
|
/** Alternative names that resolve to this workflow */
|
|
67
67
|
aliases?: string[];
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Legacy trace information containing nested destinations
|
|
71
|
+
* @nullable
|
|
72
|
+
*/
|
|
73
|
+
export type TraceInfoV1 = {
|
|
74
|
+
/** Available destinations for trace data */
|
|
75
|
+
destinations?: {
|
|
76
|
+
/** Absolute path to local trace file, omitted if not saved locally */
|
|
77
|
+
local?: string;
|
|
78
|
+
/** Remote trace location (e.g., S3 URI), omitted if not saved remotely */
|
|
79
|
+
remote?: string;
|
|
80
|
+
};
|
|
81
|
+
} | null;
|
|
69
82
|
/**
|
|
70
83
|
* Available destinations for trace data
|
|
84
|
+
* @nullable
|
|
71
85
|
*/
|
|
72
|
-
export type
|
|
86
|
+
export type TraceInfoV2 = {
|
|
73
87
|
/** Absolute path to local trace file, omitted if not saved locally */
|
|
74
88
|
local?: string;
|
|
75
89
|
/** Remote trace location (e.g., S3 URI), omitted if not saved remotely */
|
|
76
90
|
remote?: string;
|
|
77
|
-
};
|
|
78
|
-
/**
|
|
79
|
-
* An object with information about the trace generated by the execution
|
|
80
|
-
*/
|
|
81
|
-
export interface TraceInfo {
|
|
82
|
-
/** Available destinations for trace data */
|
|
83
|
-
destinations?: TraceInfoDestinations;
|
|
84
|
-
}
|
|
91
|
+
} | null;
|
|
85
92
|
/**
|
|
86
93
|
* The workflow input
|
|
87
94
|
*/
|
|
@@ -148,7 +155,7 @@ export declare const WorkflowRunInfoStatus: {
|
|
|
148
155
|
readonly running: "running";
|
|
149
156
|
readonly completed: "completed";
|
|
150
157
|
readonly failed: "failed";
|
|
151
|
-
readonly
|
|
158
|
+
readonly cancelled: "cancelled";
|
|
152
159
|
readonly terminated: "terminated";
|
|
153
160
|
readonly timed_out: "timed_out";
|
|
154
161
|
readonly continued_as_new: "continued_as_new";
|
|
@@ -180,7 +187,7 @@ export interface WorkflowRunsResponse {
|
|
|
180
187
|
*/
|
|
181
188
|
export type WorkflowStatusResponseStatus = typeof WorkflowStatusResponseStatus[keyof typeof WorkflowStatusResponseStatus];
|
|
182
189
|
export declare const WorkflowStatusResponseStatus: {
|
|
183
|
-
readonly
|
|
190
|
+
readonly cancelled: "cancelled";
|
|
184
191
|
readonly completed: "completed";
|
|
185
192
|
readonly continued_as_new: "continued_as_new";
|
|
186
193
|
readonly failed: "failed";
|
|
@@ -204,20 +211,33 @@ export interface WorkflowStatusResponse {
|
|
|
204
211
|
/**
|
|
205
212
|
* The workflow execution status
|
|
206
213
|
*/
|
|
207
|
-
export type
|
|
208
|
-
export declare const
|
|
214
|
+
export type WorkflowResultStatus = typeof WorkflowResultStatus[keyof typeof WorkflowResultStatus];
|
|
215
|
+
export declare const WorkflowResultStatus: {
|
|
209
216
|
readonly completed: "completed";
|
|
210
217
|
readonly failed: "failed";
|
|
211
|
-
readonly
|
|
218
|
+
readonly cancelled: "cancelled";
|
|
212
219
|
readonly terminated: "terminated";
|
|
213
220
|
readonly timed_out: "timed_out";
|
|
214
221
|
readonly continued_as_new: "continued_as_new";
|
|
215
222
|
};
|
|
223
|
+
/**
|
|
224
|
+
* Structured error details captured from the workflow or activity failure
|
|
225
|
+
* @nullable
|
|
226
|
+
*/
|
|
227
|
+
export type SerializedWorkflowError = {
|
|
228
|
+
/** Failing activity type, omitted when the failure did not originate in an activity */
|
|
229
|
+
activityType?: string;
|
|
230
|
+
/** Original error class name */
|
|
231
|
+
name?: string;
|
|
232
|
+
/** Original error message */
|
|
233
|
+
message?: string;
|
|
234
|
+
[key: string]: unknown;
|
|
235
|
+
} | null;
|
|
216
236
|
/**
|
|
217
237
|
* Structured failure details if the workflow failed, null otherwise
|
|
218
238
|
* @nullable
|
|
219
239
|
*/
|
|
220
|
-
export type
|
|
240
|
+
export type WorkflowResultV1ResponseErrorDetails = {
|
|
221
241
|
/**
|
|
222
242
|
* Friendly failure message (from the underlying application error)
|
|
223
243
|
* @nullable
|
|
@@ -246,29 +266,64 @@ export type WorkflowResultResponseErrorDetails = {
|
|
|
246
266
|
[key: string]: unknown;
|
|
247
267
|
} | null;
|
|
248
268
|
} | null | null;
|
|
249
|
-
|
|
269
|
+
/**
|
|
270
|
+
* Legacy wrapped workflow result
|
|
271
|
+
* @deprecated
|
|
272
|
+
*/
|
|
273
|
+
export interface WorkflowResultV1Response {
|
|
250
274
|
/** The workflow execution id */
|
|
251
|
-
workflowId
|
|
252
|
-
/**
|
|
253
|
-
|
|
275
|
+
workflowId: string;
|
|
276
|
+
/**
|
|
277
|
+
* The specific run id for this execution
|
|
278
|
+
* @nullable
|
|
279
|
+
*/
|
|
280
|
+
runId: string | null;
|
|
281
|
+
status: WorkflowResultStatus;
|
|
254
282
|
/** The original input passed to the workflow, null if unavailable */
|
|
255
|
-
input
|
|
283
|
+
input: unknown | null;
|
|
256
284
|
/** The result of workflow, null if workflow failed */
|
|
257
|
-
output
|
|
258
|
-
trace
|
|
259
|
-
/** The workflow execution status */
|
|
260
|
-
status?: WorkflowResultResponseStatus;
|
|
285
|
+
output: unknown | null;
|
|
286
|
+
trace: TraceInfoV1 | null;
|
|
261
287
|
/**
|
|
262
288
|
* Error message if workflow failed, null otherwise
|
|
263
289
|
* @nullable
|
|
264
290
|
*/
|
|
265
|
-
error
|
|
291
|
+
error: string | null;
|
|
266
292
|
/**
|
|
267
293
|
* Structured failure details if the workflow failed, null otherwise
|
|
268
294
|
* @nullable
|
|
269
295
|
*/
|
|
270
|
-
errorDetails
|
|
296
|
+
errorDetails: WorkflowResultV1ResponseErrorDetails;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Workflow result response version
|
|
300
|
+
*/
|
|
301
|
+
export type WorkflowResultV2ResponseV = typeof WorkflowResultV2ResponseV[keyof typeof WorkflowResultV2ResponseV];
|
|
302
|
+
export declare const WorkflowResultV2ResponseV: {
|
|
303
|
+
readonly NUMBER_2: "2";
|
|
304
|
+
};
|
|
305
|
+
/**
|
|
306
|
+
* Current workflow result with direct output and memo-based trace information
|
|
307
|
+
*/
|
|
308
|
+
export interface WorkflowResultV2Response {
|
|
309
|
+
/** Workflow result response version */
|
|
310
|
+
v: WorkflowResultV2ResponseV;
|
|
311
|
+
/** The workflow execution id */
|
|
312
|
+
workflowId: string;
|
|
313
|
+
/**
|
|
314
|
+
* The specific run id for this execution
|
|
315
|
+
* @nullable
|
|
316
|
+
*/
|
|
317
|
+
runId: string | null;
|
|
318
|
+
status: WorkflowResultStatus;
|
|
319
|
+
/** The original input passed to the workflow, null if unavailable */
|
|
320
|
+
input: unknown | null;
|
|
321
|
+
/** Direct workflow output, null if no output is available */
|
|
322
|
+
output: unknown | null;
|
|
323
|
+
trace: TraceInfoV2 | null;
|
|
324
|
+
error: (SerializedWorkflowError | null) | null;
|
|
271
325
|
}
|
|
326
|
+
export type WorkflowResultResponse = WorkflowResultV2Response | WorkflowResultV1Response;
|
|
272
327
|
export interface WorkflowInputResponse {
|
|
273
328
|
/** The workflow execution id */
|
|
274
329
|
workflowId: string;
|
|
@@ -19,13 +19,13 @@ export const WorkflowRunInfoStatus = {
|
|
|
19
19
|
running: 'running',
|
|
20
20
|
completed: 'completed',
|
|
21
21
|
failed: 'failed',
|
|
22
|
-
|
|
22
|
+
cancelled: 'cancelled',
|
|
23
23
|
terminated: 'terminated',
|
|
24
24
|
timed_out: 'timed_out',
|
|
25
25
|
continued_as_new: 'continued_as_new',
|
|
26
26
|
};
|
|
27
27
|
export const WorkflowStatusResponseStatus = {
|
|
28
|
-
|
|
28
|
+
cancelled: 'cancelled',
|
|
29
29
|
completed: 'completed',
|
|
30
30
|
continued_as_new: 'continued_as_new',
|
|
31
31
|
failed: 'failed',
|
|
@@ -34,14 +34,17 @@ export const WorkflowStatusResponseStatus = {
|
|
|
34
34
|
timed_out: 'timed_out',
|
|
35
35
|
unspecified: 'unspecified',
|
|
36
36
|
};
|
|
37
|
-
export const
|
|
37
|
+
export const WorkflowResultStatus = {
|
|
38
38
|
completed: 'completed',
|
|
39
39
|
failed: 'failed',
|
|
40
|
-
|
|
40
|
+
cancelled: 'cancelled',
|
|
41
41
|
terminated: 'terminated',
|
|
42
42
|
timed_out: 'timed_out',
|
|
43
43
|
continued_as_new: 'continued_as_new',
|
|
44
44
|
};
|
|
45
|
+
export const WorkflowResultV2ResponseV = {
|
|
46
|
+
NUMBER_2: '2',
|
|
47
|
+
};
|
|
45
48
|
;
|
|
46
49
|
export const getGetHealthUrl = () => {
|
|
47
50
|
return `/health`;
|
|
@@ -80,7 +80,7 @@ services:
|
|
|
80
80
|
condition: service_healthy
|
|
81
81
|
worker:
|
|
82
82
|
condition: service_healthy
|
|
83
|
-
image: outputai/api:${OUTPUT_API_VERSION:-0.10.1-next.
|
|
83
|
+
image: outputai/api:${OUTPUT_API_VERSION:-0.10.1-next.bf95a6f.0}
|
|
84
84
|
init: true
|
|
85
85
|
networks:
|
|
86
86
|
- main
|
|
@@ -118,7 +118,7 @@ services:
|
|
|
118
118
|
- OUTPUT_CATALOG_ID=${OUTPUT_CATALOG_ID:-main}
|
|
119
119
|
- OUTPUT_REDIS_URL=redis://redis:6379
|
|
120
120
|
- OUTPUT_TRACE_LOCAL_ON=${OUTPUT_TRACE_LOCAL_ON:-true}
|
|
121
|
-
- OUTPUT_TRACE_HOST_PATH=${PWD}/logs
|
|
121
|
+
- OUTPUT_TRACE_HOST_PATH=${OUTPUT_TRACE_HOST_PATH:-${PWD}/logs}
|
|
122
122
|
- OUTPUT_TRACE_HTTP_VERBOSE=${OUTPUT_TRACE_HTTP_VERBOSE:-true}
|
|
123
123
|
- OUTPUT_ENABLE_ATTRIBUTE_SIGNAL_EMISSION=${OUTPUT_ENABLE_ATTRIBUTE_SIGNAL_EMISSION:-false}
|
|
124
124
|
- TEMPORAL_ADDRESS=temporal:7233
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Args, Command } from '@oclif/core';
|
|
2
2
|
import { getWorkflowIdResult } from '#api/generated/api.js';
|
|
3
|
-
import { formatWorkflowResult,
|
|
3
|
+
import { formatWorkflowResult, isErrorWorkflowStatus } from '#utils/format_workflow_result.js';
|
|
4
4
|
import { handleApiError } from '#utils/error_handler.js';
|
|
5
5
|
export default class WorkflowResult extends Command {
|
|
6
6
|
static description = 'Get workflow execution result';
|
|
@@ -24,7 +24,7 @@ export default class WorkflowResult extends Command {
|
|
|
24
24
|
}
|
|
25
25
|
const data = response.data;
|
|
26
26
|
this.log(`\n${formatWorkflowResult(data)}`);
|
|
27
|
-
if (
|
|
27
|
+
if (isErrorWorkflowStatus(data.status)) {
|
|
28
28
|
process.exitCode = 1;
|
|
29
29
|
}
|
|
30
30
|
return data;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
1
2
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
vi.mock('
|
|
3
|
+
vi.mock('#api/generated/api.js', () => ({
|
|
3
4
|
getWorkflowIdResult: vi.fn()
|
|
4
5
|
}));
|
|
5
6
|
describe('workflow result command', () => {
|
|
6
7
|
beforeEach(() => {
|
|
7
8
|
vi.clearAllMocks();
|
|
9
|
+
process.exitCode = undefined;
|
|
8
10
|
});
|
|
9
11
|
describe('command definition', () => {
|
|
10
12
|
it('should export a valid OCLIF command', async () => {
|
|
@@ -18,4 +20,66 @@ describe('workflow result command', () => {
|
|
|
18
20
|
expect(WorkflowResult.enableJsonFlag).toBe(true);
|
|
19
21
|
});
|
|
20
22
|
});
|
|
23
|
+
describe('run()', () => {
|
|
24
|
+
const runCommand = async (data) => {
|
|
25
|
+
const WorkflowResult = (await import('./result.js')).default;
|
|
26
|
+
const { getWorkflowIdResult } = await import('#api/generated/api.js');
|
|
27
|
+
const cmd = new WorkflowResult(['wf-1'], {});
|
|
28
|
+
cmd.log = vi.fn();
|
|
29
|
+
cmd.parse = vi.fn().mockResolvedValue({ args: { workflowId: 'wf-1' } });
|
|
30
|
+
vi.mocked(getWorkflowIdResult).mockResolvedValue({
|
|
31
|
+
data,
|
|
32
|
+
status: 200,
|
|
33
|
+
headers: new Headers()
|
|
34
|
+
});
|
|
35
|
+
const result = await cmd.run();
|
|
36
|
+
return { cmd, result, getWorkflowIdResult };
|
|
37
|
+
};
|
|
38
|
+
it('returns and formats a legacy result', async () => {
|
|
39
|
+
const data = {
|
|
40
|
+
workflowId: 'wf-1',
|
|
41
|
+
runId: 'run-1',
|
|
42
|
+
status: 'failed',
|
|
43
|
+
input: {},
|
|
44
|
+
output: null,
|
|
45
|
+
trace: null,
|
|
46
|
+
error: 'Legacy failure',
|
|
47
|
+
errorDetails: null
|
|
48
|
+
};
|
|
49
|
+
const { cmd, result, getWorkflowIdResult } = await runCommand(data);
|
|
50
|
+
expect(getWorkflowIdResult).toHaveBeenCalledWith('wf-1');
|
|
51
|
+
expect(cmd.log).toHaveBeenCalledWith(expect.stringContaining('Error: Legacy failure'));
|
|
52
|
+
expect(result).toEqual(data);
|
|
53
|
+
expect(process.exitCode).toBe(1);
|
|
54
|
+
});
|
|
55
|
+
it('returns and formats a current result', async () => {
|
|
56
|
+
const data = {
|
|
57
|
+
v: '2',
|
|
58
|
+
workflowId: 'wf-1',
|
|
59
|
+
runId: 'run-1',
|
|
60
|
+
status: 'failed',
|
|
61
|
+
input: {},
|
|
62
|
+
output: null,
|
|
63
|
+
trace: null,
|
|
64
|
+
error: { name: 'ValidationError', message: 'Invalid input' }
|
|
65
|
+
};
|
|
66
|
+
const { result } = await runCommand(data);
|
|
67
|
+
expect(result).toEqual(data);
|
|
68
|
+
expect(process.exitCode).toBe(1);
|
|
69
|
+
});
|
|
70
|
+
it('sets a failure exit code for the previous canceled spelling', async () => {
|
|
71
|
+
const data = {
|
|
72
|
+
workflowId: 'wf-1',
|
|
73
|
+
runId: 'run-1',
|
|
74
|
+
status: 'canceled',
|
|
75
|
+
input: {},
|
|
76
|
+
output: null,
|
|
77
|
+
trace: null,
|
|
78
|
+
error: 'Workflow was canceled',
|
|
79
|
+
errorDetails: null
|
|
80
|
+
};
|
|
81
|
+
await runCommand(data);
|
|
82
|
+
expect(process.exitCode).toBe(1);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
21
85
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Args, Command, Flags } from '@oclif/core';
|
|
2
2
|
import { postWorkflowRun } from '#api/generated/api.js';
|
|
3
|
-
import { formatWorkflowResult,
|
|
3
|
+
import { formatWorkflowResult, isErrorWorkflowStatus } from '#utils/format_workflow_result.js';
|
|
4
4
|
import { handleApiError } from '#utils/error_handler.js';
|
|
5
5
|
import { resolveInput } from '#utils/resolve_input.js';
|
|
6
6
|
import { getRetryDelayFromResponse } from '#utils/header_utils.js';
|
|
@@ -63,7 +63,14 @@ export default class WorkflowRun extends Command {
|
|
|
63
63
|
};
|
|
64
64
|
async run() {
|
|
65
65
|
const { args, flags } = await this.parse(WorkflowRun);
|
|
66
|
-
const input = await resolveInput(
|
|
66
|
+
const input = await resolveInput({
|
|
67
|
+
workflowName: args.workflowName,
|
|
68
|
+
scenario: args.scenario,
|
|
69
|
+
inputFlag: flags.input,
|
|
70
|
+
commandName: 'run',
|
|
71
|
+
catalog: flags.catalog,
|
|
72
|
+
json: this.jsonEnabled()
|
|
73
|
+
});
|
|
67
74
|
this.log(`Executing workflow: ${args.workflowName}...`);
|
|
68
75
|
const response = await executeWorkflow({
|
|
69
76
|
body: {
|
|
@@ -79,7 +86,7 @@ export default class WorkflowRun extends Command {
|
|
|
79
86
|
}
|
|
80
87
|
const data = response.data;
|
|
81
88
|
this.log(`\n${formatWorkflowResult(data)}`);
|
|
82
|
-
if (
|
|
89
|
+
if (isErrorWorkflowStatus(data.status)) {
|
|
83
90
|
process.exitCode = 1;
|
|
84
91
|
}
|
|
85
92
|
return data;
|
|
@@ -59,12 +59,26 @@ describe('workflow run command', () => {
|
|
|
59
59
|
const { cmd, postWorkflowRun, resolveInput } = await createCommand();
|
|
60
60
|
resolveInput.mockResolvedValue({ key: 'value' });
|
|
61
61
|
postWorkflowRun.mockResolvedValue({
|
|
62
|
-
data: {
|
|
62
|
+
data: {
|
|
63
|
+
v: '2',
|
|
64
|
+
workflowId: 'wf-1',
|
|
65
|
+
runId: 'run-1',
|
|
66
|
+
status: 'completed',
|
|
67
|
+
input: { key: 'value' },
|
|
68
|
+
output: 'ok',
|
|
69
|
+
trace: null,
|
|
70
|
+
error: null
|
|
71
|
+
},
|
|
63
72
|
status: 200,
|
|
64
73
|
headers: new Headers()
|
|
65
74
|
});
|
|
66
75
|
await cmd.run();
|
|
67
|
-
expect(resolveInput).toHaveBeenCalledWith(
|
|
76
|
+
expect(resolveInput).toHaveBeenCalledWith(expect.objectContaining({
|
|
77
|
+
workflowName: 'my_workflow',
|
|
78
|
+
commandName: 'run',
|
|
79
|
+
catalog: undefined,
|
|
80
|
+
json: false
|
|
81
|
+
}));
|
|
68
82
|
expect(postWorkflowRun).toHaveBeenCalledTimes(1);
|
|
69
83
|
expect(postWorkflowRun).toHaveBeenCalledWith({ workflowName: 'my_workflow', input: { key: 'value' }, catalog: undefined }, expect.objectContaining({ config: { timeout: 600000 } }));
|
|
70
84
|
expect(cmd.log).toHaveBeenCalledWith('Executing workflow: my_workflow...');
|
|
@@ -78,12 +92,26 @@ describe('workflow run command', () => {
|
|
|
78
92
|
});
|
|
79
93
|
resolveInput.mockResolvedValue({ key: 'value' });
|
|
80
94
|
postWorkflowRun.mockResolvedValue({
|
|
81
|
-
data: {
|
|
95
|
+
data: {
|
|
96
|
+
v: '2',
|
|
97
|
+
workflowId: 'wf-1',
|
|
98
|
+
runId: 'run-1',
|
|
99
|
+
status: 'completed',
|
|
100
|
+
input: { key: 'value' },
|
|
101
|
+
output: {},
|
|
102
|
+
trace: null,
|
|
103
|
+
error: null
|
|
104
|
+
},
|
|
82
105
|
status: 200,
|
|
83
106
|
headers: new Headers()
|
|
84
107
|
});
|
|
85
108
|
await cmd.run();
|
|
86
|
-
expect(resolveInput).toHaveBeenCalledWith(
|
|
109
|
+
expect(resolveInput).toHaveBeenCalledWith(expect.objectContaining({
|
|
110
|
+
workflowName: 'my_workflow',
|
|
111
|
+
scenario: 'basic',
|
|
112
|
+
commandName: 'run',
|
|
113
|
+
catalog: 'my-catalog'
|
|
114
|
+
}));
|
|
87
115
|
expect(postWorkflowRun).toHaveBeenCalledWith(expect.objectContaining({ catalog: 'my-catalog' }), expect.anything());
|
|
88
116
|
});
|
|
89
117
|
it('retries when response has Retry-After and succeeds on second attempt', async () => {
|
|
@@ -93,7 +121,16 @@ describe('workflow run command', () => {
|
|
|
93
121
|
postWorkflowRun
|
|
94
122
|
.mockRejectedValueOnce(new HttpError('Unavailable', { status: 503, headers }))
|
|
95
123
|
.mockResolvedValueOnce({
|
|
96
|
-
data: {
|
|
124
|
+
data: {
|
|
125
|
+
v: '2',
|
|
126
|
+
workflowId: 'wf-1',
|
|
127
|
+
runId: 'run-1',
|
|
128
|
+
status: 'completed',
|
|
129
|
+
input: {},
|
|
130
|
+
output: {},
|
|
131
|
+
trace: null,
|
|
132
|
+
error: null
|
|
133
|
+
},
|
|
97
134
|
status: 200,
|
|
98
135
|
headers: new Headers()
|
|
99
136
|
});
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
|
+
import { type PostWorkflowStart200 } from '#api/generated/api.js';
|
|
2
3
|
export default class WorkflowStart extends Command {
|
|
3
4
|
static description: string;
|
|
5
|
+
static enableJsonFlag: boolean;
|
|
4
6
|
static examples: string[];
|
|
5
7
|
static args: {
|
|
6
8
|
workflowName: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
@@ -10,6 +12,6 @@ export default class WorkflowStart extends Command {
|
|
|
10
12
|
input: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
13
|
catalog: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
14
|
};
|
|
13
|
-
run(): Promise<
|
|
15
|
+
run(): Promise<PostWorkflowStart200>;
|
|
14
16
|
catch(error: Error): Promise<void>;
|
|
15
17
|
}
|
|
@@ -4,11 +4,13 @@ import { handleApiError } from '#utils/error_handler.js';
|
|
|
4
4
|
import { resolveInput } from '#utils/resolve_input.js';
|
|
5
5
|
export default class WorkflowStart extends Command {
|
|
6
6
|
static description = 'Start a workflow asynchronously without waiting for completion';
|
|
7
|
+
static enableJsonFlag = true;
|
|
7
8
|
static examples = [
|
|
8
9
|
'<%= config.bin %> <%= command.id %> simple basic_input',
|
|
9
10
|
'<%= config.bin %> <%= command.id %> simple --input \'{"values":[1,2,3]}\'',
|
|
10
11
|
'<%= config.bin %> <%= command.id %> simple --input input.json',
|
|
11
|
-
'<%= config.bin %> <%= command.id %> simple --input \'{"key":"value"}\' --catalog my-catalog'
|
|
12
|
+
'<%= config.bin %> <%= command.id %> simple --input \'{"key":"value"}\' --catalog my-catalog',
|
|
13
|
+
'<%= config.bin %> <%= command.id %> simple --json'
|
|
12
14
|
];
|
|
13
15
|
static args = {
|
|
14
16
|
workflowName: Args.string({
|
|
@@ -37,7 +39,14 @@ export default class WorkflowStart extends Command {
|
|
|
37
39
|
};
|
|
38
40
|
async run() {
|
|
39
41
|
const { args, flags } = await this.parse(WorkflowStart);
|
|
40
|
-
const input = await resolveInput(
|
|
42
|
+
const input = await resolveInput({
|
|
43
|
+
workflowName: args.workflowName,
|
|
44
|
+
scenario: args.scenario,
|
|
45
|
+
inputFlag: flags.input,
|
|
46
|
+
commandName: 'start',
|
|
47
|
+
catalog: flags.catalog,
|
|
48
|
+
json: this.jsonEnabled()
|
|
49
|
+
});
|
|
41
50
|
this.log(`Starting workflow: ${args.workflowName}...`);
|
|
42
51
|
const response = await postWorkflowStart({
|
|
43
52
|
workflowName: args.workflowName,
|
|
@@ -57,6 +66,7 @@ export default class WorkflowStart extends Command {
|
|
|
57
66
|
`Use "workflow result ${result.workflowId || '<workflow-id>'}" to get the workflow result when complete`
|
|
58
67
|
].join('\n');
|
|
59
68
|
this.log(`\n${output}`);
|
|
69
|
+
return result;
|
|
60
70
|
}
|
|
61
71
|
async catch(error) {
|
|
62
72
|
return handleApiError(error, (...args) => this.error(...args), {
|
|
@@ -36,13 +36,17 @@ describe('workflow start command', () => {
|
|
|
36
36
|
expect(WorkflowStart.flags.catalog.env).toBe('OUTPUT_CATALOG_ID');
|
|
37
37
|
expect(WorkflowStart.flags.catalog.char).toBe('c');
|
|
38
38
|
});
|
|
39
|
+
it('enables the built-in --json flag', async () => {
|
|
40
|
+
const WorkflowStart = (await import('./start.js')).default;
|
|
41
|
+
expect(WorkflowStart.enableJsonFlag).toBe(true);
|
|
42
|
+
});
|
|
39
43
|
});
|
|
40
44
|
describe('run()', () => {
|
|
41
|
-
const createCommand = async (flagOverrides = {}) => {
|
|
45
|
+
const createCommand = async (flagOverrides = {}, argv = ['my_workflow']) => {
|
|
42
46
|
const WorkflowStart = (await import('./start.js')).default;
|
|
43
47
|
const { postWorkflowStart } = await import('#api/generated/api.js');
|
|
44
48
|
const { resolveInput } = await import('#utils/resolve_input.js');
|
|
45
|
-
const cmd = new WorkflowStart(
|
|
49
|
+
const cmd = new WorkflowStart(argv, {});
|
|
46
50
|
cmd.log = vi.fn();
|
|
47
51
|
cmd.error = vi.fn(() => {
|
|
48
52
|
throw new Error('error called');
|
|
@@ -61,9 +65,15 @@ describe('workflow start command', () => {
|
|
|
61
65
|
status: 200,
|
|
62
66
|
headers: new Headers()
|
|
63
67
|
});
|
|
64
|
-
await cmd.run();
|
|
65
|
-
expect(resolveInput).toHaveBeenCalledWith(
|
|
68
|
+
const result = await cmd.run();
|
|
69
|
+
expect(resolveInput).toHaveBeenCalledWith(expect.objectContaining({
|
|
70
|
+
workflowName: 'my_workflow',
|
|
71
|
+
commandName: 'start',
|
|
72
|
+
catalog: 'my-catalog',
|
|
73
|
+
json: false
|
|
74
|
+
}));
|
|
66
75
|
expect(postWorkflowStart).toHaveBeenCalledWith(expect.objectContaining({ workflowName: 'my_workflow', catalog: 'my-catalog' }));
|
|
76
|
+
expect(result).toEqual({ workflowId: 'wf-123' });
|
|
67
77
|
});
|
|
68
78
|
it('passes undefined catalog through when none is set', async () => {
|
|
69
79
|
const { cmd, postWorkflowStart, resolveInput } = await createCommand();
|
|
@@ -74,7 +84,22 @@ describe('workflow start command', () => {
|
|
|
74
84
|
headers: new Headers()
|
|
75
85
|
});
|
|
76
86
|
await cmd.run();
|
|
77
|
-
expect(resolveInput).toHaveBeenCalledWith(
|
|
87
|
+
expect(resolveInput).toHaveBeenCalledWith(expect.objectContaining({
|
|
88
|
+
workflowName: 'my_workflow',
|
|
89
|
+
commandName: 'start',
|
|
90
|
+
catalog: undefined
|
|
91
|
+
}));
|
|
92
|
+
});
|
|
93
|
+
it('tells resolveInput to stay quiet when --json is set', async () => {
|
|
94
|
+
const { cmd, postWorkflowStart, resolveInput } = await createCommand({}, ['my_workflow', 'basic', '--json']);
|
|
95
|
+
resolveInput.mockResolvedValue({});
|
|
96
|
+
postWorkflowStart.mockResolvedValue({
|
|
97
|
+
data: { workflowId: 'wf-123' },
|
|
98
|
+
status: 200,
|
|
99
|
+
headers: new Headers()
|
|
100
|
+
});
|
|
101
|
+
await cmd.run();
|
|
102
|
+
expect(resolveInput).toHaveBeenCalledWith(expect.objectContaining({ json: true }));
|
|
78
103
|
});
|
|
79
104
|
});
|
|
80
105
|
});
|
|
@@ -2,7 +2,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
2
2
|
vi.mock('../../api/generated/api.js', () => ({
|
|
3
3
|
getWorkflowIdStatus: vi.fn(),
|
|
4
4
|
GetWorkflowIdStatus200Status: {
|
|
5
|
-
|
|
5
|
+
cancelled: 'cancelled',
|
|
6
6
|
completed: 'completed',
|
|
7
7
|
continued_as_new: 'continued_as_new',
|
|
8
8
|
failed: 'failed',
|
|
@@ -7,7 +7,6 @@ import { diagnoseMissingEvalWorkflow } from '#utils/eval_diagnostics.js';
|
|
|
7
7
|
import { handleApiError } from '#utils/error_handler.js';
|
|
8
8
|
import { getEvalWorkflowName, renderEvalOutput, computeExitCode, EvalOutputSchema } from '@outputai/evals';
|
|
9
9
|
export default class WorkflowTest extends Command {
|
|
10
|
-
static aliases = ['workflow:test'];
|
|
11
10
|
static description = 'Run evaluations against a workflow using its datasets';
|
|
12
11
|
static enableJsonFlag = true;
|
|
13
12
|
static examples = [
|
|
@@ -45,11 +45,11 @@ describe('workflow test command', () => {
|
|
|
45
45
|
});
|
|
46
46
|
describe('command definition', () => {
|
|
47
47
|
it('enables the built-in --json flag', async () => {
|
|
48
|
-
const WorkflowTest = (await import('./
|
|
48
|
+
const WorkflowTest = (await import('./test.js')).default;
|
|
49
49
|
expect(WorkflowTest.enableJsonFlag).toBe(true);
|
|
50
50
|
});
|
|
51
51
|
it('binds the catalog flag to OUTPUT_CATALOG_ID', async () => {
|
|
52
|
-
const WorkflowTest = (await import('./
|
|
52
|
+
const WorkflowTest = (await import('./test.js')).default;
|
|
53
53
|
expect(WorkflowTest.flags).toHaveProperty('catalog');
|
|
54
54
|
expect(WorkflowTest.flags.catalog.env).toBe('OUTPUT_CATALOG_ID');
|
|
55
55
|
expect(WorkflowTest.flags.catalog.char).toBe('c');
|
|
@@ -57,7 +57,7 @@ describe('workflow test command', () => {
|
|
|
57
57
|
});
|
|
58
58
|
describe('run()', () => {
|
|
59
59
|
const createCommand = async (jsonEnabled) => {
|
|
60
|
-
const WorkflowTest = (await import('./
|
|
60
|
+
const WorkflowTest = (await import('./test.js')).default;
|
|
61
61
|
const { postWorkflowRun } = await import('#api/generated/api.js');
|
|
62
62
|
const cmd = new WorkflowTest(['simple'], {});
|
|
63
63
|
cmd.log = vi.fn();
|
|
@@ -99,7 +99,7 @@ describe('workflow test command', () => {
|
|
|
99
99
|
expect(process.exitCode).toBe(1);
|
|
100
100
|
});
|
|
101
101
|
it('routes registration, dataset runs, and the eval run to the resolved catalog', async () => {
|
|
102
|
-
const WorkflowTest = (await import('./
|
|
102
|
+
const WorkflowTest = (await import('./test.js')).default;
|
|
103
103
|
const { postWorkflowRun } = await import('#api/generated/api.js');
|
|
104
104
|
const { fetchWorkflowCatalog } = await import('#api/workflow_catalog.js');
|
|
105
105
|
const cmd = new WorkflowTest(['my_workflow'], {});
|
|
@@ -17,10 +17,12 @@ claude plugin install outputai@outputai --scope project
|
|
|
17
17
|
npm run output:dev # Start dev environment (worker + Temporal)
|
|
18
18
|
npm run output:worker:build # Build TypeScript to dist/
|
|
19
19
|
npm run output:worker:check # Optional: bundle-check workflows for bad imports (node: built-ins)
|
|
20
|
-
npm run output:worker:watch # Build + restart on file changes
|
|
21
|
-
npm run output:worker #
|
|
20
|
+
npm run output:worker:watch # Build + restart on src/ file changes
|
|
21
|
+
npm run output:worker # Build and start worker
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
+
Hot-reload watches `src/` only. After changing dependencies (`package.json` / lockfile), run `npm install`, then `npx output dev down` and `npm run output:dev` again so the worker container reinstalls. A hot-reload alone is not enough; if the stack is still running, `npx output dev down` is required.
|
|
25
|
+
|
|
24
26
|
## Project Structure
|
|
25
27
|
|
|
26
28
|
```
|
|
@@ -79,7 +79,9 @@ This starts:
|
|
|
79
79
|
- Temporal server and UI (http://localhost:8080)
|
|
80
80
|
- PostgreSQL and Redis databases
|
|
81
81
|
- Output.ai API server (http://localhost:3001)
|
|
82
|
-
- Worker process for executing workflows
|
|
82
|
+
- Worker process for executing workflows (auto-reloads on `src/` changes)
|
|
83
|
+
|
|
84
|
+
Dependency changes (`package.json` / lockfile) are not picked up by hot-reload. Run `npm install`, then `npx output dev down` and `npm run output:dev` again so the worker container reinstalls. A hot-reload alone is not enough; if the stack is still running, `npx output dev down` is required.
|
|
83
85
|
|
|
84
86
|
### 4. Run a workflow
|
|
85
87
|
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
"output:worker:build": "rm -rf dist/* && tsc -p ./ && output-copy-assets",
|
|
10
10
|
"output:worker:start": "output-worker",
|
|
11
11
|
"output:worker:check": "output-worker --check",
|
|
12
|
-
"output:worker": "npm run output:worker:
|
|
13
|
-
"output:worker:watch": "npx nodemon --watch src --
|
|
12
|
+
"output:worker": "npm run output:worker:build && npm run output:worker:start",
|
|
13
|
+
"output:worker:watch": "npx nodemon --watch src --ext ts,js,json,prompt,md --ignore 'dist/**' --ignore '**/*.spec.*' --ignore '**/*.test.*' --exec 'npm run output:worker'",
|
|
14
14
|
"output:dev": "output dev"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
package/dist/utils/env_loader.js
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { existsSync } from 'node:fs';
|
|
7
7
|
import { resolve } from 'node:path';
|
|
8
|
-
import * as dotenv from 'dotenv';
|
|
9
8
|
import debugFactory from 'debug';
|
|
10
9
|
const debug = debugFactory('output-cli:env-loader');
|
|
11
10
|
export function loadEnvironment() {
|
|
@@ -17,5 +16,10 @@ export function loadEnvironment() {
|
|
|
17
16
|
return;
|
|
18
17
|
}
|
|
19
18
|
debug(`Loading env from: ${envPath}`);
|
|
20
|
-
|
|
19
|
+
try {
|
|
20
|
+
process.loadEnvFile(envPath);
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
debug(`Warning: Failed to load env file ${envPath}: ${err}`);
|
|
24
|
+
}
|
|
21
25
|
}
|
|
@@ -1,43 +1,72 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { resolve } from 'node:path';
|
|
7
|
-
import * as dotenv from 'dotenv';
|
|
8
|
-
vi.mock('node:fs');
|
|
9
|
-
vi.mock('dotenv');
|
|
1
|
+
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { loadEnvironment } from './env_loader.js';
|
|
10
6
|
describe('loadEnvironment', () => {
|
|
11
|
-
const
|
|
12
|
-
const mockCwd = '/mock/project';
|
|
7
|
+
const mockCwd = mkdtempSync(join(tmpdir(), 'output-env-loader-'));
|
|
13
8
|
beforeEach(() => {
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
for (const name of readdirSync(mockCwd)) {
|
|
10
|
+
rmSync(join(mockCwd, name), { recursive: true, force: true });
|
|
11
|
+
}
|
|
16
12
|
vi.spyOn(process, 'cwd').mockReturnValue(mockCwd);
|
|
17
|
-
vi.
|
|
18
|
-
vi.
|
|
13
|
+
vi.stubEnv('OUTPUT_CLI_ENV', undefined);
|
|
14
|
+
vi.stubEnv('OUTPUT_API_URL', undefined);
|
|
15
|
+
vi.stubEnv('OUTPUT_API_TOKEN', undefined);
|
|
19
16
|
});
|
|
20
17
|
afterEach(() => {
|
|
21
|
-
process.env = { ...originalEnv };
|
|
22
18
|
vi.restoreAllMocks();
|
|
19
|
+
vi.unstubAllEnvs();
|
|
23
20
|
});
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
21
|
+
afterAll(() => {
|
|
22
|
+
rmSync(mockCwd, { recursive: true, force: true });
|
|
23
|
+
});
|
|
24
|
+
it('loads variables from OUTPUT_CLI_ENV', () => {
|
|
25
|
+
writeFileSync(join(mockCwd, '.env'), [
|
|
26
|
+
'OUTPUT_API_URL=https://default.api.com',
|
|
27
|
+
'OUTPUT_API_TOKEN=default-token'
|
|
28
|
+
].join('\n'));
|
|
29
|
+
writeFileSync(join(mockCwd, '.env.mock'), [
|
|
30
|
+
'OUTPUT_API_URL=https://mock.api.com',
|
|
31
|
+
'OUTPUT_API_TOKEN=mock-token'
|
|
32
|
+
].join('\n'));
|
|
33
|
+
process.env.OUTPUT_CLI_ENV = '.env.mock';
|
|
34
|
+
loadEnvironment();
|
|
35
|
+
expect(process.env.OUTPUT_API_URL).toBe('https://mock.api.com');
|
|
36
|
+
expect(process.env.OUTPUT_API_TOKEN).toBe('mock-token');
|
|
37
|
+
});
|
|
38
|
+
it('loads variables from .env by default', () => {
|
|
39
|
+
writeFileSync(join(mockCwd, '.env'), [
|
|
40
|
+
'OUTPUT_API_URL=https://default.api.com',
|
|
41
|
+
'OUTPUT_API_TOKEN=default-token'
|
|
42
|
+
].join('\n'));
|
|
43
|
+
writeFileSync(join(mockCwd, '.env.mock'), [
|
|
44
|
+
'OUTPUT_API_URL=https://mock.api.com',
|
|
45
|
+
'OUTPUT_API_TOKEN=mock-token'
|
|
46
|
+
].join('\n'));
|
|
30
47
|
loadEnvironment();
|
|
31
|
-
expect(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
48
|
+
expect(process.env.OUTPUT_API_URL).toBe('https://default.api.com');
|
|
49
|
+
expect(process.env.OUTPUT_API_TOKEN).toBe('default-token');
|
|
50
|
+
});
|
|
51
|
+
it('does nothing when the env file is missing', () => {
|
|
52
|
+
expect(() => loadEnvironment()).not.toThrow();
|
|
53
|
+
expect(process.env.OUTPUT_API_URL).toBeUndefined();
|
|
54
|
+
expect(process.env.OUTPUT_API_TOKEN).toBeUndefined();
|
|
55
|
+
});
|
|
56
|
+
it('does not throw when the env path is not a readable file', () => {
|
|
57
|
+
mkdirSync(join(mockCwd, 'not-a-file.env'));
|
|
58
|
+
process.env.OUTPUT_CLI_ENV = 'not-a-file.env';
|
|
59
|
+
expect(() => loadEnvironment()).not.toThrow();
|
|
60
|
+
expect(process.env.OUTPUT_API_URL).toBeUndefined();
|
|
61
|
+
});
|
|
62
|
+
it('does not overwrite already-set process.env values', () => {
|
|
63
|
+
vi.stubEnv('OUTPUT_API_URL', 'https://ambient.api.com');
|
|
64
|
+
writeFileSync(join(mockCwd, '.env'), [
|
|
65
|
+
'OUTPUT_API_URL=https://file.api.com',
|
|
66
|
+
'OUTPUT_API_TOKEN=file-token'
|
|
67
|
+
].join('\n'));
|
|
39
68
|
loadEnvironment();
|
|
40
|
-
expect(
|
|
41
|
-
expect(
|
|
69
|
+
expect(process.env.OUTPUT_API_URL).toBe('https://ambient.api.com');
|
|
70
|
+
expect(process.env.OUTPUT_API_TOKEN).toBe('file-token');
|
|
42
71
|
});
|
|
43
72
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { WorkflowResultResponse,
|
|
1
|
+
import type { WorkflowResultResponse, WorkflowResultStatus } from '../api/generated/api.js';
|
|
2
2
|
type WorkflowResult = Pick<WorkflowResultResponse, 'workflowId' | 'output' | 'status' | 'error'>;
|
|
3
|
-
export declare const ERROR_STATUSES: ReadonlySet<
|
|
3
|
+
export declare const ERROR_STATUSES: ReadonlySet<WorkflowResultStatus>;
|
|
4
|
+
export declare const isErrorWorkflowStatus: (status: string | null | undefined) => boolean;
|
|
4
5
|
export declare const TERMINAL_STATUSES: ReadonlySet<string>;
|
|
5
6
|
export declare function formatWorkflowResult(result: WorkflowResult): string;
|
|
6
7
|
export {};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { normalizeWorkflowStatus } from './normalize_workflow_status.js';
|
|
2
|
-
export const ERROR_STATUSES = new Set(['failed', '
|
|
2
|
+
export const ERROR_STATUSES = new Set(['failed', 'cancelled', 'terminated', 'timed_out']);
|
|
3
|
+
export const isErrorWorkflowStatus = (status) => ERROR_STATUSES.has(normalizeWorkflowStatus(status));
|
|
3
4
|
// Every error status plus the one success status — derived so the two sets can't
|
|
4
5
|
// silently drift apart as error statuses evolve. Shared by `workflow monitor` and
|
|
5
6
|
// the dev TUI's `useRunDetail`/`useStepGraph` so both agree on what "done" means.
|
|
@@ -17,7 +18,10 @@ export function formatWorkflowResult(result) {
|
|
|
17
18
|
else {
|
|
18
19
|
lines.push(`Status: ${status || 'unknown'}`);
|
|
19
20
|
if (result.error) {
|
|
20
|
-
|
|
21
|
+
const error = typeof result.error === 'string' ?
|
|
22
|
+
result.error :
|
|
23
|
+
result.error.message ?? JSON.stringify(result.error, null, 2);
|
|
24
|
+
lines.push(`Error: ${error}`);
|
|
21
25
|
}
|
|
22
26
|
}
|
|
23
27
|
return lines.join('\n');
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { formatWorkflowResult } from './format_workflow_result.js';
|
|
2
|
+
import { formatWorkflowResult, isErrorWorkflowStatus } from './format_workflow_result.js';
|
|
3
3
|
describe('formatWorkflowResult', () => {
|
|
4
4
|
it('should display output for completed workflows', () => {
|
|
5
5
|
const result = formatWorkflowResult({
|
|
@@ -13,7 +13,7 @@ describe('formatWorkflowResult', () => {
|
|
|
13
13
|
expect(result).toContain('"values"');
|
|
14
14
|
expect(result).not.toContain('Status:');
|
|
15
15
|
});
|
|
16
|
-
it('should display
|
|
16
|
+
it('should display legacy string errors for failed workflows', () => {
|
|
17
17
|
const result = formatWorkflowResult({
|
|
18
18
|
workflowId: 'wf-456',
|
|
19
19
|
status: 'failed',
|
|
@@ -25,6 +25,29 @@ describe('formatWorkflowResult', () => {
|
|
|
25
25
|
expect(result).toContain('Error: Activity task failed');
|
|
26
26
|
expect(result).not.toContain('Output:');
|
|
27
27
|
});
|
|
28
|
+
it('should display the message from structured errors', () => {
|
|
29
|
+
const result = formatWorkflowResult({
|
|
30
|
+
workflowId: 'wf-v2',
|
|
31
|
+
status: 'failed',
|
|
32
|
+
output: null,
|
|
33
|
+
error: {
|
|
34
|
+
name: 'ValidationError',
|
|
35
|
+
message: 'Input is invalid',
|
|
36
|
+
code: 'INVALID_INPUT'
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
expect(result).toContain('Error: Input is invalid');
|
|
40
|
+
expect(result).not.toContain('[object Object]');
|
|
41
|
+
});
|
|
42
|
+
it('should serialize structured errors without a message', () => {
|
|
43
|
+
const result = formatWorkflowResult({
|
|
44
|
+
workflowId: 'wf-v2',
|
|
45
|
+
status: 'failed',
|
|
46
|
+
output: null,
|
|
47
|
+
error: { code: 'UNKNOWN' }
|
|
48
|
+
});
|
|
49
|
+
expect(result).toContain('"code": "UNKNOWN"');
|
|
50
|
+
});
|
|
28
51
|
it('should display status for terminated workflows', () => {
|
|
29
52
|
const result = formatWorkflowResult({
|
|
30
53
|
workflowId: 'wf-term',
|
|
@@ -35,15 +58,25 @@ describe('formatWorkflowResult', () => {
|
|
|
35
58
|
expect(result).toContain('Status: terminated');
|
|
36
59
|
expect(result).toContain('Error: Workflow terminated by user');
|
|
37
60
|
});
|
|
38
|
-
it('should display status for
|
|
61
|
+
it('should display status for cancelled workflows', () => {
|
|
39
62
|
const result = formatWorkflowResult({
|
|
63
|
+
workflowId: 'wf-cancel',
|
|
64
|
+
status: 'cancelled',
|
|
65
|
+
output: null,
|
|
66
|
+
error: 'Workflow was cancelled'
|
|
67
|
+
});
|
|
68
|
+
expect(result).toContain('Status: cancelled');
|
|
69
|
+
expect(result).toContain('Error: Workflow was cancelled');
|
|
70
|
+
});
|
|
71
|
+
it('normalizes canceled responses without changing the current status type', () => {
|
|
72
|
+
const legacyResult = {
|
|
40
73
|
workflowId: 'wf-cancel',
|
|
41
74
|
status: 'canceled',
|
|
42
75
|
output: null,
|
|
43
76
|
error: 'Workflow was canceled'
|
|
44
|
-
}
|
|
45
|
-
expect(
|
|
46
|
-
expect(
|
|
77
|
+
};
|
|
78
|
+
expect(formatWorkflowResult(legacyResult)).toContain('Status: cancelled');
|
|
79
|
+
expect(isErrorWorkflowStatus('canceled')).toBe(true);
|
|
47
80
|
});
|
|
48
81
|
it('should display status without error line for continued_as_new workflows', () => {
|
|
49
82
|
const result = formatWorkflowResult({
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Normalizes statuses from earlier API contracts.
|
|
3
|
+
*
|
|
4
|
+
* This can be removed after Aug, 2026
|
|
4
5
|
*
|
|
5
6
|
* @param status - Workflow status from the API
|
|
6
7
|
* @returns Normalized workflow status
|
|
7
8
|
*/
|
|
8
|
-
export declare const normalizeWorkflowStatus: <T extends string | null | undefined>(status: T) => T | "continued_as_new";
|
|
9
|
+
export declare const normalizeWorkflowStatus: <T extends string | null | undefined>(status: T) => T | "continued_as_new" | "cancelled";
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Normalizes statuses from earlier API contracts.
|
|
3
|
+
*
|
|
4
|
+
* This can be removed after Aug, 2026
|
|
4
5
|
*
|
|
5
6
|
* @param status - Workflow status from the API
|
|
6
7
|
* @returns Normalized workflow status
|
|
7
8
|
*/
|
|
8
|
-
export const normalizeWorkflowStatus = (status) =>
|
|
9
|
+
export const normalizeWorkflowStatus = (status) => {
|
|
10
|
+
if (status === 'continued') {
|
|
11
|
+
return 'continued_as_new';
|
|
12
|
+
}
|
|
13
|
+
if (status === 'canceled') {
|
|
14
|
+
return 'cancelled';
|
|
15
|
+
}
|
|
16
|
+
return status;
|
|
17
|
+
};
|
|
@@ -4,6 +4,9 @@ describe('normalizeWorkflowStatus', () => {
|
|
|
4
4
|
it('temporarily maps continued to continued_as_new', () => {
|
|
5
5
|
expect(normalizeWorkflowStatus('continued')).toBe('continued_as_new');
|
|
6
6
|
});
|
|
7
|
+
it('maps the previous canceled spelling to cancelled', () => {
|
|
8
|
+
expect(normalizeWorkflowStatus('canceled')).toBe('cancelled');
|
|
9
|
+
});
|
|
7
10
|
it('leaves other statuses and nullish values unchanged', () => {
|
|
8
11
|
expect(normalizeWorkflowStatus('completed')).toBe('completed');
|
|
9
12
|
expect(normalizeWorkflowStatus('continued_as_new')).toBe('continued_as_new');
|
|
@@ -1 +1,9 @@
|
|
|
1
|
-
export
|
|
1
|
+
export type ResolveInputOptions = {
|
|
2
|
+
workflowName: string;
|
|
3
|
+
scenario?: string;
|
|
4
|
+
inputFlag?: string;
|
|
5
|
+
commandName: string;
|
|
6
|
+
catalog?: string;
|
|
7
|
+
json?: boolean;
|
|
8
|
+
};
|
|
9
|
+
export declare function resolveInput(options: ResolveInputOptions): Promise<unknown>;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { ux } from '@oclif/core';
|
|
2
2
|
import { parseInputFlag } from '#utils/input_parser.js';
|
|
3
3
|
import { resolveScenarioPath, getScenarioNotFoundMessage } from '#utils/scenario_resolver.js';
|
|
4
|
-
export async function resolveInput(
|
|
4
|
+
export async function resolveInput(options) {
|
|
5
|
+
const { workflowName, scenario, inputFlag, commandName, catalog, json } = options;
|
|
5
6
|
if (inputFlag && scenario) {
|
|
6
7
|
return ux.error('Cannot use both scenario argument and --input flag. Choose one.', { exit: 1 });
|
|
7
8
|
}
|
|
@@ -13,7 +14,12 @@ export async function resolveInput(workflowName, scenario, inputFlag, commandNam
|
|
|
13
14
|
if (!resolution.found) {
|
|
14
15
|
return ux.error(getScenarioNotFoundMessage(workflowName, scenario, resolution.searchedPaths), { exit: 1 });
|
|
15
16
|
}
|
|
16
|
-
|
|
17
|
+
// Advisory notice goes to stderr so stdout stays clean for piping, and is
|
|
18
|
+
// skipped entirely under --json where even stderr is noise to a script
|
|
19
|
+
// consuming the structured output (same rule as the init hook's banner).
|
|
20
|
+
if (!json) {
|
|
21
|
+
ux.stderr(`Using scenario: ${resolution.path}`);
|
|
22
|
+
}
|
|
17
23
|
return parseInputFlag(resolution.path);
|
|
18
24
|
}
|
|
19
25
|
return ux.error('Input required. Provide either:\n' +
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
3
|
+
vi.mock('@oclif/core', () => ({
|
|
4
|
+
ux: {
|
|
5
|
+
stdout: vi.fn(),
|
|
6
|
+
stderr: vi.fn(),
|
|
7
|
+
error: vi.fn(() => {
|
|
8
|
+
throw new Error('ux.error called');
|
|
9
|
+
})
|
|
10
|
+
}
|
|
11
|
+
}));
|
|
12
|
+
vi.mock('#utils/input_parser.js', () => ({
|
|
13
|
+
parseInputFlag: vi.fn()
|
|
14
|
+
}));
|
|
15
|
+
vi.mock('#utils/scenario_resolver.js', () => ({
|
|
16
|
+
resolveScenarioPath: vi.fn(),
|
|
17
|
+
getScenarioNotFoundMessage: vi.fn()
|
|
18
|
+
}));
|
|
19
|
+
describe('resolveInput', () => {
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
vi.clearAllMocks();
|
|
22
|
+
});
|
|
23
|
+
it('emits the scenario notice on stderr so --json stdout stays clean', async () => {
|
|
24
|
+
const { ux } = await import('@oclif/core');
|
|
25
|
+
const { parseInputFlag } = await import('#utils/input_parser.js');
|
|
26
|
+
const { resolveScenarioPath } = await import('#utils/scenario_resolver.js');
|
|
27
|
+
const { resolveInput } = await import('./resolve_input.js');
|
|
28
|
+
vi.mocked(resolveScenarioPath).mockResolvedValue({
|
|
29
|
+
found: true,
|
|
30
|
+
path: '/scenarios/happy_path.json'
|
|
31
|
+
});
|
|
32
|
+
vi.mocked(parseInputFlag).mockReturnValue({ key: 'value' });
|
|
33
|
+
const result = await resolveInput({
|
|
34
|
+
workflowName: 'web_search',
|
|
35
|
+
scenario: 'happy_path',
|
|
36
|
+
commandName: 'start'
|
|
37
|
+
});
|
|
38
|
+
expect(result).toEqual({ key: 'value' });
|
|
39
|
+
expect(ux.stderr).toHaveBeenCalledWith('Using scenario: /scenarios/happy_path.json');
|
|
40
|
+
expect(ux.stdout).not.toHaveBeenCalled();
|
|
41
|
+
});
|
|
42
|
+
it('suppresses the scenario notice entirely under --json', async () => {
|
|
43
|
+
const { ux } = await import('@oclif/core');
|
|
44
|
+
const { parseInputFlag } = await import('#utils/input_parser.js');
|
|
45
|
+
const { resolveScenarioPath } = await import('#utils/scenario_resolver.js');
|
|
46
|
+
const { resolveInput } = await import('./resolve_input.js');
|
|
47
|
+
vi.mocked(resolveScenarioPath).mockResolvedValue({
|
|
48
|
+
found: true,
|
|
49
|
+
path: '/scenarios/happy_path.json'
|
|
50
|
+
});
|
|
51
|
+
vi.mocked(parseInputFlag).mockReturnValue({ key: 'value' });
|
|
52
|
+
const result = await resolveInput({
|
|
53
|
+
workflowName: 'web_search',
|
|
54
|
+
scenario: 'happy_path',
|
|
55
|
+
commandName: 'start',
|
|
56
|
+
json: true
|
|
57
|
+
});
|
|
58
|
+
expect(result).toEqual({ key: 'value' });
|
|
59
|
+
expect(ux.stderr).not.toHaveBeenCalled();
|
|
60
|
+
expect(ux.stdout).not.toHaveBeenCalled();
|
|
61
|
+
});
|
|
62
|
+
it('does not emit the scenario notice when input comes from --input', async () => {
|
|
63
|
+
const { ux } = await import('@oclif/core');
|
|
64
|
+
const { parseInputFlag } = await import('#utils/input_parser.js');
|
|
65
|
+
const { resolveInput } = await import('./resolve_input.js');
|
|
66
|
+
vi.mocked(parseInputFlag).mockReturnValue({ key: 'value' });
|
|
67
|
+
await resolveInput({
|
|
68
|
+
workflowName: 'web_search',
|
|
69
|
+
inputFlag: '{"key":"value"}',
|
|
70
|
+
commandName: 'start'
|
|
71
|
+
});
|
|
72
|
+
expect(ux.stderr).not.toHaveBeenCalled();
|
|
73
|
+
expect(ux.stdout).not.toHaveBeenCalled();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -4,7 +4,7 @@ const WORKFLOW_STATUS_MAP = {
|
|
|
4
4
|
running: { icon: '●', color: 'yellow' },
|
|
5
5
|
completed: { icon: '●', color: 'green' },
|
|
6
6
|
failed: { icon: '✗', color: 'red' },
|
|
7
|
-
|
|
7
|
+
cancelled: { icon: '○', color: 'gray' },
|
|
8
8
|
terminated: { icon: '✗', color: 'gray' },
|
|
9
9
|
timed_out: { icon: '✗', color: 'red' },
|
|
10
10
|
continued_as_new: { icon: '↻', color: 'blue' }
|
|
@@ -73,7 +73,7 @@ const readTraceLog = async (source) => {
|
|
|
73
73
|
return JSON.parse(content);
|
|
74
74
|
};
|
|
75
75
|
// Run detail and trace fetches are best-effort. Many statuses (in-progress,
|
|
76
|
-
// failed,
|
|
76
|
+
// failed, cancelled) don't have a fully-formed result or trace available at
|
|
77
77
|
// any given moment, and that's expected — the caller falls back to
|
|
78
78
|
// EMPTY_DETAIL and the UI renders whatever's there. Swallow everything.
|
|
79
79
|
const fetchTrace = async (workflowId, runId) => {
|
|
@@ -4,7 +4,7 @@ describe('isTerminalRunStatus', () => {
|
|
|
4
4
|
it('returns true for completed states', () => {
|
|
5
5
|
expect(isTerminalRunStatus('completed')).toBe(true);
|
|
6
6
|
expect(isTerminalRunStatus('failed')).toBe(true);
|
|
7
|
-
expect(isTerminalRunStatus('
|
|
7
|
+
expect(isTerminalRunStatus('cancelled')).toBe(true);
|
|
8
8
|
expect(isTerminalRunStatus('terminated')).toBe(true);
|
|
9
9
|
expect(isTerminalRunStatus('timed_out')).toBe(true);
|
|
10
10
|
});
|
|
@@ -21,7 +21,7 @@ const STATUS_ORDER = {
|
|
|
21
21
|
failed: 1,
|
|
22
22
|
timed_out: 2,
|
|
23
23
|
terminated: 3,
|
|
24
|
-
|
|
24
|
+
cancelled: 4,
|
|
25
25
|
continued_as_new: 5,
|
|
26
26
|
completed: 6
|
|
27
27
|
};
|
|
@@ -110,7 +110,7 @@ const DetailPane = ({ run, pane, rows }) => {
|
|
|
110
110
|
}
|
|
111
111
|
return _jsx(Text, { dimColor: true, children: "\u2014" });
|
|
112
112
|
}
|
|
113
|
-
if (activePane === 'output' &&
|
|
113
|
+
if (activePane === 'output' && typeof pane.error === 'string') {
|
|
114
114
|
const lines = String(pane.error).split('\n').slice(0, tabContentRows);
|
|
115
115
|
return (_jsx(Box, { flexDirection: "column", children: lines.map((line, i) => (_jsx(Text, { color: "red", wrap: "truncate-end", children: line }, i))) }));
|
|
116
116
|
}
|
package/oclif.manifest.json
CHANGED
|
@@ -1236,9 +1236,17 @@
|
|
|
1236
1236
|
"<%= config.bin %> <%= command.id %> simple basic_input",
|
|
1237
1237
|
"<%= config.bin %> <%= command.id %> simple --input '{\"values\":[1,2,3]}'",
|
|
1238
1238
|
"<%= config.bin %> <%= command.id %> simple --input input.json",
|
|
1239
|
-
"<%= config.bin %> <%= command.id %> simple --input '{\"key\":\"value\"}' --catalog my-catalog"
|
|
1239
|
+
"<%= config.bin %> <%= command.id %> simple --input '{\"key\":\"value\"}' --catalog my-catalog",
|
|
1240
|
+
"<%= config.bin %> <%= command.id %> simple --json"
|
|
1240
1241
|
],
|
|
1241
1242
|
"flags": {
|
|
1243
|
+
"json": {
|
|
1244
|
+
"description": "Format output as json.",
|
|
1245
|
+
"helpGroup": "GLOBAL",
|
|
1246
|
+
"name": "json",
|
|
1247
|
+
"allowNo": false,
|
|
1248
|
+
"type": "boolean"
|
|
1249
|
+
},
|
|
1242
1250
|
"input": {
|
|
1243
1251
|
"char": "i",
|
|
1244
1252
|
"description": "Workflow input as JSON string or file path (overrides scenario)",
|
|
@@ -1272,7 +1280,7 @@
|
|
|
1272
1280
|
"pluginName": "@outputai/cli",
|
|
1273
1281
|
"pluginType": "core",
|
|
1274
1282
|
"strict": true,
|
|
1275
|
-
"enableJsonFlag":
|
|
1283
|
+
"enableJsonFlag": true,
|
|
1276
1284
|
"isESM": true,
|
|
1277
1285
|
"relativePath": [
|
|
1278
1286
|
"dist",
|
|
@@ -1390,10 +1398,8 @@
|
|
|
1390
1398
|
"terminate.js"
|
|
1391
1399
|
]
|
|
1392
1400
|
},
|
|
1393
|
-
"workflow:
|
|
1394
|
-
"aliases": [
|
|
1395
|
-
"workflow:test"
|
|
1396
|
-
],
|
|
1401
|
+
"workflow:test": {
|
|
1402
|
+
"aliases": [],
|
|
1397
1403
|
"args": {
|
|
1398
1404
|
"workflowName": {
|
|
1399
1405
|
"description": "Name of the workflow to test",
|
|
@@ -1462,7 +1468,7 @@
|
|
|
1462
1468
|
},
|
|
1463
1469
|
"hasDynamicHelp": false,
|
|
1464
1470
|
"hiddenAliases": [],
|
|
1465
|
-
"id": "workflow:
|
|
1471
|
+
"id": "workflow:test",
|
|
1466
1472
|
"pluginAlias": "@outputai/cli",
|
|
1467
1473
|
"pluginName": "@outputai/cli",
|
|
1468
1474
|
"pluginType": "core",
|
|
@@ -1473,7 +1479,7 @@
|
|
|
1473
1479
|
"dist",
|
|
1474
1480
|
"commands",
|
|
1475
1481
|
"workflow",
|
|
1476
|
-
"
|
|
1482
|
+
"test.js"
|
|
1477
1483
|
]
|
|
1478
1484
|
},
|
|
1479
1485
|
"workflow:dataset:generate": {
|
|
@@ -1707,5 +1713,5 @@
|
|
|
1707
1713
|
]
|
|
1708
1714
|
}
|
|
1709
1715
|
},
|
|
1710
|
-
"version": "0.10.1-next.
|
|
1716
|
+
"version": "0.10.1-next.bf95a6f.0"
|
|
1711
1717
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@outputai/cli",
|
|
3
|
-
"version": "0.10.1-next.
|
|
3
|
+
"version": "0.10.1-next.bf95a6f.0",
|
|
4
4
|
"description": "CLI for Output.ai workflow generation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -27,20 +27,18 @@
|
|
|
27
27
|
"cli-table3": "0.6.5",
|
|
28
28
|
"date-fns": "4.1.0",
|
|
29
29
|
"debug": "4.4.3",
|
|
30
|
-
"dotenv": "17.4.2",
|
|
31
30
|
"handlebars": "4.7.9",
|
|
32
31
|
"ink": "7.0.1",
|
|
33
32
|
"ink-spinner": "5.0.0",
|
|
34
|
-
"js-yaml": "4.
|
|
33
|
+
"js-yaml": "4.3.1",
|
|
35
34
|
"json-schema-library": "11.4.0",
|
|
36
35
|
"ky": "2.0.2",
|
|
37
36
|
"react": "19.2.5",
|
|
38
37
|
"semver": "7.7.4",
|
|
39
|
-
"undici": "8.
|
|
40
|
-
"
|
|
41
|
-
"@outputai/credentials": "0.10.1-next.
|
|
42
|
-
"@outputai/
|
|
43
|
-
"@outputai/llm": "0.10.1-next.be4ec7f.0"
|
|
38
|
+
"undici": "8.9.0",
|
|
39
|
+
"@outputai/evals": "0.10.1-next.bf95a6f.0",
|
|
40
|
+
"@outputai/credentials": "0.10.1-next.bf95a6f.0",
|
|
41
|
+
"@outputai/llm": "0.10.1-next.bf95a6f.0"
|
|
44
42
|
},
|
|
45
43
|
"devDependencies": {
|
|
46
44
|
"@types/cli-progress": "3.11.6",
|
|
File without changes
|