@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
@@ -0,0 +1,54 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { validateDockerEnvironment, startDockerCompose, DockerComposeConfigNotFoundError, getDefaultDockerComposePath } from '#services/docker.js';
5
+ import { getErrorMessage } from '#utils/error_utils.js';
6
+ export default class Dev extends Command {
7
+ static description = 'Start Output development services (auto-restarts worker on file changes)';
8
+ static examples = [
9
+ '<%= config.bin %> <%= command.id %>',
10
+ '<%= config.bin %> <%= command.id %> --no-watch',
11
+ '<%= config.bin %> <%= command.id %> --compose-file ./custom-docker-compose.yml'
12
+ ];
13
+ static args = {};
14
+ static flags = {
15
+ 'compose-file': Flags.string({
16
+ description: 'Path to a custom docker-compose file',
17
+ required: false,
18
+ char: 'f'
19
+ }),
20
+ 'no-watch': Flags.boolean({
21
+ description: 'Disable automatic container restart on file changes',
22
+ default: false
23
+ })
24
+ };
25
+ async run() {
26
+ const { flags } = await this.parse(Dev);
27
+ validateDockerEnvironment();
28
+ const dockerComposePath = flags['compose-file'] ?
29
+ path.resolve(process.cwd(), flags['compose-file']) :
30
+ getDefaultDockerComposePath();
31
+ try {
32
+ await fs.access(dockerComposePath);
33
+ }
34
+ catch {
35
+ throw new DockerComposeConfigNotFoundError(dockerComposePath);
36
+ }
37
+ this.log('\n🚀 Starting Output development services...\n');
38
+ if (flags['compose-file']) {
39
+ this.log(`Using custom docker-compose file: ${flags['compose-file']}\n`);
40
+ }
41
+ if (!flags['no-watch']) {
42
+ this.log('👀 File watching enabled - worker will restart automatically on changes\n');
43
+ }
44
+ else {
45
+ this.log('ℹ️ File watching disabled (--no-watch flag used)\n');
46
+ }
47
+ try {
48
+ await startDockerCompose(dockerComposePath, !flags['no-watch']);
49
+ }
50
+ catch (error) {
51
+ this.error(getErrorMessage(error), { exit: 1 });
52
+ }
53
+ }
54
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,150 @@
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 * as dockerService from '#services/docker.js';
5
+ import Dev from './index.js';
6
+ vi.mock('#services/docker.js', () => ({
7
+ validateDockerEnvironment: vi.fn(),
8
+ startDockerCompose: vi.fn().mockResolvedValue(undefined),
9
+ DockerComposeConfigNotFoundError: Error,
10
+ DockerValidationError: Error,
11
+ getDefaultDockerComposePath: vi.fn(() => '/path/to/docker-compose-dev.yml')
12
+ }));
13
+ vi.mock('node:fs/promises', () => ({
14
+ default: {
15
+ access: vi.fn()
16
+ }
17
+ }));
18
+ describe('dev command', () => {
19
+ beforeEach(() => {
20
+ vi.clearAllMocks();
21
+ // By default, docker validation succeeds
22
+ vi.mocked(dockerService.validateDockerEnvironment).mockResolvedValue(undefined);
23
+ // By default, fs.access succeeds (file exists)
24
+ vi.mocked(fs).access.mockResolvedValue(undefined);
25
+ });
26
+ afterEach(() => {
27
+ vi.restoreAllMocks();
28
+ });
29
+ describe('command structure', () => {
30
+ it('should have correct description', () => {
31
+ expect(Dev.description).toBeDefined();
32
+ expect(Dev.description).toContain('development services');
33
+ });
34
+ it('should have examples', () => {
35
+ expect(Dev.examples).toBeDefined();
36
+ expect(Array.isArray(Dev.examples)).toBe(true);
37
+ expect(Dev.examples.length).toBeGreaterThan(0);
38
+ });
39
+ it('should have no required arguments', () => {
40
+ expect(Dev.args).toBeDefined();
41
+ expect(Object.keys(Dev.args)).toHaveLength(0);
42
+ });
43
+ it('should have no-watch flag defined', () => {
44
+ expect(Dev.flags).toBeDefined();
45
+ expect(Dev.flags['no-watch']).toBeDefined();
46
+ expect(Dev.flags['no-watch'].description).toContain('Disable automatic container restart');
47
+ });
48
+ it('should have compose-file flag defined', () => {
49
+ expect(Dev.flags).toBeDefined();
50
+ expect(Dev.flags['compose-file']).toBeDefined();
51
+ expect(Dev.flags['compose-file'].description).toContain('custom docker-compose');
52
+ expect(Dev.flags['compose-file'].required).toBe(false);
53
+ expect(Dev.flags['compose-file'].char).toBe('f');
54
+ });
55
+ });
56
+ describe('command instantiation', () => {
57
+ it('should be instantiable', () => {
58
+ const cmd = new Dev([], {});
59
+ expect(cmd).toBeInstanceOf(Dev);
60
+ });
61
+ it('should have a run method', () => {
62
+ const cmd = new Dev([], {});
63
+ expect(cmd.run).toBeDefined();
64
+ expect(typeof cmd.run).toBe('function');
65
+ });
66
+ });
67
+ describe('Docker validation', () => {
68
+ it('should error if Docker validation fails', async () => {
69
+ const config = {
70
+ runHook: vi.fn().mockResolvedValue({ failures: [], successes: [] })
71
+ };
72
+ const cmd = new Dev([], config);
73
+ cmd.log = vi.fn();
74
+ // Mock parse to return flags
75
+ Object.defineProperty(cmd, 'parse', {
76
+ value: vi.fn().mockResolvedValue({ flags: { 'no-watch': false, 'compose-file': undefined }, args: {} }),
77
+ configurable: true
78
+ });
79
+ const validationError = new Error('Docker is not installed');
80
+ vi.mocked(dockerService.validateDockerEnvironment).mockImplementation(() => {
81
+ throw validationError;
82
+ });
83
+ await expect(cmd.run()).rejects.toThrow('Docker is not installed');
84
+ });
85
+ it('should call validateDockerEnvironment', async () => {
86
+ const cmd = new Dev([], {});
87
+ cmd.log = vi.fn();
88
+ cmd.error = vi.fn();
89
+ // Mock the subprocess spawn to prevent actual execution
90
+ vi.doMock('node:child_process', () => ({
91
+ spawn: vi.fn().mockReturnValue({
92
+ on: vi.fn(),
93
+ kill: vi.fn()
94
+ })
95
+ }));
96
+ // This test just verifies the function is called
97
+ expect(vi.mocked(dockerService.validateDockerEnvironment)).toBeDefined();
98
+ });
99
+ });
100
+ describe('watch functionality', () => {
101
+ it('should enable watch by default', async () => {
102
+ const cmd = new Dev([], {});
103
+ cmd.log = vi.fn();
104
+ cmd.error = vi.fn();
105
+ // Mock parse to return flags
106
+ Object.defineProperty(cmd, 'parse', {
107
+ value: vi.fn().mockResolvedValue({ flags: { 'no-watch': false, 'compose-file': undefined }, args: {} }),
108
+ configurable: true
109
+ });
110
+ await cmd.run();
111
+ expect(dockerService.startDockerCompose).toHaveBeenCalledWith('/path/to/docker-compose-dev.yml', true // enableWatch should be true
112
+ );
113
+ expect(cmd.log).toHaveBeenCalledWith(expect.stringContaining('File watching enabled'));
114
+ });
115
+ it('should disable watch with --no-watch flag', async () => {
116
+ const cmd = new Dev(['--no-watch'], {});
117
+ cmd.log = vi.fn();
118
+ cmd.error = vi.fn();
119
+ // Mock parse to return flags
120
+ Object.defineProperty(cmd, 'parse', {
121
+ value: vi.fn().mockResolvedValue({ flags: { 'no-watch': true, 'compose-file': undefined }, args: {} }),
122
+ configurable: true
123
+ });
124
+ await cmd.run();
125
+ expect(dockerService.startDockerCompose).toHaveBeenCalledWith('/path/to/docker-compose-dev.yml', false // enableWatch should be false
126
+ );
127
+ expect(cmd.log).toHaveBeenCalledWith(expect.stringContaining('File watching disabled'));
128
+ });
129
+ it('should handle docker compose configuration not found', async () => {
130
+ vi.mocked(fs).access.mockRejectedValue(new Error('File not found'));
131
+ const cmd = new Dev([], {});
132
+ cmd.log = vi.fn();
133
+ cmd.error = vi.fn();
134
+ await expect(cmd.run()).rejects.toThrow();
135
+ });
136
+ it('should handle startDockerCompose errors', async () => {
137
+ vi.mocked(dockerService.startDockerCompose).mockRejectedValue(new Error('Docker error'));
138
+ const cmd = new Dev([], {});
139
+ cmd.log = vi.fn();
140
+ cmd.error = vi.fn();
141
+ // Mock parse to return flags
142
+ Object.defineProperty(cmd, 'parse', {
143
+ value: vi.fn().mockResolvedValue({ flags: { 'no-watch': false, 'compose-file': undefined }, args: {} }),
144
+ configurable: true
145
+ });
146
+ await cmd.run();
147
+ expect(cmd.error).toHaveBeenCalledWith('Docker error', { exit: 1 });
148
+ });
149
+ });
150
+ });
@@ -0,0 +1,10 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Init extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {};
6
+ static flags: {
7
+ 'skip-env': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
8
+ };
9
+ run(): Promise<void>;
10
+ }
@@ -0,0 +1,30 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import { UserCancelledError } from '#types/errors.js';
3
+ import { runInit, handleRunInitError } from '#services/project_scaffold.js';
4
+ export default class Init extends Command {
5
+ static description = 'Initialize a new Output SDK workflow project by scaffolding the complete project structure';
6
+ static examples = [
7
+ '<%= config.bin %> <%= command.id %>'
8
+ ];
9
+ static args = {};
10
+ static flags = {
11
+ 'skip-env': Flags.boolean({
12
+ description: 'Skip interactive environment variable configuration',
13
+ default: false
14
+ })
15
+ };
16
+ async run() {
17
+ try {
18
+ const { flags } = await this.parse(Init);
19
+ await runInit((msg) => this.log(msg), (msg) => this.warn(msg), flags['skip-env']);
20
+ }
21
+ catch (error) {
22
+ if (error instanceof UserCancelledError) {
23
+ this.log(error.message);
24
+ return;
25
+ }
26
+ const { errorMessage } = await handleRunInitError(error, (msg) => this.warn(msg));
27
+ this.error(errorMessage);
28
+ }
29
+ }
30
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,109 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
3
+ import * as projectScaffold from '#services/project_scaffold.js';
4
+ import { UserCancelledError } from '#types/errors.js';
5
+ import Init from './init.js';
6
+ vi.mock('#services/project_scaffold.js');
7
+ describe('init command', () => {
8
+ beforeEach(() => {
9
+ vi.clearAllMocks();
10
+ vi.mocked(projectScaffold.runInit).mockResolvedValue(undefined);
11
+ vi.mocked(projectScaffold.handleRunInitError).mockResolvedValue({
12
+ shouldCleanup: false,
13
+ errorMessage: 'Failed to create project',
14
+ projectPath: null
15
+ });
16
+ });
17
+ afterEach(() => {
18
+ vi.restoreAllMocks();
19
+ });
20
+ describe('command structure', () => {
21
+ it('should have correct description', () => {
22
+ expect(Init.description).toBeDefined();
23
+ expect(Init.description).toContain('scaffold');
24
+ });
25
+ it('should have correct examples', () => {
26
+ expect(Init.examples).toBeDefined();
27
+ expect(Array.isArray(Init.examples)).toBe(true);
28
+ expect(Init.examples.length).toBeGreaterThan(0);
29
+ });
30
+ it('should have no required arguments', () => {
31
+ expect(Init.args).toBeDefined();
32
+ expect(Object.keys(Init.args)).toHaveLength(0);
33
+ });
34
+ });
35
+ describe('command execution', () => {
36
+ const createTestCommand = (args = [], flags = {}) => {
37
+ const cmd = new Init(args, {});
38
+ cmd.log = vi.fn();
39
+ cmd.warn = vi.fn();
40
+ cmd.error = vi.fn();
41
+ Object.defineProperty(cmd, 'parse', {
42
+ value: vi.fn().mockResolvedValue({
43
+ args: {},
44
+ flags: { 'skip-env': false, ...flags }
45
+ }),
46
+ configurable: true
47
+ });
48
+ return cmd;
49
+ };
50
+ it('should be instantiable', () => {
51
+ const cmd = createTestCommand();
52
+ expect(cmd).toBeInstanceOf(Init);
53
+ });
54
+ it('should have a run method', () => {
55
+ const cmd = createTestCommand();
56
+ expect(cmd.run).toBeDefined();
57
+ expect(typeof cmd.run).toBe('function');
58
+ });
59
+ it('should call runInit with log, warn callbacks, and skip-env flag', async () => {
60
+ const cmd = createTestCommand();
61
+ await cmd.run();
62
+ expect(projectScaffold.runInit).toHaveBeenCalledWith(expect.any(Function), expect.any(Function), false);
63
+ });
64
+ it('should handle UserCancelledError', async () => {
65
+ const error = new UserCancelledError();
66
+ vi.mocked(projectScaffold.runInit).mockRejectedValue(error);
67
+ const cmd = createTestCommand();
68
+ await cmd.run();
69
+ expect(cmd.log).toHaveBeenCalledWith('Init cancelled by user.');
70
+ expect(cmd.error).not.toHaveBeenCalled();
71
+ });
72
+ it('should handle other errors with error handler', async () => {
73
+ const testError = new Error('Test error');
74
+ vi.mocked(projectScaffold.runInit).mockRejectedValue(testError);
75
+ vi.mocked(projectScaffold.handleRunInitError).mockResolvedValue({
76
+ shouldCleanup: true,
77
+ errorMessage: 'Failed to create project',
78
+ projectPath: '/some/path'
79
+ });
80
+ const cmd = createTestCommand();
81
+ await cmd.run();
82
+ expect(projectScaffold.handleRunInitError).toHaveBeenCalledWith(testError, expect.any(Function));
83
+ expect(cmd.error).toHaveBeenCalledWith('Failed to create project');
84
+ });
85
+ it('should pass error message from handler to error', async () => {
86
+ const testError = new Error('Custom error');
87
+ vi.mocked(projectScaffold.runInit).mockRejectedValue(testError);
88
+ vi.mocked(projectScaffold.handleRunInitError).mockResolvedValue({
89
+ shouldCleanup: false,
90
+ errorMessage: 'Folder already exists',
91
+ projectPath: null
92
+ });
93
+ const cmd = createTestCommand();
94
+ await cmd.run();
95
+ expect(cmd.error).toHaveBeenCalledWith('Folder already exists');
96
+ });
97
+ it('should successfully complete project creation', async () => {
98
+ const cmd = createTestCommand();
99
+ await cmd.run();
100
+ expect(projectScaffold.runInit).toHaveBeenCalled();
101
+ expect(cmd.error).not.toHaveBeenCalled();
102
+ });
103
+ it('should pass skip-env flag to runInit when --skip-env is provided', async () => {
104
+ const cmd = createTestCommand([], { 'skip-env': true });
105
+ await cmd.run();
106
+ expect(projectScaffold.runInit).toHaveBeenCalledWith(expect.any(Function), expect.any(Function), true);
107
+ });
108
+ });
109
+ });
@@ -43,6 +43,11 @@ export default class WorkflowRun extends Command {
43
43
  workflowName: args.workflowName,
44
44
  input,
45
45
  taskQueue: flags['task-queue']
46
+ }, {
47
+ // Set a 10-minute timeout for workflow execution
48
+ config: {
49
+ timeout: 600000
50
+ }
46
51
  });
