@output.ai/cli 0.0.9 → 0.2.4

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.
Files changed (65) hide show
  1. package/README.md +48 -0
  2. package/bin/copyassets.sh +8 -0
  3. package/bin/run.js +4 -0
  4. package/dist/api/generated/api.d.ts +35 -13
  5. package/dist/api/http_client.d.ts +8 -2
  6. package/dist/api/http_client.js +14 -6
  7. package/dist/api/orval_post_process.d.ts +2 -1
  8. package/dist/api/orval_post_process.js +18 -5
  9. package/dist/assets/docker/docker-compose-dev.yml +130 -0
  10. package/dist/commands/agents/init.js +4 -2
  11. package/dist/commands/dev/eject.d.ts +11 -0
  12. package/dist/commands/dev/eject.js +58 -0
  13. package/dist/commands/dev/eject.spec.d.ts +1 -0
  14. package/dist/commands/dev/eject.spec.js +109 -0
  15. package/dist/commands/dev/index.d.ts +11 -0
  16. package/dist/commands/dev/index.js +54 -0
  17. package/dist/commands/dev/index.spec.d.ts +1 -0
  18. package/dist/commands/dev/index.spec.js +150 -0
  19. package/dist/commands/init.d.ts +10 -0
  20. package/dist/commands/init.js +30 -0
  21. package/dist/commands/init.spec.d.ts +1 -0
  22. package/dist/commands/init.spec.js +109 -0
  23. package/dist/commands/workflow/run.js +5 -0
  24. package/dist/services/claude_client.js +15 -2
  25. package/dist/services/claude_client.spec.js +5 -1
  26. package/dist/services/coding_agents.js +13 -4
  27. package/dist/services/coding_agents.spec.js +2 -1
  28. package/dist/services/docker.d.ts +12 -0
  29. package/dist/services/docker.js +79 -0
  30. package/dist/services/env_configurator.d.ts +11 -0
  31. package/dist/services/env_configurator.js +158 -0
  32. package/dist/services/env_configurator.spec.d.ts +1 -0
  33. package/dist/services/env_configurator.spec.js +123 -0
  34. package/dist/services/messages.d.ts +5 -0
  35. package/dist/services/messages.js +233 -0
  36. package/dist/services/project_scaffold.d.ts +6 -0
  37. package/dist/services/project_scaffold.js +140 -0
  38. package/dist/services/project_scaffold.spec.d.ts +1 -0
  39. package/dist/services/project_scaffold.spec.js +43 -0
  40. package/dist/services/template_processor.d.ts +1 -1
  41. package/dist/services/template_processor.js +26 -11
  42. package/dist/services/workflow_builder.js +2 -1
  43. package/dist/templates/project/.env.template +9 -0
  44. package/dist/templates/project/.gitignore.template +33 -0
  45. package/dist/templates/project/README.md.template +60 -0
  46. package/dist/templates/project/package.json.template +26 -0
  47. package/dist/templates/project/src/simple/prompts/answer_question@v1.prompt.template +13 -0
  48. package/dist/templates/project/src/simple/steps.ts.template +16 -0
  49. package/dist/templates/project/src/simple/workflow.ts.template +22 -0
  50. package/dist/templates/project/tsconfig.json.template +20 -0
  51. package/dist/types/errors.d.ts +32 -0
  52. package/dist/types/errors.js +48 -0
  53. package/dist/utils/env_loader.d.ts +6 -0
  54. package/dist/utils/env_loader.js +43 -0
  55. package/dist/utils/error_utils.d.ts +24 -0
  56. package/dist/utils/error_utils.js +87 -0
  57. package/dist/utils/file_system.d.ts +3 -0
  58. package/dist/utils/file_system.js +33 -0
  59. package/dist/utils/process.d.ts +4 -0
  60. package/dist/utils/process.js +48 -0
  61. package/dist/utils/sdk_versions.d.ts +7 -0
  62. package/dist/utils/sdk_versions.js +28 -0
  63. package/dist/utils/sdk_versions.spec.d.ts +1 -0
  64. package/dist/utils/sdk_versions.spec.js +19 -0
  65. package/package.json +4 -2
package/README.md CHANGED
@@ -14,6 +14,54 @@ npx @output.ai/cli
14
14
 
15
15
  ## Usage
16
16
 
