@phnx-labs/agents-cli 1.20.34 → 1.20.36
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 +113 -0
- package/LICENSE +185 -21
- package/README.md +13 -5
- package/dist/commands/audit.d.ts +14 -0
- package/dist/commands/audit.js +68 -0
- package/dist/commands/browser.js +82 -8
- package/dist/commands/check.d.ts +15 -0
- package/dist/commands/check.js +84 -0
- package/dist/commands/cloud.js +143 -3
- package/dist/commands/computer-actions.d.ts +15 -0
- package/dist/commands/computer-actions.js +120 -22
- package/dist/commands/computer.js +81 -0
- package/dist/commands/cost.js +17 -12
- package/dist/commands/daemon.js +4 -1
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.js +54 -97
- package/dist/commands/events.d.ts +16 -0
- package/dist/commands/events.js +173 -0
- package/dist/commands/exec.d.ts +19 -0
- package/dist/commands/exec.js +283 -12
- package/dist/commands/hosts.js +37 -29
- package/dist/commands/inspect.d.ts +2 -0
- package/dist/commands/inspect.js +62 -16
- package/dist/commands/lock.d.ts +12 -0
- package/dist/commands/lock.js +70 -0
- package/dist/commands/logs.d.ts +17 -0
- package/dist/commands/logs.js +139 -0
- package/dist/commands/message.d.ts +15 -0
- package/dist/commands/message.js +56 -0
- package/dist/commands/models.d.ts +3 -0
- package/dist/commands/models.js +27 -8
- package/dist/commands/permissions.js +9 -2
- package/dist/commands/repo.d.ts +34 -0
- package/dist/commands/repo.js +243 -65
- package/dist/commands/resource-view.d.ts +20 -0
- package/dist/commands/resource-view.js +90 -28
- package/dist/commands/routines.js +101 -5
- package/dist/commands/secrets-migrate.js +106 -57
- package/dist/commands/secrets.d.ts +31 -18
- package/dist/commands/secrets.js +196 -91
- package/dist/commands/serve.d.ts +10 -0
- package/dist/commands/serve.js +37 -0
- package/dist/commands/sessions-inject.d.ts +14 -0
- package/dist/commands/sessions-inject.js +111 -0
- package/dist/commands/sessions-picker.d.ts +2 -0
- package/dist/commands/sessions-picker.js +24 -3
- package/dist/commands/sessions-resume.d.ts +2 -0
- package/dist/commands/sessions-resume.js +221 -0
- package/dist/commands/sessions-tail.d.ts +10 -0
- package/dist/commands/sessions-tail.js +11 -0
- package/dist/commands/sessions.d.ts +143 -1
- package/dist/commands/sessions.js +628 -80
- package/dist/commands/setup.js +4 -2
- package/dist/commands/sync.d.ts +3 -1
- package/dist/commands/sync.js +156 -4
- package/dist/commands/teams.js +217 -0
- package/dist/commands/versions.js +2 -4
- package/dist/commands/view.d.ts +3 -0
- package/dist/commands/view.js +19 -8
- package/dist/commands/wallet.d.ts +6 -0
- package/dist/commands/wallet.js +22 -5
- package/dist/commands/watchdog.d.ts +18 -0
- package/dist/commands/watchdog.js +238 -0
- package/dist/index.js +82 -32
- package/dist/lib/agents.js +30 -9
- package/dist/lib/audit/log.d.ts +92 -0
- package/dist/lib/audit/log.js +177 -0
- package/dist/lib/auto-pull.js +2 -1
- package/dist/lib/browser/cdp.js +5 -1
- package/dist/lib/browser/chrome.d.ts +10 -0
- package/dist/lib/browser/chrome.js +18 -7
- package/dist/lib/browser/drivers/ssh.d.ts +8 -0
- package/dist/lib/browser/drivers/ssh.js +77 -16
- package/dist/lib/browser/har.d.ts +84 -0
- package/dist/lib/browser/har.js +77 -0
- package/dist/lib/browser/ipc.d.ts +3 -0
- package/dist/lib/browser/ipc.js +37 -12
- package/dist/lib/browser/profiles.d.ts +1 -1
- package/dist/lib/browser/profiles.js +8 -10
- package/dist/lib/browser/refs.d.ts +65 -0
- package/dist/lib/browser/refs.js +73 -1
- package/dist/lib/browser/runtime-state.js +1 -0
- package/dist/lib/browser/service.d.ts +38 -2
- package/dist/lib/browser/service.js +183 -23
- package/dist/lib/browser/types.d.ts +14 -1
- package/dist/lib/budget/live-cloud.d.ts +42 -0
- package/dist/lib/budget/live-cloud.js +79 -0
- package/dist/lib/budget/live-team.d.ts +31 -0
- package/dist/lib/budget/live-team.js +115 -0
- package/dist/lib/cloud/codex.js +4 -0
- package/dist/lib/cloud/rush.d.ts +12 -1
- package/dist/lib/cloud/rush.js +13 -3
- package/dist/lib/cloud/types.d.ts +9 -0
- package/dist/lib/computer/dispatch.d.ts +8 -0
- package/dist/lib/computer/dispatch.js +125 -0
- package/dist/lib/computer/loop.d.ts +62 -0
- package/dist/lib/computer/loop.js +98 -0
- package/dist/lib/computer/model.d.ts +44 -0
- package/dist/lib/computer/model.js +157 -0
- package/dist/lib/concurrency.d.ts +19 -0
- package/dist/lib/concurrency.js +33 -0
- package/dist/lib/daemon.d.ts +92 -0
- package/dist/lib/daemon.js +222 -18
- package/dist/lib/devices/connect.d.ts +3 -2
- package/dist/lib/devices/connect.js +5 -3
- package/dist/lib/devices/registry.d.ts +7 -0
- package/dist/lib/devices/registry.js +24 -0
- package/dist/lib/devices/tailscale.js +1 -1
- package/dist/lib/drift.d.ts +52 -0
- package/dist/lib/drift.js +112 -0
- package/dist/lib/events.d.ts +10 -3
- package/dist/lib/events.js +101 -24
- package/dist/lib/exec.d.ts +17 -0
- package/dist/lib/exec.js +100 -12
- package/dist/lib/git.d.ts +27 -0
- package/dist/lib/git.js +56 -1
- package/dist/lib/hooks/cache.d.ts +6 -0
- package/dist/lib/hooks/cache.js +54 -12
- package/dist/lib/hooks.d.ts +27 -0
- package/dist/lib/hooks.js +135 -10
- package/dist/lib/hosts/dispatch.d.ts +15 -0
- package/dist/lib/hosts/dispatch.js +45 -13
- package/dist/lib/hosts/logs.d.ts +16 -0
- package/dist/lib/hosts/logs.js +74 -0
- package/dist/lib/hosts/option.js +1 -1
- package/dist/lib/hosts/passthrough.js +3 -1
- package/dist/lib/hosts/progress.d.ts +66 -0
- package/dist/lib/hosts/progress.js +125 -17
- package/dist/lib/hosts/ready.d.ts +51 -7
- package/dist/lib/hosts/ready.js +98 -25
- package/dist/lib/hosts/reconcile.d.ts +53 -0
- package/dist/lib/hosts/reconcile.js +81 -0
- package/dist/lib/hosts/registry.d.ts +19 -2
- package/dist/lib/hosts/registry.js +58 -2
- package/dist/lib/hosts/remote-cmd.d.ts +62 -1
- package/dist/lib/hosts/remote-cmd.js +70 -1
- package/dist/lib/hosts/remote-os.d.ts +17 -0
- package/dist/lib/hosts/remote-os.js +30 -0
- package/dist/lib/hosts/session-index.d.ts +34 -0
- package/dist/lib/hosts/session-index.js +56 -0
- package/dist/lib/hosts/tasks.d.ts +22 -0
- package/dist/lib/hosts/tasks.js +29 -0
- package/dist/lib/lock.d.ts +93 -0
- package/dist/lib/lock.js +207 -0
- package/dist/lib/loop.js +16 -1
- package/dist/lib/machine-id.d.ts +21 -0
- package/dist/lib/machine-id.js +26 -0
- package/dist/lib/mailbox-target.d.ts +36 -0
- package/dist/lib/mailbox-target.js +45 -0
- package/dist/lib/mailbox.d.ts +47 -0
- package/dist/lib/mailbox.js +194 -0
- package/dist/lib/mcp.d.ts +5 -0
- package/dist/lib/mcp.js +24 -8
- package/dist/lib/migrate.d.ts +19 -0
- package/dist/lib/migrate.js +134 -26
- package/dist/lib/overdue.js +3 -0
- package/dist/lib/permissions.d.ts +4 -0
- package/dist/lib/permissions.js +35 -0
- package/dist/lib/picker.d.ts +26 -0
- package/dist/lib/picker.js +127 -0
- package/dist/lib/platform/exec.d.ts +46 -0
- package/dist/lib/platform/exec.js +74 -0
- package/dist/lib/platform/process.d.ts +31 -0
- package/dist/lib/platform/process.js +34 -1
- package/dist/lib/platform/winpath.js +2 -0
- package/dist/lib/plugin-marketplace.d.ts +30 -0
- package/dist/lib/plugin-marketplace.js +215 -2
- package/dist/lib/plugins.d.ts +5 -0
- package/dist/lib/plugins.js +61 -10
- package/dist/lib/profiles.d.ts +25 -0
- package/dist/lib/profiles.js +22 -6
- package/dist/lib/pty-client.js +2 -1
- package/dist/lib/rotate.d.ts +61 -0
- package/dist/lib/rotate.js +52 -0
- package/dist/lib/routines.d.ts +40 -2
- package/dist/lib/routines.js +66 -8
- package/dist/lib/runner.d.ts +11 -2
- package/dist/lib/runner.js +49 -7
- package/dist/lib/scheduler.js +6 -1
- package/dist/lib/secrets/bundles.d.ts +60 -4
- package/dist/lib/secrets/bundles.js +131 -12
- package/dist/lib/secrets/filestore.d.ts +3 -0
- package/dist/lib/secrets/filestore.js +42 -16
- package/dist/lib/secrets/index.d.ts +43 -2
- package/dist/lib/secrets/index.js +102 -3
- package/dist/lib/secrets/mcp.d.ts +93 -0
- package/dist/lib/secrets/mcp.js +205 -0
- package/dist/lib/secrets/remote.js +12 -5
- package/dist/lib/secrets/sync.js +83 -4
- package/dist/lib/secrets/windows.js +14 -3
- package/dist/lib/serve/data.d.ts +81 -0
- package/dist/lib/serve/data.js +91 -0
- package/dist/lib/serve/page.d.ts +7 -0
- package/dist/lib/serve/page.js +140 -0
- package/dist/lib/serve/server.d.ts +46 -0
- package/dist/lib/serve/server.js +115 -0
- package/dist/lib/session/active.d.ts +61 -0
- package/dist/lib/session/active.js +196 -17
- package/dist/lib/session/discover.d.ts +37 -0
- package/dist/lib/session/discover.js +204 -27
- package/dist/lib/session/inject.d.ts +18 -0
- package/dist/lib/session/inject.js +21 -0
- package/dist/lib/session/parse.d.ts +6 -0
- package/dist/lib/session/parse.js +286 -2
- package/dist/lib/session/pid-registry.d.ts +22 -0
- package/dist/lib/session/pid-registry.js +104 -0
- package/dist/lib/session/provenance.d.ts +14 -2
- package/dist/lib/session/provenance.js +39 -8
- package/dist/lib/session/remote-active.d.ts +26 -0
- package/dist/lib/session/remote-active.js +153 -0
- package/dist/lib/session/remote-list.d.ts +51 -0
- package/dist/lib/session/remote-list.js +213 -0
- package/dist/lib/session/remote.d.ts +7 -1
- package/dist/lib/session/remote.js +18 -8
- package/dist/lib/session/render.js +1 -1
- package/dist/lib/session/sync/config.d.ts +1 -7
- package/dist/lib/session/sync/config.js +4 -11
- package/dist/lib/session/types.d.ts +18 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/shims.d.ts +44 -6
- package/dist/lib/shims.js +143 -57
- package/dist/lib/ssh-exec.d.ts +26 -3
- package/dist/lib/ssh-exec.js +47 -3
- package/dist/lib/ssh-tunnel.d.ts +24 -5
- package/dist/lib/ssh-tunnel.js +62 -63
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/startup/command-registry.d.ts +8 -0
- package/dist/lib/startup/command-registry.js +17 -1
- package/dist/lib/state.d.ts +13 -0
- package/dist/lib/state.js +103 -9
- package/dist/lib/sync-umbrella.d.ts +14 -7
- package/dist/lib/sync-umbrella.js +17 -9
- package/dist/lib/teams/forEach.d.ts +110 -0
- package/dist/lib/teams/forEach.js +186 -0
- package/dist/lib/teams/index.d.ts +1 -0
- package/dist/lib/teams/index.js +1 -0
- package/dist/lib/teams/pr-watch.d.ts +226 -0
- package/dist/lib/teams/pr-watch.js +371 -0
- package/dist/lib/teams/registry.js +25 -9
- package/dist/lib/teams/supervisor.d.ts +14 -1
- package/dist/lib/teams/supervisor.js +19 -0
- package/dist/lib/teams/worktree.d.ts +9 -0
- package/dist/lib/teams/worktree.js +32 -0
- package/dist/lib/terminal/backends/ghostty.d.ts +6 -0
- package/dist/lib/terminal/backends/ghostty.js +69 -0
- package/dist/lib/terminal/backends/index.d.ts +19 -0
- package/dist/lib/terminal/backends/index.js +31 -0
- package/dist/lib/terminal/backends/iterm.d.ts +6 -0
- package/dist/lib/terminal/backends/iterm.js +62 -0
- package/dist/lib/terminal/backends/tmux.d.ts +14 -0
- package/dist/lib/terminal/backends/tmux.js +23 -0
- package/dist/lib/terminal/backends/vscodium-agent.d.ts +31 -0
- package/dist/lib/terminal/backends/vscodium-agent.js +72 -0
- package/dist/lib/terminal/engine.d.ts +39 -0
- package/dist/lib/terminal/engine.js +54 -0
- package/dist/lib/terminal/index.d.ts +17 -0
- package/dist/lib/terminal/index.js +8 -0
- package/dist/lib/terminal/inject.d.ts +204 -0
- package/dist/lib/terminal/inject.js +247 -0
- package/dist/lib/terminal/policy.d.ts +11 -0
- package/dist/lib/terminal/policy.js +11 -0
- package/dist/lib/terminal/quote.d.ts +11 -0
- package/dist/lib/terminal/quote.js +13 -0
- package/dist/lib/terminal/resolve.d.ts +64 -0
- package/dist/lib/terminal/resolve.js +90 -0
- package/dist/lib/terminal/shell.d.ts +6 -0
- package/dist/lib/terminal/shell.js +23 -0
- package/dist/lib/terminal/transport.d.ts +15 -0
- package/dist/lib/terminal/transport.js +40 -0
- package/dist/lib/terminal/types.d.ts +59 -0
- package/dist/lib/terminal/types.js +13 -0
- package/dist/lib/triggers/webhook.d.ts +85 -0
- package/dist/lib/triggers/webhook.js +141 -0
- package/dist/lib/types.d.ts +7 -0
- package/dist/lib/versions.d.ts +30 -3
- package/dist/lib/versions.js +129 -18
- package/dist/lib/watchdog/index.d.ts +3 -0
- package/dist/lib/watchdog/index.js +5 -0
- package/dist/lib/watchdog/read.d.ts +35 -0
- package/dist/lib/watchdog/read.js +149 -0
- package/dist/lib/watchdog/runner.d.ts +127 -0
- package/dist/lib/watchdog/runner.js +322 -0
- package/dist/lib/watchdog/watchdog.d.ts +40 -0
- package/dist/lib/watchdog/watchdog.js +166 -0
- package/dist/lib/watchdog/watchdogTail.d.ts +5 -0
- package/dist/lib/watchdog/watchdogTail.js +154 -0
- package/dist/lib/whats-new.d.ts +9 -0
- package/dist/lib/whats-new.js +35 -0
- package/dist/lib/workflows.d.ts +166 -0
- package/dist/lib/workflows.js +193 -0
- package/package.json +5 -4
package/dist/lib/ssh-tunnel.js
CHANGED
|
@@ -22,27 +22,26 @@ import * as fs from 'fs';
|
|
|
22
22
|
import * as path from 'path';
|
|
23
23
|
import { fileURLToPath } from 'url';
|
|
24
24
|
import { randomBytes } from 'crypto';
|
|
25
|
-
import { Transform } from 'stream';
|
|
26
25
|
import { sshExec, SSH_OPTS } from './ssh-exec.js';
|
|
26
|
+
import { backgroundSpawnOptions } from './platform/process.js';
|
|
27
27
|
import { encodePowerShell } from './browser/drivers/ssh.js';
|
|
28
28
|
import { getDevice } from './devices/registry.js';
|
|
29
29
|
import { sshTargetFor } from './devices/connect.js';
|
|
30
30
|
import { hostNameFor } from './devices/ssh-config.js';
|
|
31
31
|
import { getCacheDir } from './state.js';
|
|
32
32
|
import { openComputerClient, resolveTcpEndpoint } from './computer-rpc.js';
|
|
33
|
-
/** Build the ssh argv (after the `ssh` program name) for an `-L` tunnel. Pure.
|
|
33
|
+
/** Build the ssh argv (after the `ssh` program name) for an `-L` tunnel. Pure.
|
|
34
|
+
*
|
|
35
|
+
* Composes the shared hardened baseline (`SSH_OPTS`) rather than re-listing it,
|
|
36
|
+
* so the tunnel inherits the same options — crucially the keepalive, which lets
|
|
37
|
+
* a dropped `-N` tunnel exit instead of lingering as a zombie on the laptop. */
|
|
34
38
|
export function buildTunnelArgs(user, host, localPort, remotePort) {
|
|
35
39
|
return [
|
|
36
40
|
'-L',
|
|
37
41
|
`${localPort}:127.0.0.1:${remotePort}`,
|
|
38
42
|
`${user}@${host}`,
|
|
39
43
|
'-N',
|
|
40
|
-
|
|
41
|
-
'StrictHostKeyChecking=accept-new',
|
|
42
|
-
'-o',
|
|
43
|
-
'BatchMode=yes',
|
|
44
|
-
'-o',
|
|
45
|
-
'ConnectTimeout=10',
|
|
44
|
+
...SSH_OPTS,
|
|
46
45
|
];
|
|
47
46
|
}
|
|
48
47
|
/**
|
|
@@ -58,7 +57,7 @@ export function startSSHTunnel(user, host, localPort, remotePort, opts = {}) {
|
|
|
58
57
|
const args = buildTunnelArgs(user, host, localPort, remotePort);
|
|
59
58
|
const tunnel = spawn('ssh', args, {
|
|
60
59
|
stdio: opts.detached ? 'ignore' : ['ignore', 'ignore', 'pipe'],
|
|
61
|
-
|
|
60
|
+
...(opts.detached ? backgroundSpawnOptions() : { detached: false, windowsHide: true }),
|
|
62
61
|
});
|
|
63
62
|
let stderr = '';
|
|
64
63
|
tunnel.stderr?.on('data', (data) => {
|
|
@@ -150,10 +149,15 @@ export async function resolveRemoteDevice(name) {
|
|
|
150
149
|
return { device, target, user, host };
|
|
151
150
|
}
|
|
152
151
|
/**
|
|
153
|
-
*
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
152
|
+
* Single-quote a string for embedding inside a PowerShell literal.
|
|
153
|
+
*/
|
|
154
|
+
function psSingleQuote(s) {
|
|
155
|
+
return "'" + s.replace(/'/g, "''") + "'";
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* PowerShell that resolves the destination under %LOCALAPPDATA%\agents and
|
|
159
|
+
* stops any running instance first so the file is not locked. The caller copies
|
|
160
|
+
* the exe with scp and then verifies the byte count separately.
|
|
157
161
|
*/
|
|
158
162
|
export function buildPushScript() {
|
|
159
163
|
return [
|
|
@@ -161,15 +165,18 @@ export function buildPushScript() {
|
|
|
161
165
|
`New-Item -ItemType Directory -Force -Path $dir | Out-Null`,
|
|
162
166
|
`$dst = Join-Path $dir '${WIN_HELPER_EXE}'`,
|
|
163
167
|
`Get-Process -Name 'computer-helper-win' -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue`,
|
|
164
|
-
`$si = [Console]::OpenStandardInput()`,
|
|
165
|
-
`$t = New-Object Security.Cryptography.FromBase64Transform`,
|
|
166
|
-
`$cs = New-Object Security.Cryptography.CryptoStream($si, $t, [Security.Cryptography.CryptoStreamMode]::Read)`,
|
|
167
|
-
`$fs = [IO.File]::Create($dst)`,
|
|
168
|
-
`$cs.CopyTo($fs)`,
|
|
169
|
-
`$fs.Close(); $cs.Close()`,
|
|
170
168
|
`Write-Output $dst`,
|
|
171
169
|
].join('; ');
|
|
172
170
|
}
|
|
171
|
+
/** PowerShell that verifies scp wrote the expected number of bytes. */
|
|
172
|
+
export function buildVerifyPushScript(remotePath, expectedBytes) {
|
|
173
|
+
return [
|
|
174
|
+
`$dst = ${psSingleQuote(remotePath)}`,
|
|
175
|
+
`$item = Get-Item -LiteralPath $dst -ErrorAction Stop`,
|
|
176
|
+
`if ($item.Length -ne ${expectedBytes}) { throw "helper copy length mismatch: expected ${expectedBytes}, got $($item.Length)" }`,
|
|
177
|
+
`Write-Output "$dst $($item.Length)"`,
|
|
178
|
+
].join('; ');
|
|
179
|
+
}
|
|
173
180
|
/**
|
|
174
181
|
* PowerShell that registers the daemon as a LOGON scheduled task. Interactive
|
|
175
182
|
* logon type + Highest run level so the daemon runs in the real desktop session
|
|
@@ -195,51 +202,32 @@ export function buildUnregisterTaskScript(taskName) {
|
|
|
195
202
|
`Get-Process -Name 'computer-helper-win' -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue`,
|
|
196
203
|
].join('; ');
|
|
197
204
|
}
|
|
205
|
+
/** Convert a Windows path returned by PowerShell into the scp/SFTP path form. */
|
|
206
|
+
export function scpRemotePath(remotePath) {
|
|
207
|
+
return remotePath.replace(/\\/g, '/');
|
|
208
|
+
}
|
|
198
209
|
/**
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*/
|
|
203
|
-
/**
|
|
204
|
-
* Base64-encode a byte stream in 3-byte-aligned chunks so the concatenated
|
|
205
|
-
* output is valid (every chunk boundary lands on a base64 quantum).
|
|
210
|
+
* Build the scp argv used for the helper exe transfer. Exported so tests can
|
|
211
|
+
* assert the real binary copy path keeps BatchMode and does not route bytes
|
|
212
|
+
* through a PowerShell decoder.
|
|
206
213
|
*/
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
_transform(chunk, _enc, cb) {
|
|
210
|
-
const buf = this.leftover.length ? Buffer.concat([this.leftover, chunk]) : chunk;
|
|
211
|
-
const usable = buf.length - (buf.length % 3);
|
|
212
|
-
this.leftover = Buffer.from(buf.subarray(usable));
|
|
213
|
-
if (usable > 0)
|
|
214
|
-
this.push(buf.subarray(0, usable).toString('base64'));
|
|
215
|
-
cb();
|
|
216
|
-
}
|
|
217
|
-
_flush(cb) {
|
|
218
|
-
if (this.leftover.length)
|
|
219
|
-
this.push(this.leftover.toString('base64'));
|
|
220
|
-
cb();
|
|
221
|
-
}
|
|
214
|
+
export function buildScpArgs(target, remotePath, filePath) {
|
|
215
|
+
return [...SSH_OPTS, filePath, `${target}:${scpRemotePath(remotePath)}`];
|
|
222
216
|
}
|
|
223
217
|
/**
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
* `spawnSync({ input })` blob deadlocked once the ssh socket buffer filled
|
|
227
|
-
* (~4MB) on large files (the 157MB Windows helper reproduced this reliably),
|
|
228
|
-
* and worse, reported a false success leaving a 0-byte remote file. Rejects on
|
|
229
|
-
* any pipe error so a broken transfer fails loudly instead.
|
|
218
|
+
* Copy a local file to the remote destination with scp. This is a binary
|
|
219
|
+
* transfer; no base64 transform runs on either side.
|
|
230
220
|
*/
|
|
231
|
-
function
|
|
221
|
+
function copyFileOverScp(target, remotePath, filePath, timeoutMs = 600_000) {
|
|
232
222
|
return new Promise((resolve, reject) => {
|
|
233
|
-
const child = spawn('
|
|
234
|
-
stdio: ['
|
|
223
|
+
const child = spawn('scp', buildScpArgs(target, remotePath, filePath), {
|
|
224
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
235
225
|
});
|
|
236
226
|
let stderr = '';
|
|
237
|
-
let stdout = '';
|
|
238
227
|
child.stderr.on('data', (d) => (stderr += d.toString()));
|
|
239
|
-
child.stdout.on('data', (d) => (stdout += d.toString()));
|
|
240
228
|
const timer = setTimeout(() => {
|
|
241
229
|
child.kill('SIGKILL');
|
|
242
|
-
reject(new Error(`
|
|
230
|
+
reject(new Error(`scp push to ${target} timed out after ${timeoutMs}ms`));
|
|
243
231
|
}, timeoutMs);
|
|
244
232
|
const fail = (e) => {
|
|
245
233
|
clearTimeout(timer);
|
|
@@ -247,30 +235,41 @@ function streamFileOverSsh(target, remoteCmd, filePath, timeoutMs = 600_000) {
|
|
|
247
235
|
reject(e);
|
|
248
236
|
};
|
|
249
237
|
child.on('error', fail);
|
|
250
|
-
child.stdin.on('error', fail); // EPIPE if the remote decoder dies mid-stream
|
|
251
238
|
child.on('close', (code) => {
|
|
252
239
|
clearTimeout(timer);
|
|
253
|
-
resolve({ code, stderr
|
|
240
|
+
resolve({ code, stderr });
|
|
254
241
|
});
|
|
255
|
-
const src = fs.createReadStream(filePath);
|
|
256
|
-
src.on('error', fail);
|
|
257
|
-
// disk -> aligned base64 -> ssh stdin; .pipe() applies backpressure
|
|
258
|
-
src.pipe(new Base64Encode()).pipe(child.stdin);
|
|
259
242
|
});
|
|
260
243
|
}
|
|
244
|
+
/**
|
|
245
|
+
* `setup --host`: push the exe, then register + start the LOGON task. Remote
|
|
246
|
+
* PowerShell hops go through `sshExec` (BatchMode key auth — the same hardening
|
|
247
|
+
* the browser driver and `agents ssh` use), and the large exe rides a binary
|
|
248
|
+
* scp transfer. Throws with the remote stderr on any failure.
|
|
249
|
+
*/
|
|
261
250
|
export async function setupRemoteHelper(name) {
|
|
262
251
|
const { target } = await resolveRemoteDevice(name);
|
|
263
252
|
const exe = resolveWinHelperExe();
|
|
264
253
|
if (!exe) {
|
|
265
254
|
throw new Error(`Windows helper exe not built. Run: bash scripts/build-win.sh`);
|
|
266
255
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
256
|
+
const prep = sshExec(target, encodePowerShell(buildPushScript()), { timeoutMs: 60_000 });
|
|
257
|
+
if (prep.code !== 0) {
|
|
258
|
+
throw new Error(`preparing helper exe path on '${name}' failed (exit ${prep.code ?? 'null'}): ${prep.stderr.trim() || prep.stdout.trim()}`);
|
|
259
|
+
}
|
|
260
|
+
const remotePath = prep.stdout.trim().split(/\r?\n/).filter(Boolean).at(-1);
|
|
261
|
+
if (!remotePath) {
|
|
262
|
+
throw new Error(`preparing helper exe path on '${name}' did not return a destination path`);
|
|
263
|
+
}
|
|
264
|
+
const push = await copyFileOverScp(target, remotePath, exe);
|
|
271
265
|
if (push.code !== 0) {
|
|
272
266
|
throw new Error(`pushing helper exe to '${name}' failed (exit ${push.code ?? 'null'}): ${push.stderr.trim()}`);
|
|
273
267
|
}
|
|
268
|
+
const expectedBytes = fs.statSync(exe).size;
|
|
269
|
+
const verify = sshExec(target, encodePowerShell(buildVerifyPushScript(remotePath, expectedBytes)), { timeoutMs: 60_000 });
|
|
270
|
+
if (verify.code !== 0) {
|
|
271
|
+
throw new Error(`verifying helper exe on '${name}' failed (exit ${verify.code ?? 'null'}): ${verify.stderr.trim() || verify.stdout.trim()}`);
|
|
272
|
+
}
|
|
274
273
|
// Register + start the LOGON task.
|
|
275
274
|
const reg = sshExec(target, encodePowerShell(buildRegisterTaskScript(REMOTE_HELPER_PORT, REMOTE_TASK_NAME)), {
|
|
276
275
|
timeoutMs: 60_000,
|
|
@@ -32,7 +32,7 @@ function buildHooksWriter(agent) {
|
|
|
32
32
|
}
|
|
33
33
|
// Native hook registration in settings.json/hooks.json. Grok auto-
|
|
34
34
|
// discovers from ~/.grok/hooks/ so the file copy is sufficient.
|
|
35
|
-
if (agent === 'claude' || agent === 'codex' || agent === 'gemini' || agent === 'antigravity' || agent === 'kimi') {
|
|
35
|
+
if (agent === 'claude' || agent === 'codex' || agent === 'gemini' || agent === 'antigravity' || agent === 'kimi' || agent === 'droid') {
|
|
36
36
|
registerHooksToSettings(agent, versionHome);
|
|
37
37
|
}
|
|
38
38
|
return { synced };
|
|
@@ -49,6 +49,7 @@ export declare const loadPrune: ModuleLoader;
|
|
|
49
49
|
export declare const loadTrash: ModuleLoader;
|
|
50
50
|
export declare const loadRestore: ModuleLoader;
|
|
51
51
|
export declare const loadDoctor: ModuleLoader;
|
|
52
|
+
export declare const loadCheck: ModuleLoader;
|
|
52
53
|
export declare const loadStatus: ModuleLoader;
|
|
53
54
|
export declare const loadProfiles: ModuleLoader;
|
|
54
55
|
export declare const loadSecrets: ModuleLoader;
|
|
@@ -57,6 +58,7 @@ export declare const loadHelper: ModuleLoader;
|
|
|
57
58
|
export declare const loadMenubar: ModuleLoader;
|
|
58
59
|
export declare const loadBeta: ModuleLoader;
|
|
59
60
|
export declare const loadSync: ModuleLoader;
|
|
61
|
+
export declare const loadLock: ModuleLoader;
|
|
60
62
|
export declare const loadRefreshRules: ModuleLoader;
|
|
61
63
|
export declare const loadDrive: ModuleLoader;
|
|
62
64
|
export declare const loadFactory: ModuleLoader;
|
|
@@ -66,9 +68,12 @@ export declare const loadBudget: ModuleLoader;
|
|
|
66
68
|
export declare const loadAlias: ModuleLoader;
|
|
67
69
|
export declare const loadPty: ModuleLoader;
|
|
68
70
|
export declare const loadTmux: ModuleLoader;
|
|
71
|
+
export declare const loadWatchdog: ModuleLoader;
|
|
69
72
|
export declare const loadBrowser: ModuleLoader;
|
|
70
73
|
export declare const loadComputer: ModuleLoader;
|
|
71
74
|
export declare const loadHosts: ModuleLoader;
|
|
75
|
+
export declare const loadLogs: ModuleLoader;
|
|
76
|
+
export declare const loadEvents: ModuleLoader;
|
|
72
77
|
export declare const loadSsh: ModuleLoader;
|
|
73
78
|
export declare const loadPull: ModuleLoader;
|
|
74
79
|
export declare const loadPush: ModuleLoader;
|
|
@@ -77,6 +82,9 @@ export declare const loadSetup: ModuleLoader;
|
|
|
77
82
|
export declare const loadSessions: ModuleLoader;
|
|
78
83
|
export declare const loadTeams: ModuleLoader;
|
|
79
84
|
export declare const loadCloud: ModuleLoader;
|
|
85
|
+
export declare const loadMessage: ModuleLoader;
|
|
86
|
+
export declare const loadServe: ModuleLoader;
|
|
87
|
+
export declare const loadAudit: ModuleLoader;
|
|
80
88
|
/**
|
|
81
89
|
* Commands whose modules pull in the SQLite-backed session/cloud stack. They are
|
|
82
90
|
* registered AFTER `applyGlobalHelpConventions` (mirroring main's order: help
|
|
@@ -27,6 +27,7 @@ export const loadPrune = async () => (await import('../../commands/prune.js')).r
|
|
|
27
27
|
export const loadTrash = async () => (await import('../../commands/trash.js')).registerTrashCommands;
|
|
28
28
|
export const loadRestore = async () => (await import('../../commands/trash.js')).registerRestoreCommand;
|
|
29
29
|
export const loadDoctor = async () => (await import('../../commands/doctor.js')).registerDoctorCommand;
|
|
30
|
+
export const loadCheck = async () => (await import('../../commands/check.js')).registerCheckCommand;
|
|
30
31
|
export const loadStatus = async () => (await import('../../commands/status.js')).registerStatusCommand;
|
|
31
32
|
export const loadProfiles = async () => (await import('../../commands/profiles.js')).registerProfilesCommands;
|
|
32
33
|
export const loadSecrets = async () => (await import('../../commands/secrets.js')).registerSecretsCommands;
|
|
@@ -35,6 +36,7 @@ export const loadHelper = async () => (await import('../../commands/helper.js'))
|
|
|
35
36
|
export const loadMenubar = async () => (await import('../../commands/menubar.js')).registerMenubarCommands;
|
|
36
37
|
export const loadBeta = async () => (await import('../../commands/beta.js')).registerBetaCommands;
|
|
37
38
|
export const loadSync = async () => (await import('../../commands/sync.js')).registerSyncCommand;
|
|
39
|
+
export const loadLock = async () => (await import('../../commands/lock.js')).registerLockCommand;
|
|
38
40
|
export const loadRefreshRules = async () => (await import('../../commands/refresh-rules.js')).registerRefreshRulesCommand;
|
|
39
41
|
export const loadDrive = async () => (await import('../../commands/drive.js')).registerDriveCommands;
|
|
40
42
|
export const loadFactory = async () => (await import('../../commands/factory.js')).registerFactoryCommands;
|
|
@@ -44,9 +46,12 @@ export const loadBudget = async () => (await import('../../commands/budget.js'))
|
|
|
44
46
|
export const loadAlias = async () => (await import('../../commands/alias.js')).registerAliasCommand;
|
|
45
47
|
export const loadPty = async () => (await import('../../commands/pty.js')).registerPtyCommands;
|
|
46
48
|
export const loadTmux = async () => (await import('../../commands/tmux.js')).registerTmuxCommands;
|
|
49
|
+
export const loadWatchdog = async () => (await import('../../commands/watchdog.js')).registerWatchdogCommand;
|
|
47
50
|
export const loadBrowser = async () => (await import('../../commands/browser.js')).registerBrowserCommand;
|
|
48
51
|
export const loadComputer = async () => (await import('../../commands/computer.js')).registerComputerCommand;
|
|
49
52
|
export const loadHosts = async () => (await import('../../commands/hosts.js')).registerHostsCommand;
|
|
53
|
+
export const loadLogs = async () => (await import('../../commands/logs.js')).registerLogsCommand;
|
|
54
|
+
export const loadEvents = async () => (await import('../../commands/events.js')).registerEventsCommand;
|
|
50
55
|
export const loadSsh = async () => (await import('../../commands/ssh.js')).registerSshCommands;
|
|
51
56
|
export const loadPull = async () => (await import('../../commands/pull.js')).registerPullCommand;
|
|
52
57
|
export const loadPush = async () => (await import('../../commands/push.js')).registerPushCommand;
|
|
@@ -55,6 +60,9 @@ export const loadSetup = async () => (await import('../../commands/setup.js')).r
|
|
|
55
60
|
export const loadSessions = async () => (await import('../../commands/sessions.js')).registerSessionsCommands;
|
|
56
61
|
export const loadTeams = async () => (await import('../../commands/teams.js')).registerTeamsCommands;
|
|
57
62
|
export const loadCloud = async () => (await import('../../commands/cloud.js')).registerCloudCommands;
|
|
63
|
+
export const loadMessage = async () => (await import('../../commands/message.js')).registerMessageCommand;
|
|
64
|
+
export const loadServe = async () => (await import('../../commands/serve.js')).registerServeCommand;
|
|
65
|
+
export const loadAudit = async () => (await import('../../commands/audit.js')).registerAuditCommands;
|
|
58
66
|
/**
|
|
59
67
|
* Commands whose modules pull in the SQLite-backed session/cloud stack. They are
|
|
60
68
|
* registered AFTER `applyGlobalHelpConventions` (mirroring main's order: help
|
|
@@ -62,7 +70,7 @@ export const loadCloud = async () => (await import('../../commands/cloud.js')).r
|
|
|
62
70
|
* inherit the root's custom help formatter rather than getting the per-command
|
|
63
71
|
* recursive pass. Keeping that ordering preserves their `--help` output exactly.
|
|
64
72
|
*/
|
|
65
|
-
export const LAZY_COMMAND_NAMES = new Set(['sessions', 'teams', 'cloud']);
|
|
73
|
+
export const LAZY_COMMAND_NAMES = new Set(['sessions', 'teams', 'cloud', 'message', 'serve']);
|
|
66
74
|
/**
|
|
67
75
|
* User-typed top-level command name -> ordered list of module loaders to run.
|
|
68
76
|
*
|
|
@@ -111,6 +119,7 @@ export const COMMAND_LOADERS = {
|
|
|
111
119
|
trash: [loadTrash],
|
|
112
120
|
restore: [loadRestore],
|
|
113
121
|
doctor: [loadDoctor],
|
|
122
|
+
check: [loadCheck],
|
|
114
123
|
status: [loadStatus],
|
|
115
124
|
profiles: [loadProfiles],
|
|
116
125
|
secrets: [loadSecrets],
|
|
@@ -119,6 +128,7 @@ export const COMMAND_LOADERS = {
|
|
|
119
128
|
menubar: [loadMenubar],
|
|
120
129
|
beta: [loadBeta],
|
|
121
130
|
sync: [loadSync],
|
|
131
|
+
lock: [loadLock],
|
|
122
132
|
'refresh-rules': [loadRefreshRules],
|
|
123
133
|
drive: [loadDrive],
|
|
124
134
|
factory: [loadFactory],
|
|
@@ -128,9 +138,12 @@ export const COMMAND_LOADERS = {
|
|
|
128
138
|
alias: [loadAlias],
|
|
129
139
|
pty: [loadPty],
|
|
130
140
|
tmux: [loadTmux],
|
|
141
|
+
watchdog: [loadWatchdog],
|
|
131
142
|
browser: [loadBrowser],
|
|
132
143
|
computer: [loadComputer],
|
|
133
144
|
hosts: [loadHosts],
|
|
145
|
+
logs: [loadLogs],
|
|
146
|
+
events: [loadEvents],
|
|
134
147
|
ssh: [loadSsh],
|
|
135
148
|
devices: [loadSsh],
|
|
136
149
|
pull: [loadPull],
|
|
@@ -140,4 +153,7 @@ export const COMMAND_LOADERS = {
|
|
|
140
153
|
sessions: [loadSessions],
|
|
141
154
|
teams: [loadTeams],
|
|
142
155
|
cloud: [loadCloud],
|
|
156
|
+
message: [loadMessage],
|
|
157
|
+
serve: [loadServe],
|
|
158
|
+
audit: [loadAudit],
|
|
143
159
|
};
|
package/dist/lib/state.d.ts
CHANGED
|
@@ -120,6 +120,8 @@ export declare function getRoutinesDir(): string;
|
|
|
120
120
|
export declare function getProjectRoutinesDir(cwd?: string): string | null;
|
|
121
121
|
/** Path to routine execution logs (~/.agents/.history/runs/). */
|
|
122
122
|
export declare function getRunsDir(): string;
|
|
123
|
+
/** Root for per-agent mailboxes (~/.agents/.history/mailbox/). */
|
|
124
|
+
export declare function getMailboxRootDir(): string;
|
|
123
125
|
/** Path to installed agent CLI binaries (~/.agents/.history/versions/). */
|
|
124
126
|
export declare function getVersionsDir(): string;
|
|
125
127
|
/** Path to version-switching shim scripts (~/.agents/.cache/shims/). */
|
|
@@ -226,6 +228,17 @@ export declare function getEnabledExtraRepos(): Array<{
|
|
|
226
228
|
export declare function ensureAgentsDir(): void;
|
|
227
229
|
/** Return an empty Meta object used when no agents.yaml exists yet. */
|
|
228
230
|
export declare function createDefaultMeta(): Meta;
|
|
231
|
+
/**
|
|
232
|
+
* Per-device machine-local version pins — `~/.agents/devices/<machine>/agents.yaml`.
|
|
233
|
+
* Committed and synced, but each machine only ever writes its OWN folder, so
|
|
234
|
+
* pulls never conflict. `<machine>` = machineId() (Tailscale-aligned short name).
|
|
235
|
+
*/
|
|
236
|
+
export declare function getDeviceMetaPath(): string;
|
|
237
|
+
/**
|
|
238
|
+
* Machine-local per-version resource tracking — `~/.agents/.history/version-resources.json`.
|
|
239
|
+
* Gitignored (under .history/) and regenerable; never synced.
|
|
240
|
+
*/
|
|
241
|
+
export declare function getVersionResourcesPath(): string;
|
|
229
242
|
/**
|
|
230
243
|
* Read and cache ~/.agents/agents.yaml, migrating from legacy locations if needed.
|
|
231
244
|
*
|
package/dist/lib/state.js
CHANGED
|
@@ -28,6 +28,7 @@ import * as os from 'os';
|
|
|
28
28
|
import * as yaml from 'yaml';
|
|
29
29
|
import { ensureLockTarget, atomicWriteFileSync, withFileLock } from './fs-atomic.js';
|
|
30
30
|
import { SEEDED_REGISTRIES } from './types.js';
|
|
31
|
+
import { machineId } from './machine-id.js';
|
|
31
32
|
const HOME = process.env.HOME ?? os.homedir();
|
|
32
33
|
/**
|
|
33
34
|
* Compare two filesystem paths for identity, resolving symlinks and (on
|
|
@@ -87,6 +88,7 @@ const RUNS_DIR = path.join(HISTORY_DIR, 'runs');
|
|
|
87
88
|
const TEAMS_AGENTS_DIR = path.join(HISTORY_DIR, 'teams', 'agents');
|
|
88
89
|
const BACKUPS_DIR = path.join(HISTORY_DIR, 'backups');
|
|
89
90
|
const TRASH_DIR = path.join(HISTORY_DIR, 'trash');
|
|
91
|
+
const MAILBOX_DIR = path.join(HISTORY_DIR, 'mailbox');
|
|
90
92
|
// Cache bucket (regenerable).
|
|
91
93
|
const SHIMS_DIR = path.join(CACHE_DIR, 'shims');
|
|
92
94
|
const HOOK_SHIMS_DIR = path.join(SHIMS_DIR, 'hooks');
|
|
@@ -309,6 +311,8 @@ export function getProjectRoutinesDir(cwd = process.cwd()) {
|
|
|
309
311
|
}
|
|
310
312
|
/** Path to routine execution logs (~/.agents/.history/runs/). */
|
|
311
313
|
export function getRunsDir() { return RUNS_DIR; }
|
|
314
|
+
/** Root for per-agent mailboxes (~/.agents/.history/mailbox/). */
|
|
315
|
+
export function getMailboxRootDir() { return MAILBOX_DIR; }
|
|
312
316
|
/** Path to installed agent CLI binaries (~/.agents/.history/versions/). */
|
|
313
317
|
export function getVersionsDir() { return VERSIONS_DIR; }
|
|
314
318
|
/** Path to version-switching shim scripts (~/.agents/.cache/shims/). */
|
|
@@ -495,13 +499,37 @@ function safeMtimeMs(filePath) {
|
|
|
495
499
|
return 0;
|
|
496
500
|
}
|
|
497
501
|
}
|
|
498
|
-
/**
|
|
502
|
+
/**
|
|
503
|
+
* Per-device machine-local version pins — `~/.agents/devices/<machine>/agents.yaml`.
|
|
504
|
+
* Committed and synced, but each machine only ever writes its OWN folder, so
|
|
505
|
+
* pulls never conflict. `<machine>` = machineId() (Tailscale-aligned short name).
|
|
506
|
+
*/
|
|
507
|
+
export function getDeviceMetaPath() {
|
|
508
|
+
return path.join(USER_AGENTS_DIR, 'devices', machineId(), 'agents.yaml');
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Machine-local per-version resource tracking — `~/.agents/.history/version-resources.json`.
|
|
512
|
+
* Gitignored (under .history/) and regenerable; never synced.
|
|
513
|
+
*/
|
|
514
|
+
export function getVersionResourcesPath() {
|
|
515
|
+
return path.join(HISTORY_DIR, 'version-resources.json');
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Combined cache stamp across all four Meta sources: central + system
|
|
519
|
+
* agents.yaml, this machine's device pins, and the version-resources tracking.
|
|
520
|
+
* A delimited string, NOT a numeric sum — summing down-scaled epoch-ms values
|
|
521
|
+
* loses precision (float64 rounds sub-unit terms away at ~1.75e12), so a change
|
|
522
|
+
* in any one file must contribute at full resolution.
|
|
523
|
+
*/
|
|
499
524
|
function currentMetaStamp() {
|
|
500
|
-
return safeMtimeMs(META_FILE)
|
|
525
|
+
return safeMtimeMs(META_FILE)
|
|
526
|
+
+ '|' + safeMtimeMs(SYSTEM_META_FILE)
|
|
527
|
+
+ '|' + safeMtimeMs(getDeviceMetaPath())
|
|
528
|
+
+ '|' + safeMtimeMs(getVersionResourcesPath());
|
|
501
529
|
}
|
|
502
530
|
/** Memoize a parsed Meta against the current file mtimes. */
|
|
503
531
|
function rememberMeta(meta) {
|
|
504
|
-
metaCache = {
|
|
532
|
+
metaCache = { stamp: currentMetaStamp(), meta };
|
|
505
533
|
return meta;
|
|
506
534
|
}
|
|
507
535
|
function withMetaLock(fn) {
|
|
@@ -526,11 +554,78 @@ function withMetaLock(fn) {
|
|
|
526
554
|
}
|
|
527
555
|
});
|
|
528
556
|
}
|
|
557
|
+
/** Atomic write only when the on-disk content differs — avoids needless mtime
|
|
558
|
+
* bumps (which would thrash the meta cache) on no-op field routing. */
|
|
559
|
+
function writeIfChanged(filePath, content) {
|
|
560
|
+
let current = null;
|
|
561
|
+
try {
|
|
562
|
+
current = fs.readFileSync(filePath, 'utf-8');
|
|
563
|
+
}
|
|
564
|
+
catch { /* absent */ }
|
|
565
|
+
if (current === content)
|
|
566
|
+
return;
|
|
567
|
+
atomicWriteFileSync(filePath, content);
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Partition the in-memory Meta across three files by sync-domain:
|
|
571
|
+
* - central `~/.agents/agents.yaml` — portable, everything else
|
|
572
|
+
* - device `~/.agents/devices/<machine>/agents.yaml` — `agents:` pins (per-device)
|
|
573
|
+
* - history `~/.agents/.history/version-resources.json` — `versions:` (machine-local)
|
|
574
|
+
* All callers funnel through writeMeta → here, so nothing else changes. Empty
|
|
575
|
+
* `agents:` / `versions:` are not written (no empty committed files).
|
|
576
|
+
*/
|
|
529
577
|
function writeMetaUnlocked(meta) {
|
|
530
|
-
const
|
|
531
|
-
|
|
578
|
+
const { agents, versions, ...central } = meta;
|
|
579
|
+
// Write the machine-local files FIRST, then strip central — so a crash mid-write
|
|
580
|
+
// never removes pins/versions from central before they're persisted elsewhere.
|
|
581
|
+
const devicePath = getDeviceMetaPath();
|
|
582
|
+
if (agents && Object.keys(agents).length > 0) {
|
|
583
|
+
fs.mkdirSync(path.dirname(devicePath), { recursive: true });
|
|
584
|
+
writeIfChanged(devicePath, META_HEADER + yaml.stringify({ agents }));
|
|
585
|
+
}
|
|
586
|
+
else if (fs.existsSync(devicePath)) {
|
|
587
|
+
// Every pin was cleared. Persist the emptied map instead of skipping the
|
|
588
|
+
// write — otherwise the stale device file survives and overlayMachineLocal
|
|
589
|
+
// re-applies the removed pin on the next read, leaving a dangling default
|
|
590
|
+
// (e.g. a launcher pointing at a version that was just uninstalled).
|
|
591
|
+
writeIfChanged(devicePath, META_HEADER + yaml.stringify({ agents: {} }));
|
|
592
|
+
}
|
|
593
|
+
if (versions && Object.keys(versions).length > 0) {
|
|
594
|
+
const vrPath = getVersionResourcesPath();
|
|
595
|
+
fs.mkdirSync(path.dirname(vrPath), { recursive: true });
|
|
596
|
+
writeIfChanged(vrPath, JSON.stringify(versions, null, 2) + '\n');
|
|
597
|
+
}
|
|
598
|
+
writeIfChanged(META_FILE, META_HEADER + yaml.stringify(central));
|
|
532
599
|
metaCache = null;
|
|
533
600
|
}
|
|
601
|
+
/**
|
|
602
|
+
* Overlay this machine's local state onto a central-portable Meta:
|
|
603
|
+
* - `agents:` from the device file (device wins; the union both preserves the
|
|
604
|
+
* one-level merge and self-heals a pre-migration central that still has pins)
|
|
605
|
+
* - `versions:` from the history JSON (wholesale replace; falls back to
|
|
606
|
+
* whatever central carried when the history file doesn't exist yet)
|
|
607
|
+
*/
|
|
608
|
+
function overlayMachineLocal(meta) {
|
|
609
|
+
const devicePath = getDeviceMetaPath();
|
|
610
|
+
if (fs.existsSync(devicePath)) {
|
|
611
|
+
try {
|
|
612
|
+
const dm = yaml.parse(fs.readFileSync(devicePath, 'utf-8'));
|
|
613
|
+
if (dm?.agents)
|
|
614
|
+
meta.agents = { ...meta.agents, ...dm.agents };
|
|
615
|
+
}
|
|
616
|
+
catch { /* ignore malformed device file */ }
|
|
617
|
+
}
|
|
618
|
+
const vrPath = getVersionResourcesPath();
|
|
619
|
+
if (fs.existsSync(vrPath)) {
|
|
620
|
+
try {
|
|
621
|
+
const vr = JSON.parse(fs.readFileSync(vrPath, 'utf-8'));
|
|
622
|
+
if (vr)
|
|
623
|
+
meta.versions = vr;
|
|
624
|
+
}
|
|
625
|
+
catch { /* ignore malformed history file */ }
|
|
626
|
+
}
|
|
627
|
+
return meta;
|
|
628
|
+
}
|
|
534
629
|
function applyRegistrySeeds(meta) {
|
|
535
630
|
const seeded = new Set(meta.seededPresets || []);
|
|
536
631
|
let changed = false;
|
|
@@ -590,10 +685,7 @@ export function readMeta() {
|
|
|
590
685
|
// what we last parsed. Reduces N readMeta calls per CLI invocation to ~2 stat
|
|
591
686
|
// syscalls plus an in-memory object spread.
|
|
592
687
|
if (metaCache) {
|
|
593
|
-
|
|
594
|
-
const systemMtime = safeMtimeMs(SYSTEM_META_FILE);
|
|
595
|
-
const stamp = userMtime + systemMtime * 1e-3;
|
|
596
|
-
if (stamp === metaCache.mtime) {
|
|
688
|
+
if (currentMetaStamp() === metaCache.stamp) {
|
|
597
689
|
return metaCache.meta;
|
|
598
690
|
}
|
|
599
691
|
}
|
|
@@ -666,6 +758,7 @@ export function readMeta() {
|
|
|
666
758
|
...userMeta?.registries,
|
|
667
759
|
};
|
|
668
760
|
}
|
|
761
|
+
overlayMachineLocal(meta);
|
|
669
762
|
if (applyRegistrySeeds(meta)) {
|
|
670
763
|
writeMeta(meta);
|
|
671
764
|
return rememberMeta(meta);
|
|
@@ -673,6 +766,7 @@ export function readMeta() {
|
|
|
673
766
|
return rememberMeta(meta);
|
|
674
767
|
}
|
|
675
768
|
const meta = createDefaultMeta();
|
|
769
|
+
overlayMachineLocal(meta);
|
|
676
770
|
if (applyRegistrySeeds(meta)) {
|
|
677
771
|
writeMeta(meta);
|
|
678
772
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Umbrella `agents sync` orchestration — "make this machine current".
|
|
3
3
|
*
|
|
4
|
-
* Bare `agents sync` fetches
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Bare `agents sync` fetches the config repos then reconciles them into every
|
|
5
|
+
* installed agent's version home. Secrets and sessions are opt-in stages
|
|
6
|
+
* (`--secrets` / `--sessions`) — see `planUmbrellaStages` for why they're off by
|
|
7
|
+
* default. Each stage is an existing exported library function; this module only
|
|
8
|
+
* sequences them and decides — from the flags — which stages run. The planner is
|
|
9
|
+
* pure so the flag matrix is unit-tested without any I/O.
|
|
9
10
|
*
|
|
10
11
|
* Stage backends:
|
|
11
12
|
* repos -> git pull of ~/.agents + enabled ~/.agents-* extras (pullRepo)
|
|
@@ -32,11 +33,17 @@ export interface UmbrellaPlan {
|
|
|
32
33
|
}
|
|
33
34
|
/**
|
|
34
35
|
* Decide which stages run. Pure — no I/O. Semantics:
|
|
35
|
-
* bare (no flags) fetch
|
|
36
|
+
* bare (no flags) fetch repos, then reconcile
|
|
36
37
|
* --local reconcile only, no fetch
|
|
37
|
-
* --cloud fetch (
|
|
38
|
+
* --cloud fetch repos (or the selected subset), skip reconcile
|
|
38
39
|
* --repos/--secrets/... fetch only the selected types, then reconcile
|
|
39
40
|
* `--local` wins over everything; `--cloud` suppresses reconcile.
|
|
41
|
+
*
|
|
42
|
+
* Secrets and sessions are NOT part of the bare default — they are opt-in via
|
|
43
|
+
* `--secrets` / `--sessions`. Pulling every secret bundle onto the machine on a
|
|
44
|
+
* bare `agents sync` is more blast radius than the verb should carry by
|
|
45
|
+
* default, and session transcripts are queryable on demand (`agents sessions
|
|
46
|
+
* --host <machine>`) so they don't need eager mirroring.
|
|
40
47
|
*/
|
|
41
48
|
export declare function planUmbrellaStages(f: UmbrellaFlags): UmbrellaPlan;
|
|
42
49
|
export interface UmbrellaResult {
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Umbrella `agents sync` orchestration — "make this machine current".
|
|
3
3
|
*
|
|
4
|
-
* Bare `agents sync` fetches
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Bare `agents sync` fetches the config repos then reconciles them into every
|
|
5
|
+
* installed agent's version home. Secrets and sessions are opt-in stages
|
|
6
|
+
* (`--secrets` / `--sessions`) — see `planUmbrellaStages` for why they're off by
|
|
7
|
+
* default. Each stage is an existing exported library function; this module only
|
|
8
|
+
* sequences them and decides — from the flags — which stages run. The planner is
|
|
9
|
+
* pure so the flag matrix is unit-tested without any I/O.
|
|
9
10
|
*
|
|
10
11
|
* Stage backends:
|
|
11
12
|
* repos -> git pull of ~/.agents + enabled ~/.agents-* extras (pullRepo)
|
|
@@ -20,11 +21,17 @@ import { getUserAgentsDir, getEnabledExtraRepos } from './state.js';
|
|
|
20
21
|
import { listRemoteBundles, pullBundle } from './secrets/sync.js';
|
|
21
22
|
/**
|
|
22
23
|
* Decide which stages run. Pure — no I/O. Semantics:
|
|
23
|
-
* bare (no flags) fetch
|
|
24
|
+
* bare (no flags) fetch repos, then reconcile
|
|
24
25
|
* --local reconcile only, no fetch
|
|
25
|
-
* --cloud fetch (
|
|
26
|
+
* --cloud fetch repos (or the selected subset), skip reconcile
|
|
26
27
|
* --repos/--secrets/... fetch only the selected types, then reconcile
|
|
27
28
|
* `--local` wins over everything; `--cloud` suppresses reconcile.
|
|
29
|
+
*
|
|
30
|
+
* Secrets and sessions are NOT part of the bare default — they are opt-in via
|
|
31
|
+
* `--secrets` / `--sessions`. Pulling every secret bundle onto the machine on a
|
|
32
|
+
* bare `agents sync` is more blast radius than the verb should carry by
|
|
33
|
+
* default, and session transcripts are queryable on demand (`agents sessions
|
|
34
|
+
* --host <machine>`) so they don't need eager mirroring.
|
|
28
35
|
*/
|
|
29
36
|
export function planUmbrellaStages(f) {
|
|
30
37
|
if (f.local) {
|
|
@@ -39,8 +46,9 @@ export function planUmbrellaStages(f) {
|
|
|
39
46
|
reconcile: !f.cloud,
|
|
40
47
|
};
|
|
41
48
|
}
|
|
42
|
-
// No per-type selector: bare =
|
|
43
|
-
|
|
49
|
+
// No per-type selector: bare = repos + reconcile; --cloud = repos, no
|
|
50
|
+
// reconcile. Secrets/sessions stay off unless explicitly selected above.
|
|
51
|
+
return { fetchRepos: true, fetchSecrets: false, fetchSessions: false, reconcile: !f.cloud };
|
|
44
52
|
}
|
|
45
53
|
/**
|
|
46
54
|
* Execute the planned stages in order: repos -> secrets -> sessions -> reconcile.
|