suthep 0.1.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/.editorconfig +17 -0
- package/.prettierignore +6 -0
- package/.prettierrc +7 -0
- package/.vscode/settings.json +19 -0
- package/LICENSE +21 -0
- package/README.md +214 -0
- package/dist/commands/deploy.js +104 -0
- package/dist/commands/deploy.js.map +1 -0
- package/dist/commands/init.js +188 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/setup.js +90 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/utils/certbot.js +27 -0
- package/dist/utils/certbot.js.map +1 -0
- package/dist/utils/config-loader.js +65 -0
- package/dist/utils/config-loader.js.map +1 -0
- package/dist/utils/deployment.js +52 -0
- package/dist/utils/deployment.js.map +1 -0
- package/dist/utils/docker.js +57 -0
- package/dist/utils/docker.js.map +1 -0
- package/dist/utils/nginx.js +154 -0
- package/dist/utils/nginx.js.map +1 -0
- package/docs/README.md +62 -0
- package/docs/api-reference.md +545 -0
- package/docs/architecture.md +367 -0
- package/docs/commands.md +273 -0
- package/docs/configuration.md +347 -0
- package/docs/examples.md +537 -0
- package/docs/getting-started.md +197 -0
- package/docs/troubleshooting.md +441 -0
- package/example/README.md +81 -0
- package/example/docker-compose.yml +72 -0
- package/example/suthep.yml +31 -0
- package/package.json +45 -0
- package/src/commands/deploy.ts +133 -0
- package/src/commands/init.ts +214 -0
- package/src/commands/setup.ts +112 -0
- package/src/index.ts +34 -0
- package/src/types/config.ts +51 -0
- package/src/utils/certbot.ts +82 -0
- package/src/utils/config-loader.ts +81 -0
- package/src/utils/deployment.ts +132 -0
- package/src/utils/docker.ts +151 -0
- package/src/utils/nginx.ts +143 -0
- package/suthep.example.yml +69 -0
- package/todo.md +6 -0
- package/tsconfig.json +26 -0
- package/vite.config.ts +46 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import fs from 'fs-extra'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import { requestCertificate } from '../utils/certbot'
|
|
5
|
+
import { loadConfig } from '../utils/config-loader'
|
|
6
|
+
import { deployService, performHealthCheck } from '../utils/deployment'
|
|
7
|
+
import { startDockerContainer } from '../utils/docker'
|
|
8
|
+
import { enableSite, generateNginxConfig, reloadNginx } from '../utils/nginx'
|
|
9
|
+
|
|
10
|
+
interface DeployOptions {
|
|
11
|
+
file: string
|
|
12
|
+
https: boolean
|
|
13
|
+
nginx: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function deployCommand(options: DeployOptions): Promise<void> {
|
|
17
|
+
console.log(chalk.blue.bold('\nš Deploying Services\n'))
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
// Load configuration
|
|
21
|
+
if (!(await fs.pathExists(options.file))) {
|
|
22
|
+
throw new Error(`Configuration file not found: ${options.file}`)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
console.log(chalk.cyan(`š Loading configuration from ${options.file}...`))
|
|
26
|
+
const config = await loadConfig(options.file)
|
|
27
|
+
|
|
28
|
+
console.log(chalk.green(`ā
Configuration loaded for project: ${config.project.name}`))
|
|
29
|
+
console.log(chalk.dim(` Services: ${config.services.map((s) => s.name).join(', ')}\n`))
|
|
30
|
+
|
|
31
|
+
// Deploy each service
|
|
32
|
+
for (const service of config.services) {
|
|
33
|
+
console.log(chalk.cyan(`\nš¦ Deploying service: ${service.name}`))
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
// Start Docker container if configured
|
|
37
|
+
if (service.docker) {
|
|
38
|
+
console.log(chalk.dim(' š³ Managing Docker container...'))
|
|
39
|
+
await startDockerContainer(service)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Deploy the service
|
|
43
|
+
await deployService(service, config.deployment)
|
|
44
|
+
|
|
45
|
+
// Generate and configure Nginx
|
|
46
|
+
if (options.nginx) {
|
|
47
|
+
console.log(chalk.dim(' āļø Configuring Nginx reverse proxy...'))
|
|
48
|
+
const nginxConfigContent = generateNginxConfig(service, false)
|
|
49
|
+
const nginxConfigPath = path.join(config.nginx.configPath, `${service.name}.conf`)
|
|
50
|
+
|
|
51
|
+
await fs.writeFile(nginxConfigPath, nginxConfigContent)
|
|
52
|
+
await enableSite(service.name, config.nginx.configPath)
|
|
53
|
+
|
|
54
|
+
console.log(chalk.green(` ā
Nginx configured for ${service.domains.join(', ')}`))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Setup HTTPS with Certbot
|
|
58
|
+
if (options.https && service.domains.length > 0) {
|
|
59
|
+
console.log(chalk.dim(' š Setting up HTTPS certificates...'))
|
|
60
|
+
|
|
61
|
+
for (const domain of service.domains) {
|
|
62
|
+
try {
|
|
63
|
+
await requestCertificate(domain, config.certbot.email, config.certbot.staging)
|
|
64
|
+
console.log(chalk.green(` ā
SSL certificate obtained for ${domain}`))
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.log(
|
|
67
|
+
chalk.yellow(
|
|
68
|
+
` ā ļø Failed to obtain SSL for ${domain}: ${
|
|
69
|
+
error instanceof Error ? error.message : error
|
|
70
|
+
}`
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Update Nginx config with HTTPS
|
|
77
|
+
if (options.nginx) {
|
|
78
|
+
const nginxConfigContent = generateNginxConfig(service, true)
|
|
79
|
+
const nginxConfigPath = path.join(config.nginx.configPath, `${service.name}.conf`)
|
|
80
|
+
await fs.writeFile(nginxConfigPath, nginxConfigContent)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Reload Nginx after all configurations
|
|
85
|
+
if (options.nginx) {
|
|
86
|
+
console.log(chalk.dim(' š Reloading Nginx...'))
|
|
87
|
+
await reloadNginx(config.nginx.reloadCommand)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Perform health check
|
|
91
|
+
if (service.healthCheck) {
|
|
92
|
+
console.log(chalk.dim(` š„ Performing health check...`))
|
|
93
|
+
const isHealthy = await performHealthCheck(
|
|
94
|
+
`http://localhost:${service.port}${service.healthCheck.path}`,
|
|
95
|
+
config.deployment.healthCheckTimeout
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if (isHealthy) {
|
|
99
|
+
console.log(chalk.green(` ā
Service ${service.name} is healthy`))
|
|
100
|
+
} else {
|
|
101
|
+
throw new Error(`Health check failed for service ${service.name}`)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
console.log(chalk.green.bold(`\n⨠Service ${service.name} deployed successfully!`))
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error(
|
|
108
|
+
chalk.red(`\nā Failed to deploy service ${service.name}:`),
|
|
109
|
+
error instanceof Error ? error.message : error
|
|
110
|
+
)
|
|
111
|
+
throw error
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
console.log(chalk.green.bold('\nš All services deployed successfully!\n'))
|
|
116
|
+
|
|
117
|
+
// Print service URLs
|
|
118
|
+
console.log(chalk.cyan('š Service URLs:'))
|
|
119
|
+
for (const service of config.services) {
|
|
120
|
+
for (const domain of service.domains) {
|
|
121
|
+
const protocol = options.https ? 'https' : 'http'
|
|
122
|
+
console.log(chalk.dim(` ${service.name}: ${protocol}://${domain}`))
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
console.log()
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.error(
|
|
128
|
+
chalk.red('\nā Deployment failed:'),
|
|
129
|
+
error instanceof Error ? error.message : error
|
|
130
|
+
)
|
|
131
|
+
process.exit(1)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import fs from 'fs-extra'
|
|
3
|
+
import inquirer from 'inquirer'
|
|
4
|
+
import type { DeployConfig } from '../types/config'
|
|
5
|
+
import { saveConfig } from '../utils/config-loader'
|
|
6
|
+
|
|
7
|
+
interface InitOptions {
|
|
8
|
+
file: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function initCommand(options: InitOptions): Promise<void> {
|
|
12
|
+
console.log(chalk.blue.bold('\nš Suthep Deployment Configuration\n'))
|
|
13
|
+
|
|
14
|
+
// Check if file already exists
|
|
15
|
+
if (await fs.pathExists(options.file)) {
|
|
16
|
+
const { overwrite } = await inquirer.prompt([
|
|
17
|
+
{
|
|
18
|
+
type: 'confirm',
|
|
19
|
+
name: 'overwrite',
|
|
20
|
+
message: `File ${options.file} already exists. Overwrite?`,
|
|
21
|
+
default: false,
|
|
22
|
+
},
|
|
23
|
+
])
|
|
24
|
+
|
|
25
|
+
if (!overwrite) {
|
|
26
|
+
console.log(chalk.yellow('Aborted.'))
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Gather project information
|
|
32
|
+
const projectAnswers = await inquirer.prompt([
|
|
33
|
+
{
|
|
34
|
+
type: 'input',
|
|
35
|
+
name: 'projectName',
|
|
36
|
+
message: 'Project name:',
|
|
37
|
+
default: 'my-app',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
type: 'input',
|
|
41
|
+
name: 'projectVersion',
|
|
42
|
+
message: 'Project version:',
|
|
43
|
+
default: '1.0.0',
|
|
44
|
+
},
|
|
45
|
+
])
|
|
46
|
+
|
|
47
|
+
// Gather service information
|
|
48
|
+
const services = []
|
|
49
|
+
let addMoreServices = true
|
|
50
|
+
|
|
51
|
+
while (addMoreServices) {
|
|
52
|
+
console.log(chalk.cyan(`\nš¦ Service ${services.length + 1} Configuration`))
|
|
53
|
+
|
|
54
|
+
const serviceAnswers = await inquirer.prompt([
|
|
55
|
+
{
|
|
56
|
+
type: 'input',
|
|
57
|
+
name: 'name',
|
|
58
|
+
message: 'Service name:',
|
|
59
|
+
validate: (input) => input.trim() !== '' || 'Service name is required',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
type: 'number',
|
|
63
|
+
name: 'port',
|
|
64
|
+
message: 'Service port:',
|
|
65
|
+
default: 3000,
|
|
66
|
+
validate: (input: number | undefined) => {
|
|
67
|
+
if (input === undefined) return 'Port is required'
|
|
68
|
+
return (input > 0 && input < 65536) || 'Port must be between 1 and 65535'
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
type: 'input',
|
|
73
|
+
name: 'domains',
|
|
74
|
+
message: 'Domain names (comma-separated):',
|
|
75
|
+
validate: (input) => input.trim() !== '' || 'At least one domain is required',
|
|
76
|
+
filter: (input: string) => input.split(',').map((d: string) => d.trim()),
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
type: 'confirm',
|
|
80
|
+
name: 'useDocker',
|
|
81
|
+
message: 'Use Docker?',
|
|
82
|
+
default: false,
|
|
83
|
+
},
|
|
84
|
+
])
|
|
85
|
+
|
|
86
|
+
// Docker configuration
|
|
87
|
+
let dockerConfig = undefined
|
|
88
|
+
if (serviceAnswers.useDocker) {
|
|
89
|
+
const dockerAnswers = await inquirer.prompt([
|
|
90
|
+
{
|
|
91
|
+
type: 'input',
|
|
92
|
+
name: 'image',
|
|
93
|
+
message: 'Docker image (leave empty to connect to existing container):',
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
type: 'input',
|
|
97
|
+
name: 'container',
|
|
98
|
+
message: 'Container name:',
|
|
99
|
+
validate: (input) => input.trim() !== '' || 'Container name is required',
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
type: 'number',
|
|
103
|
+
name: 'port',
|
|
104
|
+
message: 'Container port:',
|
|
105
|
+
default: serviceAnswers.port,
|
|
106
|
+
},
|
|
107
|
+
])
|
|
108
|
+
|
|
109
|
+
dockerConfig = {
|
|
110
|
+
image: dockerAnswers.image || undefined,
|
|
111
|
+
container: dockerAnswers.container,
|
|
112
|
+
port: dockerAnswers.port,
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Health check configuration
|
|
117
|
+
const { addHealthCheck } = await inquirer.prompt([
|
|
118
|
+
{
|
|
119
|
+
type: 'confirm',
|
|
120
|
+
name: 'addHealthCheck',
|
|
121
|
+
message: 'Add health check?',
|
|
122
|
+
default: true,
|
|
123
|
+
},
|
|
124
|
+
])
|
|
125
|
+
|
|
126
|
+
let healthCheck = undefined
|
|
127
|
+
if (addHealthCheck) {
|
|
128
|
+
const healthCheckAnswers = await inquirer.prompt([
|
|
129
|
+
{
|
|
130
|
+
type: 'input',
|
|
131
|
+
name: 'path',
|
|
132
|
+
message: 'Health check path:',
|
|
133
|
+
default: '/health',
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
type: 'number',
|
|
137
|
+
name: 'interval',
|
|
138
|
+
message: 'Health check interval (seconds):',
|
|
139
|
+
default: 30,
|
|
140
|
+
},
|
|
141
|
+
])
|
|
142
|
+
|
|
143
|
+
healthCheck = healthCheckAnswers
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
services.push({
|
|
147
|
+
name: serviceAnswers.name,
|
|
148
|
+
port: serviceAnswers.port,
|
|
149
|
+
domains: serviceAnswers.domains,
|
|
150
|
+
docker: dockerConfig,
|
|
151
|
+
healthCheck,
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
const { addMore } = await inquirer.prompt([
|
|
155
|
+
{
|
|
156
|
+
type: 'confirm',
|
|
157
|
+
name: 'addMore',
|
|
158
|
+
message: 'Add another service?',
|
|
159
|
+
default: false,
|
|
160
|
+
},
|
|
161
|
+
])
|
|
162
|
+
|
|
163
|
+
addMoreServices = addMore
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Certbot configuration
|
|
167
|
+
const certbotAnswers = await inquirer.prompt([
|
|
168
|
+
{
|
|
169
|
+
type: 'input',
|
|
170
|
+
name: 'email',
|
|
171
|
+
message: 'Email for SSL certificates:',
|
|
172
|
+
validate: (input) => {
|
|
173
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
174
|
+
return emailRegex.test(input) || 'Please enter a valid email address'
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
type: 'confirm',
|
|
179
|
+
name: 'staging',
|
|
180
|
+
message: 'Use Certbot staging environment? (for testing)',
|
|
181
|
+
default: false,
|
|
182
|
+
},
|
|
183
|
+
])
|
|
184
|
+
|
|
185
|
+
// Build configuration object
|
|
186
|
+
const config: DeployConfig = {
|
|
187
|
+
project: {
|
|
188
|
+
name: projectAnswers.projectName,
|
|
189
|
+
version: projectAnswers.projectVersion,
|
|
190
|
+
},
|
|
191
|
+
services,
|
|
192
|
+
nginx: {
|
|
193
|
+
configPath: '/etc/nginx/sites-available',
|
|
194
|
+
reloadCommand: 'sudo nginx -t && sudo systemctl reload nginx',
|
|
195
|
+
},
|
|
196
|
+
certbot: {
|
|
197
|
+
email: certbotAnswers.email,
|
|
198
|
+
staging: certbotAnswers.staging,
|
|
199
|
+
},
|
|
200
|
+
deployment: {
|
|
201
|
+
strategy: 'rolling',
|
|
202
|
+
healthCheckTimeout: 30000,
|
|
203
|
+
},
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Save configuration
|
|
207
|
+
await saveConfig(options.file, config)
|
|
208
|
+
|
|
209
|
+
console.log(chalk.green(`\nā
Configuration saved to ${options.file}`))
|
|
210
|
+
console.log(chalk.dim('\nNext steps:'))
|
|
211
|
+
console.log(chalk.dim(` 1. Review and edit ${options.file} if needed`))
|
|
212
|
+
console.log(chalk.dim(' 2. Run: suthep setup'))
|
|
213
|
+
console.log(chalk.dim(' 3. Run: suthep deploy\n'))
|
|
214
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { execa } from 'execa';
|
|
3
|
+
|
|
4
|
+
interface SetupOptions {
|
|
5
|
+
nginxOnly?: boolean;
|
|
6
|
+
certbotOnly?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function setupCommand(options: SetupOptions): Promise<void> {
|
|
10
|
+
console.log(chalk.blue.bold('\nš§ Setting up prerequisites\n'));
|
|
11
|
+
|
|
12
|
+
const setupNginx = !options.certbotOnly;
|
|
13
|
+
const setupCertbot = !options.nginxOnly;
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
// Setup Nginx
|
|
17
|
+
if (setupNginx) {
|
|
18
|
+
console.log(chalk.cyan('š¦ Installing Nginx...'));
|
|
19
|
+
|
|
20
|
+
// Check if Nginx is already installed
|
|
21
|
+
try {
|
|
22
|
+
await execa('nginx', ['-v']);
|
|
23
|
+
console.log(chalk.green('ā
Nginx is already installed'));
|
|
24
|
+
} catch {
|
|
25
|
+
// Install Nginx based on OS
|
|
26
|
+
const platform = process.platform;
|
|
27
|
+
|
|
28
|
+
if (platform === 'linux') {
|
|
29
|
+
// Detect Linux distribution
|
|
30
|
+
try {
|
|
31
|
+
await execa('apt-get', ['--version']);
|
|
32
|
+
console.log(chalk.dim('Using apt-get...'));
|
|
33
|
+
await execa('sudo', ['apt-get', 'update'], { stdio: 'inherit' });
|
|
34
|
+
await execa('sudo', ['apt-get', 'install', '-y', 'nginx'], { stdio: 'inherit' });
|
|
35
|
+
} catch {
|
|
36
|
+
try {
|
|
37
|
+
await execa('yum', ['--version']);
|
|
38
|
+
console.log(chalk.dim('Using yum...'));
|
|
39
|
+
await execa('sudo', ['yum', 'install', '-y', 'nginx'], { stdio: 'inherit' });
|
|
40
|
+
} catch {
|
|
41
|
+
throw new Error('Unsupported Linux distribution. Please install Nginx manually.');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
} else if (platform === 'darwin') {
|
|
45
|
+
console.log(chalk.dim('Using Homebrew...'));
|
|
46
|
+
await execa('brew', ['install', 'nginx'], { stdio: 'inherit' });
|
|
47
|
+
} else {
|
|
48
|
+
throw new Error(`Unsupported platform: ${platform}. Please install Nginx manually.`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.log(chalk.green('ā
Nginx installed successfully'));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Start Nginx service
|
|
55
|
+
console.log(chalk.cyan('š Starting Nginx service...'));
|
|
56
|
+
try {
|
|
57
|
+
await execa('sudo', ['systemctl', 'start', 'nginx']);
|
|
58
|
+
await execa('sudo', ['systemctl', 'enable', 'nginx']);
|
|
59
|
+
console.log(chalk.green('ā
Nginx service started'));
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.log(chalk.yellow('ā ļø Could not start Nginx via systemctl (might not be available)'));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Setup Certbot
|
|
66
|
+
if (setupCertbot) {
|
|
67
|
+
console.log(chalk.cyan('\nš Installing Certbot...'));
|
|
68
|
+
|
|
69
|
+
// Check if Certbot is already installed
|
|
70
|
+
try {
|
|
71
|
+
await execa('certbot', ['--version']);
|
|
72
|
+
console.log(chalk.green('ā
Certbot is already installed'));
|
|
73
|
+
} catch {
|
|
74
|
+
const platform = process.platform;
|
|
75
|
+
|
|
76
|
+
if (platform === 'linux') {
|
|
77
|
+
// Install Certbot based on package manager
|
|
78
|
+
try {
|
|
79
|
+
await execa('apt-get', ['--version']);
|
|
80
|
+
console.log(chalk.dim('Using apt-get...'));
|
|
81
|
+
await execa('sudo', ['apt-get', 'update'], { stdio: 'inherit' });
|
|
82
|
+
await execa('sudo', ['apt-get', 'install', '-y', 'certbot', 'python3-certbot-nginx'], { stdio: 'inherit' });
|
|
83
|
+
} catch {
|
|
84
|
+
try {
|
|
85
|
+
await execa('yum', ['--version']);
|
|
86
|
+
console.log(chalk.dim('Using yum...'));
|
|
87
|
+
await execa('sudo', ['yum', 'install', '-y', 'certbot', 'python3-certbot-nginx'], { stdio: 'inherit' });
|
|
88
|
+
} catch {
|
|
89
|
+
throw new Error('Unsupported Linux distribution. Please install Certbot manually.');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} else if (platform === 'darwin') {
|
|
93
|
+
console.log(chalk.dim('Using Homebrew...'));
|
|
94
|
+
await execa('brew', ['install', 'certbot'], { stdio: 'inherit' });
|
|
95
|
+
} else {
|
|
96
|
+
throw new Error(`Unsupported platform: ${platform}. Please install Certbot manually.`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
console.log(chalk.green('ā
Certbot installed successfully'));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
console.log(chalk.green.bold('\n⨠Setup completed successfully!\n'));
|
|
104
|
+
console.log(chalk.dim('Next steps:'));
|
|
105
|
+
console.log(chalk.dim(' 1. Create a configuration file: suthep init'));
|
|
106
|
+
console.log(chalk.dim(' 2. Deploy your services: suthep deploy\n'));
|
|
107
|
+
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error(chalk.red('\nā Setup failed:'), error instanceof Error ? error.message : error);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import { deployCommand } from './commands/deploy'
|
|
3
|
+
import { initCommand } from './commands/init'
|
|
4
|
+
import { setupCommand } from './commands/setup'
|
|
5
|
+
|
|
6
|
+
const program = new Command()
|
|
7
|
+
|
|
8
|
+
program
|
|
9
|
+
.name('suthep')
|
|
10
|
+
.description('CLI tool for deploying projects with automatic Nginx reverse proxy and HTTPS setup')
|
|
11
|
+
.version('0.1.0')
|
|
12
|
+
|
|
13
|
+
program
|
|
14
|
+
.command('init')
|
|
15
|
+
.description('Initialize a new deployment configuration file')
|
|
16
|
+
.option('-f, --file <path>', 'Configuration file path', 'suthep.yml')
|
|
17
|
+
.action(initCommand)
|
|
18
|
+
|
|
19
|
+
program
|
|
20
|
+
.command('setup')
|
|
21
|
+
.description('Setup Nginx and Certbot on the system')
|
|
22
|
+
.option('--nginx-only', 'Only setup Nginx')
|
|
23
|
+
.option('--certbot-only', 'Only setup Certbot')
|
|
24
|
+
.action(setupCommand)
|
|
25
|
+
|
|
26
|
+
program
|
|
27
|
+
.command('deploy')
|
|
28
|
+
.description('Deploy a project using the configuration file')
|
|
29
|
+
.option('-f, --file <path>', 'Configuration file path', 'suthep.yml')
|
|
30
|
+
.option('--no-https', 'Skip HTTPS setup')
|
|
31
|
+
.option('--no-nginx', 'Skip Nginx configuration')
|
|
32
|
+
.action(deployCommand)
|
|
33
|
+
|
|
34
|
+
program.parse()
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration type definitions for the Suthep deployment tool
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface ProjectConfig {
|
|
6
|
+
name: string;
|
|
7
|
+
version: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface HealthCheckConfig {
|
|
11
|
+
path: string;
|
|
12
|
+
interval: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface DockerConfig {
|
|
16
|
+
image?: string;
|
|
17
|
+
container: string;
|
|
18
|
+
port: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ServiceConfig {
|
|
22
|
+
name: string;
|
|
23
|
+
port: number;
|
|
24
|
+
domains: string[];
|
|
25
|
+
docker?: DockerConfig;
|
|
26
|
+
healthCheck?: HealthCheckConfig;
|
|
27
|
+
environment?: Record<string, string>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface NginxConfig {
|
|
31
|
+
configPath: string;
|
|
32
|
+
reloadCommand: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CertbotConfig {
|
|
36
|
+
email: string;
|
|
37
|
+
staging: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface DeploymentConfig {
|
|
41
|
+
strategy: 'rolling' | 'blue-green';
|
|
42
|
+
healthCheckTimeout: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface DeployConfig {
|
|
46
|
+
project: ProjectConfig;
|
|
47
|
+
services: ServiceConfig[];
|
|
48
|
+
nginx: NginxConfig;
|
|
49
|
+
certbot: CertbotConfig;
|
|
50
|
+
deployment: DeploymentConfig;
|
|
51
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { execa } from 'execa'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Request an SSL certificate from Let's Encrypt using Certbot
|
|
5
|
+
*/
|
|
6
|
+
export async function requestCertificate(
|
|
7
|
+
domain: string,
|
|
8
|
+
email: string,
|
|
9
|
+
staging: boolean = false
|
|
10
|
+
): Promise<void> {
|
|
11
|
+
const args = [
|
|
12
|
+
'certonly',
|
|
13
|
+
'--nginx',
|
|
14
|
+
'-d',
|
|
15
|
+
domain,
|
|
16
|
+
'--non-interactive',
|
|
17
|
+
'--agree-tos',
|
|
18
|
+
'--email',
|
|
19
|
+
email,
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
if (staging) {
|
|
23
|
+
args.push('--staging')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
await execa('sudo', ['certbot', ...args])
|
|
28
|
+
} catch (error) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`Failed to obtain SSL certificate for ${domain}: ${
|
|
31
|
+
error instanceof Error ? error.message : error
|
|
32
|
+
}`
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Renew all SSL certificates
|
|
39
|
+
*/
|
|
40
|
+
export async function renewCertificates(): Promise<void> {
|
|
41
|
+
try {
|
|
42
|
+
await execa('sudo', ['certbot', 'renew', '--quiet'])
|
|
43
|
+
} catch (error) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`Failed to renew SSL certificates: ${error instanceof Error ? error.message : error}`
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Check certificate expiration for a domain
|
|
52
|
+
*/
|
|
53
|
+
export async function checkCertificateExpiration(domain: string): Promise<Date | null> {
|
|
54
|
+
try {
|
|
55
|
+
const { stdout } = await execa('sudo', ['certbot', 'certificates', '-d', domain])
|
|
56
|
+
|
|
57
|
+
// Parse expiration date from output
|
|
58
|
+
const expiryMatch = stdout.match(/Expiry Date: ([^\n]+)/)
|
|
59
|
+
if (expiryMatch) {
|
|
60
|
+
return new Date(expiryMatch[1])
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return null
|
|
64
|
+
} catch (error) {
|
|
65
|
+
return null
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Revoke a certificate for a domain
|
|
71
|
+
*/
|
|
72
|
+
export async function revokeCertificate(domain: string): Promise<void> {
|
|
73
|
+
try {
|
|
74
|
+
await execa('sudo', ['certbot', 'revoke', '-d', domain, '--non-interactive'])
|
|
75
|
+
} catch (error) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`Failed to revoke certificate for ${domain}: ${
|
|
78
|
+
error instanceof Error ? error.message : error
|
|
79
|
+
}`
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
}
|