dsh-plugin-wsl-env 0.12.0 → 0.13.1

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.
@@ -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
+ }