@outputai/cli 0.1.13-next.9118b84.0 → 0.1.13-next.91c5d78.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.
@@ -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-next.9118b84.0}
84
+ image: outputai/api:${OUTPUT_API_VERSION:-0.1.13-next.91c5d78.0}
85
85
  init: true
86
86
  networks:
87
87
  - main
@@ -4,6 +4,7 @@ import { buildWorkflow, buildWorkflowInteractiveLoop } from '#services/workflow_
4
4
  import { ensureOutputAISystem } from '#services/coding_agents.js';
5
5
  import { getWorkflowGenerateSuccessMessage } from '#services/messages.js';
6
6
  import { DEFAULT_OUTPUT_DIRS } from '#utils/paths.js';
7
+ import { parseWorkflowDir } from '#utils/workflow_dir_parser.js';
7
8
  import path from 'node:path';
8
9
  import * as fsSync from 'node:fs';
9
10
  import { getErrorMessage } from '#utils/error_utils.js';
@@ -83,7 +84,8 @@ export default class Generate extends Command {
83
84
  this.displaySuccess(result);
84
85
  }
85
86
  displaySuccess(result) {
86
- const message = getWorkflowGenerateSuccessMessage(result.workflowName, result.targetDir, result.filesCreated);
87
+ const dirInfo = parseWorkflowDir(result.targetDir);
88
+ const message = getWorkflowGenerateSuccessMessage(result.workflowName, dirInfo.workflowId ?? result.workflowName, dirInfo.scenarioNames[0], result.targetDir, result.filesCreated);
87
89
  this.log(message);
88
90
  }
89
91
  }
@@ -2,10 +2,13 @@
2
2
  import { describe, it, expect, beforeEach, vi } from 'vitest';
3
3
  import Generate from './generate.js';
4
4
  import { generateWorkflow } from '#services/workflow_generator.js';
5
+ import { parseWorkflowDir } from '#utils/workflow_dir_parser.js';
5
6
  import { InvalidNameError, WorkflowExistsError } from '#types/errors.js';
6
7
  vi.mock('../../services/workflow_generator.js');
