mandat 0.0.0-stage → 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.
Files changed (70) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +293 -3
  3. package/dist/approvals/approvals.d.ts +136 -0
  4. package/dist/approvals/approvals.js +262 -0
  5. package/dist/bridge.d.ts +38 -0
  6. package/dist/bridge.js +117 -0
  7. package/dist/cli/args.d.ts +9 -0
  8. package/dist/cli/args.js +39 -0
  9. package/dist/cli/bin.d.ts +2 -0
  10. package/dist/cli/bin.js +3 -0
  11. package/dist/cli/main.d.ts +1 -0
  12. package/dist/cli/main.js +649 -0
  13. package/dist/cli/templates.d.ts +91 -0
  14. package/dist/cli/templates.js +57 -0
  15. package/dist/client.d.ts +54 -0
  16. package/dist/client.js +88 -0
  17. package/dist/controlplane/config.d.ts +48 -0
  18. package/dist/controlplane/config.js +33 -0
  19. package/dist/controlplane/console/.claude-flow/neural/patterns.json +402 -0
  20. package/dist/controlplane/console/.claude-flow/neural/stats.json +6 -0
  21. package/dist/controlplane/console/.claude-flow/policy/ledger-anchors.json +3 -0
  22. package/dist/controlplane/console/.claude-flow/policy/state.json +54 -0
  23. package/dist/controlplane/console/app.css +93 -0
  24. package/dist/controlplane/console/app.js +330 -0
  25. package/dist/controlplane/console/index.html +70 -0
  26. package/dist/controlplane/server.d.ts +17 -0
  27. package/dist/controlplane/server.js +427 -0
  28. package/dist/guard/guard.d.ts +154 -0
  29. package/dist/guard/guard.js +290 -0
  30. package/dist/guard/pinning.d.ts +46 -0
  31. package/dist/guard/pinning.js +49 -0
  32. package/dist/guard/remote.d.ts +78 -0
  33. package/dist/guard/remote.js +185 -0
  34. package/dist/index.d.ts +43 -0
  35. package/dist/index.js +67 -0
  36. package/dist/ledger/export.d.ts +27 -0
  37. package/dist/ledger/export.js +169 -0
  38. package/dist/ledger/ledger.d.ts +117 -0
  39. package/dist/ledger/ledger.js +139 -0
  40. package/dist/ledger/redact.d.ts +13 -0
  41. package/dist/ledger/redact.js +73 -0
  42. package/dist/ledger/report.d.ts +71 -0
  43. package/dist/ledger/report.js +128 -0
  44. package/dist/mcp/protect.d.ts +8 -0
  45. package/dist/mcp/protect.js +68 -0
  46. package/dist/mcp/server.d.ts +49 -0
  47. package/dist/mcp/server.js +211 -0
  48. package/dist/oidc.d.ts +52 -0
  49. package/dist/oidc.js +194 -0
  50. package/dist/policy/engine.d.ts +135 -0
  51. package/dist/policy/engine.js +243 -0
  52. package/dist/policy/expr.d.ts +56 -0
  53. package/dist/policy/expr.js +506 -0
  54. package/dist/scan/findings.d.ts +32 -0
  55. package/dist/scan/findings.js +105 -0
  56. package/dist/scan/remote.d.ts +7 -0
  57. package/dist/scan/remote.js +225 -0
  58. package/dist/scan/static.d.ts +15 -0
  59. package/dist/scan/static.js +245 -0
  60. package/dist/store.d.ts +14 -0
  61. package/dist/store.js +40 -0
  62. package/dist/sts/jwt.d.ts +64 -0
  63. package/dist/sts/jwt.js +168 -0
  64. package/dist/sts/revocation.d.ts +38 -0
  65. package/dist/sts/revocation.js +52 -0
  66. package/dist/sts/sts.d.ts +83 -0
  67. package/dist/sts/sts.js +239 -0
  68. package/dist/util.d.ts +28 -0
  69. package/dist/util.js +137 -0
  70. package/package.json +47 -4
