@output.ai/cli 0.0.8 → 0.2.3

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 +31 -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
  };
@@ -32,11 +40,19 @@ export type PostWorkflowRunBody = {
32
40
  /** The name of the task queue to send the workflow to */
33
41
  taskQueue?: string;
34
42
  };
43
+ /**
44
+ * An object with information about the trace generated by the execution
45
+ */
46
+ export type PostWorkflowRun200Trace = {
47
+ [key: string]: unknown;
48
+ };
35
49
  export type PostWorkflowRun200 = {
36
50
  /** The workflow execution id */
37
51
  workflowId?: string;
38
52
  /** The output of the workflow */
39
53
  output?: unknown;
54
+ /** An object with information about the trace generated by the execution */
55
+ trace?: PostWorkflowRun200Trace;
40
56
  };
41
57
  export type PostWorkflowStartBody = {
42
58
  /** The name of the workflow to execute */
@@ -75,16 +91,18 @@ export type GetWorkflowIdStatus200 = {
75
91
  completedAt?: number;
76
92
  };
77
93
  /**
78
- * The output of workflow
94
+ * An object with information about the trace generated by the execution
79
95
  */
80
- export type GetWorkflowIdOutput200Output = {
96
+ export type GetWorkflowIdOutput200Trace = {
81
97
  [key: string]: unknown;
82
98
  };
83
99
  export type GetWorkflowIdOutput200 = {
84
100
  /** The workflow execution id */
85
101
  workflowId?: string;
86
102
  /** The output of workflow */
87
- output?: GetWorkflowIdOutput200Output;
103
+ output?: unknown;
104
+ /** An object with information about the trace generated by the execution */
105
+ trace?: GetWorkflowIdOutput200Trace;
88
106
  };
89
107
  export type GetWorkflowCatalogId200 = {
90
108
  /** Each workflow available in this catalog */
@@ -110,7 +128,7 @@ export type getHealthResponseSuccess = (getHealthResponse200) & {
110
128
  };
111
129
  export type getHealthResponse = (getHealthResponseSuccess);
112
130
  export declare const getGetHealthUrl: () => string;
113
- export declare const getHealth: (options?: RequestInit) => Promise<getHealthResponse>;
131
+ export declare const getHealth: (options?: ApiRequestOptions) => Promise<getHealthResponse>;
114
132
  /**
115
133
  * Executes a workflow and waits for it to complete before returning the result
116
134
  * @summary Execute a workflow synchronously
@@ -124,7 +142,7 @@ export type postWorkflowRunResponseSuccess = (postWorkflowRunResponse200) & {
124
142
  };
125
143
  export type postWorkflowRunResponse = (postWorkflowRunResponseSuccess);
126
144
  export declare const getPostWorkflowRunUrl: () => string;
127
- export declare const postWorkflowRun: (postWorkflowRunBody: PostWorkflowRunBody, options?: RequestInit) => Promise<postWorkflowRunResponse>;
145
+ export declare const postWorkflowRun: (postWorkflowRunBody: PostWorkflowRunBody, options?: ApiRequestOptions) => Promise<postWorkflowRunResponse>;
128
146
  /**
129
147
  * @summary Start a workflow asynchronously
130
148
  */
@@ -137,7 +155,7 @@ export type postWorkflowStartResponseSuccess = (postWorkflowStartResponse200) &
137
155
  };
138
156
  export type postWorkflowStartResponse = (postWorkflowStartResponseSuccess);
139
157
  export declare const getPostWorkflowStartUrl: () => string;
140
- export declare const postWorkflowStart: (postWorkflowStartBody: PostWorkflowStartBody, options?: RequestInit) => Promise<postWorkflowStartResponse>;
158
+ export declare const postWorkflowStart: (postWorkflowStartBody: PostWorkflowStartBody, options?: ApiRequestOptions) => Promise<postWorkflowStartResponse>;
141
159
  /**
142
160
  * @summary Get workflow execution status
143
161
  */
@@ -157,7 +175,7 @@ export type getWorkflowIdStatusResponseError = (getWorkflowIdStatusResponse404)
157
175
  };
158
176
  export type getWorkflowIdStatusResponse = (getWorkflowIdStatusResponseSuccess | getWorkflowIdStatusResponseError);
159
177
  export declare const getGetWorkflowIdStatusUrl: (id: string) => string;
160
- export declare const getWorkflowIdStatus: (id: string, options?: RequestInit) => Promise<getWorkflowIdStatusResponse>;
178
+ export declare const getWorkflowIdStatus: (id: string, options?: ApiRequestOptions) => Promise<getWorkflowIdStatusResponse>;
161
179
  /**
162
180
  * @summary Stop a workflow execution
163
181
  */
@@ -177,7 +195,7 @@ export type patchWorkflowIdStopResponseError = (patchWorkflowIdStopResponse404)
177
195
  };
