@output.ai/cli 0.0.8 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -0
- package/bin/copyassets.sh +8 -0
- package/bin/run.js +4 -0
- package/dist/api/generated/api.d.ts +31 -13
- package/dist/api/http_client.d.ts +8 -2
- package/dist/api/http_client.js +14 -6
- package/dist/api/orval_post_process.d.ts +2 -1
- package/dist/api/orval_post_process.js +18 -5
- package/dist/assets/docker/docker-compose-dev.yml +130 -0
- package/dist/commands/agents/init.js +4 -2
- package/dist/commands/dev/eject.d.ts +11 -0
- package/dist/commands/dev/eject.js +58 -0
- package/dist/commands/dev/eject.spec.d.ts +1 -0
- package/dist/commands/dev/eject.spec.js +109 -0
- package/dist/commands/dev/index.d.ts +11 -0
- package/dist/commands/dev/index.js +54 -0
- package/dist/commands/dev/index.spec.d.ts +1 -0
- package/dist/commands/dev/index.spec.js +150 -0
- package/dist/commands/init.d.ts +10 -0
- package/dist/commands/init.js +30 -0
- package/dist/commands/init.spec.d.ts +1 -0
- package/dist/commands/init.spec.js +109 -0
- package/dist/commands/workflow/run.js +5 -0
- package/dist/services/claude_client.js +15 -2
- package/dist/services/claude_client.spec.js +5 -1
- package/dist/services/coding_agents.js +13 -4
- package/dist/services/coding_agents.spec.js +2 -1
- package/dist/services/docker.d.ts +12 -0
- package/dist/services/docker.js +79 -0
- package/dist/services/env_configurator.d.ts +11 -0
- package/dist/services/env_configurator.js +158 -0
- package/dist/services/env_configurator.spec.d.ts +1 -0
- package/dist/services/env_configurator.spec.js +123 -0
- package/dist/services/messages.d.ts +5 -0
- package/dist/services/messages.js +233 -0
- package/dist/services/project_scaffold.d.ts +6 -0
- package/dist/services/project_scaffold.js +140 -0
- package/dist/services/project_scaffold.spec.d.ts +1 -0
- package/dist/services/project_scaffold.spec.js +43 -0
- package/dist/services/template_processor.d.ts +1 -1
- package/dist/services/template_processor.js +26 -11
- package/dist/services/workflow_builder.js +2 -1
- package/dist/templates/project/.env.template +9 -0
- package/dist/templates/project/.gitignore.template +33 -0
- package/dist/templates/project/README.md.template +60 -0
- package/dist/templates/project/package.json.template +26 -0
- package/dist/templates/project/src/simple/prompts/answer_question@v1.prompt.template +13 -0
- package/dist/templates/project/src/simple/steps.ts.template +16 -0
- package/dist/templates/project/src/simple/workflow.ts.template +22 -0
- package/dist/templates/project/tsconfig.json.template +20 -0
- package/dist/types/errors.d.ts +32 -0
- package/dist/types/errors.js +48 -0
- package/dist/utils/env_loader.d.ts +6 -0
- package/dist/utils/env_loader.js +43 -0
- package/dist/utils/error_utils.d.ts +24 -0
- package/dist/utils/error_utils.js +87 -0
- package/dist/utils/file_system.d.ts +3 -0
- package/dist/utils/file_system.js +33 -0
- package/dist/utils/process.d.ts +4 -0
- package/dist/utils/process.js +48 -0
- package/dist/utils/sdk_versions.d.ts +7 -0
- package/dist/utils/sdk_versions.js +28 -0
- package/dist/utils/sdk_versions.spec.d.ts +1 -0
- package/dist/utils/sdk_versions.spec.js +19 -0
- package/package.json +4 -2
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { handleRunInitError } from './project_scaffold.js';
|
|
3
|
+
import { UserCancelledError } from '#types/errors.js';
|
|
4
|
+
// Mock the SDK versions utility
|
|
5
|
+
vi.mock('#utils/sdk_versions.js', () => ({
|
|
6
|
+
getSDKVersions: vi.fn().mockResolvedValue({
|
|
7
|
+
core: '0.1.5',
|
|
8
|
+
llm: '0.2.3',
|
|
9
|
+
http: '0.0.3',
|
|
10
|
+
cli: '0.1.1'
|
|
11
|
+
})
|
|
12
|
+
}));
|
|
13
|
+
// Mock other dependencies
|
|
14
|
+
vi.mock('@inquirer/prompts', () => ({
|
|
15
|
+
input: vi.fn()
|
|
16
|
+
}));
|
|
17
|
+
vi.mock('#utils/file_system.js');
|
|
18
|
+
vi.mock('#utils/process.js');
|
|
19
|
+
vi.mock('./env_configurator.js', () => ({
|
|
20
|
+
configureEnvironmentVariables: vi.fn().mockResolvedValue(false)
|
|
21
|
+
}));
|
|
22
|
+
vi.mock('./template_processor.js');
|
|
23
|
+
vi.mock('./coding_agents.js');
|
|
24
|
+
describe('project_scaffold', () => {
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
vi.clearAllMocks();
|
|
27
|
+
});
|
|
28
|
+
describe('handleRunInitError', () => {
|
|
29
|
+
it('should handle UserCancelledError', async () => {
|
|
30
|
+
const error = new UserCancelledError();
|
|
31
|
+
const warn = vi.fn();
|
|
32
|
+
const result = await handleRunInitError(error, warn);
|
|
33
|
+
expect(result.errorMessage).toBe(error.message);
|
|
34
|
+
expect(result.shouldCleanup).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
it('should return error message for generic errors', async () => {
|
|
37
|
+
const error = new Error('Unknown error');
|
|
38
|
+
const warn = vi.fn();
|
|
39
|
+
const result = await handleRunInitError(error, warn);
|
|
40
|
+
expect(result.errorMessage).toBe('Failed to create project');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { TemplateFile } from '#types/generator.js';
|
|
2
2
|
/**
|
|
3
3
|
* Get list of template files from a directory
|
|
4
|
-
* Automatically discovers all .template files and derives output names
|
|
4
|
+
* Automatically discovers all .template files (including in subdirectories) and derives output names
|
|
5
5
|
*/
|
|
6
6
|
export declare function getTemplateFiles(templatesDir: string): Promise<TemplateFile[]>;
|
|
7
7
|
/**
|
|
@@ -2,25 +2,37 @@ import * as fs from 'node:fs/promises';
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import { processTemplate } from '#utils/template.js';
|
|
4
4
|
const TEMPLATE_EXTENSION = '.template';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
function fileToTemplateFile(file, templatesDir) {
|
|
5
|
+
const isTemplateFile = (file) => file.endsWith(TEMPLATE_EXTENSION);
|
|
6
|
+
const fileToTemplateFile = (file, templatesDir, relativePath = '') => {
|
|
7
|
+
const fullPath = relativePath ? path.join(relativePath, file) : file;
|
|
9
8
|
return {
|
|
10
9
|
name: file,
|
|
11
|
-
path: path.join(templatesDir, file),
|
|
12
|
-
outputName:
|
|
10
|
+
path: path.join(templatesDir, relativePath, file),
|
|
11
|
+
outputName: fullPath.replace(TEMPLATE_EXTENSION, '')
|
|
13
12
|
};
|
|
13
|
+
};
|
|
14
|
+
const processEntry = async (entry, templatesDir, relativePath, getFilesRecursively) => {
|
|
15
|
+
if (entry.isDirectory()) {
|
|
16
|
+
const subDir = relativePath ? path.join(relativePath, entry.name) : entry.name;
|
|
17
|
+
return getFilesRecursively(templatesDir, subDir);
|
|
18
|
+
}
|
|
19
|
+
if (entry.isFile() && isTemplateFile(entry.name)) {
|
|
20
|
+
return [fileToTemplateFile(entry.name, templatesDir, relativePath)];
|
|
21
|
+
}
|
|
22
|
+
return [];
|
|
23
|
+
};
|
|
24
|
+
async function getTemplateFilesRecursive(templatesDir, relativePath = '') {
|
|
25
|
+
const fullPath = relativePath ? path.join(templatesDir, relativePath) : templatesDir;
|
|
26
|
+
const entries = await fs.readdir(fullPath, { withFileTypes: true });
|
|
27
|
+
const results = await Promise.all(entries.map(entry => processEntry(entry, templatesDir, relativePath, getTemplateFilesRecursive)));
|
|
28
|
+
return results.flatMap(x => x);
|
|
14
29
|
}
|
|
15
30
|
/**
|
|
16
31
|
* Get list of template files from a directory
|
|
17
|
-
* Automatically discovers all .template files and derives output names
|
|
32
|
+
* Automatically discovers all .template files (including in subdirectories) and derives output names
|
|
18
33
|
*/
|
|
19
34
|
export async function getTemplateFiles(templatesDir) {
|
|
20
|
-
|
|
21
|
-
return files
|
|
22
|
-
.filter(isTemplateFile)
|
|
23
|
-
.map(file => fileToTemplateFile(file, templatesDir));
|
|
35
|
+
return getTemplateFilesRecursive(templatesDir);
|
|
24
36
|
}
|
|
25
37
|
/**
|
|
26
38
|
* Process a single template file
|
|
@@ -29,6 +41,9 @@ export async function processTemplateFile(templateFile, targetDir, variables) {
|
|
|
29
41
|
const templateContent = await fs.readFile(templateFile.path, 'utf-8');
|
|
30
42
|
const processedContent = processTemplate(templateContent, variables);
|
|
31
43
|
const outputPath = path.join(targetDir, templateFile.outputName);
|
|
44
|
+
// Create parent directories if they don't exist
|
|
45
|
+
const outputDir = path.dirname(outputPath);
|
|
46
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
32
47
|
await fs.writeFile(outputPath, processedContent, 'utf-8');
|
|
33
48
|
}
|
|
34
49
|
/**
|
|
@@ -6,6 +6,7 @@ import { input } from '@inquirer/prompts';
|
|
|
6
6
|
import { ux } from '@oclif/core';
|
|
7
7
|
import fs from 'node:fs/promises';
|
|
8
8
|
import path from 'node:path';
|
|
9
|
+
import { getErrorMessage } from '#utils/error_utils.js';
|
|
9
10
|
const ACCEPT_KEY = 'ACCEPT';
|
|
10
11
|
const SEPARATOR_LINE = '─'.repeat(80);
|
|
11
12
|
function displayImplementationOutput(output, message) {
|
|
@@ -60,7 +61,7 @@ async function processModification(modification, currentOutput) {
|
|
|
60
61
|
return updatedOutput;
|
|
61
62
|
}
|
|
62
63
|
catch (error) {
|
|
63
|
-
ux.error(`Failed to apply modifications: ${error
|
|
64
|
+
ux.error(`Failed to apply modifications: ${getErrorMessage(error)}`);
|
|
64
65
|
ux.stdout('Continuing with previous version...\n');
|
|
65
66
|
return currentOutput;
|
|
66
67
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Docker Compose service name for development environment
|
|
2
|
+
DOCKER_SERVICE_NAME={{projectName}}
|
|
3
|
+
|
|
4
|
+
# Configure if you plan to use Anthropic Models in your LLM prompts
|
|
5
|
+
ANTHROPIC_API_KEY=<SECRET>
|
|
6
|
+
|
|
7
|
+
# Configure if you plan to use OpenAI in your LLM prompts
|
|
8
|
+
OPENAI_API_KEY=<SECRET>
|
|
9
|
+
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Dependencies
|
|
2
|
+
node_modules/
|
|
3
|
+
package-lock.json
|
|
4
|
+
yarn.lock
|
|
5
|
+
bun.lockb
|
|
6
|
+
|
|
7
|
+
# Build output
|
|
8
|
+
dist/
|
|
9
|
+
build/
|
|
10
|
+
|
|
11
|
+
# IDE & Editor
|
|
12
|
+
.vscode/
|
|
13
|
+
.idea/
|
|
14
|
+
*~
|
|
15
|
+
.env
|
|
16
|
+
.DS_Store
|
|
17
|
+
.env.local
|
|
18
|
+
.env.*.local
|
|
19
|
+
|
|
20
|
+
# TypeScript
|
|
21
|
+
*.tsbuildinfo
|
|
22
|
+
*.js.map
|
|
23
|
+
*.d.ts.map
|
|
24
|
+
|
|
25
|
+
# Logs
|
|
26
|
+
logs/
|
|
27
|
+
*.log
|
|
28
|
+
npm-debug.log*
|
|
29
|
+
yarn-debug.log*
|
|
30
|
+
|
|
31
|
+
# OS specific
|
|
32
|
+
Thumbs.db
|
|
33
|
+
.DS_Store
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# {{projectName}}
|
|
2
|
+
|
|
3
|
+
{{description}}
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- Node.js >= 24.3
|
|
8
|
+
- Docker and Docker Compose (for local development)
|
|
9
|
+
|
|
10
|
+
## Getting Started
|
|
11
|
+
|
|
12
|
+
### 1. Install Dependencies
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### 2. Configure Environment
|
|
19
|
+
|
|
20
|
+
Copy `.env.example` to `.env` and add your API keys:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
cp .env.example .env
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Edit `.env` to add:
|
|
27
|
+
- `ANTHROPIC_API_KEY` - for Claude LLM integration
|
|
28
|
+
- `OPENAI_API_KEY` - for OpenAI LLM integration (optional)
|
|
29
|
+
|
|
30
|
+
### 3. Start Output Services
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
output dev
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
This starts:
|
|
37
|
+
- Temporal server and UI (http://localhost:8080)
|
|
38
|
+
- PostgreSQL and Redis databases
|
|
39
|
+
- Output.ai API server (http://localhost:3001)
|
|
40
|
+
- Worker process for executing workflows
|
|
41
|
+
|
|
42
|
+
### 4. Run a workflow
|
|
43
|
+
|
|
44
|
+
In a new terminal:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
output workflow run simple --input '{"question": "who really is ada lovelace?"}'
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 5. Stop Services
|
|
51
|
+
|
|
52
|
+
Press `Ctrl+C` in the terminal running `output dev` to stop all services gracefully.
|
|
53
|
+
|
|
54
|
+
### 6. View Logs
|
|
55
|
+
|
|
56
|
+
Monitor workflow execution and system status in the Temporal UI:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
open http://localhost:8080
|
|
60
|
+
```
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{projectName}}",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "{{description}}",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/worker.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "rm -rf dist/* && tsc -p ./ && copyfiles -u 1 './src/**/*.prompt' dist",
|
|
9
|
+
"start-worker": "npm install && npm run build && output-worker",
|
|
10
|
+
"dev": "output dev"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@output.ai/core": "^{{coreVersion}}",
|
|
14
|
+
"@output.ai/llm": "^{{llmVersion}}",
|
|
15
|
+
"@output.ai/http": "^{{httpVersion}}",
|
|
16
|
+
"copyfiles": "^{{cliVersion}}"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@output.ai/cli": "latest",
|
|
20
|
+
"@types/node": "^22.0.0",
|
|
21
|
+
"typescript": "^5.7.0"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=24.3.0"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
provider: anthropic
|
|
3
|
+
model: claude-opus-4-1-20250805
|
|
4
|
+
temperature: 0.7
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<system>
|
|
8
|
+
You are a helpful assistant. Answer the user's question concisely and clearly.
|
|
9
|
+
</system>
|
|
10
|
+
|
|
11
|
+
<user>
|
|
12
|
+
Answer the following question: \{{ question }}
|
|
13
|
+
</user>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { step, z } from '@output.ai/core';
|
|
2
|
+
import { generateText } from '@output.ai/llm';
|
|
3
|
+
|
|
4
|
+
export const answerQuestion = step( {
|
|
5
|
+
name: 'answerQuestion',
|
|
6
|
+
description: 'Answer a question using an LLM',
|
|
7
|
+
inputSchema: z.string(),
|
|
8
|
+
outputSchema: z.string(),
|
|
9
|
+
fn: async question => {
|
|
10
|
+
const response = await generateText( {
|
|
11
|
+
prompt: 'answer_question@v1',
|
|
12
|
+
variables: { question }
|
|
13
|
+
} );
|
|
14
|
+
return response;
|
|
15
|
+
}
|
|
16
|
+
} );
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { workflow, z } from '@output.ai/core';
|
|
2
|
+
import { answerQuestion } from './steps.js';
|
|
3
|
+
|
|
4
|
+
export default workflow( {
|
|
5
|
+
name: 'simple',
|
|
6
|
+
description: '{{description}}',
|
|
7
|
+
inputSchema: z.object( {
|
|
8
|
+
question: z.string().describe( 'A question to answer' )
|
|
9
|
+
} ),
|
|
10
|
+
outputSchema: z.object( {
|
|
11
|
+
answer: z.string().describe( 'The answer to the question' )
|
|
12
|
+
} ),
|
|
13
|
+
fn: async input => {
|
|
14
|
+
const answer = await answerQuestion( input.question );
|
|
15
|
+
return { answer };
|
|
16
|
+
},
|
|
17
|
+
options: {
|
|
18
|
+
retry: {
|
|
19
|
+
maximumAttempts: 3
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
} );
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ES2022",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"resolveJsonModule": true,
|
|
7
|
+
"allowJs": true,
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"declarationMap": true,
|
|
14
|
+
"sourceMap": true,
|
|
15
|
+
"outDir": "./dist",
|
|
16
|
+
"rootDir": "./src",
|
|
17
|
+
},
|
|
18
|
+
"include": ["./**/*.js", "./**/*.ts", "./**/*.prompt" ],
|
|
19
|
+
"exclude": ["node_modules", "dist"]
|
|
20
|
+
}
|
package/dist/types/errors.d.ts
CHANGED
|
@@ -22,3 +22,35 @@ export declare class TemplateNotFoundError extends Error {
|
|
|
22
22
|
export declare class InvalidOutputDirectoryError extends Error {
|
|
23
23
|
constructor(outputDir: string, reason?: string);
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Error thrown when folder already exists during initialization
|
|
27
|
+
*/
|
|
28
|
+
export declare class FolderAlreadyExistsError extends Error {
|
|
29
|
+
folderPath: string;
|
|
30
|
+
constructor(folderPath: string);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Error thrown when user cancels initialization
|
|
34
|
+
*/
|
|
35
|
+
export declare class UserCancelledError extends Error {
|
|
36
|
+
constructor();
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Type-safe error wrapper that preserves original error with context
|
|
40
|
+
*/
|
|
41
|
+
export declare class ProjectInitError extends Error {
|
|
42
|
+
readonly originalError: Error;
|
|
43
|
+
readonly projectPath: string | null;
|
|
44
|
+
constructor(message: string, originalError: Error, projectPath?: string | null);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Error thrown when directory creation fails with project context
|
|
48
|
+
*/
|
|
49
|
+
export declare class DirectoryCreationError extends Error {
|
|
50
|
+
readonly projectPath: string;
|
|
51
|
+
/**
|
|
52
|
+
* @param message - The error message
|
|
53
|
+
* @param projectPath - The path where directory creation failed
|
|
54
|
+
*/
|
|
55
|
+
constructor(message: string, projectPath: string);
|
|
56
|
+
}
|
package/dist/types/errors.js
CHANGED
|
@@ -33,3 +33,51 @@ export class InvalidOutputDirectoryError extends Error {
|
|
|
33
33
|
super(message);
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Error thrown when folder already exists during initialization
|
|
38
|
+
*/
|
|
39
|
+
export class FolderAlreadyExistsError extends Error {
|
|
40
|
+
folderPath;
|
|
41
|
+
constructor(folderPath) {
|
|
42
|
+
super(`Folder already exists: ${folderPath}`);
|
|
43
|
+
this.folderPath = folderPath;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Error thrown when user cancels initialization
|
|
48
|
+
*/
|
|
49
|
+
export class UserCancelledError extends Error {
|
|
50
|
+
constructor() {
|
|
51
|
+
super('Init cancelled by user.');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Type-safe error wrapper that preserves original error with context
|
|
56
|
+
*/
|
|
57
|
+
export class ProjectInitError extends Error {
|
|
58
|
+
originalError;
|
|
59
|
+
projectPath;
|
|
60
|
+
constructor(message, originalError, projectPath = null) {
|
|
61
|
+
super(message);
|
|
62
|
+
this.originalError = originalError;
|
|
63
|
+
this.projectPath = projectPath;
|
|
64
|
+
// Preserve original stack trace
|
|
65
|
+
if (originalError.stack) {
|
|
66
|
+
this.stack = originalError.stack;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Error thrown when directory creation fails with project context
|
|
72
|
+
*/
|
|
73
|
+
export class DirectoryCreationError extends Error {
|
|
74
|
+
projectPath;
|
|
75
|
+
/**
|
|
76
|
+
* @param message - The error message
|
|
77
|
+
* @param projectPath - The path where directory creation failed
|
|
78
|
+
*/
|
|
79
|
+
constructor(message, projectPath) {
|
|
80
|
+
super(message);
|
|
81
|
+
this.projectPath = projectPath;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment loader utility
|
|
3
|
+
* Loads .env files from the current working directory
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from 'node:fs';
|
|
6
|
+
import * as path from 'node:path';
|
|
7
|
+
import * as dotenv from 'dotenv';
|
|
8
|
+
/**
|
|
9
|
+
* Load environment variables from .env files in the current working directory
|
|
10
|
+
* Loads in order: .env, then .env.local (if exists)
|
|
11
|
+
* .env.local overrides values from .env
|
|
12
|
+
*/
|
|
13
|
+
export function loadEnvironment() {
|
|
14
|
+
const cwd = process.cwd();
|
|
15
|
+
// Load .env file
|
|
16
|
+
const envPath = path.join(cwd, '.env');
|
|
17
|
+
if (fs.existsSync(envPath)) {
|
|
18
|
+
try {
|
|
19
|
+
const result = dotenv.config({ path: envPath });
|
|
20
|
+
if (result.error) {
|
|
21
|
+
// Log warning but don't fail - malformed .env shouldn't break the CLI
|
|
22
|
+
console.warn(`Warning: Error parsing .env file: ${result.error.message}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
// Silent failure - .env loading is optional
|
|
27
|
+
console.warn(`Warning: Could not load .env file: ${error}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// Load .env.local file (overrides .env)
|
|
31
|
+
const envLocalPath = path.join(cwd, '.env.local');
|
|
32
|
+
if (fs.existsSync(envLocalPath)) {
|
|
33
|
+
try {
|
|
34
|
+
const result = dotenv.config({ path: envLocalPath });
|
|
35
|
+
if (result.error) {
|
|
36
|
+
console.warn(`Warning: Error parsing .env.local file: ${result.error.message}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
console.warn(`Warning: Could not load .env.local file: ${error}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type guard to check if an unknown value is an Error object
|
|
3
|
+
*/
|
|
4
|
+
export declare function isError(error: unknown): error is Error;
|
|
5
|
+
/**
|
|
6
|
+
* Type guard to check if an error is a Node.js system error
|
|
7
|
+
*/
|
|
8
|
+
export declare function isNodeError(error: unknown): error is NodeJS.ErrnoException;
|
|
9
|
+
/**
|
|
10
|
+
* Safely extract an error message from an unknown error type
|
|
11
|
+
*/
|
|
12
|
+
export declare function getErrorMessage(error: unknown): string;
|
|
13
|
+
/**
|
|
14
|
+
* Safely extract an error code from a Node.js error
|
|
15
|
+
*/
|
|
16
|
+
export declare function getErrorCode(error: unknown): string | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Convert an unknown error to a proper Error object
|
|
19
|
+
*/
|
|
20
|
+
export declare function toError(error: unknown): Error;
|
|
21
|
+
/**
|
|
22
|
+
* Create a formatted error message with optional context
|
|
23
|
+
*/
|
|
24
|
+
export declare function formatError(error: unknown, context?: string): string;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type guard to check if an unknown value is an Error object
|
|
3
|
+
*/
|
|
4
|
+
export function isError(error) {
|
|
5
|
+
return (error instanceof Error ||
|
|
6
|
+
(typeof error === 'object' &&
|
|
7
|
+
error !== null &&
|
|
8
|
+
'message' in error &&
|
|
9
|
+
typeof error.message === 'string'));
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Type guard to check if an error is a Node.js system error
|
|
13
|
+
*/
|
|
14
|
+
export function isNodeError(error) {
|
|
15
|
+
return isError(error) && 'code' in error;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Safely extract an error message from an unknown error type
|
|
19
|
+
*/
|
|
20
|
+
export function getErrorMessage(error) {
|
|
21
|
+
if (isError(error)) {
|
|
22
|
+
return error.message;
|
|
23
|
+
}
|
|
24
|
+
if (typeof error === 'string') {
|
|
25
|
+
return error;
|
|
26
|
+
}
|
|
27
|
+
// Handle objects with a code property (like Node.js errors)
|
|
28
|
+
if (error && typeof error === 'object' && 'code' in error) {
|
|
29
|
+
const errorObj = error;
|
|
30
|
+
if (errorObj.message) {
|
|
31
|
+
return errorObj.message;
|
|
32
|
+
}
|
|
33
|
+
if (errorObj.code) {
|
|
34
|
+
return `Error: ${errorObj.code}`;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// Handle objects with a custom toString
|
|
38
|
+
if (error && typeof error === 'object' && 'toString' in error) {
|
|
39
|
+
const str = String(error);
|
|
40
|
+
// Avoid returning '[object Object]'
|
|
41
|
+
if (str !== '[object Object]') {
|
|
42
|
+
return str;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Try to extract any useful information from the object
|
|
46
|
+
if (error && typeof error === 'object') {
|
|
47
|
+
try {
|
|
48
|
+
const json = JSON.stringify(error);
|
|
49
|
+
if (json && json !== '{}') {
|
|
50
|
+
return `Error: ${json}`;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Ignore circular reference errors
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return 'An unknown error occurred';
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Safely extract an error code from a Node.js error
|
|
61
|
+
*/
|
|
62
|
+
export function getErrorCode(error) {
|
|
63
|
+
// Check if it's a proper Node.js error with a code
|
|
64
|
+
if (isNodeError(error)) {
|
|
65
|
+
return error.code;
|
|
66
|
+
}
|
|
67
|
+
// Also handle plain objects with a code property (like mocked errors in tests)
|
|
68
|
+
return error && typeof error === 'object' && 'code' in error ?
|
|
69
|
+
error.code :
|
|
70
|
+
undefined;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Convert an unknown error to a proper Error object
|
|
74
|
+
*/
|
|
75
|
+
export function toError(error) {
|
|
76
|
+
if (isError(error)) {
|
|
77
|
+
return error;
|
|
78
|
+
}
|
|
79
|
+
return new Error(getErrorMessage(error));
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Create a formatted error message with optional context
|
|
83
|
+
*/
|
|
84
|
+
export function formatError(error, context) {
|
|
85
|
+
const message = getErrorMessage(error);
|
|
86
|
+
return context ? `${context}: ${message}` : message;
|
|
87
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { FolderAlreadyExistsError } from '#types/errors.js';
|
|
3
|
+
import { getErrorCode, getErrorMessage } from './error_utils.js';
|
|
4
|
+
export function directoryExists(dirPath) {
|
|
5
|
+
try {
|
|
6
|
+
fs.accessSync(dirPath);
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
if (getErrorCode(error) === 'ENOENT') {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
throw error;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function createDirectory(dirPath) {
|
|
17
|
+
const exists = directoryExists(dirPath);
|
|
18
|
+
if (exists) {
|
|
19
|
+
throw new FolderAlreadyExistsError(dirPath);
|
|
20
|
+
}
|
|
21
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
export function removeDirectory(dirPath, onError) {
|
|
24
|
+
try {
|
|
25
|
+
fs.rmSync(dirPath, { recursive: true, force: true });
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
const message = getErrorMessage(error);
|
|
29
|
+
if (onError) {
|
|
30
|
+
onError(`Failed to cleanup folder: ${message}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|