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
package/.editorconfig
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Editor configuration, see http://editorconfig.org
|
|
2
|
+
root = true
|
|
3
|
+
|
|
4
|
+
[*]
|
|
5
|
+
charset = utf-8
|
|
6
|
+
indent_style = space
|
|
7
|
+
indent_size = 2
|
|
8
|
+
insert_final_newline = true
|
|
9
|
+
trim_trailing_whitespace = true
|
|
10
|
+
|
|
11
|
+
[*.md]
|
|
12
|
+
max_line_length = off
|
|
13
|
+
trim_trailing_whitespace = false
|
|
14
|
+
|
|
15
|
+
[{Makefile,**.mk}]
|
|
16
|
+
indent_style = tab
|
|
17
|
+
indent_size = 2
|
package/.prettierignore
ADDED
package/.prettierrc
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"eslint.workingDirectories": [
|
|
3
|
+
{
|
|
4
|
+
"mode": "auto"
|
|
5
|
+
}
|
|
6
|
+
],
|
|
7
|
+
"[typescript]": {
|
|
8
|
+
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
9
|
+
},
|
|
10
|
+
"editor.codeActionsOnSave": {
|
|
11
|
+
"source.organizeImports": "explicit"
|
|
12
|
+
},
|
|
13
|
+
"editor.formatOnSave": true,
|
|
14
|
+
"editor.insertSpaces": false,
|
|
15
|
+
"files.eol": "\n",
|
|
16
|
+
"files.insertFinalNewline": true,
|
|
17
|
+
"files.trimTrailingWhitespace": true
|
|
18
|
+
}
|
|
19
|
+
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Montol Saklor
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# Suthep
|
|
2
|
+
|
|
3
|
+
A powerful CLI tool for deploying projects with automatic Nginx reverse proxy setup, HTTPS with Certbot, and zero-downtime deployments.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ā
Automatic Nginx reverse proxy setup
|
|
8
|
+
- ā
Automatic HTTPS with Certbot
|
|
9
|
+
- ā
Zero-downtime deployment
|
|
10
|
+
- ā
Docker container support
|
|
11
|
+
- ā
Multiple domain/subdomain support
|
|
12
|
+
- ā
Health check integration
|
|
13
|
+
- ā
YAML-based configuration
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install
|
|
19
|
+
npm link # Makes 'deploy' command available globally
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The tool uses `tsx` to run TypeScript directly, so no build step is required for development. For production, you can still build:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm run build # Optional: compile to JavaScript
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
1. **Initialize a configuration file:**
|
|
31
|
+
```bash
|
|
32
|
+
deploy init
|
|
33
|
+
```
|
|
34
|
+
This will create a `deploy.yml` file with interactive prompts.
|
|
35
|
+
|
|
36
|
+
2. **Or use the example configuration:**
|
|
37
|
+
```bash
|
|
38
|
+
cp example.yml deploy.yml
|
|
39
|
+
# Edit deploy.yml with your settings
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
3. **Setup prerequisites (Nginx and Certbot):**
|
|
43
|
+
```bash
|
|
44
|
+
deploy setup
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
4. **Deploy your project:**
|
|
48
|
+
```bash
|
|
49
|
+
deploy deploy
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Configuration
|
|
53
|
+
|
|
54
|
+
The configuration file (`deploy.yml`) supports the following structure:
|
|
55
|
+
|
|
56
|
+
```yaml
|
|
57
|
+
project:
|
|
58
|
+
name: my-app
|
|
59
|
+
version: 1.0.0
|
|
60
|
+
|
|
61
|
+
services:
|
|
62
|
+
- name: api
|
|
63
|
+
port: 3000
|
|
64
|
+
domains:
|
|
65
|
+
- api.example.com
|
|
66
|
+
- www.api.example.com
|
|
67
|
+
healthCheck:
|
|
68
|
+
path: /health
|
|
69
|
+
interval: 30
|
|
70
|
+
environment:
|
|
71
|
+
NODE_ENV: production
|
|
72
|
+
|
|
73
|
+
- name: webapp
|
|
74
|
+
port: 8080
|
|
75
|
+
docker:
|
|
76
|
+
image: nginx:latest
|
|
77
|
+
container: webapp-container
|
|
78
|
+
port: 80
|
|
79
|
+
domains:
|
|
80
|
+
- example.com
|
|
81
|
+
- www.example.com
|
|
82
|
+
|
|
83
|
+
nginx:
|
|
84
|
+
configPath: /etc/nginx/sites-available
|
|
85
|
+
reloadCommand: sudo nginx -t && sudo systemctl reload nginx
|
|
86
|
+
|
|
87
|
+
certbot:
|
|
88
|
+
email: admin@example.com
|
|
89
|
+
staging: false
|
|
90
|
+
|
|
91
|
+
deployment:
|
|
92
|
+
strategy: rolling
|
|
93
|
+
healthCheckTimeout: 30000
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Service Configuration
|
|
97
|
+
|
|
98
|
+
Each service can have:
|
|
99
|
+
|
|
100
|
+
- **name**: Unique service name
|
|
101
|
+
- **port**: Port the service runs on
|
|
102
|
+
- **domains**: Array of domain names or subdomains
|
|
103
|
+
- **docker** (optional):
|
|
104
|
+
- **image**: Docker image to use
|
|
105
|
+
- **container**: Container name
|
|
106
|
+
- **port**: Container's internal port
|
|
107
|
+
- **healthCheck** (optional):
|
|
108
|
+
- **path**: Health check endpoint path
|
|
109
|
+
- **interval**: Check interval in seconds
|
|
110
|
+
- **environment** (optional): Environment variables
|
|
111
|
+
|
|
112
|
+
## Commands
|
|
113
|
+
|
|
114
|
+
### `deploy init`
|
|
115
|
+
|
|
116
|
+
Initialize a new deployment configuration file.
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
deploy init [-f deploy.yml]
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### `deploy setup`
|
|
123
|
+
|
|
124
|
+
Setup Nginx and Certbot on the system.
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
deploy setup [--nginx-only] [--certbot-only]
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### `deploy deploy`
|
|
131
|
+
|
|
132
|
+
Deploy a project using the configuration file.
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
deploy deploy [-f deploy.yml] [--no-https] [--no-nginx]
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Options:
|
|
139
|
+
- `-f, --file`: Configuration file path (default: `deploy.yml`)
|
|
140
|
+
- `--no-https`: Skip HTTPS setup
|
|
141
|
+
- `--no-nginx`: Skip Nginx configuration
|
|
142
|
+
|
|
143
|
+
## Examples
|
|
144
|
+
|
|
145
|
+
### Simple Node.js Service
|
|
146
|
+
|
|
147
|
+
```yaml
|
|
148
|
+
services:
|
|
149
|
+
- name: api
|
|
150
|
+
port: 3000
|
|
151
|
+
domains:
|
|
152
|
+
- api.example.com
|
|
153
|
+
healthCheck:
|
|
154
|
+
path: /health
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Docker Container Service
|
|
158
|
+
|
|
159
|
+
```yaml
|
|
160
|
+
services:
|
|
161
|
+
- name: webapp
|
|
162
|
+
port: 8080
|
|
163
|
+
docker:
|
|
164
|
+
image: myapp/webapp:latest
|
|
165
|
+
container: webapp-container
|
|
166
|
+
port: 80
|
|
167
|
+
domains:
|
|
168
|
+
- example.com
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Multiple Domains
|
|
172
|
+
|
|
173
|
+
```yaml
|
|
174
|
+
services:
|
|
175
|
+
- name: dashboard
|
|
176
|
+
port: 5000
|
|
177
|
+
domains:
|
|
178
|
+
- dashboard.example.com
|
|
179
|
+
- admin.example.com
|
|
180
|
+
- app.example.com
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Connect to Existing Docker Container
|
|
184
|
+
|
|
185
|
+
```yaml
|
|
186
|
+
services:
|
|
187
|
+
- name: database-proxy
|
|
188
|
+
port: 5432
|
|
189
|
+
docker:
|
|
190
|
+
container: postgres-container
|
|
191
|
+
port: 5432
|
|
192
|
+
domains:
|
|
193
|
+
- db.example.com
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Requirements
|
|
197
|
+
|
|
198
|
+
- Node.js 16+
|
|
199
|
+
- Nginx (installed via `deploy setup`)
|
|
200
|
+
- Certbot (installed via `deploy setup`)
|
|
201
|
+
- Docker (optional, for Docker-based services)
|
|
202
|
+
- sudo access (for Nginx and Certbot operations)
|
|
203
|
+
|
|
204
|
+
## Cost Optimization
|
|
205
|
+
|
|
206
|
+
This tool helps save costs on VMs by:
|
|
207
|
+
- Efficiently managing multiple services on a single server
|
|
208
|
+
- Automatic reverse proxy setup reduces manual configuration
|
|
209
|
+
- Zero-downtime deployments reduce service interruptions
|
|
210
|
+
- Health checks ensure service reliability
|
|
211
|
+
|
|
212
|
+
## License
|
|
213
|
+
|
|
214
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { requestCertificate } from "../utils/certbot.js";
|
|
5
|
+
import { loadConfig } from "../utils/config-loader.js";
|
|
6
|
+
import { deployService, performHealthCheck } from "../utils/deployment.js";
|
|
7
|
+
import { startDockerContainer } from "../utils/docker.js";
|
|
8
|
+
import { generateNginxConfig, enableSite, reloadNginx } from "../utils/nginx.js";
|
|
9
|
+
async function deployCommand(options) {
|
|
10
|
+
console.log(chalk.blue.bold("\nš Deploying Services\n"));
|
|
11
|
+
try {
|
|
12
|
+
if (!await fs.pathExists(options.file)) {
|
|
13
|
+
throw new Error(`Configuration file not found: ${options.file}`);
|
|
14
|
+
}
|
|
15
|
+
console.log(chalk.cyan(`š Loading configuration from ${options.file}...`));
|
|
16
|
+
const config = await loadConfig(options.file);
|
|
17
|
+
console.log(chalk.green(`ā
Configuration loaded for project: ${config.project.name}`));
|
|
18
|
+
console.log(chalk.dim(` Services: ${config.services.map((s) => s.name).join(", ")}
|
|
19
|
+
`));
|
|
20
|
+
for (const service of config.services) {
|
|
21
|
+
console.log(chalk.cyan(`
|
|
22
|
+
š¦ Deploying service: ${service.name}`));
|
|
23
|
+
try {
|
|
24
|
+
if (service.docker) {
|
|
25
|
+
console.log(chalk.dim(" š³ Managing Docker container..."));
|
|
26
|
+
await startDockerContainer(service);
|
|
27
|
+
}
|
|
28
|
+
await deployService(service, config.deployment);
|
|
29
|
+
if (options.nginx) {
|
|
30
|
+
console.log(chalk.dim(" āļø Configuring Nginx reverse proxy..."));
|
|
31
|
+
const nginxConfigContent = generateNginxConfig(service, false);
|
|
32
|
+
const nginxConfigPath = path.join(config.nginx.configPath, `${service.name}.conf`);
|
|
33
|
+
await fs.writeFile(nginxConfigPath, nginxConfigContent);
|
|
34
|
+
await enableSite(service.name, config.nginx.configPath);
|
|
35
|
+
console.log(chalk.green(` ā
Nginx configured for ${service.domains.join(", ")}`));
|
|
36
|
+
}
|
|
37
|
+
if (options.https && service.domains.length > 0) {
|
|
38
|
+
console.log(chalk.dim(" š Setting up HTTPS certificates..."));
|
|
39
|
+
for (const domain of service.domains) {
|
|
40
|
+
try {
|
|
41
|
+
await requestCertificate(domain, config.certbot.email, config.certbot.staging);
|
|
42
|
+
console.log(chalk.green(` ā
SSL certificate obtained for ${domain}`));
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.log(
|
|
45
|
+
chalk.yellow(
|
|
46
|
+
` ā ļø Failed to obtain SSL for ${domain}: ${error instanceof Error ? error.message : error}`
|
|
47
|
+
)
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (options.nginx) {
|
|
52
|
+
const nginxConfigContent = generateNginxConfig(service, true);
|
|
53
|
+
const nginxConfigPath = path.join(config.nginx.configPath, `${service.name}.conf`);
|
|
54
|
+
await fs.writeFile(nginxConfigPath, nginxConfigContent);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (options.nginx) {
|
|
58
|
+
console.log(chalk.dim(" š Reloading Nginx..."));
|
|
59
|
+
await reloadNginx(config.nginx.reloadCommand);
|
|
60
|
+
}
|
|
61
|
+
if (service.healthCheck) {
|
|
62
|
+
console.log(chalk.dim(` š„ Performing health check...`));
|
|
63
|
+
const isHealthy = await performHealthCheck(
|
|
64
|
+
`http://localhost:${service.port}${service.healthCheck.path}`,
|
|
65
|
+
config.deployment.healthCheckTimeout
|
|
66
|
+
);
|
|
67
|
+
if (isHealthy) {
|
|
68
|
+
console.log(chalk.green(` ā
Service ${service.name} is healthy`));
|
|
69
|
+
} else {
|
|
70
|
+
throw new Error(`Health check failed for service ${service.name}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
console.log(chalk.green.bold(`
|
|
74
|
+
⨠Service ${service.name} deployed successfully!`));
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error(
|
|
77
|
+
chalk.red(`
|
|
78
|
+
ā Failed to deploy service ${service.name}:`),
|
|
79
|
+
error instanceof Error ? error.message : error
|
|
80
|
+
);
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
console.log(chalk.green.bold("\nš All services deployed successfully!\n"));
|
|
85
|
+
console.log(chalk.cyan("š Service URLs:"));
|
|
86
|
+
for (const service of config.services) {
|
|
87
|
+
for (const domain of service.domains) {
|
|
88
|
+
const protocol = options.https ? "https" : "http";
|
|
89
|
+
console.log(chalk.dim(` ${service.name}: ${protocol}://${domain}`));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
console.log();
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.error(
|
|
95
|
+
chalk.red("\nā Deployment failed:"),
|
|
96
|
+
error instanceof Error ? error.message : error
|
|
97
|
+
);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
export {
|
|
102
|
+
deployCommand
|
|
103
|
+
};
|
|
104
|
+
//# sourceMappingURL=deploy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deploy.js","sources":["../../src/commands/deploy.ts"],"sourcesContent":["import chalk from 'chalk'\nimport fs from 'fs-extra'\nimport path from 'path'\nimport { requestCertificate } from '../utils/certbot'\nimport { loadConfig } from '../utils/config-loader'\nimport { deployService, performHealthCheck } from '../utils/deployment'\nimport { startDockerContainer } from '../utils/docker'\nimport { enableSite, generateNginxConfig, reloadNginx } from '../utils/nginx'\n\ninterface DeployOptions {\n file: string\n https: boolean\n nginx: boolean\n}\n\nexport async function deployCommand(options: DeployOptions): Promise<void> {\n console.log(chalk.blue.bold('\\nš Deploying Services\\n'))\n\n try {\n // Load configuration\n if (!(await fs.pathExists(options.file))) {\n throw new Error(`Configuration file not found: ${options.file}`)\n }\n\n console.log(chalk.cyan(`š Loading configuration from ${options.file}...`))\n const config = await loadConfig(options.file)\n\n console.log(chalk.green(`ā
Configuration loaded for project: ${config.project.name}`))\n console.log(chalk.dim(` Services: ${config.services.map((s) => s.name).join(', ')}\\n`))\n\n // Deploy each service\n for (const service of config.services) {\n console.log(chalk.cyan(`\\nš¦ Deploying service: ${service.name}`))\n\n try {\n // Start Docker container if configured\n if (service.docker) {\n console.log(chalk.dim(' š³ Managing Docker container...'))\n await startDockerContainer(service)\n }\n\n // Deploy the service\n await deployService(service, config.deployment)\n\n // Generate and configure Nginx\n if (options.nginx) {\n console.log(chalk.dim(' āļø Configuring Nginx reverse proxy...'))\n const nginxConfigContent = generateNginxConfig(service, false)\n const nginxConfigPath = path.join(config.nginx.configPath, `${service.name}.conf`)\n\n await fs.writeFile(nginxConfigPath, nginxConfigContent)\n await enableSite(service.name, config.nginx.configPath)\n\n console.log(chalk.green(` ā
Nginx configured for ${service.domains.join(', ')}`))\n }\n\n // Setup HTTPS with Certbot\n if (options.https && service.domains.length > 0) {\n console.log(chalk.dim(' š Setting up HTTPS certificates...'))\n\n for (const domain of service.domains) {\n try {\n await requestCertificate(domain, config.certbot.email, config.certbot.staging)\n console.log(chalk.green(` ā
SSL certificate obtained for ${domain}`))\n } catch (error) {\n console.log(\n chalk.yellow(\n ` ā ļø Failed to obtain SSL for ${domain}: ${\n error instanceof Error ? error.message : error\n }`\n )\n )\n }\n }\n\n // Update Nginx config with HTTPS\n if (options.nginx) {\n const nginxConfigContent = generateNginxConfig(service, true)\n const nginxConfigPath = path.join(config.nginx.configPath, `${service.name}.conf`)\n await fs.writeFile(nginxConfigPath, nginxConfigContent)\n }\n }\n\n // Reload Nginx after all configurations\n if (options.nginx) {\n console.log(chalk.dim(' š Reloading Nginx...'))\n await reloadNginx(config.nginx.reloadCommand)\n }\n\n // Perform health check\n if (service.healthCheck) {\n console.log(chalk.dim(` š„ Performing health check...`))\n const isHealthy = await performHealthCheck(\n `http://localhost:${service.port}${service.healthCheck.path}`,\n config.deployment.healthCheckTimeout\n )\n\n if (isHealthy) {\n console.log(chalk.green(` ā
Service ${service.name} is healthy`))\n } else {\n throw new Error(`Health check failed for service ${service.name}`)\n }\n }\n\n console.log(chalk.green.bold(`\\n⨠Service ${service.name} deployed successfully!`))\n } catch (error) {\n console.error(\n chalk.red(`\\nā Failed to deploy service ${service.name}:`),\n error instanceof Error ? error.message : error\n )\n throw error\n }\n }\n\n console.log(chalk.green.bold('\\nš All services deployed successfully!\\n'))\n\n // Print service URLs\n console.log(chalk.cyan('š Service URLs:'))\n for (const service of config.services) {\n for (const domain of service.domains) {\n const protocol = options.https ? 'https' : 'http'\n console.log(chalk.dim(` ${service.name}: ${protocol}://${domain}`))\n }\n }\n console.log()\n } catch (error) {\n console.error(\n chalk.red('\\nā Deployment failed:'),\n error instanceof Error ? error.message : error\n )\n process.exit(1)\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
import inquirer from "inquirer";
|
|
4
|
+
import { saveConfig } from "../utils/config-loader.js";
|
|
5
|
+
async function initCommand(options) {
|
|
6
|
+
console.log(chalk.blue.bold("\nš Suthep Deployment Configuration\n"));
|
|
7
|
+
if (await fs.pathExists(options.file)) {
|
|
8
|
+
const { overwrite } = await inquirer.prompt([
|
|
9
|
+
{
|
|
10
|
+
type: "confirm",
|
|
11
|
+
name: "overwrite",
|
|
12
|
+
message: `File ${options.file} already exists. Overwrite?`,
|
|
13
|
+
default: false
|
|
14
|
+
}
|
|
15
|
+
]);
|
|
16
|
+
if (!overwrite) {
|
|
17
|
+
console.log(chalk.yellow("Aborted."));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const projectAnswers = await inquirer.prompt([
|
|
22
|
+
{
|
|
23
|
+
type: "input",
|
|
24
|
+
name: "projectName",
|
|
25
|
+
message: "Project name:",
|
|
26
|
+
default: "my-app"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
type: "input",
|
|
30
|
+
name: "projectVersion",
|
|
31
|
+
message: "Project version:",
|
|
32
|
+
default: "1.0.0"
|
|
33
|
+
}
|
|
34
|
+
]);
|
|
35
|
+
const services = [];
|
|
36
|
+
let addMoreServices = true;
|
|
37
|
+
while (addMoreServices) {
|
|
38
|
+
console.log(chalk.cyan(`
|
|
39
|
+
š¦ Service ${services.length + 1} Configuration`));
|
|
40
|
+
const serviceAnswers = await inquirer.prompt([
|
|
41
|
+
{
|
|
42
|
+
type: "input",
|
|
43
|
+
name: "name",
|
|
44
|
+
message: "Service name:",
|
|
45
|
+
validate: (input) => input.trim() !== "" || "Service name is required"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
type: "number",
|
|
49
|
+
name: "port",
|
|
50
|
+
message: "Service port:",
|
|
51
|
+
default: 3e3,
|
|
52
|
+
validate: (input) => {
|
|
53
|
+
if (input === void 0) return "Port is required";
|
|
54
|
+
return input > 0 && input < 65536 || "Port must be between 1 and 65535";
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
type: "input",
|
|
59
|
+
name: "domains",
|
|
60
|
+
message: "Domain names (comma-separated):",
|
|
61
|
+
validate: (input) => input.trim() !== "" || "At least one domain is required",
|
|
62
|
+
filter: (input) => input.split(",").map((d) => d.trim())
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
type: "confirm",
|
|
66
|
+
name: "useDocker",
|
|
67
|
+
message: "Use Docker?",
|
|
68
|
+
default: false
|
|
69
|
+
}
|
|
70
|
+
]);
|
|
71
|
+
let dockerConfig = void 0;
|
|
72
|
+
if (serviceAnswers.useDocker) {
|
|
73
|
+
const dockerAnswers = await inquirer.prompt([
|
|
74
|
+
{
|
|
75
|
+
type: "input",
|
|
76
|
+
name: "image",
|
|
77
|
+
message: "Docker image (leave empty to connect to existing container):"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
type: "input",
|
|
81
|
+
name: "container",
|
|
82
|
+
message: "Container name:",
|
|
83
|
+
validate: (input) => input.trim() !== "" || "Container name is required"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
type: "number",
|
|
87
|
+
name: "port",
|
|
88
|
+
message: "Container port:",
|
|
89
|
+
default: serviceAnswers.port
|
|
90
|
+
}
|
|
91
|
+
]);
|
|
92
|
+
dockerConfig = {
|
|
93
|
+
image: dockerAnswers.image || void 0,
|
|
94
|
+
container: dockerAnswers.container,
|
|
95
|
+
port: dockerAnswers.port
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const { addHealthCheck } = await inquirer.prompt([
|
|
99
|
+
{
|
|
100
|
+
type: "confirm",
|
|
101
|
+
name: "addHealthCheck",
|
|
102
|
+
message: "Add health check?",
|
|
103
|
+
default: true
|
|
104
|
+
}
|
|
105
|
+
]);
|
|
106
|
+
let healthCheck = void 0;
|
|
107
|
+
if (addHealthCheck) {
|
|
108
|
+
const healthCheckAnswers = await inquirer.prompt([
|
|
109
|
+
{
|
|
110
|
+
type: "input",
|
|
111
|
+
name: "path",
|
|
112
|
+
message: "Health check path:",
|
|
113
|
+
default: "/health"
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
type: "number",
|
|
117
|
+
name: "interval",
|
|
118
|
+
message: "Health check interval (seconds):",
|
|
119
|
+
default: 30
|
|
120
|
+
}
|
|
121
|
+
]);
|
|
122
|
+
healthCheck = healthCheckAnswers;
|
|
123
|
+
}
|
|
124
|
+
services.push({
|
|
125
|
+
name: serviceAnswers.name,
|
|
126
|
+
port: serviceAnswers.port,
|
|
127
|
+
domains: serviceAnswers.domains,
|
|
128
|
+
docker: dockerConfig,
|
|
129
|
+
healthCheck
|
|
130
|
+
});
|
|
131
|
+
const { addMore } = await inquirer.prompt([
|
|
132
|
+
{
|
|
133
|
+
type: "confirm",
|
|
134
|
+
name: "addMore",
|
|
135
|
+
message: "Add another service?",
|
|
136
|
+
default: false
|
|
137
|
+
}
|
|
138
|
+
]);
|
|
139
|
+
addMoreServices = addMore;
|
|
140
|
+
}
|
|
141
|
+
const certbotAnswers = await inquirer.prompt([
|
|
142
|
+
{
|
|
143
|
+
type: "input",
|
|
144
|
+
name: "email",
|
|
145
|
+
message: "Email for SSL certificates:",
|
|
146
|
+
validate: (input) => {
|
|
147
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
148
|
+
return emailRegex.test(input) || "Please enter a valid email address";
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
type: "confirm",
|
|
153
|
+
name: "staging",
|
|
154
|
+
message: "Use Certbot staging environment? (for testing)",
|
|
155
|
+
default: false
|
|
156
|
+
}
|
|
157
|
+
]);
|
|
158
|
+
const config = {
|
|
159
|
+
project: {
|
|
160
|
+
name: projectAnswers.projectName,
|
|
161
|
+
version: projectAnswers.projectVersion
|
|
162
|
+
},
|
|
163
|
+
services,
|
|
164
|
+
nginx: {
|
|
165
|
+
configPath: "/etc/nginx/sites-available",
|
|
166
|
+
reloadCommand: "sudo nginx -t && sudo systemctl reload nginx"
|
|
167
|
+
},
|
|
168
|
+
certbot: {
|
|
169
|
+
email: certbotAnswers.email,
|
|
170
|
+
staging: certbotAnswers.staging
|
|
171
|
+
},
|
|
172
|
+
deployment: {
|
|
173
|
+
strategy: "rolling",
|
|
174
|
+
healthCheckTimeout: 3e4
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
await saveConfig(options.file, config);
|
|
178
|
+
console.log(chalk.green(`
|
|
179
|
+
ā
Configuration saved to ${options.file}`));
|
|
180
|
+
console.log(chalk.dim("\nNext steps:"));
|
|
181
|
+
console.log(chalk.dim(` 1. Review and edit ${options.file} if needed`));
|
|
182
|
+
console.log(chalk.dim(" 2. Run: suthep setup"));
|
|
183
|
+
console.log(chalk.dim(" 3. Run: suthep deploy\n"));
|
|
184
|
+
}
|
|
185
|
+
export {
|
|
186
|
+
initCommand
|
|
187
|
+
};
|
|
188
|
+
//# sourceMappingURL=init.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init.js","sources":["../../src/commands/init.ts"],"sourcesContent":["import chalk from 'chalk'\nimport fs from 'fs-extra'\nimport inquirer from 'inquirer'\nimport type { DeployConfig } from '../types/config'\nimport { saveConfig } from '../utils/config-loader'\n\ninterface InitOptions {\n file: string\n}\n\nexport async function initCommand(options: InitOptions): Promise<void> {\n console.log(chalk.blue.bold('\\nš Suthep Deployment Configuration\\n'))\n\n // Check if file already exists\n if (await fs.pathExists(options.file)) {\n const { overwrite } = await inquirer.prompt([\n {\n type: 'confirm',\n name: 'overwrite',\n message: `File ${options.file} already exists. Overwrite?`,\n default: false,\n },\n ])\n\n if (!overwrite) {\n console.log(chalk.yellow('Aborted.'))\n return\n }\n }\n\n // Gather project information\n const projectAnswers = await inquirer.prompt([\n {\n type: 'input',\n name: 'projectName',\n message: 'Project name:',\n default: 'my-app',\n },\n {\n type: 'input',\n name: 'projectVersion',\n message: 'Project version:',\n default: '1.0.0',\n },\n ])\n\n // Gather service information\n const services = []\n let addMoreServices = true\n\n while (addMoreServices) {\n console.log(chalk.cyan(`\\nš¦ Service ${services.length + 1} Configuration`))\n\n const serviceAnswers = await inquirer.prompt([\n {\n type: 'input',\n name: 'name',\n message: 'Service name:',\n validate: (input) => input.trim() !== '' || 'Service name is required',\n },\n {\n type: 'number',\n name: 'port',\n message: 'Service port:',\n default: 3000,\n validate: (input: number | undefined) => {\n if (input === undefined) return 'Port is required'\n return (input > 0 && input < 65536) || 'Port must be between 1 and 65535'\n },\n },\n {\n type: 'input',\n name: 'domains',\n message: 'Domain names (comma-separated):',\n validate: (input) => input.trim() !== '' || 'At least one domain is required',\n filter: (input: string) => input.split(',').map((d: string) => d.trim()),\n },\n {\n type: 'confirm',\n name: 'useDocker',\n message: 'Use Docker?',\n default: false,\n },\n ])\n\n // Docker configuration\n let dockerConfig = undefined\n if (serviceAnswers.useDocker) {\n const dockerAnswers = await inquirer.prompt([\n {\n type: 'input',\n name: 'image',\n message: 'Docker image (leave empty to connect to existing container):',\n },\n {\n type: 'input',\n name: 'container',\n message: 'Container name:',\n validate: (input) => input.trim() !== '' || 'Container name is required',\n },\n {\n type: 'number',\n name: 'port',\n message: 'Container port:',\n default: serviceAnswers.port,\n },\n ])\n\n dockerConfig = {\n image: dockerAnswers.image || undefined,\n container: dockerAnswers.container,\n port: dockerAnswers.port,\n }\n }\n\n // Health check configuration\n const { addHealthCheck } = await inquirer.prompt([\n {\n type: 'confirm',\n name: 'addHealthCheck',\n message: 'Add health check?',\n default: true,\n },\n ])\n\n let healthCheck = undefined\n if (addHealthCheck) {\n const healthCheckAnswers = await inquirer.prompt([\n {\n type: 'input',\n name: 'path',\n message: 'Health check path:',\n default: '/health',\n },\n {\n type: 'number',\n name: 'interval',\n message: 'Health check interval (seconds):',\n default: 30,\n },\n ])\n\n healthCheck = healthCheckAnswers\n }\n\n services.push({\n name: serviceAnswers.name,\n port: serviceAnswers.port,\n domains: serviceAnswers.domains,\n docker: dockerConfig,\n healthCheck,\n })\n\n const { addMore } = await inquirer.prompt([\n {\n type: 'confirm',\n name: 'addMore',\n message: 'Add another service?',\n default: false,\n },\n ])\n\n addMoreServices = addMore\n }\n\n // Certbot configuration\n const certbotAnswers = await inquirer.prompt([\n {\n type: 'input',\n name: 'email',\n message: 'Email for SSL certificates:',\n validate: (input) => {\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n return emailRegex.test(input) || 'Please enter a valid email address'\n },\n },\n {\n type: 'confirm',\n name: 'staging',\n message: 'Use Certbot staging environment? (for testing)',\n default: false,\n },\n ])\n\n // Build configuration object\n const config: DeployConfig = {\n project: {\n name: projectAnswers.projectName,\n version: projectAnswers.projectVersion,\n },\n services,\n nginx: {\n configPath: '/etc/nginx/sites-available',\n reloadCommand: 'sudo nginx -t && sudo systemctl reload nginx',\n },\n certbot: {\n email: certbotAnswers.email,\n staging: certbotAnswers.staging,\n },\n deployment: {\n strategy: 'rolling',\n healthCheckTimeout: 30000,\n },\n }\n\n // Save configuration\n await saveConfig(options.file, config)\n\n console.log(chalk.green(`\\nā
Configuration saved to ${options.file}`))\n console.log(chalk.dim('\\nNext steps:'))\n console.log(chalk.dim(` 1. Review and edit ${options.file} if needed`))\n console.log(chalk.dim(' 2. Run: suthep setup'))\n console.log(chalk.dim(' 3. Run: suthep deploy\\n'))\n}\n"],"names":[],"mappings":"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"}
|