@adhdev/daemon-core 0.9.82-rc.9 → 0.9.82-rc.91

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.
Files changed (67) hide show
  1. package/dist/cli-adapters/provider-cli-adapter.d.ts +2 -0
  2. package/dist/cli-adapters/provider-cli-parse.d.ts +1 -0
  3. package/dist/cli-adapters/provider-cli-shared.d.ts +2 -0
  4. package/dist/commands/router.d.ts +22 -0
  5. package/dist/config/mesh-config.d.ts +66 -1
  6. package/dist/index.d.ts +13 -6
  7. package/dist/index.js +5417 -1207
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +5381 -1193
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/installer.d.ts +1 -4
  12. package/dist/launch.d.ts +1 -1
  13. package/dist/logging/async-batch-writer.d.ts +10 -0
  14. package/dist/mesh/beads-db.d.ts +18 -0
  15. package/dist/mesh/mesh-active-work.d.ts +60 -0
  16. package/dist/mesh/mesh-events.d.ts +29 -5
  17. package/dist/mesh/mesh-fast-forward.d.ts +39 -0
  18. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  19. package/dist/mesh/mesh-ledger.d.ts +38 -1
  20. package/dist/mesh/mesh-work-queue.d.ts +27 -5
  21. package/dist/mesh/refine-config.d.ts +176 -0
  22. package/dist/providers/chat-message-normalization.d.ts +1 -0
  23. package/dist/providers/cli-provider-instance.d.ts +2 -1
  24. package/dist/repo-mesh-types.d.ts +46 -0
  25. package/dist/status/reporter.d.ts +2 -0
  26. package/package.json +3 -1
  27. package/src/boot/daemon-lifecycle.ts +1 -0
  28. package/src/cli-adapters/provider-cli-adapter.ts +91 -3
  29. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  30. package/src/cli-adapters/provider-cli-parse.ts +4 -0
  31. package/src/cli-adapters/provider-cli-runtime.ts +3 -1
  32. package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
  33. package/src/cli-adapters/provider-cli-shared.ts +20 -10
  34. package/src/commands/chat-commands.ts +472 -15
  35. package/src/commands/cli-manager.ts +126 -0
  36. package/src/commands/handler.ts +8 -1
  37. package/src/commands/mesh-coordinator.ts +13 -143
  38. package/src/commands/router.ts +2687 -435
  39. package/src/config/chat-history.ts +9 -7
  40. package/src/config/mesh-config.ts +245 -1
  41. package/src/daemon/dev-cli-debug.ts +10 -1
  42. package/src/detection/ide-detector.ts +26 -16
  43. package/src/index.ts +31 -5
  44. package/src/installer.d.ts +1 -1
  45. package/src/installer.ts +8 -6
  46. package/src/launch.d.ts +1 -1
  47. package/src/launch.ts +37 -28
  48. package/src/logging/async-batch-writer.ts +55 -0
  49. package/src/logging/logger.ts +2 -1
  50. package/src/mesh/beads-db.ts +176 -0
  51. package/src/mesh/coordinator-prompt.ts +30 -7
  52. package/src/mesh/mesh-active-work.ts +255 -0
  53. package/src/mesh/mesh-events.ts +400 -47
  54. package/src/mesh/mesh-fast-forward.ts +430 -0
  55. package/src/mesh/mesh-host-ownership.ts +73 -0
  56. package/src/mesh/mesh-ledger.ts +138 -1
  57. package/src/mesh/mesh-work-queue.ts +199 -137
  58. package/src/mesh/refine-config.ts +356 -0
  59. package/src/providers/chat-message-normalization.ts +7 -12
  60. package/src/providers/cli-provider-instance.ts +93 -14
  61. package/src/providers/ide-provider-instance.ts +17 -3
  62. package/src/providers/provider-loader.ts +10 -4
  63. package/src/providers/read-chat-contract.ts +1 -1
  64. package/src/providers/version-archive.ts +38 -20
  65. package/src/repo-mesh-types.ts +51 -0
  66. package/src/status/reporter.ts +15 -0
  67. package/src/system/host-memory.ts +29 -12
