@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/src/order.ts
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Community bundle ordering — issue #98 (phase 2): let the user reorder the
|
|
3
|
+
* community bundles of the profile's layer stack, with author-declared
|
|
4
|
+
* before/after rules enforced before anything is written.
|
|
5
|
+
*
|
|
6
|
+
* Official in-box bundles (@deepseek-ai/dsh-base, @deepseek-ai/dsh-web-app,
|
|
7
|
+
* @deepseek-ai/dsh-headless) are fixed: they keep their exact positions in
|
|
8
|
+
* the stack, are never part of a user-supplied order, and are never added,
|
|
9
|
+
* removed or duplicated by a reorder (#98 boundary). The profile's own
|
|
10
|
+
* cordis.patch.yml and --patch overlays are not part of the bundle stack and
|
|
11
|
+
* are never touched here.
|
|
12
|
+
*
|
|
13
|
+
* Pure functions plus one manifest write-back; no processes, no network.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs'
|
|
17
|
+
import { createRequire } from 'node:module'
|
|
18
|
+
import { join } from 'node:path'
|
|
19
|
+
import { findDshInstallDir } from './dsh-install.ts'
|
|
20
|
+
|
|
21
|
+
/** Profile bundles that ship with the dsh host and must stay put (#98). */
|
|
22
|
+
export const INBOX_BUNDLES = new Set([
|
|
23
|
+
'@deepseek-ai/dsh-base',
|
|
24
|
+
'@deepseek-ai/dsh-web-app',
|
|
25
|
+
'@deepseek-ai/dsh-headless',
|
|
26
|
+
])
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Atomic same-directory replace (write temp + rename): a crash mid-write can
|
|
30
|
+
* never leave the profile manifest truncated, which would break every later
|
|
31
|
+
* pnpm run. Used for every package.json write this module makes.
|
|
32
|
+
*/
|
|
33
|
+
function writeFileAtomic(file: string, content: string): void {
|
|
34
|
+
const temp = `${file}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
|
35
|
+
writeFileSync(temp, content)
|
|
36
|
+
renameSync(temp, file)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The bundle stack as it appears in the profile manifest. */
|
|
40
|
+
export interface BundleStack {
|
|
41
|
+
/** Full ordered list from dsh.profile.bundles. */
|
|
42
|
+
bundles: string[]
|
|
43
|
+
/** The subset that may be reordered (community bundles). */
|
|
44
|
+
community: string[]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Author-declared ordering constraints of one bundle. */
|
|
48
|
+
export interface BundleRule {
|
|
49
|
+
name: string
|
|
50
|
+
/** This bundle must load after every name in this list. */
|
|
51
|
+
after: string[]
|
|
52
|
+
/** This bundle must load before every name in this list. */
|
|
53
|
+
before: string[]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A violated before/after rule in the current or proposed order. */
|
|
57
|
+
export interface OrderConflict {
|
|
58
|
+
name: string
|
|
59
|
+
reason: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Read the profile's bundle stack (empty when the manifest is unreadable). */
|
|
63
|
+
export function readBundleStack(profileDir: string): BundleStack {
|
|
64
|
+
try {
|
|
65
|
+
const manifest = JSON.parse(readFileSync(join(profileDir, 'package.json'), 'utf8')) as {
|
|
66
|
+
dsh?: { profile?: { bundles?: unknown } }
|
|
67
|
+
}
|
|
68
|
+
const bundles = Array.isArray(manifest.dsh?.profile?.bundles)
|
|
69
|
+
? manifest.dsh.profile.bundles.filter((name): name is string => typeof name === 'string')
|
|
70
|
+
: []
|
|
71
|
+
return {
|
|
72
|
+
bundles,
|
|
73
|
+
community: bundles.filter(name => !INBOX_BUNDLES.has(name)),
|
|
74
|
+
}
|
|
75
|
+
} catch {
|
|
76
|
+
return { bundles: [], community: [] }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The bundle package.json, resolved the way the dsh boot resolves bundles
|
|
82
|
+
* (dsh-app-boot's resolveBundleDir, mirrored by check.ts): the dsh
|
|
83
|
+
* installation anchor first — official in-box bundles live in the install's
|
|
84
|
+
* node_modules, never the profile's — then the profile directory, whose
|
|
85
|
+
* createRequire search paths also cover pnpm workspace-root hoisting
|
|
86
|
+
* (`<profiles>/node_modules` when the profile lives under `<profiles>/<name>`).
|
|
87
|
+
*/
|
|
88
|
+
function resolveBundlePackageJson(profileDir: string, name: string): string | null {
|
|
89
|
+
const dshInstall = findDshInstallDir()
|
|
90
|
+
const anchors = [
|
|
91
|
+
dshInstall !== null ? join(dshInstall, 'package.json') : null,
|
|
92
|
+
join(profileDir, 'package.json'),
|
|
93
|
+
]
|
|
94
|
+
for (const anchor of anchors) {
|
|
95
|
+
if (anchor === null) continue
|
|
96
|
+
let paths: string[] = []
|
|
97
|
+
try {
|
|
98
|
+
paths = createRequire(anchor).resolve.paths(name) ?? []
|
|
99
|
+
} catch {
|
|
100
|
+
continue
|
|
101
|
+
}
|
|
102
|
+
for (const searchPath of paths) {
|
|
103
|
+
const candidate = join(searchPath, name)
|
|
104
|
+
if (existsSync(join(candidate, 'package.json'))) return join(candidate, 'package.json')
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return null
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Read each bundle's declared ordering rules from its package manifest
|
|
112
|
+
* (`dsh.bundle.order.{before,after}` — a list of bundle package names).
|
|
113
|
+
* Unresolvable packages and missing declarations contribute nothing.
|
|
114
|
+
*/
|
|
115
|
+
export function readBundleRules(profileDir: string): BundleRule[] {
|
|
116
|
+
const { bundles } = readBundleStack(profileDir)
|
|
117
|
+
const rules: BundleRule[] = []
|
|
118
|
+
for (const name of bundles) {
|
|
119
|
+
const packageJson = resolveBundlePackageJson(profileDir, name)
|
|
120
|
+
if (packageJson === null) continue
|
|
121
|
+
try {
|
|
122
|
+
const manifest = JSON.parse(
|
|
123
|
+
readFileSync(packageJson, 'utf8'),
|
|
124
|
+
) as { dsh?: { bundle?: { order?: unknown } } }
|
|
125
|
+
const order = manifest.dsh?.bundle?.order
|
|
126
|
+
if (order === null || typeof order !== 'object' || Array.isArray(order)) continue
|
|
127
|
+
const listOf = (value: unknown): string[] => Array.isArray(value)
|
|
128
|
+
? value.filter((item): item is string => typeof item === 'string')
|
|
129
|
+
: []
|
|
130
|
+
const rule: BundleRule = {
|
|
131
|
+
name,
|
|
132
|
+
after: listOf((order as Record<string, unknown>).after),
|
|
133
|
+
before: listOf((order as Record<string, unknown>).before),
|
|
134
|
+
}
|
|
135
|
+
if (rule.after.length > 0 || rule.before.length > 0) rules.push(rule)
|
|
136
|
+
} catch { /* package unreadable — no rule */ }
|
|
137
|
+
}
|
|
138
|
+
return rules
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Check a bundle order against the declared before/after rules. Rules naming
|
|
143
|
+
* bundles outside `order` are ignored (a rule for a not-yet-installed bundle
|
|
144
|
+
* must not block the current stack).
|
|
145
|
+
* @returns every violated rule with a readable reason; [] when all hold.
|
|
146
|
+
*/
|
|
147
|
+
export function validateOrder(bundleNames: string[], rules: BundleRule[]): OrderConflict[] {
|
|
148
|
+
const position = new Map(bundleNames.map((name, index) => [name, index]))
|
|
149
|
+
const conflicts: OrderConflict[] = []
|
|
150
|
+
for (const rule of rules) {
|
|
151
|
+
const pos = position.get(rule.name)
|
|
152
|
+
if (pos === undefined) continue
|
|
153
|
+
for (const other of rule.after) {
|
|
154
|
+
const otherPos = position.get(other)
|
|
155
|
+
if (otherPos === undefined) continue
|
|
156
|
+
if (otherPos >= pos) {
|
|
157
|
+
conflicts.push({
|
|
158
|
+
name: rule.name,
|
|
159
|
+
reason: `must load after ${other}, but ${other} is currently before/equal (position ${otherPos} vs ${pos})`,
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
for (const other of rule.before) {
|
|
164
|
+
const otherPos = position.get(other)
|
|
165
|
+
if (otherPos === undefined) continue
|
|
166
|
+
if (otherPos <= pos) {
|
|
167
|
+
conflicts.push({
|
|
168
|
+
name: rule.name,
|
|
169
|
+
reason: `must load before ${other}, but ${other} is currently after/equal (position ${otherPos} vs ${pos})`,
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return conflicts
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Merge a community-bundle permutation into the full stack. Official in-box
|
|
179
|
+
* bundles keep their EXACT positions (never moved); community bundles are
|
|
180
|
+
* replaced by `newOrder` in order of appearance. Pure — nothing is written.
|
|
181
|
+
* @returns the merged full stack, or the rejection reason when `newOrder` is
|
|
182
|
+
* not a permutation of the community bundles (duplicates, additions,
|
|
183
|
+
* omissions, official names).
|
|
184
|
+
*/
|
|
185
|
+
export function mergeOrder(bundles: string[], newOrder: string[]): { ok: true; bundles: string[] } | { ok: false; error: string } {
|
|
186
|
+
const communitySet = new Set(bundles.filter(name => !INBOX_BUNDLES.has(name)))
|
|
187
|
+
if (new Set(newOrder).size !== newOrder.length) {
|
|
188
|
+
return { ok: false, error: 'duplicate bundle names in the new order / 新顺序包含重复的 bundle' }
|
|
189
|
+
}
|
|
190
|
+
if (newOrder.length !== communitySet.size) {
|
|
191
|
+
return { ok: false, error: 'the new order must contain exactly the current community bundles / 新顺序必须恰好包含全部社区 bundle' }
|
|
192
|
+
}
|
|
193
|
+
for (const name of newOrder) {
|
|
194
|
+
if (!communitySet.has(name)) {
|
|
195
|
+
return { ok: false, error: `${name} is not a reorderable community bundle / ${name} 不是可排序的社区 bundle` }
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const merged = [...bundles]
|
|
199
|
+
let cursor = 0
|
|
200
|
+
for (let index = 0; index < merged.length; index += 1) {
|
|
201
|
+
const name = merged[index]
|
|
202
|
+
if (name === undefined || INBOX_BUNDLES.has(name)) continue
|
|
203
|
+
merged[index] = newOrder[cursor]
|
|
204
|
+
cursor += 1
|
|
205
|
+
}
|
|
206
|
+
return { ok: true, bundles: merged }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Topologically sort the community bundles by their before/after rules — the
|
|
211
|
+
* "auto-fix" counterpart to validateOrder. Returns null when no declared rule
|
|
212
|
+
* applies to the current stack (nothing to suggest). With rules, Kahn's
|
|
213
|
+
* algorithm breaks ties by the CURRENT order: unconstrained bundles keep
|
|
214
|
+
* their current relative order and constrained bundles move only as far as
|
|
215
|
+
* the rules require — the suggestion is the minimal change that satisfies
|
|
216
|
+
* every rule, never an arbitrary canonical rewrite of a hand-picked order
|
|
217
|
+
* (issue #125 review).
|
|
218
|
+
* @returns the suggested community order, null when there are no rules, or a
|
|
219
|
+
* cycle report when the constraints cannot be satisfied (references to
|
|
220
|
+
* unlisted bundles ignored).
|
|
221
|
+
*/
|
|
222
|
+
export function suggestOrder(
|
|
223
|
+
bundleNames: string[],
|
|
224
|
+
rules: BundleRule[],
|
|
225
|
+
): { ok: true; order: string[] } | { ok: false; cycle: string[] } | null {
|
|
226
|
+
const names = bundleNames.filter(name => !INBOX_BUNDLES.has(name))
|
|
227
|
+
const inOrder = new Set(names)
|
|
228
|
+
const active = rules.filter(rule => inOrder.has(rule.name))
|
|
229
|
+
// No rule applies to the current stack — nothing to suggest.
|
|
230
|
+
if (active.length === 0) return null
|
|
231
|
+
const position = new Map(names.map((name, index) => [name, index]))
|
|
232
|
+
// Constraint: "a must load before b" (from a.before or b.after) → edge a → b.
|
|
233
|
+
const beforeOf = new Map<string, Set<string>>() // name → bundles that must come after it
|
|
234
|
+
const deps = new Map<string, Set<string>>() // name → bundles that must come before it
|
|
235
|
+
for (const name of names) {
|
|
236
|
+
beforeOf.set(name, new Set())
|
|
237
|
+
deps.set(name, new Set())
|
|
238
|
+
}
|
|
239
|
+
const addEdge = (a: string, b: string): void => {
|
|
240
|
+
if (!inOrder.has(a) || !inOrder.has(b) || a === b) return
|
|
241
|
+
beforeOf.get(a)?.add(b)
|
|
242
|
+
deps.get(b)?.add(a)
|
|
243
|
+
}
|
|
244
|
+
for (const rule of active) {
|
|
245
|
+
for (const other of rule.before) addEdge(rule.name, other)
|
|
246
|
+
for (const other of rule.after) addEdge(other, rule.name)
|
|
247
|
+
}
|
|
248
|
+
const remaining = new Map<string, Set<string>>()
|
|
249
|
+
for (const [name, depsOf] of deps) remaining.set(name, new Set(depsOf))
|
|
250
|
+
const ready: string[] = names.filter(name => (remaining.get(name)?.size ?? 0) === 0)
|
|
251
|
+
const ordered: string[] = []
|
|
252
|
+
while (ready.length > 0) {
|
|
253
|
+
// Minimal-change tie-break: among the ready bundles, prefer the one that
|
|
254
|
+
// comes FIRST in the current order. Bundles the rules do not constrain
|
|
255
|
+
// therefore keep their current relative order; constrained bundles move
|
|
256
|
+
// only as far as the rules require (issue #125 review).
|
|
257
|
+
let best = 0
|
|
258
|
+
for (let i = 1; i < ready.length; i += 1) {
|
|
259
|
+
const a = ready[i]
|
|
260
|
+
const b = ready[best]
|
|
261
|
+
if (a !== undefined && b !== undefined && (position.get(a) ?? 0) < (position.get(b) ?? 0)) best = i
|
|
262
|
+
}
|
|
263
|
+
const name = ready.splice(best, 1)[0]
|
|
264
|
+
if (name === undefined) break
|
|
265
|
+
ordered.push(name)
|
|
266
|
+
for (const dependent of beforeOf.get(name) ?? []) {
|
|
267
|
+
const depsOf = remaining.get(dependent)
|
|
268
|
+
if (depsOf === undefined) continue
|
|
269
|
+
depsOf.delete(name)
|
|
270
|
+
if (depsOf.size === 0 && !ordered.includes(dependent) && !ready.includes(dependent)) ready.push(dependent)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (ordered.length < names.length) {
|
|
274
|
+
return { ok: false, cycle: names.filter(name => !ordered.includes(name)) }
|
|
275
|
+
}
|
|
276
|
+
return { ok: true, order: ordered }
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Apply a new community-bundle order to the profile manifest. The official
|
|
281
|
+
* in-box bundles keep their exact positions; `newOrder` must be a permutation
|
|
282
|
+
* of the current community bundles (no duplicates, no additions, no
|
|
283
|
+
* omissions). On any failure the manifest is left untouched.
|
|
284
|
+
* @returns the new full stack on success, or an error description.
|
|
285
|
+
*/
|
|
286
|
+
export function applyBundleOrder(profileDir: string, newOrder: string[]): { ok: true; bundles: string[] } | { ok: false; error: string } {
|
|
287
|
+
const { bundles } = readBundleStack(profileDir)
|
|
288
|
+
const merged = mergeOrder(bundles, newOrder)
|
|
289
|
+
if (!merged.ok) return merged
|
|
290
|
+
try {
|
|
291
|
+
const manifestPath = join(profileDir, 'package.json')
|
|
292
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as {
|
|
293
|
+
dsh?: { profile?: { bundles?: unknown } }
|
|
294
|
+
}
|
|
295
|
+
manifest.dsh ??= {}
|
|
296
|
+
manifest.dsh.profile ??= {}
|
|
297
|
+
manifest.dsh.profile.bundles = merged.bundles
|
|
298
|
+
writeFileAtomic(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`)
|
|
299
|
+
return merged
|
|
300
|
+
} catch (error) {
|
|
301
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) }
|
|
302
|
+
}
|
|
303
|
+
}
|