@phnx-labs/agents-cli 1.22.115 → 1.22.116
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 +149 -14
- package/README.md +1 -1
- package/dist/commands/browser.js +11 -0
- package/dist/commands/exec.js +212 -141
- package/dist/commands/feed.js +65 -14
- package/dist/commands/sessions-picker.d.ts +11 -0
- package/dist/commands/sessions-picker.js +88 -7
- package/dist/commands/sessions.d.ts +21 -2
- package/dist/commands/sessions.js +157 -3
- package/dist/commands/setup-secrets.d.ts +2 -2
- package/dist/commands/setup-term.d.ts +24 -0
- package/dist/commands/setup-term.js +70 -0
- package/dist/commands/setup.d.ts +1 -1
- package/dist/commands/setup.js +12 -4
- package/dist/commands/ssh.js +1 -69
- package/dist/lib/accounting/rotate.d.ts +63 -1
- package/dist/lib/accounting/rotate.js +56 -0
- package/dist/lib/accounts/add.js +2 -2
- package/dist/lib/accounts/slots.js +32 -2
- package/dist/lib/answer-router.d.ts +11 -2
- package/dist/lib/answer-router.js +26 -2
- package/dist/lib/auth-mint.d.ts +5 -4
- package/dist/lib/auth-mint.js +4 -3
- package/dist/lib/browser/drivers/arc.d.ts +1 -1
- package/dist/lib/browser/service.d.ts +10 -0
- package/dist/lib/browser/service.js +208 -36
- package/dist/lib/browser/types.d.ts +18 -0
- package/dist/lib/config-keys.d.ts +1 -1
- package/dist/lib/config-keys.js +5 -0
- package/dist/lib/device-config.js +61 -0
- package/dist/lib/devices/doctor-findings.js +2 -6
- package/dist/lib/feed/answer.d.ts +153 -4
- package/dist/lib/feed/answer.js +716 -105
- package/dist/lib/feed/feed.d.ts +61 -1
- package/dist/lib/feed/feed.js +226 -14
- package/dist/lib/feed/hub-server.d.ts +58 -3
- package/dist/lib/feed/hub-server.js +306 -54
- package/dist/lib/feed/pr-status.d.ts +8 -0
- package/dist/lib/feed/pr-status.js +9 -1
- package/dist/lib/feed-outcome.d.ts +1 -1
- package/dist/lib/feed-outcome.js +9 -2
- package/dist/lib/feed-policy.js +9 -3
- package/dist/lib/fleet/auth-sync.d.ts +2 -55
- package/dist/lib/fleet/auth-sync.js +2 -89
- package/dist/lib/harness-auth-capabilities.js +7 -2
- package/dist/lib/hosts/dispatch.d.ts +20 -1
- package/dist/lib/hosts/dispatch.js +52 -30
- package/dist/lib/hosts/remote-cmd.d.ts +21 -0
- package/dist/lib/hosts/remote-cmd.js +26 -2
- package/dist/lib/mailbox.d.ts +12 -0
- package/dist/lib/mailbox.js +16 -2
- package/dist/lib/menubar/snapshot.d.ts +51 -0
- package/dist/lib/menubar/snapshot.js +42 -3
- package/dist/lib/open-url.js +2 -2
- package/dist/lib/projects.d.ts +23 -0
- package/dist/lib/projects.js +78 -0
- package/dist/lib/secrets-cli.d.ts +3 -3
- package/dist/lib/secrets-cli.js +1 -1
- package/dist/lib/session/active.d.ts +1 -0
- package/dist/lib/session/active.js +8 -0
- package/dist/lib/session/db.d.ts +67 -3
- package/dist/lib/session/db.js +381 -126
- package/dist/lib/session/prompt.d.ts +23 -7
- package/dist/lib/session/prompt.js +46 -8
- package/dist/lib/session/remote/remote-list.d.ts +20 -0
- package/dist/lib/session/remote/remote-list.js +22 -6
- package/dist/lib/session/remote/watch.d.ts +12 -0
- package/dist/lib/session/remote/watch.js +9 -0
- package/dist/lib/session/remote-preview-cache.d.ts +29 -0
- package/dist/lib/session/remote-preview-cache.js +373 -0
- package/dist/lib/session/tail.d.ts +50 -0
- package/dist/lib/session/tail.js +219 -0
- package/dist/lib/setup-tool-install.js +2 -1
- package/dist/lib/setup-tool-status.d.ts +1 -1
- package/dist/lib/setup-tool-status.js +6 -1
- package/dist/lib/signin-badge.d.ts +19 -4
- package/dist/lib/signin-badge.js +29 -11
- package/dist/lib/term-driver.d.ts +24 -0
- package/dist/lib/term-driver.js +36 -0
- package/dist/lib/terminal/index.d.ts +1 -1
- package/dist/lib/terminal/index.js +1 -1
- package/dist/lib/terminal/inject.d.ts +38 -0
- package/dist/lib/terminal/inject.js +55 -9
- package/dist/lib/terminal/transport.d.ts +15 -5
- package/dist/lib/terminal/transport.js +61 -11
- package/package.json +1 -1
- package/dist/lib/fleet/remote-login.d.ts +0 -170
- package/dist/lib/fleet/remote-login.js +0 -568
|
@@ -11,30 +11,80 @@
|
|
|
11
11
|
import { spawn } from 'child_process';
|
|
12
12
|
import { sshExec } from '../ssh-exec.js';
|
|
13
13
|
import { shellQuote } from './quote.js';
|
|
14
|
-
/**
|
|
15
|
-
export
|
|
14
|
+
/** Grace between SIGTERM and SIGKILL when a deadline cancels a launcher. */
|
|
15
|
+
export const SPEC_KILL_GRACE_MS = 250;
|
|
16
|
+
/**
|
|
17
|
+
* Run the spec on this machine: spawn the launcher, resolve when it exits.
|
|
18
|
+
*
|
|
19
|
+
* `timeoutMs` makes the call genuinely bounded rather than merely raced: the
|
|
20
|
+
* child is spawned in its OWN process group (`detached`) and the whole group is
|
|
21
|
+
* signalled on expiry, so a launcher that itself spawned something (osascript →
|
|
22
|
+
* the app, tmux → the server) cannot keep running after the caller has given up.
|
|
23
|
+
* Racing the promise alone leaves the process behind (PHNX-3999).
|
|
24
|
+
*/
|
|
25
|
+
export function runLocal(spec, timeoutMs) {
|
|
16
26
|
return new Promise((resolve) => {
|
|
17
|
-
const child = spawn(spec.argv[0], spec.argv.slice(1), { stdio: 'ignore' });
|
|
18
|
-
|
|
19
|
-
|
|
27
|
+
const child = spawn(spec.argv[0], spec.argv.slice(1), { stdio: 'ignore', detached: !!timeoutMs });
|
|
28
|
+
let settled = false;
|
|
29
|
+
let killTimer = null;
|
|
30
|
+
// NOTE: the pending SIGKILL is deliberately NOT cancelled here. Resolving
|
|
31
|
+
// the promise is the CALLER giving up; it is not the child exiting. Clearing
|
|
32
|
+
// the kill timer on the timeout path (which resolves immediately after
|
|
33
|
+
// arming it) meant a SIGTERM-ignoring child was never killed and simply
|
|
34
|
+
// survived the bound this function exists to enforce.
|
|
35
|
+
const done = (result) => {
|
|
36
|
+
if (settled)
|
|
37
|
+
return;
|
|
38
|
+
settled = true;
|
|
39
|
+
if (timer)
|
|
40
|
+
clearTimeout(timer);
|
|
41
|
+
resolve(result);
|
|
42
|
+
};
|
|
43
|
+
const signalGroup = (signal) => {
|
|
44
|
+
try {
|
|
45
|
+
if (child.pid)
|
|
46
|
+
process.kill(timeoutMs ? -child.pid : child.pid, signal);
|
|
47
|
+
}
|
|
48
|
+
catch { /* already gone */ }
|
|
49
|
+
};
|
|
50
|
+
const timer = timeoutMs
|
|
51
|
+
? setTimeout(() => {
|
|
52
|
+
signalGroup('SIGTERM');
|
|
53
|
+
killTimer = setTimeout(() => signalGroup('SIGKILL'), SPEC_KILL_GRACE_MS);
|
|
54
|
+
killTimer.unref?.();
|
|
55
|
+
done({ ok: false, error: `${spec.argv[0]} did not finish in ${timeoutMs}ms` });
|
|
56
|
+
}, timeoutMs)
|
|
57
|
+
: null;
|
|
58
|
+
// Only the child ACTUALLY exiting retires the pending SIGKILL.
|
|
59
|
+
const childGone = () => { if (killTimer) {
|
|
60
|
+
clearTimeout(killTimer);
|
|
61
|
+
killTimer = null;
|
|
62
|
+
} };
|
|
63
|
+
child.on('error', (err) => { childGone(); done({ ok: false, error: err.message }); });
|
|
64
|
+
child.on('close', (code) => {
|
|
65
|
+
childGone();
|
|
66
|
+
done(code === 0 ? { ok: true } : { ok: false, error: `${spec.argv[0]} exited with code ${code}` });
|
|
67
|
+
});
|
|
20
68
|
});
|
|
21
69
|
}
|
|
22
70
|
/** Serialize a launch argv into a single POSIX-quoted shell command string. */
|
|
23
71
|
export function remoteCommand(spec) {
|
|
24
72
|
return spec.argv.map(shellQuote).join(' ');
|
|
25
73
|
}
|
|
26
|
-
/** Run the spec on a remote host over SSH. */
|
|
27
|
-
export function runRemote(spec, target) {
|
|
28
|
-
const res = sshExec(target, remoteCommand(spec), { multiplex:
|
|
74
|
+
/** Run the spec on a remote host over SSH. `timeoutMs` bounds the ssh client. */
|
|
75
|
+
export function runRemote(spec, target, timeoutMs) {
|
|
76
|
+
const res = sshExec(target, remoteCommand(spec), { multiplex: !timeoutMs, timeoutMs });
|
|
77
|
+
if (res.timedOut)
|
|
78
|
+
return { ok: false, error: `ssh to ${target} did not finish in ${timeoutMs}ms` };
|
|
29
79
|
if (res.code === 0)
|
|
30
80
|
return { ok: true };
|
|
31
81
|
const err = (res.stderr || '').trim();
|
|
32
82
|
return { ok: false, error: err || `ssh exited with code ${res.code}` };
|
|
33
83
|
}
|
|
34
84
|
/** Run a spec locally (no host / 'local') or on a resolved remote host. */
|
|
35
|
-
export async function runSpec(spec, host, resolveHost) {
|
|
85
|
+
export async function runSpec(spec, host, resolveHost, timeoutMs) {
|
|
36
86
|
if (!host || host === 'local')
|
|
37
|
-
return runLocal(spec);
|
|
87
|
+
return runLocal(spec, timeoutMs);
|
|
38
88
|
const target = resolveHost ? resolveHost(host) : host;
|
|
39
|
-
return runRemote(spec, target);
|
|
89
|
+
return runRemote(spec, target, timeoutMs);
|
|
40
90
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phnx-labs/agents-cli",
|
|
3
|
-
"version": "1.22.
|
|
3
|
+
"version": "1.22.116",
|
|
4
4
|
"description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
import type { AgentId } from '../types.js';
|
|
2
|
-
import type { DeviceProfile } from '../devices/registry.js';
|
|
3
|
-
import { type AuthVerdict, type AuthHealth } from '../auth-health.js';
|
|
4
|
-
import { type LoginFlow } from './auth-sync.js';
|
|
5
|
-
/** A verification URL + user code scraped from a device-code login screen. */
|
|
6
|
-
interface ScrapedLogin {
|
|
7
|
-
url?: string;
|
|
8
|
-
code?: string;
|
|
9
|
-
}
|
|
10
|
-
/**
|
|
11
|
-
* Extract the verification URL and user code from an ANSI-stripped login screen
|
|
12
|
-
* using the flow's regexes. Pure — no I/O, so it is the highest-value unit test
|
|
13
|
-
* target (a wrong code boundary or a missed URL is exactly the bug class this
|
|
14
|
-
* whole feature turns on). Group 1 is preferred over the whole match so a
|
|
15
|
-
* surrounding phrase ("enter code XXXX-XXXX") never leaks into the captured
|
|
16
|
-
* value. A flow with no regex for a field yields `undefined` for that field.
|
|
17
|
-
*/
|
|
18
|
-
export declare function scrapeLogin(screenText: string, flow: LoginFlow): ScrapedLogin;
|
|
19
|
-
/** Why a logged-out (agent, device) pair can or cannot be driven remotely. */
|
|
20
|
-
interface FlowClassification {
|
|
21
|
-
flow: LoginFlow | null;
|
|
22
|
-
remotable: boolean;
|
|
23
|
-
reason?: string;
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Decide whether `agent`'s login can be driven on a box of `platform`. Only a
|
|
27
|
-
* `device-code` flow is remotable; loopback (browser + 127.0.0.1 on the box),
|
|
28
|
-
* api-key, unknown, or a macOS keychain-bound token are surfaced with an honest
|
|
29
|
-
* reason instead of a mis-drive. Pure.
|
|
30
|
-
*/
|
|
31
|
-
export declare function classifyLoginFlow(agent: AgentId | string, platform: string | undefined): FlowClassification;
|
|
32
|
-
/**
|
|
33
|
-
* Does a cached auth verdict mean "this (host, agent) needs an interactive
|
|
34
|
-
* login"? A missing row (never signed in / credential absent) and a `revoked`
|
|
35
|
-
* token do; `live`, `unverified` (signed in, no probe), `expired`, and
|
|
36
|
-
* `rate_limited` are treated as already-logged-in (expired self-heals on next
|
|
37
|
-
* launch — never re-login on it). Pure.
|
|
38
|
-
*/
|
|
39
|
-
export declare function isPendingVerdict(verdict: AuthVerdict | undefined): boolean;
|
|
40
|
-
/** A logged-out (agent, device) pair `fleet login` will target or explain. */
|
|
41
|
-
interface PendingLogin {
|
|
42
|
-
device: string;
|
|
43
|
-
agent: AgentId | string;
|
|
44
|
-
platform?: string;
|
|
45
|
-
/** ssh dial target for the device. */
|
|
46
|
-
target: string;
|
|
47
|
-
extraSshArgs?: string[];
|
|
48
|
-
flow: LoginFlow;
|
|
49
|
-
/** True when a device-code flow this command can actually drive. */
|
|
50
|
-
remotable: boolean;
|
|
51
|
-
/** Present when `!remotable` — why it must be done on the box itself. */
|
|
52
|
-
reason?: string;
|
|
53
|
-
}
|
|
54
|
-
interface SelectLoginOptions {
|
|
55
|
-
/** Include pairs already logged-in (verdict live/unverified) — for a forced re-login. */
|
|
56
|
-
includeLoggedIn?: boolean;
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* From a device list, requested agents, and the auth-health cache, compute the
|
|
60
|
-
* pending logins — partitioned (via {@link classifyLoginFlow}) into remotable
|
|
61
|
-
* device-code flows and non-remotable pairs with a reason. Pure: the caller
|
|
62
|
-
* supplies the already-loaded devices + cache, so this is fully unit-testable.
|
|
63
|
-
*
|
|
64
|
-
* A pair is pending when its cached verdict {@link isPendingVerdict} (or
|
|
65
|
-
* `includeLoggedIn` forces it). Agents with no defined login flow are dropped
|
|
66
|
-
* silently (nothing to log into).
|
|
67
|
-
*/
|
|
68
|
-
export declare function selectLoginTargets(devices: DeviceProfile[], agents: (AgentId | string)[], cache: Record<string, AuthHealth>, opts?: SelectLoginOptions): PendingLogin[];
|
|
69
|
-
/**
|
|
70
|
-
* Build the `ssh -tt <target> <loginCommand>` command string driven through the
|
|
71
|
-
* `term` CLI. `-tt` forces a tty so the remote CLI enters its interactive
|
|
72
|
-
* device-code flow; `accept-new` learns a first-seen host key without a prompt.
|
|
73
|
-
* Both the target and the login command are shell-quoted. Pure/testable.
|
|
74
|
-
*
|
|
75
|
-
* NOTE: this uses key-based ssh auth (the tailnet fleet's norm). Password-bundle
|
|
76
|
-
* devices need the askpass shim env, which the PTY env allowlist strips — those
|
|
77
|
-
* are a follow-up (TODO), surfaced as an error rather than silently mis-driven.
|
|
78
|
-
*/
|
|
79
|
-
export declare function buildRemoteLoginSshCommand(target: string, flow: LoginFlow, extraSshArgs?: string[]): string;
|
|
80
|
-
type LoginMode = 'bulk' | 'interactive';
|
|
81
|
-
/** Per-pair status shape shared between the driver, the status endpoint, and the page. */
|
|
82
|
-
export type LoginStatusState = 'pending' | 'driving' | 'ready' | 'authorized' | 'error' | 'skipped';
|
|
83
|
-
export interface LoginStatus {
|
|
84
|
-
device: string;
|
|
85
|
-
agent: string;
|
|
86
|
-
remotable: boolean;
|
|
87
|
-
reason?: string;
|
|
88
|
-
state: LoginStatusState;
|
|
89
|
-
url?: string;
|
|
90
|
-
code?: string;
|
|
91
|
-
/** epoch ms the scraped code expires (best-effort ~15 min TTL). */
|
|
92
|
-
expiresAt?: number;
|
|
93
|
-
detail?: string;
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* Render the self-contained dashboard page. Inlines all CSS/JS; the page polls
|
|
97
|
-
* `/api/status` and flips rows to their live state. `bulk` shows a grid of every
|
|
98
|
-
* pending login at once; `interactive` shows a one-at-a-time wizard. Pure — a
|
|
99
|
-
* string in, a string out — so it renders (and is asserted) without a server.
|
|
100
|
-
* Terminal-coded per the agents-cli brand (#0a0a0a bg, #a3e635 lime), with a
|
|
101
|
-
* light variant via `prefers-color-scheme`.
|
|
102
|
-
*/
|
|
103
|
-
export declare function buildDashboardHtml(pending: PendingLogin[], mode: LoginMode): string;
|
|
104
|
-
/** The subset of the `term` CLI `driveRemoteLogin` needs — faked in tests. */
|
|
105
|
-
export interface TermDriver {
|
|
106
|
-
start(opts?: {
|
|
107
|
-
rows?: number;
|
|
108
|
-
cols?: number;
|
|
109
|
-
}): Promise<string>;
|
|
110
|
-
exec(id: string, command: string): Promise<void>;
|
|
111
|
-
write(id: string, input: string): Promise<void>;
|
|
112
|
-
screen(id: string): Promise<{
|
|
113
|
-
screen: string;
|
|
114
|
-
exited: boolean;
|
|
115
|
-
}>;
|
|
116
|
-
stop(id: string): Promise<void>;
|
|
117
|
-
}
|
|
118
|
-
/** Real driver over the standalone `term` CLI (`../term-client.js`). */
|
|
119
|
-
export declare function defaultTermDriver(): TermDriver;
|
|
120
|
-
export interface DriveOptions {
|
|
121
|
-
/** Wait after launching ssh before steering / scraping (default 4000ms). */
|
|
122
|
-
initialDelayMs?: number;
|
|
123
|
-
/** Poll cadence for the scrape loop (default 1000ms). */
|
|
124
|
-
pollMs?: number;
|
|
125
|
-
/** Overall scrape deadline (default 90000ms). */
|
|
126
|
-
timeoutMs?: number;
|
|
127
|
-
}
|
|
128
|
-
interface DriveResult extends ScrapedLogin {
|
|
129
|
-
/** True when the underlying ssh session already exited (login finished or failed). */
|
|
130
|
-
exited: boolean;
|
|
131
|
-
/** The term session id, so the caller can keep polling / stop it. */
|
|
132
|
-
sessionId: string;
|
|
133
|
-
}
|
|
134
|
-
/**
|
|
135
|
-
* Drive one remote device-code login: launch `ssh -tt <target> <loginCommand>`
|
|
136
|
-
* in a `term` session, send the flow's `deviceCodeSelect` keystrokes (if any) to reach the
|
|
137
|
-
* device-code path, then poll the screen until {@link scrapeLogin} yields both a
|
|
138
|
-
* URL and a code (or the deadline / session-exit). The human completes the code
|
|
139
|
-
* in the browser; the caller polls the credential file for completion. Testable
|
|
140
|
-
* against a fake {@link TermDriver}.
|
|
141
|
-
*/
|
|
142
|
-
export declare function driveRemoteLogin(target: string, flow: LoginFlow, driver: TermDriver, opts?: DriveOptions, extraSshArgs?: string[]): Promise<DriveResult>;
|
|
143
|
-
interface RunFleetLoginOptions {
|
|
144
|
-
agents?: (AgentId | string)[];
|
|
145
|
-
devices?: string[];
|
|
146
|
-
interactive?: boolean;
|
|
147
|
-
json?: boolean;
|
|
148
|
-
/** Target every device-code pair regardless of cached login state (cold cache / forced re-login). */
|
|
149
|
-
all?: boolean;
|
|
150
|
-
/** Overrides for testing / non-default cadence. */
|
|
151
|
-
driver?: TermDriver;
|
|
152
|
-
drive?: DriveOptions;
|
|
153
|
-
/** Port for the local dashboard (0 = ephemeral). */
|
|
154
|
-
port?: number;
|
|
155
|
-
/** Open the browser (default true). */
|
|
156
|
-
open?: boolean;
|
|
157
|
-
/** Max wall-clock to wait for all logins before giving up (default 20 min). */
|
|
158
|
-
overallTimeoutMs?: number;
|
|
159
|
-
/** ~15 min device-code TTL used for the countdown. */
|
|
160
|
-
codeTtlMs?: number;
|
|
161
|
-
}
|
|
162
|
-
/**
|
|
163
|
-
* Orchestrate `agents fleet login`: detect pending -> serve the local dashboard
|
|
164
|
-
* + open the browser -> drive each box's device-code flow (concurrent in bulk,
|
|
165
|
-
* sequential just-in-time in interactive) -> poll the credential file for
|
|
166
|
-
* completion -> print a final matrix. The pure pieces above carry the logic;
|
|
167
|
-
* this wires them to the live fleet.
|
|
168
|
-
*/
|
|
169
|
-
export declare function runFleetLogin(opts?: RunFleetLoginOptions): Promise<LoginStatus[]>;
|
|
170
|
-
export {};
|