@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.
- package/dist/commands/dev/index.js +32 -3
- package/dist/services/docker.js +1 -1
- package/dist/services/docker.spec.js +1 -1
- package/dist/services/workflow_planner.js +1 -1
- package/dist/services/workflow_planner.spec.js +10 -4
- package/dist/templates/project/src/workflows/example_question/steps.ts.template +2 -2
- package/dist/templates/workflow/steps.ts.template +2 -2
- package/dist/test_helpers/mocks.js +12 -2
- package/package.json +1 -1
|
@@ -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
|
'',
|
package/dist/services/docker.js
CHANGED
|
@@ -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
|
|
10
|
+
const { result } = await generateText( {
|
|
11
11
|
prompt: 'answer_question@v1',
|
|
12
12
|
variables: { question }
|
|
13
13
|
} );
|
|
14
|
-
return
|
|
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
|
|
13
|
+
const { result } = await generateText( {
|
|
14
14
|
prompt: 'prompt@v1',
|
|
15
15
|
variables: { userInput }
|
|
16
16
|
} );
|
|
17
|
-
return
|
|
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(
|
|
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(
|
|
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
|
}
|