@deepseek-ai/dsh-plugin-manager 0.2.0-rc.2 → 0.2.1-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +6 -6
- package/README.md +17 -6
- package/README.zh.md +17 -6
- package/lib/index.js +200 -28
- package/lib/typert.host.js +34 -20
- package/lib/typert.remote-client.js +26 -12
- package/lib/types/build-approval.d.ts +14 -2
- package/lib/types/build-approval.js +35 -5
- package/lib/types/index.d.ts +8 -6
- package/lib/types/index.js +91 -29
- package/lib/types/install-spec.d.ts +13 -0
- package/lib/types/install-spec.js +33 -1
- package/lib/types/official-install-target.d.ts +15 -0
- package/lib/types/official-install-target.js +54 -0
- package/lib/types/tools.js +1 -1
- package/lib/types/types.d.ts +20 -2
- package/package.json +35 -34
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, reconcileProfilePatches, readProfilePatches, OPTIONAL_BUNDLES, bundlePatchPaths, evaluatePluginCompatibility, readProfileCompatibility, readProfileVersionExemptions, setProfileVersionExemption, PROFILE_COMPATIBILITY_FILENAME, } from '@deepseek-ai/dsh-app-boot';
|
|
45
|
+
import { readPluginMeta, readProfileManifest, resolveBundleDir, loadOverlayPatches, composeEntries, reconcileProfilePatches, readProfilePatches, OPTIONAL_BUNDLES, bundlePatchPaths, evaluatePluginCompatibility, readProfileCompatibility, readProfileVersionExemptions, setProfileVersionExemption, PROFILE_COMPATIBILITY_FILENAME, OFFICIAL_ON_DEMAND_CATALOG, getDshRuntimeVersion, } 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 {
|
|
48
|
+
import { officialBundleInstallTarget } from "./official-install-target.js";
|
|
49
|
+
import { dependencySpec, InvalidInstallSpecError, parseInstallSpec } from "./install-spec.js";
|
|
49
50
|
import { attributeFailure, normalizeRegistry, NPMMIRROR_REGISTRY, registryPlan } from "./registry.js";
|
|
50
51
|
import { 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";
|
|
@@ -274,49 +275,83 @@ let PluginManager = (() => {
|
|
|
274
275
|
return { ...entry, patchId: candidate.id };
|
|
275
276
|
});
|
|
276
277
|
}
|
|
277
|
-
/** Read
|
|
278
|
+
/** Read installed, installation-provided, and offline Official catalog bundles, plus selected non-bundle names.
|
|
278
279
|
* A dependency without a bundle patch is listed, as a `not-bundle` problem, only while it is selected.
|
|
279
|
-
* @returns Package versions, manifest descriptions,
|
|
280
|
-
* whether the installation offers the bundle, and removal availability.
|
|
280
|
+
* @returns Package versions, manifest descriptions, the installable spec of profile dependencies, rows, optional
|
|
281
|
+
* display metadata, activation selections, whether the installation offers the bundle, and removal availability.
|
|
281
282
|
*/
|
|
282
283
|
listBundles() {
|
|
283
284
|
const manifest = readProfileManifest('dsh', this.profile.dir);
|
|
284
285
|
const exemptions = readProfileVersionExemptions(this.profile.dir);
|
|
285
286
|
const selected = manifest.dsh?.profile?.bundles ?? [];
|
|
286
|
-
const
|
|
287
|
+
const recorded = manifest.dependencies ?? {};
|
|
288
|
+
const dependencies = Object.keys(recorded);
|
|
287
289
|
const installation = JSON.parse(readFileSync(this.profile.installAnchor, 'utf8'));
|
|
288
|
-
const
|
|
290
|
+
const catalog = new Map(OFFICIAL_ON_DEMAND_CATALOG.map(entry => [entry.packageName, entry]));
|
|
291
|
+
const names = [...new Set([...selected, ...dependencies, ...Object.keys(installation.dependencies ?? {}), ...catalog.keys()])];
|
|
289
292
|
const bundles = [];
|
|
293
|
+
const runtimeVersion = getDshRuntimeVersion();
|
|
290
294
|
for (const name of names) {
|
|
291
295
|
const installed = dependencies.includes(name);
|
|
292
296
|
const optional = OPTIONAL_BUNDLES.includes(name);
|
|
293
|
-
const removable = installed && !Object.hasOwn(installation.dependencies ?? {}, name);
|
|
294
297
|
const enabled = selected.includes(name);
|
|
298
|
+
const shipped = Object.hasOwn(installation.dependencies ?? {}, name);
|
|
299
|
+
const offered = catalog.get(name);
|
|
300
|
+
const official = optional || offered !== undefined;
|
|
301
|
+
let target = {};
|
|
302
|
+
let targetError;
|
|
303
|
+
if (offered !== undefined) {
|
|
304
|
+
try {
|
|
305
|
+
target = { installTarget: officialBundleInstallTarget(name, this.profile.installAnchor, runtimeVersion) };
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
targetError = managementError(error);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const catalogMeta = offered === undefined ? {} : { meta: offered.meta };
|
|
312
|
+
if (offered !== undefined && !installed && !shipped) {
|
|
313
|
+
bundles.push({ name, official, availability: 'missing', ...catalogMeta, ...target, enabled, installed, optional,
|
|
314
|
+
removable: enabled, rows: [], overrides: [],
|
|
315
|
+
...targetError !== undefined ? { error: targetError } : enabled ? { error: { code: 'not-bundle' } } : {} });
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
// Installation-owned copies remain protected even if a profile also declares their names.
|
|
319
|
+
const owned = installed && !shipped;
|
|
320
|
+
const removable = owned || (enabled && !installed && !shipped);
|
|
321
|
+
const sourceOf = (packageName) => owned ? { source: dependencySpec(name, recorded[name], this.profile.dir, packageName) } : {};
|
|
295
322
|
const readOnlyReason = this.protectsManager(name) ? 'management-required' : undefined;
|
|
323
|
+
let availability = 'missing';
|
|
324
|
+
let version;
|
|
296
325
|
try {
|
|
297
326
|
const info = bundleManifest(name, this.profile.dir, this.profile.installAnchor);
|
|
327
|
+
availability = shipped ? 'installation' : 'profile';
|
|
298
328
|
if (info === undefined) {
|
|
299
|
-
if (enabled)
|
|
300
|
-
bundles.push({ name,
|
|
301
|
-
...(
|
|
329
|
+
if (enabled || offered !== undefined)
|
|
330
|
+
bundles.push({ name, official, availability, ...catalogMeta, ...target,
|
|
331
|
+
...sourceOf(), enabled, installed, optional, removable: removable && readOnlyReason === undefined,
|
|
332
|
+
...(readOnlyReason === undefined ? {} : { readOnlyReason }), error: targetError ?? { code: 'not-bundle' }, rows: [], overrides: [] });
|
|
302
333
|
continue;
|
|
303
334
|
}
|
|
335
|
+
version = info.version;
|
|
304
336
|
const compatibility = evaluatePluginCompatibility(info, exemptions);
|
|
305
337
|
if (compatibility !== undefined && !compatibility.exempted)
|
|
306
338
|
throw new ManagementFailure('incompatible-version', [incompatiblePlugin(compatibility)]);
|
|
307
339
|
const dir = resolveBundleDir('dsh', name, this.profile.installAnchor, this.profile.dir);
|
|
308
|
-
const meta = readPluginMeta(info.name ?? name, pathToFileURL(join(dir, 'package.json')).href);
|
|
309
|
-
bundles.push({ name, ...(
|
|
340
|
+
const meta = offered?.meta ?? readPluginMeta(info.name ?? name, pathToFileURL(join(dir, 'package.json')).href);
|
|
341
|
+
bundles.push({ name, official, availability, ...target, ...(version === undefined ? {} : { version }),
|
|
310
342
|
...(info.description === undefined || info.description === '' ? {} : { description: info.description }),
|
|
311
343
|
...meta === undefined ? {} : { meta },
|
|
312
|
-
enabled, installed, optional, removable: removable && readOnlyReason === undefined,
|
|
344
|
+
...sourceOf(info.name), enabled, installed, optional, removable: removable && readOnlyReason === undefined,
|
|
313
345
|
...(readOnlyReason === undefined ? {} : { readOnlyReason }),
|
|
314
|
-
...this.declaredRows(name, info)
|
|
346
|
+
...this.declaredRows(name, info),
|
|
347
|
+
...targetError === undefined ? {} : { error: targetError } });
|
|
315
348
|
}
|
|
316
349
|
catch (error) {
|
|
317
|
-
if (enabled || installed) {
|
|
318
|
-
bundles.push({ name,
|
|
319
|
-
...(
|
|
350
|
+
if (enabled || installed || optional || offered !== undefined) {
|
|
351
|
+
bundles.push({ name, official, availability, ...catalogMeta, ...target, ...(version === undefined ? {} : { version }),
|
|
352
|
+
...sourceOf(), enabled, installed, optional, removable: removable && readOnlyReason === undefined,
|
|
353
|
+
...(readOnlyReason === undefined ? {} : { readOnlyReason }),
|
|
354
|
+
error: targetError ?? managementError(error), rows: [], overrides: [] });
|
|
320
355
|
}
|
|
321
356
|
}
|
|
322
357
|
}
|
|
@@ -456,7 +491,11 @@ let PluginManager = (() => {
|
|
|
456
491
|
setBundleEnabled(name, enabled) {
|
|
457
492
|
return this.change(result => this.configure(async () => {
|
|
458
493
|
await this.selectBundle(name, enabled);
|
|
494
|
+
if (enabled)
|
|
495
|
+
await this.refreshPackages();
|
|
459
496
|
result.warnings = await this.reload(enabled ? this.bundleRows(name).map(row => row.id) : []);
|
|
497
|
+
if (!enabled && this.ownerContext.get('hmr') !== undefined)
|
|
498
|
+
await this.refreshPackages();
|
|
460
499
|
}), { stage: 'enable', target: name, enabled }, 'bundle');
|
|
461
500
|
}
|
|
462
501
|
/**
|
|
@@ -467,7 +506,7 @@ let PluginManager = (() => {
|
|
|
467
506
|
* `package.json` and `pnpm-lock.yaml` as they were; downloaded files can stay.
|
|
468
507
|
* @param spec One package spec, including local paths relative to the invocation directory.
|
|
469
508
|
* @param options Whether to activate the installed bundle (defaults to true), the request id a cancellation names,
|
|
470
|
-
* the pending build scripts to allow
|
|
509
|
+
* the pending build scripts to allow, whether to save an exact dependency, and the registry asked first.
|
|
471
510
|
* @returns Package-manager diagnostics, the registries asked, and the observed activation outcome.
|
|
472
511
|
*/
|
|
473
512
|
installBundle(spec, options) {
|
|
@@ -492,6 +531,7 @@ let PluginManager = (() => {
|
|
|
492
531
|
const files = await this.readRestoredFiles();
|
|
493
532
|
const before = readProfileManifest('dsh', this.profile.dir).dependencies ?? {};
|
|
494
533
|
let name;
|
|
534
|
+
let version;
|
|
495
535
|
try {
|
|
496
536
|
result.registries = [];
|
|
497
537
|
const connection = checkGithubConnection(parsedForRegistry(spec), this.profile.dir, {
|
|
@@ -525,7 +565,7 @@ let PluginManager = (() => {
|
|
|
525
565
|
throw new InstallCancelledError();
|
|
526
566
|
result.registries.push(registry);
|
|
527
567
|
announce('installing', { registry, index: index + 1, total: plan.length });
|
|
528
|
-
run = await this.runPnpm(['add', spec, ...registryArguments(registry)], control.abort.signal, requestId);
|
|
568
|
+
run = await this.runPnpm(['add', spec, ...options?.saveExact === true ? ['--save-exact'] : [], ...registryArguments(registry)], control.abort.signal, requestId);
|
|
529
569
|
result.packageResult = run;
|
|
530
570
|
if (stopped())
|
|
531
571
|
throw new InstallCancelledError();
|
|
@@ -557,9 +597,9 @@ let PluginManager = (() => {
|
|
|
557
597
|
if (succeeded)
|
|
558
598
|
delete result.failedAt;
|
|
559
599
|
if (!succeeded) {
|
|
560
|
-
// pnpm-workspace.yaml is not restored, so the names
|
|
600
|
+
// pnpm-workspace.yaml is not restored, so the names it left undecided, or reported as ignored, are offered for approval.
|
|
561
601
|
try {
|
|
562
|
-
result.pendingBuilds = await
|
|
602
|
+
result.pendingBuilds = await recordPendingBuilds(this.profile.dir, run.output);
|
|
563
603
|
}
|
|
564
604
|
catch (error) {
|
|
565
605
|
this.ownerContext.logger.warn('Could not read pending build approvals after pnpm failed', error);
|
|
@@ -569,8 +609,11 @@ let PluginManager = (() => {
|
|
|
569
609
|
const after = readProfileManifest('dsh', this.profile.dir).dependencies ?? {};
|
|
570
610
|
const installed = Object.keys(after).filter(name => before[name] !== after[name]);
|
|
571
611
|
// Registry retries can retain the saved range after a partial installation.
|
|
572
|
-
if (installed.length === 0)
|
|
573
|
-
|
|
612
|
+
if (installed.length === 0) {
|
|
613
|
+
const parsed = parsedForRegistry(spec);
|
|
614
|
+
const localName = parsed.kind === 'path' ? readProfileManifest('dsh', parsed.path).name : undefined;
|
|
615
|
+
installed.push(...Object.keys(after).filter(name => name === localName || spec === name || spec.startsWith(`${name}@`)));
|
|
616
|
+
}
|
|
574
617
|
const target = installed[0];
|
|
575
618
|
if (installed.length !== 1 || target === undefined)
|
|
576
619
|
throw new ManagementFailure('ambiguous-install');
|
|
@@ -584,6 +627,7 @@ let PluginManager = (() => {
|
|
|
584
627
|
throw new ManagementFailure('incompatible-version', [incompatiblePlugin(compatibility)]);
|
|
585
628
|
for (const file of bundlePatchPaths(dir, manifest.dsh.bundle))
|
|
586
629
|
loadOverlayPatches('dsh', file);
|
|
630
|
+
version = manifest.version;
|
|
587
631
|
}
|
|
588
632
|
catch (error) {
|
|
589
633
|
// pnpm has exited by now, so the files it rewrote go back as they were.
|
|
@@ -593,6 +637,8 @@ let PluginManager = (() => {
|
|
|
593
637
|
control.phase = 'applying';
|
|
594
638
|
announce('applying');
|
|
595
639
|
result.bundle = name;
|
|
640
|
+
if (version !== undefined)
|
|
641
|
+
result.version = version;
|
|
596
642
|
result.target = name;
|
|
597
643
|
result.stage = 'enable';
|
|
598
644
|
return this.configure(async () => {
|
|
@@ -600,6 +646,7 @@ let PluginManager = (() => {
|
|
|
600
646
|
await this.selectBundle(name, true);
|
|
601
647
|
if (Object.hasOwn(before, name))
|
|
602
648
|
return 'restart-required';
|
|
649
|
+
await this.refreshPackages();
|
|
603
650
|
if (options?.enabled !== false)
|
|
604
651
|
result.warnings = await this.reload();
|
|
605
652
|
});
|
|
@@ -633,13 +680,14 @@ let PluginManager = (() => {
|
|
|
633
680
|
await control.result.then(() => undefined, () => undefined);
|
|
634
681
|
return { status: 'cancelled' };
|
|
635
682
|
}
|
|
636
|
-
/** Unload and remove a profile-owned bundle dependency through dsh plugin's pnpm path
|
|
637
|
-
*
|
|
683
|
+
/** Unload and remove a profile-owned bundle dependency through dsh plugin's pnpm path; a selected name no
|
|
684
|
+
* dependency holds is only deselected.
|
|
685
|
+
* @param name Installed dependency or selected bundle name.
|
|
638
686
|
* @returns Removal diagnostics and the remaining profile state.
|
|
639
687
|
*/
|
|
640
688
|
removeBundle(name) {
|
|
641
689
|
return this.change(async (result) => {
|
|
642
|
-
await this.configure(async () => {
|
|
690
|
+
const installed = await this.configure(async () => {
|
|
643
691
|
const bundle = (await this.listBundles()).find(item => item.name === name);
|
|
644
692
|
if (bundle === undefined || !bundle.removable)
|
|
645
693
|
throw new ManagementFailure('not-removable');
|
|
@@ -657,11 +705,15 @@ let PluginManager = (() => {
|
|
|
657
705
|
&& contributions.some(row => row.id === entry.options.id && row.name === entry.options.name))) {
|
|
658
706
|
throw new ManagementFailure('bundle-in-use');
|
|
659
707
|
}
|
|
708
|
+
return bundle.installed;
|
|
660
709
|
});
|
|
710
|
+
if (!installed)
|
|
711
|
+
return;
|
|
661
712
|
result.packageResult = await this.runPnpm(['remove', name]);
|
|
662
713
|
if (result.packageResult.exitCode !== 0 || result.packageResult.timedOut === true) {
|
|
663
714
|
throw new Error(result.packageResult.output);
|
|
664
715
|
}
|
|
716
|
+
await this.configure(() => this.refreshPackages());
|
|
665
717
|
}, { stage: 'remove', target: name }, 'remove');
|
|
666
718
|
}
|
|
667
719
|
/** The rows a bundle's patch inserts and the existing rows it changes; an unreadable patch throws. */
|
|
@@ -684,7 +736,8 @@ let PluginManager = (() => {
|
|
|
684
736
|
const rows = [];
|
|
685
737
|
const packages = this.ctx.get('pluginPackages');
|
|
686
738
|
for (const row of flatten(composeEntries([patches.filter(item => item.insert !== undefined)]))) {
|
|
687
|
-
|
|
739
|
+
// Group rows are containers without a plugin inventory entry; their children carry the switches.
|
|
740
|
+
if (typeof row.id !== 'string' || typeof row.name !== 'string' || row.group === true)
|
|
688
741
|
continue;
|
|
689
742
|
const active = live.get(row.id);
|
|
690
743
|
const entryId = active?.entryId;
|
|
@@ -806,6 +859,15 @@ let PluginManager = (() => {
|
|
|
806
859
|
const apply = () => { this.abort.signal.throwIfAborted(); return operation(); };
|
|
807
860
|
return hmr === undefined ? apply() : hmr.runExclusive(apply);
|
|
808
861
|
}
|
|
862
|
+
async refreshPackages() {
|
|
863
|
+
if (this.ownerContext.get('hmr') === undefined) {
|
|
864
|
+
const selected = readProfileManifest('dsh', this.profile.dir).dsh?.profile?.bundles ?? [];
|
|
865
|
+
// Deselected startup bundles still run without HMR and need the existing package table.
|
|
866
|
+
if (this.profile.startedBundles.some(name => !selected.includes(name)))
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
await this.ownerContext.get('pluginPackages')?.refresh();
|
|
870
|
+
}
|
|
809
871
|
async reload(requiredIds = []) {
|
|
810
872
|
if (this.ownerContext.get('hmr') === undefined)
|
|
811
873
|
return [];
|
|
@@ -47,4 +47,17 @@ export declare class InvalidInstallSpecError extends Error {
|
|
|
47
47
|
* registry would refuse, or a URL that is neither a git host nor a tarball.
|
|
48
48
|
*/
|
|
49
49
|
export declare function parseInstallSpec(raw: string): ParsedInstallSpec;
|
|
50
|
+
/**
|
|
51
|
+
* The spec `pnpm add` accepts for a dependency the profile manifest records. A git address, URL, or other
|
|
52
|
+
* non-registry protocol is the recorded value without the user information of an http(s) URL, whose query string
|
|
53
|
+
* is kept; a `file:` or `link:` path, which pnpm records relative to the profile or with `~` for the home
|
|
54
|
+
* directory, becomes absolute. A registry range, tag, or alias follows the dependency name after `@`, as does a
|
|
55
|
+
* non-registry value installed under a name other than the package's own.
|
|
56
|
+
* @param name - the dependency name.
|
|
57
|
+
* @param recorded - the value the profile manifest records for it.
|
|
58
|
+
* @param profileDir - the profile directory a relative local path is resolved against.
|
|
59
|
+
* @param packageName - the name the installed package's manifest declares, when it could be read.
|
|
60
|
+
* @returns the installable spec.
|
|
61
|
+
*/
|
|
62
|
+
export declare function dependencySpec(name: string, recorded: string, profileDir: string, packageName?: string): string;
|
|
50
63
|
//# sourceMappingURL=install-spec.d.ts.map
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* takes, and for a registry name whether it is one the registry can accept.
|
|
4
4
|
* @module @deepseek-ai/dsh-plugin-manager/install-spec
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
7
8
|
/** The forms pnpm resolves through a git host: a host shorthand, a git URL, or a hosted repository URL. */
|
|
8
9
|
const GIT_SHORTHAND = /^(?:github|gitlab|bitbucket|gist):/i;
|
|
9
10
|
const GIT_URL = /^git(?:\+[a-z]+)?:\/\/|^git@[^:]+:/i;
|
|
@@ -84,4 +85,35 @@ export function parseInstallSpec(raw) {
|
|
|
84
85
|
throw invalid(spec, 'a version after @ must not be empty');
|
|
85
86
|
return range === undefined ? { kind: 'registry', spec, name } : { kind: 'registry', spec, name, range };
|
|
86
87
|
}
|
|
88
|
+
/** A recorded dependency value pnpm resolves outside a registry: a protocol other than a registry alias, or an scp-like git address. */
|
|
89
|
+
const NON_REGISTRY_VALUE = /^(?!(?:npm|jsr|workspace|catalog):)[a-z][a-z0-9+.-]*:|^[^@/:\s]+@[^:/\s]+:/i;
|
|
90
|
+
/** The user information of an http(s) URL, up to the last `@` before the path, which holds a password or access token when present. */
|
|
91
|
+
const HTTP_USER_INFO = /^((?:git\+)?https?:\/\/)[^/]*@/i;
|
|
92
|
+
/**
|
|
93
|
+
* The spec `pnpm add` accepts for a dependency the profile manifest records. A git address, URL, or other
|
|
94
|
+
* non-registry protocol is the recorded value without the user information of an http(s) URL, whose query string
|
|
95
|
+
* is kept; a `file:` or `link:` path, which pnpm records relative to the profile or with `~` for the home
|
|
96
|
+
* directory, becomes absolute. A registry range, tag, or alias follows the dependency name after `@`, as does a
|
|
97
|
+
* non-registry value installed under a name other than the package's own.
|
|
98
|
+
* @param name - the dependency name.
|
|
99
|
+
* @param recorded - the value the profile manifest records for it.
|
|
100
|
+
* @param profileDir - the profile directory a relative local path is resolved against.
|
|
101
|
+
* @param packageName - the name the installed package's manifest declares, when it could be read.
|
|
102
|
+
* @returns the installable spec.
|
|
103
|
+
*/
|
|
104
|
+
export function dependencySpec(name, recorded, profileDir, packageName = name) {
|
|
105
|
+
const local = /^(file|link):(.*)$/s.exec(recorded);
|
|
106
|
+
let spec;
|
|
107
|
+
if (local !== null) {
|
|
108
|
+
const path = local[2].replace(/^~(?=$|[\\/])/, () => homedir());
|
|
109
|
+
spec = `${local[1]}:${resolve(profileDir, path)}`;
|
|
110
|
+
}
|
|
111
|
+
else if (NON_REGISTRY_VALUE.test(recorded)) {
|
|
112
|
+
spec = recorded.replace(HTTP_USER_INFO, '$1');
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
return `${name}@${recorded}`;
|
|
116
|
+
}
|
|
117
|
+
return packageName === name ? spec : `${name}@${spec}`;
|
|
118
|
+
}
|
|
87
119
|
//# sourceMappingURL=install-spec.js.map
|
|
@@ -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
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** Select Official bundle packages from the running installation without importing provider code. */
|
|
2
|
+
import { existsSync, globSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { bundlePatchPaths } from '@deepseek-ai/dsh-app-boot';
|
|
5
|
+
function manifestAt(file) {
|
|
6
|
+
return JSON.parse(readFileSync(file, 'utf8'));
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Offer a source checkout link or an exact published package version. Source installations are
|
|
10
|
+
* identified by the real CLI manifest and its unpacked workspace dependencies, including Desktop's
|
|
11
|
+
* development symlink. An incomplete checkout is an error, never a registry fallback.
|
|
12
|
+
* @param name - Official catalog package name.
|
|
13
|
+
* @param installAnchor - Running application's DSH package.json, possibly reached through a symlink.
|
|
14
|
+
* @param version - Running DSH version used by its release family.
|
|
15
|
+
* @returns An ordinary package-manager spec and its package version.
|
|
16
|
+
* @throws when a source installation lacks its workspace package, bundle patch, or built runtime.
|
|
17
|
+
*/
|
|
18
|
+
export function officialBundleInstallTarget(name, installAnchor, version) {
|
|
19
|
+
const anchor = realpathSync(installAnchor);
|
|
20
|
+
const installation = manifestAt(anchor);
|
|
21
|
+
const source = installation.name === '@deepseek-ai/dsh'
|
|
22
|
+
&& Object.values(installation.dependencies ?? {}).some(value => value.startsWith('workspace:'));
|
|
23
|
+
if (!source)
|
|
24
|
+
return { spec: `${name}@${version}`, version };
|
|
25
|
+
const root = resolve(dirname(anchor), '../..');
|
|
26
|
+
if (anchor !== join(root, 'apps', 'cli', 'package.json')
|
|
27
|
+
|| !existsSync(join(root, 'package.json'))
|
|
28
|
+
|| manifestAt(join(root, 'package.json')).name !== '@deepseek-ai/dsh-root') {
|
|
29
|
+
throw new Error(`Official bundle ${name}: source DSH installation ${anchor} requires its complete workspace checkout`);
|
|
30
|
+
}
|
|
31
|
+
const matches = globSync('packages/*/*/package.json', { cwd: root })
|
|
32
|
+
.map(file => ({ file: join(root, file), manifest: manifestAt(join(root, file)) }))
|
|
33
|
+
.filter(entry => entry.manifest.name === name);
|
|
34
|
+
const selected = matches[0];
|
|
35
|
+
if (matches.length !== 1 || selected === undefined) {
|
|
36
|
+
throw new Error(`Official bundle ${name}: expected one workspace package in ${root}, found ${matches.length}; restore the checkout and run pnpm install`);
|
|
37
|
+
}
|
|
38
|
+
const { file, manifest } = selected;
|
|
39
|
+
const directory = realpathSync(dirname(file));
|
|
40
|
+
if (manifest.version !== version || manifest.private === true || manifest.dsh?.bundle === undefined) {
|
|
41
|
+
throw new Error(`Official bundle ${name}: ${file} must declare a public bundle at DSH version ${version}`);
|
|
42
|
+
}
|
|
43
|
+
const main = typeof manifest.main === 'string' ? resolve(directory, manifest.main) : undefined;
|
|
44
|
+
if (main === undefined || !existsSync(main) || !statSync(main).isFile()) {
|
|
45
|
+
throw new Error(`Official bundle ${name}: built runtime is missing in ${directory}; run pnpm install and pnpm run build in ${root}`);
|
|
46
|
+
}
|
|
47
|
+
for (const patch of bundlePatchPaths(directory, manifest.dsh.bundle)) {
|
|
48
|
+
if (!existsSync(patch) || !statSync(patch).isFile()) {
|
|
49
|
+
throw new Error(`Official bundle ${name}: bundle patch ${patch} is missing; restore the checkout`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return { spec: `link:${directory}`, version };
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=official-install-target.js.map
|
package/lib/types/tools.js
CHANGED
|
@@ -14,7 +14,7 @@ export function apply(ctx) {
|
|
|
14
14
|
description: 'List plugins or bundles in the current profile, enable or disable them, install a bundle, or remove an installed bundle. Every action requires danger-full-access permission or approval for this call. Approval does not change the session permission mode. Changes affect every session in this profile. List first to obtain exact identifiers. Package installation can execute allowed build scripts. Live profiles apply changes immediately; startup profiles require restart. Incompatible DSH peer dependencies block installation and activation. Version exemptions risk crashes and data loss: warn the user and obtain explicit permission for the exact plugin and runtime versions before granting one.',
|
|
15
15
|
parameters: {
|
|
16
16
|
action: { type: 'string', required: true, enum: ['list_plugins', 'list_bundles', 'set_plugin', 'set_bundle', 'install_bundle', 'remove_bundle', 'list_version_exemptions', 'set_version_exemption'], description: 'Management operation.' },
|
|
17
|
-
target: { type: 'string', description: 'Plugin entry id, bundle package name, or installation spec, according to action.' },
|
|
17
|
+
target: { type: 'string', description: 'Plugin entry id, bundle package name, or installation spec, according to action. For an Official catalog entry, pass list_bundles installTarget.spec to install_bundle.' },
|
|
18
18
|
enabled: { type: 'boolean', description: 'Required for set operations; defaults to true for installation. For set_version_exemption, true grants and false revokes.' },
|
|
19
19
|
runtimeVersion: { type: 'string', description: 'For set_version_exemption: exact DSH version from list_version_exemptions. Target must be the manifest package-name@version, not an alias or version range.' },
|
|
20
20
|
acceptRisk: { type: 'boolean', description: 'For granting an exemption: true only after warning the user about possible crashes and data loss and receiving explicit permission for this exact plugin/runtime pair. General installation permission is not enough.' },
|
package/lib/types/types.d.ts
CHANGED
|
@@ -40,9 +40,18 @@ export interface BundleRowInfo {
|
|
|
40
40
|
/** The Loader entry carrying this row, when exactly one live entry has its id. */
|
|
41
41
|
entryId?: PluginEntryId;
|
|
42
42
|
}
|
|
43
|
-
/** One installed or
|
|
43
|
+
/** One shipped, profile-installed, or discoverable on-demand bundle. */
|
|
44
44
|
export interface BundleInfo {
|
|
45
45
|
name: string;
|
|
46
|
+
/** Project-maintained shipped optional bundle or on-demand catalog entry, independent of installation and selection. */
|
|
47
|
+
official: boolean;
|
|
48
|
+
/** Readable package location; on-demand catalog entries exclude undeclared transitive copies, and declared files may be missing. */
|
|
49
|
+
availability: 'installation' | 'profile' | 'missing';
|
|
50
|
+
/** Exact released Host version or same-checkout development link offered for an on-demand catalog entry. */
|
|
51
|
+
installTarget?: {
|
|
52
|
+
spec: string;
|
|
53
|
+
version: string;
|
|
54
|
+
};
|
|
46
55
|
version?: string;
|
|
47
56
|
/** Local display text with available translations or literal fallbacks, or a metadata diagnostic. */
|
|
48
57
|
meta?: PluginLocalizedMeta;
|
|
@@ -50,8 +59,13 @@ export interface BundleInfo {
|
|
|
50
59
|
description?: string;
|
|
51
60
|
/** Selected in the profile manifest; a load error means its layer was skipped. */
|
|
52
61
|
enabled: boolean;
|
|
53
|
-
/** Whether the profile
|
|
62
|
+
/** Whether the profile declares this dependency; availability separately reports whether its files can be read. */
|
|
54
63
|
installed: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Present for a profile dependency the installation does not also supply: the spec `pnpm add` accepts, with local
|
|
66
|
+
* paths made absolute and the user information of an http(s) URL removed.
|
|
67
|
+
*/
|
|
68
|
+
source?: string;
|
|
55
69
|
/**
|
|
56
70
|
* Whether the installation ships the bundle for the person to switch on: named by the launcher's `OPTIONAL_BUNDLES`,
|
|
57
71
|
* held by the installation's dependencies, selected by no shipped template, and never removable.
|
|
@@ -104,6 +118,8 @@ export interface ChangeResult {
|
|
|
104
118
|
packageResult?: PackageResult;
|
|
105
119
|
/** The bundle an installation added, once pnpm and the bundle check accepted it. */
|
|
106
120
|
bundle?: string;
|
|
121
|
+
/** The installed bundle's manifest version, when declared; pnpm's `minimumReleaseAge` can make it older than the newest release. */
|
|
122
|
+
version?: string;
|
|
107
123
|
/** Exact package names awaiting explicit script approval in the profile's pnpm settings, read after a failed run. */
|
|
108
124
|
pendingBuilds?: string[];
|
|
109
125
|
/** Package script permissions saved before this installation attempt. */
|
|
@@ -121,6 +137,8 @@ export type PluginInstallRequestId = Branded<'PluginInstallRequestId'>;
|
|
|
121
137
|
/** Bundle installation defaults to activation; callers that offer cancellation supply their request id. */
|
|
122
138
|
export interface InstallBundleOptions {
|
|
123
139
|
enabled?: boolean;
|
|
140
|
+
/** Save the resolved dependency version without a range; applies to any bundle installation. */
|
|
141
|
+
saveExact?: boolean;
|
|
124
142
|
requestId?: PluginInstallRequestId;
|
|
125
143
|
/** Explicitly allow these pending packages' scripts for this profile, then install; a name no longer pending refuses the call. */
|
|
126
144
|
approvedBuilds?: string[];
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-plugin-manager",
|
|
3
3
|
"description": "Current-profile plugin and bundle management shared by dsh CLI, Web and agent tools",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.1-alpha.2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -56,26 +56,26 @@
|
|
|
56
56
|
],
|
|
57
57
|
"license": "MIT",
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"
|
|
59
|
+
"@deepseek-ai/dsh-app-boot": "0.2.1-alpha.2",
|
|
60
|
+
"@deepseek-ai/dsh-atomic-write": "0.2.1-alpha.2",
|
|
61
|
+
"@deepseek-ai/schemastery": "~3.18.5-alpha.1",
|
|
62
|
+
"execa": "^10.0.1",
|
|
60
63
|
"yaml": "^2.9.0",
|
|
61
64
|
"zod": "^4.4.3",
|
|
62
|
-
"@deepseek-ai/dsh-
|
|
63
|
-
"@deepseek-ai/dsh-atomic-write": "0.2.0-rc.2",
|
|
64
|
-
"@deepseek-ai/schemastery": "~3.18.4",
|
|
65
|
-
"@deepseek-ai/dsh-util-values": "0.2.0-rc.2"
|
|
65
|
+
"@deepseek-ai/dsh-util-values": "0.2.1-alpha.2"
|
|
66
66
|
},
|
|
67
67
|
"peerDependencies": {
|
|
68
|
-
"@deepseek-ai/cordis": "~4.0.
|
|
69
|
-
"@deepseek-ai/cordis-plugin-loader": "~1.0.
|
|
70
|
-
"@deepseek-ai/dsh-host-plugin-inventory": "0.2.
|
|
71
|
-
"@deepseek-ai/dsh-subprocess": "0.2.
|
|
72
|
-
"@deepseek-ai/dsh-typert-protocol": "0.2.
|
|
73
|
-
"@deepseek-ai/dsh-tools": "0.2.
|
|
74
|
-
"@deepseek-ai/dsh-
|
|
75
|
-
"@deepseek-ai/dsh-
|
|
76
|
-
"@deepseek-ai/dsh-sandbox": "0.2.
|
|
77
|
-
"@deepseek-ai/dsh-sandbox
|
|
78
|
-
"@deepseek-ai/dsh-user-approval": "0.2.
|
|
68
|
+
"@deepseek-ai/cordis": "~4.0.5-alpha.1",
|
|
69
|
+
"@deepseek-ai/cordis-plugin-loader": "~1.0.6-alpha.1",
|
|
70
|
+
"@deepseek-ai/dsh-host-plugin-inventory": "0.2.1-alpha.2",
|
|
71
|
+
"@deepseek-ai/dsh-subprocess": "0.2.1-alpha.2",
|
|
72
|
+
"@deepseek-ai/dsh-typert-protocol": "0.2.1-alpha.2",
|
|
73
|
+
"@deepseek-ai/dsh-tools": "0.2.1-alpha.2",
|
|
74
|
+
"@deepseek-ai/dsh-hmr": "0.2.1-alpha.2",
|
|
75
|
+
"@deepseek-ai/dsh-brand": "0.2.1-alpha.2",
|
|
76
|
+
"@deepseek-ai/dsh-sandbox-policy": "0.2.1-alpha.2",
|
|
77
|
+
"@deepseek-ai/dsh-sandbox": "0.2.1-alpha.2",
|
|
78
|
+
"@deepseek-ai/dsh-user-approval": "0.2.1-alpha.2"
|
|
79
79
|
},
|
|
80
80
|
"peerDependenciesMeta": {
|
|
81
81
|
"@deepseek-ai/dsh-hmr": {
|
|
@@ -86,22 +86,23 @@
|
|
|
86
86
|
}
|
|
87
87
|
},
|
|
88
88
|
"devDependencies": {
|
|
89
|
-
"
|
|
90
|
-
"@deepseek-ai/
|
|
91
|
-
"@deepseek-ai/
|
|
92
|
-
"@deepseek-ai/
|
|
93
|
-
"@deepseek-ai/dsh-
|
|
94
|
-
"@deepseek-ai/dsh-
|
|
95
|
-
"@deepseek-ai/dsh-
|
|
96
|
-
"@deepseek-ai/
|
|
97
|
-
"@deepseek-ai/
|
|
98
|
-
"@deepseek-ai/dsh-
|
|
99
|
-
"@deepseek-ai/
|
|
100
|
-
"@deepseek-ai/dsh-
|
|
101
|
-
"@deepseek-ai/dsh-
|
|
102
|
-
"@deepseek-ai/dsh-
|
|
103
|
-
"@deepseek-ai/dsh-
|
|
104
|
-
"@deepseek-ai/dsh-
|
|
105
|
-
"@deepseek-ai/dsh-
|
|
89
|
+
"chokidar": "^4.0.3",
|
|
90
|
+
"@deepseek-ai/dsh-package-manifest": "0.2.1-alpha.2",
|
|
91
|
+
"@deepseek-ai/cordis": "~4.0.5-alpha.1",
|
|
92
|
+
"@deepseek-ai/cordis-plugin-loader": "~1.0.6-alpha.1",
|
|
93
|
+
"@deepseek-ai/dsh-host-plugin-inventory": "0.2.1-alpha.2",
|
|
94
|
+
"@deepseek-ai/dsh-subprocess": "0.2.1-alpha.2",
|
|
95
|
+
"@deepseek-ai/dsh-typert-protocol": "0.2.1-alpha.2",
|
|
96
|
+
"@deepseek-ai/dsh-tools": "0.2.1-alpha.2",
|
|
97
|
+
"@deepseek-ai/dsh-system-prompt": "0.2.1-alpha.2",
|
|
98
|
+
"@deepseek-ai/dsh-hmr": "0.2.1-alpha.2",
|
|
99
|
+
"@deepseek-ai/cordis-plugin-timer": "~1.1.7-alpha.1",
|
|
100
|
+
"@deepseek-ai/dsh-brand": "0.2.1-alpha.2",
|
|
101
|
+
"@deepseek-ai/dsh-sandbox-policy": "0.2.1-alpha.2",
|
|
102
|
+
"@deepseek-ai/dsh-agent": "0.2.1-alpha.2",
|
|
103
|
+
"@deepseek-ai/dsh-session": "0.2.1-alpha.2",
|
|
104
|
+
"@deepseek-ai/dsh-session-projection": "0.2.1-alpha.2",
|
|
105
|
+
"@deepseek-ai/dsh-sandbox": "0.2.1-alpha.2",
|
|
106
|
+
"@deepseek-ai/dsh-user-approval": "0.2.1-alpha.2"
|
|
106
107
|
}
|
|
107
108
|
}
|