suthep 0.1.0-beta.1

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 (65) 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 +217 -0
  7. package/dist/commands/deploy.js +318 -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 +19 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/utils/certbot.js +64 -0
  16. package/dist/utils/certbot.js.map +1 -0
  17. package/dist/utils/config-loader.js +95 -0
  18. package/dist/utils/config-loader.js.map +1 -0
  19. package/dist/utils/deployment.js +76 -0
  20. package/dist/utils/deployment.js.map +1 -0
  21. package/dist/utils/docker.js +393 -0
  22. package/dist/utils/docker.js.map +1 -0
  23. package/dist/utils/nginx.js +303 -0
  24. package/dist/utils/nginx.js.map +1 -0
  25. package/docs/README.md +95 -0
  26. package/docs/TRANSLATIONS.md +211 -0
  27. package/docs/en/README.md +76 -0
  28. package/docs/en/api-reference.md +545 -0
  29. package/docs/en/architecture.md +369 -0
  30. package/docs/en/commands.md +273 -0
  31. package/docs/en/configuration.md +347 -0
  32. package/docs/en/developer-guide.md +588 -0
  33. package/docs/en/docker-ports-config.md +333 -0
  34. package/docs/en/examples.md +537 -0
  35. package/docs/en/getting-started.md +202 -0
  36. package/docs/en/port-binding.md +268 -0
  37. package/docs/en/troubleshooting.md +441 -0
  38. package/docs/th/README.md +64 -0
  39. package/docs/th/commands.md +202 -0
  40. package/docs/th/configuration.md +325 -0
  41. package/docs/th/getting-started.md +203 -0
  42. package/example/README.md +85 -0
  43. package/example/docker-compose.yml +76 -0
  44. package/example/docker-ports-example.yml +81 -0
  45. package/example/muacle.yml +47 -0
  46. package/example/port-binding-example.yml +45 -0
  47. package/example/suthep.yml +46 -0
  48. package/example/suthep=1.yml +46 -0
  49. package/package.json +45 -0
  50. package/src/commands/deploy.ts +405 -0
  51. package/src/commands/init.ts +214 -0
  52. package/src/commands/setup.ts +112 -0
  53. package/src/index.ts +42 -0
  54. package/src/types/config.ts +52 -0
  55. package/src/utils/certbot.ts +144 -0
  56. package/src/utils/config-loader.ts +121 -0
  57. package/src/utils/deployment.ts +157 -0
  58. package/src/utils/docker.ts +755 -0
  59. package/src/utils/nginx.ts +326 -0
  60. package/suthep-0.1.1.tgz +0 -0
  61. package/suthep.example.yml +98 -0
  62. package/test +0 -0
  63. package/todo.md +6 -0
  64. package/tsconfig.json +26 -0
  65. package/vite.config.ts +46 -0
