@outputai/cli 0.9.3-next.14a0cfc.0 → 0.9.3-next.5289bca.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/assets/docker/docker-compose-dev.yml +1 -1
- package/dist/commands/workflow/dataset/generate.d.ts +2 -1
- package/dist/commands/workflow/dataset/generate.js +42 -21
- package/dist/commands/workflow/dataset/generate.spec.js +116 -11
- package/dist/commands/workflow/runs/list.js +6 -3
- package/dist/commands/workflow/runs/list.spec.d.ts +1 -0
- package/dist/commands/workflow/runs/list.spec.js +45 -0
- package/dist/config.d.ts +0 -6
- package/dist/config.js +1 -9
- package/dist/config.spec.js +1 -17
- package/dist/generated/framework_version.json +1 -1
- package/dist/services/datasets.d.ts +1 -0
- package/dist/services/datasets.js +7 -0
- package/dist/services/datasets.test.js +10 -1
- package/dist/services/workflow_runs.d.ts +4 -1
- package/dist/services/workflow_runs.js +3 -1
- package/dist/services/workflow_runs.spec.d.ts +1 -0
- package/dist/services/workflow_runs.spec.js +39 -0
- package/dist/views/dev/components/run_info_sidebar.js +1 -1
- package/dist/views/dev/panels/runs_panel.js +2 -2
- package/oclif.manifest.json +79 -79
- package/package.json +4 -5
- package/dist/services/s3_trace_downloader.d.ts +0 -12
- package/dist/services/s3_trace_downloader.js +0 -57
|
@@ -17,7 +17,8 @@ export default class DatasetGenerate extends Command {
|
|
|
17
17
|
run(): Promise<void>;
|
|
18
18
|
private generateFromScenario;
|
|
19
19
|
private generateFromTrace;
|
|
20
|
-
private
|
|
20
|
+
private generateFromRuns;
|
|
21
|
+
private generateFromRun;
|
|
21
22
|
private resolveScenarioInput;
|
|
22
23
|
catch(error: Error): Promise<void>;
|
|
23
24
|
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
2
|
import { Args, Command, Flags, ux } from '@oclif/core';
|
|
4
3
|
import { postWorkflowRun } from '#api/generated/api.js';
|
|
5
|
-
import { writeDataset, resolveDefaultDatasetsDir, buildDataset, getExecutionTime, extractDatasetName } from '#services/datasets.js';
|
|
6
|
-
import {
|
|
4
|
+
import { writeDataset, resolveDefaultDatasetsDir, buildDataset, getExecutionTime, extractDatasetName, datasetFilePath } from '#services/datasets.js';
|
|
5
|
+
import { fetchWorkflowRuns } from '#services/workflow_runs.js';
|
|
6
|
+
import { getTrace } from '#services/trace_reader.js';
|
|
7
7
|
import { extractDatasetFromTrace } from '#utils/trace_extractor.js';
|
|
8
8
|
import { resolveScenarioPath, getScenarioNotFoundMessage } from '#utils/scenario_resolver.js';
|
|
9
9
|
import { parseInputFlag } from '#utils/input_parser.js';
|
|
10
10
|
import { handleApiError } from '#utils/error_handler.js';
|
|
11
11
|
export default class DatasetGenerate extends Command {
|
|
12
|
-
static description = 'Generate a dataset for a workflow from a scenario, trace file, or
|
|
12
|
+
static description = 'Generate a dataset for a workflow from a scenario, trace file, or recent runs';
|
|
13
13
|
static examples = [
|
|
14
14
|
'<%= config.bin %> <%= command.id %> simple basic_input',
|
|
15
15
|
'<%= config.bin %> <%= command.id %> simple --trace logs/runs/simple/trace.json --name edge_case',
|
|
@@ -45,13 +45,13 @@ export default class DatasetGenerate extends Command {
|
|
|
45
45
|
}),
|
|
46
46
|
download: Flags.boolean({
|
|
47
47
|
char: 'd',
|
|
48
|
-
description: '
|
|
48
|
+
description: 'Generate datasets from recent workflow runs fetched via the Output API',
|
|
49
49
|
default: false,
|
|
50
50
|
exclusive: ['trace']
|
|
51
51
|
}),
|
|
52
52
|
limit: Flags.integer({
|
|
53
53
|
char: 'l',
|
|
54
|
-
description: 'Maximum number of
|
|
54
|
+
description: 'Maximum number of recent runs to fetch',
|
|
55
55
|
default: 5
|
|
56
56
|
}),
|
|
57
57
|
input: Flags.string({
|
|
@@ -62,7 +62,7 @@ export default class DatasetGenerate extends Command {
|
|
|
62
62
|
async run() {
|
|
63
63
|
const { args, flags } = await this.parse(DatasetGenerate);
|
|
64
64
|
if (flags.download) {
|
|
65
|
-
await this.
|
|
65
|
+
await this.generateFromRuns(args.workflowName, flags.limit, flags.catalog);
|
|
66
66
|
return;
|
|
67
67
|
}
|
|
68
68
|
if (flags.trace) {
|
|
@@ -89,7 +89,7 @@ export default class DatasetGenerate extends Command {
|
|
|
89
89
|
const executionTimeMs = await getExecutionTime(workflowId);
|
|
90
90
|
const dataset = buildDataset(datasetName, resolvedInput, output, executionTimeMs);
|
|
91
91
|
const dir = await resolveDefaultDatasetsDir(workflowName);
|
|
92
|
-
const filePath =
|
|
92
|
+
const filePath = datasetFilePath(dir, datasetName);
|
|
93
93
|
await writeDataset(dataset, filePath);
|
|
94
94
|
this.log(`Dataset saved: ${filePath}`);
|
|
95
95
|
}
|
|
@@ -101,29 +101,50 @@ export default class DatasetGenerate extends Command {
|
|
|
101
101
|
const datasetName = nameOverride ?? extractDatasetName(tracePath);
|
|
102
102
|
const dataset = buildDataset(datasetName, extracted.input, extracted.output, extracted.executionTimeMs);
|
|
103
103
|
const dir = await resolveDefaultDatasetsDir(workflowName);
|
|
104
|
-
const filePath =
|
|
104
|
+
const filePath = datasetFilePath(dir, datasetName);
|
|
105
105
|
await writeDataset(dataset, filePath);
|
|
106
106
|
this.log(`Dataset saved: ${filePath}`);
|
|
107
107
|
}
|
|
108
|
-
async
|
|
109
|
-
this.log(`
|
|
110
|
-
const
|
|
111
|
-
if (
|
|
112
|
-
|
|
108
|
+
async generateFromRuns(workflowName, limit, catalog) {
|
|
109
|
+
this.log(`Fetching recent runs for "${workflowName}"...`);
|
|
110
|
+
const { runs, skipped } = await fetchWorkflowRuns({ workflowType: workflowName, catalog, limit });
|
|
111
|
+
if (runs.length === 0) {
|
|
112
|
+
if (skipped > 0) {
|
|
113
|
+
this.error(`Found ${skipped} run(s) but none had a workflow ID.`, { exit: 1 });
|
|
114
|
+
}
|
|
115
|
+
this.log('No recent runs found.');
|
|
113
116
|
return;
|
|
114
117
|
}
|
|
115
|
-
|
|
118
|
+
if (skipped > 0) {
|
|
119
|
+
this.warn(`Skipping ${skipped} run(s) with no workflow ID.`);
|
|
120
|
+
}
|
|
121
|
+
// The trace-log endpoint always targets a workflow's latest run, so distinct
|
|
122
|
+
// runIds sharing one workflowId (continue-as-new, reset) collapse to one dataset.
|
|
123
|
+
const workflowIds = [...new Set(runs.map(run => run.workflowId))];
|
|
124
|
+
this.log(`Found ${workflowIds.length} run(s). Fetching traces...`);
|
|
116
125
|
const dir = await resolveDefaultDatasetsDir(workflowName);
|
|
117
|
-
|
|
118
|
-
|
|
126
|
+
const generated = (await Promise.all(workflowIds.map(id => this.generateFromRun(id, dir))))
|
|
127
|
+
.filter(Boolean).length;
|
|
128
|
+
this.log(`\nGenerated ${generated} dataset(s)`);
|
|
129
|
+
if (generated === 0) {
|
|
130
|
+
this.error(`Failed to generate any datasets from ${workflowIds.length} run(s).`, { exit: 1 });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async generateFromRun(workflowId, dir) {
|
|
134
|
+
try {
|
|
135
|
+
const { data: traceData } = await getTrace(workflowId);
|
|
119
136
|
const extracted = extractDatasetFromTrace(traceData);
|
|
120
|
-
const
|
|
121
|
-
const
|
|
122
|
-
const filePath = join(dir, `${datasetName}.yml`);
|
|
137
|
+
const dataset = buildDataset(workflowId, extracted.input, extracted.output, extracted.executionTimeMs);
|
|
138
|
+
const filePath = datasetFilePath(dir, workflowId);
|
|
123
139
|
await writeDataset(dataset, filePath);
|
|
124
140
|
this.log(` Saved: ${filePath}`);
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
145
|
+
this.warn(` Skipped ${workflowId}: ${message}`);
|
|
146
|
+
return false;
|
|
125
147
|
}
|
|
126
|
-
this.log(`\nGenerated ${traces.length} dataset(s)`);
|
|
127
148
|
}
|
|
128
149
|
async resolveScenarioInput(workflowName, scenario, inputFlag, catalog) {
|
|
129
150
|
if (inputFlag && scenario) {
|
|
@@ -3,6 +3,15 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
3
3
|
vi.mock('#api/generated/api.js', () => ({
|
|
4
4
|
postWorkflowRun: vi.fn()
|
|
5
5
|
}));
|
|
6
|
+
vi.mock('#services/workflow_runs.js', () => ({
|
|
7
|
+
fetchWorkflowRuns: vi.fn()
|
|
8
|
+
}));
|
|
9
|
+
vi.mock('#services/trace_reader.js', () => ({
|
|
10
|
+
getTrace: vi.fn()
|
|
11
|
+
}));
|
|
12
|
+
vi.mock('#utils/trace_extractor.js', () => ({
|
|
13
|
+
extractDatasetFromTrace: vi.fn().mockReturnValue({ input: { a: 1 }, output: { ok: true }, executionTimeMs: 5 })
|
|
14
|
+
}));
|
|
6
15
|
vi.mock('#utils/scenario_resolver.js', () => ({
|
|
7
16
|
resolveScenarioPath: vi.fn(),
|
|
8
17
|
getScenarioNotFoundMessage: vi.fn().mockReturnValue('not found')
|
|
@@ -15,13 +24,25 @@ vi.mock('#services/datasets.js', () => ({
|
|
|
15
24
|
resolveDefaultDatasetsDir: vi.fn().mockResolvedValue('/datasets'),
|
|
16
25
|
buildDataset: vi.fn().mockReturnValue({ name: 'basic' }),
|
|
17
26
|
getExecutionTime: vi.fn().mockResolvedValue(100),
|
|
18
|
-
extractDatasetName: vi.fn()
|
|
27
|
+
extractDatasetName: vi.fn(),
|
|
28
|
+
datasetFilePath: vi.fn((dir, name) => `${dir}/${name.replace(/[^a-zA-Z0-9._-]/g, '_')}.yml`)
|
|
19
29
|
}));
|
|
20
30
|
describe('workflow dataset generate command', () => {
|
|
21
31
|
beforeEach(() => {
|
|
22
32
|
vi.clearAllMocks();
|
|
23
33
|
delete process.env.OUTPUT_CATALOG_ID;
|
|
24
34
|
});
|
|
35
|
+
const makeCmd = async (args, flags) => {
|
|
36
|
+
const DatasetGenerate = (await import('./generate.js')).default;
|
|
37
|
+
const cmd = new DatasetGenerate(['my_workflow'], {});
|
|
38
|
+
cmd.log = vi.fn();
|
|
39
|
+
cmd.warn = vi.fn();
|
|
40
|
+
cmd.error = vi.fn(() => {
|
|
41
|
+
throw new Error('error called');
|
|
42
|
+
});
|
|
43
|
+
cmd.parse = vi.fn().mockResolvedValue({ args, flags });
|
|
44
|
+
return cmd;
|
|
45
|
+
};
|
|
25
46
|
describe('command definition', () => {
|
|
26
47
|
it('binds the catalog flag to OUTPUT_CATALOG_ID', async () => {
|
|
27
48
|
const DatasetGenerate = (await import('./generate.js')).default;
|
|
@@ -32,19 +53,10 @@ describe('workflow dataset generate command', () => {
|
|
|
32
53
|
});
|
|
33
54
|
describe('run()', () => {
|
|
34
55
|
const createCommand = async (flagOverrides = {}) => {
|
|
35
|
-
const DatasetGenerate = (await import('./generate.js')).default;
|
|
36
56
|
const { postWorkflowRun } = await import('#api/generated/api.js');
|
|
37
57
|
const { resolveScenarioPath } = await import('#utils/scenario_resolver.js');
|
|
38
58
|
const { parseInputFlag } = await import('#utils/input_parser.js');
|
|
39
|
-
const cmd =
|
|
40
|
-
cmd.log = vi.fn();
|
|
41
|
-
cmd.error = vi.fn(() => {
|
|
42
|
-
throw new Error('error called');
|
|
43
|
-
});
|
|
44
|
-
cmd.parse = vi.fn().mockResolvedValue({
|
|
45
|
-
args: { workflowName: 'my_workflow', scenario: 'basic' },
|
|
46
|
-
flags: { catalog: undefined, trace: undefined, name: undefined, download: false, limit: 5, input: undefined, ...flagOverrides }
|
|
47
|
-
});
|
|
59
|
+
const cmd = await makeCmd({ workflowName: 'my_workflow', scenario: 'basic' }, { catalog: undefined, trace: undefined, name: undefined, download: false, limit: 5, input: undefined, ...flagOverrides });
|
|
48
60
|
return {
|
|
49
61
|
cmd,
|
|
50
62
|
postWorkflowRun: vi.mocked(postWorkflowRun),
|
|
@@ -66,4 +78,97 @@ describe('workflow dataset generate command', () => {
|
|
|
66
78
|
expect(postWorkflowRun).toHaveBeenCalledWith(expect.objectContaining({ workflowName: 'my_workflow', catalog: 'my-catalog' }), expect.anything());
|
|
67
79
|
});
|
|
68
80
|
});
|
|
81
|
+
describe('run() --download', () => {
|
|
82
|
+
const createDownloadCommand = async (flagOverrides = {}) => {
|
|
83
|
+
const { fetchWorkflowRuns } = await import('#services/workflow_runs.js');
|
|
84
|
+
const { getTrace } = await import('#services/trace_reader.js');
|
|
85
|
+
const { writeDataset } = await import('#services/datasets.js');
|
|
86
|
+
const cmd = await makeCmd({ workflowName: 'my_workflow', scenario: undefined }, { catalog: 'my-catalog', trace: undefined, name: undefined, download: true, limit: 5, input: undefined, ...flagOverrides });
|
|
87
|
+
return {
|
|
88
|
+
cmd,
|
|
89
|
+
fetchWorkflowRuns: vi.mocked(fetchWorkflowRuns),
|
|
90
|
+
getTrace: vi.mocked(getTrace),
|
|
91
|
+
writeDataset: vi.mocked(writeDataset)
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
it('fetches recent runs scoped to the catalog and writes a dataset per run', async () => {
|
|
95
|
+
const { cmd, fetchWorkflowRuns, getTrace, writeDataset } = await createDownloadCommand();
|
|
96
|
+
fetchWorkflowRuns.mockResolvedValue({
|
|
97
|
+
runs: [{ workflowId: 'wf-1' }, { workflowId: 'wf-2' }],
|
|
98
|
+
skipped: 0,
|
|
99
|
+
count: 2
|
|
100
|
+
});
|
|
101
|
+
getTrace.mockResolvedValue({ data: {}, location: { path: 'remote', isRemote: true } });
|
|
102
|
+
await cmd.run();
|
|
103
|
+
expect(fetchWorkflowRuns).toHaveBeenCalledWith({ workflowType: 'my_workflow', catalog: 'my-catalog', limit: 5 });
|
|
104
|
+
expect(getTrace).toHaveBeenCalledTimes(2);
|
|
105
|
+
expect(getTrace).toHaveBeenCalledWith('wf-1');
|
|
106
|
+
expect(getTrace).toHaveBeenCalledWith('wf-2');
|
|
107
|
+
expect(writeDataset).toHaveBeenCalledTimes(2);
|
|
108
|
+
});
|
|
109
|
+
it('skips runs whose trace cannot be fetched and continues', async () => {
|
|
110
|
+
const { cmd, fetchWorkflowRuns, getTrace, writeDataset } = await createDownloadCommand();
|
|
111
|
+
fetchWorkflowRuns.mockResolvedValue({
|
|
112
|
+
runs: [{ workflowId: 'wf-1' }, { workflowId: 'wf-2' }],
|
|
113
|
+
skipped: 0,
|
|
114
|
+
count: 2
|
|
115
|
+
});
|
|
116
|
+
getTrace
|
|
117
|
+
.mockRejectedValueOnce(new Error('no trace available'))
|
|
118
|
+
.mockResolvedValueOnce({ data: {}, location: { path: 'remote', isRemote: true } });
|
|
119
|
+
await cmd.run();
|
|
120
|
+
expect(cmd.warn).toHaveBeenCalledWith(expect.stringContaining('wf-1'));
|
|
121
|
+
expect(writeDataset).toHaveBeenCalledTimes(1);
|
|
122
|
+
});
|
|
123
|
+
it('reports when no recent runs are found', async () => {
|
|
124
|
+
const { cmd, fetchWorkflowRuns, getTrace, writeDataset } = await createDownloadCommand();
|
|
125
|
+
fetchWorkflowRuns.mockResolvedValue({ runs: [], skipped: 0, count: 0 });
|
|
126
|
+
await cmd.run();
|
|
127
|
+
expect(getTrace).not.toHaveBeenCalled();
|
|
128
|
+
expect(writeDataset).not.toHaveBeenCalled();
|
|
129
|
+
});
|
|
130
|
+
it('exits non-zero when every run fails to produce a dataset', async () => {
|
|
131
|
+
const { cmd, fetchWorkflowRuns, getTrace } = await createDownloadCommand();
|
|
132
|
+
fetchWorkflowRuns.mockResolvedValue({ runs: [{ workflowId: 'wf-1' }], skipped: 0, count: 1 });
|
|
133
|
+
getTrace.mockRejectedValue(new Error('no trace available'));
|
|
134
|
+
await expect(cmd.run()).rejects.toThrow('error called');
|
|
135
|
+
expect(cmd.error).toHaveBeenCalledWith(expect.stringContaining('Failed to generate'), { exit: 1 });
|
|
136
|
+
});
|
|
137
|
+
it('warns about runs the service skipped for missing a workflow ID', async () => {
|
|
138
|
+
const { cmd, fetchWorkflowRuns, getTrace, writeDataset } = await createDownloadCommand();
|
|
139
|
+
fetchWorkflowRuns.mockResolvedValue({ runs: [{ workflowId: 'wf-1' }], skipped: 1, count: 2 });
|
|
140
|
+
getTrace.mockResolvedValue({ data: {}, location: { path: 'remote', isRemote: true } });
|
|
141
|
+
await cmd.run();
|
|
142
|
+
expect(cmd.warn).toHaveBeenCalledWith(expect.stringContaining('no workflow ID'));
|
|
143
|
+
expect(getTrace).toHaveBeenCalledTimes(1);
|
|
144
|
+
expect(writeDataset).toHaveBeenCalledTimes(1);
|
|
145
|
+
});
|
|
146
|
+
it('exits non-zero when every run was skipped for missing a workflow ID', async () => {
|
|
147
|
+
const { cmd, fetchWorkflowRuns, getTrace } = await createDownloadCommand();
|
|
148
|
+
fetchWorkflowRuns.mockResolvedValue({ runs: [], skipped: 2, count: 2 });
|
|
149
|
+
await expect(cmd.run()).rejects.toThrow('error called');
|
|
150
|
+
expect(getTrace).not.toHaveBeenCalled();
|
|
151
|
+
expect(cmd.error).toHaveBeenCalledWith(expect.stringContaining('none had a workflow ID'), { exit: 1 });
|
|
152
|
+
});
|
|
153
|
+
it('deduplicates runs that share a workflow ID', async () => {
|
|
154
|
+
const { cmd, fetchWorkflowRuns, getTrace, writeDataset } = await createDownloadCommand();
|
|
155
|
+
fetchWorkflowRuns.mockResolvedValue({
|
|
156
|
+
runs: [{ workflowId: 'wf-1' }, { workflowId: 'wf-1' }],
|
|
157
|
+
skipped: 0,
|
|
158
|
+
count: 2
|
|
159
|
+
});
|
|
160
|
+
getTrace.mockResolvedValue({ data: {}, location: { path: 'remote', isRemote: true } });
|
|
161
|
+
await cmd.run();
|
|
162
|
+
expect(getTrace).toHaveBeenCalledTimes(1);
|
|
163
|
+
expect(writeDataset).toHaveBeenCalledTimes(1);
|
|
164
|
+
});
|
|
165
|
+
it('sanitizes path separators in the workflow ID used for the filename', async () => {
|
|
166
|
+
const { cmd, fetchWorkflowRuns, getTrace, writeDataset } = await createDownloadCommand();
|
|
167
|
+
fetchWorkflowRuns.mockResolvedValue({ runs: [{ workflowId: '../../escape' }], skipped: 0, count: 1 });
|
|
168
|
+
getTrace.mockResolvedValue({ data: {}, location: { path: 'remote', isRemote: true } });
|
|
169
|
+
await cmd.run();
|
|
170
|
+
const writtenPath = writeDataset.mock.calls[0][1];
|
|
171
|
+
expect(writtenPath).toBe('/datasets/.._.._escape.yml');
|
|
172
|
+
});
|
|
173
|
+
});
|
|
69
174
|
});
|
|
@@ -18,7 +18,7 @@ function createRunsTable(runs) {
|
|
|
18
18
|
});
|
|
19
19
|
runs.forEach(run => {
|
|
20
20
|
table.push([
|
|
21
|
-
run.workflowId
|
|
21
|
+
run.workflowId,
|
|
22
22
|
run.workflowType || '-',
|
|
23
23
|
run.status || '-',
|
|
24
24
|
formatDate(run.startedAt),
|
|
@@ -79,11 +79,14 @@ export default class WorkflowRunsList extends Command {
|
|
|
79
79
|
};
|
|
80
80
|
async run() {
|
|
81
81
|
const { args, flags } = await this.parse(WorkflowRunsList);
|
|
82
|
-
const { runs,
|
|
82
|
+
const { runs, skipped } = await fetchWorkflowRuns({
|
|
83
83
|
workflowType: args.workflowName,
|
|
84
84
|
catalog: flags.catalog,
|
|
85
85
|
limit: flags.limit
|
|
86
86
|
});
|
|
87
|
+
if (skipped > 0) {
|
|
88
|
+
this.warn(`Skipping ${skipped} run(s) with no workflow ID.`);
|
|
89
|
+
}
|
|
87
90
|
if (this.jsonEnabled()) {
|
|
88
91
|
return runs;
|
|
89
92
|
}
|
|
@@ -95,7 +98,7 @@ export default class WorkflowRunsList extends Command {
|
|
|
95
98
|
const output = formatRuns(runs, flags.format);
|
|
96
99
|
this.log(output);
|
|
97
100
|
const filterMsg = args.workflowName ? ` of type "${args.workflowName}"` : '';
|
|
98
|
-
this.log(`\nFound ${
|
|
101
|
+
this.log(`\nFound ${runs.length} run(s)${filterMsg}`);
|
|
99
102
|
return runs;
|
|
100
103
|
}
|
|
101
104
|
async catch(error) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
3
|
+
vi.mock('#services/workflow_runs.js', () => ({
|
|
4
|
+
fetchWorkflowRuns: vi.fn()
|
|
5
|
+
}));
|
|
6
|
+
describe('workflow runs list command', () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
vi.clearAllMocks();
|
|
9
|
+
});
|
|
10
|
+
const createCommand = async (flagOverrides = {}) => {
|
|
11
|
+
const WorkflowRunsList = (await import('./list.js')).default;
|
|
12
|
+
const { fetchWorkflowRuns } = await import('#services/workflow_runs.js');
|
|
13
|
+
const cmd = new WorkflowRunsList([], {});
|
|
14
|
+
cmd.log = vi.fn();
|
|
15
|
+
cmd.warn = vi.fn();
|
|
16
|
+
cmd.jsonEnabled = vi.fn().mockReturnValue(false);
|
|
17
|
+
cmd.parse = vi.fn().mockResolvedValue({
|
|
18
|
+
args: { workflowName: undefined },
|
|
19
|
+
flags: { catalog: undefined, limit: 100, format: 'table', ...flagOverrides }
|
|
20
|
+
});
|
|
21
|
+
return { cmd, fetchWorkflowRuns: vi.mocked(fetchWorkflowRuns) };
|
|
22
|
+
};
|
|
23
|
+
it('reports the actual number of returned runs, not the API\'s pre-filter count', async () => {
|
|
24
|
+
const { cmd, fetchWorkflowRuns } = await createCommand();
|
|
25
|
+
fetchWorkflowRuns.mockResolvedValue({
|
|
26
|
+
runs: [{ workflowId: 'wf-1', workflowType: 'demo', status: 'completed', startedAt: '2026-01-01T00:00:00Z', completedAt: null }],
|
|
27
|
+
skipped: 1,
|
|
28
|
+
count: 2
|
|
29
|
+
});
|
|
30
|
+
await cmd.run();
|
|
31
|
+
expect(cmd.log).toHaveBeenCalledWith(expect.stringContaining('Found 1 run(s)'));
|
|
32
|
+
});
|
|
33
|
+
it('warns when runs were skipped for missing a workflow ID', async () => {
|
|
34
|
+
const { cmd, fetchWorkflowRuns } = await createCommand();
|
|
35
|
+
fetchWorkflowRuns.mockResolvedValue({ runs: [], skipped: 2, count: 2 });
|
|
36
|
+
await cmd.run();
|
|
37
|
+
expect(cmd.warn).toHaveBeenCalledWith(expect.stringContaining('no workflow ID'));
|
|
38
|
+
});
|
|
39
|
+
it('does not warn when no runs were skipped', async () => {
|
|
40
|
+
const { cmd, fetchWorkflowRuns } = await createCommand();
|
|
41
|
+
fetchWorkflowRuns.mockResolvedValue({ runs: [], skipped: 0, count: 0 });
|
|
42
|
+
await cmd.run();
|
|
43
|
+
expect(cmd.warn).not.toHaveBeenCalled();
|
|
44
|
+
});
|
|
45
|
+
});
|
package/dist/config.d.ts
CHANGED
|
@@ -12,10 +12,4 @@ export declare const config: {
|
|
|
12
12
|
readonly debugMode: boolean;
|
|
13
13
|
readonly envFile: string;
|
|
14
14
|
agentConfigDir: string;
|
|
15
|
-
readonly s3: {
|
|
16
|
-
bucket: string | undefined;
|
|
17
|
-
region: string | undefined;
|
|
18
|
-
accessKeyId: string | undefined;
|
|
19
|
-
secretAccessKey: string | undefined;
|
|
20
|
-
};
|
|
21
15
|
};
|
package/dist/config.js
CHANGED
|
@@ -29,13 +29,5 @@ export const config = {
|
|
|
29
29
|
get envFile() {
|
|
30
30
|
return process.env.OUTPUT_CLI_ENV || '.env';
|
|
31
31
|
},
|
|
32
|
-
agentConfigDir: '.outputai'
|
|
33
|
-
get s3() {
|
|
34
|
-
return {
|
|
35
|
-
bucket: process.env.OUTPUT_TRACE_REMOTE_S3_BUCKET,
|
|
36
|
-
region: process.env.OUTPUT_AWS_REGION,
|
|
37
|
-
accessKeyId: process.env.OUTPUT_AWS_ACCESS_KEY_ID,
|
|
38
|
-
secretAccessKey: process.env.OUTPUT_AWS_SECRET_ACCESS_KEY
|
|
39
|
-
};
|
|
40
|
-
}
|
|
32
|
+
agentConfigDir: '.outputai'
|
|
41
33
|
};
|
package/dist/config.spec.js
CHANGED
|
@@ -10,11 +10,7 @@ describe('config', () => {
|
|
|
10
10
|
'OUTPUT_API_AUTH_TOKEN',
|
|
11
11
|
'DOCKER_SERVICE_NAME',
|
|
12
12
|
'OUTPUT_DEBUG',
|
|
13
|
-
'OUTPUT_CLI_ENV'
|
|
14
|
-
'OUTPUT_TRACE_REMOTE_S3_BUCKET',
|
|
15
|
-
'OUTPUT_AWS_REGION',
|
|
16
|
-
'OUTPUT_AWS_ACCESS_KEY_ID',
|
|
17
|
-
'OUTPUT_AWS_SECRET_ACCESS_KEY'
|
|
13
|
+
'OUTPUT_CLI_ENV'
|
|
18
14
|
];
|
|
19
15
|
const saved = {};
|
|
20
16
|
beforeEach(() => {
|
|
@@ -118,18 +114,6 @@ describe('config', () => {
|
|
|
118
114
|
process.env.OUTPUT_DEBUG = 'false';
|
|
119
115
|
expect(config.debugMode).toBe(false);
|
|
120
116
|
});
|
|
121
|
-
it('reads s3 config lazily', () => {
|
|
122
|
-
process.env.OUTPUT_TRACE_REMOTE_S3_BUCKET = 'my-bucket';
|
|
123
|
-
process.env.OUTPUT_AWS_REGION = 'us-west-2';
|
|
124
|
-
process.env.OUTPUT_AWS_ACCESS_KEY_ID = 'AKIA123';
|
|
125
|
-
process.env.OUTPUT_AWS_SECRET_ACCESS_KEY = 'secret123';
|
|
126
|
-
expect(config.s3).toEqual({
|
|
127
|
-
bucket: 'my-bucket',
|
|
128
|
-
region: 'us-west-2',
|
|
129
|
-
accessKeyId: 'AKIA123',
|
|
130
|
-
secretAccessKey: 'secret123'
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
117
|
it('has static properties that are not env-derived', () => {
|
|
134
118
|
expect(config.requestTimeout).toBe(30000);
|
|
135
119
|
expect(config.agentConfigDir).toBe('.outputai');
|
|
@@ -17,4 +17,5 @@ export declare function writeDataset(dataset: Dataset, filePath: string): Promis
|
|
|
17
17
|
export declare function listDatasets(workflowName: string, basePath?: string): Promise<DatasetInfo[]>;
|
|
18
18
|
export declare function buildDataset(name: string, input: Record<string, unknown>, output: unknown, executionTimeMs?: number): Dataset;
|
|
19
19
|
export declare function extractDatasetName(tracePathOrKey: string): string;
|
|
20
|
+
export declare function datasetFilePath(dir: string, name: string): string;
|
|
20
21
|
export declare function getExecutionTime(workflowId: string | undefined): Promise<number | undefined>;
|
|
@@ -123,6 +123,13 @@ export function buildDataset(name, input, output, executionTimeMs) {
|
|
|
123
123
|
export function extractDatasetName(tracePathOrKey) {
|
|
124
124
|
return tracePathOrKey.replace(/^.*\//, '').replace(/\.json$/, '');
|
|
125
125
|
}
|
|
126
|
+
// Resolve the .yml path for a dataset case, sanitizing the name so it can never
|
|
127
|
+
// escape the datasets directory regardless of where the name came from
|
|
128
|
+
// (scenario arg, --name flag, or a workflow ID).
|
|
129
|
+
export function datasetFilePath(dir, name) {
|
|
130
|
+
const safeName = name.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
131
|
+
return join(dir, `${safeName}.yml`);
|
|
132
|
+
}
|
|
126
133
|
export function getExecutionTime(workflowId) {
|
|
127
134
|
if (!workflowId) {
|
|
128
135
|
return Promise.resolve(undefined);
|
|
@@ -3,7 +3,7 @@ import { mkdtemp, rm, writeFile, readFile, mkdir } from 'node:fs/promises';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import yaml from 'js-yaml';
|
|
6
|
-
import { readDatasetFile, readAllDatasets, writeDataset, listDatasets, resolveDefaultDatasetsDir } from './datasets.js';
|
|
6
|
+
import { readDatasetFile, readAllDatasets, writeDataset, listDatasets, resolveDefaultDatasetsDir, datasetFilePath } from './datasets.js';
|
|
7
7
|
import * as catalog from '#api/workflow_catalog.js';
|
|
8
8
|
vi.mock('#api/workflow_catalog.js', () => ({
|
|
9
9
|
fetchWorkflowCatalog: vi.fn()
|
|
@@ -235,3 +235,12 @@ describe('resolveDefaultDatasetsDir', () => {
|
|
|
235
235
|
expect(dir).toBe(join(ctx.tmpDir, 'src', 'workflows', 'unknown_flow', 'tests', 'datasets'));
|
|
236
236
|
});
|
|
237
237
|
});
|
|
238
|
+
describe('datasetFilePath', () => {
|
|
239
|
+
it('joins the directory with a .yml file named after the case', () => {
|
|
240
|
+
expect(datasetFilePath('/datasets', 'basic_input')).toBe(join('/datasets', 'basic_input.yml'));
|
|
241
|
+
});
|
|
242
|
+
it('replaces path separators and other unsafe characters so the name cannot escape the directory', () => {
|
|
243
|
+
expect(datasetFilePath('/datasets', '../../escape')).toBe(join('/datasets', '.._.._escape.yml'));
|
|
244
|
+
expect(datasetFilePath('/datasets', 'a/b c')).toBe(join('/datasets', 'a_b_c.yml'));
|
|
245
|
+
});
|
|
246
|
+
});
|
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
* Workflow runs service for fetching workflow run data from the API
|
|
3
3
|
*/
|
|
4
4
|
import { type WorkflowRunInfo } from '#api/generated/api.js';
|
|
5
|
-
export type WorkflowRun = WorkflowRunInfo
|
|
5
|
+
export type WorkflowRun = Omit<WorkflowRunInfo, 'workflowId'> & {
|
|
6
|
+
workflowId: string;
|
|
7
|
+
};
|
|
6
8
|
export interface WorkflowRunsResult {
|
|
7
9
|
runs: WorkflowRun[];
|
|
10
|
+
skipped: number;
|
|
8
11
|
count: number;
|
|
9
12
|
}
|
|
10
13
|
export interface FetchWorkflowRunsOptions {
|
|
@@ -22,12 +22,14 @@ export async function fetchWorkflowRuns(options = {}) {
|
|
|
22
22
|
throw new Error('API returned invalid response (missing data)');
|
|
23
23
|
}
|
|
24
24
|
const data = response.data;
|
|
25
|
-
const
|
|
25
|
+
const allRuns = (data.runs || []).map(run => ({
|
|
26
26
|
...run,
|
|
27
27
|
status: normalizeWorkflowStatus(run.status)
|
|
28
28
|
}));
|
|
29
|
+
const runs = allRuns.filter(run => Boolean(run.workflowId));
|
|
29
30
|
return {
|
|
30
31
|
runs,
|
|
32
|
+
skipped: allRuns.length - runs.length,
|
|
31
33
|
count: data.count || 0
|
|
32
34
|
};
|
|
33
35
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
vi.mock('#api/generated/api.js', () => ({
|
|
3
|
+
getWorkflowRuns: vi.fn()
|
|
4
|
+
}));
|
|
5
|
+
import { getWorkflowRuns } from '#api/generated/api.js';
|
|
6
|
+
import { fetchWorkflowRuns } from '#services/workflow_runs.js';
|
|
7
|
+
describe('fetchWorkflowRuns', () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
vi.clearAllMocks();
|
|
10
|
+
});
|
|
11
|
+
it('filters out runs with no workflow ID and reports how many were skipped', async () => {
|
|
12
|
+
vi.mocked(getWorkflowRuns).mockResolvedValue({
|
|
13
|
+
data: {
|
|
14
|
+
runs: [{ workflowId: 'wf-1' }, { workflowId: undefined }, { workflowId: 'wf-2' }],
|
|
15
|
+
count: 3
|
|
16
|
+
},
|
|
17
|
+
status: 200,
|
|
18
|
+
headers: new Headers()
|
|
19
|
+
});
|
|
20
|
+
const result = await fetchWorkflowRuns();
|
|
21
|
+
expect(result.runs.map(run => run.workflowId)).toEqual(['wf-1', 'wf-2']);
|
|
22
|
+
expect(result.skipped).toBe(1);
|
|
23
|
+
expect(result.count).toBe(3);
|
|
24
|
+
});
|
|
25
|
+
it('reports zero skipped when every run has a workflow ID', async () => {
|
|
26
|
+
vi.mocked(getWorkflowRuns).mockResolvedValue({
|
|
27
|
+
data: { runs: [{ workflowId: 'wf-1' }], count: 1 },
|
|
28
|
+
status: 200,
|
|
29
|
+
headers: new Headers()
|
|
30
|
+
});
|
|
31
|
+
const result = await fetchWorkflowRuns();
|
|
32
|
+
expect(result.skipped).toBe(0);
|
|
33
|
+
expect(result.runs).toHaveLength(1);
|
|
34
|
+
});
|
|
35
|
+
it('throws when the API server cannot be reached', async () => {
|
|
36
|
+
vi.mocked(getWorkflowRuns).mockResolvedValue(undefined);
|
|
37
|
+
await expect(fetchWorkflowRuns()).rejects.toThrow('Failed to connect to API server');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -9,7 +9,7 @@ export const RunInfoSidebar = ({ run, resultStatus, maxRows }) => {
|
|
|
9
9
|
const rows = [
|
|
10
10
|
_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "RUN STATUS\u00A0" }), _jsx(WorkflowStatusIcon, { status: status }), _jsx(Text, { children: "\u00A0" }), _jsx(Text, { bold: true, color: workflowStatusColor(status), children: status.toUpperCase() })] }, "status"),
|
|
11
11
|
_jsx(SidebarKV, { label: "RUN ID", value: run.runId ?? '-' }, "run-id"),
|
|
12
|
-
_jsx(SidebarKV, { label: "WORKFLOW ID", value: run.workflowId
|
|
12
|
+
_jsx(SidebarKV, { label: "WORKFLOW ID", value: run.workflowId }, "workflow-id"),
|
|
13
13
|
_jsx(SidebarKV, { label: "TYPE", value: run.workflowType ?? '-' }, "type"),
|
|
14
14
|
_jsx(SidebarKV, { label: "DURATION", value: duration }, "duration"),
|
|
15
15
|
_jsx(SidebarKV, { label: "START", value: formatDate(run.startedAt) }, "start"),
|
|
@@ -40,7 +40,7 @@ const matchesFilter = (run, query) => {
|
|
|
40
40
|
}
|
|
41
41
|
const q = query.toLowerCase();
|
|
42
42
|
return (run.workflowType ?? '').toLowerCase().includes(q) ||
|
|
43
|
-
|
|
43
|
+
run.workflowId.toLowerCase().includes(q) ||
|
|
44
44
|
(run.status ?? '').toLowerCase().includes(q);
|
|
45
45
|
};
|
|
46
46
|
export const buildVisibleRuns = (runs, query) => {
|
|
@@ -68,7 +68,7 @@ const RunRow = ({ run, selected }) => {
|
|
|
68
68
|
const status = run.status ?? 'running';
|
|
69
69
|
const color = workflowStatusColor(status);
|
|
70
70
|
const duration = run.startedAt ? formatDurationCompact(elapsedMs(run.startedAt, run.completedAt)) : '-';
|
|
71
|
-
return (_jsxs(Box, { children: [_jsx(Box, { width: COL.indicator, children: _jsx(SelectionIndicator, { selected: selected }) }), _jsx(Box, { width: COL.icon, children: _jsx(WorkflowStatusIcon, { status: status }) }), _jsx(Box, { width: COL.status, children: _jsx(Text, { color: color, children: status }) }), _jsx(Box, { width: COL.type, children: _jsx(Text, { bold: selected, children: truncate(run.workflowType ?? '-', COL.type - 1) }) }), _jsx(Box, { width: COL.id, children: _jsx(Text, { dimColor: !selected, children: truncate(run.workflowId
|
|
71
|
+
return (_jsxs(Box, { children: [_jsx(Box, { width: COL.indicator, children: _jsx(SelectionIndicator, { selected: selected }) }), _jsx(Box, { width: COL.icon, children: _jsx(WorkflowStatusIcon, { status: status }) }), _jsx(Box, { width: COL.status, children: _jsx(Text, { color: color, children: status }) }), _jsx(Box, { width: COL.type, children: _jsx(Text, { bold: selected, children: truncate(run.workflowType ?? '-', COL.type - 1) }) }), _jsx(Box, { width: COL.id, children: _jsx(Text, { dimColor: !selected, children: truncate(run.workflowId, COL.id - 1) }) }), _jsx(Box, { width: COL.duration, justifyContent: "flex-end", children: _jsx(Text, { dimColor: !selected, children: duration }) }), _jsx(Box, { width: COL.started, marginLeft: 2, children: _jsx(Text, { dimColor: !selected, children: formatStartedShort(run.startedAt) }) })] }));
|
|
72
72
|
};
|
|
73
73
|
const statusPaneValue = (run, pane) => ({
|
|
74
74
|
status: pane.status,
|
package/oclif.manifest.json
CHANGED
|
@@ -1366,81 +1366,6 @@
|
|
|
1366
1366
|
"test_eval.js"
|
|
1367
1367
|
]
|
|
1368
1368
|
},
|
|
1369
|
-
"workflow:runs:list": {
|
|
1370
|
-
"aliases": [],
|
|
1371
|
-
"args": {
|
|
1372
|
-
"workflowName": {
|
|
1373
|
-
"description": "Filter by workflow type/name",
|
|
1374
|
-
"name": "workflowName",
|
|
1375
|
-
"required": false
|
|
1376
|
-
}
|
|
1377
|
-
},
|
|
1378
|
-
"description": "List workflow runs with optional filtering by workflow type",
|
|
1379
|
-
"examples": [
|
|
1380
|
-
"<%= config.bin %> <%= command.id %>",
|
|
1381
|
-
"<%= config.bin %> <%= command.id %> simple",
|
|
1382
|
-
"<%= config.bin %> <%= command.id %> simple --limit 10",
|
|
1383
|
-
"<%= config.bin %> <%= command.id %> --catalog my-catalog",
|
|
1384
|
-
"<%= config.bin %> <%= command.id %> --json",
|
|
1385
|
-
"<%= config.bin %> <%= command.id %> --format table"
|
|
1386
|
-
],
|
|
1387
|
-
"flags": {
|
|
1388
|
-
"json": {
|
|
1389
|
-
"description": "Format output as json.",
|
|
1390
|
-
"helpGroup": "GLOBAL",
|
|
1391
|
-
"name": "json",
|
|
1392
|
-
"allowNo": false,
|
|
1393
|
-
"type": "boolean"
|
|
1394
|
-
},
|
|
1395
|
-
"catalog": {
|
|
1396
|
-
"char": "c",
|
|
1397
|
-
"description": "Filter runs by catalog (defaults to OUTPUT_CATALOG_ID)",
|
|
1398
|
-
"env": "OUTPUT_CATALOG_ID",
|
|
1399
|
-
"name": "catalog",
|
|
1400
|
-
"hasDynamicHelp": false,
|
|
1401
|
-
"multiple": false,
|
|
1402
|
-
"type": "option"
|
|
1403
|
-
},
|
|
1404
|
-
"limit": {
|
|
1405
|
-
"char": "l",
|
|
1406
|
-
"description": "Maximum number of runs to return",
|
|
1407
|
-
"name": "limit",
|
|
1408
|
-
"default": 100,
|
|
1409
|
-
"hasDynamicHelp": false,
|
|
1410
|
-
"multiple": false,
|
|
1411
|
-
"type": "option"
|
|
1412
|
-
},
|
|
1413
|
-
"format": {
|
|
1414
|
-
"char": "f",
|
|
1415
|
-
"description": "Output format (use --json for JSON output)",
|
|
1416
|
-
"name": "format",
|
|
1417
|
-
"default": "table",
|
|
1418
|
-
"hasDynamicHelp": false,
|
|
1419
|
-
"multiple": false,
|
|
1420
|
-
"options": [
|
|
1421
|
-
"table",
|
|
1422
|
-
"text"
|
|
1423
|
-
],
|
|
1424
|
-
"type": "option"
|
|
1425
|
-
}
|
|
1426
|
-
},
|
|
1427
|
-
"hasDynamicHelp": false,
|
|
1428
|
-
"hiddenAliases": [],
|
|
1429
|
-
"id": "workflow:runs:list",
|
|
1430
|
-
"pluginAlias": "@outputai/cli",
|
|
1431
|
-
"pluginName": "@outputai/cli",
|
|
1432
|
-
"pluginType": "core",
|
|
1433
|
-
"strict": true,
|
|
1434
|
-
"enableJsonFlag": true,
|
|
1435
|
-
"isESM": true,
|
|
1436
|
-
"relativePath": [
|
|
1437
|
-
"dist",
|
|
1438
|
-
"commands",
|
|
1439
|
-
"workflow",
|
|
1440
|
-
"runs",
|
|
1441
|
-
"list.js"
|
|
1442
|
-
]
|
|
1443
|
-
},
|
|
1444
1369
|
"workflow:dataset:generate": {
|
|
1445
1370
|
"aliases": [],
|
|
1446
1371
|
"args": {
|
|
@@ -1455,7 +1380,7 @@
|
|
|
1455
1380
|
"required": false
|
|
1456
1381
|
}
|
|
1457
1382
|
},
|
|
1458
|
-
"description": "Generate a dataset for a workflow from a scenario, trace file, or
|
|
1383
|
+
"description": "Generate a dataset for a workflow from a scenario, trace file, or recent runs",
|
|
1459
1384
|
"examples": [
|
|
1460
1385
|
"<%= config.bin %> <%= command.id %> simple basic_input",
|
|
1461
1386
|
"<%= config.bin %> <%= command.id %> simple --trace logs/runs/simple/trace.json --name edge_case",
|
|
@@ -1499,7 +1424,7 @@
|
|
|
1499
1424
|
},
|
|
1500
1425
|
"download": {
|
|
1501
1426
|
"char": "d",
|
|
1502
|
-
"description": "
|
|
1427
|
+
"description": "Generate datasets from recent workflow runs fetched via the Output API",
|
|
1503
1428
|
"exclusive": [
|
|
1504
1429
|
"trace"
|
|
1505
1430
|
],
|
|
@@ -1509,7 +1434,7 @@
|
|
|
1509
1434
|
},
|
|
1510
1435
|
"limit": {
|
|
1511
1436
|
"char": "l",
|
|
1512
|
-
"description": "Maximum number of
|
|
1437
|
+
"description": "Maximum number of recent runs to fetch",
|
|
1513
1438
|
"name": "limit",
|
|
1514
1439
|
"default": 5,
|
|
1515
1440
|
"hasDynamicHelp": false,
|
|
@@ -1595,7 +1520,82 @@
|
|
|
1595
1520
|
"dataset",
|
|
1596
1521
|
"list.js"
|
|
1597
1522
|
]
|
|
1523
|
+
},
|
|
1524
|
+
"workflow:runs:list": {
|
|
1525
|
+
"aliases": [],
|
|
1526
|
+
"args": {
|
|
1527
|
+
"workflowName": {
|
|
1528
|
+
"description": "Filter by workflow type/name",
|
|
1529
|
+
"name": "workflowName",
|
|
1530
|
+
"required": false
|
|
1531
|
+
}
|
|
1532
|
+
},
|
|
1533
|
+
"description": "List workflow runs with optional filtering by workflow type",
|
|
1534
|
+
"examples": [
|
|
1535
|
+
"<%= config.bin %> <%= command.id %>",
|
|
1536
|
+
"<%= config.bin %> <%= command.id %> simple",
|
|
1537
|
+
"<%= config.bin %> <%= command.id %> simple --limit 10",
|
|
1538
|
+
"<%= config.bin %> <%= command.id %> --catalog my-catalog",
|
|
1539
|
+
"<%= config.bin %> <%= command.id %> --json",
|
|
1540
|
+
"<%= config.bin %> <%= command.id %> --format table"
|
|
1541
|
+
],
|
|
1542
|
+
"flags": {
|
|
1543
|
+
"json": {
|
|
1544
|
+
"description": "Format output as json.",
|
|
1545
|
+
"helpGroup": "GLOBAL",
|
|
1546
|
+
"name": "json",
|
|
1547
|
+
"allowNo": false,
|
|
1548
|
+
"type": "boolean"
|
|
1549
|
+
},
|
|
1550
|
+
"catalog": {
|
|
1551
|
+
"char": "c",
|
|
1552
|
+
"description": "Filter runs by catalog (defaults to OUTPUT_CATALOG_ID)",
|
|
1553
|
+
"env": "OUTPUT_CATALOG_ID",
|
|
1554
|
+
"name": "catalog",
|
|
1555
|
+
"hasDynamicHelp": false,
|
|
1556
|
+
"multiple": false,
|
|
1557
|
+
"type": "option"
|
|
1558
|
+
},
|
|
1559
|
+
"limit": {
|
|
1560
|
+
"char": "l",
|
|
1561
|
+
"description": "Maximum number of runs to return",
|
|
1562
|
+
"name": "limit",
|
|
1563
|
+
"default": 100,
|
|
1564
|
+
"hasDynamicHelp": false,
|
|
1565
|
+
"multiple": false,
|
|
1566
|
+
"type": "option"
|
|
1567
|
+
},
|
|
1568
|
+
"format": {
|
|
1569
|
+
"char": "f",
|
|
1570
|
+
"description": "Output format (use --json for JSON output)",
|
|
1571
|
+
"name": "format",
|
|
1572
|
+
"default": "table",
|
|
1573
|
+
"hasDynamicHelp": false,
|
|
1574
|
+
"multiple": false,
|
|
1575
|
+
"options": [
|
|
1576
|
+
"table",
|
|
1577
|
+
"text"
|
|
1578
|
+
],
|
|
1579
|
+
"type": "option"
|
|
1580
|
+
}
|
|
1581
|
+
},
|
|
1582
|
+
"hasDynamicHelp": false,
|
|
1583
|
+
"hiddenAliases": [],
|
|
1584
|
+
"id": "workflow:runs:list",
|
|
1585
|
+
"pluginAlias": "@outputai/cli",
|
|
1586
|
+
"pluginName": "@outputai/cli",
|
|
1587
|
+
"pluginType": "core",
|
|
1588
|
+
"strict": true,
|
|
1589
|
+
"enableJsonFlag": true,
|
|
1590
|
+
"isESM": true,
|
|
1591
|
+
"relativePath": [
|
|
1592
|
+
"dist",
|
|
1593
|
+
"commands",
|
|
1594
|
+
"workflow",
|
|
1595
|
+
"runs",
|
|
1596
|
+
"list.js"
|
|
1597
|
+
]
|
|
1598
1598
|
}
|
|
1599
1599
|
},
|
|
1600
|
-
"version": "0.9.3-next.
|
|
1600
|
+
"version": "0.9.3-next.5289bca.0"
|
|
1601
1601
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@outputai/cli",
|
|
3
|
-
"version": "0.9.3-next.
|
|
3
|
+
"version": "0.9.3-next.5289bca.0",
|
|
4
4
|
"description": "CLI for Output.ai workflow generation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@anthropic-ai/claude-agent-sdk": "0.2.92",
|
|
21
|
-
"@aws-sdk/client-s3": "3.1038.0",
|
|
22
21
|
"@hackylabs/deep-redact": "3.0.5",
|
|
23
22
|
"@inquirer/prompts": "8.4.2",
|
|
24
23
|
"@oclif/core": "4.10.6",
|
|
@@ -39,9 +38,9 @@
|
|
|
39
38
|
"semver": "7.7.4",
|
|
40
39
|
"undici": "8.5.0",
|
|
41
40
|
"yaml": "^2.8.3",
|
|
42
|
-
"@outputai/
|
|
43
|
-
"@outputai/
|
|
44
|
-
"@outputai/
|
|
41
|
+
"@outputai/evals": "0.9.3-next.5289bca.0",
|
|
42
|
+
"@outputai/llm": "0.9.3-next.5289bca.0",
|
|
43
|
+
"@outputai/credentials": "0.9.3-next.5289bca.0"
|
|
45
44
|
},
|
|
46
45
|
"devDependencies": {
|
|
47
46
|
"@types/cli-progress": "3.11.6",
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { TraceData } from '#types/trace.js';
|
|
2
|
-
interface RemoteTraceInfo {
|
|
3
|
-
key: string;
|
|
4
|
-
lastModified?: Date;
|
|
5
|
-
size?: number;
|
|
6
|
-
}
|
|
7
|
-
export declare function listRemoteTraces(workflowName: string, options?: {
|
|
8
|
-
limit?: number;
|
|
9
|
-
since?: Date;
|
|
10
|
-
}): Promise<RemoteTraceInfo[]>;
|
|
11
|
-
export declare function downloadRemoteTrace(key: string): Promise<TraceData>;
|
|
12
|
-
export {};
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { S3Client, ListObjectsV2Command, GetObjectCommand } from '@aws-sdk/client-s3';
|
|
2
|
-
import { config } from '#config.js';
|
|
3
|
-
function getS3Config() {
|
|
4
|
-
const { bucket, region, accessKeyId, secretAccessKey } = config.s3;
|
|
5
|
-
if (!bucket || !region || !accessKeyId || !secretAccessKey) {
|
|
6
|
-
throw new Error('Missing S3 configuration. Set OUTPUT_TRACE_REMOTE_S3_BUCKET, OUTPUT_AWS_REGION, ' +
|
|
7
|
-
'OUTPUT_AWS_ACCESS_KEY_ID, and OUTPUT_AWS_SECRET_ACCESS_KEY environment variables.');
|
|
8
|
-
}
|
|
9
|
-
return { bucket, region, accessKeyId, secretAccessKey };
|
|
10
|
-
}
|
|
11
|
-
function createS3Client(s3Config) {
|
|
12
|
-
return new S3Client({
|
|
13
|
-
region: s3Config.region,
|
|
14
|
-
credentials: {
|
|
15
|
-
accessKeyId: s3Config.accessKeyId,
|
|
16
|
-
secretAccessKey: s3Config.secretAccessKey
|
|
17
|
-
}
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
export async function listRemoteTraces(workflowName, options = {}) {
|
|
21
|
-
const s3Config = getS3Config();
|
|
22
|
-
const client = createS3Client(s3Config);
|
|
23
|
-
const limit = options.limit ?? 20;
|
|
24
|
-
const command = new ListObjectsV2Command({
|
|
25
|
-
Bucket: s3Config.bucket,
|
|
26
|
-
Prefix: `${workflowName}/`,
|
|
27
|
-
MaxKeys: limit
|
|
28
|
-
});
|
|
29
|
-
const response = await client.send(command);
|
|
30
|
-
const contents = response.Contents ?? [];
|
|
31
|
-
return contents
|
|
32
|
-
.filter(obj => {
|
|
33
|
-
if (!obj.Key) {
|
|
34
|
-
return false;
|
|
35
|
-
}
|
|
36
|
-
if (options.since && obj.LastModified && obj.LastModified < options.since) {
|
|
37
|
-
return false;
|
|
38
|
-
}
|
|
39
|
-
return true;
|
|
40
|
-
})
|
|
41
|
-
.map(obj => ({
|
|
42
|
-
key: obj.Key,
|
|
43
|
-
lastModified: obj.LastModified,
|
|
44
|
-
size: obj.Size
|
|
45
|
-
}));
|
|
46
|
-
}
|
|
47
|
-
export async function downloadRemoteTrace(key) {
|
|
48
|
-
const s3Config = getS3Config();
|
|
49
|
-
const client = createS3Client(s3Config);
|
|
50
|
-
const command = new GetObjectCommand({ Bucket: s3Config.bucket, Key: key });
|
|
51
|
-
const response = await client.send(command);
|
|
52
|
-
const body = await response.Body?.transformToString();
|
|
53
|
-
if (!body) {
|
|
54
|
-
throw new Error(`Empty response for S3 object: ${key}`);
|
|
55
|
-
}
|
|
56
|
-
return JSON.parse(body);
|
|
57
|
-
}
|