@outputai/cli 0.2.1-next.fd72d95.0 → 0.3.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.
Files changed (71) hide show
  1. package/bin/run.js +4 -2
  2. package/dist/api/generated/api.d.ts +160 -7
  3. package/dist/api/generated/api.js +33 -1
  4. package/dist/api/http_client.js +24 -19
  5. package/dist/assets/docker/docker-compose-dev.yml +5 -9
  6. package/dist/commands/dev/index.js +12 -1
  7. package/dist/commands/fix.js +1 -1
  8. package/dist/commands/fix.spec.js +2 -2
  9. package/dist/commands/init.d.ts +1 -0
  10. package/dist/commands/init.js +5 -1
  11. package/dist/commands/init.spec.js +10 -5
  12. package/dist/commands/update.js +1 -1
  13. package/dist/commands/update.spec.js +2 -2
  14. package/dist/commands/workflow/plan.js +5 -1
  15. package/dist/commands/workflow/plan.spec.js +3 -2
  16. package/dist/commands/workflow/run.d.ts +1 -1
  17. package/dist/commands/workflow/run.js +8 -5
  18. package/dist/commands/workflow/run.spec.js +3 -3
  19. package/dist/commands/workflow/runs/list.d.ts +1 -0
  20. package/dist/commands/workflow/runs/list.js +7 -0
  21. package/dist/commands/workflow/start.d.ts +1 -1
  22. package/dist/commands/workflow/start.js +8 -5
  23. package/dist/commands/workflow/start.spec.js +1 -1
  24. package/dist/config.d.ts +11 -38
  25. package/dist/config.js +34 -42
  26. package/dist/config.spec.d.ts +1 -0
  27. package/dist/config.spec.js +129 -0
  28. package/dist/generated/framework_version.json +1 -1
  29. package/dist/hooks/init.d.ts +4 -0
  30. package/dist/hooks/init.js +17 -1
  31. package/dist/hooks/init.spec.js +79 -5
  32. package/dist/services/coding_agents.js +5 -1
  33. package/dist/services/coding_agents.spec.js +19 -6
  34. package/dist/services/credentials_configurator.js +1 -1
  35. package/dist/services/docker.js +5 -2
  36. package/dist/services/docker.spec.js +74 -3
  37. package/dist/services/env_configurator.js +1 -1
  38. package/dist/services/env_configurator.spec.js +12 -12
  39. package/dist/services/messages.js +2 -1
  40. package/dist/services/project_scaffold.d.ts +1 -1
  41. package/dist/services/project_scaffold.js +17 -2
  42. package/dist/services/project_scaffold.spec.js +6 -6
  43. package/dist/services/workflow_builder.js +5 -1
  44. package/dist/services/workflow_builder.spec.js +3 -2
  45. package/dist/services/workflow_runs.d.ts +1 -0
  46. package/dist/services/workflow_runs.js +3 -0
  47. package/dist/templates/project/.env.example.template +17 -0
  48. package/dist/utils/credentials_loader.d.ts +1 -0
  49. package/dist/utils/credentials_loader.js +18 -0
  50. package/dist/utils/credentials_loader.spec.d.ts +1 -0
  51. package/dist/utils/credentials_loader.spec.js +84 -0
  52. package/dist/utils/env_loader.js +1 -2
  53. package/dist/utils/error_handler.js +10 -8
  54. package/dist/utils/interactive.d.ts +2 -0
  55. package/dist/utils/interactive.js +5 -0
  56. package/dist/utils/interactive.spec.d.ts +1 -0
  57. package/dist/utils/interactive.spec.js +40 -0
  58. package/dist/utils/prompt.d.ts +17 -0
  59. package/dist/utils/prompt.js +20 -0
  60. package/dist/utils/prompt.spec.d.ts +1 -0
  61. package/dist/utils/prompt.spec.js +70 -0
  62. package/dist/utils/proxy.d.ts +9 -0
  63. package/dist/utils/proxy.js +24 -0
  64. package/dist/utils/proxy.spec.d.ts +1 -0
  65. package/dist/utils/proxy.spec.js +48 -0
  66. package/dist/utils/validation.d.ts +13 -0
  67. package/dist/utils/validation.js +31 -0
  68. package/dist/utils/validation.spec.js +47 -1
  69. package/dist/views/dev.js +3 -3
  70. package/dist/views/workflow/list.js +10 -8
  71. package/package.json +10 -9
