@max-null/dsh-plugin-center 0.2.12 → 0.2.13

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/client.js CHANGED
@@ -1041,9 +1041,9 @@ function CenterPanel({ variant = "section" }) {
1041
1041
  const id = p.entryId;
1042
1042
  const nextEnabled = !p.enabled;
1043
1043
  const wasPending = pendingToggles.has(id);
1044
- console.log("[plugin-center] toggle", { id, fromEnabled: p.enabled, toEnabled: nextEnabled, wasPending });
1044
+ console.log("[plugin-center] toggle", { id, name: p.name, fromEnabled: p.enabled, toEnabled: nextEnabled, wasPending });
1045
1045
  setTogglingId(p.name);
1046
- void rpc("toggle", { id, disabled: !nextEnabled }).then(
1046
+ void rpc("toggle", { id, name: p.name, disabled: !nextEnabled }).then(
1047
1047
  (v) => {
1048
1048
  setTogglingId(null);
1049
1049
  const nowDisabled = typeof v === "object" && v !== null ? v.nowDisabled : null;
package/dist/engine.d.ts CHANGED
@@ -116,8 +116,12 @@ export declare class PluginCenterEngine extends Service {
116
116
  source: string;
117
117
  }[];
118
118
  }>;
119
- /** Disable/enable one loader entry through the profile patch layer. */
120
- toggle(id: string, disabled: boolean): Promise<{
119
+ /** Disable/enable one loader entry through the profile patch layer.
120
+ * 2026-08-25 禁用失效:`dsh plugin add` 清单的 insert 子条目无 id,loader
121
+ * 每次启动分配随机运行时 id,按它写禁用行重启后永远匹配不到。当 patch
122
+ * 文件中没有 `- id: <entryId>` 行时,改用该条目的包名 name 作寻址键
123
+ * (setDisabled 内按 name 把 insert 子条目升级为稳定 id 后再写禁用行)。 */
124
+ toggle(id: string, name: string, disabled: boolean): Promise<{
121
125
  ok: boolean;
122
126
  detail: string;
123
127
  nowDisabled: boolean | null;
package/dist/engine.js CHANGED
@@ -14,7 +14,7 @@ import { buildInstalledPlugin, clearPackageCache, resolvePackage } from "./meta.
14
14
  import { fetchAwesomePluginsJson, fetchDshMarketPlugins, fetchOhMyDshOverrides, fetchOhMyDshPlugins, mapConcurrent, mergePlugins, } from "./market.js";
15
15
  import { detectUpdate, installPlugin, preparePluginUpdate, updatePlugin } from "./update.js";
16
16
  import { reconcileInstalled, readDependencyKeys } from "./reconcile.js";
17
- import { readDisabledState, setDisabled } from "./toggle.js";
17
+ import { readDisabledState, setDisabled, escapeRegExp } from "./toggle.js";
18
18
  /** Runtime mirror of cordis FiberState (a cross-package const enum). */
19
19
  const FIBER_PHASE = {
20
20
  0: 'pending',
@@ -378,9 +378,30 @@ export class PluginCenterEngine extends Service {
378
378
  installed: (await this.listInstalled()).map(p => ({ name: p.name, version: p.version, source: p.source })),
379
379
  };
380
380
  }
381
- /** Disable/enable one loader entry through the profile patch layer. */
382
- async toggle(id, disabled) {
383
- const result = await setDisabled(this.baseUrl, id, disabled);
381
+ /** Disable/enable one loader entry through the profile patch layer.
382
+ * 2026-08-25 禁用失效:`dsh plugin add` 清单的 insert 子条目无 id,loader
383
+ * 每次启动分配随机运行时 id,按它写禁用行重启后永远匹配不到。当 patch
384
+ * 文件中没有 `- id: <entryId>` 行时,改用该条目的包名 name 作寻址键
385
+ * (setDisabled 内按 name 把 insert 子条目升级为稳定 id 后再写禁用行)。 */
386
+ async toggle(id, name, disabled) {
387
+ // 老调用方(未透传 name 的 client)兜底:从 loader 实时取包名。
388
+ const entryName = name !== '' ? name : [...this.ctx.loader.entries()].find(e => e.id === id)?.options.name ?? '';
389
+ if (entryName === '') {
390
+ return { ok: false, detail: `entry "${id}" not found in loader`, nowDisabled: null };
391
+ }
392
+ // 寻址键:patch 文件中已有 `- id: <id>` 行 → 稳定 id 直接用;否则是该
393
+ // 条目的随机运行时 id → 只能用 name 找到它的 insert 子条目。
394
+ const patchId = id.replace(/^include:/u, '');
395
+ let key = entryName;
396
+ try {
397
+ const patchPath = join(this.baseUrl, 'cordis.patch.yml');
398
+ if (existsSync(patchPath)
399
+ && new RegExp(`^- id: ${escapeRegExp(patchId)}$`, 'm').test(readFileSync(patchPath, 'utf8'))) {
400
+ key = patchId;
401
+ }
402
+ }
403
+ catch { /* 读失败:走 name 寻址,由 setDisabled 报具体错误 */ }
404
+ const result = await setDisabled(this.baseUrl, key, entryName, disabled);
384
405
  this.installedNamesCache = null;
385
406
  return result;
386
407
  }
package/dist/rpc.js CHANGED
@@ -55,11 +55,14 @@ export class PluginCenterRpc extends Service {
55
55
  };
56
56
  }
57
57
  case 'toggle': {
58
- const id = payload?.id;
59
- const disabled = payload?.disabled;
58
+ const payload2 = payload;
59
+ const id = payload2?.id;
60
+ const name = payload2?.name;
61
+ const disabled = payload2?.disabled;
60
62
  if (typeof id !== 'string' || id === '')
61
63
  return internal('toggle: id is required');
62
- const result = await ctx.pluginCenter.toggle(id, disabled === true);
64
+ // name 用于无稳定 id 条目的 seek-by-name 寻址(2026-08-25 禁用失效修复)。
65
+ const result = await ctx.pluginCenter.toggle(id, typeof name === 'string' ? name : '', disabled === true);
63
66
  if (!result.ok)
64
67
  return internal(`toggle ${id} 失败:${result.detail}`);
65
68
  return { ok: true, value: { nowDisabled: result.nowDisabled } };
package/dist/toggle.d.ts CHANGED
@@ -10,6 +10,15 @@
10
10
  * followed by an optional `disabled:` line), serialized so concurrent
11
11
  * toggles cannot interleave a read-modify-write, refused when the file is
12
12
  * not a plain entry list, and protected for host-infrastructure rows.
13
+ *
14
+ * Stable ids: `dsh plugin add` install lists mount entries as id-less
15
+ * `insert` children (`- name: X`), which the Loader gives a RANDOM runtime
16
+ * id on every boot (cordis-plugin-loader ensureId). Toggling by that
17
+ * runtime id writes a row no later boot matches (applyEntryPatches warns
18
+ * and skips) — the 2026-08-25 disable-broken bug. When no `- id:` row
19
+ * matches, this module addresses the entry by `name` instead: the id-less
20
+ * insert child is upgraded in place to a stable `- id: X` so the appended
21
+ * disable row actually hits. It never guesses: no match = refused write.
13
22
  */
14
23
  export interface ToggleResult {
15
24
  ok: boolean;
@@ -19,13 +28,18 @@ export interface ToggleResult {
19
28
  }
20
29
  /** What the user patch layer currently says about every row id. */
21
30
  export declare function readDisabledState(patchPath: string): Map<string, boolean>;
31
+ /** Escape a literal for use inside a RegExp (plugin names may contain `.` etc.). */
32
+ export declare function escapeRegExp(text: string): string;
22
33
  /**
23
34
  * Set one entry's disabled stance in the profile patch layer. The file is
24
35
  * only touched when the stance changes; a malformed file (not a plain
25
36
  * entry list) is reported instead of being made worse.
26
37
  * @param profileDir - the profile directory holding cordis.patch.yml.
27
38
  * @param id - the loader entry id to toggle.
39
+ * @param name - the entry's package name; used as the addressing key when
40
+ * `id` is a Loader-assigned random runtime id with no `- id:` row in the
41
+ * patch file (id-less insert children of `dsh plugin add` lists).
28
42
  * @param disabled - the target stance.
29
43
  * @returns the outcome; `nowDisabled` mirrors the stance or null when refused.
30
44
  */
31
- export declare function setDisabled(profileDir: string, entryId: string, disabled: boolean): Promise<ToggleResult>;
45
+ export declare function setDisabled(profileDir: string, entryId: string, name: string, disabled: boolean): Promise<ToggleResult>;
package/dist/toggle.js CHANGED
@@ -10,6 +10,15 @@
10
10
  * followed by an optional `disabled:` line), serialized so concurrent
11
11
  * toggles cannot interleave a read-modify-write, refused when the file is
12
12
  * not a plain entry list, and protected for host-infrastructure rows.
13
+ *
14
+ * Stable ids: `dsh plugin add` install lists mount entries as id-less
15
+ * `insert` children (`- name: X`), which the Loader gives a RANDOM runtime
16
+ * id on every boot (cordis-plugin-loader ensureId). Toggling by that
17
+ * runtime id writes a row no later boot matches (applyEntryPatches warns
18
+ * and skips) — the 2026-08-25 disable-broken bug. When no `- id:` row
19
+ * matches, this module addresses the entry by `name` instead: the id-less
20
+ * insert child is upgraded in place to a stable `- id: X` so the appended
21
+ * disable row actually hits. It never guesses: no match = refused write.
13
22
  */
14
23
  import { readFileSync, writeFileSync, existsSync } from 'node:fs';
15
24
  import { join } from 'node:path';
@@ -89,6 +98,46 @@ export function readDisabledState(patchPath) {
89
98
  }
90
99
  return state;
91
100
  }
101
+ /** Escape a literal for use inside a RegExp (plugin names may contain `.` etc.). */
102
+ export function escapeRegExp(text) {
103
+ return text.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
104
+ }
105
+ /**
106
+ * Address a `dsh plugin add` install-list insert child by name: an id-less
107
+ * child (` - name: X`, 4-space indent under `- insert:`) gets upgraded in
108
+ * place to a stable-id form (` - id: X` + ` name: X`), then a
109
+ * `- id: X / disabled: <bool>` row is appended. A child that already carries
110
+ * the stable id (` - id: X`) just gets the row appended (the upgrade must
111
+ * not regress to a random runtime id). The insert block always stays before
112
+ * the appended row, so applyEntryPatches registers the id before the toggle
113
+ * row reads it. Returns false when nothing matches (or `name` is empty) —
114
+ * callers must refuse the write, never append blindly.
115
+ */
116
+ function patchInsertChildByName(lines, name, disabled) {
117
+ if (name === '')
118
+ return false;
119
+ const idPattern = new RegExp(`^ {4}- id: ${escapeRegExp(name)}$`, 'u');
120
+ const namePattern = new RegExp(`^ {4}- name: ${escapeRegExp(name)}$`, 'u');
121
+ let found = false;
122
+ for (let i = 0; i < lines.length; i++) {
123
+ if (namePattern.test(lines[i])) {
124
+ // 无 id 子条目 → 原地升级为稳定 id(4 空格 + 6 空格 name)。
125
+ lines[i] = ` - id: ${name}\n name: ${name}`;
126
+ found = true;
127
+ break;
128
+ }
129
+ if (idPattern.test(lines[i])) {
130
+ // 已是稳定 id 子条目 → 无需升级,直接追加禁用行。
131
+ found = true;
132
+ break;
133
+ }
134
+ }
135
+ if (!found)
136
+ return false;
137
+ const tail = lines.length > 0 && lines[lines.length - 1] !== '' ? '\n' : '';
138
+ lines.push(`${tail}- id: ${name}\n disabled: ${String(disabled)}`);
139
+ return true;
140
+ }
92
141
  /** Serialize toggles so concurrent writes cannot interleave. */
93
142
  let toggleChain = Promise.resolve();
94
143
  /**
@@ -97,10 +146,13 @@ let toggleChain = Promise.resolve();
97
146
  * entry list) is reported instead of being made worse.
98
147
  * @param profileDir - the profile directory holding cordis.patch.yml.
99
148
  * @param id - the loader entry id to toggle.
149
+ * @param name - the entry's package name; used as the addressing key when
150
+ * `id` is a Loader-assigned random runtime id with no `- id:` row in the
151
+ * patch file (id-less insert children of `dsh plugin add` lists).
100
152
  * @param disabled - the target stance.
101
153
  * @returns the outcome; `nowDisabled` mirrors the stance or null when refused.
102
154
  */
103
- export function setDisabled(profileDir, entryId, disabled) {
155
+ export function setDisabled(profileDir, entryId, name, disabled) {
104
156
  const run = toggleChain.then(async () => {
105
157
  // Loader runtime ids (include:<id>) never match patch composition —
106
158
  // always address rows by their original patch id.
@@ -157,9 +209,22 @@ export function setDisabled(profileDir, entryId, disabled) {
157
209
  out.push(line);
158
210
  }
159
211
  if (!patched) {
160
- // Append a new row (the file ends with a newline when non-empty).
161
- const tail = out.length > 0 && out[out.length - 1] !== '' ? '\n' : '';
162
- out.push(`${tail}- id: ${id}\n disabled: ${String(disabled)}`);
212
+ // 2026-08-25 禁用失效根因:id-less insert 子条目每次启动拿随机运行时
213
+ // id,按它追加的禁用行重启后永远匹配不到(applyEntryPatches warn
214
+ // 静默跳过)。所以这里绝不静默追加:先按 name 寻址 insert 子条目行,
215
+ // 原地升级为稳定 id 再追加;都找不到 → 拒绝(不写文件)。
216
+ if (!patchInsertChildByName(out, name, disabled)) {
217
+ if (/^[0-9a-f]{8}$/u.test(id)) {
218
+ return {
219
+ ok: false,
220
+ detail: `entry "${id}" has no stable patch id (random runtime id); ` +
221
+ 'edit cordis.patch.yml to give its insert child an explicit id',
222
+ nowDisabled: null,
223
+ };
224
+ }
225
+ return { ok: false, detail: `no patch row or insert child matches "${id}"`, nowDisabled: null };
226
+ }
227
+ patched = true;
163
228
  }
164
229
  try {
165
230
  writeFileSync(patchPath, out.join('\n') + '\n');
package/dist/update.d.ts CHANGED
@@ -64,6 +64,15 @@ export declare function logPnpm(profileDir: string, args: readonly string[], res
64
64
  * CreateProcess 只找 pnpm.exe(.cmd/.ps1 必须经 shell)——2026-08-17 实测
65
65
  * spawn('pnpm', shell:false) 直接 ENOENT,更新永远假成功。 */
66
66
  export declare function pnpmCandidates(): string[];
67
+ /** Wrap a bundled pnpm CLI path into a runnable command line. SSID_PNPM
68
+ * (SSiD 捆绑 pnpm) points at `pnpm.cjs` — a node script. On Windows,
69
+ * spawning it directly through `shell: true` makes cmd hand the .cjs to
70
+ * its file association (ShellExecute): cmd returns exit 0 immediately and
71
+ * node never runs (2026-08-25 another machine: 142ms fake success, npm
72
+ * add 未生效; output redirection test produced a 0-byte file). Non-.cjs
73
+ * paths (e.g. pnpm.exe) are used as-is.
74
+ */
75
+ export declare function pnpmExecCommand(bundled: string): string;
67
76
  /** 归档 profile 的 node_modules 由 pnpm `<major>` 生成(SSiD 部署时把构建机
68
77
  * store 元数据改写成本机路径且保留 major 后缀——shell/main.mjs rewire)。
69
78
  * 若执行机全局 pnpm 是另一个 major(常见:机器装 pnpm 10,归档是 pnpm 11
package/dist/update.js CHANGED
@@ -185,6 +185,32 @@ export function pnpmCandidates() {
185
185
  }
186
186
  return commands;
187
187
  }
188
+ /** One node executable that can run pnpm scripts.
189
+ * SSiD 注入的 SSID_MCP_NODE(与 SSID_PNPM 同模式注入,存在即用)→ 本进程
190
+ * execPath(官方 dsh 是 node 进程)→ PATH 的 node。 */
191
+ function nodeCandidate() {
192
+ const fromEnv = process.env.SSID_MCP_NODE;
193
+ if (fromEnv !== undefined && fromEnv !== '')
194
+ return fromEnv;
195
+ const exe = process.execPath;
196
+ if (/node(?:\.exe)?$/i.test(exe))
197
+ return exe;
198
+ return 'node';
199
+ }
200
+ /** Wrap a bundled pnpm CLI path into a runnable command line. SSID_PNPM
201
+ * (SSiD 捆绑 pnpm) points at `pnpm.cjs` — a node script. On Windows,
202
+ * spawning it directly through `shell: true` makes cmd hand the .cjs to
203
+ * its file association (ShellExecute): cmd returns exit 0 immediately and
204
+ * node never runs (2026-08-25 another machine: 142ms fake success, npm
205
+ * add 未生效; output redirection test produced a 0-byte file). Non-.cjs
206
+ * paths (e.g. pnpm.exe) are used as-is.
207
+ */
208
+ export function pnpmExecCommand(bundled) {
209
+ if (!/\.(cjs|mjs|js)$/i.test(bundled))
210
+ return bundled;
211
+ const node = nodeCandidate();
212
+ return node === null ? bundled : `"${node}" "${bundled}"`;
213
+ }
188
214
  /** 归档 profile 的 node_modules 由 pnpm `<major>` 生成(SSiD 部署时把构建机
189
215
  * store 元数据改写成本机路径且保留 major 后缀——shell/main.mjs rewire)。
190
216
  * 若执行机全局 pnpm 是另一个 major(常见:机器装 pnpm 10,归档是 pnpm 11
@@ -214,7 +240,7 @@ export function pnpmCommandCandidates(profileDir) {
214
240
  const commands = [];
215
241
  const bundled = process.env.SSID_PNPM;
216
242
  if (bundled !== undefined && bundled !== '')
217
- commands.push(bundled);
243
+ commands.push(pnpmExecCommand(bundled));
218
244
  commands.push(...pnpmCandidates());
219
245
  const major = detectStoreMajor(profileDir);
220
246
  if (major !== undefined) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@max-null/dsh-plugin-center",
3
- "version": "0.2.12",
3
+ "version": "0.2.13",
4
4
  "description": "Plugin center for DeepSeek Harness 閳?installed metadata, community market, update detection, and What's New",
5
5
  "license": "MIT",
6
6
  "publishConfig": {