@twinsearth/w2m-dsh-plugin 0.5.4 → 0.5.6
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/CHANGELOG.md +133 -0
- package/README.md +3 -1
- package/cordis.patch.yml +27 -4
- package/lib/client.js +145 -18
- package/lib/index.js +16 -11
- package/lib/tools.js +9 -3
- package/package.json +1 -1
- package/src/agent/mdns-wire.mjs +1062 -0
- package/src/agent/mdns.mjs +1311 -0
- package/src/plugin/fleet-target.mjs +84 -20
- package/src/plugin/mdns-discover.mjs +577 -0
- package/src/plugin/tools.mjs +43 -5
package/CHANGELOG.md
CHANGED
|
@@ -4,8 +4,141 @@ All notable changes to this project are documented here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
|
|
5
5
|
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.5.5] — 2026-10-11
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
* **The v0.5.4 loader failure, with the cause the loader actually reported** rather than the hypothesis
|
|
12
|
+
that stood in for it:
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
w2m (@twinsearth/w2m-dsh-plugin/tools): Error: cannot get property "connection" without inject
|
|
16
|
+
at registerTargetRoutes (src/plugin/fleet-target.mjs:483:25)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
cordis throws on reading a service property the fiber never declared, so the entry died and took all
|
|
20
|
+
eight tools with it. **Every existing test was blind to this**, because they drive the plugin with a
|
|
21
|
+
plain-object context where an undeclared read merely yields `undefined` — the fake was more forgiving
|
|
22
|
+
than the runtime. Fixed with `ctx.inject(['connection'], …)`, which is also the only shape that can
|
|
23
|
+
work: in the real composition the service is genuinely absent while `apply()` runs, and siblings are
|
|
24
|
+
mounted concurrently.
|
|
25
|
+
* **The client half was never composed either, for a second and independent reason:** the bundle row
|
|
26
|
+
named the subpath `@twinsearth/w2m-dsh-plugin/tools` instead of the package, and a client package is
|
|
27
|
+
identified by its Loader row specifier — a subpath resolves to no `package.json`, so `dsh.client` was
|
|
28
|
+
never read and no boot-graph row was built. The row now names the package.
|
|
29
|
+
* **A silent route rejection:** the control fetched `/w2m/target`, but fetch routes are validated
|
|
30
|
+
against `/api/`, and the refusal failed only a child fiber, so nothing surfaced. Now
|
|
31
|
+
`/api/w2m/target`, mirrored in the shipped bundle.
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
* **`test/loader-entry.test.mjs`** — the test that would have caught v0.5.4: it packs the tree, installs
|
|
36
|
+
the tarball with the installation's own `dsh plugin add` into a throwaway profile under a throwaway
|
|
37
|
+
`DSH_HOME`, and boots it with the real `runProfile` under the Harness runtime, asserting that the
|
|
38
|
+
entry activates, all eight tools are registered, the boot-graph row is composed from the shipped
|
|
39
|
+
bundle, and no failed entry names the package. **Proved in both directions**: 6/6 pass on the fixed
|
|
40
|
+
tree and 6/6 fail against the preserved v0.5.4 tarball, failing on precisely the loader error quoted
|
|
41
|
+
above. It skips loudly, naming the probes it tried, when no installation is found.
|
|
42
|
+
* Cheap always-on guards in `test/client-bundle.test.mjs` that need no Harness: the platform field, the
|
|
43
|
+
client export, the row must be the bare package name, and the bundle's route must equal the host's.
|
|
44
|
+
|
|
45
|
+
### Notes
|
|
46
|
+
|
|
47
|
+
* **An upgrade from v0.5.4 requires one edit outside this package:** `cordis.patch.yml` in the profile
|
|
48
|
+
must name the package (`@twinsearth/w2m-dsh-plugin`), not the `/tools` subpath. With the corrected
|
|
49
|
+
bundle row, a mismatch makes the loader skip the whole patch — silently reverting `rabbitUrl`,
|
|
50
|
+
`operatorToken`, `stateDir`, `projectDir`, `machineName`, `p2pMode` and `allowedCommands` to their
|
|
51
|
+
shipped defaults. Measured both ways with `--dump-config`.
|
|
52
|
+
* **0.5.4 remains DO NOT INSTALL.** This release exists so that a rebuilt artifact is distinguishable
|
|
53
|
+
from the broken one by version alone.
|
|
54
|
+
* Still unverified, and not claimed: how the control *renders*. No browser was opened. What is proven is
|
|
55
|
+
the entire host side — the entry activates, the eight tools register, the boot-graph row is composed
|
|
56
|
+
from the shipped bundle, the route is registered and attached — plus the seat shape in a VM test.
|
|
57
|
+
## [0.5.6] — 2026-10-11
|
|
58
|
+
|
|
59
|
+
### Fixed
|
|
60
|
+
|
|
61
|
+
* **The client entry could not activate, and that made the whole application refuse to start.**
|
|
62
|
+
v0.5.4 and v0.5.5 were withdrawn for it. The real cause, taken from the page itself rather than
|
|
63
|
+
guessed:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
w2m-client-probe: apply threw for @twinsearth/w2m-dsh-plugin: cannot get property "locale" without inject
|
|
67
|
+
Error: web boot: 1 entry did not activate
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Cordis throws for any `ctx.<name>` a fiber reads without declaring it, and the client half declared
|
|
71
|
+
`inject = []`. **The first throw was `locale`, not `slots`** — so declaring the seat alone, or guarding
|
|
72
|
+
the locale alone, would each still have died. Now `inject = ['slots']` (a declared name is a *required*
|
|
73
|
+
dependency, which is why `locale` is deliberately not among them), the optional carrier is read
|
|
74
|
+
inject-free with `ctx.get('locale')` while its dictionary registration rides a child fiber, a context
|
|
75
|
+
with no usable seat becomes a **no-op with a diagnostic** instead of taking the page down, and `apply`
|
|
76
|
+
prints the inner message to the error-level renderer console — the channel the crash log reads — and
|
|
77
|
+
then rethrows, because a swallow would boot a healthy-looking page with no control.
|
|
78
|
+
* **A failed entry was invisible unless you read fiber state.** `cordis-plugin-loader` no longer
|
|
79
|
+
propagates a failed mount to `await ctx.loader.await()`, and the loader driver inspected only the one
|
|
80
|
+
entry it knew about — so a failure landing on a **child** fiber (our silently-refused route) went
|
|
81
|
+
unseen. The driver now sweeps the whole registry and reports `failedFibers`.
|
|
82
|
+
|
|
83
|
+
### Added
|
|
84
|
+
|
|
85
|
+
* **A gate that would have caught all of it**: `test/loader-entry.test.mjs` packs the tree, installs the
|
|
86
|
+
tarball with the installation's own `dsh plugin add` into a throwaway profile under a throwaway
|
|
87
|
+
`DSH_HOME`, boots it with the real `runProfile`, activates the client entries through the real module
|
|
88
|
+
system, and — when a browser is present — boots the profile's own page and asserts our control renders.
|
|
89
|
+
Proven in both directions: **10/10 on this tree, 2 failures against the preserved pre-fix tarball, 9
|
|
90
|
+
against the v0.5.4 one**, and with a deliberately injected route defect it names the **child** fiber
|
|
91
|
+
while "entry active" and "eight tools" still pass. It reports one stage per line, so a failure names
|
|
92
|
+
its cause.
|
|
93
|
+
* **`test/claims.test.mjs`** — 22 checks that our own published claims match the artifact, with the
|
|
94
|
+
registered tool set *measured* by importing the entry. It caught a stale claim in our README on its
|
|
95
|
+
**first run** (`README.md` said the installer asserts 5 tools; it asserts 8), and its author's own
|
|
96
|
+
mutation proof caught two false greens in its first draft — a scan that extracted zero tool names and
|
|
97
|
+
a lookahead that invented one out of a metric.
|
|
98
|
+
* **`TROUBLESHOOTING.md`** — 17 symptom-indexed sections that name the culprit when it is not us (pnpm's
|
|
99
|
+
`minimumReleaseAge`, carrier-grade NAT, the OS firewall, an installer that rewrites the profile patch),
|
|
100
|
+
ending with a data-location table and a reason-code table.
|
|
101
|
+
* **`SECURITY.md`** — a private reporting channel and **no public-issue route**, an honest support table,
|
|
102
|
+
and the consent model written out: what a paired machine may and may not do, and that we never write to
|
|
103
|
+
another machine's `authorized_keys`.
|
|
104
|
+
* **`CONTRIBUTING.md`** — one page of falsifiable conventions, including **"a test that would pass
|
|
105
|
+
against a broken artifact is not evidence"** and **"run mutation proofs in a copy, never in the shared
|
|
106
|
+
tree"** (both learned the hard way).
|
|
107
|
+
* **Opt-in LAN discovery** (`src/agent/mdns-wire.mjs`, `src/agent/mdns.mjs`, `src/plugin/mdns-discover.mjs`):
|
|
108
|
+
a bounded, hand-written DNS/mDNS codec with parse hardening (a pointer may only name a *prior* offset, so
|
|
109
|
+
termination is a property of the format; a 128-hop budget reported separately from a loop; label, name,
|
|
110
|
+
record and packet caps; every read bounds-checked) and a discovery source exposed through
|
|
111
|
+
`w2m_devices`' new `include_discovered` (default `false`). **Discovery is a hint, never an
|
|
112
|
+
authorisation**: a discovered device is refused as a dispatch target unless the relay already lists it
|
|
113
|
+
and it is online, and the positive control is tested so the rule cannot pass by refusing everything.
|
|
114
|
+
|
|
115
|
+
### Notes
|
|
116
|
+
|
|
117
|
+
* **v0.5.4 and v0.5.5 remain DO NOT INSTALL.** This release also exists so that `latest` on the registry,
|
|
118
|
+
which still pointed at v0.5.5, stops handing out a version that prevents the application from starting.
|
|
119
|
+
* **Upgrading from v0.5.4 needs one edit outside this package**: the profile's `cordis.patch.yml` must
|
|
120
|
+
name the package (`@twinsearth/w2m-dsh-plugin`), not the `/tools` subpath. With the corrected bundle row
|
|
121
|
+
a mismatch makes the loader skip the whole patch and silently revert every configured value to its
|
|
122
|
+
default. The claims gate now checks this.
|
|
123
|
+
* **LAN discovery is unverified on a real network.** Every socket in its suite is loopback and no packet
|
|
124
|
+
left the host, so it says nothing about whether a switch forwards `224.0.0.251` or how a real
|
|
125
|
+
Bonjour/Avahi peer answers. There is **no third-party interoperability test**, no probing or conflict
|
|
126
|
+
resolution, IPv4 only, and the responder is **built and tested but not started** by the plugin —
|
|
127
|
+
loading a plugin should not silently multicast on the host's network.
|
|
7
128
|
## [0.5.4] — 2026-10-11
|
|
8
129
|
|
|
130
|
+
> **DO NOT INSTALL 0.5.4.** It fails to load in the Harness: the loader entry
|
|
131
|
+
> include:@twinsearth/w2m-dsh-plugin/tools reports iberPhase: failed, so **all eight tools
|
|
132
|
+
> disappear** and the client half never registers. The package verifies in isolation
|
|
133
|
+
> (scripts/verify-installed.mjs reports 8 tools registered) and its exports map is complete, so the
|
|
134
|
+
> failure is in the real loader path -- the newly declared dsh.client half being composed there, with
|
|
135
|
+
> lib/client.js being browser code that has no window under a host-side load. That cause is a
|
|
136
|
+
> hypothesis, not a measurement; the loader's own error is still to be read. 0.5.5 fixes it and adds the
|
|
137
|
+
> test that should have caught it: **load the packed artifact through the real loader path**, which is
|
|
138
|
+
> the step this release skipped. The tag and the npm version stay as published history; they are simply
|
|
139
|
+
> not installable.
|
|
140
|
+
|
|
141
|
+
|
|
9
142
|
### Added
|
|
10
143
|
|
|
11
144
|
* **A dispatch-target control inside the chat composer.** A client half registers into
|
package/README.md
CHANGED
|
@@ -535,7 +535,7 @@ the plugin:
|
|
|
535
535
|
|
|
536
536
|
```bash
|
|
537
537
|
node scripts/check-entrypoints.mjs # imports lib/tools.js, the exact specifier cordis.patch.yml mounts
|
|
538
|
-
node scripts/verify-installed.mjs <path-to-installed-package> # asserts the installed copy registers
|
|
538
|
+
node scripts/verify-installed.mjs <path-to-installed-package> # asserts the installed copy registers 8 tools
|
|
539
539
|
```
|
|
540
540
|
|
|
541
541
|
**Not verified:**
|
|
@@ -588,6 +588,8 @@ Issues and pull requests are welcome. If you change observable behaviour, update
|
|
|
588
588
|
`PROTOCOL.md` in the same commit — that file is the contract three components
|
|
589
589
|
agree on.
|
|
590
590
|
|
|
591
|
+
When something does not work, start from the symptom: [TROUBLESHOOTING.md](TROUBLESHOOTING.md).
|
|
592
|
+
|
|
591
593
|
## License
|
|
592
594
|
|
|
593
595
|
[MIT](LICENSE)
|
package/cordis.patch.yml
CHANGED
|
@@ -1,7 +1,30 @@
|
|
|
1
1
|
# This file is the bundle patch declared by package.json's `dsh.bundle.patch`.
|
|
2
|
-
# It mounts the plugin
|
|
3
|
-
#
|
|
4
|
-
#
|
|
2
|
+
# It mounts the plugin by its package name, whose root export is the plugin
|
|
3
|
+
# (`lib/index.js` re-exports `./tools.js`), so reading this file tells you which
|
|
4
|
+
# package becomes the plugin.
|
|
5
|
+
#
|
|
6
|
+
# ---------------------------------------------------------------------------
|
|
7
|
+
# Why the row names the package and not the `/tools` subpath (v0.5.4)
|
|
8
|
+
# ---------------------------------------------------------------------------
|
|
9
|
+
# Until v0.5.4 this row said `name: '@twinsearth/w2m-dsh-plugin/tools'`, and for a
|
|
10
|
+
# host-only plugin that was fine. It stops being fine the moment the package also
|
|
11
|
+
# ships a browser half, because the client module system identifies a package by
|
|
12
|
+
# the *package specifier* its Loader row carries:
|
|
13
|
+
#
|
|
14
|
+
# client-modules: resolveMeta(loaderName, baseUrl) (dsh-client-modules)
|
|
15
|
+
# exactPackageSpecifier('@twinsearth/w2m-dsh-plugin/tools') -> undefined
|
|
16
|
+
# ... -> locatePkgJson returns undefined -> the entry is not a client package
|
|
17
|
+
#
|
|
18
|
+
# A subpath specifier resolves to no package manifest at all, so a `/tools` row is
|
|
19
|
+
# invisible to the scan: `lib/client.js` is never composed into `__DSH_BOOT__`, and
|
|
20
|
+
# the composer control can never register -- whatever else is fixed. Measured on the
|
|
21
|
+
# 0.5.4 tarball through the real Loader: the composed boot graph held rows for
|
|
22
|
+
# `@deepseek-ai/dsh-typert-registry` and `@deepseek-ai/dsh-api-gateway` and none for
|
|
23
|
+
# this package. Naming the bare package fixes it, because the root export already
|
|
24
|
+
# re-exports exactly what `/tools` exports (`test/tools.test.mjs` asserts the two
|
|
25
|
+
# carry the same `inject`), so the host half is unchanged. `./tools` remains a
|
|
26
|
+
# supported mount point for anyone who wants it -- it is simply not the one this
|
|
27
|
+
# bundle uses.
|
|
5
28
|
#
|
|
6
29
|
# ---------------------------------------------------------------------------
|
|
7
30
|
# Why this is an `insert:` list and not a bare `- id: w2m`
|
|
@@ -83,7 +106,7 @@
|
|
|
83
106
|
# is visible rather than assumed.
|
|
84
107
|
- insert:
|
|
85
108
|
- id: w2m
|
|
86
|
-
name: '@twinsearth/w2m-dsh-plugin
|
|
109
|
+
name: '@twinsearth/w2m-dsh-plugin'
|
|
87
110
|
config:
|
|
88
111
|
rabbitUrl: ''
|
|
89
112
|
operatorToken: ''
|
package/lib/client.js
CHANGED
|
@@ -8,7 +8,25 @@
|
|
|
8
8
|
* web shell's loader consumes (`window.__ModuleLoader__.load({ id, factory })`, a lazy CJS table
|
|
9
9
|
* whose factory runs at materialization). `package.json` declares it with
|
|
10
10
|
* `dsh.client: { platform: 'web' }` and `exports['./client'] = './lib/client.js'`, which is what the
|
|
11
|
-
* host scans for
|
|
11
|
+
* host scans for. There is no second copy to keep in sync: this file **is** the source, and
|
|
12
|
+
* `test/client-bundle.test.mjs` is where its entry shape is read.
|
|
13
|
+
*
|
|
14
|
+
* # A failed client entry is fatal, and silence is not a neutral failure
|
|
15
|
+
*
|
|
16
|
+
* Measured twice in production: when this entry's `apply` throws, the page does not degrade — the
|
|
17
|
+
* shell audits every entry after activation and refuses to boot with
|
|
18
|
+
* `web boot: 1 entry did not activate / @twinsearth/w2m-dsh-plugin: failed`, which in the desktop
|
|
19
|
+
* app is forwarded to the main process and written as a `web-boot` crash: **the app does not start**.
|
|
20
|
+
* The audit prints the entry's *name* and its fiber state and nothing else, so the cause has to be
|
|
21
|
+
* carried by the failing code itself; {@link apply} therefore reports its own error to
|
|
22
|
+
* `console.error`, which is the error-level renderer console the crash log already captures.
|
|
23
|
+
*
|
|
24
|
+
* The failure that cost those two outages was a single property read. Cordis resolves `ctx.<name>`
|
|
25
|
+
* through the fiber's `inject` list: a service the fiber never declared **throws**
|
|
26
|
+
* (`cannot get property "locale" without inject`) whether or not some plugin provides it, and every
|
|
27
|
+
* name that *is* declared becomes a required dependency the entry waits for. Both halves of that rule
|
|
28
|
+
* are load-bearing here — see {@link inject} for what this bundle declares, and how the optional
|
|
29
|
+
* locale carrier is read instead.
|
|
12
30
|
*
|
|
13
31
|
* Two consequences, both deliberate:
|
|
14
32
|
*
|
|
@@ -52,10 +70,12 @@ window.__ModuleLoader__.load({
|
|
|
52
70
|
// Wire
|
|
53
71
|
// -------------------------------------------------------------------------------------------
|
|
54
72
|
//
|
|
55
|
-
// Mirrors TARGET_ROUTE_PATH in src/plugin/fleet-target.mjs
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
|
|
73
|
+
// Mirrors TARGET_ROUTE_PATH in src/plugin/fleet-target.mjs, `/api` prefix included: the host's
|
|
74
|
+
// Connection service mounts its exact-fetch-route registry under that channel and refuses any
|
|
75
|
+
// path that does not start with `/api/`. Plain same-origin `fetch` rather than a Remote method:
|
|
76
|
+
// the route is mounted inside the host's authentication fence, so the page's own cookie is the
|
|
77
|
+
// credential and there is no descriptor to keep in sync.
|
|
78
|
+
const ROUTE = '/api/w2m/target';
|
|
59
79
|
|
|
60
80
|
/**
|
|
61
81
|
* The locale namespace and its two dictionaries.
|
|
@@ -107,12 +127,18 @@ window.__ModuleLoader__.load({
|
|
|
107
127
|
* `{name}` substitution is deliberately the only templating: these strings have one variable
|
|
108
128
|
* each, and a message format would be a dependency this package does not have.
|
|
109
129
|
*
|
|
110
|
-
* @param
|
|
130
|
+
* @param localeOf - the shell's locale service, or a function returning it (read at lookup time).
|
|
131
|
+
* Passed in rather than read from `ctx` here: the service is optional, and reading an optional
|
|
132
|
+
* service off the context is the one operation in this file that can kill the page (see
|
|
133
|
+
* {@link readOptionalService}). A function is accepted because the service may arrive *after*
|
|
134
|
+
* this entry activates — the shell's `bind()` is live, so a deferred read still localises.
|
|
111
135
|
* @returns {(key: string, values?: object) => string}
|
|
112
136
|
*/
|
|
113
|
-
function makeTranslator(
|
|
114
|
-
const
|
|
137
|
+
function makeTranslator(localeOf) {
|
|
138
|
+
const resolveLocale = () => (typeof localeOf === 'function' ? localeOf() : localeOf);
|
|
115
139
|
return (key, values) => {
|
|
140
|
+
const locale = resolveLocale();
|
|
141
|
+
const bound = typeof locale?.bind === 'function' ? locale.bind(NS) : null;
|
|
116
142
|
const template = bound === null
|
|
117
143
|
? (DICT.zh[key] ?? DICT.en[key] ?? key)
|
|
118
144
|
: bound(key, values);
|
|
@@ -122,6 +148,50 @@ window.__ModuleLoader__.load({
|
|
|
122
148
|
};
|
|
123
149
|
}
|
|
124
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Read a service the composition may or may not provide, without the inject requirement.
|
|
153
|
+
*
|
|
154
|
+
* `ctx.get(name)` is Cordis's inject-free lookup, and it is the only correct way to read an
|
|
155
|
+
* *optional* service from a plugin: `ctx[name]` throws for anything the fiber did not declare as
|
|
156
|
+
* a dependency, and declaring it would make the service *required* — an entry whose declared
|
|
157
|
+
* dependency never arrives never activates either, which is fatal in exactly the same way. So a
|
|
158
|
+
* carrier that must not be able to block the control (`locale`) is read here, and a service the
|
|
159
|
+
* control cannot work without (`slots`) is declared in {@link inject}.
|
|
160
|
+
*
|
|
161
|
+
* @param ctx - the client plugin context.
|
|
162
|
+
* @param name - the service name.
|
|
163
|
+
* @returns the service, or undefined when the composition has none (or this is not a Cordis
|
|
164
|
+
* context at all, which is how `test/client-bundle.test.mjs` applies this bundle).
|
|
165
|
+
*/
|
|
166
|
+
function readOptionalService(ctx, name) {
|
|
167
|
+
return typeof ctx?.get === 'function' ? ctx.get(name) : undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Register {@link DICT} with the shell's locale service whenever that service exists.
|
|
172
|
+
*
|
|
173
|
+
* Two paths, one outcome. When the service is already up, the registration is an effect of this
|
|
174
|
+
* entry and goes away with it. When it is not up yet, the registration rides a **child fiber**
|
|
175
|
+
* (`ctx.inject`) that waits for `locale` without holding this entry back: declaring `locale` in
|
|
176
|
+
* {@link inject} instead would make it required, and a composition without a locale service
|
|
177
|
+
* would then report this entry as `pending` — which the shell's audit treats exactly like
|
|
178
|
+
* `failed`, i.e. the page does not boot. A missing localisation service must never cost an
|
|
179
|
+
* operator the control, and must never cost them the app.
|
|
180
|
+
*
|
|
181
|
+
* @param ctx - the client plugin context.
|
|
182
|
+
* @param locale - the shell's locale service, or undefined when the composition has none.
|
|
183
|
+
*/
|
|
184
|
+
function registerDictionaries(ctx, locale) {
|
|
185
|
+
if (typeof locale?.register === 'function') {
|
|
186
|
+
ctx.effect(() => locale.register(NS, DICT), 'w2m: composer target control dictionaries');
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (typeof ctx?.inject !== 'function') return;
|
|
190
|
+
ctx.inject(['locale'], (localeCtx) => {
|
|
191
|
+
localeCtx.effect(() => localeCtx.locale.register(NS, DICT), 'w2m: composer target control dictionaries');
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
125
195
|
/** One JSON call to the host half, folded into a value: a refusal is never an exception here. */
|
|
126
196
|
async function callHost(options) {
|
|
127
197
|
let response;
|
|
@@ -461,7 +531,33 @@ window.__ModuleLoader__.load({
|
|
|
461
531
|
}
|
|
462
532
|
|
|
463
533
|
/**
|
|
464
|
-
* Client plugin body.
|
|
534
|
+
* Client plugin body: install the stylesheet, register the dictionaries, take the seat.
|
|
535
|
+
*
|
|
536
|
+
* @param ctx - the client root context.
|
|
537
|
+
*/
|
|
538
|
+
function apply(ctx) {
|
|
539
|
+
try {
|
|
540
|
+
mount(ctx);
|
|
541
|
+
} catch (error) {
|
|
542
|
+
// The audit that decides whether the app boots reports the entry's name and the word
|
|
543
|
+
// "failed" and nothing else, so this is where the cause has to be said out loud. The
|
|
544
|
+
// renderer console at error level is the channel the desktop crash log already carries
|
|
545
|
+
// (`--- renderer console (error level, oldest first) ---`), which makes this the one place a
|
|
546
|
+
// browser half can put its own diagnosis next to the failure it caused.
|
|
547
|
+
console.error(
|
|
548
|
+
'@twinsearth/w2m-dsh-plugin: the client entry failed to activate, so this page will not boot: ' +
|
|
549
|
+
`${error instanceof Error ? error.message : String(error)}`,
|
|
550
|
+
error,
|
|
551
|
+
);
|
|
552
|
+
// Deliberately rethrown. A swallowed failure would boot a page that looks healthy with no
|
|
553
|
+
// target control in it — the silent-half-arrival failure this package has already shipped
|
|
554
|
+
// once (v0.5.4) and refuses to ship again.
|
|
555
|
+
throw error;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* The body of {@link apply}, wrapped so no failure can escape without a diagnostic.
|
|
465
561
|
*
|
|
466
562
|
* `slots.inject` (not a bare `register`) is what the slot's declaration requires of an entry
|
|
467
563
|
* from a package that does not own the seat: the parent declared `conversation.input.left`, and
|
|
@@ -475,17 +571,30 @@ window.__ModuleLoader__.load({
|
|
|
475
571
|
*
|
|
476
572
|
* @param ctx - the client root context.
|
|
477
573
|
*/
|
|
478
|
-
function
|
|
574
|
+
function mount(ctx) {
|
|
575
|
+
// `ctx.slots` is legal to read because {@link inject} declares it — and declaring it is also
|
|
576
|
+
// what makes this entry wait for the seat instead of racing it. The shape check is for callers
|
|
577
|
+
// that are not a Cordis fiber at all (`test/client-bundle.test.mjs`): a missing or foreign
|
|
578
|
+
// registry is a control that cannot be mounted, and a control that cannot be mounted must not
|
|
579
|
+
// be allowed to take the whole page down with it.
|
|
580
|
+
const slots = ctx?.slots;
|
|
581
|
+
if (typeof slots?.inject !== 'function' || typeof slots?.register !== 'function') {
|
|
582
|
+
console.error(
|
|
583
|
+
'@twinsearth/w2m-dsh-plugin: the client half found no slot registry on this context, so the ' +
|
|
584
|
+
'dispatch-target control is not mounted; the rest of the page is unaffected',
|
|
585
|
+
);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
479
588
|
const styleDisposer = injectStyle(typeof document === 'undefined' ? null : document);
|
|
480
589
|
if (styleDisposer !== null && typeof ctx.effect === 'function') {
|
|
481
590
|
ctx.effect(() => styleDisposer, 'w2m: composer target control styles');
|
|
482
591
|
}
|
|
483
|
-
// Dictionaries
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
const t = makeTranslator(ctx);
|
|
488
|
-
|
|
592
|
+
// Dictionaries as early as the composition allows: a namespace registered after the UI has
|
|
593
|
+
// mounted is still picked up, and the service is optional, so nothing here may block.
|
|
594
|
+
const locale = readOptionalService(ctx, 'locale');
|
|
595
|
+
registerDictionaries(ctx, locale);
|
|
596
|
+
const t = makeTranslator(() => readOptionalService(ctx, 'locale'));
|
|
597
|
+
slots.inject('conversation.input.left', () => slots.register({
|
|
489
598
|
name: 'conversation.input.left',
|
|
490
599
|
id: 'w2m-target',
|
|
491
600
|
// After the shipped permission control and the plan control, which sit earlier in the
|
|
@@ -500,8 +609,26 @@ window.__ModuleLoader__.load({
|
|
|
500
609
|
}, TargetSelect));
|
|
501
610
|
}
|
|
502
611
|
|
|
503
|
-
/**
|
|
504
|
-
|
|
612
|
+
/**
|
|
613
|
+
* Cordis services this entry depends on, read by the fiber that mounts this bundle.
|
|
614
|
+
*
|
|
615
|
+
* `slots` is the seat: the control is registered into `conversation.input.left` through the
|
|
616
|
+
* slot registry, and a fiber that cannot see `slots` has nowhere to register. Declaring it is
|
|
617
|
+
* not bookkeeping. Cordis resolves `ctx.<name>` against this list, so reading an undeclared
|
|
618
|
+
* service **throws** (`cannot get property "slots" without inject`) even when the service is
|
|
619
|
+
* provided — measured in production: this bundle shipped with `inject = []` while reading
|
|
620
|
+
* `ctx.locale` and `ctx.slots`, the first read threw, and the app refused to boot.
|
|
621
|
+
*
|
|
622
|
+
* `locale` is deliberately absent, and that is the other half of the same rule: **every**
|
|
623
|
+
* declared name is a required dependency, so an entry that declares a service the composition
|
|
624
|
+
* does not provide never reaches `active` either — the audit then reports it as
|
|
625
|
+
* `pending (waiting for service: locale)`, which is just as fatal. Optional carriers are read
|
|
626
|
+
* with {@link readOptionalService} instead, and waited for by a child fiber when waiting is
|
|
627
|
+
* useful ({@link registerDictionaries}). Keep those two decisions in step with the reads in
|
|
628
|
+
* {@link mount}: `test/client-bundle.test.mjs` mounts this bundle behind a context that throws
|
|
629
|
+
* on any undeclared service read, exactly as Cordis does.
|
|
630
|
+
*/
|
|
631
|
+
const inject = ['slots'];
|
|
505
632
|
|
|
506
633
|
exports.apply = apply;
|
|
507
634
|
exports.inject = inject;
|
package/lib/index.js
CHANGED
|
@@ -3,22 +3,27 @@
|
|
|
3
3
|
*
|
|
4
4
|
* TWO READERS, ONE EXPORT LIST
|
|
5
5
|
*
|
|
6
|
-
* `cordis.patch.yml`
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* `cordis.patch.yml` mounts this file: the profile reads the patch, sees
|
|
7
|
+
* `@twinsearth/w2m-dsh-plugin`, and loads `./index.js`. It names the *package* rather than the
|
|
8
|
+
* `tools` subpath on purpose, and the reason is measured rather than stylistic: the client module
|
|
9
|
+
* system (`@deepseek-ai/dsh-client-modules`) identifies a client package by the specifier its Loader
|
|
10
|
+
* row carries (`exactPackageSpecifier`), so a row naming `…/tools` resolves to no package.json at all
|
|
11
|
+
* — `dsh.client` is never read and the browser half is never composed into `__DSH_BOOT__`. The host
|
|
12
|
+
* would keep working and the composer control would silently never arrive; that is what v0.5.4
|
|
13
|
+
* shipped, and `test/loader-entry.test.mjs` now catches it through the real Loader.
|
|
9
14
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* module", and
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
15
|
+
* `./tools.js` remains a supported mount point for anyone who wants it, and it is still what the
|
|
16
|
+
* DSH plugin listing requirements look at: the checklist asks for a package that "exports an
|
|
17
|
+
* `apply(ctx)` module", and this root is that module. It used to be documentation only, so importing
|
|
18
|
+
* the package root yielded **no exports at all** (`Object.keys() === []`): a plugin that works
|
|
19
|
+
* perfectly when mounted by its patch and looks like an empty package to every tool that inspects it.
|
|
20
|
+
* That is a bad trade for one re-export line.
|
|
16
21
|
*
|
|
17
22
|
* WHY NOT MOVE THE PLUGIN HERE
|
|
18
23
|
*
|
|
19
24
|
* Re-exporting is not relocating: `./tools.js` stays the single definition, so there is still
|
|
20
25
|
* exactly one implementation of the plugin surface to keep in sync. Mounting *both* this entry and
|
|
21
|
-
* the subpath would register the eight tools twice, which is why the patch
|
|
22
|
-
*
|
|
26
|
+
* the subpath would register the eight tools twice, which is why the patch mounts exactly one of
|
|
27
|
+
* them.
|
|
23
28
|
*/
|
|
24
29
|
export { apply, inject, resetDefineToolCacheForTests } from './tools.js';
|
package/lib/tools.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Package-facing plugin entry: `@twinsearth/w2m-dsh-plugin/tools`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The documented subpath entry point, and the seam between the published package
|
|
5
|
+
* layout (`lib/`, what npm ships as the documented entry) and the implementation
|
|
6
|
+
* (`src/`, where the code is actually readable).
|
|
7
|
+
*
|
|
8
|
+
* `cordis.patch.yml` mounts the package *root* (`lib/index.js`), which re-exports
|
|
9
|
+
* this file, because the client module system only recognises a package whose
|
|
10
|
+
* Loader row carries the bare package specifier. Both paths land on the same
|
|
11
|
+
* `apply` and `inject` -- `test/tools.test.mjs` asserts that -- so this remains a
|
|
12
|
+
* supported mount point for a profile that prefers it.
|
|
7
13
|
*
|
|
8
14
|
* Nothing is re-implemented here on purpose: a second definition of the plugin
|
|
9
15
|
* surface would be a second thing to keep in sync.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twinsearth/w2m-dsh-plugin",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.6",
|
|
4
4
|
"description": "DSH: Windows2MacOS — drive the same project on online Windows and macOS machines from one DeepSeek account, across networks and regions. P2P hole punching is the default path (relay + shared server as rendezvous, ledger and fallback), with a bundled STUN responder, Tailscale/public-VPS/tunnel shapes, Rabbit relay + per-machine Localside + eight DSH tools (replicate / split), nine aggregate verdicts and signed requests.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|