@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/presets.js
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Named plugin presets — issue #98 (phase 3), the "save different plugin
|
|
3
|
+
* combinations" product shape. A preset captures the community-bundle order
|
|
4
|
+
* and the disabled-plugin list of the profile at save time; applying one
|
|
5
|
+
* replays the composition under the candidate order (trialValidate), refuses
|
|
6
|
+
* on failure, auto-snapshots the profile (createProfileSnapshot), and only
|
|
7
|
+
* then writes the bundle order and disable list.
|
|
8
|
+
*
|
|
9
|
+
* Presets persist in `<profile>/.dsh-market/presets.json` (market-owned
|
|
10
|
+
* state, like snapshots) — deliberately separate from state.json, whose
|
|
11
|
+
* shape routes.ts owns.
|
|
12
|
+
*/
|
|
13
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { readMarketState, writeMarketState } from './hot.js';
|
|
16
|
+
import { applyBundleOrder, mergeOrder, readBundleRules, readBundleStack, validateOrder } from './order.js';
|
|
17
|
+
import { createProfileSnapshot, DEFAULT_MAX_SNAPSHOTS } from './snapshot.js';
|
|
18
|
+
import { trialValidate } from './trial.js';
|
|
19
|
+
import { logEvent } from './log.js';
|
|
20
|
+
/** Group-style name rule: letters/digits (incl. CJK), spaces, _, -; ≤ 40 chars, at least one non-space. */
|
|
21
|
+
const PRESET_NAME_RE = /^[\p{L}\p{N}_ -]{1,40}$/u;
|
|
22
|
+
/**
|
|
23
|
+
* The market's own package names. The toggle route refuses to disable them;
|
|
24
|
+
* a preset must never carry them in its disabled list either — otherwise
|
|
25
|
+
* applying a preset (or importing one) could disable the very page doing the
|
|
26
|
+
* applying (issue #98 analysis: applyPreset self-disable guard). They are
|
|
27
|
+
* filtered at save/import time and again at apply time (defense in depth).
|
|
28
|
+
*/
|
|
29
|
+
const MARKET_SELF_NAMES = new Set(['@chatcode/cco-market', 'dsh-market', 'dshmarket']);
|
|
30
|
+
/** Maximum presets stored per profile (quota — issue #98 analysis). */
|
|
31
|
+
export const MAX_PRESETS = 50;
|
|
32
|
+
/**
|
|
33
|
+
* Atomic same-directory replace (write temp + rename): a crash mid-write can
|
|
34
|
+
* never leave presets.json truncated, which would silently drop every saved
|
|
35
|
+
* preset on the next read.
|
|
36
|
+
*/
|
|
37
|
+
function writeFileAtomic(file, content) {
|
|
38
|
+
const temp = `${file}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
39
|
+
writeFileSync(temp, content);
|
|
40
|
+
renameSync(temp, file);
|
|
41
|
+
}
|
|
42
|
+
/** Drop the market's own names from a raw disabled list (strings only). */
|
|
43
|
+
function sanitizeDisabled(disabled) {
|
|
44
|
+
if (!Array.isArray(disabled))
|
|
45
|
+
return [];
|
|
46
|
+
const seen = new Set();
|
|
47
|
+
const out = [];
|
|
48
|
+
for (const item of disabled) {
|
|
49
|
+
if (typeof item !== 'string' || item === '')
|
|
50
|
+
continue;
|
|
51
|
+
if (MARKET_SELF_NAMES.has(item))
|
|
52
|
+
continue;
|
|
53
|
+
if (seen.has(item))
|
|
54
|
+
continue;
|
|
55
|
+
seen.add(item);
|
|
56
|
+
out.push(item);
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
function presetsFile(profileDir) {
|
|
61
|
+
return join(profileDir, '.dsh-market', 'presets.json');
|
|
62
|
+
}
|
|
63
|
+
function readPresets(profileDir) {
|
|
64
|
+
try {
|
|
65
|
+
const value = JSON.parse(readFileSync(presetsFile(profileDir), 'utf8'));
|
|
66
|
+
if (!Array.isArray(value.presets))
|
|
67
|
+
return [];
|
|
68
|
+
return value.presets.filter((preset) => preset !== null && typeof preset === 'object'
|
|
69
|
+
&& typeof preset.name === 'string'
|
|
70
|
+
&& Array.isArray(preset.bundleOrder)
|
|
71
|
+
&& Array.isArray(preset.disabled));
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function writePresets(profileDir, presets) {
|
|
78
|
+
mkdirSync(join(profileDir, '.dsh-market'), { recursive: true, mode: 0o700 });
|
|
79
|
+
writeFileAtomic(presetsFile(profileDir), `${JSON.stringify({ presets }, null, 2)}\n`);
|
|
80
|
+
}
|
|
81
|
+
/** All saved presets, newest first. */
|
|
82
|
+
export function listPresets(profileDir) {
|
|
83
|
+
return readPresets(profileDir).sort((a, b) => b.createdAt - a.createdAt);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Save the current composition state as a named preset. The bundle order is
|
|
87
|
+
* validated against the current community bundles so a stale snapshot can
|
|
88
|
+
* never be stored.
|
|
89
|
+
*/
|
|
90
|
+
export function savePreset(profileDir, name, bundleOrder, disabled) {
|
|
91
|
+
if (typeof name !== 'string' || !PRESET_NAME_RE.test(name) || name.trim() === '') {
|
|
92
|
+
return { ok: false, error: 'invalid preset name / 组合名称无效' };
|
|
93
|
+
}
|
|
94
|
+
if (!Array.isArray(bundleOrder) || !bundleOrder.every(item => typeof item === 'string')) {
|
|
95
|
+
return { ok: false, error: 'bundle order must be an array of names / bundle 顺序必须是名称数组' };
|
|
96
|
+
}
|
|
97
|
+
// The stored order must be a permutation of the CURRENT community bundles:
|
|
98
|
+
// a stale snapshot is refused here instead of failing later at apply time
|
|
99
|
+
// (issue #98 review M3 — the comment now matches the implementation).
|
|
100
|
+
const { community } = readBundleStack(profileDir);
|
|
101
|
+
const order = bundleOrder;
|
|
102
|
+
if (new Set(order).size !== order.length || order.length !== community.length || order.some(name => !community.includes(name))) {
|
|
103
|
+
return { ok: false, error: 'bundle order must be a permutation of the current community bundles / bundle 顺序必须是当前社区 bundle 的排列' };
|
|
104
|
+
}
|
|
105
|
+
const normalizedDisabled = sanitizeDisabled(disabled);
|
|
106
|
+
const presets = readPresets(profileDir);
|
|
107
|
+
if (presets.some(preset => preset.name === name)) {
|
|
108
|
+
return { ok: false, error: 'a preset with this name already exists / 同名组合已存在' };
|
|
109
|
+
}
|
|
110
|
+
// Quota (issue #98 analysis): a bounded store keeps the file small and the
|
|
111
|
+
// list usable; refuse instead of silently trimming.
|
|
112
|
+
if (presets.length >= MAX_PRESETS) {
|
|
113
|
+
return { ok: false, error: `preset quota reached (${MAX_PRESETS}) — delete one first / 组合数量已达上限(${MAX_PRESETS}),请先删除一个` };
|
|
114
|
+
}
|
|
115
|
+
presets.push({
|
|
116
|
+
name,
|
|
117
|
+
bundleOrder: order,
|
|
118
|
+
disabled: normalizedDisabled,
|
|
119
|
+
createdAt: Date.now(),
|
|
120
|
+
});
|
|
121
|
+
writePresets(profileDir, presets);
|
|
122
|
+
logEvent('info', 'preset', `saved "${name}" (${order.length} bundles, ${normalizedDisabled.length} disabled)`);
|
|
123
|
+
return { ok: true };
|
|
124
|
+
}
|
|
125
|
+
/** Delete a named preset. */
|
|
126
|
+
export function deletePreset(profileDir, name) {
|
|
127
|
+
if (typeof name !== 'string')
|
|
128
|
+
return { ok: false, error: 'invalid preset name / 组合名称无效' };
|
|
129
|
+
const presets = readPresets(profileDir);
|
|
130
|
+
const next = presets.filter(preset => preset.name !== name);
|
|
131
|
+
if (next.length === presets.length)
|
|
132
|
+
return { ok: false, error: 'preset not found / 组合不存在' };
|
|
133
|
+
writePresets(profileDir, next);
|
|
134
|
+
logEvent('info', 'preset', `deleted "${name}"`);
|
|
135
|
+
return { ok: true };
|
|
136
|
+
}
|
|
137
|
+
/** Compare a preset's order against the current community bundle set. */
|
|
138
|
+
function presetMismatch(profileDir, bundleOrder) {
|
|
139
|
+
const { community } = readBundleStack(profileDir);
|
|
140
|
+
const current = new Set(community);
|
|
141
|
+
const preset = new Set(bundleOrder);
|
|
142
|
+
const missing = community.filter(name => !preset.has(name));
|
|
143
|
+
const extra = bundleOrder.filter(name => !current.has(name));
|
|
144
|
+
return { missing, extra, stale: missing.length > 0 || extra.length > 0 };
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Preview what applying a preset would change, WITHOUT writing anything.
|
|
148
|
+
* A stale preset (bundle set mismatch) is NOT a hard failure: the preview
|
|
149
|
+
* reports the mismatch alongside the still-computable changes (relative
|
|
150
|
+
* order + enabled/disabled diffs over the intersection).
|
|
151
|
+
*/
|
|
152
|
+
export function previewPreset(profileDir, name) {
|
|
153
|
+
if (typeof name !== 'string')
|
|
154
|
+
return { ok: false, error: 'invalid preset name / 组合名称无效' };
|
|
155
|
+
const preset = readPresets(profileDir).find(item => item.name === name);
|
|
156
|
+
if (preset === undefined)
|
|
157
|
+
return { ok: false, error: 'preset not found / 组合不存在' };
|
|
158
|
+
// Full composition replay only makes sense when the bundle SET matches;
|
|
159
|
+
// otherwise report the mismatch (and keep the apply path's hard refusal).
|
|
160
|
+
const mismatch = presetMismatch(profileDir, preset.bundleOrder);
|
|
161
|
+
if (mismatch.stale) {
|
|
162
|
+
const detail = [...mismatch.missing.map(n => `+${n}`), ...mismatch.extra.map(n => `-${n}`)].join(' ');
|
|
163
|
+
return {
|
|
164
|
+
ok: false,
|
|
165
|
+
mismatch,
|
|
166
|
+
error: `preset is out of date — current profile differs: ${detail} / 组合已过期——当前 profile 的插件列表已变化:${detail}`,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const trial = trialValidate(profileDir, preset.bundleOrder);
|
|
170
|
+
if (!trial.ok) {
|
|
171
|
+
const first = trial.errors[0];
|
|
172
|
+
return {
|
|
173
|
+
ok: false,
|
|
174
|
+
error: `trial validation failed — ${first?.message ?? 'composition would not boot'} / 试启动校验失败:${first?.message ?? '组合无法启动'}`,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
const { community } = readBundleStack(profileDir);
|
|
178
|
+
const reordered = community.filter((name, index) => name !== preset.bundleOrder[index]);
|
|
179
|
+
const currentDisabled = readMarketState(profileDir).disabled;
|
|
180
|
+
// The market's own names are never applied (self-disable guard) — exclude
|
|
181
|
+
// them from the diff too, so the preview matches what apply will actually do.
|
|
182
|
+
const presetDisabled = new Set(preset.disabled.filter(name => !MARKET_SELF_NAMES.has(name)));
|
|
183
|
+
const enabled = [...currentDisabled].filter(name => !presetDisabled.has(name));
|
|
184
|
+
const disabled = [...presetDisabled].filter(name => !currentDisabled.has(name));
|
|
185
|
+
return {
|
|
186
|
+
ok: true,
|
|
187
|
+
changes: {
|
|
188
|
+
reordered,
|
|
189
|
+
enabled,
|
|
190
|
+
disabled,
|
|
191
|
+
noop: reordered.length === 0 && enabled.length === 0 && disabled.length === 0,
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Apply a saved preset: trial-validate the candidate order first (refuse
|
|
197
|
+
* without writing on any boot-breaking issue), auto-snapshot the profile,
|
|
198
|
+
* then write the bundle order and the disable list. The response carries the
|
|
199
|
+
* change preview so the UI can report exactly what moved.
|
|
200
|
+
*/
|
|
201
|
+
export function applyPreset(profileDir, name, maxSnapshots = DEFAULT_MAX_SNAPSHOTS) {
|
|
202
|
+
if (typeof name !== 'string')
|
|
203
|
+
return { ok: false, error: 'invalid preset name / 组合名称无效' };
|
|
204
|
+
const preset = readPresets(profileDir).find(item => item.name === name);
|
|
205
|
+
if (preset === undefined)
|
|
206
|
+
return { ok: false, error: 'preset not found / 组合不存在' };
|
|
207
|
+
// A stale preset (bundle set differs) cannot be applied as-is; say exactly
|
|
208
|
+
// what differs instead of the raw trial-merge error (issue #98 report).
|
|
209
|
+
const mismatch = presetMismatch(profileDir, preset.bundleOrder);
|
|
210
|
+
if (mismatch.stale) {
|
|
211
|
+
const detail = [...mismatch.missing.map(n => `+${n}`), ...mismatch.extra.map(n => `-${n}`)].join(' ');
|
|
212
|
+
logEvent('warn', 'preset', `apply "${name}" rejected: preset out of date — ${detail}`);
|
|
213
|
+
return {
|
|
214
|
+
ok: false,
|
|
215
|
+
error: `preset is out of date — current profile differs: ${detail} / 组合已过期——当前 profile 的插件列表已变化:${detail}`,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
const trial = trialValidate(profileDir, preset.bundleOrder);
|
|
219
|
+
if (!trial.ok) {
|
|
220
|
+
const first = trial.errors[0];
|
|
221
|
+
logEvent('warn', 'preset', `apply "${name}" rejected by trial validation: ${first?.message ?? 'unknown'}`);
|
|
222
|
+
return {
|
|
223
|
+
ok: false,
|
|
224
|
+
error: `trial validation failed — ${first?.message ?? 'composition would not boot'} / 试启动校验失败:${first?.message ?? '组合无法启动'}`,
|
|
225
|
+
trial: { errors: trial.errors, warnings: trial.warnings, diff: trial.diff },
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
// Before/after rules (review B5): the reorder endpoint refuses rule-violating
|
|
229
|
+
// stacks; the preset path must enforce the same gate before writing.
|
|
230
|
+
const { bundles } = readBundleStack(profileDir);
|
|
231
|
+
const merged = mergeOrder(bundles, preset.bundleOrder);
|
|
232
|
+
if (merged.ok) {
|
|
233
|
+
const conflicts = validateOrder(merged.bundles, readBundleRules(profileDir));
|
|
234
|
+
if (conflicts.length > 0) {
|
|
235
|
+
logEvent('warn', 'preset', `apply "${name}" rejected by before/after rules: ${conflicts.map(c => c.reason).join('; ')}`);
|
|
236
|
+
return {
|
|
237
|
+
ok: false,
|
|
238
|
+
error: 'the preset order violates declared before/after rules / 组合顺序违反了插件声明的 before/after 规则',
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const preview = previewPreset(profileDir, name);
|
|
243
|
+
const captured = createProfileSnapshot(profileDir, maxSnapshots);
|
|
244
|
+
if (!captured.ok) {
|
|
245
|
+
logEvent('error', 'preset', `apply "${name}" refused: ${captured.error}`);
|
|
246
|
+
return { ok: false, error: captured.error };
|
|
247
|
+
}
|
|
248
|
+
const snapshot = captured.snapshot;
|
|
249
|
+
const ordered = applyBundleOrder(profileDir, preset.bundleOrder);
|
|
250
|
+
if (!ordered.ok) {
|
|
251
|
+
return { ok: false, error: ordered.error };
|
|
252
|
+
}
|
|
253
|
+
const state = readMarketState(profileDir);
|
|
254
|
+
// Self-disable guard (issue #98 analysis): a preset (possibly imported)
|
|
255
|
+
// carrying the market's own name must never switch this page off — drop
|
|
256
|
+
// those names from the applied disabled list.
|
|
257
|
+
const filtered = preset.disabled.filter(name => !MARKET_SELF_NAMES.has(name));
|
|
258
|
+
if (filtered.length !== preset.disabled.length) {
|
|
259
|
+
logEvent('warn', 'preset', `apply "${name}": dropped market self-names from the disabled list`);
|
|
260
|
+
}
|
|
261
|
+
state.disabled = new Set(filtered);
|
|
262
|
+
writeMarketState(profileDir, state);
|
|
263
|
+
logEvent('info', 'preset', `applied "${name}" (snapshot ${snapshot.id})`);
|
|
264
|
+
return { ok: true, snapshot: snapshot.id, changes: preview.ok ? preview.changes : undefined };
|
|
265
|
+
}
|
|
266
|
+
/*
|
|
267
|
+
* Presets deliberately have NO import/export of their own.
|
|
268
|
+
*
|
|
269
|
+
* They had one on the branch this came from, and it was fine code — but
|
|
270
|
+
* the market already answers "take this configuration elsewhere" under
|
|
271
|
+
* Advanced -> Backup & Restore, which carries the whole profile rather
|
|
272
|
+
* than one slice of it. Two export buttons in two tabs, writing two file
|
|
273
|
+
* formats that overlap, is a question the user has to stop and answer
|
|
274
|
+
* before they can do the thing they came to do.
|
|
275
|
+
*
|
|
276
|
+
* Presets stay the local answer to "switch between combinations I have
|
|
277
|
+
* saved". Moving them to another machine is the backup's job.
|
|
278
|
+
*/
|