@deepseek-ai/dsh-plugin-manager 0.1.7-alpha.1 → 0.1.7-rc.1
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 +2 -2
- package/README.md +19 -2
- package/README.zh.md +19 -2
- package/lib/index.js +691 -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 +113 -23
- package/lib/types/install-failure.js +1 -0
- package/lib/types/operations.d.ts +15 -1
- package/lib/types/operations.js +341 -21
- package/lib/types/registry.d.ts +2 -0
- package/lib/types/registry.js +2 -0
- package/lib/types/run-tree.d.ts +37 -0
- package/lib/types/run-tree.js +51 -0
- package/lib/types/tools.js +13 -3
- package/lib/types/types.d.ts +17 -3
- package/package.json +33 -33
package/lib/index.js
CHANGED
|
@@ -7,11 +7,200 @@ 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
|
+
* Wait until a terminated run's tree is gone, so the caller restores and
|
|
144
|
+
* unlocks the profile only after the scripts it started stopped writing.
|
|
145
|
+
* @param tree The run's process id and whether it leads its own group.
|
|
146
|
+
* @param internals Injectable process operations.
|
|
147
|
+
* @returns Fulfillment once no member remains, or the wait bound elapsed.
|
|
148
|
+
*/
|
|
149
|
+
async function awaitTreeGone(tree, internals = {}) {
|
|
150
|
+
const pid = tree.pid;
|
|
151
|
+
if (pid === void 0) return;
|
|
152
|
+
const platform = internals.platform ?? process.platform;
|
|
153
|
+
const alive = internals.alive ?? ((target) => {
|
|
154
|
+
process.kill(target, 0);
|
|
155
|
+
return true;
|
|
156
|
+
});
|
|
157
|
+
const remaining = () => {
|
|
158
|
+
try {
|
|
159
|
+
return alive(targetOf(pid, tree.grouped, platform));
|
|
160
|
+
} catch {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
const deadline = Date.now() + (internals.waitMs ?? TREE_WAIT_MS);
|
|
165
|
+
while (remaining()) {
|
|
166
|
+
if (Date.now() >= deadline) return;
|
|
167
|
+
await new Promise((resolve) => setTimeout(resolve, TREE_POLL_MS));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
//#endregion
|
|
171
|
+
//#region lib/types/failure.js
|
|
172
|
+
/** Expected management rejection; presentation belongs to the caller's locale. */
|
|
173
|
+
var ManagementFailure = class extends Error {
|
|
174
|
+
/** Code rendered by the caller's locale dictionary. */
|
|
175
|
+
code;
|
|
176
|
+
/** The packages an `incompatible-version` rejection names. */
|
|
177
|
+
incompatible;
|
|
178
|
+
/**
|
|
179
|
+
* @param code Localizable management rejection.
|
|
180
|
+
* @param incompatible Packages the running DSH version rejects, for `incompatible-version`.
|
|
181
|
+
*/
|
|
182
|
+
constructor(code, incompatible) {
|
|
183
|
+
super(code);
|
|
184
|
+
this.code = code;
|
|
185
|
+
this.incompatible = incompatible;
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
/**
|
|
189
|
+
* Drop the exemption status from an unexempted compatibility result.
|
|
190
|
+
* @param issue Result whose exemption is not active.
|
|
191
|
+
* @returns The package, runtime, and rejected peer ranges.
|
|
192
|
+
*/
|
|
193
|
+
function incompatiblePlugin(issue) {
|
|
194
|
+
return {
|
|
195
|
+
name: issue.name,
|
|
196
|
+
version: issue.version,
|
|
197
|
+
runtimeVersion: issue.runtimeVersion,
|
|
198
|
+
peers: issue.peers
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
//#endregion
|
|
14
202
|
//#region lib/types/operations.js
|
|
203
|
+
/** Shared profile package operations used by dsh plugin and the running manager. */
|
|
15
204
|
/** Resolve relative package specs against the caller's directory.
|
|
16
205
|
* @param argument One pnpm argument.
|
|
17
206
|
* @param cwd Invocation directory, never the profile directory.
|
|
@@ -69,15 +258,148 @@ async function reconcile(before, dir, anchor, options) {
|
|
|
69
258
|
};
|
|
70
259
|
await saveManifest(dir, after);
|
|
71
260
|
}
|
|
261
|
+
/**
|
|
262
|
+
* How long the pipes keep draining after their process exited, as a fixed part of
|
|
263
|
+
* finishing a run rather than a deployment knob: a descendant that inherited them
|
|
264
|
+
* holds them open, and the tail a failure classification reads is written by then.
|
|
265
|
+
*/
|
|
266
|
+
const DRAIN_AFTER_EXIT_MS = 2e3;
|
|
267
|
+
/** Whether every collector finished within `ms`.
|
|
268
|
+
* @param collectors The pipe readers racing the bound.
|
|
269
|
+
* @param ms The longest wait, in milliseconds.
|
|
270
|
+
* @returns True when all collectors settled in time.
|
|
271
|
+
*/
|
|
272
|
+
async function drainWithin(collectors, ms) {
|
|
273
|
+
if (collectors.length === 0) return true;
|
|
274
|
+
let timer;
|
|
275
|
+
try {
|
|
276
|
+
return await Promise.race([Promise.allSettled(collectors).then(() => true), new Promise((resolve) => {
|
|
277
|
+
timer = setTimeout(() => {
|
|
278
|
+
resolve(false);
|
|
279
|
+
}, ms);
|
|
280
|
+
})]);
|
|
281
|
+
} finally {
|
|
282
|
+
clearTimeout(timer);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
/** Install commands that take the packages to install as positionals. */
|
|
286
|
+
const INSTALL_COMMANDS = new Set([
|
|
287
|
+
"add",
|
|
288
|
+
"install",
|
|
289
|
+
"i"
|
|
290
|
+
]);
|
|
291
|
+
/** Bound on a pre-install registry lookup when the caller names none. */
|
|
292
|
+
const LOOKUP_TIMEOUT_MS = 2e4;
|
|
293
|
+
/** Package specs an install command names explicitly, in order. */
|
|
294
|
+
function namedSpecs(args) {
|
|
295
|
+
const index = args.findIndex((argument) => !argument.startsWith("-"));
|
|
296
|
+
const command = index < 0 ? void 0 : args[index];
|
|
297
|
+
if (command === void 0 || !INSTALL_COMMANDS.has(command)) return [];
|
|
298
|
+
return args.slice(index + 1).filter((argument) => !argument.startsWith("-"));
|
|
299
|
+
}
|
|
300
|
+
/** The manifest a named spec would install, read without installing it.
|
|
301
|
+
* A path spec is read from disk. A registry spec asks pnpm's own configuration for the version the
|
|
302
|
+
* range selects and its peer requirements. A git or tarball spec needs the fetch itself, so the
|
|
303
|
+
* check after installation is what judges it.
|
|
304
|
+
* @param dir Profile directory the lookup runs in.
|
|
305
|
+
* @param spec Anchored install spec.
|
|
306
|
+
* @param options Pnpm executable, prefix arguments, the caller's bound and signal.
|
|
307
|
+
* @param environment Environment of the caller's pnpm invocations.
|
|
308
|
+
* @param flags Flags of the run itself, so the lookup asks the registry that run will use.
|
|
309
|
+
* @returns The package manifest, or undefined when reading it would need the installation itself.
|
|
310
|
+
*/
|
|
311
|
+
async function namedSpecManifest(dir, spec, options, environment, flags) {
|
|
312
|
+
const parsed = parseInstallSpec(spec);
|
|
313
|
+
if (parsed.kind === "path") {
|
|
314
|
+
const filename = join(parsed.path, "package.json");
|
|
315
|
+
return existsSync(filename) ? JSON.parse(readFileSync(filename, "utf8")) : void 0;
|
|
316
|
+
}
|
|
317
|
+
if (parsed.kind !== "registry") return void 0;
|
|
318
|
+
const viewed = await execa(options.command ?? "pnpm", [
|
|
319
|
+
...options.args ?? [],
|
|
320
|
+
"view",
|
|
321
|
+
parsed.spec,
|
|
322
|
+
"name",
|
|
323
|
+
"version",
|
|
324
|
+
"peerDependencies",
|
|
325
|
+
"--json",
|
|
326
|
+
...flags,
|
|
327
|
+
"--config.fetch-retries=0"
|
|
328
|
+
], {
|
|
329
|
+
cwd: dir,
|
|
330
|
+
env: environment,
|
|
331
|
+
extendEnv: false,
|
|
332
|
+
reject: false,
|
|
333
|
+
stdin: "ignore",
|
|
334
|
+
...options.signal === void 0 ? {} : { cancelSignal: options.signal },
|
|
335
|
+
timeout: options.lookupTimeoutMs ?? LOOKUP_TIMEOUT_MS
|
|
336
|
+
});
|
|
337
|
+
if (viewed.exitCode !== 0) return void 0;
|
|
338
|
+
const value = JSON.parse(viewed.stdout);
|
|
339
|
+
return Array.isArray(value) ? value.at(-1) : value;
|
|
340
|
+
}
|
|
341
|
+
/** Missing installed packages are repairable; their absence is part of the before/after comparison. */
|
|
342
|
+
function optionalFile(path) {
|
|
343
|
+
try {
|
|
344
|
+
return readFileSync(path, "utf8");
|
|
345
|
+
} catch (error) {
|
|
346
|
+
if (error.code === "ENOENT") return void 0;
|
|
347
|
+
throw error;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
/** pnpm can install plugins through any direct-dependency field. */
|
|
351
|
+
function directDependencies(manifest) {
|
|
352
|
+
const extra = manifest;
|
|
353
|
+
return {
|
|
354
|
+
...extra.devDependencies,
|
|
355
|
+
...manifest.dependencies,
|
|
356
|
+
...extra.optionalDependencies
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
/** Inspect only plugin rows contributed by the changed bundle, not its dependency closure. */
|
|
360
|
+
function bundleComponentManifests(manifest, dir, anchor) {
|
|
361
|
+
const bundle = manifest.dsh?.bundle;
|
|
362
|
+
if (bundle === void 0) return [];
|
|
363
|
+
const patches = bundlePatchPaths(dir, bundle).flatMap((file) => loadOverlayPatches("dsh", file));
|
|
364
|
+
const names = /* @__PURE__ */ new Set();
|
|
365
|
+
const visit = (rows) => {
|
|
366
|
+
for (const row of rows) {
|
|
367
|
+
if (row.group && Array.isArray(row.config)) visit(row.config);
|
|
368
|
+
if (typeof row.name !== "string" || row.name.startsWith(".") || row.name.startsWith("/") || row.name.includes(":")) continue;
|
|
369
|
+
const parts = row.name.split("/");
|
|
370
|
+
names.add(parts.slice(0, row.name.startsWith("@") ? 2 : 1).join("/"));
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
visit(composeEntries([patches.filter((patch) => patch.insert !== void 0)]));
|
|
374
|
+
return [...names].flatMap((name) => {
|
|
375
|
+
let packageDir;
|
|
376
|
+
try {
|
|
377
|
+
packageDir = resolveBundleDir("dsh", name, anchor, dir);
|
|
378
|
+
} catch (error) {
|
|
379
|
+
return [];
|
|
380
|
+
}
|
|
381
|
+
return [readProfileManifest("dsh", packageDir)];
|
|
382
|
+
});
|
|
383
|
+
}
|
|
72
384
|
/** Execute pnpm inside a profile whose caller already holds the profile write lock.
|
|
385
|
+
* Newly installed or updated direct dependencies are checked even when activation is disabled;
|
|
386
|
+
* an untouched dependency never blocks an unrelated operation and stays denied at startup.
|
|
387
|
+
* Compatibility denial restores the profile manifest and lockfile, but leaves downloaded modules on disk.
|
|
73
388
|
* @param context Launcher-owned profile and resolution locations.
|
|
74
389
|
* @param args Pnpm arguments, before relative path anchoring.
|
|
75
390
|
* @param options Output, activation and cancellation policy.
|
|
76
|
-
* @returns Exit status
|
|
391
|
+
* @returns Exit status, whether the silence bound stopped the run, and the diagnostic path.
|
|
392
|
+
* A compatibility denial returns exit code 1. Service output is bounded; CLI output uses inherited descriptors.
|
|
77
393
|
*/
|
|
78
394
|
async function runProfilePnpm(context, args, options) {
|
|
79
395
|
const dir = context.dir ?? resolveProfileDir(context.profile, context.home);
|
|
80
396
|
const before = readProfileManifest("dsh", dir);
|
|
397
|
+
const savedFiles = ["package.json", "pnpm-lock.yaml"].map((name) => ({
|
|
398
|
+
path: join(dir, name),
|
|
399
|
+
text: optionalFile(join(dir, name))
|
|
400
|
+
}));
|
|
401
|
+
const beforeDependencies = directDependencies(before);
|
|
402
|
+
const installedBefore = new Map(Object.keys(beforeDependencies).map((name) => [name, optionalFile(join(dir, "node_modules", name, "package.json"))]));
|
|
81
403
|
const logRoot = join(dir, ".plugin-manager", "logs");
|
|
82
404
|
await mkdir(logRoot, {
|
|
83
405
|
recursive: true,
|
|
@@ -87,68 +409,210 @@ async function runProfilePnpm(context, args, options) {
|
|
|
87
409
|
const log = await open(logPath, "wx", 384);
|
|
88
410
|
let output = Buffer.alloc(0);
|
|
89
411
|
let truncated = false;
|
|
412
|
+
const append = (bytes) => {
|
|
413
|
+
output = Buffer.concat([output, bytes]);
|
|
414
|
+
if (output.length > options.outputBytes) {
|
|
415
|
+
truncated = true;
|
|
416
|
+
output = output.subarray(output.length - options.outputBytes);
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
const environment = {
|
|
420
|
+
...options.execution === "cli" ? process.env : scrubbedParentEnv(),
|
|
421
|
+
...options.env
|
|
422
|
+
};
|
|
423
|
+
const restore = async () => {
|
|
424
|
+
for (const file of savedFiles) if (file.text === void 0) await rm(file.path, { force: true });
|
|
425
|
+
else await writeFileAtomic(file.path, file.text, { mode: 384 });
|
|
426
|
+
};
|
|
427
|
+
/** Packages a compatibility check refused; callers render them for their own surface. */
|
|
428
|
+
const incompatible = [];
|
|
429
|
+
const rejected = async (warnings, restoration) => {
|
|
430
|
+
const diagnostic = `\ndsh: installation rejected: ${warnings.join("\n")}\ndsh: ${restoration}.\n`;
|
|
431
|
+
await log.write(diagnostic);
|
|
432
|
+
options.onOutput?.(diagnostic, "stderr");
|
|
433
|
+
append(Buffer.from(diagnostic));
|
|
434
|
+
await log.close();
|
|
435
|
+
return {
|
|
436
|
+
exitCode: 1,
|
|
437
|
+
output: output.toString("utf8"),
|
|
438
|
+
truncated,
|
|
439
|
+
logPath,
|
|
440
|
+
incompatible
|
|
441
|
+
};
|
|
442
|
+
};
|
|
443
|
+
const preflight = [];
|
|
444
|
+
const exemptions = readProfileVersionExemptions(dir);
|
|
445
|
+
const registryFlags = args.filter((argument) => argument.startsWith("--registry="));
|
|
446
|
+
for (const raw of namedSpecs(args)) try {
|
|
447
|
+
const manifest = await namedSpecManifest(dir, anchorPathSpec(raw, context.cwd), options, environment, registryFlags);
|
|
448
|
+
if (manifest === void 0) continue;
|
|
449
|
+
const issue = evaluatePluginCompatibility(manifest, exemptions);
|
|
450
|
+
if (issue !== void 0 && !issue.exempted) {
|
|
451
|
+
preflight.push(pluginCompatibilityWarning(issue));
|
|
452
|
+
incompatible.push(incompatiblePlugin(issue));
|
|
453
|
+
}
|
|
454
|
+
} catch (error) {
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
if (preflight.length > 0) return rejected(preflight, "nothing was installed");
|
|
90
458
|
const cancellation = new AbortController();
|
|
459
|
+
const grouped = leadsOwnGroup(options.execution);
|
|
91
460
|
const child = execa(options.command ?? "pnpm", [...options.args ?? [], ...args.map((arg) => anchorPathSpec(arg, context.cwd))], {
|
|
92
461
|
cwd: dir,
|
|
93
|
-
env:
|
|
94
|
-
...options.execution === "cli" ? process.env : scrubbedParentEnv(),
|
|
95
|
-
...options.env
|
|
96
|
-
},
|
|
462
|
+
env: environment,
|
|
97
463
|
extendEnv: false,
|
|
98
464
|
reject: false,
|
|
99
465
|
stdout: options.execution === "cli" ? "inherit" : "pipe",
|
|
100
466
|
stderr: options.execution === "cli" ? "inherit" : "pipe",
|
|
467
|
+
killDescendants: options.execution === "service",
|
|
101
468
|
buffer: false,
|
|
102
469
|
stdin: options.execution === "cli" ? "inherit" : "ignore",
|
|
103
470
|
cancelSignal: options.signal === void 0 ? cancellation.signal : AbortSignal.any([cancellation.signal, options.signal])
|
|
104
471
|
});
|
|
105
472
|
let writes = Promise.resolve();
|
|
473
|
+
/** `settled` records that the process outcome is known; `stalled` that the silence bound stopped the run. */
|
|
474
|
+
const control = {
|
|
475
|
+
settled: false,
|
|
476
|
+
stalled: false
|
|
477
|
+
};
|
|
478
|
+
/** Set once this call cuts the reading short itself, so the close it causes is not read as a run failure. */
|
|
479
|
+
let cut = false;
|
|
480
|
+
/** The first failure a reading hit before that cut, which the run still reports. */
|
|
481
|
+
let failure;
|
|
482
|
+
let idleTimer;
|
|
483
|
+
/** The silence bound: a captured run that stops printing without exiting is terminated, never awaited. */
|
|
484
|
+
const armIdle = () => {
|
|
485
|
+
if (control.settled || options.idleTimeoutMs === void 0) return;
|
|
486
|
+
clearTimeout(idleTimer);
|
|
487
|
+
idleTimer = setTimeout(() => {
|
|
488
|
+
control.stalled = true;
|
|
489
|
+
child.kill();
|
|
490
|
+
}, options.idleTimeoutMs);
|
|
491
|
+
};
|
|
106
492
|
const collect = async (stream, kind) => {
|
|
107
493
|
try {
|
|
108
494
|
for await (const chunk of stream) {
|
|
495
|
+
armIdle();
|
|
109
496
|
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
110
497
|
writes = writes.then(async () => {
|
|
111
498
|
await log.write(bytes);
|
|
112
499
|
});
|
|
113
500
|
await writes;
|
|
114
501
|
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
|
-
}
|
|
502
|
+
append(bytes);
|
|
120
503
|
}
|
|
121
504
|
} catch (error) {
|
|
122
|
-
|
|
505
|
+
if (!cut) {
|
|
506
|
+
failure ??= error instanceof Error ? error : new Error(String(error));
|
|
507
|
+
cancellation.abort();
|
|
508
|
+
}
|
|
123
509
|
throw error;
|
|
124
510
|
}
|
|
125
511
|
};
|
|
512
|
+
const collectors = [...child.stdout === null ? [] : [collect(child.stdout, "stdout")], ...child.stderr === null ? [] : [collect(child.stderr, "stderr")]];
|
|
513
|
+
for (const collector of collectors) collector.catch(() => {});
|
|
514
|
+
if (collectors.length > 0) armIdle();
|
|
126
515
|
let exitCode;
|
|
127
516
|
try {
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
517
|
+
const settled = Promise.allSettled([child]);
|
|
518
|
+
await Promise.race([once(child.nodeChildProcess, "exit").catch(() => void 0), settled]);
|
|
519
|
+
control.settled = true;
|
|
520
|
+
clearTimeout(idleTimer);
|
|
521
|
+
if (control.stalled) await awaitTreeGone({
|
|
522
|
+
pid: child.pid,
|
|
523
|
+
grouped
|
|
524
|
+
});
|
|
525
|
+
if (await drainWithin(collectors, DRAIN_AFTER_EXIT_MS)) {
|
|
526
|
+
for (const stream of await Promise.allSettled(collectors)) if (stream.status === "rejected") throw stream.reason;
|
|
527
|
+
} else {
|
|
528
|
+
cut = true;
|
|
529
|
+
child.stdout?.destroy();
|
|
530
|
+
child.stderr?.destroy();
|
|
531
|
+
const notice = "dsh: pnpm output was cut short after its process exited\n";
|
|
532
|
+
await log.write(notice);
|
|
533
|
+
if (failure !== void 0) throw failure;
|
|
534
|
+
options.onOutput?.(notice, "stderr");
|
|
535
|
+
append(Buffer.from(notice));
|
|
536
|
+
}
|
|
537
|
+
const [completion] = await settled;
|
|
134
538
|
if (completion.status === "rejected") throw completion.reason;
|
|
135
539
|
const result = completion.value;
|
|
136
540
|
exitCode = result.exitCode ?? (result.code === "ENOENT" ? 127 : 1);
|
|
541
|
+
if (control.stalled) {
|
|
542
|
+
const notice = `dsh: pnpm printed nothing for ${String(options.idleTimeoutMs)}ms and was terminated\n`;
|
|
543
|
+
await log.write(notice);
|
|
544
|
+
options.onOutput?.(notice, "stderr");
|
|
545
|
+
append(Buffer.from(notice));
|
|
546
|
+
}
|
|
137
547
|
if (result.failed && output.length === 0) {
|
|
138
548
|
const diagnostic = result.shortMessage ?? "pnpm failed";
|
|
139
549
|
await log.write(diagnostic);
|
|
140
550
|
truncated = Buffer.byteLength(diagnostic) > options.outputBytes;
|
|
141
551
|
output = Buffer.from(diagnostic).subarray(0, options.outputBytes);
|
|
142
552
|
}
|
|
143
|
-
if (exitCode === 0 &&
|
|
553
|
+
if (exitCode === 0 && !control.stalled) {
|
|
554
|
+
const after = readProfileManifest("dsh", dir);
|
|
555
|
+
const warnings = [];
|
|
556
|
+
for (const [name, spec] of Object.entries(directDependencies(after))) {
|
|
557
|
+
const packageDir = join(dir, "node_modules", name);
|
|
558
|
+
const installed = optionalFile(join(packageDir, "package.json"));
|
|
559
|
+
if (installed === void 0) continue;
|
|
560
|
+
const untouched = beforeDependencies[name] === spec && installedBefore.get(name) === installed;
|
|
561
|
+
const found = [];
|
|
562
|
+
const issues = [];
|
|
563
|
+
try {
|
|
564
|
+
const manifest = readProfileManifest("dsh", packageDir);
|
|
565
|
+
for (const candidate of [manifest, ...bundleComponentManifests(manifest, packageDir, context.installAnchor)]) {
|
|
566
|
+
const issue = evaluatePluginCompatibility(candidate, readProfileVersionExemptions(dir));
|
|
567
|
+
if (issue !== void 0 && !issue.exempted) {
|
|
568
|
+
found.push(pluginCompatibilityWarning(issue));
|
|
569
|
+
issues.push(incompatiblePlugin(issue));
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
} catch (error) {
|
|
573
|
+
found.push(`Cannot validate installed package ${name}: ${String(error)}`);
|
|
574
|
+
}
|
|
575
|
+
if (found.length === 0) continue;
|
|
576
|
+
if (!untouched) {
|
|
577
|
+
warnings.push(...found);
|
|
578
|
+
incompatible.push(...issues);
|
|
579
|
+
} else {
|
|
580
|
+
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`;
|
|
581
|
+
await log.write(notice);
|
|
582
|
+
options.onOutput?.(notice, "stderr");
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
if (warnings.length > 0) {
|
|
586
|
+
await restore();
|
|
587
|
+
const repair = ["install", savedFiles.some((file) => file.path.endsWith("pnpm-lock.yaml") && file.text !== void 0) ? "--frozen-lockfile" : "--config.lockfile=false"];
|
|
588
|
+
const repaired = await execa(options.command ?? "pnpm", [...options.args ?? [], ...repair], {
|
|
589
|
+
cwd: dir,
|
|
590
|
+
env: environment,
|
|
591
|
+
extendEnv: false,
|
|
592
|
+
reject: false,
|
|
593
|
+
stdin: "ignore",
|
|
594
|
+
...options.idleTimeoutMs === void 0 ? {} : { timeout: options.idleTimeoutMs }
|
|
595
|
+
});
|
|
596
|
+
exitCode = 1;
|
|
597
|
+
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'";
|
|
598
|
+
const diagnostic = `\ndsh: installation rejected: ${warnings.join("\n")}\ndsh: ${restoration}.\n`;
|
|
599
|
+
await log.write(diagnostic);
|
|
600
|
+
options.onOutput?.(diagnostic, "stderr");
|
|
601
|
+
append(Buffer.from(diagnostic));
|
|
602
|
+
} else if (options.activateNewBundles !== false) await reconcile(before, dir, context.installAnchor, options);
|
|
603
|
+
}
|
|
144
604
|
} finally {
|
|
605
|
+
control.settled = true;
|
|
606
|
+
clearTimeout(idleTimer);
|
|
145
607
|
await log.close();
|
|
146
608
|
}
|
|
147
609
|
return {
|
|
148
610
|
exitCode,
|
|
149
611
|
output: output.toString("utf8"),
|
|
150
612
|
truncated,
|
|
151
|
-
logPath
|
|
613
|
+
logPath,
|
|
614
|
+
...control.stalled ? { timedOut: true } : {},
|
|
615
|
+
...incompatible.length > 0 ? { incompatible } : {}
|
|
152
616
|
};
|
|
153
617
|
}
|
|
154
618
|
/**
|
|
@@ -246,6 +710,7 @@ const LOG_KINDS = [
|
|
|
246
710
|
["disk-full", /\bENOSPC\b|no space left on device/i],
|
|
247
711
|
["permission", /\bEACCES\b|\bEPERM\b|permission denied/i],
|
|
248
712
|
["integrity", /ERR_PNPM_TARBALL_INTEGRITY|ERR_PNPM_BAD_TARBALL_SIZE|\bEINTEGRITY\b/],
|
|
713
|
+
["unknown", /The requested URL returned error: 40[134]\b/],
|
|
249
714
|
["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
715
|
];
|
|
251
716
|
/**
|
|
@@ -259,110 +724,8 @@ function classifyInstallFailure(facts) {
|
|
|
259
724
|
for (const [kind, pattern] of LOG_KINDS) if (pattern.test(facts.log)) return kind;
|
|
260
725
|
return "unknown";
|
|
261
726
|
}
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
}
|
|
727
|
+
/** Public npmmirror URL shared by the fallback configuration and public-registry comparison. */
|
|
728
|
+
const NPMMIRROR_REGISTRY = "https://registry.npmmirror.com/";
|
|
366
729
|
/**
|
|
367
730
|
* Parse a registry URL into the form pnpm compares registries in: lower-case host, trailing slash.
|
|
368
731
|
* @param url - the registry as configured or requested.
|
|
@@ -480,18 +843,6 @@ async function writePluginEnabled(filename, id, name, enabled) {
|
|
|
480
843
|
return true;
|
|
481
844
|
}
|
|
482
845
|
//#endregion
|
|
483
|
-
//#region lib/types/failure.js
|
|
484
|
-
/** Expected management rejection; presentation belongs to the caller's locale. */
|
|
485
|
-
var ManagementFailure = class extends Error {
|
|
486
|
-
/** Code rendered by the caller's locale dictionary. */
|
|
487
|
-
code;
|
|
488
|
-
/** @param code Localizable management rejection. */
|
|
489
|
-
constructor(code) {
|
|
490
|
-
super(code);
|
|
491
|
-
this.code = code;
|
|
492
|
-
}
|
|
493
|
-
};
|
|
494
|
-
//#endregion
|
|
495
846
|
//#region lib/types/build-approval.js
|
|
496
847
|
/** Approve pnpm's pending dependency scripts in the current profile's workspace settings. */
|
|
497
848
|
async function readPolicy(dir) {
|
|
@@ -535,6 +886,84 @@ async function approveBuilds(dir, names) {
|
|
|
535
886
|
await writeFileAtomic(join(dir, "pnpm-workspace.yaml"), String(document), { mode: 384 });
|
|
536
887
|
}
|
|
537
888
|
//#endregion
|
|
889
|
+
//#region lib/types/github-connection.js
|
|
890
|
+
/** Bounded GitHub repository checks using the installer's Git configuration and environment. */
|
|
891
|
+
/**
|
|
892
|
+
* Check a GitHub repository before pnpm starts, without downloading or building its package.
|
|
893
|
+
* Git reads the profile's Git and proxy configuration without invoking credential helpers or prompting.
|
|
894
|
+
* Timeout and cancellation terminate its descendants too; pnpm owns authentication and transport fallback.
|
|
895
|
+
* @param spec The parsed installation address; npm packages, local paths and other hosts are not checked.
|
|
896
|
+
* @param dir The profile directory where installation runs.
|
|
897
|
+
* @param options The connection deadline, output bound and operation cancellation.
|
|
898
|
+
* @returns A failed check with bounded output and a complete log at logPath, or undefined for a reachable repository or an unhandled spec.
|
|
899
|
+
*/
|
|
900
|
+
async function checkGithubConnection(spec, dir, options) {
|
|
901
|
+
if (spec.kind !== "git") return void 0;
|
|
902
|
+
const host = spec.host.toLowerCase().replace(/:.*$/, "");
|
|
903
|
+
if (host !== "github.com" && !host.endsWith(".github.com")) return void 0;
|
|
904
|
+
const repository = spec.spec.replace(/#.*$/s, "").replace(/^github:/i, "https://github.com/").replace(/^gist:/i, "https://gist.github.com/").replace(/^git\+/i, "");
|
|
905
|
+
const logRoot = join(dir, ".plugin-manager", "logs");
|
|
906
|
+
await mkdir(logRoot, {
|
|
907
|
+
recursive: true,
|
|
908
|
+
mode: 448
|
|
909
|
+
});
|
|
910
|
+
const logPath = join(await mkdtemp(join(logRoot, "github-connection-")), "git.log");
|
|
911
|
+
const log = await open(logPath, "ax+", 384);
|
|
912
|
+
try {
|
|
913
|
+
const result = await execa("git", [
|
|
914
|
+
"-c",
|
|
915
|
+
"credential.helper=",
|
|
916
|
+
"ls-remote",
|
|
917
|
+
"--",
|
|
918
|
+
repository,
|
|
919
|
+
"HEAD"
|
|
920
|
+
], {
|
|
921
|
+
cwd: dir,
|
|
922
|
+
env: {
|
|
923
|
+
...scrubbedParentEnv(),
|
|
924
|
+
...options.env,
|
|
925
|
+
LC_ALL: "C",
|
|
926
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
927
|
+
GIT_ASKPASS: "",
|
|
928
|
+
SSH_ASKPASS: "",
|
|
929
|
+
SSH_ASKPASS_REQUIRE: "never"
|
|
930
|
+
},
|
|
931
|
+
extendEnv: false,
|
|
932
|
+
stdin: "ignore",
|
|
933
|
+
stdout: "ignore",
|
|
934
|
+
stderr: {
|
|
935
|
+
file: logPath,
|
|
936
|
+
append: true
|
|
937
|
+
},
|
|
938
|
+
buffer: false,
|
|
939
|
+
reject: false,
|
|
940
|
+
timeout: options.timeoutMs,
|
|
941
|
+
cancelSignal: options.signal,
|
|
942
|
+
killDescendants: true,
|
|
943
|
+
killSignal: "SIGKILL"
|
|
944
|
+
});
|
|
945
|
+
if (!result.failed) return void 0;
|
|
946
|
+
if (result.timedOut) await log.write(`dsh: connection to ${spec.host} timed out after ${String(options.timeoutMs)}ms\n`);
|
|
947
|
+
if ((await log.stat()).size === 0) await log.write(result.shortMessage ?? "GitHub connection check failed");
|
|
948
|
+
const { size } = await log.stat();
|
|
949
|
+
const bytes = Buffer.alloc(Math.min(size, options.outputBytes));
|
|
950
|
+
await log.read(bytes, 0, bytes.length, size - bytes.length);
|
|
951
|
+
const output = bytes.toString("utf8");
|
|
952
|
+
return {
|
|
953
|
+
exitCode: result.exitCode ?? (result.code === "ENOENT" ? 127 : 1),
|
|
954
|
+
output,
|
|
955
|
+
truncated: size > options.outputBytes,
|
|
956
|
+
logPath,
|
|
957
|
+
kind: classifyInstallFailure({
|
|
958
|
+
log: output,
|
|
959
|
+
timedOut: result.timedOut
|
|
960
|
+
})
|
|
961
|
+
};
|
|
962
|
+
} finally {
|
|
963
|
+
await log.close();
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
//#endregion
|
|
538
967
|
//#region lib/types/index.js
|
|
539
968
|
/** Current-profile plugin and bundle management over shared dsh plugin operations. */
|
|
540
969
|
var __runInitializers = function(thisArg, initializers, value) {
|
|
@@ -609,10 +1038,14 @@ function messageOf(error) {
|
|
|
609
1038
|
}
|
|
610
1039
|
/** An expected refusal keeps its code; anything else becomes an operation error carrying its exact diagnostic. */
|
|
611
1040
|
function managementError(error) {
|
|
612
|
-
|
|
1041
|
+
if (!(error instanceof ManagementFailure)) return {
|
|
613
1042
|
code: "operation-error",
|
|
614
1043
|
diagnostic: messageOf(error)
|
|
615
1044
|
};
|
|
1045
|
+
return {
|
|
1046
|
+
code: error.code,
|
|
1047
|
+
...error.incompatible === void 0 ? {} : { incompatible: error.incompatible }
|
|
1048
|
+
};
|
|
616
1049
|
}
|
|
617
1050
|
/** The caller stopped an installation; its files are restored before this is thrown. */
|
|
618
1051
|
var InstallCancelledError = class extends Error {
|
|
@@ -682,6 +1115,8 @@ function parsedForRegistry(spec) {
|
|
|
682
1115
|
let PluginManager = (() => {
|
|
683
1116
|
let _classSuper = TypertRemoteService;
|
|
684
1117
|
let _instanceExtraInitializers = [];
|
|
1118
|
+
let _listVersionExemptions_decorators;
|
|
1119
|
+
let _setVersionExemption_decorators;
|
|
685
1120
|
let _listPlugins_decorators;
|
|
686
1121
|
let _listBundles_decorators;
|
|
687
1122
|
let _registries_decorators;
|
|
@@ -695,6 +1130,8 @@ let PluginManager = (() => {
|
|
|
695
1130
|
return class PluginManager extends _classSuper {
|
|
696
1131
|
static {
|
|
697
1132
|
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
1133
|
+
_listVersionExemptions_decorators = [Remote];
|
|
1134
|
+
_setVersionExemption_decorators = [Remote];
|
|
698
1135
|
_listPlugins_decorators = [Remote];
|
|
699
1136
|
_listBundles_decorators = [Remote];
|
|
700
1137
|
_registries_decorators = [Remote];
|
|
@@ -705,6 +1142,28 @@ let PluginManager = (() => {
|
|
|
705
1142
|
_waitForInstall_decorators = [Remote];
|
|
706
1143
|
_cancelInstall_decorators = [Remote];
|
|
707
1144
|
_removeBundle_decorators = [Remote];
|
|
1145
|
+
__esDecorate(this, null, _listVersionExemptions_decorators, {
|
|
1146
|
+
kind: "method",
|
|
1147
|
+
name: "listVersionExemptions",
|
|
1148
|
+
static: false,
|
|
1149
|
+
private: false,
|
|
1150
|
+
access: {
|
|
1151
|
+
has: (obj) => "listVersionExemptions" in obj,
|
|
1152
|
+
get: (obj) => obj.listVersionExemptions
|
|
1153
|
+
},
|
|
1154
|
+
metadata: _metadata
|
|
1155
|
+
}, null, _instanceExtraInitializers);
|
|
1156
|
+
__esDecorate(this, null, _setVersionExemption_decorators, {
|
|
1157
|
+
kind: "method",
|
|
1158
|
+
name: "setVersionExemption",
|
|
1159
|
+
static: false,
|
|
1160
|
+
private: false,
|
|
1161
|
+
access: {
|
|
1162
|
+
has: (obj) => "setVersionExemption" in obj,
|
|
1163
|
+
get: (obj) => obj.setVersionExemption
|
|
1164
|
+
},
|
|
1165
|
+
metadata: _metadata
|
|
1166
|
+
}, null, _instanceExtraInitializers);
|
|
708
1167
|
__esDecorate(this, null, _listPlugins_decorators, {
|
|
709
1168
|
kind: "method",
|
|
710
1169
|
name: "listPlugins",
|
|
@@ -828,8 +1287,10 @@ let PluginManager = (() => {
|
|
|
828
1287
|
outputBytes: z.number().step(1).min(1).default(16384),
|
|
829
1288
|
lockWaitMs: z.number().step(1).min(0).default(12e4),
|
|
830
1289
|
inspectTimeoutMs: z.number().step(1).min(1e3).default(2e4),
|
|
1290
|
+
githubConnectionTimeoutMs: z.number().step(1).min(1e3).default(5e3),
|
|
1291
|
+
idleTimeoutMs: z.number().step(1).min(1e3).default(6e5),
|
|
831
1292
|
registry: z.string().pattern(REGISTRY_URL),
|
|
832
|
-
fallbackRegistries: z.array(z.string().pattern(REGISTRY_URL)).default([
|
|
1293
|
+
fallbackRegistries: z.array(z.string().pattern(REGISTRY_URL)).default([NPMMIRROR_REGISTRY])
|
|
833
1294
|
});
|
|
834
1295
|
/** Management bundles remain protected if their files become unreadable. */
|
|
835
1296
|
managementBundles = (__runInitializers(this, _instanceExtraInitializers), /* @__PURE__ */ new Set());
|
|
@@ -839,6 +1300,8 @@ let PluginManager = (() => {
|
|
|
839
1300
|
outputBytes;
|
|
840
1301
|
lockWaitMs;
|
|
841
1302
|
inspectTimeoutMs;
|
|
1303
|
+
githubConnectionTimeoutMs;
|
|
1304
|
+
idleTimeoutMs;
|
|
842
1305
|
pnpmCommand;
|
|
843
1306
|
configuredRegistries;
|
|
844
1307
|
ownerContext;
|
|
@@ -854,6 +1317,8 @@ let PluginManager = (() => {
|
|
|
854
1317
|
this.outputBytes = config.outputBytes;
|
|
855
1318
|
this.lockWaitMs = config.lockWaitMs;
|
|
856
1319
|
this.inspectTimeoutMs = config.inspectTimeoutMs;
|
|
1320
|
+
this.githubConnectionTimeoutMs = config.githubConnectionTimeoutMs;
|
|
1321
|
+
this.idleTimeoutMs = config.idleTimeoutMs;
|
|
857
1322
|
this.pnpmCommand = config.pnpmCommand;
|
|
858
1323
|
this.configuredRegistries = {
|
|
859
1324
|
registry: config.registry === void 0 ? null : normalizeRegistry(config.registry),
|
|
@@ -864,6 +1329,34 @@ let PluginManager = (() => {
|
|
|
864
1329
|
await Promise.allSettled([...this.packageOperations]);
|
|
865
1330
|
}, "plugin-manager: package cancellation");
|
|
866
1331
|
}
|
|
1332
|
+
/** Read exact plugin-version exemptions saved in this profile.
|
|
1333
|
+
* @returns Accepted package-name@version keys with the runtime versions they may run on, and any
|
|
1334
|
+
* record or file problem the reader rejected, which the caller reports instead of failing.
|
|
1335
|
+
*/
|
|
1336
|
+
listVersionExemptions() {
|
|
1337
|
+
const { exemptions, warnings } = readProfileCompatibility(this.profile.dir);
|
|
1338
|
+
return {
|
|
1339
|
+
exemptions,
|
|
1340
|
+
warnings
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
/** Grant or revoke one exact plugin/runtime exemption and reevaluate live plugins.
|
|
1344
|
+
* @param packageVersion Exact manifest package name followed by @ and its version; never an installation spec or alias.
|
|
1345
|
+
* @param runtimeVersion Exact current DSH version for grants; revocation may name a previous runtime.
|
|
1346
|
+
* @param enabled Whether to grant rather than revoke the exemption.
|
|
1347
|
+
* @param acceptRisk Required true for grants after the user accepts possible crashes and data loss.
|
|
1348
|
+
* @returns Saved and runtime outcomes. Startup-only profiles require restart.
|
|
1349
|
+
*/
|
|
1350
|
+
setVersionExemption(packageVersion, runtimeVersion, enabled, acceptRisk) {
|
|
1351
|
+
return this.change((result) => this.configure(async () => {
|
|
1352
|
+
await setProfileVersionExemption(this.profile.dir, packageVersion, runtimeVersion, enabled, acceptRisk === true);
|
|
1353
|
+
result.warnings = await this.reload();
|
|
1354
|
+
}), {
|
|
1355
|
+
stage: "enable",
|
|
1356
|
+
target: packageVersion,
|
|
1357
|
+
enabled
|
|
1358
|
+
}, "bundle");
|
|
1359
|
+
}
|
|
867
1360
|
/** Read current plugins, including why a row cannot be changed through the profile patch.
|
|
868
1361
|
* @returns Current runtime entries with persistent patch targets.
|
|
869
1362
|
*/
|
|
@@ -894,6 +1387,7 @@ let PluginManager = (() => {
|
|
|
894
1387
|
*/
|
|
895
1388
|
listBundles() {
|
|
896
1389
|
const manifest = readProfileManifest("dsh", this.profile.dir);
|
|
1390
|
+
const exemptions = readProfileVersionExemptions(this.profile.dir);
|
|
897
1391
|
const selected = manifest.dsh?.profile?.bundles ?? [];
|
|
898
1392
|
const dependencies = Object.keys(manifest.dependencies ?? {});
|
|
899
1393
|
const installation = JSON.parse(readFileSync(this.profile.installAnchor, "utf8"));
|
|
@@ -925,6 +1419,8 @@ let PluginManager = (() => {
|
|
|
925
1419
|
});
|
|
926
1420
|
continue;
|
|
927
1421
|
}
|
|
1422
|
+
const compatibility = evaluatePluginCompatibility(info, exemptions);
|
|
1423
|
+
if (compatibility !== void 0 && !compatibility.exempted) throw new ManagementFailure("incompatible-version", [incompatiblePlugin(compatibility)]);
|
|
928
1424
|
const dir = resolveBundleDir("dsh", name, this.profile.installAnchor, this.profile.dir);
|
|
929
1425
|
const meta = readPluginMeta(info.name ?? name, pathToFileURL(join(dir, "package.json")).href);
|
|
930
1426
|
bundles.push({
|
|
@@ -1114,7 +1610,9 @@ let PluginManager = (() => {
|
|
|
1114
1610
|
}, "bundle");
|
|
1115
1611
|
}
|
|
1116
1612
|
/**
|
|
1117
|
-
* Install a package using the same pnpm implementation as dsh plugin.
|
|
1613
|
+
* Install a package using the same pnpm implementation as dsh plugin. GitHub
|
|
1614
|
+
* repositories get a connection check bounded by githubConnectionTimeoutMs before pnpm starts;
|
|
1615
|
+
* only network failures or timeouts stop installation, while pnpm owns authentication and transport fallback. A run
|
|
1118
1616
|
* that fails, is cancelled, or adds a package without a bundle patch restores
|
|
1119
1617
|
* `package.json` and `pnpm-lock.yaml` as they were; downloaded files can stay.
|
|
1120
1618
|
* @param spec One package spec, including local paths relative to the invocation directory.
|
|
@@ -1129,7 +1627,7 @@ let PluginManager = (() => {
|
|
|
1129
1627
|
phase: "installing",
|
|
1130
1628
|
result: Promise.resolve(null)
|
|
1131
1629
|
};
|
|
1132
|
-
const stopped = () => control.abort.signal.aborted;
|
|
1630
|
+
const stopped = () => control.abort.signal.aborted || this.abort.signal.aborted;
|
|
1133
1631
|
if (requestId !== void 0) this.installs.set(requestId, control);
|
|
1134
1632
|
const announce = (phase, attempt) => {
|
|
1135
1633
|
if (requestId !== void 0) this.ownerContext.emit("plugin-manager/install-state", {
|
|
@@ -1147,11 +1645,30 @@ let PluginManager = (() => {
|
|
|
1147
1645
|
}
|
|
1148
1646
|
const files = await this.readRestoredFiles();
|
|
1149
1647
|
const before = readProfileManifest("dsh", this.profile.dir).dependencies ?? {};
|
|
1150
|
-
const plan = registryPlan(options?.registry, await this.registries());
|
|
1151
1648
|
let name;
|
|
1152
1649
|
try {
|
|
1153
|
-
let run;
|
|
1154
1650
|
result.registries = [];
|
|
1651
|
+
const connection = checkGithubConnection(parsedForRegistry(spec), this.profile.dir, {
|
|
1652
|
+
timeoutMs: this.githubConnectionTimeoutMs,
|
|
1653
|
+
outputBytes: this.outputBytes,
|
|
1654
|
+
signal: AbortSignal.any([this.abort.signal, control.abort.signal]),
|
|
1655
|
+
...this.profile.packageManager?.env === void 0 ? {} : { env: this.profile.packageManager.env }
|
|
1656
|
+
});
|
|
1657
|
+
this.packageOperations.add(connection);
|
|
1658
|
+
let connectionFailure;
|
|
1659
|
+
try {
|
|
1660
|
+
connectionFailure = await connection;
|
|
1661
|
+
} finally {
|
|
1662
|
+
this.packageOperations.delete(connection);
|
|
1663
|
+
}
|
|
1664
|
+
if (stopped()) throw new InstallCancelledError();
|
|
1665
|
+
if (connectionFailure?.kind === "network" || connectionFailure?.kind === "timeout") {
|
|
1666
|
+
result.packageResult = connectionFailure;
|
|
1667
|
+
result.failedAt = "spec-host";
|
|
1668
|
+
throw new Error(connectionFailure.output);
|
|
1669
|
+
}
|
|
1670
|
+
const plan = registryPlan(options?.registry, await this.registries());
|
|
1671
|
+
let run;
|
|
1155
1672
|
for (const [index, registry] of plan.entries()) {
|
|
1156
1673
|
if (index > 0) await this.restoreFiles(files);
|
|
1157
1674
|
if (stopped()) throw new InstallCancelledError();
|
|
@@ -1168,17 +1685,21 @@ let PluginManager = (() => {
|
|
|
1168
1685
|
], control.abort.signal, requestId);
|
|
1169
1686
|
result.packageResult = run;
|
|
1170
1687
|
if (stopped()) throw new InstallCancelledError();
|
|
1171
|
-
|
|
1172
|
-
if (run.exitCode === 0
|
|
1173
|
-
|
|
1688
|
+
if (run.incompatible !== void 0) throw new ManagementFailure("incompatible-version", run.incompatible);
|
|
1689
|
+
if (run.exitCode === 0 && run.timedOut !== true) break;
|
|
1690
|
+
/* v8 ignore next 2 -- runPnpm classifies every run it does not report as succeeded */
|
|
1691
|
+
if (run.kind === void 0) break;
|
|
1174
1692
|
delete result.failedAt;
|
|
1693
|
+
if (run.timedOut === true) break;
|
|
1694
|
+
const failedAt = attributeFailure(run.kind, run.output, parsedForRegistry(spec));
|
|
1175
1695
|
if (failedAt !== "other") result.failedAt = failedAt;
|
|
1176
1696
|
if (failedAt !== "registry" || index === plan.length - 1) break;
|
|
1177
1697
|
}
|
|
1178
1698
|
/* v8 ignore next -- the plan is never empty, so a run always settled */
|
|
1179
1699
|
if (run === void 0) throw new Error("no registry was asked");
|
|
1180
|
-
|
|
1181
|
-
if (
|
|
1700
|
+
const succeeded = run.exitCode === 0 && run.timedOut !== true;
|
|
1701
|
+
if (succeeded) delete result.failedAt;
|
|
1702
|
+
if (!succeeded) {
|
|
1182
1703
|
try {
|
|
1183
1704
|
result.pendingBuilds = await readPendingBuilds(this.profile.dir);
|
|
1184
1705
|
} catch (error) {
|
|
@@ -1195,6 +1716,8 @@ let PluginManager = (() => {
|
|
|
1195
1716
|
const dir = resolveBundleDir("dsh", name, this.profile.installAnchor, this.profile.dir);
|
|
1196
1717
|
const manifest = bundleManifest(name, this.profile.dir, this.profile.installAnchor);
|
|
1197
1718
|
if (manifest?.dsh?.bundle === void 0) throw new ManagementFailure("not-bundle");
|
|
1719
|
+
const compatibility = evaluatePluginCompatibility(manifest, readProfileVersionExemptions(this.profile.dir));
|
|
1720
|
+
if (compatibility !== void 0 && !compatibility.exempted) throw new ManagementFailure("incompatible-version", [incompatiblePlugin(compatibility)]);
|
|
1198
1721
|
for (const file of bundlePatchPaths(dir, manifest.dsh.bundle)) loadOverlayPatches("dsh", file);
|
|
1199
1722
|
} catch (error) {
|
|
1200
1723
|
await this.restoreFiles(files);
|
|
@@ -1230,7 +1753,7 @@ let PluginManager = (() => {
|
|
|
1230
1753
|
}
|
|
1231
1754
|
/** Stop an installation this manager owns and wait until its files are back.
|
|
1232
1755
|
* @param requestId The id the installation was started with.
|
|
1233
|
-
* @returns `cancelled` once pnpm exited and the files are restored, `too-late` once the bundle is being
|
|
1756
|
+
* @returns `cancelled` once the Git check or pnpm exited and the files are restored, `too-late` once the bundle is being
|
|
1234
1757
|
* applied, `not-running` for any other id.
|
|
1235
1758
|
*/
|
|
1236
1759
|
async cancelInstall(requestId) {
|
|
@@ -1264,7 +1787,7 @@ let PluginManager = (() => {
|
|
|
1264
1787
|
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");
|
|
1265
1788
|
});
|
|
1266
1789
|
result.packageResult = await this.runPnpm(["remove", name]);
|
|
1267
|
-
if (result.packageResult.exitCode !== 0) throw new Error(result.packageResult.output);
|
|
1790
|
+
if (result.packageResult.exitCode !== 0 || result.packageResult.timedOut === true) throw new Error(result.packageResult.output);
|
|
1268
1791
|
}, {
|
|
1269
1792
|
stage: "remove",
|
|
1270
1793
|
target: name
|
|
@@ -1323,6 +1846,8 @@ let PluginManager = (() => {
|
|
|
1323
1846
|
signal: signal === void 0 ? this.abort.signal : AbortSignal.any([this.abort.signal, signal]),
|
|
1324
1847
|
outputBytes: this.outputBytes,
|
|
1325
1848
|
activateNewBundles: false,
|
|
1849
|
+
idleTimeoutMs: this.idleTimeoutMs,
|
|
1850
|
+
lookupTimeoutMs: this.inspectTimeoutMs,
|
|
1326
1851
|
onOutput: (text, stream) => {
|
|
1327
1852
|
this.ownerContext.emit("plugin-manager/install-log", {
|
|
1328
1853
|
...identity,
|
|
@@ -1346,9 +1871,13 @@ let PluginManager = (() => {
|
|
|
1346
1871
|
text: "",
|
|
1347
1872
|
exitCode: signal?.aborted === true ? null : result.exitCode
|
|
1348
1873
|
});
|
|
1349
|
-
|
|
1874
|
+
if (result.exitCode === 0 && result.timedOut !== true) return result;
|
|
1875
|
+
return {
|
|
1350
1876
|
...result,
|
|
1351
|
-
kind: classifyInstallFailure({
|
|
1877
|
+
kind: classifyInstallFailure({
|
|
1878
|
+
log: result.output,
|
|
1879
|
+
...result.timedOut === true ? { timedOut: true } : {}
|
|
1880
|
+
})
|
|
1352
1881
|
};
|
|
1353
1882
|
} catch (error) {
|
|
1354
1883
|
this.ownerContext.emit("plugin-manager/install-log", {
|
|
@@ -1382,8 +1911,15 @@ let PluginManager = (() => {
|
|
|
1382
1911
|
async selectBundle(name, enabled) {
|
|
1383
1912
|
const manifest = readProfileManifest("dsh", this.profile.dir);
|
|
1384
1913
|
const previous = manifest.dsh?.profile?.bundles ?? [];
|
|
1385
|
-
if (
|
|
1386
|
-
|
|
1914
|
+
if (enabled || !previous.includes(name)) {
|
|
1915
|
+
const metadata = bundleManifest(name, this.profile.dir, this.profile.installAnchor);
|
|
1916
|
+
if (metadata === void 0) throw new ManagementFailure("not-bundle");
|
|
1917
|
+
if (enabled) {
|
|
1918
|
+
const compatibility = evaluatePluginCompatibility(metadata, readProfileVersionExemptions(this.profile.dir));
|
|
1919
|
+
if (compatibility !== void 0 && !compatibility.exempted) throw new ManagementFailure("incompatible-version", [incompatiblePlugin(compatibility)]);
|
|
1920
|
+
this.bundleRows(name);
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1387
1923
|
if (!enabled && previous.includes(name)) {
|
|
1388
1924
|
if (this.protectsManager(name)) throw new ManagementFailure("management-required");
|
|
1389
1925
|
}
|
|
@@ -1455,7 +1991,8 @@ let PluginManager = (() => {
|
|
|
1455
1991
|
return [
|
|
1456
1992
|
"package.json",
|
|
1457
1993
|
"cordis.patch.yml",
|
|
1458
|
-
"pnpm-workspace.yaml"
|
|
1994
|
+
"pnpm-workspace.yaml",
|
|
1995
|
+
PROFILE_COMPATIBILITY_FILENAME
|
|
1459
1996
|
].map((file) => {
|
|
1460
1997
|
try {
|
|
1461
1998
|
return readFileSync(join(this.profile.dir, file), "utf8");
|