@aaricchen1991/deploy 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/PUBLISHING.md +131 -0
  2. package/README.md +256 -0
  3. package/dist/cli.d.ts +6 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +95 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/commands/init.d.ts +10 -0
  8. package/dist/commands/init.d.ts.map +1 -0
  9. package/dist/commands/init.js +104 -0
  10. package/dist/commands/init.js.map +1 -0
  11. package/dist/commands/nginx.d.ts +10 -0
  12. package/dist/commands/nginx.d.ts.map +1 -0
  13. package/dist/commands/nginx.js +70 -0
  14. package/dist/commands/nginx.js.map +1 -0
  15. package/dist/commands/ssl-logs.d.ts +17 -0
  16. package/dist/commands/ssl-logs.d.ts.map +1 -0
  17. package/dist/commands/ssl-logs.js +54 -0
  18. package/dist/commands/ssl-logs.js.map +1 -0
  19. package/dist/commands/ssl.d.ts +16 -0
  20. package/dist/commands/ssl.d.ts.map +1 -0
  21. package/dist/commands/ssl.js +98 -0
  22. package/dist/commands/ssl.js.map +1 -0
  23. package/dist/lib/domains.d.ts +22 -0
  24. package/dist/lib/domains.d.ts.map +1 -0
  25. package/dist/lib/domains.js +70 -0
  26. package/dist/lib/domains.js.map +1 -0
  27. package/dist/lib/logger.d.ts +14 -0
  28. package/dist/lib/logger.d.ts.map +1 -0
  29. package/dist/lib/logger.js +60 -0
  30. package/dist/lib/logger.js.map +1 -0
  31. package/dist/lib/nginx.d.ts +6 -0
  32. package/dist/lib/nginx.d.ts.map +1 -0
  33. package/dist/lib/nginx.js +113 -0
  34. package/dist/lib/nginx.js.map +1 -0
  35. package/dist/lib/paths.d.ts +8 -0
  36. package/dist/lib/paths.d.ts.map +1 -0
  37. package/dist/lib/paths.js +28 -0
  38. package/dist/lib/paths.js.map +1 -0
  39. package/package.json +24 -0
  40. package/src/cli.ts +122 -0
  41. package/src/commands/init.ts +127 -0
  42. package/src/commands/nginx.ts +82 -0
  43. package/src/commands/ssl-logs.ts +80 -0
  44. package/src/commands/ssl.ts +140 -0
  45. package/src/lib/domains.ts +77 -0
  46. package/src/lib/logger.ts +79 -0
  47. package/src/lib/nginx.ts +120 -0
  48. package/src/lib/paths.ts +30 -0
  49. package/tsconfig.json +19 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/lib/paths.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAE/D,MAAM,UAAU,aAAa,CAAC,MAAc,OAAO,CAAC,GAAG,EAAE;IACvD,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC;IACjD,IAAI,MAAM,IAAI,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,EAAE,CAAC;QAC5D,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC;IACrD,IAAI,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC,EAAE,CAAC;QACpD,OAAO,QAAQ,CAAC;IAClB,CAAC;IACD,sEAAsE;IACtE,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IACrD,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAChE,IAAI,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,WAAW,CAAC,CAAC,EAAE,CAAC;QACxD,OAAO,YAAY,CAAC;IACtB,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@aaricchen1991/deploy",
