@msvesper/vesper-link 0.1.4 → 0.1.6
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 -44
- package/README.md +45 -26
- package/dist/cli.js +24 -4
- package/dist/collectors/copilot.js +82 -1
- package/dist/collectors/types.d.ts +1 -0
- package/dist/config.d.ts +3 -1
- package/dist/config.js +24 -3
- package/dist/installation.d.ts +1 -0
- package/dist/installation.js +14 -1
- package/dist/installer.d.ts +8 -0
- package/dist/installer.js +47 -3
- package/dist/managed-runtime.js +19 -15
- package/dist/paths.d.ts +3 -1
- package/dist/paths.js +22 -1
- package/dist/scheduler.d.ts +1 -0
- package/dist/scheduler.js +25 -4
- package/dist/sync.js +5 -1
- package/dist/transport.d.ts +6 -0
- package/dist/transport.js +230 -51
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +39 -39
package/LICENSE.md
CHANGED
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
# Vesper Link End User License Agreement (EULA)
|
|
2
|
-
|
|
3
|
-
**Copyright (c) 2026 VSWork. All Rights Reserved.**
|
|
4
|
-
|
|
5
|
-
## 1. Grant of License
|
|
6
|
-
|
|
7
|
-
This End User License Agreement ("Agreement") grants you ("User") a limited, non-exclusive, non-transferable, revocable license to install and use Vesper Link ("Software") for personal or internal business purposes, subject to the terms below.
|
|
8
|
-
|
|
9
|
-
## 2. Restrictions
|
|
10
|
-
|
|
11
|
-
You may NOT:
|
|
12
|
-
- Copy, modify, adapt, translate, or create derivative works of the Software
|
|
13
|
-
- Reverse engineer, decompile, disassemble, or attempt to derive the source code
|
|
14
|
-
- Redistribute, sublicense, rent, lease, or lend the Software to any third party
|
|
15
|
-
- Remove or alter any proprietary notices, labels, or marks on the Software
|
|
16
|
-
- Use the Software for any unlawful purpose or in violation of any applicable laws
|
|
17
|
-
|
|
18
|
-
## 3. Intellectual Property
|
|
19
|
-
|
|
20
|
-
The Software, including all copies, modifications, and derivative works, is the exclusive property of VSWork and is protected by copyright and other intellectual property laws. This Agreement does not grant you any ownership rights to the Software.
|
|
21
|
-
|
|
22
|
-
## 4. No Warranty
|
|
23
|
-
|
|
24
|
-
THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU.
|
|
25
|
-
|
|
26
|
-
## 5. Limitation of Liability
|
|
27
|
-
|
|
28
|
-
IN NO EVENT SHALL VSWORK BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, LOSS OF DATA, BUSINESS INTERRUPTION, OR LOSS OF PROFITS) ARISING OUT OF OR IN CONNECTION WITH THIS AGREEMENT OR THE USE OF THE SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
|
29
|
-
|
|
30
|
-
## 6. Termination
|
|
31
|
-
|
|
32
|
-
This license is effective until terminated. Your rights under this license will terminate automatically without notice if you fail to comply with any of its terms. Upon termination, you must destroy all copies of the Software in your possession.
|
|
33
|
-
|
|
34
|
-
## 7. Governing Law
|
|
35
|
-
|
|
36
|
-
This Agreement shall be governed by and construed in accordance with the laws of the jurisdiction in which VSWork operates, without regard to its conflict of law provisions.
|
|
37
|
-
|
|
38
|
-
## 8. Entire Agreement
|
|
39
|
-
|
|
40
|
-
This Agreement constitutes the entire agreement between you and VSWork regarding the Software and supersedes all prior agreements and understandings, whether written or oral.
|
|
41
|
-
|
|
42
|
-
---
|
|
43
|
-
|
|
44
|
-
By installing or using this Software, you acknowledge that you have read, understood, and agree to be bound by the terms of this Agreement.
|
|
1
|
+
# Vesper Link End User License Agreement (EULA)
|
|
2
|
+
|
|
3
|
+
**Copyright (c) 2026 VSWork. All Rights Reserved.**
|
|
4
|
+
|
|
5
|
+
## 1. Grant of License
|
|
6
|
+
|
|
7
|
+
This End User License Agreement ("Agreement") grants you ("User") a limited, non-exclusive, non-transferable, revocable license to install and use Vesper Link ("Software") for personal or internal business purposes, subject to the terms below.
|
|
8
|
+
|
|
9
|
+
## 2. Restrictions
|
|
10
|
+
|
|
11
|
+
You may NOT:
|
|
12
|
+
- Copy, modify, adapt, translate, or create derivative works of the Software
|
|
13
|
+
- Reverse engineer, decompile, disassemble, or attempt to derive the source code
|
|
14
|
+
- Redistribute, sublicense, rent, lease, or lend the Software to any third party
|
|
15
|
+
- Remove or alter any proprietary notices, labels, or marks on the Software
|
|
16
|
+
- Use the Software for any unlawful purpose or in violation of any applicable laws
|
|
17
|
+
|
|
18
|
+
## 3. Intellectual Property
|
|
19
|
+
|
|
20
|
+
The Software, including all copies, modifications, and derivative works, is the exclusive property of VSWork and is protected by copyright and other intellectual property laws. This Agreement does not grant you any ownership rights to the Software.
|
|
21
|
+
|
|
22
|
+
## 4. No Warranty
|
|
23
|
+
|
|
24
|
+
THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU.
|
|
25
|
+
|
|
26
|
+
## 5. Limitation of Liability
|
|
27
|
+
|
|
28
|
+
IN NO EVENT SHALL VSWORK BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, LOSS OF DATA, BUSINESS INTERRUPTION, OR LOSS OF PROFITS) ARISING OUT OF OR IN CONNECTION WITH THIS AGREEMENT OR THE USE OF THE SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
|
29
|
+
|
|
30
|
+
## 6. Termination
|
|
31
|
+
|
|
32
|
+
This license is effective until terminated. Your rights under this license will terminate automatically without notice if you fail to comply with any of its terms. Upon termination, you must destroy all copies of the Software in your possession.
|
|
33
|
+
|
|
34
|
+
## 7. Governing Law
|
|
35
|
+
|
|
36
|
+
This Agreement shall be governed by and construed in accordance with the laws of the jurisdiction in which VSWork operates, without regard to its conflict of law provisions.
|
|
37
|
+
|
|
38
|
+
## 8. Entire Agreement
|
|
39
|
+
|
|
40
|
+
This Agreement constitutes the entire agreement between you and VSWork regarding the Software and supersedes all prior agreements and understandings, whether written or oral.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
By installing or using this Software, you acknowledge that you have read, understood, and agree to be bound by the terms of this Agreement.
|
package/README.md
CHANGED
|
@@ -1,26 +1,45 @@
|
|
|
1
|
-
# vesper-link
|
|
2
|
-
|
|
3
|
-
**Vesper Memory Link — One Memory across all your devices.**
|
|
4
|
-
|
|
5
|
-
`vesper-link` connects a supported Windows or Linux device to Vesper Memory. It can:
|
|
6
|
-
|
|
7
|
-
- Sync completed Claude Code, GitHub Copilot CLI, and OpenAI Codex sessions to your Vesper Host.
|
|
8
|
-
- Let full-access devices search and add Vesper Memory through MCP.
|
|
9
|
-
|
|
10
|
-
## Connect a Device
|
|
11
|
-
|
|
12
|
-
On the Windows device running Vesper, open **Memory Links** and copy a connection command. Run it on the device you want to connect; no global installation is required.
|
|
13
|
-
|
|
14
|
-
Node.js 20
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
```bash
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
1
|
+
# vesper-link
|
|
2
|
+
|
|
3
|
+
**Vesper Memory Link — One Memory across all your devices.**
|
|
4
|
+
|
|
5
|
+
`vesper-link` connects a supported Windows or Linux device to Vesper Memory. It can:
|
|
6
|
+
|
|
7
|
+
- Sync completed Claude Code, GitHub Copilot CLI, VS Code Copilot Chat, and OpenAI Codex sessions to your Vesper Host.
|
|
8
|
+
- Let full-access devices search and add Vesper Memory through MCP.
|
|
9
|
+
|
|
10
|
+
## Connect a Device
|
|
11
|
+
|
|
12
|
+
On the Windows device running Vesper, open **Memory Links** and copy a connection command. Run it on the device you want to connect; no global installation is required.
|
|
13
|
+
|
|
14
|
+
The Remote must already have Node.js 20+ and npm 9+. V1 supports Windows x64 and Linux x64.
|
|
15
|
+
|
|
16
|
+
Linux also requires the `crontab` command for periodic session sync. Install it before running the copied command, then ensure the service is running:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# Ubuntu/Debian
|
|
20
|
+
sudo apt-get install cron
|
|
21
|
+
sudo systemctl enable --now cron
|
|
22
|
+
|
|
23
|
+
# Fedora/RHEL
|
|
24
|
+
sudo dnf install cronie
|
|
25
|
+
sudo systemctl enable --now crond
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Vesper Link does not use `sudo` or install operating-system packages for you.
|
|
29
|
+
|
|
30
|
+
VS Code Copilot Chat transcripts are discovered from local VS Code workspace storage and from VS Code Server storage on Remote SSH hosts. On Windows this includes `%APPDATA%\Code\User\workspaceStorage` and the default `%USERPROFILE%\.vscode-server\data\User\workspaceStorage`; on Linux it includes `~/.config/Code/User/workspaceStorage` and `~/.vscode-server/data/User/workspaceStorage`. `VSCODE_AGENT_FOLDER` is honored when set. Vesper Link reads the per-session `GitHub.copilot-chat/transcripts/*.jsonl` files, not Copilot's global `session-store.db`.
|
|
31
|
+
|
|
32
|
+
## Manage the Connection
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npx --yes @msvesper/vesper-link@host-v1 upgrade
|
|
36
|
+
npx --yes @msvesper/vesper-link@host-v1 status
|
|
37
|
+
npx --yes @msvesper/vesper-link@host-v1 pause
|
|
38
|
+
npx --yes @msvesper/vesper-link@host-v1 resume
|
|
39
|
+
npx --yes @msvesper/vesper-link@host-v1 doctor
|
|
40
|
+
npx --yes @msvesper/vesper-link@host-v1 uninstall
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`upgrade` reads the existing Host and access role from `~/.vesper-link/`, replaces the managed runtime and integrations transactionally, and preserves the Remote identity and queued work. It accepts no Host or auth key. If the existing installation, configuration, or Tailscale identity is missing or inconsistent, it stops without relinking. Resume a paused Link before upgrading.
|
|
44
|
+
|
|
45
|
+
Session transcripts are sent to the Vesper Host selected during setup and retained there. They are not uploaded to a Vesper-operated cloud service. Uninstalling the connection does not delete the original transcripts or Memory already saved on the Host.
|
package/dist/cli.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
2
|
+
import * as fs from 'node:fs/promises';
|
|
3
|
+
import { readConfig, writeConfig } from './config.js';
|
|
3
4
|
import { getDoctorReport, getStatusReport } from './diagnostics.js';
|
|
4
5
|
import { resolveHookReport } from './hook-report.js';
|
|
5
6
|
import { pauseIntegrations, resetRemoteIdentity, resumeIntegrations, uninstallIntegrations } from './installation.js';
|
|
6
7
|
import { parseInstallArguments } from './install-args.js';
|
|
7
|
-
import { installVesperLink } from './installer.js';
|
|
8
|
+
import { installVesperLink, upgradeVesperLink } from './installer.js';
|
|
8
9
|
import { runMemoryMcp } from './memory-mcp.js';
|
|
9
10
|
import { appendSyncLog } from './operational-log.js';
|
|
10
11
|
import { writePendingMarker } from './pending.js';
|
|
11
12
|
import { resolveLinkPaths } from './paths.js';
|
|
12
13
|
import { runSyncOnce } from './sync.js';
|
|
13
14
|
import { VESPER_LINK_VERSION } from './version.js';
|
|
14
|
-
const USAGE = 'Usage: vesper-link <configure|report-session|sync-once|scheduled-sync|mcp|install|pause|resume|uninstall|reset-identity|status|doctor>';
|
|
15
|
+
const USAGE = 'Usage: vesper-link <configure|report-session|sync-once|scheduled-sync|mcp|install|upgrade|pause|resume|uninstall|reset-identity|status|doctor>';
|
|
15
16
|
function flags(args) {
|
|
16
17
|
const result = new Map();
|
|
17
18
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -51,7 +52,15 @@ async function main() {
|
|
|
51
52
|
if (!host) {
|
|
52
53
|
throw new Error('Usage: vesper-link configure --host <url>');
|
|
53
54
|
}
|
|
54
|
-
const
|
|
55
|
+
const existing = await fs.access(paths.configPath)
|
|
56
|
+
.then(() => readConfig(paths.configPath))
|
|
57
|
+
.catch((error) => {
|
|
58
|
+
if (error.code === 'ENOENT') {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
throw error;
|
|
62
|
+
});
|
|
63
|
+
const config = await writeConfig(paths.configPath, host, process.env.VSCODE_AGENT_FOLDER ?? existing?.vscodeAgentFolder);
|
|
55
64
|
process.stdout.write(`${JSON.stringify(config)}\n`);
|
|
56
65
|
return;
|
|
57
66
|
}
|
|
@@ -116,6 +125,17 @@ async function main() {
|
|
|
116
125
|
})}\n`);
|
|
117
126
|
return;
|
|
118
127
|
}
|
|
128
|
+
if (command === 'upgrade') {
|
|
129
|
+
if (args.length > 0) {
|
|
130
|
+
throw new Error('Usage: vesper-link upgrade');
|
|
131
|
+
}
|
|
132
|
+
const result = await upgradeVesperLink({ paths });
|
|
133
|
+
process.stdout.write(`${JSON.stringify({
|
|
134
|
+
upgraded: true, runtimeVersion: result.state.runtime.version,
|
|
135
|
+
sourceTrust: result.state.sourceTrust, firstSyncStarted: result.firstSyncStarted,
|
|
136
|
+
})}\n`);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
119
139
|
if (command === 'uninstall') {
|
|
120
140
|
process.stdout.write(`${JSON.stringify({ removed: await uninstallIntegrations(paths) })}\n`);
|
|
121
141
|
return;
|
|
@@ -16,6 +16,42 @@ function parseYamlScalars(text) {
|
|
|
16
16
|
}
|
|
17
17
|
return values;
|
|
18
18
|
}
|
|
19
|
+
async function readVscodeWorkspace(workspaceDir, context) {
|
|
20
|
+
try {
|
|
21
|
+
const workspace = JSON.parse(await fs.readFile(path.join(workspaceDir, 'workspace.json'), 'utf8'));
|
|
22
|
+
const cwdUri = typeof workspace.folder === 'string'
|
|
23
|
+
? workspace.folder
|
|
24
|
+
: typeof workspace.workspace === 'string' ? workspace.workspace : undefined;
|
|
25
|
+
if (!cwdUri) {
|
|
26
|
+
return {};
|
|
27
|
+
}
|
|
28
|
+
let projectPath = cwdUri;
|
|
29
|
+
try {
|
|
30
|
+
projectPath = decodeURIComponent(new URL(cwdUri).pathname).replace(/^\/([A-Za-z]:\/)/, '$1');
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// Keep the opaque location for project grouping when it is not a valid URL.
|
|
34
|
+
}
|
|
35
|
+
return { cwdUri, projectKey: pathToProjectKey(projectPath) };
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (error.code !== 'ENOENT') {
|
|
39
|
+
context.reportIssue?.({ provider: 'github-copilot', code: 'workspace_metadata_unreadable', message: String(error) });
|
|
40
|
+
}
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function preferNewest(candidates) {
|
|
45
|
+
const byId = new Map();
|
|
46
|
+
for (const candidate of candidates) {
|
|
47
|
+
const current = byId.get(candidate.providerSessionId);
|
|
48
|
+
if (!current || candidate.mtimeMs > current.mtimeMs
|
|
49
|
+
|| (candidate.mtimeMs === current.mtimeMs && candidate.transcriptPath < current.transcriptPath)) {
|
|
50
|
+
byId.set(candidate.providerSessionId, candidate);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return [...byId.values()];
|
|
54
|
+
}
|
|
19
55
|
export class CopilotCollector {
|
|
20
56
|
async collect(context) {
|
|
21
57
|
const files = await walkFiles(context.copilotSessionsDir, (filePath) => path.basename(filePath) === 'events.jsonl');
|
|
@@ -59,6 +95,51 @@ export class CopilotCollector {
|
|
|
59
95
|
context.reportIssue?.({ provider: 'github-copilot', code: 'source_unreadable', message: String(error) });
|
|
60
96
|
}
|
|
61
97
|
}
|
|
62
|
-
|
|
98
|
+
for (const workspaceStorageDir of context.copilotVscodeWorkspaceStorageDirs) {
|
|
99
|
+
let workspaces;
|
|
100
|
+
try {
|
|
101
|
+
workspaces = await fs.readdir(workspaceStorageDir, { withFileTypes: true });
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
if (['ENOENT', 'EACCES', 'EPERM'].includes(String(error.code))) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
for (const workspace of workspaces) {
|
|
110
|
+
if (!workspace.isDirectory()) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const workspaceDir = path.join(workspaceStorageDir, workspace.name);
|
|
114
|
+
const transcriptDir = path.join(workspaceDir, 'GitHub.copilot-chat', 'transcripts');
|
|
115
|
+
const transcripts = await walkFiles(transcriptDir, (filePath) => path.extname(filePath).toLowerCase() === '.jsonl');
|
|
116
|
+
if (transcripts.length === 0) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const metadata = await readVscodeWorkspace(workspaceDir, context);
|
|
120
|
+
for (const transcriptPath of transcripts) {
|
|
121
|
+
try {
|
|
122
|
+
const stat = await fs.stat(transcriptPath);
|
|
123
|
+
candidates.push({
|
|
124
|
+
provider: 'github-copilot',
|
|
125
|
+
format: 'github-copilot-events-jsonl',
|
|
126
|
+
formatVersion: 'observed-v1',
|
|
127
|
+
providerSessionId: 'vscode:' + path.basename(transcriptPath, path.extname(transcriptPath)),
|
|
128
|
+
transcriptPath,
|
|
129
|
+
...metadata,
|
|
130
|
+
sourceCreatedAt: stat.birthtime.toISOString(),
|
|
131
|
+
sourceUpdatedAt: stat.mtime.toISOString(),
|
|
132
|
+
size: stat.size,
|
|
133
|
+
mtimeMs: stat.mtimeMs,
|
|
134
|
+
discovery: 'scan',
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
context.reportIssue?.({ provider: 'github-copilot', code: 'source_unreadable', message: String(error) });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return preferNewest(candidates);
|
|
63
144
|
}
|
|
64
145
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export interface LinkConfig {
|
|
2
2
|
schemaVersion: 1;
|
|
3
3
|
host: string;
|
|
4
|
+
vscodeAgentFolder?: string;
|
|
4
5
|
}
|
|
6
|
+
export declare function normalizeVscodeAgentFolder(value: string | undefined): string | undefined;
|
|
5
7
|
export declare function normalizeHost(value: string): string;
|
|
6
8
|
export declare function readConfig(configPath: string): Promise<LinkConfig>;
|
|
7
|
-
export declare function writeConfig(configPath: string, host: string): Promise<LinkConfig>;
|
|
9
|
+
export declare function writeConfig(configPath: string, host: string, vscodeAgentFolder?: string): Promise<LinkConfig>;
|
package/dist/config.js
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import { isIPv4 } from 'node:net';
|
|
2
|
+
import * as path from 'node:path';
|
|
2
3
|
import { readJsonFile, writeJsonAtomic } from './atomic-json.js';
|
|
4
|
+
export function normalizeVscodeAgentFolder(value) {
|
|
5
|
+
if (value === undefined) {
|
|
6
|
+
return undefined;
|
|
7
|
+
}
|
|
8
|
+
const normalized = path.normalize(value).replace(/[\\/]+$/, '');
|
|
9
|
+
if (!normalized || !path.isAbsolute(normalized) || normalized === path.parse(normalized).root) {
|
|
10
|
+
throw new Error('VSCODE_AGENT_FOLDER must be an absolute non-root path');
|
|
11
|
+
}
|
|
12
|
+
return normalized;
|
|
13
|
+
}
|
|
3
14
|
function isTailscaleIpv4(hostname) {
|
|
4
15
|
if (!isIPv4(hostname)) {
|
|
5
16
|
return false;
|
|
@@ -32,10 +43,20 @@ export async function readConfig(configPath) {
|
|
|
32
43
|
if (!config || config.schemaVersion !== 1 || typeof config.host !== 'string') {
|
|
33
44
|
throw new Error(`Missing or invalid config at ${configPath}; run vesper-link configure --host <url>`);
|
|
34
45
|
}
|
|
35
|
-
|
|
46
|
+
if (config.vscodeAgentFolder !== undefined && typeof config.vscodeAgentFolder !== 'string') {
|
|
47
|
+
throw new Error(`Invalid VSCODE_AGENT_FOLDER in config at ${configPath}`);
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
schemaVersion: 1,
|
|
51
|
+
host: normalizeHost(config.host),
|
|
52
|
+
vscodeAgentFolder: normalizeVscodeAgentFolder(config.vscodeAgentFolder),
|
|
53
|
+
};
|
|
36
54
|
}
|
|
37
|
-
export async function writeConfig(configPath, host) {
|
|
38
|
-
const config = {
|
|
55
|
+
export async function writeConfig(configPath, host, vscodeAgentFolder) {
|
|
56
|
+
const config = {
|
|
57
|
+
schemaVersion: 1, host: normalizeHost(host),
|
|
58
|
+
vscodeAgentFolder: normalizeVscodeAgentFolder(vscodeAgentFolder),
|
|
59
|
+
};
|
|
39
60
|
await writeJsonAtomic(configPath, config);
|
|
40
61
|
return config;
|
|
41
62
|
}
|
package/dist/installation.d.ts
CHANGED
|
@@ -51,6 +51,7 @@ export declare function installManagedRuntime(options: {
|
|
|
51
51
|
now?: Date;
|
|
52
52
|
postInstallDoctor?: () => Promise<void>;
|
|
53
53
|
faults?: InstallFaults;
|
|
54
|
+
vscodeAgentFolder?: string;
|
|
54
55
|
}): Promise<InstalledState>;
|
|
55
56
|
export declare function uninstallIntegrations(paths: LinkPaths, runner?: ProcessRunner, probeProcess?: (pid: number) => 'alive' | 'absent' | 'unknown'): Promise<boolean>;
|
|
56
57
|
export declare function pauseIntegrations(paths: LinkPaths, runner?: ProcessRunner): Promise<boolean>;
|
package/dist/installation.js
CHANGED
|
@@ -2,6 +2,7 @@ import * as fs from 'node:fs/promises';
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { readJsonFile, writeJsonAtomic } from './atomic-json.js';
|
|
5
|
+
import { normalizeVscodeAgentFolder } from './config.js';
|
|
5
6
|
import { stableLauncherSource } from './managed-runtime.js';
|
|
6
7
|
import { acquireSyncLock } from './lock.js';
|
|
7
8
|
import { NodeProcessRunner } from './process-runner.js';
|
|
@@ -150,6 +151,14 @@ async function prepareInstall(options) {
|
|
|
150
151
|
JSON.parse(snapshot.content.toString('utf8'));
|
|
151
152
|
}
|
|
152
153
|
}
|
|
154
|
+
let previousAgentFolder;
|
|
155
|
+
if (config.existed) {
|
|
156
|
+
const previousConfig = JSON.parse(config.content.toString('utf8'));
|
|
157
|
+
if (typeof previousConfig.vscodeAgentFolder === 'string') {
|
|
158
|
+
previousAgentFolder = previousConfig.vscodeAgentFolder;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const vscodeAgentFolder = normalizeVscodeAgentFolder(options.vscodeAgentFolder ?? previousAgentFolder);
|
|
153
162
|
const installationId = previous?.installationId ?? randomUUID();
|
|
154
163
|
const minuteOffset = previous?.minuteOffset ?? stableMinuteOffset(options.paths.homeDir);
|
|
155
164
|
const scheduler = await prepareScheduler({
|
|
@@ -174,6 +183,7 @@ async function prepareInstall(options) {
|
|
|
174
183
|
...(previous?.createdProviderFiles ?? []),
|
|
175
184
|
...providerFiles.filter((snapshot) => !snapshot.existed).map((snapshot) => snapshot.filePath),
|
|
176
185
|
])],
|
|
186
|
+
vscodeAgentFolder,
|
|
177
187
|
};
|
|
178
188
|
}
|
|
179
189
|
export async function installManagedRuntime(options) {
|
|
@@ -185,6 +195,7 @@ export async function installManagedRuntime(options) {
|
|
|
185
195
|
paths: options.paths, runtime: options.runtime, host: options.host,
|
|
186
196
|
sourceTrust: options.capabilities.sourceTrust, providers: options.providers,
|
|
187
197
|
nodePath, platform, runner, now,
|
|
198
|
+
vscodeAgentFolder: options.vscodeAgentFolder,
|
|
188
199
|
});
|
|
189
200
|
const installationId = prepared.installationId;
|
|
190
201
|
const pointer = {
|
|
@@ -196,7 +207,9 @@ export async function installManagedRuntime(options) {
|
|
|
196
207
|
await ensureLauncher(options.paths);
|
|
197
208
|
await writeJsonAtomic(options.paths.currentPath, pointer);
|
|
198
209
|
await options.faults?.afterStep?.('current');
|
|
199
|
-
await writeJsonAtomic(options.paths.configPath, {
|
|
210
|
+
await writeJsonAtomic(options.paths.configPath, {
|
|
211
|
+
schemaVersion: 1, host: options.host, vscodeAgentFolder: prepared.vscodeAgentFolder,
|
|
212
|
+
});
|
|
200
213
|
await options.faults?.afterStep?.('config');
|
|
201
214
|
providersApplied = true;
|
|
202
215
|
const integrations = await applyProviderIntegrations({
|
package/dist/installer.d.ts
CHANGED
|
@@ -38,6 +38,8 @@ export interface InstallerDependencies {
|
|
|
38
38
|
onNeedsLogin?: (url: string) => void;
|
|
39
39
|
requiredCompanion?: ManagedRuntime['companion'];
|
|
40
40
|
bootstrapAuthKey?: string;
|
|
41
|
+
failIfLoginRequired?: boolean;
|
|
42
|
+
replaceExistingTransport?: boolean;
|
|
41
43
|
}): Promise<RemoteTransport>;
|
|
42
44
|
getHealth: typeof getHealth;
|
|
43
45
|
getCapabilities: typeof getCapabilities;
|
|
@@ -56,4 +58,10 @@ export declare function installVesperLink(options: {
|
|
|
56
58
|
pollIntervalMs?: number;
|
|
57
59
|
signal?: AbortSignal;
|
|
58
60
|
dependencies?: InstallerDependencies;
|
|
61
|
+
requireExistingIdentity?: boolean;
|
|
62
|
+
}): Promise<InstallResult>;
|
|
63
|
+
export declare function upgradeVesperLink(options: {
|
|
64
|
+
paths: LinkPaths;
|
|
65
|
+
runner?: ProcessRunner;
|
|
66
|
+
dependencies?: InstallerDependencies;
|
|
59
67
|
}): Promise<InstallResult>;
|
package/dist/installer.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as fs from 'node:fs/promises';
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
|
-
import { normalizeHost } from './config.js';
|
|
3
|
+
import { normalizeHost, readConfig } from './config.js';
|
|
4
4
|
import { getCapabilities, getHealth, SystemicUploadError } from './http-client.js';
|
|
5
|
-
import { installManagedRuntime } from './installation.js';
|
|
5
|
+
import { installManagedRuntime, readInstalledState } from './installation.js';
|
|
6
6
|
import { acquireSyncLock } from './lock.js';
|
|
7
7
|
import { prepareManagedRuntime, validateInstallPrerequisites, validateManagedRuntime, } from './managed-runtime.js';
|
|
8
8
|
import { NodeProcessRunner } from './process-runner.js';
|
|
@@ -58,6 +58,9 @@ export async function installVesperLink(options) {
|
|
|
58
58
|
let bootstrapAuthKey = consumeBootstrapAuthKey(options.bootstrapAuthKey);
|
|
59
59
|
options.bootstrapAuthKey = undefined;
|
|
60
60
|
const automaticEnrollment = bootstrapAuthKey !== undefined;
|
|
61
|
+
if (options.requireExistingIdentity && automaticEnrollment) {
|
|
62
|
+
throw new Error('upgrade does not accept a Tailscale auth key');
|
|
63
|
+
}
|
|
61
64
|
const host = normalizeHost(options.host);
|
|
62
65
|
const companion = await dependencies.validatePrerequisites({ hostUrl: host, runner });
|
|
63
66
|
const releaseCandidateLock = await acquireSyncLock(options.paths.lockPath);
|
|
@@ -72,13 +75,17 @@ export async function installVesperLink(options) {
|
|
|
72
75
|
const progress = {
|
|
73
76
|
schemaVersion: 1, state: 'enrolling', host, expectedRole: options.expectedRole, updatedAt: new Date().toISOString(),
|
|
74
77
|
};
|
|
75
|
-
|
|
78
|
+
if (!options.requireExistingIdentity) {
|
|
79
|
+
await writeProgress(options.paths, progress);
|
|
80
|
+
}
|
|
76
81
|
let transport;
|
|
77
82
|
try {
|
|
78
83
|
transport = await dependencies.acquireTransport({
|
|
79
84
|
paths: options.paths, hostUrl: host,
|
|
80
85
|
requiredCompanion: runtime.companion,
|
|
81
86
|
bootstrapAuthKey,
|
|
87
|
+
failIfLoginRequired: options.requireExistingIdentity,
|
|
88
|
+
replaceExistingTransport: options.requireExistingIdentity,
|
|
82
89
|
onNeedsLogin: (url) => {
|
|
83
90
|
options.onNeedsLogin?.(url);
|
|
84
91
|
},
|
|
@@ -102,6 +109,9 @@ export async function installVesperLink(options) {
|
|
|
102
109
|
if (!(error instanceof SystemicUploadError) || error.httpStatus !== 403) {
|
|
103
110
|
throw error;
|
|
104
111
|
}
|
|
112
|
+
if (options.requireExistingIdentity) {
|
|
113
|
+
throw new Error('The existing Tailscale identity is no longer authorized; upgrade stopped without relinking');
|
|
114
|
+
}
|
|
105
115
|
if (automaticEnrollment) {
|
|
106
116
|
throw new AutomaticEnrollmentConflictError(progress);
|
|
107
117
|
}
|
|
@@ -144,6 +154,7 @@ export async function installVesperLink(options) {
|
|
|
144
154
|
await dependencies.validateRuntime(runtime);
|
|
145
155
|
const candidateTransport = await dependencies.acquireTransport({
|
|
146
156
|
paths: options.paths, hostUrl: host, requiredCompanion: runtime.companion,
|
|
157
|
+
failIfLoginRequired: options.requireExistingIdentity,
|
|
147
158
|
});
|
|
148
159
|
try {
|
|
149
160
|
const [candidateHealth, candidateCapabilities] = await Promise.all([
|
|
@@ -160,10 +171,12 @@ export async function installVesperLink(options) {
|
|
|
160
171
|
}
|
|
161
172
|
installed = await dependencies.installManaged({
|
|
162
173
|
paths: options.paths, runtime, host, capabilities, runner,
|
|
174
|
+
vscodeAgentFolder: process.env.VSCODE_AGENT_FOLDER,
|
|
163
175
|
postInstallDoctor: async () => {
|
|
164
176
|
await dependencies.validateRuntime(runtime);
|
|
165
177
|
const postTransport = await dependencies.acquireTransport({
|
|
166
178
|
paths: options.paths, hostUrl: host, requiredCompanion: runtime.companion,
|
|
179
|
+
failIfLoginRequired: options.requireExistingIdentity,
|
|
167
180
|
});
|
|
168
181
|
try {
|
|
169
182
|
const [health, currentCapabilities] = await Promise.all([
|
|
@@ -190,6 +203,37 @@ export async function installVesperLink(options) {
|
|
|
190
203
|
await dependencies.triggerFirstSync(options.paths);
|
|
191
204
|
return { state: installed, firstSyncStarted: true };
|
|
192
205
|
}
|
|
206
|
+
export async function upgradeVesperLink(options) {
|
|
207
|
+
const inheritedAuthKey = consumeBootstrapAuthKey(undefined);
|
|
208
|
+
if (inheritedAuthKey !== undefined) {
|
|
209
|
+
throw new Error('upgrade does not accept a Tailscale auth key');
|
|
210
|
+
}
|
|
211
|
+
const installed = await readInstalledState(options.paths);
|
|
212
|
+
if (!installed) {
|
|
213
|
+
throw new Error('No existing Vesper Link installation; use a Host-generated install command first');
|
|
214
|
+
}
|
|
215
|
+
if (installed.paused) {
|
|
216
|
+
throw new Error('Vesper Link is paused; resume it before upgrading');
|
|
217
|
+
}
|
|
218
|
+
const config = await readConfig(options.paths.configPath);
|
|
219
|
+
if (installed.host !== config.host) {
|
|
220
|
+
throw new Error('Installed Host does not match Vesper Link config; refusing to guess during upgrade');
|
|
221
|
+
}
|
|
222
|
+
const identityEntries = await fs.readdir(options.paths.tsnetDir).catch((error) => {
|
|
223
|
+
if (error.code === 'ENOENT') {
|
|
224
|
+
return [];
|
|
225
|
+
}
|
|
226
|
+
throw error;
|
|
227
|
+
});
|
|
228
|
+
if (identityEntries.length === 0) {
|
|
229
|
+
throw new Error('No existing Tailscale identity; upgrade stopped without relinking');
|
|
230
|
+
}
|
|
231
|
+
return installVesperLink({
|
|
232
|
+
paths: options.paths, host: config.host, expectedRole: installed.sourceTrust,
|
|
233
|
+
runner: options.runner, dependencies: options.dependencies, requireExistingIdentity: true,
|
|
234
|
+
pollAuthorization: false,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
193
237
|
async function waitForPoll(milliseconds, signal) {
|
|
194
238
|
if (signal?.aborted) {
|
|
195
239
|
throw signal.reason ?? new Error('Installation cancelled');
|
package/dist/managed-runtime.js
CHANGED
|
@@ -4,6 +4,7 @@ import { createRequire } from 'node:module';
|
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { assertCompanionProtocol, resolveCompanion } from './companion.js';
|
|
6
6
|
import { NodeProcessRunner } from './process-runner.js';
|
|
7
|
+
import { validateLinuxSchedulerPrerequisite } from './scheduler.js';
|
|
7
8
|
async function metadata(filePath) {
|
|
8
9
|
return JSON.parse(await fs.readFile(filePath, 'utf8'));
|
|
9
10
|
}
|
|
@@ -34,21 +35,21 @@ async function copyDirectory(source, destination) {
|
|
|
34
35
|
await fs.cp(source, destination, { recursive: true, force: false, errorOnExist: true });
|
|
35
36
|
}
|
|
36
37
|
export function stableLauncherSource() {
|
|
37
|
-
return `#!/usr/bin/env node
|
|
38
|
-
import fs from 'node:fs/promises';
|
|
39
|
-
import path from 'node:path';
|
|
40
|
-
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
41
|
-
const stateDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
42
|
-
const current = JSON.parse(await fs.readFile(path.join(stateDir, 'current.json'), 'utf8'));
|
|
43
|
-
if (current.schemaVersion !== 1 || typeof current.runtimePath !== 'string') {
|
|
44
|
-
throw new Error('vesper-link current runtime pointer is malformed');
|
|
45
|
-
}
|
|
46
|
-
const runtimeRoot = path.join(stateDir, 'runtime');
|
|
47
|
-
const relative = path.relative(runtimeRoot, current.runtimePath);
|
|
48
|
-
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
49
|
-
throw new Error('vesper-link current runtime escaped the managed runtime root');
|
|
50
|
-
}
|
|
51
|
-
await import(pathToFileURL(path.join(current.runtimePath, 'package', 'dist', 'cli.js')).href);
|
|
38
|
+
return `#!/usr/bin/env node
|
|
39
|
+
import fs from 'node:fs/promises';
|
|
40
|
+
import path from 'node:path';
|
|
41
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
42
|
+
const stateDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
43
|
+
const current = JSON.parse(await fs.readFile(path.join(stateDir, 'current.json'), 'utf8'));
|
|
44
|
+
if (current.schemaVersion !== 1 || typeof current.runtimePath !== 'string') {
|
|
45
|
+
throw new Error('vesper-link current runtime pointer is malformed');
|
|
46
|
+
}
|
|
47
|
+
const runtimeRoot = path.join(stateDir, 'runtime');
|
|
48
|
+
const relative = path.relative(runtimeRoot, current.runtimePath);
|
|
49
|
+
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
50
|
+
throw new Error('vesper-link current runtime escaped the managed runtime root');
|
|
51
|
+
}
|
|
52
|
+
await import(pathToFileURL(path.join(current.runtimePath, 'package', 'dist', 'cli.js')).href);
|
|
52
53
|
`;
|
|
53
54
|
}
|
|
54
55
|
export async function validateInstallPrerequisites(options) {
|
|
@@ -70,6 +71,9 @@ export async function validateInstallPrerequisites(options) {
|
|
|
70
71
|
if (npm.code !== 0 || majorVersion(npm.stdout.trim()) < 9) {
|
|
71
72
|
throw new Error(`A working npm 9 or newer is required: ${npm.stderr.trim() || npm.stdout.trim()}`);
|
|
72
73
|
}
|
|
74
|
+
if (platform === 'linux') {
|
|
75
|
+
await validateLinuxSchedulerPrerequisite(runner);
|
|
76
|
+
}
|
|
73
77
|
const companion = await (options.resolveSourceCompanion ?? (() => resolveCompanion(platform, architecture)))();
|
|
74
78
|
await (options.probeCompanion ?? assertCompanionProtocol)(companion, protocolProbeEnvironment(process.env));
|
|
75
79
|
return companion;
|
package/dist/paths.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ export interface LinkPaths {
|
|
|
18
18
|
logsDir: string;
|
|
19
19
|
claudeProjectsDir: string;
|
|
20
20
|
copilotSessionsDir: string;
|
|
21
|
+
copilotVscodeWorkspaceStorageDirs: string[];
|
|
21
22
|
codexHome: string;
|
|
22
23
|
}
|
|
23
|
-
export declare function
|
|
24
|
+
export declare function vscodeAgentWorkspaceStorageDir(agentFolder: string): string;
|
|
25
|
+
export declare function resolveLinkPaths(environment?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): LinkPaths;
|
package/dist/paths.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import * as os from 'node:os';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
-
export function
|
|
3
|
+
export function vscodeAgentWorkspaceStorageDir(agentFolder) {
|
|
4
|
+
return path.join(agentFolder, 'data', 'User', 'workspaceStorage');
|
|
5
|
+
}
|
|
6
|
+
export function resolveLinkPaths(environment = process.env, platform = process.platform) {
|
|
4
7
|
const homeDir = environment.HOME || environment.USERPROFILE || os.homedir();
|
|
5
8
|
const rawStateDir = path.normalize(environment.VESPER_LINK_HOME || path.join(homeDir, '.vesper-link'));
|
|
6
9
|
const root = path.parse(rawStateDir).root;
|
|
@@ -9,6 +12,23 @@ export function resolveLinkPaths(environment = process.env) {
|
|
|
9
12
|
}
|
|
10
13
|
const stateDir = rawStateDir.replace(/[\\/]+$/, '');
|
|
11
14
|
const codexHome = environment.CODEX_HOME || path.join(homeDir, '.codex');
|
|
15
|
+
const appData = environment.APPDATA || path.join(homeDir, 'AppData', 'Roaming');
|
|
16
|
+
const localWorkspaceStorageDirs = platform === 'win32'
|
|
17
|
+
? [
|
|
18
|
+
path.join(appData, 'Code', 'User', 'workspaceStorage'),
|
|
19
|
+
path.join(appData, 'Code - Insiders', 'User', 'workspaceStorage'),
|
|
20
|
+
]
|
|
21
|
+
: [
|
|
22
|
+
path.join(homeDir, '.config', 'Code', 'User', 'workspaceStorage'),
|
|
23
|
+
path.join(homeDir, '.config', 'Code - Insiders', 'User', 'workspaceStorage'),
|
|
24
|
+
];
|
|
25
|
+
const agentRoots = environment.VSCODE_AGENT_FOLDER
|
|
26
|
+
? [environment.VSCODE_AGENT_FOLDER]
|
|
27
|
+
: [path.join(homeDir, '.vscode-server'), path.join(homeDir, '.vscode-server-insiders')];
|
|
28
|
+
const copilotVscodeWorkspaceStorageDirs = [
|
|
29
|
+
...localWorkspaceStorageDirs,
|
|
30
|
+
...agentRoots.map(vscodeAgentWorkspaceStorageDir),
|
|
31
|
+
];
|
|
12
32
|
return {
|
|
13
33
|
homeDir,
|
|
14
34
|
stateDir,
|
|
@@ -31,6 +51,7 @@ export function resolveLinkPaths(environment = process.env) {
|
|
|
31
51
|
logsDir: path.join(stateDir, 'logs'),
|
|
32
52
|
claudeProjectsDir: path.join(homeDir, '.claude', 'projects'),
|
|
33
53
|
copilotSessionsDir: path.join(homeDir, '.copilot', 'session-state'),
|
|
54
|
+
copilotVscodeWorkspaceStorageDirs,
|
|
34
55
|
codexHome,
|
|
35
56
|
};
|
|
36
57
|
}
|
package/dist/scheduler.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ export type SchedulerSnapshot = {
|
|
|
22
22
|
content: string;
|
|
23
23
|
};
|
|
24
24
|
export declare function stableMinuteOffset(homeDir: string, machineName?: string): number;
|
|
25
|
+
export declare function validateLinuxSchedulerPrerequisite(runner: ProcessRunner): Promise<void>;
|
|
25
26
|
export declare function prepareScheduler(options: {
|
|
26
27
|
platform?: NodeJS.Platform;
|
|
27
28
|
runner?: ProcessRunner;
|
package/dist/scheduler.js
CHANGED
|
@@ -14,12 +14,31 @@ function shellQuote(value) {
|
|
|
14
14
|
function ownerToken(installationId) {
|
|
15
15
|
return '--owner ' + installationId;
|
|
16
16
|
}
|
|
17
|
+
async function runCron(runner, args, input) {
|
|
18
|
+
const environment = { ...process.env, LC_ALL: 'C', LANG: 'C' };
|
|
19
|
+
try {
|
|
20
|
+
return await runner.run('crontab', args, input, environment);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
const code = error.code;
|
|
24
|
+
if (code === 'ENOENT') {
|
|
25
|
+
throw new Error('Linux requires the crontab command for periodic session sync.\n'
|
|
26
|
+
+ 'Ubuntu/Debian: sudo apt-get install cron\n'
|
|
27
|
+
+ 'Fedora/RHEL: sudo dnf install cronie\n'
|
|
28
|
+
+ 'Ensure the cron service is running, then rerun this command.');
|
|
29
|
+
}
|
|
30
|
+
if (code === 'EACCES') {
|
|
31
|
+
throw new Error('Cannot execute crontab. Check that the crontab executable has execute permission and is not on a filesystem '
|
|
32
|
+
+ 'mounted with noexec, then rerun this command.');
|
|
33
|
+
}
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
17
37
|
export function stableMinuteOffset(homeDir, machineName = os.hostname()) {
|
|
18
38
|
return Number.parseInt(createHash('sha256').update(machineName).update('\0').update(homeDir).digest('hex').slice(0, 8), 16) % 60;
|
|
19
39
|
}
|
|
20
40
|
async function readCron(runner) {
|
|
21
|
-
const
|
|
22
|
-
const result = await runner.run('crontab', ['-l'], undefined, environment);
|
|
41
|
+
const result = await runCron(runner, ['-l']);
|
|
23
42
|
if (result.code === 0) {
|
|
24
43
|
return result.stdout;
|
|
25
44
|
}
|
|
@@ -33,12 +52,14 @@ function withoutOwnedCron(content) {
|
|
|
33
52
|
return content.split(/\r?\n/).filter((line) => !line.trimEnd().endsWith(CRON_MARKER)).join('\n');
|
|
34
53
|
}
|
|
35
54
|
async function writeCron(runner, content) {
|
|
36
|
-
const
|
|
37
|
-
const result = await runner.run('crontab', ['-'], content, environment);
|
|
55
|
+
const result = await runCron(runner, ['-'], content);
|
|
38
56
|
if (result.code !== 0) {
|
|
39
57
|
throw new Error('Cannot update user crontab: ' + result.stderr.trim());
|
|
40
58
|
}
|
|
41
59
|
}
|
|
60
|
+
export async function validateLinuxSchedulerPrerequisite(runner) {
|
|
61
|
+
await readCron(runner);
|
|
62
|
+
}
|
|
42
63
|
async function queryWindowsTask(runner, name) {
|
|
43
64
|
return runner.run('schtasks.exe', ['/Query', '/TN', name, '/XML']);
|
|
44
65
|
}
|
package/dist/sync.js
CHANGED
|
@@ -6,7 +6,7 @@ import { getHealth, postManifest, putContent, SystemicUploadError, TaskUploadErr
|
|
|
6
6
|
import { readInstalledState } from './installation.js';
|
|
7
7
|
import { acquireSyncLock, SyncAlreadyRunningError } from './lock.js';
|
|
8
8
|
import { readPendingMarkers, removePendingMarker } from './pending.js';
|
|
9
|
-
import { resolveLinkPaths } from './paths.js';
|
|
9
|
+
import { resolveLinkPaths, vscodeAgentWorkspaceStorageDir } from './paths.js';
|
|
10
10
|
import { isStableForUpload, selectQueueItems } from './queue.js';
|
|
11
11
|
import { fingerprintStableSource, LocalTaskError, assertSourceUnchanged } from './source.js';
|
|
12
12
|
import { clearSyncState, readSessionStates, readSyncState, writeSessionState, writeSyncState } from './state.js';
|
|
@@ -101,6 +101,10 @@ export async function runSyncOnce(options = {}) {
|
|
|
101
101
|
nowMs,
|
|
102
102
|
claudeProjectsDir: paths.claudeProjectsDir,
|
|
103
103
|
copilotSessionsDir: paths.copilotSessionsDir,
|
|
104
|
+
copilotVscodeWorkspaceStorageDirs: [...new Set([
|
|
105
|
+
...paths.copilotVscodeWorkspaceStorageDirs,
|
|
106
|
+
...(config.vscodeAgentFolder ? [vscodeAgentWorkspaceStorageDir(config.vscodeAgentFolder)] : []),
|
|
107
|
+
])],
|
|
104
108
|
codexHome: paths.codexHome,
|
|
105
109
|
reportIssue: (issue) => summary.collectorIssues.push(issue),
|
|
106
110
|
};
|
package/dist/transport.d.ts
CHANGED
|
@@ -20,8 +20,14 @@ export interface AcquireTransportOptions {
|
|
|
20
20
|
probeCompanion?: (companion: CompanionInfo, environment: NodeJS.ProcessEnv) => Promise<void>;
|
|
21
21
|
spawnChild?: (executable: string, args: string[], env: NodeJS.ProcessEnv) => ChildProcessWithoutNullStreams;
|
|
22
22
|
onNeedsLogin?: (url: string) => void;
|
|
23
|
+
failIfLoginRequired?: boolean;
|
|
24
|
+
replaceExistingTransport?: boolean;
|
|
23
25
|
bootstrapAuthKey?: string;
|
|
24
26
|
startTimeoutMs?: number;
|
|
27
|
+
handoffTimeoutMs?: number;
|
|
28
|
+
terminateProcess?: (pid: number) => void;
|
|
29
|
+
isProcessAlive?: (pid: number) => boolean;
|
|
30
|
+
restartPriorTransport?: (executable: string, args: string[], env: NodeJS.ProcessEnv) => void;
|
|
25
31
|
}
|
|
26
32
|
export declare function consumeBootstrapAuthKey(explicitAuthKey: string | undefined, environment?: NodeJS.ProcessEnv): string | undefined;
|
|
27
33
|
export declare function acquireRemoteTransport(options: AcquireTransportOptions): Promise<RemoteTransport>;
|
package/dist/transport.js
CHANGED
|
@@ -2,6 +2,7 @@ import * as fs from 'node:fs/promises';
|
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
3
|
import { isIPv4 } from 'node:net';
|
|
4
4
|
import * as http from 'node:http';
|
|
5
|
+
import * as path from 'node:path';
|
|
5
6
|
import { Readable } from 'node:stream';
|
|
6
7
|
import { assertCompanionProtocol, VESPER_NET_PROTOCOL_VERSION, resolveCompanion } from './companion.js';
|
|
7
8
|
const START_TIMEOUT_MS = 10 * 60_000;
|
|
@@ -9,7 +10,9 @@ const ATTACH_TIMEOUT_MS = 2_000;
|
|
|
9
10
|
const POLL_INTERVAL_MS = 100;
|
|
10
11
|
const MAX_CONTROL_BYTES = 64 * 1024;
|
|
11
12
|
const LOCAL_SECRET_HEADER = 'x-vesper-transport-secret';
|
|
13
|
+
const EXPECTED_OWNER_PID_HEADER = 'x-vesper-expected-owner-pid';
|
|
12
14
|
const KEEPALIVE_MS = 10_000;
|
|
15
|
+
const HANDOFF_TIMEOUT_MS = 10_000;
|
|
13
16
|
const PROXY_ENVIRONMENT_KEYS = new Set([
|
|
14
17
|
'http_proxy', 'https_proxy', 'all_proxy', 'no_proxy',
|
|
15
18
|
]);
|
|
@@ -17,6 +20,8 @@ const AUTH_KEY_ENVIRONMENT_KEYS = new Set([
|
|
|
17
20
|
'vesper_ts_authkey', 'ts_authkey', 'ts_auth_key',
|
|
18
21
|
]);
|
|
19
22
|
const AUTH_KEY_PATTERN = /^tskey-auth-[A-Za-z0-9_-]{16,512}$/;
|
|
23
|
+
const TRANSPORT_SECRET_ENVIRONMENT_KEY = 'VESPER_TRANSPORT_SECRET';
|
|
24
|
+
const TRANSPORT_SECRET_PATTERN = /^[0-9a-fA-F]{64}$/;
|
|
20
25
|
function directLoopbackFetch(input, init = {}) {
|
|
21
26
|
const inputRequest = input instanceof Request ? input : undefined;
|
|
22
27
|
const url = new URL(inputRequest?.url ?? input.toString());
|
|
@@ -140,7 +145,16 @@ async function readRendezvous(filePath) {
|
|
|
140
145
|
throw error;
|
|
141
146
|
}
|
|
142
147
|
}
|
|
143
|
-
|
|
148
|
+
function validateLease(candidate, hostUrl) {
|
|
149
|
+
if (candidate.protocolVersion !== VESPER_NET_PROTOCOL_VERSION || candidate.hostUrl !== hostUrl
|
|
150
|
+
|| typeof candidate.tailnetName !== 'string' || !candidate.tailnetName
|
|
151
|
+
|| typeof candidate.nodeName !== 'string' || !candidate.nodeName
|
|
152
|
+
|| typeof candidate.ipv4 !== 'string' || !isIPv4(candidate.ipv4)
|
|
153
|
+
|| typeof candidate.executablePath !== 'string' || !candidate.executablePath) {
|
|
154
|
+
throw new Error('Running vesper-net transport targets a different host or protocol');
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async function inspectTransport(paths, hostUrl, fetchImpl, renewLease) {
|
|
144
158
|
let rendezvous;
|
|
145
159
|
try {
|
|
146
160
|
rendezvous = await readRendezvous(paths.transportRendezvousPath);
|
|
@@ -154,54 +168,49 @@ async function attach(paths, hostUrl, fetchImpl, requiredExecutablePath) {
|
|
|
154
168
|
if (!rendezvous || !isAlive(rendezvous.pid)) {
|
|
155
169
|
return null;
|
|
156
170
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
catch {
|
|
166
|
-
return null;
|
|
167
|
-
}
|
|
168
|
-
};
|
|
169
|
-
const readLease = async (inspectOnly) => {
|
|
170
|
-
const response = await requestLease(inspectOnly);
|
|
171
|
-
if (!response?.ok) {
|
|
172
|
-
return null;
|
|
173
|
-
}
|
|
174
|
-
return await response.json();
|
|
175
|
-
};
|
|
176
|
-
let lease = await readLease(requiredExecutablePath !== undefined);
|
|
177
|
-
if (!lease) {
|
|
171
|
+
let response;
|
|
172
|
+
try {
|
|
173
|
+
response = await fetchImpl(`${rendezvous.endpoint}/__vesper_transport/${renewLease ? 'lease' : 'identity'}`, {
|
|
174
|
+
headers: { [LOCAL_SECRET_HEADER]: rendezvous.secret },
|
|
175
|
+
signal: AbortSignal.timeout(ATTACH_TIMEOUT_MS),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
178
179
|
return null;
|
|
179
180
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
181
|
+
if (!response.ok) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
const lease = await response.json();
|
|
185
|
+
validateLease(lease, hostUrl);
|
|
186
|
+
return { rendezvous, lease };
|
|
187
|
+
}
|
|
188
|
+
function sameExecutable(left, right) {
|
|
189
|
+
return process.platform === 'win32' ? left.toLowerCase() === right.toLowerCase() : left === right;
|
|
190
|
+
}
|
|
191
|
+
async function canonicalExecutable(executable) {
|
|
192
|
+
return fs.realpath(executable).catch(() => path.resolve(executable));
|
|
193
|
+
}
|
|
194
|
+
async function attach(paths, hostUrl, fetchImpl, requiredExecutablePath) {
|
|
195
|
+
let inspected = await inspectTransport(paths, hostUrl, fetchImpl, requiredExecutablePath === undefined);
|
|
196
|
+
if (!inspected) {
|
|
197
|
+
return null;
|
|
188
198
|
}
|
|
189
|
-
validateLease(lease);
|
|
190
199
|
if (requiredExecutablePath) {
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
if (!
|
|
200
|
+
const actualExecutablePath = await canonicalExecutable(inspected.lease.executablePath);
|
|
201
|
+
if (!sameExecutable(actualExecutablePath, requiredExecutablePath)) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
inspected = await inspectTransport(paths, hostUrl, fetchImpl, true);
|
|
205
|
+
if (!inspected) {
|
|
197
206
|
return null;
|
|
198
207
|
}
|
|
199
|
-
|
|
200
|
-
if (!
|
|
208
|
+
const renewedExecutablePath = await canonicalExecutable(inspected.lease.executablePath);
|
|
209
|
+
if (!sameExecutable(renewedExecutablePath, requiredExecutablePath)) {
|
|
201
210
|
return null;
|
|
202
211
|
}
|
|
203
|
-
validateLease(lease);
|
|
204
212
|
}
|
|
213
|
+
const { rendezvous, lease } = inspected;
|
|
205
214
|
const transportFetch = async (input, init) => {
|
|
206
215
|
const remote = input instanceof Request ? new URL(input.url) : new URL(input.toString());
|
|
207
216
|
if (remote.origin !== new URL(hostUrl).origin) {
|
|
@@ -238,6 +247,122 @@ async function attach(paths, hostUrl, fetchImpl, requiredExecutablePath) {
|
|
|
238
247
|
},
|
|
239
248
|
};
|
|
240
249
|
}
|
|
250
|
+
function isManagedRuntimeExecutable(paths, executable) {
|
|
251
|
+
const relative = path.relative(path.resolve(paths.runtimeDir), path.resolve(executable));
|
|
252
|
+
const executableName = path.basename(executable).toLowerCase();
|
|
253
|
+
return relative !== '' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative)
|
|
254
|
+
&& (executableName === 'vesper-net' || executableName === 'vesper-net.exe')
|
|
255
|
+
&& relative.split(path.sep).includes('node_modules');
|
|
256
|
+
}
|
|
257
|
+
async function prepareTransportHandoff(options, fetchImpl, requiredExecutablePath) {
|
|
258
|
+
const inspected = await inspectTransport(options.paths, options.hostUrl, fetchImpl, false);
|
|
259
|
+
if (!inspected) {
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
const oldExecutablePath = await canonicalExecutable(inspected.lease.executablePath);
|
|
263
|
+
if (sameExecutable(oldExecutablePath, requiredExecutablePath)) {
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
266
|
+
if (!isManagedRuntimeExecutable(options.paths, oldExecutablePath)) {
|
|
267
|
+
throw new Error(`Refusing to replace vesper-net transport outside the managed runtime: ${oldExecutablePath}`);
|
|
268
|
+
}
|
|
269
|
+
return { inspected, oldExecutablePath };
|
|
270
|
+
}
|
|
271
|
+
async function stopTransportForHandoff(options, fetchImpl, handoff) {
|
|
272
|
+
const { inspected, oldExecutablePath } = handoff;
|
|
273
|
+
const oldPid = inspected.rendezvous.pid;
|
|
274
|
+
const processIsAlive = options.isProcessAlive ?? isAlive;
|
|
275
|
+
if (inspected.lease.supportsShutdown === true) {
|
|
276
|
+
let response;
|
|
277
|
+
try {
|
|
278
|
+
response = await fetchImpl(`${inspected.rendezvous.endpoint}/__vesper_transport/shutdown`, {
|
|
279
|
+
method: 'POST', headers: {
|
|
280
|
+
[LOCAL_SECRET_HEADER]: inspected.rendezvous.secret,
|
|
281
|
+
[EXPECTED_OWNER_PID_HEADER]: String(oldPid),
|
|
282
|
+
},
|
|
283
|
+
signal: AbortSignal.timeout(ATTACH_TIMEOUT_MS),
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
throw new Error(`Failed to gracefully stop prior vesper-net transport (pid=${oldPid})`);
|
|
288
|
+
}
|
|
289
|
+
if (response.status === 409) {
|
|
290
|
+
// The inspected owner has already yielded the inherited listener.
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (!response.ok) {
|
|
294
|
+
throw new Error(`Failed to gracefully stop prior vesper-net transport (pid=${oldPid})`);
|
|
295
|
+
}
|
|
296
|
+
if (await waitForProcessExit(oldPid, options.handoffTimeoutMs ?? HANDOFF_TIMEOUT_MS, processIsAlive)) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
throw new Error(`Timed out stopping prior vesper-net transport (pid=${oldPid})`);
|
|
300
|
+
}
|
|
301
|
+
const revalidated = await inspectTransport(options.paths, options.hostUrl, fetchImpl, false);
|
|
302
|
+
if (!revalidated) {
|
|
303
|
+
throw new Error(`Timed out stopping prior vesper-net transport (pid=${oldPid})`);
|
|
304
|
+
}
|
|
305
|
+
const revalidatedExecutablePath = await canonicalExecutable(revalidated.lease.executablePath);
|
|
306
|
+
if (revalidated.rendezvous.pid !== oldPid
|
|
307
|
+
|| revalidated.rendezvous.endpoint !== inspected.rendezvous.endpoint
|
|
308
|
+
|| revalidated.rendezvous.secret !== inspected.rendezvous.secret
|
|
309
|
+
|| !sameExecutable(revalidatedExecutablePath, oldExecutablePath)
|
|
310
|
+
|| !isManagedRuntimeExecutable(options.paths, revalidatedExecutablePath)) {
|
|
311
|
+
throw new Error('vesper-net transport changed ownership during upgrade handoff');
|
|
312
|
+
}
|
|
313
|
+
(options.terminateProcess ?? ((pid) => process.kill(pid, 'SIGTERM')))(oldPid);
|
|
314
|
+
if (!await waitForProcessExit(oldPid, options.handoffTimeoutMs ?? HANDOFF_TIMEOUT_MS, processIsAlive)) {
|
|
315
|
+
throw new Error(`Timed out stopping prior vesper-net transport (pid=${oldPid})`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
async function evictUnexpectedTransportOwner(options, fetchImpl, requiredExecutablePath) {
|
|
319
|
+
const inspected = await inspectTransport(options.paths, options.hostUrl, fetchImpl, false);
|
|
320
|
+
if (!inspected) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const executablePath = await canonicalExecutable(inspected.lease.executablePath);
|
|
324
|
+
if (sameExecutable(executablePath, requiredExecutablePath)) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (!isManagedRuntimeExecutable(options.paths, executablePath)) {
|
|
328
|
+
throw new Error(`Refusing to replace vesper-net transport outside the managed runtime: ${executablePath}`);
|
|
329
|
+
}
|
|
330
|
+
await stopTransportForHandoff(options, fetchImpl, { inspected, oldExecutablePath: executablePath });
|
|
331
|
+
}
|
|
332
|
+
async function waitForProcessExit(pid, timeoutMs, processIsAlive) {
|
|
333
|
+
const deadline = Date.now() + timeoutMs;
|
|
334
|
+
while (Date.now() < deadline) {
|
|
335
|
+
if (!processIsAlive(pid)) {
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
338
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
339
|
+
}
|
|
340
|
+
return !processIsAlive(pid);
|
|
341
|
+
}
|
|
342
|
+
function defaultRestartPriorTransport(executable, args, env) {
|
|
343
|
+
const child = spawn(executable, args, { env, detached: true, windowsHide: true, stdio: 'ignore' });
|
|
344
|
+
child.unref();
|
|
345
|
+
}
|
|
346
|
+
async function restartPriorTransport(options, handoff, args, error) {
|
|
347
|
+
if (handoff) {
|
|
348
|
+
try {
|
|
349
|
+
const oldPid = handoff.inspected.rendezvous.pid;
|
|
350
|
+
const processIsAlive = options.isProcessAlive ?? isAlive;
|
|
351
|
+
if (processIsAlive(oldPid) && !await waitForProcessExit(oldPid, options.handoffTimeoutMs ?? HANDOFF_TIMEOUT_MS, processIsAlive)) {
|
|
352
|
+
throw new Error(`Prior vesper-net transport is still exiting (pid=${oldPid})`);
|
|
353
|
+
}
|
|
354
|
+
(options.restartPriorTransport ?? defaultRestartPriorTransport)(handoff.oldExecutablePath, args, companionEnvironment(process.env));
|
|
355
|
+
}
|
|
356
|
+
catch (recoveryError) {
|
|
357
|
+
throw new AggregateError([error, recoveryError], 'vesper-net candidate failed and prior transport restart failed');
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
throw error;
|
|
361
|
+
}
|
|
362
|
+
async function failAfterHandoff(options, handoff, baseArgs, child, error) {
|
|
363
|
+
child.kill('SIGTERM');
|
|
364
|
+
return restartPriorTransport(options, handoff, baseArgs, error);
|
|
365
|
+
}
|
|
241
366
|
function defaultSpawn(executable, args, env) {
|
|
242
367
|
return spawn(executable, args, {
|
|
243
368
|
env, detached: true, windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -246,7 +371,8 @@ function defaultSpawn(executable, args, env) {
|
|
|
246
371
|
function companionEnvironment(environment, bootstrapAuthKey) {
|
|
247
372
|
const filtered = Object.fromEntries(Object.entries(environment).filter(([key]) => {
|
|
248
373
|
const normalized = key.toLowerCase();
|
|
249
|
-
return !PROXY_ENVIRONMENT_KEYS.has(normalized) && !AUTH_KEY_ENVIRONMENT_KEYS.has(normalized)
|
|
374
|
+
return !PROXY_ENVIRONMENT_KEYS.has(normalized) && !AUTH_KEY_ENVIRONMENT_KEYS.has(normalized)
|
|
375
|
+
&& normalized !== TRANSPORT_SECRET_ENVIRONMENT_KEY.toLowerCase();
|
|
250
376
|
}));
|
|
251
377
|
if (bootstrapAuthKey !== undefined) {
|
|
252
378
|
filtered.VESPER_TS_AUTHKEY = bootstrapAuthKey;
|
|
@@ -292,7 +418,7 @@ export async function acquireRemoteTransport(options) {
|
|
|
292
418
|
await (options.probeCompanion ?? assertCompanionProtocol)(companion, protocolProbeEnvironment(process.env));
|
|
293
419
|
}
|
|
294
420
|
const requiredExecutablePath = companion
|
|
295
|
-
? await
|
|
421
|
+
? await canonicalExecutable(companion.executable)
|
|
296
422
|
: undefined;
|
|
297
423
|
const existing = await attach(options.paths, options.hostUrl, fetchImpl, requiredExecutablePath);
|
|
298
424
|
if (existing) {
|
|
@@ -304,16 +430,30 @@ export async function acquireRemoteTransport(options) {
|
|
|
304
430
|
if (!options.requiredCompanion) {
|
|
305
431
|
await (options.probeCompanion ?? assertCompanionProtocol)(companion, protocolProbeEnvironment(process.env));
|
|
306
432
|
}
|
|
307
|
-
const
|
|
308
|
-
|
|
433
|
+
const handoff = options.replaceExistingTransport && requiredExecutablePath
|
|
434
|
+
? await prepareTransportHandoff(options, fetchImpl, requiredExecutablePath)
|
|
435
|
+
: undefined;
|
|
436
|
+
if (handoff && !TRANSPORT_SECRET_PATTERN.test(handoff.inspected.rendezvous.secret)) {
|
|
437
|
+
throw new Error('Running vesper-net transport has a malformed handoff secret');
|
|
438
|
+
}
|
|
439
|
+
const baseArgs = [
|
|
309
440
|
'--mode', 'transport', '--state-dir', options.paths.tsnetDir, '--hostname', 'vesper-link',
|
|
310
441
|
'--host-url', options.hostUrl, '--rendezvous', options.paths.transportRendezvousPath,
|
|
311
442
|
'--lock', options.paths.transportLockPath, '--idle-timeout', '30s',
|
|
312
443
|
];
|
|
313
|
-
const
|
|
444
|
+
const candidateArgs = handoff
|
|
445
|
+
? [...baseArgs, '--transport-listen', new URL(handoff.inspected.rendezvous.endpoint).host]
|
|
446
|
+
: baseArgs;
|
|
447
|
+
const candidateEnvironment = companionEnvironment(process.env, bootstrapAuthKey);
|
|
448
|
+
if (handoff) {
|
|
449
|
+
candidateEnvironment[TRANSPORT_SECRET_ENVIRONMENT_KEY] = handoff.inspected.rendezvous.secret;
|
|
450
|
+
}
|
|
451
|
+
const child = (options.spawnChild ?? defaultSpawn)(companion.executable, candidateArgs, candidateEnvironment);
|
|
314
452
|
child.stdin.end();
|
|
315
453
|
let controlBuffer = '';
|
|
316
454
|
let fatalMessage;
|
|
455
|
+
let waitingForOwner = false;
|
|
456
|
+
let ownerAcquired = false;
|
|
317
457
|
child.once('error', (error) => { fatalMessage = `spawn failed: ${error.message}`; });
|
|
318
458
|
child.once('exit', (code, signal) => {
|
|
319
459
|
fatalMessage ??= `exited before ready (code=${code ?? 'none'}, signal=${signal ?? 'none'})`;
|
|
@@ -339,11 +479,21 @@ export async function acquireRemoteTransport(options) {
|
|
|
339
479
|
fatalMessage = 'vesper-net protocol mismatch';
|
|
340
480
|
child.kill('SIGTERM');
|
|
341
481
|
}
|
|
482
|
+
else if (event.type === 'waiting_for_owner') {
|
|
483
|
+
waitingForOwner = true;
|
|
484
|
+
}
|
|
485
|
+
else if (event.type === 'owner_acquired') {
|
|
486
|
+
ownerAcquired = true;
|
|
487
|
+
}
|
|
342
488
|
else if (event.type === 'needs_login' && typeof event.loginUrl === 'string') {
|
|
343
489
|
if (bootstrapAuthKey !== undefined) {
|
|
344
490
|
fatalMessage = 'automatic enrollment key was not accepted; copy a fresh Memory Link command';
|
|
345
491
|
child.kill('SIGTERM');
|
|
346
492
|
}
|
|
493
|
+
else if (options.failIfLoginRequired) {
|
|
494
|
+
fatalMessage = 'the existing Tailscale identity is unavailable; upgrade stopped without relinking';
|
|
495
|
+
child.kill('SIGTERM');
|
|
496
|
+
}
|
|
347
497
|
else {
|
|
348
498
|
options.onNeedsLogin?.(event.loginUrl);
|
|
349
499
|
}
|
|
@@ -369,14 +519,43 @@ export async function acquireRemoteTransport(options) {
|
|
|
369
519
|
});
|
|
370
520
|
// Never persist helper stderr. Drain it to prevent a blocked pipe.
|
|
371
521
|
child.stderr.resume();
|
|
522
|
+
const deadline = Date.now() + (options.startTimeoutMs ?? START_TIMEOUT_MS);
|
|
523
|
+
let handoffNeedsRecovery = false;
|
|
524
|
+
if (handoff) {
|
|
525
|
+
// Wait until the candidate confirms it has contended on the owner lock.
|
|
526
|
+
// This closes the stop/start gap where an old runtime could otherwise win.
|
|
527
|
+
while (!waitingForOwner && !ownerAcquired && !fatalMessage && Date.now() < deadline) {
|
|
528
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
529
|
+
}
|
|
530
|
+
if (fatalMessage) {
|
|
531
|
+
child.kill('SIGTERM');
|
|
532
|
+
return restartPriorTransport(options, undefined, baseArgs, new Error(`vesper-net transport failed: ${fatalMessage}`));
|
|
533
|
+
}
|
|
534
|
+
if (!waitingForOwner && !ownerAcquired) {
|
|
535
|
+
child.kill('SIGTERM');
|
|
536
|
+
return restartPriorTransport(options, undefined, baseArgs, new Error('Timed out waiting for vesper-net candidate to queue for transport ownership'));
|
|
537
|
+
}
|
|
538
|
+
if (waitingForOwner) {
|
|
539
|
+
try {
|
|
540
|
+
await stopTransportForHandoff(options, fetchImpl, handoff);
|
|
541
|
+
}
|
|
542
|
+
catch (error) {
|
|
543
|
+
child.kill('SIGTERM');
|
|
544
|
+
throw error;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
handoffNeedsRecovery = true;
|
|
548
|
+
}
|
|
372
549
|
while (Date.now() < deadline) {
|
|
373
550
|
let attached;
|
|
374
551
|
try {
|
|
552
|
+
if (handoff && requiredExecutablePath) {
|
|
553
|
+
await evictUnexpectedTransportOwner(options, fetchImpl, requiredExecutablePath);
|
|
554
|
+
}
|
|
375
555
|
attached = await attach(options.paths, options.hostUrl, fetchImpl, requiredExecutablePath);
|
|
376
556
|
}
|
|
377
557
|
catch (error) {
|
|
378
|
-
child
|
|
379
|
-
throw error;
|
|
558
|
+
return failAfterHandoff(options, handoffNeedsRecovery ? handoff : undefined, baseArgs, child, error);
|
|
380
559
|
}
|
|
381
560
|
if (attached) {
|
|
382
561
|
if (attached.ownerPid === child.pid) {
|
|
@@ -390,11 +569,11 @@ export async function acquireRemoteTransport(options) {
|
|
|
390
569
|
return attached.transport;
|
|
391
570
|
}
|
|
392
571
|
if (fatalMessage) {
|
|
393
|
-
|
|
394
|
-
|
|
572
|
+
const error = new Error(`vesper-net transport failed: ${fatalMessage}`);
|
|
573
|
+
return failAfterHandoff(options, handoffNeedsRecovery ? handoff : undefined, baseArgs, child, error);
|
|
395
574
|
}
|
|
396
575
|
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
397
576
|
}
|
|
398
|
-
|
|
399
|
-
|
|
577
|
+
const error = new Error('Timed out waiting for vesper-net transport');
|
|
578
|
+
return failAfterHandoff(options, handoffNeedsRecovery ? handoff : undefined, baseArgs, child, error);
|
|
400
579
|
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VESPER_LINK_VERSION = "0.1.
|
|
1
|
+
export declare const VESPER_LINK_VERSION = "0.1.6";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VESPER_LINK_VERSION = '0.1.
|
|
1
|
+
export const VESPER_LINK_VERSION = '0.1.6';
|
package/package.json
CHANGED
|
@@ -1,39 +1,39 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@msvesper/vesper-link",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Connect agent sessions and tools to Vesper Memory",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"vesper-link": "dist/cli.js"
|
|
8
|
-
},
|
|
9
|
-
"files": [
|
|
10
|
-
"dist/**/*.js",
|
|
11
|
-
"dist/**/*.d.ts",
|
|
12
|
-
"README.md",
|
|
13
|
-
"LICENSE.md"
|
|
14
|
-
],
|
|
15
|
-
"scripts": {
|
|
16
|
-
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
17
|
-
"build": "npm run clean && tsc -p tsconfig.json",
|
|
18
|
-
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
19
|
-
"test": "npm run build && vitest run",
|
|
20
|
-
"pack-check": "npm run build && npm pack --dry-run",
|
|
21
|
-
"prepack": "npm run build"
|
|
22
|
-
},
|
|
23
|
-
"engines": {
|
|
24
|
-
"node": ">=20"
|
|
25
|
-
},
|
|
26
|
-
"license": "SEE LICENSE IN LICENSE.md",
|
|
27
|
-
"publishConfig": {
|
|
28
|
-
"access": "public"
|
|
29
|
-
},
|
|
30
|
-
"optionalDependencies": {
|
|
31
|
-
"@msvesper/vesper-net-linux-x64": "0.1.
|
|
32
|
-
"@msvesper/vesper-net-win32-x64": "0.1.
|
|
33
|
-
},
|
|
34
|
-
"devDependencies": {
|
|
35
|
-
"@types/node": "^22.15.0",
|
|
36
|
-
"typescript": "^5.8.3",
|
|
37
|
-
"vitest": "^3.2.4"
|
|
38
|
-
}
|
|
39
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@msvesper/vesper-link",
|
|
3
|
+
"version": "0.1.6",
|
|
4
|
+
"description": "Connect agent sessions and tools to Vesper Memory",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"vesper-link": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist/**/*.js",
|
|
11
|
+
"dist/**/*.d.ts",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
17
|
+
"build": "npm run clean && tsc -p tsconfig.json",
|
|
18
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
19
|
+
"test": "npm run build && vitest run",
|
|
20
|
+
"pack-check": "npm run build && npm pack --dry-run",
|
|
21
|
+
"prepack": "npm run build"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=20"
|
|
25
|
+
},
|
|
26
|
+
"license": "SEE LICENSE IN LICENSE.md",
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"optionalDependencies": {
|
|
31
|
+
"@msvesper/vesper-net-linux-x64": "0.1.6",
|
|
32
|
+
"@msvesper/vesper-net-win32-x64": "0.1.6"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^22.15.0",
|
|
36
|
+
"typescript": "^5.8.3",
|
|
37
|
+
"vitest": "^3.2.4"
|
|
38
|
+
}
|
|
39
|
+
}
|