@outputai/cli 0.7.1-next.bd6bd49.0 → 0.7.1-next.c005dac.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 +2 -14
- package/dist/api/generated/api.js +2 -2
- package/dist/api/workflow_catalog.d.ts +7 -0
- package/dist/api/workflow_catalog.js +11 -0
- package/dist/api/workflow_catalog.spec.d.ts +1 -0
- package/dist/api/workflow_catalog.spec.js +30 -0
- package/dist/assets/docker/docker-compose-dev.yml +1 -1
- package/dist/commands/workflow/list.d.ts +2 -0
- package/dist/commands/workflow/list.js +23 -21
- package/dist/commands/workflow/list.spec.js +57 -0
- package/dist/commands/workflow/status.js +6 -1
- package/dist/generated/framework_version.json +1 -1
- package/dist/services/fix_package.spec.js +3 -2
- package/dist/services/workflow_runs.js +6 -1
- package/dist/templates/agent_instructions/CLAUDE.md.template +1 -0
- package/dist/templates/project/README.md.template +32 -0
- package/dist/templates/project/package.json.template +1 -0
- package/dist/utils/format_workflow_result.js +4 -2
- package/dist/utils/format_workflow_result.spec.js +13 -5
- package/dist/utils/normalize_workflow_status.d.ts +8 -0
- package/dist/utils/normalize_workflow_status.js +8 -0
- package/dist/utils/normalize_workflow_status.spec.d.ts +1 -0
- package/dist/utils/normalize_workflow_status.spec.js +13 -0
- package/dist/utils/scenario_resolver.js +3 -11
- package/dist/utils/scenario_resolver.spec.js +5 -9
- package/dist/views/dev/components/workflow_status.js +1 -1
- package/dist/views/dev/hooks/use_run_detail.js +6 -1
- package/dist/views/dev/hooks/use_run_detail.spec.js +1 -1
- package/dist/views/dev/hooks/use_workflow_catalog.js +2 -6
- package/dist/views/dev/panels/runs_panel.js +5 -9
- package/dist/views/dev/state/ui_state.d.ts +1 -1
- package/oclif.manifest.json +19 -2
- package/package.json +4 -4
|
@@ -157,7 +157,7 @@ export declare const WorkflowRunInfoStatus: {
|
|
|
157
157
|
readonly canceled: "canceled";
|
|
158
158
|
readonly terminated: "terminated";
|
|
159
159
|
readonly timed_out: "timed_out";
|
|
160
|
-
readonly
|
|
160
|
+
readonly continued_as_new: "continued_as_new";
|
|
161
161
|
};
|
|
162
162
|
export interface WorkflowRunInfo {
|
|
163
163
|
/** Unique identifier for this run */
|
|
@@ -207,13 +207,6 @@ export interface WorkflowStatusResponse {
|
|
|
207
207
|
/** An epoch timestamp representing when the workflow ended */
|
|
208
208
|
completedAt?: number;
|
|
209
209
|
}
|
|
210
|
-
/**
|
|
211
|
-
* Convenience totals aggregated from LLM and http usage and cost
|
|
212
|
-
* @nullable
|
|
213
|
-
*/
|
|
214
|
-
export type WorkflowResultResponseAggregations = {
|
|
215
|
-
[key: string]: unknown;
|
|
216
|
-
} | null;
|
|
217
210
|
/**
|
|
218
211
|
* The workflow execution status
|
|
219
212
|
*/
|
|
@@ -224,7 +217,7 @@ export declare const WorkflowResultResponseStatus: {
|
|
|
224
217
|
readonly canceled: "canceled";
|
|
225
218
|
readonly terminated: "terminated";
|
|
226
219
|
readonly timed_out: "timed_out";
|
|
227
|
-
readonly
|
|
220
|
+
readonly continued_as_new: "continued_as_new";
|
|
228
221
|
};
|
|
229
222
|
/**
|
|
230
223
|
* Structured failure details if the workflow failed, null otherwise
|
|
@@ -269,11 +262,6 @@ export interface WorkflowResultResponse {
|
|
|
269
262
|
/** The result of workflow, null if workflow failed */
|
|
270
263
|
output?: unknown;
|
|
271
264
|
trace?: TraceInfo;
|
|
272
|
-
/**
|
|
273
|
-
* Convenience totals aggregated from LLM and http usage and cost
|
|
274
|
-
* @nullable
|
|
275
|
-
*/
|
|
276
|
-
aggregations?: WorkflowResultResponseAggregations;
|
|
277
265
|
/** The workflow execution status */
|
|
278
266
|
status?: WorkflowResultResponseStatus;
|
|
279
267
|
/**
|
|
@@ -22,7 +22,7 @@ export const WorkflowRunInfoStatus = {
|
|
|
22
22
|
canceled: 'canceled',
|
|
23
23
|
terminated: 'terminated',
|
|
24
24
|
timed_out: 'timed_out',
|
|
25
|
-
|
|
25
|
+
continued_as_new: 'continued_as_new',
|
|
26
26
|
};
|
|
27
27
|
export const WorkflowStatusResponseStatus = {
|
|
28
28
|
canceled: 'canceled',
|
|
@@ -40,7 +40,7 @@ export const WorkflowResultResponseStatus = {
|
|
|
40
40
|
canceled: 'canceled',
|
|
41
41
|
terminated: 'terminated',
|
|
42
42
|
timed_out: 'timed_out',
|
|
43
|
-
|
|
43
|
+
continued_as_new: 'continued_as_new',
|
|
44
44
|
};
|
|
45
45
|
;
|
|
46
46
|
export const getGetHealthUrl = () => {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type Workflow } from './generated/api.js';
|
|
2
|
+
/**
|
|
3
|
+
* Resolve the workflows in a catalog. When `catalog` is provided (e.g. from
|
|
4
|
+
* `--catalog`/`OUTPUT_CATALOG_ID`) it resolves that specific catalog, otherwise
|
|
5
|
+
* the API server's default catalog. Returns `[]` when the catalog has no workflows.
|
|
6
|
+
*/
|
|
7
|
+
export declare function fetchWorkflowCatalog(catalog?: string): Promise<Workflow[]>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { getWorkflowCatalog, getWorkflowCatalogId } from './generated/api.js';
|
|
2
|
+
/**
|
|
3
|
+
* Resolve the workflows in a catalog. When `catalog` is provided (e.g. from
|
|
4
|
+
* `--catalog`/`OUTPUT_CATALOG_ID`) it resolves that specific catalog, otherwise
|
|
5
|
+
* the API server's default catalog. Returns `[]` when the catalog has no workflows.
|
|
6
|
+
*/
|
|
7
|
+
export async function fetchWorkflowCatalog(catalog) {
|
|
8
|
+
const response = catalog ? await getWorkflowCatalogId(catalog) : await getWorkflowCatalog();
|
|
9
|
+
const data = response?.data;
|
|
10
|
+
return data?.workflows ?? [];
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import * as api from './generated/api.js';
|
|
3
|
+
import { fetchWorkflowCatalog } from './workflow_catalog.js';
|
|
4
|
+
vi.mock('./generated/api.js', () => ({
|
|
5
|
+
getWorkflowCatalog: vi.fn(),
|
|
6
|
+
getWorkflowCatalogId: vi.fn()
|
|
7
|
+
}));
|
|
8
|
+
describe('fetchWorkflowCatalog', () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
vi.clearAllMocks();
|
|
11
|
+
});
|
|
12
|
+
it('fetches the default catalog when no catalog id is provided', async () => {
|
|
13
|
+
vi.mocked(api.getWorkflowCatalog).mockResolvedValue({ data: { workflows: [{ name: 'a' }] } });
|
|
14
|
+
const result = await fetchWorkflowCatalog();
|
|
15
|
+
expect(api.getWorkflowCatalog).toHaveBeenCalledTimes(1);
|
|
16
|
+
expect(api.getWorkflowCatalogId).not.toHaveBeenCalled();
|
|
17
|
+
expect(result).toEqual([{ name: 'a' }]);
|
|
18
|
+
});
|
|
19
|
+
it('fetches a specific catalog by id when one is provided', async () => {
|
|
20
|
+
vi.mocked(api.getWorkflowCatalogId).mockResolvedValue({ data: { workflows: [{ name: 'b' }] } });
|
|
21
|
+
const result = await fetchWorkflowCatalog('my-catalog');
|
|
22
|
+
expect(api.getWorkflowCatalogId).toHaveBeenCalledWith('my-catalog');
|
|
23
|
+
expect(api.getWorkflowCatalog).not.toHaveBeenCalled();
|
|
24
|
+
expect(result).toEqual([{ name: 'b' }]);
|
|
25
|
+
});
|
|
26
|
+
it('returns an empty array when the catalog response has no workflows', async () => {
|
|
27
|
+
vi.mocked(api.getWorkflowCatalog).mockResolvedValue({ data: {} });
|
|
28
|
+
expect(await fetchWorkflowCatalog()).toEqual([]);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -9,10 +9,12 @@ interface WorkflowDisplay {
|
|
|
9
9
|
aliases: string;
|
|
10
10
|
}
|
|
11
11
|
export declare function parseWorkflowForDisplay(workflow: Workflow): WorkflowDisplay;
|
|
12
|
+
export declare function formatWorkflowsAsList(workflows: Workflow[]): string;
|
|
12
13
|
export default class WorkflowList extends Command {
|
|
13
14
|
static description: string;
|
|
14
15
|
static examples: string[];
|
|
15
16
|
static flags: {
|
|
17
|
+
catalog: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
18
|
format: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
19
|
detailed: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
18
20
|
filter: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Command, Flags } from '@oclif/core';
|
|
2
2
|
import Table from 'cli-table3';
|
|
3
|
-
import {
|
|
3
|
+
import { fetchWorkflowCatalog } from '#api/workflow_catalog.js';
|
|
4
4
|
import { parseWorkflowDefinition, formatParameters } from '#api/parser.js';
|
|
5
5
|
import { handleApiError } from '#utils/error_handler.js';
|
|
6
6
|
import { listScenariosForWorkflow } from '#utils/scenario_resolver.js';
|
|
@@ -65,10 +65,13 @@ function createWorkflowTable(workflows, detailed) {
|
|
|
65
65
|
});
|
|
66
66
|
return table.toString();
|
|
67
67
|
}
|
|
68
|
-
function
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
function formatWorkflowAsListItem(workflow) {
|
|
69
|
+
const { name, aliases } = parseWorkflowForDisplay(workflow);
|
|
70
|
+
return aliases === 'none' ? `- ${name}` : `- ${name} (aliases: ${aliases})`;
|
|
71
|
+
}
|
|
72
|
+
export function formatWorkflowsAsList(workflows) {
|
|
73
|
+
const lines = sortWorkflowsByName(workflows).map(formatWorkflowAsListItem);
|
|
74
|
+
return `\nWorkflows:\n\n${lines.join('\n')}`;
|
|
72
75
|
}
|
|
73
76
|
function formatWorkflowsAsJson(workflows) {
|
|
74
77
|
const output = {
|
|
@@ -103,9 +106,18 @@ export default class WorkflowList extends Command {
|
|
|
103
106
|
'<%= config.bin %> <%= command.id %> --format table',
|
|
104
107
|
'<%= config.bin %> <%= command.id %> --format json',
|
|
105
108
|
'<%= config.bin %> <%= command.id %> --detailed',
|
|
106
|
-
'<%= config.bin %> <%= command.id %> --filter simple'
|
|
109
|
+
'<%= config.bin %> <%= command.id %> --filter simple',
|
|
110
|
+
'<%= config.bin %> <%= command.id %> --catalog my-catalog'
|
|
107
111
|
];
|
|
108
112
|
static flags = {
|
|
113
|
+
catalog: Flags.string({
|
|
114
|
+
char: 'c',
|
|
115
|
+
aliases: ['task-queue'],
|
|
116
|
+
charAliases: ['q'],
|
|
117
|
+
deprecateAliases: true,
|
|
118
|
+
description: 'Catalog to list workflows from (defaults to OUTPUT_CATALOG_ID)',
|
|
119
|
+
env: 'OUTPUT_CATALOG_ID'
|
|
120
|
+
}),
|
|
109
121
|
format: Flags.string({
|
|
110
122
|
char: 'f',
|
|
111
123
|
description: 'Output format',
|
|
@@ -123,25 +135,15 @@ export default class WorkflowList extends Command {
|
|
|
123
135
|
};
|
|
124
136
|
async run() {
|
|
125
137
|
const { flags } = await this.parse(WorkflowList);
|
|
126
|
-
this.log('Fetching workflow catalog...');
|
|
127
|
-
const
|
|
128
|
-
if (
|
|
129
|
-
this.error('Failed to connect to API server. Is it running?', { exit: 1 });
|
|
130
|
-
}
|
|
131
|
-
if (!response.data) {
|
|
132
|
-
this.error('API returned invalid response (missing data)', { exit: 1 });
|
|
133
|
-
}
|
|
134
|
-
const data = response.data;
|
|
135
|
-
if (!data.workflows) {
|
|
136
|
-
this.error('API returned invalid response (missing workflows)', { exit: 1 });
|
|
137
|
-
}
|
|
138
|
-
if (data.workflows.length === 0) {
|
|
138
|
+
this.log(flags.catalog ? `Fetching workflow catalog: ${flags.catalog}...` : 'Fetching workflow catalog...');
|
|
139
|
+
const catalogWorkflows = await fetchWorkflowCatalog(flags.catalog);
|
|
140
|
+
if (catalogWorkflows.length === 0) {
|
|
139
141
|
this.log('No workflows found in catalog.');
|
|
140
142
|
return;
|
|
141
143
|
}
|
|
142
144
|
const workflows = flags.filter ?
|
|
143
|
-
|
|
144
|
-
|
|
145
|
+
catalogWorkflows.filter(matchName(flags.filter)) :
|
|
146
|
+
catalogWorkflows;
|
|
145
147
|
if (workflows.length === 0 && flags.filter) {
|
|
146
148
|
this.log(`No workflows matching filter: ${flags.filter}`);
|
|
147
149
|
return;
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
1
2
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
3
|
const mockListScenarios = vi.fn().mockReturnValue([]);
|
|
3
4
|
vi.mock('#utils/scenario_resolver.js', () => ({
|
|
4
5
|
listScenariosForWorkflow: mockListScenarios
|
|
5
6
|
}));
|
|
7
|
+
vi.mock('#api/workflow_catalog.js', () => ({
|
|
8
|
+
fetchWorkflowCatalog: vi.fn()
|
|
9
|
+
}));
|
|
6
10
|
describe('workflow list command', () => {
|
|
7
11
|
beforeEach(() => {
|
|
8
12
|
vi.clearAllMocks();
|
|
@@ -15,6 +19,12 @@ describe('workflow list command', () => {
|
|
|
15
19
|
expect(WorkflowList.flags).toHaveProperty('format');
|
|
16
20
|
expect(WorkflowList.flags).toHaveProperty('detailed');
|
|
17
21
|
expect(WorkflowList.flags).toHaveProperty('filter');
|
|
22
|
+
expect(WorkflowList.flags).toHaveProperty('catalog');
|
|
23
|
+
});
|
|
24
|
+
it('reads the catalog flag from OUTPUT_CATALOG_ID', async () => {
|
|
25
|
+
const WorkflowList = (await import('./list.js')).default;
|
|
26
|
+
expect(WorkflowList.flags.catalog.env).toBe('OUTPUT_CATALOG_ID');
|
|
27
|
+
expect(WorkflowList.flags.catalog.char).toBe('c');
|
|
18
28
|
});
|
|
19
29
|
it('should have correct flag configuration', async () => {
|
|
20
30
|
const WorkflowList = (await import('./list.js')).default;
|
|
@@ -118,3 +128,50 @@ describe('workflow list parsing', () => {
|
|
|
118
128
|
expect(parsed.inputs).toContain('user.email: string');
|
|
119
129
|
});
|
|
120
130
|
});
|
|
131
|
+
describe('formatWorkflowsAsList', () => {
|
|
132
|
+
it('appends aliases to the default list when present', async () => {
|
|
133
|
+
const { formatWorkflowsAsList } = await import('./list.js');
|
|
134
|
+
const output = formatWorkflowsAsList([
|
|
135
|
+
{ name: 'galileoExtractKeyword', aliases: ['seoContentExtractKeywordWorkflow'] }
|
|
136
|
+
]);
|
|
137
|
+
expect(output).toContain('- galileoExtractKeyword (aliases: seoContentExtractKeywordWorkflow)');
|
|
138
|
+
});
|
|
139
|
+
it('omits the aliases segment when a workflow has none', async () => {
|
|
140
|
+
const { formatWorkflowsAsList } = await import('./list.js');
|
|
141
|
+
const output = formatWorkflowsAsList([{ name: 'simple' }]);
|
|
142
|
+
expect(output).toContain('- simple');
|
|
143
|
+
expect(output).not.toContain('aliases:');
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
describe('run() catalog resolution', () => {
|
|
147
|
+
beforeEach(() => {
|
|
148
|
+
vi.clearAllMocks();
|
|
149
|
+
});
|
|
150
|
+
const catalogWorkflows = [{ name: 'simple' }];
|
|
151
|
+
const createCommand = async (flagOverrides) => {
|
|
152
|
+
const WorkflowList = (await import('./list.js')).default;
|
|
153
|
+
const cmd = new WorkflowList([], {});
|
|
154
|
+
cmd.log = vi.fn();
|
|
155
|
+
cmd.error = vi.fn(() => {
|
|
156
|
+
throw new Error('error called');
|
|
157
|
+
});
|
|
158
|
+
cmd.parse = vi.fn().mockResolvedValue({
|
|
159
|
+
flags: { format: 'list', detailed: false, filter: undefined, catalog: undefined, ...flagOverrides }
|
|
160
|
+
});
|
|
161
|
+
return cmd;
|
|
162
|
+
};
|
|
163
|
+
it('fetches a specific catalog by id when a catalog is provided', async () => {
|
|
164
|
+
const { fetchWorkflowCatalog } = await import('#api/workflow_catalog.js');
|
|
165
|
+
vi.mocked(fetchWorkflowCatalog).mockResolvedValue(catalogWorkflows);
|
|
166
|
+
const cmd = await createCommand({ catalog: 'my-catalog' });
|
|
167
|
+
await cmd.run();
|
|
168
|
+
expect(fetchWorkflowCatalog).toHaveBeenCalledWith('my-catalog');
|
|
169
|
+
});
|
|
170
|
+
it('falls back to the default catalog when no catalog is provided', async () => {
|
|
171
|
+
const { fetchWorkflowCatalog } = await import('#api/workflow_catalog.js');
|
|
172
|
+
vi.mocked(fetchWorkflowCatalog).mockResolvedValue(catalogWorkflows);
|
|
173
|
+
const cmd = await createCommand({ catalog: undefined });
|
|
174
|
+
await cmd.run();
|
|
175
|
+
expect(fetchWorkflowCatalog).toHaveBeenCalledWith(undefined);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
@@ -3,6 +3,7 @@ import { getWorkflowIdStatus } from '#api/generated/api.js';
|
|
|
3
3
|
import { OUTPUT_FORMAT } from '#utils/constants.js';
|
|
4
4
|
import { formatOutput } from '#utils/output_formatter.js';
|
|
5
5
|
import { handleApiError } from '#utils/error_handler.js';
|
|
6
|
+
import { normalizeWorkflowStatus } from '#utils/normalize_workflow_status.js';
|
|
6
7
|
export default class WorkflowStatus extends Command {
|
|
7
8
|
static description = 'Get workflow execution status';
|
|
8
9
|
static examples = [
|
|
@@ -30,7 +31,11 @@ export default class WorkflowStatus extends Command {
|
|
|
30
31
|
if (!response || !response.data) {
|
|
31
32
|
this.error('API returned invalid response', { exit: 1 });
|
|
32
33
|
}
|
|
33
|
-
const
|
|
34
|
+
const rawData = response.data;
|
|
35
|
+
const data = {
|
|
36
|
+
...rawData,
|
|
37
|
+
status: normalizeWorkflowStatus(rawData.status)
|
|
38
|
+
};
|
|
34
39
|
const output = formatOutput(data, flags.format, (result) => {
|
|
35
40
|
const lines = [
|
|
36
41
|
`Workflow ID: ${result.workflowId || 'unknown'}`,
|
|
@@ -19,7 +19,7 @@ describe('fix package', () => {
|
|
|
19
19
|
expect(plan.scriptsToRemove.map(r => r.key).sort()).toEqual([...legacyScripts].sort());
|
|
20
20
|
expect(plan.hasChanges).toBe(true);
|
|
21
21
|
expect(plan.scriptsToReplace).toEqual([]);
|
|
22
|
-
expect(plan.scriptsToAdd).toHaveLength(
|
|
22
|
+
expect(plan.scriptsToAdd).toHaveLength(7);
|
|
23
23
|
applyFix(plan);
|
|
24
24
|
const next = JSON.parse(await fs.readFile(path.join(tmpDir, 'package.json'), 'utf-8'));
|
|
25
25
|
expect(next.scripts['dev']).toBeUndefined();
|
|
@@ -42,10 +42,11 @@ describe('fix package', () => {
|
|
|
42
42
|
expect(plan.scriptsToReplace).toEqual([
|
|
43
43
|
{ key: 'output:dev', before: 'bad-dev-command', after: 'output dev' }
|
|
44
44
|
]);
|
|
45
|
-
expect(plan.scriptsToAdd).toHaveLength(
|
|
45
|
+
expect(plan.scriptsToAdd).toHaveLength(6);
|
|
46
46
|
expect(plan.scriptsToAdd.map(a => a.key).sort()).toEqual([
|
|
47
47
|
'output:worker',
|
|
48
48
|
'output:worker:build',
|
|
49
|
+
'output:worker:check',
|
|
49
50
|
'output:worker:install',
|
|
50
51
|
'output:worker:start',
|
|
51
52
|
'output:worker:watch'
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Workflow runs service for fetching workflow run data from the API
|
|
3
3
|
*/
|
|
4
4
|
import { getWorkflowRuns } from '#api/generated/api.js';
|
|
5
|
+
import { normalizeWorkflowStatus } from '#utils/normalize_workflow_status.js';
|
|
5
6
|
export async function fetchWorkflowRuns(options = {}) {
|
|
6
7
|
const params = {};
|
|
7
8
|
if (options.limit) {
|
|
@@ -21,8 +22,12 @@ export async function fetchWorkflowRuns(options = {}) {
|
|
|
21
22
|
throw new Error('API returned invalid response (missing data)');
|
|
22
23
|
}
|
|
23
24
|
const data = response.data;
|
|
25
|
+
const runs = (data.runs || []).map(run => ({
|
|
26
|
+
...run,
|
|
27
|
+
status: normalizeWorkflowStatus(run.status)
|
|
28
|
+
}));
|
|
24
29
|
return {
|
|
25
|
-
runs
|
|
30
|
+
runs,
|
|
26
31
|
count: data.count || 0
|
|
27
32
|
};
|
|
28
33
|
}
|
|
@@ -16,6 +16,7 @@ claude plugin install outputai@outputai --scope project
|
|
|
16
16
|
```bash
|
|
17
17
|
npm run output:dev # Start dev environment (worker + Temporal)
|
|
18
18
|
npm run output:worker:build # Build TypeScript to dist/
|
|
19
|
+
npm run output:worker:check # Optional: bundle-check workflows for bad imports (node: built-ins)
|
|
19
20
|
npm run output:worker:watch # Build + restart on file changes
|
|
20
21
|
npm run output:worker # Install, build, and start worker
|
|
21
22
|
```
|
|
@@ -100,3 +100,35 @@ Monitor workflow execution and system status in the Temporal UI:
|
|
|
100
100
|
```bash
|
|
101
101
|
open http://localhost:8080
|
|
102
102
|
```
|
|
103
|
+
|
|
104
|
+
## Checking Workflows for Bad Imports
|
|
105
|
+
|
|
106
|
+
Temporal bundles your workflows before they run. If a workflow — or anything it
|
|
107
|
+
transitively imports — pulls in a `node:` built-in (e.g. `node:fs`), the bundle fails
|
|
108
|
+
**at worker startup**, not at `tsc` build time. The optional bundle check reproduces
|
|
109
|
+
that bundling so you catch it early:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
npm run output:worker:build # compile to dist/
|
|
113
|
+
npm run output:worker:check # bundle-check workflows (runs output-worker --check)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
It exits non-zero and names the offending module when a workflow can't be bundled.
|
|
117
|
+
|
|
118
|
+
To gate merges, wire it into your CI — for example, GitHub Actions:
|
|
119
|
+
|
|
120
|
+
```yaml
|
|
121
|
+
# .github/workflows/output-check.yml
|
|
122
|
+
name: Output checks
|
|
123
|
+
on: [pull_request]
|
|
124
|
+
jobs:
|
|
125
|
+
workflows:
|
|
126
|
+
runs-on: ubuntu-latest
|
|
127
|
+
steps:
|
|
128
|
+
- uses: actions/checkout@v4
|
|
129
|
+
- uses: actions/setup-node@v4
|
|
130
|
+
with: { node-version: 24 }
|
|
131
|
+
- run: npm ci
|
|
132
|
+
- run: npm run output:worker:build
|
|
133
|
+
- run: npm run output:worker:check
|
|
134
|
+
```
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"output:worker:install": "npm install",
|
|
9
9
|
"output:worker:build": "rm -rf dist/* && tsc -p ./ && output-copy-assets",
|
|
10
10
|
"output:worker:start": "output-worker",
|
|
11
|
+
"output:worker:check": "output-worker --check",
|
|
11
12
|
"output:worker": "npm run output:worker:install && npm run output:worker:build && npm run output:worker:start",
|
|
12
13
|
"output:worker:watch": "npx nodemon --watch src --watch package.json --ext ts,js,json,prompt,md --ignore 'dist/**' --ignore '**/*.spec.*' --ignore '**/*.test.*' --exec 'npm run output:worker'",
|
|
13
14
|
"output:dev": "output dev"
|
|
@@ -1,15 +1,17 @@
|
|
|
1
|
+
import { normalizeWorkflowStatus } from './normalize_workflow_status.js';
|
|
1
2
|
export const ERROR_STATUSES = new Set(['failed', 'canceled', 'terminated', 'timed_out']);
|
|
2
3
|
export function formatWorkflowResult(result) {
|
|
4
|
+
const status = normalizeWorkflowStatus(result.status);
|
|
3
5
|
const lines = [
|
|
4
6
|
`Workflow ID: ${result.workflowId || 'unknown'}`,
|
|
5
7
|
''
|
|
6
8
|
];
|
|
7
|
-
if (
|
|
9
|
+
if (status === 'completed') {
|
|
8
10
|
lines.push('Output:');
|
|
9
11
|
lines.push(JSON.stringify(result.output, null, 2));
|
|
10
12
|
}
|
|
11
13
|
else {
|
|
12
|
-
lines.push(`Status: ${
|
|
14
|
+
lines.push(`Status: ${status || 'unknown'}`);
|
|
13
15
|
if (result.error) {
|
|
14
16
|
lines.push(`Error: ${result.error}`);
|
|
15
17
|
}
|
|
@@ -46,16 +46,26 @@ describe('formatWorkflowResult', () => {
|
|
|
46
46
|
expect(result).toContain('Status: canceled');
|
|
47
47
|
expect(result).toContain('Error: Workflow was canceled');
|
|
48
48
|
});
|
|
49
|
-
it('should display status without error line for
|
|
49
|
+
it('should display status without error line for continued_as_new workflows', () => {
|
|
50
50
|
const result = formatWorkflowResult({
|
|
51
51
|
workflowId: 'wf-cont',
|
|
52
|
-
status: '
|
|
52
|
+
status: 'continued_as_new',
|
|
53
53
|
output: null,
|
|
54
54
|
error: null
|
|
55
55
|
});
|
|
56
|
-
expect(result).toContain('Status:
|
|
56
|
+
expect(result).toContain('Status: continued_as_new');
|
|
57
57
|
expect(result).not.toContain('Error:');
|
|
58
58
|
});
|
|
59
|
+
it('temporarily normalizes legacy continued status to continued_as_new', () => {
|
|
60
|
+
const legacyResult = {
|
|
61
|
+
workflowId: 'wf-cont',
|
|
62
|
+
status: 'continued',
|
|
63
|
+
output: null,
|
|
64
|
+
error: null
|
|
65
|
+
};
|
|
66
|
+
const result = formatWorkflowResult(legacyResult);
|
|
67
|
+
expect(result).toContain('Status: continued_as_new');
|
|
68
|
+
});
|
|
59
69
|
it('should omit error line when error is null on failed workflow', () => {
|
|
60
70
|
const result = formatWorkflowResult({
|
|
61
71
|
workflowId: 'wf-789',
|
|
@@ -71,13 +81,11 @@ describe('formatWorkflowResult', () => {
|
|
|
71
81
|
workflowId: 'wf-456',
|
|
72
82
|
status: 'failed',
|
|
73
83
|
output: null,
|
|
74
|
-
aggregations: { cost: { total: 0.4 }, tokens: { total: 20 }, httpRequests: { total: 0 } },
|
|
75
84
|
error: 'Activity task failed'
|
|
76
85
|
};
|
|
77
86
|
const output = formatOutput(data, 'json', formatWorkflowResult);
|
|
78
87
|
const parsed = JSON.parse(output);
|
|
79
88
|
expect(parsed.status).toBe('failed');
|
|
80
|
-
expect(parsed.aggregations).toEqual({ cost: { total: 0.4 }, tokens: { total: 20 }, httpRequests: { total: 0 } });
|
|
81
89
|
expect(parsed.error).toBe('Activity task failed');
|
|
82
90
|
});
|
|
83
91
|
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Temporary compatibility for API responses produced before CONTINUED_AS_NEW
|
|
3
|
+
* was exposed as `continued_as_new`.
|
|
4
|
+
*
|
|
5
|
+
* @param status - Workflow status from the API
|
|
6
|
+
* @returns Normalized workflow status
|
|
7
|
+
*/
|
|
8
|
+
export declare const normalizeWorkflowStatus: <T extends string | null | undefined>(status: T) => T | "continued_as_new";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Temporary compatibility for API responses produced before CONTINUED_AS_NEW
|
|
3
|
+
* was exposed as `continued_as_new`.
|
|
4
|
+
*
|
|
5
|
+
* @param status - Workflow status from the API
|
|
6
|
+
* @returns Normalized workflow status
|
|
7
|
+
*/
|
|
8
|
+
export const normalizeWorkflowStatus = (status) => status === 'continued' ? 'continued_as_new' : status;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { normalizeWorkflowStatus } from './normalize_workflow_status.js';
|
|
3
|
+
describe('normalizeWorkflowStatus', () => {
|
|
4
|
+
it('temporarily maps continued to continued_as_new', () => {
|
|
5
|
+
expect(normalizeWorkflowStatus('continued')).toBe('continued_as_new');
|
|
6
|
+
});
|
|
7
|
+
it('leaves other statuses and nullish values unchanged', () => {
|
|
8
|
+
expect(normalizeWorkflowStatus('completed')).toBe('completed');
|
|
9
|
+
expect(normalizeWorkflowStatus('continued_as_new')).toBe('continued_as_new');
|
|
10
|
+
expect(normalizeWorkflowStatus(null)).toBeNull();
|
|
11
|
+
expect(normalizeWorkflowStatus(undefined)).toBeUndefined();
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readdirSync } from 'node:fs';
|
|
2
2
|
import { dirname, resolve } from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { fetchWorkflowCatalog } from '#api/workflow_catalog.js';
|
|
4
4
|
import { getWorkflowsBasePath } from '#utils/paths.js';
|
|
5
5
|
const SCENARIOS_DIR = 'scenarios';
|
|
6
6
|
const WORKFLOWS_PATHS = ['src/workflows', 'workflows'];
|
|
@@ -30,17 +30,9 @@ export function findWorkflowDirectoryFromPath(workflowPath, basePath = getWorkfl
|
|
|
30
30
|
}
|
|
31
31
|
async function fetchWorkflowPath(workflowName) {
|
|
32
32
|
try {
|
|
33
|
-
const
|
|
34
|
-
const data = response?.data;
|
|
35
|
-
const workflows = data?.workflows;
|
|
36
|
-
if (!workflows) {
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
33
|
+
const workflows = await fetchWorkflowCatalog();
|
|
39
34
|
const workflow = workflows.find(w => w.name === workflowName);
|
|
40
|
-
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
43
|
-
return workflow.path ?? null;
|
|
35
|
+
return workflow?.path ?? null;
|
|
44
36
|
}
|
|
45
37
|
catch {
|
|
46
38
|
return null;
|
|
@@ -1,23 +1,19 @@
|
|
|
1
1
|
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
|
|
2
2
|
import { resolveScenarioPath, getScenarioNotFoundMessage, extractWorkflowRelativePath, findWorkflowDirectoryFromPath, listScenariosForWorkflow } from './scenario_resolver.js';
|
|
3
3
|
import * as fs from 'node:fs';
|
|
4
|
-
import * as
|
|
4
|
+
import * as catalog from '#api/workflow_catalog.js';
|
|
5
5
|
vi.mock('node:fs', () => ({
|
|
6
6
|
existsSync: vi.fn(),
|
|
7
7
|
readdirSync: vi.fn()
|
|
8
8
|
}));
|
|
9
|
-
vi.mock('#api/
|
|
10
|
-
|
|
9
|
+
vi.mock('#api/workflow_catalog.js', () => ({
|
|
10
|
+
fetchWorkflowCatalog: vi.fn()
|
|
11
11
|
}));
|
|
12
12
|
function mockCatalog(workflows) {
|
|
13
|
-
vi.mocked(
|
|
14
|
-
data: { workflows },
|
|
15
|
-
status: 200,
|
|
16
|
-
headers: new Headers()
|
|
17
|
-
});
|
|
13
|
+
vi.mocked(catalog.fetchWorkflowCatalog).mockResolvedValue(workflows);
|
|
18
14
|
}
|
|
19
15
|
function mockCatalogFailure() {
|
|
20
|
-
vi.mocked(
|
|
16
|
+
vi.mocked(catalog.fetchWorkflowCatalog).mockRejectedValue(new Error('API unavailable'));
|
|
21
17
|
}
|
|
22
18
|
describe('extractWorkflowRelativePath', () => {
|
|
23
19
|
it('should extract relative path from workflow.js path', () => {
|
|
@@ -7,7 +7,7 @@ const WORKFLOW_STATUS_MAP = {
|
|
|
7
7
|
canceled: { icon: '○', color: 'gray' },
|
|
8
8
|
terminated: { icon: '✗', color: 'gray' },
|
|
9
9
|
timed_out: { icon: '✗', color: 'red' },
|
|
10
|
-
|
|
10
|
+
continued_as_new: { icon: '↻', color: 'blue' }
|
|
11
11
|
};
|
|
12
12
|
const DEFAULT_DISPLAY = { icon: '?', color: 'white' };
|
|
13
13
|
export const resolveWorkflowStatus = (status) => WORKFLOW_STATUS_MAP[status] ?? DEFAULT_DISPLAY;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from 'react';
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { getWorkflowIdResult, getWorkflowIdRunsRidResult, getWorkflowIdTraceLog, getWorkflowIdRunsRidTraceLog } from '#api/generated/api.js';
|
|
4
|
+
import { normalizeWorkflowStatus } from '#utils/normalize_workflow_status.js';
|
|
4
5
|
const EMPTY_DETAIL = {
|
|
5
6
|
result: null,
|
|
6
7
|
trace: null,
|
|
@@ -88,7 +89,11 @@ const fetchResult = async (workflowId, runId) => {
|
|
|
88
89
|
const response = runId ?
|
|
89
90
|
await getWorkflowIdRunsRidResult(workflowId, runId) :
|
|
90
91
|
await getWorkflowIdResult(workflowId);
|
|
91
|
-
|
|
92
|
+
const data = response.data;
|
|
93
|
+
return {
|
|
94
|
+
...data,
|
|
95
|
+
status: normalizeWorkflowStatus(data.status)
|
|
96
|
+
};
|
|
92
97
|
}
|
|
93
98
|
catch {
|
|
94
99
|
return null;
|
|
@@ -10,7 +10,7 @@ describe('isTerminalRunStatus', () => {
|
|
|
10
10
|
});
|
|
11
11
|
it('returns false for in-progress states', () => {
|
|
12
12
|
expect(isTerminalRunStatus('running')).toBe(false);
|
|
13
|
-
expect(isTerminalRunStatus('
|
|
13
|
+
expect(isTerminalRunStatus('continued_as_new')).toBe(false);
|
|
14
14
|
});
|
|
15
15
|
it('returns false for nullish input', () => {
|
|
16
16
|
expect(isTerminalRunStatus(null)).toBe(false);
|
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { fetchWorkflowCatalog } from '#api/workflow_catalog.js';
|
|
3
3
|
import { usePoll } from '#views/dev/hooks/use_poll.js';
|
|
4
4
|
const CATALOG_INTERVAL_MS = 10_000;
|
|
5
5
|
export const useWorkflowCatalog = (enabled) => {
|
|
6
6
|
const [workflows, setWorkflows] = useState([]);
|
|
7
7
|
usePoll(enabled, CATALOG_INTERVAL_MS, async () => {
|
|
8
8
|
try {
|
|
9
|
-
|
|
10
|
-
const data = response?.data;
|
|
11
|
-
if (data?.workflows) {
|
|
12
|
-
setWorkflows(data.workflows);
|
|
13
|
-
}
|
|
9
|
+
setWorkflows(await fetchWorkflowCatalog());
|
|
14
10
|
}
|
|
15
11
|
catch {
|
|
16
12
|
// API may not be ready yet
|
|
@@ -22,7 +22,7 @@ const STATUS_ORDER = {
|
|
|
22
22
|
timed_out: 2,
|
|
23
23
|
terminated: 3,
|
|
24
24
|
canceled: 4,
|
|
25
|
-
|
|
25
|
+
continued_as_new: 5,
|
|
26
26
|
completed: 6
|
|
27
27
|
};
|
|
28
28
|
const sortRuns = (runs) => [...runs].sort((a, b) => {
|
|
@@ -60,10 +60,9 @@ const COL = {
|
|
|
60
60
|
const RUN_INFO_TABS = [
|
|
61
61
|
{ id: 'status', label: 'Status' },
|
|
62
62
|
{ id: 'input', label: 'Input' },
|
|
63
|
-
{ id: 'output', label: 'Output' }
|
|
64
|
-
{ id: 'aggregations', label: 'Aggregations' }
|
|
63
|
+
{ id: 'output', label: 'Output' }
|
|
65
64
|
];
|
|
66
|
-
const RUN_INFO_TAB_ORDER = ['status', 'input', 'output'
|
|
65
|
+
const RUN_INFO_TAB_ORDER = ['status', 'input', 'output'];
|
|
67
66
|
const HeaderRow = () => (_jsxs(Box, { children: [_jsx(Box, { width: COL.indicator, children: _jsx(Text, { children: "\u00A0" }) }), _jsx(Box, { width: COL.icon, children: _jsx(Text, { children: "\u00A0" }) }), _jsx(Box, { width: COL.status, children: _jsx(Text, { dimColor: true, bold: true, children: "STATUS" }) }), _jsx(Box, { width: COL.type, children: _jsx(Text, { dimColor: true, bold: true, children: "TYPE" }) }), _jsx(Box, { width: COL.id, children: _jsx(Text, { dimColor: true, bold: true, children: "ID" }) }), _jsx(Box, { width: COL.duration, justifyContent: "flex-end", children: _jsx(Text, { dimColor: true, bold: true, children: "DURATION" }) }), _jsx(Box, { width: COL.started, marginLeft: 2, children: _jsx(Text, { dimColor: true, bold: true, children: "STARTED" }) })] }));
|
|
68
67
|
const RunRow = ({ run, selected }) => {
|
|
69
68
|
const status = run.status ?? 'running';
|
|
@@ -86,10 +85,8 @@ const runPaneValue = (run, pane, activePane) => {
|
|
|
86
85
|
if (activePane === 'input') {
|
|
87
86
|
return pane.input;
|
|
88
87
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
return pane.aggregations;
|
|
88
|
+
// 'output'
|
|
89
|
+
return pane.error ?? pane.output;
|
|
93
90
|
};
|
|
94
91
|
const DetailPane = ({ run, pane, rows }) => {
|
|
95
92
|
const ui = useUiState();
|
|
@@ -148,7 +145,6 @@ export const RunsPanel = ({ runs, height }) => {
|
|
|
148
145
|
input: result?.input,
|
|
149
146
|
output: result?.output,
|
|
150
147
|
error: result?.error,
|
|
151
|
-
aggregations: result?.aggregations,
|
|
152
148
|
status: result?.status ?? selectedRun.status ?? 'unknown',
|
|
153
149
|
loading
|
|
154
150
|
} : null;
|
|
@@ -2,7 +2,7 @@ import React from 'react';
|
|
|
2
2
|
export type Tab = 'workflows' | 'runs' | 'services' | 'help';
|
|
3
3
|
export declare const TAB_ORDER: Tab[];
|
|
4
4
|
export declare const TAB_LABELS: Record<Tab, string>;
|
|
5
|
-
export type RunListPaneTab = 'status' | 'input' | 'output'
|
|
5
|
+
export type RunListPaneTab = 'status' | 'input' | 'output';
|
|
6
6
|
export type RunStepPaneTab = 'input' | 'output' | 'meta';
|
|
7
7
|
export type RunsView = 'list' | 'detail';
|
|
8
8
|
export interface Selection {
|
package/oclif.manifest.json
CHANGED
|
@@ -704,9 +704,26 @@
|
|
|
704
704
|
"<%= config.bin %> <%= command.id %> --format table",
|
|
705
705
|
"<%= config.bin %> <%= command.id %> --format json",
|
|
706
706
|
"<%= config.bin %> <%= command.id %> --detailed",
|
|
707
|
-
"<%= config.bin %> <%= command.id %> --filter simple"
|
|
707
|
+
"<%= config.bin %> <%= command.id %> --filter simple",
|
|
708
|
+
"<%= config.bin %> <%= command.id %> --catalog my-catalog"
|
|
708
709
|
],
|
|
709
710
|
"flags": {
|
|
711
|
+
"catalog": {
|
|
712
|
+
"aliases": [
|
|
713
|
+
"task-queue"
|
|
714
|
+
],
|
|
715
|
+
"char": "c",
|
|
716
|
+
"charAliases": [
|
|
717
|
+
"q"
|
|
718
|
+
],
|
|
719
|
+
"deprecateAliases": true,
|
|
720
|
+
"description": "Catalog to list workflows from (defaults to OUTPUT_CATALOG_ID)",
|
|
721
|
+
"env": "OUTPUT_CATALOG_ID",
|
|
722
|
+
"name": "catalog",
|
|
723
|
+
"hasDynamicHelp": false,
|
|
724
|
+
"multiple": false,
|
|
725
|
+
"type": "option"
|
|
726
|
+
},
|
|
710
727
|
"format": {
|
|
711
728
|
"char": "f",
|
|
712
729
|
"description": "Output format",
|
|
@@ -1424,5 +1441,5 @@
|
|
|
1424
1441
|
]
|
|
1425
1442
|
}
|
|
1426
1443
|
},
|
|
1427
|
-
"version": "0.7.1-next.
|
|
1444
|
+
"version": "0.7.1-next.c005dac.0"
|
|
1428
1445
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@outputai/cli",
|
|
3
|
-
"version": "0.7.1-next.
|
|
3
|
+
"version": "0.7.1-next.c005dac.0",
|
|
4
4
|
"description": "CLI for Output.ai workflow generation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
"semver": "7.7.4",
|
|
37
37
|
"undici": "8.1.0",
|
|
38
38
|
"yaml": "^2.8.3",
|
|
39
|
-
"@outputai/
|
|
40
|
-
"@outputai/
|
|
41
|
-
"@outputai/llm": "0.7.1-next.
|
|
39
|
+
"@outputai/evals": "0.7.1-next.c005dac.0",
|
|
40
|
+
"@outputai/credentials": "0.7.1-next.c005dac.0",
|
|
41
|
+
"@outputai/llm": "0.7.1-next.c005dac.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@types/cli-progress": "3.11.6",
|