@@ -0,0 +1,40 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ describe('interactive', () => {
3
+ beforeEach(async () => {
4
+ // Re-import to reset singleton state
5
+ const mod = await import('./interactive.js');
6
+ mod.setNonInteractive(false);
7
+ });
8
+ it('isInteractive returns true by default when TTY is available', async () => {
9
+ const originalIsTTY = process.stdin.isTTY;
10
+ Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true });
11
+ const { isInteractive } = await import('./interactive.js');
12
+ expect(isInteractive()).toBe(true);
13
+ Object.defineProperty(process.stdin, 'isTTY', { value: originalIsTTY, configurable: true });
14
+ });
15
+ it('isInteractive returns false when no TTY', async () => {
16
+ const originalIsTTY = process.stdin.isTTY;
17
+ Object.defineProperty(process.stdin, 'isTTY', { value: undefined, configurable: true });
18
+ const { isInteractive } = await import('./interactive.js');
19
+ expect(isInteractive()).toBe(false);
20
+ Object.defineProperty(process.stdin, 'isTTY', { value: originalIsTTY, configurable: true });
21
+ });
22
+ it('isInteractive returns false after setNonInteractive(true)', async () => {
23
+ const originalIsTTY = process.stdin.isTTY;
24
+ Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true });
25
+ const { isInteractive, setNonInteractive } = await import('./interactive.js');
26
+ setNonInteractive(true);
27
+ expect(isInteractive()).toBe(false);
28
+ Object.defineProperty(process.stdin, 'isTTY', { value: originalIsTTY, configurable: true });
29
+ });
30
+ it('setNonInteractive(false) restores interactive mode', async () => {
31
+ const originalIsTTY = process.stdin.isTTY;
32
+ Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true });
33
+ const { isInteractive, setNonInteractive } = await import('./interactive.js');
34
+ setNonInteractive(true);
35
+ expect(isInteractive()).toBe(false);
36
+ setNonInteractive(false);
37
+ expect(isInteractive()).toBe(true);
38
+ Object.defineProperty(process.stdin, 'isTTY', { value: originalIsTTY, configurable: true });
39
+ });
40
+ });
@@ -0,0 +1,17 @@
1
+ type ConfirmOptions = {
2
+ message: string;
3
+ default?: boolean;
4
+ };
5
+ type InputOptions = {
6
+ message: string;
7
+ default?: string;
8
+ validate?: (value: string) => boolean | string;
9
+ };
10
+ type PasswordOptions = {
11
+ message: string;
12
+ mask?: boolean;
13
+ };
14
+ export declare const confirm: (options: ConfirmOptions) => Promise<boolean>;
15
+ export declare const input: (options: InputOptions) => Promise<string>;
16
+ export declare const password: (options: PasswordOptions) => Promise<string>;
17
+ export {};
@@ -0,0 +1,20 @@
1
+ import { confirm as inquirerConfirm, input as inquirerInput, password as inquirerPassword } from '@inquirer/prompts';
2
+ import { isInteractive } from './interactive.js';
3
+ export const confirm = async (options) => {
4
+ if (!isInteractive()) {
5
+ return true;
6
+ }
7
+ return inquirerConfirm(options);
8
+ };
9
+ export const input = async (options) => {
10
+ if (!isInteractive()) {
11
+ return options.default ?? '';
12
+ }
13
+ return inquirerInput(options);
14
+ };
15
+ export const password = async (options) => {
16
+ if (!isInteractive()) {
17
+ return '';
18
+ }
19
+ return inquirerPassword(options);
20
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,70 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { confirm as inquirerConfirm, input as inquirerInput, password as inquirerPassword } from '@inquirer/prompts';
3
+ vi.mock('@inquirer/prompts', () => ({
4
+ confirm: vi.fn(),
5
+ input: vi.fn(),
6
+ password: vi.fn()
7
+ }));
8
+ vi.mock('./interactive.js', () => ({
9
+ isInteractive: vi.fn()
10
+ }));
11
+ describe('prompt wrapper', () => {
12
+ beforeEach(() => {
13
+ vi.clearAllMocks();
14
+ });
15
+ describe('when interactive', () => {
16
+ beforeEach(async () => {
17
+ const { isInteractive } = await import('./interactive.js');
18
+ vi.mocked(isInteractive).mockReturnValue(true);
19
+ });
20
+ it('confirm delegates to inquirer', async () => {
21
+ vi.mocked(inquirerConfirm).mockResolvedValue(false);
22
+ const { confirm } = await import('./prompt.js');
23
+ const result = await confirm({ message: 'Continue?', default: true });
24
+ expect(inquirerConfirm).toHaveBeenCalledWith({ message: 'Continue?', default: true });
25
+ expect(result).toBe(false);
26
+ });
27
+ it('input delegates to inquirer', async () => {
28
+ vi.mocked(inquirerInput).mockResolvedValue('user input');
29
+ const { input } = await import('./prompt.js');
30
+ const result = await input({ message: 'Name?', default: 'default' });
31
+ expect(inquirerInput).toHaveBeenCalledWith({ message: 'Name?', default: 'default' });
32
+ expect(result).toBe('user input');
33
+ });
34
+ it('password delegates to inquirer', async () => {
35
+ vi.mocked(inquirerPassword).mockResolvedValue('secret');
36
+ const { password } = await import('./prompt.js');
37
+ const result = await password({ message: 'Token?' });
38
+ expect(inquirerPassword).toHaveBeenCalledWith({ message: 'Token?' });
39
+ expect(result).toBe('secret');
40
+ });
41
+ });
42
+ describe('when non-interactive', () => {
43
+ beforeEach(async () => {
44
+ const { isInteractive } = await import('./interactive.js');
45
+ vi.mocked(isInteractive).mockReturnValue(false);
46
+ });
47
+ it('confirm always returns true regardless of default', async () => {
48
+ const { confirm } = await import('./prompt.js');
49
+ expect(await confirm({ message: 'Continue?', default: false })).toBe(true);
50
+ expect(await confirm({ message: 'Continue?', default: true })).toBe(true);
51
+ expect(await confirm({ message: 'Continue?' })).toBe(true);
52
+ expect(inquirerConfirm).not.toHaveBeenCalled();
53
+ });
54
+ it('input returns default value', async () => {
55
+ const { input } = await import('./prompt.js');
56
+ expect(await input({ message: 'Name?', default: 'fallback' })).toBe('fallback');
57
+ expect(inquirerInput).not.toHaveBeenCalled();
58
+ });
59
+ it('input returns empty string when no default', async () => {
60
+ const { input } = await import('./prompt.js');
61
+ expect(await input({ message: 'Name?' })).toBe('');
62
+ expect(inquirerInput).not.toHaveBeenCalled();
63
+ });
64
+ it('password returns empty string', async () => {
65
+ const { password } = await import('./prompt.js');
66
+ expect(await password({ message: 'Token?' })).toBe('');
67
+ expect(inquirerPassword).not.toHaveBeenCalled();
68
+ });
69
+ });
70
+ });
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Routes all `fetch()` calls through an HTTP/HTTPS proxy when standard
3
+ * proxy env vars are set (`HTTPS_PROXY`, `https_proxy`, `HTTP_PROXY`,
4
+ * `http_proxy`). No-op when none are set. Invalid URLs are logged and
5
+ * skipped so the CLI keeps running.
6
+ *
7
+ * Call once at CLI startup, before any network activity.
8
+ */
9
+ export declare const bootstrapProxy: () => void;
@@ -0,0 +1,24 @@
1
+ import { EnvHttpProxyAgent, setGlobalDispatcher } from 'undici';
2
+ /**
3
+ * Routes all `fetch()` calls through an HTTP/HTTPS proxy when standard
4
+ * proxy env vars are set (`HTTPS_PROXY`, `https_proxy`, `HTTP_PROXY`,
5
+ * `http_proxy`). No-op when none are set. Invalid URLs are logged and
6
+ * skipped so the CLI keeps running.
7
+ *
8
+ * Call once at CLI startup, before any network activity.
9
+ */
10
+ export const bootstrapProxy = () => {
11
+ const proxyUrl = process.env.HTTPS_PROXY || process.env.https_proxy ||
12
+ process.env.HTTP_PROXY || process.env.http_proxy;
13
+ if (!proxyUrl) {
14
+ return;
15
+ }
16
+ try {
17
+ new URL(proxyUrl);
18
+ }
19
+ catch {
20
+ console.warn(`[proxy] Ignoring invalid proxy URL: ${proxyUrl}`);
21
+ return;
22
+ }
23
+ setGlobalDispatcher(new EnvHttpProxyAgent());
24
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,48 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ const mockSetGlobalDispatcher = vi.fn();
3
+ const MockEnvHttpProxyAgent = vi.fn();
4
+ vi.mock('undici', () => ({
5
+ EnvHttpProxyAgent: MockEnvHttpProxyAgent,
6
+ setGlobalDispatcher: mockSetGlobalDispatcher
7
+ }));
8
+ describe('proxy bootstrap', () => {
9
+ const originalEnv = { ...process.env };
10
+ beforeEach(() => {
11
+ vi.clearAllMocks();
12
+ delete process.env.HTTPS_PROXY;
13
+ delete process.env.https_proxy;
14
+ delete process.env.HTTP_PROXY;
15
+ delete process.env.http_proxy;
16
+ });
17
+ afterEach(() => {
18
+ process.env = { ...originalEnv };
19
+ });
20
+ it('does nothing when no proxy env vars are set', async () => {
21
+ const { bootstrapProxy } = await import('./proxy.js');
22
+ bootstrapProxy();
23
+ expect(mockSetGlobalDispatcher).not.toHaveBeenCalled();
24
+ });
25
+ it('sets global dispatcher when HTTPS_PROXY is set', async () => {
26
+ process.env.HTTPS_PROXY = 'http://proxy:8080';
27
+ const { bootstrapProxy } = await import('./proxy.js');
28
+ bootstrapProxy();
29
+ expect(MockEnvHttpProxyAgent).toHaveBeenCalled();
30
+ expect(mockSetGlobalDispatcher).toHaveBeenCalledTimes(1);
31
+ });
32
+ it('sets global dispatcher when HTTP_PROXY is set', async () => {
33
+ process.env.HTTP_PROXY = 'http://proxy:8080';
34
+ const { bootstrapProxy } = await import('./proxy.js');
35
+ bootstrapProxy();
36
+ expect(MockEnvHttpProxyAgent).toHaveBeenCalled();
37
+ expect(mockSetGlobalDispatcher).toHaveBeenCalledTimes(1);
38
+ });
39
+ it('does not set global dispatcher when proxy URL is malformed', async () => {
40
+ process.env.HTTPS_PROXY = 'not a url';
41
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
42
+ const { bootstrapProxy } = await import('./proxy.js');
43
+ bootstrapProxy();
44
+ expect(mockSetGlobalDispatcher).not.toHaveBeenCalled();
45
+ expect(warnSpy).toHaveBeenCalled();
46
+ warnSpy.mockRestore();
47
+ });
48
+ });
@@ -11,3 +11,16 @@ export declare function validateWorkflowName(name: string): void;
11
11
  * Validate that a directory path is safe to create
12
12
  */
13
13
  export declare function validateOutputDirectory(outputDir: string): void;
14
+ export declare class InvalidPortError extends Error {
15
+ constructor(envVarName: string, raw: string, reason: string);
16
+ }
17
+ /**
18
+ * Parse a port number from an env var. Empty string and undefined fall back to
19
+ * the default silently (matching Compose's `${VAR:-default}` semantics).
20
+ * Throws InvalidPortError on anything else: non-numeric, signed, decimal,
21
+ * trailing junk (e.g. "3001abc"), or out of range 1-65535. Throwing (vs
22
+ * warn-and-fallback) prevents CLI/Docker disagreement: Compose reads the same
23
+ * env var via `${VAR:-default}` and uses its own parser, so a CLI fallback
24
+ * would silently desync from the bound port.
25
+ */
26
+ export declare function parsePort(raw: string | undefined, defaultPort: number, envVarName: string): number;
@@ -23,3 +23,34 @@ export function validateOutputDirectory(outputDir) {
23
23
  throw new InvalidOutputDirectoryError(outputDir, 'Output directory cannot be empty');
24
24
  }
25
25
  }
26
+ const MIN_PORT = 1;
27
+ const MAX_PORT = 65535;
28
+ export class InvalidPortError extends Error {
29
+ constructor(envVarName, raw, reason) {
30
+ super(`${envVarName}=${raw} is invalid (${reason}). ` +
31
+ `Set a port in ${MIN_PORT}-${MAX_PORT} in your .env file, or unset the variable to use the default.`);
32
+ this.name = 'InvalidPortError';
33
+ }
34
+ }
35
+ /**
36
+ * Parse a port number from an env var. Empty string and undefined fall back to
37
+ * the default silently (matching Compose's `${VAR:-default}` semantics).
38
+ * Throws InvalidPortError on anything else: non-numeric, signed, decimal,
39
+ * trailing junk (e.g. "3001abc"), or out of range 1-65535. Throwing (vs
40
+ * warn-and-fallback) prevents CLI/Docker disagreement: Compose reads the same
41
+ * env var via `${VAR:-default}` and uses its own parser, so a CLI fallback
42
+ * would silently desync from the bound port.
43
+ */
44
+ export function parsePort(raw, defaultPort, envVarName) {
45
+ if (raw === undefined || raw === '') {
46
+ return defaultPort;
47
+ }
48
+ if (!/^\d+$/.test(raw)) {
49
+ throw new InvalidPortError(envVarName, raw, 'not a positive integer');
50
+ }
51
+ const n = parseInt(raw, 10);
52
+ if (n < MIN_PORT || n > MAX_PORT) {
53
+ throw new InvalidPortError(envVarName, raw, `out of range ${MIN_PORT}-${MAX_PORT}`);
54
+ }
55
+ return n;
56
+ }
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { isValidWorkflowName } from './validation.js';
2
+ import { isValidWorkflowName, parsePort, InvalidPortError } from './validation.js';
3
3
  describe('isValidWorkflowName', () => {
4
4
  describe('valid workflow names', () => {
5
5
  it('should accept single letter', () => {
@@ -138,3 +138,49 @@ describe('isValidWorkflowName', () => {
138
138
  });
139
139
  });
140
140
  });
141
+ describe('parsePort', () => {
142
+ it('returns the parsed value for a valid port', () => {
143
+ expect(parsePort('3001', 3001, 'P')).toBe(3001);
144
+ expect(parsePort('7234', 7233, 'P')).toBe(7234);
145
+ });
146
+ it('falls back to default for undefined silently', () => {
147
+ expect(parsePort(undefined, 3001, 'P')).toBe(3001);
148
+ });
149
+ it('falls back to default for empty string silently (matches Compose ${VAR:-default})', () => {
150
+ expect(parsePort('', 3001, 'P')).toBe(3001);
151
+ });
152
+ it('throws InvalidPortError for non-numeric input', () => {
153
+ expect(() => parsePort('abc', 3001, 'OUTPUT_API_HOST_PORT')).toThrow(InvalidPortError);
154
+ expect(() => parsePort('abc', 3001, 'OUTPUT_API_HOST_PORT'))
155
+ .toThrow(/OUTPUT_API_HOST_PORT=abc/);
156
+ });
157
+ it('throws for trailing junk (parseInt would silently truncate)', () => {
158
+ expect(() => parsePort('3001abc', 3001, 'P')).toThrow(InvalidPortError);
159
+ });
160
+ it('throws for negative input', () => {
161
+ expect(() => parsePort('-1', 3001, 'P')).toThrow(InvalidPortError);
162
+ });
163
+ it('throws for port 0 (CLI rejects but Compose would treat as ephemeral - prevents desync)', () => {
164
+ expect(() => parsePort('0', 3001, 'P')).toThrow(InvalidPortError);
165
+ });
166
+ it('throws for port above 65535', () => {
167
+ expect(() => parsePort('65536', 3001, 'P')).toThrow(InvalidPortError);
168
+ expect(() => parsePort('99999', 3001, 'P')).toThrow(InvalidPortError);
169
+ });
170
+ it('accepts boundary ports 1 and 65535', () => {
171
+ expect(parsePort('1', 3001, 'P')).toBe(1);
172
+ expect(parsePort('65535', 3001, 'P')).toBe(65535);
173
+ });
174
+ it('error message includes env var name and remediation hint', () => {
175
+ try {
176
+ parsePort('abc', 3001, 'OUTPUT_API_HOST_PORT');
177
+ expect.fail('expected throw');
178
+ }
179
+ catch (err) {
180
+ expect(err).toBeInstanceOf(InvalidPortError);
181
+ expect(err.message).toContain('OUTPUT_API_HOST_PORT=abc');
182
+ expect(err.message).toContain('1-65535');
183
+ expect(err.message).toContain('.env file');
184
+ }
185
+ });
186
+ });
package/dist/views/dev.js CHANGED
@@ -138,10 +138,10 @@ const DevSuccessMessage = ({ services }) => {
138
138
  const divider = '─'.repeat(80);
139
139
  const sortedNames = services.map(s => s.name).sort().join('|');
140
140
  const logsCommand = `docker compose -p ${config.dockerServiceName} logs -f <${sortedNames}>`;
141
- return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsx(Text, { dimColor: true, children: divider }) }), _jsxs(Box, { children: [_jsx(Text, { color: "green", bold: true, children: '✅ SUCCESS! ' }), _jsx(Text, { bold: true, children: "Development services are running" })] }), _jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsx(Text, { dimColor: true, children: divider }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "\uD83D\uDC33 SERVICES" }) }), _jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Box, { children: [_jsx(Text, { color: "white", children: 'Temporal: ' }), _jsx(Text, { color: "yellow", children: "localhost:7233" })] }), _jsxs(Box, { children: [_jsx(Text, { color: "white", children: 'Temporal UI: ' }), _jsx(Text, { color: "cyan", children: "http://localhost:8080" })] }), _jsxs(Box, { children: [_jsx(Text, { color: "white", children: 'API Server: ' }), _jsx(Text, { color: "yellow", children: "localhost:3001" })] }), _jsxs(Box, { children: [_jsx(Text, { color: "white", children: 'Redis: ' }), _jsx(Text, { color: "yellow", children: "localhost:6379" })] })] }), _jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsx(Text, { dimColor: true, children: divider }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "\uD83D\uDE80 RUN A WORKFLOW" }) }), _jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsx(Text, { color: "white", children: "In a new terminal, execute:" }), _jsx(Box, { marginLeft: 2, children: _jsx(Text, { color: "cyan", children: "npx output workflow run blog_evaluator paulgraham_hwh" }) })] }), _jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsx(Text, { dimColor: true, children: divider }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "\u26A1 USEFUL COMMANDS" }) }), _jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Box, { children: [_jsx(Text, { color: "white", children: 'Open Temporal UI: ' }), _jsx(Text, { color: "cyan", children: "open http://localhost:8080" })] }), _jsxs(Box, { children: [_jsx(Text, { color: "white", children: 'View logs: ' }), _jsx(Text, { color: "cyan", children: logsCommand })] }), _jsxs(Box, { children: [_jsx(Text, { color: "white", children: 'Stop services: ' }), _jsx(Text, { color: "cyan", children: "Press Ctrl+C" })] })] }), _jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsx(Text, { dimColor: true, children: divider }) }), _jsx(Text, { dimColor: true, children: "\uD83D\uDCA1 Tip: The Temporal UI lets you monitor workflow executions in real-time" })] }));
141
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsx(Text, { dimColor: true, children: divider }) }), _jsxs(Box, { children: [_jsx(Text, { color: "green", bold: true, children: '✅ SUCCESS! ' }), _jsx(Text, { bold: true, children: "Development services are running" })] }), _jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsx(Text, { dimColor: true, children: divider }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "\uD83D\uDC33 SERVICES" }) }), _jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Box, { children: [_jsx(Text, { color: "white", children: 'Temporal UI: ' }), _jsx(Text, { color: "cyan", children: config.temporalUiUrl })] }), _jsxs(Box, { children: [_jsx(Text, { color: "white", children: 'API Server: ' }), _jsxs(Text, { color: "yellow", children: ["localhost:", config.ports.api] })] })] }), _jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsx(Text, { dimColor: true, children: divider }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "\uD83D\uDE80 RUN A WORKFLOW" }) }), _jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsx(Text, { color: "white", children: "In a new terminal, execute:" }), _jsx(Box, { marginLeft: 2, children: _jsx(Text, { color: "cyan", children: "npx output workflow run blog_evaluator paulgraham_hwh" }) })] }), _jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsx(Text, { dimColor: true, children: divider }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "\u26A1 USEFUL COMMANDS" }) }), _jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Box, { children: [_jsx(Text, { color: "white", children: 'Open Temporal UI: ' }), _jsxs(Text, { color: "cyan", children: ["open ", config.temporalUiUrl] })] }), _jsxs(Box, { children: [_jsx(Text, { color: "white", children: 'View logs: ' }), _jsx(Text, { color: "cyan", children: logsCommand })] }), _jsxs(Box, { children: [_jsx(Text, { color: "white", children: 'Stop services: ' }), _jsx(Text, { color: "cyan", children: "Press Ctrl+C" })] })] }), _jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsx(Text, { dimColor: true, children: divider }) }), _jsx(Text, { dimColor: true, children: "\uD83D\uDCA1 Tip: The Temporal UI lets you monitor workflow executions in real-time" })] }));
142
142
  };
