@deepseek-ai/dsh-plugin-manager 0.1.6-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js ADDED
@@ -0,0 +1,1174 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { mkdir, mkdtemp, open, readFile, rm } from "node:fs/promises";
4
+ import { isAbsolute, join, resolve } from "node:path";
5
+ import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
6
+ import z from "@deepseek-ai/schemastery";
7
+ import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
8
+ import { pluginEntryId, readPluginInventory } from "@deepseek-ai/dsh-host-plugin-inventory";
9
+ import { OPTIONAL_BUNDLES, composeEntries, loadOptionalPatches, loadOverlayPatches, readProfileManifest, readProfilePatches, reconcileProfilePatches, resolveBundleDir, resolveProfileDir } from "@deepseek-ai/dsh-app-boot";
10
+ import { execa } from "execa";
11
+ import { scrubbedParentEnv } from "@deepseek-ai/dsh-subprocess";
12
+ import { isAlias, isMap, isNode, isScalar, isSeq, parseDocument, visit } from "yaml";
13
+ //#region lib/types/operations.js
14
+ /** Resolve relative package specs against the caller's directory.
15
+ * @param argument One pnpm argument.
16
+ * @param cwd Invocation directory, never the profile directory.
17
+ * @returns Anchored argument.
18
+ */
19
+ function anchorPathSpec(argument, cwd) {
20
+ const match = /^(?<prefix>(?:file|link):)?(?<path>\.{1,2}(?:[/\\].*)?)$/.exec(argument);
21
+ if (match?.groups?.path === void 0) return argument;
22
+ return `${match.groups.prefix ?? ""}${resolve(cwd, match.groups.path)}`;
23
+ }
24
+ /** Read bundle metadata without loading its JavaScript.
25
+ * @param name Installed dependency or installation-owned package name.
26
+ * @param dir Profile directory.
27
+ * @param anchor Installation manifest.
28
+ * @returns Resolved metadata, or undefined for packages without bundle metadata.
29
+ */
30
+ function bundleManifest(name, dir, anchor) {
31
+ const manifest = readProfileManifest("dsh", resolveBundleDir("dsh", name, anchor, dir));
32
+ return manifest.dsh?.bundle?.patch === void 0 ? void 0 : manifest;
33
+ }
34
+ /** Atomically save a profile manifest while retaining unrelated fields.
35
+ * @param dir Profile directory.
36
+ * @param manifest Updated document.
37
+ */
38
+ async function saveManifest(dir, manifest) {
39
+ await writeFileAtomic(join(dir, "package.json"), JSON.stringify(manifest, void 0, 2) + "\n", { mode: 384 });
40
+ }
41
+ /** Reconcile package removals and newly installed bundles without re-enabling retained dependencies. */
42
+ async function reconcile(before, dir, anchor, options) {
43
+ const after = readProfileManifest("dsh", dir);
44
+ const dependencies = Object.keys(after.dependencies ?? {});
45
+ const beforeDeps = new Set(Object.keys(before.dependencies ?? {}));
46
+ const previous = after.dsh?.profile?.bundles ?? [];
47
+ const bundles = previous.filter((name) => {
48
+ if (!beforeDeps.has(name) && !dependencies.includes(name)) return true;
49
+ return dependencies.includes(name) && bundleManifest(name, dir, anchor) !== void 0;
50
+ });
51
+ for (const name of dependencies) {
52
+ if (beforeDeps.has(name)) continue;
53
+ const metadata = bundleManifest(name, dir, anchor);
54
+ if (metadata?.dsh?.bundle === void 0) {
55
+ options.onOutput?.(`dsh: warning: ${name} declares no dsh.bundle — installed as a plain dependency, not a profile layer\n`, "stderr");
56
+ continue;
57
+ }
58
+ loadOverlayPatches("dsh", join(resolveBundleDir("dsh", name, anchor, dir), metadata.dsh.bundle.patch));
59
+ if (!bundles.includes(name)) bundles.push(name);
60
+ }
61
+ if (JSON.stringify(previous) === JSON.stringify(bundles)) return;
62
+ after.dsh = {
63
+ ...after.dsh,
64
+ profile: {
65
+ ...after.dsh?.profile,
66
+ bundles
67
+ }
68
+ };
69
+ await saveManifest(dir, after);
70
+ }
71
+ /** Execute pnpm inside a profile whose caller already holds the profile write lock.
72
+ * @param context Launcher-owned profile and resolution locations.
73
+ * @param args Pnpm arguments, before relative path anchoring.
74
+ * @param options Output, activation and cancellation policy.
75
+ * @returns Exit status and diagnostic path; service output is bounded, CLI output uses inherited descriptors.
76
+ */
77
+ async function runProfilePnpm(context, args, options) {
78
+ const dir = context.dir ?? resolveProfileDir(context.profile, context.home);
79
+ const before = readProfileManifest("dsh", dir);
80
+ const logRoot = join(dir, ".plugin-manager", "logs");
81
+ await mkdir(logRoot, {
82
+ recursive: true,
83
+ mode: 448
84
+ });
85
+ const logPath = join(await mkdtemp(join(logRoot, "operation-")), "pnpm.log");
86
+ const log = await open(logPath, "wx", 384);
87
+ let output = Buffer.alloc(0);
88
+ let truncated = false;
89
+ const cancellation = new AbortController();
90
+ const child = execa(options.command ?? "pnpm", [...options.args ?? [], ...args.map((arg) => anchorPathSpec(arg, context.cwd))], {
91
+ cwd: dir,
92
+ env: {
93
+ ...options.execution === "cli" ? process.env : scrubbedParentEnv(),
94
+ ...options.env
95
+ },
96
+ extendEnv: false,
97
+ reject: false,
98
+ stdout: options.execution === "cli" ? "inherit" : "pipe",
99
+ stderr: options.execution === "cli" ? "inherit" : "pipe",
100
+ buffer: false,
101
+ stdin: options.execution === "cli" ? "inherit" : "ignore",
102
+ cancelSignal: options.signal === void 0 ? cancellation.signal : AbortSignal.any([cancellation.signal, options.signal])
103
+ });
104
+ let writes = Promise.resolve();
105
+ const collect = async (stream, kind) => {
106
+ try {
107
+ for await (const chunk of stream) {
108
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
109
+ writes = writes.then(async () => {
110
+ await log.write(bytes);
111
+ });
112
+ await writes;
113
+ options.onOutput?.(bytes.toString("utf8"), kind);
114
+ output = Buffer.concat([output, bytes]);
115
+ if (output.length > options.outputBytes) {
116
+ truncated = true;
117
+ output = output.subarray(output.length - options.outputBytes);
118
+ }
119
+ }
120
+ } catch (error) {
121
+ cancellation.abort();
122
+ throw error;
123
+ }
124
+ };
125
+ let exitCode;
126
+ try {
127
+ const [completion, ...streams] = await Promise.allSettled([
128
+ child,
129
+ ...child.stdout === null ? [] : [collect(child.stdout, "stdout")],
130
+ ...child.stderr === null ? [] : [collect(child.stderr, "stderr")]
131
+ ]);
132
+ for (const stream of streams) if (stream.status === "rejected") throw stream.reason;
133
+ if (completion.status === "rejected") throw completion.reason;
134
+ const result = completion.value;
135
+ exitCode = result.exitCode ?? (result.code === "ENOENT" ? 127 : 1);
136
+ if (result.failed && output.length === 0) {
137
+ const diagnostic = result.shortMessage ?? "pnpm failed";
138
+ await log.write(diagnostic);
139
+ truncated = Buffer.byteLength(diagnostic) > options.outputBytes;
140
+ output = Buffer.from(diagnostic).subarray(0, options.outputBytes);
141
+ }
142
+ if (exitCode === 0 && options.activateNewBundles !== false) await reconcile(before, dir, context.installAnchor, options);
143
+ } finally {
144
+ await log.close();
145
+ }
146
+ return {
147
+ exitCode,
148
+ output: output.toString("utf8"),
149
+ truncated,
150
+ logPath
151
+ };
152
+ }
153
+ /**
154
+ * Ask the registry what a spec names through `pnpm view`, run in the profile
155
+ * directory so the registry, proxy, and authentication settings of an install apply.
156
+ * @param dir Profile directory.
157
+ * @param spec One registry spec: a package name with an optional range.
158
+ * @param options Cancellation and the time bound.
159
+ * @returns pnpm's exit, output, and how the lookup ended.
160
+ */
161
+ async function viewProfilePackage(dir, spec, options) {
162
+ const result = await execa(options.command ?? "pnpm", [
163
+ ...options.args ?? [],
164
+ "view",
165
+ spec,
166
+ "name",
167
+ "version",
168
+ "description",
169
+ "dsh",
170
+ "--json"
171
+ ], {
172
+ cwd: dir,
173
+ env: {
174
+ ...scrubbedParentEnv(),
175
+ ...options.env
176
+ },
177
+ extendEnv: false,
178
+ reject: false,
179
+ stdin: "ignore",
180
+ timeout: options.timeoutMs,
181
+ ...options.signal === void 0 ? {} : { cancelSignal: options.signal }
182
+ });
183
+ const cause = result.exitCode === void 0 && !result.timedOut && !result.isCanceled ? Object.assign(new Error(result.shortMessage), { code: result.code }) : void 0;
184
+ return {
185
+ exitCode: result.exitCode ?? null,
186
+ stdout: result.stdout,
187
+ stderr: result.stderr,
188
+ timedOut: result.timedOut,
189
+ ...cause === void 0 ? {} : { cause }
190
+ };
191
+ }
192
+ //#endregion
193
+ //#region lib/types/install-failure.js
194
+ /**
195
+ * What a failed pnpm run was, read off how it ended and what it printed:
196
+ * pnpm names its failures with stable `ERR_PNPM_*` codes and Node's errno
197
+ * names, which the run's captured tail carries whatever the locale.
198
+ * @module @deepseek-ai/dsh-plugin-manager/install-failure
199
+ */
200
+ /** Patterns in the order they decide: a specific code before the generic network family. */
201
+ const LOG_KINDS = [
202
+ ["build-blocked", /ERR_PNPM_IGNORED_BUILDS|Ignored build scripts/],
203
+ ["not-found", /ERR_PNPM_FETCH_404|\bE404\b|404 Not Found|Not Found - GET/],
204
+ ["no-matching-version", /ERR_PNPM_NO_MATCHING_VERSION|\bETARGET\b|No matching version/],
205
+ ["disk-full", /\bENOSPC\b|no space left on device/i],
206
+ ["permission", /\bEACCES\b|\bEPERM\b|permission denied/i],
207
+ ["integrity", /ERR_PNPM_TARBALL_INTEGRITY|ERR_PNPM_BAD_TARBALL_SIZE|\bEINTEGRITY\b/],
208
+ ["network", /\bENOTFOUND\b|\bECONNRESET\b|\bETIMEDOUT\b|\bECONNREFUSED\b|\bEAI_AGAIN\b|ERR_PNPM_META_FETCH_FAIL|ERR_PNPM_FETCH_5\d\d|ERR_PNPM_FETCH_TIMEOUT|socket hang up|Could not resolve host|unable to access/]
209
+ ];
210
+ /**
211
+ * Classify a failed run.
212
+ * @param facts - how the run ended and what it printed.
213
+ * @returns the kind, `unknown` when nothing in the facts names one.
214
+ */
215
+ function classifyInstallFailure(facts) {
216
+ if (facts.timedOut === true) return "timeout";
217
+ if (facts.cause?.code === "ENOENT") return "pnpm-missing";
218
+ for (const [kind, pattern] of LOG_KINDS) if (pattern.test(facts.log)) return kind;
219
+ return "unknown";
220
+ }
221
+ //#endregion
222
+ //#region lib/types/install-spec.js
223
+ /**
224
+ * Reading an install spec before pnpm sees it: which of pnpm's spec forms it
225
+ * takes, and for a registry name whether it is one the registry can accept.
226
+ * @module @deepseek-ai/dsh-plugin-manager/install-spec
227
+ */
228
+ /** The forms pnpm resolves through a git host: a host shorthand, a git URL, or a hosted repository URL. */
229
+ const GIT_SHORTHAND = /^(?:github|gitlab|bitbucket|gist):/i;
230
+ const GIT_URL = /^git(?:\+[a-z]+)?:\/\/|^git@[^:]+:/i;
231
+ const HOSTED_REPOSITORY_URL = /^https?:\/\/[^/]+\/[^/]+\/[^/#]+(?:\.git)?(?:#.*)?$/i;
232
+ /** A tarball, on disk or over HTTP. */
233
+ const TARBALL_SPEC = /\.(?:tgz|tar\.gz)(?:#.*)?$/i;
234
+ /** An npm package name: lowercase URL-safe segments, an optional scope, no leading dot or underscore. */
235
+ const PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/;
236
+ const PACKAGE_NAME_MAX_LENGTH = 214;
237
+ /** A spec neither pnpm nor the registry would take; `reason` is what the person reads. */
238
+ var InvalidInstallSpecError = class extends Error {
239
+ spec;
240
+ reason;
241
+ /**
242
+ * @param spec - the spec as typed, trimmed.
243
+ * @param reason - why it is refused, as one sentence.
244
+ */
245
+ constructor(spec, reason) {
246
+ super(`plugin-manager: ${reason}: ${spec}`);
247
+ this.spec = spec;
248
+ this.reason = reason;
249
+ this.name = "InvalidInstallSpecError";
250
+ }
251
+ };
252
+ function invalid(spec, reason) {
253
+ return new InvalidInstallSpecError(spec, reason);
254
+ }
255
+ /**
256
+ * Read a spec into its form. A path must be absolute: the Host's working
257
+ * directory means nothing to the person typing into a browser, and a
258
+ * relative path resolved against the profile would point inside it.
259
+ * @param raw - the spec as typed.
260
+ * @returns the parsed spec.
261
+ * @throws {InvalidInstallSpecError} for an empty spec, a relative path, a name the
262
+ * registry would refuse, or a URL that is neither a git host nor a tarball.
263
+ */
264
+ function parseInstallSpec(raw) {
265
+ const spec = raw.trim();
266
+ if (spec === "") throw invalid(spec, "the package spec must not be empty");
267
+ const path = spec.replace(/^(?:file|link):/, "");
268
+ if (path !== spec || isAbsolute(path)) {
269
+ if (!isAbsolute(path)) throw invalid(spec, "a local path must be absolute");
270
+ return TARBALL_SPEC.test(path) ? {
271
+ kind: "tarball",
272
+ spec,
273
+ path
274
+ } : {
275
+ kind: "path",
276
+ spec,
277
+ path
278
+ };
279
+ }
280
+ if (/^\.{1,2}(?:[\\/]|$)/.test(spec)) throw invalid(spec, "a local path must be absolute");
281
+ if ((GIT_SHORTHAND.test(spec) || GIT_URL.test(spec) || HOSTED_REPOSITORY_URL.test(spec)) && !TARBALL_SPEC.test(spec)) return {
282
+ kind: "git",
283
+ spec
284
+ };
285
+ if (/^https?:\/\//i.test(spec)) {
286
+ if (TARBALL_SPEC.test(spec)) return {
287
+ kind: "tarball",
288
+ spec
289
+ };
290
+ throw invalid(spec, "a URL must point at a git repository or a tarball");
291
+ }
292
+ const at = spec.indexOf("@", 1);
293
+ const name = at === -1 ? spec : spec.slice(0, at);
294
+ const range = at === -1 ? void 0 : spec.slice(at + 1);
295
+ if (name.length > PACKAGE_NAME_MAX_LENGTH || !PACKAGE_NAME.test(name)) throw invalid(spec, "not a package name the registry accepts");
296
+ if (range === "") throw invalid(spec, "a version after @ must not be empty");
297
+ return range === void 0 ? {
298
+ kind: "registry",
299
+ spec,
300
+ name
301
+ } : {
302
+ kind: "registry",
303
+ spec,
304
+ name,
305
+ range
306
+ };
307
+ }
308
+ //#endregion
309
+ //#region lib/types/patch.js
310
+ /** Comment-preserving profile plugin enablement edits. */
311
+ /** Replace the last matching override or append one after existing insertions.
312
+ * @param filename Current profile patch file.
313
+ * @param id Unique composition entry id.
314
+ * @param name Module name used to match name-qualified overrides.
315
+ * @param enabled Desired entry enablement.
316
+ * @returns Whether the file changed.
317
+ */
318
+ async function writePluginEnabled(filename, id, name, enabled) {
319
+ let text;
320
+ try {
321
+ text = await readFile(filename, "utf8");
322
+ } catch (error) {
323
+ if (error.code !== "ENOENT") throw error;
324
+ text = "[]\n";
325
+ }
326
+ const document = parseDocument(text, { customTags: [{
327
+ tag: "tag:yaml.org,2002:js",
328
+ resolve: (value) => value
329
+ }] });
330
+ const error = document.errors[0];
331
+ if (error !== void 0) throw error;
332
+ if (!isSeq(document.contents)) throw new Error("Profile patch must be a YAML sequence");
333
+ loadOptionalPatches("dsh", filename);
334
+ const items = document.contents.items;
335
+ const target = items.findLast((item, index) => {
336
+ if (!isMap(item) || document.getIn([index, "id"]) !== id || item.has("insert")) return false;
337
+ const expectedName = document.getIn([index, "name"]);
338
+ return !expectedName || expectedName === name;
339
+ });
340
+ if (isMap(target)) {
341
+ if (document.getIn([items.indexOf(target), "disabled"]) === !enabled) return false;
342
+ document.setIn([items.indexOf(target), "disabled"], !enabled);
343
+ } else document.add({
344
+ id,
345
+ disabled: !enabled
346
+ });
347
+ await writeFileAtomic(filename, String(document), { mode: 384 });
348
+ return true;
349
+ }
350
+ //#endregion
351
+ //#region lib/types/failure.js
352
+ /** Expected management rejection; presentation belongs to the caller's locale. */
353
+ var ManagementFailure = class extends Error {
354
+ /** Code rendered by the caller's locale dictionary. */
355
+ code;
356
+ /** @param code Localizable management rejection. */
357
+ constructor(code) {
358
+ super(code);
359
+ this.code = code;
360
+ }
361
+ };
362
+ //#endregion
363
+ //#region lib/types/build-approval.js
364
+ /** Approve pnpm's pending dependency scripts in the current profile's workspace settings. */
365
+ async function readPolicy(dir) {
366
+ let text;
367
+ try {
368
+ text = await readFile(join(dir, "pnpm-workspace.yaml"), "utf8");
369
+ } catch (error) {
370
+ if (error.code !== "ENOENT") throw error;
371
+ text = "{}\n";
372
+ }
373
+ const document = parseDocument(text);
374
+ if (document.errors[0] !== void 0) throw document.errors[0];
375
+ if (!isMap(document.contents)) throw new Error("pnpm-workspace.yaml must be a YAML mapping");
376
+ const builds = document.get("allowBuilds");
377
+ if (builds !== void 0 && !isMap(builds)) throw new Error("allowBuilds must be a YAML mapping");
378
+ visit(builds ?? null, (_key, node) => {
379
+ if (isAlias(node) || isNode(node) && "anchor" in node && node.anchor) throw new Error("allowBuilds must not contain YAML anchors or aliases");
380
+ });
381
+ return {
382
+ document,
383
+ pending: isMap(builds) ? builds.items.flatMap(({ key, value }) => isScalar(key) && typeof key.value === "string" && !/[*?]/.test(key.value) && isScalar(value) && value.value === "set this to true or false" ? [key.value] : []) : []
384
+ };
385
+ }
386
+ /** Read package names left undecided by pnpm 11, including after installation cleanup.
387
+ * @param dir Current profile directory.
388
+ * @returns Exact package names awaiting a build decision; wildcard rules are excluded.
389
+ */
390
+ async function readPendingBuilds(dir) {
391
+ return (await readPolicy(dir)).pending;
392
+ }
393
+ /** Persist approval without running scripts; the caller holds the profile manifest lock.
394
+ * @param dir Current profile directory.
395
+ * @param names Explicit package names from the pending build list.
396
+ * @throws If a name is no longer pending or allowBuilds contains YAML anchors or aliases; no approvals are written.
397
+ */
398
+ async function approveBuilds(dir, names) {
399
+ const { document, pending } = await readPolicy(dir);
400
+ if (names.some((name) => !pending.includes(name))) throw new ManagementFailure("stale-approval");
401
+ if (names.length === 0) return;
402
+ for (const name of names) document.setIn(["allowBuilds", name], true);
403
+ await writeFileAtomic(join(dir, "pnpm-workspace.yaml"), String(document), { mode: 384 });
404
+ }
405
+ //#endregion
406
+ //#region lib/types/index.js
407
+ /** Current-profile plugin and bundle management over shared dsh plugin operations. */
408
+ var __runInitializers = function(thisArg, initializers, value) {
409
+ var useValue = arguments.length > 2;
410
+ for (var i = 0; i < initializers.length; i++) value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
411
+ return useValue ? value : void 0;
412
+ };
413
+ var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
414
+ function accept(f) {
415
+ if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected");
416
+ return f;
417
+ }
418
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
419
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
420
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
421
+ var _, done = false;
422
+ for (var i = decorators.length - 1; i >= 0; i--) {
423
+ var context = {};
424
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
425
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
426
+ context.addInitializer = function(f) {
427
+ if (done) throw new TypeError("Cannot add initializers after decoration has completed");
428
+ extraInitializers.push(accept(f || null));
429
+ };
430
+ var result = (0, decorators[i])(kind === "accessor" ? {
431
+ get: descriptor.get,
432
+ set: descriptor.set
433
+ } : descriptor[key], context);
434
+ if (kind === "accessor") {
435
+ if (result === void 0) continue;
436
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
437
+ if (_ = accept(result.get)) descriptor.get = _;
438
+ if (_ = accept(result.set)) descriptor.set = _;
439
+ if (_ = accept(result.init)) initializers.unshift(_);
440
+ } else if (_ = accept(result)) if (kind === "field") initializers.unshift(_);
441
+ else descriptor[key] = _;
442
+ }
443
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
444
+ done = true;
445
+ };
446
+ const protectedModules = new Set([
447
+ "@deepseek-ai/dsh-plugin-manager",
448
+ "@deepseek-ai/cordis-plugin-loader",
449
+ "@deepseek-ai/cordis-plugin-include",
450
+ "@deepseek-ai/dsh-api-gateway",
451
+ "@deepseek-ai/dsh-host-webserver",
452
+ "@deepseek-ai/dsh-client-modules",
453
+ "@deepseek-ai/dsh-client-ui-settings-plugin-inventory",
454
+ "@deepseek-ai/dsh-client-ui-plugin-manager",
455
+ "@deepseek-ai/dsh-host-plugin-inventory",
456
+ "@deepseek-ai/dsh-typert-registry",
457
+ "@deepseek-ai/dsh-api-remotes",
458
+ "@deepseek-ai/cordis-plugin-timer",
459
+ "@deepseek-ai/dsh-client-connection",
460
+ "@deepseek-ai/dsh-host-frontend-static",
461
+ "@deepseek-ai/dsh-tools",
462
+ "@deepseek-ai/dsh-hmr"
463
+ ]);
464
+ /** The profile files an installation writes and a failed or cancelled one restores. */
465
+ const RESTORED_FILES = ["package.json", "pnpm-lock.yaml"];
466
+ /** pnpm's colour escapes, which a JSON answer may be wrapped in. */
467
+ const ANSI_SEQUENCE = /\x1b\[[0-9;]*m/g;
468
+ /** Flatten only the groups addressable by the profile's patch composer. */
469
+ function flatten(rows) {
470
+ return rows.flatMap((row) => [row, ...row.group && Array.isArray(row.config) ? flatten(row.config) : []]);
471
+ }
472
+ /** Preserve the exact observed diagnostic, including non-Error failures. */
473
+ function messageOf(error) {
474
+ return error instanceof Error ? error.message : String(error);
475
+ }
476
+ /** An expected refusal keeps its code; anything else becomes an operation error carrying its exact diagnostic. */
477
+ function managementError(error) {
478
+ return error instanceof ManagementFailure ? { code: error.code } : {
479
+ code: "operation-error",
480
+ diagnostic: messageOf(error)
481
+ };
482
+ }
483
+ /** The caller stopped an installation; its files are restored before this is thrown. */
484
+ var InstallCancelledError = class extends Error {
485
+ constructor() {
486
+ super("Installation cancelled");
487
+ this.name = "InstallCancelledError";
488
+ }
489
+ };
490
+ /** A manifest field that is a string, when the manifest carries one. */
491
+ function stringField(manifest, field) {
492
+ const value = manifest[field];
493
+ return typeof value === "string" ? value : void 0;
494
+ }
495
+ /** What a package manifest says about the package: identity, one-liner, and whether it is a bundle. */
496
+ function inspectionOf(kind, manifest) {
497
+ const dsh = manifest.dsh;
498
+ const declared = typeof dsh === "object" && dsh !== null ? dsh : void 0;
499
+ const bundle = declared !== void 0 && typeof declared.bundle === "object" && declared.bundle !== null;
500
+ const name = stringField(manifest, "name");
501
+ const version = stringField(manifest, "version");
502
+ const description = stringField(manifest, "description");
503
+ return {
504
+ status: "accepted",
505
+ kind,
506
+ bundle,
507
+ ...name === void 0 ? {} : { name },
508
+ ...version === void 0 ? {} : { version },
509
+ ...description === void 0 || description === "" ? {} : { description }
510
+ };
511
+ }
512
+ function refused(problem, reason) {
513
+ return {
514
+ status: "refused",
515
+ problem,
516
+ reason
517
+ };
518
+ }
519
+ /** Manage profile files and apply their declared reload lifecycle. */
520
+ let PluginManager = (() => {
521
+ let _classSuper = TypertRemoteService;
522
+ let _instanceExtraInitializers = [];
523
+ let _listPlugins_decorators;
524
+ let _listBundles_decorators;
525
+ let _inspect_decorators;
526
+ let _setPluginEnabled_decorators;
527
+ let _setBundleEnabled_decorators;
528
+ let _installBundle_decorators;
529
+ let _cancelInstall_decorators;
530
+ let _removeBundle_decorators;
531
+ return class PluginManager extends _classSuper {
532
+ static {
533
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
534
+ _listPlugins_decorators = [Remote];
535
+ _listBundles_decorators = [Remote];
536
+ _inspect_decorators = [Remote];
537
+ _setPluginEnabled_decorators = [Remote];
538
+ _setBundleEnabled_decorators = [Remote];
539
+ _installBundle_decorators = [Remote];
540
+ _cancelInstall_decorators = [Remote];
541
+ _removeBundle_decorators = [Remote];
542
+ __esDecorate(this, null, _listPlugins_decorators, {
543
+ kind: "method",
544
+ name: "listPlugins",
545
+ static: false,
546
+ private: false,
547
+ access: {
548
+ has: (obj) => "listPlugins" in obj,
549
+ get: (obj) => obj.listPlugins
550
+ },
551
+ metadata: _metadata
552
+ }, null, _instanceExtraInitializers);
553
+ __esDecorate(this, null, _listBundles_decorators, {
554
+ kind: "method",
555
+ name: "listBundles",
556
+ static: false,
557
+ private: false,
558
+ access: {
559
+ has: (obj) => "listBundles" in obj,
560
+ get: (obj) => obj.listBundles
561
+ },
562
+ metadata: _metadata
563
+ }, null, _instanceExtraInitializers);
564
+ __esDecorate(this, null, _inspect_decorators, {
565
+ kind: "method",
566
+ name: "inspect",
567
+ static: false,
568
+ private: false,
569
+ access: {
570
+ has: (obj) => "inspect" in obj,
571
+ get: (obj) => obj.inspect
572
+ },
573
+ metadata: _metadata
574
+ }, null, _instanceExtraInitializers);
575
+ __esDecorate(this, null, _setPluginEnabled_decorators, {
576
+ kind: "method",
577
+ name: "setPluginEnabled",
578
+ static: false,
579
+ private: false,
580
+ access: {
581
+ has: (obj) => "setPluginEnabled" in obj,
582
+ get: (obj) => obj.setPluginEnabled
583
+ },
584
+ metadata: _metadata
585
+ }, null, _instanceExtraInitializers);
586
+ __esDecorate(this, null, _setBundleEnabled_decorators, {
587
+ kind: "method",
588
+ name: "setBundleEnabled",
589
+ static: false,
590
+ private: false,
591
+ access: {
592
+ has: (obj) => "setBundleEnabled" in obj,
593
+ get: (obj) => obj.setBundleEnabled
594
+ },
595
+ metadata: _metadata
596
+ }, null, _instanceExtraInitializers);
597
+ __esDecorate(this, null, _installBundle_decorators, {
598
+ kind: "method",
599
+ name: "installBundle",
600
+ static: false,
601
+ private: false,
602
+ access: {
603
+ has: (obj) => "installBundle" in obj,
604
+ get: (obj) => obj.installBundle
605
+ },
606
+ metadata: _metadata
607
+ }, null, _instanceExtraInitializers);
608
+ __esDecorate(this, null, _cancelInstall_decorators, {
609
+ kind: "method",
610
+ name: "cancelInstall",
611
+ static: false,
612
+ private: false,
613
+ access: {
614
+ has: (obj) => "cancelInstall" in obj,
615
+ get: (obj) => obj.cancelInstall
616
+ },
617
+ metadata: _metadata
618
+ }, null, _instanceExtraInitializers);
619
+ __esDecorate(this, null, _removeBundle_decorators, {
620
+ kind: "method",
621
+ name: "removeBundle",
622
+ static: false,
623
+ private: false,
624
+ access: {
625
+ has: (obj) => "removeBundle" in obj,
626
+ get: (obj) => obj.removeBundle
627
+ },
628
+ metadata: _metadata
629
+ }, null, _instanceExtraInitializers);
630
+ if (_metadata) Object.defineProperty(this, Symbol.metadata, {
631
+ enumerable: true,
632
+ configurable: true,
633
+ writable: true,
634
+ value: _metadata
635
+ });
636
+ }
637
+ static inject = ["loader", "profileContext"];
638
+ static Config = z.object({
639
+ pnpmCommand: z.string().default("pnpm"),
640
+ outputBytes: z.number().step(1).min(1).default(16384),
641
+ lockWaitMs: z.number().step(1).min(0).default(12e4),
642
+ inspectTimeoutMs: z.number().step(1).min(1e3).default(2e4)
643
+ });
644
+ ownerEntryId = __runInitializers(this, _instanceExtraInitializers);
645
+ packageOperations = /* @__PURE__ */ new Set();
646
+ profile;
647
+ outputBytes;
648
+ lockWaitMs;
649
+ inspectTimeoutMs;
650
+ pnpmCommand;
651
+ ownerContext;
652
+ abort = new AbortController();
653
+ /** Installations by request id, from their call until it settles. */
654
+ installs = /* @__PURE__ */ new Map();
655
+ constructor(ctx, config) {
656
+ super(ctx, "pluginManager");
657
+ this.ownerEntryId = ctx.fiber.entry?.id;
658
+ this.ownerContext = ctx;
659
+ this.profile = ctx.profileContext;
660
+ this.outputBytes = config.outputBytes;
661
+ this.lockWaitMs = config.lockWaitMs;
662
+ this.inspectTimeoutMs = config.inspectTimeoutMs;
663
+ this.pnpmCommand = config.pnpmCommand;
664
+ ctx.effect(() => async () => {
665
+ this.abort.abort();
666
+ await Promise.allSettled([...this.packageOperations]);
667
+ }, "plugin-manager: package cancellation");
668
+ }
669
+ /** Read current plugins, including why a row cannot be changed through the profile patch.
670
+ * @returns Current runtime entries with persistent patch targets.
671
+ */
672
+ async listPlugins() {
673
+ const rows = flatten(composeEntries([readProfilePatches("dsh", this.profile)]));
674
+ return (await readPluginInventory(this.ctx)).entries.map((entry) => {
675
+ const actual = [...this.ctx.loader.entries()].find((row) => row.id === entry.entryId);
676
+ const candidates = rows.filter((row) => row.id === actual?.options.id);
677
+ const candidate = candidates[0];
678
+ if (protectedModules.has(entry.moduleName) || entry.entryId === this.ownerEntryId) return {
679
+ ...entry,
680
+ readOnlyReason: "management-required"
681
+ };
682
+ if (candidate === void 0 || candidates.length > 1 || candidate.name !== entry.moduleName || actual?.parent.tree.ctx.fiber.entry?.id !== "include") return {
683
+ ...entry,
684
+ readOnlyReason: "unaddressable"
685
+ };
686
+ return {
687
+ ...entry,
688
+ patchId: candidate.id
689
+ };
690
+ });
691
+ }
692
+ /** Read the profile's installed bundles, the bundles this dsh installation supplies, and the selected names that are not bundles.
693
+ * A dependency without a bundle patch is listed, as a `not-bundle` problem, only while it is selected.
694
+ * @returns Package versions, one-liners, rows, activation selections, whether the installation offers the
695
+ * bundle, and removal availability.
696
+ */
697
+ listBundles() {
698
+ const manifest = readProfileManifest("dsh", this.profile.dir);
699
+ const selected = manifest.dsh?.profile?.bundles ?? [];
700
+ const dependencies = Object.keys(manifest.dependencies ?? {});
701
+ const installation = JSON.parse(readFileSync(this.profile.installAnchor, "utf8"));
702
+ const names = [...new Set([
703
+ ...selected,
704
+ ...dependencies,
705
+ ...Object.keys(installation.dependencies ?? {})
706
+ ])];
707
+ const bundles = [];
708
+ for (const name of names) {
709
+ const installed = dependencies.includes(name);
710
+ const optional = OPTIONAL_BUNDLES.includes(name);
711
+ const removable = installed && !Object.hasOwn(installation.dependencies ?? {}, name);
712
+ const enabled = selected.includes(name);
713
+ try {
714
+ const info = bundleManifest(name, this.profile.dir, this.profile.installAnchor);
715
+ if (info === void 0) {
716
+ if (enabled) bundles.push({
717
+ name,
718
+ enabled,
719
+ installed,
720
+ optional,
721
+ removable,
722
+ error: { code: "not-bundle" },
723
+ rows: [],
724
+ overrides: []
725
+ });
726
+ continue;
727
+ }
728
+ const readOnlyReason = this.protectsManager(name) ? "management-required" : void 0;
729
+ bundles.push({
730
+ name,
731
+ ...info.version === void 0 ? {} : { version: info.version },
732
+ ...info.description === void 0 || info.description === "" ? {} : { description: info.description },
733
+ enabled,
734
+ installed,
735
+ optional,
736
+ removable: removable && readOnlyReason === void 0,
737
+ ...readOnlyReason === void 0 ? {} : { readOnlyReason },
738
+ ...this.declaredRows(name, info)
739
+ });
740
+ } catch (error) {
741
+ if (enabled || installed) bundles.push({
742
+ name,
743
+ enabled,
744
+ installed,
745
+ optional,
746
+ removable,
747
+ error: managementError(error),
748
+ rows: [],
749
+ overrides: []
750
+ });
751
+ }
752
+ }
753
+ return Promise.resolve(bundles);
754
+ }
755
+ /** Read what a spec names before installing it.
756
+ * @param spec One package spec: a registry name, an absolute path, a git address, or a tarball.
757
+ * @param signal Ends a registry lookup early.
758
+ * @returns The package the spec names, or why it is refused.
759
+ */
760
+ async inspect(spec, signal) {
761
+ let parsed;
762
+ try {
763
+ parsed = parseInstallSpec(spec);
764
+ } catch (error) {
765
+ /* v8 ignore next 2 -- parseInstallSpec throws nothing but its own refusal */
766
+ if (!(error instanceof InvalidInstallSpecError)) throw error;
767
+ return refused("invalid-spec", error.reason);
768
+ }
769
+ const manifest = readProfileManifest("dsh", this.profile.dir);
770
+ const installation = JSON.parse(readFileSync(this.profile.installAnchor, "utf8"));
771
+ const known = new Set([
772
+ ...manifest.dsh?.profile?.bundles ?? [],
773
+ ...Object.keys(manifest.dependencies ?? {}),
774
+ ...Object.keys(installation.dependencies ?? {})
775
+ ]);
776
+ switch (parsed.kind) {
777
+ case "git": return {
778
+ status: "accepted",
779
+ kind: "git",
780
+ bundle: null
781
+ };
782
+ case "tarball":
783
+ if (parsed.path !== void 0 && !existsSync(parsed.path)) return refused("not-a-package", "the tarball does not exist");
784
+ return {
785
+ status: "accepted",
786
+ kind: "tarball",
787
+ bundle: null
788
+ };
789
+ case "path": {
790
+ if (!existsSync(parsed.path)) return refused("not-a-package", "the path does not exist");
791
+ let read;
792
+ try {
793
+ read = JSON.parse(await readFile(join(parsed.path, "package.json"), "utf8"));
794
+ } catch (error) {
795
+ return refused("not-a-package", `no readable package.json at the path: ${messageOf(error)}`);
796
+ }
797
+ const inspection = inspectionOf("path", read);
798
+ if (inspection.name === void 0) return refused("not-a-package", "the package.json names no package");
799
+ if (known.has(inspection.name)) return refused("already-installed", `${inspection.name} is already installed`);
800
+ if (!inspection.bundle) return refused("not-a-bundle", `${inspection.name} declares no dsh.bundle`);
801
+ return inspection;
802
+ }
803
+ case "registry": {
804
+ if (known.has(parsed.name)) return refused("already-installed", `${parsed.name} is already installed`);
805
+ const view = await viewProfilePackage(this.profile.dir, spec.trim(), {
806
+ ...this.profile.packageManager ?? { command: this.pnpmCommand },
807
+ timeoutMs: this.inspectTimeoutMs,
808
+ ...signal === void 0 ? {} : { signal }
809
+ });
810
+ const log = `${view.stderr}${view.cause === void 0 ? "" : `${messageOf(view.cause)}\n`}`.trim();
811
+ if (view.exitCode !== 0 || view.cause !== void 0 || view.timedOut) {
812
+ const kind = classifyInstallFailure({
813
+ log,
814
+ timedOut: view.timedOut,
815
+ ...view.cause === void 0 ? {} : { cause: view.cause }
816
+ });
817
+ const reason = log || view.stdout.trim() || `pnpm view exited with ${String(view.exitCode)}`;
818
+ if (kind === "not-found" || kind === "no-matching-version") return refused("not-found", reason);
819
+ if (kind === "network") return refused("network", reason);
820
+ return refused("unknown", view.timedOut ? `pnpm view timed out after ${String(this.inspectTimeoutMs)}ms` : reason);
821
+ }
822
+ let answer;
823
+ try {
824
+ answer = JSON.parse(view.stdout.replace(ANSI_SEQUENCE, "").trim() || "null");
825
+ } catch (error) {
826
+ return refused("unknown", `unreadable pnpm view output: ${messageOf(error)}`);
827
+ }
828
+ const latest = Array.isArray(answer) ? answer.at(-1) : answer;
829
+ if (typeof latest !== "object" || latest === null) return refused("unknown", "pnpm view answered no package");
830
+ const inspection = inspectionOf("registry", latest);
831
+ const named = inspection.name === void 0 ? {
832
+ ...inspection,
833
+ name: parsed.name
834
+ } : inspection;
835
+ if (!named.bundle) return refused("not-a-bundle", `${named.name} declares no dsh.bundle`);
836
+ return named;
837
+ }
838
+ }
839
+ }
840
+ /** Persist a plugin entry's desired enablement and apply it on live profiles.
841
+ * @param id Loader entry identity returned by listPlugins.
842
+ * @param enabled Whether the plugin should run.
843
+ * @returns Saved and runtime outcomes, including higher-priority overrides.
844
+ */
845
+ setPluginEnabled(id, enabled) {
846
+ return this.change((result) => this.configure(async () => {
847
+ const row = (await this.listPlugins()).find((item) => item.entryId === id);
848
+ if (row === void 0) throw new ManagementFailure("unknown-plugin");
849
+ if (row.readOnlyReason !== void 0) throw new ManagementFailure(row.readOnlyReason);
850
+ await writePluginEnabled(this.profile.patchPath, row.patchId, row.moduleName, enabled);
851
+ result.warnings = await this.reload(enabled ? [row.patchId] : []);
852
+ return (await this.listPlugins()).find((item) => item.entryId === id)?.enabled !== enabled && this.ownerContext.get("hmr") !== void 0 ? "overridden" : void 0;
853
+ }), {
854
+ stage: "enable",
855
+ target: id,
856
+ enabled
857
+ }, "plugin");
858
+ }
859
+ /** Select or remove a bundle layer while retaining installed dependencies.
860
+ * @param name Bundle package name.
861
+ * @param enabled Whether the bundle contributes its patch layer.
862
+ * @returns Persisted and runtime outcomes.
863
+ */
864
+ setBundleEnabled(name, enabled) {
865
+ return this.change((result) => this.configure(async () => {
866
+ await this.selectBundle(name, enabled);
867
+ result.warnings = await this.reload(enabled ? this.bundleRows(name).map((row) => row.id) : []);
868
+ }), {
869
+ stage: "enable",
870
+ target: name,
871
+ enabled
872
+ }, "bundle");
873
+ }
874
+ /**
875
+ * Install a package using the same pnpm implementation as dsh plugin. A run
876
+ * that fails, is cancelled, or adds a package without a bundle patch restores
877
+ * `package.json` and `pnpm-lock.yaml` as they were; downloaded files can stay.
878
+ * @param spec One package spec, including local paths relative to the invocation directory.
879
+ * @param options Whether to activate the installed bundle (defaults to true), the request id a cancellation names, and
880
+ * the pending build scripts to allow for this profile before pnpm runs.
881
+ * @returns Package-manager diagnostics and observed activation outcome.
882
+ */
883
+ installBundle(spec, options) {
884
+ const requestId = options?.requestId;
885
+ const control = {
886
+ abort: new AbortController(),
887
+ phase: "installing",
888
+ settled: Promise.resolve()
889
+ };
890
+ const stopped = () => control.abort.signal.aborted;
891
+ if (requestId !== void 0) this.installs.set(requestId, control);
892
+ const announce = (phase) => {
893
+ if (requestId !== void 0) this.ownerContext.emit("plugin-manager/install-state", {
894
+ requestId,
895
+ phase
896
+ });
897
+ };
898
+ const result = this.change(async (result) => {
899
+ if (spec.trim() === "" || spec.startsWith("-")) throw new ManagementFailure("invalid-spec");
900
+ if (stopped()) throw new InstallCancelledError();
901
+ if (options?.approvedBuilds !== void 0) {
902
+ await approveBuilds(this.profile.dir, options.approvedBuilds);
903
+ result.approvedBuilds = options.approvedBuilds;
904
+ }
905
+ const files = await this.readRestoredFiles();
906
+ const before = readProfileManifest("dsh", this.profile.dir).dependencies ?? {};
907
+ announce("installing");
908
+ let name;
909
+ try {
910
+ result.packageResult = await this.runPnpm(["add", spec], control.abort.signal, requestId);
911
+ if (stopped()) throw new InstallCancelledError();
912
+ if (result.packageResult.exitCode !== 0) {
913
+ try {
914
+ result.pendingBuilds = await readPendingBuilds(this.profile.dir);
915
+ } catch (error) {
916
+ this.ownerContext.logger.warn("Could not read pending build approvals after pnpm failed", error);
917
+ }
918
+ throw new Error(result.packageResult.output);
919
+ }
920
+ const after = readProfileManifest("dsh", this.profile.dir).dependencies ?? {};
921
+ const installed = Object.keys(after).filter((name) => before[name] !== after[name]);
922
+ if (installed.length === 0) installed.push(...Object.keys(after).filter((name) => spec === name || spec.startsWith(`${name}@`)));
923
+ const target = installed[0];
924
+ if (installed.length !== 1 || target === void 0) throw new ManagementFailure("ambiguous-install");
925
+ name = target;
926
+ const dir = resolveBundleDir("dsh", name, this.profile.installAnchor, this.profile.dir);
927
+ const manifest = bundleManifest(name, this.profile.dir, this.profile.installAnchor);
928
+ if (manifest?.dsh?.bundle?.patch === void 0) throw new ManagementFailure("not-bundle");
929
+ loadOverlayPatches("dsh", join(dir, manifest.dsh.bundle.patch));
930
+ } catch (error) {
931
+ await this.restoreFiles(files);
932
+ throw error;
933
+ }
934
+ control.phase = "applying";
935
+ announce("applying");
936
+ result.bundle = name;
937
+ result.target = name;
938
+ result.stage = "enable";
939
+ return this.configure(async () => {
940
+ if (options?.enabled !== false) await this.selectBundle(name, true);
941
+ if (Object.hasOwn(before, name)) return "restart-required";
942
+ if (options?.enabled !== false) result.warnings = await this.reload();
943
+ });
944
+ }, {
945
+ stage: "install",
946
+ target: spec,
947
+ enabled: options?.enabled !== false
948
+ }, "install");
949
+ /* v8 ignore next -- change() folds every failure into its result; only a lock or disposal error rejects */
950
+ control.settled = result.then(() => void 0, () => void 0);
951
+ return result.finally(() => {
952
+ if (requestId !== void 0) this.installs.delete(requestId);
953
+ });
954
+ }
955
+ /** Stop an installation this manager owns and wait until its files are back.
956
+ * @param requestId The id the installation was started with.
957
+ * @returns `cancelled` once pnpm exited and the files are restored, `too-late` once the bundle is being
958
+ * applied, `not-running` for any other id.
959
+ */
960
+ async cancelInstall(requestId) {
961
+ const control = this.installs.get(requestId);
962
+ if (control === void 0) return { status: "not-running" };
963
+ if (control.phase === "applying") return { status: "too-late" };
964
+ this.ownerContext.emit("plugin-manager/install-state", {
965
+ requestId,
966
+ phase: "cancelling"
967
+ });
968
+ control.abort.abort();
969
+ await control.settled;
970
+ return { status: "cancelled" };
971
+ }
972
+ /** Unload and remove a profile-owned bundle dependency through dsh plugin's pnpm path.
973
+ * @param name Installed dependency name.
974
+ * @returns Removal diagnostics and the remaining profile state.
975
+ */
976
+ removeBundle(name) {
977
+ return this.change(async (result) => {
978
+ await this.configure(async () => {
979
+ const bundle = (await this.listBundles()).find((item) => item.name === name);
980
+ if (bundle === void 0 || !bundle.removable) throw new ManagementFailure("not-removable");
981
+ if (this.ownerContext.get("hmr") === void 0 && (this.profile.startedBundles.includes(name) || this.bundleRows(name).some((row) => [...this.ctx.loader.entries()].some((entry) => entry.options.id === row.id && entry.fiber !== void 0)))) throw new ManagementFailure("stop-profile");
982
+ const contributions = bundle.error === void 0 ? this.bundleRows(name) : [];
983
+ if (bundle.enabled) {
984
+ await this.selectBundle(name, false);
985
+ result.warnings = await this.reload();
986
+ }
987
+ if ([...this.ctx.loader.entries()].some((entry) => entry.fiber?.uid != null && contributions.some((row) => row.id === entry.options.id && row.name === entry.options.name))) throw new ManagementFailure("bundle-in-use");
988
+ });
989
+ result.packageResult = await this.runPnpm(["remove", name]);
990
+ if (result.packageResult.exitCode !== 0) throw new Error(result.packageResult.output);
991
+ }, {
992
+ stage: "remove",
993
+ target: name
994
+ }, "remove");
995
+ }
996
+ /** The rows a bundle's patch inserts and the existing rows it changes; an unreadable patch throws. */
997
+ declaredRows(name, info) {
998
+ const patch = info.dsh?.bundle?.patch;
999
+ /* v8 ignore next -- bundleManifest answers only manifests that declare a patch */
1000
+ if (patch === void 0) return {
1001
+ rows: [],
1002
+ overrides: []
1003
+ };
1004
+ const patches = loadOverlayPatches("dsh", join(resolveBundleDir("dsh", name, this.profile.installAnchor, this.profile.dir), patch));
1005
+ const live = /* @__PURE__ */ new Map();
1006
+ for (const entry of this.ctx.loader.entries())
1007
+ /* v8 ignore next -- the Loader gives every entry an id before it is listed */
1008
+ if (typeof entry.options.id === "string") live.set(entry.options.id, pluginEntryId(entry.id));
1009
+ const rows = [];
1010
+ for (const row of flatten(composeEntries([patches.filter((item) => item.insert !== void 0)]))) {
1011
+ if (typeof row.id !== "string" || typeof row.name !== "string") continue;
1012
+ const entryId = live.get(row.id);
1013
+ rows.push({
1014
+ rowId: row.id,
1015
+ moduleName: row.name,
1016
+ ...entryId === void 0 ? {} : { entryId }
1017
+ });
1018
+ }
1019
+ const declared = new Set(rows.map((row) => row.rowId));
1020
+ return {
1021
+ rows,
1022
+ overrides: [...new Set(patches.flatMap((item) => item.insert === void 0 && typeof item.id === "string" && !declared.has(item.id) ? [item.id] : []))]
1023
+ };
1024
+ }
1025
+ /** Run one pnpm command in the profile, streaming its output as install-log chunks. */
1026
+ async runPnpm(args, signal, requestId) {
1027
+ const jobId = randomUUID();
1028
+ const argv = ["pnpm", ...args];
1029
+ const cwd = this.profile.dir;
1030
+ const identity = requestId === void 0 ? {} : { requestId };
1031
+ const task = runProfilePnpm({
1032
+ ...this.profile,
1033
+ profile: this.profile.name
1034
+ }, args, {
1035
+ execution: "service",
1036
+ ...this.profile.packageManager ?? { command: this.pnpmCommand },
1037
+ signal: signal === void 0 ? this.abort.signal : AbortSignal.any([this.abort.signal, signal]),
1038
+ outputBytes: this.outputBytes,
1039
+ activateNewBundles: false,
1040
+ onOutput: (text, stream) => {
1041
+ this.ownerContext.emit("plugin-manager/install-log", {
1042
+ ...identity,
1043
+ jobId,
1044
+ argv,
1045
+ cwd,
1046
+ stream,
1047
+ text
1048
+ });
1049
+ }
1050
+ });
1051
+ this.packageOperations.add(task);
1052
+ try {
1053
+ const result = await task;
1054
+ this.ownerContext.emit("plugin-manager/install-log", {
1055
+ ...identity,
1056
+ jobId,
1057
+ argv,
1058
+ cwd,
1059
+ stream: "stdout",
1060
+ text: "",
1061
+ exitCode: signal?.aborted === true ? null : result.exitCode
1062
+ });
1063
+ return result.exitCode === 0 ? result : {
1064
+ ...result,
1065
+ kind: classifyInstallFailure({ log: result.output })
1066
+ };
1067
+ } catch (error) {
1068
+ this.ownerContext.emit("plugin-manager/install-log", {
1069
+ ...identity,
1070
+ jobId,
1071
+ argv,
1072
+ cwd,
1073
+ stream: "stderr",
1074
+ text: messageOf(error),
1075
+ exitCode: null
1076
+ });
1077
+ throw error;
1078
+ } finally {
1079
+ this.packageOperations.delete(task);
1080
+ }
1081
+ }
1082
+ /** The profile files an installation may rewrite, as they are now; absent files read as undefined. */
1083
+ async readRestoredFiles() {
1084
+ const files = /* @__PURE__ */ new Map();
1085
+ for (const name of RESTORED_FILES) {
1086
+ const path = join(this.profile.dir, name);
1087
+ files.set(path, existsSync(path) ? await readFile(path, "utf8") : void 0);
1088
+ }
1089
+ return files;
1090
+ }
1091
+ /** Put the profile files back; pnpm has exited by the time this runs. */
1092
+ async restoreFiles(files) {
1093
+ for (const [path, content] of files) if (content === void 0) await rm(path, { force: true });
1094
+ else await writeFileAtomic(path, content, { mode: 384 });
1095
+ }
1096
+ async selectBundle(name, enabled) {
1097
+ const manifest = readProfileManifest("dsh", this.profile.dir);
1098
+ const previous = manifest.dsh?.profile?.bundles ?? [];
1099
+ if ((enabled || !previous.includes(name)) && bundleManifest(name, this.profile.dir, this.profile.installAnchor) === void 0) throw new ManagementFailure("not-bundle");
1100
+ if (!enabled && previous.includes(name)) {
1101
+ if (this.protectsManager(name)) throw new ManagementFailure("management-required");
1102
+ }
1103
+ const bundles = enabled ? [...previous, ...previous.includes(name) ? [] : [name]] : previous.filter((item) => item !== name);
1104
+ if (JSON.stringify(previous) === JSON.stringify(bundles)) return;
1105
+ manifest.dsh = {
1106
+ ...manifest.dsh,
1107
+ profile: {
1108
+ ...manifest.dsh?.profile,
1109
+ bundles
1110
+ }
1111
+ };
1112
+ await saveManifest(this.profile.dir, manifest);
1113
+ }
1114
+ bundleRows(name) {
1115
+ const info = bundleManifest(name, this.profile.dir, this.profile.installAnchor);
1116
+ if (info?.dsh?.bundle === void 0) return [];
1117
+ return flatten(composeEntries([loadOverlayPatches("dsh", join(resolveBundleDir("dsh", name, this.profile.installAnchor, this.profile.dir), info.dsh.bundle.patch))]));
1118
+ }
1119
+ protectsManager(name) {
1120
+ return this.bundleRows(name).some((row) => protectedModules.has(row.name) || `include:${row.id}` === this.ownerEntryId);
1121
+ }
1122
+ configure(operation) {
1123
+ const hmr = this.ownerContext.get("hmr");
1124
+ const apply = () => {
1125
+ this.abort.signal.throwIfAborted();
1126
+ return operation();
1127
+ };
1128
+ return hmr === void 0 ? apply() : hmr.runExclusive(apply);
1129
+ }
1130
+ async reload(requiredIds = []) {
1131
+ if (this.ownerContext.get("hmr") === void 0) return [];
1132
+ return reconcileProfilePatches(this.ownerContext.root, readProfilePatches("dsh", this.profile), "dsh", requiredIds);
1133
+ }
1134
+ async change(operation, request, reason) {
1135
+ return withFileLock(join(this.profile.dir, "package.json"), async () => {
1136
+ this.abort.signal.throwIfAborted();
1137
+ const before = this.diskState();
1138
+ const result = {
1139
+ ...request,
1140
+ changed: false,
1141
+ application: this.ownerContext.get("hmr") !== void 0 ? "applied" : "restart-required"
1142
+ };
1143
+ try {
1144
+ result.application = await operation(result) ?? result.application;
1145
+ } catch (error) {
1146
+ if (error instanceof InstallCancelledError) result.application = "cancelled";
1147
+ else {
1148
+ result.application = "failed";
1149
+ result.error = managementError(error);
1150
+ }
1151
+ }
1152
+ result.changed = before !== this.diskState();
1153
+ this.ownerContext.emit("plugin-manager/changed", { reason });
1154
+ return result;
1155
+ }, { waitMs: this.lockWaitMs });
1156
+ }
1157
+ diskState() {
1158
+ return [
1159
+ "package.json",
1160
+ "cordis.patch.yml",
1161
+ "pnpm-workspace.yaml"
1162
+ ].map((file) => {
1163
+ try {
1164
+ return readFileSync(join(this.profile.dir, file), "utf8");
1165
+ } catch (error) {
1166
+ if (error.code === "ENOENT") return "";
1167
+ throw error;
1168
+ }
1169
+ }).join("\0");
1170
+ }
1171
+ };
1172
+ })();
1173
+ //#endregion
1174
+ export { InvalidInstallSpecError, PluginManager, PluginManager as default, classifyInstallFailure, parseInstallSpec };