@@ -19,10 +19,37 @@ export interface RepoMesh {
19
19
  defaultBranch?: string;
20
20
  policy: RepoMeshPolicy;
21
21
  coordinator: RepoMeshCoordinatorConfig;
22
+ meshHost?: RepoMeshHostMetadata;
22
23
  projectContext: ProjectContextSnapshot;
23
24
  nodes: RepoMeshNode[];
24
25
  status: 'active' | 'archived' | 'deleted';
25
26
  }
27
+ export type RepoMeshDaemonRole = 'host' | 'member';
28
+ export interface RepoMeshHostPairingMetadata {
29
+ status: 'not_configured' | 'pairing' | 'paired' | 'rejected' | 'revoked';
30
+ tokenId?: string;
31
+ joinedAt?: string;
32
+ lastPairedAt?: string;
33
+ lastRejectedAt?: string;
34
+ expiresAt?: string;
35
+ }
36
+ export interface RepoMeshHostMetadata {
37
+ /** Local daemon role for this mesh. Missing metadata defaults to host for standalone compatibility. */
38
+ role: RepoMeshDaemonRole;
39
+ /** Daemon that owns mesh truth/status/git/queue/session/ledger/coordinator ownership. */
40
+ hostDaemonId?: string;
41
+ /** Mesh node that represents the host daemon, when known. */
42
+ hostNodeId?: string;
43
+ /** Future standalone manual pairing endpoint entered by member daemons. */
44
+ hostAddress?: string;
45
+ /** Redacted pairing state only; raw join tokens must not be persisted here. */
46
+ pairing?: RepoMeshHostPairingMetadata;
47
+ }
48
+ export interface RepoMeshHostStatus extends RepoMeshHostMetadata {
49
+ canOwnCoordinator: boolean;
50
+ canOwnQueue: boolean;
51
+ defaulted: boolean;
52
+ }
26
53
  export interface RepoMeshNode {
27
54
  id: string;
28
55
  daemonId: string;
@@ -37,6 +64,7 @@ export interface RepoMeshNode {
37
64
  effectiveCapabilities: RepoMeshNodeCapabilities;
38
65
  policy: RepoMeshNodePolicy;
39
66
  health: RepoMeshNodeHealth;
67
+ role?: RepoMeshDaemonRole;
40
68
  status: 'enabled' | 'disabled' | 'removed';
41
69
  }
42
70
  export type RepoMeshNodeHealth = 'online' | 'offline' | 'degraded' | 'dirty' | 'wrong_branch' | 'unknown';
@@ -46,6 +74,13 @@ export interface RepoMeshPolicy {
46
74
  requirePreTaskCheckpoint: boolean;
47
75
  requirePostTaskCheckpoint: boolean;
48
76
  requireApprovalForPush: boolean;
77
+ /**
78
+ * Narrow Refinery opt-in: when validation and patch-equivalence have passed,
79
+ * allow Refinery to publish submodule gitlink commits to each submodule's
80
+ * configured remote main branch with a non-force push, then verify reachability.
81
+ * Defaults to false; root branch pushes/merges are not affected.
82
+ */
83
+ allowAutoPublishSubmoduleMainCommits?: boolean;
49
84
  requireApprovalForDestructiveGit: boolean;
50
85
  dirtyWorkspaceBehavior: 'block' | 'warn' | 'checkpoint_then_continue';
51
86
  maxParallelTasks: number;
@@ -185,6 +220,7 @@ export interface LocalMeshEntry {
185
220
  defaultBranch?: string;
186
221
  policy: RepoMeshPolicy;
187
222
  coordinator: RepoMeshCoordinatorConfig;
223
+ meshHost?: RepoMeshHostMetadata;
188
224
  nodes: LocalMeshNodeEntry[];
189
225
  createdAt: string;
190
226
  updatedAt: string;
@@ -206,12 +242,15 @@ export interface LocalMeshNodeEntry {
206
242
  clonedFromNodeId?: string;
207
243
  /** Optional associated/external repos configured as node metadata. */
208
244
  relatedRepos?: RepoMeshRelatedRepo[];
245
+ role?: RepoMeshDaemonRole;
209
246
  }
210
247
  export interface RepoMeshStatus {
211
248
  meshId: string;
212
249
  meshName: string;
213
250
  repoIdentity: string;
251
+ defaultBranch?: string;
214
252
  refreshedAt: string;
253
+ meshHost?: RepoMeshHostStatus;
215
254
  nodes: RepoMeshNodeStatus[];
216
255
  queue?: RepoMeshQueueStatus;
217
256
  ledger?: RepoMeshLedgerStatus;
@@ -248,11 +287,18 @@ export interface RepoMeshNodeStatus {
248
287
  repoRoot?: string;
249
288
  daemonId?: string;
250
289
  machineId?: string;
290
+ role?: RepoMeshDaemonRole;
251
291
  machineStatus?: string;
252
292
  isLocalWorktree?: boolean;
253
293
  worktreeBranch?: string;
254
294
  health: RepoMeshNodeHealth;
255
295
  git?: GitRepoStatus;
296
+ /**
297
+ * True when the selected coordinator has evidence that a peer git probe is still
298
+ * in flight or just timed out during initial mesh handshake, so callers should
299
+ * treat missing git data as pending instead of authoritative absence.
300
+ */
301
+ gitProbePending?: boolean;
256
302
  providers: string[];
257
303
  activeSessions: string[];
258
304
  activeSessionDetails?: RepoMeshSessionStatus[];
@@ -46,6 +46,8 @@ export declare class DaemonStatusReporter {
46
46
  private lastStatusSentAt;
47
47
  private statusPendingThrottle;
48
48
  private lastP2PStatusHash;
49
+ private lastP2PStatusSentAt;
50
+ private p2pDebounceTimer;
49
51
  private lastServerStatusHash;
50
52
  private lastStatusSummary;
51
53
  private statusTimer;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.9",
3
+ "version": "0.9.82-rc.91",
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",
@@ -49,6 +49,7 @@
49
49
  "@adhdev/session-host-core": "*",
50
50
  "@agentclientprotocol/sdk": "^0.16.1",
51
51
  "@xterm/xterm": "^6.0.0",
52
+ "better-sqlite3": "^12.10.0",
52
53
  "chalk": "^5.3.0",
53
54
  "chokidar": "^4.0.3",
54
55
  "conf": "^13.0.0",
@@ -60,6 +61,7 @@
60
61
  "@adhdev/ghostty-vt-node": "*"
61
62
  },
62
63
  "devDependencies": {
64
+ "@types/better-sqlite3": "^7.6.13",
63
65
  "@types/js-yaml": "^4.0.9",
64
66
  "@types/node": "^22.0.0",
65
67
  "@types/ws": "^8.18.1",
@@ -309,6 +309,7 @@ export async function initDaemonComponents(config: DaemonInitConfig): Promise<Da
309
309
  statusInstanceId: config.statusInstanceId,
310
310
  statusVersion: config.statusVersion,
311
311
  getMeshPeerConnectionStatus: config.getMeshPeerConnectionStatus,
312
+ dispatchMeshCommand: config.dispatchMeshCommand,
312
313
  getCdpLogFn: config.getCdpLogFn || ((ideType: string) => LOG.forComponent(`CDP:${ideType}`).asLogFn()),
313
314
  });
314
315
 
@@ -761,6 +761,17 @@ export class ProviderCliAdapter implements CliAdapter {
761
761
  if (stableMs < 2000) return;
762
762
 
763
763
  const startupModal = this.runParseApproval(this.recentOutputBuffer);
764
+ const startupStatus = this.runDetectStatus(screenText || this.recentOutputBuffer);
765
+ if (!startupModal && startupStatus !== 'idle') {
766
+ this.recordTrace('startup_settle_deferred', {
767
+ trigger,
768
+ startupStatus,
769
+ stableMs,
770
+ screenText: summarizeCliTraceText(screenText, 500),
771
+ });
772
+ this.scheduleStartupSettleCheck();
773
+ return;
774
+ }
764
775
  this.startupParseGate = false;
765
776
  if (this.startupSettleTimer) {
766
777
  clearTimeout(this.startupSettleTimer);
@@ -956,6 +967,38 @@ export class ProviderCliAdapter implements CliAdapter {
956
967
  return true;
957
968
  }
958
969
 
970
+ private clearParsedIdleResponseGuard(reason: string, parsedStatus: any): boolean {
971
+ const parsedRawStatus = typeof parsedStatus?.status === 'string' ? parsedStatus.status.trim() : '';
972
+ const parsedModal = parsedStatus?.activeModal ?? parsedStatus?.modal ?? null;
973
+ const blockingModal = this.activeModal || this.runParseApproval(this.recentOutputBuffer);
974
+ if (
975
+ !this.isWaitingForResponse
976
+ || parsedRawStatus !== 'idle'
977
+ || !!parsedModal
978
+ || !!blockingModal
979
+ || !this.parsedStatusHasFinalAssistantMessage(parsedStatus)
980
+ ) {
981
+ return false;
982
+ }
983
+ this.clearAllTimers();
984
+ this.clearIdleFinishCandidate(reason);
985
+ this.responseBuffer = '';
986
+ this.isWaitingForResponse = false;
987
+ this.responseSettleIgnoreUntil = 0;
988
+ this.submitRetryUsed = false;
989
+ this.submitRetryPromptSnippet = '';
990
+ this.finishRetryCount = 0;
991
+ this.currentTurnScope = null;
992
+ this.activeModal = null;
993
+ this.setStatus('idle', reason);
994
+ this.recordTrace('parsed_idle_response_cleared', {
995
+ reason,
996
+ parsedStatus: parsedRawStatus,
997
+ parsedMessageCount: Array.isArray(parsedStatus?.messages) ? parsedStatus.messages.length : 0,
998
+ });
999
+ return true;
1000
+ }
1001
+
959
1002
  private hasMeaningfulResponseBuffer(promptSnippet: string): boolean {
960
1003
  const raw = String(this.responseBuffer || '').trim();
961
1004
  if (!raw) return false;
@@ -1489,6 +1532,7 @@ export class ProviderCliAdapter implements CliAdapter {
1489
1532
  accumulatedRawBuffer: this.accumulatedRawBuffer,
1490
1533
  recentOutputBuffer: this.recentOutputBuffer,
1491
1534
  terminalScreenText: parseScreenText,
1535
+ workingDir: this.workingDir,
1492
1536
  baseMessages: [],
1493
1537
  partialResponse: this.responseBuffer,
1494
1538
  isWaitingForResponse: this.isWaitingForResponse,
@@ -1552,6 +1596,15 @@ export class ProviderCliAdapter implements CliAdapter {
1552
1596
  return !!(startupModal || this.activeModal);
1553
1597
  }
1554
1598
 
1599
+ private parsedStatusHasFinalAssistantMessage(parsed: any): boolean {
1600
+ const messages = Array.isArray(parsed?.messages) ? parsed.messages : [];
1601
+ const lastAssistant = [...messages].reverse().find((message: any) => {
1602
+ if (!message || message.role !== 'assistant') return false;
1603
+ return typeof message.content === 'string' && message.content.trim().length > 0;
1604
+ });
1605
+ return !!lastAssistant;
1606
+ }
1607
+
1555
1608
  private projectEffectiveStatus(startupModal: { message: string; buttons: string[] } | null = null): CliSessionStatus['status'] {
1556
1609
  if (this.parseErrorMessage) return 'error';
1557
1610
  if (this.hasActionableApproval(startupModal)) return 'waiting_approval';
@@ -1564,8 +1617,14 @@ export class ProviderCliAdapter implements CliAdapter {
1564
1617
  getStatus(options: { allowParse?: boolean } = {}): CliSessionStatus {
1565
1618
  const allowParse = options.allowParse !== false;
1566
1619
  const startupModal = allowParse && this.startupParseGate ? this.runParseApproval(this.recentOutputBuffer) : null;
1620
+ const startupDetectedStatus = allowParse && this.startupParseGate && !startupModal
1621
+ ? this.runDetectStatus(this.recentOutputBuffer || this.terminalScreen.getText())
1622
+ : null;
1567
1623
  let effectiveStatus = this.projectEffectiveStatus(startupModal);
1568
1624
  let effectiveModal = startupModal || this.activeModal;
1625
+ if (startupDetectedStatus === 'waiting_approval') {
1626
+ effectiveStatus = 'waiting_approval';
1627
+ }
1569
1628
  if (allowParse && !startupModal && !effectiveModal) {
1570
1629
  const parsed = this.getFreshParsedStatusCache();
1571
1630
  const parsedModal = parsed?.activeModal && Array.isArray(parsed.activeModal.buttons)
@@ -1575,6 +1634,18 @@ export class ProviderCliAdapter implements CliAdapter {
1575
1634
  if (parsed?.status === 'waiting_approval' && parsedModal) {
1576
1635
  effectiveStatus = 'waiting_approval';
1577
1636
  effectiveModal = parsedModal;
1637
+ } else if (
1638
+ effectiveStatus === 'idle'
1639
+ && parsed?.status === 'generating'
1640
+ && !this.parsedStatusHasFinalAssistantMessage(parsed)
1641
+ ) {
1642
+ effectiveStatus = 'generating';
1643
+ } else if (
1644
+ effectiveStatus === 'generating'
1645
+ && parsed?.status === 'idle'
1646
+ && this.parsedStatusHasFinalAssistantMessage(parsed)
1647
+ ) {
1648
+ effectiveStatus = 'idle';
1578
1649
  }
1579
1650
  }
1580
1651
  const bufferState = this.getBufferState();
@@ -1665,6 +1736,7 @@ export class ProviderCliAdapter implements CliAdapter {
1665
1736
  accumulatedRawBuffer: this.accumulatedRawBuffer,
1666
1737
  recentOutputBuffer: this.recentOutputBuffer,
1667
1738
  terminalScreenText: this.getParseScreenText(this.terminalScreen.getText()),
1739
+ workingDir: this.workingDir,
1668
1740
  baseMessages: [],
1669
1741
  partialResponse: this.responseBuffer,
1670
1742
  isWaitingForResponse: this.isWaitingForResponse,
@@ -1979,7 +2051,10 @@ export class ProviderCliAdapter implements CliAdapter {
1979
2051
  }
1980
2052
  }
1981
2053
  if (this.isWaitingForResponse && !allowInputDuringGeneration) {
1982
- if (!this.clearStaleIdleResponseGuard('send_message_guard')) {
2054
+ if (
2055
+ !this.clearStaleIdleResponseGuard('send_message_guard')
2056
+ && !this.clearParsedIdleResponseGuard('send_message_parsed_idle_guard', parsedStatusBeforeSend)
2057
+ ) {
1983
2058
  throw new Error(`${this.cliName} is still processing the previous prompt`);
1984
2059
  }
1985
2060
  }
@@ -2369,10 +2444,23 @@ export class ProviderCliAdapter implements CliAdapter {
2369
2444
  getDebugState(): Record<string, any> {
2370
2445
  const screenText = sanitizeTerminalText(this.terminalScreen.getText());
2371
2446
  const startupModal = this.startupParseGate ? this.runParseApproval(this.recentOutputBuffer) : null;
2372
- const effectiveStatus = this.projectEffectiveStatus(startupModal);
2373
- const effectiveReady = this.ready || !!startupModal;
2447
+ const startupDetectedStatus = this.startupParseGate && !startupModal
2448
+ ? this.runDetectStatus(this.recentOutputBuffer || screenText)
2449
+ : null;
2450
+ const effectiveReady = this.ready || !!startupModal || startupDetectedStatus === 'waiting_approval';
2374
2451
  const parsedDebugState = this.getParsedDebugState();
2375
2452
  const parsedMessages = Array.isArray(parsedDebugState?.messages) ? parsedDebugState.messages : [];
2453
+ let effectiveStatus = this.projectEffectiveStatus(startupModal);
2454
+ if (startupDetectedStatus === 'waiting_approval') {
2455
+ effectiveStatus = 'waiting_approval';
2456
+ }
2457
+ if (
2458
+ effectiveStatus === 'idle'
2459
+ && parsedDebugState?.status === 'generating'
2460
+ && !this.parsedStatusHasFinalAssistantMessage(parsedDebugState)
2461
+ ) {
2462
+ effectiveStatus = 'generating';
2463
+ }
2376
2464
  return {
2377
2465
  type: this.cliType,
2378
2466
  name: this.cliName,
@@ -15,6 +15,7 @@ export declare function buildCliParseInput(options: {
15
15
  accumulatedRawBuffer: string;
16
16
  recentOutputBuffer: string;
17
17
  terminalScreenText: string;
18
+ workingDir?: string;
18
19
  baseMessages: CliChatMessage[];
19
20
  partialResponse: string;
20
21
  isWaitingForResponse?: boolean;
@@ -35,6 +35,7 @@ export function buildCliParseInput(options: {
35
35
  accumulatedRawBuffer: string;
36
36
  recentOutputBuffer: string;
37
37
  terminalScreenText: string;
38
+ workingDir?: string;
38
39
  baseMessages: CliChatMessage[];
39
40
  partialResponse: string;
40
41
  isWaitingForResponse?: boolean;
@@ -46,6 +47,7 @@ export function buildCliParseInput(options: {
46
47
  accumulatedRawBuffer,
47
48
  recentOutputBuffer,
48
49
  terminalScreenText,
50
+ workingDir,
49
51
  baseMessages,
50
52
  partialResponse,
51
53
  isWaitingForResponse,
@@ -66,6 +68,8 @@ export function buildCliParseInput(options: {
66
68
  rawBuffer,
67
69
  recentBuffer,
68
70
  screenText,
71
+ workspace: workingDir,
72
+ workingDir,
69
73
  screen: buildCliScreenSnapshot(screenText),
70
74
  bufferScreen: buildCliScreenSnapshot(buffer),
71
75
  recentScreen: buildCliScreenSnapshot(recentBuffer),
@@ -36,7 +36,9 @@ export function resolveCliSpawnPlan(options: {
36
36
  : spawnConfig.command;
37
37
  const binaryPath = findBinary(configuredCommand);
38
38
  const isWin = os.platform() === 'win32';
39
- const allArgs = [...spawnConfig.args, ...extraArgs];
39
+ const allArgs = [...spawnConfig.args, ...extraArgs].map((arg) =>
40
+ typeof arg === 'string' ? arg.replace(/\{\{workingDir\}\}/g, workingDir) : arg,
41
+ );
40
42
 
41
43
  let shellCmd: string;
42
44
  let shellArgs: string[];
@@ -55,6 +55,8 @@ export interface CliScriptInput {
55
55
  rawBuffer: string;
56
56
  recentBuffer: string;
57
57
  screenText: string;
58
+ workspace?: string;
59
+ workingDir?: string;
58
60
  screen: CliScreenSnapshot;
59
61
  bufferScreen: CliScreenSnapshot;
60
62
  recentScreen: CliScreenSnapshot;
@@ -94,6 +94,8 @@ export interface CliScriptInput {
94
94
  rawBuffer: string;
95
95
  recentBuffer: string;
96
96
  screenText: string;
97
+ workspace?: string;
98
+ workingDir?: string;
97
99
  screen: CliScreenSnapshot;
98
100
  bufferScreen: CliScreenSnapshot;
99
101
  recentScreen: CliScreenSnapshot;
@@ -484,17 +486,25 @@ export function findBinary(name: string): string {
484
486
  return path.isAbsolute(expanded) ? expanded : path.resolve(expanded);
485
487
  }
486
488
  const isWin = os.platform() === 'win32';
487
- try {
488
- const cmd = isWin ? `where ${trimmed}` : `which ${trimmed}`;
489
- return execSync(cmd, {
490
- encoding: 'utf-8',
491
- timeout: 5000,
492
- stdio: ['pipe', 'pipe', 'pipe'],
493
- ...(isWin ? { windowsHide: true } : {}),
494
- }).trim().split('\n')[0].trim();
495
- } catch {
496
- return isWin ? `${trimmed}.cmd` : trimmed;
489
+ const paths = (process.env.PATH || '').split(path.delimiter);
490
+ const exes = isWin ? ['.exe', '.cmd', '.bat', ''] : [''];
491
+
492
+ for (const p of paths) {
493
+ if (!p) continue;
494
+ for (const ext of exes) {
495
+ const fullPath = path.join(p, trimmed + ext);
496
+ try {
497
+ const fs = require('fs');
498
+ if (fs.existsSync(fullPath)) {
499
+ const stat = fs.statSync(fullPath);
500
+ if (stat.isFile() && (isWin || (stat.mode & 0o111))) {
501
+ return fullPath;
502
+ }
503
+ }
504
+ } catch { }
505
+ }
497
506
  }
507
+ return isWin ? `${trimmed}.cmd` : trimmed;
498
508
  }
499
509
 
500
510
  export function isScriptBinary(binaryPath: string): boolean {