@outputai/cli 0.2.1-next.fd72d95.0 → 0.3.1-next.00e0047.0
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/bin/run.js +4 -2
- package/dist/api/generated/api.d.ts +160 -7
- package/dist/api/generated/api.js +33 -1
- package/dist/api/http_client.js +24 -19
- package/dist/assets/docker/docker-compose-dev.yml +5 -9
- package/dist/commands/dev/index.js +12 -1
- package/dist/commands/fix.js +1 -1
- package/dist/commands/fix.spec.js +2 -2
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +5 -1
- package/dist/commands/init.spec.js +10 -5
- package/dist/commands/update.js +1 -1
- package/dist/commands/update.spec.js +2 -2
- package/dist/commands/workflow/plan.js +5 -1
- package/dist/commands/workflow/plan.spec.js +3 -2
- package/dist/commands/workflow/run.d.ts +1 -1
- package/dist/commands/workflow/run.js +8 -5
- package/dist/commands/workflow/run.spec.js +3 -3
- package/dist/commands/workflow/runs/list.d.ts +1 -0
- package/dist/commands/workflow/runs/list.js +7 -0
- package/dist/commands/workflow/start.d.ts +1 -1
- package/dist/commands/workflow/start.js +8 -5
- package/dist/commands/workflow/start.spec.js +1 -1
- package/dist/config.d.ts +11 -38
- package/dist/config.js +34 -42
- package/dist/config.spec.d.ts +1 -0
- package/dist/config.spec.js +129 -0
- package/dist/generated/framework_version.json +1 -1
- package/dist/hooks/init.d.ts +4 -0
- package/dist/hooks/init.js +17 -1
- package/dist/hooks/init.spec.js +79 -5
- package/dist/services/coding_agents.js +5 -1
- package/dist/services/coding_agents.spec.js +19 -6
- package/dist/services/credentials_configurator.js +1 -1
- package/dist/services/docker.js +5 -2
- package/dist/services/docker.spec.js +74 -3
- package/dist/services/env_configurator.js +1 -1
- package/dist/services/env_configurator.spec.js +12 -12
- package/dist/services/messages.js +2 -1
- package/dist/services/project_scaffold.d.ts +1 -1
- package/dist/services/project_scaffold.js +17 -2
- package/dist/services/project_scaffold.spec.js +6 -6
- package/dist/services/workflow_builder.js +5 -1
- package/dist/services/workflow_builder.spec.js +3 -2
- package/dist/services/workflow_runs.d.ts +1 -0
- package/dist/services/workflow_runs.js +3 -0
- package/dist/templates/project/.env.example.template +17 -0
- package/dist/utils/credentials_loader.d.ts +1 -0
- package/dist/utils/credentials_loader.js +18 -0
- package/dist/utils/credentials_loader.spec.d.ts +1 -0
- package/dist/utils/credentials_loader.spec.js +84 -0
- package/dist/utils/env_loader.js +1 -2
- package/dist/utils/error_handler.js +10 -8
- package/dist/utils/interactive.d.ts +2 -0
- package/dist/utils/interactive.js +5 -0
- package/dist/utils/interactive.spec.d.ts +1 -0
- package/dist/utils/interactive.spec.js +40 -0
- package/dist/utils/prompt.d.ts +17 -0
- package/dist/utils/prompt.js +20 -0
- package/dist/utils/prompt.spec.d.ts +1 -0
- package/dist/utils/prompt.spec.js +70 -0
- package/dist/utils/proxy.d.ts +9 -0
- package/dist/utils/proxy.js +24 -0
- package/dist/utils/proxy.spec.d.ts +1 -0
- package/dist/utils/proxy.spec.js +48 -0
- package/dist/utils/validation.d.ts +13 -0
- package/dist/utils/validation.js +31 -0
- package/dist/utils/validation.spec.js +47 -1
- package/dist/views/dev.js +3 -3
- package/dist/views/workflow/list.js +10 -8
- package/package.json +10 -9
package/dist/services/docker.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { ux } from '@oclif/core';
|
|
5
5
|
import semver from 'semver';
|
|
6
|
+
import { config } from '#config.js';
|
|
6
7
|
const DEFAULT_COMPOSE_PATH = '../assets/docker/docker-compose-dev.yml';
|
|
7
8
|
export const SERVICE_HEALTH = {
|
|
8
9
|
HEALTHY: 'healthy',
|
|
@@ -100,7 +101,7 @@ export function parseServiceStatus(jsonOutput) {
|
|
|
100
101
|
});
|
|
101
102
|
}
|
|
102
103
|
export async function getServiceStatus(dockerComposePath) {
|
|
103
|
-
const result = execFileSync('docker', ['compose', '-f', dockerComposePath, 'ps', '--all', '--format', 'json'], { encoding: 'utf-8', cwd: process.cwd() });
|
|
104
|
+
const result = execFileSync('docker', ['compose', '-f', dockerComposePath, '--project-name', config.dockerServiceName, 'ps', '--all', '--format', 'json'], { encoding: 'utf-8', cwd: process.cwd() });
|
|
104
105
|
return parseServiceStatus(result);
|
|
105
106
|
}
|
|
106
107
|
export function isServiceHealthy(service) {
|
|
@@ -126,6 +127,7 @@ export async function startDockerCompose(dockerComposePath, pullPolicy) {
|
|
|
126
127
|
'compose',
|
|
127
128
|
'-f', dockerComposePath,
|
|
128
129
|
'--project-directory', process.cwd(),
|
|
130
|
+
'--project-name', config.dockerServiceName,
|
|
129
131
|
'up'
|
|
130
132
|
];
|
|
131
133
|
if (pullPolicy) {
|
|
@@ -143,6 +145,7 @@ export function startDockerComposeDetached(dockerComposePath, pullPolicy) {
|
|
|
143
145
|
'compose',
|
|
144
146
|
'-f', dockerComposePath,
|
|
145
147
|
'--project-directory', process.cwd(),
|
|
148
|
+
'--project-name', config.dockerServiceName,
|
|
146
149
|
'up', '-d'
|
|
147
150
|
];
|
|
148
151
|
if (pullPolicy) {
|
|
@@ -152,6 +155,6 @@ export function startDockerComposeDetached(dockerComposePath, pullPolicy) {
|
|
|
152
155
|
}
|
|
153
156
|
export async function stopDockerCompose(dockerComposePath) {
|
|
154
157
|
ux.stdout('⏹️ Stopping services...\n');
|
|
155
|
-
execFileSync('docker', ['compose', '-f', dockerComposePath, 'down'], { stdio: 'inherit', cwd: process.cwd() });
|
|
158
|
+
execFileSync('docker', ['compose', '-f', dockerComposePath, '--project-directory', process.cwd(), '--project-name', config.dockerServiceName, 'down'], { stdio: 'inherit', cwd: process.cwd() });
|
|
156
159
|
}
|
|
157
160
|
export { isDockerInstalled, DockerValidationError };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { execFileSync } from 'node:child_process';
|
|
3
|
-
import { parseServiceStatus, getServiceStatus, waitForServicesHealthy, isServiceHealthy, isServiceFailed } from './docker.js';
|
|
2
|
+
import { execFileSync, spawn } from 'node:child_process';
|
|
3
|
+
import { parseServiceStatus, getServiceStatus, startDockerCompose, startDockerComposeDetached, stopDockerCompose, waitForServicesHealthy, isServiceHealthy, isServiceFailed } from './docker.js';
|
|
4
4
|
vi.mock('node:child_process', () => ({
|
|
5
5
|
execSync: vi.fn(),
|
|
6
6
|
execFileSync: vi.fn(),
|
|
@@ -73,7 +73,7 @@ describe('docker service', () => {
|
|
|
73
73
|
const mockOutput = '{"Service":"redis","State":"running","Health":"healthy","Publishers":[]}';
|
|
74
74
|
vi.mocked(execFileSync).mockReturnValue(mockOutput);
|
|
75
75
|
await getServiceStatus('/path/to/docker-compose.yml');
|
|
76
|
-
expect(execFileSync).toHaveBeenCalledWith('docker', ['compose', '-f', '/path/to/docker-compose.yml', 'ps', '--all', '--format', 'json'], expect.objectContaining({ encoding: 'utf-8' }));
|
|
76
|
+
expect(execFileSync).toHaveBeenCalledWith('docker', ['compose', '-f', '/path/to/docker-compose.yml', '--project-name', 'output-sdk', 'ps', '--all', '--format', 'json'], expect.objectContaining({ encoding: 'utf-8' }));
|
|
77
77
|
});
|
|
78
78
|
it('should return parsed service status', async () => {
|
|
79
79
|
const mockOutput = '{"Service":"redis","State":"running","Health":"healthy","Publishers":[{"PublishedPort":6379,"TargetPort":6379}]}';
|
|
@@ -89,6 +89,77 @@ describe('docker service', () => {
|
|
|
89
89
|
await expect(getServiceStatus('/path/to/docker-compose.yml')).rejects.toThrow();
|
|
90
90
|
});
|
|
91
91
|
});
|
|
92
|
+
describe('startDockerCompose', () => {
|
|
93
|
+
it('should pass --project-name to docker compose up', async () => {
|
|
94
|
+
await startDockerCompose('/path/to/docker-compose.yml');
|
|
95
|
+
expect(spawn).toHaveBeenCalledWith('docker', [
|
|
96
|
+
'compose', '-f', '/path/to/docker-compose.yml',
|
|
97
|
+
'--project-directory', process.cwd(),
|
|
98
|
+
'--project-name', 'output-sdk',
|
|
99
|
+
'up'
|
|
100
|
+
], expect.objectContaining({ stdio: ['ignore', 'pipe', 'pipe'], cwd: process.cwd() }));
|
|
101
|
+
});
|
|
102
|
+
it('should append --pull when pullPolicy is provided', async () => {
|
|
103
|
+
await startDockerCompose('/path/to/docker-compose.yml', 'always');
|
|
104
|
+
expect(spawn).toHaveBeenCalledWith('docker', [
|
|
105
|
+
'compose', '-f', '/path/to/docker-compose.yml',
|
|
106
|
+
'--project-directory', process.cwd(),
|
|
107
|
+
'--project-name', 'output-sdk',
|
|
108
|
+
'up', '--pull', 'always'
|
|
109
|
+
], expect.objectContaining({ stdio: ['ignore', 'pipe', 'pipe'], cwd: process.cwd() }));
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
describe('startDockerComposeDetached', () => {
|
|
113
|
+
it('should pass --project-name and -d to docker compose up', () => {
|
|
114
|
+
vi.mocked(execFileSync).mockReturnValue('');
|
|
115
|
+
startDockerComposeDetached('/path/to/docker-compose.yml');
|
|
116
|
+
expect(execFileSync).toHaveBeenCalledWith('docker', [
|
|
117
|
+
'compose', '-f', '/path/to/docker-compose.yml',
|
|
118
|
+
'--project-directory', process.cwd(),
|
|
119
|
+
'--project-name', 'output-sdk',
|
|
120
|
+
'up', '-d'
|
|
121
|
+
], expect.objectContaining({ stdio: 'inherit', cwd: process.cwd() }));
|
|
122
|
+
});
|
|
123
|
+
it('should append --pull when pullPolicy is provided', () => {
|
|
124
|
+
vi.mocked(execFileSync).mockReturnValue('');
|
|
125
|
+
startDockerComposeDetached('/path/to/docker-compose.yml', 'missing');
|
|
126
|
+
expect(execFileSync).toHaveBeenCalledWith('docker', [
|
|
127
|
+
'compose', '-f', '/path/to/docker-compose.yml',
|
|
128
|
+
'--project-directory', process.cwd(),
|
|
129
|
+
'--project-name', 'output-sdk',
|
|
130
|
+
'up', '-d', '--pull', 'missing'
|
|
131
|
+
], expect.objectContaining({ stdio: 'inherit', cwd: process.cwd() }));
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
describe('DOCKER_SERVICE_NAME wiring', () => {
|
|
135
|
+
const saved = process.env.DOCKER_SERVICE_NAME;
|
|
136
|
+
afterEach(() => {
|
|
137
|
+
if (saved === undefined) {
|
|
138
|
+
delete process.env.DOCKER_SERVICE_NAME;
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
process.env.DOCKER_SERVICE_NAME = saved;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
it('threads DOCKER_SERVICE_NAME through to --project-name (not hardcoded output-sdk)', async () => {
|
|
145
|
+
process.env.DOCKER_SERVICE_NAME = 'custom-project';
|
|
146
|
+
vi.mocked(execFileSync).mockReturnValue('');
|
|
147
|
+
await stopDockerCompose('/path/to/docker-compose.yml');
|
|
148
|
+
expect(execFileSync).toHaveBeenCalledWith('docker', [
|
|
149
|
+
'compose', '-f', '/path/to/docker-compose.yml',
|
|
150
|
+
'--project-directory', process.cwd(),
|
|
151
|
+
'--project-name', 'custom-project',
|
|
152
|
+
'down'
|
|
153
|
+
], expect.objectContaining({ stdio: 'inherit' }));
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
describe('stopDockerCompose', () => {
|
|
157
|
+
it('should pass --project-name and --project-directory to docker compose down', async () => {
|
|
158
|
+
vi.mocked(execFileSync).mockReturnValue('');
|
|
159
|
+
await stopDockerCompose('/path/to/docker-compose.yml');
|
|
160
|
+
expect(execFileSync).toHaveBeenCalledWith('docker', ['compose', '-f', '/path/to/docker-compose.yml', '--project-directory', process.cwd(), '--project-name', 'output-sdk', 'down'], expect.objectContaining({ stdio: 'inherit' }));
|
|
161
|
+
});
|
|
162
|
+
});
|
|
92
163
|
describe('isServiceHealthy', () => {
|
|
93
164
|
it('should return true for a running service with health: healthy', () => {
|
|
94
165
|
expect(isServiceHealthy({ name: 'redis', state: 'running', health: 'healthy', ports: [] })).toBe(true);
|
|
@@ -3,7 +3,7 @@ import fs from 'node:fs/promises';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { configureEnvironmentVariables } from './env_configurator.js';
|
|
5
5
|
// Mock inquirer prompts
|
|
6
|
-
vi.mock('
|
|
6
|
+
vi.mock('#utils/prompt.js', () => ({
|
|
7
7
|
input: vi.fn(),
|
|
8
8
|
confirm: vi.fn(),
|
|
9
9
|
password: vi.fn()
|
|
@@ -45,7 +45,7 @@ describe('configureEnvironmentVariables', () => {
|
|
|
45
45
|
expect(result).toBe(false);
|
|
46
46
|
});
|
|
47
47
|
it('should return false if user declines configuration', async () => {
|
|
48
|
-
const { confirm } = await import('
|
|
48
|
+
const { confirm } = await import('#utils/prompt.js');
|
|
49
49
|
vi.mocked(confirm).mockResolvedValue(false);
|
|
50
50
|
await fs.writeFile(testState.envExamplePath, '# API key\nAPIKEY=');
|
|
51
51
|
const result = await configureEnvironmentVariables(testState.tempDir, false);
|
|
@@ -53,14 +53,14 @@ describe('configureEnvironmentVariables', () => {
|
|
|
53
53
|
expect(vi.mocked(confirm)).toHaveBeenCalled();
|
|
54
54
|
});
|
|
55
55
|
it('should return false if no empty variables exist', async () => {
|
|
56
|
-
const { confirm } = await import('
|
|
56
|
+
const { confirm } = await import('#utils/prompt.js');
|
|
57
57
|
vi.mocked(confirm).mockResolvedValue(true);
|
|
58
58
|
await fs.writeFile(testState.envExamplePath, 'APIKEY=my-secret-key');
|
|
59
59
|
const result = await configureEnvironmentVariables(testState.tempDir, false);
|
|
60
60
|
expect(result).toBe(false);
|
|
61
61
|
});
|
|
62
62
|
it('should copy .env.example to .env when user confirms configuration', async () => {
|
|
63
|
-
const { input, confirm } = await import('
|
|
63
|
+
const { input, confirm } = await import('#utils/prompt.js');
|
|
64
64
|
vi.mocked(confirm).mockResolvedValue(true);
|
|
65
65
|
vi.mocked(input).mockResolvedValueOnce('sk-proj-123');
|
|
66
66
|
const originalContent = `# API key
|
|
@@ -72,7 +72,7 @@ APIKEY=`;
|
|
|
72
72
|
await expect(fs.access(testState.envPath)).resolves.toBeUndefined();
|
|
73
73
|
});
|
|
74
74
|
it('should write configured values to .env while leaving .env.example unchanged', async () => {
|
|
75
|
-
const { input, confirm } = await import('
|
|
75
|
+
const { input, confirm } = await import('#utils/prompt.js');
|
|
76
76
|
vi.mocked(confirm).mockResolvedValue(true);
|
|
77
77
|
vi.mocked(input).mockResolvedValueOnce('sk-proj-123');
|
|
78
78
|
const originalContent = `# API key
|
|
@@ -88,7 +88,7 @@ APIKEY=`;
|
|
|
88
88
|
expect(envExampleContent).toBe(originalContent);
|
|
89
89
|
});
|
|
90
90
|
it('should prompt for empty variables and update .env', async () => {
|
|
91
|
-
const { input, confirm } = await import('
|
|
91
|
+
const { input, confirm } = await import('#utils/prompt.js');
|
|
92
92
|
vi.mocked(confirm).mockResolvedValue(true);
|
|
93
93
|
vi.mocked(input).mockResolvedValueOnce('sk-proj-123');
|
|
94
94
|
vi.mocked(input).mockResolvedValueOnce('');
|
|
@@ -105,7 +105,7 @@ OPENAI_API_KEY=`);
|
|
|
105
105
|
expect(content).toContain('OPENAI_API_KEY=');
|
|
106
106
|
});
|
|
107
107
|
it('should preserve comments in .env file', async () => {
|
|
108
|
-
const { input, confirm } = await import('
|
|
108
|
+
const { input, confirm } = await import('#utils/prompt.js');
|
|
109
109
|
vi.mocked(confirm).mockResolvedValue(true);
|
|
110
110
|
vi.mocked(input).mockResolvedValueOnce('test-key');
|
|
111
111
|
const originalContent = `# This is a comment
|
|
@@ -123,7 +123,7 @@ OTHER=value`;
|
|
|
123
123
|
expect(content).toContain('OTHER=value');
|
|
124
124
|
});
|
|
125
125
|
it('should skip placeholder values and only prompt for truly empty variables', async () => {
|
|
126
|
-
const { input, confirm } = await import('
|
|
126
|
+
const { input, confirm } = await import('#utils/prompt.js');
|
|
127
127
|
vi.mocked(confirm).mockResolvedValue(true);
|
|
128
128
|
vi.mocked(input).mockResolvedValueOnce('new-key');
|
|
129
129
|
await fs.writeFile(testState.envExamplePath, `APIKEY=your_api_key_here
|
|
@@ -136,7 +136,7 @@ EMPTY_KEY=`);
|
|
|
136
136
|
}));
|
|
137
137
|
});
|
|
138
138
|
it('should skip variables with existing values', async () => {
|
|
139
|
-
const { input, confirm } = await import('
|
|
139
|
+
const { input, confirm } = await import('#utils/prompt.js');
|
|
140
140
|
vi.mocked(confirm).mockResolvedValue(true);
|
|
141
141
|
vi.mocked(input).mockResolvedValueOnce('new-key');
|
|
142
142
|
await fs.writeFile(testState.envExamplePath, `EXISTING_KEY=existing-value
|
|
@@ -147,7 +147,7 @@ EMPTY_KEY=`);
|
|
|
147
147
|
expect(vi.mocked(input)).toHaveBeenCalledTimes(1);
|
|
148
148
|
});
|
|
149
149
|
it('should handle case where .env already exists (overwrite with copy)', async () => {
|
|
150
|
-
const { input, confirm } = await import('
|
|
150
|
+
const { input, confirm } = await import('#utils/prompt.js');
|
|
151
151
|
vi.mocked(confirm).mockResolvedValue(true);
|
|
152
152
|
vi.mocked(input).mockResolvedValueOnce('new-configured-value');
|
|
153
153
|
// Create existing .env with old content
|
|
@@ -162,7 +162,7 @@ EMPTY_KEY=`);
|
|
|
162
162
|
expect(envContent).not.toContain('OLD_KEY');
|
|
163
163
|
});
|
|
164
164
|
it('should return false if an error occurs during parsing', async () => {
|
|
165
|
-
const { confirm } = await import('
|
|
165
|
+
const { confirm } = await import('#utils/prompt.js');
|
|
166
166
|
vi.mocked(confirm).mockResolvedValue(true);
|
|
167
167
|
await fs.writeFile(testState.envExamplePath, 'KEY=');
|
|
168
168
|
// Delete the .env.example file after access check but before parsing would happen
|
|
@@ -178,7 +178,7 @@ EMPTY_KEY=`);
|
|
|
178
178
|
vi.mocked(fs.copyFile).mockImplementation(originalCopyFile);
|
|
179
179
|
});
|
|
180
180
|
it('should prompt for SECRET marker values with password input', async () => {
|
|
181
|
-
const { password, confirm } = await import('
|
|
181
|
+
const { password, confirm } = await import('#utils/prompt.js');
|
|
182
182
|
vi.mocked(confirm).mockResolvedValue(true);
|
|
183
183
|
vi.mocked(password).mockResolvedValueOnce('my-secret-api-key');
|
|
184
184
|
await fs.writeFile(testState.envExamplePath, `# API Key
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Success and informational messages for project initialization
|
|
3
3
|
*/
|
|
4
4
|
import { ux } from '@oclif/core';
|
|
5
|
+
import { config } from '#config.js';
|
|
5
6
|
/**
|
|
6
7
|
* Creates a colored ASCII art banner for Output.ai
|
|
7
8
|
*/
|
|
@@ -180,7 +181,7 @@ export const getProjectSuccessMessage = (folderName, installSuccess, credentials
|
|
|
180
181
|
note: 'Execute in a new terminal after services are running'
|
|
181
182
|
}, {
|
|
182
183
|
step: 'Monitor workflows',
|
|
183
|
-
command:
|
|
184
|
+
command: `open ${config.temporalUiUrl}`,
|
|
184
185
|
note: 'Access Temporal UI for workflow visualization'
|
|
185
186
|
});
|
|
186
187
|
// Format each step with proper indentation and colors
|
|
@@ -27,5 +27,5 @@ export declare function createSigintHandler(projectPath: string, folderCreated:
|
|
|
27
27
|
* @param skipEnv - Whether to skip environment configuration prompts
|
|
28
28
|
* @param folderName - Optional folder name to skip folder name prompt
|
|
29
29
|
*/
|
|
30
|
-
export declare function runInit(skipEnv?: boolean, folderName?: string): Promise<void>;
|
|
30
|
+
export declare function runInit(skipEnv?: boolean, skipGit?: boolean, folderName?: string): Promise<void>;
|
|
31
31
|
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { input, confirm } from '
|
|
1
|
+
import { input, confirm } from '#utils/prompt.js';
|
|
2
2
|
import { ux } from '@oclif/core';
|
|
3
3
|
import { kebabCase, pascalCase } from 'change-case';
|
|
4
4
|
import fs from 'node:fs/promises';
|
|
@@ -119,6 +119,18 @@ async function executeNpmInstall(projectPath) {
|
|
|
119
119
|
async function initializeAgents(projectPath) {
|
|
120
120
|
await initializeAgentConfig({ projectRoot: projectPath, force: false });
|
|
121
121
|
}
|
|
122
|
+
async function maybeInitializeGit(projectPath) {
|
|
123
|
+
const shouldInit = await confirm({
|
|
124
|
+
message: 'Initialize a git repository?',
|
|
125
|
+
default: true
|
|
126
|
+
});
|
|
127
|
+
if (!shouldInit) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
return executeCommandWithMessages(async () => {
|
|
131
|
+
await executeCommand('git', ['init'], projectPath);
|
|
132
|
+
}, 'Initializing git repository...', 'Git repository initialized');
|
|
133
|
+
}
|
|
122
134
|
/**
|
|
123
135
|
* Format error message for init errors
|
|
124
136
|
* Single responsibility: only format error messages, no cleanup logic
|
|
@@ -172,7 +184,7 @@ function handleRunInitError(error, projectPath, projectFolderCreated) {
|
|
|
172
184
|
* @param skipEnv - Whether to skip environment configuration prompts
|
|
173
185
|
* @param folderName - Optional folder name to skip folder name prompt
|
|
174
186
|
*/
|
|
175
|
-
export async function runInit(skipEnv = false, folderName) {
|
|
187
|
+
export async function runInit(skipEnv = false, skipGit = false, folderName) {
|
|
176
188
|
// Track state for SIGINT cleanup using an object to avoid let
|
|
177
189
|
const state = {
|
|
178
190
|
projectFolderCreated: false,
|
|
@@ -210,6 +222,9 @@ export async function runInit(skipEnv = false, folderName) {
|
|
|
210
222
|
await fs.copyFile(path.join(config.projectPath, '.env.example'), path.join(config.projectPath, '.env'));
|
|
211
223
|
await executeCommandWithMessages(() => initializeAgents(config.projectPath), 'Initializing agent system...', 'Agent system initialized');
|
|
212
224
|
const installSuccess = await executeCommandWithMessages(() => executeNpmInstall(config.projectPath), 'Installing dependencies...', 'Dependencies installed');
|
|
225
|
+
if (!skipGit) {
|
|
226
|
+
await maybeInitializeGit(config.projectPath);
|
|
227
|
+
}
|
|
213
228
|
const nextSteps = getProjectSuccessMessage(config.folderName, installSuccess, credentialsConfigured);
|
|
214
229
|
ux.stdout('Project created successfully!');
|
|
215
230
|
ux.stdout(nextSteps);
|
|
@@ -8,7 +8,7 @@ vi.mock('#utils/framework_version.js', () => ({
|
|
|
8
8
|
})
|
|
9
9
|
}));
|
|
10
10
|
// Mock other dependencies
|
|
11
|
-
vi.mock('
|
|
11
|
+
vi.mock('#utils/prompt.js', () => ({
|
|
12
12
|
input: vi.fn(),
|
|
13
13
|
confirm: vi.fn()
|
|
14
14
|
}));
|
|
@@ -47,7 +47,7 @@ describe('project_scaffold', () => {
|
|
|
47
47
|
});
|
|
48
48
|
describe('getProjectConfig', () => {
|
|
49
49
|
it('should skip all prompts when folderName is provided', async () => {
|
|
50
|
-
const { input } = await import('
|
|
50
|
+
const { input } = await import('#utils/prompt.js');
|
|
51
51
|
const config = await getProjectConfig('my-project');
|
|
52
52
|
expect(config.folderName).toBe('my-project');
|
|
53
53
|
expect(config.projectName).toBe('my-project');
|
|
@@ -58,7 +58,7 @@ describe('project_scaffold', () => {
|
|
|
58
58
|
expect(config.description).toBe('AI Agents & Workflows built with Output.ai for test-folder');
|
|
59
59
|
});
|
|
60
60
|
it('should prompt for project name and folder name when not provided', async () => {
|
|
61
|
-
const { input } = await import('
|
|
61
|
+
const { input } = await import('#utils/prompt.js');
|
|
62
62
|
vi.mocked(input)
|
|
63
63
|
.mockResolvedValueOnce('Test Project')
|
|
64
64
|
.mockResolvedValueOnce('test-project');
|
|
@@ -73,7 +73,7 @@ describe('project_scaffold', () => {
|
|
|
73
73
|
it('should not prompt when all dependencies are available', async () => {
|
|
74
74
|
const { isDockerInstalled } = await import('#services/docker.js');
|
|
75
75
|
const { isClaudeCliAvailable } = await import('#utils/claude.js');
|
|
76
|
-
const { confirm } = await import('
|
|
76
|
+
const { confirm } = await import('#utils/prompt.js');
|
|
77
77
|
vi.mocked(isDockerInstalled).mockReturnValue(true);
|
|
78
78
|
vi.mocked(isClaudeCliAvailable).mockReturnValue(true);
|
|
79
79
|
await checkDependencies();
|
|
@@ -82,7 +82,7 @@ describe('project_scaffold', () => {
|
|
|
82
82
|
it('should prompt user when docker is missing', async () => {
|
|
83
83
|
const { isDockerInstalled } = await import('#services/docker.js');
|
|
84
84
|
const { isClaudeCliAvailable } = await import('#utils/claude.js');
|
|
85
|
-
const { confirm } = await import('
|
|
85
|
+
const { confirm } = await import('#utils/prompt.js');
|
|
86
86
|
vi.mocked(isDockerInstalled).mockReturnValue(false);
|
|
87
87
|
vi.mocked(isClaudeCliAvailable).mockReturnValue(true);
|
|
88
88
|
vi.mocked(confirm).mockResolvedValue(true);
|
|
@@ -94,7 +94,7 @@ describe('project_scaffold', () => {
|
|
|
94
94
|
it('should throw UserCancelledError when user declines to proceed', async () => {
|
|
95
95
|
const { isDockerInstalled } = await import('#services/docker.js');
|
|
96
96
|
const { isClaudeCliAvailable } = await import('#utils/claude.js');
|
|
97
|
-
const { confirm } = await import('
|
|
97
|
+
const { confirm } = await import('#utils/prompt.js');
|
|
98
98
|
vi.mocked(isDockerInstalled).mockReturnValue(false);
|
|
99
99
|
vi.mocked(isClaudeCliAvailable).mockReturnValue(true);
|
|
100
100
|
vi.mocked(confirm).mockResolvedValue(false);
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Workflow builder service for implementing workflows from plan files
|
|
3
3
|
*/
|
|
4
4
|
import { ADDITIONAL_INSTRUCTIONS, BUILD_COMMAND_OPTIONS, invokeBuildWorkflow as invokeBuildWorkflowFromClient, replyToClaude } from './claude_client.js';
|
|
5
|
-
import { input } from '
|
|
5
|
+
import { input } from '#utils/prompt.js';
|
|
6
|
+
import { isInteractive } from '#utils/interactive.js';
|
|
6
7
|
import { ux } from '@oclif/core';
|
|
7
8
|
import fs from 'node:fs/promises';
|
|
8
9
|
import path from 'node:path';
|
|
@@ -70,6 +71,9 @@ async function processModification(modification, currentOutput) {
|
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
73
|
async function interactiveRefinementLoop(currentOutput) {
|
|
74
|
+
if (!isInteractive()) {
|
|
75
|
+
return currentOutput;
|
|
76
|
+
}
|
|
73
77
|
const modification = await promptForModification();
|
|
74
78
|
if (isAcceptCommand(modification)) {
|
|
75
79
|
return currentOutput;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
2
|
import { buildWorkflow, buildWorkflowInteractiveLoop } from './workflow_builder.js';
|
|
3
3
|
import { ADDITIONAL_INSTRUCTIONS, BUILD_COMMAND_OPTIONS, invokeBuildWorkflow, replyToClaude } from './claude_client.js';
|
|
4
|
-
import { input } from '
|
|
4
|
+
import { input } from '#utils/prompt.js';
|
|
5
5
|
import { ux } from '@oclif/core';
|
|
6
6
|
import fs from 'node:fs/promises';
|
|
7
7
|
vi.mock('./claude_client.js');
|
|
8
|
-
vi.mock('
|
|
8
|
+
vi.mock('#utils/prompt.js');
|
|
9
|
+
vi.mock('#utils/interactive.js', () => ({ isInteractive: () => true }));
|
|
9
10
|
vi.mock('@oclif/core', () => ({
|
|
10
11
|
ux: {
|
|
11
12
|
stdout: vi.fn(),
|
|
@@ -9,6 +9,7 @@ export interface WorkflowRunsResult {
|
|
|
9
9
|
}
|
|
10
10
|
export interface FetchWorkflowRunsOptions {
|
|
11
11
|
workflowType?: string;
|
|
12
|
+
catalog?: string;
|
|
12
13
|
limit?: number;
|
|
13
14
|
}
|
|
14
15
|
export declare function fetchWorkflowRuns(options?: FetchWorkflowRunsOptions): Promise<WorkflowRunsResult>;
|
|
@@ -10,6 +10,9 @@ export async function fetchWorkflowRuns(options = {}) {
|
|
|
10
10
|
if (options.workflowType) {
|
|
11
11
|
params.workflowType = options.workflowType;
|
|
12
12
|
}
|
|
13
|
+
if (options.catalog) {
|
|
14
|
+
params.catalog = options.catalog;
|
|
15
|
+
}
|
|
13
16
|
const response = await getWorkflowRuns(params);
|
|
14
17
|
if (!response) {
|
|
15
18
|
throw new Error('Failed to connect to API server. Is it running?');
|
|
@@ -7,3 +7,20 @@ ANTHROPIC_API_KEY=credential:anthropic.api_key
|
|
|
7
7
|
# Configure if you plan to use OpenAI in your LLM prompts
|
|
8
8
|
OPENAI_API_KEY=credential:openai.api_key
|
|
9
9
|
|
|
10
|
+
# --- Host port overrides (for running multiple dev stacks) ---
|
|
11
|
+
# Only the host-side port changes; inter-service traffic (e.g. worker -> Temporal
|
|
12
|
+
# at temporal:7233) is unaffected.
|
|
13
|
+
# OUTPUT_API_HOST_PORT=3001
|
|
14
|
+
# OUTPUT_TEMPORAL_UI_HOST_PORT=8080
|
|
15
|
+
|
|
16
|
+
# --- API connection ---
|
|
17
|
+
# If OUTPUT_API_URL is set, it overrides OUTPUT_API_HOST_PORT.
|
|
18
|
+
# Use OUTPUT_API_URL for remote/staging servers; use OUTPUT_API_HOST_PORT for local port shifts.
|
|
19
|
+
# OUTPUT_API_URL=http://localhost:3001
|
|
20
|
+
|
|
21
|
+
# --- Project isolation ---
|
|
22
|
+
# Compose prefixes named volumes with the project name, so changing
|
|
23
|
+
# DOCKER_SERVICE_NAME creates fresh volumes and leaves the previous ones behind.
|
|
24
|
+
# Run `docker volume ls --filter name=<old-project-name>` to find orphaned
|
|
25
|
+
# volumes and `docker volume rm` to remove them.
|
|
26
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const loadCredentialRefs: () => void;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { InvalidCredentialsKeyError, MalformedCredentialsKeyError, MissingKeyError, resolveCredentialRefs } from '@outputai/credentials';
|
|
2
|
+
const isCredentialsConfigError = (error) => error instanceof MissingKeyError ||
|
|
3
|
+
error instanceof InvalidCredentialsKeyError ||
|
|
4
|
+
error instanceof MalformedCredentialsKeyError;
|
|
5
|
+
export const loadCredentialRefs = () => {
|
|
6
|
+
try {
|
|
7
|
+
resolveCredentialRefs();
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
if (isCredentialsConfigError(error)) {
|
|
11
|
+
console.error(`Error: ${error.message}`);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import * as credentials from '@outputai/credentials';
|
|
3
|
+
import { loadCredentialRefs } from './credentials_loader.js';
|
|
4
|
+
vi.mock('@outputai/credentials', async () => {
|
|
5
|
+
const actual = await vi.importActual('@outputai/credentials');
|
|
6
|
+
return {
|
|
7
|
+
...actual,
|
|
8
|
+
resolveCredentialRefs: vi.fn()
|
|
9
|
+
};
|
|
10
|
+
});
|
|
11
|
+
describe('loadCredentialRefs', () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
vi.clearAllMocks();
|
|
14
|
+
});
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
vi.restoreAllMocks();
|
|
17
|
+
});
|
|
18
|
+
it('should call resolveCredentialRefs without errors when no credentials are misconfigured', () => {
|
|
19
|
+
vi.mocked(credentials.resolveCredentialRefs).mockReturnValue([]);
|
|
20
|
+
expect(() => loadCredentialRefs()).not.toThrow();
|
|
21
|
+
expect(credentials.resolveCredentialRefs).toHaveBeenCalledTimes(1);
|
|
22
|
+
});
|
|
23
|
+
it('should print a clean error message and exit on MissingKeyError without dumping a stack trace', () => {
|
|
24
|
+
vi.mocked(credentials.resolveCredentialRefs).mockImplementation(() => {
|
|
25
|
+
throw new credentials.MissingKeyError();
|
|
26
|
+
});
|
|
27
|
+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
28
|
+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined));
|
|
29
|
+
loadCredentialRefs();
|
|
30
|
+
expect(consoleErrorSpy).toHaveBeenCalledTimes(1);
|
|
31
|
+
const printedMessage = consoleErrorSpy.mock.calls[0]?.[0];
|
|
32
|
+
expect(printedMessage).toContain('No credentials key found');
|
|
33
|
+
expect(printedMessage).toContain('OUTPUT_CREDENTIALS_KEY');
|
|
34
|
+
expect(printedMessage).toContain('config/credentials.key');
|
|
35
|
+
expect(printedMessage).not.toContain(' at ');
|
|
36
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
37
|
+
});
|
|
38
|
+
it('should include the environment-specific hints in the printed message when an environment is set', () => {
|
|
39
|
+
vi.mocked(credentials.resolveCredentialRefs).mockImplementation(() => {
|
|
40
|
+
throw new credentials.MissingKeyError('production');
|
|
41
|
+
});
|
|
42
|
+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
43
|
+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined));
|
|
44
|
+
loadCredentialRefs();
|
|
45
|
+
const printedMessage = consoleErrorSpy.mock.calls[0]?.[0];
|
|
46
|
+
expect(printedMessage).toContain('OUTPUT_CREDENTIALS_KEY_PRODUCTION');
|
|
47
|
+
expect(printedMessage).toContain('config/credentials/production.key');
|
|
48
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
49
|
+
});
|
|
50
|
+
it('should print a clean error message and exit on InvalidCredentialsKeyError', () => {
|
|
51
|
+
vi.mocked(credentials.resolveCredentialRefs).mockImplementation(() => {
|
|
52
|
+
throw new credentials.InvalidCredentialsKeyError('config/credentials.yml.enc');
|
|
53
|
+
});
|
|
54
|
+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
55
|
+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined));
|
|
56
|
+
loadCredentialRefs();
|
|
57
|
+
expect(consoleErrorSpy).toHaveBeenCalledTimes(1);
|
|
58
|
+
const printedMessage = consoleErrorSpy.mock.calls[0]?.[0];
|
|
59
|
+
expect(printedMessage).toContain('Failed to decrypt config/credentials.yml.enc');
|
|
60
|
+
expect(printedMessage).toContain('does not match');
|
|
61
|
+
expect(printedMessage).not.toContain(' at ');
|
|
62
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
63
|
+
});
|
|
64
|
+
it('should print a clean error message and exit on MalformedCredentialsKeyError', () => {
|
|
65
|
+
vi.mocked(credentials.resolveCredentialRefs).mockImplementation(() => {
|
|
66
|
+
throw new credentials.MalformedCredentialsKeyError('config/credentials.yml.enc', 'hex string expected, got unpadded hex of length 55');
|
|
67
|
+
});
|
|
68
|
+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
69
|
+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined));
|
|
70
|
+
loadCredentialRefs();
|
|
71
|
+
const printedMessage = consoleErrorSpy.mock.calls[0]?.[0];
|
|
72
|
+
expect(printedMessage).toContain('is malformed');
|
|
73
|
+
expect(printedMessage).toContain('must be exactly 64 hex characters');
|
|
74
|
+
expect(printedMessage).toContain('unpadded hex of length 55');
|
|
75
|
+
expect(printedMessage).not.toContain(' at ');
|
|
76
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
77
|
+
});
|
|
78
|
+
it('should rethrow unexpected errors so they are not silently swallowed', () => {
|
|
79
|
+
vi.mocked(credentials.resolveCredentialRefs).mockImplementation(() => {
|
|
80
|
+
throw new Error('something else broke');
|
|
81
|
+
});
|
|
82
|
+
expect(() => loadCredentialRefs()).toThrow('something else broke');
|
|
83
|
+
});
|
|
84
|
+
});
|
package/dist/utils/env_loader.js
CHANGED
|
@@ -7,11 +7,10 @@ import { existsSync } from 'node:fs';
|
|
|
7
7
|
import { resolve } from 'node:path';
|
|
8
8
|
import * as dotenv from 'dotenv';
|
|
9
9
|
import debugFactory from 'debug';
|
|
10
|
-
import { config } from '#config.js';
|
|
11
10
|
const debug = debugFactory('output-cli:env-loader');
|
|
12
11
|
export function loadEnvironment() {
|
|
13
12
|
const cwd = process.cwd();
|
|
14
|
-
const envFile =
|
|
13
|
+
const envFile = process.env.OUTPUT_CLI_ENV || '.env';
|
|
15
14
|
const envPath = resolve(cwd, envFile);
|
|
16
15
|
if (!existsSync(envPath)) {
|
|
17
16
|
debug(`Warning: Env file not found: ${envPath}`);
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { config } from '#config.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
function getDefaultMessages() {
|
|
3
|
+
return {
|
|
4
|
+
ECONNREFUSED: `Connection refused to ${config.apiUrl}. Is the API server running?`,
|
|
5
|
+
401: 'Authentication failed. Check your OUTPUT_API_AUTH_TOKEN.',
|
|
6
|
+
404: 'Resource not found.',
|
|
7
|
+
500: 'Server error.',
|
|
8
|
+
UNKNOWN: 'An unknown error occurred.'
|
|
9
|
+
};
|
|
10
|
+
}
|
|
9
11
|
/**
|
|
10
12
|
* Extract error type and message from API response data
|
|
11
13
|
*/
|
|
@@ -49,7 +51,7 @@ function getDetailedErrorMessage(error) {
|
|
|
49
51
|
}
|
|
50
52
|
export function handleApiError(error, errorFn, overrides = {}) {
|
|
51
53
|
const apiError = error;
|
|
52
|
-
const errorMessages = { ...
|
|
54
|
+
const errorMessages = { ...getDefaultMessages(), ...overrides };
|
|
53
55
|
if (apiError.code === 'ECONNREFUSED' || apiError.cause?.code === 'ECONNREFUSED') {
|
|
54
56
|
errorFn(errorMessages.ECONNREFUSED, { exit: 1 });
|
|
55
57
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|