@output.ai/cli 0.0.9 → 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,158 @@
|
|
|
1
|
+
import { input, confirm, password } from '@inquirer/prompts';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { ux } from '@oclif/core';
|
|
5
|
+
import { getErrorMessage } from '#utils/error_utils.js';
|
|
6
|
+
const COMMENT_LINE = /^\s*#/;
|
|
7
|
+
const COMMENTED_VAR = /^\s*#\s*([A-Z_][A-Z0-9_]*)\s*=\s*(.*)$/;
|
|
8
|
+
const ACTIVE_VAR = /^\s*([A-Z_][A-Z0-9_]*)\s*=\s*(.*)$/;
|
|
9
|
+
const VAR_IN_COMMENT = /^\s*#\s*[A-Z_]+=/;
|
|
10
|
+
const SECRET_MARKER = '<SECRET>';
|
|
11
|
+
function extractDescription(commentLine) {
|
|
12
|
+
return commentLine.replace(/^\s*#\s*/, '').trim();
|
|
13
|
+
}
|
|
14
|
+
function isSecret(value) {
|
|
15
|
+
return value.trim() === SECRET_MARKER;
|
|
16
|
+
}
|
|
17
|
+
function createEnvVariable(match, options) {
|
|
18
|
+
return {
|
|
19
|
+
key: match[1],
|
|
20
|
+
value: match[2],
|
|
21
|
+
description: options.lastComment ? extractDescription(options.lastComment) : undefined,
|
|
22
|
+
lineNumber: options.lineNumber,
|
|
23
|
+
isCommented: options.isCommented,
|
|
24
|
+
originalLine: options.line,
|
|
25
|
+
isSecret: isSecret(match[2])
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
async function parseEnvFile(filePath) {
|
|
29
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
30
|
+
const lines = content.split('\n');
|
|
31
|
+
// Use an object to track state without reassigning
|
|
32
|
+
const state = { lastComment: null };
|
|
33
|
+
const variables = [];
|
|
34
|
+
lines.forEach((line, i) => {
|
|
35
|
+
// Check if line is a comment (but not a commented-out variable)
|
|
36
|
+
if (line.match(COMMENT_LINE) && !line.match(VAR_IN_COMMENT)) {
|
|
37
|
+
state.lastComment = line;
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
// Check for commented-out variable
|
|
41
|
+
const commentedMatch = line.match(COMMENTED_VAR);
|
|
42
|
+
if (commentedMatch) {
|
|
43
|
+
variables.push(createEnvVariable(commentedMatch, {
|
|
44
|
+
lineNumber: i,
|
|
45
|
+
line,
|
|
46
|
+
isCommented: true,
|
|
47
|
+
lastComment: state.lastComment
|
|
48
|
+
}));
|
|
49
|
+
state.lastComment = null;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// Check for active variable
|
|
53
|
+
const activeMatch = line.match(ACTIVE_VAR);
|
|
54
|
+
if (activeMatch) {
|
|
55
|
+
variables.push(createEnvVariable(activeMatch, {
|
|
56
|
+
lineNumber: i,
|
|
57
|
+
line,
|
|
58
|
+
isCommented: false,
|
|
59
|
+
lastComment: state.lastComment
|
|
60
|
+
}));
|
|
61
|
+
state.lastComment = null;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// Reset lastComment if we hit a blank line or non-comment line
|
|
65
|
+
if (line.trim() === '' || !line.match(COMMENT_LINE)) {
|
|
66
|
+
state.lastComment = null;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
return variables;
|
|
70
|
+
}
|
|
71
|
+
const isEmpty = (value) => value.trim() === '';
|
|
72
|
+
const promptForVariables = async (variables) => variables.reduce(async (accPromise, variable) => {
|
|
73
|
+
const acc = await accPromise;
|
|
74
|
+
// Skip if value is not empty and not a secret marker
|
|
75
|
+
if (!isEmpty(variable.value) && !variable.isSecret) {
|
|
76
|
+
return [...acc, variable];
|
|
77
|
+
}
|
|
78
|
+
const description = variable.description ? ` (${variable.description})` : '';
|
|
79
|
+
// Use password prompt for secrets, regular input for others
|
|
80
|
+
const newValue = variable.isSecret ?
|
|
81
|
+
await password({
|
|
82
|
+
message: `${variable.key}${description} (secret):`,
|
|
83
|
+
mask: true
|
|
84
|
+
}) :
|
|
85
|
+
await input({
|
|
86
|
+
message: `${variable.key}${description}:`,
|
|
87
|
+
default: ''
|
|
88
|
+
});
|
|
89
|
+
return [
|
|
90
|
+
...acc,
|
|
91
|
+
{
|
|
92
|
+
...variable,
|
|
93
|
+
value: newValue,
|
|
94
|
+
isCommented: newValue ? false : variable.isCommented,
|
|
95
|
+
isSecret: false // Clear the secret flag after getting the actual value
|
|
96
|
+
}
|
|
97
|
+
];
|
|
98
|
+
}, Promise.resolve([]));
|
|
99
|
+
async function writeEnvFile(filePath, variables) {
|
|
100
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
101
|
+
const lines = content.split('\n');
|
|
102
|
+
const variableMap = new Map(variables.map(v => [v.lineNumber, v]));
|
|
103
|
+
const outputLines = lines.map((line, i) => {
|
|
104
|
+
const variable = variableMap.get(i);
|
|
105
|
+
if (variable) {
|
|
106
|
+
// Reconstruct the variable line
|
|
107
|
+
return `${variable.isCommented ? '# ' : ''}${variable.key}=${variable.value}`;
|
|
108
|
+
}
|
|
109
|
+
// Preserve other lines (comments, blank lines, etc.)
|
|
110
|
+
return line;
|
|
111
|
+
});
|
|
112
|
+
await fs.writeFile(filePath, outputLines.join('\n'), 'utf-8');
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Interactively configures environment variables for a project by prompting the user
|
|
116
|
+
* to provide values for empty variables or variables marked as secrets.
|
|
117
|
+
*
|
|
118
|
+
* @param projectPath - The absolute path to the project directory containing the .env file
|
|
119
|
+
* @param skipPrompt - If true, skips the configuration prompt and returns false immediately
|
|
120
|
+
* @returns A promise that resolves to true if environment variables were successfully configured,
|
|
121
|
+
* false if configuration was skipped (no .env file, user declined, no variables to configure,
|
|
122
|
+
* or an error occurred)
|
|
123
|
+
*/
|
|
124
|
+
export async function configureEnvironmentVariables(projectPath, skipPrompt = false) {
|
|
125
|
+
if (skipPrompt) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
ux.stdout('configuring environment variables...');
|
|
129
|
+
const envPath = path.join(projectPath, '.env');
|
|
130
|
+
try {
|
|
131
|
+
await fs.access(envPath);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
ux.stdout(ux.colorize('red', '🔴 .env file does not exist, nothing to configure'));
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
const shouldConfigure = await confirm({
|
|
138
|
+
message: 'Would you like to configure environment variables now?',
|
|
139
|
+
default: true
|
|
140
|
+
});
|
|
141
|
+
if (!shouldConfigure) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
const variables = await parseEnvFile(envPath);
|
|
146
|
+
const variablesToConfigure = variables.filter(v => isEmpty(v.value) || v.isSecret);
|
|
147
|
+
if (variablesToConfigure.length === 0) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
const updated = await promptForVariables(variables);
|
|
151
|
+
await writeEnvFile(envPath, updated);
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
ux.warn(`Failed to configure environment variables: ${getErrorMessage(error)}`);
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { configureEnvironmentVariables } from './env_configurator.js';
|
|
5
|
+
// Mock inquirer prompts
|
|
6
|
+
vi.mock('@inquirer/prompts', () => ({
|
|
7
|
+
input: vi.fn(),
|
|
8
|
+
confirm: vi.fn()
|
|
9
|
+
}));
|
|
10
|
+
describe('configureEnvironmentVariables', () => {
|
|
11
|
+
const testState = { tempDir: '', envPath: '' };
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
// Clear all mocks before each test
|
|
14
|
+
vi.clearAllMocks();
|
|
15
|
+
// Create temporary directory for test files
|
|
16
|
+
testState.tempDir = path.join('/tmp', `test-env-${Date.now()}`);
|
|
17
|
+
await fs.mkdir(testState.tempDir, { recursive: true });
|
|
18
|
+
testState.envPath = path.join(testState.tempDir, '.env');
|
|
19
|
+
});
|
|
20
|
+
afterEach(async () => {
|
|
21
|
+
// Clean up temporary directory
|
|
22
|
+
try {
|
|
23
|
+
await fs.rm(testState.tempDir, { recursive: true, force: true });
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// Ignore cleanup errors
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
it('should return false if skipPrompt is true', async () => {
|
|
30
|
+
// Create a minimal .env file
|
|
31
|
+
await fs.writeFile(testState.envPath, 'KEY=value');
|
|
32
|
+
const result = await configureEnvironmentVariables(testState.tempDir, true);
|
|
33
|
+
expect(result).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
it('should return false if .env file does not exist', async () => {
|
|
36
|
+
const result = await configureEnvironmentVariables(testState.tempDir, false);
|
|
37
|
+
expect(result).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
it('should return false if user declines configuration', async () => {
|
|
40
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
41
|
+
vi.mocked(confirm).mockResolvedValue(false);
|
|
42
|
+
await fs.writeFile(testState.envPath, '# API key\nAPIKEY=');
|
|
43
|
+
const result = await configureEnvironmentVariables(testState.tempDir, false);
|
|
44
|
+
expect(result).toBe(false);
|
|
45
|
+
expect(vi.mocked(confirm)).toHaveBeenCalled();
|
|
46
|
+
});
|
|
47
|
+
it('should return false if no empty variables exist', async () => {
|
|
48
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
49
|
+
vi.mocked(confirm).mockResolvedValue(true);
|
|
50
|
+
await fs.writeFile(testState.envPath, 'APIKEY=my-secret-key');
|
|
51
|
+
const result = await configureEnvironmentVariables(testState.tempDir, false);
|
|
52
|
+
expect(result).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
it('should prompt for empty variables and update .env', async () => {
|
|
55
|
+
const { input, confirm } = await import('@inquirer/prompts');
|
|
56
|
+
vi.mocked(confirm).mockResolvedValue(true);
|
|
57
|
+
vi.mocked(input).mockResolvedValueOnce('sk-proj-123');
|
|
58
|
+
vi.mocked(input).mockResolvedValueOnce('');
|
|
59
|
+
await fs.writeFile(testState.envPath, `# API key for Anthropic
|
|
60
|
+
ANTHROPIC_API_KEY=
|
|
61
|
+
|
|
62
|
+
# API key for OpenAI
|
|
63
|
+
OPENAI_API_KEY=`);
|
|
64
|
+
const result = await configureEnvironmentVariables(testState.tempDir, false);
|
|
65
|
+
expect(result).toBe(true);
|
|
66
|
+
// Verify file was updated
|
|
67
|
+
const content = await fs.readFile(testState.envPath, 'utf-8');
|
|
68
|
+
expect(content).toContain('ANTHROPIC_API_KEY=sk-proj-123');
|
|
69
|
+
expect(content).toContain('OPENAI_API_KEY=');
|
|
70
|
+
});
|
|
71
|
+
it('should preserve comments in .env file', async () => {
|
|
72
|
+
const { input, confirm } = await import('@inquirer/prompts');
|
|
73
|
+
vi.mocked(confirm).mockResolvedValue(true);
|
|
74
|
+
vi.mocked(input).mockResolvedValueOnce('test-key');
|
|
75
|
+
const originalContent = `# This is a comment
|
|
76
|
+
# API key configuration
|
|
77
|
+
APIKEY=
|
|
78
|
+
|
|
79
|
+
# Another comment
|
|
80
|
+
OTHER=value`;
|
|
81
|
+
await fs.writeFile(testState.envPath, originalContent);
|
|
82
|
+
await configureEnvironmentVariables(testState.tempDir, false);
|
|
83
|
+
const content = await fs.readFile(testState.envPath, 'utf-8');
|
|
84
|
+
expect(content).toContain('# This is a comment');
|
|
85
|
+
expect(content).toContain('# API key configuration');
|
|
86
|
+
expect(content).toContain('# Another comment');
|
|
87
|
+
expect(content).toContain('OTHER=value');
|
|
88
|
+
});
|
|
89
|
+
it('should skip placeholder values and only prompt for truly empty variables', async () => {
|
|
90
|
+
const { input, confirm } = await import('@inquirer/prompts');
|
|
91
|
+
vi.mocked(confirm).mockResolvedValue(true);
|
|
92
|
+
vi.mocked(input).mockResolvedValueOnce('new-key');
|
|
93
|
+
await fs.writeFile(testState.envPath, `APIKEY=your_api_key_here
|
|
94
|
+
EMPTY_KEY=`);
|
|
95
|
+
const result = await configureEnvironmentVariables(testState.tempDir, false);
|
|
96
|
+
expect(result).toBe(true);
|
|
97
|
+
expect(vi.mocked(input)).toHaveBeenCalledTimes(1);
|
|
98
|
+
expect(vi.mocked(input)).toHaveBeenCalledWith(expect.objectContaining({
|
|
99
|
+
message: expect.stringContaining('EMPTY_KEY')
|
|
100
|
+
}));
|
|
101
|
+
});
|
|
102
|
+
it('should skip variables with existing values', async () => {
|
|
103
|
+
const { input, confirm } = await import('@inquirer/prompts');
|
|
104
|
+
vi.mocked(confirm).mockResolvedValue(true);
|
|
105
|
+
vi.mocked(input).mockResolvedValueOnce('new-key');
|
|
106
|
+
await fs.writeFile(testState.envPath, `EXISTING_KEY=existing-value
|
|
107
|
+
|
|
108
|
+
EMPTY_KEY=`);
|
|
109
|
+
await configureEnvironmentVariables(testState.tempDir, false);
|
|
110
|
+
// Should only prompt for EMPTY_KEY, not EXISTING_KEY
|
|
111
|
+
expect(vi.mocked(input)).toHaveBeenCalledTimes(1);
|
|
112
|
+
});
|
|
113
|
+
it('should return false if an error occurs during parsing', async () => {
|
|
114
|
+
// This test verifies error handling by checking behavior when file operations fail
|
|
115
|
+
// The try-catch in configureEnvironmentVariables should handle it gracefully
|
|
116
|
+
await fs.writeFile(testState.envPath, 'KEY=');
|
|
117
|
+
// Delete the file to cause parsing error
|
|
118
|
+
await fs.rm(testState.envPath);
|
|
119
|
+
const result = await configureEnvironmentVariables(testState.tempDir, false);
|
|
120
|
+
// Should return false when error occurs
|
|
121
|
+
expect(result).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Success and informational messages for project initialization
|
|
3
|
+
*/
|
|
4
|
+
export declare const getEjectSuccessMessage: (destPath: string, outputFile: string, binName: string) => string;
|
|
5
|
+
export declare const getProjectSuccessMessage: (folderName: string, installSuccess: boolean, envConfigured?: boolean) => string;
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Success and informational messages for project initialization
|
|
3
|
+
*/
|
|
4
|
+
import { ux } from '@oclif/core';
|
|
5
|
+
/**
|
|
6
|
+
* Creates a colored ASCII art banner for Output.ai
|
|
7
|
+
*/
|
|
8
|
+
const createOutputBanner = () => {
|
|
9
|
+
// ASCII art banner for "Output.ai"
|
|
10
|
+
const banner = `
|
|
11
|
+
██████╗ ██╗ ██╗████████╗██████╗ ██╗ ██╗████████╗ █████╗ ██╗
|
|
12
|
+
██╔═══██╗██║ ██║╚══██╔══╝██╔══██╗██║ ██║╚══██╔══╝ ██╔══██╗██║
|
|
13
|
+
██║ ██║██║ ██║ ██║ ██████╔╝██║ ██║ ██║ ███████║██║
|
|
14
|
+
██║ ██║██║ ██║ ██║ ██╔═══╝ ██║ ██║ ██║ ██╔══██║██║
|
|
15
|
+
╚██████╔╝╚██████╔╝ ██║ ██║ ╚██████╔╝ ██║ ██╗██║ ██║██║
|
|
16
|
+
╚═════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝`;
|
|
17
|
+
// Apply gradient colors from cyan to magenta
|
|
18
|
+
const colors = ['cyan', 'cyan', 'blue', 'blue', 'magenta', 'magenta'];
|
|
19
|
+
return banner
|
|
20
|
+
.split('\n')
|
|
21
|
+
.map((line, index) => {
|
|
22
|
+
return ux.colorize(colors[index], line);
|
|
23
|
+
})
|
|
24
|
+
.join('\n');
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Formats a command for display with proper styling
|
|
28
|
+
*/
|
|
29
|
+
const formatCommand = (command) => {
|
|
30
|
+
return ux.colorize('cyan', command);
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Formats a file path for display
|
|
34
|
+
*/
|
|
35
|
+
const formatPath = (path) => {
|
|
36
|
+
return ux.colorize('yellow', path);
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Creates a section header with styling
|
|
40
|
+
*/
|
|
41
|
+
const createSectionHeader = (title, icon = '') => {
|
|
42
|
+
const header = icon ? `${icon} ${title}` : title;
|
|
43
|
+
return ux.colorize('bold', header);
|
|
44
|
+
};
|
|
45
|
+
export const getEjectSuccessMessage = (destPath, outputFile, binName) => {
|
|
46
|
+
const divider = ux.colorize('dim', '─'.repeat(80));
|
|
47
|
+
const bulletPoint = ux.colorize('green', '▸');
|
|
48
|
+
// Build the customization tips
|
|
49
|
+
const customizationTips = [
|
|
50
|
+
{
|
|
51
|
+
title: 'Environment Variables',
|
|
52
|
+
description: 'Adjust service configurations and API settings'
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
title: 'Port Mappings',
|
|
56
|
+
description: 'Change exposed ports to avoid conflicts'
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
title: 'Service Versions',
|
|
60
|
+
description: 'Update Docker images to specific versions'
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
title: 'Volume Mounts',
|
|
64
|
+
description: 'Add custom volumes for persistent data'
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
title: 'Network Configuration',
|
|
68
|
+
description: 'Modify network settings for your infrastructure'
|
|
69
|
+
}
|
|
70
|
+
];
|
|
71
|
+
const formattedTips = customizationTips.map(tip => {
|
|
72
|
+
return ` ${bulletPoint} ${ux.colorize('white', `${tip.title}:`)} ${ux.colorize('dim', tip.description)}`;
|
|
73
|
+
}).join('\n');
|
|
74
|
+
// Build common modifications examples
|
|
75
|
+
const examples = [
|
|
76
|
+
{
|
|
77
|
+
title: 'Change Redis port',
|
|
78
|
+
code: 'ports:\n - \'6380:6379\' # Changed from 6379'
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
title: 'Add environment variable',
|
|
82
|
+
code: 'environment:\n - MY_CUSTOM_VAR=value'
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
title: 'Use specific image version',
|
|
86
|
+
code: 'image: redis:8.0.0-alpine # Pin to specific version'
|
|
87
|
+
}
|
|
88
|
+
];
|
|
89
|
+
const formattedExamples = examples.map((example, index) => {
|
|
90
|
+
const number = ux.colorize('dim', `${index + 1}.`);
|
|
91
|
+
const title = ux.colorize('white', example.title);
|
|
92
|
+
const code = ux.colorize('cyan', example.code.split('\n').map(line => ` ${line}`).join('\n'));
|
|
93
|
+
return ` ${number} ${title}\n${code}`;
|
|
94
|
+
}).join('\n\n');
|
|
95
|
+
return `
|
|
96
|
+
|
|
97
|
+
${divider}
|
|
98
|
+
|
|
99
|
+
${ux.colorize('bold', ux.colorize('green', '✅ SUCCESS!'))} ${ux.colorize('bold', 'Docker Compose configuration ejected')}
|
|
100
|
+
|
|
101
|
+
${divider}
|
|
102
|
+
|
|
103
|
+
${createSectionHeader('CONFIGURATION DETAILS', '📦')}
|
|
104
|
+
|
|
105
|
+
${bulletPoint} ${ux.colorize('white', 'Location:')} ${formatPath(destPath)}
|
|
106
|
+
${bulletPoint} ${ux.colorize('white', 'Services:')} Temporal, Redis, PostgreSQL, API, Worker, UI
|
|
107
|
+
${bulletPoint} ${ux.colorize('white', 'Network:')} Isolated bridge network for all services
|
|
108
|
+
|
|
109
|
+
${divider}
|
|
110
|
+
|
|
111
|
+
${createSectionHeader('USAGE', '🚀')}
|
|
112
|
+
|
|
113
|
+
${ux.colorize('white', 'Start services with your custom configuration:')}
|
|
114
|
+
|
|
115
|
+
${formatCommand(`${binName} dev --compose-file ${outputFile}`)}
|
|
116
|
+
|
|
117
|
+
${ux.colorize('white', 'Or use Docker Compose directly:')}
|
|
118
|
+
|
|
119
|
+
${formatCommand(`docker compose -f ${outputFile} up`)}
|
|
120
|
+
|
|
121
|
+
${divider}
|
|
122
|
+
|
|
123
|
+
${createSectionHeader('CUSTOMIZATION OPTIONS', '🎨')}
|
|
124
|
+
|
|
125
|
+
${formattedTips}
|
|
126
|
+
|
|
127
|
+
${divider}
|
|
128
|
+
|
|
129
|
+
${createSectionHeader('COMMON MODIFICATIONS', '🔧')}
|
|
130
|
+
|
|
131
|
+
${formattedExamples}
|
|
132
|
+
|
|
133
|
+
${divider}
|
|
134
|
+
|
|
135
|
+
${createSectionHeader('IMPORTANT NOTES', '⚠️')}
|
|
136
|
+
|
|
137
|
+
${bulletPoint} ${ux.colorize('yellow', 'Service Dependencies:')} Maintain the ${ux.colorize('cyan', 'depends_on')} relationships
|
|
138
|
+
${bulletPoint} ${ux.colorize('yellow', 'Health Checks:')} Keep health check configurations for service reliability
|
|
139
|
+
${bulletPoint} ${ux.colorize('yellow', 'Volume Names:')} Be careful when changing volume names (data persistence)
|
|
140
|
+
${bulletPoint} ${ux.colorize('yellow', 'Network Mode:')} The ${ux.colorize('cyan', 'main')} network connects all services
|
|
141
|
+
|
|
142
|
+
${divider}
|
|
143
|
+
|
|
144
|
+
${ux.colorize('dim', '💡 Tip: Test your changes with ')}${formatCommand('docker compose config')}${ux.colorize('dim', ' to validate the syntax')}
|
|
145
|
+
|
|
146
|
+
${ux.colorize('green', ux.colorize('bold', 'Happy customizing! 🛠️'))}
|
|
147
|
+
`;
|
|
148
|
+
};
|
|
149
|
+
export const getProjectSuccessMessage = (folderName, installSuccess, envConfigured = false) => {
|
|
150
|
+
const divider = ux.colorize('dim', '─'.repeat(80));
|
|
151
|
+
const bulletPoint = ux.colorize('green', '▸');
|
|
152
|
+
// Build the next steps array with proper formatting
|
|
153
|
+
const steps = [
|
|
154
|
+
{
|
|
155
|
+
step: 'Navigate to your project',
|
|
156
|
+
command: `cd ${folderName}`
|
|
157
|
+
}
|
|
158
|
+
];
|
|
159
|
+
if (!installSuccess) {
|
|
160
|
+
steps.push({
|
|
161
|
+
step: 'Install dependencies',
|
|
162
|
+
command: 'npm install',
|
|
163
|
+
note: 'Required before running workflows'
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
if (!envConfigured) {
|
|
167
|
+
steps.push({
|
|
168
|
+
step: 'Configure environment variables',
|
|
169
|
+
command: 'Edit .env file',
|
|
170
|
+
note: 'Add your Anthropic/OpenAI API keys'
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
steps.push({
|
|
174
|
+
step: 'Start development services',
|
|
175
|
+
command: 'output dev',
|
|
176
|
+
note: 'Launches Temporal, Redis, PostgreSQL, API, Worker, and UI'
|
|
177
|
+
}, {
|
|
178
|
+
step: 'Run example workflow',
|
|
179
|
+
command: 'output workflow run simple --input \'{"question": "who really is ada lovelace?"}\'',
|
|
180
|
+
note: 'Execute in a new terminal after services are running'
|
|
181
|
+
}, {
|
|
182
|
+
step: 'Monitor workflows',
|
|
183
|
+
command: 'open http://localhost:8080',
|
|
184
|
+
note: 'Access Temporal UI for workflow visualization'
|
|
185
|
+
});
|
|
186
|
+
// Format each step with proper indentation and colors
|
|
187
|
+
const formattedSteps = steps.map((item, index) => {
|
|
188
|
+
const stepNumber = ux.colorize('dim', `${index + 1}.`);
|
|
189
|
+
const stepText = ux.colorize('white', item.step);
|
|
190
|
+
const command = item.command ? `\n ${bulletPoint} ${formatCommand(item.command)}` : '';
|
|
191
|
+
const note = item.note ? `\n ${ux.colorize('dim', ` ${item.note}`)}` : '';
|
|
192
|
+
return ` ${stepNumber} ${stepText}${command}${note}`;
|
|
193
|
+
}).join('\n\n');
|
|
194
|
+
// Build the complete message using template string
|
|
195
|
+
return `
|
|
196
|
+
|
|
197
|
+
${createOutputBanner()}
|
|
198
|
+
|
|
199
|
+
${divider}
|
|
200
|
+
|
|
201
|
+
${ux.colorize('bold', ux.colorize('green', '🎉 SUCCESS!'))} ${ux.colorize('bold', 'Your Output SDK project has been created')}
|
|
202
|
+
|
|
203
|
+
${divider}
|
|
204
|
+
|
|
205
|
+
${createSectionHeader('PROJECT DETAILS', '📁')}
|
|
206
|
+
|
|
207
|
+
${bulletPoint} ${ux.colorize('white', 'Name:')} ${formatPath(folderName)}
|
|
208
|
+
${bulletPoint} ${ux.colorize('white', 'Type:')} Output SDK Workflow Project
|
|
209
|
+
${bulletPoint} ${ux.colorize('white', 'Structure:')} ${formatPath('.outputai/')} (agents), ${formatPath('workflows/')} (implementations)
|
|
210
|
+
|
|
211
|
+
${divider}
|
|
212
|
+
|
|
213
|
+
${createSectionHeader('NEXT STEPS', '🚀')}
|
|
214
|
+
|
|
215
|
+
${formattedSteps}
|
|
216
|
+
|
|
217
|
+
${divider}
|
|
218
|
+
|
|
219
|
+
${createSectionHeader('QUICK START COMMANDS', '⚡')}
|
|
220
|
+
|
|
221
|
+
${bulletPoint} ${ux.colorize('white', 'Plan a workflow:')} ${formatCommand('output workflow plan')}
|
|
222
|
+
${bulletPoint} ${ux.colorize('white', 'Generate from plan:')} ${formatCommand('output workflow generate')}
|
|
223
|
+
${bulletPoint} ${ux.colorize('white', 'List workflows:')} ${formatCommand('output workflow list')}
|
|
224
|
+
${bulletPoint} ${ux.colorize('white', 'View help:')} ${formatCommand('output --help')}
|
|
225
|
+
|
|
226
|
+
${divider}
|
|
227
|
+
|
|
228
|
+
${ux.colorize('dim', '💡 Tip: Use ')}${formatCommand('output workflow plan')}${ux.colorize('dim', ' to design your first custom workflow')}
|
|
229
|
+
${ux.colorize('dim', ' with AI assistance.')}
|
|
230
|
+
|
|
231
|
+
${ux.colorize('green', ux.colorize('bold', 'Happy building with Output SDK! 🚀'))}
|
|
232
|
+
`;
|
|
233
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function runInit(log: (message: string) => void, _warn: (message: string) => void, skipEnv?: boolean): Promise<void>;
|
|
2
|
+
export declare function handleRunInitError(error: unknown, warn: (message: string) => void): Promise<{
|
|
3
|
+
shouldCleanup: boolean;
|
|
4
|
+
errorMessage: string;
|
|
5
|
+
projectPath: string | null;
|
|
6
|
+
}>;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { input } from '@inquirer/prompts';
|
|
2
|
+
import { kebabCase, pascalCase } from 'change-case';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { FolderAlreadyExistsError, UserCancelledError, DirectoryCreationError } from '#types/errors.js';
|
|
6
|
+
import { createDirectory, removeDirectory } from '#utils/file_system.js';
|
|
7
|
+
import { executeCommand, executeCommandWithMessages } from '#utils/process.js';
|
|
8
|
+
import { getSDKVersions } from '#utils/sdk_versions.js';
|
|
9
|
+
import { getErrorMessage, getErrorCode } from '#utils/error_utils.js';
|
|
10
|
+
import { configureEnvironmentVariables } from './env_configurator.js';
|
|
11
|
+
import { getTemplateFiles, processTemplateFile } from './template_processor.js';
|
|
12
|
+
import { ensureOutputAIStructure } from './coding_agents.js';
|
|
13
|
+
import { getProjectSuccessMessage } from './messages.js';
|
|
14
|
+
const getProjectConfig = async () => {
|
|
15
|
+
try {
|
|
16
|
+
const projectName = await input({
|
|
17
|
+
message: 'What is your project name?',
|
|
18
|
+
default: 'my-outputai-workflows'
|
|
19
|
+
}) || 'my-outputai-workflows';
|
|
20
|
+
const folderName = await input({
|
|
21
|
+
message: 'What folder name should be used?',
|
|
22
|
+
default: kebabCase(projectName)
|
|
23
|
+
}) || kebabCase(projectName);
|
|
24
|
+
const description = await input({
|
|
25
|
+
message: 'What is your project description? (optional)',
|
|
26
|
+
default: `An Output SDK workflow for ${kebabCase(projectName)}`
|
|
27
|
+
}) || `An Output SDK workflow for ${kebabCase(projectName)}`;
|
|
28
|
+
return {
|
|
29
|
+
projectName,
|
|
30
|
+
folderName,
|
|
31
|
+
projectPath: path.resolve(process.cwd(), folderName),
|
|
32
|
+
description
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
throw new UserCancelledError();
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
async function scaffoldProjectFiles(projectPath, projectName, description) {
|
|
40
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
41
|
+
const __dirname = path.dirname(__filename);
|
|
42
|
+
const templatesDir = path.join(__dirname, '..', 'templates', 'project');
|
|
43
|
+
// Get SDK versions for dynamic template injection
|
|
44
|
+
const sdkVersions = await getSDKVersions();
|
|
45
|
+
const templateVars = {
|
|
46
|
+
projectName: kebabCase(projectName),
|
|
47
|
+
ProjectName: pascalCase(projectName),
|
|
48
|
+
description: description || `An Output SDK workflow for ${projectName}`,
|
|
49
|
+
coreVersion: sdkVersions.core,
|
|
50
|
+
llmVersion: sdkVersions.llm,
|
|
51
|
+
httpVersion: sdkVersions.http,
|
|
52
|
+
cliVersion: sdkVersions.cli
|
|
53
|
+
};
|
|
54
|
+
const templateFiles = await getTemplateFiles(templatesDir);
|
|
55
|
+
await Promise.all(templateFiles.map(templateFile => processTemplateFile(templateFile, projectPath, templateVars)));
|
|
56
|
+
return templateFiles.map(f => f.outputName);
|
|
57
|
+
}
|
|
58
|
+
async function executeNpmInstall(projectPath) {
|
|
59
|
+
await executeCommand('npm', ['install'], projectPath);
|
|
60
|
+
}
|
|
61
|
+
async function initializeAgents(projectPath) {
|
|
62
|
+
await ensureOutputAIStructure(projectPath);
|
|
63
|
+
}
|
|
64
|
+
function extractProjectPath(error) {
|
|
65
|
+
if (error instanceof FolderAlreadyExistsError) {
|
|
66
|
+
return error.folderPath;
|
|
67
|
+
}
|
|
68
|
+
const errorObject = error;
|
|
69
|
+
if (errorObject.projectPath) {
|
|
70
|
+
return errorObject.projectPath;
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
function handleInitError(error, projectPath) {
|
|
75
|
+
if (error instanceof UserCancelledError) {
|
|
76
|
+
return {
|
|
77
|
+
shouldCleanup: false,
|
|
78
|
+
errorMessage: error.message
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
if (error instanceof FolderAlreadyExistsError) {
|
|
82
|
+
return {
|
|
83
|
+
shouldCleanup: false,
|
|
84
|
+
errorMessage: error.message
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const errorCode = getErrorCode(error);
|
|
88
|
+
if (errorCode === 'EEXIST') {
|
|
89
|
+
return {
|
|
90
|
+
shouldCleanup: false,
|
|
91
|
+
errorMessage: 'Folder already exists'
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (errorCode === 'EACCES') {
|
|
95
|
+
return {
|
|
96
|
+
shouldCleanup: projectPath !== null,
|
|
97
|
+
errorMessage: 'Permission denied'
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
shouldCleanup: projectPath !== null,
|
|
102
|
+
errorMessage: 'Failed to create project'
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
export async function runInit(log, _warn, skipEnv = false) {
|
|
106
|
+
const config = await getProjectConfig();
|
|
107
|
+
try {
|
|
108
|
+
createDirectory(config.projectPath);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
throw new DirectoryCreationError(getErrorMessage(error), config.projectPath);
|
|
112
|
+
}
|
|
113
|
+
log(`✅ Created project folder: ${config.folderName}`);
|
|
114
|
+
try {
|
|
115
|
+
const filesCreated = await scaffoldProjectFiles(config.projectPath, config.projectName, config.description);
|
|
116
|
+
log(`✅ Created ${filesCreated.length} project files`);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
const enrichedError = error;
|
|
120
|
+
enrichedError.projectPath = config.projectPath;
|
|
121
|
+
throw enrichedError;
|
|
122
|
+
}
|
|
123
|
+
const envConfigured = await configureEnvironmentVariables(config.projectPath, skipEnv);
|
|
124
|
+
if (envConfigured) {
|
|
125
|
+
log('🔐 Environment variables configured in .env');
|
|
126
|
+
}
|
|
127
|
+
await executeCommandWithMessages(() => initializeAgents(config.projectPath), '🤖 Initializing agent system...', '✅ Agent system initialized in .outputai/');
|
|
128
|
+
const installSuccess = await executeCommandWithMessages(() => executeNpmInstall(config.projectPath), '📦 Installing dependencies...', '✅ Dependencies installed');
|
|
129
|
+
const nextSteps = getProjectSuccessMessage(config.folderName, installSuccess, envConfigured);
|
|
130
|
+
log('✅ Project created successfully!');
|
|
131
|
+
log(nextSteps);
|
|
132
|
+
}
|
|
133
|
+
export async function handleRunInitError(error, warn) {
|
|
134
|
+
const projectPath = extractProjectPath(error);
|
|
135
|
+
const { shouldCleanup, errorMessage } = handleInitError(error, projectPath);
|
|
136
|
+
if (shouldCleanup && projectPath) {
|
|
137
|
+
await removeDirectory(projectPath, warn);
|
|
138
|
+
}
|
|
139
|
+
return { shouldCleanup, errorMessage, projectPath };
|
|
140
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|