@phnx-labs/agents-cli 1.20.67 → 1.20.68
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 +55 -0
- package/README.md +3 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/check.js +96 -2
- package/dist/commands/doctor.js +37 -9
- package/dist/commands/exec.js +39 -1
- package/dist/commands/plugins.js +11 -2
- package/dist/commands/repo.js +47 -2
- package/dist/commands/ssh.js +103 -2
- package/dist/commands/teams.d.ts +11 -0
- package/dist/commands/teams.js +39 -1
- package/dist/commands/view.d.ts +1 -0
- package/dist/commands/view.js +12 -8
- package/dist/lib/agents.d.ts +11 -5
- package/dist/lib/agents.js +27 -24
- package/dist/lib/browser/profiles.js +4 -25
- package/dist/lib/cli-entry.d.ts +23 -0
- package/dist/lib/cli-entry.js +116 -0
- package/dist/lib/daemon.d.ts +2 -2
- package/dist/lib/daemon.js +8 -89
- package/dist/lib/devices/fleet.d.ts +23 -0
- package/dist/lib/devices/fleet.js +38 -0
- package/dist/lib/devices/health-report.d.ts +46 -0
- package/dist/lib/devices/health-report.js +159 -0
- package/dist/lib/devices/health.d.ts +14 -4
- package/dist/lib/devices/health.js +49 -8
- package/dist/lib/git.d.ts +10 -0
- package/dist/lib/git.js +23 -0
- package/dist/lib/hosts/option.js +1 -1
- package/dist/lib/hosts/ready.js +5 -3
- package/dist/lib/hosts/remote-cmd.d.ts +12 -0
- package/dist/lib/hosts/remote-cmd.js +17 -1
- package/dist/lib/mcp.d.ts +21 -0
- package/dist/lib/mcp.js +278 -13
- package/dist/lib/resources/mcp.js +20 -342
- package/dist/lib/routines.d.ts +13 -0
- package/dist/lib/routines.js +21 -0
- package/dist/lib/runner.js +18 -40
- package/dist/lib/secrets/agent.js +11 -15
- package/dist/lib/share/capture.js +21 -3
- package/dist/lib/signin-badge.d.ts +41 -0
- package/dist/lib/signin-badge.js +64 -0
- package/dist/lib/ssh-exec.d.ts +5 -0
- package/dist/lib/ssh-exec.js +55 -1
- package/dist/lib/usage.d.ts +17 -3
- package/dist/lib/usage.js +63 -16
- package/package.json +1 -1
package/dist/commands/teams.js
CHANGED
|
@@ -809,6 +809,23 @@ async function pickTeamOr(mgr, command) {
|
|
|
809
809
|
throw err;
|
|
810
810
|
}
|
|
811
811
|
}
|
|
812
|
+
/**
|
|
813
|
+
* `teams add --remote-cwd` is a no-op trap. `--remote-cwd` comes from the shared
|
|
814
|
+
* `--host` option family (option.ts) and is meaningful for commands that *route*
|
|
815
|
+
* to a host, but `teams add` special-cases `--host`/`--device` as PLACEMENT, so
|
|
816
|
+
* the flag is never read — a teammate's directory is the team's repo plus its
|
|
817
|
+
* `--worktree`, not a path passed here. Silently ignoring it misleads you into
|
|
818
|
+
* thinking it set the teammate's repo path (the exact wrong model that turns one
|
|
819
|
+
* team into a teardown-and-rebuild). Reject with guidance instead. Exported for
|
|
820
|
+
* the unit test that pins the message.
|
|
821
|
+
*/
|
|
822
|
+
export function remoteCwdOnAddError(team) {
|
|
823
|
+
return (`--remote-cwd has no effect on 'teams add' — it does not set a teammate's repo or directory.\n` +
|
|
824
|
+
` A teammate works in the team's repo plus its own --worktree:\n` +
|
|
825
|
+
` • Set the repo once, on the team: agents teams create ${team} --repo <url|path>\n` +
|
|
826
|
+
` • Place the teammate on a machine: agents teams add ${team} <agent> "<task>" --device <host> --worktree <name>\n` +
|
|
827
|
+
` (Remote worktrees fork from the host's freshly-fetched origin/<default> automatically.)`);
|
|
828
|
+
}
|
|
812
829
|
/** Register the `agents teams` command tree (list, create, add, status, start, remove, disband, logs, doctor). */
|
|
813
830
|
export function registerTeamsCommands(program) {
|
|
814
831
|
const teams = program
|
|
@@ -853,6 +870,20 @@ export function registerTeamsCommands(program) {
|
|
|
853
870
|
'<profile>' a profile from 'agents view' — runs through 'agents
|
|
854
871
|
run <profile>' with the profile's host harness
|
|
855
872
|
|
|
873
|
+
Placement & repos (the part people get wrong):
|
|
874
|
+
--remote-cwd does NOT place a teammate or set its repo — it is ignored on
|
|
875
|
+
'teams add' (and rejected, so you find out immediately). A teammate's
|
|
876
|
+
directory is the team's repo plus its --worktree:
|
|
877
|
+
--device <host> run THIS teammate on <host>
|
|
878
|
+
create --repo <r> ONE repo for the whole team (defaults to this
|
|
879
|
+
checkout's origin). Work spanning repos → one team
|
|
880
|
+
per repo — don't build a cross-repo team then rebuild.
|
|
881
|
+
With --enable-worktrees each teammate gets its own worktree + branch:
|
|
882
|
+
local teammate forks from your CURRENT local HEAD — no fetch, so
|
|
883
|
+
pull/sync the checkout first or it forks stale
|
|
884
|
+
remote (--device) forks from the freshly-fetched origin/<default> on
|
|
885
|
+
the host (no manual sync needed)
|
|
886
|
+
|
|
856
887
|
Short aliases:
|
|
857
888
|
teams c = create teams a = add teams s = status
|
|
858
889
|
teams rm = remove teams d = disband teams ls = list
|
|
@@ -978,7 +1009,7 @@ export function registerTeamsCommands(program) {
|
|
|
978
1009
|
.option('--use-worktree <path>', 'All teammates share this existing worktree path (mutually exclusive with --enable-worktrees)')
|
|
979
1010
|
.option('--devices <list>', 'Pool of machines this team may run teammates on (comma-separated). Enables distributed auto-scheduling.')
|
|
980
1011
|
.option('--hosts <list>', 'Alias for --devices.')
|
|
981
|
-
.option('--repo <urlOrPath>', 'How each device gets the code
|
|
1012
|
+
.option('--repo <urlOrPath>', 'How each remote (--device) teammate gets the code — ONE git URL/path for the whole team (existing checkout reused, else cloned). A team is single-repo; for work across repos, make one team per repo. Defaults to this checkout origin.')
|
|
982
1013
|
.option('--json', 'Output machine-readable JSON')
|
|
983
1014
|
.action(async (team, opts) => {
|
|
984
1015
|
try {
|
|
@@ -1073,6 +1104,13 @@ export function registerTeamsCommands(program) {
|
|
|
1073
1104
|
.option('--force', "Skip the advisory 'may not be signed in' / 'account throttled' warnings")
|
|
1074
1105
|
.option('--json', 'Output machine-readable JSON')
|
|
1075
1106
|
.action(async (team, teammate, task, opts) => {
|
|
1107
|
+
// `--remote-cwd` rides the shared --host option family but is never read by
|
|
1108
|
+
// `teams add` (placement, not routing). Fail loud with guidance rather than
|
|
1109
|
+
// silently ignoring it — see remoteCwdOnAddError. `!== undefined` so even an
|
|
1110
|
+
// explicit empty value (`--remote-cwd ""`) is rejected, not silently dropped.
|
|
1111
|
+
if (opts.remoteCwd !== undefined) {
|
|
1112
|
+
die(remoteCwdOnAddError(team));
|
|
1113
|
+
}
|
|
1076
1114
|
if (!VALID_MODES.includes(opts.mode)) {
|
|
1077
1115
|
die(`Invalid mode '${opts.mode}'. Use one of: ${VALID_MODES.join(', ')}`);
|
|
1078
1116
|
}
|
package/dist/commands/view.d.ts
CHANGED
|
@@ -133,6 +133,7 @@ export declare function viewAction(agentArg?: string, options?: {
|
|
|
133
133
|
dryRun?: boolean;
|
|
134
134
|
resources?: string | boolean;
|
|
135
135
|
detailed?: boolean;
|
|
136
|
+
refresh?: boolean;
|
|
136
137
|
} & ViewSectionFilter): Promise<void>;
|
|
137
138
|
/** Register the `agents view` command. */
|
|
138
139
|
export declare function registerViewCommand(program: Command): void;
|
package/dist/commands/view.js
CHANGED
|
@@ -5,7 +5,8 @@ import ora from 'ora';
|
|
|
5
5
|
import * as fs from 'fs';
|
|
6
6
|
import * as path from 'path';
|
|
7
7
|
import { AGENTS, ALL_AGENT_IDS, accountOrgBadge, getAllCliStates, getAccountInfo, resolveAgentName, formatAgentError, agentLabel, colorAgent, } from '../lib/agents.js';
|
|
8
|
-
import {
|
|
8
|
+
import { loginHint } from '../lib/signin-badge.js';
|
|
9
|
+
import { agentReportsUsage, deriveUsageStatusFromSnapshot, formatUsageSection, formatUsageSummary, formatUsageStatusBadge, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, } from '../lib/usage.js';
|
|
9
10
|
import { readManifest } from '../lib/manifest.js';
|
|
10
11
|
import { listInstalledVersions, listInstalledVersionDirs, getGlobalDefault, getVersionHomePath, getVersionDir, getAvailableResources, getActuallySyncedResources, getNewResources, getProjectOnlyResources, hasNewResources, promptNewResourceSelection, syncResourcesToVersion, removeVersion, printTrashFooter, reconcileStaleLatestForAgent, isGlobalBinaryAgent, getLiveVersion, } from '../lib/versions.js';
|
|
11
12
|
import { ensureVersionedAliasCurrent, removeShim, } from '../lib/shims.js';
|
|
@@ -244,7 +245,7 @@ function renderHostClisSection(cwd) {
|
|
|
244
245
|
console.log(` ${chalk.red('error')} ${chalk.gray(err.file)}: ${chalk.gray(err.reason)}`);
|
|
245
246
|
}
|
|
246
247
|
}
|
|
247
|
-
async function showInstalledVersions(filterAgentId) {
|
|
248
|
+
async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
248
249
|
const spinnerText = filterAgentId
|
|
249
250
|
? `Checking ${agentLabel(filterAgentId)} agents...`
|
|
250
251
|
: 'Checking installed agents...';
|
|
@@ -326,7 +327,7 @@ async function showInstalledVersions(filterAgentId) {
|
|
|
326
327
|
cliVersion,
|
|
327
328
|
info,
|
|
328
329
|
})),
|
|
329
|
-
]);
|
|
330
|
+
], { forceRefresh: viewOpts?.forceRefresh });
|
|
330
331
|
const mergeCanonical = (info) => {
|
|
331
332
|
const key = getUsageLookupKey(info);
|
|
332
333
|
if (!key)
|
|
@@ -417,7 +418,8 @@ async function showInstalledVersions(filterAgentId) {
|
|
|
417
418
|
const info = rawInfo ? mergeCanonical(rawInfo) : undefined;
|
|
418
419
|
const usageKey = getUsageLookupKey(info);
|
|
419
420
|
const usageInfo = usageKey ? usageByKey.get(usageKey) : undefined;
|
|
420
|
-
const
|
|
421
|
+
const usageUnavailable = agentReportsUsage(agentId) && !!info?.signedIn && !usageInfo?.snapshot;
|
|
422
|
+
const usageStr = formatUsageSummary(info?.plan || null, usageInfo?.snapshot || null, maxPlanWidth, { unavailable: usageUnavailable });
|
|
421
423
|
maxUsageWidth = Math.max(maxUsageWidth, visibleWidth(usageStr));
|
|
422
424
|
const statusStr = formatUsageStatusBadge(info?.usageStatus);
|
|
423
425
|
maxStatusWidth = Math.max(maxStatusWidth, visibleWidth(statusStr));
|
|
@@ -457,7 +459,8 @@ async function showInstalledVersions(filterAgentId) {
|
|
|
457
459
|
const parts = [` ${label}`];
|
|
458
460
|
const hasEmail = !!vInfo?.email;
|
|
459
461
|
const signedIn = !!vInfo?.signedIn;
|
|
460
|
-
const
|
|
462
|
+
const usageUnavailable = agentReportsUsage(agentId) && signedIn && !usageInfo?.snapshot;
|
|
463
|
+
const usageStr = formatUsageSummary(vInfo?.plan || null, usageInfo?.snapshot || null, maxPlanWidth, { unavailable: usageUnavailable });
|
|
461
464
|
const hasUsage = usageStr.length > 0;
|
|
462
465
|
// Only show lastActive for versions with an actual logged-in account.
|
|
463
466
|
// Otherwise it reflects install time (misleading "just now" for fresh installs).
|
|
@@ -471,7 +474,7 @@ async function showInstalledVersions(filterAgentId) {
|
|
|
471
474
|
runDefaultBits.push(`model:${runDefaults.model}`);
|
|
472
475
|
if (!hasEmail && !hasUsage && !signedIn) {
|
|
473
476
|
// Installed but never signed in
|
|
474
|
-
parts.push(chalk.gray('(
|
|
477
|
+
parts.push(chalk.gray('(logged out — log in with: ' + loginHint(agentId) + ')'));
|
|
475
478
|
}
|
|
476
479
|
else {
|
|
477
480
|
if (hasEmail || hasUsage || hasActive || signedIn) {
|
|
@@ -1438,7 +1441,7 @@ export async function viewAction(agentArg, options) {
|
|
|
1438
1441
|
return;
|
|
1439
1442
|
}
|
|
1440
1443
|
// No argument: show all installed versions
|
|
1441
|
-
await showInstalledVersions();
|
|
1444
|
+
await showInstalledVersions(undefined, { forceRefresh: options?.refresh === true });
|
|
1442
1445
|
return;
|
|
1443
1446
|
}
|
|
1444
1447
|
// Parse agent@version syntax
|
|
@@ -1499,7 +1502,7 @@ export async function viewAction(agentArg, options) {
|
|
|
1499
1502
|
}
|
|
1500
1503
|
else {
|
|
1501
1504
|
// Just agent name: show versions for that agent
|
|
1502
|
-
await showInstalledVersions(agentId);
|
|
1505
|
+
await showInstalledVersions(agentId, { forceRefresh: options?.refresh === true });
|
|
1503
1506
|
}
|
|
1504
1507
|
}
|
|
1505
1508
|
/** Register the `agents view` command. */
|
|
@@ -1509,6 +1512,7 @@ export function registerViewCommand(program) {
|
|
|
1509
1512
|
.option('--json', 'Emit machine-readable JSON (version list, usage, signed-in status).')
|
|
1510
1513
|
.option('--resources [sections]', 'In --json mode, include each version\'s resources: "all" (default) or a comma list (skills,plugins,mcp,commands,workflows,memory,hooks). Implies --json.')
|
|
1511
1514
|
.option('--detailed', 'Include all resources in --json output (alias for --resources all). Implies --json.')
|
|
1515
|
+
.option('-r, --refresh', 'Force a live usage refresh, bypassing the cache (slower). Repopulates the S:/W: limit bars for every account whose token is reachable.')
|
|
1512
1516
|
.option('--prune', 'Remove older installed versions that share an account with a newer installed version. Skips the global default.')
|
|
1513
1517
|
.option('--dry-run', 'With --prune, show duplicate versions without deleting')
|
|
1514
1518
|
.option('-y, --yes', 'Skip the prune confirmation prompt.')
|
package/dist/lib/agents.d.ts
CHANGED
|
@@ -139,11 +139,13 @@ export interface AccountInfo {
|
|
|
139
139
|
*/
|
|
140
140
|
export declare function formatClaudeOrgLabel(orgType: string | null | undefined): string | null;
|
|
141
141
|
/**
|
|
142
|
-
* Short badge identifying which
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
142
|
+
* Short badge identifying which ORG an account belongs to — "Turing Labs" for a
|
|
143
|
+
* multi-seat Team/Enterprise org, whose name is real identity that disambiguates
|
|
144
|
+
* a Team seat from a same-email personal plan. Returns null for personal plans
|
|
145
|
+
* (Max/Pro/Free): the tier label now lives in the aligned plan column, so a badge
|
|
146
|
+
* would only duplicate it, and a personal org's name is auto-generated boilerplate
|
|
147
|
+
* ("<email>'s Organization"), not identity. Also null when the account carries no
|
|
148
|
+
* organizationType (signed out, non-Claude agents, configs predating the field).
|
|
147
149
|
*/
|
|
148
150
|
export declare function accountOrgBadge(info?: Pick<AccountInfo, 'organizationType' | 'organizationName'> | null): string | null;
|
|
149
151
|
/** Return the email address associated with the agent's auth config, or null. */
|
|
@@ -303,6 +305,10 @@ export declare function getUserMcpConfigPath(agentId: AgentId): string;
|
|
|
303
305
|
* Get MCP config path for a specific HOME directory (used for version-managed agents).
|
|
304
306
|
*/
|
|
305
307
|
export declare function getMcpConfigPathForHome(agentId: AgentId, home: string): string;
|
|
308
|
+
/**
|
|
309
|
+
* Get project-scoped MCP config path for an agent.
|
|
310
|
+
*/
|
|
311
|
+
export declare function getProjectMcpConfigPath(agentId: AgentId, cwd?: string): string;
|
|
306
312
|
/**
|
|
307
313
|
* Parse MCP config based on agent type.
|
|
308
314
|
*/
|
package/dist/lib/agents.js
CHANGED
|
@@ -978,20 +978,19 @@ export function formatClaudeOrgLabel(orgType) {
|
|
|
978
978
|
.join(' ');
|
|
979
979
|
}
|
|
980
980
|
/**
|
|
981
|
-
* Short badge identifying which
|
|
982
|
-
*
|
|
983
|
-
*
|
|
984
|
-
*
|
|
985
|
-
*
|
|
981
|
+
* Short badge identifying which ORG an account belongs to — "Turing Labs" for a
|
|
982
|
+
* multi-seat Team/Enterprise org, whose name is real identity that disambiguates
|
|
983
|
+
* a Team seat from a same-email personal plan. Returns null for personal plans
|
|
984
|
+
* (Max/Pro/Free): the tier label now lives in the aligned plan column, so a badge
|
|
985
|
+
* would only duplicate it, and a personal org's name is auto-generated boilerplate
|
|
986
|
+
* ("<email>'s Organization"), not identity. Also null when the account carries no
|
|
987
|
+
* organizationType (signed out, non-Claude agents, configs predating the field).
|
|
986
988
|
*/
|
|
987
989
|
export function accountOrgBadge(info) {
|
|
988
|
-
const label = formatClaudeOrgLabel(info?.organizationType);
|
|
989
|
-
if (!label)
|
|
990
|
-
return null;
|
|
991
990
|
const isMultiSeat = info?.organizationType === 'claude_team' || info?.organizationType === 'claude_enterprise';
|
|
992
991
|
if (isMultiSeat && info?.organizationName)
|
|
993
|
-
return
|
|
994
|
-
return
|
|
992
|
+
return info.organizationName;
|
|
993
|
+
return null;
|
|
995
994
|
}
|
|
996
995
|
/** Return the email address associated with the agent's auth config, or null. */
|
|
997
996
|
export async function getAccountEmail(agentId, home) {
|
|
@@ -1321,19 +1320,23 @@ export async function getAccountInfo(agentId, home) {
|
|
|
1321
1320
|
['org', organizationId],
|
|
1322
1321
|
]);
|
|
1323
1322
|
const usageKey = buildIdentityKey(agentId, [['org', organizationId]]);
|
|
1324
|
-
// Plan is derived from .claude.json's
|
|
1325
|
-
//
|
|
1326
|
-
//
|
|
1327
|
-
//
|
|
1328
|
-
//
|
|
1329
|
-
//
|
|
1330
|
-
//
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1323
|
+
// Plan tier is derived from .claude.json's organizationType, which carries
|
|
1324
|
+
// the TRUE tier (claude_max → "Max", claude_pro → "Pro", claude_team →
|
|
1325
|
+
// "Team") and is already in-hand from the config we just read — no Keychain
|
|
1326
|
+
// prompt. billingType only distinguishes "has a Stripe subscription" and so
|
|
1327
|
+
// mislabels every Max account as "Pro"; keep it as a fallback for older
|
|
1328
|
+
// configs predating organizationType. (Reading subscriptionType from the
|
|
1329
|
+
// keychain item would force a macOS Keychain ACL prompt on every `agents
|
|
1330
|
+
// run`, so we deliberately avoid it — organizationType gives us the tier
|
|
1331
|
+
// without that cost.)
|
|
1332
|
+
let plan = formatClaudeOrgLabel(oa?.organizationType);
|
|
1333
|
+
if (!plan) {
|
|
1334
|
+
if (oa?.billingType === 'stripe_subscription') {
|
|
1335
|
+
plan = 'Pro';
|
|
1336
|
+
}
|
|
1337
|
+
else if (oa?.billingType) {
|
|
1338
|
+
plan = oa.billingType;
|
|
1339
|
+
}
|
|
1337
1340
|
}
|
|
1338
1341
|
// usageStatus is NOT derived from cachedExtraUsageDisabledReason. That
|
|
1339
1342
|
// field reports why pay-as-you-go overage is off (out_of_credits = no
|
|
@@ -2194,7 +2197,7 @@ export function getMcpConfigPathForHome(agentId, home) {
|
|
|
2194
2197
|
/**
|
|
2195
2198
|
* Get project-scoped MCP config path for an agent.
|
|
2196
2199
|
*/
|
|
2197
|
-
function getProjectMcpConfigPath(agentId, cwd = process.cwd()) {
|
|
2200
|
+
export function getProjectMcpConfigPath(agentId, cwd = process.cwd()) {
|
|
2198
2201
|
switch (agentId) {
|
|
2199
2202
|
case 'claude':
|
|
2200
2203
|
// Claude uses .mcp.json at project root for project-scoped MCPs
|
|
@@ -228,27 +228,6 @@ function hasSshEndpoint(endpoints) {
|
|
|
228
228
|
}
|
|
229
229
|
});
|
|
230
230
|
}
|
|
231
|
-
/**
|
|
232
|
-
* True when any endpoint is an `ssh://…?os=windows` target — i.e. the browser
|
|
233
|
-
* lives on a remote Windows host. Such a profile's binary (`msedge.exe`) will
|
|
234
|
-
* never exist on this Mac, so create-time local-binary validation must be
|
|
235
|
-
* skipped; the binary is resolved on the remote at connect time instead.
|
|
236
|
-
*/
|
|
237
|
-
function hasRemoteWindowsEndpoint(endpoints) {
|
|
238
|
-
const targets = Array.isArray(endpoints)
|
|
239
|
-
? endpoints
|
|
240
|
-
: Object.values(endpoints).map((preset) => preset.target);
|
|
241
|
-
return targets.some((target) => {
|
|
242
|
-
try {
|
|
243
|
-
const url = new URL(target);
|
|
244
|
-
return (url.protocol === 'ssh:' &&
|
|
245
|
-
(url.searchParams.get('os') || '').toLowerCase() === 'windows');
|
|
246
|
-
}
|
|
247
|
-
catch {
|
|
248
|
-
return false;
|
|
249
|
-
}
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
231
|
export async function createProfile(profile) {
|
|
253
232
|
const meta = readMeta();
|
|
254
233
|
if (meta.browser?.[profile.name]) {
|
|
@@ -276,10 +255,10 @@ export async function createProfile(profile) {
|
|
|
276
255
|
// deferring the failure to the first task. `findBrowserPath` short-circuits
|
|
277
256
|
// for browser=custom without a binary by throwing — same outcome.
|
|
278
257
|
//
|
|
279
|
-
// Skip for
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
if (!
|
|
258
|
+
// Skip for SSH profiles: the browser binary lives on the remote host, so a
|
|
259
|
+
// local lookup would validate the wrong machine. The remote launcher resolves
|
|
260
|
+
// it at connect time.
|
|
261
|
+
if (!hasSshEndpoint(profile.endpoints)) {
|
|
283
262
|
findBrowserPath(profile.browser, profile.binary);
|
|
284
263
|
}
|
|
285
264
|
meta.browser = meta.browser ?? {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare const BUN_VIRTUAL_ROOT: RegExp;
|
|
2
|
+
export declare function getAgentsBinPath(argv1?: string | undefined, execPath?: string): string;
|
|
3
|
+
/**
|
|
4
|
+
* A CLI entry must be launched through the Node runtime when it is a Node
|
|
5
|
+
* script — a `.js`/`.cjs`/`.mjs` file, OR a symlink/extension-less shim whose
|
|
6
|
+
* shebang names `node`. Package installs link `bin/agents` to a `dist/index.js`
|
|
7
|
+
* (a symlink) or drop an extension-less `#!/usr/bin/env node` shim, so an
|
|
8
|
+
* extension check alone misses them and they get run directly. A real compiled
|
|
9
|
+
* binary (Mach-O/ELF/PE) has no `#!node` shebang, so it takes the direct branch
|
|
10
|
+
* and owns its own runtime resolution.
|
|
11
|
+
*/
|
|
12
|
+
export declare function isNodeScriptEntry(agentsBin: string): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Build the `{ command, args }` to re-invoke this CLI with `sub` as its argv,
|
|
15
|
+
* resolving the JS-vs-standalone shape above. This is the single primitive behind
|
|
16
|
+
* both the daemon launch and the secrets-broker spawn — never hand-roll
|
|
17
|
+
* `[process.execPath, process.argv[1], …]`, which appends the bun virtual entry
|
|
18
|
+
* as a bogus subcommand on standalone builds.
|
|
19
|
+
*/
|
|
20
|
+
export declare function getCliLaunch(sub: string[], agentsBin?: string): {
|
|
21
|
+
command: string;
|
|
22
|
+
args: string[];
|
|
23
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve how to re-invoke THIS `agents` CLI as a child process, correctly across
|
|
3
|
+
* both install shapes:
|
|
4
|
+
*
|
|
5
|
+
* 1. **JS install** — `agents` is a `dist/index.js` (or a symlink / `#!node`
|
|
6
|
+
* shim to it). `process.execPath` is `node`, `process.argv[1]` is the script.
|
|
7
|
+
* Relaunch as `node <entry> <sub…>`.
|
|
8
|
+
* 2. **Bun standalone binary** (#315) — `agents` is a compiled Mach-O/ELF/PE.
|
|
9
|
+
* `process.execPath` is the physical signed binary, and `process.argv[1]` is
|
|
10
|
+
* the *virtual* embedded entry `/$bunfs/root/agents`, which Bun reports as an
|
|
11
|
+
* existing path. Passing that virtual path as an argv element makes the CLI
|
|
12
|
+
* receive it as a subcommand and die with `unknown command '/$bunfs/root/agents'`.
|
|
13
|
+
* Relaunch by executing the physical binary directly: `<binary> <sub…>`.
|
|
14
|
+
*
|
|
15
|
+
* Both `getDaemonLaunch` (daemon.ts) and the secrets-broker `cliSpawn`
|
|
16
|
+
* (secrets/agent.ts) route through here so the two never drift. This module is a
|
|
17
|
+
* leaf — it imports nothing from `lib/` — so it can be pulled into either without
|
|
18
|
+
* an import cycle (daemon.ts ↔ secrets/agent.ts already form one).
|
|
19
|
+
*/
|
|
20
|
+
import { execFileSync } from 'child_process';
|
|
21
|
+
import * as fs from 'fs';
|
|
22
|
+
import * as path from 'path';
|
|
23
|
+
export const BUN_VIRTUAL_ROOT = /[/\\]\$bunfs[/\\]root[/\\]/;
|
|
24
|
+
function resolveBunStandaloneEntry(entry, execPath) {
|
|
25
|
+
if (!BUN_VIRTUAL_ROOT.test(entry))
|
|
26
|
+
return entry;
|
|
27
|
+
if (!execPath || BUN_VIRTUAL_ROOT.test(execPath) || !fs.existsSync(execPath)) {
|
|
28
|
+
throw new Error(`Cannot resolve agents CLI: Bun standalone executable not found at ${execPath || '(empty path)'}`);
|
|
29
|
+
}
|
|
30
|
+
return execPath;
|
|
31
|
+
}
|
|
32
|
+
export function getAgentsBinPath(argv1 = process.argv[1], execPath = process.execPath) {
|
|
33
|
+
// Prefer the binary actively executing this code. `which agents` returns
|
|
34
|
+
// whatever happens to be first on PATH, which means a side-by-side dev
|
|
35
|
+
// build at ~/.local/bin would silently spawn the registry-installed
|
|
36
|
+
// daemon and run stale code. For a JS install, process.argv[1] is the
|
|
37
|
+
// absolute entrypoint the user actually invoked. A Bun standalone instead
|
|
38
|
+
// exposes its embedded /$bunfs/root entry at argv[1] and its physical signed
|
|
39
|
+
// executable at process.execPath; Bun reports both as existing paths.
|
|
40
|
+
const runningEntry = argv1 ? resolveBunStandaloneEntry(argv1, execPath) : undefined;
|
|
41
|
+
if (runningEntry && fs.existsSync(runningEntry)) {
|
|
42
|
+
// The package's browser/computer entrypoints are sibling shims without a
|
|
43
|
+
// `daemon` command. A daemon started as their IPC side effect must launch
|
|
44
|
+
// through the main agents entrypoint instead of replaying the shim path.
|
|
45
|
+
const entryName = path.basename(runningEntry);
|
|
46
|
+
const compiledShim = /^(browser|computer)\.(c|m)?js$/.test(entryName);
|
|
47
|
+
const installedShim = /^(browser|computer)$/.test(entryName);
|
|
48
|
+
if (compiledShim || installedShim) {
|
|
49
|
+
const agentsEntry = path.join(path.dirname(runningEntry), compiledShim ? 'index.js' : 'agents');
|
|
50
|
+
if (!fs.existsSync(agentsEntry)) {
|
|
51
|
+
throw new Error(`Cannot start agents daemon: main CLI entry not found at ${agentsEntry}`);
|
|
52
|
+
}
|
|
53
|
+
return agentsEntry;
|
|
54
|
+
}
|
|
55
|
+
return runningEntry;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
return execFileSync('which', ['agents'], { encoding: 'utf-8' }).trim();
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return 'agents';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* A CLI entry must be launched through the Node runtime when it is a Node
|
|
66
|
+
* script — a `.js`/`.cjs`/`.mjs` file, OR a symlink/extension-less shim whose
|
|
67
|
+
* shebang names `node`. Package installs link `bin/agents` to a `dist/index.js`
|
|
68
|
+
* (a symlink) or drop an extension-less `#!/usr/bin/env node` shim, so an
|
|
69
|
+
* extension check alone misses them and they get run directly. A real compiled
|
|
70
|
+
* binary (Mach-O/ELF/PE) has no `#!node` shebang, so it takes the direct branch
|
|
71
|
+
* and owns its own runtime resolution.
|
|
72
|
+
*/
|
|
73
|
+
export function isNodeScriptEntry(agentsBin) {
|
|
74
|
+
let resolved = agentsBin;
|
|
75
|
+
try {
|
|
76
|
+
resolved = fs.realpathSync(agentsBin);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// Unresolvable (e.g. a template path that does not exist on this box): fall
|
|
80
|
+
// back to the extension check on the path as given.
|
|
81
|
+
}
|
|
82
|
+
if (/\.(c|m)?js$/.test(resolved))
|
|
83
|
+
return true;
|
|
84
|
+
try {
|
|
85
|
+
const fd = fs.openSync(resolved, 'r');
|
|
86
|
+
try {
|
|
87
|
+
const buf = Buffer.alloc(128);
|
|
88
|
+
const n = fs.readSync(fd, buf, 0, 128, 0);
|
|
89
|
+
const firstLine = buf.toString('utf-8', 0, n).split('\n', 1)[0];
|
|
90
|
+
return firstLine.startsWith('#!') && /\bnode\b/.test(firstLine);
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
fs.closeSync(fd);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Build the `{ command, args }` to re-invoke this CLI with `sub` as its argv,
|
|
102
|
+
* resolving the JS-vs-standalone shape above. This is the single primitive behind
|
|
103
|
+
* both the daemon launch and the secrets-broker spawn — never hand-roll
|
|
104
|
+
* `[process.execPath, process.argv[1], …]`, which appends the bun virtual entry
|
|
105
|
+
* as a bogus subcommand on standalone builds.
|
|
106
|
+
*/
|
|
107
|
+
export function getCliLaunch(sub, agentsBin = getAgentsBinPath()) {
|
|
108
|
+
// Resolve a bun virtual entry to the physical executable even when a caller
|
|
109
|
+
// passes agentsBin explicitly (getAgentsBinPath already does this for the
|
|
110
|
+
// default), so a `/$bunfs/root/agents` never becomes the command or an argv.
|
|
111
|
+
const bin = resolveBunStandaloneEntry(agentsBin, process.execPath);
|
|
112
|
+
if (isNodeScriptEntry(bin)) {
|
|
113
|
+
return { command: process.execPath, args: [bin, ...sub] };
|
|
114
|
+
}
|
|
115
|
+
return { command: bin, args: [...sub] };
|
|
116
|
+
}
|
package/dist/lib/daemon.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* (macOS), systemd (Linux), or as a plain detached process. PID tracking,
|
|
7
7
|
* log output, reload (SIGHUP), and graceful shutdown are handled here.
|
|
8
8
|
*/
|
|
9
|
+
import { getAgentsBinPath } from './cli-entry.js';
|
|
9
10
|
/** Read the stored daemon PID from disk. Returns null if not present or invalid. */
|
|
10
11
|
export declare function readDaemonPid(): number | null;
|
|
11
12
|
/** Write the daemon PID to the pid file. */
|
|
@@ -101,7 +102,7 @@ export declare function generateLaunchdPlist(agentsBin?: string): string;
|
|
|
101
102
|
* store and never touches the unit file.
|
|
102
103
|
*/
|
|
103
104
|
export declare function generateSystemdUnit(agentsBin?: string): string;
|
|
104
|
-
export
|
|
105
|
+
export { getAgentsBinPath };
|
|
105
106
|
/** Start the daemon via launchd, systemd, or as a detached process. */
|
|
106
107
|
export declare function startDaemon(agentsBin?: string): {
|
|
107
108
|
pid: number | null;
|
|
@@ -201,4 +202,3 @@ export declare function getDaemonStatus(): {
|
|
|
201
202
|
export declare function readDaemonLog(lines?: number): string;
|
|
202
203
|
/** Send SIGHUP to the daemon to trigger a job reload. */
|
|
203
204
|
export declare function signalDaemonReload(): boolean;
|
|
204
|
-
export {};
|
package/dist/lib/daemon.js
CHANGED
|
@@ -21,6 +21,7 @@ import { BrowserService } from './browser/service.js';
|
|
|
21
21
|
import { BrowserIPCServer } from './browser/ipc.js';
|
|
22
22
|
import { readAndResolveBundleEnv } from './secrets/bundles.js';
|
|
23
23
|
import { redactSecrets } from './redact.js';
|
|
24
|
+
import { getAgentsBinPath, getCliLaunch, BUN_VIRTUAL_ROOT } from './cli-entry.js';
|
|
24
25
|
const PID_FILE = 'daemon.pid';
|
|
25
26
|
const LOCK_FILE = 'daemon.lock';
|
|
26
27
|
const LOG_FILE = 'logs.jsonl';
|
|
@@ -781,47 +782,11 @@ Environment=PATH=${daemonNodeBinDir()}:/usr/local/bin:/usr/bin:/bin
|
|
|
781
782
|
[Install]
|
|
782
783
|
WantedBy=default.target`;
|
|
783
784
|
}
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
throw new Error(`Cannot resolve agents CLI: Bun standalone executable not found at ${execPath || '(empty path)'}`);
|
|
790
|
-
}
|
|
791
|
-
return execPath;
|
|
792
|
-
}
|
|
793
|
-
export function getAgentsBinPath(argv1 = process.argv[1], execPath = process.execPath) {
|
|
794
|
-
// Prefer the binary actively executing this code. `which agents` returns
|
|
795
|
-
// whatever happens to be first on PATH, which means a side-by-side dev
|
|
796
|
-
// build at ~/.local/bin would silently spawn the registry-installed
|
|
797
|
-
// daemon and run stale code. For a JS install, process.argv[1] is the
|
|
798
|
-
// absolute entrypoint the user actually invoked. A Bun standalone instead
|
|
799
|
-
// exposes its embedded /$bunfs/root entry at argv[1] and its physical signed
|
|
800
|
-
// executable at process.execPath; Bun reports both as existing paths.
|
|
801
|
-
const runningEntry = argv1 ? resolveBunStandaloneEntry(argv1, execPath) : undefined;
|
|
802
|
-
if (runningEntry && fs.existsSync(runningEntry)) {
|
|
803
|
-
// The package's browser/computer entrypoints are sibling shims without a
|
|
804
|
-
// `daemon` command. A daemon started as their IPC side effect must launch
|
|
805
|
-
// through the main agents entrypoint instead of replaying the shim path.
|
|
806
|
-
const entryName = path.basename(runningEntry);
|
|
807
|
-
const compiledShim = /^(browser|computer)\.(c|m)?js$/.test(entryName);
|
|
808
|
-
const installedShim = /^(browser|computer)$/.test(entryName);
|
|
809
|
-
if (compiledShim || installedShim) {
|
|
810
|
-
const agentsEntry = path.join(path.dirname(runningEntry), compiledShim ? 'index.js' : 'agents');
|
|
811
|
-
if (!fs.existsSync(agentsEntry)) {
|
|
812
|
-
throw new Error(`Cannot start agents daemon: main CLI entry not found at ${agentsEntry}`);
|
|
813
|
-
}
|
|
814
|
-
return agentsEntry;
|
|
815
|
-
}
|
|
816
|
-
return runningEntry;
|
|
817
|
-
}
|
|
818
|
-
try {
|
|
819
|
-
return execFileSync('which', ['agents'], { encoding: 'utf-8' }).trim();
|
|
820
|
-
}
|
|
821
|
-
catch {
|
|
822
|
-
return 'agents';
|
|
823
|
-
}
|
|
824
|
-
}
|
|
785
|
+
// Binary-resolution helpers (getAgentsBinPath / isNodeScriptEntry / getCliLaunch)
|
|
786
|
+
// live in ./cli-entry.js — a leaf module the secrets broker also imports without
|
|
787
|
+
// forming a cycle. Re-exported so existing `from './daemon.js'` importers of
|
|
788
|
+
// getAgentsBinPath keep resolving.
|
|
789
|
+
export { getAgentsBinPath };
|
|
825
790
|
/**
|
|
826
791
|
* Ask the service manager for the daemon's live PID. Used as a fallback when
|
|
827
792
|
* the daemon hasn't yet written its pid file but launchd/systemd already report
|
|
@@ -980,49 +945,7 @@ export function getDaemonLaunch(agentsBin = getAgentsBinPath()) {
|
|
|
980
945
|
const { warnings } = validateDaemonBinary(agentsBin);
|
|
981
946
|
for (const w of warnings)
|
|
982
947
|
process.stderr.write(`[agents] ${w}\n`);
|
|
983
|
-
|
|
984
|
-
return { command: process.execPath, args: [agentsBin, 'daemon', '_run'] };
|
|
985
|
-
}
|
|
986
|
-
return { command: agentsBin, args: ['daemon', '_run'] };
|
|
987
|
-
}
|
|
988
|
-
/**
|
|
989
|
-
* A daemon entry must be launched through the Node runtime when it is a Node
|
|
990
|
-
* script — a `.js`/`.cjs`/`.mjs` file, OR a symlink/extension-less shim whose
|
|
991
|
-
* shebang names `node`. Package installs link `bin/agents` to a `dist/index.js`
|
|
992
|
-
* (a symlink) or drop an extension-less `#!/usr/bin/env node` shim, so an
|
|
993
|
-
* extension check alone misses them and they get run directly. Executing such an
|
|
994
|
-
* entry then relies on the shebang resolving `node` off the daemon's PATH — and
|
|
995
|
-
* when that PATH points at a pruned nvm version (or an ancient system node), the
|
|
996
|
-
* daemon crash-loops at import (`node:util` has no `styleText` on Node 18). A
|
|
997
|
-
* real compiled binary (Mach-O/ELF/PE) has no `#!node` shebang, so it takes the
|
|
998
|
-
* direct branch and owns its own runtime resolution.
|
|
999
|
-
*/
|
|
1000
|
-
function isNodeScriptEntry(agentsBin) {
|
|
1001
|
-
let resolved = agentsBin;
|
|
1002
|
-
try {
|
|
1003
|
-
resolved = fs.realpathSync(agentsBin);
|
|
1004
|
-
}
|
|
1005
|
-
catch {
|
|
1006
|
-
// Unresolvable (e.g. a template path that does not exist on this box): fall
|
|
1007
|
-
// back to the extension check on the path as given.
|
|
1008
|
-
}
|
|
1009
|
-
if (/\.(c|m)?js$/.test(resolved))
|
|
1010
|
-
return true;
|
|
1011
|
-
try {
|
|
1012
|
-
const fd = fs.openSync(resolved, 'r');
|
|
1013
|
-
try {
|
|
1014
|
-
const buf = Buffer.alloc(128);
|
|
1015
|
-
const n = fs.readSync(fd, buf, 0, 128, 0);
|
|
1016
|
-
const firstLine = buf.toString('utf-8', 0, n).split('\n', 1)[0];
|
|
1017
|
-
return firstLine.startsWith('#!') && /\bnode\b/.test(firstLine);
|
|
1018
|
-
}
|
|
1019
|
-
finally {
|
|
1020
|
-
fs.closeSync(fd);
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
catch {
|
|
1024
|
-
return false;
|
|
1025
|
-
}
|
|
948
|
+
return getCliLaunch(['daemon', '_run'], agentsBin);
|
|
1026
949
|
}
|
|
1027
950
|
/**
|
|
1028
951
|
* The directory of the Node runtime that generated this service manifest, kept
|
|
@@ -1050,11 +973,7 @@ function daemonNodeBinDir() {
|
|
|
1050
973
|
* getAgentsBinPath() resolves that virtual entry to the physical process.execPath.
|
|
1051
974
|
*/
|
|
1052
975
|
export function getAgentsInvocation(subArgs, agentsBin = getAgentsBinPath()) {
|
|
1053
|
-
|
|
1054
|
-
if (/\.(c|m)?js$/.test(resolvedBin)) {
|
|
1055
|
-
return { command: process.execPath, args: [resolvedBin, ...subArgs] };
|
|
1056
|
-
}
|
|
1057
|
-
return { command: resolvedBin, args: subArgs };
|
|
976
|
+
return getCliLaunch(subArgs, agentsBin);
|
|
1058
977
|
}
|
|
1059
978
|
export function validateDaemonBinary(binPath) {
|
|
1060
979
|
const warnings = [];
|
|
@@ -24,6 +24,17 @@ export interface FleetRunResult {
|
|
|
24
24
|
/** Truncated combined stderr/stdout for failures. */
|
|
25
25
|
detail?: string;
|
|
26
26
|
}
|
|
27
|
+
export interface FanOutDeviceTarget {
|
|
28
|
+
name: string;
|
|
29
|
+
skip?: FleetSkipReason | string;
|
|
30
|
+
}
|
|
31
|
+
export interface FanOutDeviceResult<T> {
|
|
32
|
+
name: string;
|
|
33
|
+
status: 'ok' | 'failed' | 'skipped';
|
|
34
|
+
value?: T;
|
|
35
|
+
error?: string;
|
|
36
|
+
reason?: FleetSkipReason | string;
|
|
37
|
+
}
|
|
27
38
|
/**
|
|
28
39
|
* Classify each registered device for a fleet operation.
|
|
29
40
|
*
|
|
@@ -34,6 +45,16 @@ export interface FleetRunResult {
|
|
|
34
45
|
* it has a registry address — same path as any other box).
|
|
35
46
|
*/
|
|
36
47
|
export declare function planFleetTargets(reg: DeviceRegistry): FleetTarget[];
|
|
48
|
+
/**
|
|
49
|
+
* Remote fan-out targets for the fleet health/drift gates (`fleet status`,
|
|
50
|
+
* `check --devices`): every planned device except this machine and control-only
|
|
51
|
+
* cockpits. A control device never runs agents (mirrors doctor's fan-out, which
|
|
52
|
+
* drops it via `isControlDevice`), so counting it as unreachable/drift would make
|
|
53
|
+
* the CI gate fail on every run for a fleet that merely has a cockpit registered.
|
|
54
|
+
* Offline / no-address devices are kept — those are genuine faults a gate should
|
|
55
|
+
* surface — so their `skip` reason still flows through as an `unreachable` row.
|
|
56
|
+
*/
|
|
57
|
+
export declare function remoteFleetTargets(planned: FleetTarget[], self: string): FleetTarget[];
|
|
37
58
|
/** Human label for a skip reason. */
|
|
38
59
|
export declare function skipLabel(reason: FleetSkipReason): string;
|
|
39
60
|
/**
|
|
@@ -61,3 +82,5 @@ export declare function upgradeCommand(version?: string): string[];
|
|
|
61
82
|
* the runner are recorded as `failed` so one bad device never aborts the rest.
|
|
62
83
|
*/
|
|
63
84
|
export declare function runFleet(targets: FleetTarget[], cmd: string[], runner?: typeof runOnDevice): FleetRunResult[];
|
|
85
|
+
/** Run one async probe per device in parallel, preserving input order. */
|
|
86
|
+
export declare function fanOutDevices<T, Target extends FanOutDeviceTarget = FanOutDeviceTarget>(targets: Target[], probe: (target: Target) => Promise<T>): Promise<FanOutDeviceResult<T>[]>;
|