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.

Potentially problematic release.


This version of suthep might be problematic. Click here for more details.

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,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,12 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { deployCommand } from "./commands/deploy.js";
4
+ import { initCommand } from "./commands/init.js";
5
+ import { setupCommand } from "./commands/setup.js";
6
+ const program = new Command();
7
+ program.name("suthep").description("CLI tool for deploying projects with automatic Nginx reverse proxy and HTTPS setup").version("0.1.0");
8
+ program.command("init").description("Initialize a new deployment configuration file").option("-f, --file <path>", "Configuration file path", "suthep.yml").action(initCommand);
9
+ 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);
10
+ 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);
11
+ program.parse();
12
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["import { Command } from 'commander'\nimport { deployCommand } from './commands/deploy'\nimport { initCommand } from './commands/init'\nimport { setupCommand } from './commands/setup'\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('0.1.0')\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":[],"mappings":";;;;AAKA,MAAM,UAAU,IAAI,QAAA;AAEpB,QACG,KAAK,QAAQ,EACb,YAAY,oFAAoF,EAChG,QAAQ,OAAO;AAElB,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,27 @@
1
+ import { execa } from "execa";
2
+ async function requestCertificate(domain, email, staging = false) {
3
+ const args = [
4
+ "certonly",
5
+ "--nginx",
6
+ "-d",
7
+ domain,
8
+ "--non-interactive",
9
+ "--agree-tos",
10
+ "--email",
11
+ email
12
+ ];
13
+ if (staging) {
14
+ args.push("--staging");
15
+ }
16
+ try {
17
+ await execa("sudo", ["certbot", ...args]);
18
+ } catch (error) {
19
+ throw new Error(
20
+ `Failed to obtain SSL certificate for ${domain}: ${error instanceof Error ? error.message : error}`
21
+ );
22
+ }
23
+ }
24
+ export {
25
+ requestCertificate
26
+ };
27
+ //# 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 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) {\n throw new Error(\n `Failed to obtain SSL certificate for ${domain}: ${\n error instanceof Error ? error.message : error\n }`\n )\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 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;AACf,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,OAAO;AACd,UAAM,IAAI;AAAA,MACR,wCAAwC,MAAM,KAC5C,iBAAiB,QAAQ,MAAM,UAAU,KAC3C;AAAA,IAAA;AAAA,EAEJ;AACF;"}
@@ -0,0 +1,65 @@
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
+ for (const service of config.services) {
24
+ if (!service.name) {
25
+ throw new Error("Each service must have a name");
26
+ }
27
+ if (!service.port) {
28
+ throw new Error(`Service ${service.name} must have a port`);
29
+ }
30
+ if (!service.domains || !Array.isArray(service.domains) || service.domains.length === 0) {
31
+ throw new Error(`Service ${service.name} must have at least one domain`);
32
+ }
33
+ }
34
+ if (!config.nginx) {
35
+ config.nginx = {
36
+ configPath: "/etc/nginx/sites-available",
37
+ reloadCommand: "sudo nginx -t && sudo systemctl reload nginx"
38
+ };
39
+ }
40
+ if (!config.certbot) {
41
+ config.certbot = {
42
+ email: "",
43
+ staging: false
44
+ };
45
+ }
46
+ if (!config.deployment) {
47
+ config.deployment = {
48
+ strategy: "rolling",
49
+ healthCheckTimeout: 3e4
50
+ };
51
+ }
52
+ }
53
+ async function saveConfig(filePath, config) {
54
+ const yamlContent = yaml.dump(config, {
55
+ indent: 2,
56
+ lineWidth: 120,
57
+ noRefs: true
58
+ });
59
+ await fs.writeFile(filePath, yamlContent, "utf8");
60
+ }
61
+ export {
62
+ loadConfig,
63
+ saveConfig
64
+ };
65
+ //# 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 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\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;AAEA,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;AAAA,EACF;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,52 @@
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) {
21
+ if (deploymentConfig.strategy === "rolling") {
22
+ await rollingDeploy(service, deploymentConfig);
23
+ } else if (deploymentConfig.strategy === "blue-green") {
24
+ await blueGreenDeploy(service, deploymentConfig);
25
+ } else {
26
+ throw new Error(`Unknown deployment strategy: ${deploymentConfig.strategy}`);
27
+ }
28
+ }
29
+ async function rollingDeploy(service, deploymentConfig) {
30
+ if (service.healthCheck) {
31
+ const healthUrl = `http://localhost:${service.port}${service.healthCheck.path}`;
32
+ const isHealthy = await performHealthCheck(healthUrl, deploymentConfig.healthCheckTimeout);
33
+ if (!isHealthy) {
34
+ throw new Error(`Service ${service.name} failed health check during rolling deployment`);
35
+ }
36
+ }
37
+ await new Promise((resolve) => setTimeout(resolve, 2e3));
38
+ }
39
+ async function blueGreenDeploy(service, deploymentConfig) {
40
+ if (service.healthCheck) {
41
+ const healthUrl = `http://localhost:${service.port}${service.healthCheck.path}`;
42
+ const isHealthy = await performHealthCheck(healthUrl, deploymentConfig.healthCheckTimeout);
43
+ if (!isHealthy) {
44
+ throw new Error(`Service ${service.name} failed health check during blue-green deployment`);
45
+ }
46
+ }
47
+ }
48
+ export {
49
+ deployService,
50
+ performHealthCheck
51
+ };
52
+ //# 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'\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): Promise<void> {\n if (deploymentConfig.strategy === 'rolling') {\n await rollingDeploy(service, deploymentConfig)\n } else if (deploymentConfig.strategy === 'blue-green') {\n await blueGreenDeploy(service, deploymentConfig)\n } else {\n throw new Error(`Unknown deployment strategy: ${deploymentConfig.strategy}`)\n }\n}\n\n/**\n * Rolling deployment strategy\n * Gradually replaces old instances with new ones\n */\nasync function rollingDeploy(\n service: ServiceConfig,\n deploymentConfig: DeploymentConfig\n): Promise<void> {\n // For rolling deployment:\n // 1. Start new service instance\n // 2. Wait for health check\n // 3. Switch traffic to new instance\n // 4. Stop old instance (if applicable)\n\n // In this implementation, we assume the service is already running\n // and we're just verifying it's healthy before proceeding\n\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\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\n * Maintains two identical environments and switches between them\n */\nasync function blueGreenDeploy(\n service: ServiceConfig,\n deploymentConfig: DeploymentConfig\n): Promise<void> {\n // For blue-green deployment:\n // 1. Deploy to \"green\" environment\n // 2. Run health checks on green\n // 3. Switch router/load balancer to green\n // 4. Keep blue as backup\n\n // This is a simplified implementation\n // In production, you'd manage multiple service instances\n\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}\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":"AAKA,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,kBACe;AACf,MAAI,iBAAiB,aAAa,WAAW;AAC3C,UAAM,cAAc,SAAS,gBAAgB;AAAA,EAC/C,WAAW,iBAAiB,aAAa,cAAc;AACrD,UAAM,gBAAgB,SAAS,gBAAgB;AAAA,EACjD,OAAO;AACL,UAAM,IAAI,MAAM,gCAAgC,iBAAiB,QAAQ,EAAE;AAAA,EAC7E;AACF;AAMA,eAAe,cACb,SACA,kBACe;AAUf,MAAI,QAAQ,aAAa;AACvB,UAAM,YAAY,oBAAoB,QAAQ,IAAI,GAAG,QAAQ,YAAY,IAAI;AAC7E,UAAM,YAAY,MAAM,mBAAmB,WAAW,iBAAiB,kBAAkB;AAEzF,QAAI,CAAC,WAAW;AACd,YAAM,IAAI,MAAM,WAAW,QAAQ,IAAI,gDAAgD;AAAA,IACzF;AAAA,EACF;AAGA,QAAM,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,GAAI,CAAC;AAC1D;AAMA,eAAe,gBACb,SACA,kBACe;AAUf,MAAI,QAAQ,aAAa;AACvB,UAAM,YAAY,oBAAoB,QAAQ,IAAI,GAAG,QAAQ,YAAY,IAAI;AAC7E,UAAM,YAAY,MAAM,mBAAmB,WAAW,iBAAiB,kBAAkB;AAEzF,QAAI,CAAC,WAAW;AACd,YAAM,IAAI,MAAM,WAAW,QAAQ,IAAI,mDAAmD;AAAA,IAC5F;AAAA,EACF;AACF;"}
@@ -0,0 +1,57 @@
1
+ import { execa } from "execa";
2
+ async function startDockerContainer(service) {
3
+ if (!service.docker) {
4
+ return;
5
+ }
6
+ const { image, container, port } = service.docker;
7
+ try {
8
+ const { stdout: existingContainers } = await execa("docker", [
9
+ "ps",
10
+ "-a",
11
+ "--filter",
12
+ `name=${container}`,
13
+ "--format",
14
+ "{{.Names}}"
15
+ ]);
16
+ if (existingContainers.includes(container)) {
17
+ const { stdout: runningContainers } = await execa("docker", [
18
+ "ps",
19
+ "--filter",
20
+ `name=${container}`,
21
+ "--format",
22
+ "{{.Names}}"
23
+ ]);
24
+ if (!runningContainers.includes(container)) {
25
+ await execa("docker", ["start", container]);
26
+ }
27
+ } else if (image) {
28
+ const args = [
29
+ "run",
30
+ "-d",
31
+ "--name",
32
+ container,
33
+ "-p",
34
+ `${service.port}:${port}`,
35
+ "--restart",
36
+ "unless-stopped"
37
+ ];
38
+ if (service.environment) {
39
+ for (const [key, value] of Object.entries(service.environment)) {
40
+ args.push("-e", `${key}=${value}`);
41
+ }
42
+ }
43
+ args.push(image);
44
+ await execa("docker", args);
45
+ } else {
46
+ throw new Error(`Container ${container} not found and no image specified`);
47
+ }
48
+ } catch (error) {
49
+ throw new Error(
50
+ `Failed to start Docker container: ${error instanceof Error ? error.message : error}`
51
+ );
52
+ }
53
+ }
54
+ export {
55
+ startDockerContainer
56
+ };
57
+ //# sourceMappingURL=docker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"docker.js","sources":["../../src/utils/docker.ts"],"sourcesContent":["import { execa } from 'execa'\nimport type { ServiceConfig } from '../types/config'\n\n/**\n * Start or connect to a Docker container for a service\n */\nexport async function startDockerContainer(service: ServiceConfig): Promise<void> {\n if (!service.docker) {\n return\n }\n\n const { image, container, port } = service.docker\n\n try {\n // Check if container exists\n const { stdout: existingContainers } = await execa('docker', [\n 'ps',\n '-a',\n '--filter',\n `name=${container}`,\n '--format',\n '{{.Names}}',\n ])\n\n if (existingContainers.includes(container)) {\n // Container exists, check if it's running\n const { stdout: runningContainers } = await execa('docker', [\n 'ps',\n '--filter',\n `name=${container}`,\n '--format',\n '{{.Names}}',\n ])\n\n if (!runningContainers.includes(container)) {\n // Container exists but not running, start it\n await execa('docker', ['start', container])\n }\n } else if (image) {\n // Container doesn't exist and image is provided, create and run it\n const args = [\n 'run',\n '-d',\n '--name',\n container,\n '-p',\n `${service.port}:${port}`,\n '--restart',\n 'unless-stopped',\n ]\n\n // Add environment variables if configured\n if (service.environment) {\n for (const [key, value] of Object.entries(service.environment)) {\n args.push('-e', `${key}=${value}`)\n }\n }\n\n args.push(image)\n\n await execa('docker', args)\n } else {\n throw new Error(`Container ${container} not found and no image specified`)\n }\n } catch (error) {\n throw new Error(\n `Failed to start Docker container: ${error instanceof Error ? error.message : error}`\n )\n }\n}\n\n/**\n * Stop a Docker container\n */\nexport async function stopDockerContainer(containerName: string): Promise<void> {\n try {\n await execa('docker', ['stop', containerName])\n } catch (error) {\n throw new Error(\n `Failed to stop container ${containerName}: ${error instanceof Error ? error.message : error}`\n )\n }\n}\n\n/**\n * Remove a Docker container\n */\nexport async function removeDockerContainer(containerName: string): Promise<void> {\n try {\n await execa('docker', ['rm', '-f', containerName])\n } catch (error) {\n throw new Error(\n `Failed to remove container ${containerName}: ${\n error instanceof Error ? error.message : error\n }`\n )\n }\n}\n\n/**\n * Check if a Docker container is running\n */\nexport async function isContainerRunning(containerName: string): Promise<boolean> {\n try {\n const { stdout } = await execa('docker', [\n 'ps',\n '--filter',\n `name=${containerName}`,\n '--format',\n '{{.Names}}',\n ])\n return stdout.includes(containerName)\n } catch (error) {\n return false\n }\n}\n\n/**\n * Get container logs\n */\nexport async function getContainerLogs(\n containerName: string,\n lines: number = 100\n): Promise<string> {\n try {\n const { stdout } = await execa('docker', ['logs', '--tail', lines.toString(), containerName])\n return stdout\n } catch (error) {\n throw new Error(\n `Failed to get logs for container ${containerName}: ${\n error instanceof Error ? error.message : error\n }`\n )\n }\n}\n\n/**\n * Inspect a Docker container\n */\nexport async function inspectContainer(containerName: string): Promise<any> {\n try {\n const { stdout } = await execa('docker', ['inspect', containerName])\n return JSON.parse(stdout)[0]\n } catch (error) {\n throw new Error(\n `Failed to inspect container ${containerName}: ${\n error instanceof Error ? error.message : error\n }`\n )\n }\n}\n"],"names":[],"mappings":";AAMA,eAAsB,qBAAqB,SAAuC;AAChF,MAAI,CAAC,QAAQ,QAAQ;AACnB;AAAA,EACF;AAEA,QAAM,EAAE,OAAO,WAAW,KAAA,IAAS,QAAQ;AAE3C,MAAI;AAEF,UAAM,EAAE,QAAQ,mBAAA,IAAuB,MAAM,MAAM,UAAU;AAAA,MAC3D;AAAA,MACA;AAAA,MACA;AAAA,MACA,QAAQ,SAAS;AAAA,MACjB;AAAA,MACA;AAAA,IAAA,CACD;AAED,QAAI,mBAAmB,SAAS,SAAS,GAAG;AAE1C,YAAM,EAAE,QAAQ,kBAAA,IAAsB,MAAM,MAAM,UAAU;AAAA,QAC1D;AAAA,QACA;AAAA,QACA,QAAQ,SAAS;AAAA,QACjB;AAAA,QACA;AAAA,MAAA,CACD;AAED,UAAI,CAAC,kBAAkB,SAAS,SAAS,GAAG;AAE1C,cAAM,MAAM,UAAU,CAAC,SAAS,SAAS,CAAC;AAAA,MAC5C;AAAA,IACF,WAAW,OAAO;AAEhB,YAAM,OAAO;AAAA,QACX;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,GAAG,QAAQ,IAAI,IAAI,IAAI;AAAA,QACvB;AAAA,QACA;AAAA,MAAA;AAIF,UAAI,QAAQ,aAAa;AACvB,mBAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,QAAQ,WAAW,GAAG;AAC9D,eAAK,KAAK,MAAM,GAAG,GAAG,IAAI,KAAK,EAAE;AAAA,QACnC;AAAA,MACF;AAEA,WAAK,KAAK,KAAK;AAEf,YAAM,MAAM,UAAU,IAAI;AAAA,IAC5B,OAAO;AACL,YAAM,IAAI,MAAM,aAAa,SAAS,mCAAmC;AAAA,IAC3E;AAAA,EACF,SAAS,OAAO;AACd,UAAM,IAAI;AAAA,MACR,qCAAqC,iBAAiB,QAAQ,MAAM,UAAU,KAAK;AAAA,IAAA;AAAA,EAEvF;AACF;"}
@@ -0,0 +1,154 @@
1
+ import { execa } from "execa";
2
+ import fs from "fs-extra";
3
+ import path from "path";
4
+ function generateNginxConfig(service, withHttps) {
5
+ const serverNames = service.domains.join(" ");
6
+ const upstreamName = `${service.name}_backend`;
7
+ let config = `# Nginx configuration for ${service.name}
8
+
9
+ `;
10
+ config += `upstream ${upstreamName} {
11
+ `;
12
+ config += ` server localhost:${service.port} max_fails=3 fail_timeout=30s;
13
+ `;
14
+ config += ` keepalive 32;
15
+ `;
16
+ config += `}
17
+
18
+ `;
19
+ if (withHttps) {
20
+ config += `server {
21
+ `;
22
+ config += ` listen 80;
23
+ `;
24
+ config += ` listen [::]:80;
25
+ `;
26
+ config += ` server_name ${serverNames};
27
+
28
+ `;
29
+ config += ` # Redirect all HTTP to HTTPS
30
+ `;
31
+ config += ` return 301 https://$server_name$request_uri;
32
+ `;
33
+ config += `}
34
+
35
+ `;
36
+ config += `server {
37
+ `;
38
+ config += ` listen 443 ssl http2;
39
+ `;
40
+ config += ` listen [::]:443 ssl http2;
41
+ `;
42
+ config += ` server_name ${serverNames};
43
+
44
+ `;
45
+ const primaryDomain = service.domains[0];
46
+ config += ` # SSL Configuration
47
+ `;
48
+ config += ` ssl_certificate /etc/letsencrypt/live/${primaryDomain}/fullchain.pem;
49
+ `;
50
+ config += ` ssl_certificate_key /etc/letsencrypt/live/${primaryDomain}/privkey.pem;
51
+ `;
52
+ config += ` ssl_protocols TLSv1.2 TLSv1.3;
53
+ `;
54
+ config += ` ssl_ciphers HIGH:!aNULL:!MD5;
55
+ `;
56
+ config += ` ssl_prefer_server_ciphers on;
57
+
58
+ `;
59
+ } else {
60
+ config += `server {
61
+ `;
62
+ config += ` listen 80;
63
+ `;
64
+ config += ` listen [::]:80;
65
+ `;
66
+ config += ` server_name ${serverNames};
67
+
68
+ `;
69
+ }
70
+ config += ` # Logging
71
+ `;
72
+ config += ` access_log /var/log/nginx/${service.name}_access.log;
73
+ `;
74
+ config += ` error_log /var/log/nginx/${service.name}_error.log;
75
+
76
+ `;
77
+ config += ` # Client settings
78
+ `;
79
+ config += ` client_max_body_size 100M;
80
+
81
+ `;
82
+ config += ` # Proxy settings
83
+ `;
84
+ config += ` location / {
85
+ `;
86
+ config += ` proxy_pass http://${upstreamName};
87
+ `;
88
+ config += ` proxy_http_version 1.1;
89
+ `;
90
+ config += ` proxy_set_header Upgrade $http_upgrade;
91
+ `;
92
+ config += ` proxy_set_header Connection 'upgrade';
93
+ `;
94
+ config += ` proxy_set_header Host $host;
95
+ `;
96
+ config += ` proxy_set_header X-Real-IP $remote_addr;
97
+ `;
98
+ config += ` proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
99
+ `;
100
+ config += ` proxy_set_header X-Forwarded-Proto $scheme;
101
+ `;
102
+ config += ` proxy_cache_bypass $http_upgrade;
103
+ `;
104
+ config += ` proxy_connect_timeout 60s;
105
+ `;
106
+ config += ` proxy_send_timeout 60s;
107
+ `;
108
+ config += ` proxy_read_timeout 60s;
109
+ `;
110
+ config += ` }
111
+ `;
112
+ if (service.healthCheck) {
113
+ config += `
114
+ # Health check endpoint
115
+ `;
116
+ config += ` location ${service.healthCheck.path} {
117
+ `;
118
+ config += ` proxy_pass http://${upstreamName};
119
+ `;
120
+ config += ` access_log off;
121
+ `;
122
+ config += ` }
123
+ `;
124
+ }
125
+ config += `}
126
+ `;
127
+ return config;
128
+ }
129
+ async function enableSite(siteName, configPath) {
130
+ const availablePath = path.join(configPath, `${siteName}.conf`);
131
+ const enabledPath = availablePath.replace("sites-available", "sites-enabled");
132
+ await fs.ensureDir(path.dirname(enabledPath));
133
+ if (await fs.pathExists(enabledPath)) {
134
+ await fs.remove(enabledPath);
135
+ }
136
+ await execa("sudo", ["ln", "-sf", availablePath, enabledPath]);
137
+ }
138
+ async function reloadNginx(reloadCommand) {
139
+ try {
140
+ await execa("sudo", ["nginx", "-t"]);
141
+ const parts = reloadCommand.split(" ");
142
+ if (parts.length > 0) {
143
+ await execa(parts[0], parts.slice(1), { shell: true });
144
+ }
145
+ } catch (error) {
146
+ throw new Error(`Failed to reload Nginx: ${error instanceof Error ? error.message : error}`);
147
+ }
148
+ }
149
+ export {
150
+ enableSite,
151
+ generateNginxConfig,
152
+ reloadNginx
153
+ };
154
+ //# sourceMappingURL=nginx.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nginx.js","sources":["../../src/utils/nginx.ts"],"sourcesContent":["import { execa } from 'execa'\nimport fs from 'fs-extra'\nimport path from 'path'\nimport type { ServiceConfig } from '../types/config'\n\n/**\n * Generate Nginx server block configuration for a service\n */\nexport function generateNginxConfig(service: ServiceConfig, withHttps: boolean): string {\n const serverNames = service.domains.join(' ')\n const upstreamName = `${service.name}_backend`\n\n let config = `# Nginx configuration for ${service.name}\\n\\n`\n\n // Upstream configuration\n config += `upstream ${upstreamName} {\\n`\n config += ` server localhost:${service.port} max_fails=3 fail_timeout=30s;\\n`\n config += ` keepalive 32;\\n`\n config += `}\\n\\n`\n\n if (withHttps) {\n // HTTP server - redirect to HTTPS\n config += `server {\\n`\n config += ` listen 80;\\n`\n config += ` listen [::]:80;\\n`\n config += ` server_name ${serverNames};\\n\\n`\n config += ` # Redirect all HTTP to HTTPS\\n`\n config += ` return 301 https://$server_name$request_uri;\\n`\n config += `}\\n\\n`\n\n // HTTPS server\n config += `server {\\n`\n config += ` listen 443 ssl http2;\\n`\n config += ` listen [::]:443 ssl http2;\\n`\n config += ` server_name ${serverNames};\\n\\n`\n\n // SSL configuration\n const primaryDomain = service.domains[0]\n config += ` # SSL Configuration\\n`\n config += ` ssl_certificate /etc/letsencrypt/live/${primaryDomain}/fullchain.pem;\\n`\n config += ` ssl_certificate_key /etc/letsencrypt/live/${primaryDomain}/privkey.pem;\\n`\n config += ` ssl_protocols TLSv1.2 TLSv1.3;\\n`\n config += ` ssl_ciphers HIGH:!aNULL:!MD5;\\n`\n config += ` ssl_prefer_server_ciphers on;\\n\\n`\n } else {\n // HTTP only server\n config += `server {\\n`\n config += ` listen 80;\\n`\n config += ` listen [::]:80;\\n`\n config += ` server_name ${serverNames};\\n\\n`\n }\n\n // Logging\n config += ` # Logging\\n`\n config += ` access_log /var/log/nginx/${service.name}_access.log;\\n`\n config += ` error_log /var/log/nginx/${service.name}_error.log;\\n\\n`\n\n // Client settings\n config += ` # Client settings\\n`\n config += ` client_max_body_size 100M;\\n\\n`\n\n // Proxy settings\n config += ` # Proxy settings\\n`\n config += ` location / {\\n`\n config += ` proxy_pass http://${upstreamName};\\n`\n config += ` proxy_http_version 1.1;\\n`\n config += ` proxy_set_header Upgrade $http_upgrade;\\n`\n config += ` proxy_set_header Connection 'upgrade';\\n`\n config += ` proxy_set_header Host $host;\\n`\n config += ` proxy_set_header X-Real-IP $remote_addr;\\n`\n config += ` proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\\n`\n config += ` proxy_set_header X-Forwarded-Proto $scheme;\\n`\n config += ` proxy_cache_bypass $http_upgrade;\\n`\n config += ` proxy_connect_timeout 60s;\\n`\n config += ` proxy_send_timeout 60s;\\n`\n config += ` proxy_read_timeout 60s;\\n`\n config += ` }\\n`\n\n // Health check endpoint (if configured)\n if (service.healthCheck) {\n config += `\\n # Health check endpoint\\n`\n config += ` location ${service.healthCheck.path} {\\n`\n config += ` proxy_pass http://${upstreamName};\\n`\n config += ` access_log off;\\n`\n config += ` }\\n`\n }\n\n config += `}\\n`\n\n return config\n}\n\n/**\n * Enable an Nginx site by creating a symbolic link\n */\nexport async function enableSite(siteName: string, configPath: string): Promise<void> {\n const availablePath = path.join(configPath, `${siteName}.conf`)\n const enabledPath = availablePath.replace('sites-available', 'sites-enabled')\n\n // Create sites-enabled directory if it doesn't exist\n await fs.ensureDir(path.dirname(enabledPath))\n\n // Remove existing symlink if present\n if (await fs.pathExists(enabledPath)) {\n await fs.remove(enabledPath)\n }\n\n // Create symlink\n await execa('sudo', ['ln', '-sf', availablePath, enabledPath])\n}\n\n/**\n * Test and reload Nginx configuration\n */\nexport async function reloadNginx(reloadCommand: string): Promise<void> {\n try {\n // Test configuration first\n await execa('sudo', ['nginx', '-t'])\n\n // Reload Nginx\n const parts = reloadCommand.split(' ')\n if (parts.length > 0) {\n // Simple execution of provided command\n await execa(parts[0], parts.slice(1), { shell: true })\n }\n } catch (error) {\n throw new Error(`Failed to reload Nginx: ${error instanceof Error ? error.message : error}`)\n }\n}\n\n/**\n * Disable an Nginx site\n */\nexport async function disableSite(siteName: string, configPath: string): Promise<void> {\n const enabledPath = path.join(\n configPath.replace('sites-available', 'sites-enabled'),\n `${siteName}.conf`\n )\n\n if (await fs.pathExists(enabledPath)) {\n await fs.remove(enabledPath)\n }\n}\n"],"names":[],"mappings":";;;AAQO,SAAS,oBAAoB,SAAwB,WAA4B;AACtF,QAAM,cAAc,QAAQ,QAAQ,KAAK,GAAG;AAC5C,QAAM,eAAe,GAAG,QAAQ,IAAI;AAEpC,MAAI,SAAS,6BAA6B,QAAQ,IAAI;AAAA;AAAA;AAGtD,YAAU,YAAY,YAAY;AAAA;AAClC,YAAU,wBAAwB,QAAQ,IAAI;AAAA;AAC9C,YAAU;AAAA;AACV,YAAU;AAAA;AAAA;AAEV,MAAI,WAAW;AAEb,cAAU;AAAA;AACV,cAAU;AAAA;AACV,cAAU;AAAA;AACV,cAAU,mBAAmB,WAAW;AAAA;AAAA;AACxC,cAAU;AAAA;AACV,cAAU;AAAA;AACV,cAAU;AAAA;AAAA;AAGV,cAAU;AAAA;AACV,cAAU;AAAA;AACV,cAAU;AAAA;AACV,cAAU,mBAAmB,WAAW;AAAA;AAAA;AAGxC,UAAM,gBAAgB,QAAQ,QAAQ,CAAC;AACvC,cAAU;AAAA;AACV,cAAU,6CAA6C,aAAa;AAAA;AACpE,cAAU,iDAAiD,aAAa;AAAA;AACxE,cAAU;AAAA;AACV,cAAU;AAAA;AACV,cAAU;AAAA;AAAA;AAAA,EACZ,OAAO;AAEL,cAAU;AAAA;AACV,cAAU;AAAA;AACV,cAAU;AAAA;AACV,cAAU,mBAAmB,WAAW;AAAA;AAAA;AAAA,EAC1C;AAGA,YAAU;AAAA;AACV,YAAU,iCAAiC,QAAQ,IAAI;AAAA;AACvD,YAAU,gCAAgC,QAAQ,IAAI;AAAA;AAAA;AAGtD,YAAU;AAAA;AACV,YAAU;AAAA;AAAA;AAGV,YAAU;AAAA;AACV,YAAU;AAAA;AACV,YAAU,6BAA6B,YAAY;AAAA;AACnD,YAAU;AAAA;AACV,YAAU;AAAA;AACV,YAAU;AAAA;AACV,YAAU;AAAA;AACV,YAAU;AAAA;AACV,YAAU;AAAA;AACV,YAAU;AAAA;AACV,YAAU;AAAA;AACV,YAAU;AAAA;AACV,YAAU;AAAA;AACV,YAAU;AAAA;AACV,YAAU;AAAA;AAGV,MAAI,QAAQ,aAAa;AACvB,cAAU;AAAA;AAAA;AACV,cAAU,gBAAgB,QAAQ,YAAY,IAAI;AAAA;AAClD,cAAU,6BAA6B,YAAY;AAAA;AACnD,cAAU;AAAA;AACV,cAAU;AAAA;AAAA,EACZ;AAEA,YAAU;AAAA;AAEV,SAAO;AACT;AAKA,eAAsB,WAAW,UAAkB,YAAmC;AACpF,QAAM,gBAAgB,KAAK,KAAK,YAAY,GAAG,QAAQ,OAAO;AAC9D,QAAM,cAAc,cAAc,QAAQ,mBAAmB,eAAe;AAG5E,QAAM,GAAG,UAAU,KAAK,QAAQ,WAAW,CAAC;AAG5C,MAAI,MAAM,GAAG,WAAW,WAAW,GAAG;AACpC,UAAM,GAAG,OAAO,WAAW;AAAA,EAC7B;AAGA,QAAM,MAAM,QAAQ,CAAC,MAAM,OAAO,eAAe,WAAW,CAAC;AAC/D;AAKA,eAAsB,YAAY,eAAsC;AACtE,MAAI;AAEF,UAAM,MAAM,QAAQ,CAAC,SAAS,IAAI,CAAC;AAGnC,UAAM,QAAQ,cAAc,MAAM,GAAG;AACrC,QAAI,MAAM,SAAS,GAAG;AAEpB,YAAM,MAAM,MAAM,CAAC,GAAG,MAAM,MAAM,CAAC,GAAG,EAAE,OAAO,MAAM;AAAA,IACvD;AAAA,EACF,SAAS,OAAO;AACd,UAAM,IAAI,MAAM,2BAA2B,iBAAiB,QAAQ,MAAM,UAAU,KAAK,EAAE;AAAA,EAC7F;AACF;"}
package/docs/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # Suthep Documentation
2
+
3
+ Welcome to the Suthep documentation! This guide will help you understand and use Suthep, a powerful CLI tool for deploying projects with automatic Nginx reverse proxy setup, HTTPS with Certbot, and zero-downtime deployments.
4
+
5
+ ## Table of Contents
6
+
7
+ 1. [Getting Started](./getting-started.md) - Installation and quick start guide
8
+ 2. [Configuration Reference](./configuration.md) - Complete configuration file reference
9
+ 3. [Commands Reference](./commands.md) - Detailed command documentation
10
+ 4. [Architecture](./architecture.md) - How Suthep works under the hood
11
+ 5. [Examples](./examples.md) - Real-world deployment examples
12
+ 6. [Troubleshooting](./troubleshooting.md) - Common issues and solutions
13
+ 7. [API Reference](./api-reference.md) - Internal utilities and functions
14
+
15
+ ## What is Suthep?
16
+
17
+ Suthep is a deployment automation tool that simplifies the process of deploying web services with:
18
+
19
+ - āœ… **Automatic Nginx reverse proxy setup** - No manual Nginx configuration needed
20
+ - āœ… **Automatic HTTPS with Certbot** - Free SSL certificates from Let's Encrypt
21
+ - āœ… **Zero-downtime deployment** - Rolling and blue-green deployment strategies
22
+ - āœ… **Docker container support** - Deploy containerized applications easily
23
+ - āœ… **Multiple domain/subdomain support** - One service, multiple domains
24
+ - āœ… **Health check integration** - Ensure services are healthy before going live
25
+ - āœ… **YAML-based configuration** - Simple, declarative configuration
26
+
27
+ ## Quick Start
28
+
29
+ ```bash
30
+ # Install dependencies
31
+ npm install
32
+ npm link
33
+
34
+ # Initialize configuration
35
+ suthep init
36
+
37
+ # Setup prerequisites
38
+ suthep setup
39
+
40
+ # Deploy your services
41
+ suthep deploy
42
+ ```
43
+
44
+ ## Requirements
45
+
46
+ - Node.js 16+
47
+ - Nginx (installed via `suthep setup`)
48
+ - Certbot (installed via `suthep setup`)
49
+ - Docker (optional, for Docker-based services)
50
+ - sudo access (for Nginx and Certbot operations)
51
+
52
+ ## Cost Optimization
53
+
54
+ Suthep helps save costs on VMs by:
55
+ - Efficiently managing multiple services on a single server
56
+ - Automatic reverse proxy setup reduces manual configuration time
57
+ - Zero-downtime deployments reduce service interruptions
58
+ - Health checks ensure service reliability
59
+
60
+ ## License
61
+
62
+ [MIT](../LICENSE)