@adhdev/daemon-core 0.9.82-rc.302 → 0.9.82-rc.303

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.
@@ -1,4 +1,15 @@
1
1
  import type { GitFailureReason, GitRepoIdentity } from './git-types.js';
2
+ /**
3
+ * Timeout for status-collection git commands (status/log/submodule/stash/fetch).
4
+ * The default 5s is fine for porcelain status, but on Windows the `git` subprocess
5
+ * spawn itself is pathologically slow (measured: `submodule status` ~3.5s, cold
6
+ * `log -1` ~4.2s) and overlaps with refreshUpstream fetches — a single command
7
+ * routinely exceeds 5s, which previously collapsed the whole status to all-null and
8
+ * dropped the node from the mesh graph. Give the collection path a much larger
9
+ * budget so a slow-but-healthy repo never reads as "not a git repo". Windows gets a
10
+ * larger budget than POSIX because the spawn cost is OS-specific, not repo-specific.
11
+ */
12
+ export declare const GIT_STATUS_TIMEOUT_MS: number;
2
13
  export interface GitExecutorOptions {
3
14
  timeoutMs?: number;
4
15
  maxBuffer?: number;
@@ -1,5 +1,7 @@
1
1
  import type { GitRepoStatus } from './git-types.js';
2
2
  import { type DaemonBuildInfo } from '../build-info.js';
3
+ /** Test seam: clear the last-known-good status cache between cases. */
4
+ export declare function __resetGitStatusCacheForTests(): void;
3
5
  export interface GitStatusOptions {
4
6
  timeoutMs?: number;
5
7
  /** When true, include submodule status in the result. Defaults to true. */
package/dist/index.js CHANGED
@@ -82,6 +82,7 @@ var init_repo_mesh_types = __esm({
82
82
  // src/git/git-executor.ts
83
83
  var git_executor_exports = {};
84
84
  __export(git_executor_exports, {
85
+ GIT_STATUS_TIMEOUT_MS: () => GIT_STATUS_TIMEOUT_MS,
85
86
  GitCommandError: () => GitCommandError,
86
87
  isPathInside: () => isPathInside,
87
88
  normalizeGitOutput: () => normalizeGitOutput,
@@ -246,7 +247,7 @@ function mapExecError(error, cwd, argv, behavior) {
246
247
  cause: error
247
248
  });
248
249
  }
249
- var import_node_child_process, import_node_fs, import_promises, path, import_node_util, execFileAsync, DEFAULT_TIMEOUT_MS, DEFAULT_MAX_BUFFER, GitCommandError;
250
+ var import_node_child_process, import_node_fs, import_promises, path, import_node_util, execFileAsync, DEFAULT_TIMEOUT_MS, DEFAULT_MAX_BUFFER, GIT_STATUS_TIMEOUT_MS, GitCommandError;
250
251
  var init_git_executor = __esm({
251
252
  "src/git/git-executor.ts"() {
252
253
  "use strict";
@@ -258,6 +259,7 @@ var init_git_executor = __esm({
258
259
  execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
259
260
  DEFAULT_TIMEOUT_MS = 5e3;
260
261
  DEFAULT_MAX_BUFFER = 1024 * 1024;
262
+ GIT_STATUS_TIMEOUT_MS = process.platform === "win32" ? 3e4 : 2e4;
261
263
  GitCommandError = class extends Error {
262
264
  reason;
263
265
  stdout;
@@ -293,10 +295,10 @@ function readInjected(value) {
293
295
  }
294
296
  function getDaemonBuildInfo() {
295
297
  if (cached) return cached;
296
- const commit = readInjected(true ? "850c147fda8183e55c145fcef876d2c25a22c8d2" : void 0) ?? "unknown";
297
- const commitShort = readInjected(true ? "850c147f" : void 0) ?? (commit !== "unknown" ? commit.slice(0, 7) : "unknown");
298
- const version = readInjected(true ? "0.9.82-rc.302" : void 0) ?? readInjected(typeof process !== "undefined" ? process.env?.ADHDEV_PKG_VERSION : void 0) ?? "unknown";
299
- const builtAt = readInjected(true ? "2026-06-17T02:29:22.806Z" : void 0);
298
+ const commit = readInjected(true ? "21741b4b2d3c1f2ed8a280045e6158b2730391ed" : void 0) ?? "unknown";
299
+ const commitShort = readInjected(true ? "21741b4b" : void 0) ?? (commit !== "unknown" ? commit.slice(0, 7) : "unknown");
300
+ const version = readInjected(true ? "0.9.82-rc.303" : void 0) ?? readInjected(typeof process !== "undefined" ? process.env?.ADHDEV_PKG_VERSION : void 0) ?? "unknown";
301
+ const builtAt = readInjected(true ? "2026-06-17T04:16:06.174Z" : void 0);
300
302
  cached = builtAt ? { commit, commitShort, version, builtAt } : { commit, commitShort, version };
301
303
  return cached;
302
304
  }
@@ -308,64 +310,79 @@ var init_build_info = __esm({
308
310
  });
309
311
 
310
312
  // src/git/git-status.ts
313
+ function isTransientGitFailure(error) {
314
+ return error.reason === "timeout" || error.reason === "git_command_failed";
315
+ }
311
316
  async function getGitRepoStatus(workspace, options = {}) {
312
317
  const lastCheckedAt = Date.now();
313
318
  const includeSubmodules = options.includeSubmodules !== false;
319
+ const effectiveOptions = options.timeoutMs === void 0 ? { ...options, timeoutMs: GIT_STATUS_TIMEOUT_MS } : options;
314
320
  try {
315
- const repo = await resolveGitRepository(workspace, options);
316
- let parsed = await readPorcelainStatus(repo, options);
317
- let upstreamProbe = getInitialUpstreamProbe(parsed);
318
- if (options.refreshUpstream) {
319
- upstreamProbe = await refreshTrackedUpstream(repo, parsed, options);
320
- if (upstreamProbe.upstreamStatus === "fresh") {
321
- parsed = await readPorcelainStatus(repo, options);
322
- }
323
- }
324
- const head = await readHead(repo, options);
325
- const stashCount = await readStashCount(repo, options);
326
- let submodules;
327
- if (includeSubmodules) {
328
- submodules = await getSubmoduleStatuses(repo, options);
329
- }
330
- const submoduleDirty = (submodules || []).some((submodule) => submodule.dirty || submodule.outOfSync || !!submodule.error);
331
- const dirty = parsed.staged + parsed.modified + parsed.untracked + parsed.deleted + parsed.renamed > 0 || parsed.conflictFiles.length > 0 || stashCount > 0 || submoduleDirty;
332
- const daemonBuildBehind = await detectDaemonBuildBehind(repo, submodules, options);
333
- return {
334
- workspace: repo.workspace,
335
- repoRoot: repo.repoRoot,
336
- isGitRepo: true,
337
- branch: parsed.branch,
338
- headCommit: head.commit,
339
- headMessage: head.message,
340
- upstream: parsed.upstream,
341
- upstreamStatus: parsed.upstream ? upstreamProbe.upstreamStatus : "no_upstream",
342
- upstreamFetchedAt: upstreamProbe.upstreamFetchedAt,
343
- upstreamFetchError: upstreamProbe.upstreamFetchError,
344
- ahead: parsed.ahead,
345
- behind: parsed.behind,
346
- staged: parsed.staged,
347
- modified: parsed.modified,
348
- untracked: parsed.untracked,
349
- deleted: parsed.deleted,
350
- renamed: parsed.renamed,
351
- dirty,
352
- hasConflicts: parsed.conflictFiles.length > 0,
353
- conflictFiles: parsed.conflictFiles,
354
- stashCount,
355
- lastCheckedAt,
356
- submodules,
357
- ...daemonBuildBehind ? { daemonBuildBehind } : {}
358
- };
321
+ const repo = await resolveGitRepository(workspace, effectiveOptions);
322
+ const status = await collectGitRepoStatus(repo, includeSubmodules, lastCheckedAt, effectiveOptions);
323
+ lastKnownGoodStatus.set(workspace, status);
324
+ return status;
359
325
  } catch (error) {
360
- if (error instanceof GitCommandError) {
361
- return emptyStatus(workspace, lastCheckedAt, error);
326
+ const gitError = error instanceof GitCommandError ? error : new GitCommandError("git_command_failed", "Failed to read Git status", { cause: error });
327
+ if (isTransientGitFailure(gitError)) {
328
+ const cached2 = lastKnownGoodStatus.get(workspace);
329
+ if (cached2) {
330
+ return {
331
+ ...cached2,
332
+ lastCheckedAt,
333
+ upstreamStatus: "unavailable",
334
+ error: gitError.stderr || gitError.message,
335
+ reason: gitError.reason
336
+ };
337
+ }
362
338
  }
363
- return emptyStatus(
364
- workspace,
365
- lastCheckedAt,
366
- new GitCommandError("git_command_failed", "Failed to read Git status", { cause: error })
367
- );
339
+ return emptyStatus(workspace, lastCheckedAt, gitError);
340
+ }
341
+ }
342
+ async function collectGitRepoStatus(repo, includeSubmodules, lastCheckedAt, options) {
343
+ let parsed = await readPorcelainStatus(repo, options);
344
+ let upstreamProbe = getInitialUpstreamProbe(parsed);
345
+ if (options.refreshUpstream) {
346
+ upstreamProbe = await refreshTrackedUpstream(repo, parsed, options);
347
+ if (upstreamProbe.upstreamStatus === "fresh") {
348
+ parsed = await readPorcelainStatus(repo, options);
349
+ }
350
+ }
351
+ const head = await readHead(repo, options);
352
+ const stashCount = await readStashCount(repo, options);
353
+ let submodules;
354
+ if (includeSubmodules) {
355
+ submodules = await getSubmoduleStatuses(repo, options);
368
356
  }
357
+ const submoduleDirty = (submodules || []).some((submodule) => submodule.dirty || submodule.outOfSync || !!submodule.error);
358
+ const dirty = parsed.staged + parsed.modified + parsed.untracked + parsed.deleted + parsed.renamed > 0 || parsed.conflictFiles.length > 0 || stashCount > 0 || submoduleDirty;
359
+ const daemonBuildBehind = await detectDaemonBuildBehind(repo, submodules, options);
360
+ return {
361
+ workspace: repo.workspace,
362
+ repoRoot: repo.repoRoot,
363
+ isGitRepo: true,
364
+ branch: parsed.branch,
365
+ headCommit: head.commit,
366
+ headMessage: head.message,
367
+ upstream: parsed.upstream,
368
+ upstreamStatus: parsed.upstream ? upstreamProbe.upstreamStatus : "no_upstream",
369
+ upstreamFetchedAt: upstreamProbe.upstreamFetchedAt,
370
+ upstreamFetchError: upstreamProbe.upstreamFetchError,
371
+ ahead: parsed.ahead,
372
+ behind: parsed.behind,
373
+ staged: parsed.staged,
374
+ modified: parsed.modified,
375
+ untracked: parsed.untracked,
376
+ deleted: parsed.deleted,
377
+ renamed: parsed.renamed,
378
+ dirty,
379
+ hasConflicts: parsed.conflictFiles.length > 0,
380
+ conflictFiles: parsed.conflictFiles,
381
+ stashCount,
382
+ lastCheckedAt,
383
+ submodules,
384
+ ...daemonBuildBehind ? { daemonBuildBehind } : {}
385
+ };
369
386
  }
370
387
  function isNonRuntimeRootFile(file) {
371
388
  const base = file.slice(file.lastIndexOf("/") + 1);
@@ -607,7 +624,7 @@ function emptyStatus(workspace, lastCheckedAt, error) {
607
624
  async function getSubmoduleStatuses(repo, options) {
608
625
  if (!repo.repoRoot) return [];
609
626
  try {
610
- const result = await runGit(repo, ["submodule", "status", "--recursive"], options);
627
+ const result = await runGit(repo, ["submodule", "status"], options);
611
628
  const submodules = parseSubmoduleStatusOutput(result.stdout, repo.repoRoot, options.submoduleIgnorePaths);
612
629
  await Promise.all(submodules.map((submodule) => enrichSubmoduleWorktreeStatus(repo, submodule, options)));
613
630
  return submodules;
@@ -651,12 +668,13 @@ function parseSubmoduleStatusOutput(output, repoRoot, ignorePaths) {
651
668
  }
652
669
  return submodules;
653
670
  }
654
- var DAEMON_RUNTIME_PACKAGES, WEB_ONLY_PACKAGES;
671
+ var lastKnownGoodStatus, DAEMON_RUNTIME_PACKAGES, WEB_ONLY_PACKAGES;
655
672
  var init_git_status = __esm({
656
673
  "src/git/git-status.ts"() {
657
674
  "use strict";
658
675
  init_git_executor();
659
676
  init_build_info();
677
+ lastKnownGoodStatus = /* @__PURE__ */ new Map();
660
678
  DAEMON_RUNTIME_PACKAGES = /* @__PURE__ */ new Set([
661
679
  "daemon-core",
662
680
  "daemon-standalone",