8
+ vi.mock('../../utils/workflow_dir_parser.js');
7
9
  describe('Generate Command', () => {
8
10
  let mockGenerateWorkflow;
11
+ let mockParseWorkflowDir;
9
12
  let logSpy;
10
13
  const createCommand = () => {
11
14
  const cmd = new Generate([], {});
@@ -20,6 +23,7 @@ describe('Generate Command', () => {
20
23
  beforeEach(() => {
21
24
  vi.clearAllMocks();
22
25
  mockGenerateWorkflow = vi.mocked(generateWorkflow);
26
+ mockParseWorkflowDir = vi.mocked(parseWorkflowDir);
23
27
  });
24
28
  describe('successful workflow generation', () => {
25
29
  it('should generate workflow with skeleton flag', async () => {
@@ -38,6 +42,10 @@ describe('Generate Command', () => {
38
42
  targetDir: '/tmp/test-workflow',
39
43
  filesCreated: ['index.ts', 'steps.ts', 'types.ts']
40
44
  });
45
+ mockParseWorkflowDir.mockReturnValue({
46
+ workflowId: 'testWorkflow',
47
+ scenarioNames: ['test_input']
48
+ });
41
49
  await cmd.run();
42
50
  expect(mockGenerateWorkflow).toHaveBeenCalledWith({
43
51
  name: 'test-workflow',
@@ -105,6 +113,10 @@ describe('Generate Command', () => {
105
113
  targetDir: '/custom/path/my-workflow',
106
114
  filesCreated: ['index.ts', 'steps.ts', 'types.ts']
107
115
  });
116
+ mockParseWorkflowDir.mockReturnValue({
117
+ workflowId: 'myWorkflow',
118
+ scenarioNames: ['test_input']
119
+ });
108
120
  await cmd.run();
109
121
  expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('SUCCESS!'));
110
122
  expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('my-workflow'));
@@ -1,3 +1,3 @@
1
1
  {
2
- "framework": "0.1.13-next.9118b84.0"
2
+ "framework": "0.1.13-next.91c5d78.0"
3
3
  }
@@ -21,8 +21,6 @@ export declare class DockerComposeConfigNotFoundError extends Error {
21
21
  constructor(dockerComposePath: string);
22
22
  }
23
23
  declare const isDockerInstalled: () => boolean;
24
- declare const isDockerComposeAvailable: () => boolean;
25
- declare const isDockerDaemonRunning: () => boolean;
26
24
  export declare function validateDockerEnvironment(): void;
27
25
  export declare function getDefaultDockerComposePath(): string;
28
26
  export declare function parseServiceStatus(jsonOutput: string): ServiceStatus[];
@@ -37,4 +35,4 @@ export type PullPolicy = 'always' | 'missing' | 'never';
37
35
  export declare function startDockerCompose(dockerComposePath: string, pullPolicy?: PullPolicy): Promise<DockerComposeProcess>;
38
36
  export declare function startDockerComposeDetached(dockerComposePath: string, pullPolicy?: PullPolicy): void;
39
37
  export declare function stopDockerCompose(dockerComposePath: string): Promise<void>;
40
- export { isDockerInstalled, isDockerComposeAvailable, isDockerDaemonRunning, DockerValidationError };
38
+ export { isDockerInstalled, DockerValidationError };
@@ -2,6 +2,7 @@ import { execFileSync, execSync, spawn } from 'node:child_process';
2
2
  import path from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { ux } from '@oclif/core';
5
+ import semver from 'semver';
5
6
  const DEFAULT_COMPOSE_PATH = '../assets/docker/docker-compose-dev.yml';
6
7
  export const SERVICE_HEALTH = {
7
8
  HEALTHY: 'healthy',
@@ -30,27 +31,51 @@ const checkDockerCommand = (command) => {
30
31
  return false;
31
32
  }
32
33
  };
34
+ const getCommandVersion = (command, pattern = /(\d+\.\d+\.\d+)/) => {
35
+ try {
36
+ const output = execSync(command, { stdio: 'pipe', encoding: 'utf-8' }).trim();
37
+ const match = output.match(pattern);
38
+ return match ? match[1] : null;
39
+ }
40
+ catch {
41
+ return null;
42
+ }
43
+ };
33
44
  const isDockerInstalled = () => checkDockerCommand('docker --version');
34
- const isDockerComposeAvailable = () => checkDockerCommand('docker compose version');
35
- const isDockerDaemonRunning = () => checkDockerCommand('docker ps');
36
- const DOCKER_VALIDATIONS = [
45
+ const PREREQUISITES = [
37
46
  {
38
- check: isDockerInstalled,
39
- error: 'Docker is not installed. Please install Docker to use the dev command.\nVisit: https://docs.docker.com/get-docker/'
47
+ name: 'Docker',
48
+ semverRange: '>=20.0.0',
49
+ getVersion: () => getCommandVersion('docker --version'),
50
+ errorMessage: (current, required) => current === null ?
51
+ 'Docker is not installed. Please install Docker to use the dev command.\nVisit: https://docs.docker.com/get-docker/' :
52
+ `Docker version ${required} is required (found v${current}).\nVisit: https://docs.docker.com/get-docker/`
40
53
  },
41
54
  {
42
- check: isDockerComposeAvailable,
43
- error: 'Docker Compose is not installed. Please install Docker Compose to use the dev command.\nVisit: https://docs.docker.com/compose/install/'
55
+ name: 'Docker Compose',
56
+ semverRange: '>=2.24.0',
57
+ getVersion: () => getCommandVersion('docker compose version --short'),
58
+ errorMessage: (current, required) => current === null ?
59
+ 'Docker Compose is not installed. Please install Docker Compose to use the dev command.\nVisit: https://docs.docker.com/compose/install/' :
60
+ `Docker Compose ${required} is required (found v${current}).\nPlease update Docker Compose: https://docs.docker.com/compose/install/`
44
61
  },
45
62
  {
46
- check: isDockerDaemonRunning,
47
- error: 'Docker daemon is not running. Please start Docker and try again.'
63
+ name: 'Docker Daemon',
64
+ semverRange: '*',
65
+ getVersion: () => checkDockerCommand('docker ps') ? '0.0.0' : null,
66
+ errorMessage: () => 'Docker daemon is not running. Please start Docker and try again.'
48
67
  }
49
68
  ];
50
69
  export function validateDockerEnvironment() {
51
- const failedValidation = DOCKER_VALIDATIONS.find(v => !v.check());
52
- if (failedValidation) {
53
- throw new DockerValidationError(failedValidation.error);
70
+ for (const prereq of PREREQUISITES) {
71
+ const raw = prereq.getVersion();
72
+ const version = raw ? semver.valid(semver.coerce(raw)) : null;
73
+ if (!version) {
74
+ throw new DockerValidationError(prereq.errorMessage(null, prereq.semverRange));
75
+ }
76
+ if (!semver.satisfies(version, prereq.semverRange)) {
77
+ throw new DockerValidationError(prereq.errorMessage(version, prereq.semverRange));
78
+ }
54
79
  }
55
80
  }
56
81
  export function getDefaultDockerComposePath() {
@@ -129,4 +154,4 @@ export async function stopDockerCompose(dockerComposePath) {
129
154
  ux.stdout('⏹️ Stopping services...\n');
130
155
  execFileSync('docker', ['compose', '-f', dockerComposePath, 'down'], { stdio: 'inherit', cwd: process.cwd() });
131
156
  }
132
- export { isDockerInstalled, isDockerComposeAvailable, isDockerDaemonRunning, DockerValidationError };
157
+ export { isDockerInstalled, DockerValidationError };
@@ -3,4 +3,4 @@
3
3
  */
4
4
  export declare const getEjectSuccessMessage: (destPath: string, outputFile: string, binName: string) => string;
5
5
  export declare const getProjectSuccessMessage: (folderName: string, installSuccess: boolean, credentialsConfigured?: boolean) => string;
6
- export declare const getWorkflowGenerateSuccessMessage: (workflowName: string, targetDir: string, filesCreated: string[]) => string;
6
+ export declare const getWorkflowGenerateSuccessMessage: (workflowName: string, workflowId: string, scenarioName: string | undefined, targetDir: string, filesCreated: string[]) => string;
@@ -234,7 +234,7 @@ ${ux.colorize('dim', ' to manage your project secrets.')}
234
234
  ${ux.colorize('green', ux.colorize('bold', 'Happy building with Output! 🚀'))}
235
235
  `;
236
236
  };
237
- export const getWorkflowGenerateSuccessMessage = (workflowName, targetDir, filesCreated) => {
237
+ export const getWorkflowGenerateSuccessMessage = (workflowName, workflowId, scenarioName, targetDir, filesCreated) => {
238
238
  const divider = ux.colorize('dim', '─'.repeat(80));
239
239
  const bulletPoint = ux.colorize('green', '▸');
240
240
  const formattedFiles = filesCreated.map(file => {
@@ -256,7 +256,7 @@ export const getWorkflowGenerateSuccessMessage = (workflowName, targetDir, files
256
256
  },
257
257
  {
258
258
  step: 'Test your workflow',
259
- command: `npx output workflow run ${workflowName} test_input`,
259
+ command: `npx output workflow run ${workflowId}${scenarioName ? ` ${scenarioName}` : ''}`,
260
260
  note: 'Run after starting services with "npx output dev"'
261
261
  }
262
262
  ];
@@ -4,8 +4,8 @@
4
4
  "WebFetch",
5
5
  "Bash(npx output:*)",
6
6
  "Bash(npm run output:*)",
7
- "Skills(output*)",
8
- "Skills(flow*)"
7
+ "Skill(output*)",
8
+ "Skill(outputai:*)"
9
9
  ]
10
10
  },
11
11
  "enabledPlugins": {
@@ -0,0 +1,5 @@
1
+ export interface WorkflowDirInfo {
2
+ workflowId: string | undefined;
3
+ scenarioNames: string[];
4
+ }
5
+ export declare function parseWorkflowDir(targetDir: string): WorkflowDirInfo;
@@ -0,0 +1,39 @@
1
+ import { readFileSync, readdirSync, existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ const WORKFLOW_FILE_NAMES = ['workflow.ts', 'workflow.js'];
4
+ const SCENARIOS_DIR = 'scenarios';
5
+ const WORKFLOW_NAME_PATTERN = /name:\s*['"]([^'"]+)['"]/;
6
+ function safeReadFile(filePath) {
7
+ try {
8
+ return readFileSync(filePath, 'utf-8');
9
+ }
10
+ catch {
11
+ return '';
12
+ }
13
+ }
14
+ function safeReadDir(dirPath) {
15
+ try {
16
+ return readdirSync(dirPath);
17
+ }
18
+ catch {
19
+ return [];
20
+ }
21
+ }
22
+ function parseWorkflowId(targetDir) {
23
+ const workflowFile = WORKFLOW_FILE_NAMES
24
+ .map(name => join(targetDir, name))
25
+ .find(existsSync);
26
+ const content = workflowFile ? safeReadFile(workflowFile) : '';
27
+ return WORKFLOW_NAME_PATTERN.exec(content)?.[1];
28
+ }
29
+ function listScenarioNames(targetDir) {
30
+ return safeReadDir(join(targetDir, SCENARIOS_DIR))
31
+ .filter(f => f.endsWith('.json'))
32
+ .map(f => f.replace(/\.json$/, ''));
33
+ }
34
+ export function parseWorkflowDir(targetDir) {
35
+ return {
36
+ workflowId: parseWorkflowId(targetDir),
37
+ scenarioNames: listScenarioNames(targetDir)
38
+ };
39
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,74 @@
1
+ /* eslint-disable no-restricted-syntax, init-declarations */
2
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
3
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { tmpdir } from 'node:os';
6
+ import { parseWorkflowDir } from './workflow_dir_parser.js';
7
+ describe('parseWorkflowDir', () => {
8
+ let tempDir;
9
+ beforeEach(() => {
10
+ tempDir = mkdtempSync(join(tmpdir(), 'workflow-dir-parser-'));
11
+ });
12
+ afterEach(() => {
13
+ rmSync(tempDir, { recursive: true, force: true });
14
+ });
15
+ it('should extract workflow ID from workflow.ts', () => {
16
+ writeFileSync(join(tempDir, 'workflow.ts'), 'export default workflow( { name: \'myWorkflow\', description: \'test\' } );');
17
+ const result = parseWorkflowDir(tempDir);
18
+ expect(result.workflowId).toBe('myWorkflow');
19
+ });
20
+ it('should extract workflow ID from workflow.js', () => {
21
+ writeFileSync(join(tempDir, 'workflow.js'), 'export default workflow( { name: "blogGenerator", description: "test" } );');
22
+ const result = parseWorkflowDir(tempDir);
23
+ expect(result.workflowId).toBe('blogGenerator');
24
+ });
25
+ it('should prefer workflow.ts over workflow.js', () => {
26
+ writeFileSync(join(tempDir, 'workflow.ts'), 'export default workflow( { name: \'fromTs\' } );');
27
+ writeFileSync(join(tempDir, 'workflow.js'), 'export default workflow( { name: \'fromJs\' } );');
28
+ const result = parseWorkflowDir(tempDir);
29
+ expect(result.workflowId).toBe('fromTs');
30
+ });
31
+ it('should return undefined workflowId when no workflow file exists', () => {
32
+ const result = parseWorkflowDir(tempDir);
33
+ expect(result.workflowId).toBeUndefined();
34
+ });
35
+ it('should return undefined workflowId when workflow file has no name property', () => {
36
+ writeFileSync(join(tempDir, 'workflow.ts'), 'export default workflow( { description: \'no name here\' } );');
37
+ const result = parseWorkflowDir(tempDir);
38
+ expect(result.workflowId).toBeUndefined();
39
+ });
40
+ it('should list scenario names from scenarios directory', () => {
41
+ const scenariosDir = join(tempDir, 'scenarios');
42
+ mkdirSync(scenariosDir);
43
+ writeFileSync(join(scenariosDir, 'test_input.json'), '{}');
44
+ writeFileSync(join(scenariosDir, 'edge_case.json'), '{}');
45
+ const result = parseWorkflowDir(tempDir);
46
+ expect(result.scenarioNames).toEqual(expect.arrayContaining(['test_input', 'edge_case']));
47
+ expect(result.scenarioNames).toHaveLength(2);
48
+ });
49
+ it('should return empty scenarioNames when no scenarios directory exists', () => {
50
+ const result = parseWorkflowDir(tempDir);
51
+ expect(result.scenarioNames).toEqual([]);
52
+ });
53
+ it('should ignore non-json files in scenarios directory', () => {
54
+ const scenariosDir = join(tempDir, 'scenarios');
55
+ mkdirSync(scenariosDir);
56
+ writeFileSync(join(scenariosDir, 'test_input.json'), '{}');
57
+ writeFileSync(join(scenariosDir, 'README.md'), '# Scenarios');
58
+ const result = parseWorkflowDir(tempDir);
59
+ expect(result.scenarioNames).toEqual(['test_input']);
60
+ });
61
+ it('should handle multiline workflow files', () => {
62
+ writeFileSync(join(tempDir, 'workflow.ts'), `import { workflow } from '@outputai/core';
63
+
64
+ export default workflow( {
65
+ name: 'complexWorkflow',
66
+ description: 'A complex workflow',
67
+ fn: async ( input ) => {
68
+ return input;
69
+ }
70
+ } );`);
71
+ const result = parseWorkflowDir(tempDir);
72
+ expect(result.workflowId).toBe('complexWorkflow');
73
+ });
74
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@outputai/cli",
3
- "version": "0.1.13-next.9118b84.0",
3
+ "version": "0.1.13-next.91c5d78.0",
4
4
  "description": "CLI for Output.ai workflow generation",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -35,9 +35,9 @@
35
35
  "react": "19.2.4",
36
36
  "semver": "7.7.4",
37
37
  "yaml": "^2.8.3",
38
- "@outputai/credentials": "0.1.13-next.9118b84.0",
39
- "@outputai/llm": "0.1.13-next.9118b84.0",
40
- "@outputai/evals": "0.1.13-next.9118b84.0"
38
+ "@outputai/credentials": "0.1.13-next.91c5d78.0",
39
+ "@outputai/evals": "0.1.13-next.91c5d78.0",
40
+ "@outputai/llm": "0.1.13-next.91c5d78.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/cli-progress": "3.11.6",