@phnx-labs/agents-cli 1.20.67 → 1.20.69
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 +100 -0
- package/README.md +14 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/check.js +96 -2
- package/dist/commands/computer.d.ts +26 -0
- package/dist/commands/computer.js +173 -149
- package/dist/commands/doctor.js +37 -9
- package/dist/commands/exec.d.ts +18 -0
- package/dist/commands/exec.js +126 -6
- package/dist/commands/fork.d.ts +9 -0
- package/dist/commands/fork.js +67 -0
- package/dist/commands/plugins.js +11 -2
- package/dist/commands/repo.js +47 -2
- package/dist/commands/run-account-picker.d.ts +14 -0
- package/dist/commands/run-account-picker.js +131 -0
- package/dist/commands/setup-browser.d.ts +18 -0
- package/dist/commands/setup-browser.js +142 -0
- package/dist/commands/setup-computer.d.ts +19 -0
- package/dist/commands/setup-computer.js +133 -0
- package/dist/commands/setup-share.d.ts +17 -0
- package/dist/commands/setup-share.js +85 -0
- package/dist/commands/setup.d.ts +1 -1
- package/dist/commands/setup.js +58 -1
- package/dist/commands/share.d.ts +15 -0
- package/dist/commands/share.js +10 -4
- package/dist/commands/ssh.js +267 -2
- package/dist/commands/teams.d.ts +11 -0
- package/dist/commands/teams.js +39 -1
- package/dist/commands/view.d.ts +4 -14
- package/dist/commands/view.js +39 -36
- package/dist/index.js +2 -1
- package/dist/lib/agents.d.ts +21 -5
- package/dist/lib/agents.js +59 -24
- package/dist/lib/auth-health.d.ts +103 -0
- package/dist/lib/auth-health.js +232 -0
- package/dist/lib/browser/chrome.d.ts +9 -0
- package/dist/lib/browser/chrome.js +22 -0
- package/dist/lib/browser/profiles.js +4 -25
- package/dist/lib/cli-entry.d.ts +23 -0
- package/dist/lib/cli-entry.js +116 -0
- package/dist/lib/computer/download.d.ts +51 -0
- package/dist/lib/computer/download.js +145 -0
- package/dist/lib/computer-rpc.js +9 -3
- package/dist/lib/daemon.d.ts +2 -2
- package/dist/lib/daemon.js +8 -89
- package/dist/lib/devices/connect.d.ts +15 -0
- package/dist/lib/devices/connect.js +17 -5
- package/dist/lib/devices/fleet.d.ts +23 -0
- package/dist/lib/devices/fleet.js +38 -0
- package/dist/lib/devices/health-report.d.ts +46 -0
- package/dist/lib/devices/health-report.js +159 -0
- package/dist/lib/devices/health.d.ts +14 -4
- package/dist/lib/devices/health.js +49 -8
- package/dist/lib/devices/terminfo.d.ts +44 -0
- package/dist/lib/devices/terminfo.js +167 -0
- package/dist/lib/git.d.ts +10 -0
- package/dist/lib/git.js +23 -0
- package/dist/lib/hosts/option.js +1 -1
- package/dist/lib/hosts/ready.js +5 -3
- package/dist/lib/hosts/remote-cmd.d.ts +12 -0
- package/dist/lib/hosts/remote-cmd.js +17 -1
- package/dist/lib/mcp.d.ts +21 -0
- package/dist/lib/mcp.js +278 -13
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/resources/mcp.js +20 -342
- package/dist/lib/rotate.d.ts +12 -8
- package/dist/lib/rotate.js +22 -23
- package/dist/lib/routines.d.ts +13 -0
- package/dist/lib/routines.js +21 -0
- package/dist/lib/runner.js +18 -40
- package/dist/lib/secrets/agent.js +11 -15
- package/dist/lib/session/fork.d.ts +32 -0
- package/dist/lib/session/fork.js +101 -0
- package/dist/lib/share/capture.js +21 -3
- package/dist/lib/signin-badge.d.ts +41 -0
- package/dist/lib/signin-badge.js +64 -0
- package/dist/lib/ssh-exec.d.ts +5 -0
- package/dist/lib/ssh-exec.js +55 -1
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/usage.d.ts +40 -3
- package/dist/lib/usage.js +147 -16
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,105 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.20.69
|
|
4
|
+
|
|
5
|
+
- **Choose a safe account with `agents run <agent>@`.** A trailing `@` opens a
|
|
6
|
+
per-run picker showing each installed version's account identity, login state,
|
|
7
|
+
plan, and available session/weekly/monthly capacity. Logged-out, rate-limited,
|
|
8
|
+
and out-of-credit accounts remain visible but disabled; signed-in accounts
|
|
9
|
+
without quota data remain selectable and say `limits unavailable`. Source:
|
|
10
|
+
`apps/cli/src/commands/run-account-picker.ts`,
|
|
11
|
+
`apps/cli/src/commands/exec.ts`, `apps/cli/src/lib/rotate.ts`.
|
|
12
|
+
|
|
13
|
+
- **The macOS `agents computer` helper now ships as a signed + notarized release asset,
|
|
14
|
+
downloaded on demand.** A fresh `npm i -g @phnx-labs/agents-cli` no longer needs to build
|
|
15
|
+
the Swift helper from source: `agents computer setup` / `agents setup computer` fetch
|
|
16
|
+
`ComputerHelper.app.zip` from the matching `v<version>` GitHub release, verify it against
|
|
17
|
+
the published `.sha256`, and re-check the code signature (Developer ID Team `2HTP252L87`)
|
|
18
|
+
and notarization (`spctl --assess`) before it is ever copied to /Applications — mirroring
|
|
19
|
+
the Windows helper's distribution. The download cache is never a trusted resolver source;
|
|
20
|
+
a cached bundle is only ever read back through the verifying downloader. The helper is
|
|
21
|
+
version-stamped at build time and the release pipeline publishes the asset automatically.
|
|
22
|
+
Source: `apps/cli/src/lib/computer/download.ts`, `apps/cli/src/lib/computer-rpc.ts`,
|
|
23
|
+
`apps/cli/src/commands/computer.ts`, `native/computer-mac/scripts/build.sh`,
|
|
24
|
+
`apps/cli/scripts/publish-computer-helper-mac.sh`, `apps/cli/scripts/release.sh`.
|
|
25
|
+
|
|
26
|
+
- **Live fleet auth health (`agents fleet ping`) + `agents view` chip (#1285).** New `agents fleet ping` completes a real authenticated request for every agent account across the fleet — the ground truth the local "signed in" flag can't give (it can't tell a revoked-but-unexpired token from a good one). Claude/Kimi/Droid are network-verified; Codex/Grok are best-effort. `agents view` now shows a live-status chip per version, read from the shared cache the ping writes. The probe hits the usage endpoint (no model tokens, no session created). Source: `apps/cli/src/lib/auth-health.ts`, `apps/cli/src/commands/ssh.ts`, `apps/cli/src/commands/view.ts`.
|
|
27
|
+
|
|
28
|
+
- **`agents fork` — branch a session into a new independent copy.** Copies a Claude
|
|
29
|
+
session transcript to a fresh session id (rewriting only the `sessionId` field so the
|
|
30
|
+
per-message uuid chain stays intact) beside the original, then registers it so it
|
|
31
|
+
resumes independently from the same cwd and version — the original is left untouched.
|
|
32
|
+
`--name` labels the fork. Source: `apps/cli/src/lib/session/fork.ts`,
|
|
33
|
+
`apps/cli/src/commands/fork.ts`.
|
|
34
|
+
|
|
35
|
+
- **`agents setup` is now a capability hub with guided `browser` / `computer` / `share`
|
|
36
|
+
subcommands.** Bare `agents setup` still clones the system repo and imports unmanaged
|
|
37
|
+
agents, but on a TTY it now also offers to set up the optional capabilities a fresh
|
|
38
|
+
machine needs. Each is also runnable on its own and is idempotent (re-run to change
|
|
39
|
+
settings): `agents setup browser` detects an installed Chromium-family browser and
|
|
40
|
+
creates/points the `default` profile; `agents setup share` provisions or joins a
|
|
41
|
+
Cloudflare share endpoint (reusing `agents share setup`/`join`); `agents setup computer`
|
|
42
|
+
installs the macOS helper and walks you through the Accessibility + Screen-Recording
|
|
43
|
+
grants — opening the exact System Settings panes and polling until trust lands. The
|
|
44
|
+
existing `agents share setup` / `agents computer setup` remain for scripted use. Source:
|
|
45
|
+
`apps/cli/src/commands/setup.ts`, `setup-browser.ts`, `setup-computer.ts`,
|
|
46
|
+
`setup-share.ts`, `apps/cli/src/lib/browser/chrome.ts`, `apps/cli/src/commands/share.ts`.
|
|
47
|
+
|
|
48
|
+
## 1.20.68
|
|
49
|
+
|
|
50
|
+
- **MCP resource handler now syncs project-level agent configs alongside user-level configs (RUSH-671).** `McpHandler.sync` previously wrote resolved MCP servers only to the version-home (user-level) config path. It now also writes project-layer MCP servers to each agent CLI's project-level config path (e.g., `.mcp.json` for Claude, `.codex/config.toml` for Codex) so agent CLIs can discover project-scoped MCPs natively. User-level sync is unchanged. Source: `apps/cli/src/lib/resources/mcp.ts`, `apps/cli/src/lib/agents.ts` (`getProjectMcpConfigPath` exported), `apps/cli/src/lib/resources/mcp.test.ts`.
|
|
51
|
+
- **Production MCP sync path also writes project-level configs.** `installMcpServers` now merges project-layer servers into the agent's project-level config file, using a shared `writeMcpConfig` serializer with overwrite/merge modes. OpenClaw serialization is corrected to nest under `mcp.servers`, matching the existing reader; Grok/OpenClaw user-level configs are written directly with merge mode so multiple servers don't clobber each other; and `installMcpServers` only reports `applied` for agents it actually wrote a config for. Source: `apps/cli/src/lib/mcp.ts`, `apps/cli/src/lib/mcp.test.ts`.
|
|
52
|
+
|
|
53
|
+
- **Accurate account tier and legible usage limits in `agents view`.** Each row's plan
|
|
54
|
+
tier is now derived from `organizationType` (Max/Pro/Team/Enterprise) instead of a
|
|
55
|
+
billingType guess that mislabelled every Max account as "Pro", and the redundant tier
|
|
56
|
+
badge next to the email is dropped for personal plans (multi-seat orgs keep their org
|
|
57
|
+
name, which is real identity). The compact `S:`/`W:` usage bars now show the exact
|
|
58
|
+
percentage and a compact reset hint (`S: ███░░ 58% (3d)`), a signed-in account whose
|
|
59
|
+
usage can't be fetched reads `usage unavailable` instead of a blank gauge, and a new
|
|
60
|
+
`agents view --refresh` (`-r`) forces a live usage refresh past the cache. Source:
|
|
61
|
+
`apps/cli/src/lib/agents.ts`, `apps/cli/src/lib/usage.ts`, `apps/cli/src/commands/view.ts`.
|
|
62
|
+
|
|
63
|
+
- **Fleet health status and drift gate.** `agents fleet status` now renders a fleet-wide warnings rollup plus a device matrix for reachability, resource headroom, sync drift, CLI readiness, and agents-cli version skew; `--json` emits the same report for scripts and `--strict` exits non-zero when any warning is present. `agents check --devices` now fans the existing drift gate across registered devices and exits non-zero when any device is drifted or unreachable. Source: `apps/cli/src/commands/ssh.ts`, `apps/cli/src/commands/check.ts`, `apps/cli/src/lib/devices/fleet.ts`, `apps/cli/src/lib/devices/health-report.ts`.
|
|
64
|
+
|
|
65
|
+
- **Login state per agent — `agents doctor` shows signed-in/logged-out, and `agents run` warns before launching into a logged-out account.** `agents doctor`'s **Agent CLIs** section now renders a `✓ signed in <account>` / `✗ logged out` badge per installed agent (also surfaced in `--json` under `signIn`), and an interactive `agents run <agent>` now prints a one-line stderr warning — `⚠ <agent> looks logged out — log in with: <cmd>. Launching anyway...` — when the account looks logged out, so you find out **before** the TUI opens instead of after typing a prompt and getting `/login` back. It is advisory (file-based `getAccountInfo`, no Keychain ACL prompt) and **never blocks**: skipped for `--json`/`--quiet`, when a rotation already picked a signed-in account, for `--host`/`--lease`, and via `--no-auth-check` / `AGENTS_NO_AUTH_CHECK=1`. `agents view`'s logged-out line now names the exact login command too. One shared badge/hint renderer keeps all three surfaces consistent across claude, codex, kimi, grok, opencode, and gemini. Source: `apps/cli/src/lib/signin-badge.ts` (+ `signin-badge.test.ts`), `apps/cli/src/commands/exec.ts`, `apps/cli/src/commands/doctor.ts`, `apps/cli/src/commands/view.ts`.
|
|
66
|
+
|
|
67
|
+
- **`agents repos add` adopts an existing checkout instead of dead-ending.** When
|
|
68
|
+
the target `~/.agents-<alias>/` already holds a git repo whose origin matches the
|
|
69
|
+
requested source, `repos add` now registers it in place (no re-clone) rather than
|
|
70
|
+
erroring `Directory already exists`. A repo with a *different* origin is left
|
|
71
|
+
untouched unless you pass `--adopt`. This removes the trap that forced a second,
|
|
72
|
+
inconsistent install method when a repo had been cloned by hand. Remote matching
|
|
73
|
+
is transport-agnostic (SSH and HTTPS forms of the same repo compare equal). Source:
|
|
74
|
+
`apps/cli/src/commands/repo.ts`, `apps/cli/src/lib/git.ts`.
|
|
75
|
+
|
|
76
|
+
- **`--json` for `repos list` and `plugins list`.** Both list commands now emit
|
|
77
|
+
machine-readable JSON with `--json`, matching the `repos view --json` /
|
|
78
|
+
`plugins marketplaces --json` that already existed — so an agent can enumerate
|
|
79
|
+
registered repos (with per-repo sync/drift state) and installed plugins (with
|
|
80
|
+
per-agent-version sync targets) without scraping the human table. Source:
|
|
81
|
+
`apps/cli/src/commands/repo.ts`, `apps/cli/src/commands/plugins.ts`.
|
|
82
|
+
|
|
83
|
+
- **`teams add --remote-cwd` now fails loud instead of silently doing nothing.** The
|
|
84
|
+
flag rides the shared `--host` option family but `teams add` treats
|
|
85
|
+
`--host`/`--device` as placement and never reads it, so passing it used to be a
|
|
86
|
+
silent no-op that misled you into thinking it set the teammate's repo path. It is
|
|
87
|
+
now rejected with guidance (place with `--device`, set the code with the team's
|
|
88
|
+
`--repo`, one team per repo). The shared `--remote-cwd` help also warns that a
|
|
89
|
+
local `~` expands on your machine, not the remote host — pass a single-quoted
|
|
90
|
+
`'$HOME/…'` path or a valid remote absolute path. Teams docs + skill lead with
|
|
91
|
+
this. Source: `apps/cli/src/commands/teams.ts` (`remoteCwdOnAddError`),
|
|
92
|
+
`apps/cli/src/lib/hosts/option.ts`, `apps/cli/docs/teams.md`, `skills/teams/SKILL.md`.
|
|
93
|
+
|
|
94
|
+
- **No more CLIXML blobs from Windows hosts.** A remote `agents …` invocation
|
|
95
|
+
routed to a Windows box (`--host win-mini`, `agents doctor --devices`,
|
|
96
|
+
`agents fleet status`) no longer comes back wrapped in a raw `#< CLIXML <Objs …>`
|
|
97
|
+
envelope. PowerShell 5.1 serializes its progress stream ("Preparing modules for
|
|
98
|
+
first use.") to CLIXML when stderr is a captured pipe rather than a console; the
|
|
99
|
+
Windows command builder now silences that stream (`$ProgressPreference =
|
|
100
|
+
'SilentlyContinue'`) so failures read as plain text for humans and the JSON
|
|
101
|
+
parsers that consume the output. Source: `apps/cli/src/lib/hosts/remote-cmd.ts`.
|
|
102
|
+
|
|
3
103
|
## 1.20.67
|
|
4
104
|
|
|
5
105
|
- **Interactive session browser — `agents sessions --active` and a bare `agents sessions`
|
package/README.md
CHANGED
|
@@ -151,10 +151,18 @@ agents run claude "refactor auth module" --mode edit --fallback codex,gemini
|
|
|
151
151
|
```bash
|
|
152
152
|
# Picks the signed-in account you haven't used recently.
|
|
153
153
|
agents run claude "summarize recent commits" --strategy balanced
|
|
154
|
+
|
|
155
|
+
# Or choose one account/version interactively for only this run.
|
|
156
|
+
agents run claude@
|
|
157
|
+
agents run codex@ "review this branch"
|
|
154
158
|
```
|
|
155
159
|
|
|
156
160
|
`--strategy balanced` spreads work across available versions of the same agent -- useful when you have multiple accounts and want to avoid burning through one.
|
|
157
161
|
|
|
162
|
+
A trailing `@` opens an account picker before either an interactive or prompt-based run. Each installed version shows its account identity, exact version, login state, plan, and every available session, weekly, or monthly limit. Logged-out, rate-limited, and out-of-credit accounts remain visible with the reason they cannot be selected; signed-in accounts whose provider does not expose quota data stay selectable and say `limits unavailable`. The choice pins only that run and does not change your default version.
|
|
163
|
+
|
|
164
|
+
Account selection is available for Claude, Codex, Gemini, Grok, Antigravity, Kimi, Droid, and OpenCode. It requires a terminal and cannot be combined with `--resume`, `--strategy`/`--balanced`, `--lease`, or `--host`/`--device`; profiles and workflows must use their concrete host agent instead.
|
|
165
|
+
|
|
158
166
|
### Chain agents
|
|
159
167
|
|
|
160
168
|
```bash
|
|
@@ -424,6 +432,9 @@ agents sync --host gpu-box # make the remote machine current
|
|
|
424
432
|
agents doctor --devices # readiness matrix for every registered device
|
|
425
433
|
agents doctor --devices --json # machine-readable fleet readiness
|
|
426
434
|
agents doctor --device mac-mini # same matrix, scoped to one device
|
|
435
|
+
agents fleet status # warnings rollup + health/sync/version matrix
|
|
436
|
+
agents fleet status --json --strict # scriptable fleet health gate
|
|
437
|
+
agents check --devices # CI drift gate across every registered device
|
|
427
438
|
|
|
428
439
|
# Your Tailscale fleet, auto-discovered
|
|
429
440
|
agents devices sync # ingest `tailscale status`
|
|
@@ -431,7 +442,9 @@ agents devices list # fleet + live headroom: load, mem, idle
|
|
|
431
442
|
agents devices list --full # add per-device cores and free/total RAM
|
|
432
443
|
agents devices list --no-stats # instant: names/addresses only, skip the probe
|
|
433
444
|
agents ssh mac-mini # hardened SSH: fails fast if offline,
|
|
434
|
-
# PowerShell on Windows, password-from-Keychain
|
|
445
|
+
# PowerShell on Windows, password-from-Keychain,
|
|
446
|
+
# auto-syncs your terminfo (Ghostty/kitty/…) so
|
|
447
|
+
# backspace, colors & clear work on the remote
|
|
435
448
|
agents hosts list # devices show up here too (one host pool)
|
|
436
449
|
agents hosts add mac-mini --cap gpu # tag a device for capability routing (--host gpu)
|
|
437
450
|
|
package/dist/bin/agents
CHANGED
|
Binary file
|
package/dist/commands/check.js
CHANGED
|
@@ -2,6 +2,11 @@ import chalk from 'chalk';
|
|
|
2
2
|
import { AGENTS, ALL_AGENT_IDS } from '../lib/agents.js';
|
|
3
3
|
import { setHelpSections } from '../lib/help.js';
|
|
4
4
|
import { computeDrift } from '../lib/drift.js';
|
|
5
|
+
import { loadDevices } from '../lib/devices/registry.js';
|
|
6
|
+
import { fanOutDevices, planFleetTargets, remoteFleetTargets } from '../lib/devices/fleet.js';
|
|
7
|
+
import { machineId } from '../lib/session/sync/config.js';
|
|
8
|
+
import { buildRemoteAgentsInvocation } from '../lib/hosts/remote-cmd.js';
|
|
9
|
+
import { sshExecAsync } from '../lib/ssh-exec.js';
|
|
5
10
|
const AGENT_NAMES = Object.fromEntries(ALL_AGENT_IDS.map((id) => [id, AGENTS[id].name]));
|
|
6
11
|
function label(row) {
|
|
7
12
|
return `${AGENT_NAMES[row.agent] || row.agent}@${row.version}`;
|
|
@@ -12,7 +17,8 @@ export function registerCheckCommand(program) {
|
|
|
12
17
|
.description('CI drift gate: exit non-zero when any installed version is out of sync (stale or never-synced), zero when clean.')
|
|
13
18
|
.option('--json', 'Output machine-readable JSON')
|
|
14
19
|
.option('-q, --quiet', 'Suppress per-version lines; print only the one-line verdict')
|
|
15
|
-
.option('--cwd <path>', 'Resolution cwd for project layer detection (default: process.cwd())')
|
|
20
|
+
.option('--cwd <path>', 'Resolution cwd for project layer detection (default: process.cwd())')
|
|
21
|
+
.option('--devices', 'Run the drift gate across every registered online device');
|
|
16
22
|
setHelpSections(checkCmd, {
|
|
17
23
|
examples: `
|
|
18
24
|
# Fail the build if anything drifted (exit 1), pass if clean (exit 0)
|
|
@@ -24,12 +30,19 @@ export function registerCheckCommand(program) {
|
|
|
24
30
|
# Machine-readable, for scripting
|
|
25
31
|
agents check --json
|
|
26
32
|
|
|
33
|
+
# Gate every registered device
|
|
34
|
+
agents check --devices
|
|
35
|
+
|
|
27
36
|
# Gate in a CI step
|
|
28
37
|
agents check || { echo "resources drifted — run 'agents doctor --fix'"; exit 1; }
|
|
29
38
|
`,
|
|
30
39
|
});
|
|
31
|
-
checkCmd.action((opts) => {
|
|
40
|
+
checkCmd.action(async (opts) => {
|
|
32
41
|
const cwd = opts.cwd ? opts.cwd : process.cwd();
|
|
42
|
+
if (opts.devices) {
|
|
43
|
+
await runDevicesCheck(opts, cwd);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
33
46
|
const drift = computeDrift(cwd);
|
|
34
47
|
if (opts.json) {
|
|
35
48
|
console.log(JSON.stringify({
|
|
@@ -82,3 +95,84 @@ export function registerCheckCommand(program) {
|
|
|
82
95
|
process.exit(1);
|
|
83
96
|
});
|
|
84
97
|
}
|
|
98
|
+
function checkPayload(device, drift) {
|
|
99
|
+
return {
|
|
100
|
+
device,
|
|
101
|
+
hasDrift: drift.hasDrift,
|
|
102
|
+
stale: drift.staleCount,
|
|
103
|
+
neverSynced: drift.neverSyncedCount,
|
|
104
|
+
orphanVersions: drift.orphanVersionCount,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
async function probeDeviceCheck(target) {
|
|
108
|
+
const isWin = /^win/i.test((target.platform ?? '').trim());
|
|
109
|
+
const remoteCmd = buildRemoteAgentsInvocation(['check', '--json'], undefined, isWin ? 'windows' : undefined, isWin ? undefined : { PATH: '$HOME/.agents/.cache/shims:$HOME/.local/bin:$PATH' });
|
|
110
|
+
const res = await sshExecAsync(target.name, remoteCmd, { timeoutMs: 30000, multiplex: true });
|
|
111
|
+
if (res.code !== 0 && !res.stdout.trim()) {
|
|
112
|
+
throw new Error(res.timedOut ? 'timed out' : (res.stderr.trim() || `exit ${res.code ?? 'unknown'}`));
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
const parsed = JSON.parse(res.stdout);
|
|
116
|
+
return {
|
|
117
|
+
device: target.name,
|
|
118
|
+
hasDrift: Boolean(parsed.hasDrift),
|
|
119
|
+
stale: parsed.stale ?? 0,
|
|
120
|
+
neverSynced: parsed.neverSynced ?? 0,
|
|
121
|
+
orphanVersions: parsed.orphanVersions ?? 0,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
throw new Error(`invalid JSON (${err?.message ?? 'parse error'})`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async function runDevicesCheck(opts, cwd) {
|
|
129
|
+
const registry = await loadDevices();
|
|
130
|
+
const self = machineId();
|
|
131
|
+
const planned = planFleetTargets(registry);
|
|
132
|
+
const local = checkPayload(self, computeDrift(cwd));
|
|
133
|
+
const remoteTargets = remoteFleetTargets(planned, self)
|
|
134
|
+
.map((t) => ({
|
|
135
|
+
name: t.device.name,
|
|
136
|
+
platform: t.device.platform,
|
|
137
|
+
skip: t.skip,
|
|
138
|
+
}));
|
|
139
|
+
const remote = await fanOutDevices(remoteTargets, probeDeviceCheck);
|
|
140
|
+
const devices = [local];
|
|
141
|
+
for (const result of remote) {
|
|
142
|
+
if (result.status === 'ok' && result.value) {
|
|
143
|
+
devices.push(result.value);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
devices.push({
|
|
147
|
+
device: result.name,
|
|
148
|
+
hasDrift: true,
|
|
149
|
+
stale: 0,
|
|
150
|
+
neverSynced: 0,
|
|
151
|
+
orphanVersions: 0,
|
|
152
|
+
error: result.error ?? String(result.reason ?? 'skipped'),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const hasDrift = devices.some((d) => d.hasDrift || d.error);
|
|
157
|
+
if (opts.json) {
|
|
158
|
+
console.log(JSON.stringify({ hasDrift, devices }, null, 2));
|
|
159
|
+
process.exit(hasDrift ? 1 : 0);
|
|
160
|
+
}
|
|
161
|
+
if (!hasDrift) {
|
|
162
|
+
console.log(chalk.green('ok') + chalk.gray(` ${devices.length} device(s) in sync`));
|
|
163
|
+
process.exit(0);
|
|
164
|
+
}
|
|
165
|
+
console.error(chalk.red('drift') + chalk.gray(` ${devices.filter((d) => d.hasDrift || d.error).length} of ${devices.length} device(s)`));
|
|
166
|
+
if (!opts.quiet) {
|
|
167
|
+
for (const d of devices) {
|
|
168
|
+
if (!d.hasDrift && !d.error)
|
|
169
|
+
continue;
|
|
170
|
+
const detail = d.error
|
|
171
|
+
? d.error
|
|
172
|
+
: [`${d.stale} stale`, `${d.neverSynced} never-synced`].filter((p) => !p.startsWith('0 ')).join(', ');
|
|
173
|
+
console.error(` ${chalk.yellow(d.device.padEnd(18))} ${detail || 'drift'}`);
|
|
174
|
+
}
|
|
175
|
+
console.error(chalk.gray('\nReconcile each device with `agents doctor --fix` or `agents repo pull user`.'));
|
|
176
|
+
}
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
@@ -38,5 +38,31 @@ export declare function reconcileScreenshotExt(outPath: string, buf: Buffer): {
|
|
|
38
38
|
export declare function registerComputerCommand(program: Command): void;
|
|
39
39
|
export declare function registerComputerSubcommands(program: Command): void;
|
|
40
40
|
export declare function buildRestartTaskScript(taskName: string, exeName: string): string;
|
|
41
|
+
/**
|
|
42
|
+
* Install the macOS helper locally: resolve (or download + verify) the signed,
|
|
43
|
+
* notarized .app, copy it to /Applications, verify the destination signature,
|
|
44
|
+
* and write the LaunchAgent plist (inactive — activation is a separate opt-in
|
|
45
|
+
* step via `start`). Throws on any failure. Shared by `agents computer setup`
|
|
46
|
+
* and the unified `agents setup computer` wizard so there is one install path.
|
|
47
|
+
*/
|
|
48
|
+
export declare function installComputerHelperMacLocal(): Promise<{
|
|
49
|
+
appDest: string;
|
|
50
|
+
plistPath: string;
|
|
51
|
+
}>;
|
|
52
|
+
/**
|
|
53
|
+
* Activate the local helper daemon via launchd (render policy + peers, bootout
|
|
54
|
+
* → bootstrap → kickstart, wait for the socket, probe AX trust). Throws on any
|
|
55
|
+
* failure. Returns the trust status so callers can guide the permission grant.
|
|
56
|
+
* Shared by `agents computer start` and the `agents setup computer` wizard.
|
|
57
|
+
*/
|
|
58
|
+
export declare function activateComputerHelperMacLocal(): Promise<{
|
|
59
|
+
trusted: boolean;
|
|
60
|
+
socketPath: string;
|
|
61
|
+
logPath: string;
|
|
62
|
+
}>;
|
|
63
|
+
/** Probe the running daemon's Accessibility trust status without re-activating.
|
|
64
|
+
* Returns false (never throws) if the socket is down or the RPC errors — used to
|
|
65
|
+
* poll while the user grants permissions in System Settings. */
|
|
66
|
+
export declare function probeComputerTrust(): Promise<boolean>;
|
|
41
67
|
export { resolveHelperExec as resolveHelperPath };
|
|
42
68
|
export { resolveSocketPath };
|
|
@@ -441,6 +441,172 @@ const HELPER_BUNDLE_ID = 'com.phnx-labs.computer-helper';
|
|
|
441
441
|
const HELPER_APP_NAME = 'Computer Helper.app';
|
|
442
442
|
const HELPER_APP_DEST = `/Applications/${HELPER_APP_NAME}`;
|
|
443
443
|
const HELPER_LABEL = HELPER_BUNDLE_ID;
|
|
444
|
+
/**
|
|
445
|
+
* Install the macOS helper locally: resolve (or download + verify) the signed,
|
|
446
|
+
* notarized .app, copy it to /Applications, verify the destination signature,
|
|
447
|
+
* and write the LaunchAgent plist (inactive — activation is a separate opt-in
|
|
448
|
+
* step via `start`). Throws on any failure. Shared by `agents computer setup`
|
|
449
|
+
* and the unified `agents setup computer` wizard so there is one install path.
|
|
450
|
+
*/
|
|
451
|
+
export async function installComputerHelperMacLocal() {
|
|
452
|
+
let srcApp = resolveHelperApp();
|
|
453
|
+
if (!srcApp || !fs.existsSync(srcApp)) {
|
|
454
|
+
// No local build / bundled copy (the normal case on an npm-installed CLI).
|
|
455
|
+
// Fetch the signed + notarized helper release asset for this CLI version;
|
|
456
|
+
// it is sha256- and signature-verified before we touch /Applications.
|
|
457
|
+
const { ensureMacHelperApp } = await import('../lib/computer/download.js');
|
|
458
|
+
srcApp = await ensureMacHelperApp();
|
|
459
|
+
console.log(`helper: ${srcApp} (downloaded + verified)`);
|
|
460
|
+
}
|
|
461
|
+
const socketPath = resolveSocketPath();
|
|
462
|
+
const logPath = resolveLogPath();
|
|
463
|
+
const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', `${HELPER_LABEL}.plist`);
|
|
464
|
+
console.log(`source: ${srcApp}`);
|
|
465
|
+
console.log(`dest: ${HELPER_APP_DEST}`);
|
|
466
|
+
// 1. Copy to /Applications/ via ditto (preserves xattrs + codesign metadata).
|
|
467
|
+
if (fs.existsSync(HELPER_APP_DEST)) {
|
|
468
|
+
try {
|
|
469
|
+
fs.rmSync(HELPER_APP_DEST, { recursive: true, force: true });
|
|
470
|
+
}
|
|
471
|
+
catch (err) {
|
|
472
|
+
throw new Error(`failed to remove prior install at ${HELPER_APP_DEST}: ${err.message}. try: sudo rm -rf "${HELPER_APP_DEST}"`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
try {
|
|
476
|
+
execFileSync('/usr/bin/ditto', [srcApp, HELPER_APP_DEST], { stdio: 'inherit' });
|
|
477
|
+
}
|
|
478
|
+
catch (err) {
|
|
479
|
+
throw new Error(`ditto copy failed: ${err.message}`);
|
|
480
|
+
}
|
|
481
|
+
console.log(`copied to ${HELPER_APP_DEST}`);
|
|
482
|
+
// 2. Verify codesign on the destination — TCC needs a valid signature.
|
|
483
|
+
try {
|
|
484
|
+
execFileSync('/usr/bin/codesign', ['--verify', '--deep', '--strict', HELPER_APP_DEST], { stdio: 'inherit' });
|
|
485
|
+
console.log('codesign verify: OK');
|
|
486
|
+
}
|
|
487
|
+
catch {
|
|
488
|
+
throw new Error(`codesign verify FAILED for ${HELPER_APP_DEST}. The destination .app is unsigned or its signature was stripped.`);
|
|
489
|
+
}
|
|
490
|
+
// 3. Ensure socket + log parent dirs exist.
|
|
491
|
+
fs.mkdirSync(path.dirname(socketPath), { recursive: true });
|
|
492
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
493
|
+
// 4. Write the LaunchAgent plist but DO NOT bootstrap it (opt-in via `start`).
|
|
494
|
+
const execInsideApp = path.join(HELPER_APP_DEST, 'Contents', 'MacOS', 'ComputerHelper');
|
|
495
|
+
const plistContent = renderLaunchAgentPlist({ label: HELPER_LABEL, exec: execInsideApp, socketPath, logPath });
|
|
496
|
+
fs.mkdirSync(path.dirname(plistPath), { recursive: true });
|
|
497
|
+
fs.writeFileSync(plistPath, plistContent);
|
|
498
|
+
console.log(`wrote plist: ${plistPath} (NOT activated)`);
|
|
499
|
+
return { appDest: HELPER_APP_DEST, plistPath };
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Activate the local helper daemon via launchd (render policy + peers, bootout
|
|
503
|
+
* → bootstrap → kickstart, wait for the socket, probe AX trust). Throws on any
|
|
504
|
+
* failure. Returns the trust status so callers can guide the permission grant.
|
|
505
|
+
* Shared by `agents computer start` and the `agents setup computer` wizard.
|
|
506
|
+
*/
|
|
507
|
+
export async function activateComputerHelperMacLocal() {
|
|
508
|
+
const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', `${HELPER_LABEL}.plist`);
|
|
509
|
+
const socketPath = resolveSocketPath();
|
|
510
|
+
const logPath = resolveLogPath();
|
|
511
|
+
if (!fs.existsSync(plistPath))
|
|
512
|
+
throw new Error(`plist not found at ${plistPath}. run: agents computer setup`);
|
|
513
|
+
if (!fs.existsSync(HELPER_APP_DEST))
|
|
514
|
+
throw new Error(`helper app not found at ${HELPER_APP_DEST}. run: agents computer setup`);
|
|
515
|
+
const uid = process.getuid?.();
|
|
516
|
+
if (typeof uid !== 'number')
|
|
517
|
+
throw new Error('cannot resolve uid');
|
|
518
|
+
const domain = `gui/${uid}`;
|
|
519
|
+
// Render the policy file BEFORE bootstrap so the daemon reads a fresh allow
|
|
520
|
+
// list at startup (fail-safe: missing/unparseable → everything denied).
|
|
521
|
+
const allowed = loadComputerAllowList();
|
|
522
|
+
writeComputerPolicy(allowed);
|
|
523
|
+
console.log(`policy: ${allowed.length} app${allowed.length === 1 ? '' : 's'} allowed (${resolvePolicyPath()})`);
|
|
524
|
+
if (allowed.length > 0) {
|
|
525
|
+
const preview = allowed.slice(0, 5).join(', ');
|
|
526
|
+
const more = allowed.length > 5 ? ` (+${allowed.length - 5} more)` : '';
|
|
527
|
+
console.log(` ${preview}${more}`);
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
console.log(` (no Computer(...) patterns found — everything will be denied)`);
|
|
531
|
+
console.log(` add to ~/.agents/permissions/groups/<name>.yaml under allow:`);
|
|
532
|
+
console.log(` - "Computer(com.apple.finder)"`);
|
|
533
|
+
}
|
|
534
|
+
// Peer-auth allow list — which caller executables may connect to the socket.
|
|
535
|
+
const callers = loadDefaultPeers();
|
|
536
|
+
writeComputerPeers(callers);
|
|
537
|
+
console.log(`peers: ${callers.length} caller${callers.length === 1 ? '' : 's'} allowed (${resolvePeersPath()})`);
|
|
538
|
+
for (const p of callers)
|
|
539
|
+
console.log(` ${p}`);
|
|
540
|
+
// Bootout first to clear any prior registration (best-effort).
|
|
541
|
+
try {
|
|
542
|
+
execFileSync('/bin/launchctl', ['bootout', domain, plistPath], { stdio: 'pipe' });
|
|
543
|
+
}
|
|
544
|
+
catch {
|
|
545
|
+
// expected when not previously loaded
|
|
546
|
+
}
|
|
547
|
+
try {
|
|
548
|
+
execFileSync('/bin/launchctl', ['bootstrap', domain, plistPath], { stdio: 'pipe' });
|
|
549
|
+
}
|
|
550
|
+
catch (err) {
|
|
551
|
+
throw new Error(`launchctl bootstrap failed: ${err.message}`);
|
|
552
|
+
}
|
|
553
|
+
// Force restart so we pick up the latest binary.
|
|
554
|
+
try {
|
|
555
|
+
execFileSync('/bin/launchctl', ['kickstart', '-k', `${domain}/${HELPER_LABEL}`], { stdio: 'pipe' });
|
|
556
|
+
}
|
|
557
|
+
catch (err) {
|
|
558
|
+
throw new Error(`launchctl kickstart failed: ${err.message}`);
|
|
559
|
+
}
|
|
560
|
+
// Wait up to 5s for the socket.
|
|
561
|
+
const deadline = Date.now() + 5000;
|
|
562
|
+
while (Date.now() < deadline) {
|
|
563
|
+
if (fs.existsSync(socketPath))
|
|
564
|
+
break;
|
|
565
|
+
await sleep(100);
|
|
566
|
+
}
|
|
567
|
+
if (!fs.existsSync(socketPath)) {
|
|
568
|
+
throw new Error(`socket did not appear at ${socketPath} within 5s. check ${logPath} for helper startup errors`);
|
|
569
|
+
}
|
|
570
|
+
// Probe trust through the socket.
|
|
571
|
+
let trusted = false;
|
|
572
|
+
let trustStr = 'unknown';
|
|
573
|
+
try {
|
|
574
|
+
const client = openComputerClient();
|
|
575
|
+
try {
|
|
576
|
+
const r = await client.call('trust_status');
|
|
577
|
+
trusted = Boolean(r.result?.trusted);
|
|
578
|
+
trustStr = r.error ? `error (${r.error.code})` : (trusted ? 'granted' : 'denied');
|
|
579
|
+
}
|
|
580
|
+
finally {
|
|
581
|
+
await client.close();
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
catch (err) {
|
|
585
|
+
trustStr = `error (${err.message})`;
|
|
586
|
+
}
|
|
587
|
+
console.log(`daemon: running`);
|
|
588
|
+
console.log(`socket: ${socketPath}`);
|
|
589
|
+
console.log(`trust: ${trustStr}`);
|
|
590
|
+
return { trusted, socketPath, logPath };
|
|
591
|
+
}
|
|
592
|
+
/** Probe the running daemon's Accessibility trust status without re-activating.
|
|
593
|
+
* Returns false (never throws) if the socket is down or the RPC errors — used to
|
|
594
|
+
* poll while the user grants permissions in System Settings. */
|
|
595
|
+
export async function probeComputerTrust() {
|
|
596
|
+
try {
|
|
597
|
+
const client = openComputerClient();
|
|
598
|
+
try {
|
|
599
|
+
const r = await client.call('trust_status');
|
|
600
|
+
return Boolean(r.result?.trusted);
|
|
601
|
+
}
|
|
602
|
+
finally {
|
|
603
|
+
await client.close();
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
catch {
|
|
607
|
+
return false;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
444
610
|
function registerSetupCommand(program) {
|
|
445
611
|
program
|
|
446
612
|
.command('setup')
|
|
@@ -462,66 +628,14 @@ function registerSetupCommand(program) {
|
|
|
462
628
|
}
|
|
463
629
|
return;
|
|
464
630
|
}
|
|
465
|
-
|
|
466
|
-
if (!srcApp || !fs.existsSync(srcApp)) {
|
|
467
|
-
console.error('helper not built. Run: ./native/computer-mac/scripts/build.sh debug');
|
|
468
|
-
process.exit(1);
|
|
469
|
-
}
|
|
470
|
-
const home = os.homedir();
|
|
471
|
-
const socketPath = resolveSocketPath();
|
|
472
|
-
const logPath = resolveLogPath();
|
|
473
|
-
const plistPath = path.join(home, 'Library', 'LaunchAgents', `${HELPER_LABEL}.plist`);
|
|
474
|
-
console.log(`source: ${srcApp}`);
|
|
475
|
-
console.log(`dest: ${HELPER_APP_DEST}`);
|
|
476
|
-
// 1. Copy to /Applications/. Use ditto to preserve xattrs (Gatekeeper
|
|
477
|
-
// provenance + codesign metadata). Wipe any prior install first.
|
|
478
|
-
if (fs.existsSync(HELPER_APP_DEST)) {
|
|
479
|
-
try {
|
|
480
|
-
fs.rmSync(HELPER_APP_DEST, { recursive: true, force: true });
|
|
481
|
-
console.log(`removed prior install`);
|
|
482
|
-
}
|
|
483
|
-
catch (err) {
|
|
484
|
-
console.error(`failed to remove prior install at ${HELPER_APP_DEST}: ${err.message}`);
|
|
485
|
-
console.error('try: sudo rm -rf "' + HELPER_APP_DEST + '"');
|
|
486
|
-
process.exit(1);
|
|
487
|
-
}
|
|
488
|
-
}
|
|
631
|
+
let plistPath;
|
|
489
632
|
try {
|
|
490
|
-
|
|
633
|
+
({ plistPath } = await installComputerHelperMacLocal());
|
|
491
634
|
}
|
|
492
635
|
catch (err) {
|
|
493
|
-
console.error(`
|
|
494
|
-
process.exit(1);
|
|
495
|
-
}
|
|
496
|
-
console.log(`copied to ${HELPER_APP_DEST}`);
|
|
497
|
-
// 2. Verify codesign on the destination. Fail loud if the copy
|
|
498
|
-
// somehow stripped the signature — TCC needs a valid signature.
|
|
499
|
-
try {
|
|
500
|
-
execFileSync('/usr/bin/codesign', ['--verify', '--deep', '--strict', HELPER_APP_DEST], { stdio: 'inherit' });
|
|
501
|
-
console.log('codesign verify: OK');
|
|
502
|
-
}
|
|
503
|
-
catch {
|
|
504
|
-
console.error('codesign verify FAILED. The destination .app is unsigned or its signature was stripped.');
|
|
505
|
-
console.error('rebuild the helper with a Developer ID cert: ./native/computer-mac/scripts/build.sh release');
|
|
636
|
+
console.error(`error: ${err.message}`);
|
|
506
637
|
process.exit(1);
|
|
507
638
|
}
|
|
508
|
-
// 3. Ensure socket + log parent dirs exist.
|
|
509
|
-
fs.mkdirSync(path.dirname(socketPath), { recursive: true });
|
|
510
|
-
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
511
|
-
// 4. Write the LaunchAgent plist but DO NOT bootstrap it. The user
|
|
512
|
-
// explicitly opts into running the daemon via `agents computer start`.
|
|
513
|
-
// Screen Recording + Accessibility are scary permissions; we don't
|
|
514
|
-
// want an always-on listener that can drive any app the user could.
|
|
515
|
-
const execInsideApp = path.join(HELPER_APP_DEST, 'Contents', 'MacOS', 'ComputerHelper');
|
|
516
|
-
const plistContent = renderLaunchAgentPlist({
|
|
517
|
-
label: HELPER_LABEL,
|
|
518
|
-
exec: execInsideApp,
|
|
519
|
-
socketPath,
|
|
520
|
-
logPath,
|
|
521
|
-
});
|
|
522
|
-
fs.mkdirSync(path.dirname(plistPath), { recursive: true });
|
|
523
|
-
fs.writeFileSync(plistPath, plistContent);
|
|
524
|
-
console.log(`wrote plist: ${plistPath} (NOT activated)`);
|
|
525
639
|
console.log('');
|
|
526
640
|
console.log('Helper installed (inactive).');
|
|
527
641
|
console.log('');
|
|
@@ -563,105 +677,15 @@ function registerStartCommand(program) {
|
|
|
563
677
|
}
|
|
564
678
|
return;
|
|
565
679
|
}
|
|
566
|
-
|
|
567
|
-
const plistPath = path.join(home, 'Library', 'LaunchAgents', `${HELPER_LABEL}.plist`);
|
|
568
|
-
const socketPath = resolveSocketPath();
|
|
569
|
-
const logPath = resolveLogPath();
|
|
570
|
-
if (!fs.existsSync(plistPath)) {
|
|
571
|
-
console.error(`plist not found at ${plistPath}`);
|
|
572
|
-
console.error('run: agents computer setup');
|
|
573
|
-
process.exit(1);
|
|
574
|
-
}
|
|
575
|
-
if (!fs.existsSync(HELPER_APP_DEST)) {
|
|
576
|
-
console.error(`helper app not found at ${HELPER_APP_DEST}`);
|
|
577
|
-
console.error('run: agents computer setup');
|
|
578
|
-
process.exit(1);
|
|
579
|
-
}
|
|
580
|
-
const uid = process.getuid?.();
|
|
581
|
-
if (typeof uid !== 'number') {
|
|
582
|
-
console.error('cannot resolve uid');
|
|
583
|
-
process.exit(1);
|
|
584
|
-
}
|
|
585
|
-
const domain = `gui/${uid}`;
|
|
586
|
-
// Render the policy file BEFORE launchctl bootstrap so the daemon
|
|
587
|
-
// reads a fresh allow list at startup. The helper falls back to an
|
|
588
|
-
// empty allow list (everything denied) if this file is missing or
|
|
589
|
-
// unparseable — fail-safe.
|
|
590
|
-
const allowed = loadComputerAllowList();
|
|
591
|
-
writeComputerPolicy(allowed);
|
|
592
|
-
console.log(`policy: ${allowed.length} app${allowed.length === 1 ? '' : 's'} allowed (${resolvePolicyPath()})`);
|
|
593
|
-
if (allowed.length > 0) {
|
|
594
|
-
const preview = allowed.slice(0, 5).join(', ');
|
|
595
|
-
const more = allowed.length > 5 ? ` (+${allowed.length - 5} more)` : '';
|
|
596
|
-
console.log(` ${preview}${more}`);
|
|
597
|
-
}
|
|
598
|
-
else {
|
|
599
|
-
console.log(` (no Computer(...) patterns found — everything will be denied)`);
|
|
600
|
-
console.log(` add to ~/.agents/permissions/groups/<name>.yaml under allow:`);
|
|
601
|
-
console.log(` - "Computer(com.apple.finder)"`);
|
|
602
|
-
}
|
|
603
|
-
// Peer-auth allow list — which caller executables may connect to
|
|
604
|
-
// the socket. Default: this CLI's Node binary, plus Rush.app if
|
|
605
|
-
// installed. A `nc -U socket` from a malicious npm postinstall has
|
|
606
|
-
// a different exec path and gets refused at accept().
|
|
607
|
-
const callers = loadDefaultPeers();
|
|
608
|
-
writeComputerPeers(callers);
|
|
609
|
-
console.log(`peers: ${callers.length} caller${callers.length === 1 ? '' : 's'} allowed (${resolvePeersPath()})`);
|
|
610
|
-
for (const p of callers)
|
|
611
|
-
console.log(` ${p}`);
|
|
612
|
-
// Bootout first to clear any prior registration. Best-effort.
|
|
613
|
-
try {
|
|
614
|
-
execFileSync('/bin/launchctl', ['bootout', domain, plistPath], { stdio: 'pipe' });
|
|
615
|
-
}
|
|
616
|
-
catch {
|
|
617
|
-
// expected when not previously loaded
|
|
618
|
-
}
|
|
619
|
-
try {
|
|
620
|
-
execFileSync('/bin/launchctl', ['bootstrap', domain, plistPath], { stdio: 'pipe' });
|
|
621
|
-
}
|
|
622
|
-
catch (err) {
|
|
623
|
-
console.error(`launchctl bootstrap failed: ${err.message}`);
|
|
624
|
-
process.exit(1);
|
|
625
|
-
}
|
|
626
|
-
// Force restart so we pick up the latest binary.
|
|
680
|
+
let trusted;
|
|
627
681
|
try {
|
|
628
|
-
|
|
682
|
+
({ trusted } = await activateComputerHelperMacLocal());
|
|
629
683
|
}
|
|
630
684
|
catch (err) {
|
|
631
|
-
console.error(`
|
|
685
|
+
console.error(`error: ${err.message}`);
|
|
632
686
|
process.exit(1);
|
|
633
687
|
}
|
|
634
|
-
|
|
635
|
-
const deadline = Date.now() + 5000;
|
|
636
|
-
while (Date.now() < deadline) {
|
|
637
|
-
if (fs.existsSync(socketPath))
|
|
638
|
-
break;
|
|
639
|
-
await sleep(100);
|
|
640
|
-
}
|
|
641
|
-
if (!fs.existsSync(socketPath)) {
|
|
642
|
-
console.error(`socket did not appear at ${socketPath} within 5s`);
|
|
643
|
-
console.error(`check ${logPath} for helper startup errors`);
|
|
644
|
-
process.exit(1);
|
|
645
|
-
}
|
|
646
|
-
// Probe trust through the socket.
|
|
647
|
-
let trustStr = 'unknown';
|
|
648
|
-
try {
|
|
649
|
-
const client = openComputerClient();
|
|
650
|
-
try {
|
|
651
|
-
const r = await client.call('trust_status');
|
|
652
|
-
trustStr = r.error ? `error (${r.error.code})` : (r.result?.trusted ? 'granted' : 'denied');
|
|
653
|
-
}
|
|
654
|
-
finally {
|
|
655
|
-
await client.close();
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
catch (err) {
|
|
659
|
-
trustStr = `error (${err.message})`;
|
|
660
|
-
}
|
|
661
|
-
console.log(`daemon: running`);
|
|
662
|
-
console.log(`socket: ${socketPath}`);
|
|
663
|
-
console.log(`trust: ${trustStr}`);
|
|
664
|
-
if (trustStr === 'denied') {
|
|
688
|
+
if (!trusted) {
|
|
665
689
|
console.log('');
|
|
666
690
|
console.log('Grant Accessibility + Screen Recording to Computer Helper.app, then run `agents computer start` again.');
|
|
667
691
|
}
|