@dac-tech-team-tw/itsaplan-cli 0.1.0 → 0.2.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 CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { Command, CommanderError, Option } from "commander";
2
+ import { Command, CommanderError, Help, Option } from "commander";
3
3
  import { CliError, CLI_VERSION, API_BASELINE, } from "./contracts.js";
4
4
  import { createContext } from "./config.js";
5
5
  import { redact } from "./http.js";
@@ -7,20 +7,81 @@ import { runIssue } from "./issues.js";
7
7
  import { runDoctor, runAuth, runProject, runInit, runApi } from "./commands.js";
8
8
  import { runDocument, runIssueDocument, } from "./documents.js";
9
9
  import { runDevelopment } from "./development.js";
10
+ import { runProjectConfig } from "./project-config.js";
11
+ import { validateOutput, projectOutput, needsIssueStates, } from "./output.js";
10
12
  const program = new Command()
11
13
  .name("itsaplan")
12
- .description("It's a Plan: discover projects, read issues, preview and perform authorized writes")
13
- .version(CLI_VERSION)
14
- .option("--json", "Emit one stable JSON envelope")
15
- .option("--api-url <url>", "Trusted API base URL (HTTPS except localhost)")
16
- .option("--api-key <key>", "One-off key; prefer env/config; requires explicit endpoint")
17
- .option("--project <team.KEY>", "Project ref; overrides env and directory configuration")
14
+ .description("It's a Plan:探索專案、讀取工作項目、預覽並執行已授權的寫入操作")
15
+ .version(CLI_VERSION, "-V, --version", "顯示版本")
16
+ .helpOption("-h, --help", "顯示指令說明")
17
+ .helpCommand("help [command]", "顯示指令說明")
18
+ .configureHelp({
19
+ showGlobalOptions: true,
20
+ styleTitle(title) {
21
+ return ({
22
+ "Usage:": "用法:",
23
+ "Arguments:": "引數:",
24
+ "Options:": "選項:",
25
+ "Global Options:": "全域選項:",
26
+ "Commands:": "指令:",
27
+ }[title] ?? title);
28
+ },
29
+ optionDescription(option) {
30
+ if (option.long === "--help")
31
+ return "顯示指令說明";
32
+ if (option.long === "--version")
33
+ return "顯示版本";
34
+ const description = Help.prototype.optionDescription.call(this, option);
35
+ return description
36
+ .replace(/\bchoices:/g, "可選值:")
37
+ .replace(/\bdefault:/g, "預設值:")
38
+ .replace(/\bpreset:/g, "預設參數:");
39
+ },
40
+ })
41
+ .addHelpText("afterAll", `
42
+ 全域選項可放在子指令之前或之後;完整說明請見 iap --help。
43
+ 指令說明:iap <command> <subcommand> --help。
44
+ 專案來源優先序:--project > ITSAPLAN_PROJECT > 最近的 .itsaplan.toml
45
+ > 目前 API 服務網址(含基底路徑)的使用者預設值。
46
+ 使用 iap init --project <team.KEY> 為此目錄設定專案。
47
+ 使用者預設值:iap config set project <team.KEY>;
48
+ 使用 iap config get project 檢視,或以 iap config unset project 清除。`)
49
+ .option("--json", "輸出單一固定格式的 JSON 結果封裝")
50
+ .option("--summary", "精簡 project/issue 讀取資料;不截斷正文,不支援寫入")
51
+ .option("--fields <paths>", "選取 project/issue 讀取欄位(逗號分隔點路徑);與 --summary 互斥")
52
+ .option("--api-url <url>", "API 服務網址(部署網域,含 https://;非網頁登入網址)")
53
+ .option("--api-key <key>", "一次性金鑰;建議使用環境變數/設定檔;必須明確指定 API 服務網址")
54
+ .option("--project <team.KEY>", "專案參照;優先於環境變數、目錄與使用者設定")
18
55
  .showHelpAfterError()
19
56
  .exitOverride();
20
57
  let activeWrite;
21
58
  function globals(command) {
22
59
  return command.optsWithGlobals();
23
60
  }
