borgmcp 2.2.0 → 2.3.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 +33 -0
- package/dist/claude.d.ts.map +1 -1
- package/dist/claude.js +4 -0
- package/dist/claude.js.map +1 -1
- package/dist/cli-help.d.ts +2 -0
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +17 -0
- package/dist/cli-help.js.map +1 -1
- package/dist/unknown-subcommand.d.ts +1 -1
- package/dist/unknown-subcommand.d.ts.map +1 -1
- package/dist/unknown-subcommand.js +1 -0
- package/dist/unknown-subcommand.js.map +1 -1
- package/dist/update-cmd.d.ts +58 -0
- package/dist/update-cmd.d.ts.map +1 -0
- package/dist/update-cmd.js +837 -0
- package/dist/update-cmd.js.map +1 -0
- package/docs/EXTRACTION_PROVENANCE.md +3 -3
- package/docs/LOCAL_SERVER.md +39 -0
- package/docs/RELEASING.md +123 -1
- package/package.json +6 -3
- package/src/claude.ts +4 -0
- package/src/cli-help.ts +20 -0
- package/src/unknown-subcommand.ts +1 -0
- package/src/update-cmd.ts +1040 -0
|
@@ -0,0 +1,837 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { readFile, realpath, stat } from 'node:fs/promises';
|
|
3
|
+
import { constants } from 'node:os';
|
|
4
|
+
import { isAbsolute, join, relative, resolve } from 'node:path';
|
|
5
|
+
import { createInterface } from 'node:readline/promises';
|
|
6
|
+
import which from 'which';
|
|
7
|
+
import { updateHelpText } from './cli-help.js';
|
|
8
|
+
import { preflightBorgServerTag } from './server-handshake.js';
|
|
9
|
+
import { loadBorgServerTrust } from './server-trust.js';
|
|
10
|
+
const CLIENT_PACKAGE = 'borgmcp';
|
|
11
|
+
const SERVER_PACKAGE = 'borgmcp-server';
|
|
12
|
+
const SHARED_PACKAGE = 'borgmcp-shared';
|
|
13
|
+
const CANONICAL_NPM_REGISTRY = 'https://registry.npmjs.org/';
|
|
14
|
+
const REENTRY_ENV = 'BORG_UPDATE_REENTRY';
|
|
15
|
+
const MAX_CAPTURE_BYTES = 1024 * 1024;
|
|
16
|
+
const EXACT_SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
17
|
+
function signalExitCode(error) {
|
|
18
|
+
return error instanceof CommandSignalError ? error.exitCode : null;
|
|
19
|
+
}
|
|
20
|
+
function errorMessage(error, fallback) {
|
|
21
|
+
return error instanceof Error ? error.message : fallback;
|
|
22
|
+
}
|
|
23
|
+
function renderReentryPreflightFailure(error, target) {
|
|
24
|
+
return (`${errorMessage(error, 'Update preflight failed')}\n` +
|
|
25
|
+
`Observed update state:\n` +
|
|
26
|
+
` client: ${CLIENT_PACKAGE}@${target.clientVersion} installed and verified before re-entry\n` +
|
|
27
|
+
` server controller: not changed by this continuation\n` +
|
|
28
|
+
` prepared runtime: not inspected\n` +
|
|
29
|
+
` running runtime: not inspected\n` +
|
|
30
|
+
`Server mutation was not attempted.\n` +
|
|
31
|
+
`Retry with: borg update --yes\n`);
|
|
32
|
+
}
|
|
33
|
+
function renderServerState(client, server, status, update) {
|
|
34
|
+
const updateLine = update === null
|
|
35
|
+
? 'unavailable'
|
|
36
|
+
: update.status === 'failed'
|
|
37
|
+
? `failed ${update.errorCode} (${update.recovery})`
|
|
38
|
+
: `${update.status} ${update.artifact} (${update.artifactIntegrity})`;
|
|
39
|
+
return (`Observed update state:\n` +
|
|
40
|
+
` client (last verified): ${client.name}@${client.version} (${SHARED_PACKAGE}@${client.sharedVersion})\n` +
|
|
41
|
+
` server controller (last verified): ${server.name}@${server.version} (${SHARED_PACKAGE}@${server.sharedVersion})\n` +
|
|
42
|
+
` prepared runtime: ${status?.preparedRuntime ?? 'unavailable'}\n` +
|
|
43
|
+
` prepared integrity: ${status?.preparedIntegrity ?? 'unavailable'}\n` +
|
|
44
|
+
` running runtime: ${status?.runningRuntime ?? 'unavailable'}\n` +
|
|
45
|
+
` running integrity: ${status?.runningIntegrity ?? 'unavailable'}\n` +
|
|
46
|
+
` server update: ${updateLine}\n`);
|
|
47
|
+
}
|
|
48
|
+
function isExactSemver(value) {
|
|
49
|
+
return EXACT_SEMVER.test(value);
|
|
50
|
+
}
|
|
51
|
+
function isCanonicalSha512Integrity(value) {
|
|
52
|
+
if (!value.startsWith('sha512-') || value.includes(' '))
|
|
53
|
+
return false;
|
|
54
|
+
const encoded = value.slice('sha512-'.length);
|
|
55
|
+
try {
|
|
56
|
+
const bytes = Buffer.from(encoded, 'base64');
|
|
57
|
+
return bytes.length === 64 && bytes.toString('base64') === encoded;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function isHttpsOrigin(value) {
|
|
64
|
+
try {
|
|
65
|
+
const url = new URL(value);
|
|
66
|
+
return (url.protocol === 'https:' &&
|
|
67
|
+
url.origin === value &&
|
|
68
|
+
url.username === '' &&
|
|
69
|
+
url.password === '' &&
|
|
70
|
+
url.pathname === '/' &&
|
|
71
|
+
url.search === '' &&
|
|
72
|
+
url.hash === '');
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export function parseUpdateArgs(args, reentryAuthorized = false) {
|
|
79
|
+
let yes = false;
|
|
80
|
+
let help = false;
|
|
81
|
+
let clientVersion;
|
|
82
|
+
let serverVersion;
|
|
83
|
+
let serverPresent;
|
|
84
|
+
let hasInternalOption = false;
|
|
85
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
86
|
+
const arg = args[index];
|
|
87
|
+
if (arg === '--yes' || arg === '-y') {
|
|
88
|
+
yes = true;
|
|
89
|
+
}
|
|
90
|
+
else if (arg === '--help' || arg === '-h') {
|
|
91
|
+
help = true;
|
|
92
|
+
}
|
|
93
|
+
else if (arg === '--target-client' || arg === '--target-server' || arg === '--server-present') {
|
|
94
|
+
hasInternalOption = true;
|
|
95
|
+
const value = args[index + 1];
|
|
96
|
+
if (!value)
|
|
97
|
+
return { ok: false, error: `${arg} requires a value` };
|
|
98
|
+
index += 1;
|
|
99
|
+
if (arg === '--target-client')
|
|
100
|
+
clientVersion = value;
|
|
101
|
+
if (arg === '--target-server')
|
|
102
|
+
serverVersion = value;
|
|
103
|
+
if (arg === '--server-present') {
|
|
104
|
+
if (value !== 'yes' && value !== 'no') {
|
|
105
|
+
return { ok: false, error: '--server-present requires yes or no' };
|
|
106
|
+
}
|
|
107
|
+
serverPresent = value === 'yes';
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
return { ok: false, error: `unknown option: ${arg}` };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (hasInternalOption && !reentryAuthorized) {
|
|
115
|
+
return { ok: false, error: 'internal update continuation is unavailable' };
|
|
116
|
+
}
|
|
117
|
+
if (hasInternalOption) {
|
|
118
|
+
if (!clientVersion || !serverVersion || serverPresent === undefined) {
|
|
119
|
+
return { ok: false, error: 'internal update continuation requires both target versions and server presence' };
|
|
120
|
+
}
|
|
121
|
+
if (!isExactSemver(clientVersion) || !isExactSemver(serverVersion)) {
|
|
122
|
+
return { ok: false, error: 'internal update continuation requires exact versions' };
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
ok: true,
|
|
126
|
+
yes,
|
|
127
|
+
...(help ? { help: true } : {}),
|
|
128
|
+
target: { clientVersion, serverVersion, serverPresent },
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return { ok: true, yes, ...(help ? { help: true } : {}) };
|
|
132
|
+
}
|
|
133
|
+
function validatePublishedPackage(value, expectedName) {
|
|
134
|
+
if (value.name !== expectedName || !isExactSemver(value.version)) {
|
|
135
|
+
throw new Error(`registry returned an invalid ${expectedName} manifest identity`);
|
|
136
|
+
}
|
|
137
|
+
if (!isCanonicalSha512Integrity(value.integrity)) {
|
|
138
|
+
throw new Error(`registry returned invalid ${expectedName} SHA-512 integrity`);
|
|
139
|
+
}
|
|
140
|
+
if (!isExactSemver(value.sharedVersion)) {
|
|
141
|
+
throw new Error(`registry returned a non-exact ${expectedName} ${SHARED_PACKAGE} dependency`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async function publishedPair(target, deps) {
|
|
145
|
+
const [client, server] = await Promise.all([
|
|
146
|
+
deps.publishedPackage(CLIENT_PACKAGE, target?.clientVersion ?? 'latest'),
|
|
147
|
+
deps.publishedPackage(SERVER_PACKAGE, target?.serverVersion ?? 'latest'),
|
|
148
|
+
]);
|
|
149
|
+
validatePublishedPackage(client, CLIENT_PACKAGE);
|
|
150
|
+
validatePublishedPackage(server, SERVER_PACKAGE);
|
|
151
|
+
if (target && (client.version !== target.clientVersion || server.version !== target.serverVersion)) {
|
|
152
|
+
throw new Error('published update targets changed during client re-entry');
|
|
153
|
+
}
|
|
154
|
+
if (client.sharedVersion !== server.sharedVersion) {
|
|
155
|
+
throw new Error(`published pair is incompatible: ${CLIENT_PACKAGE}@${client.version} pins ${SHARED_PACKAGE}@${client.sharedVersion}; ` +
|
|
156
|
+
`${SERVER_PACKAGE}@${server.version} pins ${SHARED_PACKAGE}@${server.sharedVersion}. ` +
|
|
157
|
+
`Wait for a compatible published pair and rerun borg update.`);
|
|
158
|
+
}
|
|
159
|
+
return { client, server };
|
|
160
|
+
}
|
|
161
|
+
function assertInstalled(installed, published) {
|
|
162
|
+
if (installed.name !== published.name ||
|
|
163
|
+
installed.version !== published.version ||
|
|
164
|
+
installed.sharedVersion !== published.sharedVersion) {
|
|
165
|
+
throw new Error(`${published.name} installation verification failed: expected ${published.version} with ` +
|
|
166
|
+
`${SHARED_PACKAGE}@${published.sharedVersion}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function exactServerIdentity(version) {
|
|
170
|
+
return `${SERVER_PACKAGE}@${version}`;
|
|
171
|
+
}
|
|
172
|
+
function isNextAction(value) {
|
|
173
|
+
return value === null || value === 'borg-mcp-server update' || (typeof value === 'string' &&
|
|
174
|
+
value.startsWith('npm install --global borgmcp-server@') &&
|
|
175
|
+
isExactSemver(value.slice('npm install --global borgmcp-server@'.length)));
|
|
176
|
+
}
|
|
177
|
+
function decodeServerStatus(value) {
|
|
178
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
179
|
+
throw new Error('server returned invalid JSON status');
|
|
180
|
+
}
|
|
181
|
+
const record = value;
|
|
182
|
+
const allowed = new Set([
|
|
183
|
+
'status',
|
|
184
|
+
'installed_controller',
|
|
185
|
+
'prepared_runtime',
|
|
186
|
+
'prepared_integrity',
|
|
187
|
+
'running_runtime',
|
|
188
|
+
'running_integrity',
|
|
189
|
+
'build_identity',
|
|
190
|
+
'endpoint',
|
|
191
|
+
'mode',
|
|
192
|
+
'service_adapter',
|
|
193
|
+
'data_identity',
|
|
194
|
+
'next_action',
|
|
195
|
+
]);
|
|
196
|
+
for (const key of Object.keys(record)) {
|
|
197
|
+
if (!allowed.has(key))
|
|
198
|
+
throw new Error(`server status contains unknown field ${key}`);
|
|
199
|
+
}
|
|
200
|
+
if ((record.status !== 'running' && record.status !== 'stopped') ||
|
|
201
|
+
typeof record.installed_controller !== 'string' ||
|
|
202
|
+
(record.prepared_runtime !== null && typeof record.prepared_runtime !== 'string') ||
|
|
203
|
+
(record.prepared_integrity !== null && typeof record.prepared_integrity !== 'string') ||
|
|
204
|
+
(record.running_runtime !== null && typeof record.running_runtime !== 'string') ||
|
|
205
|
+
(record.running_integrity !== null && typeof record.running_integrity !== 'string') ||
|
|
206
|
+
(record.build_identity !== null && typeof record.build_identity !== 'string') ||
|
|
207
|
+
(record.endpoint !== null && typeof record.endpoint !== 'string') ||
|
|
208
|
+
!['foreground', 'managed', 'legacy', 'stopped'].includes(record.mode) ||
|
|
209
|
+
(record.service_adapter !== null && record.service_adapter !== 'launchd' && record.service_adapter !== 'systemd') ||
|
|
210
|
+
(record.data_identity !== 'available' && record.data_identity !== 'unavailable') ||
|
|
211
|
+
!isNextAction(record.next_action)) {
|
|
212
|
+
throw new Error('server returned invalid JSON status');
|
|
213
|
+
}
|
|
214
|
+
if ((record.prepared_integrity !== null && !isCanonicalSha512Integrity(record.prepared_integrity)) ||
|
|
215
|
+
(record.running_integrity !== null && !isCanonicalSha512Integrity(record.running_integrity))) {
|
|
216
|
+
throw new Error('server returned invalid JSON status integrity');
|
|
217
|
+
}
|
|
218
|
+
if ((record.status === 'stopped' && (record.running_runtime !== null ||
|
|
219
|
+
record.running_integrity !== null ||
|
|
220
|
+
record.build_identity !== null ||
|
|
221
|
+
record.endpoint !== null ||
|
|
222
|
+
record.mode !== 'stopped' ||
|
|
223
|
+
record.service_adapter !== null)) ||
|
|
224
|
+
(record.status === 'running' && record.mode === 'stopped')) {
|
|
225
|
+
throw new Error('server returned inconsistent JSON status');
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
state: record.status,
|
|
229
|
+
installedController: record.installed_controller,
|
|
230
|
+
preparedRuntime: record.prepared_runtime,
|
|
231
|
+
preparedIntegrity: record.prepared_integrity,
|
|
232
|
+
runningRuntime: record.running_runtime,
|
|
233
|
+
runningIntegrity: record.running_integrity,
|
|
234
|
+
buildIdentity: record.build_identity,
|
|
235
|
+
endpoint: record.endpoint,
|
|
236
|
+
mode: record.mode,
|
|
237
|
+
serviceAdapter: record.service_adapter,
|
|
238
|
+
dataIdentity: record.data_identity,
|
|
239
|
+
nextAction: record.next_action,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function decodeServerUpdate(value) {
|
|
243
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
244
|
+
throw new Error('server returned invalid JSON update result');
|
|
245
|
+
}
|
|
246
|
+
const record = value;
|
|
247
|
+
if (record.status === 'failed') {
|
|
248
|
+
const failureKeys = Object.keys(record);
|
|
249
|
+
if (failureKeys.length !== 4 ||
|
|
250
|
+
!failureKeys.every((key) => ['status', 'error_code', 'recovery', 'data_identity'].includes(key)) ||
|
|
251
|
+
(record.error_code !== 'ARTIFACT_VERIFICATION_FAILED' && record.error_code !== 'ACTIVATION_FAILED') ||
|
|
252
|
+
!['verification_failed', 'restored', 'stopped', 'recovery_failed'].includes(record.recovery) ||
|
|
253
|
+
record.data_identity !== 'preserved' ||
|
|
254
|
+
(record.error_code === 'ARTIFACT_VERIFICATION_FAILED' && record.recovery !== 'verification_failed') ||
|
|
255
|
+
(record.error_code === 'ACTIVATION_FAILED' && record.recovery === 'verification_failed')) {
|
|
256
|
+
throw new Error('server returned invalid JSON update failure');
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
status: 'failed',
|
|
260
|
+
errorCode: record.error_code,
|
|
261
|
+
recovery: record.recovery,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
const allowed = new Set([
|
|
265
|
+
'status',
|
|
266
|
+
'installed_controller',
|
|
267
|
+
'artifact',
|
|
268
|
+
'artifact_integrity',
|
|
269
|
+
'running_runtime',
|
|
270
|
+
'build_identity',
|
|
271
|
+
'mode',
|
|
272
|
+
'data_identity',
|
|
273
|
+
'next_action',
|
|
274
|
+
]);
|
|
275
|
+
for (const key of Object.keys(record)) {
|
|
276
|
+
if (!allowed.has(key))
|
|
277
|
+
throw new Error(`server update contains unknown field ${key}`);
|
|
278
|
+
}
|
|
279
|
+
if ((record.status !== 'prepared' && record.status !== 'updated') ||
|
|
280
|
+
typeof record.installed_controller !== 'string' ||
|
|
281
|
+
typeof record.artifact !== 'string' ||
|
|
282
|
+
typeof record.artifact_integrity !== 'string' ||
|
|
283
|
+
(record.running_runtime !== null && typeof record.running_runtime !== 'string') ||
|
|
284
|
+
(record.build_identity !== null && typeof record.build_identity !== 'string') ||
|
|
285
|
+
(record.mode !== 'stopped' && record.mode !== 'managed') ||
|
|
286
|
+
record.data_identity !== 'preserved' ||
|
|
287
|
+
!isNextAction(record.next_action) ||
|
|
288
|
+
!isCanonicalSha512Integrity(record.artifact_integrity)) {
|
|
289
|
+
throw new Error('server returned invalid JSON update result');
|
|
290
|
+
}
|
|
291
|
+
if ((record.status === 'prepared' && (record.running_runtime !== null || record.mode !== 'stopped')) ||
|
|
292
|
+
(record.status === 'updated' && (typeof record.running_runtime !== 'string' || record.mode !== 'managed'))) {
|
|
293
|
+
throw new Error('server returned inconsistent JSON update result');
|
|
294
|
+
}
|
|
295
|
+
return {
|
|
296
|
+
status: record.status,
|
|
297
|
+
installedController: record.installed_controller,
|
|
298
|
+
artifact: record.artifact,
|
|
299
|
+
artifactIntegrity: record.artifact_integrity,
|
|
300
|
+
runningRuntime: record.running_runtime,
|
|
301
|
+
buildIdentity: record.build_identity,
|
|
302
|
+
mode: record.mode,
|
|
303
|
+
nextAction: record.next_action,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
function verifyServerStatus(status, target) {
|
|
307
|
+
const identity = exactServerIdentity(target.version);
|
|
308
|
+
if (status.installedController !== identity ||
|
|
309
|
+
status.preparedRuntime !== identity ||
|
|
310
|
+
status.preparedIntegrity !== target.integrity ||
|
|
311
|
+
status.nextAction !== null) {
|
|
312
|
+
throw new Error('final server verification failed: controller, prepared runtime, integrity, or next action mismatched');
|
|
313
|
+
}
|
|
314
|
+
if (status.state === 'stopped') {
|
|
315
|
+
if (status.runningRuntime !== null ||
|
|
316
|
+
status.runningIntegrity !== null ||
|
|
317
|
+
status.buildIdentity !== null ||
|
|
318
|
+
status.endpoint !== null ||
|
|
319
|
+
status.mode !== 'stopped' ||
|
|
320
|
+
status.serviceAdapter !== null ||
|
|
321
|
+
status.dataIdentity !== 'available') {
|
|
322
|
+
throw new Error('final server verification failed: stopped server reported a running runtime');
|
|
323
|
+
}
|
|
324
|
+
return 'stopped';
|
|
325
|
+
}
|
|
326
|
+
if (status.runningRuntime !== identity ||
|
|
327
|
+
status.runningIntegrity !== target.integrity ||
|
|
328
|
+
status.mode === 'stopped' ||
|
|
329
|
+
status.dataIdentity !== 'available' ||
|
|
330
|
+
status.endpoint === null ||
|
|
331
|
+
!isHttpsOrigin(status.endpoint)) {
|
|
332
|
+
throw new Error('final server verification failed: running runtime mismatched');
|
|
333
|
+
}
|
|
334
|
+
return 'running';
|
|
335
|
+
}
|
|
336
|
+
export async function runUpdate(options, deps) {
|
|
337
|
+
if (options.help) {
|
|
338
|
+
deps.stdout(updateHelpText(''));
|
|
339
|
+
return 0;
|
|
340
|
+
}
|
|
341
|
+
let pair;
|
|
342
|
+
let client;
|
|
343
|
+
let discoveredServer;
|
|
344
|
+
try {
|
|
345
|
+
[pair, client, discoveredServer] = await Promise.all([
|
|
346
|
+
publishedPair(options.target, deps),
|
|
347
|
+
deps.currentClient(),
|
|
348
|
+
deps.currentServer(),
|
|
349
|
+
]);
|
|
350
|
+
}
|
|
351
|
+
catch (error) {
|
|
352
|
+
const interrupted = signalExitCode(error);
|
|
353
|
+
deps.stderr(options.target
|
|
354
|
+
? renderReentryPreflightFailure(error, options.target)
|
|
355
|
+
: (`${errorMessage(error, 'Update preflight failed')}\n` +
|
|
356
|
+
`Observed update state:\n` +
|
|
357
|
+
` client: unavailable (preflight incomplete)\n` +
|
|
358
|
+
` server controller: unavailable (preflight incomplete)\n` +
|
|
359
|
+
` prepared runtime: not inspected\n` +
|
|
360
|
+
` running runtime: not inspected\n` +
|
|
361
|
+
`No mutation was attempted.\n`));
|
|
362
|
+
return interrupted ?? 1;
|
|
363
|
+
}
|
|
364
|
+
const serverWasPresent = options.target?.serverPresent ?? discoveredServer !== null;
|
|
365
|
+
if (options.target?.serverPresent === true && discoveredServer === null) {
|
|
366
|
+
deps.stderr(`The previously installed server is no longer available.\n` +
|
|
367
|
+
`Observed update state:\n` +
|
|
368
|
+
` client: ${client.name}@${client.version} (${SHARED_PACKAGE}@${client.sharedVersion})\n` +
|
|
369
|
+
` server controller: unavailable\n` +
|
|
370
|
+
` prepared runtime: not inspected\n` +
|
|
371
|
+
` running runtime: not inspected\n` +
|
|
372
|
+
`Server mutation was not attempted.\n` +
|
|
373
|
+
`Retry with: borg update --yes\n`);
|
|
374
|
+
return 1;
|
|
375
|
+
}
|
|
376
|
+
deps.stdout(`Published update plan (npm registry):\n` +
|
|
377
|
+
` client: ${CLIENT_PACKAGE}@${client.version} -> ${CLIENT_PACKAGE}@${pair.client.version}\n` +
|
|
378
|
+
` target integrity: ${pair.client.integrity}\n` +
|
|
379
|
+
` server: ${discoveredServer ? `${SERVER_PACKAGE}@${discoveredServer.version}` : 'not installed'} -> ${SERVER_PACKAGE}@${pair.server.version}\n` +
|
|
380
|
+
` target integrity: ${pair.server.integrity}\n` +
|
|
381
|
+
` shared pin: ${SHARED_PACKAGE}@${pair.client.sharedVersion}\n` +
|
|
382
|
+
` local server: ${serverWasPresent ? 'update' : 'skip (not installed)'}\n`);
|
|
383
|
+
if (!options.yes) {
|
|
384
|
+
if (!deps.isTTY()) {
|
|
385
|
+
deps.stderr('borg update requires --yes when input is not an interactive terminal. No update was performed.\n');
|
|
386
|
+
return 1;
|
|
387
|
+
}
|
|
388
|
+
const answer = await deps.confirm(`Update ${CLIENT_PACKAGE} ${client.version} -> ${pair.client.version}` +
|
|
389
|
+
`${serverWasPresent ? ` and ${SERVER_PACKAGE} ${discoveredServer?.version ?? 'unknown'} -> ${pair.server.version}` : ''}` +
|
|
390
|
+
` with ${SHARED_PACKAGE}@${pair.client.sharedVersion}? [y/N] `);
|
|
391
|
+
if (answer === 'no') {
|
|
392
|
+
deps.stdout('Update cancelled. No changes were made.\n');
|
|
393
|
+
return 0;
|
|
394
|
+
}
|
|
395
|
+
if (answer === 'eof') {
|
|
396
|
+
deps.stderr('Update cancelled because confirmation input ended. No changes were made.\n');
|
|
397
|
+
return 1;
|
|
398
|
+
}
|
|
399
|
+
if (answer === 'interrupted') {
|
|
400
|
+
deps.stderr('Update cancelled by SIGINT. No changes were made.\n');
|
|
401
|
+
return 130;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (client.version !== pair.client.version || client.sharedVersion !== pair.client.sharedVersion) {
|
|
405
|
+
let installedClient = null;
|
|
406
|
+
try {
|
|
407
|
+
await deps.installGlobal(CLIENT_PACKAGE, pair.client.version);
|
|
408
|
+
installedClient = await deps.currentClient();
|
|
409
|
+
assertInstalled(installedClient, pair.client);
|
|
410
|
+
const args = [
|
|
411
|
+
'update',
|
|
412
|
+
'--yes',
|
|
413
|
+
'--target-client', pair.client.version,
|
|
414
|
+
'--target-server', pair.server.version,
|
|
415
|
+
'--server-present', serverWasPresent ? 'yes' : 'no',
|
|
416
|
+
];
|
|
417
|
+
return await deps.reenter(installedClient.binPath, args);
|
|
418
|
+
}
|
|
419
|
+
catch (error) {
|
|
420
|
+
const interrupted = signalExitCode(error);
|
|
421
|
+
deps.stderr(`Client update or re-entry failed: ${errorMessage(error, 'unknown failure')}.\n` +
|
|
422
|
+
`Observed update state:\n` +
|
|
423
|
+
` client: ${installedClient
|
|
424
|
+
? `${installedClient.name}@${installedClient.version} installed and verified`
|
|
425
|
+
: 'unavailable after client update failure'}\n` +
|
|
426
|
+
` server controller before client update: ${discoveredServer
|
|
427
|
+
? `${discoveredServer.name}@${discoveredServer.version}`
|
|
428
|
+
: 'not installed'}\n` +
|
|
429
|
+
` prepared runtime: not inspected\n` +
|
|
430
|
+
` running runtime: not inspected\n` +
|
|
431
|
+
`Server mutation was not attempted.\n` +
|
|
432
|
+
`Retry with: borg update --yes\n`);
|
|
433
|
+
return interrupted ?? 1;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
try {
|
|
437
|
+
assertInstalled(client, pair.client);
|
|
438
|
+
}
|
|
439
|
+
catch (error) {
|
|
440
|
+
const interrupted = signalExitCode(error);
|
|
441
|
+
deps.stderr(`${errorMessage(error, 'Client verification failed')}\n` +
|
|
442
|
+
`Observed update state:\n` +
|
|
443
|
+
` client: verification failed\n` +
|
|
444
|
+
` server controller: ${discoveredServer
|
|
445
|
+
? `${discoveredServer.name}@${discoveredServer.version}`
|
|
446
|
+
: 'not installed'}\n` +
|
|
447
|
+
` prepared runtime: not inspected\n` +
|
|
448
|
+
` running runtime: not inspected\n` +
|
|
449
|
+
`Server mutation was not attempted.\n`);
|
|
450
|
+
return interrupted ?? 1;
|
|
451
|
+
}
|
|
452
|
+
if (!serverWasPresent) {
|
|
453
|
+
deps.stdout(`Updated ${CLIENT_PACKAGE}@${pair.client.version}. Local server: skipped (not installed).\n` +
|
|
454
|
+
`Restart active agent sessions to load the updated client.\n`);
|
|
455
|
+
return 0;
|
|
456
|
+
}
|
|
457
|
+
let server;
|
|
458
|
+
try {
|
|
459
|
+
if (!discoveredServer)
|
|
460
|
+
throw new Error('previously installed server is unavailable');
|
|
461
|
+
if (discoveredServer.version !== pair.server.version ||
|
|
462
|
+
discoveredServer.sharedVersion !== pair.server.sharedVersion) {
|
|
463
|
+
await deps.installGlobal(SERVER_PACKAGE, pair.server.version);
|
|
464
|
+
}
|
|
465
|
+
const verified = await deps.currentServer();
|
|
466
|
+
if (!verified)
|
|
467
|
+
throw new Error('server controller disappeared after installation');
|
|
468
|
+
assertInstalled(verified, pair.server);
|
|
469
|
+
server = verified;
|
|
470
|
+
}
|
|
471
|
+
catch (error) {
|
|
472
|
+
const interrupted = signalExitCode(error);
|
|
473
|
+
deps.stderr(`Client updated, but server controller update failed: ${errorMessage(error, 'unknown failure')}.\n` +
|
|
474
|
+
`Observed update state:\n` +
|
|
475
|
+
` client: ${client.name}@${client.version} (${SHARED_PACKAGE}@${client.sharedVersion})\n` +
|
|
476
|
+
` server controller: unavailable after controller failure\n` +
|
|
477
|
+
` prepared runtime: not inspected\n` +
|
|
478
|
+
` running runtime: not inspected\n` +
|
|
479
|
+
`Server runtime mutation was not attempted.\n` +
|
|
480
|
+
`Retry with: borg update --yes\n`);
|
|
481
|
+
return interrupted ?? 1;
|
|
482
|
+
}
|
|
483
|
+
let observedStatus = null;
|
|
484
|
+
let observedUpdate = null;
|
|
485
|
+
try {
|
|
486
|
+
let status = decodeServerStatus(await deps.serverJson(server.binPath, 'status'));
|
|
487
|
+
observedStatus = status;
|
|
488
|
+
if (status.installedController !== exactServerIdentity(pair.server.version)) {
|
|
489
|
+
throw new Error('server status contradicted the verified controller identity');
|
|
490
|
+
}
|
|
491
|
+
try {
|
|
492
|
+
verifyServerStatus(status, pair.server);
|
|
493
|
+
}
|
|
494
|
+
catch {
|
|
495
|
+
observedStatus = null;
|
|
496
|
+
const update = decodeServerUpdate(await deps.serverJson(server.binPath, 'update'));
|
|
497
|
+
observedUpdate = update;
|
|
498
|
+
if (update.status === 'failed') {
|
|
499
|
+
throw new Error(`server update failed: ${update.errorCode} (${update.recovery})`);
|
|
500
|
+
}
|
|
501
|
+
const serverIdentity = exactServerIdentity(pair.server.version);
|
|
502
|
+
if (update.installedController !== serverIdentity ||
|
|
503
|
+
update.artifact !== serverIdentity ||
|
|
504
|
+
update.artifactIntegrity !== pair.server.integrity ||
|
|
505
|
+
update.nextAction !== null ||
|
|
506
|
+
(update.status === 'updated' && update.runningRuntime !== serverIdentity)) {
|
|
507
|
+
throw new Error('server update result did not reach the target artifact');
|
|
508
|
+
}
|
|
509
|
+
status = decodeServerStatus(await deps.serverJson(server.binPath, 'status'));
|
|
510
|
+
observedStatus = status;
|
|
511
|
+
}
|
|
512
|
+
const state = verifyServerStatus(status, pair.server);
|
|
513
|
+
const [finalClient, finalServer] = await Promise.all([
|
|
514
|
+
deps.currentClient(),
|
|
515
|
+
deps.currentServer(),
|
|
516
|
+
]);
|
|
517
|
+
assertInstalled(finalClient, pair.client);
|
|
518
|
+
if (!finalServer)
|
|
519
|
+
throw new Error('server controller disappeared during final verification');
|
|
520
|
+
assertInstalled(finalServer, pair.server);
|
|
521
|
+
if (state === 'running')
|
|
522
|
+
await deps.verifyRunningProtocol(status.endpoint);
|
|
523
|
+
deps.stdout(state === 'stopped'
|
|
524
|
+
? `Updated ${CLIENT_PACKAGE}@${pair.client.version} and ${SERVER_PACKAGE}@${pair.server.version}: prepared; still stopped.\n`
|
|
525
|
+
: `Updated ${CLIENT_PACKAGE}@${pair.client.version} and ${SERVER_PACKAGE}@${pair.server.version}; running identities and protocol verified.\n`);
|
|
526
|
+
deps.stdout('Restart active agent sessions to load the updated client.\n');
|
|
527
|
+
return 0;
|
|
528
|
+
}
|
|
529
|
+
catch (error) {
|
|
530
|
+
const interrupted = signalExitCode(error);
|
|
531
|
+
deps.stderr(`Server update or final verification failed: ${errorMessage(error, 'unknown failure')}.\n` +
|
|
532
|
+
renderServerState(client, server, observedStatus, observedUpdate) +
|
|
533
|
+
`Retry with: borg update --yes\n`);
|
|
534
|
+
return interrupted ?? 1;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
class CommandSignalError extends Error {
|
|
538
|
+
exitCode;
|
|
539
|
+
constructor(signal) {
|
|
540
|
+
super(`command stopped by ${signal}`);
|
|
541
|
+
this.name = 'CommandSignalError';
|
|
542
|
+
this.exitCode = 128 + (constants.signals[signal] ?? 1);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
function runCommand(command, args, options = {}) {
|
|
546
|
+
return new Promise((resolvePromise, reject) => {
|
|
547
|
+
const child = spawn(command, [...args], {
|
|
548
|
+
shell: false,
|
|
549
|
+
stdio: options.inherit ? 'inherit' : ['ignore', 'pipe', 'pipe'],
|
|
550
|
+
env: options.env ?? process.env,
|
|
551
|
+
});
|
|
552
|
+
let stdout = '';
|
|
553
|
+
let stderr = '';
|
|
554
|
+
let settled = false;
|
|
555
|
+
const fail = (error) => {
|
|
556
|
+
if (settled)
|
|
557
|
+
return;
|
|
558
|
+
settled = true;
|
|
559
|
+
reject(error);
|
|
560
|
+
};
|
|
561
|
+
const append = (current, chunk) => {
|
|
562
|
+
const next = current + chunk.toString('utf8');
|
|
563
|
+
if (Buffer.byteLength(next) > MAX_CAPTURE_BYTES) {
|
|
564
|
+
child.kill('SIGTERM');
|
|
565
|
+
fail(new Error('command output exceeded the update limit'));
|
|
566
|
+
}
|
|
567
|
+
return next;
|
|
568
|
+
};
|
|
569
|
+
child.stdout?.on('data', (chunk) => { stdout = append(stdout, chunk); });
|
|
570
|
+
child.stderr?.on('data', (chunk) => { stderr = append(stderr, chunk); });
|
|
571
|
+
child.once('error', fail);
|
|
572
|
+
child.once('exit', (code, signal) => {
|
|
573
|
+
if (settled)
|
|
574
|
+
return;
|
|
575
|
+
if (signal) {
|
|
576
|
+
fail(new CommandSignalError(signal));
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
settled = true;
|
|
580
|
+
resolvePromise({ code: code ?? 1, stdout, stderr });
|
|
581
|
+
});
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
async function readJson(path) {
|
|
585
|
+
const parsed = JSON.parse(await readFile(path, 'utf8'));
|
|
586
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
587
|
+
throw new Error(`invalid package manifest at ${path}`);
|
|
588
|
+
}
|
|
589
|
+
return parsed;
|
|
590
|
+
}
|
|
591
|
+
function singleLine(text, label) {
|
|
592
|
+
const value = text.trim();
|
|
593
|
+
if (value === '' || value.includes('\n') || value.includes('\r')) {
|
|
594
|
+
throw new Error(`npm returned an invalid ${label}`);
|
|
595
|
+
}
|
|
596
|
+
return value;
|
|
597
|
+
}
|
|
598
|
+
async function npmText(commandPath, args, label) {
|
|
599
|
+
const result = await runCommand(commandPath, args);
|
|
600
|
+
if (result.code !== 0)
|
|
601
|
+
throw new Error(`npm ${label} lookup failed`);
|
|
602
|
+
return singleLine(result.stdout, label);
|
|
603
|
+
}
|
|
604
|
+
function requireCanonicalRegistry(value) {
|
|
605
|
+
let normalized;
|
|
606
|
+
try {
|
|
607
|
+
normalized = new URL(value).href;
|
|
608
|
+
}
|
|
609
|
+
catch {
|
|
610
|
+
throw new Error('npm registry configuration is invalid');
|
|
611
|
+
}
|
|
612
|
+
if (normalized !== CANONICAL_NPM_REGISTRY) {
|
|
613
|
+
throw new Error(`borg update requires the canonical npm registry ${CANONICAL_NPM_REGISTRY}; ` +
|
|
614
|
+
`the configured registry is unsupported. Use your package manager manually for this installation.`);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
async function resolveNpmContext() {
|
|
618
|
+
const commandPath = which.sync('npm');
|
|
619
|
+
const commandIdentity = await realpath(commandPath);
|
|
620
|
+
const registry = await npmText(commandPath, ['config', 'get', 'registry'], 'registry');
|
|
621
|
+
requireCanonicalRegistry(registry);
|
|
622
|
+
const prefixText = await npmText(commandPath, ['prefix', '--global'], 'global prefix');
|
|
623
|
+
const rootText = await npmText(commandPath, ['root', '--global'], 'global root');
|
|
624
|
+
if (!isAbsolute(prefixText) || !isAbsolute(rootText)) {
|
|
625
|
+
throw new Error('npm returned a non-absolute global context');
|
|
626
|
+
}
|
|
627
|
+
const prefix = await realpath(prefixText);
|
|
628
|
+
const root = await realpath(rootText);
|
|
629
|
+
const relativeRoot = relative(prefix, root);
|
|
630
|
+
if (relativeRoot === '' || relativeRoot.startsWith('..') || isAbsolute(relativeRoot)) {
|
|
631
|
+
throw new Error('npm global root is outside its global prefix');
|
|
632
|
+
}
|
|
633
|
+
return { commandPath, commandIdentity, prefix, root };
|
|
634
|
+
}
|
|
635
|
+
async function assertNpmContext(context) {
|
|
636
|
+
const activeCommand = which.sync('npm');
|
|
637
|
+
if (await realpath(activeCommand) !== context.commandIdentity) {
|
|
638
|
+
throw new Error('active npm executable changed during update');
|
|
639
|
+
}
|
|
640
|
+
const registry = await npmText(context.commandPath, ['config', 'get', 'registry'], 'registry');
|
|
641
|
+
requireCanonicalRegistry(registry);
|
|
642
|
+
const prefix = await realpath(await npmText(context.commandPath, ['prefix', '--global'], 'global prefix'));
|
|
643
|
+
if (prefix !== context.prefix)
|
|
644
|
+
throw new Error('npm global prefix changed during update');
|
|
645
|
+
const root = await realpath(await npmText(context.commandPath, ['root', '--global'], 'global root'));
|
|
646
|
+
if (root !== context.root)
|
|
647
|
+
throw new Error('npm global root changed during update');
|
|
648
|
+
return context;
|
|
649
|
+
}
|
|
650
|
+
function packageBin(manifest, binName) {
|
|
651
|
+
const bin = manifest.bin;
|
|
652
|
+
if (typeof bin === 'string')
|
|
653
|
+
return bin;
|
|
654
|
+
if (bin && typeof bin === 'object' && !Array.isArray(bin)) {
|
|
655
|
+
const value = bin[binName];
|
|
656
|
+
if (typeof value === 'string')
|
|
657
|
+
return value;
|
|
658
|
+
}
|
|
659
|
+
throw new Error(`package manifest does not declare ${binName}`);
|
|
660
|
+
}
|
|
661
|
+
export async function inspectNpmPackageAt(input) {
|
|
662
|
+
const npmRoot = await realpath(input.npmRoot);
|
|
663
|
+
const packageRoot = await realpath(join(npmRoot, input.name));
|
|
664
|
+
const packageRelative = relative(npmRoot, packageRoot);
|
|
665
|
+
if (packageRelative !== input.name) {
|
|
666
|
+
throw new Error(`${input.name} is not owned by the active npm global root`);
|
|
667
|
+
}
|
|
668
|
+
const manifest = await readJson(join(packageRoot, 'package.json'));
|
|
669
|
+
if (manifest.name !== input.name || typeof manifest.version !== 'string' || !isExactSemver(manifest.version)) {
|
|
670
|
+
throw new Error(`installed ${input.name} manifest identity is invalid`);
|
|
671
|
+
}
|
|
672
|
+
const binRelative = packageBin(manifest, input.binName);
|
|
673
|
+
const expectedBin = await realpath(resolve(packageRoot, binRelative));
|
|
674
|
+
const relativeBin = relative(packageRoot, expectedBin);
|
|
675
|
+
if (relativeBin.startsWith('..') || isAbsolute(relativeBin)) {
|
|
676
|
+
throw new Error(`${input.binName} resolves outside the npm-owned package`);
|
|
677
|
+
}
|
|
678
|
+
if (!(await stat(expectedBin)).isFile()) {
|
|
679
|
+
throw new Error(`${input.binName} is not a regular npm package file`);
|
|
680
|
+
}
|
|
681
|
+
if (await realpath(input.commandPath) !== expectedBin) {
|
|
682
|
+
throw new Error(`${input.binName} on PATH is not the npm-owned package binary`);
|
|
683
|
+
}
|
|
684
|
+
if (input.invokedPath !== undefined) {
|
|
685
|
+
const invoked = await realpath(input.invokedPath);
|
|
686
|
+
if (invoked !== expectedBin) {
|
|
687
|
+
throw new Error('running borg entrypoint is not the npm-owned package binary');
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
const shared = await readJson(join(packageRoot, 'node_modules', SHARED_PACKAGE, 'package.json'));
|
|
691
|
+
if (shared.name !== SHARED_PACKAGE || typeof shared.version !== 'string' || !isExactSemver(shared.version)) {
|
|
692
|
+
throw new Error(`installed ${input.name} ${SHARED_PACKAGE} identity is invalid`);
|
|
693
|
+
}
|
|
694
|
+
return {
|
|
695
|
+
name: input.name,
|
|
696
|
+
version: manifest.version,
|
|
697
|
+
sharedVersion: shared.version,
|
|
698
|
+
packageRoot,
|
|
699
|
+
binPath: expectedBin,
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
async function inspectNpmPackage(name, binName, required, context) {
|
|
703
|
+
let commandPath;
|
|
704
|
+
try {
|
|
705
|
+
commandPath = which.sync(binName);
|
|
706
|
+
}
|
|
707
|
+
catch {
|
|
708
|
+
if (!required)
|
|
709
|
+
return null;
|
|
710
|
+
throw new Error(`${binName} is not available on PATH`);
|
|
711
|
+
}
|
|
712
|
+
return inspectNpmPackageAt({
|
|
713
|
+
name,
|
|
714
|
+
binName,
|
|
715
|
+
npmRoot: context.root,
|
|
716
|
+
commandPath,
|
|
717
|
+
...(name === CLIENT_PACKAGE ? { invokedPath: process.argv[1] } : {}),
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
async function defaultPublishedPackage(name, version, context) {
|
|
721
|
+
if (version !== 'latest' && !isExactSemver(version))
|
|
722
|
+
throw new Error('invalid registry target version');
|
|
723
|
+
const result = await runCommand(context.commandPath, [
|
|
724
|
+
'view',
|
|
725
|
+
`${name}@${version}`,
|
|
726
|
+
'name',
|
|
727
|
+
'version',
|
|
728
|
+
'dist.integrity',
|
|
729
|
+
`dependencies.${SHARED_PACKAGE}`,
|
|
730
|
+
`--registry=${CANONICAL_NPM_REGISTRY}`,
|
|
731
|
+
'--json',
|
|
732
|
+
]);
|
|
733
|
+
if (result.code !== 0)
|
|
734
|
+
throw new Error(`registry lookup failed for ${name}@${version}`);
|
|
735
|
+
const manifest = JSON.parse(result.stdout);
|
|
736
|
+
return {
|
|
737
|
+
name: manifest.name,
|
|
738
|
+
version: manifest.version,
|
|
739
|
+
integrity: manifest['dist.integrity'],
|
|
740
|
+
sharedVersion: manifest[`dependencies.${SHARED_PACKAGE}`],
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
async function defaultConfirm(message) {
|
|
744
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
745
|
+
let interrupted = false;
|
|
746
|
+
rl.once('SIGINT', () => {
|
|
747
|
+
interrupted = true;
|
|
748
|
+
rl.close();
|
|
749
|
+
});
|
|
750
|
+
try {
|
|
751
|
+
const answer = (await rl.question(message)).trim().toLowerCase();
|
|
752
|
+
return answer === 'y' || answer === 'yes' ? 'yes' : 'no';
|
|
753
|
+
}
|
|
754
|
+
catch (error) {
|
|
755
|
+
if (interrupted)
|
|
756
|
+
return 'interrupted';
|
|
757
|
+
if (error.code === 'ERR_USE_AFTER_CLOSE')
|
|
758
|
+
return 'eof';
|
|
759
|
+
throw error;
|
|
760
|
+
}
|
|
761
|
+
finally {
|
|
762
|
+
rl.close();
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
export function buildDefaultUpdateDeps() {
|
|
766
|
+
let contextPromise;
|
|
767
|
+
const context = async () => {
|
|
768
|
+
contextPromise ??= resolveNpmContext();
|
|
769
|
+
return assertNpmContext(await contextPromise);
|
|
770
|
+
};
|
|
771
|
+
return {
|
|
772
|
+
currentClient: async () => {
|
|
773
|
+
const value = await inspectNpmPackage(CLIENT_PACKAGE, 'borg', true, await context());
|
|
774
|
+
if (!value)
|
|
775
|
+
throw new Error('borgmcp is not installed');
|
|
776
|
+
return value;
|
|
777
|
+
},
|
|
778
|
+
currentServer: async () => inspectNpmPackage(SERVER_PACKAGE, 'borg-mcp-server', false, await context()),
|
|
779
|
+
publishedPackage: async (name, version) => defaultPublishedPackage(name, version, await context()),
|
|
780
|
+
installGlobal: async (name, version) => {
|
|
781
|
+
const npm = await context();
|
|
782
|
+
const result = await runCommand(npm.commandPath, [
|
|
783
|
+
'install',
|
|
784
|
+
'--global',
|
|
785
|
+
`--prefix=${npm.prefix}`,
|
|
786
|
+
`--registry=${CANONICAL_NPM_REGISTRY}`,
|
|
787
|
+
`${name}@${version}`,
|
|
788
|
+
], { inherit: true });
|
|
789
|
+
if (result.code !== 0)
|
|
790
|
+
throw new Error(`${name} installation exited ${result.code}`);
|
|
791
|
+
},
|
|
792
|
+
reenter: async (binPath, args) => {
|
|
793
|
+
const result = await runCommand(process.execPath, [binPath, ...args], {
|
|
794
|
+
inherit: true,
|
|
795
|
+
env: { ...process.env, [REENTRY_ENV]: '1' },
|
|
796
|
+
});
|
|
797
|
+
return result.code;
|
|
798
|
+
},
|
|
799
|
+
serverJson: async (binPath, command) => {
|
|
800
|
+
const result = await runCommand(process.execPath, [binPath, command, '--json']);
|
|
801
|
+
let parsed;
|
|
802
|
+
try {
|
|
803
|
+
parsed = JSON.parse(result.stdout);
|
|
804
|
+
}
|
|
805
|
+
catch {
|
|
806
|
+
throw new Error(`server ${command} returned invalid JSON`);
|
|
807
|
+
}
|
|
808
|
+
if (result.code !== 0 && command !== 'update') {
|
|
809
|
+
throw new Error(`server ${command} exited ${result.code}`);
|
|
810
|
+
}
|
|
811
|
+
return parsed;
|
|
812
|
+
},
|
|
813
|
+
verifyRunningProtocol: async (origin) => {
|
|
814
|
+
const trust = await loadBorgServerTrust(origin);
|
|
815
|
+
await preflightBorgServerTag(origin, trust.fetchImpl);
|
|
816
|
+
},
|
|
817
|
+
confirm: defaultConfirm,
|
|
818
|
+
isTTY: () => process.stdin.isTTY === true && process.stdout.isTTY === true,
|
|
819
|
+
stdout: (text) => process.stdout.write(text),
|
|
820
|
+
stderr: (text) => process.stderr.write(text),
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
export async function runEarlyUpdate(argv, deps = buildDefaultUpdateDeps()) {
|
|
824
|
+
if (argv[2] !== 'update')
|
|
825
|
+
return null;
|
|
826
|
+
const parsed = parseUpdateArgs(argv.slice(3), process.env[REENTRY_ENV] === '1');
|
|
827
|
+
if (!parsed.ok) {
|
|
828
|
+
deps.stderr(`${parsed.error}\nRun \`borg update --help\` for usage.\n`);
|
|
829
|
+
return 1;
|
|
830
|
+
}
|
|
831
|
+
if (parsed.help) {
|
|
832
|
+
deps.stdout(updateHelpText(''));
|
|
833
|
+
return 0;
|
|
834
|
+
}
|
|
835
|
+
return runUpdate(parsed, deps);
|
|
836
|
+
}
|
|
837
|
+
//# sourceMappingURL=update-cmd.js.map
|