mqgov-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 +265 -0
- package/bin/mqgov-cli.js +35 -0
- package/package.json +38 -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,265 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# mqgov-cli
|
|
4
|
+
|
|
5
|
+
**Governed message-broker operations for humans _and_ AI agents.**
|
|
6
|
+
|
|
7
|
+
One safe command line for **Kafka**, **RabbitMQ**, **Pulsar**, and **RocketMQ** — list, describe, peek, produce, reset offsets, purge, and delete topics without ever fat-fingering production or silently draining a queue.
|
|
8
|
+
|
|
9
|
+
[](https://www.npmjs.com/package/mqgov-cli)
|
|
10
|
+
[](https://github.com/JiangHe12/mqgov-cli/actions/workflows/ci.yml)
|
|
11
|
+
[](LICENSE)
|
|
12
|
+
[](#-trust--verification)
|
|
13
|
+
|
|
14
|
+
[English](README.md) · [简体中文](README_zh.md)
|
|
15
|
+
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 🧭 What is this? (read me first)
|
|
21
|
+
|
|
22
|
+
Message brokers — **Kafka**, **RabbitMQ**, **Pulsar**, **RocketMQ** — are the backbone of event-driven systems. The operations against them are deceptively dangerous: **resetting a consumer group's offset** can trigger a reprocessing storm or silently skip unprocessed messages; **purging a topic** or **deleting it** destroys data; **producing to an internal topic** like `__consumer_offsets` can corrupt cluster state. These mistakes are often *silent* — you don't notice until hours later.
|
|
23
|
+
|
|
24
|
+
**mqgov-cli puts guardrails around every one of those operations.** Think of it as a careful assistant that:
|
|
25
|
+
|
|
26
|
+
- 🔎 **Shows you the blast radius first** — `--dry-run` / `--plan` print the exact per-partition impact (how many messages an offset reset will replay or skip) before anything happens.
|
|
27
|
+
- 🛡️ **Refuses to do something dangerous without explicit sign-off** — risky commands need a confirmation flag, a change ticket, and an explicit `--allow-*` for the operation.
|
|
28
|
+
- 👀 **Peeks without consuming** — inspecting messages never advances a consumer's position or drains a queue.
|
|
29
|
+
- 📜 **Records everything in a tamper-evident audit log** — sha256 fingerprints and counts only, **never your message bodies**.
|
|
30
|
+
- 🤖 **Is safe to hand to an AI agent** — the agent can read and preview freely, but **cannot** invent the human approvals required for dangerous actions.
|
|
31
|
+
|
|
32
|
+
It's built on the shared [`opskit-core`](https://github.com/JiangHe12/opskit-core) governance engine and is part of the **opskit** family of governed CLIs for AI agents.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## ✨ Features
|
|
37
|
+
|
|
38
|
+
| | |
|
|
39
|
+
|---|---|
|
|
40
|
+
| 📨 **Four brokers** | **Kafka** (franz-go), **RabbitMQ** (AMQP + management API), **Pulsar** (client + admin REST), **RocketMQ** (rocketmq-client-go/v2). One backend-agnostic governance model; pick per context or override per command. |
|
|
41
|
+
| 🧱 **topic / group / message** | topics: list · describe · create · alter · delete · purge. consumer groups: list · lag · reset-offset. messages: non-destructive peek · produce. |
|
|
42
|
+
| 🔐 **R0–R3 governance** | every operation is risk-classified by the fail-closed `mqclass` engine; protected contexts and internal/system topics escalate one tier; AI callers can never self-authorize. |
|
|
43
|
+
| 🎯 **Real blast-radius preview** | `reset-offset --dry-run` and `purge --dry-run` compute the actual per-partition message delta from the live broker — no guessing. The preview is read-only and never mutates. |
|
|
44
|
+
| 👀 **Non-destructive peek** | inspect messages as fingerprints without consuming them or moving any cursor (Pulsar Reader, RabbitMQ get+requeue). Where a broker can't guarantee this, peek fails closed rather than silently consuming. |
|
|
45
|
+
| 🧭 **Honest capabilities** | brokers differ — mqgov reports what each one actually supports (`capabilities -o json`) and **fails closed with `NOT_IMPLEMENTED`** for the rest, never faking it. |
|
|
46
|
+
| 📜 **Tamper-evident audit** | hash-chained log of every action (sha256 fingerprints + counts, **no message bodies/keys/headers**); `audit verify` detects tampering. |
|
|
47
|
+
| 🩺 **Ops & DX** | backend-bound `ctx` contexts with credstore-backed secrets, `doctor` diagnostics, shell `completion`, OpenTelemetry traces/metrics, JSON output everywhere. |
|
|
48
|
+
| 🔏 **Trusted supply chain** | binaries are **cosign-signed**, the npm package ships with **provenance**, and the installer verifies a **SHA-256** checksum. |
|
|
49
|
+
|
|
50
|
+
### Per-backend capability matrix
|
|
51
|
+
|
|
52
|
+
| | Kafka | Pulsar | RabbitMQ | RocketMQ |
|
|
53
|
+
|---|:---:|:---:|:---:|:---:|
|
|
54
|
+
| topic list / describe / create / delete | ✅ | ✅ | ✅ | ✅ |
|
|
55
|
+
| produce | ✅ | ✅ | ✅ | ✅ |
|
|
56
|
+
| **non-destructive peek** | ✅ | ✅ (Reader) | ✅ (get+requeue) | ❌ `NOT_IMPLEMENTED`¹ |
|
|
57
|
+
| **offset lag / reset** | ✅ | ✅ (cursor) | ❌ (no offsets) | ❌ |
|
|
58
|
+
| alter partitions | ✅ | ✅ | ❌ | ❌ |
|
|
59
|
+
| purge | ✅ | ✅ | ✅ | ❌ |
|
|
60
|
+
|
|
61
|
+
¹ RocketMQ's Go v2 `PullConsumer` enters the consumer-group lifecycle and commits offsets, so it cannot guarantee a non-destructive peek — mqgov fails closed instead of silently advancing offsets. Unsupported operations always return `NOT_IMPLEMENTED` (exit 12), never a fake success.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 📦 Install
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
npm install -g mqgov-cli
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
This installs a tiny launcher; on first run it downloads the right pre-built binary for your OS/arch from the signed [GitHub Release](https://github.com/JiangHe12/mqgov-cli/releases) and **verifies its SHA-256** before use. Requires Node.js ≥ 14 for the installer (the CLI itself is a self-contained Go binary).
|
|
72
|
+
|
|
73
|
+
<details>
|
|
74
|
+
<summary>Other ways to install</summary>
|
|
75
|
+
|
|
76
|
+
- **Direct download** — grab the binary for your platform from the [Releases page](https://github.com/JiangHe12/mqgov-cli/releases), verify it against `checksums.txt` (cosign-signed), put it on your `PATH`, and rename it to `mqgov`.
|
|
77
|
+
- **From source** — `go install github.com/JiangHe12/mqgov-cli@latest` (Go 1.26+).
|
|
78
|
+
- **Mirror / air-gapped** — set `MQGOV_CLI_DOWNLOAD_MIRROR=<base-url>` to fetch the binary from your own mirror.
|
|
79
|
+
|
|
80
|
+
Verify the install:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
mqgov version
|
|
84
|
+
mqgov doctor # checks context, backend reachability, and audit-log writability
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
</details>
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## 🚀 Quick start (60 seconds)
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# 1. Point mqgov at your broker (stored as a reusable "context")
|
|
95
|
+
mqgov ctx set dev --backend kafka --brokers 127.0.0.1:9092
|
|
96
|
+
mqgov ctx use dev
|
|
97
|
+
mqgov ctx test # ping the broker through the context
|
|
98
|
+
|
|
99
|
+
# 2. Read something — reads are always free (R0), no flags needed
|
|
100
|
+
mqgov topic list -o json
|
|
101
|
+
mqgov topic describe orders -o json
|
|
102
|
+
mqgov message peek orders --count 5 -o json # fingerprints only, nothing consumed
|
|
103
|
+
|
|
104
|
+
# 3. Preview the blast radius of a dangerous op — nothing is changed yet
|
|
105
|
+
mqgov group reset-offset billing orders --to latest --dry-run -o json # shows per-partition delta
|
|
106
|
+
|
|
107
|
+
# 4. Apply it — an R3 op needs your confirmation, a ticket, AND the allow flag
|
|
108
|
+
mqgov group reset-offset billing orders --to latest --yes --ticket OPS-123 --allow-offset-reset
|
|
109
|
+
|
|
110
|
+
# 5. See what happened
|
|
111
|
+
mqgov audit query --since 1h -o json
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
> 💡 **Tip:** mark production contexts with `--protected` when you create them. mqgov then raises the bar for every dangerous operation in that context automatically.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## 🔐 The governance model (the important part)
|
|
119
|
+
|
|
120
|
+
Every command is sorted into one of four **risk tiers** by the fail-closed `mqclass` classifier. The higher the tier, the more explicit human sign-off it needs:
|
|
121
|
+
|
|
122
|
+
| Tier | What it covers | What you must provide |
|
|
123
|
+
|:---:|---|---|
|
|
124
|
+
| **R0** | Reads & previews (`topic list/describe`, `group list/lag`, `message peek`, `*-dry-run`, `audit query/verify`, `doctor`) | Nothing — but it's still audited |
|
|
125
|
+
| **R1** | Ordinary writes (`message produce`, `topic create`) | `--yes` (or an interactive confirmation) |
|
|
126
|
+
| **R2** | Elevated mutations (`topic alter`, `group create/delete`, produce to a **protected** topic) | `--yes` **and** a non-empty `--ticket` |
|
|
127
|
+
| **R3** | Destructive / irreversible (`group reset-offset`, `topic purge`, `topic delete`, produce to an **internal/system** topic) | The above **plus** the exact `--allow-*` flag |
|
|
128
|
+
|
|
129
|
+
The R3 allow flags: `--allow-offset-reset`, `--allow-topic-purge`, `--allow-topic-delete`, `--allow-internal-produce`.
|
|
130
|
+
|
|
131
|
+
**Protected contexts, protected topics, and internal/system topics raise the tier by one.** For example, producing to `__consumer_offsets` is treated as a destructive R3 operation and needs `--allow-internal-produce`.
|
|
132
|
+
|
|
133
|
+
Three rules keep this safe — especially for automation:
|
|
134
|
+
|
|
135
|
+
1. **Blast radius comes from the tool, not a guess.** Use `--dry-run` / `--plan` to see the exact per-partition impact. Never estimate it by reasoning.
|
|
136
|
+
2. **`mqclass` is fail-closed and structure-aware.** All offset changes, purge, and delete are pinned R3; wildcard/glob targets escalate; an unknown operation fails closed to the highest tier — it never falls to R0.
|
|
137
|
+
3. **🤖 AI agents must never invent `--ticket`, `--allow-*`, or a high-risk `--yes`.** Those are *human* authorization inputs. An agent should surface "this needs approval X" to its operator and stop.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## 📚 Command reference
|
|
142
|
+
|
|
143
|
+
`mqgov <noun> <verb> [flags]`. Add `-o json` for machine-readable output, `--help` on any command for its full flag set, and `mqgov capabilities -o json` to ask the bound backend what it actually supports.
|
|
144
|
+
|
|
145
|
+
<details open>
|
|
146
|
+
<summary><b>topic</b> — topics / queues</summary>
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
# Read (R0)
|
|
150
|
+
mqgov topic list [--pattern <name|glob>] -o json
|
|
151
|
+
mqgov topic describe <topic> -o json
|
|
152
|
+
|
|
153
|
+
# Write
|
|
154
|
+
mqgov topic create <topic> [--partitions N] --yes # R1 (R2 if protected)
|
|
155
|
+
mqgov topic alter <topic> --partitions N --yes --ticket <t> # R2 (Kafka/Pulsar)
|
|
156
|
+
mqgov topic purge <topic> [--dlq] --dry-run # R0 preview
|
|
157
|
+
mqgov topic purge <topic> [--dlq] --yes --ticket <t> --allow-topic-purge # R3
|
|
158
|
+
mqgov topic delete <topic> --yes --ticket <t> --allow-topic-delete # R3
|
|
159
|
+
```
|
|
160
|
+
</details>
|
|
161
|
+
|
|
162
|
+
<details>
|
|
163
|
+
<summary><b>group</b> — consumer groups / subscriptions</summary>
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
# Read (R0)
|
|
167
|
+
mqgov group list [--pattern <name>] -o json
|
|
168
|
+
mqgov group lag <group> <topic> -o json
|
|
169
|
+
|
|
170
|
+
# Reset a consumer group's position
|
|
171
|
+
mqgov group reset-offset <group> <topic> --to <target> --dry-run -o json # R0 preview (real per-partition delta)
|
|
172
|
+
mqgov group reset-offset <group> <topic> --to <target> --yes --ticket <t> --allow-offset-reset # R3
|
|
173
|
+
|
|
174
|
+
# --to: earliest | latest | offset:N | datetime:<RFC3339> | shift:±N
|
|
175
|
+
# (offset:N / shift:N are Kafka-only; unsupported targets/backends return a clear error)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Offsets are a Kafka and Pulsar concept. On RabbitMQ and RocketMQ, `group lag` / `reset-offset` fail closed with `NOT_IMPLEMENTED`.
|
|
179
|
+
</details>
|
|
180
|
+
|
|
181
|
+
<details>
|
|
182
|
+
<summary><b>message</b> — peek & produce</summary>
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
mqgov message peek <topic> [--partition N] [--offset N] [--count N] -o json # R0, non-destructive, fingerprints only
|
|
186
|
+
mqgov message produce <topic> [--key <k>] [--body <text>] --yes # R1 (R3 + --allow-internal-produce for internal topics)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
`peek` never consumes a message or moves a cursor, and returns only sha256 fingerprints (`keySha256`, `bodySha256`, size) — never the body. On RocketMQ, `peek` fails closed (`NOT_IMPLEMENTED`).
|
|
190
|
+
</details>
|
|
191
|
+
|
|
192
|
+
<details>
|
|
193
|
+
<summary><b>ctx</b>, <b>audit</b>, <b>doctor</b> & friends</summary>
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
# Backend-bound contexts (credentials go through credstore, never plaintext)
|
|
197
|
+
mqgov ctx set <name> --backend kafka --brokers <h:p,h:p> [--sasl-mechanism PLAIN] [--tls --ca-cert <f>] [--protected]
|
|
198
|
+
mqgov ctx set <name> --backend rabbitmq (--amqp-url <url> | --host <h> --port <p> --vhost </>) --management-url <url>
|
|
199
|
+
mqgov ctx set <name> --backend pulsar --service-url pulsar://<h:p> --admin-url http://<h:p> [--tenant public] [--pulsar-namespace default]
|
|
200
|
+
mqgov ctx set <name> --backend rocketmq --nameservers <h:p,h:p> [--broker-addr <h:p>]
|
|
201
|
+
mqgov ctx use|list|current|delete|test
|
|
202
|
+
# secrets: --password <pw|token|secretKey> --credential-backend <encrypted-file|keychain|...> (a non-plain backend is required)
|
|
203
|
+
|
|
204
|
+
# Audit (tamper-evident, fingerprint-only)
|
|
205
|
+
mqgov audit query [--since 24h] [--type <t>] [--operator <o>] [--status <s>] [--limit 100] -o json
|
|
206
|
+
mqgov audit verify [--strict] -o json
|
|
207
|
+
|
|
208
|
+
# Diagnostics & ecosystem
|
|
209
|
+
mqgov doctor -o json # read-only health check (redacted output)
|
|
210
|
+
mqgov capabilities -o json # what the bound backend supports
|
|
211
|
+
mqgov completion bash|zsh|fish|powershell
|
|
212
|
+
mqgov install <agent> --skills # install the mqgov AI skill into an agent (claude, codex, …) or a custom path
|
|
213
|
+
mqgov version
|
|
214
|
+
```
|
|
215
|
+
</details>
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## 🤖 For AI agents
|
|
220
|
+
|
|
221
|
+
mqgov-cli is designed to be driven by autonomous agents safely:
|
|
222
|
+
|
|
223
|
+
- Run `mqgov capabilities -o json` first to discover what the bound backend supports — brokers differ, don't assume (e.g. RabbitMQ/RocketMQ have no offsets; RocketMQ has no peek).
|
|
224
|
+
- Use `-o json` everywhere; every command returns a stable, versioned envelope.
|
|
225
|
+
- Get blast radius from `--dry-run` / `--plan`, never from your own reasoning.
|
|
226
|
+
- **Never self-fill `--ticket`, `--allow-*`, or a high-risk `--yes`.** Surface the required human approval and stop.
|
|
227
|
+
|
|
228
|
+
Install the bundled skill into your agent so it learns these rules automatically:
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
mqgov install claude --skills # also: codex, opencode, copilot, cursor, windsurf, aider, cc-switch
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## 🔏 Trust & verification
|
|
237
|
+
|
|
238
|
+
- **Signed binaries** — every release artifact is signed with [cosign](https://github.com/sigstore/cosign) (keyless / OIDC). A `checksums.txt` (also signed) covers all platforms.
|
|
239
|
+
- **npm provenance** — the npm package is published from CI via OpenID Connect with [provenance attestations](https://docs.npmjs.com/generating-provenance-statements) linking it to this exact repo and workflow.
|
|
240
|
+
- **Verified installs** — the npm postinstall downloads the binary over an allow-listed host and checks its SHA-256 against the signed `checksums.txt` before installing.
|
|
241
|
+
- **Tamper-evident audit** — `mqgov audit verify --strict` re-walks the hash chain and reports any gap or modification.
|
|
242
|
+
- **No insecure transport** — SASL/TLS and mTLS only; mqgov never offers an insecure-skip-verify escape hatch.
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## 🏗️ Build from source & contribute
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
git clone https://github.com/JiangHe12/mqgov-cli && cd mqgov-cli
|
|
250
|
+
go build ./...
|
|
251
|
+
go test -count=1 ./...
|
|
252
|
+
gofmt -l main.go cmd internal # must print nothing
|
|
253
|
+
golangci-lint run --timeout=5m
|
|
254
|
+
go vet -tags=integration ./...
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Real-backend integration tests (`//go:build integration`, env-gated, skipped by default) run against live Kafka/RabbitMQ/Pulsar/RocketMQ containers in the nightly `integration.yml` workflow; see [`docs/`](docs/) for how to run them locally with the bundled `docker-compose.*.yml` files.
|
|
258
|
+
|
|
259
|
+
mqgov-cli is built on the shared [`opskit-core`](https://github.com/JiangHe12/opskit-core) governance engine and is part of the **opskit** family of governed CLIs for AI agents — alongside [`dbgov-cli`](https://www.npmjs.com/package/dbgov-cli) (databases), [`srvgov-cli`](https://www.npmjs.com/package/srvgov-cli) (remote servers), and [`cfgov-cli`](https://www.npmjs.com/package/cfgov-cli) (config centers).
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## 📄 License
|
|
264
|
+
|
|
265
|
+
[MIT](LICENSE) © JiangHe12
|
package/bin/mqgov-cli.js
ADDED
|
@@ -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' ? 'mqgov.exe' : 'mqgov';
|
|
10
|
+
return path.join(__dirname, binaryName);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function main() {
|
|
14
|
+
const binaryPath = getBinaryPath();
|
|
15
|
+
if (!fs.existsSync(binaryPath)) {
|
|
16
|
+
console.error('mqgov binary not found. Please reinstall:');
|
|
17
|
+
console.error(' npm install -g mqgov-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 mqgov:', 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,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mqgov-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Governed message-broker operations CLI for AI agents (Kafka, RabbitMQ, Pulsar, RocketMQ)",
|
|
5
|
+
"bin": {
|
|
6
|
+
"mqgov": "bin/mqgov-cli.js",
|
|
7
|
+
"mqgov-cli": "bin/mqgov-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
|
+
"kafka",
|
|
20
|
+
"rabbitmq",
|
|
21
|
+
"pulsar",
|
|
22
|
+
"rocketmq",
|
|
23
|
+
"message-queue",
|
|
24
|
+
"governance",
|
|
25
|
+
"cli",
|
|
26
|
+
"ai"
|
|
27
|
+
],
|
|
28
|
+
"author": "JiangHe12",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/JiangHe12/mqgov-cli.git"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/JiangHe12/mqgov-cli",
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=14"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -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/mqgov-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.MQGOV_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 `mqgov-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.MQGOV_CLI_SKIP_VERIFY === '1') {
|
|
174
|
+
console.log('Verification skipped (MQGOV_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 MQGOV_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 MQGOV_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' ? 'mqgov.exe' : 'mqgov');
|
|
212
|
+
|
|
213
|
+
console.log(`Installing mqgov 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('mqgov installed successfully!');
|
|
221
|
+
} catch (err) {
|
|
222
|
+
console.error('Failed to install mqgov:', 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();
|