dsh-crate-web 0.1.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.
Files changed (57) hide show
  1. package/README.md +302 -0
  2. package/README.zh-CN.md +304 -0
  3. package/cordis.patch.yml +11 -0
  4. package/core/dsh_pack/__init__.py +77 -0
  5. package/core/dsh_pack/__main__.py +4 -0
  6. package/core/dsh_pack/cli.py +372 -0
  7. package/core/dsh_pack/errors.py +167 -0
  8. package/core/dsh_pack/export.py +912 -0
  9. package/core/dsh_pack/importer.py +1347 -0
  10. package/core/dsh_pack/network.py +77 -0
  11. package/core/dsh_pack/pack.py +248 -0
  12. package/core/dsh_pack/preflight.py +811 -0
  13. package/core/dsh_pack/user_cli.py +462 -0
  14. package/core/dsh_pack/validation.py +272 -0
  15. package/core/dsh_pack/verify.py +1336 -0
  16. package/lib/client.js +1623 -0
  17. package/lib/index.d.ts +30 -0
  18. package/lib/index.js +1275 -0
  19. package/package.json +73 -0
  20. package/restart-helper.mjs +121 -0
  21. package/runtime/python/LICENSE.txt +676 -0
  22. package/runtime/python/__install__.json +82 -0
  23. package/runtime/python/_asyncio.pyd +0 -0
  24. package/runtime/python/_bz2.pyd +0 -0
  25. package/runtime/python/_ctypes.pyd +0 -0
  26. package/runtime/python/_decimal.pyd +0 -0
  27. package/runtime/python/_elementtree.pyd +0 -0
  28. package/runtime/python/_hashlib.pyd +0 -0
  29. package/runtime/python/_lzma.pyd +0 -0
  30. package/runtime/python/_multiprocessing.pyd +0 -0
  31. package/runtime/python/_overlapped.pyd +0 -0
  32. package/runtime/python/_queue.pyd +0 -0
  33. package/runtime/python/_remote_debugging.pyd +0 -0
  34. package/runtime/python/_socket.pyd +0 -0
  35. package/runtime/python/_sqlite3.pyd +0 -0
  36. package/runtime/python/_ssl.pyd +0 -0
  37. package/runtime/python/_uuid.pyd +0 -0
  38. package/runtime/python/_wmi.pyd +0 -0
  39. package/runtime/python/_zoneinfo.pyd +0 -0
  40. package/runtime/python/_zstd.pyd +0 -0
  41. package/runtime/python/libcrypto-3.dll +0 -0
  42. package/runtime/python/libffi-8.dll +0 -0
  43. package/runtime/python/libssl-3.dll +0 -0
  44. package/runtime/python/pyexpat.pyd +0 -0
  45. package/runtime/python/python.cat +0 -0
  46. package/runtime/python/python.exe +0 -0
  47. package/runtime/python/python3.dll +0 -0
  48. package/runtime/python/python314._pth +6 -0
  49. package/runtime/python/python314.dll +0 -0
  50. package/runtime/python/python314.zip +0 -0
  51. package/runtime/python/pythonw.exe +0 -0
  52. package/runtime/python/select.pyd +0 -0
  53. package/runtime/python/sqlite3.dll +0 -0
  54. package/runtime/python/unicodedata.pyd +0 -0
  55. package/runtime/python/vcruntime140.dll +0 -0
  56. package/runtime/python/vcruntime140_1.dll +0 -0
  57. package/runtime/python/winsound.pyd +0 -0
