@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.
Files changed (94) hide show
  1. package/README.md +214 -0
  2. package/dist/__tests__/errors.test.d.ts +2 -0
  3. package/dist/__tests__/errors.test.d.ts.map +1 -0
  4. package/dist/__tests__/errors.test.js +125 -0
  5. package/dist/__tests__/errors.test.js.map +1 -0
  6. package/dist/__tests__/utils.test.d.ts +2 -0
  7. package/dist/__tests__/utils.test.d.ts.map +1 -0
  8. package/dist/__tests__/utils.test.js +105 -0
  9. package/dist/__tests__/utils.test.js.map +1 -0
  10. package/dist/__tests__/validators.test.d.ts +2 -0
  11. package/dist/__tests__/validators.test.d.ts.map +1 -0
  12. package/dist/__tests__/validators.test.js +148 -0
  13. package/dist/__tests__/validators.test.js.map +1 -0
  14. package/dist/config/constants.d.ts +69 -0
  15. package/dist/config/constants.d.ts.map +1 -0
  16. package/dist/config/constants.js +79 -0
  17. package/dist/config/constants.js.map +1 -0
  18. package/dist/config/index.d.ts +2 -0
  19. package/dist/config/index.d.ts.map +1 -0
  20. package/dist/config/index.js +2 -0
  21. package/dist/config/index.js.map +1 -0
  22. package/dist/env-config.d.ts +33 -0
  23. package/dist/env-config.d.ts.map +1 -0
  24. package/dist/env-config.js +285 -0
  25. package/dist/env-config.js.map +1 -0
  26. package/dist/index.d.ts +3 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +153 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/infra.d.ts +31 -0
  31. package/dist/infra.d.ts.map +1 -0
  32. package/dist/infra.js +267 -0
  33. package/dist/infra.js.map +1 -0
  34. package/dist/plugins.d.ts +35 -0
  35. package/dist/plugins.d.ts.map +1 -0
  36. package/dist/plugins.js +164 -0
  37. package/dist/plugins.js.map +1 -0
  38. package/dist/services.d.ts +52 -0
  39. package/dist/services.d.ts.map +1 -0
  40. package/dist/services.js +158 -0
  41. package/dist/services.js.map +1 -0
  42. package/dist/templates.d.ts +3 -0
  43. package/dist/templates.d.ts.map +1 -0
  44. package/dist/templates.js +202 -0
  45. package/dist/templates.js.map +1 -0
  46. package/dist/types/domain.d.ts +119 -0
  47. package/dist/types/domain.d.ts.map +1 -0
  48. package/dist/types/domain.js +5 -0
  49. package/dist/types/domain.js.map +1 -0
  50. package/dist/types/errors.d.ts +69 -0
  51. package/dist/types/errors.d.ts.map +1 -0
  52. package/dist/types/errors.js +129 -0
  53. package/dist/types/errors.js.map +1 -0
  54. package/dist/types/index.d.ts +3 -0
  55. package/dist/types/index.d.ts.map +1 -0
  56. package/dist/types/index.js +3 -0
  57. package/dist/types/index.js.map +1 -0
  58. package/dist/utils/index.d.ts +2 -0
  59. package/dist/utils/index.d.ts.map +1 -0
  60. package/dist/utils/index.js +2 -0
  61. package/dist/utils/index.js.map +1 -0
  62. package/dist/utils/logger.d.ts +54 -0
  63. package/dist/utils/logger.d.ts.map +1 -0
  64. package/dist/utils/logger.js +92 -0
  65. package/dist/utils/logger.js.map +1 -0
  66. package/dist/utils.d.ts +32 -0
  67. package/dist/utils.d.ts.map +1 -0
  68. package/dist/utils.js +79 -0
  69. package/dist/utils.js.map +1 -0
  70. package/dist/validators.d.ts +93 -0
  71. package/dist/validators.d.ts.map +1 -0
  72. package/dist/validators.js +180 -0
  73. package/dist/validators.js.map +1 -0
  74. package/package.json +45 -0
  75. package/src/__tests__/errors.test.ts +187 -0
  76. package/src/__tests__/utils.test.ts +142 -0
  77. package/src/__tests__/validators.test.ts +195 -0
  78. package/src/config/constants.ts +86 -0
  79. package/src/config/index.ts +1 -0
  80. package/src/env-config.ts +320 -0
  81. package/src/index.ts +203 -0
  82. package/src/infra.ts +300 -0
  83. package/src/plugins.ts +190 -0
  84. package/src/services.ts +190 -0
  85. package/src/templates.ts +203 -0
  86. package/src/types/domain.ts +127 -0
  87. package/src/types/errors.ts +173 -0
  88. package/src/types/index.ts +2 -0
  89. package/src/utils/index.ts +1 -0
  90. package/src/utils/logger.ts +118 -0
  91. package/src/utils.ts +105 -0
  92. package/src/validators.ts +192 -0
  93. package/tsconfig.json +19 -0
  94. package/vitest.config.ts +20 -0
