@adhdev/daemon-core 0.9.48 → 0.9.49

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/session-host-core",
3
- "version": "0.9.48",
3
+ "version": "0.9.49",
4
4
  "description": "ADHDev local session host core \u2014 session registry, protocol, buffers",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.48",
3
+ "version": "0.9.49",
4
4
  "description": "ADHDev daemon core \u2014 CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -41,6 +41,7 @@ export interface CliAdapter {
41
41
  sendMessage(text: string): Promise<void>;
42
42
  getStatus(): CliAdapterStatus;
43
43
  getScriptParsedStatus?(): unknown;
44
+ getDebugSnapshot?(): unknown;
44
45
  invokeScript?(scriptName: string, args?: Record<string, unknown>): Promise<unknown>;
45
46
  getPartialResponse(): string;
46
47
  saveAndStop?(): Promise<void>;
@@ -2545,6 +2545,88 @@ export class ProviderCliAdapter implements CliAdapter {
2545
2545
  return this.responseBuffer;
2546
2546
  }
2547
2547
 
2548
+ getDebugSnapshot(): Record<string, unknown> {
2549
+ const screenText = this.readTerminalScreenText();
2550
+ const parsedResult = this.parsedStatusCache?.result && typeof this.parsedStatusCache.result === 'object'
2551
+ ? this.parsedStatusCache.result as Record<string, any>
2552
+ : null;
2553
+ return {
2554
+ cliType: this.cliType,
2555
+ cliName: this.cliName,
2556
+ workingDir: this.workingDir,
2557
+ currentStatus: this.currentStatus,
2558
+ ready: this.ready,
2559
+ isWaitingForResponse: this.isWaitingForResponse,
2560
+ activeModal: this.activeModal,
2561
+ parseErrorMessage: this.parseErrorMessage,
2562
+ messageCounts: {
2563
+ committed: this.committedMessages.length,
2564
+ structured: this.structuredMessages.length,
2565
+ visible: this.messages.length,
2566
+ parsedCache: Array.isArray(parsedResult?.messages) ? parsedResult.messages.length : undefined,
2567
+ },
2568
+ buffers: {
2569
+ accumulatedLength: this.accumulatedBuffer.length,
2570
+ accumulatedRawLength: this.accumulatedRawBuffer.length,
2571
+ recentOutputLength: this.recentOutputBuffer.length,
2572
+ responseLength: this.responseBuffer.length,
2573
+ startupLength: this.startupBuffer.length,
2574
+ accumulatedTail: this.accumulatedBuffer.slice(-24_000),
2575
+ accumulatedRawTail: this.accumulatedRawBuffer.slice(-24_000),
2576
+ recentOutputTail: this.recentOutputBuffer.slice(-12_000),
2577
+ responseTail: this.responseBuffer.slice(-12_000),
2578
+ },
2579
+ terminal: {
2580
+ screenText,
2581
+ lastScreenSnapshot: this.lastScreenSnapshot,
2582
+ lastScreenText: this.lastScreenText,
2583
+ lastOutputAt: this.lastOutputAt,
2584
+ lastNonEmptyOutputAt: this.lastNonEmptyOutputAt,
2585
+ lastScreenChangeAt: this.lastScreenChangeAt,
2586
+ lastScreenSnapshotReadAt: this.lastScreenSnapshotReadAt,
2587
+ },
2588
+ parser: {
2589
+ scriptNames: listCliScriptNames(this.cliScripts),
2590
+ traceSessionId: this.traceSessionId,
2591
+ traceSeq: this.traceSeq,
2592
+ currentTurnScope: this.currentTurnScope,
2593
+ parsedStatusCache: parsedResult
2594
+ ? {
2595
+ id: parsedResult.id,
2596
+ status: parsedResult.status,
2597
+ title: parsedResult.title,
2598
+ providerSessionId: parsedResult.providerSessionId,
2599
+ transcriptAuthority: parsedResult.transcriptAuthority,
2600
+ coverage: parsedResult.coverage,
2601
+ messageCount: Array.isArray(parsedResult.messages) ? parsedResult.messages.length : undefined,
2602
+ activeModal: parsedResult.activeModal,
2603
+ }
2604
+ : null,
2605
+ pendingScriptStatus: this.pendingScriptStatus,
2606
+ pendingScriptStatusSince: this.pendingScriptStatusSince,
2607
+ },
2608
+ runtimeMetadata: this.getRuntimeMetadata(),
2609
+ statusHistory: this.statusHistory.slice(-80),
2610
+ traceEntries: this.traceEntries.slice(-120),
2611
+ timing: {
2612
+ spawnAt: this.spawnAt,
2613
+ startupFirstOutputAt: this.startupFirstOutputAt,
2614
+ submitPendingUntil: this.submitPendingUntil,
2615
+ responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
2616
+ responseEpoch: this.responseEpoch,
2617
+ resizeSuppressUntil: this.resizeSuppressUntil,
2618
+ lastApprovalResolvedAt: this.lastApprovalResolvedAt,
2619
+ committedMessagesChangedAt: this.committedMessagesChangedAt,
2620
+ },
2621
+ finish: {
2622
+ idleFinishCandidate: this.idleFinishCandidate,
2623
+ finishRetryCount: this.finishRetryCount,
2624
+ submitRetryUsed: this.submitRetryUsed,
2625
+ submitRetryPromptSnippet: this.submitRetryPromptSnippet,
2626
+ },
2627
+ };
2628
+ }
2629
+
2548
2630
  getRuntimeMetadata(): PtyRuntimeMetadata | null {
2549
2631
  if (!this.ptyProcess || typeof this.ptyProcess.getMetadata !== 'function') return null;
2550
2632
  return this.ptyProcess.getMetadata();
@@ -277,7 +277,12 @@ export function findBinary(name: string): string {
277
277
  const isWin = os.platform() === 'win32';
278
278
  try {
279
279
  const cmd = isWin ? `where ${trimmed}` : `which ${trimmed}`;
280
- return execSync(cmd, { encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }).trim().split('\n')[0].trim();
280
+ return execSync(cmd, {
281
+ encoding: 'utf-8',
282
+ timeout: 5000,
283
+ stdio: ['pipe', 'pipe', 'pipe'],
284
+ ...(isWin ? { windowsHide: true } : {}),
285
+ }).trim().split('\n')[0].trim();
281
286
  } catch {
282
287
  return isWin ? `${trimmed}.cmd` : trimmed;
283
288
  }
@@ -10,8 +10,8 @@ import { assertProviderSupportsDeclaredInput, assertTextOnlyInput } from '../pro
10
10
  import { validateReadChatResultPayload } from '../providers/read-chat-contract.js';
11
11
  import type { ProviderInstance } from '../providers/provider-instance.js';
12
12
  import { readProviderChatHistory } from '../config/chat-history.js';
13
- import { LOG } from '../logging/logger.js';
14
- import { recordDebugTrace } from '../logging/debug-trace.js';
13
+ import { LOG, getRecentLogs } from '../logging/logger.js';
14
+ import { getRecentDebugTrace, recordDebugTrace } from '../logging/debug-trace.js';
15
15
  import { buildChatMessageSignature } from '../chat/chat-signatures.js';
16
16
  import type { ChatMessage } from '../types.js';
17
17
  import type { ReadChatCursor, ReadChatSyncMode, SessionTransport } from '../shared-types.js';
@@ -489,6 +489,271 @@ function buildReadChatCommandResult(payload: Record<string, any>, args: any): Co
489
489
  };
490
490
  }
