@adhdev/daemon-core 0.8.27 → 0.8.28

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.
@@ -77,7 +77,7 @@ export declare class AcpProviderInstance implements ProviderInstance {
77
77
  sendPrompt(text: string, contentBlocks?: ContentBlock[]): Promise<void>;
78
78
  private cancelSession;
79
79
  private permissionResolvers;
80
- private resolvePermission;
80
+ resolvePermission(approved: boolean): Promise<void>;
81
81
  private handleSessionUpdate;
82
82
  /** Handle legacy session/update formats (pre-standardization compat) */
83
83
  private handleLegacyUpdate;
@@ -16,6 +16,7 @@ import { VersionArchive } from './version-archive.js';
16
16
  import type { ProviderModule, ProviderCategory, ProviderSettingSchema, ResolvedProvider } from './contracts.js';
17
17
  export declare class ProviderLoader {
18
18
  private providers;
19
+ private providerAvailability;
19
20
  private userDir;
20
21
  private upstreamDir;
21
22
  private disableUpstream;
@@ -163,6 +164,28 @@ export declare class ProviderLoader {
163
164
  * Available IDE types (only those with cdpPorts)
164
165
  */
165
166
  getAvailableIdeTypes(): string[];
167
+ getSpawnCommand(type: string, fallback?: string): string;
168
+ getIdeCliCommand(type: string, fallback?: string | null): string | null;
169
+ getIdePathCandidates(type: string, fallback?: string[]): string[];
170
+ setProviderAvailability(type: string, state: {
171
+ installed: boolean;
172
+ detectedPath?: string | null;
173
+ }): void;
174
+ setCliDetectionResults(results: Array<{
175
+ id: string;
176
+ installed: boolean;
177
+ path?: string;
178
+ }>, replace?: boolean): void;
179
+ setIdeDetectionResults(results: Array<{
180
+ id: string;
181
+ installed: boolean;
182
+ path?: string | null;
183
+ cliCommand?: string | null;
184
+ }>, replace?: boolean): void;
185
+ getAvailableProviderInfos(): Array<ProviderModule & {
186
+ installed?: boolean;
187
+ detectedPath?: string | null;
188
+ }>;
166
189
  /**
167
190
  * Register IDE providers to core/detector registry
168
191
  * → Enables detectIDEs() to detect provider.js-based IDEs
@@ -242,6 +265,9 @@ export declare class ProviderLoader {
242
265
  * Save provider setting value (writes to config.json)
243
266
  */
244
267
  setSetting(type: string, key: string, value: any): boolean;
268
+ private getOptionalStringSetting;
269
+ private getSettingsSchema;
270
+ private getSyntheticSettings;
245
271
  /**
246
272
  * Find the on-disk directory for a provider by type.
247
273
  * Canonical shape: root/category/type.
@@ -84,6 +84,8 @@ export interface AvailableProviderInfo {
84
84
  category: 'ide' | 'extension' | 'cli' | 'acp';
85
85
  displayName: string;
86
86
  icon: string;
87
+ installed?: boolean;
88
+ detectedPath?: string | null;
87
89
  }
88
90
  /** ACP config option (model/mode/thought_level selection) */
89
91
  export interface AcpConfigOption {
@@ -17,6 +17,14 @@ export interface StatusSnapshotOptions {
17
17
  displayName?: string;
18
18
  category: 'ide' | 'extension' | 'cli' | 'acp';
19
19
  }>;
20
+ getAvailableProviderInfos?: () => Array<{
21
+ type: string;
22
+ icon?: string;
23
+ displayName?: string;
24
+ category: 'ide' | 'extension' | 'cli' | 'acp';
25
+ installed?: boolean;
26
+ detectedPath?: string | null;
27
+ }>;
20
28
  };
21
29
  detectedIdes: Array<{
22
30
  id: string;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/session-host-core",
3
- "version": "0.8.27",
3
+ "version": "0.8.28",
4
4
  "description": "ADHDev local session host core — 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.8.27",
3
+ "version": "0.8.28",
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",
@@ -171,14 +171,27 @@ export class ProviderStreamAdapter implements IAgentStreamAdapter {
171
171
  const raw = await evaluate(script, 10000) as string;
172
172
  const data = typeof raw === 'string' ? JSON.parse(raw) : raw;
173
173
  if (data?.error) return [];
174
- return Array.isArray(data) ? data : [];
174
+ if (Array.isArray(data)) return data;
175
+ if (Array.isArray(data?.sessions)) return data.sessions;
176
+ if (Array.isArray(data?.chats)) return data.chats;
177
+ return [];
175
178
  } catch { return []; }
176
179
  }
177
180
 
178
181
  async switchSession(evaluate: AgentEvaluateFn, sessionId: string): Promise<boolean> {
179
182
  const script = this.callScript('switchSession', sessionId);
180
183
  if (!script) return false;
181
- return (await evaluate(script, 10000)) === true;
184
+ const raw = await evaluate(script, 10000);
185
+ const data = this.parseMaybeJson(raw);
186
+ if (data === true) return true;
187
+ if (typeof data === 'string') {
188
+ const normalized = data.trim().toLowerCase();
189
+ return normalized === 'true' || normalized === 'ok' || normalized === 'switched' || normalized === 'success';
190
+ }
191
+ if (data && typeof data === 'object') {
192
+ return data.switched === true || data.success === true || data.ok === true;
193
+ }
194
+ return false;
182
195
  }
183
196
 
184
197
  async focusEditor(evaluate: AgentEvaluateFn): Promise<void> {
@@ -25,6 +25,7 @@ import { VersionArchive, detectAllVersions } from '../providers/version-archive.
25
25
  import { ProviderInstanceManager } from '../providers/provider-instance-manager.js';
26
26
  import { DevServer } from '../daemon/dev-server.js';
27
27
  import { detectIDEs } from '../detection/ide-detector.js';
28
+ import { detectCLI, detectCLIs } from '../detection/cli-detector.js';
28
29
  import { SessionRegistry } from '../sessions/registry.js';
29
30
  import { installGlobalInterceptor, LOG } from '../logging/logger.js';
30
31
  import { loadConfig } from '../config/config.js';
@@ -160,6 +161,28 @@ export async function initDaemonComponents(config: DaemonInitConfig): Promise<Da
160
161
  let agentStreamManager: DaemonAgentStreamManager | null = null;
161
162
  let poller: AgentStreamPoller | null = null;
162
163
 
164
+ const refreshProviderAvailability = async (providerType?: string) => {
165
+ const targetProvider = providerType ? providerLoader.getMeta(providerLoader.resolveAlias(providerType)) : null;
166
+ const targetCategory = targetProvider?.category;
167
+
168
+ if (!providerType || targetCategory === 'cli' || targetCategory === 'acp') {
169
+ if (providerType && targetProvider) {
170
+ const detected = await detectCLI(targetProvider.type, providerLoader, { includeVersion: false });
171
+ providerLoader.setProviderAvailability(targetProvider.type, {
172
+ installed: !!detected,
173
+ detectedPath: detected?.path || null,
174
+ });
175
+ } else {
176
+ providerLoader.setCliDetectionResults(await detectCLIs(providerLoader, { includeVersion: false }), true);
177
+ }
178
+ }
179
+
180
+ if (!providerType || targetCategory === 'ide') {
181
+ detectedIdesRef.value = await detectIDEs(providerLoader);
182
+ providerLoader.setIdeDetectionResults(detectedIdesRef.value, true);
183
+ }
184
+ };
185
+
163
186
  // 4. CLI Manager
164
187
  const cliManager = new DaemonCliManager({
165
188
  ...config.cliManagerDeps,
@@ -169,7 +192,7 @@ export async function initDaemonComponents(config: DaemonInitConfig): Promise<Da
169
192
 
170
193
  // 5. Detect IDEs
171
194
  LOG.info('Init', 'Detecting IDEs...');
172
- detectedIdesRef.value = await detectIDEs();
195
+ await refreshProviderAvailability();
173
196
  const installed = detectedIdesRef.value.filter((i: any) => i.installed);
174
197
  LOG.info('Init', `Found ${installed.length} IDE(s): ${installed.map((i: any) => i.id).join(', ') || 'none'}`);
175
198
 
@@ -219,6 +242,10 @@ export async function initDaemonComponents(config: DaemonInitConfig): Promise<Da
219
242
  providerLoader,
220
243
  instanceManager,
221
244
  sessionRegistry,
245
+ onProviderSettingChanged: async (providerType) => {
246
+ await refreshProviderAvailability(providerType);
247
+ config.onStatusChange?.();
248
+ },
222
249
  });
223
250
 
224
251
  // 8. AgentStreamManager
@@ -304,12 +304,20 @@ function computeTerminalQueryTail(buffer: string): string {
304
304
  }
305
305
 
306
306
  function findBinary(name: string): string {
307
+ const trimmed = String(name || '').trim();
308
+ if (!trimmed) return trimmed;
309
+ const expanded = trimmed.startsWith('~')
310
+ ? path.join(os.homedir(), trimmed.slice(1))
311
+ : trimmed;
312
+ if (path.isAbsolute(expanded) || expanded.includes('/') || expanded.includes('\\')) {
313
+ return path.isAbsolute(expanded) ? expanded : path.resolve(expanded);
314
+ }
307
315
  const isWin = os.platform() === 'win32';
308
316
  try {
309
- const cmd = isWin ? `where ${name}` : `which ${name}`;
317
+ const cmd = isWin ? `where ${trimmed}` : `which ${trimmed}`;
310
318
  return execSync(cmd, { encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }).trim().split('\n')[0].trim();
311
319
  } catch {
312
- return isWin ? `${name}.cmd` : name;
320
+ return isWin ? `${trimmed}.cmd` : trimmed;
313
321
  }
314
322
  }
315
323
 
@@ -928,7 +936,10 @@ export class ProviderCliAdapter implements CliAdapter {
928
936
  if (this.ptyProcess) return;
929
937
 
930
938
  const { spawn: spawnConfig } = this.provider;
931
- const binaryPath = findBinary(spawnConfig.command);
939
+ const configuredCommand = typeof this.runtimeSettings.executablePath === 'string' && this.runtimeSettings.executablePath.trim()
940
+ ? this.runtimeSettings.executablePath.trim()
941
+ : spawnConfig.command;
942
+ const binaryPath = findBinary(configuredCommand);
932
943
  const isWin = os.platform() === 'win32';
933
944
  const allArgs = [...spawnConfig.args, ...this.extraArgs];
934
945
 
@@ -478,6 +478,14 @@ export async function handleListChats(h: CommandHelpers, args: any): Promise<Com
478
478
  if (evalResult) {
479
479
  let parsed = evalResult.result;
480
480
  if (typeof parsed === 'string') { try { parsed = JSON.parse(parsed); } catch { } }
481
+ if (parsed?.sessions && Array.isArray(parsed.sessions)) {
482
+ LOG.info('Command', `[list_chats] OK: ${parsed.sessions.length} chats`);
483
+ return { success: true, chats: parsed.sessions };
484
+ }
485
+ if (parsed?.chats && Array.isArray(parsed.chats)) {
486
+ LOG.info('Command', `[list_chats] OK: ${parsed.chats.length} chats`);
487
+ return { success: true, chats: parsed.chats };
488
+ }
481
489
  if (Array.isArray(parsed)) {
482
490
  LOG.info('Command', `[list_chats] OK: ${parsed.length} chats`);
483
491
  return { success: true, chats: parsed };
@@ -561,8 +569,14 @@ export async function handleSwitchChat(h: CommandHelpers, args: any): Promise<Co
561
569
  return { success: false, error: `webviewSwitchSession failed: ${e.message}` };
562
570
  }
563
571
 
564
- const script = h.getProviderScript('switchSession', { SESSION_ID: JSON.stringify(sessionId) })
565
- || h.getProviderScript('switch_session', { SESSION_ID: JSON.stringify(sessionId) });
572
+ const switchParams = {
573
+ sessionId,
574
+ title: sessionId,
575
+ id: sessionId,
576
+ SESSION_ID: JSON.stringify(sessionId),
577
+ };
578
+ const script = h.getProviderScript('switchSession', switchParams)
579
+ || h.getProviderScript('switch_session', switchParams);
566
580
  if (!script) return { success: false, error: 'switch_session script not available' };
567
581
 
568
582
  try {
@@ -630,8 +644,8 @@ export async function handleSetMode(h: CommandHelpers, args: any): Promise<Comma
630
644
  const adapter = getTargetedCliAdapter(h, args, provider?.type);
631
645
  if (adapter) {
632
646
  const acpInstance = (adapter as any)._acpInstance;
633
- if (acpInstance && typeof acpInstance.onEvent === 'function') {
634
- acpInstance.onEvent('set_mode', { mode });
647
+ if (acpInstance && typeof acpInstance.setMode === 'function') {
648
+ await acpInstance.setMode(mode);
635
649
  return { success: true, mode };
636
650
  }
637
651
  }
@@ -687,9 +701,9 @@ export async function handleChangeModel(h: CommandHelpers, args: any): Promise<C
687
701
  LOG.info('Command', `[change_model] ACP adapter found: ${!!adapter}, type=${(adapter as any)?.cliType}, hasAcpInstance=${!!(adapter as any)?._acpInstance}`);
688
702
  if (adapter) {
689
703
  const acpInstance = (adapter as any)._acpInstance;
690
- if (acpInstance && typeof acpInstance.onEvent === 'function') {
691
- acpInstance.onEvent('change_model', { model });
692
- LOG.info('Command', `[change_model] Dispatched change_model event to ACP instance`);
704
+ if (acpInstance && typeof acpInstance.setConfigOption === 'function') {
705
+ await acpInstance.setConfigOption('model', model);
706
+ LOG.info('Command', `[change_model] Updated ACP model to ${model}`);
693
707
  return { success: true, model };
694
708
  }
695
709
  }
@@ -819,6 +833,21 @@ export async function handleResolveAction(h: CommandHelpers, args: any): Promise
819
833
  return { success: ok };
820
834
  }
821
835
 
836
+ // 1.5 ACP transport: resolve protocol permission request directly
837
+ if (transport === 'acp') {
838
+ const adapter = getTargetedCliAdapter(h, args, provider?.type);
839
+ const acpInstance = adapter?._acpInstance;
840
+ if (!acpInstance) return { success: false, error: 'ACP instance not found' };
841
+
842
+ try {
843
+ await acpInstance.resolvePermission(action === 'approve' || action === 'accept' || action === 'always');
844
+ LOG.info('Command', `[resolveAction] ACP → ${action}`);
845
+ return { success: true, action };
846
+ } catch (e: any) {
847
+ return { success: false, error: e?.message || 'ACP resolve action failed' };
848
+ }
849
+ }
850
+
822
851
  // 2. Webview Provider script
823
852
  if (provider?.scripts?.webviewResolveAction || provider?.scripts?.webview_resolve_action) {
824
853
  const script = h.getProviderScript('webviewResolveAction', { action, button, buttonText: button })
@@ -522,10 +522,10 @@ export class DaemonCliManager {
522
522
  if (!cliInfo) {
523
523
  const installHint = provider?.install || '';
524
524
  const displayName = provider?.displayName || provider?.name || cliType;
525
- const spawnCmd = provider?.spawn?.command || cliType;
525
+ const spawnCmd = this.providerLoader.getSpawnCommand(normalizedType, provider?.spawn?.command || cliType);
526
526
  throw new Error(
527
527
  `${displayName} is not installed.\n` +
528
- `Command '${spawnCmd}' not found on PATH.\n` +
528
+ `Command '${spawnCmd}' is not available.\n` +
529
529
  (installHint ? `\n${installHint}\n` : '') +
530
530
  `\nRun 'adhdev doctor' for detailed diagnostics.`
531
531
  );
@@ -44,6 +44,7 @@ export interface CommandContext {
44
44
  /** ProviderInstanceManager — for runtime settings propagation */
45
45
  instanceManager?: ProviderInstanceManager;
46
46
  sessionRegistry?: SessionRegistry;
47
+ onProviderSettingChanged?: (providerType: string, key: string, value: any) => Promise<void> | void;
47
48
  }
48
49
 
49
50
  /**
@@ -365,6 +365,11 @@ export class DaemonCommandRouter {
365
365
  if (!ideType) throw new Error('ideType required');
366
366
  const killProcess = args?.killProcess !== false; // default true
367
367
  await this.stopIde(ideType, killProcess);
368
+ try {
369
+ const results = await detectIDEs(this.deps.providerLoader);
370
+ this.deps.detectedIdes.value = results;
371
+ this.deps.providerLoader.setIdeDetectionResults(results, true);
372
+ } catch { /* ignore detection refresh errors */ }
368
373
  return { success: true, ideType, stopped: true, processKilled: killProcess };
369
374
  }
370
375
 
@@ -415,6 +420,11 @@ export class DaemonCommandRouter {
415
420
  }
416
421
  }
417
422
  this.deps.onIdeConnected?.();
423
+ try {
424
+ const results = await detectIDEs(this.deps.providerLoader);
425
+ this.deps.detectedIdes.value = results;
426
+ this.deps.providerLoader.setIdeDetectionResults(results, true);
427
+ } catch { /* ignore detection refresh errors */ }
418
428
  if (result.success && resolvedWorkspace) {
419
429
  try {
420
430
  const next = appendRecentActivity(loadState(), {
@@ -441,8 +451,9 @@ export class DaemonCommandRouter {
441
451
 
442
452
  // ─── Detect IDEs ───
443
453
  case 'detect_ides': {
444
- const results = await detectIDEs();
454
+ const results = await detectIDEs(this.deps.providerLoader);
445
455
  this.deps.detectedIdes.value = results;
456
+ this.deps.providerLoader.setIdeDetectionResults(results, true);
446
457
  return { success: true, detectedInfo: results };
447
458
  }
448
459
 
@@ -76,7 +76,7 @@ export function handleGetProviderSettings(h: CommandHelpers, args: any): Command
76
76
  return { success: true, settings: allSettings, values: allValues };
77
77
  }
78
78
 
79
- export function handleSetProviderSetting(h: CommandHelpers, args: any): CommandResult {
79
+ export async function handleSetProviderSetting(h: CommandHelpers, args: any): Promise<CommandResult> {
80
80
  const loader = h.ctx.providerLoader as ProviderLoader | undefined;
81
81
  const { providerType, key, value } = args || {};
82
82
  if (!providerType || !key || value === undefined) {
@@ -89,6 +89,7 @@ export function handleSetProviderSetting(h: CommandHelpers, args: any): CommandR
89
89
  const updated = h.ctx.instanceManager.updateInstanceSettings(providerType, allSettings);
90
90
  LOG.info('Command', `[set_provider_setting] ${providerType}.${key}=${JSON.stringify(value)} → ${updated} instance(s) updated`);
91
91
  }
92
+ await h.ctx.onProviderSettingChanged?.(providerType, key, value);
92
93
  return { success: true, providerType, key, value };
93
94
  }
94
95
  return { success: false, error: `Failed to set ${providerType}.${key} — invalid key, value, or not a public setting` };
@@ -133,7 +134,7 @@ function getCliScriptCommand(payload: any): { type: string; text?: string } | nu
133
134
 
134
135
  const command = payload.command;
135
136
  if (!command || typeof command !== 'object') return null;
136
- if (command.type !== 'send_message') return null;
137
+ if (command.type !== 'send_message' && command.type !== 'pty_write') return null;
137
138
 
138
139
  const text = typeof command.text === 'string'
139
140
  ? command.text.trim()
@@ -141,7 +142,7 @@ function getCliScriptCommand(payload: any): { type: string; text?: string } | nu
141
142
  ? command.message.trim()
142
143
  : '';
143
144
  if (!text) return null;
144
- return { type: 'send_message', text };
145
+ return { type: command.type, text };
145
146
  }
146
147
 
147
148
  function applyProviderPatch(h: CommandHelpers, args: any, payload: any): void {
@@ -191,6 +192,8 @@ async function executeProviderScript(h: CommandHelpers, args: any, scriptName: s
191
192
  const cliCommand = getCliScriptCommand(parsed.payload);
192
193
  if (cliCommand?.type === 'send_message' && cliCommand.text) {
193
194
  await adapter.sendMessage(cliCommand.text);
195
+ } else if (cliCommand?.type === 'pty_write' && cliCommand.text && adapter.writeRaw) {
196
+ adapter.writeRaw(cliCommand.text + '\r');
194
197
  }
195
198
  applyProviderPatch(h, args, parsed.payload);
196
199
  return { success: true, ...(parsed.payload && typeof parsed.payload === 'object' ? parsed.payload : { result: parsed.payload }) };
@@ -9,6 +9,8 @@
9
9
 
10
10
  import { exec } from 'child_process';
11
11
  import * as os from 'os';
12
+ import * as path from 'path';
13
+ import { existsSync } from 'fs';
12
14
  import type { ProviderLoader } from '../providers/provider-loader.js';
13
15
 
14
16
  export interface CLIInfo {
@@ -28,6 +30,33 @@ function parseVersion(raw: string): string {
28
30
  return match ? match[1] : raw.split('\n')[0].slice(0, 100);
29
31
  }
30
32
 
33
+ function shellQuote(value: string): string {
34
+ if (/^[a-zA-Z0-9_./:@%+=,-]+$/.test(value)) return value;
35
+ return `"${value.replace(/(["\\$`])/g, '\\$1')}"`;
36
+ }
37
+
38
+ function expandHome(value: string): string {
39
+ const trimmed = value.trim();
40
+ if (!trimmed.startsWith('~')) return trimmed;
41
+ return path.join(os.homedir(), trimmed.slice(1));
42
+ }
43
+
44
+ function isExplicitCommandPath(command: string): boolean {
45
+ const trimmed = command.trim();
46
+ return path.isAbsolute(trimmed) || trimmed.includes('/') || trimmed.includes('\\') || trimmed.startsWith('~');
47
+ }
48
+
49
+ function resolveCommandPath(command: string): string | null {
50
+ const trimmed = command.trim();
51
+ if (!trimmed) return null;
52
+ if (isExplicitCommandPath(trimmed)) {
53
+ const expanded = expandHome(trimmed);
54
+ const candidate = path.isAbsolute(expanded) ? expanded : path.resolve(expanded);
55
+ return existsSync(candidate) ? candidate : null;
56
+ }
57
+ return null;
58
+ }
59
+
31
60
  /** Run a shell command with timeout, returning stdout or null on failure */
32
61
  function execAsync(cmd: string, timeoutMs = 5000): Promise<string | null> {
33
62
  return new Promise((resolve) => {
@@ -47,9 +76,13 @@ function execAsync(cmd: string, timeoutMs = 5000): Promise<string | null> {
47
76
  * Detect all CLI/ACP agents (parallel)
48
77
  * @param providerLoader ProviderLoader instance (dynamic list creation)
49
78
  */
50
- export async function detectCLIs(providerLoader?: ProviderLoader): Promise<CLIInfo[]> {
79
+ export async function detectCLIs(
80
+ providerLoader?: ProviderLoader,
81
+ options?: { includeVersion?: boolean },
82
+ ): Promise<CLIInfo[]> {
51
83
  const platform = os.platform();
52
84
  const whichCmd = platform === 'win32' ? 'where' : 'which';
85
+ const includeVersion = options?.includeVersion !== false;
53
86
 
54
87
  // Provider-based dynamic list creation, fallback is empty array
55
88
  const cliList = providerLoader
@@ -60,28 +93,31 @@ export async function detectCLIs(providerLoader?: ProviderLoader): Promise<CLIIn
60
93
  const results = await Promise.all(
61
94
  cliList.map(async (cli): Promise<CLIInfo> => {
62
95
  try {
63
- const pathResult = await execAsync(`${whichCmd} ${cli.command}`);
96
+ const explicitPath = resolveCommandPath(cli.command);
97
+ const pathResult = explicitPath || await execAsync(`${whichCmd} ${shellQuote(cli.command)}`);
64
98
  if (!pathResult) return { ...cli, installed: false };
65
99
 
66
- const firstPath = pathResult.split('\n')[0];
100
+ const firstPath = explicitPath || pathResult.split('\n')[0];
67
101
 
68
102
  // Get version (parallel with other checks)
69
103
  let version: string | undefined;
70
- try {
104
+ if (includeVersion) {
71
105
  const versionCommands = [
106
+ `"${firstPath}" --version`,
107
+ `"${firstPath}" -V`,
108
+ `"${firstPath}" -v`,
72
109
  cli.versionCommand,
73
- `${cli.command} --version`,
74
- `${cli.command} -V`,
75
- `${cli.command} -v`,
76
110
  ].filter((v): v is string => !!v);
77
- for (const versionCommand of versionCommands) {
78
- const versionResult = await execAsync(versionCommand, 3000);
79
- if (versionResult) {
80
- version = parseVersion(versionResult);
81
- break;
111
+ try {
112
+ for (const versionCommand of versionCommands) {
113
+ const versionResult = await execAsync(versionCommand, 3000);
114
+ if (versionResult) {
115
+ version = parseVersion(versionResult);
116
+ break;
117
+ }
82
118
  }
83
- }
84
- } catch { }
119
+ } catch { }
120
+ }
85
121
 
86
122
  return { ...cli, installed: true, version, path: firstPath };
87
123
  } catch {
@@ -94,7 +130,11 @@ export async function detectCLIs(providerLoader?: ProviderLoader): Promise<CLIIn
94
130
  }
95
131
 
96
132
  /** Detect specific CLI — only probes the one requested provider */
97
- export async function detectCLI(cliId: string, providerLoader?: ProviderLoader): Promise<CLIInfo | null> {
133
+ export async function detectCLI(
134
+ cliId: string,
135
+ providerLoader?: ProviderLoader,
136
+ options?: { includeVersion?: boolean },
137
+ ): Promise<CLIInfo | null> {
98
138
  const resolvedId = providerLoader ? providerLoader.resolveAlias(cliId) : cliId;
99
139
 
100
140
  if (providerLoader) {
@@ -104,25 +144,28 @@ export async function detectCLI(cliId: string, providerLoader?: ProviderLoader):
104
144
  const platform = os.platform();
105
145
  const whichCmd = platform === 'win32' ? 'where' : 'which';
106
146
  try {
107
- const pathResult = await execAsync(`${whichCmd} ${target.command}`);
147
+ const explicitPath = resolveCommandPath(target.command);
148
+ const pathResult = explicitPath || await execAsync(`${whichCmd} ${shellQuote(target.command)}`);
108
149
  if (!pathResult) return null;
109
- const firstPath = pathResult.split('\n')[0];
150
+ const firstPath = explicitPath || pathResult.split('\n')[0];
110
151
  let version: string | undefined;
111
- try {
152
+ if (options?.includeVersion !== false) {
112
153
  const versionCommands = [
154
+ `"${firstPath}" --version`,
155
+ `"${firstPath}" -V`,
156
+ `"${firstPath}" -v`,
113
157
  target.versionCommand,
114
- `${target.command} --version`,
115
- `${target.command} -V`,
116
- `${target.command} -v`,
117
158
  ].filter((v): v is string => !!v);
118
- for (const versionCommand of versionCommands) {
119
- const versionResult = await execAsync(versionCommand, 3000);
120
- if (versionResult) {
121
- version = parseVersion(versionResult);
122
- break;
159
+ try {
160
+ for (const versionCommand of versionCommands) {
161
+ const versionResult = await execAsync(versionCommand, 3000);
162
+ if (versionResult) {
163
+ version = parseVersion(versionResult);
164
+ break;
165
+ }
123
166
  }
124
- }
125
- } catch { }
167
+ } catch { }
168
+ }
126
169
  return { ...target, installed: true, version, path: firstPath };
127
170
  } catch {
128
171
  return null;
@@ -131,6 +174,6 @@ export async function detectCLI(cliId: string, providerLoader?: ProviderLoader):
131
174
  }
132
175
 
133
176
  // Fallback: full scan for unknown provider IDs
134
- const all = await detectCLIs(providerLoader);
177
+ const all = await detectCLIs(providerLoader, options);
135
178
  return all.find((c) => c.id === resolvedId && c.installed) || null;
136
179
  }
@@ -10,6 +10,8 @@
10
10
  import { execSync } from 'child_process';
11
11
  import { existsSync } from 'fs';
12
12
  import { platform, homedir } from 'os';
13
+ import * as path from 'path';
14
+ import type { ProviderLoader } from '../providers/provider-loader.js';
13
15
 
14
16
  // ─── Types ──────────────────────────────────────
15
17
 
@@ -62,9 +64,18 @@ function getMergedDefinitions(): IDEDefinition[] {
62
64
  }
63
65
 
64
66
  function findCliCommand(command: string): string | null {
67
+ const trimmed = String(command || '').trim();
68
+ if (!trimmed) return null;
69
+ if (path.isAbsolute(trimmed) || trimmed.includes('/') || trimmed.includes('\\') || trimmed.startsWith('~')) {
70
+ const candidate = trimmed.startsWith('~')
71
+ ? path.join(homedir(), trimmed.slice(1))
72
+ : trimmed;
73
+ const resolved = path.isAbsolute(candidate) ? candidate : path.resolve(candidate);
74
+ return existsSync(resolved) ? resolved : null;
75
+ }
65
76
  try {
66
77
  const result = execSync(
67
- platform() === 'win32' ? `where ${command}` : `which ${command}`,
78
+ platform() === 'win32' ? `where ${trimmed}` : `which ${trimmed}`,
68
79
  { encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }
69
80
  ).trim();
70
81
  return result.split('\n')[0] || null;
@@ -89,27 +100,29 @@ function getIdeVersion(cliCommand: string): string | null {
89
100
  function checkPathExists(paths: string[]): string | null {
90
101
  const home = homedir();
91
102
  for (const p of paths) {
92
- if (p.includes('*')) {
103
+ const normalized = p.startsWith('~')
104
+ ? path.join(home, p.slice(1))
105
+ : p;
106
+ if (normalized.includes('*')) {
93
107
  // Wildcard expansion: replace `*` with the current user's home folder name
94
108
  // e.g. "C:\Users\*\AppData\..." → "C:\Users\vilmi\AppData\..."
95
109
  const username = home.split(/[\\/]/).pop() || '';
96
- const resolved = p.replace('*', username);
110
+ const resolved = normalized.replace('*', username);
97
111
  if (existsSync(resolved)) return resolved;
98
112
  } else {
99
- if (existsSync(p)) return p;
113
+ if (existsSync(normalized)) return normalized;
100
114
  }
101
115
  }
102
116
  return null;
103
117
  }
104
118
 
105
- export async function detectIDEs(): Promise<IDEInfo[]> {
119
+ export async function detectIDEs(providerLoader?: ProviderLoader): Promise<IDEInfo[]> {
106
120
  const os = platform() as 'darwin' | 'win32' | 'linux';
107
121
  const results: IDEInfo[] = [];
108
122
 
109
123
  for (const def of getMergedDefinitions()) {
110
- const cliPath = findCliCommand(def.cli);
111
- const appPath = checkPathExists(def.paths[os] || []);
112
- const installed = !!(cliPath || appPath);
124
+ const cliPath = findCliCommand(providerLoader?.getIdeCliCommand(def.id, def.cli) || def.cli);
125
+ const appPath = checkPathExists(providerLoader?.getIdePathCandidates(def.id, def.paths[os] || []) || []);
113
126
 
114
127
  let resolvedCli = cliPath;
115
128
 
@@ -136,6 +149,9 @@ export async function detectIDEs(): Promise<IDEInfo[]> {
136
149
  }
137
150
  }
138
151
 
152
+ const installed = os === 'darwin'
153
+ ? !!(resolvedCli || appPath)
154
+ : !!resolvedCli;
139
155
  const version = resolvedCli ? getIdeVersion(resolvedCli) : null;
140
156
 
141
157
  results.push({
package/src/launch.ts CHANGED
@@ -310,7 +310,7 @@ export async function launchWithCdp(options: LaunchOptions = {}): Promise<Launch
310
310
 
311
311
  // 1. IDE determine
312
312
  let targetIde: IDEInfo | undefined;
313
- const ides = await detectIDEs();
313
+ const ides = await detectIDEs(getProviderLoader());
314
314
 
315
315
  if (options.ideId) {
316
316
  targetIde = ides.find(i => i.id === options.ideId && i.installed);