@simplens/onboard 1.0.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/README.md +214 -0
- package/dist/__tests__/errors.test.d.ts +2 -0
- package/dist/__tests__/errors.test.d.ts.map +1 -0
- package/dist/__tests__/errors.test.js +125 -0
- package/dist/__tests__/errors.test.js.map +1 -0
- package/dist/__tests__/utils.test.d.ts +2 -0
- package/dist/__tests__/utils.test.d.ts.map +1 -0
- package/dist/__tests__/utils.test.js +105 -0
- package/dist/__tests__/utils.test.js.map +1 -0
- package/dist/__tests__/validators.test.d.ts +2 -0
- package/dist/__tests__/validators.test.d.ts.map +1 -0
- package/dist/__tests__/validators.test.js +148 -0
- package/dist/__tests__/validators.test.js.map +1 -0
- package/dist/config/constants.d.ts +69 -0
- package/dist/config/constants.d.ts.map +1 -0
- package/dist/config/constants.js +79 -0
- package/dist/config/constants.js.map +1 -0
- package/dist/config/index.d.ts +2 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +2 -0
- package/dist/config/index.js.map +1 -0
- package/dist/env-config.d.ts +33 -0
- package/dist/env-config.d.ts.map +1 -0
- package/dist/env-config.js +285 -0
- package/dist/env-config.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +153 -0
- package/dist/index.js.map +1 -0
- package/dist/infra.d.ts +31 -0
- package/dist/infra.d.ts.map +1 -0
- package/dist/infra.js +267 -0
- package/dist/infra.js.map +1 -0
- package/dist/plugins.d.ts +35 -0
- package/dist/plugins.d.ts.map +1 -0
- package/dist/plugins.js +164 -0
- package/dist/plugins.js.map +1 -0
- package/dist/services.d.ts +52 -0
- package/dist/services.d.ts.map +1 -0
- package/dist/services.js +158 -0
- package/dist/services.js.map +1 -0
- package/dist/templates.d.ts +3 -0
- package/dist/templates.d.ts.map +1 -0
- package/dist/templates.js +202 -0
- package/dist/templates.js.map +1 -0
- package/dist/types/domain.d.ts +119 -0
- package/dist/types/domain.d.ts.map +1 -0
- package/dist/types/domain.js +5 -0
- package/dist/types/domain.js.map +1 -0
- package/dist/types/errors.d.ts +69 -0
- package/dist/types/errors.d.ts.map +1 -0
- package/dist/types/errors.js +129 -0
- package/dist/types/errors.js.map +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +2 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/logger.d.ts +54 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +92 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils.d.ts +32 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +79 -0
- package/dist/utils.js.map +1 -0
- package/dist/validators.d.ts +93 -0
- package/dist/validators.d.ts.map +1 -0
- package/dist/validators.js +180 -0
- package/dist/validators.js.map +1 -0
- package/package.json +45 -0
- package/src/__tests__/errors.test.ts +187 -0
- package/src/__tests__/utils.test.ts +142 -0
- package/src/__tests__/validators.test.ts +195 -0
- package/src/config/constants.ts +86 -0
- package/src/config/index.ts +1 -0
- package/src/env-config.ts +320 -0
- package/src/index.ts +203 -0
- package/src/infra.ts +300 -0
- package/src/plugins.ts +190 -0
- package/src/services.ts +190 -0
- package/src/templates.ts +203 -0
- package/src/types/domain.ts +127 -0
- package/src/types/errors.ts +173 -0
- package/src/types/index.ts +2 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/logger.ts +118 -0
- package/src/utils.ts +105 -0
- package/src/validators.ts +192 -0
- package/tsconfig.json +19 -0
- package/vitest.config.ts +20 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import { logSuccess, logWarning } from './utils.js';
|
|
3
|
+
import { DockerNotInstalledError, DockerNotRunningError, DockerPermissionError, } from './types/errors.js';
|
|
4
|
+
import { VALIDATION } from './config/constants.js';
|
|
5
|
+
/**
|
|
6
|
+
* Checks if Docker is installed on the system by running `docker --version`.
|
|
7
|
+
*
|
|
8
|
+
* @throws {DockerNotInstalledError} When Docker is not found in PATH or not installed
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* try {
|
|
13
|
+
* await checkDockerInstalled();
|
|
14
|
+
* console.log('Docker is available');
|
|
15
|
+
* } catch (error) {
|
|
16
|
+
* // Handle DockerNotInstalledError
|
|
17
|
+
* }
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export async function checkDockerInstalled() {
|
|
21
|
+
try {
|
|
22
|
+
await execa('docker', ['--version']);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
throw new DockerNotInstalledError();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Checks if the Docker daemon is running by executing `docker ps`.
|
|
30
|
+
* Provides specific error types based on the failure reason.
|
|
31
|
+
*
|
|
32
|
+
* @throws {DockerPermissionError} When user lacks permissions to access Docker socket
|
|
33
|
+
* @throws {DockerNotRunningError} When Docker daemon is not running or unreachable
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* try {
|
|
38
|
+
* await checkDockerRunning();
|
|
39
|
+
* } catch (error) {
|
|
40
|
+
* if (error instanceof DockerPermissionError) {
|
|
41
|
+
* // Guide user to add sudo or docker group
|
|
42
|
+
* }
|
|
43
|
+
* }
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export async function checkDockerRunning() {
|
|
47
|
+
try {
|
|
48
|
+
await execa('docker', ['ps']);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
const errorMessage = error.message?.toLowerCase() || '';
|
|
52
|
+
if (errorMessage.includes('permission denied') || errorMessage.includes('eacces')) {
|
|
53
|
+
throw new DockerPermissionError();
|
|
54
|
+
}
|
|
55
|
+
if (errorMessage.includes('cannot connect') || errorMessage.includes('is the docker daemon running')) {
|
|
56
|
+
throw new DockerNotRunningError();
|
|
57
|
+
}
|
|
58
|
+
// Generic docker error
|
|
59
|
+
throw new DockerNotRunningError();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Detects the operating system platform.
|
|
64
|
+
*
|
|
65
|
+
* @returns OS type: 'windows', 'darwin' (macOS), or 'linux'
|
|
66
|
+
* @note Defaults to 'linux' for unknown platforms
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```ts
|
|
70
|
+
* const os = detectOS();
|
|
71
|
+
* if (os === 'linux') {
|
|
72
|
+
* // Linux-specific configuration
|
|
73
|
+
* }
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export function detectOS() {
|
|
77
|
+
const platform = process.platform;
|
|
78
|
+
if (platform === 'win32')
|
|
79
|
+
return 'windows';
|
|
80
|
+
if (platform === 'linux')
|
|
81
|
+
return 'linux';
|
|
82
|
+
if (platform === 'darwin')
|
|
83
|
+
return 'darwin';
|
|
84
|
+
return 'linux'; // Default fallback
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Validates all system prerequisites before starting the onboarding process.
|
|
88
|
+
* Checks Docker installation, daemon status, and detects the operating system.
|
|
89
|
+
*
|
|
90
|
+
* @throws {DockerNotInstalledError} If Docker is not installed
|
|
91
|
+
* @throws {DockerNotRunningError} If Docker daemon is not running
|
|
92
|
+
* @throws {DockerPermissionError} If user lacks Docker permissions
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```ts
|
|
96
|
+
* try {
|
|
97
|
+
* await validatePrerequisites();
|
|
98
|
+
* // Proceed with setup
|
|
99
|
+
* } catch (error) {
|
|
100
|
+
* // Handle prerequisite errors
|
|
101
|
+
* }
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
export async function validatePrerequisites() {
|
|
105
|
+
console.log('\nπ Checking prerequisites...\n');
|
|
106
|
+
// Check Docker installation
|
|
107
|
+
try {
|
|
108
|
+
await checkDockerInstalled();
|
|
109
|
+
logSuccess('Docker is installed');
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
// Error will be caught by main() and displayed with troubleshooting
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
// Check Docker daemon
|
|
116
|
+
try {
|
|
117
|
+
await checkDockerRunning();
|
|
118
|
+
logSuccess('Docker daemon is running');
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
// Error will be caught by main() and displayed with troubleshooting
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
// Detect OS
|
|
125
|
+
const os = detectOS();
|
|
126
|
+
logSuccess(`Detected OS: ${os}`);
|
|
127
|
+
if (os === 'linux') {
|
|
128
|
+
logWarning('Linux detected: You may need to provide your machine IP for infra services.');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Validates environment variable values based on the variable name/type.
|
|
133
|
+
* Performs format-specific validation for URLs, ports, and security credentials.
|
|
134
|
+
*
|
|
135
|
+
* @param key - Environment variable name (e.g., 'MONGO_URI', 'PORT', 'API_KEY')
|
|
136
|
+
* @param value - Value to validate
|
|
137
|
+
* @returns `true` if valid, `false` otherwise
|
|
138
|
+
*
|
|
139
|
+
* @remarks
|
|
140
|
+
* Validation rules:
|
|
141
|
+
* - URLs: Must contain protocol (mongodb://, redis://)
|
|
142
|
+
* - Ports: Must be 1-65535
|
|
143
|
+
* - Secrets/Keys/Passwords: Must be at least 8 characters
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* ```ts
|
|
147
|
+
* validateEnvValue('MONGO_URI', 'mongodb://localhost:27017'); // true
|
|
148
|
+
* validateEnvValue('PORT', '3000'); // true
|
|
149
|
+
* validateEnvValue('API_KEY', 'short'); // false (too short)
|
|
150
|
+
* ```
|
|
151
|
+
*/
|
|
152
|
+
export function validateEnvValue(key, value) {
|
|
153
|
+
// URL validation
|
|
154
|
+
if (key.includes('URL') || key.includes('URI')) {
|
|
155
|
+
if (!value)
|
|
156
|
+
return false;
|
|
157
|
+
// Basic URL format check
|
|
158
|
+
if (key === 'MONGO_URI' && !value.includes('mongodb://')) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
if (key === 'REDIS_URL' && !value.includes('redis://')) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// Port validation
|
|
166
|
+
if (key.includes('PORT')) {
|
|
167
|
+
const port = parseInt(value, 10);
|
|
168
|
+
if (isNaN(port) || port < VALIDATION.PORT_MIN || port > VALIDATION.PORT_MAX) {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// API Key validation (should not be empty for security)
|
|
173
|
+
if (key.includes('API_KEY') || key.includes('SECRET') || key.includes('PASSWORD')) {
|
|
174
|
+
if (!value || value.length < VALIDATION.MIN_PASSWORD_LENGTH) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
//# sourceMappingURL=validators.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validators.js","sourceRoot":"","sources":["../src/validators.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@simplens/onboard",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"simplens-onboard": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"dev": "tsc --watch",
|
|
12
|
+
"prepublishOnly": "npm run build",
|
|
13
|
+
"test": "vitest",
|
|
14
|
+
"test:ui": "vitest --ui",
|
|
15
|
+
"test:coverage": "vitest --coverage"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"simplens",
|
|
19
|
+
"notification",
|
|
20
|
+
"cli",
|
|
21
|
+
"onboard",
|
|
22
|
+
"setup"
|
|
23
|
+
],
|
|
24
|
+
"author": "SimpleNS Team",
|
|
25
|
+
"license": "ISC",
|
|
26
|
+
"description": "CLI tool to setup SimpleNS instance on any AMD64 machine",
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/figlet": "^1.7.0",
|
|
29
|
+
"@types/inquirer": "^9.0.7",
|
|
30
|
+
"@types/js-yaml": "^4.0.9",
|
|
31
|
+
"@types/node": "^20.19.30",
|
|
32
|
+
"@vitest/ui": "^4.0.18",
|
|
33
|
+
"typescript": "^5.9.3",
|
|
34
|
+
"vitest": "^4.0.18"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"chalk": "^5.3.0",
|
|
38
|
+
"commander": "^14.0.2",
|
|
39
|
+
"execa": "^8.0.1",
|
|
40
|
+
"figlet": "^1.10.0",
|
|
41
|
+
"inquirer": "^9.2.12",
|
|
42
|
+
"js-yaml": "^4.1.0",
|
|
43
|
+
"ora": "^7.0.1"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
OnboardingError,
|
|
4
|
+
DockerNotInstalledError,
|
|
5
|
+
DockerNotRunningError,
|
|
6
|
+
DockerPermissionError,
|
|
7
|
+
DockerComposeError,
|
|
8
|
+
FileSystemError,
|
|
9
|
+
DirectoryNotWritableError,
|
|
10
|
+
isOnboardingError,
|
|
11
|
+
formatErrorForUser,
|
|
12
|
+
} from '../types/errors.js';
|
|
13
|
+
|
|
14
|
+
describe('error types', () => {
|
|
15
|
+
describe('OnboardingError', () => {
|
|
16
|
+
it('should create error with code and message', () => {
|
|
17
|
+
const error = new OnboardingError('TEST_CODE', 'Test message');
|
|
18
|
+
|
|
19
|
+
expect(error.code).toBe('TEST_CODE');
|
|
20
|
+
expect(error.message).toBe('Test message');
|
|
21
|
+
expect(error.name).toBe('OnboardingError');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should include troubleshooting when provided', () => {
|
|
25
|
+
const error = new OnboardingError(
|
|
26
|
+
'TEST_CODE',
|
|
27
|
+
'Test message',
|
|
28
|
+
'Try this fix'
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
expect(error.troubleshooting).toBe('Try this fix');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should have proper stack trace', () => {
|
|
35
|
+
const error = new OnboardingError('TEST', 'message');
|
|
36
|
+
|
|
37
|
+
expect(error.stack).toBeDefined();
|
|
38
|
+
expect(error.stack).toContain('OnboardingError');
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('DockerNotInstalledError', () => {
|
|
43
|
+
it('should have correct message and troubleshooting', () => {
|
|
44
|
+
const error = new DockerNotInstalledError();
|
|
45
|
+
|
|
46
|
+
expect(error.message).toContain('not installed');
|
|
47
|
+
expect(error.troubleshooting).toContain('https://docs.docker.com/get-docker/');
|
|
48
|
+
expect(error.name).toBe('DockerNotInstalledError');
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('DockerNotRunningError', () => {
|
|
53
|
+
it('should have correct message and troubleshooting', () => {
|
|
54
|
+
const error = new DockerNotRunningError();
|
|
55
|
+
|
|
56
|
+
expect(error.message).toContain('not running');
|
|
57
|
+
expect(error.troubleshooting).toContain('Docker daemon');
|
|
58
|
+
expect(error.name).toBe('DockerNotRunningError');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('DockerPermissionError', () => {
|
|
63
|
+
it('should have correct message and troubleshooting', () => {
|
|
64
|
+
const error = new DockerPermissionError();
|
|
65
|
+
|
|
66
|
+
expect(error.message).toContain('Permission denied');
|
|
67
|
+
expect(error.troubleshooting).toContain('usermod');
|
|
68
|
+
expect(error.name).toBe('DockerPermissionError');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('DockerComposeError', () => {
|
|
73
|
+
it('should include operation in message', () => {
|
|
74
|
+
const error = new DockerComposeError('start services');
|
|
75
|
+
|
|
76
|
+
expect(error.message).toContain('start services');
|
|
77
|
+
expect(error.name).toBe('DockerComposeError');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should use custom troubleshooting when provided', () => {
|
|
81
|
+
const error = new DockerComposeError('pull images', 'Check network connection');
|
|
82
|
+
|
|
83
|
+
expect(error.troubleshooting).toBe('Check network connection');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should have default troubleshooting', () => {
|
|
87
|
+
const error = new DockerComposeError('operation');
|
|
88
|
+
|
|
89
|
+
expect(error.troubleshooting).toContain('docker-compose logs');
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('FileSystemError', () => {
|
|
94
|
+
it('should include path in error', () => {
|
|
95
|
+
const error = new FileSystemError(
|
|
96
|
+
'Cannot write',
|
|
97
|
+
'/path/to/file',
|
|
98
|
+
'Check permissions'
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
expect(error.path).toBe('/path/to/file');
|
|
102
|
+
expect(error.troubleshooting).toBe('Check permissions');
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('DirectoryNotWritableError', () => {
|
|
107
|
+
it('should include path in message and error', () => {
|
|
108
|
+
const error = new DirectoryNotWritableError('/opt/app');
|
|
109
|
+
|
|
110
|
+
expect(error.message).toContain('/opt/app');
|
|
111
|
+
expect(error.path).toBe('/opt/app');
|
|
112
|
+
expect(error.troubleshooting).toContain('permissions');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('isOnboardingError', () => {
|
|
117
|
+
it('should return true for OnboardingError instances', () => {
|
|
118
|
+
const error = new OnboardingError('CODE', 'message');
|
|
119
|
+
|
|
120
|
+
expect(isOnboardingError(error)).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should return true for subclasses of OnboardingError', () => {
|
|
124
|
+
const error = new DockerNotInstalledError();
|
|
125
|
+
|
|
126
|
+
expect(isOnboardingError(error)).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should return false for regular Error', () => {
|
|
130
|
+
const error = new Error('regular error');
|
|
131
|
+
|
|
132
|
+
expect(isOnboardingError(error)).toBe(false);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should return false for non-error values', () => {
|
|
136
|
+
expect(isOnboardingError('string')).toBe(false);
|
|
137
|
+
expect(isOnboardingError(null)).toBe(false);
|
|
138
|
+
expect(isOnboardingError(undefined)).toBe(false);
|
|
139
|
+
expect(isOnboardingError({})).toBe(false);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe('formatErrorForUser', () => {
|
|
144
|
+
it('should format OnboardingError with troubleshooting', () => {
|
|
145
|
+
const error = new OnboardingError(
|
|
146
|
+
'TEST',
|
|
147
|
+
'Something went wrong',
|
|
148
|
+
'Try this fix'
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const formatted = formatErrorForUser(error);
|
|
152
|
+
|
|
153
|
+
expect(formatted).toContain('β Something went wrong');
|
|
154
|
+
expect(formatted).toContain('π‘ Troubleshooting:');
|
|
155
|
+
expect(formatted).toContain('Try this fix');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should format OnboardingError without troubleshooting', () => {
|
|
159
|
+
const error = new OnboardingError('TEST', 'Something went wrong');
|
|
160
|
+
|
|
161
|
+
const formatted = formatErrorForUser(error);
|
|
162
|
+
|
|
163
|
+
expect(formatted).toBe('β Something went wrong');
|
|
164
|
+
expect(formatted).not.toContain('Troubleshooting');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should format regular Error', () => {
|
|
168
|
+
const error = new Error('Regular error message');
|
|
169
|
+
|
|
170
|
+
const formatted = formatErrorForUser(error);
|
|
171
|
+
|
|
172
|
+
expect(formatted).toContain('Unexpected error');
|
|
173
|
+
expect(formatted).toContain('Regular error message');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('should handle non-Error values', () => {
|
|
177
|
+
const formatted = formatErrorForUser('some string');
|
|
178
|
+
|
|
179
|
+
expect(formatted).toBe('β An unknown error occurred');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should handle null/undefined', () => {
|
|
183
|
+
expect(formatErrorForUser(null)).toBe('β An unknown error occurred');
|
|
184
|
+
expect(formatErrorForUser(undefined)).toBe('β An unknown error occurred');
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
});
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import {
|
|
6
|
+
fileExists,
|
|
7
|
+
writeFile,
|
|
8
|
+
readFile,
|
|
9
|
+
appendFile,
|
|
10
|
+
} from '../utils.js';
|
|
11
|
+
|
|
12
|
+
describe('utils - file operations', () => {
|
|
13
|
+
let tempDir: string;
|
|
14
|
+
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
// Create a temporary directory for tests
|
|
17
|
+
tempDir = path.join(os.tmpdir(), `onboard-test-${Date.now()}`);
|
|
18
|
+
await fs.mkdir(tempDir, { recursive: true });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(async () => {
|
|
22
|
+
// Clean up temporary directory
|
|
23
|
+
try {
|
|
24
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
25
|
+
} catch (error) {
|
|
26
|
+
// Ignore cleanup errors
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('fileExists', () => {
|
|
31
|
+
it('should return true for existing file', async () => {
|
|
32
|
+
const filePath = path.join(tempDir, 'test.txt');
|
|
33
|
+
await fs.writeFile(filePath, 'content');
|
|
34
|
+
|
|
35
|
+
expect(await fileExists(filePath)).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should return false for non-existing file', async () => {
|
|
39
|
+
const filePath = path.join(tempDir, 'nonexistent.txt');
|
|
40
|
+
|
|
41
|
+
expect(await fileExists(filePath)).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should return true for existing directory', async () => {
|
|
45
|
+
expect(await fileExists(tempDir)).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('writeFile', () => {
|
|
50
|
+
it('should write content to file', async () => {
|
|
51
|
+
const filePath = path.join(tempDir, 'write-test.txt');
|
|
52
|
+
const content = 'Hello, World!';
|
|
53
|
+
|
|
54
|
+
await writeFile(filePath, content);
|
|
55
|
+
|
|
56
|
+
const written = await fs.readFile(filePath, 'utf-8');
|
|
57
|
+
expect(written).toBe(content);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should create parent directories if they do not exist', async () => {
|
|
61
|
+
const filePath = path.join(tempDir, 'nested', 'dir', 'file.txt');
|
|
62
|
+
const content = 'Nested content';
|
|
63
|
+
|
|
64
|
+
await writeFile(filePath, content);
|
|
65
|
+
|
|
66
|
+
expect(await fileExists(filePath)).toBe(true);
|
|
67
|
+
const written = await fs.readFile(filePath, 'utf-8');
|
|
68
|
+
expect(written).toBe(content);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should overwrite existing file', async () => {
|
|
72
|
+
const filePath = path.join(tempDir, 'overwrite.txt');
|
|
73
|
+
|
|
74
|
+
await fs.writeFile(filePath, 'original');
|
|
75
|
+
await writeFile(filePath, 'new content');
|
|
76
|
+
|
|
77
|
+
const written = await fs.readFile(filePath, 'utf-8');
|
|
78
|
+
expect(written).toBe('new content');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('readFile', () => {
|
|
83
|
+
it('should read file content', async () => {
|
|
84
|
+
const filePath = path.join(tempDir, 'read-test.txt');
|
|
85
|
+
const content = 'File content to read';
|
|
86
|
+
await fs.writeFile(filePath, content);
|
|
87
|
+
|
|
88
|
+
const read = await readFile(filePath);
|
|
89
|
+
|
|
90
|
+
expect(read).toBe(content);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should throw error for non-existing file', async () => {
|
|
94
|
+
const filePath = path.join(tempDir, 'nonexistent.txt');
|
|
95
|
+
|
|
96
|
+
await expect(readFile(filePath)).rejects.toThrow();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should handle UTF-8 content', async () => {
|
|
100
|
+
const filePath = path.join(tempDir, 'utf8.txt');
|
|
101
|
+
const content = 'Hello δΈη π';
|
|
102
|
+
await fs.writeFile(filePath, content);
|
|
103
|
+
|
|
104
|
+
const read = await readFile(filePath);
|
|
105
|
+
|
|
106
|
+
expect(read).toBe(content);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('appendFile', () => {
|
|
111
|
+
it('should append content to existing file', async () => {
|
|
112
|
+
const filePath = path.join(tempDir, 'append-test.txt');
|
|
113
|
+
await fs.writeFile(filePath, 'Line 1\n');
|
|
114
|
+
|
|
115
|
+
await appendFile(filePath, 'Line 2\n');
|
|
116
|
+
|
|
117
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
118
|
+
expect(content).toBe('Line 1\nLine 2\n');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should create file if it does not exist', async () => {
|
|
122
|
+
const filePath = path.join(tempDir, 'new-append.txt');
|
|
123
|
+
|
|
124
|
+
await appendFile(filePath, 'First line\n');
|
|
125
|
+
|
|
126
|
+
expect(await fileExists(filePath)).toBe(true);
|
|
127
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
128
|
+
expect(content).toBe('First line\n');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should handle multiple appends', async () => {
|
|
132
|
+
const filePath = path.join(tempDir, 'multi-append.txt');
|
|
133
|
+
|
|
134
|
+
await appendFile(filePath, 'Line 1\n');
|
|
135
|
+
await appendFile(filePath, 'Line 2\n');
|
|
136
|
+
await appendFile(filePath, 'Line 3\n');
|
|
137
|
+
|
|
138
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
139
|
+
expect(content).toBe('Line 1\nLine 2\nLine 3\n');
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
});
|