dsh-plugin-wsl-env 0.11.0 → 0.13.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 +181 -0
- package/CONTRIBUTING.md +29 -7
- package/README.md +124 -138
- package/SECURITY.md +11 -1
- package/SUPPORT.md +29 -2
- package/cordis.patch.yml +17 -19
- package/docs/ARCHITECTURE.md +30 -29
- package/docs/CONFIGURATION.md +81 -21
- package/docs/LIMITATIONS.md +71 -6
- package/docs/MIGRATION.md +98 -0
- package/docs/PARITY.md +1 -1
- package/docs/PERFORMANCE.md +60 -0
- package/docs/QUICKSTART.md +125 -0
- package/docs/README.zh.md +117 -137
- package/docs/ROADMAP-1.0.md +780 -0
- package/docs/ROADMAP.md +4 -0
- package/docs/UPSTREAM-FILE-REFERENCE-SEAM.md +80 -0
- package/docs/UPSTREAM-LSP-SEAM.md +90 -0
- package/docs/UPSTREAM-SESSION-NOTICE.md +95 -0
- package/lib/agent-errors.d.ts +19 -0
- package/lib/agent-errors.js +22 -0
- package/lib/agent-exec.js +2 -2
- package/lib/agent.d.ts +68 -5
- package/lib/agent.js +129 -22
- package/lib/distro-route.d.ts +75 -0
- package/lib/distro-route.js +86 -0
- package/lib/fs-routing.d.ts +4 -1
- package/lib/index.d.ts +166 -37
- package/lib/index.js +353 -75
- package/lib/paths.d.ts +15 -0
- package/lib/paths.js +28 -0
- package/lib/preflight.d.ts +46 -3
- package/lib/preflight.js +83 -14
- package/lib/shell-env.d.ts +9 -3
- package/lib/shell-env.js +121 -43
- package/lib/subprocess.d.ts +12 -3
- package/lib/subprocess.js +21 -3
- package/package.json +23 -15
- package/scripts/diagnose.sh +9 -88
- package/scripts/doctor.mjs +548 -0
- package/scripts/profile-preset.mjs +254 -0
- package/lib/workspace-files-route.d.ts +0 -136
- package/lib/workspace-files-route.js +0 -225
- package/lib/workspace-files-wsl.d.ts +0 -257
- package/lib/workspace-files-wsl.js +0 -418
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Print or write the complete `- id: preset-wsl` block (P0.3 / WSL-103).
|
|
4
|
+
*
|
|
5
|
+
* `wsl-shell` and `wsl-fs` live inside the `preset-wsl` row's `config.plugins`,
|
|
6
|
+
* so a patch that targets them by id changes nothing — the loader logs
|
|
7
|
+
* `patch: entry "wsl-shell" not found` and boots the shipped values. The only
|
|
8
|
+
* route that works is replacing the whole `preset-wsl` row, which must carry
|
|
9
|
+
* every nested plugin verbatim. This helper extracts that row from the shipped
|
|
10
|
+
* `cordis.patch.yml`, applies the common edits, and can validate or write a
|
|
11
|
+
* profile layer.
|
|
12
|
+
*
|
|
13
|
+
* node scripts/profile-preset.mjs [--distro Debian] [--session-sandbox false|auto]
|
|
14
|
+
* [--sandbox false] [--mask-windows-drive true] [--agent-backend native]
|
|
15
|
+
* [--out <profile/cordis.patch.yml>] [--check <file>] [--force]
|
|
16
|
+
*
|
|
17
|
+
* With no `--out`/`--check`, the block (with the edits) goes to stdout.
|
|
18
|
+
* `--check` fails loudly on a nested id patch and prints the block to paste.
|
|
19
|
+
* `--out` refuses to overwrite a patch that has no `preset-wsl` row unless
|
|
20
|
+
* `--force`, so it cannot silently drop another override.
|
|
21
|
+
*
|
|
22
|
+
* @module dsh-plugin-wsl/scripts/profile-preset
|
|
23
|
+
*/
|
|
24
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
25
|
+
import { dirname, join, resolve } from "node:path";
|
|
26
|
+
import { fileURLToPath } from "node:url";
|
|
27
|
+
import { tmpdir } from "node:os";
|
|
28
|
+
|
|
29
|
+
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
30
|
+
const SOURCE = join(ROOT, "cordis.patch.yml");
|
|
31
|
+
/** The shipped row's first line: four spaces deep inside the second `insert:` list. */
|
|
32
|
+
const ROW_START = /^ {4}- id: preset-wsl\s*$/m;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Extract the shipped `preset-wsl` row, dedented to a top-level patch entry.
|
|
36
|
+
* @param {string} source - the `cordis.patch.yml` text.
|
|
37
|
+
* @returns {string} the block, without a trailing newline.
|
|
38
|
+
*/
|
|
39
|
+
export function presetBlock(source) {
|
|
40
|
+
const match = ROW_START.exec(source);
|
|
41
|
+
if (match === null) throw new Error("profile-preset: cordis.patch.yml carries no `- id: preset-wsl` row");
|
|
42
|
+
return source.slice(match.index).replace(/^ {4}/gm, "").replace(/\s+$/u, "");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The top-level nested-row ids a profile layer wrongly targets.
|
|
47
|
+
* @param {string} text - a profile patch file.
|
|
48
|
+
* @returns {string[]} the offending ids, in file order.
|
|
49
|
+
*/
|
|
50
|
+
export function nestedRowIds(text) {
|
|
51
|
+
/** @type {string[]} */
|
|
52
|
+
const found = [];
|
|
53
|
+
for (const line of text.split("\n")) {
|
|
54
|
+
const match = /^ {0,4}-\s*id:\s*(wsl-shell|wsl-fs)\s*$/u.exec(line);
|
|
55
|
+
if (match?.[1] !== undefined && !found.includes(match[1])) found.push(match[1]);
|
|
56
|
+
}
|
|
57
|
+
return found;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The line range of one nested row inside the block.
|
|
62
|
+
* @param {string[]} lines - the block's lines.
|
|
63
|
+
* @param {string} id - `wsl-shell` or `wsl-fs`.
|
|
64
|
+
* @returns {{ start: number, end: number }} the range, end exclusive.
|
|
65
|
+
*/
|
|
66
|
+
function rowRange(lines, id) {
|
|
67
|
+
const start = lines.findIndex((line) => line.trim() === `- id: ${id}`);
|
|
68
|
+
if (start < 0) throw new Error(`profile-preset: the preset block carries no "${id}" row`);
|
|
69
|
+
const indent = /^\s*/u.exec(lines[start] ?? "")?.[0].length ?? 0;
|
|
70
|
+
let end = lines.length;
|
|
71
|
+
for (let i = start + 1; i < lines.length; i += 1) {
|
|
72
|
+
const head = /^(\s*)- id: /u.exec(lines[i] ?? "");
|
|
73
|
+
if (head !== null && head[1]?.length === indent) {
|
|
74
|
+
end = i;
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { start, end };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Set one config key inside one nested row, keeping every other line byte-for-byte.
|
|
83
|
+
* @param {string[]} lines - the block's lines.
|
|
84
|
+
* @param {string} id - the nested row.
|
|
85
|
+
* @param {string} key - the config key.
|
|
86
|
+
* @param {string} value - the rendered YAML value.
|
|
87
|
+
*/
|
|
88
|
+
function setKey(lines, id, key, value) {
|
|
89
|
+
const { start, end } = rowRange(lines, id);
|
|
90
|
+
const keyLine = new RegExp(`^(\\s*)${key}:\\s`, "u");
|
|
91
|
+
for (let i = start; i < end; i += 1) {
|
|
92
|
+
const match = keyLine.exec(lines[i] ?? "");
|
|
93
|
+
if (match !== null) {
|
|
94
|
+
lines[i] = `${match[1]}${key}: ${value}`;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Absent keys go under `distro:` (or `config:`), at the same depth as the
|
|
99
|
+
// keys around them.
|
|
100
|
+
const anchor = lines.slice(start, end).findIndex((line) => /^\s*distro:/u.test(line));
|
|
101
|
+
const config = lines.slice(start, end).findIndex((line) => /^\s*config:\s*$/u.test(line));
|
|
102
|
+
const offset = anchor >= 0 ? start + anchor : start + config;
|
|
103
|
+
const base = /^\s*/u.exec(lines[offset] ?? "")?.[0] ?? "";
|
|
104
|
+
// Alongside `distro:` the new key takes the same depth; under a bare
|
|
105
|
+
// `config:` it takes the next level.
|
|
106
|
+
const indent = anchor >= 0 ? base : `${base} `;
|
|
107
|
+
lines.splice(offset + 1, 0, `${indent}${key}: ${value}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Apply the supported edits to a preset block.
|
|
112
|
+
* @param {string} block - the block from {@link presetBlock}.
|
|
113
|
+
* @param {{ distro?: string, sessionSandbox?: boolean | "auto", sandbox?: boolean, maskWindowsDrive?: boolean, agentBackend?: string }} edits - the edits.
|
|
114
|
+
* @returns {string} the edited block.
|
|
115
|
+
*/
|
|
116
|
+
export function applyEdits(block, edits) {
|
|
117
|
+
const lines = block.split("\n");
|
|
118
|
+
const both = /** @type {Array<[string, string | undefined]>} */ ([
|
|
119
|
+
["distro", edits.distro],
|
|
120
|
+
["sessionSandbox", edits.sessionSandbox === undefined ? undefined : String(edits.sessionSandbox)],
|
|
121
|
+
["sandbox", edits.sandbox === undefined ? undefined : String(edits.sandbox)],
|
|
122
|
+
["maskWindowsDrive", edits.maskWindowsDrive === undefined ? undefined : String(edits.maskWindowsDrive)],
|
|
123
|
+
]);
|
|
124
|
+
for (const [key, value] of both) {
|
|
125
|
+
if (value === undefined) continue;
|
|
126
|
+
setKey(lines, "wsl-shell", key, value);
|
|
127
|
+
setKey(lines, "wsl-fs", key, value);
|
|
128
|
+
}
|
|
129
|
+
if (edits.agentBackend !== undefined) setKey(lines, "wsl-shell", "agentBackend", edits.agentBackend);
|
|
130
|
+
return lines.join("\n");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Parse the helper's arguments.
|
|
135
|
+
* @param {string[]} argv - `process.argv.slice(2)`.
|
|
136
|
+
* @returns {{ edits: Record<string, string | boolean>, out?: string, check?: string, force: boolean }} the options.
|
|
137
|
+
*/
|
|
138
|
+
function parseArgs(argv) {
|
|
139
|
+
/** @type {Record<string, string | boolean>} */
|
|
140
|
+
const edits = {};
|
|
141
|
+
/** @type {{ edits: Record<string, string | boolean>, out?: string, check?: string, force: boolean }} */
|
|
142
|
+
const options = { edits, force: false };
|
|
143
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
144
|
+
const arg = argv[i] ?? "";
|
|
145
|
+
const [flag, inline] = arg.includes("=") ? [arg.slice(0, arg.indexOf("=")), arg.slice(arg.indexOf("=") + 1)] : [arg, undefined];
|
|
146
|
+
const value = inline ?? argv[++i] ?? "";
|
|
147
|
+
if (flag === "--distro") {
|
|
148
|
+
if (value.length === 0) throw new Error("profile-preset: --distro needs a name");
|
|
149
|
+
edits.distro = value;
|
|
150
|
+
} else if (flag === "--session-sandbox") edits.sessionSandbox = parseSandboxMode(flag, value);
|
|
151
|
+
else if (flag === "--sandbox") edits.sandbox = parseBoolean(flag, value);
|
|
152
|
+
else if (flag === "--mask-windows-drive") edits.maskWindowsDrive = parseBoolean(flag, value);
|
|
153
|
+
else if (flag === "--agent-backend") {
|
|
154
|
+
if (!["auto", "native", "shell"].includes(value)) throw new Error(`profile-preset: --agent-backend must be auto, native or shell (got "${value}")`);
|
|
155
|
+
edits.agentBackend = value;
|
|
156
|
+
} else if (flag === "--out") options.out = value;
|
|
157
|
+
else if (flag === "--check") options.check = value;
|
|
158
|
+
else if (flag === "--force") {
|
|
159
|
+
options.force = true;
|
|
160
|
+
i -= 1;
|
|
161
|
+
} else if (flag === "--help" || flag === "-h") {
|
|
162
|
+
console.log("node scripts/profile-preset.mjs [--distro NAME] [--session-sandbox true|false|auto] [--sandbox true|false] [--mask-windows-drive true|false] [--agent-backend auto|native|shell] [--out FILE] [--check FILE] [--force]");
|
|
163
|
+
process.exit(0);
|
|
164
|
+
} else throw new Error(`profile-preset: unknown option "${arg}"`);
|
|
165
|
+
}
|
|
166
|
+
return options;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Parse one boolean flag value.
|
|
171
|
+
* @param {string} flag - the flag, for the error.
|
|
172
|
+
* @param {string} value - the value.
|
|
173
|
+
* @returns {boolean} the boolean.
|
|
174
|
+
*/
|
|
175
|
+
function parseBoolean(flag, value) {
|
|
176
|
+
if (value === "true") return true;
|
|
177
|
+
if (value === "false") return false;
|
|
178
|
+
throw new Error(`profile-preset: ${flag} takes true or false (got "${value}")`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Parse the `--session-sandbox` value: the boolean modes plus `auto`.
|
|
183
|
+
* @param {string} flag - the flag, for the error.
|
|
184
|
+
* @param {string} value - the value.
|
|
185
|
+
* @returns {boolean | "auto"} the mode.
|
|
186
|
+
*/
|
|
187
|
+
function parseSandboxMode(flag, value) {
|
|
188
|
+
if (value === "auto") return "auto";
|
|
189
|
+
return parseBoolean(flag, value);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Report a nested id patch with the exact block to paste.
|
|
194
|
+
* @param {string} file - the profile patch.
|
|
195
|
+
* @param {string[]} ids - the offending ids.
|
|
196
|
+
* @param {string} block - the corrected block.
|
|
197
|
+
* @returns {number} the exit status.
|
|
198
|
+
*/
|
|
199
|
+
function nestedFailure(file, ids, block) {
|
|
200
|
+
console.error(`profile-preset: ${file} patches ${ids.map((id) => `"${id}"`).join(" and ")} by id.`);
|
|
201
|
+
console.error("Those rows live inside preset-wsl's config.plugins; the loader ignores the");
|
|
202
|
+
console.error("entry and boots the shipped values. Replace that entry with this complete");
|
|
203
|
+
console.error("preset-wsl block (it also carries the edits you asked for):");
|
|
204
|
+
console.error("");
|
|
205
|
+
console.error(block);
|
|
206
|
+
return 1;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Run the helper.
|
|
211
|
+
* @param {string[]} argv - `process.argv.slice(2)`.
|
|
212
|
+
* @returns {number} the exit status.
|
|
213
|
+
*/
|
|
214
|
+
function main(argv) {
|
|
215
|
+
const options = parseArgs(argv);
|
|
216
|
+
const block = applyEdits(presetBlock(readFileSync(SOURCE, "utf8")), options.edits);
|
|
217
|
+
|
|
218
|
+
if (options.check !== undefined) {
|
|
219
|
+
if (!existsSync(options.check)) throw new Error(`profile-preset: no such patch: ${options.check}`);
|
|
220
|
+
const ids = nestedRowIds(readFileSync(options.check, "utf8"));
|
|
221
|
+
if (ids.length > 0) return nestedFailure(options.check, ids, block);
|
|
222
|
+
console.log(`ok: ${options.check} has no nested wsl-shell/wsl-fs id patch`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (options.out !== undefined) {
|
|
226
|
+
if (existsSync(options.out)) {
|
|
227
|
+
const existing = readFileSync(options.out, "utf8");
|
|
228
|
+
const ids = nestedRowIds(existing);
|
|
229
|
+
if (ids.length > 0) return nestedFailure(options.out, ids, block);
|
|
230
|
+
if (!existing.includes("- id: preset-wsl") && !options.force) {
|
|
231
|
+
console.error(`profile-preset: ${options.out} exists and has no preset-wsl row; refusing to replace its other overrides (pass --force to overwrite)`);
|
|
232
|
+
return 1;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
mkdirSync(dirname(options.out), { recursive: true });
|
|
236
|
+
const temporary = join(tmpdir(), `dsh-preset-${process.pid}.yml`);
|
|
237
|
+
writeFileSync(temporary, `${block}\n`);
|
|
238
|
+
renameSync(temporary, options.out);
|
|
239
|
+
console.log(`wrote ${options.out}`);
|
|
240
|
+
return 0;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (options.check === undefined) process.stdout.write(`${block}\n`);
|
|
244
|
+
return 0;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (process.argv[1] !== undefined && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
248
|
+
try {
|
|
249
|
+
process.exitCode = main(process.argv.slice(2));
|
|
250
|
+
} catch (error) {
|
|
251
|
+
console.error(/** @type {Error} */ (error).message);
|
|
252
|
+
process.exitCode = 1;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure builders and parsers for the distro-routed `workspace-files` service —
|
|
3
|
-
* the GUI file tree and previews for a `\\wsl.localhost\<distro>` workspace,
|
|
4
|
-
* served from inside the distro instead of Windows-side 9P walks.
|
|
5
|
-
*
|
|
6
|
-
* Peer-free by construction (no DSH imports): the service subclass imports
|
|
7
|
-
* this alongside the shipped `dsh-api-workspace-files` base class, and the
|
|
8
|
-
* unit tests import it standalone.
|
|
9
|
-
*
|
|
10
|
-
* This is a TypeScript source built to `lib/workspace-files-route.js`; edit
|
|
11
|
-
* THIS file and run `pnpm run build` — the artifact under `lib/` is generated,
|
|
12
|
-
* and `pnpm test` fails when it drifts.
|
|
13
|
-
*
|
|
14
|
-
* @module dsh-plugin-wsl/workspace-files-route
|
|
15
|
-
*/
|
|
16
|
-
/**
|
|
17
|
-
* Translate one path-shaped value into distro coordinates: UNC identities map
|
|
18
|
-
* onto their Linux paths, drive-letter paths onto their `/mnt/<drive>` mounts,
|
|
19
|
-
* everything else passes through verbatim.
|
|
20
|
-
*
|
|
21
|
-
* @param value - the value to translate.
|
|
22
|
-
* @returns the distro-side spelling.
|
|
23
|
-
*/
|
|
24
|
-
export declare function distroPath(value: string): string;
|
|
25
|
-
/**
|
|
26
|
-
* Whether a workspace root belongs to a distro (and therefore routes
|
|
27
|
-
* distro-side).
|
|
28
|
-
*
|
|
29
|
-
* @param workspaceRoot - the scope's workspace root, any coordinate system.
|
|
30
|
-
* @returns true for a `\\wsl.localhost\<distro>` UNC root.
|
|
31
|
-
*/
|
|
32
|
-
export declare function isDistroWorkspace(workspaceRoot: string): boolean;
|
|
33
|
-
/**
|
|
34
|
-
* The Linux working path for one request: UNC absolutes map onto their Linux
|
|
35
|
-
* paths, POSIX absolutes and relative paths join onto the root.
|
|
36
|
-
*
|
|
37
|
-
* @param linuxRoot - the workspace root in Linux form.
|
|
38
|
-
* @param path - the requested path, any coordinate system.
|
|
39
|
-
* @returns the absolute Linux path.
|
|
40
|
-
*/
|
|
41
|
-
export declare function linuxJoin(linuxRoot: string, path: string): string;
|
|
42
|
-
/**
|
|
43
|
-
* The record invocation for one path: line 1 is the no-follow kind (the
|
|
44
|
-
* upstream lstat gate), line 2 the follow-stat fields (the upstream final
|
|
45
|
-
* stat), tab-joined. The path is resolved and contained first — the GUI's
|
|
46
|
-
* workspace is a boundary for every read, not just for listings.
|
|
47
|
-
*
|
|
48
|
-
* @param file - absolute Linux path.
|
|
49
|
-
* @param root - the workspace root in Linux form (containment boundary).
|
|
50
|
-
* @returns the agent argv.
|
|
51
|
-
*/
|
|
52
|
-
export declare function statRecordArgv(file: string, root: string): string[];
|
|
53
|
-
/**
|
|
54
|
-
* Parse one stat record (kind + follow fields, tab-joined).
|
|
55
|
-
*
|
|
56
|
-
* @param line - the record line.
|
|
57
|
-
* @returns the upstream kind spelling, the byte size, and the synthesized
|
|
58
|
-
* opaque version.
|
|
59
|
-
*/
|
|
60
|
-
export declare function parseStatRecord(line: string | Buffer): {
|
|
61
|
-
kind: string;
|
|
62
|
-
type: string;
|
|
63
|
-
size: number;
|
|
64
|
-
version: string;
|
|
65
|
-
};
|
|
66
|
-
/**
|
|
67
|
-
* Map a `stat` kind onto the wire's kind token.
|
|
68
|
-
*
|
|
69
|
-
* @param kind - the `stat -c %F` spelling.
|
|
70
|
-
* @returns the wire kind token.
|
|
71
|
-
*/
|
|
72
|
-
export declare function wireKind(kind: string): string;
|
|
73
|
-
/**
|
|
74
|
-
* The read invocation for one line-paged text read: the stat record rides
|
|
75
|
-
* stderr (the caller builds the stat half of the response), the page rides
|
|
76
|
-
* stdout — one line beyond the page, so EOF is decidable from the yield.
|
|
77
|
-
* `$1` is the file, `$2` the 1-based first line, `$3` the line count.
|
|
78
|
-
*
|
|
79
|
-
* @param file - absolute Linux path.
|
|
80
|
-
* @param offset - first line to return (1-based).
|
|
81
|
-
* @param limit - maximum lines to return.
|
|
82
|
-
* @param root - the workspace root in Linux form (containment boundary).
|
|
83
|
-
* @returns the agent argv.
|
|
84
|
-
*/
|
|
85
|
-
export declare function pageArgv(file: string, offset: number, limit: number, root: string): string[];
|
|
86
|
-
/**
|
|
87
|
-
* The byte-window invocation: the stat record rides stderr, the window rides
|
|
88
|
-
* stdout. `$1` is the file, `$2` the byte offset, `$3` the byte count.
|
|
89
|
-
*
|
|
90
|
-
* @param file - absolute Linux path.
|
|
91
|
-
* @param offset - byte offset (0-based).
|
|
92
|
-
* @param length - byte count.
|
|
93
|
-
* @param root - the workspace root in Linux form (containment boundary).
|
|
94
|
-
* @returns the agent argv.
|
|
95
|
-
*/
|
|
96
|
-
export declare function bytesArgv(file: string, offset: number, length: number, root: string): string[];
|
|
97
|
-
/**
|
|
98
|
-
* Parse the stat record a read or byte-window relayed on stderr.
|
|
99
|
-
*
|
|
100
|
-
* @param stderr - the relay's raw stderr.
|
|
101
|
-
* @returns the parsed stat record, or undefined when stderr carries none.
|
|
102
|
-
*/
|
|
103
|
-
export declare function parseStatRecordFromStderr(stderr: string | Buffer): {
|
|
104
|
-
kind: string;
|
|
105
|
-
type: string;
|
|
106
|
-
size: number;
|
|
107
|
-
version: string;
|
|
108
|
-
} | undefined;
|
|
109
|
-
/**
|
|
110
|
-
* The listing invocation for one directory: resolve the final component
|
|
111
|
-
* through symlinks, refuse paths outside the root and non-directories, then
|
|
112
|
-
* list one more entry than the cap so truncation is decidable from the yield.
|
|
113
|
-
* `-L` matches the upstream child semantics (a symlink to a directory lists
|
|
114
|
-
* as a directory, a broken link drops out); `-p` marks directories with the
|
|
115
|
-
* trailing `/` the parser keys on.
|
|
116
|
-
*
|
|
117
|
-
* @param linuxRoot - the workspace root in Linux form (containment boundary).
|
|
118
|
-
* @param linuxDir - the directory to list.
|
|
119
|
-
* @param maxEntries - the caller's entry cap.
|
|
120
|
-
* @returns the agent argv.
|
|
121
|
-
*/
|
|
122
|
-
export declare function listDistroArgv(linuxRoot: string, linuxDir: string, maxEntries: number): string[];
|
|
123
|
-
/**
|
|
124
|
-
* Parse one capped `ls -1ALp` listing into wire entries.
|
|
125
|
-
*
|
|
126
|
-
* @param stdout - the listing's raw stdout.
|
|
127
|
-
* @param maxEntries - the entry cap the caller configured.
|
|
128
|
-
* @returns at most `maxEntries` entries and whether the level had more.
|
|
129
|
-
*/
|
|
130
|
-
export declare function parseDirListing(stdout: string | Buffer, maxEntries: number): {
|
|
131
|
-
entries: Array<{
|
|
132
|
-
name: string;
|
|
133
|
-
type: "file" | "directory";
|
|
134
|
-
}>;
|
|
135
|
-
truncated: boolean;
|
|
136
|
-
};
|
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure builders and parsers for the distro-routed `workspace-files` service —
|
|
3
|
-
* the GUI file tree and previews for a `\\wsl.localhost\<distro>` workspace,
|
|
4
|
-
* served from inside the distro instead of Windows-side 9P walks.
|
|
5
|
-
*
|
|
6
|
-
* Peer-free by construction (no DSH imports): the service subclass imports
|
|
7
|
-
* this alongside the shipped `dsh-api-workspace-files` base class, and the
|
|
8
|
-
* unit tests import it standalone.
|
|
9
|
-
*
|
|
10
|
-
* This is a TypeScript source built to `lib/workspace-files-route.js`; edit
|
|
11
|
-
* THIS file and run `pnpm run build` — the artifact under `lib/` is generated,
|
|
12
|
-
* and `pnpm test` fails when it drifts.
|
|
13
|
-
*
|
|
14
|
-
* @module dsh-plugin-wsl/workspace-files-route
|
|
15
|
-
*/
|
|
16
|
-
import { isWslUnc, uncToPosix, windowsToLinuxMount } from "./paths.js";
|
|
17
|
-
/**
|
|
18
|
-
* Translate one path-shaped value into distro coordinates: UNC identities map
|
|
19
|
-
* onto their Linux paths, drive-letter paths onto their `/mnt/<drive>` mounts,
|
|
20
|
-
* everything else passes through verbatim.
|
|
21
|
-
*
|
|
22
|
-
* @param value - the value to translate.
|
|
23
|
-
* @returns the distro-side spelling.
|
|
24
|
-
*/
|
|
25
|
-
export function distroPath(value) {
|
|
26
|
-
if (typeof value !== "string")
|
|
27
|
-
return value;
|
|
28
|
-
if (isWslUnc(value)) {
|
|
29
|
-
const parsed = uncToPosix(value);
|
|
30
|
-
return parsed === undefined ? value : parsed.linuxPath.length > 0 ? parsed.linuxPath : "/";
|
|
31
|
-
}
|
|
32
|
-
const mounted = windowsToLinuxMount(value);
|
|
33
|
-
return mounted === undefined ? value : mounted;
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Whether a workspace root belongs to a distro (and therefore routes
|
|
37
|
-
* distro-side).
|
|
38
|
-
*
|
|
39
|
-
* @param workspaceRoot - the scope's workspace root, any coordinate system.
|
|
40
|
-
* @returns true for a `\\wsl.localhost\<distro>` UNC root.
|
|
41
|
-
*/
|
|
42
|
-
export function isDistroWorkspace(workspaceRoot) {
|
|
43
|
-
return isWslUnc(workspaceRoot);
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* The Linux working path for one request: UNC absolutes map onto their Linux
|
|
47
|
-
* paths, POSIX absolutes and relative paths join onto the root.
|
|
48
|
-
*
|
|
49
|
-
* @param linuxRoot - the workspace root in Linux form.
|
|
50
|
-
* @param path - the requested path, any coordinate system.
|
|
51
|
-
* @returns the absolute Linux path.
|
|
52
|
-
*/
|
|
53
|
-
export function linuxJoin(linuxRoot, path) {
|
|
54
|
-
// The UNC check runs on the RAW spelling: the backslash-to-slash rewrite
|
|
55
|
-
// below would leave `//wsl.localhost/...`, which the identity regex cannot
|
|
56
|
-
// see.
|
|
57
|
-
const parsed = uncToPosix(path);
|
|
58
|
-
if (parsed !== undefined)
|
|
59
|
-
return parsed.linuxPath.length > 0 ? parsed.linuxPath : "/";
|
|
60
|
-
const text = String(path).replaceAll("\\", "/");
|
|
61
|
-
if (text.startsWith("/"))
|
|
62
|
-
return text.length > 1 ? text.replace(/\/+$/, "") : "/";
|
|
63
|
-
const base = linuxRoot === "/" ? "" : linuxRoot;
|
|
64
|
-
const joined = text.length === 0 ? base : `${base}/${text}`.replace(/\/\.(?=\/|$)/gu, "");
|
|
65
|
-
return joined.length === 0 ? "/" : joined;
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* The containment guard every distro-side workspace read shares. `$1` is the
|
|
69
|
-
* requested file, `$2` the workspace root; both are canonicalized with
|
|
70
|
-
* `realpath -L`, and a target that is neither the root itself nor below it is
|
|
71
|
-
* refused with exit 3 (the `workspace-file/outside-workspace` refusal). The
|
|
72
|
-
* resolved file is left in `real` for the rest of the script.
|
|
73
|
-
*
|
|
74
|
-
* A root of `/` admits every absolute path: the workspace IS the distro. The
|
|
75
|
-
* `-L` spelling matches the listing guard's, so symlinked workspace roots and
|
|
76
|
-
* `..` segments resolve the same way on both sides.
|
|
77
|
-
*/
|
|
78
|
-
const CONTAIN_FN = [
|
|
79
|
-
'contain() {',
|
|
80
|
-
' real=$(realpath -L -- "$1") || exit 2',
|
|
81
|
-
' root=$(realpath -L -- "$2") || exit 2',
|
|
82
|
-
' case "$root" in /) ;; *) case "$real" in "$root"|"$root"/*) ;; *) echo "OUTSIDE:$real" >&2; exit 3;; esac;; esac',
|
|
83
|
-
'}',
|
|
84
|
-
].join("\n");
|
|
85
|
-
/**
|
|
86
|
-
* The record invocation for one path: line 1 is the no-follow kind (the
|
|
87
|
-
* upstream lstat gate), line 2 the follow-stat fields (the upstream final
|
|
88
|
-
* stat), tab-joined. The path is resolved and contained first — the GUI's
|
|
89
|
-
* workspace is a boundary for every read, not just for listings.
|
|
90
|
-
*
|
|
91
|
-
* @param file - absolute Linux path.
|
|
92
|
-
* @param root - the workspace root in Linux form (containment boundary).
|
|
93
|
-
* @returns the agent argv.
|
|
94
|
-
*/
|
|
95
|
-
export function statRecordArgv(file, root) {
|
|
96
|
-
const script = [
|
|
97
|
-
CONTAIN_FN,
|
|
98
|
-
'contain "$1" "$2"',
|
|
99
|
-
'k=$(stat -c %F -- "$real") || exit 2',
|
|
100
|
-
's=$(stat -L -c %s\\t%D\\t%i\\t%Y\\t%Z -- "$real") || exit 2',
|
|
101
|
-
"printf '%s\\t%s\\n' \"$k\" \"$s\"",
|
|
102
|
-
].join("\n");
|
|
103
|
-
return ["sh", "-c", script, "sh", file, root];
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Parse one stat record (kind + follow fields, tab-joined).
|
|
107
|
-
*
|
|
108
|
-
* @param line - the record line.
|
|
109
|
-
* @returns the upstream kind spelling, the byte size, and the synthesized
|
|
110
|
-
* opaque version.
|
|
111
|
-
*/
|
|
112
|
-
export function parseStatRecord(line) {
|
|
113
|
-
const [kind, size, dev, ino, mtime, ctime] = String(line).trim().split("\t");
|
|
114
|
-
return {
|
|
115
|
-
kind,
|
|
116
|
-
type: wireKind(kind),
|
|
117
|
-
size: Number(size),
|
|
118
|
-
version: `${dev}:${ino}:${size}:${mtime}:${ctime}`,
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
/**
|
|
122
|
-
* Map a `stat` kind onto the wire's kind token.
|
|
123
|
-
*
|
|
124
|
-
* @param kind - the `stat -c %F` spelling.
|
|
125
|
-
* @returns the wire kind token.
|
|
126
|
-
*/
|
|
127
|
-
export function wireKind(kind) {
|
|
128
|
-
const mapped = { "regular file": "file", directory: "directory", "symbolic link": "symlink" };
|
|
129
|
-
return mapped[kind] ?? kind;
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* The read invocation for one line-paged text read: the stat record rides
|
|
133
|
-
* stderr (the caller builds the stat half of the response), the page rides
|
|
134
|
-
* stdout — one line beyond the page, so EOF is decidable from the yield.
|
|
135
|
-
* `$1` is the file, `$2` the 1-based first line, `$3` the line count.
|
|
136
|
-
*
|
|
137
|
-
* @param file - absolute Linux path.
|
|
138
|
-
* @param offset - first line to return (1-based).
|
|
139
|
-
* @param limit - maximum lines to return.
|
|
140
|
-
* @param root - the workspace root in Linux form (containment boundary).
|
|
141
|
-
* @returns the agent argv.
|
|
142
|
-
*/
|
|
143
|
-
export function pageArgv(file, offset, limit, root) {
|
|
144
|
-
const script = [
|
|
145
|
-
CONTAIN_FN,
|
|
146
|
-
'contain "$1" "$4"',
|
|
147
|
-
'k=$(stat -c %F -- "$real") || exit 2',
|
|
148
|
-
's=$(stat -L -c %s\\t%D\\t%i\\t%Y\\t%Z -- "$real") || exit 2',
|
|
149
|
-
"printf '%s\\t%s\\n' \"$k\" \"$s\" >&2",
|
|
150
|
-
'sed -n "${2},$(( $2 + $3 ))p" -- "$real"',
|
|
151
|
-
].join("\n");
|
|
152
|
-
return ["sh", "-c", script, "sh", file, String(offset), String(limit), root];
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* The byte-window invocation: the stat record rides stderr, the window rides
|
|
156
|
-
* stdout. `$1` is the file, `$2` the byte offset, `$3` the byte count.
|
|
157
|
-
*
|
|
158
|
-
* @param file - absolute Linux path.
|
|
159
|
-
* @param offset - byte offset (0-based).
|
|
160
|
-
* @param length - byte count.
|
|
161
|
-
* @param root - the workspace root in Linux form (containment boundary).
|
|
162
|
-
* @returns the agent argv.
|
|
163
|
-
*/
|
|
164
|
-
export function bytesArgv(file, offset, length, root) {
|
|
165
|
-
const script = [
|
|
166
|
-
CONTAIN_FN,
|
|
167
|
-
'contain "$1" "$4"',
|
|
168
|
-
's=$(stat -L -c %s\\t%D\\t%i\\t%Y\\t%Z -- "$real") || exit 2',
|
|
169
|
-
"printf '%s\\n' \"$s\" >&2",
|
|
170
|
-
'tail -c "+$(( $2 + 1 ))" -- "$real" | head -c "$3"',
|
|
171
|
-
].join("\n");
|
|
172
|
-
return ["sh", "-c", script, "sh", file, String(offset), String(length), root];
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* Parse the stat record a read or byte-window relayed on stderr.
|
|
176
|
-
*
|
|
177
|
-
* @param stderr - the relay's raw stderr.
|
|
178
|
-
* @returns the parsed stat record, or undefined when stderr carries none.
|
|
179
|
-
*/
|
|
180
|
-
export function parseStatRecordFromStderr(stderr) {
|
|
181
|
-
const line = String(stderr)
|
|
182
|
-
.split("\n")
|
|
183
|
-
.find((candidate) => candidate.includes("\t"));
|
|
184
|
-
return line === undefined ? undefined : parseStatRecord(line);
|
|
185
|
-
}
|
|
186
|
-
/**
|
|
187
|
-
* The listing invocation for one directory: resolve the final component
|
|
188
|
-
* through symlinks, refuse paths outside the root and non-directories, then
|
|
189
|
-
* list one more entry than the cap so truncation is decidable from the yield.
|
|
190
|
-
* `-L` matches the upstream child semantics (a symlink to a directory lists
|
|
191
|
-
* as a directory, a broken link drops out); `-p` marks directories with the
|
|
192
|
-
* trailing `/` the parser keys on.
|
|
193
|
-
*
|
|
194
|
-
* @param linuxRoot - the workspace root in Linux form (containment boundary).
|
|
195
|
-
* @param linuxDir - the directory to list.
|
|
196
|
-
* @param maxEntries - the caller's entry cap.
|
|
197
|
-
* @returns the agent argv.
|
|
198
|
-
*/
|
|
199
|
-
export function listDistroArgv(linuxRoot, linuxDir, maxEntries) {
|
|
200
|
-
const script = [
|
|
201
|
-
"root=$1 dir=$2 max=$3",
|
|
202
|
-
'real=$(realpath -L -- "$dir") || exit 2',
|
|
203
|
-
'case "$real" in "$root") ;; "$root"/*) ;; *) echo "OUTSIDE:$real" >&2; exit 3;; esac',
|
|
204
|
-
'[ -d "$real" ] || { echo "NOTDIR:$real" >&2; exit 4; }',
|
|
205
|
-
'ls -1ALp -- "$real" | head -n "$max"',
|
|
206
|
-
].join("\n");
|
|
207
|
-
return ["sh", "-c", script, "sh", linuxRoot, linuxDir, String(maxEntries + 1)];
|
|
208
|
-
}
|
|
209
|
-
/**
|
|
210
|
-
* Parse one capped `ls -1ALp` listing into wire entries.
|
|
211
|
-
*
|
|
212
|
-
* @param stdout - the listing's raw stdout.
|
|
213
|
-
* @param maxEntries - the entry cap the caller configured.
|
|
214
|
-
* @returns at most `maxEntries` entries and whether the level had more.
|
|
215
|
-
*/
|
|
216
|
-
export function parseDirListing(stdout, maxEntries) {
|
|
217
|
-
const lines = String(stdout).split("\n").filter((line) => line.length > 0);
|
|
218
|
-
const truncated = lines.length > maxEntries;
|
|
219
|
-
const entries = lines.slice(0, maxEntries).map((line) => {
|
|
220
|
-
const directory = line.endsWith("/");
|
|
221
|
-
const name = directory ? line.slice(0, -1) : line;
|
|
222
|
-
return { name, type: directory ? "directory" : "file" };
|
|
223
|
-
});
|
|
224
|
-
return { entries, truncated };
|
|
225
|
-
}
|