dsh-plugin-shop 0.8.3 → 0.8.4-beta.0

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.
@@ -0,0 +1,223 @@
1
+ /** Harness compatibility as the AUTHOR states it: which `dsh.compatibility`
2
+ * declarations the running installation does not meet (design
3
+ * 2026-09-01-harness-compatibility §8.2, as amended §9.9). The presence check
4
+ * in `peers.ts` infers from what a plugin imports; this reads what its author
5
+ * wrote down. And, from dsh 0.1.7 on, what dsh ITSELF refuses: the harness
6
+ * peers its installer rejects an install on (`peerVerdictsOf`, design
7
+ * 2026-09-26-dsh-017-readiness, B1).
8
+ *
9
+ * Pure: the running version, the running profile, the harness's own
10
+ * template table and its peer check arrive as arguments, so fixtures drive
11
+ * every verdict and the reads of the installation stay outside — the running
12
+ * dsh, its template table and its peer check in `harness.ts`, the running
13
+ * profile's bundles and exemptions in the gateway. */
14
+ import { type EntryIdentity } from '../shared/identity.ts';
15
+ import type { PeerCheck } from './harness.ts';
16
+ /** What an author declared in `dsh.compatibility` that this installation does
17
+ * not meet. Each half is present only when the author declared it AND it is
18
+ * unmet here, and carries both sides, so the reader is told what was declared
19
+ * and what is actually running rather than only that something is wrong. */
20
+ export interface HarnessVerdict {
21
+ /** The declared `dsh` range, and the `@deepseek-ai/dsh` version running. */
22
+ dsh?: {
23
+ range: string;
24
+ running: string;
25
+ };
26
+ /** The declared profile templates, and the name of the profile this dsh was
27
+ * booted with. */
28
+ profile?: {
29
+ declared: string[];
30
+ running: string;
31
+ };
32
+ /** What the running dsh refuses this install on — no author's claim, and
33
+ * the one half the shop blocks an install for (`PeerVerdict`). */
34
+ peers?: PeerVerdict;
35
+ }
36
+ /**
37
+ * An install the running dsh will refuse (0.1.7 on): the harness peers it
38
+ * refuses, each with the range the package declared; the dsh version it
39
+ * judged them against; and the command that records dsh's exact-version
40
+ * exemption for this package on this dsh, after which dsh installs it. The
41
+ * command is null when dsh would refuse that too (`allowVersionCommand`).
42
+ */
43
+ export interface PeerVerdict {
44
+ refused: Record<string, string>;
45
+ running: string;
46
+ allowCommand: string | null;
47
+ }
48
+ /** The running profile as the profile half needs it: its name, for the copy,
49
+ * and the bundles it composes, for the comparison. `bundles` is null when the
50
+ * profile manifest could not be read, which is no verdict rather than an
51
+ * accusation. */
52
+ export interface RunningProfile {
53
+ name: string;
54
+ bundles: readonly string[] | null;
55
+ }
56
+ /** The running side of the comparison. `dshVersion` is null when the
57
+ * running dsh's version could not be read — this process was not started by
58
+ * dsh's CLI, or that CLI's manifest carries none (`harness.ts`). The profile
59
+ * is always known, because the gateway asking was booted inside one — but
60
+ * what it composes may not be. */
61
+ export interface HarnessRuntime {
62
+ dshVersion: string | null;
63
+ profile: RunningProfile;
64
+ }
65
+ /** Harness profile template name → the bundles that template composes, or an
66
+ * empty record when the harness's own table could not be read. The names are
67
+ * matched against catalog input, so a reader takes own keys only
68
+ * (`unmetProfile`), and `profileTemplatesOf` builds one with no prototype. */
69
+ export type ProfileTemplates = Readonly<Record<string, readonly string[]>>;
70
+ /**
71
+ * Normalize the harness's OWN `PROFILE_TEMPLATES` into template name → bundle
72
+ * names, or an empty record for a shape this build does not know.
73
+ *
74
+ * Two shapes exist and both are live: 0.1.5-rc.3 maps a name to
75
+ * `{ bundles, patchReload }` (five templates), while the version this package
76
+ * pins as a devDependency maps it to a bare array (two). A name whose value is
77
+ * neither, or whose bundle list holds a non-string, is dropped rather than
78
+ * guessed at — an unreadable table means no profile verdict, never a wrong
79
+ * one. Copied from nowhere: the caller passes the harness's exported object.
80
+ *
81
+ * The record has NO PROTOTYPE, empty answer included. It is indexed by the
82
+ * profile names a catalog entry declares, which are hostile input, and a
83
+ * record built on `{}` answered `constructor`, `__proto__`, `toString`,
84
+ * `hasOwnProperty` and `valueOf` from `Object.prototype`: a value that passed
85
+ * the `undefined` guard and threw out of `compatibilityMap`, so one entry
86
+ * declaring `profiles: ["constructor"]` rejected every `catalog()` call for
87
+ * every user. Without a prototype there is also no `__proto__` setter, so a
88
+ * template exported under that name stays an own key instead of replacing
89
+ * the record's prototype. `unmetProfile` takes own keys only as well — the
90
+ * type is exported, and a caller may hand in a plain object.
91
+ */
92
+ export declare function profileTemplatesOf(exported: unknown): ProfileTemplates;
93
+ /**
94
+ * Install identity → the half or halves of an entry's declaration this
95
+ * installation does not meet. A key is present only when something was
96
+ * declared AND is unmet, so an absent key means "runs here, or we could not
97
+ * tell" — the rule `incompatibilityMap` follows, and for the same reason: one
98
+ * false warning teaches a reader to ignore every warning.
99
+ *
100
+ * The halves are judged apart, and an unknown on either side silences only its
101
+ * own half:
102
+ *
103
+ * - `dsh` needs a range semver can parse and a running version that is semver,
104
+ * and is then `satisfies` with `includePrerelease`. That option is
105
+ * load-bearing: the harness ships nothing but prereleases, and strict semver
106
+ * refuses a prerelease against any range whose comparators carry none —
107
+ * `0.1.5-rc.3` fails `>=0.1.0`, and even `*` — so every author who wrote a
108
+ * plain range would be told they exclude a harness their range includes.
109
+ *
110
+ * What the option does NOT do is read a range the way its author probably
111
+ * meant it. Under it a prerelease is compared like any other version, and a
112
+ * prerelease sorts BELOW its own release (measured with semver 7.8.5; the
113
+ * rows are pinned in compatibility.test.ts):
114
+ * - An upper bound written without `-0` admits the next line's
115
+ * prereleases: `<0.2.0` and `>=0.1.0 <0.2.0` both admit `0.2.0-rc.1`.
116
+ * Only a bound that desugars to `<0.2.0-0` refuses it — `^0.1.0`,
117
+ * `~0.1.0` and `0.1.x` do, as does `<0.2.0-0` written out.
118
+ * - A floor written without `-0` refuses its own prereleases: `>=0.1.5`,
119
+ * `^0.1.5` and `0.1.5` all refuse `0.1.5-rc.3`, because `0.1.5` sorts
120
+ * above it. `>=0.1.5-0` admits every `0.1.5-rc.N`.
121
+ * So `>=0.1.5-0` and `<0.2.0-0` are the spellings that say what an author
122
+ * usually means. The comparison stays semver's own and is never rewritten
123
+ * here: a verdict that coerced prereleases would answer differently from
124
+ * semver, which is what an author checks a range against. An exact list of
125
+ * other versions — `@xmanrui/dsh-im`'s declaration — is refused, as it
126
+ * should be.
127
+ * - `profile` is met when the running profile's NAME is one of the declared
128
+ * names, or when it composes every bundle of a declared template.
129
+ *
130
+ * The name is trusted for the names dsh ships, because for those it is not
131
+ * the reader's choice. On 0.1.5-rc.3 both paths that create a missing
132
+ * profile build one of a shipped name from that name's template
133
+ * (dsh-app-boot's `loadProfile`, and `dsh plugin`), and dsh refuses a
134
+ * shipped name as the target of `--from-default-profile`, so a profile
135
+ * carrying one was built from that template. That is all the name
136
+ * guarantees: which template the profile was built from, not what it
137
+ * composes now. Nothing holds the bundle list to the template afterwards:
138
+ * `dsh plugin`'s reconcile, not app-boot, appends each bundle it installs,
139
+ * and app-boot's `normalizeShippedProfile` rewrites only a list that is
140
+ * exactly a retired tuple or the current one. Bundles alone could not
141
+ * decide there: a profile keeps the bundle list it was created with, so the
142
+ * first harness release that added a bundle to `web` would badge every
143
+ * `profiles: ["web"]` plugin (`@xmanrui/dsh-im`) on every existing `web`
144
+ * profile, with copy saying it supports web and this dsh was launched with
145
+ * web.
146
+ *
147
+ * Any other name is the reader's choice — `dsh --profile rescue
148
+ * --from-default-profile web` builds a profile called `rescue` from the web
149
+ * bundles, and dsh records nothing about which template it came from — so a
150
+ * running name no declaration matches is judged by what the profile IS:
151
+ * each declared name is read as one of the harness's own templates, met
152
+ * when every bundle of that template is in the running profile's bundles. A
153
+ * declared name that is no template — an own key of the table, never an
154
+ * inherited one — cannot be judged, and a list with such a name and no met
155
+ * template gives no verdict. The half is unmet only when the running name is
156
+ * not declared and every declared name is a template this profile does not
157
+ * compose — see the amendment for the whole rule.
158
+ *
159
+ * The residual, knowingly kept: a custom-named profile is still judged by
160
+ * bundles, so a template that later gained a bundle would badge the custom
161
+ * profiles created before the change. No PUBLISHED
162
+ * `@deepseek-ai/dsh-app-boot` had changed a template's bundle list as of
163
+ * 2026-09-25: all 28 versions, read that day, give every template they
164
+ * share the same list. One list was retired before the first publish:
165
+ * every published app-boot, from the first, carries a retired `headless`
166
+ * tuple (`@deepseek-ai/dsh-base`, `@deepseek-ai/dsh-web-app`,
167
+ * `@deepseek-ai/dsh-headless`) in `INSTALLATION_OWNED_PROFILE_TUPLES`, and
168
+ * `normalizeShippedProfile` rewrites a `headless` profile carrying exactly
169
+ * that list to the current template when it loads.
170
+ */
171
+ export declare function compatibilityMap(entries: readonly (EntryIdentity & {
172
+ compatibility?: {
173
+ dsh?: string;
174
+ profiles?: string[];
175
+ };
176
+ })[], runtime: HarnessRuntime, templates: ProfileTemplates): Record<string, HarnessVerdict>;
177
+ /**
178
+ * Install identity → the refusal the running dsh will make, for each entry
179
+ * whose harness peers it refuses and the profile has not exempted. A key is
180
+ * present only for a refusal, so an absent key means "dsh installs it, or we
181
+ * could not tell".
182
+ *
183
+ * The rule is dsh's own (`check`, from the running app-boot), handed the
184
+ * manifest dsh's installer reads before it installs a registry spec — `pnpm
185
+ * view <spec> name version peerDependencies`, measured in dsh-plugin-manager
186
+ * 0.1.7-rc.2 — with the catalog's `dshPeers` for the peers. Those are the
187
+ * only peers the rule reads, verbatim. What this cannot see errs toward
188
+ * silence, never toward a refusal dsh would not make: a malformed peer range
189
+ * elsewhere in the manifest, which dsh rejects only after installing, and
190
+ * the plugins a bundle's patch rows name, which it checks then too. Those
191
+ * reach the reader as the install's own failure (`installFailureDetail`).
192
+ *
193
+ * npm entries only. dsh keys an exemption by the INSTALLED manifest's
194
+ * `name@version`, and a github entry's catalog version is a commit: its
195
+ * exemption status cannot be read, and a refusal named here could never be
196
+ * cleared from the shop. dsh refuses such an install itself, and says how
197
+ * to exempt it.
198
+ *
199
+ * A throw from the check forms no verdict for that entry, as a resolver's
200
+ * throw does in `incompatibilityMap`.
201
+ */
202
+ export declare function peerVerdictsOf(entries: readonly (EntryIdentity & {
203
+ version: string;
204
+ dshPeers?: Record<string, string>;
205
+ })[], check: PeerCheck, exemptions: Record<string, string[]>, profile: string): Record<string, PeerVerdict>;
206
+ /**
207
+ * The command that records dsh's exact-version exemption for one refused
208
+ * install, spelled as dsh's own CLI prints it after a refusal — or null when
209
+ * `dsh plugin allow-version` would refuse the package or either version,
210
+ * which it checks against the grammar above. The name and version come from
211
+ * the catalog, which is hostile input bound for a terminal, and the two
212
+ * grammars admit no whitespace, quote, `$`, backtick, `;`, `|`, `&`,
213
+ * redirection or bracket, so no catalog value can add a command to the line
214
+ * it is pasted into.
215
+ *
216
+ * The profile is written as it is, as dsh prints it: it is the reader's own
217
+ * choice of name, never catalog input.
218
+ */
219
+ export declare function allowVersionCommand(profile: string, issue: {
220
+ name: string;
221
+ version: string;
222
+ runtimeVersion: string;
223
+ }): string | null;
@@ -23,6 +23,14 @@
23
23
  */
