@deepseek-ai/dsh-hmr 0.1.6-alpha.2

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021-present Shigma
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.
@@ -0,0 +1,6 @@
1
+ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
2
+ # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
+ # after editing either side, bring the other along and re-record with:
4
+ # pnpm run verify-translation-pairing --write packages/boot/hmr/README.md
5
+ README.md: 735b37c41e50c6a1f663cbfa0cde42f7250ec415
6
+ README.zh.md: b0d3196913cb11e434b397350c81ad1bb45dc0c6
package/README.md ADDED
@@ -0,0 +1,95 @@
1
+ ---
2
+ description: "Reload plugin code and profile configuration through one coordinated queue."
3
+ kind: "package-reference"
4
+ ---
5
+
6
+ # @deepseek-ai/dsh-hmr
7
+
8
+ English | [中文](README.zh.md)
9
+
10
+ ## Summary
11
+
12
+ Reload plugin source and configuration while an application is running. Module replacements, Include refreshes and profile configuration changes share one queue. Package installation runs outside that queue. Existing Cordis HMR configuration and events remain available under `ctx.hmr`.
13
+
14
+ ## Table of Contents
15
+
16
+ - [Use this package](#use-this-package)
17
+ - [Understand the implementation](#understand-the-implementation)
18
+ - [Model Experience](#model-experience)
19
+ - [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
20
+ - [Dev Note](#dev-note)
21
+
22
+ -----
23
+
24
+ <a id="use-this-package"></a>
25
+ ## Use this package
26
+
27
+ The base bundle enables HMR with `root: []` when the launcher supplies `profileContext`; hosts without that profile context leave this entry disabled. Headless, SDK and ACP bundles disable that entry in YAML; a later profile patch can enable it. Disabling or omitting HMR applies changes on restart. To enable source-module watching, configure the `hmr` entry supplied by the base bundle in the profile patch before launching:
28
+
29
+ ```yaml
30
+ - id: hmr
31
+ disabled: false
32
+ config:
33
+ root: ["."]
34
+ ```
35
+
36
+ Existing configurations replace the module name `@deepseek-ai/cordis-plugin-hmr` with `@deepseek-ai/dsh-hmr`. The `hmr` service key, `baseDir`, `config`, `getLinked()`, `getOuterStack()`, `hmr/change` and `hmr/reload` remain available. The vendored package remains available; DSH profiles use this package.
37
+
38
+ ### Configuration
39
+
40
+ | Field | Default | Meaning |
41
+ |---|---|---|
42
+ | `base` | Context base URL | Base directory for module watching. |
43
+ | `root` | `["."]` | Module watch roots; `[]` retains only explicit configuration watches. |
44
+ | `ignored` | `["**/node_modules", "**/.*", "cache", "data"]` | Excluded module paths. |
45
+ | `debounce` | `100` | Milliseconds for combining module changes. |
46
+
47
+ Chokidar options, including polling, retain their existing meaning. Exact configuration watches also observe additions, removals and initially missing parent directories. They default to `awaitWriteFinish: true`: edits wait for Chokidar's 2-second write-stability window, avoiding its lossy change-event throttle. Configure `awaitWriteFinish` to adjust that window; disabling it can miss rapid consecutive edits. Direct Plugin Manager operations apply without waiting for file events.
48
+
49
+ -----
50
+
51
+ <a id="understand-the-implementation"></a>
52
+ ## Understand the implementation
53
+
54
+ <details>
55
+ <summary>Implementation internals — click to expand</summary>
56
+
57
+ `watchConfig()` registers an awaited configuration handler. `runExclusive()` serializes configuration changes and Loader updates with automatic reloads and rejects nested transactions. Package installation and removal run outside this queue. HMR does not acquire the package writer lock; manifest notifications reload only when the ordered `dsh.profile.bundles` list changes. Profile and home patch changes also trigger recomposition. File events received during a configuration transaction are processed afterward.
58
+
59
+ App-boot owns profile parsing and patch precedence. HMR reads the launcher’s data-only `profileContext`, registers the profile manifest and both user patch watches during initialization, and waits for application readiness before processing changes. Its disposal closes the watchers and cancels reloads waiting for startup. HMR also owns module-cache replacement and reload scheduling. Configuration watchers start outside the active transaction context so later notifications can enter the queue. No invariant companion is published because the queue and watcher registrations have no independent persisted projection.
60
+
61
+ Watched module paths use Node ESM resolution's `realpathSync()` spelling, including Windows short directory names, so file events match the module cache.
62
+
63
+ The module replacement implementation derives from `@cordisjs/plugin-hmr` 1.0.15, with Harness Node-loader and lazy-config changes. Its [MIT license](LICENSE) is retained.
64
+
65
+ </details>
66
+
67
+ <a id="model-experience"></a>
68
+ ## Model Experience
69
+
70
+ ### Reloaded plugins
71
+
72
+ #### What the model sees
73
+
74
+ `ctx.hmr` adds no model-facing tools or messages. Loaded plugins determine subsequent tool and prompt contributions.
75
+
76
+ #### Token effect
77
+
78
+ No direct token contribution.
79
+
80
+ #### KV Cache effect
81
+
82
+ Reloading a contributing plugin can change later request prefixes; HMR does not rewrite conversation history.
83
+
84
+ ## Known Limitations and Deferred Work
85
+
86
+ <a id="known-limitations-and-deferred-work"></a>
87
+
88
+ - Module replacement requires Node loader internals. Framework dependency changes call the host-provided `loader.exit()` hook; HMR itself does not restart the process.
89
+ - Replacing installed package versions still requires a restart through Plugin Manager. The browser Client module graph retains its separate browser-side loading mechanism.
90
+
91
+ ### Dev Note
92
+
93
+ <a id="dev-note"></a>
94
+
95
+ None.
package/README.zh.md ADDED
@@ -0,0 +1,95 @@
1
+ ---
2
+ description: "通过统一协调队列热重载插件代码和 profile 配置。"
3
+ kind: "package-reference"
4
+ ---
5
+
6
+ # @deepseek-ai/dsh-hmr
7
+
8
+ [English](README.md) | 中文
9
+
10
+ ## 概述
11
+
12
+ 在应用运行期间重载插件源码和配置。模块替换、Include 刷新与 profile 配置变更共用一个队列。包安装在该队列之外执行。`ctx.hmr` 保留现有 Cordis HMR 的配置和事件。
13
+
14
+ ## 目录
15
+
16
+ - [使用本包](#use-this-package)
17
+ - [理解实现](#understand-the-implementation)
18
+ - [模型体验](#model-experience)
19
+ - [已知限制与延期工作](#known-limitations-and-deferred-work)
20
+ - [开发备注](#dev-note)
21
+
22
+ -----
23
+
24
+ <a id="use-this-package"></a>
25
+ ## 使用本包
26
+
27
+ 启动器提供 `profileContext` 时,base 组合包以 `root: []` 启用 HMR;没有该 profile 上下文的宿主保留此条目为禁用状态。Headless、SDK 和 ACP 组合包在 YAML 中禁用此条目,后续 profile patch 可以重新启用。禁用或省略 HMR 时,更改在重启后生效。如需监听源码模块,在启动前通过 profile patch 配置 base 组合包提供的 `hmr` 条目:
28
+
29
+ ```yaml
30
+ - id: hmr
31
+ disabled: false
32
+ config:
33
+ root: ["."]
34
+ ```
35
+
36
+ 已有配置将模块名 `@deepseek-ai/cordis-plugin-hmr` 替换为 `@deepseek-ai/dsh-hmr`。继续提供 `hmr` 服务键、`baseDir`、`config`、`getLinked()`、`getOuterStack()`、`hmr/change` 和 `hmr/reload`。工作区保留 vendored 包;DSH profile 使用本包。
37
+
38
+ ### 配置
39
+
40
+ | 字段 | 默认值 | 含义 |
41
+ |---|---|---|
42
+ | `base` | Context 的 base URL | 模块监听的基准目录。 |
43
+ | `root` | `["."]` | 模块监听目录;`[]` 仅保留显式注册的配置监听。 |
44
+ | `ignored` | `["**/node_modules", "**/.*", "cache", "data"]` | 排除的模块路径。 |
45
+ | `debounce` | `100` | 合并模块变化的毫秒数。 |
46
+
47
+ Chokidar 选项(包括轮询)保持原有含义。精确配置监听同时观察新增、删除及初始不存在的父目录。它们默认使用 `awaitWriteFinish: true`:编辑后等待 Chokidar 的 2 秒写入稳定窗口,避免其变化事件节流丢失通知。可通过 `awaitWriteFinish` 调整窗口;禁用它可能漏掉快速连续编辑。直接通过 Plugin Manager 发起的操作无需等待文件事件即可应用。
48
+
49
+ -----
50
+
51
+ <a id="understand-the-implementation"></a>
52
+ ## 理解实现
53
+
54
+ <details>
55
+ <summary>实现细节——点击展开</summary>
56
+
57
+ `watchConfig()` 注册会被等待的配置处理器。`runExclusive()` 将配置变更、Loader 更新与自动重载串行化,并拒绝嵌套事务。包安装和删除在该队列之外执行。HMR 不获取包操作写锁;manifest 通知仅在有序的 `dsh.profile.bundles` 列表变化时触发重载。profile 与 home patch 变化也会触发重新组合。配置事务期间收到的文件事件在事务结束后处理。
58
+
59
+ App-boot 负责 profile 解析和 patch 优先级规则。HMR 读取启动器提供的纯数据 `profileContext`,在初始化时注册 profile manifest 和两份用户 patch 的监听,并等待应用就绪后处理更改。销毁 HMR 时会关闭监听器并取消等待启动的重载。HMR 也负责模块缓存替换和重载调度。配置监听器在当前事务上下文之外启动,使后续通知可以进入队列。不发布 invariant 伴生入口,因为队列和监听注册没有独立的持久投影。
60
+
61
+ 被监听模块的路径沿用 Node ESM 解析所用的 `realpathSync()` 表示,包括 Windows 短目录名,使文件事件与模块缓存匹配。
62
+
63
+ 模块替换实现源自 `@cordisjs/plugin-hmr` 1.0.15,包含 Harness 的 Node loader 和惰性配置修改。保留其 [MIT 许可证](LICENSE)。
64
+
65
+ </details>
66
+
67
+ <a id="model-experience"></a>
68
+ ## 模型体验
69
+
70
+ ### 被重载的插件
71
+
72
+ #### 模型看到什么
73
+
74
+ `ctx.hmr` 不添加模型工具或消息。加载后的插件决定后续工具和提示词贡献。
75
+
76
+ #### Token 影响
77
+
78
+ 没有直接的 token 贡献。
79
+
80
+ #### KV Cache 影响
81
+
82
+ 重载提供上下文的插件可能改变后续请求前缀;HMR 不改写对话历史。
83
+
84
+ ## 已知限制与延期工作
85
+
86
+ <a id="known-limitations-and-deferred-work"></a>
87
+
88
+ - 模块替换需要 Node loader 内部接口。框架依赖变化调用宿主提供的 `loader.exit()` 钩子;HMR 本身不重启进程。
89
+ - 通过插件管理器替换已安装包版本仍需要重启。浏览器 Client 模块图保留独立的浏览器侧加载机制。
90
+
91
+ ### 开发备注
92
+
93
+ <a id="dev-note"></a>
94
+
95
+ 无。
package/lib/index.js ADDED
@@ -0,0 +1,672 @@
1
+ import { createRequire } from "node:module";
2
+ import { AsyncLocalStorage } from "node:async_hooks";
3
+ import { basename, dirname, join, relative, resolve } from "node:path";
4
+ import { realpath, stat } from "node:fs/promises";
5
+ import { watch } from "chokidar";
6
+ import { Inject, Service } from "@deepseek-ai/cordis";
7
+ import { readFileSync, realpathSync } from "node:fs";
8
+ import { PROFILE_PATCH_FILENAME, readProfileManifest, readProfilePatches, reconcileProfilePatches } from "@deepseek-ai/dsh-app-boot";
9
+ import { codeFrameColumns } from "@babel/code-frame";
10
+ import { fileURLToPath, pathToFileURL } from "node:url";
11
+ import picomatch from "picomatch";
12
+ import z from "@deepseek-ai/schemastery";
13
+ //#region lib/types/watch-config.js
14
+ /** Exact-path watching for live profile patch files outside Cordis module roots. */
15
+ const registrations = /* @__PURE__ */ new WeakMap();
16
+ async function findWatchRoot(filename) {
17
+ let root = dirname(filename);
18
+ let depth = 0;
19
+ while (true) try {
20
+ if (!(await stat(root)).isDirectory()) throw new Error(`config watch parent is not a directory: ${root}`);
21
+ const canonicalRoot = await realpath(root);
22
+ return {
23
+ filename: resolve(canonicalRoot, relative(root, filename)),
24
+ root: canonicalRoot,
25
+ depth
26
+ };
27
+ } catch (error) {
28
+ if (error.code !== "ENOENT") throw error;
29
+ const parent = dirname(root);
30
+ if (parent === root) throw error;
31
+ root = parent;
32
+ depth += 1;
33
+ }
34
+ }
35
+ /**
36
+ * Watch one patch path, including missing parents, and serialize refresh callbacks.
37
+ * @param ctx Context that owns watcher disposal and receives refresh failures.
38
+ * @param filename Absolute patch-file path.
39
+ * @param options Deployment watcher options; configuration watches enable write stabilization by default.
40
+ * @param refresh Callback for additions, changes, and removals.
41
+ * @param inTransaction Whether disposal is running inside the refresh being removed.
42
+ * @returns A disposer that closes the watcher and drains its current refresh.
43
+ * @throws When path resolution, watcher startup, or effect registration fails.
44
+ */
45
+ async function watchConfig(ctx, filename, options, refresh, inTransaction = () => false) {
46
+ const target = await findWatchRoot(filename);
47
+ const paths = registrations.get(ctx) ?? /* @__PURE__ */ new Set();
48
+ registrations.set(ctx, paths);
49
+ if (paths.has(target.filename)) throw new Error(`config path already registered: ${filename}`);
50
+ const { cwd: _cwd, ignored: _ignored, ...watchOptions } = options;
51
+ const watcher = watch(target.root, {
52
+ awaitWriteFinish: true,
53
+ ...watchOptions,
54
+ depth: target.depth,
55
+ ignoreInitial: false
56
+ });
57
+ paths.add(target.filename);
58
+ const state = { dirty: false };
59
+ let running;
60
+ const onChange = (path) => {
61
+ const observed = resolve(path);
62
+ if (observed !== filename && observed !== target.filename) return;
63
+ state.dirty = true;
64
+ if (running) return;
65
+ running = (async () => {
66
+ while (state.dirty) {
67
+ state.dirty = false;
68
+ try {
69
+ await refresh();
70
+ } catch (reason) {
71
+ const error = reason instanceof Error ? reason : new Error(String(reason), { cause: reason });
72
+ ctx.logger.warn("config reload at %C failed", filename);
73
+ ctx.logger.warn(error);
74
+ }
75
+ }
76
+ })().finally(() => {
77
+ running = void 0;
78
+ });
79
+ };
80
+ watcher.on("add", onChange);
81
+ watcher.on("change", onChange);
82
+ watcher.on("unlink", onChange);
83
+ const ready = Promise.withResolvers();
84
+ let pending = true;
85
+ watcher.once("ready", () => {
86
+ pending = false;
87
+ ready.resolve();
88
+ });
89
+ watcher.on("error", (error) => {
90
+ if (pending) {
91
+ pending = false;
92
+ ready.reject(error);
93
+ } else ctx.logger.warn(error);
94
+ });
95
+ const dispose = async () => {
96
+ await watcher.close();
97
+ paths.delete(target.filename);
98
+ if (!inTransaction()) await running;
99
+ };
100
+ try {
101
+ await ready.promise;
102
+ return ctx.effect(() => dispose, "hmr.watchConfig()");
103
+ } catch (error) {
104
+ await dispose();
105
+ throw error;
106
+ }
107
+ }
108
+ //#endregion
109
+ //#region lib/types/error.js
110
+ function isBuildFailure(e) {
111
+ return e !== null && typeof e === "object" && "errors" in e && Array.isArray(e.errors) && e.errors.every((error) => error !== null && typeof error === "object" && "text" in error && typeof error.text === "string");
112
+ }
113
+ /** Log HMR build failures with code frames when source locations are available.
114
+ * @param ctx Context owning reload diagnostics.
115
+ * @param e Failure from the module loader or compiler.
116
+ */
117
+ function handleError(ctx, e) {
118
+ if (!isBuildFailure(e)) {
119
+ ctx.logger.warn(e);
120
+ return;
121
+ }
122
+ for (const error of e.errors) {
123
+ if (!error.location) {
124
+ ctx.logger.warn(error.text);
125
+ continue;
126
+ }
127
+ try {
128
+ const { file, line, column } = error.location;
129
+ const formatted = codeFrameColumns(readFileSync(file, "utf8"), { start: {
130
+ line,
131
+ column
132
+ } }, {
133
+ highlightCode: true,
134
+ message: error.text
135
+ });
136
+ ctx.logger.warn(`File: ${file}:${line}:${column}\n` + formatted);
137
+ } catch (e) {
138
+ ctx.logger.warn(e);
139
+ }
140
+ }
141
+ }
142
+ //#endregion
143
+ //#region lib/types/index.js
144
+ /** Serialized module and profile-configuration reloads. */
145
+ var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
146
+ function accept(f) {
147
+ if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected");
148
+ return f;
149
+ }
150
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
151
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
152
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
153
+ var _, done = false;
154
+ for (var i = decorators.length - 1; i >= 0; i--) {
155
+ var context = {};
156
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
157
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
158
+ context.addInitializer = function(f) {
159
+ if (done) throw new TypeError("Cannot add initializers after decoration has completed");
160
+ extraInitializers.push(accept(f || null));
161
+ };
162
+ var result = (0, decorators[i])(kind === "accessor" ? {
163
+ get: descriptor.get,
164
+ set: descriptor.set
165
+ } : descriptor[key], context);
166
+ if (kind === "accessor") {
167
+ if (result === void 0) continue;
168
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
169
+ if (_ = accept(result.get)) descriptor.get = _;
170
+ if (_ = accept(result.set)) descriptor.set = _;
171
+ if (_ = accept(result.init)) initializers.unshift(_);
172
+ } else if (_ = accept(result)) if (kind === "field") initializers.unshift(_);
173
+ else descriptor[key] = _;
174
+ }
175
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
176
+ done = true;
177
+ };
178
+ var __runInitializers = function(thisArg, initializers, value) {
179
+ var useValue = arguments.length > 2;
180
+ for (var i = 0; i < initializers.length; i++) value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
181
+ return useValue ? value : void 0;
182
+ };
183
+ function canonicalPath(filename) {
184
+ try {
185
+ return realpathSync(filename);
186
+ } catch (error) {
187
+ if (error.code !== "ENOENT") throw error;
188
+ const parent = dirname(filename);
189
+ if (parent === filename) throw error;
190
+ return resolve(canonicalPath(parent), basename(filename));
191
+ }
192
+ }
193
+ /**
194
+ * Recursively collect all module dependencies from a ModuleJob.
195
+ * Skips node: builtins and node_modules to focus on user code.
196
+ */
197
+ async function loadDependencies(job, ignored = /* @__PURE__ */ new Set()) {
198
+ const dependencies = /* @__PURE__ */ new Set();
199
+ async function traverse(job) {
200
+ if (ignored.has(job.url) || dependencies.has(job.url)) return;
201
+ if (job.url.startsWith("node:") || job.url.includes("/node_modules/")) return;
202
+ dependencies.add(job.url);
203
+ const children = await job.linked;
204
+ await Promise.all(Array.prototype.map.call(children, traverse));
205
+ }
206
+ await traverse(job);
207
+ return dependencies;
208
+ }
209
+ /** Hot reload service with Cordis-compatible module configuration and events. */
210
+ let Hmr = (() => {
211
+ let _classDecorators = [Inject("loader"), Inject("timer")];
212
+ let _classDescriptor;
213
+ let _classExtraInitializers = [];
214
+ let _classThis;
215
+ let _classSuper = Service;
216
+ var Hmr = class extends _classSuper {
217
+ static {
218
+ _classThis = this;
219
+ }
220
+ static {
221
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
222
+ __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, {
223
+ kind: "class",
224
+ name: _classThis.name,
225
+ metadata: _metadata
226
+ }, null, _classExtraInitializers);
227
+ Hmr = _classThis = _classDescriptor.value;
228
+ if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, {
229
+ enumerable: true,
230
+ configurable: true,
231
+ writable: true,
232
+ value: _metadata
233
+ });
234
+ }
235
+ config;
236
+ /** Cordis-compatible watcher defaults. */
237
+ static Config = z.object({
238
+ base: z.string(),
239
+ root: z.array(String).role("table").default(["."]),
240
+ ignored: z.array(String).role("table").default([
241
+ "**/node_modules",
242
+ "**/.*",
243
+ "cache",
244
+ "data"
245
+ ]),
246
+ debounce: z.natural().role("ms").default(100)
247
+ });
248
+ /** Absolute base directory used to resolve module watch roots. */
249
+ baseDir;
250
+ ownerContext;
251
+ internal;
252
+ watcher;
253
+ /**
254
+ * Changes from externals will always trigger a full reload.
255
+ * Externals are the dependency tree of the CLI worker entry point.
256
+ */
257
+ externals;
258
+ /**
259
+ * Files that should be reloaded (accepted changes).
260
+ * Includes all stashed files and their dependents.
261
+ */
262
+ accepted;
263
+ /**
264
+ * Files that should NOT be reloaded.
265
+ * Includes externals and files whose dependents are all declined.
266
+ */
267
+ declined;
268
+ /** Stashed file changes waiting to be processed */
269
+ stashed = /* @__PURE__ */ new Set();
270
+ operations = Promise.resolve();
271
+ executing = new AsyncLocalStorage();
272
+ applicationReady = Promise.resolve(true);
273
+ closing = false;
274
+ configPaths = /* @__PURE__ */ new Set();
275
+ /** Serialize a caller-owned mutation with all automatic reload paths.
276
+ * @param operation Work that must not overlap module or configuration replacement.
277
+ * @returns The operation result after its asynchronous work completes.
278
+ */
279
+ runExclusive(operation) {
280
+ if (this.executing.getStore()) return Promise.reject(/* @__PURE__ */ new Error("HMR transactions cannot be nested"));
281
+ const task = this.operations.then(async () => {
282
+ if (this.closing) throw new Error("HMR is disposed");
283
+ return this.executing.run(true, operation);
284
+ });
285
+ this.operations = task.catch(() => {});
286
+ return task;
287
+ }
288
+ runReload(operation) {
289
+ return this.runExclusive(async () => {
290
+ if (await this.applicationReady) await operation();
291
+ });
292
+ }
293
+ /** Watch a configuration path through the same queue as module replacement.
294
+ * @param filename Absolute path, which may not exist yet.
295
+ * @param refresh Rebuilds configuration from its current files and awaits Loader completion.
296
+ * @returns Disposer closing this registration and waiting for its pending refresh.
297
+ */
298
+ async watchConfig(filename, refresh) {
299
+ const paths = [resolve(filename), canonicalPath(filename)];
300
+ if (paths.some((path) => this.configPaths.has(path))) throw new Error(`config path already registered: ${filename}`);
301
+ for (const path of paths) this.configPaths.add(path);
302
+ try {
303
+ const dispose = await this.executing.exit(() => watchConfig(this.ownerContext, filename, this.config, () => this.runReload(refresh), () => this.executing.getStore() === true));
304
+ return async () => {
305
+ await dispose();
306
+ for (const path of paths) this.configPaths.delete(path);
307
+ };
308
+ } catch (error) {
309
+ for (const path of paths) this.configPaths.delete(path);
310
+ throw error;
311
+ }
312
+ }
313
+ constructor(ctx, config) {
314
+ super(ctx, "hmr");
315
+ this.config = config;
316
+ this.ownerContext = ctx;
317
+ if (!this.ctx.loader.internal) throw new Error("--expose-internals is required for HMR service");
318
+ this.internal = this.ctx.loader.internal;
319
+ this.baseDir = fileURLToPath(new URL(config.base || ".", ctx.baseUrl));
320
+ }
321
+ /**
322
+ * Resolve a module specifier to a URL, compatible with Node 22-24.
323
+ */
324
+ async _resolve(specifier, parentURL, attrs) {
325
+ switch (this.internal.version) {
326
+ case "v1": return await this.internal.resolve(specifier, parentURL, attrs);
327
+ case "v2": return this.internal.resolveSync(parentURL, {
328
+ specifier,
329
+ attributes: attrs
330
+ });
331
+ }
332
+ }
333
+ async *[Service.init]() {
334
+ yield async () => {
335
+ this.closing = true;
336
+ await this.watcher?.close();
337
+ if (!this.executing.getStore()) await this.operations;
338
+ };
339
+ const profile = this.ownerContext.get("profileContext");
340
+ if (profile !== void 0) {
341
+ const ready = this.ownerContext.get("appReady");
342
+ if (ready === void 0) throw new Error("Profile HMR requires application readiness");
343
+ const started = Promise.withResolvers();
344
+ this.applicationReady = started.promise;
345
+ const unsubscribe = ready.onReady(() => {
346
+ started.resolve(true);
347
+ });
348
+ yield () => {
349
+ unsubscribe();
350
+ started.resolve(false);
351
+ return Promise.resolve();
352
+ };
353
+ const manifestPath = join(profile.dir, "package.json");
354
+ const patchFiles = [profile.patchPath, join(profile.home, PROFILE_PATCH_FILENAME)];
355
+ let lastInputs;
356
+ let lastBundles = JSON.stringify(profile.startedBundles);
357
+ const refresh = async (manifestOnly) => {
358
+ const bundles = JSON.stringify(readProfileManifest("dsh", profile.dir).dsh?.profile?.bundles ?? []);
359
+ if (manifestOnly && bundles === lastBundles) return;
360
+ const inputs = JSON.stringify([bundles, ...patchFiles.map((filename) => {
361
+ try {
362
+ return readFileSync(filename, "utf8");
363
+ } catch (error) {
364
+ if (error.code === "ENOENT") return null;
365
+ throw error;
366
+ }
367
+ })]);
368
+ if (inputs === lastInputs) return;
369
+ const patches = readProfilePatches("dsh", profile);
370
+ const warnings = await reconcileProfilePatches(this.ownerContext.root, patches, "dsh");
371
+ lastInputs = inputs;
372
+ lastBundles = bundles;
373
+ for (const diagnostic of warnings) this.ctx.logger.warn(diagnostic);
374
+ };
375
+ for (const filename of patchFiles) await this.watchConfig(filename, () => refresh(false));
376
+ await this.watchConfig(manifestPath, () => refresh(true));
377
+ }
378
+ const { loader } = this.ctx;
379
+ const { root, ignored } = this.config;
380
+ if (!this.config.base) this.ctx.logger.info("watching %o", root);
381
+ else this.ctx.logger.info("watching %o in %s", root, this.baseDir);
382
+ const match = picomatch(ignored);
383
+ const watchBaseDir = realpathSync(this.baseDir);
384
+ const mainJob = process.argv[1] === void 0 ? void 0 : this.internal.loadCache.get(pathToFileURL(resolve(process.argv[1])).href);
385
+ if (mainJob) this.externals = await loadDependencies(mainJob);
386
+ else this.externals = /* @__PURE__ */ new Set();
387
+ this.watcher = watch(root, {
388
+ ...this.config,
389
+ cwd: watchBaseDir,
390
+ ignored: (path) => match(relative(watchBaseDir, path)),
391
+ ignoreInitial: true
392
+ });
393
+ const changed = /* @__PURE__ */ new Set();
394
+ const dispatch = this.ctx.debounce(() => {
395
+ this.runExclusive(async () => {
396
+ if (!await this.applicationReady) return;
397
+ const batch = [...changed];
398
+ changed.clear();
399
+ const includes = /* @__PURE__ */ new Set();
400
+ let fullReload = false;
401
+ for (const path of batch) {
402
+ const filename = canonicalPath(resolve(watchBaseDir, path));
403
+ const configuredFilename = resolve(this.baseDir, path);
404
+ if (this.configPaths.has(filename) || this.configPaths.has(configuredFilename)) continue;
405
+ const url = pathToFileURL(filename).href;
406
+ if (this.externals.has(url)) {
407
+ fullReload = true;
408
+ continue;
409
+ }
410
+ if (this.internal.loadCache.has(url)) {
411
+ this.stashed.add(url);
412
+ continue;
413
+ }
414
+ const include = [...loader.entries()].map((entry) => entry.subtree).find((tree) => tree?.filename === filename || tree?.filename === configuredFilename);
415
+ if (include !== void 0) includes.add(include);
416
+ else this.ctx.emit("hmr/change", url);
417
+ }
418
+ if (!fullReload && includes.size === 0 && this.stashed.size === 0) return;
419
+ if (fullReload) {
420
+ loader.exit();
421
+ return;
422
+ }
423
+ for (const include of includes) await include.refresh();
424
+ if (this.stashed.size > 0) try {
425
+ await this.partialReload();
426
+ } finally {
427
+ this.stashed.clear();
428
+ }
429
+ await loader.await();
430
+ }).catch((error) => {
431
+ this.ctx.logger.warn(error);
432
+ });
433
+ }, this.config.debounce);
434
+ this.watcher.on("change", (path) => {
435
+ changed.add(path);
436
+ dispatch();
437
+ });
438
+ const ready = Promise.withResolvers();
439
+ let readyState = root.length === 0 ? "resolved" : "pending";
440
+ if (root.length === 0) ready.resolve();
441
+ else this.watcher.once("ready", () => {
442
+ readyState = "resolved";
443
+ ready.resolve();
444
+ });
445
+ this.watcher.on("error", (error) => {
446
+ if (readyState === "pending") {
447
+ readyState = "rejected";
448
+ ready.reject(error);
449
+ } else this.ctx.logger.warn(error);
450
+ });
451
+ await ready.promise;
452
+ }
453
+ /** Omit internal HMR frames from module import diagnostics.
454
+ * @returns The preserved outer stack frames.
455
+ */
456
+ getOuterStack = () => [];
457
+ /** Read direct module dependency URLs from the active Node loader.
458
+ * @param url Module URL.
459
+ * @returns Linked module URLs, or an empty list for an uncached module.
460
+ */
461
+ async getLinked(url) {
462
+ const job = this.internal.loadCache.get(url);
463
+ if (!job) return [];
464
+ const linked = await job.linked;
465
+ return Array.prototype.map.call(linked, (job) => job.url);
466
+ }
467
+ /**
468
+ * Classify changed files into accepted (should reload) and declined (should not).
469
+ *
470
+ * A file is accepted if it's directly changed (stashed) or if any of its
471
+ * dependents are accepted. A file is declined if all its dependents are
472
+ * declined or if it's an external.
473
+ */
474
+ async analyzeChanges() {
475
+ const pending = [];
476
+ this.accepted = new Set(this.stashed);
477
+ this.declined = new Set(this.externals);
478
+ const isExcluded = (url) => url.startsWith("node:") || url.includes("/node_modules/");
479
+ await Promise.all([...this.stashed].map(async (url) => {
480
+ const children = await this.getLinked(url);
481
+ for (const child of children) {
482
+ if (this.accepted.has(child) || this.declined.has(child) || isExcluded(child)) continue;
483
+ pending.push(child);
484
+ }
485
+ }));
486
+ while (pending.length) {
487
+ let index = 0, hasUpdate = false;
488
+ while (index < pending.length) {
489
+ const url = pending[index];
490
+ const children = await this.getLinked(url);
491
+ let isDeclined = true, isAccepted = false;
492
+ for (const child of children) {
493
+ if (this.declined.has(child) || isExcluded(child)) continue;
494
+ if (this.accepted.has(child)) {
495
+ isAccepted = true;
496
+ break;
497
+ } else {
498
+ isDeclined = false;
499
+ if (!pending.includes(child)) {
500
+ hasUpdate = true;
501
+ pending.push(child);
502
+ }
503
+ }
504
+ }
505
+ if (isAccepted || isDeclined) {
506
+ hasUpdate = true;
507
+ pending.splice(index, 1);
508
+ if (isAccepted) this.accepted.add(url);
509
+ else this.declined.add(url);
510
+ } else index++;
511
+ }
512
+ if (!hasUpdate) break;
513
+ }
514
+ for (const url of pending) this.declined.add(url);
515
+ }
516
+ async partialReload() {
517
+ await this.analyzeChanges();
518
+ const pending = /* @__PURE__ */ new Map();
519
+ const reloads = /* @__PURE__ */ new Map();
520
+ const nameMap = /* @__PURE__ */ new Map();
521
+ for (const entry of this.ctx.loader.entries()) {
522
+ const baseUrl = entry.parent.tree.ctx.baseUrl;
523
+ if (baseUrl === void 0) throw new Error("HMR entry tree has no base URL");
524
+ const names = nameMap.get(baseUrl) ?? /* @__PURE__ */ new Set();
525
+ names.add(entry.options.name);
526
+ nameMap.set(baseUrl, names);
527
+ }
528
+ for (const [baseUrl, names] of nameMap) for (const name of names) try {
529
+ const { url } = await this._resolve(name, baseUrl, {});
530
+ if (this.declined.has(url)) continue;
531
+ const job = this.internal.loadCache.get(url);
532
+ const plugin = this.ctx.loader.unwrapExports(job?.module?.getNamespace());
533
+ if (!job || !plugin) continue;
534
+ pending.set(job, plugin);
535
+ this.declined.add(url);
536
+ } catch (err) {
537
+ this.ctx.logger.warn(err);
538
+ }
539
+ for (const [job, plugin] of pending) {
540
+ this.declined.delete(job.url);
541
+ const dependencies = [...await loadDependencies(job, this.declined)];
542
+ this.declined.add(job.url);
543
+ if (!dependencies.some((dep) => this.accepted.has(dep))) continue;
544
+ dependencies.forEach((dep) => this.accepted.add(dep));
545
+ reloads.set(plugin, {
546
+ filename: job.url,
547
+ runtime: this.ctx.registry.get(plugin)
548
+ });
549
+ }
550
+ /**
551
+ * Clear module caches for all accepted files before re-importing.
552
+ *
553
+ * We need to clear both:
554
+ * 1. ESM loadCache — managed by Node's internal ModuleLoader
555
+ * 2. CJS Module._cache — for CJS modules that were imported via import()
556
+ *
557
+ * In Node 24, CJS modules loaded via import() appear in both caches.
558
+ * If we only clear loadCache, the CJS cache may serve stale modules.
559
+ *
560
+ * We use Map.prototype methods directly on loadCache because:
561
+ * - In Node 22/23, loadCache is a plain Map<url, ModuleJob>
562
+ * - In Node 24, loadCache is a LoadCache extends Map<url, { [type]: ModuleJob }>
563
+ * where .delete() only sets the type slot to undefined (doesn't remove the entry)
564
+ * Using Map.prototype.delete ensures complete removal in both versions.
565
+ */
566
+ const esmBackup = /* @__PURE__ */ new Map();
567
+ const cjsBackup = /* @__PURE__ */ new Map();
568
+ const require = createRequire(import.meta.url);
569
+ for (const filename of this.accepted) {
570
+ const job = Map.prototype.get.call(this.internal.loadCache, filename);
571
+ esmBackup.set(filename, job);
572
+ Map.prototype.delete.call(this.internal.loadCache, filename);
573
+ try {
574
+ const filepath = fileURLToPath(filename);
575
+ if (require.cache[filepath]) {
576
+ cjsBackup.set(filepath, require.cache[filepath]);
577
+ Reflect.deleteProperty(require.cache, filepath);
578
+ }
579
+ } catch {}
580
+ }
581
+ const rollback = () => {
582
+ for (const [filename, job] of esmBackup) Map.prototype.set.call(this.internal.loadCache, filename, job);
583
+ for (const [filepath, module] of cjsBackup) require.cache[filepath] = module;
584
+ };
585
+ const generations = [...reloads].map(([previous, info]) => ({
586
+ ...info,
587
+ previous,
588
+ fibers: [...info.runtime?.fibers ?? []].map((fiber) => {
589
+ const entry = fiber.entry?.fiber?.uid === fiber.uid ? fiber.entry : void 0;
590
+ return {
591
+ fiber,
592
+ entry,
593
+ config: entry === void 0 ? fiber._config : entry.options.config
594
+ };
595
+ })
596
+ }));
597
+ const attempts = [];
598
+ try {
599
+ for (const generation of generations) {
600
+ const replacement = this.ctx.loader.unwrapExports(await this.ctx.loader.import(generation.filename, this.getOuterStack));
601
+ attempts.push({
602
+ ...generation,
603
+ replacement
604
+ });
605
+ }
606
+ } catch (e) {
607
+ handleError(this.ctx, e);
608
+ rollback();
609
+ throw e;
610
+ }
611
+ const reload = async (plugin, fibers) => {
612
+ const activated = [];
613
+ for (const previousFiber of fibers) {
614
+ if (previousFiber.fiber.parent.fiber.uid === null) continue;
615
+ const fiber = previousFiber.fiber.parent.registry.plugin(plugin, previousFiber.config, this.getOuterStack).ctx.fiber;
616
+ if (previousFiber.entry !== void 0) {
617
+ fiber.entry = previousFiber.entry;
618
+ previousFiber.entry.fiber = fiber;
619
+ }
620
+ activated.push(fiber);
621
+ }
622
+ await Promise.all(activated.map((fiber) => fiber.await()));
623
+ };
624
+ const removed = /* @__PURE__ */ new Set();
625
+ try {
626
+ for (const { previous: plugin, replacement, filename, runtime, fibers } of attempts) {
627
+ if (!runtime) continue;
628
+ const path = relative(this.baseDir, fileURLToPath(filename));
629
+ removed.add(plugin);
630
+ try {
631
+ this.ctx.registry.delete(plugin);
632
+ await Promise.all(fibers.map(({ fiber }) => fiber.await()));
633
+ } catch (err) {
634
+ this.ctx.logger.warn("failed to dispose plugin at %C", path);
635
+ this.ctx.logger.warn(err);
636
+ }
637
+ try {
638
+ await reload(replacement, fibers);
639
+ this.ctx.logger.info("reload plugin at %C", path);
640
+ } catch (err) {
641
+ this.ctx.logger.warn("failed to reload plugin at %C", path);
642
+ this.ctx.logger.warn(err);
643
+ throw err;
644
+ }
645
+ }
646
+ } catch (error) {
647
+ rollback();
648
+ for (const { previous: plugin, replacement, fibers } of attempts) {
649
+ if (!removed.has(plugin)) continue;
650
+ try {
651
+ const replacementFibers = [...this.ctx.registry.get(replacement)?.fibers ?? []];
652
+ this.ctx.registry.delete(replacement);
653
+ await Promise.allSettled(replacementFibers.map((fiber) => fiber.await()));
654
+ await reload(plugin, fibers);
655
+ } catch (err) {
656
+ this.ctx.logger.warn(err);
657
+ }
658
+ }
659
+ throw error;
660
+ }
661
+ await this.ctx.loader.await();
662
+ this.ctx.emit("hmr/reload", reloads);
663
+ this.stashed = /* @__PURE__ */ new Set();
664
+ }
665
+ static {
666
+ __runInitializers(_classThis, _classExtraInitializers);
667
+ }
668
+ };
669
+ return _classThis;
670
+ })();
671
+ //#endregion
672
+ export { Hmr as default };
@@ -0,0 +1,7 @@
1
+ import { Context } from '@deepseek-ai/cordis';
2
+ /** Log HMR build failures with code frames when source locations are available.
3
+ * @param ctx Context owning reload diagnostics.
4
+ * @param e Failure from the module loader or compiler.
5
+ */
6
+ export declare function handleError(ctx: Context, e: unknown): void;
7
+ //# sourceMappingURL=error.d.ts.map
@@ -0,0 +1,112 @@
1
+ import { Context, Service, type Plugin } from '@deepseek-ai/cordis';
2
+ import { type ChokidarOptions } from 'chokidar';
3
+ import z from '@deepseek-ai/schemastery';
4
+ declare module '@deepseek-ai/cordis' {
5
+ interface Context {
6
+ /** Serialized plugin-code and configuration reloads. */
7
+ hmr: Hmr;
8
+ }
9
+ interface Events {
10
+ /** A watched file has no module or configuration handler.
11
+ * @mode emit
12
+ * @param url Canonical file URL.
13
+ */
14
+ 'hmr/change'(url: string): void;
15
+ /** Module replacements have finished loading.
16
+ * @mode emit
17
+ * @param reloads Replaced plugins and their module locations.
18
+ */
19
+ 'hmr/reload'(reloads: Map<Plugin, Reload>): void;
20
+ }
21
+ }
22
+ /** Module roots and watcher timing, with Chokidar deployment options. */
23
+ export interface HmrConfig extends ChokidarOptions {
24
+ /** Directory resolved against the owning context's base URL. */
25
+ base?: string;
26
+ /** Module watch roots; an empty list leaves only explicit configuration watches. */
27
+ root: string[];
28
+ /** Milliseconds for combining module changes. */
29
+ debounce: number;
30
+ /** Glob patterns excluded from module watching. */
31
+ ignored: string[];
32
+ }
33
+ /** Module location and runtime retained during a replacement. */
34
+ export interface Reload {
35
+ filename: string;
36
+ runtime?: Plugin.Runtime | undefined;
37
+ }
38
+ /** Hot reload service with Cordis-compatible module configuration and events. */
39
+ declare class Hmr extends Service {
40
+ config: HmrConfig;
41
+ /** Cordis-compatible watcher defaults. */
42
+ static Config: z<HmrConfig>;
43
+ /** Absolute base directory used to resolve module watch roots. */
44
+ baseDir: string;
45
+ private readonly ownerContext;
46
+ private internal;
47
+ private watcher;
48
+ /**
49
+ * Changes from externals will always trigger a full reload.
50
+ * Externals are the dependency tree of the CLI worker entry point.
51
+ */
52
+ private externals;
53
+ /**
54
+ * Files that should be reloaded (accepted changes).
55
+ * Includes all stashed files and their dependents.
56
+ */
57
+ private accepted;
58
+ /**
59
+ * Files that should NOT be reloaded.
60
+ * Includes externals and files whose dependents are all declined.
61
+ */
62
+ private declined;
63
+ /** Stashed file changes waiting to be processed */
64
+ private stashed;
65
+ private operations;
66
+ private readonly executing;
67
+ private applicationReady;
68
+ private closing;
69
+ private readonly configPaths;
70
+ /** Serialize a caller-owned mutation with all automatic reload paths.
71
+ * @param operation Work that must not overlap module or configuration replacement.
72
+ * @returns The operation result after its asynchronous work completes.
73
+ */
74
+ runExclusive<T>(operation: () => Promise<T>): Promise<T>;
75
+ private runReload;
76
+ /** Watch a configuration path through the same queue as module replacement.
77
+ * @param filename Absolute path, which may not exist yet.
78
+ * @param refresh Rebuilds configuration from its current files and awaits Loader completion.
79
+ * @returns Disposer closing this registration and waiting for its pending refresh.
80
+ */
81
+ watchConfig(filename: string, refresh: () => Promise<void>): Promise<() => Promise<void>>;
82
+ constructor(ctx: Context, config: HmrConfig);
83
+ /**
84
+ * Resolve a module specifier to a URL, compatible with Node 22-24.
85
+ */
86
+ private _resolve;
87
+ [Service.init](): AsyncGenerator<() => Promise<void>, void, unknown>;
88
+ /** Omit internal HMR frames from module import diagnostics.
89
+ * @returns The preserved outer stack frames.
90
+ */
91
+ getOuterStack: () => string[];
92
+ /** Read direct module dependency URLs from the active Node loader.
93
+ * @param url Module URL.
94
+ * @returns Linked module URLs, or an empty list for an uncached module.
95
+ */
96
+ getLinked(url: string): Promise<string[]>;
97
+ /**
98
+ * Classify changed files into accepted (should reload) and declined (should not).
99
+ *
100
+ * A file is accepted if it's directly changed (stashed) or if any of its
101
+ * dependents are accepted. A file is declined if all its dependents are
102
+ * declined or if it's an external.
103
+ */
104
+ private analyzeChanges;
105
+ private partialReload;
106
+ }
107
+ declare namespace Hmr {
108
+ /** Cordis-compatible configuration type. */
109
+ type Config = HmrConfig;
110
+ }
111
+ export default Hmr;
112
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,14 @@
1
+ import { type ChokidarOptions } from 'chokidar';
2
+ import type { Context } from '@deepseek-ai/cordis';
3
+ /**
4
+ * Watch one patch path, including missing parents, and serialize refresh callbacks.
5
+ * @param ctx Context that owns watcher disposal and receives refresh failures.
6
+ * @param filename Absolute patch-file path.
7
+ * @param options Deployment watcher options; configuration watches enable write stabilization by default.
8
+ * @param refresh Callback for additions, changes, and removals.
9
+ * @param inTransaction Whether disposal is running inside the refresh being removed.
10
+ * @returns A disposer that closes the watcher and drains its current refresh.
11
+ * @throws When path resolution, watcher startup, or effect registration fails.
12
+ */
13
+ export declare function watchConfig(ctx: Context, filename: string, options: ChokidarOptions, refresh: () => Promise<void> | void, inTransaction?: () => boolean): Promise<() => Promise<void>>;
14
+ //# sourceMappingURL=watch-config.d.ts.map
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@deepseek-ai/dsh-hmr",
3
+ "description": "Coordinated module and profile configuration hot reload",
4
+ "version": "0.1.6-alpha.2",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
11
+ "directory": "packages/boot/hmr"
12
+ },
13
+ "type": "module",
14
+ "main": "lib/index.js",
15
+ "types": "lib/types/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./lib/types/index.d.ts",
19
+ "default": "./lib/index.js"
20
+ },
21
+ "./src/*": "./src/*",
22
+ "./package.json": "./package.json"
23
+ },
24
+ "files": [
25
+ "lib/index.js",
26
+ "lib/types/**/*.d.ts"
27
+ ],
28
+ "author": "Shigma <shigma10826@gmail.com>",
29
+ "license": "MIT",
30
+ "peerDependencies": {
31
+ "@deepseek-ai/cordis": "^4.0.2",
32
+ "@deepseek-ai/cordis-plugin-loader": "^1.0.3",
33
+ "@deepseek-ai/cordis-plugin-include": "^1.0.7",
34
+ "@deepseek-ai/dsh-app-boot": "^0.1.6-alpha.2",
35
+ "@deepseek-ai/cordis-plugin-timer": "^1.1.4",
36
+ "@deepseek-ai/dsh-cmdline": "^0.1.6-alpha.2"
37
+ },
38
+ "dependencies": {
39
+ "@babel/code-frame": "^7.29.0",
40
+ "chokidar": "^4.0.3",
41
+ "picomatch": "^4.0.3",
42
+ "@deepseek-ai/schemastery": "^3.18.2"
43
+ },
44
+ "devDependencies": {
45
+ "@types/babel__code-frame": "^7.27.0",
46
+ "@types/picomatch": "^3.0.2",
47
+ "esbuild": "^0.28.1",
48
+ "@deepseek-ai/cordis-plugin-timer": "^1.1.4",
49
+ "@deepseek-ai/cordis": "^4.0.2",
50
+ "@deepseek-ai/cordis-plugin-loader": "^1.0.3",
51
+ "@deepseek-ai/cordis-plugin-include": "^1.0.7",
52
+ "@deepseek-ai/dsh-app-boot": "^0.1.6-alpha.2",
53
+ "@deepseek-ai/dsh-cmdline": "^0.1.6-alpha.2",
54
+ "@deepseek-ai/dsh-atomic-write": "^0.1.6-alpha.2"
55
+ }
56
+ }