@phnx-labs/agents-cli 1.22.27 → 1.22.28
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 +49 -0
- package/README.md +62 -26
- package/dist/bin/agents +0 -0
- package/dist/commands/exec.js +54 -10
- package/dist/commands/hosts.js +4 -3
- package/dist/commands/perf.d.ts +10 -0
- package/dist/commands/perf.js +14 -6
- package/dist/commands/projects.js +24 -10
- package/dist/commands/routines.js +31 -4
- package/dist/commands/run-account-picker.d.ts +37 -0
- package/dist/commands/run-account-picker.js +101 -17
- package/dist/commands/secrets.js +86 -2
- package/dist/commands/sessions.d.ts +26 -6
- package/dist/commands/sessions.js +46 -12
- package/dist/commands/teams.js +19 -8
- package/dist/lib/activity.js +45 -99
- package/dist/lib/claude-account-token.js +38 -11
- package/dist/lib/cloud/host.js +1 -0
- package/dist/lib/devices/resolve-target.d.ts +1 -0
- package/dist/lib/devices/resolve-target.js +9 -2
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/events.js +2 -2
- package/dist/lib/git.d.ts +9 -3
- package/dist/lib/git.js +39 -22
- package/dist/lib/hooks/profile.d.ts +12 -1
- package/dist/lib/hooks/profile.js +6 -1
- package/dist/lib/hooks.js +4 -1
- package/dist/lib/hosts/dispatch.d.ts +10 -0
- package/dist/lib/hosts/dispatch.js +78 -22
- package/dist/lib/hosts/logs.js +11 -1
- package/dist/lib/hosts/passthrough.d.ts +5 -0
- package/dist/lib/hosts/passthrough.js +5 -2
- package/dist/lib/hosts/progress.d.ts +10 -0
- package/dist/lib/hosts/progress.js +37 -5
- package/dist/lib/hosts/ready.d.ts +41 -2
- package/dist/lib/hosts/ready.js +100 -12
- package/dist/lib/hosts/reconcile.d.ts +2 -1
- package/dist/lib/hosts/reconcile.js +13 -5
- package/dist/lib/hosts/reconnect.js +5 -3
- package/dist/lib/hosts/registry.js +1 -0
- package/dist/lib/hosts/tasks.d.ts +1 -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/snapshot.js +15 -2
- package/dist/lib/models.d.ts +18 -11
- package/dist/lib/models.js +19 -15
- package/dist/lib/perf/db.js +16 -3
- package/dist/lib/perf/types.d.ts +12 -1
- package/dist/lib/project-probe.d.ts +22 -3
- package/dist/lib/project-probe.js +105 -17
- package/dist/lib/project-status.d.ts +9 -0
- package/dist/lib/project-status.js +15 -0
- package/dist/lib/refresh.js +22 -11
- package/dist/lib/rotate.d.ts +22 -0
- package/dist/lib/rotate.js +26 -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/agent.d.ts +19 -3
- package/dist/lib/secrets/agent.js +58 -20
- package/dist/lib/secrets/audit.d.ts +1 -1
- package/dist/lib/secrets/audit.js +2 -0
- package/dist/lib/secrets/bundles.d.ts +3 -3
- package/dist/lib/secrets/bundles.js +15 -5
- package/dist/lib/secrets/filestore.d.ts +2 -0
- package/dist/lib/secrets/filestore.js +4 -0
- package/dist/lib/secrets/session-store.d.ts +7 -0
- package/dist/lib/secrets/session-store.js +21 -0
- package/dist/lib/session/active.d.ts +49 -0
- package/dist/lib/session/active.js +175 -11
- package/dist/lib/session/bash-command.d.ts +16 -0
- package/dist/lib/session/bash-command.js +65 -0
- package/dist/lib/session/db.d.ts +4 -0
- package/dist/lib/session/db.js +32 -15
- package/dist/lib/session/remote-bundle.js +1 -1
- package/dist/lib/staleness/detectors/skills.d.ts +2 -0
- package/dist/lib/staleness/detectors/skills.js +17 -2
- package/dist/lib/staleness/index.d.ts +10 -1
- package/dist/lib/staleness/index.js +28 -3
- package/dist/lib/teams/agents.d.ts +1 -0
- package/dist/lib/teams/agents.js +38 -11
- package/dist/lib/teams/remoteWorktree.d.ts +11 -7
- package/dist/lib/teams/remoteWorktree.js +29 -27
- package/dist/lib/versions.d.ts +1 -0
- package/dist/lib/versions.js +40 -22
- package/package.json +3 -1
- package/dist/lib/exec.bench.d.ts +0 -1
- package/dist/lib/exec.bench.js +0 -186
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { select } from '@inquirer/prompts';
|
|
2
|
+
import chalk from 'chalk';
|
|
2
3
|
import { agentLabel } from '../lib/agents.js';
|
|
3
|
-
import {
|
|
4
|
+
import { loginHint } from '../lib/signin-badge.js';
|
|
5
|
+
import { collectRunCandidates, readinessFromCandidate, isSignInRecoverable, } from '../lib/rotate.js';
|
|
4
6
|
import { compareVersions, getGlobalDefault } from '../lib/versions.js';
|
|
5
7
|
import { isInteractiveTerminal, isPromptCancelled, requireInteractiveSelection } from './utils.js';
|
|
6
8
|
const CANCEL_SELECTION = '__agents_cancel_account_selection__';
|
|
@@ -30,14 +32,21 @@ export function formatAccountLimits(candidate) {
|
|
|
30
32
|
})
|
|
31
33
|
.join(' · ');
|
|
32
34
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Why a row cannot be picked. Only a THROTTLE disables a row: the account is
|
|
37
|
+
* signed in and out of capacity, so launching it just hammers an exhausted
|
|
38
|
+
* account (RUSH-2132) and nothing the user does at this prompt helps.
|
|
39
|
+
*
|
|
40
|
+
* An AUTH exclusion (`signed_out` / `revoked`) is deliberately NOT disabled —
|
|
41
|
+
* the harness's own TUI is the login surface, so picking that row and launching
|
|
42
|
+
* is the only way to sign in through agents-cli. Disabling it left a fully
|
|
43
|
+
* logged-out harness with no reachable account at all (RUSH-2334).
|
|
44
|
+
*/
|
|
45
|
+
function disabledReason(candidate, readiness) {
|
|
35
46
|
if (readiness.ready)
|
|
36
47
|
return undefined;
|
|
37
|
-
if (readiness
|
|
38
|
-
return
|
|
39
|
-
if (readiness.reason === 'revoked')
|
|
40
|
-
return 'needs re-login';
|
|
48
|
+
if (isSignInRecoverable(readiness))
|
|
49
|
+
return undefined;
|
|
41
50
|
if (readiness.reason === 'out_of_credits')
|
|
42
51
|
return 'out of credits';
|
|
43
52
|
const windows = candidate.usageSnapshot?.windows ?? [];
|
|
@@ -53,24 +62,44 @@ function disabledReason(candidate) {
|
|
|
53
62
|
/** Build aligned picker rows with usable accounts first and unsafe rows disabled. */
|
|
54
63
|
export function buildRunAccountChoices(candidates, globalDefault) {
|
|
55
64
|
const rows = candidates.map((candidate) => {
|
|
56
|
-
const
|
|
65
|
+
const readiness = readinessFromCandidate(candidate);
|
|
66
|
+
const disabled = disabledReason(candidate, readiness);
|
|
67
|
+
const signInRequired = isSignInRecoverable(readiness);
|
|
57
68
|
const version = candidate.version === globalDefault
|
|
58
69
|
? `${candidate.version} (default)`
|
|
59
70
|
: candidate.version;
|
|
71
|
+
// `revoked` is signed-in-but-rejected, so it reads as a re-login rather
|
|
72
|
+
// than "logged out"; every throttle is still a signed-in account.
|
|
73
|
+
const authReason = readiness.ready ? null : readiness.reason;
|
|
74
|
+
const status = authReason === 'revoked'
|
|
75
|
+
? 'needs re-login'
|
|
76
|
+
: authReason === 'signed_out'
|
|
77
|
+
? 'logged out'
|
|
78
|
+
: 'logged in';
|
|
60
79
|
return {
|
|
61
80
|
candidate,
|
|
62
81
|
account: candidate.accountLabel || 'account unavailable',
|
|
63
82
|
version,
|
|
64
|
-
status
|
|
83
|
+
status,
|
|
65
84
|
plan: candidate.usageSnapshot?.plan ?? candidate.plan ?? 'plan unavailable',
|
|
66
|
-
|
|
85
|
+
// An auth-blocked row shows what picking it DOES; its quota is moot until
|
|
86
|
+
// there is a credential to spend it with.
|
|
87
|
+
limits: authReason === 'revoked'
|
|
88
|
+
? 'launch to re-authenticate'
|
|
89
|
+
: authReason === 'signed_out'
|
|
90
|
+
? 'launch to sign in'
|
|
91
|
+
: formatAccountLimits(candidate),
|
|
67
92
|
disabled,
|
|
68
|
-
ready:
|
|
93
|
+
ready: readiness.ready,
|
|
94
|
+
signInRequired,
|
|
69
95
|
};
|
|
70
96
|
});
|
|
97
|
+
// Ready accounts first, then the ones a login would unlock (actionable), then
|
|
98
|
+
// the throttled rows the user can do nothing about at this prompt.
|
|
99
|
+
const rank = (row) => row.ready ? 0 : row.signInRequired ? 1 : 2;
|
|
71
100
|
rows.sort((a, b) => {
|
|
72
|
-
if (a
|
|
73
|
-
return a
|
|
101
|
+
if (rank(a) !== rank(b))
|
|
102
|
+
return rank(a) - rank(b);
|
|
74
103
|
const aDefault = a.candidate.version === globalDefault;
|
|
75
104
|
const bDefault = b.candidate.version === globalDefault;
|
|
76
105
|
if (aDefault !== bDefault)
|
|
@@ -92,8 +121,56 @@ export function buildRunAccountChoices(candidates, globalDefault) {
|
|
|
92
121
|
value: row.candidate.version,
|
|
93
122
|
disabled: row.disabled,
|
|
94
123
|
ready: row.ready,
|
|
124
|
+
signInRequired: row.signInRequired,
|
|
95
125
|
}));
|
|
96
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Whether a zero-healthy run may recover by launching for a login, or must keep
|
|
129
|
+
* failing loud. Three inputs, all of which have to hold:
|
|
130
|
+
*
|
|
131
|
+
* - `recoverable` — at least one excluded account is only auth-blocked. An
|
|
132
|
+
* all-throttled set is never launched (RUSH-2132): only a window reset clears it.
|
|
133
|
+
* - `tty` — a login needs a human present; off a TTY nobody can complete one.
|
|
134
|
+
* - `json` — `--json` marks a MACHINE consumer, which must never be handed a
|
|
135
|
+
* picker or dropped into a login TUI. This mirrors the canonical
|
|
136
|
+
* `Surface.interactive = tty && !json` in `commands/utils.ts`; a `--json` caller
|
|
137
|
+
* gets the parseable fail-loud error instead.
|
|
138
|
+
*/
|
|
139
|
+
export function signInLaunchDecision(input) {
|
|
140
|
+
const humanPresent = input.tty && !input.json;
|
|
141
|
+
return input.recoverable > 0 && humanPresent ? 'launch' : 'fail-loud';
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Choose which installed version to launch so the user can authenticate, when a
|
|
145
|
+
* strategy found zero healthy accounts but at least one is merely signed out
|
|
146
|
+
* (RUSH-2334). Returns the version to launch, or null if the user cancelled.
|
|
147
|
+
*
|
|
148
|
+
* A single candidate does NOT prompt — a one-item picker is pure noise, and the
|
|
149
|
+
* only thing to decide has one answer. Several candidates fall through to the
|
|
150
|
+
* normal account picker, which shows every account with its state so the choice
|
|
151
|
+
* is informed (throttled rows stay disabled there).
|
|
152
|
+
*
|
|
153
|
+
* Callers MUST have already confirmed an interactive terminal: off a TTY there
|
|
154
|
+
* is nobody to complete the login, and the run should fail loud instead.
|
|
155
|
+
*/
|
|
156
|
+
export async function pickSignInLaunchVersion(agent, recoverable, quiet = false) {
|
|
157
|
+
if (recoverable.length === 0)
|
|
158
|
+
return null;
|
|
159
|
+
if (recoverable.length > 1) {
|
|
160
|
+
const selected = await pickRunAccountCandidate(agent);
|
|
161
|
+
return selected?.version ?? null;
|
|
162
|
+
}
|
|
163
|
+
const [only] = recoverable;
|
|
164
|
+
if (!quiet) {
|
|
165
|
+
const readiness = readinessFromCandidate(only);
|
|
166
|
+
const why = !readiness.ready && readiness.reason === 'revoked'
|
|
167
|
+
? 'has no valid credential (the server rejected its token)'
|
|
168
|
+
: 'has no signed-in account';
|
|
169
|
+
process.stderr.write(chalk.yellow(`${agentLabel(agent)} ${why} — launching ${agent}@${only.version} so you can sign in.\n`));
|
|
170
|
+
process.stderr.write(chalk.gray(`Sign in with: ${loginHint(agent)}\n`));
|
|
171
|
+
}
|
|
172
|
+
return only.version;
|
|
173
|
+
}
|
|
97
174
|
/** Prompt for one safe installed account/version. A cancelled picker launches nothing. */
|
|
98
175
|
export async function pickRunAccountCandidate(agent) {
|
|
99
176
|
if (!isInteractiveTerminal()) {
|
|
@@ -107,9 +184,14 @@ export async function pickRunAccountCandidate(agent) {
|
|
|
107
184
|
throw new Error(`No installed ${agentLabel(agent)} versions are available. Run: agents add ${agent}@latest`);
|
|
108
185
|
}
|
|
109
186
|
const choices = buildRunAccountChoices(candidates, getGlobalDefault(agent));
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
187
|
+
// "Selectable" is broader than "ready": an auth-blocked row is pickable so the
|
|
188
|
+
// launch can carry you into the harness's login (RUSH-2334). Only offer the
|
|
189
|
+
// bail-out row when literally nothing can be chosen — i.e. every account is
|
|
190
|
+
// throttled, which no amount of signing in fixes.
|
|
191
|
+
const hasSelectableAccount = choices.some((choice) => !choice.disabled);
|
|
192
|
+
const needsSignIn = choices.some((choice) => choice.signInRequired);
|
|
193
|
+
const promptChoices = choices.map(({ ready: _ready, signInRequired: _signInRequired, ...choice }) => choice);
|
|
194
|
+
if (!hasSelectableAccount) {
|
|
113
195
|
promptChoices.push({
|
|
114
196
|
name: 'No usable accounts — cancel',
|
|
115
197
|
value: CANCEL_SELECTION,
|
|
@@ -117,7 +199,9 @@ export async function pickRunAccountCandidate(agent) {
|
|
|
117
199
|
}
|
|
118
200
|
try {
|
|
119
201
|
const version = await select({
|
|
120
|
-
message:
|
|
202
|
+
message: needsSignIn
|
|
203
|
+
? `Select a ${agentLabel(agent)} account for this run (pick a logged-out one to sign in):`
|
|
204
|
+
: `Select a ${agentLabel(agent)} account for this run:`,
|
|
121
205
|
choices: promptChoices,
|
|
122
206
|
loop: false,
|
|
123
207
|
});
|
package/dist/commands/secrets.js
CHANGED
|
@@ -24,8 +24,9 @@ import { encryptForFallback, decryptForFallback } from '../lib/secrets/filestore
|
|
|
24
24
|
import { getKeychainToken, hasKeychainToken, secretsKeychainItem, setKeychainToken, maybeAutoRekey, } from '../lib/secrets/index.js';
|
|
25
25
|
import { assertOpAvailable, createPasswordItem, deleteItemByTitle, extractSecrets, itemExistsByTitle, listItems, listVaults, } from '../lib/onepassword.js';
|
|
26
26
|
import { GLOBAL_HARNESS } from '../lib/secrets/scope.js';
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
27
|
+
import { createSecretLease, selectLeasedEnv } from '../lib/secrets/lease.js';
|
|
28
|
+
import { secretsHoldMs, secretsAgentDurable, agentLoad, agentLock, agentPing, agentSocketExists, agentStatus, agentRevoke, ensureAgentRunning, runAgentLoadFromStdin, runSecretsAgent, uninstallSecretsAgentService, } from '../lib/secrets/agent.js';
|
|
29
|
+
import { activeLeaseSessions, saveSession, deleteBundleSessions, deleteAllSessions, deleteLeaseSession } from '../lib/secrets/session-store.js';
|
|
29
30
|
import { getCliVersionFresh } from '../lib/version.js';
|
|
30
31
|
import { readMeta } from '../lib/state.js';
|
|
31
32
|
import { parseDuration } from '../lib/hooks/cache.js';
|
|
@@ -2426,6 +2427,89 @@ Examples:
|
|
|
2426
2427
|
console.log(password);
|
|
2427
2428
|
}
|
|
2428
2429
|
});
|
|
2430
|
+
cmd
|
|
2431
|
+
.command('lease <bundle>')
|
|
2432
|
+
.description('Hold only an explicit subset of a bundle until an independent expiry.')
|
|
2433
|
+
.requiredOption('--keys <keys>', 'Comma-separated key subset')
|
|
2434
|
+
.requiredOption('--for <duration>', 'Lease duration, for example 30m or 8h')
|
|
2435
|
+
.option('--agent <agent>', 'Narrow the lease to one harness; default is global')
|
|
2436
|
+
.option('--durable', 'Keep the lease across sleep as well as broker restart')
|
|
2437
|
+
.action(async (name, opts) => {
|
|
2438
|
+
if (process.platform !== 'darwin') {
|
|
2439
|
+
throw new Error('Scoped lease brokering is not available on this platform yet.');
|
|
2440
|
+
}
|
|
2441
|
+
const seconds = parseDuration(opts.for);
|
|
2442
|
+
if (seconds === null)
|
|
2443
|
+
throw new Error(`Invalid lease duration '${opts.for}'.`);
|
|
2444
|
+
const ttlMs = seconds * 1000;
|
|
2445
|
+
const harness = opts.agent || GLOBAL_HARNESS;
|
|
2446
|
+
const { bundle, env } = readAndResolveBundleEnv(name, {
|
|
2447
|
+
noAgent: true,
|
|
2448
|
+
interactiveUnlock: true,
|
|
2449
|
+
caller: 'lease secrets',
|
|
2450
|
+
agent: harness,
|
|
2451
|
+
keyMode: 'storage',
|
|
2452
|
+
});
|
|
2453
|
+
const lease = createSecretLease({
|
|
2454
|
+
bundle: name,
|
|
2455
|
+
keys: opts.keys.split(','),
|
|
2456
|
+
availableKeys: Object.keys(env),
|
|
2457
|
+
ttlMs,
|
|
2458
|
+
harness,
|
|
2459
|
+
sleepPersist: opts.durable,
|
|
2460
|
+
});
|
|
2461
|
+
const leasedEnv = selectLeasedEnv(lease, env);
|
|
2462
|
+
if (!(await ensureAgentRunning()) || !(await agentLoad(name, bundle, leasedEnv, ttlMs, harness, lease))) {
|
|
2463
|
+
throw new Error('Could not load the scoped lease into the secrets broker.');
|
|
2464
|
+
}
|
|
2465
|
+
ensureDaemonStarted();
|
|
2466
|
+
saveSession(name, {
|
|
2467
|
+
bundle,
|
|
2468
|
+
env: leasedEnv,
|
|
2469
|
+
expiresAt: lease.expiresAt,
|
|
2470
|
+
sleepPersist: lease.sleepPersist,
|
|
2471
|
+
harness,
|
|
2472
|
+
lease,
|
|
2473
|
+
});
|
|
2474
|
+
emitSecretAudit({
|
|
2475
|
+
event: 'secrets.unlocked', bundle: name, operation: 'lease', source: lease.sleepPersist ? 'broker+durable' : 'broker',
|
|
2476
|
+
status: 'success', keys: lease.keys, keyCount: lease.keys.length, agent: harness, ttlMs: lease.expiresAt - lease.createdAt,
|
|
2477
|
+
});
|
|
2478
|
+
console.log(`${chalk.green('leased')} ${chalk.cyan(lease.id)} ${chalk.gray(`(${name}: ${lease.keys.join(', ')}, ${humanRemaining(lease.expiresAt)})`)}`);
|
|
2479
|
+
});
|
|
2480
|
+
cmd
|
|
2481
|
+
.command('leases')
|
|
2482
|
+
.description('List active scoped secret leases.')
|
|
2483
|
+
.action(async () => {
|
|
2484
|
+
const brokerLeases = (await agentStatus()).filter((entry) => entry.leaseId);
|
|
2485
|
+
const byId = new Map(brokerLeases.map((entry) => [entry.leaseId, entry]));
|
|
2486
|
+
for (const { name, lease } of activeLeaseSessions()) {
|
|
2487
|
+
if (!byId.has(lease.id))
|
|
2488
|
+
byId.set(lease.id, { name, expiresAt: lease.expiresAt, keyCount: lease.keys.length, harness: lease.harness, leaseId: lease.id, keys: lease.keys });
|
|
2489
|
+
}
|
|
2490
|
+
const leases = [...byId.values()];
|
|
2491
|
+
if (leases.length === 0) {
|
|
2492
|
+
console.log(chalk.gray('No active secret leases.'));
|
|
2493
|
+
return;
|
|
2494
|
+
}
|
|
2495
|
+
console.log(`${'ID'.padEnd(24)} ${'BUNDLE'.padEnd(24)} ${'KEYS'.padEnd(24)} EXPIRES IN`);
|
|
2496
|
+
for (const lease of leases) {
|
|
2497
|
+
console.log(`${lease.leaseId.padEnd(24)} ${lease.name.padEnd(24)} ${(lease.keys ?? []).join(',').padEnd(24)} ${humanRemaining(lease.expiresAt)}`);
|
|
2498
|
+
}
|
|
2499
|
+
});
|
|
2500
|
+
cmd
|
|
2501
|
+
.command('revoke <lease-id>')
|
|
2502
|
+
.description('Revoke one scoped secret lease immediately.')
|
|
2503
|
+
.action(async (leaseId) => {
|
|
2504
|
+
if (agentSocketExists() && !(await agentPing()).reachable) {
|
|
2505
|
+
throw new Error('Cannot revoke while the secrets broker is using an incompatible protocol. Restart the broker and retry.');
|
|
2506
|
+
}
|
|
2507
|
+
const persisted = deleteLeaseSession(leaseId);
|
|
2508
|
+
const wiped = await agentRevoke(leaseId);
|
|
2509
|
+
if (wiped + persisted === 0)
|
|
2510
|
+
throw new Error(`Secret lease '${leaseId}' is not active.`);
|
|
2511
|
+
console.log(chalk.green(`Revoked secret lease ${leaseId}.`));
|
|
2512
|
+
});
|
|
2429
2513
|
cmd
|
|
2430
2514
|
.command('unlock [names...]')
|
|
2431
2515
|
.description('Hold a bundle in the secrets-agent after one Touch ID, so concurrent runs read it without re-prompting (macOS). With --host, unlock FILE-backed bundle(s) on a remote (the passphrase prompt surfaces over the SSH TTY); keychain/biometry bundles are GUI-only and can\'t be remote-unlocked.')
|
|
@@ -187,13 +187,33 @@ export declare function liveStatusWord(a: ActiveSession | undefined): string;
|
|
|
187
187
|
*/
|
|
188
188
|
export declare function isAwaitingUser(s: ActiveSession): boolean;
|
|
189
189
|
/**
|
|
190
|
-
* Whether a row belongs in an unqualified `--active` view
|
|
190
|
+
* Whether a row belongs in an unqualified `--active` view (RUSH-2336) — the
|
|
191
|
+
* ONE canonical selector every bare-active surface shares: the CLI's grouped
|
|
192
|
+
* table and `--json` (`renderActiveSessions`), the interactive browser's
|
|
193
|
+
* `--active` filter ({@link applyFilters} in `sessions-browser.ts`), `focus`'s
|
|
194
|
+
* attach gate (`isAttachableLiveSession`), and the menubar snapshot
|
|
195
|
+
* ({@link computeMenubarSnapshot}).
|
|
191
196
|
*
|
|
192
|
-
* The live registry deliberately retains terminally-dead
|
|
193
|
-
* `--closed` / `--crashed` and recovery to
|
|
194
|
-
* therefore is not, by itself, evidence
|
|
195
|
-
* Explicit lifecycle filters bypass this
|
|
196
|
-
* rows through {@link matchesLiveStatus}
|
|
197
|
+
* The live registry deliberately retains terminally-dead AND not-yet-started
|
|
198
|
+
* rows long enough for `--closed` / `--crashed` / `--queued` and recovery to
|
|
199
|
+
* find them. Presence in that registry therefore is not, by itself, evidence
|
|
200
|
+
* that a session is CURRENTLY active. Explicit lifecycle filters bypass this
|
|
201
|
+
* predicate and select those retained rows through {@link matchesLiveStatus}
|
|
202
|
+
* instead — this function only decides the unqualified default.
|
|
203
|
+
*
|
|
204
|
+
* - `queued` / `closed` / `crashed` are never active here: queued hasn't
|
|
205
|
+
* started yet, closed/crashed are unconditionally dead. All three stay
|
|
206
|
+
* reachable through their explicit filter.
|
|
207
|
+
* - A cloud row carries no local pid at all — it is active on the
|
|
208
|
+
* provider's own word (`cloudProvider` + `cloudTaskId`), never a
|
|
209
|
+
* fabricated pid.
|
|
210
|
+
* - Every other row is a real OS process (terminal/tmux/headless/team). It
|
|
211
|
+
* is active only when the row names the machine it runs on, carries a
|
|
212
|
+
* positive pid, AND has POSITIVELY verified that pid is alive
|
|
213
|
+
* (`pidAlive === true` — not merely "not known dead"). A live orphaned
|
|
214
|
+
* row, or a live-but-stuck abandoned row, still qualifies this way since
|
|
215
|
+
* both carry a genuinely alive pid; unknown liveness (an older peer's
|
|
216
|
+
* row, or a pid that could never be resolved) does not.
|
|
197
217
|
*/
|
|
198
218
|
export declare function isRunningLiveSession(s: ActiveSession): boolean;
|
|
199
219
|
/**
|
|
@@ -342,10 +342,9 @@ export function formatActiveRowDescription(s) {
|
|
|
342
342
|
if (todo)
|
|
343
343
|
parts.push(todo);
|
|
344
344
|
// Latest-turn snippet — avoid repeating label/topic when already used as identity.
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
else if (s.preview) {
|
|
345
|
+
// A cloud row with no preview yet says nothing here; its provider + task id
|
|
346
|
+
// ride the locator badge instead (RUSH-2336), not duplicated on this line.
|
|
347
|
+
if (s.preview) {
|
|
349
348
|
pushText(s.preview);
|
|
350
349
|
}
|
|
351
350
|
else if (!s.label && s.topic) {
|
|
@@ -540,18 +539,40 @@ export function isAwaitingUser(s) {
|
|
|
540
539
|
return s.status === 'input_required' || s.activity === 'waiting_input';
|
|
541
540
|
}
|
|
542
541
|
/**
|
|
543
|
-
* Whether a row belongs in an unqualified `--active` view
|
|
542
|
+
* Whether a row belongs in an unqualified `--active` view (RUSH-2336) — the
|
|
543
|
+
* ONE canonical selector every bare-active surface shares: the CLI's grouped
|
|
544
|
+
* table and `--json` (`renderActiveSessions`), the interactive browser's
|
|
545
|
+
* `--active` filter ({@link applyFilters} in `sessions-browser.ts`), `focus`'s
|
|
546
|
+
* attach gate (`isAttachableLiveSession`), and the menubar snapshot
|
|
547
|
+
* ({@link computeMenubarSnapshot}).
|
|
544
548
|
*
|
|
545
|
-
* The live registry deliberately retains terminally-dead
|
|
546
|
-
* `--closed` / `--crashed` and recovery to
|
|
547
|
-
* therefore is not, by itself, evidence
|
|
548
|
-
* Explicit lifecycle filters bypass this
|
|
549
|
-
* rows through {@link matchesLiveStatus}
|
|
549
|
+
* The live registry deliberately retains terminally-dead AND not-yet-started
|
|
550
|
+
* rows long enough for `--closed` / `--crashed` / `--queued` and recovery to
|
|
551
|
+
* find them. Presence in that registry therefore is not, by itself, evidence
|
|
552
|
+
* that a session is CURRENTLY active. Explicit lifecycle filters bypass this
|
|
553
|
+
* predicate and select those retained rows through {@link matchesLiveStatus}
|
|
554
|
+
* instead — this function only decides the unqualified default.
|
|
555
|
+
*
|
|
556
|
+
* - `queued` / `closed` / `crashed` are never active here: queued hasn't
|
|
557
|
+
* started yet, closed/crashed are unconditionally dead. All three stay
|
|
558
|
+
* reachable through their explicit filter.
|
|
559
|
+
* - A cloud row carries no local pid at all — it is active on the
|
|
560
|
+
* provider's own word (`cloudProvider` + `cloudTaskId`), never a
|
|
561
|
+
* fabricated pid.
|
|
562
|
+
* - Every other row is a real OS process (terminal/tmux/headless/team). It
|
|
563
|
+
* is active only when the row names the machine it runs on, carries a
|
|
564
|
+
* positive pid, AND has POSITIVELY verified that pid is alive
|
|
565
|
+
* (`pidAlive === true` — not merely "not known dead"). A live orphaned
|
|
566
|
+
* row, or a live-but-stuck abandoned row, still qualifies this way since
|
|
567
|
+
* both carry a genuinely alive pid; unknown liveness (an older peer's
|
|
568
|
+
* row, or a pid that could never be resolved) does not.
|
|
550
569
|
*/
|
|
551
570
|
export function isRunningLiveSession(s) {
|
|
552
|
-
if (s.status === 'closed' || s.status === 'crashed')
|
|
571
|
+
if (s.status === 'queued' || s.status === 'closed' || s.status === 'crashed')
|
|
553
572
|
return false;
|
|
554
|
-
|
|
573
|
+
if (s.context === 'cloud')
|
|
574
|
+
return Boolean(s.cloudProvider) && Boolean(s.cloudTaskId);
|
|
575
|
+
return Boolean(s.machine) && typeof s.pid === 'number' && s.pid > 0 && s.pidAlive === true;
|
|
555
576
|
}
|
|
556
577
|
/** Width of the live status column — `crashed` is the longest word it renders. */
|
|
557
578
|
const LIVE_STATUS_W = 8;
|
|
@@ -640,6 +661,11 @@ function signalBadges(s) {
|
|
|
640
661
|
* (a real `tmux attach -t <session:window>` target) over the raw `%pane` id. For
|
|
641
662
|
* a local Ghostty session we know the tab, show `tab N`. Local, unlocatable
|
|
642
663
|
* sessions add nothing (the common case).
|
|
664
|
+
*
|
|
665
|
+
* RUSH-2336: every row also carries its raw process/provider handle — a
|
|
666
|
+
* `machine:pid` for a real OS process (terminal/tmux/headless/team), or
|
|
667
|
+
* `provider · taskId` for a cloud row with no local pid — so the human view
|
|
668
|
+
* exposes the same locator `--json` now guarantees on every active row.
|
|
643
669
|
*/
|
|
644
670
|
function locatorBadge(s) {
|
|
645
671
|
const p = s.provenance;
|
|
@@ -662,6 +688,14 @@ function locatorBadge(s) {
|
|
|
662
688
|
}
|
|
663
689
|
if (s.ghosttyTab != null)
|
|
664
690
|
parts.push(chalk.green(`tab ${s.ghosttyTab}`));
|
|
691
|
+
if (s.context === 'cloud') {
|
|
692
|
+
const bits = [s.cloudProvider, s.cloudTaskId ? s.cloudTaskId.slice(0, 12) : undefined].filter(Boolean);
|
|
693
|
+
if (bits.length)
|
|
694
|
+
parts.push(chalk.dim(bits.join(' · ')));
|
|
695
|
+
}
|
|
696
|
+
else if (typeof s.pid === 'number' && s.pid > 0) {
|
|
697
|
+
parts.push(chalk.dim(`${s.machine ? `${s.machine}:` : ''}pid ${s.pid}`));
|
|
698
|
+
}
|
|
665
699
|
return parts.join(' ');
|
|
666
700
|
}
|
|
667
701
|
/**
|
package/dist/commands/teams.js
CHANGED
|
@@ -1395,10 +1395,17 @@ export function registerTeamsCommands(program) {
|
|
|
1395
1395
|
catch (err) {
|
|
1396
1396
|
dieFriction('teams', 'ssh-target-unresolvable', `Can't resolve an ssh target for "${host.name}": ${err.message}`);
|
|
1397
1397
|
}
|
|
1398
|
-
// Ensure agents-cli is present +
|
|
1399
|
-
//
|
|
1398
|
+
// Ensure agents-cli is present + the pin is installed on the host.
|
|
1399
|
+
// Bare agent names still only warn; a concrete pin fails loud so
|
|
1400
|
+
// `teams add --device` never stages a teammate the box cannot run
|
|
1401
|
+
// (RUSH-2313). parseTeammate already resolves @latest/etc. to a concrete
|
|
1402
|
+
// version when one is pinned on the teammate spec.
|
|
1400
1403
|
try {
|
|
1401
|
-
const
|
|
1404
|
+
const parsed = parseTeammate(teammate);
|
|
1405
|
+
const { warnings } = ensureHostReady(host, {
|
|
1406
|
+
agent: parsed.agent,
|
|
1407
|
+
version: parsed.version ?? undefined,
|
|
1408
|
+
});
|
|
1402
1409
|
for (const w of warnings)
|
|
1403
1410
|
process.stderr.write(chalk.yellow(`[teams] warning: ${w}\n`));
|
|
1404
1411
|
}
|
|
@@ -1417,7 +1424,9 @@ export function registerTeamsCommands(program) {
|
|
|
1417
1424
|
effectiveRepo = (await getRemoteUrl(process.cwd())) ?? '';
|
|
1418
1425
|
}
|
|
1419
1426
|
try {
|
|
1420
|
-
hostRepoPath = ensureRemoteRepo(hostTarget, effectiveRepo, team
|
|
1427
|
+
hostRepoPath = ensureRemoteRepo(hostTarget, effectiveRepo, team, {
|
|
1428
|
+
extraSshArgs: host.identityFile ? ['-i', host.identityFile, '-o', 'IdentitiesOnly=yes'] : [],
|
|
1429
|
+
});
|
|
1421
1430
|
}
|
|
1422
1431
|
catch (err) {
|
|
1423
1432
|
dieFriction('teams', 'repo-provision-failed', `Couldn't provision the repo on "${host.name}": ${err.message}\n` +
|
|
@@ -2018,11 +2027,12 @@ export function registerTeamsCommands(program) {
|
|
|
2018
2027
|
try {
|
|
2019
2028
|
if (agent.hostName && agent.hostTarget && agent.repoPath) {
|
|
2020
2029
|
// Distributed teammate: guard + remove the worktree ON THE HOST.
|
|
2021
|
-
|
|
2030
|
+
const ssh = { extraSshArgs: agent.hostIdentityFile ? ['-i', agent.hostIdentityFile, '-o', 'IdentitiesOnly=yes'] : [] };
|
|
2031
|
+
if (remoteWorktreeDirty(agent.hostTarget, agent.worktreePath, ssh)) {
|
|
2022
2032
|
worktreeKept = true;
|
|
2023
2033
|
}
|
|
2024
2034
|
else {
|
|
2025
|
-
removeRemoteWorktree(agent.hostTarget, agent.repoPath, agent.worktreeName);
|
|
2035
|
+
removeRemoteWorktree(agent.hostTarget, agent.repoPath, agent.worktreeName, true, ssh);
|
|
2026
2036
|
}
|
|
2027
2037
|
}
|
|
2028
2038
|
else {
|
|
@@ -2231,11 +2241,12 @@ export function registerTeamsCommands(program) {
|
|
|
2231
2241
|
try {
|
|
2232
2242
|
if (agent.hostName && agent.hostTarget && agent.repoPath) {
|
|
2233
2243
|
// Distributed teammate: guard + remove the worktree ON THE HOST.
|
|
2234
|
-
|
|
2244
|
+
const ssh = { extraSshArgs: agent.hostIdentityFile ? ['-i', agent.hostIdentityFile, '-o', 'IdentitiesOnly=yes'] : [] };
|
|
2245
|
+
if (remoteWorktreeDirty(agent.hostTarget, agent.worktreePath, ssh)) {
|
|
2235
2246
|
keptWorktrees.push(agent.worktreeName);
|
|
2236
2247
|
}
|
|
2237
2248
|
else {
|
|
2238
|
-
removeRemoteWorktree(agent.hostTarget, agent.repoPath, agent.worktreeName);
|
|
2249
|
+
removeRemoteWorktree(agent.hostTarget, agent.repoPath, agent.worktreeName, true, ssh);
|
|
2239
2250
|
}
|
|
2240
2251
|
}
|
|
2241
2252
|
else {
|