@mu-cabin/coding-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/dist/cli.js ADDED
@@ -0,0 +1,1181 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import * as fs6 from "fs";
5
+ import * as path5 from "path";
6
+ import { Command } from "commander";
7
+
8
+ // src/core/errors.ts
9
+ var EXIT = {
10
+ OK: 0,
11
+ INTERNAL: 1,
12
+ USAGE: 2,
13
+ AUTH: 3,
14
+ NOT_FOUND: 4,
15
+ REMOTE: 5,
16
+ CONFIG_MISSING: 6,
17
+ CONFIG_INVALID: 7,
18
+ FORBIDDEN: 8,
19
+ RATE_LIMIT: 9,
20
+ CONFIRM: 10,
21
+ VALIDATION: 11
22
+ };
23
+ var CliError = class extends Error {
24
+ exit;
25
+ body;
26
+ action;
27
+ requestId;
28
+ warnings;
29
+ constructor(exit, body, opts = {}) {
30
+ super(body.message);
31
+ this.name = "CliError";
32
+ this.exit = exit;
33
+ this.body = body;
34
+ this.action = opts.action;
35
+ this.requestId = opts.requestId;
36
+ this.warnings = opts.warnings && opts.warnings.length ? opts.warnings : void 0;
37
+ }
38
+ };
39
+ function exitForServerCode(code) {
40
+ switch (code) {
41
+ case "AuthFailure":
42
+ return EXIT.AUTH;
43
+ case "UnauthorizedOperation":
44
+ case "OperationDenied":
45
+ return EXIT.FORBIDDEN;
46
+ case "ResourceNotFound":
47
+ return EXIT.NOT_FOUND;
48
+ case "RequestLimitExceeded":
49
+ return EXIT.RATE_LIMIT;
50
+ default:
51
+ return EXIT.REMOTE;
52
+ }
53
+ }
54
+ function hintForServerCode(code) {
55
+ switch (code) {
56
+ case "AuthFailure":
57
+ return "Token is missing/invalid/revoked. Re-run `coding auth login`.";
58
+ case "UnauthorizedOperation":
59
+ return "Token is valid but its scope does not cover this action. Mint a token with the required scope in your instance's token settings.";
60
+ case "RequestLimitExceeded":
61
+ return "Rate limit is 30 req/s per team per action. Back off and pace your requests (the CLI does not auto-retry).";
62
+ default:
63
+ return void 0;
64
+ }
65
+ }
66
+
67
+ // src/core/envelope.ts
68
+ function success(data, opts = {}) {
69
+ const warnings = opts.warnings && opts.warnings.length ? opts.warnings : void 0;
70
+ return { ok: true, action: opts.action, requestId: opts.requestId, warnings, data };
71
+ }
72
+ function errorEnvelope(err) {
73
+ return { ok: false, action: err.action, requestId: err.requestId, warnings: err.warnings, error: err.body };
74
+ }
75
+ function compact(obj) {
76
+ const out = {};
77
+ for (const [k, v] of Object.entries(obj)) {
78
+ if (v !== void 0) out[k] = v;
79
+ }
80
+ return out;
81
+ }
82
+ function renderEnvelope(env, json) {
83
+ if (json) return JSON.stringify(compact(env));
84
+ if (env.ok) {
85
+ const head = env.action ? `\u2713 ${env.action}` : "\u2713 ok";
86
+ const warn = (env.warnings || []).map((w) => `\u26A0 ${w}`);
87
+ const body = typeof env.data === "string" ? env.data : JSON.stringify(env.data, null, 2);
88
+ return [head, ...warn, body].join("\n");
89
+ }
90
+ const e = env.error;
91
+ const label = e.code || e.type || "error";
92
+ const lines = [`\u2717 ${label}: ${e.message}`];
93
+ for (const w of env.warnings || []) lines.push(` \u26A0 ${w}`);
94
+ if (e.risk) lines.push(` risk: ${e.risk.level} (${e.risk.action})`);
95
+ if (e.hint) lines.push(` hint: ${e.hint}`);
96
+ if (env.requestId) lines.push(` requestId: ${env.requestId}`);
97
+ return lines.join("\n");
98
+ }
99
+
100
+ // src/core/config.ts
101
+ import * as fs from "fs";
102
+ import * as os from "os";
103
+ import * as path from "path";
104
+ var DEFAULT_BASE = "http://coding.example.com/open-api";
105
+ var DEFAULT_PROFILE = "default";
106
+ function configDir() {
107
+ return process.env.CODING_CONFIG_DIR || path.join(os.homedir(), ".coding");
108
+ }
109
+ function configPath() {
110
+ return path.join(configDir(), "config.json");
111
+ }
112
+ function credentialsPath() {
113
+ return path.join(configDir(), "credentials.json");
114
+ }
115
+ function readJson(file) {
116
+ if (!fs.existsSync(file)) return null;
117
+ let text;
118
+ try {
119
+ text = fs.readFileSync(file, "utf8");
120
+ } catch (e) {
121
+ throw new CliError(EXIT.CONFIG_INVALID, { type: "config_invalid", message: `Cannot read ${file}: ${e.message}` });
122
+ }
123
+ try {
124
+ return JSON.parse(text);
125
+ } catch {
126
+ throw new CliError(EXIT.CONFIG_INVALID, {
127
+ type: "config_invalid",
128
+ message: `Malformed JSON in ${file}`,
129
+ hint: "Fix the file by hand or re-run `coding config init`."
130
+ });
131
+ }
132
+ }
133
+ function asObject(v) {
134
+ return v && typeof v === "object" && !Array.isArray(v) ? v : void 0;
135
+ }
136
+ function configInvalid(file, detail) {
137
+ return new CliError(EXIT.CONFIG_INVALID, { type: "config_invalid", message: `Invalid config (${file}): ${detail}`, hint: "Fix the file by hand or re-run `coding config init`." });
138
+ }
139
+ function loadConfig() {
140
+ const file = configPath();
141
+ const raw = readJson(file);
142
+ if (raw === null) return null;
143
+ const obj = asObject(raw);
144
+ if (!obj) throw configInvalid(file, "top-level value must be a JSON object");
145
+ if (obj.activeProfile !== void 0 && typeof obj.activeProfile !== "string") throw configInvalid(file, "activeProfile must be a string");
146
+ const profiles = asObject(obj.profiles) ?? {};
147
+ for (const [name, p] of Object.entries(profiles)) {
148
+ const po = asObject(p);
149
+ if (!po) throw configInvalid(file, `profile "${name}" must be an object`);
150
+ if (po.baseUrl !== void 0 && typeof po.baseUrl !== "string") throw configInvalid(file, `profile "${name}".baseUrl must be a string`);
151
+ }
152
+ return obj;
153
+ }
154
+ function loadCredentials() {
155
+ const file = credentialsPath();
156
+ const raw = readJson(file);
157
+ if (raw === null) return null;
158
+ const obj = asObject(raw);
159
+ if (!obj) throw configInvalid(file, "top-level value must be a JSON object");
160
+ const profiles = asObject(obj.profiles) ?? {};
161
+ for (const [name, p] of Object.entries(profiles)) {
162
+ const po = asObject(p);
163
+ if (!po) throw configInvalid(file, `profile "${name}" must be an object`);
164
+ if (po.token !== void 0 && typeof po.token !== "string") throw configInvalid(file, `profile "${name}".token must be a string`);
165
+ }
166
+ return obj;
167
+ }
168
+ function insecureHttpWarning(baseUrl) {
169
+ if (process.env.CODING_ALLOW_HTTP) return void 0;
170
+ let u;
171
+ try {
172
+ u = new URL(baseUrl);
173
+ } catch {
174
+ return void 0;
175
+ }
176
+ if (u.protocol === "https:") return void 0;
177
+ if (u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "::1") return void 0;
178
+ return `Access token is being sent over plaintext ${u.protocol}// to ${u.host}. Prefer HTTPS if your instance supports it, or set CODING_ALLOW_HTTP=1 to silence this warning.`;
179
+ }
180
+ function resolveProfileName(cliProfile) {
181
+ return cliProfile || process.env.CODING_PROFILE || loadConfig()?.activeProfile || DEFAULT_PROFILE;
182
+ }
183
+ function normalizeBaseUrl(url) {
184
+ return url.replace(/\/+$/, "");
185
+ }
186
+ function resolveBaseUrl(opts) {
187
+ const raw = opts.cliBaseUrl || process.env.CODING_API_BASE || loadConfig()?.profiles?.[opts.profile]?.baseUrl || DEFAULT_BASE;
188
+ return normalizeBaseUrl(raw);
189
+ }
190
+ function resolveToken(profile) {
191
+ const fromEnv = process.env.CODING_TOKEN;
192
+ if (fromEnv && fromEnv.trim()) return fromEnv.trim();
193
+ const fromFile = loadCredentials()?.profiles?.[profile]?.token;
194
+ if (fromFile && fromFile.trim()) return fromFile.trim();
195
+ throw new CliError(EXIT.AUTH, {
196
+ type: "auth_required",
197
+ message: `No token for profile "${profile}".`,
198
+ hint: "Run `coding auth login` (in your own terminal) or set CODING_TOKEN."
199
+ });
200
+ }
201
+ function ensureDir() {
202
+ fs.mkdirSync(configDir(), { recursive: true });
203
+ }
204
+ function writeConfig(cfg) {
205
+ ensureDir();
206
+ fs.writeFileSync(configPath(), JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
207
+ }
208
+ function writeCredentials(creds) {
209
+ ensureDir();
210
+ fs.writeFileSync(credentialsPath(), JSON.stringify(creds, null, 2) + "\n", { mode: 384 });
211
+ try {
212
+ fs.chmodSync(credentialsPath(), 384);
213
+ } catch {
214
+ }
215
+ }
216
+ function storeToken(profile, token) {
217
+ const creds = loadCredentials() || { version: 1, profiles: {} };
218
+ creds.version = creds.version || 1;
219
+ creds.profiles = creds.profiles || {};
220
+ creds.profiles[profile] = { ...creds.profiles[profile], token };
221
+ writeCredentials(creds);
222
+ }
223
+ function initConfig(profile, baseUrl) {
224
+ const existing = loadConfig();
225
+ const cfg = existing || { version: 1, activeProfile: profile, profiles: {} };
226
+ cfg.version = cfg.version || 1;
227
+ cfg.activeProfile = profile;
228
+ cfg.profiles = cfg.profiles || {};
229
+ cfg.profiles[profile] = { ...cfg.profiles[profile], baseUrl: normalizeBaseUrl(baseUrl) };
230
+ writeConfig(cfg);
231
+ return cfg;
232
+ }
233
+
234
+ // src/core/paths.ts
235
+ import * as fs2 from "fs";
236
+ import * as path2 from "path";
237
+ import { fileURLToPath } from "url";
238
+ var HERE = path2.dirname(fileURLToPath(import.meta.url));
239
+ function packageRoot(start = HERE) {
240
+ let dir = start;
241
+ for (let i = 0; i < 8; i++) {
242
+ if (fs2.existsSync(path2.join(dir, "package.json"))) return dir;
243
+ const parent = path2.dirname(dir);
244
+ if (parent === dir) break;
245
+ dir = parent;
246
+ }
247
+ return start;
248
+ }
249
+
250
+ // src/commands/config.ts
251
+ function cmdConfigInit(ctx, baseUrl) {
252
+ const profile = ctx.profile;
253
+ const url = baseUrl || ctx.cliBaseUrl || DEFAULT_BASE;
254
+ initConfig(profile, url);
255
+ return { data: { path: configPath(), activeProfile: profile, baseUrl: url } };
256
+ }
257
+ function cmdConfigShow(ctx) {
258
+ const profile = resolveProfileName(ctx.profile);
259
+ let tokenPresent = false;
260
+ try {
261
+ resolveToken(profile);
262
+ tokenPresent = true;
263
+ } catch (e) {
264
+ if (!(e instanceof CliError && e.exit === EXIT.AUTH)) throw e;
265
+ }
266
+ return {
267
+ data: {
268
+ configPath: configPath(),
269
+ credentialsPath: credentialsPath(),
270
+ activeProfile: profile,
271
+ baseUrl: resolveBaseUrl({ cliBaseUrl: ctx.cliBaseUrl, profile }),
272
+ tokenPresent
273
+ }
274
+ };
275
+ }
276
+
277
+ // src/core/credentials.ts
278
+ import * as readline from "readline";
279
+ function readStdin() {
280
+ return new Promise((resolve, reject) => {
281
+ let buf = "";
282
+ process.stdin.setEncoding("utf8");
283
+ process.stdin.on("data", (chunk) => buf += chunk);
284
+ process.stdin.on("end", () => resolve(buf));
285
+ process.stdin.on("error", reject);
286
+ });
287
+ }
288
+ function promptHidden(prompt) {
289
+ if (!process.stdin.isTTY) {
290
+ throw new CliError(EXIT.USAGE, {
291
+ type: "no_tty",
292
+ message: "Cannot prompt for a token without an interactive terminal.",
293
+ hint: "Run `coding auth login` in your own terminal, or pipe the token via `coding auth login --token -`."
294
+ });
295
+ }
296
+ return new Promise((resolve, reject) => {
297
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
298
+ const rlAny = rl;
299
+ let muted = false;
300
+ rlAny._writeToOutput = (s) => {
301
+ if (!muted) process.stdout.write(s);
302
+ };
303
+ process.stdout.write(prompt);
304
+ muted = true;
305
+ rl.question("", (answer) => {
306
+ muted = false;
307
+ rl.close();
308
+ process.stdout.write("\n");
309
+ resolve(answer.trim());
310
+ });
311
+ rl.on("error", reject);
312
+ });
313
+ }
314
+
315
+ // src/core/http.ts
316
+ function actionUrl(baseUrl, action) {
317
+ return `${baseUrl}/?action=${encodeURIComponent(action)}`;
318
+ }
319
+ function buildDryRun(baseUrl, action, body) {
320
+ return {
321
+ dryRun: true,
322
+ request: {
323
+ method: "POST",
324
+ url: actionUrl(baseUrl, action),
325
+ headers: { Authorization: "token <redacted>", "Content-Type": "application/json" },
326
+ body
327
+ }
328
+ };
329
+ }
330
+ async function callAction(opts) {
331
+ const url = actionUrl(opts.baseUrl, opts.action);
332
+ const controller = new AbortController();
333
+ const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 6e4);
334
+ let res;
335
+ let text;
336
+ try {
337
+ res = await fetch(url, {
338
+ method: "POST",
339
+ headers: {
340
+ Authorization: `token ${opts.token}`,
341
+ "Content-Type": "application/json",
342
+ Accept: "application/json"
343
+ },
344
+ body: JSON.stringify(opts.body ?? {}),
345
+ signal: controller.signal
346
+ });
347
+ text = await res.text();
348
+ } catch (e) {
349
+ const msg = e.name === "AbortError" ? "Request timed out" : `Request failed: ${e.message}`;
350
+ throw new CliError(EXIT.REMOTE, { type: "network", message: msg }, { action: opts.action });
351
+ } finally {
352
+ clearTimeout(timer);
353
+ }
354
+ let json;
355
+ try {
356
+ json = JSON.parse(text);
357
+ } catch {
358
+ throw new CliError(
359
+ EXIT.REMOTE,
360
+ { type: "bad_response", message: `Non-JSON response (HTTP ${res.status}): ${text.slice(0, 200)}` },
361
+ { action: opts.action }
362
+ );
363
+ }
364
+ const root = json && typeof json === "object" && !Array.isArray(json) ? json : void 0;
365
+ const resp = root?.Response;
366
+ if (!resp || typeof resp !== "object" || Array.isArray(resp)) {
367
+ throw new CliError(
368
+ EXIT.REMOTE,
369
+ { type: "bad_response", message: `Unexpected response shape (HTTP ${res.status}); missing "Response" object: ${text.slice(0, 200)}` },
370
+ { action: opts.action }
371
+ );
372
+ }
373
+ const respObj = resp;
374
+ const requestId = respObj.RequestId;
375
+ const errObj = respObj.Error;
376
+ if (errObj && typeof errObj === "object") {
377
+ const code = errObj.Code || "UnknownError";
378
+ const message = errObj.Message || "Unknown error";
379
+ throw new CliError(exitForServerCode(code), { code, message, hint: hintForServerCode(code) }, { action: opts.action, requestId });
380
+ }
381
+ const data = { ...respObj };
382
+ delete data.RequestId;
383
+ return { action: opts.action, requestId, data };
384
+ }
385
+
386
+ // src/commands/auth.ts
387
+ async function cmdAuthLogin(ctx, fromStdin) {
388
+ const token = (fromStdin ? await readStdin() : await promptHidden("CODING token: ")).trim();
389
+ if (!token) {
390
+ throw new CliError(EXIT.USAGE, { type: "empty_token", message: "No token provided." });
391
+ }
392
+ storeToken(ctx.profile, token);
393
+ return { data: { activeProfile: ctx.profile, credentialsPath: credentialsPath(), stored: true } };
394
+ }
395
+ async function cmdAuthStatus(ctx) {
396
+ const baseUrl = resolveBaseUrl({ cliBaseUrl: ctx.cliBaseUrl, profile: ctx.profile });
397
+ const token = resolveToken(ctx.profile);
398
+ const warn = insecureHttpWarning(baseUrl);
399
+ const res = await callAction({ baseUrl, token, action: "DescribeCodingCurrentUser", body: {} });
400
+ return {
401
+ action: res.action,
402
+ requestId: res.requestId,
403
+ warnings: warn ? [warn] : void 0,
404
+ data: { profile: ctx.profile, baseUrl, user: res.data.User }
405
+ };
406
+ }
407
+
408
+ // src/core/indexLoader.ts
409
+ import * as fs3 from "fs";
410
+ import * as path3 from "path";
411
+ function findIndexFile() {
412
+ const candidate = path3.join(packageRoot(), "generated", "index.json");
413
+ return fs3.existsSync(candidate) ? candidate : null;
414
+ }
415
+ var cached = null;
416
+ function loadIndex() {
417
+ if (cached) return cached;
418
+ const file = findIndexFile();
419
+ if (!file) {
420
+ throw new CliError(EXIT.INTERNAL, {
421
+ type: "index_missing",
422
+ message: "Bundled action index (generated/index.json) not found.",
423
+ hint: "Run `npm run regen` to build it from the OpenAPI spec."
424
+ });
425
+ }
426
+ try {
427
+ cached = JSON.parse(fs3.readFileSync(file, "utf8"));
428
+ } catch (e) {
429
+ throw new CliError(EXIT.INTERNAL, { type: "index_invalid", message: `Cannot parse action index: ${e.message}` });
430
+ }
431
+ return cached;
432
+ }
433
+ function getAction(action) {
434
+ return loadIndex().actions[action];
435
+ }
436
+
437
+ // src/commands/discover.ts
438
+ function cmdCategories(_ctx) {
439
+ return { data: loadIndex().categories };
440
+ }
441
+ function cmdActions(_ctx, opts) {
442
+ const index = loadIndex();
443
+ const kw = opts.search?.toLowerCase();
444
+ const cat = opts.category?.toLowerCase();
445
+ const list = Object.entries(index.actions).filter(([name, meta]) => {
446
+ if (kw && !(name.toLowerCase().includes(kw) || (meta.summary || "").toLowerCase().includes(kw) || (meta.description || "").toLowerCase().includes(kw))) {
447
+ return false;
448
+ }
449
+ if (cat && !meta.category.toLowerCase().includes(cat)) return false;
450
+ return true;
451
+ }).map(([name, meta]) => ({ action: name, summary: meta.summary, category: meta.category, risk: meta.risk })).sort((a, b) => a.action.localeCompare(b.action));
452
+ return { data: { count: list.length, actions: list } };
453
+ }
454
+ function cmdSchema(_ctx, action) {
455
+ const meta = getAction(action);
456
+ if (!meta) {
457
+ throw new CliError(EXIT.NOT_FOUND, {
458
+ type: "unknown_action",
459
+ message: `Action "${action}" is not in the bundled index.`,
460
+ hint: "Search with `coding actions --search <keyword>` (your self-hosted instance may still accept it; use `coding call ... --force`)."
461
+ });
462
+ }
463
+ return { data: { action, ...meta } };
464
+ }
465
+
466
+ // src/commands/call.ts
467
+ import * as fs4 from "fs";
468
+
469
+ // src/core/risk.ts
470
+ var DESTRUCTIVE = ["Delete", "Remove", "Disable", "Revoke", "Reset", "Clear", "Stop", "Terminate", "Unbind", "Archive", "Cancel", "Kill", "Destroy"];
471
+ var WRITE = ["Create", "Modify", "Update", "Add", "Set", "Bind", "Enable", "Start", "Trigger", "Import", "Generate", "Move", "Copy", "Apply", "Submit", "Publish", "Release", "Rename", "Transfer", "Merge", "Close", "Open", "Restore", "Sync", "Upload", "Send", "Batch"];
472
+ var READ = ["Describe", "List", "Get", "Query", "Check", "Search", "Preview", "Export", "Download", "Count", "Has", "Is", "Fetch"];
473
+ function startsWithVerb(action, verbs) {
474
+ return verbs.some((v) => action.startsWith(v));
475
+ }
476
+ function hasKnownVerb(action) {
477
+ return startsWithVerb(action, DESTRUCTIVE) || startsWithVerb(action, READ) || startsWithVerb(action, WRITE);
478
+ }
479
+ function classifyRisk(action) {
480
+ if (startsWithVerb(action, DESTRUCTIVE)) return "destructive";
481
+ if (startsWithVerb(action, READ)) return "read";
482
+ if (startsWithVerb(action, WRITE)) return "write";
483
+ return "write";
484
+ }
485
+ var VALID_LEVELS = /* @__PURE__ */ new Set(["read", "write", "destructive"]);
486
+ function effectiveRisk(action, indexed) {
487
+ if (indexed && VALID_LEVELS.has(indexed)) return indexed;
488
+ if (!hasKnownVerb(action)) return "destructive";
489
+ return classifyRisk(action);
490
+ }
491
+ function enforceGate(action, risk, yes) {
492
+ if (risk === "destructive" && !yes) {
493
+ const info = { level: risk, action };
494
+ throw new CliError(
495
+ EXIT.CONFIRM,
496
+ {
497
+ type: "confirmation_required",
498
+ message: `${action} is a destructive operation and requires confirmation`,
499
+ hint: "Re-run with --yes after the user explicitly approves. Use --dry-run first to preview the request.",
500
+ risk: info
501
+ },
502
+ { action }
503
+ );
504
+ }
505
+ }
506
+
507
+ // src/core/validate.ts
508
+ function tryJson(v) {
509
+ try {
510
+ const parsed = JSON.parse(v);
511
+ if (typeof parsed === "number" && !Number.isFinite(parsed)) return v;
512
+ return parsed;
513
+ } catch {
514
+ return v;
515
+ }
516
+ }
517
+ function coerceParam(value, type) {
518
+ if (!type) return tryJson(value);
519
+ const t = type.toLowerCase();
520
+ const blank = value.trim() === "";
521
+ if (t.startsWith("integer")) {
522
+ if (blank) return value;
523
+ const n = Number(value);
524
+ return Number.isInteger(n) ? n : value;
525
+ }
526
+ if (t.startsWith("number")) {
527
+ if (blank) return value;
528
+ const n = Number(value);
529
+ return Number.isFinite(n) ? n : value;
530
+ }
531
+ if (t.startsWith("boolean") || t.startsWith("bool")) {
532
+ if (value === "true" || value === "1") return true;
533
+ if (value === "false" || value === "0") return false;
534
+ return value;
535
+ }
536
+ if (t.startsWith("string")) return value;
537
+ return tryJson(value);
538
+ }
539
+ function mergeParams(base, pairs, meta) {
540
+ const out = { ...base };
541
+ const types = /* @__PURE__ */ new Map();
542
+ if (meta) for (const p of meta.params) types.set(p.name, p.type);
543
+ for (const pair of pairs) {
544
+ const eq = pair.indexOf("=");
545
+ if (eq < 0) {
546
+ throw new Error(`Invalid --param "${pair}" (expected Key=Value)`);
547
+ }
548
+ const key = pair.slice(0, eq);
549
+ const raw = pair.slice(eq + 1);
550
+ out[key] = coerceParam(raw, types.get(key));
551
+ }
552
+ return out;
553
+ }
554
+ function validateCall(action, body, meta) {
555
+ const problems = [];
556
+ if (!meta) {
557
+ problems.push({ kind: "unknown_action", detail: `Action "${action}" is not in the bundled index (possible typo, or your instance has it but the public spec does not).` });
558
+ return problems;
559
+ }
560
+ const known = new Set(meta.params.map((p) => p.name));
561
+ for (const p of meta.params) {
562
+ if (p.required && !(p.name in body)) {
563
+ problems.push({ kind: "missing_required", detail: `Missing required param "${p.name}" (${p.type}).` });
564
+ }
565
+ }
566
+ for (const key of Object.keys(body)) {
567
+ if (!known.has(key)) {
568
+ problems.push({ kind: "unknown_param", detail: `Unknown param "${key}".` });
569
+ }
570
+ }
571
+ return problems;
572
+ }
573
+
574
+ // src/core/select.ts
575
+ function getByPath(value, path6) {
576
+ const parts = path6.split(".").filter((p) => p.length > 0);
577
+ let cur = value;
578
+ for (let i = 0; i < parts.length; i++) {
579
+ const m = parts[i].match(/^(.*?)(\[(\d*)\])?$/);
580
+ const key = m ? m[1] : parts[i];
581
+ const hasBracket = !!(m && m[2] !== void 0);
582
+ const idxStr = m ? m[3] : void 0;
583
+ if (key !== "") {
584
+ if (cur == null || typeof cur !== "object") return void 0;
585
+ cur = cur[key];
586
+ }
587
+ if (hasBracket) {
588
+ if (!Array.isArray(cur)) return void 0;
589
+ if (idxStr === "" || idxStr === void 0) {
590
+ const restPath = parts.slice(i + 1).join(".");
591
+ return cur.map((item) => restPath ? getByPath(item, restPath) : item);
592
+ }
593
+ cur = cur[Number(idxStr)];
594
+ }
595
+ }
596
+ return cur;
597
+ }
598
+ function applySelect(data, paths) {
599
+ const out = {};
600
+ for (const p of paths) out[p] = getByPath(data, p);
601
+ return out;
602
+ }
603
+ function applyLimit(data, n) {
604
+ if (n < 0) return data;
605
+ if (Array.isArray(data)) return data.slice(0, n);
606
+ if (data && typeof data === "object") {
607
+ const out = {};
608
+ for (const [k, v] of Object.entries(data)) {
609
+ out[k] = Array.isArray(v) ? v.slice(0, n) : v;
610
+ }
611
+ return out;
612
+ }
613
+ return data;
614
+ }
615
+
616
+ // src/commands/call.ts
617
+ async function readBody(data) {
618
+ if (!data) return {};
619
+ let text;
620
+ if (data === "-") text = await readStdin();
621
+ else if (data.startsWith("@")) {
622
+ const file = data.slice(1);
623
+ try {
624
+ text = fs4.readFileSync(file, "utf8");
625
+ } catch (e) {
626
+ throw new CliError(EXIT.USAGE, { type: "bad_data", message: `Cannot read --data file "${file}": ${e.message}` });
627
+ }
628
+ } else text = data;
629
+ text = text.trim();
630
+ if (!text) return {};
631
+ let parsed;
632
+ try {
633
+ parsed = JSON.parse(text);
634
+ } catch (e) {
635
+ throw new CliError(EXIT.USAGE, { type: "bad_data", message: `--data is not valid JSON: ${e.message}` });
636
+ }
637
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
638
+ throw new CliError(EXIT.USAGE, { type: "bad_data", message: "--data must be a JSON object." });
639
+ }
640
+ return parsed;
641
+ }
642
+ async function cmdCall(ctx, action, opts) {
643
+ const meta = getAction(action);
644
+ const base = await readBody(opts.data);
645
+ let body;
646
+ try {
647
+ body = mergeParams(base, opts.param, meta);
648
+ } catch (e) {
649
+ throw new CliError(EXIT.USAGE, { type: "bad_param", message: e.message });
650
+ }
651
+ const warnings = [];
652
+ const problems = validateCall(action, body, meta);
653
+ if (problems.length) {
654
+ if (opts.force) {
655
+ for (const p of problems) warnings.push(p.detail);
656
+ } else {
657
+ throw new CliError(EXIT.VALIDATION, {
658
+ type: "validation_failed",
659
+ message: problems.map((p) => p.detail).join(" "),
660
+ hint: "Fix the params, or pass --force/--no-validate to send anyway (e.g. for an action your self-hosted instance has but the public spec lacks)."
661
+ });
662
+ }
663
+ }
664
+ const baseUrl = resolveBaseUrl({ cliBaseUrl: ctx.cliBaseUrl, profile: ctx.profile });
665
+ if (opts.dryRun) {
666
+ return { action, data: buildDryRun(baseUrl, action, body), warnings };
667
+ }
668
+ try {
669
+ const risk = effectiveRisk(action, meta?.risk);
670
+ enforceGate(action, risk, opts.yes);
671
+ const insecure = insecureHttpWarning(baseUrl);
672
+ if (insecure) warnings.push(insecure);
673
+ const token = resolveToken(ctx.profile);
674
+ const res = await callAction({ baseUrl, token, action, body });
675
+ let data = res.data;
676
+ if (opts.select) {
677
+ const paths = opts.select.split(",").map((s) => s.trim()).filter(Boolean);
678
+ if (paths.length) data = applySelect(data, paths);
679
+ }
680
+ if (opts.limit !== void 0) data = applyLimit(data, opts.limit);
681
+ return { action: res.action, requestId: res.requestId, data, warnings };
682
+ } catch (e) {
683
+ if (e instanceof CliError && warnings.length && !e.warnings) e.warnings = warnings;
684
+ throw e;
685
+ }
686
+ }
687
+
688
+ // src/core/specBuild.ts
689
+ import * as fs5 from "fs";
690
+ import * as path4 from "path";
691
+ import { parse as parseYaml } from "yaml";
692
+ var DOCS_PAGE = "https://coding.net/help/openapi";
693
+ var NEW_BASE = DEFAULT_BASE;
694
+ var NEW_HOST = new URL(DEFAULT_BASE).host;
695
+ var OLD_BASE = "https://e.coding.net/open-api";
696
+ var OLD_HOST = "coding.tencentcloudapi.com";
697
+ var COMMON_ERROR_CODES = [
698
+ ["AuthFailure", "\u9274\u6743\u5931\u8D25\u3002"],
699
+ ["DryRunOperation", "DryRun \u64CD\u4F5C\uFF0C\u4EE3\u8868\u8BF7\u6C42\u5C06\u4F1A\u662F\u6210\u529F\u7684\uFF0C\u53EA\u662F\u591A\u4F20\u4E86 DryRun \u53C2\u6570\u3002"],
700
+ ["FailedOperation", "\u64CD\u4F5C\u5931\u8D25\u3002"],
701
+ ["InternalError", "\u5185\u90E8\u9519\u8BEF\u3002"],
702
+ ["InvalidParameter", "\u53C2\u6570\u9519\u8BEF\u3002"],
703
+ ["InvalidParameterValue", "\u53C2\u6570\u53D6\u503C\u9519\u8BEF\u3002"],
704
+ ["LimitExceeded", "\u8D85\u8FC7\u914D\u989D\u9650\u5236\u3002"],
705
+ ["MissingParameter", "\u7F3A\u5C11\u53C2\u6570\u9519\u8BEF\u3002"],
706
+ ["OperationDenied", "\u64CD\u4F5C\u88AB\u62D2\u7EDD\u3002"],
707
+ ["RequestLimitExceeded", "\u8BF7\u6C42\u7684\u6B21\u6570\u8D85\u8FC7\u4E86\u9891\u7387\u9650\u5236\u3002"],
708
+ ["ResourceInUse", "\u8D44\u6E90\u88AB\u5360\u7528\u3002"],
709
+ ["ResourceInsufficient", "\u8D44\u6E90\u4E0D\u8DB3\u3002"],
710
+ ["ResourceNotFound", "\u8D44\u6E90\u4E0D\u5B58\u5728\u3002"],
711
+ ["ResourceUnavailable", "\u8D44\u6E90\u4E0D\u53EF\u7528\u3002"],
712
+ ["ResourcesSoldOut", "\u8D44\u6E90\u552E\u7F44\u3002"],
713
+ ["UnauthorizedOperation", "\u672A\u6388\u6743\u64CD\u4F5C\u3002"],
714
+ ["UnknownParameter", "\u672A\u77E5\u53C2\u6570\u9519\u8BEF\u3002"],
715
+ ["UnsupportedOperation", "\u64CD\u4F5C\u4E0D\u652F\u6301\u3002"]
716
+ ];
717
+ var COMMON_ERROR_SET = new Set(COMMON_ERROR_CODES.map(([c]) => c));
718
+ var ERROR_CODE_FIXES = { "Missing\u6781arameter": "MissingParameter" };
719
+ var VALID_RISK = /* @__PURE__ */ new Set(["read", "write", "destructive"]);
720
+ function rewrite(text) {
721
+ if (!text) return text ?? "";
722
+ return text.split(OLD_BASE).join(NEW_BASE).split(OLD_HOST).join(NEW_HOST);
723
+ }
724
+ function cell(text) {
725
+ if (text === null || text === void 0) return "";
726
+ let s = String(text).trim();
727
+ s = s.split("|").join("\\|").replace(/\r\n/g, "\n").replace(/\n/g, "<br>");
728
+ return rewrite(s);
729
+ }
730
+ function schemaAnchor(name) {
731
+ return "schema-" + name;
732
+ }
733
+ function refName(ref) {
734
+ return ref.split("/").pop();
735
+ }
736
+ function typeOf(prop, link) {
737
+ if (!prop || typeof prop !== "object") return "";
738
+ if (prop.$ref) {
739
+ const n = refName(prop.$ref);
740
+ return link ? `[${n}](#${schemaAnchor(n)})` : n;
741
+ }
742
+ const t = prop.type;
743
+ if (t === "array") {
744
+ const items = prop.items || {};
745
+ if (items.$ref) {
746
+ const n = refName(items.$ref);
747
+ return link ? `Array of [${n}](#${schemaAnchor(n)})` : `Array of ${n}`;
748
+ }
749
+ return `Array of ${items.type || "object"}`;
750
+ }
751
+ if (t) return prop.format ? `${t} (${prop.format})` : t;
752
+ if (prop.properties) return "object";
753
+ return "";
754
+ }
755
+ function collectRefs(schema, schemas, seen) {
756
+ if (Array.isArray(schema)) {
757
+ for (const item of schema) collectRefs(item, schemas, seen);
758
+ return;
759
+ }
760
+ if (schema && typeof schema === "object") {
761
+ if (schema.$ref) {
762
+ const n = refName(schema.$ref);
763
+ if (!seen.has(n) && schemas[n]) {
764
+ seen.add(n);
765
+ collectRefs(schemas[n], schemas, seen);
766
+ }
767
+ }
768
+ for (const [k, v] of Object.entries(schema)) {
769
+ if (k === "$ref") continue;
770
+ collectRefs(v, schemas, seen);
771
+ }
772
+ }
773
+ }
774
+ function paramsTable(properties, required, lines) {
775
+ const keys = Object.keys(properties || {});
776
+ if (keys.length === 0) {
777
+ lines.push("_\u65E0_\n");
778
+ return;
779
+ }
780
+ lines.push("| \u53C2\u6570\u540D | \u7C7B\u578B | \u5FC5\u9009 | \u63CF\u8FF0 |");
781
+ lines.push("| --- | --- | --- | --- |");
782
+ for (const name of keys) {
783
+ const prop = properties[name] || {};
784
+ const req = required.has(name) ? "\u662F" : "\u5426";
785
+ lines.push(`| ${cell(name)} | ${typeOf(prop, true)} | ${req} | ${cell(prop.description)} |`);
786
+ }
787
+ lines.push("");
788
+ }
789
+ function paramMetas(properties, required) {
790
+ return Object.keys(properties || {}).map((name) => {
791
+ const prop = properties[name] || {};
792
+ return { name, type: typeOf(prop, false), required: required.has(name), description: prop.description ? rewrite(String(prop.description)).trim() : void 0 };
793
+ });
794
+ }
795
+ function reqSchemaOf(op) {
796
+ return op?.requestBody?.content?.["application/json"]?.schema || {};
797
+ }
798
+ function respPropsOf(op) {
799
+ const respSchema = op?.responses?.["200"]?.content?.["application/json"]?.schema || {};
800
+ return respSchema?.properties?.Response || {};
801
+ }
802
+ function specificErrors(op) {
803
+ const errs = (op["x-tcapi-errorcodes"] || []).map((e) => ERROR_CODE_FIXES[e] || e);
804
+ return [...new Set(errs)].filter((e) => !COMMON_ERROR_SET.has(e));
805
+ }
806
+ function renderAction(key, op, schemas, refAcc, lines) {
807
+ const action = op.operationId || key;
808
+ const title = op.summary || op["x-tcapi-action-name"] || action;
809
+ lines.push(`<a id="action-${action}"></a>`);
810
+ lines.push(`## ${title}\uFF08${action}\uFF09
811
+ `);
812
+ lines.push(`**\u63A5\u53E3\u8BF7\u6C42\u65B9\u5F0F\u4E0E\u5730\u5740\uFF1A** \`POST ${NEW_BASE}/?action=${action}\`
813
+ `);
814
+ if (op.description) lines.push(rewrite(op.description).trim() + "\n");
815
+ const reqSchema = reqSchemaOf(op);
816
+ lines.push("### \u8BF7\u6C42\u53C2\u6570\n");
817
+ paramsTable(reqSchema.properties || {}, new Set(reqSchema.required || []), lines);
818
+ collectRefs(reqSchema, schemas, refAcc);
819
+ const respProps = respPropsOf(op);
820
+ const outRequired = new Set(respProps["x-tcapi-output-required"] || []);
821
+ lines.push("### \u8FD4\u56DE\u53C2\u6570\n");
822
+ paramsTable(respProps.properties || {}, outRequired, lines);
823
+ const respSchema = op?.responses?.["200"]?.content?.["application/json"]?.schema || {};
824
+ collectRefs(respSchema, schemas, refAcc);
825
+ const errs = (op["x-tcapi-errorcodes"] || []).map((e) => ERROR_CODE_FIXES[e] || e);
826
+ const specific = specificErrors(op);
827
+ if (errs.length) {
828
+ lines.push("### \u9519\u8BEF\u7801\n");
829
+ if (specific.length) {
830
+ lines.push("\u672C\u63A5\u53E3\u7279\u6709\u9519\u8BEF\u7801\uFF1A\n");
831
+ lines.push(specific.map((e) => `\`${e}\``).join(", "));
832
+ lines.push("\n\u6B64\u5916\u8FD8\u53EF\u80FD\u8FD4\u56DE[\u516C\u5171\u9519\u8BEF\u7801](./README.md#\u516C\u5171\u9519\u8BEF\u7801)\u3002\n");
833
+ } else {
834
+ lines.push("\u672C\u63A5\u53E3\u53EF\u80FD\u8FD4\u56DE\u7684\u9519\u8BEF\u7801\u5747\u4E3A[\u516C\u5171\u9519\u8BEF\u7801](./README.md#\u516C\u5171\u9519\u8BEF\u7801)\u3002\n");
835
+ }
836
+ }
837
+ const examples = op["x-tcapi-examples"] || [];
838
+ if (examples.length) {
839
+ lines.push("### \u793A\u4F8B\n");
840
+ for (const ex of examples) {
841
+ const t = ex.title || ex.description || "\u793A\u4F8B";
842
+ lines.push(`**${cell(t)}**
843
+ `);
844
+ if (ex.input) {
845
+ lines.push("\u8BF7\u6C42\uFF1A\n");
846
+ lines.push("```http");
847
+ lines.push(rewrite(ex.input).replace(/\s+$/, ""));
848
+ lines.push("```\n");
849
+ }
850
+ if (ex.output) {
851
+ lines.push("\u8FD4\u56DE\uFF1A\n");
852
+ lines.push("```json");
853
+ lines.push(rewrite(ex.output).replace(/\s+$/, ""));
854
+ lines.push("```\n");
855
+ }
856
+ }
857
+ }
858
+ lines.push("---\n");
859
+ }
860
+ function renderSchemaAppendix(refNames, schemas, lines) {
861
+ if (refNames.size === 0) return;
862
+ lines.push("## \u6570\u636E\u7ED3\u6784\n");
863
+ for (const name of [...refNames].sort()) {
864
+ const sch = schemas[name] || {};
865
+ lines.push(`<a id="${schemaAnchor(name)}"></a>`);
866
+ lines.push(`### ${name}
867
+ `);
868
+ if (sch.description) lines.push(rewrite(sch.description).trim() + "\n");
869
+ if (sch.properties) {
870
+ paramsTable(sch.properties, new Set(sch.required || []), lines);
871
+ } else if (sch.type === "array") {
872
+ lines.push(`\u7C7B\u578B\uFF1A${typeOf(sch, true)}
873
+ `);
874
+ } else {
875
+ lines.push(`\u7C7B\u578B\uFF1A${sch.type || "object"}
876
+ `);
877
+ }
878
+ }
879
+ }
880
+ function slugIndex(tagsInOrder, usedTags) {
881
+ const ordered = tagsInOrder.map((t) => t.name).filter((n) => usedTags.has(n));
882
+ for (const t of usedTags) if (!ordered.includes(t)) ordered.push(t);
883
+ return ordered.map((t, i) => [String(i + 1).padStart(2, "0"), t]);
884
+ }
885
+ async function fetchText(url) {
886
+ const res = await fetch(url, { headers: { "User-Agent": "coding-openapi-docgen" } });
887
+ if (!res.ok) throw new Error(`GET ${url} -> HTTP ${res.status}`);
888
+ return res.text();
889
+ }
890
+ async function loadSpecText(specCache, opts) {
891
+ const haveCache = fs5.existsSync(specCache);
892
+ if (opts.offline) {
893
+ if (!haveCache) throw new Error(`--offline requires a cached spec at ${specCache}, but none exists.`);
894
+ return { text: fs5.readFileSync(specCache, "utf8"), source: specCache };
895
+ }
896
+ if (haveCache && !opts.forceRefresh) {
897
+ return { text: fs5.readFileSync(specCache, "utf8"), source: specCache };
898
+ }
899
+ const html = await fetchText(DOCS_PAGE);
900
+ const m = html.match(/DOCUMENT_YAML_URL"\s+content="([^"]+)"/);
901
+ if (!m) throw new Error("Could not find DOCUMENT_YAML_URL in the docs page.");
902
+ return { text: await fetchText(m[1]), source: m[1] };
903
+ }
904
+ function loadRiskOverrides(file) {
905
+ if (!fs5.existsSync(file)) return {};
906
+ let parsed;
907
+ try {
908
+ parsed = JSON.parse(fs5.readFileSync(file, "utf8"));
909
+ } catch (e) {
910
+ console.warn(`Ignoring malformed ${file}: ${e.message}`);
911
+ return {};
912
+ }
913
+ const out = {};
914
+ for (const [action, level] of Object.entries(parsed)) {
915
+ if (typeof level === "string" && VALID_RISK.has(level)) out[action] = level;
916
+ else console.warn(`Ignoring invalid risk override for ${action}: ${JSON.stringify(level)} (expected read|write|destructive)`);
917
+ }
918
+ return out;
919
+ }
920
+ function readExistingActions(indexOut) {
921
+ if (!fs5.existsSync(indexOut)) return /* @__PURE__ */ new Set();
922
+ try {
923
+ const idx = JSON.parse(fs5.readFileSync(indexOut, "utf8"));
924
+ return new Set(Object.keys(idx.actions || {}));
925
+ } catch {
926
+ return /* @__PURE__ */ new Set();
927
+ }
928
+ }
929
+ function rmrf(p) {
930
+ try {
931
+ fs5.rmSync(p, { recursive: true, force: true });
932
+ } catch {
933
+ }
934
+ }
935
+ function atomicWriteFile(file, content) {
936
+ const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
937
+ fs5.writeFileSync(tmp, content);
938
+ fs5.renameSync(tmp, file);
939
+ }
940
+ function swapDir(targetDir, builtDir) {
941
+ const bak = `${targetDir}.bak-${process.pid}-${Date.now()}`;
942
+ const hadTarget = fs5.existsSync(targetDir);
943
+ if (hadTarget) fs5.renameSync(targetDir, bak);
944
+ try {
945
+ fs5.renameSync(builtDir, targetDir);
946
+ } catch (e) {
947
+ if (hadTarget && !fs5.existsSync(targetDir)) fs5.renameSync(bak, targetDir);
948
+ throw e;
949
+ }
950
+ rmrf(bak);
951
+ }
952
+ async function buildSpec(opts) {
953
+ const specCache = path4.join(opts.root, "spec", "coding-openapi.yaml");
954
+ const outDir = path4.join(opts.root, "docs");
955
+ const indexOut = path4.join(opts.root, "generated", "index.json");
956
+ const overridesFile = path4.join(opts.root, "risk-overrides.json");
957
+ const { text, source } = await loadSpecText(specCache, opts);
958
+ const spec = parseYaml(text);
959
+ const schemas = spec.components?.schemas || {};
960
+ const paths = spec.paths;
961
+ const overrides = loadRiskOverrides(overridesFile);
962
+ const groups = /* @__PURE__ */ new Map();
963
+ for (const [key, methods] of Object.entries(paths)) {
964
+ for (const op of Object.values(methods)) {
965
+ const tag = op.tags && op.tags[0] || "\u5176\u4ED6\u63A5\u53E3";
966
+ if (!groups.has(tag)) groups.set(tag, []);
967
+ groups.get(tag).push({ key, op });
968
+ }
969
+ }
970
+ const indexed = slugIndex(spec.tags || [], new Set(groups.keys()));
971
+ const index = {
972
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
973
+ source,
974
+ baseUrl: NEW_BASE,
975
+ total: 0,
976
+ categories: [],
977
+ actions: {}
978
+ };
979
+ const docFiles = [];
980
+ const tocRows = [];
981
+ for (const [nn, tag] of indexed) {
982
+ const ops = groups.get(tag);
983
+ ops.sort((a, b) => (a.op.operationId || a.key).localeCompare(b.op.operationId || b.key));
984
+ const fname = `${nn}-${tag}.md`;
985
+ const categoryLabel = `${nn}-${tag}`;
986
+ const lines = [`# ${tag}
987
+ `, `> \u672C\u7C7B\u76EE\u5171 **${ops.length}** \u4E2A\u63A5\u53E3\u3002\u8BF7\u6C42\u57FA\u7840\u5730\u5740\uFF1A\`${NEW_BASE}\`
988
+ `, "## \u63A5\u53E3\u5217\u8868\n"];
989
+ for (const { key, op } of ops) {
990
+ const action = op.operationId || key;
991
+ const title = op.summary || action;
992
+ lines.push(`- [${cell(title)}\uFF08${action}\uFF09](#action-${action})`);
993
+ }
994
+ lines.push("");
995
+ const refAcc = /* @__PURE__ */ new Set();
996
+ for (const { key, op } of ops) {
997
+ renderAction(key, op, schemas, refAcc, lines);
998
+ const action = op.operationId || key;
999
+ const reqSchema = reqSchemaOf(op);
1000
+ const respProps = respPropsOf(op);
1001
+ const examples = op["x-tcapi-examples"] || [];
1002
+ const meta = {
1003
+ summary: op.summary || op["x-tcapi-action-name"] || void 0,
1004
+ category: categoryLabel,
1005
+ description: op.description ? rewrite(op.description).trim() : void 0,
1006
+ params: paramMetas(reqSchema.properties || {}, new Set(reqSchema.required || [])),
1007
+ response: paramMetas(respProps.properties || {}, new Set(respProps["x-tcapi-output-required"] || [])),
1008
+ errors: specificErrors(op),
1009
+ risk: overrides[action] || classifyRisk(action),
1010
+ example: examples[0]?.input ? rewrite(examples[0].input).trim() : void 0
1011
+ };
1012
+ index.actions[action] = meta;
1013
+ }
1014
+ renderSchemaAppendix(refAcc, schemas, lines);
1015
+ docFiles.push({ name: fname, content: lines.join("\n").replace(/\s+$/, "") + "\n" });
1016
+ tocRows.push([nn, tag, fname, ops.length]);
1017
+ }
1018
+ index.total = Object.keys(index.actions).length;
1019
+ index.categories = tocRows.map(([nn, tag, , count]) => ({ slug: nn, name: tag, count }));
1020
+ const info = spec.info || {};
1021
+ const rd = [
1022
+ `# ${info.title || "CODING OPEN API"}
1023
+ `,
1024
+ `\u81EA\u6258\u7BA1\u5B9E\u4F8B OpenAPI \u63A5\u53E3\u6587\u6863\uFF08\u5171 **${index.total}** \u4E2A\u63A5\u53E3\uFF0C\u5206 **${tocRows.length}** \u4E2A\u7C7B\u76EE\uFF09\u3002
1025
+ `,
1026
+ "## \u57FA\u7840\u4FE1\u606F\n",
1027
+ `- **\u8BF7\u6C42\u57FA\u7840\u5730\u5740\uFF08Base URL\uFF09\uFF1A** \`${NEW_BASE}\``,
1028
+ "- **\u8BF7\u6C42\u65B9\u5F0F\uFF1A** \u6240\u6709\u63A5\u53E3\u5747\u4E3A `POST`\uFF0C\u901A\u8FC7 `?action=<Action>` \u6307\u5B9A\u5177\u4F53\u63A5\u53E3",
1029
+ "- **\u8BF7\u6C42/\u8FD4\u56DE\u683C\u5F0F\uFF1A** `application/json`",
1030
+ `- **\u8BA4\u8BC1\u65B9\u5F0F\uFF1A** OAuth 2.0 / \u4E2A\u4EBA\u8BBF\u95EE\u4EE4\u724C / \u9879\u76EE\u4EE4\u724C\uFF08\u8BE6\u89C1 ${NEW_BASE.replace(/\/[^/]*$/, "")}\uFF09`,
1031
+ "- **\u9891\u7387\u9650\u5236\uFF1A** \u5355\u56E2\u961F\u5355\u63A5\u53E3\u6BCF\u79D2\u6700\u591A 30 \u6B21\u8BF7\u6C42\n",
1032
+ "## \u7C7B\u76EE\u7D22\u5F15\n",
1033
+ "| # | \u7C7B\u76EE | \u63A5\u53E3\u6570 | \u6587\u6863 |",
1034
+ "| --- | --- | --- | --- |"
1035
+ ];
1036
+ for (const [nn, tag, fname, count] of tocRows) rd.push(`| ${nn} | ${tag} | ${count} | [${fname}](./${fname.replace(/ /g, "%20")}) |`);
1037
+ rd.push("");
1038
+ rd.push("## \u516C\u5171\u9519\u8BEF\u7801\n");
1039
+ rd.push("\u4EE5\u4E0B\u9519\u8BEF\u7801\u4E3A\u5404\u63A5\u53E3\u901A\u7528\uFF0C\u5355\u4E2A\u63A5\u53E3\u6587\u6863\u4E2D\u4EC5\u989D\u5916\u5217\u51FA\u5176\u7279\u6709\u9519\u8BEF\u7801\u3002\n");
1040
+ rd.push("| \u9519\u8BEF\u7801 | \u63CF\u8FF0 |");
1041
+ rd.push("| --- | --- |");
1042
+ for (const [code, desc] of COMMON_ERROR_CODES) rd.push(`| \`${code}\` | ${desc} |`);
1043
+ rd.push("");
1044
+ const oldActions = readExistingActions(indexOut);
1045
+ const newActions = new Set(Object.keys(index.actions));
1046
+ const added = [...newActions].filter((a) => !oldActions.has(a)).sort();
1047
+ const removed = [...oldActions].filter((a) => !newActions.has(a)).sort();
1048
+ const readmeContent = rd.join("\n").replace(/\s+$/, "") + "\n";
1049
+ const fetchedLive = /^https?:/i.test(source);
1050
+ let wrote = false;
1051
+ if (!opts.dryRun) {
1052
+ const stagingDocs = `${outDir}.new-${process.pid}-${Date.now()}`;
1053
+ try {
1054
+ fs5.mkdirSync(stagingDocs, { recursive: true });
1055
+ for (const f of docFiles) fs5.writeFileSync(path4.join(stagingDocs, f.name), f.content);
1056
+ fs5.writeFileSync(path4.join(stagingDocs, "README.md"), readmeContent);
1057
+ swapDir(outDir, stagingDocs);
1058
+ fs5.mkdirSync(path4.dirname(indexOut), { recursive: true });
1059
+ atomicWriteFile(indexOut, JSON.stringify(index) + "\n");
1060
+ if (fetchedLive) {
1061
+ fs5.mkdirSync(path4.dirname(specCache), { recursive: true });
1062
+ atomicWriteFile(specCache, text);
1063
+ }
1064
+ wrote = true;
1065
+ } catch (e) {
1066
+ rmrf(stagingDocs);
1067
+ throw e;
1068
+ }
1069
+ }
1070
+ return { total: index.total, categories: index.categories, added, removed, wrote, source };
1071
+ }
1072
+
1073
+ // src/commands/update.ts
1074
+ async function cmdUpdate(_ctx, opts) {
1075
+ const root = packageRoot();
1076
+ let result;
1077
+ try {
1078
+ result = await buildSpec({ root, offline: opts.offline, forceRefresh: !opts.offline, dryRun: opts.dryRun });
1079
+ } catch (e) {
1080
+ throw new CliError(EXIT.REMOTE, {
1081
+ type: "update_failed",
1082
+ message: e.message,
1083
+ hint: opts.offline ? "No cached spec to rebuild from." : "Spec fetch failed; retry, or use `coding update --offline` if a cached spec exists."
1084
+ });
1085
+ }
1086
+ return {
1087
+ data: {
1088
+ root,
1089
+ source: result.source,
1090
+ total: result.total,
1091
+ categories: result.categories.length,
1092
+ added: result.added,
1093
+ removed: result.removed,
1094
+ wrote: result.wrote,
1095
+ note: opts.dryRun ? "dry-run: nothing written" : result.wrote ? "index + docs regenerated" : "no changes"
1096
+ }
1097
+ };
1098
+ }
1099
+
1100
+ // src/cli.ts
1101
+ function readVersion() {
1102
+ try {
1103
+ return JSON.parse(fs6.readFileSync(path5.join(packageRoot(), "package.json"), "utf8")).version || "0.0.0";
1104
+ } catch {
1105
+ return "0.0.0";
1106
+ }
1107
+ }
1108
+ function ctxOf(cmd) {
1109
+ const o = cmd.optsWithGlobals();
1110
+ return { json: !!o.json, profile: resolveProfileName(o.profile), cliBaseUrl: o.baseUrl };
1111
+ }
1112
+ async function run(cmd, fn) {
1113
+ const ctx = ctxOf(cmd);
1114
+ try {
1115
+ const result = await fn(ctx);
1116
+ process.stdout.write(renderEnvelope(success(result.data, { action: result.action, requestId: result.requestId, warnings: result.warnings }), ctx.json) + "\n");
1117
+ process.exit(EXIT.OK);
1118
+ } catch (e) {
1119
+ const err = e instanceof CliError ? e : new CliError(EXIT.INTERNAL, { type: "internal", message: e.message || String(e) });
1120
+ process.stderr.write(renderEnvelope(errorEnvelope(err), ctx.json) + "\n");
1121
+ process.exit(err.exit);
1122
+ }
1123
+ }
1124
+ function collect(value, acc) {
1125
+ acc.push(value);
1126
+ return acc;
1127
+ }
1128
+ function parseLimit(value) {
1129
+ const n = Number(value);
1130
+ if (!Number.isInteger(n) || n < 0) {
1131
+ throw new CliError(EXIT.USAGE, { type: "bad_option", message: `--limit must be a non-negative integer (got "${value}")` });
1132
+ }
1133
+ return n;
1134
+ }
1135
+ function main() {
1136
+ const program = new Command();
1137
+ program.name("coding").description("Agent-first CLI for a self-hosted CODING instance (generic OpenAPI passthrough).").version(readVersion()).option("--json", "emit a machine-readable JSON envelope").option("--profile <name>", "config profile to use").option("--base-url <url>", "override the instance base URL");
1138
+ const config = program.command("config").description("manage instance config");
1139
+ config.command("init").description("write ~/.coding/config.json (use --base-url to set the instance URL)").action((_opts, cmd) => run(cmd, (ctx) => cmdConfigInit(ctx, cmd.optsWithGlobals().baseUrl)));
1140
+ config.command("show").description("show resolved config and whether a token is present").action((_opts, cmd) => run(cmd, (ctx) => cmdConfigShow(ctx)));
1141
+ const auth = program.command("auth").description("manage the access token");
1142
+ auth.command("login").description("store a token (hidden prompt by default; use --token - to read stdin)").option("--token <src>", "pass '-' to read the token from stdin").action((opts, cmd) => run(cmd, (ctx) => cmdAuthLogin(ctx, opts.token === "-")));
1143
+ auth.command("status").description("verify credentials via DescribeCodingCurrentUser").action((_opts, cmd) => run(cmd, (ctx) => cmdAuthStatus(ctx)));
1144
+ program.command("whoami").description("alias of `auth status`").action((_opts, cmd) => run(cmd, (ctx) => cmdAuthStatus(ctx)));
1145
+ program.command("categories").description("list the API categories").action((_opts, cmd) => run(cmd, (ctx) => cmdCategories(ctx)));
1146
+ program.command("actions").description("list/search actions in the bundled index").option("-s, --search <keyword>", "filter by name/summary/description").option("-c, --category <category>", "filter by category slug or name").action((opts, cmd) => run(cmd, (ctx) => cmdActions(ctx, opts)));
1147
+ program.command("schema <action>").description("show an action's params, response fields, errors and risk").action((action, _opts, cmd) => run(cmd, (ctx) => cmdSchema(ctx, action)));
1148
+ program.command("update").description("refresh the bundled action index + docs from the live CODING spec").option("--offline", "rebuild from the cached spec instead of fetching", false).option("--dry-run", "report what would change without writing", false).action((opts, cmd) => run(cmd, (ctx) => cmdUpdate(ctx, { offline: !!opts.offline, dryRun: !!opts.dryRun })));
1149
+ program.command("call <action>").description("invoke any action (generic passthrough)").option("-d, --data <src>", "JSON body: inline string, @file, or - for stdin").option("-p, --param <Key=Value>", "set one param (repeatable); coerced by index type", collect, []).option("--select <paths>", "comma-separated dotpaths to project from the response").option("--limit <n>", "truncate response arrays to the first N items", parseLimit).option("--dry-run", "print the request without sending (never trips the confirm gate)", false).option("--force", "bypass client-side validation (alias of --no-validate)", false).option("--no-validate", "bypass client-side validation").option("-y, --yes", "confirm a destructive (exit-10) action", false).action(
1150
+ (action, opts, cmd) => run(
1151
+ cmd,
1152
+ (ctx) => cmdCall(ctx, action, {
1153
+ data: opts.data,
1154
+ param: opts.param,
1155
+ select: opts.select,
1156
+ limit: opts.limit,
1157
+ dryRun: !!opts.dryRun,
1158
+ force: !!opts.force || opts.validate === false,
1159
+ yes: !!opts.yes
1160
+ })
1161
+ )
1162
+ );
1163
+ const applyOverride = (cmd) => {
1164
+ cmd.exitOverride();
1165
+ cmd.configureOutput({ writeErr: () => {
1166
+ } });
1167
+ cmd.commands.forEach(applyOverride);
1168
+ };
1169
+ applyOverride(program);
1170
+ program.parseAsync(process.argv).catch((e) => {
1171
+ const ce = e;
1172
+ if (ce && (ce.code === "commander.helpDisplayed" || ce.code === "commander.help" || ce.code === "commander.version")) {
1173
+ process.exit(0);
1174
+ }
1175
+ const json = process.argv.includes("--json");
1176
+ const err = e instanceof CliError ? e : new CliError(EXIT.USAGE, { type: "usage", message: ce?.message || String(e) });
1177
+ process.stderr.write(renderEnvelope(errorEnvelope(err), json) + "\n");
1178
+ process.exit(err.exit);
1179
+ });
1180
+ }
1181
+ main();