@outputai/cli 0.10.1-dev.b7b2fbe.0 → 0.10.1-next.2caa4a1.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 (63) hide show
  1. package/dist/api/generated/api.d.ts +12 -0
  2. package/dist/api/http_client.js +2 -2
  3. package/dist/assets/docker/docker-compose-dev.yml +1 -1
  4. package/dist/commands/dev/down.d.ts +10 -0
  5. package/dist/commands/dev/down.js +34 -0
  6. package/dist/commands/dev/down.spec.d.ts +1 -0
  7. package/dist/commands/dev/down.spec.js +71 -0
  8. package/dist/commands/dev/index.d.ts +4 -0
  9. package/dist/commands/dev/index.js +200 -53
  10. package/dist/commands/dev/index.spec.js +390 -42
  11. package/dist/commands/workflow/history.js +3 -3
  12. package/dist/commands/workflow/history.spec.js +31 -2
  13. package/dist/commands/workflow/monitor.d.ts +49 -0
  14. package/dist/commands/workflow/monitor.js +230 -0
  15. package/dist/commands/workflow/monitor.spec.d.ts +1 -0
  16. package/dist/commands/workflow/monitor.spec.js +243 -0
  17. package/dist/commands/workflow/run.js +8 -1
  18. package/dist/commands/workflow/run.spec.js +12 -2
  19. package/dist/commands/workflow/start.d.ts +3 -1
  20. package/dist/commands/workflow/start.js +12 -2
  21. package/dist/commands/workflow/start.spec.js +30 -5
  22. package/dist/generated/framework_version.json +1 -1
  23. package/dist/services/docker.d.ts +28 -1
  24. package/dist/services/docker.js +106 -12
  25. package/dist/services/docker.spec.js +144 -14
  26. package/dist/services/workflow_history/correlator.d.ts +2 -0
  27. package/dist/services/workflow_history/correlator.js +2 -2
  28. package/dist/services/workflow_history.d.ts +28 -0
  29. package/dist/services/workflow_history.js +95 -12
  30. package/dist/services/workflow_history.spec.js +183 -1
  31. package/dist/templates/agent_instructions/CLAUDE.md.template +1 -1
  32. package/dist/templates/project/src/clients/jina.ts.template +4 -4
  33. package/dist/utils/color.d.ts +7 -0
  34. package/dist/utils/color.js +12 -0
  35. package/dist/utils/color.spec.d.ts +1 -0
  36. package/dist/utils/color.spec.js +43 -0
  37. package/dist/utils/format_workflow_result.d.ts +1 -0
  38. package/dist/utils/format_workflow_result.js +4 -0
  39. package/dist/utils/monitor_log.d.ts +20 -0
  40. package/dist/utils/monitor_log.js +48 -0
  41. package/dist/utils/monitor_log.spec.d.ts +1 -0
  42. package/dist/utils/monitor_log.spec.js +71 -0
  43. package/dist/utils/port_collision.d.ts +22 -7
  44. package/dist/utils/port_collision.js +39 -14
  45. package/dist/utils/port_collision.spec.js +40 -1
  46. package/dist/utils/resolve_input.d.ts +9 -1
  47. package/dist/utils/resolve_input.js +8 -2
  48. package/dist/utils/resolve_input.spec.d.ts +1 -0
  49. package/dist/utils/resolve_input.spec.js +75 -0
  50. package/dist/utils/waterfall.d.ts +3 -1
  51. package/dist/utils/waterfall.js +8 -2
  52. package/dist/views/dev/chrome/footer.d.ts +2 -0
  53. package/dist/views/dev/chrome/footer.js +4 -4
  54. package/dist/views/dev/dev_app.d.ts +1 -0
  55. package/dist/views/dev/dev_app.js +13 -4
  56. package/dist/views/dev/hooks/use_run_detail.js +7 -8
  57. package/dist/views/dev/hooks/use_step_graph.js +3 -1
  58. package/dist/views/dev/utils/bounded_cache.d.ts +14 -0
  59. package/dist/views/dev/utils/bounded_cache.js +42 -0
  60. package/dist/views/dev/utils/bounded_cache.spec.d.ts +1 -0
  61. package/dist/views/dev/utils/bounded_cache.spec.js +53 -0
  62. package/oclif.manifest.json +122 -4
  63. package/package.json +7 -8
