@phnx-labs/agents-cli 1.22.46 → 1.22.49

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.
Files changed (348) hide show
  1. package/CHANGELOG.md +663 -0
  2. package/LICENSE +102 -182
  3. package/README.md +96 -49
  4. package/dist/bootstrap.js +4 -2
  5. package/dist/cli/command-registry.d.ts +2 -1
  6. package/dist/cli/command-registry.js +6 -2
  7. package/dist/commands/accounts.d.ts +58 -0
  8. package/dist/commands/accounts.js +243 -25
  9. package/dist/commands/artifacts.js +7 -6
  10. package/dist/commands/attach.js +1 -1
  11. package/dist/commands/auth.js +7 -5
  12. package/dist/commands/browser-sessions-picker.js +9 -7
  13. package/dist/commands/browser.js +711 -62
  14. package/dist/commands/config.d.ts +0 -1
  15. package/dist/commands/config.js +26 -24
  16. package/dist/commands/cost.d.ts +1 -1
  17. package/dist/commands/daemon.d.ts +21 -0
  18. package/dist/commands/daemon.js +147 -12
  19. package/dist/commands/detach.d.ts +13 -0
  20. package/dist/commands/detach.js +11 -8
  21. package/dist/commands/exec.js +107 -53
  22. package/dist/commands/feed-watch.d.ts +2 -0
  23. package/dist/commands/feed-watch.js +28 -0
  24. package/dist/commands/feed.js +27 -0
  25. package/dist/commands/feedback.js +3 -19
  26. package/dist/commands/focus.d.ts +14 -0
  27. package/dist/commands/focus.js +48 -5
  28. package/dist/commands/go.d.ts +3 -0
  29. package/dist/commands/go.js +16 -3
  30. package/dist/commands/insights.d.ts +1 -1
  31. package/dist/commands/insights.js +2 -2
  32. package/dist/commands/inspect.js +2 -2
  33. package/dist/commands/lease.d.ts +1 -1
  34. package/dist/commands/lease.js +7 -6
  35. package/dist/commands/mailboxes.js +4 -3
  36. package/dist/commands/menubar.js +11 -4
  37. package/dist/commands/projects.d.ts +16 -1
  38. package/dist/commands/projects.js +58 -64
  39. package/dist/commands/resume.js +1 -1
  40. package/dist/commands/route.js +4 -4
  41. package/dist/commands/routines.js +4 -3
  42. package/dist/commands/run-broadcast.d.ts +1 -1
  43. package/dist/commands/run-broadcast.js +1 -1
  44. package/dist/commands/sessions-picker-factory.d.ts +2 -1
  45. package/dist/commands/sessions-picker-factory.js +1 -1
  46. package/dist/commands/sessions-picker.d.ts +22 -0
  47. package/dist/commands/sessions-picker.js +48 -9
  48. package/dist/commands/sessions-stop.d.ts +24 -0
  49. package/dist/commands/sessions-stop.js +66 -0
  50. package/dist/commands/sessions-trace.d.ts +82 -0
  51. package/dist/commands/sessions-trace.js +387 -0
  52. package/dist/commands/sessions.js +5 -0
  53. package/dist/commands/setup-secrets.js +23 -0
  54. package/dist/commands/share.d.ts +20 -5
  55. package/dist/commands/share.js +152 -52
  56. package/dist/commands/ssh.d.ts +15 -0
  57. package/dist/commands/ssh.js +243 -18
  58. package/dist/commands/teams.js +34 -2
  59. package/dist/commands/traces.d.ts +2 -0
  60. package/dist/commands/traces.js +184 -0
  61. package/dist/commands/view.d.ts +2 -0
  62. package/dist/commands/view.js +32 -62
  63. package/dist/commands/watchdog.js +9 -14
  64. package/dist/index.d.ts +1 -0
  65. package/dist/index.js +5 -0
  66. package/dist/lib/account-capabilities.d.ts +4 -0
  67. package/dist/lib/account-capabilities.js +13 -9
  68. package/dist/lib/account-registry.d.ts +2 -0
  69. package/dist/lib/account-registry.js +30 -2
  70. package/dist/lib/accounting/rotate.d.ts +9 -0
  71. package/dist/lib/accounting/rotate.js +12 -2
  72. package/dist/lib/accounting/usage.d.ts +84 -6
  73. package/dist/lib/accounting/usage.js +293 -75
  74. package/dist/lib/agent-modes.js +29 -1
  75. package/dist/lib/agent-spec/agents.d.ts +4 -3
  76. package/dist/lib/agent-spec/agents.js +25 -15
  77. package/dist/lib/analytics/mix-commands.d.ts +1 -1
  78. package/dist/lib/analytics/mix-commands.js +2 -2
  79. package/dist/lib/auth-health.d.ts +13 -3
  80. package/dist/lib/auth-health.js +50 -8
  81. package/dist/lib/browser/devices.d.ts +19 -0
  82. package/dist/lib/browser/devices.js +23 -0
  83. package/dist/lib/browser/drivers/ssh.d.ts +16 -2
  84. package/dist/lib/browser/drivers/ssh.js +36 -9
  85. package/dist/lib/browser/ipc.d.ts +1 -1
  86. package/dist/lib/browser/ipc.js +40 -9
  87. package/dist/lib/browser/profiles.d.ts +104 -65
  88. package/dist/lib/browser/profiles.js +277 -165
  89. package/dist/lib/browser/registry.d.ts +39 -0
  90. package/dist/lib/browser/registry.js +149 -0
  91. package/dist/lib/browser/remote-control.d.ts +28 -2
  92. package/dist/lib/browser/remote-control.js +35 -3
  93. package/dist/lib/browser/resolve-target.d.ts +68 -0
  94. package/dist/lib/browser/resolve-target.js +250 -0
  95. package/dist/lib/browser/runtime-state.d.ts +44 -11
  96. package/dist/lib/browser/runtime-state.js +98 -15
  97. package/dist/lib/browser/service.d.ts +65 -9
  98. package/dist/lib/browser/service.js +196 -61
  99. package/dist/lib/browser/sessions-list.d.ts +1 -3
  100. package/dist/lib/browser/sessions-list.js +3 -16
  101. package/dist/lib/browser/task-index.d.ts +70 -0
  102. package/dist/lib/browser/task-index.js +180 -0
  103. package/dist/lib/browser/types.d.ts +12 -1
  104. package/dist/lib/byok-usage.js +2 -2
  105. package/dist/lib/claude-statusline.d.ts +29 -0
  106. package/dist/lib/claude-statusline.js +139 -0
  107. package/dist/lib/cloud/host.d.ts +1 -1
  108. package/dist/lib/cloud/host.js +1 -1
  109. package/dist/lib/cloud/rush.js +1 -1
  110. package/dist/lib/codex-policy.d.ts +11 -1
  111. package/dist/lib/codex-policy.js +20 -5
  112. package/dist/lib/computer/computer-rpc.js +2 -2
  113. package/dist/lib/computer/download.d.ts +15 -15
  114. package/dist/lib/computer/download.js +35 -98
  115. package/dist/lib/computer/ssh-tunnel.d.ts +0 -3
  116. package/dist/lib/computer/ssh-tunnel.js +12 -22
  117. package/dist/lib/config-keys.d.ts +6 -11
  118. package/dist/lib/config-keys.js +18 -19
  119. package/dist/lib/crabbox/cli.d.ts +1 -1
  120. package/dist/lib/crabbox/cli.js +2 -2
  121. package/dist/lib/crabbox/lease.d.ts +9 -0
  122. package/dist/lib/crabbox/lease.js +70 -15
  123. package/dist/lib/crabbox/runtimes.js +1 -1
  124. package/dist/lib/daemon/account-state-daemon-service.d.ts +16 -0
  125. package/dist/lib/daemon/account-state-daemon-service.js +26 -0
  126. package/dist/lib/daemon/browser-ipc-service.d.ts +20 -0
  127. package/dist/lib/daemon/browser-ipc-service.js +46 -0
  128. package/dist/lib/daemon/daemon.d.ts +3 -0
  129. package/dist/lib/daemon/daemon.js +212 -444
  130. package/dist/lib/daemon/device-probe-service.d.ts +26 -0
  131. package/dist/lib/daemon/device-probe-service.js +47 -0
  132. package/dist/lib/daemon/keychain-reap-service.d.ts +17 -0
  133. package/dist/lib/daemon/keychain-reap-service.js +32 -0
  134. package/dist/lib/daemon/monitor-engine-service.d.ts +22 -0
  135. package/dist/lib/daemon/monitor-engine-service.js +29 -0
  136. package/dist/lib/daemon/runner.js +15 -2
  137. package/dist/lib/daemon/secrets-broker-service.d.ts +20 -0
  138. package/dist/lib/daemon/secrets-broker-service.js +66 -0
  139. package/dist/lib/daemon/self-heal-service.d.ts +27 -0
  140. package/dist/lib/daemon/self-heal-service.js +48 -0
  141. package/dist/lib/daemon/service.d.ts +102 -0
  142. package/dist/lib/daemon/service.js +98 -0
  143. package/dist/lib/daemon/session-index-service.d.ts +22 -0
  144. package/dist/lib/daemon/session-index-service.js +40 -0
  145. package/dist/lib/daemon/state-dir-check-service.d.ts +43 -0
  146. package/dist/lib/daemon/state-dir-check-service.js +63 -0
  147. package/dist/lib/daemon/supervisor.d.ts +87 -0
  148. package/dist/lib/daemon/supervisor.js +298 -0
  149. package/dist/lib/daemon/watchdog-service.d.ts +19 -0
  150. package/dist/lib/daemon/watchdog-service.js +40 -0
  151. package/dist/lib/daemon-health.d.ts +15 -0
  152. package/dist/lib/daemon-health.js +29 -5
  153. package/dist/lib/daemon-services.d.ts +8 -1
  154. package/dist/lib/daemon-services.js +55 -0
  155. package/dist/lib/daemon-ticks.d.ts +13 -2
  156. package/dist/lib/daemon-ticks.js +22 -20
  157. package/dist/lib/device-config.d.ts +25 -10
  158. package/dist/lib/device-config.js +90 -39
  159. package/dist/lib/devices/config-migration.d.ts +7 -0
  160. package/dist/lib/devices/config-migration.js +67 -3
  161. package/dist/lib/devices/health.d.ts +27 -2
  162. package/dist/lib/devices/health.js +39 -9
  163. package/dist/lib/devices/interactive-host.d.ts +15 -0
  164. package/dist/lib/devices/interactive-host.js +53 -0
  165. package/dist/lib/devices/registry.d.ts +61 -4
  166. package/dist/lib/devices/registry.js +94 -38
  167. package/dist/lib/devices/stats-cache.d.ts +26 -0
  168. package/dist/lib/devices/stats-cache.js +59 -7
  169. package/dist/lib/devices/worker-pick.d.ts +48 -0
  170. package/dist/lib/devices/worker-pick.js +113 -0
  171. package/dist/lib/exec.d.ts +70 -14
  172. package/dist/lib/exec.js +110 -38
  173. package/dist/lib/feed/answer.d.ts +29 -0
  174. package/dist/lib/feed/answer.js +145 -0
  175. package/dist/lib/feed/attention.d.ts +83 -0
  176. package/dist/lib/feed/attention.js +0 -0
  177. package/dist/lib/feed/feed.d.ts +109 -0
  178. package/dist/lib/feed/feed.js +154 -3
  179. package/dist/lib/feed/pr-status.d.ts +14 -0
  180. package/dist/lib/feed/pr-status.js +47 -0
  181. package/dist/lib/feed/watch.d.ts +59 -0
  182. package/dist/lib/feed/watch.js +180 -0
  183. package/dist/lib/fleet/capture.js +6 -0
  184. package/dist/lib/fleet/remote-login.js +8 -3
  185. package/dist/lib/fleet/types.d.ts +17 -0
  186. package/dist/lib/harness/adapter.d.ts +11 -0
  187. package/dist/lib/harness/adapters/claude.js +38 -24
  188. package/dist/lib/harness/adapters/codex.js +2 -4
  189. package/dist/lib/harness/adapters/cursor.js +8 -11
  190. package/dist/lib/helper-download.d.ts +108 -0
  191. package/dist/lib/helper-download.js +194 -0
  192. package/dist/lib/helper-versions.d.ts +52 -0
  193. package/dist/lib/helper-versions.js +58 -0
  194. package/dist/lib/hooks/cache.js +19 -0
  195. package/dist/lib/hooks/install.js +2 -2
  196. package/dist/lib/hooks/match.d.ts +3 -0
  197. package/dist/lib/hooks/match.js +25 -0
  198. package/dist/lib/hosts/credentials.d.ts +1 -1
  199. package/dist/lib/hosts/credentials.js +1 -1
  200. package/dist/lib/hosts/dispatch.d.ts +7 -1
  201. package/dist/lib/hosts/dispatch.js +24 -5
  202. package/dist/lib/hosts/passthrough.d.ts +8 -15
  203. package/dist/lib/hosts/passthrough.js +35 -16
  204. package/dist/lib/hosts/providers/devices.d.ts +1 -1
  205. package/dist/lib/hosts/providers/devices.js +1 -1
  206. package/dist/lib/hosts/reconnect.d.ts +179 -21
  207. package/dist/lib/hosts/reconnect.js +266 -45
  208. package/dist/lib/hosts/registry.js +7 -0
  209. package/dist/lib/installations/migrate.d.ts +4 -5
  210. package/dist/lib/installations/migrate.js +6 -7
  211. package/dist/lib/installations/shims.d.ts +1 -1
  212. package/dist/lib/installations/shims.js +23 -13
  213. package/dist/lib/installations/versions.js +25 -2
  214. package/dist/lib/linear-autoclose.d.ts +1 -1
  215. package/dist/lib/linear-autoclose.js +1 -1
  216. package/dist/lib/loop.d.ts +2 -2
  217. package/dist/lib/loop.js +1 -1
  218. package/dist/lib/menubar/download-menubar.d.ts +47 -0
  219. package/dist/lib/menubar/download-menubar.js +60 -0
  220. package/dist/lib/menubar/install-menubar.d.ts +44 -7
  221. package/dist/lib/menubar/install-menubar.js +105 -23
  222. package/dist/lib/menubar/notify-desktop.d.ts +3 -3
  223. package/dist/lib/menubar/notify-desktop.js +6 -6
  224. package/dist/lib/model-tiers.d.ts +1 -1
  225. package/dist/lib/open-url.d.ts +60 -2
  226. package/dist/lib/open-url.js +167 -12
  227. package/dist/lib/project-probe.d.ts +1 -1
  228. package/dist/lib/project-probe.js +1 -1
  229. package/dist/lib/project-pull.d.ts +1 -1
  230. package/dist/lib/project-pull.js +2 -2
  231. package/dist/lib/projects.d.ts +1 -1
  232. package/dist/lib/projects.js +1 -1
  233. package/dist/lib/refresh-coordinator.d.ts +1 -1
  234. package/dist/lib/refresh-coordinator.js +1 -1
  235. package/dist/lib/sandbox.js +6 -7
  236. package/dist/lib/secrets/download-keychain.d.ts +47 -0
  237. package/dist/lib/secrets/download-keychain.js +70 -0
  238. package/dist/lib/secrets/install-helper.d.ts +20 -1
  239. package/dist/lib/secrets/install-helper.js +44 -3
  240. package/dist/lib/session/active.d.ts +67 -0
  241. package/dist/lib/session/active.js +86 -2
  242. package/dist/lib/session/db.d.ts +11 -1
  243. package/dist/lib/session/db.js +142 -8
  244. package/dist/lib/session/discover.d.ts +23 -1
  245. package/dist/lib/session/discover.js +44 -10
  246. package/dist/lib/session/highlights.d.ts +49 -1
  247. package/dist/lib/session/highlights.js +95 -0
  248. package/dist/lib/session/host-link.d.ts +16 -2
  249. package/dist/lib/session/host-link.js +7 -1
  250. package/dist/lib/session/parse.js +33 -1
  251. package/dist/lib/session/prompt.d.ts +35 -0
  252. package/dist/lib/session/prompt.js +56 -0
  253. package/dist/lib/session/remote/remote-list.d.ts +9 -0
  254. package/dist/lib/session/remote/remote-list.js +19 -2
  255. package/dist/lib/session/remote/watch.d.ts +1 -1
  256. package/dist/lib/session/remote/watch.js +17 -3
  257. package/dist/lib/session/render.js +7 -3
  258. package/dist/lib/session/session-cache.d.ts +53 -0
  259. package/dist/lib/session/session-cache.js +93 -1
  260. package/dist/lib/session/state.js +1 -1
  261. package/dist/lib/session/tool-calls.d.ts +2 -0
  262. package/dist/lib/session/tool-calls.js +1 -1
  263. package/dist/lib/session/trajectory-compare.d.ts +47 -0
  264. package/dist/lib/session/trajectory-compare.js +116 -0
  265. package/dist/lib/session/trajectory-html.d.ts +19 -0
  266. package/dist/lib/session/trajectory-html.js +770 -0
  267. package/dist/lib/session/trajectory-lineage.d.ts +92 -0
  268. package/dist/lib/session/trajectory-lineage.js +195 -0
  269. package/dist/lib/session/trajectory-text.d.ts +37 -0
  270. package/dist/lib/session/trajectory-text.js +284 -0
  271. package/dist/lib/session/trajectory.d.ts +102 -0
  272. package/dist/lib/session/trajectory.js +384 -0
  273. package/dist/lib/session/types.d.ts +15 -0
  274. package/dist/lib/sha256-asset.d.ts +26 -0
  275. package/dist/lib/sha256-asset.js +41 -0
  276. package/dist/lib/share/backend.d.ts +95 -0
  277. package/dist/lib/share/backend.js +139 -0
  278. package/dist/lib/share/delete.d.ts +6 -2
  279. package/dist/lib/share/delete.js +8 -9
  280. package/dist/lib/share/html.d.ts +22 -0
  281. package/dist/lib/share/html.js +88 -0
  282. package/dist/lib/share/provision.d.ts +18 -4
  283. package/dist/lib/share/provision.js +38 -4
  284. package/dist/lib/share/publish.d.ts +39 -15
  285. package/dist/lib/share/publish.js +55 -26
  286. package/dist/lib/share/worker-template.js +171 -11
  287. package/dist/lib/sqlite.js +16 -3
  288. package/dist/lib/ssh-exec.d.ts +65 -0
  289. package/dist/lib/ssh-exec.js +120 -4
  290. package/dist/lib/startup/command-registry.d.ts +3 -1
  291. package/dist/lib/startup/command-registry.js +6 -3
  292. package/dist/lib/startup/dev-build.js +3 -3
  293. package/dist/lib/state.d.ts +2 -2
  294. package/dist/lib/state.js +11 -5
  295. package/dist/lib/subagents-registry.js +1 -1
  296. package/dist/lib/teams/agents.js +9 -0
  297. package/dist/lib/terminal/index.d.ts +1 -1
  298. package/dist/lib/terminal/inject.d.ts +19 -0
  299. package/dist/lib/terminal/inject.js +19 -9
  300. package/dist/lib/terminal/types.d.ts +1 -1
  301. package/dist/lib/terminal/types.js +1 -1
  302. package/dist/lib/tmux/orphan-reap.js +1 -1
  303. package/dist/lib/tmux/session.d.ts +46 -13
  304. package/dist/lib/tmux/session.js +204 -27
  305. package/dist/lib/traces/backend.d.ts +29 -0
  306. package/dist/lib/traces/backend.js +46 -0
  307. package/dist/lib/traces/classify.d.ts +52 -0
  308. package/dist/lib/traces/classify.js +72 -0
  309. package/dist/lib/traces/config.d.ts +10 -0
  310. package/dist/lib/traces/config.js +2 -0
  311. package/dist/lib/traces/provision.d.ts +19 -0
  312. package/dist/lib/traces/provision.js +30 -0
  313. package/dist/lib/traces/sync.d.ts +150 -0
  314. package/dist/lib/traces/sync.js +447 -0
  315. package/dist/lib/traces/worker-template.d.ts +4 -0
  316. package/dist/lib/traces/worker-template.js +159 -0
  317. package/dist/lib/types.d.ts +34 -19
  318. package/dist/lib/types.js +14 -0
  319. package/dist/lib/usage-backoff.d.ts +2 -1
  320. package/dist/lib/usage-backoff.js +60 -19
  321. package/dist/lib/usage-refresh.d.ts +18 -4
  322. package/dist/lib/usage-refresh.js +68 -19
  323. package/dist/lib/watchdog/log.d.ts +1 -1
  324. package/dist/lib/watchdog/log.js +1 -1
  325. package/dist/lib/watchdog/runner.d.ts +36 -45
  326. package/dist/lib/watchdog/runner.js +150 -119
  327. package/dist/lib/watchdog/service.d.ts +0 -1
  328. package/dist/lib/watchdog/service.js +0 -1
  329. package/dist/lib/watchdog/watchdog-agent.d.ts +38 -0
  330. package/dist/lib/watchdog/watchdog-agent.js +59 -0
  331. package/dist/lib/watchdog/watchdog.d.ts +11 -2
  332. package/dist/lib/watchdog/watchdog.js +37 -104
  333. package/dist/lib/workflows.d.ts +1 -1
  334. package/package.json +5 -7
  335. package/dist/commands/usage.d.ts +0 -27
  336. package/dist/commands/usage.js +0 -96
  337. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  338. package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +0 -24
  339. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  340. package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
  341. package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +0 -128
  342. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  343. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -24
  344. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  345. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +0 -123
  346. package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
  347. package/dist/lib/usage-fleet.d.ts +0 -32
  348. package/dist/lib/usage-fleet.js +0 -125
