@livedesk/client 0.1.232 → 0.1.234

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.
@@ -26,6 +26,12 @@ import {
26
26
  inspectLinuxVideoAcceleration,
27
27
  installLinuxVideoAcceleration
28
28
  } from '../src/runtime/linux-video-acceleration.js';
29
+ import {
30
+ ensureRepairedFastRuntime,
31
+ inspectFastRuntimePackage,
32
+ inspectRepairedFastRuntime,
33
+ resolveFastPlatformSpec
34
+ } from '../src/runtime/fast-runtime-repair.js';
29
35
  import { normalizeRuntimeAuthSession } from '../../runtime-core/src/auth-session.js';
30
36
  import { createOsSecretStore, OS_SECRET_REFERENCE } from '../../runtime-core/src/os-secret-store.js';
31
37
  import { startRoleTransitionSupervisor } from '../../runtime-core/src/role-transition-supervisor.js';
@@ -330,8 +336,8 @@ Options:
330
336
  --version Show the agent version.
331
337
  --help Show this help.
332
338
 
333
- Auto uses C# RemoteFast when supported and falls back to Node only when a
334
- packaged RemoteFast runtime is unavailable.
339
+ Auto uses the verified C# RemoteFast screen engine on supported desktop platforms.
340
+ If an npx cache omitted it, LiveDesk restores the exact pinned platform package before connecting.
335
341
  Enable Windows auto-start from the connection page when this client signs in.
