suthep 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +17 -0
- package/.github/workflows/publish.yml +42 -0
- package/.prettierignore +6 -0
- package/.prettierrc +7 -0
- package/.scannerwork/.sonar_lock +0 -0
- package/.scannerwork/report-task.txt +6 -0
- package/.vscode/settings.json +19 -0
- package/LICENSE +21 -0
- package/README.md +317 -0
- package/dist/commands/deploy.js +371 -0
- package/dist/commands/deploy.js.map +1 -0
- package/dist/commands/down.js +179 -0
- package/dist/commands/down.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/commands/up.js +213 -0
- package/dist/commands/up.js.map +1 -0
- package/dist/index.js +66 -0
- package/dist/index.js.map +1 -0
- package/dist/utils/certbot.js +64 -0
- package/dist/utils/certbot.js.map +1 -0
- package/dist/utils/config-loader.js +127 -0
- package/dist/utils/config-loader.js.map +1 -0
- package/dist/utils/deployment.js +85 -0
- package/dist/utils/deployment.js.map +1 -0
- package/dist/utils/docker.js +425 -0
- package/dist/utils/docker.js.map +1 -0
- package/dist/utils/env-loader.js +53 -0
- package/dist/utils/env-loader.js.map +1 -0
- package/dist/utils/nginx.js +378 -0
- package/dist/utils/nginx.js.map +1 -0
- package/docs/README.md +38 -0
- package/docs/english/01-introduction.md +84 -0
- package/docs/english/02-installation.md +200 -0
- package/docs/english/03-quick-start.md +258 -0
- package/docs/english/04-configuration.md +433 -0
- package/docs/english/05-commands.md +336 -0
- package/docs/english/06-examples.md +456 -0
- package/docs/english/07-troubleshooting.md +417 -0
- package/docs/english/08-advanced.md +411 -0
- package/docs/english/README.md +48 -0
- package/docs/thai/01-introduction.md +84 -0
- package/docs/thai/02-installation.md +200 -0
- package/docs/thai/03-quick-start.md +258 -0
- package/docs/thai/04-configuration.md +433 -0
- package/docs/thai/05-commands.md +336 -0
- package/docs/thai/06-examples.md +456 -0
- package/docs/thai/07-troubleshooting.md +417 -0
- package/docs/thai/08-advanced.md +411 -0
- package/docs/thai/README.md +48 -0
- package/example/suthep-complete.yml +103 -0
- package/example/suthep-docker-only.yml +71 -0
- package/example/suthep-env-example.yml +113 -0
- package/example/suthep-no-docker.yml +51 -0
- package/example/suthep-path-routing.yml +62 -0
- package/example/suthep.example.yml +88 -0
- package/package.json +51 -0
- package/src/commands/deploy.ts +488 -0
- package/src/commands/down.ts +240 -0
- package/src/commands/init.ts +214 -0
- package/src/commands/setup.ts +112 -0
- package/src/commands/up.ts +271 -0
- package/src/index.ts +109 -0
- package/src/types/config.ts +52 -0
- package/src/utils/__tests__/certbot.test.ts +222 -0
- package/src/utils/__tests__/config-loader.test.ts +419 -0
- package/src/utils/__tests__/deployment.test.ts +243 -0
- package/src/utils/__tests__/nginx.test.ts +412 -0
- package/src/utils/certbot.ts +144 -0
- package/src/utils/config-loader.ts +184 -0
- package/src/utils/deployment.ts +157 -0
- package/src/utils/docker.ts +768 -0
- package/src/utils/env-loader.ts +135 -0
- package/src/utils/nginx.ts +443 -0
- package/suthep-1.0.0.tgz +0 -0
- package/suthep.example.yml +98 -0
- package/suthep.yml +39 -0
- package/todo.md +6 -0
- package/tsconfig.json +26 -0
- package/vite.config.ts +46 -0
- package/vitest.config.ts +21 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import fs from 'fs-extra'
|
|
3
|
+
import type { ServiceConfig } from '../types/config'
|
|
4
|
+
import { loadConfig } from '../utils/config-loader'
|
|
5
|
+
import { isContainerRunning, removeDockerContainer, stopDockerContainer } from '../utils/docker'
|
|
6
|
+
import {
|
|
7
|
+
disableSite,
|
|
8
|
+
enableSite,
|
|
9
|
+
generateMultiServiceNginxConfig,
|
|
10
|
+
generateNginxConfig,
|
|
11
|
+
reloadNginx,
|
|
12
|
+
writeNginxConfig,
|
|
13
|
+
} from '../utils/nginx'
|
|
14
|
+
|
|
15
|
+
interface DownOptions {
|
|
16
|
+
file: string
|
|
17
|
+
all: boolean
|
|
18
|
+
serviceName?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function downCommand(options: DownOptions): Promise<void> {
|
|
22
|
+
console.log(chalk.blue.bold('\nš Bringing Down Services\n'))
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
// Load configuration
|
|
26
|
+
if (!(await fs.pathExists(options.file))) {
|
|
27
|
+
throw new Error(`Configuration file not found: ${options.file}`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
console.log(chalk.cyan(`š Loading configuration from ${options.file}...`))
|
|
31
|
+
const config = await loadConfig(options.file)
|
|
32
|
+
|
|
33
|
+
console.log(chalk.green(`ā
Configuration loaded for project: ${config.project.name}`))
|
|
34
|
+
|
|
35
|
+
// Determine which services to bring down
|
|
36
|
+
let servicesToDown: ServiceConfig[] = []
|
|
37
|
+
|
|
38
|
+
if (options.all) {
|
|
39
|
+
servicesToDown = config.services
|
|
40
|
+
console.log(
|
|
41
|
+
chalk.cyan(
|
|
42
|
+
`š Bringing down all services: ${servicesToDown.map((s) => s.name).join(', ')}\n`
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
} else if (options.serviceName) {
|
|
46
|
+
const service = config.services.find((s) => s.name === options.serviceName)
|
|
47
|
+
if (!service) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Service "${
|
|
50
|
+
options.serviceName
|
|
51
|
+
}" not found in configuration. Available services: ${config.services
|
|
52
|
+
.map((s) => s.name)
|
|
53
|
+
.join(', ')}`
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
servicesToDown = [service]
|
|
57
|
+
console.log(chalk.cyan(`š Bringing down service: ${options.serviceName}\n`))
|
|
58
|
+
} else {
|
|
59
|
+
throw new Error('Either specify a service name or use --all flag')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Group services by domain for nginx config management
|
|
63
|
+
const domainToServices = new Map<string, ServiceConfig[]>()
|
|
64
|
+
const allDomains = new Set<string>()
|
|
65
|
+
|
|
66
|
+
for (const service of servicesToDown) {
|
|
67
|
+
for (const domain of service.domains) {
|
|
68
|
+
allDomains.add(domain)
|
|
69
|
+
if (!domainToServices.has(domain)) {
|
|
70
|
+
domainToServices.set(domain, [])
|
|
71
|
+
}
|
|
72
|
+
domainToServices.get(domain)!.push(service)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Stop and remove Docker containers
|
|
77
|
+
for (const service of servicesToDown) {
|
|
78
|
+
if (service.docker) {
|
|
79
|
+
console.log(chalk.cyan(`\nš³ Stopping Docker container for service: ${service.name}`))
|
|
80
|
+
try {
|
|
81
|
+
const containerName = service.docker.container
|
|
82
|
+
|
|
83
|
+
// Stop container
|
|
84
|
+
try {
|
|
85
|
+
await stopDockerContainer(containerName)
|
|
86
|
+
console.log(chalk.green(` ā
Stopped container: ${containerName}`))
|
|
87
|
+
} catch (error: any) {
|
|
88
|
+
const errorMessage = error?.message || String(error) || 'Unknown error'
|
|
89
|
+
if (
|
|
90
|
+
errorMessage.toLowerCase().includes('no such container') ||
|
|
91
|
+
errorMessage.toLowerCase().includes('container not found')
|
|
92
|
+
) {
|
|
93
|
+
console.log(
|
|
94
|
+
chalk.yellow(` ā ļø Container ${containerName} not found (already stopped)`)
|
|
95
|
+
)
|
|
96
|
+
} else {
|
|
97
|
+
throw error
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Remove container
|
|
102
|
+
try {
|
|
103
|
+
await removeDockerContainer(containerName)
|
|
104
|
+
console.log(chalk.green(` ā
Removed container: ${containerName}`))
|
|
105
|
+
} catch (error: any) {
|
|
106
|
+
const errorMessage = error?.message || String(error) || 'Unknown error'
|
|
107
|
+
if (
|
|
108
|
+
errorMessage.toLowerCase().includes('no such container') ||
|
|
109
|
+
errorMessage.toLowerCase().includes('container not found')
|
|
110
|
+
) {
|
|
111
|
+
console.log(
|
|
112
|
+
chalk.yellow(` ā ļø Container ${containerName} not found (already removed)`)
|
|
113
|
+
)
|
|
114
|
+
} else {
|
|
115
|
+
throw error
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error(
|
|
120
|
+
chalk.red(` ā Failed to stop/remove container for service ${service.name}:`),
|
|
121
|
+
error instanceof Error ? error.message : error
|
|
122
|
+
)
|
|
123
|
+
throw error
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Handle Nginx configs by domain
|
|
129
|
+
// Check which services remain active for each domain
|
|
130
|
+
if (allDomains.size > 0) {
|
|
131
|
+
console.log(chalk.cyan(`\nāļø Updating Nginx configurations...`))
|
|
132
|
+
|
|
133
|
+
// Create a set of service names being brought down for quick lookup
|
|
134
|
+
const servicesBeingDowned = new Set(servicesToDown.map((s) => s.name))
|
|
135
|
+
|
|
136
|
+
// For each domain, find all services that use it (from all config services)
|
|
137
|
+
// and determine which ones remain active
|
|
138
|
+
for (const domain of allDomains) {
|
|
139
|
+
const configName = domain.replace(/\./g, '_')
|
|
140
|
+
|
|
141
|
+
// Find all services that use this domain (from entire config)
|
|
142
|
+
const allServicesForDomain = config.services.filter((s) => s.domains.includes(domain))
|
|
143
|
+
|
|
144
|
+
// Filter out services being brought down
|
|
145
|
+
const remainingServices = allServicesForDomain.filter(
|
|
146
|
+
(s) => !servicesBeingDowned.has(s.name)
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
// Check if remaining services are actually running (have active containers)
|
|
150
|
+
const activeServices: ServiceConfig[] = []
|
|
151
|
+
for (const service of remainingServices) {
|
|
152
|
+
if (service.docker) {
|
|
153
|
+
const isRunning = await isContainerRunning(service.docker.container)
|
|
154
|
+
if (isRunning) {
|
|
155
|
+
activeServices.push(service)
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
// Service without docker - assume it's running if not being brought down
|
|
159
|
+
activeServices.push(service)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
if (activeServices.length === 0) {
|
|
165
|
+
// No active services on this domain, disable the config
|
|
166
|
+
console.log(
|
|
167
|
+
chalk.cyan(` š No active services on ${domain}, disabling Nginx config...`)
|
|
168
|
+
)
|
|
169
|
+
await disableSite(configName, config.nginx.configPath)
|
|
170
|
+
console.log(chalk.green(` ā
Disabled Nginx config for ${domain}`))
|
|
171
|
+
} else {
|
|
172
|
+
// Regenerate config with remaining active services
|
|
173
|
+
console.log(
|
|
174
|
+
chalk.cyan(
|
|
175
|
+
` š Regenerating Nginx config for ${domain} with ${
|
|
176
|
+
activeServices.length
|
|
177
|
+
} active service(s): ${activeServices.map((s) => s.name).join(', ')}`
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
// Generate config for remaining services
|
|
182
|
+
let nginxConfigContent: string
|
|
183
|
+
if (activeServices.length === 1) {
|
|
184
|
+
nginxConfigContent = generateNginxConfig(activeServices[0], false)
|
|
185
|
+
} else {
|
|
186
|
+
nginxConfigContent = generateMultiServiceNginxConfig(activeServices, domain, false)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Check if HTTPS is enabled (check if certificate exists)
|
|
190
|
+
const { certificateExists } = await import('../utils/certbot')
|
|
191
|
+
const hasHttps = await certificateExists(domain)
|
|
192
|
+
if (hasHttps) {
|
|
193
|
+
// Regenerate with HTTPS
|
|
194
|
+
if (activeServices.length === 1) {
|
|
195
|
+
nginxConfigContent = generateNginxConfig(activeServices[0], true)
|
|
196
|
+
} else {
|
|
197
|
+
nginxConfigContent = generateMultiServiceNginxConfig(activeServices, domain, true)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
await writeNginxConfig(configName, config.nginx.configPath, nginxConfigContent)
|
|
202
|
+
await enableSite(configName, config.nginx.configPath)
|
|
203
|
+
console.log(
|
|
204
|
+
chalk.green(
|
|
205
|
+
` ā
Updated Nginx config for ${domain} (${activeServices.length} service(s) active)`
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
} catch (error) {
|
|
210
|
+
console.error(
|
|
211
|
+
chalk.red(` ā Failed to update Nginx config for ${domain}:`),
|
|
212
|
+
error instanceof Error ? error.message : error
|
|
213
|
+
)
|
|
214
|
+
throw error
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Reload Nginx to apply changes
|
|
219
|
+
console.log(chalk.cyan(`\nš Reloading Nginx...`))
|
|
220
|
+
try {
|
|
221
|
+
await reloadNginx(config.nginx.reloadCommand)
|
|
222
|
+
console.log(chalk.green(` ā
Nginx reloaded`))
|
|
223
|
+
} catch (error) {
|
|
224
|
+
console.error(
|
|
225
|
+
chalk.red(` ā Failed to reload Nginx:`),
|
|
226
|
+
error instanceof Error ? error.message : error
|
|
227
|
+
)
|
|
228
|
+
throw error
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
console.log(chalk.green.bold('\nā
Services brought down successfully!\n'))
|
|
233
|
+
} catch (error) {
|
|
234
|
+
console.error(
|
|
235
|
+
chalk.red('\nā Failed to bring down services:'),
|
|
236
|
+
error instanceof Error ? error.message : error
|
|
237
|
+
)
|
|
238
|
+
process.exit(1)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
@@ -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
|
+
}
|