Skip to content

Commit 5b45390

Browse files
authored
Spawn bundled CLI via Node, not the missing .bin shim (#19)
The packaged .vsix never contained npm's `node_modules/.bin/agent-device` shim, so installed extensions hit `ENOENT` on the first run step. Resolve the bundled CLI as `process.execPath` plus the actual `.mjs` entrypoint (`node_modules/agent-device/bin/agent-device.mjs`), with `ELECTRON_RUN_AS_NODE=1` set so the host's Electron binary runs the script as Node. Same pattern works in VS Code and every Electron fork (Cursor, VSCodium, etc.). User-supplied `agentDevice.cliPath` overrides still spawn directly. Bumps to 0.1.2.
1 parent f593640 commit 5b45390

6 files changed

Lines changed: 54 additions & 18 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
66

77
## [Unreleased]
88

9+
## [0.1.2] - 2026-05-03
10+
11+
### Fixed
12+
13+
- **Run steps now work in installed extensions.** The `node_modules/.bin/agent-device` shim that npm creates locally is not preserved in the packaged `.vsix`, so spawning the bundled CLI failed with `ENOENT` on every step. The extension now spawns the CLI's `.mjs` entrypoint directly via the host's Node runtime (`process.execPath` with `ELECTRON_RUN_AS_NODE=1`), which works in VS Code, Cursor, and other Electron-based forks.
14+
915
## [0.1.1] - 2026-05-03
1016

1117
### Changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "agent-device",
33
"displayName": "Agent Device",
4-
"version": "0.1.1",
4+
"version": "0.1.2",
55
"description": "Author, run, and inspect agent-device .ad scripts inside your IDE.",
66
"categories": [
77
"Other",

src/runners/cliRunner.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import { spawn } from 'node:child_process';
22

3-
export type BinPath = string | (() => string);
3+
export interface ResolvedBin {
4+
readonly command: string;
5+
readonly prefixArgs: readonly string[];
6+
readonly env?: NodeJS.ProcessEnv;
7+
}
8+
9+
type BinSpec = string | ResolvedBin;
10+
export type BinPath = BinSpec | (() => BinSpec);
411

512
export interface CliExecution {
613
readonly exitCode: number;
@@ -17,14 +24,16 @@ export interface CliRunOptions {
1724
export class CliRunner {
1825
constructor(private readonly binPath: BinPath) {}
1926

20-
private resolveBin(): string {
21-
return typeof this.binPath === 'function' ? this.binPath() : this.binPath;
27+
private resolveBin(): ResolvedBin {
28+
const value = typeof this.binPath === 'function' ? this.binPath() : this.binPath;
29+
return typeof value === 'string' ? { command: value, prefixArgs: [] } : value;
2230
}
2331

2432
run(argv: readonly string[], options: CliRunOptions = {}): Promise<CliExecution> {
2533
return new Promise((resolve, reject) => {
26-
const proc = spawn(this.resolveBin(), [...argv], {
27-
env: { ...process.env, ...options.env },
34+
const bin = this.resolveBin();
35+
const proc = spawn(bin.command, [...bin.prefixArgs, ...argv], {
36+
env: { ...process.env, ...bin.env, ...options.env },
2837
cwd: options.cwd,
2938
signal: options.signal,
3039
});
@@ -46,8 +55,9 @@ export class CliRunner {
4655
}
4756

4857
spawnDetached(argv: readonly string[], options: CliRunOptions = {}): void {
49-
const proc = spawn(this.resolveBin(), [...argv], {
50-
env: { ...process.env, ...options.env },
58+
const bin = this.resolveBin();
59+
const proc = spawn(bin.command, [...bin.prefixArgs, ...argv], {
60+
env: { ...process.env, ...bin.env, ...options.env },
5161
cwd: options.cwd,
5262
detached: true,
5363
stdio: 'ignore',

src/wiring/commands.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as vscode from 'vscode';
22

33
import { SCRIPT_TEMPLATES } from '../data/templates';
44
import type { HtmlReportWriter } from '../reports/htmlReportWriter';
5-
import { CliRunner } from '../runners/cliRunner';
5+
import { CliRunner, type ResolvedBin } from '../runners/cliRunner';
66
import type { ReplayRunner } from '../runners/replayRunner';
77
import type { DeviceCatalog, DeviceEntry } from '../services/deviceCatalog';
88
import { parseSnapshotRefs, type SnapshotIndex } from '../services/snapshotIndex';
@@ -15,7 +15,7 @@ export interface CommandsDeps {
1515
readonly deviceCatalog: DeviceCatalog;
1616
readonly reportWriter: HtmlReportWriter;
1717
readonly snapshotIndex: SnapshotIndex;
18-
readonly resolveCliPath: () => string;
18+
readonly resolveCliPath: () => ResolvedBin;
1919
readonly sessionName: () => string;
2020
}
2121

@@ -149,7 +149,7 @@ function registerDeviceCommands(context: vscode.ExtensionContext, catalog: Devic
149149
function registerSnapshotCommands(
150150
context: vscode.ExtensionContext,
151151
snapshotIndex: SnapshotIndex,
152-
cliPath: () => string,
152+
cliPath: () => ResolvedBin,
153153
sessionName: () => string,
154154
): void {
155155
const cli = new CliRunner(cliPath);

src/wiring/services.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as path from 'node:path';
22
import * as vscode from 'vscode';
33

44
import { HtmlReportWriter } from '../reports/htmlReportWriter';
5+
import { type ResolvedBin } from '../runners/cliRunner';
56
import { ReplayRunner } from '../runners/replayRunner';
67
import { AdFileIndex } from '../services/adFileIndex';
78
import { AgentDeviceConfig } from '../services/config';
@@ -10,7 +11,7 @@ import { SnapshotIndex } from '../services/snapshotIndex';
1011

1112
export interface ExtensionServices {
1213
readonly config: AgentDeviceConfig;
13-
readonly resolveCliPath: () => string;
14+
readonly resolveCliPath: () => ResolvedBin;
1415
readonly runner: ReplayRunner;
1516
readonly fileIndex: AdFileIndex;
1617
readonly deviceCatalog: DeviceCatalog;
@@ -24,7 +25,12 @@ export function createServices(context: vscode.ExtensionContext): ExtensionServi
2425
context.subscriptions.push(output);
2526

2627
const config = new AgentDeviceConfig();
27-
const resolveCliPath = (): string => config.cliPathOverride() ?? resolveBundledCliPath(context);
28+
const resolveCliPath = (): ResolvedBin => {
29+
const override = config.cliPathOverride();
30+
return override !== undefined
31+
? { command: override, prefixArgs: [] }
32+
: resolveBundledCli(context);
33+
};
2834

2935
const runner = new ReplayRunner({
3036
cliPath: resolveCliPath,
@@ -56,7 +62,21 @@ export function createServices(context: vscode.ExtensionContext): ExtensionServi
5662
};
5763
}
5864

59-
function resolveBundledCliPath(context: vscode.ExtensionContext): string {
60-
const binName = process.platform === 'win32' ? 'agent-device.cmd' : 'agent-device';
61-
return path.join(context.extensionPath, 'node_modules', '.bin', binName);
65+
function resolveBundledCli(context: vscode.ExtensionContext): ResolvedBin {
66+
// The .vsix doesn't include npm's `.bin` shim directory, so spawn the CLI's
67+
// .mjs entrypoint directly via the host's Node runtime. In the extension
68+
// host, process.execPath is Electron — ELECTRON_RUN_AS_NODE makes it behave
69+
// as a plain Node interpreter for the child process.
70+
const scriptPath = path.join(
71+
context.extensionPath,
72+
'node_modules',
73+
'agent-device',
74+
'bin',
75+
'agent-device.mjs',
76+
);
77+
return {
78+
command: process.execPath,
79+
prefixArgs: [scriptPath],
80+
env: { ELECTRON_RUN_AS_NODE: '1' },
81+
};
6282
}

0 commit comments

Comments
 (0)