@msvesper/vesper-link 0.1.4
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/LICENSE.md +44 -0
- package/README.md +26 -0
- package/dist/atomic-json.d.ts +3 -0
- package/dist/atomic-json.js +47 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +170 -0
- package/dist/collectors/claude.d.ts +5 -0
- package/dist/collectors/claude.js +42 -0
- package/dist/collectors/codex.d.ts +5 -0
- package/dist/collectors/codex.js +65 -0
- package/dist/collectors/copilot.d.ts +5 -0
- package/dist/collectors/copilot.js +64 -0
- package/dist/collectors/index.d.ts +4 -0
- package/dist/collectors/index.js +3 -0
- package/dist/collectors/types.d.ts +16 -0
- package/dist/collectors/types.js +1 -0
- package/dist/companion.d.ts +12 -0
- package/dist/companion.js +62 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.js +41 -0
- package/dist/contracts.d.ts +83 -0
- package/dist/contracts.js +144 -0
- package/dist/diagnostics.d.ts +37 -0
- package/dist/diagnostics.js +64 -0
- package/dist/fs-utils.d.ts +5 -0
- package/dist/fs-utils.js +69 -0
- package/dist/hook-report.d.ts +8 -0
- package/dist/hook-report.js +21 -0
- package/dist/http-client.d.ts +25 -0
- package/dist/http-client.js +228 -0
- package/dist/install-args.d.ts +7 -0
- package/dist/install-args.js +35 -0
- package/dist/installation.d.ts +59 -0
- package/dist/installation.js +524 -0
- package/dist/installer.d.ts +59 -0
- package/dist/installer.js +214 -0
- package/dist/lock.d.ts +5 -0
- package/dist/lock.js +57 -0
- package/dist/managed-runtime.d.ts +33 -0
- package/dist/managed-runtime.js +130 -0
- package/dist/memory-mcp.d.ts +27 -0
- package/dist/memory-mcp.js +155 -0
- package/dist/operational-log.d.ts +17 -0
- package/dist/operational-log.js +48 -0
- package/dist/paths.d.ts +23 -0
- package/dist/paths.js +36 -0
- package/dist/pending.d.ts +7 -0
- package/dist/pending.js +35 -0
- package/dist/process-runner.d.ts +11 -0
- package/dist/process-runner.js +21 -0
- package/dist/provider-integrations.d.ts +42 -0
- package/dist/provider-integrations.js +339 -0
- package/dist/queue.d.ts +19 -0
- package/dist/queue.js +129 -0
- package/dist/scheduler.d.ts +51 -0
- package/dist/scheduler.js +176 -0
- package/dist/session.d.ts +50 -0
- package/dist/session.js +12 -0
- package/dist/source.d.ts +11 -0
- package/dist/source.js +45 -0
- package/dist/state.d.ts +11 -0
- package/dist/state.js +37 -0
- package/dist/sync.d.ts +18 -0
- package/dist/sync.js +247 -0
- package/dist/transport.d.ts +27 -0
- package/dist/transport.js +400 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/package.json +39 -0
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { readJsonFile, writeJsonAtomic } from './atomic-json.js';
|
|
5
|
+
import { stableLauncherSource } from './managed-runtime.js';
|
|
6
|
+
import { acquireSyncLock } from './lock.js';
|
|
7
|
+
import { NodeProcessRunner } from './process-runner.js';
|
|
8
|
+
import { applyProviderIntegrations, detectInstalledProviders, providerLocations, providerTargetPaths, removeProviderIntegrations, } from './provider-integrations.js';
|
|
9
|
+
import { applyScheduler, assertSchedulerOwned, prepareScheduler, restoreScheduler, schedulerInstalled, stableMinuteOffset, uninstallScheduler, } from './scheduler.js';
|
|
10
|
+
async function snapshotFile(filePath) {
|
|
11
|
+
try {
|
|
12
|
+
return { filePath, existed: true, content: await fs.readFile(filePath) };
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
if (error.code === 'ENOENT') {
|
|
16
|
+
return { filePath, existed: false };
|
|
17
|
+
}
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async function restoreFile(snapshot) {
|
|
22
|
+
if (!snapshot.existed) {
|
|
23
|
+
await fs.rm(snapshot.filePath, { force: true });
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
await fs.mkdir(path.dirname(snapshot.filePath), { recursive: true });
|
|
27
|
+
const temporary = snapshot.filePath + '.' + process.pid + '.rollback';
|
|
28
|
+
try {
|
|
29
|
+
await fs.writeFile(temporary, snapshot.content);
|
|
30
|
+
await fs.rename(temporary, snapshot.filePath);
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
await fs.rm(temporary, { force: true }).catch(() => undefined);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function ensureLauncher(paths) {
|
|
37
|
+
await fs.mkdir(path.dirname(paths.launcherPath), { recursive: true });
|
|
38
|
+
const desired = stableLauncherSource();
|
|
39
|
+
const current = await fs.readFile(paths.launcherPath, 'utf8').catch((error) => {
|
|
40
|
+
if (error.code === 'ENOENT') {
|
|
41
|
+
return '';
|
|
42
|
+
}
|
|
43
|
+
throw error;
|
|
44
|
+
});
|
|
45
|
+
if (current !== desired) {
|
|
46
|
+
await fs.writeFile(paths.launcherPath, desired, 'utf8');
|
|
47
|
+
if (process.platform !== 'win32') {
|
|
48
|
+
await fs.chmod(paths.launcherPath, 0o755);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async function readJsonObject(filePath) {
|
|
53
|
+
const value = await readJsonFile(filePath);
|
|
54
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
55
|
+
throw new Error('Configuration file must contain a JSON object: ' + filePath);
|
|
56
|
+
}
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
function containsJson(value, expected) {
|
|
60
|
+
if (JSON.stringify(value) === JSON.stringify(expected)) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
if (Array.isArray(value)) {
|
|
64
|
+
return value.some((entry) => containsJson(entry, expected));
|
|
65
|
+
}
|
|
66
|
+
if (value && typeof value === 'object') {
|
|
67
|
+
return Object.values(value).some((entry) => containsJson(entry, expected));
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
async function verifyProviderIntegrations(paths, state) {
|
|
72
|
+
const locations = providerLocations(paths);
|
|
73
|
+
const owned = state.integrations;
|
|
74
|
+
if (owned.claudeHook && !containsJson(await readJsonObject(locations.claudeSettingsPath), owned.claudeHook)) {
|
|
75
|
+
throw new Error('Claude hook is missing after install');
|
|
76
|
+
}
|
|
77
|
+
if (owned.copilotHook && !containsJson(await readJsonObject(locations.copilotSettingsPath), owned.copilotHook)) {
|
|
78
|
+
throw new Error('Copilot hook is missing after install');
|
|
79
|
+
}
|
|
80
|
+
if (state.sourceTrust === 'trusted') {
|
|
81
|
+
if (owned.claudeMcp && !containsJson(await readJsonObject(locations.claudeUserConfigPath), owned.claudeMcp)) {
|
|
82
|
+
throw new Error('Claude MCP is missing after install');
|
|
83
|
+
}
|
|
84
|
+
if (owned.copilotMcp && !containsJson(await readJsonObject(locations.copilotMcpPath), owned.copilotMcp)) {
|
|
85
|
+
throw new Error('Copilot MCP is missing after install');
|
|
86
|
+
}
|
|
87
|
+
for (const [filePath, marker] of [
|
|
88
|
+
[locations.claudeInstructionPath, owned.claudeInstruction],
|
|
89
|
+
[locations.copilotInstructionPath, owned.copilotInstruction],
|
|
90
|
+
[locations.codexConfigPath, owned.codexMcpBlock],
|
|
91
|
+
[locations.codexInstructionPath, owned.codexInstruction],
|
|
92
|
+
]) {
|
|
93
|
+
if (marker && !(await fs.readFile(filePath, 'utf8')).includes(marker)) {
|
|
94
|
+
throw new Error('Managed provider block is missing after install: ' + filePath);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
export async function verifyInstalledState(paths, expected, runner = new NodeProcessRunner()) {
|
|
100
|
+
const current = await readJsonFile(paths.currentPath);
|
|
101
|
+
if (!current || current.schemaVersion !== 1 || current.version !== expected.runtime.version
|
|
102
|
+
|| current.runtimePath !== expected.runtime.runtimePath) {
|
|
103
|
+
throw new Error('Managed runtime pointer is not current');
|
|
104
|
+
}
|
|
105
|
+
await fs.access(path.join(current.runtimePath, 'package', 'dist', 'cli.js'));
|
|
106
|
+
if (!await schedulerInstalled(expected.scheduler, runner)) {
|
|
107
|
+
throw new Error('Scheduler is missing after install');
|
|
108
|
+
}
|
|
109
|
+
await verifyProviderIntegrations(paths, expected);
|
|
110
|
+
}
|
|
111
|
+
async function backupSnapshot(snapshot, backupDir) {
|
|
112
|
+
if (!snapshot.existed) {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
await fs.mkdir(backupDir, { recursive: true });
|
|
116
|
+
const name = Buffer.from(snapshot.filePath).toString('base64url') + '.before-vesper-link';
|
|
117
|
+
const backupPath = path.join(backupDir, name);
|
|
118
|
+
try {
|
|
119
|
+
await fs.writeFile(backupPath, snapshot.content, { flag: 'wx' });
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (error.code !== 'EEXIST') {
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return backupPath;
|
|
127
|
+
}
|
|
128
|
+
export async function readInstalledState(paths) {
|
|
129
|
+
const state = await readJsonFile(paths.installStatePath);
|
|
130
|
+
if (!state) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
if (state.schemaVersion !== 2 || !state.installationId || !state.runtime || !state.scheduler || !state.integrations
|
|
134
|
+
|| !Array.isArray(state.createdProviderFiles)) {
|
|
135
|
+
throw new Error('Malformed installed state: ' + paths.installStatePath);
|
|
136
|
+
}
|
|
137
|
+
return { ...state, paused: state.paused === true };
|
|
138
|
+
}
|
|
139
|
+
async function prepareInstall(options) {
|
|
140
|
+
const previous = await readInstalledState(options.paths);
|
|
141
|
+
const providers = options.providers ?? await detectInstalledProviders(options.paths);
|
|
142
|
+
const targetPaths = providerTargetPaths(options.paths, providers);
|
|
143
|
+
const [currentPointer, installState, launcher, config, ...providerFiles] = await Promise.all([
|
|
144
|
+
snapshotFile(options.paths.currentPath), snapshotFile(options.paths.installStatePath),
|
|
145
|
+
snapshotFile(options.paths.launcherPath), snapshotFile(options.paths.configPath), ...targetPaths.map(snapshotFile),
|
|
146
|
+
]);
|
|
147
|
+
// Validate every provider file before the first transaction mutation.
|
|
148
|
+
for (const snapshot of providerFiles) {
|
|
149
|
+
if (snapshot.existed && snapshot.filePath.endsWith('.json')) {
|
|
150
|
+
JSON.parse(snapshot.content.toString('utf8'));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const installationId = previous?.installationId ?? randomUUID();
|
|
154
|
+
const minuteOffset = previous?.minuteOffset ?? stableMinuteOffset(options.paths.homeDir);
|
|
155
|
+
const scheduler = await prepareScheduler({
|
|
156
|
+
platform: options.platform, runner: options.runner, minuteOffset, nodePath: options.nodePath,
|
|
157
|
+
launcherPath: options.paths.launcherPath, installationId,
|
|
158
|
+
});
|
|
159
|
+
const backups = { ...previous?.backups };
|
|
160
|
+
const backupDir = path.join(options.paths.stateDir, 'backups');
|
|
161
|
+
for (const snapshot of providerFiles) {
|
|
162
|
+
if (!backups[snapshot.filePath]) {
|
|
163
|
+
const backup = await backupSnapshot(snapshot, backupDir);
|
|
164
|
+
if (backup) {
|
|
165
|
+
backups[snapshot.filePath] = backup;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
previous, currentPointer, installState, launcher, config, providerFiles,
|
|
171
|
+
schedulerState: scheduler.state, schedulerSnapshot: scheduler.snapshot, providers, backups,
|
|
172
|
+
installationId,
|
|
173
|
+
createdProviderFiles: [...new Set([
|
|
174
|
+
...(previous?.createdProviderFiles ?? []),
|
|
175
|
+
...providerFiles.filter((snapshot) => !snapshot.existed).map((snapshot) => snapshot.filePath),
|
|
176
|
+
])],
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
export async function installManagedRuntime(options) {
|
|
180
|
+
const runner = options.runner ?? new NodeProcessRunner();
|
|
181
|
+
const platform = options.platform ?? process.platform;
|
|
182
|
+
const nodePath = options.nodePath ?? process.execPath;
|
|
183
|
+
const now = options.now ?? new Date();
|
|
184
|
+
const prepared = await prepareInstall({
|
|
185
|
+
paths: options.paths, runtime: options.runtime, host: options.host,
|
|
186
|
+
sourceTrust: options.capabilities.sourceTrust, providers: options.providers,
|
|
187
|
+
nodePath, platform, runner, now,
|
|
188
|
+
});
|
|
189
|
+
const installationId = prepared.installationId;
|
|
190
|
+
const pointer = {
|
|
191
|
+
schemaVersion: 1, version: options.runtime.version, runtimePath: options.runtime.runtimePath,
|
|
192
|
+
};
|
|
193
|
+
let providersApplied = false;
|
|
194
|
+
let schedulerApplied = false;
|
|
195
|
+
try {
|
|
196
|
+
await ensureLauncher(options.paths);
|
|
197
|
+
await writeJsonAtomic(options.paths.currentPath, pointer);
|
|
198
|
+
await options.faults?.afterStep?.('current');
|
|
199
|
+
await writeJsonAtomic(options.paths.configPath, { schemaVersion: 1, host: options.host });
|
|
200
|
+
await options.faults?.afterStep?.('config');
|
|
201
|
+
providersApplied = true;
|
|
202
|
+
const integrations = await applyProviderIntegrations({
|
|
203
|
+
paths: options.paths, providers: prepared.providers, trusted: options.capabilities.sourceTrust === 'trusted',
|
|
204
|
+
nodePath, launcherPath: options.paths.launcherPath, previous: prepared.previous?.integrations,
|
|
205
|
+
afterMutation: options.faults?.afterProviderMutation,
|
|
206
|
+
});
|
|
207
|
+
await options.faults?.afterStep?.('providers');
|
|
208
|
+
schedulerApplied = true;
|
|
209
|
+
await applyScheduler(prepared.schedulerState, prepared.schedulerSnapshot, runner);
|
|
210
|
+
await options.faults?.afterStep?.('scheduler');
|
|
211
|
+
const installed = {
|
|
212
|
+
schemaVersion: 2, installedAt: now.toISOString(), host: options.host,
|
|
213
|
+
sourceTrust: options.capabilities.sourceTrust, installationId, runtime: pointer, nodePath,
|
|
214
|
+
launcherPath: options.paths.launcherPath, minuteOffset: prepared.schedulerState.minuteOffset,
|
|
215
|
+
scheduler: prepared.schedulerState, integrations, backups: prepared.backups,
|
|
216
|
+
createdProviderFiles: prepared.createdProviderFiles,
|
|
217
|
+
};
|
|
218
|
+
await writeJsonAtomic(options.paths.installStatePath, installed);
|
|
219
|
+
await options.faults?.afterStep?.('state');
|
|
220
|
+
await verifyInstalledState(options.paths, installed, runner);
|
|
221
|
+
await options.postInstallDoctor?.();
|
|
222
|
+
return installed;
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
const rollbackErrors = [];
|
|
226
|
+
if (schedulerApplied) {
|
|
227
|
+
try {
|
|
228
|
+
await restoreScheduler(prepared.schedulerState, prepared.schedulerSnapshot, runner);
|
|
229
|
+
}
|
|
230
|
+
catch (rollback) {
|
|
231
|
+
rollbackErrors.push(rollback);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (providersApplied) {
|
|
235
|
+
for (const snapshot of prepared.providerFiles) {
|
|
236
|
+
try {
|
|
237
|
+
await restoreFile(snapshot);
|
|
238
|
+
}
|
|
239
|
+
catch (rollback) {
|
|
240
|
+
rollbackErrors.push(rollback);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
for (const snapshot of [prepared.config, prepared.installState, prepared.currentPointer, prepared.launcher]) {
|
|
245
|
+
try {
|
|
246
|
+
await restoreFile(snapshot);
|
|
247
|
+
}
|
|
248
|
+
catch (rollback) {
|
|
249
|
+
rollbackErrors.push(rollback);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (rollbackErrors.length > 0) {
|
|
253
|
+
throw new AggregateError([error, ...rollbackErrors], 'Installation failed and rollback was incomplete');
|
|
254
|
+
}
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
export async function uninstallIntegrations(paths, runner = new NodeProcessRunner(), probeProcess = probeProcessState) {
|
|
259
|
+
const resolvedStateDir = path.resolve(paths.stateDir);
|
|
260
|
+
if (resolvedStateDir === path.parse(resolvedStateDir).root) {
|
|
261
|
+
throw new Error('Refusing to uninstall a filesystem root');
|
|
262
|
+
}
|
|
263
|
+
const stateDirectoryExisted = await fs.stat(resolvedStateDir).then((stat) => {
|
|
264
|
+
if (!stat.isDirectory()) {
|
|
265
|
+
throw new Error('Vesper Link state path is not a directory');
|
|
266
|
+
}
|
|
267
|
+
return true;
|
|
268
|
+
}).catch((error) => {
|
|
269
|
+
if (error.code === 'ENOENT') {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
throw error;
|
|
273
|
+
});
|
|
274
|
+
const releaseLock = await acquireSyncLock(paths.lockPath);
|
|
275
|
+
try {
|
|
276
|
+
const state = await readInstalledState(paths);
|
|
277
|
+
if (!state && !stateDirectoryExisted) {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
const defaultStateDir = path.resolve(paths.homeDir, '.vesper-link');
|
|
281
|
+
if (!state && resolvedStateDir !== defaultStateDir) {
|
|
282
|
+
throw new Error('Refusing to remove a custom directory without Vesper Link installed state');
|
|
283
|
+
}
|
|
284
|
+
await assertTransportStopped(paths, probeProcess);
|
|
285
|
+
if (state && !state.paused) {
|
|
286
|
+
await removeActiveIntegrations(paths, state, runner, () => writeJsonAtomic(paths.installStatePath, { ...state, paused: true }));
|
|
287
|
+
}
|
|
288
|
+
await fs.rm(resolvedStateDir, { recursive: true, force: true });
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
finally {
|
|
292
|
+
await releaseLock();
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
async function removeEmptyCreatedProviderFiles(state) {
|
|
296
|
+
for (const filePath of state.createdProviderFiles) {
|
|
297
|
+
try {
|
|
298
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
299
|
+
const emptyJson = filePath.endsWith('.json') && JSON.stringify(JSON.parse(content)) === '{}';
|
|
300
|
+
if (!content.trim() || emptyJson) {
|
|
301
|
+
await fs.rm(filePath, { force: true });
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
if (error.code !== 'ENOENT') {
|
|
306
|
+
throw error;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
async function removeActiveIntegrations(paths, state, runner, finalize) {
|
|
312
|
+
await assertSchedulerOwned(state.scheduler, runner);
|
|
313
|
+
const providerFiles = await Promise.all(providerTargetPaths(paths, state.integrations.providers).map(snapshotFile));
|
|
314
|
+
const scheduler = await prepareScheduler({
|
|
315
|
+
platform: state.scheduler.kind === 'windows-task' ? 'win32' : 'linux', runner,
|
|
316
|
+
minuteOffset: state.minuteOffset, nodePath: state.nodePath,
|
|
317
|
+
launcherPath: state.launcherPath, installationId: state.installationId,
|
|
318
|
+
});
|
|
319
|
+
try {
|
|
320
|
+
await removeProviderIntegrations(paths, state.integrations);
|
|
321
|
+
await uninstallScheduler(state.scheduler, runner);
|
|
322
|
+
await removeEmptyCreatedProviderFiles(state);
|
|
323
|
+
await finalize?.();
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
const rollbackErrors = [];
|
|
327
|
+
for (const snapshot of providerFiles) {
|
|
328
|
+
try {
|
|
329
|
+
await restoreFile(snapshot);
|
|
330
|
+
}
|
|
331
|
+
catch (rollback) {
|
|
332
|
+
rollbackErrors.push(rollback);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
try {
|
|
336
|
+
await restoreScheduler(scheduler.state, scheduler.snapshot, runner);
|
|
337
|
+
}
|
|
338
|
+
catch (rollback) {
|
|
339
|
+
rollbackErrors.push(rollback);
|
|
340
|
+
}
|
|
341
|
+
if (rollbackErrors.length > 0) {
|
|
342
|
+
throw new AggregateError([error, ...rollbackErrors], 'Integration removal failed and rollback was incomplete');
|
|
343
|
+
}
|
|
344
|
+
throw error;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
export async function pauseIntegrations(paths, runner = new NodeProcessRunner()) {
|
|
348
|
+
const releaseLock = await acquireSyncLock(paths.lockPath);
|
|
349
|
+
try {
|
|
350
|
+
const state = await readInstalledState(paths);
|
|
351
|
+
if (!state || state.paused) {
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
await removeActiveIntegrations(paths, state, runner, () => writeJsonAtomic(paths.installStatePath, { ...state, paused: true }));
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
finally {
|
|
358
|
+
await releaseLock();
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
export async function resumeIntegrations(paths, runner = new NodeProcessRunner()) {
|
|
362
|
+
const releaseLock = await acquireSyncLock(paths.lockPath);
|
|
363
|
+
try {
|
|
364
|
+
const state = await readInstalledState(paths);
|
|
365
|
+
if (!state || !state.paused) {
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
const providerFiles = await Promise.all(providerTargetPaths(paths, state.integrations.providers).map(snapshotFile));
|
|
369
|
+
const installState = await snapshotFile(paths.installStatePath);
|
|
370
|
+
const scheduler = await prepareScheduler({
|
|
371
|
+
platform: state.scheduler.kind === 'windows-task' ? 'win32' : 'linux', runner,
|
|
372
|
+
minuteOffset: state.minuteOffset, nodePath: state.nodePath,
|
|
373
|
+
launcherPath: state.launcherPath, installationId: state.installationId,
|
|
374
|
+
});
|
|
375
|
+
let providersApplied = false;
|
|
376
|
+
let schedulerApplied = false;
|
|
377
|
+
try {
|
|
378
|
+
const current = await readJsonFile(paths.currentPath);
|
|
379
|
+
if (!current || current.schemaVersion !== 1 || current.version !== state.runtime.version
|
|
380
|
+
|| current.runtimePath !== state.runtime.runtimePath) {
|
|
381
|
+
throw new Error('Cannot resume because the managed runtime pointer changed');
|
|
382
|
+
}
|
|
383
|
+
await fs.access(path.join(current.runtimePath, 'package', 'dist', 'cli.js'));
|
|
384
|
+
providersApplied = true;
|
|
385
|
+
const integrations = await applyProviderIntegrations({
|
|
386
|
+
paths, providers: state.integrations.providers, trusted: state.sourceTrust === 'trusted',
|
|
387
|
+
nodePath: state.nodePath, launcherPath: state.launcherPath, previous: state.integrations,
|
|
388
|
+
});
|
|
389
|
+
schedulerApplied = true;
|
|
390
|
+
await applyScheduler(scheduler.state, scheduler.snapshot, runner);
|
|
391
|
+
const resumed = { ...state, integrations, paused: false };
|
|
392
|
+
await writeJsonAtomic(paths.installStatePath, resumed);
|
|
393
|
+
await verifyInstalledState(paths, resumed, runner);
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
396
|
+
catch (error) {
|
|
397
|
+
const rollbackErrors = [];
|
|
398
|
+
if (schedulerApplied) {
|
|
399
|
+
try {
|
|
400
|
+
await restoreScheduler(scheduler.state, scheduler.snapshot, runner);
|
|
401
|
+
}
|
|
402
|
+
catch (rollback) {
|
|
403
|
+
rollbackErrors.push(rollback);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (providersApplied) {
|
|
407
|
+
for (const snapshot of providerFiles) {
|
|
408
|
+
try {
|
|
409
|
+
await restoreFile(snapshot);
|
|
410
|
+
}
|
|
411
|
+
catch (rollback) {
|
|
412
|
+
rollbackErrors.push(rollback);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
try {
|
|
417
|
+
await restoreFile(installState);
|
|
418
|
+
}
|
|
419
|
+
catch (rollback) {
|
|
420
|
+
rollbackErrors.push(rollback);
|
|
421
|
+
}
|
|
422
|
+
if (rollbackErrors.length > 0) {
|
|
423
|
+
throw new AggregateError([error, ...rollbackErrors], 'Resume failed and rollback was incomplete');
|
|
424
|
+
}
|
|
425
|
+
throw error;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
finally {
|
|
429
|
+
await releaseLock();
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
function probeProcessState(pid) {
|
|
433
|
+
try {
|
|
434
|
+
process.kill(pid, 0);
|
|
435
|
+
return 'alive';
|
|
436
|
+
}
|
|
437
|
+
catch (error) {
|
|
438
|
+
return error.code === 'ESRCH' ? 'absent' : 'unknown';
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
async function assertTransportStopped(paths, probeProcess = probeProcessState) {
|
|
442
|
+
let rendezvous;
|
|
443
|
+
try {
|
|
444
|
+
rendezvous = JSON.parse(await fs.readFile(paths.transportRendezvousPath, 'utf8'));
|
|
445
|
+
}
|
|
446
|
+
catch (error) {
|
|
447
|
+
if (error.code === 'ENOENT') {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
throw error;
|
|
451
|
+
}
|
|
452
|
+
if (!rendezvous || typeof rendezvous !== 'object' || Array.isArray(rendezvous)) {
|
|
453
|
+
throw new Error('Cannot verify whether vesper-net transport is stopped');
|
|
454
|
+
}
|
|
455
|
+
const pid = rendezvous.pid;
|
|
456
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) {
|
|
457
|
+
throw new Error('Cannot verify whether vesper-net transport is stopped');
|
|
458
|
+
}
|
|
459
|
+
const state = probeProcess(pid);
|
|
460
|
+
if (state === 'alive') {
|
|
461
|
+
throw new Error('Cannot modify identity while vesper-net transport is running');
|
|
462
|
+
}
|
|
463
|
+
if (state === 'unknown') {
|
|
464
|
+
throw new Error('Cannot verify whether vesper-net transport is stopped');
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
export async function resetRemoteIdentity(paths, confirmed) {
|
|
468
|
+
if (!confirmed) {
|
|
469
|
+
throw new Error('reset-identity requires --confirm; this changes the remote Node ID and transcript identity');
|
|
470
|
+
}
|
|
471
|
+
await assertTransportStopped(paths);
|
|
472
|
+
await fs.rm(paths.tsnetDir, { recursive: true, force: true });
|
|
473
|
+
await fs.rm(paths.transportDir, { recursive: true, force: true });
|
|
474
|
+
}
|
|
475
|
+
export async function getInstallationStatus(paths, runner = new NodeProcessRunner()) {
|
|
476
|
+
const state = await readInstalledState(paths);
|
|
477
|
+
if (!state) {
|
|
478
|
+
return {
|
|
479
|
+
installed: false, paused: false, providers: [], claudeHookInstalled: false, copilotHookInstalled: false,
|
|
480
|
+
schedulerInstalled: false, trustedIntegrationsInstalled: false,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
const current = await readJsonFile(paths.currentPath);
|
|
484
|
+
const runtimeCurrent = current?.schemaVersion === 1 && current.runtimePath === state.runtime.runtimePath
|
|
485
|
+
&& current.version === state.runtime.version;
|
|
486
|
+
const locations = providerLocations(paths);
|
|
487
|
+
if (state.paused) {
|
|
488
|
+
return {
|
|
489
|
+
installed: runtimeCurrent, paused: true, providers: state.integrations.providers,
|
|
490
|
+
claudeHookInstalled: false, copilotHookInstalled: false, sourceTrust: state.sourceTrust,
|
|
491
|
+
schedulerInstalled: false, schedulerKind: state.scheduler.kind, minuteOffset: state.minuteOffset,
|
|
492
|
+
runtimeVersion: state.runtime.version, trustedIntegrationsInstalled: false,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
const claudeHookInstalled = !state.integrations.providers.includes('claude-code')
|
|
496
|
+
|| (state.integrations.claudeHook !== undefined
|
|
497
|
+
&& containsJson(await readJsonObject(locations.claudeSettingsPath), state.integrations.claudeHook));
|
|
498
|
+
const copilotHookInstalled = !state.integrations.providers.includes('github-copilot')
|
|
499
|
+
|| (state.integrations.copilotHook !== undefined
|
|
500
|
+
&& containsJson(await readJsonObject(locations.copilotSettingsPath), state.integrations.copilotHook));
|
|
501
|
+
let trustedIntegrationsInstalled = false;
|
|
502
|
+
if (state.sourceTrust === 'trusted') {
|
|
503
|
+
try {
|
|
504
|
+
await verifyProviderIntegrations(paths, state);
|
|
505
|
+
trustedIntegrationsInstalled = true;
|
|
506
|
+
}
|
|
507
|
+
catch {
|
|
508
|
+
trustedIntegrationsInstalled = false;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return {
|
|
512
|
+
installed: runtimeCurrent,
|
|
513
|
+
paused: false,
|
|
514
|
+
providers: state.integrations.providers,
|
|
515
|
+
claudeHookInstalled,
|
|
516
|
+
copilotHookInstalled,
|
|
517
|
+
sourceTrust: state.sourceTrust,
|
|
518
|
+
schedulerInstalled: await schedulerInstalled(state.scheduler, runner),
|
|
519
|
+
schedulerKind: state.scheduler.kind,
|
|
520
|
+
minuteOffset: state.minuteOffset,
|
|
521
|
+
runtimeVersion: state.runtime.version,
|
|
522
|
+
trustedIntegrationsInstalled,
|
|
523
|
+
};
|
|
524
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { getCapabilities, getHealth } from './http-client.js';
|
|
2
|
+
import { installManagedRuntime, type InstalledState } from './installation.js';
|
|
3
|
+
import { prepareManagedRuntime, validateInstallPrerequisites, type ManagedRuntime } from './managed-runtime.js';
|
|
4
|
+
import type { LinkPaths } from './paths.js';
|
|
5
|
+
import { type ProcessRunner } from './process-runner.js';
|
|
6
|
+
import { type RemoteTransport } from './transport.js';
|
|
7
|
+
export type ExpectedRole = 'trusted' | 'uploader';
|
|
8
|
+
export interface InstallProgress {
|
|
9
|
+
schemaVersion: 1;
|
|
10
|
+
state: 'enrolling' | 'waiting_for_tag';
|
|
11
|
+
host: string;
|
|
12
|
+
expectedRole?: ExpectedRole;
|
|
13
|
+
tailnetName?: string;
|
|
14
|
+
nodeName?: string;
|
|
15
|
+
ipv4?: string;
|
|
16
|
+
updatedAt: string;
|
|
17
|
+
}
|
|
18
|
+
export interface InstallResult {
|
|
19
|
+
state: InstalledState;
|
|
20
|
+
firstSyncStarted: boolean;
|
|
21
|
+
}
|
|
22
|
+
export declare class WaitingForTagError extends Error {
|
|
23
|
+
readonly progress: InstallProgress;
|
|
24
|
+
readonly code = "waiting_for_tag";
|
|
25
|
+
constructor(progress: InstallProgress);
|
|
26
|
+
}
|
|
27
|
+
export declare class AutomaticEnrollmentConflictError extends Error {
|
|
28
|
+
readonly code = "automatic_enrollment_existing_identity";
|
|
29
|
+
constructor(progress: InstallProgress);
|
|
30
|
+
}
|
|
31
|
+
export interface InstallerDependencies {
|
|
32
|
+
validatePrerequisites: typeof validateInstallPrerequisites;
|
|
33
|
+
prepareRuntime(options: Parameters<typeof prepareManagedRuntime>[0]): Promise<ManagedRuntime>;
|
|
34
|
+
validateRuntime(runtime: ManagedRuntime): Promise<void>;
|
|
35
|
+
acquireTransport(options: {
|
|
36
|
+
paths: LinkPaths;
|
|
37
|
+
hostUrl: string;
|
|
38
|
+
onNeedsLogin?: (url: string) => void;
|
|
39
|
+
requiredCompanion?: ManagedRuntime['companion'];
|
|
40
|
+
bootstrapAuthKey?: string;
|
|
41
|
+
}): Promise<RemoteTransport>;
|
|
42
|
+
getHealth: typeof getHealth;
|
|
43
|
+
getCapabilities: typeof getCapabilities;
|
|
44
|
+
installManaged: typeof installManagedRuntime;
|
|
45
|
+
triggerFirstSync(paths: LinkPaths): Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
export declare function installVesperLink(options: {
|
|
48
|
+
paths: LinkPaths;
|
|
49
|
+
host: string;
|
|
50
|
+
expectedRole?: ExpectedRole;
|
|
51
|
+
bootstrapAuthKey?: string;
|
|
52
|
+
runner?: ProcessRunner;
|
|
53
|
+
onNeedsLogin?: (url: string) => void;
|
|
54
|
+
onWaitingForTag?: (progress: InstallProgress) => void;
|
|
55
|
+
pollAuthorization?: boolean;
|
|
56
|
+
pollIntervalMs?: number;
|
|
57
|
+
signal?: AbortSignal;
|
|
58
|
+
dependencies?: InstallerDependencies;
|
|
59
|
+
}): Promise<InstallResult>;
|