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.
@@ -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 production resolver: the same question the harness's own
11
- * ClientModuleRegistry asks, through a require anchored at the profile. Asking
12
- * what the loader asks is what keeps this verdict and the runtime's behaviour
13
- * from drifting apart.
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 own resolution, kept this time
46
- * instead of collapsed to a boolean. Anything that leaves no version to read
47
- * answers null — a peer that is absent, one that restricts `./package.json`
48
- * in its exports, an unreadable or malformed manifest, a manifest with no
49
- * `version`. Absence is deliberately NOT reported as a version violation:
50
- * `incompatibilityMap` is what covers a missing peer.
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
- * Upsert one row of the profile's user layer (`cordis.patch.yml`). Enabling
23
- * removes the row so the bundle default rules again; disabling writes
24
- * `{ id, disabled: true }` (§8: the CLI's watchUserPatches applies the change
25
- * hot through HMR). Existing rows for other ids are preserved verbatim.
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 declared peer dependency names (schemaVersion 6). */
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
- export declare function ownVersion(): string;
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 `ownVersion`, so the
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",
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": {