@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
@@ -24,11 +24,7 @@ export const WorkflowRunInfoStatus = {
24
24
  timed_out: 'timed_out',
25
25
  continued: 'continued',
26
26
  };
27
- export const PostWorkflowRun200Status = {
28
- completed: 'completed',
29
- failed: 'failed',
30
- };
31
- export const GetWorkflowIdStatus200Status = {
27
+ export const WorkflowStatusResponseStatus = {
32
28
  canceled: 'canceled',
33
29
  completed: 'completed',
34
30
  continued_as_new: 'continued_as_new',
@@ -38,7 +34,7 @@ export const GetWorkflowIdStatus200Status = {
38
34
  timed_out: 'timed_out',
39
35
  unspecified: 'unspecified',
40
36
  };
41
- export const GetWorkflowIdResult200Status = {
37
+ export const WorkflowResultResponseStatus = {
42
38
  completed: 'completed',
43
39
  failed: 'failed',
44
40
  canceled: 'canceled',
@@ -46,6 +42,10 @@ export const GetWorkflowIdResult200Status = {
46
42
  timed_out: 'timed_out',
47
43
  continued: 'continued',
48
44
  };
45
+ export const PostWorkflowRun200Status = {
46
+ completed: 'completed',
47
+ failed: 'failed',
48
+ };
49
49
  ;
50
50
  export const getGetHealthUrl = () => {
51
51
  return `/health`;
@@ -87,6 +87,24 @@ export const getWorkflowIdStatus = async (id, options) => {
87
87
  method: 'GET'
88
88
  });
89
89
  };
90
+ export const getGetWorkflowIdRunsRidStatusUrl = (id, rid) => {
91
+ return `/workflow/${id}/runs/${rid}/status`;
92
+ };
93
+ export const getWorkflowIdRunsRidStatus = async (id, rid, options) => {
94
+ return customFetchInstance(getGetWorkflowIdRunsRidStatusUrl(id, rid), {
95
+ ...options,
96
+ method: 'GET'
97
+ });
98
+ };
99
+ export const getPatchWorkflowIdRunsRidStopUrl = (id, rid) => {
100
+ return `/workflow/${id}/runs/${rid}/stop`;
101
+ };
102
+ export const patchWorkflowIdRunsRidStop = async (id, rid, options) => {
103
+ return customFetchInstance(getPatchWorkflowIdRunsRidStopUrl(id, rid), {
104
+ ...options,
105
+ method: 'PATCH'
106
+ });
107
+ };
90
108
  export const getPatchWorkflowIdStopUrl = (id) => {
91
109
  return `/workflow/${id}/stop`;
92
110
  };
@@ -96,6 +114,17 @@ export const patchWorkflowIdStop = async (id, options) => {
96
114
  method: 'PATCH'
97
115
  });
98
116
  };
117
+ export const getPostWorkflowIdRunsRidTerminateUrl = (id, rid) => {
118
+ return `/workflow/${id}/runs/${rid}/terminate`;
119
+ };
120
+ export const postWorkflowIdRunsRidTerminate = async (id, rid, postWorkflowIdRunsRidTerminateBody, options) => {
121
+ return customFetchInstance(getPostWorkflowIdRunsRidTerminateUrl(id, rid), {
122
+ ...options,
123
+ method: 'POST',
124
+ headers: { 'Content-Type': 'application/json', ...options?.headers },
125
+ body: JSON.stringify(postWorkflowIdRunsRidTerminateBody)
126
+ });
127
+ };
99
128
  export const getPostWorkflowIdTerminateUrl = (id) => {
100
129
  return `/workflow/${id}/terminate`;
101
130
  };
@@ -107,15 +136,26 @@ export const postWorkflowIdTerminate = async (id, postWorkflowIdTerminateBody, o
107
136
  body: JSON.stringify(postWorkflowIdTerminateBody)
108
137
  });
109
138
  };
139
+ export const getPostWorkflowIdRunsRidResetUrl = (id, rid) => {
140
+ return `/workflow/${id}/runs/${rid}/reset`;
141
+ };
142
+ export const postWorkflowIdRunsRidReset = async (id, rid, resetWorkflowRequest, options) => {
143
+ return customFetchInstance(getPostWorkflowIdRunsRidResetUrl(id, rid), {
144
+ ...options,
145
+ method: 'POST',
146
+ headers: { 'Content-Type': 'application/json', ...options?.headers },
147
+ body: JSON.stringify(resetWorkflowRequest)
148
+ });
149
+ };
110
150
  export const getPostWorkflowIdResetUrl = (id) => {
111
151
  return `/workflow/${id}/reset`;
112
152
  };
