@outputai/cli 0.8.1-next.e92f632.0 → 0.8.2-next.2da7213.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 (51) hide show
  1. package/dist/assets/docker/docker-compose-dev.yml +1 -1
  2. package/dist/commands/workflow/cost.d.ts +3 -2
  3. package/dist/commands/workflow/cost.js +4 -11
  4. package/dist/commands/workflow/cost.spec.js +1 -3
  5. package/dist/commands/workflow/dataset/generate.js +3 -3
  6. package/dist/commands/workflow/dataset/list.d.ts +3 -1
  7. package/dist/commands/workflow/dataset/list.js +10 -14
  8. package/dist/commands/workflow/debug.d.ts +2 -6
  9. package/dist/commands/workflow/debug.js +10 -29
  10. package/dist/commands/workflow/debug.spec.js +2 -5
  11. package/dist/commands/workflow/list.d.ts +4 -1
  12. package/dist/commands/workflow/list.js +15 -19
  13. package/dist/commands/workflow/list.spec.js +2 -1
  14. package/dist/commands/workflow/result.d.ts +3 -4
  15. package/dist/commands/workflow/result.js +6 -15
  16. package/dist/commands/workflow/result.spec.js +2 -4
  17. package/dist/commands/workflow/run.d.ts +3 -2
  18. package/dist/commands/workflow/run.js +4 -11
  19. package/dist/commands/workflow/run.spec.js +1 -3
  20. package/dist/commands/workflow/runs/list.d.ts +3 -1
  21. package/dist/commands/workflow/runs/list.js +11 -15
  22. package/dist/commands/workflow/status.d.ts +3 -4
  23. package/dist/commands/workflow/status.js +20 -30
  24. package/dist/commands/workflow/status.spec.js +2 -4
  25. package/dist/commands/workflow/test_eval.d.ts +4 -2
  26. package/dist/commands/workflow/test_eval.js +14 -15
  27. package/dist/commands/workflow/test_eval.spec.d.ts +1 -0
  28. package/dist/commands/workflow/test_eval.spec.js +84 -0
  29. package/dist/generated/framework_version.json +1 -1
  30. package/dist/hooks/init.d.ts +1 -0
  31. package/dist/hooks/init.js +40 -30
  32. package/dist/hooks/init.spec.js +28 -4
  33. package/dist/services/datasets.d.ts +2 -2
  34. package/dist/services/datasets.js +19 -17
  35. package/dist/services/datasets.test.js +37 -2
  36. package/dist/utils/eval_diagnostics.d.ts +7 -0
  37. package/dist/utils/eval_diagnostics.js +35 -0
  38. package/dist/utils/format_workflow_result.spec.js +0 -13
  39. package/dist/utils/scenario_resolver.d.ts +1 -2
  40. package/dist/utils/scenario_resolver.js +3 -33
  41. package/dist/utils/trace_formatter.js +1 -2
  42. package/dist/utils/workflow_dir.d.ts +13 -0
  43. package/dist/utils/workflow_dir.js +58 -0
  44. package/dist/utils/workflow_dir.spec.d.ts +1 -0
  45. package/dist/utils/workflow_dir.spec.js +60 -0
  46. package/oclif.manifest.json +182 -201
  47. package/package.json +4 -4
  48. package/dist/utils/constants.d.ts +0 -5
  49. package/dist/utils/constants.js +0 -4
  50. package/dist/utils/output_formatter.d.ts +0 -2
  51. package/dist/utils/output_formatter.js +0 -11
@@ -1,12 +1,20 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
2
  import { mkdtemp, rm, writeFile, readFile, mkdir } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
4
  import { tmpdir } from 'node:os';
5
5
  import yaml from 'js-yaml';
6
- import { readDatasetFile, readAllDatasets, writeDataset, listDatasets } from './datasets.js';
6
+ import { readDatasetFile, readAllDatasets, writeDataset, listDatasets, resolveDefaultDatasetsDir } from './datasets.js';
7
+ import * as catalog from '#api/workflow_catalog.js';
8
+ vi.mock('#api/workflow_catalog.js', () => ({
9
+ fetchWorkflowCatalog: vi.fn()
10
+ }));
7
11
  const ctx = { tmpDir: '' };
8
12
  beforeEach(async () => {
9
13
  ctx.tmpDir = await mkdtemp(join(tmpdir(), 'output-datasets-test-'));
14
+ // Default: no API. Flat-layout cases resolve offline before this is hit;
15
+ // nested cases override with a resolved catalog.
16
+ vi.mocked(catalog.fetchWorkflowCatalog).mockReset();
17
+ vi.mocked(catalog.fetchWorkflowCatalog).mockRejectedValue(new Error('API unavailable'));
10
18
  });
