@outputai/cli 0.1.13-dev.98dfd72.0 → 0.1.13-dev.e3e3291.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 (47) hide show
  1. package/bin/run.js +0 -2
  2. package/dist/api/generated/api.d.ts +319 -85
  3. package/dist/api/generated/api.js +66 -8
  4. package/dist/assets/docker/docker-compose-dev.yml +1 -1
  5. package/dist/commands/fix.js +1 -1
  6. package/dist/commands/fix.spec.js +2 -2
  7. package/dist/commands/update.js +1 -1
  8. package/dist/commands/update.spec.js +2 -2
  9. package/dist/commands/workflow/plan.js +1 -5
  10. package/dist/commands/workflow/plan.spec.js +2 -3
  11. package/dist/commands/workflow/run.js +1 -2
  12. package/dist/commands/workflow/run.spec.js +0 -1
  13. package/dist/commands/workflow/start.js +1 -2
  14. package/dist/commands/workflow/start.spec.js +0 -1
  15. package/dist/commands/workflow/test_eval.js +2 -2
  16. package/dist/generated/framework_version.json +1 -1
  17. package/dist/hooks/init.js +0 -4
  18. package/dist/services/coding_agents.js +1 -1
  19. package/dist/services/coding_agents.spec.js +6 -6
  20. package/dist/services/credentials_configurator.js +1 -1
  21. package/dist/services/datasets.d.ts +1 -1
  22. package/dist/services/datasets.js +41 -37
  23. package/dist/services/datasets.test.js +202 -0
  24. package/dist/services/env_configurator.js +1 -1
  25. package/dist/services/env_configurator.spec.js +12 -12
  26. package/dist/services/project_scaffold.js +2 -2
  27. package/dist/services/project_scaffold.spec.js +6 -6
  28. package/dist/services/workflow_builder.js +1 -5
  29. package/dist/services/workflow_builder.spec.js +2 -3
  30. package/dist/utils/format_workflow_result.d.ts +3 -3
  31. package/package.json +4 -5
  32. package/dist/commands/credentials/set.d.ts +0 -14
  33. package/dist/commands/credentials/set.js +0 -57
  34. package/dist/commands/credentials/set.spec.js +0 -95
  35. package/dist/utils/interactive.d.ts +0 -2
  36. package/dist/utils/interactive.js +0 -5
  37. package/dist/utils/interactive.spec.d.ts +0 -1
  38. package/dist/utils/interactive.spec.js +0 -40
  39. package/dist/utils/prompt.d.ts +0 -17
  40. package/dist/utils/prompt.js +0 -20
  41. package/dist/utils/prompt.spec.d.ts +0 -1
  42. package/dist/utils/prompt.spec.js +0 -74
  43. package/dist/utils/proxy.d.ts +0 -1
  44. package/dist/utils/proxy.js +0 -9
  45. package/dist/utils/proxy.spec.d.ts +0 -1
  46. package/dist/utils/proxy.spec.js +0 -39
  47. /package/dist/{commands/credentials/set.spec.d.ts → services/datasets.test.d.ts} +0 -0
