@adhdev/daemon-core 0.9.82-rc.318 → 0.9.82-rc.319

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.
@@ -22,6 +22,7 @@ import * as fs from 'node:fs';
22
22
  import * as os from 'node:os';
23
23
  import * as path from 'node:path';
24
24
  import { TerminalAdapter, type TerminalAdapterOpts } from './adapter.js';
25
+ import { resolveCliSpawnPlanFromParts } from '../../cli-adapters/provider-cli-runtime.js';
25
26
  import type { PtyTransportFactory } from '../../cli-adapters/pty-transport.js';
26
27
  import { DEFAULT_SESSION_HOST_COLS, DEFAULT_SESSION_HOST_ROWS } from '@adhdev/session-host-core';
27
28
  import {
@@ -389,15 +390,35 @@ export class FsmDriver implements ISpecDriver {
389
390
  }
390
391
 
391
392
  private buildAdapterOpts(): TerminalAdapterOpts {
392
- const baseArgs = this.spec.spawn_args ?? [];
393
- const extra = this.opts.extraCliArgs ?? [];
393
+ // Single-source spawn resolution: route the spec's binary/args/env
394
+ // through the SAME planner the legacy ProviderCliAdapter uses
395
+ // (resolveCliSpawnPlanFromParts). This gives the spec/FSM path
396
+ // findBinary (PATH + npm-global / Node-dir fallback so an off-PATH
397
+ // `codex`/`claude` resolves), `{{workingDir}}` token substitution, shell
398
+ // wrapping for script-shims / non-absolute / non-native binaries, and a
399
+ // sanitized env with TERMINAL_CWD — none of which it had when it passed
400
+ // `this.spec.binary` straight to the PTY.
401
+ const cols = this.opts.cols ?? DEFAULT_SESSION_HOST_COLS;
402
+ const rows = this.opts.rows ?? DEFAULT_SESSION_HOST_ROWS;
403
+ const plan = resolveCliSpawnPlanFromParts({
404
+ command: this.spec.binary,
405
+ baseArgs: this.spec.spawn_args ?? [],
406
+ baseEnv: this.spec.env ?? {},
407
+ workingDir: this.opts.workingDir,
408
+ extraArgs: this.opts.extraCliArgs ?? [],
409
+ extraEnv: this.opts.extraEnv ?? {},
410
+ geometry: { cols, rows },
411
+ });
394
412
  return {
395
- binary: this.spec.binary,
396
- args: [...baseArgs, ...extra],
397
- cwd: this.opts.workingDir,
398
- env: { ...(this.spec.env ?? {}), ...(this.opts.extraEnv ?? {}) },
399
- cols: this.opts.cols ?? DEFAULT_SESSION_HOST_COLS,
400
- rows: this.opts.rows ?? DEFAULT_SESSION_HOST_ROWS,
413
+ binary: plan.shellCmd,
414
+ args: plan.shellArgs,
415
+ cwd: plan.ptyOptions.cwd,
416
+ // plan.ptyOptions.env is already a complete, sanitized environment
417
+ // pass it verbatim, do not overlay process.env (see envIsComplete).
418
+ env: plan.ptyOptions.env,
419
+ envIsComplete: true,
420
+ cols,
421
+ rows,
401
422
  transportFactory: this.opts.transportFactory,
402
423
  };
403
424
  }
@@ -16,6 +16,7 @@ import * as os from 'os';
16
16
  import { platform } from 'os';
17
17
  import type { ProviderLoader } from './provider-loader.js';
18
18
  import type { ProviderModule } from './contracts.js';
19
+ import { isKnownWin32GuiExe, readWin32IdeVersionFromDisk } from '../detection/win32-ide-version.js';
19
20
 
20
21
  // ─── Types ──────────────────────────────────────
21
22
 
@@ -226,6 +227,10 @@ export async function detectAllVersions(
226
227
  ): Promise<ProviderVersionInfo[]> {
227
228
  const results: ProviderVersionInfo[] = [];
228
229
  const currentOs = platform() as string;
230
+ // Map of provider type → GUI exe names (win32), used to refuse spawning a
231
+ // GUI executable for version detection (which would boot the IDE window).
232
+ const win32ProcessNames: Record<string, string[]> =
233
+ typeof loader.getWinProcessNames === 'function' ? loader.getWinProcessNames() : {};
229
234
 
230
235
  for (const provider of loader.getAll()) {
231
236
  const info: ProviderVersionInfo = {
@@ -258,12 +263,29 @@ export async function detectAllVersions(
258
263
  info.path = appPath || null;
259
264
  info.binary = resolvedBin || null;
260
265
 
261
- // Version: try CLI first, then plist
262
- if (resolvedBin) {
263
- info.version = await getVersion(resolvedBin, versionCommand);
264
- }
265
- if (!info.version && appPath) {
266
- info.version = await getMacAppVersion(appPath);
266
+ // Version detection for IDEs must avoid spawning the GUI executable.
267
+ // On Windows the resolved binary is frequently the GUI Electron exe
268
+ // (case-insensitive FS), and `<exe> --version` boots the IDE window.
269
+ // Strategy (all spawn-free where possible):
270
+ // 1. win32: read bundled product.json/package.json next to the exe.
271
+ // 2. darwin: read Info.plist via PlistBuddy (read-only).
272
+ // 3. Fallback to `<bin> --version` ONLY when the binary is not a
273
+ // known GUI exe — the safety-net guard (#4).
274
+ if (currentOs === 'win32') {
275
+ info.version = readWin32IdeVersionFromDisk(resolvedBin || appPath || '');
276
+ if (!info.version && resolvedBin && !isKnownWin32GuiExe(resolvedBin, win32ProcessNames)) {
277
+ info.version = await getVersion(resolvedBin, versionCommand);
278
+ }
279
+ } else if (currentOs === 'darwin') {
280
+ if (appPath) info.version = await getMacAppVersion(appPath);
281
+ if (!info.version && resolvedBin) {
282
+ info.version = await getVersion(resolvedBin, versionCommand);
283
+ }
284
+ } else {
285
+ // linux and others: bundled CLI wrappers are real CLIs, safe to exec.
286
+ if (resolvedBin) {
287
+ info.version = await getVersion(resolvedBin, versionCommand);
288
+ }
267
289
  }
268
290
 
269
291
  } else if (provider.category === 'cli' || provider.category === 'acp') {
@@ -0,0 +1,23 @@
1
+ /**
2
+ * working-dir — shared helpers for deriving display names from a session's
3
+ * working directory. Used by CLI/ACP provider instances to build session/tab
4
+ * titles.
5
+ */
6
+
7
+ /**
8
+ * OS-aware basename of a working directory path.
9
+ *
10
+ * Splits on BOTH POSIX (`/`) and Windows (`\`) separators so a win32 path like
11
+ * `D:\gh\adhdev-cloud` resolves to `adhdev-cloud` even when the daemon's own
12
+ * `path.basename` is POSIX-only — and a path that mixes separators still works.
13
+ * Trailing separators and root-only paths fall back to `'session'`, matching
14
+ * the historical `.split('/').filter(Boolean).pop() || 'session'` behavior.
15
+ *
16
+ * Mirrors the web dashboard's `getWorkspaceName` (`ws.split(/[/\\]/)`).
17
+ */
18
+ export function workingDirBasename(p: string): string {
19
+ return (p || '')
20
+ .split(/[\\/]/)
21
+ .filter(Boolean)
22
+ .pop() || 'session';
23
+ }