@@ -1 +1,9 @@
1
- export declare function resolveInput(workflowName: string, scenario: string | undefined, inputFlag: string | undefined, commandName: string, catalog?: string): Promise<unknown>;
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(workflowName, scenario, inputFlag, commandName, catalog) {
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
- ux.stdout(`Using scenario: ${resolution.path}\n`);
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
+ });
@@ -8,7 +8,7 @@
8
8
  * Atlas's `StepGantt`; the bar geometry mirrors its leftPct/widthPct as integer
9
9
  * terminal columns.
10
10
  */
11
- import type { Span } from '#services/workflow_history/correlator.js';
11
+ import type { Span, SpanStatus } from '#services/workflow_history/correlator.js';
12
12
  export interface WaterfallOptions {
13
13
  width: number;
14
14
  color: boolean;
@@ -22,6 +22,8 @@ export interface BarGeometry {
22
22
  }
23
23
  export declare const FULL_BLOCK = "\u2588";
24
24
  export declare const THIN_BLOCK = "\u258F";
25
+ export declare const ANSI: Record<SpanStatus | 'dim' | 'reset', string>;
26
+ export declare function makeTint(color: boolean): (text: string, code: string) => string;
25
27
  export declare function pickTickStep(totalMs: number): number;
26
28
  export declare function buildTicks(totalMs: number): number[];
27
29
  export declare function formatTickLabel(ms: number): string;
@@ -12,7 +12,7 @@ const TICK_STEPS_MS = [
12
12
  60 * 60_000, 2 * 60 * 60_000, 6 * 60 * 60_000, 12 * 60 * 60_000, 24 * 60 * 60_000
13
13
  ];
14
14
  const TARGET_TICKS = 8;
15
- const ANSI = {
15
+ export const ANSI = {
16
16
  completed: '',
17
17
  running: '',
18
18
  failed: '',
@@ -20,6 +20,12 @@ const ANSI = {
20
20
  dim: '',
21
21
  reset: ''
22
22
  };
23
+ // Shared by renderWaterfall and monitor_log's live status lines so there's one
24
+ // place that knows how to wrap text in an ANSI code (and reset it) -- or not,
25
+ // when color is disabled.
26
+ export function makeTint(color) {
27
+ return (text, code) => (color ? `${code}${text}${ANSI.reset}` : text);
28
+ }
23
29
  function clamp(value, lo, hi) {
24
30
  return Math.min(Math.max(value, lo), hi);
25
31
  }
@@ -162,7 +168,7 @@ export default function renderWaterfall(spans, totalDurationMs, options) {
162
168
  if (spans.length === 0) {
163
169
  return [header, 'No steps found for this run.'].filter(Boolean).join('\n\n');
164
170
  }
165
- const tint = (text, code) => (color ? `${code}${text}${ANSI.reset}` : text);
171
+ const tint = makeTint(color);
166
172
  const labelFor = (span) => labels?.get(span.id) ?? span.name;
167
173
  const durationFor = (span) => formatDurationLabel(Math.max(0, span.durationMs));
168
174
  const longestLabel = Math.max(...spans.map(s => labelFor(s).length));
@@ -8,5 +8,7 @@ export interface FooterState {
8
8
  hints?: CommandHint[];
9
9
  itemCount?: number;
10
10
  itemLabel?: string;
11
+ /** Attached to a pre-existing stack — quitting leaves services running. */
12
+ attached?: boolean;
11
13
  }
12
14
  export declare const Footer: React.FC<FooterState>;
@@ -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 GLOBAL_HINTS = [
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: GLOBAL_HINTS }), 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] })] })] }));
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
  };
@@ -3,4 +3,5 @@ export type Phase = 'waiting' | 'running' | 'failed';
3
3
  export declare const DevApp: React.FC<{
4
4
  dockerComposePath: string;
5
5
  onCleanup: () => Promise<void>;
6
+ attached?: boolean;
6
7
  }>;
@@ -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
- onTimeout: () => exit(new Error('Timeout waiting for services to become healthy'))
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,13 +2,16 @@ 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 { TERMINAL_STATUSES } from '#utils/format_workflow_result.js';
6
+ import { createBoundedCache } from '#views/dev/utils/bounded_cache.js';
5
7
  const EMPTY_DETAIL = {
6
8
  result: null,
7
9
  trace: null,
8
10
  steps: [],
9
11
  loading: false
10
12
  };
