@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.
@@ -0,0 +1,623 @@
1
+ var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
2
+ var useValue = arguments.length > 2;
3
+ for (var i = 0; i < initializers.length; i++) {
4
+ value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
5
+ }
6
+ return useValue ? value : void 0;
7
+ };
8
+ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
9
+ function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
10
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
11
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
12
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
13
+ var _, done = false;
14
+ for (var i = decorators.length - 1; i >= 0; i--) {
15
+ var context = {};
16
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
17
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
18
+ context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
19
+ var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
20
+ if (kind === "accessor") {
21
+ if (result === void 0) continue;
22
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
23
+ if (_ = accept(result.get)) descriptor.get = _;
24
+ if (_ = accept(result.set)) descriptor.set = _;
25
+ if (_ = accept(result.init)) initializers.unshift(_);
26
+ }
27
+ else if (_ = accept(result)) {
28
+ if (kind === "field") initializers.unshift(_);
29
+ else descriptor[key] = _;
30
+ }
31
+ }
32
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
33
+ done = true;
34
+ };
35
+ /** Current-profile plugin and bundle management over shared dsh plugin operations. */
36
+ import { randomUUID } from 'node:crypto';
37
+ import { existsSync, readFileSync } from 'node:fs';
38
+ import { readFile, rm } from 'node:fs/promises';
39
+ import { join } from 'node:path';
40
+ import { withFileLock, writeFileAtomic } from '@deepseek-ai/dsh-atomic-write';
41
+ import z from '@deepseek-ai/schemastery';
42
+ import { TypertRemoteService, Remote } from '@deepseek-ai/dsh-typert-protocol';
43
+ import { pluginEntryId, readPluginInventory } from '@deepseek-ai/dsh-host-plugin-inventory';
44
+ import { readProfileManifest, resolveBundleDir, loadOverlayPatches, composeEntries, reconcileProfilePatches, readProfilePatches, OPTIONAL_BUNDLES, } from '@deepseek-ai/dsh-app-boot';
45
+ import { bundleManifest, runProfilePnpm, saveManifest, viewProfilePackage } from "./operations.js";
46
+ import { classifyInstallFailure } from "./install-failure.js";
47
+ import { InvalidInstallSpecError, parseInstallSpec } from "./install-spec.js";
48
+ import { writePluginEnabled } from "./patch.js";
49
+ import { ManagementFailure } from "./failure.js";
50
+ import { approveBuilds, readPendingBuilds } from "./build-approval.js";
51
+ export { classifyInstallFailure } from "./install-failure.js";
52
+ export { InvalidInstallSpecError, parseInstallSpec } from "./install-spec.js";
53
+ const protectedModules = new Set([
54
+ '@deepseek-ai/dsh-plugin-manager', '@deepseek-ai/cordis-plugin-loader',
55
+ '@deepseek-ai/cordis-plugin-include', '@deepseek-ai/dsh-api-gateway',
56
+ '@deepseek-ai/dsh-host-webserver', '@deepseek-ai/dsh-client-modules',
57
+ '@deepseek-ai/dsh-client-ui-settings-plugin-inventory', '@deepseek-ai/dsh-client-ui-plugin-manager',
58
+ '@deepseek-ai/dsh-host-plugin-inventory', '@deepseek-ai/dsh-typert-registry',
59
+ '@deepseek-ai/dsh-api-remotes',
60
+ '@deepseek-ai/cordis-plugin-timer', '@deepseek-ai/dsh-client-connection',
61
+ '@deepseek-ai/dsh-host-frontend-static', '@deepseek-ai/dsh-tools',
62
+ '@deepseek-ai/dsh-hmr',
63
+ ]);
64
+ /** The profile files an installation writes and a failed or cancelled one restores. */
65
+ const RESTORED_FILES = ['package.json', 'pnpm-lock.yaml'];
66
+ /** pnpm's colour escapes, which a JSON answer may be wrapped in. */
67
+ const ANSI_SEQUENCE = /\x1b\[[0-9;]*m/g;
68
+ /** Flatten only the groups addressable by the profile's patch composer. */
69
+ function flatten(rows) {
70
+ return rows.flatMap(row => [row, ...(row.group && Array.isArray(row.config) ? flatten(row.config) : [])]);
71
+ }
72
+ /** Preserve the exact observed diagnostic, including non-Error failures. */
73
+ function messageOf(error) { return error instanceof Error ? error.message : String(error); }
74
+ /** An expected refusal keeps its code; anything else becomes an operation error carrying its exact diagnostic. */
75
+ function managementError(error) {
76
+ return error instanceof ManagementFailure ? { code: error.code } : { code: 'operation-error', diagnostic: messageOf(error) };
77
+ }
78
+ /** The caller stopped an installation; its files are restored before this is thrown. */
79
+ class InstallCancelledError extends Error {
80
+ constructor() {
81
+ super('Installation cancelled');
82
+ this.name = 'InstallCancelledError';
83
+ }
84
+ }
85
+ /** A manifest field that is a string, when the manifest carries one. */
86
+ function stringField(manifest, field) {
87
+ const value = manifest[field];
88
+ return typeof value === 'string' ? value : undefined;
89
+ }
90
+ /** What a package manifest says about the package: identity, one-liner, and whether it is a bundle. */
91
+ function inspectionOf(kind, manifest) {
92
+ const dsh = manifest.dsh;
93
+ const declared = typeof dsh === 'object' && dsh !== null ? dsh : undefined;
94
+ const bundle = declared !== undefined && typeof declared.bundle === 'object' && declared.bundle !== null;
95
+ const name = stringField(manifest, 'name');
96
+ const version = stringField(manifest, 'version');
97
+ const description = stringField(manifest, 'description');
98
+ return {
99
+ status: 'accepted', kind, bundle,
100
+ ...name === undefined ? {} : { name },
101
+ ...version === undefined ? {} : { version },
102
+ ...description === undefined || description === '' ? {} : { description },
103
+ };
104
+ }
105
+ function refused(problem, reason) {
106
+ return { status: 'refused', problem, reason };
107
+ }
108
+ /** Manage profile files and apply their declared reload lifecycle. */
109
+ let PluginManager = (() => {
110
+ let _classSuper = TypertRemoteService;
111
+ let _instanceExtraInitializers = [];
112
+ let _listPlugins_decorators;
113
+ let _listBundles_decorators;
114
+ let _inspect_decorators;
115
+ let _setPluginEnabled_decorators;
116
+ let _setBundleEnabled_decorators;
117
+ let _installBundle_decorators;
118
+ let _cancelInstall_decorators;
119
+ let _removeBundle_decorators;
120
+ return class PluginManager extends _classSuper {
121
+ static {
122
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
123
+ _listPlugins_decorators = [Remote];
124
+ _listBundles_decorators = [Remote];
125
+ _inspect_decorators = [Remote];
126
+ _setPluginEnabled_decorators = [Remote];
127
+ _setBundleEnabled_decorators = [Remote];
128
+ _installBundle_decorators = [Remote];
129
+ _cancelInstall_decorators = [Remote];
130
+ _removeBundle_decorators = [Remote];
131
+ __esDecorate(this, null, _listPlugins_decorators, { kind: "method", name: "listPlugins", static: false, private: false, access: { has: obj => "listPlugins" in obj, get: obj => obj.listPlugins }, metadata: _metadata }, null, _instanceExtraInitializers);
132
+ __esDecorate(this, null, _listBundles_decorators, { kind: "method", name: "listBundles", static: false, private: false, access: { has: obj => "listBundles" in obj, get: obj => obj.listBundles }, metadata: _metadata }, null, _instanceExtraInitializers);
133
+ __esDecorate(this, null, _inspect_decorators, { kind: "method", name: "inspect", static: false, private: false, access: { has: obj => "inspect" in obj, get: obj => obj.inspect }, metadata: _metadata }, null, _instanceExtraInitializers);
134
+ __esDecorate(this, null, _setPluginEnabled_decorators, { kind: "method", name: "setPluginEnabled", static: false, private: false, access: { has: obj => "setPluginEnabled" in obj, get: obj => obj.setPluginEnabled }, metadata: _metadata }, null, _instanceExtraInitializers);
135
+ __esDecorate(this, null, _setBundleEnabled_decorators, { kind: "method", name: "setBundleEnabled", static: false, private: false, access: { has: obj => "setBundleEnabled" in obj, get: obj => obj.setBundleEnabled }, metadata: _metadata }, null, _instanceExtraInitializers);
136
+ __esDecorate(this, null, _installBundle_decorators, { kind: "method", name: "installBundle", static: false, private: false, access: { has: obj => "installBundle" in obj, get: obj => obj.installBundle }, metadata: _metadata }, null, _instanceExtraInitializers);
137
+ __esDecorate(this, null, _cancelInstall_decorators, { kind: "method", name: "cancelInstall", static: false, private: false, access: { has: obj => "cancelInstall" in obj, get: obj => obj.cancelInstall }, metadata: _metadata }, null, _instanceExtraInitializers);
138
+ __esDecorate(this, null, _removeBundle_decorators, { kind: "method", name: "removeBundle", static: false, private: false, access: { has: obj => "removeBundle" in obj, get: obj => obj.removeBundle }, metadata: _metadata }, null, _instanceExtraInitializers);
139
+ if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
140
+ }
141
+ static inject = ['loader', 'profileContext'];
142
+ static Config = z.object({
143
+ pnpmCommand: z.string().default('pnpm'),
144
+ outputBytes: z.number().step(1).min(1).default(16384),
145
+ lockWaitMs: z.number().step(1).min(0).default(120000),
146
+ inspectTimeoutMs: z.number().step(1).min(1000).default(20000),
147
+ });
148
+ ownerEntryId = __runInitializers(this, _instanceExtraInitializers);
149
+ packageOperations = new Set();
150
+ profile;
151
+ outputBytes;
152
+ lockWaitMs;
153
+ inspectTimeoutMs;
154
+ pnpmCommand;
155
+ ownerContext;
156
+ abort = new AbortController();
157
+ /** Installations by request id, from their call until it settles. */
158
+ installs = new Map();
159
+ constructor(ctx, config) {
160
+ super(ctx, 'pluginManager');
161
+ this.ownerEntryId = ctx.fiber.entry?.id;
162
+ this.ownerContext = ctx;
163
+ this.profile = ctx.profileContext;
164
+ this.outputBytes = config.outputBytes;
165
+ this.lockWaitMs = config.lockWaitMs;
166
+ this.inspectTimeoutMs = config.inspectTimeoutMs;
167
+ this.pnpmCommand = config.pnpmCommand;
168
+ ctx.effect(() => async () => {
169
+ this.abort.abort();
170
+ await Promise.allSettled([...this.packageOperations]);
171
+ }, 'plugin-manager: package cancellation');
172
+ }
173
+ /** Read current plugins, including why a row cannot be changed through the profile patch.
174
+ * @returns Current runtime entries with persistent patch targets.
175
+ */
176
+ async listPlugins() {
177
+ const rows = flatten(composeEntries([readProfilePatches('dsh', this.profile)]));
178
+ const snapshot = await readPluginInventory(this.ctx);
179
+ return snapshot.entries.map((entry) => {
180
+ const actual = [...this.ctx.loader.entries()].find(row => row.id === entry.entryId);
181
+ const candidates = rows.filter(row => row.id === actual?.options.id);
182
+ const candidate = candidates[0];
183
+ if (protectedModules.has(entry.moduleName) || entry.entryId === this.ownerEntryId) {
184
+ return { ...entry, readOnlyReason: 'management-required' };
185
+ }
186
+ if (candidate === undefined || candidates.length > 1 || candidate.name !== entry.moduleName
187
+ || actual?.parent.tree.ctx.fiber.entry?.id !== 'include') {
188
+ return { ...entry, readOnlyReason: 'unaddressable' };
189
+ }
190
+ return { ...entry, patchId: candidate.id };
191
+ });
192
+ }
193
+ /** Read the profile's installed bundles, the bundles this dsh installation supplies, and the selected names that are not bundles.
194
+ * A dependency without a bundle patch is listed, as a `not-bundle` problem, only while it is selected.
195
+ * @returns Package versions, one-liners, rows, activation selections, whether the installation offers the
196
+ * bundle, and removal availability.
197
+ */
198
+ listBundles() {
199
+ const manifest = readProfileManifest('dsh', this.profile.dir);
200
+ const selected = manifest.dsh?.profile?.bundles ?? [];
201
+ const dependencies = Object.keys(manifest.dependencies ?? {});
202
+ const installation = JSON.parse(readFileSync(this.profile.installAnchor, 'utf8'));
203
+ const names = [...new Set([...selected, ...dependencies, ...Object.keys(installation.dependencies ?? {})])];
204
+ const bundles = [];
205
+ for (const name of names) {
206
+ const installed = dependencies.includes(name);
207
+ const optional = OPTIONAL_BUNDLES.includes(name);
208
+ const removable = installed && !Object.hasOwn(installation.dependencies ?? {}, name);
209
+ const enabled = selected.includes(name);
210
+ try {
211
+ const info = bundleManifest(name, this.profile.dir, this.profile.installAnchor);
212
+ if (info === undefined) {
213
+ if (enabled)
214
+ bundles.push({ name, enabled, installed, optional, removable, error: { code: 'not-bundle' }, rows: [], overrides: [] });
215
+ continue;
216
+ }
217
+ const readOnlyReason = this.protectsManager(name) ? 'management-required' : undefined;
218
+ bundles.push({ name, ...(info.version === undefined ? {} : { version: info.version }),
219
+ ...(info.description === undefined || info.description === '' ? {} : { description: info.description }),
220
+ enabled, installed, optional, removable: removable && readOnlyReason === undefined,
221
+ ...(readOnlyReason === undefined ? {} : { readOnlyReason }),
222
+ ...this.declaredRows(name, info) });
223
+ }
224
+ catch (error) {
225
+ if (enabled || installed) {
226
+ bundles.push({ name, enabled, installed, optional, removable, error: managementError(error), rows: [], overrides: [] });
227
+ }
228
+ }
229
+ }
230
+ return Promise.resolve(bundles);
231
+ }
232
+ /** Read what a spec names before installing it.
233
+ * @param spec One package spec: a registry name, an absolute path, a git address, or a tarball.
234
+ * @param signal Ends a registry lookup early.
235
+ * @returns The package the spec names, or why it is refused.
236
+ */
237
+ async inspect(spec, signal) {
238
+ let parsed;
239
+ try {
240
+ parsed = parseInstallSpec(spec);
241
+ }
242
+ catch (error) {
243
+ /* v8 ignore next 2 -- parseInstallSpec throws nothing but its own refusal */
244
+ if (!(error instanceof InvalidInstallSpecError))
245
+ throw error;
246
+ return refused('invalid-spec', error.reason);
247
+ }
248
+ const manifest = readProfileManifest('dsh', this.profile.dir);
249
+ const installation = JSON.parse(readFileSync(this.profile.installAnchor, 'utf8'));
250
+ const known = new Set([
251
+ ...manifest.dsh?.profile?.bundles ?? [], ...Object.keys(manifest.dependencies ?? {}), ...Object.keys(installation.dependencies ?? {}),
252
+ ]);
253
+ switch (parsed.kind) {
254
+ case 'git': return { status: 'accepted', kind: 'git', bundle: null };
255
+ case 'tarball':
256
+ if (parsed.path !== undefined && !existsSync(parsed.path))
257
+ return refused('not-a-package', 'the tarball does not exist');
258
+ return { status: 'accepted', kind: 'tarball', bundle: null };
259
+ case 'path': {
260
+ if (!existsSync(parsed.path))
261
+ return refused('not-a-package', 'the path does not exist');
262
+ let read;
263
+ try {
264
+ read = JSON.parse(await readFile(join(parsed.path, 'package.json'), 'utf8'));
265
+ }
266
+ catch (error) {
267
+ return refused('not-a-package', `no readable package.json at the path: ${messageOf(error)}`);
268
+ }
269
+ const inspection = inspectionOf('path', read);
270
+ if (inspection.name === undefined)
271
+ return refused('not-a-package', 'the package.json names no package');
272
+ if (known.has(inspection.name))
273
+ return refused('already-installed', `${inspection.name} is already installed`);
274
+ if (!inspection.bundle)
275
+ return refused('not-a-bundle', `${inspection.name} declares no dsh.bundle`);
276
+ return inspection;
277
+ }
278
+ case 'registry': {
279
+ if (known.has(parsed.name))
280
+ return refused('already-installed', `${parsed.name} is already installed`);
281
+ const view = await viewProfilePackage(this.profile.dir, spec.trim(), {
282
+ ...this.profile.packageManager ?? { command: this.pnpmCommand },
283
+ timeoutMs: this.inspectTimeoutMs, ...signal === undefined ? {} : { signal },
284
+ });
285
+ const log = `${view.stderr}${view.cause === undefined ? '' : `${messageOf(view.cause)}\n`}`.trim();
286
+ if (view.exitCode !== 0 || view.cause !== undefined || view.timedOut) {
287
+ const kind = classifyInstallFailure({ log, timedOut: view.timedOut, ...view.cause === undefined ? {} : { cause: view.cause } });
288
+ const reason = log || view.stdout.trim() || `pnpm view exited with ${String(view.exitCode)}`;
289
+ if (kind === 'not-found' || kind === 'no-matching-version')
290
+ return refused('not-found', reason);
291
+ if (kind === 'network')
292
+ return refused('network', reason);
293
+ return refused('unknown', view.timedOut ? `pnpm view timed out after ${String(this.inspectTimeoutMs)}ms` : reason);
294
+ }
295
+ let answer;
296
+ try {
297
+ answer = JSON.parse(view.stdout.replace(ANSI_SEQUENCE, '').trim() || 'null');
298
+ }
299
+ catch (error) {
300
+ return refused('unknown', `unreadable pnpm view output: ${messageOf(error)}`);
301
+ }
302
+ // A range answers one object per matching version, oldest first.
303
+ const latest = Array.isArray(answer) ? answer.at(-1) : answer;
304
+ if (typeof latest !== 'object' || latest === null)
305
+ return refused('unknown', 'pnpm view answered no package');
306
+ const inspection = inspectionOf('registry', latest);
307
+ const named = inspection.name === undefined ? { ...inspection, name: parsed.name } : inspection;
308
+ if (!named.bundle)
309
+ return refused('not-a-bundle', `${named.name} declares no dsh.bundle`);
310
+ return named;
311
+ }
312
+ }
313
+ }
314
+ /** Persist a plugin entry's desired enablement and apply it on live profiles.
315
+ * @param id Loader entry identity returned by listPlugins.
316
+ * @param enabled Whether the plugin should run.
317
+ * @returns Saved and runtime outcomes, including higher-priority overrides.
318
+ */
319
+ setPluginEnabled(id, enabled) {
320
+ return this.change(result => this.configure(async () => {
321
+ const row = (await this.listPlugins()).find(item => item.entryId === id);
322
+ if (row === undefined)
323
+ throw new ManagementFailure('unknown-plugin');
324
+ if (row.readOnlyReason !== undefined)
325
+ throw new ManagementFailure(row.readOnlyReason);
326
+ await writePluginEnabled(this.profile.patchPath, row.patchId, row.moduleName, enabled);
327
+ result.warnings = await this.reload(enabled ? [row.patchId] : []);
328
+ const current = (await this.listPlugins()).find(item => item.entryId === id);
329
+ return current?.enabled !== enabled && this.ownerContext.get('hmr') !== undefined ? 'overridden' : undefined;
330
+ }), { stage: 'enable', target: id, enabled }, 'plugin');
331
+ }
332
+ /** Select or remove a bundle layer while retaining installed dependencies.
333
+ * @param name Bundle package name.
334
+ * @param enabled Whether the bundle contributes its patch layer.
335
+ * @returns Persisted and runtime outcomes.
336
+ */
337
+ setBundleEnabled(name, enabled) {
338
+ return this.change(result => this.configure(async () => {
339
+ await this.selectBundle(name, enabled);
340
+ result.warnings = await this.reload(enabled ? this.bundleRows(name).map(row => row.id) : []);
341
+ }), { stage: 'enable', target: name, enabled }, 'bundle');
342
+ }
343
+ /**
344
+ * Install a package using the same pnpm implementation as dsh plugin. A run
345
+ * that fails, is cancelled, or adds a package without a bundle patch restores
346
+ * `package.json` and `pnpm-lock.yaml` as they were; downloaded files can stay.
347
+ * @param spec One package spec, including local paths relative to the invocation directory.
348
+ * @param options Whether to activate the installed bundle (defaults to true), the request id a cancellation names, and
349
+ * the pending build scripts to allow for this profile before pnpm runs.
350
+ * @returns Package-manager diagnostics and observed activation outcome.
351
+ */
352
+ installBundle(spec, options) {
353
+ const requestId = options?.requestId;
354
+ const control = { abort: new AbortController(), phase: 'installing', settled: Promise.resolve() };
355
+ const stopped = () => control.abort.signal.aborted;
356
+ if (requestId !== undefined)
357
+ this.installs.set(requestId, control);
358
+ const announce = (phase) => {
359
+ if (requestId !== undefined)
360
+ this.ownerContext.emit('plugin-manager/install-state', { requestId, phase });
361
+ };
362
+ const result = this.change(async (result) => {
363
+ if (spec.trim() === '' || spec.startsWith('-'))
364
+ throw new ManagementFailure('invalid-spec');
365
+ if (stopped())
366
+ throw new InstallCancelledError();
367
+ if (options?.approvedBuilds !== undefined) {
368
+ await approveBuilds(this.profile.dir, options.approvedBuilds);
369
+ result.approvedBuilds = options.approvedBuilds;
370
+ }
371
+ const files = await this.readRestoredFiles();
372
+ const before = readProfileManifest('dsh', this.profile.dir).dependencies ?? {};
373
+ announce('installing');
374
+ let name;
375
+ try {
376
+ result.packageResult = await this.runPnpm(['add', spec], control.abort.signal, requestId);
377
+ if (stopped())
378
+ throw new InstallCancelledError();
379
+ if (result.packageResult.exitCode !== 0) {
380
+ // pnpm-workspace.yaml is not restored, so the names pnpm left undecided there can be offered for approval.
381
+ try {
382
+ result.pendingBuilds = await readPendingBuilds(this.profile.dir);
383
+ }
384
+ catch (error) {
385
+ this.ownerContext.logger.warn('Could not read pending build approvals after pnpm failed', error);
386
+ }
387
+ throw new Error(result.packageResult.output);
388
+ }
389
+ const after = readProfileManifest('dsh', this.profile.dir).dependencies ?? {};
390
+ const installed = Object.keys(after).filter(name => before[name] !== after[name]);
391
+ // Registry retries can retain the saved range after a partial installation.
392
+ if (installed.length === 0)
393
+ installed.push(...Object.keys(after).filter(name => spec === name || spec.startsWith(`${name}@`)));
394
+ const target = installed[0];
395
+ if (installed.length !== 1 || target === undefined)
396
+ throw new ManagementFailure('ambiguous-install');
397
+ name = target;
398
+ const dir = resolveBundleDir('dsh', name, this.profile.installAnchor, this.profile.dir);
399
+ const manifest = bundleManifest(name, this.profile.dir, this.profile.installAnchor);
400
+ if (manifest?.dsh?.bundle?.patch === undefined)
401
+ throw new ManagementFailure('not-bundle');
402
+ loadOverlayPatches('dsh', join(dir, manifest.dsh.bundle.patch));
403
+ }
404
+ catch (error) {
405
+ // pnpm has exited by now, so the files it rewrote go back as they were.
406
+ await this.restoreFiles(files);
407
+ throw error;
408
+ }
409
+ control.phase = 'applying';
410
+ announce('applying');
411
+ result.bundle = name;
412
+ result.target = name;
413
+ result.stage = 'enable';
414
+ return this.configure(async () => {
415
+ if (options?.enabled !== false)
416
+ await this.selectBundle(name, true);
417
+ if (Object.hasOwn(before, name))
418
+ return 'restart-required';
419
+ if (options?.enabled !== false)
420
+ result.warnings = await this.reload();
421
+ });
422
+ }, { stage: 'install', target: spec, enabled: options?.enabled !== false }, 'install');
423
+ /* v8 ignore next -- change() folds every failure into its result; only a lock or disposal error rejects */
424
+ control.settled = result.then(() => undefined, () => undefined);
425
+ return result.finally(() => { if (requestId !== undefined)
426
+ this.installs.delete(requestId); });
427
+ }
428
+ /** Stop an installation this manager owns and wait until its files are back.
429
+ * @param requestId The id the installation was started with.
430
+ * @returns `cancelled` once pnpm exited and the files are restored, `too-late` once the bundle is being
431
+ * applied, `not-running` for any other id.
432
+ */
433
+ async cancelInstall(requestId) {
434
+ const control = this.installs.get(requestId);
435
+ if (control === undefined)
436
+ return { status: 'not-running' };
437
+ if (control.phase === 'applying')
438
+ return { status: 'too-late' };
439
+ this.ownerContext.emit('plugin-manager/install-state', { requestId, phase: 'cancelling' });
440
+ control.abort.abort();
441
+ await control.settled;
442
+ return { status: 'cancelled' };
443
+ }
444
+ /** Unload and remove a profile-owned bundle dependency through dsh plugin's pnpm path.
445
+ * @param name Installed dependency name.
446
+ * @returns Removal diagnostics and the remaining profile state.
447
+ */
448
+ removeBundle(name) {
449
+ return this.change(async (result) => {
450
+ await this.configure(async () => {
451
+ const bundle = (await this.listBundles()).find(item => item.name === name);
452
+ if (bundle === undefined || !bundle.removable)
453
+ throw new ManagementFailure('not-removable');
454
+ if (this.ownerContext.get('hmr') === undefined && (this.profile.startedBundles.includes(name)
455
+ || this.bundleRows(name).some(row => [...this.ctx.loader.entries()]
456
+ .some(entry => entry.options.id === row.id && entry.fiber !== undefined)))) {
457
+ throw new ManagementFailure('stop-profile');
458
+ }
459
+ const contributions = bundle.error === undefined ? this.bundleRows(name) : [];
460
+ if (bundle.enabled) {
461
+ await this.selectBundle(name, false);
462
+ result.warnings = await this.reload();
463
+ }
464
+ if ([...this.ctx.loader.entries()].some(entry => entry.fiber?.uid != null
465
+ && contributions.some(row => row.id === entry.options.id && row.name === entry.options.name))) {
466
+ throw new ManagementFailure('bundle-in-use');
467
+ }
468
+ });
469
+ result.packageResult = await this.runPnpm(['remove', name]);
470
+ if (result.packageResult.exitCode !== 0)
471
+ throw new Error(result.packageResult.output);
472
+ }, { stage: 'remove', target: name }, 'remove');
473
+ }
474
+ /** The rows a bundle's patch inserts and the existing rows it changes; an unreadable patch throws. */
475
+ declaredRows(name, info) {
476
+ const patch = info.dsh?.bundle?.patch;
477
+ /* v8 ignore next -- bundleManifest answers only manifests that declare a patch */
478
+ if (patch === undefined)
479
+ return { rows: [], overrides: [] };
480
+ const dir = resolveBundleDir('dsh', name, this.profile.installAnchor, this.profile.dir);
481
+ const patches = loadOverlayPatches('dsh', join(dir, patch));
482
+ // One entry per row id: the Loader keeps a single entry for an id, whichever layer declared it last.
483
+ const live = new Map();
484
+ for (const entry of this.ctx.loader.entries()) {
485
+ /* v8 ignore next -- the Loader gives every entry an id before it is listed */
486
+ if (typeof entry.options.id === 'string')
487
+ live.set(entry.options.id, pluginEntryId(entry.id));
488
+ }
489
+ const rows = [];
490
+ for (const row of flatten(composeEntries([patches.filter(item => item.insert !== undefined)]))) {
491
+ if (typeof row.id !== 'string' || typeof row.name !== 'string')
492
+ continue;
493
+ const entryId = live.get(row.id);
494
+ rows.push({ rowId: row.id, moduleName: row.name, ...entryId === undefined ? {} : { entryId } });
495
+ }
496
+ const declared = new Set(rows.map(row => row.rowId));
497
+ const overrides = [...new Set(patches.flatMap(item => item.insert === undefined && typeof item.id === 'string' && !declared.has(item.id) ? [item.id] : []))];
498
+ return { rows, overrides };
499
+ }
500
+ /** Run one pnpm command in the profile, streaming its output as install-log chunks. */
501
+ async runPnpm(args, signal, requestId) {
502
+ const jobId = randomUUID();
503
+ const argv = ['pnpm', ...args];
504
+ const cwd = this.profile.dir;
505
+ const identity = requestId === undefined ? {} : { requestId };
506
+ const task = runProfilePnpm({ ...this.profile, profile: this.profile.name }, args, {
507
+ execution: 'service', ...this.profile.packageManager ?? { command: this.pnpmCommand },
508
+ signal: signal === undefined ? this.abort.signal : AbortSignal.any([this.abort.signal, signal]),
509
+ outputBytes: this.outputBytes, activateNewBundles: false,
510
+ onOutput: (text, stream) => {
511
+ this.ownerContext.emit('plugin-manager/install-log', { ...identity, jobId, argv, cwd, stream, text });
512
+ },
513
+ });
514
+ this.packageOperations.add(task);
515
+ try {
516
+ const result = await task;
517
+ this.ownerContext.emit('plugin-manager/install-log', {
518
+ ...identity, jobId, argv, cwd, stream: 'stdout', text: '', exitCode: signal?.aborted === true ? null : result.exitCode,
519
+ });
520
+ return result.exitCode === 0 ? result : { ...result, kind: classifyInstallFailure({ log: result.output }) };
521
+ }
522
+ catch (error) {
523
+ this.ownerContext.emit('plugin-manager/install-log', { ...identity, jobId, argv, cwd, stream: 'stderr', text: messageOf(error), exitCode: null });
524
+ throw error;
525
+ }
526
+ finally {
527
+ this.packageOperations.delete(task);
528
+ }
529
+ }
530
+ /** The profile files an installation may rewrite, as they are now; absent files read as undefined. */
531
+ async readRestoredFiles() {
532
+ const files = new Map();
533
+ for (const name of RESTORED_FILES) {
534
+ const path = join(this.profile.dir, name);
535
+ files.set(path, existsSync(path) ? await readFile(path, 'utf8') : undefined);
536
+ }
537
+ return files;
538
+ }
539
+ /** Put the profile files back; pnpm has exited by the time this runs. */
540
+ async restoreFiles(files) {
541
+ for (const [path, content] of files) {
542
+ if (content === undefined)
543
+ await rm(path, { force: true });
544
+ else
545
+ await writeFileAtomic(path, content, { mode: 0o600 });
546
+ }
547
+ }
548
+ async selectBundle(name, enabled) {
549
+ const manifest = readProfileManifest('dsh', this.profile.dir);
550
+ const previous = manifest.dsh?.profile?.bundles ?? [];
551
+ if ((enabled || !previous.includes(name)) && bundleManifest(name, this.profile.dir, this.profile.installAnchor) === undefined) {
552
+ throw new ManagementFailure('not-bundle');
553
+ }
554
+ if (!enabled && previous.includes(name)) {
555
+ if (this.protectsManager(name))
556
+ throw new ManagementFailure('management-required');
557
+ }
558
+ const bundles = enabled ? [...previous, ...previous.includes(name) ? [] : [name]] : previous.filter(item => item !== name);
559
+ if (JSON.stringify(previous) === JSON.stringify(bundles))
560
+ return;
561
+ manifest.dsh = { ...manifest.dsh, profile: { ...manifest.dsh?.profile, bundles } };
562
+ await saveManifest(this.profile.dir, manifest);
563
+ }
564
+ bundleRows(name) {
565
+ const info = bundleManifest(name, this.profile.dir, this.profile.installAnchor);
566
+ if (info?.dsh?.bundle === undefined)
567
+ return [];
568
+ const dir = resolveBundleDir('dsh', name, this.profile.installAnchor, this.profile.dir);
569
+ return flatten(composeEntries([loadOverlayPatches('dsh', join(dir, info.dsh.bundle.patch))]));
570
+ }
571
+ protectsManager(name) {
572
+ return this.bundleRows(name).some(row => protectedModules.has(row.name) || `include:${row.id}` === this.ownerEntryId);
573
+ }
574
+ configure(operation) {
575
+ const hmr = this.ownerContext.get('hmr');
576
+ const apply = () => { this.abort.signal.throwIfAborted(); return operation(); };
577
+ return hmr === undefined ? apply() : hmr.runExclusive(apply);
578
+ }
579
+ async reload(requiredIds = []) {
580
+ if (this.ownerContext.get('hmr') === undefined)
581
+ return [];
582
+ return reconcileProfilePatches(this.ownerContext.root, readProfilePatches('dsh', this.profile), 'dsh', requiredIds);
583
+ }
584
+ async change(operation, request, reason) {
585
+ return withFileLock(join(this.profile.dir, 'package.json'), async () => {
586
+ this.abort.signal.throwIfAborted();
587
+ const before = this.diskState();
588
+ const result = { ...request, changed: false,
589
+ application: this.ownerContext.get('hmr') !== undefined ? 'applied' : 'restart-required' };
590
+ try {
591
+ result.application = await operation(result) ?? result.application;
592
+ }
593
+ catch (error) {
594
+ if (error instanceof InstallCancelledError) {
595
+ result.application = 'cancelled';
596
+ }
597
+ else {
598
+ result.application = 'failed';
599
+ result.error = managementError(error);
600
+ }
601
+ }
602
+ result.changed = before !== this.diskState();
603
+ this.ownerContext.emit('plugin-manager/changed', { reason });
604
+ return result;
605
+ }, { waitMs: this.lockWaitMs });
606
+ }
607
+ diskState() {
608
+ return ['package.json', 'cordis.patch.yml', 'pnpm-workspace.yaml'].map((file) => {
609
+ try {
610
+ return readFileSync(join(this.profile.dir, file), 'utf8');
611
+ }
612
+ catch (error) {
613
+ if (error.code === 'ENOENT')
614
+ return '';
615
+ throw error;
616
+ }
617
+ }).join('\0');
618
+ }
619
+ };
620
+ })();
621
+ export { PluginManager };
622
+ export default PluginManager;
623
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,23 @@
1
+ /**
2
+ * What a failed pnpm run was, read off how it ended and what it printed:
3
+ * pnpm names its failures with stable `ERR_PNPM_*` codes and Node's errno
4
+ * names, which the run's captured tail carries whatever the locale.
5
+ * @module @deepseek-ai/dsh-plugin-manager/install-failure
6
+ */
7
+ import type { PluginInstallFailureKind } from './types.ts';
8
+ /** How a run ended, beyond its exit code. */
9
+ export interface InstallFailureFacts {
10
+ /** The output tail the failure reports. */
11
+ readonly log: string;
12
+ /** The spawn error, when the child never ran. */
13
+ readonly cause?: unknown;
14
+ /** Whether the run outlived its bound. */
15
+ readonly timedOut?: boolean;
16
+ }
17
+ /**
18
+ * Classify a failed run.
19
+ * @param facts - how the run ended and what it printed.
20
+ * @returns the kind, `unknown` when nothing in the facts names one.
21
+ */
22
+ export declare function classifyInstallFailure(facts: InstallFailureFacts): PluginInstallFailureKind;
23
+ //# sourceMappingURL=install-failure.d.ts.map