@@ -0,0 +1,202 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mkdtemp, rm, writeFile, readFile, mkdir } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import yaml from 'js-yaml';
6
+ import { readDatasetFile, readAllDatasets, writeDataset, listDatasets } from './datasets.js';
7
+ const ctx = { tmpDir: '' };
8
+ beforeEach(async () => {
9
+ ctx.tmpDir = await mkdtemp(join(tmpdir(), 'output-datasets-test-'));
10
+ });
11
+ afterEach(async () => {
12
+ await rm(ctx.tmpDir, { recursive: true, force: true });
13
+ });
14
+ function writeYaml(filePath, obj) {
15
+ return writeFile(filePath, yaml.dump(obj, { lineWidth: 120, noRefs: true, sortKeys: false }), 'utf-8');
16
+ }
17
+ // ---------------------------------------------------------------------------
18
+ // readDatasetFile
19
+ // ---------------------------------------------------------------------------
20
+ describe('readDatasetFile', () => {
21
+ it('parses a multi-case file and returns all cases', async () => {
22
+ const filePath = join(ctx.tmpDir, 'cases.yml');
23
+ await writeYaml(filePath, {
24
+ case_a: { input: { query: 'foo' }, ground_truth: { expected: 1 } },
25
+ case_b: { input: { query: 'bar' } },
26
+ case_c: { input: { query: 'baz' }, ground_truth: { expected: 3 } }
27
+ });
28
+ const datasets = await readDatasetFile(filePath);
29
+ expect(datasets).toHaveLength(3);
30
+ expect(datasets.map(d => d.name)).toEqual(['case_a', 'case_b', 'case_c']);
31
+ expect(datasets[0].input).toEqual({ query: 'foo' });
32
+ expect(datasets[0].ground_truth).toEqual({ expected: 1 });
33
+ });
34
+ it('attaches _source with the absolute file path to each dataset', async () => {
35
+ const filePath = join(ctx.tmpDir, 'cases.yml');
36
+ await writeYaml(filePath, {
37
+ my_case: { input: { x: 1 } }
38
+ });
39
+ const [dataset] = await readDatasetFile(filePath);
40
+ expect(dataset._source).toBe(filePath);
41
+ });
42
+ it('throws when a case is missing input', async () => {
43
+ const filePath = join(ctx.tmpDir, 'bad.yml');
44
+ await writeYaml(filePath, {
45
+ good_case: { input: { x: 1 } },
46
+ bad_case: { ground_truth: { expected: 42 } }
47
+ });
48
+ await expect(readDatasetFile(filePath)).rejects.toThrow('Dataset case "bad_case" in');
49
+ });
50
+ it('throws when file content is not an object', async () => {
51
+ const filePath = join(ctx.tmpDir, 'bad.yml');
52
+ await writeFile(filePath, 'just a string', 'utf-8');
53
+ await expect(readDatasetFile(filePath)).rejects.toThrow('Invalid dataset file');
54
+ });
55
+ it('throws with clear message when file content is a YAML array', async () => {
56
+ const filePath = join(ctx.tmpDir, 'bad.yml');
57
+ await writeFile(filePath, '- foo\n- bar\n', 'utf-8');
58
+ await expect(readDatasetFile(filePath)).rejects.toThrow('Invalid dataset file');
59
+ });
60
+ it('preserves last_output and last_eval fields', async () => {
61
+ const filePath = join(ctx.tmpDir, 'cases.yml');
62
+ await writeYaml(filePath, {
63
+ cached_case: {
64
+ input: { q: 'hello' },
65
+ last_output: { output: { result: 42 }, executionTimeMs: 100, date: '2026-01-01T00:00:00.000Z' }
66
+ }
67
+ });
68
+ const [dataset] = await readDatasetFile(filePath);
69
+ expect(dataset.last_output?.output).toEqual({ result: 42 });
70
+ expect(dataset.last_output?.executionTimeMs).toBe(100);
71
+ });
72
+ });
73
+ // ---------------------------------------------------------------------------
74
+ // readAllDatasets
75
+ // ---------------------------------------------------------------------------
76
+ describe('readAllDatasets', () => {
77
+ it('flattens cases from multiple files', async () => {
78
+ const datasetsDir = join(ctx.tmpDir, 'src', 'workflows', 'my_workflow', 'tests', 'datasets');
79
+ await mkdir(datasetsDir, { recursive: true });
80
+ await writeYaml(join(datasetsDir, 'group_a.yml'), {
81
+ case_1: { input: { x: 1 } },
82
+ case_2: { input: { x: 2 } }
83
+ });
84
+ await writeYaml(join(datasetsDir, 'group_b.yml'), {
85
+ case_3: { input: { x: 3 } }
86
+ });
87
+ const { datasets } = await readAllDatasets('my_workflow', undefined, ctx.tmpDir);
88
+ expect(datasets).toHaveLength(3);
89
+ expect(datasets.map(d => d.name).sort()).toEqual(['case_1', 'case_2', 'case_3']);
90
+ });
91
+ it('filters by case name across files', async () => {
92
+ const datasetsDir = join(ctx.tmpDir, 'src', 'workflows', 'my_workflow', 'tests', 'datasets');
93
+ await mkdir(datasetsDir, { recursive: true });
94
+ await writeYaml(join(datasetsDir, 'group_a.yml'), {
95
+ case_1: { input: { x: 1 } },
96
+ case_2: { input: { x: 2 } }
97
+ });
98
+ await writeYaml(join(datasetsDir, 'group_b.yml'), {
99
+ case_3: { input: { x: 3 } }
100
+ });
101
+ const { datasets } = await readAllDatasets('my_workflow', ['case_2', 'case_3'], ctx.tmpDir);
102
+ expect(datasets).toHaveLength(2);
103
+ expect(datasets.map(d => d.name).sort()).toEqual(['case_2', 'case_3']);
104
+ });
105
+ it('returns empty datasets and a default dir when workflow has no datasets dir', async () => {
106
+ const { datasets, dir } = await readAllDatasets('nonexistent_workflow', undefined, ctx.tmpDir);
107
+ expect(datasets).toHaveLength(0);
108
+ expect(dir).toContain('nonexistent_workflow');
109
+ });
110
+ it('throws when the same case name appears in two different files', async () => {
111
+ const datasetsDir = join(ctx.tmpDir, 'src', 'workflows', 'my_workflow', 'tests', 'datasets');
112
+ await mkdir(datasetsDir, { recursive: true });
113
+ await writeYaml(join(datasetsDir, 'group_a.yml'), { case_1: { input: { x: 1 } } });
114
+ await writeYaml(join(datasetsDir, 'group_b.yml'), { case_1: { input: { x: 2 } } });
115
+ await expect(readAllDatasets('my_workflow', undefined, ctx.tmpDir)).rejects.toThrow('Duplicate dataset case name "case_1"');
116
+ });
117
+ });
118
+ // ---------------------------------------------------------------------------
119
+ // writeDataset
120
+ // ---------------------------------------------------------------------------
121
+ describe('writeDataset', () => {
122
+ it('creates a new file with one case keyed by name', async () => {
123
+ const filePath = join(ctx.tmpDir, 'cases.yml');
124
+ const dataset = { name: 'new_case', input: { q: 'hello' } };
125
+ await writeDataset(dataset, filePath);
126
+ const raw = yaml.load(await readFile(filePath, 'utf-8'));
127
+ expect(raw).toHaveProperty('new_case');
128
+ expect(raw.new_case.input).toEqual({ q: 'hello' });
129
+ expect(raw.new_case).not.toHaveProperty('name');
130
+ });
131
+ it('does not write _source into the file', async () => {
132
+ const filePath = join(ctx.tmpDir, 'cases.yml');
133
+ const dataset = { name: 'my_case', input: { q: 'x' }, _source: '/some/path.yml' };
134
+ await writeDataset(dataset, filePath);
135
+ const raw = yaml.load(await readFile(filePath, 'utf-8'));
136
+ expect(raw.my_case).not.toHaveProperty('_source');
137
+ });
138
+ it('updates only the target case, leaving other cases untouched', async () => {
139
+ const filePath = join(ctx.tmpDir, 'cases.yml');
140
+ await writeYaml(filePath, {
141
+ case_a: { input: { x: 1 }, ground_truth: { expected: 1 } },
142
+ case_b: { input: { x: 2 }, ground_truth: { expected: 2 } }
143
+ });
144
+ await writeDataset({ name: 'case_a', input: { x: 1 }, last_output: { output: { result: 1 }, date: '2026-01-01T00:00:00.000Z' } }, filePath);
145
+ const raw = yaml.load(await readFile(filePath, 'utf-8'));
146
+ expect(raw).toHaveProperty('case_b');
147
+ expect(raw.case_b.ground_truth).toEqual({ expected: 2 });
148
+ });
149
+ it('preserves existing fields when writing last_output then last_eval', async () => {
150
+ const filePath = join(ctx.tmpDir, 'cases.yml');
151
+ await writeYaml(filePath, {
152
+ my_case: { input: { q: 'hello' }, ground_truth: { expected: 42 } }
153
+ });
154
+ await writeDataset({ name: 'my_case', input: { q: 'hello' }, last_output: { output: { result: 42 }, executionTimeMs: 50, date: '2026-01-01T00:00:00.000Z' } }, filePath);
155
+ await writeDataset({
156
+ name: 'my_case', input: { q: 'hello' },
157
+ last_eval: { output: { datasetName: 'my_case', verdict: 'pass', evaluators: [] }, date: '2026-01-01T00:01:00.000Z' }
158
+ }, filePath);
159
+ const raw = yaml.load(await readFile(filePath, 'utf-8'));
160
+ const caseObj = raw.my_case;
161
+ expect(caseObj).toHaveProperty('last_output');
162
+ expect(caseObj).toHaveProperty('last_eval');
163
+ expect(caseObj).toHaveProperty('ground_truth');
164
+ });
165
+ it('creates parent directories if they do not exist', async () => {
166
+ const filePath = join(ctx.tmpDir, 'deep', 'nested', 'cases.yml');
167
+ await writeDataset({ name: 'my_case', input: { q: 'x' } }, filePath);
168
+ const raw = yaml.load(await readFile(filePath, 'utf-8'));
169
+ expect(raw).toHaveProperty('my_case');
170
+ });
171
+ it('recovers gracefully when existing file contains non-object YAML', async () => {
172
+ const filePath = join(ctx.tmpDir, 'cases.yml');
173
+ await writeFile(filePath, 'just a string', 'utf-8');
174
+ await writeDataset({ name: 'my_case', input: { q: 'x' } }, filePath);
175
+ const raw = yaml.load(await readFile(filePath, 'utf-8'));
176
+ expect(raw).toHaveProperty('my_case');
177
+ });
178
+ });
179
+ // ---------------------------------------------------------------------------
180
+ // listDatasets
181
+ // ---------------------------------------------------------------------------
182
+ describe('listDatasets', () => {
183
+ it('returns one DatasetInfo per case across all files', async () => {
184
+ const datasetsDir = join(ctx.tmpDir, 'src', 'workflows', 'my_workflow', 'tests', 'datasets');
185
+ await mkdir(datasetsDir, { recursive: true });
186
+ await writeYaml(join(datasetsDir, 'core.yml'), {
187
+ case_1: { input: { x: 1 }, last_output: { output: { r: 1 }, date: '2026-01-01T00:00:00.000Z' } },
188
+ case_2: { input: { x: 2 } }
189
+ });
190
+ const infos = await listDatasets('my_workflow', ctx.tmpDir);
191
+ expect(infos).toHaveLength(2);
192
+ const case1 = infos.find(i => i.name === 'case_1');
193
+ expect(case1.hasLastOutput).toBe(true);
194
+ expect(case1.path).toContain('core.yml');
195
+ const case2 = infos.find(i => i.name === 'case_2');
196
+ expect(case2.hasLastOutput).toBe(false);
197
+ });
198
+ it('returns empty array when no datasets directory exists', async () => {
199
+ const infos = await listDatasets('nonexistent_workflow', ctx.tmpDir);
200
+ expect(infos).toHaveLength(0);
201
+ });
202
+ });
@@ -1,4 +1,4 @@
1
- import { input, confirm, password } from '#utils/prompt.js';
1
+ import { input, confirm, password } from '@inquirer/prompts';
2
2
  import fs from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import { ux } from '@oclif/core';
