@phnx-labs/agents-cli 1.22.117 → 1.22.118
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 +235 -0
- package/dist/bootstrap.js +4 -11
- package/dist/commands/browser.d.ts +52 -70
- package/dist/commands/browser.js +192 -3562
- package/dist/commands/daemon.d.ts +39 -2
- package/dist/commands/daemon.js +72 -73
- package/dist/commands/events.js +1 -1
- package/dist/commands/exec.js +53 -1
- package/dist/commands/repo.js +13 -49
- package/dist/commands/route.js +5 -5
- package/dist/commands/routines.js +39 -12
- package/dist/commands/sessions-render.d.ts +1 -1
- package/dist/commands/sessions-render.js +0 -1
- package/dist/commands/sessions.test-fixture.d.ts +0 -1
- package/dist/commands/sessions.test-fixture.js +0 -37
- package/dist/commands/setup-browser.d.ts +17 -12
- package/dist/commands/setup-browser.js +52 -87
- package/dist/commands/setup-preferences.d.ts +0 -17
- package/dist/commands/setup-preferences.js +18 -49
- package/dist/commands/setup.js +9 -9
- package/dist/commands/ssh.js +10 -1
- package/dist/commands/sync.js +6 -44
- package/dist/commands/teams.js +0 -1
- package/dist/commands/update.d.ts +19 -0
- package/dist/commands/update.js +34 -6
- package/dist/commands/view.d.ts +12 -1
- package/dist/commands/view.js +47 -2
- package/dist/index.js +107 -26
- package/dist/lib/account-capabilities.js +0 -2
- package/dist/lib/account-catalog.d.ts +38 -0
- package/dist/lib/account-catalog.js +110 -26
- package/dist/lib/account-provider-registry.js +1 -1
- package/dist/lib/account-registry.d.ts +1 -1
- package/dist/lib/account-registry.js +36 -2
- package/dist/lib/accounting/rotate.js +22 -8
- package/dist/lib/accounting/usage-ingest.d.ts +6 -1
- package/dist/lib/accounting/usage-ingest.js +115 -45
- package/dist/lib/accounting/usage-sync.d.ts +134 -31
- package/dist/lib/accounting/usage-sync.js +227 -66
- package/dist/lib/accounting/usage.d.ts +16 -3
- package/dist/lib/accounting/usage.js +26 -8
- package/dist/lib/accounts/slots.d.ts +8 -0
- package/dist/lib/accounts/slots.js +17 -0
- package/dist/lib/add-dir.js +0 -1
- package/dist/lib/agent-cli-commands.js +0 -1
- package/dist/lib/agent-spec/agents.d.ts +6 -7
- package/dist/lib/agent-spec/agents.js +6 -60
- package/dist/lib/auth-health.d.ts +69 -1
- package/dist/lib/auth-health.js +133 -4
- package/dist/lib/browser/context.d.ts +82 -0
- package/dist/lib/browser/context.js +88 -0
- package/dist/lib/browser/paths.d.ts +25 -0
- package/dist/lib/browser/paths.js +51 -0
- package/dist/lib/browser/record.d.ts +38 -0
- package/dist/lib/browser/record.js +57 -0
- package/dist/lib/browser/sessions-list.js +1 -2
- package/dist/lib/browser-client.d.ts +158 -0
- package/dist/lib/browser-client.js +234 -0
- package/dist/lib/channels/owner-sink.d.ts +1 -18
- package/dist/lib/channels/owner-sink.js +29 -82
- package/dist/lib/channels/providers/rush.d.ts +7 -7
- package/dist/lib/channels/providers/rush.js +127 -57
- package/dist/lib/claude-statusline.d.ts +1 -1
- package/dist/lib/claude-statusline.js +15 -3
- package/dist/lib/commands.js +1 -1
- package/dist/lib/crabbox/lease.js +1 -1
- package/dist/lib/crabbox/runtimes.js +1 -3
- package/dist/lib/daemon/account-state-daemon-service.js +1 -0
- package/dist/lib/daemon/auth-sync-service.d.ts +21 -18
- package/dist/lib/daemon/auth-sync-service.js +37 -43
- package/dist/lib/daemon/daemon.d.ts +25 -14
- package/dist/lib/daemon/daemon.js +92 -154
- package/dist/lib/daemon/runner.d.ts +2 -2
- package/dist/lib/daemon/runner.js +32 -9
- package/dist/lib/daemon/self-update-service.d.ts +35 -34
- package/dist/lib/daemon/self-update-service.js +40 -36
- package/dist/lib/daemon/service.d.ts +12 -8
- package/dist/lib/daemon/supervisor.d.ts +65 -54
- package/dist/lib/daemon/supervisor.js +136 -171
- package/dist/lib/daemon/testdata/process-view-start.js +5 -2
- package/dist/lib/daemon/usage-sync-service.d.ts +21 -30
- package/dist/lib/daemon/usage-sync-service.js +46 -78
- package/dist/lib/daemon-health.d.ts +22 -5
- package/dist/lib/daemon-health.js +47 -3
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +0 -10
- package/dist/lib/daemon-ticks.js +4 -2
- package/dist/lib/devices/doctor-findings.js +8 -11
- package/dist/lib/devices/harness-inventory.js +14 -5
- package/dist/lib/exec-account-home.js +5 -0
- package/dist/lib/exec.js +0 -11
- package/dist/lib/feed/events.js +0 -1
- package/dist/lib/feed/tool-activity.js +5 -2
- package/dist/lib/fleet-shared-state.d.ts +45 -0
- package/dist/lib/fleet-shared-state.js +78 -9
- package/dist/lib/fs-atomic.js +25 -0
- package/dist/lib/harness-auth-capabilities.js +0 -1
- package/dist/lib/helper-versions.js +1 -1
- package/dist/lib/hooks/cache.d.ts +7 -0
- package/dist/lib/hooks/cache.js +27 -2
- package/dist/lib/hooks/install.js +7 -1
- package/dist/lib/hosts/ready.d.ts +38 -8
- package/dist/lib/hosts/ready.js +62 -14
- package/dist/lib/installations/active-check.d.ts +27 -0
- package/dist/lib/installations/active-check.js +61 -0
- package/dist/lib/installations/migrate.js +6 -3
- package/dist/lib/installations/shims.js +9 -7
- package/dist/lib/installations/update.js +21 -15
- package/dist/lib/installations/versions.js +12 -1
- package/dist/lib/manifest.js +1 -1
- package/dist/lib/models.js +0 -2
- package/dist/lib/open-url.js +28 -54
- package/dist/lib/profiles.js +0 -2
- package/dist/lib/refresh.js +1 -1
- package/dist/lib/routine-readiness.d.ts +35 -0
- package/dist/lib/routine-readiness.js +74 -16
- package/dist/lib/rules/compile.js +6 -0
- package/dist/lib/sandbox.js +0 -2
- package/dist/lib/secrets-policy.d.ts +89 -11
- package/dist/lib/secrets-policy.js +175 -28
- package/dist/lib/session/db.d.ts +1 -1
- package/dist/lib/session/db.js +65 -18
- package/dist/lib/session/discover.js +1 -283
- package/dist/lib/session/mirror.d.ts +4 -3
- package/dist/lib/session/mirror.js +4 -3
- package/dist/lib/session/parse.d.ts +0 -2
- package/dist/lib/session/parse.js +1 -138
- package/dist/lib/session/throughput.d.ts +1 -3
- package/dist/lib/session/throughput.js +0 -20
- package/dist/lib/session/types.d.ts +2 -2
- package/dist/lib/session/types.js +2 -2
- package/dist/lib/sessions-client.d.ts +60 -1
- package/dist/lib/sessions-client.js +227 -4
- package/dist/lib/setup-tool-status.js +5 -10
- package/dist/lib/signin-badge.d.ts +3 -2
- package/dist/lib/signin-badge.js +6 -3
- package/dist/lib/smart-launch.js +4 -1
- package/dist/lib/staleness/detectors/commands.d.ts +1 -1
- package/dist/lib/staleness/detectors/hooks.d.ts +1 -1
- package/dist/lib/staleness/detectors/mcp.d.ts +1 -1
- package/dist/lib/staleness/detectors/permissions.d.ts +1 -1
- package/dist/lib/staleness/detectors/plugins.d.ts +1 -1
- package/dist/lib/staleness/detectors/rules.d.ts +1 -1
- package/dist/lib/staleness/detectors/skills.d.ts +1 -1
- package/dist/lib/staleness/detectors/subagents.d.ts +1 -1
- package/dist/lib/staleness/detectors/workflows.d.ts +1 -1
- package/dist/lib/staleness/writers/commands.d.ts +1 -1
- package/dist/lib/staleness/writers/hooks.d.ts +1 -1
- package/dist/lib/staleness/writers/mcp.d.ts +1 -1
- package/dist/lib/staleness/writers/permissions.d.ts +1 -1
- package/dist/lib/staleness/writers/plugins.d.ts +1 -1
- package/dist/lib/staleness/writers/rules.d.ts +1 -1
- package/dist/lib/staleness/writers/skills.d.ts +1 -1
- package/dist/lib/staleness/writers/sources.js +5 -0
- package/dist/lib/staleness/writers/subagents.d.ts +1 -1
- package/dist/lib/staleness/writers/workflows.d.ts +1 -1
- package/dist/lib/state.d.ts +6 -4
- package/dist/lib/state.js +7 -17
- package/dist/lib/teams/parsers.d.ts +1 -1
- package/dist/lib/teams/parsers.js +1 -136
- package/dist/lib/teams/placement-probe.js +50 -15
- package/dist/lib/teams/scheduler.d.ts +7 -0
- package/dist/lib/types.d.ts +10 -1
- package/dist/lib/types.js +1 -1
- package/dist/lib/view-types.d.ts +32 -0
- package/dist/lib/watchdog/read.js +0 -1
- package/dist/lib/watchdog/rotate.js +1 -2
- package/dist/lib/watchdog/runner.js +2 -2
- package/dist/lib/watchdog/watchdog.d.ts +1 -1
- package/dist/lib/watchdog/watchdogTail.js +0 -31
- package/package.json +1 -1
- package/scripts/postinstall.js +8 -8
- package/dist/browser.d.ts +0 -2
- package/dist/browser.js +0 -17
- package/dist/commands/browser-picker.d.ts +0 -18
- package/dist/commands/browser-picker.js +0 -97
- package/dist/lib/browser/arc-discovery.d.ts +0 -52
- package/dist/lib/browser/arc-discovery.js +0 -197
- package/dist/lib/browser/arc-dom.d.ts +0 -14
- package/dist/lib/browser/arc-dom.js +0 -121
- package/dist/lib/browser/caller-identity.d.ts +0 -40
- package/dist/lib/browser/caller-identity.js +0 -175
- package/dist/lib/browser/cdp.d.ts +0 -51
- package/dist/lib/browser/cdp.js +0 -254
- package/dist/lib/browser/chrome.d.ts +0 -141
- package/dist/lib/browser/chrome.js +0 -704
- package/dist/lib/browser/chromium-discovery.d.ts +0 -28
- package/dist/lib/browser/chromium-discovery.js +0 -96
- package/dist/lib/browser/devices.d.ts +0 -34
- package/dist/lib/browser/devices.js +0 -61
- package/dist/lib/browser/domain-skills.d.ts +0 -71
- package/dist/lib/browser/domain-skills.js +0 -195
- package/dist/lib/browser/drivers/arc.d.ts +0 -40
- package/dist/lib/browser/drivers/arc.js +0 -276
- package/dist/lib/browser/drivers/firefox.d.ts +0 -99
- package/dist/lib/browser/drivers/firefox.js +0 -377
- package/dist/lib/browser/drivers/local.d.ts +0 -55
- package/dist/lib/browser/drivers/local.js +0 -266
- package/dist/lib/browser/drivers/ssh.d.ts +0 -120
- package/dist/lib/browser/drivers/ssh.js +0 -467
- package/dist/lib/browser/editor.d.ts +0 -3
- package/dist/lib/browser/editor.js +0 -50
- package/dist/lib/browser/ffmpeg.d.ts +0 -12
- package/dist/lib/browser/ffmpeg.js +0 -184
- package/dist/lib/browser/firefox-discovery.d.ts +0 -69
- package/dist/lib/browser/firefox-discovery.js +0 -162
- package/dist/lib/browser/har.d.ts +0 -85
- package/dist/lib/browser/har.js +0 -77
- package/dist/lib/browser/hygiene.d.ts +0 -97
- package/dist/lib/browser/hygiene.js +0 -153
- package/dist/lib/browser/index.d.ts +0 -5
- package/dist/lib/browser/index.js +0 -5
- package/dist/lib/browser/input.d.ts +0 -7
- package/dist/lib/browser/input.js +0 -92
- package/dist/lib/browser/ipc.d.ts +0 -204
- package/dist/lib/browser/ipc.js +0 -1453
- package/dist/lib/browser/login-detection.d.ts +0 -87
- package/dist/lib/browser/login-detection.js +0 -274
- package/dist/lib/browser/profiles.d.ts +0 -422
- package/dist/lib/browser/profiles.js +0 -1157
- package/dist/lib/browser/refs.d.ts +0 -89
- package/dist/lib/browser/refs.js +0 -191
- package/dist/lib/browser/registry.d.ts +0 -70
- package/dist/lib/browser/registry.js +0 -252
- package/dist/lib/browser/remote-control.d.ts +0 -59
- package/dist/lib/browser/remote-control.js +0 -85
- package/dist/lib/browser/resolve-target.d.ts +0 -66
- package/dist/lib/browser/resolve-target.js +0 -264
- package/dist/lib/browser/runtime-state.d.ts +0 -291
- package/dist/lib/browser/runtime-state.js +0 -584
- package/dist/lib/browser/secret-ref.d.ts +0 -10
- package/dist/lib/browser/secret-ref.js +0 -14
- package/dist/lib/browser/service.d.ts +0 -812
- package/dist/lib/browser/service.js +0 -4693
- package/dist/lib/browser/stream.d.ts +0 -17
- package/dist/lib/browser/stream.js +0 -72
- package/dist/lib/browser/task-index.d.ts +0 -78
- package/dist/lib/browser/task-index.js +0 -199
- package/dist/lib/browser/types.d.ts +0 -647
- package/dist/lib/browser/types.js +0 -61
- package/dist/lib/browser/upload.d.ts +0 -29
- package/dist/lib/browser/upload.js +0 -298
- package/dist/lib/daemon/browser-ipc-service.d.ts +0 -20
- package/dist/lib/daemon/browser-ipc-service.js +0 -46
- package/dist/lib/daemon/browser-task-reap-service.d.ts +0 -14
- package/dist/lib/daemon/browser-task-reap-service.js +0 -26
- package/dist/lib/fleet-shared-repo-sync.d.ts +0 -55
- package/dist/lib/fleet-shared-repo-sync.js +0 -490
|
@@ -1,490 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Automatic transport for the non-secret fleet state stored in the user repo.
|
|
3
|
-
*
|
|
4
|
-
* Usage/auth services publish only their own conflict-free
|
|
5
|
-
* `devices/<device>/daemon-state.json` file. This module commits that one path,
|
|
6
|
-
* rebases the user repo, and pushes it so peer daemons can read the snapshot
|
|
7
|
-
* after their own fetch/rebase. Git runs asynchronously under one cross-process
|
|
8
|
-
* lock; every invocation has a wall-clock deadline and a timed-out process tree
|
|
9
|
-
* is terminated, including git's ssh child and its remote channel.
|
|
10
|
-
*/
|
|
11
|
-
import { spawn } from 'node:child_process';
|
|
12
|
-
import * as fs from 'node:fs';
|
|
13
|
-
import * as path from 'node:path';
|
|
14
|
-
import lockfile from 'proper-lockfile';
|
|
15
|
-
import { fleetSharedStatePath } from './fleet-shared-state.js';
|
|
16
|
-
import { ensureLockTarget } from './fs-atomic.js';
|
|
17
|
-
import { logAndContinueOnLockCompromised } from './lock-compromise.js';
|
|
18
|
-
import { machineId } from './session/sync/config.js';
|
|
19
|
-
import { getDaemonDir, getUserAgentsDir } from './state.js';
|
|
20
|
-
export const FLEET_SHARED_REPO_SYNC_DEADLINE_MS = 45_000;
|
|
21
|
-
const FLEET_SHARED_REPO_KILL_GRACE_MS = 250;
|
|
22
|
-
export const FLEET_SHARED_REPO_OUTPUT_MAX_BYTES = 1024 * 1024;
|
|
23
|
-
const FLEET_SHARED_REPO_PUSH_ATTEMPTS = 3;
|
|
24
|
-
const FLEET_SHARED_REPO_REBASE_CLEANUP_RESERVE_MS = 5_000;
|
|
25
|
-
/**
|
|
26
|
-
* The exchange records its last SUCCESSFUL completion beside its lock so a
|
|
27
|
-
* sibling service (auth-sync) can gate work on the freshness of the peer state
|
|
28
|
-
* this exchange delivers into the local checkout (PHNX-4051). auth-sync's
|
|
29
|
-
* credential pushes read the peer verdicts the LAST usage-sync exchange wrote; if
|
|
30
|
-
* that exchange has not landed within a tick interval, those verdicts are stale
|
|
31
|
-
* and pushing off them would act on a peer that may already hold the key.
|
|
32
|
-
*/
|
|
33
|
-
function exchangeMarkerPath(lockPath) {
|
|
34
|
-
return `${lockPath}.last-success`;
|
|
35
|
-
}
|
|
36
|
-
function writeExchangeMarker(lockPath, nowMs = Date.now()) {
|
|
37
|
-
try {
|
|
38
|
-
const p = exchangeMarkerPath(lockPath);
|
|
39
|
-
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
40
|
-
fs.writeFileSync(p, JSON.stringify({ at: nowMs }), 'utf-8');
|
|
41
|
-
}
|
|
42
|
-
catch {
|
|
43
|
-
// Best-effort telemetry — never fail the exchange over its own marker.
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* When the shared-state exchange last completed successfully, in epoch ms, or
|
|
48
|
-
* `null` if it never has on this box (no marker yet) or the marker is unreadable.
|
|
49
|
-
* Reads the default daemon-dir marker the periodic exchange writes.
|
|
50
|
-
*/
|
|
51
|
-
export function readLastSuccessfulExchangeMs() {
|
|
52
|
-
try {
|
|
53
|
-
const lockPath = path.join(getDaemonDir(), 'fleet-shared-repo-sync');
|
|
54
|
-
const raw = fs.readFileSync(exchangeMarkerPath(lockPath), 'utf-8');
|
|
55
|
-
const at = JSON.parse(raw).at;
|
|
56
|
-
return typeof at === 'number' && Number.isFinite(at) ? at : null;
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
function signalProcessTree(child, signal) {
|
|
63
|
-
const pid = child.pid;
|
|
64
|
-
if (!pid)
|
|
65
|
-
return;
|
|
66
|
-
if (process.platform === 'win32') {
|
|
67
|
-
// Windows has no process-group signal. taskkill is asynchronous here so a
|
|
68
|
-
// timeout path cannot replace one event-loop stall with another.
|
|
69
|
-
const killer = spawn('taskkill', ['/F', '/T', '/PID', String(pid)], {
|
|
70
|
-
stdio: 'ignore',
|
|
71
|
-
windowsHide: true,
|
|
72
|
-
});
|
|
73
|
-
killer.unref();
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
try {
|
|
77
|
-
// The child is spawned detached on POSIX, so -pid reaches git plus ssh.
|
|
78
|
-
process.kill(-pid, signal);
|
|
79
|
-
}
|
|
80
|
-
catch {
|
|
81
|
-
try {
|
|
82
|
-
child.kill(signal);
|
|
83
|
-
}
|
|
84
|
-
catch { /* already gone */ }
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
/** Run one real process asynchronously with a hard wall-clock/process-tree bound. */
|
|
88
|
-
export function runBoundedProcess(command, args, options) {
|
|
89
|
-
return new Promise((resolve) => {
|
|
90
|
-
const child = spawn(command, args, {
|
|
91
|
-
cwd: options.cwd,
|
|
92
|
-
env: options.env ?? process.env,
|
|
93
|
-
detached: process.platform !== 'win32',
|
|
94
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
95
|
-
windowsHide: true,
|
|
96
|
-
});
|
|
97
|
-
let stdout = '';
|
|
98
|
-
let stderr = '';
|
|
99
|
-
let settled = false;
|
|
100
|
-
let timedOut = false;
|
|
101
|
-
let overflow = false;
|
|
102
|
-
let killTimer = null;
|
|
103
|
-
const finish = (code) => {
|
|
104
|
-
if (settled)
|
|
105
|
-
return;
|
|
106
|
-
settled = true;
|
|
107
|
-
clearTimeout(timer);
|
|
108
|
-
if (killTimer)
|
|
109
|
-
clearTimeout(killTimer);
|
|
110
|
-
resolve({ code, stdout, stderr, timedOut });
|
|
111
|
-
};
|
|
112
|
-
const hardKill = () => {
|
|
113
|
-
signalProcessTree(child, 'SIGKILL');
|
|
114
|
-
if (process.platform === 'win32' && !killTimer) {
|
|
115
|
-
// taskkill owns the descendant cleanup; this fallback only guarantees
|
|
116
|
-
// the direct child's Promise settles if taskkill itself cannot run.
|
|
117
|
-
killTimer = setTimeout(() => {
|
|
118
|
-
if (!settled) {
|
|
119
|
-
try {
|
|
120
|
-
child.kill('SIGKILL');
|
|
121
|
-
}
|
|
122
|
-
catch { /* already gone */ }
|
|
123
|
-
}
|
|
124
|
-
}, FLEET_SHARED_REPO_KILL_GRACE_MS);
|
|
125
|
-
}
|
|
126
|
-
};
|
|
127
|
-
const timer = setTimeout(() => {
|
|
128
|
-
timedOut = true;
|
|
129
|
-
if (process.platform === 'win32') {
|
|
130
|
-
hardKill();
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
signalProcessTree(child, 'SIGTERM');
|
|
134
|
-
killTimer = setTimeout(() => {
|
|
135
|
-
if (!settled)
|
|
136
|
-
hardKill();
|
|
137
|
-
}, FLEET_SHARED_REPO_KILL_GRACE_MS);
|
|
138
|
-
}, Math.max(1, options.timeoutMs));
|
|
139
|
-
const append = (stream, chunk) => {
|
|
140
|
-
if (overflow)
|
|
141
|
-
return;
|
|
142
|
-
const value = String(chunk);
|
|
143
|
-
if (Buffer.byteLength(stdout) + Buffer.byteLength(stderr) + Buffer.byteLength(value) > FLEET_SHARED_REPO_OUTPUT_MAX_BYTES) {
|
|
144
|
-
overflow = true;
|
|
145
|
-
stderr += '\ngit output exceeded 1 MiB';
|
|
146
|
-
hardKill();
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
if (stream === 'stdout')
|
|
150
|
-
stdout += value;
|
|
151
|
-
else
|
|
152
|
-
stderr += value;
|
|
153
|
-
};
|
|
154
|
-
child.stdout?.on('data', (chunk) => append('stdout', chunk));
|
|
155
|
-
child.stderr?.on('data', (chunk) => append('stderr', chunk));
|
|
156
|
-
child.on('error', (err) => {
|
|
157
|
-
stderr += err.message;
|
|
158
|
-
finish(null);
|
|
159
|
-
});
|
|
160
|
-
child.on('close', (code) => finish(code));
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
function failure(command, result) {
|
|
164
|
-
const detail = result.timedOut
|
|
165
|
-
? `${command} timed out`
|
|
166
|
-
: result.stderr.trim() || result.stdout.trim() || `${command} exited ${result.code ?? 'without a status'}`;
|
|
167
|
-
return {
|
|
168
|
-
success: false,
|
|
169
|
-
committed: false,
|
|
170
|
-
pushed: false,
|
|
171
|
-
commit: null,
|
|
172
|
-
timedOut: result.timedOut,
|
|
173
|
-
skipped: null,
|
|
174
|
-
error: detail,
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
function isPushRace(result) {
|
|
178
|
-
const text = `${result.stdout}\n${result.stderr}`.toLowerCase();
|
|
179
|
-
return text.includes('non-fast-forward') || text.includes('fetch first') || text.includes('[rejected]');
|
|
180
|
-
}
|
|
181
|
-
function createdAutostashObject(result) {
|
|
182
|
-
return `${result.stdout}\n${result.stderr}`.match(/created autostash:\s*([0-9a-f]+)/i)?.[1] ?? null;
|
|
183
|
-
}
|
|
184
|
-
function retainedAutostash(stashList, objectId) {
|
|
185
|
-
if (stashList.code === 0) {
|
|
186
|
-
const entries = stashList.stdout.trim().split('\n').filter(Boolean).map((line) => {
|
|
187
|
-
const [ref = '', hash = '', ...subject] = line.split('\t');
|
|
188
|
-
return { ref, hash, subject: subject.join('\t') };
|
|
189
|
-
});
|
|
190
|
-
const matched = objectId
|
|
191
|
-
? entries.find((entry) => entry.hash.startsWith(objectId) || objectId.startsWith(entry.hash))
|
|
192
|
-
: entries.find((entry) => /autostash/i.test(entry.subject));
|
|
193
|
-
if (matched?.ref)
|
|
194
|
-
return { label: `${matched.ref} (${matched.hash.slice(0, 8)})`, ref: matched.ref };
|
|
195
|
-
}
|
|
196
|
-
return {
|
|
197
|
-
label: objectId ? `autostash object ${objectId}` : 'the retained autostash',
|
|
198
|
-
ref: null,
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
/**
|
|
202
|
-
* `git rebase` checks out its base (origin/<branch>) and aborts when an
|
|
203
|
-
* untracked working-tree file would be overwritten by a file that origin
|
|
204
|
-
* tracks. `--autostash` only sets aside *tracked* changes, so these untracked
|
|
205
|
-
* collisions wedge the rebase on every sync and the device silently falls
|
|
206
|
-
* hundreds of commits behind — the fleet-wide drift root cause (PHNX-3923).
|
|
207
|
-
*
|
|
208
|
-
* In this shared-state repo an untracked file that also exists on origin is a
|
|
209
|
-
* stale local snapshot: the canonical copy is on origin and every device
|
|
210
|
-
* republishes its own state (this device's own owned file is already committed
|
|
211
|
-
* before we get here, so it is tracked and never appears below). Drop the ones
|
|
212
|
-
* byte-identical to origin (lossless) and move any that differ into a backup
|
|
213
|
-
* dir beside the repo root — outside the repo's tracked tree, so the move
|
|
214
|
-
* cannot create a fresh collision — so nothing is silently destroyed. Then let
|
|
215
|
-
* the rebase check out origin's version. A file we cannot clear is left in
|
|
216
|
-
* place; the rebase may still abort, no worse than today and never losing data.
|
|
217
|
-
*
|
|
218
|
-
* The scan is scoped to the TRACKED ROOTS origin carries — the top-level entries
|
|
219
|
-
* of `git ls-tree --name-only origin/<branch>` — NOT the whole working tree
|
|
220
|
-
* (PHNX-4051). Only a file origin tracks can be overwritten by the rebase
|
|
221
|
-
* checkout, so a tracked root is the tightest superset of the collidable paths:
|
|
222
|
-
* every path origin tracks stays in scope (a stale untracked `projects/rush.yaml`
|
|
223
|
-
* under a tracked `projects/` root is still cleared), while an untracked-ONLY tree
|
|
224
|
-
* origin never tracks — artifacts-cli's revision store
|
|
225
|
-
* (`~/.agents/artifact-history/`, ~11k files / >1 MiB of paths) is the live case —
|
|
226
|
-
* is never a tracked root, so it never enters the `git ls-files --others` listing.
|
|
227
|
-
* An unbounded listing there blows past {@link FLEET_SHARED_REPO_OUTPUT_MAX_BYTES}
|
|
228
|
-
* and kills the exchange on every tick; scoping to the tracked roots keeps the
|
|
229
|
-
* PHNX-3923 coverage whole while an unrelated untracked tree cannot push the
|
|
230
|
-
* listing over the cap. `ls-tree`'s own output is bounded — it lists only the root
|
|
231
|
-
* entries, never the recursive tree.
|
|
232
|
-
*/
|
|
233
|
-
async function clearCollidingUntracked(git, root, branch) {
|
|
234
|
-
const tree = await git(['ls-tree', '--name-only', '-z', `origin/${branch}`]);
|
|
235
|
-
if (tree.code !== 0)
|
|
236
|
-
return { error: tree };
|
|
237
|
-
const trackedRoots = tree.stdout.split('\0').filter(Boolean);
|
|
238
|
-
if (trackedRoots.length === 0)
|
|
239
|
-
return { cleared: 0, backedUp: [], backupDir: null };
|
|
240
|
-
const others = await git(['ls-files', '--others', '--exclude-standard', '-z', '--', ...trackedRoots]);
|
|
241
|
-
if (others.code !== 0)
|
|
242
|
-
return { error: others };
|
|
243
|
-
const relPaths = others.stdout.split('\0').filter(Boolean);
|
|
244
|
-
let cleared = 0;
|
|
245
|
-
const backedUp = [];
|
|
246
|
-
let backupDir = null;
|
|
247
|
-
for (const rel of relPaths) {
|
|
248
|
-
// rev-parse doubles as the "does origin track this path" test (non-zero =
|
|
249
|
-
// absent) — only paths origin tracks are overwritten by the rebase checkout.
|
|
250
|
-
const originHash = await git(['rev-parse', `origin/${branch}:${rel}`]);
|
|
251
|
-
if (originHash.code !== 0 || !originHash.stdout.trim())
|
|
252
|
-
continue;
|
|
253
|
-
const abs = path.join(root, rel);
|
|
254
|
-
// Byte-exact identity via git blob SHAs. Comparing UTF-8-decoded strings
|
|
255
|
-
// would collapse distinct invalid bytes to U+FFFD and could delete
|
|
256
|
-
// non-identical content. `--no-filters` hashes the raw on-disk bytes (no
|
|
257
|
-
// clean/autocrlf normalization), so `identical` is true only when the local
|
|
258
|
-
// bytes exactly equal origin's stored blob — never a filter-normalized
|
|
259
|
-
// near-match. A hash-object failure (unreadable / vanished under a
|
|
260
|
-
// concurrent writer) means "leave it in place".
|
|
261
|
-
const localHash = await git(['hash-object', '--no-filters', '--', abs]);
|
|
262
|
-
if (localHash.code !== 0 || !localHash.stdout.trim())
|
|
263
|
-
continue;
|
|
264
|
-
const identical = originHash.stdout.trim() === localHash.stdout.trim();
|
|
265
|
-
try {
|
|
266
|
-
if (identical) {
|
|
267
|
-
fs.rmSync(abs, { force: true });
|
|
268
|
-
cleared++;
|
|
269
|
-
}
|
|
270
|
-
else {
|
|
271
|
-
if (!backupDir) {
|
|
272
|
-
// A sibling of the repo root: outside the tracked tree (so the move
|
|
273
|
-
// cannot create a fresh collision) yet colocated for easy recovery.
|
|
274
|
-
backupDir = path.join(`${root}-fleet-sync-backups`, String(Date.now()));
|
|
275
|
-
}
|
|
276
|
-
const dest = path.join(backupDir, rel);
|
|
277
|
-
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
278
|
-
fs.renameSync(abs, dest);
|
|
279
|
-
backedUp.push(rel);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
catch {
|
|
283
|
-
// Could not clear this collision; leave it in place.
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
return { cleared, backedUp, backupDir };
|
|
287
|
-
}
|
|
288
|
-
async function performFleetSharedRepoSync(root, device, timeoutMs) {
|
|
289
|
-
if (!fs.existsSync(path.join(root, '.git'))) {
|
|
290
|
-
return {
|
|
291
|
-
success: false,
|
|
292
|
-
committed: false,
|
|
293
|
-
pushed: false,
|
|
294
|
-
commit: null,
|
|
295
|
-
timedOut: false,
|
|
296
|
-
skipped: `user store is not git-backed: ${root}`,
|
|
297
|
-
error: null,
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
const deadline = Date.now() + timeoutMs;
|
|
301
|
-
const git = async (args, reserveMs = 0) => {
|
|
302
|
-
const remaining = deadline - Date.now() - reserveMs;
|
|
303
|
-
if (remaining <= 0)
|
|
304
|
-
return { code: null, stdout: '', stderr: '', timedOut: true };
|
|
305
|
-
return runBoundedProcess('git', args, {
|
|
306
|
-
cwd: root,
|
|
307
|
-
timeoutMs: remaining,
|
|
308
|
-
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
309
|
-
});
|
|
310
|
-
};
|
|
311
|
-
const branchResult = await git(['branch', '--show-current']);
|
|
312
|
-
if (branchResult.code !== 0)
|
|
313
|
-
return failure('git branch --show-current', branchResult);
|
|
314
|
-
const branch = branchResult.stdout.trim();
|
|
315
|
-
if (!branch) {
|
|
316
|
-
return {
|
|
317
|
-
success: false,
|
|
318
|
-
committed: false,
|
|
319
|
-
pushed: false,
|
|
320
|
-
commit: null,
|
|
321
|
-
timedOut: false,
|
|
322
|
-
skipped: null,
|
|
323
|
-
error: 'user store is on a detached HEAD',
|
|
324
|
-
};
|
|
325
|
-
}
|
|
326
|
-
const ownedFile = fleetSharedStatePath(device, root);
|
|
327
|
-
const relativeOwnedFile = path.relative(root, ownedFile).split(path.sep).join('/');
|
|
328
|
-
// The central `agents.yaml` is fleet-shared state too — native account rows
|
|
329
|
-
// land there (lib/state.ts writeMetaUnlocked) as a plain file write with no
|
|
330
|
-
// commit. Publishing only the per-device file and then `rebase --autostash`-ing
|
|
331
|
-
// over a dirty central file silently destroyed those names: every box lost its
|
|
332
|
-
// account names on its next daemon publish (PHNX-3887). Commit it alongside the
|
|
333
|
-
// device doc so the rebase carries the rows instead of stashing them.
|
|
334
|
-
const centralFile = path.join(root, 'agents.yaml');
|
|
335
|
-
const publishPaths = [relativeOwnedFile];
|
|
336
|
-
if (fs.existsSync(centralFile))
|
|
337
|
-
publishPaths.push('agents.yaml');
|
|
338
|
-
const existingPaths = publishPaths.filter(rel => fs.existsSync(path.join(root, rel)));
|
|
339
|
-
let committed = false;
|
|
340
|
-
if (existingPaths.length > 0) {
|
|
341
|
-
const status = await git(['status', '--porcelain=v1', '--', ...existingPaths]);
|
|
342
|
-
if (status.code !== 0)
|
|
343
|
-
return failure('git status', status);
|
|
344
|
-
if (status.stdout.trim()) {
|
|
345
|
-
const add = await git(['add', '--', ...existingPaths]);
|
|
346
|
-
if (add.code !== 0)
|
|
347
|
-
return failure('git add', add);
|
|
348
|
-
const commit = await git([
|
|
349
|
-
'-c', 'commit.gpgsign=false',
|
|
350
|
-
'commit', '--no-verify', '-m', `chore(devices): publish ${device} daemon state`,
|
|
351
|
-
'--', ...existingPaths,
|
|
352
|
-
]);
|
|
353
|
-
if (commit.code !== 0)
|
|
354
|
-
return failure('git commit', commit);
|
|
355
|
-
committed = true;
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
let untrackedCleared = 0;
|
|
359
|
-
const untrackedBackedUp = [];
|
|
360
|
-
let untrackedBackupDir;
|
|
361
|
-
for (let attempt = 1; attempt <= FLEET_SHARED_REPO_PUSH_ATTEMPTS; attempt++) {
|
|
362
|
-
const fetch = await git(['fetch', 'origin']);
|
|
363
|
-
if (fetch.code !== 0)
|
|
364
|
-
return { ...failure('git fetch', fetch), committed };
|
|
365
|
-
// Untracked files that origin tracks would abort the rebase's checkout
|
|
366
|
-
// (autostash only covers tracked changes). Clear them first — this is the
|
|
367
|
-
// fleet-drift root cause (PHNX-3923).
|
|
368
|
-
const reconcile = await clearCollidingUntracked(git, root, branch);
|
|
369
|
-
if ('error' in reconcile) {
|
|
370
|
-
return { ...failure('git ls-files --others', reconcile.error), committed };
|
|
371
|
-
}
|
|
372
|
-
untrackedCleared += reconcile.cleared;
|
|
373
|
-
untrackedBackedUp.push(...reconcile.backedUp);
|
|
374
|
-
if (reconcile.backupDir)
|
|
375
|
-
untrackedBackupDir = reconcile.backupDir;
|
|
376
|
-
// Keep enough of the same wall-clock bound available to remove a botched
|
|
377
|
-
// autostash pop from the operator's live checkout before returning.
|
|
378
|
-
const rebase = await git(['rebase', '--autostash', `origin/${branch}`], FLEET_SHARED_REPO_REBASE_CLEANUP_RESERVE_MS);
|
|
379
|
-
if (rebase.code !== 0) {
|
|
380
|
-
// Rebase can leave control files behind even when the child was killed.
|
|
381
|
-
// Abort is bounded by the same overall deadline and never hides the cause.
|
|
382
|
-
await git(['rebase', '--abort']);
|
|
383
|
-
return { ...failure('git rebase', rebase), committed };
|
|
384
|
-
}
|
|
385
|
-
// `git rebase --autostash` exits zero when the rebase succeeds but applying
|
|
386
|
-
// the autostash conflicts. Never push or leave those conflict markers in
|
|
387
|
-
// the operator's real ~/.agents checkout. Git retains the original changes
|
|
388
|
-
// in the stash on this path, so reset only the failed pop to rebased HEAD.
|
|
389
|
-
const unmerged = await git(['ls-files', '--unmerged']);
|
|
390
|
-
if (unmerged.code !== 0)
|
|
391
|
-
return { ...failure('git ls-files --unmerged', unmerged), committed };
|
|
392
|
-
if (unmerged.stdout.trim()) {
|
|
393
|
-
const autostashObject = createdAutostashObject(rebase);
|
|
394
|
-
const reset = await git(['reset', '--hard', 'HEAD']);
|
|
395
|
-
const cleaned = await git(['ls-files', '--unmerged']);
|
|
396
|
-
const stashList = await git(['stash', 'list', '--format=%gd%x09%H%x09%gs']);
|
|
397
|
-
const stash = retainedAutostash(stashList, autostashObject);
|
|
398
|
-
if (reset.code !== 0 || cleaned.code !== 0 || cleaned.stdout.trim()) {
|
|
399
|
-
const cleanup = reset.code !== 0
|
|
400
|
-
? failure('git reset --hard HEAD', reset).error
|
|
401
|
-
: cleaned.code !== 0
|
|
402
|
-
? failure('git ls-files --unmerged', cleaned).error
|
|
403
|
-
: 'unmerged index entries remain after git reset --hard HEAD';
|
|
404
|
-
return {
|
|
405
|
-
success: false,
|
|
406
|
-
committed,
|
|
407
|
-
pushed: false,
|
|
408
|
-
commit: null,
|
|
409
|
-
timedOut: reset.timedOut || cleaned.timedOut,
|
|
410
|
-
skipped: null,
|
|
411
|
-
error: `autostash pop conflicted; push refused, but cleanup failed: ${cleanup}. Original tracked changes remain safe in ${stash.label}`,
|
|
412
|
-
};
|
|
413
|
-
}
|
|
414
|
-
return {
|
|
415
|
-
success: false,
|
|
416
|
-
committed,
|
|
417
|
-
pushed: false,
|
|
418
|
-
commit: null,
|
|
419
|
-
timedOut: false,
|
|
420
|
-
skipped: null,
|
|
421
|
-
error: `autostash pop conflicted; push refused and the worktree was reset to rebased HEAD without conflict markers. Original tracked changes remain safe in ${stash.label}; inspect with ${stash.ref ? `git stash show -p ${stash.ref}` : 'git stash list'}`,
|
|
422
|
-
};
|
|
423
|
-
}
|
|
424
|
-
const push = await git(['push', '--', 'origin', branch]);
|
|
425
|
-
if (push.code === 0) {
|
|
426
|
-
const head = await git(['rev-parse', '--short=8', 'HEAD']);
|
|
427
|
-
return {
|
|
428
|
-
success: true,
|
|
429
|
-
committed,
|
|
430
|
-
pushed: committed || push.stdout.trim().length > 0 || push.stderr.includes('->'),
|
|
431
|
-
commit: head.code === 0 ? head.stdout.trim() : null,
|
|
432
|
-
timedOut: false,
|
|
433
|
-
skipped: null,
|
|
434
|
-
error: null,
|
|
435
|
-
untrackedCleared,
|
|
436
|
-
untrackedBackedUp,
|
|
437
|
-
untrackedBackupDir,
|
|
438
|
-
};
|
|
439
|
-
}
|
|
440
|
-
if (attempt === FLEET_SHARED_REPO_PUSH_ATTEMPTS || !isPushRace(push)) {
|
|
441
|
-
return { ...failure('git push', push), committed };
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
throw new Error('unreachable fleet shared repo push loop');
|
|
445
|
-
}
|
|
446
|
-
/**
|
|
447
|
-
* Commit/pull/push this device's shared state under one cross-process lock.
|
|
448
|
-
* Callers publish their local fields first and consume peer fields only after
|
|
449
|
-
* this resolves successfully.
|
|
450
|
-
*/
|
|
451
|
-
export async function syncFleetSharedStateRepo(options = {}) {
|
|
452
|
-
const root = options.userAgentsDir ?? getUserAgentsDir();
|
|
453
|
-
const device = options.device ?? machineId();
|
|
454
|
-
const timeoutMs = options.timeoutMs ?? FLEET_SHARED_REPO_SYNC_DEADLINE_MS;
|
|
455
|
-
const lockPath = options.lockPath ?? path.join(getDaemonDir(), 'fleet-shared-repo-sync');
|
|
456
|
-
ensureLockTarget(lockPath, '');
|
|
457
|
-
let release = null;
|
|
458
|
-
try {
|
|
459
|
-
release = await lockfile.lock(lockPath, {
|
|
460
|
-
stale: Math.max(10_000, timeoutMs + 5_000),
|
|
461
|
-
retries: { retries: 20, factor: 1, minTimeout: 100, maxTimeout: 100 },
|
|
462
|
-
onCompromised: logAndContinueOnLockCompromised('fleet shared repo sync'),
|
|
463
|
-
});
|
|
464
|
-
const result = await performFleetSharedRepoSync(root, device, timeoutMs);
|
|
465
|
-
// Record freshness only on a real success, beside the lock so a test-redirected
|
|
466
|
-
// lockPath keeps its marker isolated too (PHNX-4051).
|
|
467
|
-
if (result.success)
|
|
468
|
-
writeExchangeMarker(lockPath);
|
|
469
|
-
return result;
|
|
470
|
-
}
|
|
471
|
-
catch (err) {
|
|
472
|
-
return {
|
|
473
|
-
success: false,
|
|
474
|
-
committed: false,
|
|
475
|
-
pushed: false,
|
|
476
|
-
commit: null,
|
|
477
|
-
timedOut: false,
|
|
478
|
-
skipped: null,
|
|
479
|
-
error: err.message,
|
|
480
|
-
};
|
|
481
|
-
}
|
|
482
|
-
finally {
|
|
483
|
-
if (release) {
|
|
484
|
-
try {
|
|
485
|
-
await release();
|
|
486
|
-
}
|
|
487
|
-
catch { /* compromised/already released */ }
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
}
|