@outputai/cli 0.1.13-next.c93cdb4.0 → 0.1.13-next.d19646c.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 +321 -83
- package/dist/api/generated/api.js +66 -8
- package/dist/assets/docker/docker-compose-dev.yml +1 -1
- package/dist/commands/workflow/generate.js +3 -1
- package/dist/commands/workflow/generate.spec.js +12 -0
- package/dist/commands/workflow/list.d.ts +1 -0
- package/dist/commands/workflow/list.js +12 -6
- package/dist/commands/workflow/list.spec.js +21 -0
- package/dist/components/command_footer.d.ts +8 -0
- package/dist/components/command_footer.js +4 -0
- package/dist/components/status_icon.d.ts +11 -0
- package/dist/components/status_icon.js +25 -0
- package/dist/components/workflow_summary.d.ts +10 -0
- package/dist/components/workflow_summary.js +4 -0
- package/dist/generated/framework_version.json +1 -1
- package/dist/services/claude_client.js +4 -1
- package/dist/services/docker.d.ts +1 -3
- package/dist/services/docker.js +38 -13
- package/dist/services/messages.d.ts +1 -1
- package/dist/services/messages.js +2 -2
- package/dist/templates/agent_instructions/CLAUDE.md.template +36 -2
- package/dist/templates/agent_instructions/dotclaude/settings.json.template +2 -2
- package/dist/utils/date_formatter.d.ts +11 -1
- package/dist/utils/date_formatter.js +26 -1
- package/dist/utils/format_workflow_result.d.ts +3 -3
- package/dist/utils/open_url.d.ts +1 -0
- package/dist/utils/open_url.js +12 -0
- package/dist/utils/workflow_dir_parser.d.ts +5 -0
- package/dist/utils/workflow_dir_parser.js +39 -0
- package/dist/utils/workflow_dir_parser.spec.d.ts +1 -0
- package/dist/utils/workflow_dir_parser.spec.js +74 -0
- package/dist/views/dev.js +62 -26
- package/dist/views/workflow/list.d.ts +6 -0
- package/dist/views/workflow/list.js +127 -0
- package/package.json +11 -11
|
@@ -24,11 +24,7 @@ export const WorkflowRunInfoStatus = {
|
|
|
24
24
|
timed_out: 'timed_out',
|
|
25
25
|
continued: 'continued',
|
|
26
26
|
};
|
|
27
|
-
export const
|
|
28
|
-
completed: 'completed',
|
|
29
|
-
failed: 'failed',
|
|
30
|
-
};
|
|
31
|
-
export const GetWorkflowIdStatus200Status = {
|
|
27
|
+
export const WorkflowStatusResponseStatus = {
|
|
32
28
|
canceled: 'canceled',
|
|
33
29
|
completed: 'completed',
|
|
34
30
|
continued_as_new: 'continued_as_new',
|
|
@@ -38,7 +34,7 @@ export const GetWorkflowIdStatus200Status = {
|
|
|
38
34
|
timed_out: 'timed_out',
|
|
39
35
|
unspecified: 'unspecified',
|
|
40
36
|
};
|
|
41
|
-
export const
|
|
37
|
+
export const WorkflowResultResponseStatus = {
|
|
42
38
|
completed: 'completed',
|
|
43
39
|
failed: 'failed',
|
|
44
40
|
canceled: 'canceled',
|
|
@@ -46,6 +42,10 @@ export const GetWorkflowIdResult200Status = {
|
|
|
46
42
|
timed_out: 'timed_out',
|
|
47
43
|
continued: 'continued',
|
|
48
44
|
};
|
|
45
|
+
export const PostWorkflowRun200Status = {
|
|
46
|
+
completed: 'completed',
|
|
47
|
+
failed: 'failed',
|
|
48
|
+
};
|
|
49
49
|
;
|
|
50
50
|
export const getGetHealthUrl = () => {
|
|
51
51
|
return `/health`;
|
|
@@ -87,6 +87,24 @@ export const getWorkflowIdStatus = async (id, options) => {
|
|
|
87
87
|
method: 'GET'
|
|
88
88
|
});
|
|
89
89
|
};
|
|
90
|
+
export const getGetWorkflowIdRunsRidStatusUrl = (id, rid) => {
|
|
91
|
+
return `/workflow/${id}/runs/${rid}/status`;
|
|
92
|
+
};
|
|
93
|
+
export const getWorkflowIdRunsRidStatus = async (id, rid, options) => {
|
|
94
|
+
return customFetchInstance(getGetWorkflowIdRunsRidStatusUrl(id, rid), {
|
|
95
|
+
...options,
|
|
96
|
+
method: 'GET'
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
export const getPatchWorkflowIdRunsRidStopUrl = (id, rid) => {
|
|
100
|
+
return `/workflow/${id}/runs/${rid}/stop`;
|
|
101
|
+
};
|
|
102
|
+
export const patchWorkflowIdRunsRidStop = async (id, rid, options) => {
|
|
103
|
+
return customFetchInstance(getPatchWorkflowIdRunsRidStopUrl(id, rid), {
|
|
104
|
+
...options,
|
|
105
|
+
method: 'PATCH'
|
|
106
|
+
});
|
|
107
|
+
};
|
|
90
108
|
export const getPatchWorkflowIdStopUrl = (id) => {
|
|
91
109
|
return `/workflow/${id}/stop`;
|
|
92
110
|
};
|
|
@@ -96,6 +114,17 @@ export const patchWorkflowIdStop = async (id, options) => {
|
|
|
96
114
|
method: 'PATCH'
|
|
97
115
|
});
|
|
98
116
|
};
|
|
117
|
+
export const getPostWorkflowIdRunsRidTerminateUrl = (id, rid) => {
|
|
118
|
+
return `/workflow/${id}/runs/${rid}/terminate`;
|
|
119
|
+
};
|
|
120
|
+
export const postWorkflowIdRunsRidTerminate = async (id, rid, postWorkflowIdRunsRidTerminateBody, options) => {
|
|
121
|
+
return customFetchInstance(getPostWorkflowIdRunsRidTerminateUrl(id, rid), {
|
|
122
|
+
...options,
|
|
123
|
+
method: 'POST',
|
|
124
|
+
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
|
125
|
+
body: JSON.stringify(postWorkflowIdRunsRidTerminateBody)
|
|
126
|
+
});
|
|
127
|
+
};
|
|
99
128
|
export const getPostWorkflowIdTerminateUrl = (id) => {
|
|
100
129
|
return `/workflow/${id}/terminate`;
|
|
101
130
|
};
|
|
@@ -107,15 +136,26 @@ export const postWorkflowIdTerminate = async (id, postWorkflowIdTerminateBody, o
|
|
|
107
136
|
body: JSON.stringify(postWorkflowIdTerminateBody)
|
|
108
137
|
});
|
|
109
138
|
};
|
|
139
|
+
export const getPostWorkflowIdRunsRidResetUrl = (id, rid) => {
|
|
140
|
+
return `/workflow/${id}/runs/${rid}/reset`;
|
|
141
|
+
};
|
|
142
|
+
export const postWorkflowIdRunsRidReset = async (id, rid, resetWorkflowRequest, options) => {
|
|
143
|
+
return customFetchInstance(getPostWorkflowIdRunsRidResetUrl(id, rid), {
|
|
144
|
+
...options,
|
|
145
|
+
method: 'POST',
|
|
146
|
+
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
|
147
|
+
body: JSON.stringify(resetWorkflowRequest)
|
|
148
|
+
});
|
|
149
|
+
};
|
|
110
150
|
export const getPostWorkflowIdResetUrl = (id) => {
|
|
111
151
|
return `/workflow/${id}/reset`;
|
|
112
152
|
};
|
|
113
|
-
export const postWorkflowIdReset = async (id,
|
|
153
|
+
export const postWorkflowIdReset = async (id, resetWorkflowRequest, options) => {
|
|
114
154
|
return customFetchInstance(getPostWorkflowIdResetUrl(id), {
|
|
115
155
|
...options,
|
|
116
156
|
method: 'POST',
|
|
117
157
|
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
|
118
|
-
body: JSON.stringify(
|
|
158
|
+
body: JSON.stringify(resetWorkflowRequest)
|
|
119
159
|
});
|
|
120
160
|
};
|
|
121
161
|
export const getGetWorkflowIdResultUrl = (id) => {
|
|
@@ -127,6 +167,15 @@ export const getWorkflowIdResult = async (id, options) => {
|
|
|
127
167
|
method: 'GET'
|
|
128
168
|
});
|
|
129
169
|
};
|
|
170
|
+
export const getGetWorkflowIdRunsRidResultUrl = (id, rid) => {
|
|
171
|
+
return `/workflow/${id}/runs/${rid}/result`;
|
|
172
|
+
};
|
|
173
|
+
export const getWorkflowIdRunsRidResult = async (id, rid, options) => {
|
|
174
|
+
return customFetchInstance(getGetWorkflowIdRunsRidResultUrl(id, rid), {
|
|
175
|
+
...options,
|
|
176
|
+
method: 'GET'
|
|
177
|
+
});
|
|
178
|
+
};
|
|
130
179
|
export const getGetWorkflowIdTraceLogUrl = (id) => {
|
|
131
180
|
return `/workflow/${id}/trace-log`;
|
|
132
181
|
};
|
|
@@ -136,6 +185,15 @@ export const getWorkflowIdTraceLog = async (id, options) => {
|
|
|
136
185
|
method: 'GET'
|
|
137
186
|
});
|
|
138
187
|
};
|
|
188
|
+
export const getGetWorkflowIdRunsRidTraceLogUrl = (id, rid) => {
|
|
189
|
+
return `/workflow/${id}/runs/${rid}/trace-log`;
|
|
190
|
+
};
|
|
191
|
+
export const getWorkflowIdRunsRidTraceLog = async (id, rid, options) => {
|
|
192
|
+
return customFetchInstance(getGetWorkflowIdRunsRidTraceLogUrl(id, rid), {
|
|
193
|
+
...options,
|
|
194
|
+
method: 'GET'
|
|
195
|
+
});
|
|
196
|
+
};
|
|
139
197
|
export const getGetWorkflowCatalogIdUrl = (id) => {
|
|
140
198
|
return `/workflow/catalog/${id}`;
|
|
141
199
|
};
|
|
@@ -4,6 +4,7 @@ import { buildWorkflow, buildWorkflowInteractiveLoop } from '#services/workflow_
|
|
|
4
4
|
import { ensureOutputAISystem } from '#services/coding_agents.js';
|
|
5
5
|
import { getWorkflowGenerateSuccessMessage } from '#services/messages.js';
|
|
6
6
|
import { DEFAULT_OUTPUT_DIRS } from '#utils/paths.js';
|
|
7
|
+
import { parseWorkflowDir } from '#utils/workflow_dir_parser.js';
|
|
7
8
|
import path from 'node:path';
|
|
8
9
|
import * as fsSync from 'node:fs';
|
|
9
10
|
import { getErrorMessage } from '#utils/error_utils.js';
|
|
@@ -83,7 +84,8 @@ export default class Generate extends Command {
|
|
|
83
84
|
this.displaySuccess(result);
|
|
84
85
|
}
|
|
85
86
|
displaySuccess(result) {
|
|
86
|
-
const
|
|
87
|
+
const dirInfo = parseWorkflowDir(result.targetDir);
|
|
88
|
+
const message = getWorkflowGenerateSuccessMessage(result.workflowName, dirInfo.workflowId ?? result.workflowName, dirInfo.scenarioNames[0], result.targetDir, result.filesCreated);
|
|
87
89
|
this.log(message);
|
|
88
90
|
}
|
|
89
91
|
}
|
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
3
3
|
import Generate from './generate.js';
|
|
4
4
|
import { generateWorkflow } from '#services/workflow_generator.js';
|
|
5
|
+
import { parseWorkflowDir } from '#utils/workflow_dir_parser.js';
|
|
5
6
|
import { InvalidNameError, WorkflowExistsError } from '#types/errors.js';
|
|
6
7
|
vi.mock('../../services/workflow_generator.js');
|
|
8
|
+
vi.mock('../../utils/workflow_dir_parser.js');
|
|
7
9
|
describe('Generate Command', () => {
|
|
8
10
|
let mockGenerateWorkflow;
|
|
11
|
+
let mockParseWorkflowDir;
|
|
9
12
|
let logSpy;
|
|
10
13
|
const createCommand = () => {
|
|
11
14
|
const cmd = new Generate([], {});
|
|
@@ -20,6 +23,7 @@ describe('Generate Command', () => {
|
|
|
20
23
|
beforeEach(() => {
|
|
21
24
|
vi.clearAllMocks();
|
|
22
25
|
mockGenerateWorkflow = vi.mocked(generateWorkflow);
|
|
26
|
+
mockParseWorkflowDir = vi.mocked(parseWorkflowDir);
|
|
23
27
|
});
|
|
24
28
|
describe('successful workflow generation', () => {
|
|
25
29
|
it('should generate workflow with skeleton flag', async () => {
|
|
@@ -38,6 +42,10 @@ describe('Generate Command', () => {
|
|
|
38
42
|
targetDir: '/tmp/test-workflow',
|
|
39
43
|
filesCreated: ['index.ts', 'steps.ts', 'types.ts']
|
|
40
44
|
});
|
|
45
|
+
mockParseWorkflowDir.mockReturnValue({
|
|
46
|
+
workflowId: 'testWorkflow',
|
|
47
|
+
scenarioNames: ['test_input']
|
|
48
|
+
});
|
|
41
49
|
await cmd.run();
|
|
42
50
|
expect(mockGenerateWorkflow).toHaveBeenCalledWith({
|
|
43
51
|
name: 'test-workflow',
|
|
@@ -105,6 +113,10 @@ describe('Generate Command', () => {
|
|
|
105
113
|
targetDir: '/custom/path/my-workflow',
|
|
106
114
|
filesCreated: ['index.ts', 'steps.ts', 'types.ts']
|
|
107
115
|
});
|
|
116
|
+
mockParseWorkflowDir.mockReturnValue({
|
|
117
|
+
workflowId: 'myWorkflow',
|
|
118
|
+
scenarioNames: ['test_input']
|
|
119
|
+
});
|
|
108
120
|
await cmd.run();
|
|
109
121
|
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('SUCCESS!'));
|
|
110
122
|
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('my-workflow'));
|
|
@@ -17,7 +17,8 @@ export function parseWorkflowForDisplay(workflow) {
|
|
|
17
17
|
description: parsed.description || 'No description',
|
|
18
18
|
inputs: formatParameters(parsed.inputs),
|
|
19
19
|
outputs: formatParameters(parsed.outputs),
|
|
20
|
-
scenarios: scenarioNames.length > 0 ? scenarioNames.join(', ') : 'none'
|
|
20
|
+
scenarios: scenarioNames.length > 0 ? scenarioNames.join(', ') : 'none',
|
|
21
|
+
aliases: workflow.aliases?.length ? workflow.aliases.join(', ') : 'none'
|
|
21
22
|
};
|
|
22
23
|
}
|
|
23
24
|
function caseInsensitiveIncludes(str, filter) {
|
|
@@ -26,7 +27,10 @@ function caseInsensitiveIncludes(str, filter) {
|
|
|
26
27
|
function matchName(filterString) {
|
|
27
28
|
return workflow => {
|
|
28
29
|
const name = workflow.name || '';
|
|
29
|
-
|
|
30
|
+
if (caseInsensitiveIncludes(name, filterString)) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
return (workflow.aliases ?? []).some(alias => caseInsensitiveIncludes(alias, filterString));
|
|
30
34
|
};
|
|
31
35
|
}
|
|
32
36
|
function sortWorkflowsByName(workflows) {
|
|
@@ -38,8 +42,8 @@ function sortWorkflowsByName(workflows) {
|
|
|
38
42
|
}
|
|
39
43
|
function createWorkflowTable(workflows, detailed) {
|
|
40
44
|
const table = new Table({
|
|
41
|
-
head: ['Name', 'Description', 'Inputs', 'Outputs', 'Scenarios'],
|
|
42
|
-
colWidths: detailed ? [
|
|
45
|
+
head: ['Name', 'Description', 'Aliases', 'Inputs', 'Outputs', 'Scenarios'],
|
|
46
|
+
colWidths: detailed ? [28, 36, 36, 36, 36, 48] : [22, 26, 26, 22, 22, 36],
|
|
43
47
|
wordWrap: true,
|
|
44
48
|
style: {
|
|
45
49
|
head: ['cyan']
|
|
@@ -49,13 +53,14 @@ function createWorkflowTable(workflows, detailed) {
|
|
|
49
53
|
sortedWorkflows.forEach(workflow => {
|
|
50
54
|
const display = parseWorkflowForDisplay(workflow);
|
|
51
55
|
if (detailed) {
|
|
56
|
+
const aliases = workflow.aliases?.length ? workflow.aliases.join('\n') : 'none';
|
|
52
57
|
const inputs = display.inputs.split(', ').join('\n');
|
|
53
58
|
const outputs = display.outputs.split(', ').join('\n');
|
|
54
59
|
const scenarios = display.scenarios.split(', ').join('\n');
|
|
55
|
-
table.push([display.name, display.description, inputs, outputs, scenarios]);
|
|
60
|
+
table.push([display.name, display.description, aliases, inputs, outputs, scenarios]);
|
|
56
61
|
}
|
|
57
62
|
else {
|
|
58
|
-
table.push([display.name, display.description, display.inputs, display.outputs, display.scenarios]);
|
|
63
|
+
table.push([display.name, display.description, display.aliases, display.inputs, display.outputs, display.scenarios]);
|
|
59
64
|
}
|
|
60
65
|
});
|
|
61
66
|
return table.toString();
|
|
@@ -72,6 +77,7 @@ function formatWorkflowsAsJson(workflows) {
|
|
|
72
77
|
return {
|
|
73
78
|
name: display.name,
|
|
74
79
|
description: display.description,
|
|
80
|
+
aliases: w.aliases ?? [],
|
|
75
81
|
inputs: display.inputs.split(', '),
|
|
76
82
|
outputs: display.outputs.split(', '),
|
|
77
83
|
scenarios: display.scenarios === 'none' ? [] : display.scenarios.split(', '),
|
|
@@ -64,6 +64,27 @@ describe('workflow list parsing', () => {
|
|
|
64
64
|
expect(parsed.inputs).toBe('none');
|
|
65
65
|
expect(parsed.outputs).toBe('none');
|
|
66
66
|
expect(parsed.scenarios).toBe('none');
|
|
67
|
+
expect(parsed.aliases).toBe('none');
|
|
68
|
+
});
|
|
69
|
+
it('should include aliases when present', async () => {
|
|
70
|
+
const { parseWorkflowForDisplay } = await import('./list.js');
|
|
71
|
+
const mockWorkflow = {
|
|
72
|
+
name: 'aliased-workflow',
|
|
73
|
+
description: 'Has aliases',
|
|
74
|
+
aliases: ['old_name', 'legacy_name']
|
|
75
|
+
};
|
|
76
|
+
const parsed = parseWorkflowForDisplay(mockWorkflow);
|
|
77
|
+
expect(parsed.aliases).toBe('old_name, legacy_name');
|
|
78
|
+
});
|
|
79
|
+
it('should show none when aliases array is empty', async () => {
|
|
80
|
+
const { parseWorkflowForDisplay } = await import('./list.js');
|
|
81
|
+
const mockWorkflow = {
|
|
82
|
+
name: 'no-aliases',
|
|
83
|
+
description: 'Empty aliases',
|
|
84
|
+
aliases: []
|
|
85
|
+
};
|
|
86
|
+
const parsed = parseWorkflowForDisplay(mockWorkflow);
|
|
87
|
+
expect(parsed.aliases).toBe('none');
|
|
67
88
|
});
|
|
68
89
|
it('should include scenario names when scenarios exist', async () => {
|
|
69
90
|
mockListScenarios.mockReturnValueOnce(['basic', 'advanced', 'stress_test']);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
export const CommandFooter = ({ hints }) => (_jsx(Box, { marginTop: 1, children: hints.map((hint, i) => (_jsxs(React.Fragment, { children: [i > 0 && _jsx(Text, { dimColor: true, children: ' | ' }), _jsx(Text, { dimColor: true, children: '(' }), _jsx(Text, { dimColor: true, bold: true, children: hint.key }), _jsx(Text, { dimColor: true, children: ')' }), _jsx(Text, { dimColor: true, children: ` ${hint.label}` })] }, hint.key))) }));
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface StatusDisplay {
|
|
3
|
+
icon: string;
|
|
4
|
+
color: string;
|
|
5
|
+
}
|
|
6
|
+
export declare const resolveStatus: (status: string) => StatusDisplay;
|
|
7
|
+
export declare const statusColor: (status: string) => string;
|
|
8
|
+
export declare const StatusIcon: React.FC<{
|
|
9
|
+
status: string;
|
|
10
|
+
}>;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Text } from 'ink';
|
|
3
|
+
const STATUS_MAP = {
|
|
4
|
+
// Docker service health
|
|
5
|
+
healthy: { icon: '●', color: 'green' },
|
|
6
|
+
unhealthy: { icon: '○', color: 'red' },
|
|
7
|
+
starting: { icon: '◐', color: 'yellow' },
|
|
8
|
+
none: { icon: '●', color: 'blue' },
|
|
9
|
+
exited: { icon: '✗', color: 'red' },
|
|
10
|
+
// Workflow run status
|
|
11
|
+
running: { icon: '●', color: 'blue' },
|
|
12
|
+
completed: { icon: '●', color: 'green' },
|
|
13
|
+
failed: { icon: '✗', color: 'red' },
|
|
14
|
+
canceled: { icon: '○', color: 'gray' },
|
|
15
|
+
terminated: { icon: '✗', color: 'red' },
|
|
16
|
+
timed_out: { icon: '✗', color: 'red' },
|
|
17
|
+
continued: { icon: '↻', color: 'blue' }
|
|
18
|
+
};
|
|
19
|
+
const DEFAULT_DISPLAY = { icon: '?', color: 'white' };
|
|
20
|
+
export const resolveStatus = (status) => STATUS_MAP[status] ?? DEFAULT_DISPLAY;
|
|
21
|
+
export const statusColor = (status) => resolveStatus(status).color;
|
|
22
|
+
export const StatusIcon = ({ status }) => {
|
|
23
|
+
const { icon, color } = resolveStatus(status);
|
|
24
|
+
return _jsx(Text, { color: color, children: icon });
|
|
25
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { statusColor } from '#components/status_icon.js';
|
|
4
|
+
export const WorkflowSummarySection = ({ summary }) => (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, children: "\uD83D\uDCCB Workflows" }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: statusColor('running'), children: [summary.running, " running"] }), _jsx(Text, { children: ", " }), _jsxs(Text, { color: statusColor('failed'), children: [summary.failed, " failed"] }), _jsx(Text, { children: ", " }), _jsxs(Text, { color: statusColor('completed'), children: [summary.completed, " complete"] })] })] }));
|
|
@@ -105,7 +105,10 @@ function getTodoWriteMessage(message) {
|
|
|
105
105
|
if (message.type !== 'assistant') {
|
|
106
106
|
return null;
|
|
107
107
|
}
|
|
108
|
-
const todoWriteMessage = message.message.content.find((c) =>
|
|
108
|
+
const todoWriteMessage = message.message.content.find((c) => {
|
|
109
|
+
const block = c;
|
|
110
|
+
return block.type === 'tool_use' && block.name === 'TodoWrite';
|
|
111
|
+
});
|
|
109
112
|
return todoWriteMessage ?? null;
|
|
110
113
|
}
|
|
111
114
|
function applyInstructions(message, instructions) {
|
|
@@ -21,8 +21,6 @@ export declare class DockerComposeConfigNotFoundError extends Error {
|
|
|
21
21
|
constructor(dockerComposePath: string);
|
|
22
22
|
}
|
|
23
23
|
declare const isDockerInstalled: () => boolean;
|
|
24
|
-
declare const isDockerComposeAvailable: () => boolean;
|
|
25
|
-
declare const isDockerDaemonRunning: () => boolean;
|
|
26
24
|
export declare function validateDockerEnvironment(): void;
|
|
27
25
|
export declare function getDefaultDockerComposePath(): string;
|
|
28
26
|
export declare function parseServiceStatus(jsonOutput: string): ServiceStatus[];
|
|
@@ -37,4 +35,4 @@ export type PullPolicy = 'always' | 'missing' | 'never';
|
|
|
37
35
|
export declare function startDockerCompose(dockerComposePath: string, pullPolicy?: PullPolicy): Promise<DockerComposeProcess>;
|
|
38
36
|
export declare function startDockerComposeDetached(dockerComposePath: string, pullPolicy?: PullPolicy): void;
|
|
39
37
|
export declare function stopDockerCompose(dockerComposePath: string): Promise<void>;
|
|
40
|
-
export { isDockerInstalled,
|
|
38
|
+
export { isDockerInstalled, DockerValidationError };
|
package/dist/services/docker.js
CHANGED
|
@@ -2,6 +2,7 @@ import { execFileSync, execSync, spawn } from 'node:child_process';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { ux } from '@oclif/core';
|
|
5
|
+
import semver from 'semver';
|
|
5
6
|
const DEFAULT_COMPOSE_PATH = '../assets/docker/docker-compose-dev.yml';
|
|
6
7
|
export const SERVICE_HEALTH = {
|
|
7
8
|
HEALTHY: 'healthy',
|
|
@@ -30,27 +31,51 @@ const checkDockerCommand = (command) => {
|
|
|
30
31
|
return false;
|
|
31
32
|
}
|
|
32
33
|
};
|
|
34
|
+
const getCommandVersion = (command, pattern = /(\d+\.\d+\.\d+)/) => {
|
|
35
|
+
try {
|
|
36
|
+
const output = execSync(command, { stdio: 'pipe', encoding: 'utf-8' }).trim();
|
|
37
|
+
const match = output.match(pattern);
|
|
38
|
+
return match ? match[1] : null;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
33
44
|
const isDockerInstalled = () => checkDockerCommand('docker --version');
|
|
34
|
-
const
|
|
35
|
-
const isDockerDaemonRunning = () => checkDockerCommand('docker ps');
|
|
36
|
-
const DOCKER_VALIDATIONS = [
|
|
45
|
+
const PREREQUISITES = [
|
|
37
46
|
{
|
|
38
|
-
|
|
39
|
-
|
|
47
|
+
name: 'Docker',
|
|
48
|
+
semverRange: '>=20.0.0',
|
|
49
|
+
getVersion: () => getCommandVersion('docker --version'),
|
|
50
|
+
errorMessage: (current, required) => current === null ?
|
|
51
|
+
'Docker is not installed. Please install Docker to use the dev command.\nVisit: https://docs.docker.com/get-docker/' :
|
|
52
|
+
`Docker version ${required} is required (found v${current}).\nVisit: https://docs.docker.com/get-docker/`
|
|
40
53
|
},
|
|
41
54
|
{
|
|
42
|
-
|
|
43
|
-
|
|
55
|
+
name: 'Docker Compose',
|
|
56
|
+
semverRange: '>=2.24.0',
|
|
57
|
+
getVersion: () => getCommandVersion('docker compose version --short'),
|
|
58
|
+
errorMessage: (current, required) => current === null ?
|
|
59
|
+
'Docker Compose is not installed. Please install Docker Compose to use the dev command.\nVisit: https://docs.docker.com/compose/install/' :
|
|
60
|
+
`Docker Compose ${required} is required (found v${current}).\nPlease update Docker Compose: https://docs.docker.com/compose/install/`
|
|
44
61
|
},
|
|
45
62
|
{
|
|
46
|
-
|
|
47
|
-
|
|
63
|
+
name: 'Docker Daemon',
|
|
64
|
+
semverRange: '*',
|
|
65
|
+
getVersion: () => checkDockerCommand('docker ps') ? '0.0.0' : null,
|
|
66
|
+
errorMessage: () => 'Docker daemon is not running. Please start Docker and try again.'
|
|
48
67
|
}
|
|
49
68
|
];
|
|
50
69
|
export function validateDockerEnvironment() {
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
70
|
+
for (const prereq of PREREQUISITES) {
|
|
71
|
+
const raw = prereq.getVersion();
|
|
72
|
+
const version = raw ? semver.valid(semver.coerce(raw)) : null;
|
|
73
|
+
if (!version) {
|
|
74
|
+
throw new DockerValidationError(prereq.errorMessage(null, prereq.semverRange));
|
|
75
|
+
}
|
|
76
|
+
if (!semver.satisfies(version, prereq.semverRange)) {
|
|
77
|
+
throw new DockerValidationError(prereq.errorMessage(version, prereq.semverRange));
|
|
78
|
+
}
|
|
54
79
|
}
|
|
55
80
|
}
|
|
56
81
|
export function getDefaultDockerComposePath() {
|
|
@@ -129,4 +154,4 @@ export async function stopDockerCompose(dockerComposePath) {
|
|
|
129
154
|
ux.stdout('⏹️ Stopping services...\n');
|
|
130
155
|
execFileSync('docker', ['compose', '-f', dockerComposePath, 'down'], { stdio: 'inherit', cwd: process.cwd() });
|
|
131
156
|
}
|
|
132
|
-
export { isDockerInstalled,
|
|
157
|
+
export { isDockerInstalled, DockerValidationError };
|
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export declare const getEjectSuccessMessage: (destPath: string, outputFile: string, binName: string) => string;
|
|
5
5
|
export declare const getProjectSuccessMessage: (folderName: string, installSuccess: boolean, credentialsConfigured?: boolean) => string;
|
|
6
|
-
export declare const getWorkflowGenerateSuccessMessage: (workflowName: string, targetDir: string, filesCreated: string[]) => string;
|
|
6
|
+
export declare const getWorkflowGenerateSuccessMessage: (workflowName: string, workflowId: string, scenarioName: string | undefined, targetDir: string, filesCreated: string[]) => string;
|
|
@@ -234,7 +234,7 @@ ${ux.colorize('dim', ' to manage your project secrets.')}
|
|
|
234
234
|
${ux.colorize('green', ux.colorize('bold', 'Happy building with Output! 🚀'))}
|
|
235
235
|
`;
|
|
236
236
|
};
|
|
237
|
-
export const getWorkflowGenerateSuccessMessage = (workflowName, targetDir, filesCreated) => {
|
|
237
|
+
export const getWorkflowGenerateSuccessMessage = (workflowName, workflowId, scenarioName, targetDir, filesCreated) => {
|
|
238
238
|
const divider = ux.colorize('dim', '─'.repeat(80));
|
|
239
239
|
const bulletPoint = ux.colorize('green', '▸');
|
|
240
240
|
const formattedFiles = filesCreated.map(file => {
|
|
@@ -256,7 +256,7 @@ export const getWorkflowGenerateSuccessMessage = (workflowName, targetDir, files
|
|
|
256
256
|
},
|
|
257
257
|
{
|
|
258
258
|
step: 'Test your workflow',
|
|
259
|
-
command: `npx output workflow run ${
|
|
259
|
+
command: `npx output workflow run ${workflowId}${scenarioName ? ` ${scenarioName}` : ''}`,
|
|
260
260
|
note: 'Run after starting services with "npx output dev"'
|
|
261
261
|
}
|
|
262
262
|
];
|
|
@@ -4,16 +4,50 @@ This is an **Output.ai** project - a framework for building reliable, production
|
|
|
4
4
|
|
|
5
5
|
## Getting Started
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Install Claude Code plugins for full framework documentation and AI-assisted development:
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
claude plugin marketplace add growthxai/output
|
|
11
11
|
claude plugin install outputai@outputai --scope project
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
+
## Commands
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm run output:dev # Start dev environment (worker + Temporal)
|
|
18
|
+
npm run output:worker:build # Build TypeScript to dist/
|
|
19
|
+
npm run output:worker:watch # Build + restart on file changes
|
|
20
|
+
npm run output:worker # Install, build, and start worker
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Project Structure
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
src/
|
|
27
|
+
workflows/ # Each subfolder is one workflow
|
|
28
|
+
<name>/
|
|
29
|
+
workflow.ts # Workflow orchestration (must be deterministic - no I/O)
|
|
30
|
+
steps.ts # Step functions (all I/O: HTTP, LLM, DB)
|
|
31
|
+
evaluators.ts # Evaluator functions (LLM-based quality assessment)
|
|
32
|
+
types.ts # Zod schemas and TypeScript types
|
|
33
|
+
prompts/ # .prompt files (Liquid.js templates with YAML frontmatter)
|
|
34
|
+
scenarios/ # Test scenario JSON files
|
|
35
|
+
clients/ # Shared HTTP clients (use @outputai/http, not fetch/axios)
|
|
36
|
+
shared/ # Shared utilities across workflows
|
|
37
|
+
config/
|
|
38
|
+
costs.yml # Token/API pricing overrides
|
|
39
|
+
credentials.yml.enc # Encrypted secrets (edit via: output credentials edit)
|
|
40
|
+
credentials.yml.template # Credential structure reference
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Key Conventions
|
|
44
|
+
|
|
45
|
+
- **Workflows are deterministic**: No I/O, no `Date.now()`, no `Math.random()` in `workflow.ts`. All side effects go in steps or evaluators.
|
|
46
|
+
- **HTTP clients**: Always use `httpClient` from `@outputai/http` -- never raw `fetch` or `axios`. This enables automatic tracing and cost tracking.
|
|
47
|
+
- **LLM calls**: Use `generateText` from `@outputai/llm` with `.prompt` files. Never call LLM APIs directly.
|
|
48
|
+
|
|
14
49
|
---
|
|
15
50
|
|
|
16
51
|
## Project-Specific Instructions
|
|
17
52
|
|
|
18
53
|
<!-- Add your project-specific instructions below -->
|
|
19
|
-
|
|
@@ -14,7 +14,17 @@ export declare function formatDuration(ms: number): string;
|
|
|
14
14
|
*/
|
|
15
15
|
export declare function formatDate(isoString: string | null | undefined): string;
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
17
|
+
* Calculate elapsed milliseconds between two ISO timestamps.
|
|
18
|
+
* If completedAt is null/undefined, uses current time (for in-progress durations).
|
|
19
|
+
*/
|
|
20
|
+
export declare function elapsedMs(startedAt: string, completedAt?: string | null): number;
|
|
21
|
+
/**
|
|
22
|
+
* Format a duration in milliseconds to a compact string that fits in narrow columns.
|
|
23
|
+
* Always returns a short single-token string (e.g., "150ms", "7.56s", "24.2m", "1.3h").
|
|
24
|
+
*/
|
|
25
|
+
export declare function formatDurationCompact(ms: number): string;
|
|
26
|
+
/**
|
|
27
|
+
* Format a duration between two ISO timestamps.
|
|
18
28
|
*
|
|
19
29
|
* @param startedAt - ISO 8601 start timestamp
|
|
20
30
|
* @param completedAt - ISO 8601 end timestamp (or null if still running)
|
|
@@ -33,7 +33,32 @@ export function formatDate(isoString) {
|
|
|
33
33
|
return format(parseISO(isoString), 'MMM d, yyyy h:mm a');
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
36
|
-
*
|
|
36
|
+
* Calculate elapsed milliseconds between two ISO timestamps.
|
|
37
|
+
* If completedAt is null/undefined, uses current time (for in-progress durations).
|
|
38
|
+
*/
|
|
39
|
+
export function elapsedMs(startedAt, completedAt) {
|
|
40
|
+
const start = parseISO(startedAt).getTime();
|
|
41
|
+
const end = completedAt ? parseISO(completedAt).getTime() : Date.now();
|
|
42
|
+
return end - start;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Format a duration in milliseconds to a compact string that fits in narrow columns.
|
|
46
|
+
* Always returns a short single-token string (e.g., "150ms", "7.56s", "24.2m", "1.3h").
|
|
47
|
+
*/
|
|
48
|
+
export function formatDurationCompact(ms) {
|
|
49
|
+
if (ms < 1000) {
|
|
50
|
+
return `${ms}ms`;
|
|
51
|
+
}
|
|
52
|
+
if (ms < 60_000) {
|
|
53
|
+
return `${(ms / 1000).toFixed(2)}s`;
|
|
54
|
+
}
|
|
55
|
+
if (ms < 3_600_000) {
|
|
56
|
+
return `${(ms / 60_000).toFixed(1)}m`;
|
|
57
|
+
}
|
|
58
|
+
return `${(ms / 3_600_000).toFixed(1)}h`;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Format a duration between two ISO timestamps.
|
|
37
62
|
*
|
|
38
63
|
* @param startedAt - ISO 8601 start timestamp
|
|
39
64
|
* @param completedAt - ISO 8601 end timestamp (or null if still running)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
type WorkflowResult = Pick<
|
|
3
|
-
export declare const ERROR_STATUSES: ReadonlySet<
|
|
1
|
+
import type { WorkflowResultResponse, WorkflowResultResponseStatus } from '../api/generated/api.js';
|
|
2
|
+
type WorkflowResult = Pick<WorkflowResultResponse, 'workflowId' | 'output' | 'status' | 'error'>;
|
|
3
|
+
export declare const ERROR_STATUSES: ReadonlySet<WorkflowResultResponseStatus | undefined>;
|
|
4
4
|
export declare function formatWorkflowResult(result: WorkflowResult): string;
|
|
5
5
|
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const openUrl: (url: string) => void;
|