@simplens/onboard 1.0.8 → 1.0.10
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/dist/index.js +35 -10
- package/dist/index.js.map +1 -1
- package/dist/infra.d.ts +1 -0
- package/dist/infra.d.ts.map +1 -1
- package/dist/infra.js +4 -3
- package/dist/infra.js.map +1 -1
- package/dist/services.d.ts +13 -0
- package/dist/services.d.ts.map +1 -1
- package/dist/services.js +74 -8
- package/dist/services.js.map +1 -1
- package/dist/templates.d.ts +2 -2
- package/dist/templates.d.ts.map +1 -1
- package/dist/templates.js +8 -4
- package/dist/templates.js.map +1 -1
- package/package.json +5 -1
- package/src/__tests__/env-config.test.ts +0 -28
- package/src/__tests__/errors.test.ts +0 -188
- package/src/__tests__/infra-prompts.test.ts +0 -56
- package/src/__tests__/infra.test.ts +0 -30
- package/src/__tests__/utils.test.ts +0 -142
- package/src/__tests__/validators.test.ts +0 -221
- package/src/config/constants.ts +0 -87
- package/src/config/index.ts +0 -1
- package/src/env-config.ts +0 -503
- package/src/index.ts +0 -683
- package/src/infra.ts +0 -514
- package/src/plugins.ts +0 -221
- package/src/services.ts +0 -308
- package/src/templates.ts +0 -278
- package/src/types/domain.ts +0 -135
- package/src/types/errors.ts +0 -173
- package/src/types/index.ts +0 -2
- package/src/ui.ts +0 -91
- package/src/utils/index.ts +0 -1
- package/src/utils/logger.ts +0 -144
- package/src/utils.ts +0 -183
- package/src/validators.ts +0 -196
- package/tsconfig.json +0 -19
- package/vitest.config.ts +0 -22
package/src/plugins.ts
DELETED
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
import { execa } from 'execa';
|
|
2
|
-
import yaml from 'js-yaml';
|
|
3
|
-
import crypto from 'crypto';
|
|
4
|
-
import { readFile, logInfo, logSuccess, logError, logWarning } from './utils.js';
|
|
5
|
-
import { multiselect, text, password } from '@clack/prompts';
|
|
6
|
-
import { handleCancel, spinner } from './ui.js';
|
|
7
|
-
import path from 'path';
|
|
8
|
-
import type { PluginInfo, SimplensConfig } from './types/domain.js';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Fetches available SimpleNS plugins using the config-gen CLI tool.
|
|
12
|
-
* Falls back to default plugins if fetching fails.
|
|
13
|
-
*
|
|
14
|
-
* @returns Array of available plugin information
|
|
15
|
-
*/
|
|
16
|
-
export async function fetchAvailablePlugins(): Promise<PluginInfo[]> {
|
|
17
|
-
const s = spinner();
|
|
18
|
-
s.start('Fetching available plugins...');
|
|
19
|
-
|
|
20
|
-
try {
|
|
21
|
-
// Execute config-gen list command
|
|
22
|
-
const { stdout } = await execa('npx', ['@simplens/config-gen', 'list', '--official'], {
|
|
23
|
-
stdio: 'pipe',
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
// Parse output to extract plugins
|
|
27
|
-
// Expected format: " @simplens/package-name Plugin Name"
|
|
28
|
-
const plugins: PluginInfo[] = [];
|
|
29
|
-
const lines = stdout.split('\n');
|
|
30
|
-
|
|
31
|
-
for (const line of lines) {
|
|
32
|
-
// Match plugin lines (starts with @simplens/)
|
|
33
|
-
const match = line.match(/^\s+(@simplens\/[\w-]+)\s+(.+)$/);
|
|
34
|
-
if (match) {
|
|
35
|
-
const [, packageName, rest] = match;
|
|
36
|
-
// Extract name and description
|
|
37
|
-
const parts = rest.split(/\s{2,}/); // Split by multiple spaces
|
|
38
|
-
plugins.push({
|
|
39
|
-
package: packageName.trim(),
|
|
40
|
-
name: parts[0]?.trim() || packageName,
|
|
41
|
-
description: parts[1]?.trim() || '',
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
s.stop(`Found ${plugins.length} available plugins`);
|
|
47
|
-
return plugins;
|
|
48
|
-
} catch (error: unknown) {
|
|
49
|
-
s.stop('Could not fetch plugins list. Using defaults.');
|
|
50
|
-
logWarning('Falling back to default plugin list.');
|
|
51
|
-
// Return default plugins as fallback
|
|
52
|
-
return [
|
|
53
|
-
{ package: '@simplens/mock', name: 'Mock Provider', description: 'Mock notification provider for testing' },
|
|
54
|
-
{ package: '@simplens/nodemailer-gmail', name: 'Gmail', description: 'Send emails via Gmail' },
|
|
55
|
-
{ package: '@simplens/resend', name: 'Resend', description: 'Send emails via Resend' },
|
|
56
|
-
];
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Prompt user to select plugins
|
|
62
|
-
*/
|
|
63
|
-
export async function promptPluginSelection(availablePlugins: PluginInfo[]): Promise<string[]> {
|
|
64
|
-
if (availablePlugins.length === 0) {
|
|
65
|
-
logWarning('No plugins available to select.');
|
|
66
|
-
return [];
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const selected = await multiselect({
|
|
70
|
-
message: 'Select plugins to install (Space to select, Enter to confirm):',
|
|
71
|
-
options: availablePlugins.map(p => ({
|
|
72
|
-
value: p.package,
|
|
73
|
-
label: `${p.name} (${p.package})`,
|
|
74
|
-
hint: p.description,
|
|
75
|
-
})),
|
|
76
|
-
initialValues: availablePlugins
|
|
77
|
-
.filter(p => p.package === '@simplens/mock')
|
|
78
|
-
.map(p => p.package),
|
|
79
|
-
withGuide: true,
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
handleCancel(selected);
|
|
83
|
-
return selected as string[];
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Generate plugin configuration using config-gen
|
|
88
|
-
*/
|
|
89
|
-
export async function generatePluginConfig(
|
|
90
|
-
targetDir: string,
|
|
91
|
-
selectedPlugins: string[]
|
|
92
|
-
): Promise<void> {
|
|
93
|
-
if (selectedPlugins.length === 0) {
|
|
94
|
-
logInfo('No plugins selected, skipping config generation.');
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const s = spinner();
|
|
99
|
-
s.start(`Generating configuration for ${selectedPlugins.length} plugin(s)...`);
|
|
100
|
-
|
|
101
|
-
try {
|
|
102
|
-
// Execute config-gen for all selected plugins
|
|
103
|
-
// Use relative path to avoid WSL path issues when npx runs Windows binaries
|
|
104
|
-
await execa(
|
|
105
|
-
'npx',
|
|
106
|
-
['@simplens/config-gen', 'gen', ...selectedPlugins, '-o', 'simplens.config.yaml'],
|
|
107
|
-
{ cwd: targetDir, stdio: 'pipe' }
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
s.stop('Generated simplens.config.yaml');
|
|
111
|
-
} catch (error: unknown) {
|
|
112
|
-
s.error('Failed to generate plugin configuration');
|
|
113
|
-
throw error;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Parse simplens.config.yaml to extract credential keys
|
|
119
|
-
*/
|
|
120
|
-
export async function parseConfigCredentials(configPath: string): Promise<string[]> {
|
|
121
|
-
try {
|
|
122
|
-
const content = await readFile(configPath);
|
|
123
|
-
const config: any = yaml.load(content);
|
|
124
|
-
|
|
125
|
-
const credentialKeys = new Set<string>();
|
|
126
|
-
|
|
127
|
-
// Extract credential keys from providers (ONLY from credentials, not optionalConfig)
|
|
128
|
-
if (config.providers && Array.isArray(config.providers)) {
|
|
129
|
-
for (const provider of config.providers) {
|
|
130
|
-
if (provider.credentials && typeof provider.credentials === 'object') {
|
|
131
|
-
for (const [key, value] of Object.entries(provider.credentials)) {
|
|
132
|
-
// Extract env var name from ${ENV_VAR} format
|
|
133
|
-
if (typeof value === 'string' && value.startsWith('${') && value.endsWith('}')) {
|
|
134
|
-
const envVar = value.slice(2, -1);
|
|
135
|
-
credentialKeys.add(envVar);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
// NOTE: We intentionally skip optionalConfig - those are optional!
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return Array.from(credentialKeys);
|
|
144
|
-
} catch (error) {
|
|
145
|
-
logWarning('Could not parse config file for credentials');
|
|
146
|
-
return [];
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Generate default placeholder values for plugin credentials
|
|
152
|
-
* Used in --full mode for non-interactive setup
|
|
153
|
-
*/
|
|
154
|
-
export function generateDefaultPluginCredentials(credentialKeys: string[]): Map<string, string> {
|
|
155
|
-
const result = new Map<string, string>();
|
|
156
|
-
|
|
157
|
-
for (const key of credentialKeys) {
|
|
158
|
-
// Generate placeholder values based on key name patterns
|
|
159
|
-
if (key.toLowerCase().includes('password') || key.toLowerCase().includes('secret')) {
|
|
160
|
-
result.set(key, crypto.randomBytes(16).toString('base64'));
|
|
161
|
-
} else if (key.toLowerCase().includes('apikey') || key.toLowerCase().includes('api_key')) {
|
|
162
|
-
result.set(key, `sk_${crypto.randomBytes(24).toString('base64').slice(0, 32)}`);
|
|
163
|
-
} else if (key.toLowerCase().includes('token')) {
|
|
164
|
-
result.set(key, crypto.randomBytes(32).toString('hex'));
|
|
165
|
-
} else if (key.toLowerCase().includes('email') || key.toLowerCase().includes('user')) {
|
|
166
|
-
result.set(key, 'CHANGE_ME@example.com');
|
|
167
|
-
} else {
|
|
168
|
-
// Generic placeholder
|
|
169
|
-
result.set(key, 'CHANGE_ME');
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return result;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Prompt for plugin-specific credentials
|
|
178
|
-
*/
|
|
179
|
-
export async function promptPluginCredentials(credentialKeys: string[]): Promise<Map<string, string>> {
|
|
180
|
-
if (credentialKeys.length === 0) {
|
|
181
|
-
logInfo('No plugin credentials required.');
|
|
182
|
-
return new Map();
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
logInfo('Configuring plugin credentials...');
|
|
186
|
-
|
|
187
|
-
const result = new Map<string, string>();
|
|
188
|
-
|
|
189
|
-
for (const key of credentialKeys) {
|
|
190
|
-
const isSecret = key.toLowerCase().includes('password') || key.toLowerCase().includes('key');
|
|
191
|
-
|
|
192
|
-
let answer: string | symbol;
|
|
193
|
-
if (isSecret) {
|
|
194
|
-
answer = await password({
|
|
195
|
-
message: `${key}:`,
|
|
196
|
-
validate: (input: string | undefined) => {
|
|
197
|
-
if (!input || input.trim().length === 0) {
|
|
198
|
-
return `${key} is required`;
|
|
199
|
-
}
|
|
200
|
-
return undefined;
|
|
201
|
-
},
|
|
202
|
-
});
|
|
203
|
-
} else {
|
|
204
|
-
answer = await text({
|
|
205
|
-
message: `${key}:`,
|
|
206
|
-
validate: (input: string | undefined) => {
|
|
207
|
-
if (!input || input.trim().length === 0) {
|
|
208
|
-
return `${key} is required`;
|
|
209
|
-
}
|
|
210
|
-
return undefined;
|
|
211
|
-
},
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
handleCancel(answer);
|
|
216
|
-
result.set(key, answer as string);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
logSuccess('Plugin credentials configured');
|
|
220
|
-
return result;
|
|
221
|
-
}
|
package/src/services.ts
DELETED
|
@@ -1,308 +0,0 @@
|
|
|
1
|
-
import { execa } from 'execa';
|
|
2
|
-
import chalk from 'chalk';
|
|
3
|
-
import { logInfo, logWarning, divider, printSummaryCard, printCommandHints } from './utils.js';
|
|
4
|
-
import { confirm } from '@clack/prompts';
|
|
5
|
-
import { handleCancel, spinner } from './ui.js';
|
|
6
|
-
import { HEALTH_CHECK, getServiceURL } from './config/constants.js';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Execute docker compose command with fallback to docker-compose.
|
|
10
|
-
* Tries 'docker compose' first (newer), then falls back to 'docker-compose' (legacy).
|
|
11
|
-
*/
|
|
12
|
-
async function execDockerCompose(args: string[], cwd: string): Promise<void> {
|
|
13
|
-
try {
|
|
14
|
-
// Try newer 'docker compose' first
|
|
15
|
-
await execa('docker', ['compose', ...args], { cwd });
|
|
16
|
-
} catch (error) {
|
|
17
|
-
// Fallback to legacy 'docker-compose'
|
|
18
|
-
await execa('docker-compose', args, { cwd });
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async function waitForContainerRunning(
|
|
23
|
-
containerName: string,
|
|
24
|
-
timeoutMs: number = 60_000,
|
|
25
|
-
intervalMs: number = 1_500
|
|
26
|
-
): Promise<void> {
|
|
27
|
-
const maxRetries = Math.ceil(timeoutMs / intervalMs);
|
|
28
|
-
|
|
29
|
-
for (let i = 0; i < maxRetries; i++) {
|
|
30
|
-
try {
|
|
31
|
-
const { stdout } = await execa('docker', [
|
|
32
|
-
'ps',
|
|
33
|
-
'--filter',
|
|
34
|
-
`name=^${containerName}$`,
|
|
35
|
-
'--filter',
|
|
36
|
-
'status=running',
|
|
37
|
-
'--format',
|
|
38
|
-
'{{.Names}}',
|
|
39
|
-
]);
|
|
40
|
-
|
|
41
|
-
const running = stdout
|
|
42
|
-
.split('\n')
|
|
43
|
-
.map(line => line.trim())
|
|
44
|
-
.filter(Boolean)
|
|
45
|
-
.includes(containerName);
|
|
46
|
-
|
|
47
|
-
if (running) {
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
} catch {
|
|
51
|
-
// continue polling
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
await sleep(intervalMs);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
throw new Error(`Container '${containerName}' did not reach running state within ${timeoutMs}ms`);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
type ComposeFile = 'docker-compose.yaml' | 'docker-compose.infra.yaml';
|
|
61
|
-
|
|
62
|
-
function withComposeFile(args: string[], composeFile?: ComposeFile): string[] {
|
|
63
|
-
if (!composeFile) {
|
|
64
|
-
return args;
|
|
65
|
-
}
|
|
66
|
-
return ['-f', composeFile, ...args];
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Prompts user whether to start the services immediately after setup.
|
|
71
|
-
*
|
|
72
|
-
* @returns `true` if user wants to start services, `false` otherwise
|
|
73
|
-
*/
|
|
74
|
-
export async function promptStartServices(): Promise<boolean> {
|
|
75
|
-
const shouldStart = await confirm({
|
|
76
|
-
message: 'Start services now after setup?',
|
|
77
|
-
initialValue: true,
|
|
78
|
-
withGuide: true,
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
handleCancel(shouldStart);
|
|
82
|
-
return shouldStart as boolean;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Starts infrastructure services using docker compose.
|
|
87
|
-
* Runs `docker compose -f docker-compose.infra.yaml up -d` first,
|
|
88
|
-
* then falls back to `docker-compose -f docker-compose.infra.yaml up -d`.
|
|
89
|
-
*
|
|
90
|
-
* @param targetDir - Directory containing docker-compose.infra.yaml
|
|
91
|
-
* @throws Error if both docker compose and docker-compose commands fail
|
|
92
|
-
*/
|
|
93
|
-
export async function startInfraServices(targetDir: string): Promise<void> {
|
|
94
|
-
logInfo('Starting infrastructure services...');
|
|
95
|
-
|
|
96
|
-
const s = spinner();
|
|
97
|
-
s.start('Starting docker-compose.infra.yaml...');
|
|
98
|
-
|
|
99
|
-
try {
|
|
100
|
-
await execDockerCompose(
|
|
101
|
-
['-f', 'docker-compose.infra.yaml', 'up', '-d'],
|
|
102
|
-
targetDir
|
|
103
|
-
);
|
|
104
|
-
s.stop('Infrastructure services started');
|
|
105
|
-
} catch (error: unknown) {
|
|
106
|
-
s.error('Failed to start infrastructure services');
|
|
107
|
-
throw error;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Waits for infrastructure services to become healthy.
|
|
113
|
-
* Polls Docker health checks for up to 60 seconds (30 retries x 2s).
|
|
114
|
-
*
|
|
115
|
-
* @param targetDir - Directory where services are running
|
|
116
|
-
*/
|
|
117
|
-
export async function waitForInfraHealth(targetDir: string): Promise<void> {
|
|
118
|
-
logInfo('Waiting for infrastructure services to be healthy...');
|
|
119
|
-
|
|
120
|
-
const s = spinner();
|
|
121
|
-
s.start('Checking service health...');
|
|
122
|
-
|
|
123
|
-
// Wait for mongo, redis health checks
|
|
124
|
-
const maxRetries = HEALTH_CHECK.MAX_RETRIES;
|
|
125
|
-
const retryDelay = HEALTH_CHECK.RETRY_DELAY_MS;
|
|
126
|
-
|
|
127
|
-
for (let i = 0; i < maxRetries; i++) {
|
|
128
|
-
try {
|
|
129
|
-
// Check if containers are healthy
|
|
130
|
-
const { stdout } = await execa('docker', ['ps', '--filter', 'health=healthy', '--format', '{{.Names}}']);
|
|
131
|
-
const healthyContainers = stdout.split('\n').filter(Boolean);
|
|
132
|
-
|
|
133
|
-
// Check for critical services
|
|
134
|
-
const hasMongoOrRedis = healthyContainers.some(name =>
|
|
135
|
-
name.includes('mongo') || name.includes('redis')
|
|
136
|
-
);
|
|
137
|
-
|
|
138
|
-
if (hasMongoOrRedis) {
|
|
139
|
-
s.stop('Infrastructure services are healthy');
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
s.message(`Waiting for services... (${i + 1}/${maxRetries})`);
|
|
144
|
-
await sleep(retryDelay);
|
|
145
|
-
} catch (error) {
|
|
146
|
-
s.message(`Checking health... (${i + 1}/${maxRetries})`);
|
|
147
|
-
await sleep(retryDelay);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
s.stop('Health check timed out, but services may still be starting');
|
|
152
|
-
logWarning('You may need to wait a bit longer for all services to be ready.');
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Start application services
|
|
157
|
-
*/
|
|
158
|
-
export async function startAppServices(targetDir: string): Promise<void> {
|
|
159
|
-
logInfo('Starting application services...');
|
|
160
|
-
|
|
161
|
-
const s = spinner();
|
|
162
|
-
s.start('Starting docker-compose.yaml...');
|
|
163
|
-
|
|
164
|
-
try {
|
|
165
|
-
await execDockerCompose(
|
|
166
|
-
['up', '-d'],
|
|
167
|
-
targetDir
|
|
168
|
-
);
|
|
169
|
-
s.stop('Application services started');
|
|
170
|
-
} catch (error: unknown) {
|
|
171
|
-
s.error('Failed to start application services');
|
|
172
|
-
throw error;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
export function getSslManualCommands(options: {
|
|
177
|
-
composeFile: ComposeFile;
|
|
178
|
-
domain: string;
|
|
179
|
-
email: string;
|
|
180
|
-
}): string[] {
|
|
181
|
-
const composeFlag = options.composeFile === 'docker-compose.infra.yaml'
|
|
182
|
-
? '-f docker-compose.infra.yaml '
|
|
183
|
-
: '';
|
|
184
|
-
|
|
185
|
-
return [
|
|
186
|
-
`docker compose ${composeFlag}up -d nginx certbot certbot-renew`,
|
|
187
|
-
`docker compose ${composeFlag}exec -T certbot certbot certonly --webroot -w /var/www/certbot --email ${options.email} --agree-tos --no-eff-email -d ${options.domain} --non-interactive`,
|
|
188
|
-
`docker compose ${composeFlag}exec -T nginx nginx -s reload`,
|
|
189
|
-
`docker compose ${composeFlag}up -d certbot-renew`,
|
|
190
|
-
];
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
export async function setupSslCertificates(targetDir: string, options: {
|
|
194
|
-
composeFile: ComposeFile;
|
|
195
|
-
domain: string;
|
|
196
|
-
email: string;
|
|
197
|
-
}): Promise<void> {
|
|
198
|
-
logInfo(`Setting up SSL certificate for ${options.domain}...`);
|
|
199
|
-
|
|
200
|
-
const s = spinner();
|
|
201
|
-
const composeArgs = (args: string[]) => withComposeFile(args, options.composeFile);
|
|
202
|
-
|
|
203
|
-
s.start('Ensuring nginx/certbot services are running...');
|
|
204
|
-
await execDockerCompose(composeArgs(['up', '-d', 'nginx', 'certbot']), targetDir);
|
|
205
|
-
await waitForContainerRunning('nginx');
|
|
206
|
-
await waitForContainerRunning('certbot');
|
|
207
|
-
s.stop('Nginx and certbot services are running');
|
|
208
|
-
|
|
209
|
-
s.start('Requesting initial certificate from Let\'s Encrypt...');
|
|
210
|
-
await execDockerCompose(
|
|
211
|
-
composeArgs([
|
|
212
|
-
'exec',
|
|
213
|
-
'-T',
|
|
214
|
-
'certbot',
|
|
215
|
-
'certbot',
|
|
216
|
-
'certonly',
|
|
217
|
-
'--webroot',
|
|
218
|
-
'-w',
|
|
219
|
-
'/var/www/certbot',
|
|
220
|
-
'--email',
|
|
221
|
-
options.email,
|
|
222
|
-
'--agree-tos',
|
|
223
|
-
'--no-eff-email',
|
|
224
|
-
'-d',
|
|
225
|
-
options.domain,
|
|
226
|
-
'--non-interactive',
|
|
227
|
-
]),
|
|
228
|
-
targetDir
|
|
229
|
-
);
|
|
230
|
-
s.stop('Initial certificate issued');
|
|
231
|
-
|
|
232
|
-
s.start('Reloading nginx to apply certificates...');
|
|
233
|
-
await execDockerCompose(composeArgs(['exec', '-T', 'nginx', 'nginx', '-s', 'reload']), targetDir);
|
|
234
|
-
s.stop('Nginx reloaded');
|
|
235
|
-
|
|
236
|
-
s.start('Starting automatic certificate renewal service...');
|
|
237
|
-
await execDockerCompose(composeArgs(['up', '-d', 'certbot-renew']), targetDir);
|
|
238
|
-
s.stop('Certificate auto-renewal service started');
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
export async function reloadNginxConfiguration(targetDir: string, options: {
|
|
242
|
-
composeFile: ComposeFile;
|
|
243
|
-
}): Promise<void> {
|
|
244
|
-
const composeArgs = (args: string[]) => withComposeFile(args, options.composeFile);
|
|
245
|
-
await execDockerCompose(composeArgs(['exec', '-T', 'nginx', 'nginx', '-t']), targetDir);
|
|
246
|
-
await execDockerCompose(composeArgs(['exec', '-T', 'nginx', 'nginx', '-s', 'reload']), targetDir);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Display service status and URLs
|
|
251
|
-
*/
|
|
252
|
-
export async function displayServiceStatus(): Promise<void> {
|
|
253
|
-
console.log(`\n${divider('green', '═')}`);
|
|
254
|
-
console.log(chalk.greenBright(chalk.bold('Services Started')));
|
|
255
|
-
console.log(divider('green', '═'));
|
|
256
|
-
|
|
257
|
-
try {
|
|
258
|
-
// Get running containers
|
|
259
|
-
const { stdout } = await execa('docker', ['ps', '--format', '{{.Names}}']);
|
|
260
|
-
const containers = stdout.split('\n').filter(Boolean).sort();
|
|
261
|
-
|
|
262
|
-
const accessRows: Array<{ label: string; value: string }> = [];
|
|
263
|
-
|
|
264
|
-
// Display URLs for known services
|
|
265
|
-
if (containers.some(c => c.includes('api'))) {
|
|
266
|
-
accessRows.push({ label: 'API Server', value: getServiceURL('API') });
|
|
267
|
-
accessRows.push({ label: 'API Health', value: `${getServiceURL('API')}/health` });
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
if (containers.some(c => c.includes('dashboard'))) {
|
|
271
|
-
accessRows.push({ label: 'Dashboard', value: getServiceURL('DASHBOARD') });
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
if (containers.some(c => c.includes('kafka-ui'))) {
|
|
275
|
-
accessRows.push({ label: 'Kafka UI', value: getServiceURL('KAFKA_UI') });
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
if (containers.some(c => c.includes('grafana'))) {
|
|
279
|
-
accessRows.push({ label: 'Grafana', value: `${getServiceURL('GRAFANA')} (admin/admin)` });
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if (accessRows.length > 0) {
|
|
283
|
-
printSummaryCard('Access URLs', accessRows);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
console.log(chalk.cyanBright('Running Containers'));
|
|
287
|
-
console.log(divider());
|
|
288
|
-
for (const container of containers) {
|
|
289
|
-
console.log(` ${chalk.greenBright('•')} ${container}`);
|
|
290
|
-
}
|
|
291
|
-
console.log('');
|
|
292
|
-
|
|
293
|
-
printCommandHints('Helpful commands', [
|
|
294
|
-
'docker compose logs -f',
|
|
295
|
-
'docker compose down',
|
|
296
|
-
]);
|
|
297
|
-
console.log(`${divider('green', '═')}\n`);
|
|
298
|
-
} catch (error) {
|
|
299
|
-
logWarning('Could not fetch container status');
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* Helper: Sleep for specified milliseconds
|
|
305
|
-
*/
|
|
306
|
-
function sleep(ms: number): Promise<void> {
|
|
307
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
308
|
-
}
|