@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.
- package/README.md +48 -0
- package/bin/copyassets.sh +8 -0
- package/bin/run.js +4 -0
- package/dist/api/generated/api.d.ts +31 -13
- package/dist/api/http_client.d.ts +8 -2
- package/dist/api/http_client.js +14 -6
- package/dist/api/orval_post_process.d.ts +2 -1
- package/dist/api/orval_post_process.js +18 -5
- package/dist/assets/docker/docker-compose-dev.yml +130 -0
- package/dist/commands/agents/init.js +4 -2
- package/dist/commands/dev/eject.d.ts +11 -0
- package/dist/commands/dev/eject.js +58 -0
- package/dist/commands/dev/eject.spec.d.ts +1 -0
- package/dist/commands/dev/eject.spec.js +109 -0
- package/dist/commands/dev/index.d.ts +11 -0
- package/dist/commands/dev/index.js +54 -0
- package/dist/commands/dev/index.spec.d.ts +1 -0
- package/dist/commands/dev/index.spec.js +150 -0
- package/dist/commands/init.d.ts +10 -0
- package/dist/commands/init.js +30 -0
- package/dist/commands/init.spec.d.ts +1 -0
- package/dist/commands/init.spec.js +109 -0
- package/dist/commands/workflow/run.js +5 -0
- package/dist/services/claude_client.js +15 -2
- package/dist/services/claude_client.spec.js +5 -1
- package/dist/services/coding_agents.js +13 -4
- package/dist/services/coding_agents.spec.js +2 -1
- package/dist/services/docker.d.ts +12 -0
- package/dist/services/docker.js +79 -0
- package/dist/services/env_configurator.d.ts +11 -0
- package/dist/services/env_configurator.js +158 -0
- package/dist/services/env_configurator.spec.d.ts +1 -0
- package/dist/services/env_configurator.spec.js +123 -0
- package/dist/services/messages.d.ts +5 -0
- package/dist/services/messages.js +233 -0
- package/dist/services/project_scaffold.d.ts +6 -0
- package/dist/services/project_scaffold.js +140 -0
- package/dist/services/project_scaffold.spec.d.ts +1 -0
- package/dist/services/project_scaffold.spec.js +43 -0
- package/dist/services/template_processor.d.ts +1 -1
- package/dist/services/template_processor.js +26 -11
- package/dist/services/workflow_builder.js +2 -1
- package/dist/templates/project/.env.template +9 -0
- package/dist/templates/project/.gitignore.template +33 -0
- package/dist/templates/project/README.md.template +60 -0
- package/dist/templates/project/package.json.template +26 -0
- package/dist/templates/project/src/simple/prompts/answer_question@v1.prompt.template +13 -0
- package/dist/templates/project/src/simple/steps.ts.template +16 -0
- package/dist/templates/project/src/simple/workflow.ts.template +22 -0
- package/dist/templates/project/tsconfig.json.template +20 -0
- package/dist/types/errors.d.ts +32 -0
- package/dist/types/errors.js +48 -0
- package/dist/utils/env_loader.d.ts +6 -0
- package/dist/utils/env_loader.js +43 -0
- package/dist/utils/error_utils.d.ts +24 -0
- package/dist/utils/error_utils.js +87 -0
- package/dist/utils/file_system.d.ts +3 -0
- package/dist/utils/file_system.js +33 -0
- package/dist/utils/process.d.ts +4 -0
- package/dist/utils/process.js +48 -0
- package/dist/utils/sdk_versions.d.ts +7 -0
- package/dist/utils/sdk_versions.js +28 -0
- package/dist/utils/sdk_versions.spec.d.ts +1 -0
- package/dist/utils/sdk_versions.spec.js +19 -0
- 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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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>;
|