@aaricchen1991/n2-cli 1.0.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 (92) hide show
  1. package/README.md +92 -0
  2. package/assets/deploy/deploy.sh +172 -0
  3. package/assets/deploy/domains.yaml +18 -0
  4. package/assets/deploy/lib/common.sh +62 -0
  5. package/assets/deploy/nginx/n2.conf +162 -0
  6. package/assets/deploy/server-setup.sh +285 -0
  7. package/assets/deploy/ssl/README.md +320 -0
  8. package/assets/deploy/ssl/check-and-setup-ssl.sh +222 -0
  9. package/assets/deploy/ssl/domains.txt +3 -0
  10. package/assets/deploy/ssl/renew-ssl.sh +236 -0
  11. package/assets/deploy/ssl/setup-ssl.sh +474 -0
  12. package/dist/cli.d.ts +7 -0
  13. package/dist/cli.d.ts.map +1 -0
  14. package/dist/cli.js +186 -0
  15. package/dist/cli.js.map +1 -0
  16. package/dist/commands/config.d.ts +29 -0
  17. package/dist/commands/config.d.ts.map +1 -0
  18. package/dist/commands/config.js +134 -0
  19. package/dist/commands/config.js.map +1 -0
  20. package/dist/commands/config.test.d.ts +2 -0
  21. package/dist/commands/config.test.d.ts.map +1 -0
  22. package/dist/commands/config.test.js +215 -0
  23. package/dist/commands/config.test.js.map +1 -0
  24. package/dist/commands/init.d.ts +10 -0
  25. package/dist/commands/init.d.ts.map +1 -0
  26. package/dist/commands/init.js +106 -0
  27. package/dist/commands/init.js.map +1 -0
  28. package/dist/commands/init.test.d.ts +2 -0
  29. package/dist/commands/init.test.d.ts.map +1 -0
  30. package/dist/commands/init.test.js +70 -0
  31. package/dist/commands/init.test.js.map +1 -0
  32. package/dist/commands/nginx.d.ts +10 -0
  33. package/dist/commands/nginx.d.ts.map +1 -0
  34. package/dist/commands/nginx.js +72 -0
  35. package/dist/commands/nginx.js.map +1 -0
  36. package/dist/commands/nginx.test.d.ts +2 -0
  37. package/dist/commands/nginx.test.d.ts.map +1 -0
  38. package/dist/commands/nginx.test.js +75 -0
  39. package/dist/commands/nginx.test.js.map +1 -0
  40. package/dist/commands/ssl-logs.d.ts +17 -0
  41. package/dist/commands/ssl-logs.d.ts.map +1 -0
  42. package/dist/commands/ssl-logs.js +55 -0
  43. package/dist/commands/ssl-logs.js.map +1 -0
  44. package/dist/commands/ssl-logs.test.d.ts +2 -0
  45. package/dist/commands/ssl-logs.test.d.ts.map +1 -0
  46. package/dist/commands/ssl-logs.test.js +54 -0
  47. package/dist/commands/ssl-logs.test.js.map +1 -0
  48. package/dist/commands/ssl.d.ts +16 -0
  49. package/dist/commands/ssl.d.ts.map +1 -0
  50. package/dist/commands/ssl.js +105 -0
  51. package/dist/commands/ssl.js.map +1 -0
  52. package/dist/commands/ssl.test.d.ts +2 -0
  53. package/dist/commands/ssl.test.d.ts.map +1 -0
  54. package/dist/commands/ssl.test.js +95 -0
  55. package/dist/commands/ssl.test.js.map +1 -0
  56. package/dist/lib/config-store.d.ts +14 -0
  57. package/dist/lib/config-store.d.ts.map +1 -0
  58. package/dist/lib/config-store.js +111 -0
  59. package/dist/lib/config-store.js.map +1 -0
  60. package/dist/lib/config-store.test.d.ts +2 -0
  61. package/dist/lib/config-store.test.d.ts.map +1 -0
  62. package/dist/lib/config-store.test.js +173 -0
  63. package/dist/lib/config-store.test.js.map +1 -0
  64. package/dist/lib/domains.d.ts +37 -0
  65. package/dist/lib/domains.d.ts.map +1 -0
  66. package/dist/lib/domains.js +134 -0
  67. package/dist/lib/domains.js.map +1 -0
  68. package/dist/lib/domains.test.d.ts +2 -0
  69. package/dist/lib/domains.test.d.ts.map +1 -0
  70. package/dist/lib/domains.test.js +141 -0
  71. package/dist/lib/domains.test.js.map +1 -0
  72. package/dist/lib/logger.d.ts +19 -0
  73. package/dist/lib/logger.d.ts.map +1 -0
  74. package/dist/lib/logger.js +58 -0
  75. package/dist/lib/logger.js.map +1 -0
  76. package/dist/lib/nginx.d.ts +7 -0
  77. package/dist/lib/nginx.d.ts.map +1 -0
  78. package/dist/lib/nginx.js +86 -0
  79. package/dist/lib/nginx.js.map +1 -0
  80. package/dist/lib/nginx.test.d.ts +2 -0
  81. package/dist/lib/nginx.test.d.ts.map +1 -0
  82. package/dist/lib/nginx.test.js +46 -0
  83. package/dist/lib/nginx.test.js.map +1 -0
  84. package/dist/lib/paths.d.ts +13 -0
  85. package/dist/lib/paths.d.ts.map +1 -0
  86. package/dist/lib/paths.js +36 -0
  87. package/dist/lib/paths.js.map +1 -0
  88. package/dist/lib/paths.test.d.ts +2 -0
  89. package/dist/lib/paths.test.d.ts.map +1 -0
  90. package/dist/lib/paths.test.js +52 -0
  91. package/dist/lib/paths.test.js.map +1 -0
  92. package/package.json +34 -0
