@bridge4dev/runner 0.46.1 → 0.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -2
- package/dist/adapters/claude.js +6 -2
- package/dist/adapters/codex.js +7 -4
- package/dist/agent-auto-update.d.ts +75 -0
- package/dist/agent-auto-update.js +134 -0
- package/dist/agent-binary.d.ts +48 -0
- package/dist/agent-binary.js +55 -0
- package/dist/agent-cleanup.d.ts +78 -0
- package/dist/agent-cleanup.js +184 -0
- package/dist/agent-install.d.ts +140 -0
- package/dist/agent-install.js +475 -0
- package/dist/agent-registry.d.ts +223 -0
- package/dist/agent-registry.js +131 -0
- package/dist/agent-versions.d.ts +93 -0
- package/dist/agent-versions.js +157 -0
- package/dist/auth-relay.d.ts +9 -1
- package/dist/auth-relay.js +3 -1
- package/dist/commit-message.js +5 -0
- package/dist/config.d.ts +44 -4
- package/dist/config.js +43 -0
- package/dist/index.js +46 -12
- package/dist/levels.d.ts +49 -0
- package/dist/levels.js +51 -0
- package/dist/protocol.d.ts +155 -28
- package/dist/protocol.js +33 -1
- package/dist/recipe-schema.d.ts +12 -12
- package/dist/self-update.d.ts +14 -0
- package/dist/self-update.js +45 -13
- package/dist/supervisor.d.ts +186 -2
- package/dist/supervisor.js +503 -14
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { AGENT_VERSION_PATTERN, agentByWireKey, compareAgentVersions, } from './agent-registry.js';
|
|
6
|
+
import { measureAgent } from './agent-versions.js';
|
|
7
|
+
import { ensureAgentPath, whichExecutable } from './environment.js';
|
|
8
|
+
import { log } from './log.js';
|
|
9
|
+
import { stateDir } from './paths.js';
|
|
10
|
+
import { describeCommandFailure, installIsWritable, installPrefixFor, resolveInstalledPackageDir, } from './self-update.js';
|
|
11
|
+
/**
|
|
12
|
+
* Run a child and wait for it, with stdin closed.
|
|
13
|
+
*
|
|
14
|
+
* The EOF is not decoration: `agent-versions.ts` had to learn it because
|
|
15
|
+
* `claude doctor` reads stdin and sits there until the timeout otherwise
|
|
16
|
+
* (measured, not assumed). This path spawns the same vendor CLI twice —
|
|
17
|
+
* `bash install.sh` execs `claude install <version>`, and the rollback runs it
|
|
18
|
+
* directly — and a block there would cost the full install timeout with the
|
|
19
|
+
* install lock held.
|
|
20
|
+
*/
|
|
21
|
+
const runChild = (file, args, options) => new Promise((resolve, reject) => {
|
|
22
|
+
const child = execFile(file, args, { timeout: options.timeout, env: options.env, maxBuffer: 4_000_000 }, (error, stdout, stderr) => {
|
|
23
|
+
if (error)
|
|
24
|
+
reject(error);
|
|
25
|
+
else
|
|
26
|
+
resolve({ stdout, stderr });
|
|
27
|
+
});
|
|
28
|
+
child.stdin?.end();
|
|
29
|
+
});
|
|
30
|
+
/**
|
|
31
|
+
* Generous on purpose. The API stops waiting after 240s and reports «installing,
|
|
32
|
+
* waiting for the report», with the `agent_versions` frame as the late source of
|
|
33
|
+
* truth — so the runner giving up early would turn a slow 300 MB download into a
|
|
34
|
+
* failure that never happened.
|
|
35
|
+
*/
|
|
36
|
+
const INSTALL_TIMEOUT_MS = 600_000;
|
|
37
|
+
/**
|
|
38
|
+
* Both agents ship 200–330 MB, and the native installer needs room for the new
|
|
39
|
+
* file beside the old one. Below this we refuse with a number rather than let
|
|
40
|
+
* the machine fill up — `self-update.ts` has no such check, and this is the
|
|
41
|
+
* first install path in the runner that downloads that much.
|
|
42
|
+
*/
|
|
43
|
+
const MIN_FREE_BYTES = 1024 * 1024 * 1024;
|
|
44
|
+
/** A shell script is a few kilobytes; a minute is a slow network, not a hang. */
|
|
45
|
+
const DOWNLOAD_TIMEOUT_MS = 60_000;
|
|
46
|
+
/**
|
|
47
|
+
* Anthropic's native installer keeps every version it has ever downloaded as a
|
|
48
|
+
* separate file here and switches a launcher between them; `claude install
|
|
49
|
+
* <version>` is how you move the launcher, and that is what makes a rollback
|
|
50
|
+
* possible at all for a `script`-kind agent.
|
|
51
|
+
*
|
|
52
|
+
* Deliberately NOT a registry field: the registry is mirrored character for
|
|
53
|
+
* character into `@devbridge/shared`, and today `script` has exactly one member.
|
|
54
|
+
* When a second one arrives (Antigravity ships a script installer too) this
|
|
55
|
+
* becomes a per-agent field and the mirror grows with it.
|
|
56
|
+
*/
|
|
57
|
+
export const NATIVE_VERSIONS_DIR = ['.local', 'share', 'claude', 'versions'];
|
|
58
|
+
/**
|
|
59
|
+
* How the network is reached, on a machine that does not reach it directly.
|
|
60
|
+
*
|
|
61
|
+
* npm reads a proxy out of its own config as well; `curl` inside a vendor's
|
|
62
|
+
* `install.sh` reads ONLY the environment. Stripping these was the difference
|
|
63
|
+
* between «Update runner» working behind an outbound proxy and «Install Claude»
|
|
64
|
+
* failing there with a bare network error nobody could act on (QA of stage B,
|
|
65
|
+
* m8).
|
|
66
|
+
*
|
|
67
|
+
* Passed through rather than invented, and only these: they say WHERE to go, not
|
|
68
|
+
* who we are. No credential of ours is in this list, and the payload is still
|
|
69
|
+
* the registry's — the URL and the package name come from the build.
|
|
70
|
+
*/
|
|
71
|
+
const NETWORK_ENV_KEYS = [
|
|
72
|
+
'HTTP_PROXY',
|
|
73
|
+
'HTTPS_PROXY',
|
|
74
|
+
'http_proxy',
|
|
75
|
+
'https_proxy',
|
|
76
|
+
'NO_PROXY',
|
|
77
|
+
'no_proxy',
|
|
78
|
+
'ALL_PROXY',
|
|
79
|
+
'all_proxy',
|
|
80
|
+
'NODE_EXTRA_CA_CERTS',
|
|
81
|
+
'SSL_CERT_FILE',
|
|
82
|
+
'SSL_CERT_DIR',
|
|
83
|
+
];
|
|
84
|
+
/**
|
|
85
|
+
* npm and the vendor scripts need PATH, HOME and a writable cache; everything
|
|
86
|
+
* else is stripped so provider credentials never reach a child that touches the
|
|
87
|
+
* network, and a stray `npm_config_prefix` cannot redirect the install.
|
|
88
|
+
*/
|
|
89
|
+
function installEnv(homeDir) {
|
|
90
|
+
const env = {
|
|
91
|
+
PATH: process.env['PATH'] ?? '/usr/local/bin:/usr/bin:/bin',
|
|
92
|
+
HOME: homeDir,
|
|
93
|
+
npm_config_progress: 'false',
|
|
94
|
+
npm_config_fund: 'false',
|
|
95
|
+
npm_config_audit: 'false',
|
|
96
|
+
};
|
|
97
|
+
if (process.env['XDG_CACHE_HOME'])
|
|
98
|
+
env['XDG_CACHE_HOME'] = process.env['XDG_CACHE_HOME'];
|
|
99
|
+
for (const key of NETWORK_ENV_KEYS) {
|
|
100
|
+
const value = process.env[key];
|
|
101
|
+
if (value)
|
|
102
|
+
env[key] = value;
|
|
103
|
+
}
|
|
104
|
+
return env;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* `--include=optional` is not a copy-paste slip next to C5, which takes the flag
|
|
108
|
+
* OFF the runner's own install. The two say the same thing about different
|
|
109
|
+
* packages: the runner no longer wants Claude's optional platform binary, while
|
|
110
|
+
* `@openai/codex` keeps its 320 MB executable in exactly such an optional
|
|
111
|
+
* package (`@openai/codex-linux-x64`). Dropping it here would install a shim
|
|
112
|
+
* that cannot run, and npm would still exit 0 — гоча #297, second helping.
|
|
113
|
+
*
|
|
114
|
+
* `--ignore-scripts` is safe and checked: neither agent package declares any
|
|
115
|
+
* lifecycle script.
|
|
116
|
+
*/
|
|
117
|
+
function npmInstallArgs(packageName, version, prefix) {
|
|
118
|
+
return [
|
|
119
|
+
'install',
|
|
120
|
+
'-g',
|
|
121
|
+
'--ignore-scripts',
|
|
122
|
+
'--include=optional',
|
|
123
|
+
'--loglevel=error',
|
|
124
|
+
...(prefix ? ['--prefix', prefix] : []),
|
|
125
|
+
`${packageName}@${version}`,
|
|
126
|
+
];
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The command a human runs when we will not: an agent someone else's package
|
|
130
|
+
* manager owns, or a machine this daemon cannot write to.
|
|
131
|
+
*/
|
|
132
|
+
export function manualAgentInstallCommand(runtime, version) {
|
|
133
|
+
return runtime.install.kind === 'npm-global'
|
|
134
|
+
? `npm install -g --include=optional ${runtime.install.package}@${version}`
|
|
135
|
+
: `curl -fsSL ${runtime.install.url} | bash -s ${version}`;
|
|
136
|
+
}
|
|
137
|
+
/** Free bytes on the filesystem `target` lives on; null when we cannot tell. */
|
|
138
|
+
function freeBytesFor(target) {
|
|
139
|
+
try {
|
|
140
|
+
const stat = fs.statfsSync(target);
|
|
141
|
+
return Number(stat.bavail) * Number(stat.bsize);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* The first existing directory of the chain — statfs needs a path that is
|
|
149
|
+
* actually there, and the place we are about to write into may not be yet.
|
|
150
|
+
*/
|
|
151
|
+
function existingAncestor(candidates) {
|
|
152
|
+
for (const candidate of candidates) {
|
|
153
|
+
if (fs.existsSync(candidate))
|
|
154
|
+
return candidate;
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
/** Where this kind of install writes, for the space and ownership checks. */
|
|
159
|
+
function installTarget(runtime, homeDir, prefix) {
|
|
160
|
+
if (runtime.install.kind === 'npm-global') {
|
|
161
|
+
const lib = prefix ? path.join(prefix, 'lib', 'node_modules') : null;
|
|
162
|
+
return {
|
|
163
|
+
probe: existingAncestor([...(lib ? [lib] : []), ...(prefix ? [prefix] : []), homeDir]),
|
|
164
|
+
describe: prefix ?? 'the npm prefix',
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
const share = path.join(homeDir, '.local', 'share');
|
|
168
|
+
return {
|
|
169
|
+
probe: existingAncestor([share, path.join(homeDir, '.local'), homeDir]),
|
|
170
|
+
describe: homeDir,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Can this daemon write where this agent has to go?
|
|
175
|
+
*
|
|
176
|
+
* Checked per kind because the two kinds land in different places: a global npm
|
|
177
|
+
* package goes into the runner's own npm prefix (so the runner's own writability
|
|
178
|
+
* check is exactly the right question), while the native installer writes only
|
|
179
|
+
* into the daemon user's home.
|
|
180
|
+
*/
|
|
181
|
+
function targetIsWritable(runtime, homeDir, packageDir) {
|
|
182
|
+
if (runtime.install.kind === 'npm-global')
|
|
183
|
+
return installIsWritable(packageDir);
|
|
184
|
+
// The native installer creates these if they are missing, so an ABSENT
|
|
185
|
+
// directory is not a refusal — but an existing one owned by somebody else is,
|
|
186
|
+
// and that is the real shape: Claude first installed as root, the daemon
|
|
187
|
+
// later moved to a dedicated user. Checking only `$HOME` passed that machine
|
|
188
|
+
// and let it fail with an EACCES from inside the vendor's script, which is
|
|
189
|
+
// precisely the error this section exists to replace with our own.
|
|
190
|
+
for (const target of [
|
|
191
|
+
homeDir,
|
|
192
|
+
path.join(homeDir, '.local', 'bin'),
|
|
193
|
+
path.join(homeDir, ...NATIVE_VERSIONS_DIR.slice(0, -1)),
|
|
194
|
+
]) {
|
|
195
|
+
if (target !== homeDir && !fs.existsSync(target))
|
|
196
|
+
continue;
|
|
197
|
+
try {
|
|
198
|
+
fs.accessSync(target, fs.constants.W_OK);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
/** Versions the native installer has on disk, newest first. */
|
|
207
|
+
export function nativeVersionsOnDisk(homeDir) {
|
|
208
|
+
const dir = path.join(homeDir, ...NATIVE_VERSIONS_DIR);
|
|
209
|
+
try {
|
|
210
|
+
return fs
|
|
211
|
+
.readdirSync(dir)
|
|
212
|
+
.filter((name) => new RegExp(AGENT_VERSION_PATTERN).test(name))
|
|
213
|
+
.sort((a, b) => compareAgentVersions(b, a) ?? 0);
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return [];
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Fetch the vendor's installer script to a file.
|
|
221
|
+
*
|
|
222
|
+
* `curl` rather than Node's `fetch`, and behind an outbound proxy that is the
|
|
223
|
+
* whole difference between working and not: undici reads no proxy from the
|
|
224
|
+
* environment, so a machine that reaches the internet only through
|
|
225
|
+
* `HTTPS_PROXY` failed here — while the script it was fetching would itself
|
|
226
|
+
* have reached the network perfectly well, because `curl` inside it does read
|
|
227
|
+
* the environment. Fixing one half and not the other would have been an
|
|
228
|
+
* improvement nobody could use.
|
|
229
|
+
*
|
|
230
|
+
* `curl` is not an extra dependency: the vendor's script runs it on the next
|
|
231
|
+
* line, so a machine without it cannot install this agent either way.
|
|
232
|
+
*
|
|
233
|
+
* Downloaded to a file and never piped into a shell, exactly as before — a
|
|
234
|
+
* truncated transfer then fails to parse instead of running half a script.
|
|
235
|
+
* `--fail` turns an HTTP error into a non-zero exit rather than an error page
|
|
236
|
+
* saved as an installer; `--proto =https` refuses a redirect that leaves TLS.
|
|
237
|
+
*/
|
|
238
|
+
async function downloadToFile(url, destination, exec, env) {
|
|
239
|
+
await exec('curl', [
|
|
240
|
+
'--fail',
|
|
241
|
+
'--silent',
|
|
242
|
+
'--show-error',
|
|
243
|
+
'--location',
|
|
244
|
+
'--proto',
|
|
245
|
+
'=https',
|
|
246
|
+
'--output',
|
|
247
|
+
destination,
|
|
248
|
+
url,
|
|
249
|
+
], { timeout: DOWNLOAD_TIMEOUT_MS, env });
|
|
250
|
+
const written = fs.statSync(destination);
|
|
251
|
+
if (written.size === 0)
|
|
252
|
+
throw new Error('the installer script came back empty');
|
|
253
|
+
fs.chmodSync(destination, 0o700);
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Install or update one agent CLI.
|
|
257
|
+
*
|
|
258
|
+
* Never throws: every failure comes back as an outcome, because the caller is a
|
|
259
|
+
* command handler whose reply is the only thing the dashboard will ever see.
|
|
260
|
+
*/
|
|
261
|
+
export async function installAgent(options) {
|
|
262
|
+
const exec = options.exec ?? runChild;
|
|
263
|
+
const measure = options.measure ?? ((runtime) => measureAgent(runtime));
|
|
264
|
+
const homeDir = options.homeDir ?? process.env['HOME'] ?? os.homedir();
|
|
265
|
+
const free = options.freeBytes ?? freeBytesFor;
|
|
266
|
+
const download = options.download ??
|
|
267
|
+
((url, destination) => downloadToFile(url, destination, exec, installEnv(homeDir)));
|
|
268
|
+
const refreshPath = options.refreshPath ?? ensureAgentPath;
|
|
269
|
+
const fail = (detail, extra = {}) => ({
|
|
270
|
+
ok: false,
|
|
271
|
+
agent: options.agent,
|
|
272
|
+
fromVersion: null,
|
|
273
|
+
detail,
|
|
274
|
+
...extra,
|
|
275
|
+
});
|
|
276
|
+
// ── Refusals, all of them before anything is touched ──────────────────────
|
|
277
|
+
const runtime = agentByWireKey(options.agent);
|
|
278
|
+
if (!runtime)
|
|
279
|
+
return fail(`\`${options.agent}\` is not an agent this runner knows`);
|
|
280
|
+
if (!new RegExp(AGENT_VERSION_PATTERN).test(options.version)) {
|
|
281
|
+
return fail(`\`${options.version}\` is not a version number`);
|
|
282
|
+
}
|
|
283
|
+
const packageDir = options.packageDir === undefined ? resolveInstalledPackageDir() : options.packageDir;
|
|
284
|
+
const prefix = installPrefixFor(packageDir);
|
|
285
|
+
const before = await measure(runtime);
|
|
286
|
+
const fromVersion = before.version;
|
|
287
|
+
// A binary that is there but would not answer. Installing over it would throw
|
|
288
|
+
// away a version we cannot name, and «roll back to null» is not a rollback.
|
|
289
|
+
if (before.probeFailed) {
|
|
290
|
+
return fail(`${runtime.label} is installed on this server but does not report its version, ` +
|
|
291
|
+
'so there would be no way back if the new one turned out to be broken. ' +
|
|
292
|
+
`Check it on the server with \`${runtime.bin} ${runtime.versionProbe.argv.join(' ')}\`.`, { fromVersion });
|
|
293
|
+
}
|
|
294
|
+
// Someone else's package manager owns this copy. Replacing it would leave two
|
|
295
|
+
// installations racing for the same command name.
|
|
296
|
+
if (fromVersion !== null && before.managedBy !== runtime.install.managedBy) {
|
|
297
|
+
return fail(`${runtime.label} on this server was installed with ${before.managedBy}, ` +
|
|
298
|
+
'and this runner only manages its own installs. Update it on the server with:\n ' +
|
|
299
|
+
manualAgentInstallCommand(runtime, options.version), { fromVersion });
|
|
300
|
+
}
|
|
301
|
+
if (fromVersion === options.version) {
|
|
302
|
+
return {
|
|
303
|
+
ok: true,
|
|
304
|
+
agent: runtime.wireKey,
|
|
305
|
+
fromVersion,
|
|
306
|
+
toVersion: options.version,
|
|
307
|
+
detail: `${runtime.label} is already ${options.version}`,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
if (fromVersion !== null &&
|
|
311
|
+
!options.allowDowngrade &&
|
|
312
|
+
(compareAgentVersions(options.version, fromVersion) ?? 0) < 0) {
|
|
313
|
+
return fail(`${options.version} is older than the ${fromVersion} already on this server`, {
|
|
314
|
+
fromVersion,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
if (!targetIsWritable(runtime, homeDir, packageDir)) {
|
|
318
|
+
return fail(`${runtime.label} would be installed into ${installTarget(runtime, homeDir, prefix).describe}, ` +
|
|
319
|
+
`which does not belong to ${os.userInfo().username} — the user this runner runs as. ` +
|
|
320
|
+
'Run this on the server instead:\n ' +
|
|
321
|
+
manualAgentInstallCommand(runtime, options.version), { fromVersion });
|
|
322
|
+
}
|
|
323
|
+
const target = installTarget(runtime, homeDir, prefix);
|
|
324
|
+
const freeBytes = target.probe ? free(target.probe) : null;
|
|
325
|
+
// null means «this platform cannot tell», which is a reason to go ahead rather
|
|
326
|
+
// than to refuse every install — the same call the verify runner makes.
|
|
327
|
+
if (freeBytes !== null && freeBytes < MIN_FREE_BYTES) {
|
|
328
|
+
return fail(`Only ${Math.round(freeBytes / 1024 / 1024)} MB free where ${runtime.label} would go — ` +
|
|
329
|
+
`it needs at least ${Math.round(MIN_FREE_BYTES / 1024 / 1024)} MB. Free some space and try again.`, { fromVersion });
|
|
330
|
+
}
|
|
331
|
+
// ── The install itself ────────────────────────────────────────────────────
|
|
332
|
+
// What a rollback would restore. For `npm-global` this is the version number,
|
|
333
|
+
// not a packed tarball: the plan wrote `npm pack`, copying the runner's own
|
|
334
|
+
// update, but the runner packs itself because the registry has no copy of the
|
|
335
|
+
// build it is replacing. An agent version DID come from the registry, and
|
|
336
|
+
// packing its installed directory would produce an 8 KB shim without the
|
|
337
|
+
// platform binary anyway — so the number is both the honest artefact and the
|
|
338
|
+
// sufficient one. The safety property the plan asked for is kept exactly:
|
|
339
|
+
// nothing is installed over a version we could not name (see `probeFailed`).
|
|
340
|
+
const rollbackVersion = fromVersion;
|
|
341
|
+
try {
|
|
342
|
+
if (runtime.install.kind === 'npm-global') {
|
|
343
|
+
await exec('npm', npmInstallArgs(runtime.install.package, options.version, prefix), {
|
|
344
|
+
timeout: INSTALL_TIMEOUT_MS,
|
|
345
|
+
env: installEnv(homeDir),
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
const scriptDir = path.join(stateDir(), 'agent-install');
|
|
350
|
+
fs.mkdirSync(scriptDir, { recursive: true, mode: 0o700 });
|
|
351
|
+
const scriptPath = path.join(scriptDir, `${runtime.wireKey}-install.sh`);
|
|
352
|
+
// Downloaded to a file and executed, never piped into a shell: a truncated
|
|
353
|
+
// transfer then fails to parse instead of running half a script.
|
|
354
|
+
await download(runtime.install.url, scriptPath);
|
|
355
|
+
try {
|
|
356
|
+
await exec('bash', [scriptPath, options.version], {
|
|
357
|
+
timeout: INSTALL_TIMEOUT_MS,
|
|
358
|
+
env: installEnv(homeDir),
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
finally {
|
|
362
|
+
// In a `finally`, or every failed install leaves a vendor script behind
|
|
363
|
+
// in the runner's state directory.
|
|
364
|
+
fs.rmSync(scriptPath, { force: true });
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
catch (error) {
|
|
369
|
+
const detail = describeCommandFailure(error);
|
|
370
|
+
log.warn('agent-install: install failed', { agent: runtime.wireKey, error: detail });
|
|
371
|
+
return fail(`Installing ${runtime.label} ${options.version} failed: ${detail}`, {
|
|
372
|
+
fromVersion,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
// ── The smoke test: ask the binary, not npm ───────────────────────────────
|
|
376
|
+
// Look again for directories the install has just created. `ensureAgentPath`
|
|
377
|
+
// runs once at daemon start and adds `~/.local/bin` only if it EXISTS then —
|
|
378
|
+
// and on a machine getting Claude for the first time it does not. Without
|
|
379
|
+
// this the smoke test below would search a PATH that cannot contain the file
|
|
380
|
+
// we just wrote, call a good install a failure, and roll it back. That is the
|
|
381
|
+
// headline case of this whole feature (Р11), not a corner of it.
|
|
382
|
+
refreshPath();
|
|
383
|
+
const after = await measure(runtime);
|
|
384
|
+
const landed = after.version === options.version && !after.probeFailed;
|
|
385
|
+
if (landed) {
|
|
386
|
+
log.info('agent-install: installed', {
|
|
387
|
+
agent: runtime.wireKey,
|
|
388
|
+
from: fromVersion,
|
|
389
|
+
to: options.version,
|
|
390
|
+
});
|
|
391
|
+
return {
|
|
392
|
+
ok: true,
|
|
393
|
+
agent: runtime.wireKey,
|
|
394
|
+
fromVersion,
|
|
395
|
+
toVersion: options.version,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
const wrong = after.probeFailed
|
|
399
|
+
? `${runtime.label} does not run after the install`
|
|
400
|
+
: after.version === null
|
|
401
|
+
? `${runtime.label} is not on this server after the install`
|
|
402
|
+
: `${runtime.label} reports ${after.version} after installing ${options.version}`;
|
|
403
|
+
log.error('agent-install: the new version did not come up', {
|
|
404
|
+
agent: runtime.wireKey,
|
|
405
|
+
wanted: options.version,
|
|
406
|
+
got: after.version,
|
|
407
|
+
});
|
|
408
|
+
const rolledBack = await rollBack({
|
|
409
|
+
runtime,
|
|
410
|
+
version: rollbackVersion,
|
|
411
|
+
exec,
|
|
412
|
+
prefix,
|
|
413
|
+
homeDir,
|
|
414
|
+
measure,
|
|
415
|
+
});
|
|
416
|
+
return fail(rollbackVersion === null
|
|
417
|
+
? `${wrong}. Nothing was replaced — the server is as it was.`
|
|
418
|
+
: rolledBack
|
|
419
|
+
? `${wrong}. The previous version (${rollbackVersion}) was put back.`
|
|
420
|
+
: `${wrong}, and putting ${rollbackVersion} back failed too. Fix it on the server with:\n ` +
|
|
421
|
+
manualAgentInstallCommand(runtime, rollbackVersion), { fromVersion, ...(rollbackVersion !== null ? { rolledBack } : {}) });
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Put the previous version back, by the route its own installer understands.
|
|
425
|
+
*
|
|
426
|
+
* Returns whether the machine really ended up on that version — asked of the
|
|
427
|
+
* binary again, because a rollback that silently failed is the one state worse
|
|
428
|
+
* than the broken install it was meant to undo.
|
|
429
|
+
*/
|
|
430
|
+
async function rollBack(input) {
|
|
431
|
+
const { runtime, version, exec, prefix, homeDir, measure } = input;
|
|
432
|
+
if (version === null)
|
|
433
|
+
return false;
|
|
434
|
+
try {
|
|
435
|
+
if (runtime.install.kind === 'npm-global') {
|
|
436
|
+
await exec('npm', npmInstallArgs(runtime.install.package, version, prefix), {
|
|
437
|
+
timeout: INSTALL_TIMEOUT_MS,
|
|
438
|
+
env: installEnv(homeDir),
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
// The native installer keeps the file; `claude install <version>` only has
|
|
443
|
+
// to point the launcher back at it. Prefer the version file itself over
|
|
444
|
+
// the launcher, which is exactly what the failed install may have broken.
|
|
445
|
+
const onDisk = nativeVersionsOnDisk(homeDir).includes(version)
|
|
446
|
+
? path.join(homeDir, ...NATIVE_VERSIONS_DIR, version)
|
|
447
|
+
: whichExecutable(runtime.bin);
|
|
448
|
+
if (!onDisk)
|
|
449
|
+
return false;
|
|
450
|
+
await exec(onDisk, ['install', version], {
|
|
451
|
+
timeout: INSTALL_TIMEOUT_MS,
|
|
452
|
+
env: installEnv(homeDir),
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
catch (error) {
|
|
457
|
+
log.error('agent-install: rollback failed', {
|
|
458
|
+
agent: runtime.wireKey,
|
|
459
|
+
version,
|
|
460
|
+
error: describeCommandFailure(error),
|
|
461
|
+
});
|
|
462
|
+
return false;
|
|
463
|
+
}
|
|
464
|
+
const restored = await measure(runtime);
|
|
465
|
+
return restored.version === version;
|
|
466
|
+
}
|
|
467
|
+
export const AGENT_INSTALL_INTERNALS = {
|
|
468
|
+
INSTALL_TIMEOUT_MS,
|
|
469
|
+
MIN_FREE_BYTES,
|
|
470
|
+
downloadToFile,
|
|
471
|
+
installEnv,
|
|
472
|
+
nativeVersionsOnDisk,
|
|
473
|
+
npmInstallArgs,
|
|
474
|
+
};
|
|
475
|
+
//# sourceMappingURL=agent-install.js.map
|