@outputai/cli 0.1.10 → 0.1.11-next.7b8340c.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.
package/bin/run.js CHANGED
@@ -2,8 +2,10 @@
2
2
 
3
3
  import { execute } from '@oclif/core';
4
4
  import { loadEnvironment } from '../dist/utils/env_loader.js';
5
+ import { resolveCredentialRefs } from '@outputai/credentials';
5
6
 
6
7
  // Load environment variables from .env files before executing CLI
7
8
  loadEnvironment();
9
+ resolveCredentialRefs();
8
10
 
9
11
  await execute( { dir: import.meta.url } );
@@ -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.10}
84
+ image: outputai/api:${OUTPUT_API_VERSION:-0.1.11-next.7b8340c.0}
85
85
  init: true
86
86
  networks:
87
87
  - main
@@ -105,11 +105,11 @@ services:
105
105
  condition: service_healthy
106
106
  image: node:24.13.0-slim
107
107
  healthcheck:
108
- test: [ 'CMD', 'npx', '--yes', 'output-healthcheck' ]
108
+ test: [ 'CMD-SHELL', 'npx output-healthcheck' ]
109
109
  interval: 3s
110
- timeout: 10s
111
- retries: 20
112
- start_period: 30s
110
+ timeout: 3s
111
+ retries: 2
112
+ start_period: 60s
113
113
  init: true
114
114
  networks:
115
115
  - main
@@ -118,6 +118,7 @@ services:
118
118
  required: false
119
119
  environment:
120
120
  - NODE_ENV=development
121
+ - COREPACK_ENABLE_DOWNLOAD_PROMPT=0
121
122
  - OUTPUT_CATALOG_ID=${OUTPUT_CATALOG_ID:-main}
122
123
  - OUTPUT_REDIS_URL=redis://redis:6379
123
124
  - OUTPUT_TRACE_LOCAL_ON=${OUTPUT_TRACE_LOCAL_ON:-true}
@@ -127,6 +128,7 @@ services:
127
128
  - NODE_OPTIONS=${NODE_OPTIONS:---max-old-space-size=4096}
128
129
  command: >
129
130
  sh -c "
131
+ corepack enable &&
130
132
  npm run output:worker:install &&
131
133
  echo 'Installed dependencies' &&
132
134
  npx nodemon --watch src --watch package.json --ext ts,js,json,prompt --ignore 'dist/**' --ignore '**/*.test.ts' --ignore '**/*.spec.ts' --exec 'npm run output:worker:install && npm run output:worker:build && npm run output:worker:start'
@@ -2,7 +2,7 @@ import { Command, Flags } from '@oclif/core';
2
2
  import fs from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import logUpdate from 'log-update';
5
- import { validateDockerEnvironment, startDockerCompose, startDockerComposeDetached, stopDockerCompose, getServiceStatus, DockerComposeConfigNotFoundError, getDefaultDockerComposePath, SERVICE_HEALTH, SERVICE_STATE } from '#services/docker.js';
5
+ import { validateDockerEnvironment, startDockerCompose, startDockerComposeDetached, stopDockerCompose, getServiceStatus, isServiceFailed, DockerComposeConfigNotFoundError, getDefaultDockerComposePath, SERVICE_HEALTH, SERVICE_STATE } from '#services/docker.js';
6
6
  import { getErrorMessage } from '#utils/error_utils.js';
7
7
  import { getDevSuccessMessage } from '#services/messages.js';
8
8
  import { ensureClaudePlugin } from '#services/coding_agents.js';
@@ -43,7 +43,7 @@ const formatService = (service) => {
43
43
  return ` ${color}${icon}${ANSI.RESET} ${name} ${ANSI.DIM}${statusPadded}${ANSI.RESET} ${ANSI.DIM}${ports}${ANSI.RESET}`;
44
44
  };
