From 2346c620b7dae1a67fed40e56774d701ce4c56f2 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 1 Jun 2026 13:40:56 +0000 Subject: [PATCH] feat: support gh issue search --- docs/reference/cli.md | 2 +- scripts/gh-cli-issue-guard-contract-test.ts | 11 ++++ scripts/src/gh.ts | 57 ++++++++++++++++----- 3 files changed, 55 insertions(+), 15 deletions(-) diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 1bd611ee..a14f0271 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -58,7 +58,7 @@ CI/CD、GitOps、rollout、artifact 发布、PR 合并后的 DEV/PROD 滚动、P - `hwlab cd status|audit|preflight|apply --env dev [--dry-run]` 是旧 D601 HWLAB DEV CD 指挥侧 wrapper,仅用于显式 legacy 诊断和迁移对照。默认通过 UniDesk provider `host.ssh` 进入 D601,再调用 HWLAB repo-owned `scripts/dev-cd-apply.mjs`,不内嵌发布 kubectl 逻辑:`status` 汇总固定 CD mirror、Git clean/main/origin-main、`deploy/deploy.json`/artifact catalog/report、D601 native k3s guard 和 CD Lease lock,并用 `scripts/dev-cd-apply.mjs --status --skip-live-verify` 取得 target/promotion 摘要;`audit` 在 k3s/CD 恢复后做只读健康审计,返回有界 JSON 的 blocker 分类、D601 guard/node、SecretRef 存在性、registry 可达性、Lease phase/holder/staleness、deploy.json 与 artifact/workload image 收敛、current Deployment image/revision/rollout、16666/16667 public health commit/readiness 和 DB/runtime durability 摘要;`preflight` 进一步检查必需 SecretRef 对象/键存在性并运行 HWLAB `scripts/dev-cd-apply.mjs --dry-run --skip-live-verify` 受控事务摘要。完整远端 stdout/stderr 写入 D601 `~/.state/unidesk-hwlab-cd//` 和本地 `.state/hwlab-cd//` task dump,stdout 只返回有界摘要。默认 HWLAB CD repo 是 `/home/ubuntu/hwlab_cd`,`/home/ubuntu/hwlab` runner 历史目录不得作为发布真相。wrapper 强制 `KUBECONFIG=/etc/rancher/k3s/k3s.yaml` 并只以这个显式目标作为 gate;显式目标出现 `docker-desktop`、`desktop-control-plane` 或 `127.0.0.1:11700` 信号会结构化拒绝,audit/preflight/apply --dry-run 都必须观察到 node `d601`。真实 apply 只暴露 `scripts/dev-cd-apply.mjs --apply --confirm-dev --confirmed-non-production --write-report` 命令形状并标注 host-commander-only,本 runner 不执行 live apply、rollout、Lease mutation 或 DEV deploy apply。长期规则见 `docs/reference/hwlab.md`。 - `gh auth status [--repo owner/name]` 探测 GitHub 操作前置条件并输出脱敏 JSON:是否存在 `gh` binary、是否存在 `GH_TOKEN`/`GITHUB_TOKEN` 或可用 `gh auth token` fallback、REST API 是否可达、目标 repo 是否可见、issue 是否可读。degraded reason 必须归类为 `missing-binary`、`missing-token`、`auth-failed`、`github-transient`、`network-proxy-failed`、`permission-denied`、`repo-not-found`、`repo-forbidden`、`issue-not-found`、`pr-not-found`、`scope-insufficient`、`validation-failed`、`invalid-response` 或 `unsupported-command`,不得打印 token;失败对象必须包含 `runnerDisposition=infra-blocked|business-failed`,runner 应优先用该字段分流。`github-transient` 表示 GitHub DNS/API 连接在收到 HTTP 状态前失败,输出应带 `retryable=true` 或等价 commander action;这不是缺 token、认证失败、权限不足或 PR 语义失败。 - `codex prompt-lint [prompt|--prompt-file path|--prompt-stdin]` 是派发/steer 前的本地 dry-run prompt lint。它只读取 prompt 文本,返回 `dryRun=true`、`mutation=false`、`declaredClass`、`effectiveClass`、`requiredClass`、`dispatchDisposition`、缺失或矛盾项和有界 evidence,不访问 live service、不提交任务、不打印完整 prompt。分级固定为 `read-only`、`live-read`、`live-mutating`;未声明时按 `read-only` 处理。`codex submit --dry-run` 与 `codex steer --dry-run` 会嵌入同一 `promptLint` 结果,帮助指挥官在 dispatch/steer 前发现缺失或矛盾的 live mutation 授权。长期规则见 `docs/reference/code-queue-supervision.md` 的 DEV 测试授权分级。 -- `gh issue list [owner/repo] [--state open|closed|all] [--limit N] [--label label[,label...]]... [--repo owner/name] [--json number,title,state,url,updatedAt,createdAt,author,labels] [--raw|--full]` 通过 GitHub REST 列出 issue,默认 `state=open`、`limit=30`,输出稳定 JSON 且不依赖系统 `gh` binary。`owner/repo` 位置参数是 `--repo owner/repo` 的兼容别名;若位置 repo 与 `--repo` 冲突,或位置参数不是 `owner/repo`,必须结构化失败,禁止静默 fallback 到默认 repo。`--limit` 会映射到 GitHub `per_page` 并限制返回数量,避免一次拉爆上下文;未知 state 或未知 `--json` 字段必须结构化失败并带 `runnerDisposition=business-failed`。`--label` 是 GitHub REST `labels=label1,label2` 服务端过滤,支持重复 `--label` 和逗号分隔;filter 不在本命令上下文中使用(如 `issue read`、`pr list`)必须结构化失败并指明 `gh issue create and gh issue list` 才是合法作用域。GitHub issues API 可能混入 PR,CLI 会从 `.data.issues` 中过滤 pull request。`--raw|--full` 在 `gh issue list` 上是绕过 20 KiB stdout 截断的显式开关:响应结果会带 `noDump=true`,`output.ts` 据此跳过 head/tail 替换并把完整数据 inline 输出;当响应未超阈值时 `--raw|--full` 行为等价默认。 +- `gh issue list [owner/repo] [--state open|closed|all] [--limit N] [--search text] [--label label[,label...]]... [--repo owner/name] [--json number,title,state,url,updatedAt,createdAt,author,labels] [--raw|--full]` 通过 GitHub REST 列出 issue,默认 `state=open`、`limit=30`,输出稳定 JSON 且不依赖系统 `gh` binary。`owner/repo` 位置参数是 `--repo owner/repo` 的兼容别名;若位置 repo 与 `--repo` 冲突,或位置参数不是 `owner/repo`,必须结构化失败,禁止静默 fallback 到默认 repo。`--limit` 会映射到 GitHub `per_page` 并限制返回数量,避免一次拉爆上下文;`--search` 使用 GitHub Search Issues API,并自动追加 `repo:/`、`type:issue` 和 state qualifier,用于创建新 issue 前做低摩擦查重;未知 state 或未知 `--json` 字段必须结构化失败并带 `runnerDisposition=business-failed`。`--label` 是 GitHub REST `labels=label1,label2` 服务端过滤,支持重复 `--label` 和逗号分隔;filter 不在本命令上下文中使用(如 `issue read`、`pr list`)必须结构化失败并指明 `gh issue create and gh issue list` 才是合法作用域。GitHub issues API 可能混入 PR,CLI 会从 `.data.issues` 中过滤 pull request。`--raw|--full` 在 `gh issue list` 上是绕过 20 KiB stdout 截断的显式开关:响应结果会带 `noDump=true`,`output.ts` 据此跳过 head/tail 替换并把完整数据 inline 输出;当响应未超阈值时 `--raw|--full` 行为等价默认。 - `gh issue lifecycle`:`--state` 只能作为 `gh issue list` / `gh issue board-row list` / `gh pr list` 的过滤参数;`gh issue update` / `gh issue edit` 只写 body/title,**不接受** `--state` 改 open/closed。把 `gh issue update --state closed` 落到错命令上时,CLI 必须返回 `validation-failed` 并显式提示 `gh issue close ` / `gh issue reopen `(PR 用 `gh pr close|reopen `),并把 5 条受支持命令放进 `supportedCommands`,禁止把"无 `--state` 改 issue 状态"的命令升级为"接受 `--state`"。issue 硬删除走 `close`,PR 硬删除走 `close`,两者都没有"delete"语义。 - `gh issue read [--repo owner/name] [--json body,title,state,comments] [--raw|--full]` 通过 GitHub REST 读取 issue title/body/state/url 和 comments,默认输出 JSON;`view` 只保留为兼容别名。`owner/repo#number` shorthand 会自动派生 `--repo owner/repo` 和 issue number;若同时提供冲突的显式 `--repo`,CLI 必须结构化失败并给出 `gh issue read --repo owner/repo --json body,title,state,comments` 与 shorthand raw 的可执行命令。兼容旧脚本的 `--json body` 和 `--json body,title,state,comments` 字段选择,且正文仍稳定暴露在 `.data.issue.body`,避免调用方因为 JSON 路径变化把空值当成正文。字段白名单是 `body,title,state,comments,number,url,author,createdAt,updatedAt`,未知字段必须结构化失败并带 `runnerDisposition=business-failed`。`--raw` 与 `--full` 是显式完整披露别名:read/view 会选择完整支持字段集;issue update/edit 只有显式传入时才在成功响应里包含完整 `.data.issue.body`。当最终 `gh` JSON 超过 20 KiB 时,CLI 必须把完整 JSON 写入 `/tmp/unidesk-cli-output/*.json`,stdout 只返回 `outputTruncated=true`、dump path、总 bytes/lines 和 head/tail 预览。默认 list/read 输出仍不得扩散到无界非 JSON 文本。`gh issue create --title --body-file <file|-> [--label label[,label...]]... [--dry-run]`、`gh issue update <number> --mode replace|append --body-file <file|-> [--title ...] [--dry-run] [--full|--raw]`、`gh issue comment create <number> (--body-file <file|->|--body <short-text>) [--dry-run]`、`gh issue comment delete <commentId> [--dry-run]`、`gh issue close|reopen <number> [--dry-run]` 都走 REST,不依赖 `gh` binary。`--body` 仅用于 issue comment 的短单行文本;空白、多行、疑似 shell 污染、secret-like 或过长 inline body 必须结构化失败,Markdown/生成内容/长评论继续用 `--body-file <file|->`。`--label` 仅用于 `issue create`,支持重复传入和逗号分隔;`--dry-run` 会展示解析后的 labels 与 request plan,正式创建时把 labels 放入 GitHub REST create-issue payload,GitHub 返回不存在 label 等 422 校验失败时 CLI 结构化返回 `validation-failed`,不静默成功。`gh issue delete <number>` 是结构化 `unsupported-command`,因为 GitHub REST 不支持 issue 硬删除;生命周期删除语义请使用 `close`。 - `gh issue update <number> --mode replace|append --body-file <file|->` 是正文更新主入口,`edit` 保留为兼容别名。`replace` 用文件或 stdin 正文替换现有 body;`append` 先读取当前 body,再按 UTF-8 文件或 stdin 字节追加,保留真实换行、反引号和 Markdown 表格。更新默认拒绝字面量 `null`、空白正文和过短正文;只有真实需要写短正文时才允许显式加 `--allow-short-body`,返回 JSON 会报告该风险。#20 总看板和指挥简报类 issue 是长期 body-only issue,`--body-profile auto` 会按 issue number 自动启用 #20/#24 legacy guard:#20 必须包含 `## 看板(OPEN)`,#24 legacy 指挥简报必须包含 `## 常驻观察与长期建议`。显式 `--body-profile commander-brief` 不再固定 #24;#24 仍兼容,标题为 `YYYY-MM-DD 指挥简报(北京时间)` 或既有正文首行/关键 heading 表明为每日滚动指挥简报的 issue 也合法,并仍必须包含 `## 常驻观察与长期建议`。对非简报 issue 显式使用 `commander-brief` 会结构化失败为 `profile-issue-mismatch`。`--dry-run` 不 PATCH GitHub,输出有界 `bodyPreview`/`bodyPreviewLines`、新正文长度、SHA、关键标题检查结果、字面量 `\n`、反引号、Markdown 表格、shell 污染信号、`guard`、`concurrency`、`bodyOnlySafety` 和 `wouldPatch`;若环境里有 `GH_TOKEN` 或 `GITHUB_TOKEN`,dry-run 还会只读抓取旧正文长度、SHA 和 `updatedAt` 作为更新前对照。正式写入默认先读取当前 issue,执行 guard 和显式 `--expect-*` 并发校验,再 PATCH;成功输出 compact issue 摘要、old/new body SHA、updatedAt、bodySource 和 drill-down `readCommands`,不包含完整 `issue.body`。完整正文必须显式 `--full|--raw` 或后续执行 `readCommands.body/full/raw` 获取。 diff --git a/scripts/gh-cli-issue-guard-contract-test.ts b/scripts/gh-cli-issue-guard-contract-test.ts index db8cbfe8..26c5f0e5 100644 --- a/scripts/gh-cli-issue-guard-contract-test.ts +++ b/scripts/gh-cli-issue-guard-contract-test.ts @@ -525,6 +525,10 @@ async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockReque sendJson(res, 200, issueList.slice(0, 2)); return; } + if (req.method === "GET" && req.url === "/search/issues?q=AgentRun+final+response+repo%3ApikasTech%2Funidesk+type%3Aissue&per_page=4") { + sendJson(res, 200, { total_count: 1, incomplete_results: false, items: issueList.slice(0, 1) }); + return; + } if (req.method === "GET" && req.url === "/repos/pikasTech/unidesk/issues?state=open&per_page=5") { sendJson(res, 200, issueList); return; @@ -656,6 +660,7 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> { assertCondition(usage.some((line) => line.includes("gh issue board-row upsert")), "gh help should list board-row upsert", { usage }); assertCondition(usage.some((line) => line.includes("gh issue board-row move")), "gh help should list board-row move", { usage }); assertCondition(usage.some((line) => line.includes("gh issue board-row delete")), "gh help should list board-row delete", { usage }); + assertCondition(usage.some((line) => line.includes("gh issue list") && line.includes("--search text")), "gh help should list issue list search", { usage }); assertCondition(notes.some((line) => line.includes("canonical read path")), "gh help should state issue read is canonical", { notes }); assertCondition(notes.some((line) => line.includes("compatibility alias")), "gh help should state issue view is alias", { notes }); assertCondition(notes.some((line) => line.includes("owner/repo#number shorthand")), "gh help should explain read/view shorthand", { notes }); @@ -696,6 +701,12 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> { assertCondition(listDefaultStateData.state === "open", "issue list should keep default state=open", listDefaultStateData); assertCondition(mock.requests.some((request) => request.method === "GET" && request.url === "/repos/pikasTech/unidesk/issues?state=open&per_page=2"), "issue list default should query state=open", mock.requests); + const searchList = await runCli(["gh", "issue", "list", "--repo", "pikasTech/unidesk", "--state", "all", "--limit", "4", "--search", "AgentRun final response", "--json", "number,title,state,url"], env); + assertCondition(searchList.status === 0, "issue list should support search query", searchList.json ?? { stdout: searchList.stdout }); + const searchListData = dataOf(searchList.json ?? {}); + assertCondition(searchListData.search === "AgentRun final response", "issue list should expose search query", searchListData); + assertCondition(mock.requests.some((request) => request.method === "GET" && request.url === "/search/issues?q=AgentRun+final+response+repo%3ApikasTech%2Funidesk+type%3Aissue&per_page=4"), "issue list search should use GitHub Search Issues API with repo/type qualifiers", mock.requests); + const positionalRepoList = await runCli(["gh", "issue", "list", "pikasTech/HWLAB", "--state", "open", "--limit", "2", "--json", "number,title,state,url"], env); assertCondition(positionalRepoList.status === 0, "issue list positional owner/repo should succeed", positionalRepoList.json ?? { stdout: positionalRepoList.stdout }); const positionalRepoListData = dataOf(positionalRepoList.json ?? {}); diff --git a/scripts/src/gh.ts b/scripts/src/gh.ts index 13490ac9..c8a1ba41 100644 --- a/scripts/src/gh.ts +++ b/scripts/src/gh.ts @@ -35,6 +35,7 @@ const GH_VALUE_OPTIONS = new Set([ "--expect-updated-at", "--expect-body-sha", "--body-profile", "--label", "--field", "--value", "--section", "--to", "--status", "--row-file", "--category", "--branch", "--tasks", "--summary", "--focus", "--validation", "--progress", "--number", "--pr", + "--search", ]); const GH_FLAG_OPTIONS = new Set(["--dry-run", "--draft", "--notify-claudeqq-brief-diff", "--allow-short-body", "--raw", "--full", "--stat", "--merge", "--squash", "--rebase", "--delete-branch"]); const MIN_SAFE_BODY_SCAN_CHARS = MIN_SAFE_ISSUE_BODY_CHARS; @@ -324,6 +325,7 @@ interface GitHubOptions { notifyClaudeQqBriefDiff: boolean; allowShortBody: boolean; labels: string[]; + search?: string; title?: string; body?: string; bodyFile?: string; @@ -411,6 +413,12 @@ interface GitHubComment { updated_at?: string; } +interface GitHubIssueSearchResponse { + total_count?: number; + incomplete_results?: boolean; + items?: GitHubIssue[]; +} + interface GitHubPullRequest { id: number; number: number; @@ -765,6 +773,7 @@ function parseOptions(args: string[]): GitHubOptions { notifyClaudeQqBriefDiff: hasFlag(args, "--notify-claudeqq-brief-diff"), allowShortBody: hasFlag(args, "--allow-short-body"), labels: labelsOption(args), + search: optionValue(args, "--search"), title: optionValue(args, "--title"), body: optionValue(args, "--body"), bodyFile: optionValue(args, "--body-file"), @@ -4906,9 +4915,17 @@ async function listIssueComments(token: string, repo: string, issueNumber: numbe return githubRequest<GitHubComment[]>(token, "GET", `/repos/${owner}/${name}/issues/${issueNumber}/comments?per_page=100`); } -async function listIssues(token: string, repo: string, state: IssueListState, limit: number): Promise<GitHubIssue[] | GitHubErrorPayload> { +async function listIssues(token: string, repo: string, state: IssueListState, limit: number, search?: string): Promise<GitHubIssue[] | GitHubErrorPayload | GitHubIssueSearchResponse> { const { owner, name } = repoParts(repo); - return githubRequest<GitHubIssue[]>(token, "GET", `/repos/${owner}/${name}/issues?state=${state}&per_page=${limit}`); + const normalizedSearch = String(search ?? "").trim(); + if (normalizedSearch) { + const qualifiers = [`repo:${owner}/${name}`, "type:issue"]; + if (state !== "all") qualifiers.push(`state:${state}`); + const params = new URLSearchParams({ q: `${normalizedSearch} ${qualifiers.join(" ")}`, per_page: String(limit) }); + return githubRequest<GitHubIssueSearchResponse>(token, "GET", `/search/issues?${params.toString()}`); + } + const params = new URLSearchParams({ state, per_page: String(limit) }); + return githubRequest<GitHubIssue[]>(token, "GET", `/repos/${owner}/${name}/issues?${params.toString()}`); } async function getIssue(token: string, repo: string, issueNumber: number): Promise<GitHubIssue | GitHubErrorPayload> { @@ -4958,10 +4975,13 @@ async function issueView(repo: string, token: string, issueNumber: number, jsonF return issueRead(repo, token, issueNumber, jsonFields, "issue view", disclosure); } -async function issueList(repo: string, token: string, state: IssueListState, limit: number, jsonFields: IssueListJsonField[] | undefined): Promise<GitHubCommandResult> { - const rawIssues = await listIssues(token, repo, state, limit); - if (isGitHubError(rawIssues)) return commandError("issue list", repo, rawIssues, { state, limit }); - const issues = rawIssues.filter((issue) => issue.pull_request === undefined).slice(0, limit); +async function issueList(repo: string, token: string, state: IssueListState, limit: number, jsonFields: IssueListJsonField[] | undefined, search?: string): Promise<GitHubCommandResult> { + const normalizedSearch = String(search ?? "").trim(); + const rawIssues = await listIssues(token, repo, state, limit, normalizedSearch); + if (isGitHubError(rawIssues)) return commandError("issue list", repo, rawIssues, { state, limit, search: normalizedSearch || null }); + const searchResponse = Array.isArray(rawIssues) ? null : rawIssues; + const rawIssueItems = Array.isArray(rawIssues) ? rawIssues : Array.isArray(rawIssues.items) ? rawIssues.items : []; + const issues = rawIssueItems.filter((issue) => issue.pull_request === undefined).slice(0, limit); const fields = jsonFields ?? ISSUE_LIST_JSON_FIELDS.slice(); return { ok: true, @@ -4969,14 +4989,19 @@ async function issueList(repo: string, token: string, state: IssueListState, lim repo, state, limit, + search: normalizedSearch || null, count: issues.length, - rawCount: rawIssues.length, + rawCount: rawIssueItems.length, + searchTotalCount: searchResponse?.total_count, + searchIncomplete: searchResponse?.incomplete_results, jsonFields: fields, issues: issues.map((issue) => issueListSummary(issue, fields)), request: { method: "GET", - path: "/repos/{owner}/{repo}/issues", - query: { state, per_page: limit }, + path: normalizedSearch ? "/search/issues" : "/repos/{owner}/{repo}/issues", + query: normalizedSearch + ? { q: `${normalizedSearch} repo:${repo} type:issue${state === "all" ? "" : ` state:${state}`}`, per_page: limit } + : { state, per_page: limit }, }, note: "GitHub's issues endpoint may include pull requests; this command filters pull requests from .issues.", }; @@ -5934,7 +5959,7 @@ export function ghHelp(): unknown { output: "json", usage: [ "bun scripts/cli.ts gh auth status [--repo owner/name]", - "bun scripts/cli.ts gh issue list [owner/repo] [--state open|closed|all] [--limit N] [--repo owner/name] [--json number,title,state,url,updatedAt,createdAt,author,labels]", + "bun scripts/cli.ts gh issue list [owner/repo] [--state open|closed|all] [--limit N] [--search text] [--repo owner/name] [--json number,title,state,url,updatedAt,createdAt,author,labels]", "bun scripts/cli.ts gh issue read <number|owner/repo#number> [--repo owner/name] [--json body,title,state,comments] [--raw|--full]", "bun scripts/cli.ts gh issue view <number|owner/repo#number> [--repo owner/name] [--raw|--full] [compatibility alias for issue read]", "bun scripts/cli.ts gh issue create --title <title> --body-file <file|-> [--label label[,label...]]... [--repo owner/name] [--dry-run]", @@ -5977,7 +6002,7 @@ export function ghHelp(): unknown { "Issue and PR create/read/update/comment/close/reopen use GitHub REST and do not require the gh binary when GH_TOKEN or GITHUB_TOKEN is present.", "Token values are never printed; auth status reports only token source and presence.", "issue list and pr list accept a single positional owner/repo as a compatibility alias for --repo owner/name. The positional repo and --repo must match if both are supplied; non-repo positionals fail structurally instead of falling back to the default repo.", - "issue list defaults to --state open and bounded --limit 30; supported --json fields are number,title,state,url,updatedAt,createdAt,author,labels and unknown fields fail structurally.", + "issue list defaults to --state open and bounded --limit 30; --search uses GitHub Search Issues API with repo/type/state qualifiers for low-friction dedupe lookup before creating a new issue. Supported --json fields are number,title,state,url,updatedAt,createdAt,author,labels and unknown fields fail structurally.", "PR list defaults to --state all for compatibility with earlier UniDesk CLI behavior; supported states are open, closed, and all.", "issue read is the canonical read path; view remains a compatibility alias. Read/view accept owner/repo#number shorthand and derive --repo unless an explicit conflicting --repo is supplied, which fails structurally with suggested commands. Read supports legacy --json field selection such as --json body and still exposes .data.issue.body for compatibility; unsupported fields fail structurally.", "--raw and --full are explicit full-disclosure aliases for gh issue read/view/update/edit and gh pr read/view. For issue writes, default success output omits full issue.body and returns bodyChars/bodySha/bodyPreview plus readCommands; --full|--raw includes the full returned issue body.", @@ -6110,9 +6135,13 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult const command = [top, sub, third].filter((value): value is string => value !== undefined).join(" ") || "gh"; return validationError(command, options.repo, "--category, --branch, --tasks, --summary, --focus, --validation, and --progress are only supported by gh issue board-row upsert"); } - if (optionWasProvided(args, "--label") && !(top === "issue" && sub === "create")) { + if (optionWasProvided(args, "--label") && !(top === "issue" && (sub === "create" || sub === "list"))) { const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh"; - return validationError(command, options.repo, "--label is only supported by gh issue create"); + return validationError(command, options.repo, "--label is only supported by gh issue create and gh issue list"); + } + if (optionWasProvided(args, "--search") && !(top === "issue" && sub === "list")) { + const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh"; + return validationError(command, options.repo, "--search is only supported by gh issue list"); } if (top === "auth" && sub === "status") return authStatus(options.repo); @@ -6205,7 +6234,7 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult const missing = authRequired(options.repo, `issue ${sub ?? ""}`.trim(), probe); if (missing !== null || token === null) return missing ?? authRequired(options.repo, `issue ${sub ?? ""}`.trim(), { present: false, source: null, ghFallbackAttempted: true }); - if (sub === "list") return issueList(options.repo, token, options.listState, options.limit, options.issueListJsonFields); + if (sub === "list") return issueList(options.repo, token, options.listState, options.limit, options.issueListJsonFields, options.search); if (sub === "create") return issueCreate(options.repo, token, options); if (sub === "edit") return issueEdit(options.repo, token, parseNumber(third, "issue edit"), options); if (sub === "update") return issueEdit(options.repo, token, parseNumber(third, "issue update"), options, "issue update");