@phnx-labs/agents-cli 1.20.87 → 1.20.89
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.
- package/CHANGELOG.md +323 -0
- package/README.md +12 -4
- package/dist/bin/agents +0 -0
- package/dist/commands/commands.js +7 -7
- package/dist/commands/doctor.d.ts +0 -19
- package/dist/commands/doctor.js +219 -305
- package/dist/commands/exec.js +7 -19
- package/dist/commands/factory.js +26 -2
- package/dist/commands/funnel.js +16 -1
- package/dist/commands/inspect.js +3 -5
- package/dist/commands/menubar.js +117 -34
- package/dist/commands/routines.js +25 -3
- package/dist/commands/secrets-rotate-passphrase.d.ts +17 -0
- package/dist/commands/secrets-rotate-passphrase.js +96 -0
- package/dist/commands/secrets.js +2 -0
- package/dist/commands/sessions.d.ts +7 -1
- package/dist/commands/sessions.js +40 -12
- package/dist/commands/ssh.js +3 -3
- package/dist/commands/usage.d.ts +3 -2
- package/dist/commands/usage.js +2 -9
- package/dist/commands/webhook.js +7 -2
- package/dist/lib/agents.d.ts +31 -1
- package/dist/lib/agents.js +55 -0
- package/dist/lib/command-skills.d.ts +10 -0
- package/dist/lib/command-skills.js +14 -0
- package/dist/lib/commands.js +28 -2
- package/dist/lib/daemon.d.ts +29 -0
- package/dist/lib/daemon.js +75 -6
- package/dist/lib/devices/doctor-findings.d.ts +167 -0
- package/dist/lib/devices/doctor-findings.js +893 -0
- package/dist/lib/devices/fleet-divergence.d.ts +22 -0
- package/dist/lib/devices/fleet-divergence.js +34 -10
- package/dist/lib/devices/fleet-inventory.d.ts +17 -6
- package/dist/lib/devices/fleet-inventory.js +56 -8
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/exec.d.ts +14 -3
- package/dist/lib/exec.js +41 -8
- package/dist/lib/factory/snapshot.d.ts +78 -0
- package/dist/lib/factory/snapshot.js +209 -0
- package/dist/lib/fs-atomic.d.ts +14 -1
- package/dist/lib/fs-atomic.js +35 -3
- package/dist/lib/funnel.d.ts +1 -0
- package/dist/lib/funnel.js +8 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +2 -2
- package/dist/lib/menubar/install-menubar.d.ts +53 -2
- package/dist/lib/menubar/install-menubar.js +183 -28
- package/dist/lib/platform/process.d.ts +2 -0
- package/dist/lib/platform/process.js +5 -3
- package/dist/lib/project-resources.js +34 -20
- package/dist/lib/resources.d.ts +8 -0
- package/dist/lib/resources.js +34 -1
- package/dist/lib/routines-placement.d.ts +2 -1
- package/dist/lib/routines-placement.js +8 -4
- package/dist/lib/routines.d.ts +57 -1
- package/dist/lib/routines.js +74 -1
- package/dist/lib/runner.d.ts +16 -1
- package/dist/lib/runner.js +58 -16
- package/dist/lib/sandbox.d.ts +2 -0
- package/dist/lib/sandbox.js +38 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/bundles.js +9 -34
- package/dist/lib/secrets/filestore.d.ts +152 -34
- package/dist/lib/secrets/filestore.js +676 -123
- package/dist/lib/secrets/rc-hygiene.d.ts +0 -6
- package/dist/lib/secrets/rc-hygiene.js +0 -24
- package/dist/lib/session/active.d.ts +6 -6
- package/dist/lib/session/active.js +6 -6
- package/dist/lib/session/discover.d.ts +5 -0
- package/dist/lib/session/discover.js +137 -1
- package/dist/lib/session/parse.d.ts +2 -0
- package/dist/lib/session/parse.js +76 -37
- package/dist/lib/session/remote-active.d.ts +4 -1
- package/dist/lib/session/remote-active.js +8 -2
- package/dist/lib/session/sync/agents.js +0 -0
- package/dist/lib/session/types.d.ts +1 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/session/viewing-in.d.ts +31 -0
- package/dist/lib/session/viewing-in.js +47 -0
- package/dist/lib/staleness/detectors/commands.js +14 -5
- package/dist/lib/staleness/types.d.ts +2 -0
- package/dist/lib/staleness/writers/commands.js +13 -7
- package/dist/lib/state.d.ts +17 -0
- package/dist/lib/state.js +30 -2
- package/dist/lib/triggers/handlers.d.ts +95 -0
- package/dist/lib/triggers/handlers.js +384 -0
- package/dist/lib/triggers/webhook.d.ts +10 -2
- package/dist/lib/triggers/webhook.js +65 -11
- package/dist/lib/usage.d.ts +72 -1
- package/dist/lib/usage.js +21 -27
- package/dist/lib/versions.js +30 -13
- package/package.json +1 -1
|
@@ -20,7 +20,7 @@ import { looksLikePath, toComparablePath, homeDir, needsWindowsShell, findExecut
|
|
|
20
20
|
import { getActiveSessions } from '../lib/session/active.js';
|
|
21
21
|
import { enumerateGhosttyTabs, assignGhosttyTabs } from '../lib/session/ghostty-tabs.js';
|
|
22
22
|
import { mapPanesToTargets, listClients } from '../lib/tmux/session.js';
|
|
23
|
-
import { resolveViewingIn } from '../lib/session/viewing-in.js';
|
|
23
|
+
import { resolveViewingIn, viewingInLabel } from '../lib/session/viewing-in.js';
|
|
24
24
|
import { machineId, normalizeHost } from '../lib/session/sync/config.js';
|
|
25
25
|
import { gatherRemoteActive, NO_FANOUT_ENV } from '../lib/session/remote-active.js';
|
|
26
26
|
import { gatherRemoteList, runOnPeer } from '../lib/session/remote-list.js';
|
|
@@ -442,6 +442,11 @@ function modelLabel(model) {
|
|
|
442
442
|
* (null when unknown) alongside the raw fields, so every active row is joinable.
|
|
443
443
|
* `project` uses the same derivation SessionMeta does — basename(cwd) (see
|
|
444
444
|
* discover.ts) — so the active view and the history view join identically.
|
|
445
|
+
*
|
|
446
|
+
* `viewingIn` flattens to the same display string the row renderer prints —
|
|
447
|
+
* `'codium tab 3'` / `'detached'` / null — so a consumer can tell a watched
|
|
448
|
+
* session from an orphaned one (its terminal died, the agent is still running)
|
|
449
|
+
* without re-implementing the tmux client lookup.
|
|
445
450
|
*/
|
|
446
451
|
export function serializeActiveSessionsForJson(sessions) {
|
|
447
452
|
return sessions.map((s) => ({
|
|
@@ -449,6 +454,7 @@ export function serializeActiveSessionsForJson(sessions) {
|
|
|
449
454
|
ticketId: s.ticket?.id ?? null,
|
|
450
455
|
project: s.cwd ? path.basename(s.cwd) : null,
|
|
451
456
|
prLink: s.pr?.url ?? null,
|
|
457
|
+
viewingIn: viewingInLabel(s) ?? null,
|
|
452
458
|
}));
|
|
453
459
|
}
|
|
454
460
|
/**
|
|
@@ -496,13 +502,9 @@ function locatorBadge(s) {
|
|
|
496
502
|
// For a tmux-hosted session, say which app+tab is looking at it right now
|
|
497
503
|
// (or that it's running detached). Only meaningful for tmux (the pane is the
|
|
498
504
|
// durable handle; the viewer is transient).
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
parts.push(chalk.gray(`viewing in ${
|
|
502
|
-
}
|
|
503
|
-
else {
|
|
504
|
-
parts.push(chalk.gray('detached'));
|
|
505
|
-
}
|
|
505
|
+
const label = viewingInLabel(s);
|
|
506
|
+
if (label)
|
|
507
|
+
parts.push(chalk.gray(label === 'detached' ? label : `viewing in ${label}`));
|
|
506
508
|
}
|
|
507
509
|
else if (p?.mux?.kind === 'screen') {
|
|
508
510
|
parts.push(chalk.green('screen'));
|
|
@@ -860,13 +862,33 @@ async function enrichLocalLocators(local) {
|
|
|
860
862
|
}
|
|
861
863
|
}
|
|
862
864
|
catch { /* non-fatal */ }
|
|
863
|
-
//
|
|
865
|
+
// One Ghostty enumeration shared across every socket's viewing-in resolve
|
|
866
|
+
// (a tmux client can be attached from a Ghostty tab).
|
|
867
|
+
await enrichTmuxLocators(local, await enumerateGhosttyTabsQuietly());
|
|
868
|
+
}
|
|
869
|
+
/** {@link enumerateGhosttyTabs}, best-effort — an osascript failure yields no surfaces. */
|
|
870
|
+
async function enumerateGhosttyTabsQuietly() {
|
|
871
|
+
try {
|
|
872
|
+
return await enumerateGhosttyTabs();
|
|
873
|
+
}
|
|
874
|
+
catch {
|
|
875
|
+
return [];
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* The tmux half of {@link enrichLocalLocators}: the `session:window.pane` attach
|
|
880
|
+
* target and "viewing in <app> tab N" / detached, one batched query per socket.
|
|
881
|
+
*
|
|
882
|
+
* Split out because it is the only locator the `--json` path can afford. It costs
|
|
883
|
+
* tmux queries and a `ps` read — no osascript — so scriptable output stays cheap
|
|
884
|
+
* while still answering the question a consumer actually needs: is anyone looking
|
|
885
|
+
* at this session, or is it running orphaned? Without `surfaces`, a Ghostty-attached
|
|
886
|
+
* client still resolves as attached, just without its tab number.
|
|
887
|
+
*/
|
|
888
|
+
async function enrichTmuxLocators(local, surfaces = []) {
|
|
864
889
|
try {
|
|
865
890
|
const tmux = local.filter(s => s.provenance?.mux?.kind === 'tmux' && s.provenance.mux.pane);
|
|
866
891
|
if (tmux.length > 0) {
|
|
867
|
-
// One Ghostty enumeration shared across every socket's viewing-in resolve
|
|
868
|
-
// (a tmux client can be attached from a Ghostty tab).
|
|
869
|
-
const surfaces = await enumerateGhosttyTabs();
|
|
870
892
|
const sockets = new Set(tmux.map(s => s.provenance.mux.socket));
|
|
871
893
|
for (const socket of sockets) {
|
|
872
894
|
const paneMap = await mapPanesToTargets(socket);
|
|
@@ -964,6 +986,11 @@ async function renderActiveSessions(asJson, waitingOnly = false, opts = {}) {
|
|
|
964
986
|
// present so a supervising agent or hook can poll it as a gate.
|
|
965
987
|
const sessions = waitingOnly ? merged.filter(s => s.status === 'input_required') : merged;
|
|
966
988
|
if (asJson) {
|
|
989
|
+
// Resolve who is watching each local tmux pane before serializing: `viewingIn`
|
|
990
|
+
// is how a consumer distinguishes a session someone is looking at from one
|
|
991
|
+
// running orphaned after its terminal died. tmux-only (no osascript) so the
|
|
992
|
+
// scriptable path stays cheap — see enrichTmuxLocators.
|
|
993
|
+
await enrichTmuxLocators(sessions.filter(s => !s.machine || s.machine === self));
|
|
967
994
|
process.stdout.write(JSON.stringify(serializeActiveSessionsForJson(sessions), null, 2) + '\n');
|
|
968
995
|
if (waitingOnly && sessions.length > 0)
|
|
969
996
|
process.exitCode = 1;
|
|
@@ -2368,6 +2395,7 @@ export function buildResumeCommand(session) {
|
|
|
2368
2395
|
case 'grok':
|
|
2369
2396
|
case 'kimi':
|
|
2370
2397
|
case 'droid':
|
|
2398
|
+
case 'cursor':
|
|
2371
2399
|
// Grok (and some others) sessions are captured artifacts, not resumable the same way.
|
|
2372
2400
|
return null;
|
|
2373
2401
|
}
|
package/dist/commands/ssh.js
CHANGED
|
@@ -313,7 +313,7 @@ function printFleetResults(results) {
|
|
|
313
313
|
if (failed > 0)
|
|
314
314
|
process.exitCode = 1;
|
|
315
315
|
}
|
|
316
|
-
function localHealthRow(self, stats) {
|
|
316
|
+
async function localHealthRow(self, stats) {
|
|
317
317
|
return {
|
|
318
318
|
name: self,
|
|
319
319
|
platform: process.platform === 'darwin' ? 'macos' : process.platform,
|
|
@@ -324,7 +324,7 @@ function localHealthRow(self, stats) {
|
|
|
324
324
|
orphans: countOrphans(),
|
|
325
325
|
// Local baseline inventory for cross-device divergence (RUSH-2027) — the
|
|
326
326
|
// yardstick every remote box is compared against.
|
|
327
|
-
inventory: collectLocalFleetInventory(process.cwd()),
|
|
327
|
+
inventory: await collectLocalFleetInventory(process.cwd()),
|
|
328
328
|
};
|
|
329
329
|
}
|
|
330
330
|
async function probeRemoteHealth(target) {
|
|
@@ -369,7 +369,7 @@ async function runFleetStatus(opts) {
|
|
|
369
369
|
// read from a fresh probe, not a stale tailscale snapshot (RUSH-1965).
|
|
370
370
|
// Best-effort: a registry write must never break the status render.
|
|
371
371
|
await writeReachability(collectReachabilityWriteBacks(reg, statsMap)).catch(() => { });
|
|
372
|
-
const rows = [localHealthRow(self, statsMap.get(self))];
|
|
372
|
+
const rows = [await localHealthRow(self, statsMap.get(self))];
|
|
373
373
|
const remoteTargets = remoteFleetTargets(planned, self)
|
|
374
374
|
.map((t) => ({
|
|
375
375
|
name: t.device.name,
|
package/dist/commands/usage.d.ts
CHANGED
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
* - codex: parsed from latest session log's rate_limits event
|
|
7
7
|
* - kimi: live Kimi Code /usages API call (cached for 2 minutes)
|
|
8
8
|
* - droid: live Factory billing/limits API call (cached for 2 minutes)
|
|
9
|
-
* -
|
|
10
|
-
*
|
|
9
|
+
* - grok: parsed from the latest local usage event
|
|
10
|
+
* - cursor: live Cursor usage API call (cached for 2 minutes)
|
|
11
|
+
* - others: marked as "not exposed by CLI"
|
|
11
12
|
*/
|
|
12
13
|
import type { Command } from 'commander';
|
|
13
14
|
import type { AgentId } from '../lib/types.js';
|
package/dist/commands/usage.js
CHANGED
|
@@ -2,14 +2,7 @@ import { addHostOption } from '../lib/hosts/option.js';
|
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { ALL_AGENT_IDS, AGENTS, getAccountInfo, agentLabel, resolveAgentName, formatAgentError, } from '../lib/agents.js';
|
|
4
4
|
import { listInstalledVersions, getGlobalDefault, getVersionHomePath } from '../lib/versions.js';
|
|
5
|
-
import { formatUsageSection, getUsageInfoForIdentity } from '../lib/usage.js';
|
|
6
|
-
/**
|
|
7
|
-
* Agents whose CLI surfaces usage data we can read today. Kept in sync with the
|
|
8
|
-
* live/last-seen sources `getUsageInfo` dispatches on in `../lib/usage.js`
|
|
9
|
-
* (claude, codex, kimi, droid) — an agent with a usage source but missing here
|
|
10
|
-
* would wrongly print "does not publish usage data" for a signed-in account.
|
|
11
|
-
*/
|
|
12
|
-
const USAGE_SUPPORTED = new Set(['claude', 'codex', 'kimi', 'droid']);
|
|
5
|
+
import { agentReportsUsage, formatUsageSection, getUsageInfoForIdentity } from '../lib/usage.js';
|
|
13
6
|
export function registerUsageCommand(program) {
|
|
14
7
|
addHostOption(program.command('usage [agent]'))
|
|
15
8
|
.description('Show rate-limit / quota usage per agent')
|
|
@@ -55,7 +48,7 @@ async function collectAgentUsage(agentId) {
|
|
|
55
48
|
// Plain name — color is applied only at text-render time (formatAgentUsage), so
|
|
56
49
|
// `--json` never emits ANSI escapes in `label` (e.g. under FORCE_COLOR=1).
|
|
57
50
|
const label = AGENTS[agentId].name;
|
|
58
|
-
if (!
|
|
51
|
+
if (!agentReportsUsage(agentId)) {
|
|
59
52
|
return { agent: agentId, label, status: 'unsupported' };
|
|
60
53
|
}
|
|
61
54
|
const versions = listInstalledVersions(agentId);
|
package/dist/commands/webhook.js
CHANGED
|
@@ -77,9 +77,14 @@ export function registerWebhookCommand(program) {
|
|
|
77
77
|
// Durable delivery dedup: replays survive a receiver restart (an
|
|
78
78
|
// in-memory store would forget every seen delivery on restart).
|
|
79
79
|
deliveryStore: createFileDeliveryStore(path.join(getRuntimeStateDir(), 'webhook', 'deliveries.json')),
|
|
80
|
-
onDelivery: (webhook, fired) => {
|
|
80
|
+
onDelivery: (webhook, fired, handlers) => {
|
|
81
|
+
const parts = [];
|
|
82
|
+
if (fired.length)
|
|
83
|
+
parts.push(`routines ${fired.map((f) => f.jobName).join(', ')}`);
|
|
84
|
+
if (handlers.length)
|
|
85
|
+
parts.push(`handlers ${handlers.map((h) => h.handlerName).join(', ')}`);
|
|
81
86
|
console.log(`${new Date().toISOString()} ${webhook.source}:${webhook.event} ` +
|
|
82
|
-
|
|
87
|
+
(parts.length ? `fired ${parts.join('; ')}` : 'no match'));
|
|
83
88
|
},
|
|
84
89
|
});
|
|
85
90
|
await waitForListening(server);
|
package/dist/lib/agents.d.ts
CHANGED
|
@@ -165,7 +165,7 @@ export declare function formatClaudeOrgLabel(orgType: string | null | undefined)
|
|
|
165
165
|
*/
|
|
166
166
|
export declare function accountOrgBadge(info?: Pick<AccountInfo, 'organizationType' | 'organizationName'> | null): string | null;
|
|
167
167
|
/** Agents whose local credential formats expose enough state for account selection. */
|
|
168
|
-
export declare const ACCOUNT_INSPECTION_AGENT_IDS: readonly ["claude", "codex", "gemini", "grok", "antigravity", "kimi", "droid", "opencode"];
|
|
168
|
+
export declare const ACCOUNT_INSPECTION_AGENT_IDS: readonly ["claude", "codex", "gemini", "cursor", "grok", "antigravity", "kimi", "droid", "opencode"];
|
|
169
169
|
/** Whether agents-cli can determine this agent's per-version sign-in state. */
|
|
170
170
|
export declare function supportsAccountInspection(agentId: AgentId): boolean;
|
|
171
171
|
/**
|
|
@@ -176,6 +176,36 @@ export declare function supportsAccountInspection(agentId: AgentId): boolean;
|
|
|
176
176
|
export declare function accountDisplayLabel(info?: Pick<AccountInfo, 'email' | 'accountId' | 'signedIn' | 'organizationType' | 'organizationName'> | null): string;
|
|
177
177
|
/** Return the email address associated with the agent's auth config, or null. */
|
|
178
178
|
export declare function getAccountEmail(agentId: AgentId, home?: string): Promise<string | null>;
|
|
179
|
+
/** Where an agent's credential file lives, split into the per-version copy and
|
|
180
|
+
* the active/global copy under the real HOME. */
|
|
181
|
+
export interface CredentialPresence {
|
|
182
|
+
/** The credential file exists inside the passed version home. */
|
|
183
|
+
perVersion: boolean;
|
|
184
|
+
/** The credential file exists under the active/global HOME (the one the login
|
|
185
|
+
* symlink actually targets), independent of the version home. */
|
|
186
|
+
active: boolean;
|
|
187
|
+
/** Whether agents-cli knows WHERE this agent's credential lives at all — i.e.
|
|
188
|
+
* the agent has an entry in {@link CREDENTIAL_FILE_SEGMENTS}. When false both
|
|
189
|
+
* probes are trivially false because there is nothing to look for, so absence
|
|
190
|
+
* is NOT evidence of a logout and no caller may treat it as provable.
|
|
191
|
+
*
|
|
192
|
+
* This is deliberately separate from `supportsAccountInspection`: the two
|
|
193
|
+
* registries move independently, and an agent has already been added to the
|
|
194
|
+
* inspection set without a credential path (cursor), which without this flag
|
|
195
|
+
* produced a false "logged out" critical for every installed version. */
|
|
196
|
+
knownLocation: boolean;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* File-presence probe for an agent's credential, split by location: whether it
|
|
200
|
+
* exists in a SPECIFIC version home (`perVersion`) and whether it exists under
|
|
201
|
+
* the active/global HOME (`active`). A logged-out claim is only *provable* when
|
|
202
|
+
* BOTH are absent — a version that merely lacks its own copy but shares the
|
|
203
|
+
* global login is signed in, not logged out. Pure file existence; no decrypt,
|
|
204
|
+
* no network, no keychain prompt. Agents with no inspectable identity return
|
|
205
|
+
* `{ perVersion: false, active: false }` and must NEVER yield a provable-logout
|
|
206
|
+
* claim (the caller gates on {@link supportsAccountInspection}).
|
|
207
|
+
*/
|
|
208
|
+
export declare function credentialPresence(agentId: AgentId, versionHome: string): CredentialPresence;
|
|
179
209
|
/** Decrypted contents of Droid's auth.v2.file (subset we consume). */
|
|
180
210
|
export interface DroidAuthPayload {
|
|
181
211
|
access_token?: string;
|
package/dist/lib/agents.js
CHANGED
|
@@ -1014,6 +1014,7 @@ export const ACCOUNT_INSPECTION_AGENT_IDS = [
|
|
|
1014
1014
|
'claude',
|
|
1015
1015
|
'codex',
|
|
1016
1016
|
'gemini',
|
|
1017
|
+
'cursor',
|
|
1017
1018
|
'grok',
|
|
1018
1019
|
'antigravity',
|
|
1019
1020
|
'kimi',
|
|
@@ -1077,6 +1078,60 @@ function resolveAccountCredentialPath(base, ...segments) {
|
|
|
1077
1078
|
}
|
|
1078
1079
|
return null;
|
|
1079
1080
|
}
|
|
1081
|
+
/**
|
|
1082
|
+
* The on-disk credential file(s) each account-inspectable agent authenticates
|
|
1083
|
+
* from, expressed as path segments under a home. Mirrors the exact files
|
|
1084
|
+
* {@link getAccountInfo} reads, so a presence check here matches what a real
|
|
1085
|
+
* launch would find. Each entry is a list of alternatives — the FIRST that
|
|
1086
|
+
* exists counts as present (claude writes either `.claude/.claude.json` under
|
|
1087
|
+
* the shimmed config dir or a home-level `.claude.json`). Agents whose login is
|
|
1088
|
+
* stored only in the OS keychain on some platforms (antigravity, and claude's
|
|
1089
|
+
* token) still expose a credential FILE — the presence of that file is the
|
|
1090
|
+
* signal we key off; its absence on BOTH the per-version home and the active
|
|
1091
|
+
* home is what makes a logged-out claim provable.
|
|
1092
|
+
*/
|
|
1093
|
+
const CREDENTIAL_FILE_SEGMENTS = {
|
|
1094
|
+
claude: [['.claude', '.claude.json'], ['.claude.json']],
|
|
1095
|
+
codex: [['.codex', 'auth.json']],
|
|
1096
|
+
gemini: [['.gemini', 'google_accounts.json']],
|
|
1097
|
+
grok: [['.grok', 'auth.json']],
|
|
1098
|
+
kimi: [['.kimi-code', 'credentials', 'kimi-code.json']],
|
|
1099
|
+
droid: [['.factory', 'auth.v2.file']],
|
|
1100
|
+
antigravity: [['.gemini', 'antigravity-cli', 'antigravity-oauth-token']],
|
|
1101
|
+
opencode: [['.local', 'share', 'opencode', 'auth.json']],
|
|
1102
|
+
};
|
|
1103
|
+
/** Whether an agent's credential file exists under a given home. */
|
|
1104
|
+
function credentialFileExistsUnder(agentId, home) {
|
|
1105
|
+
const alternatives = CREDENTIAL_FILE_SEGMENTS[agentId];
|
|
1106
|
+
if (!alternatives)
|
|
1107
|
+
return false;
|
|
1108
|
+
for (const segments of alternatives) {
|
|
1109
|
+
const p = path.join(home, ...segments);
|
|
1110
|
+
try {
|
|
1111
|
+
if (fs.existsSync(p))
|
|
1112
|
+
return true;
|
|
1113
|
+
}
|
|
1114
|
+
catch { /* unreadable */ }
|
|
1115
|
+
}
|
|
1116
|
+
return false;
|
|
1117
|
+
}
|
|
1118
|
+
/**
|
|
1119
|
+
* File-presence probe for an agent's credential, split by location: whether it
|
|
1120
|
+
* exists in a SPECIFIC version home (`perVersion`) and whether it exists under
|
|
1121
|
+
* the active/global HOME (`active`). A logged-out claim is only *provable* when
|
|
1122
|
+
* BOTH are absent — a version that merely lacks its own copy but shares the
|
|
1123
|
+
* global login is signed in, not logged out. Pure file existence; no decrypt,
|
|
1124
|
+
* no network, no keychain prompt. Agents with no inspectable identity return
|
|
1125
|
+
* `{ perVersion: false, active: false }` and must NEVER yield a provable-logout
|
|
1126
|
+
* claim (the caller gates on {@link supportsAccountInspection}).
|
|
1127
|
+
*/
|
|
1128
|
+
export function credentialPresence(agentId, versionHome) {
|
|
1129
|
+
const realHome = process.env.AGENTS_REAL_HOME || os.homedir();
|
|
1130
|
+
const perVersion = credentialFileExistsUnder(agentId, versionHome);
|
|
1131
|
+
const active = credentialFileExistsUnder(agentId, realHome);
|
|
1132
|
+
const knownLocation = (CREDENTIAL_FILE_SEGMENTS[agentId]?.length ?? 0) > 0;
|
|
1133
|
+
return { perVersion, active, knownLocation };
|
|
1134
|
+
}
|
|
1080
1135
|
/**
|
|
1081
1136
|
* Factory Droid stores its OAuth credential encrypted at ~/.factory/auth.v2.file
|
|
1082
1137
|
* (AES-256-GCM, format `ivB64:tagB64:ctB64`) with the 32-byte key base64-stored
|
|
@@ -4,6 +4,16 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { AgentId } from './types.js';
|
|
6
6
|
export declare function shouldInstallCommandAsSkill(agent: AgentId, version: string): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Agents whose native command files serve a separate surface while their CLI
|
|
9
|
+
* consumes the generated skill form. Keep this registry distinct from
|
|
10
|
+
* `shouldInstallCommandAsSkill`: these targets need both writes, not a format
|
|
11
|
+
* replacement.
|
|
12
|
+
*/
|
|
13
|
+
export declare const COMMAND_SKILL_DUAL_WRITE_TARGETS: {
|
|
14
|
+
cursor: true;
|
|
15
|
+
};
|
|
16
|
+
export declare function shouldAlsoInstallCommandAsSkill(agent: AgentId, version: string): boolean;
|
|
7
17
|
export declare function commandSkillName(commandName: string): string;
|
|
8
18
|
export declare function buildCommandSkillContent(commandName: string, sourcePath: string): string;
|
|
9
19
|
export declare function skillSourceExists(skillName: string, skillSourceDirs: Array<string | null | undefined>): boolean;
|
|
@@ -52,6 +52,20 @@ function readSkillCommandMarker(skillMdPath) {
|
|
|
52
52
|
export function shouldInstallCommandAsSkill(agent, version) {
|
|
53
53
|
return !supports(agent, 'commands', version).ok && supports(agent, 'skills', version).ok;
|
|
54
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Agents whose native command files serve a separate surface while their CLI
|
|
57
|
+
* consumes the generated skill form. Keep this registry distinct from
|
|
58
|
+
* `shouldInstallCommandAsSkill`: these targets need both writes, not a format
|
|
59
|
+
* replacement.
|
|
60
|
+
*/
|
|
61
|
+
export const COMMAND_SKILL_DUAL_WRITE_TARGETS = {
|
|
62
|
+
cursor: true,
|
|
63
|
+
};
|
|
64
|
+
export function shouldAlsoInstallCommandAsSkill(agent, version) {
|
|
65
|
+
return COMMAND_SKILL_DUAL_WRITE_TARGETS[agent] === true
|
|
66
|
+
&& supports(agent, 'commands', version).ok
|
|
67
|
+
&& supports(agent, 'skills', version).ok;
|
|
68
|
+
}
|
|
55
69
|
export function commandSkillName(commandName) {
|
|
56
70
|
return commandName;
|
|
57
71
|
}
|
package/dist/lib/commands.js
CHANGED
|
@@ -11,11 +11,12 @@ import * as path from 'path';
|
|
|
11
11
|
import * as yaml from 'yaml';
|
|
12
12
|
import { AGENTS, ensureCommandsDir, agentConfigDirName, resolveAgentName } from './agents.js';
|
|
13
13
|
import { capableAgents, isCapable, supports } from './capabilities.js';
|
|
14
|
+
import { isDirectoryDoc } from './resources.js';
|
|
14
15
|
import { markdownToToml } from './convert.js';
|
|
15
16
|
import { getCommandsDir, getUserCommandsDir, getEnabledExtraRepos, getProjectAgentsDir, getSkillsDir, getTrashCommandsDir } from './state.js';
|
|
16
17
|
import { getEffectiveHome, getVersionHomePath, listInstalledVersions, resolveVersion } from './versions.js';
|
|
17
18
|
import { discoverPlugins } from './plugins.js';
|
|
18
|
-
import { commandSkillMatches, installCommandSkillToVersion, listCommandSkillsInVersion, removeCommandSkillFromVersion, shouldInstallCommandAsSkill, } from './command-skills.js';
|
|
19
|
+
import { commandSkillMatches, installCommandSkillToVersion, listCommandSkillsInVersion, removeCommandSkillFromVersion, shouldAlsoInstallCommandAsSkill, shouldInstallCommandAsSkill, } from './command-skills.js';
|
|
19
20
|
import { installGooseCommandToVersion, listGooseCommandsInVersion, gooseCommandMatches, removeGooseCommandFromVersion, } from './goose-commands.js';
|
|
20
21
|
function compareVersions(a, b) {
|
|
21
22
|
const aParts = a.split('.').map((n) => parseInt(n, 10) || 0);
|
|
@@ -150,6 +151,8 @@ export function discoverCommands(repoPath) {
|
|
|
150
151
|
for (const file of fs.readdirSync(commandsDir)) {
|
|
151
152
|
if (file.endsWith('.md')) {
|
|
152
153
|
const name = file.replace('.md', '');
|
|
154
|
+
if (isDirectoryDoc('commands', name))
|
|
155
|
+
continue;
|
|
153
156
|
const sourcePath = path.join(commandsDir, file);
|
|
154
157
|
const metadata = parseCommandMetadata(sourcePath);
|
|
155
158
|
const validation = validateCommandMetadata(metadata, name);
|
|
@@ -211,6 +214,14 @@ export function installCommand(sourcePath, agentId, commandName, method = 'symli
|
|
|
211
214
|
const agent = AGENTS[agentId];
|
|
212
215
|
ensureCommandsDir(agentId);
|
|
213
216
|
const home = getEffectiveHome(agentId);
|
|
217
|
+
const installVersion = pinnedVersion ?? listInstalledVersions(agentId)[0] ?? '';
|
|
218
|
+
const alsoInstallAsSkill = shouldAlsoInstallCommandAsSkill(agentId, installVersion);
|
|
219
|
+
if (alsoInstallAsSkill) {
|
|
220
|
+
const installed = installCommandSkillToVersion(path.join(home, agentConfigDirName(agentId)), commandName, sourcePath, [getSkillsDir(), ...getEnabledExtraRepos().map((repo) => path.join(repo.dir, 'skills'))]);
|
|
221
|
+
if (!installed.success) {
|
|
222
|
+
return { path: '', method: 'copy', error: installed.error, warnings: validation.warnings };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
214
225
|
// Goose: a slash command is a recipe YAML registered in config.yaml, not a
|
|
215
226
|
// native command file under commandsSubdir.
|
|
216
227
|
if (agentId === 'goose') {
|
|
@@ -401,6 +412,11 @@ export function installCommandToVersion(agent, version, commandName, method = 'c
|
|
|
401
412
|
...getEnabledExtraRepos().map((repo) => path.join(repo.dir, 'skills')),
|
|
402
413
|
]);
|
|
403
414
|
}
|
|
415
|
+
if (shouldAlsoInstallCommandAsSkill(agent, version)) {
|
|
416
|
+
const installed = installCommandSkillToVersion(agentDir, commandName, sourcePath, [getSkillsDir(), ...getEnabledExtraRepos().map((repo) => path.join(repo.dir, 'skills'))]);
|
|
417
|
+
if (!installed.success)
|
|
418
|
+
return installed;
|
|
419
|
+
}
|
|
404
420
|
// Goose: a slash command is a recipe YAML registered in config.yaml, not a
|
|
405
421
|
// native command file. Write the recipe + slash_commands entry.
|
|
406
422
|
if (agent === 'goose') {
|
|
@@ -441,6 +457,11 @@ export function removeCommandFromVersion(agent, version, commandName) {
|
|
|
441
457
|
if (shouldInstallCommandAsSkill(agent, version)) {
|
|
442
458
|
return removeCommandSkillFromVersion(agentDir, commandName);
|
|
443
459
|
}
|
|
460
|
+
if (shouldAlsoInstallCommandAsSkill(agent, version)) {
|
|
461
|
+
const removed = removeCommandSkillFromVersion(agentDir, commandName);
|
|
462
|
+
if (!removed.success)
|
|
463
|
+
return removed;
|
|
464
|
+
}
|
|
444
465
|
if (agent === 'goose') {
|
|
445
466
|
const trashDir = path.join(getTrashCommandsDir(), agent, version, commandName);
|
|
446
467
|
return removeGooseCommandFromVersion(versionHome, commandName, trashDir);
|
|
@@ -676,7 +697,12 @@ export function listCentralCommands() {
|
|
|
676
697
|
if (!fs.existsSync(dir))
|
|
677
698
|
continue;
|
|
678
699
|
for (const f of fs.readdirSync(dir).filter((f) => f.endsWith('.md'))) {
|
|
679
|
-
|
|
700
|
+
const name = f.replace('.md', '');
|
|
701
|
+
// A directory's README/AGENTS/CLAUDE/GEMINI documents the dir, it is not a
|
|
702
|
+
// command. Without this the picker offers a name resolveResource refuses.
|
|
703
|
+
if (isDirectoryDoc('commands', name))
|
|
704
|
+
continue;
|
|
705
|
+
seen.add(name);
|
|
680
706
|
}
|
|
681
707
|
}
|
|
682
708
|
return Array.from(seen);
|
package/dist/lib/daemon.d.ts
CHANGED
|
@@ -86,6 +86,23 @@ export declare function log(level: string, message: string): void;
|
|
|
86
86
|
* anchoring failed (logged, non-fatal).
|
|
87
87
|
*/
|
|
88
88
|
export declare function anchorDaemonCwd(): string | null;
|
|
89
|
+
/**
|
|
90
|
+
* Surface, at the daemon's OWN startup, that it was launched from an ephemeral
|
|
91
|
+
* root that will wedge it if the directory is removed. This is the runtime
|
|
92
|
+
* companion to the launch-time check in validateDaemonBinary (which only runs
|
|
93
|
+
* when the daemon is *spawned* via getDaemonLaunch): a direct
|
|
94
|
+
* `agents __daemon-run` from a temp or worktree build — e.g. a review/verify
|
|
95
|
+
* checkout under /tmp — never passes through that path, so without this the
|
|
96
|
+
* wedge risk stays invisible until jobs start ENOENT-ing on their dynamic
|
|
97
|
+
* imports. Best-effort and non-fatal; the cwd is already handled by
|
|
98
|
+
* anchorDaemonCwd, but a deleted module root can only be flagged, not repaired.
|
|
99
|
+
*
|
|
100
|
+
* `resolveBin` is injectable (defaults to getAgentsBinPath) so the wiring — the
|
|
101
|
+
* predicate call, the WARN, and the non-fatal guard around a throwing resolver —
|
|
102
|
+
* is testable. Returns the warning message it logged, or null when the launch
|
|
103
|
+
* root is stable (or could not be resolved).
|
|
104
|
+
*/
|
|
105
|
+
export declare function warnEphemeralDaemonRoot(resolveBin?: () => string): string | null;
|
|
89
106
|
export declare function runDaemon(): Promise<void>;
|
|
90
107
|
/**
|
|
91
108
|
* Write a launchd plist or systemd unit with owner-only permissions atomically.
|
|
@@ -175,6 +192,18 @@ export declare function getAgentsInvocation(subArgs: string[], agentsBin?: strin
|
|
|
175
192
|
command: string;
|
|
176
193
|
args: string[];
|
|
177
194
|
};
|
|
195
|
+
/**
|
|
196
|
+
* A daemon binary living under an ephemeral path — a git worktree, or a temp
|
|
197
|
+
* directory (`/tmp`, `/var/folders`, `/dev/shm`) — is a latent wedge. The daemon
|
|
198
|
+
* is long-lived but resolves its own job modules by dynamic `import()` rooted at
|
|
199
|
+
* this entry (getAgentsBinPath → process.argv[1]). If that directory is later
|
|
200
|
+
* removed (`git worktree remove`, a `/tmp` cleanup, a review/verify checkout
|
|
201
|
+
* teardown) the running daemon keeps ENOENT-ing on every job it loads —
|
|
202
|
+
* `anchorDaemonCwd` rescues the cwd, but nothing can re-root a deleted module
|
|
203
|
+
* tree. Returns a human phrase naming the ephemeral kind, or null for a stable
|
|
204
|
+
* install path (version home, a global npm prefix, a normal source checkout).
|
|
205
|
+
*/
|
|
206
|
+
export declare function describeEphemeralDaemonRoot(binPath: string): string | null;
|
|
178
207
|
export declare function validateDaemonBinary(binPath: string): {
|
|
179
208
|
warnings: string[];
|
|
180
209
|
};
|
package/dist/lib/daemon.js
CHANGED
|
@@ -321,6 +321,39 @@ export function anchorDaemonCwd() {
|
|
|
321
321
|
return null;
|
|
322
322
|
}
|
|
323
323
|
}
|
|
324
|
+
/**
|
|
325
|
+
* Surface, at the daemon's OWN startup, that it was launched from an ephemeral
|
|
326
|
+
* root that will wedge it if the directory is removed. This is the runtime
|
|
327
|
+
* companion to the launch-time check in validateDaemonBinary (which only runs
|
|
328
|
+
* when the daemon is *spawned* via getDaemonLaunch): a direct
|
|
329
|
+
* `agents __daemon-run` from a temp or worktree build — e.g. a review/verify
|
|
330
|
+
* checkout under /tmp — never passes through that path, so without this the
|
|
331
|
+
* wedge risk stays invisible until jobs start ENOENT-ing on their dynamic
|
|
332
|
+
* imports. Best-effort and non-fatal; the cwd is already handled by
|
|
333
|
+
* anchorDaemonCwd, but a deleted module root can only be flagged, not repaired.
|
|
334
|
+
*
|
|
335
|
+
* `resolveBin` is injectable (defaults to getAgentsBinPath) so the wiring — the
|
|
336
|
+
* predicate call, the WARN, and the non-fatal guard around a throwing resolver —
|
|
337
|
+
* is testable. Returns the warning message it logged, or null when the launch
|
|
338
|
+
* root is stable (or could not be resolved).
|
|
339
|
+
*/
|
|
340
|
+
export function warnEphemeralDaemonRoot(resolveBin = getAgentsBinPath) {
|
|
341
|
+
try {
|
|
342
|
+
const bin = resolveBin();
|
|
343
|
+
const ephemeralRoot = describeEphemeralDaemonRoot(bin);
|
|
344
|
+
if (!ephemeralRoot)
|
|
345
|
+
return null;
|
|
346
|
+
const message = `Daemon launched from ${ephemeralRoot} (${bin}); if that directory is removed, ` +
|
|
347
|
+
`every routine will fail with ENOENT on its module imports. Run the daemon from the ` +
|
|
348
|
+
`globally installed binary instead (npm i -g @phnx-labs/agents-cli), then restart it.`;
|
|
349
|
+
log('WARN', message);
|
|
350
|
+
return message;
|
|
351
|
+
}
|
|
352
|
+
catch (err) {
|
|
353
|
+
log('WARN', `Could not check daemon launch root: ${err.message}`);
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
324
357
|
export async function runDaemon() {
|
|
325
358
|
// Single-instance guard: a direct `agents __daemon-run` (manual, or a
|
|
326
359
|
// service-manager restart racing a live predecessor) must not clobber a
|
|
@@ -334,6 +367,7 @@ export async function runDaemon() {
|
|
|
334
367
|
}
|
|
335
368
|
log('INFO', `Daemon started (PID: ${process.pid})`);
|
|
336
369
|
anchorDaemonCwd();
|
|
370
|
+
warnEphemeralDaemonRoot();
|
|
337
371
|
// The daemon holds NO Claude credential of its own. Routine runs authenticate
|
|
338
372
|
// exactly like an interactive `agents run`: through the per-account
|
|
339
373
|
// CLAUDE_CONFIG_DIR login on this device (its own auto-refreshing
|
|
@@ -880,7 +914,7 @@ ${[launch.command, ...launch.args].map((arg) => ` <string>${xmlEscape(arg)}</
|
|
|
880
914
|
<key>EnvironmentVariables</key>
|
|
881
915
|
<dict>
|
|
882
916
|
<key>PATH</key>
|
|
883
|
-
<string>${
|
|
917
|
+
<string>${daemonPathValue(agentsBin, ['/usr/local/bin', '/usr/bin', '/bin', '/opt/homebrew/bin', `${os.homedir()}/.bun/bin`])}</string>
|
|
884
918
|
</dict>
|
|
885
919
|
</dict>
|
|
886
920
|
</plist>`;
|
|
@@ -909,7 +943,7 @@ Type=simple
|
|
|
909
943
|
ExecStart=${execStart}
|
|
910
944
|
Restart=always
|
|
911
945
|
RestartSec=10
|
|
912
|
-
Environment=PATH=${
|
|
946
|
+
Environment=PATH=${daemonPathValue(agentsBin, ['/usr/local/bin', '/usr/bin', '/bin'])}
|
|
913
947
|
|
|
914
948
|
[Install]
|
|
915
949
|
WantedBy=default.target`;
|
|
@@ -1074,6 +1108,21 @@ export function getDaemonLaunch(agentsBin = getAgentsBinPath()) {
|
|
|
1074
1108
|
function daemonNodeBinDir() {
|
|
1075
1109
|
return path.dirname(process.execPath);
|
|
1076
1110
|
}
|
|
1111
|
+
/**
|
|
1112
|
+
* The full PATH value the daemon service manifest pins, in order: the Node runtime
|
|
1113
|
+
* dir (so the shim's shebang and any child resolve the exact installing Node — see
|
|
1114
|
+
* {@link daemonNodeBinDir}), then the directory of the `agents` shim itself, then
|
|
1115
|
+
* the platform's system dirs. The shim's own dir matters because a scheduled
|
|
1116
|
+
* `command` routine shells out to the bare name `agents`
|
|
1117
|
+
* (`/bin/sh -c 'agents watchdog --nudge'`): when the shim lives outside the Node
|
|
1118
|
+
* bin dir — a `~/.local/bin` global install, a separate npm prefix — a PATH
|
|
1119
|
+
* carrying only the Node dir resolves `agents` to nothing and every routine dies
|
|
1120
|
+
* with `exit 127`. Deduped across the whole list, so a Node/shim dir that already
|
|
1121
|
+
* appears among the system dirs (e.g. a `/usr/local/bin` install) never doubles.
|
|
1122
|
+
*/
|
|
1123
|
+
function daemonPathValue(agentsBin, systemDirs) {
|
|
1124
|
+
return [...new Set([daemonNodeBinDir(), path.dirname(agentsBin), ...systemDirs])].join(':');
|
|
1125
|
+
}
|
|
1077
1126
|
/**
|
|
1078
1127
|
* Build the argv to relaunch the `agents` CLI with the given subcommand args.
|
|
1079
1128
|
*
|
|
@@ -1091,15 +1140,35 @@ function daemonNodeBinDir() {
|
|
|
1091
1140
|
export function getAgentsInvocation(subArgs, agentsBin = getAgentsBinPath()) {
|
|
1092
1141
|
return getCliLaunch(subArgs, agentsBin);
|
|
1093
1142
|
}
|
|
1143
|
+
/**
|
|
1144
|
+
* A daemon binary living under an ephemeral path — a git worktree, or a temp
|
|
1145
|
+
* directory (`/tmp`, `/var/folders`, `/dev/shm`) — is a latent wedge. The daemon
|
|
1146
|
+
* is long-lived but resolves its own job modules by dynamic `import()` rooted at
|
|
1147
|
+
* this entry (getAgentsBinPath → process.argv[1]). If that directory is later
|
|
1148
|
+
* removed (`git worktree remove`, a `/tmp` cleanup, a review/verify checkout
|
|
1149
|
+
* teardown) the running daemon keeps ENOENT-ing on every job it loads —
|
|
1150
|
+
* `anchorDaemonCwd` rescues the cwd, but nothing can re-root a deleted module
|
|
1151
|
+
* tree. Returns a human phrase naming the ephemeral kind, or null for a stable
|
|
1152
|
+
* install path (version home, a global npm prefix, a normal source checkout).
|
|
1153
|
+
*/
|
|
1154
|
+
export function describeEphemeralDaemonRoot(binPath) {
|
|
1155
|
+
if (/[/\\]\.agents[/\\]worktrees[/\\]/.test(binPath))
|
|
1156
|
+
return 'a git worktree';
|
|
1157
|
+
if (/^(?:\/private)?\/tmp[/\\]|^(?:\/private)?\/var\/folders[/\\]|^\/dev\/shm[/\\]/.test(binPath)) {
|
|
1158
|
+
return 'a temporary directory';
|
|
1159
|
+
}
|
|
1160
|
+
return null;
|
|
1161
|
+
}
|
|
1094
1162
|
export function validateDaemonBinary(binPath) {
|
|
1095
1163
|
const warnings = [];
|
|
1096
1164
|
if (BUN_VIRTUAL_ROOT.test(binPath)) {
|
|
1097
1165
|
throw new Error(`Refusing to supervise daemon: resolved binary is a bun virtual path (${binPath}). ` +
|
|
1098
1166
|
`Install agents globally (npm i -g @phnx-labs/agents-cli) and restart.`);
|
|
1099
1167
|
}
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1168
|
+
const ephemeralRoot = describeEphemeralDaemonRoot(binPath);
|
|
1169
|
+
if (ephemeralRoot) {
|
|
1170
|
+
warnings.push(`Warning: daemon binary is inside ${ephemeralRoot} (${binPath}). ` +
|
|
1171
|
+
`Deleting it will wedge the daemon. Use the globally installed binary instead.`);
|
|
1103
1172
|
}
|
|
1104
1173
|
if (!fs.existsSync(binPath) && !/\.(c|m)?js$/.test(binPath)) {
|
|
1105
1174
|
warnings.push(`Warning: daemon binary does not exist on disk (${binPath}).`);
|
|
@@ -1117,7 +1186,7 @@ export function startDetached(opts = {}) {
|
|
|
1117
1186
|
// and a console-close event tears it down when the launcher exits (#556).
|
|
1118
1187
|
const child = spawn(command, args, {
|
|
1119
1188
|
stdio: ['ignore', logFd, logFd],
|
|
1120
|
-
...backgroundSpawnOptions({ fdStdio: true }),
|
|
1189
|
+
...backgroundSpawnOptions({ cwd: os.homedir(), fdStdio: true }),
|
|
1121
1190
|
env: opts.env ?? process.env,
|
|
1122
1191
|
});
|
|
1123
1192
|
// A failed spawn (ENOENT/EACCES) emits 'error' asynchronously; without a
|