dsh-hot-reload 0.1.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.
- package/LICENSE +21 -0
- package/README.md +91 -0
- package/cordis.patch.yml +17 -0
- package/lib/index.js +265 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Stuart Hu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# dsh-hot-reload
|
|
2
|
+
|
|
3
|
+
Live-reload upgraded [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh) plugins **without restarting dsh**.
|
|
4
|
+
|
|
5
|
+
dsh's built-in hot-reload (`cordis-plugin-hmr`) deliberately ignores
|
|
6
|
+
`node_modules`, so upgrading an installed plugin (`dsh plugin add pkg@x`)
|
|
7
|
+
normally requires a full `dsh` restart to take effect. This plugin closes that
|
|
8
|
+
gap: it watches your profile's `pnpm-lock.yaml`, and when an already-loaded
|
|
9
|
+
plugin package's version changes, it swaps the running plugin in place.
|
|
10
|
+
|
|
11
|
+
## Behavior
|
|
12
|
+
|
|
13
|
+
On a plugin package upgrade, for each affected plugin:
|
|
14
|
+
|
|
15
|
+
- **Reload it live** — invalidate the module caches, re-import the new code, and
|
|
16
|
+
re-instantiate the plugin fiber in place. dsh, your sessions, and every other
|
|
17
|
+
plugin keep running.
|
|
18
|
+
- **If the reload fails** — you're left with the **working old version**, never a
|
|
19
|
+
dead plugin, plus a logged note that a **manual `dsh` restart** is needed to
|
|
20
|
+
pick up the new code. Two cases, both handled:
|
|
21
|
+
- a failure while *loading* the new code (bad import, syntax error) is caught
|
|
22
|
+
*before* the running plugin is touched — the old plugin is never disturbed;
|
|
23
|
+
- a failure while *initializing* it (the new `apply` throws, sync **or** async)
|
|
24
|
+
is rolled back — the old version is re-instantiated in place.
|
|
25
|
+
|
|
26
|
+
It **never restarts dsh for you** — restarting is left to you (and your
|
|
27
|
+
supervisor, if any).
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
dsh plugin --profile web add dsh-hot-reload
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Then restart dsh once (bundle patch layers load at boot). After that, upgrades
|
|
36
|
+
apply live:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
dsh plugin --profile web add some-plugin@newer # reloaded automatically
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Opting out
|
|
43
|
+
|
|
44
|
+
A plugin that knows it isn't safe to hot-reload can force the restart-needed
|
|
45
|
+
path (no reload attempt) by declaring, in its own `package.json`:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{ "dsh": { "hotReload": false } }
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Configuration
|
|
52
|
+
|
|
53
|
+
Set on the `hot-reload` row in your profile's `cordis.patch.yml`:
|
|
54
|
+
|
|
55
|
+
| Key | Default | Meaning |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| `debounce` | `300` | ms to wait after a lockfile change before acting |
|
|
58
|
+
| `profileDir` | auto | absolute path to the profile dir to watch (auto-detected from the loader base URL if omitted) |
|
|
59
|
+
|
|
60
|
+
## Limitations — read this
|
|
61
|
+
|
|
62
|
+
This plugin is **optimistic**, not verified. It attempts the reload and only
|
|
63
|
+
falls back to "restart needed" on a **thrown** error. It does **not** detect
|
|
64
|
+
*silent* leaks:
|
|
65
|
+
|
|
66
|
+
- A plugin that acquires a **raw resource outside cordis** — a bare
|
|
67
|
+
`setInterval`, a `net`/`http` server, a `WebSocketServer`, an `fs.watch`,
|
|
68
|
+
a `child_process` — **without a `ctx.effect` disposer** can reload without
|
|
69
|
+
throwing yet leave that resource dangling (a stray timer, a duplicate
|
|
70
|
+
listener, an orphaned watcher). These accumulate across upgrades and are
|
|
71
|
+
cleared only by an eventual restart.
|
|
72
|
+
- Cordis auto-unwinds everything a plugin registers **through `ctx`**
|
|
73
|
+
(`ctx.effect`, `ctx.on`, `ctx.provide`, tool schemas, adapters), so
|
|
74
|
+
well-behaved plugins reload cleanly. The risk is limited to plugins that
|
|
75
|
+
bypass `ctx`. If in doubt, have such a plugin set `dsh.hotReload: false`.
|
|
76
|
+
- Reloading a plugin that holds **live connections** (e.g. a WebSocket bridge)
|
|
77
|
+
drops and re-establishes them; clients must reconnect. That's expected, not an
|
|
78
|
+
error.
|
|
79
|
+
- The reload path relies on cordis/loader internals
|
|
80
|
+
(`loader.internal.loadCache`, `registry.plugin`/`delete`, `fiber.entry`) — the
|
|
81
|
+
same ones `cordis-plugin-hmr` uses. If those internals are unavailable (no
|
|
82
|
+
`--expose-internals` and no `node-addon-require-builtin` addon), the plugin
|
|
83
|
+
degrades to reporting "restart needed" for every change instead of reloading.
|
|
84
|
+
|
|
85
|
+
Scope note: this handles **upgrades of already-loaded plugins**. Installing a
|
|
86
|
+
*brand-new* plugin is a separate concern (adding its row to `cordis.patch.yml`,
|
|
87
|
+
which dsh already hot-applies).
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
[MIT](LICENSE) © Stuart Hu
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# dsh-hot-reload bundle patch — mounts the watcher plugin that live-reloads
|
|
2
|
+
# upgraded plugin packages without restarting dsh.
|
|
3
|
+
#
|
|
4
|
+
# Behavior: on a plugin package change it either hot-reloads the plugin in
|
|
5
|
+
# place (if judged safe) or logs that a manual `dsh` restart is needed (if not).
|
|
6
|
+
# It never restarts dsh itself. See README.md for the full safety model.
|
|
7
|
+
- insert:
|
|
8
|
+
- id: hot-reload
|
|
9
|
+
name: 'dsh-hot-reload'
|
|
10
|
+
# config:
|
|
11
|
+
# # Extra package names you personally vouch are safe to hot-reload,
|
|
12
|
+
# # even if they don't declare `dsh.hotReload: true` themselves.
|
|
13
|
+
# reloadable: []
|
|
14
|
+
# # Debounce (ms) after a lockfile change before acting.
|
|
15
|
+
# debounce: 300
|
|
16
|
+
# # Absolute path to the profile dir to watch; auto-detected if omitted.
|
|
17
|
+
# profileDir: ''
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
// dsh-hot-reload — live-reload upgraded dsh plugins without restarting dsh.
|
|
2
|
+
//
|
|
3
|
+
// It watches the profile's pnpm-lock.yaml; when an already-loaded plugin
|
|
4
|
+
// package's version changes, it invalidates that module's caches, re-imports
|
|
5
|
+
// the new code, and swaps the running plugin fiber in place — the same
|
|
6
|
+
// technique cordis-plugin-hmr uses, but reaching into node_modules (which HMR
|
|
7
|
+
// deliberately ignores).
|
|
8
|
+
//
|
|
9
|
+
// It is OPTIMISTIC: it attempts the reload and, if anything throws, rolls the
|
|
10
|
+
// plugin back to the old version and logs that a manual `dsh` restart is needed
|
|
11
|
+
// to pick up the new code. It does NOT detect silent leaks — a plugin that
|
|
12
|
+
// acquires a raw resource (a bare setInterval, socket, or fs.watch) without a
|
|
13
|
+
// ctx.effect disposer can reload without error yet leave that resource dangling.
|
|
14
|
+
// A plugin can opt out of reload entirely with `dsh.hotReload: false` in its
|
|
15
|
+
// package.json, which forces the restart-needed path without a reload attempt.
|
|
16
|
+
//
|
|
17
|
+
// NOTE: the reload path uses cordis/loader internals (loader.internal.loadCache,
|
|
18
|
+
// registry.plugin/delete, fiber.entry) — the same ones HMR uses. If a future
|
|
19
|
+
// cordis changes them, reloads will fail closed to "restart needed", never
|
|
20
|
+
// crash dsh.
|
|
21
|
+
|
|
22
|
+
import { watch } from "chokidar";
|
|
23
|
+
import { readFileSync } from "node:fs";
|
|
24
|
+
import { createRequire } from "node:module";
|
|
25
|
+
import { fileURLToPath } from "node:url";
|
|
26
|
+
import { dirname, join } from "node:path";
|
|
27
|
+
|
|
28
|
+
export const name = "dsh-hot-reload";
|
|
29
|
+
|
|
30
|
+
const getOuterStack = () => [];
|
|
31
|
+
const cjsRequire = createRequire(import.meta.url);
|
|
32
|
+
|
|
33
|
+
export function apply(ctx, config = {}) {
|
|
34
|
+
const log = ctx.logger ?? console;
|
|
35
|
+
const loader = ctx.loader;
|
|
36
|
+
const internal = loader?.internal;
|
|
37
|
+
const debounceMs = Number(config.debounce ?? 300);
|
|
38
|
+
|
|
39
|
+
if (!loader || typeof loader.entries !== "function") {
|
|
40
|
+
log.warn?.("dsh-hot-reload: no loader on context; plugin inactive");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (!internal) {
|
|
44
|
+
// Without loader.internal we cannot invalidate the module cache, so every
|
|
45
|
+
// change degrades to a restart-needed notice rather than a reload attempt.
|
|
46
|
+
log.warn?.(
|
|
47
|
+
"dsh-hot-reload: loader.internal unavailable — upgrades will be reported as 'restart needed' (no live reload)"
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const profileDir = resolveProfileDir(ctx, config);
|
|
52
|
+
if (!profileDir) {
|
|
53
|
+
log.warn?.("dsh-hot-reload: could not locate profile dir (set config.profileDir); plugin inactive");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const lockfile = join(profileDir, "pnpm-lock.yaml");
|
|
57
|
+
const nodeModules = join(profileDir, "node_modules");
|
|
58
|
+
|
|
59
|
+
// ---- package <-> loader-entry helpers ----
|
|
60
|
+
|
|
61
|
+
/** Package name backing a loader entry's module specifier, or null for local/builtin. */
|
|
62
|
+
function pkgOf(specifier) {
|
|
63
|
+
if (typeof specifier !== "string" || !specifier || specifier.startsWith(".") || specifier.startsWith("cordis:")) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
if (specifier.startsWith("@")) {
|
|
67
|
+
const [scope, pkg] = specifier.split("/");
|
|
68
|
+
return scope && pkg ? `${scope}/${pkg}` : null;
|
|
69
|
+
}
|
|
70
|
+
return specifier.split("/")[0];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function readPkgJson(pkg) {
|
|
74
|
+
try {
|
|
75
|
+
return JSON.parse(readFileSync(join(nodeModules, pkg, "package.json"), "utf8"));
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function versionOf(pkg) {
|
|
82
|
+
return readPkgJson(pkg)?.version ?? null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function optedOut(pkg) {
|
|
86
|
+
return readPkgJson(pkg)?.dsh?.hotReload === false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function entries() {
|
|
90
|
+
try {
|
|
91
|
+
return [...loader.entries()];
|
|
92
|
+
} catch {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function entriesForPkg(pkg) {
|
|
98
|
+
return entries().filter((e) => pkgOf(e?.options?.name) === pkg);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** pkg -> installed version, across every package that backs a loaded entry. */
|
|
102
|
+
function snapshotVersions() {
|
|
103
|
+
const map = Object.create(null);
|
|
104
|
+
for (const e of entries()) {
|
|
105
|
+
const pkg = pkgOf(e?.options?.name);
|
|
106
|
+
if (!pkg || pkg in map) continue;
|
|
107
|
+
const v = versionOf(pkg);
|
|
108
|
+
if (v) map[pkg] = v;
|
|
109
|
+
}
|
|
110
|
+
return map;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ---- reload primitives (mirroring cordis-plugin-hmr) ----
|
|
114
|
+
|
|
115
|
+
async function resolveUrl(specifier, parentURL) {
|
|
116
|
+
const attrs = {};
|
|
117
|
+
let res;
|
|
118
|
+
switch (internal.version) {
|
|
119
|
+
case "v1":
|
|
120
|
+
res = await internal.resolve(specifier, parentURL, attrs);
|
|
121
|
+
break;
|
|
122
|
+
case "v2":
|
|
123
|
+
res = internal.resolveSync(parentURL, { specifier, attributes: attrs });
|
|
124
|
+
break;
|
|
125
|
+
default:
|
|
126
|
+
if (typeof internal.resolve === "function") res = await internal.resolve(specifier, parentURL, attrs);
|
|
127
|
+
else if (typeof internal.resolveSync === "function") res = internal.resolveSync(parentURL, { specifier, attributes: attrs });
|
|
128
|
+
else throw new Error("loader.internal exposes no resolver");
|
|
129
|
+
}
|
|
130
|
+
return typeof res === "string" ? res : res?.url;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function invalidate(url) {
|
|
134
|
+
// ESM: Map.prototype.delete for full removal across Node 22/24 loadCache shapes.
|
|
135
|
+
try {
|
|
136
|
+
Map.prototype.delete.call(internal.loadCache, url);
|
|
137
|
+
} catch {}
|
|
138
|
+
// CJS: modules imported via import() also land in the require cache on Node 24.
|
|
139
|
+
try {
|
|
140
|
+
const fp = fileURLToPath(url);
|
|
141
|
+
if (cjsRequire.cache[fp]) delete cjsRequire.cache[fp];
|
|
142
|
+
} catch {}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Reload one loaded entry's module in place. Throws (after rollback) on failure. */
|
|
146
|
+
async function reloadEntry(entry) {
|
|
147
|
+
const specifier = entry?.options?.name;
|
|
148
|
+
const parentURL = entry?.parent?.tree?.ctx?.baseUrl ?? ctx.baseUrl;
|
|
149
|
+
const oldFiber = entry.fiber;
|
|
150
|
+
const runtime = oldFiber?.runtime;
|
|
151
|
+
const oldPlugin = runtime?.callback;
|
|
152
|
+
if (!oldPlugin || !runtime) throw new Error(`no live fiber for ${specifier}`);
|
|
153
|
+
|
|
154
|
+
const url = await resolveUrl(specifier, parentURL);
|
|
155
|
+
if (!url) throw new Error(`could not resolve ${specifier}`);
|
|
156
|
+
|
|
157
|
+
invalidate(url); // matters for in-place edits; harmless no-op for a version bump (new realpath)
|
|
158
|
+
|
|
159
|
+
const newPlugin = loader.unwrapExports(await loader.import(url, getOuterStack));
|
|
160
|
+
if (!newPlugin) throw new Error(`fresh import produced no plugin for ${specifier}`);
|
|
161
|
+
|
|
162
|
+
// Snapshot fibers before disposal, then swap: dispose old (runs ctx disposers),
|
|
163
|
+
// re-instantiate the new plugin against each old fiber's entry + config.
|
|
164
|
+
const fibers = [...runtime.fibers];
|
|
165
|
+
ctx.registry.delete(oldPlugin);
|
|
166
|
+
try {
|
|
167
|
+
const fresh = fibers.map((of) => reattach(newPlugin, of));
|
|
168
|
+
// fiber.await() settles activation and RETHROWS a startup error — this is
|
|
169
|
+
// how an async apply() throw is surfaced into this try/catch (a plain
|
|
170
|
+
// reattach would let it escape asynchronously and leave the plugin dead).
|
|
171
|
+
await Promise.all(fresh.map((f) => f?.await?.()));
|
|
172
|
+
} catch (err) {
|
|
173
|
+
// Rollback to the old plugin so a failed reload never leaves it dead.
|
|
174
|
+
try {
|
|
175
|
+
ctx.registry.delete(newPlugin);
|
|
176
|
+
} catch {}
|
|
177
|
+
const restored = [];
|
|
178
|
+
for (const of of fibers) {
|
|
179
|
+
try {
|
|
180
|
+
restored.push(reattach(oldPlugin, of));
|
|
181
|
+
} catch {}
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
await Promise.all(restored.map((f) => f?.await?.()));
|
|
185
|
+
} catch {}
|
|
186
|
+
throw err;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function reattach(plugin, oldFiber) {
|
|
191
|
+
const fiber = oldFiber.parent.registry.plugin(plugin, oldFiber._config, getOuterStack);
|
|
192
|
+
fiber.entry = oldFiber.entry;
|
|
193
|
+
if (fiber.entry) fiber.entry.fiber = fiber;
|
|
194
|
+
return fiber;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ---- change handling ----
|
|
198
|
+
|
|
199
|
+
async function handlePackage(pkg) {
|
|
200
|
+
const affected = entriesForPkg(pkg);
|
|
201
|
+
if (!affected.length) return; // not a loaded plugin (e.g. a fresh install) — out of scope
|
|
202
|
+
const version = versionOf(pkg) ?? "?";
|
|
203
|
+
|
|
204
|
+
if (optedOut(pkg)) {
|
|
205
|
+
log.info?.(`dsh-hot-reload: ${pkg}@${version} sets dsh.hotReload:false — restart dsh to load the new version`);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (!internal) {
|
|
209
|
+
log.info?.(`dsh-hot-reload: ${pkg}@${version} changed — restart dsh to load the new version`);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
for (const entry of affected) await reloadEntry(entry);
|
|
215
|
+
log.info?.(`dsh-hot-reload: hot-reloaded ${pkg}@${version} (${affected.length} plugin row(s))`);
|
|
216
|
+
} catch (err) {
|
|
217
|
+
log.warn?.(`dsh-hot-reload: could not hot-reload ${pkg}@${version} — restart dsh to load the new version`);
|
|
218
|
+
log.warn?.(err);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ---- watcher ----
|
|
223
|
+
|
|
224
|
+
let versions = snapshotVersions();
|
|
225
|
+
let timer = null;
|
|
226
|
+
const trigger = () => {
|
|
227
|
+
if (timer) clearTimeout(timer);
|
|
228
|
+
timer = setTimeout(async () => {
|
|
229
|
+
timer = null;
|
|
230
|
+
const next = snapshotVersions();
|
|
231
|
+
const changed = [];
|
|
232
|
+
for (const pkg in next) {
|
|
233
|
+
if (versions[pkg] !== next[pkg]) changed.push(pkg);
|
|
234
|
+
}
|
|
235
|
+
versions = next;
|
|
236
|
+
for (const pkg of changed) await handlePackage(pkg);
|
|
237
|
+
}, debounceMs);
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const watcher = watch(lockfile, { ignoreInitial: true });
|
|
241
|
+
watcher.on("change", trigger);
|
|
242
|
+
watcher.on("add", trigger);
|
|
243
|
+
watcher.on("error", (e) => log.warn?.("dsh-hot-reload: watcher error", e));
|
|
244
|
+
|
|
245
|
+
ctx.effect(() => () => {
|
|
246
|
+
if (timer) clearTimeout(timer);
|
|
247
|
+
watcher.close();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
log.info?.(
|
|
251
|
+
`dsh-hot-reload: watching ${lockfile} (${Object.keys(versions).length} plugin package(s) tracked)`
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Best-effort profile-dir resolution: explicit config, then the loader base URL. */
|
|
256
|
+
function resolveProfileDir(ctx, config) {
|
|
257
|
+
if (config.profileDir) return config.profileDir;
|
|
258
|
+
try {
|
|
259
|
+
if (ctx.baseUrl) {
|
|
260
|
+
const p = fileURLToPath(new URL(".", ctx.baseUrl));
|
|
261
|
+
return p.replace(/\/$/, "");
|
|
262
|
+
}
|
|
263
|
+
} catch {}
|
|
264
|
+
return null;
|
|
265
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-hot-reload",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Live-reload upgraded DeepSeek Harness (dsh) plugins without restarting dsh — safe plugins are hot-reloaded in place; unsafe ones are flagged for a manual restart.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"dsh",
|
|
8
|
+
"dsh-plugin",
|
|
9
|
+
"deepseek-harness",
|
|
10
|
+
"deepseek-harness-plugin",
|
|
11
|
+
"hot-reload",
|
|
12
|
+
"hmr",
|
|
13
|
+
"reload",
|
|
14
|
+
"cordis"
|
|
15
|
+
],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"author": "Stuart Hu",
|
|
18
|
+
"homepage": "https://github.com/stuarthu/dsh-hot-reload#readme",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/stuarthu/dsh-hot-reload.git"
|
|
22
|
+
},
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/stuarthu/dsh-hot-reload/issues"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=22"
|
|
28
|
+
},
|
|
29
|
+
"main": "lib/index.js",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": "./lib/index.js",
|
|
32
|
+
"./package.json": "./package.json"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"lib",
|
|
36
|
+
"cordis.patch.yml",
|
|
37
|
+
"README.md",
|
|
38
|
+
"LICENSE"
|
|
39
|
+
],
|
|
40
|
+
"dsh": {
|
|
41
|
+
"bundle": {
|
|
42
|
+
"patch": "./cordis.patch.yml"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"chokidar": "^4.0.0"
|
|
47
|
+
}
|
|
48
|
+
}
|