fix: require explicit trans shell operations
This commit is contained in:
+21
-21
@@ -2045,7 +2045,7 @@ async function controlPlaneApply(config: UniDeskConfig, options: LaneConfirmOpti
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
const applied = await capture(config, spec.nodeKubeRoute, ["script", "--", applyYamlScript(manifestYaml, "unidesk-agentrun-control-plane", false)]);
|
||||
const applied = await capture(config, spec.nodeKubeRoute, ["sh", "--", applyYamlScript(manifestYaml, "unidesk-agentrun-control-plane", false)]);
|
||||
const payload = captureJsonPayload(applied);
|
||||
return {
|
||||
ok: applied.exitCode === 0 && payload.ok !== false,
|
||||
@@ -2071,15 +2071,15 @@ async function status(config: UniDeskConfig, options: StatusOptions): Promise<Re
|
||||
|
||||
async function statusYamlLane(config: UniDeskConfig, options: StatusOptions, target: { configPath: string; spec: AgentRunLaneSpec }): Promise<Record<string, unknown>> {
|
||||
const spec = target.spec;
|
||||
const sourceProbe = await timedStatusStage("source", () => capture(config, `${spec.nodeRoute}:${spec.source.workspace}`, ["script", "--", yamlLaneSourceStatusScript(spec)]));
|
||||
const sourceProbe = await timedStatusStage("source", () => capture(config, `${spec.nodeRoute}:${spec.source.workspace}`, ["sh", "--", yamlLaneSourceStatusScript(spec)]));
|
||||
const sourcePayload = captureJsonPayload(sourceProbe.value);
|
||||
const sourceCommit = options.sourceCommit
|
||||
?? stringOrNull(sourcePayload.remoteBranchCommit)
|
||||
?? stringOrNull(sourcePayload.localHead);
|
||||
const pipelineRunName = options.pipelineRun ?? (sourceCommit ? agentRunPipelineRunName(spec, sourceCommit) : null);
|
||||
const [runtimeProbe, mirrorProbe] = await Promise.all([
|
||||
timedStatusStage("runtime", () => capture(config, spec.nodeKubeRoute, ["script", "--", yamlLaneRuntimeStatusScript(spec, pipelineRunName)])),
|
||||
timedStatusStage("git-mirror", () => capture(config, spec.nodeKubeRoute, ["script", "--", yamlLaneGitMirrorStatusScript(spec)])),
|
||||
timedStatusStage("runtime", () => capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneRuntimeStatusScript(spec, pipelineRunName)])),
|
||||
timedStatusStage("git-mirror", () => capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneGitMirrorStatusScript(spec)])),
|
||||
]);
|
||||
const runtimePayload = captureJsonPayload(runtimeProbe.value);
|
||||
const mirrorPayload = captureJsonPayload(mirrorProbe.value);
|
||||
@@ -2218,7 +2218,7 @@ async function restartYamlLane(config: UniDeskConfig, options: LaneConfirmOption
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
const result = await capture(config, spec.nodeKubeRoute, ["script", "--", restartYamlLaneScript(spec)]);
|
||||
const result = await capture(config, spec.nodeKubeRoute, ["sh", "--", restartYamlLaneScript(spec)]);
|
||||
const payload = captureJsonPayload(result);
|
||||
return {
|
||||
ok: result.exitCode === 0 && payload.ok !== false,
|
||||
@@ -2281,7 +2281,7 @@ async function secretSync(config: UniDeskConfig, options: SecretSyncOptions): Pr
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
const result = await capture(config, spec.nodeKubeRoute, ["script", "--", secretSyncScript(spec, values.map(({ spec: item, value }) => ({ targetRef: item.targetRef, value: value.value })))]);
|
||||
const result = await capture(config, spec.nodeKubeRoute, ["sh", "--", secretSyncScript(spec, values.map(({ spec: item, value }) => ({ targetRef: item.targetRef, value: value.value })))]);
|
||||
const payload = captureJsonPayload(result);
|
||||
return {
|
||||
ok: result.exitCode === 0 && payload.ok !== false,
|
||||
@@ -2470,7 +2470,7 @@ async function triggerCurrent(config: UniDeskConfig, options: TriggerOptions): P
|
||||
|
||||
async function triggerCurrentYamlLane(config: UniDeskConfig, options: TriggerOptions, target: { configPath: string; spec: AgentRunLaneSpec }): Promise<Record<string, unknown>> {
|
||||
const spec = target.spec;
|
||||
const probe = await capture(config, spec.nodeRoute, ["script", "--", yamlLaneSourceBootstrapProbeScript(spec)]);
|
||||
const probe = await capture(config, spec.nodeRoute, ["sh", "--", yamlLaneSourceBootstrapProbeScript(spec)]);
|
||||
const source = captureJsonPayload(probe);
|
||||
const sourceCommit = stringOrNull(source.sourceCommit);
|
||||
const remoteBranchExists = source.remoteBranchExists === true;
|
||||
@@ -2544,7 +2544,7 @@ async function triggerCurrentYamlLaneConfirmed(config: UniDeskConfig, spec: Agen
|
||||
lane: spec.lane,
|
||||
status: "submitting",
|
||||
});
|
||||
const bootstrapSubmit = await capture(config, spec.nodeRoute, ["script", "--", yamlLaneSourceBootstrapSubmitScript(spec)]);
|
||||
const bootstrapSubmit = await capture(config, spec.nodeRoute, ["sh", "--", yamlLaneSourceBootstrapSubmitScript(spec)]);
|
||||
const bootstrapSubmitPayload = captureJsonPayload(bootstrapSubmit);
|
||||
if (bootstrapSubmit.exitCode !== 0 || bootstrapSubmitPayload.ok === false) {
|
||||
return {
|
||||
@@ -2577,7 +2577,7 @@ async function triggerCurrentYamlLaneConfirmed(config: UniDeskConfig, spec: Agen
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
const buildSubmit = await capture(config, spec.nodeRoute, ["script", "--", yamlLaneBuildImageSubmitScript(spec, sourceCommit)]);
|
||||
const buildSubmit = await capture(config, spec.nodeRoute, ["sh", "--", yamlLaneBuildImageSubmitScript(spec, sourceCommit)]);
|
||||
const buildSubmitPayload = captureJsonPayload(buildSubmit);
|
||||
if (buildSubmit.exitCode !== 0 || buildSubmitPayload.ok === false) {
|
||||
return {
|
||||
@@ -2618,7 +2618,7 @@ async function triggerCurrentYamlLaneConfirmed(config: UniDeskConfig, spec: Agen
|
||||
}
|
||||
const image = agentRunImageArtifact(spec, { sourceCommit, envIdentity, digest, status: stringOrNull(buildPayload.status) ?? "built" });
|
||||
const renderedFiles = renderAgentRunGitopsFiles(spec, { sourceCommit, image });
|
||||
const gitops = await capture(config, spec.nodeRoute, ["script", "--", yamlLaneGitopsPublishScript(spec, renderedFiles)]);
|
||||
const gitops = await capture(config, spec.nodeRoute, ["sh", "--", yamlLaneGitopsPublishScript(spec, renderedFiles)]);
|
||||
const gitopsPayload = captureJsonPayload(gitops);
|
||||
if (gitops.exitCode !== 0 || gitopsPayload.ok === false) {
|
||||
return {
|
||||
@@ -2656,7 +2656,7 @@ async function triggerCurrentYamlLaneConfirmed(config: UniDeskConfig, spec: Agen
|
||||
};
|
||||
}
|
||||
const pipelineRun = agentRunPipelineRunName(spec, sourceCommit);
|
||||
const created = await capture(config, spec.nodeKubeRoute, ["script", "--", yamlLanePipelineRunCreateScript(spec, sourceCommit, pipelineRun)]);
|
||||
const created = await capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLanePipelineRunCreateScript(spec, sourceCommit, pipelineRun)]);
|
||||
const createPayload = captureJsonPayload(created);
|
||||
return {
|
||||
ok: created.exitCode === 0 && createPayload.ok !== false,
|
||||
@@ -2719,7 +2719,7 @@ async function refreshYamlLane(config: UniDeskConfig, options: RefreshOptions):
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
const refreshed = await capture(config, spec.nodeKubeRoute, ["script", "--", refreshYamlLaneScript(spec)]);
|
||||
const refreshed = await capture(config, spec.nodeKubeRoute, ["sh", "--", refreshYamlLaneScript(spec)]);
|
||||
const payload = captureJsonPayload(refreshed);
|
||||
return {
|
||||
ok: refreshed.exitCode === 0 && payload.ok !== false,
|
||||
@@ -2740,7 +2740,7 @@ async function refreshYamlLane(config: UniDeskConfig, options: RefreshOptions):
|
||||
|
||||
async function cleanupRuns(config: UniDeskConfig, options: CleanupRunsOptions): Promise<Record<string, unknown>> {
|
||||
const { configPath, spec } = resolveAgentRunLaneTarget(options);
|
||||
const result = await capture(config, spec.nodeKubeRoute, ["script", "--", cleanupRunsScript(options, spec.ci.namespace, spec.ci.pipelineRunPrefix)]);
|
||||
const result = await capture(config, spec.nodeKubeRoute, ["sh", "--", cleanupRunsScript(options, spec.ci.namespace, spec.ci.pipelineRunPrefix)]);
|
||||
const payload = captureJsonPayload(result);
|
||||
const ok = result.exitCode === 0 && payload.ok !== false;
|
||||
const base = {
|
||||
@@ -2779,7 +2779,7 @@ async function cleanupRuns(config: UniDeskConfig, options: CleanupRunsOptions):
|
||||
|
||||
async function cleanupReleasedPvs(config: UniDeskConfig, options: CleanupReleasedPvOptions): Promise<Record<string, unknown>> {
|
||||
const { configPath, spec } = resolveAgentRunLaneTarget(options);
|
||||
const result = await capture(config, spec.nodeKubeRoute, ["script", "--", cleanupReleasedPvsScript(options, spec.ci.namespace)]);
|
||||
const result = await capture(config, spec.nodeKubeRoute, ["sh", "--", cleanupReleasedPvsScript(options, spec.ci.namespace)]);
|
||||
const payload = captureJsonPayload(result);
|
||||
const ok = result.exitCode === 0 && payload.ok !== false;
|
||||
const base = {
|
||||
@@ -3111,7 +3111,7 @@ async function waitForYamlLaneSourceBootstrap(config: UniDeskConfig, spec: Agent
|
||||
let polls = 0;
|
||||
while (Date.now() - startedAt < timeoutMs) {
|
||||
polls += 1;
|
||||
const probe = await capture(config, spec.nodeRoute, ["script", "--", yamlLaneSourceBootstrapStatusScript(spec, jobId)]);
|
||||
const probe = await capture(config, spec.nodeRoute, ["sh", "--", yamlLaneSourceBootstrapStatusScript(spec, jobId)]);
|
||||
const payload = captureJsonPayload(probe);
|
||||
lastPayload = payload;
|
||||
progressEvent("agentrun.yaml-lane.source-bootstrap.progress", {
|
||||
@@ -3231,7 +3231,7 @@ async function waitForYamlLaneBuildImage(config: UniDeskConfig, spec: AgentRunLa
|
||||
let polls = 0;
|
||||
while (Date.now() - startedAt < timeoutMs) {
|
||||
polls += 1;
|
||||
const probe = await capture(config, spec.nodeRoute, ["script", "--", yamlLaneBuildImageStatusScript(spec, jobId)]);
|
||||
const probe = await capture(config, spec.nodeRoute, ["sh", "--", yamlLaneBuildImageStatusScript(spec, jobId)]);
|
||||
const payload = captureJsonPayload(probe);
|
||||
lastPayload = payload;
|
||||
progressEvent("agentrun.yaml-lane.image-build.progress", {
|
||||
@@ -3311,7 +3311,7 @@ function yamlLaneGitopsPublishScript(spec: AgentRunLaneSpec, files: readonly { p
|
||||
async function runYamlLaneGitMirrorSyncJob(config: UniDeskConfig, spec: AgentRunLaneSpec): Promise<Record<string, unknown>> {
|
||||
const jobName = `${spec.gitMirror.syncJobPrefix}-${Date.now().toString(36)}`.slice(0, 63);
|
||||
const manifest = yamlLaneGitMirrorJobManifest(spec, "sync", jobName);
|
||||
const created = await capture(config, spec.nodeKubeRoute, ["script", "--", createYamlLaneJobScript(spec.gitMirror.namespace, jobName, manifest)]);
|
||||
const created = await capture(config, spec.nodeKubeRoute, ["sh", "--", createYamlLaneJobScript(spec.gitMirror.namespace, jobName, manifest)]);
|
||||
if (created.exitCode !== 0) {
|
||||
return { ok: false, phase: "create-job", jobName, capture: compactCapture(created, { full: true, stdoutTailChars: 4000, stderrTailChars: 4000 }), valuesPrinted: false };
|
||||
}
|
||||
@@ -3320,7 +3320,7 @@ async function runYamlLaneGitMirrorSyncJob(config: UniDeskConfig, spec: AgentRun
|
||||
let lastProbe: SshCaptureResult | null = null;
|
||||
while (Date.now() - startedAt < 300_000) {
|
||||
polls += 1;
|
||||
lastProbe = await capture(config, spec.nodeKubeRoute, ["script", "--", yamlLaneJobProbeScript(spec.gitMirror.namespace, jobName)]);
|
||||
lastProbe = await capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneJobProbeScript(spec.gitMirror.namespace, jobName)]);
|
||||
const payload = captureJsonPayload(lastProbe);
|
||||
progressEvent("agentrun.yaml-lane.git-mirror.progress", {
|
||||
node: spec.nodeId,
|
||||
@@ -4265,7 +4265,7 @@ async function gitMirrorStatus(config: UniDeskConfig, options: GitMirrorStatusOp
|
||||
|
||||
async function readGitMirrorStatus(config: UniDeskConfig, target: { configPath: string; spec: AgentRunLaneSpec }): Promise<Record<string, unknown> & { result: SshCaptureResult; raw: string; summary: Record<string, unknown> }> {
|
||||
const spec = target.spec;
|
||||
const result = await capture(config, spec.nodeKubeRoute, ["script", "--", yamlLaneGitMirrorStatusScript(spec)]);
|
||||
const result = await capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneGitMirrorStatusScript(spec)]);
|
||||
const raw = result.stdout;
|
||||
const summary = captureJsonPayload(result);
|
||||
return {
|
||||
@@ -4300,7 +4300,7 @@ async function runGitMirrorJob(config: UniDeskConfig, action: "sync" | "flush",
|
||||
next: { confirm: `bun scripts/cli.ts ${command} --node ${spec.nodeId} --lane ${spec.lane} --confirm` },
|
||||
};
|
||||
}
|
||||
const created = await capture(config, spec.nodeKubeRoute, ["script", "--", createYamlLaneJobScript(spec.gitMirror.namespace, jobName, manifest)]);
|
||||
const created = await capture(config, spec.nodeKubeRoute, ["sh", "--", createYamlLaneJobScript(spec.gitMirror.namespace, jobName, manifest)]);
|
||||
if (created.exitCode !== 0 || !options.wait) {
|
||||
const status = await gitMirrorStatus(config, { full: false, raw: false, node: spec.nodeId, lane: spec.lane });
|
||||
return {
|
||||
@@ -4347,7 +4347,7 @@ async function waitForGitMirrorJob(config: UniDeskConfig, spec: AgentRunLaneSpec
|
||||
let polls = 0;
|
||||
while (Date.now() - startedAtMs <= timeoutSeconds * 1000) {
|
||||
polls += 1;
|
||||
lastProbe = await capture(config, spec.nodeKubeRoute, ["script", "--", yamlLaneJobProbeScript(spec.gitMirror.namespace, jobName)]);
|
||||
lastProbe = await capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneJobProbeScript(spec.gitMirror.namespace, jobName)]);
|
||||
const summary = captureJsonPayload(lastProbe);
|
||||
process.stderr.write(`${JSON.stringify({
|
||||
event: `agentrun.git-mirror.${action}.progress`,
|
||||
|
||||
+3
-3
@@ -1077,7 +1077,7 @@ async function remoteApplyManifest(config: UniDeskConfig, path: string, target =
|
||||
"kubectl apply -f \"$tmp\"",
|
||||
].join("\n");
|
||||
emitCiInstallProgress("kubectl-apply", "started", { providerId: target.providerId, manifest: path });
|
||||
const result = await runSshCommandCapture(config, `${target.providerId}:k3s`, ["script"], script);
|
||||
const result = await runSshCommandCapture(config, `${target.providerId}:k3s`, ["sh"], script);
|
||||
if (result.exitCode !== 0) throw new Error(`kubectl apply failed for ${path}: ${result.stderr || result.stdout}`);
|
||||
emitCiInstallProgress("upload-manifest", "succeeded", { providerId: target.providerId, manifest: path, upload: result.stdout.split(/\r?\n/u).find((line) => line.startsWith("manifest_bytes=")) ?? "" });
|
||||
emitCiInstallProgress("kubectl-apply", "succeeded", { providerId: target.providerId, manifest: path });
|
||||
@@ -2987,7 +2987,7 @@ async function logs(config: UniDeskConfig, name: string, target = ciTarget(null)
|
||||
if (/^[a-z0-9]([-a-z0-9]{0,46}[a-z0-9])?$/u.test(name)) {
|
||||
const runScript = remoteRunLogsScript(name, target, options);
|
||||
const result = options.capture === "ssh-stream"
|
||||
? await runSshCommandCapture(config, `${target.providerId}:k3s`, ["script"], runScript)
|
||||
? await runSshCommandCapture(config, `${target.providerId}:k3s`, ["sh"], runScript)
|
||||
: await dispatchSsh(runScript, 60_000, 45_000, true, target);
|
||||
const resultOk = "ok" in result ? result.ok : result.exitCode === 0;
|
||||
if (resultOk || (result.exitCode !== 42 && !result.stderr.includes("no_run_files="))) {
|
||||
@@ -3006,7 +3006,7 @@ async function logs(config: UniDeskConfig, name: string, target = ciTarget(null)
|
||||
}
|
||||
const script = pipelineRunLogsScript(name, options);
|
||||
const result = options.capture === "ssh-stream"
|
||||
? await runSshCommandCapture(config, `${target.providerId}:k3s`, ["script"], script)
|
||||
? await runSshCommandCapture(config, `${target.providerId}:k3s`, ["sh"], script)
|
||||
: await runRemoteKubectl(script, 60_000, 45_000, target);
|
||||
return ciLogsResultFromCapture({
|
||||
ok: result.exitCode === 0,
|
||||
|
||||
+15
-14
@@ -28,13 +28,13 @@ export function rootHelp(): unknown {
|
||||
{ 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." },
|
||||
{ command: "trans <providerId> py [script-args...] < script.py", description: "Run remote Python from local stdin through SSH passthrough without nested shell quoting; extra args become script argv." },
|
||||
{ command: "trans <providerId> script [--shell sh|bash] [script-args...] <<'SCRIPT' ...", description: "Run a remote shell script from local stdin using shell -s; default sh inherits provider proxy env and gets the portable printf helper used by shell/script." },
|
||||
{ command: "trans <providerId> sh|bash [arg...] <<'SH|BASH' ...", description: "Run a remote POSIX sh or Bash body from local stdin; the operation name declares the shell dialect, and removed `script`/`shell` operations fail with a migration hint." },
|
||||
{ command: "trans <providerId> skills [--scope all|wsl|windows] [--limit N]", description: "Discover WSL/Linux and, for WSL providers, Windows skill directories in one SSH passthrough call." },
|
||||
{ command: "trans <providerId> find <path...> [--max-depth N] [--type d|f|l] [--contains TEXT] [--iname PATTERN] [--limit N] [--sort]", description: "Run a structured remote find command without nested shell quoting or parentheses." },
|
||||
{ command: "trans <providerId> glob [--root DIR] [--pattern PATTERN] [--contains TEXT] [--type any|f|d] [--limit N] [--sort]", description: "Run remote glob matching through the injected helper without shell glob expansion." },
|
||||
{ command: "trans <providerId>:/absolute/workspace <operation args...>", description: "Route directly into a host workspace while keeping the operation parser independent from the location." },
|
||||
{ command: "trans <providerId>:k3s[:namespace:workload[:container]] <kubectl|logs|exec|script|apply-patch|apply-patch-v1|command> ...", description: "Locate a native k3s control plane or workload with route syntax, then run a separate operation with KUBECONFIG fixed and argv assembled by the CLI." },
|
||||
{ command: "trans <providerId> argv <command> [args...]", description: "Run a non-interactive remote command with each argv token shell-quoted by UniDesk before SSH passthrough; use `trans <providerId> script` when shell features are required." },
|
||||
{ command: "trans <providerId>:k3s[:namespace:workload[:container]] <kubectl|logs|exec|sh|bash|apply-patch|apply-patch-v1|command> ...", description: "Locate a native k3s control plane or workload with route syntax, then run a separate operation with KUBECONFIG fixed and argv assembled by the CLI." },
|
||||
{ command: "trans <providerId> argv <command> [args...]", description: "Run a non-interactive remote command with each argv token shell-quoted by UniDesk before SSH passthrough; use explicit `sh` or `bash` when shell features are required." },
|
||||
{ command: "microservice list", description: "List UniDesk-managed user services and their provider/runtime mapping." },
|
||||
{ command: "microservice status <id>", description: "Show one user service config, repository reference, backend mapping, and runtime status." },
|
||||
{ command: "microservice health <id> [--compact|--raw|--full]", description: "Probe one user service through backend-core -> provider-gateway HTTP proxy; default output is compact, and code-queue uses a commander-safe liveness summary unless raw/full is requested." },
|
||||
@@ -168,8 +168,8 @@ export function sshHelp(): unknown {
|
||||
"trans <providerId> playwright [--local-dir /tmp] <<'PW'",
|
||||
"trans <providerId> apply-patch-v1 [--allow-loose] < patch.diff",
|
||||
"trans <providerId> py [script-args...] < script.py",
|
||||
"trans <providerId> script [--shell sh|bash] [script-args...] <<'SCRIPT'",
|
||||
"trans <providerId> shell [--shell sh|bash] \"sed -n '1,20p' a && sed -n '1,20p' b\"",
|
||||
"trans <providerId> sh [arg...] <<'SH'",
|
||||
"trans <providerId> bash [arg...] <<'BASH'",
|
||||
"trans <providerId> skills [--scope all|wsl|windows] [--limit N]",
|
||||
"trans <providerId> find <path...> [--contains TEXT] [--limit N]",
|
||||
"trans <providerId> glob [--root DIR] [--pattern PATTERN]",
|
||||
@@ -184,7 +184,7 @@ export function sshHelp(): unknown {
|
||||
"trans D601:k3s kubectl get pods -n hwlab-dev",
|
||||
"trans G14:k3s",
|
||||
"trans G14:k3s kubectl get pipelineruns -n hwlab-ci",
|
||||
"trans D601:k3s script <<'SCRIPT'",
|
||||
"trans D601:k3s sh <<'SH'",
|
||||
"trans D601:k3s:hwlab-dev:hwlab-cloud-api:hwlab-cloud-api exec --cwd /app -- pwd",
|
||||
"trans D601:k3s:hwlab-dev:hwlab-cloud-api:hwlab-cloud-api apply-patch --cwd /app <<'PATCH'",
|
||||
"trans D601:k3s:hwlab-dev:hwlab-cloud-api apply-patch-v1 <<'PATCH'",
|
||||
@@ -193,26 +193,27 @@ export function sshHelp(): unknown {
|
||||
"trans D601:win upload ./tool.mjs F:\\Work\\hwlab\\.tmp\\tool.mjs",
|
||||
"trans D601:win download F:\\Work\\hwlab\\.tmp\\tool.mjs ./tool.mjs",
|
||||
"trans D601:k3s:hwlab-dev:hwlab-cloud-api node -e 'console.log(process.version)'",
|
||||
"trans D601:k3s:hwlab-dev:hwlab-cloud-api script <<'SCRIPT'",
|
||||
"trans D601:k3s:hwlab-dev:hwlab-cloud-api sh <<'SH'",
|
||||
"trans D601:k3s:hwlab-dev:hwlab-cloud-api logs --tail 80",
|
||||
"trans G14:k3s logs -n agentrun-ci -l tekton.dev/pipelineRun=<run> --tail 120",
|
||||
],
|
||||
notes: [
|
||||
"trans --help and trans <route> --help print this JSON help and never open an interactive session; the underlying ssh subcommand keeps the same help behavior.",
|
||||
"For non-interactive remote commands, prefer argv for a single process and script/stdin for shell logic.",
|
||||
"For non-interactive remote commands, prefer argv for a single process and explicit sh/bash stdin for shell logic.",
|
||||
"Windows routes have explicit Windows operations, not POSIX shell aliases: `ps` runs Windows PowerShell from stdin or one inline command, `cmd` runs cmd.exe/batch from stdin or one command line, and `skills` discovers Windows skill directories.",
|
||||
"For Windows PowerShell, use `trans <provider>:win ps <<'PS'`; the PowerShell body is written to a temporary .ps1 with UTF-8 settings and executed by powershell.exe. Do not use `script` for Windows PowerShell.",
|
||||
"For Windows PowerShell, use `trans <provider>:win ps <<'PS'`; the PowerShell body is written to a temporary .ps1 with UTF-8 settings and executed by powershell.exe. Do not use POSIX `sh` or `bash` for Windows PowerShell.",
|
||||
"For Windows cmd.exe, use `trans <provider>:win/<drive>/<path> cmd <<'CMD'`; `cmd` with no command-line arguments reads the UTF-8 batch body from stdin, injects UTF-8/Python encoding defaults, runs it from a temp .cmd file, and deletes the temp file.",
|
||||
"`argv` executes direct argv tokens only: `trans <route> argv ls -la` is valid, but `trans <route> argv 'ls -la'` is rejected because the single string would be treated as an executable path; use `script -- 'ls -la'` for one-line shell logic.",
|
||||
"For one-line remote shell logic without a heredoc, use `script -- '<command && command>'`; outer shell operators written outside trans, such as `trans G14:/repo sed ... && sed ...`, are parsed by the local shell before UniDesk starts and therefore cannot be redirected by the CLI. The explicit `shell '<command>'` operation remains available for the same sh -c path.",
|
||||
"When a one-line shell command is easier to type through the script path, `script -- '<command && command>'` runs that single string through the remote shell without waiting for stdin. When `script --` is followed by multiple tokens, it stays a direct argv form for commands such as `trans D601:/work script -- sed -n '1,20p' file`.",
|
||||
"script and shell helper modes inject a tiny POSIX-compatible printf wrapper before user shell text, so portable printf headings such as `printf \"--- section ---\\n\"` work consistently under dash/sh and bash. Direct argv commands are unchanged.",
|
||||
"`argv` executes direct argv tokens only: `trans <route> argv ls -la` is valid, but `trans <route> argv 'ls -la'` is rejected because the single string would be treated as an executable path; use `sh -- 'ls -la'` or `bash -- 'ls -la'` for one-line shell logic.",
|
||||
"For one-line remote shell logic without a heredoc, use `sh -- '<command && command>'` for POSIX syntax or `bash -- '<command && command>'` for Bash syntax. Outer shell operators written outside trans, such as `trans G14:/repo sed ... && sed ...`, are parsed by the local shell before UniDesk starts and therefore cannot be redirected by the CLI.",
|
||||
"`sh --` and `bash --` accept exactly one shell command string. For direct argv commands with multiple tokens, use `argv`, `exec`, or a known direct subcommand such as `git`, `rg`, `sed`, or `cat`.",
|
||||
"The removed `script` and `shell` operations intentionally fail. The operation name must declare the shell dialect: `sh` maps to target `/bin/sh`, while `bash` maps to target `bash`.",
|
||||
"sh and bash helper modes inject a tiny POSIX-compatible printf wrapper before user shell text, so portable printf headings such as `printf \"--- section ---\\n\"` work consistently under dash/sh and bash. Direct argv commands are unchanged.",
|
||||
"For arbitrary stdin streams into a workload command, use a workload route plus `exec --stdin -- <command> ...`; this keeps the route as location-only and avoids heredoc/base64/tar shell wrapping.",
|
||||
"`apply-patch` is the default remote text patch entry and uses the v2 local line-based patch engine with remote read/write operations, including Windows routes such as `D601:win/c/test`, so long Unicode/Chinese lines and pure insertion hunks avoid the legacy remote shell hunk parser. Plain multi-file Update File patches on POSIX host/k3s and Windows workspace routes use bulk read/write operations to avoid per-file SSH round trips. Its stdout follows Codex apply_patch text output rather than UniDesk JSON output; stderr keeps Codex-style failure text and appends one `UNIDESK_APPLY_PATCH_TIMING` JSON summary with durationMs, patchBytes, fileCount, hunkCount, changedCount, remoteOperationCount, remoteOperationCounts and remoteElapsedMs so slow patch runs can be attributed without changing success stdout.",
|
||||
"`upload` and `download` are the default whole-file transfer entries for non-text and generated files. They write through remote temp files, verify byte count and SHA-256 on both sides, and return `verification.automatic=true`, `verification.verified=true`, and `verification.match.{bytes,sha256}=true`; this JSON is the transfer integrity proof, so callers do not need a separate manual `sha256sum` check. Downloads stream over `host.ssh.tcp-pool`, emit progress JSON, and may receive a caller-supplied `--inactivity-timeout-ms` from async artifact/deploy jobs so active large transfers are not killed by the generic short-command budget.",
|
||||
"`playwright` runs a stdin heredoc on the target POSIX host/workload with a temporary `playwright-cli` wrapper in PATH, submits the remote script as a background job, polls short status commands for the manifest, then downloads image/pdf artifacts to local `/tmp` by default with the same verified SHA-256 transfer path. Canonical syntax is `trans D601 playwright <<'PW' ... PW`; workspace and known UniDesk host workspaces are preferred for wrapper discovery before external skill passthroughs.",
|
||||
"`apply-patch-v1` is the only legacy fallback entry: it rejects low-context update hunks by default, reports the matched file:line for each hunk on stderr, and only accepts --allow-loose when the caller has manually reviewed an intentionally ambiguous insertion.",
|
||||
"script defaults to target /bin/sh and inherits provider proxy variables such as HTTP_PROXY/HTTPS_PROXY/ALL_PROXY/NO_PROXY; it is for host/k3s POSIX shell only. Use --shell bash only for bash syntax such as pipefail, arrays, or [[ ... ]], not as a proxy workaround.",
|
||||
"`sh` inherits provider proxy variables such as HTTP_PROXY/HTTPS_PROXY/ALL_PROXY/NO_PROXY and runs target `/bin/sh`; use `bash` only for Bash syntax such as `pipefail`, arrays, substring expansion, or `[[ ... ]]`, not as a proxy workaround.",
|
||||
"Route syntax is `{provider}:{plane}[:{scope...}] {operation} [operation-args...]`: the first argv token locates a distributed target only, and every following token belongs to the operation parser. Host workspace routes use `<provider>:/absolute/workspace`; WSL providers can use `<provider>:win ps` for Windows PowerShell and `<provider>:win cmd` for Windows cmd.exe, with `<provider>:win/c/test ...` mapping the Windows cwd to `C:\\test`; native k3s providers such as D601 and G14 use `<provider>:k3s` for the control plane and `<provider>:k3s:<namespace>:<workload>[:<container>]` for a workload/container. In k3s routes, `:` is the distributed route separator; `/...` is only an in-container filesystem cwd and never selects a container. Prefer operation `--cwd /path` when a container is also specified.",
|
||||
"Use `win`, not `win32`; the win route is a Windows operation plane. `ps` and `cmd` both set UTF-8/Python encoding defaults, while `cmd` also sets `chcp 65001`.",
|
||||
"`<provider>:win skills` discovers the current Windows user's `%USERPROFILE%\\.agents\\skills` by default; use `--scope all` to include `%USERPROFILE%\\.codex\\skills`.",
|
||||
|
||||
+26
-26
@@ -972,11 +972,11 @@ function shortSha(sha: string): string {
|
||||
}
|
||||
|
||||
function precheckWorkspace(): CommandJsonResult {
|
||||
return cliJson(["ssh", `${G14_PROVIDER}:${G14_WORKSPACE}`, "script", "--", "pwd; git fetch origin G14 --prune; git status --short --branch; git remote -v | sed -n '1,4p'"], 120_000);
|
||||
return cliJson(["ssh", `${G14_PROVIDER}:${G14_WORKSPACE}`, "sh", "--", "pwd; git fetch origin G14 --prune; git status --short --branch; git remote -v | sed -n '1,4p'"], 120_000);
|
||||
}
|
||||
|
||||
function g14HostScript(script: string, timeoutMs = 120_000): CommandJsonResult {
|
||||
return cliJson(["ssh", G14_PROVIDER, "script", "--", script], timeoutMs);
|
||||
return cliJson(["ssh", G14_PROVIDER, "sh", "--", script], timeoutMs);
|
||||
}
|
||||
|
||||
function g14K3s(args: string[], timeoutMs = 60_000): CommandJsonResult {
|
||||
@@ -984,7 +984,7 @@ function g14K3s(args: string[], timeoutMs = 60_000): CommandJsonResult {
|
||||
}
|
||||
|
||||
function g14K3sInlineScriptWithInput(script: string, input: string, timeoutMs = 60_000): CommandJsonResult {
|
||||
return commandJsonWithInput(["bun", "scripts/cli.ts", "ssh", `${G14_PROVIDER}:k3s`, "script", "--", script], input, timeoutMs);
|
||||
return commandJsonWithInput(["bun", "scripts/cli.ts", "ssh", `${G14_PROVIDER}:k3s`, "sh", "--", script], input, timeoutMs);
|
||||
}
|
||||
|
||||
function remoteAsyncStatusDir(label: string): string {
|
||||
@@ -1119,7 +1119,7 @@ function parseRemoteAsyncPayload(result: CommandJsonResult): Record<string, unkn
|
||||
|
||||
function runG14K3sRemoteAsync(spec: RemoteAsyncCommandSpec): CommandJsonResult {
|
||||
const startedAtMs = Date.now();
|
||||
const start = g14K3s(["script", "--", remoteAsyncStartScript(spec)], 55_000);
|
||||
const start = g14K3s(["sh", "--", remoteAsyncStartScript(spec)], 55_000);
|
||||
const startPayload = parseRemoteAsyncPayload(start);
|
||||
if (!isCommandSuccess(start) || startPayload.ok !== true) return start;
|
||||
const deadlineMs = startedAtMs + Math.max(1, spec.timeoutSeconds) * 1000;
|
||||
@@ -1129,7 +1129,7 @@ function runG14K3sRemoteAsync(spec: RemoteAsyncCommandSpec): CommandJsonResult {
|
||||
attempts += 1;
|
||||
const wait = runCommand(["sleep", attempts <= 1 ? "1" : "2"], repoRoot, { timeoutMs: 3_000 });
|
||||
if (wait.exitCode !== 0 && wait.timedOut) break;
|
||||
const status = g14K3s(["script", "--", remoteAsyncStatusScript(spec.label, spec.token)], 55_000);
|
||||
const status = g14K3s(["sh", "--", remoteAsyncStatusScript(spec.label, spec.token)], 55_000);
|
||||
lastStatus = status;
|
||||
const payload = parseRemoteAsyncPayload(status);
|
||||
const state = typeof payload.state === "string" ? payload.state : null;
|
||||
@@ -1156,7 +1156,7 @@ function runG14K3sRemoteAsync(spec: RemoteAsyncCommandSpec): CommandJsonResult {
|
||||
};
|
||||
}
|
||||
}
|
||||
const killed = g14K3s(["script", "--", remoteAsyncKillScript(spec.label, spec.token)], 55_000);
|
||||
const killed = g14K3s(["sh", "--", remoteAsyncKillScript(spec.label, spec.token)], 55_000);
|
||||
const payload = parseRemoteAsyncPayload(killed);
|
||||
return {
|
||||
ok: false,
|
||||
@@ -1358,7 +1358,7 @@ function getV02TriggerSnapshot(): V02TriggerSnapshot {
|
||||
"printf 'message\\t%s\\n' \"$(printf '%s\\n' \"$status_text\" | sed -n '3p')\"",
|
||||
"printf 'pipelineRunStderr\\t%s\\n' \"$(one_line < \"$tmp_dir/pipelinerun.err\")\"",
|
||||
].join("\n");
|
||||
const result = g14K3s(["script", "--", script], 180_000);
|
||||
const result = g14K3s(["sh", "--", script], 180_000);
|
||||
return parseV02TriggerSnapshot(result, Date.now());
|
||||
}
|
||||
|
||||
@@ -1629,7 +1629,7 @@ function v02ControlPlaneStatusBundle(target: V02ControlPlaneStatusTarget = {}):
|
||||
`section gitMirrorCache kubectl exec -n ${shellQuote(GIT_MIRROR_NAMESPACE)} deploy/git-mirror-http -- sh -lc ${shellQuote(gitMirrorCacheProbeScript())}`,
|
||||
`section webAssets sh -c ${shellQuote(v02WebAssetsProbeScript())}`,
|
||||
].join("\n");
|
||||
return g14K3s(["script", "--", script], 60_000);
|
||||
return g14K3s(["sh", "--", script], 60_000);
|
||||
}
|
||||
|
||||
function v02SourceHeadsProbeScript(): string {
|
||||
@@ -2373,7 +2373,7 @@ export function v02CloseoutVerdict(status: Record<string, unknown>): Record<stri
|
||||
exitCode: sourceHeadsTarget.ancestorExitCode ?? null,
|
||||
command: sourceCommit === null
|
||||
? null
|
||||
: `trans G14:/root/hwlab-v02 script -- 'git fetch origin v0.2 && git merge-base --is-ancestor ${sourceCommit} origin/v0.2'`,
|
||||
: `trans G14:/root/hwlab-v02 sh -- 'git fetch origin v0.2 && git merge-base --is-ancestor ${sourceCommit} origin/v0.2'`,
|
||||
};
|
||||
const recommendedNext = v02CloseoutRecommendedNext({
|
||||
closeable,
|
||||
@@ -3052,7 +3052,7 @@ function runControlPlaneReleasedPvCleanup(options: G14ControlPlaneOptions): Reco
|
||||
deletion,
|
||||
followUp: {
|
||||
diskPressure: "trans G14:k3s kubectl get node ubuntu-rog-zephyrus-g14-ga401iv-ga401iv -o jsonpath='{range .status.conditions[*]}{.type}{\"=\"}{.status}{\" \"}{.reason}{\"\\n\"}{end}'",
|
||||
storage: "trans G14 script -- 'df -h /; sudo du -xh -d 1 /var/lib/rancher/k3s/storage 2>/dev/null | sort -h | tail -20'",
|
||||
storage: "trans G14 sh -- 'df -h /; sudo du -xh -d 1 /var/lib/rancher/k3s/storage 2>/dev/null | sort -h | tail -20'",
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -3342,7 +3342,7 @@ function applyRuntimeLaneControlPlaneFiles(spec: HwlabRuntimeLaneSpec, renderDir
|
||||
? ["set -eu", shellCommand.replace(/^exec /, ""), ...cleanupCommands].join("\n")
|
||||
: shellCommand;
|
||||
const visibleCommand = cleanupCommands.length > 0
|
||||
? ["bun", "scripts/cli.ts", "ssh", `${G14_PROVIDER}:k3s`, "script", "--", script]
|
||||
? ["bun", "scripts/cli.ts", "ssh", `${G14_PROVIDER}:k3s`, "sh", "--", script]
|
||||
: command;
|
||||
return runG14K3sRemoteAsync({
|
||||
script,
|
||||
@@ -3642,7 +3642,7 @@ function createRuntimeLanePipelineRun(spec: HwlabRuntimeLaneSpec, sourceCommit:
|
||||
"fi",
|
||||
`kubectl get pipelinerun -n ${shellQuote(CI_NAMESPACE)} ${shellQuote(pipelineRun)} -o jsonpath='{.metadata.name}{\"\\n\"}{.metadata.labels.hwlab\\.pikastech\\.local/source-commit}{\"\\n\"}{.status.conditions[0].status}{\"\\n\"}{.status.conditions[0].reason}{\"\\n\"}'`,
|
||||
].join("\n");
|
||||
return g14K3s(["script", "--", script], timeoutSeconds * 1000);
|
||||
return g14K3s(["sh", "--", script], timeoutSeconds * 1000);
|
||||
}
|
||||
|
||||
function createV02PipelineRun(sourceCommit: string, timeoutSeconds: number): CommandJsonResult {
|
||||
@@ -3927,7 +3927,7 @@ function runtimeLaneControlPlaneStatusBundle(spec: HwlabRuntimeLaneSpec, target:
|
||||
shellQuote(pipelineRunRowsJsonPath()),
|
||||
].join(" "),
|
||||
].join("\n");
|
||||
return g14K3s(["script", "--", script], 120_000);
|
||||
return g14K3s(["sh", "--", script], 120_000);
|
||||
}
|
||||
|
||||
function runtimeLanePublicProbeScript(spec: HwlabRuntimeLaneSpec): string {
|
||||
@@ -4174,7 +4174,7 @@ function runtimeLaneArgoRefreshScript(spec: HwlabRuntimeLaneSpec, dryRun: boolea
|
||||
}
|
||||
|
||||
function refreshRuntimeLaneArgoApplication(spec: HwlabRuntimeLaneSpec, options: G14ControlPlaneOptions): Record<string, unknown> {
|
||||
const result = g14K3s(["script", "--", runtimeLaneArgoRefreshScript(spec, options.dryRun)], options.timeoutSeconds * 1000);
|
||||
const result = g14K3s(["sh", "--", runtimeLaneArgoRefreshScript(spec, options.dryRun)], options.timeoutSeconds * 1000);
|
||||
const fields = keyValueLinesFromText(statusText(result));
|
||||
const ok = isCommandSuccess(result);
|
||||
return {
|
||||
@@ -5315,7 +5315,7 @@ function runG14Secret(options: G14SecretOptions): Record<string, unknown> {
|
||||
: null;
|
||||
const result = options.preset === "master-server-admin-api-key"
|
||||
? g14K3sInlineScriptWithInput(script, localAdminApiKey?.key ?? "", options.timeoutSeconds * 1000 + 2000)
|
||||
: g14K3s(["script", "--", script], options.timeoutSeconds * 1000);
|
||||
: g14K3s(["sh", "--", script], options.timeoutSeconds * 1000);
|
||||
const status = v02SecretStatusFromText(statusText(result), isCommandSuccess(result), result.exitCode, result.stderr);
|
||||
const dryRunOk = options.action === "ensure" && options.dryRun && isCommandSuccess(result);
|
||||
const deleteDryRunOk = options.action === "delete" && options.dryRun && isCommandSuccess(result);
|
||||
@@ -5482,7 +5482,7 @@ function legacyG14RetirementStatusScript(): string {
|
||||
}
|
||||
|
||||
function legacyG14RetirementStatus(): Record<string, unknown> {
|
||||
const result = g14K3s(["script", "--", legacyG14RetirementStatusScript()], 60_000);
|
||||
const result = g14K3s(["sh", "--", legacyG14RetirementStatusScript()], 60_000);
|
||||
const sections = parseShellSections(statusText(result));
|
||||
const legacyApplications = parseSectionJsonArray(sections.legacyApplications).map(applicationSummary);
|
||||
const legacyNamespaces = parseSectionJsonArray(sections.legacyNamespaces).map(namespaceSummary);
|
||||
@@ -5628,7 +5628,7 @@ function runLegacyG14Retirement(options: G14LegacyRetirementOptions): Record<str
|
||||
|
||||
const markerBeforeDelete = writeLegacyG14RetirementMarker("retiring", options.reason, { command: "hwlab g14 retirement execute --confirm" });
|
||||
const localJobs = cancelLegacyG14MonitorJobs(false);
|
||||
const deleteResult = g14K3s(["script", "--", legacyG14RetirementDeleteScript()], Math.max(60_000, options.timeoutSeconds * 1000));
|
||||
const deleteResult = g14K3s(["sh", "--", legacyG14RetirementDeleteScript()], Math.max(60_000, options.timeoutSeconds * 1000));
|
||||
const deleteSections = parseShellSections(statusText(deleteResult));
|
||||
const afterDelete = legacyG14RetirementStatus();
|
||||
const wait = options.wait ? waitForLegacyG14Retirement(options.timeoutSeconds) : null;
|
||||
@@ -6387,7 +6387,7 @@ function runGitMirrorStatus(lane: HwlabRuntimeLane = "v02"): Record<string, unkn
|
||||
shellQuote(gitMirrorCacheProbeScript()),
|
||||
].join(" "),
|
||||
].join("\n");
|
||||
const bundle = g14K3s(["script", "--", script], 60_000);
|
||||
const bundle = g14K3s(["sh", "--", script], 60_000);
|
||||
const sections = parseShellSections(statusText(bundle));
|
||||
const resources = sections.resources;
|
||||
const legacyCronJob = sections.legacyCronJob;
|
||||
@@ -6518,7 +6518,7 @@ function runGitMirrorSync(options: G14GitMirrorOptions): Record<string, unknown>
|
||||
`kubectl exec -n ${shellQuote(GIT_MIRROR_NAMESPACE)} deploy/git-mirror-http -- sh -lc ${shellQuote(`cat /cache/HWLAB.last-sync.json 2>/dev/null || true; printf "\\n"; git --git-dir=/cache/pikasTech/HWLAB.git rev-parse refs/heads/${spec.sourceBranch} refs/mirror-stage/heads/${spec.sourceBranch} refs/heads/${spec.gitopsBranch} refs/mirror-stage/heads/${spec.gitopsBranch} 2>/dev/null || true`)}`,
|
||||
].join("\n"),
|
||||
].join("\n");
|
||||
const result = g14K3s(["script", "--", script], options.timeoutSeconds * 1000 + 30_000);
|
||||
const result = g14K3s(["sh", "--", script], options.timeoutSeconds * 1000 + 30_000);
|
||||
const syncCommand = spec.lane === "v02"
|
||||
? "bun scripts/cli.ts hwlab g14 git-mirror sync --lane v02 --confirm"
|
||||
: `bun scripts/cli.ts hwlab nodes git-mirror sync --node ${spec.nodeId} --lane ${spec.lane} --confirm`;
|
||||
@@ -6574,7 +6574,7 @@ function runGitMirrorFlush(options: G14GitMirrorOptions): Record<string, unknown
|
||||
`kubectl exec -n ${shellQuote(GIT_MIRROR_NAMESPACE)} deploy/git-mirror-http -- sh -lc ${shellQuote(`cat /cache/HWLAB.last-flush.json 2>/dev/null || true; printf "\\n"; git --git-dir=/cache/pikasTech/HWLAB.git rev-parse refs/heads/${options.lane === "v02" ? "v0.2-gitops" : hwlabRuntimeLaneSpec(options.lane).gitopsBranch} refs/mirror-stage/heads/${options.lane === "v02" ? "v0.2-gitops" : hwlabRuntimeLaneSpec(options.lane).gitopsBranch} 2>/dev/null || true`)}`,
|
||||
].join("\n"),
|
||||
].join("\n");
|
||||
const result = g14K3s(["script", "--", script], options.timeoutSeconds * 1000 + 30_000);
|
||||
const result = g14K3s(["sh", "--", script], options.timeoutSeconds * 1000 + 30_000);
|
||||
return {
|
||||
ok: isCommandSuccess(result),
|
||||
command: "hwlab g14 git-mirror flush",
|
||||
@@ -7367,7 +7367,7 @@ function g14ObservabilityStatus(): Record<string, unknown> {
|
||||
`section workloadMonitors kubectl get servicemonitor,prometheusrule -n ${shellQuote(V02_RUNTIME_NAMESPACE)} -l hwlab.pikastech.local/monitoring=enabled -o json`,
|
||||
`section query kubectl get --raw ${shellQuote(queryPath)}`,
|
||||
].join("\n");
|
||||
const bundle = g14K3s(["script", "--", script], 120_000);
|
||||
const bundle = g14K3s(["sh", "--", script], 120_000);
|
||||
const sections = parseShellSections(statusText(bundle));
|
||||
const namespace = parseSectionJson(sections.namespace);
|
||||
const discoveryNamespace = parseSectionJson(sections.discoveryNamespace);
|
||||
@@ -7538,7 +7538,7 @@ function runG14ObservabilityApply(options: G14ObservabilityOptions): Record<stri
|
||||
const manifest = g14PrometheusManifest();
|
||||
const manifestB64 = Buffer.from(JSON.stringify(manifest), "utf8").toString("base64");
|
||||
const script = g14ObservabilityApplyScript(options, manifestB64);
|
||||
const result = g14K3s(["script", "--", script], options.timeoutSeconds * 1000 + 90_000);
|
||||
const result = g14K3s(["sh", "--", script], options.timeoutSeconds * 1000 + 90_000);
|
||||
const ok = isCommandSuccess(result);
|
||||
return {
|
||||
ok,
|
||||
@@ -7629,7 +7629,7 @@ function runG14ObservabilityTargets(options: G14ObservabilityOptions): Record<st
|
||||
`section podResourceMetrics kubectl get --raw ${shellQuote(`/apis/metrics.k8s.io/v1beta1/namespaces/${V02_RUNTIME_NAMESPACE}/pods`)}`,
|
||||
...queryCommands,
|
||||
].join("\n");
|
||||
const bundle = g14K3s(["script", "--", script], options.timeoutSeconds * 1000);
|
||||
const bundle = g14K3s(["sh", "--", script], options.timeoutSeconds * 1000);
|
||||
const sections = parseShellSections(statusText(bundle));
|
||||
const pods = k8sItems(parseSectionJson(sections.pods));
|
||||
const monitors = k8sItems(parseSectionJson(sections.monitors));
|
||||
@@ -7730,7 +7730,7 @@ function runG14ObservabilityBoundary(options: G14ObservabilityOptions): Record<s
|
||||
`section workloadMonitoring kubectl get servicemonitor,podmonitor,prometheusrule,prometheus,alertmanager -n ${shellQuote(V02_RUNTIME_NAMESPACE)} -o json`,
|
||||
`section infraControlPlane kubectl get prometheus,alertmanager -n ${shellQuote(G14_OBSERVABILITY_NAMESPACE)} -o json`,
|
||||
].join("\n");
|
||||
const bundle = g14K3s(["script", "--", script], options.timeoutSeconds * 1000);
|
||||
const bundle = g14K3s(["sh", "--", script], options.timeoutSeconds * 1000);
|
||||
const sections = parseShellSections(statusText(bundle));
|
||||
const workloadItems = k8sItems(parseSectionJson(sections.workloadMonitoring));
|
||||
const infraItems = k8sItems(parseSectionJson(sections.infraControlPlane));
|
||||
@@ -7969,7 +7969,7 @@ function startGitMirrorJob(options: G14GitMirrorOptions): Record<string, unknown
|
||||
}
|
||||
|
||||
function g14HostScript(script: string, timeoutMs = 120_000): CommandJsonResult {
|
||||
return cliJson(["ssh", G14_PROVIDER, "script", "--", script], timeoutMs);
|
||||
return cliJson(["ssh", G14_PROVIDER, "sh", "--", script], timeoutMs);
|
||||
}
|
||||
|
||||
function g14CiToolsImage(tag: string): string {
|
||||
@@ -8223,7 +8223,7 @@ function mergePullRequest(number: number, dryRun: boolean): CommandJsonResult {
|
||||
}
|
||||
|
||||
function getG14Head(): string | null {
|
||||
const result = cliJson(["ssh", `${G14_PROVIDER}:${G14_WORKSPACE}`, "script", "--", "git fetch origin G14 --prune >/dev/null 2>&1; git rev-parse origin/G14"], 120_000);
|
||||
const result = cliJson(["ssh", `${G14_PROVIDER}:${G14_WORKSPACE}`, "sh", "--", "git fetch origin G14 --prune >/dev/null 2>&1; git rev-parse origin/G14"], 120_000);
|
||||
if (!isCommandSuccess(result)) return null;
|
||||
const output = String(nested(result.parsed, ["data", "stdout"]) ?? result.stdout).trim();
|
||||
const match = /[0-9a-f]{40}/iu.exec(output);
|
||||
|
||||
@@ -1748,7 +1748,7 @@ function manifestObjectSummary(manifest: readonly Record<string, unknown>[]): Re
|
||||
}
|
||||
|
||||
function runTransK3s(kubeRoute: string, script: string, timeoutSeconds: number): CommandResult {
|
||||
return runCommand(["/root/.local/bin/trans", kubeRoute, "script", "--", script], rootPath(), { timeoutMs: timeoutSeconds * 1000 });
|
||||
return runCommand(["/root/.local/bin/trans", kubeRoute, "sh", "--", script], rootPath(), { timeoutMs: timeoutSeconds * 1000 });
|
||||
}
|
||||
|
||||
function proxyExportBlock(node: ControlPlaneNodeSpec): string {
|
||||
|
||||
@@ -449,7 +449,7 @@ function transPath(): string {
|
||||
}
|
||||
|
||||
function runNodeHostScript(spec: HwlabRuntimeLaneSpec, script: string, timeoutSeconds: number, input = ""): CommandResult {
|
||||
return runCommand([transPath(), spec.nodeRoute, "script", "--", script], repoRoot, { input, timeoutMs: timeoutSeconds * 1000 });
|
||||
return runCommand([transPath(), spec.nodeRoute, "sh", "--", script], repoRoot, { input, timeoutMs: timeoutSeconds * 1000 });
|
||||
}
|
||||
|
||||
function runNodeHostScriptAsync(spec: HwlabRuntimeLaneSpec, script: string, timeoutSeconds: number, label: string): CommandResult {
|
||||
@@ -535,7 +535,7 @@ function runNodeHostScriptAsync(spec: HwlabRuntimeLaneSpec, script: string, time
|
||||
"cat \"$status_path\" 2>/dev/null || true",
|
||||
].join("\n"), 55);
|
||||
return {
|
||||
command: [transPath(), spec.nodeRoute, "script", "--", "<remote async script>"],
|
||||
command: [transPath(), spec.nodeRoute, "sh", "--", "<remote async script>"],
|
||||
cwd: repoRoot,
|
||||
exitCode: 124,
|
||||
stdout: typeof payload.stdout === "string" ? payload.stdout : "",
|
||||
@@ -561,7 +561,7 @@ function parseJsonObject(text: string): Record<string, unknown> {
|
||||
|
||||
function commandResultFromAsync(spec: HwlabRuntimeLaneSpec, payload: Record<string, unknown>, statusPath: string, timedOut: boolean): CommandResult {
|
||||
return {
|
||||
command: [transPath(), spec.nodeRoute, "script", "--", "<remote async script>"],
|
||||
command: [transPath(), spec.nodeRoute, "sh", "--", "<remote async script>"],
|
||||
cwd: repoRoot,
|
||||
exitCode: typeof payload.exitCode === "number" ? payload.exitCode : payload.ok === true ? 0 : 1,
|
||||
stdout: typeof payload.stdout === "string" ? payload.stdout : "",
|
||||
@@ -576,7 +576,7 @@ function runNodeK3sArgs(spec: HwlabRuntimeLaneSpec, args: string[], timeoutSecon
|
||||
}
|
||||
|
||||
function runNodeK3sScript(spec: HwlabRuntimeLaneSpec, script: string, timeoutSeconds: number, input = ""): CommandResult {
|
||||
return runCommand([transPath(), spec.nodeKubeRoute, "script", "--", script], repoRoot, { input, timeoutMs: timeoutSeconds * 1000 });
|
||||
return runCommand([transPath(), spec.nodeKubeRoute, "sh", "--", script], repoRoot, { input, timeoutMs: timeoutSeconds * 1000 });
|
||||
}
|
||||
|
||||
function isCommandSuccess(result: CommandResult): boolean {
|
||||
@@ -3925,7 +3925,7 @@ function runPublicExposureFrpcRecreate(options: NodePublicExposureOptions): Comm
|
||||
&& fields.rolloutStatusExitCode === "0";
|
||||
const stdout = Object.entries(fields).map(([key, value]) => `${key}\t${value}`).join("\n") + "\n";
|
||||
return {
|
||||
command: [transPath(), `${options.node}:k3s`, "script", "--", "<public-exposure-frpc-recreate>"],
|
||||
command: [transPath(), `${options.node}:k3s`, "sh", "--", "<public-exposure-frpc-recreate>"],
|
||||
cwd: repoRoot,
|
||||
exitCode: ok ? 0 : 1,
|
||||
stdout: [stdout.trim(), ...stdoutParts].filter(Boolean).join("\n") + "\n",
|
||||
@@ -4082,11 +4082,11 @@ ${tlsLines} @api path /health* /auth* /v1* /json-rpc* /openapi* /docs* /swagg
|
||||
}
|
||||
|
||||
function runTransScript(node: string, script: string, input: string, timeoutSeconds: number): CommandResult {
|
||||
return runCommand([transPath(), `${node}:k3s`, "script", "--", script], repoRoot, { input, timeoutMs: timeoutSeconds * 1000 });
|
||||
return runCommand([transPath(), `${node}:k3s`, "sh", "--", script], repoRoot, { input, timeoutMs: timeoutSeconds * 1000 });
|
||||
}
|
||||
|
||||
function runTransHostScript(node: string, script: string, input: string, timeoutSeconds: number): CommandResult {
|
||||
return runCommand([transPath(), node, "script", "--", script], repoRoot, { input, timeoutMs: timeoutSeconds * 1000 });
|
||||
return runCommand([transPath(), node, "sh", "--", script], repoRoot, { input, timeoutMs: timeoutSeconds * 1000 });
|
||||
}
|
||||
|
||||
function runObsoleteSecretCleanup(options: NodeSecretOptions, spec: RuntimeSecretSpec): Record<string, unknown> {
|
||||
|
||||
@@ -138,7 +138,7 @@ print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||
sys.exit(0 if payload["ok"] else 1)
|
||||
PY
|
||||
`;
|
||||
const result = await runSshCommandCapture(config, exposure.pk01.route, ["script"], script);
|
||||
const result = await runSshCommandCapture(config, exposure.pk01.route, ["sh"], script);
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
return parsed ?? { ok: false, capture: compactCapture(result, { full: true }) };
|
||||
}
|
||||
|
||||
@@ -1226,7 +1226,7 @@ function desiredChecks(pg: PostgresHostConfig, facts: RemoteFacts, secrets: Secr
|
||||
}
|
||||
|
||||
async function remoteFacts(config: UniDeskConfig, pg: PostgresHostConfig, secrets: SecretInspection | null): Promise<{ capture: SshCaptureResult; parsed: RemoteFacts | null }> {
|
||||
const capture = await runSshCommandCapture(config, pg.node.route, ["script"], factsScript(pg, appProbes(pg, secrets)));
|
||||
const capture = await runSshCommandCapture(config, pg.node.route, ["sh"], factsScript(pg, appProbes(pg, secrets)));
|
||||
const parsed = parseJsonOutput(capture.stdout) as RemoteFacts | null;
|
||||
return { capture, parsed };
|
||||
}
|
||||
@@ -1486,7 +1486,7 @@ function releaseUrl(pg: PostgresHostConfig): string {
|
||||
async function startRemoteApplyJob(config: UniDeskConfig, pg: PostgresHostConfig, localState: { inspection: SecretInspection }): Promise<RemoteJobStart> {
|
||||
const payload = remoteApplyPayload(pg, localState.inspection);
|
||||
const script = startRemoteApplyJobScript(pg, payload);
|
||||
const capture = await runSshCommandCapture(config, pg.node.route, ["script"], script);
|
||||
const capture = await runSshCommandCapture(config, pg.node.route, ["sh"], script);
|
||||
const parsed = parseJsonOutput(capture.stdout);
|
||||
return {
|
||||
ok: capture.exitCode === 0 && parsed !== null && parsed.ok === true,
|
||||
@@ -1938,7 +1938,7 @@ printf '\\n---LOG---\\n'
|
||||
if [ -f "$log" ]; then tail -80 "$log"; fi
|
||||
ROOT
|
||||
`;
|
||||
const capture = await runSshCommandCapture(config, pg.node.route, ["script"], script);
|
||||
const capture = await runSshCommandCapture(config, pg.node.route, ["sh"], script);
|
||||
const [stateText, logTail = ""] = capture.stdout.split("\n---LOG---\n");
|
||||
const parsed = parseJsonOutput(stateText) ?? {};
|
||||
const statusValue = typeof parsed.status === "string" ? parsed.status : null;
|
||||
|
||||
@@ -500,7 +500,7 @@ async function apply(config: UniDeskConfig, options: ApplyOptions): Promise<Reco
|
||||
};
|
||||
}
|
||||
if (options.dryRun) {
|
||||
const result = await capture(config, target.route, ["script"], dryRunManifestScript({ yaml, target, fieldManager, manifestName: serviceName }));
|
||||
const result = await capture(config, target.route, ["sh"], dryRunManifestScript({ yaml, target, fieldManager, manifestName: serviceName }));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
return {
|
||||
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
|
||||
@@ -522,7 +522,7 @@ async function apply(config: UniDeskConfig, options: ApplyOptions): Promise<Reco
|
||||
readTextFile,
|
||||
});
|
||||
const confirmedYaml = renderManifest(langbot, target, secretMaterial.fingerprint);
|
||||
const result = await capture(config, target.route, ["script"], applyScript(confirmedYaml, langbot, target, secretMaterial, frpcSecret));
|
||||
const result = await capture(config, target.route, ["sh"], applyScript(confirmedYaml, langbot, target, secretMaterial, frpcSecret));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
const caddy = await applyPk01CaddyBlock(config, serviceName, target.publicExposure);
|
||||
return {
|
||||
@@ -547,7 +547,7 @@ async function apply(config: UniDeskConfig, options: ApplyOptions): Promise<Reco
|
||||
async function status(config: UniDeskConfig, options: CommonOptions): Promise<Record<string, unknown>> {
|
||||
const langbot = readLangBotConfig();
|
||||
const target = resolveTarget(langbot, options.targetId);
|
||||
const result = await capture(config, target.route, ["script"], statusScript(langbot, target));
|
||||
const result = await capture(config, target.route, ["sh"], statusScript(langbot, target));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
return {
|
||||
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
|
||||
@@ -562,7 +562,7 @@ async function status(config: UniDeskConfig, options: CommonOptions): Promise<Re
|
||||
async function logs(config: UniDeskConfig, options: LogsOptions): Promise<Record<string, unknown>> {
|
||||
const langbot = readLangBotConfig();
|
||||
const target = resolveTarget(langbot, options.targetId);
|
||||
const result = await capture(config, target.route, ["script"], logsScript(target, options));
|
||||
const result = await capture(config, target.route, ["sh"], logsScript(target, options));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
return {
|
||||
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
|
||||
@@ -579,7 +579,7 @@ async function logs(config: UniDeskConfig, options: LogsOptions): Promise<Record
|
||||
async function validate(config: UniDeskConfig, options: CommonOptions): Promise<Record<string, unknown>> {
|
||||
const langbot = readLangBotConfig();
|
||||
const target = resolveTarget(langbot, options.targetId);
|
||||
const k8s = await capture(config, target.route, ["script"], validateScript(target));
|
||||
const k8s = await capture(config, target.route, ["sh"], validateScript(target));
|
||||
const k8sParsed = parseJsonOutput(k8s.stdout);
|
||||
const publicProbe = publicHttpProbe(target.publicExposure.publicBaseUrl, "/api/v1/system/info");
|
||||
return {
|
||||
|
||||
@@ -289,7 +289,7 @@ async function apply(config: UniDeskConfig, options: ApplyOptions): Promise<Reco
|
||||
};
|
||||
}
|
||||
if (options.dryRun) {
|
||||
const result = await capture(config, target.route, ["script"], dryRunManifestScript({ yaml, target, fieldManager, manifestName: serviceName }));
|
||||
const result = await capture(config, target.route, ["sh"], dryRunManifestScript({ yaml, target, fieldManager, manifestName: serviceName }));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
return {
|
||||
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
|
||||
@@ -311,7 +311,7 @@ async function apply(config: UniDeskConfig, options: ApplyOptions): Promise<Reco
|
||||
readTextFile,
|
||||
});
|
||||
const confirmedYaml = renderManifest(n8n, target, secretMaterial.fingerprint);
|
||||
const result = await capture(config, target.route, ["script"], applyScript(confirmedYaml, n8n, target, secretMaterial, frpcSecret));
|
||||
const result = await capture(config, target.route, ["sh"], applyScript(confirmedYaml, n8n, target, secretMaterial, frpcSecret));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
const caddy = await applyPk01CaddyBlock(config, serviceName, target.publicExposure);
|
||||
return {
|
||||
@@ -335,7 +335,7 @@ async function apply(config: UniDeskConfig, options: ApplyOptions): Promise<Reco
|
||||
async function status(config: UniDeskConfig, options: CommonOptions): Promise<Record<string, unknown>> {
|
||||
const n8n = readN8nConfig();
|
||||
const target = resolveTarget(n8n, options.targetId);
|
||||
const result = await capture(config, target.route, ["script"], statusScript(n8n, target));
|
||||
const result = await capture(config, target.route, ["sh"], statusScript(n8n, target));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
return {
|
||||
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
|
||||
@@ -350,7 +350,7 @@ async function status(config: UniDeskConfig, options: CommonOptions): Promise<Re
|
||||
async function logs(config: UniDeskConfig, options: LogsOptions): Promise<Record<string, unknown>> {
|
||||
const n8n = readN8nConfig();
|
||||
const target = resolveTarget(n8n, options.targetId);
|
||||
const result = await capture(config, target.route, ["script"], logsScript(target, options));
|
||||
const result = await capture(config, target.route, ["sh"], logsScript(target, options));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
return {
|
||||
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
|
||||
@@ -367,7 +367,7 @@ async function logs(config: UniDeskConfig, options: LogsOptions): Promise<Record
|
||||
async function validate(config: UniDeskConfig, options: CommonOptions): Promise<Record<string, unknown>> {
|
||||
const n8n = readN8nConfig();
|
||||
const target = resolveTarget(n8n, options.targetId);
|
||||
const k8s = await capture(config, target.route, ["script"], validateScript(target));
|
||||
const k8s = await capture(config, target.route, ["sh"], validateScript(target));
|
||||
const k8sParsed = parseJsonOutput(k8s.stdout);
|
||||
const publicProbe = publicHttpProbe(target.publicExposure.publicBaseUrl, "/");
|
||||
return {
|
||||
|
||||
@@ -655,7 +655,7 @@ print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||
sys.exit(0 if payload["ok"] else 1)
|
||||
PY
|
||||
`;
|
||||
const result = await capture(config, params.targetRoute, ["script"], script);
|
||||
const result = await capture(config, params.targetRoute, ["sh"], script);
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
return {
|
||||
ok: result.exitCode === 0 && parsed?.ok === true,
|
||||
|
||||
@@ -1058,7 +1058,7 @@ async function codexPoolValidate(config: UniDeskConfig, options: DisclosureOptio
|
||||
async function codexPoolTrace(config: UniDeskConfig, options: TraceOptions): Promise<Record<string, unknown> | RenderedCliResult> {
|
||||
const pool = readCodexPoolConfig();
|
||||
const runtimeTarget = codexPoolRuntimeTarget(options.targetId);
|
||||
const result = await capture(config, runtimeTarget.route, ["script"], traceScript(pool, options, runtimeTarget));
|
||||
const result = await capture(config, runtimeTarget.route, ["sh"], traceScript(pool, options, runtimeTarget));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
const ok = result.exitCode === 0 && boolField(parsed, "ok", false);
|
||||
if (options.raw) {
|
||||
@@ -1081,7 +1081,7 @@ async function codexPoolTrace(config: UniDeskConfig, options: TraceOptions): Pro
|
||||
async function codexPoolSentinelReport(config: UniDeskConfig, options: SentinelReportOptions): Promise<Record<string, unknown> | RenderedCliResult> {
|
||||
const pool = readCodexPoolConfig();
|
||||
const runtimeTarget = codexPoolRuntimeTarget(options.targetId);
|
||||
const result = await capture(config, runtimeTarget.route, ["script"], sentinelReportScript(pool, options.events, runtimeTarget));
|
||||
const result = await capture(config, runtimeTarget.route, ["sh"], sentinelReportScript(pool, options.events, runtimeTarget));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
const ok = result.exitCode === 0 && boolField(parsed, "ok", false);
|
||||
if (options.raw) {
|
||||
@@ -1177,7 +1177,7 @@ async function codexPoolCleanupProbes(config: UniDeskConfig, options: ConfirmOpt
|
||||
};
|
||||
}
|
||||
const pool = readCodexPoolConfig();
|
||||
const result = await capture(config, runtimeTarget.route, ["script"], cleanupProbesScript(pool, runtimeTarget));
|
||||
const result = await capture(config, runtimeTarget.route, ["sh"], cleanupProbesScript(pool, runtimeTarget));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
if (options.raw) {
|
||||
return {
|
||||
@@ -1221,7 +1221,7 @@ async function codexPoolExpose(config: UniDeskConfig, options: ConfirmOptions):
|
||||
}
|
||||
const secretMaterial = prepareTargetPublicExposureSecret(runtimeTarget);
|
||||
const caddyResult = await applyPk01CaddyBlock(config, serviceName, runtimeTarget.publicExposure);
|
||||
const remoteResult = await capture(config, runtimeTarget.route, ["script"], targetPublicExposureApplyScript(runtimeTarget, secretMaterial));
|
||||
const remoteResult = await capture(config, runtimeTarget.route, ["sh"], targetPublicExposureApplyScript(runtimeTarget, secretMaterial));
|
||||
const parsed = parseJsonOutput(remoteResult.stdout);
|
||||
const publicProbe = await probePublicModels(pool, "without-api-key", undefined, runtimeTarget);
|
||||
const ok = caddyResult.ok === true && remoteResult.exitCode === 0 && boolField(parsed, "ok", false) && publicProbe.httpStatus === 401;
|
||||
@@ -3420,7 +3420,7 @@ PY
|
||||
}
|
||||
|
||||
async function fetchPoolApiKey(config: UniDeskConfig, pool: CodexPoolConfig, target = codexPoolRuntimeTarget()): Promise<{ apiKey: string | null; error: string | null }> {
|
||||
const result = await capture(config, target.route, ["script"], `
|
||||
const result = await capture(config, target.route, ["sh"], `
|
||||
set -u
|
||||
kubectl -n ${target.namespace} get secret ${pool.apiKeySecretName} -o json
|
||||
`);
|
||||
@@ -7237,14 +7237,14 @@ type RemoteCodexPoolMode = "sync" | "validate" | "sentinel-probe" | "sentinel-im
|
||||
async function runRemoteCodexPoolScript(config: UniDeskConfig, mode: RemoteCodexPoolMode, script: string, target = codexPoolRuntimeTarget()): Promise<SshCaptureResult> {
|
||||
const jobName = `codex-pool-${mode}-${Date.now().toString(36)}`.slice(0, 63);
|
||||
const startedAtMs = Date.now();
|
||||
const start = await capture(config, target.route, ["script"], remoteJobStartScript(jobName, script));
|
||||
const start = await capture(config, target.route, ["sh"], remoteJobStartScript(jobName, script));
|
||||
const started = parseJsonOutput(start.stdout);
|
||||
if (start.exitCode !== 0 || boolField(started, "ok", false) !== true) return start;
|
||||
|
||||
let latest: RemoteCodexPoolJobStatus | null = null;
|
||||
while (Date.now() - startedAtMs <= remoteJobTimeoutMs) {
|
||||
await sleep(remoteJobPollMs);
|
||||
const probe = await capture(config, target.route, ["script"], remoteJobStatusScript(jobName));
|
||||
const probe = await capture(config, target.route, ["sh"], remoteJobStatusScript(jobName));
|
||||
const parsed = parseJsonOutput(probe.stdout);
|
||||
latest = normalizeRemoteJobStatus(parsed);
|
||||
process.stderr.write(`${JSON.stringify({
|
||||
|
||||
@@ -529,7 +529,7 @@ async function collectorImageBuild(config: UniDeskConfig, options: OpsApplyOptio
|
||||
source: collectorSourceSummary(archive),
|
||||
};
|
||||
}
|
||||
const result = await capture(config, archive.personalWechatIngress.target.hostRoute, ["script"], collectorImageBuildScript(archive));
|
||||
const result = await capture(config, archive.personalWechatIngress.target.hostRoute, ["sh"], collectorImageBuildScript(archive));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
return {
|
||||
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
|
||||
@@ -546,7 +546,7 @@ async function collectorImageBuild(config: UniDeskConfig, options: OpsApplyOptio
|
||||
async function collectorImageStatus(config: UniDeskConfig, options: OpsCommonOptions): Promise<Record<string, unknown>> {
|
||||
const archive = readConfig();
|
||||
assertTarget(archive, options.targetId);
|
||||
const result = await capture(config, archive.personalWechatIngress.target.hostRoute, ["script"], collectorImageStatusScript(archive));
|
||||
const result = await capture(config, archive.personalWechatIngress.target.hostRoute, ["sh"], collectorImageStatusScript(archive));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
return {
|
||||
ok: result.exitCode === 0 && parsed !== null && parsed.ok === true,
|
||||
@@ -580,7 +580,7 @@ async function collectorApply(config: UniDeskConfig, options: OpsApplyOptions):
|
||||
};
|
||||
}
|
||||
if (options.dryRun) {
|
||||
const result = await capture(config, archive.personalWechatIngress.target.kubeRoute, ["script"], collectorApplyScript(archive, manifest, null, true));
|
||||
const result = await capture(config, archive.personalWechatIngress.target.kubeRoute, ["sh"], collectorApplyScript(archive, manifest, null, true));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
return {
|
||||
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
|
||||
@@ -595,7 +595,7 @@ async function collectorApply(config: UniDeskConfig, options: OpsApplyOptions):
|
||||
};
|
||||
}
|
||||
const token = readArchiveCallbackToken(archive);
|
||||
const result = await capture(config, archive.personalWechatIngress.target.kubeRoute, ["script"], collectorApplyScript(archive, manifest, token.value, false));
|
||||
const result = await capture(config, archive.personalWechatIngress.target.kubeRoute, ["sh"], collectorApplyScript(archive, manifest, token.value, false));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
return {
|
||||
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
|
||||
@@ -624,7 +624,7 @@ async function collectorApply(config: UniDeskConfig, options: OpsApplyOptions):
|
||||
async function collectorStatus(config: UniDeskConfig, options: OpsCommonOptions): Promise<Record<string, unknown>> {
|
||||
const archive = readConfig();
|
||||
assertTarget(archive, options.targetId);
|
||||
const result = await capture(config, archive.personalWechatIngress.target.kubeRoute, ["script"], collectorStatusScript(archive, options.full));
|
||||
const result = await capture(config, archive.personalWechatIngress.target.kubeRoute, ["sh"], collectorStatusScript(archive, options.full));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
return {
|
||||
ok: result.exitCode === 0 && parsed !== null && parsed.ok === true,
|
||||
|
||||
@@ -1731,7 +1731,7 @@ async function apply(config: UniDeskConfig, options: ApplyOptions): Promise<Reco
|
||||
};
|
||||
}
|
||||
if (options.dryRun) {
|
||||
const result = await capture(config, target.route, ["script"], dryRunScript(yaml, target));
|
||||
const result = await capture(config, target.route, ["sh"], dryRunScript(yaml, target));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
return {
|
||||
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
|
||||
@@ -1749,7 +1749,7 @@ async function apply(config: UniDeskConfig, options: ApplyOptions): Promise<Reco
|
||||
const secretMaterial = target.databaseMode === "external-active" ? prepareExternalActiveSecret(sub2api) : null;
|
||||
const publicExposureSecretMaterial = preparePublicExposureSecret(sub2api, target);
|
||||
const egressProxySecretMaterial = prepareEgressProxySecret(sub2api, target);
|
||||
const result = await capture(config, target.route, ["script"], applyScript(sub2api, yaml, target, secretMaterial, publicExposureSecretMaterial, egressProxySecretMaterial));
|
||||
const result = await capture(config, target.route, ["sh"], applyScript(sub2api, yaml, target, secretMaterial, publicExposureSecretMaterial, egressProxySecretMaterial));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
const pk01Exposure = publicExposureSecretMaterial === null ? null : await applyPk01PublicExposure(config, target);
|
||||
return {
|
||||
@@ -1774,7 +1774,7 @@ async function apply(config: UniDeskConfig, options: ApplyOptions): Promise<Reco
|
||||
async function status(config: UniDeskConfig, options: DisclosureOptions): Promise<Record<string, unknown>> {
|
||||
const sub2api = readSub2ApiConfig();
|
||||
const target = resolveTarget(sub2api, options.targetId);
|
||||
const result = await capture(config, target.route, ["script"], statusScript(sub2api, target));
|
||||
const result = await capture(config, target.route, ["sh"], statusScript(sub2api, target));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
if (options.raw) {
|
||||
return {
|
||||
@@ -1810,7 +1810,7 @@ async function validate(config: UniDeskConfig, options: DisclosureOptions): Prom
|
||||
: target.databaseMode === "external-active"
|
||||
? validateExternalActiveScript(sub2api, target)
|
||||
: validateScript(sub2api, target);
|
||||
const result = await capture(config, target.route, ["script"], script);
|
||||
const result = await capture(config, target.route, ["sh"], script);
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
if (options.raw) {
|
||||
return {
|
||||
@@ -2333,7 +2333,7 @@ payload = {
|
||||
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||
PY
|
||||
`;
|
||||
const result = await capture(config, exposure.pk01.route, ["script"], script);
|
||||
const result = await capture(config, exposure.pk01.route, ["sh"], script);
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
return {
|
||||
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
|
||||
@@ -2403,7 +2403,7 @@ download_cache="$HOME/.cache/unidesk/platform-infra/caddy-linux-amd64"
|
||||
if [ -f "$download_part" ]; then stat -c 'part_bytes=%s part_mtime=%y' "$download_part"; fi
|
||||
if [ -f "$download_cache" ]; then stat -c 'cache_bytes=%s cache_mtime=%y' "$download_cache"; fi
|
||||
`;
|
||||
const result = await capture(config, exposure.pk01.route, ["script"], script);
|
||||
const result = await capture(config, exposure.pk01.route, ["sh"], script);
|
||||
const [stateText, rest = ""] = result.stdout.split("\n---STDOUT---\n");
|
||||
const [stdoutAndMaybeStderr = "", downloadProgress = ""] = rest.split("\n---DOWNLOAD---\n");
|
||||
const [stdoutTail = "", stderrTail = ""] = stdoutAndMaybeStderr.split("\n---STDERR---\n");
|
||||
|
||||
@@ -492,7 +492,7 @@ function selectedTargets(config: SecretDistributionConfig, options: SecretsOptio
|
||||
async function applyTargetSecrets(config: UniDeskConfig, target: DistributionTarget, secrets: DesiredSecret[], options: SecretsOptions): Promise<Record<string, unknown>> {
|
||||
if (secrets.length === 0) return { ok: true, target: targetSummary(target), mode: "skipped-no-secrets" };
|
||||
const yaml = renderSecretManifest(target, secrets);
|
||||
const result = await capture(config, target.route, ["script"], applySecretScript(target, secrets, yaml));
|
||||
const result = await capture(config, target.route, ["sh"], applySecretScript(target, secrets, yaml));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
return {
|
||||
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
|
||||
@@ -504,7 +504,7 @@ async function applyTargetSecrets(config: UniDeskConfig, target: DistributionTar
|
||||
}
|
||||
|
||||
async function statusTargetSecrets(config: UniDeskConfig, target: DistributionTarget, secrets: DesiredSecret[], options: SecretsOptions): Promise<Record<string, unknown>> {
|
||||
const result = await capture(config, target.route, ["script"], statusSecretScript(target, secrets));
|
||||
const result = await capture(config, target.route, ["sh"], statusSecretScript(target, secrets));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
return {
|
||||
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
|
||||
|
||||
+72
-103
@@ -187,6 +187,9 @@ const legacyK3sOperationRouteSegments = new Set([
|
||||
"kubectl",
|
||||
"exec",
|
||||
"script",
|
||||
"shell",
|
||||
"sh",
|
||||
"bash",
|
||||
"apply-patch",
|
||||
"apply-patch-v1",
|
||||
"patch",
|
||||
@@ -945,11 +948,11 @@ export function parseSshArgs(args: string[]): ParsedSshArgs {
|
||||
if (subcommand === "playwright") {
|
||||
return { remoteCommand: null, requiresStdin: true, invocationKind: "helper" };
|
||||
}
|
||||
if (subcommand === "script" || subcommand === "sh") {
|
||||
return buildShellCommand(args.slice(1));
|
||||
if (subcommand === "script" || subcommand === "shell") {
|
||||
throw removedShellAliasError(subcommand);
|
||||
}
|
||||
if (subcommand === "shell") {
|
||||
return buildShellStringCommand(args.slice(1));
|
||||
if (subcommand === "sh" || subcommand === "bash") {
|
||||
return buildShellCommand(args.slice(1), subcommand, `ssh ${subcommand}`);
|
||||
}
|
||||
if (subcommand === "argv" || subcommand === "exec") {
|
||||
const toolArgs = args.slice(1);
|
||||
@@ -1003,7 +1006,7 @@ export function sshRouteSeparatorCompatibilityHint(rawArgs: string[], normalized
|
||||
if (rawArgs === normalizedArgs || rawArgs[0] !== "--") return "";
|
||||
const operation = normalizedArgs[0] ?? "";
|
||||
const operationText = operation.length === 0 ? "operation" : `operation \`${operation}\``;
|
||||
return `UNIDESK_SSH_HINT route-level -- is ignored before ${operationText}; canonical form is \`trans <route> ${normalizedArgs.join(" ")}\`. Keep -- only inside operations such as \`script -- <command>\` or \`exec -- <command>\`.\n`;
|
||||
return `UNIDESK_SSH_HINT route-level -- is ignored before ${operationText}; canonical form is \`trans <route> ${normalizedArgs.join(" ")}\`. Keep -- only inside operations such as \`sh -- <command>\`, \`bash -- <command>\`, or \`exec -- <command>\`.\n`;
|
||||
}
|
||||
|
||||
function validateDirectArgvCommand(commandName: string, toolArgs: string[]): void {
|
||||
@@ -1012,7 +1015,7 @@ function validateDirectArgvCommand(commandName: string, toolArgs: string[]): voi
|
||||
if (!looksLikeShellCommandString(command)) return;
|
||||
throw new Error(
|
||||
`ssh ${commandName} received one shell-like command string; ${commandName} executes a single process and treats that string as the executable path. ` +
|
||||
`Use \`trans <route> script -- ${shellQuote(command)}\` for one-line shell logic, or split argv tokens, for example \`trans <route> ${commandName} ls -la\`.`
|
||||
`Use \`trans <route> sh -- ${shellQuote(command)}\` or \`trans <route> bash -- ${shellQuote(command)}\` for one-line shell logic, or split argv tokens, for example \`trans <route> ${commandName} ls -la\`.`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1533,6 +1536,9 @@ function k3sOperationInRouteMessage(target: string, operation: string): string {
|
||||
if (operation === "v2" || operation === "patch" || operation === "patch-v1") {
|
||||
return `ssh k3s route must locate a target only; remote patch entrypoints are "apply-patch" for the default v2 engine and "apply-patch-v1" for the legacy helper instead of "${target}"`;
|
||||
}
|
||||
if (operation === "script" || operation === "shell") {
|
||||
return `ssh k3s route must locate a target only; operation "${operation}" has been removed because it hides the shell dialect; use explicit "sh" or "bash" after the route instead of "${target}"`;
|
||||
}
|
||||
const operationExample = operation === "guard" ? "guard" : `${operation} ...`;
|
||||
return `ssh k3s route must locate a target only; put operation "${operation}" after the route, for example "ssh ${providerId}:k3s ${operationExample}" or "ssh ${providerId}:k3s:<namespace>:<workload> ${operationExample}" instead of "${target}"`;
|
||||
}
|
||||
@@ -1662,12 +1668,11 @@ function parseK3sControlPlaneOperation(route: ParsedSshRoute, args: string[]): P
|
||||
if (operation === "patch" || operation === "patch-v1" || operation === "v2") {
|
||||
throw new Error("remote patch entrypoints are `apply-patch` for the default v2 engine and `apply-patch-v1` for the legacy helper");
|
||||
}
|
||||
if (operation === "script" || operation === "sh") {
|
||||
return buildK3sScriptOperation(args.slice(1));
|
||||
if (operation === "script" || operation === "shell") {
|
||||
throw removedShellAliasError(operation);
|
||||
}
|
||||
if (operation === "shell") {
|
||||
const parsed = parseShellStringOperationArgs(args.slice(1), `ssh ${route.providerId}:k3s shell`);
|
||||
return { remoteCommand: shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, parsed.shell, "-c", shellScriptWithCompatibility(parsed.command)]), requiresStdin: false, invocationKind: "helper" };
|
||||
if (operation === "sh" || operation === "bash") {
|
||||
return buildK3sScriptOperation(args.slice(1), operation, `ssh ${route.providerId}:k3s ${operation}`);
|
||||
}
|
||||
if (operation === "guard") {
|
||||
if (args.length > 1) throw new Error(`ssh ${route.providerId}:k3s guard does not accept extra arguments`);
|
||||
@@ -1700,10 +1705,11 @@ function parseK3sTargetOperation(route: ParsedSshRoute, args: string[]): ParsedS
|
||||
if (operation === "patch" || operation === "patch-v1" || operation === "v2") {
|
||||
throw new Error("remote patch entrypoints are `apply-patch` for the default v2 engine and `apply-patch-v1` for the legacy helper");
|
||||
}
|
||||
if (operation === "script") return buildK3sScriptOperation([...targetArgs, ...operationArgs]);
|
||||
if (operation === "shell") {
|
||||
const parsed = parseShellStringOperationArgs(operationArgs, `ssh ${route.raw} shell`);
|
||||
return { remoteCommand: buildK3sExecCommand([...targetArgs, "--", parsed.shell, "-c", shellScriptWithCompatibility(parsed.command)]), requiresStdin: false, invocationKind: "helper" };
|
||||
if (operation === "script" || operation === "shell") {
|
||||
throw removedShellAliasError(operation);
|
||||
}
|
||||
if (operation === "sh" || operation === "bash") {
|
||||
return buildK3sScriptOperation([...targetArgs, ...operationArgs], operation, `ssh ${route.raw} ${operation}`);
|
||||
}
|
||||
if (operation === "logs") return { remoteCommand: buildK3sLogsCommand([...targetArgs, ...operationArgs]), requiresStdin: false, invocationKind: "helper" };
|
||||
if (operation === "argv") {
|
||||
@@ -1832,9 +1838,12 @@ function buildK3sCommand(providerId: string, args: string[]): string {
|
||||
}
|
||||
if (action === "guard") return buildK3sGuardCommand(providerId);
|
||||
if (action === "exec") return buildK3sExecCommand(args.slice(1));
|
||||
if (action === "script") {
|
||||
const parsed = buildK3sScriptOperation(args.slice(1));
|
||||
if (parsed.remoteCommand === null) throw new Error("ssh k3s script resolved to an interactive command unexpectedly");
|
||||
if (action === "script" || action === "shell") {
|
||||
throw removedShellAliasError(action);
|
||||
}
|
||||
if (action === "sh" || action === "bash") {
|
||||
const parsed = buildK3sScriptOperation(args.slice(1), action, `ssh k3s ${action}`);
|
||||
if (parsed.remoteCommand === null) throw new Error(`ssh k3s ${action} resolved to an interactive command unexpectedly`);
|
||||
return parsed.remoteCommand;
|
||||
}
|
||||
if (action === "logs") return buildK3sLogsCommand(args.slice(1));
|
||||
@@ -1911,21 +1920,24 @@ function buildK3sExecCommand(args: string[]): string {
|
||||
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, "kubectl", ...kubectlArgs]);
|
||||
}
|
||||
|
||||
function buildK3sScriptOperation(args: string[]): ParsedSshArgs {
|
||||
const parsed = parseK3sTargetOptions(args, "ssh k3s script", { requireCommand: false, allowCommand: true, allowShell: true });
|
||||
if (parsed.shell === null && parsed.command.length > 0) {
|
||||
return { remoteCommand: buildK3sInlineScriptCommand(parsed), requiresStdin: false, invocationKind: "helper" };
|
||||
}
|
||||
return { remoteCommand: buildK3sStdinScriptCommand(parsed), requiresStdin: true, invocationKind: "helper", stdinPrefix: shellScriptStdinPrefix() };
|
||||
function removedShellAliasError(operation: string): Error {
|
||||
return new Error(`ssh ${operation} operation has been removed because it hides the shell dialect; use explicit \`sh\` for POSIX /bin/sh or \`bash\` for Bash syntax`);
|
||||
}
|
||||
|
||||
function buildK3sStdinScriptCommand(parsed: K3sTargetOptions): string {
|
||||
if (parsed.namespace === null && parsed.resource === null) return buildK3sHostScriptCommand(parsed);
|
||||
if (parsed.namespace === null) throw new Error("ssh k3s script target requires --namespace <name>");
|
||||
if (parsed.selector !== null) throw new Error("ssh k3s script does not support --selector");
|
||||
if (parsed.resource === null) throw new Error("ssh k3s script target requires --deployment <name>, --pod <name> or --resource <type/name>");
|
||||
if (parsed.tty) throw new Error("ssh k3s script does not support --tty; stdin is reserved for the script body");
|
||||
const shell = parsed.shell ?? "sh";
|
||||
function buildK3sScriptOperation(args: string[], shell: "sh" | "bash", commandName: string): ParsedSshArgs {
|
||||
const parsed = parseK3sTargetOptions(args, commandName, { requireCommand: false, allowCommand: true });
|
||||
if (parsed.command.length > 0) {
|
||||
return { remoteCommand: buildK3sInlineScriptCommand(parsed, shell, commandName), requiresStdin: false, invocationKind: "helper" };
|
||||
}
|
||||
return { remoteCommand: buildK3sStdinScriptCommand(parsed, shell, commandName), requiresStdin: true, invocationKind: "helper", stdinPrefix: shellScriptStdinPrefix() };
|
||||
}
|
||||
|
||||
function buildK3sStdinScriptCommand(parsed: K3sTargetOptions, shell: "sh" | "bash", commandName: string): string {
|
||||
if (parsed.namespace === null && parsed.resource === null) return buildK3sHostScriptCommand(parsed, shell, commandName);
|
||||
if (parsed.namespace === null) throw new Error(`${commandName} target requires --namespace <name>`);
|
||||
if (parsed.selector !== null) throw new Error(`${commandName} does not support --selector`);
|
||||
if (parsed.resource === null) throw new Error(`${commandName} target requires --deployment <name>, --pod <name> or --resource <type/name>`);
|
||||
if (parsed.tty) throw new Error(`${commandName} does not support --tty; stdin is reserved for the shell body`);
|
||||
const kubectlArgs = [
|
||||
"exec",
|
||||
"-i",
|
||||
@@ -1939,20 +1951,21 @@ function buildK3sStdinScriptCommand(parsed: K3sTargetOptions): string {
|
||||
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, "kubectl", ...kubectlArgs]);
|
||||
}
|
||||
|
||||
function buildK3sInlineScriptCommand(parsed: K3sTargetOptions): string {
|
||||
if (parsed.command.length === 0) throw new Error("ssh k3s script -- requires a command");
|
||||
if (parsed.selector !== null) throw new Error("ssh k3s script -- does not support --selector");
|
||||
if (parsed.tty) throw new Error("ssh k3s script does not support --tty; stdin is reserved for the script body");
|
||||
if (parsed.stdin) throw new Error("ssh k3s script -- does not accept --stdin");
|
||||
const command = parsed.command.length === 1 ? ["sh", "-c", shellScriptWithCompatibility(parsed.command[0] ?? "")] : parsed.command;
|
||||
function buildK3sInlineScriptCommand(parsed: K3sTargetOptions, shell: "sh" | "bash", commandName: string): string {
|
||||
if (parsed.command.length === 0) throw new Error(`${commandName} -- requires a command`);
|
||||
if (parsed.command.length !== 1) throw new Error(`${commandName} -- requires exactly one shell command string; use exec/argv for direct argv commands`);
|
||||
if (parsed.selector !== null) throw new Error(`${commandName} -- does not support --selector`);
|
||||
if (parsed.tty) throw new Error(`${commandName} does not support --tty; stdin is reserved for the shell body`);
|
||||
if (parsed.stdin) throw new Error(`${commandName} -- does not accept --stdin`);
|
||||
const command = [shell, "-c", shellScriptWithCompatibility(parsed.command[0] ?? "")];
|
||||
if (parsed.namespace === null && parsed.resource === null) {
|
||||
if (parsed.container !== null) throw new Error("ssh k3s script without a workload does not accept --container");
|
||||
if (parsed.workspace !== null) throw new Error("ssh k3s script without a workload does not accept --workdir");
|
||||
if (parsed.kubectlOptions.length > 0) throw new Error("ssh k3s script without a workload does not accept kubectl log options");
|
||||
if (parsed.container !== null) throw new Error(`${commandName} without a workload does not accept --container`);
|
||||
if (parsed.workspace !== null) throw new Error(`${commandName} without a workload does not accept --workdir`);
|
||||
if (parsed.kubectlOptions.length > 0) throw new Error(`${commandName} without a workload does not accept kubectl log options`);
|
||||
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, ...command]);
|
||||
}
|
||||
if (parsed.namespace === null) throw new Error("ssh k3s script target requires --namespace <name>");
|
||||
if (parsed.resource === null) throw new Error("ssh k3s script target requires --deployment <name>, --pod <name> or --resource <type/name>");
|
||||
if (parsed.namespace === null) throw new Error(`${commandName} target requires --namespace <name>`);
|
||||
if (parsed.resource === null) throw new Error(`${commandName} target requires --deployment <name>, --pod <name> or --resource <type/name>`);
|
||||
const kubectlArgs = [
|
||||
"exec",
|
||||
"-n", parsed.namespace,
|
||||
@@ -1992,36 +2005,15 @@ function buildK3sApplyPatchCommand(args: string[]): ParsedSshArgs {
|
||||
};
|
||||
}
|
||||
|
||||
function buildK3sHostScriptCommand(parsed: K3sTargetOptions): string {
|
||||
if (parsed.tty) throw new Error("ssh k3s script does not support --tty; stdin is reserved for the script body");
|
||||
if (parsed.stdin) throw new Error("ssh k3s script does not accept --stdin; stdin is always the script body");
|
||||
if (parsed.container !== null) throw new Error("ssh k3s script without a workload does not accept --container");
|
||||
if (parsed.workspace !== null) throw new Error("ssh k3s script without a workload does not accept --workdir");
|
||||
if (parsed.kubectlOptions.length > 0) throw new Error("ssh k3s script without a workload does not accept kubectl log options");
|
||||
const shell = parsed.shell ?? "sh";
|
||||
function buildK3sHostScriptCommand(parsed: K3sTargetOptions, shell: "sh" | "bash", commandName: string): string {
|
||||
if (parsed.tty) throw new Error(`${commandName} does not support --tty; stdin is reserved for the shell body`);
|
||||
if (parsed.stdin) throw new Error(`${commandName} does not accept --stdin; stdin is always the shell body`);
|
||||
if (parsed.container !== null) throw new Error(`${commandName} without a workload does not accept --container`);
|
||||
if (parsed.workspace !== null) throw new Error(`${commandName} without a workload does not accept --workdir`);
|
||||
if (parsed.kubectlOptions.length > 0) throw new Error(`${commandName} without a workload does not accept kubectl log options`);
|
||||
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, shell, "-s", "--", ...parsed.command]);
|
||||
}
|
||||
|
||||
function shellStringFromArgs(args: string[], commandName = "ssh shell"): string {
|
||||
if (args.length === 0) throw new Error(`${commandName} requires a command string`);
|
||||
return args.join(" ");
|
||||
}
|
||||
|
||||
function parseShellStringOperationArgs(args: string[], commandName: string): { shell: string; command: string } {
|
||||
let shell = "sh";
|
||||
const commandArgs: string[] = [];
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index] ?? "";
|
||||
if (arg === "--shell") {
|
||||
shell = k3sScriptShell(k3sOptionValue(args, index, `${commandName} --shell`), `${commandName} --shell`);
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
commandArgs.push(arg);
|
||||
}
|
||||
return { shell, command: shellStringFromArgs(commandArgs, commandName) };
|
||||
}
|
||||
|
||||
function buildK3sLogsCommand(args: string[]): string {
|
||||
const parsed = parseK3sTargetOptions(args, "ssh k3s logs", { requireCommand: false, allowSelector: true });
|
||||
if (parsed.namespace === null) throw new Error("ssh k3s logs requires --namespace <name>");
|
||||
@@ -2264,49 +2256,26 @@ function shellScriptStdinPrefix(): string {
|
||||
return `${shellScriptPrelude()}\n`;
|
||||
}
|
||||
|
||||
function buildShellCommand(args: string[]): ParsedSshArgs {
|
||||
let shell = "sh";
|
||||
function buildShellCommand(args: string[], shell: "sh" | "bash", commandName: string): ParsedSshArgs {
|
||||
const scriptArgs: string[] = [];
|
||||
let afterDoubleDash = false;
|
||||
let directArgvMode = false;
|
||||
let shellOptionSeen = false;
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index] ?? "";
|
||||
if (afterDoubleDash) {
|
||||
scriptArgs.push(arg);
|
||||
continue;
|
||||
}
|
||||
if (arg === "--") {
|
||||
if (!shellOptionSeen && scriptArgs.length === 0) {
|
||||
directArgvMode = true;
|
||||
scriptArgs.push(...args.slice(index + 1));
|
||||
break;
|
||||
if (scriptArgs.length === 0) {
|
||||
const command = args.slice(index + 1);
|
||||
if (command.length === 0) throw new Error(`${commandName} -- requires a command`);
|
||||
if (command.length !== 1) throw new Error(`${commandName} -- requires exactly one shell command string; use argv or a direct subcommand for direct argv commands`);
|
||||
return { remoteCommand: shellArgv([shell, "-c", shellScriptWithCompatibility(command[0] ?? "")]), requiresStdin: false, invocationKind: "helper" };
|
||||
}
|
||||
afterDoubleDash = true;
|
||||
continue;
|
||||
scriptArgs.push(...args.slice(index + 1));
|
||||
break;
|
||||
}
|
||||
if (arg === "--shell") {
|
||||
shell = k3sScriptShell(k3sOptionValue(args, index, "ssh script --shell"), "ssh script --shell");
|
||||
shellOptionSeen = true;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg.startsWith("-")) throw new Error(`unsupported ssh script option: ${arg}`);
|
||||
if (arg.startsWith("-")) throw new Error(`unsupported ${commandName} option: ${arg}`);
|
||||
scriptArgs.push(arg);
|
||||
}
|
||||
if (directArgvMode) {
|
||||
if (scriptArgs.length === 0) throw new Error("ssh script -- requires a command");
|
||||
if (scriptArgs.length === 1) return { remoteCommand: shellArgv([shell, "-c", shellScriptWithCompatibility(scriptArgs[0] ?? "")]), requiresStdin: false, invocationKind: "helper" };
|
||||
return { remoteCommand: shellArgv(scriptArgs), requiresStdin: false, invocationKind: "argv" };
|
||||
}
|
||||
return { remoteCommand: shellArgv([shell, "-s", "--", ...scriptArgs]), requiresStdin: true, invocationKind: "helper", stdinPrefix: shellScriptStdinPrefix() };
|
||||
}
|
||||
|
||||
function buildShellStringCommand(args: string[]): ParsedSshArgs {
|
||||
const parsed = parseShellStringOperationArgs(args, "ssh shell");
|
||||
return { remoteCommand: shellArgv([parsed.shell, "-c", shellScriptWithCompatibility(parsed.command)]), requiresStdin: false, invocationKind: "helper" };
|
||||
}
|
||||
|
||||
function podApplyPatchStdinWrapper(): { prefix: string; suffix: string } {
|
||||
const toolMarker = "__UNIDESK_APPLY_PATCH_TOOL__";
|
||||
const patchMarker = "__UNIDESK_APPLY_PATCH_PAYLOAD__";
|
||||
@@ -2399,8 +2368,8 @@ export function sshFailureHint(providerId: string, parsed: ParsedSshArgs, exitCo
|
||||
providerId: shownProviderId,
|
||||
trigger,
|
||||
exitCode,
|
||||
message: "ssh-like remote command failed before proving Host SSH is globally unavailable; prefer structured argv or stdin script passthrough for non-interactive commands.",
|
||||
try: `trans ${shownProviderId} script <<'SCRIPT'`,
|
||||
message: "ssh-like remote command failed before proving Host SSH is globally unavailable; prefer structured argv or explicit sh/bash stdin passthrough for non-interactive commands.",
|
||||
try: `trans ${shownProviderId} sh <<'SH'`,
|
||||
triage: `bun scripts/cli.ts provider triage ${shownProviderId} --observed-scope ssh --observed-error '<ssh-like timeout or kex failure>'`,
|
||||
note: "This hint intentionally does not echo the original remote command.",
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user