dsh-plugin-wsl-env 0.7.5 → 0.9.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 +345 -8
- package/CONTRIBUTING.md +221 -68
- package/README.md +50 -35
- package/SECURITY.md +18 -2
- package/SUPPORT.md +23 -7
- package/cordis.patch.yml +9 -0
- package/docs/ARCHITECTURE.md +200 -57
- package/docs/CONFIGURATION.md +180 -16
- package/docs/LIMITATIONS.md +91 -33
- package/docs/PARITY.md +66 -38
- package/docs/PEER-PARITY.md +95 -32
- package/docs/README.zh.md +49 -31
- package/docs/UPSTREAM-FSIO-EXPORT.md +159 -50
- package/docs/UPSTREAM-TERMINAL-TITLE.md +49 -14
- package/lib/agent-confined.d.ts +42 -0
- package/lib/agent-confined.js +25 -41
- package/lib/agent-errors.d.ts +31 -0
- package/lib/agent-errors.js +19 -16
- package/lib/agent-exec.d.ts +159 -0
- package/lib/agent-exec.js +151 -165
- package/lib/agent-protocol.d.ts +192 -0
- package/lib/agent-protocol.js +76 -100
- package/lib/agent-shared.d.ts +27 -0
- package/lib/agent-shared.js +12 -12
- package/lib/agent.d.ts +353 -0
- package/lib/agent.js +536 -552
- package/lib/auto-preset.d.ts +110 -0
- package/lib/auto-preset.js +157 -141
- package/lib/bwrap.d.ts +90 -0
- package/lib/bwrap.js +18 -18
- package/lib/file-reference-wsl.d.ts +160 -0
- package/lib/file-reference-wsl.js +192 -173
- package/lib/fs-decisions.d.ts +107 -0
- package/lib/fs-decisions.js +64 -60
- package/lib/fs-routing.d.ts +182 -0
- package/lib/fs-routing.js +173 -116
- package/lib/fs-substrate.d.ts +258 -0
- package/lib/fs-substrate.js +248 -248
- package/lib/fs.d.ts +10 -0
- package/lib/fs.js +5 -0
- package/lib/fsio-agent.d.ts +307 -0
- package/lib/fsio-agent.js +414 -410
- package/lib/fsio-text.d.ts +115 -0
- package/lib/fsio-text.js +104 -84
- package/lib/git-route.d.ts +100 -0
- package/lib/git-route.js +46 -47
- package/lib/index.d.ts +1003 -0
- package/lib/index.js +1106 -1003
- package/lib/listing.d.ts +114 -0
- package/lib/listing.js +68 -55
- package/lib/paths.d.ts +182 -0
- package/lib/paths.js +134 -109
- package/lib/picker.d.ts +266 -0
- package/lib/picker.js +371 -351
- package/lib/ports.d.ts +44 -0
- package/lib/ports.js +39 -29
- package/lib/preflight.d.ts +91 -0
- package/lib/preflight.js +94 -0
- package/lib/preset-choice.d.ts +33 -0
- package/lib/preset-choice.js +15 -7
- package/lib/probe-cache.d.ts +51 -0
- package/lib/probe-cache.js +43 -29
- package/lib/sandbox-core.d.ts +149 -0
- package/lib/sandbox-core.js +239 -87
- package/lib/sandbox.d.ts +48 -0
- package/lib/sandbox.js +4 -4
- package/lib/search-exec.d.ts +96 -0
- package/lib/search-exec.js +52 -79
- package/lib/search-route.d.ts +75 -0
- package/lib/search-route.js +30 -31
- package/lib/shell-env.d.ts +88 -0
- package/lib/shell-env.js +78 -74
- package/lib/shell.d.ts +10 -0
- package/lib/shell.js +5 -0
- package/lib/subprocess.d.ts +313 -0
- package/lib/subprocess.js +267 -262
- package/lib/terminal-activity.d.ts +98 -0
- package/lib/terminal-activity.js +29 -39
- package/lib/terminal-route.d.ts +27 -0
- package/lib/terminal-route.js +16 -8
- package/lib/watcher.d.ts +77 -0
- package/lib/watcher.js +120 -126
- package/lib/workspace-files-route.d.ts +132 -0
- package/lib/workspace-files-route.js +73 -81
- package/lib/workspace-files-wsl.d.ts +257 -0
- package/lib/workspace-files-wsl.js +311 -251
- package/lib/wsl.d.ts +266 -0
- package/lib/wsl.js +150 -155
- package/locale/preflight/en.json +6 -0
- package/locale/preflight/zh.json +6 -0
- package/package.json +78 -22
- package/scripts/diagnose.sh +92 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The text mechanics of the distro-side filesystem, replicated from
|
|
3
|
+
* `@deepseek-ai/dsh-fs-local`'s fsio module so `lib/fsio-agent.js` can run the
|
|
4
|
+
* same orchestration without importing the peer.
|
|
5
|
+
*
|
|
6
|
+
* Everything here is pure and validation-only: no I/O, no Node fs. The
|
|
7
|
+
* replication is deliberate and temporary — once the upstream package exports
|
|
8
|
+
* its fsio module (the export is proposed in docs/UPSTREAM-FSIO-EXPORT.md),
|
|
9
|
+
* these functions are deleted in favour of the imported originals. Until then
|
|
10
|
+
* they are kept byte-for-byte faithful, including error messages, because the
|
|
11
|
+
* tool layer matches on both the `code` and what the model reads — and that
|
|
12
|
+
* faithfulness is verified, not assumed: docs/PEER-PARITY.md records the
|
|
13
|
+
* function-by-function comparison against the pinned peer's shipped code.
|
|
14
|
+
*
|
|
15
|
+
* This is a TypeScript source built to `lib/fsio-text.js`; edit THIS file and
|
|
16
|
+
* run `pnpm run build` — the artifact under `lib/` is generated, and `pnpm test`
|
|
17
|
+
* fails when it drifts.
|
|
18
|
+
*
|
|
19
|
+
* @module dsh-plugin-wsl/fsio-text
|
|
20
|
+
*/
|
|
21
|
+
/** The byte prefix scanned for a NUL before a read is declared binary. */
|
|
22
|
+
export declare const BINARY_SAMPLE_BYTES = 8192;
|
|
23
|
+
/**
|
|
24
|
+
* A coded filesystem failure. `code` carries the `FS_*` token the provider
|
|
25
|
+
* maps onto the peer's `FsError`; the class stays local so this module imports
|
|
26
|
+
* no peer and stays unit-testable in a bare checkout.
|
|
27
|
+
*/
|
|
28
|
+
export declare class FsCodedError extends Error {
|
|
29
|
+
/** The `FS_*` token, for callers that must not match on the message. */
|
|
30
|
+
code: string;
|
|
31
|
+
/**
|
|
32
|
+
* @param message - human-readable, model-facing.
|
|
33
|
+
* @param code - the `FS_*` token.
|
|
34
|
+
* @param cause - the underlying failure, when there is one.
|
|
35
|
+
*/
|
|
36
|
+
constructor(message: string, code: string, cause?: Error);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The structured not-text failure.
|
|
40
|
+
*
|
|
41
|
+
* @param verb - the operation being attempted, for the message.
|
|
42
|
+
* @param displayPath - the caller-facing path.
|
|
43
|
+
* @returns the refusal every decode failure raises.
|
|
44
|
+
*/
|
|
45
|
+
export declare function notTextError(verb: string, displayPath: string): FsCodedError;
|
|
46
|
+
/**
|
|
47
|
+
* Decode a buffer as strict UTF-8, rejecting invalid byte sequences the way
|
|
48
|
+
* the peer's fsio does.
|
|
49
|
+
*
|
|
50
|
+
* @param buffer - the bytes to decode.
|
|
51
|
+
* @param verb - the operation being attempted, for the message.
|
|
52
|
+
* @param displayPath - the caller-facing path.
|
|
53
|
+
* @returns the decoded text.
|
|
54
|
+
* @throws {FsCodedError} with `FS_NOT_TEXT` on invalid UTF-8.
|
|
55
|
+
*/
|
|
56
|
+
export declare function decodeUtf8(buffer: Buffer, verb: string, displayPath: string): string;
|
|
57
|
+
/**
|
|
58
|
+
* Decode one streamed chunk, carrying multi-byte sequences across chunks.
|
|
59
|
+
*
|
|
60
|
+
* @param decoder - the streaming decoder owned by the caller.
|
|
61
|
+
* @param chunk - the next chunk, or `undefined` to flush.
|
|
62
|
+
* @param verb - the operation being attempted, for the message.
|
|
63
|
+
* @param displayPath - the caller-facing path.
|
|
64
|
+
* @returns the decoded text.
|
|
65
|
+
* @throws {FsCodedError} with `FS_NOT_TEXT` on invalid UTF-8.
|
|
66
|
+
*/
|
|
67
|
+
export declare function decodeUtf8Stream(decoder: import("node:util").TextDecoder, chunk: Buffer | undefined, verb: string, displayPath: string): string;
|
|
68
|
+
/**
|
|
69
|
+
* Collapse CRLF to LF — the canonical in-memory form every edit/diff basis uses.
|
|
70
|
+
*
|
|
71
|
+
* @param content - the text to normalize.
|
|
72
|
+
* @returns the text with every CRLF collapsed to LF.
|
|
73
|
+
*/
|
|
74
|
+
export declare function normalizeLineEndings(content: string): string;
|
|
75
|
+
/**
|
|
76
|
+
* The line-ending style a file was read with.
|
|
77
|
+
*
|
|
78
|
+
* @param raw - decoded text in whatever line-ending style the file had.
|
|
79
|
+
* @returns the dominant style of the first 4096 bytes.
|
|
80
|
+
*/
|
|
81
|
+
export declare function detectLineEndings(raw: string): "LF" | "CRLF";
|
|
82
|
+
/**
|
|
83
|
+
* Convert LF-normalized content back to the style detected at read time.
|
|
84
|
+
*
|
|
85
|
+
* @param content - the LF-normalized (edited) text.
|
|
86
|
+
* @param lineEndings - the original file's style.
|
|
87
|
+
* @returns the content in the original file's line-ending style.
|
|
88
|
+
*/
|
|
89
|
+
export declare function restoreLineEndings(content: string, lineEndings: "LF" | "CRLF"): string;
|
|
90
|
+
/**
|
|
91
|
+
* Apply a literal replacement to LF-normalized content — the peer's edit
|
|
92
|
+
* algorithm, replicated: CRLF inside the needle is normalized before
|
|
93
|
+
* matching, an empty needle is refused, and a multi-match needle is ambiguous
|
|
94
|
+
* unless `replaceAll` is set.
|
|
95
|
+
*
|
|
96
|
+
* @param content - the current file content, already LF-normalized.
|
|
97
|
+
* @param oldString - literal text to find.
|
|
98
|
+
* @param newString - literal replacement text.
|
|
99
|
+
* @param replaceAll - replace every match instead of requiring one.
|
|
100
|
+
* @param displayPath - the caller-facing path for error messages.
|
|
101
|
+
* @returns the edited text and count.
|
|
102
|
+
* @throws {FsCodedError} with `FS_EDIT_NOT_FOUND` or `FS_AMBIGUOUS_EDIT`.
|
|
103
|
+
*/
|
|
104
|
+
export declare function applyLiteralEdit(content: string, oldString: string, newString: string, replaceAll: boolean | undefined, displayPath: string): {
|
|
105
|
+
content: string;
|
|
106
|
+
replacements: number;
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* Abort boundary mirroring the peer's `throwIfAborted`.
|
|
110
|
+
*
|
|
111
|
+
* @param signal - the caller's signal.
|
|
112
|
+
* @param verb - the operation being attempted, for the message.
|
|
113
|
+
* @throws {FsCodedError} with `FS_ABORTED` when the signal is done.
|
|
114
|
+
*/
|
|
115
|
+
export declare function throwIfAborted(signal: AbortSignal | undefined, verb: string): void;
|
package/lib/fsio-text.js
CHANGED
|
@@ -12,142 +12,162 @@
|
|
|
12
12
|
* faithfulness is verified, not assumed: docs/PEER-PARITY.md records the
|
|
13
13
|
* function-by-function comparison against the pinned peer's shipped code.
|
|
14
14
|
*
|
|
15
|
+
* This is a TypeScript source built to `lib/fsio-text.js`; edit THIS file and
|
|
16
|
+
* run `pnpm run build` — the artifact under `lib/` is generated, and `pnpm test`
|
|
17
|
+
* fails when it drifts.
|
|
18
|
+
*
|
|
15
19
|
* @module dsh-plugin-wsl/fsio-text
|
|
16
20
|
*/
|
|
17
|
-
|
|
18
21
|
/** The byte prefix scanned for a NUL before a read is declared binary. */
|
|
19
22
|
export const BINARY_SAMPLE_BYTES = 8192;
|
|
20
|
-
|
|
21
23
|
/**
|
|
22
24
|
* A coded filesystem failure. `code` carries the `FS_*` token the provider
|
|
23
25
|
* maps onto the peer's `FsError`; the class stays local so this module imports
|
|
24
26
|
* no peer and stays unit-testable in a bare checkout.
|
|
25
27
|
*/
|
|
26
28
|
export class FsCodedError extends Error {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
29
|
+
/**
|
|
30
|
+
* @param message - human-readable, model-facing.
|
|
31
|
+
* @param code - the `FS_*` token.
|
|
32
|
+
* @param cause - the underlying failure, when there is one.
|
|
33
|
+
*/
|
|
34
|
+
constructor(message, code, cause) {
|
|
35
|
+
super(message, cause === undefined ? {} : { cause });
|
|
36
|
+
this.name = "FsCodedError";
|
|
37
|
+
this.code = code;
|
|
38
|
+
}
|
|
37
39
|
}
|
|
38
|
-
|
|
39
40
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* @
|
|
41
|
+
* The structured not-text failure.
|
|
42
|
+
*
|
|
43
|
+
* @param verb - the operation being attempted, for the message.
|
|
44
|
+
* @param displayPath - the caller-facing path.
|
|
45
|
+
* @returns the refusal every decode failure raises.
|
|
43
46
|
*/
|
|
44
47
|
export function notTextError(verb, displayPath) {
|
|
45
|
-
|
|
48
|
+
return new FsCodedError(`cannot ${verb} "${displayPath}": invalid UTF-8 text`, "FS_NOT_TEXT");
|
|
46
49
|
}
|
|
47
|
-
|
|
48
50
|
/**
|
|
49
51
|
* Decode a buffer as strict UTF-8, rejecting invalid byte sequences the way
|
|
50
52
|
* the peer's fsio does.
|
|
51
|
-
*
|
|
52
|
-
* @param
|
|
53
|
-
* @param
|
|
54
|
-
* @
|
|
53
|
+
*
|
|
54
|
+
* @param buffer - the bytes to decode.
|
|
55
|
+
* @param verb - the operation being attempted, for the message.
|
|
56
|
+
* @param displayPath - the caller-facing path.
|
|
57
|
+
* @returns the decoded text.
|
|
55
58
|
* @throws {FsCodedError} with `FS_NOT_TEXT` on invalid UTF-8.
|
|
56
59
|
*/
|
|
57
60
|
export function decodeUtf8(buffer, verb, displayPath) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
try {
|
|
62
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(buffer);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
if (!(error instanceof TypeError))
|
|
66
|
+
throw error;
|
|
67
|
+
throw notTextError(verb, displayPath);
|
|
68
|
+
}
|
|
64
69
|
}
|
|
65
|
-
|
|
66
70
|
/**
|
|
67
71
|
* Decode one streamed chunk, carrying multi-byte sequences across chunks.
|
|
68
|
-
*
|
|
69
|
-
* @param
|
|
70
|
-
* @param
|
|
71
|
-
* @param
|
|
72
|
-
* @
|
|
72
|
+
*
|
|
73
|
+
* @param decoder - the streaming decoder owned by the caller.
|
|
74
|
+
* @param chunk - the next chunk, or `undefined` to flush.
|
|
75
|
+
* @param verb - the operation being attempted, for the message.
|
|
76
|
+
* @param displayPath - the caller-facing path.
|
|
77
|
+
* @returns the decoded text.
|
|
73
78
|
* @throws {FsCodedError} with `FS_NOT_TEXT` on invalid UTF-8.
|
|
74
79
|
*/
|
|
75
80
|
export function decodeUtf8Stream(decoder, chunk, verb, displayPath) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
81
|
+
try {
|
|
82
|
+
return chunk ? decoder.decode(chunk, { stream: true }) : decoder.decode();
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
if (!(error instanceof TypeError))
|
|
86
|
+
throw error;
|
|
87
|
+
throw notTextError(verb, displayPath);
|
|
88
|
+
}
|
|
82
89
|
}
|
|
83
|
-
|
|
84
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Collapse CRLF to LF — the canonical in-memory form every edit/diff basis uses.
|
|
92
|
+
*
|
|
93
|
+
* @param content - the text to normalize.
|
|
94
|
+
* @returns the text with every CRLF collapsed to LF.
|
|
95
|
+
*/
|
|
85
96
|
export function normalizeLineEndings(content) {
|
|
86
|
-
|
|
97
|
+
return content.replaceAll("\r\n", "\n");
|
|
87
98
|
}
|
|
88
|
-
|
|
89
|
-
|
|
99
|
+
/**
|
|
100
|
+
* The line-ending style a file was read with.
|
|
101
|
+
*
|
|
102
|
+
* @param raw - decoded text in whatever line-ending style the file had.
|
|
103
|
+
* @returns the dominant style of the first 4096 bytes.
|
|
104
|
+
*/
|
|
90
105
|
export function detectLineEndings(raw) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
106
|
+
const sample = raw.slice(0, 4096);
|
|
107
|
+
const crlfCount = sample.split("\r\n").length - 1;
|
|
108
|
+
return crlfCount > sample.split("\n").length - 1 - crlfCount ? "CRLF" : "LF";
|
|
94
109
|
}
|
|
95
|
-
|
|
96
110
|
/**
|
|
97
111
|
* Convert LF-normalized content back to the style detected at read time.
|
|
98
|
-
*
|
|
99
|
-
* @param
|
|
100
|
-
* @
|
|
112
|
+
*
|
|
113
|
+
* @param content - the LF-normalized (edited) text.
|
|
114
|
+
* @param lineEndings - the original file's style.
|
|
115
|
+
* @returns the content in the original file's line-ending style.
|
|
101
116
|
*/
|
|
102
117
|
export function restoreLineEndings(content, lineEndings) {
|
|
103
|
-
|
|
104
|
-
|
|
118
|
+
if (lineEndings === "LF")
|
|
119
|
+
return content;
|
|
120
|
+
return normalizeLineEndings(content).replaceAll("\n", "\r\n");
|
|
105
121
|
}
|
|
106
|
-
|
|
122
|
+
/**
|
|
123
|
+
* Count how many times `search` appears in `content`.
|
|
124
|
+
*
|
|
125
|
+
* @param content - the haystack.
|
|
126
|
+
* @param search - the needle.
|
|
127
|
+
* @returns the occurrence count.
|
|
128
|
+
*/
|
|
107
129
|
function countOccurrences(content, search) {
|
|
108
|
-
|
|
130
|
+
return content.split(search).length - 1;
|
|
109
131
|
}
|
|
110
|
-
|
|
111
132
|
/**
|
|
112
133
|
* Apply a literal replacement to LF-normalized content — the peer's edit
|
|
113
134
|
* algorithm, replicated: CRLF inside the needle is normalized before
|
|
114
135
|
* matching, an empty needle is refused, and a multi-match needle is ambiguous
|
|
115
136
|
* unless `replaceAll` is set.
|
|
116
137
|
*
|
|
117
|
-
* @param
|
|
118
|
-
* @param
|
|
119
|
-
* @param
|
|
120
|
-
* @param
|
|
121
|
-
* @param
|
|
122
|
-
* @returns
|
|
138
|
+
* @param content - the current file content, already LF-normalized.
|
|
139
|
+
* @param oldString - literal text to find.
|
|
140
|
+
* @param newString - literal replacement text.
|
|
141
|
+
* @param replaceAll - replace every match instead of requiring one.
|
|
142
|
+
* @param displayPath - the caller-facing path for error messages.
|
|
143
|
+
* @returns the edited text and count.
|
|
123
144
|
* @throws {FsCodedError} with `FS_EDIT_NOT_FOUND` or `FS_AMBIGUOUS_EDIT`.
|
|
124
145
|
*/
|
|
125
146
|
export function applyLiteralEdit(content, oldString, newString, replaceAll, displayPath) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
};
|
|
147
|
+
const oldNorm = normalizeLineEndings(oldString);
|
|
148
|
+
if (oldNorm.length === 0)
|
|
149
|
+
throw new FsCodedError("old_string must be a non-empty string", "FS_EDIT_NOT_FOUND");
|
|
150
|
+
const newNorm = normalizeLineEndings(newString);
|
|
151
|
+
const replacements = countOccurrences(content, oldNorm);
|
|
152
|
+
if (replacements === 0)
|
|
153
|
+
throw new FsCodedError(`old_string was not found in "${displayPath}"`, "FS_EDIT_NOT_FOUND");
|
|
154
|
+
if (!replaceAll && replacements > 1) {
|
|
155
|
+
throw new FsCodedError(`old_string matched ${replacements} times in "${displayPath}"; provide a more specific old_string or set replace_all to true`, "FS_AMBIGUOUS_EDIT");
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
content: content.split(oldNorm).join(newNorm),
|
|
159
|
+
replacements,
|
|
160
|
+
};
|
|
141
161
|
}
|
|
142
|
-
|
|
143
162
|
/**
|
|
144
163
|
* Abort boundary mirroring the peer's `throwIfAborted`.
|
|
145
|
-
*
|
|
146
|
-
* @param
|
|
164
|
+
*
|
|
165
|
+
* @param signal - the caller's signal.
|
|
166
|
+
* @param verb - the operation being attempted, for the message.
|
|
147
167
|
* @throws {FsCodedError} with `FS_ABORTED` when the signal is done.
|
|
148
168
|
*/
|
|
149
169
|
export function throwIfAborted(signal, verb) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
170
|
+
if (signal?.aborted) {
|
|
171
|
+
throw new FsCodedError(`${verb} aborted`, "FS_ABORTED", signal.reason instanceof Error ? signal.reason : undefined);
|
|
172
|
+
}
|
|
153
173
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
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
|
+
* This is a TypeScript source built to `lib/git-route.js`; edit THIS file and
|
|
42
|
+
* run `pnpm run build` — the artifact under `lib/` is generated, and `pnpm test`
|
|
43
|
+
* fails when it drifts.
|
|
44
|
+
*
|
|
45
|
+
* @module dsh-plugin-wsl/git-route
|
|
46
|
+
*/
|
|
47
|
+
/**
|
|
48
|
+
* Whether a spawn's program is git.
|
|
49
|
+
*
|
|
50
|
+
* @param program - the spawn's `argv[0]`.
|
|
51
|
+
* @returns true when the spawn is a git launch.
|
|
52
|
+
*/
|
|
53
|
+
export declare function isGitProgram(program: unknown): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Translate one `GIT_*` value into the distro's coordinate system.
|
|
56
|
+
*
|
|
57
|
+
* UNC identities (`\\wsl.localhost\<distro>\...`) map onto their Linux paths;
|
|
58
|
+
* drive-letter paths map onto their `/mnt/<drive>` mounts; anything else is
|
|
59
|
+
* not a path we can name and passes through verbatim.
|
|
60
|
+
*
|
|
61
|
+
* @param value - the env value.
|
|
62
|
+
* @returns the distro-side spelling of the value.
|
|
63
|
+
*/
|
|
64
|
+
export declare function translateGitEnvValue(value: string | undefined): string | undefined;
|
|
65
|
+
/**
|
|
66
|
+
* Translate a git spawn's environment for the distro and name the keys that
|
|
67
|
+
* must ride `WSLENV`.
|
|
68
|
+
*
|
|
69
|
+
* @param env - the spawn's environment, or undefined.
|
|
70
|
+
* @returns the environment to forward (every key, path-shaped values
|
|
71
|
+
* translated) and the `WSLENV` names.
|
|
72
|
+
*/
|
|
73
|
+
export declare function translateGitEnv(env: Record<string, string | undefined> | undefined): {
|
|
74
|
+
env: Record<string, string | undefined>;
|
|
75
|
+
wslenv: string[];
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* The distro-side rewrite of one git spawn, when it belongs in the distro.
|
|
79
|
+
*
|
|
80
|
+
* `gitArgv` is the distro command itself; `wslArgv` is the same command
|
|
81
|
+
* wrapped for a one-shot `wsl.exe --exec` launch — git snapshot operations
|
|
82
|
+
* are not latency-sensitive, so the routing stays on the simple transport
|
|
83
|
+
* and does not need the resident agent. `wsl.exe` itself must start in a
|
|
84
|
+
* Windows directory, so the caller supplies its own host cwd.
|
|
85
|
+
*
|
|
86
|
+
* @param object - the spawn's inputs.
|
|
87
|
+
* @returns the rewrite, or undefined when the spawn is not a routable distro
|
|
88
|
+
* git launch (non-git programs, host directories, and the absolute-output
|
|
89
|
+
* discovery flags all keep the shipped spawn).
|
|
90
|
+
*/
|
|
91
|
+
export declare function gitSpawnRewrite({ argv, cwd, wslPath }: {
|
|
92
|
+
argv?: readonly string[];
|
|
93
|
+
cwd?: string;
|
|
94
|
+
wslPath: string;
|
|
95
|
+
}): {
|
|
96
|
+
distro: string;
|
|
97
|
+
linuxCwd: string;
|
|
98
|
+
gitArgv: string[];
|
|
99
|
+
wslArgv: string[];
|
|
100
|
+
} | undefined;
|
package/lib/git-route.js
CHANGED
|
@@ -38,38 +38,39 @@
|
|
|
38
38
|
* limit: a colon-separated multi-directory value (none of the current
|
|
39
39
|
* callers set one) passes through untranslated.
|
|
40
40
|
*
|
|
41
|
+
* This is a TypeScript source built to `lib/git-route.js`; edit THIS file and
|
|
42
|
+
* run `pnpm run build` — the artifact under `lib/` is generated, and `pnpm test`
|
|
43
|
+
* fails when it drifts.
|
|
44
|
+
*
|
|
41
45
|
* @module dsh-plugin-wsl/git-route
|
|
42
46
|
*/
|
|
43
|
-
|
|
44
47
|
import { isWslUnc, uncToPosix, windowsToLinuxMount } from "./paths.js";
|
|
45
|
-
|
|
46
48
|
/** The one program this rewrite passes through to the distro. */
|
|
47
49
|
const GIT_PROGRAM = "git";
|
|
48
|
-
|
|
49
50
|
/** Discovery flags print absolute paths — host git keeps that contract. */
|
|
50
51
|
const DISCOVERY_FLAGS = ["--show-toplevel", "--absolute-git-dir", "--git-path"];
|
|
51
|
-
|
|
52
52
|
/**
|
|
53
53
|
* Whether a spawn's program is git.
|
|
54
|
-
*
|
|
54
|
+
*
|
|
55
|
+
* @param program - the spawn's `argv[0]`.
|
|
55
56
|
* @returns true when the spawn is a git launch.
|
|
56
57
|
*/
|
|
57
58
|
export function isGitProgram(program) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
if (typeof program !== "string" || program.length === 0)
|
|
60
|
+
return false;
|
|
61
|
+
const base = program.slice(Math.max(program.lastIndexOf("/"), program.lastIndexOf("\\")) + 1);
|
|
62
|
+
return base.toLowerCase().replace(/\.exe$/u, "") === GIT_PROGRAM;
|
|
61
63
|
}
|
|
62
|
-
|
|
63
64
|
/**
|
|
64
65
|
* Whether a git argv carries the discovery flags whose absolute output the
|
|
65
66
|
* caller resolves against the UNC world.
|
|
66
|
-
*
|
|
67
|
+
*
|
|
68
|
+
* @param argv - the spawn's argv; `argv[0]` is the program.
|
|
67
69
|
* @returns true when the spawn must stay on host git.
|
|
68
70
|
*/
|
|
69
71
|
function namesDiscoveryPaths(argv) {
|
|
70
|
-
|
|
72
|
+
return argv.some((arg) => typeof arg === "string" && DISCOVERY_FLAGS.includes(arg));
|
|
71
73
|
}
|
|
72
|
-
|
|
73
74
|
/**
|
|
74
75
|
* Translate one `GIT_*` value into the distro's coordinate system.
|
|
75
76
|
*
|
|
@@ -81,15 +82,15 @@ function namesDiscoveryPaths(argv) {
|
|
|
81
82
|
* @returns the distro-side spelling of the value.
|
|
82
83
|
*/
|
|
83
84
|
export function translateGitEnvValue(value) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
85
|
+
if (typeof value !== "string")
|
|
86
|
+
return value;
|
|
87
|
+
if (isWslUnc(value)) {
|
|
88
|
+
const parsed = uncToPosix(value);
|
|
89
|
+
return parsed === undefined ? value : parsed.linuxPath.length > 0 ? parsed.linuxPath : "/";
|
|
90
|
+
}
|
|
91
|
+
const mounted = windowsToLinuxMount(value);
|
|
92
|
+
return mounted === undefined ? value : mounted;
|
|
91
93
|
}
|
|
92
|
-
|
|
93
94
|
/**
|
|
94
95
|
* Translate a git spawn's environment for the distro and name the keys that
|
|
95
96
|
* must ride `WSLENV`.
|
|
@@ -99,15 +100,14 @@ export function translateGitEnvValue(value) {
|
|
|
99
100
|
* translated) and the `WSLENV` names.
|
|
100
101
|
*/
|
|
101
102
|
export function translateGitEnv(env) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
103
|
+
const translated = {};
|
|
104
|
+
const names = [];
|
|
105
|
+
for (const [name, value] of Object.entries(env ?? {})) {
|
|
106
|
+
translated[name] = name.startsWith("GIT_") ? translateGitEnvValue(value) : value;
|
|
107
|
+
names.push(name);
|
|
108
|
+
}
|
|
109
|
+
return { env: translated, wslenv: names };
|
|
109
110
|
}
|
|
110
|
-
|
|
111
111
|
/**
|
|
112
112
|
* The distro-side rewrite of one git spawn, when it belongs in the distro.
|
|
113
113
|
*
|
|
@@ -118,25 +118,24 @@ export function translateGitEnv(env) {
|
|
|
118
118
|
* Windows directory, so the caller supplies its own host cwd.
|
|
119
119
|
*
|
|
120
120
|
* @param object - the spawn's inputs.
|
|
121
|
-
* @
|
|
122
|
-
*
|
|
123
|
-
*
|
|
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).
|
|
121
|
+
* @returns the rewrite, or undefined when the spawn is not a routable distro
|
|
122
|
+
* git launch (non-git programs, host directories, and the absolute-output
|
|
123
|
+
* discovery flags all keep the shipped spawn).
|
|
128
124
|
*/
|
|
129
125
|
export function gitSpawnRewrite({ argv, cwd, wslPath }) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
126
|
+
if (!Array.isArray(argv) || !isGitProgram(argv[0]))
|
|
127
|
+
return undefined;
|
|
128
|
+
if (namesDiscoveryPaths(argv))
|
|
129
|
+
return undefined;
|
|
130
|
+
const named = uncToPosix(cwd);
|
|
131
|
+
if (named === undefined)
|
|
132
|
+
return undefined;
|
|
133
|
+
const linuxCwd = named.linuxPath.length > 0 ? named.linuxPath : "/";
|
|
134
|
+
const gitArgv = [GIT_PROGRAM, ...argv.slice(1)];
|
|
135
|
+
return {
|
|
136
|
+
distro: named.distro,
|
|
137
|
+
linuxCwd,
|
|
138
|
+
gitArgv,
|
|
139
|
+
wslArgv: [wslPath, "-d", named.distro, "--cd", linuxCwd, "--exec", ...gitArgv],
|
|
140
|
+
};
|
|
142
141
|
}
|