ssh-release 0.4.0 → 0.5.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0 - 2026-06-25
4
+
5
+ ### Added
6
+
7
+ - 增加全局 `--json` 输出,支持 `deploy`、`doctor`、`list`、`rollback`、`unlock` 等命令输出单行结构化结果,便于 CI/CD 解析。
8
+
3
9
  ## 0.4.0 - 2026-06-25
4
10
 
5
11
  ### Added
package/README.md CHANGED
@@ -16,6 +16,7 @@
16
16
  - `ssh-release list`:查看远程版本和当前版本。
17
17
  - `ssh-release rollback [version]`:回滚到上一个版本或指定版本。
18
18
  - `ssh-release unlock`:查看远端锁,并在显式确认锁路径后删除锁。
19
+ - `--json`:输出单行 JSON,便于 CI/CD 解析。
19
20
  - `release` 模式:上传压缩包、远端解压、切换 `current`、清理旧版本。
20
21
  - `overwrite` 模式:直接覆盖发布到目标目录。
21
22
  - 远端 `tar` 解压失败时回退逐文件上传。
@@ -224,6 +225,32 @@ ssh-release rollback 20260625-150000
224
225
 
225
226
  `overwrite` 模式没有版本列表,也不支持回滚。
226
227
 
228
+ ## JSON 输出
229
+
230
+ 需要在自动化脚本中解析结果时,可以给命令增加 `--json`:
231
+
232
+ ```bash
233
+ ssh-release deploy --dry-run --json
234
+ ssh-release doctor --json
235
+ ssh-release list --json
236
+ ssh-release rollback --json
237
+ ssh-release unlock --json
238
+ ```
239
+
240
+ 成功时输出单行 JSON:
241
+
242
+ ```json
243
+ {"ok":true,"command":"deploy","result":{"dryRun":true,"mode":"release"}}
244
+ ```
245
+
246
+ 命令执行失败时输出:
247
+
248
+ ```json
249
+ {"ok":false,"command":"deploy","error":"错误信息"}
250
+ ```
251
+
252
+ `doctor` 检查失败、`unlock` 发现锁但未删除时会返回非零退出码,并在 JSON 的 `result` 中保留检查结果。
253
+
227
254
  ## 安全边界
228
255
 
229
256
  配置模板不包含真实 IP、密码或生产路径。
@@ -363,6 +390,7 @@ docs/
363
390
  - 危险远程路径拒绝。
364
391
  - 发布模式和压缩格式校验。
365
392
  - CLI 命令分发。
393
+ - CLI JSON 输出。
366
394
  - 本地 `.tgz` 打包和排除项。
367
395
  - macOS AppleDouble 元数据排除。
368
396
  - `release` 和 `overwrite` 发布流程。
package/dist/cli.js CHANGED
@@ -19,15 +19,28 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
19
19
  const io = options.io ?? console;
20
20
  const parsed = parseCliArgs(argv);
21
21
  if (parsed.error) {
22
+ if (parsed.json) {
23
+ printJsonError(parsed.command, parsed.error, io);
24
+ return 1;
25
+ }
22
26
  io.error(parsed.error);
23
27
  return 1;
24
28
  }
25
29
  if (parsed.help) {
30
+ if (parsed.json) {
31
+ printJsonResult('help', { usage: createUsageText() }, 0, io);
32
+ return 0;
33
+ }
26
34
  printUsage(io);
27
35
  return 0;
28
36
  }
29
37
  if (parsed.version) {
30
- io.log(readPackageVersion());
38
+ const version = readPackageVersion();
39
+ if (parsed.json) {
40
+ printJsonResult('version', { version }, 0, io);
41
+ return 0;
42
+ }
43
+ io.log(version);
31
44
  return 0;
32
45
  }
33
46
  const handlers = options.handlers ?? createDefaultHandlers(parsed.configPath);
@@ -36,35 +49,76 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
36
49
  try {
37
50
  if (command === 'init') {
38
51
  await handlers.init();
52
+ if (parsed.json) {
53
+ printJsonResult('init', { configPath: parsed.configPath }, 0, io);
54
+ return 0;
55
+ }
39
56
  io.log(`已创建 ${parsed.configPath}`);
40
57
  return 0;
41
58
  }
42
59
  if (command === 'deploy') {
43
- printDeployResult(await handlers.deploy({ dryRun: parsed.dryRun }), io);
60
+ const result = await handlers.deploy({ dryRun: parsed.dryRun });
61
+ if (parsed.json) {
62
+ printJsonResult('deploy', result, 0, io);
63
+ return 0;
64
+ }
65
+ printDeployResult(result, io);
44
66
  return 0;
45
67
  }
46
68
  if (command === 'rollback') {
47
- printRollbackResult(await handlers.rollback(args[0]), io);
69
+ const result = await handlers.rollback(args[0]);
70
+ if (parsed.json) {
71
+ printJsonResult('rollback', result, 0, io);
72
+ return 0;
73
+ }
74
+ printRollbackResult(result, io);
48
75
  return 0;
49
76
  }
50
77
  if (command === 'list') {
51
- printListResult(await handlers.list(), io);
78
+ const result = await handlers.list();
79
+ if (parsed.json) {
80
+ printJsonResult('list', result, 0, io);
81
+ return 0;
82
+ }
83
+ printListResult(result, io);
52
84
  return 0;
53
85
  }
54
86
  if (command === 'doctor') {
55
87
  const report = await handlers.doctor();
88
+ const exitCode = report.ok ? 0 : 1;
89
+ if (parsed.json) {
90
+ printJsonResult('doctor', report, exitCode, io);
91
+ return exitCode;
92
+ }
56
93
  printDoctorReport(report, io);
57
- return report.ok ? 0 : 1;
94
+ return exitCode;
58
95
  }
59
96
  if (command === 'unlock') {
60
97
  const result = await handlers.unlock({ confirmPath: parsed.confirmPath });
98
+ const exitCode = result.locked && !result.removed ? 1 : 0;
99
+ if (parsed.json) {
100
+ printJsonResult('unlock', result, exitCode, io);
101
+ return exitCode;
102
+ }
61
103
  printUnlockResult(result, io);
62
- return result.locked && !result.removed ? 1 : 0;
104
+ return exitCode;
105
+ }
106
+ if (parsed.json) {
107
+ if (!command) {
108
+ printJsonResult('help', { usage: createUsageText() }, 0, io);
109
+ return 0;
110
+ }
111
+ printJsonError(command, `未知命令: ${command}`, io);
112
+ return 1;
63
113
  }
64
114
  printUsage(io);
65
115
  return command ? 1 : 0;
66
116
  }
