cfgov-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 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,56 @@
1
+ # cfgov-cli
2
+
3
+ > **Status: early development (P0).** Private, pre-release. The command surface and APIs may change without notice until `v0.1.0`.
4
+
5
+ Governed CLI for the configuration-governance domain. `cfgov-cli` aims to be the single entry point for config / rule / service-governance middleware (Nacos, Sentinel rules, Apollo, and more), built on the shared [`opskit-core`](https://github.com/JiangHe12/opskit-core) governance engine. It sits alongside `dbgov-cli` (database governance) and `srvgov-cli` (server governance).
6
+
7
+ P0 ships a Nacos-only governance kernel that proves the spine: a unified backend abstraction, the R0–R3 authorization ladder with protected-context escalation, fail-closed config-write risk classification, audit, and capabilities introspection.
8
+
9
+ ## Architecture
10
+
11
+ `cfgov-cli` is built on two orthogonal layers:
12
+
13
+ - **Storage backends** — a namespaced key/value + blob store with revisions and CAS. P0 implements **Nacos**; Apollo / Consul / etcd / Kubernetes are planned. Backend-specific addressing (e.g. Nacos `group/dataId`) is confined to the adapter.
14
+ - **Typed schemas** — config blobs today; Sentinel rules and other policy types (gateway routes, feature flags, …) layer on top of any backend in later phases.
15
+
16
+ A backend is bound to a context (`ctx set --backend nacos`), like `dbgov`'s engine selection; `--backend` overrides per command.
17
+
18
+ ## Governance Model
19
+
20
+ | Risk | Meaning | Authorization |
21
+ |---|---|---|
22
+ | R0 | reads and local inspection (still audited) | none |
23
+ | R1 | ordinary writes (`config push`) | `--yes` or interactive confirmation |
24
+ | R2 | destructive / elevated (`config delete`) | `--ticket` + `--yes` |
25
+ | R3 | protected destructive operations | `--ticket` + command-specific `--allow-*` + `--yes` |
26
+
27
+ Protected contexts raise every operation one tier (`config delete` in a protected context becomes R3 and requires `--allow-production-config-delete`). Impact and blast radius come from the CLI's own `--dry-run` / `--diff`, never from a model guess. **AI agents and automation must never invent `--ticket`, `--allow-*`, or a high-risk `--yes`** — surface missing authorization to the operator.
28
+
29
+ ## Commands (P0)
30
+
31
+ ```
32
+ cfgov ctx set <name> --backend nacos --server <url> [--username <u>] [--namespace <ns>] [--protected]
33
+ cfgov ctx use|list|current
34
+ cfgov config get --key <dataId|group/dataId>
35
+ cfgov config push --key <key> --file <path> [--type text|properties|json|yaml] [--dry-run]
36
+ cfgov config delete --key <key> [--ticket <t> --yes] [--allow-production-config-delete]
37
+ cfgov capabilities
38
+ cfgov audit query|verify
39
+ cfgov version
40
+ ```
41
+
42
+ Use `-o json` for automation and AI agents. Credentials are stored via the `opskit-core` credential store; prefer the `NACOS_PASSWORD` env var over `--password`.
43
+
44
+ ## Build & Verify
45
+
46
+ ```bash
47
+ go build ./...
48
+ go test -count=1 ./...
49
+ gofmt -l main.go cmd internal # must print nothing
50
+ golangci-lint run --timeout=5m
51
+ go vet -tags=integration ./...
52
+ ```
53
+
54
+ ## License
55
+
56
+ [MIT](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' ? 'cfgov.exe' : 'cfgov';
10
+ return path.join(__dirname, binaryName);
11
+ }
12
+
13
+ function main() {
14
+ const binaryPath = getBinaryPath();
15
+ if (!fs.existsSync(binaryPath)) {
16
+ console.error('cfgov binary not found. Please reinstall:');
17
+ console.error(' npm install -g cfgov-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 cfgov:', 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,37 @@
1
+ {
2
+ "name": "cfgov-cli",
3
+ "version": "0.1.0",
4
+ "description": "Governed configuration and Sentinel-rule operations CLI for AI agents (Nacos and Apollo)",
5
+ "bin": {
6
+ "cfgov": "bin/cfgov-cli.js",
7
+ "cfgov-cli": "bin/cfgov-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
+ "config",
20
+ "nacos",
21
+ "apollo",
22
+ "sentinel",
23
+ "governance",
24
+ "cli",
25
+ "ai"
26
+ ],
27
+ "author": "JiangHe12",
28
+ "license": "MIT",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/JiangHe12/cfgov-cli.git"
32
+ },
33
+ "homepage": "https://github.com/JiangHe12/cfgov-cli",
34
+ "engines": {
35
+ "node": ">=14"
36
+ }
37
+ }
@@ -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/cfgov-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.CFGOV_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 `cfgov-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.CFGOV_CLI_SKIP_VERIFY === '1') {
174
+ console.log('Verification skipped (CFGOV_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 CFGOV_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 CFGOV_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' ? 'cfgov.exe' : 'cfgov');
212
+
213
+ console.log(`Installing cfgov 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('cfgov installed successfully!');
221
+ } catch (err) {
222
+ console.error('Failed to install cfgov:', 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();