feat: add safe GitHub CLI wrapper

This commit is contained in:
Codex
2026-05-20 08:32:29 +00:00
parent a6e21ec201
commit ca74677703
6 changed files with 641 additions and 2 deletions
+618
View File
@@ -0,0 +1,618 @@
import { execFileSync } from "node:child_process";
import { existsSync, readFileSync } from "node:fs";
const DEFAULT_REPO = "pikasTech/unidesk";
const GITHUB_API = "https://api.github.com";
const USER_AGENT = "unidesk-cli-gh";
const PREVIEW_CHARS = 240;
type GitHubDegradedReason =
| "missing-binary"
| "missing-token"
| "egress-failed"
| "permission-denied"
| "repo-not-found"
| "issue-not-found"
| "invalid-response"
| "unsupported-command";
interface GitHubCommandResult {
ok: boolean;
repo: string;
command: string;
degradedReason?: GitHubDegradedReason;
degraded?: GitHubDegradedReason[];
[key: string]: unknown;
}
interface GitHubTokenProbe {
present: boolean;
source: "GH_TOKEN" | "GITHUB_TOKEN" | "gh-auth-token" | null;
ghFallbackAttempted: boolean;
}
interface GitHubOptions {
repo: string;
dryRun: boolean;
limit: number;
title?: string;
bodyFile?: string;
}
interface GitHubErrorPayload {
ok: false;
degradedReason: GitHubDegradedReason;
status?: number;
message: string;
details?: unknown;
}
interface GitHubIssue {
id: number;
number: number;
title: string;
body: string | null;
state: string;
html_url: string;
comments: number;
user?: { login?: string };
created_at?: string;
updated_at?: string;
}
interface GitHubComment {
id: number;
body: string | null;
html_url: string;
user?: { login?: string };
created_at?: string;
updated_at?: string;
}
interface GitHubPullRequest {
id: number;
number: number;
title: string;
body: string | null;
state: string;
html_url: string;
draft?: boolean;
user?: { login?: string };
head?: { ref?: string; sha?: string };
base?: { ref?: string; sha?: string };
created_at?: string;
updated_at?: string;
}
function optionValue(args: string[], name: string): string | undefined {
const index = args.indexOf(name);
if (index === -1) return undefined;
const value = args[index + 1];
if (value === undefined || value.length === 0 || value.startsWith("--")) throw new Error(`${name} requires a value`);
return value;
}
function hasFlag(args: string[], name: string): boolean {
return args.includes(name);
}
function positiveIntegerOption(args: string[], name: string, defaultValue: number, maxValue: number): number {
const raw = optionValue(args, name);
if (raw === undefined) return defaultValue;
const value = Number(raw);
if (!Number.isInteger(value) || value <= 0) throw new Error(`${name} must be a positive integer`);
return Math.min(value, maxValue);
}
function parseOptions(args: string[]): GitHubOptions {
return {
repo: optionValue(args, "--repo") ?? DEFAULT_REPO,
dryRun: hasFlag(args, "--dry-run"),
limit: positiveIntegerOption(args, "--limit", 30, 100),
title: optionValue(args, "--title"),
bodyFile: optionValue(args, "--body-file"),
};
}
function parseNumber(raw: string | undefined, label: string): number {
if (raw === undefined) throw new Error(`${label} requires a number`);
const value = Number(raw);
if (!Number.isInteger(value) || value <= 0) throw new Error(`${label} must be a positive integer`);
return value;
}
function readBodyFile(path: string | undefined, command: string): string {
if (path === undefined) throw new Error(`${command} requires --body-file <file>`);
if (!existsSync(path)) throw new Error(`body file not found: ${path}`);
return readFileSync(path, "utf8");
}
function tokenFromEnvironment(): GitHubTokenProbe {
if (process.env.GH_TOKEN && process.env.GH_TOKEN.length > 0) {
return { present: true, source: "GH_TOKEN", ghFallbackAttempted: false };
}
if (process.env.GITHUB_TOKEN && process.env.GITHUB_TOKEN.length > 0) {
return { present: true, source: "GITHUB_TOKEN", ghFallbackAttempted: false };
}
return { present: false, source: null, ghFallbackAttempted: false };
}
function ghBinaryPath(): string | null {
try {
const output = execFileSync("sh", ["-lc", "command -v gh"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
return output.length > 0 ? output : null;
} catch {
return null;
}
}
function ghAuthToken(): string | null {
try {
const output = execFileSync("gh", ["auth", "token"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5000 }).trim();
return output.length > 0 ? output : null;
} catch {
return null;
}
}
function resolveToken(allowGhFallback: boolean): { token: string | null; probe: GitHubTokenProbe } {
const envProbe = tokenFromEnvironment();
if (envProbe.present) {
const token = envProbe.source === "GH_TOKEN" ? process.env.GH_TOKEN ?? null : process.env.GITHUB_TOKEN ?? null;
return { token, probe: envProbe };
}
if (!allowGhFallback) return { token: null, probe: envProbe };
const token = ghAuthToken();
if (token !== null) return { token, probe: { present: true, source: "gh-auth-token", ghFallbackAttempted: true } };
return { token: null, probe: { present: false, source: null, ghFallbackAttempted: true } };
}
function repoParts(repo: string): { owner: string; name: string } {
const [owner, name, extra] = repo.split("/");
if (!owner || !name || extra !== undefined) throw new Error("--repo must be in owner/name format");
return { owner, name };
}
function preview(text: string): string {
return text.length > PREVIEW_CHARS ? `${text.slice(0, PREVIEW_CHARS)}...` : text;
}
function dryRunBody(repo: string, title: string | undefined, body: string): Record<string, unknown> {
return {
repo,
...(title === undefined ? {} : { title }),
bodyChars: body.length,
bodyPreview: preview(body),
bodyPreviewLines: body.split(/\r?\n/).slice(0, 12),
preservesRawNewlines: body.includes("\n"),
containsLiteralBackslashN: body.includes("\\n"),
containsBackticks: body.includes("`"),
containsMarkdownTable: /^\s*\|.+\|\s*$/m.test(body),
};
}
async function parseGitHubResponse(response: Response): Promise<unknown> {
const text = await response.text();
if (text.length === 0) return null;
try {
return JSON.parse(text) as unknown;
} catch {
return { raw: preview(text) };
}
}
function classifyHttpStatus(status: number, message: string, path: string): GitHubDegradedReason {
if (status === 401 || status === 403) return "permission-denied";
if (status === 404) return path.includes("/issues/") || message.toLowerCase().includes("issue") ? "issue-not-found" : "repo-not-found";
return "invalid-response";
}
async function githubRequest<T>(
token: string,
method: string,
path: string,
body?: Record<string, unknown>,
): Promise<T | GitHubErrorPayload> {
let response: Response;
try {
response = await fetch(`${GITHUB_API}${path}`, {
method,
headers: {
Accept: "application/vnd.github+json",
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
"User-Agent": USER_AGENT,
"X-GitHub-Api-Version": "2022-11-28",
},
body: body === undefined ? undefined : JSON.stringify(body),
});
} catch (error) {
return {
ok: false,
degradedReason: "egress-failed",
message: error instanceof Error ? error.message : String(error),
};
}
const parsed = await parseGitHubResponse(response);
if (!response.ok) {
const message = typeof parsed === "object" && parsed !== null && "message" in parsed ? String((parsed as { message?: unknown }).message) : response.statusText;
return {
ok: false,
degradedReason: classifyHttpStatus(response.status, message, path),
status: response.status,
message,
details: parsed,
};
}
return parsed as T;
}
function authRequired(repo: string, command: string, tokenProbe: GitHubTokenProbe): GitHubCommandResult | null {
if (!tokenProbe.present) {
return {
ok: false,
command,
repo,
degradedReason: "missing-token",
token: tokenProbe,
};
}
return null;
}
function isGitHubError(value: unknown): value is GitHubErrorPayload {
return typeof value === "object" && value !== null && (value as { ok?: unknown }).ok === false && "degradedReason" in value;
}
function issueSummary(issue: GitHubIssue): Record<string, unknown> {
return {
id: issue.id,
number: issue.number,
title: issue.title,
body: issue.body ?? "",
state: issue.state,
url: issue.html_url,
author: issue.user?.login ?? null,
createdAt: issue.created_at ?? null,
updatedAt: issue.updated_at ?? null,
};
}
function commentSummary(comment: GitHubComment): Record<string, unknown> {
return {
id: comment.id,
body: comment.body ?? "",
url: comment.html_url,
author: comment.user?.login ?? null,
createdAt: comment.created_at ?? null,
updatedAt: comment.updated_at ?? null,
};
}
function prSummary(pr: GitHubPullRequest): Record<string, unknown> {
return {
id: pr.id,
number: pr.number,
title: pr.title,
body: pr.body ?? "",
state: pr.state,
draft: pr.draft ?? false,
url: pr.html_url,
author: pr.user?.login ?? null,
head: { ref: pr.head?.ref ?? null, sha: pr.head?.sha ?? null },
base: { ref: pr.base?.ref ?? null, sha: pr.base?.sha ?? null },
createdAt: pr.created_at ?? null,
updatedAt: pr.updated_at ?? null,
};
}
async function listIssueComments(token: string, repo: string, issueNumber: number): Promise<GitHubComment[] | GitHubErrorPayload> {
const { owner, name } = repoParts(repo);
return githubRequest<GitHubComment[]>(token, "GET", `/repos/${owner}/${name}/issues/${issueNumber}/comments?per_page=100`);
}
async function issueView(repo: string, token: string, issueNumber: number): Promise<GitHubCommandResult> {
const { owner, name } = repoParts(repo);
const issue = await githubRequest<GitHubIssue>(token, "GET", `/repos/${owner}/${name}/issues/${issueNumber}`);
if (isGitHubError(issue)) return { ok: false, command: "issue view", repo, degradedReason: issue.degradedReason, details: issue };
const comments = await listIssueComments(token, repo, issueNumber);
if (isGitHubError(comments)) return { ok: false, command: "issue view", repo, degradedReason: comments.degradedReason, issue: issueSummary(issue), details: comments };
return {
ok: true,
command: "issue view",
repo,
issue: issueSummary(issue),
comments: comments.map(commentSummary),
};
}
async function issueCreate(repo: string, token: string, options: GitHubOptions): Promise<GitHubCommandResult> {
if (options.title === undefined) throw new Error("issue create requires --title <title>");
const body = readBodyFile(options.bodyFile, "issue create");
if (options.dryRun) return { ok: true, command: "issue create", repo, dryRun: true, ...dryRunBody(repo, options.title, body) };
const { owner, name } = repoParts(repo);
const issue = await githubRequest<GitHubIssue>(token, "POST", `/repos/${owner}/${name}/issues`, { title: options.title, body });
if (isGitHubError(issue)) return { ok: false, command: "issue create", repo, degradedReason: issue.degradedReason, details: issue };
return { ok: true, command: "issue create", repo, issue: issueSummary(issue), rest: true };
}
async function issueEdit(repo: string, token: string, issueNumber: number, options: GitHubOptions): Promise<GitHubCommandResult> {
const body = readBodyFile(options.bodyFile, "issue edit");
if (options.dryRun) {
return {
ok: true,
command: "issue edit",
repo,
dryRun: true,
issueNumber,
...dryRunBody(repo, options.title, body),
wouldPatch: { title: options.title ?? null, bodyFromFile: options.bodyFile },
};
}
const { owner, name } = repoParts(repo);
const payload: Record<string, unknown> = { body };
if (options.title !== undefined) payload.title = options.title;
const issue = await githubRequest<GitHubIssue>(token, "PATCH", `/repos/${owner}/${name}/issues/${issueNumber}`, payload);
if (isGitHubError(issue)) return { ok: false, command: "issue edit", repo, degradedReason: issue.degradedReason, details: issue };
return { ok: true, command: "issue edit", repo, issue: issueSummary(issue), rest: true };
}
async function issueComment(repo: string, token: string, issueNumber: number, options: GitHubOptions): Promise<GitHubCommandResult> {
const body = readBodyFile(options.bodyFile, "issue comment");
if (options.dryRun) return { ok: true, command: "issue comment", repo, dryRun: true, issueNumber, ...dryRunBody(repo, undefined, body) };
const { owner, name } = repoParts(repo);
const comment = await githubRequest<GitHubComment>(token, "POST", `/repos/${owner}/${name}/issues/${issueNumber}/comments`, { body });
if (isGitHubError(comment)) return { ok: false, command: "issue comment", repo, degradedReason: comment.degradedReason, details: comment };
return { ok: true, command: "issue comment", repo, comment: commentSummary(comment), rest: true };
}
async function issueState(repo: string, token: string, issueNumber: number, state: "open" | "closed", dryRun: boolean): Promise<GitHubCommandResult> {
if (dryRun) return { ok: true, command: state === "closed" ? "issue close" : "issue reopen", dryRun: true, repo, issueNumber, wouldPatch: { state } };
const { owner, name } = repoParts(repo);
const issue = await githubRequest<GitHubIssue>(token, "PATCH", `/repos/${owner}/${name}/issues/${issueNumber}`, { state });
if (isGitHubError(issue)) return { ok: false, command: state === "closed" ? "issue close" : "issue reopen", repo, degradedReason: issue.degradedReason, details: issue };
return { ok: true, command: state === "closed" ? "issue close" : "issue reopen", repo, issue: issueSummary(issue), rest: true };
}
function escapeSnippet(text: string, index: number): string {
const start = Math.max(0, index - 80);
const end = Math.min(text.length, index + 120);
return text.slice(start, end).replace(/\n/g, "\\n");
}
function scanText(text: string, patterns: Array<{ kind: string; pattern: RegExp }>): Array<{ kind: string; snippet: string }> {
const findings: Array<{ kind: string; snippet: string }> = [];
for (const item of patterns) {
item.pattern.lastIndex = 0;
let match = item.pattern.exec(text);
while (match !== null) {
findings.push({ kind: item.kind, snippet: escapeSnippet(text, match.index) });
if (findings.length >= 5) return findings;
match = item.pattern.exec(text);
}
}
return findings;
}
async function issueScanEscape(repo: string, token: string, limit: number): Promise<GitHubCommandResult> {
const { owner, name } = repoParts(repo);
const issues = await githubRequest<GitHubIssue[]>(token, "GET", `/repos/${owner}/${name}/issues?state=all&per_page=${limit}`);
if (isGitHubError(issues)) return { ok: false, command: "issue scan-escape", repo, degradedReason: issues.degradedReason, details: issues };
const patterns = [
{ kind: "literal-backslash-n", pattern: /\\n/g },
{ kind: "literal-backslash-t", pattern: /\\t/g },
{ kind: "shell-escaped-newline", pattern: /\\r\\n|\\012|\\x0a/g },
{ kind: "ansi-escape-literal", pattern: /\\u001b|\\033/g },
];
const findings: Array<Record<string, unknown>> = [];
for (const issue of issues) {
for (const finding of scanText(issue.body ?? "", patterns)) {
findings.push({ type: "issue", issueNumber: issue.number, id: issue.id, url: issue.html_url, ...finding });
}
const comments = await listIssueComments(token, repo, issue.number);
if (isGitHubError(comments)) {
findings.push({ type: "comment-scan-error", issueNumber: issue.number, url: issue.html_url, degradedReason: comments.degradedReason, details: comments });
continue;
}
for (const comment of comments) {
for (const finding of scanText(comment.body ?? "", patterns)) {
findings.push({ type: "comment", issueNumber: issue.number, id: comment.id, url: comment.html_url, ...finding });
}
}
}
return {
ok: true,
command: "issue scan-escape",
repo,
scannedIssues: issues.length,
findings,
note: "Read-only scan; no issue or comment content was modified.",
};
}
async function authStatus(repo: string): Promise<GitHubCommandResult> {
const ghPath = ghBinaryPath();
const { token, probe } = resolveToken(ghPath !== null);
const degraded: GitHubDegradedReason[] = [];
if (ghPath === null) degraded.push("missing-binary");
if (!probe.present || token === null) {
degraded.push("missing-token");
return {
ok: false,
command: "auth status",
repo,
degradedReason: "missing-token",
degraded,
gh: { binaryFound: ghPath !== null, path: ghPath },
token: probe,
probes: { restApi: "skipped", repo: "skipped", issueRead: "skipped" },
};
}
const { owner, name } = repoParts(repo);
const api = await githubRequest<Record<string, unknown>>(token, "GET", "/rate_limit");
if (isGitHubError(api)) {
return {
ok: false,
command: "auth status",
repo,
degradedReason: api.degradedReason,
degraded: [...degraded, api.degradedReason],
gh: { binaryFound: ghPath !== null, path: ghPath },
token: probe,
probes: { restApi: api, repo: "skipped", issueRead: "skipped" },
};
}
const repoProbe = await githubRequest<{ full_name?: string; private?: boolean }>(token, "GET", `/repos/${owner}/${name}`);
if (isGitHubError(repoProbe)) {
return {
ok: false,
command: "auth status",
repo,
degradedReason: repoProbe.degradedReason,
degraded: [...degraded, repoProbe.degradedReason],
gh: { binaryFound: ghPath !== null, path: ghPath },
token: probe,
probes: { restApi: "ok", repo: repoProbe, issueRead: "skipped" },
};
}
const issueProbe = await githubRequest<GitHubIssue[]>(token, "GET", `/repos/${owner}/${name}/issues?per_page=1&state=all`);
if (isGitHubError(issueProbe)) {
return {
ok: false,
command: "auth status",
repo,
degradedReason: issueProbe.degradedReason,
degraded: [...degraded, issueProbe.degradedReason],
gh: { binaryFound: ghPath !== null, path: ghPath },
token: probe,
probes: { restApi: "ok", repo: { ok: true, fullName: repoProbe.full_name ?? repo, private: repoProbe.private ?? null }, issueRead: issueProbe },
};
}
return {
ok: true,
command: "auth status",
repo,
degraded,
gh: { binaryFound: ghPath !== null, path: ghPath },
token: probe,
probes: {
restApi: "ok",
repo: { ok: true, fullName: repoProbe.full_name ?? repo, private: repoProbe.private ?? null },
issueRead: { ok: true, readable: true, sampleCount: issueProbe.length },
},
restFallback: true,
};
}
async function prList(repo: string, token: string, limit: number): Promise<GitHubCommandResult> {
const { owner, name } = repoParts(repo);
const prs = await githubRequest<GitHubPullRequest[]>(token, "GET", `/repos/${owner}/${name}/pulls?state=all&per_page=${limit}`);
if (isGitHubError(prs)) return { ok: false, command: "pr list", repo, degradedReason: prs.degradedReason, details: prs };
return {
ok: true,
command: "pr list",
repo,
plannedScope: "first-stage read-only REST support",
pullRequests: prs.map(prSummary),
};
}
async function prView(repo: string, token: string, number: number): Promise<GitHubCommandResult> {
const { owner, name } = repoParts(repo);
const pr = await githubRequest<GitHubPullRequest>(token, "GET", `/repos/${owner}/${name}/pulls/${number}`);
if (isGitHubError(pr)) return { ok: false, command: "pr view", repo, degradedReason: pr.degradedReason, details: pr };
return {
ok: true,
command: "pr view",
repo,
plannedScope: "first-stage read-only REST support",
pullRequest: prSummary(pr),
};
}
export function ghHelp(): unknown {
return {
command: "gh",
output: "json",
usage: [
"bun scripts/cli.ts gh auth status [--repo owner/name]",
"bun scripts/cli.ts gh issue view <number> [--repo owner/name]",
"bun scripts/cli.ts gh issue create --title <title> --body-file <file> [--repo owner/name] [--dry-run]",
"bun scripts/cli.ts gh issue edit <number> --body-file <file> [--title title] [--repo owner/name] [--dry-run]",
"bun scripts/cli.ts gh issue comment <number> --body-file <file> [--repo owner/name] [--dry-run]",
"bun scripts/cli.ts gh issue close|reopen <number> [--repo owner/name] [--dry-run]",
"bun scripts/cli.ts gh issue scan-escape [--repo owner/name] [--limit N]",
"bun scripts/cli.ts gh pr list [--repo owner/name] [--limit N]",
"bun scripts/cli.ts gh pr view <number> [--repo owner/name]",
],
defaults: { repo: DEFAULT_REPO },
notes: [
"Issue create/edit/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.",
"--body-file is required for mutating Markdown bodies so real newlines, backticks, and tables are read as file bytes instead of shell arguments.",
"Issue body stdin is intentionally unsupported in the first phase; write generated Markdown to a file and pass --body-file.",
"PR support is first-stage read-only list/view; create/edit/merge are planned and intentionally unsupported.",
],
};
}
export async function runGhCommand(args: string[]): Promise<GitHubCommandResult | unknown> {
const [top, sub, third] = args;
if (top === undefined || top === "help" || top === "--help" || top === "-h") return ghHelp();
const options = parseOptions(args);
if (top === "auth" && sub === "status") return authStatus(options.repo);
if (top === "issue") {
if (options.dryRun) {
if (sub === "create") return issueCreate(options.repo, "", options);
if (sub === "edit") return issueEdit(options.repo, "", parseNumber(third, "issue edit"), options);
if (sub === "comment") return issueComment(options.repo, "", parseNumber(third, "issue comment"), options);
if (sub === "close") return issueState(options.repo, "", parseNumber(third, "issue close"), "closed", true);
if (sub === "reopen") return issueState(options.repo, "", parseNumber(third, "issue reopen"), "open", true);
}
const { token, probe } = resolveToken(true);
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 === "view") return issueView(options.repo, token, parseNumber(third, "issue view"));
if (sub === "create") return issueCreate(options.repo, token, options);
if (sub === "edit") return issueEdit(options.repo, token, parseNumber(third, "issue edit"), options);
if (sub === "comment") return issueComment(options.repo, token, parseNumber(third, "issue comment"), options);
if (sub === "close") return issueState(options.repo, token, parseNumber(third, "issue close"), "closed", options.dryRun);
if (sub === "reopen") return issueState(options.repo, token, parseNumber(third, "issue reopen"), "open", options.dryRun);
if (sub === "scan-escape") return issueScanEscape(options.repo, token, options.limit);
}
if (top === "pr") {
if (sub !== "list" && sub !== "view") {
return {
ok: false,
command: `pr ${sub ?? ""}`.trim(),
repo: options.repo,
degradedReason: "unsupported-command",
planned: true,
message: "PR create/edit/comment/merge are planned for a later phase; first-stage support is read-only pr list/view.",
};
}
const { token, probe } = resolveToken(true);
const missing = authRequired(options.repo, `pr ${sub}`, probe);
if (missing !== null || token === null) return missing ?? authRequired(options.repo, `pr ${sub}`, { present: false, source: null, ghFallbackAttempted: true });
if (sub === "list") return prList(options.repo, token, options.limit);
return prView(options.repo, token, parseNumber(third, "pr view"));
}
return {
ok: false,
command: args.join(" ") || "gh",
repo: options.repo,
degradedReason: "unsupported-command",
help: ghHelp(),
};
}
+4
View File
@@ -1,3 +1,5 @@
import { ghHelp } from "./gh";
export function rootHelp(): unknown {
return {
entry: "bun scripts/cli.ts",
@@ -41,6 +43,7 @@ export function rootHelp(): unknown {
{ command: "deploy check|plan|apply [--file deploy.json|--env dev|prod] [--service id] [--commit full-sha] [--dry-run] [--force]", description: "Reconcile services from a repo+commit manifest; --env reads origin/master:deploy.json environments and applies supported dev target-side rollouts or reviewed D601 registry artifact consumers. code-queue artifact consumption is dev-only." },
{ command: "dev-env validate|prewarm-images", description: "Validate D601 unidesk-dev guardrails or prewarm dev foundation images into native k3s containerd through a bounded async job." },
{ command: "artifact-registry plan|render|status|health|install|deploy-backend-core|deploy-service", description: "Manage the D601 host-managed CNCF Distribution registry and run pull-only artifact CD for supported services, including D601 direct, k3s-managed, and code-queue dev-only consumers." },
{ command: "gh auth|issue|pr", description: "Run safe GitHub issue operations through REST with body-file support, token diagnostics, escape scanning, and first-stage read-only PR list/view." },
{ command: "code-agent-sandbox", description: "Independent Code Agent Sandbox service skeleton for adapter, mode, and credential-boundary diagnostics." },
{ command: "schedule list|get|runs|run|delete", description: "Manage backend-core scheduled tasks and run history; schedule run <id> supports --wait-ms N." },
{ command: "schedule upsert-pgdata-backup [--time HH:MM] [--remote-base /SERVER_DATA/UNIDESK_PG_DATA]", description: "Create or update the daily PGDATA physical backup task that uploads monthly rotated archives to Baidu Netdisk." },
@@ -332,5 +335,6 @@ export function staticNamespaceHelp(args: string[]): unknown | null {
if (top === "e2e") return e2eHelp();
if (top === "dev-env") return devEnvHelp();
if (top === "artifact-registry") return artifactRegistryHelp();
if (top === "gh") return ghHelp();
return null;
}