@output.ai/cli 0.7.3 → 0.7.5

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.
@@ -9,21 +9,27 @@ const ANSI = {
9
9
  RESET: '\x1b[0m',
10
10
  DIM: '\x1b[2m',
11
11
  BOLD: '\x1b[1m',
12
- CYAN: '\x1b[36m'
12
+ CYAN: '\x1b[36m',
13
+ RED: '\x1b[31m',
14
+ YELLOW: '\x1b[33m',
15
+ BG_RED: '\x1b[41m',
16
+ WHITE: '\x1b[37m'
13
17
  };
14
18
  const STATUS_ICONS = {
15
19
  [SERVICE_HEALTH.HEALTHY]: '●',
16
20
  [SERVICE_HEALTH.UNHEALTHY]: '○',
17
21
  [SERVICE_HEALTH.STARTING]: '◐',
18
22
  [SERVICE_HEALTH.NONE]: '●',
19
- [SERVICE_STATE.RUNNING]: '●'
23
+ [SERVICE_STATE.RUNNING]: '●',
24
+ [SERVICE_STATE.EXITED]: '✗'
20
25
  };
21
26
  const STATUS_COLORS = {
22
27
  [SERVICE_HEALTH.HEALTHY]: '\x1b[32m',
23
28
  [SERVICE_HEALTH.UNHEALTHY]: '\x1b[31m',
24
29
  [SERVICE_HEALTH.STARTING]: '\x1b[33m',
25
30
  [SERVICE_HEALTH.NONE]: '\x1b[34m',
26
- [SERVICE_STATE.RUNNING]: '\x1b[34m'
31
+ [SERVICE_STATE.RUNNING]: '\x1b[34m',
32
+ [SERVICE_STATE.EXITED]: '\x1b[31m'
27
33
  };
