@dac-tech-team-tw/itsaplan-cli 0.0.0-stage → 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/README.md +244 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +444 -0
- package/dist/commands.d.ts +15 -0
- package/dist/commands.js +681 -0
- package/dist/config.d.ts +32 -0
- package/dist/config.js +608 -0
- package/dist/contracts.d.ts +47 -0
- package/dist/contracts.js +14 -0
- package/dist/development.d.ts +9 -0
- package/dist/development.js +220 -0
- package/dist/documents.d.ts +9 -0
- package/dist/documents.js +256 -0
- package/dist/http.d.ts +11 -0
- package/dist/http.js +306 -0
- package/dist/issues.d.ts +63 -0
- package/dist/issues.js +800 -0
- package/dist/output.d.ts +12 -0
- package/dist/output.js +260 -0
- package/dist/project-config.d.ts +2 -0
- package/dist/project-config.js +56 -0
- package/dist/writes.d.ts +8 -0
- package/dist/writes.js +57 -0
- package/package.json +45 -4
package/dist/cli.js
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command, CommanderError, Help, Option } from "commander";
|
|
3
|
+
import { CliError, CLI_VERSION, API_BASELINE, } from "./contracts.js";
|
|
4
|
+
import { createContext } from "./config.js";
|
|
5
|
+
import { redact } from "./http.js";
|
|
6
|
+
import { runIssue } from "./issues.js";
|
|
7
|
+
import { runDoctor, runAuth, runProject, runInit, runApi } from "./commands.js";
|
|
8
|
+
import { runDocument, runIssueDocument, } from "./documents.js";
|
|
9
|
+
import { runDevelopment } from "./development.js";
|
|
10
|
+
import { runProjectConfig } from "./project-config.js";
|
|
11
|
+
import { validateOutput, projectOutput, needsIssueStates, } from "./output.js";
|
|
12
|
+
const program = new Command()
|
|
13
|
+
.name("itsaplan")
|
|
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>", "專案參照;優先於環境變數、目錄與使用者設定")
|
|
55
|
+
.showHelpAfterError()
|
|
56
|
+
.exitOverride();
|
|
57
|
+
let activeWrite;
|
|
58
|
+
function globals(command) {
|
|
59
|
+
return command.optsWithGlobals();
|
|
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
|
+
}
|
|
85
|
+
const wrap = (handler) => async (...args) => {
|
|
86
|
+
const command = args.at(-1);
|
|
87
|
+
if (!(command instanceof Command))
|
|
88
|
+
throw new CliError("INTERNAL_ERROR", "Missing command context", 1);
|
|
89
|
+
activeWrite =
|
|
90
|
+
[
|
|
91
|
+
"edit",
|
|
92
|
+
"comment",
|
|
93
|
+
"link-document",
|
|
94
|
+
"link-pr",
|
|
95
|
+
"login",
|
|
96
|
+
"logout",
|
|
97
|
+
"init",
|
|
98
|
+
].includes(command.name()) ||
|
|
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") ||
|
|
103
|
+
(command.name() === "api" &&
|
|
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
|
+
}
|
|
112
|
+
const result = await handler(command);
|
|
113
|
+
emit(kind ? projectOutput(kind, result, selection) : result, command);
|
|
114
|
+
};
|
|
115
|
+
function emit(result, command) {
|
|
116
|
+
const envelope = redact({
|
|
117
|
+
ok: !result.error,
|
|
118
|
+
...(result.outcome
|
|
119
|
+
? { outcome: result.outcome, verification: result.verification }
|
|
120
|
+
: {}),
|
|
121
|
+
data: result.data,
|
|
122
|
+
...(result.error ? { error: result.error } : {}),
|
|
123
|
+
meta: { schemaVersion: 1, apiVersion: API_BASELINE, ...result.meta },
|
|
124
|
+
});
|
|
125
|
+
if (process.argv.includes("--json"))
|
|
126
|
+
console.log(JSON.stringify(envelope));
|
|
127
|
+
else if (result.error)
|
|
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
|
+
}
|
|
139
|
+
else
|
|
140
|
+
console.log(typeof result.data === "string"
|
|
141
|
+
? result.data
|
|
142
|
+
: JSON.stringify(envelope, null, 2));
|
|
143
|
+
process.exitCode = result.exitCode ?? (result.error ? 1 : 0);
|
|
144
|
+
}
|
|
145
|
+
program.configureOutput({
|
|
146
|
+
writeErr: (text) => {
|
|
147
|
+
if (!process.argv.includes("--json"))
|
|
148
|
+
process.stderr.write(String(redact(text)));
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
program
|
|
152
|
+
.command("doctor")
|
|
153
|
+
.description("診斷設定、認證、連線狀態與 API 版本")
|
|
154
|
+
.action(wrap((c) => runDoctor(globals(c))));
|
|
155
|
+
const auth = program
|
|
156
|
+
.command("auth")
|
|
157
|
+
.description("管理綁定 API 服務網址的個人 API 金鑰憑證");
|
|
158
|
+
auth
|
|
159
|
+
.command("login")
|
|
160
|
+
.description("驗證並儲存個人 API 金鑰;隱藏輸入或明確從標準輸入讀取")
|
|
161
|
+
.option("--with-token", "從標準輸入讀取 API 金鑰")
|
|
162
|
+
.addOption(new Option("--storage <kind>", "憑證儲存方式;file 明確選用明文檔案").choices(["file", "keyring"]))
|
|
163
|
+
.action(wrap((c) => runAuth("login", {
|
|
164
|
+
...globals(c),
|
|
165
|
+
...c.opts(),
|
|
166
|
+
})));
|
|
167
|
+
for (const action of ["status", "logout"])
|
|
168
|
+
auth
|
|
169
|
+
.command(action)
|
|
170
|
+
.description(action === "status"
|
|
171
|
+
? "顯示生效中的認證來源與身份,不顯示金鑰"
|
|
172
|
+
: "移除選取的本機憑證;不會撤銷環境變數中的金鑰")
|
|
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))));
|
|
192
|
+
program
|
|
193
|
+
.command("init")
|
|
194
|
+
.description("驗證 --project <team.KEY>,並只在此目錄儲存 project_ref")
|
|
195
|
+
.action(wrap(async (c) => {
|
|
196
|
+
const options = globals(c);
|
|
197
|
+
if (!options.project) {
|
|
198
|
+
throw new CliError("PROJECT_REQUIRED", "Provide --project <team.KEY> to initialize this directory.", 2);
|
|
199
|
+
}
|
|
200
|
+
return runInit(await createContext(options), options.project);
|
|
201
|
+
}));
|
|
202
|
+
const project = program
|
|
203
|
+
.command("project")
|
|
204
|
+
.description("探索專案並檢視工作流程值");
|
|
205
|
+
project
|
|
206
|
+
.command("list")
|
|
207
|
+
.description("列出可用專案(有筆數上限)")
|
|
208
|
+
.option("--limit <number>", "專案數量上限", "30")
|
|
209
|
+
.action(wrap(async (c) => runProject(await createContext(globals(c)), "list", undefined, {
|
|
210
|
+
limit: Number(c.opts().limit),
|
|
211
|
+
})));
|
|
212
|
+
project
|
|
213
|
+
.command("view <ref>")
|
|
214
|
+
.description("讀取欄、標籤、成員、類型與自訂欄位")
|
|
215
|
+
.action(wrap(async (c) => runProject(await createContext(globals(c)), "view", c.args[0])));
|
|
216
|
+
const issue = program
|
|
217
|
+
.command("issue")
|
|
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。`;
|
|
228
|
+
for (const action of [
|
|
229
|
+
"resolve",
|
|
230
|
+
"view",
|
|
231
|
+
"activity",
|
|
232
|
+
"comments",
|
|
233
|
+
"edit",
|
|
234
|
+
"comment",
|
|
235
|
+
]) {
|
|
236
|
+
const command = issue
|
|
237
|
+
.command(action)
|
|
238
|
+
.description({
|
|
239
|
+
resolve: "直接解析識別碼並取得穩定工作項目 ID",
|
|
240
|
+
view: "讀取完整工作項目背景與專案名稱",
|
|
241
|
+
activity: "讀取有分頁游標筆數上限的工作項目活動動態",
|
|
242
|
+
comments: "讀取有分頁游標筆數上限且包含回覆的討論",
|
|
243
|
+
edit: "只更新明確指定的工作項目欄位",
|
|
244
|
+
comment: "發布一則留言或回覆",
|
|
245
|
+
}[action] ?? action)
|
|
246
|
+
.argument("[identifier]", "KEY-42 或專案範圍內的序號")
|
|
247
|
+
.option("--id <id>", "內部工作項目 ID;不可與 identifier 同時使用");
|
|
248
|
+
if (action === "view")
|
|
249
|
+
command
|
|
250
|
+
.option("--comments", "包含最新 25 個留言討論串及下一個分頁游標")
|
|
251
|
+
.addHelpText("after", issueReadExamples);
|
|
252
|
+
if (action === "activity" || action === "comments")
|
|
253
|
+
command
|
|
254
|
+
.option("--limit <number>", "頂層項目數量上限", "25")
|
|
255
|
+
.option("--cursor <cursor>", "此 CLI 回傳的不透明分頁游標");
|
|
256
|
+
if (action === "edit" || action === "comment") {
|
|
257
|
+
command
|
|
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", "只讀取與驗證;顯示確切請求但不寫入");
|
|
263
|
+
if (action === "comment")
|
|
264
|
+
command.option("--reply-to <id>", "回覆此工作項目的留言");
|
|
265
|
+
else
|
|
266
|
+
command
|
|
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", "清除負責人")
|
|
273
|
+
.option("--priority <priority>", "urgent/high/medium/low")
|
|
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], []);
|
|
279
|
+
}
|
|
280
|
+
command.action(wrap(async (c) => runIssue(await createContext(globals(c)), action, c.args[0], {
|
|
281
|
+
...c.opts(),
|
|
282
|
+
project: globals(c).project,
|
|
283
|
+
...issueOutputOptions(c),
|
|
284
|
+
})));
|
|
285
|
+
}
|
|
286
|
+
issue
|
|
287
|
+
.command("list")
|
|
288
|
+
.description("列出或搜尋專案工作項目;僅支援有筆數上限的查詢,不提供接續分頁;完整性未知(unknown)")
|
|
289
|
+
.option("--search <text>", "專案文字搜尋;不可與負責人篩選條件並用")
|
|
290
|
+
.option("--assignee <username|@me>", "依專案成員篩選")
|
|
291
|
+
.option("--limit <number>", "本地範圍為 1–500;伺服器可能回傳較少項目", "30")
|
|
292
|
+
.addHelpText("after", issueReadExamples)
|
|
293
|
+
.action(wrap(async (c) => runIssue(await createContext(globals(c)), "list", undefined, {
|
|
294
|
+
...c.opts(),
|
|
295
|
+
project: globals(c).project,
|
|
296
|
+
...issueOutputOptions(c),
|
|
297
|
+
})));
|
|
298
|
+
const document = program
|
|
299
|
+
.command("document")
|
|
300
|
+
.description("發布 Markdown 文件;專案可見不等於網際網路公開");
|
|
301
|
+
document
|
|
302
|
+
.command("create")
|
|
303
|
+
.description("建立明確設定 private 或 project 可見範圍的文件頁面")
|
|
304
|
+
.requiredOption("--title <text>", "文件標題(1–255 個字元)")
|
|
305
|
+
.addOption(new Option("--visibility <scope>", "project:專案可見,並非網際網路公開;private:僅擁有者可見")
|
|
306
|
+
.choices(["project", "private"])
|
|
307
|
+
.makeOptionMandatory())
|
|
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", "只讀取與驗證;顯示確切請求但不寫入")
|
|
313
|
+
.action(wrap(async (c) => runDocument(await createContext(globals(c)), "create", undefined, {
|
|
314
|
+
...c.opts(),
|
|
315
|
+
project: globals(c).project,
|
|
316
|
+
})));
|
|
317
|
+
for (const action of ["view", "issues"]) {
|
|
318
|
+
document
|
|
319
|
+
.command(`${action} <document-id>`)
|
|
320
|
+
.description(action === "view"
|
|
321
|
+
? "依穩定 ID 讀取可見文件"
|
|
322
|
+
: "列出連結至此文件的工作項目")
|
|
323
|
+
.action(wrap(async (c) => runDocument(await createContext(globals(c)), action, c.args[0], {
|
|
324
|
+
project: globals(c).project,
|
|
325
|
+
})));
|
|
326
|
+
}
|
|
327
|
+
for (const action of ["documents", "link-document"]) {
|
|
328
|
+
const command = issue
|
|
329
|
+
.command(`${action} [identifier]`)
|
|
330
|
+
.description(action === "documents"
|
|
331
|
+
? "列出連結至工作項目的可見文件"
|
|
332
|
+
: "將同專案文件連結至工作項目;已存在連結時不執行操作")
|
|
333
|
+
.option("--id <id>", "內部工作項目 ID;不可與 identifier 同時使用");
|
|
334
|
+
if (action === "link-document")
|
|
335
|
+
command
|
|
336
|
+
.requiredOption("--document <id>", "同一專案中的穩定文件 ID")
|
|
337
|
+
.option("--agent <name>", "宣告代理:codex 或 claude")
|
|
338
|
+
.option("--human", "明確宣告人工作業;不可與代理環境變數並用")
|
|
339
|
+
.option("--dry-run", "預覽確切關聯內容,不執行寫入");
|
|
340
|
+
command.action(wrap(async (c) => runIssueDocument(await createContext(globals(c)), action, c.args[0], {
|
|
341
|
+
...c.opts(),
|
|
342
|
+
project: globals(c).project,
|
|
343
|
+
})));
|
|
344
|
+
}
|
|
345
|
+
for (const action of [
|
|
346
|
+
"repositories",
|
|
347
|
+
"pull-requests",
|
|
348
|
+
"link-pr",
|
|
349
|
+
"development",
|
|
350
|
+
]) {
|
|
351
|
+
const descriptions = {
|
|
352
|
+
repositories: "探索可供工作項目使用的已連接儲存庫",
|
|
353
|
+
"pull-requests": "列出已連接儲存庫的現有 PR/MR(單頁)",
|
|
354
|
+
"link-pr": "依已連接儲存庫 ID 與編號連結現有 PR/MR;不會建立 PR",
|
|
355
|
+
development: "列出工作項目現有的開發連結",
|
|
356
|
+
};
|
|
357
|
+
const command = issue
|
|
358
|
+
.command(`${action} [identifier]`)
|
|
359
|
+
.description(descriptions[action])
|
|
360
|
+
.option("--id <id>", "內部工作項目 ID;不可與 identifier 同時使用");
|
|
361
|
+
if (action === "pull-requests" || action === "link-pr")
|
|
362
|
+
command.requiredOption("--repository <id>", "由 iap issue repositories 列出的已連接儲存庫 ID");
|
|
363
|
+
if (action === "pull-requests")
|
|
364
|
+
command
|
|
365
|
+
.option("--page <number>", "API 頁碼(依 nextPage 繼續)", "1")
|
|
366
|
+
.addOption(new Option("--state <state>", "PR 狀態篩選")
|
|
367
|
+
.choices(["open", "all"])
|
|
368
|
+
.default("open"));
|
|
369
|
+
if (action === "link-pr")
|
|
370
|
+
command
|
|
371
|
+
.requiredOption("--number <number>", "現有 PR/MR 編號;不是任意 URL")
|
|
372
|
+
.option("--agent <name>", "宣告代理:codex 或 claude")
|
|
373
|
+
.option("--human", "明確宣告人工作業;不可與代理環境變數並用")
|
|
374
|
+
.option("--dry-run", "預覽但不連結或建立 PR");
|
|
375
|
+
command.action(wrap(async (c) => runDevelopment(await createContext(globals(c)), action, c.args[0], {
|
|
376
|
+
...c.opts(),
|
|
377
|
+
project: globals(c).project,
|
|
378
|
+
})));
|
|
379
|
+
}
|
|
380
|
+
program
|
|
381
|
+
.command("api <relative-path>")
|
|
382
|
+
.description("原始 API 入口;工作項目與文件寫入必須使用高階指令")
|
|
383
|
+
.addOption(new Option("--method <method>", "HTTP 方法")
|
|
384
|
+
.choices(["GET", "HEAD", "POST", "PATCH", "PUT", "DELETE"])
|
|
385
|
+
.default("GET"))
|
|
386
|
+
.option("--input <file|->", "從檔案或明確指定的標準輸入讀取 JSON 請求本文")
|
|
387
|
+
.option("--dry-run", "預覽但不送出寫入請求")
|
|
388
|
+
.action(wrap(async (c) => runApi(await createContext(globals(c)), c.args[0] ?? "", c.opts())));
|
|
389
|
+
try {
|
|
390
|
+
await program.parseAsync();
|
|
391
|
+
}
|
|
392
|
+
catch (error) {
|
|
393
|
+
if (error instanceof CommanderError && error.exitCode === 0)
|
|
394
|
+
process.exitCode = 0;
|
|
395
|
+
else {
|
|
396
|
+
const known = error instanceof CliError
|
|
397
|
+
? error
|
|
398
|
+
: new CliError(error instanceof CommanderError
|
|
399
|
+
? "INVALID_INPUT"
|
|
400
|
+
: "UNEXPECTED_ERROR", error instanceof Error ? error.message : "Unexpected failure", error instanceof CommanderError ? 2 : 1);
|
|
401
|
+
const words = process.argv.slice(2);
|
|
402
|
+
const rawMethod = words
|
|
403
|
+
.find((word) => word.startsWith("--method="))
|
|
404
|
+
?.slice("--method=".length) ??
|
|
405
|
+
(words.indexOf("--method") >= 0
|
|
406
|
+
? words[words.indexOf("--method") + 1]
|
|
407
|
+
: "GET");
|
|
408
|
+
const writing = activeWrite ??
|
|
409
|
+
((words.includes("issue") &&
|
|
410
|
+
["edit", "comment", "link-document", "link-pr"].some((action) => words.includes(action))) ||
|
|
411
|
+
(words.includes("document") && words.includes("create")) ||
|
|
412
|
+
words.includes("init") ||
|
|
413
|
+
(words.includes("auth") &&
|
|
414
|
+
(words.includes("login") || words.includes("logout"))) ||
|
|
415
|
+
(words.includes("config") &&
|
|
416
|
+
(words.includes("set") || words.includes("unset"))) ||
|
|
417
|
+
(words.includes("api") &&
|
|
418
|
+
/^(POST|PATCH|PUT|DELETE)$/.test(rawMethod ?? "GET")));
|
|
419
|
+
const { outcome, verification, data, meta, ...details } = known.details;
|
|
420
|
+
emit({
|
|
421
|
+
data: data ?? null,
|
|
422
|
+
meta: typeof meta === "object" && meta !== null && !Array.isArray(meta)
|
|
423
|
+
? Object.fromEntries(Object.entries(meta))
|
|
424
|
+
: undefined,
|
|
425
|
+
error: {
|
|
426
|
+
code: known.code,
|
|
427
|
+
message: known.message,
|
|
428
|
+
retryable: false,
|
|
429
|
+
...details,
|
|
430
|
+
},
|
|
431
|
+
exitCode: known.exitCode,
|
|
432
|
+
...(writing
|
|
433
|
+
? {
|
|
434
|
+
outcome: outcome === "applied" || outcome === "unknown"
|
|
435
|
+
? outcome
|
|
436
|
+
: "not_applied",
|
|
437
|
+
verification: verification === "passed" || verification === "failed"
|
|
438
|
+
? verification
|
|
439
|
+
: "not_run",
|
|
440
|
+
}
|
|
441
|
+
: {}),
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type Context, type GlobalOptions, type Result } from "./contracts.js";
|
|
2
|
+
export declare function runDoctor(options: GlobalOptions): Promise<Result>;
|
|
3
|
+
export declare function runAuth(action: string, options: GlobalOptions & {
|
|
4
|
+
withToken?: boolean;
|
|
5
|
+
storage?: string;
|
|
6
|
+
}): Promise<Result>;
|
|
7
|
+
export declare function runProject(ctx: Context, action: string, ref?: string, options?: {
|
|
8
|
+
limit?: number;
|
|
9
|
+
}): Promise<Result>;
|
|
10
|
+
export declare function runInit(ctx: Context, project: string): Promise<Result>;
|
|
11
|
+
export declare function runApi(ctx: Context, path: string, options: {
|
|
12
|
+
method?: string;
|
|
13
|
+
input?: string;
|
|
14
|
+
dryRun?: boolean;
|
|
15
|
+
}): Promise<Result>;
|