17
+ ### Initialize a New Project
18
+
19
+ ```bash
20
+ # Create a new Output SDK project
21
+ output init my-project
22
+ ```
23
+
24
+ #### What It Does
25
+
26
+ Creates a complete Output SDK project structure with:
27
+ - Example workflows demonstrating SDK features
28
+ - TypeScript project configuration
29
+ - Agent configuration files
30
+
31
+ ### Start Development Services
32
+
33
+ ```bash
34
+ # Start with automatic file watching (default)
35
+ output dev
36
+
37
+ # Start without file watching
38
+ output dev --no-watch
39
+ ```
40
+
41
+ #### What It Does
42
+
43
+ The `dev` command orchestrates your entire development environment:
44
+ 1. Starts Docker services (Temporal, Redis, PostgreSQL)
45
+ 2. Launches the API server for workflow execution
46
+ 3. Starts the worker to process workflows
47
+ 4. Opens Temporal UI for workflow monitoring
48
+ 5. Automatically restarts the worker when source files change
49
+ 6. Provides a unified log view of all services
50
+
51
+ This is the primary command for local development - it handles all the complexity of running multiple services and keeps them synchronized.
52
+
53
+ #### Automatic File Watching
54
+
55
+ By default, the worker container automatically restarts when you modify files in:
56
+ - `src/` - Your workflow source files and implementations
57
+ - `package.json` - When dependencies change
58
+
59
+ This feature uses Docker Compose's native watch functionality (requires Docker Compose v2.22+).
60
+
61
+ #### Command Options
62
+
63
+ - `--no-watch` - Disable automatic container restart on file changes
64
+
17
65
  ### List Workflows
18
66
 