11
- const runDetailCache = new Map();
13
+ const RUN_DETAIL_CACHE_MAX = 50;
14
+ const runDetailCache = createBoundedCache(RUN_DETAIL_CACHE_MAX);
12
15
  const stepNameOf = (node) => {
13
16
  if (node.name) {
14
17
  return node.name;
@@ -99,13 +102,9 @@ const fetchResult = async (workflowId, runId) => {
99
102
  return null;
100
103
  }
101
104
  };
102
- /**
103
- * Statuses that mean the workflow has stopped advancing. The cache is
104
- * intentionally only populated for these partial results from a still-
105
- * running workflow would otherwise stick and stall the UI when the run
106
- * eventually finishes.
107
- */
108
- const TERMINAL_STATUSES = new Set(['completed', 'failed', 'canceled', 'terminated', 'timed_out']);
105
+ // The cache is intentionally only populated for terminal statuses — partial results
106
+ // from a still-running workflow would otherwise stick and stall the UI when the run
107
+ // eventually finishes. `TERMINAL_STATUSES` is shared with `workflow monitor`.
109
108
  export const isTerminalRunStatus = (status) => Boolean(status && TERMINAL_STATUSES.has(status));
110
109
  export const useRunDetail = (workflowId, runId, status) => {
111
110
  const [detail, setDetail] = useState(EMPTY_DETAIL);
@@ -3,6 +3,7 @@ import { fetchWorkflowHistory } from '#services/workflow_history.js';
3
3
  import buildSpanLabels from '#utils/span_labels.js';
4
4
  import { isTerminalRunStatus } from '#views/dev/hooks/use_run_detail.js';
5
5
  import { usePoll, POLL_INTERVAL_MS } from '#views/dev/hooks/use_poll.js';
6
+ import { createBoundedCache } from '#views/dev/utils/bounded_cache.js';
6
7
  const EMPTY_GRAPH = {
7
8
  spans: [],
8
9
  totalDurationMs: 0,
@@ -11,7 +12,8 @@ const EMPTY_GRAPH = {
11
12
  loading: false,
12
13
  error: null
13
14
  };
14
- const stepGraphCache = new Map();
15
+ const STEP_GRAPH_CACHE_MAX = 50;
16
+ const stepGraphCache = createBoundedCache(STEP_GRAPH_CACHE_MAX);
15
17
  /**
16
18
  * Fetches a run's correlated step spans for the dev TUI's waterfall overlay,
17
19
  * reusing the same `fetchWorkflowHistory` path as the `workflow history` CLI
@@ -0,0 +1,14 @@
1
+ export interface BoundedCache<K, V extends {}> {
2
+ get(key: K): V | undefined;
3
+ set(key: K, value: V): void;
4
+ has(key: K): boolean;
5
+ clear(): void;
6
+ size(): number;
7
+ }
8
+ /**
9
+ * A small LRU cache backed by an insertion-ordered `Map`, capped at `maxSize`
10
+ * entries. Reading a key refreshes its recency; once the cap is exceeded the
11
+ * least-recently-used entries are evicted. Drop-in compatible with the
12
+ * `Map.get` / `Map.set` calls it replaces.
13
+ */
14
+ export declare const createBoundedCache: <K, V extends {}>(maxSize: number) => BoundedCache<K, V>;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * A small LRU cache backed by an insertion-ordered `Map`, capped at `maxSize`
3
+ * entries. Reading a key refreshes its recency; once the cap is exceeded the
4
+ * least-recently-used entries are evicted. Drop-in compatible with the
5
+ * `Map.get` / `Map.set` calls it replaces.
6
+ */
7
+ export const createBoundedCache = (maxSize) => {
8
+ if (maxSize < 1) {
9
+ throw new Error('createBoundedCache: maxSize must be >= 1');
10
+ }
11
+ const entries = new Map();
12
+ return {
13
+ get(key) {
14
+ const value = entries.get(key);
15
+ if (value === undefined) {
16
+ return value;
17
+ }
18
+ entries.delete(key);
19
+ entries.set(key, value);
20
+ return value;
21
+ },
22
+ set(key, value) {
23
+ entries.delete(key);
24
+ entries.set(key, value);
25
+ if (entries.size > maxSize) {
26
+ const oldest = entries.keys().next();
27
+ if (!oldest.done) {
28
+ entries.delete(oldest.value);
29
+ }
30
+ }
31
+ },
32
+ has(key) {
33
+ return entries.has(key);
34
+ },
35
+ clear() {
36
+ entries.clear();
37
+ },
38
+ size() {
39
+ return entries.size;
40
+ }
41
+ };
42
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,53 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { createBoundedCache } from './bounded_cache.js';
3
+ describe('createBoundedCache', () => {
4
+ it('round-trips set and get', () => {
5
+ const cache = createBoundedCache(3);
6
+ cache.set('a', 1);
7
+ expect(cache.get('a')).toBe(1);
8
+ expect(cache.get('missing')).toBeUndefined();
9
+ });
10
+ it('evicts the oldest entry once maxSize is exceeded', () => {
11
+ const cache = createBoundedCache(2);
12
+ cache.set('a', 1);
13
+ cache.set('b', 2);
14
+ cache.set('c', 3);
15
+ expect(cache.has('a')).toBe(false);
16
+ expect(cache.get('b')).toBe(2);
17
+ expect(cache.get('c')).toBe(3);
18
+ });
19
+ it('refreshes recency on get so the touched entry survives eviction', () => {
20
+ const cache = createBoundedCache(2);
21
+ cache.set('a', 1);
22
+ cache.set('b', 2);
23
+ // Touch 'a' so 'b' becomes the least-recently-used entry.
24
+ expect(cache.get('a')).toBe(1);
25
+ cache.set('c', 3);
26
+ expect(cache.has('a')).toBe(true);
27
+ expect(cache.has('b')).toBe(false);
28
+ expect(cache.has('c')).toBe(true);
29
+ });
30
+ it('updates an existing key in place without growing', () => {
31
+ const cache = createBoundedCache(2);
32
+ cache.set('a', 1);
33
+ cache.set('a', 2);
34
+ expect(cache.get('a')).toBe(2);
35
+ expect(cache.size()).toBe(1);
36
+ });
37
+ it('supports has and clear', () => {
38
+ const cache = createBoundedCache(2);
39
+ cache.set('a', 1);
40
+ expect(cache.has('a')).toBe(true);
41
+ cache.clear();
42
+ expect(cache.has('a')).toBe(false);
43
+ expect(cache.size()).toBe(0);
44
+ });
45
+ it('never exceeds maxSize', () => {
46
+ const cache = createBoundedCache(3);
47
+ Array.from({ length: 100 }, (_, i) => i).forEach(i => {
48
+ cache.set(`key-${i}`, i);
49
+ expect(cache.size()).toBeLessThanOrEqual(3);
50
+ });
51
+ expect(cache.size()).toBe(3);
52
+ });
53
+ });
@@ -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, override host ports in .env:\n\n OUTPUT_API_HOST_PORT=3002\n OUTPUT_TEMPORAL_UI_HOST_PORT=8081\n OUTPUT_TEMPORAL_HOST_PORT=7234",
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
  ],
@@ -905,6 +941,80 @@
905
941
  "list.js"
906
942
  ]
907
943
  },
944
+ "workflow:monitor": {
945
+ "aliases": [],
946
+ "args": {
947
+ "workflowId": {
948
+ "description": "The workflow ID to monitor",
949
+ "name": "workflowId",
950
+ "required": true
951
+ }
952
+ },
953
+ "description": "Attach to a workflow run and stream status updates until it ends",
954
+ "examples": [
955
+ "<%= config.bin %> <%= command.id %> wf-12345",
956
+ "<%= config.bin %> <%= command.id %> wf-12345 --run-id 2fe0b36b-...",
957
+ "<%= config.bin %> <%= command.id %> wf-12345 --format json"
958
+ ],
959
+ "flags": {
960
+ "run-id": {
961
+ "char": "r",
962
+ "description": "Monitor a specific run (defaults to the latest run; continue-as-new chains are followed regardless)",
963
+ "name": "run-id",
964
+ "hasDynamicHelp": false,
965
+ "multiple": false,
966
+ "type": "option"
967
+ },
968
+ "format": {
969
+ "char": "f",
970
+ "description": "Output format",
971
+ "name": "format",
972
+ "default": "text",
973
+ "hasDynamicHelp": false,
974
+ "multiple": false,
975
+ "options": [
976
+ "text",
977
+ "json"
978
+ ],
979
+ "type": "option"
980
+ },
981
+ "include-payloads": {
982
+ "description": "Include decoded step input/output payloads",
983
+ "name": "include-payloads",
984
+ "allowNo": false,
985
+ "type": "boolean"
986
+ },
987
+ "interval": {
988
+ "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.",
989
+ "name": "interval",
990
+ "default": 2500,
991
+ "hasDynamicHelp": false,
992
+ "multiple": false,
993
+ "type": "option"
994
+ },
995
+ "color": {
996
+ "description": "Colorize status output (use --no-color to disable)",
997
+ "name": "color",
998
+ "allowNo": true,
999
+ "type": "boolean"
1000
+ }
1001
+ },
1002
+ "hasDynamicHelp": false,
1003
+ "hiddenAliases": [],
1004
+ "id": "workflow:monitor",
1005
+ "pluginAlias": "@outputai/cli",
1006
+ "pluginName": "@outputai/cli",
1007
+ "pluginType": "core",
1008
+ "strict": true,
1009
+ "enableJsonFlag": false,
1010
+ "isESM": true,
1011
+ "relativePath": [
1012
+ "dist",
1013
+ "commands",
1014
+ "workflow",
1015
+ "monitor.js"
1016
+ ]
1017
+ },
908
1018
  "workflow:plan": {
909
1019
  "aliases": [],
910
1020
  "args": {},
@@ -1126,9 +1236,17 @@
1126
1236
  "<%= config.bin %> <%= command.id %> simple basic_input",
1127
1237
  "<%= config.bin %> <%= command.id %> simple --input '{\"values\":[1,2,3]}'",
1128
1238
  "<%= config.bin %> <%= command.id %> simple --input input.json",
1129
- "<%= config.bin %> <%= command.id %> simple --input '{\"key\":\"value\"}' --catalog my-catalog"
1239
+ "<%= config.bin %> <%= command.id %> simple --input '{\"key\":\"value\"}' --catalog my-catalog",
1240
+ "<%= config.bin %> <%= command.id %> simple --json"
1130
1241
  ],
1131
1242
  "flags": {
1243
+ "json": {
1244
+ "description": "Format output as json.",
1245
+ "helpGroup": "GLOBAL",
1246
+ "name": "json",
1247
+ "allowNo": false,
1248
+ "type": "boolean"
1249
+ },
1132
1250
  "input": {
1133
1251
  "char": "i",
1134
1252
  "description": "Workflow input as JSON string or file path (overrides scenario)",
@@ -1162,7 +1280,7 @@
1162
1280
  "pluginName": "@outputai/cli",
1163
1281
  "pluginType": "core",
1164
1282
  "strict": true,
1165
- "enableJsonFlag": false,
1283
+ "enableJsonFlag": true,
1166
1284
  "isESM": true,
1167
1285
  "relativePath": [
1168
1286
  "dist",
@@ -1597,5 +1715,5 @@
1597
1715
  ]
1598
1716
  }
1599
1717
  },
1600
- "version": "0.10.1-dev.b7b2fbe.0"
1718
+ "version": "0.10.1-next.2caa4a1.0"
1601
1719
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@outputai/cli",
3
- "version": "0.10.1-dev.b7b2fbe.0",
3
+ "version": "0.10.1-next.2caa4a1.0",
4
4
  "description": "CLI for Output.ai workflow generation",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -31,16 +31,15 @@
31
31
  "handlebars": "4.7.9",
32
32
  "ink": "7.0.1",
33
33
  "ink-spinner": "5.0.0",
34
- "js-yaml": "4.1.1",
34
+ "js-yaml": "4.3.0",
35
35
  "json-schema-library": "11.4.0",
36
- "ky": "1.14.3",
36
+ "ky": "2.0.2",
37
37
  "react": "19.2.5",
38
38
  "semver": "7.7.4",
39
- "undici": "8.5.0",
40
- "yaml": "^2.8.3",
41
- "@outputai/credentials": "0.10.1-dev.b7b2fbe.0",
42
- "@outputai/evals": "0.10.1-dev.b7b2fbe.0",
43
- "@outputai/llm": "0.10.1-dev.b7b2fbe.0"
39
+ "undici": "8.9.0",
40
+ "@outputai/credentials": "0.10.1-next.2caa4a1.0",
41
+ "@outputai/evals": "0.10.1-next.2caa4a1.0",
42
+ "@outputai/llm": "0.10.1-next.2caa4a1.0"
44
43
  },
45
44
  "devDependencies": {
46
45
  "@types/cli-progress": "3.11.6",