dsh-plugin-shop 0.8.3 → 0.8.4-beta.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.md +11 -8
- package/lib/client.js +1282 -443
- package/lib/index.js +2296 -250
- package/lib/typert.host.js +69 -22
- package/lib/typert.remote-client.js +52 -13
- package/lib/types/client/ShopTab.d.ts +19 -2
- package/lib/types/client/locales.d.ts +44 -2
- package/lib/types/client/module-table.d.ts +131 -0
- package/lib/types/client/present.d.ts +127 -2
- package/lib/types/client/restart-monitor.d.ts +114 -0
- package/lib/types/host/activation.d.ts +12 -9
- package/lib/types/host/bundle-patch.d.ts +35 -0
- package/lib/types/host/catalog.d.ts +9 -3
- package/lib/types/host/compatibility.d.ts +223 -0
- package/lib/types/host/dsh-cli.d.ts +8 -0
- package/lib/types/host/executor.d.ts +50 -3
- package/lib/types/host/harness.d.ts +109 -0
- package/lib/types/host/hot.d.ts +8 -8
- package/lib/types/host/index.d.ts +259 -29
- package/lib/types/host/install.d.ts +1 -1
- package/lib/types/host/manager-runner.d.ts +51 -0
- package/lib/types/host/peers.d.ts +181 -10
- package/lib/types/host/plugin-manager.d.ts +100 -0
- package/lib/types/host/profile.d.ts +61 -4
- package/lib/types/host/restart.d.ts +5 -1
- package/lib/types/host/types.d.ts +27 -1
- package/lib/types/own-version.d.ts +18 -2
- package/package.json +2 -1
|
@@ -7,12 +7,151 @@ import { type EntryIdentity } from '../shared/identity.ts';
|
|
|
7
7
|
* drive every verdict and exactly one call site touches the filesystem. */
|
|
8
8
|
export type PeerResolver = (spec: string) => boolean;
|
|
9
9
|
/**
|
|
10
|
-
* The
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* The two filesystem reads the package lookup makes. Injected so fixtures can
|
|
11
|
+
* drive every branch of the walk — including a permission failure, which a
|
|
12
|
+
* real directory cannot stage when the suite runs as root — while
|
|
13
|
+
* `NODE_LOOKUP_FS` below stays the one place this module touches the disk.
|
|
14
|
+
*/
|
|
15
|
+
export interface PackageLookupFs {
|
|
16
|
+
/** `statSync`'s contract: follows symlinks, and throws an errno error
|
|
17
|
+
* (`code` set) for a path it cannot stat. */
|
|
18
|
+
stat(path: string): {
|
|
19
|
+
isDirectory(): boolean;
|
|
20
|
+
isFile(): boolean;
|
|
21
|
+
};
|
|
22
|
+
/** `readFileSync(path, 'utf8')`'s contract: throws for a file it cannot read. */
|
|
23
|
+
readFile(path: string): string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The directory `spec` is installed at as seen from `fromDir`, or null when
|
|
27
|
+
* it is not installed there: the walk's match (`findPackageDir`), and only
|
|
28
|
+
* when that directory holds a `package.json` that stats as a FILE. Anything
|
|
29
|
+
* else is ABSENT — a match with no manifest, a manifest that is not a file,
|
|
30
|
+
* one whose stat fails — because the loader matches that directory and the
|
|
31
|
+
* import fails there. The case this exists for: an uninstall that stops
|
|
32
|
+
* part-way (on Windows a locked file is enough) can leave
|
|
33
|
+
* `<profile>/node_modules/L/` holding nothing but a nested `node_modules/`,
|
|
34
|
+
* and a plugin declaring L then fails with ERR_MODULE_NOT_FOUND; while any
|
|
35
|
+
* directory counted as present, the badge naming L disappeared. The lookup
|
|
36
|
+
* before that one, `require.resolve('<spec>/package.json')`, required the
|
|
37
|
+
* manifest too.
|
|
38
|
+
*
|
|
39
|
+
* The manifest is stat-ed and never read here: presence is the file, and
|
|
40
|
+
* what it says is the version resolver's question. So a manifest that exists
|
|
41
|
+
* but cannot be read or parsed is a present package with no readable version.
|
|
42
|
+
*
|
|
43
|
+
* One edge is knowingly given up: a directory holding `index.js` and no
|
|
44
|
+
* manifest DOES import — Node's legacy main resolution tries `index.js` in a
|
|
45
|
+
* match without a manifest — but reads absent here. No package manager
|
|
46
|
+
* installs that shape.
|
|
47
|
+
*
|
|
48
|
+
* Throws only for a `spec` that is not a bare package name. This one lookup
|
|
49
|
+
* is behind the presence resolver, the version resolver, and the running
|
|
50
|
+
* harness's own `@deepseek-ai/dsh-app-boot` (`harness.ts`).
|
|
51
|
+
*/
|
|
52
|
+
export declare function packageDirectory(fromDir: string, spec: string, fs?: PackageLookupFs): string | null;
|
|
53
|
+
/** `nodeResolver` over an injected filesystem, walking up from `fromDir` —
|
|
54
|
+
* the seam fixtures drive the lookup through. */
|
|
55
|
+
export declare function packageResolver(fromDir: string, fs: PackageLookupFs): PeerResolver;
|
|
56
|
+
/**
|
|
57
|
+
* The production resolver, anchored at the profile. A peer is present when
|
|
58
|
+
* the first ancestor `node_modules/<spec>` that is a directory holds a
|
|
59
|
+
* `package.json` file — the directory the ESM resolver that loads plugins'
|
|
60
|
+
* host halves matches for a bare package, and a manifest in it
|
|
61
|
+
* (`findPackageDir`, `packageDirectory`). From `<profile>/cordis.yml` the
|
|
62
|
+
* walk reads `<profile>/node_modules`, then `$DSH_HOME/profiles/node_modules`
|
|
63
|
+
* — the link farm dsh-app-boot's `healProfilesModuleFallback` keeps, pointing
|
|
64
|
+
* into the global dsh install, on 0.1.5; 0.1.7 keeps none, and on a harness
|
|
65
|
+
* that serves its packages another way `harnessPackageResolver` asks it in
|
|
66
|
+
* front of this walk — and so on up to the root, past any candidate
|
|
67
|
+
* it cannot stat, as Node's reader walks past one (measured on Node 26.6.0
|
|
68
|
+
* with a looping link in front of a real copy: the import loads the copy).
|
|
69
|
+
* The one shape where this and the loader knowingly disagree is a directory
|
|
70
|
+
* holding `index.js` and no manifest, which imports and reads absent here;
|
|
71
|
+
* no package manager installs it (`packageDirectory`).
|
|
72
|
+
*
|
|
73
|
+
* It used to ask `require.resolve('<spec>/package.json')`, a question that
|
|
74
|
+
* answered wrongly twice:
|
|
75
|
+
*
|
|
76
|
+
* - Under a packaged dsh executable (`process.pkg`) the link farm holds ESM
|
|
77
|
+
* proxy packages instead of symlinks, and a proxy's `exports` leaves
|
|
78
|
+
* `./package.json` out on purpose, so resolving it threw
|
|
79
|
+
* ERR_PACKAGE_PATH_NOT_EXPORTED for every proxied peer. Presence read that
|
|
80
|
+
* as present; the version resolver below could read nothing. Reproduced:
|
|
81
|
+
* a proxy at `0.2.0-rc.1` against a declared `^0.1.1-rc.2` gave presence
|
|
82
|
+
* true, version null, mismatches `[]` — the load-time self-check could
|
|
83
|
+
* never fire under a packaged dsh.
|
|
84
|
+
* - Node caches a SUCCESSFUL CJS resolution for the life of the process
|
|
85
|
+
* (`Module._pathCache`, keyed on request + lookup paths) and hands the
|
|
86
|
+
* cached filename back without looking at the disk; a failure is not
|
|
87
|
+
* cached. Measured on Node 26.6.0 in one process: absent → false,
|
|
88
|
+
* installed → true, uninstalled → STILL true. A peer the reader removed
|
|
89
|
+
* left every plugin declaring it unflagged until dsh restarted.
|
|
90
|
+
*
|
|
91
|
+
* So nothing is cached and `exports` is never consulted: every call stats
|
|
92
|
+
* afresh, and a package with a manifest is present whatever its `exports`
|
|
93
|
+
* says. `stat` follows symlinks, so a link to nothing reads absent — which
|
|
94
|
+
* matters, because the link farm adds links and never prunes them: 29 of its
|
|
95
|
+
* 511 dangled on the machine this was measured on, after upgrades.
|
|
96
|
+
*
|
|
97
|
+
* The global folders — `NODE_PATH`, `~/.node_modules`, `~/.node_libraries`,
|
|
98
|
+
* `$PREFIX/lib/node` — are deliberately not searched: the ESM resolver that
|
|
99
|
+
* loads plugin host code does not search them either. That is not academic.
|
|
100
|
+
* pnpm's bin shims export `NODE_PATH` (this repo's own `node_modules/.bin/
|
|
101
|
+
* vitest` points it into the virtual store), and under one the old lookup
|
|
102
|
+
* found harness packages from any anchor at all, a temp directory included.
|
|
103
|
+
*
|
|
104
|
+
* It throws only for a `spec` that is not a bare package name, and
|
|
105
|
+
* `incompatibilityMap` turns the throw into no verdict: a hostile name is not
|
|
106
|
+
* a fact about this installation, and false is an accusation. Nothing on the
|
|
107
|
+
* filesystem makes it throw. It used to throw on a stat failure other than
|
|
108
|
+
* ENOENT or ENOTDIR as well, reasoning that such a candidate "may hold the
|
|
109
|
+
* package" — but the loader never asks what a candidate it cannot stat holds;
|
|
110
|
+
* it walks on (`findPackageDir`), so the throw silenced true badges and
|
|
111
|
+
* established nothing.
|
|
14
112
|
*/
|
|
15
113
|
export declare function nodeResolver(baseUrl: string): PeerResolver;
|
|
114
|
+
/**
|
|
115
|
+
* The one method of dsh's `pluginPackages` service the peer check reads: the
|
|
116
|
+
* harness's own answer to "which package does this import reach?".
|
|
117
|
+
*
|
|
118
|
+
* dsh 0.1.7 stopped mirroring its installation into
|
|
119
|
+
* `$DSH_HOME/profiles/node_modules`. It serves those packages to every
|
|
120
|
+
* profile through Node's module hooks instead — its runtime resolution
|
|
121
|
+
* routes a plugin's bare import of any package in the installation's
|
|
122
|
+
* dependency closure to the package that declares it — so the walk
|
|
123
|
+
* `nodeResolver` makes, which reads the disk around the profile, finds none
|
|
124
|
+
* of them. Measured on 0.1.7-rc.2 against the live catalog, the walk read
|
|
125
|
+
* `@deepseek-ai/dsh-tools` as missing for 1,038 entries, and the shop badged
|
|
126
|
+
* 2,214 where 0.1.5-rc.3 badges 570 (design
|
|
127
|
+
* 2026-09-01-harness-compatibility §11). `packageOf` answers from that
|
|
128
|
+
* resolution for the importer `parentURL` names; 0.1.5 has no such service.
|
|
129
|
+
*
|
|
130
|
+
* Typed loosely because the answer crosses in from the harness: only a
|
|
131
|
+
* string `manifestPath` is used.
|
|
132
|
+
*/
|
|
133
|
+
export interface HarnessPackageLookup {
|
|
134
|
+
packageOf(specifier: string, parentURL: string): {
|
|
135
|
+
manifestPath?: unknown;
|
|
136
|
+
} | undefined;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Presence on a harness that serves its packages through its runtime
|
|
140
|
+
* resolution (`HarnessPackageLookup`): present when the harness reaches a
|
|
141
|
+
* package for `spec` from `parentURL`, the profile anchor, and otherwise
|
|
142
|
+
* whatever `walk` answers — `nodeResolver` over the same anchor, in
|
|
143
|
+
* production. Asking the harness first asks the loader's own question, since
|
|
144
|
+
* a plugin's import goes through the same resolution. The walk stays behind
|
|
145
|
+
* it because either answer can only clear a badge, never raise one, and a
|
|
146
|
+
* false badge is the failure this module exists to avoid.
|
|
147
|
+
*
|
|
148
|
+
* Throws, which `incompatibilityMap` turns into no verdict for the entries
|
|
149
|
+
* declaring `spec`, for a name that is not a bare package name and for
|
|
150
|
+
* anything the service throws. A harness that cannot answer is silence:
|
|
151
|
+
* falling back to the walk alone would read every package it serves as
|
|
152
|
+
* missing.
|
|
153
|
+
*/
|
|
154
|
+
export declare function harnessPackageResolver(walk: PeerResolver, lookup: HarnessPackageLookup, parentURL: string, fs?: PackageLookupFs): PeerResolver;
|
|
16
155
|
/**
|
|
17
156
|
* Package name → the peer names that did not resolve. A key is present only
|
|
18
157
|
* when at least one peer is missing, so an absent key means "runs here, or we
|
|
@@ -41,15 +180,47 @@ export interface PeerVersionMismatch {
|
|
|
41
180
|
/** The version the installation actually provides. */
|
|
42
181
|
found: string;
|
|
43
182
|
}
|
|
183
|
+
/** `nodeVersionResolver` over an injected filesystem, walking up from
|
|
184
|
+
* `fromDir` — the seam fixtures drive the lookup through. */
|
|
185
|
+
export declare function packageVersionResolver(fromDir: string, fs: PackageLookupFs): PeerVersionResolver;
|
|
44
186
|
/**
|
|
45
|
-
* The production resolver: `nodeResolver`'s
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* `
|
|
187
|
+
* The production version resolver: `nodeResolver`'s lookup, then the matched
|
|
188
|
+
* directory's own `package.json`, read directly. Reading it directly is the
|
|
189
|
+
* fix, not a shortcut: resolving `<spec>/package.json` is what an exports map
|
|
190
|
+
* can refuse, and a packaged dsh's module proxy refuses it by construction
|
|
191
|
+
* while carrying the proxied package's version in exactly that file (see
|
|
192
|
+
* `nodeResolver`).
|
|
193
|
+
*
|
|
194
|
+
* The directory is the one the presence resolver finds present — the walk's
|
|
195
|
+
* first directory match, holding a `package.json` file (`packageDirectory`;
|
|
196
|
+
* `findPackageDir` cites Node's reader) — so the two never disagree about
|
|
197
|
+
* which copy is installed: a candidate nobody can stat is walked past to the
|
|
198
|
+
* copy further up, whose version is then the answer, exactly as the loader
|
|
199
|
+
* imports that copy (measured on Node 26.6.0 with a looping link in front of
|
|
200
|
+
* it); a match without a manifest is absent, and shadows every copy above
|
|
201
|
+
* it. An `index.js`-only directory, which the loader would import, has no
|
|
202
|
+
* manifest to read a version from either way.
|
|
203
|
+
*
|
|
204
|
+
* Anything that leaves no version to read answers null: a spec that is not a
|
|
205
|
+
* bare package name, a peer that is absent, a manifest that is unreadable or
|
|
206
|
+
* malformed, a `version` that is not a non-empty string. Absence is
|
|
207
|
+
* deliberately NOT reported as a version violation: `incompatibilityMap` is
|
|
208
|
+
* what covers a missing peer.
|
|
51
209
|
*/
|
|
52
210
|
export declare function nodeVersionResolver(baseUrl: string): PeerVersionResolver;
|
|
211
|
+
/**
|
|
212
|
+
* `harnessPackageResolver`'s twin for versions: read afresh from the manifest
|
|
213
|
+
* of the package the harness reaches for `spec` — the copy a plugin's import
|
|
214
|
+
* loads, which the walk cannot see on dsh 0.1.7 — and `walk`'s answer only
|
|
215
|
+
* when the harness reaches none. A manifest the harness names but nobody can
|
|
216
|
+
* read or parse answers null, never the walk's version: the walk may match
|
|
217
|
+
* another copy, and the version must describe the copy presence found (see
|
|
218
|
+
* `nodeVersionResolver`).
|
|
219
|
+
*
|
|
220
|
+
* Null for a name that is not a bare package name and for a service that
|
|
221
|
+
* throws, as null is already this resolver's no-verdict signal.
|
|
222
|
+
*/
|
|
223
|
+
export declare function harnessPackageVersionResolver(walk: PeerVersionResolver, lookup: HarnessPackageLookup, parentURL: string, fs?: PackageLookupFs): PeerVersionResolver;
|
|
53
224
|
/**
|
|
54
225
|
* The declared peers whose provided version is outside the declared range,
|
|
55
226
|
* ordered by peer name. Pure: the module graph arrives through `resolve`.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh's `pluginManager` service (0.1.7 on), as far as this shop calls it:
|
|
3
|
+
* design 2026-09-26-plugin-manager-delegation. Structural throughout: the
|
|
4
|
+
* build compiles against the 0.1.1-rc.2 harness floor, where
|
|
5
|
+
* `@deepseek-ai/dsh-plugin-manager` does not exist.
|
|
6
|
+
*/
|
|
7
|
+
import { type Activation } from './activation.ts';
|
|
8
|
+
import type { HotRestartReason } from './hot.ts';
|
|
9
|
+
/** The operations the shop calls. `cancelInstall` is optional because only
|
|
10
|
+
* the install deadline uses it. Answers stay `unknown` until `readChange`
|
|
11
|
+
* reads them: a later harness may add or drop fields. */
|
|
12
|
+
export interface PluginManagerLike {
|
|
13
|
+
installBundle(spec: string, options: {
|
|
14
|
+
requestId: string;
|
|
15
|
+
}): Promise<unknown>;
|
|
16
|
+
removeBundle(name: string): Promise<unknown>;
|
|
17
|
+
/** `id` is the live loader entry id `listPlugins` reports (`include:...`),
|
|
18
|
+
* not the patch row id (dsh 0.1.7-rc.2 `setPluginEnabled`). */
|
|
19
|
+
setPluginEnabled(id: string, enabled: boolean): Promise<unknown>;
|
|
20
|
+
setBundleEnabled(name: string, enabled: boolean): Promise<unknown>;
|
|
21
|
+
cancelInstall?(requestId: string): Promise<unknown>;
|
|
22
|
+
}
|
|
23
|
+
/** The service, when `service` offers every required operation; else null.
|
|
24
|
+
* All or nothing: half a service would take an install and fail its
|
|
25
|
+
* uninstall. */
|
|
26
|
+
export declare function asPluginManager(service: unknown): PluginManagerLike | null;
|
|
27
|
+
export interface ManagerIncompatible {
|
|
28
|
+
name: string;
|
|
29
|
+
version: string;
|
|
30
|
+
runtimeVersion: string;
|
|
31
|
+
peers: Record<string, string>;
|
|
32
|
+
}
|
|
33
|
+
/** The facts of one answer, each checked for its type. */
|
|
34
|
+
export interface ManagerChange {
|
|
35
|
+
application: string | null;
|
|
36
|
+
stage: string | null;
|
|
37
|
+
errorCode: string | null;
|
|
38
|
+
diagnostic: string | null;
|
|
39
|
+
incompatible: ManagerIncompatible[];
|
|
40
|
+
kind: string | null;
|
|
41
|
+
output: string;
|
|
42
|
+
pendingBuilds: string[];
|
|
43
|
+
failedAt: string | null;
|
|
44
|
+
/** Whether the profile's files differ from before the operation (dsh's
|
|
45
|
+
* `diskState`, which covers package.json). */
|
|
46
|
+
changed: boolean | null;
|
|
47
|
+
/** dsh keeps only the last 16 KB of a pnpm run's output; true when this
|
|
48
|
+
* answer's `packageResult.output` was cut short, so an absence in it
|
|
49
|
+
* proves nothing. */
|
|
50
|
+
truncated: boolean;
|
|
51
|
+
}
|
|
52
|
+
/** Read one `ChangeResult`. Every field is optional: one a later harness
|
|
53
|
+
* drops reads as absent, never as a crash. */
|
|
54
|
+
export declare function readChange(raw: unknown): ManagerChange;
|
|
55
|
+
/** `change`, with every clause naming a `dsh plugin` command removed from
|
|
56
|
+
* `diagnostic` and `output`: dsh's CLI refuses every `plugin` subcommand for
|
|
57
|
+
* the desktop profile, so a step dsh wrote assuming that command exists must
|
|
58
|
+
* not reach a reader there. Two dsh texts this exists for: dsh-app-boot's
|
|
59
|
+
* `pluginCompatibilityWarning` ("... with `dsh plugin allow-version` or the
|
|
60
|
+
* plugin manager ..."), whose sentence goes whole, and the restoration line
|
|
61
|
+
* after a failed repair ("..., but node_modules could not be reinstalled;
|
|
62
|
+
* run 'dsh plugin install'"), which keeps what it says dsh restored. A
|
|
63
|
+
* clause is a run between `; ` separators within a sentence; a sentence left
|
|
64
|
+
* with no clause is dropped, and so is a line left with no sentence. Every
|
|
65
|
+
* other line stays untouched, and a diagnostic left empty afterward reads as
|
|
66
|
+
* absent, the same way `codeReason` already treats one. */
|
|
67
|
+
export declare function forDesktopReader(change: ManagerChange): ManagerChange;
|
|
68
|
+
/** The end of a detail that passes on what a service call threw: `: ` and
|
|
69
|
+
* the message, scrubbed for a desktop reader as `forDesktopReader` scrubs
|
|
70
|
+
* dsh's answer, since a thrown message is dsh's text as well. A message left
|
|
71
|
+
* with nothing to say ends the detail with a period instead. */
|
|
72
|
+
export declare function thrownTail(message: string, desktop: boolean): string;
|
|
73
|
+
export interface ManagerOutcome {
|
|
74
|
+
state: 'done' | 'failed';
|
|
75
|
+
activation?: Activation;
|
|
76
|
+
restartReason?: HotRestartReason;
|
|
77
|
+
detail?: string;
|
|
78
|
+
}
|
|
79
|
+
export interface OutcomeContext {
|
|
80
|
+
profile: string;
|
|
81
|
+
name: string;
|
|
82
|
+
operation: 'install' | 'update' | 'uninstall';
|
|
83
|
+
/** The gateway's `imported` record holds the name (market borrowings section 1). */
|
|
84
|
+
alreadyImported: boolean;
|
|
85
|
+
hasClientHalf: boolean;
|
|
86
|
+
/** The desktop profile, whose readers can run no `dsh plugin` command. */
|
|
87
|
+
desktop: boolean;
|
|
88
|
+
timeoutMs: number;
|
|
89
|
+
}
|
|
90
|
+
/** The sentence a management code gets, plus dsh's own diagnostic. The
|
|
91
|
+
* diagnostic is trimmed and stripped of one trailing period so one that
|
|
92
|
+
* already ends in a period is not doubled; empty after that reads as
|
|
93
|
+
* absent. Exported: Task 7 builds the same tail for a non-terminal notice. */
|
|
94
|
+
export declare function codeReason(change: ManagerChange, code: string): string;
|
|
95
|
+
/** A `ChangeResult` as the shop's terminal install record: design
|
|
96
|
+
* 2026-09-26-plugin-manager-delegation, section 5, read by the first rule
|
|
97
|
+
* that matches. A removal that failed after dsh changed the profile says,
|
|
98
|
+
* after whichever rule read it, that the package is still installed and
|
|
99
|
+
* switched off. */
|
|
100
|
+
export declare function managerOutcome(raw: unknown, context: OutcomeContext): ManagerOutcome;
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
export interface UserLayerRow {
|
|
4
4
|
id: string;
|
|
5
5
|
disabled: boolean;
|
|
6
|
+
/** The module the entry mounts, when the caller knows it. A user-layer row
|
|
7
|
+
* that names a DIFFERENT module is one the harness skips
|
|
8
|
+
* (applyEntryPatches), so only this name lets a named row be written. */
|
|
9
|
+
name?: string;
|
|
6
10
|
}
|
|
7
11
|
/**
|
|
8
12
|
* Find the profile directory that owns `startPath`.
|
|
@@ -19,10 +23,10 @@ export declare function discoverProfile(startPath: string, baseDir?: string): {
|
|
|
19
23
|
dir: string;
|
|
20
24
|
};
|
|
21
25
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* hot through HMR).
|
|
26
|
+
* Set one entry's `disabled` key in the profile's user layer
|
|
27
|
+
* (`cordis.patch.yml`) — {@link setUserLayerRows} for a single row, with the
|
|
28
|
+
* same rules: one key changes and nothing else does (§8: the CLI's
|
|
29
|
+
* watchUserPatches applies the change hot through HMR).
|
|
26
30
|
*/
|
|
27
31
|
export declare function setUserLayerRow(options: {
|
|
28
32
|
profileDir: string;
|
|
@@ -35,6 +39,31 @@ export declare function setUserLayerRow(options: {
|
|
|
35
39
|
* {@link ownedEntryIds}), and they toggle together: writing them one at a
|
|
36
40
|
* time would read-modify-write the file once per entry, so a crash between
|
|
37
41
|
* two writes would leave the package half disabled.
|
|
42
|
+
*
|
|
43
|
+
* The edit changes one key and nothing else (design
|
|
44
|
+
* 2026-09-26-market-borrowings §2). The layer is validated by the harness's
|
|
45
|
+
* own parser first, so one it cannot load still throws and is never
|
|
46
|
+
* rewritten; it is then edited as a YAML document, which keeps every comment,
|
|
47
|
+
* every other row, every other key and every `!!js` scalar as the user wrote
|
|
48
|
+
* them. The whole-file rewrite this replaced dropped each toggled id's row —
|
|
49
|
+
* and with it a `config:` override the user had put there — and dumped the
|
|
50
|
+
* list back through a library that models no comments, so the first toggle
|
|
51
|
+
* erased the header dsh writes into every new profile.
|
|
52
|
+
*
|
|
53
|
+
* For each entry the row written is the LAST one carrying its id, when the
|
|
54
|
+
* harness is sure to apply it: applyEntryPatches applies rows in order and
|
|
55
|
+
* each key replaces the entry's value, so the last row that sets `disabled`
|
|
56
|
+
* decides it. A row with `insert` is an insertion into a group, never an
|
|
57
|
+
* override; a row naming another module is skipped by the harness; a named
|
|
58
|
+
* row whose module the caller did not say cannot be judged. In each of those
|
|
59
|
+
* cases a row is appended instead, which the harness applies last whatever
|
|
60
|
+
* came before it — so appending is never wrong, only less tidy.
|
|
61
|
+
*
|
|
62
|
+
* An enable writes `disabled: false` rather than deleting the row, which is
|
|
63
|
+
* what lost a user's row or the comment above it; it is also the harness's
|
|
64
|
+
* own convention for this file from 0.1.7. The file keeps its permission bits
|
|
65
|
+
* across the rename (a new one is created owner-only, as the harness's writer
|
|
66
|
+
* does), because a config override can hold a credential.
|
|
38
67
|
*/
|
|
39
68
|
export declare function setUserLayerRows(options: {
|
|
40
69
|
profileDir: string;
|
|
@@ -65,6 +94,34 @@ export declare function ownedEntryIds(options: {
|
|
|
65
94
|
profileDir: string;
|
|
66
95
|
packageName: string;
|
|
67
96
|
}): string[];
|
|
97
|
+
/** One loader entry a package's bundle patch inserts: its id, and the module
|
|
98
|
+
* it mounts when the patch names one as a plain string. */
|
|
99
|
+
export interface OwnedEntry {
|
|
100
|
+
id: string;
|
|
101
|
+
name: string | undefined;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* {@link ownedEntryIds} with the module each entry mounts beside its id. The
|
|
105
|
+
* name is what the harness judges a NAMED user-layer row by — a row naming a
|
|
106
|
+
* different module is skipped (applyEntryPatches) — so it is what lets the
|
|
107
|
+
* toggle write a user's own named row rather than append one beside it. It is
|
|
108
|
+
* read through the harness's parser, so a relative name arrives anchored to a
|
|
109
|
+
* file URL exactly as the composed entry carries it; an `!!js` name has no
|
|
110
|
+
* plain value and reads as undefined. Throws where `ownedEntryIds` does.
|
|
111
|
+
*/
|
|
112
|
+
export declare function ownedEntries(options: {
|
|
113
|
+
profileDir: string;
|
|
114
|
+
packageName: string;
|
|
115
|
+
}): OwnedEntry[];
|
|
116
|
+
/**
|
|
117
|
+
* An installed package's own `dsh.bundle.patch`, exactly as declared, or
|
|
118
|
+
* undefined when the package, its manifest or the field is absent — or the
|
|
119
|
+
* manifest cannot be read, which no post-install check can judge either.
|
|
120
|
+
*/
|
|
121
|
+
export declare function declaredBundlePatch(options: {
|
|
122
|
+
profileDir: string;
|
|
123
|
+
packageName: string;
|
|
124
|
+
}): unknown;
|
|
68
125
|
/**
|
|
69
126
|
* Does a live loader entry id belong to the package that owns `owned`?
|
|
70
127
|
*
|
|
@@ -12,9 +12,13 @@
|
|
|
12
12
|
/** `shop/restart` result: committed, or a typed refusal issued BEFORE
|
|
13
13
|
* anything is torn down. Once `ok` is returned the old process WILL exit —
|
|
14
14
|
* the client monitors the new server and reports a failed boot with the
|
|
15
|
-
* manual command
|
|
15
|
+
* manual command and `logFile`, the file the new process's output goes to:
|
|
16
|
+
* where a boot that died explains itself, at a path that depends on DSH_HOME
|
|
17
|
+
* and the shop row's `cacheDir`, which only the host knows. A host older than
|
|
18
|
+
* the field answers without it, so the client reads it as optional. */
|
|
16
19
|
export type RestartOutcome = {
|
|
17
20
|
ok: true;
|
|
21
|
+
logFile?: string;
|
|
18
22
|
} | {
|
|
19
23
|
ok: false;
|
|
20
24
|
detail: string;
|
|
@@ -56,8 +56,34 @@ export interface CatalogEntry {
|
|
|
56
56
|
};
|
|
57
57
|
/** The date this entry first appeared in the catalog (YYYY-MM-DD). */
|
|
58
58
|
added?: string;
|
|
59
|
-
/** The package's
|
|
59
|
+
/** The names of the package's REQUIRED peer dependencies — a peer its
|
|
60
|
+
* author marks optional in `peerDependenciesMeta` is left out at harvest.
|
|
61
|
+
* Additive and optional, so it rides every schemaVersion: the version-6
|
|
62
|
+
* gate this comment used to name came off on 2026-09-03 without ever being
|
|
63
|
+
* opened. Present on npm entries. Not on github entries yet: the registry
|
|
64
|
+
* harvests their peers from 2026-09-24 but withholds them from the catalog
|
|
65
|
+
* (`withholdRepoPeers`) until `SHOP_EMIT_REPO_PEERS` flips, in the release
|
|
66
|
+
* commit that first promotes a shop refining peers against the browser's
|
|
67
|
+
* module table to `latest` (design 2026-09-01-harness-compatibility §9.8).
|
|
68
|
+
* An absent list forms no peer verdict. */
|
|
60
69
|
peers?: string[];
|
|
70
|
+
/** The author's own `dsh.compatibility` declaration, when they published
|
|
71
|
+
* one. A REQUIREMENT, never a verdict: the host compares it against the
|
|
72
|
+
* running installation (`ShopCatalogResult.incompatibleHarness`). Additive
|
|
73
|
+
* and optional, so it rides every schemaVersion. */
|
|
74
|
+
compatibility?: {
|
|
75
|
+
dsh?: string;
|
|
76
|
+
profiles?: string[];
|
|
77
|
+
};
|
|
78
|
+
/** The package's peers on the harness itself (`@deepseek-ai/dsh` and
|
|
79
|
+
* `@deepseek-ai/dsh-*`) with their ranges verbatim, optional ones and empty
|
|
80
|
+
* ranges included — what dsh 0.1.7 refuses an install on. The host hands
|
|
81
|
+
* them to the running dsh's own check (`peerVerdictsOf`). npm entries only:
|
|
82
|
+
* dsh keys the exemption that clears its refusal by the installed
|
|
83
|
+
* manifest's version, which a github entry's catalog version is not
|
|
84
|
+
* (registry `Entry.dshPeers`). Absent when the package declares none, or
|
|
85
|
+
* when its catalog predates the field. */
|
|
86
|
+
dshPeers?: Record<string, string>;
|
|
61
87
|
/** Wire-compatibility key, not a field to read — see {@link installSize},
|
|
62
88
|
* which the catalog parse fills from this one. It names npm's OWN quantity
|
|
63
89
|
* (`dist.unpackedSize`), so it is npm-only and the parse REFUSES one on a
|
|
@@ -4,10 +4,26 @@
|
|
|
4
4
|
* both the source tree (tests) and the bundled `lib/index.js` sit exactly
|
|
5
5
|
* one level below the package root, so the same relative URL resolves in
|
|
6
6
|
* both. */
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Read a version once and keep it: the answer to "which code is this".
|
|
9
|
+
*
|
|
10
|
+
* The file cannot answer that question later. A profile installs hoisted, so
|
|
11
|
+
* a self-update rewrites package.json in the directory the RUNNING code was
|
|
12
|
+
* loaded from, and every read after it names the version that will run after
|
|
13
|
+
* the next restart — which the version row then printed as the one running
|
|
14
|
+
* (design §7.3, 2026-09-27 amendment).
|
|
15
|
+
*
|
|
16
|
+
* A first read that throws is not kept: the module import must not fail over
|
|
17
|
+
* a version string, so the answer falls back to reading on demand, which is
|
|
18
|
+
* what the row always did.
|
|
19
|
+
*/
|
|
20
|
+
export declare function versionAtLoad(read: () => string): () => string;
|
|
21
|
+
/** The version of the code in this process, read when the module was first
|
|
22
|
+
* evaluated — at boot, before any self-update could rewrite the file. */
|
|
23
|
+
export declare const runningVersion: () => string;
|
|
8
24
|
/**
|
|
9
25
|
* The peer ranges this build declares — the input to the load-time harness
|
|
10
|
-
* self-check. Read from the same shipped manifest as `
|
|
26
|
+
* self-check. Read from the same shipped manifest as `runningVersion`, so the
|
|
11
27
|
* ranges checked are the ones this build was actually published with, never
|
|
12
28
|
* a second copy that can drift from them.
|
|
13
29
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-shop",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.4-beta.1",
|
|
4
4
|
"description": "The DeepSeek Harness plugin shop: browse, install, enable, and update dsh plugins from a git-auditable catalog.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh-plugin",
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
"dependencies": {
|
|
76
76
|
"js-yaml": "^4.2.0",
|
|
77
77
|
"semver": "^7.6.3",
|
|
78
|
+
"yaml": "^2.5.1",
|
|
78
79
|
"zod": "^4.4.3"
|
|
79
80
|
},
|
|
80
81
|
"peerDependencies": {
|