11
19
  afterEach(async () => {
12
20
  await rm(ctx.tmpDir, { recursive: true, force: true });
@@ -102,6 +110,17 @@ describe('readAllDatasets', () => {
102
110
  expect(datasets).toHaveLength(2);
103
111
  expect(datasets.map(d => d.name).sort()).toEqual(['case_2', 'case_3']);
104
112
  });
113
+ it('resolves a nested workflow folder via the catalog', async () => {
114
+ const datasetsDir = join(ctx.tmpDir, 'src', 'workflows', 'a', 'b', 'c', 'tests', 'datasets');
115
+ await mkdir(datasetsDir, { recursive: true });
116
+ await writeYaml(join(datasetsDir, 'cases.yml'), {
117
+ nested_case: { input: { x: 1 } }
118
+ });
119
+ vi.mocked(catalog.fetchWorkflowCatalog).mockResolvedValue([{ name: 'a_b_c', path: '/app/dist/workflows/a/b/c/workflow.js' }]);
120
+ const { datasets, dir } = await readAllDatasets('a_b_c', undefined, ctx.tmpDir);
121
+ expect(datasets.map(d => d.name)).toEqual(['nested_case']);
122
+ expect(dir).toBe(datasetsDir);
123
+ });
105
124
  it('returns empty datasets and a default dir when workflow has no datasets dir', async () => {
106
125
  const { datasets, dir } = await readAllDatasets('nonexistent_workflow', undefined, ctx.tmpDir);
107
126
  expect(datasets).toHaveLength(0);
@@ -200,3 +219,19 @@ describe('listDatasets', () => {
200
219
  expect(infos).toHaveLength(0);
201
220
  });
202
221
  });
222
+ // ---------------------------------------------------------------------------
223
+ // resolveDefaultDatasetsDir
224
+ // ---------------------------------------------------------------------------
225
+ describe('resolveDefaultDatasetsDir', () => {
226
+ it('returns the nested workflow tests/datasets for first-time generation via catalog', async () => {
227
+ // Workflow dir exists; its tests/datasets does not yet (first generation).
228
+ await mkdir(join(ctx.tmpDir, 'src', 'workflows', 'a', 'b', 'c'), { recursive: true });
229
+ vi.mocked(catalog.fetchWorkflowCatalog).mockResolvedValue([{ name: 'a_b_c', path: '/app/dist/workflows/a/b/c/workflow.js' }]);
230
+ const dir = await resolveDefaultDatasetsDir('a_b_c', ctx.tmpDir);
231
+ expect(dir).toBe(join(ctx.tmpDir, 'src', 'workflows', 'a', 'b', 'c', 'tests', 'datasets'));
232
+ });
233
+ it('falls back to the flat convention when the workflow cannot be resolved', async () => {
234
+ const dir = await resolveDefaultDatasetsDir('unknown_flow', ctx.tmpDir);
235
+ expect(dir).toBe(join(ctx.tmpDir, 'src', 'workflows', 'unknown_flow', 'tests', 'datasets'));
236
+ });
237
+ });
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Explain why a `<wf>_eval` workflow isn't registered. When the eval source
3
+ * exists on disk but isn't in the catalog, it almost always means tests/evals
4
+ * never compiled to dist (a tsconfig exclude dropped it) — so point at that
5
+ * instead of a bare WorkflowNotFoundError.
6
+ */
7
+ export declare function diagnoseMissingEvalWorkflow(workflowName: string, basePath?: string): Promise<string>;
@@ -0,0 +1,35 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { getEvalWorkflowName } from '@outputai/evals';
4
+ import { resolveWorkflowDir } from '#utils/workflow_dir.js';
5
+ const EVAL_WORKFLOW_FILES = ['tests/evals/workflow.ts', 'tests/evals/workflow.js'];
6
+ /**
7
+ * Explain why a `<wf>_eval` workflow isn't registered. When the eval source
8
+ * exists on disk but isn't in the catalog, it almost always means tests/evals
9
+ * never compiled to dist (a tsconfig exclude dropped it) — so point at that
10
+ * instead of a bare WorkflowNotFoundError.
11
+ */
12
+ export async function diagnoseMissingEvalWorkflow(workflowName, basePath) {
13
+ const evalName = getEvalWorkflowName(workflowName);
14
+ const workflowDir = await resolveWorkflowDir(workflowName, basePath);
15
+ const evalSource = workflowDir ?
16
+ EVAL_WORKFLOW_FILES.map(file => resolve(workflowDir, file)).find(existsSync) :
17
+ undefined;
18
+ if (evalSource) {
19
+ return [
20
+ `Eval workflow "${evalName}" is not registered, but its source exists at:`,
21
+ ` ${evalSource}`,
22
+ '',
23
+ 'This usually means tests/evals did not compile to dist. Check your tsconfig:',
24
+ ' - Ensure tests/evals is not excluded (avoid excluding "src/**/tests").',
25
+ ' - Prefer excluding "**/*.spec.ts" and "**/*.test.ts" instead.',
26
+ '',
27
+ 'Rebuild the worker so dist/.../tests/evals/workflow.js exists, then retry.'
28
+ ].join('\n');
29
+ }
30
+ return [
31
+ `No eval workflow defined for "${workflowName}".`,
32
+ '',
33
+ `Create an eval workflow at tests/evals/workflow.ts that registers "${evalName}".`
34
+ ].join('\n');
35
+ }
@@ -1,6 +1,5 @@
1
1
  import { describe, it, expect } from 'vitest';
2
2
  import { formatWorkflowResult } from './format_workflow_result.js';
3
- import { formatOutput } from './output_formatter.js';
4
3
  describe('formatWorkflowResult', () => {
5
4
  it('should display output for completed workflows', () => {
6
5
  const result = formatWorkflowResult({
@@ -76,16 +75,4 @@ describe('formatWorkflowResult', () => {
76
75
  expect(result).toContain('Status: failed');
77
76
  expect(result).not.toContain('Error:');
78
77
  });
79
- it('should work with formatOutput for json format', () => {
80
- const data = {
81
- workflowId: 'wf-456',
82
- status: 'failed',
83
- output: null,
84
- error: 'Activity task failed'
85
- };
86
- const output = formatOutput(data, 'json', formatWorkflowResult);
87
- const parsed = JSON.parse(output);
88
- expect(parsed.status).toBe('failed');
89
- expect(parsed.error).toBe('Activity task failed');
90
- });
91
78
  });
@@ -1,10 +1,9 @@
1
+ export { extractWorkflowRelativePath, findWorkflowDirectoryFromPath } from '#utils/workflow_dir.js';
1
2
  export interface ScenarioResolutionResult {
2
3
  found: boolean;
3
4
  path?: string;
4
5
  searchedPaths: string[];
5
6
  }
6
- export declare function extractWorkflowRelativePath(path: string): string | null;
7
- export declare function findWorkflowDirectoryFromPath(workflowPath: string | undefined, basePath?: string): string | null;
8
7
  export declare function resolveScenarioPath(workflowName: string, scenarioName: string, basePath?: string, workflowPath?: string): Promise<ScenarioResolutionResult>;
9
8
  export declare function listScenariosForWorkflow(workflowName: string, workflowPath?: string, basePath?: string): string[];
10
9
  export declare function getScenarioNotFoundMessage(workflowName: string, scenarioName: string, searchedPaths: string[]): string;
@@ -1,43 +1,13 @@
1
1
  import { existsSync, readdirSync } from 'node:fs';
2
- import { dirname, resolve } from 'node:path';
3
- import { fetchWorkflowCatalog } from '#api/workflow_catalog.js';
2
+ import { resolve } from 'node:path';
4
3
  import { getWorkflowsBasePath } from '#utils/paths.js';
4
+ import { WORKFLOWS_PATHS, candidateWorkflowDirsFromPath, fetchWorkflowPath } from '#utils/workflow_dir.js';
5
+ export { extractWorkflowRelativePath, findWorkflowDirectoryFromPath } from '#utils/workflow_dir.js';
5
6
  const SCENARIOS_DIR = 'scenarios';
6
- const WORKFLOWS_PATHS = ['src/workflows', 'workflows'];
7
- export function extractWorkflowRelativePath(path) {
8
- const match = path.match(/(?:^|\/)workflows\/(.+)\/workflow\.[jt]s$/);
9
- return match ? match[1] : null;
10
- }
11
- function unique(values) {
12
- return [...new Set(values)];
13
- }
14
- function workflowPathSuffixes(workflowPath) {
15
- const parts = dirname(workflowPath).split(/[/\\]+/).filter(Boolean);
16
- return parts.map((_, index) => parts.slice(index));
17
- }
18
- function candidateWorkflowDirsFromPath(workflowPath, basePath) {
19
- return unique(workflowPathSuffixes(workflowPath).flatMap(suffix => WORKFLOWS_PATHS.map(workflowsDir => resolve(basePath, workflowsDir, ...suffix))));
20
- }
21
7
  function candidateScenarioDirsFromPath(workflowPath, basePath) {
22
8
  return candidateWorkflowDirsFromPath(workflowPath, basePath)
23
9
  .map(workflowDir => resolve(workflowDir, SCENARIOS_DIR));
24
10
  }
25
- export function findWorkflowDirectoryFromPath(workflowPath, basePath = getWorkflowsBasePath()) {
26
- if (!workflowPath) {
27
- return null;
28
- }
29
- return candidateWorkflowDirsFromPath(workflowPath, basePath).find(existsSync) ?? null;
30
- }
31
- async function fetchWorkflowPath(workflowName) {
32
- try {
33
- const workflows = await fetchWorkflowCatalog();
34
- const workflow = workflows.find(w => w.name === workflowName);
35
- return workflow?.path ?? null;
36
- }
37
- catch {
38
- return null;
39
- }
40
- }
41
11
  function resolveScenarioFromDirectory(relativeDir, scenarioFileName, basePath) {
42
12
  const searchedPaths = [];
43
13
  for (const workflowsDir of WORKFLOWS_PATHS) {
@@ -1,6 +1,5 @@
1
1
  import Table from 'cli-table3';
2
2
  import { ux } from '@oclif/core';
3
- import { formatOutput } from '#utils/output_formatter.js';
4
3
  import { formatDuration } from '#utils/date_formatter.js';
5
4
  import { getErrorMessage } from '#utils/error_utils.js';
6
5
  import { isTraceEvent, isValidTimestamp } from '#types/trace.js';
@@ -369,7 +368,7 @@ const formatAsText = (trace) => {
369
368
  export function format(traceData, outputFormat = 'text') {
370
369
  const trace = typeof traceData === 'string' ? JSON.parse(traceData) : traceData;
371
370
  if (outputFormat === 'json') {
372
- return formatOutput(trace, 'json');
371
+ return JSON.stringify(trace, null, 2);
373
372
  }
374
373
  return formatAsText(trace);
375
374
  }
@@ -0,0 +1,13 @@
1
+ export declare const WORKFLOWS_PATHS: string[];
2
+ export declare function extractWorkflowRelativePath(path: string): string | null;
3
+ export declare function candidateWorkflowDirsFromPath(workflowPath: string, basePath: string): string[];
4
+ export declare function findWorkflowDirectoryFromPath(workflowPath: string | undefined, basePath?: string): string | null;
5
+ export declare function fetchWorkflowPath(workflowName: string): Promise<string | null>;
6
+ /**
7
+ * Resolve the on-disk directory of a registered workflow by name.
8
+ *
9
+ * Flat-first so flat-layout projects resolve offline (no catalog round-trip)
10
+ * exactly as before; nested folders fall through to the worker catalog, whose
11
+ * `path` is re-rooted under `src/workflows` / `workflows`.
12
+ */
13
+ export declare function resolveWorkflowDir(workflowName: string, basePath?: string, workflowPath?: string): Promise<string | null>;
@@ -0,0 +1,58 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { fetchWorkflowCatalog } from '#api/workflow_catalog.js';
4
+ import { getWorkflowsBasePath } from '#utils/paths.js';
5
+ export const WORKFLOWS_PATHS = ['src/workflows', 'workflows'];
6
+ export function extractWorkflowRelativePath(path) {
7
+ const match = path.match(/(?:^|\/)workflows\/(.+)\/workflow\.[jt]s$/);
8
+ return match ? match[1] : null;
9
+ }
10
+ function unique(values) {
11
+ return [...new Set(values)];
12
+ }
13
+ function workflowPathSuffixes(workflowPath) {
14
+ const parts = dirname(workflowPath).split(/[/\\]+/).filter(Boolean);
15
+ return parts.map((_, index) => parts.slice(index));
16
+ }
17
+ export function candidateWorkflowDirsFromPath(workflowPath, basePath) {
18
+ return unique(workflowPathSuffixes(workflowPath).flatMap(suffix => WORKFLOWS_PATHS.map(workflowsDir => resolve(basePath, workflowsDir, ...suffix))));
19
+ }
20
+ export function findWorkflowDirectoryFromPath(workflowPath, basePath = getWorkflowsBasePath()) {
21
+ if (!workflowPath) {
22
+ return null;
23
+ }
24
+ return candidateWorkflowDirsFromPath(workflowPath, basePath).find(existsSync) ?? null;
25
+ }
26
+ export async function fetchWorkflowPath(workflowName) {
27
+ try {
28
+ const workflows = await fetchWorkflowCatalog();
29
+ const workflow = workflows.find(w => w.name === workflowName);
30
+ return workflow?.path ?? null;
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ }
36
+ /**
37
+ * Resolve the on-disk directory of a registered workflow by name.
38
+ *
39
+ * Flat-first so flat-layout projects resolve offline (no catalog round-trip)
40
+ * exactly as before; nested folders fall through to the worker catalog, whose
41
+ * `path` is re-rooted under `src/workflows` / `workflows`.
42
+ */
43
+ export async function resolveWorkflowDir(workflowName, basePath = getWorkflowsBasePath(), workflowPath) {
44
+ for (const workflowsDir of WORKFLOWS_PATHS) {
45
+ const candidate = resolve(basePath, workflowsDir, workflowName);
46
+ if (existsSync(candidate)) {
47
+ return candidate;
48
+ }
49
+ }
50
+ if (workflowPath) {
51
+ const found = findWorkflowDirectoryFromPath(workflowPath, basePath);
52
+ if (found) {
53
+ return found;
54
+ }
55
+ }
56
+ const catalogPath = await fetchWorkflowPath(workflowName);
57
+ return findWorkflowDirectoryFromPath(catalogPath ?? undefined, basePath);
58
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,60 @@
1
+ import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
2
+ import { resolveWorkflowDir } from './workflow_dir.js';
3
+ import * as fs from 'node:fs';
4
+ import * as catalog from '#api/workflow_catalog.js';
5
+ vi.mock('node:fs', () => ({
6
+ existsSync: vi.fn()
7
+ }));
8
+ vi.mock('#api/workflow_catalog.js', () => ({
9
+ fetchWorkflowCatalog: vi.fn()
10
+ }));
11
+ function mockCatalog(workflows) {
12
+ vi.mocked(catalog.fetchWorkflowCatalog).mockResolvedValue(workflows);
13
+ }
14
+ function mockCatalogFailure() {
15
+ vi.mocked(catalog.fetchWorkflowCatalog).mockRejectedValue(new Error('API unavailable'));
16
+ }
17
+ describe('resolveWorkflowDir', () => {
18
+ beforeEach(() => {
19
+ vi.resetAllMocks();
20
+ });
21
+ afterEach(() => {
22
+ vi.restoreAllMocks();
23
+ });
24
+ it('resolves a flat layout without querying the catalog', async () => {
25
+ vi.mocked(fs.existsSync).mockImplementation(path => String(path) === '/project/src/workflows/simple');
26
+ const result = await resolveWorkflowDir('simple', '/project');
27
+ expect(result).toBe('/project/src/workflows/simple');
28
+ expect(catalog.fetchWorkflowCatalog).not.toHaveBeenCalled();
29
+ });
30
+ it('resolves the workflows/ fallback without querying the catalog', async () => {
31
+ vi.mocked(fs.existsSync).mockImplementation(path => String(path) === '/project/workflows/simple');
32
+ const result = await resolveWorkflowDir('simple', '/project');
33
+ expect(result).toBe('/project/workflows/simple');
34
+ expect(catalog.fetchWorkflowCatalog).not.toHaveBeenCalled();
35
+ });
36
+ it('resolves a nested folder via the catalog path', async () => {
37
+ mockCatalog([{ name: 'a_b_c', path: '/app/dist/workflows/a/b/c/workflow.js' }]);
38
+ vi.mocked(fs.existsSync).mockImplementation(path => String(path) === '/project/src/workflows/a/b/c');
39
+ const result = await resolveWorkflowDir('a_b_c', '/project');
40
+ expect(result).toBe('/project/src/workflows/a/b/c');
41
+ });
42
+ it('returns null when the catalog is unavailable and no flat dir exists', async () => {
43
+ mockCatalogFailure();
44
+ vi.mocked(fs.existsSync).mockReturnValue(false);
45
+ const result = await resolveWorkflowDir('a_b_c', '/project');
46
+ expect(result).toBeNull();
47
+ });
48
+ it('returns null when the workflow is not in the catalog', async () => {
49
+ mockCatalog([{ name: 'other', path: '/app/dist/workflows/other/workflow.js' }]);
50
+ vi.mocked(fs.existsSync).mockReturnValue(false);
51
+ const result = await resolveWorkflowDir('a_b_c', '/project');
52
+ expect(result).toBeNull();
53
+ });
54
+ it('uses a provided workflowPath without querying the catalog', async () => {
55
+ vi.mocked(fs.existsSync).mockImplementation(path => String(path) === '/project/src/workflows/writing/editor');
56
+ const result = await resolveWorkflowDir('writing_editor', '/project', '/app/build-output/writing/editor/workflow.js');
57
+ expect(result).toBe('/project/src/workflows/writing/editor');
58
+ expect(catalog.fetchWorkflowCatalog).not.toHaveBeenCalled();
59
+ });
60
+ });