mirror of
https://github.com/czcdr/dsh-plugin-rea.git
synced 2026-11-07 10:25:59 -10:00
The setup step is mandatory but invisible: a market install or update replaces the package and stops there, and a skipped setup fails silently in both directions - no tools on a first install, the old REA after an upgrade. The guide names the actual state instead of asking whether it was shown before. - bin/rea-guide.mjs: computeGuideState compares the REA version recorded in plugins/rea-bundle/bundle.json with the manifest pin and reports missing / stale / unknown / unreadable / ok. Same pin rules as scripts/pin-match.ps1 (dotted pin exact, bare major as a family), and it reads UTF-16 records the way the shim and setup.mjs now do. - index.mjs: serves GET /dsh-plugin-rea/guide-state, mounted only through ctx.inject(['webServer']), so headless and ACP hosts load the plugin exactly as before. Read-only: no provisioning, no spawning, no writes. - client.js: shell.overlay card shown once per plugin version, with the exact command for this install; "Later" and "I ran it" record the version in localStorage, so the next upgrade re-arms it and the same version stays quiet. - cordis.patch.yml: one extra row for the package entry only (no tools, no REA). - 5 new tests (78 total); verify-branch section 9 checks all four ends of the chain, so a rename cannot make the guide silently never appear.
110 lines
4.0 KiB
JavaScript
110 lines
4.0 KiB
JavaScript
/**
|
|
* Package entry — a loadable package, plus the read-only route behind the
|
|
* one-shot setup guide.
|
|
*
|
|
* Two jobs, deliberately kept apart:
|
|
*
|
|
* 1. Make this package loadable. The row that does the real work lives in
|
|
* `cordis.patch.yml`: it inserts one row of `@deepseek-ai/dsh-mcp-client`
|
|
* that launches `bin/rea-shim.mjs`. A package that declares no
|
|
* `main`/`exports` is judged by its consumers as "has no entry file",
|
|
* whether or not the loader needs one: dshmarket's `hasLoadableEntry`
|
|
* asks that on every update and rolls a working build back when the answer
|
|
* is false (measured 2026-10-18, 2.0.1 → 2.0.4), and npm metadata resolves
|
|
* `main` too. So this is a real module with a real Cordis shape.
|
|
*
|
|
* 2. Answer GET /dsh-plugin-rea/guide-state for `client.js`. The guide has to
|
|
* tell a first install apart from an upgrade — not by guessing, but by
|
|
* comparing the REA version recorded in `plugins/rea-bundle/bundle.json`
|
|
* with the version this package pins. `bin/rea-guide.mjs` computes it as a
|
|
* pure function; this file only serves the result. Nothing here provisions,
|
|
* spawns or mutates: the route is read-only, and the guide never runs
|
|
* setup for the user.
|
|
*
|
|
* The route is mounted only when the host has a webServer (the Web GUI), through
|
|
* `ctx.inject`, so a headless/ACP host loads this plugin exactly like before.
|
|
*/
|
|
|
|
import { GUIDE_ROUTE, computeGuideState, localPackageFacts } from './bin/rea-guide.mjs'
|
|
|
|
/** Cordis plugin name. Also the package name, which is what the loader expects. */
|
|
export const name = 'dsh-plugin-rea'
|
|
|
|
/** Cached package facts; the files change only when the package is replaced. */
|
|
let cachedFacts = null
|
|
|
|
function packageFacts() {
|
|
if (!cachedFacts) cachedFacts = localPackageFacts()
|
|
return cachedFacts
|
|
}
|
|
|
|
function sendJson(res, status, payload, headOnly = false) {
|
|
const body = JSON.stringify(payload)
|
|
res.writeHead(status, {
|
|
'content-type': 'application/json; charset=utf-8',
|
|
'cache-control': 'no-store',
|
|
'content-length': Buffer.byteLength(body),
|
|
})
|
|
res.end(headOnly ? undefined : body)
|
|
}
|
|
|
|
/**
|
|
* Register the read-only guide route on a host that has a webServer.
|
|
*
|
|
* `webServer.register` takes `{ kind: 'exact', path, handler }` and returns a
|
|
* disposer; `ctx.effect` ties that disposer to this plugin's lifetime, so a
|
|
* hot disable removes the route instead of leaving a handler over unloaded code.
|
|
*/
|
|
function mountGuideRoute(host) {
|
|
const server = host && host.webServer
|
|
if (!server || typeof server.register !== 'function') return
|
|
if (typeof host.effect !== 'function') return
|
|
|
|
host.effect(() =>
|
|
server.register({
|
|
kind: 'exact',
|
|
path: GUIDE_ROUTE,
|
|
handler: (req, res) => {
|
|
try {
|
|
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
sendJson(res, 405, { error: 'GET only' })
|
|
return
|
|
}
|
|
const facts = packageFacts()
|
|
const state = computeGuideState({
|
|
pluginVersion: facts.pluginVersion,
|
|
pinnedRea: facts.pinnedRea,
|
|
})
|
|
sendJson(res, 200, state, req.method === 'HEAD')
|
|
} catch (error) {
|
|
sendJson(res, 500, {
|
|
error: error && error.message ? error.message : String(error),
|
|
})
|
|
}
|
|
},
|
|
}),
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Mount the guide route when the host can serve it.
|
|
*
|
|
* `ctx.inject` is the normal path: it runs the callback once `webServer` exists,
|
|
* and never on a host that has none. The direct fallback keeps older Cordis
|
|
* shapes working. Neither path touches the MCP row — that row is the bundle
|
|
* patch's, and registering a second `serverName: rea` here would be the exact
|
|
* duplicate the shim warns about.
|
|
*/
|
|
export function apply(ctx) {
|
|
if (!ctx) return
|
|
if (typeof ctx.inject === 'function') {
|
|
try {
|
|
ctx.inject(['webServer'], (host) => mountGuideRoute(host))
|
|
return
|
|
} catch {
|
|
/* fall through to the direct check */
|
|
}
|
|
}
|
|
if (ctx.webServer) mountGuideRoute(ctx)
|
|
}
|