19
67
  ```bash
@@ -0,0 +1,8 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ copyfiles -u 1 --all './src/**/*.template' dist
5
+ copyfiles -u 1 './src/**/*.prompt' dist
6
+ copyfiles -u 1 './src/assets/**/*' dist
7
+
8
+ echo "✅ Assets copied to dist/"
package/bin/run.js CHANGED
@@ -1,5 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { execute } from '@oclif/core';
4
+ import { loadEnvironment } from '../dist/utils/env_loader.js';
5
+
6
+ // Load environment variables from .env files before executing CLI
7
+ loadEnvironment();
4
8
 
5
9
  await execute( { dir: import.meta.url } );
@@ -1,3 +1,11 @@
1
+ /**
2
+ * Generated by orval v7.13.2 🍺
3
+ * Do not edit manually.
4
+ * Output.ai SDK API
5
+ * API for managing and executing Temporal workflows through Output SDK
6
+ * OpenAPI spec version: 1.0.0
7
+ */
8
+ import { type ApiRequestOptions } from '../http_client.js';
1
9
  export type JSONSchemaProperties = {
2
10
  [key: string]: JSONSchema;
3
11
  };
@@ -29,20 +37,32 @@ export type PostWorkflowRunBody = {
29
37
  workflowName: string;
30
38
  /** The payload to send to the workflow */
31
39
  input: unknown;
40
+ /** (Optional) The workflowId to use. Must be unique */
41
+ workflowId?: string;
32
42
  /** The name of the task queue to send the workflow to */
33
43
  taskQueue?: string;
34
44
  };
45
+ /**
46
+ * An object with information about the trace generated by the execution
47
+ */
48
+ export type PostWorkflowRun200Trace = {
49
+ [key: string]: unknown;
50
+ };
35
51
  export type PostWorkflowRun200 = {
36
52
  /** The workflow execution id */
37
53
  workflowId?: string;
38
54
  /** The output of the workflow */
39
55
  output?: unknown;
56
+ /** An object with information about the trace generated by the execution */
57
+ trace?: PostWorkflowRun200Trace;
40
58
  };
41
59
  export type PostWorkflowStartBody = {
42
60
  /** The name of the workflow to execute */
43
61
  workflowName: string;
44
62
  /** The payload to send to the workflow */
45
63
  input: unknown;
64
+ /** (Optional) The workflowId to use. Must be unique */
65
+ workflowId?: string;
46
66
  /** The name of the task queue to send the workflow to */
47
67
  taskQueue?: string;
48
68
  };
@@ -75,16 +95,18 @@ export type GetWorkflowIdStatus200 = {
75
95
  completedAt?: number;
76
96
  };
77
97
  /**
78
- * The output of workflow
98
+ * An object with information about the trace generated by the execution
79
99
  */
80
- export type GetWorkflowIdOutput200Output = {
100
+ export type GetWorkflowIdOutput200Trace = {
81
101
  [key: string]: unknown;
82
102
  };
83
103
  export type GetWorkflowIdOutput200 = {
84
104
  /** The workflow execution id */
85
105
  workflowId?: string;
86
106
  /** The output of workflow */
87
- output?: GetWorkflowIdOutput200Output;
107
+ output?: unknown;
108
+ /** An object with information about the trace generated by the execution */
109
+ trace?: GetWorkflowIdOutput200Trace;
88
110
  };
89
111
  export type GetWorkflowCatalogId200 = {
90
112
  /** Each workflow available in this catalog */
@@ -110,7 +132,7 @@ export type getHealthResponseSuccess = (getHealthResponse200) & {
110
132
  };
111
133
  export type getHealthResponse = (getHealthResponseSuccess);
112
134
  export declare const getGetHealthUrl: () => string;
113
- export declare const getHealth: (options?: RequestInit) => Promise<getHealthResponse>;
135
+ export declare const getHealth: (options?: ApiRequestOptions) => Promise<getHealthResponse>;
114
136
  /**
115
137
  * Executes a workflow and waits for it to complete before returning the result
116
138
  * @summary Execute a workflow synchronously
@@ -124,7 +146,7 @@ export type postWorkflowRunResponseSuccess = (postWorkflowRunResponse200) & {
124
146
  };
125
147
  export type postWorkflowRunResponse = (postWorkflowRunResponseSuccess);
126
148
  export declare const getPostWorkflowRunUrl: () => string;
127
- export declare const postWorkflowRun: (postWorkflowRunBody: PostWorkflowRunBody, options?: RequestInit) => Promise<postWorkflowRunResponse>;
149
+ export declare const postWorkflowRun: (postWorkflowRunBody: PostWorkflowRunBody, options?: ApiRequestOptions) => Promise<postWorkflowRunResponse>;
128
150
  /**
129
151
  * @summary Start a workflow asynchronously
130
152
  */
@@ -137,7 +159,7 @@ export type postWorkflowStartResponseSuccess = (postWorkflowStartResponse200) &
137
159
  };
138
160
  export type postWorkflowStartResponse = (postWorkflowStartResponseSuccess);
139
161
  export declare const getPostWorkflowStartUrl: () => string;
140
- export declare const postWorkflowStart: (postWorkflowStartBody: PostWorkflowStartBody, options?: RequestInit) => Promise<postWorkflowStartResponse>;
162
+ export declare const postWorkflowStart: (postWorkflowStartBody: PostWorkflowStartBody, options?: ApiRequestOptions) => Promise<postWorkflowStartResponse>;
141
163
  /**
142
164
  * @summary Get workflow execution status
143
165
  */
@@ -157,7 +179,7 @@ export type getWorkflowIdStatusResponseError = (getWorkflowIdStatusResponse404)
157
179
  };
158
180
  export type getWorkflowIdStatusResponse = (getWorkflowIdStatusResponseSuccess | getWorkflowIdStatusResponseError);
159
181
  export declare const getGetWorkflowIdStatusUrl: (id: string) => string;
160
- export declare const getWorkflowIdStatus: (id: string, options?: RequestInit) => Promise<getWorkflowIdStatusResponse>;
182
+ export declare const getWorkflowIdStatus: (id: string, options?: ApiRequestOptions) => Promise<getWorkflowIdStatusResponse>;
161
183
  /**
162
184
  * @summary Stop a workflow execution
163
185
  */
@@ -177,7 +199,7 @@ export type patchWorkflowIdStopResponseError = (patchWorkflowIdStopResponse404)
177
199
  };
178
200
  export type patchWorkflowIdStopResponse = (patchWorkflowIdStopResponseSuccess | patchWorkflowIdStopResponseError);
179
201
  export declare const getPatchWorkflowIdStopUrl: (id: string) => string;
180
- export declare const patchWorkflowIdStop: (id: string, options?: RequestInit) => Promise<patchWorkflowIdStopResponse>;
202
+ export declare const patchWorkflowIdStop: (id: string, options?: ApiRequestOptions) => Promise<patchWorkflowIdStopResponse>;
181
203
  /**
182
204
  * @summary Return the output of a workflow
183
205
  */
@@ -197,7 +219,7 @@ export type getWorkflowIdOutputResponseError = (getWorkflowIdOutputResponse404)
197
219
  };
198
220
  export type getWorkflowIdOutputResponse = (getWorkflowIdOutputResponseSuccess | getWorkflowIdOutputResponseError);
199
221
  export declare const getGetWorkflowIdOutputUrl: (id: string) => string;
200
- export declare const getWorkflowIdOutput: (id: string, options?: RequestInit) => Promise<getWorkflowIdOutputResponse>;
222
+ export declare const getWorkflowIdOutput: (id: string, options?: ApiRequestOptions) => Promise<getWorkflowIdOutputResponse>;
201
223
  /**
202
224
  * @summary Get a specific workflow catalog by ID
203
225
  */
@@ -210,7 +232,7 @@ export type getWorkflowCatalogIdResponseSuccess = (getWorkflowCatalogIdResponse2
210
232
  };
211
233
  export type getWorkflowCatalogIdResponse = (getWorkflowCatalogIdResponseSuccess);
212
234
  export declare const getGetWorkflowCatalogIdUrl: (id: string) => string;
213
- export declare const getWorkflowCatalogId: (id: string, options?: RequestInit) => Promise<getWorkflowCatalogIdResponse>;
235
+ export declare const getWorkflowCatalogId: (id: string, options?: ApiRequestOptions) => Promise<getWorkflowCatalogIdResponse>;
214
236
  /**
215
237
  * @summary Get the default workflow catalog
216
238
  */
@@ -223,7 +245,7 @@ export type getWorkflowCatalogResponseSuccess = (getWorkflowCatalogResponse200)
223
245
  };
224
246
  export type getWorkflowCatalogResponse = (getWorkflowCatalogResponseSuccess);
225
247
  export declare const getGetWorkflowCatalogUrl: () => string;
226
- export declare const getWorkflowCatalog: (options?: RequestInit) => Promise<getWorkflowCatalogResponse>;
248
+ export declare const getWorkflowCatalog: (options?: ApiRequestOptions) => Promise<getWorkflowCatalogResponse>;
227
249
  /**
228
250
  * @summary Send feedback to a payload
229
251
  */
@@ -236,7 +258,7 @@ export type postWorkflowIdFeedbackResponseSuccess = (postWorkflowIdFeedbackRespo
236
258
  };
237
259
  export type postWorkflowIdFeedbackResponse = (postWorkflowIdFeedbackResponseSuccess);
238
260
  export declare const getPostWorkflowIdFeedbackUrl: (id: string) => string;
239
- export declare const postWorkflowIdFeedback: (id: string, postWorkflowIdFeedbackBody: PostWorkflowIdFeedbackBody, options?: RequestInit) => Promise<postWorkflowIdFeedbackResponse>;
261
+ export declare const postWorkflowIdFeedback: (id: string, postWorkflowIdFeedbackBody: PostWorkflowIdFeedbackBody, options?: ApiRequestOptions) => Promise<postWorkflowIdFeedbackResponse>;
240
262
  /**
241
263
  * @summary A dummy post endpoint for test only
242
264
  */
@@ -249,4 +271,4 @@ export type postHeartbeatResponseSuccess = (postHeartbeatResponse204) & {
249
271
  };
250
272
  export type postHeartbeatResponse = (postHeartbeatResponseSuccess);
251
273
  export declare const getPostHeartbeatUrl: () => string;
252
- export declare const postHeartbeat: (options?: RequestInit) => Promise<postHeartbeatResponse>;
274
+ export declare const postHeartbeat: (options?: ApiRequestOptions) => Promise<postHeartbeatResponse>;
@@ -1,6 +1,12 @@
1
1
  /**
2
2
  * Custom ky-based HTTP client for Orval-generated API
3
3
  */
4
- export declare const customFetchInstance: <T>(url: string, options: RequestInit & {
4
+ import type { Options as KyOptions } from 'ky';
5
+ /**
6
+ * Custom API request options that extend RequestInit with additional config
7
+ */
8
+ export type ApiRequestOptions = RequestInit & {
5
9
  params?: Record<string, unknown>;
6
- }) => Promise<T>;
10
+ config?: KyOptions;
11
+ };
12
+ export declare const customFetchInstance: <T>(url: string, options: ApiRequestOptions) => Promise<T>;
@@ -23,12 +23,20 @@ const api = ky.create({
23
23
  }
24
24
  });
25
25
  const stripLeadingSlash = (url) => url.startsWith('/') ? url.slice(1) : url;
26
- const buildKyOptions = (options) => ({
27
- method: options.method,
28
- headers: options.headers,
29
- searchParams: options.params,
30
- body: options.body && options.method !== 'GET' ? options.body : undefined
31
- });
26
+ const buildKyOptions = (options) => {
27
+ // Extract params, config, and body for special handling
28
+ const { params, config: customConfig, body, ...restOptions } = options;
29
+ return {
30
+ // Pass through standard RequestInit options
31
+ ...restOptions,
32
+ // Convert params to searchParams for ky (if not already in config)
33
+ searchParams: customConfig?.searchParams || params,
34
+ // Only include body for non-GET requests
35
+ ...(body && options.method !== 'GET' ? { body } : {}),
36
+ // Spread any ky-specific config options
37
+ ...customConfig
38
+ };
39
+ };
32
40
  const wrapResponse = (response, data) => ({
33
41
  data,
34
42
  status: response.status,
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Orval post-generation hook to fix ES module imports by adding .js extensions.
2
+ * Orval post-generation hook to fix ES module imports by adding .js extensions
3
+ * and update RequestInit types to use our custom ApiRequestOptions.
3
4
  * This is necessary because the SDK uses "type": "module" in package.json,
4
5
  * which requires all relative imports to have explicit .js extensions.
5
6
  */
@@ -1,15 +1,28 @@
1
1
  import { readFileSync, writeFileSync } from 'fs';
2
2
  import { execSync } from 'child_process';
3
3
  /**
4
- * Orval post-generation hook to fix ES module imports by adding .js extensions.
4
+ * Orval post-generation hook to fix ES module imports by adding .js extensions
5
+ * and update RequestInit types to use our custom ApiRequestOptions.
5
6
  * This is necessary because the SDK uses "type": "module" in package.json,
6
7
  * which requires all relative imports to have explicit .js extensions.
7
8
  */
8
9
  export async function fixEsmImports(outputPath) {
9
- const content = readFileSync(outputPath, 'utf8');
10
- const fixedContent = content.replace(/from '\.\.\/http_client'/g, 'from \'../http_client.js\'');
11
- writeFileSync(outputPath, fixedContent, 'utf8');
12
- console.log('✅ Fixed ESM imports in Orval generated file');
10
+ const originalContent = readFileSync(outputPath, 'utf8');
11
+ // Apply all transformations in sequence
12
+ const transformedContent = originalContent
13
+ // Fix ESM imports
14
+ .replace(/from '\.\.\/http_client'/g, 'from \'../http_client.js\'')
15
+ // Import ApiRequestOptions type from http_client
16
+ .replace(/import { customFetchInstance } from '\.\.\/http_client\.js';/, match => {
17
+ if (!originalContent.includes('ApiRequestOptions')) {
18
+ return 'import { customFetchInstance, type ApiRequestOptions } from \'../http_client.js\';';
19
+ }
20
+ return match;
21
+ })
22
+ // Replace RequestInit with ApiRequestOptions in function signatures
23
+ .replace(/options\?: RequestInit/g, 'options?: ApiRequestOptions');
24
+ writeFileSync(outputPath, transformedContent, 'utf8');
25
+ console.log('✅ Fixed ESM imports and updated types in Orval generated file');
13
26
  }
14
27
  /**
15
28
  * Run ESLint fix on generated files to ensure they follow project standards
@@ -0,0 +1,130 @@
1
+ name: ${DOCKER_SERVICE_NAME:-output-sdk}
2
+ services:
3
+ redis:
4
+ image: redis:8-alpine
5
+ networks:
6
+ - main
7
+ ports:
8
+ - '6379:6379'
9
+ volumes:
10
+ - redis:/data
11
+ healthcheck:
12
+ test: ['CMD', 'redis-cli', 'ping']
13
+ interval: 2s
14
+ timeout: 2s
15
+ retries: 5
16
+ start_period: 3s
17
+
18
+ postgresql:
19
+ environment:
20
+ POSTGRES_PASSWORD: temporal
21
+ POSTGRES_USER: temporal
22
+ image: postgres:17.5
23
+ networks:
24
+ - main
25
+ expose:
26
+ - 5432
27
+ volumes:
28
+ - postgres:/var/lib/postgresql/data
29
+ healthcheck:
30
+ test: ['CMD-SHELL', 'pg_isready -U temporal -d temporal']
31
+ interval: 2s
32
+ timeout: 2s
33
+ retries: 5
34
+ start_period: 3s
35
+
36
+ temporal:
37
+ depends_on:
38
+ postgresql:
39
+ condition: service_healthy
40
+ redis:
41
+ condition: service_healthy
42
+ environment:
43
+ - DB=postgres12
44
+ - DB_PORT=5432
45
+ - POSTGRES_USER=temporal
46
+ - POSTGRES_PWD=temporal
47
+ - POSTGRES_SEEDS=postgresql
48
+ image: temporalio/auto-setup:latest
49
+ networks:
50
+ - main
51
+ ports:
52
+ - '7233:7233'
53
+ healthcheck:
54
+ test:
55
+ [
56
+ 'CMD',
57
+ 'sh',
58
+ '-c',
59
+ 'tctl --address temporal:7233 cluster health 2>&1 | grep -q "temporal.api.workflowservice.v1.WorkflowService: SERVING"'
60
+ ]
61
+ interval: 3s
62
+ timeout: 5s
63
+ retries: 20
64
+ start_period: 10s
65
+
66
+ temporal-ui:
67
+ depends_on:
68
+ - temporal
69
+ environment:
70
+ - TEMPORAL_ADDRESS=temporal:7233
71
+ - TEMPORAL_CORS_ORIGINS=http://localhost:3000
72
+ image: temporalio/ui:latest
73
+ networks:
74
+ - main
75
+ ports:
76
+ - '8080:8080'
77
+
78
+ api:
79
+ depends_on:
80
+ temporal:
81
+ condition: service_healthy
82
+ image: growthxteam/output-api:latest
83
+ networks:
84
+ - main
85
+ environment:
86
+ - PORT=3001
87
+ - CATALOG_ID=main
88
+ - TEMPORAL_ADDRESS=temporal:7233
89
+ - NODE_ENV=development
90
+ ports:
91
+ - '3001:3001'
92
+
93
+ worker:
94
+ depends_on:
95
+ temporal:
96
+ condition: service_healthy
97
+ image: node:24.3-slim
98
+ networks:
99
+ - main
100
+ env_file: './.env'
101
+ environment:
102
+ - CATALOG_ID=main
103
+ - LOG_HTTP_VERBOSE=true
104
+ - REDIS_URL=redis://redis:6379
105
+ - TEMPORAL_ADDRESS=temporal:7233
106
+ - TRACE_LOCAL_ON=true
107
+ command: npm run start-worker
108
+ working_dir: /app
109
+ volumes:
110
+ - ./:/app
111
+ develop:
112
+ watch:
113
+ - path: ./src
114
+ target: /app/src
115
+ action: restart
116
+ ignore:
117
+ - node_modules/
118
+ - '**/*.test.ts'
119
+ - '**/*.spec.ts'
120
+ - path: ./package.json
121
+ target: /app/package.json
122
+ action: restart
123
+
124
+ volumes:
125
+ postgres:
126
+ redis:
127
+
128
+ networks:
129
+ main:
130
+ driver: bridge
@@ -1,6 +1,7 @@
1
1
  import { Command, Flags } from '@oclif/core';
2
2
  import { AGENT_CONFIG_DIR } from '#config.js';
3
3
  import { initializeAgentConfig, AGENT_CONFIGS } from '#services/coding_agents.js';
4
+ import { getErrorMessage, getErrorCode } from '#utils/error_utils.js';
4
5
  export default class Init extends Command {
5
6
  static description = 'Initialize agent configuration files for AI assistant integration';
6
7
  static examples = [
@@ -39,10 +40,11 @@ export default class Init extends Command {
39
40
  this.log('Claude Code will automatically detect and use these configurations.');
40
41
  }
41
42
  catch (error) {
42
- if (error.code === 'EACCES') {
43
+ if (getErrorCode(error) === 'EACCES') {
43
44
  this.error('Permission denied. Please check file permissions and try again.');
45
+ return;
44
46
  }
45
- this.error(`Failed to initialize agent configuration: ${error.message}`);
47
+ this.error(`Failed to initialize agent configuration: ${getErrorMessage(error)}`);
46
48
  }
47
49
  }
48
50
  }
@@ -0,0 +1,11 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class DevEject extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {};
6
+ static flags: {
7
+ output: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
8
+ force: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
9
+ };
10
+ run(): Promise<void>;
11
+ }
@@ -0,0 +1,58 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { getDefaultDockerComposePath } from '#services/docker.js';
5
+ import { getErrorMessage } from '#utils/error_utils.js';
6
+ import { getEjectSuccessMessage } from '#services/messages.js';
7
+ export default class DevEject extends Command {
8
+ static description = 'Eject the Docker Compose configuration to your project root for customization';
9
+ static examples = [
10
+ '<%= config.bin %> <%= command.id %>',
11
+ '<%= config.bin %> <%= command.id %> --output ./custom-compose.yml'
12
+ ];
13
+ static args = {};
14
+ static flags = {
15
+ output: Flags.string({
16
+ description: 'Output path for the docker-compose file',
17
+ required: false,
18
+ char: 'o',
19
+ default: 'docker-compose.yml'
20
+ }),
21
+ force: Flags.boolean({
22
+ description: 'Overwrite existing file without prompting',
23
+ required: false,
24
+ char: 'f',
25
+ default: false
26
+ })
27
+ };
28
+ async run() {
29
+ const { flags } = await this.parse(DevEject);
30
+ // Source docker-compose file from assets
31
+ const sourcePath = getDefaultDockerComposePath();
32
+ // Destination path (relative to current working directory)
33
+ const destPath = path.resolve(process.cwd(), flags.output);
34
+ try {
35
+ // Check if source file exists
36
+ await fs.access(sourcePath);
37
+ }
38
+ catch {
39
+ this.error(`Docker Compose template not found at: ${sourcePath}`, { exit: 1 });
40
+ }
41
+ // Check if destination file already exists
42
+ const fileExists = await fs.access(destPath).then(() => true).catch(() => false);
43
+ if (fileExists && !flags.force) {
44
+ this.error(`File already exists at ${destPath}. Use --force to overwrite or specify a different output path with --output`, { exit: 1 });
45
+ }
46
+ try {
47
+ // Read the source file
48
+ const dockerComposeContent = await fs.readFile(sourcePath, 'utf-8');
49
+ // Write to destination
50
+ await fs.writeFile(destPath, dockerComposeContent, 'utf-8');
51
+ // Display the styled success message
52
+ this.log(getEjectSuccessMessage(destPath, flags.output, this.config.bin));
53
+ }
54
+ catch (error) {
55
+ this.error(`Failed to eject docker-compose configuration: ${getErrorMessage(error)}`, { exit: 1 });
56
+ }
57
+ }
58
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,109 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
3
+ import fs from 'node:fs/promises';
4
+ import DevEject from './eject.js';
5
+ vi.mock('node:fs/promises');
6
+ describe('dev eject command', () => {
7
+ beforeEach(() => {
8
+ vi.clearAllMocks();
9
+ });
10
+ afterEach(() => {
11
+ vi.restoreAllMocks();
12
+ });
13
+ describe('command structure', () => {
14
+ it('should have correct description', () => {
15
+ expect(DevEject.description).toBeDefined();
16
+ expect(DevEject.description).toContain('Eject');
17
+ expect(DevEject.description).toContain('Docker Compose');
18
+ });
19
+ it('should have examples', () => {
20
+ expect(DevEject.examples).toBeDefined();
21
+ expect(Array.isArray(DevEject.examples)).toBe(true);
22
+ expect(DevEject.examples.length).toBeGreaterThan(0);
23
+ });
24
+ it('should have no required arguments', () => {
25
+ expect(DevEject.args).toBeDefined();
26
+ expect(Object.keys(DevEject.args)).toHaveLength(0);
27
+ });
28
+ it('should have output and force flags defined', () => {
29
+ expect(DevEject.flags).toBeDefined();
30
+ expect(DevEject.flags.output).toBeDefined();
31
+ expect(DevEject.flags.output.description).toContain('Output path');
32
+ expect(DevEject.flags.output.required).toBe(false);
33
+ expect(DevEject.flags.output.char).toBe('o');
34
+ expect(DevEject.flags.output.default).toBe('docker-compose.yml');
35
+ expect(DevEject.flags.force).toBeDefined();
36
+ expect(DevEject.flags.force.description).toContain('Overwrite');
37
+ expect(DevEject.flags.force.required).toBe(false);
38
+ expect(DevEject.flags.force.char).toBe('f');
39
+ expect(DevEject.flags.force.default).toBe(false);
40
+ });
41
+ });
42
+ describe('command instantiation', () => {
43
+ it('should be instantiable', () => {
44
+ const cmd = new DevEject([], {});
45
+ expect(cmd).toBeInstanceOf(DevEject);
46
+ });
47
+ it('should have a run method', () => {
48
+ const cmd = new DevEject([], {});
49
+ expect(cmd.run).toBeDefined();
50
+ expect(typeof cmd.run).toBe('function');
51
+ });
52
+ });
53
+ describe('file ejection', () => {
54
+ it('should eject docker-compose file to default location', async () => {
55
+ const config = {
56
+ runHook: vi.fn().mockResolvedValue({ failures: [], successes: [] })
57
+ };
58
+ const cmd = new DevEject([], config);
59
+ cmd.log = vi.fn();
60
+ cmd.error = vi.fn();
61
+ const mockDockerComposeContent = 'name: output-sdk\nservices:\n redis:\n image: redis:8-alpine';
62
+ // Mock source file exists and can be read
63
+ vi.mocked(fs.access).mockImplementation(path => {
64
+ if (path.toString().includes('assets/docker/docker-compose-dev.yml')) {
65
+ return Promise.resolve();
66
+ }
67
+ // Destination file doesn't exist
68
+ return Promise.reject(new Error('File not found'));
69
+ });
70
+ vi.mocked(fs.readFile).mockResolvedValue(mockDockerComposeContent);
71
+ vi.mocked(fs.writeFile).mockResolvedValue();
72
+ await cmd.run();
73
+ expect(fs.readFile).toHaveBeenCalled();
74
+ expect(fs.writeFile).toHaveBeenCalledWith(expect.stringContaining('docker-compose.yml'), mockDockerComposeContent, 'utf-8');
75
+ expect(cmd.log).toHaveBeenCalledWith(expect.stringContaining('SUCCESS!'));
76
+ });
77
+ it('should error if destination file exists and force flag is not set', async () => {
78
+ const config = {
79
+ runHook: vi.fn().mockResolvedValue({ failures: [], successes: [] })
80
+ };
81
+ const cmd = new DevEject([], config);
82
+ cmd.log = vi.fn();
83
+ cmd.error = vi.fn().mockImplementation(msg => {
84
+ throw new Error(msg);
85
+ });
86
+ // Mock both source and destination files exist
87
+ vi.mocked(fs.access).mockResolvedValue();
88
+ await expect(cmd.run()).rejects.toThrow('File already exists');
89
+ expect(cmd.error).toHaveBeenCalledWith(expect.stringContaining('File already exists'), { exit: 1 });
90
+ });
91
+ it('should overwrite file if force flag is set', async () => {
92
+ const config = {
93
+ runHook: vi.fn().mockResolvedValue({ failures: [], successes: [] })
94
+ };
95
+ const cmd = new DevEject(['--force'], config);
96
+ cmd.log = vi.fn();
97
+ cmd.error = vi.fn();
98
+ const mockDockerComposeContent = 'name: output-sdk\nservices:\n redis:\n image: redis:8-alpine';
99
+ // Mock both source and destination files exist
100
+ vi.mocked(fs.access).mockResolvedValue();
101
+ vi.mocked(fs.readFile).mockResolvedValue(mockDockerComposeContent);
102
+ vi.mocked(fs.writeFile).mockResolvedValue();
103
+ await cmd.run();
104
+ expect(fs.readFile).toHaveBeenCalled();
105
+ expect(fs.writeFile).toHaveBeenCalledWith(expect.stringContaining('docker-compose.yml'), mockDockerComposeContent, 'utf-8');
106
+ expect(cmd.log).toHaveBeenCalledWith(expect.stringContaining('SUCCESS!'));
107
+ });
108
+ });
109
+ });
@@ -0,0 +1,11 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Dev extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {};
6
+ static flags: {
7
+ 'compose-file': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
8
+ 'no-watch': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
9
+ };
10
+ run(): Promise<void>;
11
+ }