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.
Files changed (50) hide show
  1. package/.editorconfig +17 -0
  2. package/.prettierignore +6 -0
  3. package/.prettierrc +7 -0
  4. package/.vscode/settings.json +19 -0
  5. package/LICENSE +21 -0
  6. package/README.md +214 -0
  7. package/dist/commands/deploy.js +104 -0
  8. package/dist/commands/deploy.js.map +1 -0
  9. package/dist/commands/init.js +188 -0
  10. package/dist/commands/init.js.map +1 -0
  11. package/dist/commands/setup.js +90 -0
  12. package/dist/commands/setup.js.map +1 -0
  13. package/dist/index.js +12 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/utils/certbot.js +27 -0
  16. package/dist/utils/certbot.js.map +1 -0
  17. package/dist/utils/config-loader.js +65 -0
  18. package/dist/utils/config-loader.js.map +1 -0
  19. package/dist/utils/deployment.js +52 -0
  20. package/dist/utils/deployment.js.map +1 -0
  21. package/dist/utils/docker.js +57 -0
  22. package/dist/utils/docker.js.map +1 -0
  23. package/dist/utils/nginx.js +154 -0
  24. package/dist/utils/nginx.js.map +1 -0
  25. package/docs/README.md +62 -0
  26. package/docs/api-reference.md +545 -0
  27. package/docs/architecture.md +367 -0
  28. package/docs/commands.md +273 -0
  29. package/docs/configuration.md +347 -0
  30. package/docs/examples.md +537 -0
  31. package/docs/getting-started.md +197 -0
  32. package/docs/troubleshooting.md +441 -0
  33. package/example/README.md +81 -0
  34. package/example/docker-compose.yml +72 -0
  35. package/example/suthep.yml +31 -0
  36. package/package.json +45 -0
  37. package/src/commands/deploy.ts +133 -0
  38. package/src/commands/init.ts +214 -0
  39. package/src/commands/setup.ts +112 -0
  40. package/src/index.ts +34 -0
  41. package/src/types/config.ts +51 -0
  42. package/src/utils/certbot.ts +82 -0
  43. package/src/utils/config-loader.ts +81 -0
  44. package/src/utils/deployment.ts +132 -0
  45. package/src/utils/docker.ts +151 -0
  46. package/src/utils/nginx.ts +143 -0
  47. package/suthep.example.yml +69 -0
  48. package/todo.md +6 -0
  49. package/tsconfig.json +26 -0
  50. 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
+ }