@deepseek-ai/dsh-plugin-manager 0.2.1-alpha.1 → 0.2.1-alpha.3
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 +10 -10
- package/README.md +14 -7
- package/README.zh.md +15 -8
- package/lib/index.js +585 -292
- package/lib/typert.host.js +407 -242
- package/lib/typert.remote-client.d.ts +16 -2
- package/lib/typert.remote-client.js +390 -234
- package/lib/types/build-approval.d.ts +14 -2
- package/lib/types/build-approval.js +35 -5
- package/lib/types/index.d.ts +22 -2
- package/lib/types/index.js +172 -62
- package/lib/types/official-install-target.d.ts +15 -0
- package/lib/types/official-install-target.js +54 -0
- package/lib/types/operations.d.ts +1 -0
- package/lib/types/operations.js +320 -255
- package/lib/types/patch.d.ts +12 -3
- package/lib/types/patch.js +47 -9
- package/lib/types/tools.js +70 -8
- package/lib/types/types.d.ts +24 -3
- package/locale/en.json +5 -0
- package/locale/tools/en.json +5 -0
- package/locale/tools/zh.json +5 -0
- package/locale/zh.json +6 -0
- package/package.json +40 -36
|
@@ -1,8 +1,20 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Record the build scripts a failed run reported as ignored, then list every name awaiting a decision.
|
|
3
|
+
*
|
|
4
|
+
* pnpm writes those policy entries itself only when it can prompt, and the
|
|
5
|
+
* manager always runs it non-interactively, so the run's own report is the
|
|
6
|
+
* record that survives.
|
|
2
7
|
* @param dir Current profile directory.
|
|
8
|
+
* @param output Captured output of the failed run.
|
|
3
9
|
* @returns Exact package names awaiting a build decision; wildcard rules are excluded.
|
|
4
10
|
*/
|
|
5
|
-
export declare function
|
|
11
|
+
export declare function recordPendingBuilds(dir: string, output: string): Promise<string[]>;
|
|
12
|
+
/**
|
|
13
|
+
* Read the package names a pnpm run reported as ignored build scripts.
|
|
14
|
+
* @param output Captured output of the run.
|
|
15
|
+
* @returns Exact names in report order; wildcard patterns are excluded.
|
|
16
|
+
*/
|
|
17
|
+
export declare function ignoredBuildNames(output: string): string[];
|
|
6
18
|
/** Persist approval without running scripts; the caller holds the profile manifest lock.
|
|
7
19
|
* @param dir Current profile directory.
|
|
8
20
|
* @param names Explicit package names from the pending build list.
|
|
@@ -4,6 +4,10 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { isAlias, isMap, isNode, isScalar, parseDocument, visit } from 'yaml';
|
|
5
5
|
import { ManagementFailure } from "./failure.js";
|
|
6
6
|
import { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write';
|
|
7
|
+
/** Value pnpm leaves for a build script that still needs a decision. */
|
|
8
|
+
const UNDECIDED_BUILD = 'set this to true or false';
|
|
9
|
+
/** Where a non-interactive run names the build scripts it skipped. */
|
|
10
|
+
const IGNORED_BUILDS = /\bIgnored build scripts: (.+)/;
|
|
7
11
|
async function readPolicy(dir) {
|
|
8
12
|
let text;
|
|
9
13
|
try {
|
|
@@ -27,16 +31,42 @@ async function readPolicy(dir) {
|
|
|
27
31
|
throw new Error('allowBuilds must not contain YAML anchors or aliases');
|
|
28
32
|
}
|
|
29
33
|
});
|
|
34
|
+
const keys = isMap(builds) ? builds.items.flatMap(({ key }) => isScalar(key) && typeof key.value === 'string' ? [key.value] : []) : [];
|
|
30
35
|
const pending = isMap(builds) ? builds.items.flatMap(({ key, value }) => isScalar(key) && typeof key.value === 'string' && !/[*?]/.test(key.value)
|
|
31
|
-
&& isScalar(value) && value.value ===
|
|
32
|
-
return { document, pending };
|
|
36
|
+
&& isScalar(value) && value.value === UNDECIDED_BUILD ? [key.value] : []) : [];
|
|
37
|
+
return { document, pending, keys };
|
|
33
38
|
}
|
|
34
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* Record the build scripts a failed run reported as ignored, then list every name awaiting a decision.
|
|
41
|
+
*
|
|
42
|
+
* pnpm writes those policy entries itself only when it can prompt, and the
|
|
43
|
+
* manager always runs it non-interactively, so the run's own report is the
|
|
44
|
+
* record that survives.
|
|
35
45
|
* @param dir Current profile directory.
|
|
46
|
+
* @param output Captured output of the failed run.
|
|
36
47
|
* @returns Exact package names awaiting a build decision; wildcard rules are excluded.
|
|
37
48
|
*/
|
|
38
|
-
export async function
|
|
39
|
-
|
|
49
|
+
export async function recordPendingBuilds(dir, output) {
|
|
50
|
+
const { document, pending, keys } = await readPolicy(dir);
|
|
51
|
+
const known = new Set(keys);
|
|
52
|
+
const added = ignoredBuildNames(output).filter(name => !known.has(name));
|
|
53
|
+
if (added.length === 0)
|
|
54
|
+
return pending;
|
|
55
|
+
for (const name of added)
|
|
56
|
+
document.setIn(['allowBuilds', name], UNDECIDED_BUILD);
|
|
57
|
+
await writeFileAtomic(join(dir, 'pnpm-workspace.yaml'), String(document), { mode: 0o600 });
|
|
58
|
+
return [...pending, ...added];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Read the package names a pnpm run reported as ignored build scripts.
|
|
62
|
+
* @param output Captured output of the run.
|
|
63
|
+
* @returns Exact names in report order; wildcard patterns are excluded.
|
|
64
|
+
*/
|
|
65
|
+
export function ignoredBuildNames(output) {
|
|
66
|
+
const reported = IGNORED_BUILDS.exec(output)?.[1];
|
|
67
|
+
if (reported === undefined)
|
|
68
|
+
return [];
|
|
69
|
+
return reported.split(',').map(name => name.trim()).filter(name => name !== '' && !/[*?]/.test(name));
|
|
40
70
|
}
|
|
41
71
|
/** Persist approval without running scripts; the caller holds the profile manifest lock.
|
|
42
72
|
* @param dir Current profile directory.
|
package/lib/types/index.d.ts
CHANGED
|
@@ -40,6 +40,11 @@ export declare class PluginManager extends TypertRemoteService {
|
|
|
40
40
|
static Config: z<Config>;
|
|
41
41
|
/** Management bundles remain protected if their files become unreadable. */
|
|
42
42
|
private readonly managementBundles;
|
|
43
|
+
/**
|
|
44
|
+
* The registry each package was last installed from by this manager in this process. pnpm verifies lockfile tarball
|
|
45
|
+
* URLs against its default registry unless they belong to a named registry, so a later removal names it again.
|
|
46
|
+
*/
|
|
47
|
+
private readonly installedFrom;
|
|
43
48
|
private readonly ownerEntryId;
|
|
44
49
|
private readonly packageOperations;
|
|
45
50
|
private readonly profile;
|
|
@@ -75,7 +80,7 @@ export declare class PluginManager extends TypertRemoteService {
|
|
|
75
80
|
* @returns Current runtime entries with persistent patch targets.
|
|
76
81
|
*/
|
|
77
82
|
listPlugins(): Promise<PluginInfo[]>;
|
|
78
|
-
/** Read
|
|
83
|
+
/** Read installed, installation-provided, and offline Official catalog bundles, plus selected non-bundle names.
|
|
79
84
|
* A dependency without a bundle patch is listed, as a `not-bundle` problem, only while it is selected.
|
|
80
85
|
* @returns Package versions, manifest descriptions, the installable spec of profile dependencies, rows, optional
|
|
81
86
|
* display metadata, activation selections, whether the installation offers the bundle, and removal availability.
|
|
@@ -112,7 +117,7 @@ export declare class PluginManager extends TypertRemoteService {
|
|
|
112
117
|
* `package.json` and `pnpm-lock.yaml` as they were; downloaded files can stay.
|
|
113
118
|
* @param spec One package spec, including local paths relative to the invocation directory.
|
|
114
119
|
* @param options Whether to activate the installed bundle (defaults to true), the request id a cancellation names,
|
|
115
|
-
* the pending build scripts to allow
|
|
120
|
+
* the pending build scripts to allow, whether to save an exact dependency, and the registry asked first.
|
|
116
121
|
* @returns Package-manager diagnostics, the registries asked, and the observed activation outcome.
|
|
117
122
|
*/
|
|
118
123
|
installBundle(spec: string, options?: InstallBundleOptions): Promise<ChangeResult>;
|
|
@@ -146,7 +151,22 @@ export declare class PluginManager extends TypertRemoteService {
|
|
|
146
151
|
private bundleRows;
|
|
147
152
|
private protectsManager;
|
|
148
153
|
private configure;
|
|
154
|
+
/**
|
|
155
|
+
* Publish the profile's runtime resolution without HMR, unless a deselected startup bundle still runs and needs the
|
|
156
|
+
* current table. A publication that needs a restart waits for the restart the result already reports.
|
|
157
|
+
*/
|
|
149
158
|
private refreshPackages;
|
|
159
|
+
/**
|
|
160
|
+
* Apply a package change: without HMR, publish the runtime resolution when that is safe, then {@link reload}.
|
|
161
|
+
* @param requiredIds Entry ids that must start.
|
|
162
|
+
* @returns The reload's outcome.
|
|
163
|
+
*/
|
|
164
|
+
private publishAndReload;
|
|
165
|
+
/**
|
|
166
|
+
* Apply the saved profile; with HMR it recomposes live, including the runtime resolution.
|
|
167
|
+
* @param requiredIds Entry ids that must start.
|
|
168
|
+
* @returns Warnings about entries that were already inactive, and why the change waits for a restart, if it does.
|
|
169
|
+
*/
|
|
150
170
|
private reload;
|
|
151
171
|
private change;
|
|
152
172
|
private diskState;
|
package/lib/types/index.js
CHANGED
|
@@ -42,14 +42,15 @@ import { withFileLock, writeFileAtomic } from '@deepseek-ai/dsh-atomic-write';
|
|
|
42
42
|
import z from '@deepseek-ai/schemastery';
|
|
43
43
|
import { TypertRemoteService, Remote } from '@deepseek-ai/dsh-typert-protocol';
|
|
44
44
|
import { pluginEntryId, readPluginInventory } from '@deepseek-ai/dsh-host-plugin-inventory';
|
|
45
|
-
import { readPluginMeta, readProfileManifest, resolveBundleDir, loadOverlayPatches, composeEntries,
|
|
45
|
+
import { readPluginMeta, readProfileManifest, resolveBundleDir, loadOverlayPatches, composeEntries, readProfilePatches, OPTIONAL_BUNDLES, bundlePatchPaths, evaluatePluginCompatibility, readProfileCompatibility, readProfileVersionExemptions, setProfileVersionExemption, PROFILE_COMPATIBILITY_FILENAME, OFFICIAL_ON_DEMAND_CATALOG, getDshRuntimeVersion, ResolutionRestartRequired, } from '@deepseek-ai/dsh-app-boot';
|
|
46
46
|
import { bundleManifest, readProfileRegistry, registryArguments, runProfilePnpm, saveManifest, viewProfilePackage } from "./operations.js";
|
|
47
47
|
import { classifyInstallFailure } from "./install-failure.js";
|
|
48
|
+
import { officialBundleInstallTarget } from "./official-install-target.js";
|
|
48
49
|
import { dependencySpec, InvalidInstallSpecError, parseInstallSpec } from "./install-spec.js";
|
|
49
50
|
import { attributeFailure, normalizeRegistry, NPMMIRROR_REGISTRY, registryPlan } from "./registry.js";
|
|
50
|
-
import { writePluginEnabled } from "./patch.js";
|
|
51
|
+
import { flatten, writePluginEnabled } from "./patch.js";
|
|
51
52
|
import { incompatiblePlugin, ManagementFailure } from "./failure.js";
|
|
52
|
-
import { approveBuilds,
|
|
53
|
+
import { approveBuilds, recordPendingBuilds } from "./build-approval.js";
|
|
53
54
|
import { checkGithubConnection } from "./github-connection.js";
|
|
54
55
|
export { classifyInstallFailure } from "./install-failure.js";
|
|
55
56
|
export { InvalidInstallSpecError, parseInstallSpec } from "./install-spec.js";
|
|
@@ -61,7 +62,7 @@ const protectedModules = new Set([
|
|
|
61
62
|
'@deepseek-ai/dsh-host-webserver', '@deepseek-ai/dsh-client-modules',
|
|
62
63
|
'@deepseek-ai/dsh-client-ui-settings-plugin-inventory', '@deepseek-ai/dsh-client-ui-plugin-manager',
|
|
63
64
|
'@deepseek-ai/dsh-host-plugin-inventory', '@deepseek-ai/dsh-typert-registry',
|
|
64
|
-
'@deepseek-ai/dsh-api-remotes',
|
|
65
|
+
'@deepseek-ai/dsh-api-remotes', '@deepseek-ai/dsh-api-remote-access',
|
|
65
66
|
'@deepseek-ai/cordis-plugin-timer', '@deepseek-ai/dsh-client-connection',
|
|
66
67
|
'@deepseek-ai/dsh-host-frontend-static', '@deepseek-ai/dsh-tools',
|
|
67
68
|
'@deepseek-ai/dsh-hmr',
|
|
@@ -70,10 +71,6 @@ const protectedModules = new Set([
|
|
|
70
71
|
const RESTORED_FILES = ['package.json', 'pnpm-lock.yaml'];
|
|
71
72
|
/** pnpm's colour escapes, which a JSON answer may be wrapped in. */
|
|
72
73
|
const ANSI_SEQUENCE = /\x1b\[[0-9;]*m/g;
|
|
73
|
-
/** Flatten only the groups addressable by the profile's patch composer. */
|
|
74
|
-
function flatten(rows) {
|
|
75
|
-
return rows.flatMap(row => [row, ...(row.group && Array.isArray(row.config) ? flatten(row.config) : [])]);
|
|
76
|
-
}
|
|
77
74
|
/** Preserve the exact observed diagnostic, including non-Error failures. */
|
|
78
75
|
function messageOf(error) { return error instanceof Error ? error.message : String(error); }
|
|
79
76
|
/** An expected refusal keeps its code; anything else becomes an operation error carrying its exact diagnostic. */
|
|
@@ -127,6 +124,37 @@ function printedError(printed) {
|
|
|
127
124
|
const { code, message } = error;
|
|
128
125
|
return [code, message].filter((part) => typeof part === 'string').join(' ');
|
|
129
126
|
}
|
|
127
|
+
/** The bundles a profile manifest selects. */
|
|
128
|
+
function bundleSelection(manifest) {
|
|
129
|
+
return manifest.dsh?.profile?.bundles ?? [];
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* The package an installation spec names before pnpm runs: a registry name, a local directory's manifest name, or the
|
|
133
|
+
* dependency saved with this git or tarball spec, compared as the plugin list shows saved specs.
|
|
134
|
+
* @param spec The installation spec.
|
|
135
|
+
* @param dependencies The profile's saved dependencies.
|
|
136
|
+
* @param profileDir The profile directory saved local paths are relative to.
|
|
137
|
+
* @returns The package name, or `undefined` when only the installation tells it.
|
|
138
|
+
*/
|
|
139
|
+
function specPackageName(spec, dependencies, profileDir) {
|
|
140
|
+
const parsed = parsedForRegistry(spec);
|
|
141
|
+
if (parsed.kind === 'registry')
|
|
142
|
+
return parsed.name;
|
|
143
|
+
if (parsed.kind === 'path')
|
|
144
|
+
return packageName(parsed.path);
|
|
145
|
+
const specs = parsed.kind === 'tarball' && parsed.path !== undefined ? [spec, `file:${parsed.path}`] : [spec];
|
|
146
|
+
return Object.entries(dependencies).find(([dependency, saved]) => specs.includes(dependencySpec(dependency, saved, profileDir)))?.[0];
|
|
147
|
+
}
|
|
148
|
+
/** The package name a local directory declares, or `undefined` without a readable manifest. */
|
|
149
|
+
function packageName(dir) {
|
|
150
|
+
try {
|
|
151
|
+
return readProfileManifest('dsh', dir).name;
|
|
152
|
+
}
|
|
153
|
+
catch (_error) {
|
|
154
|
+
// pnpm reports the unreadable directory itself.
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
130
158
|
/** The spec's form, for deciding whether a failed attempt was the registry's; a form the parser refuses has no host of its own. */
|
|
131
159
|
function parsedForRegistry(spec) {
|
|
132
160
|
try {
|
|
@@ -197,6 +225,11 @@ let PluginManager = (() => {
|
|
|
197
225
|
});
|
|
198
226
|
/** Management bundles remain protected if their files become unreadable. */
|
|
199
227
|
managementBundles = (__runInitializers(this, _instanceExtraInitializers), new Set());
|
|
228
|
+
/**
|
|
229
|
+
* The registry each package was last installed from by this manager in this process. pnpm verifies lockfile tarball
|
|
230
|
+
* URLs against its default registry unless they belong to a named registry, so a later removal names it again.
|
|
231
|
+
*/
|
|
232
|
+
installedFrom = new Map();
|
|
200
233
|
ownerEntryId;
|
|
201
234
|
packageOperations = new Set();
|
|
202
235
|
profile;
|
|
@@ -251,7 +284,7 @@ let PluginManager = (() => {
|
|
|
251
284
|
setVersionExemption(packageVersion, runtimeVersion, enabled, acceptRisk) {
|
|
252
285
|
return this.change(result => this.configure(async () => {
|
|
253
286
|
await setProfileVersionExemption(this.profile.dir, packageVersion, runtimeVersion, enabled, acceptRisk === true);
|
|
254
|
-
result
|
|
287
|
+
Object.assign(result, await this.reload());
|
|
255
288
|
}), { stage: 'enable', target: packageVersion, enabled }, 'bundle');
|
|
256
289
|
}
|
|
257
290
|
/** Read current plugins, including why a row cannot be changed through the profile patch.
|
|
@@ -274,7 +307,7 @@ let PluginManager = (() => {
|
|
|
274
307
|
return { ...entry, patchId: candidate.id };
|
|
275
308
|
});
|
|
276
309
|
}
|
|
277
|
-
/** Read
|
|
310
|
+
/** Read installed, installation-provided, and offline Official catalog bundles, plus selected non-bundle names.
|
|
278
311
|
* A dependency without a bundle patch is listed, as a `not-bundle` problem, only while it is selected.
|
|
279
312
|
* @returns Package versions, manifest descriptions, the installable spec of profile dependencies, rows, optional
|
|
280
313
|
* display metadata, activation selections, whether the installation offers the bundle, and removal availability.
|
|
@@ -282,49 +315,75 @@ let PluginManager = (() => {
|
|
|
282
315
|
listBundles() {
|
|
283
316
|
const manifest = readProfileManifest('dsh', this.profile.dir);
|
|
284
317
|
const exemptions = readProfileVersionExemptions(this.profile.dir);
|
|
285
|
-
const selected = manifest
|
|
318
|
+
const selected = bundleSelection(manifest);
|
|
286
319
|
const recorded = manifest.dependencies ?? {};
|
|
287
320
|
const dependencies = Object.keys(recorded);
|
|
288
321
|
const installation = JSON.parse(readFileSync(this.profile.installAnchor, 'utf8'));
|
|
289
|
-
const
|
|
322
|
+
const catalog = new Map(OFFICIAL_ON_DEMAND_CATALOG.map(entry => [entry.packageName, entry]));
|
|
323
|
+
const names = [...new Set([...selected, ...dependencies, ...Object.keys(installation.dependencies ?? {}), ...catalog.keys()])];
|
|
290
324
|
const bundles = [];
|
|
325
|
+
const runtimeVersion = getDshRuntimeVersion();
|
|
291
326
|
for (const name of names) {
|
|
292
327
|
const installed = dependencies.includes(name);
|
|
293
328
|
const optional = OPTIONAL_BUNDLES.includes(name);
|
|
294
329
|
const enabled = selected.includes(name);
|
|
295
330
|
const shipped = Object.hasOwn(installation.dependencies ?? {}, name);
|
|
296
|
-
|
|
297
|
-
|
|
331
|
+
const offered = catalog.get(name);
|
|
332
|
+
const official = optional || offered !== undefined;
|
|
333
|
+
let target = {};
|
|
334
|
+
let targetError;
|
|
335
|
+
if (offered !== undefined) {
|
|
336
|
+
try {
|
|
337
|
+
target = { installTarget: officialBundleInstallTarget(name, this.profile.installAnchor, runtimeVersion) };
|
|
338
|
+
}
|
|
339
|
+
catch (error) {
|
|
340
|
+
targetError = managementError(error);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
const catalogMeta = offered === undefined ? {} : { meta: offered.meta };
|
|
344
|
+
if (offered !== undefined && !installed && !shipped) {
|
|
345
|
+
bundles.push({ name, official, availability: 'missing', ...catalogMeta, ...target, enabled, installed, optional,
|
|
346
|
+
removable: enabled, rows: [], overrides: [],
|
|
347
|
+
...targetError !== undefined ? { error: targetError } : enabled ? { error: { code: 'not-bundle' } } : {} });
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
// Installation-owned copies remain protected even if a profile also declares their names.
|
|
298
351
|
const owned = installed && !shipped;
|
|
299
|
-
// A selection neither the profile nor the installation holds, such as a retired bundle, is removed by deselecting it.
|
|
300
352
|
const removable = owned || (enabled && !installed && !shipped);
|
|
301
353
|
const sourceOf = (packageName) => owned ? { source: dependencySpec(name, recorded[name], this.profile.dir, packageName) } : {};
|
|
302
354
|
const readOnlyReason = this.protectsManager(name) ? 'management-required' : undefined;
|
|
355
|
+
let availability = 'missing';
|
|
356
|
+
let version;
|
|
303
357
|
try {
|
|
304
358
|
const info = bundleManifest(name, this.profile.dir, this.profile.installAnchor);
|
|
359
|
+
availability = shipped ? 'installation' : 'profile';
|
|
305
360
|
if (info === undefined) {
|
|
306
|
-
if (enabled)
|
|
307
|
-
bundles.push({ name,
|
|
308
|
-
removable: removable && readOnlyReason === undefined,
|
|
309
|
-
...(readOnlyReason === undefined ? {} : { readOnlyReason }), error: { code: 'not-bundle' }, rows: [], overrides: [] });
|
|
361
|
+
if (enabled || offered !== undefined)
|
|
362
|
+
bundles.push({ name, official, availability, ...catalogMeta, ...target,
|
|
363
|
+
...sourceOf(), enabled, installed, optional, removable: removable && readOnlyReason === undefined,
|
|
364
|
+
...(readOnlyReason === undefined ? {} : { readOnlyReason }), error: targetError ?? { code: 'not-bundle' }, rows: [], overrides: [] });
|
|
310
365
|
continue;
|
|
311
366
|
}
|
|
367
|
+
version = info.version;
|
|
312
368
|
const compatibility = evaluatePluginCompatibility(info, exemptions);
|
|
313
369
|
if (compatibility !== undefined && !compatibility.exempted)
|
|
314
370
|
throw new ManagementFailure('incompatible-version', [incompatiblePlugin(compatibility)]);
|
|
315
371
|
const dir = resolveBundleDir('dsh', name, this.profile.installAnchor, this.profile.dir);
|
|
316
|
-
const meta = readPluginMeta(info.name ?? name, pathToFileURL(join(dir, 'package.json')).href);
|
|
317
|
-
bundles.push({ name, ...(
|
|
372
|
+
const meta = offered?.meta ?? readPluginMeta(info.name ?? name, pathToFileURL(join(dir, 'package.json')).href);
|
|
373
|
+
bundles.push({ name, official, availability, ...target, ...(version === undefined ? {} : { version }),
|
|
318
374
|
...(info.description === undefined || info.description === '' ? {} : { description: info.description }),
|
|
319
375
|
...meta === undefined ? {} : { meta },
|
|
320
376
|
...sourceOf(info.name), enabled, installed, optional, removable: removable && readOnlyReason === undefined,
|
|
321
377
|
...(readOnlyReason === undefined ? {} : { readOnlyReason }),
|
|
322
|
-
...this.declaredRows(name, info)
|
|
378
|
+
...this.declaredRows(name, info),
|
|
379
|
+
...targetError === undefined ? {} : { error: targetError } });
|
|
323
380
|
}
|
|
324
381
|
catch (error) {
|
|
325
|
-
if (enabled || installed) {
|
|
326
|
-
bundles.push({ name,
|
|
327
|
-
...(
|
|
382
|
+
if (enabled || installed || optional || offered !== undefined) {
|
|
383
|
+
bundles.push({ name, official, availability, ...catalogMeta, ...target, ...(version === undefined ? {} : { version }),
|
|
384
|
+
...sourceOf(), enabled, installed, optional, removable: removable && readOnlyReason === undefined,
|
|
385
|
+
...(readOnlyReason === undefined ? {} : { readOnlyReason }),
|
|
386
|
+
error: targetError ?? managementError(error), rows: [], overrides: [] });
|
|
328
387
|
}
|
|
329
388
|
}
|
|
330
389
|
}
|
|
@@ -361,7 +420,7 @@ let PluginManager = (() => {
|
|
|
361
420
|
const manifest = readProfileManifest('dsh', this.profile.dir);
|
|
362
421
|
const installation = JSON.parse(readFileSync(this.profile.installAnchor, 'utf8'));
|
|
363
422
|
const known = new Set([
|
|
364
|
-
...manifest
|
|
423
|
+
...bundleSelection(manifest), ...Object.keys(manifest.dependencies ?? {}), ...Object.keys(installation.dependencies ?? {}),
|
|
365
424
|
]);
|
|
366
425
|
const plan = registryPlan(options?.registry, await this.registries());
|
|
367
426
|
const registry = plan[0];
|
|
@@ -450,8 +509,8 @@ let PluginManager = (() => {
|
|
|
450
509
|
throw new ManagementFailure('unknown-plugin');
|
|
451
510
|
if (row.readOnlyReason !== undefined)
|
|
452
511
|
throw new ManagementFailure(row.readOnlyReason);
|
|
453
|
-
await writePluginEnabled(this.profile
|
|
454
|
-
result
|
|
512
|
+
await writePluginEnabled(this.profile, row.patchId, row.moduleName, enabled);
|
|
513
|
+
Object.assign(result, await this.reload(enabled ? [row.patchId] : []));
|
|
455
514
|
const current = (await this.listPlugins()).find(item => item.entryId === id);
|
|
456
515
|
return current?.enabled !== enabled && this.ownerContext.get('hmr') !== undefined ? 'overridden' : undefined;
|
|
457
516
|
}), { stage: 'enable', target: id, enabled }, 'plugin');
|
|
@@ -464,11 +523,7 @@ let PluginManager = (() => {
|
|
|
464
523
|
setBundleEnabled(name, enabled) {
|
|
465
524
|
return this.change(result => this.configure(async () => {
|
|
466
525
|
await this.selectBundle(name, enabled);
|
|
467
|
-
|
|
468
|
-
await this.refreshPackages();
|
|
469
|
-
result.warnings = await this.reload(enabled ? this.bundleRows(name).map(row => row.id) : []);
|
|
470
|
-
if (!enabled && this.ownerContext.get('hmr') !== undefined)
|
|
471
|
-
await this.refreshPackages();
|
|
526
|
+
Object.assign(result, enabled ? await this.publishAndReload(this.bundleRows(name).map(row => row.id)) : await this.reload());
|
|
472
527
|
}), { stage: 'enable', target: name, enabled }, 'bundle');
|
|
473
528
|
}
|
|
474
529
|
/**
|
|
@@ -479,7 +534,7 @@ let PluginManager = (() => {
|
|
|
479
534
|
* `package.json` and `pnpm-lock.yaml` as they were; downloaded files can stay.
|
|
480
535
|
* @param spec One package spec, including local paths relative to the invocation directory.
|
|
481
536
|
* @param options Whether to activate the installed bundle (defaults to true), the request id a cancellation names,
|
|
482
|
-
* the pending build scripts to allow
|
|
537
|
+
* the pending build scripts to allow, whether to save an exact dependency, and the registry asked first.
|
|
483
538
|
* @returns Package-manager diagnostics, the registries asked, and the observed activation outcome.
|
|
484
539
|
*/
|
|
485
540
|
installBundle(spec, options) {
|
|
@@ -497,12 +552,17 @@ let PluginManager = (() => {
|
|
|
497
552
|
throw new ManagementFailure('invalid-spec');
|
|
498
553
|
if (stopped())
|
|
499
554
|
throw new InstallCancelledError();
|
|
555
|
+
const before = readProfileManifest('dsh', this.profile.dir).dependencies ?? {};
|
|
556
|
+
// With HMR, an installed package is removed before another version is installed, so its plugins stop first.
|
|
557
|
+
const known = specPackageName(spec, before, this.profile.dir);
|
|
558
|
+
if (this.ownerContext.get('hmr') !== undefined && known !== undefined && Object.hasOwn(before, known)) {
|
|
559
|
+
throw new ManagementFailure('already-installed');
|
|
560
|
+
}
|
|
500
561
|
if (options?.approvedBuilds !== undefined) {
|
|
501
562
|
await approveBuilds(this.profile.dir, options.approvedBuilds);
|
|
502
563
|
result.approvedBuilds = options.approvedBuilds;
|
|
503
564
|
}
|
|
504
565
|
const files = await this.readRestoredFiles();
|
|
505
|
-
const before = readProfileManifest('dsh', this.profile.dir).dependencies ?? {};
|
|
506
566
|
let name;
|
|
507
567
|
let version;
|
|
508
568
|
try {
|
|
@@ -530,6 +590,7 @@ let PluginManager = (() => {
|
|
|
530
590
|
// The last run is the result's; the registries asked stay listed whatever the outcome.
|
|
531
591
|
const plan = registryPlan(options?.registry, await this.registries());
|
|
532
592
|
let run;
|
|
593
|
+
let used = null;
|
|
533
594
|
for (const [index, registry] of plan.entries()) {
|
|
534
595
|
if (index > 0)
|
|
535
596
|
await this.restoreFiles(files);
|
|
@@ -537,8 +598,9 @@ let PluginManager = (() => {
|
|
|
537
598
|
if (stopped())
|
|
538
599
|
throw new InstallCancelledError();
|
|
539
600
|
result.registries.push(registry);
|
|
601
|
+
used = registry;
|
|
540
602
|
announce('installing', { registry, index: index + 1, total: plan.length });
|
|
541
|
-
run = await this.runPnpm(['add', spec, ...registryArguments(registry)], control.abort.signal, requestId);
|
|
603
|
+
run = await this.runPnpm(['add', spec, ...options?.saveExact === true ? ['--save-exact'] : [], ...registryArguments(registry)], control.abort.signal, requestId);
|
|
542
604
|
result.packageResult = run;
|
|
543
605
|
if (stopped())
|
|
544
606
|
throw new InstallCancelledError();
|
|
@@ -570,9 +632,9 @@ let PluginManager = (() => {
|
|
|
570
632
|
if (succeeded)
|
|
571
633
|
delete result.failedAt;
|
|
572
634
|
if (!succeeded) {
|
|
573
|
-
// pnpm-workspace.yaml is not restored, so the names
|
|
635
|
+
// pnpm-workspace.yaml is not restored, so the names it left undecided, or reported as ignored, are offered for approval.
|
|
574
636
|
try {
|
|
575
|
-
result.pendingBuilds = await
|
|
637
|
+
result.pendingBuilds = await recordPendingBuilds(this.profile.dir, run.output);
|
|
576
638
|
}
|
|
577
639
|
catch (error) {
|
|
578
640
|
this.ownerContext.logger.warn('Could not read pending build approvals after pnpm failed', error);
|
|
@@ -581,9 +643,12 @@ let PluginManager = (() => {
|
|
|
581
643
|
}
|
|
582
644
|
const after = readProfileManifest('dsh', this.profile.dir).dependencies ?? {};
|
|
583
645
|
const installed = Object.keys(after).filter(name => before[name] !== after[name]);
|
|
584
|
-
//
|
|
585
|
-
if (installed.length === 0)
|
|
586
|
-
|
|
646
|
+
// Without HMR a repeated installation can leave the saved dependency as it was.
|
|
647
|
+
if (installed.length === 0) {
|
|
648
|
+
const local = specPackageName(spec, after, this.profile.dir);
|
|
649
|
+
if (local !== undefined && Object.hasOwn(after, local))
|
|
650
|
+
installed.push(local);
|
|
651
|
+
}
|
|
587
652
|
const target = installed[0];
|
|
588
653
|
if (installed.length !== 1 || target === undefined)
|
|
589
654
|
throw new ManagementFailure('ambiguous-install');
|
|
@@ -598,6 +663,7 @@ let PluginManager = (() => {
|
|
|
598
663
|
for (const file of bundlePatchPaths(dir, manifest.dsh.bundle))
|
|
599
664
|
loadOverlayPatches('dsh', file);
|
|
600
665
|
version = manifest.version;
|
|
666
|
+
this.installedFrom.set(name, used);
|
|
601
667
|
}
|
|
602
668
|
catch (error) {
|
|
603
669
|
// pnpm has exited by now, so the files it rewrote go back as they were.
|
|
@@ -614,11 +680,12 @@ let PluginManager = (() => {
|
|
|
614
680
|
return this.configure(async () => {
|
|
615
681
|
if (options?.enabled !== false)
|
|
616
682
|
await this.selectBundle(name, true);
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
683
|
+
// Without HMR the replaced package's plugins keep running until the restart the result already reports.
|
|
684
|
+
if (Object.hasOwn(before, name) && this.ownerContext.get('hmr') === undefined)
|
|
685
|
+
return;
|
|
686
|
+
// A selected bundle's own rows must start.
|
|
687
|
+
const selected = bundleSelection(readProfileManifest('dsh', this.profile.dir)).includes(name);
|
|
688
|
+
Object.assign(result, await this.publishAndReload(selected ? this.bundleRows(name).map(row => row.id) : []));
|
|
622
689
|
});
|
|
623
690
|
}, { stage: 'install', target: spec, enabled: options?.enabled !== false }, 'install');
|
|
624
691
|
control.result = result;
|
|
@@ -669,7 +736,7 @@ let PluginManager = (() => {
|
|
|
669
736
|
const contributions = bundle.error === undefined ? this.bundleRows(name) : [];
|
|
670
737
|
if (bundle.enabled) {
|
|
671
738
|
await this.selectBundle(name, false);
|
|
672
|
-
result
|
|
739
|
+
Object.assign(result, await this.reload());
|
|
673
740
|
}
|
|
674
741
|
if ([...this.ctx.loader.entries()].some(entry => entry.fiber?.uid != null
|
|
675
742
|
&& contributions.some(row => row.id === entry.options.id && row.name === entry.options.name))) {
|
|
@@ -679,11 +746,15 @@ let PluginManager = (() => {
|
|
|
679
746
|
});
|
|
680
747
|
if (!installed)
|
|
681
748
|
return;
|
|
682
|
-
|
|
749
|
+
// `pnpm remove` takes no `--registry` option; the generic configuration flag sets the same setting. A package
|
|
750
|
+
// installed before this process started is removed with the registry an installation asks first.
|
|
751
|
+
const registry = this.installedFrom.has(name) ? this.installedFrom.get(name) : this.configuredRegistries.registry;
|
|
752
|
+
result.packageResult = await this.runPnpm(['remove', name, ...typeof registry === 'string' ? [`--config.registry=${registry}`] : []]);
|
|
683
753
|
if (result.packageResult.exitCode !== 0 || result.packageResult.timedOut === true) {
|
|
684
754
|
throw new Error(result.packageResult.output);
|
|
685
755
|
}
|
|
686
|
-
|
|
756
|
+
this.installedFrom.delete(name);
|
|
757
|
+
Object.assign(result, await this.configure(() => this.publishAndReload()));
|
|
687
758
|
}, { stage: 'remove', target: name }, 'remove');
|
|
688
759
|
}
|
|
689
760
|
/** The rows a bundle's patch inserts and the existing rows it changes; an unreadable patch throws. */
|
|
@@ -706,7 +777,8 @@ let PluginManager = (() => {
|
|
|
706
777
|
const rows = [];
|
|
707
778
|
const packages = this.ctx.get('pluginPackages');
|
|
708
779
|
for (const row of flatten(composeEntries([patches.filter(item => item.insert !== undefined)]))) {
|
|
709
|
-
|
|
780
|
+
// Group rows are containers without a plugin inventory entry; their children carry the switches.
|
|
781
|
+
if (typeof row.id !== 'string' || typeof row.name !== 'string' || row.group === true)
|
|
710
782
|
continue;
|
|
711
783
|
const active = live.get(row.id);
|
|
712
784
|
const entryId = active?.entryId;
|
|
@@ -776,7 +848,7 @@ let PluginManager = (() => {
|
|
|
776
848
|
}
|
|
777
849
|
async selectBundle(name, enabled) {
|
|
778
850
|
const manifest = readProfileManifest('dsh', this.profile.dir);
|
|
779
|
-
const previous = manifest
|
|
851
|
+
const previous = bundleSelection(manifest);
|
|
780
852
|
if (enabled || !previous.includes(name)) {
|
|
781
853
|
const metadata = bundleManifest(name, this.profile.dir, this.profile.installAnchor);
|
|
782
854
|
if (metadata === undefined)
|
|
@@ -828,28 +900,64 @@ let PluginManager = (() => {
|
|
|
828
900
|
const apply = () => { this.abort.signal.throwIfAborted(); return operation(); };
|
|
829
901
|
return hmr === undefined ? apply() : hmr.runExclusive(apply);
|
|
830
902
|
}
|
|
903
|
+
/**
|
|
904
|
+
* Publish the profile's runtime resolution without HMR, unless a deselected startup bundle still runs and needs the
|
|
905
|
+
* current table. A publication that needs a restart waits for the restart the result already reports.
|
|
906
|
+
*/
|
|
831
907
|
async refreshPackages() {
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
908
|
+
const selected = bundleSelection(readProfileManifest('dsh', this.profile.dir));
|
|
909
|
+
if (this.profile.startedBundles.some(name => !selected.includes(name)))
|
|
910
|
+
return;
|
|
911
|
+
try {
|
|
912
|
+
const packages = this.ownerContext.get('pluginPackages');
|
|
913
|
+
// A dropped mapping's bundle is not running: the guard above keeps the table while a deselected startup bundle runs.
|
|
914
|
+
if (packages?.resolution !== undefined)
|
|
915
|
+
await packages.refresh();
|
|
916
|
+
}
|
|
917
|
+
catch (error) {
|
|
918
|
+
if (!(error instanceof ResolutionRestartRequired))
|
|
919
|
+
throw error;
|
|
837
920
|
}
|
|
838
|
-
await this.ownerContext.get('pluginPackages')?.refresh();
|
|
839
921
|
}
|
|
840
|
-
|
|
922
|
+
/**
|
|
923
|
+
* Apply a package change: without HMR, publish the runtime resolution when that is safe, then {@link reload}.
|
|
924
|
+
* @param requiredIds Entry ids that must start.
|
|
925
|
+
* @returns The reload's outcome.
|
|
926
|
+
*/
|
|
927
|
+
async publishAndReload(requiredIds = []) {
|
|
841
928
|
if (this.ownerContext.get('hmr') === undefined)
|
|
842
|
-
|
|
843
|
-
return
|
|
929
|
+
await this.refreshPackages();
|
|
930
|
+
return this.reload(requiredIds);
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* Apply the saved profile; with HMR it recomposes live, including the runtime resolution.
|
|
934
|
+
* @param requiredIds Entry ids that must start.
|
|
935
|
+
* @returns Warnings about entries that were already inactive, and why the change waits for a restart, if it does.
|
|
936
|
+
*/
|
|
937
|
+
async reload(requiredIds = []) {
|
|
938
|
+
const hmr = this.ownerContext.get('hmr');
|
|
939
|
+
if (hmr === undefined) {
|
|
940
|
+
// Startup-only profiles recompose at restart; the saved patches still have to compose.
|
|
941
|
+
composeEntries([readProfilePatches('dsh', this.profile)]);
|
|
942
|
+
return { warnings: [] };
|
|
943
|
+
}
|
|
944
|
+
const { warnings, pending, restartReason } = await hmr.reconcileProfile(requiredIds);
|
|
945
|
+
const restart = restartReason ?? pending[0]?.reason;
|
|
946
|
+
return restart === undefined ? { warnings } : { warnings, restartReason: restart };
|
|
844
947
|
}
|
|
845
948
|
async change(operation, request, reason) {
|
|
846
949
|
return withFileLock(join(this.profile.dir, 'package.json'), async () => {
|
|
847
950
|
this.abort.signal.throwIfAborted();
|
|
848
951
|
const before = this.diskState();
|
|
849
952
|
const result = { ...request, changed: false,
|
|
850
|
-
|
|
953
|
+
...this.ownerContext.get('hmr') !== undefined ? { application: 'applied' } : { application: 'restart-required', restartReason: 'no-hmr' } };
|
|
851
954
|
try {
|
|
852
|
-
|
|
955
|
+
const outcome = await operation(result);
|
|
956
|
+
if (outcome !== undefined)
|
|
957
|
+
result.application = outcome;
|
|
958
|
+
// A reload that waits for a restart leaves the change saved but not applied.
|
|
959
|
+
else if (result.application === 'applied' && result.restartReason !== undefined)
|
|
960
|
+
result.application = 'restart-required';
|
|
853
961
|
}
|
|
854
962
|
catch (error) {
|
|
855
963
|
if (error instanceof InstallCancelledError) {
|
|
@@ -860,6 +968,8 @@ let PluginManager = (() => {
|
|
|
860
968
|
result.error = managementError(error);
|
|
861
969
|
}
|
|
862
970
|
}
|
|
971
|
+
if (result.application !== 'restart-required')
|
|
972
|
+
delete result.restartReason;
|
|
863
973
|
result.changed = before !== this.diskState();
|
|
864
974
|
this.ownerContext.emit('plugin-manager/changed', { reason });
|
|
865
975
|
return result;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offer a source checkout link or an exact published package version. Source installations are
|
|
3
|
+
* identified by the real CLI manifest and its unpacked workspace dependencies, including Desktop's
|
|
4
|
+
* development symlink. An incomplete checkout is an error, never a registry fallback.
|
|
5
|
+
* @param name - Official catalog package name.
|
|
6
|
+
* @param installAnchor - Running application's DSH package.json, possibly reached through a symlink.
|
|
7
|
+
* @param version - Running DSH version used by its release family.
|
|
8
|
+
* @returns An ordinary package-manager spec and its package version.
|
|
9
|
+
* @throws when a source installation lacks its workspace package, bundle patch, or built runtime.
|
|
10
|
+
*/
|
|
11
|
+
export declare function officialBundleInstallTarget(name: string, installAnchor: string, version: string): {
|
|
12
|
+
spec: string;
|
|
13
|
+
version: string;
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=official-install-target.d.ts.map
|