@outputai/cli 0.1.10 → 0.1.11-next.49171f5.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/dist/assets/docker/docker-compose-dev.yml +7 -5
- package/dist/commands/dev/index.js +2 -2
- package/dist/commands/dev/index.spec.js +1 -0
- package/dist/generated/framework_version.json +1 -1
- package/dist/services/docker.d.ts +2 -0
- package/dist/services/docker.js +8 -1
- package/dist/services/docker.spec.js +59 -1
- package/package.json +4 -4
|
@@ -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.
|
|
84
|
+
image: outputai/api:${OUTPUT_API_VERSION:-0.1.11-next.49171f5.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
|
|
108
|
+
test: [ 'CMD-SHELL', 'npx output-healthcheck' ]
|
|
109
109
|
interval: 3s
|
|
110
|
-
timeout:
|
|
111
|
-
retries:
|
|
112
|
-
start_period:
|
|
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(
|
|
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'),
|
|
@@ -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;
|
package/dist/services/docker.js
CHANGED
|
@@ -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(
|
|
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(() => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@outputai/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11-next.49171f5.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.
|
|
37
|
-
"@outputai/llm": "0.1.
|
|
38
|
-
"@outputai/evals": "0.1.
|
|
36
|
+
"@outputai/credentials": "0.1.11-next.49171f5.0",
|
|
37
|
+
"@outputai/llm": "0.1.11-next.49171f5.0",
|
|
38
|
+
"@outputai/evals": "0.1.11-next.49171f5.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@types/cli-progress": "3.11.6",
|