@phnx-labs/agents-cli 1.20.92 → 1.21.0
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 +385 -0
- package/README.md +3 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/beta.js +3 -5
- package/dist/commands/doctor.js +23 -0
- package/dist/commands/events.js +91 -1
- package/dist/commands/exec.js +13 -0
- package/dist/commands/mailboxes.js +39 -1
- package/dist/commands/message.js +12 -1
- package/dist/commands/packages.js +14 -5
- package/dist/commands/projects.d.ts +38 -0
- package/dist/commands/projects.js +355 -48
- package/dist/commands/routines.js +27 -1
- package/dist/commands/rules.js +6 -0
- package/dist/commands/secrets.d.ts +17 -0
- package/dist/commands/secrets.js +198 -7
- package/dist/commands/send.d.ts +14 -12
- package/dist/commands/send.js +105 -35
- package/dist/commands/sessions.d.ts +17 -0
- package/dist/commands/sessions.js +16 -8
- package/dist/commands/setup-browser.js +5 -1
- package/dist/commands/setup-fleet.js +5 -0
- package/dist/commands/setup-preferences.d.ts +53 -0
- package/dist/commands/setup-preferences.js +142 -0
- package/dist/commands/setup.js +8 -9
- package/dist/commands/ssh.js +220 -10
- package/dist/commands/sync.d.ts +2 -2
- package/dist/commands/sync.js +13 -12
- package/dist/commands/view.js +4 -0
- package/dist/commands/watchdog.js +26 -3
- package/dist/index.js +38 -6
- package/dist/lib/activity.d.ts +15 -0
- package/dist/lib/activity.js +26 -2
- package/dist/lib/auto-dispatch.d.ts +6 -1
- package/dist/lib/auto-dispatch.js +7 -2
- package/dist/lib/beta.d.ts +1 -1
- package/dist/lib/beta.js +1 -1
- package/dist/lib/channels/send.d.ts +83 -0
- package/dist/lib/channels/send.js +112 -0
- package/dist/lib/daemon.d.ts +13 -0
- package/dist/lib/daemon.js +80 -60
- package/dist/lib/device-config.d.ts +82 -0
- package/dist/lib/device-config.js +296 -0
- package/dist/lib/event-provenance.d.ts +19 -0
- package/dist/lib/event-provenance.js +48 -0
- package/dist/lib/events-ingest.d.ts +46 -0
- package/dist/lib/events-ingest.js +182 -0
- package/dist/lib/events.d.ts +17 -5
- package/dist/lib/events.js +59 -56
- package/dist/lib/feed.d.ts +1 -1
- package/dist/lib/feed.js +2 -0
- package/dist/lib/git.d.ts +13 -2
- package/dist/lib/git.js +38 -6
- package/dist/lib/linear-project-counts.d.ts +129 -0
- package/dist/lib/linear-project-counts.js +206 -0
- package/dist/lib/linear-projects.d.ts +62 -0
- package/dist/lib/linear-projects.js +144 -0
- package/dist/lib/mailbox-gc.js +30 -7
- package/dist/lib/mailbox.d.ts +14 -1
- package/dist/lib/mailbox.js +35 -3
- 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 +5 -21
- package/dist/lib/menubar/install-menubar.js +35 -44
- package/dist/lib/menubar/notify-desktop.d.ts +17 -2
- package/dist/lib/menubar/notify-desktop.js +8 -2
- package/dist/lib/migrate.js +1 -1
- package/dist/lib/models.js +21 -11
- package/dist/lib/overdue.d.ts +1 -1
- package/dist/lib/overdue.js +54 -19
- package/dist/lib/project-import.d.ts +96 -0
- package/dist/lib/project-import.js +171 -0
- package/dist/lib/project-probe.d.ts +75 -0
- package/dist/lib/project-probe.js +160 -0
- package/dist/lib/project-resources.d.ts +8 -0
- package/dist/lib/project-resources.js +31 -3
- package/dist/lib/project-status.d.ts +32 -1
- package/dist/lib/project-status.js +82 -1
- package/dist/lib/projects.d.ts +6 -0
- package/dist/lib/projects.js +12 -0
- package/dist/lib/remote-agents-json.d.ts +14 -1
- package/dist/lib/remote-agents-json.js +21 -3
- package/dist/lib/routine-notify.d.ts +11 -0
- package/dist/lib/routine-notify.js +22 -0
- package/dist/lib/routines.d.ts +53 -0
- package/dist/lib/routines.js +105 -4
- package/dist/lib/rules/run-sync.d.ts +18 -0
- package/dist/lib/rules/run-sync.js +92 -0
- package/dist/lib/run-notify.js +3 -0
- package/dist/lib/scheduler.js +12 -2
- 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/audit.d.ts +1 -1
- package/dist/lib/secrets/audit.js +53 -10
- package/dist/lib/secrets/index.d.ts +4 -5
- package/dist/lib/secrets/index.js +4 -5
- package/dist/lib/secrets/list-filter.d.ts +20 -5
- package/dist/lib/secrets/list-filter.js +22 -6
- package/dist/lib/secrets/usage-db.d.ts +106 -0
- package/dist/lib/secrets/usage-db.js +236 -0
- package/dist/lib/session/active.d.ts +16 -2
- package/dist/lib/session/active.js +8 -4
- package/dist/lib/session/bundle.js +5 -1
- package/dist/lib/session/remote-active.d.ts +5 -1
- package/dist/lib/session/remote-active.js +4 -1
- package/dist/lib/session/remote-list.d.ts +1 -15
- package/dist/lib/session/remote-list.js +22 -111
- package/dist/lib/session/sync/config.d.ts +13 -5
- package/dist/lib/session/sync/config.js +21 -13
- package/dist/lib/sqlite.js +28 -1
- package/dist/lib/startup/command-registry.d.ts +0 -2
- package/dist/lib/startup/command-registry.js +1 -5
- package/dist/lib/state.d.ts +19 -2
- package/dist/lib/state.js +99 -10
- package/dist/lib/sync-umbrella.d.ts +11 -23
- package/dist/lib/sync-umbrella.js +14 -45
- package/dist/lib/teams/agents.d.ts +35 -3
- package/dist/lib/teams/agents.js +52 -8
- package/dist/lib/teams/scheduler.d.ts +29 -2
- package/dist/lib/teams/scheduler.js +60 -16
- package/dist/lib/types.d.ts +22 -5
- package/dist/lib/versions.d.ts +6 -0
- package/dist/lib/versions.js +6 -4
- package/package.json +1 -2
- package/dist/commands/drive.d.ts +0 -10
- package/dist/commands/drive.js +0 -183
- package/dist/commands/hq.d.ts +0 -2
- package/dist/commands/hq.js +0 -58
- package/dist/commands/sessions-sync.d.ts +0 -17
- package/dist/commands/sessions-sync.js +0 -135
- package/dist/commands/sync-provision.d.ts +0 -23
- package/dist/commands/sync-provision.js +0 -107
- package/dist/lib/drive-sync.d.ts +0 -45
- package/dist/lib/drive-sync.js +0 -238
- package/dist/lib/hq/floor.d.ts +0 -87
- package/dist/lib/hq/floor.js +0 -243
- package/dist/lib/session/sync/crdt.d.ts +0 -44
- package/dist/lib/session/sync/crdt.js +0 -119
- package/dist/lib/session/sync/manifest.d.ts +0 -62
- package/dist/lib/session/sync/manifest.js +0 -100
- package/dist/lib/session/sync/provision.d.ts +0 -49
- package/dist/lib/session/sync/provision.js +0 -91
- package/dist/lib/session/sync/r2.d.ts +0 -32
- package/dist/lib/session/sync/r2.js +0 -121
- package/dist/lib/session/sync/sync.d.ts +0 -106
- package/dist/lib/session/sync/sync.js +0 -374
|
@@ -46,7 +46,6 @@ import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
|
46
46
|
import { sessionPicker, buildPreview, formatTodoCompact, githubRepoUrlFromCwd, } from './sessions-picker.js';
|
|
47
47
|
import { setHelpSections } from '../lib/help.js';
|
|
48
48
|
import { registerSessionsTailCommand } from './sessions-tail.js';
|
|
49
|
-
import { registerSessionsSyncCommand } from './sessions-sync.js';
|
|
50
49
|
import { registerSessionsResumeCommand } from './sessions-resume.js';
|
|
51
50
|
import { registerSessionsFavoriteCommand } from './sessions-favorite.js';
|
|
52
51
|
import { isFavorite, listFavorites } from '../lib/session/favorites.js';
|
|
@@ -1044,7 +1043,11 @@ export async function gatherActiveSessions(opts = {}) {
|
|
|
1044
1043
|
const self = machineId();
|
|
1045
1044
|
// An explicit --host/--device list scopes the view: seed local sessions only
|
|
1046
1045
|
// when no hosts are named, or when this machine is one of the named targets.
|
|
1047
|
-
|
|
1046
|
+
// `localOnly: opts.local` (RUSH-2118) keeps a `--local` query from dialing a
|
|
1047
|
+
// remote-host teammate over ssh even for this machine's OWN local gather —
|
|
1048
|
+
// "this machine only" must mean zero ssh, not just "skip the cross-machine
|
|
1049
|
+
// device fan-out below".
|
|
1050
|
+
const local = shouldIncludeLocal(opts.hosts, self) ? await getActiveSessions({ localOnly: opts.local }) : [];
|
|
1048
1051
|
for (const s of local)
|
|
1049
1052
|
if (!s.machine)
|
|
1050
1053
|
s.machine = self;
|
|
@@ -1218,7 +1221,7 @@ function canonicalSessionsCommand(query, options) {
|
|
|
1218
1221
|
}
|
|
1219
1222
|
/** Resolve a session by id/query globally and print its compact preview (no pager).
|
|
1220
1223
|
* Backs `--preview` — the fast path for the "peek before resume" hot loop. */
|
|
1221
|
-
async function renderSessionPreview(query, scope) {
|
|
1224
|
+
export async function renderSessionPreview(query, scope) {
|
|
1222
1225
|
const discovered = await discoverSessions({ all: true, cwd: process.cwd(), limit: 5000 });
|
|
1223
1226
|
const pool = applyScopeFilters(discovered, scope);
|
|
1224
1227
|
const { matches, completeId } = resolveSessionQuery(pool, query);
|
|
@@ -1234,9 +1237,11 @@ async function renderSessionPreview(query, scope) {
|
|
|
1234
1237
|
}
|
|
1235
1238
|
// Lead with the live status when the session is still running, so the preview
|
|
1236
1239
|
// says working / waiting / idle up front — not just the historical transcript.
|
|
1240
|
+
// `--local --preview` is freely combinable with `--local` (RUSH-2118): thread
|
|
1241
|
+
// it through so this probe never dials a remote-host teammate either.
|
|
1237
1242
|
let live;
|
|
1238
1243
|
try {
|
|
1239
|
-
live = indexActiveBySessionId(await getActiveSessions()).get(session.id);
|
|
1244
|
+
live = indexActiveBySessionId(await getActiveSessions({ localOnly: scope.local === true })).get(session.id);
|
|
1240
1245
|
}
|
|
1241
1246
|
catch { /* plain preview on any probe failure */ }
|
|
1242
1247
|
const headline = formatLiveStatusHeadline(live, isFavorite(session.id));
|
|
@@ -1390,7 +1395,7 @@ limitSource) {
|
|
|
1390
1395
|
console.error(chalk.red('--preview requires a session id or query.'));
|
|
1391
1396
|
process.exit(1);
|
|
1392
1397
|
}
|
|
1393
|
-
await renderSessionPreview(query, { agent: options.agent, project: options.project });
|
|
1398
|
+
await renderSessionPreview(query, { agent: options.agent, project: options.project, local: options.local });
|
|
1394
1399
|
return;
|
|
1395
1400
|
}
|
|
1396
1401
|
if (options.active) {
|
|
@@ -1907,11 +1912,15 @@ function treeSessionRow(session, live) {
|
|
|
1907
1912
|
* not a hot loop, so the `ps`/`lsof` cost is acceptable; `--no-live` is the
|
|
1908
1913
|
* escape hatch. Never throws — a probe failure just yields a plain listing.
|
|
1909
1914
|
*/
|
|
1910
|
-
async function maybeLiveIndex(options) {
|
|
1915
|
+
export async function maybeLiveIndex(options) {
|
|
1911
1916
|
if (options.live === false || options.json)
|
|
1912
1917
|
return undefined;
|
|
1913
1918
|
try {
|
|
1914
|
-
|
|
1919
|
+
// `--local` promises "this machine only" for the default listing too (see
|
|
1920
|
+
// the `--local` help text), so it must gate the live-enrichment probe the
|
|
1921
|
+
// same way `--active --local` does — never dial a remote-host teammate
|
|
1922
|
+
// over ssh here either (RUSH-2118).
|
|
1923
|
+
return indexActiveBySessionId(await getActiveSessions({ localOnly: options.local === true }));
|
|
1915
1924
|
}
|
|
1916
1925
|
catch {
|
|
1917
1926
|
return undefined;
|
|
@@ -3387,7 +3396,6 @@ export function registerSessionsCommands(program) {
|
|
|
3387
3396
|
await sessionsAction(query, options, command.getOptionValueSource('limit'));
|
|
3388
3397
|
});
|
|
3389
3398
|
registerSessionsTailCommand(sessionsCmd);
|
|
3390
|
-
registerSessionsSyncCommand(sessionsCmd);
|
|
3391
3399
|
registerSessionsResumeCommand(sessionsCmd);
|
|
3392
3400
|
registerSessionsFavoriteCommand(sessionsCmd);
|
|
3393
3401
|
registerGoCommand(sessionsCmd);
|
|
@@ -13,6 +13,7 @@ import { findFirstInstalledBrowser, listInstalledBrowsers } from '../lib/browser
|
|
|
13
13
|
import { DEFAULT_BROWSER_PROFILE_NAME, createProfile, findFreeProfilePort, getConfiguredDefaultProfileName, getProfile, } from '../lib/browser/profiles.js';
|
|
14
14
|
import { DEFAULT_VIEWPORT } from '../lib/browser/devices.js';
|
|
15
15
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
16
|
+
import { defaultBrowserChoice } from './setup-preferences.js';
|
|
16
17
|
const INSTALL_HINT = 'Install one of: Google Chrome, Brave, Microsoft Edge, Chromium, or Comet, then re-run `agents setup browser`.\n' +
|
|
17
18
|
'(Safari and Firefox are not supported — agents browser drives over the Chrome DevTools Protocol.)';
|
|
18
19
|
/**
|
|
@@ -58,11 +59,14 @@ export async function runBrowserWizard() {
|
|
|
58
59
|
const { deleteProfile } = await import('../lib/browser/profiles.js');
|
|
59
60
|
await deleteProfile(existing.name);
|
|
60
61
|
}
|
|
61
|
-
// Pick which installed browser to pin (auto-select if only one).
|
|
62
|
+
// Pick which installed browser to pin (auto-select if only one). The picker
|
|
63
|
+
// highlights the same browser auto-detect would win — choosing is an
|
|
64
|
+
// override of the default, never a guess.
|
|
62
65
|
let chosen = installed[0];
|
|
63
66
|
if (installed.length > 1) {
|
|
64
67
|
const value = await select({
|
|
65
68
|
message: 'Which browser should the default profile use?',
|
|
69
|
+
default: defaultBrowserChoice(installed) ?? undefined,
|
|
66
70
|
choices: installed.map((b) => ({ name: `${b.browserType} ${chalk.dim(b.binary)}`, value: b.browserType })),
|
|
67
71
|
});
|
|
68
72
|
chosen = installed.find((b) => b.browserType === value) ?? installed[0];
|
|
@@ -12,6 +12,7 @@ import { loadDevices } from '../lib/devices/registry.js';
|
|
|
12
12
|
import { runDeviceSync } from '../lib/devices/sync.js';
|
|
13
13
|
import { tailscaleStatusJson } from '../lib/devices/tailscale.js';
|
|
14
14
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
15
|
+
import { maybePickInteractiveHost } from './setup-preferences.js';
|
|
15
16
|
function parseAuth(raw) {
|
|
16
17
|
const value = (raw ?? 'key').toLowerCase();
|
|
17
18
|
if (value === 'key' || value === 'password')
|
|
@@ -171,6 +172,10 @@ export async function runFleetSetupWizard(opts = {}) {
|
|
|
171
172
|
if (choices.fleetUpdate)
|
|
172
173
|
runAgentsSubcommand(['fleet', 'update']);
|
|
173
174
|
printOnboardingSummary(names, choices.auth);
|
|
175
|
+
// With a fleet registered, the next wrong-machine trap is artifacts opening on
|
|
176
|
+
// some other box — offer to pin the interactive host (TTY-only, skippable,
|
|
177
|
+
// silent non-TTY and when already set).
|
|
178
|
+
await maybePickInteractiveHost();
|
|
174
179
|
return true;
|
|
175
180
|
}
|
|
176
181
|
/** Register `agents setup fleet` under the parent `setup` command. */
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preferences onboarding — the guided path over the same device-config keys the
|
|
3
|
+
* `agents devices …` commands write (lib/device-config.ts). Two questions, each
|
|
4
|
+
* TTY-only, skippable, and absent non-interactively:
|
|
5
|
+
*
|
|
6
|
+
* 1. "Which machine do you sit at?" → `interactive.host` (central config:)
|
|
7
|
+
* 2. "Which browser should agents drive on THIS machine?"
|
|
8
|
+
* → `browser.profile` (device-local default)
|
|
9
|
+
*
|
|
10
|
+
* Shared by the bare `agents setup` flow (a short step after the capability
|
|
11
|
+
* hub) and by `agents setup fleet` (interactive host after a successful sync).
|
|
12
|
+
* Unset always keeps today's behavior — skipping is a real choice, not a
|
|
13
|
+
* partial state.
|
|
14
|
+
*/
|
|
15
|
+
import { type DeviceRegistry } from '../lib/devices/registry.js';
|
|
16
|
+
/** Registered macOS device names, sorted — the interactive-host candidates. */
|
|
17
|
+
export declare function macDeviceNames(reg: DeviceRegistry): string[];
|
|
18
|
+
/**
|
|
19
|
+
* The interactive-host picker's highlighted default: this machine when it is a
|
|
20
|
+
* candidate (the common case — you run setup on the box you sit at), else the
|
|
21
|
+
* first candidate. null when there are no candidates.
|
|
22
|
+
*/
|
|
23
|
+
export declare function defaultInteractiveHostChoice(candidates: string[], self?: string): string | null;
|
|
24
|
+
/**
|
|
25
|
+
* The browser picker's highlighted default — the same browser auto-detect
|
|
26
|
+
* would win, since `listInstalledBrowsers` returns platform priority order
|
|
27
|
+
* (macOS: chrome first). null when nothing is installed.
|
|
28
|
+
*/
|
|
29
|
+
export declare function defaultBrowserChoice<T extends {
|
|
30
|
+
browserType: string;
|
|
31
|
+
}>(installed: T[]): T['browserType'] | null;
|
|
32
|
+
/**
|
|
33
|
+
* Offer to set the interactive host when none is configured and the registry
|
|
34
|
+
* has more than one macOS device. Returns true when a host was set. Silent
|
|
35
|
+
* no-op non-TTY, when already set, or with fewer than two candidates (the
|
|
36
|
+
* answer is obvious or absent).
|
|
37
|
+
*/
|
|
38
|
+
export declare function maybePickInteractiveHost(): Promise<boolean>;
|
|
39
|
+
/**
|
|
40
|
+
* Offer to pin this machine's default browser profile to a chosen browser.
|
|
41
|
+
* Only asked when there is nothing to preserve: no configured device default
|
|
42
|
+
* AND no existing `default` profile (an existing profile is the user's earlier
|
|
43
|
+
* choice — never re-pinned behind their back). Returns true when a profile was
|
|
44
|
+
* created and set as the device default. Silent no-op non-TTY.
|
|
45
|
+
*/
|
|
46
|
+
export declare function maybePickBrowserProfile(): Promise<boolean>;
|
|
47
|
+
/**
|
|
48
|
+
* The bare-`agents setup` preferences step: interactive host, then browser.
|
|
49
|
+
* Runs AFTER the capability hub so it never delays the bootstrap. Never
|
|
50
|
+
* throws — a prompt cancel or a failed pick ends the step quietly and lets
|
|
51
|
+
* setup complete (the same semantics as the capability hub).
|
|
52
|
+
*/
|
|
53
|
+
export declare function runPreferencesStep(): Promise<void>;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preferences onboarding — the guided path over the same device-config keys the
|
|
3
|
+
* `agents devices …` commands write (lib/device-config.ts). Two questions, each
|
|
4
|
+
* TTY-only, skippable, and absent non-interactively:
|
|
5
|
+
*
|
|
6
|
+
* 1. "Which machine do you sit at?" → `interactive.host` (central config:)
|
|
7
|
+
* 2. "Which browser should agents drive on THIS machine?"
|
|
8
|
+
* → `browser.profile` (device-local default)
|
|
9
|
+
*
|
|
10
|
+
* Shared by the bare `agents setup` flow (a short step after the capability
|
|
11
|
+
* hub) and by `agents setup fleet` (interactive host after a successful sync).
|
|
12
|
+
* Unset always keeps today's behavior — skipping is a real choice, not a
|
|
13
|
+
* partial state.
|
|
14
|
+
*/
|
|
15
|
+
import chalk from 'chalk';
|
|
16
|
+
import { listInstalledBrowsers } from '../lib/browser/chrome.js';
|
|
17
|
+
import { DEFAULT_BROWSER_PROFILE_NAME, createProfile, findFreeProfilePort, getConfiguredDefaultProfileName, getProfile, } from '../lib/browser/profiles.js';
|
|
18
|
+
import { DEFAULT_VIEWPORT } from '../lib/browser/devices.js';
|
|
19
|
+
import { getConfigValue, setConfigValue } from '../lib/device-config.js';
|
|
20
|
+
import { loadDevices } from '../lib/devices/registry.js';
|
|
21
|
+
import { machineId } from '../lib/machine-id.js';
|
|
22
|
+
import { isInteractiveTerminal } from './utils.js';
|
|
23
|
+
const SKIP = '__skip__';
|
|
24
|
+
/** Registered macOS device names, sorted — the interactive-host candidates. */
|
|
25
|
+
export function macDeviceNames(reg) {
|
|
26
|
+
return Object.values(reg)
|
|
27
|
+
.filter((d) => d.platform === 'macos')
|
|
28
|
+
.map((d) => d.name)
|
|
29
|
+
.sort();
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The interactive-host picker's highlighted default: this machine when it is a
|
|
33
|
+
* candidate (the common case — you run setup on the box you sit at), else the
|
|
34
|
+
* first candidate. null when there are no candidates.
|
|
35
|
+
*/
|
|
36
|
+
export function defaultInteractiveHostChoice(candidates, self = machineId()) {
|
|
37
|
+
if (candidates.length === 0)
|
|
38
|
+
return null;
|
|
39
|
+
return candidates.includes(self) ? self : candidates[0];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The browser picker's highlighted default — the same browser auto-detect
|
|
43
|
+
* would win, since `listInstalledBrowsers` returns platform priority order
|
|
44
|
+
* (macOS: chrome first). null when nothing is installed.
|
|
45
|
+
*/
|
|
46
|
+
export function defaultBrowserChoice(installed) {
|
|
47
|
+
return installed.length > 0 ? installed[0].browserType : null;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Offer to set the interactive host when none is configured and the registry
|
|
51
|
+
* has more than one macOS device. Returns true when a host was set. Silent
|
|
52
|
+
* no-op non-TTY, when already set, or with fewer than two candidates (the
|
|
53
|
+
* answer is obvious or absent).
|
|
54
|
+
*/
|
|
55
|
+
export async function maybePickInteractiveHost() {
|
|
56
|
+
if (!isInteractiveTerminal())
|
|
57
|
+
return false;
|
|
58
|
+
if (getConfigValue('interactive.host').value !== undefined)
|
|
59
|
+
return false;
|
|
60
|
+
const macs = macDeviceNames(await loadDevices());
|
|
61
|
+
if (macs.length < 2)
|
|
62
|
+
return false;
|
|
63
|
+
const { select } = await import('@inquirer/prompts');
|
|
64
|
+
const self = machineId();
|
|
65
|
+
const picked = await select({
|
|
66
|
+
message: 'Which machine do you sit at? (agents open browser windows and artifacts there)',
|
|
67
|
+
default: defaultInteractiveHostChoice(macs, self) ?? SKIP,
|
|
68
|
+
choices: [
|
|
69
|
+
...macs.map((n) => ({ name: n === self ? `${n} ${chalk.dim('(this machine)')}` : n, value: n })),
|
|
70
|
+
{ name: `Skip ${chalk.dim('— decide later: agents devices set-interactive <name>')}`, value: SKIP },
|
|
71
|
+
],
|
|
72
|
+
});
|
|
73
|
+
if (picked === SKIP)
|
|
74
|
+
return false;
|
|
75
|
+
setConfigValue('interactive.host', picked);
|
|
76
|
+
console.log(chalk.green(`Interactive host: '${picked}'`) + chalk.dim(' — marked ★ interactive in `agents devices list`.'));
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Offer to pin this machine's default browser profile to a chosen browser.
|
|
81
|
+
* Only asked when there is nothing to preserve: no configured device default
|
|
82
|
+
* AND no existing `default` profile (an existing profile is the user's earlier
|
|
83
|
+
* choice — never re-pinned behind their back). Returns true when a profile was
|
|
84
|
+
* created and set as the device default. Silent no-op non-TTY.
|
|
85
|
+
*/
|
|
86
|
+
export async function maybePickBrowserProfile() {
|
|
87
|
+
if (!isInteractiveTerminal())
|
|
88
|
+
return false;
|
|
89
|
+
if (getConfiguredDefaultProfileName())
|
|
90
|
+
return false;
|
|
91
|
+
if (await getProfile(DEFAULT_BROWSER_PROFILE_NAME))
|
|
92
|
+
return false;
|
|
93
|
+
const installed = listInstalledBrowsers();
|
|
94
|
+
if (installed.length === 0)
|
|
95
|
+
return false;
|
|
96
|
+
const { select } = await import('@inquirer/prompts');
|
|
97
|
+
const picked = await select({
|
|
98
|
+
message: 'Which browser should agents drive on THIS machine?',
|
|
99
|
+
default: defaultBrowserChoice(installed) ?? SKIP,
|
|
100
|
+
choices: [
|
|
101
|
+
...installed.map((b) => ({ name: `${b.browserType} ${chalk.dim(b.binary)}`, value: b.browserType })),
|
|
102
|
+
{ name: `Skip ${chalk.dim('— auto-detect on first use (priority order)')}`, value: SKIP },
|
|
103
|
+
],
|
|
104
|
+
});
|
|
105
|
+
if (picked === SKIP)
|
|
106
|
+
return false;
|
|
107
|
+
const chosen = installed.find((b) => b.browserType === picked) ?? installed[0];
|
|
108
|
+
const freePort = await findFreeProfilePort();
|
|
109
|
+
const profile = {
|
|
110
|
+
name: DEFAULT_BROWSER_PROFILE_NAME,
|
|
111
|
+
description: `${chosen.browserType} profile (chosen during setup)`,
|
|
112
|
+
browser: chosen.browserType,
|
|
113
|
+
binary: chosen.binary,
|
|
114
|
+
endpoints: [`cdp://127.0.0.1:${freePort}`],
|
|
115
|
+
viewport: { width: DEFAULT_VIEWPORT.width, height: DEFAULT_VIEWPORT.height },
|
|
116
|
+
};
|
|
117
|
+
await createProfile(profile);
|
|
118
|
+
// The same key `agents browser profiles set-default` writes (device-local).
|
|
119
|
+
setConfigValue('browser.profile', profile.name);
|
|
120
|
+
console.log(chalk.green(`Browser: '${chosen.browserType}'`) +
|
|
121
|
+
chalk.dim(` — profile "${profile.name}" is this machine's default (agents browser profiles set-default to change).`));
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The bare-`agents setup` preferences step: interactive host, then browser.
|
|
126
|
+
* Runs AFTER the capability hub so it never delays the bootstrap. Never
|
|
127
|
+
* throws — a prompt cancel or a failed pick ends the step quietly and lets
|
|
128
|
+
* setup complete (the same semantics as the capability hub).
|
|
129
|
+
*/
|
|
130
|
+
export async function runPreferencesStep() {
|
|
131
|
+
if (!isInteractiveTerminal())
|
|
132
|
+
return;
|
|
133
|
+
try {
|
|
134
|
+
const pickedHost = await maybePickInteractiveHost();
|
|
135
|
+
const pickedBrowser = await maybePickBrowserProfile();
|
|
136
|
+
if (pickedHost || pickedBrowser)
|
|
137
|
+
console.log();
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// Cancel (ctrl-c) or a picker failure — the step is optional; end it.
|
|
141
|
+
}
|
|
142
|
+
}
|
package/dist/commands/setup.js
CHANGED
|
@@ -24,6 +24,7 @@ import { registerSetupShareCommand, runShareWizard } from './setup-share.js';
|
|
|
24
24
|
import { registerSetupMineCommand } from './setup-mine.js';
|
|
25
25
|
import { registerSetupSecretsCommand } from './setup-secrets.js';
|
|
26
26
|
import { registerSetupFleetCommand } from './setup-fleet.js';
|
|
27
|
+
import { runPreferencesStep } from './setup-preferences.js';
|
|
27
28
|
const HOME = os.homedir();
|
|
28
29
|
/**
|
|
29
30
|
* Import an existing unmanaged agent installation into agents-cli.
|
|
@@ -130,15 +131,6 @@ export async function runSetup(program, options = {}) {
|
|
|
130
131
|
if (dev.ok && dev.synced > 0) {
|
|
131
132
|
console.log(chalk.gray(`Discovered ${dev.synced} device${dev.synced === 1 ? '' : 's'} on your tailnet (agents devices list).`));
|
|
132
133
|
}
|
|
133
|
-
// Offer guided cross-machine session-sync provisioning (interactive, opt-in,
|
|
134
|
-
// and never blocking — any failure/decline falls through to the rest of setup).
|
|
135
|
-
try {
|
|
136
|
-
const { promptAndProvisionSessionSync } = await import('./sync-provision.js');
|
|
137
|
-
await promptAndProvisionSessionSync({ explicit: false });
|
|
138
|
-
}
|
|
139
|
-
catch (err) {
|
|
140
|
-
console.log(chalk.yellow(`Session-sync setup skipped: ${err.message}`));
|
|
141
|
-
}
|
|
142
134
|
// Offer to import existing unmanaged installations
|
|
143
135
|
if (unmanaged.length > 0 && isInteractiveTerminal()) {
|
|
144
136
|
console.log(chalk.bold('\nFound existing installations:\n'));
|
|
@@ -201,6 +193,10 @@ export async function runSetup(program, options = {}) {
|
|
|
201
193
|
// own guided flow. TTY-only and fully opt-in — a non-interactive `agents setup`
|
|
202
194
|
// stops at the system-repo bootstrap above, unchanged.
|
|
203
195
|
await runSetupHub();
|
|
196
|
+
// Preferences step: the two questions that keep agents off the wrong machine —
|
|
197
|
+
// which box you sit at (interactive host) and which browser agents drive here.
|
|
198
|
+
// TTY-only, skippable, and writes the same keys as `agents devices …`.
|
|
199
|
+
await runPreferencesStep();
|
|
204
200
|
console.log(chalk.bold('\nSetup complete. Try:'));
|
|
205
201
|
console.log(chalk.cyan(' agents view ') + chalk.gray(' # see what\'s installed'));
|
|
206
202
|
console.log(chalk.cyan(' agents run <agent> "hello" ') + chalk.gray(' # run an agent'));
|
|
@@ -307,6 +303,9 @@ export function registerSetupCommand(program) {
|
|
|
307
303
|
1. Clones the system repo into ~/.agents/.system/
|
|
308
304
|
2. Imports any unmanaged agent installations it finds
|
|
309
305
|
3. On a TTY, offers to set up optional capabilities (browser/computer/share/secrets/fleet)
|
|
306
|
+
4. On a TTY, asks preferences: which machine you sit at (interactive host)
|
|
307
|
+
and which browser agents drive here — both skippable, both the same
|
|
308
|
+
keys 'agents devices set-interactive' / 'browser profiles set-default' write
|
|
310
309
|
|
|
311
310
|
Capability setup can also be run any time on its own:
|
|
312
311
|
agents setup browser # detect a browser + create the default profile
|
package/dist/commands/ssh.js
CHANGED
|
@@ -47,9 +47,12 @@ import { crabboxList, crabboxFind, crabboxSshArgv } from '../lib/crabbox/cli.js'
|
|
|
47
47
|
import { boxAddress, boxStatus, fmtIdleShort, fmtExpiresShort } from './lease.js';
|
|
48
48
|
import { authCellColor, formatCheckedAge, isDeadVerdict, probeLocalFleetAuth, readAuthHealthCache, summarizeHostAuth, summarizeVerdicts, verdictColor, verdictLabel, writeFleetAuthRows, } from '../lib/auth-health.js';
|
|
49
49
|
import { runFleetLogin } from '../lib/fleet/remote-login.js';
|
|
50
|
+
import { getConfigValue, listConfig, setConfigValue, unsetConfigValue } from '../lib/device-config.js';
|
|
51
|
+
import { setHelpSections } from '../lib/help.js';
|
|
50
52
|
/** One-line summary of a device for `list`. `isSelf` marks the machine this
|
|
51
|
-
* command is running on so it stands out from the rest of the tailnet.
|
|
52
|
-
|
|
53
|
+
* command is running on so it stands out from the rest of the tailnet.
|
|
54
|
+
* `isInteractive` marks the configured interactive host (`devices set-interactive`). */
|
|
55
|
+
function deviceSummary(d, isSelf = false, stats, isInteractive = false) {
|
|
53
56
|
const addr = hostNameFor(d) ?? chalk.gray('no address');
|
|
54
57
|
// Prefer a fresh live verdict (this run's probe, else the written-back
|
|
55
58
|
// reachability) over the stale tailscale.online snapshot (RUSH-1965).
|
|
@@ -63,7 +66,8 @@ function deviceSummary(d, isSelf = false, stats) {
|
|
|
63
66
|
const marker = isSelf ? chalk.cyan('▸ ') : ' ';
|
|
64
67
|
const name = isSelf ? chalk.bold.cyan(d.name.padEnd(16)) : chalk.bold(d.name.padEnd(16));
|
|
65
68
|
const here = isSelf ? chalk.cyan(' ← this machine') : '';
|
|
66
|
-
|
|
69
|
+
const interactive = isInteractive ? chalk.yellow(' ★ interactive') : '';
|
|
70
|
+
return `${marker}${name} ${String(d.platform).padEnd(8)} ${(d.user ? d.user + '@' : '') + addr} ${online}${reach}${here}${interactive}`;
|
|
67
71
|
}
|
|
68
72
|
const HEADROOM_BADGE = {
|
|
69
73
|
idle: chalk.green('○ idle'),
|
|
@@ -90,9 +94,9 @@ function pctCell(v, width) {
|
|
|
90
94
|
* Without it (probe skipped) the classic reachability line is used. A fleet
|
|
91
95
|
* capacity summary is appended whenever stats were gathered.
|
|
92
96
|
*/
|
|
93
|
-
function renderDeviceTable(reg, names, self, statsMap, full = false) {
|
|
97
|
+
function renderDeviceTable(reg, names, self, statsMap, full = false, interactiveHost) {
|
|
94
98
|
if (!statsMap)
|
|
95
|
-
return names.map((n) => deviceSummary(reg[n], n === self));
|
|
99
|
+
return names.map((n) => deviceSummary(reg[n], n === self, undefined, n === interactiveHost));
|
|
96
100
|
const lines = [];
|
|
97
101
|
const head = ' ' +
|
|
98
102
|
chalk.gray('device'.padEnd(16)) +
|
|
@@ -132,7 +136,8 @@ function renderDeviceTable(reg, names, self, statsMap, full = false) {
|
|
|
132
136
|
: '';
|
|
133
137
|
const badge = HEADROOM_BADGE[headroom(stats)];
|
|
134
138
|
const here = isSelf ? chalk.cyan(' ← this machine') : '';
|
|
135
|
-
|
|
139
|
+
const interactive = name === interactiveHost ? chalk.yellow(' ★ interactive') : '';
|
|
140
|
+
lines.push(`${marker}${label}${plat} ${cores}${load}${mem}${freeTotal} ${badge}${relay}${here}${interactive}`);
|
|
136
141
|
}
|
|
137
142
|
// Fleet capacity summary — total cores + how much RAM is free right now.
|
|
138
143
|
const cap = fleetCapacity(statsMap.values());
|
|
@@ -685,10 +690,13 @@ function registerDevicesCommands(program) {
|
|
|
685
690
|
Typical workflow:
|
|
686
691
|
agents devices sync # curate: pick which tailscale nodes to keep (TTY)
|
|
687
692
|
agents devices sync --yes # non-interactive: register all non-ignored nodes
|
|
688
|
-
agents devices list # see what's registered
|
|
693
|
+
agents devices list # see what's registered (★ = interactive host)
|
|
689
694
|
agents devices ignore ipad165 # dismiss a node so it's never re-suggested
|
|
690
695
|
agents devices disable zion # exclude a device from Factory auto-launch
|
|
691
696
|
agents devices prefer mac-mini # boost a device in Factory auto-launch ranking
|
|
697
|
+
agents devices set-interactive zion # where agents show YOU artifacts
|
|
698
|
+
agents devices configure mac-mini --max-agents 4 --scheduler off
|
|
699
|
+
agents devices note mac-mini "runs the releases — don't reboot"
|
|
692
700
|
agents devices set win-mini --auth password --bundle muqsit
|
|
693
701
|
agents devices render --write # write ~/.ssh/config.d/agents include
|
|
694
702
|
agents fleet update # roll out latest agents-cli to every online device
|
|
@@ -824,12 +832,214 @@ Typical workflow:
|
|
|
824
832
|
process.exit(1);
|
|
825
833
|
}
|
|
826
834
|
});
|
|
835
|
+
const setInteractiveCmd = devicesCmd
|
|
836
|
+
.command('set-interactive [name]')
|
|
837
|
+
.description('Get or set the interactive host — the one device that shows YOU artifacts (browser opens, dashboards, rendered plans). Stored fleet-wide as config.interactiveHost in central agents.yaml.')
|
|
838
|
+
.option('--unset', 'clear the interactive host')
|
|
839
|
+
.option('--json', 'output machine-readable JSON')
|
|
840
|
+
.action(async (name, opts) => {
|
|
841
|
+
try {
|
|
842
|
+
if (opts.unset) {
|
|
843
|
+
unsetConfigValue('interactive.host');
|
|
844
|
+
if (opts.json)
|
|
845
|
+
process.stdout.write(JSON.stringify({ interactiveHost: null }, null, 2) + '\n');
|
|
846
|
+
else
|
|
847
|
+
console.log(chalk.green('Cleared the interactive host.'));
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
if (name) {
|
|
851
|
+
await mustGetDevice(name);
|
|
852
|
+
setConfigValue('interactive.host', name);
|
|
853
|
+
if (opts.json)
|
|
854
|
+
process.stdout.write(JSON.stringify({ interactiveHost: name }, null, 2) + '\n');
|
|
855
|
+
else
|
|
856
|
+
console.log(chalk.green(`Interactive host: '${name}'`) + chalk.gray(' — agents show you artifacts there. Clear with --unset.'));
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
const current = getConfigValue('interactive.host').value;
|
|
860
|
+
if (opts.json) {
|
|
861
|
+
process.stdout.write(JSON.stringify({ interactiveHost: current ?? null }, null, 2) + '\n');
|
|
862
|
+
}
|
|
863
|
+
else if (current) {
|
|
864
|
+
console.log(`${chalk.bold('Interactive host:')} ${chalk.cyan(current)}`);
|
|
865
|
+
}
|
|
866
|
+
else {
|
|
867
|
+
console.log(chalk.gray("No interactive host set. Set one with 'agents devices set-interactive <name>'."));
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
catch (err) {
|
|
871
|
+
console.error(chalk.red(err.message));
|
|
872
|
+
process.exit(1);
|
|
873
|
+
}
|
|
874
|
+
});
|
|
875
|
+
setHelpSections(setInteractiveCmd, {
|
|
876
|
+
examples: `
|
|
877
|
+
agents devices set-interactive zion # zion is where artifacts open for you
|
|
878
|
+
agents devices set-interactive # print the current interactive host
|
|
879
|
+
agents devices set-interactive --unset # back to no interactive host
|
|
880
|
+
agents devices set-interactive --json # machine-readable (for skills)
|
|
881
|
+
`,
|
|
882
|
+
notes: `
|
|
883
|
+
The interactive host answers "which online macOS device do I show this on?"
|
|
884
|
+
so skills stop guessing. It is marked ★ interactive in 'agents devices list'.
|
|
885
|
+
The value syncs fleet-wide (central agents.yaml, config.interactiveHost);
|
|
886
|
+
per-device settings live under 'agents devices configure' instead.
|
|
887
|
+
`,
|
|
888
|
+
});
|
|
889
|
+
const configureCmd = devicesCmd
|
|
890
|
+
.command('configure <name>')
|
|
891
|
+
.description('Get or set per-device config: --max-agents, --scheduler. Written to ~/.agents/devices/<name>/agents.yaml (works for any device — the devices/ tree syncs). Unset = default behavior.')
|
|
892
|
+
.option('--max-agents <n>', 'cap concurrent agents (Factory auto-launch counts device-wide; teams placement counts the team’s roster on the device)')
|
|
893
|
+
.option('--scheduler <on|off>', 'allow the routines scheduler (daemon) to fire on this device (takes effect on daemon reload/restart)')
|
|
894
|
+
.option('--json', 'output machine-readable JSON')
|
|
895
|
+
.action(async (name, opts) => {
|
|
896
|
+
try {
|
|
897
|
+
await mustGetDevice(name);
|
|
898
|
+
const parseOnOff = (flag, raw) => {
|
|
899
|
+
if (raw === 'on')
|
|
900
|
+
return true;
|
|
901
|
+
if (raw === 'off')
|
|
902
|
+
return false;
|
|
903
|
+
throw new Error(`--${flag} expects 'on' or 'off', got '${raw}'.`);
|
|
904
|
+
};
|
|
905
|
+
const writes = [];
|
|
906
|
+
if (opts.maxAgents !== undefined) {
|
|
907
|
+
const n = Number(opts.maxAgents);
|
|
908
|
+
if (!Number.isInteger(n))
|
|
909
|
+
throw new Error(`--max-agents expects an integer, got '${opts.maxAgents}'.`);
|
|
910
|
+
writes.push(['agents.max-concurrent', n]);
|
|
911
|
+
}
|
|
912
|
+
if (opts.scheduler !== undefined)
|
|
913
|
+
writes.push(['scheduler.enabled', parseOnOff('scheduler', opts.scheduler)]);
|
|
914
|
+
if (writes.length > 0) {
|
|
915
|
+
for (const [key, value] of writes)
|
|
916
|
+
setConfigValue(key, value, { device: name });
|
|
917
|
+
if (!opts.json) {
|
|
918
|
+
for (const [key, value] of writes) {
|
|
919
|
+
console.log(chalk.green(`Set ${key} = ${JSON.stringify(value)}`) + chalk.gray(` on '${name}'.`));
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
if (opts.json || writes.length === 0) {
|
|
924
|
+
const entries = listConfig({ device: name }).filter((e) => e.spec.scope === 'device');
|
|
925
|
+
if (opts.json) {
|
|
926
|
+
const config = {};
|
|
927
|
+
for (const e of entries)
|
|
928
|
+
if (e.value !== undefined)
|
|
929
|
+
config[e.spec.name] = e.value;
|
|
930
|
+
process.stdout.write(JSON.stringify({ device: name, config }, null, 2) + '\n');
|
|
931
|
+
}
|
|
932
|
+
else {
|
|
933
|
+
console.log(chalk.bold(`Config for '${name}'`));
|
|
934
|
+
for (const e of entries) {
|
|
935
|
+
const value = e.value === undefined ? chalk.gray('— (default)') : chalk.cyan(JSON.stringify(e.value));
|
|
936
|
+
console.log(` ${e.spec.name.padEnd(24)} ${value}${chalk.gray(` ${e.spec.description}`)}`);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
catch (err) {
|
|
942
|
+
console.error(chalk.red(err.message));
|
|
943
|
+
process.exit(1);
|
|
944
|
+
}
|
|
945
|
+
});
|
|
946
|
+
setHelpSections(configureCmd, {
|
|
947
|
+
examples: `
|
|
948
|
+
agents devices configure mac-mini --max-agents 4 # cap concurrent agents
|
|
949
|
+
agents devices configure mac-mini --scheduler off # no routines firing there
|
|
950
|
+
agents devices configure mac-mini # print its current config
|
|
951
|
+
agents devices configure mac-mini --json # machine-readable
|
|
952
|
+
`,
|
|
953
|
+
notes: `
|
|
954
|
+
Run it on any machine for any device: the value lands in
|
|
955
|
+
~/.agents/devices/<name>/agents.yaml locally and reaches the device on the
|
|
956
|
+
next 'agents repo push/pull'. Unset keys keep today's behavior.
|
|
957
|
+
--scheduler takes effect when the daemon reloads or restarts on that
|
|
958
|
+
device ('agents routines start' / the reload a 'routines add' sends).
|
|
959
|
+
For the default browser profile use 'agents browser profiles set-default
|
|
960
|
+
<name>'; for free-form text use 'agents devices note'.
|
|
961
|
+
`,
|
|
962
|
+
});
|
|
963
|
+
const noteCmd = devicesCmd
|
|
964
|
+
.command('note <name> [text...]')
|
|
965
|
+
.description('Append a free-form note to a device (repeat to append more). No text prints the notes; --clear empties them.')
|
|
966
|
+
.option('--clear', 'remove all notes from the device')
|
|
967
|
+
.option('--json', 'output machine-readable JSON')
|
|
968
|
+
.action(async (name, text, opts) => {
|
|
969
|
+
try {
|
|
970
|
+
await mustGetDevice(name);
|
|
971
|
+
if (opts.clear) {
|
|
972
|
+
unsetConfigValue('notes', { device: name });
|
|
973
|
+
if (opts.json)
|
|
974
|
+
process.stdout.write(JSON.stringify({ device: name, notes: [] }, null, 2) + '\n');
|
|
975
|
+
else
|
|
976
|
+
console.log(chalk.green(`Cleared notes on '${name}'.`));
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
if (text.length > 0) {
|
|
980
|
+
const existing = getConfigValue('notes', { device: name }).value ?? [];
|
|
981
|
+
const notes = [...existing, text.join(' ')];
|
|
982
|
+
setConfigValue('notes', notes, { device: name });
|
|
983
|
+
if (opts.json)
|
|
984
|
+
process.stdout.write(JSON.stringify({ device: name, notes }, null, 2) + '\n');
|
|
985
|
+
else
|
|
986
|
+
console.log(chalk.green(`Noted on '${name}':`) + ` ${text.join(' ')}`);
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
const notes = getConfigValue('notes', { device: name }).value ?? [];
|
|
990
|
+
if (opts.json) {
|
|
991
|
+
process.stdout.write(JSON.stringify({ device: name, notes }, null, 2) + '\n');
|
|
992
|
+
}
|
|
993
|
+
else if (notes.length > 0) {
|
|
994
|
+
console.log(chalk.bold(`Notes for '${name}'`));
|
|
995
|
+
for (const n of notes)
|
|
996
|
+
console.log(` ${chalk.gray('•')} ${n}`);
|
|
997
|
+
}
|
|
998
|
+
else {
|
|
999
|
+
console.log(chalk.gray(`No notes on '${name}'. Add one with 'agents devices note ${name} "..."'.`));
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
catch (err) {
|
|
1003
|
+
console.error(chalk.red(err.message));
|
|
1004
|
+
process.exit(1);
|
|
1005
|
+
}
|
|
1006
|
+
});
|
|
1007
|
+
setHelpSections(noteCmd, {
|
|
1008
|
+
examples: `
|
|
1009
|
+
agents devices note mac-mini "runs the releases — don't reboot"
|
|
1010
|
+
agents devices note mac-mini "4 displays attached" # appends a second note
|
|
1011
|
+
agents devices note mac-mini # print its notes
|
|
1012
|
+
agents devices note mac-mini --clear # drop them all
|
|
1013
|
+
`,
|
|
1014
|
+
notes: `
|
|
1015
|
+
Notes are operator memory for a box (why it exists, what to never do to
|
|
1016
|
+
it). They sync like every other device doc; 'agents devices list --json'
|
|
1017
|
+
carries them under config.notes.
|
|
1018
|
+
`,
|
|
1019
|
+
});
|
|
1020
|
+
/** Device-scope config block for `list --json`, keyed by yamlKey (set keys only). */
|
|
1021
|
+
const deviceConfigJson = (name) => {
|
|
1022
|
+
const config = {};
|
|
1023
|
+
for (const entry of listConfig({ device: name })) {
|
|
1024
|
+
if (entry.spec.scope !== 'device' || entry.value === undefined)
|
|
1025
|
+
continue;
|
|
1026
|
+
config[entry.spec.yamlKey] = entry.value;
|
|
1027
|
+
}
|
|
1028
|
+
return Object.keys(config).length > 0 ? config : undefined;
|
|
1029
|
+
};
|
|
827
1030
|
const runList = async (opts = {}) => {
|
|
828
1031
|
const reg = await loadDevices();
|
|
829
1032
|
const names = Object.keys(reg).sort();
|
|
1033
|
+
const interactiveHost = getConfigValue('interactive.host').value;
|
|
830
1034
|
if (opts.json) {
|
|
831
|
-
// Registry
|
|
832
|
-
|
|
1035
|
+
// Registry + local device docs, always fast — the Factory extension polls
|
|
1036
|
+
// this path. Each row carries its device-scope `config` (maxAgents,
|
|
1037
|
+
// schedulerEnabled, notes, defaultBrowserProfile — set keys
|
|
1038
|
+
// only) and an `interactive` flag for the configured interactive host.
|
|
1039
|
+
process.stdout.write(JSON.stringify(names.map((n) => {
|
|
1040
|
+
const config = deviceConfigJson(n);
|
|
1041
|
+
return { ...reg[n], interactive: n === interactiveHost, ...(config ? { config } : {}) };
|
|
1042
|
+
}), null, 2) + '\n');
|
|
833
1043
|
return;
|
|
834
1044
|
}
|
|
835
1045
|
if (names.length === 0) {
|
|
@@ -867,7 +1077,7 @@ Typical workflow:
|
|
|
867
1077
|
await writeReachability(collectReachabilityWriteBacks(reg, statsMap)).catch(() => { });
|
|
868
1078
|
}
|
|
869
1079
|
console.log(chalk.bold(`Devices (${names.length})`));
|
|
870
|
-
for (const line of renderDeviceTable(reg, names, self, statsMap, opts.full))
|
|
1080
|
+
for (const line of renderDeviceTable(reg, names, self, statsMap, opts.full, interactiveHost))
|
|
871
1081
|
console.log(line);
|
|
872
1082
|
if (freshness?.servedFromCache && freshness.oldestFetchedAt != null) {
|
|
873
1083
|
console.log(chalk.gray(` updated ${formatCheckedAge(freshness.oldestFetchedAt)} — pass --refresh (--live) for a live probe`));
|