47
52
  if (!response || !response.data) {
48
53
  this.error('API returned invalid response', { exit: 1 });
@@ -4,6 +4,7 @@
4
4
  import { query } from '@anthropic-ai/claude-agent-sdk';
5
5
  import { ux } from '@oclif/core';
6
6
  import * as cliProgress from 'cli-progress';
7
+ import { getErrorMessage, toError } from '#utils/error_utils.js';
7
8
  const ADDITIONAL_INSTRUCTIONS = `
8
9
  ! IMPORTANT !
9
10
  1. Use TodoWrite to track your progress through plan creation.
@@ -53,7 +54,19 @@ export class ClaudeInvocationError extends Error {
53
54
  }
54
55
  function validateEnvironment() {
55
56
  if (!process.env.ANTHROPIC_API_KEY) {
56
- throw new Error('ANTHROPIC_API_KEY environment variable is required');
57
+ throw new Error('ANTHROPIC_API_KEY environment variable is required.\n' +
58
+ '\n' +
59
+ 'Please set it using one of these methods:\n' +
60
+ '1. Add it to a .env file in your project root:\n' +
61
+ ' ANTHROPIC_API_KEY=your-api-key\n' +
62
+ '\n' +
63
+ '2. Export it in your shell:\n' +
64
+ ' export ANTHROPIC_API_KEY=your-api-key\n' +
65
+ '\n' +
66
+ '3. Set it when running the command:\n' +
67
+ ' ANTHROPIC_API_KEY=your-api-key output workflow plan\n' +
68
+ '\n' +
69
+ 'Get your API key from: https://console.anthropic.com/');
57
70
  }
58
71
  }
59
72
  function validateSystem(systemMessage) {
@@ -166,7 +179,7 @@ async function singleQuery(prompt, options = {}) {
166
179
  }
167
180
  catch (error) {
168
181
  progressBar.stop();
169
- throw new ClaudeInvocationError(`Failed to invoke claude-code: ${error.message}`, error);
182
+ throw new ClaudeInvocationError(`Failed to invoke claude-code: ${getErrorMessage(error)}`, toError(error));
170
183
  }
171
184
  }
172
185
  export async function replyToClaude(message, options = {}) {
@@ -1,5 +1,6 @@
1
1
  import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
2
2
  import { invokePlanWorkflow, ClaudeInvocationError } from './claude_client.js';
3
+ import { isError } from '#utils/error_utils.js';
3
4
  // Mock Claude SDK
4
5
  vi.mock('@anthropic-ai/claude-agent-sdk', () => ({
5
6
  query: vi.fn()
@@ -108,7 +109,10 @@ describe('invokePlanWorkflow', () => {
108
109
  await invokePlanWorkflow('Test');
109
110
  }
110
111
  catch (error) {
111
- expect(error.message).toMatch(/Failed to invoke/i);
112
+ expect(isError(error)).toBe(true);
113
+ if (isError(error)) {
114
+ expect(error.message).toMatch(/Failed to invoke/i);
115
+ }
112
116
  }
113
117
  });
114
118
  it('should throw error when no result received', async () => {
@@ -177,12 +177,17 @@ async function createFromTemplate(template, output, variables) {
177
177
  /**
178
178
  * Create a symlink, falling back to copying if symlinks are not supported
179
179
  */
180
- async function createSymlink(source, target) {
180
+ async function createSymlink(source, target, projectRoot) {
181
181
  try {
182
182
  if (await fileExists(target)) {
183
183
  await fs.unlink(target);
184
184
  }
185
- const relativePath = path.relative(path.dirname(target), source);
185
+ // Resolve source path relative to project root if it's relative
186
+ const resolvedSource = path.isAbsolute(source) ?
187
+ source :
188
+ path.join(projectRoot, source);
189
+ // Calculate relative path from target directory to resolved source
190
+ const relativePath = path.relative(path.dirname(target), resolvedSource);
186
191
  await fs.symlink(relativePath, target);
187
192
  ux.stdout(ux.colorize('gray', `Created symlink: ${target} -> ${source}`));
188
193
  }
@@ -190,7 +195,11 @@ async function createSymlink(source, target) {
190
195
  const code = error.code;
191
196
  if (code === 'ENOTSUP' || code === 'EPERM') {
192
197
  ux.stdout(ux.colorize('gray', `Symlinks not supported, creating copy: ${target}`));
193
- const content = await fs.readFile(source, 'utf-8');
198
+ // Use resolved source path for copying
199
+ const resolvedSource = path.isAbsolute(source) ?
200
+ source :
201
+ path.join(projectRoot, source);
202
+ const content = await fs.readFile(resolvedSource, 'utf-8');
194
203
  await fs.writeFile(target, content, 'utf-8');
195
204
  return;
196
205
  }
@@ -224,7 +233,7 @@ async function processMappings(config, variables, force, projectRoot) {
224
233
  await createFromTemplate(mapping.from, fullPath, variables);
225
234
  break;
226
235
  case 'symlink':
227
- await createSymlink(mapping.from, fullPath);
236
+ await createSymlink(mapping.from, fullPath, projectRoot);
228
237
  break;
229
238
  case 'copy':
230
239
  await copyFile(mapping.from, fullPath);
@@ -2,6 +2,7 @@ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
2
  import { getRequiredFiles, checkAgentConfigDirExists, checkAgentStructure, getAgentConfigDir, prepareTemplateVariables, initializeAgentConfig, ensureOutputAIStructure, AGENT_CONFIGS } from './coding_agents.js';
3
3
  import { access } from 'node:fs/promises';
4
4
  import fs from 'node:fs/promises';
5
+ import { getErrorMessage } from '#utils/error_utils.js';
5
6
  vi.mock('node:fs/promises');
6
7
  vi.mock('../utils/paths.js', () => ({
7
8
  getTemplateDir: vi.fn().mockReturnValue('/templates')
@@ -362,7 +363,7 @@ describe('coding_agents service', () => {
362
363
  expect.fail('Should have thrown an error');
363
364
  }
364
365
  catch (error) {
365
- const message = error.message;
366
+ const message = getErrorMessage(error);
366
367
  expect(message).toContain('.outputai/AGENTS.md');
367
368
  expect(message).toContain('.outputai/agents/workflow_planner.md');
368
369
  expect(message).toContain('Run "output-cli agents init --force"');
@@ -0,0 +1,12 @@
1
+ declare class DockerValidationError extends Error {
2
+ }
3
+ export declare class DockerComposeConfigNotFoundError extends Error {
4
+ constructor(dockerComposePath: string);
5
+ }
6
+ declare const isDockerInstalled: () => boolean;
7
+ declare const isDockerComposeAvailable: () => boolean;
8
+ declare const isDockerDaemonRunning: () => boolean;
9
+ export declare function validateDockerEnvironment(): void;
10
+ export declare function startDockerCompose(dockerComposePath: string, enableWatch?: boolean): Promise<void>;
11
+ export declare function getDefaultDockerComposePath(): string;
12
+ export { isDockerInstalled, isDockerComposeAvailable, isDockerDaemonRunning, DockerValidationError };
@@ -0,0 +1,79 @@
1
+ import { execSync, spawn } from 'node:child_process';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { ux } from '@oclif/core';
5
+ class DockerValidationError extends Error {
6
+ }
7
+ export class DockerComposeConfigNotFoundError extends Error {
8
+ constructor(dockerComposePath) {
9
+ super(`Docker Compose configuration not found at: ${dockerComposePath}\n\
10
+ This may indicate a problem with the CLI installation.`);
11
+ }
12
+ }
13
+ const checkDockerCommand = (command) => {
14
+ try {
15
+ execSync(command, { stdio: 'pipe' });
16
+ return true;
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ };
22
+ const isDockerInstalled = () => checkDockerCommand('docker --version');
23
+ const isDockerComposeAvailable = () => checkDockerCommand('docker compose version');
24
+ const isDockerDaemonRunning = () => checkDockerCommand('docker ps');
25
+ const DOCKER_VALIDATIONS = [
26
+ {
27
+ check: isDockerInstalled,
28
+ error: 'Docker is not installed. Please install Docker to use the dev command.\nVisit: https://docs.docker.com/get-docker/'
29
+ },
30
+ {
31
+ check: isDockerComposeAvailable,
32
+ error: 'Docker Compose is not installed. Please install Docker Compose to use the dev command.\nVisit: https://docs.docker.com/compose/install/'
33
+ },
34
+ {
35
+ check: isDockerDaemonRunning,
36
+ error: 'Docker daemon is not running. Please start Docker and try again.'
37
+ }
38
+ ];
39
+ export function validateDockerEnvironment() {
40
+ const failedValidation = DOCKER_VALIDATIONS.find(v => !v.check());
41
+ if (failedValidation) {
42
+ throw new DockerValidationError(failedValidation.error);
43
+ }
44
+ }
45
+ export async function startDockerCompose(dockerComposePath, enableWatch = false) {
46
+ const args = [
47
+ 'compose',
48
+ '-f', dockerComposePath,
49
+ '--project-directory', process.cwd(),
50
+ 'up'
51
+ ];
52
+ if (enableWatch) {
53
+ args.push('--watch');
54
+ }
55
+ const childProcess = spawn('docker', args, {
56
+ stdio: 'inherit',
57
+ cwd: process.cwd()
58
+ });
59
+ const cleanup = () => {
60
+ ux.stdout('⏹️ Stopping services...\n');
61
+ childProcess.kill('SIGTERM');
62
+ };
63
+ process.on('SIGINT', cleanup);
64
+ process.on('SIGTERM', cleanup);
65
+ return new Promise((resolve, reject) => {
66
+ childProcess.on('exit', _code => {
67
+ process.removeListener('SIGINT', cleanup);
68
+ process.removeListener('SIGTERM', cleanup);
69
+ resolve();
70
+ });
71
+ childProcess.on('error', error => {
72
+ reject(new Error(`Failed to start services: ${error.message}`));
73
+ });
74
+ });
75
+ }
76
+ export function getDefaultDockerComposePath() {
77
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../assets/docker/docker-compose-dev.yml');
78
+ }
79
+ export { isDockerInstalled, isDockerComposeAvailable, isDockerDaemonRunning, DockerValidationError };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Interactively configures environment variables for a project by prompting the user
3
+ * to provide values for empty variables or variables marked as secrets.
4
+ *
5
+ * @param projectPath - The absolute path to the project directory containing the .env file
6
+ * @param skipPrompt - If true, skips the configuration prompt and returns false immediately
7
+ * @returns A promise that resolves to true if environment variables were successfully configured,
8
+ * false if configuration was skipped (no .env file, user declined, no variables to configure,
9
+ * or an error occurred)
10
+ */
11
+ export declare function configureEnvironmentVariables(projectPath: string, skipPrompt?: boolean): Promise<boolean>;