@outputai/cli 0.8.2-next.c12766f.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/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/generate.spec.js +2 -2
- package/dist/commands/workflow/run.js +1 -1
- package/dist/commands/workflow/run.spec.js +17 -0
- package/dist/commands/workflow/start.js +1 -1
- package/dist/commands/workflow/start.spec.js +53 -2
- package/dist/commands/workflow/test_eval.d.ts +1 -0
- package/dist/commands/workflow/test_eval.js +18 -8
- package/dist/commands/workflow/test_eval.spec.js +37 -0
- package/dist/generated/framework_version.json +1 -1
- package/dist/services/coding_agents.spec.js +10 -10
- 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/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 +33 -1
- package/package.json +5 -5
|
@@ -452,6 +452,26 @@ export type GetWorkflowIdRunsRidHistory200 = {
|
|
|
452
452
|
/** @nullable */
|
|
453
453
|
nextPageToken?: string | null;
|
|
454
454
|
};
|
|
455
|
+
export type GetWorkflowIdHistoryStreamParams = {
|
|
456
|
+
/**
|
|
457
|
+
* Include decoded input/output payloads in events
|
|
458
|
+
*/
|
|
459
|
+
includePayloads?: boolean;
|
|
460
|
+
/**
|
|
461
|
+
* Resume from this event ID (alternative to Last-Event-ID header)
|
|
462
|
+
*/
|
|
463
|
+
lastEventId?: number;
|
|
464
|
+
};
|
|
465
|
+
export type GetWorkflowIdRunsRidHistoryStreamParams = {
|
|
466
|
+
/**
|
|
467
|
+
* Include decoded input/output payloads in events
|
|
468
|
+
*/
|
|
469
|
+
includePayloads?: boolean;
|
|
470
|
+
/**
|
|
471
|
+
* Resume from this event ID (alternative to Last-Event-ID header)
|
|
472
|
+
*/
|
|
473
|
+
lastEventId?: number;
|
|
474
|
+
};
|
|
455
475
|
export type GetWorkflowCatalogId200 = {
|
|
456
476
|
/** Each workflow available in this catalog */
|
|
457
477
|
workflows?: Workflow[];
|
|
@@ -1043,6 +1063,65 @@ export type getWorkflowIdRunsRidHistoryResponseError = (getWorkflowIdRunsRidHist
|
|
|
1043
1063
|
export type getWorkflowIdRunsRidHistoryResponse = (getWorkflowIdRunsRidHistoryResponseSuccess | getWorkflowIdRunsRidHistoryResponseError);
|
|
1044
1064
|
export declare const getGetWorkflowIdRunsRidHistoryUrl: (id: string, rid: string, params?: GetWorkflowIdRunsRidHistoryParams) => string;
|
|
1045
1065
|
export declare const getWorkflowIdRunsRidHistory: (id: string, rid: string, params?: GetWorkflowIdRunsRidHistoryParams, options?: ApiRequestOptions) => Promise<getWorkflowIdRunsRidHistoryResponse>;
|
|
1066
|
+
/**
|
|
1067
|
+
* Opens a persistent SSE connection that delivers Temporal workflow history events in real time. Emits named events: `workflow` (metadata, once), `history` (event batches), `done` (terminal state), `server_error` (post-flush errors). The `done` event carries `{ reason, newRunId? }` where `reason` is the terminal Temporal event type (`WORKFLOW_EXECUTION_COMPLETED`, `WORKFLOW_EXECUTION_FAILED`, `WORKFLOW_EXECUTION_TIMED_OUT`, `WORKFLOW_EXECUTION_CANCELED`, `WORKFLOW_EXECUTION_TERMINATED`, `WORKFLOW_EXECUTION_CONTINUED_AS_NEW`) and `newRunId` is present only when the terminal event chains a follow-on run. `server_error` carries `{ error, message, workflowId, runId }`. Errors before the stream opens are returned as JSON HTTP responses (400/404); once open, failures arrive as a `server_error` event. Supports reconnect via `Last-Event-ID` header or `lastEventId` query param.
|
|
1068
|
+
|
|
1069
|
+
* @summary Stream workflow history events via Server-Sent Events
|
|
1070
|
+
*/
|
|
1071
|
+
export type getWorkflowIdHistoryStreamResponse200 = {
|
|
1072
|
+
data: string;
|
|
1073
|
+
status: 200;
|
|
1074
|
+
};
|
|
1075
|
+
export type getWorkflowIdHistoryStreamResponse400 = {
|
|
1076
|
+
data: BadRequestResponse;
|
|
1077
|
+
status: 400;
|
|
1078
|
+
};
|
|
1079
|
+
export type getWorkflowIdHistoryStreamResponse404 = {
|
|
1080
|
+
data: NotFoundResponse;
|
|
1081
|
+
status: 404;
|
|
1082
|
+
};
|
|
1083
|
+
export type getWorkflowIdHistoryStreamResponse500 = {
|
|
1084
|
+
data: InternalServerErrorResponse;
|
|
1085
|
+
status: 500;
|
|
1086
|
+
};
|
|
1087
|
+
export type getWorkflowIdHistoryStreamResponseSuccess = (getWorkflowIdHistoryStreamResponse200) & {
|
|
1088
|
+
headers: Headers;
|
|
1089
|
+
};
|
|
1090
|
+
export type getWorkflowIdHistoryStreamResponseError = (getWorkflowIdHistoryStreamResponse400 | getWorkflowIdHistoryStreamResponse404 | getWorkflowIdHistoryStreamResponse500) & {
|
|
1091
|
+
headers: Headers;
|
|
1092
|
+
};
|
|
1093
|
+
export type getWorkflowIdHistoryStreamResponse = (getWorkflowIdHistoryStreamResponseSuccess | getWorkflowIdHistoryStreamResponseError);
|
|
1094
|
+
export declare const getGetWorkflowIdHistoryStreamUrl: (id: string, params?: GetWorkflowIdHistoryStreamParams) => string;
|
|
1095
|
+
export declare const getWorkflowIdHistoryStream: (id: string, params?: GetWorkflowIdHistoryStreamParams, options?: ApiRequestOptions) => Promise<getWorkflowIdHistoryStreamResponse>;
|
|
1096
|
+
/**
|
|
1097
|
+
* Same as /workflow/{id}/history/stream but targets a specific run ID.
|
|
1098
|
+
* @summary Stream pinned-run workflow history events via Server-Sent Events
|
|
1099
|
+
*/
|
|
1100
|
+
export type getWorkflowIdRunsRidHistoryStreamResponse200 = {
|
|
1101
|
+
data: string;
|
|
1102
|
+
status: 200;
|
|
1103
|
+
};
|
|
1104
|
+
export type getWorkflowIdRunsRidHistoryStreamResponse400 = {
|
|
1105
|
+
data: BadRequestResponse;
|
|
1106
|
+
status: 400;
|
|
1107
|
+
};
|
|
1108
|
+
export type getWorkflowIdRunsRidHistoryStreamResponse404 = {
|
|
1109
|
+
data: NotFoundResponse;
|
|
1110
|
+
status: 404;
|
|
1111
|
+
};
|
|
1112
|
+
export type getWorkflowIdRunsRidHistoryStreamResponse500 = {
|
|
1113
|
+
data: InternalServerErrorResponse;
|
|
1114
|
+
status: 500;
|
|
1115
|
+
};
|
|
1116
|
+
export type getWorkflowIdRunsRidHistoryStreamResponseSuccess = (getWorkflowIdRunsRidHistoryStreamResponse200) & {
|
|
1117
|
+
headers: Headers;
|
|
1118
|
+
};
|
|
1119
|
+
export type getWorkflowIdRunsRidHistoryStreamResponseError = (getWorkflowIdRunsRidHistoryStreamResponse400 | getWorkflowIdRunsRidHistoryStreamResponse404 | getWorkflowIdRunsRidHistoryStreamResponse500) & {
|
|
1120
|
+
headers: Headers;
|
|
1121
|
+
};
|
|
1122
|
+
export type getWorkflowIdRunsRidHistoryStreamResponse = (getWorkflowIdRunsRidHistoryStreamResponseSuccess | getWorkflowIdRunsRidHistoryStreamResponseError);
|
|
1123
|
+
export declare const getGetWorkflowIdRunsRidHistoryStreamUrl: (id: string, rid: string, params?: GetWorkflowIdRunsRidHistoryStreamParams) => string;
|
|
1124
|
+
export declare const getWorkflowIdRunsRidHistoryStream: (id: string, rid: string, params?: GetWorkflowIdRunsRidHistoryStreamParams, options?: ApiRequestOptions) => Promise<getWorkflowIdRunsRidHistoryStreamResponse>;
|
|
1046
1125
|
/**
|
|
1047
1126
|
* @summary Get a specific workflow catalog by ID
|
|
1048
1127
|
*/
|
|
@@ -231,6 +231,38 @@ export const getWorkflowIdRunsRidHistory = async (id, rid, params, options) => {
|
|
|
231
231
|
method: 'GET'
|
|
232
232
|
});
|
|
233
233
|
};
|
|
234
|
+
export const getGetWorkflowIdHistoryStreamUrl = (id, params) => {
|
|
235
|
+
const normalizedParams = new URLSearchParams();
|
|
236
|
+
Object.entries(params || {}).forEach(([key, value]) => {
|
|
237
|
+
if (value !== undefined) {
|
|
238
|
+
normalizedParams.append(key, value === null ? 'null' : value.toString());
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
const stringifiedParams = normalizedParams.toString();
|
|
242
|
+
return stringifiedParams.length > 0 ? `/workflow/${id}/history/stream?${stringifiedParams}` : `/workflow/${id}/history/stream`;
|
|
243
|
+
};
|
|
244
|
+
export const getWorkflowIdHistoryStream = async (id, params, options) => {
|
|
245
|
+
return customFetchInstance(getGetWorkflowIdHistoryStreamUrl(id, params), {
|
|
246
|
+
...options,
|
|
247
|
+
method: 'GET'
|
|
248
|
+
});
|
|
249
|
+
};
|
|
250
|
+
export const getGetWorkflowIdRunsRidHistoryStreamUrl = (id, rid, params) => {
|
|
251
|
+
const normalizedParams = new URLSearchParams();
|
|
252
|
+
Object.entries(params || {}).forEach(([key, value]) => {
|
|
253
|
+
if (value !== undefined) {
|
|
254
|
+
normalizedParams.append(key, value === null ? 'null' : value.toString());
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
const stringifiedParams = normalizedParams.toString();
|
|
258
|
+
return stringifiedParams.length > 0 ? `/workflow/${id}/runs/${rid}/history/stream?${stringifiedParams}` : `/workflow/${id}/runs/${rid}/history/stream`;
|
|
259
|
+
};
|
|
260
|
+
export const getWorkflowIdRunsRidHistoryStream = async (id, rid, params, options) => {
|
|
261
|
+
return customFetchInstance(getGetWorkflowIdRunsRidHistoryStreamUrl(id, rid, params), {
|
|
262
|
+
...options,
|
|
263
|
+
method: 'GET'
|
|
264
|
+
});
|
|
265
|
+
};
|
|
234
266
|
export const getGetWorkflowCatalogIdUrl = (id) => {
|
|
235
267
|
return `/workflow/catalog/${id}`;
|
|
236
268
|
};
|
|
@@ -7,6 +7,7 @@ export default class DatasetGenerate extends Command {
|
|
|
7
7
|
scenario: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
8
8
|
};
|
|
9
9
|
static flags: {
|
|
10
|
+
catalog: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
11
|
trace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
12
|
name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
13
|
download: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
@@ -26,6 +26,14 @@ export default class DatasetGenerate extends Command {
|
|
|
26
26
|
})
|
|
27
27
|
};
|
|
28
28
|
static flags = {
|
|
29
|
+
catalog: Flags.string({
|
|
30
|
+
char: 'c',
|
|
31
|
+
aliases: ['task-queue'],
|
|
32
|
+
charAliases: ['q'],
|
|
33
|
+
deprecateAliases: true,
|
|
34
|
+
description: 'Catalog name for workflow execution (defaults to OUTPUT_CATALOG_ID)',
|
|
35
|
+
env: 'OUTPUT_CATALOG_ID'
|
|
36
|
+
}),
|
|
29
37
|
trace: Flags.string({
|
|
30
38
|
char: 't',
|
|
31
39
|
description: 'Path to a local trace file to extract dataset from',
|
|
@@ -61,15 +69,16 @@ export default class DatasetGenerate extends Command {
|
|
|
61
69
|
await this.generateFromTrace(args.workflowName, flags.trace, flags.name);
|
|
62
70
|
return;
|
|
63
71
|
}
|
|
64
|
-
await this.generateFromScenario(args.workflowName, args.scenario, flags.input, flags.name);
|
|
72
|
+
await this.generateFromScenario(args.workflowName, args.scenario, flags.input, flags.name, flags.catalog);
|
|
65
73
|
}
|
|
66
|
-
async generateFromScenario(workflowName, scenario, inputFlag, nameOverride) {
|
|
67
|
-
const resolvedInput = await this.resolveScenarioInput(workflowName, scenario, inputFlag);
|
|
74
|
+
async generateFromScenario(workflowName, scenario, inputFlag, nameOverride, catalog) {
|
|
75
|
+
const resolvedInput = await this.resolveScenarioInput(workflowName, scenario, inputFlag, catalog);
|
|
68
76
|
const datasetName = nameOverride ?? scenario ?? 'dataset';
|
|
69
77
|
this.log(`Running workflow "${workflowName}"...`);
|
|
70
78
|
const response = await postWorkflowRun({
|
|
71
79
|
workflowName,
|
|
72
|
-
input: resolvedInput
|
|
80
|
+
input: resolvedInput,
|
|
81
|
+
catalog
|
|
73
82
|
}, {
|
|
74
83
|
config: { timeout: 600000 }
|
|
75
84
|
});
|
|
@@ -116,7 +125,7 @@ export default class DatasetGenerate extends Command {
|
|
|
116
125
|
}
|
|
117
126
|
this.log(`\nGenerated ${traces.length} dataset(s)`);
|
|
118
127
|
}
|
|
119
|
-
async resolveScenarioInput(workflowName, scenario, inputFlag) {
|
|
128
|
+
async resolveScenarioInput(workflowName, scenario, inputFlag, catalog) {
|
|
120
129
|
if (inputFlag && scenario) {
|
|
121
130
|
return ux.error('Cannot use both scenario argument and --input flag. Choose one.', { exit: 1 });
|
|
122
131
|
}
|
|
@@ -124,7 +133,7 @@ export default class DatasetGenerate extends Command {
|
|
|
124
133
|
return parseInputFlag(inputFlag);
|
|
125
134
|
}
|
|
126
135
|
if (scenario) {
|
|
127
|
-
const resolution = await resolveScenarioPath(workflowName, scenario);
|
|
136
|
+
const resolution = await resolveScenarioPath(workflowName, scenario, undefined, undefined, catalog);
|
|
128
137
|
if (!resolution.found) {
|
|
129
138
|
return ux.error(getScenarioNotFoundMessage(workflowName, scenario, resolution.searchedPaths), { exit: 1 });
|
|
130
139
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
3
|
+
vi.mock('#api/generated/api.js', () => ({
|
|
4
|
+
postWorkflowRun: vi.fn()
|
|
5
|
+
}));
|
|
6
|
+
vi.mock('#utils/scenario_resolver.js', () => ({
|
|
7
|
+
resolveScenarioPath: vi.fn(),
|
|
8
|
+
getScenarioNotFoundMessage: vi.fn().mockReturnValue('not found')
|
|
9
|
+
}));
|
|
10
|
+
vi.mock('#utils/input_parser.js', () => ({
|
|
11
|
+
parseInputFlag: vi.fn()
|
|
12
|
+
}));
|
|
13
|
+
vi.mock('#services/datasets.js', () => ({
|
|
14
|
+
writeDataset: vi.fn(),
|
|
15
|
+
resolveDefaultDatasetsDir: vi.fn().mockResolvedValue('/datasets'),
|
|
16
|
+
buildDataset: vi.fn().mockReturnValue({ name: 'basic' }),
|
|
17
|
+
getExecutionTime: vi.fn().mockResolvedValue(100),
|
|
18
|
+
extractDatasetName: vi.fn()
|
|
19
|
+
}));
|
|
20
|
+
describe('workflow dataset generate command', () => {
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
vi.clearAllMocks();
|
|
23
|
+
delete process.env.OUTPUT_CATALOG_ID;
|
|
24
|
+
});
|
|
25
|
+
describe('command definition', () => {
|
|
26
|
+
it('binds the catalog flag to OUTPUT_CATALOG_ID', async () => {
|
|
27
|
+
const DatasetGenerate = (await import('./generate.js')).default;
|
|
28
|
+
expect(DatasetGenerate.flags).toHaveProperty('catalog');
|
|
29
|
+
expect(DatasetGenerate.flags.catalog.env).toBe('OUTPUT_CATALOG_ID');
|
|
30
|
+
expect(DatasetGenerate.flags.catalog.char).toBe('c');
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
describe('run()', () => {
|
|
34
|
+
const createCommand = async (flagOverrides = {}) => {
|
|
35
|
+
const DatasetGenerate = (await import('./generate.js')).default;
|
|
36
|
+
const { postWorkflowRun } = await import('#api/generated/api.js');
|
|
37
|
+
const { resolveScenarioPath } = await import('#utils/scenario_resolver.js');
|
|
38
|
+
const { parseInputFlag } = await import('#utils/input_parser.js');
|
|
39
|
+
const cmd = new DatasetGenerate(['my_workflow'], {});
|
|
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
|
+
});
|
|
48
|
+
return {
|
|
49
|
+
cmd,
|
|
50
|
+
postWorkflowRun: vi.mocked(postWorkflowRun),
|
|
51
|
+
resolveScenarioPath: vi.mocked(resolveScenarioPath),
|
|
52
|
+
parseInputFlag: vi.mocked(parseInputFlag)
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
it('resolves the scenario and runs the workflow against the resolved catalog', async () => {
|
|
56
|
+
const { cmd, postWorkflowRun, resolveScenarioPath, parseInputFlag } = await createCommand({ catalog: 'my-catalog' });
|
|
57
|
+
resolveScenarioPath.mockResolvedValue({ found: true, path: '/scenarios/basic.json', searchedPaths: [] });
|
|
58
|
+
parseInputFlag.mockResolvedValue({ foo: 'bar' });
|
|
59
|
+
postWorkflowRun.mockResolvedValue({
|
|
60
|
+
data: { workflowId: 'wf-1', output: { ok: true } },
|
|
61
|
+
status: 200,
|
|
62
|
+
headers: new Headers()
|
|
63
|
+
});
|
|
64
|
+
await cmd.run();
|
|
65
|
+
expect(resolveScenarioPath).toHaveBeenCalledWith('my_workflow', 'basic', undefined, undefined, 'my-catalog');
|
|
66
|
+
expect(postWorkflowRun).toHaveBeenCalledWith(expect.objectContaining({ workflowName: 'my_workflow', catalog: 'my-catalog' }), expect.anything());
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -4,8 +4,8 @@ import Generate from './generate.js';
|
|
|
4
4
|
import { generateWorkflow } from '#services/workflow_generator.js';
|
|
5
5
|
import { parseWorkflowDir } from '#utils/workflow_dir_parser.js';
|
|
6
6
|
import { InvalidNameError, WorkflowExistsError } from '#types/errors.js';
|
|
7
|
-
vi.mock('
|
|
8
|
-
vi.mock('
|
|
7
|
+
vi.mock('#services/workflow_generator.js');
|
|
8
|
+
vi.mock('#utils/workflow_dir_parser.js');
|
|
9
9
|
describe('Generate Command', () => {
|
|
10
10
|
let mockGenerateWorkflow;
|
|
11
11
|
let mockParseWorkflowDir;
|
|
@@ -63,7 +63,7 @@ export default class WorkflowRun extends Command {
|
|
|
63
63
|
};
|
|
64
64
|
async run() {
|
|
65
65
|
const { args, flags } = await this.parse(WorkflowRun);
|
|
66
|
-
const input = await resolveInput(args.workflowName, args.scenario, flags.input, 'run');
|
|
66
|
+
const input = await resolveInput(args.workflowName, args.scenario, flags.input, 'run', flags.catalog);
|
|
67
67
|
this.log(`Executing workflow: ${args.workflowName}...`);
|
|
68
68
|
const response = await executeWorkflow({
|
|
69
69
|
body: {
|
|
@@ -64,11 +64,28 @@ describe('workflow run command', () => {
|
|
|
64
64
|
headers: new Headers()
|
|
65
65
|
});
|
|
66
66
|
await cmd.run();
|
|
67
|
+
expect(resolveInput).toHaveBeenCalledWith('my_workflow', undefined, undefined, 'run', undefined);
|
|
67
68
|
expect(postWorkflowRun).toHaveBeenCalledTimes(1);
|
|
68
69
|
expect(postWorkflowRun).toHaveBeenCalledWith({ workflowName: 'my_workflow', input: { key: 'value' }, catalog: undefined }, expect.objectContaining({ config: { timeout: 600000 } }));
|
|
69
70
|
expect(cmd.log).toHaveBeenCalledWith('Executing workflow: my_workflow...');
|
|
70
71
|
expect(cmd.log).toHaveBeenCalledWith(expect.stringMatching(/\n/));
|
|
71
72
|
});
|
|
73
|
+
it('threads the resolved catalog to resolveInput and postWorkflowRun', async () => {
|
|
74
|
+
const { cmd, postWorkflowRun, resolveInput } = await createCommand();
|
|
75
|
+
cmd.parse = vi.fn().mockResolvedValue({
|
|
76
|
+
args: { workflowName: 'my_workflow', scenario: 'basic' },
|
|
77
|
+
flags: { input: undefined, catalog: 'my-catalog', format: 'text' }
|
|
78
|
+
});
|
|
79
|
+
resolveInput.mockResolvedValue({ key: 'value' });
|
|
80
|
+
postWorkflowRun.mockResolvedValue({
|
|
81
|
+
data: { status: 'completed', result: {} },
|
|
82
|
+
status: 200,
|
|
83
|
+
headers: new Headers()
|
|
84
|
+
});
|
|
85
|
+
await cmd.run();
|
|
86
|
+
expect(resolveInput).toHaveBeenCalledWith('my_workflow', 'basic', undefined, 'run', 'my-catalog');
|
|
87
|
+
expect(postWorkflowRun).toHaveBeenCalledWith(expect.objectContaining({ catalog: 'my-catalog' }), expect.anything());
|
|
88
|
+
});
|
|
72
89
|
it('retries when response has Retry-After and succeeds on second attempt', async () => {
|
|
73
90
|
const { cmd, postWorkflowRun, resolveInput } = await createCommand();
|
|
74
91
|
resolveInput.mockResolvedValue({});
|
|
@@ -37,7 +37,7 @@ export default class WorkflowStart extends Command {
|
|
|
37
37
|
};
|
|
38
38
|
async run() {
|
|
39
39
|
const { args, flags } = await this.parse(WorkflowStart);
|
|
40
|
-
const input = await resolveInput(args.workflowName, args.scenario, flags.input, 'start');
|
|
40
|
+
const input = await resolveInput(args.workflowName, args.scenario, flags.input, 'start', flags.catalog);
|
|
41
41
|
this.log(`Starting workflow: ${args.workflowName}...`);
|
|
42
42
|
const response = await postWorkflowStart({
|
|
43
43
|
workflowName: args.workflowName,
|
|
@@ -1,11 +1,17 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
1
2
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
vi.mock('
|
|
3
|
+
vi.mock('#api/generated/api.js', () => ({
|
|
3
4
|
postWorkflowStart: vi.fn()
|
|
4
5
|
}));
|
|
6
|
+
vi.mock('#utils/resolve_input.js', () => ({
|
|
7
|
+
resolveInput: vi.fn()
|
|
8
|
+
}));
|
|
5
9
|
describe('workflow start command', () => {
|
|
6
|
-
beforeEach(() => {
|
|
10
|
+
beforeEach(async () => {
|
|
7
11
|
vi.clearAllMocks();
|
|
8
12
|
delete process.env.OUTPUT_CATALOG_ID;
|
|
13
|
+
const { resolveInput } = await import('#utils/resolve_input.js');
|
|
14
|
+
vi.mocked(resolveInput).mockResolvedValue({});
|
|
9
15
|
});
|
|
10
16
|
describe('command definition', () => {
|
|
11
17
|
it('should export a valid OCLIF command', async () => {
|
|
@@ -25,5 +31,50 @@ describe('workflow start command', () => {
|
|
|
25
31
|
expect(WorkflowStart.args).toHaveProperty('scenario');
|
|
26
32
|
expect(WorkflowStart.args.scenario.required).toBe(false);
|
|
27
33
|
});
|
|
34
|
+
it('binds the catalog flag to OUTPUT_CATALOG_ID', async () => {
|
|
35
|
+
const WorkflowStart = (await import('./start.js')).default;
|
|
36
|
+
expect(WorkflowStart.flags.catalog.env).toBe('OUTPUT_CATALOG_ID');
|
|
37
|
+
expect(WorkflowStart.flags.catalog.char).toBe('c');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
describe('run()', () => {
|
|
41
|
+
const createCommand = async (flagOverrides = {}) => {
|
|
42
|
+
const WorkflowStart = (await import('./start.js')).default;
|
|
43
|
+
const { postWorkflowStart } = await import('#api/generated/api.js');
|
|
44
|
+
const { resolveInput } = await import('#utils/resolve_input.js');
|
|
45
|
+
const cmd = new WorkflowStart(['my_workflow'], {});
|
|
46
|
+
cmd.log = vi.fn();
|
|
47
|
+
cmd.error = vi.fn(() => {
|
|
48
|
+
throw new Error('error called');
|
|
49
|
+
});
|
|
50
|
+
cmd.parse = vi.fn().mockResolvedValue({
|
|
51
|
+
args: { workflowName: 'my_workflow', scenario: undefined },
|
|
52
|
+
flags: { input: undefined, catalog: undefined, ...flagOverrides }
|
|
53
|
+
});
|
|
54
|
+
return { cmd, postWorkflowStart: vi.mocked(postWorkflowStart), resolveInput: vi.mocked(resolveInput) };
|
|
55
|
+
};
|
|
56
|
+
it('threads the resolved catalog to resolveInput and postWorkflowStart', async () => {
|
|
57
|
+
const { cmd, postWorkflowStart, resolveInput } = await createCommand({ catalog: 'my-catalog' });
|
|
58
|
+
resolveInput.mockResolvedValue({ key: 'value' });
|
|
59
|
+
postWorkflowStart.mockResolvedValue({
|
|
60
|
+
data: { workflowId: 'wf-123' },
|
|
61
|
+
status: 200,
|
|
62
|
+
headers: new Headers()
|
|
63
|
+
});
|
|
64
|
+
await cmd.run();
|
|
65
|
+
expect(resolveInput).toHaveBeenCalledWith('my_workflow', undefined, undefined, 'start', 'my-catalog');
|
|
66
|
+
expect(postWorkflowStart).toHaveBeenCalledWith(expect.objectContaining({ workflowName: 'my_workflow', catalog: 'my-catalog' }));
|
|
67
|
+
});
|
|
68
|
+
it('passes undefined catalog through when none is set', async () => {
|
|
69
|
+
const { cmd, postWorkflowStart, resolveInput } = await createCommand();
|
|
70
|
+
resolveInput.mockResolvedValue({});
|
|
71
|
+
postWorkflowStart.mockResolvedValue({
|
|
72
|
+
data: { workflowId: 'wf-123' },
|
|
73
|
+
status: 200,
|
|
74
|
+
headers: new Headers()
|
|
75
|
+
});
|
|
76
|
+
await cmd.run();
|
|
77
|
+
expect(resolveInput).toHaveBeenCalledWith('my_workflow', undefined, undefined, 'start', undefined);
|
|
78
|
+
});
|
|
28
79
|
});
|
|
29
80
|
});
|
|
@@ -9,6 +9,7 @@ export default class WorkflowTest extends Command {
|
|
|
9
9
|
workflowName: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
10
10
|
};
|
|
11
11
|
static flags: {
|
|
12
|
+
catalog: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
13
|
cached: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
14
|
save: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
15
|
dataset: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -24,6 +24,14 @@ export default class WorkflowTest extends Command {
|
|
|
24
24
|
})
|
|
25
25
|
};
|
|
26
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
|
+
}),
|
|
27
35
|
cached: Flags.boolean({
|
|
28
36
|
description: 'Use cached output from dataset files (skip workflow execution)',
|
|
29
37
|
default: false,
|
|
@@ -43,7 +51,7 @@ export default class WorkflowTest extends Command {
|
|
|
43
51
|
const { args, flags } = await this.parse(WorkflowTest);
|
|
44
52
|
const filterNames = flags.dataset?.split(',').map(s => s.trim());
|
|
45
53
|
const evalName = getEvalWorkflowName(args.workflowName);
|
|
46
|
-
await this.ensureEvalWorkflowRegistered(args.workflowName, evalName);
|
|
54
|
+
await this.ensureEvalWorkflowRegistered(args.workflowName, evalName, flags.catalog);
|
|
47
55
|
const { datasets, dir } = await readAllDatasets(args.workflowName, filterNames);
|
|
48
56
|
if (datasets.length === 0) {
|
|
49
57
|
this.error(`No datasets found for workflow "${args.workflowName}".\n` +
|
|
@@ -51,11 +59,12 @@ export default class WorkflowTest extends Command {
|
|
|
51
59
|
}
|
|
52
60
|
const preparedDatasets = flags.cached ?
|
|
53
61
|
this.validateDatasets(datasets) :
|
|
54
|
-
await this.runWorkflowForDatasets(args.workflowName, datasets, flags.save, dir);
|
|
62
|
+
await this.runWorkflowForDatasets(args.workflowName, datasets, flags.save, dir, flags.catalog);
|
|
55
63
|
this.log(`Running eval workflow "${evalName}"...\n`);
|
|
56
64
|
const response = await postWorkflowRun({
|
|
57
65
|
workflowName: evalName,
|
|
58
|
-
input: { datasets: preparedDatasets }
|
|
66
|
+
input: { datasets: preparedDatasets },
|
|
67
|
+
catalog: flags.catalog
|
|
59
68
|
}, {
|
|
60
69
|
config: { timeout: 600000 }
|
|
61
70
|
});
|
|
@@ -73,9 +82,9 @@ export default class WorkflowTest extends Command {
|
|
|
73
82
|
process.exitCode = computeExitCode(evalOutput);
|
|
74
83
|
return evalOutput;
|
|
75
84
|
}
|
|
76
|
-
async ensureEvalWorkflowRegistered(workflowName, evalName) {
|
|
77
|
-
const
|
|
78
|
-
if (
|
|
85
|
+
async ensureEvalWorkflowRegistered(workflowName, evalName, catalog) {
|
|
86
|
+
const workflows = await fetchWorkflowCatalog(catalog).catch(() => null);
|
|
87
|
+
if (workflows && !workflows.some(w => w.name === evalName)) {
|
|
79
88
|
this.error(await diagnoseMissingEvalWorkflow(workflowName), { exit: 1 });
|
|
80
89
|
}
|
|
81
90
|
}
|
|
@@ -88,7 +97,7 @@ export default class WorkflowTest extends Command {
|
|
|
88
97
|
}
|
|
89
98
|
return datasets;
|
|
90
99
|
}
|
|
91
|
-
async runWorkflowForDatasets(workflowName, datasets, save, dir) {
|
|
100
|
+
async runWorkflowForDatasets(workflowName, datasets, save, dir, catalog) {
|
|
92
101
|
this.log(`Running workflow "${workflowName}" for ${datasets.length} dataset(s)...\n`);
|
|
93
102
|
const results = [];
|
|
94
103
|
for (const dataset of datasets) {
|
|
@@ -96,7 +105,8 @@ export default class WorkflowTest extends Command {
|
|
|
96
105
|
const startMs = Date.now();
|
|
97
106
|
const response = await postWorkflowRun({
|
|
98
107
|
workflowName,
|
|
99
|
-
input: dataset.input
|
|
108
|
+
input: dataset.input,
|
|
109
|
+
catalog
|
|
100
110
|
}, {
|
|
101
111
|
config: { timeout: 600000 }
|
|
102
112
|
});
|
|
@@ -4,10 +4,16 @@ import { getEvalWorkflowName, renderEvalOutput } from '@outputai/evals';
|
|
|
4
4
|
vi.mock('#api/generated/api.js', () => ({
|
|
5
5
|
postWorkflowRun: vi.fn()
|
|
6
6
|
}));
|
|
7
|
+
vi.mock('#api/workflow_catalog.js', () => ({
|
|
8
|
+
fetchWorkflowCatalog: vi.fn()
|
|
9
|
+
}));
|
|
7
10
|
vi.mock('#services/datasets.js', () => ({
|
|
8
11
|
readAllDatasets: vi.fn(),
|
|
9
12
|
writeDataset: vi.fn()
|
|
10
13
|
}));
|
|
14
|
+
vi.mock('#utils/eval_diagnostics.js', () => ({
|
|
15
|
+
diagnoseMissingEvalWorkflow: vi.fn().mockResolvedValue('missing eval workflow')
|
|
16
|
+
}));
|
|
11
17
|
const passingOutput = {
|
|
12
18
|
cases: [{ datasetName: 'd1', verdict: 'pass', evaluators: [] }],
|
|
13
19
|
summary: { total: 1, passed: 1, partial: 0, failed: 0, acceptableRate: 1 }
|
|
@@ -23,10 +29,16 @@ describe('workflow test command', () => {
|
|
|
23
29
|
exitState.original = process.exitCode;
|
|
24
30
|
process.exitCode = undefined;
|
|
25
31
|
const { readAllDatasets } = await import('#services/datasets.js');
|
|
32
|
+
const { fetchWorkflowCatalog } = await import('#api/workflow_catalog.js');
|
|
26
33
|
vi.mocked(readAllDatasets).mockResolvedValue({
|
|
27
34
|
datasets: [{ name: 'd1', input: {}, last_output: { output: {}, date: '2026-01-01' } }],
|
|
28
35
|
dir: '/tmp/datasets'
|
|
29
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
|
+
]);
|
|
30
42
|
});
|
|
31
43
|
afterEach(() => {
|
|
32
44
|
process.exitCode = exitState.original;
|
|
@@ -36,6 +48,12 @@ describe('workflow test command', () => {
|
|
|
36
48
|
const WorkflowTest = (await import('./test_eval.js')).default;
|
|
37
49
|
expect(WorkflowTest.enableJsonFlag).toBe(true);
|
|
38
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
|
+
});
|
|
39
57
|
});
|
|
40
58
|
describe('run()', () => {
|
|
41
59
|
const createCommand = async (jsonEnabled) => {
|
|
@@ -80,5 +98,24 @@ describe('workflow test command', () => {
|
|
|
80
98
|
expect(result).toEqual(failingOutput);
|
|
81
99
|
expect(process.exitCode).toBe(1);
|
|
82
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
|
+
});
|
|
83
120
|
});
|
|
84
121
|
});
|
|
@@ -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 +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(() => {
|
|
@@ -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 {};
|
|
@@ -9,26 +9,40 @@ import { startWorkflow } from '#views/dev/services/run_workflow.js';
|
|
|
9
9
|
import { readScenario, writeScenario } from '#views/dev/services/scenario_io.js';
|
|
10
10
|
import { JsonEditor } from '#views/dev/utils/json_editor.js';
|
|
11
11
|
import { ModalFrame } from '#views/dev/modals/modal_frame.js';
|
|
12
|
-
const CUSTOM_SEED = { '': '' };
|
|
13
12
|
const SCENARIO_NAME_RE = /^[a-zA-Z0-9_-]+$/;
|
|
14
|
-
|
|
13
|
+
// Seed for a brand-new input — an empty "": "" pair reads friendlier than a bare {}.
|
|
14
|
+
const CUSTOM_SEED = { '': '' };
|
|
15
|
+
export const buildEntries = (scenarios) => {
|
|
15
16
|
const list = scenarios.map(s => ({
|
|
16
17
|
kind: 'scenario',
|
|
17
18
|
label: s,
|
|
18
19
|
scenarioName: s
|
|
19
20
|
}));
|
|
20
|
-
list.push({ kind: 'custom', label: '[
|
|
21
|
+
list.push({ kind: 'custom', label: '[Enter input]' });
|
|
21
22
|
return list;
|
|
22
23
|
};
|
|
24
|
+
export const validateScenarioName = (raw, existing) => {
|
|
25
|
+
const name = raw.trim();
|
|
26
|
+
if (!name) {
|
|
27
|
+
return 'Scenario name cannot be empty.';
|
|
28
|
+
}
|
|
29
|
+
if (!SCENARIO_NAME_RE.test(name)) {
|
|
30
|
+
return 'Use letters, numbers, dashes, and underscores only.';
|
|
31
|
+
}
|
|
32
|
+
if (existing.includes(name)) {
|
|
33
|
+
return `A scenario named '${name}' already exists.`;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
};
|
|
23
37
|
const SELECT_SHORTCUTS = [
|
|
24
38
|
['↑/↓', 'navigate'],
|
|
25
39
|
['enter', 'run'],
|
|
26
40
|
['d', 'duplicate'],
|
|
27
41
|
['esc', 'cancel']
|
|
28
42
|
];
|
|
29
|
-
const
|
|
30
|
-
['enter', '
|
|
31
|
-
['esc', 'back']
|
|
43
|
+
const SAVE_SHORTCUTS = [
|
|
44
|
+
['enter', 'save & run'],
|
|
45
|
+
['esc', 'back to editor']
|
|
32
46
|
];
|
|
33
47
|
const ERROR_SHORTCUTS = [
|
|
34
48
|
{ key: 'enter', label: 'return' },
|
|
@@ -41,9 +55,11 @@ export const RunModal = ({ workflowName, workflowPath }) => {
|
|
|
41
55
|
const entries = useMemo(() => buildEntries(scenarios), [scenarios]);
|
|
42
56
|
const [mode, setMode] = useState('select');
|
|
43
57
|
const [index, setIndex] = useState(0);
|
|
44
|
-
const [
|
|
45
|
-
const [editSeed, setEditSeed] = useState(CUSTOM_SEED);
|
|
58
|
+
const [editSeed, setEditSeed] = useState({});
|
|
46
59
|
const [editFrameTitle, setEditFrameTitle] = useState('');
|
|
60
|
+
const [defaultSaveName, setDefaultSaveName] = useState('');
|
|
61
|
+
const [editName, setEditName] = useState('');
|
|
62
|
+
const [pendingValue, setPendingValue] = useState(null);
|
|
47
63
|
const [nameError, setNameError] = useState(null);
|
|
48
64
|
const [errorMessage, setErrorMessage] = useState(null);
|
|
49
65
|
const closeWith = (message, tone = 'info') => {
|
|
@@ -76,63 +92,55 @@ export const RunModal = ({ workflowName, workflowPath }) => {
|
|
|
76
92
|
setMode('error');
|
|
77
93
|
}
|
|
78
94
|
};
|
|
95
|
+
// Custom + duplicate both open the editor first; saving a scenario is opt-in (ctrl+s).
|
|
96
|
+
const startCustom = () => {
|
|
97
|
+
setEditSeed(CUSTOM_SEED);
|
|
98
|
+
setDefaultSaveName('');
|
|
99
|
+
setEditFrameTitle('Enter input');
|
|
100
|
+
setMode('edit_content');
|
|
101
|
+
};
|
|
79
102
|
const startDuplicate = async (scenarioName) => {
|
|
80
103
|
try {
|
|
81
104
|
const sourceContent = await readScenario(workflowName, scenarioName, workflowPath);
|
|
82
|
-
setEditName(`${scenarioName}_copy`);
|
|
83
105
|
setEditSeed(sourceContent);
|
|
106
|
+
setDefaultSaveName(`${scenarioName}_copy`);
|
|
84
107
|
setEditFrameTitle(`Duplicate '${scenarioName}'`);
|
|
85
|
-
|
|
86
|
-
setMode('edit_name');
|
|
108
|
+
setMode('edit_content');
|
|
87
109
|
}
|
|
88
110
|
catch (err) {
|
|
89
111
|
setErrorMessage(err instanceof Error ? err.message : String(err));
|
|
90
112
|
setMode('error');
|
|
91
113
|
}
|
|
92
114
|
};
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
setEditFrameTitle('New scenario');
|
|
97
|
-
setNameError(null);
|
|
98
|
-
setMode('edit_name');
|
|
115
|
+
// ctrl+r in the editor: run the payload as-is, nothing written to disk.
|
|
116
|
+
const runEphemeral = (value) => {
|
|
117
|
+
void submit(value, defaultSaveName || 'input');
|
|
99
118
|
};
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
if (scenarios.includes(name)) {
|
|
109
|
-
return `A scenario named '${name}' already exists.`;
|
|
110
|
-
}
|
|
111
|
-
return null;
|
|
119
|
+
// ctrl+s in the editor: keep the payload and ask for a name before saving + running.
|
|
120
|
+
const beginSave = (value) => {
|
|
121
|
+
setPendingValue(value);
|
|
122
|
+
setEditSeed(value);
|
|
123
|
+
setEditName(defaultSaveName);
|
|
124
|
+
setNameError(null);
|
|
125
|
+
setMode('name_for_save');
|
|
112
126
|
};
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
setNameError(writeError);
|
|
118
|
-
setMode('edit_name');
|
|
127
|
+
const confirmSave = async () => {
|
|
128
|
+
const validationError = validateScenarioName(editName, scenarios);
|
|
129
|
+
if (validationError) {
|
|
130
|
+
setNameError(validationError);
|
|
119
131
|
return;
|
|
120
132
|
}
|
|
121
133
|
setMode('submitting');
|
|
122
134
|
try {
|
|
123
|
-
const writtenPath = await writeScenario(workflowName,
|
|
135
|
+
const writtenPath = await writeScenario(workflowName, editName.trim(), pendingValue, workflowPath);
|
|
124
136
|
ui.pushToast(`Saved scenario at ${writtenPath}`, 'info');
|
|
125
|
-
await submit(
|
|
137
|
+
await submit(pendingValue, editName.trim());
|
|
126
138
|
}
|
|
127
139
|
catch (err) {
|
|
128
140
|
setErrorMessage(err instanceof Error ? err.message : String(err));
|
|
129
141
|
setMode('error');
|
|
130
142
|
}
|
|
131
143
|
};
|
|
132
|
-
const handleEditorCancel = () => {
|
|
133
|
-
// Bring the user back to the name step so they can adjust it or bail.
|
|
134
|
-
setMode('edit_name');
|
|
135
|
-
};
|
|
136
144
|
useInput((input, key) => {
|
|
137
145
|
if (mode === 'edit_content' || mode === 'submitting') {
|
|
138
146
|
return;
|
|
@@ -168,19 +176,13 @@ export const RunModal = ({ workflowName, workflowPath }) => {
|
|
|
168
176
|
}
|
|
169
177
|
return;
|
|
170
178
|
}
|
|
171
|
-
if (mode === '
|
|
179
|
+
if (mode === 'name_for_save') {
|
|
172
180
|
if (key.escape) {
|
|
173
|
-
setMode('
|
|
181
|
+
setMode('edit_content');
|
|
174
182
|
return;
|
|
175
183
|
}
|
|
176
184
|
if (key.return) {
|
|
177
|
-
|
|
178
|
-
if (err) {
|
|
179
|
-
setNameError(err);
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
setNameError(null);
|
|
183
|
-
setMode('edit_content');
|
|
185
|
+
void confirmSave();
|
|
184
186
|
return;
|
|
185
187
|
}
|
|
186
188
|
if (key.backspace || key.delete) {
|
|
@@ -206,12 +208,10 @@ export const RunModal = ({ workflowName, workflowPath }) => {
|
|
|
206
208
|
}
|
|
207
209
|
});
|
|
208
210
|
if (mode === 'edit_content') {
|
|
209
|
-
return (_jsx(ModalFrame, { title: editFrameTitle, children: _jsx(JsonEditor, { seed: editSeed, title: `${
|
|
210
|
-
void handleEditorSubmit(value);
|
|
211
|
-
}, onCancel: handleEditorCancel }) }));
|
|
211
|
+
return (_jsx(ModalFrame, { title: editFrameTitle, children: _jsx(JsonEditor, { seed: editSeed, title: defaultSaveName ? `${defaultSaveName}.json` : 'input', isActive: true, onSubmit: runEphemeral, onSave: beginSave, onCancel: () => setMode('select') }) }));
|
|
212
212
|
}
|
|
213
|
-
if (mode === '
|
|
214
|
-
return (_jsxs(ModalFrame, { title:
|
|
213
|
+
if (mode === 'name_for_save') {
|
|
214
|
+
return (_jsxs(ModalFrame, { title: "Save & run", shortcuts: SAVE_SHORTCUTS, children: [_jsx(TextPrompt, { label: "Scenario name:", value: editName }), nameError ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "red", children: nameError }) })) : null] }));
|
|
215
215
|
}
|
|
216
216
|
if (mode === 'submitting') {
|
|
217
217
|
return (_jsxs(ModalFrame, { title: `Run ${workflowName}`, children: [_jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: "\u00A0Starting workflow\u2026" })] }));
|
|
@@ -219,5 +219,5 @@ export const RunModal = ({ workflowName, workflowPath }) => {
|
|
|
219
219
|
if (mode === 'error') {
|
|
220
220
|
return (_jsx(ModalFrame, { title: `Run workflow "${workflowName}"`, shortcuts: ERROR_SHORTCUTS, children: _jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", errorMessage ?? 'Something went wrong.'] }) }));
|
|
221
221
|
}
|
|
222
|
-
return (_jsx(ModalFrame, { title: `Run ${workflowName}`, shortcuts: SELECT_SHORTCUTS, children: _jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { dimColor: true, children: scenarios.length === 0 ? 'No scenarios
|
|
222
|
+
return (_jsx(ModalFrame, { title: `Run ${workflowName}`, shortcuts: SELECT_SHORTCUTS, children: _jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { dimColor: true, children: scenarios.length === 0 ? 'No saved scenarios. Enter input to run:' : 'Select a scenario:' }), _jsx(Box, { flexDirection: "column", children: entries.map((entry, i) => (_jsxs(Box, { children: [_jsx(SelectionIndicator, { selected: i === index }), _jsxs(Text, { bold: i === index, children: ["\u00A0", entry.label] })] }, `${entry.kind}-${entry.scenarioName ?? i}`))) })] }) }));
|
|
223
223
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { buildEntries, validateScenarioName } from './run_modal.js';
|
|
3
|
+
describe('buildEntries', () => {
|
|
4
|
+
it('lists scenarios then the custom-JSON entry', () => {
|
|
5
|
+
const entries = buildEntries(['basic', 'edge']);
|
|
6
|
+
expect(entries.map(e => e.label)).toEqual(['basic', 'edge', '[Enter input]']);
|
|
7
|
+
expect(entries[0]).toMatchObject({ kind: 'scenario', scenarioName: 'basic' });
|
|
8
|
+
expect(entries.at(-1)).toMatchObject({ kind: 'custom' });
|
|
9
|
+
});
|
|
10
|
+
it('offers the custom entry even with no saved scenarios', () => {
|
|
11
|
+
const entries = buildEntries([]);
|
|
12
|
+
expect(entries).toHaveLength(1);
|
|
13
|
+
expect(entries[0].kind).toBe('custom');
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
describe('validateScenarioName', () => {
|
|
17
|
+
it('rejects empty or whitespace-only names', () => {
|
|
18
|
+
expect(validateScenarioName('', [])).toMatch(/cannot be empty/);
|
|
19
|
+
expect(validateScenarioName(' ', [])).toMatch(/cannot be empty/);
|
|
20
|
+
});
|
|
21
|
+
it('rejects names with unsupported characters', () => {
|
|
22
|
+
expect(validateScenarioName('has space', [])).toMatch(/letters, numbers/);
|
|
23
|
+
expect(validateScenarioName('bad/name', [])).toMatch(/letters, numbers/);
|
|
24
|
+
});
|
|
25
|
+
it('rejects names that already exist', () => {
|
|
26
|
+
expect(validateScenarioName('basic', ['basic'])).toMatch(/already exists/);
|
|
27
|
+
});
|
|
28
|
+
it('trims before checking for duplicates', () => {
|
|
29
|
+
expect(validateScenarioName(' basic ', ['basic'])).toMatch(/already exists/);
|
|
30
|
+
});
|
|
31
|
+
it('accepts a unique name with allowed characters', () => {
|
|
32
|
+
expect(validateScenarioName('edge_case-1', ['basic'])).toBeNull();
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -10,7 +10,7 @@ const DOCS_URL = 'https://docs.output.ai';
|
|
|
10
10
|
export const Section = ({ children, title, direction = 'v' }) => (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { bold: true, children: title }), _jsx(Box, { flexDirection: direction === 'v' ? 'column' : 'row', gap: 1, flexWrap: 'wrap', children: children })] }));
|
|
11
11
|
export const SubSection = ({ children, title }) => (_jsxs(Box, { flexDirection: "column", gap: 1, borderStyle: "single", borderColor: "blackBright", paddingLeft: 1, paddingRight: 1, children: [_jsx(Text, { italic: true, dimColor: true, children: title }), _jsx(Box, { flexDirection: "column", children: children })] }));
|
|
12
12
|
const KV = ({ label, value }) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: 26, children: _jsx(Text, { children: label }) }), _jsx(Text, { bold: true, children: value })] }));
|
|
13
|
-
const RunFromCli = () => (_jsxs(Section, { title: "Running a workflow", children: [_jsxs(SubSection, { title: "From the CLI", children: [_jsx(InlineSnippet, { content: "npx output workflow run blog_evaluator paulgraham_hwh" }), _jsx(InlineSnippet, { content: 'npx output workflow run simple --input {"values":[1,2,3]}' }), _jsx(InlineSnippet, { content: "npx output workflow run simple --input scenario.json" })] }), _jsxs(SubSection, { title: "From the TUI", children: [_jsxs(Text, { children: ["Open Workflows tab, hover a workflow, press ", _jsx(Text, { bold: true, children: "r" }), "."] }), _jsx(Text, { children: "
|
|
13
|
+
const RunFromCli = () => (_jsxs(Section, { title: "Running a workflow", children: [_jsxs(SubSection, { title: "From the CLI", children: [_jsx(InlineSnippet, { content: "npx output workflow run blog_evaluator paulgraham_hwh" }), _jsx(InlineSnippet, { content: 'npx output workflow run simple --input {"values":[1,2,3]}' }), _jsx(InlineSnippet, { content: "npx output workflow run simple --input scenario.json" })] }), _jsxs(SubSection, { title: "From the TUI", children: [_jsxs(Text, { children: ["Open Workflows tab, hover a workflow, press ", _jsx(Text, { bold: true, children: "r" }), "."] }), _jsx(Text, { children: "Pick a saved scenario, or edit JSON with live validation." }), _jsxs(Text, { children: ["Press ", _jsx(Text, { bold: true, children: "ctrl+r" }), " to run as-is, or ", _jsx(Text, { bold: true, children: "ctrl+s" }), " to save it as a scenario first."] })] })] }));
|
|
14
14
|
const ServiceUrls = () => (_jsx(Section, { title: "Service URLs", children: _jsxs(SubSection, { title: "Where the services are available", children: [_jsx(KV, { label: "Temporal gRPC", value: "localhost:7233" }), _jsx(KV, { label: "Temporal UI", value: "http://localhost:8080" }), _jsx(KV, { label: "API server", value: "localhost:3001" }), _jsx(KV, { label: "Redis", value: "localhost:6379" })] }) }));
|
|
15
15
|
const UpdatingMigrating = () => (_jsxs(Section, { title: "Updating / Migrating", children: [_jsxs(SubSection, { title: "Update", children: [_jsx(Text, { wrap: "wrap", children: "Update the CLI to the latest published version:" }), _jsx(InlineSnippet, { content: "output update" })] }), _jsxs(SubSection, { title: "Migrate", children: [_jsx(Text, { wrap: "wrap", children: "Migrate a workflow project to the SDK version this CLI ships with:" }), _jsx(InlineSnippet, { content: "output migrate" }), _jsx(Text, { wrap: "wrap", children: "The migration walks `package.json` and project files, updates `@outputai/*` deps, and applies any code-mod steps the SDK ships with the new version." })] })] }));
|
|
16
16
|
const ClaudePlugins = () => (_jsx(Section, { title: "Claude Plugins", children: _jsxs(SubSection, { title: "Reinstall", children: [_jsx(Text, { wrap: "wrap", children: "Command `output init` already installs the Claude Code plugins (skills, commands, agents) into your project during scaffolding." }), _jsx(Text, { wrap: "wrap", children: "But if it is necessary to reinstall it, use the update command:" }), _jsx(InlineSnippet, { content: "output update --agents" }), _jsx(Text, { children: "This pulls the latest plugin bundle that ships with the installed CLI version." })] }) }));
|
|
@@ -11,11 +11,13 @@ export declare const tryParseJson: (text: string) => {
|
|
|
11
11
|
ok: false;
|
|
12
12
|
error: string;
|
|
13
13
|
};
|
|
14
|
+
export declare const initialCursor: (buffer: string) => number;
|
|
14
15
|
export declare const JsonEditor: React.FC<{
|
|
15
16
|
seed: unknown;
|
|
16
17
|
title: string;
|
|
17
18
|
isActive?: boolean;
|
|
18
19
|
onSubmit: (value: unknown) => void;
|
|
20
|
+
onSave?: (value: unknown) => void;
|
|
19
21
|
onCancel: () => void;
|
|
20
22
|
}>;
|
|
21
23
|
export {};
|
|
@@ -25,11 +25,17 @@ export const tryParseJson = (text) => {
|
|
|
25
25
|
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
26
26
|
}
|
|
27
27
|
};
|
|
28
|
+
// Start the cursor inside the first empty "" pair (e.g. the key of a fresh `{ "": "" }`
|
|
29
|
+
// seed) so the user can type immediately; otherwise sit at the end of the buffer.
|
|
30
|
+
export const initialCursor = (buffer) => {
|
|
31
|
+
const emptyPair = buffer.indexOf('""');
|
|
32
|
+
return emptyPair === -1 ? buffer.length : emptyPair + 1;
|
|
33
|
+
};
|
|
28
34
|
const VISIBLE_BUFFER = 6;
|
|
29
|
-
export const JsonEditor = ({ seed, title, isActive = true, onSubmit, onCancel }) => {
|
|
35
|
+
export const JsonEditor = ({ seed, title, isActive = true, onSubmit, onSave, onCancel }) => {
|
|
30
36
|
const initial = JSON.stringify(seed ?? {}, null, 2);
|
|
31
37
|
const [buffer, setBuffer] = useState(initial);
|
|
32
|
-
const [cursor, setCursor] = useState(initial
|
|
38
|
+
const [cursor, setCursor] = useState(() => initialCursor(initial));
|
|
33
39
|
const [status, setStatus] = useState(() => tryParseJson(initial));
|
|
34
40
|
const [submitMessage, setSubmitMessage] = useState(null);
|
|
35
41
|
useEffect(() => {
|
|
@@ -46,23 +52,30 @@ export const JsonEditor = ({ seed, title, isActive = true, onSubmit, onCancel })
|
|
|
46
52
|
setBuffer(b => b.slice(0, cursor - 1) + b.slice(cursor));
|
|
47
53
|
setCursor(c => Math.max(0, c - 1));
|
|
48
54
|
};
|
|
55
|
+
const commit = (handler) => {
|
|
56
|
+
const parsed = tryParseJson(buffer);
|
|
57
|
+
if (!parsed.ok) {
|
|
58
|
+
setSubmitMessage(`Invalid JSON — ${parsed.error}`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
handler(JSON.parse(buffer));
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
setSubmitMessage(err instanceof Error ? err.message : String(err));
|
|
66
|
+
}
|
|
67
|
+
};
|
|
49
68
|
useInput((input, key) => {
|
|
50
69
|
if (key.escape) {
|
|
51
70
|
onCancel();
|
|
52
71
|
return;
|
|
53
72
|
}
|
|
54
|
-
if (key.ctrl && input === '
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
try {
|
|
61
|
-
onSubmit(JSON.parse(buffer));
|
|
62
|
-
}
|
|
63
|
-
catch (err) {
|
|
64
|
-
setSubmitMessage(err instanceof Error ? err.message : String(err));
|
|
65
|
-
}
|
|
73
|
+
if (key.ctrl && input === 'r') {
|
|
74
|
+
commit(onSubmit);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (key.ctrl && input === 's' && onSave) {
|
|
78
|
+
commit(onSave);
|
|
66
79
|
return;
|
|
67
80
|
}
|
|
68
81
|
if (key.return) {
|
|
@@ -113,5 +126,5 @@ export const JsonEditor = ({ seed, title, isActive = true, onSubmit, onCancel })
|
|
|
113
126
|
const at = line[cursorPos.col] ?? ' ';
|
|
114
127
|
const after = line.slice(cursorPos.col + 1);
|
|
115
128
|
return (_jsxs(Text, { bold: true, children: [_jsx(Text, { children: before }), _jsx(Text, { inverse: true, children: at }), _jsx(Text, { children: after })] }, lineIdx));
|
|
116
|
-
}) }), !status.ok && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "red", wrap: "truncate-end", children: status.error }) })), submitMessage && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "yellow", children: submitMessage }) })), _jsxs(Box, { marginTop: 1, columnGap: 2, children: [_jsxs(Box, { columnGap: 1, children: [_jsx(Text, { bold: true, children: "ctrl+s" }), _jsx(Text, { dimColor: true, children: "
|
|
129
|
+
}) }), !status.ok && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "red", wrap: "truncate-end", children: status.error }) })), submitMessage && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "yellow", children: submitMessage }) })), _jsxs(Box, { marginTop: 1, columnGap: 2, children: [_jsxs(Box, { columnGap: 1, children: [_jsx(Text, { bold: true, children: "ctrl+r" }), _jsx(Text, { dimColor: true, children: "run" })] }), onSave ? (_jsxs(Box, { columnGap: 1, children: [_jsx(Text, { bold: true, children: "ctrl+s" }), _jsx(Text, { dimColor: true, children: "save & run" })] })) : null, _jsxs(Box, { columnGap: 1, children: [_jsx(Text, { bold: true, children: "esc" }), _jsx(Text, { dimColor: true, children: "cancel" })] }), _jsxs(Box, { columnGap: 1, children: [_jsx(Text, { bold: true, children: "\u2191\u2193\u2190\u2192" }), _jsx(Text, { dimColor: true, children: "move" })] }), _jsxs(Box, { columnGap: 1, children: [_jsx(Text, { bold: true, children: "tab" }), _jsx(Text, { dimColor: true, children: "indent" })] })] })] }));
|
|
117
130
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { cursorToPosition, positionToCursor, tryParseJson } from './json_editor.js';
|
|
2
|
+
import { cursorToPosition, initialCursor, positionToCursor, tryParseJson } from './json_editor.js';
|
|
3
3
|
describe('cursorToPosition', () => {
|
|
4
4
|
it('returns line 0 col 0 for an empty buffer', () => {
|
|
5
5
|
expect(cursorToPosition('', 0)).toEqual({ line: 0, col: 0 });
|
|
@@ -55,3 +55,18 @@ describe('tryParseJson', () => {
|
|
|
55
55
|
}
|
|
56
56
|
});
|
|
57
57
|
});
|
|
58
|
+
describe('initialCursor', () => {
|
|
59
|
+
it('lands inside the first empty "" pair so typing starts there', () => {
|
|
60
|
+
const buffer = JSON.stringify({ '': '' }, null, 2); // {\n "": ""\n}
|
|
61
|
+
const cursor = initialCursor(buffer);
|
|
62
|
+
expect(cursor).toBe(buffer.indexOf('""') + 1);
|
|
63
|
+
expect(buffer[cursor - 1]).toBe('"');
|
|
64
|
+
});
|
|
65
|
+
it('falls back to the end when there is no empty pair', () => {
|
|
66
|
+
const buffer = JSON.stringify({ values: [1, 2, 3] }, null, 2);
|
|
67
|
+
expect(initialCursor(buffer)).toBe(buffer.length);
|
|
68
|
+
});
|
|
69
|
+
it('returns 0 for an empty buffer', () => {
|
|
70
|
+
expect(initialCursor('')).toBe(0);
|
|
71
|
+
});
|
|
72
|
+
});
|
package/oclif.manifest.json
CHANGED
|
@@ -1164,6 +1164,22 @@
|
|
|
1164
1164
|
"allowNo": false,
|
|
1165
1165
|
"type": "boolean"
|
|
1166
1166
|
},
|
|
1167
|
+
"catalog": {
|
|
1168
|
+
"aliases": [
|
|
1169
|
+
"task-queue"
|
|
1170
|
+
],
|
|
1171
|
+
"char": "c",
|
|
1172
|
+
"charAliases": [
|
|
1173
|
+
"q"
|
|
1174
|
+
],
|
|
1175
|
+
"deprecateAliases": true,
|
|
1176
|
+
"description": "Catalog name for workflow execution (defaults to OUTPUT_CATALOG_ID)",
|
|
1177
|
+
"env": "OUTPUT_CATALOG_ID",
|
|
1178
|
+
"name": "catalog",
|
|
1179
|
+
"hasDynamicHelp": false,
|
|
1180
|
+
"multiple": false,
|
|
1181
|
+
"type": "option"
|
|
1182
|
+
},
|
|
1167
1183
|
"cached": {
|
|
1168
1184
|
"description": "Use cached output from dataset files (skip workflow execution)",
|
|
1169
1185
|
"exclusive": [
|
|
@@ -1228,6 +1244,22 @@
|
|
|
1228
1244
|
"<%= config.bin %> <%= command.id %> simple --download --limit 5"
|
|
1229
1245
|
],
|
|
1230
1246
|
"flags": {
|
|
1247
|
+
"catalog": {
|
|
1248
|
+
"aliases": [
|
|
1249
|
+
"task-queue"
|
|
1250
|
+
],
|
|
1251
|
+
"char": "c",
|
|
1252
|
+
"charAliases": [
|
|
1253
|
+
"q"
|
|
1254
|
+
],
|
|
1255
|
+
"deprecateAliases": true,
|
|
1256
|
+
"description": "Catalog name for workflow execution (defaults to OUTPUT_CATALOG_ID)",
|
|
1257
|
+
"env": "OUTPUT_CATALOG_ID",
|
|
1258
|
+
"name": "catalog",
|
|
1259
|
+
"hasDynamicHelp": false,
|
|
1260
|
+
"multiple": false,
|
|
1261
|
+
"type": "option"
|
|
1262
|
+
},
|
|
1231
1263
|
"trace": {
|
|
1232
1264
|
"char": "t",
|
|
1233
1265
|
"description": "Path to a local trace file to extract dataset from",
|
|
@@ -1422,5 +1454,5 @@
|
|
|
1422
1454
|
]
|
|
1423
1455
|
}
|
|
1424
1456
|
},
|
|
1425
|
-
"version": "0.8.2-next.
|
|
1457
|
+
"version": "0.8.2-next.e658cc2.0"
|
|
1426
1458
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@outputai/cli",
|
|
3
|
-
"version": "0.8.2-next.
|
|
3
|
+
"version": "0.8.2-next.e658cc2.0",
|
|
4
4
|
"description": "CLI for Output.ai workflow generation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -34,11 +34,11 @@
|
|
|
34
34
|
"ky": "1.14.3",
|
|
35
35
|
"react": "19.2.5",
|
|
36
36
|
"semver": "7.7.4",
|
|
37
|
-
"undici": "8.
|
|
37
|
+
"undici": "8.5.0",
|
|
38
38
|
"yaml": "^2.8.3",
|
|
39
|
-
"@outputai/
|
|
40
|
-
"@outputai/
|
|
41
|
-
"@outputai/credentials": "0.8.2-next.
|
|
39
|
+
"@outputai/evals": "0.8.2-next.e658cc2.0",
|
|
40
|
+
"@outputai/llm": "0.8.2-next.e658cc2.0",
|
|
41
|
+
"@outputai/credentials": "0.8.2-next.e658cc2.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@types/cli-progress": "3.11.6",
|