@output.ai/cli 0.7.4 → 0.7.6
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.
|
@@ -6,6 +6,7 @@ export default class Dev extends Command {
|
|
|
6
6
|
static flags: {
|
|
7
7
|
'compose-file': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
8
8
|
'no-watch': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
9
|
+
'image-pull-policy': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
9
10
|
};
|
|
10
11
|
private dockerProcess;
|
|
11
12
|
run(): Promise<void>;
|
|
@@ -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();
|
|
@@ -46,7 +73,8 @@ export default class Dev extends Command {
|
|
|
46
73
|
static examples = [
|
|
47
74
|
'<%= config.bin %> <%= command.id %>',
|
|
48
75
|
'<%= config.bin %> <%= command.id %> --no-watch',
|
|
49
|
-
'<%= config.bin %> <%= command.id %> --compose-file ./custom-docker-compose.yml'
|
|
76
|
+
'<%= config.bin %> <%= command.id %> --compose-file ./custom-docker-compose.yml',
|
|
77
|
+
'<%= config.bin %> <%= command.id %> --image-pull-policy missing'
|
|
50
78
|
];
|
|
51
79
|
static args = {};
|
|
52
80
|
static flags = {
|
|
@@ -58,6 +86,11 @@ export default class Dev extends Command {
|
|
|
58
86
|
'no-watch': Flags.boolean({
|
|
59
87
|
description: 'Disable automatic container restart on file changes',
|
|
60
88
|
default: false
|
|
89
|
+
}),
|
|
90
|
+
'image-pull-policy': Flags.string({
|
|
91
|
+
description: 'Image pull policy for docker compose (always, missing, never)',
|
|
92
|
+
options: ['always', 'missing', 'never'],
|
|
93
|
+
default: 'always'
|
|
61
94
|
})
|
|
62
95
|
};
|
|
63
96
|
dockerProcess = null;
|
|
@@ -93,8 +126,9 @@ export default class Dev extends Command {
|
|
|
93
126
|
};
|
|
94
127
|
process.on('SIGINT', cleanup);
|
|
95
128
|
process.on('SIGTERM', cleanup);
|
|
129
|
+
const pullPolicy = flags['image-pull-policy'];
|
|
96
130
|
try {
|
|
97
|
-
const { process: dockerProc, waitForHealthy } = await startDockerCompose(dockerComposePath, !flags['no-watch']);
|
|
131
|
+
const { process: dockerProc, waitForHealthy } = await startDockerCompose(dockerComposePath, !flags['no-watch'], pullPolicy);
|
|
98
132
|
this.dockerProcess = dockerProc;
|
|
99
133
|
dockerProc.on('error', error => {
|
|
100
134
|
this.error(`Docker process error: ${getErrorMessage(error)}`, { exit: 1 });
|
|
@@ -113,10 +147,12 @@ export default class Dev extends Command {
|
|
|
113
147
|
const outputServiceStatus = async () => {
|
|
114
148
|
try {
|
|
115
149
|
const services = await getServiceStatus(dockerComposePath);
|
|
150
|
+
const failureWarning = getFailedServicesWarning(services);
|
|
116
151
|
const lines = [
|
|
117
152
|
`${ANSI.BOLD}📊 Service Status${ANSI.RESET}`,
|
|
118
153
|
'',
|
|
119
154
|
...services.map(formatService),
|
|
155
|
+
...failureWarning,
|
|
120
156
|
'',
|
|
121
157
|
`${ANSI.CYAN}🌐 Temporal UI:${ANSI.RESET} ${ANSI.BOLD}http://localhost:8080${ANSI.RESET}`,
|
|
122
158
|
'',
|
|
@@ -78,6 +78,11 @@ describe('dev command', () => {
|
|
|
78
78
|
expect(Dev.flags['compose-file'].required).toBe(false);
|
|
79
79
|
expect(Dev.flags['compose-file'].char).toBe('f');
|
|
80
80
|
});
|
|
81
|
+
it('should have image-pull-policy flag defined', () => {
|
|
82
|
+
expect(Dev.flags).toBeDefined();
|
|
83
|
+
expect(Dev.flags['image-pull-policy']).toBeDefined();
|
|
84
|
+
expect(Dev.flags['image-pull-policy'].description).toContain('pull policy');
|
|
85
|
+
});
|
|
81
86
|
});
|
|
82
87
|
describe('command instantiation', () => {
|
|
83
88
|
it('should be instantiable', () => {
|
|
@@ -130,14 +135,15 @@ describe('dev command', () => {
|
|
|
130
135
|
cmd.error = vi.fn();
|
|
131
136
|
// Mock parse to return flags
|
|
132
137
|
Object.defineProperty(cmd, 'parse', {
|
|
133
|
-
value: vi.fn().mockResolvedValue({ flags: { 'no-watch': false, 'compose-file': undefined }, args: {} }),
|
|
138
|
+
value: vi.fn().mockResolvedValue({ flags: { 'no-watch': false, 'compose-file': undefined, 'image-pull-policy': 'always' }, args: {} }),
|
|
134
139
|
configurable: true
|
|
135
140
|
});
|
|
136
141
|
// Run the command but don't await it since it waits forever after startup
|
|
137
142
|
const runPromise = cmd.run();
|
|
138
143
|
// Wait a tick for startDockerCompose to be called
|
|
139
144
|
await new Promise(resolve => setImmediate(resolve));
|
|
140
|
-
expect(dockerService.startDockerCompose).toHaveBeenCalledWith('/path/to/docker-compose-dev.yml', true // enableWatch should be true
|
|
145
|
+
expect(dockerService.startDockerCompose).toHaveBeenCalledWith('/path/to/docker-compose-dev.yml', true, // enableWatch should be true
|
|
146
|
+
'always' // default pull policy
|
|
141
147
|
);
|
|
142
148
|
expect(cmd.log).toHaveBeenCalledWith(expect.stringContaining('File watching enabled'));
|
|
143
149
|
// Cancel the promise (it will be rejected but we don't care)
|
|
@@ -149,14 +155,15 @@ describe('dev command', () => {
|
|
|
149
155
|
cmd.error = vi.fn();
|
|
150
156
|
// Mock parse to return flags
|
|
151
157
|
Object.defineProperty(cmd, 'parse', {
|
|
152
|
-
value: vi.fn().mockResolvedValue({ flags: { 'no-watch': true, 'compose-file': undefined }, args: {} }),
|
|
158
|
+
value: vi.fn().mockResolvedValue({ flags: { 'no-watch': true, 'compose-file': undefined, 'image-pull-policy': 'always' }, args: {} }),
|
|
153
159
|
configurable: true
|
|
154
160
|
});
|
|
155
161
|
// Run the command but don't await it since it waits forever after startup
|
|
156
162
|
const runPromise = cmd.run();
|
|
157
163
|
// Wait a tick for startDockerCompose to be called
|
|
158
164
|
await new Promise(resolve => setImmediate(resolve));
|
|
159
|
-
expect(dockerService.startDockerCompose).toHaveBeenCalledWith('/path/to/docker-compose-dev.yml', false // enableWatch should be false
|
|
165
|
+
expect(dockerService.startDockerCompose).toHaveBeenCalledWith('/path/to/docker-compose-dev.yml', false, // enableWatch should be false
|
|
166
|
+
'always' // default pull policy
|
|
160
167
|
);
|
|
161
168
|
expect(cmd.log).toHaveBeenCalledWith(expect.stringContaining('File watching disabled'));
|
|
162
169
|
// Cancel the promise (it will be rejected but we don't care)
|
|
@@ -176,11 +183,47 @@ describe('dev command', () => {
|
|
|
176
183
|
cmd.error = vi.fn();
|
|
177
184
|
// Mock parse to return flags
|
|
178
185
|
Object.defineProperty(cmd, 'parse', {
|
|
179
|
-
value: vi.fn().mockResolvedValue({ flags: { 'no-watch': false, 'compose-file': undefined }, args: {} }),
|
|
186
|
+
value: vi.fn().mockResolvedValue({ flags: { 'no-watch': false, 'compose-file': undefined, 'image-pull-policy': 'always' }, args: {} }),
|
|
180
187
|
configurable: true
|
|
181
188
|
});
|
|
182
189
|
await cmd.run();
|
|
183
190
|
expect(cmd.error).toHaveBeenCalledWith('Docker error', { exit: 1 });
|
|
184
191
|
});
|
|
185
192
|
});
|
|
193
|
+
describe('image pull policy', () => {
|
|
194
|
+
it('should pass pull policy to startDockerCompose', async () => {
|
|
195
|
+
const cmd = new Dev([], {});
|
|
196
|
+
cmd.log = vi.fn();
|
|
197
|
+
cmd.error = vi.fn();
|
|
198
|
+
// Mock parse to return flags with missing pull policy
|
|
199
|
+
Object.defineProperty(cmd, 'parse', {
|
|
200
|
+
value: vi.fn().mockResolvedValue({ flags: { 'no-watch': false, 'compose-file': undefined, 'image-pull-policy': 'missing' }, args: {} }),
|
|
201
|
+
configurable: true
|
|
202
|
+
});
|
|
203
|
+
// Run the command but don't await it since it waits forever after startup
|
|
204
|
+
const runPromise = cmd.run();
|
|
205
|
+
// Wait a tick for startDockerCompose to be called
|
|
206
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
207
|
+
expect(dockerService.startDockerCompose).toHaveBeenCalledWith('/path/to/docker-compose-dev.yml', true, 'missing');
|
|
208
|
+
// Cancel the promise (it will be rejected but we don't care)
|
|
209
|
+
runPromise.catch(() => { });
|
|
210
|
+
});
|
|
211
|
+
it('should use never pull policy when specified', async () => {
|
|
212
|
+
const cmd = new Dev([], {});
|
|
213
|
+
cmd.log = vi.fn();
|
|
214
|
+
cmd.error = vi.fn();
|
|
215
|
+
// Mock parse to return flags with never pull policy
|
|
216
|
+
Object.defineProperty(cmd, 'parse', {
|
|
217
|
+
value: vi.fn().mockResolvedValue({ flags: { 'no-watch': false, 'compose-file': undefined, 'image-pull-policy': 'never' }, args: {} }),
|
|
218
|
+
configurable: true
|
|
219
|
+
});
|
|
220
|
+
// Run the command but don't await it since it waits forever after startup
|
|
221
|
+
const runPromise = cmd.run();
|
|
222
|
+
// Wait a tick for startDockerCompose to be called
|
|
223
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
224
|
+
expect(dockerService.startDockerCompose).toHaveBeenCalledWith('/path/to/docker-compose-dev.yml', true, 'never');
|
|
225
|
+
// Cancel the promise (it will be rejected but we don't care)
|
|
226
|
+
runPromise.catch(() => { });
|
|
227
|
+
});
|
|
228
|
+
});
|
|
186
229
|
});
|
|
@@ -32,6 +32,7 @@ export interface DockerComposeProcess {
|
|
|
32
32
|
process: ChildProcess;
|
|
33
33
|
waitForHealthy: () => Promise<void>;
|
|
34
34
|
}
|
|
35
|
-
export
|
|
35
|
+
export type PullPolicy = 'always' | 'missing' | 'never';
|
|
36
|
+
export declare function startDockerCompose(dockerComposePath: string, enableWatch?: boolean, pullPolicy?: PullPolicy): Promise<DockerComposeProcess>;
|
|
36
37
|
export declare function stopDockerCompose(dockerComposePath: string): Promise<void>;
|
|
37
38
|
export { isDockerInstalled, isDockerComposeAvailable, isDockerDaemonRunning, DockerValidationError };
|
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 = {
|
|
@@ -121,13 +121,16 @@ export async function waitForServicesHealthy(dockerComposePath, timeoutMs = 1200
|
|
|
121
121
|
logUpdate.done();
|
|
122
122
|
throw new Error('Timeout waiting for services to become healthy');
|
|
123
123
|
}
|
|
124
|
-
export async function startDockerCompose(dockerComposePath, enableWatch = false) {
|
|
124
|
+
export async function startDockerCompose(dockerComposePath, enableWatch = false, pullPolicy) {
|
|
125
125
|
const args = [
|
|
126
126
|
'compose',
|
|
127
127
|
'-f', dockerComposePath,
|
|
128
128
|
'--project-directory', process.cwd(),
|
|
129
129
|
'up'
|
|
130
130
|
];
|
|
131
|
+
if (pullPolicy) {
|
|
132
|
+
args.push('--pull', pullPolicy);
|
|
133
|
+
}
|
|
131
134
|
if (enableWatch) {
|
|
132
135
|
args.push('--watch');
|
|
133
136
|
}
|
|
@@ -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}]}';
|