ssh-release 0.2.0 → 0.3.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.3.0 - 2026-06-25
4
+
5
+ ### Added
6
+
7
+ - 增加 `ssh-release doctor` 远端锁状态检查,无锁时通过,有锁时提示锁路径、pid、创建时间和安全清理条件。
8
+
3
9
  ## 0.2.0 - 2026-06-25
4
10
 
5
11
  ### Added
package/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
  已实现:
12
12
 
13
13
  - `ssh-release init`:生成 `ssh-release.config.ts` 配置模板。
14
- - `ssh-release doctor`:检查配置、本地源路径、SSH 连接、远程目录和远端 `tar`。
14
+ - `ssh-release doctor`:检查配置、本地源路径、SSH 连接、远程目录、远端锁和远端 `tar`。
15
15
  - `ssh-release deploy`:发布本地文件或目录。
16
16
  - `ssh-release list`:查看远程版本和当前版本。
17
17
  - `ssh-release rollback [version]`:回滚到上一个版本或指定版本。
@@ -168,6 +168,8 @@ source files -> package -> upload -> release -> activate -> rollback
168
168
  ssh-release doctor
169
169
  ```
170
170
 
171
+ `doctor` 会检查 `.ssh-release.lock`。没有锁时正常通过;如果发现锁,会以 `warn` 显示锁路径、pid、创建时间和安全清理提示。
172
+
171
173
  需要先查看发布计划但不修改服务器时:
172
174
 
173
175
  ```bash
@@ -347,6 +349,7 @@ docs/
347
349
  - macOS AppleDouble 元数据排除。
348
350
  - `release` 和 `overwrite` 发布流程。
349
351
  - 发布和回滚锁获取、释放和锁冲突拦截。
352
+ - `doctor` 远端锁状态检查和安全清理提示。
350
353
  - 远端 `tar` 失败后的逐文件上传回退。
351
354
  - 远程版本列表读取和当前版本标记。
352
355
  - `doctor` 检查结果。
package/dist/doctor.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { access } from 'node:fs/promises';
2
2
  import { loadConfigFile } from './config.js';
3
+ import { readRemoteLockStatus } from './lock.js';
3
4
  import { shellQuote } from './remote.js';
4
5
  export async function runDoctorFromFile(configPath, createClient) {
5
6
  let config;
@@ -33,6 +34,7 @@ export async function runDoctor(config, client, options = {}) {
33
34
  await addSourcePathCheck(checks, config.source.path);
34
35
  await addRemoteCheck(checks, 'SSH 连接', () => client.exec('true'), 'SSH 可连接');
35
36
  await addRemoteCheck(checks, '远程目录', () => client.exec(`mkdir -p ${shellQuote(config.target.path)} && test -w ${shellQuote(config.target.path)}`), '远程目录可创建且可写');
37
+ await addRemoteLockCheck(checks, config, client);
36
38
  try {
37
39
  await client.exec('command -v tar');
38
40
  checks.push({
@@ -78,6 +80,36 @@ async function addConfigFileCheck(checks, configPath) {
78
80
  });
79
81
  }
80
82
  }
83
+ async function addRemoteLockCheck(checks, config, client) {
84
+ try {
85
+ const lockStatus = await readRemoteLockStatus(config, client);
86
+ if (!lockStatus.locked) {
87
+ checks.push({
88
+ name: '远端锁',
89
+ status: 'pass',
90
+ message: '没有发现发布或回滚锁',
91
+ });
92
+ return;
93
+ }
94
+ checks.push({
95
+ name: '远端锁',
96
+ status: 'warn',
97
+ message: [
98
+ `发现远端锁: ${lockStatus.lockPath}`,
99
+ `pid: ${lockStatus.pid ?? '未知'}`,
100
+ `创建时间: ${lockStatus.createdAt ?? '未知'}`,
101
+ '确认没有发布或回滚任务后再手动删除该目录',
102
+ ].join(';'),
103
+ });
104
+ }
105
+ catch (error) {
106
+ checks.push({
107
+ name: '远端锁',
108
+ status: 'fail',
109
+ message: error instanceof Error ? error.message : String(error),
110
+ });
111
+ }
112
+ }
81
113
  async function addSourcePathCheck(checks, sourcePath) {
82
114
  try {
83
115
  await access(sourcePath);
package/dist/lock.js CHANGED
@@ -3,6 +3,25 @@ const remoteLockDir = '.ssh-release.lock';
3
3
  export function getRemoteLockPath(config) {
4
4
  return remoteJoin(config.target.path, remoteLockDir);
5
5
  }
6
+ export async function readRemoteLockStatus(config, client) {
7
+ const lockPath = getRemoteLockPath(config);
8
+ const pidPath = remoteJoin(lockPath, 'pid');
9
+ const createdAtPath = remoteJoin(lockPath, 'created_at');
10
+ const result = await client.exec(`if [ -d ${shellQuote(lockPath)} ]; then echo locked; printf 'pid='; cat ${shellQuote(pidPath)} 2>/dev/null || true; printf '\\ncreated_at='; cat ${shellQuote(createdAtPath)} 2>/dev/null || true; printf '\\n'; else echo unlocked; fi`);
11
+ const lines = result.stdout.split('\n').map((line) => line.trim());
12
+ if (lines[0] !== 'locked') {
13
+ return {
14
+ locked: false,
15
+ lockPath,
16
+ };
17
+ }
18
+ return {
19
+ locked: true,
20
+ lockPath,
21
+ pid: parseRemoteLockValue(lines, 'pid'),
22
+ createdAt: parseRemoteLockValue(lines, 'created_at'),
23
+ };
24
+ }
6
25
  export async function acquireRemoteLock(config, client, options = {}) {
7
26
  const createTargetPath = options.createTargetPath ?? true;
8
27
  const lockPath = getRemoteLockPath(config);
@@ -19,3 +38,9 @@ export async function acquireRemoteLock(config, client, options = {}) {
19
38
  await client.exec(`rm -rf ${shellQuote(lockPath)}`);
20
39
  };
21
40
  }
41
+ function parseRemoteLockValue(lines, fieldName) {
42
+ const prefix = `${fieldName}=`;
43
+ const line = lines.find((entry) => entry.startsWith(prefix));
44
+ const value = line?.slice(prefix.length).trim();
45
+ return value || undefined;
46
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ssh-release",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "A general-purpose SSH file release CLI.",
5
5
  "type": "module",
6
6
  "bin": {