dsh-mesh-node 0.0.7 → 0.0.9
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/package.json +1 -1
- package/src/core/git-exec.js +77 -0
- package/src/core/remote-path.js +53 -0
- package/src/tools/cluster.js +31 -9
- package/src/tools/delegate.js +653 -653
- package/src/tools/files.js +18 -15
- package/src/tools/git.js +85 -30
package/package.json
CHANGED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// @generated 共享的「git 可执行文件解析 + 执行」,供 src/tools/git.js 与 src/tools/cluster.js 共用
|
|
2
|
+
// [why] 2026-11-10:mesh server 由计划任务/启动脚本拉起,PATH 里可能没有 git(peer-node 实测:git 在
|
|
3
|
+
// C:\Users\<user>\tools\git\cmd,只有 DSH 的 start-dsh0.cmd 把它写进 PATH)。git.js 已有
|
|
4
|
+
// 「DSH_MESH_GIT → PATH → 常见安装目录」回退;cluster.js 的 gitInfo() 却仍直接 execFileSync('git'),
|
|
5
|
+
// 于是 mesh_overview 的本机 repo 摘要在干净 PATH 下失效且只报空。抽成共用模块,两处行为一致。
|
|
6
|
+
import { join } from 'node:path'
|
|
7
|
+
import { run } from './util.js'
|
|
8
|
+
|
|
9
|
+
/** 找不到 git 时给用户的可诊断提示。 */
|
|
10
|
+
const SPAWN_HINT =
|
|
11
|
+
'the mesh server process cannot find git on its PATH. Add the git directory to PATH in the mesh server start script, or set DSH_MESH_GIT to the git executable, then restart mesh (no DSH restart needed).'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* git 可执行文件候选,按优先级:DSH_MESH_GIT 覆盖 → PATH 里的 git → 平台常见安装位置。
|
|
15
|
+
* @returns {string[]} 去重后的候选列表
|
|
16
|
+
*/
|
|
17
|
+
export function gitCandidates() {
|
|
18
|
+
const env = process.env
|
|
19
|
+
const list = []
|
|
20
|
+
if (env.DSH_MESH_GIT) list.push(env.DSH_MESH_GIT)
|
|
21
|
+
list.push('git')
|
|
22
|
+
if (process.platform === 'win32') {
|
|
23
|
+
for (const root of [env.ProgramFiles, env.ProgramW6432, env['ProgramFiles(x86)']].filter(Boolean)) {
|
|
24
|
+
list.push(join(root, 'Git', 'cmd', 'git.exe'))
|
|
25
|
+
}
|
|
26
|
+
if (env.LOCALAPPDATA) list.push(join(env.LOCALAPPDATA, 'Programs', 'Git', 'cmd', 'git.exe'))
|
|
27
|
+
if (env.USERPROFILE) list.push(join(env.USERPROFILE, 'tools', 'git', 'cmd', 'git.exe'))
|
|
28
|
+
} else {
|
|
29
|
+
list.push('/usr/bin/git', '/usr/local/bin/git', '/opt/homebrew/bin/git')
|
|
30
|
+
}
|
|
31
|
+
return [...new Set(list)]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 解析一次并缓存(`git --version` 成功才算命中)。 */
|
|
35
|
+
let gitProbe = null
|
|
36
|
+
/**
|
|
37
|
+
* @returns {Promise<{bin: string, tried: string[]} | {error: {code: string, message: string}, tried: string[]}>}
|
|
38
|
+
*/
|
|
39
|
+
export function gitBinary() {
|
|
40
|
+
if (!gitProbe) {
|
|
41
|
+
gitProbe = (async () => {
|
|
42
|
+
const tried = []
|
|
43
|
+
for (const candidate of gitCandidates()) {
|
|
44
|
+
tried.push(candidate)
|
|
45
|
+
const out = await run(candidate, ['--version'], { timeout: 5000 })
|
|
46
|
+
if (!out.error && /\bgit version\b/i.test(out.stdout)) return { bin: candidate, tried }
|
|
47
|
+
}
|
|
48
|
+
return { error: { code: 'ENOENT', message: `git not found; probed: ${tried.join(', ')}` }, tried }
|
|
49
|
+
})()
|
|
50
|
+
}
|
|
51
|
+
return gitProbe
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 用解析到的 git 跑命令;解析失败时返回与 run() 同形的 error 结果,调用方不必各自处理。
|
|
56
|
+
* @param {string[]} args git 参数
|
|
57
|
+
* @param {{timeout?: number, cwd?: string, signal?: AbortSignal, maxBuffer?: number}} [options] 透传给 run()
|
|
58
|
+
* @returns {Promise<{error: unknown, stdout: string, stderr: string, code: number, killed: boolean, timedOut: boolean}>}
|
|
59
|
+
*/
|
|
60
|
+
export async function runGit(args, options) {
|
|
61
|
+
const found = await gitBinary()
|
|
62
|
+
if (found.error) return { error: found.error, stdout: '', stderr: '', code: 0, killed: false, timedOut: false }
|
|
63
|
+
return run(found.bin, args, options)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 诊断信息:spawn 失败时附加到工具错误里,避免把「找不到 git」误报成「不是仓库」。
|
|
68
|
+
* @returns {{hint: string, gitCandidates: string[], pathEnv: string, nodePid: number}}
|
|
69
|
+
*/
|
|
70
|
+
export function gitSpawnDiagnostics() {
|
|
71
|
+
return {
|
|
72
|
+
hint: SPAWN_HINT,
|
|
73
|
+
gitCandidates: gitCandidates(),
|
|
74
|
+
pathEnv: String(process.env.PATH || process.env.Path || ''),
|
|
75
|
+
nodePid: process.pid
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// @generated 平台无关的「对端路径」拼接 / 取父目录(src 与 scripts 共用)
|
|
2
|
+
// [why] 2026-11-10:peer-inspect.mjs 用本机 `node:path.dirname` 推导对端用户目录。调用方可能是 Linux
|
|
3
|
+
// (手机 proot 里的 node),对端路径却是 Windows(`C:\Users\<user>\.dsh-mesh`)——
|
|
4
|
+
// posix dirname 把整串当成一个文件名,返回 `.`,于是拼出相对路径 `.dsh0/storages/workspace.json`,
|
|
5
|
+
// 被对端按 mesh server 的 cwd 解析成 `...\Code\dsh-mesh\.dsh0\storages\workspace.json` → E_NOT_FOUND。
|
|
6
|
+
// 对端路径的分隔符由**对端平台**决定,不能交给本机 path 模块;这里只用字符串处理,`/` 和 `\` 都支持。
|
|
7
|
+
// 2026-11-10 batch2(B5):从 scripts/lib/remote-path.mjs 提升到 src/core,供 src/tools/cluster.js 等共用;
|
|
8
|
+
// scripts/lib/remote-path.mjs 只做 re-export,保持既有 import 不变。
|
|
9
|
+
|
|
10
|
+
/** 探测一串路径使用的分隔符:出现反斜杠按 Windows 处理,否则按 POSIX。 */
|
|
11
|
+
function sepOf(p) {
|
|
12
|
+
return String(p ?? '').includes('\\') ? '\\' : '/'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 平台无关的取父目录。
|
|
17
|
+
* `C:\Users\<user>\.dsh-mesh` → `C:\Users\<user>`
|
|
18
|
+
* `/root/.dsh-mesh` → `/root`;`C:\` → `C:\`;`/` → `/`
|
|
19
|
+
* 没有分隔符的单段(相对名)原样返回——调用方只应对绝对路径使用它。
|
|
20
|
+
* @param {string} p 路径(对端风格,`/` 或 `\` 均可)
|
|
21
|
+
* @returns {string} 父目录
|
|
22
|
+
*/
|
|
23
|
+
export function parentDir(p) {
|
|
24
|
+
const s = String(p ?? '')
|
|
25
|
+
if (!s) return ''
|
|
26
|
+
const trimmed = s.replace(/[\\/]+$/, '')
|
|
27
|
+
if (!trimmed) return s[0]
|
|
28
|
+
const m = /^(.*)[\\/][^\\/]*$/.exec(trimmed)
|
|
29
|
+
if (!m) {
|
|
30
|
+
// 单段:盘符(C:)要补回分隔符,相对名原样返回
|
|
31
|
+
return /^[A-Za-z]:$/.test(trimmed) ? trimmed + sepOf(s) : trimmed
|
|
32
|
+
}
|
|
33
|
+
if (/^[A-Za-z]:$/.test(m[1])) return m[1] + sepOf(s)
|
|
34
|
+
return m[1] || sepOf(s)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 平台无关拼接:分隔符跟随 base(base 含反斜杠就用 `\`),避免在本机用错分隔符拼出相对路径。
|
|
39
|
+
* `joinRemote('C:\\Users\\Administrator', '.dsh0', 'storages')` → `C:\Users\<user>\.dsh0\storages`
|
|
40
|
+
* @param {string} base 基准目录
|
|
41
|
+
* @param {...string} parts 追加的路径段
|
|
42
|
+
* @returns {string} 拼接结果
|
|
43
|
+
*/
|
|
44
|
+
export function joinRemote(base, ...parts) {
|
|
45
|
+
const sep = sepOf(base)
|
|
46
|
+
let out = String(base ?? '').replace(/[\\/]+$/, '')
|
|
47
|
+
for (const part of parts) {
|
|
48
|
+
const p = String(part ?? '').replace(/^[\\/]+|[\\/]+$/g, '')
|
|
49
|
+
if (!p) continue
|
|
50
|
+
out = out ? `${out}${sep}${p}` : p
|
|
51
|
+
}
|
|
52
|
+
return out || sep
|
|
53
|
+
}
|
package/src/tools/cluster.js
CHANGED
|
@@ -1,25 +1,42 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
|
3
|
-
import { execFileSync } from 'node:child_process'
|
|
4
3
|
import { join } from 'node:path'
|
|
5
4
|
import { loadPeers } from '../core/pairing.js'
|
|
6
5
|
import { clusterReport, probePeer } from '../core/cluster.js'
|
|
7
6
|
import { callPeerTool } from '../core/peer-client.js'
|
|
7
|
+
import { gitSpawnDiagnostics, runGit } from '../core/git-exec.js'
|
|
8
|
+
import { joinRemote } from '../core/remote-path.js'
|
|
8
9
|
import { fail, ok } from '../core/result.js'
|
|
9
10
|
|
|
10
11
|
const TERMINAL = new Set(['done', 'failed', 'cancelled'])
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
/**
|
|
14
|
+
* 本机仓库摘要。走共享的 git 解析(DSH_MESH_GIT → PATH → 常见安装目录),
|
|
15
|
+
* 否则计划任务式干净 PATH 下这里会把「找不到 git」报成空摘要。
|
|
16
|
+
* @param {string} repo 仓库目录
|
|
17
|
+
* @returns {Promise<object>} { path, head, branch, dirty, ahead, behind } 或 { path, error, ...诊断 }
|
|
18
|
+
*/
|
|
19
|
+
async function gitInfo(repo) {
|
|
13
20
|
if (!repo) return { path: '', error: 'no repo path' }
|
|
14
|
-
const run = (args) =>
|
|
21
|
+
const run = async (args) => {
|
|
22
|
+
const out = await runGit(['-C', repo, ...args], { timeout: 5000 })
|
|
23
|
+
if (out.error) {
|
|
24
|
+
const code = typeof out.error.code === 'string' ? out.error.code : `exit-${out.code}`
|
|
25
|
+
const error = new Error(out.error.message || out.stderr.trim() || `git ${args[0]} failed`)
|
|
26
|
+
error.spawnCode = typeof out.error.code === 'string' ? out.error.code : null
|
|
27
|
+
error.code = code
|
|
28
|
+
throw error
|
|
29
|
+
}
|
|
30
|
+
return out.stdout.trim()
|
|
31
|
+
}
|
|
15
32
|
try {
|
|
16
|
-
const head = run(['rev-parse', '--short', 'HEAD'])
|
|
17
|
-
const branch = run(['rev-parse', '--abbrev-ref', 'HEAD'])
|
|
18
|
-
const dirty = run(['status', '--porcelain']) !== ''
|
|
33
|
+
const head = await run(['rev-parse', '--short', 'HEAD'])
|
|
34
|
+
const branch = await run(['rev-parse', '--abbrev-ref', 'HEAD'])
|
|
35
|
+
const dirty = (await run(['status', '--porcelain'])) !== ''
|
|
19
36
|
let ahead = null
|
|
20
37
|
let behind = null
|
|
21
38
|
try {
|
|
22
|
-
const counts = run(['rev-list', '--left-right', '--count', 'HEAD...@{upstream}']).split(/\s+/).map(Number)
|
|
39
|
+
const counts = (await run(['rev-list', '--left-right', '--count', 'HEAD...@{upstream}'])).split(/\s+/).map(Number)
|
|
23
40
|
ahead = counts[0] || 0
|
|
24
41
|
behind = counts[1] || 0
|
|
25
42
|
} catch {
|
|
@@ -27,6 +44,9 @@ function gitInfo(repo) {
|
|
|
27
44
|
}
|
|
28
45
|
return { path: repo, head, branch, dirty, ahead, behind }
|
|
29
46
|
} catch (error) {
|
|
47
|
+
if (error?.spawnCode) {
|
|
48
|
+
return { path: repo, error: `E_GIT_SPAWN: ${error.message}`, ...gitSpawnDiagnostics() }
|
|
49
|
+
}
|
|
30
50
|
return { path: repo, error: String(error?.message || error) }
|
|
31
51
|
}
|
|
32
52
|
}
|
|
@@ -140,7 +160,8 @@ export function buildCluster(ctx) {
|
|
|
140
160
|
const selfOut = await callPeerTool(peer, { tool: 'mesh_self', args: {}, timeoutMs: Math.min(timeoutMs, 8000) })
|
|
141
161
|
const selfText = selfOut.result?.content?.[0]?.text || '{}'
|
|
142
162
|
const self = JSON.parse(selfText)
|
|
143
|
-
|
|
163
|
+
// 对端仓库路径由**对端**决定:用平台无关拼接(Linux 节点拼 Windows 对端路径不能走本机 path.join)
|
|
164
|
+
const repoPath = self.mesh?.repo || joinRemote(self.acl?.fsRoots?.[0]?.path || '', 'dsh-mesh')
|
|
144
165
|
const statusOut = await callPeerTool(peer, { tool: 'git_status', args: { path: repoPath }, timeoutMs: Math.min(timeoutMs, 8000) })
|
|
145
166
|
const statusText = statusOut.result?.content?.[0]?.text || '{}'
|
|
146
167
|
const status = JSON.parse(statusText)
|
|
@@ -157,12 +178,13 @@ export function buildCluster(ctx) {
|
|
|
157
178
|
peerRepos.push({ peer: peer.name, error: String(error?.message || error) })
|
|
158
179
|
}
|
|
159
180
|
}
|
|
181
|
+
const localRepo = await gitInfo(process.cwd())
|
|
160
182
|
return ok({
|
|
161
183
|
node: report.node,
|
|
162
184
|
summary: report.summary,
|
|
163
185
|
verdict: report.verdict,
|
|
164
186
|
issues: report.issues,
|
|
165
|
-
repos: { local:
|
|
187
|
+
repos: { local: localRepo, peers: peerRepos },
|
|
166
188
|
instances: (report.instances || []).map((instance) => ({
|
|
167
189
|
id: instance.id,
|
|
168
190
|
label: instance.label,
|