178
196
  export type patchWorkflowIdStopResponse = (patchWorkflowIdStopResponseSuccess | patchWorkflowIdStopResponseError);
179
197
  export declare const getPatchWorkflowIdStopUrl: (id: string) => string;
180
- export declare const patchWorkflowIdStop: (id: string, options?: RequestInit) => Promise<patchWorkflowIdStopResponse>;
198
+ export declare const patchWorkflowIdStop: (id: string, options?: ApiRequestOptions) => Promise<patchWorkflowIdStopResponse>;
181
199
  /**
182
200
  * @summary Return the output of a workflow
183
201
  */
@@ -197,7 +215,7 @@ export type getWorkflowIdOutputResponseError = (getWorkflowIdOutputResponse404)
197
215
  };
198
216
  export type getWorkflowIdOutputResponse = (getWorkflowIdOutputResponseSuccess | getWorkflowIdOutputResponseError);
199
217
  export declare const getGetWorkflowIdOutputUrl: (id: string) => string;
200
- export declare const getWorkflowIdOutput: (id: string, options?: RequestInit) => Promise<getWorkflowIdOutputResponse>;
218
+ export declare const getWorkflowIdOutput: (id: string, options?: ApiRequestOptions) => Promise<getWorkflowIdOutputResponse>;
201
219
  /**
202
220
  * @summary Get a specific workflow catalog by ID
203
221
  */
@@ -210,7 +228,7 @@ export type getWorkflowCatalogIdResponseSuccess = (getWorkflowCatalogIdResponse2
210
228
  };
211
229
  export type getWorkflowCatalogIdResponse = (getWorkflowCatalogIdResponseSuccess);
212
230
  export declare const getGetWorkflowCatalogIdUrl: (id: string) => string;
213
- export declare const getWorkflowCatalogId: (id: string, options?: RequestInit) => Promise<getWorkflowCatalogIdResponse>;
231
+ export declare const getWorkflowCatalogId: (id: string, options?: ApiRequestOptions) => Promise<getWorkflowCatalogIdResponse>;
214
232
  /**
215
233
  * @summary Get the default workflow catalog
216
234
  */
@@ -223,7 +241,7 @@ export type getWorkflowCatalogResponseSuccess = (getWorkflowCatalogResponse200)
223
241
  };
224
242
  export type getWorkflowCatalogResponse = (getWorkflowCatalogResponseSuccess);
225
243
  export declare const getGetWorkflowCatalogUrl: () => string;
226
- export declare const getWorkflowCatalog: (options?: RequestInit) => Promise<getWorkflowCatalogResponse>;
244
+ export declare const getWorkflowCatalog: (options?: ApiRequestOptions) => Promise<getWorkflowCatalogResponse>;
227
245
  /**
228
246
  * @summary Send feedback to a payload
229
247
  */
@@ -236,7 +254,7 @@ export type postWorkflowIdFeedbackResponseSuccess = (postWorkflowIdFeedbackRespo
236
254
  };
237
255
  export type postWorkflowIdFeedbackResponse = (postWorkflowIdFeedbackResponseSuccess);
238
256
  export declare const getPostWorkflowIdFeedbackUrl: (id: string) => string;
239
- export declare const postWorkflowIdFeedback: (id: string, postWorkflowIdFeedbackBody: PostWorkflowIdFeedbackBody, options?: RequestInit) => Promise<postWorkflowIdFeedbackResponse>;
257
+ export declare const postWorkflowIdFeedback: (id: string, postWorkflowIdFeedbackBody: PostWorkflowIdFeedbackBody, options?: ApiRequestOptions) => Promise<postWorkflowIdFeedbackResponse>;
240
258
  /**
241
259
  * @summary A dummy post endpoint for test only
242
260
  */
@@ -249,4 +267,4 @@ export type postHeartbeatResponseSuccess = (postHeartbeatResponse204) & {
249
267
  };
250
268
  export type postHeartbeatResponse = (postHeartbeatResponseSuccess);
251
269
  export declare const getPostHeartbeatUrl: () => string;
252
- export declare const postHeartbeat: (options?: RequestInit) => Promise<postHeartbeatResponse>;
270
+ 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
+ }