@@ -1,8 +1,12 @@
1
1
  import * as path from 'path';
2
+ import * as fs from 'fs';
2
3
  import { getBrowserRuntimeDir as getBrowserRuntimeDirRoot, readMeta, updateMeta, } from '../state.js';
3
4
  import { getConfigValue } from '../device-config.js';
5
+ import { machineId } from '../machine-id.js';
6
+ import { declaringDevices, profileRegistry, } from './registry.js';
4
7
  import { findBrowserPath, findFirstInstalledBrowser, isPortInUse } from './chrome.js';
5
8
  import { DEFAULT_VIEWPORT } from './devices.js';
9
+ export { declaringDevices, profileKind, profileRegistry, } from './registry.js';
6
10
  /**
7
11
  * Name of the profile `ensureDefaultBrowserProfile` auto-detects and pins.
8
12
  *
@@ -28,25 +32,6 @@ export const LEGACY_DEFAULT_BROWSER_PROFILE_NAME = 'default';
28
32
  * wins, so no existing config breaks.
29
33
  */
30
34
  export const DEFAULT_PROFILE_ALIAS = 'default';
31
- /**
32
- * True for a profile whose contents only make sense on THIS machine, so it must
33
- * live in the per-machine doc rather than the fleet-synced agents.yaml.
34
- *
35
- * The auto-detected `default` always qualifies: it is generated by probing local
36
- * browser installs and a free local port, so its `binary:` is an absolute path
37
- * for one OS. Shared fleet-wide it caused a permanent rewrite loop — a macOS box
38
- * wrote `/Applications/...`, a Linux box rewrote `/usr/bin/chromium-browser`,
39
- * each on every `agents browser start`.
40
- *
41
- * As of RUSH-2716 it is not the only one: EVERY newly created profile is
42
- * machine-local unless the caller explicitly asks for the fleet-synced store
43
- * (`createProfile(p, { fleet: true })` / `--fleet`). See {@link createProfile}.
44
- * This predicate is what keeps an existing `default` that an older version wrote
45
- * into central from being written back there by {@link updateProfile}.
46
- */
47
- function isMachineLocalProfile(name) {
48
- return name === DEFAULT_BROWSER_PROFILE_NAME || name === LEGACY_DEFAULT_BROWSER_PROFILE_NAME;
49
- }
50
35
  /**
51
36
  * The device-local configured default profile name (set via
52
37
  * `agents browser use`), or undefined when unset. When set, it
@@ -81,7 +66,7 @@ export function getProfileDownloadsDir(name) {
81
66
  export function getProfileSessionsDir(name, task) {
82
67
  return path.join(getProfileRuntimeDir(name), 'sessions', task);
83
68
  }
84
- function configToProfile(name, config) {
69
+ function configToProfile(name, config, devices = []) {
85
70
  validateRemoteBrowserBinaries(config);
86
71
  return {
87
72
  name,
@@ -97,6 +82,7 @@ function configToProfile(name, config) {
97
82
  viewport: config.viewport,
98
83
  logDir: config.logDir,
99
84
  logHost: config.logHost,
85
+ devices,
100
86
  };
101
87
  }
102
88
  function profileToConfig(profile) {
@@ -127,38 +113,24 @@ function profileToConfig(profile) {
127
113
  config.logHost = profile.logHost;
128
114
  return config;
129
115
  }
130
- /**
131
- * Every profile visible on this machine: the fleet-shared ones plus this
132
- * machine's own. A machine-local entry WINS on a name collision — it was written
133
- * by this box for this box, whereas the shared copy may carry another OS's
134
- * binary path (that mismatch is exactly why `default` moved local).
135
- */
136
- function allProfileConfigs() {
137
- const meta = readMeta();
138
- return { ...meta.browser, ...meta.deviceBrowser };
116
+ function selectedDeclaration(declarations) {
117
+ return declarations.find((declaration) => declaration.device === machineId()) ?? declarations[0];
139
118
  }
140
- export async function listProfiles() {
141
- const configs = allProfileConfigs();
142
- return Object.entries(configs).map(([name, config]) => configToProfile(name, config));
119
+ function localDeclaration(name) {
120
+ return profileRegistry().get(name)?.find((declaration) => declaration.device === machineId());
143
121
  }
144
- /**
145
- * Every visible profile paired with the store it lives in. Same precedence as
146
- * {@link listProfiles} — a machine-local entry wins a name collision, so a name
147
- * present in both maps reports `local`, which is the copy that is actually used.
148
- */
149
- export async function listProfilesWithScope() {
150
- const meta = readMeta();
151
- const local = meta.deviceBrowser ?? {};
152
- return Object.entries(allProfileConfigs()).map(([name, config]) => ({
153
- profile: configToProfile(name, config),
154
- scope: (local[name] ? 'local' : 'fleet'),
155
- }));
122
+ export async function listProfiles() {
123
+ return [...profileRegistry()].map(([name, declarations]) => {
124
+ const selected = selectedDeclaration(declarations);
125
+ return configToProfile(name, selected.config, declarations.map((declaration) => declaration.device));
126
+ });
156
127
  }
157
128
  export async function getProfile(name) {
158
- const config = allProfileConfigs()[name];
159
- if (!config)
129
+ const declarations = profileRegistry().get(name);
130
+ if (!declarations?.length)
160
131
  return null;
161
- return configToProfile(name, config);
132
+ const selected = selectedDeclaration(declarations);
133
+ return configToProfile(name, selected.config, declarations.map((declaration) => declaration.device));
162
134
  }
163
135
  /**
164
136
  * True when `profile` can actually launch or attach on THIS machine.
@@ -182,26 +154,6 @@ export function isProfileLaunchableHere(profile) {
182
154
  return false;
183
155
  }
184
156
  }
185
- /**
186
- * Resolve the profile `agents browser start` uses when no `--profile` is given.
187
- *
188
- * Order: (1) the device-local configured default (`agents browser profiles
189
- * set-default <name>`) when it names an existing profile that can launch here;
190
- * (2) an existing `default` profile that can launch here; (3) auto-pick the
191
- * first installed Chromium-family browser per the platform priority list in
192
- * chrome.ts (macOS: chrome > brave > edge > chromium > comet > arc; Linux: chrome >
193
- * chromium > brave > edge; Windows: edge > chrome > brave > comet) and pin a
194
- * `default` profile to it. Throws an actionable error if none of those binaries
195
- * are installed.
196
- *
197
- * "Can launch here" matters because `agents.yaml` syncs across the fleet: a
198
- * `default` auto-created on macOS carries a `/Applications/...` binary path that
199
- * doesn't exist on a Linux box. Rather than hand that broken profile back (the
200
- * top browser roadblock — "Custom binary not found"), we re-detect and
201
- * regenerate the `default` for this machine. A configured or existing default
202
- * that no longer exists, or can't launch here, warns and falls through — never a
203
- * hard fail.
204
- */
205
157
  /**
206
158
  * The auto-detected profile this machine already carries, under either name —
207
159
  * the current `auto-chrome` or the `default` an older build wrote. Callers that
@@ -209,8 +161,14 @@ export function isProfileLaunchableHere(profile) {
209
161
  * name, or a machine that predates the RUSH-2709 rename grows a second one.
210
162
  */
211
163
  export async function getAutoDetectedProfile() {
212
- return ((await getProfile(DEFAULT_BROWSER_PROFILE_NAME)) ??
213
- (await getProfile(LEGACY_DEFAULT_BROWSER_PROFILE_NAME)));
164
+ const current = machineId();
165
+ const registry = profileRegistry();
166
+ for (const name of [DEFAULT_BROWSER_PROFILE_NAME, LEGACY_DEFAULT_BROWSER_PROFILE_NAME]) {
167
+ const declaration = registry.get(name)?.find((entry) => entry.device === current);
168
+ if (declaration)
169
+ return configToProfile(name, declaration.config, [current]);
170
+ }
171
+ return null;
214
172
  }
215
173
  /**
216
174
  * Resolve what a caller typed after `--profile` to a real, BARE profile name.
@@ -261,15 +219,16 @@ export async function resolveProfileRef(ref) {
261
219
  * `start`'s resolver. Same order as {@link resolveProfileRef} for an explicit
262
220
  * name, but the IMPLICIT path (no `--profile`, or the bare `default` alias with
263
221
  * no profile of that name) goes through {@link ensureDefaultBrowserProfile} —
264
- * which additionally verifies the resolved default can launch on THIS machine,
265
- * warns and falls through when it can't, and regenerates or creates one.
222
+ * which additionally verifies the resolved default can launch on THIS machine.
223
+ * An undeclared configured default is an error. A declared default whose
224
+ * browser isn't installed here warns and falls through to auto-detect.
266
225
  *
267
226
  * `start` is the only command that launches a browser, so it is the only one
268
227
  * that may do those things; routing a filter-only command through this would
269
228
  * warn about, and rewrite, config the user never asked it to touch.
270
229
  *
271
- * Throws when no profile exists and no supported browser is installed
272
- * `ensureDefaultBrowserProfile`'s actionable "install one of …" error.
230
+ * Throws when the configured default is undeclared, or when no profile exists
231
+ * and no supported browser is installed.
273
232
  */
274
233
  export async function resolveProfileRefForStart(ref) {
275
234
  if (ref && ref !== DEFAULT_PROFILE_ALIAS)
@@ -279,25 +238,51 @@ export async function resolveProfileRefForStart(ref) {
279
238
  return ref;
280
239
  return (await ensureDefaultBrowserProfile()).name;
281
240
  }
241
+ /**
242
+ * Resolve the profile a bare `agents browser start` uses.
243
+ *
244
+ * Order: (1) the device-local configured default (`agents browser use <name>`)
245
+ * when it names a profile that exists and can launch here; (2) an existing
246
+ * auto-detected profile that can launch here; (3) auto-pick the first installed
247
+ * Chromium-family browser and pin `auto-chrome` (or repair a stale legacy
248
+ * `default`) to it.
249
+ *
250
+ * Two failure modes at the configured-default step are not the same:
251
+ * - No device declares the name (including a leftover central `browser:`
252
+ * entry that was never claimed) → throw. Auto-creating `auto-chrome` would
253
+ * hand the agent a logged-out browser while `browser.profile` still names
254
+ * the credentialed one.
255
+ * - The name is declared, but its browser/binary is not installed HERE →
256
+ * warn and fall through. That is a missing binary on this box, not a
257
+ * missing identity; auto-detect is the existing repair.
258
+ */
282
259
  export async function ensureDefaultBrowserProfile() {
283
260
  const configured = getConfiguredDefaultProfileName();
284
261
  if (configured && configured !== DEFAULT_PROFILE_ALIAS) {
285
262
  const chosen = await getProfile(configured);
286
263
  if (chosen && isProfileLaunchableHere(chosen))
287
264
  return chosen;
288
- console.warn(chosen
289
- ? `warning: configured default browser profile "${configured}" can't launch on this ` +
290
- `machine (its browser/binary isn't installed here); falling back to auto-detect. ` +
291
- `Fix with: agents browser use <name> (or --unset)`
292
- : `warning: configured default browser profile "${configured}" no longer exists; ` +
293
- `falling back to auto-detect. Fix with: agents browser use <name> (or --unset)`);
265
+ if (!chosen) {
266
+ const central = readMeta().browser ?? {};
267
+ if (central[configured]) {
268
+ throw new Error(`configured default browser profile "${configured}" is not declared by any device. ` +
269
+ `It still lives in the central agents.yaml browser: map. ` +
270
+ `Claim it on the machine that hosts that browser with: agents browser profiles claim ${configured}`);
271
+ }
272
+ throw new Error(`configured default browser profile "${configured}" is not declared by any device. ` +
273
+ `Create it with: agents browser profiles create ${configured} --browser <chrome|comet|chromium|brave|edge|arc|custom> ` +
274
+ `(or, if it is a leftover central profile, claim it with: agents browser profiles claim ${configured}). ` +
275
+ `Or unset the default with: agents browser use --unset`);
276
+ }
277
+ console.warn(`warning: configured default browser profile "${configured}" can't launch on this ` +
278
+ `machine (its browser/binary isn't installed here); falling back to auto-detect. ` +
279
+ `Fix with: agents browser use <name> (or --unset)`);
294
280
  }
295
281
  // Prefer whichever auto-detected profile this machine already carries: the
296
282
  // current `auto-chrome`, else the `default` an older build wrote. Reusing the
297
283
  // legacy one is what keeps a currently-running browser (and its runtime dirs,
298
284
  // keyed `default@endpoint-0`) attached after the rename.
299
- const existing = (await getProfile(DEFAULT_BROWSER_PROFILE_NAME)) ??
300
- (await getProfile(LEGACY_DEFAULT_BROWSER_PROFILE_NAME));
285
+ const existing = await getAutoDetectedProfile();
301
286
  if (existing && isProfileLaunchableHere(existing))
302
287
  return existing;
303
288
  const detected = findFirstInstalledBrowser();
@@ -420,45 +405,40 @@ function hasSshEndpoint(endpoints) {
420
405
  });
421
406
  }
422
407
  /**
423
- * Create a profile. **It lands in this machine's own store by default** — pass
424
- * `{ fleet: true }` (the CLI's `--fleet`) to put it in the synced `agents.yaml`
425
- * where every box will see it.
408
+ * Refuse a profile whose LOCAL port another profile already owns.
426
409
  *
427
- * Local-by-default is the RUSH-2716 fix. A profile is a local browser install
428
- * (`binary:` is an absolute path for one OS) bound to a locally chosen free port,
429
- * so the fleet-synced copy is wrong on every other machine to begin with; and
430
- * because agents mint throwaway profiles freely, every ad-hoc one was syncing to
431
- * every machine as junk that nothing cleaned up (`agents browser profiles prune`
432
- * is the other half of that fix). Opting in with `--fleet` is for a profile that
433
- * really is fleet config — a remote `ssh://` endpoint, or a named profile whose
434
- * shape you want on every box.
410
+ * Every CDP/SSH profile ends up listening on (or tunneling to) the same LOCAL
411
+ * port number as the one configured in the endpoint URL SSH profiles reuse
412
+ * `?port=N` locally, so we no longer scope by host. Two profiles that would
413
+ * need the same local port can't both run at the same time.
435
414
  *
436
- * Existing profiles are NOT migrated: this only decides where a NEW entry is
437
- * written, so a fleet profile created before this change keeps syncing.
415
+ * `opts.ignore` is the profile's own name on an edit. Without it every edit
416
+ * collides with itself, since the stored copy still owns the port being kept.
438
417
  */
439
- export async function createProfile(profile, opts = {}) {
440
- const meta = readMeta();
441
- const existingConfigs = allProfileConfigs();
442
- if (existingConfigs[profile.name]) {
443
- throw new Error(`Profile "${profile.name}" already exists`);
444
- }
445
- // Collision check. Every CDP/SSH profile ends up listening on (or
446
- // tunneling to) the same LOCAL port number as the one configured in the
447
- // endpoint URL — SSH profiles now reuse `?port=N` locally so we no
448
- // longer need to scope by host. Two profiles that would need the same
449
- // local port can't both run at the same time.
450
- const newLocal = effectiveLocalPort(profile);
451
- if (newLocal !== undefined) {
452
- for (const [existingName, existingConfig] of Object.entries(existingConfigs)) {
453
- const existingProfile = configToProfile(existingName, existingConfig);
454
- const existingLocal = effectiveLocalPort(existingProfile);
455
- if (existingLocal === newLocal) {
456
- throw new Error(`Local port ${newLocal} is already used by profile "${existingName}". ` +
457
- `Each profile must own a unique local port (SSH tunnels now bind ` +
458
- `to their configured port locally too). Pick a different port.`);
459
- }
418
+ export function assertLocalPortFree(profile, opts = {}) {
419
+ const wanted = effectiveLocalPort(profile);
420
+ if (wanted === undefined)
421
+ return;
422
+ for (const [existingName, declarations] of profileRegistry()) {
423
+ if (existingName === opts.ignore)
424
+ continue;
425
+ const declaration = declarations.find((entry) => entry.device === machineId());
426
+ if (!declaration)
427
+ continue;
428
+ const existingProfile = configToProfile(existingName, declaration.config, [machineId()]);
429
+ if (effectiveLocalPort(existingProfile) === wanted) {
430
+ throw new Error(`Local port ${wanted} is already used by profile "${existingName}". ` +
431
+ `Each profile must own a unique local port (SSH tunnels now bind ` +
432
+ `to their configured port locally too). Pick a different port.`);
460
433
  }
461
434
  }
435
+ }
436
+ /** Declare a profile on this device. The same name may be declared by peers. */
437
+ export async function createProfile(profile) {
438
+ if (localDeclaration(profile.name)) {
439
+ throw new Error(`Profile "${profile.name}" already exists on ${machineId()}`);
440
+ }
441
+ assertLocalPortFree(profile);
462
442
  // Resolve the browser binary at create time. Fails fast with an actionable
463
443
  // error ("Comet not installed at /Applications/Comet.app") rather than
464
444
  // deferring the failure to the first task. `findBrowserPath` short-circuits
@@ -471,46 +451,188 @@ export async function createProfile(profile, opts = {}) {
471
451
  findBrowserPath(profile.browser, profile.binary);
472
452
  }
473
453
  const config = profileToConfig(profile);
474
- // Machine-local unless the caller explicitly asked for the fleet store. The
475
- // auto `default` is always local regardless of what a caller passes — its
476
- // binary path and port are this box's, and fleet-syncing it is the rewrite
477
- // loop RUSH-2161 fixed.
478
- if (!opts.fleet || isMachineLocalProfile(profile.name)) {
479
- updateMeta((m) => ({ ...m, deviceBrowser: { ...m.deviceBrowser, [profile.name]: config } }));
480
- return;
481
- }
482
- updateMeta((m) => ({ ...m, browser: { ...m.browser, [profile.name]: config } }));
454
+ updateMeta((meta) => ({
455
+ ...meta,
456
+ deviceBrowser: { ...meta.deviceBrowser, [profile.name]: config },
457
+ }));
483
458
  }
484
459
  export async function updateProfile(profile) {
485
- const meta = readMeta();
486
- const local = meta.deviceBrowser?.[profile.name];
487
- if (!local && !meta.browser?.[profile.name]) {
488
- throw new Error(`Profile "${profile.name}" does not exist`);
460
+ const local = localDeclaration(profile.name);
461
+ if (!local) {
462
+ const devices = declaringDevices(profile.name);
463
+ const where = devices.length > 0 ? `; declared on ${devices.join(', ')}` : '';
464
+ throw new Error(`Profile "${profile.name}" is not declared on ${machineId()}${where}`);
489
465
  }
490
466
  const config = profileToConfig(profile);
491
- // A profile that already lives machine-locally stays there; so does the auto
492
- // `default`, whose binary path and port are specific to this machine.
493
- if (local || isMachineLocalProfile(profile.name)) {
494
- updateMeta((m) => ({ ...m, deviceBrowser: { ...m.deviceBrowser, [profile.name]: config } }));
495
- return;
467
+ updateMeta((meta) => ({
468
+ ...meta,
469
+ deviceBrowser: { ...meta.deviceBrowser, [profile.name]: config },
470
+ }));
471
+ }
472
+ /**
473
+ * Merge `patch` onto the stored profile and persist it in the store it already
474
+ * lives in (see {@link updateProfile} for the scope rules).
475
+ *
476
+ * Runs {@link createProfile}'s validations against the MERGED record, not just
477
+ * the patched fields — a binary edit re-resolves the browser path, a
478
+ * targetFilter edit re-checks the electron gate, and the local-port scan runs
479
+ * with this profile excluded so an unchanged port is not a self-collision.
480
+ */
481
+ export async function editProfile(name, patch) {
482
+ const local = localDeclaration(name);
483
+ if (!local) {
484
+ const devices = declaringDevices(name);
485
+ const where = devices.length > 0 ? `; declared on ${devices.join(', ')}` : '';
486
+ throw new Error(`Profile "${name}" is not declared on ${machineId()}${where}`);
487
+ }
488
+ const devices = declaringDevices(name);
489
+ const current = configToProfile(name, local.config, devices);
490
+ const merged = { ...current, ...patch, name };
491
+ const changed = Object.keys(patch).filter((k) => JSON.stringify(current[k]) !== JSON.stringify(merged[k]));
492
+ // A target filter only means anything for an Electron app, and the gate must
493
+ // read the MERGED record: `--target-filter x` on a profile that is already
494
+ // electron is valid, and `--no-electron` on one that still carries a filter is
495
+ // not. Checking the patch alone would accept both.
496
+ if (merged.targetFilter && !merged.electron) {
497
+ throw new Error(`--target-filter only applies to an Electron profile. ` +
498
+ `Pass --electron, or clear the filter with --target-filter ''.`);
499
+ }
500
+ assertLocalPortFree(merged, { ignore: name });
501
+ // Same rule as create: the binary lives on the remote for an SSH profile, so a
502
+ // local lookup would validate the wrong machine.
503
+ if (!hasSshEndpoint(merged.endpoints)) {
504
+ findBrowserPath(merged.browser, merged.binary);
505
+ }
506
+ await updateProfile(merged);
507
+ return {
508
+ profile: merged,
509
+ devices,
510
+ changed,
511
+ };
512
+ }
513
+ /** Profile names an agent has to be able to type and an fs path can hold. */
514
+ const PROFILE_NAME_RE = /^[a-z][a-z0-9-]*$/;
515
+ /**
516
+ * Throw if `name` cannot be used for a NEW profile.
517
+ *
518
+ * Shared by `profiles create` and {@link renameProfile} so the two cannot drift
519
+ * — the shape rule used to live inline in the command, which is how `rename`
520
+ * would have accepted a name `create` rejects.
521
+ *
522
+ * `default` is refused because it is the reserved ALIAS meaning "whatever
523
+ * profile this machine is configured to use" (RUSH-2709). A literal profile by
524
+ * that name makes `--profile default` mean two different things.
525
+ */
526
+ export function assertRegistrableProfileName(name) {
527
+ if (!PROFILE_NAME_RE.test(name)) {
528
+ throw new Error(`Invalid profile name ${JSON.stringify(name)}. Use lowercase letters, digits and hyphens, ` +
529
+ `starting with a letter — e.g. 'agents'.`);
530
+ }
531
+ if (name === DEFAULT_PROFILE_ALIAS) {
532
+ throw new Error(`"${DEFAULT_PROFILE_ALIAS}" is the reserved alias for this machine's configured profile, ` +
533
+ `not a profile name. Pick another name.`);
534
+ }
535
+ // `browser.viewer: os` means "use the OS default handler". A profile by that
536
+ // name turns the opt-out into a pointer at a profile — silently, since both
537
+ // are just strings in the same key.
538
+ if (name === 'os') {
539
+ throw new Error(`"os" is the reserved browser.viewer value meaning the OS default handler, ` +
540
+ `not a profile name. Pick another name.`);
496
541
  }
497
- updateMeta((m) => ({ ...m, browser: { ...m.browser, [profile.name]: config } }));
498
542
  }
499
- export async function deleteProfile(name) {
500
- const meta = readMeta();
501
- const isLocal = !!meta.deviceBrowser?.[name];
502
- if (!isLocal && !meta.browser?.[name]) {
503
- throw new Error(`Profile "${name}" does not exist`);
504
- }
505
- updateMeta((m) => {
506
- if (isLocal) {
507
- const next = { ...m.deviceBrowser };
508
- delete next[name];
509
- return { ...m, deviceBrowser: Object.keys(next).length > 0 ? next : undefined };
543
+ /**
544
+ * Rename a profile, taking its on-disk state with it.
545
+ *
546
+ * `editProfile` deliberately refuses a name change, and this is why: the name
547
+ * keys the runtime dir ({@link getProfileRuntimeDir}), every fork/endpoint dir
548
+ * derived from it, and the `browser.profile` pointer. Delete-and-recreate — the
549
+ * only route before this — silently abandons the browser's `--user-data-dir`,
550
+ * which is where a profile's logins live. On a real agent browser that is
551
+ * gigabytes of session state and every account it has ever signed into.
552
+ *
553
+ * Refuses when the profile is in use: moving a `--user-data-dir` out from under
554
+ * a running browser corrupts it.
555
+ */
556
+ export async function renameProfile(from, to) {
557
+ if (from === to)
558
+ throw new Error(`"${from}" is already its own name.`);
559
+ const local = localDeclaration(from);
560
+ if (!local) {
561
+ const devices = declaringDevices(from);
562
+ const where = devices.length > 0 ? `; declared on ${devices.join(', ')}` : '';
563
+ throw new Error(`Profile "${from}" is not declared on ${machineId()}${where}`);
564
+ }
565
+ if (profileRegistry().has(to))
566
+ throw new Error(`Profile "${to}" already exists`);
567
+ assertRegistrableProfileName(to);
568
+ const { isProfileInUse, listProfileCacheDirs } = await import('./runtime-state.js');
569
+ if (isProfileInUse(from)) {
570
+ throw new Error(`"${from}" is in use (live browser, tunnel, or open task). Renaming would move its ` +
571
+ `user-data-dir out from under the running browser. Stop it first: agents browser stop --profile ${from}`);
572
+ }
573
+ const config = local.config;
574
+ const devices = declaringDevices(from);
575
+ // Move the on-disk state BEFORE the config, so a crash between the two leaves
576
+ // a profile whose dirs are already where the new name expects them rather than
577
+ // a config pointing at dirs that no longer exist.
578
+ // PRE-FLIGHT every destination before moving ANY of them. With the check
579
+ // inside the loop, dir N was validated only after dirs 0..N-1 had already
580
+ // moved: a collision on the second endpoint left the first one's logins
581
+ // stranded under a name with no config entry, and the error named only the
582
+ // squatter. Nothing repaired that, and the user was never told.
583
+ const plan = [];
584
+ for (const dir of listProfileCacheDirs(from)) {
585
+ const base = path.basename(dir);
586
+ const suffix = base.slice(from.length);
587
+ const dest = path.join(path.dirname(dir), `${to}${suffix}`);
588
+ if (fs.existsSync(dest)) {
589
+ throw new Error(`Cannot rename: ${dest} already exists. Nothing was moved. Remove or rename it first.`);
510
590
  }
511
- const next = { ...m.browser };
591
+ plan.push({ dir, dest });
592
+ }
593
+ const movedDirs = [];
594
+ for (const { dir, dest } of plan) {
595
+ fs.renameSync(dir, dest);
596
+ movedDirs.push(dest);
597
+ }
598
+ updateMeta((meta) => {
599
+ const next = { ...meta.deviceBrowser };
600
+ delete next[from];
601
+ return { ...meta, deviceBrowser: { ...next, [to]: config } };
602
+ });
603
+ // Both pointers are separate keys. Leaving `browser.profile` behind falls back
604
+ // to auto-detect on the next `browser start`; leaving `browser.viewer` behind
605
+ // sends every artifact back to the OS default handler — which is the exact bug
606
+ // the viewer seam was built to fix, reintroduced by a rename.
607
+ const { setConfigValue, getConfigValue } = await import('../device-config.js');
608
+ let repointedDefault = false;
609
+ if (getConfiguredDefaultProfileName() === from) {
610
+ setConfigValue('browser.profile', to);
611
+ repointedDefault = true;
612
+ }
613
+ let repointedViewer = false;
614
+ if (getConfigValue('browser.viewer').value === from) {
615
+ setConfigValue('browser.viewer', to);
616
+ repointedViewer = true;
617
+ }
618
+ // A rename changes only this device's declaration. If it was the only
619
+ // declaration, peer pins become stale and are reported rather than rewritten.
620
+ const { devicesPinningBrowserProfile } = await import('../device-config.js');
621
+ const stalePins = devices.length === 1
622
+ ? devicesPinningBrowserProfile(from).filter((p) => p.device !== machineId())
623
+ : [];
624
+ return { devices, movedDirs, repointedDefault, repointedViewer, stalePins };
625
+ }
626
+ export async function deleteProfile(name) {
627
+ if (!localDeclaration(name)) {
628
+ const devices = declaringDevices(name);
629
+ const where = devices.length > 0 ? `; declared on ${devices.join(', ')}` : '';
630
+ throw new Error(`Profile "${name}" is not declared on ${machineId()}${where}`);
631
+ }
632
+ updateMeta((meta) => {
633
+ const next = { ...meta.deviceBrowser };
512
634
  delete next[name];
513
- return { ...m, browser: Object.keys(next).length > 0 ? next : undefined };
635
+ return { ...meta, deviceBrowser: Object.keys(next).length > 0 ? next : undefined };
514
636
  });
515
637
  }
516
638
  /** Widest NAME column we will grow to before truncating instead. */
@@ -545,13 +667,10 @@ export function padColumn(text, width) {
545
667
  * a leading column, explained by a legend line — so the name column carries just
546
668
  * the name and `default` in it always means the profile of that name.
547
669
  *
548
- * The SCOPE column is the RUSH-2716 companion: it says whether an entry lives on
549
- * this machine only (`local`) or in the fleet-synced `agents.yaml` (`fleet`), so
550
- * "why does this box have a profile I never made here?" is answerable at a
551
- * glance.
670
+ * WHERE lists the devices whose own files declare the name.
552
671
  */
553
672
  export function formatProfilesTable(rows, configuredDefault) {
554
- const cells = rows.map(({ profile, scope }) => {
673
+ const cells = rows.map((profile) => {
555
674
  const presets = getEndpointPresets(profile);
556
675
  const endpoints = Object.entries(presets)
557
676
  .map(([name, ep]) => (name.startsWith('endpoint-') ? ep.target : `${name}=${ep.target}`))
@@ -560,42 +679,35 @@ export function formatProfilesTable(rows, configuredDefault) {
560
679
  marker: profile.name === configuredDefault ? '*' : ' ',
561
680
  name: profile.name,
562
681
  browser: profile.browser || '-',
563
- scope,
682
+ where: profile.devices.join(', '),
564
683
  description: profile.description ?? '',
565
684
  endpoints,
566
685
  };
567
686
  });
568
687
  const nameWidth = Math.min(MAX_NAME_COLUMN, Math.max('NAME'.length, ...cells.map((c) => c.name.length)));
569
688
  const browserWidth = Math.max('BROWSER'.length, ...cells.map((c) => c.browser.length));
570
- // 'local' / 'fleet' are both 5 chars, so the header is the widest value.
571
- const scopeWidth = 'SCOPE'.length;
689
+ const whereWidth = Math.min(36, Math.max('WHERE'.length, ...cells.map((c) => c.where.length)));
572
690
  const hasDescriptions = cells.some((c) => c.description);
573
691
  const descWidth = hasDescriptions
574
692
  ? Math.min(36, Math.max('DESCRIPTION'.length, ...cells.map((c) => c.description.length)))
575
693
  : 0;
576
- const row = (marker, name, browser, scope, desc, endpoints) => `${marker} ` +
694
+ const row = (marker, name, browser, where, desc, endpoints) => `${marker} ` +
577
695
  padColumn(name, nameWidth) + ' ' +
578
696
  padColumn(browser, browserWidth) + ' ' +
579
- padColumn(scope, scopeWidth) + ' ' +
697
+ padColumn(where, whereWidth) + ' ' +
580
698
  (hasDescriptions ? padColumn(desc, descWidth) + ' ' : '') +
581
699
  endpoints;
582
- const header = row(' ', 'NAME', 'BROWSER', 'SCOPE', 'DESCRIPTION', 'ENDPOINTS');
700
+ const header = row(' ', 'NAME', 'BROWSER', 'WHERE', 'DESCRIPTION', 'ENDPOINTS');
583
701
  const lines = [header, '-'.repeat(header.length)];
584
702
  for (const c of cells) {
585
- lines.push(row(c.marker, c.name, c.browser, c.scope, c.description, c.endpoints));
703
+ lines.push(row(c.marker, c.name, c.browser, c.where, c.description, c.endpoints));
586
704
  }
587
705
  if (configuredDefault) {
588
706
  lines.push('');
589
707
  lines.push(`* = this machine's default profile (${configuredDefault}) — what a bare \`agents browser start\` uses.`);
590
708
  }
591
- lines.push(hasFleetRow(rows)
592
- ? 'scope: local = this machine only · fleet = synced to every machine (agents.yaml).'
593
- : 'scope: local = this machine only. Create a synced one with: agents browser profiles create <name> --fleet');
594
709
  return lines;
595
710
  }
596
- function hasFleetRow(rows) {
597
- return rows.some((r) => r.scope === 'fleet');
598
- }
599
711
  /**
600
712
  * Resolve a profile's endpoint presets into a normalized map regardless of
601
713
  * whether the YAML uses the legacy `string[]` shape or the new map shape.
@@ -0,0 +1,39 @@
1
+ import type { BrowserProfileConfig } from '../types.js';
2
+ export interface ProfileDeclaration {
3
+ device: string;
4
+ config: BrowserProfileConfig;
5
+ }
6
+ /** Leftover central `browser:` map from before per-device declarations. Empty when none remain. */
7
+ export declare function centralBrowserProfiles(): Record<string, BrowserProfileConfig>;
8
+ export interface CentralClaimResult {
9
+ claimed: string[];
10
+ skipped: string[];
11
+ }
12
+ /**
13
+ * Fold leftover central `browser:` entries into THIS device's declaration file.
14
+ *
15
+ * Never called implicitly, and that is the point. Every device can read the
16
+ * central map, so an implicit claim races: whichever box happens to read first
17
+ * claims the name, {@link profileKind} then reports it `identity`, and the
18
+ * daemon tunnels to that box — which for a fleet-wide `cdp://localhost:*`
19
+ * profile is a logged-out headless browser wearing a credentialed browser's
20
+ * name. That is the exact bug this module exists to remove, and an implicit
21
+ * migration would write it to disk as a stored fact.
22
+ *
23
+ * The claim is an explicit operator action (`agents browser profiles claim`),
24
+ * run on the machine that actually owns the browser. `canHostHere` is supplied
25
+ * by the command layer so this module stays a leaf — it must not import
26
+ * `isProfileLaunchableHere` (that would cycle through chrome.ts → profiles.ts).
27
+ * Only profiles this machine can host are claimed; the rest stay central,
28
+ * undeclared, and fail loudly on resolve.
29
+ */
30
+ export declare function migrateCentralBrowserProfiles(canHostHere: (config: BrowserProfileConfig) => boolean, name?: string): CentralClaimResult;
31
+ /**
32
+ * Every profile any device declares, keyed by name to all declaring devices.
33
+ * Reads every `devices/<name>/agents.yaml`; declarations never overwrite.
34
+ */
35
+ export declare function profileRegistry(): Map<string, ProfileDeclaration[]>;
36
+ /** Devices declaring `name`, empty when nobody does. */
37
+ export declare function declaringDevices(name: string): string[];
38
+ /** Exactly one declaring device is identity-bearing; several are fungible. */
39
+ export declare function profileKind(name: string): 'identity' | 'fungible' | null;