@deepseek-ai/dsh-plugin-manager 0.1.7-alpha.2 → 0.1.7-rc.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/README.i18n.yaml +54 -5
- package/README.md +17 -2
- package/README.zh.md +17 -2
- package/lib/index.js +757 -154
- package/lib/typert.host.js +237 -26
- package/lib/typert.remote-client.d.ts +4 -0
- package/lib/typert.remote-client.js +215 -22
- package/lib/types/failure.d.ts +15 -3
- package/lib/types/failure.js +19 -2
- package/lib/types/github-connection.d.ts +21 -0
- package/lib/types/github-connection.js +61 -0
- package/lib/types/index.d.ts +28 -4
- package/lib/types/index.js +111 -21
- package/lib/types/install-failure.js +1 -0
- package/lib/types/operations.d.ts +15 -1
- package/lib/types/operations.js +403 -22
- package/lib/types/run-tree.d.ts +44 -0
- package/lib/types/run-tree.js +57 -0
- package/lib/types/tools.js +13 -3
- package/lib/types/types.d.ts +17 -3
- package/package.json +28 -28
package/lib/index.js
CHANGED
|
@@ -7,11 +7,206 @@ import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
|
|
|
7
7
|
import z from "@deepseek-ai/schemastery";
|
|
8
8
|
import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
9
9
|
import { pluginEntryId, readPluginInventory } from "@deepseek-ai/dsh-host-plugin-inventory";
|
|
10
|
-
import { OPTIONAL_BUNDLES, bundlePatchPaths, composeEntries, loadOptionalPatches, loadOverlayPatches, readPluginMeta, readProfileManifest, readProfilePatches, reconcileProfilePatches, resolveBundleDir, resolveProfileDir } from "@deepseek-ai/dsh-app-boot";
|
|
10
|
+
import { OPTIONAL_BUNDLES, PROFILE_COMPATIBILITY_FILENAME, bundlePatchPaths, composeEntries, evaluatePluginCompatibility, loadOptionalPatches, loadOverlayPatches, pluginCompatibilityWarning, readPluginMeta, readProfileCompatibility, readProfileManifest, readProfilePatches, readProfileVersionExemptions, reconcileProfilePatches, resolveBundleDir, resolveProfileDir, setProfileVersionExemption } from "@deepseek-ai/dsh-app-boot";
|
|
11
|
+
import { once } from "node:events";
|
|
11
12
|
import { execa } from "execa";
|
|
12
13
|
import { scrubbedParentEnv } from "@deepseek-ai/dsh-subprocess";
|
|
13
14
|
import { isAlias, isMap, isNode, isScalar, isSeq, parseDocument, visit } from "yaml";
|
|
15
|
+
//#region lib/types/install-spec.js
|
|
16
|
+
/**
|
|
17
|
+
* Reading an install spec before pnpm sees it: which of pnpm's spec forms it
|
|
18
|
+
* takes, and for a registry name whether it is one the registry can accept.
|
|
19
|
+
* @module @deepseek-ai/dsh-plugin-manager/install-spec
|
|
20
|
+
*/
|
|
21
|
+
/** The forms pnpm resolves through a git host: a host shorthand, a git URL, or a hosted repository URL. */
|
|
22
|
+
const GIT_SHORTHAND = /^(?:github|gitlab|bitbucket|gist):/i;
|
|
23
|
+
const GIT_URL = /^git(?:\+[a-z]+)?:\/\/|^git@[^:]+:/i;
|
|
24
|
+
const HOSTED_REPOSITORY_URL = /^https?:\/\/[^/]+\/[^/]+\/[^/#]+(?:\.git)?(?:#.*)?$/i;
|
|
25
|
+
/** The hosts pnpm's shorthands stand for. */
|
|
26
|
+
const GIT_SHORTHAND_HOSTS = {
|
|
27
|
+
github: "github.com",
|
|
28
|
+
gitlab: "gitlab.com",
|
|
29
|
+
bitbucket: "bitbucket.org",
|
|
30
|
+
gist: "gist.github.com"
|
|
31
|
+
};
|
|
32
|
+
/** A tarball, on disk or over HTTP. */
|
|
33
|
+
const TARBALL_SPEC = /\.(?:tgz|tar\.gz)(?:#.*)?$/i;
|
|
34
|
+
/** An npm package name: lowercase URL-safe segments, an optional scope, no leading dot or underscore. */
|
|
35
|
+
const PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/;
|
|
36
|
+
const PACKAGE_NAME_MAX_LENGTH = 214;
|
|
37
|
+
/** A spec neither pnpm nor the registry would take; `reason` is what the person reads. */
|
|
38
|
+
var InvalidInstallSpecError = class extends Error {
|
|
39
|
+
spec;
|
|
40
|
+
reason;
|
|
41
|
+
/**
|
|
42
|
+
* @param spec - the spec as typed, trimmed.
|
|
43
|
+
* @param reason - why it is refused, as one sentence.
|
|
44
|
+
*/
|
|
45
|
+
constructor(spec, reason) {
|
|
46
|
+
super(`plugin-manager: ${reason}: ${spec}`);
|
|
47
|
+
this.spec = spec;
|
|
48
|
+
this.reason = reason;
|
|
49
|
+
this.name = "InvalidInstallSpecError";
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
function invalid(spec, reason) {
|
|
53
|
+
return new InvalidInstallSpecError(spec, reason);
|
|
54
|
+
}
|
|
55
|
+
/** The host a git spec is cloned from: the shorthand's host, the scp-like user@host, or the URL's host with its port. */
|
|
56
|
+
function gitHost(spec) {
|
|
57
|
+
const shorthand = /^([a-z]+):/i.exec(spec)?.[1]?.toLowerCase();
|
|
58
|
+
if (shorthand !== void 0 && Object.hasOwn(GIT_SHORTHAND_HOSTS, shorthand)) return GIT_SHORTHAND_HOSTS[shorthand];
|
|
59
|
+
const scp = /^git@([^:]+):/i.exec(spec)?.[1];
|
|
60
|
+
if (scp !== void 0) return scp.toLowerCase();
|
|
61
|
+
return new URL(spec.replace(/^git\+/i, "")).host;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Read a spec into its form. A path must be absolute: the Host's working
|
|
65
|
+
* directory means nothing to the person typing into a browser, and a
|
|
66
|
+
* relative path resolved against the profile would point inside it.
|
|
67
|
+
* @param raw - the spec as typed.
|
|
68
|
+
* @returns the parsed spec.
|
|
69
|
+
* @throws {InvalidInstallSpecError} for an empty spec, a relative path, a name the
|
|
70
|
+
* registry would refuse, or a URL that is neither a git host nor a tarball.
|
|
71
|
+
*/
|
|
72
|
+
function parseInstallSpec(raw) {
|
|
73
|
+
const spec = raw.trim();
|
|
74
|
+
if (spec === "") throw invalid(spec, "the package spec must not be empty");
|
|
75
|
+
const path = spec.replace(/^(?:file|link):/, "");
|
|
76
|
+
if (path !== spec || isAbsolute(path)) {
|
|
77
|
+
if (!isAbsolute(path)) throw invalid(spec, "a local path must be absolute");
|
|
78
|
+
return TARBALL_SPEC.test(path) ? {
|
|
79
|
+
kind: "tarball",
|
|
80
|
+
spec,
|
|
81
|
+
path
|
|
82
|
+
} : {
|
|
83
|
+
kind: "path",
|
|
84
|
+
spec,
|
|
85
|
+
path
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
if (/^\.{1,2}(?:[\\/]|$)/.test(spec)) throw invalid(spec, "a local path must be absolute");
|
|
89
|
+
if ((GIT_SHORTHAND.test(spec) || GIT_URL.test(spec) || HOSTED_REPOSITORY_URL.test(spec)) && !TARBALL_SPEC.test(spec)) return {
|
|
90
|
+
kind: "git",
|
|
91
|
+
spec,
|
|
92
|
+
host: gitHost(spec)
|
|
93
|
+
};
|
|
94
|
+
if (/^https?:\/\//i.test(spec)) {
|
|
95
|
+
if (TARBALL_SPEC.test(spec)) return {
|
|
96
|
+
kind: "tarball",
|
|
97
|
+
spec,
|
|
98
|
+
host: new URL(spec).host
|
|
99
|
+
};
|
|
100
|
+
throw invalid(spec, "a URL must point at a git repository or a tarball");
|
|
101
|
+
}
|
|
102
|
+
const at = spec.indexOf("@", 1);
|
|
103
|
+
const name = at === -1 ? spec : spec.slice(0, at);
|
|
104
|
+
const range = at === -1 ? void 0 : spec.slice(at + 1);
|
|
105
|
+
if (name.length > PACKAGE_NAME_MAX_LENGTH || !PACKAGE_NAME.test(name)) throw invalid(spec, "not a package name the registry accepts");
|
|
106
|
+
if (range === "") throw invalid(spec, "a version after @ must not be empty");
|
|
107
|
+
return range === void 0 ? {
|
|
108
|
+
kind: "registry",
|
|
109
|
+
spec,
|
|
110
|
+
name
|
|
111
|
+
} : {
|
|
112
|
+
kind: "registry",
|
|
113
|
+
spec,
|
|
114
|
+
name,
|
|
115
|
+
range
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
//#endregion
|
|
119
|
+
//#region lib/types/run-tree.js
|
|
120
|
+
/** Waiting for one package run's process tree to disappear before its caller touches the profile. */
|
|
121
|
+
/** How long a terminated tree may take to disappear before the caller stops waiting. */
|
|
122
|
+
const TREE_WAIT_MS = 5e3;
|
|
123
|
+
/** Poll cadence while waiting for a terminated tree to disappear. */
|
|
124
|
+
const TREE_POLL_MS = 15;
|
|
125
|
+
/**
|
|
126
|
+
* Whether one run leads its own process group, which is the target a POSIX
|
|
127
|
+
* liveness probe addresses for the whole tree. A run that captures output is
|
|
128
|
+
* spawned as its own group leader, because its tree is terminated as a unit; a
|
|
129
|
+
* run that inherits the caller's descriptors keeps the caller's group, so an
|
|
130
|
+
* interrupt still reaches it.
|
|
131
|
+
* @param execution Whether the run captures output or inherits the caller's descriptors.
|
|
132
|
+
* @param platform Host platform deciding how a tree is addressed.
|
|
133
|
+
* @returns True when the run leads its own process group.
|
|
134
|
+
*/
|
|
135
|
+
function leadsOwnGroup(execution, platform = process.platform) {
|
|
136
|
+
return execution === "service" && platform !== "win32";
|
|
137
|
+
}
|
|
138
|
+
/** The target a tree probe addresses: a POSIX group when the run leads one, else the process itself. */
|
|
139
|
+
function targetOf(pid, grouped, platform) {
|
|
140
|
+
return platform !== "win32" && grouped ? -pid : pid;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Whether any member of a run's tree is still alive.
|
|
144
|
+
* @param tree The run's process id and whether it leads its own group.
|
|
145
|
+
* @param internals Injectable process operations.
|
|
146
|
+
* @returns True while the probe finds the process or a group member.
|
|
147
|
+
*/
|
|
148
|
+
function treeAlive(tree, internals = {}) {
|
|
149
|
+
const pid = tree.pid;
|
|
150
|
+
if (pid === void 0) return false;
|
|
151
|
+
const platform = internals.platform ?? process.platform;
|
|
152
|
+
const alive = internals.alive ?? ((target) => {
|
|
153
|
+
process.kill(target, 0);
|
|
154
|
+
return true;
|
|
155
|
+
});
|
|
156
|
+
try {
|
|
157
|
+
return alive(targetOf(pid, tree.grouped, platform));
|
|
158
|
+
} catch {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Wait until a terminated run's tree is gone, so the caller restores and
|
|
164
|
+
* unlocks the profile only after the scripts it started stopped writing.
|
|
165
|
+
* @param tree The run's process id and whether it leads its own group.
|
|
166
|
+
* @param internals Injectable process operations.
|
|
167
|
+
* @returns Fulfillment once no member remains, or the wait bound elapsed.
|
|
168
|
+
*/
|
|
169
|
+
async function awaitTreeGone(tree, internals = {}) {
|
|
170
|
+
const deadline = Date.now() + (internals.waitMs ?? TREE_WAIT_MS);
|
|
171
|
+
while (treeAlive(tree, internals)) {
|
|
172
|
+
if (Date.now() >= deadline) return;
|
|
173
|
+
await new Promise((resolve) => setTimeout(resolve, TREE_POLL_MS));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
//#endregion
|
|
177
|
+
//#region lib/types/failure.js
|
|
178
|
+
/** Expected management rejection; presentation belongs to the caller's locale. */
|
|
179
|
+
var ManagementFailure = class extends Error {
|
|
180
|
+
/** Code rendered by the caller's locale dictionary. */
|
|
181
|
+
code;
|
|
182
|
+
/** The packages an `incompatible-version` rejection names. */
|
|
183
|
+
incompatible;
|
|
184
|
+
/**
|
|
185
|
+
* @param code Localizable management rejection.
|
|
186
|
+
* @param incompatible Packages the running DSH version rejects, for `incompatible-version`.
|
|
187
|
+
*/
|
|
188
|
+
constructor(code, incompatible) {
|
|
189
|
+
super(code);
|
|
190
|
+
this.code = code;
|
|
191
|
+
this.incompatible = incompatible;
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
/**
|
|
195
|
+
* Drop the exemption status from an unexempted compatibility result.
|
|
196
|
+
* @param issue Result whose exemption is not active.
|
|
197
|
+
* @returns The package, runtime, and rejected peer ranges.
|
|
198
|
+
*/
|
|
199
|
+
function incompatiblePlugin(issue) {
|
|
200
|
+
return {
|
|
201
|
+
name: issue.name,
|
|
202
|
+
version: issue.version,
|
|
203
|
+
runtimeVersion: issue.runtimeVersion,
|
|
204
|
+
peers: issue.peers
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
//#endregion
|
|
14
208
|
//#region lib/types/operations.js
|
|
209
|
+
/** Shared profile package operations used by dsh plugin and the running manager. */
|
|
15
210
|
/** Resolve relative package specs against the caller's directory.
|
|
16
211
|
* @param argument One pnpm argument.
|
|
17
212
|
* @param cwd Invocation directory, never the profile directory.
|
|
@@ -69,15 +264,180 @@ async function reconcile(before, dir, anchor, options) {
|
|
|
69
264
|
};
|
|
70
265
|
await saveManifest(dir, after);
|
|
71
266
|
}
|
|
267
|
+
/**
|
|
268
|
+
* How long the pipes keep draining after their process exited, as a fixed part of
|
|
269
|
+
* finishing a run rather than a deployment knob: a descendant that inherited them
|
|
270
|
+
* holds them open, and the tail a failure classification reads is written by then.
|
|
271
|
+
*/
|
|
272
|
+
const DRAIN_AFTER_EXIT_MS = 2e3;
|
|
273
|
+
/** Whether every collector finished within `ms`.
|
|
274
|
+
* @param collectors The pipe readers racing the bound.
|
|
275
|
+
* @param ms The longest wait, in milliseconds.
|
|
276
|
+
* @returns True when all collectors settled in time.
|
|
277
|
+
*/
|
|
278
|
+
async function drainWithin(collectors, ms) {
|
|
279
|
+
if (collectors.length === 0) return true;
|
|
280
|
+
let timer;
|
|
281
|
+
try {
|
|
282
|
+
return await Promise.race([Promise.allSettled(collectors).then(() => true), new Promise((resolve) => {
|
|
283
|
+
timer = setTimeout(() => {
|
|
284
|
+
resolve(false);
|
|
285
|
+
}, ms);
|
|
286
|
+
})]);
|
|
287
|
+
} finally {
|
|
288
|
+
clearTimeout(timer);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
/** Install commands that take the packages to install as positionals. */
|
|
292
|
+
const INSTALL_COMMANDS = new Set([
|
|
293
|
+
"add",
|
|
294
|
+
"install",
|
|
295
|
+
"i"
|
|
296
|
+
]);
|
|
297
|
+
/** Bound on a pre-install registry lookup when the caller names none. */
|
|
298
|
+
const LOOKUP_TIMEOUT_MS = 2e4;
|
|
299
|
+
/** Package specs an install command names explicitly, in order. */
|
|
300
|
+
function namedSpecs(args) {
|
|
301
|
+
const index = args.findIndex((argument) => !argument.startsWith("-"));
|
|
302
|
+
const command = index < 0 ? void 0 : args[index];
|
|
303
|
+
if (command === void 0 || !INSTALL_COMMANDS.has(command)) return [];
|
|
304
|
+
return args.slice(index + 1).filter((argument) => !argument.startsWith("-"));
|
|
305
|
+
}
|
|
306
|
+
/** The manifest a named spec would install, read without installing it.
|
|
307
|
+
* A path spec is read from disk. A registry spec asks pnpm's own configuration for the version the
|
|
308
|
+
* range selects and its peer requirements. A git or tarball spec needs the fetch itself, so the
|
|
309
|
+
* check after installation is what judges it.
|
|
310
|
+
* @param dir Profile directory the lookup runs in.
|
|
311
|
+
* @param spec Anchored install spec.
|
|
312
|
+
* @param options Pnpm executable, prefix arguments, the caller's bound and signal.
|
|
313
|
+
* @param environment Environment of the caller's pnpm invocations.
|
|
314
|
+
* @param flags Flags of the run itself, so the lookup asks the registry that run will use.
|
|
315
|
+
* @returns The package manifest, or undefined when reading it would need the installation itself.
|
|
316
|
+
*/
|
|
317
|
+
async function namedSpecManifest(dir, spec, options, environment, flags) {
|
|
318
|
+
const parsed = parseInstallSpec(spec);
|
|
319
|
+
if (parsed.kind === "path") {
|
|
320
|
+
const filename = join(parsed.path, "package.json");
|
|
321
|
+
return existsSync(filename) ? JSON.parse(readFileSync(filename, "utf8")) : void 0;
|
|
322
|
+
}
|
|
323
|
+
if (parsed.kind !== "registry") return void 0;
|
|
324
|
+
const viewed = await execa(options.command ?? "pnpm", [
|
|
325
|
+
...options.args ?? [],
|
|
326
|
+
"view",
|
|
327
|
+
parsed.spec,
|
|
328
|
+
"name",
|
|
329
|
+
"version",
|
|
330
|
+
"peerDependencies",
|
|
331
|
+
"--json",
|
|
332
|
+
...flags,
|
|
333
|
+
"--config.fetch-retries=0"
|
|
334
|
+
], {
|
|
335
|
+
cwd: dir,
|
|
336
|
+
env: environment,
|
|
337
|
+
extendEnv: false,
|
|
338
|
+
reject: false,
|
|
339
|
+
stdin: "ignore",
|
|
340
|
+
...options.signal === void 0 ? {} : { cancelSignal: options.signal },
|
|
341
|
+
timeout: options.lookupTimeoutMs ?? LOOKUP_TIMEOUT_MS
|
|
342
|
+
});
|
|
343
|
+
if (viewed.exitCode !== 0) return void 0;
|
|
344
|
+
const value = JSON.parse(viewed.stdout);
|
|
345
|
+
return Array.isArray(value) ? value.at(-1) : value;
|
|
346
|
+
}
|
|
347
|
+
/** Missing installed packages are repairable; their absence is part of the before/after comparison. */
|
|
348
|
+
function optionalFile(path) {
|
|
349
|
+
try {
|
|
350
|
+
return readFileSync(path, "utf8");
|
|
351
|
+
} catch (error) {
|
|
352
|
+
if (error.code === "ENOENT") return void 0;
|
|
353
|
+
throw error;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
/** Where an operation records the pnpm run it started, for a successor when the operation's own process ends first. */
|
|
357
|
+
function runRecordPath(dir) {
|
|
358
|
+
return join(dir, ".plugin-manager", "run.json");
|
|
359
|
+
}
|
|
360
|
+
/** Record a started run; a successor that takes over the profile lock from an exited process waits for it. */
|
|
361
|
+
async function recordRun(dir, tree) {
|
|
362
|
+
if (tree.pid === void 0) return;
|
|
363
|
+
await writeFileAtomic(runRecordPath(dir), `${JSON.stringify(tree)}\n`, {
|
|
364
|
+
mode: 384,
|
|
365
|
+
dirMode: 448
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Why this operation must not run: a record left by an operation whose process
|
|
370
|
+
* ended mid-run names a run that is still writing the profile. The profile
|
|
371
|
+
* lock is taken over once its holder exits, but its pnpm tree can outlive it.
|
|
372
|
+
* A recorded run that stopped, within a bounded wait, has its record removed.
|
|
373
|
+
* @param dir Profile directory.
|
|
374
|
+
* @returns The diagnostic, or undefined when no recorded run is active.
|
|
375
|
+
*/
|
|
376
|
+
async function activeRecordedRun(dir) {
|
|
377
|
+
const path = runRecordPath(dir);
|
|
378
|
+
const text = optionalFile(path);
|
|
379
|
+
if (text === void 0) return void 0;
|
|
380
|
+
let tree;
|
|
381
|
+
try {
|
|
382
|
+
const value = JSON.parse(text);
|
|
383
|
+
const { pid, grouped } = typeof value === "object" && value !== null ? value : {};
|
|
384
|
+
if (Number.isSafeInteger(pid) && pid > 0 && typeof grouped === "boolean") tree = {
|
|
385
|
+
pid,
|
|
386
|
+
grouped
|
|
387
|
+
};
|
|
388
|
+
} catch (error) {}
|
|
389
|
+
if (tree === void 0) return `dsh: ${path} does not name a package run; delete it once no earlier package operation is still running in this profile\n`;
|
|
390
|
+
await awaitTreeGone(tree);
|
|
391
|
+
if (treeAlive(tree)) return `dsh: process ${String(tree.pid)}, started by an earlier package operation whose own process ended, is still running in this profile; wait for it or stop it, then retry. If process ${String(tree.pid)} is not that package run, delete ${path}.\n`;
|
|
392
|
+
await rm(path, { force: true });
|
|
393
|
+
}
|
|
394
|
+
/** pnpm can install plugins through any direct-dependency field. */
|
|
395
|
+
function directDependencies(manifest) {
|
|
396
|
+
const extra = manifest;
|
|
397
|
+
return {
|
|
398
|
+
...extra.devDependencies,
|
|
399
|
+
...manifest.dependencies,
|
|
400
|
+
...extra.optionalDependencies
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
/** Inspect only plugin rows contributed by the changed bundle, not its dependency closure. */
|
|
404
|
+
function bundleComponentManifests(manifest, dir, anchor) {
|
|
405
|
+
const bundle = manifest.dsh?.bundle;
|
|
406
|
+
if (bundle === void 0) return [];
|
|
407
|
+
const patches = bundlePatchPaths(dir, bundle).flatMap((file) => loadOverlayPatches("dsh", file));
|
|
408
|
+
const names = /* @__PURE__ */ new Set();
|
|
409
|
+
const visit = (rows) => {
|
|
410
|
+
for (const row of rows) {
|
|
411
|
+
if (row.group && Array.isArray(row.config)) visit(row.config);
|
|
412
|
+
if (typeof row.name !== "string" || row.name.startsWith(".") || row.name.startsWith("/") || row.name.includes(":")) continue;
|
|
413
|
+
const parts = row.name.split("/");
|
|
414
|
+
names.add(parts.slice(0, row.name.startsWith("@") ? 2 : 1).join("/"));
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
visit(composeEntries([patches.filter((patch) => patch.insert !== void 0)]));
|
|
418
|
+
return [...names].flatMap((name) => {
|
|
419
|
+
let packageDir;
|
|
420
|
+
try {
|
|
421
|
+
packageDir = resolveBundleDir("dsh", name, anchor, dir);
|
|
422
|
+
} catch (error) {
|
|
423
|
+
return [];
|
|
424
|
+
}
|
|
425
|
+
return [readProfileManifest("dsh", packageDir)];
|
|
426
|
+
});
|
|
427
|
+
}
|
|
72
428
|
/** Execute pnpm inside a profile whose caller already holds the profile write lock.
|
|
429
|
+
* Newly installed or updated direct dependencies are checked even when activation is disabled;
|
|
430
|
+
* an untouched dependency never blocks an unrelated operation and stays denied at startup.
|
|
431
|
+
* Compatibility denial restores the profile manifest and lockfile, but leaves downloaded modules on disk.
|
|
73
432
|
* @param context Launcher-owned profile and resolution locations.
|
|
74
433
|
* @param args Pnpm arguments, before relative path anchoring.
|
|
75
434
|
* @param options Output, activation and cancellation policy.
|
|
76
|
-
* @returns Exit status
|
|
435
|
+
* @returns Exit status, whether the silence bound stopped the run, and the diagnostic path.
|
|
436
|
+
* A compatibility denial returns exit code 1. Service output is bounded; CLI output uses inherited descriptors.
|
|
77
437
|
*/
|
|
78
438
|
async function runProfilePnpm(context, args, options) {
|
|
79
439
|
const dir = context.dir ?? resolveProfileDir(context.profile, context.home);
|
|
80
|
-
const
|
|
440
|
+
const active = await activeRecordedRun(dir);
|
|
81
441
|
const logRoot = join(dir, ".plugin-manager", "logs");
|
|
82
442
|
await mkdir(logRoot, {
|
|
83
443
|
recursive: true,
|
|
@@ -85,70 +445,242 @@ async function runProfilePnpm(context, args, options) {
|
|
|
85
445
|
});
|
|
86
446
|
const logPath = join(await mkdtemp(join(logRoot, "operation-")), "pnpm.log");
|
|
87
447
|
const log = await open(logPath, "wx", 384);
|
|
448
|
+
if (active !== void 0) {
|
|
449
|
+
await log.write(active);
|
|
450
|
+
await log.close();
|
|
451
|
+
options.onOutput?.(active, "stderr");
|
|
452
|
+
const bytes = Buffer.from(active);
|
|
453
|
+
return {
|
|
454
|
+
exitCode: 1,
|
|
455
|
+
output: bytes.subarray(Math.max(0, bytes.length - options.outputBytes)).toString("utf8"),
|
|
456
|
+
truncated: bytes.length > options.outputBytes,
|
|
457
|
+
logPath
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
const before = readProfileManifest("dsh", dir);
|
|
461
|
+
const savedFiles = ["package.json", "pnpm-lock.yaml"].map((name) => ({
|
|
462
|
+
path: join(dir, name),
|
|
463
|
+
text: optionalFile(join(dir, name))
|
|
464
|
+
}));
|
|
465
|
+
const beforeDependencies = directDependencies(before);
|
|
466
|
+
const installedBefore = new Map(Object.keys(beforeDependencies).map((name) => [name, optionalFile(join(dir, "node_modules", name, "package.json"))]));
|
|
88
467
|
let output = Buffer.alloc(0);
|
|
89
468
|
let truncated = false;
|
|
469
|
+
const append = (bytes) => {
|
|
470
|
+
output = Buffer.concat([output, bytes]);
|
|
471
|
+
if (output.length > options.outputBytes) {
|
|
472
|
+
truncated = true;
|
|
473
|
+
output = output.subarray(output.length - options.outputBytes);
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
const environment = {
|
|
477
|
+
...options.execution === "cli" ? process.env : scrubbedParentEnv(),
|
|
478
|
+
...options.env
|
|
479
|
+
};
|
|
480
|
+
const restore = async () => {
|
|
481
|
+
for (const file of savedFiles) if (file.text === void 0) await rm(file.path, { force: true });
|
|
482
|
+
else await writeFileAtomic(file.path, file.text, { mode: 384 });
|
|
483
|
+
};
|
|
484
|
+
/** Packages a compatibility check refused; callers render them for their own surface. */
|
|
485
|
+
const incompatible = [];
|
|
486
|
+
const rejected = async (warnings, restoration) => {
|
|
487
|
+
const diagnostic = `\ndsh: installation rejected: ${warnings.join("\n")}\ndsh: ${restoration}.\n`;
|
|
488
|
+
await log.write(diagnostic);
|
|
489
|
+
options.onOutput?.(diagnostic, "stderr");
|
|
490
|
+
append(Buffer.from(diagnostic));
|
|
491
|
+
await log.close();
|
|
492
|
+
return {
|
|
493
|
+
exitCode: 1,
|
|
494
|
+
output: output.toString("utf8"),
|
|
495
|
+
truncated,
|
|
496
|
+
logPath,
|
|
497
|
+
incompatible
|
|
498
|
+
};
|
|
499
|
+
};
|
|
500
|
+
const preflight = [];
|
|
501
|
+
const exemptions = readProfileVersionExemptions(dir);
|
|
502
|
+
const registryFlags = args.filter((argument) => argument.startsWith("--registry="));
|
|
503
|
+
for (const raw of namedSpecs(args)) try {
|
|
504
|
+
const manifest = await namedSpecManifest(dir, anchorPathSpec(raw, context.cwd), options, environment, registryFlags);
|
|
505
|
+
if (manifest === void 0) continue;
|
|
506
|
+
const issue = evaluatePluginCompatibility(manifest, exemptions);
|
|
507
|
+
if (issue !== void 0 && !issue.exempted) {
|
|
508
|
+
preflight.push(pluginCompatibilityWarning(issue));
|
|
509
|
+
incompatible.push(incompatiblePlugin(issue));
|
|
510
|
+
}
|
|
511
|
+
} catch (error) {
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
if (preflight.length > 0) return rejected(preflight, "nothing was installed");
|
|
90
515
|
const cancellation = new AbortController();
|
|
516
|
+
const grouped = leadsOwnGroup(options.execution);
|
|
91
517
|
const child = execa(options.command ?? "pnpm", [...options.args ?? [], ...args.map((arg) => anchorPathSpec(arg, context.cwd))], {
|
|
92
518
|
cwd: dir,
|
|
93
|
-
env:
|
|
94
|
-
...options.execution === "cli" ? process.env : scrubbedParentEnv(),
|
|
95
|
-
...options.env
|
|
96
|
-
},
|
|
519
|
+
env: environment,
|
|
97
520
|
extendEnv: false,
|
|
98
521
|
reject: false,
|
|
99
522
|
stdout: options.execution === "cli" ? "inherit" : "pipe",
|
|
100
523
|
stderr: options.execution === "cli" ? "inherit" : "pipe",
|
|
524
|
+
killDescendants: options.execution === "service",
|
|
101
525
|
buffer: false,
|
|
102
526
|
stdin: options.execution === "cli" ? "inherit" : "ignore",
|
|
103
527
|
cancelSignal: options.signal === void 0 ? cancellation.signal : AbortSignal.any([cancellation.signal, options.signal])
|
|
104
528
|
});
|
|
529
|
+
const exited = once(child.nodeChildProcess, "exit").catch(() => void 0);
|
|
105
530
|
let writes = Promise.resolve();
|
|
531
|
+
/** `settled` records that the process outcome is known; `stalled` that the silence bound stopped the run. */
|
|
532
|
+
const control = {
|
|
533
|
+
settled: false,
|
|
534
|
+
stalled: false
|
|
535
|
+
};
|
|
536
|
+
/** Set once this call cuts the reading short itself, so the close it causes is not read as a run failure. */
|
|
537
|
+
let cut = false;
|
|
538
|
+
/** The first failure a reading hit before that cut, which the run still reports. */
|
|
539
|
+
let failure;
|
|
540
|
+
let idleTimer;
|
|
541
|
+
/** The silence bound: a captured run that stops printing without exiting is terminated, never awaited. */
|
|
542
|
+
const armIdle = () => {
|
|
543
|
+
if (control.settled || options.idleTimeoutMs === void 0) return;
|
|
544
|
+
clearTimeout(idleTimer);
|
|
545
|
+
idleTimer = setTimeout(() => {
|
|
546
|
+
control.stalled = true;
|
|
547
|
+
child.kill();
|
|
548
|
+
}, options.idleTimeoutMs);
|
|
549
|
+
};
|
|
106
550
|
const collect = async (stream, kind) => {
|
|
107
551
|
try {
|
|
108
552
|
for await (const chunk of stream) {
|
|
553
|
+
armIdle();
|
|
109
554
|
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
110
555
|
writes = writes.then(async () => {
|
|
111
556
|
await log.write(bytes);
|
|
112
557
|
});
|
|
113
558
|
await writes;
|
|
114
559
|
options.onOutput?.(bytes.toString("utf8"), kind);
|
|
115
|
-
|
|
116
|
-
if (output.length > options.outputBytes) {
|
|
117
|
-
truncated = true;
|
|
118
|
-
output = output.subarray(output.length - options.outputBytes);
|
|
119
|
-
}
|
|
560
|
+
append(bytes);
|
|
120
561
|
}
|
|
121
562
|
} catch (error) {
|
|
122
|
-
|
|
563
|
+
if (!cut) {
|
|
564
|
+
failure ??= error instanceof Error ? error : new Error(String(error));
|
|
565
|
+
cancellation.abort();
|
|
566
|
+
}
|
|
123
567
|
throw error;
|
|
124
568
|
}
|
|
125
569
|
};
|
|
570
|
+
const collectors = [...child.stdout === null ? [] : [collect(child.stdout, "stdout")], ...child.stderr === null ? [] : [collect(child.stderr, "stderr")]];
|
|
571
|
+
for (const collector of collectors) collector.catch(() => {});
|
|
572
|
+
if (collectors.length > 0) armIdle();
|
|
126
573
|
let exitCode;
|
|
127
574
|
try {
|
|
128
|
-
|
|
129
|
-
child,
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
]);
|
|
133
|
-
|
|
575
|
+
await recordRun(dir, {
|
|
576
|
+
pid: child.pid,
|
|
577
|
+
grouped
|
|
578
|
+
});
|
|
579
|
+
const settled = Promise.allSettled([child]);
|
|
580
|
+
await Promise.race([exited, settled]);
|
|
581
|
+
control.settled = true;
|
|
582
|
+
clearTimeout(idleTimer);
|
|
583
|
+
if (control.stalled) await awaitTreeGone({
|
|
584
|
+
pid: child.pid,
|
|
585
|
+
grouped
|
|
586
|
+
});
|
|
587
|
+
if (await drainWithin(collectors, DRAIN_AFTER_EXIT_MS)) {
|
|
588
|
+
for (const stream of await Promise.allSettled(collectors)) if (stream.status === "rejected") throw stream.reason;
|
|
589
|
+
} else {
|
|
590
|
+
cut = true;
|
|
591
|
+
child.stdout?.destroy();
|
|
592
|
+
child.stderr?.destroy();
|
|
593
|
+
const notice = "dsh: pnpm output was cut short after its process exited\n";
|
|
594
|
+
await log.write(notice);
|
|
595
|
+
if (failure !== void 0) throw failure;
|
|
596
|
+
options.onOutput?.(notice, "stderr");
|
|
597
|
+
append(Buffer.from(notice));
|
|
598
|
+
}
|
|
599
|
+
const [completion] = await settled;
|
|
134
600
|
if (completion.status === "rejected") throw completion.reason;
|
|
135
601
|
const result = completion.value;
|
|
136
602
|
exitCode = result.exitCode ?? (result.code === "ENOENT" ? 127 : 1);
|
|
603
|
+
if (control.stalled) {
|
|
604
|
+
const notice = `dsh: pnpm printed nothing for ${String(options.idleTimeoutMs)}ms and was terminated\n`;
|
|
605
|
+
await log.write(notice);
|
|
606
|
+
options.onOutput?.(notice, "stderr");
|
|
607
|
+
append(Buffer.from(notice));
|
|
608
|
+
}
|
|
137
609
|
if (result.failed && output.length === 0) {
|
|
138
610
|
const diagnostic = result.shortMessage ?? "pnpm failed";
|
|
139
611
|
await log.write(diagnostic);
|
|
140
612
|
truncated = Buffer.byteLength(diagnostic) > options.outputBytes;
|
|
141
613
|
output = Buffer.from(diagnostic).subarray(0, options.outputBytes);
|
|
142
614
|
}
|
|
143
|
-
if (exitCode === 0 &&
|
|
615
|
+
if (exitCode === 0 && !control.stalled) {
|
|
616
|
+
const after = readProfileManifest("dsh", dir);
|
|
617
|
+
const warnings = [];
|
|
618
|
+
for (const [name, spec] of Object.entries(directDependencies(after))) {
|
|
619
|
+
const packageDir = join(dir, "node_modules", name);
|
|
620
|
+
const installed = optionalFile(join(packageDir, "package.json"));
|
|
621
|
+
if (installed === void 0) continue;
|
|
622
|
+
const untouched = beforeDependencies[name] === spec && installedBefore.get(name) === installed;
|
|
623
|
+
const found = [];
|
|
624
|
+
const issues = [];
|
|
625
|
+
try {
|
|
626
|
+
const manifest = readProfileManifest("dsh", packageDir);
|
|
627
|
+
for (const candidate of [manifest, ...bundleComponentManifests(manifest, packageDir, context.installAnchor)]) {
|
|
628
|
+
const issue = evaluatePluginCompatibility(candidate, readProfileVersionExemptions(dir));
|
|
629
|
+
if (issue !== void 0 && !issue.exempted) {
|
|
630
|
+
found.push(pluginCompatibilityWarning(issue));
|
|
631
|
+
issues.push(incompatiblePlugin(issue));
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
} catch (error) {
|
|
635
|
+
found.push(`Cannot validate installed package ${name}: ${String(error)}`);
|
|
636
|
+
}
|
|
637
|
+
if (found.length === 0) continue;
|
|
638
|
+
if (!untouched) {
|
|
639
|
+
warnings.push(...found);
|
|
640
|
+
incompatible.push(...issues);
|
|
641
|
+
} else {
|
|
642
|
+
const notice = `\ndsh: warning: ${found.join("\n")}\ndsh: it stays installed but profile startup denies it until you grant an exemption for those exact versions.\n`;
|
|
643
|
+
await log.write(notice);
|
|
644
|
+
options.onOutput?.(notice, "stderr");
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
if (warnings.length > 0) {
|
|
648
|
+
await restore();
|
|
649
|
+
const repair = ["install", savedFiles.some((file) => file.path.endsWith("pnpm-lock.yaml") && file.text !== void 0) ? "--frozen-lockfile" : "--config.lockfile=false"];
|
|
650
|
+
const repairing = execa(options.command ?? "pnpm", [...options.args ?? [], ...repair], {
|
|
651
|
+
cwd: dir,
|
|
652
|
+
env: environment,
|
|
653
|
+
extendEnv: false,
|
|
654
|
+
reject: false,
|
|
655
|
+
stdin: "ignore",
|
|
656
|
+
...options.idleTimeoutMs === void 0 ? {} : { timeout: options.idleTimeoutMs }
|
|
657
|
+
});
|
|
658
|
+
await recordRun(dir, {
|
|
659
|
+
pid: repairing.pid,
|
|
660
|
+
grouped: false
|
|
661
|
+
});
|
|
662
|
+
const repaired = await repairing;
|
|
663
|
+
exitCode = 1;
|
|
664
|
+
const restoration = repaired.exitCode === 0 ? "restored package.json, pnpm-lock.yaml, and node_modules" : "restored package.json and pnpm-lock.yaml, but node_modules could not be reinstalled; run 'dsh plugin install'";
|
|
665
|
+
const diagnostic = `\ndsh: installation rejected: ${warnings.join("\n")}\ndsh: ${restoration}.\n`;
|
|
666
|
+
await log.write(diagnostic);
|
|
667
|
+
options.onOutput?.(diagnostic, "stderr");
|
|
668
|
+
append(Buffer.from(diagnostic));
|
|
669
|
+
} else if (options.activateNewBundles !== false) await reconcile(before, dir, context.installAnchor, options);
|
|
670
|
+
}
|
|
144
671
|
} finally {
|
|
672
|
+
control.settled = true;
|
|
673
|
+
clearTimeout(idleTimer);
|
|
674
|
+
await rm(runRecordPath(dir), { force: true });
|
|
145
675
|
await log.close();
|
|
146
676
|
}
|
|
147
677
|
return {
|
|
148
678
|
exitCode,
|
|
149
679
|
output: output.toString("utf8"),
|
|
150
680
|
truncated,
|
|
151
|
-
logPath
|
|
681
|
+
logPath,
|
|
682
|
+
...control.stalled ? { timedOut: true } : {},
|
|
683
|
+
...incompatible.length > 0 ? { incompatible } : {}
|
|
152
684
|
};
|
|
153
685
|
}
|
|
154
686
|
/**
|
|
@@ -246,6 +778,7 @@ const LOG_KINDS = [
|
|
|
246
778
|
["disk-full", /\bENOSPC\b|no space left on device/i],
|
|
247
779
|
["permission", /\bEACCES\b|\bEPERM\b|permission denied/i],
|
|
248
780
|
["integrity", /ERR_PNPM_TARBALL_INTEGRITY|ERR_PNPM_BAD_TARBALL_SIZE|\bEINTEGRITY\b/],
|
|
781
|
+
["unknown", /The requested URL returned error: 40[134]\b/],
|
|
249
782
|
["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|\bFETCH_ERROR\b|socket hang up|Could not resolve host|unable to access/]
|
|
250
783
|
];
|
|
251
784
|
/**
|
|
@@ -259,110 +792,6 @@ function classifyInstallFailure(facts) {
|
|
|
259
792
|
for (const [kind, pattern] of LOG_KINDS) if (pattern.test(facts.log)) return kind;
|
|
260
793
|
return "unknown";
|
|
261
794
|
}
|
|
262
|
-
//#endregion
|
|
263
|
-
//#region lib/types/install-spec.js
|
|
264
|
-
/**
|
|
265
|
-
* Reading an install spec before pnpm sees it: which of pnpm's spec forms it
|
|
266
|
-
* takes, and for a registry name whether it is one the registry can accept.
|
|
267
|
-
* @module @deepseek-ai/dsh-plugin-manager/install-spec
|
|
268
|
-
*/
|
|
269
|
-
/** The forms pnpm resolves through a git host: a host shorthand, a git URL, or a hosted repository URL. */
|
|
270
|
-
const GIT_SHORTHAND = /^(?:github|gitlab|bitbucket|gist):/i;
|
|
271
|
-
const GIT_URL = /^git(?:\+[a-z]+)?:\/\/|^git@[^:]+:/i;
|
|
272
|
-
const HOSTED_REPOSITORY_URL = /^https?:\/\/[^/]+\/[^/]+\/[^/#]+(?:\.git)?(?:#.*)?$/i;
|
|
273
|
-
/** The hosts pnpm's shorthands stand for. */
|
|
274
|
-
const GIT_SHORTHAND_HOSTS = {
|
|
275
|
-
github: "github.com",
|
|
276
|
-
gitlab: "gitlab.com",
|
|
277
|
-
bitbucket: "bitbucket.org",
|
|
278
|
-
gist: "gist.github.com"
|
|
279
|
-
};
|
|
280
|
-
/** A tarball, on disk or over HTTP. */
|
|
281
|
-
const TARBALL_SPEC = /\.(?:tgz|tar\.gz)(?:#.*)?$/i;
|
|
282
|
-
/** An npm package name: lowercase URL-safe segments, an optional scope, no leading dot or underscore. */
|
|
283
|
-
const PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/;
|
|
284
|
-
const PACKAGE_NAME_MAX_LENGTH = 214;
|
|
285
|
-
/** A spec neither pnpm nor the registry would take; `reason` is what the person reads. */
|
|
286
|
-
var InvalidInstallSpecError = class extends Error {
|
|
287
|
-
spec;
|
|
288
|
-
reason;
|
|
289
|
-
/**
|
|
290
|
-
* @param spec - the spec as typed, trimmed.
|
|
291
|
-
* @param reason - why it is refused, as one sentence.
|
|
292
|
-
*/
|
|
293
|
-
constructor(spec, reason) {
|
|
294
|
-
super(`plugin-manager: ${reason}: ${spec}`);
|
|
295
|
-
this.spec = spec;
|
|
296
|
-
this.reason = reason;
|
|
297
|
-
this.name = "InvalidInstallSpecError";
|
|
298
|
-
}
|
|
299
|
-
};
|
|
300
|
-
function invalid(spec, reason) {
|
|
301
|
-
return new InvalidInstallSpecError(spec, reason);
|
|
302
|
-
}
|
|
303
|
-
/** The host a git spec is cloned from: the shorthand's host, the scp-like user@host, or the URL's host with its port. */
|
|
304
|
-
function gitHost(spec) {
|
|
305
|
-
const shorthand = /^([a-z]+):/i.exec(spec)?.[1]?.toLowerCase();
|
|
306
|
-
if (shorthand !== void 0 && Object.hasOwn(GIT_SHORTHAND_HOSTS, shorthand)) return GIT_SHORTHAND_HOSTS[shorthand];
|
|
307
|
-
const scp = /^git@([^:]+):/i.exec(spec)?.[1];
|
|
308
|
-
if (scp !== void 0) return scp.toLowerCase();
|
|
309
|
-
return new URL(spec.replace(/^git\+/i, "")).host;
|
|
310
|
-
}
|
|
311
|
-
/**
|
|
312
|
-
* Read a spec into its form. A path must be absolute: the Host's working
|
|
313
|
-
* directory means nothing to the person typing into a browser, and a
|
|
314
|
-
* relative path resolved against the profile would point inside it.
|
|
315
|
-
* @param raw - the spec as typed.
|
|
316
|
-
* @returns the parsed spec.
|
|
317
|
-
* @throws {InvalidInstallSpecError} for an empty spec, a relative path, a name the
|
|
318
|
-
* registry would refuse, or a URL that is neither a git host nor a tarball.
|
|
319
|
-
*/
|
|
320
|
-
function parseInstallSpec(raw) {
|
|
321
|
-
const spec = raw.trim();
|
|
322
|
-
if (spec === "") throw invalid(spec, "the package spec must not be empty");
|
|
323
|
-
const path = spec.replace(/^(?:file|link):/, "");
|
|
324
|
-
if (path !== spec || isAbsolute(path)) {
|
|
325
|
-
if (!isAbsolute(path)) throw invalid(spec, "a local path must be absolute");
|
|
326
|
-
return TARBALL_SPEC.test(path) ? {
|
|
327
|
-
kind: "tarball",
|
|
328
|
-
spec,
|
|
329
|
-
path
|
|
330
|
-
} : {
|
|
331
|
-
kind: "path",
|
|
332
|
-
spec,
|
|
333
|
-
path
|
|
334
|
-
};
|
|
335
|
-
}
|
|
336
|
-
if (/^\.{1,2}(?:[\\/]|$)/.test(spec)) throw invalid(spec, "a local path must be absolute");
|
|
337
|
-
if ((GIT_SHORTHAND.test(spec) || GIT_URL.test(spec) || HOSTED_REPOSITORY_URL.test(spec)) && !TARBALL_SPEC.test(spec)) return {
|
|
338
|
-
kind: "git",
|
|
339
|
-
spec,
|
|
340
|
-
host: gitHost(spec)
|
|
341
|
-
};
|
|
342
|
-
if (/^https?:\/\//i.test(spec)) {
|
|
343
|
-
if (TARBALL_SPEC.test(spec)) return {
|
|
344
|
-
kind: "tarball",
|
|
345
|
-
spec,
|
|
346
|
-
host: new URL(spec).host
|
|
347
|
-
};
|
|
348
|
-
throw invalid(spec, "a URL must point at a git repository or a tarball");
|
|
349
|
-
}
|
|
350
|
-
const at = spec.indexOf("@", 1);
|
|
351
|
-
const name = at === -1 ? spec : spec.slice(0, at);
|
|
352
|
-
const range = at === -1 ? void 0 : spec.slice(at + 1);
|
|
353
|
-
if (name.length > PACKAGE_NAME_MAX_LENGTH || !PACKAGE_NAME.test(name)) throw invalid(spec, "not a package name the registry accepts");
|
|
354
|
-
if (range === "") throw invalid(spec, "a version after @ must not be empty");
|
|
355
|
-
return range === void 0 ? {
|
|
356
|
-
kind: "registry",
|
|
357
|
-
spec,
|
|
358
|
-
name
|
|
359
|
-
} : {
|
|
360
|
-
kind: "registry",
|
|
361
|
-
spec,
|
|
362
|
-
name,
|
|
363
|
-
range
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
795
|
/** Public npmmirror URL shared by the fallback configuration and public-registry comparison. */
|
|
367
796
|
const NPMMIRROR_REGISTRY = "https://registry.npmmirror.com/";
|
|
368
797
|
/**
|
|
@@ -482,18 +911,6 @@ async function writePluginEnabled(filename, id, name, enabled) {
|
|
|
482
911
|
return true;
|
|
483
912
|
}
|
|
484
913
|
//#endregion
|
|
485
|
-
//#region lib/types/failure.js
|
|
486
|
-
/** Expected management rejection; presentation belongs to the caller's locale. */
|
|
487
|
-
var ManagementFailure = class extends Error {
|
|
488
|
-
/** Code rendered by the caller's locale dictionary. */
|
|
489
|
-
code;
|
|
490
|
-
/** @param code Localizable management rejection. */
|
|
491
|
-
constructor(code) {
|
|
492
|
-
super(code);
|
|
493
|
-
this.code = code;
|
|
494
|
-
}
|
|
495
|
-
};
|
|
496
|
-
//#endregion
|
|
497
914
|
//#region lib/types/build-approval.js
|
|
498
915
|
/** Approve pnpm's pending dependency scripts in the current profile's workspace settings. */
|
|
499
916
|
async function readPolicy(dir) {
|
|
@@ -537,6 +954,84 @@ async function approveBuilds(dir, names) {
|
|
|
537
954
|
await writeFileAtomic(join(dir, "pnpm-workspace.yaml"), String(document), { mode: 384 });
|
|
538
955
|
}
|
|
539
956
|
//#endregion
|
|
957
|
+
//#region lib/types/github-connection.js
|
|
958
|
+
/** Bounded GitHub repository checks using the installer's Git configuration and environment. */
|
|
959
|
+
/**
|
|
960
|
+
* Check a GitHub repository before pnpm starts, without downloading or building its package.
|
|
961
|
+
* Git reads the profile's Git and proxy configuration without invoking credential helpers or prompting.
|
|
962
|
+
* Timeout and cancellation terminate its descendants too; pnpm owns authentication and transport fallback.
|
|
963
|
+
* @param spec The parsed installation address; npm packages, local paths and other hosts are not checked.
|
|
964
|
+
* @param dir The profile directory where installation runs.
|
|
965
|
+
* @param options The connection deadline, output bound and operation cancellation.
|
|
966
|
+
* @returns A failed check with bounded output and a complete log at logPath, or undefined for a reachable repository or an unhandled spec.
|
|
967
|
+
*/
|
|
968
|
+
async function checkGithubConnection(spec, dir, options) {
|
|
969
|
+
if (spec.kind !== "git") return void 0;
|
|
970
|
+
const host = spec.host.toLowerCase().replace(/:.*$/, "");
|
|
971
|
+
if (host !== "github.com" && !host.endsWith(".github.com")) return void 0;
|
|
972
|
+
const repository = spec.spec.replace(/#.*$/s, "").replace(/^github:/i, "https://github.com/").replace(/^gist:/i, "https://gist.github.com/").replace(/^git\+/i, "");
|
|
973
|
+
const logRoot = join(dir, ".plugin-manager", "logs");
|
|
974
|
+
await mkdir(logRoot, {
|
|
975
|
+
recursive: true,
|
|
976
|
+
mode: 448
|
|
977
|
+
});
|
|
978
|
+
const logPath = join(await mkdtemp(join(logRoot, "github-connection-")), "git.log");
|
|
979
|
+
const log = await open(logPath, "ax+", 384);
|
|
980
|
+
try {
|
|
981
|
+
const result = await execa("git", [
|
|
982
|
+
"-c",
|
|
983
|
+
"credential.helper=",
|
|
984
|
+
"ls-remote",
|
|
985
|
+
"--",
|
|
986
|
+
repository,
|
|
987
|
+
"HEAD"
|
|
988
|
+
], {
|
|
989
|
+
cwd: dir,
|
|
990
|
+
env: {
|
|
991
|
+
...scrubbedParentEnv(),
|
|
992
|
+
...options.env,
|
|
993
|
+
LC_ALL: "C",
|
|
994
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
995
|
+
GIT_ASKPASS: "",
|
|
996
|
+
SSH_ASKPASS: "",
|
|
997
|
+
SSH_ASKPASS_REQUIRE: "never"
|
|
998
|
+
},
|
|
999
|
+
extendEnv: false,
|
|
1000
|
+
stdin: "ignore",
|
|
1001
|
+
stdout: "ignore",
|
|
1002
|
+
stderr: {
|
|
1003
|
+
file: logPath,
|
|
1004
|
+
append: true
|
|
1005
|
+
},
|
|
1006
|
+
buffer: false,
|
|
1007
|
+
reject: false,
|
|
1008
|
+
timeout: options.timeoutMs,
|
|
1009
|
+
cancelSignal: options.signal,
|
|
1010
|
+
killDescendants: true,
|
|
1011
|
+
killSignal: "SIGKILL"
|
|
1012
|
+
});
|
|
1013
|
+
if (!result.failed) return void 0;
|
|
1014
|
+
if (result.timedOut) await log.write(`dsh: connection to ${spec.host} timed out after ${String(options.timeoutMs)}ms\n`);
|
|
1015
|
+
if ((await log.stat()).size === 0) await log.write(result.shortMessage ?? "GitHub connection check failed");
|
|
1016
|
+
const { size } = await log.stat();
|
|
1017
|
+
const bytes = Buffer.alloc(Math.min(size, options.outputBytes));
|
|
1018
|
+
await log.read(bytes, 0, bytes.length, size - bytes.length);
|
|
1019
|
+
const output = bytes.toString("utf8");
|
|
1020
|
+
return {
|
|
1021
|
+
exitCode: result.exitCode ?? (result.code === "ENOENT" ? 127 : 1),
|
|
1022
|
+
output,
|
|
1023
|
+
truncated: size > options.outputBytes,
|
|
1024
|
+
logPath,
|
|
1025
|
+
kind: classifyInstallFailure({
|
|
1026
|
+
log: output,
|
|
1027
|
+
timedOut: result.timedOut
|
|
1028
|
+
})
|
|
1029
|
+
};
|
|
1030
|
+
} finally {
|
|
1031
|
+
await log.close();
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
//#endregion
|
|
540
1035
|
//#region lib/types/index.js
|
|
541
1036
|
/** Current-profile plugin and bundle management over shared dsh plugin operations. */
|
|
542
1037
|
var __runInitializers = function(thisArg, initializers, value) {
|
|
@@ -611,10 +1106,14 @@ function messageOf(error) {
|
|
|
611
1106
|
}
|
|
612
1107
|
/** An expected refusal keeps its code; anything else becomes an operation error carrying its exact diagnostic. */
|
|
613
1108
|
function managementError(error) {
|
|
614
|
-
|
|
1109
|
+
if (!(error instanceof ManagementFailure)) return {
|
|
615
1110
|
code: "operation-error",
|
|
616
1111
|
diagnostic: messageOf(error)
|
|
617
1112
|
};
|
|
1113
|
+
return {
|
|
1114
|
+
code: error.code,
|
|
1115
|
+
...error.incompatible === void 0 ? {} : { incompatible: error.incompatible }
|
|
1116
|
+
};
|
|
618
1117
|
}
|
|
619
1118
|
/** The caller stopped an installation; its files are restored before this is thrown. */
|
|
620
1119
|
var InstallCancelledError = class extends Error {
|
|
@@ -684,6 +1183,8 @@ function parsedForRegistry(spec) {
|
|
|
684
1183
|
let PluginManager = (() => {
|
|
685
1184
|
let _classSuper = TypertRemoteService;
|
|
686
1185
|
let _instanceExtraInitializers = [];
|
|
1186
|
+
let _listVersionExemptions_decorators;
|
|
1187
|
+
let _setVersionExemption_decorators;
|
|
687
1188
|
let _listPlugins_decorators;
|
|
688
1189
|
let _listBundles_decorators;
|
|
689
1190
|
let _registries_decorators;
|
|
@@ -697,6 +1198,8 @@ let PluginManager = (() => {
|
|
|
697
1198
|
return class PluginManager extends _classSuper {
|
|
698
1199
|
static {
|
|
699
1200
|
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
1201
|
+
_listVersionExemptions_decorators = [Remote];
|
|
1202
|
+
_setVersionExemption_decorators = [Remote];
|
|
700
1203
|
_listPlugins_decorators = [Remote];
|
|
701
1204
|
_listBundles_decorators = [Remote];
|
|
702
1205
|
_registries_decorators = [Remote];
|
|
@@ -707,6 +1210,28 @@ let PluginManager = (() => {
|
|
|
707
1210
|
_waitForInstall_decorators = [Remote];
|
|
708
1211
|
_cancelInstall_decorators = [Remote];
|
|
709
1212
|
_removeBundle_decorators = [Remote];
|
|
1213
|
+
__esDecorate(this, null, _listVersionExemptions_decorators, {
|
|
1214
|
+
kind: "method",
|
|
1215
|
+
name: "listVersionExemptions",
|
|
1216
|
+
static: false,
|
|
1217
|
+
private: false,
|
|
1218
|
+
access: {
|
|
1219
|
+
has: (obj) => "listVersionExemptions" in obj,
|
|
1220
|
+
get: (obj) => obj.listVersionExemptions
|
|
1221
|
+
},
|
|
1222
|
+
metadata: _metadata
|
|
1223
|
+
}, null, _instanceExtraInitializers);
|
|
1224
|
+
__esDecorate(this, null, _setVersionExemption_decorators, {
|
|
1225
|
+
kind: "method",
|
|
1226
|
+
name: "setVersionExemption",
|
|
1227
|
+
static: false,
|
|
1228
|
+
private: false,
|
|
1229
|
+
access: {
|
|
1230
|
+
has: (obj) => "setVersionExemption" in obj,
|
|
1231
|
+
get: (obj) => obj.setVersionExemption
|
|
1232
|
+
},
|
|
1233
|
+
metadata: _metadata
|
|
1234
|
+
}, null, _instanceExtraInitializers);
|
|
710
1235
|
__esDecorate(this, null, _listPlugins_decorators, {
|
|
711
1236
|
kind: "method",
|
|
712
1237
|
name: "listPlugins",
|
|
@@ -830,6 +1355,8 @@ let PluginManager = (() => {
|
|
|
830
1355
|
outputBytes: z.number().step(1).min(1).default(16384),
|
|
831
1356
|
lockWaitMs: z.number().step(1).min(0).default(12e4),
|
|
832
1357
|
inspectTimeoutMs: z.number().step(1).min(1e3).default(2e4),
|
|
1358
|
+
githubConnectionTimeoutMs: z.number().step(1).min(1e3).default(5e3),
|
|
1359
|
+
idleTimeoutMs: z.number().step(1).min(1e3).default(6e5),
|
|
833
1360
|
registry: z.string().pattern(REGISTRY_URL),
|
|
834
1361
|
fallbackRegistries: z.array(z.string().pattern(REGISTRY_URL)).default([NPMMIRROR_REGISTRY])
|
|
835
1362
|
});
|
|
@@ -841,6 +1368,8 @@ let PluginManager = (() => {
|
|
|
841
1368
|
outputBytes;
|
|
842
1369
|
lockWaitMs;
|
|
843
1370
|
inspectTimeoutMs;
|
|
1371
|
+
githubConnectionTimeoutMs;
|
|
1372
|
+
idleTimeoutMs;
|
|
844
1373
|
pnpmCommand;
|
|
845
1374
|
configuredRegistries;
|
|
846
1375
|
ownerContext;
|
|
@@ -856,6 +1385,8 @@ let PluginManager = (() => {
|
|
|
856
1385
|
this.outputBytes = config.outputBytes;
|
|
857
1386
|
this.lockWaitMs = config.lockWaitMs;
|
|
858
1387
|
this.inspectTimeoutMs = config.inspectTimeoutMs;
|
|
1388
|
+
this.githubConnectionTimeoutMs = config.githubConnectionTimeoutMs;
|
|
1389
|
+
this.idleTimeoutMs = config.idleTimeoutMs;
|
|
859
1390
|
this.pnpmCommand = config.pnpmCommand;
|
|
860
1391
|
this.configuredRegistries = {
|
|
861
1392
|
registry: config.registry === void 0 ? null : normalizeRegistry(config.registry),
|
|
@@ -866,6 +1397,34 @@ let PluginManager = (() => {
|
|
|
866
1397
|
await Promise.allSettled([...this.packageOperations]);
|
|
867
1398
|
}, "plugin-manager: package cancellation");
|
|
868
1399
|
}
|
|
1400
|
+
/** Read exact plugin-version exemptions saved in this profile.
|
|
1401
|
+
* @returns Accepted package-name@version keys with the runtime versions they may run on, and any
|
|
1402
|
+
* record or file problem the reader rejected, which the caller reports instead of failing.
|
|
1403
|
+
*/
|
|
1404
|
+
listVersionExemptions() {
|
|
1405
|
+
const { exemptions, warnings } = readProfileCompatibility(this.profile.dir);
|
|
1406
|
+
return {
|
|
1407
|
+
exemptions,
|
|
1408
|
+
warnings
|
|
1409
|
+
};
|
|
1410
|
+
}
|
|
1411
|
+
/** Grant or revoke one exact plugin/runtime exemption and reevaluate live plugins.
|
|
1412
|
+
* @param packageVersion Exact manifest package name followed by @ and its version; never an installation spec or alias.
|
|
1413
|
+
* @param runtimeVersion Exact current DSH version for grants; revocation may name a previous runtime.
|
|
1414
|
+
* @param enabled Whether to grant rather than revoke the exemption.
|
|
1415
|
+
* @param acceptRisk Required true for grants after the user accepts possible crashes and data loss.
|
|
1416
|
+
* @returns Saved and runtime outcomes. Startup-only profiles require restart.
|
|
1417
|
+
*/
|
|
1418
|
+
setVersionExemption(packageVersion, runtimeVersion, enabled, acceptRisk) {
|
|
1419
|
+
return this.change((result) => this.configure(async () => {
|
|
1420
|
+
await setProfileVersionExemption(this.profile.dir, packageVersion, runtimeVersion, enabled, acceptRisk === true);
|
|
1421
|
+
result.warnings = await this.reload();
|
|
1422
|
+
}), {
|
|
1423
|
+
stage: "enable",
|
|
1424
|
+
target: packageVersion,
|
|
1425
|
+
enabled
|
|
1426
|
+
}, "bundle");
|
|
1427
|
+
}
|
|
869
1428
|
/** Read current plugins, including why a row cannot be changed through the profile patch.
|
|
870
1429
|
* @returns Current runtime entries with persistent patch targets.
|
|
871
1430
|
*/
|
|
@@ -896,6 +1455,7 @@ let PluginManager = (() => {
|
|
|
896
1455
|
*/
|
|
897
1456
|
listBundles() {
|
|
898
1457
|
const manifest = readProfileManifest("dsh", this.profile.dir);
|
|
1458
|
+
const exemptions = readProfileVersionExemptions(this.profile.dir);
|
|
899
1459
|
const selected = manifest.dsh?.profile?.bundles ?? [];
|
|
900
1460
|
const dependencies = Object.keys(manifest.dependencies ?? {});
|
|
901
1461
|
const installation = JSON.parse(readFileSync(this.profile.installAnchor, "utf8"));
|
|
@@ -927,6 +1487,8 @@ let PluginManager = (() => {
|
|
|
927
1487
|
});
|
|
928
1488
|
continue;
|
|
929
1489
|
}
|
|
1490
|
+
const compatibility = evaluatePluginCompatibility(info, exemptions);
|
|
1491
|
+
if (compatibility !== void 0 && !compatibility.exempted) throw new ManagementFailure("incompatible-version", [incompatiblePlugin(compatibility)]);
|
|
930
1492
|
const dir = resolveBundleDir("dsh", name, this.profile.installAnchor, this.profile.dir);
|
|
931
1493
|
const meta = readPluginMeta(info.name ?? name, pathToFileURL(join(dir, "package.json")).href);
|
|
932
1494
|
bundles.push({
|
|
@@ -1116,7 +1678,9 @@ let PluginManager = (() => {
|
|
|
1116
1678
|
}, "bundle");
|
|
1117
1679
|
}
|
|
1118
1680
|
/**
|
|
1119
|
-
* Install a package using the same pnpm implementation as dsh plugin.
|
|
1681
|
+
* Install a package using the same pnpm implementation as dsh plugin. GitHub
|
|
1682
|
+
* repositories get a connection check bounded by githubConnectionTimeoutMs before pnpm starts;
|
|
1683
|
+
* only network failures or timeouts stop installation, while pnpm owns authentication and transport fallback. A run
|
|
1120
1684
|
* that fails, is cancelled, or adds a package without a bundle patch restores
|
|
1121
1685
|
* `package.json` and `pnpm-lock.yaml` as they were; downloaded files can stay.
|
|
1122
1686
|
* @param spec One package spec, including local paths relative to the invocation directory.
|
|
@@ -1131,7 +1695,7 @@ let PluginManager = (() => {
|
|
|
1131
1695
|
phase: "installing",
|
|
1132
1696
|
result: Promise.resolve(null)
|
|
1133
1697
|
};
|
|
1134
|
-
const stopped = () => control.abort.signal.aborted;
|
|
1698
|
+
const stopped = () => control.abort.signal.aborted || this.abort.signal.aborted;
|
|
1135
1699
|
if (requestId !== void 0) this.installs.set(requestId, control);
|
|
1136
1700
|
const announce = (phase, attempt) => {
|
|
1137
1701
|
if (requestId !== void 0) this.ownerContext.emit("plugin-manager/install-state", {
|
|
@@ -1149,11 +1713,30 @@ let PluginManager = (() => {
|
|
|
1149
1713
|
}
|
|
1150
1714
|
const files = await this.readRestoredFiles();
|
|
1151
1715
|
const before = readProfileManifest("dsh", this.profile.dir).dependencies ?? {};
|
|
1152
|
-
const plan = registryPlan(options?.registry, await this.registries());
|
|
1153
1716
|
let name;
|
|
1154
1717
|
try {
|
|
1155
|
-
let run;
|
|
1156
1718
|
result.registries = [];
|
|
1719
|
+
const connection = checkGithubConnection(parsedForRegistry(spec), this.profile.dir, {
|
|
1720
|
+
timeoutMs: this.githubConnectionTimeoutMs,
|
|
1721
|
+
outputBytes: this.outputBytes,
|
|
1722
|
+
signal: AbortSignal.any([this.abort.signal, control.abort.signal]),
|
|
1723
|
+
...this.profile.packageManager?.env === void 0 ? {} : { env: this.profile.packageManager.env }
|
|
1724
|
+
});
|
|
1725
|
+
this.packageOperations.add(connection);
|
|
1726
|
+
let connectionFailure;
|
|
1727
|
+
try {
|
|
1728
|
+
connectionFailure = await connection;
|
|
1729
|
+
} finally {
|
|
1730
|
+
this.packageOperations.delete(connection);
|
|
1731
|
+
}
|
|
1732
|
+
if (stopped()) throw new InstallCancelledError();
|
|
1733
|
+
if (connectionFailure?.kind === "network" || connectionFailure?.kind === "timeout") {
|
|
1734
|
+
result.packageResult = connectionFailure;
|
|
1735
|
+
result.failedAt = "spec-host";
|
|
1736
|
+
throw new Error(connectionFailure.output);
|
|
1737
|
+
}
|
|
1738
|
+
const plan = registryPlan(options?.registry, await this.registries());
|
|
1739
|
+
let run;
|
|
1157
1740
|
for (const [index, registry] of plan.entries()) {
|
|
1158
1741
|
if (index > 0) await this.restoreFiles(files);
|
|
1159
1742
|
if (stopped()) throw new InstallCancelledError();
|
|
@@ -1170,17 +1753,21 @@ let PluginManager = (() => {
|
|
|
1170
1753
|
], control.abort.signal, requestId);
|
|
1171
1754
|
result.packageResult = run;
|
|
1172
1755
|
if (stopped()) throw new InstallCancelledError();
|
|
1173
|
-
|
|
1174
|
-
if (run.exitCode === 0
|
|
1175
|
-
|
|
1756
|
+
if (run.incompatible !== void 0) throw new ManagementFailure("incompatible-version", run.incompatible);
|
|
1757
|
+
if (run.exitCode === 0 && run.timedOut !== true) break;
|
|
1758
|
+
/* v8 ignore next 2 -- runPnpm classifies every run it does not report as succeeded */
|
|
1759
|
+
if (run.kind === void 0) break;
|
|
1176
1760
|
delete result.failedAt;
|
|
1761
|
+
if (run.timedOut === true) break;
|
|
1762
|
+
const failedAt = attributeFailure(run.kind, run.output, parsedForRegistry(spec));
|
|
1177
1763
|
if (failedAt !== "other") result.failedAt = failedAt;
|
|
1178
1764
|
if (failedAt !== "registry" || index === plan.length - 1) break;
|
|
1179
1765
|
}
|
|
1180
1766
|
/* v8 ignore next -- the plan is never empty, so a run always settled */
|
|
1181
1767
|
if (run === void 0) throw new Error("no registry was asked");
|
|
1182
|
-
|
|
1183
|
-
if (
|
|
1768
|
+
const succeeded = run.exitCode === 0 && run.timedOut !== true;
|
|
1769
|
+
if (succeeded) delete result.failedAt;
|
|
1770
|
+
if (!succeeded) {
|
|
1184
1771
|
try {
|
|
1185
1772
|
result.pendingBuilds = await readPendingBuilds(this.profile.dir);
|
|
1186
1773
|
} catch (error) {
|
|
@@ -1197,6 +1784,8 @@ let PluginManager = (() => {
|
|
|
1197
1784
|
const dir = resolveBundleDir("dsh", name, this.profile.installAnchor, this.profile.dir);
|
|
1198
1785
|
const manifest = bundleManifest(name, this.profile.dir, this.profile.installAnchor);
|
|
1199
1786
|
if (manifest?.dsh?.bundle === void 0) throw new ManagementFailure("not-bundle");
|
|
1787
|
+
const compatibility = evaluatePluginCompatibility(manifest, readProfileVersionExemptions(this.profile.dir));
|
|
1788
|
+
if (compatibility !== void 0 && !compatibility.exempted) throw new ManagementFailure("incompatible-version", [incompatiblePlugin(compatibility)]);
|
|
1200
1789
|
for (const file of bundlePatchPaths(dir, manifest.dsh.bundle)) loadOverlayPatches("dsh", file);
|
|
1201
1790
|
} catch (error) {
|
|
1202
1791
|
await this.restoreFiles(files);
|
|
@@ -1232,7 +1821,7 @@ let PluginManager = (() => {
|
|
|
1232
1821
|
}
|
|
1233
1822
|
/** Stop an installation this manager owns and wait until its files are back.
|
|
1234
1823
|
* @param requestId The id the installation was started with.
|
|
1235
|
-
* @returns `cancelled` once pnpm exited and the files are restored, `too-late` once the bundle is being
|
|
1824
|
+
* @returns `cancelled` once the Git check or pnpm exited and the files are restored, `too-late` once the bundle is being
|
|
1236
1825
|
* applied, `not-running` for any other id.
|
|
1237
1826
|
*/
|
|
1238
1827
|
async cancelInstall(requestId) {
|
|
@@ -1266,7 +1855,7 @@ let PluginManager = (() => {
|
|
|
1266
1855
|
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");
|
|
1267
1856
|
});
|
|
1268
1857
|
result.packageResult = await this.runPnpm(["remove", name]);
|
|
1269
|
-
if (result.packageResult.exitCode !== 0) throw new Error(result.packageResult.output);
|
|
1858
|
+
if (result.packageResult.exitCode !== 0 || result.packageResult.timedOut === true) throw new Error(result.packageResult.output);
|
|
1270
1859
|
}, {
|
|
1271
1860
|
stage: "remove",
|
|
1272
1861
|
target: name
|
|
@@ -1325,6 +1914,8 @@ let PluginManager = (() => {
|
|
|
1325
1914
|
signal: signal === void 0 ? this.abort.signal : AbortSignal.any([this.abort.signal, signal]),
|
|
1326
1915
|
outputBytes: this.outputBytes,
|
|
1327
1916
|
activateNewBundles: false,
|
|
1917
|
+
idleTimeoutMs: this.idleTimeoutMs,
|
|
1918
|
+
lookupTimeoutMs: this.inspectTimeoutMs,
|
|
1328
1919
|
onOutput: (text, stream) => {
|
|
1329
1920
|
this.ownerContext.emit("plugin-manager/install-log", {
|
|
1330
1921
|
...identity,
|
|
@@ -1348,9 +1939,13 @@ let PluginManager = (() => {
|
|
|
1348
1939
|
text: "",
|
|
1349
1940
|
exitCode: signal?.aborted === true ? null : result.exitCode
|
|
1350
1941
|
});
|
|
1351
|
-
|
|
1942
|
+
if (result.exitCode === 0 && result.timedOut !== true) return result;
|
|
1943
|
+
return {
|
|
1352
1944
|
...result,
|
|
1353
|
-
kind: classifyInstallFailure({
|
|
1945
|
+
kind: classifyInstallFailure({
|
|
1946
|
+
log: result.output,
|
|
1947
|
+
...result.timedOut === true ? { timedOut: true } : {}
|
|
1948
|
+
})
|
|
1354
1949
|
};
|
|
1355
1950
|
} catch (error) {
|
|
1356
1951
|
this.ownerContext.emit("plugin-manager/install-log", {
|
|
@@ -1384,8 +1979,15 @@ let PluginManager = (() => {
|
|
|
1384
1979
|
async selectBundle(name, enabled) {
|
|
1385
1980
|
const manifest = readProfileManifest("dsh", this.profile.dir);
|
|
1386
1981
|
const previous = manifest.dsh?.profile?.bundles ?? [];
|
|
1387
|
-
if (
|
|
1388
|
-
|
|
1982
|
+
if (enabled || !previous.includes(name)) {
|
|
1983
|
+
const metadata = bundleManifest(name, this.profile.dir, this.profile.installAnchor);
|
|
1984
|
+
if (metadata === void 0) throw new ManagementFailure("not-bundle");
|
|
1985
|
+
if (enabled) {
|
|
1986
|
+
const compatibility = evaluatePluginCompatibility(metadata, readProfileVersionExemptions(this.profile.dir));
|
|
1987
|
+
if (compatibility !== void 0 && !compatibility.exempted) throw new ManagementFailure("incompatible-version", [incompatiblePlugin(compatibility)]);
|
|
1988
|
+
this.bundleRows(name);
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1389
1991
|
if (!enabled && previous.includes(name)) {
|
|
1390
1992
|
if (this.protectsManager(name)) throw new ManagementFailure("management-required");
|
|
1391
1993
|
}
|
|
@@ -1457,7 +2059,8 @@ let PluginManager = (() => {
|
|
|
1457
2059
|
return [
|
|
1458
2060
|
"package.json",
|
|
1459
2061
|
"cordis.patch.yml",
|
|
1460
|
-
"pnpm-workspace.yaml"
|
|
2062
|
+
"pnpm-workspace.yaml",
|
|
2063
|
+
PROFILE_COMPATIBILITY_FILENAME
|
|
1461
2064
|
].map((file) => {
|
|
1462
2065
|
try {
|
|
1463
2066
|
return readFileSync(join(this.profile.dir, file), "utf8");
|