@@ -0,0 +1,474 @@
1
+ #!/bin/bash
2
+ # SSL 证书申请和安装脚本
3
+ # 使用 acme.sh 和阿里云 DNS API 自动申请 Let's Encrypt 证书
4
+ # 使用方法: setup-ssl.sh --domains <域名> [--ali-key ... --ali-secret ...]
5
+
6
+ set -e
7
+
8
+ # 颜色输出
9
+ RED='\033[0;31m'
10
+ GREEN='\033[0;32m'
11
+ YELLOW='\033[1;33m'
12
+ NC='\033[0m' # No Color
13
+
14
+ log() {
15
+ echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1"
16
+ }
17
+
18
+ error() {
19
+ echo -e "${RED}[ERROR]${NC} $1" >&2
20
+ }
21
+
22
+ warning() {
23
+ echo -e "${YELLOW}[WARNING]${NC} $1"
24
+ }
25
+
26
+ # 检查是否以 root 权限运行
27
+ if [ "$EUID" -ne 0 ]; then
28
+ error "请使用 sudo 运行此脚本"
29
+ exit 1
30
+ fi
31
+
32
+ # 确保在有效目录下执行,避免 getcwd 报错
33
+ cd /
34
+
35
+ # 要申请证书的域名(通过 --domains 传入,如 --domains n2-admin.cdqxtech.com)
36
+ TARGET_DOMAIN=""
37
+
38
+ # 阿里云 API 凭证
39
+ ALI_KEY=""
40
+ ALI_SECRET=""
41
+
42
+ # Email 地址(用于 ZeroSSL 账户注册,可选)
43
+ ACME_EMAIL=""
44
+
45
+ # 调试模式
46
+ DEBUG_MODE=false
47
+
48
+ # 解析命令行参数
49
+ while [[ $# -gt 0 ]]; do
50
+ case $1 in
51
+ --domains)
52
+ if [ -n "$2" ]; then
53
+ IFS=',' read -ra DOMAIN_ARRAY <<< "$2"
54
+ if [ ${#DOMAIN_ARRAY[@]} -ge 1 ]; then
55
+ TARGET_DOMAIN="${DOMAIN_ARRAY[0]}"
56
+ fi
57
+ shift 2
58
+ else
59
+ shift
60
+ fi
61
+ ;;
62
+ --ali-key)
63
+ if [ -n "$2" ]; then
64
+ ALI_KEY="$2"
65
+ shift 2
66
+ else
67
+ shift
68
+ fi
69
+ ;;
70
+ --ali-secret)
71
+ if [ -n "$2" ]; then
72
+ ALI_SECRET="$2"
73
+ shift 2
74
+ else
75
+ shift
76
+ fi
77
+ ;;
78
+ --email)
79
+ if [ -n "$2" ]; then
80
+ ACME_EMAIL="$2"
81
+ shift 2
82
+ else
83
+ shift
84
+ fi
85
+ ;;
86
+ --debug)
87
+ DEBUG_MODE=true
88
+ set -x # 启用 bash 调试模式,显示所有执行的命令
89
+ shift
90
+ ;;
91
+ *)
92
+ shift
93
+ ;;
94
+ esac
95
+ done
96
+
97
+ log "开始配置 SSL 证书..."
98
+ if [ -z "$TARGET_DOMAIN" ]; then
99
+ error "请通过 --domains 指定要申请证书的域名,例如: --domains n2-admin.cdqxtech.com"
100
+ exit 1
101
+ fi
102
+ log "申请域名: $TARGET_DOMAIN"
103
+
104
+ # 检查阿里云 API 凭证(优先使用参数,其次使用环境变量)
105
+ if [ -z "$ALI_KEY" ]; then
106
+ ALI_KEY="$Ali_Key"
107
+ fi
108
+ if [ -z "$ALI_SECRET" ]; then
109
+ ALI_SECRET="$Ali_Secret"
110
+ fi
111
+
112
+ if [ -z "$ALI_KEY" ] || [ -z "$ALI_SECRET" ]; then
113
+ error "未设置阿里云 API 凭证"
114
+ error "请通过参数传递:"
115
+ error " --ali-key \"your_access_key_id\" --ali-secret \"your_access_key_secret\""
116
+ error "或者设置环境变量:"
117
+ error " export Ali_Key=\"your_access_key_id\""
118
+ error " export Ali_Secret=\"your_access_key_secret\""
119
+ exit 1
120
+ fi
121
+
122
+ # 设置环境变量供 acme.sh 使用
123
+ export Ali_Key="$ALI_KEY"
124
+ export Ali_Secret="$ALI_SECRET"
125
+
126
+ # 验证阿里云 API 凭证的函数
127
+ verify_ali_api_credentials() {
128
+ log "验证阿里云 API 凭证..."
129
+
130
+ # 检查凭证是否为空
131
+ if [ -z "$ALI_KEY" ] || [ -z "$ALI_SECRET" ]; then
132
+ error "API 凭证为空"
133
+ return 1
134
+ fi
135
+
136
+ # 检查凭证格式(AccessKey ID 通常以 LTAI 开头,长度约 16-32 字符)
137
+ if [ ${#ALI_KEY} -lt 10 ] || [ ${#ALI_KEY} -gt 64 ]; then
138
+ warning "AccessKey ID 长度异常,请检查是否正确"
139
+ fi
140
+
141
+ if [ ${#ALI_SECRET} -lt 20 ] || [ ${#ALI_SECRET} -gt 64 ]; then
142
+ warning "AccessKey Secret 长度异常,请检查是否正确"
143
+ fi
144
+
145
+ log "API 凭证格式检查通过(实际有效性将在证书申请时验证)"
146
+ return 0
147
+ }
148
+
149
+ # 检查域名是否在阿里云 DNS 管理的函数
150
+ check_domain_in_aliyun() {
151
+ local domain=$1
152
+ log "检查域名 $domain 是否在阿里云 DNS 管理..."
153
+
154
+ # 提取主域名(例如 tools.qxtool.vip -> qxtool.vip)
155
+ local main_domain=$(echo "$domain" | sed -E 's/^[^.]*\.(.+)$/\1/')
156
+ if [ "$main_domain" = "$domain" ]; then
157
+ main_domain="$domain"
158
+ fi
159
+
160
+ log "主域名: $main_domain"
161
+ log "注意:如果域名不在阿里云 DNS 管理,证书申请将失败"
162
+ log "请确保域名已在阿里云 DNS 控制台添加"
163
+ }
164
+
165
+ # 检查并处理 Let's Encrypt 速率限制错误
166
+ check_rate_limit_error() {
167
+ local error_output="$1"
168
+ local domain="$2"
169
+
170
+ # 检查是否包含速率限制错误
171
+ if echo "$error_output" | grep -qi "rateLimited\|429\|too many failed authorizations"; then
172
+ error ""
173
+ error "⚠️ 检测到 Let's Encrypt 速率限制错误"
174
+ error ""
175
+ error "原因:在过去的1小时内,域名 $domain 有太多失败的授权尝试(最多5次)"
176
+ error ""
177
+
178
+ # 尝试提取重试时间
179
+ local retry_after=$(echo "$error_output" | grep -i "retry after" | sed -n 's/.*retry after \([^:]*\):.*/\1/p' | head -1)
180
+ if [ -n "$retry_after" ]; then
181
+ error "下次可重试时间:"
182
+ error " UTC: $retry_after UTC"
183
+
184
+ # 尝试转换为本地时间
185
+ local local_time=""
186
+ local wait_info=""
187
+
188
+ # 方法1: 使用 GNU date (Linux)
189
+ if local_time=$(TZ=Asia/Shanghai date -d "$retry_after UTC" +"%Y-%m-%d %H:%M:%S %Z" 2>/dev/null); then
190
+ error " 本地 (CST): $local_time"
191
+
192
+ # 计算剩余等待时间
193
+ local current_timestamp=$(date +%s 2>/dev/null)
194
+ local retry_timestamp=$(date -d "$retry_after UTC" +%s 2>/dev/null)
195
+ if [ -n "$current_timestamp" ] && [ -n "$retry_timestamp" ] && [ $retry_timestamp -gt $current_timestamp ]; then
196
+ local wait_seconds=$((retry_timestamp - current_timestamp))
197
+ local wait_hours=$((wait_seconds / 3600))
198
+ local wait_mins=$(((wait_seconds % 3600) / 60))
199
+ wait_info="${wait_hours}小时${wait_mins}分钟"
200
+ fi
201
+ # 方法2: 使用 BSD date (macOS) - 先转换为时间戳,再加8小时
202
+ elif retry_timestamp=$(date -j -f "%Y-%m-%d %H:%M:%S" "$retry_after" +%s 2>/dev/null); then
203
+ # 加8小时(28800秒)
204
+ local local_timestamp=$((retry_timestamp + 28800))
205
+ if local_time=$(TZ=Asia/Shanghai date -j -f "%s" "$local_timestamp" +"%Y-%m-%d %H:%M:%S %Z" 2>/dev/null); then
206
+ error " 本地 (CST): $local_time"
207
+
208
+ # 计算剩余等待时间
209
+ local current_timestamp=$(date +%s 2>/dev/null)
210
+ if [ -n "$current_timestamp" ] && [ $retry_timestamp -gt $current_timestamp ]; then
211
+ local wait_seconds=$((retry_timestamp - current_timestamp))
212
+ local wait_hours=$((wait_seconds / 3600))
213
+ local wait_mins=$(((wait_seconds % 3600) / 60))
214
+ wait_info="${wait_hours}小时${wait_mins}分钟"
215
+ fi
216
+ fi
217
+ # 方法3: 手动计算(UTC+8)
218
+ else
219
+ local retry_date=$(echo "$retry_after" | awk '{print $1}')
220
+ local retry_time=$(echo "$retry_after" | awk '{print $2}')
221
+ if [ -n "$retry_date" ] && [ -n "$retry_time" ]; then
222
+ local utc_hour=$(echo "$retry_time" | cut -d: -f1 | sed 's/^0//')
223
+ [ -z "$utc_hour" ] && utc_hour=0
224
+ local utc_min=$(echo "$retry_time" | cut -d: -f2)
225
+ local utc_sec=$(echo "$retry_time" | cut -d: -f3)
226
+
227
+ local local_hour=$((utc_hour + 8))
228
+ local local_date="$retry_date"
229
+ if [ $local_hour -ge 24 ]; then
230
+ local_hour=$((local_hour - 24))
231
+ # 简单跨天处理(假设月份不超过31天)
232
+ local year=$(echo "$retry_date" | cut -d- -f1)
233
+ local month=$(echo "$retry_date" | cut -d- -f2)
234
+ local day=$(echo "$retry_date" | cut -d- -f3 | sed 's/^0//')
235
+ [ -z "$day" ] && day=0
236
+ day=$((day + 1))
237
+ local_date=$(printf "%04d-%02d-%02d" $year $month $day)
238
+ fi
239
+
240
+ local local_time_str=$(printf "%02d:%s:%s" $local_hour "$utc_min" "$utc_sec")
241
+ error " 本地 (CST): $local_date $local_time_str CST"
242
+ fi
243
+ fi
244
+
245
+ if [ -n "$wait_info" ]; then
246
+ error " 还需等待: $wait_info"
247
+ fi
248
+ error ""
249
+ fi
250
+
251
+ error "解决方案:"
252
+ error "1. 等待速率限制解除(通常需要等待1小时)"
253
+ error "2. 检查 DNS 配置是否正确,避免重复失败"
254
+ error "3. 如果急需证书,可以考虑:"
255
+ error " - 使用 Let's Encrypt Staging 环境进行测试(不会触发速率限制)"
256
+ error " - 使用其他 CA(如 ZeroSSL)"
257
+ error ""
258
+ error "使用 Staging 环境测试(不会触发速率限制):"
259
+ error " $0 --domains \"$domain\" --ali-key \"\$Ali_Key\" --ali-secret \"\$Ali_Secret\" --staging"
260
+ error ""
261
+ error "查看详细错误信息:"
262
+ error " tail -100 ~/.acme.sh/acme.sh.log"
263
+ error ""
264
+ return 1
265
+ fi
266
+
267
+ return 0
268
+ }
269
+
270
+ # 创建 SSL 证书目录
271
+ log "创建 SSL 证书目录..."
272
+ mkdir -p /etc/nginx/ssl
273
+ chmod 755 /etc/nginx/ssl
274
+
275
+ # 检查并安装 acme.sh
276
+ log "检查 acme.sh 安装..."
277
+ ACME_SH_HOME="$HOME/.acme.sh"
278
+ if [ ! -f "$ACME_SH_HOME/acme.sh" ]; then
279
+ log "安装 acme.sh..."
280
+ curl https://get.acme.sh | sh
281
+ if [ $? -ne 0 ]; then
282
+ error "acme.sh 安装失败"
283
+ exit 1
284
+ fi
285
+ # 重新加载环境变量
286
+ source "$HOME/.bashrc" 2>/dev/null || true
287
+ source "$HOME/.profile" 2>/dev/null || true
288
+ else
289
+ log "acme.sh 已安装"
290
+ fi
291
+
292
+ # 确保 acme.sh 在 PATH 中
293
+ export PATH="$HOME/.acme.sh:$PATH"
294
+
295
+ # 检查 acme.sh 是否可用
296
+ if ! command -v acme.sh >/dev/null 2>&1; then
297
+ # 尝试直接使用完整路径
298
+ if [ -f "$ACME_SH_HOME/acme.sh" ]; then
299
+ alias acme.sh="$ACME_SH_HOME/acme.sh"
300
+ ACME_CMD="$ACME_SH_HOME/acme.sh"
301
+ else
302
+ error "无法找到 acme.sh,请检查安装"
303
+ exit 1
304
+ fi
305
+ else
306
+ ACME_CMD="acme.sh"
307
+ fi
308
+
309
+ # 注册 acme.sh 账户(如果需要)
310
+ # 如果提供了 email,注册账户;否则使用 Let's Encrypt(不需要注册)
311
+ USE_ZEROSSL=false
312
+ if [ -n "$ACME_EMAIL" ]; then
313
+ log "注册 acme.sh 账户(使用 ZeroSSL)..."
314
+ if [ -f "$ACME_SH_HOME/acme.sh" ]; then
315
+ if "$ACME_SH_HOME/acme.sh" --register-account -m "$ACME_EMAIL"; then
316
+ USE_ZEROSSL=true
317
+ log "ZeroSSL 账户注册成功"
318
+ else
319
+ warning "账户注册失败,将使用 Let's Encrypt"
320
+ USE_ZEROSSL=false
321
+ fi
322
+ else
323
+ if $ACME_CMD --register-account -m "$ACME_EMAIL"; then
324
+ USE_ZEROSSL=true
325
+ log "ZeroSSL 账户注册成功"
326
+ else
327
+ warning "账户注册失败,将使用 Let's Encrypt"
328
+ USE_ZEROSSL=false
329
+ fi
330
+ fi
331
+ fi
332
+
333
+ # 如果没有使用 ZeroSSL,明确设置使用 Let's Encrypt CA
334
+ if [ "$USE_ZEROSSL" = false ]; then
335
+ log "使用 Let's Encrypt CA(不需要账户注册)..."
336
+ # 明确设置使用 Let's Encrypt CA
337
+ if [ -f "$ACME_SH_HOME/acme.sh" ]; then
338
+ "$ACME_SH_HOME/acme.sh" --set-default-ca --server letsencrypt || true
339
+ else
340
+ $ACME_CMD --set-default-ca --server letsencrypt || true
341
+ fi
342
+ fi
343
+
344
+ # 验证 API 凭证
345
+ verify_ali_api_credentials || {
346
+ error "API 凭证验证失败"
347
+ exit 1
348
+ }
349
+
350
+ # 检查域名
351
+ check_domain_in_aliyun "$TARGET_DOMAIN"
352
+
353
+ # 申请 Tools 域名证书
354
+ log "申请 $TARGET_DOMAIN 证书..."
355
+ ISSUE_CMD_ARGS="--dns dns_ali -d $TARGET_DOMAIN"
356
+ if [ "$USE_ZEROSSL" = false ]; then
357
+ ISSUE_CMD_ARGS="$ISSUE_CMD_ARGS --server letsencrypt"
358
+ fi
359
+
360
+ # 添加调试选项
361
+ if [ "$DEBUG_MODE" = true ]; then
362
+ ISSUE_CMD_ARGS="$ISSUE_CMD_ARGS --debug"
363
+ log "启用调试模式"
364
+ fi
365
+
366
+ # 添加日志选项以便查看详细错误
367
+ ISSUE_CMD_ARGS="$ISSUE_CMD_ARGS --log"
368
+
369
+ log "执行命令: $ACME_CMD --issue $ISSUE_CMD_ARGS"
370
+ log "如果失败,请查看日志: tail -f ~/.acme.sh/acme.sh.log"
371
+
372
+ # 捕获错误输出
373
+ ERROR_OUTPUT=""
374
+ if [ -f "$ACME_SH_HOME/acme.sh" ]; then
375
+ if ! ERROR_OUTPUT=$("$ACME_SH_HOME/acme.sh" --issue $ISSUE_CMD_ARGS 2>&1); then
376
+ # 检查是否是速率限制错误
377
+ if ! check_rate_limit_error "$ERROR_OUTPUT" "$TARGET_DOMAIN"; then
378
+ exit 1
379
+ fi
380
+
381
+ error "$TARGET_DOMAIN 证书申请失败"
382
+ error ""
383
+ error "可能的原因:"
384
+ error "1. 阿里云 API 凭证无效或权限不足"
385
+ error "2. 域名 $TARGET_DOMAIN 不在阿里云 DNS 管理"
386
+ error "3. RAM 用户缺少 DNS 管理权限(需要 AliyunDNSFullAccess 或自定义权限)"
387
+ error "4. 网络连接问题"
388
+ error ""
389
+ error "排查步骤:"
390
+ error "1. 检查 API 凭证是否正确: echo \$Ali_Key"
391
+ error "2. 确认域名在阿里云 DNS 控制台已添加"
392
+ error "3. 检查 RAM 用户权限是否包含 alidns:AddDomainRecord"
393
+ error "4. 查看详细日志: tail -100 ~/.acme.sh/acme.sh.log"
394
+ error "5. 使用调试模式重新运行: $0 --debug --ali-key \"\$Ali_Key\" --ali-secret \"\$Ali_Secret\""
395
+ exit 1
396
+ fi
397
+ else
398
+ if ! ERROR_OUTPUT=$($ACME_CMD --issue $ISSUE_CMD_ARGS 2>&1); then
399
+ # 检查是否是速率限制错误
400
+ if ! check_rate_limit_error "$ERROR_OUTPUT" "$TARGET_DOMAIN"; then
401
+ exit 1
402
+ fi
403
+
404
+ error "$TARGET_DOMAIN 证书申请失败"
405
+ error ""
406
+ error "可能的原因:"
407
+ error "1. 阿里云 API 凭证无效或权限不足"
408
+ error "2. 域名 $TARGET_DOMAIN 不在阿里云 DNS 管理"
409
+ error "3. RAM 用户缺少 DNS 管理权限(需要 AliyunDNSFullAccess 或自定义权限)"
410
+ error "4. 网络连接问题"
411
+ error ""
412
+ error "排查步骤:"
413
+ error "1. 检查 API 凭证是否正确: echo \$Ali_Key"
414
+ error "2. 确认域名在阿里云 DNS 控制台已添加"
415
+ error "3. 检查 RAM 用户权限是否包含 alidns:AddDomainRecord"
416
+ error "4. 查看详细日志: tail -100 ~/.acme.sh/acme.sh.log"
417
+ error "5. 使用调试模式重新运行: $0 --debug --ali-key \"\$Ali_Key\" --ali-secret \"\$Ali_Secret\""
418
+ exit 1
419
+ fi
420
+ fi
421
+
422
+ log "$TARGET_DOMAIN 证书申请成功"
423
+
424
+ # 安装 Tools 域名证书到 nginx
425
+ log "安装 $TARGET_DOMAIN 证书到 nginx..."
426
+ if [ -f "$ACME_SH_HOME/acme.sh" ]; then
427
+ "$ACME_SH_HOME/acme.sh" --install-cert \
428
+ -d "$TARGET_DOMAIN" \
429
+ --key-file /etc/nginx/ssl/${TARGET_DOMAIN}.key \
430
+ --fullchain-file /etc/nginx/ssl/${TARGET_DOMAIN}.crt \
431
+ --reloadcmd "systemctl reload nginx || true"
432
+ else
433
+ $ACME_CMD --install-cert \
434
+ -d "$TARGET_DOMAIN" \
435
+ --key-file /etc/nginx/ssl/${TARGET_DOMAIN}.key \
436
+ --fullchain-file /etc/nginx/ssl/${TARGET_DOMAIN}.crt \
437
+ --reloadcmd "systemctl reload nginx || true"
438
+ fi
439
+
440
+ # 设置证书文件权限
441
+ chmod 600 /etc/nginx/ssl/${TARGET_DOMAIN}.key
442
+ chmod 644 /etc/nginx/ssl/${TARGET_DOMAIN}.crt
443
+ chown root:root /etc/nginx/ssl/${TARGET_DOMAIN}.key
444
+ chown root:root /etc/nginx/ssl/${TARGET_DOMAIN}.crt
445
+
446
+ log "$TARGET_DOMAIN 证书安装完成"
447
+
448
+ # 测试 nginx 配置
449
+ log "测试 nginx 配置..."
450
+ if nginx -t; then
451
+ log "Nginx 配置验证成功"
452
+ # 重新加载 nginx
453
+ if systemctl is-active --quiet nginx 2>/dev/null; then
454
+ log "重新加载 Nginx..."
455
+ systemctl reload nginx || warning "Nginx 重新加载失败,请手动检查"
456
+ else
457
+ warning "Nginx 未运行,请手动启动: sudo systemctl start nginx"
458
+ fi
459
+ else
460
+ error "Nginx 配置验证失败,请检查配置文件"
461
+ exit 1
462
+ fi
463
+
464
+ log ""
465
+ log "SSL 证书配置完成!"
466
+ log ""
467
+ log "证书文件位置:"
468
+ log " - $TARGET_DOMAIN: /etc/nginx/ssl/${TARGET_DOMAIN}.crt"
469
+ log ""
470
+ log "注意:"
471
+ log " - acme.sh 已自动配置 cron 任务,证书将自动续期"
472
+ log " - 证书续期后会自动重新加载 nginx"
473
+ log " - 可以通过 'crontab -l' 查看续期任务"
474
+
package/dist/cli.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * n2-deploy CLI: server init, nginx config, SSL management.
4
+ * Config: default ~/.deploy/config.yaml (ALIYUN keys + domains).
5
+ */
6
+ export {};
7
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;GAGG"}
package/dist/cli.js ADDED
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * n2-deploy CLI: server init, nginx config, SSL management.
4
+ * Config: default ~/.deploy/config.yaml (ALIYUN keys + domains).
5
+ */
6
+ import { Command } from "commander";
7
+ import consola from "consola";
8
+ import { runConfigDelete, runConfigGet, runConfigInit, runConfigList, runConfigSet, } from "./commands/config.js";
9
+ import { runInit } from "./commands/init.js";
10
+ import { runNginx } from "./commands/nginx.js";
11
+ import { runSslLogs } from "./commands/ssl-logs.js";
12
+ import { runSsl } from "./commands/ssl.js";
13
+ import { getDefaultConfigPath } from "./lib/paths.js";
14
+ const defaultConfig = getDefaultConfigPath();
15
+ const program = new Command();
16
+ program
17
+ .name("n2-deploy")
18
+ .description("Server init, nginx config generation, and SSL certificate management")
19
+ .version("0.1.0");
20
+ const configCmd = program
21
+ .command("config")
22
+ .description("配置管理:初始化、查看、设置、删除配置项")
23
+ .option("-c, --config <path>", "配置文件路径(默认 ~/.deploy/config.yaml)", defaultConfig);
24
+ function configPathFromCmd(cmd) {
25
+ const selfOpts = cmd.opts?.() ?? {};
26
+ const parentOpts = cmd.parent?.opts?.() ?? {};
27
+ return (selfOpts.config ??
28
+ parentOpts.config ??
29
+ defaultConfig);
30
+ }
31
+ configCmd
32
+ .command("init")
33
+ .description("初始化配置文件(创建 ~/.deploy/config.yaml 默认模板)")
34
+ .option("-c, --config <path>", "配置文件路径", defaultConfig)
35
+ .option("-f, --force", "已存在时覆盖")
36
+ .action(async (opts, cmd) => {
37
+ const configPath = opts.config ?? configPathFromCmd(cmd);
38
+ try {
39
+ await runConfigInit({ config: configPath, force: opts.force });
40
+ }
41
+ catch (e) {
42
+ consola.error(e);
43
+ process.exit(1);
44
+ }
45
+ });
46
+ configCmd
47
+ .command("list")
48
+ .description("列出所有配置项")
49
+ .option("-c, --config <path>", "配置文件路径", defaultConfig)
50
+ .action(async (opts, cmd) => {
51
+ const configPath = opts?.config ?? configPathFromCmd(cmd);
52
+ try {
53
+ await runConfigList({ config: configPath });
54
+ }
55
+ catch (e) {
56
+ consola.error(e);
57
+ process.exit(1);
58
+ }
59
+ });
60
+ configCmd
61
+ .command("get [key]")
62
+ .description("查看配置项(不传 key 则列出全部)。key: aliyun_access_key_id, api.backend_port, admin.domains 等")
63
+ .option("-c, --config <path>", "配置文件路径", defaultConfig)
64
+ .action(async (key, opts, cmd) => {
65
+ const configPath = opts?.config ?? configPathFromCmd(cmd);
66
+ try {
67
+ await runConfigGet({ config: configPath, key });
68
+ }
69
+ catch (e) {
70
+ consola.error(e);
71
+ process.exit(1);
72
+ }
73
+ });
74
+ configCmd
75
+ .command("set <key> [values...]")
76
+ .description("设置配置项。domains 类可传多个值,如: config set admin.domains a.com b.com")
77
+ .option("-c, --config <path>", "配置文件路径", defaultConfig)
78
+ .action(async (key, values, opts, cmd) => {
79
+ const configPath = opts?.config ?? configPathFromCmd(cmd);
80
+ try {
81
+ await runConfigSet({
82
+ config: configPath,
83
+ key,
84
+ values: Array.isArray(values) ? values : values ? [values] : [],
85
+ });
86
+ }
87
+ catch (e) {
88
+ consola.error(e);
89
+ process.exit(1);
90
+ }
91
+ });
92
+ configCmd
93
+ .command("delete <key>")
94
+ .description("删除/清空配置项(domains 清空列表,密钥置空)")
95
+ .option("-c, --config <path>", "配置文件路径", defaultConfig)
96
+ .action(async (key, opts, cmd) => {
97
+ const configPath = opts?.config ?? configPathFromCmd(cmd);
98
+ try {
99
+ await runConfigDelete({ config: configPath, key });
100
+ }
101
+ catch (e) {
102
+ consola.error(e);
103
+ process.exit(1);
104
+ }
105
+ });
106
+ program
107
+ .command("init")
108
+ .description("初始化服务器(幂等):nginx、目录、deploy.sh、占位证书")
109
+ .option("-c, --config <path>", "配置文件路径(默认 ~/.deploy/config.yaml)", defaultConfig)
110
+ .option("--scripts-dir <path>", "部署脚本目录(默认 N2_DEPLOY_SCRIPTS_DIR 或包内 assets/deploy)")
111
+ .action(async (opts) => {
112
+ try {
113
+ await runInit({ config: opts.config, scriptsDir: opts.scriptsDir });
114
+ }
115
+ catch (e) {
116
+ consola.error(e);
117
+ process.exit(1);
118
+ }
119
+ });
120
+ program
121
+ .command("nginx")
122
+ .description("Generate nginx config from domains config; optionally install and reload")
123
+ .option("-c, --config <path>", "Path to deploy config (default: ~/.deploy/config.yaml)", defaultConfig)
124
+ .option("-o, --output <path>", "Output path for nginx config", "nginx/n2.conf")
125
+ .option("-i, --install", "Install to system nginx dir and reload")
126
+ .action(async (opts) => {
127
+ try {
128
+ await runNginx({
129
+ config: opts.config,
130
+ output: opts.output,
131
+ install: opts.install,
132
+ });
133
+ }
134
+ catch (e) {
135
+ consola.error(e);
136
+ process.exit(1);
137
+ }
138
+ });
139
+ program
140
+ .command("ssl")
141
+ .description("Setup/renew SSL certificates for all domains (acme.sh + Aliyun DNS)")
142
+ .option("-c, --config <path>", "Path to deploy config (default: ~/.deploy/config.yaml)", defaultConfig)
143
+ .option("--ali-key <key>", "Aliyun Access Key ID (or set ALIYUN_ACCESS_KEY_ID)")
144
+ .option("--ali-secret <secret>", "Aliyun Access Key Secret (or set ALIYUN_ACCESS_KEY_SECRET)")
145
+ .option("--log-file <path>", "SSL log file path", "/opt/deploy/logs/ssl.log")
146
+ .option("--scripts-dir <path>", "Path to deploy scripts")
147
+ .action(async (opts) => {
148
+ try {
149
+ await runSsl({
150
+ config: opts.config,
151
+ aliKey: opts.aliKey,
152
+ aliSecret: opts.aliSecret,
153
+ logFile: opts.logFile,
154
+ scriptsDir: opts.scriptsDir,
155
+ });
156
+ }
157
+ catch (e) {
158
+ consola.error(e);
159
+ process.exit(1);
160
+ }
161
+ });
162
+ program
163
+ .command("ssl-logs")
164
+ .description("View SSL log (create, renew, success, failure)")
165
+ .option("--log-file <path>", "SSL log file path", "/opt/deploy/logs/ssl.log")
166
+ .option("-n, --lines <n>", "Number of lines to show", "50")
167
+ .option("-f, --follow", "Follow log (tail -f)")
168
+ .option("--action <action>", "Filter by action: create | renew | success | failure | run")
169
+ .option("--result <result>", "Filter by result: ok | fail")
170
+ .action(async (opts) => {
171
+ try {
172
+ await runSslLogs({
173
+ logFile: opts.logFile,
174
+ lines: parseInt(opts.lines, 10) || 50,
175
+ follow: opts.follow,
176
+ action: opts.action,
177
+ result: opts.result,
178
+ });
179
+ }
180
+ catch (e) {
181
+ consola.error(e);
182
+ process.exit(1);
183
+ }
184
+ });
185
+ program.parse();
186
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}