@outputai/cli 0.10.1-next.be4ec7f.0 → 0.10.1-next.c717e35.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.
@@ -80,7 +80,7 @@ services:
80
80
  condition: service_healthy
81
81
  worker:
82
82
  condition: service_healthy
83
- image: outputai/api:${OUTPUT_API_VERSION:-0.10.1-next.be4ec7f.0}
83
+ image: outputai/api:${OUTPUT_API_VERSION:-0.10.1-next.c717e35.0}
84
84
  init: true
85
85
  networks:
86
86
  - main
@@ -63,7 +63,14 @@ export default class WorkflowRun extends Command {
63
63
  };
64
64
  async run() {
65
65
  const { args, flags } = await this.parse(WorkflowRun);
66
- const input = await resolveInput(args.workflowName, args.scenario, flags.input, 'run', flags.catalog);
66
+ const input = await resolveInput({
67
+ workflowName: args.workflowName,
68
+ scenario: args.scenario,
69
+ inputFlag: flags.input,
70
+ commandName: 'run',
71
+ catalog: flags.catalog,
72
+ json: this.jsonEnabled()
73
+ });
67
74
  this.log(`Executing workflow: ${args.workflowName}...`);
68
75
  const response = await executeWorkflow({
69
76
  body: {
@@ -64,7 +64,12 @@ describe('workflow run command', () => {
64
64
  headers: new Headers()
65
65
  });
66
66
  await cmd.run();
67
- expect(resolveInput).toHaveBeenCalledWith('my_workflow', undefined, undefined, 'run', undefined);
67
+ expect(resolveInput).toHaveBeenCalledWith(expect.objectContaining({
68
+ workflowName: 'my_workflow',
69
+ commandName: 'run',
70
+ catalog: undefined,
71
+ json: false
72
+ }));
68
73
  expect(postWorkflowRun).toHaveBeenCalledTimes(1);
69
74
  expect(postWorkflowRun).toHaveBeenCalledWith({ workflowName: 'my_workflow', input: { key: 'value' }, catalog: undefined }, expect.objectContaining({ config: { timeout: 600000 } }));
70
75
  expect(cmd.log).toHaveBeenCalledWith('Executing workflow: my_workflow...');
@@ -83,7 +88,12 @@ describe('workflow run command', () => {
83
88
  headers: new Headers()
84
89
  });
85
90
  await cmd.run();
86
- expect(resolveInput).toHaveBeenCalledWith('my_workflow', 'basic', undefined, 'run', 'my-catalog');
91
+ expect(resolveInput).toHaveBeenCalledWith(expect.objectContaining({
92
+ workflowName: 'my_workflow',
93
+ scenario: 'basic',
94
+ commandName: 'run',
95
+ catalog: 'my-catalog'
96
+ }));
87
97
  expect(postWorkflowRun).toHaveBeenCalledWith(expect.objectContaining({ catalog: 'my-catalog' }), expect.anything());
88
98
  });
89
99
  it('retries when response has Retry-After and succeeds on second attempt', async () => {
@@ -1,6 +1,8 @@
1
1
  import { Command } from '@oclif/core';
2
+ import { type PostWorkflowStart200 } from '#api/generated/api.js';
2
3
  export default class WorkflowStart extends Command {
3
4
  static description: string;
5
+ static enableJsonFlag: boolean;
4
6
  static examples: string[];
5
7
  static args: {
6
8
  workflowName: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
@@ -10,6 +12,6 @@ export default class WorkflowStart extends Command {
10
12
  input: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
13
  catalog: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
14
  };
13
- run(): Promise<void>;
15
+ run(): Promise<PostWorkflowStart200>;
14
16
  catch(error: Error): Promise<void>;
15
17
  }
@@ -4,11 +4,13 @@ import { handleApiError } from '#utils/error_handler.js';
4
4
  import { resolveInput } from '#utils/resolve_input.js';
5
5
  export default class WorkflowStart extends Command {
6
6
  static description = 'Start a workflow asynchronously without waiting for completion';
7
+ static enableJsonFlag = true;
7
8
  static examples = [
8
9
  '<%= config.bin %> <%= command.id %> simple basic_input',
9
10
  '<%= config.bin %> <%= command.id %> simple --input \'{"values":[1,2,3]}\'',
10
11
  '<%= config.bin %> <%= command.id %> simple --input input.json',
11
- '<%= config.bin %> <%= command.id %> simple --input \'{"key":"value"}\' --catalog my-catalog'
12
+ '<%= config.bin %> <%= command.id %> simple --input \'{"key":"value"}\' --catalog my-catalog',
13
+ '<%= config.bin %> <%= command.id %> simple --json'
12
14
  ];
13
15
  static args = {
14
16
  workflowName: Args.string({
@@ -37,7 +39,14 @@ export default class WorkflowStart extends Command {
37
39
  };
38
40
  async run() {
39
41
  const { args, flags } = await this.parse(WorkflowStart);
40
- const input = await resolveInput(args.workflowName, args.scenario, flags.input, 'start', flags.catalog);
42
+ const input = await resolveInput({
43
+ workflowName: args.workflowName,
44
+ scenario: args.scenario,
45
+ inputFlag: flags.input,
46
+ commandName: 'start',
47
+ catalog: flags.catalog,
48
+ json: this.jsonEnabled()
49
+ });
41
50
  this.log(`Starting workflow: ${args.workflowName}...`);
42
51
  const response = await postWorkflowStart({
43
52
  workflowName: args.workflowName,
@@ -57,6 +66,7 @@ export default class WorkflowStart extends Command {
57
66
  `Use "workflow result ${result.workflowId || '<workflow-id>'}" to get the workflow result when complete`
58
67
  ].join('\n');
59
68
  this.log(`\n${output}`);
69
+ return result;
60
70
  }
61
71
  async catch(error) {
62
72
  return handleApiError(error, (...args) => this.error(...args), {
@@ -36,13 +36,17 @@ describe('workflow start command', () => {
36
36
  expect(WorkflowStart.flags.catalog.env).toBe('OUTPUT_CATALOG_ID');
37
37
  expect(WorkflowStart.flags.catalog.char).toBe('c');
38
38
  });
39
+ it('enables the built-in --json flag', async () => {
40
+ const WorkflowStart = (await import('./start.js')).default;
41
+ expect(WorkflowStart.enableJsonFlag).toBe(true);
42
+ });
39
43
  });
40
44
  describe('run()', () => {
41
- const createCommand = async (flagOverrides = {}) => {
45
+ const createCommand = async (flagOverrides = {}, argv = ['my_workflow']) => {
42
46
  const WorkflowStart = (await import('./start.js')).default;
43
47
  const { postWorkflowStart } = await import('#api/generated/api.js');
44
48
  const { resolveInput } = await import('#utils/resolve_input.js');
45
- const cmd = new WorkflowStart(['my_workflow'], {});
49
+ const cmd = new WorkflowStart(argv, {});
46
50
  cmd.log = vi.fn();
47
51
  cmd.error = vi.fn(() => {
48
52
  throw new Error('error called');
@@ -61,9 +65,15 @@ describe('workflow start command', () => {
61
65
  status: 200,
62
66
  headers: new Headers()
63
67
  });
64
- await cmd.run();
65
- expect(resolveInput).toHaveBeenCalledWith('my_workflow', undefined, undefined, 'start', 'my-catalog');
68
+ const result = await cmd.run();
69
+ expect(resolveInput).toHaveBeenCalledWith(expect.objectContaining({
70
+ workflowName: 'my_workflow',
71
+ commandName: 'start',
72
+ catalog: 'my-catalog',
73
+ json: false
74
+ }));
66
75
  expect(postWorkflowStart).toHaveBeenCalledWith(expect.objectContaining({ workflowName: 'my_workflow', catalog: 'my-catalog' }));
76
+ expect(result).toEqual({ workflowId: 'wf-123' });
67
77
  });
68
78
  it('passes undefined catalog through when none is set', async () => {
69
79
  const { cmd, postWorkflowStart, resolveInput } = await createCommand();
@@ -74,7 +84,22 @@ describe('workflow start command', () => {
74
84
  headers: new Headers()
75
85
  });
76
86
  await cmd.run();
77
- expect(resolveInput).toHaveBeenCalledWith('my_workflow', undefined, undefined, 'start', undefined);
87
+ expect(resolveInput).toHaveBeenCalledWith(expect.objectContaining({
88
+ workflowName: 'my_workflow',
89
+ commandName: 'start',
90
+ catalog: undefined
91
+ }));
92
+ });
93
+ it('tells resolveInput to stay quiet when --json is set', async () => {
94
+ const { cmd, postWorkflowStart, resolveInput } = await createCommand({}, ['my_workflow', 'basic', '--json']);
95
+ resolveInput.mockResolvedValue({});
96
+ postWorkflowStart.mockResolvedValue({
97
+ data: { workflowId: 'wf-123' },
98
+ status: 200,
99
+ headers: new Headers()
100
+ });
101
+ await cmd.run();
102
+ expect(resolveInput).toHaveBeenCalledWith(expect.objectContaining({ json: true }));
78
103
  });
79
104
  });
80
105
  });
@@ -1,7 +1,6 @@
1
1
  import { Command } from '@oclif/core';
2
2
  import type { EvalOutput } from '@outputai/evals';
3
3
  export default class WorkflowTest extends Command {
4
- static aliases: string[];
5
4
  static description: string;
6
5
  static enableJsonFlag: boolean;
7
6
  static examples: string[];
@@ -7,7 +7,6 @@ import { diagnoseMissingEvalWorkflow } from '#utils/eval_diagnostics.js';
7
7
  import { handleApiError } from '#utils/error_handler.js';
8
8
  import { getEvalWorkflowName, renderEvalOutput, computeExitCode, EvalOutputSchema } from '@outputai/evals';
9
9
  export default class WorkflowTest extends Command {
10
- static aliases = ['workflow:test'];
11
10
  static description = 'Run evaluations against a workflow using its datasets';
12
11
  static enableJsonFlag = true;
13
12
  static examples = [
@@ -45,11 +45,11 @@ describe('workflow test command', () => {
45
45
  });
46
46
  describe('command definition', () => {
47
47
  it('enables the built-in --json flag', async () => {
48
- const WorkflowTest = (await import('./test_eval.js')).default;
48
+ const WorkflowTest = (await import('./test.js')).default;
49
49
  expect(WorkflowTest.enableJsonFlag).toBe(true);
50
50
  });
51
51
  it('binds the catalog flag to OUTPUT_CATALOG_ID', async () => {
52
- const WorkflowTest = (await import('./test_eval.js')).default;
52
+ const WorkflowTest = (await import('./test.js')).default;
53
53
  expect(WorkflowTest.flags).toHaveProperty('catalog');
54
54
  expect(WorkflowTest.flags.catalog.env).toBe('OUTPUT_CATALOG_ID');
55
55
  expect(WorkflowTest.flags.catalog.char).toBe('c');
@@ -57,7 +57,7 @@ describe('workflow test command', () => {
57
57
  });
58
58
  describe('run()', () => {
59
59
  const createCommand = async (jsonEnabled) => {
60
- const WorkflowTest = (await import('./test_eval.js')).default;
60
+ const WorkflowTest = (await import('./test.js')).default;
61
61
  const { postWorkflowRun } = await import('#api/generated/api.js');
62
62
  const cmd = new WorkflowTest(['simple'], {});
63
63
  cmd.log = vi.fn();
@@ -99,7 +99,7 @@ describe('workflow test command', () => {
99
99
  expect(process.exitCode).toBe(1);
100
100
  });
101
101
  it('routes registration, dataset runs, and the eval run to the resolved catalog', async () => {
102
- const WorkflowTest = (await import('./test_eval.js')).default;
102
+ const WorkflowTest = (await import('./test.js')).default;
103
103
  const { postWorkflowRun } = await import('#api/generated/api.js');
104
104
  const { fetchWorkflowCatalog } = await import('#api/workflow_catalog.js');
105
105
  const cmd = new WorkflowTest(['my_workflow'], {});
@@ -1,3 +1,3 @@
1
1
  {
2
- "framework": "0.10.1-next.be4ec7f.0"
2
+ "framework": "0.10.1-next.c717e35.0"
3
3
  }
@@ -17,10 +17,12 @@ claude plugin install outputai@outputai --scope project
17
17
  npm run output:dev # Start dev environment (worker + Temporal)
18
18
  npm run output:worker:build # Build TypeScript to dist/
19
19
  npm run output:worker:check # Optional: bundle-check workflows for bad imports (node: built-ins)
20
- npm run output:worker:watch # Build + restart on file changes
21
- npm run output:worker # Install, build, and start worker
20
+ npm run output:worker:watch # Build + restart on src/ file changes
21
+ npm run output:worker # Build and start worker
22
22
  ```
23
23
 
24
+ Hot-reload watches `src/` only. After changing dependencies (`package.json` / lockfile), run `npm install`, then `npx output dev down` and `npm run output:dev` again so the worker container reinstalls. A hot-reload alone is not enough; if the stack is still running, `npx output dev down` is required.
25
+
24
26
  ## Project Structure
25
27
 
26
28
  ```
@@ -79,7 +79,9 @@ This starts:
79
79
  - Temporal server and UI (http://localhost:8080)
80
80
  - PostgreSQL and Redis databases
81
81
  - Output.ai API server (http://localhost:3001)
82
- - Worker process for executing workflows
82
+ - Worker process for executing workflows (auto-reloads on `src/` changes)
83
+
84
+ Dependency changes (`package.json` / lockfile) are not picked up by hot-reload. Run `npm install`, then `npx output dev down` and `npm run output:dev` again so the worker container reinstalls. A hot-reload alone is not enough; if the stack is still running, `npx output dev down` is required.
83
85
 
84
86
  ### 4. Run a workflow
85
87
 
@@ -9,8 +9,8 @@
9
9
  "output:worker:build": "rm -rf dist/* && tsc -p ./ && output-copy-assets",
10
10
  "output:worker:start": "output-worker",
11
11
  "output:worker:check": "output-worker --check",
12
- "output:worker": "npm run output:worker:install && npm run output:worker:build && npm run output:worker:start",
13
- "output:worker:watch": "npx nodemon --watch src --watch package.json --ext ts,js,json,prompt,md --ignore 'dist/**' --ignore '**/*.spec.*' --ignore '**/*.test.*' --exec 'npm run output:worker'",
12
+ "output:worker": "npm run output:worker:build && npm run output:worker:start",
13
+ "output:worker:watch": "npx nodemon --watch src --ext ts,js,json,prompt,md --ignore 'dist/**' --ignore '**/*.spec.*' --ignore '**/*.test.*' --exec 'npm run output:worker'",
14
14
  "output:dev": "output dev"
15
15
  },
16
16
  "dependencies": {
@@ -5,7 +5,6 @@
5
5
  */
6
6
  import { existsSync } from 'node:fs';
7
7
  import { resolve } from 'node:path';
8
- import * as dotenv from 'dotenv';
9
8
  import debugFactory from 'debug';
10
9
  const debug = debugFactory('output-cli:env-loader');
11
10
  export function loadEnvironment() {
@@ -17,5 +16,10 @@ export function loadEnvironment() {
17
16
  return;
18
17
  }
19
18
  debug(`Loading env from: ${envPath}`);
20
- dotenv.config({ path: envPath, quiet: true });
19
+ try {
20
+ process.loadEnvFile(envPath);
21
+ }
22
+ catch (err) {
23
+ debug(`Warning: Failed to load env file ${envPath}: ${err}`);
24
+ }
21
25
  }
@@ -1,43 +1,72 @@
1
- /**
2
- * Tests for the env loader utility
3
- */
4
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
5
- import { existsSync } from 'node:fs';
6
- import { resolve } from 'node:path';
7
- import * as dotenv from 'dotenv';
8
- vi.mock('node:fs');
9
- vi.mock('dotenv');
1
+ import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { loadEnvironment } from './env_loader.js';
10
6
  describe('loadEnvironment', () => {
11
- const originalEnv = { ...process.env };
12
- const mockCwd = '/mock/project';
7
+ const mockCwd = mkdtempSync(join(tmpdir(), 'output-env-loader-'));
13
8
  beforeEach(() => {
14
- vi.resetModules();
15
- vi.clearAllMocks();
9
+ for (const name of readdirSync(mockCwd)) {
10
+ rmSync(join(mockCwd, name), { recursive: true, force: true });
11
+ }
16
12
  vi.spyOn(process, 'cwd').mockReturnValue(mockCwd);
17
- vi.spyOn(console, 'log').mockImplementation(() => { });
18
- vi.spyOn(console, 'warn').mockImplementation(() => { });
13
+ vi.stubEnv('OUTPUT_CLI_ENV', undefined);
14
+ vi.stubEnv('OUTPUT_API_URL', undefined);
15
+ vi.stubEnv('OUTPUT_API_TOKEN', undefined);
19
16
  });
20
17
  afterEach(() => {
21
- process.env = { ...originalEnv };
22
18
  vi.restoreAllMocks();
19
+ vi.unstubAllEnvs();
23
20
  });
24
- it('should load from OUTPUT_CLI_ENV when set and file exists', async () => {
25
- process.env.OUTPUT_CLI_ENV = '.env.prod';
26
- const expectedPath = resolve(mockCwd, '.env.prod');
27
- vi.mocked(existsSync).mockReturnValue(true);
28
- vi.mocked(dotenv.config).mockReturnValue({ parsed: { OUTPUT_API_URL: 'https://prod.api.com' } });
29
- const { loadEnvironment } = await import('./env_loader.js');
21
+ afterAll(() => {
22
+ rmSync(mockCwd, { recursive: true, force: true });
23
+ });
24
+ it('loads variables from OUTPUT_CLI_ENV', () => {
25
+ writeFileSync(join(mockCwd, '.env'), [
26
+ 'OUTPUT_API_URL=https://default.api.com',
27
+ 'OUTPUT_API_TOKEN=default-token'
28
+ ].join('\n'));
29
+ writeFileSync(join(mockCwd, '.env.mock'), [
30
+ 'OUTPUT_API_URL=https://mock.api.com',
31
+ 'OUTPUT_API_TOKEN=mock-token'
32
+ ].join('\n'));
33
+ process.env.OUTPUT_CLI_ENV = '.env.mock';
34
+ loadEnvironment();
35
+ expect(process.env.OUTPUT_API_URL).toBe('https://mock.api.com');
36
+ expect(process.env.OUTPUT_API_TOKEN).toBe('mock-token');
37
+ });
38
+ it('loads variables from .env by default', () => {
39
+ writeFileSync(join(mockCwd, '.env'), [
40
+ 'OUTPUT_API_URL=https://default.api.com',
41
+ 'OUTPUT_API_TOKEN=default-token'
42
+ ].join('\n'));
43
+ writeFileSync(join(mockCwd, '.env.mock'), [
44
+ 'OUTPUT_API_URL=https://mock.api.com',
45
+ 'OUTPUT_API_TOKEN=mock-token'
46
+ ].join('\n'));
30
47
  loadEnvironment();
31
- expect(dotenv.config).toHaveBeenCalledWith({ path: expectedPath, quiet: true });
32
- });
33
- it('should load .env by default and log', async () => {
34
- delete process.env.OUTPUT_CLI_ENV;
35
- const envPath = resolve(mockCwd, '.env');
36
- vi.mocked(existsSync).mockImplementation(p => p === envPath);
37
- vi.mocked(dotenv.config).mockReturnValue({ parsed: {} });
38
- const { loadEnvironment } = await import('./env_loader.js');
48
+ expect(process.env.OUTPUT_API_URL).toBe('https://default.api.com');
49
+ expect(process.env.OUTPUT_API_TOKEN).toBe('default-token');
50
+ });
51
+ it('does nothing when the env file is missing', () => {
52
+ expect(() => loadEnvironment()).not.toThrow();
53
+ expect(process.env.OUTPUT_API_URL).toBeUndefined();
54
+ expect(process.env.OUTPUT_API_TOKEN).toBeUndefined();
55
+ });
56
+ it('does not throw when the env path is not a readable file', () => {
57
+ mkdirSync(join(mockCwd, 'not-a-file.env'));
58
+ process.env.OUTPUT_CLI_ENV = 'not-a-file.env';
59
+ expect(() => loadEnvironment()).not.toThrow();
60
+ expect(process.env.OUTPUT_API_URL).toBeUndefined();
61
+ });
62
+ it('does not overwrite already-set process.env values', () => {
63
+ vi.stubEnv('OUTPUT_API_URL', 'https://ambient.api.com');
64
+ writeFileSync(join(mockCwd, '.env'), [
65
+ 'OUTPUT_API_URL=https://file.api.com',
66
+ 'OUTPUT_API_TOKEN=file-token'
67
+ ].join('\n'));
39
68
  loadEnvironment();
40
- expect(dotenv.config).toHaveBeenCalledTimes(1);
41
- expect(dotenv.config).toHaveBeenCalledWith({ path: envPath, quiet: true });
69
+ expect(process.env.OUTPUT_API_URL).toBe('https://ambient.api.com');
70
+ expect(process.env.OUTPUT_API_TOKEN).toBe('file-token');
42
71
  });
43
72
  });
@@ -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
+ });
@@ -1236,9 +1236,17 @@
1236
1236
  "<%= config.bin %> <%= command.id %> simple basic_input",
1237
1237
  "<%= config.bin %> <%= command.id %> simple --input '{\"values\":[1,2,3]}'",
1238
1238
  "<%= config.bin %> <%= command.id %> simple --input input.json",
1239
- "<%= 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"
1240
1241
  ],
1241
1242
  "flags": {
1243
+ "json": {
1244
+ "description": "Format output as json.",
1245
+ "helpGroup": "GLOBAL",
1246
+ "name": "json",
1247
+ "allowNo": false,
1248
+ "type": "boolean"
1249
+ },
1242
1250
  "input": {
1243
1251
  "char": "i",
1244
1252
  "description": "Workflow input as JSON string or file path (overrides scenario)",
@@ -1272,7 +1280,7 @@
1272
1280
  "pluginName": "@outputai/cli",
1273
1281
  "pluginType": "core",
1274
1282
  "strict": true,
1275
- "enableJsonFlag": false,
1283
+ "enableJsonFlag": true,
1276
1284
  "isESM": true,
1277
1285
  "relativePath": [
1278
1286
  "dist",
@@ -1390,10 +1398,8 @@
1390
1398
  "terminate.js"
1391
1399
  ]
1392
1400
  },
1393
- "workflow:test_eval": {
1394
- "aliases": [
1395
- "workflow:test"
1396
- ],
1401
+ "workflow:test": {
1402
+ "aliases": [],
1397
1403
  "args": {
1398
1404
  "workflowName": {
1399
1405
  "description": "Name of the workflow to test",
@@ -1462,7 +1468,7 @@
1462
1468
  },
1463
1469
  "hasDynamicHelp": false,
1464
1470
  "hiddenAliases": [],
1465
- "id": "workflow:test_eval",
1471
+ "id": "workflow:test",
1466
1472
  "pluginAlias": "@outputai/cli",
1467
1473
  "pluginName": "@outputai/cli",
1468
1474
  "pluginType": "core",
@@ -1473,7 +1479,7 @@
1473
1479
  "dist",
1474
1480
  "commands",
1475
1481
  "workflow",
1476
- "test_eval.js"
1482
+ "test.js"
1477
1483
  ]
1478
1484
  },
1479
1485
  "workflow:dataset:generate": {
@@ -1707,5 +1713,5 @@
1707
1713
  ]
1708
1714
  }
1709
1715
  },
1710
- "version": "0.10.1-next.be4ec7f.0"
1716
+ "version": "0.10.1-next.c717e35.0"
1711
1717
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@outputai/cli",
3
- "version": "0.10.1-next.be4ec7f.0",
3
+ "version": "0.10.1-next.c717e35.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.1.1",
33
+ "js-yaml": "4.3.0",
35
34
  "json-schema-library": "11.4.0",
36
35
  "ky": "2.0.2",
37
36
  "react": "19.2.5",
38
37
  "semver": "7.7.4",
39
- "undici": "8.5.0",
40
- "yaml": "^2.8.3",
41
- "@outputai/credentials": "0.10.1-next.be4ec7f.0",
42
- "@outputai/evals": "0.10.1-next.be4ec7f.0",
43
- "@outputai/llm": "0.10.1-next.be4ec7f.0"
38
+ "undici": "8.9.0",
39
+ "@outputai/credentials": "0.10.1-next.c717e35.0",
40
+ "@outputai/llm": "0.10.1-next.c717e35.0",
41
+ "@outputai/evals": "0.10.1-next.c717e35.0"
44
42
  },
45
43
  "devDependencies": {
46
44
  "@types/cli-progress": "3.11.6",