@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.
Files changed (177) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +146 -0
  3. package/README.zh.md +143 -0
  4. package/UPDATE-API-V1.md +132 -0
  5. package/client/client.js +11534 -0
  6. package/cordis.patch.yml +5 -0
  7. package/lib/accelerate.js +184 -0
  8. package/lib/agents.js +36 -0
  9. package/lib/backup.js +572 -0
  10. package/lib/catalog-local-match.js +126 -0
  11. package/lib/catalog-npm.js +111 -0
  12. package/lib/changelog.js +242 -0
  13. package/lib/channels.js +64 -0
  14. package/lib/check.js +1067 -0
  15. package/lib/compatibility.js +198 -0
  16. package/lib/diagnostics.js +49 -0
  17. package/lib/discovery-compatibility.js +260 -0
  18. package/lib/dsh-cli.js +949 -0
  19. package/lib/dsh-install.js +108 -0
  20. package/lib/gist.js +352 -0
  21. package/lib/groups.js +97 -0
  22. package/lib/home-paths.js +42 -0
  23. package/lib/hot.js +519 -0
  24. package/lib/http.js +39 -0
  25. package/lib/index.js +97 -0
  26. package/lib/install.js +387 -0
  27. package/lib/log.js +197 -0
  28. package/lib/ndjson.js +154 -0
  29. package/lib/net.js +98 -0
  30. package/lib/order.js +284 -0
  31. package/lib/patch.js +521 -0
  32. package/lib/pnpm-compat.js +490 -0
  33. package/lib/presets.js +278 -0
  34. package/lib/profile.js +911 -0
  35. package/lib/region-probe.js +93 -0
  36. package/lib/regions.js +253 -0
  37. package/lib/registry.js +204 -0
  38. package/lib/restart.js +373 -0
  39. package/lib/routes.js +4372 -0
  40. package/lib/settings.js +109 -0
  41. package/lib/skill-market/api.js +119 -0
  42. package/lib/skill-market/config.js +29 -0
  43. package/lib/skill-market/installer.js +352 -0
  44. package/lib/skill-market/runtime.js +13 -0
  45. package/lib/skill-market/tui.js +119 -0
  46. package/lib/skill-market/types.js +1 -0
  47. package/lib/skill-market/web-routes.js +123 -0
  48. package/lib/snapshot.js +500 -0
  49. package/lib/source-migration.js +54 -0
  50. package/lib/sources.js +596 -0
  51. package/lib/store.js +91 -0
  52. package/lib/themes.js +102 -0
  53. package/lib/trial.js +116 -0
  54. package/lib/types/accelerate.d.ts +85 -0
  55. package/lib/types/agents.d.ts +22 -0
  56. package/lib/types/backup.d.ts +131 -0
  57. package/lib/types/catalog-local-match.d.ts +50 -0
  58. package/lib/types/catalog-npm.d.ts +47 -0
  59. package/lib/types/changelog.d.ts +102 -0
  60. package/lib/types/channels.d.ts +56 -0
  61. package/lib/types/check.d.ts +266 -0
  62. package/lib/types/compatibility.d.ts +64 -0
  63. package/lib/types/diagnostics.d.ts +29 -0
  64. package/lib/types/discovery-compatibility.d.ts +80 -0
  65. package/lib/types/dsh-cli.d.ts +324 -0
  66. package/lib/types/dsh-install.d.ts +46 -0
  67. package/lib/types/gist.d.ts +53 -0
  68. package/lib/types/groups.d.ts +34 -0
  69. package/lib/types/home-paths.d.ts +16 -0
  70. package/lib/types/hot.d.ts +221 -0
  71. package/lib/types/http.d.ts +12 -0
  72. package/lib/types/index.d.ts +14 -0
  73. package/lib/types/install.d.ts +172 -0
  74. package/lib/types/log.d.ts +43 -0
  75. package/lib/types/ndjson.d.ts +52 -0
  76. package/lib/types/net.d.ts +58 -0
  77. package/lib/types/order.d.ts +100 -0
  78. package/lib/types/patch.d.ts +129 -0
  79. package/lib/types/pnpm-compat.d.ts +93 -0
  80. package/lib/types/presets.d.ts +90 -0
  81. package/lib/types/profile.d.ts +253 -0
  82. package/lib/types/region-probe.d.ts +50 -0
  83. package/lib/types/regions.d.ts +122 -0
  84. package/lib/types/registry.d.ts +86 -0
  85. package/lib/types/restart.d.ts +194 -0
  86. package/lib/types/routes.d.ts +67 -0
  87. package/lib/types/settings.d.ts +78 -0
  88. package/lib/types/skill-market/api.d.ts +10 -0
  89. package/lib/types/skill-market/config.d.ts +3 -0
  90. package/lib/types/skill-market/installer.d.ts +23 -0
  91. package/lib/types/skill-market/runtime.d.ts +10 -0
  92. package/lib/types/skill-market/tui.d.ts +4 -0
  93. package/lib/types/skill-market/types.d.ts +71 -0
  94. package/lib/types/skill-market/web-routes.d.ts +19 -0
  95. package/lib/types/snapshot.d.ts +93 -0
  96. package/lib/types/source-migration.d.ts +11 -0
  97. package/lib/types/sources.d.ts +216 -0
  98. package/lib/types/store.d.ts +30 -0
  99. package/lib/types/themes.d.ts +40 -0
  100. package/lib/types/trial.d.ts +61 -0
  101. package/lib/types/update-api-v1.d.ts +66 -0
  102. package/lib/types/updates.d.ts +123 -0
  103. package/lib/types/verify.d.ts +139 -0
  104. package/lib/update-api-v1.js +215 -0
  105. package/lib/updates.js +361 -0
  106. package/lib/verify.js +453 -0
  107. package/package.json +121 -0
  108. package/src/accelerate.ts +213 -0
  109. package/src/agents.ts +43 -0
  110. package/src/backup.ts +583 -0
  111. package/src/catalog-local-match.ts +144 -0
  112. package/src/catalog-npm.ts +120 -0
  113. package/src/changelog.ts +282 -0
  114. package/src/channels.ts +70 -0
  115. package/src/check.ts +1239 -0
  116. package/src/client/CommentsModal.tsx +119 -0
  117. package/src/client/Diagnostics.tsx +907 -0
  118. package/src/client/ErrorBoundary.tsx +111 -0
  119. package/src/client/InstallToast.tsx +31 -0
  120. package/src/client/Market.module.css +830 -0
  121. package/src/client/MarketSection.tsx +5301 -0
  122. package/src/client/OperationsPanel.tsx +365 -0
  123. package/src/client/SettingsCard.tsx +621 -0
  124. package/src/client/SkillMarket.module.css +21 -0
  125. package/src/client/SkillMarketSection.tsx +162 -0
  126. package/src/client/comments.ts +54 -0
  127. package/src/client/globals.d.ts +13 -0
  128. package/src/client/index.ts +180 -0
  129. package/src/client/locales.ts +1112 -0
  130. package/src/client/market-data.ts +1321 -0
  131. package/src/client/operations.ts +201 -0
  132. package/src/client/preset-panel.tsx +263 -0
  133. package/src/client/primitives.d.ts +140 -0
  134. package/src/client/self-check.ts +147 -0
  135. package/src/client/snapshot-panel.tsx +244 -0
  136. package/src/compatibility.ts +237 -0
  137. package/src/diagnostics.ts +84 -0
  138. package/src/discovery-compatibility.ts +315 -0
  139. package/src/dsh-cli.ts +1126 -0
  140. package/src/dsh-install.ts +118 -0
  141. package/src/gist.ts +362 -0
  142. package/src/groups.ts +111 -0
  143. package/src/home-paths.ts +53 -0
  144. package/src/hot.ts +628 -0
  145. package/src/http.ts +41 -0
  146. package/src/index.ts +128 -0
  147. package/src/install.ts +420 -0
  148. package/src/log.ts +206 -0
  149. package/src/ndjson.ts +185 -0
  150. package/src/net.ts +106 -0
  151. package/src/order.ts +303 -0
  152. package/src/patch.ts +522 -0
  153. package/src/pnpm-compat.ts +527 -0
  154. package/src/presets.ts +344 -0
  155. package/src/profile.ts +940 -0
  156. package/src/region-probe.ts +97 -0
  157. package/src/regions.ts +310 -0
  158. package/src/registry.ts +250 -0
  159. package/src/restart.ts +396 -0
  160. package/src/routes.ts +4506 -0
  161. package/src/settings.ts +141 -0
  162. package/src/skill-market/api.ts +130 -0
  163. package/src/skill-market/config.ts +32 -0
  164. package/src/skill-market/installer.ts +337 -0
  165. package/src/skill-market/runtime.ts +14 -0
  166. package/src/skill-market/tui.ts +157 -0
  167. package/src/skill-market/types.ts +80 -0
  168. package/src/skill-market/web-routes.ts +132 -0
  169. package/src/snapshot.ts +532 -0
  170. package/src/source-migration.ts +61 -0
  171. package/src/sources.ts +565 -0
  172. package/src/store.ts +89 -0
  173. package/src/themes.ts +125 -0
  174. package/src/trial.ts +156 -0
  175. package/src/update-api-v1.ts +277 -0
  176. package/src/updates.ts +400 -0
  177. package/src/verify.ts +492 -0
