@chatcode/cco-market 1.45.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/LICENSE +21 -0
- package/README.md +146 -0
- package/README.zh.md +143 -0
- package/UPDATE-API-V1.md +132 -0
- package/client/client.js +11534 -0
- package/cordis.patch.yml +5 -0
- package/lib/accelerate.js +184 -0
- package/lib/agents.js +36 -0
- package/lib/backup.js +572 -0
- package/lib/catalog-local-match.js +126 -0
- package/lib/catalog-npm.js +111 -0
- package/lib/changelog.js +242 -0
- package/lib/channels.js +64 -0
- package/lib/check.js +1067 -0
- package/lib/compatibility.js +198 -0
- package/lib/diagnostics.js +49 -0
- package/lib/discovery-compatibility.js +260 -0
- package/lib/dsh-cli.js +949 -0
- package/lib/dsh-install.js +108 -0
- package/lib/gist.js +352 -0
- package/lib/groups.js +97 -0
- package/lib/home-paths.js +42 -0
- package/lib/hot.js +519 -0
- package/lib/http.js +39 -0
- package/lib/index.js +97 -0
- package/lib/install.js +387 -0
- package/lib/log.js +197 -0
- package/lib/ndjson.js +154 -0
- package/lib/net.js +98 -0
- package/lib/order.js +284 -0
- package/lib/patch.js +521 -0
- package/lib/pnpm-compat.js +490 -0
- package/lib/presets.js +278 -0
- package/lib/profile.js +911 -0
- package/lib/region-probe.js +93 -0
- package/lib/regions.js +253 -0
- package/lib/registry.js +204 -0
- package/lib/restart.js +373 -0
- package/lib/routes.js +4372 -0
- package/lib/settings.js +109 -0
- package/lib/skill-market/api.js +119 -0
- package/lib/skill-market/config.js +29 -0
- package/lib/skill-market/installer.js +352 -0
- package/lib/skill-market/runtime.js +13 -0
- package/lib/skill-market/tui.js +119 -0
- package/lib/skill-market/types.js +1 -0
- package/lib/skill-market/web-routes.js +123 -0
- package/lib/snapshot.js +500 -0
- package/lib/source-migration.js +54 -0
- package/lib/sources.js +596 -0
- package/lib/store.js +91 -0
- package/lib/themes.js +102 -0
- package/lib/trial.js +116 -0
- package/lib/types/accelerate.d.ts +85 -0
- package/lib/types/agents.d.ts +22 -0
- package/lib/types/backup.d.ts +131 -0
- package/lib/types/catalog-local-match.d.ts +50 -0
- package/lib/types/catalog-npm.d.ts +47 -0
- package/lib/types/changelog.d.ts +102 -0
- package/lib/types/channels.d.ts +56 -0
- package/lib/types/check.d.ts +266 -0
- package/lib/types/compatibility.d.ts +64 -0
- package/lib/types/diagnostics.d.ts +29 -0
- package/lib/types/discovery-compatibility.d.ts +80 -0
- package/lib/types/dsh-cli.d.ts +324 -0
- package/lib/types/dsh-install.d.ts +46 -0
- package/lib/types/gist.d.ts +53 -0
- package/lib/types/groups.d.ts +34 -0
- package/lib/types/home-paths.d.ts +16 -0
- package/lib/types/hot.d.ts +221 -0
- package/lib/types/http.d.ts +12 -0
- package/lib/types/index.d.ts +14 -0
- package/lib/types/install.d.ts +172 -0
- package/lib/types/log.d.ts +43 -0
- package/lib/types/ndjson.d.ts +52 -0
- package/lib/types/net.d.ts +58 -0
- package/lib/types/order.d.ts +100 -0
- package/lib/types/patch.d.ts +129 -0
- package/lib/types/pnpm-compat.d.ts +93 -0
- package/lib/types/presets.d.ts +90 -0
- package/lib/types/profile.d.ts +253 -0
- package/lib/types/region-probe.d.ts +50 -0
- package/lib/types/regions.d.ts +122 -0
- package/lib/types/registry.d.ts +86 -0
- package/lib/types/restart.d.ts +194 -0
- package/lib/types/routes.d.ts +67 -0
- package/lib/types/settings.d.ts +78 -0
- package/lib/types/skill-market/api.d.ts +10 -0
- package/lib/types/skill-market/config.d.ts +3 -0
- package/lib/types/skill-market/installer.d.ts +23 -0
- package/lib/types/skill-market/runtime.d.ts +10 -0
- package/lib/types/skill-market/tui.d.ts +4 -0
- package/lib/types/skill-market/types.d.ts +71 -0
- package/lib/types/skill-market/web-routes.d.ts +19 -0
- package/lib/types/snapshot.d.ts +93 -0
- package/lib/types/source-migration.d.ts +11 -0
- package/lib/types/sources.d.ts +216 -0
- package/lib/types/store.d.ts +30 -0
- package/lib/types/themes.d.ts +40 -0
- package/lib/types/trial.d.ts +61 -0
- package/lib/types/update-api-v1.d.ts +66 -0
- package/lib/types/updates.d.ts +123 -0
- package/lib/types/verify.d.ts +139 -0
- package/lib/update-api-v1.js +215 -0
- package/lib/updates.js +361 -0
- package/lib/verify.js +453 -0
- package/package.json +121 -0
- package/src/accelerate.ts +213 -0
- package/src/agents.ts +43 -0
- package/src/backup.ts +583 -0
- package/src/catalog-local-match.ts +144 -0
- package/src/catalog-npm.ts +120 -0
- package/src/changelog.ts +282 -0
- package/src/channels.ts +70 -0
- package/src/check.ts +1239 -0
- package/src/client/CommentsModal.tsx +119 -0
- package/src/client/Diagnostics.tsx +907 -0
- package/src/client/ErrorBoundary.tsx +111 -0
- package/src/client/InstallToast.tsx +31 -0
- package/src/client/Market.module.css +830 -0
- package/src/client/MarketSection.tsx +5301 -0
- package/src/client/OperationsPanel.tsx +365 -0
- package/src/client/SettingsCard.tsx +621 -0
- package/src/client/SkillMarket.module.css +21 -0
- package/src/client/SkillMarketSection.tsx +162 -0
- package/src/client/comments.ts +54 -0
- package/src/client/globals.d.ts +13 -0
- package/src/client/index.ts +180 -0
- package/src/client/locales.ts +1112 -0
- package/src/client/market-data.ts +1321 -0
- package/src/client/operations.ts +201 -0
- package/src/client/preset-panel.tsx +263 -0
- package/src/client/primitives.d.ts +140 -0
- package/src/client/self-check.ts +147 -0
- package/src/client/snapshot-panel.tsx +244 -0
- package/src/compatibility.ts +237 -0
- package/src/diagnostics.ts +84 -0
- package/src/discovery-compatibility.ts +315 -0
- package/src/dsh-cli.ts +1126 -0
- package/src/dsh-install.ts +118 -0
- package/src/gist.ts +362 -0
- package/src/groups.ts +111 -0
- package/src/home-paths.ts +53 -0
- package/src/hot.ts +628 -0
- package/src/http.ts +41 -0
- package/src/index.ts +128 -0
- package/src/install.ts +420 -0
- package/src/log.ts +206 -0
- package/src/ndjson.ts +185 -0
- package/src/net.ts +106 -0
- package/src/order.ts +303 -0
- package/src/patch.ts +522 -0
- package/src/pnpm-compat.ts +527 -0
- package/src/presets.ts +344 -0
- package/src/profile.ts +940 -0
- package/src/region-probe.ts +97 -0
- package/src/regions.ts +310 -0
- package/src/registry.ts +250 -0
- package/src/restart.ts +396 -0
- package/src/routes.ts +4506 -0
- package/src/settings.ts +141 -0
- package/src/skill-market/api.ts +130 -0
- package/src/skill-market/config.ts +32 -0
- package/src/skill-market/installer.ts +337 -0
- package/src/skill-market/runtime.ts +14 -0
- package/src/skill-market/tui.ts +157 -0
- package/src/skill-market/types.ts +80 -0
- package/src/skill-market/web-routes.ts +132 -0
- package/src/snapshot.ts +532 -0
- package/src/source-migration.ts +61 -0
- package/src/sources.ts +565 -0
- package/src/store.ts +89 -0
- package/src/themes.ts +125 -0
- package/src/trial.ts +156 -0
- package/src/update-api-v1.ts +277 -0
- package/src/updates.ts +400 -0
- package/src/verify.ts +492 -0
package/lib/patch.js
ADDED
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Patch-layer plugin toggles — hot disable/enable through the profile's
|
|
3
|
+
* user patch layer (cordis.patch.yml), the mechanism ported from
|
|
4
|
+
* Noob-stupid/dsh-plugin-hub's plugin console.
|
|
5
|
+
*
|
|
6
|
+
* DSH composes a web profile from the bundle layers + the user patch layer
|
|
7
|
+
* (`$CHATCODE_CLI_HOME/profiles/<name>/cordis.patch.yml`), with per-key override
|
|
8
|
+
* semantics: a patch row `- id: X` + `disabled: true` stops that loader
|
|
9
|
+
* entry, and `disabled: false` force-enables one a lower layer disabled.
|
|
10
|
+
* The profile's config-file watcher (HMR) re-composes within ~1s of the
|
|
11
|
+
* save — no restart — and the loader re-applies the same file on every
|
|
12
|
+
* boot, so the choice survives restarts through the official mechanism.
|
|
13
|
+
*
|
|
14
|
+
* The market ALSO keeps its own in-memory/state.json bookkeeping (hot-mount
|
|
15
|
+
* shims have no bundle row to patch, and the client's disable list drives
|
|
16
|
+
* the switches); this module is the durable, HMR-driven layer on top.
|
|
17
|
+
*
|
|
18
|
+
* Safety (borrowed from the plugin-hub implementation):
|
|
19
|
+
* - writes are serialized so concurrent toggles cannot interleave a
|
|
20
|
+
* read-modify-write;
|
|
21
|
+
* - an append is REFUSED when the patch file is not a valid entry list —
|
|
22
|
+
* a malformed file (e.g. a stray `[]` followed by items) is never made
|
|
23
|
+
* worse, the market reports it instead;
|
|
24
|
+
* - host infrastructure rows (transport / hot-reload / storage / settings
|
|
25
|
+
* chains) are protected and refuse to toggle.
|
|
26
|
+
*/
|
|
27
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
28
|
+
import { join } from 'node:path';
|
|
29
|
+
import { fileURLToPath } from 'node:url';
|
|
30
|
+
import { logEvent } from './log.js';
|
|
31
|
+
import { parsePatchFile, parsePatchText } from './check.js';
|
|
32
|
+
import { bundlePatchInsertedIds, parsePatchRows } from './profile.js';
|
|
33
|
+
/**
|
|
34
|
+
* Host infrastructure rows: disabling any of these breaks the very chain
|
|
35
|
+
* the patch layer runs on (e.g. timer → HMR, webserver → the page itself),
|
|
36
|
+
* so they refuse to toggle. Same list the plugin-hub console uses — the
|
|
37
|
+
* target host is the same DSH.
|
|
38
|
+
*/
|
|
39
|
+
const PROTECTED_MODULE_PATTERNS = [
|
|
40
|
+
/^cordis:/u,
|
|
41
|
+
/^@deepseek-ai\/cordis-plugin-/u,
|
|
42
|
+
/^@deepseek-ai\/dsh-host-/u,
|
|
43
|
+
/^@deepseek-ai\/dsh-client-modules$/u,
|
|
44
|
+
/^@deepseek-ai\/dsh-client-connection$/u,
|
|
45
|
+
/^@deepseek-ai\/dsh-client-hmr$/u,
|
|
46
|
+
/^@deepseek-ai\/dsh-client-runtime$/u,
|
|
47
|
+
/^@deepseek-ai\/dsh-client-locale$/u,
|
|
48
|
+
/^@deepseek-ai\/dsh-client-web/u,
|
|
49
|
+
/^@deepseek-ai\/dsh-web-frontend$/u,
|
|
50
|
+
/^@deepseek-ai\/dsh-web-app$/u,
|
|
51
|
+
/^@deepseek-ai\/dsh-settings/u,
|
|
52
|
+
/^@deepseek-ai\/dsh-credentials/u,
|
|
53
|
+
/^@deepseek-ai\/dsh-session/u,
|
|
54
|
+
/^@deepseek-ai\/dsh-storage/u,
|
|
55
|
+
/^@deepseek-ai\/dsh-typert/u,
|
|
56
|
+
/^@deepseek-ai\/dsh-api-remotes$/u,
|
|
57
|
+
/^@deepseek-ai\/dsh-tools$/u,
|
|
58
|
+
/^@deepseek-ai\/dsh-system-prompt$/u,
|
|
59
|
+
/^@deepseek-ai\/dsh-agent/u,
|
|
60
|
+
/^@deepseek-ai\/dsh-llm/u,
|
|
61
|
+
/^@deepseek-ai\/dsh-persona$/u,
|
|
62
|
+
/^@deepseek-ai\/dsh-scope$/u,
|
|
63
|
+
/^@deepseek-ai\/dsh-launch-environment$/u,
|
|
64
|
+
/^@deepseek-ai\/dsh-shell$/u,
|
|
65
|
+
/^@deepseek-ai\/dsh-subprocess/u,
|
|
66
|
+
/^@deepseek-ai\/dsh-fs/u,
|
|
67
|
+
/^@deepseek-ai\/dsh-sandbox/u,
|
|
68
|
+
/^@deepseek-ai\/dsh-jobs/u,
|
|
69
|
+
/^@deepseek-ai\/dsh-skill/u,
|
|
70
|
+
/^@deepseek-ai\/dsh-goal/u,
|
|
71
|
+
/^@deepseek-ai\/dsh-workflow/u,
|
|
72
|
+
/^@deepseek-ai\/dsh-subagent/u,
|
|
73
|
+
/^@deepseek-ai\/dsh-web$/u,
|
|
74
|
+
/^@deepseek-ai\/dsh-workspace/u,
|
|
75
|
+
/^@deepseek-ai\/dsh-user-approval$/u,
|
|
76
|
+
/^@deepseek-ai\/dsh-user-questions$/u,
|
|
77
|
+
/^@deepseek-ai\/dsh-commands$/u,
|
|
78
|
+
/^@deepseek-ai\/dsh-hook/u,
|
|
79
|
+
/^@deepseek-ai\/dsh-spill/u,
|
|
80
|
+
/^@deepseek-ai\/dsh-guard/u,
|
|
81
|
+
/^@deepseek-ai\/dsh-tool-call-timeout-policy$/u,
|
|
82
|
+
/^@deepseek-ai\/dsh-repeat-tool-reminder$/u,
|
|
83
|
+
];
|
|
84
|
+
/** True when the module name sits on the host infrastructure chain. */
|
|
85
|
+
export function isProtectedModule(moduleName) {
|
|
86
|
+
return typeof moduleName === 'string' && PROTECTED_MODULE_PATTERNS.some(pattern => pattern.test(moduleName));
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Resolve the profile's user patch layer. Prefers the path the loader's
|
|
90
|
+
* cordis:include entry actually read (authoritative under hosts that own
|
|
91
|
+
* the profile directory, like DSH Desktop); falls back to the conventional
|
|
92
|
+
* `<profile>/cordis.patch.yml`.
|
|
93
|
+
*/
|
|
94
|
+
export function findUserPatchPath(host, profileDir) {
|
|
95
|
+
for (const entry of host.loader.entries()) {
|
|
96
|
+
const cfg = entry.options?.config;
|
|
97
|
+
if (entry.options?.name !== 'cordis:include' || cfg == null || typeof cfg.path !== 'string')
|
|
98
|
+
continue;
|
|
99
|
+
if (!cfg.path.includes('cordis.yml'))
|
|
100
|
+
continue;
|
|
101
|
+
let includePath = cfg.path;
|
|
102
|
+
if (includePath.startsWith('file://')) {
|
|
103
|
+
try {
|
|
104
|
+
includePath = fileURLToPath(includePath);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// fileURLToPath rejects POSIX-style URLs on Windows; the scheme is
|
|
108
|
+
// all we need to strip for the path to be usable.
|
|
109
|
+
includePath = includePath.replace(/^file:\/\//u, '');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return includePath.replace(/cordis\.yml$/u, 'cordis.patch.yml');
|
|
113
|
+
}
|
|
114
|
+
return join(profileDir, 'cordis.patch.yml');
|
|
115
|
+
}
|
|
116
|
+
/** Row ids the market is allowed to write: plain unquoted YAML scalars. */
|
|
117
|
+
const ROW_ID_RE = /^[A-Za-z0-9_.-]+$/u;
|
|
118
|
+
/**
|
|
119
|
+
* Line-wise scan of one patch file — the plugin-hub shapes. Deliberately
|
|
120
|
+
* not a YAML parse: the file may hold structures the market's dialect
|
|
121
|
+
* rejects, but a plain `- id: X` + `disabled: true|false` pair is enough
|
|
122
|
+
* to know what the user patch layer says.
|
|
123
|
+
*/
|
|
124
|
+
export function readUserPatchState(patchPath) {
|
|
125
|
+
const disables = [];
|
|
126
|
+
const forced = [];
|
|
127
|
+
const inserts = [];
|
|
128
|
+
let text = '';
|
|
129
|
+
try {
|
|
130
|
+
text = readFileSync(patchPath, 'utf8');
|
|
131
|
+
}
|
|
132
|
+
catch { /* no patch file — empty state */ }
|
|
133
|
+
const lines = text.split(/\r?\n/u);
|
|
134
|
+
let inInsert = false;
|
|
135
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
136
|
+
const line = lines[index] ?? '';
|
|
137
|
+
if (/^- insert:\s*$/u.test(line)) {
|
|
138
|
+
inInsert = true;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (/^- /u.test(line))
|
|
142
|
+
inInsert = false;
|
|
143
|
+
if (inInsert) {
|
|
144
|
+
const insertRow = /^ {4}- id: ([A-Za-z0-9_.-]+)/u.exec(line);
|
|
145
|
+
if (insertRow !== null)
|
|
146
|
+
inserts.push(insertRow[1]);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const disableRow = /^- id: ([A-Za-z0-9_.-]+)\s*$/u.exec(line);
|
|
150
|
+
if (disableRow === null)
|
|
151
|
+
continue;
|
|
152
|
+
const next = lines[index + 1] ?? '';
|
|
153
|
+
if (/^ {2}disabled: true\s*$/u.test(next))
|
|
154
|
+
disables.push(disableRow[1]);
|
|
155
|
+
else if (/^ {2}disabled: false\s*$/u.test(next))
|
|
156
|
+
forced.push(disableRow[1]);
|
|
157
|
+
}
|
|
158
|
+
return { disables, forced, inserts };
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* User-authored insert rows that still load `packageName` (or one of its
|
|
162
|
+
* exported subpaths). This evidence deliberately stays separate from the
|
|
163
|
+
* package's own bundle declaration: uninstall may clean market-owned rows,
|
|
164
|
+
* but it must never rewrite an insert the user owns.
|
|
165
|
+
*
|
|
166
|
+
* A missing patch is the ordinary empty-profile shape. `null` means the
|
|
167
|
+
* existing file could not be read as DSH's patch dialect; callers must treat
|
|
168
|
+
* that as indeterminate and refuse the destructive step.
|
|
169
|
+
*/
|
|
170
|
+
export function userPatchPackageReferences(patchPath, packageName) {
|
|
171
|
+
let source;
|
|
172
|
+
try {
|
|
173
|
+
source = readFileSync(patchPath, 'utf8');
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
const code = error !== null && typeof error === 'object' && 'code' in error
|
|
177
|
+
? error.code
|
|
178
|
+
: undefined;
|
|
179
|
+
if (code === 'ENOENT')
|
|
180
|
+
return [];
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
// Use the same schema as profile composition, including !!js scalars. A
|
|
184
|
+
// line scanner cannot distinguish flow-style entries or nested loader
|
|
185
|
+
// groups from arbitrary config keys in a hand-written user patch.
|
|
186
|
+
const rows = parsePatchText(source);
|
|
187
|
+
if (rows === null)
|
|
188
|
+
return null;
|
|
189
|
+
const insertedNames = new Set();
|
|
190
|
+
const visiting = new Set();
|
|
191
|
+
const visited = new Set();
|
|
192
|
+
const collect = (entries) => {
|
|
193
|
+
if (visited.has(entries))
|
|
194
|
+
return true;
|
|
195
|
+
if (visiting.has(entries))
|
|
196
|
+
return false;
|
|
197
|
+
visiting.add(entries);
|
|
198
|
+
for (const entry of entries) {
|
|
199
|
+
if (entry === null || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
200
|
+
visiting.delete(entries);
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
const row = entry;
|
|
204
|
+
if ('name' in row && typeof row.name !== 'string') {
|
|
205
|
+
visiting.delete(entries);
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
if (typeof row.name === 'string')
|
|
209
|
+
insertedNames.add(row.name);
|
|
210
|
+
// Only group rows treat an array-valued config as child loader rows.
|
|
211
|
+
// Other plugins may use arrays of `{ name: ... }` as ordinary options,
|
|
212
|
+
// and those must not become false package references.
|
|
213
|
+
if (row.group === true && Array.isArray(row.config)) {
|
|
214
|
+
if (!collect(row.config)) {
|
|
215
|
+
visiting.delete(entries);
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
visiting.delete(entries);
|
|
221
|
+
visited.add(entries);
|
|
222
|
+
return true;
|
|
223
|
+
};
|
|
224
|
+
for (const patch of rows) {
|
|
225
|
+
if (patch === null || typeof patch !== 'object' || Array.isArray(patch))
|
|
226
|
+
return null;
|
|
227
|
+
const patchRow = patch;
|
|
228
|
+
if (!('insert' in patchRow))
|
|
229
|
+
continue;
|
|
230
|
+
if (!Array.isArray(patchRow.insert) || !collect(patchRow.insert))
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
return [...insertedNames].filter(reference => reference === packageName || reference.startsWith(`${packageName}/`));
|
|
234
|
+
}
|
|
235
|
+
/** The include entry's id prefix (loader entry ids look like `include:X`). */
|
|
236
|
+
function includePrefix(host) {
|
|
237
|
+
for (const entry of host.loader.entries()) {
|
|
238
|
+
if (entry.options?.name === 'cordis:include' && typeof entry.options.id === 'string') {
|
|
239
|
+
return `${entry.options.id}:`;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return '';
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* The user-patch row ids one installed package owns: its bundle patch's
|
|
246
|
+
* insert rows, plus the loader entries currently carrying its name.
|
|
247
|
+
* Empty for client-only packages (no bundle rows) — the market's own
|
|
248
|
+
* state.json mechanism covers those, and there is nothing to patch.
|
|
249
|
+
* Market-owned namespaces (hot-mount `mkt-*`, shim `client-*`) are
|
|
250
|
+
* excluded: their rows live in the market's own include subtree, and a
|
|
251
|
+
* permanent patch row targeting them would be a boot-time orphan.
|
|
252
|
+
*/
|
|
253
|
+
export function rowIdsForPackage(host, profileDirectory, packageName) {
|
|
254
|
+
const ids = new Set();
|
|
255
|
+
const packageDir = join(profileDirectory, 'node_modules', packageName);
|
|
256
|
+
// ONLY the rows this package inserts (#147). A bundle patch also carries
|
|
257
|
+
// rows that merely reconfigure OTHER plugins — dsh-vision-router tunes the
|
|
258
|
+
// official `attachment-local` row — and writing `disabled: true` onto
|
|
259
|
+
// those took down attachments and the DeepSeek model along with it.
|
|
260
|
+
try {
|
|
261
|
+
for (const id of bundlePatchInsertedIds(packageDir))
|
|
262
|
+
ids.add(id);
|
|
263
|
+
}
|
|
264
|
+
catch { /* package not installed — loader side may still know it */ }
|
|
265
|
+
// The conventional location too: a package may ship cordis.patch.yml at
|
|
266
|
+
// its root without declaring dsh.bundle.patch (the loader probes it too).
|
|
267
|
+
// Same parser, deliberately: this used to be a second hand-rolled scan
|
|
268
|
+
// that closed the insert block only on `id:` lines, so `- disable:` with
|
|
269
|
+
// nested ids under it claimed the neighbour's rows — #147 all over again
|
|
270
|
+
// on the path #147 did not touch.
|
|
271
|
+
try {
|
|
272
|
+
for (const id of parsePatchRows(readFileSync(join(packageDir, 'cordis.patch.yml'), 'utf8')).insertedIds) {
|
|
273
|
+
ids.add(id);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
catch { /* no conventional patch — nothing more to add */ }
|
|
277
|
+
const prefix = includePrefix(host);
|
|
278
|
+
for (const entry of host.loader.entries()) {
|
|
279
|
+
if (entry.options?.name !== packageName)
|
|
280
|
+
continue;
|
|
281
|
+
let id = entry.options.id ?? '';
|
|
282
|
+
if (id === '')
|
|
283
|
+
continue;
|
|
284
|
+
if (prefix !== '' && id.startsWith(prefix))
|
|
285
|
+
id = id.slice(prefix.length);
|
|
286
|
+
if (/^(?:mkt-|client-)/u.test(id))
|
|
287
|
+
continue;
|
|
288
|
+
ids.add(id);
|
|
289
|
+
}
|
|
290
|
+
return [...ids];
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Top-level patch rows that DISABLE another plugin: a row carrying both an
|
|
294
|
+
* `id` and `disabled: true`. Rows nested under an `insert:` block are separate
|
|
295
|
+
* array elements shaped `{ insert: [...] }`, so anything here is by definition
|
|
296
|
+
* a sibling row targeting a plugin this package does not own.
|
|
297
|
+
*/
|
|
298
|
+
function foreignDisableIds(rows) {
|
|
299
|
+
const ids = [];
|
|
300
|
+
for (const row of rows) {
|
|
301
|
+
if (row === null || typeof row !== 'object' || Array.isArray(row))
|
|
302
|
+
continue;
|
|
303
|
+
const record = row;
|
|
304
|
+
const id = record.id;
|
|
305
|
+
if (typeof id === 'string' && record.disabled === true && !ids.includes(id))
|
|
306
|
+
ids.push(id);
|
|
307
|
+
}
|
|
308
|
+
return ids;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* The ids of OTHER plugins a package's bundle patch DISABLES — top-level
|
|
312
|
+
* `- id: X` + `disabled: true` rows targeting plugins it does not own. This is
|
|
313
|
+
* the precise marker of a bundle whose toggle-off can brick the boot (#224):
|
|
314
|
+
* dsh-postgres-backends disables session-persistence-jsonl, so once the market
|
|
315
|
+
* also disables the postgres backends nothing provides sessionPersistence.
|
|
316
|
+
*
|
|
317
|
+
* A bundle that merely RECONFIGURES a neighbour is deliberately NOT counted:
|
|
318
|
+
* the e2e fixture-cross tweaks dshm-fixture-b's config, and #147 requires
|
|
319
|
+
* disabling it to leave that neighbour live — dropping such a bundle from the
|
|
320
|
+
* stack broke its re-enable. Config-only side effects stay on the normal #147
|
|
321
|
+
* path; only a foreign `disabled: true` triggers the bundle removal. Removing
|
|
322
|
+
* the bundle still neutralizes any config side effects it carries, since its
|
|
323
|
+
* whole patch stops applying.
|
|
324
|
+
*
|
|
325
|
+
* Reads both patch sources like rowIdsForPackage — the declared dsh.bundle.patch
|
|
326
|
+
* and the conventional root cordis.patch.yml — so either form is detected.
|
|
327
|
+
*/
|
|
328
|
+
export function carrierDisableIds(profileDirectory, packageName) {
|
|
329
|
+
const packageDir = join(profileDirectory, 'node_modules', packageName);
|
|
330
|
+
const disabled = new Set();
|
|
331
|
+
const collectFromFile = (patchPath) => {
|
|
332
|
+
const rows = parsePatchFile(patchPath);
|
|
333
|
+
if (rows === null)
|
|
334
|
+
return;
|
|
335
|
+
for (const id of foreignDisableIds(rows))
|
|
336
|
+
disabled.add(id);
|
|
337
|
+
};
|
|
338
|
+
try {
|
|
339
|
+
const manifest = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf8'));
|
|
340
|
+
const declared = manifest.dsh?.bundle?.patch;
|
|
341
|
+
if (typeof declared === 'string' && declared !== '')
|
|
342
|
+
collectFromFile(join(packageDir, declared));
|
|
343
|
+
}
|
|
344
|
+
catch { /* package not installed — nothing to attribute */ }
|
|
345
|
+
collectFromFile(join(packageDir, 'cordis.patch.yml'));
|
|
346
|
+
return [...disabled];
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Per-package patch-layer flags for the installed list: names whose rows the
|
|
350
|
+
* user patch layer disables / force-enables. These cover toggles made
|
|
351
|
+
* OUTSIDE the market (hand-edited cordis.patch.yml, dsh-web-plugin-manager,
|
|
352
|
+
* the dsh CLI), which the market's own state.json never sees.
|
|
353
|
+
*/
|
|
354
|
+
export function packagePatchFlags(host, profileDirectory, names, state) {
|
|
355
|
+
const disabled = [];
|
|
356
|
+
const forced = [];
|
|
357
|
+
for (const name of names) {
|
|
358
|
+
const rows = rowIdsForPackage(host, profileDirectory, name);
|
|
359
|
+
if (rows.some(id => state.disables.includes(id)))
|
|
360
|
+
disabled.push(name);
|
|
361
|
+
if (rows.some(id => state.forced.includes(id)))
|
|
362
|
+
forced.push(name);
|
|
363
|
+
}
|
|
364
|
+
return { disabled, forced };
|
|
365
|
+
}
|
|
366
|
+
/** Serialize patch-file writes: concurrent toggles must not interleave. */
|
|
367
|
+
let writeQueue = Promise.resolve();
|
|
368
|
+
function queuedWrite(fn) {
|
|
369
|
+
const run = writeQueue.then(fn, fn);
|
|
370
|
+
writeQueue = run.then(() => undefined, () => undefined);
|
|
371
|
+
return run;
|
|
372
|
+
}
|
|
373
|
+
function escapeRegExp(value) {
|
|
374
|
+
return value.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
|
|
375
|
+
}
|
|
376
|
+
function rowBlock(rowId, disabled) {
|
|
377
|
+
return `- id: ${rowId}\n disabled: ${disabled ? 'true' : 'false'}\n`;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Append one top-level patch entry, refusing when the file is not a valid
|
|
381
|
+
* entry list. The refusal is the point: a malformed patch layer (the
|
|
382
|
+
* `[]` + items mistake, or any broken YAML) must never be made worse —
|
|
383
|
+
* the market reports it and keeps its own live/state.json toggle instead.
|
|
384
|
+
*/
|
|
385
|
+
function appendPatchEntry(patchPath, block) {
|
|
386
|
+
let text = '';
|
|
387
|
+
try {
|
|
388
|
+
text = readFileSync(patchPath, 'utf8');
|
|
389
|
+
}
|
|
390
|
+
catch { /* created below */ }
|
|
391
|
+
const core = text.trim();
|
|
392
|
+
if (core === '') {
|
|
393
|
+
writeFileSync(patchPath, block);
|
|
394
|
+
return { ok: true, reason: null };
|
|
395
|
+
}
|
|
396
|
+
// The dsh profile template ships an empty `[]` placeholder (a valid
|
|
397
|
+
// empty list). Appending after it would create TWO top-level elements in
|
|
398
|
+
// one document — the exact YAML error a hand-edit runs into. Comment the
|
|
399
|
+
// placeholder out (keeping the template header) and append the entry.
|
|
400
|
+
const withoutComments = text.replace(/^[ \t]*#.*$/gmu, '').trim();
|
|
401
|
+
// Comments only (the user dropped the placeholder): append after them.
|
|
402
|
+
if (withoutComments === '') {
|
|
403
|
+
const next = text.endsWith('\n') ? text : `${text}\n`;
|
|
404
|
+
writeFileSync(patchPath, `${next}${block}`);
|
|
405
|
+
return { ok: true, reason: null };
|
|
406
|
+
}
|
|
407
|
+
if (withoutComments === '[]' || withoutComments === '[ ]') {
|
|
408
|
+
const commented = text.replace(/^[ \t]*\[[ \t]*\][ \t]*(?:#.*)?(?:\r?\n|$)/mu, '# []\n');
|
|
409
|
+
const next = commented.endsWith('\n') ? commented : `${commented}\n`;
|
|
410
|
+
writeFileSync(patchPath, `${next}${block}`);
|
|
411
|
+
return { ok: true, reason: null };
|
|
412
|
+
}
|
|
413
|
+
// A top-level flow LIST (other than the placeholder above) cannot be
|
|
414
|
+
// appended to: `- id: X` after `[1, 2]` would create two top-level
|
|
415
|
+
// elements in one document. Block items whose VALUE is flow style (e.g.
|
|
416
|
+
// `- config: [1, 2]`) are fine — their last line starts with `- `.
|
|
417
|
+
const lastContentLine = text.split(/\r?\n/u)
|
|
418
|
+
.map(line => line.trim())
|
|
419
|
+
.filter(line => line !== '' && !line.startsWith('#'))
|
|
420
|
+
.pop() ?? '';
|
|
421
|
+
if (/^[[{]/u.test(lastContentLine)) {
|
|
422
|
+
return {
|
|
423
|
+
ok: false,
|
|
424
|
+
reason: '补丁层以顶层流式结构结尾,不支持自动追加;请先整理为条目列表 / the patch layer ends in a top-level flow structure; refusing to append — tidy the file into an entry list first',
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
// The dsh entry-list dialect decides: anything that does not parse as a
|
|
428
|
+
// top-level array is left untouched (appending would deepen the break).
|
|
429
|
+
if (parsePatchFile(patchPath) === null) {
|
|
430
|
+
return {
|
|
431
|
+
ok: false,
|
|
432
|
+
reason: '补丁层不是合法的条目数组,已拒绝追加以免破坏;请先修正 YAML / the patch layer is not a valid entry list; refused to append — fix the YAML first',
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
const next = text.endsWith('\n') ? text : `${text}\n`;
|
|
436
|
+
writeFileSync(patchPath, `${next}${block}`);
|
|
437
|
+
return { ok: true, reason: null };
|
|
438
|
+
}
|
|
439
|
+
/** Disable one row: append `- id: X` + `disabled: true` (idempotent). */
|
|
440
|
+
export function disableRow(patchPath, rowId) {
|
|
441
|
+
return queuedWrite(async () => {
|
|
442
|
+
if (!ROW_ID_RE.test(rowId)) {
|
|
443
|
+
return { ok: false, reason: `行 id 含特殊字符,不支持写入补丁层 / row id ${rowId} cannot be written to the patch layer` };
|
|
444
|
+
}
|
|
445
|
+
const state = readUserPatchState(patchPath);
|
|
446
|
+
if (state.disables.includes(rowId))
|
|
447
|
+
return { ok: true, reason: null };
|
|
448
|
+
const result = appendPatchEntry(patchPath, rowBlock(rowId, true));
|
|
449
|
+
if (result.ok)
|
|
450
|
+
logEvent('info', 'patch', `disabled row ${rowId} in ${patchPath}`);
|
|
451
|
+
return result;
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
/** Enable one row: remove the `disabled: true` block; force-enable with
|
|
455
|
+
* `disabled: false` when a lower layer (bundle/home patch) holds it down. */
|
|
456
|
+
export function enableRow(patchPath, rowId) {
|
|
457
|
+
return queuedWrite(async () => {
|
|
458
|
+
if (!ROW_ID_RE.test(rowId)) {
|
|
459
|
+
return { ok: false, reason: `行 id 含特殊字符,不支持写入补丁层 / row id ${rowId} cannot be written to the patch layer` };
|
|
460
|
+
}
|
|
461
|
+
const state = readUserPatchState(patchPath);
|
|
462
|
+
const blockRe = new RegExp(`^- id: ['\"]?${escapeRegExp(rowId)}['\"]?\\r?\\n disabled: true\\r?\\n`, 'mu');
|
|
463
|
+
const text = (() => {
|
|
464
|
+
try {
|
|
465
|
+
return readFileSync(patchPath, 'utf8');
|
|
466
|
+
}
|
|
467
|
+
catch {
|
|
468
|
+
return '';
|
|
469
|
+
}
|
|
470
|
+
})();
|
|
471
|
+
if (blockRe.test(text)) {
|
|
472
|
+
writeFileSync(patchPath, withPlaceholderRestored(text.replace(blockRe, '')));
|
|
473
|
+
logEvent('info', 'patch', `enabled row ${rowId} in ${patchPath}`);
|
|
474
|
+
return { ok: true, reason: null };
|
|
475
|
+
}
|
|
476
|
+
if (state.forced.includes(rowId))
|
|
477
|
+
return { ok: true, reason: null };
|
|
478
|
+
const result = appendPatchEntry(patchPath, rowBlock(rowId, false));
|
|
479
|
+
if (result.ok)
|
|
480
|
+
logEvent('info', 'patch', `force-enabled row ${rowId} in ${patchPath}`);
|
|
481
|
+
return result;
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Put the empty-list placeholder back when nothing else is left.
|
|
486
|
+
*
|
|
487
|
+
* Appending the first row comments the template's `[]` out (see
|
|
488
|
+
* appendPatchEntry), so removing the LAST row leaves a file of pure
|
|
489
|
+
* comments. That is not a top-level array, and dsh refuses to boot the
|
|
490
|
+
* profile at all — "must be a top-level YAML array of loader patch
|
|
491
|
+
* entries". Disable a plugin, enable it again, and the profile is bricked.
|
|
492
|
+
*/
|
|
493
|
+
function withPlaceholderRestored(text) {
|
|
494
|
+
if (text.replace(/^[ \t]*#.*$/gmu, '').trim() !== '')
|
|
495
|
+
return text;
|
|
496
|
+
const uncommented = text.replace(/^[ \t]*#[ \t]*\[[ \t]*\][ \t]*(?:\r?\n|$)/mu, '[]\n');
|
|
497
|
+
if (uncommented !== text)
|
|
498
|
+
return uncommented;
|
|
499
|
+
// No commented placeholder to revive (hand-written file): add one.
|
|
500
|
+
return text === '' || text.endsWith('\n') ? `${text}[]\n` : `${text}\n[]\n`;
|
|
501
|
+
}
|
|
502
|
+
/** Remove every disable/force block the market (or the user) wrote for a
|
|
503
|
+
* row — the uninstall cleanup, so a removed plugin leaves no orphan rows. */
|
|
504
|
+
export function removeRowBlocks(patchPath, rowIds) {
|
|
505
|
+
let text = '';
|
|
506
|
+
try {
|
|
507
|
+
text = readFileSync(patchPath, 'utf8');
|
|
508
|
+
}
|
|
509
|
+
catch {
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
let next = text;
|
|
513
|
+
for (const rowId of rowIds) {
|
|
514
|
+
const blockRe = new RegExp(`^- id: ['\"]?${escapeRegExp(rowId)}['\"]?\\r?\\n disabled: (?:true|false)\\r?\\n`, 'mu');
|
|
515
|
+
next = next.replace(blockRe, '');
|
|
516
|
+
}
|
|
517
|
+
if (next !== text) {
|
|
518
|
+
writeFileSync(patchPath, withPlaceholderRestored(next));
|
|
519
|
+
logEvent('info', 'patch', `removed patch rows for ${rowIds.join(', ')}`);
|
|
520
|
+
}
|
|
521
|
+
}
|