61
+ function outputKind(command) {
62
+ const key = `${command.parent?.name()}.${command.name()}`;
63
+ return [
64
+ "project.list",
65
+ "project.view",
66
+ "issue.list",
67
+ "issue.view",
68
+ "issue.resolve",
69
+ "issue.comments",
70
+ "issue.activity",
71
+ ].includes(key)
72
+ ? key
73
+ : undefined;
74
+ }
75
+ function issueOutputOptions(command) {
76
+ const options = command.optsWithGlobals();
77
+ const kind = outputKind(command);
78
+ if (!kind)
79
+ return { outputSelected: false, includeState: false };
80
+ return {
81
+ outputSelected: options.summary === true || options.fields !== undefined,
82
+ includeState: needsIssueStates(kind, options),
83
+ };
84
+ }
24
85
  const wrap = (handler) => async (...args) => {
25
86
  const command = args.at(-1);
26
87
  if (!(command instanceof Command))
@@ -36,12 +97,22 @@ const wrap = (handler) => async (...args) => {
36
97
  "init",
37
98
  ].includes(command.name()) ||
38
99
  (command.parent?.name() === "document" && command.name() === "create") ||
100
+ (command.name() === "project" &&
101
+ ["set", "unset"].includes(command.parent?.name() ?? "") &&
102
+ command.parent?.parent?.name() === "config") ||
39
103
  (command.name() === "api" &&
40
104
  !["GET", "HEAD"].includes(command.opts().method));
105
+ const selection = command.optsWithGlobals();
106
+ const kind = outputKind(command);
107
+ if (selection.summary || selection.fields !== undefined) {
108
+ if (!kind)
109
+ throw new CliError("UNSUPPORTED_OUTPUT", "--summary and --fields only support project list/view and issue list/view/resolve/comments/activity");
110
+ validateOutput(kind, selection);
111
+ }
41
112
  const result = await handler(command);
42
- emit(result);
113
+ emit(kind ? projectOutput(kind, result, selection) : result, command);
43
114
  };