113
- export const postWorkflowIdReset = async (id, postWorkflowIdResetBody, options) => {
153
+ export const postWorkflowIdReset = async (id, resetWorkflowRequest, options) => {
114
154
  return customFetchInstance(getPostWorkflowIdResetUrl(id), {
115
155
  ...options,
116
156
  method: 'POST',
117
157
  headers: { 'Content-Type': 'application/json', ...options?.headers },
118
- body: JSON.stringify(postWorkflowIdResetBody)
158
+ body: JSON.stringify(resetWorkflowRequest)
119
159
  });
120
160
  };
121
161
  export const getGetWorkflowIdResultUrl = (id) => {
@@ -127,6 +167,15 @@ export const getWorkflowIdResult = async (id, options) => {
127
167
  method: 'GET'
128
168
  });
129
169
  };
170
+ export const getGetWorkflowIdRunsRidResultUrl = (id, rid) => {
171
+ return `/workflow/${id}/runs/${rid}/result`;
172
+ };
173
+ export const getWorkflowIdRunsRidResult = async (id, rid, options) => {
174
+ return customFetchInstance(getGetWorkflowIdRunsRidResultUrl(id, rid), {
175
+ ...options,
176
+ method: 'GET'
177
+ });
178
+ };
130
179
  export const getGetWorkflowIdTraceLogUrl = (id) => {
131
180
  return `/workflow/${id}/trace-log`;
132
181
  };
@@ -136,6 +185,15 @@ export const getWorkflowIdTraceLog = async (id, options) => {
136
185
  method: 'GET'
137
186
  });
138
187
  };
188
+ export const getGetWorkflowIdRunsRidTraceLogUrl = (id, rid) => {
189
+ return `/workflow/${id}/runs/${rid}/trace-log`;
190
+ };
191
+ export const getWorkflowIdRunsRidTraceLog = async (id, rid, options) => {
192
+ return customFetchInstance(getGetWorkflowIdRunsRidTraceLogUrl(id, rid), {
193
+ ...options,
194
+ method: 'GET'
195
+ });
196
+ };
139
197
  export const getGetWorkflowCatalogIdUrl = (id) => {
140
198
  return `/workflow/catalog/${id}`;
141
199
  };
@@ -81,7 +81,7 @@ services:
81
81
  condition: service_healthy
82
82
  worker:
83
83
  condition: service_healthy
84
- image: outputai/api:${OUTPUT_API_VERSION:-0.1.13-dev.98dfd72.0}
84
+ image: outputai/api:${OUTPUT_API_VERSION:-0.1.13-dev.e3e3291.0}
85
85
  init: true
86
86
  networks:
87
87
  - main
@@ -1,5 +1,5 @@
1
1
  import { Command } from '@oclif/core';
2
- import { confirm } from '#utils/prompt.js';
2
+ import { confirm } from '@inquirer/prompts';
3
3
  import { applyFix, planFix } from '#services/fix_package.js';
4
4
  import { getErrorMessage } from '#utils/error_utils.js';