package/lib/index.js ADDED
@@ -0,0 +1,1275 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { createReadStream } from "node:fs";
3
+ import { access, cp, mkdir, readFile, readdir, rename, stat, unlink, writeFile } from "node:fs/promises";
4
+ import { platform } from "node:os";
5
+ import { dirname, join, relative, resolve } from "node:path";
6
+ import { spawn } from "node:child_process";
7
+ import { fileURLToPath } from "node:url";
8
+ //#region src/history.ts
9
+ const historyLocks = /* @__PURE__ */ new Map();
10
+ async function withHistoryLock(path, operation) {
11
+ const previous = historyLocks.get(path) ?? Promise.resolve();
12
+ let release;
13
+ const current = new Promise((resolve) => {
14
+ release = resolve;
15
+ });
16
+ historyLocks.set(path, current);
17
+ await previous;
18
+ try {
19
+ return await operation();
20
+ } finally {
21
+ release();
22
+ if (historyLocks.get(path) === current) historyLocks.delete(path);
23
+ }
24
+ }
25
+ async function readHistoryUnlocked(path) {
26
+ try {
27
+ const value = JSON.parse(await readFile(path, "utf8"));
28
+ return Array.isArray(value) ? value.filter((item) => item !== null && typeof item === "object" && !Array.isArray(item)) : [];
29
+ } catch (error) {
30
+ if (error.code === "ENOENT") return [];
31
+ throw error;
32
+ }
33
+ }
34
+ async function writeHistoryAtomically(path, entries) {
35
+ const temporary = `${path}.${randomUUID()}.tmp`;
36
+ await writeFile(temporary, `${JSON.stringify(entries, null, 2)}\n`, {
37
+ encoding: "utf8",
38
+ flag: "wx"
39
+ });
40
+ try {
41
+ await rename(temporary, path);
42
+ } catch (error) {
43
+ await unlink(temporary).catch(() => void 0);
44
+ throw error;
45
+ }
46
+ }
47
+ async function readHistory(path) {
48
+ return withHistoryLock(path, () => readHistoryUnlocked(path));
49
+ }
50
+ async function record(path, item) {
51
+ return withHistoryLock(path, async () => {
52
+ await writeHistoryAtomically(path, [...await readHistoryUnlocked(path), item].slice(-100));
53
+ return item;
54
+ });
55
+ }
56
+ //#endregion
57
+ //#region src/index.ts
58
+ /** Host half: a small local HTTP bridge to the installed dsh-crate Core CLI. */
59
+ const API_PATH = "/dsh-crate/api";
60
+ const DOWNLOAD_PATH = "/dsh-crate/download";
61
+ const MAX_BODY_BYTES = 67108864;
62
+ const SAFE_NAME = /^[A-Za-z0-9._-]+$/;
63
+ const SAFE_PLUGIN_NAME = /^@?[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)?$/;
64
+ const SAFE_OPERATION_ID = /^[0-9a-f-]{36}$/i;
65
+ const CRATE_PACKAGE_NAME = "dsh-crate-web";
66
+ /** Cordis FiberState enum values; mirrors dsh-host-plugin-inventory phases. */
67
+ const RUNTIME_FIBER_PHASE = {
68
+ 0: "pending",
69
+ 1: "loading",
70
+ 2: "active",
71
+ 3: "failed",
72
+ 4: null,
73
+ 5: "unloading"
74
+ };
75
+ /** Read-only projection of the currently running Loader's non-group entries. */
76
+ function listRuntimePlugins(ctx) {
77
+ const loader = ctx.loader;
78
+ if (!loader || typeof loader.entries !== "function") return [];
79
+ const rows = [];
80
+ try {
81
+ for (const entry of loader.entries()) {
82
+ if (entry.options.group) continue;
83
+ if (entry.options.name.startsWith("cordis:")) continue;
84
+ rows.push({
85
+ id: entry.id,
86
+ name: entry.options.name,
87
+ enabled: entry.disabled !== true,
88
+ phase: entry.fiber ? RUNTIME_FIBER_PHASE[entry.fiber.state] ?? null : null
89
+ });
90
+ }
91
+ } catch {}
92
+ return rows;
93
+ }
94
+ function json(res, status, body) {
95
+ const payload = JSON.stringify(body);
96
+ res.writeHead(status, {
97
+ "content-type": "application/json; charset=utf-8",
98
+ "cache-control": "no-store"
99
+ });
100
+ res.end(payload);
101
+ }
102
+ function failMessage(error) {
103
+ return error instanceof Error ? error.message : String(error);
104
+ }
105
+ function safeName(value, label) {
106
+ if (typeof value !== "string" || !SAFE_NAME.test(value)) throw new Error(`${label} must be a simple file or Profile name`);
107
+ return value;
108
+ }
109
+ function safePluginName(value, label) {
110
+ if (typeof value !== "string" || !SAFE_PLUGIN_NAME.test(value)) throw new Error(`${label} must be a valid package name`);
111
+ return value;
112
+ }
113
+ async function readRequest(req) {
114
+ const chunks = [];
115
+ let length = 0;
116
+ for await (const chunk of req) {
117
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
118
+ length += bytes.length;
119
+ if (length > MAX_BODY_BYTES) throw new Error("request body is too large");
120
+ chunks.push(bytes);
121
+ }
122
+ if (length === 0) return {};
123
+ const value = JSON.parse(Buffer.concat(chunks).toString("utf8"));
124
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error("request body must be a JSON object");
125
+ return value;
126
+ }
127
+ function decodePack(value) {
128
+ if (typeof value !== "string" || value.length === 0) throw new Error("packBase64 is required");
129
+ const pack = Buffer.from(value, "base64");
130
+ if (pack.length === 0) throw new Error("packBase64 is empty");
131
+ return pack;
132
+ }
133
+ async function ensureWorkspace(home) {
134
+ const root = join(home, ".dsh-pack");
135
+ const exports = join(root, "exports");
136
+ const work = join(root, "ui-work");
137
+ const history = join(root, "ui-history.json");
138
+ await mkdir(exports, { recursive: true });
139
+ await mkdir(work, { recursive: true });
140
+ return {
141
+ root,
142
+ exports,
143
+ work,
144
+ history
145
+ };
146
+ }
147
+ async function bundledCorePath() {
148
+ const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
149
+ const core = join(packageRoot, "core");
150
+ try {
151
+ await access(join(core, "dsh_pack", "__main__.py"));
152
+ return core;
153
+ } catch {
154
+ return;
155
+ }
156
+ }
157
+ async function bundledRuntimeCommand() {
158
+ const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
159
+ const command = join(packageRoot, "runtime", "python", "python.exe");
160
+ try {
161
+ await access(command);
162
+ return {
163
+ command,
164
+ prefix: [
165
+ "-B",
166
+ "-m",
167
+ "dsh_pack"
168
+ ]
169
+ };
170
+ } catch {
171
+ return;
172
+ }
173
+ }
174
+ function configuredCoreCommand() {
175
+ if (!process.env.DSH_PACK_CLI) return void 0;
176
+ let prefix = [];
177
+ if (process.env.DSH_PACK_CLI_PREFIX) {
178
+ const parsed = JSON.parse(process.env.DSH_PACK_CLI_PREFIX);
179
+ if (!Array.isArray(parsed) || parsed.some((item) => typeof item !== "string")) throw new Error("DSH_PACK_CLI_PREFIX must be a JSON string array");
180
+ prefix = parsed;
181
+ }
182
+ return {
183
+ command: process.env.DSH_PACK_CLI,
184
+ prefix
185
+ };
186
+ }
187
+ function isCommandNotFound(error) {
188
+ return isObject(error) && error.code === "ENOENT";
189
+ }
190
+ async function spawnCore(home, command, args) {
191
+ return await new Promise((resolveRun, reject) => {
192
+ const child = spawn(command.command, [...command.prefix, ...args], {
193
+ cwd: home,
194
+ env: {
195
+ ...process.env,
196
+ ...command.env,
197
+ DSH_HOME: home,
198
+ DSH_PACK_CORE_MODE: "1",
199
+ PYTHONDONTWRITEBYTECODE: "1"
200
+ },
201
+ shell: false,
202
+ windowsHide: true
203
+ });
204
+ const stdout = [];
205
+ const stderr = [];
206
+ child.stdout.on("data", (chunk) => stdout.push(Buffer.from(chunk)));
207
+ child.stderr.on("data", (chunk) => stderr.push(Buffer.from(chunk)));
208
+ child.once("error", reject);
209
+ child.once("close", (code) => resolveRun({
210
+ code: code ?? 1,
211
+ stdout: Buffer.concat(stdout).toString("utf8"),
212
+ stderr: Buffer.concat(stderr).toString("utf8")
213
+ }));
214
+ });
215
+ }
216
+ async function runCore(home, args) {
217
+ const configured = configuredCoreCommand();
218
+ if (configured) return spawnCore(home, configured, args);
219
+ const candidates = [];
220
+ const bundledRuntime = await bundledRuntimeCommand();
221
+ if (bundledRuntime) candidates.push(bundledRuntime);
222
+ candidates.push({
223
+ command: "dsh-crate",
224
+ prefix: []
225
+ });
226
+ const bundled = await bundledCorePath();
227
+ if (bundled) {
228
+ const pythonPath = [bundled, process.env.PYTHONPATH].filter(Boolean).join(process.platform === "win32" ? ";" : ":");
229
+ candidates.push({
230
+ command: "python",
231
+ prefix: [
232
+ "-B",
233
+ "-m",
234
+ "dsh_pack"
235
+ ],
236
+ env: { PYTHONPATH: pythonPath }
237
+ }, {
238
+ command: "python3",
239
+ prefix: [
240
+ "-B",
241
+ "-m",
242
+ "dsh_pack"
243
+ ],
244
+ env: { PYTHONPATH: pythonPath }
245
+ }, {
246
+ command: "py",
247
+ prefix: [
248
+ "-3",
249
+ "-B",
250
+ "-m",
251
+ "dsh_pack"
252
+ ],
253
+ env: { PYTHONPATH: pythonPath }
254
+ });
255
+ }
256
+ let lastError;
257
+ for (const candidate of candidates) try {
258
+ return await spawnCore(home, candidate, args);
259
+ } catch (error) {
260
+ lastError = error;
261
+ if (!isCommandNotFound(error)) throw error;
262
+ }
263
+ throw new Error(`DSH Crate Core is unavailable: ${failMessage(lastError)}. Install dsh-crate or keep the bundled Core runner in the plugin package.`);
264
+ }
265
+ function parseCore(stdout) {
266
+ return JSON.parse(stdout);
267
+ }
268
+ function isObject(value) {
269
+ return value !== null && typeof value === "object" && !Array.isArray(value);
270
+ }
271
+ function coreDiagnostic(result, fallback) {
272
+ if (isObject(result) && isObject(result.error)) return result.error;
273
+ return { message: fallback };
274
+ }
275
+ function runtimeCoreArgs() {
276
+ const args = [
277
+ "--node-version",
278
+ process.version.replace(/^v/, ""),
279
+ "--os-name",
280
+ platform() === "win32" ? "windows" : platform()
281
+ ];
282
+ if (process.env.DSH_VERSION) args.push("--dsh-version", process.env.DSH_VERSION);
283
+ return args;
284
+ }
285
+ function argumentValue(args, flag) {
286
+ const index = args.indexOf(flag);
287
+ return index >= 0 && typeof args[index + 1] === "string" ? args[index + 1] : void 0;
288
+ }
289
+ function replaceArgument(args, flag, value) {
290
+ const next = [...args];
291
+ const index = next.indexOf(flag);
292
+ if (index >= 0 && index + 1 < next.length) next[index + 1] = value;
293
+ else next.push(flag, value);
294
+ return next;
295
+ }
296
+ function currentRuntime(home) {
297
+ const args = process.argv.slice(2);
298
+ const portValue = argumentValue(args, "--port") ?? process.env.DSH_PACK_PORT;
299
+ const port = portValue !== void 0 && /^\d+$/.test(portValue) ? Number(portValue) : void 0;
300
+ return {
301
+ currentProfile: argumentValue(args, "--profile") ?? process.env.DSH_PACK_ACTIVE_PROFILE ?? null,
302
+ pid: process.pid,
303
+ dshHome: home,
304
+ port: port ?? null,
305
+ restartConfigured: typeof process.argv[1] === "string" && port !== void 0
306
+ };
307
+ }
308
+ function operationDiagnostic(code, stage, item, message, impact, suggestedNextStep) {
309
+ return {
310
+ code,
311
+ stage,
312
+ severity: "BLOCKER",
313
+ item,
314
+ expected: "the requested Profile operation completes safely",
315
+ observed: message,
316
+ evidence: {
317
+ code,
318
+ stage,
319
+ item
320
+ },
321
+ impact,
322
+ canContinue: false,
323
+ suggestedNextStep,
324
+ suggestedChecks: [
325
+ "confirm the exact Profile name and operation evidence above",
326
+ "verify the target DSH_HOME and Profile path are correct",
327
+ "retry after correcting the reported item"
328
+ ],
329
+ message
330
+ };
331
+ }
332
+ async function savePack(work, action, value) {
333
+ const path = join(work, `${action}-${randomUUID()}.dshcrate`);
334
+ await writeFile(path, decodePack(value), { flag: "wx" });
335
+ return path;
336
+ }
337
+ async function readInstalledBundle(packagePath, location) {
338
+ try {
339
+ const packageJson = JSON.parse(await readFile(join(packagePath, "package.json"), "utf8"));
340
+ if (!isObject(packageJson)) return void 0;
341
+ const dsh = packageJson.dsh;
342
+ const bundle = dsh !== null && typeof dsh === "object" && !Array.isArray(dsh) ? dsh.bundle : void 0;
343
+ if (bundle === null || typeof bundle !== "object" || Array.isArray(bundle)) return void 0;
344
+ const patch = bundle.patch;
345
+ if (typeof packageJson.name !== "string" || typeof packageJson.version !== "string" || typeof patch !== "string" || patch.trim() === "") return void 0;
346
+ return {
347
+ name: packageJson.name,
348
+ version: packageJson.version,
349
+ patch,
350
+ location
351
+ };
352
+ } catch {
353
+ return;
354
+ }
355
+ }
356
+ let profileInvariantPromise;
357
+ async function ensureCrateBundleInAllProfilesOnce(home) {
358
+ const profilesRoot = join(home, "profiles");
359
+ let entries;
360
+ try {
361
+ entries = await readdir(profilesRoot, { withFileTypes: true });
362
+ } catch (error) {
363
+ if (error.code === "ENOENT") return;
364
+ throw error;
365
+ }
366
+ const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
367
+ const sourceManifest = JSON.parse(await readFile(join(packageRoot, "package.json"), "utf8"));
368
+ const sourceVersion = typeof sourceManifest.version === "string" ? sourceManifest.version : void 0;
369
+ if (sourceVersion === void 0) throw new Error("DSH Crate package version is missing");
370
+ const anchorPackage = join(profilesRoot, "node_modules", ...CRATE_PACKAGE_NAME.split("/"));
371
+ let anchorManifest;
372
+ try {
373
+ const value = JSON.parse(await readFile(join(anchorPackage, "package.json"), "utf8"));
374
+ if (isObject(value)) anchorManifest = value;
375
+ } catch (error) {
376
+ if (error.code !== "ENOENT") throw error;
377
+ }
378
+ if (anchorManifest === void 0 && resolve(packageRoot) !== resolve(anchorPackage)) {
379
+ try {
380
+ await stat(anchorPackage);
381
+ throw new Error(`DSH Crate installation anchor exists but has no valid package.json: ${anchorPackage}`);
382
+ } catch (error) {
383
+ if (error.code !== "ENOENT") throw error;
384
+ }
385
+ await mkdir(dirname(anchorPackage), { recursive: true });
386
+ await cp(packageRoot, anchorPackage, { recursive: true });
387
+ const value = JSON.parse(await readFile(join(anchorPackage, "package.json"), "utf8"));
388
+ if (isObject(value)) anchorManifest = value;
389
+ }
390
+ const crateVersion = typeof anchorManifest?.version === "string" ? anchorManifest.version : sourceVersion;
391
+ for (const entry of entries) {
392
+ if (!entry.isDirectory() || !SAFE_NAME.test(entry.name)) continue;
393
+ const manifestPath = join(profilesRoot, entry.name, "package.json");
394
+ let manifest;
395
+ try {
396
+ const value = JSON.parse(await readFile(manifestPath, "utf8"));
397
+ if (!isObject(value)) continue;
398
+ manifest = value;
399
+ } catch {
400
+ continue;
401
+ }
402
+ const dependenciesValue = manifest.dependencies;
403
+ if (dependenciesValue !== void 0 && !isObject(dependenciesValue)) continue;
404
+ const dependencies = isObject(dependenciesValue) ? dependenciesValue : {};
405
+ const dshValue = manifest.dsh;
406
+ if (dshValue !== void 0 && !isObject(dshValue)) continue;
407
+ const dsh = isObject(dshValue) ? dshValue : {};
408
+ const profileValue = dsh.profile;
409
+ if (profileValue !== void 0 && !isObject(profileValue)) continue;
410
+ const profile = isObject(profileValue) ? profileValue : {};
411
+ const bundlesValue = profile.bundles;
412
+ if (bundlesValue !== void 0 && (!Array.isArray(bundlesValue) || bundlesValue.some((item) => typeof item !== "string"))) continue;
413
+ const bundles = Array.isArray(bundlesValue) ? [...bundlesValue] : [];
414
+ let changed = false;
415
+ if (typeof dependencies[CRATE_PACKAGE_NAME] !== "string") {
416
+ dependencies[CRATE_PACKAGE_NAME] = crateVersion;
417
+ changed = true;
418
+ }
419
+ if (!bundles.includes(CRATE_PACKAGE_NAME)) {
420
+ bundles.push(CRATE_PACKAGE_NAME);
421
+ changed = true;
422
+ }
423
+ if (!changed) continue;
424
+ manifest.dependencies = dependencies;
425
+ manifest.dsh = {
426
+ ...dsh,
427
+ profile: {
428
+ ...profile,
429
+ bundles
430
+ }
431
+ };
432
+ await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
433
+ }
434
+ }
435
+ async function ensureCrateBundleInAllProfiles(home) {
436
+ if (profileInvariantPromise !== void 0) return profileInvariantPromise;
437
+ profileInvariantPromise = ensureCrateBundleInAllProfilesOnce(home);
438
+ try {
439
+ await profileInvariantPromise;
440
+ } finally {
441
+ profileInvariantPromise = void 0;
442
+ }
443
+ }
444
+ async function listInstalledBundles(profileModules, anchorModules) {
445
+ const bundles = /* @__PURE__ */ new Map();
446
+ const addBundle = (bundle) => {
447
+ if (bundle === void 0 || typeof bundle.name !== "string" || bundles.has(bundle.name)) return;
448
+ bundles.set(bundle.name, bundle);
449
+ };
450
+ const scanRoot = async (root, location) => {
451
+ let entries;
452
+ try {
453
+ entries = await readdir(root, { withFileTypes: true });
454
+ } catch (error) {
455
+ if (error.code === "ENOENT") return;
456
+ throw error;
457
+ }
458
+ for (const entry of entries) {
459
+ if (entry.name.startsWith(".")) continue;
460
+ const candidate = join(root, entry.name);
461
+ const direct = await readInstalledBundle(candidate, location);
462
+ if (direct !== void 0) {
463
+ addBundle(direct);
464
+ continue;
465
+ }
466
+ if (!entry.name.startsWith("@")) continue;
467
+ let scopedEntries;
468
+ try {
469
+ scopedEntries = await readdir(candidate, { withFileTypes: true });
470
+ } catch (error) {
471
+ if (error.code === "ENOENT") continue;
472
+ throw error;
473
+ }
474
+ for (const scopedEntry of scopedEntries) {
475
+ if (scopedEntry.name.startsWith(".")) continue;
476
+ addBundle(await readInstalledBundle(join(candidate, scopedEntry.name), location));
477
+ }
478
+ }
479
+ };
480
+ await scanRoot(profileModules, "profile");
481
+ await scanRoot(anchorModules, "installation-anchor");
482
+ return [...bundles.values()].sort((left, right) => String(left.name).localeCompare(String(right.name)));
483
+ }
484
+ /**
485
+ * Packages published by the DSH project are treated as official built-ins.
486
+ * This is the single source of truth for the official/user split used by the
487
+ * plugin inventory UI: a runtime loader row is official only when the package
488
+ * itself is an official bundle, or when an official bundle's patch inserts it
489
+ * as a built-in loader row (for example the official web-app bundle composes
490
+ * `dsh-better-sidebar` under its own package name). Loader row ids are
491
+ * deliberately NOT used, because a user plugin may register a row id that
492
+ * collides with an official built-in (for example a custom sidebar) and must
493
+ * stay in the user group.
494
+ */
495
+ function isOfficialPackage(name) {
496
+ return name.startsWith("@deepseek-ai/");
497
+ }
498
+ /** Loader row identities (`id`/`name`) registered by an ``insert`` block. */
499
+ function insertedLoaderRows(patchText) {
500
+ const rows = [];
501
+ let inInsert = false;
502
+ let insertIndent;
503
+ let fieldIndent;
504
+ for (const raw of patchText.split(/\r?\n/)) {
505
+ const line = raw.replace(/\s+$/, "");
506
+ const stripped = line.trim();
507
+ if (!stripped || stripped.startsWith("#")) continue;
508
+ const indent = line.length - line.trimStart().length;
509
+ if (indent === 0 && stripped.startsWith("- ")) {
510
+ inInsert = stripped.slice(2).trim() === "insert:";
511
+ insertIndent = void 0;
512
+ fieldIndent = void 0;
513
+ continue;
514
+ }
515
+ if (!inInsert) continue;
516
+ if (stripped.startsWith("- ")) {
517
+ if (insertIndent === void 0) insertIndent = indent;
518
+ if (indent !== insertIndent) continue;
519
+ fieldIndent = indent + 2;
520
+ const row = stripped.slice(2).trim();
521
+ const match = /^([a-zA-Z][a-zA-Z0-9_-]*):\s*(.*)$/.exec(row);
522
+ if (match === null) continue;
523
+ const value = match[2].trim().replace(/^["']|["']$/g, "");
524
+ if (value === "") continue;
525
+ if (match[1] === "id") rows.push({ id: value });
526
+ if (match[1] === "name") rows.push({ name: value });
527
+ continue;
528
+ }
529
+ if (fieldIndent === void 0 || indent !== fieldIndent) continue;
530
+ const field = /^([a-zA-Z][a-zA-Z0-9_-]*):\s*(.*)$/.exec(stripped);
531
+ if (field === null) continue;
532
+ const value = field[2].trim().replace(/^["']|["']$/g, "");
533
+ if (value === "") continue;
534
+ if (field[1] === "id") rows.push({ id: value });
535
+ if (field[1] === "name") rows.push({ name: value });
536
+ }
537
+ return rows;
538
+ }
539
+ /** Loader row ids registered via top-level ``insert`` blocks. */
540
+ function insertedLoaderIds(patchText) {
541
+ return insertedLoaderRows(patchText).filter((row) => row.id !== void 0).map((row) => row.id);
542
+ }
543
+ /** Loader row package names registered via top-level ``insert`` blocks. */
544
+ function insertedLoaderNames(patchText) {
545
+ return insertedLoaderRows(patchText).filter((row) => row.name !== void 0).map((row) => row.name);
546
+ }
547
+ /** Read one installed Bundle's patch text from either resolution root. */
548
+ async function readInstalledBundlePatch(home, profileName, bundleName, patch) {
549
+ const segments = bundleName.split("/");
550
+ const candidates = [join(home, "profiles", profileName, "node_modules", ...segments), join(home, "profiles", "node_modules", ...segments)];
551
+ for (const candidate of candidates) try {
552
+ return await readFile(join(candidate, patch), "utf8");
553
+ } catch {}
554
+ }
555
+ /** Loader row ids registered by one installed Bundle patch. */
556
+ async function installedBundlePatchIds(home, profileName, bundleName, patch) {
557
+ const ids = /* @__PURE__ */ new Set();
558
+ const patchText = await readInstalledBundlePatch(home, profileName, bundleName, patch);
559
+ if (patchText === void 0) return ids;
560
+ for (const id of insertedLoaderIds(patchText)) ids.add(id);
561
+ return ids;
562
+ }
563
+ /**
564
+ * The set of package names treated as official built-ins for a Profile:
565
+ * every @deepseek-ai/* bundle plus every loader row name its patches insert.
566
+ */
567
+ async function officialBundleNames(home, profileName, bundles) {
568
+ const official = /* @__PURE__ */ new Set();
569
+ const pending = [];
570
+ for (const bundle of bundles) {
571
+ if (typeof bundle.name !== "string" || !bundle.name.startsWith("@deepseek-ai/")) continue;
572
+ official.add(bundle.name);
573
+ if (typeof bundle.patch === "string" && bundle.patch.trim() !== "") pending.push({
574
+ name: bundle.name,
575
+ patch: bundle.patch
576
+ });
577
+ }
578
+ for (const item of pending) {
579
+ const patchText = await readInstalledBundlePatch(home, profileName, item.name, item.patch);
580
+ if (patchText === void 0) continue;
581
+ for (const name of insertedLoaderNames(patchText)) official.add(name);
582
+ }
583
+ return official;
584
+ }
585
+ async function listProfiles(home) {
586
+ const root = join(home, "profiles");
587
+ try {
588
+ const entries = await readdir(root, { withFileTypes: true });
589
+ const profiles = [];
590
+ for (const entry of entries) {
591
+ if (!entry.isDirectory() || !SAFE_NAME.test(entry.name)) continue;
592
+ try {
593
+ const packageJson = JSON.parse(await readFile(join(root, entry.name, "package.json"), "utf8"));
594
+ if (packageJson === null || typeof packageJson !== "object" || Array.isArray(packageJson)) continue;
595
+ const manifest = packageJson;
596
+ const dsh = manifest.dsh;
597
+ const profile = dsh !== null && typeof dsh === "object" && !Array.isArray(dsh) ? dsh.profile : void 0;
598
+ const bundles = profile !== null && typeof profile === "object" && !Array.isArray(profile) ? profile.bundles : [];
599
+ const dependencies = manifest.dependencies;
600
+ const dependencyNames = dependencies !== null && typeof dependencies === "object" && !Array.isArray(dependencies) ? Object.keys(dependencies) : [];
601
+ const composedBundles = Array.isArray(bundles) ? bundles.filter((name) => typeof name === "string") : [];
602
+ const activePlugins = new Set(composedBundles);
603
+ const scannedBundles = await listInstalledBundles(join(root, entry.name, "node_modules"), join(root, "node_modules"));
604
+ const officialNames = await officialBundleNames(home, entry.name, scannedBundles);
605
+ const installedBundles = scannedBundles.map((bundle) => ({
606
+ ...bundle,
607
+ active: activePlugins.has(String(bundle.name)),
608
+ official: officialNames.has(String(bundle.name))
609
+ })).filter((bundle) => bundle.active === true || dependencyNames.includes(String(bundle.name)));
610
+ const activeRowIds = /* @__PURE__ */ new Set();
611
+ const installedRowIds = /* @__PURE__ */ new Map();
612
+ for (const bundle of installedBundles) {
613
+ if (typeof bundle.patch !== "string") continue;
614
+ const rowIds = await installedBundlePatchIds(home, entry.name, String(bundle.name), bundle.patch);
615
+ installedRowIds.set(String(bundle.name), rowIds);
616
+ if (bundle.active === true) for (const id of rowIds) activeRowIds.add(id);
617
+ }
618
+ const conflicts = /* @__PURE__ */ new Map();
619
+ for (const bundle of installedBundles) {
620
+ if (bundle.active === true) continue;
621
+ const rowIds = installedRowIds.get(String(bundle.name)) ?? /* @__PURE__ */ new Set();
622
+ for (const id of rowIds) if (activeRowIds.has(id)) {
623
+ conflicts.set(String(bundle.name), true);
624
+ break;
625
+ }
626
+ }
627
+ const composedInstalledBundles = installedBundles.map((bundle) => ({
628
+ ...bundle,
629
+ conflict: conflicts.get(String(bundle.name)) === true
630
+ }));
631
+ profiles.push({
632
+ name: entry.name,
633
+ plugins: [...activePlugins],
634
+ installedBundles: composedInstalledBundles
635
+ });
636
+ } catch {}
637
+ }
638
+ return profiles.sort((a, b) => String(a.name).localeCompare(String(b.name)));
639
+ } catch (error) {
640
+ if (error.code === "ENOENT") return [];
641
+ throw error;
642
+ }
643
+ }
644
+ const PROFILE_PATCH_TEMPLATE = `# Your patch layer for this dsh profile, applied after every bundle layer:
645
+ # a top-level YAML array of loader patch entries (id-targeted config
646
+ # overrides, disables, and insert lists; \`!!js\` expressions allowed).
647
+ []
648
+ `;
649
+ const PROFILE_PNPM_WORKSPACE = `packages:
650
+ - .
651
+
652
+ nodeLinker: hoisted
653
+ autoInstallPeers: false
654
+ `;
655
+ /** Official bundles every newly created Profile composes by default. */
656
+ const DEFAULT_CREATE_BUNDLES = [
657
+ "@deepseek-ai/dsh-base",
658
+ "@deepseek-ai/dsh-web-app",
659
+ CRATE_PACKAGE_NAME
660
+ ];
661
+ async function readInstalledVersion(packageRoot) {
662
+ try {
663
+ const value = JSON.parse(await readFile(join(packageRoot, "package.json"), "utf8"));
664
+ if (isObject(value) && typeof value.version === "string" && value.version !== "") return value.version;
665
+ return;
666
+ } catch {
667
+ return;
668
+ }
669
+ }
670
+ async function createProfile(home, body, workspace) {
671
+ const profileName = safeName(body.profileName, "profileName");
672
+ if (profileName === "." || profileName === ".." || profileName === "node_modules") return {
673
+ status: "failed",
674
+ command: "create-profile",
675
+ exitCode: 2,
676
+ error: operationDiagnostic("PROFILE_NAME_RESERVED", "planning", profileName, `Profile name is reserved: ${profileName}`, "The requested Profile was not created.", "Choose another Profile name and retry.")
677
+ };
678
+ const profilesRoot = join(home, "profiles");
679
+ const profileDir = join(profilesRoot, profileName);
680
+ const profileRelative = relative(profilesRoot, resolve(profileDir));
681
+ if (!profileRelative || profileRelative.startsWith("..") || profileRelative.includes("\\")) throw new Error("profileName escaped DSH_HOME");
682
+ try {
683
+ await access(join(profileDir, "package.json"));
684
+ return {
685
+ status: "failed",
686
+ command: "create-profile",
687
+ exitCode: 2,
688
+ error: operationDiagnostic("PROFILE_EXISTS", "planning", profileName, `Profile already exists: ${profileName}`, "No Profile was created or modified.", "Choose a different Profile name, or delete the existing Profile first.")
689
+ };
690
+ } catch {}
691
+ const officialBase = await readInstalledVersion(join(profilesRoot, "node_modules", "@deepseek-ai", "dsh-base"));
692
+ const officialWeb = await readInstalledVersion(join(profilesRoot, "node_modules", "@deepseek-ai", "dsh-web-app"));
693
+ if (officialBase === void 0 || officialWeb === void 0) return {
694
+ status: "failed",
695
+ command: "create-profile",
696
+ exitCode: 2,
697
+ error: operationDiagnostic("OFFICIAL_BUNDLE_MISSING", "planning", profileName, `cannot resolve official bundle version: ${officialBase === void 0 ? "@deepseek-ai/dsh-base" : "@deepseek-ai/dsh-web-app"}`, "The requested Profile was not created.", `Install the official DSH bundles into this DSH_HOME first (${profilesRoot}/node_modules), then retry.`)
698
+ };
699
+ const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
700
+ const sourceManifest = JSON.parse(await readFile(join(packageRoot, "package.json"), "utf8"));
701
+ const crateVersion = typeof sourceManifest.version === "string" ? sourceManifest.version : void 0;
702
+ if (crateVersion === void 0) return {
703
+ status: "failed",
704
+ command: "create-profile",
705
+ exitCode: 2,
706
+ error: operationDiagnostic("CRATE_VERSION_MISSING", "planning", profileName, "DSH Crate package version is missing", "The requested Profile was not created.", "Repair the DSH Crate installation and retry.")
707
+ };
708
+ await mkdir(profileDir, { recursive: true });
709
+ const manifest = {
710
+ name: `dsh-profile-${profileName}`,
711
+ private: true,
712
+ dependencies: {
713
+ "@deepseek-ai/dsh-base": officialBase,
714
+ "@deepseek-ai/dsh-web-app": officialWeb,
715
+ [CRATE_PACKAGE_NAME]: crateVersion
716
+ },
717
+ dsh: { profile: { bundles: [...DEFAULT_CREATE_BUNDLES] } }
718
+ };
719
+ await writeFile(join(profileDir, "package.json"), `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
720
+ await writeFile(join(profileDir, "cordis.patch.yml"), PROFILE_PATCH_TEMPLATE, "utf8");
721
+ await writeFile(join(profileDir, "pnpm-workspace.yaml"), PROFILE_PNPM_WORKSPACE, "utf8");
722
+ await ensureCrateBundleInAllProfiles(home);
723
+ const item = await record(workspace.history, {
724
+ time: (/* @__PURE__ */ new Date()).toISOString(),
725
+ action: "create-profile",
726
+ status: "PASS",
727
+ profile: profileName
728
+ });
729
+ return {
730
+ status: "ok",
731
+ command: "create-profile",
732
+ exitCode: 0,
733
+ profile: (await listProfiles(home)).find((candidate) => candidate.name === profileName) ?? {
734
+ name: profileName,
735
+ plugins: DEFAULT_CREATE_BUNDLES,
736
+ installedBundles: []
737
+ },
738
+ history: item
739
+ };
740
+ }
741
+ async function handleApi(home, body, runtimePlugins = []) {
742
+ const action = body.action;
743
+ const workspace = await ensureWorkspace(home);
744
+ await ensureCrateBundleInAllProfiles(home);
745
+ if (action === "profiles") {
746
+ const runtime = currentRuntime(home);
747
+ const profiles = await listProfiles(home);
748
+ const runtimeOfficialNames = /* @__PURE__ */ new Set();
749
+ const runtimeRow = typeof runtime.currentProfile === "string" ? profiles.find((candidate) => candidate.name === runtime.currentProfile) : void 0;
750
+ if (runtimeRow !== void 0 && Array.isArray(runtimeRow.installedBundles)) {
751
+ const names = await officialBundleNames(home, String(runtimeRow.name), runtimeRow.installedBundles);
752
+ for (const name of names) runtimeOfficialNames.add(name);
753
+ }
754
+ const isOfficialRow = (row) => {
755
+ const name = typeof row.name === "string" ? row.name : "";
756
+ return isOfficialPackage(name) || runtimeOfficialNames.has(name);
757
+ };
758
+ return {
759
+ status: "ok",
760
+ profiles,
761
+ runtimePlugins: runtimePlugins.map((row) => ({
762
+ ...row,
763
+ official: isOfficialRow(row)
764
+ })),
765
+ runtime
766
+ };
767
+ }
768
+ if (action === "create-profile") return createProfile(home, body, workspace);
769
+ if (action === "history") return {
770
+ status: "ok",
771
+ history: await readHistory(workspace.history)
772
+ };
773
+ if (action === "inspect") {
774
+ const pack = await savePack(workspace.work, "inspect", body.packBase64);
775
+ const run = await runCore(home, [
776
+ "inspect",
777
+ pack,
778
+ "--json",
779
+ "--allow-network-reference-install",
780
+ ...runtimeCoreArgs()
781
+ ]);
782
+ try {
783
+ const result = parseCore(run.stdout);
784
+ let targetProfile;
785
+ let requestedProfileName;
786
+ try {
787
+ const planArgs = [
788
+ "import",
789
+ pack,
790
+ "--dsh-home",
791
+ home,
792
+ "--plan-only",
793
+ "--json",
794
+ ...runtimeCoreArgs()
795
+ ];
796
+ if (typeof body.targetProfile === "string") planArgs.push("--target-profile", safeName(body.targetProfile, "targetProfile"));
797
+ if (body.overwrite === true) planArgs.push("--overwrite");
798
+ const planRun = await runCore(home, planArgs);
799
+ if (planRun.code === 0) {
800
+ const planned = parseCore(planRun.stdout);
801
+ if (isObject(planned) && isObject(planned.plan)) {
802
+ if (typeof planned.plan.profileName === "string") targetProfile = planned.plan.profileName;
803
+ if (typeof planned.plan.requestedProfileName === "string") requestedProfileName = planned.plan.requestedProfileName;
804
+ }
805
+ }
806
+ } catch {}
807
+ const enriched = isObject(result) ? {
808
+ ...result,
809
+ ...targetProfile ? { targetProfile } : {},
810
+ ...requestedProfileName ? { requestedProfileName } : {}
811
+ } : result;
812
+ return {
813
+ status: "ok",
814
+ command: action,
815
+ exitCode: run.code,
816
+ result: enriched
817
+ };
818
+ } catch {
819
+ return {
820
+ status: "failed",
821
+ command: action,
822
+ exitCode: run.code,
823
+ error: coreDiagnostic(void 0, run.stderr || run.stdout || "dsh-crate inspect failed")
824
+ };
825
+ }
826
+ }
827
+ if (action === "export") {
828
+ const profileName = safeName(body.profileName, "profileName");
829
+ const profile = join(home, "profiles", profileName);
830
+ const profileRelative = relative(join(home, "profiles"), resolve(profile));
831
+ if (!profileRelative || profileRelative.startsWith("..") || profileRelative.includes("\\")) throw new Error("profileName escaped DSH_HOME");
832
+ const outputName = `${profileName}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${randomUUID()}.dshcrate`;
833
+ const args = [
834
+ "export",
835
+ "--profile",
836
+ profile,
837
+ "--output",
838
+ join(workspace.exports, outputName),
839
+ "--json",
840
+ ...runtimeCoreArgs()
841
+ ];
842
+ if (body.includeInstalledBundles === true) args.push("--include-installed-bundles");
843
+ const embed = Array.isArray(body.embed) ? body.embed : [];
844
+ const referenceOnly = Array.isArray(body.referenceOnly) ? body.referenceOnly : [];
845
+ for (const name of embed) args.push("--embed", safePluginName(name, "embed plugin"));
846
+ for (const name of referenceOnly) args.push("--reference-only", safePluginName(name, "reference-only plugin"));
847
+ const requiredSecrets = Array.isArray(body.requiredSecrets) ? body.requiredSecrets : [];
848
+ for (const name of requiredSecrets) args.push("--required-secret", safeName(name, "required Secret"));
849
+ const run = await runCore(home, args);
850
+ let result;
851
+ try {
852
+ result = parseCore(run.stdout);
853
+ } catch {
854
+ result = void 0;
855
+ }
856
+ if (run.code !== 0 || result === void 0) {
857
+ const failure = {
858
+ status: "failed",
859
+ action,
860
+ exitCode: run.code,
861
+ error: coreDiagnostic(result, run.stderr || run.stdout || "dsh-crate export failed"),
862
+ result
863
+ };
864
+ await record(workspace.history, {
865
+ time: (/* @__PURE__ */ new Date()).toISOString(),
866
+ action,
867
+ status: "FAIL",
868
+ profile: profileName,
869
+ message: String(failure.error.message ?? "dsh-crate export failed"),
870
+ code: failure.error.code,
871
+ stage: failure.error.stage
872
+ });
873
+ return failure;
874
+ }
875
+ const item = await record(workspace.history, {
876
+ time: (/* @__PURE__ */ new Date()).toISOString(),
877
+ action,
878
+ status: "PASS",
879
+ profile: profileName,
880
+ pack: outputName
881
+ });
882
+ return {
883
+ status: "ok",
884
+ command: action,
885
+ exitCode: run.code,
886
+ result,
887
+ downloadName: outputName,
888
+ history: item
889
+ };
890
+ }
891
+ if (action === "import") {
892
+ const args = [
893
+ "import",
894
+ await savePack(workspace.work, "import", body.packBase64),
895
+ "--dsh-home",
896
+ home,
897
+ "--json",
898
+ ...runtimeCoreArgs()
899
+ ];
900
+ if (typeof body.targetProfile === "string") args.push("--target-profile", safeName(body.targetProfile, "targetProfile"));
901
+ if (body.overwrite === true) args.push("--overwrite");
902
+ if (body.confirmOverwrite === true) args.push("--confirm-overwrite");
903
+ const run = await runCore(home, args);
904
+ let result;
905
+ try {
906
+ result = parseCore(run.stdout);
907
+ } catch {
908
+ result = void 0;
909
+ }
910
+ if (run.code !== 0 || result === void 0) {
911
+ const message = run.stderr || run.stdout || "dsh-crate import failed";
912
+ const failure = {
913
+ status: "failed",
914
+ action,
915
+ exitCode: run.code,
916
+ error: coreDiagnostic(result, message),
917
+ result
918
+ };
919
+ await record(workspace.history, {
920
+ time: (/* @__PURE__ */ new Date()).toISOString(),
921
+ action,
922
+ status: "FAIL",
923
+ message: String(failure.error.message ?? message),
924
+ code: failure.error.code,
925
+ stage: failure.error.stage
926
+ });
927
+ return failure;
928
+ }
929
+ await ensureCrateBundleInAllProfiles(home);
930
+ const plan = result.plan;
931
+ const profileName = plan !== null && typeof plan === "object" && !Array.isArray(plan) && typeof plan.profileName === "string" ? plan.profileName : void 0;
932
+ const item = await record(workspace.history, {
933
+ time: (/* @__PURE__ */ new Date()).toISOString(),
934
+ action,
935
+ status: "PASS",
936
+ ...profileName ? { profile: profileName } : {}
937
+ });
938
+ let profileRow;
939
+ if (profileName !== void 0) try {
940
+ profileRow = (await listProfiles(home)).find((candidate) => candidate.name === profileName);
941
+ } catch {}
942
+ return {
943
+ status: "ok",
944
+ command: action,
945
+ exitCode: run.code,
946
+ result,
947
+ ...profileRow !== void 0 ? { profile: profileRow } : {},
948
+ history: item
949
+ };
950
+ }
951
+ if (action === "delete-profile") {
952
+ const profileName = safeName(body.profileName, "profileName");
953
+ if (currentRuntime(home).currentProfile === profileName) return {
954
+ status: "failed",
955
+ command: action,
956
+ exitCode: 2,
957
+ error: operationDiagnostic("ACTIVE_PROFILE", "planning", profileName, `cannot delete the active Profile: ${profileName}`, "The running DSH process would lose its Profile files.", "Switch to another Profile first, then delete this Profile.")
958
+ };
959
+ const args = [
960
+ "delete-profile",
961
+ "--dsh-home",
962
+ home,
963
+ "--profile",
964
+ profileName,
965
+ "--json"
966
+ ];
967
+ if (body.confirmDelete === true) args.push("--confirm-delete");
968
+ const run = await runCore(home, args);
969
+ let result;
970
+ try {
971
+ result = parseCore(run.stdout);
972
+ } catch {
973
+ result = void 0;
974
+ }
975
+ if (run.code !== 0 || result === void 0) {
976
+ const message = run.stderr || run.stdout || "dsh-crate Profile deletion failed";
977
+ const failure = {
978
+ status: "failed",
979
+ action,
980
+ exitCode: run.code,
981
+ error: coreDiagnostic(result, message),
982
+ result
983
+ };
984
+ await record(workspace.history, {
985
+ time: (/* @__PURE__ */ new Date()).toISOString(),
986
+ action,
987
+ status: "FAIL",
988
+ profile: profileName,
989
+ message: String(failure.error.message ?? message),
990
+ code: failure.error.code,
991
+ stage: failure.error.stage
992
+ });
993
+ return failure;
994
+ }
995
+ const item = await record(workspace.history, {
996
+ time: (/* @__PURE__ */ new Date()).toISOString(),
997
+ action,
998
+ status: "PASS",
999
+ profile: profileName
1000
+ });
1001
+ return {
1002
+ status: "ok",
1003
+ command: action,
1004
+ exitCode: run.code,
1005
+ result,
1006
+ history: item
1007
+ };
1008
+ }
1009
+ if (action === "switch-status") {
1010
+ const operationId = typeof body.operationId === "string" && SAFE_OPERATION_ID.test(body.operationId) ? body.operationId : (() => {
1011
+ throw new Error("operationId is invalid");
1012
+ })();
1013
+ const reportPath = join(workspace.work, `switch-${operationId}.json`);
1014
+ try {
1015
+ const value = JSON.parse(await readFile(reportPath, "utf8"));
1016
+ return {
1017
+ status: "ok",
1018
+ command: action,
1019
+ operationId,
1020
+ result: isObject(value) ? value : {
1021
+ status: "failed",
1022
+ message: "switch report is malformed"
1023
+ }
1024
+ };
1025
+ } catch (error) {
1026
+ if (error.code === "ENOENT") return {
1027
+ status: "ok",
1028
+ command: action,
1029
+ operationId,
1030
+ result: {
1031
+ status: "pending",
1032
+ operationId,
1033
+ message: "Switch operation has not produced a report yet."
1034
+ }
1035
+ };
1036
+ throw error;
1037
+ }
1038
+ }
1039
+ if (action === "switch-profile") {
1040
+ const profileName = safeName(body.profileName, "profileName");
1041
+ if (body.confirmSwitch !== true) return {
1042
+ status: "failed",
1043
+ command: action,
1044
+ exitCode: 2,
1045
+ error: operationDiagnostic("SWITCH_CONFIRMATION_REQUIRED", "planning", profileName, "switching Profile requires explicit confirmation", "The current DSH process was not changed.", "Confirm the switch and retry.")
1046
+ };
1047
+ if (!(await listProfiles(home)).some((profile) => profile.name === profileName)) return {
1048
+ status: "failed",
1049
+ command: action,
1050
+ exitCode: 2,
1051
+ error: operationDiagnostic("PROFILE_MISSING", "planning", profileName, `Profile does not exist: ${profileName}`, "The current DSH process was not changed.", "Choose an existing Profile and retry.")
1052
+ };
1053
+ const runtime = currentRuntime(home);
1054
+ const oldProfile = typeof runtime.currentProfile === "string" ? runtime.currentProfile : void 0;
1055
+ if (oldProfile === profileName) {
1056
+ const item = await record(workspace.history, {
1057
+ time: (/* @__PURE__ */ new Date()).toISOString(),
1058
+ action,
1059
+ status: "PASS",
1060
+ profile: profileName,
1061
+ message: "Profile is already active; no restart was needed"
1062
+ });
1063
+ return {
1064
+ status: "ok",
1065
+ command: action,
1066
+ exitCode: 0,
1067
+ result: {
1068
+ status: "already-active",
1069
+ profileName,
1070
+ message: `Profile is already active: ${profileName}`,
1071
+ impact: "The current DSH process was not changed; the requested Profile is already running.",
1072
+ canContinue: true
1073
+ },
1074
+ history: item
1075
+ };
1076
+ }
1077
+ if (runtime.restartConfigured !== true || typeof process.argv[1] !== "string" || typeof runtime.port !== "number") return {
1078
+ status: "failed",
1079
+ command: action,
1080
+ exitCode: 2,
1081
+ error: operationDiagnostic("RESTART_NOT_CONFIGURED", "planning", profileName, "the current DSH launch command cannot be safely reconstructed", "The current DSH process was not changed.", "Start DSH with an explicit --profile and --port, or configure the DSH Crate restart launcher.")
1082
+ };
1083
+ const operationId = randomUUID();
1084
+ const reportPath = join(workspace.work, `switch-${operationId}.json`);
1085
+ const args = replaceArgument(replaceArgument(process.argv.slice(2), "--profile", profileName), "--port", String(runtime.port));
1086
+ const helperPath = join(dirname(fileURLToPath(import.meta.url)), "..", "restart-helper.mjs");
1087
+ try {
1088
+ await access(helperPath);
1089
+ } catch {
1090
+ throw new Error(`restart helper is missing: ${helperPath}`);
1091
+ }
1092
+ const spec = {
1093
+ operationId,
1094
+ reportPath,
1095
+ oldPid: process.pid,
1096
+ oldProfile,
1097
+ targetProfile: profileName,
1098
+ nodePath: process.execPath,
1099
+ scriptPath: resolve(process.argv[1]),
1100
+ args,
1101
+ cwd: process.cwd(),
1102
+ env: {
1103
+ ...process.env,
1104
+ DSH_HOME: home,
1105
+ DSH_PACK_ACTIVE_PROFILE: profileName,
1106
+ DSH_PACK_PORT: String(runtime.port)
1107
+ },
1108
+ url: `http://127.0.0.1:${runtime.port}/`
1109
+ };
1110
+ await writeFile(reportPath, `${JSON.stringify({
1111
+ status: "pending",
1112
+ operationId,
1113
+ stage: "scheduled",
1114
+ oldProfile,
1115
+ targetProfile: profileName,
1116
+ message: "Switch scheduled."
1117
+ }, null, 2)}\n`, {
1118
+ encoding: "utf8",
1119
+ flag: "wx"
1120
+ });
1121
+ const encoded = Buffer.from(JSON.stringify(spec), "utf8").toString("base64url");
1122
+ spawn(process.execPath, [helperPath, encoded], {
1123
+ detached: true,
1124
+ windowsHide: true,
1125
+ stdio: "ignore"
1126
+ }).unref();
1127
+ const item = await record(workspace.history, {
1128
+ time: (/* @__PURE__ */ new Date()).toISOString(),
1129
+ action,
1130
+ status: "PASS",
1131
+ profile: profileName,
1132
+ oldProfile,
1133
+ operationId,
1134
+ message: "Switch scheduled; waiting for the target Profile to become ready."
1135
+ });
1136
+ setTimeout(() => process.exit(0), 650);
1137
+ return {
1138
+ status: "ok",
1139
+ command: action,
1140
+ operationId,
1141
+ history: item,
1142
+ result: {
1143
+ status: "pending",
1144
+ operationId,
1145
+ oldProfile,
1146
+ targetProfile: profileName,
1147
+ message: "Switch scheduled; waiting for the target Profile to become ready."
1148
+ }
1149
+ };
1150
+ }
1151
+ if (action === "verify") {
1152
+ const profileName = safeName(body.profileName, "profileName");
1153
+ const mode = body.mode === void 0 ? "web" : body.mode;
1154
+ if (mode !== "web" && mode !== "headless") throw new Error("mode must be web or headless");
1155
+ const args = [
1156
+ "verify",
1157
+ "--dsh-home",
1158
+ home,
1159
+ "--profile",
1160
+ profileName,
1161
+ "--mode",
1162
+ mode,
1163
+ "--json",
1164
+ ...runtimeCoreArgs()
1165
+ ];
1166
+ let runnerConfigPath;
1167
+ if (body.runnerConfig !== void 0) {
1168
+ if (!isObject(body.runnerConfig)) throw new Error("runnerConfig must be a JSON object");
1169
+ const verifyDir = join(workspace.work, "verify");
1170
+ await mkdir(verifyDir, { recursive: true });
1171
+ runnerConfigPath = join(verifyDir, "verify-" + randomUUID() + ".json");
1172
+ await writeFile(runnerConfigPath, JSON.stringify(body.runnerConfig, null, 2) + "\n", {
1173
+ encoding: "utf8",
1174
+ flag: "wx"
1175
+ });
1176
+ args.push("--runner-config", runnerConfigPath);
1177
+ }
1178
+ const run = await runCore(home, args);
1179
+ let result;
1180
+ try {
1181
+ result = parseCore(run.stdout);
1182
+ } catch {
1183
+ result = void 0;
1184
+ }
1185
+ if (result === void 0) return {
1186
+ status: "failed",
1187
+ action,
1188
+ exitCode: run.code,
1189
+ error: coreDiagnostic(void 0, run.stderr || run.stdout || "dsh-crate verify failed")
1190
+ };
1191
+ const resultObject = isObject(result) ? result : {};
1192
+ const status = typeof resultObject.status === "string" ? resultObject.status : "FAIL";
1193
+ const success = status === "PASS";
1194
+ const item = await record(workspace.history, {
1195
+ time: (/* @__PURE__ */ new Date()).toISOString(),
1196
+ action,
1197
+ status,
1198
+ profile: profileName
1199
+ });
1200
+ return {
1201
+ status: "ok",
1202
+ command: action,
1203
+ exitCode: run.code,
1204
+ result,
1205
+ history: item,
1206
+ success
1207
+ };
1208
+ }
1209
+ throw new Error(`unsupported DSH Crate action: ${String(action)}`);
1210
+ }
1211
+ async function handleDownload(home, req, res) {
1212
+ if (req.method !== "GET" && req.method !== "HEAD") {
1213
+ res.writeHead(405);
1214
+ res.end();
1215
+ return;
1216
+ }
1217
+ const safe = safeName(new URL(req.url ?? DOWNLOAD_PATH, "http://dsh.local").searchParams.get("name"), "download name");
1218
+ const root = resolve(join(home, ".dsh-pack", "exports"));
1219
+ const path = resolve(join(root, safe));
1220
+ if (dirname(path) !== root) throw new Error("download path escaped export directory");
1221
+ const info = await stat(path);
1222
+ res.writeHead(200, {
1223
+ "content-type": "application/octet-stream",
1224
+ "content-length": info.size,
1225
+ "content-disposition": `attachment; filename="${safe}"`
1226
+ });
1227
+ if (req.method === "HEAD") {
1228
+ res.end();
1229
+ return;
1230
+ }
1231
+ createReadStream(path).pipe(res);
1232
+ }
1233
+ /** Register the host bridge. The Core executable is intentionally external. */
1234
+ const inject = ["webServer"];
1235
+ function apply(ctx) {
1236
+ const home = resolve(process.env.DSH_HOME || process.cwd());
1237
+ ensureCrateBundleInAllProfiles(home).catch((error) => console.error(`dsh-crate-web: failed to ensure Profile invariant: ${failMessage(error)}`));
1238
+ ctx.effect(() => {
1239
+ const disposeApi = ctx.webServer.register({
1240
+ kind: "exact",
1241
+ path: API_PATH,
1242
+ handler: async (req, res) => {
1243
+ if (req.method !== "POST") {
1244
+ res.writeHead(405);
1245
+ res.end();
1246
+ return;
1247
+ }
1248
+ try {
1249
+ json(res, 200, await handleApi(home, await readRequest(req), listRuntimePlugins(ctx)));
1250
+ } catch (error) {
1251
+ json(res, 400, {
1252
+ status: "failed",
1253
+ error: { message: failMessage(error) }
1254
+ });
1255
+ }
1256
+ }
1257
+ });
1258
+ const disposeDownload = ctx.webServer.register({
1259
+ kind: "exact",
1260
+ path: DOWNLOAD_PATH,
1261
+ handler: (req, res) => {
1262
+ handleDownload(home, req, res).catch((error) => json(res, 404, {
1263
+ status: "failed",
1264
+ error: { message: failMessage(error) }
1265
+ }));
1266
+ }
1267
+ });
1268
+ return () => {
1269
+ disposeApi();
1270
+ disposeDownload();
1271
+ };
1272
+ }, "dsh-crate-web: local Core bridge");
1273
+ }
1274
+ //#endregion
1275
+ export { apply, inject };