@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/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +133 -0
- package/README.zh.md +133 -0
- package/lib/index.js +1174 -0
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +604 -0
- package/lib/typert.remote-client.d.ts +37 -0
- package/lib/typert.remote-client.js +393 -0
- package/lib/types/build-approval.d.ts +12 -0
- package/lib/types/build-approval.js +56 -0
- package/lib/types/failure.d.ts +10 -0
- package/lib/types/failure.js +8 -0
- package/lib/types/index.d.ts +107 -0
- package/lib/types/index.js +623 -0
- package/lib/types/install-failure.d.ts +23 -0
- package/lib/types/install-failure.js +33 -0
- package/lib/types/install-spec.d.ts +44 -0
- package/lib/types/install-spec.js +73 -0
- package/lib/types/operations.d.ts +93 -0
- package/lib/types/operations.js +177 -0
- package/lib/types/patch.d.ts +9 -0
- package/lib/types/patch.js +50 -0
- package/lib/types/tools.d.ts +8 -0
- package/lib/types/tools.js +74 -0
- package/lib/types/types.d.ts +169 -0
- package/lib/types/types.js +2 -0
- package/package.json +102 -0
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 };
|