@@ -3,7 +3,7 @@ import fs from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import { configureEnvironmentVariables } from './env_configurator.js';
5
5
  // Mock inquirer prompts
6
- vi.mock('#utils/prompt.js', () => ({
6
+ vi.mock('@inquirer/prompts', () => ({
7
7
  input: vi.fn(),
8
8
  confirm: vi.fn(),
9
9
  password: vi.fn()
@@ -45,7 +45,7 @@ describe('configureEnvironmentVariables', () => {
45
45
  expect(result).toBe(false);
46
46
  });
47
47
  it('should return false if user declines configuration', async () => {
48
- const { confirm } = await import('#utils/prompt.js');
48
+ const { confirm } = await import('@inquirer/prompts');
49
49
  vi.mocked(confirm).mockResolvedValue(false);
50
50
  await fs.writeFile(testState.envExamplePath, '# API key\nAPIKEY=');
51
51
  const result = await configureEnvironmentVariables(testState.tempDir, false);
@@ -53,14 +53,14 @@ describe('configureEnvironmentVariables', () => {
53
53
  expect(vi.mocked(confirm)).toHaveBeenCalled();
54
54
  });
55
55
  it('should return false if no empty variables exist', async () => {
56
- const { confirm } = await import('#utils/prompt.js');
56
+ const { confirm } = await import('@inquirer/prompts');
57
57
  vi.mocked(confirm).mockResolvedValue(true);
58
58
  await fs.writeFile(testState.envExamplePath, 'APIKEY=my-secret-key');
59
59
  const result = await configureEnvironmentVariables(testState.tempDir, false);
60
60
  expect(result).toBe(false);
61
61
  });
62
62
  it('should copy .env.example to .env when user confirms configuration', async () => {
63
- const { input, confirm } = await import('#utils/prompt.js');
63
+ const { input, confirm } = await import('@inquirer/prompts');
64
64
  vi.mocked(confirm).mockResolvedValue(true);
65
65
  vi.mocked(input).mockResolvedValueOnce('sk-proj-123');
66
66
  const originalContent = `# API key
@@ -72,7 +72,7 @@ APIKEY=`;
72
72
  await expect(fs.access(testState.envPath)).resolves.toBeUndefined();
73
73
  });
74
74
  it('should write configured values to .env while leaving .env.example unchanged', async () => {
75
- const { input, confirm } = await import('#utils/prompt.js');
75
+ const { input, confirm } = await import('@inquirer/prompts');
76
76
  vi.mocked(confirm).mockResolvedValue(true);
77
77
  vi.mocked(input).mockResolvedValueOnce('sk-proj-123');
78
78
  const originalContent = `# API key
@@ -88,7 +88,7 @@ APIKEY=`;
88
88
  expect(envExampleContent).toBe(originalContent);
89
89
  });
90
90
  it('should prompt for empty variables and update .env', async () => {
91
- const { input, confirm } = await import('#utils/prompt.js');
91
+ const { input, confirm } = await import('@inquirer/prompts');
92
92
  vi.mocked(confirm).mockResolvedValue(true);
93
93
  vi.mocked(input).mockResolvedValueOnce('sk-proj-123');
94
94
  vi.mocked(input).mockResolvedValueOnce('');
@@ -105,7 +105,7 @@ OPENAI_API_KEY=`);
105
105
  expect(content).toContain('OPENAI_API_KEY=');
106
106
  });
107
107
  it('should preserve comments in .env file', async () => {
108
- const { input, confirm } = await import('#utils/prompt.js');
108
+ const { input, confirm } = await import('@inquirer/prompts');
109
109
  vi.mocked(confirm).mockResolvedValue(true);
110
110
  vi.mocked(input).mockResolvedValueOnce('test-key');
111
111
  const originalContent = `# This is a comment
@@ -123,7 +123,7 @@ OTHER=value`;
123
123
  expect(content).toContain('OTHER=value');
124
124
  });
125
125
  it('should skip placeholder values and only prompt for truly empty variables', async () => {
126
- const { input, confirm } = await import('#utils/prompt.js');
126
+ const { input, confirm } = await import('@inquirer/prompts');
127
127
  vi.mocked(confirm).mockResolvedValue(true);
128
128
  vi.mocked(input).mockResolvedValueOnce('new-key');
129
129
  await fs.writeFile(testState.envExamplePath, `APIKEY=your_api_key_here
@@ -136,7 +136,7 @@ EMPTY_KEY=`);
136
136
  }));
137
137
  });
138
138
  it('should skip variables with existing values', async () => {
139
- const { input, confirm } = await import('#utils/prompt.js');
139
+ const { input, confirm } = await import('@inquirer/prompts');
140
140
  vi.mocked(confirm).mockResolvedValue(true);
141
141
  vi.mocked(input).mockResolvedValueOnce('new-key');
142
142
  await fs.writeFile(testState.envExamplePath, `EXISTING_KEY=existing-value
@@ -147,7 +147,7 @@ EMPTY_KEY=`);
147
147
  expect(vi.mocked(input)).toHaveBeenCalledTimes(1);
148
148
  });
149
149
  it('should handle case where .env already exists (overwrite with copy)', async () => {
150
- const { input, confirm } = await import('#utils/prompt.js');
150
+ const { input, confirm } = await import('@inquirer/prompts');
151
151
  vi.mocked(confirm).mockResolvedValue(true);
152
152
  vi.mocked(input).mockResolvedValueOnce('new-configured-value');
153
153
  // Create existing .env with old content
@@ -162,7 +162,7 @@ EMPTY_KEY=`);
162
162
  expect(envContent).not.toContain('OLD_KEY');
163
163
  });
164
164
  it('should return false if an error occurs during parsing', async () => {
165
- const { confirm } = await import('#utils/prompt.js');
165
+ const { confirm } = await import('@inquirer/prompts');
166
166
  vi.mocked(confirm).mockResolvedValue(true);
167
167
  await fs.writeFile(testState.envExamplePath, 'KEY=');
168
168
  // Delete the .env.example file after access check but before parsing would happen
@@ -178,7 +178,7 @@ EMPTY_KEY=`);
178
178
  vi.mocked(fs.copyFile).mockImplementation(originalCopyFile);
179
179
  });
180
180
  it('should prompt for SECRET marker values with password input', async () => {
181
- const { password, confirm } = await import('#utils/prompt.js');
181
+ const { password, confirm } = await import('@inquirer/prompts');
182
182
  vi.mocked(confirm).mockResolvedValue(true);
183
183
  vi.mocked(password).mockResolvedValueOnce('my-secret-api-key');
184
184
  await fs.writeFile(testState.envExamplePath, `# API Key
@@ -1,4 +1,4 @@
1
- import { input, confirm } from '#utils/prompt.js';
1
+ import { input, confirm } from '@inquirer/prompts';
2
2
  import { ux } from '@oclif/core';
3
3
  import { kebabCase, pascalCase } from 'change-case';
4
4
  import fs from 'node:fs/promises';
@@ -41,7 +41,7 @@ export async function checkDependencies() {
41
41
  try {
42
42
  const shouldProceed = await confirm({
43
43
  message: 'Would you like to proceed anyway?',
44
- default: true
44
+ default: false
45
45
  });
46
46
  if (!shouldProceed) {
47
47
  throw new UserCancelledError();
@@ -8,7 +8,7 @@ vi.mock('#utils/framework_version.js', () => ({
8
8
  })
9
9
  }));
10
10
  // Mock other dependencies
11
- vi.mock('#utils/prompt.js', () => ({
11
+ vi.mock('@inquirer/prompts', () => ({
12
12
  input: vi.fn(),
13
13
  confirm: vi.fn()
14
14
  }));
@@ -47,7 +47,7 @@ describe('project_scaffold', () => {
47
47
  });
48
48
  describe('getProjectConfig', () => {
49
49
  it('should skip all prompts when folderName is provided', async () => {
50
- const { input } = await import('#utils/prompt.js');
50
+ const { input } = await import('@inquirer/prompts');
51
51
  const config = await getProjectConfig('my-project');
52
52
  expect(config.folderName).toBe('my-project');
53
53
  expect(config.projectName).toBe('my-project');
@@ -58,7 +58,7 @@ describe('project_scaffold', () => {
58
58
  expect(config.description).toBe('AI Agents & Workflows built with Output.ai for test-folder');
59
59
  });
60
60
  it('should prompt for project name and folder name when not provided', async () => {
61
- const { input } = await import('#utils/prompt.js');
61
+ const { input } = await import('@inquirer/prompts');
62
62
  vi.mocked(input)
63
63
  .mockResolvedValueOnce('Test Project')
64
64
  .mockResolvedValueOnce('test-project');
@@ -73,7 +73,7 @@ describe('project_scaffold', () => {
73
73
  it('should not prompt when all dependencies are available', async () => {
74
74
  const { isDockerInstalled } = await import('#services/docker.js');
75
75
  const { isClaudeCliAvailable } = await import('#utils/claude.js');
76
- const { confirm } = await import('#utils/prompt.js');
76
+ const { confirm } = await import('@inquirer/prompts');
77
77
  vi.mocked(isDockerInstalled).mockReturnValue(true);
78
78
  vi.mocked(isClaudeCliAvailable).mockReturnValue(true);
79
79
  await checkDependencies();
@@ -82,7 +82,7 @@ describe('project_scaffold', () => {
82
82
  it('should prompt user when docker is missing', async () => {
83
83
  const { isDockerInstalled } = await import('#services/docker.js');
84
84
  const { isClaudeCliAvailable } = await import('#utils/claude.js');
85
- const { confirm } = await import('#utils/prompt.js');
85
+ const { confirm } = await import('@inquirer/prompts');
86
86
  vi.mocked(isDockerInstalled).mockReturnValue(false);
87
87
  vi.mocked(isClaudeCliAvailable).mockReturnValue(true);
88
88
  vi.mocked(confirm).mockResolvedValue(true);
@@ -94,7 +94,7 @@ describe('project_scaffold', () => {
94
94
  it('should throw UserCancelledError when user declines to proceed', async () => {
95
95
  const { isDockerInstalled } = await import('#services/docker.js');
96
96
  const { isClaudeCliAvailable } = await import('#utils/claude.js');
97
- const { confirm } = await import('#utils/prompt.js');
97
+ const { confirm } = await import('@inquirer/prompts');
98
98
  vi.mocked(isDockerInstalled).mockReturnValue(false);
99
99
  vi.mocked(isClaudeCliAvailable).mockReturnValue(true);
100
100
  vi.mocked(confirm).mockResolvedValue(false);
@@ -2,8 +2,7 @@
2
2
  * Workflow builder service for implementing workflows from plan files
3
3
  */
4
4
  import { ADDITIONAL_INSTRUCTIONS, BUILD_COMMAND_OPTIONS, invokeBuildWorkflow as invokeBuildWorkflowFromClient, replyToClaude } from './claude_client.js';
5
- import { input } from '#utils/prompt.js';
6
- import { isInteractive } from '#utils/interactive.js';
5
+ import { input } from '@inquirer/prompts';
7
6
  import { ux } from '@oclif/core';
8
7
  import fs from 'node:fs/promises';
9
8
  import path from 'node:path';
@@ -71,9 +70,6 @@ async function processModification(modification, currentOutput) {
71
70
  }
72
71
  }
73
72
  async function interactiveRefinementLoop(currentOutput) {
74
- if (!isInteractive()) {
75
- return currentOutput;
76
- }
77
73
  const modification = await promptForModification();
78
74
  if (isAcceptCommand(modification)) {
79
75
  return currentOutput;
@@ -1,12 +1,11 @@
1
1
  import { describe, it, expect, beforeEach, vi } from 'vitest';
2
2
  import { buildWorkflow, buildWorkflowInteractiveLoop } from './workflow_builder.js';
3
3
  import { ADDITIONAL_INSTRUCTIONS, BUILD_COMMAND_OPTIONS, invokeBuildWorkflow, replyToClaude } from './claude_client.js';
4
- import { input } from '#utils/prompt.js';
4
+ import { input } from '@inquirer/prompts';
5
5
  import { ux } from '@oclif/core';
6
6
  import fs from 'node:fs/promises';
7
7
  vi.mock('./claude_client.js');
8
- vi.mock('#utils/prompt.js');
9
- vi.mock('#utils/interactive.js', () => ({ isInteractive: () => true }));
8
+ vi.mock('@inquirer/prompts');
10
9
  vi.mock('@oclif/core', () => ({
11
10
  ux: {
12
11
  stdout: vi.fn(),
@@ -1,5 +1,5 @@
1
- import type { GetWorkflowIdResult200, GetWorkflowIdResult200Status } from '../api/generated/api.js';
2
- type WorkflowResult = Pick<GetWorkflowIdResult200, 'workflowId' | 'output' | 'status' | 'error'>;
3
- export declare const ERROR_STATUSES: ReadonlySet<GetWorkflowIdResult200Status | undefined>;
1
+ import type { WorkflowResultResponse, WorkflowResultResponseStatus } from '../api/generated/api.js';
2
+ type WorkflowResult = Pick<WorkflowResultResponse, 'workflowId' | 'output' | 'status' | 'error'>;
3
+ export declare const ERROR_STATUSES: ReadonlySet<WorkflowResultResponseStatus | undefined>;
4
4
  export declare function formatWorkflowResult(result: WorkflowResult): string;
5
5
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@outputai/cli",
3
- "version": "0.1.13-dev.98dfd72.0",
3
+ "version": "0.1.13-dev.e3e3291.0",
4
4
  "description": "CLI for Output.ai workflow generation",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -34,11 +34,10 @@
34
34
  "ky": "1.14.3",
35
35
  "react": "19.2.4",
36
36
  "semver": "7.7.4",
37
- "undici": "8.0.2",
38
37
  "yaml": "^2.8.3",
39
- "@outputai/credentials": "0.1.13-dev.98dfd72.0",
40
- "@outputai/evals": "0.1.13-dev.98dfd72.0",
41
- "@outputai/llm": "0.1.13-dev.98dfd72.0"
38
+ "@outputai/evals": "0.1.13-dev.e3e3291.0",
39
+ "@outputai/credentials": "0.1.13-dev.e3e3291.0",
40
+ "@outputai/llm": "0.1.13-dev.e3e3291.0"
42
41
  },
43
42
  "devDependencies": {
44
43
  "@types/cli-progress": "3.11.6",
@@ -1,14 +0,0 @@
1
- import { Command } from '@oclif/core';
2
- export default class CredentialsSet extends Command {
3
- static description: string;
4
- static examples: string[];
5
- static args: {
6
- path: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
7
- value: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
8
- };
9
- static flags: {
10
- environment: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
- workflow: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
- };
13
- run(): Promise<void>;
14
- }
@@ -1,57 +0,0 @@
1
- import { Args, Command, Flags } from '@oclif/core';
2
- import { load as parseYaml, dump as stringifyYaml } from 'js-yaml';
3
- import { decryptCredentials, credentialsExist, writeEncrypted, resolveCredentialsPath } from '#services/credentials_service.js';
4
- const setNestedValue = (obj, dotPath, value) => {
5
- const parts = dotPath.split('.');
6
- const parent = parts.slice(0, -1).reduce((current, key) => {
7
- if (!current[key] || typeof current[key] !== 'object') {
8
- current[key] = {};
9
- }
10
- return current[key];
11
- }, obj);
12
- parent[parts[parts.length - 1]] = value;
13
- };
14
- export default class CredentialsSet extends Command {
15
- static description = 'Set a credential value by dot-notation path';
16
- static examples = [
17
- '<%= config.bin %> <%= command.id %> anthropic.api_key sk-ant-...',
18
- '<%= config.bin %> <%= command.id %> openai.api_key sk-... --environment production',
19
- '<%= config.bin %> <%= command.id %> stripe.key sk_live_... --workflow my_workflow'
20
- ];
21
- static args = {
22
- path: Args.string({
23
- description: 'Dot-notation path to the credential (e.g. anthropic.api_key)',
24
- required: true
25
- }),
26
- value: Args.string({
27
- description: 'Value to set',
28
- required: true
29
- })
30
- };
31
- static flags = {
32
- environment: Flags.string({
33
- char: 'e',
34
- description: 'Target environment (e.g. production, development)'
35
- }),
36
- workflow: Flags.string({
37
- char: 'w',
38
- description: 'Target a specific workflow directory'
39
- })
40
- };
41
- async run() {
42
- const { args, flags } = await this.parse(CredentialsSet);
43
- const environment = flags.environment;
44
- const workflow = flags.workflow;
45
- if (environment && workflow) {
46
- this.error('Cannot specify both --environment and --workflow.');
47
- }
48
- if (!credentialsExist(environment, workflow)) {
49
- this.error(`No credentials file found at ${resolveCredentialsPath(environment, workflow)}. Run "output credentials init" first.`);
50
- }
51
- const plaintext = decryptCredentials(environment, workflow);
52
- const data = (parseYaml(plaintext) || {});
53
- setNestedValue(data, args.path, args.value);
54
- writeEncrypted(environment, stringifyYaml(data), workflow);
55
- this.log(`Set ${args.path}`);
56
- }
57
- }
@@ -1,95 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
3
- import * as credentialsService from '#services/credentials_service.js';
4
- import CredentialsSet from './set.js';
5
- vi.mock('#services/credentials_service.js');
6
- vi.mock('js-yaml', () => ({
7
- load: vi.fn((yaml) => {
8
- if (yaml.includes('sk-existing')) {
9
- return { anthropic: { api_key: 'sk-existing' } };
10
- }
11
- return {};
12
- }),
13
- dump: vi.fn((obj) => JSON.stringify(obj))
14
- }));
15
- describe('credentials set command', () => {
16
- beforeEach(() => {
17
- vi.clearAllMocks();
18
- vi.mocked(credentialsService.credentialsExist).mockReturnValue(true);
19
- vi.mocked(credentialsService.decryptCredentials).mockReturnValue('anthropic:\n api_key: sk-existing\n');
20
- vi.mocked(credentialsService.writeEncrypted).mockImplementation(() => { });
21
- });
22
- afterEach(() => {
23
- vi.restoreAllMocks();
24
- });
25
- const createTestCommand = (parsedArgs = {}, flags = {}) => {
26
- const cmd = new CredentialsSet([], {});
27
- cmd.log = vi.fn();
28
- cmd.error = vi.fn((msg) => {
29
- throw new Error(msg);
30
- });
31
- Object.defineProperty(cmd, 'parse', {
32
- value: vi.fn().mockResolvedValue({
33
- args: { path: 'anthropic.api_key', value: 'sk-new-key', ...parsedArgs },
34
- flags: { environment: undefined, workflow: undefined, ...flags }
35
- }),
36
- configurable: true
37
- });
38
- return cmd;
39
- };
40
- describe('command structure', () => {
41
- it('should have correct description', () => {
42
- expect(CredentialsSet.description).toContain('credential value');
43
- });
44
- it('should have required path and value arguments', () => {
45
- expect(CredentialsSet.args.path).toBeDefined();
46
- expect(CredentialsSet.args.path.required).toBe(true);
47
- expect(CredentialsSet.args.value).toBeDefined();
48
- expect(CredentialsSet.args.value.required).toBe(true);
49
- });
50
- it('should have environment and workflow flags', () => {
51
- expect(CredentialsSet.flags.environment).toBeDefined();
52
- expect(CredentialsSet.flags.workflow).toBeDefined();
53
- });
54
- });
55
- describe('command execution', () => {
56
- it('should decrypt, update, and re-encrypt credentials', async () => {
57
- const cmd = createTestCommand();
58
- await cmd.run();
59
- expect(credentialsService.decryptCredentials).toHaveBeenCalledWith(undefined, undefined);
60
- expect(credentialsService.writeEncrypted).toHaveBeenCalledWith(undefined, expect.any(String), undefined);
61
- expect(cmd.log).toHaveBeenCalledWith('Set anthropic.api_key');
62
- });
63
- it('should create nested keys that do not exist', async () => {
64
- vi.mocked(credentialsService.decryptCredentials).mockReturnValue('');
65
- const cmd = createTestCommand({ path: 'new.nested.key', value: 'my-value' });
66
- await cmd.run();
67
- expect(credentialsService.writeEncrypted).toHaveBeenCalledTimes(1);
68
- expect(cmd.log).toHaveBeenCalledWith('Set new.nested.key');
69
- });
70
- it('should pass environment flag to service functions', async () => {
71
- const cmd = createTestCommand({}, { environment: 'production' });
72
- await cmd.run();
73
- expect(credentialsService.credentialsExist).toHaveBeenCalledWith('production', undefined);
74
- expect(credentialsService.decryptCredentials).toHaveBeenCalledWith('production', undefined);
75
- expect(credentialsService.writeEncrypted).toHaveBeenCalledWith('production', expect.any(String), undefined);
76
- });
77
- it('should pass workflow flag to service functions', async () => {
78
- const cmd = createTestCommand({}, { workflow: 'my_workflow' });
79
- await cmd.run();
80
- expect(credentialsService.credentialsExist).toHaveBeenCalledWith(undefined, 'my_workflow');
81
- expect(credentialsService.decryptCredentials).toHaveBeenCalledWith(undefined, 'my_workflow');
82
- expect(credentialsService.writeEncrypted).toHaveBeenCalledWith(undefined, expect.any(String), 'my_workflow');
83
- });
84
- it('should error when both environment and workflow are specified', async () => {
85
- const cmd = createTestCommand({}, { environment: 'production', workflow: 'my_workflow' });
86
- await expect(cmd.run()).rejects.toThrow('Cannot specify both');
87
- });
88
- it('should error when credentials file does not exist', async () => {
89
- vi.mocked(credentialsService.credentialsExist).mockReturnValue(false);
90
- vi.mocked(credentialsService.resolveCredentialsPath).mockReturnValue('/project/config/credentials.yml.enc');
91
- const cmd = createTestCommand();
92
- await expect(cmd.run()).rejects.toThrow('No credentials file found');
93
- });
94
- });
95
- });
@@ -1,2 +0,0 @@
1
- export declare const setNonInteractive: (value: boolean) => void;
2
- export declare const isInteractive: () => boolean;
@@ -1,5 +0,0 @@
1
- const state = { nonInteractive: false };
2
- export const setNonInteractive = (value) => {
3
- state.nonInteractive = value;
4
- };
5
- export const isInteractive = () => !state.nonInteractive && !!process.stdin.isTTY;
@@ -1 +0,0 @@
1
- export {};