@output.ai/cli 0.0.8 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +48 -0
  2. package/bin/copyassets.sh +8 -0
  3. package/bin/run.js +4 -0
  4. package/dist/api/generated/api.d.ts +31 -13
  5. package/dist/api/http_client.d.ts +8 -2
  6. package/dist/api/http_client.js +14 -6
  7. package/dist/api/orval_post_process.d.ts +2 -1
  8. package/dist/api/orval_post_process.js +18 -5
  9. package/dist/assets/docker/docker-compose-dev.yml +130 -0
  10. package/dist/commands/agents/init.js +4 -2
  11. package/dist/commands/dev/eject.d.ts +11 -0
  12. package/dist/commands/dev/eject.js +58 -0
  13. package/dist/commands/dev/eject.spec.d.ts +1 -0
  14. package/dist/commands/dev/eject.spec.js +109 -0
  15. package/dist/commands/dev/index.d.ts +11 -0
  16. package/dist/commands/dev/index.js +54 -0
  17. package/dist/commands/dev/index.spec.d.ts +1 -0
  18. package/dist/commands/dev/index.spec.js +150 -0
  19. package/dist/commands/init.d.ts +10 -0
  20. package/dist/commands/init.js +30 -0
  21. package/dist/commands/init.spec.d.ts +1 -0
  22. package/dist/commands/init.spec.js +109 -0
  23. package/dist/commands/workflow/run.js +5 -0
  24. package/dist/services/claude_client.js +15 -2
  25. package/dist/services/claude_client.spec.js +5 -1
  26. package/dist/services/coding_agents.js +13 -4
  27. package/dist/services/coding_agents.spec.js +2 -1
  28. package/dist/services/docker.d.ts +12 -0
  29. package/dist/services/docker.js +79 -0
  30. package/dist/services/env_configurator.d.ts +11 -0
  31. package/dist/services/env_configurator.js +158 -0
  32. package/dist/services/env_configurator.spec.d.ts +1 -0
  33. package/dist/services/env_configurator.spec.js +123 -0
  34. package/dist/services/messages.d.ts +5 -0
  35. package/dist/services/messages.js +233 -0
  36. package/dist/services/project_scaffold.d.ts +6 -0
  37. package/dist/services/project_scaffold.js +140 -0
  38. package/dist/services/project_scaffold.spec.d.ts +1 -0
  39. package/dist/services/project_scaffold.spec.js +43 -0
  40. package/dist/services/template_processor.d.ts +1 -1
  41. package/dist/services/template_processor.js +26 -11
  42. package/dist/services/workflow_builder.js +2 -1
  43. package/dist/templates/project/.env.template +9 -0
  44. package/dist/templates/project/.gitignore.template +33 -0
  45. package/dist/templates/project/README.md.template +60 -0
  46. package/dist/templates/project/package.json.template +26 -0
  47. package/dist/templates/project/src/simple/prompts/answer_question@v1.prompt.template +13 -0
  48. package/dist/templates/project/src/simple/steps.ts.template +16 -0
  49. package/dist/templates/project/src/simple/workflow.ts.template +22 -0
  50. package/dist/templates/project/tsconfig.json.template +20 -0
  51. package/dist/types/errors.d.ts +32 -0
  52. package/dist/types/errors.js +48 -0
  53. package/dist/utils/env_loader.d.ts +6 -0
  54. package/dist/utils/env_loader.js +43 -0
  55. package/dist/utils/error_utils.d.ts +24 -0
  56. package/dist/utils/error_utils.js +87 -0
  57. package/dist/utils/file_system.d.ts +3 -0
  58. package/dist/utils/file_system.js +33 -0
  59. package/dist/utils/process.d.ts +4 -0
  60. package/dist/utils/process.js +48 -0
  61. package/dist/utils/sdk_versions.d.ts +7 -0
  62. package/dist/utils/sdk_versions.js +28 -0
  63. package/dist/utils/sdk_versions.spec.d.ts +1 -0
  64. package/dist/utils/sdk_versions.spec.js +19 -0
  65. package/package.json +4 -2
@@ -0,0 +1,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 {};