@simplens/onboard 1.0.1 → 1.0.2
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 +331 -214
- package/dist/__tests__/env-config.test.d.ts +2 -0
- package/dist/__tests__/env-config.test.d.ts.map +1 -0
- package/dist/__tests__/env-config.test.js +23 -0
- package/dist/__tests__/env-config.test.js.map +1 -0
- package/dist/__tests__/infra-prompts.test.d.ts +2 -0
- package/dist/__tests__/infra-prompts.test.d.ts.map +1 -0
- package/dist/__tests__/infra-prompts.test.js +43 -0
- package/dist/__tests__/infra-prompts.test.js.map +1 -0
- package/dist/__tests__/infra.test.d.ts +2 -0
- package/dist/__tests__/infra.test.d.ts.map +1 -0
- package/dist/__tests__/infra.test.js +14 -0
- package/dist/__tests__/infra.test.js.map +1 -0
- package/dist/__tests__/nginx.test.d.ts +2 -0
- package/dist/__tests__/nginx.test.d.ts.map +1 -0
- package/dist/__tests__/nginx.test.js +16 -0
- package/dist/__tests__/nginx.test.js.map +1 -0
- package/dist/env-config.d.ts +27 -12
- package/dist/env-config.d.ts.map +1 -1
- package/dist/env-config.js +253 -128
- package/dist/env-config.js.map +1 -1
- package/dist/index.js +340 -69
- package/dist/index.js.map +1 -1
- package/dist/infra.d.ts +19 -8
- package/dist/infra.d.ts.map +1 -1
- package/dist/infra.js +267 -128
- package/dist/infra.js.map +1 -1
- package/dist/plugins.d.ts +5 -10
- package/dist/plugins.d.ts.map +1 -1
- package/dist/plugins.js +75 -44
- package/dist/plugins.js.map +1 -1
- package/dist/services.d.ts +1 -23
- package/dist/services.d.ts.map +1 -1
- package/dist/services.js +47 -62
- package/dist/services.js.map +1 -1
- package/dist/templates.d.ts +2 -1
- package/dist/templates.d.ts.map +1 -1
- package/dist/templates.js +203 -191
- package/dist/templates.js.map +1 -1
- package/dist/types/domain.d.ts +2 -0
- package/dist/types/domain.d.ts.map +1 -1
- package/dist/ui.d.ts +45 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +93 -0
- package/dist/ui.js.map +1 -0
- package/dist/utils/logger.d.ts +1 -0
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +32 -7
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils.d.ts +8 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +66 -2
- package/dist/utils.js.map +1 -1
- package/dist/validators.d.ts +1 -52
- package/dist/validators.d.ts.map +1 -1
- package/dist/validators.js +10 -57
- package/dist/validators.js.map +1 -1
- package/package.json +3 -5
- package/src/__tests__/env-config.test.ts +28 -0
- package/src/__tests__/errors.test.ts +187 -187
- package/src/__tests__/infra-prompts.test.ts +54 -0
- package/src/__tests__/infra.test.ts +15 -0
- package/src/__tests__/utils.test.ts +142 -142
- package/src/__tests__/validators.test.ts +195 -195
- package/src/config/constants.ts +86 -86
- package/src/config/index.ts +1 -1
- package/src/env-config.ts +455 -311
- package/src/index.ts +534 -202
- package/src/infra.ts +404 -245
- package/src/plugins.ts +221 -190
- package/src/services.ts +175 -190
- package/src/templates.ts +209 -196
- package/src/types/domain.ts +129 -127
- package/src/types/errors.ts +173 -173
- package/src/types/index.ts +2 -2
- package/src/ui.ts +91 -0
- package/src/utils/index.ts +1 -1
- package/src/utils/logger.ts +144 -118
- package/src/utils.ts +183 -105
- package/src/validators.ts +145 -192
- package/tsconfig.json +18 -18
- package/vitest.config.ts +22 -22
package/src/index.ts
CHANGED
|
@@ -1,202 +1,534 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { Command } from 'commander';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
} from './
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
} from './
|
|
25
|
-
import {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import {
|
|
6
|
+
displayBanner,
|
|
7
|
+
logSuccess,
|
|
8
|
+
logInfo,
|
|
9
|
+
initLogger,
|
|
10
|
+
logDebug,
|
|
11
|
+
printStepHeader,
|
|
12
|
+
printSummaryCard,
|
|
13
|
+
printCommandHints,
|
|
14
|
+
logWarning,
|
|
15
|
+
} from './utils.js';
|
|
16
|
+
import { text, confirm, select } from '@clack/prompts';
|
|
17
|
+
import { intro, outro, handleCancel, log, note } from './ui.js';
|
|
18
|
+
import { validatePrerequisites } from './validators.js';
|
|
19
|
+
import {
|
|
20
|
+
promptInfraServicesWithBasePath,
|
|
21
|
+
generateInfraCompose,
|
|
22
|
+
writeAppCompose,
|
|
23
|
+
generateNginxConfig,
|
|
24
|
+
} from './infra.js';
|
|
25
|
+
import {
|
|
26
|
+
promptEnvVariables,
|
|
27
|
+
generateEnvFile,
|
|
28
|
+
appendPluginEnv,
|
|
29
|
+
promptBasePath,
|
|
30
|
+
normalizeBasePath,
|
|
31
|
+
validateBasePath,
|
|
32
|
+
DEFAULT_BASE_PATH,
|
|
33
|
+
} from './env-config.js';
|
|
34
|
+
import {
|
|
35
|
+
fetchAvailablePlugins,
|
|
36
|
+
promptPluginSelection,
|
|
37
|
+
generatePluginConfig,
|
|
38
|
+
parseConfigCredentials,
|
|
39
|
+
promptPluginCredentials,
|
|
40
|
+
generateDefaultPluginCredentials,
|
|
41
|
+
} from './plugins.js';
|
|
42
|
+
import {
|
|
43
|
+
promptStartServices,
|
|
44
|
+
startInfraServices,
|
|
45
|
+
waitForInfraHealth,
|
|
46
|
+
startAppServices,
|
|
47
|
+
displayServiceStatus,
|
|
48
|
+
} from './services.js';
|
|
49
|
+
|
|
50
|
+
const program = new Command();
|
|
51
|
+
|
|
52
|
+
program
|
|
53
|
+
.name('@simplens/onboard')
|
|
54
|
+
.description('A CLI tool to setup a SimpleNS instance on your machine/server')
|
|
55
|
+
.version('1.0.0')
|
|
56
|
+
.option('--full', 'Non-interactive mode - all options must be provided via CLI')
|
|
57
|
+
.option('--infra [services...]', 'Infrastructure services (mongo, kafka, kafka-ui, redis, nginx, loki, grafana)')
|
|
58
|
+
.option('--env <mode>', 'Environment setup mode: "default" or "interactive"')
|
|
59
|
+
.option('--dir <path>', 'Target directory for setup')
|
|
60
|
+
.option('--base-path <path>', 'Dashboard BASE_PATH (example: /dashboard, default: root)')
|
|
61
|
+
.option('--plugin [plugins...]', 'Plugins to install (e.g., @simplens/mock @simplens/nodemailer-gmail)')
|
|
62
|
+
.option('--no-output', 'Suppress all console output (silent mode)');
|
|
63
|
+
|
|
64
|
+
interface OnboardSetupOptions {
|
|
65
|
+
infra: boolean;
|
|
66
|
+
infraServices: string[];
|
|
67
|
+
envMode: 'default' | 'interactive';
|
|
68
|
+
targetDir: string;
|
|
69
|
+
basePath: string;
|
|
70
|
+
plugins: string[];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function printStep(step: number, total: number, title: string): void {
|
|
74
|
+
printStepHeader(step, total, title);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function shouldAutoEnableNginx(basePath: string): boolean {
|
|
78
|
+
return normalizeBasePath(basePath) !== DEFAULT_BASE_PATH;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Valid infrastructure services
|
|
83
|
+
*/
|
|
84
|
+
const VALID_INFRA_SERVICES = ['mongo', 'kafka', 'kafka-ui', 'redis', 'nginx', 'loki', 'grafana'];
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Validate infrastructure service names
|
|
88
|
+
*/
|
|
89
|
+
function validateInfraServices(services: string[]): { valid: boolean; invalid: string[] } {
|
|
90
|
+
const invalid = services.filter(s => !VALID_INFRA_SERVICES.includes(s));
|
|
91
|
+
return { valid: invalid.length === 0, invalid };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Validate plugin names (must start with @simplens/ or be a valid npm package)
|
|
96
|
+
*/
|
|
97
|
+
function validatePlugins(plugins: string[]): { valid: boolean; invalid: string[] } {
|
|
98
|
+
const invalid = plugins.filter(p => {
|
|
99
|
+
// Must start with @ or be a valid npm package name
|
|
100
|
+
return !p.match(/^(@[\w-]+\/[\w-]+|[\w-]+)$/);
|
|
101
|
+
});
|
|
102
|
+
return { valid: invalid.length === 0, invalid };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function showSetupSummary(setupOptions: OnboardSetupOptions, targetDir: string, autoNginx: boolean): void {
|
|
106
|
+
const basePathLabel = setupOptions.basePath || '(root)';
|
|
107
|
+
const infraLabel = setupOptions.infra
|
|
108
|
+
? `enabled (${setupOptions.infraServices.join(', ')})`
|
|
109
|
+
: 'disabled';
|
|
110
|
+
const pluginsLabel = setupOptions.plugins.length > 0
|
|
111
|
+
? setupOptions.plugins.join(', ')
|
|
112
|
+
: 'none';
|
|
113
|
+
|
|
114
|
+
const summaryLines = [
|
|
115
|
+
`Target directory : ${targetDir}`,
|
|
116
|
+
`Infrastructure : ${infraLabel}`,
|
|
117
|
+
`Environment mode : ${setupOptions.envMode}`,
|
|
118
|
+
`BASE_PATH : ${basePathLabel}`,
|
|
119
|
+
`Plugins : ${pluginsLabel}`,
|
|
120
|
+
`Nginx auto-include : ${autoNginx ? 'enabled (BASE_PATH is non-default)' : 'disabled'}`,
|
|
121
|
+
].join('\n');
|
|
122
|
+
|
|
123
|
+
note(summaryLines, 'Setup Summary');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Prompt for setup options if not provided via CLI args
|
|
128
|
+
* In --full mode, all required options must be provided via CLI
|
|
129
|
+
*/
|
|
130
|
+
async function promptSetupOptions(options: any): Promise<OnboardSetupOptions> {
|
|
131
|
+
const isFullMode = options.full === true;
|
|
132
|
+
|
|
133
|
+
// --- Validate --full mode requirements ---
|
|
134
|
+
if (isFullMode) {
|
|
135
|
+
const errors: string[] = [];
|
|
136
|
+
|
|
137
|
+
// --env is required in full mode
|
|
138
|
+
if (!options.env) {
|
|
139
|
+
errors.push('--env <mode> is required in --full mode (use \"default\" or \"interactive\")');
|
|
140
|
+
} else if (options.env !== 'default' && options.env !== 'interactive') {
|
|
141
|
+
errors.push('--env must be either \"default\" or \"interactive\"');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Validate --base-path if provided
|
|
145
|
+
if (options.basePath) {
|
|
146
|
+
const validation = validateBasePath(normalizeBasePath(options.basePath));
|
|
147
|
+
if (validation !== true) {
|
|
148
|
+
errors.push(`Invalid --base-path: ${validation}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Validate --infra services if provided
|
|
153
|
+
if (options.infra && Array.isArray(options.infra)) {
|
|
154
|
+
const { valid, invalid } = validateInfraServices(options.infra);
|
|
155
|
+
if (!valid) {
|
|
156
|
+
errors.push(
|
|
157
|
+
`Invalid infrastructure services: ${invalid.join(', ')}. ` +
|
|
158
|
+
`Valid options: ${VALID_INFRA_SERVICES.join(', ')}`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Validate --plugin if provided
|
|
164
|
+
if (options.plugin && Array.isArray(options.plugin)) {
|
|
165
|
+
const { valid, invalid } = validatePlugins(options.plugin);
|
|
166
|
+
if (!valid) {
|
|
167
|
+
errors.push(`Invalid plugin names: ${invalid.join(', ')}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (errors.length > 0) {
|
|
172
|
+
console.error('\\n❌ Validation errors in --full mode:\\n');
|
|
173
|
+
errors.forEach(err => console.error(` • ${err}`));
|
|
174
|
+
console.error('\\nRun with --help to see usage examples.\\n');
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// --- BASE_PATH ---
|
|
180
|
+
let basePathValue: string;
|
|
181
|
+
const cliBasePath = typeof options.basePath === 'string'
|
|
182
|
+
? normalizeBasePath(options.basePath)
|
|
183
|
+
: undefined;
|
|
184
|
+
|
|
185
|
+
if (cliBasePath !== undefined) {
|
|
186
|
+
const validation = validateBasePath(cliBasePath);
|
|
187
|
+
if (validation !== true) {
|
|
188
|
+
throw new Error(`Invalid --base-path value: ${validation}`);
|
|
189
|
+
}
|
|
190
|
+
basePathValue = cliBasePath;
|
|
191
|
+
} else if (isFullMode) {
|
|
192
|
+
basePathValue = DEFAULT_BASE_PATH; // Default to root in full mode
|
|
193
|
+
} else {
|
|
194
|
+
basePathValue = await promptBasePath(DEFAULT_BASE_PATH);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// --- Infra flag and services ---
|
|
198
|
+
let infraValue: boolean;
|
|
199
|
+
let infraServices: string[] = [];
|
|
200
|
+
|
|
201
|
+
if (Array.isArray(options.infra) && options.infra.length > 0) {
|
|
202
|
+
// --infra with services provided
|
|
203
|
+
infraValue = true;
|
|
204
|
+
infraServices = options.infra;
|
|
205
|
+
} else if (options.infra === true) {
|
|
206
|
+
// --infra flag without services (backward compatibility - prompt for services)
|
|
207
|
+
infraValue = true;
|
|
208
|
+
if (isFullMode) {
|
|
209
|
+
// In full mode, empty --infra means no services selected (error)
|
|
210
|
+
console.error('\\n❌ In --full mode, --infra requires service names.\\n');
|
|
211
|
+
console.error('Example: --infra mongo kafka redis\\n');
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
// Not in full mode, will prompt later
|
|
215
|
+
} else {
|
|
216
|
+
// No --infra flag provided
|
|
217
|
+
if (isFullMode) {
|
|
218
|
+
infraValue = false; // Default to no infrastructure in full mode
|
|
219
|
+
} else {
|
|
220
|
+
const result = await confirm({
|
|
221
|
+
message: 'Do you want to setup infrastructure services (MongoDB, Kafka, Redis, etc.)?',
|
|
222
|
+
initialValue: true,
|
|
223
|
+
withGuide: true,
|
|
224
|
+
});
|
|
225
|
+
handleCancel(result);
|
|
226
|
+
infraValue = result as boolean;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// --- Env mode ---
|
|
231
|
+
let envModeValue: 'default' | 'interactive';
|
|
232
|
+
if (options.env) {
|
|
233
|
+
envModeValue = options.env;
|
|
234
|
+
} else if (isFullMode) {
|
|
235
|
+
// Already validated above, this shouldn't happen
|
|
236
|
+
envModeValue = 'default';
|
|
237
|
+
} else {
|
|
238
|
+
const result = await select({
|
|
239
|
+
message: 'Select environment configuration mode:',
|
|
240
|
+
options: [
|
|
241
|
+
{ value: 'default', label: 'Default', hint: 'use preset values, prompt only for critical' },
|
|
242
|
+
{ value: 'interactive', label: 'Interactive', hint: 'prompt for all variables' },
|
|
243
|
+
],
|
|
244
|
+
initialValue: 'default',
|
|
245
|
+
withGuide: true,
|
|
246
|
+
});
|
|
247
|
+
handleCancel(result);
|
|
248
|
+
envModeValue = result as 'default' | 'interactive';
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// --- Target directory ---
|
|
252
|
+
let targetDirValue: string;
|
|
253
|
+
if (options.dir) {
|
|
254
|
+
targetDirValue = options.dir;
|
|
255
|
+
} else if (isFullMode) {
|
|
256
|
+
targetDirValue = process.cwd(); // Default to current directory in full mode
|
|
257
|
+
} else {
|
|
258
|
+
const result = await text({
|
|
259
|
+
message: 'Target directory for setup:',
|
|
260
|
+
defaultValue: process.cwd(),
|
|
261
|
+
initialValue: process.cwd(),
|
|
262
|
+
withGuide: true,
|
|
263
|
+
});
|
|
264
|
+
handleCancel(result);
|
|
265
|
+
targetDirValue = result as string;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// --- Plugins ---
|
|
269
|
+
let pluginsValue: string[] = [];
|
|
270
|
+
if (Array.isArray(options.plugin) && options.plugin.length > 0) {
|
|
271
|
+
pluginsValue = options.plugin;
|
|
272
|
+
}
|
|
273
|
+
// If not provided and not in full mode, will prompt later in the main workflow
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
infra: infraValue,
|
|
277
|
+
infraServices: infraServices,
|
|
278
|
+
envMode: envModeValue || 'default',
|
|
279
|
+
targetDir: targetDirValue || process.cwd(),
|
|
280
|
+
basePath: basePathValue,
|
|
281
|
+
plugins: pluginsValue,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Main onboarding workflow
|
|
287
|
+
*/
|
|
288
|
+
async function main() {
|
|
289
|
+
try {
|
|
290
|
+
const totalSteps = 6;
|
|
291
|
+
|
|
292
|
+
// Parse command line arguments FIRST
|
|
293
|
+
program.parse(process.argv);
|
|
294
|
+
const options = program.opts();
|
|
295
|
+
|
|
296
|
+
// Initialize logger based on CLI flags (before any output)
|
|
297
|
+
initLogger({
|
|
298
|
+
verbose: options.verbose || false,
|
|
299
|
+
debug: options.debug || false,
|
|
300
|
+
silent: !options.output, // --no-output sets options.output to false
|
|
301
|
+
logFile: options.debug ? path.join(process.cwd(), 'onboard-debug.log') : undefined,
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// Display banner (after logger is initialized)
|
|
305
|
+
displayBanner();
|
|
306
|
+
|
|
307
|
+
// Clack intro
|
|
308
|
+
intro('SimpleNS Onboard');
|
|
309
|
+
|
|
310
|
+
logDebug('Logger initialized');
|
|
311
|
+
logDebug(`CLI options: ${JSON.stringify(options)}`);
|
|
312
|
+
|
|
313
|
+
// Prompt for setup options if not provided
|
|
314
|
+
const setupOptions = await promptSetupOptions(options);
|
|
315
|
+
|
|
316
|
+
// Get target directory
|
|
317
|
+
const targetDir = path.resolve(setupOptions.targetDir);
|
|
318
|
+
const autoEnableNginx = shouldAutoEnableNginx(setupOptions.basePath);
|
|
319
|
+
|
|
320
|
+
logDebug(`Resolved target directory: ${targetDir}`);
|
|
321
|
+
showSetupSummary(setupOptions, targetDir, autoEnableNginx);
|
|
322
|
+
|
|
323
|
+
// Step 1: Validate prerequisites
|
|
324
|
+
log.step('Step 1/6 — Prerequisites Validation');
|
|
325
|
+
await validatePrerequisites();
|
|
326
|
+
|
|
327
|
+
// Step 2: Infrastructure setup (if --infra flag is provided)
|
|
328
|
+
log.step('Step 2/6 — Infrastructure Setup');
|
|
329
|
+
let selectedInfraServices: string[] = [];
|
|
330
|
+
|
|
331
|
+
if (setupOptions.infra) {
|
|
332
|
+
// Use pre-provided services from CLI, or prompt for them
|
|
333
|
+
if (setupOptions.infraServices.length > 0) {
|
|
334
|
+
selectedInfraServices = setupOptions.infraServices;
|
|
335
|
+
log.info(`Using infrastructure services: ${selectedInfraServices.join(', ')}`);
|
|
336
|
+
} else {
|
|
337
|
+
// Prompt for services (interactive mode)
|
|
338
|
+
if (!autoEnableNginx) {
|
|
339
|
+
log.info('BASE_PATH is empty, nginx reverse proxy is disabled.');
|
|
340
|
+
selectedInfraServices = await promptInfraServicesWithBasePath({ allowNginx: false });
|
|
341
|
+
} else {
|
|
342
|
+
selectedInfraServices = await promptInfraServicesWithBasePath({ allowNginx: true });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (autoEnableNginx && !selectedInfraServices.includes('nginx')) {
|
|
347
|
+
selectedInfraServices.push('nginx');
|
|
348
|
+
log.info('BASE_PATH is non-default, so nginx was added automatically.');
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
await generateInfraCompose(targetDir, selectedInfraServices);
|
|
352
|
+
} else {
|
|
353
|
+
log.info('Skipping infrastructure setup (use --infra to enable).');
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Step 3: Always write app docker-compose
|
|
357
|
+
log.step('Step 3/6 — Application Compose Setup');
|
|
358
|
+
const includeNginxInAppCompose = autoEnableNginx && !selectedInfraServices.includes('nginx');
|
|
359
|
+
if (includeNginxInAppCompose) {
|
|
360
|
+
log.info('Including nginx in docker-compose.yaml because BASE_PATH is non-default.');
|
|
361
|
+
}
|
|
362
|
+
await writeAppCompose(targetDir, { includeNginx: includeNginxInAppCompose });
|
|
363
|
+
|
|
364
|
+
// Step 4: Environment configuration
|
|
365
|
+
log.step('Step 4/6 — Environment Configuration');
|
|
366
|
+
const envMode = setupOptions.envMode;
|
|
367
|
+
const envVars = await promptEnvVariables(
|
|
368
|
+
envMode,
|
|
369
|
+
selectedInfraServices,
|
|
370
|
+
setupOptions.basePath,
|
|
371
|
+
options.full || false
|
|
372
|
+
);
|
|
373
|
+
await generateEnvFile(targetDir, envVars);
|
|
374
|
+
|
|
375
|
+
// In full mode, notify user about auto-generated credentials
|
|
376
|
+
if (options.full) {
|
|
377
|
+
logWarning(
|
|
378
|
+
'⚠️ Auto-generated credentials in .env file. ' +
|
|
379
|
+
'Please update NS_API_KEY, AUTH_SECRET, and ADMIN_PASSWORD before deploying to production!'
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Generate nginx.conf whenever nginx is active in either compose file
|
|
384
|
+
const nginxEnabled = selectedInfraServices.includes('nginx') || includeNginxInAppCompose;
|
|
385
|
+
if (nginxEnabled) {
|
|
386
|
+
await generateNginxConfig(targetDir, setupOptions.basePath);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Step 5: Plugin installation
|
|
390
|
+
log.step('Step 5/6 — Plugin Installation');
|
|
391
|
+
let selectedPlugins: string[] = [];
|
|
392
|
+
let pluginCredentialKeys: string[] = [];
|
|
393
|
+
|
|
394
|
+
// Use pre-provided plugins from CLI, or prompt for them
|
|
395
|
+
if (setupOptions.plugins.length > 0) {
|
|
396
|
+
selectedPlugins = setupOptions.plugins;
|
|
397
|
+
log.info(`Using plugins: ${selectedPlugins.join(', ')}`);
|
|
398
|
+
} else if (!options.full) {
|
|
399
|
+
// Only prompt in interactive mode
|
|
400
|
+
const availablePlugins = await fetchAvailablePlugins();
|
|
401
|
+
selectedPlugins = await promptPluginSelection(availablePlugins);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (selectedPlugins.length > 0) {
|
|
405
|
+
await generatePluginConfig(targetDir, selectedPlugins);
|
|
406
|
+
|
|
407
|
+
// Extract and prompt for plugin credentials
|
|
408
|
+
const configPath = path.join(targetDir, 'simplens.config.yaml');
|
|
409
|
+
const credentialKeys = await parseConfigCredentials(configPath);
|
|
410
|
+
pluginCredentialKeys = credentialKeys; // Store for later use
|
|
411
|
+
|
|
412
|
+
if (credentialKeys.length > 0) {
|
|
413
|
+
if (options.full) {
|
|
414
|
+
// In full mode, auto-generate placeholder credentials
|
|
415
|
+
const pluginCreds = generateDefaultPluginCredentials(credentialKeys);
|
|
416
|
+
await appendPluginEnv(targetDir, pluginCreds);
|
|
417
|
+
logWarning(
|
|
418
|
+
`⚠️ Auto-generated placeholder plugin credentials. ` +
|
|
419
|
+
`Please update these in .env file: ${credentialKeys.join(', ')}`
|
|
420
|
+
);
|
|
421
|
+
} else {
|
|
422
|
+
const pluginCreds = await promptPluginCredentials(credentialKeys);
|
|
423
|
+
await appendPluginEnv(targetDir, pluginCreds);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Step 6: Service orchestration
|
|
429
|
+
log.step('Step 6/6 — Service Orchestration');
|
|
430
|
+
|
|
431
|
+
let shouldStart = false;
|
|
432
|
+
if (options.full) {
|
|
433
|
+
// In full mode, don't auto-start services, just show commands
|
|
434
|
+
log.info('In --full mode, services are not auto-started.');
|
|
435
|
+
} else {
|
|
436
|
+
shouldStart = await promptStartServices();
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (shouldStart) {
|
|
440
|
+
// Start infra services first (if --infra was used)
|
|
441
|
+
if (setupOptions.infra && selectedInfraServices.length > 0) {
|
|
442
|
+
await startInfraServices(targetDir);
|
|
443
|
+
await waitForInfraHealth(targetDir);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Start app services
|
|
447
|
+
await startAppServices(targetDir);
|
|
448
|
+
|
|
449
|
+
// Display service status
|
|
450
|
+
await displayServiceStatus();
|
|
451
|
+
} else {
|
|
452
|
+
log.info('Services not started. You can start them later with:');
|
|
453
|
+
const commands: string[] = [];
|
|
454
|
+
if (setupOptions.infra) {
|
|
455
|
+
commands.push('docker-compose -f docker-compose.infra.yaml up -d');
|
|
456
|
+
}
|
|
457
|
+
commands.push('docker-compose up -d');
|
|
458
|
+
printCommandHints('Manual startup commands', commands);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Final success message
|
|
462
|
+
logSuccess('SimpleNS onboarding completed successfully.');
|
|
463
|
+
|
|
464
|
+
// In full mode, show a comprehensive security warning
|
|
465
|
+
if (options.full) {
|
|
466
|
+
const credentialWarnings = [
|
|
467
|
+
' • NS_API_KEY - API authentication key',
|
|
468
|
+
' • AUTH_SECRET - Session secret for dashboard',
|
|
469
|
+
' • ADMIN_PASSWORD - Dashboard admin password',
|
|
470
|
+
];
|
|
471
|
+
|
|
472
|
+
if (pluginCredentialKeys.length > 0) {
|
|
473
|
+
credentialWarnings.push(` • Plugin credentials: ${pluginCredentialKeys.join(', ')}`);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
note(
|
|
477
|
+
'⚠️ IMPORTANT: Auto-generated credentials were used for non-interactive setup.\n' +
|
|
478
|
+
'\n' +
|
|
479
|
+
'Please update the following in your .env file before production use:\n' +
|
|
480
|
+
credentialWarnings.join('\n') +
|
|
481
|
+
'\n\n' +
|
|
482
|
+
'Default credentials are NOT secure for production environments.',
|
|
483
|
+
'Security Notice'
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Display access information
|
|
488
|
+
if (nginxEnabled) {
|
|
489
|
+
if (setupOptions.basePath) {
|
|
490
|
+
note(
|
|
491
|
+
`Dashboard : http://localhost${setupOptions.basePath}\nAPI : http://localhost/api/notification/`,
|
|
492
|
+
'Service Access'
|
|
493
|
+
);
|
|
494
|
+
} else {
|
|
495
|
+
note(
|
|
496
|
+
'Dashboard : http://localhost\nAPI : http://localhost/api/notification/',
|
|
497
|
+
'Service Access'
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
} else {
|
|
501
|
+
note(
|
|
502
|
+
'Dashboard : http://localhost:3002\nAPI : http://localhost:3000',
|
|
503
|
+
'Service Access'
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Clack outro
|
|
508
|
+
outro('Setup complete — happy notifying! 🚀');
|
|
509
|
+
|
|
510
|
+
} catch (error: unknown) {
|
|
511
|
+
// Import at top of file
|
|
512
|
+
const { formatErrorForUser } = await import('./types/errors.js');
|
|
513
|
+
const { getLoggerConfig } = await import('./utils/logger.js');
|
|
514
|
+
|
|
515
|
+
// Always log errors to stderr, even in silent mode
|
|
516
|
+
if (!getLoggerConfig().silent) {
|
|
517
|
+
console.log('\n' + formatErrorForUser(error as Error));
|
|
518
|
+
} else {
|
|
519
|
+
// In silent mode, write to stderr
|
|
520
|
+
console.error(formatErrorForUser(error as Error));
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Log full error to stderr for debugging
|
|
524
|
+
if (process.env.DEBUG) {
|
|
525
|
+
console.error('\nFull error details:');
|
|
526
|
+
console.error(error);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
process.exit(1);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Run main function
|
|
534
|
+
main();
|