67
117
  catch (error) {
118
+ if (parsed.json) {
119
+ printJsonError(command, formatError(error), io);
120
+ return 1;
121
+ }
68
122
  io.error(error instanceof Error ? error.message : String(error));
69
123
  return 1;
70
124
  }
@@ -75,6 +129,7 @@ function parseCliArgs(argv) {
75
129
  let configPath = CONFIG_FILE_NAME;
76
130
  let dryRun = false;
77
131
  let help = false;
132
+ let json = false;
78
133
  let version = false;
79
134
  for (let index = 0; index < argv.length; index += 1) {
80
135
  const arg = argv[index];
@@ -86,10 +141,14 @@ function parseCliArgs(argv) {
86
141
  version = true;
87
142
  continue;
88
143
  }
144
+ if (arg === '--json') {
145
+ json = true;
146
+ continue;
147
+ }
89
148
  if (arg === '--config' || arg === '-c') {
90
149
  const value = argv[index + 1];
91
150
  if (!value || value.startsWith('-')) {
92
- return createParsedError('--config 需要配置文件路径', configPath);
151
+ return createParsedError('--config 需要配置文件路径', configPath, json);
93
152
  }
94
153
  configPath = value;
95
154
  index += 1;
@@ -102,14 +161,14 @@ function parseCliArgs(argv) {
102
161
  if (arg === '--confirm') {
103
162
  const value = argv[index + 1];
104
163
  if (!value || value.startsWith('-')) {
105
- return createParsedError('--confirm 需要远端锁路径', configPath);
164
+ return createParsedError('--confirm 需要远端锁路径', configPath, json);
106
165
  }
107
166
  confirmPath = value;
108
167
  index += 1;
109
168
  continue;
110
169
  }
111
170
  if (arg.startsWith('-')) {
112
- return createParsedError(`未知选项: ${arg}`, configPath);
171
+ return createParsedError(`未知选项: ${arg}`, configPath, json);
113
172
  }
114
173
  args.push(arg);
115
174
  }
@@ -120,16 +179,18 @@ function parseCliArgs(argv) {
120
179
  configPath,
121
180
  dryRun,
122
181
  help,
182
+ json,
123
183
  version,
124
184
  };
125
185
  }
126
- function createParsedError(error, configPath) {
186
+ function createParsedError(error, configPath, json = false) {
127
187
  return {
128
188
  args: [],
129
189
  configPath,
130
190
  dryRun: false,
131
191
  error,
132
192
  help: false,
193
+ json,
133
194
  version: false,
134
195
  };
135
196
  }
@@ -230,6 +291,20 @@ function printUnlockResult(result, io) {
230
291
  io.log('确认没有发布或回滚任务后再执行:');
231
292
  io.log(`ssh-release unlock --confirm ${result.lockPath}`);
232
293
  }
294
+ function printJsonResult(command, result, exitCode, io) {
295
+ io.log(JSON.stringify({
296
+ ok: exitCode === 0,
297
+ command,
298
+ result,
299
+ }));
300
+ }
301
+ function printJsonError(command, error, io) {
302
+ io.log(JSON.stringify({
303
+ ok: false,
304
+ command,
305
+ error,
306
+ }));
307
+ }
233
308
  function toRealPath(filePath) {
234
309
  try {
235
310
  return realpathSync(filePath);
@@ -239,7 +314,10 @@ function toRealPath(filePath) {
239
314
  }
240
315
  }
241
316
  function printUsage(io) {
242
- io.log(`用法:
317
+ io.log(createUsageText());
318
+ }
319
+ function createUsageText() {
320
+ return `用法:
243
321
  ssh-release init [--config <path>]
244
322
  ssh-release doctor [--config <path>]
245
323
  ssh-release deploy [--config <path>]
@@ -247,13 +325,17 @@ function printUsage(io) {
247
325
  ssh-release list [--config <path>]
248
326
  ssh-release rollback [version] [--config <path>]
249
327
  ssh-release unlock [--confirm <lock-path>] [--config <path>]
328
+ ssh-release <command> --json
250
329
  ssh-release --help
251
- ssh-release --version`);
330
+ ssh-release --version`;
252
331
  }
253
332
  function readPackageVersion() {
254
333
  const packageJson = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
255
334
  return packageJson.version ?? '0.0.0';
256
335
  }
336
+ function formatError(error) {
337
+ return error instanceof Error ? error.message : String(error);
338
+ }
257
339
  if (isCliEntrypoint(import.meta.url, process.argv[1])) {
258
340
  const exitCode = await runCli();
259
341
  process.exit(exitCode);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ssh-release",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "A general-purpose SSH file release CLI.",
5
5
  "type": "module",
6
6
  "bin": {