package/src/presets.ts ADDED
@@ -0,0 +1,344 @@
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
+
14
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'
15
+ import { join } from 'node:path'
16
+ import { readMarketState, writeMarketState } from './hot.ts'
17
+ import { applyBundleOrder, mergeOrder, readBundleRules, readBundleStack, validateOrder } from './order.ts'
18
+ import { createProfileSnapshot, DEFAULT_MAX_SNAPSHOTS } from './snapshot.ts'
19
+ import { trialValidate, type TrialDiff, type TrialIssue } from './trial.ts'
20
+ import { logEvent } from './log.ts'
21
+
22
+ /** Group-style name rule: letters/digits (incl. CJK), spaces, _, -; ≤ 40 chars, at least one non-space. */
23
+ const PRESET_NAME_RE = /^[\p{L}\p{N}_ -]{1,40}$/u
24
+
25
+ /**
26
+ * The market's own package names. The toggle route refuses to disable them;
27
+ * a preset must never carry them in its disabled list either — otherwise
28
+ * applying a preset (or importing one) could disable the very page doing the
29
+ * applying (issue #98 analysis: applyPreset self-disable guard). They are
30
+ * filtered at save/import time and again at apply time (defense in depth).
31
+ */
32
+ const MARKET_SELF_NAMES = new Set(['@chatcode/cco-market', 'dsh-market', 'dshmarket'])
33
+
34
+ /** Maximum presets stored per profile (quota — issue #98 analysis). */
35
+ export const MAX_PRESETS = 50
36
+
37
+ /**
38
+ * Atomic same-directory replace (write temp + rename): a crash mid-write can
39
+ * never leave presets.json truncated, which would silently drop every saved
40
+ * preset on the next read.
41
+ */
42
+ function writeFileAtomic(file: string, content: string): void {
43
+ const temp = `${file}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
44
+ writeFileSync(temp, content)
45
+ renameSync(temp, file)
46
+ }
47
+
48
+ /** Drop the market's own names from a raw disabled list (strings only). */
49
+ function sanitizeDisabled(disabled: unknown): string[] {
50
+ if (!Array.isArray(disabled)) return []
51
+ const seen = new Set<string>()
52
+ const out: string[] = []
53
+ for (const item of disabled) {
54
+ if (typeof item !== 'string' || item === '') continue
55
+ if (MARKET_SELF_NAMES.has(item)) continue
56
+ if (seen.has(item)) continue
57
+ seen.add(item)
58
+ out.push(item)
59
+ }
60
+ return out
61
+ }
62
+
63
+ export interface Preset {
64
+ name: string
65
+ /** Community-bundle order this preset restores. */
66
+ bundleOrder: string[]
67
+ /** Disabled plugin names this preset restores. */
68
+ disabled: string[]
69
+ createdAt: number
70
+ }
71
+
72
+ export interface PresetResult {
73
+ ok: boolean
74
+ error?: string
75
+ /** Set when applyPreset auto-created a pre-change snapshot. */
76
+ snapshot?: string
77
+ }
78
+
79
+ function presetsFile(profileDir: string): string {
80
+ return join(profileDir, '.dsh-market', 'presets.json')
81
+ }
82
+
83
+ function readPresets(profileDir: string): Preset[] {
84
+ try {
85
+ const value = JSON.parse(readFileSync(presetsFile(profileDir), 'utf8')) as { presets?: unknown }
86
+ if (!Array.isArray(value.presets)) return []
87
+ return value.presets.filter((preset): preset is Preset =>
88
+ preset !== null && typeof preset === 'object'
89
+ && typeof (preset as Preset).name === 'string'
90
+ && Array.isArray((preset as Preset).bundleOrder)
91
+ && Array.isArray((preset as Preset).disabled),
92
+ )
93
+ } catch {
94
+ return []
95
+ }
96
+ }
97
+
98
+ function writePresets(profileDir: string, presets: Preset[]): void {
99
+ mkdirSync(join(profileDir, '.dsh-market'), { recursive: true, mode: 0o700 })
100
+ writeFileAtomic(presetsFile(profileDir), `${JSON.stringify({ presets }, null, 2)}\n`)
101
+ }
102
+
103
+ /** All saved presets, newest first. */
104
+ export function listPresets(profileDir: string): Preset[] {
105
+ return readPresets(profileDir).sort((a, b) => b.createdAt - a.createdAt)
106
+ }
107
+
108
+ /**
109
+ * Save the current composition state as a named preset. The bundle order is
110
+ * validated against the current community bundles so a stale snapshot can
111
+ * never be stored.
112
+ */
113
+ export function savePreset(
114
+ profileDir: string,
115
+ name: unknown,
116
+ bundleOrder: unknown,
117
+ disabled: unknown,
118
+ ): PresetResult {
119
+ if (typeof name !== 'string' || !PRESET_NAME_RE.test(name) || name.trim() === '') {
120
+ return { ok: false, error: 'invalid preset name / 组合名称无效' }
121
+ }
122
+ if (!Array.isArray(bundleOrder) || !bundleOrder.every(item => typeof item === 'string')) {
123
+ return { ok: false, error: 'bundle order must be an array of names / bundle 顺序必须是名称数组' }
124
+ }
125
+ // The stored order must be a permutation of the CURRENT community bundles:
126
+ // a stale snapshot is refused here instead of failing later at apply time
127
+ // (issue #98 review M3 — the comment now matches the implementation).
128
+ const { community } = readBundleStack(profileDir)
129
+ const order = bundleOrder as string[]
130
+ if (new Set(order).size !== order.length || order.length !== community.length || order.some(name => !community.includes(name))) {
131
+ return { ok: false, error: 'bundle order must be a permutation of the current community bundles / bundle 顺序必须是当前社区 bundle 的排列' }
132
+ }
133
+ const normalizedDisabled = sanitizeDisabled(disabled)
134
+ const presets = readPresets(profileDir)
135
+ if (presets.some(preset => preset.name === name)) {
136
+ return { ok: false, error: 'a preset with this name already exists / 同名组合已存在' }
137
+ }
138
+ // Quota (issue #98 analysis): a bounded store keeps the file small and the
139
+ // list usable; refuse instead of silently trimming.
140
+ if (presets.length >= MAX_PRESETS) {
141
+ return { ok: false, error: `preset quota reached (${MAX_PRESETS}) — delete one first / 组合数量已达上限(${MAX_PRESETS}),请先删除一个` }
142
+ }
143
+ presets.push({
144
+ name,
145
+ bundleOrder: order,
146
+ disabled: normalizedDisabled,
147
+ createdAt: Date.now(),
148
+ })
149
+ writePresets(profileDir, presets)
150
+ logEvent('info', 'preset', `saved "${name}" (${order.length} bundles, ${normalizedDisabled.length} disabled)`)
151
+ return { ok: true }
152
+ }
153
+
154
+ /** Delete a named preset. */
155
+ export function deletePreset(profileDir: string, name: unknown): PresetResult {
156
+ if (typeof name !== 'string') return { ok: false, error: 'invalid preset name / 组合名称无效' }
157
+ const presets = readPresets(profileDir)
158
+ const next = presets.filter(preset => preset.name !== name)
159
+ if (next.length === presets.length) return { ok: false, error: 'preset not found / 组合不存在' }
160
+ writePresets(profileDir, next)
161
+ logEvent('info', 'preset', `deleted "${name}"`)
162
+ return { ok: true }
163
+ }
164
+
165
+ /** The concrete change a preset apply would make — computed BEFORE writing. */
166
+ export interface PresetChange {
167
+ /** Bundles whose position changes under the preset order. */
168
+ reordered: string[]
169
+ /** Plugins the preset would ENABLE (currently disabled, enabled by the preset). */
170
+ enabled: string[]
171
+ /** Plugins the preset would DISABLE (currently enabled, disabled by the preset). */
172
+ disabled: string[]
173
+ /** True when nothing would change. */
174
+ noop: boolean
175
+ }
176
+
177
+ export interface PresetApplyResult extends PresetResult {
178
+ changes?: PresetChange
179
+ /** Set when the preset order fails trial validation — errors + current-vs-candidate diff (issue #125 review). */
180
+ trial?: { errors: TrialIssue[]; warnings: TrialIssue[]; diff: TrialDiff }
181
+ }
182
+
183
+ /**
184
+ * The preset's bundle set vs the profile's current community bundles —
185
+ * a stale preset (saved before a plugin was installed/uninstalled) can no
186
+ * longer be applied as-is, but its intent (enabled/disabled plugins,
187
+ * relative order) is still previewable.
188
+ */
189
+ export interface PresetMismatch {
190
+ /** Bundles in the current profile that the preset does not mention. */
191
+ missing: string[]
192
+ /** Bundles the preset mentions that are not installed anymore. */
193
+ extra: string[]
194
+ /** True when the preset's bundle set differs from the current one. */
195
+ stale: boolean
196
+ }
197
+
198
+ /** Compare a preset's order against the current community bundle set. */
199
+ function presetMismatch(profileDir: string, bundleOrder: string[]): PresetMismatch {
200
+ const { community } = readBundleStack(profileDir)
201
+ const current = new Set(community)
202
+ const preset = new Set(bundleOrder)
203
+ const missing = community.filter(name => !preset.has(name))
204
+ const extra = bundleOrder.filter(name => !current.has(name))
205
+ return { missing, extra, stale: missing.length > 0 || extra.length > 0 }
206
+ }
207
+
208
+ /**
209
+ * Preview what applying a preset would change, WITHOUT writing anything.
210
+ * A stale preset (bundle set mismatch) is NOT a hard failure: the preview
211
+ * reports the mismatch alongside the still-computable changes (relative
212
+ * order + enabled/disabled diffs over the intersection).
213
+ */
214
+ export function previewPreset(profileDir: string, name: unknown): PresetResult & { changes?: PresetChange; mismatch?: PresetMismatch } {
215
+ if (typeof name !== 'string') return { ok: false, error: 'invalid preset name / 组合名称无效' }
216
+ const preset = readPresets(profileDir).find(item => item.name === name)
217
+ if (preset === undefined) return { ok: false, error: 'preset not found / 组合不存在' }
218
+
219
+ // Full composition replay only makes sense when the bundle SET matches;
220
+ // otherwise report the mismatch (and keep the apply path's hard refusal).
221
+ const mismatch = presetMismatch(profileDir, preset.bundleOrder)
222
+ if (mismatch.stale) {
223
+ const detail = [...mismatch.missing.map(n => `+${n}`), ...mismatch.extra.map(n => `-${n}`)].join(' ')
224
+ return {
225
+ ok: false,
226
+ mismatch,
227
+ error: `preset is out of date — current profile differs: ${detail} / 组合已过期——当前 profile 的插件列表已变化:${detail}`,
228
+ }
229
+ }
230
+
231
+ const trial = trialValidate(profileDir, preset.bundleOrder)
232
+ if (!trial.ok) {
233
+ const first = trial.errors[0]
234
+ return {
235
+ ok: false,
236
+ error: `trial validation failed — ${first?.message ?? 'composition would not boot'} / 试启动校验失败:${first?.message ?? '组合无法启动'}`,
237
+ }
238
+ }
239
+
240
+ const { community } = readBundleStack(profileDir)
241
+ const reordered = community.filter((name, index) => name !== preset.bundleOrder[index])
242
+ const currentDisabled = readMarketState(profileDir).disabled
243
+ // The market's own names are never applied (self-disable guard) — exclude
244
+ // them from the diff too, so the preview matches what apply will actually do.
245
+ const presetDisabled = new Set(preset.disabled.filter(name => !MARKET_SELF_NAMES.has(name)))
246
+ const enabled = [...currentDisabled].filter(name => !presetDisabled.has(name))
247
+ const disabled = [...presetDisabled].filter(name => !currentDisabled.has(name))
248
+ return {
249
+ ok: true,
250
+ changes: {
251
+ reordered,
252
+ enabled,
253
+ disabled,
254
+ noop: reordered.length === 0 && enabled.length === 0 && disabled.length === 0,
255
+ },
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Apply a saved preset: trial-validate the candidate order first (refuse
261
+ * without writing on any boot-breaking issue), auto-snapshot the profile,
262
+ * then write the bundle order and the disable list. The response carries the
263
+ * change preview so the UI can report exactly what moved.
264
+ */
265
+ export function applyPreset(profileDir: string, name: unknown, maxSnapshots: number = DEFAULT_MAX_SNAPSHOTS): PresetApplyResult {
266
+ if (typeof name !== 'string') return { ok: false, error: 'invalid preset name / 组合名称无效' }
267
+ const preset = readPresets(profileDir).find(item => item.name === name)
268
+ if (preset === undefined) return { ok: false, error: 'preset not found / 组合不存在' }
269
+
270
+ // A stale preset (bundle set differs) cannot be applied as-is; say exactly
271
+ // what differs instead of the raw trial-merge error (issue #98 report).
272
+ const mismatch = presetMismatch(profileDir, preset.bundleOrder)
273
+ if (mismatch.stale) {
274
+ const detail = [...mismatch.missing.map(n => `+${n}`), ...mismatch.extra.map(n => `-${n}`)].join(' ')
275
+ logEvent('warn', 'preset', `apply "${name}" rejected: preset out of date — ${detail}`)
276
+ return {
277
+ ok: false,
278
+ error: `preset is out of date — current profile differs: ${detail} / 组合已过期——当前 profile 的插件列表已变化:${detail}`,
279
+ }
280
+ }
281
+
282
+ const trial = trialValidate(profileDir, preset.bundleOrder)
283
+ if (!trial.ok) {
284
+ const first = trial.errors[0]
285
+ logEvent('warn', 'preset', `apply "${name}" rejected by trial validation: ${first?.message ?? 'unknown'}`)
286
+ return {
287
+ ok: false,
288
+ error: `trial validation failed — ${first?.message ?? 'composition would not boot'} / 试启动校验失败:${first?.message ?? '组合无法启动'}`,
289
+ trial: { errors: trial.errors, warnings: trial.warnings, diff: trial.diff },
290
+ }
291
+ }
292
+ // Before/after rules (review B5): the reorder endpoint refuses rule-violating
293
+ // stacks; the preset path must enforce the same gate before writing.
294
+ const { bundles } = readBundleStack(profileDir)
295
+ const merged = mergeOrder(bundles, preset.bundleOrder)
296
+ if (merged.ok) {
297
+ const conflicts = validateOrder(merged.bundles, readBundleRules(profileDir))
298
+ if (conflicts.length > 0) {
299
+ logEvent('warn', 'preset', `apply "${name}" rejected by before/after rules: ${conflicts.map(c => c.reason).join('; ')}`)
300
+ return {
301
+ ok: false,
302
+ error: 'the preset order violates declared before/after rules / 组合顺序违反了插件声明的 before/after 规则',
303
+ }
304
+ }
305
+ }
306
+
307
+ const preview = previewPreset(profileDir, name)
308
+ const captured = createProfileSnapshot(profileDir, maxSnapshots)
309
+ if (!captured.ok) {
310
+ logEvent('error', 'preset', `apply "${name}" refused: ${captured.error}`)
311
+ return { ok: false, error: captured.error }
312
+ }
313
+ const snapshot = captured.snapshot
314
+ const ordered = applyBundleOrder(profileDir, preset.bundleOrder)
315
+ if (!ordered.ok) {
316
+ return { ok: false, error: ordered.error }
317
+ }
318
+ const state = readMarketState(profileDir)
319
+ // Self-disable guard (issue #98 analysis): a preset (possibly imported)
320
+ // carrying the market's own name must never switch this page off — drop
321
+ // those names from the applied disabled list.
322
+ const filtered = preset.disabled.filter(name => !MARKET_SELF_NAMES.has(name))
323
+ if (filtered.length !== preset.disabled.length) {
324
+ logEvent('warn', 'preset', `apply "${name}": dropped market self-names from the disabled list`)
325
+ }
326
+ state.disabled = new Set(filtered)
327
+ writeMarketState(profileDir, state)
328
+ logEvent('info', 'preset', `applied "${name}" (snapshot ${snapshot.id})`)
329
+ return { ok: true, snapshot: snapshot.id, changes: preview.ok ? preview.changes : undefined }
330
+ }
331
+
332
+ /*
333
+ * Presets deliberately have NO import/export of their own.
334
+ *
335
+ * They had one on the branch this came from, and it was fine code — but
336
+ * the market already answers "take this configuration elsewhere" under
337
+ * Advanced -> Backup & Restore, which carries the whole profile rather
338
+ * than one slice of it. Two export buttons in two tabs, writing two file
339
+ * formats that overlap, is a question the user has to stop and answer
340
+ * before they can do the thing they came to do.
341
+ *
342
+ * Presets stay the local answer to "switch between combinations I have
343
+ * saved". Moving them to another machine is the backup's job.
344
+ */