@outputai/cli 0.8.1-next.e92f632.0 → 0.8.2-next.2da7213.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/cost.d.ts +3 -2
- package/dist/commands/workflow/cost.js +4 -11
- package/dist/commands/workflow/cost.spec.js +1 -3
- package/dist/commands/workflow/dataset/generate.js +3 -3
- package/dist/commands/workflow/dataset/list.d.ts +3 -1
- package/dist/commands/workflow/dataset/list.js +10 -14
- package/dist/commands/workflow/debug.d.ts +2 -6
- package/dist/commands/workflow/debug.js +10 -29
- package/dist/commands/workflow/debug.spec.js +2 -5
- package/dist/commands/workflow/list.d.ts +4 -1
- package/dist/commands/workflow/list.js +15 -19
- package/dist/commands/workflow/list.spec.js +2 -1
- package/dist/commands/workflow/result.d.ts +3 -4
- package/dist/commands/workflow/result.js +6 -15
- package/dist/commands/workflow/result.spec.js +2 -4
- package/dist/commands/workflow/run.d.ts +3 -2
- package/dist/commands/workflow/run.js +4 -11
- package/dist/commands/workflow/run.spec.js +1 -3
- package/dist/commands/workflow/runs/list.d.ts +3 -1
- package/dist/commands/workflow/runs/list.js +11 -15
- package/dist/commands/workflow/status.d.ts +3 -4
- package/dist/commands/workflow/status.js +20 -30
- package/dist/commands/workflow/status.spec.js +2 -4
- package/dist/commands/workflow/test_eval.d.ts +4 -2
- package/dist/commands/workflow/test_eval.js +14 -15
- package/dist/commands/workflow/test_eval.spec.d.ts +1 -0
- package/dist/commands/workflow/test_eval.spec.js +84 -0
- package/dist/generated/framework_version.json +1 -1
- package/dist/hooks/init.d.ts +1 -0
- package/dist/hooks/init.js +40 -30
- package/dist/hooks/init.spec.js +28 -4
- package/dist/services/datasets.d.ts +2 -2
- package/dist/services/datasets.js +19 -17
- package/dist/services/datasets.test.js +37 -2
- package/dist/utils/eval_diagnostics.d.ts +7 -0
- package/dist/utils/eval_diagnostics.js +35 -0
- package/dist/utils/format_workflow_result.spec.js +0 -13
- package/dist/utils/scenario_resolver.d.ts +1 -2
- package/dist/utils/scenario_resolver.js +3 -33
- package/dist/utils/trace_formatter.js +1 -2
- package/dist/utils/workflow_dir.d.ts +13 -0
- package/dist/utils/workflow_dir.js +58 -0
- package/dist/utils/workflow_dir.spec.d.ts +1 -0
- package/dist/utils/workflow_dir.spec.js +60 -0
- package/oclif.manifest.json +182 -201
- package/package.json +4 -4
- package/dist/utils/constants.d.ts +0 -5
- package/dist/utils/constants.js +0 -4
- package/dist/utils/output_formatter.d.ts +0 -2
- package/dist/utils/output_formatter.js +0 -11
|
@@ -5,7 +5,6 @@ import { formatDate, formatDurationFromTimestamps } from '#utils/date_formatter.
|
|
|
5
5
|
import { handleApiError } from '#utils/error_handler.js';
|
|
6
6
|
const OUTPUT_FORMAT = {
|
|
7
7
|
TABLE: 'table',
|
|
8
|
-
JSON: 'json',
|
|
9
8
|
TEXT: 'text'
|
|
10
9
|
};
|
|
11
10
|
function createRunsTable(runs) {
|
|
@@ -37,13 +36,7 @@ function formatRunsAsText(runs) {
|
|
|
37
36
|
return `${run.workflowId} (${run.workflowType}) - ${run.status} [${duration}]`;
|
|
38
37
|
}).join('\n');
|
|
39
38
|
}
|
|
40
|
-
function formatRunsAsJson(runs) {
|
|
41
|
-
return JSON.stringify(runs, null, 2);
|
|
42
|
-
}
|
|
43
39
|
function formatRuns(runs, format) {
|
|
44
|
-
if (format === OUTPUT_FORMAT.JSON) {
|
|
45
|
-
return formatRunsAsJson(runs);
|
|
46
|
-
}
|
|
47
40
|
if (format === OUTPUT_FORMAT.TABLE) {
|
|
48
41
|
return createRunsTable(runs);
|
|
49
42
|
}
|
|
@@ -51,12 +44,13 @@ function formatRuns(runs, format) {
|
|
|
51
44
|
}
|
|
52
45
|
export default class WorkflowRunsList extends Command {
|
|
53
46
|
static description = 'List workflow runs with optional filtering by workflow type';
|
|
47
|
+
static enableJsonFlag = true;
|
|
54
48
|
static examples = [
|
|
55
49
|
'<%= config.bin %> <%= command.id %>',
|
|
56
50
|
'<%= config.bin %> <%= command.id %> simple',
|
|
57
51
|
'<%= config.bin %> <%= command.id %> simple --limit 10',
|
|
58
52
|
'<%= config.bin %> <%= command.id %> --catalog my-catalog',
|
|
59
|
-
'<%= config.bin %> <%= command.id %> --
|
|
53
|
+
'<%= config.bin %> <%= command.id %> --json',
|
|
60
54
|
'<%= config.bin %> <%= command.id %> --format table'
|
|
61
55
|
];
|
|
62
56
|
static args = {
|
|
@@ -78,8 +72,8 @@ export default class WorkflowRunsList extends Command {
|
|
|
78
72
|
}),
|
|
79
73
|
format: Flags.string({
|
|
80
74
|
char: 'f',
|
|
81
|
-
description: 'Output format',
|
|
82
|
-
options: [OUTPUT_FORMAT.TABLE, OUTPUT_FORMAT.
|
|
75
|
+
description: 'Output format (use --json for JSON output)',
|
|
76
|
+
options: [OUTPUT_FORMAT.TABLE, OUTPUT_FORMAT.TEXT],
|
|
83
77
|
default: OUTPUT_FORMAT.TABLE
|
|
84
78
|
})
|
|
85
79
|
};
|
|
@@ -90,17 +84,19 @@ export default class WorkflowRunsList extends Command {
|
|
|
90
84
|
catalog: flags.catalog,
|
|
91
85
|
limit: flags.limit
|
|
92
86
|
});
|
|
87
|
+
if (this.jsonEnabled()) {
|
|
88
|
+
return runs;
|
|
89
|
+
}
|
|
93
90
|
if (runs.length === 0) {
|
|
94
91
|
const filterMsg = args.workflowName ? ` for workflow type "${args.workflowName}"` : '';
|
|
95
92
|
this.log(`No workflow runs found${filterMsg}.`);
|
|
96
|
-
return;
|
|
93
|
+
return runs;
|
|
97
94
|
}
|
|
98
95
|
const output = formatRuns(runs, flags.format);
|
|
99
96
|
this.log(output);
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
97
|
+
const filterMsg = args.workflowName ? ` of type "${args.workflowName}"` : '';
|
|
98
|
+
this.log(`\nFound ${count} run(s)${filterMsg}`);
|
|
99
|
+
return runs;
|
|
104
100
|
}
|
|
105
101
|
async catch(error) {
|
|
106
102
|
return handleApiError(error, (...args) => this.error(...args), {
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
|
+
import { WorkflowStatusResponse } from '#api/generated/api.js';
|
|
2
3
|
export default class WorkflowStatus extends Command {
|
|
3
4
|
static description: string;
|
|
5
|
+
static enableJsonFlag: boolean;
|
|
4
6
|
static examples: string[];
|
|
5
7
|
static args: {
|
|
6
8
|
workflowId: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
7
9
|
};
|
|
8
|
-
|
|
9
|
-
format: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
-
};
|
|
11
|
-
run(): Promise<void>;
|
|
10
|
+
run(): Promise<WorkflowStatusResponse>;
|
|
12
11
|
catch(error: Error): Promise<void>;
|
|
13
12
|
}
|
|
@@ -1,14 +1,27 @@
|
|
|
1
|
-
import { Args, Command
|
|
1
|
+
import { Args, Command } from '@oclif/core';
|
|
2
2
|
import { getWorkflowIdStatus } from '#api/generated/api.js';
|
|
3
|
-
import { OUTPUT_FORMAT } from '#utils/constants.js';
|
|
4
|
-
import { formatOutput } from '#utils/output_formatter.js';
|
|
5
3
|
import { handleApiError } from '#utils/error_handler.js';
|
|
6
4
|
import { normalizeWorkflowStatus } from '#utils/normalize_workflow_status.js';
|
|
5
|
+
function formatStatusText(result) {
|
|
6
|
+
const lines = [
|
|
7
|
+
`Workflow ID: ${result.workflowId || 'unknown'}`,
|
|
8
|
+
`Status: ${result.status || 'unknown'}`,
|
|
9
|
+
''
|
|
10
|
+
];
|
|
11
|
+
if (result.startedAt) {
|
|
12
|
+
lines.push(`Started At: ${new Date(result.startedAt).toISOString()}`);
|
|
13
|
+
}
|
|
14
|
+
if (result.completedAt) {
|
|
15
|
+
lines.push(`Completed At: ${new Date(result.completedAt).toISOString()}`);
|
|
16
|
+
}
|
|
17
|
+
return lines.join('\n');
|
|
18
|
+
}
|
|
7
19
|
export default class WorkflowStatus extends Command {
|
|
8
20
|
static description = 'Get workflow execution status';
|
|
21
|
+
static enableJsonFlag = true;
|
|
9
22
|
static examples = [
|
|
10
23
|
'<%= config.bin %> <%= command.id %> wf-12345',
|
|
11
|
-
'<%= config.bin %> <%= command.id %> wf-12345 --
|
|
24
|
+
'<%= config.bin %> <%= command.id %> wf-12345 --json'
|
|
12
25
|
];
|
|
13
26
|
static args = {
|
|
14
27
|
workflowId: Args.string({
|
|
@@ -16,16 +29,8 @@ export default class WorkflowStatus extends Command {
|
|
|
16
29
|
required: true
|
|
17
30
|
})
|
|
18
31
|
};
|
|
19
|
-
static flags = {
|
|
20
|
-
format: Flags.string({
|
|
21
|
-
char: 'f',
|
|
22
|
-
description: 'Output format',
|
|
23
|
-
options: [OUTPUT_FORMAT.JSON, OUTPUT_FORMAT.TEXT],
|
|
24
|
-
default: OUTPUT_FORMAT.TEXT
|
|
25
|
-
})
|
|
26
|
-
};
|
|
27
32
|
async run() {
|
|
28
|
-
const { args
|
|
33
|
+
const { args } = await this.parse(WorkflowStatus);
|
|
29
34
|
this.log(`Fetching status for workflow: ${args.workflowId}...`);
|
|
30
35
|
const response = await getWorkflowIdStatus(args.workflowId);
|
|
31
36
|
if (!response || !response.data) {
|
|
@@ -36,23 +41,8 @@ export default class WorkflowStatus extends Command {
|
|
|
36
41
|
...rawData,
|
|
37
42
|
status: normalizeWorkflowStatus(rawData.status)
|
|
38
43
|
};
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
`Workflow ID: ${result.workflowId || 'unknown'}`,
|
|
42
|
-
`Status: ${result.status || 'unknown'}`,
|
|
43
|
-
''
|
|
44
|
-
];
|
|
45
|
-
if (result.startedAt) {
|
|
46
|
-
const startDate = new Date(result.startedAt);
|
|
47
|
-
lines.push(`Started At: ${startDate.toISOString()}`);
|
|
48
|
-
}
|
|
49
|
-
if (result.completedAt) {
|
|
50
|
-
const completedDate = new Date(result.completedAt);
|
|
51
|
-
lines.push(`Completed At: ${completedDate.toISOString()}`);
|
|
52
|
-
}
|
|
53
|
-
return lines.join('\n');
|
|
54
|
-
});
|
|
55
|
-
this.log(`\n${output}`);
|
|
44
|
+
this.log(`\n${formatStatusText(data)}`);
|
|
45
|
+
return data;
|
|
56
46
|
}
|
|
57
47
|
async catch(error) {
|
|
58
48
|
return handleApiError(error, (...args) => this.error(...args), {
|
|
@@ -22,12 +22,10 @@ describe('workflow status command', () => {
|
|
|
22
22
|
expect(WorkflowStatus).toBeDefined();
|
|
23
23
|
expect(WorkflowStatus.description).toContain('Get workflow execution status');
|
|
24
24
|
expect(WorkflowStatus.args).toHaveProperty('workflowId');
|
|
25
|
-
expect(WorkflowStatus.flags).toHaveProperty('format');
|
|
26
25
|
});
|
|
27
|
-
it('
|
|
26
|
+
it('enables the built-in --json flag', async () => {
|
|
28
27
|
const WorkflowStatus = (await import('./status.js')).default;
|
|
29
|
-
expect(WorkflowStatus.
|
|
30
|
-
expect(WorkflowStatus.flags.format.default).toBe('text');
|
|
28
|
+
expect(WorkflowStatus.enableJsonFlag).toBe(true);
|
|
31
29
|
});
|
|
32
30
|
});
|
|
33
31
|
});
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
|
+
import type { EvalOutput } from '@outputai/evals';
|
|
2
3
|
export default class WorkflowTest extends Command {
|
|
3
4
|
static aliases: string[];
|
|
4
5
|
static description: string;
|
|
6
|
+
static enableJsonFlag: boolean;
|
|
5
7
|
static examples: string[];
|
|
6
8
|
static args: {
|
|
7
9
|
workflowName: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
@@ -10,9 +12,9 @@ export default class WorkflowTest extends Command {
|
|
|
10
12
|
cached: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
13
|
save: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
14
|
dataset: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
-
format: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
15
|
};
|
|
15
|
-
run(): Promise<
|
|
16
|
+
run(): Promise<EvalOutput>;
|
|
17
|
+
private ensureEvalWorkflowRegistered;
|
|
16
18
|
private validateDatasets;
|
|
17
19
|
private runWorkflowForDatasets;
|
|
18
20
|
private saveEvalResults;
|
|
@@ -2,17 +2,20 @@ import { join } from 'node:path';
|
|
|
2
2
|
import { Args, Command, Flags } from '@oclif/core';
|
|
3
3
|
import { postWorkflowRun } from '#api/generated/api.js';
|
|
4
4
|
import { readAllDatasets, writeDataset } from '#services/datasets.js';
|
|
5
|
+
import { fetchWorkflowCatalog } from '#api/workflow_catalog.js';
|
|
6
|
+
import { diagnoseMissingEvalWorkflow } from '#utils/eval_diagnostics.js';
|
|
5
7
|
import { handleApiError } from '#utils/error_handler.js';
|
|
6
8
|
import { getEvalWorkflowName, renderEvalOutput, computeExitCode, EvalOutputSchema } from '@outputai/evals';
|
|
7
9
|
export default class WorkflowTest extends Command {
|
|
8
10
|
static aliases = ['workflow:test'];
|
|
9
11
|
static description = 'Run evaluations against a workflow using its datasets';
|
|
12
|
+
static enableJsonFlag = true;
|
|
10
13
|
static examples = [
|
|
11
14
|
'<%= config.bin %> <%= command.id %> simple',
|
|
12
15
|
'<%= config.bin %> <%= command.id %> simple --cached',
|
|
13
16
|
'<%= config.bin %> <%= command.id %> simple --save',
|
|
14
17
|
'<%= config.bin %> <%= command.id %> simple --dataset basic_input,edge_case',
|
|
15
|
-
'<%= config.bin %> <%= command.id %> simple --
|
|
18
|
+
'<%= config.bin %> <%= command.id %> simple --json'
|
|
16
19
|
];
|
|
17
20
|
static args = {
|
|
18
21
|
workflowName: Args.string({
|
|
@@ -34,17 +37,13 @@ export default class WorkflowTest extends Command {
|
|
|
34
37
|
dataset: Flags.string({
|
|
35
38
|
description: 'Comma-separated list of dataset names to run',
|
|
36
39
|
char: 'd'
|
|
37
|
-
}),
|
|
38
|
-
format: Flags.string({
|
|
39
|
-
char: 'f',
|
|
40
|
-
description: 'Output format',
|
|
41
|
-
options: ['json', 'text'],
|
|
42
|
-
default: 'text'
|
|
43
40
|
})
|
|
44
41
|
};
|
|
45
42
|
async run() {
|
|
46
43
|
const { args, flags } = await this.parse(WorkflowTest);
|
|
47
44
|
const filterNames = flags.dataset?.split(',').map(s => s.trim());
|
|
45
|
+
const evalName = getEvalWorkflowName(args.workflowName);
|
|
46
|
+
await this.ensureEvalWorkflowRegistered(args.workflowName, evalName);
|
|
48
47
|
const { datasets, dir } = await readAllDatasets(args.workflowName, filterNames);
|
|
49
48
|
if (datasets.length === 0) {
|
|
50
49
|
this.error(`No datasets found for workflow "${args.workflowName}".\n` +
|
|
@@ -53,7 +52,6 @@ export default class WorkflowTest extends Command {
|
|
|
53
52
|
const preparedDatasets = flags.cached ?
|
|
54
53
|
this.validateDatasets(datasets) :
|
|
55
54
|
await this.runWorkflowForDatasets(args.workflowName, datasets, flags.save, dir);
|
|
56
|
-
const evalName = getEvalWorkflowName(args.workflowName);
|
|
57
55
|
this.log(`Running eval workflow "${evalName}"...\n`);
|
|
58
56
|
const response = await postWorkflowRun({
|
|
59
57
|
workflowName: evalName,
|
|
@@ -69,15 +67,16 @@ export default class WorkflowTest extends Command {
|
|
|
69
67
|
if (flags.save) {
|
|
70
68
|
await this.saveEvalResults(evalOutput, preparedDatasets, dir);
|
|
71
69
|
}
|
|
72
|
-
if (
|
|
73
|
-
this.log(JSON.stringify(evalOutput, null, 2));
|
|
74
|
-
}
|
|
75
|
-
else {
|
|
70
|
+
if (!this.jsonEnabled()) {
|
|
76
71
|
this.log(renderEvalOutput(evalOutput, evalName));
|
|
77
72
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
73
|
+
process.exitCode = computeExitCode(evalOutput);
|
|
74
|
+
return evalOutput;
|
|
75
|
+
}
|
|
76
|
+
async ensureEvalWorkflowRegistered(workflowName, evalName) {
|
|
77
|
+
const catalog = await fetchWorkflowCatalog().catch(() => null);
|
|
78
|
+
if (catalog && !catalog.some(w => w.name === evalName)) {
|
|
79
|
+
this.error(await diagnoseMissingEvalWorkflow(workflowName), { exit: 1 });
|
|
81
80
|
}
|
|
82
81
|
}
|
|
83
82
|
validateDatasets(datasets) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
3
|
+
import { getEvalWorkflowName, renderEvalOutput } from '@outputai/evals';
|
|
4
|
+
vi.mock('#api/generated/api.js', () => ({
|
|
5
|
+
postWorkflowRun: vi.fn()
|
|
6
|
+
}));
|
|
7
|
+
vi.mock('#services/datasets.js', () => ({
|
|
8
|
+
readAllDatasets: vi.fn(),
|
|
9
|
+
writeDataset: vi.fn()
|
|
10
|
+
}));
|
|
11
|
+
const passingOutput = {
|
|
12
|
+
cases: [{ datasetName: 'd1', verdict: 'pass', evaluators: [] }],
|
|
13
|
+
summary: { total: 1, passed: 1, partial: 0, failed: 0, acceptableRate: 1 }
|
|
14
|
+
};
|
|
15
|
+
const failingOutput = {
|
|
16
|
+
cases: [{ datasetName: 'd1', verdict: 'fail', evaluators: [] }],
|
|
17
|
+
summary: { total: 1, passed: 0, partial: 0, failed: 1, acceptableRate: 0 }
|
|
18
|
+
};
|
|
19
|
+
describe('workflow test command', () => {
|
|
20
|
+
const exitState = { original: undefined };
|
|
21
|
+
beforeEach(async () => {
|
|
22
|
+
vi.clearAllMocks();
|
|
23
|
+
exitState.original = process.exitCode;
|
|
24
|
+
process.exitCode = undefined;
|
|
25
|
+
const { readAllDatasets } = await import('#services/datasets.js');
|
|
26
|
+
vi.mocked(readAllDatasets).mockResolvedValue({
|
|
27
|
+
datasets: [{ name: 'd1', input: {}, last_output: { output: {}, date: '2026-01-01' } }],
|
|
28
|
+
dir: '/tmp/datasets'
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
process.exitCode = exitState.original;
|
|
33
|
+
});
|
|
34
|
+
describe('command definition', () => {
|
|
35
|
+
it('enables the built-in --json flag', async () => {
|
|
36
|
+
const WorkflowTest = (await import('./test_eval.js')).default;
|
|
37
|
+
expect(WorkflowTest.enableJsonFlag).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
describe('run()', () => {
|
|
41
|
+
const createCommand = async (jsonEnabled) => {
|
|
42
|
+
const WorkflowTest = (await import('./test_eval.js')).default;
|
|
43
|
+
const { postWorkflowRun } = await import('#api/generated/api.js');
|
|
44
|
+
const cmd = new WorkflowTest(['simple'], {});
|
|
45
|
+
cmd.log = vi.fn();
|
|
46
|
+
cmd.jsonEnabled = vi.fn().mockReturnValue(jsonEnabled);
|
|
47
|
+
cmd.parse = vi.fn().mockResolvedValue({
|
|
48
|
+
args: { workflowName: 'simple' },
|
|
49
|
+
flags: { cached: true, save: false, dataset: undefined }
|
|
50
|
+
});
|
|
51
|
+
return { cmd, postWorkflowRun: vi.mocked(postWorkflowRun) };
|
|
52
|
+
};
|
|
53
|
+
it('sets a non-zero exit code and returns the eval output when a case fails', async () => {
|
|
54
|
+
const { cmd, postWorkflowRun } = await createCommand(false);
|
|
55
|
+
postWorkflowRun.mockResolvedValue({ data: { output: failingOutput } });
|
|
56
|
+
const result = await cmd.run();
|
|
57
|
+
expect(result).toEqual(failingOutput);
|
|
58
|
+
expect(process.exitCode).toBe(1);
|
|
59
|
+
});
|
|
60
|
+
it('leaves the exit code at zero and returns the eval output when all cases pass', async () => {
|
|
61
|
+
const { cmd, postWorkflowRun } = await createCommand(false);
|
|
62
|
+
postWorkflowRun.mockResolvedValue({ data: { output: passingOutput } });
|
|
63
|
+
const result = await cmd.run();
|
|
64
|
+
expect(result).toEqual(passingOutput);
|
|
65
|
+
expect(process.exitCode).toBe(0);
|
|
66
|
+
});
|
|
67
|
+
it('renders the human-readable summary in text mode', async () => {
|
|
68
|
+
const { cmd, postWorkflowRun } = await createCommand(false);
|
|
69
|
+
postWorkflowRun.mockResolvedValue({ data: { output: passingOutput } });
|
|
70
|
+
await cmd.run();
|
|
71
|
+
const rendered = renderEvalOutput(passingOutput, getEvalWorkflowName('simple'));
|
|
72
|
+
expect(cmd.log).toHaveBeenCalledWith(rendered);
|
|
73
|
+
});
|
|
74
|
+
it('suppresses the rendered summary in JSON mode but still returns and sets exit code', async () => {
|
|
75
|
+
const { cmd, postWorkflowRun } = await createCommand(true);
|
|
76
|
+
postWorkflowRun.mockResolvedValue({ data: { output: failingOutput } });
|
|
77
|
+
const result = await cmd.run();
|
|
78
|
+
const rendered = renderEvalOutput(failingOutput, getEvalWorkflowName('simple'));
|
|
79
|
+
expect(cmd.log).not.toHaveBeenCalledWith(rendered);
|
|
80
|
+
expect(result).toEqual(failingOutput);
|
|
81
|
+
expect(process.exitCode).toBe(1);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
package/dist/hooks/init.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Hook } from '@oclif/core';
|
|
|
2
2
|
export declare const INTERACTIVE_FLAGS: string[];
|
|
3
3
|
export declare const GLOBAL_FLAGS: Set<string>;
|
|
4
4
|
export declare const hasInteractiveFlag: (argv: string[]) => boolean;
|
|
5
|
+
export declare const hasJsonFlag: (argv: string[]) => boolean;
|
|
5
6
|
export declare const stripGlobalFlags: (argv: string[]) => void;
|
|
6
7
|
declare const hook: Hook<'init'>;
|
|
7
8
|
export default hook;
|
package/dist/hooks/init.js
CHANGED
|
@@ -6,6 +6,10 @@ const debug = debugFactory('output-cli:init');
|
|
|
6
6
|
export const INTERACTIVE_FLAGS = ['--yes', '--non-interactive'];
|
|
7
7
|
export const GLOBAL_FLAGS = new Set(INTERACTIVE_FLAGS);
|
|
8
8
|
export const hasInteractiveFlag = (argv) => argv.some(arg => INTERACTIVE_FLAGS.includes(arg));
|
|
9
|
+
// The version banner must never reach stdout in JSON mode, where it would
|
|
10
|
+
// corrupt the machine-readable output. oclif only suppresses `this.log` inside
|
|
11
|
+
// the command, not hook output, so we detect `--json` ourselves.
|
|
12
|
+
export const hasJsonFlag = (argv) => argv.includes('--json');
|
|
9
13
|
export const stripGlobalFlags = (argv) => {
|
|
10
14
|
const kept = argv.filter(arg => !GLOBAL_FLAGS.has(arg));
|
|
11
15
|
if (kept.length !== argv.length) {
|
|
@@ -19,37 +23,43 @@ const hook = async function (opts) {
|
|
|
19
23
|
if (interactive) {
|
|
20
24
|
setNonInteractive(true);
|
|
21
25
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const warning = ux.colorize('yellow', 'Uhoh! Your Output.ai CLI is behind!');
|
|
35
|
-
const latestVer = ux.colorize('green', `v${result.latestVersion}`);
|
|
36
|
-
const currentVer = ux.colorize('yellow', `v${result.currentVersion}`);
|
|
37
|
-
const updateCmd = ux.colorize('cyan', 'npx output update');
|
|
38
|
-
ux.stdout('');
|
|
39
|
-
ux.stdout(border);
|
|
40
|
-
ux.stdout('');
|
|
41
|
-
ux.stdout(` ⚠️ ${warning}`);
|
|
42
|
-
ux.stdout('');
|
|
43
|
-
ux.stdout(` Latest is ${latestVer}, and you're using ${currentVer}`);
|
|
44
|
-
ux.stdout('');
|
|
45
|
-
ux.stdout(` Run \`${updateCmd}\` to update`);
|
|
46
|
-
ux.stdout('');
|
|
47
|
-
ux.stdout(border);
|
|
48
|
-
ux.stdout('');
|
|
26
|
+
// Guard only the version-check IO: a broken or unreadable cache must never
|
|
27
|
+
// block CLI execution, so a failure is treated as "no cached result". Banner
|
|
28
|
+
// rendering below is intentionally left unguarded — a fault there is a real
|
|
29
|
+
// bug that should surface, not fail dark.
|
|
30
|
+
const result = await readCachedResult(this.config.version, this.config.cacheDir)
|
|
31
|
+
.catch((error) => {
|
|
32
|
+
debug('Version check failed: %O', error);
|
|
33
|
+
return null;
|
|
34
|
+
});
|
|
35
|
+
if (!result) {
|
|
36
|
+
spawnBackgroundRefresh(this.config.version, this.config.cacheDir);
|
|
37
|
+
return;
|
|
49
38
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
debug('Version banner failed: %O', error);
|
|
39
|
+
if (!result.updateAvailable) {
|
|
40
|
+
return;
|
|
53
41
|
}
|
|
42
|
+
// Skip the banner entirely in JSON mode: even on stderr it is pure noise to
|
|
43
|
+
// a script consuming the command's structured output.
|
|
44
|
+
if (hasJsonFlag(opts.argv) || hasJsonFlag(process.argv)) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const border = ux.colorize('dim', '─'.repeat(80));
|
|
48
|
+
const warning = ux.colorize('yellow', 'Uhoh! Your Output.ai CLI is behind!');
|
|
49
|
+
const latestVer = ux.colorize('green', `v${result.latestVersion}`);
|
|
50
|
+
const currentVer = ux.colorize('yellow', `v${result.currentVersion}`);
|
|
51
|
+
const updateCmd = ux.colorize('cyan', 'npx output update');
|
|
52
|
+
// Advisory notice goes to stderr so stdout stays clean for piping in every mode.
|
|
53
|
+
ux.stderr('');
|
|
54
|
+
ux.stderr(border);
|
|
55
|
+
ux.stderr('');
|
|
56
|
+
ux.stderr(` ⚠️ ${warning}`);
|
|
57
|
+
ux.stderr('');
|
|
58
|
+
ux.stderr(` Latest is ${latestVer}, and you're using ${currentVer}`);
|
|
59
|
+
ux.stderr('');
|
|
60
|
+
ux.stderr(` Run \`${updateCmd}\` to update`);
|
|
61
|
+
ux.stderr('');
|
|
62
|
+
ux.stderr(border);
|
|
63
|
+
ux.stderr('');
|
|
54
64
|
};
|
|
55
65
|
export default hook;
|
package/dist/hooks/init.spec.js
CHANGED
|
@@ -12,11 +12,12 @@ vi.mock('#utils/interactive.js', () => ({
|
|
|
12
12
|
vi.mock('@oclif/core', () => ({
|
|
13
13
|
ux: {
|
|
14
14
|
stdout: vi.fn(),
|
|
15
|
+
stderr: vi.fn(),
|
|
15
16
|
colorize: vi.fn((_color, text) => text)
|
|
16
17
|
}
|
|
17
18
|
}));
|
|
18
19
|
import { ux } from '@oclif/core';
|
|
19
|
-
import hook, { hasInteractiveFlag, stripGlobalFlags } from './init.js';
|
|
20
|
+
import hook, { hasInteractiveFlag, hasJsonFlag, stripGlobalFlags } from './init.js';
|
|
20
21
|
describe('init hook', () => {
|
|
21
22
|
beforeEach(() => {
|
|
22
23
|
vi.clearAllMocks();
|
|
@@ -24,7 +25,7 @@ describe('init hook', () => {
|
|
|
24
25
|
const createHookContext = (version = '0.8.4') => ({
|
|
25
26
|
config: { version, cacheDir: '/tmp/test-cache' }
|
|
26
27
|
});
|
|
27
|
-
it('should display warning when cached result says an update is available', async () => {
|
|
28
|
+
it('should display warning on stderr when cached result says an update is available', async () => {
|
|
28
29
|
vi.mocked(readCachedResult).mockResolvedValue({
|
|
29
30
|
updateAvailable: true,
|
|
30
31
|
currentVersion: '0.8.4',
|
|
@@ -34,13 +35,25 @@ describe('init hook', () => {
|
|
|
34
35
|
await hook.call(ctx, { argv: [], id: undefined });
|
|
35
36
|
expect(readCachedResult).toHaveBeenCalledWith('0.8.4', '/tmp/test-cache');
|
|
36
37
|
expect(spawnBackgroundRefresh).not.toHaveBeenCalled();
|
|
37
|
-
expect(ux.
|
|
38
|
-
|
|
38
|
+
expect(ux.stderr).toHaveBeenCalled();
|
|
39
|
+
expect(ux.stdout).not.toHaveBeenCalled();
|
|
40
|
+
const output = vi.mocked(ux.stderr).mock.calls.map(c => c[0]).join('\n');
|
|
39
41
|
expect(output).toContain('Uhoh');
|
|
40
42
|
expect(output).toContain('v1.0.0');
|
|
41
43
|
expect(output).toContain('v0.8.4');
|
|
42
44
|
expect(output).toContain('npx output update');
|
|
43
45
|
});
|
|
46
|
+
it('should suppress the warning entirely in JSON mode', async () => {
|
|
47
|
+
vi.mocked(readCachedResult).mockResolvedValue({
|
|
48
|
+
updateAvailable: true,
|
|
49
|
+
currentVersion: '0.8.4',
|
|
50
|
+
latestVersion: '1.0.0'
|
|
51
|
+
});
|
|
52
|
+
const ctx = createHookContext();
|
|
53
|
+
await hook.call(ctx, { argv: ['--json'], id: undefined });
|
|
54
|
+
expect(ux.stderr).not.toHaveBeenCalled();
|
|
55
|
+
expect(ux.stdout).not.toHaveBeenCalled();
|
|
56
|
+
});
|
|
44
57
|
it('should not display anything when up to date', async () => {
|
|
45
58
|
vi.mocked(readCachedResult).mockResolvedValue({
|
|
46
59
|
updateAvailable: false,
|
|
@@ -121,6 +134,17 @@ describe('init hook', () => {
|
|
|
121
134
|
expect(hasInteractiveFlag([])).toBe(false);
|
|
122
135
|
});
|
|
123
136
|
});
|
|
137
|
+
describe('hasJsonFlag', () => {
|
|
138
|
+
it('returns true when --json is present', () => {
|
|
139
|
+
expect(hasJsonFlag(['workflow', 'runs', 'list', '--json'])).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
it('returns false when --json is absent', () => {
|
|
142
|
+
expect(hasJsonFlag(['workflow', 'runs', 'list', '--format', 'table'])).toBe(false);
|
|
143
|
+
});
|
|
144
|
+
it('returns false for an empty argv', () => {
|
|
145
|
+
expect(hasJsonFlag([])).toBe(false);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
124
148
|
describe('stripGlobalFlags', () => {
|
|
125
149
|
it('mutates argv in place to remove global flags', () => {
|
|
126
150
|
const argv = ['init', '--yes', 'foo', '--non-interactive'];
|
|
@@ -6,8 +6,8 @@ export interface DatasetInfo {
|
|
|
6
6
|
lastOutputDate?: string;
|
|
7
7
|
lastEvalDate?: string;
|
|
8
8
|
}
|
|
9
|
-
export declare function resolveDatasetsDir(workflowName: string, basePath?: string): string | null
|
|
10
|
-
export declare function resolveDefaultDatasetsDir(workflowName: string, basePath?: string): string
|
|
9
|
+
export declare function resolveDatasetsDir(workflowName: string, basePath?: string, workflowPath?: string): Promise<string | null>;
|
|
10
|
+
export declare function resolveDefaultDatasetsDir(workflowName: string, basePath?: string, workflowPath?: string): Promise<string>;
|
|
11
11
|
export declare function readDatasetFile(filePath: string): Promise<Dataset[]>;
|
|
12
12
|
export declare function readAllDatasets(workflowName: string, filterNames?: string[], basePath?: string): Promise<{
|
|
13
13
|
datasets: Dataset[];
|
|
@@ -5,24 +5,26 @@ import yaml from 'js-yaml';
|
|
|
5
5
|
import { DatasetSchema } from '@outputai/evals';
|
|
6
6
|
import { getTrace } from '#services/trace_reader.js';
|
|
7
7
|
import { sanitizeSecrets } from '#utils/secret_sanitizer.js';
|
|
8
|
+
import { resolveWorkflowDir, WORKFLOWS_PATHS } from '#utils/workflow_dir.js';
|
|
9
|
+
import { getWorkflowsBasePath } from '#utils/paths.js';
|
|
8
10
|
const DATASETS_DIR = 'tests/datasets';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if (existsSync(candidate)) {
|
|
14
|
-
return candidate;
|
|
15
|
-
}
|
|
11
|
+
export async function resolveDatasetsDir(workflowName, basePath, workflowPath) {
|
|
12
|
+
const workflowDir = await resolveWorkflowDir(workflowName, basePath, workflowPath);
|
|
13
|
+
if (!workflowDir) {
|
|
14
|
+
return null;
|
|
16
15
|
}
|
|
17
|
-
|
|
16
|
+
const datasetsDir = resolve(workflowDir, DATASETS_DIR);
|
|
17
|
+
return existsSync(datasetsDir) ? datasetsDir : null;
|
|
18
18
|
}
|
|
19
|
-
export function resolveDefaultDatasetsDir(workflowName, basePath
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
export async function resolveDefaultDatasetsDir(workflowName, basePath, workflowPath) {
|
|
20
|
+
// Write into the resolved workflow's tests/datasets — even when it doesn't
|
|
21
|
+
// exist yet (first generation) — so nested workflows get the right location.
|
|
22
|
+
const workflowDir = await resolveWorkflowDir(workflowName, basePath, workflowPath);
|
|
23
|
+
if (workflowDir) {
|
|
24
|
+
return resolve(workflowDir, DATASETS_DIR);
|
|
23
25
|
}
|
|
24
|
-
//
|
|
25
|
-
return resolve(basePath, WORKFLOWS_PATHS[0], workflowName, DATASETS_DIR);
|
|
26
|
+
// Workflow not found (unknown name / API unavailable): flat convention.
|
|
27
|
+
return resolve(basePath ?? getWorkflowsBasePath(), WORKFLOWS_PATHS[0], workflowName, DATASETS_DIR);
|
|
26
28
|
}
|
|
27
29
|
export async function readDatasetFile(filePath) {
|
|
28
30
|
const raw = yaml.load(await readFile(filePath, 'utf-8'));
|
|
@@ -39,9 +41,9 @@ export async function readDatasetFile(filePath) {
|
|
|
39
41
|
});
|
|
40
42
|
}
|
|
41
43
|
export async function readAllDatasets(workflowName, filterNames, basePath) {
|
|
42
|
-
const dir = resolveDatasetsDir(workflowName, basePath);
|
|
44
|
+
const dir = await resolveDatasetsDir(workflowName, basePath);
|
|
43
45
|
if (!dir) {
|
|
44
|
-
return { datasets: [], dir: resolveDefaultDatasetsDir(workflowName, basePath) };
|
|
46
|
+
return { datasets: [], dir: await resolveDefaultDatasetsDir(workflowName, basePath) };
|
|
45
47
|
}
|
|
46
48
|
const files = await readdir(dir);
|
|
47
49
|
const ymlFiles = files.filter(f => f.endsWith('.yml') || f.endsWith('.yaml'));
|
|
@@ -76,7 +78,7 @@ export async function writeDataset(dataset, filePath) {
|
|
|
76
78
|
await writeFile(filePath, yaml.dump(fileObj, { lineWidth: 120, noRefs: true, sortKeys: false }), 'utf-8');
|
|
77
79
|
}
|
|
78
80
|
export async function listDatasets(workflowName, basePath) {
|
|
79
|
-
const dir = resolveDatasetsDir(workflowName, basePath);
|
|
81
|
+
const dir = await resolveDatasetsDir(workflowName, basePath);
|
|
80
82
|
if (!dir) {
|
|
81
83
|
return [];
|
|
82
84
|
}
|