@@ -0,0 +1,118 @@
1
+ import chalk from 'chalk';
2
+ import { appendFile as fsAppendFile } from 'fs/promises';
3
+ import path from 'path';
4
+ import { FILES } from '../config/constants.js';
5
+
6
+ /**
7
+ * Logging level
8
+ */
9
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
10
+
11
+ /**
12
+ * Logger configuration
13
+ */
14
+ interface LoggerConfig {
15
+ verbose: boolean;
16
+ debug: boolean;
17
+ logFile?: string;
18
+ }
19
+
20
+ let loggerConfig: LoggerConfig = {
21
+ verbose: false,
22
+ debug: false,
23
+ };
24
+
25
+ /**
26
+ * Initialize logger with configuration
27
+ */
28
+ export function initLogger(config: Partial<LoggerConfig>): void {
29
+ loggerConfig = { ...loggerConfig, ...config };
30
+ }
31
+
32
+ /**
33
+ * Get current logger configuration
34
+ */
35
+ export function getLoggerConfig(): Readonly<LoggerConfig> {
36
+ return { ...loggerConfig };
37
+ }
38
+
39
+ /**
40
+ * Write log message to file if configured
41
+ */
42
+ async function writeToLogFile(level: LogLevel, message: string): Promise<void> {
43
+ if (!loggerConfig.logFile) return;
44
+
45
+ try {
46
+ const timestamp = new Date().toISOString();
47
+ const logEntry = `[${timestamp}] [${level.toUpperCase()}] ${message}\n`;
48
+ await fsAppendFile(loggerConfig.logFile, logEntry, 'utf-8');
49
+ } catch (error) {
50
+ // Silently fail on log file errors
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Log debug message (only shown with --debug flag)
56
+ */
57
+ export function logDebug(message: string): void {
58
+ if (loggerConfig.debug) {
59
+ console.log(chalk.gray(`🔧 ${message}`));
60
+ }
61
+ writeToLogFile('debug', message);
62
+ }
63
+
64
+ /**
65
+ * Log verbose message (shown with --verbose or --debug)
66
+ */
67
+ export function logVerbose(message: string): void {
68
+ if (loggerConfig.verbose || loggerConfig.debug) {
69
+ console.log(chalk.cyan(`ℹ️ ${message}`));
70
+ }
71
+ writeToLogFile('info', message);
72
+ }
73
+
74
+ /**
75
+ * Log info message (always displayed)
76
+ */
77
+ export function logInfo(message: string): void {
78
+ console.log(chalk.blue(`ℹ️ ${message}`));
79
+ writeToLogFile('info', message);
80
+ }
81
+
82
+ /**
83
+ * Log success message (always displayed)
84
+ */
85
+ export function logSuccess(message: string): void {
86
+ console.log(chalk.green(`✅ ${message}`));
87
+ writeToLogFile('info', message);
88
+ }
89
+
90
+ /**
91
+ * Log warning message (always displayed)
92
+ */
93
+ export function logWarning(message: string): void {
94
+ console.log(chalk.yellow(`⚠️ ${message}`));
95
+ writeToLogFile('warn', message);
96
+ }
97
+
98
+ /**
99
+ * Log error message (always displayed)
100
+ */
101
+ export function logError(message: string): void {
102
+ console.log(chalk.red(`❌ ${message}`));
103
+ writeToLogFile('error', message);
104
+ }
105
+
106
+ /**
107
+ * Log command execution (debug level)
108
+ */
109
+ export function logCommand(command: string, args: string[]): void {
110
+ logDebug(`Executing: ${command} ${args.join(' ')}`);
111
+ }
112
+
113
+ /**
114
+ * Log file operation (debug level)
115
+ */
116
+ export function logFileOperation(operation: string, filePath: string): void {
117
+ logDebug(`${operation}: ${filePath}`);
118
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,105 @@
1
+ import chalk from 'chalk';
2
+ import { execa } from 'execa';
3
+ import fs from 'fs/promises';
4
+ import path from 'path';
5
+ import figlet from 'figlet';
6
+
7
+ // Re-export logger functions for backward compatibility
8
+ export {
9
+ logSuccess,
10
+ logError,
11
+ logWarning,
12
+ logInfo,
13
+ logDebug,
14
+ logVerbose,
15
+ logCommand,
16
+ logFileOperation,
17
+ initLogger,
18
+ getLoggerConfig,
19
+ } from './utils/logger.js';
20
+
21
+ /**
22
+ * Display SimpleNS banner in blue using figlet
23
+ */
24
+ export function displayBanner(): void {
25
+ const simpleNS = figlet.textSync('SimpleNS', {
26
+ font: 'Standard',
27
+ horizontalLayout: 'default',
28
+ });
29
+
30
+ const onboard = figlet.textSync('Onboard', {
31
+ font: 'Standard',
32
+ horizontalLayout: 'default',
33
+ });
34
+
35
+ console.log(chalk.blue(simpleNS));
36
+ console.log(chalk.blue(onboard));
37
+ console.log('');
38
+ }
39
+
40
+ /**
41
+ * Execute a shell command with error handling
42
+ */
43
+ export async function executeCommand(
44
+ command: string,
45
+ args: string[],
46
+ options?: { cwd?: string; silent?: boolean }
47
+ ): Promise<{ stdout: string; stderr: string }> {
48
+ // Import dynamically to avoid circular dependency
49
+ const { logCommand } = await import('./utils/logger.js');
50
+
51
+ logCommand(command, args);
52
+
53
+ try {
54
+ const result = await execa(command, args, {
55
+ cwd: options?.cwd || process.cwd(),
56
+ stdio: options?.silent ? 'pipe' : 'inherit',
57
+ });
58
+ return { stdout: result.stdout, stderr: result.stderr };
59
+ } catch (error: any) {
60
+ throw new Error(`Command failed: ${command} ${args.join(' ')}\n${error.message}`);
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Check if file exists
66
+ */
67
+ export async function fileExists(filePath: string): Promise<boolean> {
68
+ try {
69
+ await fs.access(filePath);
70
+ return true;
71
+ } catch {
72
+ return false;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Write file content
78
+ */
79
+ export async function writeFile(filePath: string, content: string): Promise<void> {
80
+ const { logFileOperation } = await import('./utils/logger.js');
81
+
82
+ logFileOperation('Writing file', filePath);
83
+
84
+ const dir = path.dirname(filePath);
85
+ await fs.mkdir(dir, { recursive: true });
86
+ await fs.writeFile(filePath, content, 'utf-8');
87
+ }
88
+
89
+ /**
90
+ * Read file content
91
+ */
92
+ export async function readFile(filePath: string): Promise<string> {
93
+ return await fs.readFile(filePath, 'utf-8');
94
+ }
95
+
96
+ /**
97
+ * Append content to file
98
+ */
99
+ export async function appendFile(filePath: string, content: string): Promise<void> {
100
+ // Ensure parent directory exists, similar to writeFile
101
+ const dir = path.dirname(filePath);
102
+ await fs.mkdir(dir, { recursive: true });
103
+
104
+ await fs.appendFile(filePath, content, 'utf-8');
105
+ }
@@ -0,0 +1,192 @@
1
+ import { execa } from 'execa';
2
+ import { logError, logSuccess, logWarning } from './utils.js';
3
+ import {
4
+ DockerNotInstalledError,
5
+ DockerNotRunningError,
6
+ DockerPermissionError,
7
+ } from './types/errors.js';
8
+ import { VALIDATION } from './config/constants.js';
9
+
10
+ export type OSType = 'windows' | 'linux' | 'darwin';
11
+
12
+ /**
13
+ * Checks if Docker is installed on the system by running `docker --version`.
14
+ *
15
+ * @throws {DockerNotInstalledError} When Docker is not found in PATH or not installed
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * try {
20
+ * await checkDockerInstalled();
21
+ * console.log('Docker is available');
22
+ * } catch (error) {
23
+ * // Handle DockerNotInstalledError
24
+ * }
25
+ * ```
26
+ */
27
+ export async function checkDockerInstalled(): Promise<void> {
28
+ try {
29
+ await execa('docker', ['--version']);
30
+ } catch (error: any) {
31
+ throw new DockerNotInstalledError();
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Checks if the Docker daemon is running by executing `docker ps`.
37
+ * Provides specific error types based on the failure reason.
38
+ *
39
+ * @throws {DockerPermissionError} When user lacks permissions to access Docker socket
40
+ * @throws {DockerNotRunningError} When Docker daemon is not running or unreachable
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * try {
45
+ * await checkDockerRunning();
46
+ * } catch (error) {
47
+ * if (error instanceof DockerPermissionError) {
48
+ * // Guide user to add sudo or docker group
49
+ * }
50
+ * }
51
+ * ```
52
+ */
53
+ export async function checkDockerRunning(): Promise<void> {
54
+ try {
55
+ await execa('docker', ['ps']);
56
+ } catch (error: any) {
57
+ const errorMessage = error.message?.toLowerCase() || '';
58
+
59
+ if (errorMessage.includes('permission denied') || errorMessage.includes('eacces')) {
60
+ throw new DockerPermissionError();
61
+ }
62
+
63
+ if (errorMessage.includes('cannot connect') || errorMessage.includes('is the docker daemon running')) {
64
+ throw new DockerNotRunningError();
65
+ }
66
+
67
+ // Generic docker error
68
+ throw new DockerNotRunningError();
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Detects the operating system platform.
74
+ *
75
+ * @returns OS type: 'windows', 'darwin' (macOS), or 'linux'
76
+ * @note Defaults to 'linux' for unknown platforms
77
+ *
78
+ * @example
79
+ * ```ts
80
+ * const os = detectOS();
81
+ * if (os === 'linux') {
82
+ * // Linux-specific configuration
83
+ * }
84
+ * ```
85
+ */
86
+ export function detectOS(): OSType {
87
+ const platform = process.platform;
88
+ if (platform === 'win32') return 'windows';
89
+ if (platform === 'linux') return 'linux';
90
+ if (platform === 'darwin') return 'darwin';
91
+ return 'linux'; // Default fallback
92
+ }
93
+
94
+ /**
95
+ * Validates all system prerequisites before starting the onboarding process.
96
+ * Checks Docker installation, daemon status, and detects the operating system.
97
+ *
98
+ * @throws {DockerNotInstalledError} If Docker is not installed
99
+ * @throws {DockerNotRunningError} If Docker daemon is not running
100
+ * @throws {DockerPermissionError} If user lacks Docker permissions
101
+ *
102
+ * @example
103
+ * ```ts
104
+ * try {
105
+ * await validatePrerequisites();
106
+ * // Proceed with setup
107
+ * } catch (error) {
108
+ * // Handle prerequisite errors
109
+ * }
110
+ * ```
111
+ */
112
+ export async function validatePrerequisites(): Promise<void> {
113
+ console.log('\n🔍 Checking prerequisites...\n');
114
+
115
+ // Check Docker installation
116
+ try {
117
+ await checkDockerInstalled();
118
+ logSuccess('Docker is installed');
119
+ } catch (error) {
120
+ // Error will be caught by main() and displayed with troubleshooting
121
+ throw error;
122
+ }
123
+
124
+ // Check Docker daemon
125
+ try {
126
+ await checkDockerRunning();
127
+ logSuccess('Docker daemon is running');
128
+ } catch (error) {
129
+ // Error will be caught by main() and displayed with troubleshooting
130
+ throw error;
131
+ }
132
+
133
+ // Detect OS
134
+ const os = detectOS();
135
+ logSuccess(`Detected OS: ${os}`);
136
+
137
+ if (os === 'linux') {
138
+ logWarning('Linux detected: You may need to provide your machine IP for infra services.');
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Validates environment variable values based on the variable name/type.
144
+ * Performs format-specific validation for URLs, ports, and security credentials.
145
+ *
146
+ * @param key - Environment variable name (e.g., 'MONGO_URI', 'PORT', 'API_KEY')
147
+ * @param value - Value to validate
148
+ * @returns `true` if valid, `false` otherwise
149
+ *
150
+ * @remarks
151
+ * Validation rules:
152
+ * - URLs: Must contain protocol (mongodb://, redis://)
153
+ * - Ports: Must be 1-65535
154
+ * - Secrets/Keys/Passwords: Must be at least 8 characters
155
+ *
156
+ * @example
157
+ * ```ts
158
+ * validateEnvValue('MONGO_URI', 'mongodb://localhost:27017'); // true
159
+ * validateEnvValue('PORT', '3000'); // true
160
+ * validateEnvValue('API_KEY', 'short'); // false (too short)
161
+ * ```
162
+ */
163
+ export function validateEnvValue(key: string, value: string): boolean {
164
+ // URL validation
165
+ if (key.includes('URL') || key.includes('URI')) {
166
+ if (!value) return false;
167
+ // Basic URL format check
168
+ if (key === 'MONGO_URI' && !value.includes('mongodb://')) {
169
+ return false;
170
+ }
171
+ if (key === 'REDIS_URL' && !value.includes('redis://')) {
172
+ return false;
173
+ }
174
+ }
175
+
176
+ // Port validation
177
+ if (key.includes('PORT')) {
178
+ const port = parseInt(value, 10);
179
+ if (isNaN(port) || port < VALIDATION.PORT_MIN || port > VALIDATION.PORT_MAX) {
180
+ return false;
181
+ }
182
+ }
183
+
184
+ // API Key validation (should not be empty for security)
185
+ if (key.includes('API_KEY') || key.includes('SECRET') || key.includes('PASSWORD')) {
186
+ if (!value || value.length < VALIDATION.MIN_PASSWORD_LENGTH) {
187
+ return false;
188
+ }
189
+ }
190
+
191
+ return true;
192
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "node",
6
+ "rootDir": "./src",
7
+ "outDir": "./dist",
8
+ "esModuleInterop": true,
9
+ "resolveJsonModule": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "strict": true,
12
+ "skipLibCheck": true,
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true
16
+ },
17
+ "include": ["src/**/*"],
18
+ "exclude": ["node_modules", "dist"]
19
+ }
@@ -0,0 +1,20 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: 'node',
7
+ coverage: {
8
+ provider: 'v8',
9
+ reporter: ['text', 'json', 'html'],
10
+ exclude: [
11
+ 'node_modules/**',
12
+ 'dist/**',
13
+ '**/*.test.ts',
14
+ '**/*.spec.ts',
15
+ 'src/templates.ts', // Large constant file
16
+ ],
17
+ },
18
+ testTimeout: 10000,
19
+ },
20
+ });