@calibraops/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/README.md +94 -0
- package/dist/calibra.js +451 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# `calibra` CLI
|
|
2
|
+
|
|
3
|
+
CalibraOps Operating Layer 的独立命令行客户端(#131 S4/S6)。任何 coding agent /
|
|
4
|
+
人,加载角色 bundle 后经本 CLI 操作 CalibraOps——薄 HTTP 客户端,**治理逻辑在
|
|
5
|
+
服务端**:
|
|
6
|
+
|
|
7
|
+
- **写**只走 `POST /api/apply`(受治理内核 `Calibra.Cli.apply` + 同事务不可变
|
|
8
|
+
`ChangeRecord`);身份(`actor` / `customer` / `actor_kind` / `actor_role`)由
|
|
9
|
+
服务端从认证 token 派生,CLI **不自证**。
|
|
10
|
+
- **读**走 `GET /api/calibra/{query,search,get,changelog}`,认证 actor 传 Ash,
|
|
11
|
+
租户隔离策略生效。
|
|
12
|
+
|
|
13
|
+
> 与 `mix calibra`(dev / 本地运维,in-process 无认证)相对——本 CLI 是
|
|
14
|
+
> **production 写入路径**。
|
|
15
|
+
|
|
16
|
+
## 运行
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
bun install # 首次
|
|
20
|
+
bun run bin/calibra.ts <command>
|
|
21
|
+
# 或链接为 calibra:bun link
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## 认证
|
|
25
|
+
|
|
26
|
+
两种模式,都存 `~/.calibra/credentials.json`(0600,多 profile);token 过期自动刷新。
|
|
27
|
+
|
|
28
|
+
**OAuth Authorization Code + PKCE(人,浏览器交互)**——`calibra login` 默认走此:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
calibra login [--profile <name>]
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
打开浏览器经 Hydra 登录,loopback `127.0.0.1:18988` 收回调。
|
|
35
|
+
|
|
36
|
+
**client_credentials(agent / 机器,非交互)**——给 `--client-id/--client-secret`:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
calibra login --profile agent --client-id calibra-agent --client-secret <secret>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
环境变量:`CALIBRA_API_URL`(默认 `http://localhost:4001`)、`CALIBRA_AUTH_URL`
|
|
43
|
+
(默认 `http://localhost:4544/oauth2/auth`)、`CALIBRA_TOKEN_URL`(默认
|
|
44
|
+
`…/oauth2/token`)、`CALIBRA_TOKEN`(直供 token,跳过 login,CI / 调试用)。
|
|
45
|
+
|
|
46
|
+
### 多 profile
|
|
47
|
+
|
|
48
|
+
每个 `--profile <name>` 是独立身份(人 / agent / 不同租户)。所有命令支持
|
|
49
|
+
`--profile`,缺省用 current。
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
calibra profile list # 列出所有 profile,标 current
|
|
53
|
+
calibra profile use <name> # 切换 current
|
|
54
|
+
calibra status [--profile N] # 某 profile 的身份 / 过期时间
|
|
55
|
+
calibra logout [--profile N]
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 注册 Hydra client(一次性运维)
|
|
59
|
+
|
|
60
|
+
两个 client 由运维在 Hydra(dev admin 端口 4545)注册一次:
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
# 人——PKCE public client(无 secret)
|
|
64
|
+
hydra create client --endpoint http://localhost:4545 \
|
|
65
|
+
--grant-type authorization_code,refresh_token --response-type code \
|
|
66
|
+
--token-endpoint-auth-method none --scope "openid offline_access" \
|
|
67
|
+
--redirect-uri http://127.0.0.1:18988/callback --id calibra-cli
|
|
68
|
+
|
|
69
|
+
# agent——client_credentials confidential client
|
|
70
|
+
hydra create client --endpoint http://localhost:4545 \
|
|
71
|
+
--grant-type client_credentials --token-endpoint-auth-method client_secret_post \
|
|
72
|
+
--id calibra-agent --secret <secret>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`calibra-agent` 已在 `config :calibra, :machine_principals` 白名单里映射到受控
|
|
76
|
+
service-principal Member(`calibra-agent@svc.calibra`,role `compliance_admin`)。
|
|
77
|
+
|
|
78
|
+
## 命令
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
calibra login [--profile <name>] [--client-id <id> --client-secret <s>]
|
|
82
|
+
calibra logout [--profile <name>]
|
|
83
|
+
calibra status [--profile <name>]
|
|
84
|
+
calibra profile list | use <name>
|
|
85
|
+
|
|
86
|
+
calibra query <name> [--<arg> <value> ...] # 必答查询 / 列表
|
|
87
|
+
calibra search <text> [--limit <n>] # 纯 Postgres FTS
|
|
88
|
+
calibra get <kind> <id>
|
|
89
|
+
calibra changelog <kind> <id>
|
|
90
|
+
calibra apply --kind <k> --op <create|update|...> \
|
|
91
|
+
--data '<json>' --rationale '<text>' [--cite a,b] [--target-id <id>]
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
成功输出单个 JSON(stdout);失败 stderr + 退出码 1。
|
package/dist/calibra.js
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// src/auth.ts
|
|
5
|
+
import { createServer } from "http";
|
|
6
|
+
import { exec } from "child_process";
|
|
7
|
+
import { createHash, randomBytes } from "crypto";
|
|
8
|
+
|
|
9
|
+
// src/store.ts
|
|
10
|
+
import { homedir } from "os";
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from "fs";
|
|
13
|
+
var DIR = join(homedir(), ".calibra");
|
|
14
|
+
var CREDENTIALS_FILE = join(DIR, "credentials.json");
|
|
15
|
+
function emptyStore() {
|
|
16
|
+
return { version: 1, current: "default", profiles: {} };
|
|
17
|
+
}
|
|
18
|
+
function loadStore() {
|
|
19
|
+
if (!existsSync(CREDENTIALS_FILE))
|
|
20
|
+
return emptyStore();
|
|
21
|
+
try {
|
|
22
|
+
const parsed = JSON.parse(readFileSync(CREDENTIALS_FILE, "utf8"));
|
|
23
|
+
if (parsed && parsed.version === 1 && parsed.profiles)
|
|
24
|
+
return parsed;
|
|
25
|
+
return emptyStore();
|
|
26
|
+
} catch {
|
|
27
|
+
return emptyStore();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function saveStore(s) {
|
|
31
|
+
if (!existsSync(DIR))
|
|
32
|
+
mkdirSync(DIR, { recursive: true, mode: 448 });
|
|
33
|
+
writeFileSync(CREDENTIALS_FILE, JSON.stringify(s, null, 2), { mode: 384 });
|
|
34
|
+
chmodSync(CREDENTIALS_FILE, 384);
|
|
35
|
+
}
|
|
36
|
+
function getProfile(s, name) {
|
|
37
|
+
return s.profiles[name ?? s.current] ?? null;
|
|
38
|
+
}
|
|
39
|
+
function putProfile(name, c) {
|
|
40
|
+
const s = loadStore();
|
|
41
|
+
s.profiles[name] = c;
|
|
42
|
+
s.current = name;
|
|
43
|
+
saveStore(s);
|
|
44
|
+
}
|
|
45
|
+
function removeProfile(name) {
|
|
46
|
+
const s = loadStore();
|
|
47
|
+
delete s.profiles[name];
|
|
48
|
+
if (s.current === name)
|
|
49
|
+
s.current = Object.keys(s.profiles)[0] ?? "default";
|
|
50
|
+
saveStore(s);
|
|
51
|
+
}
|
|
52
|
+
function setCurrent(name) {
|
|
53
|
+
const s = loadStore();
|
|
54
|
+
if (!s.profiles[name])
|
|
55
|
+
throw new Error(`profile not found: ${name}`);
|
|
56
|
+
s.current = name;
|
|
57
|
+
saveStore(s);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/auth.ts
|
|
61
|
+
var DEFAULT_API = process.env.CALIBRA_API_URL ?? "http://localhost:4001";
|
|
62
|
+
var DEFAULT_TOKEN_URL = process.env.CALIBRA_TOKEN_URL ?? "http://localhost:4544/oauth2/token";
|
|
63
|
+
var DEFAULT_AUTH_URL = process.env.CALIBRA_AUTH_URL ?? "http://localhost:4544/oauth2/auth";
|
|
64
|
+
var PKCE_CLIENT_ID = process.env.CALIBRA_PKCE_CLIENT_ID ?? "calibra-cli";
|
|
65
|
+
var LOOPBACK_PORT = 18988;
|
|
66
|
+
var REDIRECT_URI = `http://127.0.0.1:${LOOPBACK_PORT}/callback`;
|
|
67
|
+
function b64url(buf) {
|
|
68
|
+
return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
69
|
+
}
|
|
70
|
+
async function exchangeToken(tokenUrl, params) {
|
|
71
|
+
const res = await fetch(tokenUrl, {
|
|
72
|
+
method: "POST",
|
|
73
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
74
|
+
body: new URLSearchParams(params)
|
|
75
|
+
});
|
|
76
|
+
if (!res.ok)
|
|
77
|
+
throw new Error(`token endpoint ${res.status}: ${await res.text()}`);
|
|
78
|
+
return await res.json();
|
|
79
|
+
}
|
|
80
|
+
function identityFromJwt(jwt) {
|
|
81
|
+
try {
|
|
82
|
+
const part = jwt.split(".")[1];
|
|
83
|
+
if (!part)
|
|
84
|
+
return "unknown";
|
|
85
|
+
const payload = JSON.parse(Buffer.from(part, "base64url").toString());
|
|
86
|
+
return payload.email ?? payload.sub ?? "unknown";
|
|
87
|
+
} catch {
|
|
88
|
+
return "unknown";
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async function clientCredentialsLogin(opts) {
|
|
92
|
+
const tokenUrl = opts.tokenUrl ?? DEFAULT_TOKEN_URL;
|
|
93
|
+
const apiUrl = opts.apiUrl ?? DEFAULT_API;
|
|
94
|
+
const t = await exchangeToken(tokenUrl, {
|
|
95
|
+
grant_type: "client_credentials",
|
|
96
|
+
client_id: opts.clientId,
|
|
97
|
+
client_secret: opts.clientSecret
|
|
98
|
+
});
|
|
99
|
+
const c = {
|
|
100
|
+
apiUrl,
|
|
101
|
+
tokenUrl,
|
|
102
|
+
authMode: "client_credentials",
|
|
103
|
+
clientId: opts.clientId,
|
|
104
|
+
clientSecret: opts.clientSecret,
|
|
105
|
+
token: t.access_token,
|
|
106
|
+
expiresAt: Date.now() + (t.expires_in ?? 3600) * 1000,
|
|
107
|
+
identity: opts.clientId
|
|
108
|
+
};
|
|
109
|
+
putProfile(opts.profile, c);
|
|
110
|
+
return c;
|
|
111
|
+
}
|
|
112
|
+
async function pkceLogin(opts) {
|
|
113
|
+
const apiUrl = opts.apiUrl ?? DEFAULT_API;
|
|
114
|
+
const authUrl = opts.authUrl ?? DEFAULT_AUTH_URL;
|
|
115
|
+
const tokenUrl = opts.tokenUrl ?? DEFAULT_TOKEN_URL;
|
|
116
|
+
const verifier = b64url(randomBytes(32));
|
|
117
|
+
const challenge = b64url(createHash("sha256").update(verifier).digest());
|
|
118
|
+
const state = b64url(randomBytes(16));
|
|
119
|
+
const u = new URL(authUrl);
|
|
120
|
+
u.searchParams.set("response_type", "code");
|
|
121
|
+
u.searchParams.set("client_id", PKCE_CLIENT_ID);
|
|
122
|
+
u.searchParams.set("redirect_uri", REDIRECT_URI);
|
|
123
|
+
u.searchParams.set("scope", "openid offline_access");
|
|
124
|
+
u.searchParams.set("state", state);
|
|
125
|
+
u.searchParams.set("code_challenge", challenge);
|
|
126
|
+
u.searchParams.set("code_challenge_method", "S256");
|
|
127
|
+
const code = await waitForCode(u.toString(), state);
|
|
128
|
+
const t = await exchangeToken(tokenUrl, {
|
|
129
|
+
grant_type: "authorization_code",
|
|
130
|
+
code,
|
|
131
|
+
redirect_uri: REDIRECT_URI,
|
|
132
|
+
client_id: PKCE_CLIENT_ID,
|
|
133
|
+
code_verifier: verifier
|
|
134
|
+
});
|
|
135
|
+
const c = {
|
|
136
|
+
apiUrl,
|
|
137
|
+
tokenUrl,
|
|
138
|
+
authMode: "pkce",
|
|
139
|
+
clientId: PKCE_CLIENT_ID,
|
|
140
|
+
refreshToken: t.refresh_token,
|
|
141
|
+
token: t.access_token,
|
|
142
|
+
expiresAt: Date.now() + (t.expires_in ?? 3600) * 1000,
|
|
143
|
+
identity: identityFromJwt(t.access_token)
|
|
144
|
+
};
|
|
145
|
+
putProfile(opts.profile, c);
|
|
146
|
+
return c;
|
|
147
|
+
}
|
|
148
|
+
function waitForCode(authorizeUrl, expectedState) {
|
|
149
|
+
return new Promise((resolve, reject) => {
|
|
150
|
+
const timeout = setTimeout(() => {
|
|
151
|
+
server.close();
|
|
152
|
+
reject(new Error("login timed out (120s)"));
|
|
153
|
+
}, 120000);
|
|
154
|
+
const server = createServer((req, res) => {
|
|
155
|
+
const url = new URL(req.url ?? "/", `http://127.0.0.1:${LOOPBACK_PORT}`);
|
|
156
|
+
if (url.pathname !== "/callback") {
|
|
157
|
+
res.writeHead(404);
|
|
158
|
+
res.end("not found");
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const code = url.searchParams.get("code");
|
|
162
|
+
const state = url.searchParams.get("state");
|
|
163
|
+
const err = url.searchParams.get("error");
|
|
164
|
+
const page = (title, msg) => {
|
|
165
|
+
res.writeHead(200, { "content-type": "text/html" });
|
|
166
|
+
res.end(`<!doctype html><body style="font-family:system-ui;text-align:center;padding:48px">` + `<h2>${title}</h2><p>${msg}</p></body>`);
|
|
167
|
+
clearTimeout(timeout);
|
|
168
|
+
server.close();
|
|
169
|
+
};
|
|
170
|
+
if (err) {
|
|
171
|
+
page("Login failed", err);
|
|
172
|
+
reject(new Error(`OAuth error: ${err}`));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (!code || state !== expectedState) {
|
|
176
|
+
page("Login failed", "state mismatch or missing code");
|
|
177
|
+
reject(new Error("invalid callback: state mismatch or missing code"));
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
page("Login successful", "You can close this window and return to the terminal.");
|
|
181
|
+
resolve(code);
|
|
182
|
+
});
|
|
183
|
+
server.on("error", (e) => {
|
|
184
|
+
clearTimeout(timeout);
|
|
185
|
+
reject(e.code === "EADDRINUSE" ? new Error(`port ${LOOPBACK_PORT} in use \u2014 close other \`calibra login\` and retry`) : e);
|
|
186
|
+
});
|
|
187
|
+
server.listen(LOOPBACK_PORT, "127.0.0.1", () => {
|
|
188
|
+
console.error(`
|
|
189
|
+
calibra: opening browser for login\u2026`);
|
|
190
|
+
console.error(` \u82E5\u6D4F\u89C8\u5668\u672A\u6253\u5F00\uFF0C\u624B\u52A8\u8BBF\u95EE\uFF1A
|
|
191
|
+
${authorizeUrl}
|
|
192
|
+
`);
|
|
193
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
194
|
+
exec(`${cmd} "${authorizeUrl}"`, () => {});
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
async function bearer(profileName) {
|
|
199
|
+
const envTok = process.env.CALIBRA_TOKEN;
|
|
200
|
+
if (envTok)
|
|
201
|
+
return { token: envTok, apiUrl: DEFAULT_API };
|
|
202
|
+
const store = loadStore();
|
|
203
|
+
const c = getProfile(store, profileName);
|
|
204
|
+
if (!c) {
|
|
205
|
+
throw new Error(`not logged in${profileName ? ` (profile ${profileName})` : ""} \u2014 run \`calibra login\``);
|
|
206
|
+
}
|
|
207
|
+
if (c.expiresAt && Date.now() > c.expiresAt - 30000) {
|
|
208
|
+
const refreshed = await refresh(c);
|
|
209
|
+
putProfile(profileName ?? store.current, refreshed);
|
|
210
|
+
return { token: refreshed.token, apiUrl: refreshed.apiUrl };
|
|
211
|
+
}
|
|
212
|
+
return { token: c.token, apiUrl: c.apiUrl };
|
|
213
|
+
}
|
|
214
|
+
async function refresh(c) {
|
|
215
|
+
if (c.authMode === "client_credentials" && c.clientId && c.clientSecret) {
|
|
216
|
+
const t = await exchangeToken(c.tokenUrl, {
|
|
217
|
+
grant_type: "client_credentials",
|
|
218
|
+
client_id: c.clientId,
|
|
219
|
+
client_secret: c.clientSecret
|
|
220
|
+
});
|
|
221
|
+
return { ...c, token: t.access_token, expiresAt: Date.now() + (t.expires_in ?? 3600) * 1000 };
|
|
222
|
+
}
|
|
223
|
+
if (c.authMode === "pkce" && c.refreshToken && c.clientId) {
|
|
224
|
+
const t = await exchangeToken(c.tokenUrl, {
|
|
225
|
+
grant_type: "refresh_token",
|
|
226
|
+
refresh_token: c.refreshToken,
|
|
227
|
+
client_id: c.clientId
|
|
228
|
+
});
|
|
229
|
+
return {
|
|
230
|
+
...c,
|
|
231
|
+
token: t.access_token,
|
|
232
|
+
refreshToken: t.refresh_token ?? c.refreshToken,
|
|
233
|
+
expiresAt: Date.now() + (t.expires_in ?? 3600) * 1000
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
throw new Error("token expired and cannot refresh \u2014 run `calibra login`");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// src/api.ts
|
|
240
|
+
async function parse(res) {
|
|
241
|
+
const text = await res.text();
|
|
242
|
+
const json = text ? JSON.parse(text) : {};
|
|
243
|
+
if (!res.ok)
|
|
244
|
+
throw new Error(typeof json.error === "string" ? json.error : `HTTP ${res.status}`);
|
|
245
|
+
return json;
|
|
246
|
+
}
|
|
247
|
+
async function apiGet(path, profile) {
|
|
248
|
+
const { token, apiUrl } = await bearer(profile);
|
|
249
|
+
const res = await fetch(`${apiUrl}${path}`, {
|
|
250
|
+
headers: { authorization: `Bearer ${token}`, accept: "application/json" }
|
|
251
|
+
});
|
|
252
|
+
return parse(res);
|
|
253
|
+
}
|
|
254
|
+
async function apiPost(path, body, profile) {
|
|
255
|
+
const { token, apiUrl } = await bearer(profile);
|
|
256
|
+
const res = await fetch(`${apiUrl}${path}`, {
|
|
257
|
+
method: "POST",
|
|
258
|
+
headers: {
|
|
259
|
+
authorization: `Bearer ${token}`,
|
|
260
|
+
"content-type": "application/json",
|
|
261
|
+
accept: "application/json"
|
|
262
|
+
},
|
|
263
|
+
body: JSON.stringify(body)
|
|
264
|
+
});
|
|
265
|
+
return parse(res);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// src/main.ts
|
|
269
|
+
var USAGE = `calibra \u2014 CalibraOps Operating Layer CLI (#131)
|
|
270
|
+
|
|
271
|
+
calibra login [--profile <name>] # OAuth PKCE \u6D4F\u89C8\u5668\u767B\u5F55\uFF08\u4EBA\uFF09
|
|
272
|
+
[--client-id <id> --client-secret <s>] # \u6216 client_credentials\uFF08agent/\u673A\u5668\uFF09
|
|
273
|
+
[--api-url <u>] [--auth-url <u>] [--token-url <u>]
|
|
274
|
+
calibra logout [--profile <name>]
|
|
275
|
+
calibra status [--profile <name>]
|
|
276
|
+
calibra profile list | use <name>
|
|
277
|
+
|
|
278
|
+
calibra query <name> [--<arg> <value> ...]
|
|
279
|
+
calibra search <text> [--limit <n>]
|
|
280
|
+
calibra get <kind> <id>
|
|
281
|
+
calibra changelog <kind> <id>
|
|
282
|
+
calibra apply --kind <k> --op <create|update|...> --data <json> --rationale <text>
|
|
283
|
+
[--cite <chunk_a,chunk_b>] [--target-id <id>]
|
|
284
|
+
|
|
285
|
+
\u6240\u6709\u547D\u4EE4\u652F\u6301 --profile <name>\uFF08\u9ED8\u8BA4 current profile\uFF09\u3002
|
|
286
|
+
\u8EAB\u4EFD\u4ECE\u8BA4\u8BC1 token \u6D3E\u751F\uFF08\u670D\u52A1\u7AEF\uFF09\uFF1B\u5199\u5165\u53EA\u7ECF POST /api/apply\uFF08\u53D7\u6CBB\u7406 + ChangeRecord\uFF09\u3002
|
|
287
|
+
\u73AF\u5883\u53D8\u91CF\uFF1ACALIBRA_API_URL\u3001CALIBRA_AUTH_URL\u3001CALIBRA_TOKEN_URL\u3001CALIBRA_TOKEN\u3002`;
|
|
288
|
+
function parseArgs(args) {
|
|
289
|
+
const flags = {};
|
|
290
|
+
const positional = [];
|
|
291
|
+
for (let i = 0;i < args.length; i++) {
|
|
292
|
+
const a = args[i];
|
|
293
|
+
if (a.startsWith("--")) {
|
|
294
|
+
const key = a.slice(2);
|
|
295
|
+
const next = args[i + 1];
|
|
296
|
+
if (next !== undefined && !next.startsWith("--")) {
|
|
297
|
+
flags[key] = next;
|
|
298
|
+
i++;
|
|
299
|
+
} else {
|
|
300
|
+
flags[key] = "true";
|
|
301
|
+
}
|
|
302
|
+
} else {
|
|
303
|
+
positional.push(a);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return { flags, positional };
|
|
307
|
+
}
|
|
308
|
+
function out(v) {
|
|
309
|
+
console.log(JSON.stringify(v, null, 2));
|
|
310
|
+
}
|
|
311
|
+
function die(msg) {
|
|
312
|
+
console.error(`calibra: ${msg}`);
|
|
313
|
+
process.exit(1);
|
|
314
|
+
}
|
|
315
|
+
async function main() {
|
|
316
|
+
const argv = process.argv.slice(2);
|
|
317
|
+
const cmd = argv[0];
|
|
318
|
+
const { flags, positional } = parseArgs(argv.slice(1));
|
|
319
|
+
const profile = flags["profile"];
|
|
320
|
+
if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") {
|
|
321
|
+
console.log(USAGE);
|
|
322
|
+
process.exit(cmd ? 0 : 1);
|
|
323
|
+
}
|
|
324
|
+
try {
|
|
325
|
+
switch (cmd) {
|
|
326
|
+
case "login": {
|
|
327
|
+
const name = profile ?? "default";
|
|
328
|
+
const clientId = flags["client-id"] ?? process.env.CALIBRA_CLIENT_ID;
|
|
329
|
+
const clientSecret = flags["client-secret"] ?? process.env.CALIBRA_CLIENT_SECRET;
|
|
330
|
+
if (clientId && clientSecret) {
|
|
331
|
+
const c = await clientCredentialsLogin({
|
|
332
|
+
profile: name,
|
|
333
|
+
clientId,
|
|
334
|
+
clientSecret,
|
|
335
|
+
tokenUrl: flags["token-url"],
|
|
336
|
+
apiUrl: flags["api-url"]
|
|
337
|
+
});
|
|
338
|
+
console.error(`calibra: logged in [${name}] as ${c.identity} (client_credentials)`);
|
|
339
|
+
} else {
|
|
340
|
+
const c = await pkceLogin({
|
|
341
|
+
profile: name,
|
|
342
|
+
apiUrl: flags["api-url"],
|
|
343
|
+
authUrl: flags["auth-url"],
|
|
344
|
+
tokenUrl: flags["token-url"]
|
|
345
|
+
});
|
|
346
|
+
console.error(`calibra: logged in [${name}] as ${c.identity} (pkce)`);
|
|
347
|
+
}
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
case "logout": {
|
|
351
|
+
const store = loadStore();
|
|
352
|
+
const name = profile ?? store.current;
|
|
353
|
+
removeProfile(name);
|
|
354
|
+
console.error(`calibra: logged out [${name}]`);
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
case "status": {
|
|
358
|
+
const store = loadStore();
|
|
359
|
+
const name = profile ?? store.current;
|
|
360
|
+
const c = store.profiles[name];
|
|
361
|
+
if (!c)
|
|
362
|
+
die(`not logged in (profile ${name})`);
|
|
363
|
+
out({
|
|
364
|
+
profile: name,
|
|
365
|
+
current: store.current === name,
|
|
366
|
+
identity: c.identity ?? null,
|
|
367
|
+
authMode: c.authMode,
|
|
368
|
+
apiUrl: c.apiUrl,
|
|
369
|
+
expiresAt: c.expiresAt ? new Date(c.expiresAt).toISOString() : null
|
|
370
|
+
});
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
case "profile": {
|
|
374
|
+
const sub = positional[0];
|
|
375
|
+
const store = loadStore();
|
|
376
|
+
if (sub === "list") {
|
|
377
|
+
out(Object.entries(store.profiles).map(([name, c]) => ({
|
|
378
|
+
profile: name,
|
|
379
|
+
current: store.current === name,
|
|
380
|
+
identity: c.identity ?? null,
|
|
381
|
+
authMode: c.authMode
|
|
382
|
+
})));
|
|
383
|
+
} else if (sub === "use") {
|
|
384
|
+
const name = positional[1] ?? die("profile use \u9700 <name>");
|
|
385
|
+
setCurrent(name);
|
|
386
|
+
console.error(`calibra: current profile \u2192 ${name}`);
|
|
387
|
+
} else {
|
|
388
|
+
die("profile \u9700 list | use <name>");
|
|
389
|
+
}
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
case "query": {
|
|
393
|
+
const name = positional[0] ?? die("query \u9700 <name>");
|
|
394
|
+
const qs = new URLSearchParams;
|
|
395
|
+
for (const [k, v] of Object.entries(flags)) {
|
|
396
|
+
if (k === "profile")
|
|
397
|
+
continue;
|
|
398
|
+
qs.set(k.replace(/-/g, "_"), v);
|
|
399
|
+
}
|
|
400
|
+
const suffix = qs.toString();
|
|
401
|
+
out(await apiGet(`/api/calibra/query/${name}${suffix ? `?${suffix}` : ""}`, profile));
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
case "search": {
|
|
405
|
+
const text = positional[0] ?? die("search \u9700 <text>");
|
|
406
|
+
const p = new URLSearchParams({ q: text });
|
|
407
|
+
if (flags["limit"])
|
|
408
|
+
p.set("limit", flags["limit"]);
|
|
409
|
+
out(await apiGet(`/api/calibra/search?${p}`, profile));
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
case "get": {
|
|
413
|
+
const kind = positional[0] ?? die("get \u9700 <kind> <id>");
|
|
414
|
+
const id = positional[1] ?? die("get \u9700 <kind> <id>");
|
|
415
|
+
out(await apiGet(`/api/calibra/get/${kind}/${id}`, profile));
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
case "changelog": {
|
|
419
|
+
const kind = positional[0] ?? die("changelog \u9700 <kind> <id>");
|
|
420
|
+
const id = positional[1] ?? die("changelog \u9700 <kind> <id>");
|
|
421
|
+
out(await apiGet(`/api/calibra/changelog/${kind}/${id}`, profile));
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
case "apply": {
|
|
425
|
+
if (!flags["kind"] || !flags["op"])
|
|
426
|
+
die("apply \u9700 --kind \u4E0E --op");
|
|
427
|
+
const body = {
|
|
428
|
+
kind: flags["kind"],
|
|
429
|
+
op: flags["op"],
|
|
430
|
+
data: flags["data"] ? JSON.parse(flags["data"]) : {},
|
|
431
|
+
rationale: flags["rationale"]
|
|
432
|
+
};
|
|
433
|
+
if (flags["cite"])
|
|
434
|
+
body.cite = flags["cite"].split(",");
|
|
435
|
+
if (flags["target-id"])
|
|
436
|
+
body.target_id = flags["target-id"];
|
|
437
|
+
out(await apiPost("/api/apply", body, profile));
|
|
438
|
+
break;
|
|
439
|
+
}
|
|
440
|
+
default:
|
|
441
|
+
die(`\u672A\u77E5\u547D\u4EE4: ${cmd}
|
|
442
|
+
|
|
443
|
+
${USAGE}`);
|
|
444
|
+
}
|
|
445
|
+
} catch (e) {
|
|
446
|
+
die(e.message);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// bin/calibra.ts
|
|
451
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@calibraops/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CalibraOps CLI — Operating Layer client: calibra login + query/search/get/changelog/apply",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"calibra": "./dist/calibra.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/calibra.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"bun": ">=1.0.0"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"compliance",
|
|
19
|
+
"calibraops",
|
|
20
|
+
"cli",
|
|
21
|
+
"governance",
|
|
22
|
+
"audit"
|
|
23
|
+
],
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/luqg/calibraops.git",
|
|
27
|
+
"directory": "apps/cli"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://github.com/luqg/calibraops/tree/main/apps/cli",
|
|
30
|
+
"license": "UNLICENSED",
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"dev": "bun run bin/calibra.ts",
|
|
36
|
+
"build": "bun build bin/calibra.ts --outfile dist/calibra.js --target bun",
|
|
37
|
+
"typecheck": "tsc --noEmit"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/bun": "^1.2.0",
|
|
41
|
+
"typescript": "^5.7.0"
|
|
42
|
+
}
|
|
43
|
+
}
|