5
5
  const Ansi = {
@@ -2,12 +2,12 @@
2
2
  import { describe, it, expect, beforeEach, vi } from 'vitest';
3
3
  import Fix from './fix.js';
4
4
  import * as fixService from '#services/fix_package.js';
5
- import { confirm } from '#utils/prompt.js';
5
+ import { confirm } from '@inquirer/prompts';
6
6
  vi.mock('#services/fix_package.js', () => ({
7
7
  planFix: vi.fn(),
8
8
  applyFix: vi.fn()
9
9
  }));
10
- vi.mock('#utils/prompt.js', () => ({
10
+ vi.mock('@inquirer/prompts', () => ({
11
11
  confirm: vi.fn()
12
12
  }));
13
13
  const basePlan = () => ({
@@ -1,5 +1,5 @@
1
1
  import { Command, Flags } from '@oclif/core';
2
- import { confirm } from '#utils/prompt.js';
2
+ import { confirm } from '@inquirer/prompts';
3
3
  import { fetchLatestVersion, getGlobalInstalledVersion, getLocalInstalledVersion, updateGlobal, updateLocal, isOutdated } from '#services/npm_update_service.js';
4
4
  import { ensureClaudePlugin } from '#services/coding_agents.js';
5
5
  import { getErrorMessage } from '#utils/error_utils.js';
@@ -3,7 +3,7 @@ import { describe, it, expect, beforeEach, vi } from 'vitest';
3
3
  import Update from './update.js';
4
4
  import { fetchLatestVersion, getGlobalInstalledVersion, getLocalInstalledVersion, updateGlobal, updateLocal, isOutdated } from '#services/npm_update_service.js';
5
5
  import { ensureClaudePlugin } from '#services/coding_agents.js';
6
- import { confirm } from '#utils/prompt.js';
6
+ import { confirm } from '@inquirer/prompts';
7
7
  vi.mock('#services/npm_update_service.js', () => ({
8
8
  fetchLatestVersion: vi.fn(),
9
9
  getGlobalInstalledVersion: vi.fn(),
@@ -15,7 +15,7 @@ vi.mock('#services/npm_update_service.js', () => ({
15
15
  vi.mock('#services/coding_agents.js', () => ({
16
16
  ensureClaudePlugin: vi.fn()
17
17
  }));
18
- vi.mock('#utils/prompt.js', () => ({
18
+ vi.mock('@inquirer/prompts', () => ({
19
19
  confirm: vi.fn()
20
20
  }));
21
21
  describe('update command', () => {
@@ -1,6 +1,5 @@
1
1
  import { Command, Flags, ux } from '@oclif/core';
2
- import { input } from '#utils/prompt.js';
3
- import { isInteractive } from '#utils/interactive.js';
2
+ import { input } from '@inquirer/prompts';
4
3
  import { generatePlanName, updateAgentTemplates, writePlanFile } from '#services/workflow_planner.js';
5
4
  import { ensureOutputAISystem } from '#services/coding_agents.js';
6
5
  import { invokePlanWorkflow, PLAN_COMMAND_OPTIONS, replyToClaude } from '#services/claude_client.js';
@@ -46,9 +45,6 @@ export default class WorkflowPlan extends Command {
46
45
  this.log('=========');
47
46
  this.log(originalPlanContent);
48
47
  this.log('=========');
49
- if (!isInteractive()) {
50
- return originalPlanContent;
51
- }
52
48
  const modifications = await input({
53
49
  message: ux.colorize('gray', `Reply or type ${acceptKey} to accept the plan as is: `),
54
50
  validate: (value) => value.length >= 10 || value === acceptKey
@@ -3,12 +3,11 @@ import WorkflowPlan from './plan.js';
3
3
  import { generatePlanName, writePlanFile, updateAgentTemplates } from '#services/workflow_planner.js';
4
4
  import { ensureOutputAISystem } from '#services/coding_agents.js';
5
5
  import { invokePlanWorkflow, replyToClaude, ClaudeInvocationError } from '#services/claude_client.js';
6
- import { input } from '#utils/prompt.js';
6
+ import { input } from '@inquirer/prompts';
7
7
  vi.mock('#services/workflow_planner.js');
8
8
  vi.mock('#services/coding_agents.js');
9
9
  vi.mock('#services/claude_client.js');
10
- vi.mock('#utils/prompt.js');
11
- vi.mock('#utils/interactive.js', () => ({ isInteractive: () => true }));
10
+ vi.mock('@inquirer/prompts');
12
11
  describe('WorkflowPlan Command', () => {
13
12
  const createCommand = () => {
14
13
  const cmd = new WorkflowPlan([], {});
@@ -55,8 +55,7 @@ export default class WorkflowRun extends Command {
55
55
  }),
56
56
  'task-queue': Flags.string({
57
57
  char: 'q',
58
- description: 'Task queue name for workflow execution (defaults to OUTPUT_CATALOG_ID)',
59
- env: 'OUTPUT_CATALOG_ID'
58
+ description: 'Task queue name for workflow execution'
60
59
  }),
61
60
  format: Flags.string({
62
61
  char: 'f',
@@ -13,7 +13,6 @@ vi.mock('#utils/sleep.js', () => ({
13
13
  describe('workflow run command', () => {
14
14
  beforeEach(async () => {
15
15
  vi.clearAllMocks();
16
- delete process.env.OUTPUT_CATALOG_ID;
17
16
  const { resolveInput } = await import('#utils/resolve_input.js');
18
17
  const { sleep } = await import('#utils/sleep.js');
19
18
  vi.mocked(resolveInput).mockResolvedValue({});
@@ -28,8 +28,7 @@ export default class WorkflowStart extends Command {
28
28
  }),
29
29
  'task-queue': Flags.string({
30
30
  char: 'q',
31
- description: 'Task queue name for workflow execution (defaults to OUTPUT_CATALOG_ID)',
32
- env: 'OUTPUT_CATALOG_ID'
31
+ description: 'Task queue name for workflow execution'
33
32
  })
34
33
  };
35
34
  async run() {
@@ -5,7 +5,6 @@ vi.mock('../../api/generated/api.js', () => ({
5
5
  describe('workflow start command', () => {
6
6
  beforeEach(() => {
7
7
  vi.clearAllMocks();
8
- delete process.env.OUTPUT_CATALOG_ID;
9
8
  });
10
9
  describe('command definition', () => {
11
10
  it('should export a valid OCLIF command', async () => {
@@ -114,7 +114,7 @@ export default class WorkflowTest extends Command {
114
114
  }
115
115
  };
116
116
  if (save) {
117
- const filePath = join(dir, `${dataset.name}.yml`);
117
+ const filePath = dataset._source ?? join(dir, `${dataset.name}.yml`);
118
118
  await writeDataset(updated, filePath);
119
119
  this.log(` Saved output to ${filePath}`);
120
120
  }
@@ -137,7 +137,7 @@ export default class WorkflowTest extends Command {
137
137
  date: now
138
138
  }
139
139
  };
140
- const filePath = join(dir, `${dataset.name}.yml`);
140
+ const filePath = dataset._source ?? join(dir, `${dataset.name}.yml`);
141
141
  await writeDataset(updated, filePath);
142
142
  this.log(` Saved eval result to ${filePath}`);
143
143
  }
@@ -1,3 +1,3 @@
1
1
  {
2
- "framework": "0.1.13-dev.98dfd72.0"
2
+ "framework": "0.1.13-dev.e3e3291.0"
3
3
  }
@@ -1,10 +1,6 @@
1
1
  import { ux } from '@oclif/core';
2
2
  import { checkForUpdate } from '#services/version_check.js';
3
- import { setNonInteractive } from '#utils/interactive.js';
4
3
  const hook = async function () {
5
- if (process.argv.includes('--yes') || process.argv.includes('--non-interactive') || !process.stdin.isTTY) {
6
- setNonInteractive(true);
7
- }
8
4
  try {
9
5
  const result = await checkForUpdate(this.config.version, this.config.cacheDir);
10
6
  if (!result.updateAvailable) {
@@ -7,7 +7,7 @@ import { access } from 'node:fs/promises';
7
7
  import path from 'node:path';
8
8
  import { join } from 'node:path';
9
9
  import { ux } from '@oclif/core';
10
- import { confirm } from '#utils/prompt.js';
10
+ import { confirm } from '@inquirer/prompts';
11
11
  import debugFactory from 'debug';
12
12
  import { getTemplateDir } from '#utils/paths.js';
13
13
  import { executeClaudeCommand } from '#utils/claude.js';
@@ -19,7 +19,7 @@ vi.mock('@oclif/core', () => ({
19
19
  colorize: vi.fn().mockImplementation((_color, text) => text)
20
20
  }
21
21
  }));
22
- vi.mock('#utils/prompt.js', () => ({
22
+ vi.mock('@inquirer/prompts', () => ({
23
23
  confirm: vi.fn()
24
24
  }));
25
25
  describe('coding_agents service', () => {
@@ -157,7 +157,7 @@ describe('coding_agents service', () => {
157
157
  });
158
158
  it('should show error and prompt user when plugin commands fail', async () => {
159
159
  const { executeClaudeCommand } = await import('../utils/claude.js');
160
- const { confirm } = await import('#utils/prompt.js');
160
+ const { confirm } = await import('@inquirer/prompts');
161
161
  vi.mocked(executeClaudeCommand)
162
162
  .mockResolvedValueOnce(undefined) // marketplace add
163
163
  .mockRejectedValueOnce(new Error('Plugin update failed')); // marketplace update
@@ -169,7 +169,7 @@ describe('coding_agents service', () => {
169
169
  });
170
170
  it('should allow user to proceed without plugin setup if they confirm', async () => {
171
171
  const { executeClaudeCommand } = await import('../utils/claude.js');
172
- const { confirm } = await import('#utils/prompt.js');
172
+ const { confirm } = await import('@inquirer/prompts');
173
173
  vi.mocked(executeClaudeCommand)
174
174
  .mockRejectedValue(new Error('All plugin commands fail'));
175
175
  vi.mocked(confirm).mockResolvedValue(true);
@@ -219,7 +219,7 @@ describe('coding_agents service', () => {
219
219
  });
220
220
  it('should show error and prompt user when registerPluginMarketplace fails', async () => {
221
221
  const { executeClaudeCommand } = await import('../utils/claude.js');
222
- const { confirm } = await import('#utils/prompt.js');
222
+ const { confirm } = await import('@inquirer/prompts');
223
223
  vi.mocked(executeClaudeCommand)
224
224
  .mockResolvedValueOnce(undefined) // marketplace add
225
225
  .mockRejectedValueOnce(new Error('Plugin update failed')); // marketplace update
@@ -231,7 +231,7 @@ describe('coding_agents service', () => {
231
231
  });
232
232
  it('should show error and prompt user when installOutputAIPlugin fails', async () => {
233
233
  const { executeClaudeCommand } = await import('../utils/claude.js');
234
- const { confirm } = await import('#utils/prompt.js');
234
+ const { confirm } = await import('@inquirer/prompts');
235
235
  vi.mocked(executeClaudeCommand)
236
236
  .mockResolvedValueOnce(undefined) // marketplace add
237
237
  .mockResolvedValueOnce(undefined) // marketplace update
@@ -244,7 +244,7 @@ describe('coding_agents service', () => {
244
244
  });
245
245
  it('should allow user to proceed without plugin setup if they confirm', async () => {
246
246
  const { executeClaudeCommand } = await import('../utils/claude.js');
247
- const { confirm } = await import('#utils/prompt.js');
247
+ const { confirm } = await import('@inquirer/prompts');
248
248
  vi.mocked(executeClaudeCommand)
249
249
  .mockRejectedValue(new Error('All plugin commands fail'));
250
250
  vi.mocked(confirm).mockResolvedValue(true);
@@ -1,4 +1,4 @@
1
- import { password, confirm } from '#utils/prompt.js';
1
+ import { password, confirm } from '@inquirer/prompts';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { ux } from '@oclif/core';
@@ -8,7 +8,7 @@ export interface DatasetInfo {
8
8
  }
9
9
  export declare function resolveDatasetsDir(workflowName: string, basePath?: string): string | null;
10
10
  export declare function resolveDefaultDatasetsDir(workflowName: string, basePath?: string): string;
11
- export declare function readDataset(filePath: string): Promise<Dataset>;
11
+ export declare function readDatasetFile(filePath: string): Promise<Dataset[]>;
12
12
  export declare function readAllDatasets(workflowName: string, filterNames?: string[], basePath?: string): Promise<{
13
13
  datasets: Dataset[];
14
14
  dir: string;
@@ -24,10 +24,19 @@ export function resolveDefaultDatasetsDir(workflowName, basePath = process.cwd()
24
24
  // Default to first workflows path
25
25
  return resolve(basePath, WORKFLOWS_PATHS[0], workflowName, DATASETS_DIR);
26
26
  }
27
- export async function readDataset(filePath) {
28
- const content = await readFile(filePath, 'utf-8');
29
- const raw = yaml.load(content);
30
- return DatasetSchema.parse(raw);
27
+ export async function readDatasetFile(filePath) {
28
+ const raw = yaml.load(await readFile(filePath, 'utf-8'));
29
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
30
+ throw new Error(`Invalid dataset file: ${filePath}`);
31
+ }
32
+ return Object.entries(raw).map(([name, body]) => {
33
+ if (!body || typeof body !== 'object' || !('input' in body)) {
34
+ throw new Error(`Dataset case "${name}" in ${filePath} is missing required "input" field`);
35
+ }
36
+ const dataset = DatasetSchema.parse({ name, ...body });
37
+ dataset._source = filePath;
38
+ return dataset;
39
+ });
31
40
  }
32
41
  export async function readAllDatasets(workflowName, filterNames, basePath) {
33
42
  const dir = resolveDatasetsDir(workflowName, basePath);
@@ -36,42 +45,35 @@ export async function readAllDatasets(workflowName, filterNames, basePath) {
36
45
  }
37
46
  const files = await readdir(dir);
38
47
  const ymlFiles = files.filter(f => f.endsWith('.yml') || f.endsWith('.yaml'));
48
+ const seen = new Set();
39
49
  const datasets = [];
40
50
  for (const file of ymlFiles) {
41
- const dataset = await readDataset(join(dir, file));
42
- if (filterNames && !filterNames.includes(dataset.name)) {
43
- continue;
51
+ const cases = await readDatasetFile(join(dir, file));
52
+ for (const dataset of cases) {
53
+ if (seen.has(dataset.name)) {
54
+ throw new Error(`Duplicate dataset case name "${dataset.name}" found in ${file}`);
55
+ }
56
+ seen.add(dataset.name);
57
+ if (filterNames && !filterNames.includes(dataset.name)) {
58
+ continue;
59
+ }
60
+ datasets.push(dataset);
44
61
  }
45
- datasets.push(dataset);
46
62
  }
47
63
  return { datasets, dir };
48
64
  }
49
- async function mergeWithExisting(dataset, filePath) {
50
- if (!existsSync(filePath)) {
51
- return dataset;
52
- }
53
- try {
54
- const existing = await readDataset(filePath);
55
- return {
56
- ...existing,
57
- ...dataset,
58
- ground_truth: dataset.ground_truth ?? existing.ground_truth,
59
- last_output: dataset.last_output ?? existing.last_output,
60
- last_eval: dataset.last_eval ?? existing.last_eval
61
- };
62
- }
63
- catch {
64
- return dataset;
65
- }
66
- }
67
65
  export async function writeDataset(dataset, filePath) {
68
- const merged = await mergeWithExisting(dataset, filePath);
69
66
  const dir = resolve(filePath, '..');
70
67
  if (!existsSync(dir)) {
71
68
  await mkdir(dir, { recursive: true });
72
69
  }
73
- const content = yaml.dump(merged, { lineWidth: 120, noRefs: true, sortKeys: false });
74
- await writeFile(filePath, content, 'utf-8');
70
+ const loaded = existsSync(filePath) ? yaml.load(await readFile(filePath, 'utf-8')) : null;
71
+ const fileObj = (loaded && typeof loaded === 'object' && !Array.isArray(loaded)) ?
72
+ loaded :
73
+ {};
74
+ const { name, _source, ...caseBody } = dataset;
75
+ fileObj[name] = { ...fileObj[name], ...caseBody };
76
+ await writeFile(filePath, yaml.dump(fileObj, { lineWidth: 120, noRefs: true, sortKeys: false }), 'utf-8');
75
77
  }
76
78
  export async function listDatasets(workflowName, basePath) {
77
79
  const dir = resolveDatasetsDir(workflowName, basePath);
@@ -84,14 +86,16 @@ export async function listDatasets(workflowName, basePath) {
84
86
  for (const file of ymlFiles) {
85
87
  const filePath = join(dir, file);
86
88
  try {
87
- const dataset = await readDataset(filePath);
88
- results.push({
89
- name: dataset.name,
90
- path: filePath,
91
- hasLastOutput: dataset.last_output?.output !== undefined,
92
- lastOutputDate: dataset.last_output?.date,
93
- lastEvalDate: dataset.last_eval?.date
94
- });
89
+ const cases = await readDatasetFile(filePath);
90
+ for (const dataset of cases) {
91
+ results.push({
92
+ name: dataset.name,
93
+ path: filePath,
94
+ hasLastOutput: dataset.last_output?.output !== undefined,
95
+ lastOutputDate: dataset.last_output?.date,
96
+ lastEvalDate: dataset.last_eval?.date
97
+ });
98
+ }
95
99
  }
96
100
  catch {
97
101
  results.push({