better-dsh 0.3.1-a → 0.3.1-d
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/BrowserWebSocketTransport.js +4 -0
- package/lib/{BrowserWebSocketTransport-wm99h94r.js → BrowserWebSocketTransport2.js} +1 -1
- package/lib/LaunchOptions.js +4 -0
- package/lib/{LaunchOptions-iE9M-STS.js → LaunchOptions2.js} +1 -1
- package/lib/{NodeWebSocketTransport-D9ysYGLM.js → NodeWebSocketTransport.js} +2 -2
- package/lib/NodeWebSocketTransport2.js +4 -0
- package/lib/{bidi-CaQeBZHY.js → bidi.js} +4 -4
- package/lib/compaction/index.d.ts +1 -1
- package/lib/compaction/index.js +1 -1
- package/lib/failover/index.d.ts +1 -1
- package/lib/failover/index.js +1 -1
- package/lib/fs-aware/sandbox-plugin.js +1 -1
- package/lib/{index-browser-CJzxT9Vi.js → index-browser.js} +6 -6
- package/lib/index.d.ts +1 -1
- package/lib/index.js +4 -4
- package/lib/kernel-env.js +1 -1
- package/lib/{lsp-server-registry-BcdkHROD.js → lsp-server-registry2.js} +1 -1
- package/lib/{main-taSMz1KX.js → main2.js} +1 -1
- package/lib/mobile/plugin.d.ts +2 -2
- package/lib/mobile/plugin.js +2 -2
- package/lib/{puppeteer-core-C-4NyOpY.js → puppeteer-core.js} +8 -8
- package/lib/py-sdk.js +1 -1
- package/lib/remote/helper-bin/MANIFEST.json +35 -0
- package/lib/remote/helper-bin/aarch64-apple-darwin/BUILT-VER +1 -0
- package/lib/remote/helper-bin/aarch64-apple-darwin/superd-run +0 -0
- package/lib/remote/helper-bin/aarch64-unknown-linux-musl/BUILT-VER +1 -0
- package/lib/remote/helper-bin/aarch64-unknown-linux-musl/superd-run +0 -0
- package/lib/remote/helper-bin/x86_64-pc-windows-gnu/BUILT-VER +1 -0
- package/lib/remote/helper-bin/x86_64-pc-windows-gnu/superd-run.exe +0 -0
- package/lib/remote/helper-bin/x86_64-unknown-linux-musl/BUILT-VER +1 -0
- package/lib/remote/helper-bin/x86_64-unknown-linux-musl/superd-run +0 -0
- package/lib/remote/plugin.d.ts +51 -4
- package/lib/remote/plugin.js +590 -105
- package/lib/url-schemes/index.d.ts +11 -11
- package/lib/url-schemes/index.js +11 -11
- package/lib/{wasm_bg-CNXhHjVg.js → wasm_bg.js} +1 -1
- package/lib/web-password.d.ts +7 -7
- package/lib/web-password.js +1 -1
- package/lib/web-trust.d.ts +1 -1
- package/lib/web-trust.js +2 -2
- package/lib/{web-trust-C_Y6o8Z2.d.ts → web-trust2.d.ts} +11 -11
- package/lib/{web-trust-D6gJpUMf.js → web-trust2.js} +1 -1
- package/lib/welcome-notice.d.ts +3 -3
- package/lib/welcome-notice.js +1 -1
- package/package.json +2 -2
- package/lib/BrowserWebSocketTransport-D1aFReu_.js +0 -4
- package/lib/LaunchOptions-0Mr6meJc.js +0 -4
- package/lib/NodeWebSocketTransport-BRLjmu2R.js +0 -4
- /package/lib/{chunk-DQxbwPpZ.js → chunk.js} +0 -0
- /package/lib/{index-DTDXUe6m.d.ts → index2.d.ts} +0 -0
- /package/lib/{kernel-env-x_WkG6Fr.js → kernel-env2.js} +0 -0
- /package/lib/{lib-C8_jXvwA.js → lib.js} +0 -0
- /package/lib/{lsp-server-registry-D4VLs1Sm.js → lsp-server-registry.js} +0 -0
- /package/lib/{main-uqGqYYnA.js → main.js} +0 -0
- /package/lib/{native-capture-BEByDXYM.js → native-capture.js} +0 -0
- /package/lib/{py-sdk-D7klhpWb.js → py-sdk2.js} +0 -0
- /package/lib/{util-BHL_dfV_.js → util.js} +0 -0
- /package/lib/{web-tree-sitter-CPrPD4J2.js → web-tree-sitter.js} +0 -0
- /package/lib/{wrap-0VA0uFx8.js → wrap.js} +0 -0
package/lib/remote/plugin.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { t as Schema } from "../lib
|
|
1
|
+
import { t as Schema } from "../lib.js";
|
|
2
2
|
import { Context } from "@deepseek-ai/cordis";
|
|
3
3
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
4
|
-
import { constants, createReadStream, createWriteStream, mkdirSync, statSync } from "node:fs";
|
|
4
|
+
import { constants, createReadStream, createWriteStream, existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
|
|
5
5
|
import { dirname, isAbsolute, join, posix, resolve } from "node:path";
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
7
8
|
import { homedir, tmpdir } from "node:os";
|
|
8
9
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
9
|
-
import { chmod, lstat, mkdir, mkdtemp, open, readFile, readdir, readlink, rename, rm, symlink, writeFile } from "node:fs/promises";
|
|
10
|
+
import { chmod, lstat, mkdir, mkdtemp, open, readFile, readdir, readlink, rename, rm, stat, symlink, writeFile } from "node:fs/promises";
|
|
10
11
|
import { pipeline } from "node:stream/promises";
|
|
11
12
|
import { Readable } from "node:stream";
|
|
12
13
|
import { createConnection, createServer } from "node:net";
|
|
@@ -317,17 +318,18 @@ async function copyTree(request, source, destination, signal, scratchRoot = join
|
|
|
317
318
|
signal.throwIfAborted();
|
|
318
319
|
if (request.argv !== void 0 || request.pullArgv !== void 0 || request.pushArgv !== void 0) throw new Error("copy: argv/pullArgv/pushArgv are native CLI options; the Hub/local staged lane does not accept CLI options");
|
|
319
320
|
await mkdir(scratchRoot, { recursive: true });
|
|
320
|
-
signal.throwIfAborted();
|
|
321
|
-
const scratch = await mkdtemp(join(scratchRoot, "remote-copy-")), payload = join(scratch, "payload.bin");
|
|
322
321
|
const receipt = {
|
|
323
322
|
action: "copy",
|
|
323
|
+
route: "hub-staged",
|
|
324
324
|
src: request.src,
|
|
325
325
|
dst: request.dst,
|
|
326
|
-
|
|
326
|
+
ok: false,
|
|
327
|
+
verification: "not-applicable",
|
|
327
328
|
bytes: 0,
|
|
328
329
|
entries: 0,
|
|
329
330
|
scope: "transferred entries"
|
|
330
331
|
};
|
|
332
|
+
const scratch = await mkdtemp(join(scratchRoot, "remote-copy-")), payload = join(scratch, "payload.bin");
|
|
331
333
|
try {
|
|
332
334
|
await pipeline(await source.endpoint.exportTree(source.path, signal), createWriteStream(payload, {
|
|
333
335
|
flags: "wx",
|
|
@@ -351,24 +353,79 @@ async function copyTree(request, source, destination, signal, scratchRoot = join
|
|
|
351
353
|
const dest = await destination.endpoint.digest(destination.path, signal, selection);
|
|
352
354
|
receipt.destinationDigest = dest.sha256;
|
|
353
355
|
if (dest.sha256 !== receipt.sourceDigest) throw new Error("destination digest mismatch");
|
|
354
|
-
receipt.
|
|
356
|
+
receipt.verification = "sha256";
|
|
355
357
|
}
|
|
356
358
|
signal.throwIfAborted();
|
|
357
359
|
await rm(scratch, {
|
|
358
360
|
recursive: true,
|
|
359
361
|
force: true
|
|
360
362
|
});
|
|
363
|
+
receipt.ok = true;
|
|
361
364
|
return receipt;
|
|
362
365
|
} catch (error) {
|
|
366
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
367
|
+
const reason = signal.aborted ? "aborted" : /mismatch/i.test(message) ? "digest-mismatch" : "staging-error";
|
|
363
368
|
throw new CopyError({
|
|
364
369
|
...receipt,
|
|
365
|
-
|
|
370
|
+
ok: false,
|
|
371
|
+
reason,
|
|
366
372
|
scratch,
|
|
367
|
-
error:
|
|
373
|
+
error: message
|
|
368
374
|
});
|
|
369
375
|
}
|
|
370
376
|
}
|
|
371
377
|
|
|
378
|
+
//#endregion
|
|
379
|
+
//#region src/remote/ssh-argv.ts
|
|
380
|
+
/**
|
|
381
|
+
* ONE identity-assembly policy for every SSH-speaking lane (user close-out
|
|
382
|
+
* ruling 2026-10-17: "four builders necessarily mean four identity
|
|
383
|
+
* semantics" — the structural root of F1).
|
|
384
|
+
*
|
|
385
|
+
* Assembly: explicit connection fields (byossh registrations; caller options
|
|
386
|
+
* may override later by the CLI's own last-option-wins) in ONE order —
|
|
387
|
+
* user, port, key, proxyJump — spoken in the target CLI's dialect.
|
|
388
|
+
*
|
|
389
|
+
* Resolution: config-side identity (aliases, config-only IdentityFile paths)
|
|
390
|
+
* is read from the ORIGIN's own ssh config natively by every lane. The single
|
|
391
|
+
* exception is the scp -3 temp config (ssh -F sees no user config), where
|
|
392
|
+
* tempSshConfig inlines the `ssh -G`-resolved fields — paths only, never key
|
|
393
|
+
* material (F1, 2026-10-14). That policy lives beside this builder in
|
|
394
|
+
* native-copy.ts and must stay the only such inline site.
|
|
395
|
+
*
|
|
396
|
+
* Former duplicate builders now reduced here: sshArgv (execution/probes),
|
|
397
|
+
* identityArgv (scp routes), rsyncRsh (RSYNC_RSH wiring), relaySsh
|
|
398
|
+
* (dual-SSH relay). Behavior is byte-identical to each former site.
|
|
399
|
+
*/
|
|
400
|
+
/** Identity options ONLY for one connection, in the target CLI's dialect:
|
|
401
|
+
* user, port, key, proxyJump — conditional, one order, NO base flags (each
|
|
402
|
+
* lane's dialect base — BatchMode, -T, hardening — stays at its call site).
|
|
403
|
+
* scp carries the user inside its user@host operand, so the scp dialect
|
|
404
|
+
* never emits -l; its port flag is -P. */
|
|
405
|
+
function sshIdentityArgv(connection, dialect) {
|
|
406
|
+
const args = [];
|
|
407
|
+
if (dialect === "ssh" && connection.user !== void 0) args.push("-l", connection.user);
|
|
408
|
+
if (connection.port !== void 0) args.push(dialect === "scp" ? "-P" : "-p", String(connection.port));
|
|
409
|
+
if (connection.key !== void 0) args.push("-i", connection.key);
|
|
410
|
+
if (connection.proxyJump !== void 0) args.push("-J", connection.proxyJump);
|
|
411
|
+
return args;
|
|
412
|
+
}
|
|
413
|
+
/** Full ssh invocation around the shared identity core. base and extra are
|
|
414
|
+
* trusted internal argv (dialect flags incl. BatchMode, forwarding options)
|
|
415
|
+
* — never a model-provided shell string. */
|
|
416
|
+
function sshCommandArgv(connection, base, extra, command) {
|
|
417
|
+
const args = [
|
|
418
|
+
"ssh",
|
|
419
|
+
...base,
|
|
420
|
+
...sshIdentityArgv(connection, "ssh"),
|
|
421
|
+
...extra,
|
|
422
|
+
"--",
|
|
423
|
+
connection.host
|
|
424
|
+
];
|
|
425
|
+
if (command !== void 0) args.push(command);
|
|
426
|
+
return args;
|
|
427
|
+
}
|
|
428
|
+
|
|
372
429
|
//#endregion
|
|
373
430
|
//#region src/remote/output-ring.ts
|
|
374
431
|
const continuation = (byte) => (byte & 192) === 128;
|
|
@@ -674,15 +731,17 @@ function scpSpec(connection, path$1) {
|
|
|
674
731
|
const host = connection.host.includes(":") ? `[${connection.host}]` : connection.host;
|
|
675
732
|
return `${connection.user !== void 0 ? `${connection.user}@${host}` : host}:${path$1}`;
|
|
676
733
|
}
|
|
677
|
-
/** Connection identity as scp options
|
|
678
|
-
*
|
|
679
|
-
* precedence (last option wins).
|
|
734
|
+
/** Connection identity as scp options, placed BEFORE caller argv so an
|
|
735
|
+
* explicit caller -F/-i/-P/-J overrides the registered identity by scp's own
|
|
736
|
+
* precedence (last option wins). Assembly itself lives in ssh-argv.ts
|
|
737
|
+
* (the ONE identity policy — 2026-10-17 ruling); only the scp dialect
|
|
738
|
+
* placement rule is local. */
|
|
680
739
|
function identityArgv(connection) {
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
740
|
+
return [
|
|
741
|
+
"-o",
|
|
742
|
+
"BatchMode=yes",
|
|
743
|
+
...sshIdentityArgv(connection, "scp")
|
|
744
|
+
];
|
|
686
745
|
}
|
|
687
746
|
function sshEndpoint(plan) {
|
|
688
747
|
if (plan.kind !== "ssh") throw new Error("remote: native copy endpoint is not an SSH target");
|
|
@@ -721,24 +780,30 @@ async function nativeCopy(request, endpoints, signal, options = {}) {
|
|
|
721
780
|
stdout: [],
|
|
722
781
|
stderr: [],
|
|
723
782
|
outputTruncated: false,
|
|
724
|
-
|
|
783
|
+
verification: "native-exit-only",
|
|
784
|
+
ok: true,
|
|
725
785
|
cleanup: "none-required"
|
|
726
786
|
};
|
|
727
|
-
const finish = (route, argvs, results) => {
|
|
787
|
+
const finish = (route, argvs, results, internal = []) => {
|
|
728
788
|
receipt.route = route;
|
|
729
|
-
receipt.argv = argvs;
|
|
789
|
+
receipt.argv = argvs.map((argv) => argv.map((token) => internal.find(([secret]) => secret === token)?.[1] ?? token));
|
|
730
790
|
receipt.exits = results.map((result) => result.exit);
|
|
731
791
|
receipt.stdout = results.map((result) => result.stdout);
|
|
732
792
|
receipt.stderr = results.map((result) => result.stderr);
|
|
733
793
|
receipt.outputTruncated = results.some((result) => result.outputTruncated);
|
|
794
|
+
receipt.ok = !signal.aborted && receipt.exits.every((exit) => exit === 0);
|
|
795
|
+
if (!receipt.ok) receipt.reason = signal.aborted ? "aborted" : "non-zero-exit";
|
|
734
796
|
return receipt;
|
|
735
797
|
};
|
|
798
|
+
if (request.mkdir !== false) await ensureDestinationDir(endpoints.dst, signal, options.execArgvFor, wrap, receipt);
|
|
799
|
+
else receipt.created = "n-a";
|
|
736
800
|
if (s === "local" && t === "ssh") {
|
|
737
801
|
rejectRemoteToRemoteScp(oneShot);
|
|
738
802
|
const connection = sshEndpoint(endpoints.dst.plan).connection;
|
|
739
803
|
const argv = [
|
|
740
804
|
"scp",
|
|
741
805
|
...identityArgv(connection),
|
|
806
|
+
...await recursiveFlag(endpoints.src.path, oneShot, "-r"),
|
|
742
807
|
...oneShot,
|
|
743
808
|
endpoints.src.path,
|
|
744
809
|
scpSpec(connection, endpoints.dst.path)
|
|
@@ -771,7 +836,7 @@ async function nativeCopy(request, endpoints, signal, options = {}) {
|
|
|
771
836
|
dstSpec = scpSpec(destination, endpoints.dst.path);
|
|
772
837
|
extra = identityArgv(source);
|
|
773
838
|
} else {
|
|
774
|
-
config = await tempSshConfig(source, destination);
|
|
839
|
+
config = await tempSshConfig(source, destination, runNative);
|
|
775
840
|
extra = ["-F", config.path];
|
|
776
841
|
srcSpec = `superd-scp3-source:${endpoints.src.path}`;
|
|
777
842
|
dstSpec = `superd-scp3-destination:${endpoints.dst.path}`;
|
|
@@ -784,7 +849,7 @@ async function nativeCopy(request, endpoints, signal, options = {}) {
|
|
|
784
849
|
srcSpec,
|
|
785
850
|
dstSpec
|
|
786
851
|
];
|
|
787
|
-
return finish("scp
|
|
852
|
+
return finish("scp-3", [argv], [await runNative(wrap(argv), signal)], config !== void 0 ? [[config.path, "<temp-ssh-config>"]] : []);
|
|
788
853
|
} catch (error) {
|
|
789
854
|
if (signal.aborted) unknown = true;
|
|
790
855
|
throw error;
|
|
@@ -803,7 +868,9 @@ async function nativeCopy(request, endpoints, signal, options = {}) {
|
|
|
803
868
|
endpoints.src.path,
|
|
804
869
|
`${containerOf(endpoints.dst.plan)}:${endpoints.dst.path}`
|
|
805
870
|
];
|
|
806
|
-
|
|
871
|
+
const result = await runNative(wrap(argv), signal);
|
|
872
|
+
withSymlinkHint(result);
|
|
873
|
+
return finish("docker-cp", [argv], [result]);
|
|
807
874
|
}
|
|
808
875
|
if (s === "docker" && t === "local") {
|
|
809
876
|
const argv = [
|
|
@@ -813,18 +880,21 @@ async function nativeCopy(request, endpoints, signal, options = {}) {
|
|
|
813
880
|
`${containerOf(endpoints.src.plan)}:${endpoints.src.path}`,
|
|
814
881
|
endpoints.dst.path
|
|
815
882
|
];
|
|
816
|
-
|
|
883
|
+
const result = await runNative(wrap(argv), signal);
|
|
884
|
+
withSymlinkHint(result);
|
|
885
|
+
return finish("docker-cp", [argv], [result]);
|
|
817
886
|
}
|
|
818
887
|
if (s === "local" && t === "incus") {
|
|
819
888
|
const argv = [
|
|
820
889
|
"incus",
|
|
821
890
|
"file",
|
|
822
891
|
"push",
|
|
892
|
+
...await recursiveFlag(endpoints.src.path, oneShot, "-r"),
|
|
823
893
|
...oneShot,
|
|
824
894
|
endpoints.src.path,
|
|
825
895
|
`${containerOf(endpoints.dst.plan)}${endpoints.dst.path}`
|
|
826
896
|
];
|
|
827
|
-
return finish("incus
|
|
897
|
+
return finish("incus-file-push", [argv], [await runNative(wrap(argv), signal)]);
|
|
828
898
|
}
|
|
829
899
|
if (s === "incus" && t === "local") {
|
|
830
900
|
const argv = [
|
|
@@ -835,7 +905,7 @@ async function nativeCopy(request, endpoints, signal, options = {}) {
|
|
|
835
905
|
`${containerOf(endpoints.src.plan)}${endpoints.src.path}`,
|
|
836
906
|
endpoints.dst.path
|
|
837
907
|
];
|
|
838
|
-
return finish("incus
|
|
908
|
+
return finish("incus-file-pull", [argv], [await runNative(wrap(argv), signal)]);
|
|
839
909
|
}
|
|
840
910
|
if (s === "local" || t === "local") throw new Error("remote: this endpoint pair is a single native command route; pullArgv/pushArgv apply to cross-endpoint pairs only");
|
|
841
911
|
const pullOptions = request.pullArgv !== void 0 ? pull : oneShot;
|
|
@@ -848,9 +918,12 @@ async function nativeCopy(request, endpoints, signal, options = {}) {
|
|
|
848
918
|
const pullArgv = segmentArgv(endpoints.src.plan, endpoints.src.path, staged, pullOptions, "pull");
|
|
849
919
|
const pushArgv = segmentArgv(endpoints.dst.plan, endpoints.dst.path, staged, pushOptions, "push");
|
|
850
920
|
const first = await runNative(wrap(pullArgv), signal);
|
|
851
|
-
if (first.exit !== 0)
|
|
921
|
+
if (first.exit !== 0) {
|
|
922
|
+
receipt.note = "push not started: pull failed";
|
|
923
|
+
return finish("staged-pull-push", [pullArgv], [first], [[staged, "<staging>"]]);
|
|
924
|
+
}
|
|
852
925
|
const second = await runNative(wrap(pushArgv), signal);
|
|
853
|
-
return finish("
|
|
926
|
+
return finish("staged-pull-push", [pullArgv, pushArgv], [first, second], [[staged, "<staging>"]]);
|
|
854
927
|
} finally {
|
|
855
928
|
receipt.cleanup = await rm(scratch, {
|
|
856
929
|
recursive: true,
|
|
@@ -911,40 +984,162 @@ function segmentArgv(plan, remote, staged, opts, leg) {
|
|
|
911
984
|
spec
|
|
912
985
|
];
|
|
913
986
|
}
|
|
987
|
+
/** D3 mkdir pre-hop: ensure the destination's PARENT directory exists before
|
|
988
|
+
* the transfer (never the operand itself — see parent-only note below).
|
|
989
|
+
* transfer. Local dst is node-side; remote dst runs ONE exec `mkdir -p` probe
|
|
990
|
+
* whose stdout discloses created-vs-existed. Best-effort by contract: any
|
|
991
|
+
* failure records created:'n-a' + note and lets the copy proceed so the native
|
|
992
|
+
* CLI surfaces the real error (and stopped-container docker cp keeps working). */
|
|
993
|
+
async function ensureDestinationDir(dst, signal, execArgvFor, wrap, receipt) {
|
|
994
|
+
const dir = dirname(dst.path);
|
|
995
|
+
if (dir === "/" || dir === ".") {
|
|
996
|
+
receipt.created = false;
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
if (dst.plan.kind === "local") {
|
|
1000
|
+
try {
|
|
1001
|
+
if (existsSync(dir)) {
|
|
1002
|
+
receipt.created = false;
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
mkdirSync(dir, { recursive: true });
|
|
1006
|
+
receipt.created = true;
|
|
1007
|
+
} catch (error) {
|
|
1008
|
+
receipt.created = "n-a";
|
|
1009
|
+
receipt.note = `mkdir pre-hop failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
1010
|
+
}
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
if (execArgvFor === void 0) {
|
|
1014
|
+
receipt.created = "n-a";
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
if (/[\r\n]/.test(dir)) {
|
|
1018
|
+
receipt.created = "n-a";
|
|
1019
|
+
receipt.note = "mkdir pre-hop skipped: newline-bearing destination path";
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
const quoted = `'${dir.replaceAll("'", "'\\\\''")}'`;
|
|
1023
|
+
const script = `if [ -d ${quoted} ]; then echo existed; else mkdir -p ${quoted} && echo created; fi`;
|
|
1024
|
+
try {
|
|
1025
|
+
const result = await runNative(wrap(execArgvFor(dst.plan, script)), signal);
|
|
1026
|
+
const word = result.stdout.trim().split("\n").pop();
|
|
1027
|
+
if (result.exit === 0 && word === "created") receipt.created = true;
|
|
1028
|
+
else if (result.exit === 0 && word === "existed") receipt.created = false;
|
|
1029
|
+
else {
|
|
1030
|
+
receipt.created = "n-a";
|
|
1031
|
+
const tail = result.stderr.trim().split("\n").filter((line) => line.length > 0).pop();
|
|
1032
|
+
receipt.note = `mkdir pre-hop unavailable (exit ${result.exit})${tail !== void 0 ? `: ${tail}` : ""}`;
|
|
1033
|
+
}
|
|
1034
|
+
} catch (error) {
|
|
1035
|
+
receipt.created = "n-a";
|
|
1036
|
+
receipt.note = `mkdir pre-hop failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
914
1039
|
/** Distinguishing identities for scp -3: an origin-only temp config maps two
|
|
915
1040
|
* synthetic aliases; keys are referenced in place, never copied to any remote. */
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
1041
|
+
/** D8 (2026-10-14 user adoption, third acceptance round): a LOCAL directory source gets
|
|
1042
|
+
* the route's recursive flag appended automatically — scp / incus file push refuse
|
|
1043
|
+
* directories without it. The appended flag lands in the executed argv, which the
|
|
1044
|
+
* receipt shows verbatim: disclosure by receipt, never silent. Remote sources stay
|
|
1045
|
+
* caller-explicit (we do not dial just to stat); an explicit caller flag wins. */
|
|
1046
|
+
async function recursiveFlag(localPath, callerArgv, flag) {
|
|
1047
|
+
if (callerArgv.includes(flag) || callerArgv.includes("--recursive")) return [];
|
|
1048
|
+
try {
|
|
1049
|
+
if ((await stat(localPath)).isDirectory()) return [flag];
|
|
1050
|
+
} catch {}
|
|
1051
|
+
return [];
|
|
1052
|
+
}
|
|
1053
|
+
/** docker cp follows symlinks only with -L; its native error ("invalid symlink")
|
|
1054
|
+
* names no remedy. Append the one-line hint on that failure path (F5, 2026-10-14). */
|
|
1055
|
+
function withSymlinkHint(result) {
|
|
1056
|
+
if (result.exit !== 0 && /invalid symlink/i.test(result.stderr)) result.stderr = `${result.stderr}\nhint: docker cp follows symlinks only with -L — pass argv:["-L"] (e.g. /etc/os-release is a symlink)`;
|
|
1057
|
+
}
|
|
1058
|
+
async function tempSshConfig(source, destination, runNative$1) {
|
|
1059
|
+
const resolvedSource = await resolveThroughOriginConfig(source, runNative$1);
|
|
1060
|
+
const resolvedDestination = await resolveThroughOriginConfig(destination, runNative$1);
|
|
1061
|
+
for (const { connection: c, identityFiles } of [resolvedSource, resolvedDestination]) {
|
|
1062
|
+
for (const [field, value] of [
|
|
1063
|
+
["host", c.host],
|
|
1064
|
+
["user", c.user],
|
|
1065
|
+
["key", c.key],
|
|
1066
|
+
["proxyJump", c.proxyJump]
|
|
1067
|
+
]) if (value !== void 0 && /[\r\n]/.test(value)) throw new Error(`remote: scp -3 temp config rejects newline in SSH ${field} (no config injection)`);
|
|
1068
|
+
for (const idFile of identityFiles) if (/[\r\n]/.test(idFile)) throw new Error("remote: scp -3 temp config rejects newline in resolved IdentityFile (no config injection)");
|
|
1069
|
+
}
|
|
923
1070
|
const dir = await mkdtemp(join(tmpdir(), "superd-scp3-"));
|
|
924
1071
|
const path$1 = join(dir, "config");
|
|
925
|
-
const block = (name$1, c) => {
|
|
1072
|
+
const block = (name$1, c, identityFiles) => {
|
|
926
1073
|
const lines = [`Host ${name$1}`, ` HostName ${c.host.replace(/^.*@/, "")}`];
|
|
927
1074
|
if (c.user !== void 0) lines.push(` User ${c.user}`);
|
|
928
1075
|
if (c.port !== void 0) lines.push(` Port ${String(c.port)}`);
|
|
929
|
-
|
|
1076
|
+
for (const idFile of [c.key !== void 0 ? [c.key] : [], identityFiles].flat()) lines.push(` IdentityFile ${idFile}`);
|
|
930
1077
|
if (c.proxyJump !== void 0) lines.push(` ProxyJump ${c.proxyJump}`);
|
|
931
1078
|
return lines.join("\n");
|
|
932
1079
|
};
|
|
933
|
-
await writeFile(path$1, `${block("superd-scp3-source",
|
|
1080
|
+
await writeFile(path$1, `${block("superd-scp3-source", resolvedSource.connection, resolvedSource.identityFiles)}\n${block("superd-scp3-destination", resolvedDestination.connection, resolvedDestination.identityFiles)}\n`, { mode: 384 });
|
|
934
1081
|
await chmod(path$1, 384);
|
|
935
1082
|
return {
|
|
936
1083
|
path: path$1,
|
|
937
1084
|
dir
|
|
938
1085
|
};
|
|
939
1086
|
}
|
|
1087
|
+
/** Effective fields for one connection per the origin's own ssh config (ssh -G,
|
|
1088
|
+
* prints resolved settings without dialing). Explicit fields win; empty/none
|
|
1089
|
+
* proxyjump is dropped. IdentityFile PATHS from the config are returned separately
|
|
1090
|
+
* for inlining into the temp config — a path is not key material and stays on the
|
|
1091
|
+
* origin (F1, 2026-10-14: dev4's only authorized key lived in config, not the agent;
|
|
1092
|
+
* dropping it made scp -3 fail with a bare Permission denied). */
|
|
1093
|
+
async function resolveThroughOriginConfig(c, runNative$1) {
|
|
1094
|
+
let probe;
|
|
1095
|
+
try {
|
|
1096
|
+
probe = await runNative$1([
|
|
1097
|
+
"ssh",
|
|
1098
|
+
"-G",
|
|
1099
|
+
c.host
|
|
1100
|
+
], new AbortController().signal);
|
|
1101
|
+
} catch {
|
|
1102
|
+
probe = void 0;
|
|
1103
|
+
}
|
|
1104
|
+
if (probe === void 0 || probe.exit !== 0) return {
|
|
1105
|
+
connection: c,
|
|
1106
|
+
identityFiles: []
|
|
1107
|
+
};
|
|
1108
|
+
const lines = probe.stdout.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
1109
|
+
const pick = (key) => lines.find((line) => line.startsWith(`${key} `))?.slice(key.length + 1).trim() || void 0;
|
|
1110
|
+
const home = process.env.HOME ?? "";
|
|
1111
|
+
const compiledDefault = (path$1) => path$1 === "none" || [
|
|
1112
|
+
"id_rsa",
|
|
1113
|
+
"id_ecdsa",
|
|
1114
|
+
"id_ecdsa_sk",
|
|
1115
|
+
"id_ed25519",
|
|
1116
|
+
"id_ed25519_sk"
|
|
1117
|
+
].some((name$1) => path$1 === `~/.ssh/${name$1}` || home !== "" && path$1 === `${home}/.ssh/${name$1}`);
|
|
1118
|
+
const identityFiles = [...new Set(lines.filter((line) => line.startsWith("identityfile ")).map((line) => line.slice(13).trim()).filter((path$1) => path$1 !== "" && !compiledDefault(path$1)))];
|
|
1119
|
+
const host = (pick("hostname") ?? c.host).replace(/^.*@/, "");
|
|
1120
|
+
const user = c.user ?? pick("user");
|
|
1121
|
+
const portText = pick("port");
|
|
1122
|
+
const port = c.port ?? (portText !== void 0 && /^\d+$/.test(portText) ? Number(portText) : void 0);
|
|
1123
|
+
const proxyJumpRaw = c.proxyJump ?? pick("proxyjump");
|
|
1124
|
+
return {
|
|
1125
|
+
connection: {
|
|
1126
|
+
host,
|
|
1127
|
+
...user !== void 0 && user !== "" ? { user } : {},
|
|
1128
|
+
...port !== void 0 && Number.isInteger(port) && port > 0 ? { port } : {},
|
|
1129
|
+
...c.key !== void 0 ? { key: c.key } : {},
|
|
1130
|
+
...proxyJumpRaw !== void 0 && proxyJumpRaw !== "" && proxyJumpRaw.toLowerCase() !== "none" ? { proxyJump: proxyJumpRaw } : {}
|
|
1131
|
+
},
|
|
1132
|
+
identityFiles
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
940
1135
|
|
|
941
1136
|
//#endregion
|
|
942
1137
|
//#region src/remote/ssh-rsync-native-relay.ts
|
|
943
1138
|
/** ssh argv for the relay: batch, no agent, no multiplexing. `command` is the
|
|
944
|
-
* remote command; forwarding options go before `--`, the host after it.
|
|
1139
|
+
* remote command; forwarding options go before `--`, the host after it.
|
|
1140
|
+
* Identity core comes from ssh-argv.ts (the ONE policy — 2026-10-17 ruling). */
|
|
945
1141
|
function relaySsh(connection, extra, command) {
|
|
946
|
-
|
|
947
|
-
"ssh",
|
|
1142
|
+
return sshCommandArgv(connection, [
|
|
948
1143
|
"-o",
|
|
949
1144
|
"BatchMode=yes",
|
|
950
1145
|
"-T",
|
|
@@ -961,14 +1156,7 @@ function relaySsh(connection, extra, command) {
|
|
|
961
1156
|
"ServerAliveInterval=5",
|
|
962
1157
|
"-o",
|
|
963
1158
|
"ServerAliveCountMax=2"
|
|
964
|
-
];
|
|
965
|
-
if (connection.user !== void 0) args.push("-l", connection.user);
|
|
966
|
-
if (connection.port !== void 0) args.push("-p", String(connection.port));
|
|
967
|
-
if (connection.key !== void 0) args.push("-i", connection.key);
|
|
968
|
-
if (connection.proxyJump !== void 0) args.push("-J", connection.proxyJump);
|
|
969
|
-
args.push(...extra, "--", connection.host);
|
|
970
|
-
if (command !== void 0) args.push(command);
|
|
971
|
-
return args;
|
|
1159
|
+
], extra, command);
|
|
972
1160
|
}
|
|
973
1161
|
async function freePorts(count) {
|
|
974
1162
|
const servers = [];
|
|
@@ -1123,12 +1311,16 @@ async function forwardRoute(argv, source, destination, signal, options) {
|
|
|
1123
1311
|
return await removeRemoteTemp(destination.connection, temp, wrap) ? "released" : "unknown";
|
|
1124
1312
|
};
|
|
1125
1313
|
try {
|
|
1314
|
+
signal.throwIfAborted();
|
|
1315
|
+
const daemonState = await waitForDaemonReady(destinationChild, 5e3);
|
|
1316
|
+
if (daemonState === "exited") throw new Error(`dual-SSH rsync destination daemon exited before serving${relayDetail(destinationChild, sourceHold)}`);
|
|
1317
|
+
if (daemonState === "timeout") throw new Error(`dual-SSH rsync destination daemon never announced readiness${relayDetail(destinationChild, sourceHold)}`);
|
|
1126
1318
|
signal.throwIfAborted();
|
|
1127
1319
|
if (!await originPortOpen(originPort, 15e3)) throw new Error(`dual-SSH rsync destination daemon never became reachable${relayDetail(destinationChild, sourceHold)}`);
|
|
1128
1320
|
signal.throwIfAborted();
|
|
1129
1321
|
const runArgv = relaySsh(source.connection, [], rsyncClientCommand(argv, source.path, sourcePort));
|
|
1130
1322
|
const result = await runNative(wrap(runArgv), signal);
|
|
1131
|
-
if (result.exit
|
|
1323
|
+
if (result.exit !== 0) result.stderr = (result.stderr + relayDetail(destinationChild, sourceHold)).slice(-OUTPUT_LIMIT);
|
|
1132
1324
|
if (result.exit !== 0 && /remote port forwarding failed|administratively prohibited|could not request (local|remote) forwarding/.test(relayDetail(destinationChild, sourceHold))) throw new Error(`dual-SSH rsync forwarding route refused by an endpoint: ${relayDetail(destinationChild, sourceHold)}`);
|
|
1133
1325
|
return {
|
|
1134
1326
|
...result,
|
|
@@ -1153,6 +1345,19 @@ async function forwardRoute(argv, source, destination, signal, options) {
|
|
|
1153
1345
|
await Promise.allSettled([destinationChild.stop(), sourceHold.stop()]);
|
|
1154
1346
|
}
|
|
1155
1347
|
}
|
|
1348
|
+
/** Wait until the destination daemon script announces itself (stderr marker,
|
|
1349
|
+
* printed after its directory check and right before the daemon exec) or exits.
|
|
1350
|
+
* 'ready' only when the daemon is actually about to serve — closes the 0.3.1-e
|
|
1351
|
+
* race where -L opens at session start but the script dies ~150ms later. */
|
|
1352
|
+
async function waitForDaemonReady(held, budgetMs) {
|
|
1353
|
+
const deadline = Date.now() + budgetMs;
|
|
1354
|
+
for (;;) {
|
|
1355
|
+
if (/^SUPERD-RSYNC-TMP \S+$/m.test(held.stderrSink.text)) return "ready";
|
|
1356
|
+
if (held.child.exitCode !== null) return "exited";
|
|
1357
|
+
if (Date.now() >= deadline) return "timeout";
|
|
1358
|
+
await new Promise((resolve$1) => setTimeout(resolve$1, 50));
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1156
1361
|
function relayDetail(...held) {
|
|
1157
1362
|
const detail = held.map((child) => child.stderrSink.text).filter(Boolean).join("\n");
|
|
1158
1363
|
return detail ? `\n[relay stderr]\n${detail}` : "";
|
|
@@ -1234,14 +1439,7 @@ async function runRsync(request, source, destination, signal, options = {}) {
|
|
|
1234
1439
|
checkAbsolute(destination.path);
|
|
1235
1440
|
const argv = checkArgv("argv", request.argv);
|
|
1236
1441
|
if ("pullArgv" in request || "pushArgv" in request) throw new Error("remote: rsync takes argv only; pullArgv/pushArgv are copy-only fields");
|
|
1237
|
-
for (const
|
|
1238
|
-
"dryRun",
|
|
1239
|
-
"delete",
|
|
1240
|
-
"excludes",
|
|
1241
|
-
"preserve",
|
|
1242
|
-
"verify",
|
|
1243
|
-
"mkdir"
|
|
1244
|
-
]) if (legacy in request) throw new Error(`remote: rsync field "${legacy}" was removed — pass native options through argv instead (e.g. "--dry-run", "--delete", "--exclude=<pattern>", "-a"/"-r"; verify/mkdir belong to the copy Hub/local lane)`);
|
|
1442
|
+
for (const key of Object.keys(request)) if (key !== "src" && key !== "dst" && key !== "argv") throw new Error(`remote: rsync rejects unknown field "${key}" — native options go through argv (verify is a copy Hub-lane option; mkdir is copy-only; for destination parents pass --mkpath yourself)`);
|
|
1245
1443
|
if (!source.connection && !destination.connection) throw new Error("rsync requires at least one SSH endpoint; local-only is unsupported");
|
|
1246
1444
|
let result;
|
|
1247
1445
|
let route;
|
|
@@ -1289,6 +1487,9 @@ async function runRsync(request, source, destination, signal, options = {}) {
|
|
|
1289
1487
|
dst: request.dst,
|
|
1290
1488
|
argv,
|
|
1291
1489
|
route,
|
|
1490
|
+
verification: "native-exit-only",
|
|
1491
|
+
ok: result.exit === 0,
|
|
1492
|
+
...result.exit === 0 ? {} : { reason: signal.aborted ? "aborted" : "non-zero-exit" },
|
|
1292
1493
|
command: `rsync ${argv.join(" ")}`,
|
|
1293
1494
|
...itemized.length > 0 || argv.includes("--itemize-changes") || argv.includes("-i") ? { itemizedChanges: itemized } : {},
|
|
1294
1495
|
...literalBytes === void 0 ? {} : { literalBytes },
|
|
@@ -1303,21 +1504,14 @@ async function runRsync(request, source, destination, signal, options = {}) {
|
|
|
1303
1504
|
* placeholder host rsync appends and execs the canonical SSH argv. All tokens
|
|
1304
1505
|
* are tool-built argv (identity options/aliases), never model text. */
|
|
1305
1506
|
function rsyncRsh(connection, wrap) {
|
|
1306
|
-
const args = [
|
|
1307
|
-
"ssh",
|
|
1308
|
-
"-o",
|
|
1309
|
-
"BatchMode=yes",
|
|
1310
|
-
"-T"
|
|
1311
|
-
];
|
|
1312
|
-
if (connection.user !== void 0) args.push("-l", connection.user);
|
|
1313
|
-
if (connection.port !== void 0) args.push("-p", String(connection.port));
|
|
1314
|
-
if (connection.key !== void 0) args.push("-i", connection.key);
|
|
1315
|
-
if (connection.proxyJump !== void 0) args.push("-J", connection.proxyJump);
|
|
1316
|
-
args.push("--", connection.host);
|
|
1317
1507
|
return [
|
|
1318
1508
|
"bash",
|
|
1319
1509
|
"-c",
|
|
1320
|
-
`shift; exec ${wrap(
|
|
1510
|
+
`shift; exec ${wrap(sshCommandArgv(connection, [
|
|
1511
|
+
"-o",
|
|
1512
|
+
"BatchMode=yes",
|
|
1513
|
+
"-T"
|
|
1514
|
+
], [])).map(rshQuote).join(" ")} "$@"`,
|
|
1321
1515
|
"rsync-rsh"
|
|
1322
1516
|
].map(rshQuote).join(" ");
|
|
1323
1517
|
}
|
|
@@ -1413,26 +1607,19 @@ var SshTargets = class {
|
|
|
1413
1607
|
}
|
|
1414
1608
|
};
|
|
1415
1609
|
/** Shared by execution, probes and file/relay consumers. extraArgs is trusted
|
|
1416
|
-
* internal SSH argv (e.g. forwarding), never a model-provided shell string.
|
|
1610
|
+
* internal SSH argv (e.g. forwarding), never a model-provided shell string.
|
|
1611
|
+
* Identity assembly lives in ssh-argv.ts (the ONE policy — 2026-10-17 ruling);
|
|
1612
|
+
* this wrapper only picks the tty/batch dialect base. */
|
|
1417
1613
|
function sshArgv(connection, options = {}) {
|
|
1418
|
-
|
|
1419
|
-
"ssh",
|
|
1614
|
+
return sshCommandArgv(connection, options.tty ? [
|
|
1420
1615
|
"-tt",
|
|
1421
1616
|
"-o",
|
|
1422
1617
|
"BatchMode=yes"
|
|
1423
1618
|
] : [
|
|
1424
|
-
"ssh",
|
|
1425
1619
|
"-o",
|
|
1426
1620
|
"BatchMode=yes",
|
|
1427
1621
|
"-T"
|
|
1428
|
-
];
|
|
1429
|
-
if (connection.user !== void 0) args.push("-l", connection.user);
|
|
1430
|
-
if (connection.port !== void 0) args.push("-p", String(connection.port));
|
|
1431
|
-
if (connection.key !== void 0) args.push("-i", connection.key);
|
|
1432
|
-
if (connection.proxyJump !== void 0) args.push("-J", connection.proxyJump);
|
|
1433
|
-
args.push(...options.extraArgs ?? [], "--", connection.host);
|
|
1434
|
-
if (options.command !== void 0) args.push(options.command);
|
|
1435
|
-
return args;
|
|
1622
|
+
], options.extraArgs ?? [], options.command);
|
|
1436
1623
|
}
|
|
1437
1624
|
|
|
1438
1625
|
//#endregion
|
|
@@ -1489,6 +1676,96 @@ async function runOneShot(argv, opts = {}) {
|
|
|
1489
1676
|
};
|
|
1490
1677
|
}
|
|
1491
1678
|
|
|
1679
|
+
//#endregion
|
|
1680
|
+
//#region src/remote/helper.ts
|
|
1681
|
+
/** Reads helper-bin/MANIFEST.json next to the compiled module (lib/remote in the npm
|
|
1682
|
+
* package, super-dsh/remote in the Super closure — same relative layout). Missing or
|
|
1683
|
+
* malformed assets ⇒ null ⇒ shim-only mode; never throws. */
|
|
1684
|
+
function helperAssets(baseDir) {
|
|
1685
|
+
const dir = baseDir ?? join(dirname(fileURLToPath(import.meta.url)), "helper-bin");
|
|
1686
|
+
try {
|
|
1687
|
+
const manifest = JSON.parse(readFileSync(join(dir, "MANIFEST.json"), "utf8"));
|
|
1688
|
+
if (manifest?.format !== 1 || typeof manifest.ver !== "string" || typeof manifest.protocol !== "number" || !manifest.targets || typeof manifest.targets !== "object") return null;
|
|
1689
|
+
return {
|
|
1690
|
+
dir,
|
|
1691
|
+
manifest
|
|
1692
|
+
};
|
|
1693
|
+
} catch {
|
|
1694
|
+
return null;
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
/** `uname -s`/`uname -m` → shipped target triple. Unknown combos ⇒ null (shim). */
|
|
1698
|
+
function archTriple(sys, machine, shipped) {
|
|
1699
|
+
const s = sys.trim().toLowerCase(), m = machine.trim().toLowerCase();
|
|
1700
|
+
const candidates = s === "linux" ? m === "x86_64" || m === "amd64" ? "x86_64-unknown-linux-musl" : m === "aarch64" || m === "arm64" ? "aarch64-unknown-linux-musl" : null : s === "darwin" ? m === "arm64" || m === "aarch64" ? "aarch64-apple-darwin" : null : null;
|
|
1701
|
+
if (candidates === null) return null;
|
|
1702
|
+
return shipped.has(candidates) ? candidates : null;
|
|
1703
|
+
}
|
|
1704
|
+
function planKey(plan) {
|
|
1705
|
+
switch (plan.kind) {
|
|
1706
|
+
case "ssh": {
|
|
1707
|
+
const c = plan.connection;
|
|
1708
|
+
const host = c?.host ?? plan.host;
|
|
1709
|
+
return `ssh|${c?.user ? c.user + "@" : ""}${host}${c?.port ? ":" + c.port : ""}${c?.proxyJump ? "|" + c.proxyJump : ""}`;
|
|
1710
|
+
}
|
|
1711
|
+
case "docker": return `docker|${plan.container}`;
|
|
1712
|
+
case "incus": return `incus|${plan.container}`;
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
const PROBE_RE = /^SUPERD-PROBE (\S+) (\S+)$/m;
|
|
1716
|
+
const READY_RE = /^SUPERD-READY (\S+)$/m;
|
|
1717
|
+
var HelperRegistry = class {
|
|
1718
|
+
states = /* @__PURE__ */ new Map();
|
|
1719
|
+
constructor(assets, exec, probeArgv, uploadArgv) {
|
|
1720
|
+
this.assets = assets;
|
|
1721
|
+
this.exec = exec;
|
|
1722
|
+
this.probeArgv = probeArgv;
|
|
1723
|
+
this.uploadArgv = uploadArgv;
|
|
1724
|
+
}
|
|
1725
|
+
/** Never throws: any bootstrap failure resolves null (sticky) and the caller stays on the shim. */
|
|
1726
|
+
ensure(plan) {
|
|
1727
|
+
if (plan.kind !== "ssh" && plan.kind !== "docker" && plan.kind !== "incus") return Promise.resolve(null);
|
|
1728
|
+
const key = planKey(plan);
|
|
1729
|
+
let entry = this.states.get(key);
|
|
1730
|
+
if (entry === void 0) {
|
|
1731
|
+
entry = this.bootstrap(plan).catch(() => null);
|
|
1732
|
+
entry.catch(() => {});
|
|
1733
|
+
this.states.set(key, entry);
|
|
1734
|
+
}
|
|
1735
|
+
return entry;
|
|
1736
|
+
}
|
|
1737
|
+
async bootstrap(plan) {
|
|
1738
|
+
const { manifest } = this.assets;
|
|
1739
|
+
const first = await this.exec(this.probeArgv(plan, this.assets), { timeoutSec: 15 });
|
|
1740
|
+
const ready = this.parseReady(first.stdout, manifest.ver, manifest.protocol);
|
|
1741
|
+
if (ready !== null) return ready;
|
|
1742
|
+
const probe = PROBE_RE.exec(first.stdout);
|
|
1743
|
+
if (probe === null || first.exit !== 90 && first.exit !== 0) return null;
|
|
1744
|
+
const triple = archTriple(probe[1], probe[2], new Set(Object.keys(manifest.targets)));
|
|
1745
|
+
if (triple === null) return null;
|
|
1746
|
+
const target = manifest.targets[triple];
|
|
1747
|
+
const binary = await readFile(join(this.assets.dir, triple, target.file));
|
|
1748
|
+
if ((await this.exec(this.uploadArgv(plan, triple, target.file, this.assets), {
|
|
1749
|
+
stdin: binary,
|
|
1750
|
+
timeoutSec: 60
|
|
1751
|
+
})).exit !== 0) return null;
|
|
1752
|
+
const second = await this.exec(this.probeArgv(plan, this.assets), { timeoutSec: 15 });
|
|
1753
|
+
return this.parseReady(second.stdout, manifest.ver, manifest.protocol);
|
|
1754
|
+
}
|
|
1755
|
+
parseReady(stdout, ver, protocol) {
|
|
1756
|
+
const ready = READY_RE.exec(stdout);
|
|
1757
|
+
const banner = /^SUPERD-RUN proto=(\d+) name=superd-run ver=(\S+) target=(\S+)/m.exec(stdout);
|
|
1758
|
+
if (ready === null || banner === null) return null;
|
|
1759
|
+
if (banner[1] !== String(protocol) || banner[2] !== ver) return null;
|
|
1760
|
+
return {
|
|
1761
|
+
binPath: ready[1],
|
|
1762
|
+
triple: banner[3],
|
|
1763
|
+
ver,
|
|
1764
|
+
protocol
|
|
1765
|
+
};
|
|
1766
|
+
}
|
|
1767
|
+
};
|
|
1768
|
+
|
|
1492
1769
|
//#endregion
|
|
1493
1770
|
//#region src/remote/transports.ts
|
|
1494
1771
|
/** POSIX 单引号机械转义(唯一一层转义,spec §三"一层机械转义")。 */
|
|
@@ -1568,6 +1845,104 @@ function buildSpawnArgv(spawnCommand) {
|
|
|
1568
1845
|
"/dev/null"
|
|
1569
1846
|
];
|
|
1570
1847
|
}
|
|
1848
|
+
/** superd-run helper argv (spec: docs/specs/remote-rescope §5). Probe and upload are single
|
|
1849
|
+
* `sh -c` hops (busybox/alpine/darwin-safe); run/killgroup over ssh go through exactly one
|
|
1850
|
+
* remote-shell parse, same discipline as buildOneShotArgv. */
|
|
1851
|
+
function buildHelperProbeArgv(plan, assets) {
|
|
1852
|
+
const { ver, targets } = assets.manifest;
|
|
1853
|
+
return helperShellArgv(plan, `${Object.entries(targets).map(([triple, t]) => `d="$HOME/.cache/superd/run/${triple}-${ver}"; [ -x "$d/${t.file}" ] && { echo "SUPERD-READY $d/${t.file}"; "$d/${t.file}" banner; exit 0; };`).join(" ")} echo "SUPERD-PROBE $(uname -s 2>/dev/null || echo unknown) $(uname -m 2>/dev/null || echo unknown)"; exit 90`);
|
|
1854
|
+
}
|
|
1855
|
+
function buildHelperUploadArgv(plan, triple, file, assets) {
|
|
1856
|
+
const dir = `$HOME/.cache/superd/run/${triple}-${assets.manifest.ver}`;
|
|
1857
|
+
return helperShellArgv(plan, `mkdir -p "${dir}" && cat > "${dir}/${file}.tmp" && chmod 0755 "${dir}/${file}.tmp" && mv -f "${dir}/${file}.tmp" "${dir}/${file}"`);
|
|
1858
|
+
}
|
|
1859
|
+
function buildHelperOneShotArgv(plan, binPath, options) {
|
|
1860
|
+
const { tag, cmd } = options;
|
|
1861
|
+
switch (plan.kind) {
|
|
1862
|
+
case "ssh": return sshArgv(plan.connection ?? { host: plan.host }, { command: `exec ${shQuote(binPath)} run --tag ${shQuote(tag)} -- bash -lc ${shQuote(cmd)}` });
|
|
1863
|
+
case "docker": return [
|
|
1864
|
+
"docker",
|
|
1865
|
+
"exec",
|
|
1866
|
+
"-i",
|
|
1867
|
+
"--",
|
|
1868
|
+
plan.container,
|
|
1869
|
+
binPath,
|
|
1870
|
+
"run",
|
|
1871
|
+
"--tag",
|
|
1872
|
+
tag,
|
|
1873
|
+
"--",
|
|
1874
|
+
"bash",
|
|
1875
|
+
"-lc",
|
|
1876
|
+
cmd
|
|
1877
|
+
];
|
|
1878
|
+
case "incus": return [
|
|
1879
|
+
"incus",
|
|
1880
|
+
"exec",
|
|
1881
|
+
"--force-noninteractive",
|
|
1882
|
+
plan.container,
|
|
1883
|
+
"--",
|
|
1884
|
+
binPath,
|
|
1885
|
+
"run",
|
|
1886
|
+
"--tag",
|
|
1887
|
+
tag,
|
|
1888
|
+
"--",
|
|
1889
|
+
"bash",
|
|
1890
|
+
"-lc",
|
|
1891
|
+
cmd
|
|
1892
|
+
];
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
function buildHelperKillgroupArgv(plan, binPath, tag) {
|
|
1896
|
+
switch (plan.kind) {
|
|
1897
|
+
case "ssh": return sshArgv(plan.connection ?? { host: plan.host }, { command: `${shQuote(binPath)} killgroup --tag ${shQuote(tag)}` });
|
|
1898
|
+
case "docker": return [
|
|
1899
|
+
"docker",
|
|
1900
|
+
"exec",
|
|
1901
|
+
"--",
|
|
1902
|
+
plan.container,
|
|
1903
|
+
binPath,
|
|
1904
|
+
"killgroup",
|
|
1905
|
+
"--tag",
|
|
1906
|
+
tag
|
|
1907
|
+
];
|
|
1908
|
+
case "incus": return [
|
|
1909
|
+
"incus",
|
|
1910
|
+
"exec",
|
|
1911
|
+
"--force-noninteractive",
|
|
1912
|
+
plan.container,
|
|
1913
|
+
"--",
|
|
1914
|
+
binPath,
|
|
1915
|
+
"killgroup",
|
|
1916
|
+
"--tag",
|
|
1917
|
+
tag
|
|
1918
|
+
];
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
function helperShellArgv(plan, script) {
|
|
1922
|
+
switch (plan.kind) {
|
|
1923
|
+
case "ssh": return sshArgv(plan.connection ?? { host: plan.host }, { command: script });
|
|
1924
|
+
case "docker": return [
|
|
1925
|
+
"docker",
|
|
1926
|
+
"exec",
|
|
1927
|
+
"-i",
|
|
1928
|
+
"--",
|
|
1929
|
+
plan.container,
|
|
1930
|
+
"sh",
|
|
1931
|
+
"-c",
|
|
1932
|
+
script
|
|
1933
|
+
];
|
|
1934
|
+
case "incus": return [
|
|
1935
|
+
"incus",
|
|
1936
|
+
"exec",
|
|
1937
|
+
"--force-noninteractive",
|
|
1938
|
+
plan.container,
|
|
1939
|
+
"--",
|
|
1940
|
+
"sh",
|
|
1941
|
+
"-c",
|
|
1942
|
+
script
|
|
1943
|
+
];
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1571
1946
|
|
|
1572
1947
|
//#endregion
|
|
1573
1948
|
//#region src/remote/nonce-framing.ts
|
|
@@ -2297,6 +2672,9 @@ var RemoteDriver = class {
|
|
|
2297
2672
|
starting = /* @__PURE__ */ new Set();
|
|
2298
2673
|
fileAbort = new AbortController();
|
|
2299
2674
|
disposed = false;
|
|
2675
|
+
/** undefined = 未尝试;null = 资产缺失(shim-only);registry = 每 target sticky 状态机。 */
|
|
2676
|
+
helpers;
|
|
2677
|
+
runnerOf = /* @__PURE__ */ new WeakMap();
|
|
2300
2678
|
disposal;
|
|
2301
2679
|
constructor(opts = {}) {
|
|
2302
2680
|
this.opts = opts;
|
|
@@ -2403,13 +2781,18 @@ var RemoteDriver = class {
|
|
|
2403
2781
|
path: dst.path,
|
|
2404
2782
|
endpoint: localFileEndpoint()
|
|
2405
2783
|
}, combined);
|
|
2406
|
-
if (request.verify !== void 0
|
|
2784
|
+
if (request.verify !== void 0) throw new Error("copy: verify is a Hub/local staged lane option (independent SHA-256); native routes report exit-code-only verification — self-verify via oneshot digest commands when needed");
|
|
2407
2785
|
const planOf = (end) => end.kind === "cli" ? end.plan : end.kind === "local" ? { kind: "local" } : void 0;
|
|
2408
2786
|
const srcPlan = planOf(src), dstPlan = planOf(dst);
|
|
2409
2787
|
if (srcPlan === void 0 || dstPlan === void 0) throw new Error("copy: unreachable route resolution");
|
|
2788
|
+
const execArgvFor = (plan, script) => {
|
|
2789
|
+
const argv = helperShellArgv(plan, script);
|
|
2790
|
+
return this.opts.nativeArgvFor ? this.opts.nativeArgvFor(argv) : argv;
|
|
2791
|
+
};
|
|
2410
2792
|
return nativeCopy({
|
|
2411
2793
|
src: request.src,
|
|
2412
2794
|
dst: request.dst,
|
|
2795
|
+
...request.mkdir === void 0 ? {} : { mkdir: request.mkdir },
|
|
2413
2796
|
...request.argv === void 0 ? {} : { argv: request.argv },
|
|
2414
2797
|
...request.pullArgv === void 0 ? {} : { pullArgv: request.pullArgv },
|
|
2415
2798
|
...request.pushArgv === void 0 ? {} : { pushArgv: request.pushArgv }
|
|
@@ -2422,7 +2805,10 @@ var RemoteDriver = class {
|
|
|
2422
2805
|
plan: dstPlan,
|
|
2423
2806
|
path: dst.path
|
|
2424
2807
|
}
|
|
2425
|
-
}, combined, {
|
|
2808
|
+
}, combined, {
|
|
2809
|
+
wrapArgv: this.opts.nativeArgvFor,
|
|
2810
|
+
execArgvFor
|
|
2811
|
+
});
|
|
2426
2812
|
})();
|
|
2427
2813
|
this.starting.add(work);
|
|
2428
2814
|
work.then(() => this.starting.delete(work), () => this.starting.delete(work));
|
|
@@ -2489,12 +2875,32 @@ var RemoteDriver = class {
|
|
|
2489
2875
|
if (signal.aborted || this.disposed) process$1.cancel("owner closed during start");
|
|
2490
2876
|
return process$1;
|
|
2491
2877
|
}
|
|
2492
|
-
nativeProcess(plan, params, timeoutSec, signal = this.fileAbort.signal) {
|
|
2878
|
+
async nativeProcess(plan, params, timeoutSec, signal = this.fileAbort.signal) {
|
|
2493
2879
|
const cleanupTag = randomUUID().slice(0, 8);
|
|
2880
|
+
const helper = await this.ensureHelper(plan);
|
|
2881
|
+
if (helper !== null && !this.disposed) {
|
|
2882
|
+
const process$2 = startOneShot(buildHelperOneShotArgv(plan, helper.binPath, {
|
|
2883
|
+
tag: cleanupTag,
|
|
2884
|
+
cmd: params.cmd
|
|
2885
|
+
}), {
|
|
2886
|
+
stdin: params.stdin,
|
|
2887
|
+
timeoutSec
|
|
2888
|
+
});
|
|
2889
|
+
this.runnerOf.set(process$2, `superd-run ${helper.ver} (${helper.triple})`);
|
|
2890
|
+
process$2.done.then((result) => {
|
|
2891
|
+
if (!result.cancelled && !result.timedOut) return;
|
|
2892
|
+
startOneShot(buildHelperKillgroupArgv(plan, helper.binPath, cleanupTag), { timeoutSec: 15 }).done.catch(() => {});
|
|
2893
|
+
}).catch(() => {});
|
|
2894
|
+
const tracked = Promise.resolve(process$2);
|
|
2895
|
+
this.starting.add(tracked);
|
|
2896
|
+
tracked.then(() => this.starting.delete(tracked), () => this.starting.delete(tracked));
|
|
2897
|
+
return tracked;
|
|
2898
|
+
}
|
|
2494
2899
|
const process$1 = startOneShot((this.opts.oneshotArgvFor ?? buildOneShotArgv)(plan, params.cmd, { cleanupTag }), {
|
|
2495
2900
|
stdin: params.stdin,
|
|
2496
2901
|
timeoutSec
|
|
2497
2902
|
});
|
|
2903
|
+
this.runnerOf.set(process$1, "posix-shim");
|
|
2498
2904
|
process$1.done.then((result) => {
|
|
2499
2905
|
if (!result.cancelled && !result.timedOut) return;
|
|
2500
2906
|
const pidfile = `"\${TMPDIR:-/tmp}/superd-oneshot-${cleanupTag}.pgid"`;
|
|
@@ -2506,6 +2912,38 @@ var RemoteDriver = class {
|
|
|
2506
2912
|
work.then(() => this.starting.delete(work), () => this.starting.delete(work));
|
|
2507
2913
|
return work;
|
|
2508
2914
|
}
|
|
2915
|
+
/** Helper bootstrap is best-effort and sticky: any failure parks null for this driver
|
|
2916
|
+
* lifetime and the shim stays the path. First call to a target pays probe (+ upload). */
|
|
2917
|
+
async ensureHelper(plan) {
|
|
2918
|
+
if (this.helpers === void 0) {
|
|
2919
|
+
const assets = this.opts.helperAssetsFor?.() ?? helperAssets();
|
|
2920
|
+
this.helpers = assets === null ? null : new HelperRegistry(assets, (argv, options) => this.helperExec(argv, options), buildHelperProbeArgv, buildHelperUploadArgv);
|
|
2921
|
+
}
|
|
2922
|
+
if (this.helpers === null || this.disposed) return null;
|
|
2923
|
+
try {
|
|
2924
|
+
return await this.helpers.ensure(plan);
|
|
2925
|
+
} catch {
|
|
2926
|
+
return null;
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
async helperExec(argv, options) {
|
|
2930
|
+
this.assertOpen();
|
|
2931
|
+
const stdin = options?.stdin;
|
|
2932
|
+
if (stdin !== void 0) {
|
|
2933
|
+
const proc$1 = startBinaryProcess(argv, { timeoutSec: options?.timeoutSec });
|
|
2934
|
+
proc$1.stdin.on("error", () => {});
|
|
2935
|
+
proc$1.stdin.end(stdin);
|
|
2936
|
+
return {
|
|
2937
|
+
exit: (await proc$1.done).exit,
|
|
2938
|
+
stdout: ""
|
|
2939
|
+
};
|
|
2940
|
+
}
|
|
2941
|
+
const proc = startOneShot(argv, { timeoutSec: options?.timeoutSec });
|
|
2942
|
+
return {
|
|
2943
|
+
exit: (await proc.done).exit,
|
|
2944
|
+
stdout: proc.read("stdout", 0).text
|
|
2945
|
+
};
|
|
2946
|
+
}
|
|
2509
2947
|
own(process$1) {
|
|
2510
2948
|
this.processes.add(process$1);
|
|
2511
2949
|
process$1.done.then(() => this.processes.delete(process$1));
|
|
@@ -2783,7 +3221,8 @@ var RemoteDriver = class {
|
|
|
2783
3221
|
stderr: err?.stats ?? r$1.outputStats?.stderr
|
|
2784
3222
|
} } : {},
|
|
2785
3223
|
durationMs: r$1.durationMs,
|
|
2786
|
-
...out$1.truncated !== void 0 ? { truncated: out$1.truncated } : {}
|
|
3224
|
+
...out$1.truncated !== void 0 ? { truncated: out$1.truncated } : {},
|
|
3225
|
+
...this.runnerOf.get(process$1) === void 0 ? {} : { runner: this.runnerOf.get(process$1) }
|
|
2787
3226
|
};
|
|
2788
3227
|
}
|
|
2789
3228
|
const argv = (this.opts.ptyArgvFor ?? buildPtyArgv)(routing.plan);
|
|
@@ -2946,9 +3385,9 @@ const REMOTE_TOOL_DESCRIPTION = `Remote execution and file transfer over ssh / d
|
|
|
2946
3385
|
{target:"byop:lbl", spawn, cmd} → create the named shell once; reuse as {target:"byop:lbl", cmd} (no spawn)
|
|
2947
3386
|
Bare names resolve ssh config → docker → incus → live pty; unmatched goes to ssh and its native error surfaces. Prefixes ssh: / docker: / incus: / byop: force a channel. byop: same spawn repeated is reused; a different spawn is refused ([E_LABEL_CONFLICT], nothing runs); unknown or idle-expired → [E_NO_SESSION] (spawn is not retained — pass it again); a failed spawn frees the label.
|
|
2948
3387
|
|
|
2949
|
-
oneshot: stateless bash -lc, stdout/stderr separate, exit code verbatim
|
|
3388
|
+
oneshot: stateless bash -lc, stdout/stderr separate, exit code verbatim. Supervision prefers a pinned superd-run helper cached on the endpoint (~/.cache/superd/run; first call uploads it over the same exec lane) and falls back to a POSIX wrapper everywhere else — the result field "runner" says which ran. Helper or wrapper, backgrounded children are reaped on completion and the whole group is killed on timeout/cancel; the endpoint needs bash only (no Python).
|
|
2950
3389
|
|
|
2951
|
-
Files — {action:"copy" | "rsync", src:"[target:]abs", dst:"[target:]abs", argv:["-a"]}: argv tokens pass verbatim before src/dst — no whitelist, no forced defaults (dry run = pass "--dry-run" yourself); the CLI's own exit code and stderr come back in the receipt. copy: local↔SSH scp; SSH↔SSH scp -3 through the origin (identities stay local); docker cp; incus file push/pull; any other pair = two commands with origin staging (per-half options via pullArgv/pushArgv, exclusive with argv); scp -R refused;
|
|
3390
|
+
Files — {action:"copy" | "rsync", src:"[target:]abs", dst:"[target:]abs", argv:["-a"]}: argv tokens pass verbatim before src/dst — no whitelist, no forced defaults (dry run = pass "--dry-run" yourself); the CLI's own exit code and stderr come back in the receipt. copy: local↔SSH scp; SSH↔SSH scp -3 through the origin (identities stay local); docker cp; incus file push/pull; any other pair = two commands with origin staging (per-half options via pullArgv/pushArgv, exclusive with argv); scp -R (uppercase — scp's remote-client mode) is refused; copy of a directory from a LOCAL source auto-appends the route's recursive flag (-r, shown in the receipt argv); from a REMOTE source pass -r yourself or use rsync; mkdir defaults to ensuring the destination's PARENT directory first (one exec pre-hop, outcome in the receipt created field; the operand itself is never pre-created); receipts use stable route ids (scp / scp-3 / docker-cp / incus-file-push / incus-file-pull / staged-pull-push / hub-staged) with ok+reason, the verification tier actually performed (native-exit-only by default; sha256 on the Hub lane) and tool-owned temp paths in argv shown as placeholders. rsync: local↔SSH, or SSH↔SSH with one protocol conversation between the peers (real delta; forwarding denied → honest error, never full copy). rsync src trailing slash copies contents, no slash the directory itself.
|
|
2952
3391
|
|
|
2953
3392
|
Output: pty tails at ~30k chars ("[truncated: …]" header) — redirect bulk output to a file. Hard retention 4,000,000 UTF-16 units per stream (Hub 4MiB bytes); outputStats reports the rest; restart cannot recover discarded output. stdin feeds the command; timeout (seconds) interrupts then kills. run_in_background:true (oneshot) → jobId for job_output/job_kill. Native transport errors pass through verbatim — self-correct from them.`;
|
|
2954
3393
|
function createRemoteTool(driver, opts = {}) {
|
|
@@ -2973,7 +3412,7 @@ function createRemoteTool(driver, opts = {}) {
|
|
|
2973
3412
|
argv: {
|
|
2974
3413
|
type: "array",
|
|
2975
3414
|
items: { type: "string" },
|
|
2976
|
-
description: "Ordered native CLI options and values for one-command routes (e.g. rsync [\"-a\",\"--partial\",\"--dry-run\"], scp [\"-p\"], docker cp [\"-a\"], incus file push/pull). One element = one argv token, appended verbatim before src/dst; no shell parsing, no whitelist, no forced defaults — unknown options fail in the native CLI itself. rsync only (SSH endpoints): explicit -e/--rsh overrides the tool's SSH wiring. Two-segment cross-endpoint copy uses pullArgv/pushArgv instead."
|
|
3415
|
+
description: "Ordered native CLI options and values for one-command routes (e.g. rsync [\"-a\",\"--partial\",\"--dry-run\"], scp [\"-p\",\"-r\"], docker cp [\"-a\"], incus file push/pull). One element = one argv token, appended verbatim before src/dst; no shell parsing, no whitelist, no forced defaults — unknown options fail in the native CLI itself. rsync only (SSH endpoints): explicit -e/--rsh overrides the tool's SSH wiring. docker cp: pass -L to follow symlinks (a symlink source like /etc/os-release fails without it). Two-segment cross-endpoint copy uses pullArgv/pushArgv instead."
|
|
2977
3416
|
},
|
|
2978
3417
|
pullArgv: {
|
|
2979
3418
|
type: "array",
|
|
@@ -2987,11 +3426,11 @@ function createRemoteTool(driver, opts = {}) {
|
|
|
2987
3426
|
},
|
|
2988
3427
|
verify: {
|
|
2989
3428
|
type: "boolean",
|
|
2990
|
-
description: "copy only
|
|
3429
|
+
description: "copy only. Receipts name the verification tier actually performed (D1=A): native CLI routes are \"native-exit-only\" (no independent check — self-verify via oneshot digest commands when needed); the Hub↔local staged lane computes independent SHA-256 digests by default and verify:false skips them (\"not-applicable\"). No hash tier exists on native routes."
|
|
2991
3430
|
},
|
|
2992
3431
|
mkdir: {
|
|
2993
3432
|
type: "boolean",
|
|
2994
|
-
description: "copy only,
|
|
3433
|
+
description: "copy only, all routes. Default true: before transfer, one best-effort exec pre-hop ensures the destination's PARENT directory exists (the operand itself is never pre-created — each CLI's own layout semantics stay byte-identical). Receipt created field: true (parent created this call) / false (already existed) / \"n-a\" (skipped or pre-hop unavailable, e.g. docker exec on a stopped container — the native CLI then reports the real error itself). false skips the pre-hop."
|
|
2995
3434
|
},
|
|
2996
3435
|
run_in_background: {
|
|
2997
3436
|
type: "boolean",
|
|
@@ -3088,7 +3527,11 @@ function createRemoteTool(driver, opts = {}) {
|
|
|
3088
3527
|
outputStats: { type: "json" },
|
|
3089
3528
|
outputStatsUnavailable: { type: "boolean" },
|
|
3090
3529
|
session: { type: "string" },
|
|
3091
|
-
notice: { type: "string" }
|
|
3530
|
+
notice: { type: "string" },
|
|
3531
|
+
runner: {
|
|
3532
|
+
type: "string",
|
|
3533
|
+
description: "oneshot supervision path: superd-run <ver> (<triple>) or posix-shim"
|
|
3534
|
+
}
|
|
3092
3535
|
}
|
|
3093
3536
|
},
|
|
3094
3537
|
render: (_args, value) => [{
|
|
@@ -3110,12 +3553,26 @@ function createRemoteTool(driver, opts = {}) {
|
|
|
3110
3553
|
args.timeout,
|
|
3111
3554
|
args.run_in_background
|
|
3112
3555
|
].some((value) => value !== void 0)) throw new Error(`${args.action} cannot be combined with execution/PTY/job arguments; register byossh separately first`);
|
|
3113
|
-
|
|
3114
|
-
"
|
|
3115
|
-
"
|
|
3116
|
-
"
|
|
3117
|
-
"
|
|
3118
|
-
|
|
3556
|
+
const KNOWN_PARAMS = new Set([
|
|
3557
|
+
"action",
|
|
3558
|
+
"src",
|
|
3559
|
+
"dst",
|
|
3560
|
+
"argv",
|
|
3561
|
+
"pullArgv",
|
|
3562
|
+
"pushArgv",
|
|
3563
|
+
"verify",
|
|
3564
|
+
"mkdir",
|
|
3565
|
+
"run_in_background",
|
|
3566
|
+
"target",
|
|
3567
|
+
"ssh",
|
|
3568
|
+
"spawn",
|
|
3569
|
+
"cmd",
|
|
3570
|
+
"script",
|
|
3571
|
+
"mode",
|
|
3572
|
+
"stdin",
|
|
3573
|
+
"timeout"
|
|
3574
|
+
]);
|
|
3575
|
+
for (const key of Object.keys(args)) if (!KNOWN_PARAMS.has(key)) throw new Error(`remote: unknown parameter "${key}" — this surface has no legacy aliases (dryRun/delete/excludes/preserve are gone); pass native CLI options through argv instead`);
|
|
3119
3576
|
if (args.action === "rsync") {
|
|
3120
3577
|
if (args.pullArgv !== void 0 || args.pushArgv !== void 0) throw new Error("rsync takes argv only; pullArgv/pushArgv are copy-only fields");
|
|
3121
3578
|
if ([args.verify, args.mkdir].some((value) => value !== void 0)) throw new Error("rsync cannot be combined with copy verify/mkdir options; the Hub/local staged lane is copy-only");
|
|
@@ -3125,15 +3582,17 @@ function createRemoteTool(driver, opts = {}) {
|
|
|
3125
3582
|
dst: args.dst,
|
|
3126
3583
|
...args.argv === void 0 ? {} : { argv: args.argv }
|
|
3127
3584
|
}, exec.signal, { sessionKey: getSessionKey(exec) });
|
|
3128
|
-
if (receipt$1.exit !== 0) throw new Error(JSON.stringify(receipt$1));
|
|
3585
|
+
if (receipt$1.ok === false || receipt$1.exit !== 0) throw new Error(JSON.stringify(receipt$1));
|
|
3586
|
+
const rsyncDuration = Date.now() - start;
|
|
3129
3587
|
return {
|
|
3130
3588
|
kind: "rsync",
|
|
3131
|
-
text:
|
|
3589
|
+
text: formatTransferLine("rsync", receipt$1, rsyncDuration),
|
|
3132
3590
|
receipt: receipt$1,
|
|
3133
3591
|
exit: receipt$1.exit,
|
|
3134
|
-
durationMs:
|
|
3592
|
+
durationMs: rsyncDuration
|
|
3135
3593
|
};
|
|
3136
3594
|
}
|
|
3595
|
+
const copyStart = Date.now();
|
|
3137
3596
|
const receipt = await driver.copy({
|
|
3138
3597
|
src: args.src,
|
|
3139
3598
|
dst: args.dst,
|
|
@@ -3143,12 +3602,16 @@ function createRemoteTool(driver, opts = {}) {
|
|
|
3143
3602
|
...args.pullArgv === void 0 ? {} : { pullArgv: args.pullArgv },
|
|
3144
3603
|
...args.pushArgv === void 0 ? {} : { pushArgv: args.pushArgv }
|
|
3145
3604
|
}, exec.signal, { sessionKey: getSessionKey(exec) });
|
|
3605
|
+
const asReceipt = receipt;
|
|
3606
|
+
const exits = asReceipt.exits ?? [];
|
|
3607
|
+
if (asReceipt.ok === false || exits.some((code) => code !== 0)) throw new Error(JSON.stringify(receipt));
|
|
3608
|
+
const copyDuration = Date.now() - copyStart;
|
|
3146
3609
|
return {
|
|
3147
3610
|
kind: "copy",
|
|
3148
|
-
text:
|
|
3611
|
+
text: formatTransferLine("copy", receipt, copyDuration),
|
|
3149
3612
|
receipt,
|
|
3150
|
-
exit: null,
|
|
3151
|
-
durationMs:
|
|
3613
|
+
exit: exits.length > 0 ? 0 : null,
|
|
3614
|
+
durationMs: copyDuration
|
|
3152
3615
|
};
|
|
3153
3616
|
}
|
|
3154
3617
|
if ([
|
|
@@ -3234,6 +3697,7 @@ function createRemoteTool(driver, opts = {}) {
|
|
|
3234
3697
|
if (r.outputStatsUnavailable) v.outputStatsUnavailable = true;
|
|
3235
3698
|
if (r.session !== void 0) v.session = r.session;
|
|
3236
3699
|
if (r.notice !== void 0) v.notice = r.notice;
|
|
3700
|
+
if (r.runner !== void 0) v.runner = r.runner;
|
|
3237
3701
|
return v;
|
|
3238
3702
|
}
|
|
3239
3703
|
});
|
|
@@ -3244,6 +3708,26 @@ function createRemoteTool(driver, opts = {}) {
|
|
|
3244
3708
|
});
|
|
3245
3709
|
return tool;
|
|
3246
3710
|
}
|
|
3711
|
+
/** S8 (2026-10-17): copy/rsync render as one formatted line; the full receipt
|
|
3712
|
+
* stays in the receipt field — raw-JSON dumps are gone from text. */
|
|
3713
|
+
function formatTransferLine(action, receipt, durationMs) {
|
|
3714
|
+
const r = receipt;
|
|
3715
|
+
const parts = [];
|
|
3716
|
+
if (r.ok === false) parts.push(`✗ ${r.reason ?? "failed"}`);
|
|
3717
|
+
parts.push(`${action} ${r.src} → ${r.dst}`);
|
|
3718
|
+
if (r.route !== void 0 && r.route !== "") parts.push(`route ${r.route}`);
|
|
3719
|
+
if (r.exits !== void 0) parts.push(`exits [${r.exits.join(",")}]`);
|
|
3720
|
+
else if (r.exit !== void 0) parts.push(`exit ${r.exit}`);
|
|
3721
|
+
if (r.verification !== void 0) parts.push(`verification ${r.verification}`);
|
|
3722
|
+
if (r.created === true) parts.push("created yes");
|
|
3723
|
+
else if (r.created === false) parts.push("created pre-existing");
|
|
3724
|
+
else if (r.created === "n-a") parts.push("created n-a");
|
|
3725
|
+
if (r.cleanup !== void 0 && r.cleanup !== "none-required") parts.push(`cleanup ${r.cleanup}`);
|
|
3726
|
+
if (typeof r.bytes === "number") parts.push(`${r.bytes} bytes / ${r.entries} entries`);
|
|
3727
|
+
parts.push(`${(durationMs / 1e3).toFixed(1)}s`);
|
|
3728
|
+
if (r.note !== void 0 && r.note.length > 0) parts.push(`(${r.note})`);
|
|
3729
|
+
return parts.join(" · ");
|
|
3730
|
+
}
|
|
3247
3731
|
function renderValue(v) {
|
|
3248
3732
|
if (v.kind === "status" || v.kind === "roster" || v.kind === "job" || v.kind === "copy" || v.kind === "rsync") return v.text;
|
|
3249
3733
|
const parts = [];
|
|
@@ -3258,7 +3742,8 @@ function renderValue(v) {
|
|
|
3258
3742
|
const suffix = markers.length > 0 ? ` · ${markers.join(" · ")}` : "";
|
|
3259
3743
|
const cwdTag = typeof v.cwd === "string" && v.cwd.length > 0 ? ` · cwd ${v.cwd}` : "";
|
|
3260
3744
|
const sessionTag = typeof v.session === "string" && v.session.length > 0 ? ` · ${v.session}` : "";
|
|
3261
|
-
|
|
3745
|
+
const runnerTag = typeof v.runner === "string" && v.runner.length > 0 ? ` · runner ${v.runner}` : "";
|
|
3746
|
+
parts.push(`[exit ${v.exit} · ${secs}s${suffix}${cwdTag}${sessionTag}${runnerTag}]`);
|
|
3262
3747
|
if (v.stderr !== void 0 && v.stderr.length > 0) parts.push("[stderr]", v.stderr);
|
|
3263
3748
|
return parts.join("\n");
|
|
3264
3749
|
}
|