143
143
  const WaitingView = ({ services }) => (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Waiting for services to become healthy..." })] }), services.length > 0 && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: services.map(s => _jsx(ServiceRow, { service: s }, s.name)) }))] }));
144
- const RunningView = ({ services, workflowSummary }) => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "\uD83D\uDCCA Service Status" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: services.map(s => _jsx(ServiceRow, { service: s }, s.name)) }), _jsx(FailureWarning, { services: services }), workflowSummary && _jsx(WorkflowSummarySection, { summary: workflowSummary }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "cyan", children: '🌐 Temporal UI: ' }), _jsx(Text, { bold: true, children: "http://localhost:8080" })] }), _jsx(CommandFooter, { hints: [
144
+ const RunningView = ({ services, workflowSummary }) => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "\uD83D\uDCCA Service Status" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: services.map(s => _jsx(ServiceRow, { service: s }, s.name)) }), _jsx(FailureWarning, { services: services }), workflowSummary && _jsx(WorkflowSummarySection, { summary: workflowSummary }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "cyan", children: '🌐 Temporal UI: ' }), _jsx(Text, { bold: true, children: config.temporalUiUrl })] }), _jsx(CommandFooter, { hints: [
145
145
  { key: 'o', label: 'open ui' },
146
146
  { key: 'w', label: 'view workflow runs' },
147
147
  { key: 'ctrl+c', label: 'stop' }
@@ -173,7 +173,7 @@ export const DevApp = ({ dockerComposePath, onCleanup }) => {
173
173
  useStatusRefresh(dockerComposePath, phase === 'running', setServices);
174
174
  useWorkflowPolling(phase === 'running' || phase === 'failed', setWorkflowRuns);
175
175
  useMainViewInput(activeView === 'main' && phase !== 'waiting', {
176
- onOpenTemporal: () => openUrl('http://localhost:8080'),
176
+ onOpenTemporal: () => openUrl(config.temporalUiUrl),
177
177
  onOpenWorkflows: () => setActiveView('workflows')
178
178
  });
179
179
  useCtrlC(onCleanup);
@@ -2,12 +2,12 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState, useEffect, useRef, useMemo } from 'react';
3
3
  import { Box, Text, useInput } from 'ink';
4
4
  import Spinner from 'ink-spinner';
5
- import { getWorkflowIdResult } from '#api/generated/api.js';
5
+ import { getWorkflowIdRunsRidResult } from '#api/generated/api.js';
6
6
  import { StatusIcon, statusColor } from '#components/status_icon.js';
7
7
  import { elapsedMs, formatDurationCompact } from '#utils/date_formatter.js';
8
8
  import { CommandFooter } from '#components/command_footer.js';
9
9
  import { openUrl } from '#utils/open_url.js';
10
- const TEMPORAL_UI_BASE = 'http://localhost:8080';
10
+ import { config } from '#config.js';
11
11
  const VISIBLE_ROWS = 15;
12
12
  const STATUS_ORDER = {
13
13
  running: 0,
@@ -59,17 +59,19 @@ export const WorkflowListView = ({ runs, onBack }) => {
59
59
  const clampedIndex = Math.min(selectedIndex, Math.max(0, sortedRuns.length - 1));
60
60
  const selectedRun = sortedRuns[clampedIndex];
61
61
  const selectedWorkflowId = selectedRun?.workflowId;
62
+ const selectedRunId = selectedRun?.runId;
62
63
  useEffect(() => {
63
64
  if (clampedIndex !== selectedIndex) {
64
65
  setSelectedIndex(clampedIndex);
65
66
  }
66
67
  }, [clampedIndex, selectedIndex]);
67
68
  useEffect(() => {
68
- if (!selectedWorkflowId) {
69
+ if (!selectedWorkflowId || !selectedRunId) {
69
70
  setDetail(null);
70
71
  return;
71
72
  }
72
- const cached = cacheRef.current.get(selectedWorkflowId);
73
+ const cacheKey = `${selectedWorkflowId}:${selectedRunId}`;
74
+ const cached = cacheRef.current.get(cacheKey);
73
75
  if (cached) {
74
76
  setDetail(cached);
75
77
  setDetailLoading(false);
@@ -77,13 +79,13 @@ export const WorkflowListView = ({ runs, onBack }) => {
77
79
  }
78
80
  const currentFetchId = ++fetchIdRef.current;
79
81
  setDetailLoading(true);
80
- getWorkflowIdResult(selectedWorkflowId)
82
+ getWorkflowIdRunsRidResult(selectedWorkflowId, selectedRunId)
81
83
  .then(response => {
82
84
  if (fetchIdRef.current !== currentFetchId) {
83
85
  return;
84
86
  }
85
87
  const data = response.data;
86
- cacheRef.current.set(selectedWorkflowId, data);
88
+ cacheRef.current.set(cacheKey, data);
87
89
  setDetail(data);
88
90
  setDetailLoading(false);
89
91
  })
@@ -94,7 +96,7 @@ export const WorkflowListView = ({ runs, onBack }) => {
94
96
  setDetail(null);
95
97
  setDetailLoading(false);
96
98
  });
97
- }, [selectedWorkflowId]);
99
+ }, [selectedWorkflowId, selectedRunId]);
98
100
  useInput((input, key) => {
99
101
  if (key.upArrow) {
100
102
  setSelectedIndex(i => Math.max(0, i - 1));
@@ -106,7 +108,7 @@ export const WorkflowListView = ({ runs, onBack }) => {
106
108
  onBack();
107
109
  }
108
110
  else if (input === 'o' && selectedWorkflowId) {
109
- openUrl(`${TEMPORAL_UI_BASE}/namespaces/default/workflows/${selectedWorkflowId}`);
111
+ openUrl(`${config.temporalUiUrl}/namespaces/default/workflows/${selectedWorkflowId}`);
110
112
  }
111
113
  });
112
114
  const windowStart = useMemo(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@outputai/cli",
3
- "version": "0.2.1-next.fd72d95.0",
3
+ "version": "0.3.0",
4
4
  "description": "CLI for Output.ai workflow generation",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -15,11 +15,11 @@
15
15
  },
16
16
  "dependencies": {
17
17
  "@anthropic-ai/claude-agent-sdk": "0.2.92",
18
- "@aws-sdk/client-s3": "3.1031.0",
18
+ "@aws-sdk/client-s3": "3.1038.0",
19
19
  "@hackylabs/deep-redact": "3.0.5",
20
- "@inquirer/prompts": "8.4.1",
21
- "@oclif/core": "4.10.5",
22
- "@oclif/plugin-help": "6.2.44",
20
+ "@inquirer/prompts": "8.4.2",
21
+ "@oclif/core": "4.10.6",
22
+ "@oclif/plugin-help": "6.2.45",
23
23
  "change-case": "5.4.4",
24
24
  "cli-progress": "3.12.0",
25
25
  "cli-table3": "0.6.5",
@@ -34,10 +34,11 @@
34
34
  "ky": "1.14.3",
35
35
  "react": "19.2.5",
36
36
  "semver": "7.7.4",
37
+ "undici": "8.1.0",
37
38
  "yaml": "^2.8.3",
38
- "@outputai/credentials": "0.2.1-next.fd72d95.0",
39
- "@outputai/evals": "0.2.1-next.fd72d95.0",
40
- "@outputai/llm": "0.2.1-next.fd72d95.0"
39
+ "@outputai/credentials": "0.3.0",
40
+ "@outputai/llm": "0.3.0",
41
+ "@outputai/evals": "0.3.0"
41
42
  },
42
43
  "devDependencies": {
43
44
  "@types/cli-progress": "3.11.6",
@@ -45,7 +46,7 @@
45
46
  "@types/js-yaml": "4.0.9",
46
47
  "@types/react": "19.2.14",
47
48
  "@types/semver": "7.7.1",
48
- "orval": "8.8.0"
49
+ "orval": "8.9.0"
49
50
  },
50
51
  "license": "Apache-2.0",
51
52
  "publishConfig": {