feat: extend GitHub trans route reads

This commit is contained in:
Codex
2026-06-15 03:33:14 +00:00
parent 008f7fbb62
commit b2c2819fe2
5 changed files with 109 additions and 21 deletions
+74 -9
View File
@@ -36,6 +36,16 @@ interface CommandResult {
parsed: Record<string, unknown>;
}
type GhListState = "open" | "closed" | "all";
interface GhListOptions {
limit: number;
full: boolean;
state: GhListState;
search: string | null;
labels: string[];
}
function record(value: unknown): Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : {};
}
@@ -205,16 +215,65 @@ function readIssueMetadata(repo: string, number: number): { state: string | null
};
}
function parseListOptions(args: string[]): { limit: number; full: boolean } {
const limitIndex = args.indexOf("--limit");
return {
limit: limitIndex >= 0 ? positiveRouteNumber(args[limitIndex + 1] ?? "", "--limit") : 30,
full: args.includes("--full"),
};
function parseListState(raw: string): GhListState {
if (raw === "open" || raw === "closed" || raw === "all") return raw;
throw new Error("gh route list --state must be open, closed, or all");
}
function optionValue(args: string[], index: number, name: string): { value: string; nextIndex: number } {
const arg = args[index] ?? "";
if (arg.startsWith(`${name}=`)) return { value: arg.slice(name.length + 1), nextIndex: index };
const value = args[index + 1];
if (value === undefined || value.length === 0) throw new Error(`gh route list ${name} requires a value`);
return { value, nextIndex: index + 1 };
}
function parseLabels(raw: string): string[] {
const labels = raw.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
if (labels.length === 0) throw new Error("gh route issue ls --label requires at least one non-empty label");
return labels;
}
function parseListOptions(args: string[], kind: "pr" | "issue"): GhListOptions {
const options: GhListOptions = { limit: 30, full: false, state: "all", search: null, labels: [] };
for (let index = 0; index < args.length; index += 1) {
const arg = args[index] ?? "";
if (arg === "--full") {
options.full = true;
continue;
}
if (arg === "--limit" || arg.startsWith("--limit=")) {
const parsed = optionValue(args, index, "--limit");
options.limit = positiveRouteNumber(parsed.value, "--limit");
index = parsed.nextIndex;
continue;
}
if (arg === "--state" || arg.startsWith("--state=")) {
const parsed = optionValue(args, index, "--state");
options.state = parseListState(parsed.value);
index = parsed.nextIndex;
continue;
}
if (arg === "--search" || arg.startsWith("--search=")) {
if (kind !== "issue") throw new Error("gh route --search is only supported for issue ls");
const parsed = optionValue(args, index, "--search");
options.search = parsed.value;
index = parsed.nextIndex;
continue;
}
if (arg === "--label" || arg.startsWith("--label=")) {
if (kind !== "issue") throw new Error("gh route --label is only supported for issue ls");
const parsed = optionValue(args, index, "--label");
options.labels.push(...parseLabels(parsed.value));
index = parsed.nextIndex;
continue;
}
throw new Error(`unsupported gh route ${kind} ls arg: ${arg}`);
}
return options;
}
function listRoute(route: GhContentRoute, args: string[]): number {
const options = parseListOptions(args);
if (route.area === "root") {
printDirectory([
{ name: "pr/", type: "dir", description: "pull request directory" },
@@ -223,7 +282,8 @@ function listRoute(route: GhContentRoute, args: string[]): number {
return 0;
}
if (route.area === "pr-list") {
const result = cliJson(["gh", "pr", "list", "--repo", route.repo, "--state", "all", "--limit", String(options.limit), "--json", "number,title,state,url,updatedAt,createdAt,author,headRefName,baseRefName,draft"], 80_000);
const options = parseListOptions(args, "pr");
const result = cliJson(["gh", "pr", "list", "--repo", route.repo, "--state", options.state, "--limit", String(options.limit), "--json", "number,title,state,url,updatedAt,createdAt,author,headRefName,baseRefName,draft"], 80_000);
if (!result.ok) throw new Error(result.stderr || result.stdout || "gh pr list failed");
const prs = commandData(result).pullRequests;
const rows = Array.isArray(prs) ? prs.map((item) => prDirectoryEntry(route.repo, record(item), options.full)) : [];
@@ -231,7 +291,11 @@ function listRoute(route: GhContentRoute, args: string[]): number {
return 0;
}
if (route.area === "issue-list") {
const result = cliJson(["gh", "issue", "list", "--repo", route.repo, "--state", "all", "--limit", String(options.limit), "--json", "number,title,state,url,updatedAt,createdAt,author"], 80_000);
const options = parseListOptions(args, "issue");
const issueArgs = ["gh", "issue", "list", "--repo", route.repo, "--state", options.state, "--limit", String(options.limit), "--json", "number,title,state,url,updatedAt,createdAt,author"];
if (options.search !== null) issueArgs.push("--search", options.search);
for (const label of options.labels) issueArgs.push("--label", label);
const result = cliJson(issueArgs, 80_000);
if (!result.ok) throw new Error(result.stderr || result.stdout || "gh issue list failed");
const issues = commandData(result).issues;
const rows = Array.isArray(issues) ? issues.map((item) => issueDirectoryEntry(route.repo, record(item), options.full)) : [];
@@ -306,6 +370,7 @@ function printDirectory(rows: Array<Record<string, unknown>>): void {
row.commentCount === undefined || row.commentCount === null ? null : `commentCount=${String(row.commentCount)}`,
row.bodyChars === undefined || row.bodyChars === null ? null : `bodyChars=${String(row.bodyChars)}`,
row.updatedAt === undefined || row.updatedAt === null ? null : `updatedAt=${String(row.updatedAt)}`,
row.url === undefined || row.url === null || row.url === "" ? null : `url=${String(row.url)}`,
row.title === undefined || row.title === "" ? null : `title=${String(row.title)}`,
].filter((item): item is string => item !== null);
process.stdout.write(`${fields.join("\t")}\n`);
+1 -1
View File
@@ -23,7 +23,7 @@ export function rootHelp(): unknown {
{ command: "server restart <backend-core|frontend|dev-frontend-proxy|provider-gateway|todo-note|code-queue-mgr|project-manager|baidu-netdisk|oa-event-flow>", description: "No-build single-service Compose restart for reviewed main-server maintenance recovery; returns an async job and validates the recreated container." },
{ command: "provider attach <providerId> [--master-server URL] [--up] [--force] | provider triage <providerId> [--observed-error text] [--observed-scope scope] [--microservice id ...] [--full|--raw]", description: "Generate the minimal external provider-gateway env/compose bundle or run the low-noise read-only provider health triage contract." },
{ command: "trans <route> [operation args...] (alias of ssh <route> ...)", description: "Open a Host SSH / WSL SSH maintenance session; provider WebSocket carries control and host.ssh.tcp-pool carries stdin/stdout/stderr data." },
{ command: "trans gh:/owner/repo[/pr|/issue][/number[/1]] ls|cat|rg|apply-patch", description: "Treat GitHub PRs/issues as virtual text directories; `ls --full` shows state/floors/body length, and `apply-patch` updates first-floor `body.md` through UniDesk gh plus apply-patch v2." },
{ command: "trans gh:/owner/repo[/pr|/issue][/number[/1]] ls|cat|rg|apply-patch", description: "Treat GitHub PRs/issues as virtual text directories; `issue ls --search/--state` covers filtered reads, `cat|rg` reads first-floor body text, and `apply-patch` updates `body.md` through UniDesk gh plus apply-patch v2." },
{ command: "trans <route> apply-patch < patch.diff", description: "Default remote text patch entry: apply a standard patch with the local TypeScript v2 engine while the remote route only reads and writes files." },
{ command: "trans <route> upload <local-file> <remote-file> | trans <route> download <remote-file> <local-file>", description: "Transfer whole files through SSH passthrough with automatic endpoint byte/SHA-256 verification; downloads stream stdout over host.ssh.tcp-pool." },
{ command: "trans <providerId> apply-patch-v1 [tool args...] < patch.diff", description: "Fallback to the injected legacy remote apply_patch helper directly over SSH passthrough and stream the patch from local stdin." },