44
- function emit(result) {
115
+ function emit(result, command) {
45
116
  const envelope = redact({
46
117
  ok: !result.error,
47
118
  ...(result.outcome
@@ -55,6 +126,16 @@ function emit(result) {
55
126
  console.log(JSON.stringify(envelope));
56
127
  else if (result.error)
57
128
  console.error(JSON.stringify(envelope, null, 2));
129
+ else if (command?.name() === "login" && command.parent?.name() === "auth") {
130
+ const data = result.data;
131
+ console.log("OK:登入憑證已驗證並儲存。");
132
+ const warning = data.warning ??
133
+ (data.effective.usable !== true
134
+ ? "目前生效的登入尚未確認可用,請執行 iap auth status --json 查詢。"
135
+ : undefined);
136
+ if (warning)
137
+ console.error(`警告:${redact(warning)}`);
138
+ }
58
139
  else
59
140
  console.log(typeof result.data === "string"
60
141
  ? result.data
@@ -69,16 +150,16 @@ program.configureOutput({
69
150
  });
70
151
  program
71
152
  .command("doctor")
72
- .description("Diagnose configuration, authentication, reachability and API version")
153
+ .description("診斷設定、認證、連線狀態與 API 版本")
73
154
  .action(wrap((c) => runDoctor(globals(c))));
74
155
  const auth = program
75
156
  .command("auth")
76
- .description("Manage endpoint-bound personal API key credentials");
157
+ .description("管理綁定 API 服務網址的個人 API 金鑰憑證");
77
158
  auth
78
159
  .command("login")
79
- .description("Validate and store a key; hidden prompt or explicit stdin")
80
- .option("--with-token", "Read API key from stdin")
81
- .addOption(new Option("--storage <kind>", "Credential storage; explicit file opts into plaintext").choices(["file", "keyring"]))
160
+ .description("驗證並儲存個人 API 金鑰;隱藏輸入或明確從標準輸入讀取")
161
+ .option("--with-token", "從標準輸入讀取 API 金鑰")
162
+ .addOption(new Option("--storage <kind>", "憑證儲存方式;file 明確選用明文檔案").choices(["file", "keyring"]))
82
163
  .action(wrap((c) => runAuth("login", {
83
164
  ...globals(c),
84
165
  ...c.opts(),
@@ -87,12 +168,30 @@ for (const action of ["status", "logout"])
87
168
  auth
88
169
  .command(action)
89
170
  .description(action === "status"
90
- ? "Show effective auth source and identity without the key"
91
- : "Remove selected local credentials; does not revoke env keys")
171
+ ? "顯示生效中的認證來源與身份,不顯示金鑰"
172
+ : "移除選取的本機憑證;不會撤銷環境變數中的金鑰")
92
173
  .action(wrap((c) => runAuth(action, globals(c))));
174
+ const config = program
175
+ .command("config")
176
+ .description("管理依 API 服務網址區分的使用者預設值");
177
+ config
178
+ .command("set")
179
+ .description("儲存經驗證的使用者預設值")
180
+ .command("project <ref>")
181
+ .description("驗證並儲存此 API 服務網址的標準專案參照")
182
+ .action(wrap((c) => runProjectConfig("set", globals(c), c.args[0])));
183
+ for (const action of ["get", "unset"])
184
+ config
185
+ .command(action)
186
+ .description(action === "get" ? "檢視使用者預設值" : "移除使用者預設值")
187
+ .command("project")
188
+ .description(action === "get"
189
+ ? "不發出請求,顯示已儲存及生效中的專案與來源"
190
+ : "只移除此 API 服務網址儲存的專案預設值")
191
+ .action(wrap((c) => runProjectConfig(action, globals(c))));
93
192
  program
94
193
  .command("init")
95
- .description("Validate --project <team.KEY> and save only project_ref in this directory")
194
+ .description("驗證 --project <team.KEY>,並只在此目錄儲存 project_ref")
96
195
  .action(wrap(async (c) => {
97
196
  const options = globals(c);
98
197
  if (!options.project) {
@@ -102,21 +201,30 @@ program
102
201
  }));
103
202
  const project = program
104
203
  .command("project")
105
- .description("Discover projects and inspect workflow values");
204
+ .description("探索專案並檢視工作流程值");
106
205
  project
107
206
  .command("list")
108
- .description("List available projects (bounded)")
109
- .option("--limit <number>", "Maximum projects", "30")
207
+ .description("列出可用專案(有筆數上限)")
208
+ .option("--limit <number>", "專案數量上限", "30")
110
209
  .action(wrap(async (c) => runProject(await createContext(globals(c)), "list", undefined, {
111
210
  limit: Number(c.opts().limit),
112
211
  })));
113
212
  project
114
213
  .command("view <ref>")
115
- .description("Read columns, labels, members, types and custom fields")
214
+ .description("讀取欄、標籤、成員、類型與自訂欄位")
116
215
  .action(wrap(async (c) => runProject(await createContext(globals(c)), "view", c.args[0])));
117
216
  const issue = program
118
217
  .command("issue")
119
- .description("Resolve, inspect, search, comment and edit issues");
218
+ .description("解析、檢視、搜尋、留言與編輯工作項目");
219
+ const issueReadExamples = `
220
+ 範例使用虛構識別碼;請從清單中選擇識別碼或內部 ID:
221
+ iap issue list --project acme.MKT --limit 30 --summary --json
222
+ iap issue view MKT-42 --project acme.MKT --comments --summary --json
223
+ iap issue view --id 123 --fields identifier,title,state --json
224
+ 選取不改變 --json 序列化;省略 --summary/--fields 取得完整資料。
225
+ --fields 對列表的每個 item 選欄位;保留範圍、完整性與游標。
226
+ MKT-42 使用專案序號 42;--id 123 是內部數值工作項目 ID。
227
+ 請明確傳入其中一種:view 不會從標準輸入讀取 list 的 JSON。`;
120
228
  for (const action of [
121
229
  "resolve",
122
230
  "view",
@@ -128,75 +236,80 @@ for (const action of [
128
236
  const command = issue
129
237
  .command(action)
130
238
  .description({
131
- resolve: "Resolve an identifier directly to a stable issue ID",
132
- view: "Read full issue context and project names",
133
- activity: "Read a cursor-bounded issue activity feed",
134
- comments: "Read a cursor-bounded discussion with replies",
135
- edit: "Update only specified issue fields",
136
- comment: "Publish one comment or reply",
239
+ resolve: "直接解析識別碼並取得穩定工作項目 ID",
240
+ view: "讀取完整工作項目背景與專案名稱",
241
+ activity: "讀取有分頁游標筆數上限的工作項目活動動態",
242
+ comments: "讀取有分頁游標筆數上限且包含回覆的討論",
243
+ edit: "只更新明確指定的工作項目欄位",
244
+ comment: "發布一則留言或回覆",
137
245
  }[action] ?? action)
138
- .argument("[identifier]", "KEY-42 or project-scoped sequence number")
139
- .option("--id <id>", "Internal issue ID, mutually exclusive with identifier");
246
+ .argument("[identifier]", "KEY-42 或專案範圍內的序號")
247
+ .option("--id <id>", "內部工作項目 ID;不可與 identifier 同時使用");
140
248
  if (action === "view")
141
- command.option("--comments", "Include latest 25 comment threads and next cursor");
249
+ command
250
+ .option("--comments", "包含最新 25 個留言討論串及下一個分頁游標")
251
+ .addHelpText("after", issueReadExamples);
142
252
  if (action === "activity" || action === "comments")
143
253
  command
144
- .option("--limit <number>", "Maximum top-level entries", "25")
145
- .option("--cursor <cursor>", "Opaque cursor returned by this CLI");
254
+ .option("--limit <number>", "頂層項目數量上限", "25")
255
+ .option("--cursor <cursor>", "此 CLI 回傳的不透明分頁游標");
146
256
  if (action === "edit" || action === "comment") {
147
257
  command
148
- .option("--body <text>", "Markdown content")
149
- .option("--body-file <file|->", "UTF-8 Markdown file, or explicit stdin -")
150
- .option("--agent <name>", "Declared agent: codex or claude")
151
- .option("--human", "Explicit human source; conflicts with agent env")
152
- .option("--dry-run", "Read and validate only; show exact request without writing");
258
+ .option("--body <text>", "Markdown 內容")
259
+ .option("--body-file <file|->", "UTF-8 Markdown 檔案,或明確指定標準輸入 -")
260
+ .option("--agent <name>", "宣告代理:codex 或 claude")
261
+ .option("--human", "明確宣告人工作業;不可與代理環境變數並用")
262
+ .option("--dry-run", "只讀取與驗證;顯示確切請求但不寫入");
153
263
  if (action === "comment")
154
- command.option("--reply-to <id>", "Reply to a comment of this issue");
264
+ command.option("--reply-to <id>", "回覆此工作項目的留言");
155
265
  else
156
266
  command
157
- .option("--title <text>", "New title")
158
- .option("--state <name>", "Exact column name")
159
- .option("--state-type <type>", "Unique backlog/unstarted/started/completed/canceled column")
160
- .option("--column-id <id>", "Column ID belonging to this project")
161
- .option("--assignee <username|@me>", "Assign a project member")
162
- .option("--remove-assignee", "Clear assignee")
267
+ .option("--title <text>", "新標題")
268
+ .option("--state <name>", "精確欄位名稱")
269
+ .option("--state-type <type>", "唯一符合的 backlog/unstarted/started/completed/canceled 欄位")
270
+ .option("--column-id <id>", "屬於此專案的欄位 ID")
271
+ .option("--assignee <username|@me>", "指派工作項目負責人")
272
+ .option("--remove-assignee", "清除負責人")
163
273
  .option("--priority <priority>", "urgent/high/medium/low")
164
- .option("--remove-priority", "Clear priority")
165
- .option("--type <name>", "Exact issue type name")
166
- .option("--remove-type", "Clear issue type")
167
- .option("--add-label <name>", "Add exact label, repeatable", (value, previous) => [...previous, value], [])
168
- .option("--remove-label <name>", "Remove exact label, repeatable", (value, previous) => [...previous, value], []);
274
+ .option("--remove-priority", "清除優先級")
275
+ .option("--type <name>", "精確工作項目類型名稱")
276
+ .option("--remove-type", "清除工作項目類型")
277
+ .option("--add-label <name>", "加入精確標籤,可重複指定", (value, previous) => [...previous, value], [])
278
+ .option("--remove-label <name>", "移除精確標籤,可重複指定", (value, previous) => [...previous, value], []);
169
279
  }
170
280
  command.action(wrap(async (c) => runIssue(await createContext(globals(c)), action, c.args[0], {
171
281
  ...c.opts(),
172
282
  project: globals(c).project,
283
+ ...issueOutputOptions(c),
173
284
  })));
174
285
  }
175
286
  issue
176
287
  .command("list")
177
- .description("List or search project issues; completeness is unknown without pagination")
178
- .option("--search <text>", "Project text search; cannot combine with assignee filter")
179
- .option("--assignee <username|@me>", "Filter by project member")
180
- .option("--limit <number>", "Maximum issues (API bounded)", "30")
288
+ .description("列出或搜尋專案工作項目;僅支援有筆數上限的查詢,不提供接續分頁;完整性未知(unknown)")
289
+ .option("--search <text>", "專案文字搜尋;不可與負責人篩選條件並用")
290
+ .option("--assignee <username|@me>", "依專案成員篩選")
291
+ .option("--limit <number>", "本地範圍為 1–500;伺服器可能回傳較少項目", "30")
292
+ .addHelpText("after", issueReadExamples)
181
293
  .action(wrap(async (c) => runIssue(await createContext(globals(c)), "list", undefined, {
182
294
  ...c.opts(),
183
295
  project: globals(c).project,
296
+ ...issueOutputOptions(c),
184
297
  })));
185
298
  const document = program
186
299
  .command("document")
187
- .description("Publish Markdown Docs; project-visible is not internet-public");
300
+ .description("發布 Markdown 文件;專案可見不等於網際網路公開");
188
301
  document
189
302
  .command("create")
190
- .description("Create a Docs page with explicit private or project visibility")
191
- .requiredOption("--title <text>", "Document title (1–255 characters)")
192
- .addOption(new Option("--visibility <scope>", "project: project-visible, NOT internet-public; private: owner only")
303
+ .description("建立明確設定 private 或 project 可見範圍的文件頁面")
304
+ .requiredOption("--title <text>", "文件標題(1–255 個字元)")
305
+ .addOption(new Option("--visibility <scope>", "project:專案可見,並非網際網路公開;private:僅擁有者可見")
193
306
  .choices(["project", "private"])
194
307
  .makeOptionMandatory())
195
- .option("--body <text>", "Markdown content")
196
- .option("--body-file <file|->", "UTF-8 Markdown file, or explicit stdin -")
197
- .option("--agent <name>", "Declared agent: codex or claude")
198
- .option("--human", "Explicit human source; conflicts with agent env")
199
- .option("--dry-run", "Read and validate only; show exact request without writing")
308
+ .option("--body <text>", "Markdown 內容")
309
+ .option("--body-file <file|->", "UTF-8 Markdown 檔案,或明確指定標準輸入 -")
310
+ .option("--agent <name>", "宣告代理:codex 或 claude")
311
+ .option("--human", "明確宣告人工作業;不可與代理環境變數並用")
312
+ .option("--dry-run", "只讀取與驗證;顯示確切請求但不寫入")
200
313
  .action(wrap(async (c) => runDocument(await createContext(globals(c)), "create", undefined, {
201
314
  ...c.opts(),
202
315
  project: globals(c).project,
@@ -205,8 +318,8 @@ for (const action of ["view", "issues"]) {
205
318
  document
206
319
  .command(`${action} <document-id>`)
207
320
  .description(action === "view"
208
- ? "Read a visible document by stable ID"
209
- : "List tickets linked to this document")
321
+ ? "依穩定 ID 讀取可見文件"
322
+ : "列出連結至此文件的工作項目")
210
323
  .action(wrap(async (c) => runDocument(await createContext(globals(c)), action, c.args[0], {
211
324
  project: globals(c).project,
212
325
  })));
@@ -215,15 +328,15 @@ for (const action of ["documents", "link-document"]) {
215
328
  const command = issue
216
329
  .command(`${action} [identifier]`)
217
330
  .description(action === "documents"
218
- ? "List visible Docs linked to a ticket"
219
- : "Link a same-project document to a ticket; existing link is a no-op")
220
- .option("--id <id>", "Internal issue ID, mutually exclusive with identifier");
331
+ ? "列出連結至工作項目的可見文件"
332
+ : "將同專案文件連結至工作項目;已存在連結時不執行操作")
333
+ .option("--id <id>", "內部工作項目 ID;不可與 identifier 同時使用");
221
334
  if (action === "link-document")
222
335
  command
223
- .requiredOption("--document <id>", "Stable document ID in the same project")
224
- .option("--agent <name>", "Declared agent: codex or claude")
225
- .option("--human", "Explicit human source; conflicts with agent env")
226
- .option("--dry-run", "Preview the exact relation without writing");
336
+ .requiredOption("--document <id>", "同一專案中的穩定文件 ID")
337
+ .option("--agent <name>", "宣告代理:codex 或 claude")
338
+ .option("--human", "明確宣告人工作業;不可與代理環境變數並用")
339
+ .option("--dry-run", "預覽確切關聯內容,不執行寫入");
227
340
  command.action(wrap(async (c) => runIssueDocument(await createContext(globals(c)), action, c.args[0], {
228
341
  ...c.opts(),
229
342
  project: globals(c).project,
@@ -236,29 +349,29 @@ for (const action of [
236
349
  "development",
237
350
  ]) {
238
351
  const descriptions = {
239
- repositories: "Discover connected repositories available to a ticket",
240
- "pull-requests": "List existing PRs/MRs from a connected repository (one page)",
241
- "link-pr": "Link an existing PR/MR by connected repository ID and number; never create a PR",
242
- development: "List the ticket's existing development links",
352
+ repositories: "探索可供工作項目使用的已連接儲存庫",
353
+ "pull-requests": "列出已連接儲存庫的現有 PR/MR(單頁)",
354
+ "link-pr": "依已連接儲存庫 ID 與編號連結現有 PR/MR;不會建立 PR",
355
+ development: "列出工作項目現有的開發連結",
243
356
  };
244
357
  const command = issue
245
358
  .command(`${action} [identifier]`)
246
359
  .description(descriptions[action])
247
- .option("--id <id>", "Internal issue ID, mutually exclusive with identifier");
360
+ .option("--id <id>", "內部工作項目 ID;不可與 identifier 同時使用");
248
361
  if (action === "pull-requests" || action === "link-pr")
249
- command.requiredOption("--repository <id>", "Connected repository ID from issue repositories");
362
+ command.requiredOption("--repository <id>", "由 iap issue repositories 列出的已連接儲存庫 ID");
250
363
  if (action === "pull-requests")
251
364
  command
252
- .option("--page <number>", "API page (follow nextPage)", "1")
253
- .addOption(new Option("--state <state>", "PR state filter")
365
+ .option("--page <number>", "API 頁碼(依 nextPage 繼續)", "1")
366
+ .addOption(new Option("--state <state>", "PR 狀態篩選")
254
367
  .choices(["open", "all"])
255
368
  .default("open"));
256
369
  if (action === "link-pr")
257
370
  command
258
- .requiredOption("--number <number>", "Existing PR/MR number; not an arbitrary URL")
259
- .option("--agent <name>", "Declared agent: codex or claude")
260
- .option("--human", "Explicit human source; conflicts with agent env")
261
- .option("--dry-run", "Preview without linking or creating a PR");
371
+ .requiredOption("--number <number>", "現有 PR/MR 編號;不是任意 URL")
372
+ .option("--agent <name>", "宣告代理:codex 或 claude")
373
+ .option("--human", "明確宣告人工作業;不可與代理環境變數並用")
374
+ .option("--dry-run", "預覽但不連結或建立 PR");
262
375
  command.action(wrap(async (c) => runDevelopment(await createContext(globals(c)), action, c.args[0], {
263
376
  ...c.opts(),
264
377
  project: globals(c).project,
@@ -266,12 +379,12 @@ for (const action of [
266
379
  }
267
380
  program
268
381
  .command("api <relative-path>")
269
- .description("Raw API escape hatch; issue and Docs mutations require high-level commands")
270
- .addOption(new Option("--method <method>", "HTTP method")
382
+ .description("原始 API 入口;工作項目與文件寫入必須使用高階指令")
383
+ .addOption(new Option("--method <method>", "HTTP 方法")
271
384
  .choices(["GET", "HEAD", "POST", "PATCH", "PUT", "DELETE"])
272
385
  .default("GET"))
273
- .option("--input <file|->", "JSON request body from file or explicit stdin")
274
- .option("--dry-run", "Preview without sending writes")
386
+ .option("--input <file|->", "從檔案或明確指定的標準輸入讀取 JSON 請求本文")
387
+ .option("--dry-run", "預覽但不送出寫入請求")
275
388
  .action(wrap(async (c) => runApi(await createContext(globals(c)), c.args[0] ?? "", c.opts())));
276
389
  try {
277
390
  await program.parseAsync();
@@ -299,6 +412,8 @@ catch (error) {
299
412
  words.includes("init") ||
300
413
  (words.includes("auth") &&
301
414
  (words.includes("login") || words.includes("logout"))) ||
415
+ (words.includes("config") &&
416
+ (words.includes("set") || words.includes("unset"))) ||
302
417
  (words.includes("api") &&
303
418
  /^(POST|PATCH|PUT|DELETE)$/.test(rawMethod ?? "GET")));
304
419
  const { outcome, verification, data, meta, ...details } = known.details;