super-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/package.json +1 -1
- package/remote/helper-bin/MANIFEST.json +35 -0
- package/remote/helper-bin/aarch64-apple-darwin/BUILT-VER +1 -0
- package/remote/helper-bin/aarch64-apple-darwin/superd-run +0 -0
- package/remote/helper-bin/aarch64-unknown-linux-musl/BUILT-VER +1 -0
- package/remote/helper-bin/aarch64-unknown-linux-musl/superd-run +0 -0
- package/remote/helper-bin/x86_64-pc-windows-gnu/BUILT-VER +1 -0
- package/remote/helper-bin/x86_64-pc-windows-gnu/superd-run.exe +0 -0
- package/remote/helper-bin/x86_64-unknown-linux-musl/BUILT-VER +1 -0
- package/remote/helper-bin/x86_64-unknown-linux-musl/superd-run +0 -0
- package/remote/plugin.d.ts +51 -4
- package/remote/plugin.js +589 -104
- package/remote/provenance.json +21 -10
package/remote/plugin.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Context } from "@deepseek-ai/cordis";
|
|
2
|
-
import { chmod, lstat, mkdir, mkdtemp, open, readFile, readdir, readlink, rename, rm, symlink, writeFile } from "node:fs/promises";
|
|
3
|
-
import { constants, createReadStream, createWriteStream, mkdirSync, statSync } from "node:fs";
|
|
2
|
+
import { chmod, lstat, mkdir, mkdtemp, open, readFile, readdir, readlink, rename, rm, stat, symlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { constants, createReadStream, createWriteStream, existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
|
|
4
4
|
import { pipeline } from "node:stream/promises";
|
|
5
5
|
import { dirname, isAbsolute, join, posix, resolve } from "node:path";
|
|
6
6
|
import { Readable } from "node:stream";
|
|
@@ -8,6 +8,7 @@ import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
|
8
8
|
import { homedir, tmpdir } from "node:os";
|
|
9
9
|
import { spawn } from "node:child_process";
|
|
10
10
|
import { createConnection, createServer } from "node:net";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
11
12
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
12
13
|
|
|
13
14
|
//#region third_party/schemastery/node_modules/@deepseek-ai/cosmokit/lib/index.js
|
|
@@ -1209,17 +1210,18 @@ async function copyTree(request, source, destination, signal, scratchRoot = join
|
|
|
1209
1210
|
signal.throwIfAborted();
|
|
1210
1211
|
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");
|
|
1211
1212
|
await mkdir(scratchRoot, { recursive: true });
|
|
1212
|
-
signal.throwIfAborted();
|
|
1213
|
-
const scratch = await mkdtemp(join(scratchRoot, "remote-copy-")), payload = join(scratch, "payload.bin");
|
|
1214
1213
|
const receipt = {
|
|
1215
1214
|
action: "copy",
|
|
1215
|
+
route: "hub-staged",
|
|
1216
1216
|
src: request.src,
|
|
1217
1217
|
dst: request.dst,
|
|
1218
|
-
|
|
1218
|
+
ok: false,
|
|
1219
|
+
verification: "not-applicable",
|
|
1219
1220
|
bytes: 0,
|
|
1220
1221
|
entries: 0,
|
|
1221
1222
|
scope: "transferred entries"
|
|
1222
1223
|
};
|
|
1224
|
+
const scratch = await mkdtemp(join(scratchRoot, "remote-copy-")), payload = join(scratch, "payload.bin");
|
|
1223
1225
|
try {
|
|
1224
1226
|
await pipeline(await source.endpoint.exportTree(source.path, signal), createWriteStream(payload, {
|
|
1225
1227
|
flags: "wx",
|
|
@@ -1243,24 +1245,79 @@ async function copyTree(request, source, destination, signal, scratchRoot = join
|
|
|
1243
1245
|
const dest = await destination.endpoint.digest(destination.path, signal, selection);
|
|
1244
1246
|
receipt.destinationDigest = dest.sha256;
|
|
1245
1247
|
if (dest.sha256 !== receipt.sourceDigest) throw new Error("destination digest mismatch");
|
|
1246
|
-
receipt.
|
|
1248
|
+
receipt.verification = "sha256";
|
|
1247
1249
|
}
|
|
1248
1250
|
signal.throwIfAborted();
|
|
1249
1251
|
await rm(scratch, {
|
|
1250
1252
|
recursive: true,
|
|
1251
1253
|
force: true
|
|
1252
1254
|
});
|
|
1255
|
+
receipt.ok = true;
|
|
1253
1256
|
return receipt;
|
|
1254
1257
|
} catch (error) {
|
|
1258
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1259
|
+
const reason = signal.aborted ? "aborted" : /mismatch/i.test(message) ? "digest-mismatch" : "staging-error";
|
|
1255
1260
|
throw new CopyError({
|
|
1256
1261
|
...receipt,
|
|
1257
|
-
|
|
1262
|
+
ok: false,
|
|
1263
|
+
reason,
|
|
1258
1264
|
scratch,
|
|
1259
|
-
error:
|
|
1265
|
+
error: message
|
|
1260
1266
|
});
|
|
1261
1267
|
}
|
|
1262
1268
|
}
|
|
1263
1269
|
|
|
1270
|
+
//#endregion
|
|
1271
|
+
//#region src/remote/ssh-argv.ts
|
|
1272
|
+
/**
|
|
1273
|
+
* ONE identity-assembly policy for every SSH-speaking lane (user close-out
|
|
1274
|
+
* ruling 2026-10-17: "four builders necessarily mean four identity
|
|
1275
|
+
* semantics" — the structural root of F1).
|
|
1276
|
+
*
|
|
1277
|
+
* Assembly: explicit connection fields (byossh registrations; caller options
|
|
1278
|
+
* may override later by the CLI's own last-option-wins) in ONE order —
|
|
1279
|
+
* user, port, key, proxyJump — spoken in the target CLI's dialect.
|
|
1280
|
+
*
|
|
1281
|
+
* Resolution: config-side identity (aliases, config-only IdentityFile paths)
|
|
1282
|
+
* is read from the ORIGIN's own ssh config natively by every lane. The single
|
|
1283
|
+
* exception is the scp -3 temp config (ssh -F sees no user config), where
|
|
1284
|
+
* tempSshConfig inlines the `ssh -G`-resolved fields — paths only, never key
|
|
1285
|
+
* material (F1, 2026-10-14). That policy lives beside this builder in
|
|
1286
|
+
* native-copy.ts and must stay the only such inline site.
|
|
1287
|
+
*
|
|
1288
|
+
* Former duplicate builders now reduced here: sshArgv (execution/probes),
|
|
1289
|
+
* identityArgv (scp routes), rsyncRsh (RSYNC_RSH wiring), relaySsh
|
|
1290
|
+
* (dual-SSH relay). Behavior is byte-identical to each former site.
|
|
1291
|
+
*/
|
|
1292
|
+
/** Identity options ONLY for one connection, in the target CLI's dialect:
|
|
1293
|
+
* user, port, key, proxyJump — conditional, one order, NO base flags (each
|
|
1294
|
+
* lane's dialect base — BatchMode, -T, hardening — stays at its call site).
|
|
1295
|
+
* scp carries the user inside its user@host operand, so the scp dialect
|
|
1296
|
+
* never emits -l; its port flag is -P. */
|
|
1297
|
+
function sshIdentityArgv(connection, dialect) {
|
|
1298
|
+
const args = [];
|
|
1299
|
+
if (dialect === "ssh" && connection.user !== void 0) args.push("-l", connection.user);
|
|
1300
|
+
if (connection.port !== void 0) args.push(dialect === "scp" ? "-P" : "-p", String(connection.port));
|
|
1301
|
+
if (connection.key !== void 0) args.push("-i", connection.key);
|
|
1302
|
+
if (connection.proxyJump !== void 0) args.push("-J", connection.proxyJump);
|
|
1303
|
+
return args;
|
|
1304
|
+
}
|
|
1305
|
+
/** Full ssh invocation around the shared identity core. base and extra are
|
|
1306
|
+
* trusted internal argv (dialect flags incl. BatchMode, forwarding options)
|
|
1307
|
+
* — never a model-provided shell string. */
|
|
1308
|
+
function sshCommandArgv(connection, base, extra, command) {
|
|
1309
|
+
const args = [
|
|
1310
|
+
"ssh",
|
|
1311
|
+
...base,
|
|
1312
|
+
...sshIdentityArgv(connection, "ssh"),
|
|
1313
|
+
...extra,
|
|
1314
|
+
"--",
|
|
1315
|
+
connection.host
|
|
1316
|
+
];
|
|
1317
|
+
if (command !== void 0) args.push(command);
|
|
1318
|
+
return args;
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1264
1321
|
//#endregion
|
|
1265
1322
|
//#region src/remote/output-ring.ts
|
|
1266
1323
|
const continuation = (byte) => (byte & 192) === 128;
|
|
@@ -1566,15 +1623,17 @@ function scpSpec(connection, path) {
|
|
|
1566
1623
|
const host = connection.host.includes(":") ? `[${connection.host}]` : connection.host;
|
|
1567
1624
|
return `${connection.user !== void 0 ? `${connection.user}@${host}` : host}:${path}`;
|
|
1568
1625
|
}
|
|
1569
|
-
/** Connection identity as scp options
|
|
1570
|
-
*
|
|
1571
|
-
* precedence (last option wins).
|
|
1626
|
+
/** Connection identity as scp options, placed BEFORE caller argv so an
|
|
1627
|
+
* explicit caller -F/-i/-P/-J overrides the registered identity by scp's own
|
|
1628
|
+
* precedence (last option wins). Assembly itself lives in ssh-argv.ts
|
|
1629
|
+
* (the ONE identity policy — 2026-10-17 ruling); only the scp dialect
|
|
1630
|
+
* placement rule is local. */
|
|
1572
1631
|
function identityArgv(connection) {
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1632
|
+
return [
|
|
1633
|
+
"-o",
|
|
1634
|
+
"BatchMode=yes",
|
|
1635
|
+
...sshIdentityArgv(connection, "scp")
|
|
1636
|
+
];
|
|
1578
1637
|
}
|
|
1579
1638
|
function sshEndpoint(plan) {
|
|
1580
1639
|
if (plan.kind !== "ssh") throw new Error("remote: native copy endpoint is not an SSH target");
|
|
@@ -1613,24 +1672,30 @@ async function nativeCopy(request, endpoints, signal, options = {}) {
|
|
|
1613
1672
|
stdout: [],
|
|
1614
1673
|
stderr: [],
|
|
1615
1674
|
outputTruncated: false,
|
|
1616
|
-
|
|
1675
|
+
verification: "native-exit-only",
|
|
1676
|
+
ok: true,
|
|
1617
1677
|
cleanup: "none-required"
|
|
1618
1678
|
};
|
|
1619
|
-
const finish = (route, argvs, results) => {
|
|
1679
|
+
const finish = (route, argvs, results, internal = []) => {
|
|
1620
1680
|
receipt.route = route;
|
|
1621
|
-
receipt.argv = argvs;
|
|
1681
|
+
receipt.argv = argvs.map((argv) => argv.map((token) => internal.find(([secret]) => secret === token)?.[1] ?? token));
|
|
1622
1682
|
receipt.exits = results.map((result) => result.exit);
|
|
1623
1683
|
receipt.stdout = results.map((result) => result.stdout);
|
|
1624
1684
|
receipt.stderr = results.map((result) => result.stderr);
|
|
1625
1685
|
receipt.outputTruncated = results.some((result) => result.outputTruncated);
|
|
1686
|
+
receipt.ok = !signal.aborted && receipt.exits.every((exit) => exit === 0);
|
|
1687
|
+
if (!receipt.ok) receipt.reason = signal.aborted ? "aborted" : "non-zero-exit";
|
|
1626
1688
|
return receipt;
|
|
1627
1689
|
};
|
|
1690
|
+
if (request.mkdir !== false) await ensureDestinationDir(endpoints.dst, signal, options.execArgvFor, wrap, receipt);
|
|
1691
|
+
else receipt.created = "n-a";
|
|
1628
1692
|
if (s === "local" && t === "ssh") {
|
|
1629
1693
|
rejectRemoteToRemoteScp(oneShot);
|
|
1630
1694
|
const connection = sshEndpoint(endpoints.dst.plan).connection;
|
|
1631
1695
|
const argv = [
|
|
1632
1696
|
"scp",
|
|
1633
1697
|
...identityArgv(connection),
|
|
1698
|
+
...await recursiveFlag(endpoints.src.path, oneShot, "-r"),
|
|
1634
1699
|
...oneShot,
|
|
1635
1700
|
endpoints.src.path,
|
|
1636
1701
|
scpSpec(connection, endpoints.dst.path)
|
|
@@ -1663,7 +1728,7 @@ async function nativeCopy(request, endpoints, signal, options = {}) {
|
|
|
1663
1728
|
dstSpec = scpSpec(destination, endpoints.dst.path);
|
|
1664
1729
|
extra = identityArgv(source);
|
|
1665
1730
|
} else {
|
|
1666
|
-
config = await tempSshConfig(source, destination);
|
|
1731
|
+
config = await tempSshConfig(source, destination, runNative);
|
|
1667
1732
|
extra = ["-F", config.path];
|
|
1668
1733
|
srcSpec = `superd-scp3-source:${endpoints.src.path}`;
|
|
1669
1734
|
dstSpec = `superd-scp3-destination:${endpoints.dst.path}`;
|
|
@@ -1676,7 +1741,7 @@ async function nativeCopy(request, endpoints, signal, options = {}) {
|
|
|
1676
1741
|
srcSpec,
|
|
1677
1742
|
dstSpec
|
|
1678
1743
|
];
|
|
1679
|
-
return finish("scp
|
|
1744
|
+
return finish("scp-3", [argv], [await runNative(wrap(argv), signal)], config !== void 0 ? [[config.path, "<temp-ssh-config>"]] : []);
|
|
1680
1745
|
} catch (error) {
|
|
1681
1746
|
if (signal.aborted) unknown = true;
|
|
1682
1747
|
throw error;
|
|
@@ -1695,7 +1760,9 @@ async function nativeCopy(request, endpoints, signal, options = {}) {
|
|
|
1695
1760
|
endpoints.src.path,
|
|
1696
1761
|
`${containerOf(endpoints.dst.plan)}:${endpoints.dst.path}`
|
|
1697
1762
|
];
|
|
1698
|
-
|
|
1763
|
+
const result = await runNative(wrap(argv), signal);
|
|
1764
|
+
withSymlinkHint(result);
|
|
1765
|
+
return finish("docker-cp", [argv], [result]);
|
|
1699
1766
|
}
|
|
1700
1767
|
if (s === "docker" && t === "local") {
|
|
1701
1768
|
const argv = [
|
|
@@ -1705,18 +1772,21 @@ async function nativeCopy(request, endpoints, signal, options = {}) {
|
|
|
1705
1772
|
`${containerOf(endpoints.src.plan)}:${endpoints.src.path}`,
|
|
1706
1773
|
endpoints.dst.path
|
|
1707
1774
|
];
|
|
1708
|
-
|
|
1775
|
+
const result = await runNative(wrap(argv), signal);
|
|
1776
|
+
withSymlinkHint(result);
|
|
1777
|
+
return finish("docker-cp", [argv], [result]);
|
|
1709
1778
|
}
|
|
1710
1779
|
if (s === "local" && t === "incus") {
|
|
1711
1780
|
const argv = [
|
|
1712
1781
|
"incus",
|
|
1713
1782
|
"file",
|
|
1714
1783
|
"push",
|
|
1784
|
+
...await recursiveFlag(endpoints.src.path, oneShot, "-r"),
|
|
1715
1785
|
...oneShot,
|
|
1716
1786
|
endpoints.src.path,
|
|
1717
1787
|
`${containerOf(endpoints.dst.plan)}${endpoints.dst.path}`
|
|
1718
1788
|
];
|
|
1719
|
-
return finish("incus
|
|
1789
|
+
return finish("incus-file-push", [argv], [await runNative(wrap(argv), signal)]);
|
|
1720
1790
|
}
|
|
1721
1791
|
if (s === "incus" && t === "local") {
|
|
1722
1792
|
const argv = [
|
|
@@ -1727,7 +1797,7 @@ async function nativeCopy(request, endpoints, signal, options = {}) {
|
|
|
1727
1797
|
`${containerOf(endpoints.src.plan)}${endpoints.src.path}`,
|
|
1728
1798
|
endpoints.dst.path
|
|
1729
1799
|
];
|
|
1730
|
-
return finish("incus
|
|
1800
|
+
return finish("incus-file-pull", [argv], [await runNative(wrap(argv), signal)]);
|
|
1731
1801
|
}
|
|
1732
1802
|
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");
|
|
1733
1803
|
const pullOptions = request.pullArgv !== void 0 ? pull : oneShot;
|
|
@@ -1740,9 +1810,12 @@ async function nativeCopy(request, endpoints, signal, options = {}) {
|
|
|
1740
1810
|
const pullArgv = segmentArgv(endpoints.src.plan, endpoints.src.path, staged, pullOptions, "pull");
|
|
1741
1811
|
const pushArgv = segmentArgv(endpoints.dst.plan, endpoints.dst.path, staged, pushOptions, "push");
|
|
1742
1812
|
const first = await runNative(wrap(pullArgv), signal);
|
|
1743
|
-
if (first.exit !== 0)
|
|
1813
|
+
if (first.exit !== 0) {
|
|
1814
|
+
receipt.note = "push not started: pull failed";
|
|
1815
|
+
return finish("staged-pull-push", [pullArgv], [first], [[staged, "<staging>"]]);
|
|
1816
|
+
}
|
|
1744
1817
|
const second = await runNative(wrap(pushArgv), signal);
|
|
1745
|
-
return finish("
|
|
1818
|
+
return finish("staged-pull-push", [pullArgv, pushArgv], [first, second], [[staged, "<staging>"]]);
|
|
1746
1819
|
} finally {
|
|
1747
1820
|
receipt.cleanup = await rm(scratch, {
|
|
1748
1821
|
recursive: true,
|
|
@@ -1803,40 +1876,162 @@ function segmentArgv(plan, remote, staged, opts, leg) {
|
|
|
1803
1876
|
spec
|
|
1804
1877
|
];
|
|
1805
1878
|
}
|
|
1879
|
+
/** D3 mkdir pre-hop: ensure the destination's PARENT directory exists before
|
|
1880
|
+
* the transfer (never the operand itself — see parent-only note below).
|
|
1881
|
+
* transfer. Local dst is node-side; remote dst runs ONE exec `mkdir -p` probe
|
|
1882
|
+
* whose stdout discloses created-vs-existed. Best-effort by contract: any
|
|
1883
|
+
* failure records created:'n-a' + note and lets the copy proceed so the native
|
|
1884
|
+
* CLI surfaces the real error (and stopped-container docker cp keeps working). */
|
|
1885
|
+
async function ensureDestinationDir(dst, signal, execArgvFor, wrap, receipt) {
|
|
1886
|
+
const dir = dirname(dst.path);
|
|
1887
|
+
if (dir === "/" || dir === ".") {
|
|
1888
|
+
receipt.created = false;
|
|
1889
|
+
return;
|
|
1890
|
+
}
|
|
1891
|
+
if (dst.plan.kind === "local") {
|
|
1892
|
+
try {
|
|
1893
|
+
if (existsSync(dir)) {
|
|
1894
|
+
receipt.created = false;
|
|
1895
|
+
return;
|
|
1896
|
+
}
|
|
1897
|
+
mkdirSync(dir, { recursive: true });
|
|
1898
|
+
receipt.created = true;
|
|
1899
|
+
} catch (error) {
|
|
1900
|
+
receipt.created = "n-a";
|
|
1901
|
+
receipt.note = `mkdir pre-hop failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
1902
|
+
}
|
|
1903
|
+
return;
|
|
1904
|
+
}
|
|
1905
|
+
if (execArgvFor === void 0) {
|
|
1906
|
+
receipt.created = "n-a";
|
|
1907
|
+
return;
|
|
1908
|
+
}
|
|
1909
|
+
if (/[\r\n]/.test(dir)) {
|
|
1910
|
+
receipt.created = "n-a";
|
|
1911
|
+
receipt.note = "mkdir pre-hop skipped: newline-bearing destination path";
|
|
1912
|
+
return;
|
|
1913
|
+
}
|
|
1914
|
+
const quoted = `'${dir.replaceAll("'", "'\\\\''")}'`;
|
|
1915
|
+
const script = `if [ -d ${quoted} ]; then echo existed; else mkdir -p ${quoted} && echo created; fi`;
|
|
1916
|
+
try {
|
|
1917
|
+
const result = await runNative(wrap(execArgvFor(dst.plan, script)), signal);
|
|
1918
|
+
const word = result.stdout.trim().split("\n").pop();
|
|
1919
|
+
if (result.exit === 0 && word === "created") receipt.created = true;
|
|
1920
|
+
else if (result.exit === 0 && word === "existed") receipt.created = false;
|
|
1921
|
+
else {
|
|
1922
|
+
receipt.created = "n-a";
|
|
1923
|
+
const tail = result.stderr.trim().split("\n").filter((line) => line.length > 0).pop();
|
|
1924
|
+
receipt.note = `mkdir pre-hop unavailable (exit ${result.exit})${tail !== void 0 ? `: ${tail}` : ""}`;
|
|
1925
|
+
}
|
|
1926
|
+
} catch (error) {
|
|
1927
|
+
receipt.created = "n-a";
|
|
1928
|
+
receipt.note = `mkdir pre-hop failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1806
1931
|
/** Distinguishing identities for scp -3: an origin-only temp config maps two
|
|
1807
1932
|
* synthetic aliases; keys are referenced in place, never copied to any remote. */
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1933
|
+
/** D8 (2026-10-14 user adoption, third acceptance round): a LOCAL directory source gets
|
|
1934
|
+
* the route's recursive flag appended automatically — scp / incus file push refuse
|
|
1935
|
+
* directories without it. The appended flag lands in the executed argv, which the
|
|
1936
|
+
* receipt shows verbatim: disclosure by receipt, never silent. Remote sources stay
|
|
1937
|
+
* caller-explicit (we do not dial just to stat); an explicit caller flag wins. */
|
|
1938
|
+
async function recursiveFlag(localPath, callerArgv, flag) {
|
|
1939
|
+
if (callerArgv.includes(flag) || callerArgv.includes("--recursive")) return [];
|
|
1940
|
+
try {
|
|
1941
|
+
if ((await stat(localPath)).isDirectory()) return [flag];
|
|
1942
|
+
} catch {}
|
|
1943
|
+
return [];
|
|
1944
|
+
}
|
|
1945
|
+
/** docker cp follows symlinks only with -L; its native error ("invalid symlink")
|
|
1946
|
+
* names no remedy. Append the one-line hint on that failure path (F5, 2026-10-14). */
|
|
1947
|
+
function withSymlinkHint(result) {
|
|
1948
|
+
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)`;
|
|
1949
|
+
}
|
|
1950
|
+
async function tempSshConfig(source, destination, runNative$1) {
|
|
1951
|
+
const resolvedSource = await resolveThroughOriginConfig(source, runNative$1);
|
|
1952
|
+
const resolvedDestination = await resolveThroughOriginConfig(destination, runNative$1);
|
|
1953
|
+
for (const { connection: c, identityFiles } of [resolvedSource, resolvedDestination]) {
|
|
1954
|
+
for (const [field, value] of [
|
|
1955
|
+
["host", c.host],
|
|
1956
|
+
["user", c.user],
|
|
1957
|
+
["key", c.key],
|
|
1958
|
+
["proxyJump", c.proxyJump]
|
|
1959
|
+
]) if (value !== void 0 && /[\r\n]/.test(value)) throw new Error(`remote: scp -3 temp config rejects newline in SSH ${field} (no config injection)`);
|
|
1960
|
+
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)");
|
|
1961
|
+
}
|
|
1815
1962
|
const dir = await mkdtemp(join(tmpdir(), "superd-scp3-"));
|
|
1816
1963
|
const path = join(dir, "config");
|
|
1817
|
-
const block = (name$1, c) => {
|
|
1964
|
+
const block = (name$1, c, identityFiles) => {
|
|
1818
1965
|
const lines = [`Host ${name$1}`, ` HostName ${c.host.replace(/^.*@/, "")}`];
|
|
1819
1966
|
if (c.user !== void 0) lines.push(` User ${c.user}`);
|
|
1820
1967
|
if (c.port !== void 0) lines.push(` Port ${String(c.port)}`);
|
|
1821
|
-
|
|
1968
|
+
for (const idFile of [c.key !== void 0 ? [c.key] : [], identityFiles].flat()) lines.push(` IdentityFile ${idFile}`);
|
|
1822
1969
|
if (c.proxyJump !== void 0) lines.push(` ProxyJump ${c.proxyJump}`);
|
|
1823
1970
|
return lines.join("\n");
|
|
1824
1971
|
};
|
|
1825
|
-
await writeFile(path, `${block("superd-scp3-source",
|
|
1972
|
+
await writeFile(path, `${block("superd-scp3-source", resolvedSource.connection, resolvedSource.identityFiles)}\n${block("superd-scp3-destination", resolvedDestination.connection, resolvedDestination.identityFiles)}\n`, { mode: 384 });
|
|
1826
1973
|
await chmod(path, 384);
|
|
1827
1974
|
return {
|
|
1828
1975
|
path,
|
|
1829
1976
|
dir
|
|
1830
1977
|
};
|
|
1831
1978
|
}
|
|
1979
|
+
/** Effective fields for one connection per the origin's own ssh config (ssh -G,
|
|
1980
|
+
* prints resolved settings without dialing). Explicit fields win; empty/none
|
|
1981
|
+
* proxyjump is dropped. IdentityFile PATHS from the config are returned separately
|
|
1982
|
+
* for inlining into the temp config — a path is not key material and stays on the
|
|
1983
|
+
* origin (F1, 2026-10-14: dev4's only authorized key lived in config, not the agent;
|
|
1984
|
+
* dropping it made scp -3 fail with a bare Permission denied). */
|
|
1985
|
+
async function resolveThroughOriginConfig(c, runNative$1) {
|
|
1986
|
+
let probe;
|
|
1987
|
+
try {
|
|
1988
|
+
probe = await runNative$1([
|
|
1989
|
+
"ssh",
|
|
1990
|
+
"-G",
|
|
1991
|
+
c.host
|
|
1992
|
+
], new AbortController().signal);
|
|
1993
|
+
} catch {
|
|
1994
|
+
probe = void 0;
|
|
1995
|
+
}
|
|
1996
|
+
if (probe === void 0 || probe.exit !== 0) return {
|
|
1997
|
+
connection: c,
|
|
1998
|
+
identityFiles: []
|
|
1999
|
+
};
|
|
2000
|
+
const lines = probe.stdout.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
2001
|
+
const pick$1 = (key) => lines.find((line) => line.startsWith(`${key} `))?.slice(key.length + 1).trim() || void 0;
|
|
2002
|
+
const home = process.env.HOME ?? "";
|
|
2003
|
+
const compiledDefault = (path) => path === "none" || [
|
|
2004
|
+
"id_rsa",
|
|
2005
|
+
"id_ecdsa",
|
|
2006
|
+
"id_ecdsa_sk",
|
|
2007
|
+
"id_ed25519",
|
|
2008
|
+
"id_ed25519_sk"
|
|
2009
|
+
].some((name$1) => path === `~/.ssh/${name$1}` || home !== "" && path === `${home}/.ssh/${name$1}`);
|
|
2010
|
+
const identityFiles = [...new Set(lines.filter((line) => line.startsWith("identityfile ")).map((line) => line.slice(13).trim()).filter((path) => path !== "" && !compiledDefault(path)))];
|
|
2011
|
+
const host = (pick$1("hostname") ?? c.host).replace(/^.*@/, "");
|
|
2012
|
+
const user = c.user ?? pick$1("user");
|
|
2013
|
+
const portText = pick$1("port");
|
|
2014
|
+
const port = c.port ?? (portText !== void 0 && /^\d+$/.test(portText) ? Number(portText) : void 0);
|
|
2015
|
+
const proxyJumpRaw = c.proxyJump ?? pick$1("proxyjump");
|
|
2016
|
+
return {
|
|
2017
|
+
connection: {
|
|
2018
|
+
host,
|
|
2019
|
+
...user !== void 0 && user !== "" ? { user } : {},
|
|
2020
|
+
...port !== void 0 && Number.isInteger(port) && port > 0 ? { port } : {},
|
|
2021
|
+
...c.key !== void 0 ? { key: c.key } : {},
|
|
2022
|
+
...proxyJumpRaw !== void 0 && proxyJumpRaw !== "" && proxyJumpRaw.toLowerCase() !== "none" ? { proxyJump: proxyJumpRaw } : {}
|
|
2023
|
+
},
|
|
2024
|
+
identityFiles
|
|
2025
|
+
};
|
|
2026
|
+
}
|
|
1832
2027
|
|
|
1833
2028
|
//#endregion
|
|
1834
2029
|
//#region src/remote/ssh-rsync-native-relay.ts
|
|
1835
2030
|
/** ssh argv for the relay: batch, no agent, no multiplexing. `command` is the
|
|
1836
|
-
* remote command; forwarding options go before `--`, the host after it.
|
|
2031
|
+
* remote command; forwarding options go before `--`, the host after it.
|
|
2032
|
+
* Identity core comes from ssh-argv.ts (the ONE policy — 2026-10-17 ruling). */
|
|
1837
2033
|
function relaySsh(connection, extra, command) {
|
|
1838
|
-
|
|
1839
|
-
"ssh",
|
|
2034
|
+
return sshCommandArgv(connection, [
|
|
1840
2035
|
"-o",
|
|
1841
2036
|
"BatchMode=yes",
|
|
1842
2037
|
"-T",
|
|
@@ -1853,14 +2048,7 @@ function relaySsh(connection, extra, command) {
|
|
|
1853
2048
|
"ServerAliveInterval=5",
|
|
1854
2049
|
"-o",
|
|
1855
2050
|
"ServerAliveCountMax=2"
|
|
1856
|
-
];
|
|
1857
|
-
if (connection.user !== void 0) args.push("-l", connection.user);
|
|
1858
|
-
if (connection.port !== void 0) args.push("-p", String(connection.port));
|
|
1859
|
-
if (connection.key !== void 0) args.push("-i", connection.key);
|
|
1860
|
-
if (connection.proxyJump !== void 0) args.push("-J", connection.proxyJump);
|
|
1861
|
-
args.push(...extra, "--", connection.host);
|
|
1862
|
-
if (command !== void 0) args.push(command);
|
|
1863
|
-
return args;
|
|
2051
|
+
], extra, command);
|
|
1864
2052
|
}
|
|
1865
2053
|
async function freePorts(count) {
|
|
1866
2054
|
const servers = [];
|
|
@@ -2015,12 +2203,16 @@ async function forwardRoute(argv, source, destination, signal, options) {
|
|
|
2015
2203
|
return await removeRemoteTemp(destination.connection, temp, wrap) ? "released" : "unknown";
|
|
2016
2204
|
};
|
|
2017
2205
|
try {
|
|
2206
|
+
signal.throwIfAborted();
|
|
2207
|
+
const daemonState = await waitForDaemonReady(destinationChild, 5e3);
|
|
2208
|
+
if (daemonState === "exited") throw new Error(`dual-SSH rsync destination daemon exited before serving${relayDetail(destinationChild, sourceHold)}`);
|
|
2209
|
+
if (daemonState === "timeout") throw new Error(`dual-SSH rsync destination daemon never announced readiness${relayDetail(destinationChild, sourceHold)}`);
|
|
2018
2210
|
signal.throwIfAborted();
|
|
2019
2211
|
if (!await originPortOpen(originPort, 15e3)) throw new Error(`dual-SSH rsync destination daemon never became reachable${relayDetail(destinationChild, sourceHold)}`);
|
|
2020
2212
|
signal.throwIfAborted();
|
|
2021
2213
|
const runArgv = relaySsh(source.connection, [], rsyncClientCommand(argv, source.path, sourcePort));
|
|
2022
2214
|
const result = await runNative(wrap(runArgv), signal);
|
|
2023
|
-
if (result.exit
|
|
2215
|
+
if (result.exit !== 0) result.stderr = (result.stderr + relayDetail(destinationChild, sourceHold)).slice(-OUTPUT_LIMIT);
|
|
2024
2216
|
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)}`);
|
|
2025
2217
|
return {
|
|
2026
2218
|
...result,
|
|
@@ -2045,6 +2237,19 @@ async function forwardRoute(argv, source, destination, signal, options) {
|
|
|
2045
2237
|
await Promise.allSettled([destinationChild.stop(), sourceHold.stop()]);
|
|
2046
2238
|
}
|
|
2047
2239
|
}
|
|
2240
|
+
/** Wait until the destination daemon script announces itself (stderr marker,
|
|
2241
|
+
* printed after its directory check and right before the daemon exec) or exits.
|
|
2242
|
+
* 'ready' only when the daemon is actually about to serve — closes the 0.3.1-e
|
|
2243
|
+
* race where -L opens at session start but the script dies ~150ms later. */
|
|
2244
|
+
async function waitForDaemonReady(held, budgetMs) {
|
|
2245
|
+
const deadline = Date.now() + budgetMs;
|
|
2246
|
+
for (;;) {
|
|
2247
|
+
if (/^SUPERD-RSYNC-TMP \S+$/m.test(held.stderrSink.text)) return "ready";
|
|
2248
|
+
if (held.child.exitCode !== null) return "exited";
|
|
2249
|
+
if (Date.now() >= deadline) return "timeout";
|
|
2250
|
+
await new Promise((resolve$1) => setTimeout(resolve$1, 50));
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2048
2253
|
function relayDetail(...held) {
|
|
2049
2254
|
const detail = held.map((child) => child.stderrSink.text).filter(Boolean).join("\n");
|
|
2050
2255
|
return detail ? `\n[relay stderr]\n${detail}` : "";
|
|
@@ -2126,14 +2331,7 @@ async function runRsync(request, source, destination, signal, options = {}) {
|
|
|
2126
2331
|
checkAbsolute(destination.path);
|
|
2127
2332
|
const argv = checkArgv("argv", request.argv);
|
|
2128
2333
|
if ("pullArgv" in request || "pushArgv" in request) throw new Error("remote: rsync takes argv only; pullArgv/pushArgv are copy-only fields");
|
|
2129
|
-
for (const
|
|
2130
|
-
"dryRun",
|
|
2131
|
-
"delete",
|
|
2132
|
-
"excludes",
|
|
2133
|
-
"preserve",
|
|
2134
|
-
"verify",
|
|
2135
|
-
"mkdir"
|
|
2136
|
-
]) 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)`);
|
|
2334
|
+
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)`);
|
|
2137
2335
|
if (!source.connection && !destination.connection) throw new Error("rsync requires at least one SSH endpoint; local-only is unsupported");
|
|
2138
2336
|
let result;
|
|
2139
2337
|
let route;
|
|
@@ -2181,6 +2379,9 @@ async function runRsync(request, source, destination, signal, options = {}) {
|
|
|
2181
2379
|
dst: request.dst,
|
|
2182
2380
|
argv,
|
|
2183
2381
|
route,
|
|
2382
|
+
verification: "native-exit-only",
|
|
2383
|
+
ok: result.exit === 0,
|
|
2384
|
+
...result.exit === 0 ? {} : { reason: signal.aborted ? "aborted" : "non-zero-exit" },
|
|
2184
2385
|
command: `rsync ${argv.join(" ")}`,
|
|
2185
2386
|
...itemized.length > 0 || argv.includes("--itemize-changes") || argv.includes("-i") ? { itemizedChanges: itemized } : {},
|
|
2186
2387
|
...literalBytes === void 0 ? {} : { literalBytes },
|
|
@@ -2195,21 +2396,14 @@ async function runRsync(request, source, destination, signal, options = {}) {
|
|
|
2195
2396
|
* placeholder host rsync appends and execs the canonical SSH argv. All tokens
|
|
2196
2397
|
* are tool-built argv (identity options/aliases), never model text. */
|
|
2197
2398
|
function rsyncRsh(connection, wrap) {
|
|
2198
|
-
const args = [
|
|
2199
|
-
"ssh",
|
|
2200
|
-
"-o",
|
|
2201
|
-
"BatchMode=yes",
|
|
2202
|
-
"-T"
|
|
2203
|
-
];
|
|
2204
|
-
if (connection.user !== void 0) args.push("-l", connection.user);
|
|
2205
|
-
if (connection.port !== void 0) args.push("-p", String(connection.port));
|
|
2206
|
-
if (connection.key !== void 0) args.push("-i", connection.key);
|
|
2207
|
-
if (connection.proxyJump !== void 0) args.push("-J", connection.proxyJump);
|
|
2208
|
-
args.push("--", connection.host);
|
|
2209
2399
|
return [
|
|
2210
2400
|
"bash",
|
|
2211
2401
|
"-c",
|
|
2212
|
-
`shift; exec ${wrap(
|
|
2402
|
+
`shift; exec ${wrap(sshCommandArgv(connection, [
|
|
2403
|
+
"-o",
|
|
2404
|
+
"BatchMode=yes",
|
|
2405
|
+
"-T"
|
|
2406
|
+
], [])).map(rshQuote).join(" ")} "$@"`,
|
|
2213
2407
|
"rsync-rsh"
|
|
2214
2408
|
].map(rshQuote).join(" ");
|
|
2215
2409
|
}
|
|
@@ -2305,26 +2499,19 @@ var SshTargets = class {
|
|
|
2305
2499
|
}
|
|
2306
2500
|
};
|
|
2307
2501
|
/** Shared by execution, probes and file/relay consumers. extraArgs is trusted
|
|
2308
|
-
* internal SSH argv (e.g. forwarding), never a model-provided shell string.
|
|
2502
|
+
* internal SSH argv (e.g. forwarding), never a model-provided shell string.
|
|
2503
|
+
* Identity assembly lives in ssh-argv.ts (the ONE policy — 2026-10-17 ruling);
|
|
2504
|
+
* this wrapper only picks the tty/batch dialect base. */
|
|
2309
2505
|
function sshArgv(connection, options = {}) {
|
|
2310
|
-
|
|
2311
|
-
"ssh",
|
|
2506
|
+
return sshCommandArgv(connection, options.tty ? [
|
|
2312
2507
|
"-tt",
|
|
2313
2508
|
"-o",
|
|
2314
2509
|
"BatchMode=yes"
|
|
2315
2510
|
] : [
|
|
2316
|
-
"ssh",
|
|
2317
2511
|
"-o",
|
|
2318
2512
|
"BatchMode=yes",
|
|
2319
2513
|
"-T"
|
|
2320
|
-
];
|
|
2321
|
-
if (connection.user !== void 0) args.push("-l", connection.user);
|
|
2322
|
-
if (connection.port !== void 0) args.push("-p", String(connection.port));
|
|
2323
|
-
if (connection.key !== void 0) args.push("-i", connection.key);
|
|
2324
|
-
if (connection.proxyJump !== void 0) args.push("-J", connection.proxyJump);
|
|
2325
|
-
args.push(...options.extraArgs ?? [], "--", connection.host);
|
|
2326
|
-
if (options.command !== void 0) args.push(options.command);
|
|
2327
|
-
return args;
|
|
2514
|
+
], options.extraArgs ?? [], options.command);
|
|
2328
2515
|
}
|
|
2329
2516
|
|
|
2330
2517
|
//#endregion
|
|
@@ -2381,6 +2568,96 @@ async function runOneShot(argv, opts = {}) {
|
|
|
2381
2568
|
};
|
|
2382
2569
|
}
|
|
2383
2570
|
|
|
2571
|
+
//#endregion
|
|
2572
|
+
//#region src/remote/helper.ts
|
|
2573
|
+
/** Reads helper-bin/MANIFEST.json next to the compiled module (lib/remote in the npm
|
|
2574
|
+
* package, super-dsh/remote in the Super closure — same relative layout). Missing or
|
|
2575
|
+
* malformed assets ⇒ null ⇒ shim-only mode; never throws. */
|
|
2576
|
+
function helperAssets(baseDir) {
|
|
2577
|
+
const dir = baseDir ?? join(dirname(fileURLToPath(import.meta.url)), "helper-bin");
|
|
2578
|
+
try {
|
|
2579
|
+
const manifest = JSON.parse(readFileSync(join(dir, "MANIFEST.json"), "utf8"));
|
|
2580
|
+
if (manifest?.format !== 1 || typeof manifest.ver !== "string" || typeof manifest.protocol !== "number" || !manifest.targets || typeof manifest.targets !== "object") return null;
|
|
2581
|
+
return {
|
|
2582
|
+
dir,
|
|
2583
|
+
manifest
|
|
2584
|
+
};
|
|
2585
|
+
} catch {
|
|
2586
|
+
return null;
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
/** `uname -s`/`uname -m` → shipped target triple. Unknown combos ⇒ null (shim). */
|
|
2590
|
+
function archTriple(sys, machine, shipped) {
|
|
2591
|
+
const s = sys.trim().toLowerCase(), m = machine.trim().toLowerCase();
|
|
2592
|
+
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;
|
|
2593
|
+
if (candidates === null) return null;
|
|
2594
|
+
return shipped.has(candidates) ? candidates : null;
|
|
2595
|
+
}
|
|
2596
|
+
function planKey(plan) {
|
|
2597
|
+
switch (plan.kind) {
|
|
2598
|
+
case "ssh": {
|
|
2599
|
+
const c = plan.connection;
|
|
2600
|
+
const host = c?.host ?? plan.host;
|
|
2601
|
+
return `ssh|${c?.user ? c.user + "@" : ""}${host}${c?.port ? ":" + c.port : ""}${c?.proxyJump ? "|" + c.proxyJump : ""}`;
|
|
2602
|
+
}
|
|
2603
|
+
case "docker": return `docker|${plan.container}`;
|
|
2604
|
+
case "incus": return `incus|${plan.container}`;
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
const PROBE_RE = /^SUPERD-PROBE (\S+) (\S+)$/m;
|
|
2608
|
+
const READY_RE = /^SUPERD-READY (\S+)$/m;
|
|
2609
|
+
var HelperRegistry = class {
|
|
2610
|
+
states = /* @__PURE__ */ new Map();
|
|
2611
|
+
constructor(assets, exec, probeArgv, uploadArgv) {
|
|
2612
|
+
this.assets = assets;
|
|
2613
|
+
this.exec = exec;
|
|
2614
|
+
this.probeArgv = probeArgv;
|
|
2615
|
+
this.uploadArgv = uploadArgv;
|
|
2616
|
+
}
|
|
2617
|
+
/** Never throws: any bootstrap failure resolves null (sticky) and the caller stays on the shim. */
|
|
2618
|
+
ensure(plan) {
|
|
2619
|
+
if (plan.kind !== "ssh" && plan.kind !== "docker" && plan.kind !== "incus") return Promise.resolve(null);
|
|
2620
|
+
const key = planKey(plan);
|
|
2621
|
+
let entry = this.states.get(key);
|
|
2622
|
+
if (entry === void 0) {
|
|
2623
|
+
entry = this.bootstrap(plan).catch(() => null);
|
|
2624
|
+
entry.catch(() => {});
|
|
2625
|
+
this.states.set(key, entry);
|
|
2626
|
+
}
|
|
2627
|
+
return entry;
|
|
2628
|
+
}
|
|
2629
|
+
async bootstrap(plan) {
|
|
2630
|
+
const { manifest } = this.assets;
|
|
2631
|
+
const first = await this.exec(this.probeArgv(plan, this.assets), { timeoutSec: 15 });
|
|
2632
|
+
const ready = this.parseReady(first.stdout, manifest.ver, manifest.protocol);
|
|
2633
|
+
if (ready !== null) return ready;
|
|
2634
|
+
const probe = PROBE_RE.exec(first.stdout);
|
|
2635
|
+
if (probe === null || first.exit !== 90 && first.exit !== 0) return null;
|
|
2636
|
+
const triple = archTriple(probe[1], probe[2], new Set(Object.keys(manifest.targets)));
|
|
2637
|
+
if (triple === null) return null;
|
|
2638
|
+
const target = manifest.targets[triple];
|
|
2639
|
+
const binary = await readFile(join(this.assets.dir, triple, target.file));
|
|
2640
|
+
if ((await this.exec(this.uploadArgv(plan, triple, target.file, this.assets), {
|
|
2641
|
+
stdin: binary,
|
|
2642
|
+
timeoutSec: 60
|
|
2643
|
+
})).exit !== 0) return null;
|
|
2644
|
+
const second = await this.exec(this.probeArgv(plan, this.assets), { timeoutSec: 15 });
|
|
2645
|
+
return this.parseReady(second.stdout, manifest.ver, manifest.protocol);
|
|
2646
|
+
}
|
|
2647
|
+
parseReady(stdout, ver, protocol) {
|
|
2648
|
+
const ready = READY_RE.exec(stdout);
|
|
2649
|
+
const banner = /^SUPERD-RUN proto=(\d+) name=superd-run ver=(\S+) target=(\S+)/m.exec(stdout);
|
|
2650
|
+
if (ready === null || banner === null) return null;
|
|
2651
|
+
if (banner[1] !== String(protocol) || banner[2] !== ver) return null;
|
|
2652
|
+
return {
|
|
2653
|
+
binPath: ready[1],
|
|
2654
|
+
triple: banner[3],
|
|
2655
|
+
ver,
|
|
2656
|
+
protocol
|
|
2657
|
+
};
|
|
2658
|
+
}
|
|
2659
|
+
};
|
|
2660
|
+
|
|
2384
2661
|
//#endregion
|
|
2385
2662
|
//#region src/remote/transports.ts
|
|
2386
2663
|
/** POSIX 单引号机械转义(唯一一层转义,spec §三"一层机械转义")。 */
|
|
@@ -2460,6 +2737,104 @@ function buildSpawnArgv(spawnCommand) {
|
|
|
2460
2737
|
"/dev/null"
|
|
2461
2738
|
];
|
|
2462
2739
|
}
|
|
2740
|
+
/** superd-run helper argv (spec: docs/specs/remote-rescope §5). Probe and upload are single
|
|
2741
|
+
* `sh -c` hops (busybox/alpine/darwin-safe); run/killgroup over ssh go through exactly one
|
|
2742
|
+
* remote-shell parse, same discipline as buildOneShotArgv. */
|
|
2743
|
+
function buildHelperProbeArgv(plan, assets) {
|
|
2744
|
+
const { ver, targets } = assets.manifest;
|
|
2745
|
+
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`);
|
|
2746
|
+
}
|
|
2747
|
+
function buildHelperUploadArgv(plan, triple, file, assets) {
|
|
2748
|
+
const dir = `$HOME/.cache/superd/run/${triple}-${assets.manifest.ver}`;
|
|
2749
|
+
return helperShellArgv(plan, `mkdir -p "${dir}" && cat > "${dir}/${file}.tmp" && chmod 0755 "${dir}/${file}.tmp" && mv -f "${dir}/${file}.tmp" "${dir}/${file}"`);
|
|
2750
|
+
}
|
|
2751
|
+
function buildHelperOneShotArgv(plan, binPath, options) {
|
|
2752
|
+
const { tag, cmd } = options;
|
|
2753
|
+
switch (plan.kind) {
|
|
2754
|
+
case "ssh": return sshArgv(plan.connection ?? { host: plan.host }, { command: `exec ${shQuote(binPath)} run --tag ${shQuote(tag)} -- bash -lc ${shQuote(cmd)}` });
|
|
2755
|
+
case "docker": return [
|
|
2756
|
+
"docker",
|
|
2757
|
+
"exec",
|
|
2758
|
+
"-i",
|
|
2759
|
+
"--",
|
|
2760
|
+
plan.container,
|
|
2761
|
+
binPath,
|
|
2762
|
+
"run",
|
|
2763
|
+
"--tag",
|
|
2764
|
+
tag,
|
|
2765
|
+
"--",
|
|
2766
|
+
"bash",
|
|
2767
|
+
"-lc",
|
|
2768
|
+
cmd
|
|
2769
|
+
];
|
|
2770
|
+
case "incus": return [
|
|
2771
|
+
"incus",
|
|
2772
|
+
"exec",
|
|
2773
|
+
"--force-noninteractive",
|
|
2774
|
+
plan.container,
|
|
2775
|
+
"--",
|
|
2776
|
+
binPath,
|
|
2777
|
+
"run",
|
|
2778
|
+
"--tag",
|
|
2779
|
+
tag,
|
|
2780
|
+
"--",
|
|
2781
|
+
"bash",
|
|
2782
|
+
"-lc",
|
|
2783
|
+
cmd
|
|
2784
|
+
];
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
function buildHelperKillgroupArgv(plan, binPath, tag) {
|
|
2788
|
+
switch (plan.kind) {
|
|
2789
|
+
case "ssh": return sshArgv(plan.connection ?? { host: plan.host }, { command: `${shQuote(binPath)} killgroup --tag ${shQuote(tag)}` });
|
|
2790
|
+
case "docker": return [
|
|
2791
|
+
"docker",
|
|
2792
|
+
"exec",
|
|
2793
|
+
"--",
|
|
2794
|
+
plan.container,
|
|
2795
|
+
binPath,
|
|
2796
|
+
"killgroup",
|
|
2797
|
+
"--tag",
|
|
2798
|
+
tag
|
|
2799
|
+
];
|
|
2800
|
+
case "incus": return [
|
|
2801
|
+
"incus",
|
|
2802
|
+
"exec",
|
|
2803
|
+
"--force-noninteractive",
|
|
2804
|
+
plan.container,
|
|
2805
|
+
"--",
|
|
2806
|
+
binPath,
|
|
2807
|
+
"killgroup",
|
|
2808
|
+
"--tag",
|
|
2809
|
+
tag
|
|
2810
|
+
];
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
function helperShellArgv(plan, script) {
|
|
2814
|
+
switch (plan.kind) {
|
|
2815
|
+
case "ssh": return sshArgv(plan.connection ?? { host: plan.host }, { command: script });
|
|
2816
|
+
case "docker": return [
|
|
2817
|
+
"docker",
|
|
2818
|
+
"exec",
|
|
2819
|
+
"-i",
|
|
2820
|
+
"--",
|
|
2821
|
+
plan.container,
|
|
2822
|
+
"sh",
|
|
2823
|
+
"-c",
|
|
2824
|
+
script
|
|
2825
|
+
];
|
|
2826
|
+
case "incus": return [
|
|
2827
|
+
"incus",
|
|
2828
|
+
"exec",
|
|
2829
|
+
"--force-noninteractive",
|
|
2830
|
+
plan.container,
|
|
2831
|
+
"--",
|
|
2832
|
+
"sh",
|
|
2833
|
+
"-c",
|
|
2834
|
+
script
|
|
2835
|
+
];
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2463
2838
|
|
|
2464
2839
|
//#endregion
|
|
2465
2840
|
//#region src/remote/nonce-framing.ts
|
|
@@ -3189,6 +3564,9 @@ var RemoteDriver = class {
|
|
|
3189
3564
|
starting = /* @__PURE__ */ new Set();
|
|
3190
3565
|
fileAbort = new AbortController();
|
|
3191
3566
|
disposed = false;
|
|
3567
|
+
/** undefined = 未尝试;null = 资产缺失(shim-only);registry = 每 target sticky 状态机。 */
|
|
3568
|
+
helpers;
|
|
3569
|
+
runnerOf = /* @__PURE__ */ new WeakMap();
|
|
3192
3570
|
disposal;
|
|
3193
3571
|
constructor(opts = {}) {
|
|
3194
3572
|
this.opts = opts;
|
|
@@ -3295,13 +3673,18 @@ var RemoteDriver = class {
|
|
|
3295
3673
|
path: dst.path,
|
|
3296
3674
|
endpoint: localFileEndpoint()
|
|
3297
3675
|
}, combined);
|
|
3298
|
-
if (request.verify !== void 0
|
|
3676
|
+
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");
|
|
3299
3677
|
const planOf = (end) => end.kind === "cli" ? end.plan : end.kind === "local" ? { kind: "local" } : void 0;
|
|
3300
3678
|
const srcPlan = planOf(src), dstPlan = planOf(dst);
|
|
3301
3679
|
if (srcPlan === void 0 || dstPlan === void 0) throw new Error("copy: unreachable route resolution");
|
|
3680
|
+
const execArgvFor = (plan, script) => {
|
|
3681
|
+
const argv = helperShellArgv(plan, script);
|
|
3682
|
+
return this.opts.nativeArgvFor ? this.opts.nativeArgvFor(argv) : argv;
|
|
3683
|
+
};
|
|
3302
3684
|
return nativeCopy({
|
|
3303
3685
|
src: request.src,
|
|
3304
3686
|
dst: request.dst,
|
|
3687
|
+
...request.mkdir === void 0 ? {} : { mkdir: request.mkdir },
|
|
3305
3688
|
...request.argv === void 0 ? {} : { argv: request.argv },
|
|
3306
3689
|
...request.pullArgv === void 0 ? {} : { pullArgv: request.pullArgv },
|
|
3307
3690
|
...request.pushArgv === void 0 ? {} : { pushArgv: request.pushArgv }
|
|
@@ -3314,7 +3697,10 @@ var RemoteDriver = class {
|
|
|
3314
3697
|
plan: dstPlan,
|
|
3315
3698
|
path: dst.path
|
|
3316
3699
|
}
|
|
3317
|
-
}, combined, {
|
|
3700
|
+
}, combined, {
|
|
3701
|
+
wrapArgv: this.opts.nativeArgvFor,
|
|
3702
|
+
execArgvFor
|
|
3703
|
+
});
|
|
3318
3704
|
})();
|
|
3319
3705
|
this.starting.add(work);
|
|
3320
3706
|
work.then(() => this.starting.delete(work), () => this.starting.delete(work));
|
|
@@ -3381,12 +3767,32 @@ var RemoteDriver = class {
|
|
|
3381
3767
|
if (signal.aborted || this.disposed) process$1.cancel("owner closed during start");
|
|
3382
3768
|
return process$1;
|
|
3383
3769
|
}
|
|
3384
|
-
nativeProcess(plan, params, timeoutSec, signal = this.fileAbort.signal) {
|
|
3770
|
+
async nativeProcess(plan, params, timeoutSec, signal = this.fileAbort.signal) {
|
|
3385
3771
|
const cleanupTag = randomUUID().slice(0, 8);
|
|
3772
|
+
const helper = await this.ensureHelper(plan);
|
|
3773
|
+
if (helper !== null && !this.disposed) {
|
|
3774
|
+
const process$2 = startOneShot(buildHelperOneShotArgv(plan, helper.binPath, {
|
|
3775
|
+
tag: cleanupTag,
|
|
3776
|
+
cmd: params.cmd
|
|
3777
|
+
}), {
|
|
3778
|
+
stdin: params.stdin,
|
|
3779
|
+
timeoutSec
|
|
3780
|
+
});
|
|
3781
|
+
this.runnerOf.set(process$2, `superd-run ${helper.ver} (${helper.triple})`);
|
|
3782
|
+
process$2.done.then((result) => {
|
|
3783
|
+
if (!result.cancelled && !result.timedOut) return;
|
|
3784
|
+
startOneShot(buildHelperKillgroupArgv(plan, helper.binPath, cleanupTag), { timeoutSec: 15 }).done.catch(() => {});
|
|
3785
|
+
}).catch(() => {});
|
|
3786
|
+
const tracked = Promise.resolve(process$2);
|
|
3787
|
+
this.starting.add(tracked);
|
|
3788
|
+
tracked.then(() => this.starting.delete(tracked), () => this.starting.delete(tracked));
|
|
3789
|
+
return tracked;
|
|
3790
|
+
}
|
|
3386
3791
|
const process$1 = startOneShot((this.opts.oneshotArgvFor ?? buildOneShotArgv)(plan, params.cmd, { cleanupTag }), {
|
|
3387
3792
|
stdin: params.stdin,
|
|
3388
3793
|
timeoutSec
|
|
3389
3794
|
});
|
|
3795
|
+
this.runnerOf.set(process$1, "posix-shim");
|
|
3390
3796
|
process$1.done.then((result) => {
|
|
3391
3797
|
if (!result.cancelled && !result.timedOut) return;
|
|
3392
3798
|
const pidfile = `"\${TMPDIR:-/tmp}/superd-oneshot-${cleanupTag}.pgid"`;
|
|
@@ -3398,6 +3804,38 @@ var RemoteDriver = class {
|
|
|
3398
3804
|
work.then(() => this.starting.delete(work), () => this.starting.delete(work));
|
|
3399
3805
|
return work;
|
|
3400
3806
|
}
|
|
3807
|
+
/** Helper bootstrap is best-effort and sticky: any failure parks null for this driver
|
|
3808
|
+
* lifetime and the shim stays the path. First call to a target pays probe (+ upload). */
|
|
3809
|
+
async ensureHelper(plan) {
|
|
3810
|
+
if (this.helpers === void 0) {
|
|
3811
|
+
const assets = this.opts.helperAssetsFor?.() ?? helperAssets();
|
|
3812
|
+
this.helpers = assets === null ? null : new HelperRegistry(assets, (argv, options) => this.helperExec(argv, options), buildHelperProbeArgv, buildHelperUploadArgv);
|
|
3813
|
+
}
|
|
3814
|
+
if (this.helpers === null || this.disposed) return null;
|
|
3815
|
+
try {
|
|
3816
|
+
return await this.helpers.ensure(plan);
|
|
3817
|
+
} catch {
|
|
3818
|
+
return null;
|
|
3819
|
+
}
|
|
3820
|
+
}
|
|
3821
|
+
async helperExec(argv, options) {
|
|
3822
|
+
this.assertOpen();
|
|
3823
|
+
const stdin = options?.stdin;
|
|
3824
|
+
if (stdin !== void 0) {
|
|
3825
|
+
const proc$1 = startBinaryProcess(argv, { timeoutSec: options?.timeoutSec });
|
|
3826
|
+
proc$1.stdin.on("error", () => {});
|
|
3827
|
+
proc$1.stdin.end(stdin);
|
|
3828
|
+
return {
|
|
3829
|
+
exit: (await proc$1.done).exit,
|
|
3830
|
+
stdout: ""
|
|
3831
|
+
};
|
|
3832
|
+
}
|
|
3833
|
+
const proc = startOneShot(argv, { timeoutSec: options?.timeoutSec });
|
|
3834
|
+
return {
|
|
3835
|
+
exit: (await proc.done).exit,
|
|
3836
|
+
stdout: proc.read("stdout", 0).text
|
|
3837
|
+
};
|
|
3838
|
+
}
|
|
3401
3839
|
own(process$1) {
|
|
3402
3840
|
this.processes.add(process$1);
|
|
3403
3841
|
process$1.done.then(() => this.processes.delete(process$1));
|
|
@@ -3675,7 +4113,8 @@ var RemoteDriver = class {
|
|
|
3675
4113
|
stderr: err?.stats ?? r$1.outputStats?.stderr
|
|
3676
4114
|
} } : {},
|
|
3677
4115
|
durationMs: r$1.durationMs,
|
|
3678
|
-
...out$1.truncated !== void 0 ? { truncated: out$1.truncated } : {}
|
|
4116
|
+
...out$1.truncated !== void 0 ? { truncated: out$1.truncated } : {},
|
|
4117
|
+
...this.runnerOf.get(process$1) === void 0 ? {} : { runner: this.runnerOf.get(process$1) }
|
|
3679
4118
|
};
|
|
3680
4119
|
}
|
|
3681
4120
|
const argv = (this.opts.ptyArgvFor ?? buildPtyArgv)(routing.plan);
|
|
@@ -3838,9 +4277,9 @@ const REMOTE_TOOL_DESCRIPTION = `Remote execution and file transfer over ssh / d
|
|
|
3838
4277
|
{target:"byop:lbl", spawn, cmd} → create the named shell once; reuse as {target:"byop:lbl", cmd} (no spawn)
|
|
3839
4278
|
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.
|
|
3840
4279
|
|
|
3841
|
-
oneshot: stateless bash -lc, stdout/stderr separate, exit code verbatim
|
|
4280
|
+
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).
|
|
3842
4281
|
|
|
3843
|
-
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;
|
|
4282
|
+
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.
|
|
3844
4283
|
|
|
3845
4284
|
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.`;
|
|
3846
4285
|
function createRemoteTool(driver, opts = {}) {
|
|
@@ -3865,7 +4304,7 @@ function createRemoteTool(driver, opts = {}) {
|
|
|
3865
4304
|
argv: {
|
|
3866
4305
|
type: "array",
|
|
3867
4306
|
items: { type: "string" },
|
|
3868
|
-
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."
|
|
4307
|
+
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."
|
|
3869
4308
|
},
|
|
3870
4309
|
pullArgv: {
|
|
3871
4310
|
type: "array",
|
|
@@ -3879,11 +4318,11 @@ function createRemoteTool(driver, opts = {}) {
|
|
|
3879
4318
|
},
|
|
3880
4319
|
verify: {
|
|
3881
4320
|
type: "boolean",
|
|
3882
|
-
description: "copy only
|
|
4321
|
+
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."
|
|
3883
4322
|
},
|
|
3884
4323
|
mkdir: {
|
|
3885
4324
|
type: "boolean",
|
|
3886
|
-
description: "copy only,
|
|
4325
|
+
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."
|
|
3887
4326
|
},
|
|
3888
4327
|
run_in_background: {
|
|
3889
4328
|
type: "boolean",
|
|
@@ -3980,7 +4419,11 @@ function createRemoteTool(driver, opts = {}) {
|
|
|
3980
4419
|
outputStats: { type: "json" },
|
|
3981
4420
|
outputStatsUnavailable: { type: "boolean" },
|
|
3982
4421
|
session: { type: "string" },
|
|
3983
|
-
notice: { type: "string" }
|
|
4422
|
+
notice: { type: "string" },
|
|
4423
|
+
runner: {
|
|
4424
|
+
type: "string",
|
|
4425
|
+
description: "oneshot supervision path: superd-run <ver> (<triple>) or posix-shim"
|
|
4426
|
+
}
|
|
3984
4427
|
}
|
|
3985
4428
|
},
|
|
3986
4429
|
render: (_args, value) => [{
|
|
@@ -4002,12 +4445,26 @@ function createRemoteTool(driver, opts = {}) {
|
|
|
4002
4445
|
args.timeout,
|
|
4003
4446
|
args.run_in_background
|
|
4004
4447
|
].some((value) => value !== void 0)) throw new Error(`${args.action} cannot be combined with execution/PTY/job arguments; register byossh separately first`);
|
|
4005
|
-
|
|
4006
|
-
"
|
|
4007
|
-
"
|
|
4008
|
-
"
|
|
4009
|
-
"
|
|
4010
|
-
|
|
4448
|
+
const KNOWN_PARAMS = new Set([
|
|
4449
|
+
"action",
|
|
4450
|
+
"src",
|
|
4451
|
+
"dst",
|
|
4452
|
+
"argv",
|
|
4453
|
+
"pullArgv",
|
|
4454
|
+
"pushArgv",
|
|
4455
|
+
"verify",
|
|
4456
|
+
"mkdir",
|
|
4457
|
+
"run_in_background",
|
|
4458
|
+
"target",
|
|
4459
|
+
"ssh",
|
|
4460
|
+
"spawn",
|
|
4461
|
+
"cmd",
|
|
4462
|
+
"script",
|
|
4463
|
+
"mode",
|
|
4464
|
+
"stdin",
|
|
4465
|
+
"timeout"
|
|
4466
|
+
]);
|
|
4467
|
+
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`);
|
|
4011
4468
|
if (args.action === "rsync") {
|
|
4012
4469
|
if (args.pullArgv !== void 0 || args.pushArgv !== void 0) throw new Error("rsync takes argv only; pullArgv/pushArgv are copy-only fields");
|
|
4013
4470
|
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");
|
|
@@ -4017,15 +4474,17 @@ function createRemoteTool(driver, opts = {}) {
|
|
|
4017
4474
|
dst: args.dst,
|
|
4018
4475
|
...args.argv === void 0 ? {} : { argv: args.argv }
|
|
4019
4476
|
}, exec.signal, { sessionKey: getSessionKey(exec) });
|
|
4020
|
-
if (receipt$1.exit !== 0) throw new Error(JSON.stringify(receipt$1));
|
|
4477
|
+
if (receipt$1.ok === false || receipt$1.exit !== 0) throw new Error(JSON.stringify(receipt$1));
|
|
4478
|
+
const rsyncDuration = Date.now() - start;
|
|
4021
4479
|
return {
|
|
4022
4480
|
kind: "rsync",
|
|
4023
|
-
text:
|
|
4481
|
+
text: formatTransferLine("rsync", receipt$1, rsyncDuration),
|
|
4024
4482
|
receipt: receipt$1,
|
|
4025
4483
|
exit: receipt$1.exit,
|
|
4026
|
-
durationMs:
|
|
4484
|
+
durationMs: rsyncDuration
|
|
4027
4485
|
};
|
|
4028
4486
|
}
|
|
4487
|
+
const copyStart = Date.now();
|
|
4029
4488
|
const receipt = await driver.copy({
|
|
4030
4489
|
src: args.src,
|
|
4031
4490
|
dst: args.dst,
|
|
@@ -4035,12 +4494,16 @@ function createRemoteTool(driver, opts = {}) {
|
|
|
4035
4494
|
...args.pullArgv === void 0 ? {} : { pullArgv: args.pullArgv },
|
|
4036
4495
|
...args.pushArgv === void 0 ? {} : { pushArgv: args.pushArgv }
|
|
4037
4496
|
}, exec.signal, { sessionKey: getSessionKey(exec) });
|
|
4497
|
+
const asReceipt = receipt;
|
|
4498
|
+
const exits = asReceipt.exits ?? [];
|
|
4499
|
+
if (asReceipt.ok === false || exits.some((code) => code !== 0)) throw new Error(JSON.stringify(receipt));
|
|
4500
|
+
const copyDuration = Date.now() - copyStart;
|
|
4038
4501
|
return {
|
|
4039
4502
|
kind: "copy",
|
|
4040
|
-
text:
|
|
4503
|
+
text: formatTransferLine("copy", receipt, copyDuration),
|
|
4041
4504
|
receipt,
|
|
4042
|
-
exit: null,
|
|
4043
|
-
durationMs:
|
|
4505
|
+
exit: exits.length > 0 ? 0 : null,
|
|
4506
|
+
durationMs: copyDuration
|
|
4044
4507
|
};
|
|
4045
4508
|
}
|
|
4046
4509
|
if ([
|
|
@@ -4126,6 +4589,7 @@ function createRemoteTool(driver, opts = {}) {
|
|
|
4126
4589
|
if (r.outputStatsUnavailable) v.outputStatsUnavailable = true;
|
|
4127
4590
|
if (r.session !== void 0) v.session = r.session;
|
|
4128
4591
|
if (r.notice !== void 0) v.notice = r.notice;
|
|
4592
|
+
if (r.runner !== void 0) v.runner = r.runner;
|
|
4129
4593
|
return v;
|
|
4130
4594
|
}
|
|
4131
4595
|
});
|
|
@@ -4136,6 +4600,26 @@ function createRemoteTool(driver, opts = {}) {
|
|
|
4136
4600
|
});
|
|
4137
4601
|
return tool;
|
|
4138
4602
|
}
|
|
4603
|
+
/** S8 (2026-10-17): copy/rsync render as one formatted line; the full receipt
|
|
4604
|
+
* stays in the receipt field — raw-JSON dumps are gone from text. */
|
|
4605
|
+
function formatTransferLine(action, receipt, durationMs) {
|
|
4606
|
+
const r = receipt;
|
|
4607
|
+
const parts = [];
|
|
4608
|
+
if (r.ok === false) parts.push(`✗ ${r.reason ?? "failed"}`);
|
|
4609
|
+
parts.push(`${action} ${r.src} → ${r.dst}`);
|
|
4610
|
+
if (r.route !== void 0 && r.route !== "") parts.push(`route ${r.route}`);
|
|
4611
|
+
if (r.exits !== void 0) parts.push(`exits [${r.exits.join(",")}]`);
|
|
4612
|
+
else if (r.exit !== void 0) parts.push(`exit ${r.exit}`);
|
|
4613
|
+
if (r.verification !== void 0) parts.push(`verification ${r.verification}`);
|
|
4614
|
+
if (r.created === true) parts.push("created yes");
|
|
4615
|
+
else if (r.created === false) parts.push("created pre-existing");
|
|
4616
|
+
else if (r.created === "n-a") parts.push("created n-a");
|
|
4617
|
+
if (r.cleanup !== void 0 && r.cleanup !== "none-required") parts.push(`cleanup ${r.cleanup}`);
|
|
4618
|
+
if (typeof r.bytes === "number") parts.push(`${r.bytes} bytes / ${r.entries} entries`);
|
|
4619
|
+
parts.push(`${(durationMs / 1e3).toFixed(1)}s`);
|
|
4620
|
+
if (r.note !== void 0 && r.note.length > 0) parts.push(`(${r.note})`);
|
|
4621
|
+
return parts.join(" · ");
|
|
4622
|
+
}
|
|
4139
4623
|
function renderValue(v) {
|
|
4140
4624
|
if (v.kind === "status" || v.kind === "roster" || v.kind === "job" || v.kind === "copy" || v.kind === "rsync") return v.text;
|
|
4141
4625
|
const parts = [];
|
|
@@ -4150,7 +4634,8 @@ function renderValue(v) {
|
|
|
4150
4634
|
const suffix = markers.length > 0 ? ` · ${markers.join(" · ")}` : "";
|
|
4151
4635
|
const cwdTag = typeof v.cwd === "string" && v.cwd.length > 0 ? ` · cwd ${v.cwd}` : "";
|
|
4152
4636
|
const sessionTag = typeof v.session === "string" && v.session.length > 0 ? ` · ${v.session}` : "";
|
|
4153
|
-
|
|
4637
|
+
const runnerTag = typeof v.runner === "string" && v.runner.length > 0 ? ` · runner ${v.runner}` : "";
|
|
4638
|
+
parts.push(`[exit ${v.exit} · ${secs}s${suffix}${cwdTag}${sessionTag}${runnerTag}]`);
|
|
4154
4639
|
if (v.stderr !== void 0 && v.stderr.length > 0) parts.push("[stderr]", v.stderr);
|
|
4155
4640
|
return parts.join("\n");
|
|
4156
4641
|
}
|