@@ -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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"}
@@ -0,0 +1,90 @@
1
+ import chalk from "chalk";
2
+ import { execa } from "execa";
3
+ async function setupCommand(options) {
4
+ console.log(chalk.blue.bold("\nšŸ”§ Setting up prerequisites\n"));
5
+ const setupNginx = !options.certbotOnly;
6
+ const setupCertbot = !options.nginxOnly;
7
+ try {
8
+ if (setupNginx) {
9
+ console.log(chalk.cyan("šŸ“¦ Installing Nginx..."));
10
+ try {
11
+ await execa("nginx", ["-v"]);
12
+ console.log(chalk.green("āœ… Nginx is already installed"));
13
+ } catch {
14
+ const platform = process.platform;
15
+ if (platform === "linux") {
16
+ try {
17
+ await execa("apt-get", ["--version"]);
18
+ console.log(chalk.dim("Using apt-get..."));
19
+ await execa("sudo", ["apt-get", "update"], { stdio: "inherit" });
20
+ await execa("sudo", ["apt-get", "install", "-y", "nginx"], { stdio: "inherit" });
21
+ } catch {
22
+ try {
23
+ await execa("yum", ["--version"]);
24
+ console.log(chalk.dim("Using yum..."));
25
+ await execa("sudo", ["yum", "install", "-y", "nginx"], { stdio: "inherit" });
26
+ } catch {
27
+ throw new Error("Unsupported Linux distribution. Please install Nginx manually.");
28
+ }
29
+ }
30
+ } else if (platform === "darwin") {
31
+ console.log(chalk.dim("Using Homebrew..."));
32
+ await execa("brew", ["install", "nginx"], { stdio: "inherit" });
33
+ } else {
34
+ throw new Error(`Unsupported platform: ${platform}. Please install Nginx manually.`);
35
+ }
36
+ console.log(chalk.green("āœ… Nginx installed successfully"));
37
+ }
38
+ console.log(chalk.cyan("šŸš€ Starting Nginx service..."));
39
+ try {
40
+ await execa("sudo", ["systemctl", "start", "nginx"]);
41
+ await execa("sudo", ["systemctl", "enable", "nginx"]);
42
+ console.log(chalk.green("āœ… Nginx service started"));
43
+ } catch (error) {
44
+ console.log(chalk.yellow("āš ļø Could not start Nginx via systemctl (might not be available)"));
45
+ }
46
+ }
47
+ if (setupCertbot) {
48
+ console.log(chalk.cyan("\nšŸ” Installing Certbot..."));
49
+ try {
50
+ await execa("certbot", ["--version"]);
51
+ console.log(chalk.green("āœ… Certbot is already installed"));
52
+ } catch {
53
+ const platform = process.platform;
54
+ if (platform === "linux") {
55
+ try {
56
+ await execa("apt-get", ["--version"]);
57
+ console.log(chalk.dim("Using apt-get..."));
58
+ await execa("sudo", ["apt-get", "update"], { stdio: "inherit" });
59
+ await execa("sudo", ["apt-get", "install", "-y", "certbot", "python3-certbot-nginx"], { stdio: "inherit" });
60
+ } catch {
61
+ try {
62
+ await execa("yum", ["--version"]);
63
+ console.log(chalk.dim("Using yum..."));
64
+ await execa("sudo", ["yum", "install", "-y", "certbot", "python3-certbot-nginx"], { stdio: "inherit" });
65
+ } catch {
66
+ throw new Error("Unsupported Linux distribution. Please install Certbot manually.");
67
+ }
68
+ }
69
+ } else if (platform === "darwin") {
70
+ console.log(chalk.dim("Using Homebrew..."));
71
+ await execa("brew", ["install", "certbot"], { stdio: "inherit" });
72
+ } else {
73
+ throw new Error(`Unsupported platform: ${platform}. Please install Certbot manually.`);
74
+ }
75
+ console.log(chalk.green("āœ… Certbot installed successfully"));
76
+ }
77
+ }
78
+ console.log(chalk.green.bold("\n✨ Setup completed successfully!\n"));
79
+ console.log(chalk.dim("Next steps:"));
80
+ console.log(chalk.dim(" 1. Create a configuration file: suthep init"));
81
+ console.log(chalk.dim(" 2. Deploy your services: suthep deploy\n"));
82
+ } catch (error) {
83
+ console.error(chalk.red("\nāŒ Setup failed:"), error instanceof Error ? error.message : error);
84
+ process.exit(1);
85
+ }
86
+ }
87
+ export {
88
+ setupCommand
89
+ };
90
+ //# sourceMappingURL=setup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup.js","sources":["../../src/commands/setup.ts"],"sourcesContent":["import chalk from 'chalk';\nimport { execa } from 'execa';\n\ninterface SetupOptions {\n nginxOnly?: boolean;\n certbotOnly?: boolean;\n}\n\nexport async function setupCommand(options: SetupOptions): Promise<void> {\n console.log(chalk.blue.bold('\\nšŸ”§ Setting up prerequisites\\n'));\n\n const setupNginx = !options.certbotOnly;\n const setupCertbot = !options.nginxOnly;\n\n try {\n // Setup Nginx\n if (setupNginx) {\n console.log(chalk.cyan('šŸ“¦ Installing Nginx...'));\n\n // Check if Nginx is already installed\n try {\n await execa('nginx', ['-v']);\n console.log(chalk.green('āœ… Nginx is already installed'));\n } catch {\n // Install Nginx based on OS\n const platform = process.platform;\n\n if (platform === 'linux') {\n // Detect Linux distribution\n try {\n await execa('apt-get', ['--version']);\n console.log(chalk.dim('Using apt-get...'));\n await execa('sudo', ['apt-get', 'update'], { stdio: 'inherit' });\n await execa('sudo', ['apt-get', 'install', '-y', 'nginx'], { stdio: 'inherit' });\n } catch {\n try {\n await execa('yum', ['--version']);\n console.log(chalk.dim('Using yum...'));\n await execa('sudo', ['yum', 'install', '-y', 'nginx'], { stdio: 'inherit' });\n } catch {\n throw new Error('Unsupported Linux distribution. Please install Nginx manually.');\n }\n }\n } else if (platform === 'darwin') {\n console.log(chalk.dim('Using Homebrew...'));\n await execa('brew', ['install', 'nginx'], { stdio: 'inherit' });\n } else {\n throw new Error(`Unsupported platform: ${platform}. Please install Nginx manually.`);\n }\n\n console.log(chalk.green('āœ… Nginx installed successfully'));\n }\n\n // Start Nginx service\n console.log(chalk.cyan('šŸš€ Starting Nginx service...'));\n try {\n await execa('sudo', ['systemctl', 'start', 'nginx']);\n await execa('sudo', ['systemctl', 'enable', 'nginx']);\n console.log(chalk.green('āœ… Nginx service started'));\n } catch (error) {\n console.log(chalk.yellow('āš ļø Could not start Nginx via systemctl (might not be available)'));\n }\n }\n\n // Setup Certbot\n if (setupCertbot) {\n console.log(chalk.cyan('\\nšŸ” Installing Certbot...'));\n\n // Check if Certbot is already installed\n try {\n await execa('certbot', ['--version']);\n console.log(chalk.green('āœ… Certbot is already installed'));\n } catch {\n const platform = process.platform;\n\n if (platform === 'linux') {\n // Install Certbot based on package manager\n try {\n await execa('apt-get', ['--version']);\n console.log(chalk.dim('Using apt-get...'));\n await execa('sudo', ['apt-get', 'update'], { stdio: 'inherit' });\n await execa('sudo', ['apt-get', 'install', '-y', 'certbot', 'python3-certbot-nginx'], { stdio: 'inherit' });\n } catch {\n try {\n await execa('yum', ['--version']);\n console.log(chalk.dim('Using yum...'));\n await execa('sudo', ['yum', 'install', '-y', 'certbot', 'python3-certbot-nginx'], { stdio: 'inherit' });\n } catch {\n throw new Error('Unsupported Linux distribution. Please install Certbot manually.');\n }\n }\n } else if (platform === 'darwin') {\n console.log(chalk.dim('Using Homebrew...'));\n await execa('brew', ['install', 'certbot'], { stdio: 'inherit' });\n } else {\n throw new Error(`Unsupported platform: ${platform}. Please install Certbot manually.`);\n }\n\n console.log(chalk.green('āœ… Certbot installed successfully'));\n }\n }\n\n console.log(chalk.green.bold('\\n✨ Setup completed successfully!\\n'));\n console.log(chalk.dim('Next steps:'));\n console.log(chalk.dim(' 1. Create a configuration file: suthep init'));\n console.log(chalk.dim(' 2. Deploy your services: suthep deploy\\n'));\n\n } catch (error) {\n console.error(chalk.red('\\nāŒ Setup failed:'), error instanceof Error ? error.message : error);\n process.exit(1);\n }\n}\n"],"names":[],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { readFileSync } from "fs";
4
+ import { dirname, join } from "path";
5
+ import { fileURLToPath } from "url";
6
+ import { deployCommand } from "./commands/deploy.js";
7
+ import { initCommand } from "./commands/init.js";
8
+ import { setupCommand } from "./commands/setup.js";
9
+ const __filename$1 = fileURLToPath(import.meta.url);
10
+ const __dirname$1 = dirname(__filename$1);
11
+ const packageJsonPath = join(__dirname$1, "..", "package.json");
12
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
13
+ const program = new Command();
14
+ program.name("suthep").description("CLI tool for deploying projects with automatic Nginx reverse proxy and HTTPS setup").version(packageJson.version);
15
+ program.command("init").description("Initialize a new deployment configuration file").option("-f, --file <path>", "Configuration file path", "suthep.yml").action(initCommand);
16
+ program.command("setup").description("Setup Nginx and Certbot on the system").option("--nginx-only", "Only setup Nginx").option("--certbot-only", "Only setup Certbot").action(setupCommand);
17
+ program.command("deploy").description("Deploy a project using the configuration file").option("-f, --file <path>", "Configuration file path", "suthep.yml").option("--no-https", "Skip HTTPS setup").option("--no-nginx", "Skip Nginx configuration").action(deployCommand);
18
+ program.parse();
19
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["import { Command } from 'commander'\nimport { readFileSync } from 'fs'\nimport { dirname, join } from 'path'\nimport { fileURLToPath } from 'url'\nimport { deployCommand } from './commands/deploy'\nimport { initCommand } from './commands/init'\nimport { setupCommand } from './commands/setup'\n\nconst __filename = fileURLToPath(import.meta.url)\nconst __dirname = dirname(__filename)\nconst packageJsonPath = join(__dirname, '..', 'package.json')\nconst packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'))\n\nconst program = new Command()\n\nprogram\n .name('suthep')\n .description('CLI tool for deploying projects with automatic Nginx reverse proxy and HTTPS setup')\n .version(packageJson.version)\n\nprogram\n .command('init')\n .description('Initialize a new deployment configuration file')\n .option('-f, --file <path>', 'Configuration file path', 'suthep.yml')\n .action(initCommand)\n\nprogram\n .command('setup')\n .description('Setup Nginx and Certbot on the system')\n .option('--nginx-only', 'Only setup Nginx')\n .option('--certbot-only', 'Only setup Certbot')\n .action(setupCommand)\n\nprogram\n .command('deploy')\n .description('Deploy a project using the configuration file')\n .option('-f, --file <path>', 'Configuration file path', 'suthep.yml')\n .option('--no-https', 'Skip HTTPS setup')\n .option('--no-nginx', 'Skip Nginx configuration')\n .action(deployCommand)\n\nprogram.parse()\n"],"names":["__filename","__dirname"],"mappings":";;;;;;;AAQA,MAAMA,eAAa,cAAc,YAAY,GAAG;AAChD,MAAMC,cAAY,QAAQD,YAAU;AACpC,MAAM,kBAAkB,KAAKC,aAAW,MAAM,cAAc;AAC5D,MAAM,cAAc,KAAK,MAAM,aAAa,iBAAiB,OAAO,CAAC;AAErE,MAAM,UAAU,IAAI,QAAA;AAEpB,QACG,KAAK,QAAQ,EACb,YAAY,oFAAoF,EAChG,QAAQ,YAAY,OAAO;AAE9B,QACG,QAAQ,MAAM,EACd,YAAY,gDAAgD,EAC5D,OAAO,qBAAqB,2BAA2B,YAAY,EACnE,OAAO,WAAW;AAErB,QACG,QAAQ,OAAO,EACf,YAAY,uCAAuC,EACnD,OAAO,gBAAgB,kBAAkB,EACzC,OAAO,kBAAkB,oBAAoB,EAC7C,OAAO,YAAY;AAEtB,QACG,QAAQ,QAAQ,EAChB,YAAY,+CAA+C,EAC3D,OAAO,qBAAqB,2BAA2B,YAAY,EACnE,OAAO,cAAc,kBAAkB,EACvC,OAAO,cAAc,0BAA0B,EAC/C,OAAO,aAAa;AAEvB,QAAQ,MAAA;"}
@@ -0,0 +1,64 @@
1
+ import { execa } from "execa";
2
+ async function requestCertificate(domain, email, staging = false) {
3
+ const exists = await certificateExists(domain);
4
+ if (exists) {
5
+ throw new Error(
6
+ `Certificate for ${domain} already exists. Use certificateExists() to check before calling this function.`
7
+ );
8
+ }
9
+ const args = [
10
+ "certonly",
11
+ "--nginx",
12
+ "-d",
13
+ domain,
14
+ "--non-interactive",
15
+ "--agree-tos",
16
+ "--email",
17
+ email
18
+ ];
19
+ if (staging) {
20
+ args.push("--staging");
21
+ }
22
+ try {
23
+ await execa("sudo", ["certbot", ...args]);
24
+ } catch (error) {
25
+ const errorMessage = error?.stderr || error?.message || String(error) || "Unknown error";
26
+ const errorLower = errorMessage.toLowerCase();
27
+ if (errorLower.includes("certificate already exists") || errorLower.includes("already have a certificate") || errorLower.includes("duplicate certificate")) {
28
+ throw new Error(`Certificate for ${domain} already exists. Skipping certificate creation.`);
29
+ }
30
+ throw new Error(`Failed to obtain SSL certificate for ${domain}: ${errorMessage}`);
31
+ }
32
+ }
33
+ async function certificateExists(domain) {
34
+ try {
35
+ try {
36
+ await execa("sudo", ["test", "-f", `/etc/letsencrypt/live/${domain}/fullchain.pem`]);
37
+ await execa("sudo", ["test", "-f", `/etc/letsencrypt/live/${domain}/privkey.pem`]);
38
+ return true;
39
+ } catch {
40
+ }
41
+ try {
42
+ const { stdout } = await execa("sudo", ["certbot", "certificates"]);
43
+ const lines = stdout.split("\n");
44
+ for (let i = 0; i < lines.length; i++) {
45
+ const line = lines[i];
46
+ if (line.includes("Domains:") && line.includes(domain)) {
47
+ return true;
48
+ }
49
+ if (line.includes(domain) && (line.includes("/live/") || line.includes("Certificate Name:"))) {
50
+ return true;
51
+ }
52
+ }
53
+ } catch {
54
+ }
55
+ return false;
56
+ } catch (error) {
57
+ return false;
58
+ }
59
+ }
60
+ export {
61
+ certificateExists,
62
+ requestCertificate
63
+ };
64
+ //# sourceMappingURL=certbot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"certbot.js","sources":["../../src/utils/certbot.ts"],"sourcesContent":["import { execa } from 'execa'\n\n/**\n * Request an SSL certificate from Let's Encrypt using Certbot\n */\nexport async function requestCertificate(\n domain: string,\n email: string,\n staging: boolean = false\n): Promise<void> {\n // Check if certificate already exists before requesting\n const exists = await certificateExists(domain)\n if (exists) {\n throw new Error(\n `Certificate for ${domain} already exists. Use certificateExists() to check before calling this function.`\n )\n }\n\n const args = [\n 'certonly',\n '--nginx',\n '-d',\n domain,\n '--non-interactive',\n '--agree-tos',\n '--email',\n email,\n ]\n\n if (staging) {\n args.push('--staging')\n }\n\n try {\n await execa('sudo', ['certbot', ...args])\n } catch (error: any) {\n const errorMessage = error?.stderr || error?.message || String(error) || 'Unknown error'\n const errorLower = errorMessage.toLowerCase()\n\n // Check if error is due to certificate already existing\n if (\n errorLower.includes('certificate already exists') ||\n errorLower.includes('already have a certificate') ||\n errorLower.includes('duplicate certificate')\n ) {\n throw new Error(`Certificate for ${domain} already exists. Skipping certificate creation.`)\n }\n\n throw new Error(`Failed to obtain SSL certificate for ${domain}: ${errorMessage}`)\n }\n}\n\n/**\n * Renew all SSL certificates\n */\nexport async function renewCertificates(): Promise<void> {\n try {\n await execa('sudo', ['certbot', 'renew', '--quiet'])\n } catch (error) {\n throw new Error(\n `Failed to renew SSL certificates: ${error instanceof Error ? error.message : error}`\n )\n }\n}\n\n/**\n * Check if a certificate exists for a domain\n */\nexport async function certificateExists(domain: string): Promise<boolean> {\n try {\n // First, check if certificate files exist using test command (most reliable)\n try {\n await execa('sudo', ['test', '-f', `/etc/letsencrypt/live/${domain}/fullchain.pem`])\n await execa('sudo', ['test', '-f', `/etc/letsencrypt/live/${domain}/privkey.pem`])\n // Both files exist\n return true\n } catch {\n // Files don't exist, continue to certbot check\n }\n\n // Fallback: Check using certbot certificates command\n try {\n const { stdout } = await execa('sudo', ['certbot', 'certificates'])\n\n // Check if the domain appears in the certificates list\n const lines = stdout.split('\\n')\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i]\n // Check if this line contains \"Domains:\" and includes our domain\n if (line.includes('Domains:') && line.includes(domain)) {\n return true\n }\n // Also check for the domain in certificate paths\n if (\n line.includes(domain) &&\n (line.includes('/live/') || line.includes('Certificate Name:'))\n ) {\n return true\n }\n }\n } catch {\n // If certbot command fails, assume no certificate exists\n }\n\n return false\n } catch (error) {\n // If all checks fail, assume no certificate exists\n return false\n }\n}\n\n/**\n * Check certificate expiration for a domain\n */\nexport async function checkCertificateExpiration(domain: string): Promise<Date | null> {\n try {\n const { stdout } = await execa('sudo', ['certbot', 'certificates', '-d', domain])\n\n // Parse expiration date from output\n const expiryMatch = stdout.match(/Expiry Date: ([^\\n]+)/)\n if (expiryMatch) {\n return new Date(expiryMatch[1])\n }\n\n return null\n } catch (error) {\n return null\n }\n}\n\n/**\n * Revoke a certificate for a domain\n */\nexport async function revokeCertificate(domain: string): Promise<void> {\n try {\n await execa('sudo', ['certbot', 'revoke', '-d', domain, '--non-interactive'])\n } catch (error) {\n throw new Error(\n `Failed to revoke certificate for ${domain}: ${\n error instanceof Error ? error.message : error\n }`\n )\n }\n}\n"],"names":[],"mappings":";AAKA,eAAsB,mBACpB,QACA,OACA,UAAmB,OACJ;AAEf,QAAM,SAAS,MAAM,kBAAkB,MAAM;AAC7C,MAAI,QAAQ;AACV,UAAM,IAAI;AAAA,MACR,mBAAmB,MAAM;AAAA,IAAA;AAAA,EAE7B;AAEA,QAAM,OAAO;AAAA,IACX;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAGF,MAAI,SAAS;AACX,SAAK,KAAK,WAAW;AAAA,EACvB;AAEA,MAAI;AACF,UAAM,MAAM,QAAQ,CAAC,WAAW,GAAG,IAAI,CAAC;AAAA,EAC1C,SAAS,OAAY;AACnB,UAAM,eAAe,OAAO,UAAU,OAAO,WAAW,OAAO,KAAK,KAAK;AACzE,UAAM,aAAa,aAAa,YAAA;AAGhC,QACE,WAAW,SAAS,4BAA4B,KAChD,WAAW,SAAS,4BAA4B,KAChD,WAAW,SAAS,uBAAuB,GAC3C;AACA,YAAM,IAAI,MAAM,mBAAmB,MAAM,iDAAiD;AAAA,IAC5F;AAEA,UAAM,IAAI,MAAM,wCAAwC,MAAM,KAAK,YAAY,EAAE;AAAA,EACnF;AACF;AAkBA,eAAsB,kBAAkB,QAAkC;AACxE,MAAI;AAEF,QAAI;AACF,YAAM,MAAM,QAAQ,CAAC,QAAQ,MAAM,yBAAyB,MAAM,gBAAgB,CAAC;AACnF,YAAM,MAAM,QAAQ,CAAC,QAAQ,MAAM,yBAAyB,MAAM,cAAc,CAAC;AAEjF,aAAO;AAAA,IACT,QAAQ;AAAA,IAER;AAGA,QAAI;AACF,YAAM,EAAE,WAAW,MAAM,MAAM,QAAQ,CAAC,WAAW,cAAc,CAAC;AAGlE,YAAM,QAAQ,OAAO,MAAM,IAAI;AAC/B,eAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,cAAM,OAAO,MAAM,CAAC;AAEpB,YAAI,KAAK,SAAS,UAAU,KAAK,KAAK,SAAS,MAAM,GAAG;AACtD,iBAAO;AAAA,QACT;AAEA,YACE,KAAK,SAAS,MAAM,MACnB,KAAK,SAAS,QAAQ,KAAK,KAAK,SAAS,mBAAmB,IAC7D;AACA,iBAAO;AAAA,QACT;AAAA,MACF;AAAA,IACF,QAAQ;AAAA,IAER;AAEA,WAAO;AAAA,EACT,SAAS,OAAO;AAEd,WAAO;AAAA,EACT;AACF;"}
@@ -0,0 +1,95 @@
1
+ import fs from "fs-extra";
2
+ import yaml from "js-yaml";
3
+ async function loadConfig(filePath) {
4
+ try {
5
+ const fileContent = await fs.readFile(filePath, "utf8");
6
+ const config = yaml.load(fileContent);
7
+ validateConfig(config);
8
+ return config;
9
+ } catch (error) {
10
+ if (error instanceof Error) {
11
+ throw new Error(`Failed to load configuration from ${filePath}: ${error.message}`);
12
+ }
13
+ throw error;
14
+ }
15
+ }
16
+ function validateConfig(config) {
17
+ if (!config.project || !config.project.name) {
18
+ throw new Error("Configuration must include project.name");
19
+ }
20
+ if (!config.services || !Array.isArray(config.services) || config.services.length === 0) {
21
+ throw new Error("Configuration must include at least one service");
22
+ }
23
+ const usedPorts = /* @__PURE__ */ new Map();
24
+ const usedContainers = /* @__PURE__ */ new Map();
25
+ for (const service of config.services) {
26
+ if (!service.name) {
27
+ throw new Error("Each service must have a name");
28
+ }
29
+ if (!service.port) {
30
+ throw new Error(`Service ${service.name} must have a port`);
31
+ }
32
+ if (!service.domains || !Array.isArray(service.domains) || service.domains.length === 0) {
33
+ throw new Error(`Service ${service.name} must have at least one domain`);
34
+ }
35
+ if (usedPorts.has(service.port)) {
36
+ const conflictingServices = usedPorts.get(service.port);
37
+ throw new Error(
38
+ `Port conflict: Service "${service.name}" uses port ${service.port} which is already used by: ${conflictingServices.join(
39
+ ", "
40
+ )}. Each service must use a unique port.`
41
+ );
42
+ }
43
+ usedPorts.set(service.port, [service.name]);
44
+ if (service.docker) {
45
+ const containerName = service.docker.container;
46
+ if (usedContainers.has(containerName)) {
47
+ const conflictingService = usedContainers.get(containerName);
48
+ throw new Error(
49
+ `Docker container name conflict: Service "${service.name}" uses container name "${containerName}" which is already used by service "${conflictingService}". Each Docker container must have a unique name.`
50
+ );
51
+ }
52
+ usedContainers.set(containerName, service.name);
53
+ }
54
+ }
55
+ const serviceNames = /* @__PURE__ */ new Set();
56
+ for (const service of config.services) {
57
+ if (serviceNames.has(service.name)) {
58
+ throw new Error(
59
+ `Duplicate service name: "${service.name}" is used multiple times. Each service must have a unique name.`
60
+ );
61
+ }
62
+ serviceNames.add(service.name);
63
+ }
64
+ if (!config.nginx) {
65
+ config.nginx = {
66
+ configPath: "/etc/nginx/sites-available",
67
+ reloadCommand: "sudo nginx -t && sudo systemctl reload nginx"
68
+ };
69
+ }
70
+ if (!config.certbot) {
71
+ config.certbot = {
72
+ email: "",
73
+ staging: false
74
+ };
75
+ }
76
+ if (!config.deployment) {
77
+ config.deployment = {
78
+ strategy: "rolling",
79
+ healthCheckTimeout: 3e4
80
+ };
81
+ }
82
+ }
83
+ async function saveConfig(filePath, config) {
84
+ const yamlContent = yaml.dump(config, {
85
+ indent: 2,
86
+ lineWidth: 120,
87
+ noRefs: true
88
+ });
89
+ await fs.writeFile(filePath, yamlContent, "utf8");
90
+ }
91
+ export {
92
+ loadConfig,
93
+ saveConfig
94
+ };
95
+ //# sourceMappingURL=config-loader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-loader.js","sources":["../../src/utils/config-loader.ts"],"sourcesContent":["import fs from 'fs-extra'\nimport yaml from 'js-yaml'\nimport type { DeployConfig } from '../types/config'\n\n/**\n * Load and parse a YAML configuration file\n */\nexport async function loadConfig(filePath: string): Promise<DeployConfig> {\n try {\n const fileContent = await fs.readFile(filePath, 'utf8')\n const config = yaml.load(fileContent) as DeployConfig\n\n validateConfig(config)\n\n return config\n } catch (error) {\n if (error instanceof Error) {\n throw new Error(`Failed to load configuration from ${filePath}: ${error.message}`)\n }\n throw error\n }\n}\n\n/**\n * Validate the configuration object\n */\nfunction validateConfig(config: any): asserts config is DeployConfig {\n if (!config.project || !config.project.name) {\n throw new Error('Configuration must include project.name')\n }\n\n if (!config.services || !Array.isArray(config.services) || config.services.length === 0) {\n throw new Error('Configuration must include at least one service')\n }\n\n // Track ports and container names to detect conflicts\n const usedPorts = new Map<number, string[]>()\n const usedContainers = new Map<string, string>()\n\n for (const service of config.services) {\n if (!service.name) {\n throw new Error('Each service must have a name')\n }\n if (!service.port) {\n throw new Error(`Service ${service.name} must have a port`)\n }\n if (!service.domains || !Array.isArray(service.domains) || service.domains.length === 0) {\n throw new Error(`Service ${service.name} must have at least one domain`)\n }\n\n // Check for port conflicts\n if (usedPorts.has(service.port)) {\n const conflictingServices = usedPorts.get(service.port)!\n throw new Error(\n `Port conflict: Service \"${service.name}\" uses port ${\n service.port\n } which is already used by: ${conflictingServices.join(\n ', '\n )}. Each service must use a unique port.`\n )\n }\n usedPorts.set(service.port, [service.name])\n\n // Check for Docker container name conflicts\n if (service.docker) {\n const containerName = service.docker.container\n if (usedContainers.has(containerName)) {\n const conflictingService = usedContainers.get(containerName)!\n throw new Error(\n `Docker container name conflict: Service \"${service.name}\" uses container name \"${containerName}\" which is already used by service \"${conflictingService}\". Each Docker container must have a unique name.`\n )\n }\n usedContainers.set(containerName, service.name)\n }\n }\n\n // Check for duplicate service names\n const serviceNames = new Set<string>()\n for (const service of config.services) {\n if (serviceNames.has(service.name)) {\n throw new Error(\n `Duplicate service name: \"${service.name}\" is used multiple times. Each service must have a unique name.`\n )\n }\n serviceNames.add(service.name)\n }\n\n if (!config.nginx) {\n config.nginx = {\n configPath: '/etc/nginx/sites-available',\n reloadCommand: 'sudo nginx -t && sudo systemctl reload nginx',\n }\n }\n\n if (!config.certbot) {\n config.certbot = {\n email: '',\n staging: false,\n }\n }\n\n if (!config.deployment) {\n config.deployment = {\n strategy: 'rolling',\n healthCheckTimeout: 30000,\n }\n }\n}\n\n/**\n * Save configuration to a YAML file\n */\nexport async function saveConfig(filePath: string, config: DeployConfig): Promise<void> {\n const yamlContent = yaml.dump(config, {\n indent: 2,\n lineWidth: 120,\n noRefs: true,\n })\n\n await fs.writeFile(filePath, yamlContent, 'utf8')\n}\n"],"names":[],"mappings":";;AAOA,eAAsB,WAAW,UAAyC;AACxE,MAAI;AACF,UAAM,cAAc,MAAM,GAAG,SAAS,UAAU,MAAM;AACtD,UAAM,SAAS,KAAK,KAAK,WAAW;AAEpC,mBAAe,MAAM;AAErB,WAAO;AAAA,EACT,SAAS,OAAO;AACd,QAAI,iBAAiB,OAAO;AAC1B,YAAM,IAAI,MAAM,qCAAqC,QAAQ,KAAK,MAAM,OAAO,EAAE;AAAA,IACnF;AACA,UAAM;AAAA,EACR;AACF;AAKA,SAAS,eAAe,QAA6C;AACnE,MAAI,CAAC,OAAO,WAAW,CAAC,OAAO,QAAQ,MAAM;AAC3C,UAAM,IAAI,MAAM,yCAAyC;AAAA,EAC3D;AAEA,MAAI,CAAC,OAAO,YAAY,CAAC,MAAM,QAAQ,OAAO,QAAQ,KAAK,OAAO,SAAS,WAAW,GAAG;AACvF,UAAM,IAAI,MAAM,iDAAiD;AAAA,EACnE;AAGA,QAAM,gCAAgB,IAAA;AACtB,QAAM,qCAAqB,IAAA;AAE3B,aAAW,WAAW,OAAO,UAAU;AACrC,QAAI,CAAC,QAAQ,MAAM;AACjB,YAAM,IAAI,MAAM,+BAA+B;AAAA,IACjD;AACA,QAAI,CAAC,QAAQ,MAAM;AACjB,YAAM,IAAI,MAAM,WAAW,QAAQ,IAAI,mBAAmB;AAAA,IAC5D;AACA,QAAI,CAAC,QAAQ,WAAW,CAAC,MAAM,QAAQ,QAAQ,OAAO,KAAK,QAAQ,QAAQ,WAAW,GAAG;AACvF,YAAM,IAAI,MAAM,WAAW,QAAQ,IAAI,gCAAgC;AAAA,IACzE;AAGA,QAAI,UAAU,IAAI,QAAQ,IAAI,GAAG;AAC/B,YAAM,sBAAsB,UAAU,IAAI,QAAQ,IAAI;AACtD,YAAM,IAAI;AAAA,QACR,2BAA2B,QAAQ,IAAI,eACrC,QAAQ,IACV,8BAA8B,oBAAoB;AAAA,UAChD;AAAA,QAAA,CACD;AAAA,MAAA;AAAA,IAEL;AACA,cAAU,IAAI,QAAQ,MAAM,CAAC,QAAQ,IAAI,CAAC;AAG1C,QAAI,QAAQ,QAAQ;AAClB,YAAM,gBAAgB,QAAQ,OAAO;AACrC,UAAI,eAAe,IAAI,aAAa,GAAG;AACrC,cAAM,qBAAqB,eAAe,IAAI,aAAa;AAC3D,cAAM,IAAI;AAAA,UACR,4CAA4C,QAAQ,IAAI,0BAA0B,aAAa,uCAAuC,kBAAkB;AAAA,QAAA;AAAA,MAE5J;AACA,qBAAe,IAAI,eAAe,QAAQ,IAAI;AAAA,IAChD;AAAA,EACF;AAGA,QAAM,mCAAmB,IAAA;AACzB,aAAW,WAAW,OAAO,UAAU;AACrC,QAAI,aAAa,IAAI,QAAQ,IAAI,GAAG;AAClC,YAAM,IAAI;AAAA,QACR,4BAA4B,QAAQ,IAAI;AAAA,MAAA;AAAA,IAE5C;AACA,iBAAa,IAAI,QAAQ,IAAI;AAAA,EAC/B;AAEA,MAAI,CAAC,OAAO,OAAO;AACjB,WAAO,QAAQ;AAAA,MACb,YAAY;AAAA,MACZ,eAAe;AAAA,IAAA;AAAA,EAEnB;AAEA,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO,UAAU;AAAA,MACf,OAAO;AAAA,MACP,SAAS;AAAA,IAAA;AAAA,EAEb;AAEA,MAAI,CAAC,OAAO,YAAY;AACtB,WAAO,aAAa;AAAA,MAClB,UAAU;AAAA,MACV,oBAAoB;AAAA,IAAA;AAAA,EAExB;AACF;AAKA,eAAsB,WAAW,UAAkB,QAAqC;AACtF,QAAM,cAAc,KAAK,KAAK,QAAQ;AAAA,IACpC,QAAQ;AAAA,IACR,WAAW;AAAA,IACX,QAAQ;AAAA,EAAA,CACT;AAED,QAAM,GAAG,UAAU,UAAU,aAAa,MAAM;AAClD;"}
@@ -0,0 +1,76 @@
1
+ async function performHealthCheck(url, timeout = 3e4) {
2
+ const startTime = Date.now();
3
+ const interval = 2e3;
4
+ while (Date.now() - startTime < timeout) {
5
+ try {
6
+ const response = await fetch(url, {
7
+ method: "GET",
8
+ signal: AbortSignal.timeout(5e3)
9
+ // 5 second timeout per request
10
+ });
11
+ if (response.ok) {
12
+ return true;
13
+ }
14
+ } catch (error) {
15
+ }
16
+ await new Promise((resolve) => setTimeout(resolve, interval));
17
+ }
18
+ return false;
19
+ }
20
+ async function deployService(service, deploymentConfig, tempInfo = null) {
21
+ if (deploymentConfig.strategy === "rolling") {
22
+ await rollingDeploy(service, deploymentConfig, tempInfo);
23
+ } else if (deploymentConfig.strategy === "blue-green") {
24
+ await blueGreenDeploy(service, deploymentConfig, tempInfo);
25
+ } else {
26
+ throw new Error(`Unknown deployment strategy: ${deploymentConfig.strategy}`);
27
+ }
28
+ }
29
+ async function rollingDeploy(service, deploymentConfig, tempInfo) {
30
+ if (!tempInfo || !tempInfo.oldContainerExists) {
31
+ if (service.healthCheck) {
32
+ const healthUrl = `http://localhost:${service.port}${service.healthCheck.path}`;
33
+ const isHealthy = await performHealthCheck(healthUrl, deploymentConfig.healthCheckTimeout);
34
+ if (!isHealthy) {
35
+ throw new Error(`Service ${service.name} failed health check during rolling deployment`);
36
+ }
37
+ }
38
+ } else {
39
+ if (service.healthCheck) {
40
+ const healthUrl = `http://localhost:${tempInfo.tempPort}${service.healthCheck.path}`;
41
+ const isHealthy = await performHealthCheck(healthUrl, deploymentConfig.healthCheckTimeout);
42
+ if (!isHealthy) {
43
+ throw new Error(
44
+ `Service ${service.name} failed health check on temporary container during rolling deployment`
45
+ );
46
+ }
47
+ }
48
+ }
49
+ await new Promise((resolve) => setTimeout(resolve, 2e3));
50
+ }
51
+ async function blueGreenDeploy(service, deploymentConfig, tempInfo) {
52
+ if (!tempInfo || !tempInfo.oldContainerExists) {
53
+ if (service.healthCheck) {
54
+ const healthUrl = `http://localhost:${service.port}${service.healthCheck.path}`;
55
+ const isHealthy = await performHealthCheck(healthUrl, deploymentConfig.healthCheckTimeout);
56
+ if (!isHealthy) {
57
+ throw new Error(`Service ${service.name} failed health check during blue-green deployment`);
58
+ }
59
+ }
60
+ } else {
61
+ if (service.healthCheck) {
62
+ const healthUrl = `http://localhost:${tempInfo.tempPort}${service.healthCheck.path}`;
63
+ const isHealthy = await performHealthCheck(healthUrl, deploymentConfig.healthCheckTimeout);
64
+ if (!isHealthy) {
65
+ throw new Error(
66
+ `Service ${service.name} failed health check on temporary container during blue-green deployment`
67
+ );
68
+ }
69
+ }
70
+ }
71
+ }
72
+ export {
73
+ deployService,
74
+ performHealthCheck
75
+ };
76
+ //# sourceMappingURL=deployment.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deployment.js","sources":["../../src/utils/deployment.ts"],"sourcesContent":["import type { DeploymentConfig, ServiceConfig } from '../types/config'\nimport type { ZeroDowntimeContainerInfo } from './docker'\n\n/**\n * Perform a health check on a service endpoint\n */\nexport async function performHealthCheck(url: string, timeout: number = 30000): Promise<boolean> {\n const startTime = Date.now()\n const interval = 2000 // Check every 2 seconds\n\n while (Date.now() - startTime < timeout) {\n try {\n const response = await fetch(url, {\n method: 'GET',\n signal: AbortSignal.timeout(5000), // 5 second timeout per request\n })\n\n if (response.ok) {\n return true\n }\n } catch (error) {\n // Endpoint not ready yet, continue waiting\n }\n\n // Wait before next check\n await new Promise((resolve) => setTimeout(resolve, interval))\n }\n\n return false\n}\n\n/**\n * Deploy a service with zero-downtime strategy\n */\nexport async function deployService(\n service: ServiceConfig,\n deploymentConfig: DeploymentConfig,\n tempInfo: ZeroDowntimeContainerInfo | null = null\n): Promise<void> {\n if (deploymentConfig.strategy === 'rolling') {\n await rollingDeploy(service, deploymentConfig, tempInfo)\n } else if (deploymentConfig.strategy === 'blue-green') {\n await blueGreenDeploy(service, deploymentConfig, tempInfo)\n } else {\n throw new Error(`Unknown deployment strategy: ${deploymentConfig.strategy}`)\n }\n}\n\n/**\n * Rolling deployment strategy\n * For single instance, uses zero-downtime approach similar to blue-green\n */\nasync function rollingDeploy(\n service: ServiceConfig,\n deploymentConfig: DeploymentConfig,\n tempInfo: ZeroDowntimeContainerInfo | null\n): Promise<void> {\n // For rolling deployment with single instance:\n // Similar to blue-green - use temporary container and port\n\n if (!tempInfo || !tempInfo.oldContainerExists) {\n // No existing container, just check health on the new container\n if (service.healthCheck) {\n const healthUrl = `http://localhost:${service.port}${service.healthCheck.path}`\n const isHealthy = await performHealthCheck(healthUrl, deploymentConfig.healthCheckTimeout)\n\n if (!isHealthy) {\n throw new Error(`Service ${service.name} failed health check during rolling deployment`)\n }\n }\n } else {\n // Check health on temporary port\n if (service.healthCheck) {\n const healthUrl = `http://localhost:${tempInfo.tempPort}${service.healthCheck.path}`\n const isHealthy = await performHealthCheck(healthUrl, deploymentConfig.healthCheckTimeout)\n\n if (!isHealthy) {\n throw new Error(\n `Service ${service.name} failed health check on temporary container during rolling deployment`\n )\n }\n }\n }\n\n // Add a small delay to ensure service is fully ready\n await new Promise((resolve) => setTimeout(resolve, 2000))\n}\n\n/**\n * Blue-green deployment strategy for single instance\n * Uses temporary container and port for zero-downtime deployment\n */\nasync function blueGreenDeploy(\n service: ServiceConfig,\n deploymentConfig: DeploymentConfig,\n tempInfo: ZeroDowntimeContainerInfo | null\n): Promise<void> {\n // For blue-green deployment with single instance:\n // 1. New container is already started on temporary port (handled in deploy command)\n // 2. Run health checks on new container\n // 3. Switch nginx to new port (handled in deploy command)\n // 4. Stop old container and promote new one (handled in deploy command)\n\n if (!tempInfo || !tempInfo.oldContainerExists) {\n // No existing container, just check health on the new container\n if (service.healthCheck) {\n const healthUrl = `http://localhost:${service.port}${service.healthCheck.path}`\n const isHealthy = await performHealthCheck(healthUrl, deploymentConfig.healthCheckTimeout)\n\n if (!isHealthy) {\n throw new Error(`Service ${service.name} failed health check during blue-green deployment`)\n }\n }\n } else {\n // Check health on temporary port\n if (service.healthCheck) {\n const healthUrl = `http://localhost:${tempInfo.tempPort}${service.healthCheck.path}`\n const isHealthy = await performHealthCheck(healthUrl, deploymentConfig.healthCheckTimeout)\n\n if (!isHealthy) {\n throw new Error(\n `Service ${service.name} failed health check on temporary container during blue-green deployment`\n )\n }\n }\n }\n}\n\n/**\n * Wait for a service to become healthy\n */\nexport async function waitForService(\n service: ServiceConfig,\n timeout: number = 60000\n): Promise<boolean> {\n if (!service.healthCheck) {\n // No health check configured, assume service is ready after a short delay\n await new Promise((resolve) => setTimeout(resolve, 5000))\n return true\n }\n\n const healthUrl = `http://localhost:${service.port}${service.healthCheck.path}`\n return await performHealthCheck(healthUrl, timeout)\n}\n\n/**\n * Gracefully shutdown a service\n */\nexport async function gracefulShutdown(\n _service: ServiceConfig,\n timeout: number = 30000\n): Promise<void> {\n // Send shutdown signal and wait for graceful termination\n // This is a placeholder - actual implementation would depend on how services are managed\n\n await new Promise((resolve) => setTimeout(resolve, Math.min(timeout, 5000)))\n}\n"],"names":[],"mappings":"AAMA,eAAsB,mBAAmB,KAAa,UAAkB,KAAyB;AAC/F,QAAM,YAAY,KAAK,IAAA;AACvB,QAAM,WAAW;AAEjB,SAAO,KAAK,QAAQ,YAAY,SAAS;AACvC,QAAI;AACF,YAAM,WAAW,MAAM,MAAM,KAAK;AAAA,QAChC,QAAQ;AAAA,QACR,QAAQ,YAAY,QAAQ,GAAI;AAAA;AAAA,MAAA,CACjC;AAED,UAAI,SAAS,IAAI;AACf,eAAO;AAAA,MACT;AAAA,IACF,SAAS,OAAO;AAAA,IAEhB;AAGA,UAAM,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,QAAQ,CAAC;AAAA,EAC9D;AAEA,SAAO;AACT;AAKA,eAAsB,cACpB,SACA,kBACA,WAA6C,MAC9B;AACf,MAAI,iBAAiB,aAAa,WAAW;AAC3C,UAAM,cAAc,SAAS,kBAAkB,QAAQ;AAAA,EACzD,WAAW,iBAAiB,aAAa,cAAc;AACrD,UAAM,gBAAgB,SAAS,kBAAkB,QAAQ;AAAA,EAC3D,OAAO;AACL,UAAM,IAAI,MAAM,gCAAgC,iBAAiB,QAAQ,EAAE;AAAA,EAC7E;AACF;AAMA,eAAe,cACb,SACA,kBACA,UACe;AAIf,MAAI,CAAC,YAAY,CAAC,SAAS,oBAAoB;AAE7C,QAAI,QAAQ,aAAa;AACvB,YAAM,YAAY,oBAAoB,QAAQ,IAAI,GAAG,QAAQ,YAAY,IAAI;AAC7E,YAAM,YAAY,MAAM,mBAAmB,WAAW,iBAAiB,kBAAkB;AAEzF,UAAI,CAAC,WAAW;AACd,cAAM,IAAI,MAAM,WAAW,QAAQ,IAAI,gDAAgD;AAAA,MACzF;AAAA,IACF;AAAA,EACF,OAAO;AAEL,QAAI,QAAQ,aAAa;AACvB,YAAM,YAAY,oBAAoB,SAAS,QAAQ,GAAG,QAAQ,YAAY,IAAI;AAClF,YAAM,YAAY,MAAM,mBAAmB,WAAW,iBAAiB,kBAAkB;AAEzF,UAAI,CAAC,WAAW;AACd,cAAM,IAAI;AAAA,UACR,WAAW,QAAQ,IAAI;AAAA,QAAA;AAAA,MAE3B;AAAA,IACF;AAAA,EACF;AAGA,QAAM,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,GAAI,CAAC;AAC1D;AAMA,eAAe,gBACb,SACA,kBACA,UACe;AAOf,MAAI,CAAC,YAAY,CAAC,SAAS,oBAAoB;AAE7C,QAAI,QAAQ,aAAa;AACvB,YAAM,YAAY,oBAAoB,QAAQ,IAAI,GAAG,QAAQ,YAAY,IAAI;AAC7E,YAAM,YAAY,MAAM,mBAAmB,WAAW,iBAAiB,kBAAkB;AAEzF,UAAI,CAAC,WAAW;AACd,cAAM,IAAI,MAAM,WAAW,QAAQ,IAAI,mDAAmD;AAAA,MAC5F;AAAA,IACF;AAAA,EACF,OAAO;AAEL,QAAI,QAAQ,aAAa;AACvB,YAAM,YAAY,oBAAoB,SAAS,QAAQ,GAAG,QAAQ,YAAY,IAAI;AAClF,YAAM,YAAY,MAAM,mBAAmB,WAAW,iBAAiB,kBAAkB;AAEzF,UAAI,CAAC,WAAW;AACd,cAAM,IAAI;AAAA,UACR,WAAW,QAAQ,IAAI;AAAA,QAAA;AAAA,MAE3B;AAAA,IACF;AAAA,EACF;AACF;"}