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/src/tools/files.js
CHANGED
|
@@ -154,21 +154,24 @@ function findFiles(ctx, { query, root, limit = 100, exts = null, minSizeBytes =
|
|
|
154
154
|
if (exts?.length) args.push('-r', `.*\\.(${exts.map((e) => String(e).replace(/^\\./, '')).join('|')})$`)
|
|
155
155
|
args.push(String(query || ''))
|
|
156
156
|
const r = spawnSync(es.path, args, { encoding: 'utf8', windowsHide: true, timeout: 15000 })
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
157
|
+
// es.exe 在 session 0 连不上 Everything IPC 时退出码非 0(stderr: Error 8),必须回退 fd/walk
|
|
158
|
+
if (r.status === 0) {
|
|
159
|
+
try {
|
|
160
|
+
const rows = JSON.parse(r.stdout || '[]')
|
|
161
|
+
return ok({
|
|
162
|
+
tool: 'everything',
|
|
163
|
+
count: rows.length,
|
|
164
|
+
entries: rows.slice(0, 500).map((row) => ({
|
|
165
|
+
path: String(row.path || row.full_path || ''),
|
|
166
|
+
name: String(row.name || basename(String(row.path || ''))),
|
|
167
|
+
type: Number(row.type) === 2 ? 'dir' : 'file',
|
|
168
|
+
sizeBytes: row.size == null ? null : Number(row.size),
|
|
169
|
+
mtimeMs: isoMs(row.date_modified)
|
|
170
|
+
}))
|
|
171
|
+
})
|
|
172
|
+
} catch {
|
|
173
|
+
/* fallthrough */
|
|
174
|
+
}
|
|
172
175
|
}
|
|
173
176
|
}
|
|
174
177
|
const fd = toolInfo('fd')
|
package/src/tools/git.js
CHANGED
|
@@ -2,8 +2,9 @@ import { existsSync, readdirSync, realpathSync } from 'node:fs'
|
|
|
2
2
|
import { basename, join, resolve } from 'node:path'
|
|
3
3
|
import { z } from 'zod'
|
|
4
4
|
import { isInsideRoots } from '../core/config.js'
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { gitBinary, gitSpawnDiagnostics, runGit } from '../core/git-exec.js'
|
|
6
|
+
import { clip } from '../core/util.js'
|
|
7
|
+
import { denied, fail, invalidArg, notFound, ok } from '../core/result.js'
|
|
7
8
|
|
|
8
9
|
function guard(path, config) {
|
|
9
10
|
const abs = resolve(path)
|
|
@@ -14,10 +15,53 @@ function guard(path, config) {
|
|
|
14
15
|
return { abs }
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
// git 可执行文件解析/执行与 spawn 诊断都在 src/core/git-exec.js(cluster.js 共用同一套回退)。
|
|
19
|
+
// [why] 2026-11-10:peer-node 的 mesh server 由没设 PATH 的启动脚本拉起,spawn git 直接 ENOENT,
|
|
20
|
+
// 旧代码把 top.error 一律吞成「不是仓库」,把真因藏了。现在 spawn 失败如实报 E_GIT_SPAWN。
|
|
21
|
+
function gitFailureResult(failure, extra = {}) {
|
|
22
|
+
return fail(failure.code, failure.message, { ...failure.extra, ...extra })
|
|
23
|
+
}
|
|
24
|
+
|
|
17
25
|
async function repoRootOf(abs, { timeout, signal } = {}) {
|
|
18
|
-
const top = await
|
|
19
|
-
if (top.error
|
|
26
|
+
const top = await runGit(['-C', abs, 'rev-parse', '--show-toplevel'], { timeout, signal })
|
|
27
|
+
if (top.error) {
|
|
28
|
+
const spawnCode = top.error.code
|
|
29
|
+
// spawn 失败时 error.code 是字符串(ENOENT/EACCES…);进程正常退出(不是仓库)时是数字 exit code
|
|
30
|
+
if (typeof spawnCode === 'string') {
|
|
31
|
+
const probed = (await gitBinary()).tried || []
|
|
32
|
+
return {
|
|
33
|
+
failure: {
|
|
34
|
+
code: 'E_GIT_SPAWN',
|
|
35
|
+
message: `cannot spawn git (${spawnCode}): ${top.error.message || spawnCode}`,
|
|
36
|
+
extra: { spawnCode, ...gitSpawnDiagnostics(), probed }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (top.killed || top.timedOut) {
|
|
41
|
+
return {
|
|
42
|
+
failure: {
|
|
43
|
+
code: 'E_GIT_TIMEOUT',
|
|
44
|
+
message: `git rev-parse timed out after ${timeout ?? '?'}ms: git -C ${abs} rev-parse --show-toplevel`,
|
|
45
|
+
extra: { timeoutMs: timeout ?? null }
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const stderr = String(top.stderr || '').trim()
|
|
50
|
+
if (top.code === 128 && /not a git repository/i.test(stderr)) {
|
|
51
|
+
return { failure: { code: 'E_INVALID_ARG', message: `not a git repository: ${abs}`, extra: { exitCode: top.code, stderr: stderr.slice(0, 500) } } }
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
failure: {
|
|
55
|
+
code: 'E_GIT_EXIT',
|
|
56
|
+
message: `git rev-parse failed (exit ${top.code}): ${stderr.slice(0, 400) || 'no stderr'}`,
|
|
57
|
+
extra: { exitCode: top.code, stderr: stderr.slice(0, 500) }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
20
61
|
const root = top.stdout.trim()
|
|
62
|
+
if (!root) {
|
|
63
|
+
return { failure: { code: 'E_GIT_EXIT', message: `git rev-parse returned no path for ${abs} (exit ${top.code})`, extra: { exitCode: top.code } } }
|
|
64
|
+
}
|
|
21
65
|
try {
|
|
22
66
|
return { root: realpathSync(root) }
|
|
23
67
|
} catch {
|
|
@@ -44,18 +88,18 @@ function countDirty(entries) {
|
|
|
44
88
|
|
|
45
89
|
async function repoSummary(abs, { timeout, signal, entries = 200 } = {}) {
|
|
46
90
|
const found = await repoRootOf(abs, { timeout, signal })
|
|
47
|
-
if (found.
|
|
91
|
+
if (found.failure) return { failure: found.failure }
|
|
48
92
|
const root = found.root
|
|
49
93
|
const [branch, head, upstream, status] = await Promise.all([
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
94
|
+
runGit(['-C', root, 'rev-parse', '--abbrev-ref', 'HEAD'], { timeout, signal }),
|
|
95
|
+
runGit(['-C', root, 'rev-parse', 'HEAD'], { timeout, signal }),
|
|
96
|
+
runGit(['-C', root, 'rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'], { timeout, signal }),
|
|
97
|
+
runGit(['-C', root, 'status', '--porcelain=v1', '-b', '--untracked-files=normal'], { timeout, signal })
|
|
54
98
|
])
|
|
55
99
|
let ahead = null
|
|
56
100
|
let behind = null
|
|
57
101
|
if (!upstream.error) {
|
|
58
|
-
const counts = await
|
|
102
|
+
const counts = await runGit(['-C', root, 'rev-list', '--left-right', '--count', '@{u}...HEAD'], {
|
|
59
103
|
timeout,
|
|
60
104
|
signal
|
|
61
105
|
})
|
|
@@ -69,17 +113,19 @@ async function repoSummary(abs, { timeout, signal, entries = 200 } = {}) {
|
|
|
69
113
|
const branchLine = lines.find((l) => l.startsWith('## ')) || ''
|
|
70
114
|
const statusEntries = lines.filter((l) => !l.startsWith('## '))
|
|
71
115
|
return {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
116
|
+
summary: {
|
|
117
|
+
root,
|
|
118
|
+
name: basename(root),
|
|
119
|
+
branch: branch.error ? null : branch.stdout.trim(),
|
|
120
|
+
head: head.error ? null : head.stdout.trim(),
|
|
121
|
+
upstream: upstream.error ? null : upstream.stdout.trim(),
|
|
122
|
+
ahead,
|
|
123
|
+
behind,
|
|
124
|
+
statusBranchLine: branchLine,
|
|
125
|
+
dirty: countDirty(statusEntries),
|
|
126
|
+
entries: statusEntries.slice(0, entries),
|
|
127
|
+
truncatedEntries: Math.max(0, statusEntries.length - entries)
|
|
128
|
+
}
|
|
83
129
|
}
|
|
84
130
|
}
|
|
85
131
|
|
|
@@ -124,8 +170,8 @@ export function buildGit(ctx) {
|
|
|
124
170
|
const g = guard(path, config)
|
|
125
171
|
if (g.error) return g.error
|
|
126
172
|
const timeout = config.limits?.gitTimeoutMs ?? 20000
|
|
127
|
-
const summary = await repoSummary(g.abs, { timeout, signal })
|
|
128
|
-
if (
|
|
173
|
+
const { summary, failure } = await repoSummary(g.abs, { timeout, signal })
|
|
174
|
+
if (failure) return gitFailureResult(failure, { path: g.abs })
|
|
129
175
|
return ok(summary)
|
|
130
176
|
}
|
|
131
177
|
},
|
|
@@ -146,13 +192,13 @@ export function buildGit(ctx) {
|
|
|
146
192
|
if (g.error) return g.error
|
|
147
193
|
const timeout = config.limits?.gitTimeoutMs ?? 20000
|
|
148
194
|
const found = await repoRootOf(g.abs, { timeout, signal })
|
|
149
|
-
if (found.
|
|
195
|
+
if (found.failure) return gitFailureResult(found.failure, { path: g.abs })
|
|
150
196
|
const n = Math.min(limit ?? 20, 200)
|
|
151
197
|
const args = ['-C', found.root, 'log', `-n${n}`]
|
|
152
198
|
if (format === 'detail') args.push('--date=iso-strict', '--pretty=format:%H%x09%h%x09%an%x09%aI%x09%s')
|
|
153
199
|
else args.push('--date=short', '--pretty=format:%h%x09%ad%x09%s')
|
|
154
200
|
if (ref) args.push(ref, '--')
|
|
155
|
-
const out = await
|
|
201
|
+
const out = await runGit(args, { timeout, signal })
|
|
156
202
|
if (out.error) return invalidArg(`git log failed: ${out.stderr.trim().slice(0, 500)}`)
|
|
157
203
|
const commits = out.stdout
|
|
158
204
|
.split(/\r?\n/)
|
|
@@ -184,13 +230,13 @@ export function buildGit(ctx) {
|
|
|
184
230
|
if (g.error) return g.error
|
|
185
231
|
const timeout = config.limits?.gitTimeoutMs ?? 20000
|
|
186
232
|
const found = await repoRootOf(g.abs, { timeout, signal })
|
|
187
|
-
if (found.
|
|
233
|
+
if (found.failure) return gitFailureResult(found.failure, { path: g.abs })
|
|
188
234
|
const base = ['-C', found.root, 'diff']
|
|
189
235
|
if (staged) base.push('--staged')
|
|
190
236
|
if (ref) base.push(ref)
|
|
191
237
|
const pathspec = file ? ['--', file] : []
|
|
192
|
-
const statOut = await
|
|
193
|
-
const diffOut = await
|
|
238
|
+
const statOut = await runGit([...base, '--stat', ...pathspec], { timeout, signal })
|
|
239
|
+
const diffOut = await runGit([...base, ...pathspec], { timeout, signal })
|
|
194
240
|
if (diffOut.error && diffOut.code !== 1) {
|
|
195
241
|
return invalidArg(`git diff failed: ${diffOut.stderr.trim().slice(0, 500)}`)
|
|
196
242
|
}
|
|
@@ -233,13 +279,17 @@ export function buildGit(ctx) {
|
|
|
233
279
|
targets.push(...findRepos(abs, { depth: depth ?? 2, maxRepos: cap - targets.length }))
|
|
234
280
|
}
|
|
235
281
|
const repos = []
|
|
282
|
+
const errors = []
|
|
236
283
|
let index = 0
|
|
237
284
|
const worker = async () => {
|
|
238
285
|
while (index < targets.length) {
|
|
239
286
|
const dir = targets[index]
|
|
240
287
|
index += 1
|
|
241
|
-
const summary = await repoSummary(dir, { timeout, signal, entries: 20 })
|
|
242
|
-
if (
|
|
288
|
+
const { summary, failure } = await repoSummary(dir, { timeout, signal, entries: 20 })
|
|
289
|
+
if (failure) {
|
|
290
|
+
errors.push({ path: dir, code: failure.code, message: failure.message, ...failure.extra })
|
|
291
|
+
continue
|
|
292
|
+
}
|
|
243
293
|
const dirtyCount =
|
|
244
294
|
summary.dirty.staged + summary.dirty.modified + summary.dirty.untracked + summary.dirty.unmerged
|
|
245
295
|
if (dirtyOnly && dirtyCount === 0 && !(summary.ahead > 0)) continue
|
|
@@ -248,6 +298,10 @@ export function buildGit(ctx) {
|
|
|
248
298
|
}
|
|
249
299
|
await Promise.all(Array.from({ length: Math.min(4, targets.length) }, worker))
|
|
250
300
|
repos.sort((a, b) => a.root.localeCompare(b.root))
|
|
301
|
+
// 全部候选都失败时把真因抛出来(2026-11-10 的坑:repo_scan 只回 scanned 1/repos 0,看不出是 git spawn 失败)
|
|
302
|
+
if (targets.length > 0 && repos.length === 0 && errors.length === targets.length) {
|
|
303
|
+
return gitFailureResult(errors[0], { scanned: targets.length, errors })
|
|
304
|
+
}
|
|
251
305
|
const dirty = repos.filter(
|
|
252
306
|
(r) => r.dirty.staged + r.dirty.modified + r.dirty.untracked + r.dirty.unmerged > 0 || r.ahead > 0
|
|
253
307
|
)
|
|
@@ -258,6 +312,7 @@ export function buildGit(ctx) {
|
|
|
258
312
|
repos: repos.length,
|
|
259
313
|
dirtyRepos: dirty.length,
|
|
260
314
|
dirtyOnly: Boolean(dirtyOnly),
|
|
315
|
+
...(errors.length ? { errors } : {}),
|
|
261
316
|
items: repos
|
|
262
317
|
})
|
|
263
318
|
}
|