3
+ "version": "0.1.0",
4
+ "description": "CLI for server init, nginx config, and SSL certificate management",
5
+ "type": "module",
6
+ "bin": {
7
+ "n2-deploy": "./dist/cli.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "cli": "node dist/cli.js",
12
+ "dev": "tsc && node dist/cli.js"
13
+ },
14
+ "dependencies": {
15
+ "commander": "^12.1.0"
16
+ },
17
+ "devDependencies": {
18
+ "@types/node": "^25.0.3",
19
+ "typescript": "~5.9.3"
20
+ },
21
+ "engines": {
22
+ "node": ">=20.0.0"
23
+ }
24
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * n2-deploy CLI: server init, nginx config, SSL management.
4
+ */
5
+
6
+ import { Command } from "commander";
7
+ import { runInit } from "./commands/init.js";
8
+ import { runNginx } from "./commands/nginx.js";
9
+ import { runSslLogs } from "./commands/ssl-logs.js";
10
+ import { runSsl } from "./commands/ssl.js";
11
+
12
+ const program = new Command();
13
+
14
+ program
15
+ .name("n2-deploy")
16
+ .description(
17
+ "Server init, nginx config generation, and SSL certificate management"
18
+ )
19
+ .version("0.1.0");
20
+
21
+ program
22
+ .command("init")
23
+ .description(
24
+ "Initialize server (idempotent): nginx, dirs, deploy.sh, placeholder certs"
25
+ )
26
+ .option("-c, --config <path>", "Path to domains.yaml", "domains.yaml")
27
+ .option(
28
+ "--scripts-dir <path>",
29
+ "Path to deploy scripts (default: cwd/scripts/deploy or N2_DEPLOY_SCRIPTS_DIR)"
30
+ )
31
+ .action(async opts => {
32
+ try {
33
+ await runInit({ config: opts.config, scriptsDir: opts.scriptsDir });
34
+ } catch (e) {
35
+ console.error(e);
36
+ process.exit(1);
37
+ }
38
+ });
39
+
40
+ program
41
+ .command("nginx")
42
+ .description(
43
+ "Generate nginx config from domains config; optionally install and reload"
44
+ )
45
+ .option("-c, --config <path>", "Path to domains.yaml", "domains.yaml")
46
+ .option(
47
+ "-o, --output <path>",
48
+ "Output path for nginx config",
49
+ "nginx/n2.conf"
50
+ )
51
+ .option("-i, --install", "Install to system nginx dir and reload")
52
+ .action(async opts => {
53
+ try {
54
+ await runNginx({
55
+ config: opts.config,
56
+ output: opts.output,
57
+ install: opts.install,
58
+ });
59
+ } catch (e) {
60
+ console.error(e);
61
+ process.exit(1);
62
+ }
63
+ });
64
+
65
+ program
66
+ .command("ssl")
67
+ .description(
68
+ "Setup/renew SSL certificates for all domains (acme.sh + Aliyun DNS)"
69
+ )
70
+ .option("-c, --config <path>", "Path to domains.yaml", "domains.yaml")
71
+ .option(
72
+ "--ali-key <key>",
73
+ "Aliyun Access Key ID (or set ALIYUN_ACCESS_KEY_ID)"
74
+ )
75
+ .option(
76
+ "--ali-secret <secret>",
77
+ "Aliyun Access Key Secret (or set ALIYUN_ACCESS_KEY_SECRET)"
78
+ )
79
+ .option("--log-file <path>", "SSL log file path", "/opt/deploy/logs/ssl.log")
80
+ .option("--scripts-dir <path>", "Path to deploy scripts")
81
+ .action(async opts => {
82
+ try {
83
+ await runSsl({
84
+ config: opts.config,
85
+ aliKey: opts.aliKey,
86
+ aliSecret: opts.aliSecret,
87
+ logFile: opts.logFile,
88
+ scriptsDir: opts.scriptsDir,
89
+ });
90
+ } catch (e) {
91
+ console.error(e);
92
+ process.exit(1);
93
+ }
94
+ });
95
+
96
+ program
97
+ .command("ssl-logs")
98
+ .description("View SSL log (create, renew, success, failure)")
99
+ .option("--log-file <path>", "SSL log file path", "/opt/deploy/logs/ssl.log")
100
+ .option("-n, --lines <n>", "Number of lines to show", "50")
101
+ .option("-f, --follow", "Follow log (tail -f)")
102
+ .option(
103
+ "--action <action>",
104
+ "Filter by action: create | renew | success | failure | run"
105
+ )
106
+ .option("--result <result>", "Filter by result: ok | fail")
107
+ .action(async opts => {
108
+ try {
109
+ await runSslLogs({
110
+ logFile: opts.logFile,
111
+ lines: parseInt(opts.lines, 10) || 50,
112
+ follow: opts.follow,
113
+ action: opts.action,
114
+ result: opts.result,
115
+ });
116
+ } catch (e) {
117
+ console.error(e);
118
+ process.exit(1);
119
+ }
120
+ });
121
+
122
+ program.parse();
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Server init (idempotent): nginx, dirs, deploy.sh, placeholder certs.
3
+ * Uses server-setup.sh from scripts dir when available.
4
+ */
5
+
6
+ import { spawn } from "node:child_process";
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+ import { getAllDomains, parseDomainsYaml } from "../lib/domains.js";
10
+ import { error, info, warn } from "../lib/logger.js";
11
+ import { generateNginxConf } from "../lib/nginx.js";
12
+ import { getScriptsDir } from "../lib/paths.js";
13
+
14
+ const DEFAULT_SSL_LOG = "/opt/deploy/logs/ssl.log";
15
+
16
+ export interface InitOptions {
17
+ config: string;
18
+ scriptsDir?: string;
19
+ }
20
+
21
+ export async function runInit(opts: InitOptions): Promise<void> {
22
+ const configPath = path.resolve(process.cwd(), opts.config);
23
+ if (!fs.existsSync(configPath)) {
24
+ error(`Domains config not found: ${configPath}`);
25
+ process.exit(1);
26
+ }
27
+
28
+ const yaml = fs.readFileSync(configPath, "utf8");
29
+ const config = parseDomainsYaml(yaml);
30
+ const domains = getAllDomains(config);
31
+ const nginxConf = generateNginxConf(config);
32
+
33
+ const scriptsDir =
34
+ opts.scriptsDir ||
35
+ process.env.N2_DEPLOY_SCRIPTS_DIR ||
36
+ getScriptsDir(process.cwd());
37
+
38
+ if (!scriptsDir || !fs.existsSync(path.join(scriptsDir, "server-setup.sh"))) {
39
+ error(
40
+ "Deploy scripts directory not found. Set N2_DEPLOY_SCRIPTS_DIR or run from repo root (scripts/deploy must exist)."
41
+ );
42
+ process.exit(1);
43
+ }
44
+
45
+ info("Using scripts directory: " + scriptsDir);
46
+
47
+ const tmpDir = path.join(
48
+ process.env.TMPDIR || "/tmp",
49
+ `n2-deploy-init-${Date.now()}`
50
+ );
51
+ fs.mkdirSync(path.join(tmpDir, "nginx"), { recursive: true });
52
+ fs.mkdirSync(path.join(tmpDir, "ssl"), { recursive: true });
53
+
54
+ try {
55
+ fs.writeFileSync(path.join(tmpDir, "nginx", "n2.conf"), nginxConf);
56
+
57
+ const deploySh = path.join(scriptsDir, "deploy.sh");
58
+ if (fs.existsSync(deploySh)) {
59
+ fs.copyFileSync(deploySh, path.join(tmpDir, "deploy.sh"));
60
+ fs.chmodSync(path.join(tmpDir, "deploy.sh"), 0o755);
61
+ } else {
62
+ error("deploy.sh not found in scripts directory");
63
+ process.exit(1);
64
+ }
65
+
66
+ const setupSh = path.join(scriptsDir, "server-setup.sh");
67
+ fs.copyFileSync(setupSh, path.join(tmpDir, "server-setup.sh"));
68
+ fs.chmodSync(path.join(tmpDir, "server-setup.sh"), 0o755);
69
+
70
+ const sslDir = path.join(scriptsDir, "ssl");
71
+ if (fs.existsSync(sslDir)) {
72
+ const sslDest = path.join(tmpDir, "ssl");
73
+ for (const name of fs.readdirSync(sslDir)) {
74
+ const src = path.join(sslDir, name);
75
+ if (fs.statSync(src).isFile()) {
76
+ fs.copyFileSync(src, path.join(sslDest, name));
77
+ fs.chmodSync(path.join(sslDest, name), 0o755);
78
+ }
79
+ }
80
+ }
81
+ fs.writeFileSync(
82
+ path.join(tmpDir, "ssl", "domains.txt"),
83
+ domains.join("\n") + "\n"
84
+ );
85
+
86
+ const libDir = path.join(scriptsDir, "lib");
87
+ if (fs.existsSync(libDir)) {
88
+ const libDest = path.join(tmpDir, "lib");
89
+ fs.mkdirSync(libDest, { recursive: true });
90
+ for (const name of fs.readdirSync(libDir)) {
91
+ fs.copyFileSync(path.join(libDir, name), path.join(libDest, name));
92
+ }
93
+ }
94
+
95
+ info("Running server-setup.sh (may require sudo)...");
96
+ await new Promise<void>((resolve, reject) => {
97
+ const child = spawn("bash", [path.join(tmpDir, "server-setup.sh")], {
98
+ cwd: tmpDir,
99
+ stdio: "inherit",
100
+ env: { ...process.env, SCRIPT_DIR: tmpDir },
101
+ });
102
+ child.on("close", code => {
103
+ if (code === 0) resolve();
104
+ else reject(new Error(`server-setup.sh exited with ${code}`));
105
+ });
106
+ child.on("error", reject);
107
+ });
108
+
109
+ const logDir = path.dirname(DEFAULT_SSL_LOG);
110
+ if (logDir && !fs.existsSync(logDir)) {
111
+ try {
112
+ fs.mkdirSync(logDir, { recursive: true });
113
+ info("Created log directory: " + logDir);
114
+ } catch {
115
+ warn("Could not create log directory: " + logDir);
116
+ }
117
+ }
118
+
119
+ info("Server initialization completed.");
120
+ } finally {
121
+ try {
122
+ fs.rmSync(tmpDir, { recursive: true, force: true });
123
+ } catch {
124
+ // ignore
125
+ }
126
+ }
127
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Generate nginx config from domains config; optionally install and reload.
3
+ */
4
+
5
+ import { spawn } from "node:child_process";
6
+ import fs from "node:fs";
7
+ import path from "node:path";
8
+ import { parseDomainsYaml } from "../lib/domains.js";
9
+ import { error, info, warn } from "../lib/logger.js";
10
+ import { generateNginxConf } from "../lib/nginx.js";
11
+
12
+ export interface NginxOptions {
13
+ config: string;
14
+ output: string;
15
+ install?: boolean;
16
+ }
17
+
18
+ const NGINX_CONF_NAME = "n2.conf";
19
+
20
+ export async function runNginx(opts: NginxOptions): Promise<void> {
21
+ const configPath = path.resolve(process.cwd(), opts.config);
22
+ if (!fs.existsSync(configPath)) {
23
+ error(`Config not found: ${configPath}`);
24
+ process.exit(1);
25
+ }
26
+
27
+ const yaml = fs.readFileSync(configPath, "utf8");
28
+ const config = parseDomainsYaml(yaml);
29
+ const nginxConf = generateNginxConf(config);
30
+
31
+ const outPath = path.resolve(process.cwd(), opts.output);
32
+ const outDir = path.dirname(outPath);
33
+ fs.mkdirSync(outDir, { recursive: true });
34
+ fs.writeFileSync(outPath, nginxConf);
35
+ info(`Wrote ${outPath}`);
36
+
37
+ if (opts.install) {
38
+ const destDir = detectNginxConfDir();
39
+ const destPath = path.join(destDir, NGINX_CONF_NAME);
40
+ try {
41
+ fs.copyFileSync(outPath, destPath);
42
+ info(`Installed to ${destPath}`);
43
+ } catch (e) {
44
+ error(`Install failed (try sudo): ${e}`);
45
+ process.exit(1);
46
+ }
47
+ await nginxTestAndReload();
48
+ }
49
+ }
50
+
51
+ function detectNginxConfDir(): string {
52
+ if (fs.existsSync("/etc/nginx/conf.d")) return "/etc/nginx/conf.d";
53
+ if (fs.existsSync("/etc/nginx/sites-available"))
54
+ return "/etc/nginx/sites-available";
55
+ return "/etc/nginx/conf.d";
56
+ }
57
+
58
+ function nginxTestAndReload(): Promise<void> {
59
+ return new Promise((resolve, reject) => {
60
+ const t = spawn("nginx", ["-t"], { stdio: "inherit" });
61
+ t.on("close", code => {
62
+ if (code !== 0) {
63
+ reject(new Error("nginx -t failed"));
64
+ return;
65
+ }
66
+ const r = spawn("systemctl", ["reload", "nginx"], { stdio: "inherit" });
67
+ r.on("close", c => {
68
+ if (c === 0) {
69
+ info("Nginx reloaded.");
70
+ resolve();
71
+ } else {
72
+ warn(
73
+ "systemctl reload nginx failed; try: sudo systemctl reload nginx"
74
+ );
75
+ resolve();
76
+ }
77
+ });
78
+ r.on("error", () => resolve());
79
+ });
80
+ t.on("error", () => reject(new Error("nginx not found")));
81
+ });
82
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * View SSL log file: tail, lines, follow, optional filter by action/result.
3
+ */
4
+
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ import { getLogFilePath } from "../lib/logger.js";
8
+
9
+ const DEFAULT_SSL_LOG = "/opt/deploy/logs/ssl.log";
10
+
11
+ export interface SslLogsOptions {
12
+ /** Log file path (default: /opt/deploy/logs/ssl.log) */
13
+ logFile?: string;
14
+ /** Show last N lines (default: 50) */
15
+ lines?: number;
16
+ /** Follow (tail -f) */
17
+ follow?: boolean;
18
+ /** Filter: create | renew | success | failure | run */
19
+ action?: string;
20
+ /** Filter: ok | fail */
21
+ result?: string;
22
+ }
23
+
24
+ export async function runSslLogs(opts: SslLogsOptions): Promise<void> {
25
+ const logPath = opts.logFile ?? getLogFilePath() ?? DEFAULT_SSL_LOG;
26
+ const resolved = path.resolve(process.cwd(), logPath);
27
+
28
+ if (!fs.existsSync(resolved)) {
29
+ console.error("Log file not found:", resolved);
30
+ process.exit(1);
31
+ }
32
+
33
+ const lines = opts.lines ?? 50;
34
+ let content: string;
35
+ const stat = fs.statSync(resolved);
36
+ const fd = fs.openSync(resolved, "r");
37
+ try {
38
+ const buffer = Buffer.alloc(Math.min(stat.size, 256 * 1024));
39
+ const read = fs.readSync(
40
+ fd,
41
+ buffer,
42
+ 0,
43
+ buffer.length,
44
+ Math.max(0, stat.size - buffer.length)
45
+ );
46
+ content = buffer.subarray(0, read).toString("utf8");
47
+ } finally {
48
+ fs.closeSync(fd);
49
+ }
50
+
51
+ const allLines = content.split(/\n/).filter(l => l.length > 0);
52
+ const lastLines = allLines.slice(-lines);
53
+
54
+ const filter = (line: string): boolean => {
55
+ if (opts.action || opts.result) {
56
+ const parts = line.split("\t");
57
+ if (parts.length >= 5) {
58
+ const [, , action, , result] = parts;
59
+ if (opts.action && action !== opts.action) return false;
60
+ if (opts.result && result !== opts.result) return false;
61
+ }
62
+ }
63
+ return true;
64
+ };
65
+
66
+ const filtered =
67
+ opts.action || opts.result ? lastLines.filter(filter) : lastLines;
68
+
69
+ for (const line of filtered) {
70
+ console.log(line);
71
+ }
72
+
73
+ if (opts.follow) {
74
+ const { spawn } = await import("node:child_process");
75
+ const tail = spawn("tail", ["-n", String(lines), "-f", resolved], {
76
+ stdio: "inherit",
77
+ });
78
+ tail.on("close", code => process.exit(code ?? 0));
79
+ }
80
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * SSL certificate setup/renew for all domains from config.
3
+ * Runs check-and-setup-ssl.sh; all output and structured events go to SSL log file.
4
+ */
5
+
6
+ import { spawn } from "node:child_process";
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+ import { getAllDomains, parseDomainsYaml } from "../lib/domains.js";
10
+ import {
11
+ appendRaw,
12
+ appendSslLog,
13
+ error,
14
+ info,
15
+ setLogFile,
16
+ } from "../lib/logger.js";
17
+ import { getScriptsDir } from "../lib/paths.js";
18
+
19
+ const DEFAULT_SSL_LOG = "/opt/deploy/logs/ssl.log";
20
+
21
+ export interface SslOptions {
22
+ config: string;
23
+ /** Ali key (or ALIYUN_ACCESS_KEY_ID / Ali_Key env) */
24
+ aliKey?: string;
25
+ /** Ali secret (or ALIYUN_ACCESS_KEY_SECRET / Ali_Secret env) */
26
+ aliSecret?: string;
27
+ logFile?: string;
28
+ /** Path to deploy scripts (default: N2_DEPLOY_SCRIPTS_DIR or cwd/scripts/deploy or assets) */
29
+ scriptsDir?: string;
30
+ }
31
+
32
+ export async function runSsl(opts: SslOptions): Promise<void> {
33
+ const configPath = path.resolve(process.cwd(), opts.config);
34
+ if (!fs.existsSync(configPath)) {
35
+ error(`Config not found: ${configPath}`);
36
+ process.exit(1);
37
+ }
38
+
39
+ const aliKey =
40
+ opts.aliKey ?? process.env.ALIYUN_ACCESS_KEY_ID ?? process.env.Ali_Key;
41
+ const aliSecret =
42
+ opts.aliSecret ??
43
+ process.env.ALIYUN_ACCESS_KEY_SECRET ??
44
+ process.env.Ali_Secret;
45
+
46
+ if (!aliKey || !aliSecret) {
47
+ error(
48
+ "Aliyun API credentials required. Set --ali-key/--ali-secret or ALIYUN_ACCESS_KEY_ID/ALIYUN_ACCESS_KEY_SECRET (or Ali_Key/Ali_Secret)."
49
+ );
50
+ process.exit(1);
51
+ }
52
+
53
+ const yaml = fs.readFileSync(configPath, "utf8");
54
+ const config = parseDomainsYaml(yaml);
55
+ const domains = getAllDomains(config);
56
+ const domainsTxt = domains.join("\n") + "\n";
57
+
58
+ const logPath = opts.logFile ?? DEFAULT_SSL_LOG;
59
+ setLogFile(logPath);
60
+ const logDir = path.dirname(logPath);
61
+ fs.mkdirSync(logDir, { recursive: true });
62
+
63
+ info("SSL log file: " + logPath);
64
+ info("Domains: " + domains.join(", "));
65
+
66
+ const scriptsDir =
67
+ opts.scriptsDir ||
68
+ process.env.N2_DEPLOY_SCRIPTS_DIR ||
69
+ getScriptsDir(process.cwd());
70
+
71
+ let scriptPath: string;
72
+ let domainsFilePath: string;
73
+
74
+ if (fs.existsSync("/opt/ssl/check-and-setup-ssl.sh")) {
75
+ scriptPath = "/opt/ssl/check-and-setup-ssl.sh";
76
+ domainsFilePath = "/opt/ssl/domains.txt";
77
+ fs.writeFileSync(domainsFilePath, domainsTxt);
78
+ info("Updated /opt/ssl/domains.txt");
79
+ } else if (
80
+ scriptsDir &&
81
+ fs.existsSync(path.join(scriptsDir, "ssl", "check-and-setup-ssl.sh"))
82
+ ) {
83
+ scriptPath = path.join(scriptsDir, "ssl", "check-and-setup-ssl.sh");
84
+ const tmpDomains = path.join(logDir, "domains.txt");
85
+ fs.writeFileSync(tmpDomains, domainsTxt);
86
+ domainsFilePath = tmpDomains;
87
+ } else {
88
+ error(
89
+ "SSL scripts not found. Run 'n2-deploy init' first or set N2_DEPLOY_SCRIPTS_DIR."
90
+ );
91
+ process.exit(1);
92
+ }
93
+
94
+ appendSslLog("run", "", "ok", "start");
95
+
96
+ await new Promise<void>((resolve, reject) => {
97
+ const child = spawn(
98
+ "bash",
99
+ [scriptPath, "--ali-key", aliKey, "--ali-secret", aliSecret],
100
+ {
101
+ env: {
102
+ ...process.env,
103
+ Ali_Key: aliKey,
104
+ Ali_Secret: aliSecret,
105
+ DOMAINS_FILE: domainsFilePath,
106
+ },
107
+ stdio: ["inherit", "pipe", "pipe"],
108
+ }
109
+ );
110
+
111
+ const appendOut = (data: Buffer | string) => {
112
+ const s = data.toString();
113
+ process.stdout.write(s);
114
+ appendRaw(s.trimEnd());
115
+ };
116
+
117
+ child.stdout?.on("data", appendOut);
118
+ child.stderr?.on("data", data => {
119
+ const s = data.toString();
120
+ process.stderr.write(s);
121
+ appendRaw(s.trimEnd());
122
+ });
123
+
124
+ child.on("close", (code, signal) => {
125
+ if (code === 0) {
126
+ appendSslLog("run", "", "ok", "done");
127
+ info("SSL certificate setup completed.");
128
+ resolve();
129
+ } else {
130
+ appendSslLog("run", "", "fail", `exit ${code} ${signal ?? ""}`);
131
+ error(`SSL script exited with code ${code}`);
132
+ reject(new Error(`SSL script exited with ${code}`));
133
+ }
134
+ });
135
+ child.on("error", err => {
136
+ appendSslLog("run", "", "fail", err.message);
137
+ reject(err);
138
+ });
139
+ });
140
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Parse domains.yaml format (same as scripts/deploy/domains.yaml).
3
+ */
4
+
5
+ export interface DomainsConfig {
6
+ api: { backend_port: number; domains: string[] };
7
+ admin: { domains: string[] };
8
+ tenant: { domains: string[] };
9
+ client: { domains: string[] };
10
+ }
11
+
12
+ const DEFAULT_CONFIG: DomainsConfig = {
13
+ api: { backend_port: 3000, domains: [] },
14
+ admin: { domains: [] },
15
+ tenant: { domains: [] },
16
+ client: { domains: [] },
17
+ };
18
+
19
+ export function parseDomainsYaml(content: string): DomainsConfig {
20
+ const result: DomainsConfig = JSON.parse(JSON.stringify(DEFAULT_CONFIG));
21
+ const lines = content.split(/\r?\n/);
22
+ let current: keyof DomainsConfig | null = null;
23
+ let inDomains = false;
24
+
25
+ for (const line of lines) {
26
+ const trimmed = line.trim();
27
+ if (trimmed.startsWith("#") || !trimmed) continue;
28
+
29
+ if (/^api:\s*$/.test(trimmed)) {
30
+ current = "api";
31
+ inDomains = false;
32
+ continue;
33
+ }
34
+ if (/^admin:\s*$/.test(trimmed)) {
35
+ current = "admin";
36
+ inDomains = false;
37
+ continue;
38
+ }
39
+ if (/^tenant:\s*$/.test(trimmed)) {
40
+ current = "tenant";
41
+ inDomains = false;
42
+ continue;
43
+ }
44
+ if (/^client:\s*$/.test(trimmed)) {
45
+ current = "client";
46
+ inDomains = false;
47
+ continue;
48
+ }
49
+
50
+ const backendPortMatch = trimmed.match(/^backend_port:\s*(\d+)\s*$/);
51
+ if (current === "api" && backendPortMatch) {
52
+ result.api.backend_port = parseInt(backendPortMatch[1], 10);
53
+ continue;
54
+ }
55
+ if (current && /^domains:\s*$/.test(trimmed)) {
56
+ inDomains = true;
57
+ continue;
58
+ }
59
+ const domainMatch = trimmed.match(/^-\s*(.+)$/);
60
+ if (current && inDomains && domainMatch) {
61
+ const domain = domainMatch[1].trim();
62
+ if (domain) result[current].domains.push(domain);
63
+ }
64
+ }
65
+
66
+ return result;
67
+ }
68
+
69
+ /** Return all domains (api + admin + tenant + client) in order. */
70
+ export function getAllDomains(config: DomainsConfig): string[] {
71
+ const list: string[] = [];
72
+ for (const d of config.api.domains) list.push(d);
73
+ for (const d of config.admin.domains) list.push(d);
74
+ for (const d of config.tenant.domains) list.push(d);
75
+ for (const d of config.client.domains) list.push(d);
76
+ return list;
77
+ }