dsh-codegraph-init 0.1.0 → 0.1.1

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 CHANGED
@@ -20,3 +20,23 @@ the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html
20
20
  delegating to `codegraph install -t dsh` whenever the CLI supports it.
21
21
  - Resolution fallback so a `link:`-installed checkout can still load
22
22
  `@deepseek-ai/dsh-typert-protocol` from the running installation.
23
+
24
+ ## 0.1.1 — 2026-11-12
25
+
26
+ ### Added
27
+
28
+ - The bundle now wires itself: activating the plugin writes the
29
+ `@deepseek-ai/dsh-mcp-client` row into `$DSH_HOME/cordis.patch.yml` and the
30
+ `<!-- CODEGRAPH_START -->` block into `$DSH_HOME/AGENTS.md`, so installing the
31
+ plugin also installs the MCP server and the agent instructions. Unloading it
32
+ removes exactly the parts it wrote (the patch file returns to `[]`).
33
+ - Nothing is written when a `codegraph` server already exists in this home or in
34
+ any profile, or when `codegraph` is not on `PATH` — the sidebar dialog stays
35
+ the place that installs the CLI.
36
+ - `DSH_CODEGRAPH_AUTO_WIRE=0` disables the install-time wiring and leaves the
37
+ dialog as the only writer.
38
+
39
+ ### Fixed
40
+
41
+ - Removing the row from a patch file that only held `[]` and comments now
42
+ restores an empty-list literal instead of leaving a `null` document.
package/README.md CHANGED
@@ -24,8 +24,21 @@
24
24
 
25
25
  两个入口共用同一个对话框;对话框第一次打开时会读一次宿主状态,把 DSH 目录、CLI 版本、当前是否已接线一并列出来,再决定按钮文案。
26
26
 
27
+ ## 装上插件就已经接好线
28
+
29
+ 装插件这一步本身就是安装:插件首次激活时会自己写接线,不用非得点菜单。
30
+
31
+ 1. 读 `$DSH_HOME` 与所有 profile 的 `cordis.patch.yml`,只要已经存在 `codegraph` 的 MCP 行就**什么都不写**——那条行可能是 CLI 或你自己写的,卸插件时也不会去删别人的东西。
32
+ 2. `command -v codegraph` 找不到 CLI 就跳过:接线的 `command:` 会指向空,装 CLI 是对话框的活。
33
+ 3. 都没有才动手:`cordis.patch.yml` 写 MCP 行(`command` 用 CLI 绝对路径),`AGENTS.md` 追加 `<!-- CODEGRAPH_START -->` 块。
34
+ 4. 卸载/禁用插件时只撤掉**它自己写的那两处**:patch 文件回到 `[]`,`AGENTS.md` 若被清空则整个删除。
35
+
36
+ 关掉这个行为:`DSH_CODEGRAPH_AUTO_WIRE=0`,此后只有侧栏对话框会写接线。
37
+
27
38
  ## 安装时发生了什么
28
39
 
40
+ 从侧栏菜单点「安装 codegraph」走的是完整流程(装 CLI、修复接线、校验):
41
+
29
42
  1. **检查 CLI**:`command -v codegraph`,找不到就 `npm install -g @colbymchenry/codegraph@latest`。
30
43
  2. **判断 target**:`codegraph install --print-config dsh` 能通过就走 CLI,否则插件自己写。
31
44
  3. **写接线**:`cordis.patch.yml` 追加 MCP 客户端条目;`AGENTS.md` 追加 `<!-- CODEGRAPH_START -->` 块。
package/lib/index.js CHANGED
@@ -124,6 +124,15 @@ const RUN_RETENTION_MS = 30 * 60 * 1000;
124
124
  /** The argument vector the MCP server is started with. */
125
125
  const MCP_ARGS = ["serve", "--mcp"];
126
126
 
