@phnx-labs/agents-cli 1.22.45 → 1.22.47
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 +303 -0
- package/LICENSE +102 -182
- package/README.md +54 -17
- package/dist/cli/command-registry.d.ts +2 -1
- package/dist/cli/command-registry.js +6 -2
- package/dist/commands/accounts.js +56 -12
- package/dist/commands/auth.d.ts +2 -0
- package/dist/commands/auth.js +229 -0
- package/dist/commands/browser-sessions-picker.js +9 -7
- package/dist/commands/browser.js +266 -15
- package/dist/commands/config.js +26 -4
- package/dist/commands/cost.d.ts +1 -1
- package/dist/commands/detach.d.ts +13 -0
- package/dist/commands/detach.js +11 -8
- package/dist/commands/exec.js +55 -38
- package/dist/commands/feed-watch.d.ts +2 -0
- package/dist/commands/feed-watch.js +28 -0
- package/dist/commands/feed.js +27 -0
- package/dist/commands/feedback.js +3 -19
- package/dist/commands/insights.d.ts +1 -1
- package/dist/commands/insights.js +2 -2
- package/dist/commands/lease.js +3 -3
- package/dist/commands/routines.js +1 -1
- package/dist/commands/sessions-picker-factory.d.ts +2 -1
- package/dist/commands/sessions-picker-factory.js +1 -1
- package/dist/commands/sessions-stop.d.ts +24 -0
- package/dist/commands/sessions-stop.js +66 -0
- package/dist/commands/sessions-trace.d.ts +82 -0
- package/dist/commands/sessions-trace.js +387 -0
- package/dist/commands/sessions.js +4 -0
- package/dist/commands/ssh.d.ts +15 -0
- package/dist/commands/ssh.js +194 -16
- package/dist/commands/teams.js +34 -2
- package/dist/commands/view.d.ts +2 -0
- package/dist/commands/view.js +19 -32
- package/dist/commands/watchdog.js +9 -14
- package/dist/lib/account-capabilities.d.ts +4 -0
- package/dist/lib/account-capabilities.js +13 -9
- package/dist/lib/account-registry.d.ts +2 -0
- package/dist/lib/account-registry.js +30 -2
- package/dist/lib/accounting/rotate.d.ts +9 -0
- package/dist/lib/accounting/rotate.js +12 -2
- package/dist/lib/accounting/usage.d.ts +92 -6
- package/dist/lib/accounting/usage.js +329 -75
- package/dist/lib/agent-modes.js +29 -1
- package/dist/lib/agent-spec/agents.d.ts +4 -3
- package/dist/lib/agent-spec/agents.js +20 -7
- package/dist/lib/analytics/mix-commands.d.ts +1 -1
- package/dist/lib/analytics/mix-commands.js +2 -2
- package/dist/lib/auth-health.d.ts +13 -3
- package/dist/lib/auth-health.js +50 -8
- package/dist/lib/browser/devices.d.ts +19 -0
- package/dist/lib/browser/devices.js +23 -0
- package/dist/lib/browser/ipc.d.ts +1 -1
- package/dist/lib/browser/ipc.js +33 -7
- package/dist/lib/browser/profiles.d.ts +106 -1
- package/dist/lib/browser/profiles.js +300 -17
- package/dist/lib/browser/remote-control.d.ts +26 -2
- package/dist/lib/browser/remote-control.js +33 -3
- package/dist/lib/browser/runtime-state.d.ts +9 -0
- package/dist/lib/browser/runtime-state.js +24 -8
- package/dist/lib/browser/service.d.ts +37 -0
- package/dist/lib/browser/service.js +78 -20
- package/dist/lib/browser/sessions-list.d.ts +1 -3
- package/dist/lib/browser/sessions-list.js +3 -16
- package/dist/lib/browser/types.d.ts +12 -1
- package/dist/lib/cli-resources.js +17 -15
- package/dist/lib/codex-policy.d.ts +11 -1
- package/dist/lib/codex-policy.js +20 -5
- package/dist/lib/config-keys.d.ts +4 -3
- package/dist/lib/config-keys.js +17 -7
- package/dist/lib/daemon/runner.js +15 -2
- package/dist/lib/daemon-ticks.js +2 -2
- package/dist/lib/device-config.d.ts +25 -8
- package/dist/lib/device-config.js +90 -17
- package/dist/lib/devices/config-migration.d.ts +7 -0
- package/dist/lib/devices/config-migration.js +67 -3
- package/dist/lib/devices/harness-inventory.js +20 -3
- package/dist/lib/devices/health.d.ts +27 -2
- package/dist/lib/devices/health.js +39 -9
- package/dist/lib/devices/interactive-host.d.ts +15 -0
- package/dist/lib/devices/interactive-host.js +53 -0
- package/dist/lib/devices/registry.d.ts +61 -4
- package/dist/lib/devices/registry.js +94 -38
- package/dist/lib/devices/stats-cache.d.ts +6 -0
- package/dist/lib/devices/stats-cache.js +10 -2
- package/dist/lib/exec.d.ts +25 -3
- package/dist/lib/exec.js +75 -22
- package/dist/lib/feed/answer.d.ts +29 -0
- package/dist/lib/feed/answer.js +145 -0
- package/dist/lib/feed/attention.d.ts +83 -0
- package/dist/lib/feed/attention.js +0 -0
- package/dist/lib/feed/feed.d.ts +109 -0
- package/dist/lib/feed/feed.js +154 -3
- package/dist/lib/feed/pr-status.d.ts +14 -0
- package/dist/lib/feed/pr-status.js +47 -0
- package/dist/lib/feed/watch.d.ts +59 -0
- package/dist/lib/feed/watch.js +180 -0
- package/dist/lib/fleet/capture.js +6 -0
- package/dist/lib/fleet/remote-login.js +8 -3
- package/dist/lib/fleet/types.d.ts +17 -0
- package/dist/lib/harness/adapter.d.ts +11 -0
- package/dist/lib/harness/adapters/claude.js +38 -24
- package/dist/lib/harness/adapters/codex.js +2 -4
- package/dist/lib/hooks/cache.js +8 -0
- package/dist/lib/hooks/match.d.ts +3 -0
- package/dist/lib/hooks/match.js +25 -0
- package/dist/lib/hosts/passthrough.js +16 -0
- package/dist/lib/hosts/registry.js +7 -0
- package/dist/lib/identity/client.d.ts +53 -0
- package/dist/lib/identity/client.js +106 -0
- package/dist/lib/identity/index.d.ts +115 -0
- package/dist/lib/identity/index.js +82 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +13 -0
- package/dist/lib/menubar/install-menubar.js +26 -2
- package/dist/lib/open-url.d.ts +60 -2
- package/dist/lib/open-url.js +167 -12
- package/dist/lib/probe.d.ts +8 -0
- package/dist/lib/probe.js +105 -0
- package/dist/lib/refresh-coordinator.d.ts +1 -1
- package/dist/lib/refresh-coordinator.js +1 -1
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +2 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +13 -1
- package/dist/lib/session/active.d.ts +67 -0
- package/dist/lib/session/active.js +69 -1
- package/dist/lib/session/db.js +4 -2
- package/dist/lib/session/discover.d.ts +2 -0
- package/dist/lib/session/discover.js +12 -6
- package/dist/lib/session/prompt.d.ts +35 -0
- package/dist/lib/session/prompt.js +56 -0
- package/dist/lib/session/remote/watch.d.ts +1 -1
- package/dist/lib/session/remote/watch.js +6 -1
- package/dist/lib/session/render.js +7 -3
- package/dist/lib/session/session-cache.js +5 -1
- package/dist/lib/session/trajectory-compare.d.ts +47 -0
- package/dist/lib/session/trajectory-compare.js +116 -0
- package/dist/lib/session/trajectory-html.d.ts +15 -0
- package/dist/lib/session/trajectory-html.js +750 -0
- package/dist/lib/session/trajectory-lineage.d.ts +92 -0
- package/dist/lib/session/trajectory-lineage.js +195 -0
- package/dist/lib/session/trajectory-text.d.ts +37 -0
- package/dist/lib/session/trajectory-text.js +277 -0
- package/dist/lib/session/trajectory.d.ts +102 -0
- package/dist/lib/session/trajectory.js +335 -0
- package/dist/lib/startup/command-registry.d.ts +5 -2
- package/dist/lib/startup/command-registry.js +8 -5
- package/dist/lib/state.d.ts +1 -1
- package/dist/lib/state.js +4 -2
- package/dist/lib/teams/agents.js +9 -0
- package/dist/lib/terminal/inject.d.ts +19 -0
- package/dist/lib/terminal/inject.js +19 -9
- package/dist/lib/tmux/session.d.ts +46 -13
- package/dist/lib/tmux/session.js +173 -27
- package/dist/lib/types.d.ts +16 -0
- package/dist/lib/usage-backoff.d.ts +2 -1
- package/dist/lib/usage-backoff.js +60 -19
- package/dist/lib/usage-refresh.d.ts +17 -3
- package/dist/lib/usage-refresh.js +57 -11
- package/dist/lib/view-types.d.ts +2 -2
- package/dist/lib/watchdog/log.d.ts +1 -1
- package/dist/lib/watchdog/log.js +1 -1
- package/dist/lib/watchdog/runner.d.ts +36 -45
- package/dist/lib/watchdog/runner.js +150 -119
- package/dist/lib/watchdog/service.d.ts +0 -1
- package/dist/lib/watchdog/service.js +0 -1
- package/dist/lib/watchdog/watchdog-agent.d.ts +38 -0
- package/dist/lib/watchdog/watchdog-agent.js +59 -0
- package/dist/lib/watchdog/watchdog.d.ts +11 -2
- package/dist/lib/watchdog/watchdog.js +37 -104
- package/package.json +2 -2
- package/dist/commands/usage.d.ts +0 -27
- package/dist/commands/usage.js +0 -96
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE place agents-cli talks to its account backend (Phoenix ID).
|
|
3
|
+
*
|
|
4
|
+
* Why a seam at all: the removed Prix-coupled layer (RUSH-2581) had no single
|
|
5
|
+
* entry point — the backend URL was hardcoded in five files and the session
|
|
6
|
+
* token was re-read from `~/.rush/user.yaml` by seven separate functions, so
|
|
7
|
+
* re-pointing identity meant editing a dozen call sites and rewriting error
|
|
8
|
+
* strings scattered through the tree. This module is the correction: one base
|
|
9
|
+
* URL, one token reader, one HTTP funnel, one error type. Commands import from
|
|
10
|
+
* here and nothing else.
|
|
11
|
+
*
|
|
12
|
+
* The shape mirrors the seams this repo already proved elsewhere —
|
|
13
|
+
* `SyncBackend` (`lib/secrets/sync-backend.ts`) and `CloudProvider`
|
|
14
|
+
* (`lib/cloud/types.ts`) — so a second identity backend, if one is ever
|
|
15
|
+
* needed, is a swap here rather than a sweep across commands.
|
|
16
|
+
*/
|
|
17
|
+
import * as fs from 'fs';
|
|
18
|
+
import * as path from 'path';
|
|
19
|
+
import { getRuntimeStateDir } from '../state.js';
|
|
20
|
+
/**
|
|
21
|
+
* Where the account backend lives. Config, never a literal at a call site.
|
|
22
|
+
*
|
|
23
|
+
* The default is the deployed Phoenix ID Worker. It is a `workers.dev` URL
|
|
24
|
+
* rather than a vanity hostname because no custom domain is attached yet — and
|
|
25
|
+
* a default naming an unregistered domain is worse than an ugly one: every
|
|
26
|
+
* `agents auth login` would fail DNS with nothing to point at.
|
|
27
|
+
*/
|
|
28
|
+
export const PHOENIX_ID_BASE = process.env.PHOENIX_ID_BASE ?? 'https://phoenix-id.muqsitnawaz.workers.dev';
|
|
29
|
+
/** Our own session file. agents-cli never reads another product's credentials. */
|
|
30
|
+
export function sessionFilePath() {
|
|
31
|
+
return path.join(getRuntimeStateDir(), 'phoenix-session.json');
|
|
32
|
+
}
|
|
33
|
+
export function readSession() {
|
|
34
|
+
try {
|
|
35
|
+
const raw = fs.readFileSync(sessionFilePath(), 'utf-8');
|
|
36
|
+
const parsed = JSON.parse(raw);
|
|
37
|
+
return parsed.access_token ? parsed : null;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function writeSession(session) {
|
|
44
|
+
const file = sessionFilePath();
|
|
45
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
46
|
+
fs.writeFileSync(file, JSON.stringify(session, null, 2), { mode: 0o600 });
|
|
47
|
+
}
|
|
48
|
+
export function clearSession() {
|
|
49
|
+
try {
|
|
50
|
+
fs.rmSync(sessionFilePath(), { force: true });
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Already gone: logging out twice is not an error.
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** An error carrying the server's status and message, so callers can branch on it. */
|
|
57
|
+
export class PhoenixApiError extends Error {
|
|
58
|
+
status;
|
|
59
|
+
constructor(message, status) {
|
|
60
|
+
super(message);
|
|
61
|
+
this.status = status;
|
|
62
|
+
this.name = 'PhoenixApiError';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** The single HTTP funnel. Every request to the account backend goes through here. */
|
|
66
|
+
export async function phoenixRequest(method, route, opts = {}) {
|
|
67
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
68
|
+
if (opts.auth !== false) {
|
|
69
|
+
const token = opts.token ?? readSession()?.access_token;
|
|
70
|
+
if (!token)
|
|
71
|
+
throw new PhoenixApiError("Not signed in. Run 'agents auth login'.", 401);
|
|
72
|
+
headers.Authorization = `Bearer ${token}`;
|
|
73
|
+
}
|
|
74
|
+
let response;
|
|
75
|
+
try {
|
|
76
|
+
response = await fetch(`${PHOENIX_ID_BASE}${route}`, {
|
|
77
|
+
method,
|
|
78
|
+
headers,
|
|
79
|
+
body: opts.body === undefined ? undefined : JSON.stringify(opts.body),
|
|
80
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? 15_000),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
85
|
+
throw new PhoenixApiError(`Could not reach the account service (${detail}).`, 0);
|
|
86
|
+
}
|
|
87
|
+
if (response.status === 204)
|
|
88
|
+
return undefined;
|
|
89
|
+
const text = await response.text();
|
|
90
|
+
let payload = null;
|
|
91
|
+
if (text) {
|
|
92
|
+
try {
|
|
93
|
+
payload = JSON.parse(text);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
payload = null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (!response.ok) {
|
|
100
|
+
const message = payload && typeof payload === 'object' && 'error' in payload
|
|
101
|
+
? String(payload.error)
|
|
102
|
+
: `${response.status} ${response.statusText}`;
|
|
103
|
+
throw new PhoenixApiError(message, response.status);
|
|
104
|
+
}
|
|
105
|
+
return payload;
|
|
106
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phoenix ID — the typed surface commands use. Every route the account backend
|
|
3
|
+
* exposes is a function here; no command builds a URL or reads a token itself.
|
|
4
|
+
*/
|
|
5
|
+
import { type PhoenixSession } from './client.js';
|
|
6
|
+
export { PHOENIX_ID_BASE, PhoenixApiError, clearSession, readSession, sessionFilePath, writeSession, type PhoenixSession, } from './client.js';
|
|
7
|
+
export interface DeviceAuthorization {
|
|
8
|
+
device_code: string;
|
|
9
|
+
user_code: string;
|
|
10
|
+
verification_uri: string;
|
|
11
|
+
verification_uri_complete: string;
|
|
12
|
+
expires_in: number;
|
|
13
|
+
interval: number;
|
|
14
|
+
}
|
|
15
|
+
export interface WhoAmI {
|
|
16
|
+
userId: string;
|
|
17
|
+
email: string;
|
|
18
|
+
valid: true;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* RFC 8628 poll outcomes. `pending` and `slow_down` are normal states of a
|
|
22
|
+
* login in progress, not failures — the server signals them through the error
|
|
23
|
+
* body, and this is where that wire detail stops.
|
|
24
|
+
*/
|
|
25
|
+
export type DevicePoll = {
|
|
26
|
+
status: 'authorized';
|
|
27
|
+
access_token: string;
|
|
28
|
+
user: {
|
|
29
|
+
email: string;
|
|
30
|
+
id: string;
|
|
31
|
+
};
|
|
32
|
+
} | {
|
|
33
|
+
status: 'pending';
|
|
34
|
+
} | {
|
|
35
|
+
status: 'slow_down';
|
|
36
|
+
} | {
|
|
37
|
+
status: 'expired';
|
|
38
|
+
} | {
|
|
39
|
+
status: 'denied';
|
|
40
|
+
};
|
|
41
|
+
export declare function startDeviceAuthorization(): Promise<DeviceAuthorization>;
|
|
42
|
+
export declare function pollDeviceToken(deviceCode: string): Promise<DevicePoll>;
|
|
43
|
+
export declare function fetchWhoAmI(token?: string): Promise<WhoAmI>;
|
|
44
|
+
export interface SpaceSummary {
|
|
45
|
+
id: string;
|
|
46
|
+
slug: string;
|
|
47
|
+
name: string;
|
|
48
|
+
organization_id: string | null;
|
|
49
|
+
owner_user_id: string;
|
|
50
|
+
invite_code?: string;
|
|
51
|
+
user_role: 'owner' | 'admin' | 'member';
|
|
52
|
+
created_at: string;
|
|
53
|
+
}
|
|
54
|
+
export interface SpaceMember {
|
|
55
|
+
user_id: string;
|
|
56
|
+
email: string;
|
|
57
|
+
name?: string;
|
|
58
|
+
avatar_url?: string;
|
|
59
|
+
role: 'owner' | 'admin' | 'member';
|
|
60
|
+
joined_at: string;
|
|
61
|
+
}
|
|
62
|
+
export interface SpaceInvite {
|
|
63
|
+
id: string;
|
|
64
|
+
space_id: string;
|
|
65
|
+
email: string;
|
|
66
|
+
role: 'admin' | 'member';
|
|
67
|
+
invite_code: string;
|
|
68
|
+
created_at: string;
|
|
69
|
+
}
|
|
70
|
+
export type CreateInviteResult = {
|
|
71
|
+
invited: true;
|
|
72
|
+
email: string;
|
|
73
|
+
role: string;
|
|
74
|
+
member_added: true;
|
|
75
|
+
} | {
|
|
76
|
+
invited: true;
|
|
77
|
+
email: string;
|
|
78
|
+
role: string;
|
|
79
|
+
invite_code: string;
|
|
80
|
+
member_added: false;
|
|
81
|
+
};
|
|
82
|
+
export declare const listSpaces: () => Promise<SpaceSummary[]>;
|
|
83
|
+
export declare const createSpace: (input: {
|
|
84
|
+
name: string;
|
|
85
|
+
slug: string;
|
|
86
|
+
}) => Promise<SpaceSummary>;
|
|
87
|
+
export declare const getSpace: (id: string) => Promise<SpaceSummary>;
|
|
88
|
+
export declare const listSpaceMembers: (id: string) => Promise<SpaceMember[]>;
|
|
89
|
+
export declare const createSpaceInvite: (id: string, input: {
|
|
90
|
+
email: string;
|
|
91
|
+
role: "admin" | "member";
|
|
92
|
+
}) => Promise<CreateInviteResult>;
|
|
93
|
+
export declare const listSpaceInvites: (id: string) => Promise<SpaceInvite[]>;
|
|
94
|
+
export declare const revokeSpaceInvite: (id: string, inviteId: string) => Promise<{
|
|
95
|
+
revoked: true;
|
|
96
|
+
}>;
|
|
97
|
+
export declare const updateSpaceMemberRole: (id: string, userId: string, role: "admin" | "member") => Promise<{
|
|
98
|
+
user_id: string;
|
|
99
|
+
role: string;
|
|
100
|
+
updated: true;
|
|
101
|
+
}>;
|
|
102
|
+
export declare const removeSpaceMember: (id: string, userId: string) => Promise<void>;
|
|
103
|
+
export declare const deleteSpace: (id: string) => Promise<void>;
|
|
104
|
+
export interface Subscription {
|
|
105
|
+
tierName?: string;
|
|
106
|
+
[key: string]: unknown;
|
|
107
|
+
}
|
|
108
|
+
export declare const fetchSubscription: (agent?: string) => Promise<Subscription>;
|
|
109
|
+
/** `Design Team` → `design-team`; the slug a space gets when the user gives only a name. */
|
|
110
|
+
export declare function slugify(name: string): string;
|
|
111
|
+
/** Resolve a space by id, slug, or name from a list the caller already fetched. */
|
|
112
|
+
export declare function resolveSpaceFromList(spaces: SpaceSummary[], ref?: string): SpaceSummary | null;
|
|
113
|
+
/** Resolve a member by email or user id from a list the caller already fetched. */
|
|
114
|
+
export declare function resolveMemberFromList(members: SpaceMember[], ref: string): SpaceMember | null;
|
|
115
|
+
export type { PhoenixSession as Session };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phoenix ID — the typed surface commands use. Every route the account backend
|
|
3
|
+
* exposes is a function here; no command builds a URL or reads a token itself.
|
|
4
|
+
*/
|
|
5
|
+
import { phoenixRequest, PhoenixApiError } from './client.js';
|
|
6
|
+
export { PHOENIX_ID_BASE, PhoenixApiError, clearSession, readSession, sessionFilePath, writeSession, } from './client.js';
|
|
7
|
+
export function startDeviceAuthorization() {
|
|
8
|
+
return phoenixRequest('POST', '/api/v1/auth/device/authorization', {
|
|
9
|
+
auth: false,
|
|
10
|
+
body: {},
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
export async function pollDeviceToken(deviceCode) {
|
|
14
|
+
try {
|
|
15
|
+
return await phoenixRequest('POST', '/api/v1/auth/device/token', {
|
|
16
|
+
auth: false,
|
|
17
|
+
body: {
|
|
18
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
19
|
+
device_code: deviceCode,
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
if (!(err instanceof PhoenixApiError))
|
|
25
|
+
throw err;
|
|
26
|
+
// The server encodes poll state in the error body (RFC 8628 §3.5).
|
|
27
|
+
if (err.message.includes('authorization_pending'))
|
|
28
|
+
return { status: 'pending' };
|
|
29
|
+
if (err.message.includes('slow_down'))
|
|
30
|
+
return { status: 'slow_down' };
|
|
31
|
+
if (err.message.includes('expired_token'))
|
|
32
|
+
return { status: 'expired' };
|
|
33
|
+
if (err.message.includes('access_denied'))
|
|
34
|
+
return { status: 'denied' };
|
|
35
|
+
throw err;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function fetchWhoAmI(token) {
|
|
39
|
+
return phoenixRequest('GET', '/api/v1/auth/me', { token });
|
|
40
|
+
}
|
|
41
|
+
export const listSpaces = () => phoenixRequest('GET', '/api/v1/spaces');
|
|
42
|
+
export const createSpace = (input) => phoenixRequest('POST', '/api/v1/spaces', { body: input });
|
|
43
|
+
export const getSpace = (id) => phoenixRequest('GET', `/api/v1/spaces/${encodeURIComponent(id)}`);
|
|
44
|
+
export const listSpaceMembers = (id) => phoenixRequest('GET', `/api/v1/spaces/${encodeURIComponent(id)}/members`);
|
|
45
|
+
export const createSpaceInvite = (id, input) => phoenixRequest('POST', `/api/v1/spaces/${encodeURIComponent(id)}/invites`, {
|
|
46
|
+
body: input,
|
|
47
|
+
});
|
|
48
|
+
export const listSpaceInvites = (id) => phoenixRequest('GET', `/api/v1/spaces/${encodeURIComponent(id)}/invites`);
|
|
49
|
+
export const revokeSpaceInvite = (id, inviteId) => phoenixRequest('DELETE', `/api/v1/spaces/${encodeURIComponent(id)}/invites/${encodeURIComponent(inviteId)}`);
|
|
50
|
+
export const updateSpaceMemberRole = (id, userId, role) => phoenixRequest('PATCH', `/api/v1/spaces/${encodeURIComponent(id)}/members/${encodeURIComponent(userId)}`, {
|
|
51
|
+
body: { role },
|
|
52
|
+
});
|
|
53
|
+
export const removeSpaceMember = (id, userId) => phoenixRequest('DELETE', `/api/v1/spaces/${encodeURIComponent(id)}/members/${encodeURIComponent(userId)}`);
|
|
54
|
+
export const deleteSpace = (id) => phoenixRequest('DELETE', `/api/v1/spaces/${encodeURIComponent(id)}`);
|
|
55
|
+
export const fetchSubscription = (agent = 'agents-cli') => phoenixRequest('GET', `/api/v1/billing/subscription?agent=${encodeURIComponent(agent)}`);
|
|
56
|
+
// ─── Helpers shared by the commands ──────────────────────────────────────────
|
|
57
|
+
/** `Design Team` → `design-team`; the slug a space gets when the user gives only a name. */
|
|
58
|
+
export function slugify(name) {
|
|
59
|
+
return name
|
|
60
|
+
.trim()
|
|
61
|
+
.toLowerCase()
|
|
62
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
63
|
+
.replace(/^-+|-+$/g, '')
|
|
64
|
+
.slice(0, 63);
|
|
65
|
+
}
|
|
66
|
+
/** Resolve a space by id, slug, or name from a list the caller already fetched. */
|
|
67
|
+
export function resolveSpaceFromList(spaces, ref) {
|
|
68
|
+
if (!ref)
|
|
69
|
+
return spaces.length === 1 ? spaces[0] : null;
|
|
70
|
+
const needle = ref.trim().toLowerCase();
|
|
71
|
+
return (spaces.find((s) => s.id === ref) ??
|
|
72
|
+
spaces.find((s) => s.slug.toLowerCase() === needle) ??
|
|
73
|
+
spaces.find((s) => s.name.toLowerCase() === needle) ??
|
|
74
|
+
null);
|
|
75
|
+
}
|
|
76
|
+
/** Resolve a member by email or user id from a list the caller already fetched. */
|
|
77
|
+
export function resolveMemberFromList(members, ref) {
|
|
78
|
+
const needle = ref.trim().toLowerCase();
|
|
79
|
+
return (members.find((m) => m.user_id === ref) ??
|
|
80
|
+
members.find((m) => m.email.toLowerCase() === needle) ??
|
|
81
|
+
null);
|
|
82
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -370,6 +370,19 @@ export interface PidStaleness {
|
|
|
370
370
|
* PREVIOUS binary — the exact condition `restartMenubarHelperAfterSwap` exists
|
|
371
371
|
* to fix. Millisecond epoch timestamps in, so the truth table is unit-testable
|
|
372
372
|
* without a live process or filesystem.
|
|
373
|
+
*
|
|
374
|
+
* The two timestamps do not have the same resolution. `pidStartTimeMs` parses
|
|
375
|
+
* `ps -o lstart`, which prints whole SECONDS, while the bundle mtime carries
|
|
376
|
+
* sub-second precision — and the restart this check exists to detect happens
|
|
377
|
+
* within a second of the swap that triggered it. A raw `<` therefore called a
|
|
378
|
+
* healthy just-restarted helper stale on essentially every upgrade: measured on
|
|
379
|
+
* zion at 1.22.46, pid start 1787441353000 vs bundle mtime 1787441353700, 700ms
|
|
380
|
+
* apart inside one second, reported as "running the OLD binary" and surfaced as
|
|
381
|
+
* `accessibilityHintNeeded` — telling the user to re-grant Accessibility after
|
|
382
|
+
* an upgrade that had already restarted the helper correctly. So the bundle
|
|
383
|
+
* mtime is truncated to the same whole second `ps` reports before comparing: a
|
|
384
|
+
* pid that started in the swap's own second is fresh, and a genuinely stale pid
|
|
385
|
+
* (a full second or more older) is still caught.
|
|
373
386
|
*/
|
|
374
387
|
export declare function isMenubarProcessStaleAgainstBundle(pidStartedAtMs: number, bundleMtimeMs: number): boolean;
|
|
375
388
|
/**
|
|
@@ -602,8 +602,19 @@ export function mayInstallMenubarHelper(opts) {
|
|
|
602
602
|
// No owner recorded yet (fresh or pre-`AGENTS_ENTRY` plist) — adopt it.
|
|
603
603
|
if (!opts.plistEntry)
|
|
604
604
|
return true;
|
|
605
|
+
// The recorded owner's entry path is gone from disk, so this install may adopt
|
|
606
|
+
// the helper — but a non-Developer-ID (ad-hoc/dev) source may NOT seize a
|
|
607
|
+
// healthy install this way. Recopying an ad-hoc bundle over the Developer-ID
|
|
608
|
+
// one poisons the shared Accessibility grant (an ad-hoc signature fails the
|
|
609
|
+
// grant's stored code requirement, so macOS revokes it and re-prompts on the
|
|
610
|
+
// next paste) and Gatekeeper then rejects the result as "damaged" (RUSH-2134).
|
|
611
|
+
// This does not strand a genuinely broken helper: escape (1) above
|
|
612
|
+
// (helperExecMissing / needsDevIdHeal) already lets ANY source repair a
|
|
613
|
+
// missing-or-ad-hoc install, so refusing here only declines to re-point the
|
|
614
|
+
// plist of a helper that is already present and working — the menu bar keeps
|
|
615
|
+
// running, nothing deadlocks.
|
|
605
616
|
if (!opts.ownerEntryExists)
|
|
606
|
-
return
|
|
617
|
+
return opts.sourceIsDeveloperId;
|
|
607
618
|
if (opts.installedVersion && opts.currentVersion) {
|
|
608
619
|
const versionOrder = compareVersions(opts.currentVersion, opts.installedVersion);
|
|
609
620
|
if (versionOrder > 0)
|
|
@@ -990,9 +1001,22 @@ export function getMenubarStatus() {
|
|
|
990
1001
|
* PREVIOUS binary — the exact condition `restartMenubarHelperAfterSwap` exists
|
|
991
1002
|
* to fix. Millisecond epoch timestamps in, so the truth table is unit-testable
|
|
992
1003
|
* without a live process or filesystem.
|
|
1004
|
+
*
|
|
1005
|
+
* The two timestamps do not have the same resolution. `pidStartTimeMs` parses
|
|
1006
|
+
* `ps -o lstart`, which prints whole SECONDS, while the bundle mtime carries
|
|
1007
|
+
* sub-second precision — and the restart this check exists to detect happens
|
|
1008
|
+
* within a second of the swap that triggered it. A raw `<` therefore called a
|
|
1009
|
+
* healthy just-restarted helper stale on essentially every upgrade: measured on
|
|
1010
|
+
* zion at 1.22.46, pid start 1787441353000 vs bundle mtime 1787441353700, 700ms
|
|
1011
|
+
* apart inside one second, reported as "running the OLD binary" and surfaced as
|
|
1012
|
+
* `accessibilityHintNeeded` — telling the user to re-grant Accessibility after
|
|
1013
|
+
* an upgrade that had already restarted the helper correctly. So the bundle
|
|
1014
|
+
* mtime is truncated to the same whole second `ps` reports before comparing: a
|
|
1015
|
+
* pid that started in the swap's own second is fresh, and a genuinely stale pid
|
|
1016
|
+
* (a full second or more older) is still caught.
|
|
993
1017
|
*/
|
|
994
1018
|
export function isMenubarProcessStaleAgainstBundle(pidStartedAtMs, bundleMtimeMs) {
|
|
995
|
-
return pidStartedAtMs < bundleMtimeMs;
|
|
1019
|
+
return pidStartedAtMs < Math.floor(bundleMtimeMs / 1000) * 1000;
|
|
996
1020
|
}
|
|
997
1021
|
/** Wall-clock start time of a live pid via `ps`, or null if it can't be read. */
|
|
998
1022
|
function pidStartTimeMs(pid) {
|
package/dist/lib/open-url.d.ts
CHANGED
|
@@ -1,2 +1,60 @@
|
|
|
1
|
-
/**
|
|
2
|
-
export
|
|
1
|
+
/** Where a "show the human this" call actually landed. */
|
|
2
|
+
export type ShowOutcome = {
|
|
3
|
+
via: 'profile';
|
|
4
|
+
profile: string;
|
|
5
|
+
tabId?: string;
|
|
6
|
+
} | {
|
|
7
|
+
via: 'os';
|
|
8
|
+
command: string;
|
|
9
|
+
} | {
|
|
10
|
+
via: 'none';
|
|
11
|
+
reason: string;
|
|
12
|
+
};
|
|
13
|
+
export interface ShowOptions {
|
|
14
|
+
/**
|
|
15
|
+
* Force the OS default handler, ignoring `browser.viewer`. This is the
|
|
16
|
+
* The programmatic escape hatch, for a caller that must use the user's own
|
|
17
|
+
* browser regardless of configuration. There is deliberately no CLI flag for
|
|
18
|
+
* it: `agents config set browser.viewer os` is the user-facing control.
|
|
19
|
+
*/
|
|
20
|
+
osBrowser?: boolean;
|
|
21
|
+
/** Explicit profile override, ahead of `browser.viewer`. */
|
|
22
|
+
profile?: string;
|
|
23
|
+
/** Injected opener so the OS branch is testable without spawning anything. */
|
|
24
|
+
spawnOpen?: (cmd: string, args: string[]) => boolean;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Launch a detached opener and report whether it actually started.
|
|
28
|
+
*
|
|
29
|
+
* Detection without blocking, which is the whole trick here. A bare detached
|
|
30
|
+
* `spawn` cannot tell success from "xdg-open is not installed" — it does not
|
|
31
|
+
* throw for a missing binary, it emits `error` asynchronously — so the failure
|
|
32
|
+
* branches of every caller were dead. But `spawnSync` is not the answer either:
|
|
33
|
+
* it waits for the child's whole lifetime, and `devices lease` opens a console
|
|
34
|
+
* and then immediately prompts for a pasted key, so a blocking open would stall
|
|
35
|
+
* that prompt behind the browser.
|
|
36
|
+
*
|
|
37
|
+
* Racing `spawn` against `error` gives both: Node emits `spawn` as soon as the
|
|
38
|
+
* child is successfully created (measured: 1ms, and it does NOT wait for exit),
|
|
39
|
+
* and `error` for ENOENT. We unref on success so the opener outlives us.
|
|
40
|
+
*/
|
|
41
|
+
export declare function trySpawn(cmd: string, args: string[]): Promise<boolean>;
|
|
42
|
+
/**
|
|
43
|
+
* Decide the viewer for this call. Exported for its own test — this is the one
|
|
44
|
+
* place the policy lives.
|
|
45
|
+
*
|
|
46
|
+
* Every fall back to the OS handler prints one stderr line naming why. A silent
|
|
47
|
+
* downgrade here is what made the original bug invisible for so long: the user
|
|
48
|
+
* had configured a profile and had no way to see that it was being ignored.
|
|
49
|
+
*/
|
|
50
|
+
export declare function resolveViewer(opts?: ShowOptions): Promise<'os' | {
|
|
51
|
+
profile: string;
|
|
52
|
+
}>;
|
|
53
|
+
/** Show a URL to the human at this machine. Never throws. */
|
|
54
|
+
export declare function showUrl(url: string, opts?: ShowOptions): Promise<ShowOutcome>;
|
|
55
|
+
/**
|
|
56
|
+
* Show a local file. Browser-renderable kinds go through {@link showUrl}; every
|
|
57
|
+
* other kind goes to the OS default APP, which for a screenshot or a recording
|
|
58
|
+
* is the right viewer.
|
|
59
|
+
*/
|
|
60
|
+
export declare function showFile(filePath: string, opts?: ShowOptions): Promise<ShowOutcome>;
|
package/dist/lib/open-url.js
CHANGED
|
@@ -1,19 +1,174 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* The one place that decides WHERE a URL or file is shown to the human.
|
|
3
|
+
*
|
|
4
|
+
* Two browsers exist on a machine like this: the OS default handler, and the
|
|
5
|
+
* profile `agents browser` drives. They are not interchangeable. The configured
|
|
6
|
+
* profile is where the fleet's logins accumulate — `agents browser profiles
|
|
7
|
+
* logins` lists them — so a page opened there is a page the user is already
|
|
8
|
+
* signed in for, and a login acquired there is inherited by every later agent.
|
|
9
|
+
* The OS handler has none of that.
|
|
10
|
+
*
|
|
11
|
+
* Before this seam existed, `agents browser navigate` honoured the configured
|
|
12
|
+
* profile and nothing else did: `fleet login`, `devices lease`, `feedback`, and
|
|
13
|
+
* the browser-session artifact opener each shelled straight to `open`/`xdg-open`,
|
|
14
|
+
* so every one of them landed in whatever the OS handler happened to be. This
|
|
15
|
+
* module replaces all of those call sites; do not add a sixth raw `open`.
|
|
16
|
+
*
|
|
17
|
+
* Never throws. A viewer that cannot be reached degrades to the OS handler with
|
|
18
|
+
* one stderr line naming the reason, and a total failure returns `via: 'none'`
|
|
19
|
+
* so the caller can print the URL rather than silently doing nothing.
|
|
5
20
|
*/
|
|
6
21
|
import { spawn } from 'child_process';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
22
|
+
import * as path from 'path';
|
|
23
|
+
import { pathToFileURL } from 'url';
|
|
24
|
+
/**
|
|
25
|
+
* Extensions a CDP tab renders at least as well as the OS default app.
|
|
26
|
+
*
|
|
27
|
+
* Deliberately narrow. `sessions-list.ts` EXT_KIND covers .png/.jpg/.webp/.pdf/
|
|
28
|
+
* .webm, and for those Preview and QuickTime are the better viewer — routing a
|
|
29
|
+
* screenshot into a browser tab is a downgrade, not a fix.
|
|
30
|
+
*/
|
|
31
|
+
const BROWSER_RENDERABLE = new Set(['.html', '.htm', '.svg', '.xhtml']);
|
|
32
|
+
async function osOpen(target, spawnOpen) {
|
|
33
|
+
const candidates = process.platform === 'darwin'
|
|
34
|
+
? [['open', [target]]]
|
|
35
|
+
: process.platform === 'win32'
|
|
36
|
+
? // `start` treats a lone quoted first argument as the window TITLE, so the
|
|
37
|
+
// empty title placeholder is required before the target. The three copies
|
|
38
|
+
// of this that predated the seam disagreed on it ('' vs '""').
|
|
39
|
+
[['cmd', ['/c', 'start', '', target]]]
|
|
40
|
+
: [
|
|
41
|
+
['xdg-open', [target]],
|
|
42
|
+
['gnome-open', [target]],
|
|
43
|
+
];
|
|
44
|
+
for (const [cmd, args] of candidates) {
|
|
45
|
+
if (spawnOpen) {
|
|
46
|
+
if (spawnOpen(cmd, args))
|
|
47
|
+
return { via: 'os', command: cmd };
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (await trySpawn(cmd, args))
|
|
51
|
+
return { via: 'os', command: cmd };
|
|
52
|
+
}
|
|
53
|
+
return { via: 'none', reason: 'no working OS opener on this platform' };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Launch a detached opener and report whether it actually started.
|
|
57
|
+
*
|
|
58
|
+
* Detection without blocking, which is the whole trick here. A bare detached
|
|
59
|
+
* `spawn` cannot tell success from "xdg-open is not installed" — it does not
|
|
60
|
+
* throw for a missing binary, it emits `error` asynchronously — so the failure
|
|
61
|
+
* branches of every caller were dead. But `spawnSync` is not the answer either:
|
|
62
|
+
* it waits for the child's whole lifetime, and `devices lease` opens a console
|
|
63
|
+
* and then immediately prompts for a pasted key, so a blocking open would stall
|
|
64
|
+
* that prompt behind the browser.
|
|
65
|
+
*
|
|
66
|
+
* Racing `spawn` against `error` gives both: Node emits `spawn` as soon as the
|
|
67
|
+
* child is successfully created (measured: 1ms, and it does NOT wait for exit),
|
|
68
|
+
* and `error` for ENOENT. We unref on success so the opener outlives us.
|
|
69
|
+
*/
|
|
70
|
+
export function trySpawn(cmd, args) {
|
|
71
|
+
return new Promise((resolve) => {
|
|
72
|
+
let settled = false;
|
|
73
|
+
const done = (ok) => {
|
|
74
|
+
if (settled)
|
|
75
|
+
return;
|
|
76
|
+
settled = true;
|
|
77
|
+
resolve(ok);
|
|
78
|
+
};
|
|
79
|
+
try {
|
|
80
|
+
const child = spawn(cmd, args, { stdio: 'ignore', detached: true });
|
|
81
|
+
child.on('error', () => done(false));
|
|
82
|
+
child.on('spawn', () => {
|
|
83
|
+
child.unref();
|
|
84
|
+
done(true);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
done(false);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Decide the viewer for this call. Exported for its own test — this is the one
|
|
94
|
+
* place the policy lives.
|
|
95
|
+
*
|
|
96
|
+
* Every fall back to the OS handler prints one stderr line naming why. A silent
|
|
97
|
+
* downgrade here is what made the original bug invisible for so long: the user
|
|
98
|
+
* had configured a profile and had no way to see that it was being ignored.
|
|
99
|
+
*/
|
|
100
|
+
export async function resolveViewer(opts = {}) {
|
|
101
|
+
if (opts.osBrowser)
|
|
102
|
+
return 'os';
|
|
103
|
+
const { getConfigValue } = await import('./device-config.js');
|
|
104
|
+
const configured = opts.profile ?? (getConfigValue('browser.viewer').value || undefined);
|
|
105
|
+
// Unset means "follow the profile agents drive" — the whole point is that a
|
|
106
|
+
// machine with a configured browser stops leaking pages to the OS handler.
|
|
107
|
+
const { getConfiguredDefaultProfileName, resolveProfileRef, getProfile, isProfileLaunchableHere } = await import('./browser/profiles.js');
|
|
108
|
+
const name = configured ?? getConfiguredDefaultProfileName();
|
|
109
|
+
if (!name)
|
|
110
|
+
return 'os';
|
|
111
|
+
if (name === 'os')
|
|
112
|
+
return 'os';
|
|
113
|
+
let resolved;
|
|
11
114
|
try {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
115
|
+
resolved = await resolveProfileRef(name);
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
console.error(`[viewer] ${name}: ${err instanceof Error ? err.message : String(err)} — using the OS browser.`);
|
|
119
|
+
return 'os';
|
|
120
|
+
}
|
|
121
|
+
if (!resolved) {
|
|
122
|
+
console.error(`[viewer] "${name}" does not resolve to a profile — using the OS browser.`);
|
|
123
|
+
return 'os';
|
|
15
124
|
}
|
|
16
|
-
|
|
17
|
-
|
|
125
|
+
const profile = await getProfile(resolved);
|
|
126
|
+
if (!profile) {
|
|
127
|
+
console.error(`[viewer] profile "${resolved}" is not configured — using the OS browser.`);
|
|
128
|
+
return 'os';
|
|
129
|
+
}
|
|
130
|
+
if (profile.browser === 'arc') {
|
|
131
|
+
// Arc exposes no CDP page targets and crashes on tab creation, so it can be
|
|
132
|
+
// a configured profile but never a drivable viewer.
|
|
133
|
+
console.error(`[viewer] "${resolved}" is Arc, which cannot be driven — using the OS browser.`);
|
|
134
|
+
return 'os';
|
|
135
|
+
}
|
|
136
|
+
if (!isProfileLaunchableHere(profile)) {
|
|
137
|
+
console.error(`[viewer] "${resolved}" cannot launch on this machine — using the OS browser.`);
|
|
138
|
+
return 'os';
|
|
139
|
+
}
|
|
140
|
+
return { profile: resolved };
|
|
141
|
+
}
|
|
142
|
+
/** Show a URL to the human at this machine. Never throws. */
|
|
143
|
+
export async function showUrl(url, opts = {}) {
|
|
144
|
+
const viewer = await resolveViewer(opts);
|
|
145
|
+
if (viewer === 'os')
|
|
146
|
+
return osOpen(url, opts.spawnOpen);
|
|
147
|
+
try {
|
|
148
|
+
const { sendIPCRequest } = await import('./browser/ipc.js');
|
|
149
|
+
// Deliberately does NOT auto-start the browser daemon. Showing a page is a
|
|
150
|
+
// side errand — `devices lease` opens a console and immediately prompts for
|
|
151
|
+
// a pasted key — so blocking that on a daemon cold start is a surprising
|
|
152
|
+
// multi-second stall. Daemon already up: use the viewer. Not up: the OS
|
|
153
|
+
// handler is the fast, correct answer.
|
|
154
|
+
const response = await sendIPCRequest({ action: 'show', url, profile: viewer.profile }, { autoStartDaemon: false });
|
|
155
|
+
if (response.ok)
|
|
156
|
+
return { via: 'profile', profile: viewer.profile, tabId: response.tabId };
|
|
157
|
+
console.error(`[viewer] ${viewer.profile}: ${response.error} — using the OS browser.`);
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
console.error(`[viewer] ${viewer.profile}: ${err instanceof Error ? err.message : String(err)} — using the OS browser.`);
|
|
161
|
+
}
|
|
162
|
+
return osOpen(url, opts.spawnOpen);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Show a local file. Browser-renderable kinds go through {@link showUrl}; every
|
|
166
|
+
* other kind goes to the OS default APP, which for a screenshot or a recording
|
|
167
|
+
* is the right viewer.
|
|
168
|
+
*/
|
|
169
|
+
export async function showFile(filePath, opts = {}) {
|
|
170
|
+
if (!BROWSER_RENDERABLE.has(path.extname(filePath).toLowerCase())) {
|
|
171
|
+
return osOpen(filePath, opts.spawnOpen);
|
|
18
172
|
}
|
|
173
|
+
return showUrl(pathToFileURL(filePath).href, opts);
|
|
19
174
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Async probe capturing stdout. Rejects on spawn error, non-zero exit, or
|
|
3
|
+
* timeout — matching the `execFileAsync` contract the version probe had — and
|
|
4
|
+
* reaps the probe's whole process group on every settle path.
|
|
5
|
+
*/
|
|
6
|
+
export declare function probeCapture(cmd: string, args: string[], timeoutMs: number): Promise<{
|
|
7
|
+
stdout: string;
|
|
8
|
+
}>;
|