@adhdev/daemon-core 0.9.82-rc.301 → 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.
- package/dist/cli-adapters/resolve-executable.d.ts +14 -0
- package/dist/git/git-executor.d.ts +11 -0
- package/dist/git/git-status.d.ts +2 -0
- package/dist/index.js +578 -516
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +583 -521
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/cli-adapters/pty-transport.ts +2 -1
- package/src/cli-adapters/resolve-executable.ts +45 -0
- package/src/cli-adapters/session-host-transport.ts +2 -1
- package/src/commands/upgrade-helper.ts +29 -0
- package/src/git/git-executor.ts +12 -0
- package/src/git/git-status.ts +71 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adhdev/daemon-core",
|
|
3
|
-
"version": "0.9.82-rc.
|
|
3
|
+
"version": "0.9.82-rc.303",
|
|
4
4
|
"description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"author": "vilmire",
|
|
47
47
|
"license": "AGPL-3.0-or-later",
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@adhdev/mesh-shared": "0.9.82-rc.
|
|
49
|
+
"@adhdev/mesh-shared": "0.9.82-rc.303",
|
|
50
50
|
"@adhdev/session-host-core": "*",
|
|
51
51
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
52
52
|
"ajv": "^8.20.0",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as os from 'os';
|
|
2
2
|
import { ensureNodePtySpawnHelperPermissions } from './spawn-env.js';
|
|
3
|
+
import { resolveWin32Executable } from './resolve-executable.js';
|
|
3
4
|
|
|
4
5
|
let cachedPty: any | null | undefined;
|
|
5
6
|
|
|
@@ -119,7 +120,7 @@ export class NodePtyTransportFactory implements PtyTransportFactory {
|
|
|
119
120
|
cwd = os.homedir();
|
|
120
121
|
}
|
|
121
122
|
}
|
|
122
|
-
const handle = pty.spawn(command, args, {
|
|
123
|
+
const handle = pty.spawn(resolveWin32Executable(command), args, {
|
|
123
124
|
name: 'xterm-256color',
|
|
124
125
|
cols: options.cols,
|
|
125
126
|
rows: options.rows,
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
|
|
5
|
+
// Extensions ConPTY/CreateProcess can launch directly (not .cmd/.bat, which
|
|
6
|
+
// need a cmd.exe wrapper).
|
|
7
|
+
const DIRECT_EXEC_EXT = new Set(['.exe', '.com']);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolve a launch command to an absolute executable path on Windows.
|
|
11
|
+
*
|
|
12
|
+
* node-pty's ConPTY backend resolves a bare/relative command against the
|
|
13
|
+
* *calling process's* `Path` env var and — critically — does NOT apply PATHEXT.
|
|
14
|
+
* So a provider command like `claude` never matches `claude.exe` and the native
|
|
15
|
+
* layer throws `File not found:` (empty), which crashes the daemon. We resolve
|
|
16
|
+
* it to an absolute `.exe` here (in the daemon process, which has the full PATH)
|
|
17
|
+
* before the command ever reaches node-pty.
|
|
18
|
+
*
|
|
19
|
+
* No-op on non-Windows and when the command is already an existing absolute path
|
|
20
|
+
* or cannot be resolved (caller keeps the original behaviour).
|
|
21
|
+
*/
|
|
22
|
+
export function resolveWin32Executable(command: string): string {
|
|
23
|
+
if (process.platform !== 'win32') return command;
|
|
24
|
+
const trimmed = (command || '').trim();
|
|
25
|
+
if (!trimmed) return command;
|
|
26
|
+
|
|
27
|
+
// Already an absolute path that exists — keep it.
|
|
28
|
+
if (path.isAbsolute(trimmed) && existsSync(trimmed)) return trimmed;
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const out = execFileSync('where', [trimmed], {
|
|
32
|
+
encoding: 'utf8',
|
|
33
|
+
windowsHide: true,
|
|
34
|
+
}).trim();
|
|
35
|
+
if (out) {
|
|
36
|
+
const matches = out.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
37
|
+
// Prefer a directly-launchable executable (.exe/.com) over .cmd/.bat shims.
|
|
38
|
+
const direct = matches.find((m) => DIRECT_EXEC_EXT.has(path.extname(m).toLowerCase()));
|
|
39
|
+
return direct || matches[0] || command;
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
// `where` not found / non-zero exit — fall through to original command.
|
|
43
|
+
}
|
|
44
|
+
return command;
|
|
45
|
+
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
type SessionHostRecord,
|
|
7
7
|
} from '@adhdev/session-host-core';
|
|
8
8
|
import { LOG } from '../logging/logger.js';
|
|
9
|
+
import { resolveWin32Executable } from './resolve-executable.js';
|
|
9
10
|
import type { PtyRuntimeMetadata, PtyRuntimeTransport, PtySpawnOptions, PtyTransportFactory } from './pty-transport.js';
|
|
10
11
|
|
|
11
12
|
interface SessionHostPtyTransportFactoryOptions {
|
|
@@ -435,7 +436,7 @@ export class SessionHostPtyTransportFactory implements PtyTransportFactory {
|
|
|
435
436
|
spawn(command: string, args: string[], spawnOptions: PtySpawnOptions): PtyRuntimeTransport {
|
|
436
437
|
return new SessionHostRuntimeTransport({
|
|
437
438
|
...this.options,
|
|
438
|
-
command,
|
|
439
|
+
command: resolveWin32Executable(command),
|
|
439
440
|
args,
|
|
440
441
|
spawnOptions,
|
|
441
442
|
});
|
|
@@ -169,6 +169,34 @@ export function buildPinnedGlobalInstallCommand(options: {
|
|
|
169
169
|
};
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Build an env for the `npm install` child whose PATH is prefixed with the
|
|
174
|
+
* directory of the node binary currently running this helper.
|
|
175
|
+
*
|
|
176
|
+
* npm runs lifecycle scripts (e.g. adhdev's `preinstall` Node-version guard) by
|
|
177
|
+
* spawning a bare `node`, which resolves from PATH — NOT from the node that runs
|
|
178
|
+
* npm. On Windows a machine can have several node installs (e.g. a standalone
|
|
179
|
+
* `C:\Program Files\nodejs` ahead of an nvm-managed node on PATH). Without this,
|
|
180
|
+
* the guard sees the wrong (unsupported) node version and aborts the upgrade,
|
|
181
|
+
* even though npm/adhdev actually run under a supported node. Pinning the
|
|
182
|
+
* running node's dir to the front of PATH makes lifecycle scripts use the same
|
|
183
|
+
* node as the install itself.
|
|
184
|
+
*/
|
|
185
|
+
function buildInstallEnvWithNodeOnPath(baseEnv: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
|
|
186
|
+
// The Node-version guard this works around only fires on Windows, so scope the
|
|
187
|
+
// PATH rewrite to win32 — POSIX keeps its env untouched.
|
|
188
|
+
if (process.platform !== 'win32') return { ...baseEnv };
|
|
189
|
+
const nodeBinDir = path.dirname(process.execPath);
|
|
190
|
+
if (!nodeBinDir) return { ...baseEnv };
|
|
191
|
+
const env: NodeJS.ProcessEnv = { ...baseEnv };
|
|
192
|
+
// Windows env keys are case-insensitive and conventionally spelled `Path`;
|
|
193
|
+
// prepend to the existing key (whatever its case) to avoid creating a dupe.
|
|
194
|
+
const pathKey = Object.keys(env).find((k) => k.toLowerCase() === 'path') || 'PATH';
|
|
195
|
+
const current = env[pathKey] || '';
|
|
196
|
+
env[pathKey] = current ? `${nodeBinDir};${current}` : nodeBinDir;
|
|
197
|
+
return env;
|
|
198
|
+
}
|
|
199
|
+
|
|
172
200
|
export function getNpmExecOptions(platform: NodeJS.Platform = process.platform): NpmExecOptions {
|
|
173
201
|
if (platform === 'win32') {
|
|
174
202
|
return { shell: false, windowsHide: true };
|
|
@@ -375,6 +403,7 @@ async function runDaemonUpgradeHelper(payload: DaemonUpgradeHelperPayload): Prom
|
|
|
375
403
|
encoding: 'utf8',
|
|
376
404
|
stdio: 'pipe',
|
|
377
405
|
maxBuffer: 20 * 1024 * 1024,
|
|
406
|
+
env: buildInstallEnvWithNodeOnPath(),
|
|
378
407
|
...installCommand.execOptions,
|
|
379
408
|
},
|
|
380
409
|
);
|
package/src/git/git-executor.ts
CHANGED
|
@@ -10,6 +10,18 @@ const execFileAsync = promisify(execFile);
|
|
|
10
10
|
const DEFAULT_TIMEOUT_MS = 5_000;
|
|
11
11
|
const DEFAULT_MAX_BUFFER = 1024 * 1024;
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Timeout for status-collection git commands (status/log/submodule/stash/fetch).
|
|
15
|
+
* The default 5s is fine for porcelain status, but on Windows the `git` subprocess
|
|
16
|
+
* spawn itself is pathologically slow (measured: `submodule status` ~3.5s, cold
|
|
17
|
+
* `log -1` ~4.2s) and overlaps with refreshUpstream fetches — a single command
|
|
18
|
+
* routinely exceeds 5s, which previously collapsed the whole status to all-null and
|
|
19
|
+
* dropped the node from the mesh graph. Give the collection path a much larger
|
|
20
|
+
* budget so a slow-but-healthy repo never reads as "not a git repo". Windows gets a
|
|
21
|
+
* larger budget than POSIX because the spawn cost is OS-specific, not repo-specific.
|
|
22
|
+
*/
|
|
23
|
+
export const GIT_STATUS_TIMEOUT_MS = process.platform === 'win32' ? 30_000 : 20_000;
|
|
24
|
+
|
|
13
25
|
export interface GitExecutorOptions {
|
|
14
26
|
timeoutMs?: number;
|
|
15
27
|
maxBuffer?: number;
|
package/src/git/git-status.ts
CHANGED
|
@@ -1,9 +1,31 @@
|
|
|
1
1
|
import type { DaemonBuildBehind, GitRepoStatus, GitSubmoduleStatus, GitUpstreamFreshness } from './git-types.js';
|
|
2
|
-
import { GitCommandError, resolveGitRepository, runGit } from './git-executor.js';
|
|
2
|
+
import { GIT_STATUS_TIMEOUT_MS, GitCommandError, resolveGitRepository, runGit } from './git-executor.js';
|
|
3
3
|
import { getDaemonBuildInfo, type DaemonBuildInfo } from '../build-info.js';
|
|
4
4
|
|
|
5
5
|
type ResolvedGitRepo = { workspace: string; repoRoot: string | null; isGitRepo: boolean };
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Last successfully-collected status per workspace, used to survive a transient git
|
|
9
|
+
* failure (timeout, slow Windows spawn under load, a momentary lock) WITHOUT dropping
|
|
10
|
+
* the node out of the mesh graph. A genuine "not a git repository" answer is NOT a
|
|
11
|
+
* transient failure — it never populates this cache and always reports isGitRepo:false.
|
|
12
|
+
*/
|
|
13
|
+
const lastKnownGoodStatus = new Map<string, GitRepoStatus>();
|
|
14
|
+
|
|
15
|
+
/** Test seam: clear the last-known-good status cache between cases. */
|
|
16
|
+
export function __resetGitStatusCacheForTests(): void {
|
|
17
|
+
lastKnownGoodStatus.clear();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* git failure reasons that are transient/environmental rather than a real statement
|
|
22
|
+
* that the workspace is not a repo. On these we prefer the last-known-good status so a
|
|
23
|
+
* single slow git call cannot make a healthy node vanish from the graph.
|
|
24
|
+
*/
|
|
25
|
+
function isTransientGitFailure(error: GitCommandError): boolean {
|
|
26
|
+
return error.reason === 'timeout' || error.reason === 'git_command_failed';
|
|
27
|
+
}
|
|
28
|
+
|
|
7
29
|
export interface GitStatusOptions {
|
|
8
30
|
timeoutMs?: number;
|
|
9
31
|
/** When true, include submodule status in the result. Defaults to true. */
|
|
@@ -35,9 +57,50 @@ export async function getGitRepoStatus(
|
|
|
35
57
|
): Promise<GitRepoStatus> {
|
|
36
58
|
const lastCheckedAt = Date.now();
|
|
37
59
|
const includeSubmodules = options.includeSubmodules !== false;
|
|
60
|
+
// Status collection fans out into several git subprocesses (status, head, stash,
|
|
61
|
+
// submodule, optionally fetch). On Windows the per-spawn cost alone can exceed the
|
|
62
|
+
// 5s default, so unless the caller pinned a timeout, give the whole collection path
|
|
63
|
+
// the larger status budget. A caller that explicitly sets timeoutMs (e.g. a test
|
|
64
|
+
// injecting 1ms to exercise the transient-failure path) is respected.
|
|
65
|
+
const effectiveOptions: GitStatusOptions =
|
|
66
|
+
options.timeoutMs === undefined ? { ...options, timeoutMs: GIT_STATUS_TIMEOUT_MS } : options;
|
|
38
67
|
|
|
39
68
|
try {
|
|
40
|
-
const repo = await resolveGitRepository(workspace,
|
|
69
|
+
const repo = await resolveGitRepository(workspace, effectiveOptions);
|
|
70
|
+
const status = await collectGitRepoStatus(repo, includeSubmodules, lastCheckedAt, effectiveOptions);
|
|
71
|
+
lastKnownGoodStatus.set(workspace, status);
|
|
72
|
+
return status;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
const gitError = error instanceof GitCommandError
|
|
75
|
+
? error
|
|
76
|
+
: new GitCommandError('git_command_failed', 'Failed to read Git status', { cause: error });
|
|
77
|
+
|
|
78
|
+
// A transient/environmental failure (timeout, slow-spawn-under-load) must NOT make
|
|
79
|
+
// a healthy node lose its repo identity and drop out of the mesh graph. Prefer the
|
|
80
|
+
// last status we successfully collected for this workspace, re-stamped as stale.
|
|
81
|
+
if (isTransientGitFailure(gitError)) {
|
|
82
|
+
const cached = lastKnownGoodStatus.get(workspace);
|
|
83
|
+
if (cached) {
|
|
84
|
+
return {
|
|
85
|
+
...cached,
|
|
86
|
+
lastCheckedAt,
|
|
87
|
+
upstreamStatus: 'unavailable',
|
|
88
|
+
error: gitError.stderr || gitError.message,
|
|
89
|
+
reason: gitError.reason,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return emptyStatus(workspace, lastCheckedAt, gitError);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function collectGitRepoStatus(
|
|
99
|
+
repo: ResolvedGitRepo,
|
|
100
|
+
includeSubmodules: boolean,
|
|
101
|
+
lastCheckedAt: number,
|
|
102
|
+
options: GitStatusOptions,
|
|
103
|
+
): Promise<GitRepoStatus> {
|
|
41
104
|
let parsed = await readPorcelainStatus(repo, options);
|
|
42
105
|
let upstreamProbe: GitUpstreamProbe = getInitialUpstreamProbe(parsed);
|
|
43
106
|
|
|
@@ -89,16 +152,6 @@ export async function getGitRepoStatus(
|
|
|
89
152
|
submodules,
|
|
90
153
|
...(daemonBuildBehind ? { daemonBuildBehind } : {}),
|
|
91
154
|
};
|
|
92
|
-
} catch (error) {
|
|
93
|
-
if (error instanceof GitCommandError) {
|
|
94
|
-
return emptyStatus(workspace, lastCheckedAt, error);
|
|
95
|
-
}
|
|
96
|
-
return emptyStatus(
|
|
97
|
-
workspace,
|
|
98
|
-
lastCheckedAt,
|
|
99
|
-
new GitCommandError('git_command_failed', 'Failed to read Git status', { cause: error }),
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
155
|
}
|
|
103
156
|
|
|
104
157
|
/**
|
|
@@ -507,7 +560,12 @@ async function getSubmoduleStatuses(
|
|
|
507
560
|
if (!repo.repoRoot) return [];
|
|
508
561
|
|
|
509
562
|
try {
|
|
510
|
-
|
|
563
|
+
// No `--recursive`: this superproject's submodules (oss, adhdev-providers) are
|
|
564
|
+
// leaf repos with no nested submodules, so `--recursive` doubles the (already
|
|
565
|
+
// slow on Windows) submodule-status spawn time for zero additional rows. If a
|
|
566
|
+
// nested submodule is ever introduced, restore --recursive WITH its own longer
|
|
567
|
+
// per-command timeout rather than reverting this wholesale.
|
|
568
|
+
const result = await runGit(repo, ['submodule', 'status'], options);
|
|
511
569
|
const submodules = parseSubmoduleStatusOutput(result.stdout, repo.repoRoot, options.submoduleIgnorePaths);
|
|
512
570
|
await Promise.all(submodules.map(submodule => enrichSubmoduleWorktreeStatus(repo, submodule, options)));
|
|
513
571
|
return submodules;
|