@@ -0,0 +1,262 @@
1
+ // Human-in-the-loop approvals.
2
+ //
3
+ // An approval is bound to the exact call (sha256 over principal, agent, tool, server, args), so approving
4
+ // "refund €120 to order 42" can never authorise a different refund. Approved requests are single-use.
5
+ // Self-approval is forbidden; `required: 2` gives four-eyes. Timeout => expired => deny.
6
+ import { EventEmitter } from "node:events";
7
+ import { createHmac } from "node:crypto";
8
+ import { JsonCollection } from "../store.js";
9
+ import { canonicalJson, randomId, safeEqual, sha256 } from "../util.js";
10
+ export function approvalBinding(p) {
11
+ return sha256(canonicalJson({ sub: p.sub, agent: p.agent, tool: p.tool, server: p.server ?? null, args: p.args ?? {} }));
12
+ }
13
+ export class ApprovalError extends Error {
14
+ status;
15
+ constructor(status, message) {
16
+ super(message);
17
+ this.status = status;
18
+ }
19
+ }
20
+ export class ApprovalService {
21
+ store;
22
+ events = new EventEmitter();
23
+ notifiers;
24
+ ledger;
25
+ sweeper;
26
+ constructor(opts) {
27
+ this.store = new JsonCollection(opts.path);
28
+ this.notifiers = opts.notifiers ?? [];
29
+ this.ledger = opts.ledger;
30
+ this.events.setMaxListeners(0);
31
+ this.sweeper = setInterval(() => this.expireDue(), 1000);
32
+ this.sweeper.unref();
33
+ }
34
+ close() { clearInterval(this.sweeper); }
35
+ get(id) {
36
+ this.expireDue();
37
+ return this.store.get(id);
38
+ }
39
+ list(status) {
40
+ this.expireDue();
41
+ return this.store.filter((a) => !status || a.status === status).sort((a, b) => b.created_at.localeCompare(a.created_at));
42
+ }
43
+ /** Creates a request, or returns the live one with the same binding (pending, or approved and unused). */
44
+ async create(n) {
45
+ this.expireDue();
46
+ const existing = this.store.filter((a) => a.binding === n.binding && !a.consumed_at && (a.status === "pending" || a.status === "approved"))[0];
47
+ if (existing)
48
+ return existing;
49
+ const now = Date.now();
50
+ const req = {
51
+ id: randomId("apr_", 9),
52
+ binding: n.binding,
53
+ status: "pending",
54
+ required: Math.max(1, Math.min(n.required, 5)),
55
+ created_at: new Date(now).toISOString(),
56
+ expires_at: new Date(now + n.timeoutMs).toISOString(),
57
+ decided_at: null,
58
+ consumed_at: null,
59
+ votes: [],
60
+ channels: n.channels,
61
+ context: n.context,
62
+ };
63
+ this.store.put(req);
64
+ this.ledger?.append({
65
+ type: "approval.requested",
66
+ principal: n.context.principal,
67
+ agent: n.context.agent,
68
+ actor_chain: n.context.actor_chain,
69
+ task_id: n.context.task_id,
70
+ tool: { name: n.context.tool, server: n.context.server, risk: n.context.risk },
71
+ args: n.context.args,
72
+ approval: { id: req.id, status: "pending", required: req.required },
73
+ details: { rule: n.context.rule, reasons: n.context.reasons, expires_at: req.expires_at },
74
+ });
75
+ const targets = n.channels.length ? this.notifiers.filter((x) => n.channels.includes(x.name)) : this.notifiers;
76
+ await Promise.allSettled(targets.map((x) => x.notify(req)));
77
+ return req;
78
+ }
79
+ decide(id, vote) {
80
+ const req = this.get(id);
81
+ if (!req)
82
+ throw new ApprovalError(404, "Approval request not found");
83
+ if (req.status !== "pending")
84
+ throw new ApprovalError(409, `Approval is already ${req.status}`);
85
+ if (!vote.by)
86
+ throw new ApprovalError(400, "Approver identity is required");
87
+ if (vote.by === req.context.principal.sub || vote.by === req.context.principal.email)
88
+ throw new ApprovalError(403, "Self-approval is not allowed: the requesting principal cannot approve their own agent's action");
89
+ if (req.votes.some((v) => v.by === vote.by))
90
+ throw new ApprovalError(409, `${vote.by} has already voted`);
91
+ const votes = [...req.votes, { ...vote, at: new Date().toISOString() }];
92
+ const approvals = votes.filter((v) => v.decision === "approve").length;
93
+ const status = vote.decision === "deny" ? "denied" : approvals >= req.required ? "approved" : "pending";
94
+ const next = this.store.update(id, (r) => ({ ...r, votes, status, decided_at: status === "pending" ? null : new Date().toISOString() }));
95
+ if (status !== "pending")
96
+ this.finalised(next);
97
+ return next;
98
+ }
99
+ /** Marks an approved request as used. Returns false if it was already used (replay). */
100
+ consume(id) {
101
+ const req = this.get(id);
102
+ if (!req || req.status !== "approved" || req.consumed_at)
103
+ return false;
104
+ this.store.update(id, (r) => ({ ...r, consumed_at: new Date().toISOString() }));
105
+ return true;
106
+ }
107
+ /** Resolves when the request leaves `pending` or after `timeoutMs` (returning the current state). */
108
+ wait(id, timeoutMs) {
109
+ const cur = this.get(id);
110
+ if (!cur || cur.status !== "pending" || timeoutMs <= 0)
111
+ return Promise.resolve(cur);
112
+ return new Promise((resolve) => {
113
+ const done = () => { clearTimeout(t); this.events.off(id, done); resolve(this.store.get(id)); };
114
+ const t = setTimeout(done, Math.min(timeoutMs, Date.parse(cur.expires_at) - Date.now() + 50));
115
+ this.events.on(id, done);
116
+ });
117
+ }
118
+ expireDue() {
119
+ const now = new Date().toISOString();
120
+ for (const r of this.store.filter((a) => a.status === "pending" && a.expires_at <= now)) {
121
+ const next = this.store.update(r.id, (x) => ({ ...x, status: "expired", decided_at: now }));
122
+ this.finalised(next);
123
+ }
124
+ }
125
+ finalised(req) {
126
+ this.ledger?.append({
127
+ type: "approval.decided",
128
+ principal: req.context.principal,
129
+ agent: req.context.agent,
130
+ actor_chain: req.context.actor_chain,
131
+ task_id: req.context.task_id,
132
+ tool: { name: req.context.tool, server: req.context.server, risk: req.context.risk },
133
+ approval: {
134
+ id: req.id, status: req.status, required: req.required, decided_at: req.decided_at,
135
+ approvers: req.votes.filter((v) => v.decision === "approve").map((v) => v.by),
136
+ },
137
+ details: { votes: req.votes },
138
+ });
139
+ this.events.emit(req.id);
140
+ for (const n of this.notifiers)
141
+ void n.decided?.(req).catch(() => { });
142
+ }
143
+ }
144
+ // ---------- channels ----------
145
+ function summary(req) {
146
+ const c = req.context;
147
+ return `${c.agent.id} wants to call ${c.tool} on behalf of ${c.principal.email ?? c.principal.sub}`;
148
+ }
149
+ /** Generic signed webhook: POST JSON, `X-Mandat-Signature: sha256=<hmac(timestamp + "." + body)>`. */
150
+ export class WebhookNotifier {
151
+ name;
152
+ url;
153
+ secret;
154
+ consoleUrl;
155
+ constructor(opts) {
156
+ this.name = opts.name ?? "webhook";
157
+ this.url = opts.url;
158
+ this.secret = opts.secret;
159
+ this.consoleUrl = opts.consoleUrl;
160
+ }
161
+ async notify(req) {
162
+ await this.post({ event: "approval.requested", summary: summary(req), approval: req, console_url: `${this.consoleUrl}#${req.id}` });
163
+ }
164
+ async decided(req) {
165
+ await this.post({ event: "approval.decided", summary: `${summary(req)}: ${req.status}`, approval: req });
166
+ }
167
+ async post(payload) {
168
+ const body = JSON.stringify(payload);
169
+ const ts = Math.floor(Date.now() / 1000).toString();
170
+ const sig = createHmac("sha256", this.secret).update(`${ts}.${body}`).digest("hex");
171
+ await fetch(this.url, {
172
+ method: "POST",
173
+ headers: { "content-type": "application/json", "x-mandat-timestamp": ts, "x-mandat-signature": `sha256=${sig}` },
174
+ body,
175
+ signal: AbortSignal.timeout(5000),
176
+ });
177
+ }
178
+ }
179
+ /** Slack via an app's Incoming Webhook; buttons are handled by `handleSlackInteraction`. */
180
+ export class SlackNotifier {
181
+ name = "slack";
182
+ webhookUrl;
183
+ consoleUrl;
184
+ constructor(opts) {
185
+ this.webhookUrl = opts.webhookUrl;
186
+ this.consoleUrl = opts.consoleUrl;
187
+ }
188
+ async notify(req) {
189
+ await fetch(this.webhookUrl, {
190
+ method: "POST",
191
+ headers: { "content-type": "application/json" },
192
+ body: JSON.stringify({ text: `Approval needed: ${summary(req)}`, blocks: slackBlocks(req, this.consoleUrl) }),
193
+ signal: AbortSignal.timeout(5000),
194
+ });
195
+ }
196
+ }
197
+ export function slackBlocks(req, consoleUrl) {
198
+ const c = req.context;
199
+ const args = JSON.stringify(c.args, null, 2);
200
+ const blocks = [
201
+ { type: "header", text: { type: "plain_text", text: req.status === "pending" ? "🛂 Agent action needs approval" : `Agent action ${req.status}` } },
202
+ { type: "section", text: { type: "mrkdwn", text: `*${c.agent.id}*${c.agent.version ? ` (v${c.agent.version})` : ""} wants to call \`${c.tool}\` on behalf of *${c.principal.email ?? c.principal.sub}*` } },
203
+ {
204
+ type: "section",
205
+ fields: [
206
+ { type: "mrkdwn", text: `*Risk*\n${c.risk ?? "unclassified"}` },
207
+ { type: "mrkdwn", text: `*Rule*\n${c.rule}` },
208
+ { type: "mrkdwn", text: `*Delegation*\n${[c.principal.sub, ...(c.actor_chain ?? []).slice().reverse()].join(" → ")}` },
209
+ { type: "mrkdwn", text: `*Expires*\n<!date^${Math.floor(Date.parse(req.expires_at) / 1000)}^{time}|${req.expires_at}>` },
210
+ ],
211
+ },
212
+ { type: "section", text: { type: "mrkdwn", text: "```" + args.slice(0, 2500) + "```" } },
213
+ ];
214
+ if (c.session_tainted)
215
+ blocks.push({ type: "context", elements: [{ type: "mrkdwn", text: "⚠️ Session has processed untrusted content (possible prompt injection)." }] });
216
+ if (req.status === "pending") {
217
+ blocks.push({
218
+ type: "actions",
219
+ block_id: `mandat_${req.id}`,
220
+ elements: [
221
+ { type: "button", style: "primary", text: { type: "plain_text", text: "Approve" }, action_id: "mandat_approve", value: req.id },
222
+ { type: "button", style: "danger", text: { type: "plain_text", text: "Deny" }, action_id: "mandat_deny", value: req.id },
223
+ { type: "button", text: { type: "plain_text", text: "Open in console" }, url: `${consoleUrl}#${req.id}`, action_id: "mandat_open" },
224
+ ],
225
+ });
226
+ }
227
+ else {
228
+ const who = req.votes.map((v) => `${v.decision === "approve" ? "✅" : "⛔"} ${v.by}`).join(", ");
229
+ blocks.push({ type: "context", elements: [{ type: "mrkdwn", text: `${req.status.toUpperCase()} ${who}` }] });
230
+ }
231
+ return blocks;
232
+ }
233
+ /** Verifies Slack's request signature (v0 HMAC-SHA256, 5 minute replay window). */
234
+ export function verifySlackSignature(rawBody, timestamp, signature, signingSecret) {
235
+ if (!timestamp || !signature)
236
+ return false;
237
+ if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300)
238
+ return false;
239
+ const expected = "v0=" + createHmac("sha256", signingSecret).update(`v0:${timestamp}:${rawBody}`).digest("hex");
240
+ return safeEqual(expected, signature);
241
+ }
242
+ export function handleSlackInteraction(service, rawBody, consoleUrl) {
243
+ const payload = JSON.parse(new URLSearchParams(rawBody).get("payload") ?? "{}");
244
+ const action = payload.actions?.[0];
245
+ if (!action || (action.action_id !== "mandat_approve" && action.action_id !== "mandat_deny"))
246
+ return { replace_original: false, text: "ignored" };
247
+ const by = `slack:${payload.user?.username ?? payload.user?.id}`;
248
+ try {
249
+ const req = service.decide(action.value, { by, decision: action.action_id === "mandat_approve" ? "approve" : "deny", via: "slack" });
250
+ return { replace_original: true, text: `Approval ${req.status}`, blocks: slackBlocks(req, consoleUrl) };
251
+ }
252
+ catch (e) {
253
+ return { replace_original: false, text: `⚠️ ${e.message}` };
254
+ }
255
+ }
256
+ /** Dev channel: prints a hint to the control plane's stdout. */
257
+ export class ConsoleLogNotifier {
258
+ name = "log";
259
+ async notify(req) {
260
+ console.log(`[approval] ${req.id}: ${summary(req)} — approve with: mandat approvals approve ${req.id}`);
261
+ }
262
+ }
@@ -0,0 +1,38 @@
1
+ export interface BridgeOptions {
2
+ /** Protected MCP server URL. */
3
+ url: string;
4
+ /** Mandat control plane URL. */
5
+ mandatUrl: string;
6
+ agentId: string;
7
+ agentSecret: string;
8
+ agentVersion?: string;
9
+ /** User token from a real IdP. If absent, `subjectTokenProvider` or else the dev IdP (with `user`) is used. */
10
+ subjectToken?: string;
11
+ /** Returns a fresh user ID token, e.g. from the `mandat login` session. */
12
+ subjectTokenProvider?: () => Promise<string>;
13
+ user?: {
14
+ sub: string;
15
+ email?: string;
16
+ tenant?: string;
17
+ };
18
+ tools?: string[];
19
+ log?: (msg: string) => void;
20
+ }
21
+ export declare class Bridge {
22
+ private o;
23
+ private token;
24
+ private refreshing;
25
+ private mcpSessionId;
26
+ private sessionId;
27
+ private log;
28
+ constructor(o: BridgeOptions);
29
+ /** Returns a valid task token, exchanging a new one when the current one expires within 30 s. */
30
+ taskToken(force?: boolean): Promise<string>;
31
+ /** Forwards one JSON-RPC message; returns the response message (or null for notifications). */
32
+ forward(msg: {
33
+ id?: unknown;
34
+ method?: string;
35
+ }): Promise<unknown>;
36
+ /** Runs the stdio loop until stdin closes. Requests are handled concurrently (approvals can block). */
37
+ run(input?: NodeJS.ReadableStream, output?: NodeJS.WritableStream): Promise<void>;
38
+ }
package/dist/bridge.js ADDED
@@ -0,0 +1,117 @@
1
+ // stdio <-> Streamable HTTP bridge that lets any stdio MCP client (Claude Code, Codex, Cursor, Claude Desktop)
2
+ // talk to a Mandat-protected MCP server. It acts as the agent: it holds the agent credentials, exchanges the
3
+ // user's token for short-lived task tokens (RFC 8693) and transparently re-exchanges before they expire.
4
+ //
5
+ // stdout carries only MCP JSON-RPC messages (one per line); diagnostics go to stderr.
6
+ import { createInterface } from "node:readline";
7
+ import { decodeJwt } from "./sts/jwt.js";
8
+ import { devLogin, exchangeToken } from "./client.js";
9
+ import { randomId } from "./util.js";
10
+ export class Bridge {
11
+ o;
12
+ token = null;
13
+ refreshing = null;
14
+ mcpSessionId = null;
15
+ sessionId = randomId("sess_");
16
+ log;
17
+ constructor(o) {
18
+ this.o = o;
19
+ this.log = o.log ?? ((m) => process.stderr.write(`[mandat-bridge] ${m}\n`));
20
+ }
21
+ /** Returns a valid task token, exchanging a new one when the current one expires within 30 s. */
22
+ async taskToken(force = false) {
23
+ if (!force && this.token && this.token.exp - Date.now() / 1000 > 30)
24
+ return this.token.value;
25
+ this.refreshing ??= (async () => {
26
+ const subject = this.o.subjectToken
27
+ ?? (this.o.subjectTokenProvider ? await this.o.subjectTokenProvider() : await devLogin(this.o.mandatUrl, this.o.user ?? { sub: "user:local" }));
28
+ const details = this.o.tools?.length ? [{ type: "mandat_tool_call", tools: this.o.tools }] : undefined;
29
+ const t = await exchangeToken({
30
+ mandatUrl: this.o.mandatUrl,
31
+ clientId: this.o.agentId,
32
+ clientSecret: this.o.agentSecret,
33
+ subjectToken: subject,
34
+ resource: this.o.url,
35
+ agentVersion: this.o.agentVersion,
36
+ authorizationDetails: details,
37
+ sessionId: this.sessionId,
38
+ });
39
+ const exp = decodeJwt(t.access_token).payload.exp;
40
+ this.token = { value: t.access_token, exp };
41
+ this.log(`new task token for ${this.o.agentId}, valid ${t.expires_in}s`);
42
+ return t.access_token;
43
+ })().finally(() => { this.refreshing = null; });
44
+ return this.refreshing;
45
+ }
46
+ /** Forwards one JSON-RPC message; returns the response message (or null for notifications). */
47
+ async forward(msg) {
48
+ const send = async (token) => fetch(this.o.url, {
49
+ method: "POST",
50
+ headers: {
51
+ "content-type": "application/json",
52
+ accept: "application/json, text/event-stream",
53
+ authorization: `Bearer ${token}`,
54
+ ...(this.mcpSessionId ? { "mcp-session-id": this.mcpSessionId } : {}),
55
+ },
56
+ body: JSON.stringify(msg),
57
+ });
58
+ try {
59
+ let res = await send(await this.taskToken());
60
+ if (res.status === 401) {
61
+ // Expired or revoked: try once with a fresh token (a revoked agent fails at the exchange).
62
+ res = await send(await this.taskToken(true));
63
+ }
64
+ const sid = res.headers.get("mcp-session-id");
65
+ if (sid)
66
+ this.mcpSessionId = sid;
67
+ if (res.status === 202 || msg.id === undefined)
68
+ return null;
69
+ const text = await res.text();
70
+ if (!res.ok) {
71
+ let detail = text;
72
+ try {
73
+ detail = JSON.parse(text).error_description ?? text;
74
+ }
75
+ catch { /* keep raw */ }
76
+ return rpcError(msg.id, `Mandat: HTTP ${res.status}: ${detail}`);
77
+ }
78
+ return parseBody(text, res.headers.get("content-type") ?? "");
79
+ }
80
+ catch (e) {
81
+ this.log(e.message);
82
+ return msg.id === undefined ? null : rpcError(msg.id, `Mandat bridge: ${e.message}`);
83
+ }
84
+ }
85
+ /** Runs the stdio loop until stdin closes. Requests are handled concurrently (approvals can block). */
86
+ run(input = process.stdin, output = process.stdout) {
87
+ const rl = createInterface({ input, crlfDelay: Infinity });
88
+ const pending = new Set();
89
+ rl.on("line", (line) => {
90
+ if (!line.trim())
91
+ return;
92
+ let msg;
93
+ try {
94
+ msg = JSON.parse(line);
95
+ }
96
+ catch {
97
+ output.write(JSON.stringify({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } }) + "\n");
98
+ return;
99
+ }
100
+ const p = this.forward(msg).then((r) => { if (r)
101
+ output.write(JSON.stringify(r) + "\n"); });
102
+ pending.add(p);
103
+ void p.finally(() => pending.delete(p));
104
+ });
105
+ return new Promise((resolve) => rl.on("close", () => void Promise.allSettled([...pending]).then(() => resolve())));
106
+ }
107
+ }
108
+ function rpcError(id, message) {
109
+ return { jsonrpc: "2.0", id, error: { code: -32001, message } };
110
+ }
111
+ function parseBody(text, contentType) {
112
+ if (contentType.includes("text/event-stream")) {
113
+ const data = text.split("\n").filter((l) => l.startsWith("data:")).map((l) => l.slice(5).trim()).filter(Boolean);
114
+ return data.length ? JSON.parse(data[data.length - 1]) : null;
115
+ }
116
+ return text ? JSON.parse(text) : null;
117
+ }
@@ -0,0 +1,9 @@
1
+ export interface Args {
2
+ _: string[];
3
+ flags: Record<string, string | boolean>;
4
+ str(name: string, fallback?: string): string | undefined;
5
+ bool(name: string): boolean;
6
+ num(name: string, fallback: number): number;
7
+ list(name: string): string[];
8
+ }
9
+ export declare function parseArgs(argv: string[], booleans?: string[]): Args;
@@ -0,0 +1,39 @@
1
+ // Minimal argv parser: positionals, --flag, --key value, --key=value, -n 5.
2
+ export function parseArgs(argv, booleans = []) {
3
+ const _ = [];
4
+ const flags = {};
5
+ for (let i = 0; i < argv.length; i++) {
6
+ const a = argv[i];
7
+ if (a === "--") {
8
+ _.push(...argv.slice(i + 1));
9
+ break;
10
+ }
11
+ if (a.startsWith("--")) {
12
+ const eq = a.indexOf("=");
13
+ const key = eq > 0 ? a.slice(2, eq) : a.slice(2);
14
+ if (eq > 0)
15
+ flags[key] = a.slice(eq + 1);
16
+ else if (booleans.includes(key) || argv[i + 1] === undefined || argv[i + 1].startsWith("-"))
17
+ flags[key] = true;
18
+ else
19
+ flags[key] = argv[++i];
20
+ }
21
+ else if (a.startsWith("-") && a.length === 2) {
22
+ const key = a.slice(1);
23
+ if (booleans.includes(key) || argv[i + 1] === undefined || argv[i + 1].startsWith("-"))
24
+ flags[key] = true;
25
+ else
26
+ flags[key] = argv[++i];
27
+ }
28
+ else
29
+ _.push(a);
30
+ }
31
+ return {
32
+ _,
33
+ flags,
34
+ str: (n, f) => (typeof flags[n] === "string" ? flags[n] : f),
35
+ bool: (n) => flags[n] === true || flags[n] === "true",
36
+ num: (n, f) => (typeof flags[n] === "string" && !Number.isNaN(Number(flags[n])) ? Number(flags[n]) : f),
37
+ list: (n) => (typeof flags[n] === "string" ? flags[n].split(",").map((s) => s.trim()).filter(Boolean) : []),
38
+ };
39
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { main } from "./main.js";
3
+ main(process.argv.slice(2)).then((code) => { process.exitCode = code; }, (err) => { console.error(err instanceof Error ? err.message : err); process.exitCode = 1; });
@@ -0,0 +1 @@
1
+ export declare function main(argv: string[]): Promise<number>;