@outputai/cli 0.10.1-next.f6a7c1a.0 → 0.11.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 +81 -26
- package/dist/api/generated/api.js +7 -4
- package/dist/api/http_client.js +2 -2
- package/dist/assets/docker/docker-compose-dev.yml +2 -2
- package/dist/commands/dev/down.d.ts +10 -0
- package/dist/commands/dev/down.js +34 -0
- package/dist/commands/dev/down.spec.js +71 -0
- package/dist/commands/dev/index.d.ts +4 -0
- package/dist/commands/dev/index.js +200 -53
- package/dist/commands/dev/index.spec.js +390 -42
- package/dist/commands/workflow/monitor.d.ts +5 -20
- package/dist/commands/workflow/monitor.js +20 -182
- package/dist/commands/workflow/monitor.spec.js +82 -3
- package/dist/commands/workflow/result.js +2 -2
- package/dist/commands/workflow/result.spec.js +65 -1
- package/dist/commands/workflow/run.js +10 -3
- package/dist/commands/workflow/run.spec.js +42 -5
- package/dist/commands/workflow/start.d.ts +7 -1
- package/dist/commands/workflow/start.js +107 -12
- package/dist/commands/workflow/start.spec.js +282 -5
- package/dist/commands/workflow/status.spec.js +1 -1
- package/dist/commands/workflow/{test_eval.d.ts → test.d.ts} +0 -1
- package/dist/commands/workflow/{test_eval.js → test.js} +0 -1
- package/dist/commands/workflow/test.spec.d.ts +1 -0
- package/dist/commands/workflow/{test_eval.spec.js → test.spec.js} +4 -4
- package/dist/generated/framework_version.json +1 -1
- package/dist/services/docker.d.ts +28 -1
- package/dist/services/docker.js +106 -12
- package/dist/services/docker.spec.js +144 -14
- package/dist/services/monitor_stream.d.ts +62 -0
- package/dist/services/monitor_stream.js +285 -0
- package/dist/services/monitor_stream.spec.d.ts +1 -0
- package/dist/services/monitor_stream.spec.js +285 -0
- package/dist/services/workflow_history.js +2 -2
- package/dist/templates/agent_instructions/CLAUDE.md.template +5 -3
- package/dist/templates/project/README.md.template +3 -1
- package/dist/templates/project/package.json.template +2 -2
- package/dist/templates/project/src/clients/jina.ts.template +4 -4
- package/dist/utils/env_loader.js +6 -2
- package/dist/utils/env_loader.spec.js +61 -32
- package/dist/utils/error_handler.d.ts +10 -0
- package/dist/utils/error_handler.js +14 -0
- package/dist/utils/error_handler.spec.d.ts +1 -0
- package/dist/utils/error_handler.spec.js +62 -0
- package/dist/utils/format_workflow_result.d.ts +15 -3
- package/dist/utils/format_workflow_result.js +39 -6
- package/dist/utils/format_workflow_result.spec.js +39 -6
- package/dist/utils/monitor_flags.d.ts +35 -0
- package/dist/utils/monitor_flags.js +76 -0
- package/dist/utils/normalize_workflow_status.d.ts +4 -3
- package/dist/utils/normalize_workflow_status.js +12 -3
- package/dist/utils/normalize_workflow_status.spec.js +3 -0
- package/dist/utils/port_collision.d.ts +22 -7
- package/dist/utils/port_collision.js +39 -14
- package/dist/utils/port_collision.spec.js +40 -1
- package/dist/utils/resolve_input.d.ts +9 -1
- package/dist/utils/resolve_input.js +8 -2
- package/dist/utils/resolve_input.spec.d.ts +1 -0
- package/dist/utils/resolve_input.spec.js +75 -0
- package/dist/views/dev/chrome/footer.d.ts +2 -0
- package/dist/views/dev/chrome/footer.js +4 -4
- package/dist/views/dev/components/workflow_status.js +1 -1
- package/dist/views/dev/dev_app.d.ts +1 -0
- package/dist/views/dev/dev_app.js +13 -4
- package/dist/views/dev/hooks/use_run_detail.js +4 -4
- package/dist/views/dev/hooks/use_run_detail.spec.js +1 -1
- package/dist/views/dev/panels/runs_panel.js +2 -2
- package/oclif.manifest.json +91 -10
- package/package.json +7 -9
- /package/dist/commands/{workflow/test_eval.spec.d.ts → dev/down.spec.d.ts} +0 -0
|
@@ -3,20 +3,25 @@
|
|
|
3
3
|
* an actionable hint that names the conflicting port and the env var to
|
|
4
4
|
* override.
|
|
5
5
|
*
|
|
6
|
-
* Docker
|
|
7
|
-
*
|
|
8
|
-
* - "failed to bind host port for 0.0.0.0:7233:.../tcp: address already in use"
|
|
6
|
+
* Docker wraps the same failure differently across versions and platforms —
|
|
7
|
+
* Docker 29 on macOS nests it three deep:
|
|
9
8
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* Error response from daemon: failed to set up container networking: driver
|
|
10
|
+
* failed programming external connectivity on endpoint out-api-1 (a1b2…):
|
|
11
|
+
* Bind for 0.0.0.0:3001 failed: port is already allocated
|
|
12
|
+
*
|
|
13
|
+
* Matching whole message shapes means a new wrapper silently drops the hint, so
|
|
14
|
+
* we anchor on the terminal phrase instead and take the host port nearest to it.
|
|
15
|
+
* That survives wrappers we haven't seen.
|
|
16
|
+
*
|
|
17
|
+
* The port is then mapped back to the env var that sets it. The map prefers a
|
|
18
|
+
* runtime lookup of resolved ports (so a user who already set
|
|
19
|
+
* OUTPUT_API_HOST_PORT=3050 sees that var named when 3050 collides) and falls
|
|
20
|
+
* back to a default-port table for the unresolved case.
|
|
14
21
|
*/
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
/listen tcp [^:\s]+:(\d+):\s*bind: address already in use/
|
|
19
|
-
];
|
|
22
|
+
const COLLISION_PHRASES = ['port is already allocated', 'address already in use'];
|
|
23
|
+
/** Trailing `:<port>` in a fragment — the host port a bind failure names. */
|
|
24
|
+
const TRAILING_PORT = /:(\d+)(?!.*:\d)/s;
|
|
20
25
|
const DEFAULT_PORT_TO_ENV_VAR = {
|
|
21
26
|
3001: 'OUTPUT_API_HOST_PORT',
|
|
22
27
|
8080: 'OUTPUT_TEMPORAL_UI_HOST_PORT',
|
|
@@ -35,8 +40,15 @@ export function extractCollidedPort(stderr) {
|
|
|
35
40
|
if (!stderr) {
|
|
36
41
|
return null;
|
|
37
42
|
}
|
|
38
|
-
for (const
|
|
39
|
-
const
|
|
43
|
+
for (const phrase of COLLISION_PHRASES) {
|
|
44
|
+
const phraseIndex = stderr.indexOf(phrase);
|
|
45
|
+
if (phraseIndex === -1) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
// The host port is the last one named before the phrase — every shape puts
|
|
49
|
+
// it there ("Bind for 0.0.0.0:3001 failed: port is already allocated",
|
|
50
|
+
// "listen tcp 0.0.0.0:3001: bind: address already in use").
|
|
51
|
+
const match = stderr.slice(0, phraseIndex).match(TRAILING_PORT);
|
|
40
52
|
if (match) {
|
|
41
53
|
return parseInt(match[1], 10);
|
|
42
54
|
}
|
|
@@ -87,6 +99,19 @@ export function formatPortCollisionHint(stderr, resolvedPorts) {
|
|
|
87
99
|
}
|
|
88
100
|
return formatSingleCollision(port, resolvedPorts);
|
|
89
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Compose a docker-failure message from a caller-supplied core sentence and the
|
|
104
|
+
* process's recent output: an actionable port-collision hint (when one is
|
|
105
|
+
* detected) is prepended, and the raw recent output is appended. Shared by the
|
|
106
|
+
* foreground exit handler and the detached/reconcile path so both surface the
|
|
107
|
+
* same failure shape.
|
|
108
|
+
*/
|
|
109
|
+
export function formatComposeFailure(reason, output, resolvedPorts) {
|
|
110
|
+
const hint = formatPortCollisionHint(output, resolvedPorts);
|
|
111
|
+
const prefix = hint ? `${hint}\n\n` : '';
|
|
112
|
+
const detail = output ? `\n\nRecent Docker output:\n${output}` : '';
|
|
113
|
+
return `${prefix}${reason}${detail}`;
|
|
114
|
+
}
|
|
90
115
|
/**
|
|
91
116
|
* Build a hint from a known list of colliding ports. For a single collision
|
|
92
117
|
* the output matches `formatPortCollisionHint` exactly so callers stay
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { extractCollidedPort, formatPortCollisionHint, formatPortCollisionsHint } from './port_collision.js';
|
|
2
|
+
import { extractCollidedPort, formatPortCollisionHint, formatPortCollisionsHint, formatComposeFailure } from './port_collision.js';
|
|
3
3
|
const DEFAULT_PORTS = { api: 3001, temporalUi: 8080, temporal: 7233 };
|
|
4
4
|
describe('extractCollidedPort', () => {
|
|
5
5
|
it('matches the "Bind for ... port is already allocated" shape', () => {
|
|
@@ -22,6 +22,24 @@ describe('extractCollidedPort', () => {
|
|
|
22
22
|
it('returns null when no bind failure is present', () => {
|
|
23
23
|
expect(extractCollidedPort('some unrelated stderr line')).toBeNull();
|
|
24
24
|
});
|
|
25
|
+
// Captured verbatim from Docker 29.4.0 on macOS. The bind failure is nested
|
|
26
|
+
// three wrappers deep; matching whole message shapes missed it.
|
|
27
|
+
it('extracts the port from Docker 29\'s nested container-networking wrapper', () => {
|
|
28
|
+
const stderr = 'Error response from daemon: failed to set up container networking: ' +
|
|
29
|
+
'driver failed programming external connectivity on endpoint out-api-1 ' +
|
|
30
|
+
'(e72baf85643fb5dc19000acf62c1ad0d11bffc653cabe1fc8861387ec1ebd629): ' +
|
|
31
|
+
'Bind for 0.0.0.0:3001 failed: port is already allocated';
|
|
32
|
+
expect(extractCollidedPort(stderr)).toBe(3001);
|
|
33
|
+
});
|
|
34
|
+
it('extracts the port from the "ports are not available" wrapper', () => {
|
|
35
|
+
const stderr = 'Error: ports are not available: exposing port TCP 0.0.0.0:3001 -> 0.0.0.0:0: ' +
|
|
36
|
+
'listen tcp 0.0.0.0:3001: bind: address already in use';
|
|
37
|
+
expect(extractCollidedPort(stderr)).toBe(3001);
|
|
38
|
+
});
|
|
39
|
+
it('ignores an IP-like prefix and takes the port nearest the failure phrase', () => {
|
|
40
|
+
const stderr = 'container 172.17.0.2:5432 started\nBind for 0.0.0.0:8080 failed: port is already allocated';
|
|
41
|
+
expect(extractCollidedPort(stderr)).toBe(8080);
|
|
42
|
+
});
|
|
25
43
|
it('returns null for empty input', () => {
|
|
26
44
|
expect(extractCollidedPort('')).toBeNull();
|
|
27
45
|
});
|
|
@@ -80,3 +98,24 @@ describe('formatPortCollisionsHint', () => {
|
|
|
80
98
|
expect(hint).toContain('• Port 5432 — stop the process holding it');
|
|
81
99
|
});
|
|
82
100
|
});
|
|
101
|
+
describe('formatComposeFailure', () => {
|
|
102
|
+
const reason = 'Docker compose failed to start services (exit code 1).';
|
|
103
|
+
it('prepends the actionable hint when the output names a collision', () => {
|
|
104
|
+
const message = formatComposeFailure(reason, 'Bind for 0.0.0.0:3001 failed: port is already allocated', DEFAULT_PORTS);
|
|
105
|
+
expect(message.startsWith('Port 3001 is already in use.')).toBe(true);
|
|
106
|
+
expect(message).toContain('OUTPUT_API_HOST_PORT=<other port>');
|
|
107
|
+
expect(message).toContain(reason);
|
|
108
|
+
expect(message).toContain('Recent Docker output:');
|
|
109
|
+
});
|
|
110
|
+
it('omits the output section entirely when nothing was captured', () => {
|
|
111
|
+
const message = formatComposeFailure(reason, '', DEFAULT_PORTS);
|
|
112
|
+
expect(message).toBe(reason);
|
|
113
|
+
expect(message).not.toContain('Recent Docker output:');
|
|
114
|
+
});
|
|
115
|
+
it('returns reason plus raw output, with no hint, for an unrecognized failure', () => {
|
|
116
|
+
const message = formatComposeFailure(reason, 'no such image: outputai/api:dev', DEFAULT_PORTS);
|
|
117
|
+
expect(message.startsWith(reason)).toBe(true);
|
|
118
|
+
expect(message).toContain('Recent Docker output:\nno such image');
|
|
119
|
+
expect(message).not.toContain('is already in use');
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -1 +1,9 @@
|
|
|
1
|
-
export
|
|
1
|
+
export type ResolveInputOptions = {
|
|
2
|
+
workflowName: string;
|
|
3
|
+
scenario?: string;
|
|
4
|
+
inputFlag?: string;
|
|
5
|
+
commandName: string;
|
|
6
|
+
catalog?: string;
|
|
7
|
+
json?: boolean;
|
|
8
|
+
};
|
|
9
|
+
export declare function resolveInput(options: ResolveInputOptions): Promise<unknown>;
|
|
@@ -1,7 +1,8 @@
|
|
|
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(
|
|
4
|
+
export async function resolveInput(options) {
|
|
5
|
+
const { workflowName, scenario, inputFlag, commandName, catalog, json } = options;
|
|
5
6
|
if (inputFlag && scenario) {
|
|
6
7
|
return ux.error('Cannot use both scenario argument and --input flag. Choose one.', { exit: 1 });
|
|
7
8
|
}
|
|
@@ -13,7 +14,12 @@ export async function resolveInput(workflowName, scenario, inputFlag, commandNam
|
|
|
13
14
|
if (!resolution.found) {
|
|
14
15
|
return ux.error(getScenarioNotFoundMessage(workflowName, scenario, resolution.searchedPaths), { exit: 1 });
|
|
15
16
|
}
|
|
16
|
-
|
|
17
|
+
// Advisory notice goes to stderr so stdout stays clean for piping, and is
|
|
18
|
+
// skipped entirely under --json where even stderr is noise to a script
|
|
19
|
+
// consuming the structured output (same rule as the init hook's banner).
|
|
20
|
+
if (!json) {
|
|
21
|
+
ux.stderr(`Using scenario: ${resolution.path}`);
|
|
22
|
+
}
|
|
17
23
|
return parseInputFlag(resolution.path);
|
|
18
24
|
}
|
|
19
25
|
return ux.error('Input required. Provide either:\n' +
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
3
|
+
vi.mock('@oclif/core', () => ({
|
|
4
|
+
ux: {
|
|
5
|
+
stdout: vi.fn(),
|
|
6
|
+
stderr: vi.fn(),
|
|
7
|
+
error: vi.fn(() => {
|
|
8
|
+
throw new Error('ux.error called');
|
|
9
|
+
})
|
|
10
|
+
}
|
|
11
|
+
}));
|
|
12
|
+
vi.mock('#utils/input_parser.js', () => ({
|
|
13
|
+
parseInputFlag: vi.fn()
|
|
14
|
+
}));
|
|
15
|
+
vi.mock('#utils/scenario_resolver.js', () => ({
|
|
16
|
+
resolveScenarioPath: vi.fn(),
|
|
17
|
+
getScenarioNotFoundMessage: vi.fn()
|
|
18
|
+
}));
|
|
19
|
+
describe('resolveInput', () => {
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
vi.clearAllMocks();
|
|
22
|
+
});
|
|
23
|
+
it('emits the scenario notice on stderr so --json stdout stays clean', async () => {
|
|
24
|
+
const { ux } = await import('@oclif/core');
|
|
25
|
+
const { parseInputFlag } = await import('#utils/input_parser.js');
|
|
26
|
+
const { resolveScenarioPath } = await import('#utils/scenario_resolver.js');
|
|
27
|
+
const { resolveInput } = await import('./resolve_input.js');
|
|
28
|
+
vi.mocked(resolveScenarioPath).mockResolvedValue({
|
|
29
|
+
found: true,
|
|
30
|
+
path: '/scenarios/happy_path.json'
|
|
31
|
+
});
|
|
32
|
+
vi.mocked(parseInputFlag).mockReturnValue({ key: 'value' });
|
|
33
|
+
const result = await resolveInput({
|
|
34
|
+
workflowName: 'web_search',
|
|
35
|
+
scenario: 'happy_path',
|
|
36
|
+
commandName: 'start'
|
|
37
|
+
});
|
|
38
|
+
expect(result).toEqual({ key: 'value' });
|
|
39
|
+
expect(ux.stderr).toHaveBeenCalledWith('Using scenario: /scenarios/happy_path.json');
|
|
40
|
+
expect(ux.stdout).not.toHaveBeenCalled();
|
|
41
|
+
});
|
|
42
|
+
it('suppresses the scenario notice entirely under --json', async () => {
|
|
43
|
+
const { ux } = await import('@oclif/core');
|
|
44
|
+
const { parseInputFlag } = await import('#utils/input_parser.js');
|
|
45
|
+
const { resolveScenarioPath } = await import('#utils/scenario_resolver.js');
|
|
46
|
+
const { resolveInput } = await import('./resolve_input.js');
|
|
47
|
+
vi.mocked(resolveScenarioPath).mockResolvedValue({
|
|
48
|
+
found: true,
|
|
49
|
+
path: '/scenarios/happy_path.json'
|
|
50
|
+
});
|
|
51
|
+
vi.mocked(parseInputFlag).mockReturnValue({ key: 'value' });
|
|
52
|
+
const result = await resolveInput({
|
|
53
|
+
workflowName: 'web_search',
|
|
54
|
+
scenario: 'happy_path',
|
|
55
|
+
commandName: 'start',
|
|
56
|
+
json: true
|
|
57
|
+
});
|
|
58
|
+
expect(result).toEqual({ key: 'value' });
|
|
59
|
+
expect(ux.stderr).not.toHaveBeenCalled();
|
|
60
|
+
expect(ux.stdout).not.toHaveBeenCalled();
|
|
61
|
+
});
|
|
62
|
+
it('does not emit the scenario notice when input comes from --input', async () => {
|
|
63
|
+
const { ux } = await import('@oclif/core');
|
|
64
|
+
const { parseInputFlag } = await import('#utils/input_parser.js');
|
|
65
|
+
const { resolveInput } = await import('./resolve_input.js');
|
|
66
|
+
vi.mocked(parseInputFlag).mockReturnValue({ key: 'value' });
|
|
67
|
+
await resolveInput({
|
|
68
|
+
workflowName: 'web_search',
|
|
69
|
+
inputFlag: '{"key":"value"}',
|
|
70
|
+
commandName: 'start'
|
|
71
|
+
});
|
|
72
|
+
expect(ux.stderr).not.toHaveBeenCalled();
|
|
73
|
+
expect(ux.stdout).not.toHaveBeenCalled();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -5,16 +5,16 @@ import packageJson from '../../../../package.json' with { type: 'json' };
|
|
|
5
5
|
const GLOBAL_HINT_ROWS = 1;
|
|
6
6
|
const LOCAL_HINT_ROWS = 1;
|
|
7
7
|
export const getHeight = () => GLOBAL_HINT_ROWS + LOCAL_HINT_ROWS;
|
|
8
|
-
const
|
|
8
|
+
const globalHints = (attached) => [
|
|
9
9
|
{ key: 'tab', label: 'next tab' },
|
|
10
10
|
{ key: 'shift-tab', label: 'prev tab' },
|
|
11
11
|
{ key: '1-4', label: 'tabs' },
|
|
12
12
|
{ key: '/', label: 'search' },
|
|
13
13
|
{ key: '?', label: 'help' },
|
|
14
|
-
{ key: 'ctrl+c', label: 'quit' }
|
|
14
|
+
{ key: 'ctrl+c', label: attached ? 'detach (keeps running)' : 'quit' }
|
|
15
15
|
];
|
|
16
16
|
const VERSION = packageJson.version;
|
|
17
17
|
const HintRow = ({ hints }) => (_jsx(Box, { flexDirection: "row", children: hints.length === 0 ? (_jsx(Text, { children: " " })) : hints.map((hint, i) => (_jsxs(React.Fragment, { children: [i > 0 && _jsx(Text, { dimColor: true, children: ' ' }), _jsx(Text, { bold: true, children: hint.key }), _jsx(Text, { dimColor: true, children: ` ${hint.label}` })] }, hint.key))) }));
|
|
18
|
-
export const Footer = ({ hints = [], itemCount, itemLabel }) => {
|
|
19
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsx(HintRow, { hints:
|
|
18
|
+
export const Footer = ({ hints = [], itemCount, itemLabel, attached = false }) => {
|
|
19
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsx(HintRow, { hints: globalHints(attached) }), typeof itemCount === 'number' && itemLabel && (_jsx(Box, { children: _jsxs(Text, { dimColor: true, children: [itemCount, " ", itemLabel] }) }))] }), _jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsx(HintRow, { hints: hints }), _jsxs(Text, { color: "blackBright", children: ["v", VERSION] })] })] }));
|
|
20
20
|
};
|
|
@@ -4,7 +4,7 @@ const WORKFLOW_STATUS_MAP = {
|
|
|
4
4
|
running: { icon: '●', color: 'yellow' },
|
|
5
5
|
completed: { icon: '●', color: 'green' },
|
|
6
6
|
failed: { icon: '✗', color: 'red' },
|
|
7
|
-
|
|
7
|
+
cancelled: { icon: '○', color: 'gray' },
|
|
8
8
|
terminated: { icon: '✗', color: 'gray' },
|
|
9
9
|
timed_out: { icon: '✗', color: 'red' },
|
|
10
10
|
continued_as_new: { icon: '↻', color: 'blue' }
|
|
@@ -150,7 +150,7 @@ const overlayFor = (opts) => {
|
|
|
150
150
|
}
|
|
151
151
|
return null;
|
|
152
152
|
};
|
|
153
|
-
const Shell = ({ dockerComposePath, onCleanup }) => {
|
|
153
|
+
const Shell = ({ dockerComposePath, onCleanup, attached }) => {
|
|
154
154
|
const { exit } = useApp();
|
|
155
155
|
const ui = useUiState();
|
|
156
156
|
const [phase, setPhase] = useState('waiting');
|
|
@@ -160,7 +160,16 @@ const Shell = ({ dockerComposePath, onCleanup }) => {
|
|
|
160
160
|
onServices: setServices,
|
|
161
161
|
onAllHealthy: () => setPhase('running'),
|
|
162
162
|
onFailure: () => setPhase('failed'),
|
|
163
|
-
|
|
163
|
+
// An attach/reconcile session monitors a stack it doesn't own, so a slow
|
|
164
|
+
// health check must not be fatal — drop into the dashboard and keep
|
|
165
|
+
// polling status. Only an owned fresh start treats the timeout as an error.
|
|
166
|
+
onTimeout: () => {
|
|
167
|
+
if (attached) {
|
|
168
|
+
setPhase('running');
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
exit(new Error('Timeout waiting for services to become healthy'));
|
|
172
|
+
}
|
|
164
173
|
});
|
|
165
174
|
useStatusRefresh(dockerComposePath, phase !== 'waiting', setServices);
|
|
166
175
|
useWorkflowRunsPolling(phase !== 'waiting', setRuns);
|
|
@@ -231,6 +240,6 @@ const Shell = ({ dockerComposePath, onCleanup }) => {
|
|
|
231
240
|
if (overlay) {
|
|
232
241
|
return (_jsx(Box, { flexDirection: "column", height: rows, paddingX: 1, children: overlay }));
|
|
233
242
|
}
|
|
234
|
-
return (_jsxs(Box, { flexDirection: "column", height: rows, paddingX: 1, children: [_jsx(Header, { counters: counters }), _jsx(TabBar, { active: ui.tab, borderColor: RULE_PURPLE }), _jsx(SearchBar, { active: ui.search.open }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, height: contentRows, overflow: "hidden", children: [ui.tab === 'workflows' && _jsx(WorkflowsPanel, { workflows: workflows, runs: runs }), ui.tab === 'runs' && _jsx(RunsPanel, { runs: runs, height: contentRows }), ui.tab === 'services' && (_jsx(ServicesPanel, { height: contentRows, phase: phase, services: services, dockerComposePath: dockerComposePath })), ui.tab === 'help' && _jsx(HelpPanel, {})] }), _jsx(Toasts, {}), _jsx(Footer, { hints: footer.hints, itemCount: footer.itemCount, itemLabel: footer.itemLabel })] }));
|
|
243
|
+
return (_jsxs(Box, { flexDirection: "column", height: rows, paddingX: 1, children: [_jsx(Header, { counters: counters }), _jsx(TabBar, { active: ui.tab, borderColor: RULE_PURPLE }), _jsx(SearchBar, { active: ui.search.open }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, height: contentRows, overflow: "hidden", children: [ui.tab === 'workflows' && _jsx(WorkflowsPanel, { workflows: workflows, runs: runs }), ui.tab === 'runs' && _jsx(RunsPanel, { runs: runs, height: contentRows }), ui.tab === 'services' && (_jsx(ServicesPanel, { height: contentRows, phase: phase, services: services, dockerComposePath: dockerComposePath })), ui.tab === 'help' && _jsx(HelpPanel, {})] }), _jsx(Toasts, {}), _jsx(Footer, { hints: footer.hints, itemCount: footer.itemCount, itemLabel: footer.itemLabel, attached: attached })] }));
|
|
235
244
|
};
|
|
236
|
-
export const DevApp = ({ dockerComposePath, onCleanup }) => (_jsx(UiStateProvider, { children: _jsx(Shell, { dockerComposePath: dockerComposePath, onCleanup: onCleanup }) }));
|
|
245
|
+
export const DevApp = ({ dockerComposePath, onCleanup, attached = false }) => (_jsx(UiStateProvider, { children: _jsx(Shell, { dockerComposePath: dockerComposePath, onCleanup: onCleanup, attached: attached }) }));
|
|
@@ -2,7 +2,7 @@ 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
4
|
import { normalizeWorkflowStatus } from '#utils/normalize_workflow_status.js';
|
|
5
|
-
import {
|
|
5
|
+
import { isTerminalStatus } from '#utils/format_workflow_result.js';
|
|
6
6
|
import { createBoundedCache } from '#views/dev/utils/bounded_cache.js';
|
|
7
7
|
const EMPTY_DETAIL = {
|
|
8
8
|
result: null,
|
|
@@ -73,7 +73,7 @@ const readTraceLog = async (source) => {
|
|
|
73
73
|
return JSON.parse(content);
|
|
74
74
|
};
|
|
75
75
|
// Run detail and trace fetches are best-effort. Many statuses (in-progress,
|
|
76
|
-
// failed,
|
|
76
|
+
// failed, cancelled) don't have a fully-formed result or trace available at
|
|
77
77
|
// any given moment, and that's expected — the caller falls back to
|
|
78
78
|
// EMPTY_DETAIL and the UI renders whatever's there. Swallow everything.
|
|
79
79
|
const fetchTrace = async (workflowId, runId) => {
|
|
@@ -104,8 +104,8 @@ const fetchResult = async (workflowId, runId) => {
|
|
|
104
104
|
};
|
|
105
105
|
// The cache is intentionally only populated for terminal statuses — partial results
|
|
106
106
|
// from a still-running workflow would otherwise stick and stall the UI when the run
|
|
107
|
-
// eventually finishes. `
|
|
108
|
-
export const isTerminalRunStatus = (status) =>
|
|
107
|
+
// eventually finishes. `isTerminalStatus` is shared with `workflow monitor`.
|
|
108
|
+
export const isTerminalRunStatus = (status) => isTerminalStatus(status) !== undefined;
|
|
109
109
|
export const useRunDetail = (workflowId, runId, status) => {
|
|
110
110
|
const [detail, setDetail] = useState(EMPTY_DETAIL);
|
|
111
111
|
const fetchIdRef = useRef(0);
|
|
@@ -4,7 +4,7 @@ describe('isTerminalRunStatus', () => {
|
|
|
4
4
|
it('returns true for completed states', () => {
|
|
5
5
|
expect(isTerminalRunStatus('completed')).toBe(true);
|
|
6
6
|
expect(isTerminalRunStatus('failed')).toBe(true);
|
|
7
|
-
expect(isTerminalRunStatus('
|
|
7
|
+
expect(isTerminalRunStatus('cancelled')).toBe(true);
|
|
8
8
|
expect(isTerminalRunStatus('terminated')).toBe(true);
|
|
9
9
|
expect(isTerminalRunStatus('timed_out')).toBe(true);
|
|
10
10
|
});
|
|
@@ -21,7 +21,7 @@ const STATUS_ORDER = {
|
|
|
21
21
|
failed: 1,
|
|
22
22
|
timed_out: 2,
|
|
23
23
|
terminated: 3,
|
|
24
|
-
|
|
24
|
+
cancelled: 4,
|
|
25
25
|
continued_as_new: 5,
|
|
26
26
|
completed: 6
|
|
27
27
|
};
|
|
@@ -110,7 +110,7 @@ const DetailPane = ({ run, pane, rows }) => {
|
|
|
110
110
|
}
|
|
111
111
|
return _jsx(Text, { dimColor: true, children: "\u2014" });
|
|
112
112
|
}
|
|
113
|
-
if (activePane === 'output' &&
|
|
113
|
+
if (activePane === 'output' && typeof pane.error === 'string') {
|
|
114
114
|
const lines = String(pane.error).split('\n').slice(0, tabContentRows);
|
|
115
115
|
return (_jsx(Box, { flexDirection: "column", children: lines.map((line, i) => (_jsx(Text, { color: "red", wrap: "truncate-end", children: line }, i))) }));
|
|
116
116
|
}
|
package/oclif.manifest.json
CHANGED
|
@@ -416,6 +416,41 @@
|
|
|
416
416
|
"show.js"
|
|
417
417
|
]
|
|
418
418
|
},
|
|
419
|
+
"dev:down": {
|
|
420
|
+
"aliases": [],
|
|
421
|
+
"args": {},
|
|
422
|
+
"description": "Stop Output development services started by `output dev`\n\nUseful after `output dev -d`, or when an attached `output dev` session\nleft the services running on quit.",
|
|
423
|
+
"examples": [
|
|
424
|
+
"<%= config.bin %> <%= command.id %>",
|
|
425
|
+
"<%= config.bin %> <%= command.id %> --compose-file ./custom-docker-compose.yml"
|
|
426
|
+
],
|
|
427
|
+
"flags": {
|
|
428
|
+
"compose-file": {
|
|
429
|
+
"char": "f",
|
|
430
|
+
"description": "Path to a custom docker-compose file",
|
|
431
|
+
"name": "compose-file",
|
|
432
|
+
"required": false,
|
|
433
|
+
"hasDynamicHelp": false,
|
|
434
|
+
"multiple": false,
|
|
435
|
+
"type": "option"
|
|
436
|
+
}
|
|
437
|
+
},
|
|
438
|
+
"hasDynamicHelp": false,
|
|
439
|
+
"hiddenAliases": [],
|
|
440
|
+
"id": "dev:down",
|
|
441
|
+
"pluginAlias": "@outputai/cli",
|
|
442
|
+
"pluginName": "@outputai/cli",
|
|
443
|
+
"pluginType": "core",
|
|
444
|
+
"strict": true,
|
|
445
|
+
"enableJsonFlag": false,
|
|
446
|
+
"isESM": true,
|
|
447
|
+
"relativePath": [
|
|
448
|
+
"dist",
|
|
449
|
+
"commands",
|
|
450
|
+
"dev",
|
|
451
|
+
"down.js"
|
|
452
|
+
]
|
|
453
|
+
},
|
|
419
454
|
"dev:eject": {
|
|
420
455
|
"aliases": [],
|
|
421
456
|
"args": {},
|
|
@@ -463,9 +498,10 @@
|
|
|
463
498
|
"dev": {
|
|
464
499
|
"aliases": [],
|
|
465
500
|
"args": {},
|
|
466
|
-
"description": "Start Output development services (auto-restarts worker on file changes)\n\nTo run a second dev stack concurrently,
|
|
501
|
+
"description": "Start Output development services (auto-restarts worker on file changes)\n\nIf services are already running (e.g. after `output dev -d`), this attaches\nto monitor them rather than treating our own containers as a port collision.\nQuitting an attached session leaves the services running — stop them with\n`output dev down`.\n\nTo run a second dev stack concurrently, give it its own compose project and\nhost ports in .env — without DOCKER_SERVICE_NAME both checkouts share one\nstack, and the second will attach to the first instead of starting:\n\n DOCKER_SERVICE_NAME=output-sdk-two\n OUTPUT_API_HOST_PORT=3002\n OUTPUT_TEMPORAL_UI_HOST_PORT=8081\n OUTPUT_TEMPORAL_HOST_PORT=7234",
|
|
467
502
|
"examples": [
|
|
468
503
|
"<%= config.bin %> <%= command.id %>",
|
|
504
|
+
"<%= config.bin %> <%= command.id %> --detached",
|
|
469
505
|
"<%= config.bin %> <%= command.id %> --compose-file ./custom-docker-compose.yml",
|
|
470
506
|
"<%= config.bin %> <%= command.id %> --image-pull-policy missing"
|
|
471
507
|
],
|
|
@@ -1200,9 +1236,18 @@
|
|
|
1200
1236
|
"<%= config.bin %> <%= command.id %> simple basic_input",
|
|
1201
1237
|
"<%= config.bin %> <%= command.id %> simple --input '{\"values\":[1,2,3]}'",
|
|
1202
1238
|
"<%= config.bin %> <%= command.id %> simple --input input.json",
|
|
1203
|
-
"<%= config.bin %> <%= command.id %> simple --input
|
|
1239
|
+
"<%= config.bin %> <%= command.id %> simple --input input.json --monitor",
|
|
1240
|
+
"<%= config.bin %> <%= command.id %> simple --input '{\"key\":\"value\"}' --catalog my-catalog",
|
|
1241
|
+
"<%= config.bin %> <%= command.id %> simple --json"
|
|
1204
1242
|
],
|
|
1205
1243
|
"flags": {
|
|
1244
|
+
"json": {
|
|
1245
|
+
"description": "Format output as json.",
|
|
1246
|
+
"helpGroup": "GLOBAL",
|
|
1247
|
+
"name": "json",
|
|
1248
|
+
"allowNo": false,
|
|
1249
|
+
"type": "boolean"
|
|
1250
|
+
},
|
|
1206
1251
|
"input": {
|
|
1207
1252
|
"char": "i",
|
|
1208
1253
|
"description": "Workflow input as JSON string or file path (overrides scenario)",
|
|
@@ -1227,6 +1272,44 @@
|
|
|
1227
1272
|
"hasDynamicHelp": false,
|
|
1228
1273
|
"multiple": false,
|
|
1229
1274
|
"type": "option"
|
|
1275
|
+
},
|
|
1276
|
+
"monitor": {
|
|
1277
|
+
"char": "m",
|
|
1278
|
+
"description": "After starting, attach and stream status updates until the workflow ends (Ctrl+C detaches; the workflow keeps running). Cannot be combined with --json",
|
|
1279
|
+
"name": "monitor",
|
|
1280
|
+
"allowNo": false,
|
|
1281
|
+
"type": "boolean"
|
|
1282
|
+
},
|
|
1283
|
+
"include-payloads": {
|
|
1284
|
+
"dependsOn": [
|
|
1285
|
+
"monitor"
|
|
1286
|
+
],
|
|
1287
|
+
"description": "Include decoded step input/output payloads (requires --monitor)",
|
|
1288
|
+
"helpGroup": "MONITOR",
|
|
1289
|
+
"name": "include-payloads",
|
|
1290
|
+
"allowNo": false,
|
|
1291
|
+
"type": "boolean"
|
|
1292
|
+
},
|
|
1293
|
+
"interval": {
|
|
1294
|
+
"dependsOn": [
|
|
1295
|
+
"monitor"
|
|
1296
|
+
],
|
|
1297
|
+
"description": "Poll interval in milliseconds. Once a resumed poll is long-polling server-side for new events, this also bounds how long that block may last (capped at the server's configured max), so an idle workflow's update cadence is roughly twice this value (the long-poll bound, then this sleep) rather than a much longer, separate server default. (requires --monitor) [default: 2500]",
|
|
1298
|
+
"helpGroup": "MONITOR",
|
|
1299
|
+
"name": "interval",
|
|
1300
|
+
"hasDynamicHelp": false,
|
|
1301
|
+
"multiple": false,
|
|
1302
|
+
"type": "option"
|
|
1303
|
+
},
|
|
1304
|
+
"color": {
|
|
1305
|
+
"dependsOn": [
|
|
1306
|
+
"monitor"
|
|
1307
|
+
],
|
|
1308
|
+
"description": "Colorize status output (use --no-color to disable) (requires --monitor)",
|
|
1309
|
+
"helpGroup": "MONITOR",
|
|
1310
|
+
"name": "color",
|
|
1311
|
+
"allowNo": true,
|
|
1312
|
+
"type": "boolean"
|
|
1230
1313
|
}
|
|
1231
1314
|
},
|
|
1232
1315
|
"hasDynamicHelp": false,
|
|
@@ -1236,7 +1319,7 @@
|
|
|
1236
1319
|
"pluginName": "@outputai/cli",
|
|
1237
1320
|
"pluginType": "core",
|
|
1238
1321
|
"strict": true,
|
|
1239
|
-
"enableJsonFlag":
|
|
1322
|
+
"enableJsonFlag": true,
|
|
1240
1323
|
"isESM": true,
|
|
1241
1324
|
"relativePath": [
|
|
1242
1325
|
"dist",
|
|
@@ -1354,10 +1437,8 @@
|
|
|
1354
1437
|
"terminate.js"
|
|
1355
1438
|
]
|
|
1356
1439
|
},
|
|
1357
|
-
"workflow:
|
|
1358
|
-
"aliases": [
|
|
1359
|
-
"workflow:test"
|
|
1360
|
-
],
|
|
1440
|
+
"workflow:test": {
|
|
1441
|
+
"aliases": [],
|
|
1361
1442
|
"args": {
|
|
1362
1443
|
"workflowName": {
|
|
1363
1444
|
"description": "Name of the workflow to test",
|
|
@@ -1426,7 +1507,7 @@
|
|
|
1426
1507
|
},
|
|
1427
1508
|
"hasDynamicHelp": false,
|
|
1428
1509
|
"hiddenAliases": [],
|
|
1429
|
-
"id": "workflow:
|
|
1510
|
+
"id": "workflow:test",
|
|
1430
1511
|
"pluginAlias": "@outputai/cli",
|
|
1431
1512
|
"pluginName": "@outputai/cli",
|
|
1432
1513
|
"pluginType": "core",
|
|
@@ -1437,7 +1518,7 @@
|
|
|
1437
1518
|
"dist",
|
|
1438
1519
|
"commands",
|
|
1439
1520
|
"workflow",
|
|
1440
|
-
"
|
|
1521
|
+
"test.js"
|
|
1441
1522
|
]
|
|
1442
1523
|
},
|
|
1443
1524
|
"workflow:dataset:generate": {
|
|
@@ -1671,5 +1752,5 @@
|
|
|
1671
1752
|
]
|
|
1672
1753
|
}
|
|
1673
1754
|
},
|
|
1674
|
-
"version": "0.
|
|
1755
|
+
"version": "0.11.0"
|
|
1675
1756
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@outputai/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "CLI for Output.ai workflow generation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -27,20 +27,18 @@
|
|
|
27
27
|
"cli-table3": "0.6.5",
|
|
28
28
|
"date-fns": "4.1.0",
|
|
29
29
|
"debug": "4.4.3",
|
|
30
|
-
"dotenv": "17.4.2",
|
|
31
30
|
"handlebars": "4.7.9",
|
|
32
31
|
"ink": "7.0.1",
|
|
33
32
|
"ink-spinner": "5.0.0",
|
|
34
|
-
"js-yaml": "4.
|
|
33
|
+
"js-yaml": "4.3.1",
|
|
35
34
|
"json-schema-library": "11.4.0",
|
|
36
|
-
"ky": "
|
|
35
|
+
"ky": "2.0.2",
|
|
37
36
|
"react": "19.2.5",
|
|
38
37
|
"semver": "7.7.4",
|
|
39
|
-
"undici": "8.
|
|
40
|
-
"
|
|
41
|
-
"@outputai/
|
|
42
|
-
"@outputai/
|
|
43
|
-
"@outputai/llm": "0.10.1-next.f6a7c1a.0"
|
|
38
|
+
"undici": "8.9.0",
|
|
39
|
+
"@outputai/credentials": "0.11.0",
|
|
40
|
+
"@outputai/evals": "0.11.0",
|
|
41
|
+
"@outputai/llm": "0.11.0"
|
|
44
42
|
},
|
|
45
43
|
"devDependencies": {
|
|
46
44
|
"@types/cli-progress": "3.11.6",
|
|
File without changes
|