28
34
  const formatService = (service) => {
29
35
  const healthKey = service.health === SERVICE_HEALTH.NONE ? service.state : service.health;
@@ -35,6 +41,27 @@ const formatService = (service) => {
35
41
  const statusPadded = status.padEnd(10);
36
42
  return ` ${color}${icon}${ANSI.RESET} ${name} ${ANSI.DIM}${statusPadded}${ANSI.RESET} ${ANSI.DIM}${ports}${ANSI.RESET}`;
37
43
  };
44
+ const getFailedServicesWarning = (services) => {
45
+ const failedServices = services.filter(s => s.state === SERVICE_STATE.EXITED);
46
+ if (failedServices.length === 0) {
47
+ return [];
48
+ }
49
+ const failedNames = failedServices.map(s => s.name);
50
+ const hasWorkerFailed = failedNames.some(name => name.toLowerCase().includes('worker'));
51
+ const warningLines = [
52
+ '',
53
+ `${ANSI.BG_RED}${ANSI.WHITE}${ANSI.BOLD} ⚠️ SERVICE FAILURE DETECTED ${ANSI.RESET}`,
54
+ '',
55
+ `${ANSI.RED}${ANSI.BOLD}Failed services:${ANSI.RESET} ${failedNames.join(', ')}`
56
+ ];
57
+ if (hasWorkerFailed) {
58
+ warningLines.push('', `${ANSI.YELLOW}${ANSI.BOLD}⚡ The worker is not running!${ANSI.RESET}`, `${ANSI.YELLOW} Workflows will fail until the worker is restarted.${ANSI.RESET}`, '', `${ANSI.DIM}Check the logs with: docker compose logs worker${ANSI.RESET}`);
59
+ }
60
+ else {
61
+ warningLines.push('', `${ANSI.DIM}Check the logs with: docker compose logs <service-name>${ANSI.RESET}`);
62
+ }
63
+ return warningLines;
64
+ };
38
65
  const poll = async (fn, intervalMs) => {
39
66
  for (;;) {
40
67
  await fn();
@@ -113,10 +140,12 @@ export default class Dev extends Command {
113
140
  const outputServiceStatus = async () => {
114
141
  try {
115
142
  const services = await getServiceStatus(dockerComposePath);
143
+ const failureWarning = getFailedServicesWarning(services);
116
144
  const lines = [
117
145
  `${ANSI.BOLD}📊 Service Status${ANSI.RESET}`,
118
146
  '',
119
147
  ...services.map(formatService),
148
+ ...failureWarning,
120
149
  '',
121
150
  `${ANSI.CYAN}🌐 Temporal UI:${ANSI.RESET} ${ANSI.BOLD}http://localhost:8080${ANSI.RESET}`,
122
151
  '',
@@ -76,7 +76,7 @@ export function parseServiceStatus(jsonOutput) {
76
76
  });
77
77
  }
78
78
  export async function getServiceStatus(dockerComposePath) {
79
- const result = execFileSync('docker', ['compose', '-f', dockerComposePath, 'ps', '--format', 'json'], { encoding: 'utf-8', cwd: process.cwd() });
79
+ const result = execFileSync('docker', ['compose', '-f', dockerComposePath, 'ps', '--all', '--format', 'json'], { encoding: 'utf-8', cwd: process.cwd() });
80
80
  return parseServiceStatus(result);
81
81
  }
82
82
  const STATUS_ICONS = {
@@ -73,7 +73,7 @@ describe('docker service', () => {
73
73
  const mockOutput = '{"Service":"redis","State":"running","Health":"healthy","Publishers":[]}';
74
74
  vi.mocked(execFileSync).mockReturnValue(mockOutput);
75
75
  await getServiceStatus('/path/to/docker-compose.yml');
76
- expect(execFileSync).toHaveBeenCalledWith('docker', ['compose', '-f', '/path/to/docker-compose.yml', 'ps', '--format', 'json'], expect.objectContaining({ encoding: 'utf-8' }));
76
+ expect(execFileSync).toHaveBeenCalledWith('docker', ['compose', '-f', '/path/to/docker-compose.yml', 'ps', '--all', '--format', 'json'], expect.objectContaining({ encoding: 'utf-8' }));
77
77
  });
78
78
  it('should return parsed service status', async () => {
79
79
  const mockOutput = '{"Service":"redis","State":"running","Health":"healthy","Publishers":[{"PublishedPort":6379,"TargetPort":6379}]}';
@@ -8,7 +8,7 @@ export async function generatePlanName(description, date = new Date()) {
8
8
  const month = String(date.getMonth() + 1).padStart(2, '0');
9
9
  const day = String(date.getDate()).padStart(2, '0');
10
10
  const datePrefix = `${year}_${month}_${day}`;
11
- const planNameSlug = await generateText({
11
+ const { text: planNameSlug } = await generateText({
12
12
  prompt: 'generate_plan_name@v1',
13
13
  variables: { description }
14
14
  });
@@ -6,13 +6,19 @@ import fs from 'node:fs/promises';
6
6
  vi.mock('./coding_agents.js');
7
7
  vi.mock('@output.ai/llm');
8
8
  vi.mock('node:fs/promises');
9
+ const mockGenerateTextResult = (text) => ({
10
+ text,
11
+ result: text,
12
+ usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
13
+ finishReason: 'stop'
14
+ });
9
15
  describe('workflow-planner service', () => {
10
16
  beforeEach(() => {
11
17
  vi.clearAllMocks();
12
18
  });
13
19
  describe('generatePlanName', () => {
14
20
  it('should generate plan name with date prefix using LLM', async () => {
15
- vi.mocked(generateText).mockResolvedValue('customer_order_processing');
21
+ vi.mocked(generateText).mockResolvedValue(mockGenerateTextResult('customer_order_processing'));
16
22
  const testDate = new Date(2025, 9, 6);
17
23
  const planName = await generatePlanName('A workflow that processes customer orders', testDate);
18
24
  expect(planName).toMatch(/^2025_10_06_/);
@@ -23,7 +29,7 @@ describe('workflow-planner service', () => {
23
29
  });
24
30
  });
25
31
  it('should clean and validate LLM response', async () => {
26
- vi.mocked(generateText).mockResolvedValue(' User-Auth & Security!@# ');
32
+ vi.mocked(generateText).mockResolvedValue(mockGenerateTextResult(' User-Auth & Security!@# '));
27
33
  const testDate = new Date(2025, 9, 6);
28
34
  const planName = await generatePlanName('User authentication workflow', testDate);
29
35
  expect(planName).toBe('2025_10_06_user_auth_security');
@@ -34,14 +40,14 @@ describe('workflow-planner service', () => {
34
40
  await expect(generatePlanName('Test workflow')).rejects.toThrow('API rate limit exceeded');
35
41
  });
36
42
  it('should limit plan name length to 50 characters', async () => {
37
- vi.mocked(generateText).mockResolvedValue('this_is_an_extremely_long_plan_name_that_exceeds_the_maximum_allowed_length_for_file_names');
43
+ vi.mocked(generateText).mockResolvedValue(mockGenerateTextResult('this_is_an_extremely_long_plan_name_that_exceeds_the_maximum_allowed_length_for_file_names'));
38
44
  const testDate = new Date(2025, 9, 6);
39
45
  const planName = await generatePlanName('Long workflow description', testDate);
40
46
  const namePart = planName.replace(/^2025_10_06_/, '');
41
47
  expect(namePart.length).toBeLessThanOrEqual(50);
42
48
  });
43
49
  it('should handle multiple underscores correctly', async () => {
44
- vi.mocked(generateText).mockResolvedValue('user___auth___workflow');
50
+ vi.mocked(generateText).mockResolvedValue(mockGenerateTextResult('user___auth___workflow'));
45
51
  const testDate = new Date(2025, 9, 6);
46
52
  const planName = await generatePlanName('Test', testDate);
47
53
  expect(planName).toBe('2025_10_06_user_auth_workflow');
@@ -7,10 +7,10 @@ export const answerQuestion = step( {
7
7
  inputSchema: z.string(),
8
8
  outputSchema: z.string(),
9
9
  fn: async question => {
10
- const response = await generateText( {
10
+ const { result } = await generateText( {
11
11
  prompt: 'answer_question@v1',
12
12
  variables: { question }
13
13
  } );
14
- return response;
14
+ return result;
15
15
  }
16
16
  } );
@@ -10,11 +10,11 @@ export const exampleLLMStep = step( {
10
10
  } ),
11
11
  outputSchema: z.string(),
12
12
  fn: async ( { userInput } ) => {
13
- const response = await generateText( {
13
+ const { result } = await generateText( {
14
14
  prompt: 'prompt@v1',
15
15
  variables: { userInput }
16
16
  } );
17
- return response;
17
+ return result;
18
18
  }
19
19
  } );
20
20
 
@@ -16,7 +16,12 @@ export const mockClaudeAgentSDK = {
16
16
  * Mock for @output.ai/llm
17
17
  */
18
18
  export const mockLLM = {
19
- generateText: vi.fn().mockResolvedValue('workflow_plan_name')
19
+ generateText: vi.fn().mockResolvedValue({
20
+ text: 'workflow_plan_name',
21
+ sources: [],
22
+ usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
23
+ finishReason: 'stop'
24
+ })
20
25
  };
21
26
  /**
22
27
  * Mock for child_process spawn (for agents init)
@@ -61,7 +66,12 @@ export function resetAllMocks() {
61
66
  mockFS.writeFile.mockResolvedValue(undefined);
62
67
  mockFS.readFile.mockResolvedValue('template content');
63
68
  mockFS.stat.mockRejectedValue({ code: 'ENOENT' });
64
- mockLLM.generateText.mockResolvedValue('workflow_plan_name');
69
+ mockLLM.generateText.mockResolvedValue({
70
+ text: 'workflow_plan_name',
71
+ sources: [],
72
+ usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
73
+ finishReason: 'stop'
74
+ });
65
75
  mockInquirer.input.mockResolvedValue('Mock workflow description');
66
76
  mockInquirer.confirm.mockResolvedValue(true);
67
77
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@output.ai/cli",
3
- "version": "0.7.3",
3
+ "version": "0.7.5",
4
4
  "description": "CLI for Output.ai workflow generation",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",