feat: extend GitHub trans route reads
This commit is contained in:
+74
-9
@@ -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
@@ -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." },
|
||||
|
||||
Reference in New Issue
Block a user