@outputai/cli 0.8.2-next.e1cd79b.0 → 0.8.2-next.e658cc2.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 +79 -0
- package/dist/api/generated/api.js +32 -0
- 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.d.ts +1 -0
- package/dist/commands/workflow/dataset/generate.js +15 -6
- package/dist/commands/workflow/dataset/generate.spec.d.ts +1 -0
- package/dist/commands/workflow/dataset/generate.spec.js +69 -0
- 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/generate.spec.js +2 -2
- 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 +5 -12
- package/dist/commands/workflow/run.spec.js +18 -3
- package/dist/commands/workflow/runs/list.d.ts +3 -1
- package/dist/commands/workflow/runs/list.js +11 -15
- package/dist/commands/workflow/start.js +1 -1
- package/dist/commands/workflow/start.spec.js +53 -2
- 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 +23 -23
- package/dist/commands/workflow/test_eval.spec.d.ts +1 -0
- package/dist/commands/workflow/test_eval.spec.js +121 -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/coding_agents.spec.js +10 -10
- package/dist/utils/format_workflow_result.spec.js +0 -13
- package/dist/utils/resolve_input.d.ts +1 -1
- package/dist/utils/resolve_input.js +2 -2
- package/dist/utils/scenario_resolver.d.ts +1 -1
- package/dist/utils/scenario_resolver.js +2 -2
- package/dist/utils/scenario_resolver.spec.js +14 -0
- package/dist/utils/trace_formatter.js +1 -2
- package/dist/utils/workflow_dir.d.ts +1 -1
- package/dist/utils/workflow_dir.js +2 -2
- package/dist/views/dev/modals/run_modal.d.ts +9 -0
- package/dist/views/dev/modals/run_modal.js +56 -56
- package/dist/views/dev/modals/run_modal.spec.d.ts +1 -0
- package/dist/views/dev/modals/run_modal.spec.js +34 -0
- package/dist/views/dev/panels/help_panel.js +1 -1
- package/dist/views/dev/utils/json_editor.d.ts +2 -0
- package/dist/views/dev/utils/json_editor.js +28 -15
- package/dist/views/dev/utils/json_editor.spec.js +16 -1
- package/oclif.manifest.json +114 -101
- package/package.json +5 -5
- 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
|
@@ -9,12 +9,13 @@ import { getEvalWorkflowName, renderEvalOutput, computeExitCode, EvalOutputSchem
|
|
|
9
9
|
export default class WorkflowTest extends Command {
|
|
10
10
|
static aliases = ['workflow:test'];
|
|
11
11
|
static description = 'Run evaluations against a workflow using its datasets';
|
|
12
|
+
static enableJsonFlag = true;
|
|
12
13
|
static examples = [
|
|
13
14
|
'<%= config.bin %> <%= command.id %> simple',
|
|
14
15
|
'<%= config.bin %> <%= command.id %> simple --cached',
|
|
15
16
|
'<%= config.bin %> <%= command.id %> simple --save',
|
|
16
17
|
'<%= config.bin %> <%= command.id %> simple --dataset basic_input,edge_case',
|
|
17
|
-
'<%= config.bin %> <%= command.id %> simple --
|
|
18
|
+
'<%= config.bin %> <%= command.id %> simple --json'
|
|
18
19
|
];
|
|
19
20
|
static args = {
|
|
20
21
|
workflowName: Args.string({
|
|
@@ -23,6 +24,14 @@ export default class WorkflowTest extends Command {
|
|
|
23
24
|
})
|
|
24
25
|
};
|
|
25
26
|
static flags = {
|
|
27
|
+
catalog: Flags.string({
|
|
28
|
+
char: 'c',
|
|
29
|
+
aliases: ['task-queue'],
|
|
30
|
+
charAliases: ['q'],
|
|
31
|
+
deprecateAliases: true,
|
|
32
|
+
description: 'Catalog name for workflow execution (defaults to OUTPUT_CATALOG_ID)',
|
|
33
|
+
env: 'OUTPUT_CATALOG_ID'
|
|
34
|
+
}),
|
|
26
35
|
cached: Flags.boolean({
|
|
27
36
|
description: 'Use cached output from dataset files (skip workflow execution)',
|
|
28
37
|
default: false,
|
|
@@ -36,19 +45,13 @@ export default class WorkflowTest extends Command {
|
|
|
36
45
|
dataset: Flags.string({
|
|
37
46
|
description: 'Comma-separated list of dataset names to run',
|
|
38
47
|
char: 'd'
|
|
39
|
-
}),
|
|
40
|
-
format: Flags.string({
|
|
41
|
-
char: 'f',
|
|
42
|
-
description: 'Output format',
|
|
43
|
-
options: ['json', 'text'],
|
|
44
|
-
default: 'text'
|
|
45
48
|
})
|
|
46
49
|
};
|
|
47
50
|
async run() {
|
|
48
51
|
const { args, flags } = await this.parse(WorkflowTest);
|
|
49
52
|
const filterNames = flags.dataset?.split(',').map(s => s.trim());
|
|
50
53
|
const evalName = getEvalWorkflowName(args.workflowName);
|
|
51
|
-
await this.ensureEvalWorkflowRegistered(args.workflowName, evalName);
|
|
54
|
+
await this.ensureEvalWorkflowRegistered(args.workflowName, evalName, flags.catalog);
|
|
52
55
|
const { datasets, dir } = await readAllDatasets(args.workflowName, filterNames);
|
|
53
56
|
if (datasets.length === 0) {
|
|
54
57
|
this.error(`No datasets found for workflow "${args.workflowName}".\n` +
|
|
@@ -56,11 +59,12 @@ export default class WorkflowTest extends Command {
|
|
|
56
59
|
}
|
|
57
60
|
const preparedDatasets = flags.cached ?
|
|
58
61
|
this.validateDatasets(datasets) :
|
|
59
|
-
await this.runWorkflowForDatasets(args.workflowName, datasets, flags.save, dir);
|
|
62
|
+
await this.runWorkflowForDatasets(args.workflowName, datasets, flags.save, dir, flags.catalog);
|
|
60
63
|
this.log(`Running eval workflow "${evalName}"...\n`);
|
|
61
64
|
const response = await postWorkflowRun({
|
|
62
65
|
workflowName: evalName,
|
|
63
|
-
input: { datasets: preparedDatasets }
|
|
66
|
+
input: { datasets: preparedDatasets },
|
|
67
|
+
catalog: flags.catalog
|
|
64
68
|
}, {
|
|
65
69
|
config: { timeout: 600000 }
|
|
66
70
|
});
|
|
@@ -72,20 +76,15 @@ export default class WorkflowTest extends Command {
|
|
|
72
76
|
if (flags.save) {
|
|
73
77
|
await this.saveEvalResults(evalOutput, preparedDatasets, dir);
|
|
74
78
|
}
|
|
75
|
-
if (
|
|
76
|
-
this.log(JSON.stringify(evalOutput, null, 2));
|
|
77
|
-
}
|
|
78
|
-
else {
|
|
79
|
+
if (!this.jsonEnabled()) {
|
|
79
80
|
this.log(renderEvalOutput(evalOutput, evalName));
|
|
80
81
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
this.exit(exitCode);
|
|
84
|
-
}
|
|
82
|
+
process.exitCode = computeExitCode(evalOutput);
|
|
83
|
+
return evalOutput;
|
|
85
84
|
}
|
|
86
|
-
async ensureEvalWorkflowRegistered(workflowName, evalName) {
|
|
87
|
-
const
|
|
88
|
-
if (
|
|
85
|
+
async ensureEvalWorkflowRegistered(workflowName, evalName, catalog) {
|
|
86
|
+
const workflows = await fetchWorkflowCatalog(catalog).catch(() => null);
|
|
87
|
+
if (workflows && !workflows.some(w => w.name === evalName)) {
|
|
89
88
|
this.error(await diagnoseMissingEvalWorkflow(workflowName), { exit: 1 });
|
|
90
89
|
}
|
|
91
90
|
}
|
|
@@ -98,7 +97,7 @@ export default class WorkflowTest extends Command {
|
|
|
98
97
|
}
|
|
99
98
|
return datasets;
|
|
100
99
|
}
|
|
101
|
-
async runWorkflowForDatasets(workflowName, datasets, save, dir) {
|
|
100
|
+
async runWorkflowForDatasets(workflowName, datasets, save, dir, catalog) {
|
|
102
101
|
this.log(`Running workflow "${workflowName}" for ${datasets.length} dataset(s)...\n`);
|
|
103
102
|
const results = [];
|
|
104
103
|
for (const dataset of datasets) {
|
|
@@ -106,7 +105,8 @@ export default class WorkflowTest extends Command {
|
|
|
106
105
|
const startMs = Date.now();
|
|
107
106
|
const response = await postWorkflowRun({
|
|
108
107
|
workflowName,
|
|
109
|
-
input: dataset.input
|
|
108
|
+
input: dataset.input,
|
|
109
|
+
catalog
|
|
110
110
|
}, {
|
|
111
111
|
config: { timeout: 600000 }
|
|
112
112
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,121 @@
|
|
|
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('#api/workflow_catalog.js', () => ({
|
|
8
|
+
fetchWorkflowCatalog: vi.fn()
|
|
9
|
+
}));
|
|
10
|
+
vi.mock('#services/datasets.js', () => ({
|
|
11
|
+
readAllDatasets: vi.fn(),
|
|
12
|
+
writeDataset: vi.fn()
|
|
13
|
+
}));
|
|
14
|
+
vi.mock('#utils/eval_diagnostics.js', () => ({
|
|
15
|
+
diagnoseMissingEvalWorkflow: vi.fn().mockResolvedValue('missing eval workflow')
|
|
16
|
+
}));
|
|
17
|
+
const passingOutput = {
|
|
18
|
+
cases: [{ datasetName: 'd1', verdict: 'pass', evaluators: [] }],
|
|
19
|
+
summary: { total: 1, passed: 1, partial: 0, failed: 0, acceptableRate: 1 }
|
|
20
|
+
};
|
|
21
|
+
const failingOutput = {
|
|
22
|
+
cases: [{ datasetName: 'd1', verdict: 'fail', evaluators: [] }],
|
|
23
|
+
summary: { total: 1, passed: 0, partial: 0, failed: 1, acceptableRate: 0 }
|
|
24
|
+
};
|
|
25
|
+
describe('workflow test command', () => {
|
|
26
|
+
const exitState = { original: undefined };
|
|
27
|
+
beforeEach(async () => {
|
|
28
|
+
vi.clearAllMocks();
|
|
29
|
+
exitState.original = process.exitCode;
|
|
30
|
+
process.exitCode = undefined;
|
|
31
|
+
const { readAllDatasets } = await import('#services/datasets.js');
|
|
32
|
+
const { fetchWorkflowCatalog } = await import('#api/workflow_catalog.js');
|
|
33
|
+
vi.mocked(readAllDatasets).mockResolvedValue({
|
|
34
|
+
datasets: [{ name: 'd1', input: {}, last_output: { output: {}, date: '2026-01-01' } }],
|
|
35
|
+
dir: '/tmp/datasets'
|
|
36
|
+
});
|
|
37
|
+
// Catalog includes both eval names so ensureEvalWorkflowRegistered passes deterministically.
|
|
38
|
+
vi.mocked(fetchWorkflowCatalog).mockResolvedValue([
|
|
39
|
+
{ name: getEvalWorkflowName('simple') },
|
|
40
|
+
{ name: getEvalWorkflowName('my_workflow') }
|
|
41
|
+
]);
|
|
42
|
+
});
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
process.exitCode = exitState.original;
|
|
45
|
+
});
|
|
46
|
+
describe('command definition', () => {
|
|
47
|
+
it('enables the built-in --json flag', async () => {
|
|
48
|
+
const WorkflowTest = (await import('./test_eval.js')).default;
|
|
49
|
+
expect(WorkflowTest.enableJsonFlag).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
it('binds the catalog flag to OUTPUT_CATALOG_ID', async () => {
|
|
52
|
+
const WorkflowTest = (await import('./test_eval.js')).default;
|
|
53
|
+
expect(WorkflowTest.flags).toHaveProperty('catalog');
|
|
54
|
+
expect(WorkflowTest.flags.catalog.env).toBe('OUTPUT_CATALOG_ID');
|
|
55
|
+
expect(WorkflowTest.flags.catalog.char).toBe('c');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
describe('run()', () => {
|
|
59
|
+
const createCommand = async (jsonEnabled) => {
|
|
60
|
+
const WorkflowTest = (await import('./test_eval.js')).default;
|
|
61
|
+
const { postWorkflowRun } = await import('#api/generated/api.js');
|
|
62
|
+
const cmd = new WorkflowTest(['simple'], {});
|
|
63
|
+
cmd.log = vi.fn();
|
|
64
|
+
cmd.jsonEnabled = vi.fn().mockReturnValue(jsonEnabled);
|
|
65
|
+
cmd.parse = vi.fn().mockResolvedValue({
|
|
66
|
+
args: { workflowName: 'simple' },
|
|
67
|
+
flags: { cached: true, save: false, dataset: undefined }
|
|
68
|
+
});
|
|
69
|
+
return { cmd, postWorkflowRun: vi.mocked(postWorkflowRun) };
|
|
70
|
+
};
|
|
71
|
+
it('sets a non-zero exit code and returns the eval output when a case fails', async () => {
|
|
72
|
+
const { cmd, postWorkflowRun } = await createCommand(false);
|
|
73
|
+
postWorkflowRun.mockResolvedValue({ data: { output: failingOutput } });
|
|
74
|
+
const result = await cmd.run();
|
|
75
|
+
expect(result).toEqual(failingOutput);
|
|
76
|
+
expect(process.exitCode).toBe(1);
|
|
77
|
+
});
|
|
78
|
+
it('leaves the exit code at zero and returns the eval output when all cases pass', async () => {
|
|
79
|
+
const { cmd, postWorkflowRun } = await createCommand(false);
|
|
80
|
+
postWorkflowRun.mockResolvedValue({ data: { output: passingOutput } });
|
|
81
|
+
const result = await cmd.run();
|
|
82
|
+
expect(result).toEqual(passingOutput);
|
|
83
|
+
expect(process.exitCode).toBe(0);
|
|
84
|
+
});
|
|
85
|
+
it('renders the human-readable summary in text mode', async () => {
|
|
86
|
+
const { cmd, postWorkflowRun } = await createCommand(false);
|
|
87
|
+
postWorkflowRun.mockResolvedValue({ data: { output: passingOutput } });
|
|
88
|
+
await cmd.run();
|
|
89
|
+
const rendered = renderEvalOutput(passingOutput, getEvalWorkflowName('simple'));
|
|
90
|
+
expect(cmd.log).toHaveBeenCalledWith(rendered);
|
|
91
|
+
});
|
|
92
|
+
it('suppresses the rendered summary in JSON mode but still returns and sets exit code', async () => {
|
|
93
|
+
const { cmd, postWorkflowRun } = await createCommand(true);
|
|
94
|
+
postWorkflowRun.mockResolvedValue({ data: { output: failingOutput } });
|
|
95
|
+
const result = await cmd.run();
|
|
96
|
+
const rendered = renderEvalOutput(failingOutput, getEvalWorkflowName('simple'));
|
|
97
|
+
expect(cmd.log).not.toHaveBeenCalledWith(rendered);
|
|
98
|
+
expect(result).toEqual(failingOutput);
|
|
99
|
+
expect(process.exitCode).toBe(1);
|
|
100
|
+
});
|
|
101
|
+
it('routes registration, dataset runs, and the eval run to the resolved catalog', async () => {
|
|
102
|
+
const WorkflowTest = (await import('./test_eval.js')).default;
|
|
103
|
+
const { postWorkflowRun } = await import('#api/generated/api.js');
|
|
104
|
+
const { fetchWorkflowCatalog } = await import('#api/workflow_catalog.js');
|
|
105
|
+
const cmd = new WorkflowTest(['my_workflow'], {});
|
|
106
|
+
cmd.log = vi.fn();
|
|
107
|
+
cmd.jsonEnabled = vi.fn().mockReturnValue(false);
|
|
108
|
+
cmd.parse = vi.fn().mockResolvedValue({
|
|
109
|
+
args: { workflowName: 'my_workflow' },
|
|
110
|
+
flags: { catalog: 'my-catalog', cached: false, save: false, dataset: undefined }
|
|
111
|
+
});
|
|
112
|
+
vi.mocked(postWorkflowRun)
|
|
113
|
+
.mockResolvedValueOnce({ data: { output: {} }, status: 200, headers: new Headers() })
|
|
114
|
+
.mockResolvedValueOnce({ data: { output: passingOutput }, status: 200, headers: new Headers() });
|
|
115
|
+
await cmd.run();
|
|
116
|
+
expect(vi.mocked(fetchWorkflowCatalog)).toHaveBeenCalledWith('my-catalog');
|
|
117
|
+
expect(postWorkflowRun).toHaveBeenNthCalledWith(1, expect.objectContaining({ workflowName: 'my_workflow', catalog: 'my-catalog' }), expect.anything());
|
|
118
|
+
expect(postWorkflowRun).toHaveBeenNthCalledWith(2, expect.objectContaining({ workflowName: getEvalWorkflowName('my_workflow'), catalog: 'my-catalog' }), expect.anything());
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
});
|
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'];
|
|
@@ -3,13 +3,13 @@ import { checkAgentStructure, prepareTemplateVariables, initializeAgentConfig, e
|
|
|
3
3
|
import { access } from 'node:fs/promises';
|
|
4
4
|
import fs from 'node:fs/promises';
|
|
5
5
|
vi.mock('node:fs/promises');
|
|
6
|
-
vi.mock('
|
|
6
|
+
vi.mock('#utils/paths.js', () => ({
|
|
7
7
|
getTemplateDir: vi.fn().mockReturnValue('/templates')
|
|
8
8
|
}));
|
|
9
|
-
vi.mock('
|
|
9
|
+
vi.mock('#utils/template.js', () => ({
|
|
10
10
|
processTemplate: vi.fn().mockImplementation((content) => content)
|
|
11
11
|
}));
|
|
12
|
-
vi.mock('
|
|
12
|
+
vi.mock('#utils/claude.js', () => ({
|
|
13
13
|
executeClaudeCommand: vi.fn().mockResolvedValue(undefined)
|
|
14
14
|
}));
|
|
15
15
|
vi.mock('@oclif/core', () => ({
|
|
@@ -152,14 +152,14 @@ describe('coding_agents service', () => {
|
|
|
152
152
|
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
|
|
153
153
|
});
|
|
154
154
|
it('should call registerPluginMarketplace and installOutputAIPlugin', async () => {
|
|
155
|
-
const { executeClaudeCommand } = await import('
|
|
155
|
+
const { executeClaudeCommand } = await import('#utils/claude.js');
|
|
156
156
|
await ensureClaudePlugin('/test/project');
|
|
157
157
|
expect(executeClaudeCommand).toHaveBeenCalledWith(['plugin', 'marketplace', 'add', 'growthxai/output'], '/test/project', { ignoreFailure: true });
|
|
158
158
|
expect(executeClaudeCommand).toHaveBeenCalledWith(['plugin', 'marketplace', 'update', 'outputai'], '/test/project');
|
|
159
159
|
expect(executeClaudeCommand).toHaveBeenCalledWith(['plugin', 'install', 'outputai@outputai', '--scope', 'project'], '/test/project');
|
|
160
160
|
});
|
|
161
161
|
it('should show error and prompt user when plugin commands fail', async () => {
|
|
162
|
-
const { executeClaudeCommand } = await import('
|
|
162
|
+
const { executeClaudeCommand } = await import('#utils/claude.js');
|
|
163
163
|
const { confirm } = await import('#utils/prompt.js');
|
|
164
164
|
vi.mocked(executeClaudeCommand)
|
|
165
165
|
.mockResolvedValueOnce(undefined) // marketplace add
|
|
@@ -171,7 +171,7 @@ describe('coding_agents service', () => {
|
|
|
171
171
|
}));
|
|
172
172
|
});
|
|
173
173
|
it('should allow user to proceed without plugin setup if they confirm', async () => {
|
|
174
|
-
const { executeClaudeCommand } = await import('
|
|
174
|
+
const { executeClaudeCommand } = await import('#utils/claude.js');
|
|
175
175
|
const { confirm } = await import('#utils/prompt.js');
|
|
176
176
|
vi.mocked(executeClaudeCommand)
|
|
177
177
|
.mockRejectedValue(new Error('All plugin commands fail'));
|
|
@@ -221,7 +221,7 @@ describe('coding_agents service', () => {
|
|
|
221
221
|
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
|
|
222
222
|
});
|
|
223
223
|
it('should show error and prompt user when registerPluginMarketplace fails', async () => {
|
|
224
|
-
const { executeClaudeCommand } = await import('
|
|
224
|
+
const { executeClaudeCommand } = await import('#utils/claude.js');
|
|
225
225
|
const { confirm } = await import('#utils/prompt.js');
|
|
226
226
|
vi.mocked(executeClaudeCommand)
|
|
227
227
|
.mockResolvedValueOnce(undefined) // marketplace add
|
|
@@ -233,7 +233,7 @@ describe('coding_agents service', () => {
|
|
|
233
233
|
}));
|
|
234
234
|
});
|
|
235
235
|
it('should show error and prompt user when installOutputAIPlugin fails', async () => {
|
|
236
|
-
const { executeClaudeCommand } = await import('
|
|
236
|
+
const { executeClaudeCommand } = await import('#utils/claude.js');
|
|
237
237
|
const { confirm } = await import('#utils/prompt.js');
|
|
238
238
|
vi.mocked(executeClaudeCommand)
|
|
239
239
|
.mockResolvedValueOnce(undefined) // marketplace add
|
|
@@ -246,7 +246,7 @@ describe('coding_agents service', () => {
|
|
|
246
246
|
}));
|
|
247
247
|
});
|
|
248
248
|
it('should allow user to proceed without plugin setup if they confirm', async () => {
|
|
249
|
-
const { executeClaudeCommand } = await import('
|
|
249
|
+
const { executeClaudeCommand } = await import('#utils/claude.js');
|
|
250
250
|
const { confirm } = await import('#utils/prompt.js');
|
|
251
251
|
vi.mocked(executeClaudeCommand)
|
|
252
252
|
.mockRejectedValue(new Error('All plugin commands fail'));
|
|
@@ -256,7 +256,7 @@ describe('coding_agents service', () => {
|
|
|
256
256
|
expect(fs.mkdir).toHaveBeenCalled();
|
|
257
257
|
});
|
|
258
258
|
it('should rethrow plugin error in non-interactive mode without prompting', async () => {
|
|
259
|
-
const { executeClaudeCommand } = await import('
|
|
259
|
+
const { executeClaudeCommand } = await import('#utils/claude.js');
|
|
260
260
|
const { confirm } = await import('#utils/prompt.js');
|
|
261
261
|
const { isInteractive } = await import('#utils/interactive.js');
|
|
262
262
|
vi.mocked(isInteractive).mockReturnValueOnce(false);
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { formatWorkflowResult } from './format_workflow_result.js';
|
|
3
|
-
import { formatOutput } from './output_formatter.js';
|
|
4
3
|
describe('formatWorkflowResult', () => {
|
|
5
4
|
it('should display output for completed workflows', () => {
|
|
6
5
|
const result = formatWorkflowResult({
|
|
@@ -76,16 +75,4 @@ describe('formatWorkflowResult', () => {
|
|
|
76
75
|
expect(result).toContain('Status: failed');
|
|
77
76
|
expect(result).not.toContain('Error:');
|
|
78
77
|
});
|
|
79
|
-
it('should work with formatOutput for json format', () => {
|
|
80
|
-
const data = {
|
|
81
|
-
workflowId: 'wf-456',
|
|
82
|
-
status: 'failed',
|
|
83
|
-
output: null,
|
|
84
|
-
error: 'Activity task failed'
|
|
85
|
-
};
|
|
86
|
-
const output = formatOutput(data, 'json', formatWorkflowResult);
|
|
87
|
-
const parsed = JSON.parse(output);
|
|
88
|
-
expect(parsed.status).toBe('failed');
|
|
89
|
-
expect(parsed.error).toBe('Activity task failed');
|
|
90
|
-
});
|
|
91
78
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare function resolveInput(workflowName: string, scenario: string | undefined, inputFlag: string | undefined, commandName: string): Promise<unknown>;
|
|
1
|
+
export declare function resolveInput(workflowName: string, scenario: string | undefined, inputFlag: string | undefined, commandName: string, catalog?: string): Promise<unknown>;
|
|
@@ -1,7 +1,7 @@
|
|
|
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(workflowName, scenario, inputFlag, commandName) {
|
|
4
|
+
export async function resolveInput(workflowName, scenario, inputFlag, commandName, catalog) {
|
|
5
5
|
if (inputFlag && scenario) {
|
|
6
6
|
return ux.error('Cannot use both scenario argument and --input flag. Choose one.', { exit: 1 });
|
|
7
7
|
}
|
|
@@ -9,7 +9,7 @@ export async function resolveInput(workflowName, scenario, inputFlag, commandNam
|
|
|
9
9
|
return parseInputFlag(inputFlag);
|
|
10
10
|
}
|
|
11
11
|
if (scenario) {
|
|
12
|
-
const resolution = await resolveScenarioPath(workflowName, scenario);
|
|
12
|
+
const resolution = await resolveScenarioPath(workflowName, scenario, undefined, undefined, catalog);
|
|
13
13
|
if (!resolution.found) {
|
|
14
14
|
return ux.error(getScenarioNotFoundMessage(workflowName, scenario, resolution.searchedPaths), { exit: 1 });
|
|
15
15
|
}
|
|
@@ -4,6 +4,6 @@ export interface ScenarioResolutionResult {
|
|
|
4
4
|
path?: string;
|
|
5
5
|
searchedPaths: string[];
|
|
6
6
|
}
|
|
7
|
-
export declare function resolveScenarioPath(workflowName: string, scenarioName: string, basePath?: string, workflowPath?: string): Promise<ScenarioResolutionResult>;
|
|
7
|
+
export declare function resolveScenarioPath(workflowName: string, scenarioName: string, basePath?: string, workflowPath?: string, catalog?: string): Promise<ScenarioResolutionResult>;
|
|
8
8
|
export declare function listScenariosForWorkflow(workflowName: string, workflowPath?: string, basePath?: string): string[];
|
|
9
9
|
export declare function getScenarioNotFoundMessage(workflowName: string, scenarioName: string, searchedPaths: string[]): string;
|
|
@@ -26,7 +26,7 @@ function resolveScenarioFromScenarioDirs(scenariosDirs, scenarioFileName) {
|
|
|
26
26
|
{ found: true, path, searchedPaths } :
|
|
27
27
|
{ found: false, searchedPaths };
|
|
28
28
|
}
|
|
29
|
-
export async function resolveScenarioPath(workflowName, scenarioName, basePath = getWorkflowsBasePath(), workflowPath) {
|
|
29
|
+
export async function resolveScenarioPath(workflowName, scenarioName, basePath = getWorkflowsBasePath(), workflowPath, catalog) {
|
|
30
30
|
const scenarioFileName = scenarioName.endsWith('.json') ?
|
|
31
31
|
scenarioName :
|
|
32
32
|
`${scenarioName}.json`;
|
|
@@ -36,7 +36,7 @@ export async function resolveScenarioPath(workflowName, scenarioName, basePath =
|
|
|
36
36
|
return pathResult;
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
-
const catalogPath = workflowPath ? null : await fetchWorkflowPath(workflowName);
|
|
39
|
+
const catalogPath = workflowPath ? null : await fetchWorkflowPath(workflowName, catalog);
|
|
40
40
|
if (catalogPath) {
|
|
41
41
|
const result = resolveScenarioFromScenarioDirs(candidateScenarioDirsFromPath(catalogPath, basePath), scenarioFileName);
|
|
42
42
|
if (result.found) {
|
|
@@ -156,6 +156,20 @@ describe('resolveScenarioPath', () => {
|
|
|
156
156
|
expect(result.path).toContain('complex/deep_test.json');
|
|
157
157
|
});
|
|
158
158
|
});
|
|
159
|
+
describe('catalog routing', () => {
|
|
160
|
+
it('forwards the provided catalog to the catalog lookup', async () => {
|
|
161
|
+
mockCatalog([{ name: 'my_workflow', path: '/app/dist/workflows/my_workflow/workflow.js' }]);
|
|
162
|
+
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
163
|
+
await resolveScenarioPath('my_workflow', 'test', '/project', undefined, 'os-workflows');
|
|
164
|
+
expect(catalog.fetchWorkflowCatalog).toHaveBeenCalledWith('os-workflows');
|
|
165
|
+
});
|
|
166
|
+
it('looks up the default catalog when no catalog is provided', async () => {
|
|
167
|
+
mockCatalog([{ name: 'my_workflow', path: '/app/dist/workflows/my_workflow/workflow.js' }]);
|
|
168
|
+
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
169
|
+
await resolveScenarioPath('my_workflow', 'test', '/project');
|
|
170
|
+
expect(catalog.fetchWorkflowCatalog).toHaveBeenCalledWith(undefined);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
159
173
|
});
|
|
160
174
|
describe('listScenariosForWorkflow', () => {
|
|
161
175
|
beforeEach(() => {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import Table from 'cli-table3';
|
|
2
2
|
import { ux } from '@oclif/core';
|
|
3
|
-
import { formatOutput } from '#utils/output_formatter.js';
|
|
4
3
|
import { formatDuration } from '#utils/date_formatter.js';
|
|
5
4
|
import { getErrorMessage } from '#utils/error_utils.js';
|
|
6
5
|
import { isTraceEvent, isValidTimestamp } from '#types/trace.js';
|
|
@@ -369,7 +368,7 @@ const formatAsText = (trace) => {
|
|
|
369
368
|
export function format(traceData, outputFormat = 'text') {
|
|
370
369
|
const trace = typeof traceData === 'string' ? JSON.parse(traceData) : traceData;
|
|
371
370
|
if (outputFormat === 'json') {
|
|
372
|
-
return
|
|
371
|
+
return JSON.stringify(trace, null, 2);
|
|
373
372
|
}
|
|
374
373
|
return formatAsText(trace);
|
|
375
374
|
}
|
|
@@ -2,7 +2,7 @@ export declare const WORKFLOWS_PATHS: string[];
|
|
|
2
2
|
export declare function extractWorkflowRelativePath(path: string): string | null;
|
|
3
3
|
export declare function candidateWorkflowDirsFromPath(workflowPath: string, basePath: string): string[];
|
|
4
4
|
export declare function findWorkflowDirectoryFromPath(workflowPath: string | undefined, basePath?: string): string | null;
|
|
5
|
-
export declare function fetchWorkflowPath(workflowName: string): Promise<string | null>;
|
|
5
|
+
export declare function fetchWorkflowPath(workflowName: string, catalog?: string): Promise<string | null>;
|
|
6
6
|
/**
|
|
7
7
|
* Resolve the on-disk directory of a registered workflow by name.
|
|
8
8
|
*
|
|
@@ -23,9 +23,9 @@ export function findWorkflowDirectoryFromPath(workflowPath, basePath = getWorkfl
|
|
|
23
23
|
}
|
|
24
24
|
return candidateWorkflowDirsFromPath(workflowPath, basePath).find(existsSync) ?? null;
|
|
25
25
|
}
|
|
26
|
-
export async function fetchWorkflowPath(workflowName) {
|
|
26
|
+
export async function fetchWorkflowPath(workflowName, catalog) {
|
|
27
27
|
try {
|
|
28
|
-
const workflows = await fetchWorkflowCatalog();
|
|
28
|
+
const workflows = await fetchWorkflowCatalog(catalog);
|
|
29
29
|
const workflow = workflows.find(w => w.name === workflowName);
|
|
30
30
|
return workflow?.path ?? null;
|
|
31
31
|
}
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
type EntryKind = 'scenario' | 'custom';
|
|
3
|
+
interface Entry {
|
|
4
|
+
kind: EntryKind;
|
|
5
|
+
label: string;
|
|
6
|
+
scenarioName?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const buildEntries: (scenarios: string[]) => Entry[];
|
|
9
|
+
export declare const validateScenarioName: (raw: string, existing: string[]) => string | null;
|
|
2
10
|
export declare const RunModal: React.FC<{
|
|
3
11
|
workflowName: string;
|
|
4
12
|
workflowPath?: string;
|
|
5
13
|
}>;
|
|
14
|
+
export {};
|