srvgov-cli 0.1.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 +21 -0
- package/README.md +161 -0
- package/bin/srvgov-cli.js +35 -0
- package/package.json +35 -0
- package/scripts/install.js +230 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 JiangHe12
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# srvgov-cli
|
|
2
|
+
|
|
3
|
+
[English](README.md) | [中文](README_zh.md)
|
|
4
|
+
|
|
5
|
+
Governed remote server command execution for AI agents and operators. `srvgov`
|
|
6
|
+
combines fail-closed command classification, R0-R3 authorization, strict TOFU
|
|
7
|
+
SSH host-key pinning, output redaction, and structured audit records.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install -g srvgov-cli
|
|
13
|
+
# or
|
|
14
|
+
go install github.com/JiangHe12/srvgov-cli@latest
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
GitHub Releases provide Linux, macOS, and Windows binaries for amd64 and arm64.
|
|
18
|
+
The npm package downloads the matching release binary and verifies SHA-256
|
|
19
|
+
checksums by default.
|
|
20
|
+
|
|
21
|
+
## Quickstart
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
srvgov ctx set dev --server ssh://alice@example.com:22 --identity-file ~/.ssh/id_ed25519 -o json
|
|
25
|
+
srvgov ctx use dev -o json
|
|
26
|
+
srvgov exec --dry-run "uptime" -o json
|
|
27
|
+
srvgov exec "uptime" -o json
|
|
28
|
+
srvgov audit --limit 20 -o json
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Use `-o json` for automation and AI agents.
|
|
32
|
+
|
|
33
|
+
## Governance Model
|
|
34
|
+
|
|
35
|
+
| Risk | Meaning | Authorization |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| R0 | known read-only command | free to run, still audited |
|
|
38
|
+
| R1 | known benign change | `--reason` and `--yes` |
|
|
39
|
+
| R2 | unknown or elevated command | `--reason`, non-empty `--ticket`, and `--yes` |
|
|
40
|
+
| R3 | destructive, privileged, dynamic, or parser-uncertain command | `--reason`, `--ticket`, `--allow-destructive`, and `--yes` |
|
|
41
|
+
|
|
42
|
+
Protected contexts raise R1 to R2 and R2 to R3. AI agents must never auto-fill
|
|
43
|
+
`--ticket`, `--allow-destructive`, or high-risk `--yes`. Use `exec --dry-run`
|
|
44
|
+
to obtain the classifier's risk and required authorization; do not guess impact.
|
|
45
|
+
|
|
46
|
+
## Contexts
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
srvgov ctx set prod \
|
|
50
|
+
--server ssh://deploy@example.com:22 \
|
|
51
|
+
--identity-file ~/.ssh/id_ed25519 \
|
|
52
|
+
--auth-method private-key,agent,password \
|
|
53
|
+
--env production \
|
|
54
|
+
--protected \
|
|
55
|
+
-o json
|
|
56
|
+
|
|
57
|
+
srvgov ctx use prod -o json
|
|
58
|
+
srvgov ctx current -o json
|
|
59
|
+
srvgov ctx list -o json
|
|
60
|
+
srvgov ctx delete old-host -o json
|
|
61
|
+
srvgov ctx role set prod --target-operator alice --role writer -o json
|
|
62
|
+
srvgov ctx role list prod -o json
|
|
63
|
+
srvgov ctx export prod > prod.ctx.yaml
|
|
64
|
+
srvgov ctx import -f prod.ctx.yaml --rename prod-copy --yes -o json
|
|
65
|
+
srvgov ctx migrate-credentials --to encrypted-file --context prod -o json
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Context output never includes passwords, private-key contents, passphrases, or
|
|
69
|
+
identity-file paths.
|
|
70
|
+
|
|
71
|
+
Portable context export uses `srvgov.io/ctx-export/v1`. Literal password and
|
|
72
|
+
SSH identity passphrase values are redacted by default; credstore references are
|
|
73
|
+
preserved. `--include-credentials` is limited to plain-yaml contexts.
|
|
74
|
+
|
|
75
|
+
## Governed Execution
|
|
76
|
+
|
|
77
|
+
Preview without connecting or executing:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
srvgov exec --dry-run "touch /tmp/deploy-ready" -o json
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Execute according to the reported tier:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# R0
|
|
87
|
+
srvgov exec "systemctl status nginx" -o json
|
|
88
|
+
|
|
89
|
+
# R1
|
|
90
|
+
srvgov exec "touch /tmp/deploy-ready" \
|
|
91
|
+
--reason "mark deployment ready" --yes -o json
|
|
92
|
+
|
|
93
|
+
# R2
|
|
94
|
+
srvgov exec "custom-maintenance-command" \
|
|
95
|
+
--reason "scheduled maintenance" --ticket OPS-123 --yes -o json
|
|
96
|
+
|
|
97
|
+
# R3
|
|
98
|
+
srvgov exec "rm -rf /tmp/old-release" \
|
|
99
|
+
--reason "remove failed release" \
|
|
100
|
+
--ticket OPS-123 --allow-destructive --yes -o json
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Commands run without a PTY. stdout, stderr, command text, and audit fields are
|
|
104
|
+
redacted before output or persistence. A remote non-zero exit returns structured
|
|
105
|
+
output and process exit code 7 (`BACKEND_ERROR`).
|
|
106
|
+
|
|
107
|
+
## SSH Trust And Credentials
|
|
108
|
+
|
|
109
|
+
The first connection to an unknown `host:port` pins its SSH public key in
|
|
110
|
+
`~/.srvgov/known_hosts`. Later key mismatches, including an unpinned key type
|
|
111
|
+
for an already known address, are rejected. Host-key rotation requires manual
|
|
112
|
+
review and removal of the old pin; there is no insecure bypass.
|
|
113
|
+
|
|
114
|
+
Authentication order is private key, SSH agent, then password, subject to the
|
|
115
|
+
context's `--auth-method` preference. Passwords and key passphrases may use
|
|
116
|
+
opskit-core credential-store references. Credentials and raw SSH output are not
|
|
117
|
+
logged by the transport.
|
|
118
|
+
|
|
119
|
+
## Audit And Diagnostics
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
srvgov capabilities -o json
|
|
123
|
+
srvgov audit query -o json
|
|
124
|
+
srvgov audit query --type authorization.denied --status denied -o json
|
|
125
|
+
srvgov audit verify --strict -o json
|
|
126
|
+
srvgov audit prune --keep-last 20 -o json
|
|
127
|
+
srvgov doctor -o json
|
|
128
|
+
srvgov version -o json
|
|
129
|
+
srvgov --version
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Audit records live at `~/.srvgov/audit.log` by default and include effective
|
|
133
|
+
risk, authorization status, target, redacted command/output, exit code, and
|
|
134
|
+
error details.
|
|
135
|
+
|
|
136
|
+
`capabilities` reports the current command surface, `srvgov.io/context/v1`,
|
|
137
|
+
`srvgov.io/audit/v1`, R0-R3 authorization rules, `--allow-destructive`, JSONL
|
|
138
|
+
audit, RBAC reader/writer/admin, dry-run, strict TOFU, and redaction.
|
|
139
|
+
|
|
140
|
+
## AI Skill
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
srvgov install claude --skills
|
|
144
|
+
srvgov install codex --skills
|
|
145
|
+
srvgov install /custom/skills/path --skills
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Build
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
go build ./...
|
|
152
|
+
go test -count=1 ./...
|
|
153
|
+
gofmt -l main.go cmd internal
|
|
154
|
+
golangci-lint run --timeout=5m
|
|
155
|
+
go vet -tags=integration ./...
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Contributing, Security, License
|
|
159
|
+
|
|
160
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md), [SECURITY.md](SECURITY.md), and
|
|
161
|
+
[LICENSE](LICENSE).
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { spawn } = require('child_process');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
|
|
8
|
+
function getBinaryPath() {
|
|
9
|
+
const binaryName = os.platform() === 'win32' ? 'srvgov.exe' : 'srvgov';
|
|
10
|
+
return path.join(__dirname, binaryName);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function main() {
|
|
14
|
+
const binaryPath = getBinaryPath();
|
|
15
|
+
if (!fs.existsSync(binaryPath)) {
|
|
16
|
+
console.error('srvgov binary not found. Please reinstall:');
|
|
17
|
+
console.error(' npm install -g srvgov-cli');
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const child = spawn(binaryPath, process.argv.slice(2), { stdio: 'inherit' });
|
|
22
|
+
child.on('error', (err) => {
|
|
23
|
+
console.error('Failed to run srvgov:', err.message);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
});
|
|
26
|
+
child.on('exit', (code, signal) => {
|
|
27
|
+
if (signal) {
|
|
28
|
+
process.kill(process.pid, signal);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
process.exit(code == null ? 1 : code);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "srvgov-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Governed remote server command execution CLI for AI agents",
|
|
5
|
+
"bin": {
|
|
6
|
+
"srvgov": "bin/srvgov-cli.js",
|
|
7
|
+
"srvgov-cli": "bin/srvgov-cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"scripts/install.js",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"postinstall": "node scripts/install.js"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"ssh",
|
|
20
|
+
"server",
|
|
21
|
+
"cli",
|
|
22
|
+
"ai",
|
|
23
|
+
"governance"
|
|
24
|
+
],
|
|
25
|
+
"author": "JiangHe12",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/JiangHe12/srvgov-cli.git"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/JiangHe12/srvgov-cli",
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=14"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const https = require('https');
|
|
6
|
+
const http = require('http');
|
|
7
|
+
const crypto = require('crypto');
|
|
8
|
+
const { URL } = require('url');
|
|
9
|
+
|
|
10
|
+
const pkg = require('../package.json');
|
|
11
|
+
const VERSION = pkg.version;
|
|
12
|
+
const REPO = 'JiangHe12/srvgov-cli';
|
|
13
|
+
const TIMEOUT_MS = 30000;
|
|
14
|
+
|
|
15
|
+
const ALLOWED_REDIRECT_HOSTS = new Set([
|
|
16
|
+
'github.com',
|
|
17
|
+
'objects.githubusercontent.com',
|
|
18
|
+
'github-releases.githubusercontent.com',
|
|
19
|
+
'release-assets.githubusercontent.com',
|
|
20
|
+
'github.githubassets.com',
|
|
21
|
+
'cdn.jsdelivr.net',
|
|
22
|
+
'fastly.jsdelivr.net',
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
function isAllowedRedirectHost(urlStr) {
|
|
26
|
+
try {
|
|
27
|
+
const parsed = new URL(urlStr);
|
|
28
|
+
return ALLOWED_REDIRECT_HOSTS.has(parsed.hostname) || parsed.hostname.endsWith('.github.io');
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function applyMirror(canonicalUrl) {
|
|
35
|
+
const mirror = process.env.SRVGOV_CLI_DOWNLOAD_MIRROR;
|
|
36
|
+
if (!mirror) return canonicalUrl;
|
|
37
|
+
return mirror.replace(/\/+$/, '') + '/' + canonicalUrl;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function pickClient(url) {
|
|
41
|
+
return new URL(url).protocol === 'http:' ? http : https;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getPlatform() {
|
|
45
|
+
const platformMap = { win32: 'windows', darwin: 'darwin', linux: 'linux' };
|
|
46
|
+
const archMap = { x64: 'amd64', arm64: 'arm64' };
|
|
47
|
+
return {
|
|
48
|
+
os: platformMap[process.platform] || process.platform,
|
|
49
|
+
arch: archMap[process.arch] || process.arch,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getBinaryName() {
|
|
54
|
+
const { os, arch } = getPlatform();
|
|
55
|
+
const ext = os === 'windows' ? '.exe' : '';
|
|
56
|
+
return `srvgov-cli-${os}-${arch}${ext}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getDownloadUrl() {
|
|
60
|
+
const binary = getBinaryName();
|
|
61
|
+
return applyMirror(`https://github.com/${REPO}/releases/download/v${VERSION}/${binary}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function request(url, onResponse) {
|
|
65
|
+
const req = pickClient(url).get(url, onResponse);
|
|
66
|
+
req.setTimeout(TIMEOUT_MS, () => {
|
|
67
|
+
req.destroy(new Error(`Download timed out after ${TIMEOUT_MS / 1000}s`));
|
|
68
|
+
});
|
|
69
|
+
return req;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function redirectTarget(currentUrl, response) {
|
|
73
|
+
return new URL(response.headers.location, currentUrl).toString();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function download(url, dest, redirectCount = 0) {
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
if (redirectCount > 5) {
|
|
79
|
+
reject(new Error('Too many redirects'));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const req = request(url, (response) => {
|
|
84
|
+
if (response.statusCode === 301 || response.statusCode === 302 ||
|
|
85
|
+
response.statusCode === 307 || response.statusCode === 308) {
|
|
86
|
+
response.resume();
|
|
87
|
+
const target = redirectTarget(url, response);
|
|
88
|
+
if (!isAllowedRedirectHost(target)) {
|
|
89
|
+
reject(new Error(`Redirect to non-allowed host rejected: ${target}`));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
download(target, dest, redirectCount + 1).then(resolve).catch(reject);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (response.statusCode !== 200) {
|
|
96
|
+
response.resume();
|
|
97
|
+
reject(new Error(`Download failed: ${response.statusCode}`));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const file = fs.createWriteStream(dest);
|
|
102
|
+
response.pipe(file);
|
|
103
|
+
file.on('finish', () => file.close(resolve));
|
|
104
|
+
file.on('error', (err) => {
|
|
105
|
+
response.destroy();
|
|
106
|
+
fs.unlink(dest, () => {});
|
|
107
|
+
reject(err);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
req.on('error', (err) => {
|
|
111
|
+
fs.unlink(dest, () => {});
|
|
112
|
+
reject(err);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function getChecksumsUrl() {
|
|
118
|
+
return `https://github.com/${REPO}/releases/download/v${VERSION}/checksums.txt`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function downloadToString(url, redirectsLeft = 5) {
|
|
122
|
+
return new Promise((resolve, reject) => {
|
|
123
|
+
const req = request(url, (response) => {
|
|
124
|
+
if (response.statusCode === 301 || response.statusCode === 302 ||
|
|
125
|
+
response.statusCode === 307 || response.statusCode === 308) {
|
|
126
|
+
response.resume();
|
|
127
|
+
if (redirectsLeft <= 0) {
|
|
128
|
+
reject(new Error('Too many redirects'));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const target = redirectTarget(url, response);
|
|
132
|
+
if (!isAllowedRedirectHost(target)) {
|
|
133
|
+
reject(new Error(`Redirect to non-allowed host rejected: ${target}`));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
downloadToString(target, redirectsLeft - 1).then(resolve).catch(reject);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (response.statusCode !== 200) {
|
|
140
|
+
response.resume();
|
|
141
|
+
reject(new Error(`Download failed: ${response.statusCode}`));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
let data = '';
|
|
145
|
+
response.setEncoding('utf8');
|
|
146
|
+
response.on('data', (chunk) => { data += chunk; });
|
|
147
|
+
response.on('end', () => resolve(data));
|
|
148
|
+
});
|
|
149
|
+
req.on('error', reject);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function sha256File(filePath) {
|
|
154
|
+
return new Promise((resolve, reject) => {
|
|
155
|
+
const hash = crypto.createHash('sha256');
|
|
156
|
+
const stream = fs.createReadStream(filePath);
|
|
157
|
+
stream.on('data', (data) => hash.update(data));
|
|
158
|
+
stream.on('end', () => resolve(hash.digest('hex')));
|
|
159
|
+
stream.on('error', reject);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function parseChecksums(text) {
|
|
164
|
+
const checksums = {};
|
|
165
|
+
for (const line of text.split('\n')) {
|
|
166
|
+
const match = line.trim().match(/^([a-f0-9]{64})\s+\*?(.+)$/);
|
|
167
|
+
if (match) checksums[match[2]] = match[1];
|
|
168
|
+
}
|
|
169
|
+
return checksums;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function verifyDownloadedBinary(binaryPath, binaryName) {
|
|
173
|
+
if (process.env.SRVGOV_CLI_SKIP_VERIFY === '1') {
|
|
174
|
+
console.log('Verification skipped (SRVGOV_CLI_SKIP_VERIFY=1)');
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const checksumsUrl = getChecksumsUrl();
|
|
178
|
+
let checksums;
|
|
179
|
+
try {
|
|
180
|
+
checksums = parseChecksums(await downloadToString(checksumsUrl));
|
|
181
|
+
} catch (err) {
|
|
182
|
+
throw new Error(
|
|
183
|
+
`Could not fetch canonical checksums.txt from ${checksumsUrl}: ${err.message}. ` +
|
|
184
|
+
'Set SRVGOV_CLI_SKIP_VERIFY=1 to install without checksum verification.'
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
if (!checksums[binaryName]) {
|
|
188
|
+
throw new Error(
|
|
189
|
+
`No checksum found for ${binaryName}. ` +
|
|
190
|
+
'Set SRVGOV_CLI_SKIP_VERIFY=1 to install without checksum verification.'
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
const actual = await sha256File(binaryPath);
|
|
194
|
+
if (actual !== checksums[binaryName]) {
|
|
195
|
+
try { fs.unlinkSync(binaryPath); } catch {}
|
|
196
|
+
throw new Error(
|
|
197
|
+
`SHA-256 mismatch for ${binaryName}\n` +
|
|
198
|
+
` Expected: ${checksums[binaryName]}\n` +
|
|
199
|
+
` Actual: ${actual}\n` +
|
|
200
|
+
'The downloaded binary may be corrupted or tampered with.'
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
console.log('SHA-256 verification passed');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function main() {
|
|
207
|
+
const { os, arch } = getPlatform();
|
|
208
|
+
const binary = getBinaryName();
|
|
209
|
+
const url = getDownloadUrl();
|
|
210
|
+
const destDir = path.join(__dirname, '..', 'bin');
|
|
211
|
+
const dest = path.join(destDir, os === 'windows' ? 'srvgov.exe' : 'srvgov');
|
|
212
|
+
|
|
213
|
+
console.log(`Installing srvgov v${VERSION} for ${os}/${arch}...`);
|
|
214
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
await download(url, dest);
|
|
218
|
+
await verifyDownloadedBinary(dest, binary);
|
|
219
|
+
if (os !== 'windows') fs.chmodSync(dest, 0o755);
|
|
220
|
+
console.log('srvgov installed successfully!');
|
|
221
|
+
} catch (err) {
|
|
222
|
+
console.error('Failed to install srvgov:', err.message);
|
|
223
|
+
console.error('');
|
|
224
|
+
console.error('Please download manually from:');
|
|
225
|
+
console.error(` ${url}`);
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
main();
|