dsh-plugin-wsl-env 0.15.0 → 0.17.0
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/CHANGELOG.md +144 -15
- package/CONTRIBUTING.md +25 -33
- package/README.md +10 -10
- package/SECURITY.md +6 -4
- package/SUPPORT.md +8 -8
- package/agent/native/linux-aarch64/dsh-wsl-agent +0 -0
- package/agent/native/linux-x86_64/dsh-wsl-agent +0 -0
- package/agent/wsl-agent.sh +51 -12
- package/cordis.patch.yml +23 -10
- package/docs/ARCHITECTURE.md +56 -41
- package/docs/COMPATIBILITY.md +121 -0
- package/docs/CONFIGURATION.md +275 -273
- package/docs/LIMITATIONS.md +114 -38
- package/docs/MIGRATION.md +16 -13
- package/docs/PARITY.md +4 -3
- package/docs/PERFORMANCE.md +11 -9
- package/docs/QUICKSTART.md +46 -21
- package/docs/README.zh.md +7 -7
- package/docs/RELEASING.md +257 -0
- package/docs/REQUIREMENTS.zh.md +77 -0
- package/docs/ROADMAP-1.0.md +105 -61
- package/docs/ROADMAP.md +15 -9
- package/docs/UPSTREAM-FILE-REFERENCE-SEAM.md +3 -3
- package/docs/UPSTREAM-FSIO-EXPORT.md +7 -7
- package/docs/UPSTREAM-OPEN-IN-APP.md +57 -0
- package/docs/UPSTREAM-SESSION-NOTICE.md +4 -4
- package/docs/UPSTREAM-TERMINAL-TITLE.md +10 -12
- package/docs/VALUE.zh.md +144 -0
- package/docs/adr/0002-native-agent.md +348 -0
- package/docs/root-fs-routing.md +186 -0
- package/docs/screenshots/picker.png +0 -0
- package/docs/screenshots/preview.png +0 -0
- package/docs/screenshots/sandbox.png +0 -0
- package/docs/screenshots/terminal.png +0 -0
- package/lib/agent-exec.d.ts +7 -3
- package/lib/agent-protocol.d.ts +18 -5
- package/lib/agent-protocol.js +11 -6
- package/lib/agent-shared.js +1 -0
- package/lib/agent.d.ts +28 -9
- package/lib/agent.js +74 -14
- package/lib/fs-routing.d.ts +2 -1
- package/lib/fs-routing.js +13 -1
- package/lib/fs-substrate.d.ts +11 -9
- package/lib/fs-substrate.js +6 -9
- package/lib/fsio-agent.d.ts +2 -3
- package/lib/fsio-agent.js +2 -3
- package/lib/git-exec.d.ts +117 -0
- package/lib/git-exec.js +105 -0
- package/lib/git-route.d.ts +43 -13
- package/lib/git-route.js +54 -15
- package/lib/index.d.ts +20 -13
- package/lib/index.js +18 -11
- package/lib/session-runtime.d.ts +5 -3
- package/lib/session-runtime.js +5 -4
- package/lib/subprocess.d.ts +21 -7
- package/lib/subprocess.js +63 -9
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +13 -4
- package/scripts/diagnose.sh +1 -1
- package/scripts/doctor.mjs +23 -3
package/lib/fs-routing.js
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
* @module dsh-plugin-wsl/fs-routing
|
|
32
32
|
*/
|
|
33
33
|
import { isWslUnc, uncToPosix } from "./paths.js";
|
|
34
|
+
import z from "@deepseek-ai/schemastery";
|
|
34
35
|
import { LocalFileSystem } from "@deepseek-ai/dsh-fs-local";
|
|
35
36
|
import { WslFileSystem } from "./index.js";
|
|
36
37
|
/**
|
|
@@ -101,7 +102,18 @@ export function inputIsDistro(value) {
|
|
|
101
102
|
* mode the distro side reports through `sandboxMode`).
|
|
102
103
|
*/
|
|
103
104
|
export class WslRoutingFileSystem extends LocalFileSystem {
|
|
104
|
-
static Config =
|
|
105
|
+
static Config = z.intersect([
|
|
106
|
+
LocalFileSystem.Config,
|
|
107
|
+
z.object({
|
|
108
|
+
distro: z.string().default(""),
|
|
109
|
+
wslPath: z.string().default("wsl.exe"),
|
|
110
|
+
distroCwd: z.string().default(""),
|
|
111
|
+
sandbox: z.boolean().default(true),
|
|
112
|
+
maskWindowsDrive: z.boolean().default(false),
|
|
113
|
+
restrictToDistro: z.boolean().default(true),
|
|
114
|
+
watchMaxDepth: z.natural().default(0),
|
|
115
|
+
}),
|
|
116
|
+
]);
|
|
105
117
|
/**
|
|
106
118
|
* The routed backend's constructor reads the deployment default mode from
|
|
107
119
|
* `ctx.sandboxPolicy`, exactly as the shipped `fs-sandbox` row this class
|
package/lib/fs-substrate.d.ts
CHANGED
|
@@ -13,12 +13,9 @@
|
|
|
13
13
|
* create publishes with the distro-side no-replace link, a real atomic
|
|
14
14
|
* primitive that refuses to replace. An overwrite or edit carries the
|
|
15
15
|
* version it was based on INTO the write op, and the agent re-verifies it
|
|
16
|
-
* with a fresh stat
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* narrows the share backend's check-then-write window from a host round
|
|
20
|
-
* trip to kernel-adjacent — the honest wording; "closed" was true only
|
|
21
|
-
* for the create case.
|
|
16
|
+
* with a fresh stat before the rename: an earlier conflicting write is
|
|
17
|
+
* refused with `FS_STALE_VERSION`. A writer between that stat and the
|
|
18
|
+
* rename can still be overwritten; only guarded create is atomic.
|
|
22
19
|
* 2. A symlink needs no retry machinery: every identity is resolved where the
|
|
23
20
|
* symlinks live.
|
|
24
21
|
*
|
|
@@ -59,6 +56,7 @@ export declare class AgentSubstrate {
|
|
|
59
56
|
agentFor: ((policy: {
|
|
60
57
|
mode: string;
|
|
61
58
|
workspaceRoot: string;
|
|
59
|
+
sessionId?: string;
|
|
62
60
|
} | undefined) => Promise<WslAgent>) | undefined;
|
|
63
61
|
/** The distro-side filesystem face. */
|
|
64
62
|
fs: DistroFs;
|
|
@@ -83,6 +81,7 @@ export declare class AgentSubstrate {
|
|
|
83
81
|
agentFor?: (policy: {
|
|
84
82
|
mode: string;
|
|
85
83
|
workspaceRoot: string;
|
|
84
|
+
sessionId?: string;
|
|
86
85
|
} | undefined) => Promise<WslAgent>;
|
|
87
86
|
});
|
|
88
87
|
/**
|
|
@@ -95,6 +94,7 @@ export declare class AgentSubstrate {
|
|
|
95
94
|
mutationAgent(policy: {
|
|
96
95
|
mode: string;
|
|
97
96
|
workspaceRoot: string;
|
|
97
|
+
sessionId?: string;
|
|
98
98
|
} | undefined): Promise<WslAgent>;
|
|
99
99
|
/**
|
|
100
100
|
* Run `op` with exclusive access to `targetKey` (FIFO per key).
|
|
@@ -204,7 +204,7 @@ export declare class AgentSubstrate {
|
|
|
204
204
|
* guard, best-effort diff basis, atomic publication, outcome shape. The
|
|
205
205
|
* guard is FORWARDED — a guarded create publishes with the distro-side
|
|
206
206
|
* no-replace link (atomic), and a `replaceIfVersion` write carries its
|
|
207
|
-
* version into the op, where the agent re-verifies it
|
|
207
|
+
* version into the op, where the agent re-verifies it before
|
|
208
208
|
* the rename and refuses with `FS_STALE_VERSION` on a mismatch.
|
|
209
209
|
*
|
|
210
210
|
* @param target - the resolved target.
|
|
@@ -220,6 +220,7 @@ export declare class AgentSubstrate {
|
|
|
220
220
|
} | undefined, signal?: AbortSignal, policy?: {
|
|
221
221
|
mode: string;
|
|
222
222
|
workspaceRoot: string;
|
|
223
|
+
sessionId?: string;
|
|
223
224
|
} | undefined): Promise<{
|
|
224
225
|
operation: string;
|
|
225
226
|
version: string;
|
|
@@ -230,8 +231,8 @@ export declare class AgentSubstrate {
|
|
|
230
231
|
* Edit a text file with the host backend's orchestration: probe, version
|
|
231
232
|
* guard, LF-normalized read, literal replacement, line-ending restore,
|
|
232
233
|
* atomic publication, outcome shape. The version the edit was based on
|
|
233
|
-
* rides the write op and is re-verified distro-side
|
|
234
|
-
* rename,
|
|
234
|
+
* rides the write op and is re-verified distro-side before the
|
|
235
|
+
* rename, narrowing but not eliminating the concurrent-write race.
|
|
235
236
|
*
|
|
236
237
|
* @param target - the resolved target.
|
|
237
238
|
* @param edit - the literal request.
|
|
@@ -249,6 +250,7 @@ export declare class AgentSubstrate {
|
|
|
249
250
|
} | undefined, signal?: AbortSignal, policy?: {
|
|
250
251
|
mode: string;
|
|
251
252
|
workspaceRoot: string;
|
|
253
|
+
sessionId?: string;
|
|
252
254
|
} | undefined): Promise<{
|
|
253
255
|
version: string;
|
|
254
256
|
before: string;
|
package/lib/fs-substrate.js
CHANGED
|
@@ -13,12 +13,9 @@
|
|
|
13
13
|
* create publishes with the distro-side no-replace link, a real atomic
|
|
14
14
|
* primitive that refuses to replace. An overwrite or edit carries the
|
|
15
15
|
* version it was based on INTO the write op, and the agent re-verifies it
|
|
16
|
-
* with a fresh stat
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* narrows the share backend's check-then-write window from a host round
|
|
20
|
-
* trip to kernel-adjacent — the honest wording; "closed" was true only
|
|
21
|
-
* for the create case.
|
|
16
|
+
* with a fresh stat before the rename: an earlier conflicting write is
|
|
17
|
+
* refused with `FS_STALE_VERSION`. A writer between that stat and the
|
|
18
|
+
* rename can still be overwritten; only guarded create is atomic.
|
|
22
19
|
* 2. A symlink needs no retry machinery: every identity is resolved where the
|
|
23
20
|
* symlinks live.
|
|
24
21
|
*
|
|
@@ -196,7 +193,7 @@ export class AgentSubstrate {
|
|
|
196
193
|
* guard, best-effort diff basis, atomic publication, outcome shape. The
|
|
197
194
|
* guard is FORWARDED — a guarded create publishes with the distro-side
|
|
198
195
|
* no-replace link (atomic), and a `replaceIfVersion` write carries its
|
|
199
|
-
* version into the op, where the agent re-verifies it
|
|
196
|
+
* version into the op, where the agent re-verifies it before
|
|
200
197
|
* the rename and refuses with `FS_STALE_VERSION` on a mismatch.
|
|
201
198
|
*
|
|
202
199
|
* @param target - the resolved target.
|
|
@@ -247,8 +244,8 @@ export class AgentSubstrate {
|
|
|
247
244
|
* Edit a text file with the host backend's orchestration: probe, version
|
|
248
245
|
* guard, LF-normalized read, literal replacement, line-ending restore,
|
|
249
246
|
* atomic publication, outcome shape. The version the edit was based on
|
|
250
|
-
* rides the write op and is re-verified distro-side
|
|
251
|
-
* rename,
|
|
247
|
+
* rides the write op and is re-verified distro-side before the
|
|
248
|
+
* rename, narrowing but not eliminating the concurrent-write race.
|
|
252
249
|
*
|
|
253
250
|
* @param target - the resolved target.
|
|
254
251
|
* @param edit - the literal request.
|
package/lib/fsio-agent.d.ts
CHANGED
|
@@ -244,9 +244,8 @@ export declare class DistroFs {
|
|
|
244
244
|
* Publish `content` over `linuxPath` the POSIX way: the agent stages into a
|
|
245
245
|
* private 0700 sibling, fsyncs, chmods, and renames — or hard links for a
|
|
246
246
|
* guarded create. A `replace` publish carrying `expectedVersion` is
|
|
247
|
-
* re-verified by the agent against a fresh stat
|
|
248
|
-
*
|
|
249
|
-
* stale write refuses with `FS_STALE_VERSION` instead of landing.
|
|
247
|
+
* re-verified by the agent against a fresh stat before the rename. A change
|
|
248
|
+
* after that check can still be overwritten by the rename.
|
|
250
249
|
*
|
|
251
250
|
* @param linuxPath - the target path inside the distro.
|
|
252
251
|
* @param content - the full new content.
|
package/lib/fsio-agent.js
CHANGED
|
@@ -407,9 +407,8 @@ export class DistroFs {
|
|
|
407
407
|
* Publish `content` over `linuxPath` the POSIX way: the agent stages into a
|
|
408
408
|
* private 0700 sibling, fsyncs, chmods, and renames — or hard links for a
|
|
409
409
|
* guarded create. A `replace` publish carrying `expectedVersion` is
|
|
410
|
-
* re-verified by the agent against a fresh stat
|
|
411
|
-
*
|
|
412
|
-
* stale write refuses with `FS_STALE_VERSION` instead of landing.
|
|
410
|
+
* re-verified by the agent against a fresh stat before the rename. A change
|
|
411
|
+
* after that check can still be overwritten by the rename.
|
|
413
412
|
*
|
|
414
413
|
* @param linuxPath - the target path inside the distro.
|
|
415
414
|
* @param content - the full new content.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The agent-backed execution handle for the git-snapshot spawn.
|
|
3
|
+
*
|
|
4
|
+
* `workspace-changes`' `GitRunner` consumes a small slice of the subprocess
|
|
5
|
+
* handle: `handle.done` for `{exitCode, signal}`, then
|
|
6
|
+
* `handle.collected.stdout/stderr.readFrom(0)` for `{text, lossy}` — with
|
|
7
|
+
* per-stream caps applied by the caller's `stdio` spec (a generous stdout
|
|
8
|
+
* budget and a 16 KiB stderr tail). This module builds exactly that surface
|
|
9
|
+
* around one resident-agent `exec`, so a distro snapshot pays the warm agent's
|
|
10
|
+
* ~2 ms round trip per git call instead of the one-shot `wsl.exe`'s ~230 ms
|
|
11
|
+
* process startup — measured 2026-11-01 on `ubuntu`; a full snapshot's 6-8
|
|
12
|
+
* calls (plus per-file `cat-file`) used to spend over a second inside
|
|
13
|
+
* `wsl.exe` alone.
|
|
14
|
+
*
|
|
15
|
+
* The caps map onto the agent's single per-request capture ceiling as the
|
|
16
|
+
* larger of the two stream budgets; the reader then slices each stream to its
|
|
17
|
+
* own budget and reports `lossy` from that slice, so the caller's contract
|
|
18
|
+
* (per-stream cut flags) holds even though the wire carries one cap.
|
|
19
|
+
*
|
|
20
|
+
* Environment rides the agent's per-request `env` (an `env --` argv prefix —
|
|
21
|
+
* no `SETENV` global pollution), holding only the translated `GIT_*` names:
|
|
22
|
+
* the one-shot path forwards exactly those through `WSLENV`, and forwarding a
|
|
23
|
+
* host `PATH` into the distro would be wrong, not just different.
|
|
24
|
+
*
|
|
25
|
+
* The agent being out is the fallback cue, exactly as `search-exec.ts`:
|
|
26
|
+
* `spawnFallback` (the caller's one-shot `wsl.exe` handle) is consulted
|
|
27
|
+
* lazily inside `done`, and from that point the facade proxies the delegate
|
|
28
|
+
* verbatim. Cancellation shares one channel: the caller's `signal` and
|
|
29
|
+
* `kill()` abort the agent exec, settling `done` as killed.
|
|
30
|
+
*
|
|
31
|
+
* Pure shaping over injected collaborators, unit-testable without a distro.
|
|
32
|
+
*
|
|
33
|
+
* This is a TypeScript source built to `lib/git-exec.js`; edit THIS file and
|
|
34
|
+
* run `pnpm run build` — the artifact under `lib/` is generated, and
|
|
35
|
+
* `pnpm test` fails when it drifts.
|
|
36
|
+
*
|
|
37
|
+
* @module dsh-plugin-wsl/git-exec
|
|
38
|
+
*/
|
|
39
|
+
import type { AgentExecRunner } from "./agent-exec.js";
|
|
40
|
+
/** One collected-stream reader with the seam's standard shape. */
|
|
41
|
+
export interface GitStreamReader {
|
|
42
|
+
/** Read the stream from a byte offset; `lossy` says the cap cut it. */
|
|
43
|
+
readFrom: (from: number) => {
|
|
44
|
+
text: string;
|
|
45
|
+
lossy: boolean;
|
|
46
|
+
nextOffset: number;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The slice of the subprocess handle `GitRunner` consumes: settle on `done`,
|
|
51
|
+
* then read the collected streams.
|
|
52
|
+
*/
|
|
53
|
+
export interface GitExecutionHandle {
|
|
54
|
+
/** Settles with the exit code and signal; rejects on a genuine failure. */
|
|
55
|
+
done: Promise<{
|
|
56
|
+
exitCode: number | null;
|
|
57
|
+
signal: string | null;
|
|
58
|
+
}>;
|
|
59
|
+
/** The collected stream readers; a delegated handle's own on fallback. */
|
|
60
|
+
collected: {
|
|
61
|
+
stdout?: GitStreamReader | undefined;
|
|
62
|
+
stderr?: GitStreamReader | undefined;
|
|
63
|
+
};
|
|
64
|
+
/** Cooperative kill; the runner relies on its `signal` for cancellation. */
|
|
65
|
+
kill?: () => boolean | void;
|
|
66
|
+
}
|
|
67
|
+
/** Per-stream capture budgets the caller declared on its `stdio` spec. */
|
|
68
|
+
export interface GitStreamCaps {
|
|
69
|
+
/** The stdout budget in bytes; the snapshot's `outputMaxBytes`. */
|
|
70
|
+
stdout: number;
|
|
71
|
+
/** The stderr tail budget in bytes. */
|
|
72
|
+
stderr: number;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Run one git command through the agent and present it as a git-snapshot
|
|
76
|
+
* handle.
|
|
77
|
+
*
|
|
78
|
+
* @param object - the handle's collaborators.
|
|
79
|
+
* @returns the snapshot handle (`done`, `collected`, `kill`).
|
|
80
|
+
*/
|
|
81
|
+
export declare function gitExecutionHandle({ agent, cwd, argv, env, stdin, caps, distro, discovery, signal, spawnFallback }: {
|
|
82
|
+
/** The {@link WslAgent}-like runner; throws `AgentUnavailableError` when out. */
|
|
83
|
+
agent: AgentExecRunner;
|
|
84
|
+
/** The Linux working directory to snapshot from. */
|
|
85
|
+
cwd: string;
|
|
86
|
+
/** The distro-side argv (`git` and its arguments). */
|
|
87
|
+
argv: string[];
|
|
88
|
+
/**
|
|
89
|
+
* The distro name; required when `discovery` is set (absolute path lines
|
|
90
|
+
* become `\\wsl.localhost\<distro><path>`).
|
|
91
|
+
*/
|
|
92
|
+
distro?: string;
|
|
93
|
+
/**
|
|
94
|
+
* True for a SENTINEL discovery call (`--show-toplevel` /
|
|
95
|
+
* `--absolute-git-dir` / `--git-path` answered by the distro git): the
|
|
96
|
+
* captured stdout's absolute Linux path lines are translated into UNC form
|
|
97
|
+
* before the caller reads them. Snapshot calls leave this unset.
|
|
98
|
+
*/
|
|
99
|
+
discovery?: boolean;
|
|
100
|
+
/**
|
|
101
|
+
* Per-request environment: the caller's entries with path-shaped values
|
|
102
|
+
* translated into distro form. Empty entries are dropped; nothing but
|
|
103
|
+
* `GIT_*` names should reach here.
|
|
104
|
+
*/
|
|
105
|
+
env: Record<string, string>;
|
|
106
|
+
/** The child's entire stdin, or undefined for an empty one. */
|
|
107
|
+
stdin?: Uint8Array;
|
|
108
|
+
/** Per-stream capture budgets from the caller's `stdio` spec. */
|
|
109
|
+
caps: GitStreamCaps;
|
|
110
|
+
/** The caller's cancellation (its own timeout races in through `AbortSignal.any`). */
|
|
111
|
+
signal?: AbortSignal;
|
|
112
|
+
/**
|
|
113
|
+
* Builds the one-shot handle the facade delegates to when the agent is out;
|
|
114
|
+
* called at most once, lazily.
|
|
115
|
+
*/
|
|
116
|
+
spawnFallback: () => GitExecutionHandle;
|
|
117
|
+
}): GitExecutionHandle;
|
package/lib/git-exec.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The agent-backed execution handle for the git-snapshot spawn.
|
|
3
|
+
*
|
|
4
|
+
* `workspace-changes`' `GitRunner` consumes a small slice of the subprocess
|
|
5
|
+
* handle: `handle.done` for `{exitCode, signal}`, then
|
|
6
|
+
* `handle.collected.stdout/stderr.readFrom(0)` for `{text, lossy}` — with
|
|
7
|
+
* per-stream caps applied by the caller's `stdio` spec (a generous stdout
|
|
8
|
+
* budget and a 16 KiB stderr tail). This module builds exactly that surface
|
|
9
|
+
* around one resident-agent `exec`, so a distro snapshot pays the warm agent's
|
|
10
|
+
* ~2 ms round trip per git call instead of the one-shot `wsl.exe`'s ~230 ms
|
|
11
|
+
* process startup — measured 2026-11-01 on `ubuntu`; a full snapshot's 6-8
|
|
12
|
+
* calls (plus per-file `cat-file`) used to spend over a second inside
|
|
13
|
+
* `wsl.exe` alone.
|
|
14
|
+
*
|
|
15
|
+
* The caps map onto the agent's single per-request capture ceiling as the
|
|
16
|
+
* larger of the two stream budgets; the reader then slices each stream to its
|
|
17
|
+
* own budget and reports `lossy` from that slice, so the caller's contract
|
|
18
|
+
* (per-stream cut flags) holds even though the wire carries one cap.
|
|
19
|
+
*
|
|
20
|
+
* Environment rides the agent's per-request `env` (an `env --` argv prefix —
|
|
21
|
+
* no `SETENV` global pollution), holding only the translated `GIT_*` names:
|
|
22
|
+
* the one-shot path forwards exactly those through `WSLENV`, and forwarding a
|
|
23
|
+
* host `PATH` into the distro would be wrong, not just different.
|
|
24
|
+
*
|
|
25
|
+
* The agent being out is the fallback cue, exactly as `search-exec.ts`:
|
|
26
|
+
* `spawnFallback` (the caller's one-shot `wsl.exe` handle) is consulted
|
|
27
|
+
* lazily inside `done`, and from that point the facade proxies the delegate
|
|
28
|
+
* verbatim. Cancellation shares one channel: the caller's `signal` and
|
|
29
|
+
* `kill()` abort the agent exec, settling `done` as killed.
|
|
30
|
+
*
|
|
31
|
+
* Pure shaping over injected collaborators, unit-testable without a distro.
|
|
32
|
+
*
|
|
33
|
+
* This is a TypeScript source built to `lib/git-exec.js`; edit THIS file and
|
|
34
|
+
* run `pnpm run build` — the artifact under `lib/` is generated, and
|
|
35
|
+
* `pnpm test` fails when it drifts.
|
|
36
|
+
*
|
|
37
|
+
* @module dsh-plugin-wsl/git-exec
|
|
38
|
+
*/
|
|
39
|
+
import { AgentUnavailableError } from "./agent-errors.js";
|
|
40
|
+
import { translateDiscoveryOutput } from "./git-route.js";
|
|
41
|
+
/**
|
|
42
|
+
* Run one git command through the agent and present it as a git-snapshot
|
|
43
|
+
* handle.
|
|
44
|
+
*
|
|
45
|
+
* @param object - the handle's collaborators.
|
|
46
|
+
* @returns the snapshot handle (`done`, `collected`, `kill`).
|
|
47
|
+
*/
|
|
48
|
+
export function gitExecutionHandle({ agent, cwd, argv, env, stdin, caps, distro, discovery, signal, spawnFallback }) {
|
|
49
|
+
const state = { delegate: undefined, stdout: undefined, stderr: undefined };
|
|
50
|
+
const controller = new AbortController();
|
|
51
|
+
if (signal?.aborted)
|
|
52
|
+
controller.abort(signal.reason);
|
|
53
|
+
else
|
|
54
|
+
signal?.addEventListener("abort", () => controller.abort(signal.reason), { once: true });
|
|
55
|
+
// The agent enforces ONE cap per request; declare the larger budget on the
|
|
56
|
+
// wire and cut each stream to its own budget here, so the caller's
|
|
57
|
+
// per-stream contract holds and a stream under its cap is never flagged.
|
|
58
|
+
const wireCap = Math.max(caps.stdout, caps.stderr);
|
|
59
|
+
const reader = (slot) => {
|
|
60
|
+
const cap = slot === "stdout" ? caps.stdout : caps.stderr;
|
|
61
|
+
return {
|
|
62
|
+
readFrom(from) {
|
|
63
|
+
const buffer = state[slot] ?? Buffer.alloc(0);
|
|
64
|
+
const lossy = buffer.length > cap;
|
|
65
|
+
const visible = lossy ? buffer.subarray(0, cap) : buffer;
|
|
66
|
+
const start = Math.min(from, visible.length);
|
|
67
|
+
return { text: visible.subarray(start).toString("utf8"), lossy, nextOffset: visible.length };
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
const handle = {
|
|
72
|
+
get collected() {
|
|
73
|
+
if (state.delegate !== undefined)
|
|
74
|
+
return state.delegate.collected;
|
|
75
|
+
return { stdout: reader("stdout"), stderr: reader("stderr") };
|
|
76
|
+
},
|
|
77
|
+
done: (async () => {
|
|
78
|
+
let result;
|
|
79
|
+
try {
|
|
80
|
+
result = await agent.exec({ cwd, argv, env, stdin, maxOutputBytes: wireCap, signal: controller.signal });
|
|
81
|
+
state.stdout = discovery ? translateDiscoveryOutput(distro ?? "", result.stdout) : result.stdout;
|
|
82
|
+
state.stderr = result.stderr;
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
if (!(error instanceof AgentUnavailableError)) {
|
|
86
|
+
if (controller.signal.aborted) {
|
|
87
|
+
// Our kill() or the caller's cancellation: the killed outcome the
|
|
88
|
+
// one-shot path reports, not a failure.
|
|
89
|
+
return { exitCode: null, signal: "SIGTERM" };
|
|
90
|
+
}
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
const delegate = spawnFallback();
|
|
94
|
+
state.delegate = delegate;
|
|
95
|
+
return delegate.done;
|
|
96
|
+
}
|
|
97
|
+
return { exitCode: result.exitCode, signal: null };
|
|
98
|
+
})(),
|
|
99
|
+
kill() {
|
|
100
|
+
controller.abort(new Error("killed"));
|
|
101
|
+
state.delegate?.kill?.();
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
return handle;
|
|
105
|
+
}
|
package/lib/git-route.d.ts
CHANGED
|
@@ -13,19 +13,28 @@
|
|
|
13
13
|
* ## What is routed, and what deliberately is not
|
|
14
14
|
*
|
|
15
15
|
* A spawn is routed only when the working directory names the distro (a
|
|
16
|
-
* `\\wsl.localhost
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
16
|
+
* `\\wsl.localhost\\<distro>` UNC). Inside that, two cases:
|
|
17
|
+
*
|
|
18
|
+
* - **Snapshot calls** (`add`, `write-tree`, `ls-tree`, `cat-file`,
|
|
19
|
+
* `diff-tree`, `ls-files`, `check-ignore`) route whenever the program is
|
|
20
|
+
* git — the runs are fully isolated from the user's git state
|
|
21
|
+
* (`GIT_INDEX_FILE` / `GIT_OBJECT_DIRECTORY` /
|
|
22
|
+
* `GIT_ALTERNATE_OBJECT_DIRECTORIES` pointing at the tool's private
|
|
23
|
+
* scratch), so the distro git mutates scratch, never the repository.
|
|
24
|
+
* - **Discovery calls** (`--show-toplevel`, `--absolute-git-dir`, `--git-path`)
|
|
25
|
+
* print absolute paths that the caller resolves against the UNC workspace,
|
|
26
|
+
* so a distro answer in Linux paths would corrupt that resolution. They
|
|
27
|
+
* route only when the program is the SENTINEL path `DISTRO_GIT_SENTINEL` —
|
|
28
|
+
* the guard `resolveExecutable` returns when the HOST has no git — and the
|
|
29
|
+
* facade translates the output lines into UNC form before the caller sees
|
|
30
|
+
* them (`translateDiscoveryOutput`). A host-resolved git keeps the shipped
|
|
31
|
+
* host-side discovery, which answers UNC natively (and needs the documented
|
|
32
|
+
* `safe.directory` whitelist for WSL shares).
|
|
33
|
+
*
|
|
34
|
+
* The split means a machine without Windows git still gets full WSL change
|
|
35
|
+
* review: the sentinel resolves, every spawn routes, nothing touches the
|
|
36
|
+
* host. See `lib/git-exec.js` for the facade side.
|
|
37
|
+
*
|
|
29
38
|
*
|
|
30
39
|
* ## Environment translation
|
|
31
40
|
*
|
|
@@ -44,6 +53,14 @@
|
|
|
44
53
|
*
|
|
45
54
|
* @module dsh-plugin-wsl/git-route
|
|
46
55
|
*/
|
|
56
|
+
/**
|
|
57
|
+
* The program path `resolveExecutable` returns for git when the HOST has no
|
|
58
|
+
* git: a Linux absolute path that only makes sense inside the distro, so any
|
|
59
|
+
* spawn carrying it must route (and a discovery spawn's output must come back
|
|
60
|
+
* translated). Chosen to match a standard distro install so the argv stays
|
|
61
|
+
* honest even where it is read verbatim.
|
|
62
|
+
*/
|
|
63
|
+
export declare const DISTRO_GIT_SENTINEL = "/usr/bin/git";
|
|
47
64
|
/**
|
|
48
65
|
* Whether a spawn's program is git.
|
|
49
66
|
*
|
|
@@ -97,4 +114,17 @@ export declare function gitSpawnRewrite({ argv, cwd, wslPath }: {
|
|
|
97
114
|
linuxCwd: string;
|
|
98
115
|
gitArgv: string[];
|
|
99
116
|
wslArgv: string[];
|
|
117
|
+
discovery?: boolean;
|
|
100
118
|
} | undefined;
|
|
119
|
+
/**
|
|
120
|
+
* Translate one routed discovery call's stdout into the caller's UNC
|
|
121
|
+
* coordinate system: every absolute Linux path line becomes
|
|
122
|
+
* `\\wsl.localhost\<distro><path>`; relative lines (`--git-path` answers
|
|
123
|
+
* `.git/objects` when the repository is not bare) pass through untouched,
|
|
124
|
+
* because the caller resolves them against the UNC cwd natively.
|
|
125
|
+
*
|
|
126
|
+
* @param distro - the distro the discovery ran in.
|
|
127
|
+
* @param stdout - the distro git's captured stdout.
|
|
128
|
+
* @returns the stdout with absolute path lines rewritten.
|
|
129
|
+
*/
|
|
130
|
+
export declare function translateDiscoveryOutput(distro: string, stdout: Buffer): Buffer;
|
package/lib/git-route.js
CHANGED
|
@@ -13,19 +13,28 @@
|
|
|
13
13
|
* ## What is routed, and what deliberately is not
|
|
14
14
|
*
|
|
15
15
|
* A spawn is routed only when the working directory names the distro (a
|
|
16
|
-
* `\\wsl.localhost
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
16
|
+
* `\\wsl.localhost\\<distro>` UNC). Inside that, two cases:
|
|
17
|
+
*
|
|
18
|
+
* - **Snapshot calls** (`add`, `write-tree`, `ls-tree`, `cat-file`,
|
|
19
|
+
* `diff-tree`, `ls-files`, `check-ignore`) route whenever the program is
|
|
20
|
+
* git — the runs are fully isolated from the user's git state
|
|
21
|
+
* (`GIT_INDEX_FILE` / `GIT_OBJECT_DIRECTORY` /
|
|
22
|
+
* `GIT_ALTERNATE_OBJECT_DIRECTORIES` pointing at the tool's private
|
|
23
|
+
* scratch), so the distro git mutates scratch, never the repository.
|
|
24
|
+
* - **Discovery calls** (`--show-toplevel`, `--absolute-git-dir`, `--git-path`)
|
|
25
|
+
* print absolute paths that the caller resolves against the UNC workspace,
|
|
26
|
+
* so a distro answer in Linux paths would corrupt that resolution. They
|
|
27
|
+
* route only when the program is the SENTINEL path `DISTRO_GIT_SENTINEL` —
|
|
28
|
+
* the guard `resolveExecutable` returns when the HOST has no git — and the
|
|
29
|
+
* facade translates the output lines into UNC form before the caller sees
|
|
30
|
+
* them (`translateDiscoveryOutput`). A host-resolved git keeps the shipped
|
|
31
|
+
* host-side discovery, which answers UNC natively (and needs the documented
|
|
32
|
+
* `safe.directory` whitelist for WSL shares).
|
|
33
|
+
*
|
|
34
|
+
* The split means a machine without Windows git still gets full WSL change
|
|
35
|
+
* review: the sentinel resolves, every spawn routes, nothing touches the
|
|
36
|
+
* host. See `lib/git-exec.js` for the facade side.
|
|
37
|
+
*
|
|
29
38
|
*
|
|
30
39
|
* ## Environment translation
|
|
31
40
|
*
|
|
@@ -47,7 +56,15 @@
|
|
|
47
56
|
import { isWslUnc, uncToPosix, windowsToLinuxMount } from "./paths.js";
|
|
48
57
|
/** The one program this rewrite passes through to the distro. */
|
|
49
58
|
const GIT_PROGRAM = "git";
|
|
50
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* The program path `resolveExecutable` returns for git when the HOST has no
|
|
61
|
+
* git: a Linux absolute path that only makes sense inside the distro, so any
|
|
62
|
+
* spawn carrying it must route (and a discovery spawn's output must come back
|
|
63
|
+
* translated). Chosen to match a standard distro install so the argv stays
|
|
64
|
+
* honest even where it is read verbatim.
|
|
65
|
+
*/
|
|
66
|
+
export const DISTRO_GIT_SENTINEL = "/usr/bin/git";
|
|
67
|
+
/** Discovery flags print absolute paths — the caller resolves them. */
|
|
51
68
|
const DISCOVERY_FLAGS = ["--show-toplevel", "--absolute-git-dir", "--git-path"];
|
|
52
69
|
/**
|
|
53
70
|
* Whether a spawn's program is git.
|
|
@@ -125,7 +142,8 @@ export function translateGitEnv(env) {
|
|
|
125
142
|
export function gitSpawnRewrite({ argv, cwd, wslPath }) {
|
|
126
143
|
if (!Array.isArray(argv) || !isGitProgram(argv[0]))
|
|
127
144
|
return undefined;
|
|
128
|
-
|
|
145
|
+
const sentinel = argv[0] === DISTRO_GIT_SENTINEL;
|
|
146
|
+
if (namesDiscoveryPaths(argv) && !sentinel)
|
|
129
147
|
return undefined;
|
|
130
148
|
const named = uncToPosix(cwd);
|
|
131
149
|
if (named === undefined)
|
|
@@ -137,5 +155,26 @@ export function gitSpawnRewrite({ argv, cwd, wslPath }) {
|
|
|
137
155
|
linuxCwd,
|
|
138
156
|
gitArgv,
|
|
139
157
|
wslArgv: [wslPath, "-d", named.distro, "--cd", linuxCwd, "--exec", ...gitArgv],
|
|
158
|
+
discovery: sentinel && namesDiscoveryPaths(argv),
|
|
140
159
|
};
|
|
141
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* Translate one routed discovery call's stdout into the caller's UNC
|
|
163
|
+
* coordinate system: every absolute Linux path line becomes
|
|
164
|
+
* `\\wsl.localhost\<distro><path>`; relative lines (`--git-path` answers
|
|
165
|
+
* `.git/objects` when the repository is not bare) pass through untouched,
|
|
166
|
+
* because the caller resolves them against the UNC cwd natively.
|
|
167
|
+
*
|
|
168
|
+
* @param distro - the distro the discovery ran in.
|
|
169
|
+
* @param stdout - the distro git's captured stdout.
|
|
170
|
+
* @returns the stdout with absolute path lines rewritten.
|
|
171
|
+
*/
|
|
172
|
+
export function translateDiscoveryOutput(distro, stdout) {
|
|
173
|
+
const text = stdout.toString("utf8");
|
|
174
|
+
if (!text.includes("\n") && !text.startsWith("/"))
|
|
175
|
+
return stdout;
|
|
176
|
+
const translated = text.split("\n").map((line) => line.startsWith("/")
|
|
177
|
+
? `\\\\wsl.localhost\\${distro}${line.replaceAll("/", "\\")}`
|
|
178
|
+
: line);
|
|
179
|
+
return Buffer.from(translated.join("\n"), "utf8");
|
|
180
|
+
}
|