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
|
@@ -12,10 +12,13 @@ import { type RepoPinFs } from './repo-pins.ts';
|
|
|
12
12
|
import { type InstallState } from '../shared/install-state.ts';
|
|
13
13
|
import { type Prefetcher } from './prefetch.ts';
|
|
14
14
|
import { type PeerResolver, type PeerVersionResolver } from './peers.ts';
|
|
15
|
+
import { type HarnessVerdict } from './compatibility.ts';
|
|
16
|
+
import { type RunningHarness } from './harness.ts';
|
|
15
17
|
export type { InstallArgs, InstallRejectionCode } from './install.ts';
|
|
16
18
|
export type { HotRestartReason } from './hot.ts';
|
|
17
19
|
export type { Activation } from './activation.ts';
|
|
18
20
|
export type { CatalogEntry } from './types.ts';
|
|
21
|
+
export type { HarnessVerdict } from './compatibility.ts';
|
|
19
22
|
/** One Loader inventory entry, structurally — the shop never depends on
|
|
20
23
|
* cordis-plugin-loader, whose types do not reach this package's typecheck. */
|
|
21
24
|
export interface InventoryEntry {
|
|
@@ -75,8 +78,25 @@ export interface ShopGatewayOptions {
|
|
|
75
78
|
* defaults to the real `process.argv` minus node and the script path. */
|
|
76
79
|
restartArgv?: string[];
|
|
77
80
|
/** The JS entry `shop/restart` re-runs; defaults to `process.argv[1]`, the
|
|
78
|
-
* script this dsh was started with.
|
|
81
|
+
* script this dsh was started with. It is also what identifies the running
|
|
82
|
+
* harness for the `dsh.compatibility` verdict: the package that owns this
|
|
83
|
+
* script, through symlinks, is the dsh actually running, and its own
|
|
84
|
+
* app-boot supplies the profile templates (`harness.ts`). A script owned by
|
|
85
|
+
* anything else — a test runner, another host — leaves both halves of that
|
|
86
|
+
* verdict silent. The tests about where the running version comes from pass
|
|
87
|
+
* a fixture install here, which is the production path; the rest inject
|
|
88
|
+
* `readHarness`. */
|
|
79
89
|
restartScript?: string;
|
|
90
|
+
/** Test-only injection: how the running harness is read from
|
|
91
|
+
* `restartScript`; production uses `readRunningHarness`. It exists because
|
|
92
|
+
* an in-process test cannot import a module from another Windows drive
|
|
93
|
+
* under vitest's module runner, and a Windows runner has the checkout on D:
|
|
94
|
+
* and the temp dir, where every fixture harness lives, on C:. The read's
|
|
95
|
+
* import of the fixture's app-boot fails there, and the table reads empty.
|
|
96
|
+
* Production passes nothing, so the running dsh is read as before. Read
|
|
97
|
+
* once and kept either way (see `harnessRead`), so like the default it
|
|
98
|
+
* must never reject. */
|
|
99
|
+
readHarness?: (script: string | undefined) => Promise<RunningHarness>;
|
|
80
100
|
/** Test-only injection: the exit the restart calls after the response is
|
|
81
101
|
* delivered. Production uses `process.exit`. */
|
|
82
102
|
exit?: (code?: number) => void;
|
|
@@ -87,6 +107,9 @@ export interface ShopGatewayOptions {
|
|
|
87
107
|
/** Test-only injection: the shop's latest-version lookup; production
|
|
88
108
|
* fetches the npm packument. */
|
|
89
109
|
fetchLatestVersion?: () => Promise<string | null>;
|
|
110
|
+
/** Test-only injection: the version of the code this process loaded;
|
|
111
|
+
* production reads it once, when own-version.ts is first evaluated. */
|
|
112
|
+
runningVersion?: string;
|
|
90
113
|
/** How long the gateway waits after a successful restart response before
|
|
91
114
|
* exiting the old process; test-only shortening, production uses 2s. */
|
|
92
115
|
restartExitDelayMs?: number;
|
|
@@ -119,6 +142,11 @@ export interface ShopGatewayOptions {
|
|
|
119
142
|
* — a live registry request — from every install that finds a command
|
|
120
143
|
* already queued for its profile. */
|
|
121
144
|
prefetcher?: Prefetcher;
|
|
145
|
+
/** Test-only injection: the record of packages this process may already
|
|
146
|
+
* have imported. Production shares one module-scope set across every
|
|
147
|
+
* gateway in the process (see `importedThisProcess`), which a test file
|
|
148
|
+
* building many gateways would otherwise share as well. */
|
|
149
|
+
importedModules?: Set<string>;
|
|
122
150
|
}
|
|
123
151
|
/** `shop/installStart` result (§7.3): rejections are typed wire values with an
|
|
124
152
|
* author-readable `detail`, not thrown RPC errors. */
|
|
@@ -183,12 +211,14 @@ export type ShopRestartResult = RestartOutcome;
|
|
|
183
211
|
* restart was impossible after it had become possible. Static here, dynamic
|
|
184
212
|
* there; the split is the whole reason this is a separate predicate rather
|
|
185
213
|
* than a cache of `restart()`'s answer. */
|
|
186
|
-
export type RestartBlockedReason = 'windows' | 'systemd' | 'port-zero';
|
|
214
|
+
export type RestartBlockedReason = 'desktop' | 'windows' | 'systemd' | 'port-zero';
|
|
187
215
|
/** `shop/version` result (§7.3): the RUNNING shop version (from the shipped
|
|
188
216
|
* package.json, not the manifest's range), the npm latest when the check
|
|
189
217
|
* could answer (`null` = no answer — advisory, never an error), and the
|
|
190
218
|
* comparison verdict. */
|
|
191
219
|
export interface ShopVersionResult {
|
|
220
|
+
/** The version of the code this process is running, read when it loaded.
|
|
221
|
+
* Not the file on disk now: a self-update rewrites that in place. */
|
|
192
222
|
installed: string;
|
|
193
223
|
latest: string | null;
|
|
194
224
|
outdated: boolean;
|
|
@@ -196,6 +226,17 @@ export interface ShopVersionResult {
|
|
|
196
226
|
* its way. The client drops the restart offer on a reason and renders that
|
|
197
227
|
* reason's own copy, keeping the pending-change notice either way. */
|
|
198
228
|
restartBlocked: RestartBlockedReason | null;
|
|
229
|
+
/**
|
|
230
|
+
* The version the profile has installed when it is not the one running —
|
|
231
|
+
* an update that landed and waits for a restart — or null when the running
|
|
232
|
+
* code is what is installed, or the installed copy could not be read.
|
|
233
|
+
*
|
|
234
|
+
* This host always sends it. Absent, it comes from a host older than the
|
|
235
|
+
* field, and the client reads that as a restart still pending: a client
|
|
236
|
+
* newer than its host exists only because the disk holds a newer shop than
|
|
237
|
+
* the process running (design §7.3, 2026-09-27 amendment).
|
|
238
|
+
*/
|
|
239
|
+
pendingVersion?: string | null;
|
|
199
240
|
}
|
|
200
241
|
/** `shop/updateStart` result (§7.3): the self-update spawn, or a typed
|
|
201
242
|
* refusal (a version that is not plain semver). */
|
|
@@ -249,16 +290,44 @@ export interface ShopCatalogResult {
|
|
|
249
290
|
* or the sidecar could not be fetched/verified (§5). */
|
|
250
291
|
stars: Record<string, number>;
|
|
251
292
|
/** Install identity (`npm:<name>` / `github:<repo>#<subdir>`) → the declared
|
|
252
|
-
* peers
|
|
253
|
-
*
|
|
254
|
-
*
|
|
293
|
+
* peers node resolution cannot find from the profile. This is the HOST half
|
|
294
|
+
* of the verdict only: a module the browser's module table serves — a
|
|
295
|
+
* platform seed word such as `react` — has no package on disk and is still
|
|
296
|
+
* listed here, so the client removes every name its live module table
|
|
297
|
+
* provides before anything renders (`client/module-table.ts`), and renders
|
|
298
|
+
* no peer verdict at all on a page that offers no usable table. A key is
|
|
299
|
+
* absent when the plugin runs here or when no verdict could be formed;
|
|
300
|
+
* same-named entries stay independent. */
|
|
255
301
|
incompatible: Record<string, string[]>;
|
|
302
|
+
/** Install identity → what the entry's author declared in
|
|
303
|
+
* `dsh.compatibility` that this installation does not meet (design
|
|
304
|
+
* 2026-09-01-harness-compatibility §8.2), and what the running dsh itself
|
|
305
|
+
* will refuse the install on (`peers`, from dsh 0.1.7; design
|
|
306
|
+
* 2026-09-26-dsh-017-readiness, B1). A key is absent when nothing was
|
|
307
|
+
* declared or every declared half is met, and dsh refuses nothing. A half that cannot be judged —
|
|
308
|
+
* the running version unreadable, a range semver cannot parse — is left out
|
|
309
|
+
* while the other is still judged, so an unknown never reads as an
|
|
310
|
+
* accusation; same-named entries stay independent. A process not started
|
|
311
|
+
* by dsh's CLI judges neither half: nothing then says which harness runs
|
|
312
|
+
* (`harness.ts`). */
|
|
313
|
+
incompatibleHarness: Record<string, HarnessVerdict>;
|
|
256
314
|
}
|
|
257
315
|
/** Remote-only service exposing the shop Remote methods of §7.3.
|
|
258
316
|
*
|
|
259
317
|
* @typert service shop */
|
|
260
318
|
export declare class ShopGateway extends TypertRemoteService {
|
|
261
319
|
private readonly options;
|
|
320
|
+
/** The context this gateway was built with: the shop's own fiber. Inside an
|
|
321
|
+
* RPC method `this.ctx` is NOT that — cordis hands a service out with `ctx`
|
|
322
|
+
* rebound to the context that looked it up, so a call arriving over the
|
|
323
|
+
* wire sees the caller's, the typert gateway's (measured on 0.1.5-rc.3 and
|
|
324
|
+
* 0.1.7-rc.2). Anything whose PARENT matters is registered from here. The
|
|
325
|
+
* hot tree above all: registered from the gateway's context it was owned by
|
|
326
|
+
* the gateway's fiber rather than the shop's, was listed under the gateway's
|
|
327
|
+
* loader entry, and — the part a reader saw — sat where dsh's client
|
|
328
|
+
* registry never composed it, so a hot-installed browser half needed a
|
|
329
|
+
* restart that a reload now does (design 2026-09-26-dsh-017-readiness). */
|
|
330
|
+
private readonly home;
|
|
262
331
|
/** The profile dsh installs into; discovered from this module's own
|
|
263
332
|
* location when the caller does not supply one. */
|
|
264
333
|
private readonly profile;
|
|
@@ -278,6 +347,11 @@ export declare class ShopGateway extends TypertRemoteService {
|
|
|
278
347
|
private readonly exit;
|
|
279
348
|
private readonly restartExitDelayMs;
|
|
280
349
|
private readonly restartParentPid;
|
|
350
|
+
/** The restart this process committed, once it has: every later
|
|
351
|
+
* `shop/restart` answers with it. A page that lost the first answer to an
|
|
352
|
+
* HMR swap asks again (client/restart-monitor.ts), and inside the exit
|
|
353
|
+
* delay that ask reaches this same process. */
|
|
354
|
+
private committedRestart;
|
|
281
355
|
private readonly latestVersion;
|
|
282
356
|
private readonly pinFs;
|
|
283
357
|
private readonly allowRestart?;
|
|
@@ -294,6 +368,11 @@ export declare class ShopGateway extends TypertRemoteService {
|
|
|
294
368
|
/** One pump for the whole gateway: batching is per profile and lives inside
|
|
295
369
|
* it, so a second instance would race the first for the same store. */
|
|
296
370
|
private readonly prefetcher;
|
|
371
|
+
/** What this process may already have imported (see `importedThisProcess`). */
|
|
372
|
+
private readonly imported;
|
|
373
|
+
/** Routes the output dsh's `pluginManager` streams to the record whose
|
|
374
|
+
* request id it carries (see the constructor). */
|
|
375
|
+
private readonly managerLogs;
|
|
297
376
|
/** The install gate runs against the last loaded snapshot, never a fresh
|
|
298
377
|
* fetch per request (§7.2: the Host's cached snapshot is the truth). */
|
|
299
378
|
/** Finished install records retained, so a poll sees the true terminal
|
|
@@ -313,17 +392,13 @@ export declare class ShopGateway extends TypertRemoteService {
|
|
|
313
392
|
/** The user's own `registry=` from `~/.npmrc`, read at most once per
|
|
314
393
|
* gateway. A wrapper distinguishes a genuine `null` from an unread value. */
|
|
315
394
|
private npmRegistryCache;
|
|
316
|
-
/** The
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
*
|
|
323
|
-
* failed walk on every single call. Recomputed only when `catalog()`
|
|
324
|
-
* loads a snapshot that is not this exact object. */
|
|
325
|
-
private incompatibleCache;
|
|
326
|
-
/** Install records, running and finished; a poll finds one here or reports not found. */
|
|
395
|
+
/** The harness this process runs, read at most once per gateway: which dsh
|
|
396
|
+
* is running cannot change while it runs, and the read imports a module. A
|
|
397
|
+
* promise, so concurrent first calls share one read; `readRunningHarness`
|
|
398
|
+
* never rejects, so there is no failure to retry. */
|
|
399
|
+
private harnessRead;
|
|
400
|
+
/** Install records, running and finished, from the CLI and from dsh's
|
|
401
|
+
* `pluginManager` alike; a poll finds one here or reports not found. */
|
|
327
402
|
private readonly installs;
|
|
328
403
|
/** Every install id in insertion order, oldest first; finished-record eviction walks this from the front. */
|
|
329
404
|
private readonly installOrder;
|
|
@@ -347,6 +422,40 @@ export declare class ShopGateway extends TypertRemoteService {
|
|
|
347
422
|
* stays lazy so `setEnabled` works in tests via the `profileDir` option
|
|
348
423
|
* without requiring a real profile above this module. */
|
|
349
424
|
private profileDirResolved;
|
|
425
|
+
/** Where every "what can this installation's plugins import?" question
|
|
426
|
+
* resolves from: the profile's own Loader root, which is where the harness
|
|
427
|
+
* resolves plugins from. One definition for the peer presence check and the
|
|
428
|
+
* load-time self-check, so neither can drift onto a different notion of
|
|
429
|
+
* "the installation". NOT where the running harness is read from: a copy
|
|
430
|
+
* of `@deepseek-ai/dsh` a plugin's dependencies hoist into the profile is
|
|
431
|
+
* importable from here and is not what runs (`harness.ts`). Throws when no
|
|
432
|
+
* profile directory can be discovered and none is given. */
|
|
433
|
+
private profileAnchor;
|
|
434
|
+
/** dsh's `pluginPackages` service, when the running harness provides one
|
|
435
|
+
* (0.1.7 and later), else null. Read on each use, like `pluginInventory`.
|
|
436
|
+
* It is how such a harness serves its own packages to plugins, which no
|
|
437
|
+
* walk of the profile's disk can see (design
|
|
438
|
+
* 2026-09-01-harness-compatibility §11). */
|
|
439
|
+
private harnessPackages;
|
|
440
|
+
/** dsh's `pluginManager` service, when the running harness provides all of
|
|
441
|
+
* it (0.1.7 and later), else null. Never cached across calls, like
|
|
442
|
+
* `pluginPackages`, but read once per RPC, at its start, and passed down:
|
|
443
|
+
* read again after an await, a service disposed in between would let a
|
|
444
|
+
* desktop mutation past the desktop gate and onto the CLI path, which dsh
|
|
445
|
+
* refuses for that profile. Design 2026-09-26-plugin-manager-delegation,
|
|
446
|
+
* section 3.1. */
|
|
447
|
+
private pluginManager;
|
|
448
|
+
/** "Does this installation provide `spec`?", asked from `anchor`: the
|
|
449
|
+
* harness's own resolution in front of the walk where the harness has one,
|
|
450
|
+
* the walk alone where it has not. */
|
|
451
|
+
private peerResolver;
|
|
452
|
+
/** `peerResolver`'s twin for the versions the load-time self-check reads. */
|
|
453
|
+
private peerVersionResolver;
|
|
454
|
+
/** `profileDirResolved`, or null when no profile directory can be
|
|
455
|
+
* discovered — for the reads that answer "cannot tell" rather than fail. */
|
|
456
|
+
private profileDirOrNone;
|
|
457
|
+
/** The harness this process runs, read once (see `harnessRead`). */
|
|
458
|
+
private runningHarness;
|
|
350
459
|
/** The inventory, through the wire remote: an envelope `{ ok, value }` or
|
|
351
460
|
* `{ ok: false, error }`. Each row is re-validated before trust (same
|
|
352
461
|
* discipline as the rowConfig cast). */
|
|
@@ -358,8 +467,9 @@ export declare class ShopGateway extends TypertRemoteService {
|
|
|
358
467
|
* down. A disable can land while the entry's init is still in flight: the
|
|
359
468
|
* options flip but the finishing init brings the fiber up anyway, and a
|
|
360
469
|
* plain re-update no-ops on the empty diff (dsh-market themes.ts:74-93).
|
|
361
|
-
*
|
|
362
|
-
*
|
|
470
|
+
* The uninstall path is the one caller: an update used to swap its running
|
|
471
|
+
* instance for a hot mount through here, and no longer mounts at all
|
|
472
|
+
* (design 2026-09-26-market-borrowings §1). */
|
|
363
473
|
/** The package's owned entry ids, or none when its bundle patch cannot be
|
|
364
474
|
* read. For the paths where a live disable is an optimization and the
|
|
365
475
|
* operation must succeed regardless; `setEnabled` reports the failure
|
|
@@ -387,10 +497,15 @@ export declare class ShopGateway extends TypertRemoteService {
|
|
|
387
497
|
* Through `ownDependencySpec` for the reason spelled out there: a bare index
|
|
388
498
|
* read answers for `Object.prototype`, and `constructor` is a legal npm
|
|
389
499
|
* name. The cost on this path is behavioural twice over — the gate would
|
|
390
|
-
* weigh a function as the installed spec, and a phantom `isUpdate` would
|
|
391
|
-
*
|
|
500
|
+
* weigh a function as the installed spec, and a phantom `isUpdate` would
|
|
501
|
+
* refuse a hot mount to a package this process never imported.
|
|
392
502
|
*/
|
|
393
503
|
private installedSpecOf;
|
|
504
|
+
/** Add every dependency the profile manifest holds to the `imported`
|
|
505
|
+
* record (see `importedThisProcess`): each is a package dsh's loader may
|
|
506
|
+
* have loaded in this process, at boot or by an install since. An
|
|
507
|
+
* unreadable manifest adds nothing. */
|
|
508
|
+
private recordDependenciesImported;
|
|
394
509
|
private ownedEntryIdsOrNone;
|
|
395
510
|
/** Whether an installed package declares `dsh.client`. Reads through the
|
|
396
511
|
* `hotFs` option — `HotFs` is `hot.ts`'s type, but this gateway forwards
|
|
@@ -416,15 +531,50 @@ export declare class ShopGateway extends TypertRemoteService {
|
|
|
416
531
|
* the loop was computing.
|
|
417
532
|
*/
|
|
418
533
|
private liveEntriesDown;
|
|
419
|
-
/** Enable or disable one installed plugin, hot (§8):
|
|
420
|
-
*
|
|
421
|
-
*
|
|
422
|
-
*
|
|
423
|
-
*
|
|
534
|
+
/** Enable or disable one installed plugin, hot (§8): the write sets the
|
|
535
|
+
* `disabled` key of each owned entry's row in the user layer and changes
|
|
536
|
+
* nothing else there (design 2026-09-26-market-borrowings §2) — the CLI's
|
|
537
|
+
* watchUserPatches applies it through HMR. The shop's own row and the
|
|
538
|
+
* framework's bundles are never toggleable: disabling the host chain would
|
|
539
|
+
* break HMR itself. */
|
|
424
540
|
setEnabled(args: {
|
|
425
541
|
name: string;
|
|
426
542
|
enabled: boolean;
|
|
427
543
|
}): Promise<ShopSetEnabledResult>;
|
|
544
|
+
/**
|
|
545
|
+
* `setEnabled` for an installed package whose bundle the profile does not
|
|
546
|
+
* select.
|
|
547
|
+
*
|
|
548
|
+
* Switching it off writes the plugin-level rows and nothing else: none of
|
|
549
|
+
* it runs, and the rows keep it off if dsh selects the bundle again.
|
|
550
|
+
* Switching it on selects the bundle through dsh's own `pluginManager`, the
|
|
551
|
+
* writer behind the switch that deselected it, and then clears the
|
|
552
|
+
* plugin-level rows as an ordinary enable does. A harness without that
|
|
553
|
+
* service gets the way back in words instead. `dsh plugin add <name>` is
|
|
554
|
+
* not printed: it selects the bundle again only on 0.1.5, and it lets pnpm
|
|
555
|
+
* float a registry package to `latest`.
|
|
556
|
+
*/
|
|
557
|
+
private setDeselectedEnabled;
|
|
558
|
+
/**
|
|
559
|
+
* dsh's answer to a switch or a bundle selection, read the one way both
|
|
560
|
+
* callers need it (R32): the change, scrubbed for a desktop reader, and the
|
|
561
|
+
* detail that fails the request, or null when dsh applied it. `act` is what
|
|
562
|
+
* was asked, as "switch <name>", `request` names the request itself, as
|
|
563
|
+
* "the switch for <name>", and `stays` is the state the plugin keeps when
|
|
564
|
+
* the request does not take effect. An application this shop does not know
|
|
565
|
+
* fails rather than reading as applied: a later harness may answer
|
|
566
|
+
* something new.
|
|
567
|
+
*
|
|
568
|
+
* `overridden` fails too (R39). dsh answers it when, after it wrote the row
|
|
569
|
+
* and reloaded, the live entry still differs from the request
|
|
570
|
+
* (dsh-plugin-manager 0.1.7-rc.2, setPluginEnabled), and its README names
|
|
571
|
+
* what outranks the profile's own patch: home and invocation patches keep
|
|
572
|
+
* their higher priority. The change is saved and the plugin runs as it
|
|
573
|
+
* did, so a success would flip the client's switch over a plugin that did
|
|
574
|
+
* not change, and that switch never re-reads the live state.
|
|
575
|
+
* setBundleEnabled never answers it; a selection reads it the same way.
|
|
576
|
+
*/
|
|
577
|
+
private readManagerAnswer;
|
|
428
578
|
private rowConfig;
|
|
429
579
|
/** The user's own registry, read once. The self-update check and catalog
|
|
430
580
|
* race share this preference so neither path repeatedly touches ~/.npmrc. */
|
|
@@ -447,11 +597,16 @@ export declare class ShopGateway extends TypertRemoteService {
|
|
|
447
597
|
* static does. One ordered list, read by `restart()` before it commits and
|
|
448
598
|
* by `version()` so the client can say the same thing up front.
|
|
449
599
|
*
|
|
450
|
-
* The order is
|
|
451
|
-
*
|
|
452
|
-
*
|
|
453
|
-
*
|
|
600
|
+
* The order is load-bearing for the copy a reader sees. The desktop
|
|
601
|
+
* profile first: dsh itself refuses to launch it, so no platform,
|
|
602
|
+
* supervisor or port could make a restart possible there. Then Windows,
|
|
603
|
+
* because the platform check has no override and reporting the systemd one
|
|
604
|
+
* there sends a Windows user to set `allowRestart: true`, which this gate
|
|
605
|
+
* would still refuse.
|
|
454
606
|
*
|
|
607
|
+
* - `desktop`: dsh's CLI refuses `--profile desktop` ("managed exclusively
|
|
608
|
+
* by the Electron application"), so the relaunch would exit at once and
|
|
609
|
+
* leave nothing on the port; the app restarts it.
|
|
455
610
|
* - `windows`: the handoff helper is a POSIX shell one-liner (restart.ts)
|
|
456
611
|
* and there is no `sh` on Windows. That spawn fails ASYNCHRONOUSLY, so
|
|
457
612
|
* committing would answer `ok: true`, exit this process, and leave nothing
|
|
@@ -468,12 +623,72 @@ export declare class ShopGateway extends TypertRemoteService {
|
|
|
468
623
|
catalog(args?: {
|
|
469
624
|
refresh?: boolean;
|
|
470
625
|
}): Promise<ShopCatalogResult>;
|
|
626
|
+
/**
|
|
627
|
+
* The installs the running dsh will refuse on their harness peers
|
|
628
|
+
* (`peerVerdictsOf`), judged against the exemptions this profile holds —
|
|
629
|
+
* read on every call like the other verdicts, because recording one is
|
|
630
|
+
* exactly the event that must clear a card: the reader runs the command
|
|
631
|
+
* the card shows, presses Refresh, and the install is allowed. Empty when
|
|
632
|
+
* the running dsh has no such check (before 0.1.7) or no profile was
|
|
633
|
+
* found, and when either read throws: a refusal nobody could establish is
|
|
634
|
+
* never shown, least of all as a disabled button.
|
|
635
|
+
*/
|
|
636
|
+
private refusedByHarness;
|
|
637
|
+
/**
|
|
638
|
+
* The bundles the profile at `profileDir` composes, or null when its
|
|
639
|
+
* manifest cannot be read — which is no verdict on the profile half rather
|
|
640
|
+
* than an accusation. `dsh.profile.bundles` is the same list
|
|
641
|
+
* `discoverProfile` uses to recognize a profile directory, read through the
|
|
642
|
+
* harness's own parser so a profile this shop was booted inside is read the
|
|
643
|
+
* way dsh reads it.
|
|
644
|
+
*/
|
|
645
|
+
private runningProfileBundles;
|
|
471
646
|
/**
|
|
472
647
|
* Install one cataloged version into the profile (§7.2). The rejection
|
|
473
648
|
* paths run against this Host's snapshot before anything is spawned; only a
|
|
474
649
|
* passing request reaches the executor.
|
|
475
650
|
*/
|
|
476
651
|
install(args: InstallArgs): Promise<ShopInstallResult>;
|
|
652
|
+
/**
|
|
653
|
+
* What would stop the profile from starting now that `name`'s files are on
|
|
654
|
+
* disk, or null: an entry id another installed package already declares,
|
|
655
|
+
* or a bundle patch declaration this dsh cannot read. Neither is knowable
|
|
656
|
+
* before the files land. The package stays on disk either way, so each
|
|
657
|
+
* detail ends with `undo`, the sentence that says how to take the install
|
|
658
|
+
* back on the path that made it.
|
|
659
|
+
*/
|
|
660
|
+
private postInstallHazard;
|
|
661
|
+
/** Remember a github entry's pinned commit: the manifest records only
|
|
662
|
+
* `github:owner/slug`, so the pins file is how `installed()` reports
|
|
663
|
+
* outdated honestly. A failed install leaves a pin behind, but the
|
|
664
|
+
* manifest presence gate keeps it invisible. */
|
|
665
|
+
private recordGithubPin;
|
|
666
|
+
/** Forget an uninstalled package's pins: its own, and its identity pin when
|
|
667
|
+
* a catalog row matched the installed spec. Both uninstall paths call this
|
|
668
|
+
* once their operation has started, so a stale pin never outlives the
|
|
669
|
+
* uninstall in the shop's cache either way. The plugin manager path tracks
|
|
670
|
+
* its record first, so a write here that throws cannot hide an operation
|
|
671
|
+
* already queued. */
|
|
672
|
+
private forgetPins;
|
|
673
|
+
/** What `managerOutcome` reads dsh's answer against. The gateway's own
|
|
674
|
+
* facts are set here once, for every operation through the service: the
|
|
675
|
+
* profile, whether it is the desktop one, and the deadline a timeout
|
|
676
|
+
* detail names. */
|
|
677
|
+
private outcomeContext;
|
|
678
|
+
/**
|
|
679
|
+
* One install-shaped operation through dsh's `pluginManager` (design
|
|
680
|
+
* 2026-09-26-plugin-manager-delegation, section 6): `installBundle(spec)`
|
|
681
|
+
* under the record's own id as its request id, so the output dsh streams
|
|
682
|
+
* for it reaches this record, in the profile's queue with the download
|
|
683
|
+
* phase the CLI path has. At the shop's deadline dsh is asked to cancel it;
|
|
684
|
+
* a service that offers no `cancelInstall` gets no deadline, and its record
|
|
685
|
+
* settles when dsh answers. `install` and `updateStart` differ only in the
|
|
686
|
+
* mechanism line and in how they read the answer.
|
|
687
|
+
*/
|
|
688
|
+
private startManagerInstall;
|
|
689
|
+
/** Keep a started record where `installStatus`, the restart gate and
|
|
690
|
+
* eviction find it, then bound the finished ones. */
|
|
691
|
+
private track;
|
|
477
692
|
/** Bound retained finished records at MAX_FINISHED_INSTALLS, evicting the
|
|
478
693
|
* oldest finished ones (insertion order, oldest first). Live records —
|
|
479
694
|
* running AND queued — are never evicted; an id absent from the map reports
|
|
@@ -534,6 +749,21 @@ export declare class ShopGateway extends TypertRemoteService {
|
|
|
534
749
|
* and the client shows the version alone. `installed` is the RUNNING
|
|
535
750
|
* version (own-version.ts), not the manifest's range spec. */
|
|
536
751
|
version(): Promise<ShopVersionResult>;
|
|
752
|
+
/**
|
|
753
|
+
* The version of the shop the profile has installed, when it is not the
|
|
754
|
+
* one this process runs; null when they match or the copy cannot be read.
|
|
755
|
+
*
|
|
756
|
+
* Read from the profile's own copy — the directory `dsh plugin add` writes
|
|
757
|
+
* and the next boot loads — rather than from the file beside this module.
|
|
758
|
+
* For the hoisted install those are one directory, but a `link:` install
|
|
759
|
+
* runs from its link target, which an update replaces in the profile and
|
|
760
|
+
* never touches at the target, so reading beside the module would never see
|
|
761
|
+
* the update land.
|
|
762
|
+
*
|
|
763
|
+
* Unreadable is null, never a throw: the version check is advisory, and a
|
|
764
|
+
* missing field would tell the client this host predates it.
|
|
765
|
+
*/
|
|
766
|
+
private pendingVersion;
|
|
537
767
|
/** Update the shop itself to a published version (§7.3): the explicit pin
|
|
538
768
|
* is the only install form that bypasses pnpm's release cooldown. The
|
|
539
769
|
* version is re-validated as plain semver at the boundary — the spec
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Install gate: the gate rejection paths of §7.2, as a pure function. */
|
|
2
2
|
import type { CatalogSnapshot } from './catalog.ts';
|
|
3
3
|
import type { CatalogEntry } from './types.ts';
|
|
4
|
-
export type InstallRejectionCode = 'not-in-catalog' | 'denied' | 'version-mismatch' | 'needs-acknowledgement' | 'tarball-integrity' | 'ambiguous-identity' | 'name-taken';
|
|
4
|
+
export type InstallRejectionCode = 'not-in-catalog' | 'denied' | 'version-mismatch' | 'needs-acknowledgement' | 'tarball-integrity' | 'ambiguous-identity' | 'name-taken' | 'desktop-profile';
|
|
5
5
|
export interface InstallArgs {
|
|
6
6
|
name: string;
|
|
7
7
|
version: string;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One package operation through dsh's `pluginManager` service, tracked as the
|
|
3
|
+
* same `RunningInstall` the CLI executor produces: design
|
|
4
|
+
* 2026-09-26-plugin-manager-delegation, section 6. It runs in the same
|
|
5
|
+
* per-profile queue as CLI commands, so the two paths are never concurrent in
|
|
6
|
+
* one profile.
|
|
7
|
+
*/
|
|
8
|
+
import { type RunningInstall } from './executor.ts';
|
|
9
|
+
import { type ManagerOutcome } from './plugin-manager.ts';
|
|
10
|
+
import type { Prefetcher } from './prefetch.ts';
|
|
11
|
+
/** The first line of every record this runner writes. The shop's log panel
|
|
12
|
+
* shows it and the 0.1.7 e2e asserts it, so a silent fall back to the CLI
|
|
13
|
+
* fails a case instead of passing it. */
|
|
14
|
+
export declare const MECHANISM_PREFIX = "via dsh's plugin manager:";
|
|
15
|
+
/** Routes the service's `plugin-manager/install-log` chunks to the record
|
|
16
|
+
* whose request id they carry. A chunk for any other id is dropped. dsh
|
|
17
|
+
* tags each chunk with the stream it came from; a non-string stream reads
|
|
18
|
+
* as `stdout`, matching a harness that predates the tag. */
|
|
19
|
+
export declare class ManagerLogs {
|
|
20
|
+
private readonly sinks;
|
|
21
|
+
open(requestId: string, sink: (text: string, stream: string) => void): () => void;
|
|
22
|
+
chunk(requestId: unknown, text: unknown, stream?: unknown): void;
|
|
23
|
+
}
|
|
24
|
+
export interface ManagerOperationOptions {
|
|
25
|
+
profile: string;
|
|
26
|
+
/** The record's id, handed to the service as its request id. */
|
|
27
|
+
requestId: string;
|
|
28
|
+
/** What the mechanism line says after the prefix, e.g. `install dsh-a@1.0.0`. */
|
|
29
|
+
mechanism: string;
|
|
30
|
+
logs: ManagerLogs;
|
|
31
|
+
run: (requestId: string) => Promise<unknown>;
|
|
32
|
+
cancel?: (requestId: string) => Promise<unknown>;
|
|
33
|
+
outcome: (raw: unknown) => ManagerOutcome;
|
|
34
|
+
alsoConfirm?: () => string | null;
|
|
35
|
+
/** The download phase, as on the CLI path: `spec` is what it warms. */
|
|
36
|
+
prefetcher?: Prefetcher;
|
|
37
|
+
spec?: string;
|
|
38
|
+
env?: NodeJS.ProcessEnv;
|
|
39
|
+
timeoutMs?: number;
|
|
40
|
+
/** The desktop profile, whose readers can run no `dsh plugin` command: a
|
|
41
|
+
* message the service call throws is scrubbed as dsh's answer is. */
|
|
42
|
+
desktop?: boolean;
|
|
43
|
+
}
|
|
44
|
+
/** What a thrown value says about itself, and total: describing an error is
|
|
45
|
+
* never itself the reason a settle fails. An `Error` gives its `message`,
|
|
46
|
+
* anything else `String(value)`. A message that is not a string, or a
|
|
47
|
+
* description that throws anywhere, reads as one fixed string: a Proxy whose
|
|
48
|
+
* getPrototypeOf trap throws defeats `instanceof` itself, a `message` getter
|
|
49
|
+
* can throw, and a null-prototype object has no conversion to a string. */
|
|
50
|
+
export declare const messageOf: (error: unknown) => string;
|
|
51
|
+
export declare function startManagerOperation(options: ManagerOperationOptions): RunningInstall;
|