dbgov-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 +109 -0
- package/bin/dbgov-cli.js +40 -0
- package/package.json +34 -0
- package/scripts/install.js +241 -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,109 @@
|
|
|
1
|
+
# dbgov-cli
|
|
2
|
+
|
|
3
|
+
[English](README.md) | [中文](README_zh.md)
|
|
4
|
+
|
|
5
|
+
Governed MySQL operations CLI for AI agents and operators. It provides read queries, schema planning and apply, governed DML, GitOps import/reconcile/rollback, audit, RBAC, and local credential management.
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
`dbgov` is built around a governance spine: connect to MySQL, classify risk, require explicit authorization for writes, execute through backend interfaces, and write structured audit events. It is MySQL-only today; PostgreSQL is planned but not enabled unless capabilities report it.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install -g dbgov-cli
|
|
15
|
+
# or
|
|
16
|
+
go install github.com/JiangHe12/dbgov-cli@latest
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Release binaries are available from GitHub Releases. npm installs download the matching platform binary.
|
|
20
|
+
|
|
21
|
+
## Quickstart
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
DBGOV_PASSWORD='<password>' dbgov ctx set local --engine mysql --host 127.0.0.1 --port 3306 --database app --username appuser -o json
|
|
25
|
+
dbgov ctx use local -o json
|
|
26
|
+
dbgov query --sql "SELECT 1" -o json
|
|
27
|
+
dbgov explain --sql "SELECT * FROM users WHERE id = 1" -o json
|
|
28
|
+
dbgov schema list -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 | read-only operations and local inspection | no approval required, still audited |
|
|
38
|
+
| R1 | incremental writes such as add column, small WHERE DML, incremental import | `--yes` or interactive confirmation |
|
|
39
|
+
| R2 | large-impact WHERE DML or protected-context R1 | non-empty `--ticket` plus `--yes` |
|
|
40
|
+
| R3 | destructive schema, no-WHERE UPDATE/DELETE, prune, destructive rollback | `--ticket`, required `--allow-*`, and `--yes` |
|
|
41
|
+
|
|
42
|
+
Allow flags are precise: schema drop/modify uses `--allow-destructive`, no-WHERE DML uses `--allow-no-where`, table prune uses `--allow-production-prune`. Rollback has an R2 floor and may require one or both destructive/prune allow flags. If a context defines `ticketPattern`, tickets must match it; by default no pattern is enforced.
|
|
43
|
+
|
|
44
|
+
RBAC applies to writes: `reader` is R0, `writer` is up to R2, and `admin` is up to R3. AI agents and automation must not auto-fill `--ticket`, `--allow-*`, or high-risk `--yes`. Impact must come from `dbgov explain`, `schema plan`, or `--dry-run`, never model guesses.
|
|
45
|
+
|
|
46
|
+
All operations, including denied and failed attempts, append to `~/.dbgov/audit.log`. Use `audit query`, `audit verify`, and `audit prune` to inspect, validate, and clean rotated logs.
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
dbgov version -o json
|
|
52
|
+
dbgov capabilities -o json
|
|
53
|
+
dbgov doctor config -o json
|
|
54
|
+
dbgov ctx list -o json
|
|
55
|
+
dbgov ctx export local > local.ctx.yaml
|
|
56
|
+
dbgov ctx import -f local.ctx.yaml --rename local-copy -o json
|
|
57
|
+
dbgov query --sql "SELECT * FROM users" -o json
|
|
58
|
+
dbgov explain --sql "SELECT * FROM users WHERE active = 1" -o json
|
|
59
|
+
dbgov schema dump --dir ./schema -o json
|
|
60
|
+
dbgov schema plan -f desired.sql -o json
|
|
61
|
+
dbgov schema apply -f desired.sql --dry-run -o json
|
|
62
|
+
dbgov data exec --sql "UPDATE users SET active=0 WHERE id=1" --dry-run -o json
|
|
63
|
+
dbgov export --dir ./schema -o json
|
|
64
|
+
dbgov import ./schema --dry-run -o json
|
|
65
|
+
dbgov reconcile ./schema --dry-run -o json
|
|
66
|
+
dbgov rollback list -o json
|
|
67
|
+
dbgov audit query --since 24h -o json
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Configuration and Contexts
|
|
71
|
+
|
|
72
|
+
Contexts live under `~/.dbgov`. Use `ctx set`, `ctx use`, `ctx current`, and `ctx list` to manage them. Credentials may be literal during setup, read from `DBGOV_PASSWORD`, or migrated to secure backends:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
dbgov ctx export prod > prod.ctx.yaml
|
|
76
|
+
dbgov ctx import -f prod.ctx.yaml --rename prod-copy -o json
|
|
77
|
+
dbgov ctx migrate-credentials --to encrypted-file -o json
|
|
78
|
+
dbgov ctx role set prod --target-operator alice --role writer -o json
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Portable context export redacts passwords by default. `--include-credentials` is only allowed for `plain-yaml` or empty credential backends; secure backend credentials must be shared out of band.
|
|
82
|
+
|
|
83
|
+
Set `DBGOV_OPERATOR` in CI to make audit and RBAC identity stable.
|
|
84
|
+
|
|
85
|
+
## Rollback and Snapshots
|
|
86
|
+
|
|
87
|
+
Schema mutations capture a pre-change DDL snapshot before execution. `rollback --to <snapshot>` restores structure only; MySQL data dropped by table or column deletion is not recovered. dbgov prints this warning during rollback planning and execution.
|
|
88
|
+
|
|
89
|
+
## Build from Source
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
go build ./...
|
|
93
|
+
go test -count=1 ./...
|
|
94
|
+
gofmt -l main.go cmd internal
|
|
95
|
+
golangci-lint run --timeout=5m
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
MySQL integration tests are opt-in with `DBGOV_TEST_MYSQL_DSN`.
|
|
99
|
+
|
|
100
|
+
## AI Skill
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
dbgov install claude --skills
|
|
104
|
+
dbgov install codex --skills
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Contributing, Security, License
|
|
108
|
+
|
|
109
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md), [SECURITY.md](SECURITY.md), and [LICENSE](LICENSE).
|
package/bin/dbgov-cli.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
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 platform = os.platform();
|
|
10
|
+
const binDir = path.join(__dirname);
|
|
11
|
+
const binaryName = platform === 'win32' ? 'dbgov.exe' : 'dbgov';
|
|
12
|
+
return path.join(binDir, binaryName);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function main() {
|
|
16
|
+
const binaryPath = getBinaryPath();
|
|
17
|
+
|
|
18
|
+
if (!fs.existsSync(binaryPath)) {
|
|
19
|
+
console.error('dbgov binary not found. Please reinstall:');
|
|
20
|
+
console.error(' npm install -g dbgov-cli');
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const args = process.argv.slice(2);
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const result = spawn(binaryPath, args, {
|
|
28
|
+
stdio: 'inherit'
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
result.on('exit', (code) => {
|
|
32
|
+
process.exit(code || 0);
|
|
33
|
+
});
|
|
34
|
+
} catch (err) {
|
|
35
|
+
console.error('Failed to run dbgov:', err.message);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dbgov-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Governed MySQL operations CLI for AI agents",
|
|
5
|
+
"bin": {
|
|
6
|
+
"dbgov": "bin/dbgov-cli.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin/",
|
|
10
|
+
"scripts/install.js",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"postinstall": "node scripts/install.js"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"database",
|
|
19
|
+
"mysql",
|
|
20
|
+
"cli",
|
|
21
|
+
"ai",
|
|
22
|
+
"governance"
|
|
23
|
+
],
|
|
24
|
+
"author": "JiangHe12",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/JiangHe12/dbgov-cli.git"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/JiangHe12/dbgov-cli",
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=14"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
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
|
+
const { createWriteStream } = require('fs');
|
|
10
|
+
|
|
11
|
+
const pkg = require('../package.json');
|
|
12
|
+
const VERSION = pkg.version;
|
|
13
|
+
const REPO = 'JiangHe12/dbgov-cli';
|
|
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
|
+
if (ALLOWED_REDIRECT_HOSTS.has(parsed.hostname)) return true;
|
|
29
|
+
if (parsed.hostname.endsWith('.github.io')) return true;
|
|
30
|
+
return false;
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function applyMirror(canonicalUrl) {
|
|
37
|
+
const mirror = process.env.DBGOV_CLI_DOWNLOAD_MIRROR;
|
|
38
|
+
if (!mirror) return canonicalUrl;
|
|
39
|
+
return mirror.replace(/\/+$/, '') + '/' + canonicalUrl;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function pickClient(url) {
|
|
43
|
+
return new URL(url).protocol === 'http:' ? http : https;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getPlatform() {
|
|
47
|
+
const platform = process.platform;
|
|
48
|
+
const arch = process.arch;
|
|
49
|
+
|
|
50
|
+
const platformMap = {
|
|
51
|
+
'win32': 'windows',
|
|
52
|
+
'darwin': 'darwin',
|
|
53
|
+
'linux': 'linux'
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const archMap = {
|
|
57
|
+
'x64': 'amd64',
|
|
58
|
+
'arm64': 'arm64'
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
os: platformMap[platform] || platform,
|
|
63
|
+
arch: archMap[arch] || arch
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getBinaryName() {
|
|
68
|
+
const { os, arch } = getPlatform();
|
|
69
|
+
const ext = os === 'windows' ? '.exe' : '';
|
|
70
|
+
return `dbgov-cli-${os}-${arch}${ext}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getDownloadUrl() {
|
|
74
|
+
const binary = getBinaryName();
|
|
75
|
+
return applyMirror(`https://github.com/${REPO}/releases/download/v${VERSION}/${binary}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function download(url, dest, redirectCount = 0) {
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
if (redirectCount > 5) {
|
|
81
|
+
reject(new Error('Too many redirects'));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const file = createWriteStream(dest);
|
|
85
|
+
|
|
86
|
+
pickClient(url).get(url, { timeout: 30000 }, (response) => {
|
|
87
|
+
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
88
|
+
const target = response.headers.location;
|
|
89
|
+
if (!isAllowedRedirectHost(target)) {
|
|
90
|
+
reject(new Error(`Redirect to non-allowed host rejected: ${target}`));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
download(target, dest, redirectCount + 1).then(resolve).catch(reject);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (response.statusCode !== 200) {
|
|
98
|
+
reject(new Error(`Download failed: ${response.statusCode}`));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
response.pipe(file);
|
|
103
|
+
file.on('finish', () => {
|
|
104
|
+
file.close();
|
|
105
|
+
resolve();
|
|
106
|
+
});
|
|
107
|
+
}).on('error', (err) => {
|
|
108
|
+
fs.unlink(dest, () => {});
|
|
109
|
+
reject(err);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function getChecksumsUrl() {
|
|
115
|
+
return `https://github.com/${REPO}/releases/download/v${VERSION}/checksums.txt`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function downloadToString(url, maxRedirects = 5) {
|
|
119
|
+
return new Promise((resolve, reject) => {
|
|
120
|
+
pickClient(url).get(url, { timeout: 30000 }, (response) => {
|
|
121
|
+
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
122
|
+
if (maxRedirects <= 0) {
|
|
123
|
+
reject(new Error('Too many redirects'));
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
downloadToString(response.headers.location, maxRedirects - 1).then(resolve).catch(reject);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (response.statusCode !== 200) {
|
|
131
|
+
reject(new Error(`Download failed: ${response.statusCode}`));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let data = '';
|
|
136
|
+
response.on('data', (chunk) => {
|
|
137
|
+
data += chunk;
|
|
138
|
+
});
|
|
139
|
+
response.on('end', () => {
|
|
140
|
+
resolve(data);
|
|
141
|
+
});
|
|
142
|
+
}).on('error', reject);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function sha256File(filePath) {
|
|
147
|
+
return new Promise((resolve, reject) => {
|
|
148
|
+
const hash = crypto.createHash('sha256');
|
|
149
|
+
const stream = fs.createReadStream(filePath);
|
|
150
|
+
stream.on('data', (data) => hash.update(data));
|
|
151
|
+
stream.on('end', () => resolve(hash.digest('hex')));
|
|
152
|
+
stream.on('error', reject);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function parseChecksums(text) {
|
|
157
|
+
const checksums = {};
|
|
158
|
+
for (const line of text.split('\n')) {
|
|
159
|
+
const trimmed = line.trim();
|
|
160
|
+
if (!trimmed) continue;
|
|
161
|
+
const match = trimmed.match(/^([a-f0-9]{64})\s+\*?(.+)$/);
|
|
162
|
+
if (match) {
|
|
163
|
+
checksums[match[2]] = match[1];
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return checksums;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function verifyDownloadedBinary(binaryPath, binaryName) {
|
|
170
|
+
if (process.env.DBGOV_CLI_SKIP_VERIFY === '1') {
|
|
171
|
+
console.log('Verification skipped (DBGOV_CLI_SKIP_VERIFY=1)');
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const checksumsUrl = getChecksumsUrl();
|
|
176
|
+
let checksums = {};
|
|
177
|
+
try {
|
|
178
|
+
const checksumsText = await downloadToString(checksumsUrl);
|
|
179
|
+
checksums = parseChecksums(checksumsText);
|
|
180
|
+
} catch (err) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
`Could not fetch canonical checksums.txt from ${checksumsUrl}: ${err.message}. ` +
|
|
183
|
+
'Set DBGOV_CLI_SKIP_VERIFY=1 to install without checksum verification.'
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (!checksums[binaryName]) {
|
|
188
|
+
throw new Error(
|
|
189
|
+
`No checksum found for ${binaryName}. ` +
|
|
190
|
+
'Set DBGOV_CLI_SKIP_VERIFY=1 to install without checksum verification.'
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const expected = checksums[binaryName];
|
|
195
|
+
const actual = await sha256File(binaryPath);
|
|
196
|
+
|
|
197
|
+
if (actual !== expected) {
|
|
198
|
+
try { fs.unlinkSync(binaryPath); } catch (e) {}
|
|
199
|
+
throw new Error(
|
|
200
|
+
`SHA-256 mismatch for ${binaryName}\n` +
|
|
201
|
+
` Expected: ${expected}\n` +
|
|
202
|
+
` Actual: ${actual}\n` +
|
|
203
|
+
`The downloaded binary may be corrupted or tampered with.`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
console.log('SHA-256 verification passed');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function main() {
|
|
211
|
+
const { os, arch } = getPlatform();
|
|
212
|
+
const binary = getBinaryName();
|
|
213
|
+
const url = getDownloadUrl();
|
|
214
|
+
const destDir = path.join(__dirname, '..', 'bin');
|
|
215
|
+
const dest = path.join(destDir, os === 'windows' ? 'dbgov.exe' : 'dbgov');
|
|
216
|
+
|
|
217
|
+
console.log(`Installing dbgov v${VERSION} for ${os}/${arch}...`);
|
|
218
|
+
|
|
219
|
+
if (!fs.existsSync(destDir)) {
|
|
220
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
await download(url, dest);
|
|
225
|
+
await verifyDownloadedBinary(dest, binary);
|
|
226
|
+
|
|
227
|
+
if (os !== 'windows') {
|
|
228
|
+
fs.chmodSync(dest, 0o755);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
console.log('dbgov installed successfully!');
|
|
232
|
+
} catch (err) {
|
|
233
|
+
console.error('Failed to install dbgov:', err.message);
|
|
234
|
+
console.error('');
|
|
235
|
+
console.error('Please download manually from:');
|
|
236
|
+
console.error(` ${url}`);
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
main();
|