336
342
  `.trimStart());
337
343
  }
@@ -340,6 +346,14 @@ function readPackageVersion() {
340
346
  return readClientPackageVersion();
341
347
  }
342
348
 
349
+ function readClientPackageManifest() {
350
+ try {
351
+ return JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8'));
352
+ } catch {
353
+ return null;
354
+ }
355
+ }
356
+
343
357
  function isTruthy(value) {
344
358
  return /^(1|true|yes|on)$/i.test(String(value || '').trim());
345
359
  }
@@ -4667,42 +4681,67 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null, get
4667
4681
  };
4668
4682
  }
4669
4683
 
4684
+ function getFastRuntimeSpec() {
4685
+ return resolveFastPlatformSpec({
4686
+ platform: os.platform(),
4687
+ arch: os.arch(),
4688
+ manifest: readClientPackageManifest()
4689
+ });
4690
+ }
4691
+
4670
4692
  function getFastRuntime() {
4671
- const platform = os.platform();
4672
- const arch = os.arch();
4673
- const resolvePackagedRuntime = (packageName, rid, executableName) => {
4674
- try {
4675
- const packagePath = require.resolve(`${packageName}/package.json`);
4676
- const fastRoot = join(dirname(packagePath), 'fast');
4677
- return {
4678
- rid,
4679
- packageName,
4680
- executable: join(fastRoot, executableName),
4681
- dll: join(fastRoot, 'livedesk-client-fast.dll')
4682
- };
4683
- } catch {
4684
- return {
4685
- rid,
4686
- packageName: '',
4687
- executable: join(packageRoot, 'fast', rid, executableName),
4688
- dll: join(packageRoot, 'fast', rid, 'livedesk-client-fast.dll')
4689
- };
4690
- }
4693
+ const spec = getFastRuntimeSpec();
4694
+ if (!spec) return null;
4695
+ try {
4696
+ const packagePath = require.resolve(`${spec.packageName}/package.json`);
4697
+ const installed = inspectFastRuntimePackage(dirname(packagePath), spec);
4698
+ if (installed) return { ...installed, source: 'optional-dependency' };
4699
+ } catch {
4700
+ // A cached npx install can be complete except for its optional platform package.
4701
+ }
4702
+ const bundledFastRoot = join(packageRoot, 'fast', spec.rid);
4703
+ const bundled = {
4704
+ rid: spec.rid,
4705
+ packageName: '',
4706
+ packageVersion: spec.version,
4707
+ executable: join(bundledFastRoot, spec.executableName),
4708
+ dll: join(bundledFastRoot, 'livedesk-client-fast.dll'),
4709
+ source: 'bundled-development-runtime'
4691
4710
  };
4692
- if (platform === 'win32' && arch === 'x64') {
4693
- return resolvePackagedRuntime('@livedesk/fast-win-x64', 'win-x64', 'livedesk-client-fast.exe');
4694
- }
4695
- if (platform === 'darwin' && arch === 'arm64') {
4696
- return resolvePackagedRuntime('@livedesk/fast-osx-arm64', 'osx-arm64', 'livedesk-client-fast');
4697
- }
4698
- if (platform === 'darwin' && arch === 'x64') {
4699
- return resolvePackagedRuntime('@livedesk/fast-osx-x64', 'osx-x64', 'livedesk-client-fast');
4700
- }
4701
- if (platform === 'linux' && arch === 'x64') {
4702
- return resolvePackagedRuntime('@livedesk/fast-linux-x64', 'linux-x64', 'livedesk-client-fast');
4703
- }
4704
-
4705
- return null;
4711
+ if (hasFastExecutable(bundled) || hasFastDll(bundled)) return bundled;
4712
+ const repaired = inspectRepairedFastRuntime(UNIFIED_CLIENT_STATE_DIR, spec);
4713
+ if (repaired) return { ...repaired, source: 'repaired-runtime-cache' };
4714
+ return { ...bundled, packageName: spec.packageName, source: 'missing' };
4715
+ }
4716
+
4717
+ async function repairFastRuntimeIfMissing(runtime, connectionPage = null) {
4718
+ if (hasFastExecutable(runtime) || hasFastDll(runtime)) return runtime;
4719
+ const spec = getFastRuntimeSpec();
4720
+ if (!spec) return runtime;
4721
+ const label = `${spec.packageName}@${spec.version}`;
4722
+ console.warn(`[LiveDesk Client] RemoteFast ${spec.rid} is missing from the npx installation. Repairing ${label}.`);
4723
+ connectionPage?.update({
4724
+ message: `Installing the verified ${spec.rid} screen engine. This is needed only when the npx cache omitted it.`,
4725
+ agent: {
4726
+ requestedEngine: 'fast',
4727
+ engine: 'fast',
4728
+ state: 'repairing',
4729
+ runtimeId: spec.rid,
4730
+ command: label,
4731
+ args: []
4732
+ }
4733
+ });
4734
+ const repaired = await ensureRepairedFastRuntime({
4735
+ stateDir: UNIFIED_CLIENT_STATE_DIR,
4736
+ spec,
4737
+ nodeExecutable: process.execPath,
4738
+ npmExecPath: process.env.npm_execpath || process.env.NPM_EXECPATH,
4739
+ npmExecutable: process.env.LIVEDESK_NPM_EXECUTABLE,
4740
+ env: process.env
4741
+ });
4742
+ clearFastPreflightCache();
4743
+ console.log(`[LiveDesk Client] Repaired ${label} in the private LiveDesk runtime cache.`);
4744
+ return { ...repaired, source: 'repaired-runtime-cache' };
4706
4745
  }
4707
4746
 
4708
4747
  function summarizeText(value, maxLength = 600) {
@@ -5306,6 +5345,12 @@ export async function runClientRuntime(argv = process.argv.slice(2), runtimeOpti
5306
5345
  };
5307
5346
  let connectionPage = null;
5308
5347
  if (isTruthy(process.env.LIVEDESK_UNIFIED_RUNTIME)) {
5348
+ // Hydrate the local runtime from the DPAPI/keychain-backed saved session
5349
+ // before opening the browser. Starting the page as anonymous and only
5350
+ // reading this session later lets the renderer begin a redundant Google
5351
+ // OAuth flow on every process restart. The discovery path below still
5352
+ // refreshes and installs this session into the exact Supabase client.
5353
+ const persistedStartupSession = readSavedSessionFromFile();
5309
5354
  let resolveStarted;
5310
5355
  let rejectStarted;
5311
5356
  const started = new Promise((resolve, reject) => {
@@ -5319,8 +5364,8 @@ export async function runClientRuntime(argv = process.argv.slice(2), runtimeOpti
5319
5364
  engine: parsed.engine,
5320
5365
  slot: parsed.slot,
5321
5366
  startupArgs: buildStartupClientArgs(parsed),
5322
- savedSession: null,
5323
- loadSavedSession: false,
5367
+ savedSession: persistedStartupSession,
5368
+ loadSavedSession: true,
5324
5369
  savedPin: null,
5325
5370
  allowRelayFallback: transportAllowsRelay(parsed.transport),
5326
5371
  relayEndpoint: parsed.relay,
@@ -5356,7 +5401,31 @@ export async function runClientRuntime(argv = process.argv.slice(2), runtimeOpti
5356
5401
  connectionPage?.close?.();
5357
5402
  process.exit(0);
5358
5403
  }
5359
- const fastRuntime = getFastRuntime();
5404
+ let fastRuntime = getFastRuntime();
5405
+ const fastSpec = getFastRuntimeSpec();
5406
+ let fastRepairError = null;
5407
+ if (prepared.engine !== 'node'
5408
+ && fastSpec
5409
+ && !hasFastExecutable(fastRuntime)
5410
+ && !hasFastDll(fastRuntime)) {
5411
+ try {
5412
+ fastRuntime = await repairFastRuntimeIfMissing(fastRuntime, prepared.connectionPage);
5413
+ } catch (error) {
5414
+ fastRepairError = error;
5415
+ const message = summarizeText(error?.message || error, 600);
5416
+ console.error(`[LiveDesk Client] RemoteFast repair failed: ${message}`);
5417
+ prepared.connectionPage?.update({
5418
+ agent: {
5419
+ requestedEngine: prepared.engine,
5420
+ engine: 'fast',
5421
+ state: 'failed',
5422
+ runtimeId: fastSpec.rid,
5423
+ error: message
5424
+ },
5425
+ message: 'The screen engine could not be restored. LiveDesk did not start a reduced Node connection.'
5426
+ });
5427
+ }
5428
+ }
5360
5429
  const fastRequired = requiresFastTransport(prepared);
5361
5430
  if (fastRequired && prepared.engine === 'node') {
5362
5431
  throw new Error(
@@ -5407,13 +5476,25 @@ export async function runClientRuntime(argv = process.argv.slice(2), runtimeOpti
5407
5476
  state: 'running'
5408
5477
  });
5409
5478
  });
5410
- } else if (prepared.engine === 'fast' || fastRequired) {
5411
- console.error(
5412
- `C# RemoteFast is unavailable: ${fastLaunch.reason}. `
5413
- + (fastRequired
5414
- ? 'The requested P2P, WebSocket, or encrypted relay transport requires the packaged RemoteFast runtime; the legacy Node agent was not started.'
5415
- : 'Use --engine node to run the legacy Node agent.')
5416
- );
5479
+ } else if (prepared.engine === 'fast'
5480
+ || fastRequired
5481
+ || fastRepairError
5482
+ || (fastSpec && fastRuntime?.source === 'missing')) {
5483
+ const repairDetail = fastRepairError
5484
+ ? ` Automatic repair failed: ${summarizeText(fastRepairError?.message || fastRepairError, 500)}`
5485
+ : '';
5486
+ const unavailableMessage = `C# RemoteFast is unavailable: ${fastLaunch.reason}.${repairDetail}`;
5487
+ console.error(`${unavailableMessage} The reduced Node connection was not started because it cannot provide the required LiveDesk screen stream.`);
5488
+ prepared.connectionPage?.update({
5489
+ agent: {
5490
+ requestedEngine: prepared.engine,
5491
+ engine: 'fast',
5492
+ state: 'failed',
5493
+ runtimeId: fastRuntime?.rid || fastSpec?.rid || '',
5494
+ error: unavailableMessage
5495
+ },
5496
+ message: 'LiveDesk could not start the verified screen engine. Restart the same @latest command after network access is restored.'
5497
+ });
5417
5498
  prepared.connectionPage?.close?.();
5418
5499
  process.exit(2);
5419
5500
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/client",
3
- "version": "0.1.232",
3
+ "version": "0.1.234",
4
4
  "description": "LiveDesk local remote client",
5
5
  "type": "module",
6
6
  "bin": {
@@ -42,10 +42,10 @@
42
42
  "ws": "^8.18.3"
43
43
  },
44
44
  "optionalDependencies": {
45
- "@livedesk/fast-linux-x64": "0.1.431",
46
- "@livedesk/fast-osx-arm64": "0.1.431",
47
- "@livedesk/fast-osx-x64": "0.1.431",
48
- "@livedesk/fast-win-x64": "0.1.431"
45
+ "@livedesk/fast-linux-x64": "0.1.432",
46
+ "@livedesk/fast-osx-arm64": "0.1.432",
47
+ "@livedesk/fast-osx-x64": "0.1.432",
48
+ "@livedesk/fast-win-x64": "0.1.432"
49
49
  },
50
50
  "publishConfig": {
51
51
  "access": "public"
@@ -0,0 +1,300 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { spawn } from 'node:child_process';
3
+ import {
4
+ existsSync,
5
+ mkdirSync,
6
+ readFileSync,
7
+ renameSync,
8
+ rmSync,
9
+ statSync,
10
+ writeFileSync
11
+ } from 'node:fs';
12
+ import { dirname, join, resolve } from 'node:path';
13
+
14
+ const DEFAULT_TIMEOUT_MS = 120_000;
15
+ const LOCK_POLL_MS = 200;
16
+ const LOCK_STALE_MS = 10 * 60_000;
17
+ const MAX_OUTPUT_CHARS = 64 * 1024;
18
+
19
+ const PLATFORM_SPECS = Object.freeze({
20
+ 'win32:x64': Object.freeze({
21
+ packageName: '@livedesk/fast-win-x64',
22
+ rid: 'win-x64',
23
+ executableName: 'livedesk-client-fast.exe'
24
+ }),
25
+ 'linux:x64': Object.freeze({
26
+ packageName: '@livedesk/fast-linux-x64',
27
+ rid: 'linux-x64',
28
+ executableName: 'livedesk-client-fast'
29
+ }),
30
+ 'darwin:x64': Object.freeze({
31
+ packageName: '@livedesk/fast-osx-x64',
32
+ rid: 'osx-x64',
33
+ executableName: 'livedesk-client-fast'
34
+ }),
35
+ 'darwin:arm64': Object.freeze({
36
+ packageName: '@livedesk/fast-osx-arm64',
37
+ rid: 'osx-arm64',
38
+ executableName: 'livedesk-client-fast'
39
+ })
40
+ });
41
+
42
+ function exactVersion(value) {
43
+ const version = String(value || '').trim();
44
+ return /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(version)
45
+ ? version
46
+ : '';
47
+ }
48
+
49
+ function boundedText(previous, chunk) {
50
+ const next = `${previous}${String(chunk || '')}`;
51
+ return next.length <= MAX_OUTPUT_CHARS ? next : next.slice(next.length - MAX_OUTPUT_CHARS);
52
+ }
53
+
54
+ function delay(milliseconds) {
55
+ return new Promise(resolveDelay => setTimeout(resolveDelay, milliseconds));
56
+ }
57
+
58
+ function packageDirectory(installRoot, packageName) {
59
+ return join(installRoot, 'node_modules', ...String(packageName).split('/'));
60
+ }
61
+
62
+ function safeSegment(value) {
63
+ return String(value || '').replace(/[^A-Za-z0-9._-]/g, '_').slice(0, 160) || 'unknown';
64
+ }
65
+
66
+ function readJson(filePath) {
67
+ try {
68
+ return JSON.parse(readFileSync(filePath, 'utf8'));
69
+ } catch {
70
+ return null;
71
+ }
72
+ }
73
+
74
+ function isPidAlive(value) {
75
+ const pid = Number(value);
76
+ if (!Number.isInteger(pid) || pid <= 1) return false;
77
+ try {
78
+ process.kill(pid, 0);
79
+ return true;
80
+ } catch (error) {
81
+ return error?.code === 'EPERM';
82
+ }
83
+ }
84
+
85
+ function reclaimStaleLock(lockPath) {
86
+ try {
87
+ const owner = readJson(join(lockPath, 'owner.json'));
88
+ const ageMs = Math.max(0, Date.now() - Number(statSync(lockPath).mtimeMs || 0));
89
+ const ownerAlive = isPidAlive(owner?.pid);
90
+ if ((owner?.pid && !ownerAlive && ageMs >= 1_000) || ageMs >= LOCK_STALE_MS) {
91
+ rmSync(lockPath, { recursive: true, force: true });
92
+ return true;
93
+ }
94
+ } catch {
95
+ // A concurrent owner may be creating or removing the lock.
96
+ }
97
+ return false;
98
+ }
99
+
100
+ export function resolveFastPlatformSpec({
101
+ platform = process.platform,
102
+ arch = process.arch,
103
+ manifest = null
104
+ } = {}) {
105
+ const base = PLATFORM_SPECS[`${platform}:${arch}`];
106
+ if (!base) return null;
107
+ const version = exactVersion(manifest?.optionalDependencies?.[base.packageName]);
108
+ if (!version) return null;
109
+ return Object.freeze({ ...base, version });
110
+ }
111
+
112
+ export function inspectFastRuntimePackage(packageRoot, spec) {
113
+ if (!packageRoot || !spec) return null;
114
+ const manifest = readJson(join(packageRoot, 'package.json'));
115
+ if (manifest?.name !== spec.packageName || manifest?.version !== spec.version) return null;
116
+ if (Array.isArray(manifest.os) && !manifest.os.includes(process.platform)) return null;
117
+ if (Array.isArray(manifest.cpu) && !manifest.cpu.includes(process.arch)) return null;
118
+ const fastRoot = join(packageRoot, 'fast');
119
+ const executable = join(fastRoot, spec.executableName);
120
+ const dll = join(fastRoot, 'livedesk-client-fast.dll');
121
+ if (!existsSync(executable) && !existsSync(dll)) return null;
122
+ return Object.freeze({
123
+ rid: spec.rid,
124
+ packageName: spec.packageName,
125
+ packageVersion: spec.version,
126
+ packageRoot,
127
+ executable,
128
+ dll
129
+ });
130
+ }
131
+
132
+ export function repairedFastInstallRoot(stateDir, spec) {
133
+ return join(
134
+ resolve(stateDir),
135
+ 'runtime-packages',
136
+ `${safeSegment(spec.packageName)}-${safeSegment(spec.version)}`
137
+ );
138
+ }
139
+
140
+ export function inspectRepairedFastRuntime(stateDir, spec) {
141
+ const installRoot = repairedFastInstallRoot(stateDir, spec);
142
+ return inspectFastRuntimePackage(packageDirectory(installRoot, spec.packageName), spec);
143
+ }
144
+
145
+ function resolveNpmInvocation({ nodeExecutable, npmExecPath, npmExecutable }) {
146
+ const exactNode = resolve(String(nodeExecutable || process.execPath));
147
+ const cli = String(npmExecPath || '').trim();
148
+ const cliCandidates = cli
149
+ ? (/npx-cli\.js$/i.test(cli) ? [join(dirname(cli), 'npm-cli.js')] : [cli])
150
+ : [];
151
+ const npmCli = cliCandidates.find(candidate => existsSync(candidate));
152
+ if (npmCli) {
153
+ return { command: exactNode, argsPrefix: [resolve(npmCli)] };
154
+ }
155
+ const executable = String(npmExecutable || '').trim();
156
+ if (executable) return { command: executable, argsPrefix: [] };
157
+ const nodeDir = dirname(exactNode);
158
+ const candidates = process.platform === 'win32'
159
+ ? [join(nodeDir, 'npm.cmd'), join(nodeDir, 'npm.exe')]
160
+ : [join(nodeDir, 'npm')];
161
+ const fallback = candidates.find(existsSync);
162
+ return fallback ? { command: fallback, argsPrefix: [] } : null;
163
+ }
164
+
165
+ function runInstall({ invocation, installRoot, spec, timeoutMs, env }) {
166
+ return new Promise((resolveInstall, rejectInstall) => {
167
+ const args = [
168
+ ...invocation.argsPrefix,
169
+ 'install',
170
+ '--ignore-scripts',
171
+ '--no-audit',
172
+ '--no-fund',
173
+ '--package-lock=false',
174
+ '--save=false',
175
+ '--omit=dev',
176
+ '--prefix', installRoot,
177
+ `${spec.packageName}@${spec.version}`
178
+ ];
179
+ const child = spawn(invocation.command, args, {
180
+ env: {
181
+ ...env,
182
+ npm_config_audit: 'false',
183
+ npm_config_fund: 'false',
184
+ npm_config_ignore_scripts: 'true',
185
+ npm_config_package_lock: 'false'
186
+ },
187
+ windowsHide: true,
188
+ stdio: ['ignore', 'pipe', 'pipe']
189
+ });
190
+ let stdout = '';
191
+ let stderr = '';
192
+ let settled = false;
193
+ let timer = null;
194
+ const finish = (error, result = null) => {
195
+ if (settled) return;
196
+ settled = true;
197
+ if (timer) clearTimeout(timer);
198
+ if (error) rejectInstall(error);
199
+ else resolveInstall(result);
200
+ };
201
+ child.stdout?.on('data', chunk => { stdout = boundedText(stdout, chunk); });
202
+ child.stderr?.on('data', chunk => { stderr = boundedText(stderr, chunk); });
203
+ child.once('error', error => finish(error));
204
+ child.once('exit', (code, signal) => finish(null, {
205
+ code: Number.isInteger(code) ? code : 1,
206
+ signal: String(signal || ''),
207
+ stdout,
208
+ stderr
209
+ }));
210
+ timer = setTimeout(() => {
211
+ try { child.kill('SIGKILL'); } catch { }
212
+ finish(new Error(`remote-fast-repair-timeout:${timeoutMs}`));
213
+ }, timeoutMs);
214
+ timer.unref?.();
215
+ });
216
+ }
217
+
218
+ export async function ensureRepairedFastRuntime({
219
+ stateDir,
220
+ spec,
221
+ nodeExecutable = process.execPath,
222
+ npmExecPath = process.env.npm_execpath || process.env.NPM_EXECPATH,
223
+ npmExecutable = process.env.LIVEDESK_NPM_EXECUTABLE,
224
+ timeoutMs = DEFAULT_TIMEOUT_MS,
225
+ env = process.env,
226
+ installRunner = runInstall
227
+ } = {}) {
228
+ if (!stateDir || !spec) throw new Error('remote-fast-repair-plan-required');
229
+ const alreadyRepaired = inspectRepairedFastRuntime(stateDir, spec);
230
+ if (alreadyRepaired) return alreadyRepaired;
231
+
232
+ const installRoot = repairedFastInstallRoot(stateDir, spec);
233
+ mkdirSync(dirname(installRoot), { recursive: true });
234
+ const lockPath = `${installRoot}.lock`;
235
+ const deadline = Date.now() + Math.max(10_000, Math.min(300_000, Number(timeoutMs) || DEFAULT_TIMEOUT_MS));
236
+ let ownsLock = false;
237
+ while (!ownsLock && Date.now() < deadline) {
238
+ try {
239
+ mkdirSync(lockPath, { recursive: false });
240
+ writeFileSync(join(lockPath, 'owner.json'), JSON.stringify({
241
+ pid: process.pid,
242
+ createdAt: new Date().toISOString(),
243
+ packageName: spec.packageName,
244
+ version: spec.version
245
+ }), { encoding: 'utf8', mode: 0o600 });
246
+ ownsLock = true;
247
+ } catch {
248
+ const concurrent = inspectRepairedFastRuntime(stateDir, spec);
249
+ if (concurrent) return concurrent;
250
+ if (!reclaimStaleLock(lockPath)) await delay(LOCK_POLL_MS);
251
+ }
252
+ }
253
+ if (!ownsLock) throw new Error('remote-fast-repair-lock-timeout');
254
+
255
+ const temporaryRoot = `${installRoot}.tmp-${process.pid}-${randomBytes(6).toString('hex')}`;
256
+ try {
257
+ const afterLock = inspectRepairedFastRuntime(stateDir, spec);
258
+ if (afterLock) return afterLock;
259
+ rmSync(temporaryRoot, { recursive: true, force: true });
260
+ mkdirSync(temporaryRoot, { recursive: true });
261
+ writeFileSync(join(temporaryRoot, 'package.json'), JSON.stringify({
262
+ private: true,
263
+ name: 'livedesk-fast-runtime-repair',
264
+ version: '0.0.0'
265
+ }), { encoding: 'utf8', mode: 0o600 });
266
+
267
+ const invocation = resolveNpmInvocation({ nodeExecutable, npmExecPath, npmExecutable });
268
+ if (!invocation && installRunner === runInstall) throw new Error('remote-fast-repair-npm-unavailable');
269
+ const remainingMs = Math.max(1_000, deadline - Date.now());
270
+ const result = await installRunner({
271
+ invocation,
272
+ installRoot: temporaryRoot,
273
+ spec,
274
+ timeoutMs: remainingMs,
275
+ env
276
+ });
277
+ if (result?.code !== 0) {
278
+ const detail = String(result?.stderr || result?.stdout || `exit-${result?.code ?? 'unknown'}`)
279
+ .replace(/\s+/g, ' ')
280
+ .trim()
281
+ .slice(0, 600);
282
+ throw new Error(`remote-fast-repair-install-failed:${detail || 'unknown'}`);
283
+ }
284
+ const temporaryRuntime = inspectFastRuntimePackage(
285
+ packageDirectory(temporaryRoot, spec.packageName),
286
+ spec
287
+ );
288
+ if (!temporaryRuntime) throw new Error('remote-fast-repair-package-invalid');
289
+
290
+ rmSync(installRoot, { recursive: true, force: true });
291
+ mkdirSync(dirname(installRoot), { recursive: true });
292
+ renameSync(temporaryRoot, installRoot);
293
+ const committed = inspectRepairedFastRuntime(stateDir, spec);
294
+ if (!committed) throw new Error('remote-fast-repair-commit-invalid');
295
+ return committed;
296
+ } finally {
297
+ rmSync(temporaryRoot, { recursive: true, force: true });
298
+ rmSync(lockPath, { recursive: true, force: true });
299
+ }
300
+ }