@volute_cvc/cvc-mcp 0.4.1 โ 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/THIRD-PARTY-NOTICES.md +16357 -0
- package/bin/cvc-mcp.js +3 -112
- package/bin/cvc.js +3 -117
- package/bin/release.js +273 -0
- package/package.json +15 -4
- package/scripts/stage-third-party-notices.mjs +26 -0
- package/test-local-install.sh +0 -29
package/bin/cvc-mcp.js
CHANGED
|
@@ -1,116 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const fs = require('fs');
|
|
6
|
-
const https = require('https');
|
|
7
|
-
const child_process = require('child_process');
|
|
8
|
-
|
|
9
|
-
const CACHE_DIR = path.join(os.homedir(), '.cvc', 'mcp-cache');
|
|
10
|
-
|
|
11
|
-
function getPlatformDetails() {
|
|
12
|
-
const platform = os.platform();
|
|
13
|
-
const arch = os.arch();
|
|
14
|
-
|
|
15
|
-
let osTag = '';
|
|
16
|
-
let archTag = '';
|
|
17
|
-
|
|
18
|
-
// Determine OS Tag
|
|
19
|
-
if (platform === 'linux') {
|
|
20
|
-
osTag = 'unknown-linux-gnu';
|
|
21
|
-
} else if (platform === 'darwin') {
|
|
22
|
-
osTag = 'apple-darwin';
|
|
23
|
-
} else if (platform === 'win32') {
|
|
24
|
-
osTag = 'pc-windows-msvc';
|
|
25
|
-
} else {
|
|
26
|
-
console.error(`Unsupported OS: ${platform}`);
|
|
27
|
-
process.exit(1);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Determine Arch Tag
|
|
31
|
-
if (arch === 'x64') {
|
|
32
|
-
archTag = 'x86_64';
|
|
33
|
-
} else if (arch === 'arm64' || arch === 'aarch64') {
|
|
34
|
-
archTag = 'aarch64';
|
|
35
|
-
} else {
|
|
36
|
-
console.error(`Unsupported Architecture: ${arch}`);
|
|
37
|
-
process.exit(1);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return { platform, osTag, archTag };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function download(url, dest) {
|
|
44
|
-
return new Promise((resolve, reject) => {
|
|
45
|
-
const file = fs.createWriteStream(dest);
|
|
46
|
-
https.get(url, (response) => {
|
|
47
|
-
if ([301, 302, 307, 308].includes(response.statusCode)) {
|
|
48
|
-
return download(response.headers.location, dest).then(resolve).catch(reject);
|
|
49
|
-
}
|
|
50
|
-
if (response.statusCode !== 200) {
|
|
51
|
-
return reject(new Error(`Failed to download ${url}: ${response.statusCode}`));
|
|
52
|
-
}
|
|
53
|
-
response.pipe(file);
|
|
54
|
-
file.on('finish', () => {
|
|
55
|
-
file.close(resolve);
|
|
56
|
-
});
|
|
57
|
-
}).on('error', (err) => {
|
|
58
|
-
fs.unlink(dest, () => reject(err));
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function extract(archivePath, destDir, platform) {
|
|
64
|
-
if (platform === 'win32') {
|
|
65
|
-
// Basic unzipping on windows using powershell
|
|
66
|
-
child_process.execSync(`powershell -Command "Expand-Archive -Path '${archivePath}' -DestinationPath '${destDir}' -Force"`, { stdio: 'inherit' });
|
|
67
|
-
} else {
|
|
68
|
-
// Assuming tar is available on Unix
|
|
69
|
-
child_process.execSync(`tar -xzf "${archivePath}" -C "${destDir}"`, { stdio: 'inherit' });
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async function main() {
|
|
74
|
-
const { platform, osTag, archTag } = getPlatformDetails();
|
|
75
|
-
const ext = platform === 'win32' ? 'zip' : 'tar.gz';
|
|
76
|
-
const binName = platform === 'win32' ? 'cvc-mcp.exe' : 'cvc-mcp';
|
|
77
|
-
const assetName = `cvc-${archTag}-${osTag}.${ext}`;
|
|
78
|
-
const downloadUrl = `https://cvc.dev/api/download/${assetName}`;
|
|
79
|
-
|
|
80
|
-
const binPath = path.join(CACHE_DIR, binName);
|
|
81
|
-
|
|
82
|
-
if (!fs.existsSync(binPath)) {
|
|
83
|
-
console.error(`CVC MCP not found locally. Downloading from ${downloadUrl}...`);
|
|
84
|
-
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
85
|
-
const tmpArchive = path.join(CACHE_DIR, assetName);
|
|
86
|
-
|
|
87
|
-
try {
|
|
88
|
-
await download(downloadUrl, tmpArchive);
|
|
89
|
-
console.error('Extracting...');
|
|
90
|
-
extract(tmpArchive, CACHE_DIR, platform);
|
|
91
|
-
fs.unlinkSync(tmpArchive); // Clean up archive
|
|
92
|
-
if (platform !== 'win32') {
|
|
93
|
-
fs.chmodSync(binPath, 0o755);
|
|
94
|
-
}
|
|
95
|
-
console.error('Download and extraction complete.');
|
|
96
|
-
} catch (e) {
|
|
97
|
-
console.error(`Failed to install CVC MCP: ${e.message}`);
|
|
98
|
-
process.exit(1);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Execute the binary
|
|
103
|
-
const args = process.argv.slice(2);
|
|
104
|
-
const child = child_process.spawn(binPath, args, {
|
|
105
|
-
stdio: 'inherit'
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
child.on('close', (code) => {
|
|
109
|
-
process.exit(code);
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
main().catch(e => {
|
|
114
|
-
console.error(e);
|
|
4
|
+
require('./release').launch('cvc-mcp').catch(error => {
|
|
5
|
+
console.error(`Failed to install CVC MCP: ${error.message}`);
|
|
115
6
|
process.exit(1);
|
|
116
7
|
});
|
package/bin/cvc.js
CHANGED
|
@@ -1,121 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const fs = require('fs');
|
|
6
|
-
const https = require('https');
|
|
7
|
-
const child_process = require('child_process');
|
|
8
|
-
|
|
9
|
-
const CACHE_DIR = path.join(os.homedir(), '.cvc', 'mcp-cache');
|
|
10
|
-
|
|
11
|
-
function getPlatformDetails() {
|
|
12
|
-
const platform = os.platform();
|
|
13
|
-
const arch = os.arch();
|
|
14
|
-
|
|
15
|
-
let osTag = '';
|
|
16
|
-
let archTag = '';
|
|
17
|
-
|
|
18
|
-
// Determine OS Tag
|
|
19
|
-
if (platform === 'linux') {
|
|
20
|
-
osTag = 'unknown-linux-gnu';
|
|
21
|
-
} else if (platform === 'darwin') {
|
|
22
|
-
osTag = 'apple-darwin';
|
|
23
|
-
} else if (platform === 'win32') {
|
|
24
|
-
osTag = 'pc-windows-msvc';
|
|
25
|
-
} else {
|
|
26
|
-
console.error(`Unsupported OS: ${platform}`);
|
|
27
|
-
process.exit(1);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Determine Arch Tag
|
|
31
|
-
if (arch === 'x64') {
|
|
32
|
-
archTag = 'x86_64';
|
|
33
|
-
} else if (arch === 'arm64' || arch === 'aarch64') {
|
|
34
|
-
archTag = 'aarch64';
|
|
35
|
-
} else {
|
|
36
|
-
console.error(`Unsupported Architecture: ${arch}`);
|
|
37
|
-
process.exit(1);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return { platform, osTag, archTag };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function download(url, dest) {
|
|
44
|
-
return new Promise((resolve, reject) => {
|
|
45
|
-
const file = fs.createWriteStream(dest);
|
|
46
|
-
https.get(url, (response) => {
|
|
47
|
-
if ([301, 302, 307, 308].includes(response.statusCode)) {
|
|
48
|
-
return download(response.headers.location, dest).then(resolve).catch(reject);
|
|
49
|
-
}
|
|
50
|
-
if (response.statusCode !== 200) {
|
|
51
|
-
return reject(new Error(`Failed to download ${url}: ${response.statusCode}`));
|
|
52
|
-
}
|
|
53
|
-
response.pipe(file);
|
|
54
|
-
file.on('finish', () => {
|
|
55
|
-
file.close(resolve);
|
|
56
|
-
});
|
|
57
|
-
}).on('error', (err) => {
|
|
58
|
-
fs.unlink(dest, () => reject(err));
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function extract(archivePath, destDir, platform) {
|
|
64
|
-
if (platform === 'win32') {
|
|
65
|
-
// Basic unzipping on windows using powershell
|
|
66
|
-
child_process.execSync(`powershell -Command "Expand-Archive -Path '${archivePath}' -DestinationPath '${destDir}' -Force"`, { stdio: 'inherit' });
|
|
67
|
-
} else {
|
|
68
|
-
// Assuming tar is available on Unix
|
|
69
|
-
child_process.execSync(`tar -xzf "${archivePath}" -C "${destDir}"`, { stdio: 'inherit' });
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async function main() {
|
|
74
|
-
const { platform, osTag, archTag } = getPlatformDetails();
|
|
75
|
-
const ext = platform === 'win32' ? 'zip' : 'tar.gz';
|
|
76
|
-
const binName = platform === 'win32' ? 'cvc.exe' : 'cvc';
|
|
77
|
-
const assetName = `cvc-${archTag}-${osTag}.${ext}`;
|
|
78
|
-
const downloadUrl = `https://cvc.dev/api/download/${assetName}`;
|
|
79
|
-
|
|
80
|
-
const binPath = path.join(CACHE_DIR, binName);
|
|
81
|
-
|
|
82
|
-
if (!fs.existsSync(binPath)) {
|
|
83
|
-
console.error(`CVC CLI not found locally. Downloading from ${downloadUrl}...`);
|
|
84
|
-
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
85
|
-
const tmpArchive = path.join(CACHE_DIR, assetName);
|
|
86
|
-
|
|
87
|
-
try {
|
|
88
|
-
await download(downloadUrl, tmpArchive);
|
|
89
|
-
console.error('Extracting...');
|
|
90
|
-
extract(tmpArchive, CACHE_DIR, platform);
|
|
91
|
-
fs.unlinkSync(tmpArchive); // Clean up archive
|
|
92
|
-
if (platform !== 'win32') {
|
|
93
|
-
fs.chmodSync(binPath, 0o755);
|
|
94
|
-
// Also ensure MCP and LSP are executable since they are extracted together
|
|
95
|
-
const mcpPath = path.join(CACHE_DIR, 'cvc-mcp');
|
|
96
|
-
const lspPath = path.join(CACHE_DIR, 'cvc-lsp');
|
|
97
|
-
if (fs.existsSync(mcpPath)) fs.chmodSync(mcpPath, 0o755);
|
|
98
|
-
if (fs.existsSync(lspPath)) fs.chmodSync(lspPath, 0o755);
|
|
99
|
-
}
|
|
100
|
-
console.error('Download and extraction complete.');
|
|
101
|
-
} catch (e) {
|
|
102
|
-
console.error(`Failed to install CVC CLI: ${e.message}`);
|
|
103
|
-
process.exit(1);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Execute the binary
|
|
108
|
-
const args = process.argv.slice(2);
|
|
109
|
-
const child = child_process.spawn(binPath, args, {
|
|
110
|
-
stdio: 'inherit'
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
child.on('close', (code) => {
|
|
114
|
-
process.exit(code);
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
main().catch(e => {
|
|
119
|
-
console.error(e);
|
|
4
|
+
require('./release').launch('cvc').catch(error => {
|
|
5
|
+
console.error(`Failed to install CVC CLI: ${error.message}`);
|
|
120
6
|
process.exit(1);
|
|
121
7
|
});
|
package/bin/release.js
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const fsp = fs.promises;
|
|
6
|
+
const https = require('https');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { spawn, spawnSync } = require('child_process');
|
|
10
|
+
const { Transform } = require('stream');
|
|
11
|
+
const { pipeline } = require('stream/promises');
|
|
12
|
+
|
|
13
|
+
const DEFAULT_REPOSITORY = 'meirka8/volute';
|
|
14
|
+
const DEFAULT_BASE_URL = 'https://github.com';
|
|
15
|
+
const PACKAGE_VERSION = require('../package.json').version;
|
|
16
|
+
const MAX_REDIRECTS = 5;
|
|
17
|
+
const MAX_CHECKSUM_BYTES = 1024 * 1024;
|
|
18
|
+
const MAX_ARCHIVE_BYTES = 256 * 1024 * 1024;
|
|
19
|
+
const MAX_BINARY_BYTES = 128 * 1024 * 1024;
|
|
20
|
+
const BINARIES = ['cvc', 'cvc-mcp', 'cvc-lsp'];
|
|
21
|
+
|
|
22
|
+
function validVersion(version) {
|
|
23
|
+
return /^\d+\.\d+\.\d+(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$/.test(version);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function releaseConfiguration(env = process.env) {
|
|
27
|
+
const repository = env.CVC_RELEASE_REPOSITORY || DEFAULT_REPOSITORY;
|
|
28
|
+
const baseUrl = (env.CVC_RELEASE_BASE_URL || DEFAULT_BASE_URL).replace(/\/+$/, '');
|
|
29
|
+
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository) || repository.includes('..')) {
|
|
30
|
+
throw new Error('CVC_RELEASE_REPOSITORY must be a safe owner/repository name.');
|
|
31
|
+
}
|
|
32
|
+
const parsed = new URL(baseUrl);
|
|
33
|
+
if (parsed.protocol !== 'https:') throw new Error('CVC_RELEASE_BASE_URL must use HTTPS.');
|
|
34
|
+
if (parsed.username || parsed.password || parsed.search || parsed.hash) {
|
|
35
|
+
throw new Error('CVC_RELEASE_BASE_URL must not contain credentials, a query, or a fragment.');
|
|
36
|
+
}
|
|
37
|
+
if (!validVersion(PACKAGE_VERSION)) throw new Error('The npm package has an invalid release version.');
|
|
38
|
+
return { repository, baseUrl, version: PACKAGE_VERSION };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getPlatformDetails(platform = os.platform(), arch = os.arch()) {
|
|
42
|
+
if (platform === 'linux' && arch === 'x64') return { platform, osTag: 'unknown-linux-gnu', archTag: 'x86_64' };
|
|
43
|
+
if (platform === 'darwin' && arch === 'x64') return { platform, osTag: 'apple-darwin', archTag: 'x86_64' };
|
|
44
|
+
if (platform === 'darwin' && arch === 'arm64') return { platform, osTag: 'apple-darwin', archTag: 'aarch64' };
|
|
45
|
+
if (platform === 'win32' && arch === 'x64') return { platform, osTag: 'pc-windows-msvc', archTag: 'x86_64' };
|
|
46
|
+
if (platform === 'linux' && arch === 'arm64') throw new Error('Unsupported architecture: Linux arm64 releases are not published.');
|
|
47
|
+
throw new Error(`Unsupported platform or architecture: ${platform}/${arch}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function releaseUrls(details, env = process.env) {
|
|
51
|
+
const { repository, baseUrl, version } = releaseConfiguration(env);
|
|
52
|
+
const ext = details.platform === 'win32' ? 'zip' : 'tar.gz';
|
|
53
|
+
const assetName = `cvc-${details.archTag}-${details.osTag}.${ext}`;
|
|
54
|
+
const releaseUrl = `${baseUrl}/${repository}/releases/download/v${version}`;
|
|
55
|
+
return { assetName, archiveUrl: `${releaseUrl}/${assetName}`, checksumUrl: `${releaseUrl}/SHA256SUMS.txt` };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function checksumForAsset(manifest, assetName) {
|
|
59
|
+
const escaped = assetName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
60
|
+
const match = new RegExp(`^([a-fA-F0-9]{64})\\s+\\*?${escaped}\\s*$`);
|
|
61
|
+
const values = manifest.split(/\r?\n/).map(line => line.match(match)).filter(Boolean).map(result => result[1].toLowerCase());
|
|
62
|
+
if (values.length !== 1) throw new Error(`SHA256SUMS does not contain one valid checksum for ${assetName}.`);
|
|
63
|
+
return values[0];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function redirectAllowed(initial, target) {
|
|
67
|
+
if (target.protocol !== 'https:' || target.username || target.password) return false;
|
|
68
|
+
if (target.host === initial.host) return true;
|
|
69
|
+
return initial.hostname === 'github.com' &&
|
|
70
|
+
(target.hostname === 'githubusercontent.com' || target.hostname.endsWith('.githubusercontent.com')) &&
|
|
71
|
+
target.port === '';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function request(url, redirects = 0, initialUrl) {
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
let parsed;
|
|
77
|
+
try { parsed = new URL(url); } catch (error) { reject(error); return; }
|
|
78
|
+
const initial = initialUrl || parsed;
|
|
79
|
+
if (parsed.protocol !== 'https:' || parsed.username || parsed.password) {
|
|
80
|
+
reject(new Error(`Refusing unsafe download URL: ${parsed.href}`)); return;
|
|
81
|
+
}
|
|
82
|
+
const req = https.get(parsed, { headers: { 'User-Agent': 'cvc-mcp-npm-launcher' } }, response => {
|
|
83
|
+
if ([301, 302, 303, 307, 308].includes(response.statusCode)) {
|
|
84
|
+
response.resume();
|
|
85
|
+
if (redirects >= MAX_REDIRECTS || !response.headers.location) {
|
|
86
|
+
reject(new Error(`Too many or invalid redirects while downloading ${initial.href}`)); return;
|
|
87
|
+
}
|
|
88
|
+
const target = new URL(response.headers.location, parsed);
|
|
89
|
+
if (!redirectAllowed(initial, target)) {
|
|
90
|
+
reject(new Error(`Refusing cross-origin or insecure redirect to ${target.origin}`)); return;
|
|
91
|
+
}
|
|
92
|
+
request(target.href, redirects + 1, initial).then(resolve, reject);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (response.statusCode !== 200) {
|
|
96
|
+
response.resume(); reject(new Error(`Download failed (${response.statusCode}) for ${initial.href}`)); return;
|
|
97
|
+
}
|
|
98
|
+
resolve(response);
|
|
99
|
+
});
|
|
100
|
+
req.setTimeout(30000, () => req.destroy(new Error(`Download timed out for ${initial.href}`)));
|
|
101
|
+
req.on('error', reject);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function downloadFile(url, destination, maximumBytes = MAX_ARCHIVE_BYTES) {
|
|
106
|
+
const response = await request(url);
|
|
107
|
+
const advertised = Number(response.headers['content-length']);
|
|
108
|
+
if (Number.isFinite(advertised) && advertised > maximumBytes) {
|
|
109
|
+
response.destroy(); throw new Error('Release download is unexpectedly large.');
|
|
110
|
+
}
|
|
111
|
+
let length = 0;
|
|
112
|
+
const limiter = new Transform({
|
|
113
|
+
transform(chunk, _encoding, callback) {
|
|
114
|
+
length += chunk.length;
|
|
115
|
+
callback(length > maximumBytes ? new Error('Release download is unexpectedly large.') : null, chunk);
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
const file = fs.createWriteStream(destination, { flags: 'wx', mode: 0o600 });
|
|
119
|
+
await pipeline(response, limiter, file);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function downloadText(url) {
|
|
123
|
+
const response = await request(url);
|
|
124
|
+
const chunks = [];
|
|
125
|
+
let length = 0;
|
|
126
|
+
return new Promise((resolve, reject) => {
|
|
127
|
+
response.on('data', chunk => {
|
|
128
|
+
length += chunk.length;
|
|
129
|
+
if (length > MAX_CHECKSUM_BYTES) { response.destroy(new Error('SHA256SUMS is unexpectedly large.')); return; }
|
|
130
|
+
chunks.push(chunk);
|
|
131
|
+
});
|
|
132
|
+
response.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
133
|
+
response.on('error', reject);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function sha256(file) {
|
|
138
|
+
const hash = crypto.createHash('sha256');
|
|
139
|
+
await pipeline(fs.createReadStream(file), new Transform({ transform(chunk, _encoding, callback) { hash.update(chunk); callback(); } }));
|
|
140
|
+
return hash.digest('hex');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function trustedExecutable() {
|
|
144
|
+
if (process.platform === 'win32') return path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
|
|
145
|
+
return '/usr/bin/tar';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function extractMember(archive, destination, member, platform) {
|
|
149
|
+
const fd = fs.openSync(destination, 'wx', 0o700);
|
|
150
|
+
try {
|
|
151
|
+
let result;
|
|
152
|
+
if (platform === 'win32') {
|
|
153
|
+
const script = path.join(path.dirname(archive), 'extract-member.ps1');
|
|
154
|
+
if (!fs.existsSync(script)) {
|
|
155
|
+
fs.writeFileSync(script, [
|
|
156
|
+
'param([string]$Archive,[string]$Member,[string]$Destination,[long]$Maximum)',
|
|
157
|
+
'Add-Type -AssemblyName System.IO.Compression.FileSystem',
|
|
158
|
+
'$zip = [IO.Compression.ZipFile]::OpenRead($Archive)',
|
|
159
|
+
'try {',
|
|
160
|
+
' $entries = @($zip.Entries | Where-Object { $_.FullName -ceq $Member })',
|
|
161
|
+
' if ($entries.Count -ne 1 -or $entries[0].Length -le 0 -or $entries[0].Length -gt $Maximum) { throw "Unsafe or missing archive member: $Member" }',
|
|
162
|
+
' $input = $entries[0].Open(); $output = [IO.File]::Open($Destination, [IO.FileMode]::Truncate, [IO.FileAccess]::Write, [IO.FileShare]::None)',
|
|
163
|
+
' try {',
|
|
164
|
+
' $buffer = New-Object byte[] 65536; [long]$total = 0',
|
|
165
|
+
' while (($read = $input.Read($buffer, 0, $buffer.Length)) -gt 0) { $total += $read; if ($total -gt $Maximum) { throw "Expanded member is too large: $Member" }; $output.Write($buffer, 0, $read) }',
|
|
166
|
+
' $output.Flush($true)',
|
|
167
|
+
' } finally { $output.Dispose(); $input.Dispose() }',
|
|
168
|
+
'} finally { $zip.Dispose() }',
|
|
169
|
+
].join('\n'), { mode: 0o600 });
|
|
170
|
+
}
|
|
171
|
+
fs.closeSync(fd);
|
|
172
|
+
result = spawnSync(trustedExecutable(), ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', script, archive, member, destination, String(MAX_BINARY_BYTES)], { stdio: 'inherit' });
|
|
173
|
+
} else {
|
|
174
|
+
// Limit the child output at the OS level as well as validating the final
|
|
175
|
+
// file, so a compressed member cannot fill the destination filesystem.
|
|
176
|
+
result = spawnSync('/bin/sh', ['-c', 'ulimit -f 262144; exec /usr/bin/tar -xOzf "$1" -- "$2"', 'cvc-extract', archive, member], { stdio: ['ignore', fd, 'inherit'] });
|
|
177
|
+
}
|
|
178
|
+
if (result.error || result.status !== 0) throw result.error || new Error(`Failed to extract ${member} (exit ${result.status}).`);
|
|
179
|
+
} finally {
|
|
180
|
+
try { fs.closeSync(fd); } catch (_) { /* PowerShell path closes it before spawning. */ }
|
|
181
|
+
}
|
|
182
|
+
const stat = fs.lstatSync(destination);
|
|
183
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size <= 0 || stat.size > MAX_BINARY_BYTES) {
|
|
184
|
+
throw new Error(`Release archive contains an unsafe binary: ${member}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function safeRegularFile(file) {
|
|
189
|
+
const stat = await fsp.lstat(file);
|
|
190
|
+
return stat.isFile() && !stat.isSymbolicLink() && stat.size > 0 && stat.size <= MAX_BINARY_BYTES;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function safeDirectory(directory) {
|
|
194
|
+
const stat = await fsp.lstat(directory);
|
|
195
|
+
return stat.isDirectory() && !stat.isSymbolicLink();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function ensurePrivateDirectory(directory) {
|
|
199
|
+
await fsp.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
200
|
+
if (!await safeDirectory(directory).catch(() => false)) throw new Error(`Refusing unsafe cache directory: ${directory}`);
|
|
201
|
+
await fsp.chmod(directory, 0o700);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function validCache(cacheDir, names) {
|
|
205
|
+
if (!await safeDirectory(cacheDir).catch(() => false)) return false;
|
|
206
|
+
for (const name of names) if (!await safeRegularFile(path.join(cacheDir, name)).catch(() => false)) return false;
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function installRelease(details, executable) {
|
|
211
|
+
const { version } = releaseConfiguration();
|
|
212
|
+
const names = details.platform === 'win32' ? BINARIES.map(name => `${name}.exe`) : BINARIES;
|
|
213
|
+
const binaryName = details.platform === 'win32' ? `${executable}.exe` : executable;
|
|
214
|
+
const cacheRoot = path.join(os.homedir(), '.cvc', 'mcp-cache');
|
|
215
|
+
const cacheDir = path.join(cacheRoot, `v${version}-${details.archTag}-${details.osTag}`);
|
|
216
|
+
const binaryPath = path.join(cacheDir, binaryName);
|
|
217
|
+
await ensurePrivateDirectory(path.join(os.homedir(), '.cvc'));
|
|
218
|
+
await ensurePrivateDirectory(cacheRoot);
|
|
219
|
+
if (await validCache(cacheDir, names)) return binaryPath;
|
|
220
|
+
if (await fsp.lstat(cacheDir).catch(() => null)) {
|
|
221
|
+
if (!await safeDirectory(cacheDir).catch(() => false)) throw new Error(`Refusing unsafe cache path: ${cacheDir}`);
|
|
222
|
+
await fsp.rm(cacheDir, { recursive: true, force: true });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const tempDir = await fsp.mkdtemp(path.join(os.tmpdir(), 'cvc-mcp-'));
|
|
226
|
+
let publishDir;
|
|
227
|
+
try {
|
|
228
|
+
const urls = releaseUrls(details);
|
|
229
|
+
const archive = path.join(tempDir, urls.assetName);
|
|
230
|
+
console.error(`Downloading CVC release from ${urls.archiveUrl}...`);
|
|
231
|
+
const expected = checksumForAsset(await downloadText(urls.checksumUrl), urls.assetName);
|
|
232
|
+
await downloadFile(urls.archiveUrl, archive);
|
|
233
|
+
if (!crypto.timingSafeEqual(Buffer.from(await sha256(archive), 'hex'), Buffer.from(expected, 'hex'))) {
|
|
234
|
+
throw new Error(`Checksum verification failed for ${urls.assetName}.`);
|
|
235
|
+
}
|
|
236
|
+
const stage = path.join(tempDir, 'stage');
|
|
237
|
+
await fsp.mkdir(stage, { mode: 0o700 });
|
|
238
|
+
console.error('Extracting verified members...');
|
|
239
|
+
for (const name of names) extractMember(archive, path.join(stage, name), name, details.platform);
|
|
240
|
+
|
|
241
|
+
publishDir = await fsp.mkdtemp(path.join(cacheRoot, '.install-'));
|
|
242
|
+
for (const name of names) {
|
|
243
|
+
const target = path.join(publishDir, name);
|
|
244
|
+
await fsp.copyFile(path.join(stage, name), target, fs.constants.COPYFILE_EXCL);
|
|
245
|
+
if (details.platform !== 'win32') await fsp.chmod(target, 0o755);
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
await fsp.rename(publishDir, cacheDir);
|
|
249
|
+
publishDir = undefined;
|
|
250
|
+
} catch (error) {
|
|
251
|
+
if (error.code !== 'EEXIST' && error.code !== 'ENOTEMPTY') throw error;
|
|
252
|
+
if (!await validCache(cacheDir, names)) throw new Error('A concurrent cache installation was incomplete or unsafe.');
|
|
253
|
+
}
|
|
254
|
+
console.error('Download and extraction complete.');
|
|
255
|
+
return binaryPath;
|
|
256
|
+
} finally {
|
|
257
|
+
if (publishDir) await fsp.rm(publishDir, { recursive: true, force: true }).catch(() => {});
|
|
258
|
+
await fsp.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function run(binary, args) {
|
|
263
|
+
const child = spawn(binary, args, { stdio: 'inherit' });
|
|
264
|
+
child.on('error', error => { console.error(`Failed to run ${binary}: ${error.message}`); process.exit(1); });
|
|
265
|
+
child.on('close', (code, signal) => process.exit(code === null ? (signal ? 1 : 0) : code));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function launch(executable) {
|
|
269
|
+
if (!BINARIES.includes(executable)) throw new Error('Refusing unknown CVC executable.');
|
|
270
|
+
run(await installRelease(getPlatformDetails(), executable), process.argv.slice(2));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
module.exports = { checksumForAsset, getPlatformDetails, launch, redirectAllowed, releaseConfiguration, releaseUrls, _extractMemberForTest: extractMember };
|
package/package.json
CHANGED
|
@@ -1,20 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volute_cvc/cvc-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "MCP server for CVC (Cognitive Version Control) for saving and reading git-linked AI work history",
|
|
5
5
|
"bin": {
|
|
6
6
|
"cvc-mcp": "./bin/cvc-mcp.js",
|
|
7
7
|
"cvc": "./bin/cvc.js"
|
|
8
8
|
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/*.js",
|
|
11
|
+
"scripts/stage-third-party-notices.mjs",
|
|
12
|
+
"THIRD-PARTY-NOTICES.md"
|
|
13
|
+
],
|
|
9
14
|
"scripts": {
|
|
10
|
-
"
|
|
15
|
+
"prepack": "node scripts/stage-third-party-notices.mjs stage",
|
|
16
|
+
"postpack": "node scripts/stage-third-party-notices.mjs cleanup",
|
|
17
|
+
"test": "node --test test/*.test.js"
|
|
11
18
|
},
|
|
12
19
|
"repository": {
|
|
13
20
|
"type": "git",
|
|
14
|
-
"url": "git+https://github.com/meirka8/
|
|
21
|
+
"url": "git+https://github.com/meirka8/volute.git"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/meirka8/volute#readme",
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/meirka8/volute/issues"
|
|
15
26
|
},
|
|
16
27
|
"author": "Volute",
|
|
17
|
-
"license": "
|
|
28
|
+
"license": "Apache-2.0",
|
|
18
29
|
"publishConfig": {
|
|
19
30
|
"access": "public"
|
|
20
31
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { constants, copyFileSync, existsSync, lstatSync, readFileSync, rmSync } from 'node:fs';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { dirname, resolve } from 'node:path';
|
|
5
|
+
|
|
6
|
+
const packageRoot = resolve(dirname(new URL(import.meta.url).pathname), '..');
|
|
7
|
+
const source = resolve(packageRoot, '..', '..', 'THIRD-PARTY-NOTICES.md');
|
|
8
|
+
const destination = resolve(packageRoot, 'THIRD-PARTY-NOTICES.md');
|
|
9
|
+
const digest = file => createHash('sha256').update(readFileSync(file)).digest('hex');
|
|
10
|
+
const regular = (file, label) => {
|
|
11
|
+
if (!existsSync(file)) throw new Error(`${label} is missing`);
|
|
12
|
+
const stat = lstatSync(file);
|
|
13
|
+
if (stat.isSymbolicLink() || !stat.isFile()) throw new Error(`${label} must be a regular file`);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
if (process.argv[2] === 'stage') {
|
|
17
|
+
regular(source, 'root THIRD-PARTY-NOTICES.md');
|
|
18
|
+
if (existsSync(destination)) throw new Error('refusing to overwrite staged THIRD-PARTY-NOTICES.md; run cleanup first');
|
|
19
|
+
copyFileSync(source, destination, constants.COPYFILE_EXCL);
|
|
20
|
+
regular(destination, 'staged THIRD-PARTY-NOTICES.md');
|
|
21
|
+
if (digest(source) !== digest(destination)) throw new Error('staged notice digest mismatch');
|
|
22
|
+
} else if (process.argv[2] === 'cleanup') {
|
|
23
|
+
if (existsSync(destination)) { regular(destination, 'staged THIRD-PARTY-NOTICES.md'); rmSync(destination); }
|
|
24
|
+
} else {
|
|
25
|
+
throw new Error('Usage: stage-third-party-notices.mjs <stage|cleanup>');
|
|
26
|
+
}
|
package/test-local-install.sh
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
set -e
|
|
3
|
-
|
|
4
|
-
LOCAL_INSTALL=false
|
|
5
|
-
if [ "$1" == "--local" ]; then
|
|
6
|
-
LOCAL_INSTALL=true
|
|
7
|
-
fi
|
|
8
|
-
|
|
9
|
-
echo "๐งน Cleaning up global cvc-mcp installation..."
|
|
10
|
-
npm uninstall -g @volute_cvc/cvc-mcp || true
|
|
11
|
-
|
|
12
|
-
echo "๐๏ธ Removing downloaded binary cache..."
|
|
13
|
-
rm -rf ~/.cvc/mcp-cache
|
|
14
|
-
|
|
15
|
-
if [ "$LOCAL_INSTALL" = true ]; then
|
|
16
|
-
echo "๐ฆ Packing local npm package..."
|
|
17
|
-
cd "$(dirname "$0")"
|
|
18
|
-
TARBALL=$(npm pack)
|
|
19
|
-
|
|
20
|
-
echo "๐ Installing local tarball globally..."
|
|
21
|
-
npm install -g "./$TARBALL"
|
|
22
|
-
else
|
|
23
|
-
echo "๐ Installing from NPM registry..."
|
|
24
|
-
npm install -g @volute_cvc/cvc-mcp
|
|
25
|
-
fi
|
|
26
|
-
|
|
27
|
-
echo "โ
Installed successfully!"
|
|
28
|
-
echo "You can now test the CLI by running: cvc --version"
|
|
29
|
-
echo "And test the MCP server by running: cvc-mcp"
|