dsh-plugin-wsl-env 0.6.0 → 0.7.2
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 +176 -0
- package/README.md +6 -6
- package/cordis.patch.yml +48 -0
- package/docs/ARCHITECTURE.md +33 -20
- package/docs/CONFIGURATION.md +2 -2
- package/docs/LIMITATIONS.md +52 -54
- package/docs/PARITY.md +18 -4
- package/docs/README.zh.md +7 -7
- package/lib/fs-decisions.js +22 -25
- package/lib/fs-routing.js +176 -0
- package/lib/git-route.js +142 -0
- package/lib/index.js +77 -307
- package/lib/listing.js +47 -0
- package/lib/picker.js +121 -17
- package/lib/search-exec.js +114 -0
- package/lib/search-route.js +95 -0
- package/lib/subprocess.js +153 -16
- package/lib/terminal-activity.js +121 -0
- package/lib/workspace-files-route.js +205 -0
- package/lib/workspace-files-wsl.js +341 -0
- package/lib/wsl.js +0 -39
- package/package.json +22 -3
- package/lib/fs-publish.js +0 -42
package/docs/README.zh.md
CHANGED
|
@@ -57,18 +57,18 @@ dsh --profile wsl # 4. 启动
|
|
|
57
57
|
| `wsl-fs` | `distro` | `''` | 同上 |
|
|
58
58
|
| | `restrictToDistro` | `true` | 拒绝指向**其它子系统**共享的路径;`/mnt/c` 属于本子系统内部,不受影响。拒绝码为 `FS_OUTSIDE_DISTRO`,它不是沙箱拒绝,放宽权限也无法解除 |
|
|
59
59
|
| | `sandbox` | `true` | 写入时按策略检查 `writeText` 和 `editText` |
|
|
60
|
-
| | `substrate` | `agent` | 文件工具使用哪种 I/O
|
|
60
|
+
| | `substrate` | `agent` | 文件工具使用哪种 I/O 基座。只剩常驻子系统内代理一种:读写与路径解析直接在 ext4 上完成(原生符号链接、原生权限位,写入 guard 存活到发布时刻,受限策略下由内核强制)。原 `"share"` 退出项——Windows 侧宿主文件栈走 9p 共享——已在启动时拒绝,文件工具不再经过共享 |
|
|
61
61
|
| `directory-picker-wsl` | `includeHostHome` | `true` | 同时列出 Windows 家目录 |
|
|
62
62
|
| `subprocess-wsl` | `distro` | `''` | GUI 终端开在哪个子系统 |
|
|
63
63
|
|
|
64
|
-
[`cordis.patch.yml`](../cordis.patch.yml) 是每个随包值的注释参考。[docs/CONFIGURATION.md](CONFIGURATION.md) 列出其余键,包括 `shell`、`loginShell`、`cwd`、`timeoutMs`、`
|
|
64
|
+
[`cordis.patch.yml`](../cordis.patch.yml) 是每个随包值的注释参考。[docs/CONFIGURATION.md](CONFIGURATION.md) 列出其余键,包括 `shell`、`loginShell`、`cwd`、`timeoutMs`、`preferredDistro` 和 `maxEntries`;[examples/profile.cordis.patch.yml](../examples/profile.cordis.patch.yml) 是一份可照抄的本机层。
|
|
65
65
|
|
|
66
66
|
## 配方
|
|
67
67
|
|
|
68
68
|
- **Git 凭据共享**:让子系统里的 git 使用 Windows 侧的 Git Credential Manager,避免每次输密码:
|
|
69
69
|
`git config --global credential.helper "/mnt/c/Program\ Files/Git/mingw64/bin/git-credential-manager.exe"`
|
|
70
70
|
(路径按 Windows 侧 Git 的安装位置调整;WSL2 的 localhost 转发是平台行为,子系统内监听的端口 Windows 直接可达。)
|
|
71
|
-
- **路径与性能**:模型看到并操作的是子系统内的 Linux 路径(`/home/...`),写入子系统自身的 ext4;`/mnt/c` 通向 Windows 磁盘但走 9p,大批量小文件操作明显慢——重 IO 的项目请放在子系统文件系统内。`npm run bootstrap -- <子系统>` 会一并报告 ripgrep
|
|
71
|
+
- **路径与性能**:模型看到并操作的是子系统内的 Linux 路径(`/home/...`),写入子系统自身的 ext4;`/mnt/c` 通向 Windows 磁盘但走 9p,大批量小文件操作明显慢——重 IO 的项目请放在子系统文件系统内。`npm run bootstrap -- <子系统>` 会一并报告 ripgrep、git、inotifywait(搜索、快照与监视的后端)是否就位。
|
|
72
72
|
- **WSLENV 透传**:WSL 只导入 `WSLENV` 中列出的变量。本插件按前缀放行托管的 `DSH_*` 命名空间,其中带 Windows 路径的两个(`DSH_HOME`、`DSH_PROFILE_DIR`)加 `/p` 让 WSL 翻译成 `/mnt/c/...`。`PATH` 故意不透传——否则 Windows 的 PATH 会覆盖子系统自身的 PATH。
|
|
73
73
|
|
|
74
74
|
## 架构
|
|
@@ -86,12 +86,12 @@ dsh --profile wsl # 4. 启动
|
|
|
86
86
|
|
|
87
87
|
preset-wsl(wsl agent preset;其服务运行在 isolate realm 内)
|
|
88
88
|
├─ wsl-shell ctx.shell — wsl.exe --exec <登录 shell>,在子系统内经 bubblewrap 约束
|
|
89
|
-
└─ wsl-fs ctx.fs —
|
|
89
|
+
└─ wsl-fs ctx.fs — 子系统的真实文件;常驻代理基座落在 ext4
|
|
90
90
|
```
|
|
91
91
|
|
|
92
92
|
**为什么分两层。** `wsl-shell` 和 `wsl-fs` 位于 `wsl` agent preset 内,`auto-preset` 在会话工作区位于子系统内时自动绑定它:Windows 目录的会话继续使用原生 provider,子系统会话拿到 WSL provider——环境是会话的属性,而不是进程的。终端控制器是例外:它经根上下文解析执行世界,永远看不到 preset 的 isolate realm,所以 `subprocess-wsl` 必须挂在组合层。
|
|
93
93
|
|
|
94
|
-
**命令与文件。** 命令以 `wsl.exe -d <子系统> --cd <Linux 目录> --exec <登录 shell> -lc <命令>` 执行,外层是子系统内的 bubblewrap profile,参数与 DSH 自家 Linux runner 完全一致——约束语义与报错文案都和 Linux
|
|
94
|
+
**命令与文件。** 命令以 `wsl.exe -d <子系统> --cd <Linux 目录> --exec <登录 shell> -lc <命令>` 执行,外层是子系统内的 bubblewrap profile,参数与 DSH 自家 Linux runner 完全一致——约束语义与报错文案都和 Linux 主机相同。文件工具读写子系统的真实文件,基座只有常驻子系统内代理一种——读写与路径解析落在 ext4,原生符号链接与权限位;文件搜索的 spawn 以同样的方式改写:子系统工作区的搜索跑子系统自己的 `rg`(`wsl.exe --exec`),绝不让 Windows 侧二进制走 9p 共享。写入与命令沙箱使用同一份策略检查。
|
|
95
95
|
|
|
96
96
|
## 沙箱
|
|
97
97
|
|
|
@@ -120,8 +120,8 @@ preset-wsl(wsl agent preset;其服务运行在 isolate realm 内)
|
|
|
120
120
|
| 终端打开后仍然是 `cmd.exe` | 这一层里的 `terminal-controller` 行没有生效 | 用 `dsh --profile wsl --dump-config` 确认能看到 `shell: { path: wsl.exe, name: WSL }` |
|
|
121
121
|
| 改了 `lib/` 但不生效 | ES module 缓存 | 重启应用 |
|
|
122
122
|
| 用 `link:\\wsl.localhost\...` 安装后符号链接是坏的 | pnpm 无法链接 UNC 路径 | 改成链接 Windows 路径;在子系统内开发时用运行时镜像,见[开发](#开发) |
|
|
123
|
-
| `glob`/`grep` 很慢 |
|
|
124
|
-
| 终端活动显示 `unknown` |
|
|
123
|
+
| `glob`/`grep` 很慢 | 子系统内没有 rg 时,搜索只能报 rg 自己的 "command not found";Windows 目录的搜索走宿主原生 rg,不受影响 | 运行 `npm run bootstrap -- <子系统> --install`(安装 ripgrep);子系统工作区的搜索始终跑子系统内的 rg,绝不遍历 9p 共享 |
|
|
124
|
+
| 终端活动显示 `unknown` | 仅在常驻代理不可用时出现——distro 终端从子系统内部观测(`/proc` 中扫描 `DSH_TERMINAL_ID` 标记:shell 独处为 `idle`,运行任何命令为 `busy`) | 确认子系统在运行;空闲终端会在控制器的无人值守超时(默认 2 小时)后自动回收,`terminalIdleReclaim: false` 恢复手动关闭 |
|
|
125
125
|
| 结果里出现 `FS_*` 码 | 码本身说明了是谁拒绝的、以及怎样解除 | 见 [docs/ARCHITECTURE.md](ARCHITECTURE.md#error-codes) 的错误码表 |
|
|
126
126
|
|
|
127
127
|
## 开发
|
package/lib/fs-decisions.js
CHANGED
|
@@ -79,37 +79,33 @@ export function workspaceWriteDenial(displayPath) {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
/**
|
|
82
|
-
* The
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* @
|
|
82
|
+
* The substrate configuration decision: the Windows-side share no longer
|
|
83
|
+
* serves the file tools. Every distro read and write runs on the resident
|
|
84
|
+
* agent, so no model-facing I/O crosses the 9p share at all — the share
|
|
85
|
+
* backend it replaces was the one path that still could. The provider throws
|
|
86
|
+
* this at construction time, before any session forms, so a profile carrying
|
|
87
|
+
* the old value fails loudly with the migration instead of silently serving
|
|
88
|
+
* I/O the configuration did not mean to allow.
|
|
89
|
+
* @param {string} substrate - the configured substrate value.
|
|
90
|
+
* @returns {{message: string}|null} the refusal, or null to proceed.
|
|
90
91
|
*/
|
|
91
|
-
export function
|
|
92
|
-
if (
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
if (expected.kind === "createIfAbsent" && existing !== undefined) {
|
|
101
|
-
return refusal(`cannot overwrite existing "${displayPath}" without reading it first`, "FS_NOT_OBSERVED");
|
|
102
|
-
}
|
|
103
|
-
return null;
|
|
92
|
+
export function shareSubstrateRefusal(substrate) {
|
|
93
|
+
if (substrate !== "share") return null;
|
|
94
|
+
return {
|
|
95
|
+
message:
|
|
96
|
+
'wsl-fs: substrate "share" is no longer available — the resident agent serves every ' +
|
|
97
|
+
"distro read and write, and no file tool crosses the 9p share. Delete the substrate " +
|
|
98
|
+
'line from the provider config, or set it to "agent".',
|
|
99
|
+
};
|
|
104
100
|
}
|
|
105
101
|
|
|
106
102
|
/**
|
|
107
103
|
* The substrate failure mapping: what the tool layer should surface for an
|
|
108
104
|
* error that crossed the `ctx.fs` seam. A coded refusal keeps its code and
|
|
109
105
|
* message byte-for-byte; anything else is the substrate being UNAVAILABLE —
|
|
110
|
-
* the
|
|
111
|
-
*
|
|
112
|
-
*
|
|
106
|
+
* the resident agent that serves every distro read and write is out — and the
|
|
107
|
+
* message names the recovery path, because there is no fallback I/O for it to
|
|
108
|
+
* point at.
|
|
113
109
|
* @param {unknown} error - what the substrate raised.
|
|
114
110
|
* @returns {{message: string, code: string, cause: unknown}} the refusal the
|
|
115
111
|
* provider wraps in `FsError`.
|
|
@@ -120,7 +116,8 @@ export function substrateFailure(error) {
|
|
|
120
116
|
return {
|
|
121
117
|
message:
|
|
122
118
|
`the distro file substrate is unavailable: ${message}. ` +
|
|
123
|
-
`
|
|
119
|
+
`The resident agent serves every distro read and write; check the distro is running ` +
|
|
120
|
+
`("wsl.exe -l -v") and restart the session if it stays out.`,
|
|
124
121
|
code: "FS_IO_ERROR",
|
|
125
122
|
cause: error instanceof Error ? error : undefined,
|
|
126
123
|
};
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The root-plane filesystem with coordinate routing: targets under a
|
|
3
|
+
* `\\wsl.localhost\<distro>` identity are served by the distro substrate
|
|
4
|
+
* (the resident agent, on ext4); every other target — drive-letter paths,
|
|
5
|
+
* the deployment cwd — is served by the host `LocalFileSystem` this class
|
|
6
|
+
* extends, unchanged.
|
|
7
|
+
*
|
|
8
|
+
* ## Why a router, and why here
|
|
9
|
+
*
|
|
10
|
+
* The root `ctx.fs` is session-less: it cannot be switched per session the
|
|
11
|
+
* way a preset's isolated `fs` can, so the two workspace kinds share one
|
|
12
|
+
* backend — and the stock backend serves the distro workspace across the 9p
|
|
13
|
+
* share, in its slow direction, with no usable change notifications. Routing
|
|
14
|
+
* by coordinate makes every root consumer (the workspace-files change feed
|
|
15
|
+
* among them) correct on both workspace kinds without per-consumer patches.
|
|
16
|
+
*
|
|
17
|
+
* The design note is `docs/root-fs-routing.md` (the operation routing table,
|
|
18
|
+
* the intended behavior differences, and the dormant-root-fence rationale).
|
|
19
|
+
*
|
|
20
|
+
* ## What is intentionally not here
|
|
21
|
+
*
|
|
22
|
+
* No write fence: the root plane has no write consumers (the model's writes
|
|
23
|
+
* ride the per-session preset filesystems, which enforce their own
|
|
24
|
+
* containment — the distro side kernel-enforced). If a root-plane write
|
|
25
|
+
* consumer appears, its fence rides with that consumer.
|
|
26
|
+
*
|
|
27
|
+
* @module dsh-plugin-wsl/fs-routing
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { isWslUnc, uncToPosix, windowsToLinuxMount } from "./paths.js";
|
|
31
|
+
import { LocalFileSystem } from "@deepseek-ai/dsh-fs-local";
|
|
32
|
+
import { linuxJoin } from "./workspace-files-route.js";
|
|
33
|
+
import { WslFileSystem } from "./index.js";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Whether a resolved target identity belongs to the distro.
|
|
37
|
+
*
|
|
38
|
+
* @param targetKey - the target's identity (a UNC for distro paths).
|
|
39
|
+
* @returns true when the target lives inside a WSL distro.
|
|
40
|
+
*/
|
|
41
|
+
export function targetIsDistro(targetKey) {
|
|
42
|
+
return isWslUnc(targetKey);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Whether a raw path-or-cwd input names the distro (used by the operations
|
|
47
|
+
* that take paths before a target exists: resolve, lstat).
|
|
48
|
+
*
|
|
49
|
+
* @param value - a path in any coordinate system, or undefined.
|
|
50
|
+
* @returns true when the input names a distro UNC.
|
|
51
|
+
*/
|
|
52
|
+
export function inputIsDistro(value) {
|
|
53
|
+
return value !== undefined && isWslUnc(value);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The Linux working path for one request: UNC absolutes map onto their Linux
|
|
58
|
+
* paths, POSIX absolutes and relative paths join onto the root.
|
|
59
|
+
*
|
|
60
|
+
* @param linuxRoot - the workspace root in Linux form.
|
|
61
|
+
* @param path - the requested path, any coordinate system.
|
|
62
|
+
* @returns the absolute Linux path.
|
|
63
|
+
*/
|
|
64
|
+
/**
|
|
65
|
+
* The root-plane filesystem with coordinate routing.
|
|
66
|
+
*
|
|
67
|
+
* The distro backend is built lazily on the first routed target, sharing this
|
|
68
|
+
* context (the root `sandboxPolicy` service provides the deployment default
|
|
69
|
+
* mode the distro side reports through `sandboxMode`).
|
|
70
|
+
*/
|
|
71
|
+
export class WslRoutingFileSystem extends LocalFileSystem {
|
|
72
|
+
static Config = LocalFileSystem.Config;
|
|
73
|
+
|
|
74
|
+
/** The lazy distro backend, built on first routed use. */
|
|
75
|
+
#distro;
|
|
76
|
+
/** The distro knobs, verbatim from the row config. */
|
|
77
|
+
#distroConfig;
|
|
78
|
+
|
|
79
|
+
constructor(ctx, config) {
|
|
80
|
+
super(ctx, config);
|
|
81
|
+
this.#distroConfig = {
|
|
82
|
+
distro: config.distro ?? "",
|
|
83
|
+
wslPath: config.wslPath ?? "wsl.exe",
|
|
84
|
+
cwd: config.distroCwd ?? "",
|
|
85
|
+
diffBasisMaxBytes: config.diffBasisMaxBytes,
|
|
86
|
+
sandbox: config.sandbox ?? true,
|
|
87
|
+
maskWindowsDrive: config.maskWindowsDrive ?? false,
|
|
88
|
+
restrictToDistro: config.restrictToDistro ?? true,
|
|
89
|
+
watchMaxDepth: config.watchMaxDepth ?? 0,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The distro backend for routed targets, built on first use. */
|
|
94
|
+
distro() {
|
|
95
|
+
return (this.#distro ??= new WslFileSystem(this.ctx, this.#distroConfig));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async resolve(path, opts) {
|
|
99
|
+
if (inputIsDistro(path) || inputIsDistro(opts?.cwd)) return (await this.distro()).resolve(path, opts);
|
|
100
|
+
return super.resolve(path, opts);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async lstat(path, opts, signal) {
|
|
104
|
+
if (inputIsDistro(path) || inputIsDistro(opts?.cwd)) return (await this.distro()).lstat(path, opts, signal);
|
|
105
|
+
return super.lstat(path, opts, signal);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async stat(target, signal) {
|
|
109
|
+
if (targetIsDistro(target.targetKey)) return (await this.distro()).stat(target, signal);
|
|
110
|
+
return super.stat(target, signal);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async readText(target, signal) {
|
|
114
|
+
if (targetIsDistro(target.targetKey)) return (await this.distro()).readText(target, signal);
|
|
115
|
+
return super.readText(target, signal);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async streamText(target, signal) {
|
|
119
|
+
if (targetIsDistro(target.targetKey)) return (await this.distro()).streamText(target, signal);
|
|
120
|
+
return super.streamText(target, signal);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async readBytes(target, signal, maxBytes) {
|
|
124
|
+
if (targetIsDistro(target.targetKey)) return (await this.distro()).readBytes(target, signal, maxBytes);
|
|
125
|
+
return super.readBytes(target, signal, maxBytes);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async readByteRange(target, range, signal) {
|
|
129
|
+
if (targetIsDistro(target.targetKey)) return (await this.distro()).readByteRange(target, range, signal);
|
|
130
|
+
return super.readByteRange(target, range, signal);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async listDir(target, signal) {
|
|
134
|
+
if (targetIsDistro(target.targetKey)) return (await this.distro()).listDir(target, signal);
|
|
135
|
+
return super.listDir(target, signal);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async writeText(target, content, expected, signal, sandboxPolicy) {
|
|
139
|
+
if (targetIsDistro(target.targetKey)) {
|
|
140
|
+
return (await this.distro()).writeText(target, content, expected, signal, sandboxPolicy);
|
|
141
|
+
}
|
|
142
|
+
return super.writeText(target, content, expected, signal, sandboxPolicy);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async editText(target, edit, expected, signal, sandboxPolicy) {
|
|
146
|
+
if (targetIsDistro(target.targetKey)) {
|
|
147
|
+
return (await this.distro()).editText(target, edit, expected, signal, sandboxPolicy);
|
|
148
|
+
}
|
|
149
|
+
return super.editText(target, edit, expected, signal, sandboxPolicy);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async watch(target, changed, signal) {
|
|
153
|
+
if (targetIsDistro(target.targetKey)) return (await this.distro()).watch(target, changed, signal);
|
|
154
|
+
return super.watch(target, changed, signal);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
contains(parent, child) {
|
|
158
|
+
if (targetIsDistro(parent.targetKey)) {
|
|
159
|
+
// Both targets came from this router's distro resolve, so the plain
|
|
160
|
+
// POSIX containment over their process paths is the same check the
|
|
161
|
+
// distro substrate performs for its own guarded writes.
|
|
162
|
+
const base = this.processPath(parent);
|
|
163
|
+
const full = this.processPath(child);
|
|
164
|
+
return full === base || full.startsWith(base.endsWith("/") ? base : `${base}/`);
|
|
165
|
+
}
|
|
166
|
+
return super.contains(parent, child);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
processPath(target) {
|
|
170
|
+
const parsed = uncToPosix(target.targetKey);
|
|
171
|
+
if (parsed !== undefined) return parsed.linuxPath.length > 0 ? parsed.linuxPath : "/";
|
|
172
|
+
return super.processPath(target);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export default WslRoutingFileSystem;
|
package/lib/git-route.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which git spawns belong inside the distro, and the `wsl.exe` argv they
|
|
3
|
+
* become there.
|
|
4
|
+
*
|
|
5
|
+
* `workspace-changes` snapshots a workspace's git state by spawning the host
|
|
6
|
+
* git through `ctx.subprocess` with the workspace as the working directory.
|
|
7
|
+
* On a distro workspace that spawn runs Windows git against the 9P share —
|
|
8
|
+
* the same slow direction as the search path, and the snapshot's
|
|
9
|
+
* `git add --all` is a full work-tree scan, the most metadata-heavy operation
|
|
10
|
+
* there is. The interception point is the same `spawn` override; the decision
|
|
11
|
+
* lives here, pure and unit-testable, mirroring `search-route.js`.
|
|
12
|
+
*
|
|
13
|
+
* ## What is routed, and what deliberately is not
|
|
14
|
+
*
|
|
15
|
+
* A spawn is routed only when the working directory names the distro (a
|
|
16
|
+
* `\\wsl.localhost\<distro>` UNC) and the argv carries none of the discovery
|
|
17
|
+
* flags (`--show-toplevel`, `--absolute-git-dir`, `--git-path`). Those three
|
|
18
|
+
* print **absolute paths**, which `workspace-changes` resolves against the
|
|
19
|
+
* UNC workspace (`git.ts` `resolve(cwd, line)`) — a distro git would answer
|
|
20
|
+
* in Linux paths and corrupt that resolution. Host git answers in UNC form,
|
|
21
|
+
* which is exactly what the caller wants, and the discovery call is one
|
|
22
|
+
* cheap command rather than a work-tree scan. Everything else — `add`,
|
|
23
|
+
* `write-tree`, `ls-tree`, `cat-file`, `diff-tree`, `ls-files`,
|
|
24
|
+
* `check-ignore` — is safe to route: snapshot calls run in an environment
|
|
25
|
+
* fully isolated from the user's git state (`GIT_INDEX_FILE` /
|
|
26
|
+
* `GIT_OBJECT_DIRECTORY` / `GIT_ALTERNATE_OBJECT_DIRECTORIES` pointing at the
|
|
27
|
+
* tool's private scratch), so the distro git mutates scratch, never the
|
|
28
|
+
* repository.
|
|
29
|
+
*
|
|
30
|
+
* ## Environment translation
|
|
31
|
+
*
|
|
32
|
+
* The routed spawn must forward the caller's `GIT_*` environment into the
|
|
33
|
+
* distro, and its values are paths in Windows form (`\\wsl.localhost\...`
|
|
34
|
+
* identities and drive-letter scratch dirs). `translateGitEnv` rewrites those
|
|
35
|
+
* two shapes into distro paths with the same pure translations the fs seam
|
|
36
|
+
* uses, forwards every other value verbatim (`GIT_CONFIG_COUNT=0` and
|
|
37
|
+
* friends), and returns the names that must ride `WSLENV`. One documented
|
|
38
|
+
* limit: a colon-separated multi-directory value (none of the current
|
|
39
|
+
* callers set one) passes through untranslated.
|
|
40
|
+
*
|
|
41
|
+
* @module dsh-plugin-wsl/git-route
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
import { isWslUnc, uncToPosix, windowsToLinuxMount } from "./paths.js";
|
|
45
|
+
|
|
46
|
+
/** The one program this rewrite passes through to the distro. */
|
|
47
|
+
const GIT_PROGRAM = "git";
|
|
48
|
+
|
|
49
|
+
/** Discovery flags print absolute paths — host git keeps that contract. */
|
|
50
|
+
const DISCOVERY_FLAGS = ["--show-toplevel", "--absolute-git-dir", "--git-path"];
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Whether a spawn's program is git.
|
|
54
|
+
* @param unknown - the spawn's `argv[0]`.
|
|
55
|
+
* @returns true when the spawn is a git launch.
|
|
56
|
+
*/
|
|
57
|
+
export function isGitProgram(program) {
|
|
58
|
+
if (typeof program !== "string" || program.length === 0) return false;
|
|
59
|
+
const base = program.slice(Math.max(program.lastIndexOf("/"), program.lastIndexOf("\\")) + 1);
|
|
60
|
+
return base.toLowerCase().replace(/\.exe$/u, "") === GIT_PROGRAM;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Whether a git argv carries the discovery flags whose absolute output the
|
|
65
|
+
* caller resolves against the UNC world.
|
|
66
|
+
* @param {unknown[]} argv - the spawn's argv; `argv[0]` is the program.
|
|
67
|
+
* @returns true when the spawn must stay on host git.
|
|
68
|
+
*/
|
|
69
|
+
function namesDiscoveryPaths(argv) {
|
|
70
|
+
return argv.some((arg) => typeof arg === "string" && DISCOVERY_FLAGS.includes(arg));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Translate one `GIT_*` value into the distro's coordinate system.
|
|
75
|
+
*
|
|
76
|
+
* UNC identities (`\\wsl.localhost\<distro>\...`) map onto their Linux paths;
|
|
77
|
+
* drive-letter paths map onto their `/mnt/<drive>` mounts; anything else is
|
|
78
|
+
* not a path we can name and passes through verbatim.
|
|
79
|
+
*
|
|
80
|
+
* @param value - the env value.
|
|
81
|
+
* @returns the distro-side spelling of the value.
|
|
82
|
+
*/
|
|
83
|
+
export function translateGitEnvValue(value) {
|
|
84
|
+
if (typeof value !== "string") return value;
|
|
85
|
+
if (isWslUnc(value)) {
|
|
86
|
+
const parsed = uncToPosix(value);
|
|
87
|
+
return parsed === undefined ? value : parsed.linuxPath.length > 0 ? parsed.linuxPath : "/";
|
|
88
|
+
}
|
|
89
|
+
const mounted = windowsToLinuxMount(value);
|
|
90
|
+
return mounted === undefined ? value : mounted;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Translate a git spawn's environment for the distro and name the keys that
|
|
95
|
+
* must ride `WSLENV`.
|
|
96
|
+
*
|
|
97
|
+
* @param env - the spawn's environment, or undefined.
|
|
98
|
+
* @returns the environment to forward (every key, path-shaped values
|
|
99
|
+
* translated) and the `WSLENV` names.
|
|
100
|
+
*/
|
|
101
|
+
export function translateGitEnv(env) {
|
|
102
|
+
const translated = {};
|
|
103
|
+
const names = [];
|
|
104
|
+
for (const [name, value] of Object.entries(env ?? {})) {
|
|
105
|
+
translated[name] = name.startsWith("GIT_") ? translateGitEnvValue(value) : value;
|
|
106
|
+
names.push(name);
|
|
107
|
+
}
|
|
108
|
+
return { env: translated, wslenv: names };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The distro-side rewrite of one git spawn, when it belongs in the distro.
|
|
113
|
+
*
|
|
114
|
+
* `gitArgv` is the distro command itself; `wslArgv` is the same command
|
|
115
|
+
* wrapped for a one-shot `wsl.exe --exec` launch — git snapshot operations
|
|
116
|
+
* are not latency-sensitive, so the routing stays on the simple transport
|
|
117
|
+
* and does not need the resident agent. `wsl.exe` itself must start in a
|
|
118
|
+
* Windows directory, so the caller supplies its own host cwd.
|
|
119
|
+
*
|
|
120
|
+
* @param object - the spawn's inputs.
|
|
121
|
+
* @param {unknown[]} object.argv - the spawn's argv; `argv[0]` is the program.
|
|
122
|
+
* @param {unknown} object.cwd - the spawn's working directory, any coordinate system.
|
|
123
|
+
* @param {string} object.wslPath - path to `wsl.exe`.
|
|
124
|
+
* @returns {{distro: string, linuxCwd: string, gitArgv: string[], wslArgv: string[]}|undefined} the
|
|
125
|
+
* rewrite, or undefined when the spawn is not a routable distro git launch
|
|
126
|
+
* (non-git programs, host directories, and the absolute-output discovery
|
|
127
|
+
* flags all keep the shipped spawn).
|
|
128
|
+
*/
|
|
129
|
+
export function gitSpawnRewrite({ argv, cwd, wslPath }) {
|
|
130
|
+
if (!Array.isArray(argv) || !isGitProgram(argv[0])) return undefined;
|
|
131
|
+
if (namesDiscoveryPaths(argv)) return undefined;
|
|
132
|
+
const named = uncToPosix(cwd);
|
|
133
|
+
if (named === undefined) return undefined;
|
|
134
|
+
const linuxCwd = named.linuxPath.length > 0 ? named.linuxPath : "/";
|
|
135
|
+
const gitArgv = [GIT_PROGRAM, ...argv.slice(1)];
|
|
136
|
+
return {
|
|
137
|
+
distro: named.distro,
|
|
138
|
+
linuxCwd,
|
|
139
|
+
gitArgv,
|
|
140
|
+
wslArgv: [wslPath, "-d", named.distro, "--cd", linuxCwd, "--exec", ...gitArgv],
|
|
141
|
+
};
|
|
142
|
+
}
|