45
45
  const getFailedServicesWarning = (services) => {
46
- const failedServices = services.filter(s => s.state === SERVICE_STATE.EXITED);
46
+ const failedServices = services.filter(isServiceFailed);
47
47
  if (failedServices.length === 0) {
48
48
  return [];
49
49
  }
@@ -15,6 +15,7 @@ vi.mock('#services/docker.js', () => ({
15
15
  { name: 'redis', state: 'running', health: 'healthy', ports: ['6379:6379'] },
16
16
  { name: 'temporal', state: 'running', health: 'healthy', ports: ['7233:7233'] }
17
17
  ]),
18
+ isServiceFailed: vi.fn((s) => s.state === 'exited' || s.health === 'unhealthy'),
18
19
  DockerComposeConfigNotFoundError: Error,
19
20
  DockerValidationError: Error,
20
21
  getDefaultDockerComposePath: vi.fn(() => '/path/to/docker-compose-dev.yml'),
@@ -1,6 +1,7 @@
1
1
  import { Args, Command, Flags } from '@oclif/core';
2
2
  import { UserCancelledError } from '#types/errors.js';
3
3
  import { runInit } from '#services/project_scaffold.js';
4
+ import { getErrorMessage } from '#utils/error_utils.js';
4
5
  export default class Init extends Command {
5
6
  static description = 'Initialize a new Output project by scaffolding the complete project structure';
6
7
  static examples = [
@@ -30,8 +31,7 @@ export default class Init extends Command {
30
31
  return;
31
32
  }
32
33
  // runInit handles cleanup internally and throws Error with message
33
- const errorMessage = error instanceof Error ? error.message : String(error);
34
- this.error(errorMessage);
34
+ this.error(getErrorMessage(error));
35
35
  }
36
36
  }
37
37
  }
@@ -5,6 +5,8 @@ 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
7
  import path from 'node:path';
8
+ import * as fsSync from 'node:fs';
9
+ import { getErrorMessage } from '#utils/error_utils.js';
8
10
  export default class Generate extends Command {
9
11
  static description = 'Generate a new Output workflow from a skeleton or plan file';
10
12
  static examples = [
@@ -52,31 +54,33 @@ export default class Generate extends Command {
52
54
  if (!flags.skeleton && !planFile) {
53
55
  this.error('Full workflow generation not implemented yet. Please use --skeleton flag or --plan-file');
54
56
  }
55
- try {
56
- const result = await generateWorkflow({
57
- name: args.name,
58
- description: flags.description,
59
- outputDir: flags['output-dir'],
60
- skeleton: flags.skeleton,
61
- force: flags.force
62
- });
63
- if (planFile) {
64
- this.log('\nStarting AI-assisted workflow implementation...\n');
65
- const projectRoot = process.cwd();
66
- await ensureOutputAISystem(projectRoot);
67
- const absolutePlanPath = path.resolve(projectRoot, planFile);
68
- const buildOutput = await buildWorkflow(absolutePlanPath, result.targetDir, args.name);
69
- await buildWorkflowInteractiveLoop(buildOutput);
70
- this.log(ux.colorize('green', '\nWorkflow implementation complete!\n'));
71
- }
72
- this.displaySuccess(result);
57
+ const projectRoot = process.cwd();
58
+ const absolutePlanPath = planFile ? path.resolve(projectRoot, planFile) : '';
59
+ if (planFile && !fsSync.existsSync(absolutePlanPath)) {
60
+ this.error(`Plan file not found: ${absolutePlanPath}`);
73
61
  }
74
- catch (error) {
75
- if (error instanceof Error) {
76
- this.error(error.message);
77
- }
78
- throw error;
62
+ const result = await generateWorkflow({
63
+ name: args.name,
64
+ description: flags.description,
65
+ outputDir: flags['output-dir'],
66
+ skeleton: flags.skeleton,
67
+ force: flags.force
68
+ }).catch((error) => {
69
+ this.error(getErrorMessage(error));
70
+ });
71
+ if (planFile) {
72
+ this.log('\nStarting AI-assisted workflow implementation...\n');
73
+ await ensureOutputAISystem(projectRoot);
74
+ const buildOutput = await buildWorkflow(absolutePlanPath, result.targetDir, args.name)
75
+ .catch((error) => {
76
+ fsSync.rmSync(result.targetDir, { recursive: true, force: true });
77
+ const message = getErrorMessage(error);
78
+ this.error(`Workflow implementation failed, created files have been rolled back: ${message}`);
79
+ });
80
+ await buildWorkflowInteractiveLoop(buildOutput);
81
+ this.log(ux.colorize('green', '\nWorkflow implementation complete!\n'));
79
82
  }
83
+ this.displaySuccess(result);
80
84
  }
81
85
  displaySuccess(result) {
82
86
  const message = getWorkflowGenerateSuccessMessage(result.workflowName, result.targetDir, result.filesCreated);
@@ -52,7 +52,7 @@ export default class WorkflowPlan extends Command {
52
52
  if (modifications === acceptKey) {
53
53
  return originalPlanContent;
54
54
  }
55
- const modifiedPlanContent = await replyToClaude(modifications, PLAN_COMMAND_OPTIONS);
55
+ const modifiedPlanContent = await replyToClaude(modifications, { anthropicOpts: PLAN_COMMAND_OPTIONS });
56
56
  return this.planModificationLoop(modifiedPlanContent);
57
57
  }
58
58
  async planGenerationLoop(promptDescription, planName, projectRoot) {
@@ -62,5 +62,7 @@ export default class WorkflowPlan extends Command {
62
62
  const modifiedPlanContent = await this.planModificationLoop(planContent);
63
63
  const modifiedSavedPath = await writePlanFile(planName, modifiedPlanContent, projectRoot);
64
64
  this.log(`✅ Plan saved to: ${modifiedSavedPath}\n`);
65
+ const generateCmd = ux.colorize('cyan', `npx output workflow generate <WORKFLOW_NAME> --plan-file=${modifiedSavedPath}`);
66
+ this.log(`⏭️ To execute this plan run: ${generateCmd}\n`);
65
67
  }
66
68
  }
@@ -1,3 +1,3 @@
1
1
  {
2
- "framework": "0.1.10"
2
+ "framework": "0.1.11-next.7b8340c.0"
3
3
  }
@@ -2,13 +2,21 @@
2
2
  * Claude Agent SDK client for workflow planning
3
3
  */
4
4
  import { Options } from '@anthropic-ai/claude-agent-sdk';
5
+ export declare const ADDITIONAL_INSTRUCTIONS: {
6
+ readonly PLAN: "\n! IMPORTANT !\n1. Use TodoWrite to track your progress through plan creation.\n\n2. Please respond with only the final version of the plan content.\n\n3. Respond in a markdown format with these metadata headers:\n\n---\ntitle: <plan-title>\ndescription: <plan-description>\ndate: <plan-date>\n---\n\n<plan-content>\n\n4. After you mark all todos as complete, you must respond with the final version of the plan.\n\n5. DO NOT write the plan to disk — the CLI will handle saving the file to the plans directory.\n\n6. DO NOT suggest any next steps, follow-up commands, or instructions for the user — the CLI will inform the user of next steps after saving.\n";
7
+ readonly BUILD: "\n! IMPORTANT !\n1. Use TodoWrite to track your progress through workflow implementation.\n\n2. Follow the implementation plan exactly as specified in the plan file.\n\n3. Implement all workflow files following Output.ai patterns and best practices.\n\n4. After you mark all todos as complete, provide a summary of what was implemented.\n";
8
+ };
5
9
  export declare const PLAN_COMMAND_OPTIONS: Options;
10
+ interface ReplyToClaudeOptions {
11
+ anthropicOpts?: Options;
12
+ applyAdditionalInstructions?: string;
13
+ }
6
14
  export declare const BUILD_COMMAND_OPTIONS: Options;
7
15
  export declare class ClaudeInvocationError extends Error {
8
16
  cause?: Error | undefined;
9
17
  constructor(message: string, cause?: Error | undefined);
10
18
  }
11
- export declare function replyToClaude(message: string, options?: Options): Promise<string>;
19
+ export declare function replyToClaude(message: string, { anthropicOpts, applyAdditionalInstructions }?: ReplyToClaudeOptions): Promise<string>;
12
20
  /**
13
21
  * Invoke claude-code with /outputai:plan_workflow slash command
14
22
  * The SDK loads custom commands from .claude/commands/ when settingSources includes 'project'.
@@ -28,3 +36,4 @@ export declare function invokePlanWorkflow(description: string): Promise<string>
28
36
  * @returns Implementation output from claude-code
29
37
  */
30
38
  export declare function invokeBuildWorkflow(planFilePath: string, workflowDir: string, workflowName: string, additionalInstructions?: string): Promise<string>;
39
+ export {};
@@ -6,13 +6,14 @@ import { ux } from '@oclif/core';
6
6
  import * as cliProgress from 'cli-progress';
7
7
  import { getErrorMessage, toError } from '#utils/error_utils.js';
8
8
  import { config } from '#config.js';
9
- const ADDITIONAL_INSTRUCTIONS = `
9
+ export const ADDITIONAL_INSTRUCTIONS = {
10
+ PLAN: `
10
11
  ! IMPORTANT !
11
12
  1. Use TodoWrite to track your progress through plan creation.
12
13
 
13
- 2. Please response with only the final version of the plan.
14
+ 2. Please respond with only the final version of the plan content.
14
15
 
15
- 3. Response in a markdown format with these metadata headers:
16
+ 3. Respond in a markdown format with these metadata headers:
16
17
 
17
18
  ---
18
19
  title: <plan-title>
@@ -23,8 +24,12 @@ date: <plan-date>
23
24
  <plan-content>
24
25
 
25
26
  4. After you mark all todos as complete, you must respond with the final version of the plan.
26
- `;
27
- const ADDITIONAL_INSTRUCTIONS_BUILD = `
27
+
28
+ 5. DO NOT write the plan to disk — the CLI will handle saving the file to the plans directory.
29
+
30
+ 6. DO NOT suggest any next steps, follow-up commands, or instructions for the user — the CLI will inform the user of next steps after saving.
31
+ `,
32
+ BUILD: `
28
33
  ! IMPORTANT !
29
34
  1. Use TodoWrite to track your progress through workflow implementation.
30
35
 
@@ -33,7 +38,8 @@ const ADDITIONAL_INSTRUCTIONS_BUILD = `
33
38
  3. Implement all workflow files following Output.ai patterns and best practices.
34
39
 
35
40
  4. After you mark all todos as complete, provide a summary of what was implemented.
36
- `;
41
+ `
42
+ };
37
43
  const PLAN_COMMAND = 'outputai:plan_workflow';
38
44
  const BUILD_COMMAND = 'outputai:build_workflow';
39
45
  const GLOBAL_CLAUDE_OPTIONS = {
@@ -102,11 +108,8 @@ function getTodoWriteMessage(message) {
102
108
  const todoWriteMessage = message.message.content.find((c) => c?.type === 'tool_use' && c.name === 'TodoWrite');
103
109
  return todoWriteMessage ?? null;
104
110
  }
105
- function applyInstructions(initialMessage, instructionsType = 'plan') {
106
- const instructions = instructionsType === 'build' ?
107
- ADDITIONAL_INSTRUCTIONS_BUILD :
108
- ADDITIONAL_INSTRUCTIONS;
109
- return `${initialMessage}\n\n${instructions}`;
111
+ function applyInstructions(message, instructions) {
112
+ return `${message}\n\n${instructions}`;
110
113
  }
111
114
  function createProgressBar() {
112
115
  return new cliProgress.SingleBar({
@@ -183,8 +186,8 @@ async function singleQuery(prompt, options = {}) {
183
186
  throw new ClaudeInvocationError(`Failed to invoke claude-code: ${getErrorMessage(error)}`, toError(error));
184
187
  }
185
188
  }
186
- export async function replyToClaude(message, options = {}) {
187
- return singleQuery(applyInstructions(message), { continue: true, ...options });
189
+ export async function replyToClaude(message, { anthropicOpts, applyAdditionalInstructions = ADDITIONAL_INSTRUCTIONS.PLAN } = {}) {
190
+ return singleQuery(applyInstructions(message, applyAdditionalInstructions), { continue: true, ...anthropicOpts });
188
191
  }
189
192
  /**
190
193
  * Invoke claude-code with /outputai:plan_workflow slash command
@@ -194,7 +197,7 @@ export async function replyToClaude(message, options = {}) {
194
197
  * @returns Plan output from claude-code
195
198
  */
196
199
  export async function invokePlanWorkflow(description) {
197
- return singleQuery(applyInstructions(`/${PLAN_COMMAND} ${description}`), PLAN_COMMAND_OPTIONS);
200
+ return singleQuery(applyInstructions(`/${PLAN_COMMAND} ${description}`, ADDITIONAL_INSTRUCTIONS.PLAN), PLAN_COMMAND_OPTIONS);
198
201
  }
199
202
  /**
200
203
  * Invoke claude-code with /outputai:build_workflow slash command
@@ -211,5 +214,5 @@ export async function invokeBuildWorkflow(planFilePath, workflowDir, workflowNam
211
214
  const fullCommand = additionalInstructions ?
212
215
  `/${BUILD_COMMAND} ${commandArgs} ${additionalInstructions}` :
213
216
  `/${BUILD_COMMAND} ${commandArgs}`;
214
- return singleQuery(applyInstructions(fullCommand, 'build'), BUILD_COMMAND_OPTIONS);
217
+ return singleQuery(applyInstructions(fullCommand, ADDITIONAL_INSTRUCTIONS.BUILD), BUILD_COMMAND_OPTIONS);
215
218
  }
@@ -27,6 +27,8 @@ export declare function validateDockerEnvironment(): void;
27
27
  export declare function getDefaultDockerComposePath(): string;
28
28
  export declare function parseServiceStatus(jsonOutput: string): ServiceStatus[];
29
29
  export declare function getServiceStatus(dockerComposePath: string): Promise<ServiceStatus[]>;
30
+ export declare function isServiceHealthy(service: ServiceStatus): boolean;
31
+ export declare function isServiceFailed(service: ServiceStatus): boolean;
30
32
  export declare function waitForServicesHealthy(dockerComposePath: string, timeoutMs?: number, pollIntervalMs?: number): Promise<void>;
31
33
  export interface DockerComposeProcess {
32
34
  process: ChildProcess;
@@ -103,11 +103,18 @@ const formatServiceStatus = (services) => services.map(s => {
103
103
  const status = s.health === SERVICE_HEALTH.NONE ? s.state : s.health;
104
104
  return ` ${color}${icon}${ANSI_RESET} ${s.name}: ${status}`;
105
105
  }).join('\n');
106
+ export function isServiceHealthy(service) {
107
+ return service.state !== SERVICE_STATE.EXITED &&
108
+ (service.health === SERVICE_HEALTH.HEALTHY || service.health === SERVICE_HEALTH.NONE);
109
+ }
110
+ export function isServiceFailed(service) {
111
+ return service.state === SERVICE_STATE.EXITED || service.health === SERVICE_HEALTH.UNHEALTHY;
112
+ }
106
113
  export async function waitForServicesHealthy(dockerComposePath, timeoutMs = 120000, pollIntervalMs = 2000) {
107
114
  const startTime = Date.now();
108
115
  while (Date.now() - startTime < timeoutMs) {
109
116
  const services = await getServiceStatus(dockerComposePath);
110
- const allHealthy = services.every(s => s.health === SERVICE_HEALTH.HEALTHY || s.health === SERVICE_HEALTH.NONE);
117
+ const allHealthy = services.every(isServiceHealthy);
111
118
  if (services.length > 0) {
112
119
  const statusLines = formatServiceStatus(services);
113
120
  logUpdate(`⏳ Waiting for services to become healthy...\n${statusLines}`);
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
2
  import { execFileSync } from 'node:child_process';
3
- import { parseServiceStatus, getServiceStatus, waitForServicesHealthy } from './docker.js';
3
+ import { parseServiceStatus, getServiceStatus, waitForServicesHealthy, isServiceHealthy, isServiceFailed } from './docker.js';
4
4
  vi.mock('node:child_process', () => ({
5
5
  execSync: vi.fn(),
6
6
  execFileSync: vi.fn(),
@@ -89,6 +89,46 @@ describe('docker service', () => {
89
89
  await expect(getServiceStatus('/path/to/docker-compose.yml')).rejects.toThrow();
90
90
  });
91
91
  });
92
+ describe('isServiceHealthy', () => {
93
+ it('should return true for a running service with health: healthy', () => {
94
+ expect(isServiceHealthy({ name: 'redis', state: 'running', health: 'healthy', ports: [] })).toBe(true);
95
+ });
96
+ it('should return true for a running service with no health check (health: none)', () => {
97
+ expect(isServiceHealthy({ name: 'api', state: 'running', health: 'none', ports: [] })).toBe(true);
98
+ });
99
+ it('should return false for a running service with health: unhealthy', () => {
100
+ expect(isServiceHealthy({ name: 'worker', state: 'running', health: 'unhealthy', ports: [] })).toBe(false);
101
+ });
102
+ it('should return false for an exited service with health: none', () => {
103
+ expect(isServiceHealthy({ name: 'worker', state: 'exited', health: 'none', ports: [] })).toBe(false);
104
+ });
105
+ it('should return false for an exited service with health: unhealthy', () => {
106
+ expect(isServiceHealthy({ name: 'worker', state: 'exited', health: 'unhealthy', ports: [] })).toBe(false);
107
+ });
108
+ it('should return false for a service with health: starting', () => {
109
+ expect(isServiceHealthy({ name: 'temporal', state: 'running', health: 'starting', ports: [] })).toBe(false);
110
+ });
111
+ });
112
+ describe('isServiceFailed', () => {
113
+ it('should return true for an exited service with health: none', () => {
114
+ expect(isServiceFailed({ name: 'worker', state: 'exited', health: 'none', ports: [] })).toBe(true);
115
+ });
116
+ it('should return true for a running service with health: unhealthy', () => {
117
+ expect(isServiceFailed({ name: 'worker', state: 'running', health: 'unhealthy', ports: [] })).toBe(true);
118
+ });
119
+ it('should return true for an exited service with health: unhealthy', () => {
120
+ expect(isServiceFailed({ name: 'worker', state: 'exited', health: 'unhealthy', ports: [] })).toBe(true);
121
+ });
122
+ it('should return false for a running service with health: healthy', () => {
123
+ expect(isServiceFailed({ name: 'redis', state: 'running', health: 'healthy', ports: [] })).toBe(false);
124
+ });
125
+ it('should return false for a running service with health: none', () => {
126
+ expect(isServiceFailed({ name: 'api', state: 'running', health: 'none', ports: [] })).toBe(false);
127
+ });
128
+ it('should return false for a service with health: starting — not a failure, just in progress', () => {
129
+ expect(isServiceFailed({ name: 'temporal', state: 'running', health: 'starting', ports: [] })).toBe(false);
130
+ });
131
+ });
92
132
  describe('waitForServicesHealthy', () => {
93
133
  it('should resolve when all services are healthy', async () => {
94
134
  const mockOutput = `{"Service":"redis","State":"running","Health":"healthy","Publishers":[]}
@@ -108,6 +148,24 @@ describe('docker service', () => {
108
148
  const promise = waitForServicesHealthy('/path/to/docker-compose.yml', 100);
109
149
  await expect(promise).rejects.toThrow('Timeout waiting for services to become healthy');
110
150
  }, 10000);
151
+ it('should not resolve when a service has exited with no health check — regression OUT-334', async () => {
152
+ // Exited containers have empty Health which parses to 'none'.
153
+ // Previously, state:exited + health:none was incorrectly treated as healthy.
154
+ const mockOutput = `{"Service":"redis","State":"running","Health":"healthy","Publishers":[]}
155
+ {"Service":"worker","State":"exited","Health":"","Publishers":[]}`;
156
+ vi.mocked(execFileSync).mockReturnValue(mockOutput);
157
+ const promise = waitForServicesHealthy('/path/to/docker-compose.yml', 100, 50);
158
+ await expect(promise).rejects.toThrow('Timeout waiting for services to become healthy');
159
+ }, 10000);
160
+ it('should not resolve when a service is running but unhealthy — regression OUT-334', async () => {
161
+ // Nodemon keeps the container running even when the exec'd command fails,
162
+ // so the unhealthy case is state:running + health:unhealthy.
163
+ const mockOutput = `{"Service":"redis","State":"running","Health":"healthy","Publishers":[]}
164
+ {"Service":"worker","State":"running","Health":"unhealthy","Publishers":[]}`;
165
+ vi.mocked(execFileSync).mockReturnValue(mockOutput);
166
+ const promise = waitForServicesHealthy('/path/to/docker-compose.yml', 100, 50);
167
+ await expect(promise).rejects.toThrow('Timeout waiting for services to become healthy');
168
+ }, 10000);
111
169
  it('should poll multiple times until healthy', async () => {
112
170
  const callTracker = { count: 0 };
113
171
  vi.mocked(execFileSync).mockImplementation(() => {
@@ -139,8 +139,7 @@ function formatInitError(error, projectPath) {
139
139
  case 'EPERM': return `Operation not permitted${pathSuffix}`;
140
140
  case 'ENOENT': return `Required file or directory not found${pathSuffix}`;
141
141
  default: {
142
- const originalMessage = error instanceof Error ? error.message : String(error);
143
- return `Failed to create project${pathSuffix}: ${originalMessage}`;
142
+ return `Failed to create project${pathSuffix}: ${getErrorMessage(error)}`;
144
143
  }
145
144
  }
146
145
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Workflow builder service for implementing workflows from plan files
3
3
  */
4
- import { BUILD_COMMAND_OPTIONS, invokeBuildWorkflow as invokeBuildWorkflowFromClient, replyToClaude } from './claude_client.js';
4
+ import { ADDITIONAL_INSTRUCTIONS, BUILD_COMMAND_OPTIONS, invokeBuildWorkflow as invokeBuildWorkflowFromClient, replyToClaude } from './claude_client.js';
5
5
  import { input } from '@inquirer/prompts';
6
6
  import { ux } from '@oclif/core';
7
7
  import fs from 'node:fs/promises';
@@ -56,12 +56,15 @@ async function processModification(modification, currentOutput) {
56
56
  return currentOutput;
57
57
  }
58
58
  try {
59
- const updatedOutput = await replyToClaude(modification, BUILD_COMMAND_OPTIONS);
59
+ const updatedOutput = await replyToClaude(modification, {
60
+ anthropicOpts: BUILD_COMMAND_OPTIONS,
61
+ applyAdditionalInstructions: ADDITIONAL_INSTRUCTIONS.BUILD
62
+ });
60
63
  displayImplementationOutput(updatedOutput, '✓ Implementation updated!');
61
64
  return updatedOutput;
62
65
  }
63
66
  catch (error) {
64
- ux.error(`Failed to apply modifications: ${getErrorMessage(error)}`);
67
+ ux.stdout(ux.colorize('red', `Failed to apply modifications: ${getErrorMessage(error)}`));
65
68
  ux.stdout('Continuing with previous version...\n');
66
69
  return currentOutput;
67
70
  }
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, beforeEach, vi } from 'vitest';
2
2
  import { buildWorkflow, buildWorkflowInteractiveLoop } from './workflow_builder.js';
3
- import { BUILD_COMMAND_OPTIONS, invokeBuildWorkflow, replyToClaude } from './claude_client.js';
3
+ import { ADDITIONAL_INSTRUCTIONS, BUILD_COMMAND_OPTIONS, invokeBuildWorkflow, replyToClaude } from './claude_client.js';
4
4
  import { input } from '@inquirer/prompts';
5
5
  import { ux } from '@oclif/core';
6
6
  import fs from 'node:fs/promises';
@@ -97,7 +97,10 @@ describe('workflow-builder service', () => {
97
97
  .mockResolvedValueOnce('ACCEPT');
98
98
  vi.mocked(replyToClaude).mockResolvedValue('Updated implementation with error handling');
99
99
  const result = await buildWorkflowInteractiveLoop('Initial implementation');
100
- expect(replyToClaude).toHaveBeenCalledWith('Add error handling', BUILD_COMMAND_OPTIONS);
100
+ expect(replyToClaude).toHaveBeenCalledWith('Add error handling', {
101
+ anthropicOpts: BUILD_COMMAND_OPTIONS,
102
+ applyAdditionalInstructions: ADDITIONAL_INSTRUCTIONS.BUILD
103
+ });
101
104
  expect(result).toBe('Updated implementation with error handling');
102
105
  expect(input).toHaveBeenCalledTimes(2);
103
106
  });
@@ -111,8 +114,14 @@ describe('workflow-builder service', () => {
111
114
  .mockResolvedValueOnce('Implementation with logging and validation');
112
115
  const result = await buildWorkflowInteractiveLoop('Initial implementation');
113
116
  expect(replyToClaude).toHaveBeenCalledTimes(2);
114
- expect(replyToClaude).toHaveBeenNthCalledWith(1, 'Add logging', BUILD_COMMAND_OPTIONS);
115
- expect(replyToClaude).toHaveBeenNthCalledWith(2, 'Add validation', BUILD_COMMAND_OPTIONS);
117
+ expect(replyToClaude).toHaveBeenNthCalledWith(1, 'Add logging', {
118
+ anthropicOpts: BUILD_COMMAND_OPTIONS,
119
+ applyAdditionalInstructions: ADDITIONAL_INSTRUCTIONS.BUILD
120
+ });
121
+ expect(replyToClaude).toHaveBeenNthCalledWith(2, 'Add validation', {
122
+ anthropicOpts: BUILD_COMMAND_OPTIONS,
123
+ applyAdditionalInstructions: ADDITIONAL_INSTRUCTIONS.BUILD
124
+ });
116
125
  expect(result).toBe('Implementation with logging and validation');
117
126
  });
118
127
  it('should prompt again when user provides empty input', async () => {
@@ -146,7 +155,7 @@ describe('workflow-builder service', () => {
146
155
  const result = await buildWorkflowInteractiveLoop('Original implementation');
147
156
  // Should return original implementation after error
148
157
  expect(result).toBe('Original implementation');
149
- expect(ux.error).toHaveBeenCalledWith(expect.stringContaining('Failed to apply modifications'));
158
+ expect(ux.stdout).toHaveBeenCalledWith(expect.stringContaining('Failed to apply modifications'));
150
159
  expect(ux.stdout).toHaveBeenCalledWith(expect.stringContaining('Continuing with previous version'));
151
160
  });
152
161
  it('should continue looping after handling error', async () => {
@@ -15,9 +15,9 @@ function validateConfig(config) {
15
15
  * Check if target directory exists and handle accordingly
16
16
  */
17
17
  import * as fsSync from 'node:fs';
18
- async function checkTargetDirectory(targetDir, force) {
18
+ async function checkTargetDirectory(name, targetDir, force) {
19
19
  if (fsSync.existsSync(targetDir) && !force) {
20
- throw new WorkflowExistsError('', targetDir);
20
+ throw new WorkflowExistsError(name, targetDir);
21
21
  }
22
22
  }
23
23
  /**
@@ -27,7 +27,7 @@ export async function generateWorkflow(config) {
27
27
  validateConfig(config);
28
28
  const targetDir = createTargetDir(config.outputDir, config.name);
29
29
  const templatesDir = getTemplateDir('workflow');
30
- await checkTargetDirectory(targetDir, config.force);
30
+ await checkTargetDirectory(config.name, targetDir, config.force);
31
31
  await fs.mkdir(targetDir, { recursive: true });
32
32
  const variables = prepareTemplateVariables(config.name, config.description || '');
33
33
  const templateFiles = await getTemplateFiles(templatesDir);
@@ -3,7 +3,6 @@
3
3
  */
4
4
  export type TodoStatus = 'pending' | 'in_progress' | 'completed';
5
5
  export type FileMappingType = 'template' | 'symlink' | 'copy';
6
- export type InstructionsType = 'plan' | 'build';
7
6
  export interface Todo {
8
7
  content: string;
9
8
  activeForm: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@outputai/cli",
3
- "version": "0.1.10",
3
+ "version": "0.1.11-next.7b8340c.0",
4
4
  "description": "CLI for Output.ai workflow generation",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -33,9 +33,9 @@
33
33
  "log-update": "7.2.0",
34
34
  "semver": "7.7.4",
35
35
  "yaml": "^2.8.3",
36
- "@outputai/credentials": "0.1.10",
37
- "@outputai/llm": "0.1.10",
38
- "@outputai/evals": "0.1.10"
36
+ "@outputai/evals": "0.1.11-next.7b8340c.0",
37
+ "@outputai/credentials": "0.1.11-next.7b8340c.0",
38
+ "@outputai/llm": "0.1.11-next.7b8340c.0"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/cli-progress": "3.11.6",