@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 declare function startDockerCompose(dockerComposePath: string, enableWatch?: boolean): Promise<DockerComposeProcess>;
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 };
@@ -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}]}';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@output.ai/cli",
3
- "version": "0.7.4",
3
+ "version": "0.7.6",
4
4
  "description": "CLI for Output.ai workflow generation",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",