24
24
  /** The npm package the dsh CLI is published as. */
25
25
  export declare const DSH_PACKAGE = "@deepseek-ai/dsh";
26
+ /**
27
+ * Whether dsh's CLI refuses this profile outright: `desktop`, in any letter
28
+ * case, "is managed exclusively by the Electron application" — refused for a
29
+ * launch and for `dsh plugin` alike (`rejectElectronProfile` in the CLI's
30
+ * `lib/bin.js`, the same in 0.1.5-rc.3 and 0.1.7-rc.2). Every command the
31
+ * shop spawns names a profile, so none of them can run for this one.
32
+ */
33
+ export declare function isDesktopProfile(profile: string): boolean;
26
34
  /**
27
35
  * The command that starts `bin` through `execPath` when `bin` names a
28
36
  * JavaScript entry, or `null` when `bin` is a program to spawn as given.
@@ -0,0 +1,109 @@
1
+ /**
2
+ * The harness this process runs: which `@deepseek-ai/dsh` it is, and that
3
+ * dsh's own profile-template table — the running side of the
4
+ * `dsh.compatibility` verdict (design 2026-09-01-harness-compatibility §9.9).
5
+ *
6
+ * Both used to be read from somewhere that is not the running harness, and
7
+ * both were wrong there:
8
+ *
9
+ * - The version was the first `node_modules/@deepseek-ai/dsh` found walking up
10
+ * from the profile: what the profile can IMPORT, not what RUNS. A plugin
11
+ * that depends on the dsh package gets its copy hoisted into
12
+ * `<profile>/node_modules` — the listed `dsh-claude-tui@0.1.6` pulls
13
+ * 0.1.2-rc.1 — a second install sharing DSH_HOME re-points the link farm,
14
+ * and under a packaged dsh the bin-only package gets no link-farm proxy at
15
+ * all, so the read was dead or stale. The first was reproduced with real
16
+ * installs on 2026-09-25 (the design's section 9.9 amendment): every
17
+ * verdict said "running 0.1.2-rc.1" while 0.1.5-rc.3 ran.
18
+ * - The templates came from the shop's own `import * as appBoot`, which
19
+ * resolves from the SHOP's real path: under a `link:` install that is this
20
+ * repository's devDependency, 0.1.1-rc.2 with two templates, while
21
+ * 0.1.5-rc.3 with five runs — and in production a copy hoisted from some
22
+ * plugin's dependencies can shadow it the same way.
23
+ *
24
+ * Both now come from the one thing that names the running harness: the script
25
+ * this process was started with. Resolved through symlinks, the package that
26
+ * owns it IS the dsh CLI that is running, if it is one at all; its manifest
27
+ * says its version, and the app-boot it imports says its templates.
28
+ */
29
+ import { type ProfileTemplates } from './compatibility.ts';
30
+ /** One package's harness peers as the running dsh judges them: app-boot's
31
+ * `evaluatePluginCompatibility` result, the shape 0.1.7 returns. */
32
+ export interface PeerIssue {
33
+ name: string;
34
+ version: string;
35
+ /** The dsh version the check judged against — app-boot's own. */
36
+ runtimeVersion: string;
37
+ /** The refused peers, each with the range the package declared. */
38
+ peers: Record<string, string>;
39
+ /** Whether the profile holds dsh's exact-version exemption for this
40
+ * package on this dsh, in which case dsh accepts the install anyway. */
41
+ exempted: boolean;
42
+ }
43
+ /** The running dsh's own install-time check (app-boot, from 0.1.7): the rule
44
+ * it refuses an install by, and the exemptions a profile holds. Taken from the
45
+ * dsh that runs rather than reimplemented, so a verdict cannot drift from the
46
+ * refusal it predicts (design 2026-09-26-dsh-017-readiness, B1). */
47
+ export interface PeerCheck {
48
+ /** `evaluatePluginCompatibility(manifest, exemptions, runtimeVersion)` with
49
+ * app-boot's own version as the runtime — the version dsh's installer
50
+ * judges by, since it passes none and the default reads app-boot's
51
+ * manifest. That version is read once, here, rather than per call: the
52
+ * default re-reads the file on every call, which measured 75 ms against
53
+ * 12 ms for 2,000 manifests on 0.1.7-rc.2. The incompatible harness peers
54
+ * of one manifest, or undefined when there are none. Throws for a manifest
55
+ * it cannot judge. */
56
+ evaluate(manifest: {
57
+ name: string;
58
+ version: string;
59
+ peerDependencies: Record<string, string>;
60
+ }, exemptions: Record<string, string[]>): PeerIssue | undefined;
61
+ /** `readProfileVersionExemptions(profileDir)`: exact `name@version` keys
62
+ * mapped to the dsh versions each is exempted on. */
63
+ exemptions(profileDir: string): Record<string, string[]>;
64
+ }
65
+ /** The running side of the `dsh.compatibility` verdict. The gateway reads it
66
+ * once and keeps it: a running process cannot change which dsh it is. */
67
+ export interface RunningHarness {
68
+ /** The running dsh's own `version`, or null when this process was not
69
+ * started by dsh's CLI or its manifest carries no non-empty string there.
70
+ * Whether it is semver is `compatibilityMap`'s question. */
71
+ dshVersion: string | null;
72
+ /** That dsh's own `PROFILE_TEMPLATES`, normalized by `profileTemplatesOf`;
73
+ * empty whenever the table could not be read. */
74
+ templates: ProfileTemplates;
75
+ /** Whether that dsh reads a LIST-valued `dsh.bundle.patch`: its app-boot
76
+ * exports `bundlePatchFiles`, which 0.1.7 does and 0.1.5 does not. A dsh
77
+ * without it joins the declaration onto a path, so a package declaring a
78
+ * list stops the profile from starting (design 2026-09-26-dsh-017-readiness,
79
+ * B4). Null whenever the app-boot could not be read — unknown, which forms
80
+ * no verdict. */
81
+ patchLists: boolean | null;
82
+ /** That dsh's own install-time peer check, or null when its app-boot
83
+ * exports none (before 0.1.7, which refuses no install on peers) or could
84
+ * not be read — no verdict either way. */
85
+ peerCheck: PeerCheck | null;
86
+ }
87
+ /**
88
+ * The harness this process runs, identified from `script` — the gateway's
89
+ * `restartScript`, `process.argv[1]` in production — or nothing, never a
90
+ * guess:
91
+ *
92
+ * 1. `script` is resolved through symlinks, because `argv[1]` is the path the
93
+ * shell ran — `<prefix>/bin/dsh`, a link — and only its target lies inside
94
+ * the dsh package. No script, or one that does not resolve: nothing known.
95
+ * An empty path is no script either: `realpathSync('')` answers the
96
+ * working directory, which says where dsh was started, not what it is.
97
+ * 2. The package that owns it (`owningPackage`) must be `@deepseek-ai/dsh`.
98
+ * Anything else — a test runner, another host embedding the shop — means
99
+ * nothing here can say which harness runs, and both halves of the verdict
100
+ * stay silent.
101
+ * 3. `dshVersion` is that manifest's `version` when it is a non-empty string.
102
+ * 4. `templates`, `patchLists` and `peerCheck` come from that dsh's own
103
+ * app-boot (`appBootFacts`). Any failure there costs all three and keeps
104
+ * the version.
105
+ *
106
+ * Never throws and never rejects: a harness nobody could identify forms no
107
+ * verdict, and must never be the reason the catalog fails to load.
108
+ */
109
+ export declare function readRunningHarness(script: string | undefined): Promise<RunningHarness>;
@@ -51,19 +51,19 @@ export interface PluginHandle {
51
51
  * Why a restart is needed after the hot path ran — a stable code the client
52
52
  * turns into copy in the reader's own dsh language.
53
53
  *
54
- * Four of the five say the MOUNT could not activate, and distinguish
54
+ * Five of the six say the MOUNT could not activate, and distinguish
55
55
  * "restart will fix it" (`timeout`, `mount-failed`) from "this package can
56
56
  * never hot-mount" (`no-patch`, `not-simple`) and "this harness cannot"
57
57
  * (`host-unsupported`).
58
58
  *
59
- * `client-half` is the one that does not: the mount SUCCEEDED and the host
60
- * half is running, but the package declares `dsh.client` and a hot mount does
61
- * not enter the composition the client registry enumerates, so no reload can
62
- * fetch its browser half (measured 2026-09-14 — see `activation.ts`). It
63
- * exists so the reader is not told "installed; restart dsh to activate"
64
- * about a plugin that is demonstrably already running.
59
+ * `already-loaded` is the one that does not: no mount was attempted, because
60
+ * this process may already hold the package's module, and Node caches a
61
+ * module by its URL — the new files sit at the old URL, so a mount would re-run
62
+ * the old code under the new version's name (design
63
+ * 2026-09-26-market-borrowings §1). The gateway decides it before calling
64
+ * `hotMount`, which never returns it.
65
65
  */
66
- export type HotRestartReason = 'no-patch' | 'not-simple' | 'host-unsupported' | 'timeout' | 'mount-failed' | 'client-half';
66
+ export type HotRestartReason = 'no-patch' | 'not-simple' | 'host-unsupported' | 'timeout' | 'mount-failed' | 'already-loaded';
67
67
  export interface HotMountResult {
68
68
  ok: boolean;
69
69
  /** null exactly when ok. */