491
491
 
492
+
493
+ interface DebugSanitizeOptions {
494
+ maxDepth?: number;
495
+ maxArrayLength?: number;
496
+ maxObjectKeys?: number;
497
+ maxStringLength?: number;
498
+ }
499
+
500
+ const DEFAULT_DEBUG_SANITIZE_OPTIONS: Required<DebugSanitizeOptions> = {
501
+ maxDepth: 8,
502
+ maxArrayLength: 80,
503
+ maxObjectKeys: 120,
504
+ maxStringLength: 16_000,
505
+ };
506
+
507
+ const SECRET_KEY_PATTERN = /(?:token|secret|password|passwd|authorization|cookie|api[_-]?key|access[_-]?key|refresh[_-]?token|client[_-]?secret|private[_-]?key)/i;
508
+
509
+ function truncateDebugString(value: string, maxLength: number): string {
510
+ if (value.length <= maxLength) return value;
511
+ return `${value.slice(0, maxLength)}…[truncated ${value.length - maxLength} chars]`;
512
+ }
513
+
514
+ function redactDebugSecrets(value: string): string {
515
+ return value
516
+ .replace(/(Authorization\s*:\s*Bearer\s+)[^\s'"`]+/gi, '$1[REDACTED:bearer]')
517
+ .replace(/(Bearer\s+)[A-Za-z0-9._~+\/-]{16,}=*/gi, '$1[REDACTED:bearer]')
518
+ .replace(/\b(?:gh[pousr]|github_pat)_[A-Za-z0-9_]{20,}\b/g, '[REDACTED:github-token]')
519
+ .replace(/\bsk-[A-Za-z0-9_-]{16,}\b/g, '[REDACTED:api-key]')
520
+ .replace(/\bxox[baprs]-[A-Za-z0-9-]{12,}\b/g, '[REDACTED:slack-token]')
521
+ .replace(/\b(?:adk|adm)_[A-Za-z0-9_-]{16,}\b/g, '[REDACTED:adhdev-token]')
522
+ .replace(/((?:api[_-]?key|token|secret|password|passwd|client[_-]?secret)\s*[:=]\s*)[^\s,'"`}&]+/gi, '$1[REDACTED:secret]')
523
+ .replace(/([?&](?:api[_-]?key|token|secret|password|client_secret)=)[^&#\s]+/gi, '$1[REDACTED:secret]');
524
+ }
525
+
526
+ export function sanitizeDebugBundleValue(
527
+ value: unknown,
528
+ options: DebugSanitizeOptions = {},
529
+ depth = 0,
530
+ keyHint = '',
531
+ ): unknown {
532
+ const normalizedOptions = { ...DEFAULT_DEBUG_SANITIZE_OPTIONS, ...options };
533
+ if (value === null || value === undefined) return value;
534
+ if (typeof value === 'number' || typeof value === 'boolean') return value;
535
+ if (typeof value === 'bigint') return String(value);
536
+ if (typeof value === 'string') {
537
+ if (SECRET_KEY_PATTERN.test(keyHint) && value.trim()) return '[REDACTED:secret-field]';
538
+ return truncateDebugString(redactDebugSecrets(value), normalizedOptions.maxStringLength);
539
+ }
540
+ if (typeof value === 'function') return `[Function ${value.name || 'anonymous'}]`;
541
+ if (typeof value !== 'object') return String(value);
542
+ if (depth >= normalizedOptions.maxDepth) return '[MaxDepth]';
543
+
544
+ if (Array.isArray(value)) {
545
+ const items = value
546
+ .slice(0, normalizedOptions.maxArrayLength)
547
+ .map((item) => sanitizeDebugBundleValue(item, normalizedOptions, depth + 1, keyHint));
548
+ if (value.length > normalizedOptions.maxArrayLength) {
549
+ items.push(`[truncated ${value.length - normalizedOptions.maxArrayLength} items]`);
550
+ }
551
+ return items;
552
+ }
553
+
554
+ const record = value as Record<string, unknown>;
555
+ const result: Record<string, unknown> = {};
556
+ const entries = Object.entries(record).slice(0, normalizedOptions.maxObjectKeys);
557
+ for (const [key, item] of entries) {
558
+ result[key] = sanitizeDebugBundleValue(item, normalizedOptions, depth + 1, key);
559
+ }
560
+ const remaining = Object.keys(record).length - entries.length;
561
+ if (remaining > 0) result.__truncatedKeys = remaining;
562
+ return result;
563
+ }
564
+
565
+ function summarizeProviderForDebug(provider: ProviderModule | undefined): Record<string, unknown> | null {
566
+ if (!provider) return null;
567
+ const scripts = provider.scripts && typeof provider.scripts === 'object'
568
+ ? Object.keys(provider.scripts)
569
+ : [];
570
+ const controls = Array.isArray((provider as any).controls)
571
+ ? (provider as any).controls.map((control: any) => ({
572
+ id: control?.id,
573
+ label: control?.label,
574
+ type: control?.type,
575
+ settingKey: control?.settingKey,
576
+ invokeScript: control?.invokeScript,
577
+ listScript: control?.listScript,
578
+ location: control?.location,
579
+ }))
580
+ : [];
581
+ return {
582
+ type: provider.type,
583
+ name: provider.name,
584
+ category: provider.category,
585
+ version: (provider as any).version,
586
+ canonicalHistory: provider.canonicalHistory,
587
+ historyBehavior: provider.historyBehavior,
588
+ webviewMatchText: provider.webviewMatchText,
589
+ scriptNames: scripts,
590
+ controls,
591
+ resume: provider.resume,
592
+ };
593
+ }
594
+
595
+ function summarizeSessionForDebug(session: any): Record<string, unknown> | null {
596
+ if (!session || typeof session !== 'object') return null;
597
+ return {
598
+ sessionId: session.sessionId,
599
+ instanceKey: session.instanceKey,
600
+ adapterKey: session.adapterKey,
601
+ providerType: session.providerType,
602
+ providerName: session.providerName,
603
+ transport: session.transport,
604
+ kind: session.kind,
605
+ cdpManagerKey: session.cdpManagerKey,
606
+ parentSessionId: session.parentSessionId,
607
+ providerSessionId: session.providerSessionId,
608
+ workspace: session.workspace,
609
+ title: session.title,
610
+ status: session.status,
611
+ mode: session.mode,
612
+ capabilities: session.capabilities,
613
+ };
614
+ }
615
+
616
+ function summarizeStateForDebug(state: any): Record<string, unknown> | null {
617
+ if (!state || typeof state !== 'object') return null;
618
+ const activeChat = state.activeChat && typeof state.activeChat === 'object' ? state.activeChat : null;
619
+ return {
620
+ type: state.type,
621
+ name: state.name,
622
+ category: state.category,
623
+ status: state.status,
624
+ instanceId: state.instanceId,
625
+ providerSessionId: state.providerSessionId,
626
+ title: state.title,
627
+ transport: state.transport,
628
+ mode: state.mode,
629
+ workspace: state.workspace,
630
+ runtime: state.runtime,
631
+ errorMessage: state.errorMessage,
632
+ errorReason: state.errorReason,
633
+ activeChat: activeChat ? {
634
+ status: activeChat.status,
635
+ title: activeChat.title,
636
+ messageCount: Array.isArray(activeChat.messages) ? activeChat.messages.length : undefined,
637
+ activeModal: activeChat.activeModal,
638
+ messagesTail: Array.isArray(activeChat.messages) ? activeChat.messages.slice(-10) : undefined,
639
+ } : null,
640
+ controlValues: state.controlValues,
641
+ summaryMetadata: state.summaryMetadata,
642
+ };
643
+ }
644
+
645
+ function buildDebugBundleText(bundle: Record<string, unknown>): string {
646
+ return [
647
+ '# ADHDev Chat Debug Bundle',
648
+ '',
649
+ '```json',
650
+ JSON.stringify(bundle, null, 2),
651
+ '```',
652
+ ].join('\n');
653
+ }
654
+
655
+ export async function handleGetChatDebugBundle(h: CommandHelpers, args: any): Promise<CommandResult> {
656
+ const provider = h.getProvider(args?.agentType);
657
+ const transport = getTargetTransport(h, provider);
658
+ const targetSessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
659
+ const providerType = provider?.type || getCurrentProviderType(h, args?.agentType || '');
660
+ const adapter = isCliLikeTransport(transport) ? getTargetedCliAdapter(h, args, provider?.type) : null;
661
+ const targetInstance = getTargetInstance(h, args);
662
+
663
+ let adapterStatus: unknown = null;
664
+ let parsedStatus: unknown = null;
665
+ let adapterDebugSnapshot: unknown = null;
666
+ let partialResponse = '';
667
+ if (adapter) {
668
+ try { adapterStatus = adapter.getStatus?.(); } catch (error: any) { adapterStatus = { error: error?.message || String(error) }; }
669
+ try { parsedStatus = typeof adapter.getScriptParsedStatus === 'function' ? parseMaybeJson(adapter.getScriptParsedStatus()) : null; } catch (error: any) { parsedStatus = { error: error?.message || String(error) }; }
670
+ try { adapterDebugSnapshot = typeof adapter.getDebugSnapshot === 'function' ? adapter.getDebugSnapshot() : null; } catch (error: any) { adapterDebugSnapshot = { error: error?.message || String(error) }; }
671
+ try { partialResponse = adapter.getPartialResponse?.() || ''; } catch { partialResponse = ''; }
672
+ }
673
+
674
+ let instanceState: unknown = null;
675
+ if (targetInstance?.getState) {
676
+ try { instanceState = summarizeStateForDebug(targetInstance.getState()); } catch (error: any) { instanceState = { error: error?.message || String(error) }; }
677
+ }
678
+
679
+ let readChat: unknown = null;
680
+ try {
681
+ const readResult = await handleReadChat(h, { ...args, tailLimit: Math.max(1, Math.min(40, Number(args?.tailLimit || 40))) });
682
+ readChat = readResult.success
683
+ ? {
684
+ success: true,
685
+ status: readResult.status,
686
+ title: readResult.title,
687
+ totalMessages: readResult.totalMessages,
688
+ returnedMessages: Array.isArray(readResult.messages) ? readResult.messages.length : undefined,
689
+ syncMode: readResult.syncMode,
690
+ replaceFrom: readResult.replaceFrom,
691
+ lastMessageSignature: readResult.lastMessageSignature,
692
+ providerSessionId: readResult.providerSessionId,
693
+ transcriptAuthority: readResult.transcriptAuthority,
694
+ coverage: readResult.coverage,
695
+ activeModal: readResult.activeModal,
696
+ messagesTail: Array.isArray(readResult.messages) ? readResult.messages.slice(-20) : [],
697
+ debugReadChat: readResult.debugReadChat,
698
+ }
699
+ : { success: false, error: readResult.error };
700
+ } catch (error: any) {
701
+ readChat = { success: false, error: error?.message || String(error) };
702
+ }
703
+
704
+ const cdp = h.getCdp();
705
+ const rawBundle: Record<string, unknown> = {
706
+ version: 1,
707
+ createdAt: new Date().toISOString(),
708
+ target: {
709
+ targetSessionId,
710
+ providerType,
711
+ transport,
712
+ routeManagerKey: h.currentManagerKey,
713
+ currentIdeType: h.currentIdeType,
714
+ },
715
+ session: summarizeSessionForDebug(h.currentSession),
716
+ provider: summarizeProviderForDebug(provider),
717
+ daemon: {
718
+ pid: process.pid,
719
+ platform: process.platform,
720
+ nodeVersion: process.version,
721
+ cwd: process.cwd(),
722
+ },
723
+ cdp: {
724
+ requested: !!cdp,
725
+ connected: !!cdp?.isConnected,
726
+ managerKey: getCurrentManagerKey(h),
727
+ },
728
+ instanceState,
729
+ cli: adapter ? {
730
+ cliType: adapter.cliType,
731
+ cliName: adapter.cliName,
732
+ workingDir: adapter.workingDir,
733
+ status: (adapterStatus as any)?.status,
734
+ activeModal: (adapterStatus as any)?.activeModal,
735
+ messageCount: Array.isArray((adapterStatus as any)?.messages) ? (adapterStatus as any).messages.length : undefined,
736
+ messagesTail: Array.isArray((adapterStatus as any)?.messages) ? (adapterStatus as any).messages.slice(-20) : undefined,
737
+ parsedStatus,
738
+ partialResponse,
739
+ ready: typeof adapter.isReady === 'function' ? adapter.isReady() : undefined,
740
+ processing: typeof adapter.isProcessing === 'function' ? adapter.isProcessing() : undefined,
741
+ debugSnapshot: adapterDebugSnapshot,
742
+ } : null,
743
+ readChat,
744
+ frontend: args?.frontendSnapshot && typeof args.frontendSnapshot === 'object' ? args.frontendSnapshot : null,
745
+ recentLogs: getRecentLogs(80, 'debug'),
746
+ recentDebugTrace: getRecentDebugTrace({ limit: 120 }),
747
+ };
748
+
749
+ const bundle = sanitizeDebugBundleValue(rawBundle) as Record<string, unknown>;
750
+ return {
751
+ success: true,
752
+ bundle,
753
+ text: buildDebugBundleText(bundle),
754
+ };
755
+ }
756
+
492
757
  function didProviderConfirmSend(result: any): boolean {
493
758
  const parsed = parseMaybeJson(result);
494
759
  if (parsed === true) return true;
@@ -52,7 +52,10 @@ function commandExists(command: string): boolean {
52
52
  return existsSync(expandExecutable(trimmed));
53
53
  }
54
54
  try {
55
- execFileSync(process.platform === 'win32' ? 'where' : 'which', [trimmed], { stdio: 'ignore' });
55
+ execFileSync(process.platform === 'win32' ? 'where' : 'which', [trimmed], {
56
+ stdio: 'ignore',
57
+ ...(process.platform === 'win32' ? { windowsHide: true } : {}),
58
+ });
56
59
  return true;
57
60
  } catch {
58
61
  return false;
@@ -431,6 +431,7 @@ export class DaemonCommandHandler implements CommandHelpers {
431
431
  switch (cmd) {
432
432
  // ─── Chat commands (chat-commands.ts) ───────────────
433
433
  case 'read_chat': return Chat.handleReadChat(this, args);
434
+ case 'get_chat_debug_bundle': return Chat.handleGetChatDebugBundle(this, args);
434
435
  case 'chat_history': return Chat.handleChatHistory(this, args);
435
436
  case 'send_chat': return Chat.handleSendChat(this, args);
436
437
  case 'list_chats': return Chat.handleListChats(this, args);
@@ -36,7 +36,7 @@ import { getSessionHostSurfaceKind, partitionSessionHostRecords } from '../sessi
36
36
  import { buildSessionEntries } from '../status/builders.js';
37
37
  import { buildMachineInfo, buildStatusSnapshot } from '../status/snapshot.js';
38
38
  import { getSessionCompletionMarker } from '../status/snapshot.js';
39
- import { spawnDetachedDaemonUpgradeHelper } from './upgrade-helper.js';
39
+ import { execNpmCommandSync, resolveCurrentGlobalInstallSurface, spawnDetachedDaemonUpgradeHelper } from './upgrade-helper.js';
40
40
  import * as fs from 'fs';
41
41
 
42
42
  // ─── Types ───
@@ -847,23 +847,22 @@ export class DaemonCommandRouter {
847
847
  case 'daemon_upgrade': {
848
848
  LOG.info('Upgrade', 'Remote upgrade requested from dashboard');
849
849
  try {
850
- const { execSync } = await import('child_process');
851
-
852
850
  // Detect package name for upgrade
853
851
  const isStandalone = this.deps.packageName === '@adhdev/daemon-standalone'
854
852
  || process.argv[1]?.includes('daemon-standalone');
855
853
  const pkgName = isStandalone ? '@adhdev/daemon-standalone' : 'adhdev';
854
+ const npmSurface = resolveCurrentGlobalInstallSurface({ packageName: pkgName });
856
855
 
857
856
  // Check latest version
858
- const latest = execSync(`npm view ${pkgName} version`, { encoding: 'utf-8', timeout: 10000 }).trim();
857
+ const latest = String(execNpmCommandSync(['view', pkgName, 'version'], { encoding: 'utf-8', timeout: 10000 }, npmSurface)).trim();
859
858
  LOG.info('Upgrade', `Latest ${pkgName}: v${latest}`);
860
859
  let currentInstalled: string | null = null;
861
860
  try {
862
- const currentJson = execSync(`npm ls -g ${pkgName} --depth=0 --json`, {
861
+ const currentJson = String(execNpmCommandSync(['ls', '-g', pkgName, '--depth=0', '--json'], {
863
862
  encoding: 'utf-8',
864
863
  timeout: 10000,
865
864
  stdio: ['pipe', 'pipe', 'pipe'],
866
- }).trim();
865
+ }, npmSurface)).trim();
867
866
  const parsed = JSON.parse(currentJson);
868
867
  currentInstalled = parsed?.dependencies?.[pkgName]?.version || null;
869
868
  } catch {
@@ -1,4 +1,4 @@
1
- import { execFileSync } from 'child_process';
1
+ import { execFileSync, type ExecFileSyncOptions } from 'child_process';
2
2
  import { spawn } from 'child_process';
3
3
  import * as fs from 'fs';
4
4
  import * as os from 'os';
@@ -20,16 +20,18 @@ export interface CurrentGlobalInstallSurface {
20
20
  npmArgsPrefix?: string[];
21
21
  packageRoot: string | null;
22
22
  installPrefix: string | null;
23
- execOptions?: { shell: boolean };
23
+ execOptions?: NpmExecOptions;
24
24
  }
25
25
 
26
26
  export interface PinnedGlobalInstallCommand {
27
27
  command: string;
28
28
  args: string[];
29
29
  surface: CurrentGlobalInstallSurface;
30
- execOptions: { shell: boolean };
30
+ execOptions: NpmExecOptions;
31
31
  }
32
32
 
33
+ export type NpmExecOptions = { shell: boolean; windowsHide?: boolean };
34
+
33
35
  function getUpgradeLogPath(): string {
34
36
  const home = os.homedir();
35
37
  const dir = path.join(home, '.adhdev');
@@ -49,29 +51,29 @@ function appendUpgradeLog(message: string): void {
49
51
  function resolveSiblingNpmInvocation(nodeExecutable: string, platform: NodeJS.Platform = process.platform): {
50
52
  executable: string;
51
53
  argsPrefix: string[];
52
- execOptions: { shell: boolean };
54
+ execOptions: NpmExecOptions;
53
55
  } {
54
56
  const binDir = path.dirname(nodeExecutable);
55
57
  if (platform === 'win32') {
56
58
  const npmCliPath = path.join(binDir, 'node_modules', 'npm', 'bin', 'npm-cli.js');
57
59
  if (fs.existsSync(npmCliPath)) {
58
- return { executable: nodeExecutable, argsPrefix: [npmCliPath], execOptions: { shell: false } };
60
+ return { executable: nodeExecutable, argsPrefix: [npmCliPath], execOptions: getNpmExecOptions(platform) };
59
61
  }
60
62
  for (const candidate of ['npm.exe', 'npm']) {
61
63
  const candidatePath = path.join(binDir, candidate);
62
64
  if (fs.existsSync(candidatePath)) {
63
- return { executable: candidatePath, argsPrefix: [], execOptions: { shell: false } };
65
+ return { executable: candidatePath, argsPrefix: [], execOptions: getNpmExecOptions(platform) };
64
66
  }
65
67
  }
66
- return { executable: nodeExecutable, argsPrefix: [npmCliPath], execOptions: { shell: false } };
68
+ return { executable: nodeExecutable, argsPrefix: [npmCliPath], execOptions: getNpmExecOptions(platform) };
67
69
  }
68
70
  for (const candidate of ['npm']) {
69
71
  const candidatePath = path.join(binDir, candidate);
70
72
  if (fs.existsSync(candidatePath)) {
71
- return { executable: candidatePath, argsPrefix: [], execOptions: { shell: false } };
73
+ return { executable: candidatePath, argsPrefix: [], execOptions: getNpmExecOptions(platform) };
72
74
  }
73
75
  }
74
- return { executable: 'npm', argsPrefix: [], execOptions: { shell: false } };
76
+ return { executable: 'npm', argsPrefix: [], execOptions: getNpmExecOptions(platform) };
75
77
  }
76
78
 
77
79
  function findCurrentPackageRoot(currentCliPath: string | undefined, packageName: string): string | null {
@@ -167,14 +169,34 @@ export function buildPinnedGlobalInstallCommand(options: {
167
169
  };
168
170
  }
169
171
 
170
- function getNpmExecOptions(_platform: NodeJS.Platform = process.platform): { shell: boolean } {
172
+ export function getNpmExecOptions(platform: NodeJS.Platform = process.platform): NpmExecOptions {
173
+ if (platform === 'win32') {
174
+ return { shell: false, windowsHide: true };
175
+ }
171
176
  return { shell: false };
172
177
  }
173
178
 
179
+ export function execNpmCommandSync(
180
+ args: string[],
181
+ options: ExecFileSyncOptions = {},
182
+ surface?: Pick<CurrentGlobalInstallSurface, 'npmExecutable' | 'npmArgsPrefix' | 'execOptions'>,
183
+ ): Buffer | string {
184
+ const execOptions = surface?.execOptions || getNpmExecOptions();
185
+ return execFileSync(
186
+ surface?.npmExecutable || 'npm',
187
+ [...(surface?.npmArgsPrefix || []), ...args],
188
+ {
189
+ ...options,
190
+ ...execOptions,
191
+ ...(process.platform === 'win32' ? { windowsHide: true } : {}),
192
+ },
193
+ );
194
+ }
195
+
174
196
  function killPid(pid: number): boolean {
175
197
  try {
176
198
  if (process.platform === 'win32') {
177
- execFileSync('taskkill', ['/PID', String(pid), '/T', '/F'], { stdio: 'ignore' });
199
+ execFileSync('taskkill', ['/PID', String(pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true });
178
200
  } else {
179
201
  process.kill(pid, 'SIGTERM');
180
202
  }
@@ -193,7 +215,7 @@ function getWindowsProcessCommandLine(pid: number): string | null {
193
215
  '-ExecutionPolicy', 'Bypass',
194
216
  '-Command',
195
217
  `(Get-CimInstance Win32_Process -Filter "${pidFilter}").CommandLine`,
196
- ], { encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] }).trim();
218
+ ], { encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true }).trim();
197
219
  if (psOut) return psOut;
198
220
  } catch {
199
221
  // fall through to wmic fallback
@@ -202,7 +224,7 @@ function getWindowsProcessCommandLine(pid: number): string | null {
202
224
  try {
203
225
  const wmicOut = execFileSync('wmic', [
204
226
  'process', 'where', pidFilter, 'get', 'CommandLine',
205
- ], { encoding: 'utf8', timeout: 3000, stdio: ['ignore', 'pipe', 'ignore'] }).trim();
227
+ ], { encoding: 'utf8', timeout: 3000, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true }).trim();
206
228
  if (wmicOut) return wmicOut;
207
229
  } catch {
208
230
  // noop
@@ -273,10 +295,10 @@ function removeDaemonPidFile(): void {
273
295
 
274
296
  function cleanupStaleGlobalInstallDirs(pkgName: string, surface: CurrentGlobalInstallSurface): void {
275
297
  const prefixArgs = surface.installPrefix ? ['--prefix', surface.installPrefix] : [];
276
- const npmRoot = execFileSync(surface.npmExecutable, [...(surface.npmArgsPrefix || []), 'root', '-g', ...prefixArgs], { encoding: 'utf8', ...surface.execOptions }).trim();
298
+ const npmRoot = String(execNpmCommandSync(['root', '-g', ...prefixArgs], { encoding: 'utf8' }, surface)).trim();
277
299
  if (!npmRoot) return;
278
300
  const npmPrefix = surface.installPrefix
279
- || execFileSync(surface.npmExecutable, [...(surface.npmArgsPrefix || []), 'prefix', '-g', ...prefixArgs], { encoding: 'utf8', ...surface.execOptions }).trim();
301
+ || String(execNpmCommandSync(['prefix', '-g', ...prefixArgs], { encoding: 'utf8' }, surface)).trim();
280
302
  const binDir = process.platform === 'win32' ? npmPrefix : path.join(npmPrefix, 'bin');
281
303
  const packageBaseName = pkgName.startsWith('@') ? pkgName.split('/')[1] : pkgName;
282
304
  const binNames = new Set<string>([packageBaseName]);
@@ -60,7 +60,11 @@ function resolveCommandPath(command: string): string | null {
60
60
  /** Run a shell command with timeout, returning stdout or null on failure */
61
61
  function execAsync(cmd: string, timeoutMs = 5000): Promise<string | null> {
62
62
  return new Promise((resolve) => {
63
- const child = exec(cmd, { encoding: 'utf-8', timeout: timeoutMs }, (err, stdout) => {
63
+ const child = exec(cmd, {
64
+ encoding: 'utf-8',
65
+ timeout: timeoutMs,
66
+ ...(process.platform === 'win32' ? { windowsHide: true } : {}),
67
+ }, (err, stdout) => {
64
68
  if (err || !stdout?.trim()) {
65
69
  resolve(null);
66
70
  } else {
package/src/index.d.ts CHANGED
@@ -41,8 +41,8 @@ export { DaemonCommandHandler } from './commands/handler.js';
41
41
  export type { CommandResult, CommandContext } from './commands/handler.js';
42
42
  export { DaemonCommandRouter } from './commands/router.js';
43
43
  export type { CommandRouterDeps, CommandRouterResult } from './commands/router.js';
44
- export { maybeRunDaemonUpgradeHelperFromEnv, spawnDetachedDaemonUpgradeHelper, resolveCurrentGlobalInstallSurface, buildPinnedGlobalInstallCommand } from './commands/upgrade-helper.js';
45
- export type { DaemonUpgradeHelperPayload, CurrentGlobalInstallSurface, PinnedGlobalInstallCommand } from './commands/upgrade-helper.js';
44
+ export { maybeRunDaemonUpgradeHelperFromEnv, spawnDetachedDaemonUpgradeHelper, resolveCurrentGlobalInstallSurface, buildPinnedGlobalInstallCommand, execNpmCommandSync, getNpmExecOptions } from './commands/upgrade-helper.js';
45
+ export type { DaemonUpgradeHelperPayload, CurrentGlobalInstallSurface, PinnedGlobalInstallCommand, NpmExecOptions } from './commands/upgrade-helper.js';
46
46
  export { DaemonStatusReporter } from './status/reporter.js';
47
47
  export { buildSessionEntries, findCdpManager, hasCdpManager, isCdpConnected } from './status/builders.js';
48
48
  export { buildStatusSnapshot, buildMachineInfo } from './status/snapshot.js';
package/src/index.ts CHANGED
@@ -140,15 +140,18 @@ export type { CommandResult, CommandContext } from './commands/handler.js';
140
140
  export { DaemonCommandRouter } from './commands/router.js';
141
141
  export type { CommandRouterDeps, CommandRouterResult } from './commands/router.js';
142
142
  export {
143
- maybeRunDaemonUpgradeHelperFromEnv,
144
- spawnDetachedDaemonUpgradeHelper,
145
- resolveCurrentGlobalInstallSurface,
146
- buildPinnedGlobalInstallCommand,
143
+ maybeRunDaemonUpgradeHelperFromEnv,
144
+ spawnDetachedDaemonUpgradeHelper,
145
+ resolveCurrentGlobalInstallSurface,
146
+ buildPinnedGlobalInstallCommand,
147
+ execNpmCommandSync,
148
+ getNpmExecOptions,
147
149
  } from './commands/upgrade-helper.js';
148
150
  export type {
149
- DaemonUpgradeHelperPayload,
150
- CurrentGlobalInstallSurface,
151
- PinnedGlobalInstallCommand,
151
+ DaemonUpgradeHelperPayload,
152
+ CurrentGlobalInstallSurface,
153
+ PinnedGlobalInstallCommand,
154
+ NpmExecOptions,
152
155
  } from './commands/upgrade-helper.js';
153
156
 
154
157
  // ── Status ──