127
+ /**
128
+ * Whether activating the bundle wires the MCP server by itself. Installing the
129
+ * plugin is then the whole installation: the row and the instructions block
130
+ * land on first activation, and unloading the plugin takes away exactly the
131
+ * parts it wrote. `DSH_CODEGRAPH_AUTO_WIRE=0` turns this off for a profile that
132
+ * wants the dialog to be the only writer.
133
+ */
134
+ const AUTO_WIRE = process.env.DSH_CODEGRAPH_AUTO_WIRE !== "0";
135
+
127
136
  /**
128
137
  * The instructions block, byte-identical to `codegraph`'s own installer so a
129
138
  * later `codegraph install -t dsh` rewrites it instead of appending a second.
@@ -545,6 +554,83 @@ function removeInstructions(text) {
545
554
  return { text: next, action: "removed" };
546
555
  }
547
556
 
557
+ /**
558
+ * Wire the MCP row and the instructions block on activation, so installing the
559
+ * bundle is also installing the wiring.
560
+ *
561
+ * Nothing is written when a `codegraph` server already exists anywhere (this
562
+ * home or any profile): that row belongs to the CLI or to the user, and taking
563
+ * it away on unload would delete wiring this plugin never added. A missing CLI
564
+ * is a skip as well — the row's `command` would point at nothing, and the
565
+ * sidebar dialog is the place that installs it.
566
+ *
567
+ * @param {string} home - Absolute DSH home.
568
+ * @returns {Promise<{row: boolean, block: boolean, patchPath: string, instructionsPath: string, skipped: "server-exists" | "cli-missing" | null}>} what was written.
569
+ */
570
+ async function applyAutoWiring(home) {
571
+ const patchPath = path.join(home, PATCH_FILE);
572
+ const instructionsPath = path.join(home, INSTRUCTIONS_FILE);
573
+ const base = { row: false, block: false, patchPath, instructionsPath, skipped: null };
574
+ const state = await wiringState(home);
575
+ if (state.configured) return { ...base, skipped: "server-exists" };
576
+ const cli = await probeCli();
577
+ if (!cli.found) return { ...base, skipped: "cli-missing" };
578
+
579
+ const current = (await readText(patchPath)) ?? "";
580
+ const row = insertServerRow(current, cli.path ?? SERVER_NAME);
581
+ if (row.action !== "unchanged") {
582
+ await mkdir(path.dirname(patchPath), { recursive: true });
583
+ await writeFile(patchPath, row.text, "utf8");
584
+ }
585
+
586
+ let block = false;
587
+ const existing = await readText(instructionsPath);
588
+ if (existing === null) {
589
+ await mkdir(path.dirname(instructionsPath), { recursive: true });
590
+ await writeFile(instructionsPath, `${INSTRUCTIONS_BLOCK}\n`, "utf8");
591
+ block = true;
592
+ } else {
593
+ const edit = upsertInstructions(existing);
594
+ // Only a block this plugin appended is ours to delete later; updating one
595
+ // that the CLI wrote keeps the CLI as its owner.
596
+ if (edit.action === "created") {
597
+ await writeFile(instructionsPath, edit.text, "utf8");
598
+ block = true;
599
+ }
600
+ }
601
+
602
+ return { row: row.action !== "unchanged", block, patchPath, instructionsPath, skipped: null };
603
+ }
604
+
605
+ /**
606
+ * Undo exactly what `applyAutoWiring` added; a skipped run has nothing to undo.
607
+ *
608
+ * @param {object | null} applied - The record `applyAutoWiring` returned.
609
+ * @returns {Promise<void>} resolution after both files are restored.
610
+ */
611
+ async function undoAutoWiring(applied) {
612
+ if (applied === null || applied.skipped !== null) return;
613
+ if (applied.row) {
614
+ const current = (await readText(applied.patchPath)) ?? "";
615
+ const edit = removeServerRows(current);
616
+ if (edit.action === "removed") {
617
+ // An empty patch file parses as `null`, which the loader rejects; the
618
+ // empty-list literal is how this file looked before the row went in.
619
+ const { lines, eol } = splitLines(edit.text);
620
+ const text = layoutOf(lines).kind === "empty" ? joinLines([...lines, "[]"], eol) : edit.text;
621
+ await writeFile(applied.patchPath, text, "utf8");
622
+ }
623
+ }
624
+ if (applied.block) {
625
+ const current = await readText(applied.instructionsPath);
626
+ if (current === null) return;
627
+ const edit = removeInstructions(current);
628
+ if (edit.action !== "removed") return;
629
+ if (edit.text.trim() === "") await rm(applied.instructionsPath, { force: true });
630
+ else await writeFile(applied.instructionsPath, edit.text, "utf8");
631
+ }
632
+ }
633
+
548
634
  /**
549
635
  * Append one line to a run's bounded buffer.
550
636
  *
@@ -626,6 +712,44 @@ class CodegraphInstallService extends TypertRemoteService {
626
712
  super(ctx, SERVICE);
627
713
  this.runs = new Map();
628
714
  this.activeRunId = null;
715
+ this.autoWiring = null;
716
+ if (AUTO_WIRE) this.armAutoWiring();
717
+ }
718
+
719
+ /**
720
+ * Bind the install-time wiring to this plugin's lifetime: activation writes
721
+ * the row, unload takes back the parts it wrote. The write itself is a pure
722
+ * file edit, so no service has to be injected first.
723
+ *
724
+ * @returns {void}
725
+ */
726
+ armAutoWiring() {
727
+ this.ctx.effect(() => {
728
+ let disposed = false;
729
+ void applyAutoWiring(dshHome())
730
+ .then(async (applied) => {
731
+ if (applied.skipped !== null) {
732
+ this.ctx.logger?.debug?.(`codegraph-init: auto wiring skipped (${applied.skipped})`);
733
+ return;
734
+ }
735
+ this.autoWiring = applied;
736
+ this.ctx.logger?.info?.(`codegraph-init: wired the codegraph MCP server into ${applied.patchPath}`);
737
+ if (disposed) await undoAutoWiring(applied);
738
+ })
739
+ .catch((error) => {
740
+ const message = error instanceof Error ? error.message : String(error);
741
+ this.ctx.logger?.warn?.(`codegraph-init: auto wiring failed: ${message}`);
742
+ });
743
+ return () => {
744
+ disposed = true;
745
+ const applied = this.autoWiring;
746
+ this.autoWiring = null;
747
+ void undoAutoWiring(applied).catch((error) => {
748
+ const message = error instanceof Error ? error.message : String(error);
749
+ this.ctx.logger?.warn?.(`codegraph-init: auto unwiring failed: ${message}`);
750
+ });
751
+ };
752
+ }, "codegraph-init: auto wiring");
629
753
  }
630
754
 
631
755
  /**
@@ -995,4 +1119,4 @@ declareRemote(CodegraphInstallService, "poll");
995
1119
  declareRemote(CodegraphInstallService, "cancel");
996
1120
  declareRemote(CodegraphInstallService, "forget");
997
1121
 
998
- export { CodegraphInstallService, CodegraphInstallService as default };
1122
+ export { CodegraphInstallService, CodegraphInstallService as default, applyAutoWiring, undoAutoWiring };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-codegraph-init",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Add codegraph install / uninstall actions to the sidebar workspace context menu, streaming the npm-based CLI install and the DSH MCP wiring into one dialog.",
5
5
  "license": "MIT",
6
6
  "author": "scccy",