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.
@@ -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
- try {
158
- const rows = JSON.parse(r.stdout || '[]')
159
- return ok({
160
- tool: 'everything',
161
- count: rows.length,
162
- entries: rows.slice(0, 500).map((row) => ({
163
- path: String(row.path || row.full_path || ''),
164
- name: String(row.name || basename(String(row.path || ''))),
165
- type: Number(row.type) === 2 ? 'dir' : 'file',
166
- sizeBytes: row.size == null ? null : Number(row.size),
167
- mtimeMs: isoMs(row.date_modified)
168
- }))
169
- })
170
- } catch {
171
- /* fallthrough */
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 { clip, run } from '../core/util.js'
6
- import { denied, invalidArg, notFound, ok } from '../core/result.js'
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 run('git', ['-C', abs, 'rev-parse', '--show-toplevel'], { timeout, signal })
19
- if (top.error || !top.stdout.trim()) return { error: top }
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.error) return null
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
- run('git', ['-C', root, 'rev-parse', '--abbrev-ref', 'HEAD'], { timeout, signal }),
51
- run('git', ['-C', root, 'rev-parse', 'HEAD'], { timeout, signal }),
52
- run('git', ['-C', root, 'rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'], { timeout, signal }),
53
- run('git', ['-C', root, 'status', '--porcelain=v1', '-b', '--untracked-files=normal'], { timeout, signal })
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 run('git', ['-C', root, 'rev-list', '--left-right', '--count', '@{u}...HEAD'], {
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
- root,
73
- name: basename(root),
74
- branch: branch.error ? null : branch.stdout.trim(),
75
- head: head.error ? null : head.stdout.trim(),
76
- upstream: upstream.error ? null : upstream.stdout.trim(),
77
- ahead,
78
- behind,
79
- statusBranchLine: branchLine,
80
- dirty: countDirty(statusEntries),
81
- entries: statusEntries.slice(0, entries),
82
- truncatedEntries: Math.max(0, statusEntries.length - entries)
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 (!summary) return invalidArg(`not a git repository: ${g.abs}`)
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.error) return invalidArg(`not a git repository: ${g.abs}`)
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 run('git', args, { timeout, signal })
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.error) return invalidArg(`not a git repository: ${g.abs}`)
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 run('git', [...base, '--stat', ...pathspec], { timeout, signal })
193
- const diffOut = await run('git', [...base, ...pathspec], { timeout, signal })
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 (!summary) continue
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
  }