Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -11,4 +11,6 @@ For a Windows workspace, run tools that belong to the Windows installation or PA
|
||||
|
||||
`download` returns verified bytes/SHA-256 evidence. On Windows routes, drive-letter paths are automatically mapped through the same provider's WSL `/mnt/<drive>` host route for binary streaming; for example `trans D518:win download 'D:\tmp\image.png' /tmp/image.png` does not require manually rewriting the source path to `/mnt/d/tmp/image.png`. Relative Windows downloads use the `win/<drive>/<path>` route workspace, such as `trans D518:win/d/tmp download image.png /tmp/image.png`. UNC paths still need to be copied to a drive-letter path first.
|
||||
|
||||
`trans --help download` 返回紧凑的 download 合同、可执行示例和当前 `runtime.repoRoot` / `runtime.cliPath`。PATH wrapper 不根据 cwd 选择源码根;验证独立 worktree 时显式设置 `UNIDESK_TRANS_REPO_ROOT=<worktree>`,并先读取帮助中的实际 root,避免把主 worktree 的旧实现误认为当前 cwd 的实现。
|
||||
|
||||
Ordinary trans/SSH work must fit the short connection budget. Long build, CI/CD, trace, probe or rollout work must use submit-and-poll through a controlled job/status surface.
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# 收敛 trans download scoped help
|
||||
|
||||
- 来源:<https://github.com/pikasTech/unidesk/issues/1744>
|
||||
- 范围:仅收敛 `trans --help download` 的帮助路由、紧凑合同、定向测试和 PR 验收。
|
||||
- 禁止项:不修改文件传输实现,不提高 stdout 门限,不部署运行面。
|
||||
|
||||
|
||||
## R1 [completed]
|
||||
|
||||
闭环修复 [UniDesk #1744](https://github.com/pikasTech/unidesk/issues/1744) 的 trans download scoped help 可见性,完成任务后将详细报告写入[任务报告](./details/trans-download-scoped-help/R1_Task_Report.md)。
|
||||
### R1.1 [completed]
|
||||
|
||||
复现并确认 [#1744](https://github.com/pikasTech/unidesk/issues/1744) 的帮助路由、输出爆炸与实际 entrypoint/root 选择合同,完成任务后将详细报告写入[任务报告](./details/trans-download-scoped-help/R1.1_Task_Report.md)。
|
||||
### R1.2 [completed]
|
||||
|
||||
实现 [#1744](https://github.com/pikasTech/unidesk/issues/1744) 的紧凑 download scoped help、可执行示例和当前 repo root 披露,完成任务后将详细报告写入[任务报告](./details/trans-download-scoped-help/R1.2_Task_Report.md)。
|
||||
### R1.3 [completed]
|
||||
|
||||
完成 [#1744](https://github.com/pikasTech/unidesk/issues/1744) 的定向测试、原命令验收、提交 PR 与 preflight,完成任务后将详细报告写入[任务报告](./details/trans-download-scoped-help/R1.3_Task_Report.md)。
|
||||
+2
-2
@@ -27,7 +27,7 @@ import { withGhDefaultRendered } from "./src/gh/default-render";
|
||||
import { isGhContentRoute, runGhContentRoute } from "./src/gh-route";
|
||||
import { runGitToolsCommand } from "./src/git-tools";
|
||||
import { runCommanderCommand } from "./src/commander";
|
||||
import { isHelpToken, rootHelp, serverHelp, sshHelp, staticNamespaceHelp } from "./src/help";
|
||||
import { isHelpToken, rootHelp, serverHelp, sshHelp, sshHelpScope, staticNamespaceHelp } from "./src/help";
|
||||
import { healthHelp, runHealthCommand } from "./src/health";
|
||||
import { runServerCleanupCommand } from "./src/server-cleanup";
|
||||
import { runGcCommand } from "./src/gc";
|
||||
@@ -236,7 +236,7 @@ async function main(): Promise<void> {
|
||||
}
|
||||
|
||||
if (top === "ssh" && (sub === undefined || isHelpToken(sub) || (isHelpToken(third) && args.length === 3))) {
|
||||
emitJson(commandName, sshHelp());
|
||||
emitJson(commandName, sshHelp(sshHelpScope(args)));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+69
-1
@@ -1,11 +1,13 @@
|
||||
// SPEC: PJ2026-01060509 出站诊断 draft-2026-06-26-p8-egress-job-friction.
|
||||
// SPEC: PJ2026-01060703 CI/CD branch follower draft-2026-07-03-p0-branch-follower.
|
||||
// Static CLI help for job aliases and server lifecycle progressive disclosure.
|
||||
import { join } from "node:path";
|
||||
import { ghHelp, ghScopedHelp } from "./gh";
|
||||
import { authBrokerHelp } from "./auth-broker";
|
||||
import { platformDbHelp } from "./platform-db";
|
||||
import { secretsHelp } from "./secrets";
|
||||
import { healthHelp } from "./health";
|
||||
import { repoRoot } from "./config";
|
||||
|
||||
export function rootHelp(): unknown {
|
||||
return {
|
||||
@@ -167,7 +169,16 @@ export function serverHelp(action: string | undefined = undefined): unknown {
|
||||
};
|
||||
}
|
||||
|
||||
export function sshHelp(): unknown {
|
||||
export type SshHelpScope = "download";
|
||||
|
||||
export function sshHelpScope(args: string[]): SshHelpScope | undefined {
|
||||
const [top, helpToken, scope] = args;
|
||||
if (top !== "ssh" || args.length !== 3 || !isHelpToken(helpToken)) return undefined;
|
||||
return scope === "download" ? scope : undefined;
|
||||
}
|
||||
|
||||
export function sshHelp(scope: SshHelpScope | undefined = undefined): unknown {
|
||||
if (scope === "download") return sshDownloadHelp();
|
||||
return {
|
||||
command: "ssh",
|
||||
output: "json",
|
||||
@@ -255,6 +266,63 @@ export function sshHelp(): unknown {
|
||||
};
|
||||
}
|
||||
|
||||
function sshDownloadHelp(): unknown {
|
||||
const configuredRepoRoot = process.env.UNIDESK_TRANS_REPO_ROOT?.trim() || null;
|
||||
const rawEntrypoint = process.env.UNIDESK_SSH_ENTRYPOINT?.trim();
|
||||
const entrypoint = rawEntrypoint === "trans" || rawEntrypoint === "tran" || rawEntrypoint === "ssh"
|
||||
? rawEntrypoint
|
||||
: "ssh";
|
||||
return {
|
||||
command: `${entrypoint} download`,
|
||||
output: "json",
|
||||
scope: "download",
|
||||
description: "Download one remote file through the selected route and verify remote/local bytes plus SHA-256 automatically.",
|
||||
runtime: {
|
||||
entrypoint,
|
||||
repoRoot,
|
||||
cliPath: join(repoRoot, "scripts", "ssh-cli.ts"),
|
||||
cwdSelectsRepoRoot: false,
|
||||
rootSelection: {
|
||||
environmentVariable: "UNIDESK_TRANS_REPO_ROOT",
|
||||
configuredValue: configuredRepoRoot,
|
||||
explicitExample: `UNIDESK_TRANS_REPO_ROOT=${repoRoot} trans --help download`,
|
||||
note: "The PATH wrapper selects its source root independently of cwd; set UNIDESK_TRANS_REPO_ROOT explicitly when validating a worktree implementation.",
|
||||
},
|
||||
},
|
||||
usage: [
|
||||
"trans <route> download <remote-file> <local-file>",
|
||||
"trans <route> download --inactivity-timeout-ms <milliseconds> <remote-file> <local-file>",
|
||||
"trans --help download",
|
||||
],
|
||||
options: {
|
||||
"--inactivity-timeout-ms <milliseconds>": "Allow a controlled progress-emitting download to stay open while data continues flowing; ordinary trans operations retain the short runtime budget.",
|
||||
},
|
||||
contract: {
|
||||
pathOrder: ["remote-file", "local-file"],
|
||||
transport: "host.ssh.tcp-pool",
|
||||
strategy: "tcp-pool-stdout-stream",
|
||||
progress: "unidesk.ssh.download.progress JSON is emitted on stderr with bytes, remainingBytes and throughputBytesPerSecond.",
|
||||
verification: {
|
||||
automatic: true,
|
||||
fields: ["bytes", "sha256"],
|
||||
success: "verified=true and verification.match.{bytes,sha256}=true",
|
||||
partialFilePolicy: "The local target is removed when streaming or verification fails.",
|
||||
},
|
||||
},
|
||||
examples: {
|
||||
host: "trans D601:/tmp download /tmp/trace.json ./trace.json",
|
||||
k3sWorkload: "trans NC01:k3s:hwlab-v03:hwlab-cloud-api:hwlab-cloud-api download /tmp/trace.json ./trace.json",
|
||||
windowsDrive: "trans D601:win download 'D:\\tmp\\trace.json' ./trace.json",
|
||||
explicitWorktree: `UNIDESK_TRANS_REPO_ROOT=${repoRoot} trans NC01:k3s:hwlab-v03:hwlab-cloud-api:hwlab-cloud-api download /tmp/trace.json ./trace.json`,
|
||||
},
|
||||
notes: [
|
||||
"A k3s download requires a workload route; the control-plane-only <provider>:k3s route cannot name a remote file inside a workload.",
|
||||
"Windows drive-letter paths are mapped through the same provider's WSL /mnt/<drive> host route before verified stdout streaming.",
|
||||
"The final JSON result is the integrity proof; a separate manual sha256sum comparison is not required.",
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function configHelp(): unknown {
|
||||
return {
|
||||
command: "config show",
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { repoRoot } from "./config";
|
||||
import { sshHelp, sshHelpScope } from "./help";
|
||||
|
||||
describe("ssh download scoped help", () => {
|
||||
test("selects only the exact help-first download scope", () => {
|
||||
expect(sshHelpScope(["ssh", "--help", "download"])).toBe("download");
|
||||
expect(sshHelpScope(["ssh", "help", "download"])).toBe("download");
|
||||
expect(sshHelpScope(["ssh", "--help"])).toBeUndefined();
|
||||
expect(sshHelpScope(["ssh", "NC01:k3s", "--help"])).toBeUndefined();
|
||||
expect(sshHelpScope(["ssh", "--help", "download", "extra"])).toBeUndefined();
|
||||
});
|
||||
|
||||
test("keeps download help bounded and discloses the executing source root", () => {
|
||||
const scoped = sshHelp("download") as {
|
||||
scope: string;
|
||||
runtime: { repoRoot: string; cliPath: string; cwdSelectsRepoRoot: boolean; rootSelection: { environmentVariable: string } };
|
||||
usage: string[];
|
||||
examples: Record<string, string>;
|
||||
contract: { pathOrder: string[]; transport: string; verification: { automatic: boolean } };
|
||||
};
|
||||
const full = sshHelp() as { usage: string[] };
|
||||
const serialized = JSON.stringify(scoped);
|
||||
|
||||
expect(scoped.scope).toBe("download");
|
||||
expect(scoped.usage.length).toBeLessThanOrEqual(4);
|
||||
expect(scoped.usage.length).toBeLessThan(full.usage.length);
|
||||
expect(Buffer.byteLength(serialized, "utf8")).toBeLessThan(10_240);
|
||||
expect(serialized).not.toContain("git exact-commit");
|
||||
expect(scoped.runtime.repoRoot).toBe(repoRoot);
|
||||
expect(scoped.runtime.cliPath).toBe(`${repoRoot}/scripts/ssh-cli.ts`);
|
||||
expect(scoped.runtime.cwdSelectsRepoRoot).toBe(false);
|
||||
expect(scoped.runtime.rootSelection.environmentVariable).toBe("UNIDESK_TRANS_REPO_ROOT");
|
||||
expect(scoped.contract.pathOrder).toEqual(["remote-file", "local-file"]);
|
||||
expect(scoped.contract.transport).toBe("host.ssh.tcp-pool");
|
||||
expect(scoped.contract.verification.automatic).toBe(true);
|
||||
expect(scoped.examples.host).toContain("download /tmp/trace.json ./trace.json");
|
||||
expect(scoped.examples.k3sWorkload).toBe(
|
||||
"trans NC01:k3s:hwlab-v03:hwlab-cloud-api:hwlab-cloud-api download /tmp/trace.json ./trace.json",
|
||||
);
|
||||
expect(scoped.examples.windowsDrive).toContain("D:\\tmp\\trace.json");
|
||||
expect(scoped.examples.explicitWorktree).toBe(
|
||||
`UNIDESK_TRANS_REPO_ROOT=${repoRoot} trans NC01:k3s:hwlab-v03:hwlab-cloud-api:hwlab-cloud-api download /tmp/trace.json ./trace.json`,
|
||||
);
|
||||
});
|
||||
|
||||
test("renders the original trans command without stdout dump fallback", () => {
|
||||
const result = spawnSync("bun", ["scripts/ssh-cli.ts", "--help", "download"], {
|
||||
cwd: repoRoot,
|
||||
env: {
|
||||
...process.env,
|
||||
UNIDESK_SSH_ENTRYPOINT: "trans",
|
||||
UNIDESK_TRANS_REPO_ROOT: repoRoot,
|
||||
},
|
||||
encoding: "utf8",
|
||||
});
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stderr).toBe("");
|
||||
expect(Buffer.byteLength(result.stdout, "utf8")).toBeLessThan(10_240);
|
||||
const rendered = JSON.parse(result.stdout) as {
|
||||
ok: boolean;
|
||||
command: string;
|
||||
data: { scope: string; runtime: { repoRoot: string }; usage: string[]; outputTruncated?: boolean };
|
||||
};
|
||||
expect(rendered.ok).toBe(true);
|
||||
expect(rendered.command).toBe("trans --help download");
|
||||
expect(rendered.data.scope).toBe("download");
|
||||
expect(rendered.data.runtime.repoRoot).toBe(repoRoot);
|
||||
expect(rendered.data.usage.length).toBeLessThanOrEqual(4);
|
||||
expect(rendered.data.outputTruncated).toBeUndefined();
|
||||
|
||||
const unified = spawnSync("bun", ["scripts/cli.ts", "ssh", "--help", "download"], {
|
||||
cwd: repoRoot,
|
||||
env: process.env,
|
||||
encoding: "utf8",
|
||||
});
|
||||
expect(unified.status).toBe(0);
|
||||
expect(unified.stderr).toBe("");
|
||||
const unifiedRendered = JSON.parse(unified.stdout) as { data: { scope: string; runtime: { repoRoot: string } } };
|
||||
expect(unifiedRendered.data.scope).toBe("download");
|
||||
expect(unifiedRendered.data.runtime.repoRoot).toBe(repoRoot);
|
||||
});
|
||||
});
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { readConfig } from "./src/config";
|
||||
import { isHelpToken, sshHelp } from "./src/help";
|
||||
import { isHelpToken, sshHelp, sshHelpScope } from "./src/help";
|
||||
import { emitError, emitJson } from "./src/output";
|
||||
import { extractRemoteCliOptions } from "./src/remote-options";
|
||||
import { runRemoteSshCli } from "./src/remote-ssh";
|
||||
@@ -30,7 +30,7 @@ async function main(): Promise<void> {
|
||||
if (top !== "ssh") throw new Error(`ssh-cli supports only the ssh command, got: ${top || "<empty>"}`);
|
||||
|
||||
if (sub === undefined || isHelpToken(sub) || (isHelpToken(third) && args.length === 3)) {
|
||||
emitJson(commandName, sshHelp());
|
||||
emitJson(commandName, sshHelp(sshHelpScope(args)));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user