dsh-vscode-mode 0.1.16 → 0.1.17

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/lib/index.js CHANGED
@@ -2,7 +2,8 @@ import { createRequire } from "node:module";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { dirname, extname, join, normalize, resolve, sep } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { existsSync, readFileSync } from "node:fs";
5
+ import { existsSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
6
+ import { homedir } from "node:os";
6
7
  import { createHash } from "node:crypto";
7
8
  //#region src/mcp.ts
8
9
  /**
@@ -198,6 +199,146 @@ function normalizeFileOpenTool(value) {
198
199
  return normalizeValue(value);
199
200
  }
200
201
  //#endregion
202
+ //#region src/devForm.ts
203
+ /**
204
+ * dsh-vscode-mode host — 开发形态管理(安装形态切换)。
205
+ * 开发形态 = profile 中以 link: 依赖 + junction 指向工作区的安装(用于插件开发);
206
+ * 关闭 = 改回版本依赖 + 删 junction + pnpm install 装配正式版。
207
+ * 切换后需重启 DSH 生效(运行中的代码仍是旧形态)。
208
+ * 与 compat.ts 存在模块环(devForm 用 pluginVersionOf、compat 用 readDevForm),
209
+ * 双方仅在函数体内交叉使用,模块求值期无依赖,ESM 安全。
210
+ * 作者 ddj 2026-08-24
211
+ */
212
+ /** 扫描 $DSH_HOME/profiles 找到依赖本插件的 profile 目录(无 env 依赖,多 profile 安全)。 */
213
+ function findProfileDir() {
214
+ const home = process.env.DSH_HOME ?? join(homedir(), ".dsh");
215
+ try {
216
+ for (const name of readdirSync(join(home, "profiles"))) {
217
+ const pkgFile = join(home, "profiles", name, "package.json");
218
+ if (!existsSync(pkgFile)) continue;
219
+ try {
220
+ if (JSON.parse(readFileSync(pkgFile, "utf8"))?.dependencies?.["dsh-vscode-mode"]) return dirname(pkgFile);
221
+ } catch {}
222
+ }
223
+ } catch {}
224
+ }
225
+ /**
226
+ * 读取当前开发形态(manifest link: 判定)。
227
+ * @author ddj 2026年08月24号
228
+ * @returns 开发形态状态
229
+ */
230
+ function readDevForm() {
231
+ const profileDir = findProfileDir();
232
+ if (!profileDir) return { enabled: false };
233
+ try {
234
+ const text = readFileSync(join(profileDir, "package.json"), "utf8").replace(/^\uFEFF/, "");
235
+ const spec = JSON.parse(text)?.dependencies?.[PLUGIN_NAME];
236
+ if (typeof spec === "string" && spec.startsWith("link:")) return {
237
+ enabled: true,
238
+ path: spec.slice(5)
239
+ };
240
+ } catch {}
241
+ return { enabled: false };
242
+ }
243
+ /**
244
+ * 规划切换后的 manifest 文本(纯函数,可单测;非法 JSON 抛错)。
245
+ * @author ddj 2026年08月24号
246
+ * @param text 原 manifest 文本
247
+ * @param enabled 目标形态(true=link:,false=版本)
248
+ * @param path 工作区路径(开启时必填)
249
+ * @param version 版本号(关闭时使用,写入 ^<version>)
250
+ * @returns 新 manifest 文本(无 BOM,2 空格缩进)
251
+ */
252
+ function planManifest(text, enabled, path, version) {
253
+ const data = JSON.parse(text.replace(/^\uFEFF/, ""));
254
+ if (!data.dependencies || typeof data.dependencies !== "object") throw new Error("profile package.json 缺少 dependencies 字段");
255
+ data.dependencies[PLUGIN_NAME] = enabled ? "link:" + path.replace(/\\/g, "/") : "^" + version;
256
+ return JSON.stringify(data, null, 2) + "\n";
257
+ }
258
+ /** 建立或移除 node_modules 下的开发 junction(目标不存在时开启失败)。 */
259
+ function applyJunction(profileDir, enabled, target) {
260
+ const linkDir = join(profileDir, "node_modules", PLUGIN_NAME);
261
+ if (existsSync(linkDir)) rmSync(linkDir, {
262
+ recursive: true,
263
+ force: true
264
+ });
265
+ if (!enabled) return;
266
+ if (!existsSync(target)) throw new Error("工作区路径不存在:" + target);
267
+ symlinkSync(target, linkDir, "junction");
268
+ }
269
+ /**
270
+ * 切换开发形态:改 manifest → 建/删 junction → pnpm install(subprocess)。
271
+ * @author ddj 2026年08月24号
272
+ * @param ctx DSH host 上下文(subprocess 服务)
273
+ * @param enabled 目标形态
274
+ * @param path 工作区绝对路径(开启时必填)
275
+ * @returns 切换结果(restart=true 表示需重启 DSH 生效)
276
+ */
277
+ async function setDevForm(ctx, enabled, path) {
278
+ const profileDir = findProfileDir();
279
+ if (!profileDir) return {
280
+ ok: false,
281
+ error: "未找到依赖本插件的 profile(检查 DSH_HOME/profiles)",
282
+ restart: false
283
+ };
284
+ const current = readDevForm();
285
+ const target = (path ?? current.path ?? "").replace(/\\/g, "/");
286
+ if (enabled && !target) return {
287
+ ok: false,
288
+ error: "开启开发形态需要提供工作区路径(path 参数)",
289
+ restart: false
290
+ };
291
+ if (enabled === current.enabled && (!enabled || target === current.path)) return {
292
+ ok: true,
293
+ restart: false
294
+ };
295
+ try {
296
+ const pkgFile = join(profileDir, "package.json");
297
+ const version = pluginVersionOf();
298
+ if (!version) return {
299
+ ok: false,
300
+ error: "读取插件版本失败,无法写入版本依赖",
301
+ restart: false
302
+ };
303
+ const next = planManifest(readFileSync(pkgFile, "utf8").replace(/^\uFEFF/, ""), enabled, target, version);
304
+ writeFileSync(pkgFile, next, "utf8");
305
+ applyJunction(profileDir, enabled, target);
306
+ const installError = await runPnpmInstall(ctx, profileDir);
307
+ return {
308
+ ok: !installError,
309
+ error: installError,
310
+ restart: true
311
+ };
312
+ } catch (error) {
313
+ return {
314
+ ok: false,
315
+ error: String(error),
316
+ restart: false
317
+ };
318
+ }
319
+ }
320
+ /** 在 profile 目录执行 pnpm install(subprocess 契约:spawn({argv,cwd,stdio,graceMs}))。 */
321
+ async function runPnpmInstall(ctx, profileDir) {
322
+ const sub = ctx.get("subprocess");
323
+ if (!sub || typeof sub.spawn !== "function") return "manifest/junction 已更新,但缺少 subprocess:请手动在 profile 目录运行 pnpm install";
324
+ const handle = sub.spawn({
325
+ argv: ["pnpm", "install"],
326
+ cwd: profileDir,
327
+ stdio: {
328
+ stdout: { maxBytes: 65536 },
329
+ stderr: { maxBytes: 65536 },
330
+ stdin: "ignore"
331
+ },
332
+ graceMs: 12e4
333
+ });
334
+ const outcome = await handle.done;
335
+ const code = outcome?.exitCode ?? outcome?.code;
336
+ if (code !== 0) {
337
+ const stderr = handle.collected?.stderr?.readFrom(0).text ?? "";
338
+ return "pnpm install 失败(exit " + code + "):" + stderr.slice(0, 500);
339
+ }
340
+ }
341
+ //#endregion
201
342
  //#region src/compat.ts
202
343
  /**
203
344
  * dsh-vscode-mode host — 兼容层:与其他插件 / DSH 版本的统一探测、适配、护栏与自诊断。
@@ -327,7 +468,8 @@ async function buildReport(ctx, options) {
327
468
  pluginVersion: options?.version ?? pluginVersionOf(),
328
469
  external,
329
470
  guards,
330
- warnings
471
+ warnings,
472
+ devForm: readDevForm()
331
473
  };
332
474
  }
333
475
  //#endregion
@@ -2790,7 +2932,30 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx)) {
2790
2932
  "compat": async () => ({
2791
2933
  ok: true,
2792
2934
  report: await buildReport(ctx)
2793
- })
2935
+ }),
2936
+ "vscode.devFormGet": async () => ({
2937
+ ok: true,
2938
+ devForm: readDevForm()
2939
+ }),
2940
+ "vscode.devFormSet": async (args) => {
2941
+ try {
2942
+ const result = await setDevForm(ctx, args.enabled === true, args.path);
2943
+ if (!result.ok) return {
2944
+ ok: false,
2945
+ error: result.error ?? "切换开发形态失败"
2946
+ };
2947
+ return {
2948
+ ok: true,
2949
+ devForm: readDevForm(),
2950
+ restart: result.restart
2951
+ };
2952
+ } catch (error) {
2953
+ return {
2954
+ ok: false,
2955
+ error: String(error)
2956
+ };
2957
+ }
2958
+ }
2794
2959
  };
2795
2960
  }
2796
2961
  /**