@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/ndjson.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pnpm `--reporter=ndjson` progress parser (P1-6).
|
|
3
|
+
*
|
|
4
|
+
* pnpm's ndjson reporter is a bole stream on stdout: one JSON object per
|
|
5
|
+
* line, e.g.
|
|
6
|
+
*
|
|
7
|
+
* {"time":...,"level":"debug","name":"pnpm:stage","prefix":"...","stage":"resolution_started"}
|
|
8
|
+
* {"time":...,"level":"debug","name":"pnpm:progress","packageId":"...","status":"resolved"}
|
|
9
|
+
* {"time":...,"level":"debug","name":"pnpm:fetching-progress","packageId":"...","status":"started","size":123}
|
|
10
|
+
* {"time":...,"level":"debug","name":"pnpm:ignored-scripts","packageNames":["esbuild"]}
|
|
11
|
+
* {"time":...,"level":"error","name":"pnpm","err":{"message":"..."}}
|
|
12
|
+
*
|
|
13
|
+
* Verified against real pnpm 11.16.0 output (2026-08). Older pnpm majors
|
|
14
|
+
* emit a different shape or nothing at all — callers fall back to human
|
|
15
|
+
* line parsing when `seen` stays false.
|
|
16
|
+
*
|
|
17
|
+
* The reducer is pure and unit-testable: `feed` mutates the tracker's
|
|
18
|
+
* internal snapshot, `snapshot` returns a serializable copy.
|
|
19
|
+
*/
|
|
20
|
+
export function emptyProgress() {
|
|
21
|
+
return {
|
|
22
|
+
phase: null,
|
|
23
|
+
done: 0,
|
|
24
|
+
total: null,
|
|
25
|
+
currentPackage: null,
|
|
26
|
+
downloaded: null,
|
|
27
|
+
size: null,
|
|
28
|
+
seen: false,
|
|
29
|
+
error: null,
|
|
30
|
+
errorCode: null,
|
|
31
|
+
ignoredBuilds: [],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function createProgressTracker() {
|
|
35
|
+
const snap = emptyProgress();
|
|
36
|
+
const seenPackages = new Set();
|
|
37
|
+
function dedupe(packageId) {
|
|
38
|
+
if (typeof packageId !== 'string' || packageId === '')
|
|
39
|
+
return;
|
|
40
|
+
if (!seenPackages.has(packageId)) {
|
|
41
|
+
seenPackages.add(packageId);
|
|
42
|
+
snap.done += 1;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function feed(line) {
|
|
46
|
+
let event;
|
|
47
|
+
try {
|
|
48
|
+
event = JSON.parse(line);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return; // human fallback lines are handled by the caller
|
|
52
|
+
}
|
|
53
|
+
if (typeof event !== 'object' || event === null)
|
|
54
|
+
return;
|
|
55
|
+
const msg = event;
|
|
56
|
+
const name = msg.name;
|
|
57
|
+
if (typeof name !== 'string')
|
|
58
|
+
return;
|
|
59
|
+
if (name === 'pnpm:stage') {
|
|
60
|
+
const stage = msg.stage;
|
|
61
|
+
if (stage === 'resolution_started')
|
|
62
|
+
snap.phase = 'resolving';
|
|
63
|
+
else if (stage === 'resolution_done')
|
|
64
|
+
snap.phase = 'downloading';
|
|
65
|
+
else if (stage === 'importing_started' || stage === 'importing_done')
|
|
66
|
+
snap.phase = 'linking';
|
|
67
|
+
snap.seen = true;
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (name === 'pnpm:progress') {
|
|
71
|
+
snap.seen = true;
|
|
72
|
+
const status = msg.status;
|
|
73
|
+
if (status === 'resolved') {
|
|
74
|
+
if (snap.phase === null)
|
|
75
|
+
snap.phase = 'resolving';
|
|
76
|
+
dedupe(msg.packageId);
|
|
77
|
+
}
|
|
78
|
+
else if (status === 'fetched' || status === 'found_in_store') {
|
|
79
|
+
snap.phase = 'downloading';
|
|
80
|
+
snap.currentPackage = typeof msg.packageId === 'string' ? msg.packageId : snap.currentPackage;
|
|
81
|
+
dedupe(msg.packageId);
|
|
82
|
+
}
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (name === 'pnpm:fetching-progress') {
|
|
86
|
+
snap.seen = true;
|
|
87
|
+
snap.phase = 'downloading';
|
|
88
|
+
if (typeof msg.packageId === 'string')
|
|
89
|
+
snap.currentPackage = msg.packageId;
|
|
90
|
+
if (typeof msg.size === 'number')
|
|
91
|
+
snap.size = msg.size;
|
|
92
|
+
if (typeof msg.downloaded === 'number')
|
|
93
|
+
snap.downloaded = msg.downloaded;
|
|
94
|
+
dedupe(msg.packageId);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (name === 'pnpm:lifecycle') {
|
|
98
|
+
snap.seen = true;
|
|
99
|
+
snap.phase = 'building';
|
|
100
|
+
const wd = typeof msg.wd === 'string' ? msg.wd : '';
|
|
101
|
+
const dep = typeof msg.depPath === 'string' ? msg.depPath : '';
|
|
102
|
+
const base = wd.split(/[\\/]/).filter(Boolean).pop();
|
|
103
|
+
snap.currentPackage = base ?? (dep !== '' ? dep : snap.currentPackage);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (name === 'pnpm:stats') {
|
|
107
|
+
// added/removed counts land after the import pass — still linking.
|
|
108
|
+
if (msg.added !== undefined || msg.removed !== undefined)
|
|
109
|
+
snap.phase = 'linking';
|
|
110
|
+
snap.seen = true;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (name === 'pnpm:ignored-scripts') {
|
|
114
|
+
snap.seen = true;
|
|
115
|
+
if (Array.isArray(msg.packageNames)) {
|
|
116
|
+
for (const pkg of msg.packageNames) {
|
|
117
|
+
// pnpm's ndjson event reports version-qualified names (cloudflared@0.7.3);
|
|
118
|
+
// the approve-builds allowlist keys and node_modules lookups use bare
|
|
119
|
+
// package names, so strip the suffix (same rule as the human-line
|
|
120
|
+
// fallback in install.ts's parseIgnoredBuilds).
|
|
121
|
+
const at = typeof pkg === 'string' ? pkg.lastIndexOf('@') : -1;
|
|
122
|
+
const bare = at > 0 ? pkg.slice(0, at) : pkg;
|
|
123
|
+
if (typeof bare === 'string' && bare !== '' && !snap.ignoredBuilds.includes(bare))
|
|
124
|
+
snap.ignoredBuilds.push(bare);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (name === 'pnpm' && msg.level === 'error') {
|
|
130
|
+
const err = (msg.err ?? {});
|
|
131
|
+
const message = typeof err.message === 'string' ? err.message : '';
|
|
132
|
+
// Keep more than the old 400: ERR_PNPM_UNEXPECTED_STORE spends most of
|
|
133
|
+
// its message naming two absolute store paths, and truncating them is
|
|
134
|
+
// exactly the information a user needs to fix it by hand (#244).
|
|
135
|
+
if (message !== '')
|
|
136
|
+
snap.error = message.slice(0, 2000);
|
|
137
|
+
if (typeof err.code === 'string' && err.code !== '')
|
|
138
|
+
snap.errorCode = err.code;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function reset() {
|
|
143
|
+
seenPackages.clear();
|
|
144
|
+
const fresh = emptyProgress();
|
|
145
|
+
Object.assign(snap, fresh);
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
get snapshot() {
|
|
149
|
+
return { ...snap, ignoredBuilds: [...snap.ignoredBuilds] };
|
|
150
|
+
},
|
|
151
|
+
feed,
|
|
152
|
+
reset,
|
|
153
|
+
};
|
|
154
|
+
}
|
package/lib/net.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Outbound HTTP for the market's own server-side calls.
|
|
3
|
+
*
|
|
4
|
+
* Node's global `fetch` ignores `HTTP_PROXY` / `HTTPS_PROXY` entirely
|
|
5
|
+
* (measured on Node 25: a request with an unreachable proxy configured still
|
|
6
|
+
* succeeds directly, and setting `NODE_USE_ENV_PROXY` at runtime changes
|
|
7
|
+
* nothing — it is read at startup). On a machine whose route out is a local
|
|
8
|
+
* proxy, that is not a slowdown but a different network: the catalog fetch
|
|
9
|
+
* took 9.9s direct on a reporter's machine, seconds from the 15s timeout,
|
|
10
|
+
* while their proxy sat unused a millisecond away.
|
|
11
|
+
*
|
|
12
|
+
* `setGlobalDispatcher` from the `undici` PACKAGE cannot fix this, because
|
|
13
|
+
* `globalThis.fetch` runs on Node's INTERNAL copy of undici — a different
|
|
14
|
+
* instance. Verified: with a dispatcher installed, a global fetch still
|
|
15
|
+
* produced no CONNECT at a local proxy, while undici's own fetch produced
|
|
16
|
+
* `CONNECT awesome-dsh-plugin.com:443`.
|
|
17
|
+
*
|
|
18
|
+
* So the market calls undici's fetch with an explicit dispatcher. The scope
|
|
19
|
+
* is deliberate: only requests made by this module change, and the host's
|
|
20
|
+
* own networking is left exactly as the host configured it.
|
|
21
|
+
*/
|
|
22
|
+
import { EnvHttpProxyAgent, fetch as undiciFetch } from 'undici';
|
|
23
|
+
/**
|
|
24
|
+
* The proxy this process would use for the catalog, if any.
|
|
25
|
+
*
|
|
26
|
+
* The standard variables mirror `EnvHttpProxyAgent`'s own resolution
|
|
27
|
+
* deliberately, rather than picking the order that reads best, because the
|
|
28
|
+
* same answer does two jobs: it decides whether to route through undici at
|
|
29
|
+
* all, and it is what the failure message CLAIMS was tried. A helper that
|
|
30
|
+
* named a proxy undici would not have used would put a false statement in
|
|
31
|
+
* every bug report. `npm_config_*` is an additional source on top of that:
|
|
32
|
+
* npm holds its proxy in its own config namespace (a machine set up with
|
|
33
|
+
* `npm config set proxy` has the proxy in `npm_config_proxy` and nowhere
|
|
34
|
+
* else), and undici does not read it — so it is resolved here and handed to
|
|
35
|
+
* the agent explicitly in `marketFetch`.
|
|
36
|
+
*
|
|
37
|
+
* Three details are undici's, not ours (env-http-proxy-agent.js):
|
|
38
|
+
* - lowercase wins over uppercase (`https_proxy ?? HTTPS_PROXY`)
|
|
39
|
+
* - an https request falls back to the http proxy when no https one is set
|
|
40
|
+
* - the value is tested for truthiness, so `HTTPS_PROXY=` — which is how
|
|
41
|
+
* people turn a proxy off — falls through instead of masking HTTP_PROXY
|
|
42
|
+
*
|
|
43
|
+
* Blank-is-unset is ours, and only widens that last one: undici would hand a
|
|
44
|
+
* whitespace-only value to `new URL()` and throw out of the constructor.
|
|
45
|
+
* Scheme-less host:port values are also common in Windows proxy fields and
|
|
46
|
+
* npm config; proxy agents require URLs, so those default to `http://`.
|
|
47
|
+
*/
|
|
48
|
+
export function configuredProxy() {
|
|
49
|
+
const { http, https } = proxyFromEnv();
|
|
50
|
+
return https ?? http;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Proxy URLs resolved from the process environment, standard variables
|
|
54
|
+
* first and npm's own config (`npm_config_https_proxy` / `npm_config_proxy`)
|
|
55
|
+
* as the fallback. npm's config is the second source, not a preference: a
|
|
56
|
+
* standard variable always wins over npm's, and npm's https proxy falls
|
|
57
|
+
* back to npm's http proxy the same way undici's does.
|
|
58
|
+
*/
|
|
59
|
+
function proxyFromEnv() {
|
|
60
|
+
const pick = (raw) => {
|
|
61
|
+
const value = raw?.trim();
|
|
62
|
+
if (value === undefined || value === '')
|
|
63
|
+
return null;
|
|
64
|
+
return /^[a-z][a-z\d+.-]*:\/\//iu.test(value) ? value : `http://${value}`;
|
|
65
|
+
};
|
|
66
|
+
const https = pick(process.env.https_proxy ?? process.env.HTTPS_PROXY) ??
|
|
67
|
+
pick(process.env.npm_config_https_proxy);
|
|
68
|
+
const http = pick(process.env.http_proxy ?? process.env.HTTP_PROXY) ??
|
|
69
|
+
pick(process.env.npm_config_proxy);
|
|
70
|
+
return { http, https };
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Built once and reused: an agent per request would drop connection reuse,
|
|
74
|
+
* and this one reads NO_PROXY as well, so a host that excludes its own
|
|
75
|
+
* registry mirror keeps being excluded.
|
|
76
|
+
*/
|
|
77
|
+
let agent = null;
|
|
78
|
+
/**
|
|
79
|
+
* Fetch through the proxy this machine is configured to use.
|
|
80
|
+
*
|
|
81
|
+
* Falls back to the global fetch when no proxy is set, which keeps the
|
|
82
|
+
* ordinary case on the runtime's own path rather than routing it through a
|
|
83
|
+
* second HTTP stack for no reason.
|
|
84
|
+
*/
|
|
85
|
+
export async function marketFetch(url, init) {
|
|
86
|
+
const { http, https } = proxyFromEnv();
|
|
87
|
+
if (http === null && https === null)
|
|
88
|
+
return await fetch(url, init);
|
|
89
|
+
// Pass the resolved proxies explicitly. EnvHttpProxyAgent itself reads
|
|
90
|
+
// only http(s)_proxy out of the environment, so a proxy that lives in
|
|
91
|
+
// npm_config_* must be handed over directly — otherwise the agent would
|
|
92
|
+
// silently go direct while configuredProxy() claims a proxy was used.
|
|
93
|
+
agent ??= new EnvHttpProxyAgent({
|
|
94
|
+
httpProxy: http ?? undefined,
|
|
95
|
+
httpsProxy: https ?? undefined,
|
|
96
|
+
});
|
|
97
|
+
return await undiciFetch(url, { ...init, dispatcher: agent });
|
|
98
|
+
}
|
package/lib/order.js
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
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
|
+
import { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
16
|
+
import { createRequire } from 'node:module';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import { findDshInstallDir } from './dsh-install.js';
|
|
19
|
+
/** Profile bundles that ship with the dsh host and must stay put (#98). */
|
|
20
|
+
export const INBOX_BUNDLES = new Set([
|
|
21
|
+
'@deepseek-ai/dsh-base',
|
|
22
|
+
'@deepseek-ai/dsh-web-app',
|
|
23
|
+
'@deepseek-ai/dsh-headless',
|
|
24
|
+
]);
|
|
25
|
+
/**
|
|
26
|
+
* Atomic same-directory replace (write temp + rename): a crash mid-write can
|
|
27
|
+
* never leave the profile manifest truncated, which would break every later
|
|
28
|
+
* pnpm run. Used for every package.json write this module makes.
|
|
29
|
+
*/
|
|
30
|
+
function writeFileAtomic(file, content) {
|
|
31
|
+
const temp = `${file}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
32
|
+
writeFileSync(temp, content);
|
|
33
|
+
renameSync(temp, file);
|
|
34
|
+
}
|
|
35
|
+
/** Read the profile's bundle stack (empty when the manifest is unreadable). */
|
|
36
|
+
export function readBundleStack(profileDir) {
|
|
37
|
+
try {
|
|
38
|
+
const manifest = JSON.parse(readFileSync(join(profileDir, 'package.json'), 'utf8'));
|
|
39
|
+
const bundles = Array.isArray(manifest.dsh?.profile?.bundles)
|
|
40
|
+
? manifest.dsh.profile.bundles.filter((name) => typeof name === 'string')
|
|
41
|
+
: [];
|
|
42
|
+
return {
|
|
43
|
+
bundles,
|
|
44
|
+
community: bundles.filter(name => !INBOX_BUNDLES.has(name)),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return { bundles: [], community: [] };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The bundle package.json, resolved the way the dsh boot resolves bundles
|
|
53
|
+
* (dsh-app-boot's resolveBundleDir, mirrored by check.ts): the dsh
|
|
54
|
+
* installation anchor first — official in-box bundles live in the install's
|
|
55
|
+
* node_modules, never the profile's — then the profile directory, whose
|
|
56
|
+
* createRequire search paths also cover pnpm workspace-root hoisting
|
|
57
|
+
* (`<profiles>/node_modules` when the profile lives under `<profiles>/<name>`).
|
|
58
|
+
*/
|
|
59
|
+
function resolveBundlePackageJson(profileDir, name) {
|
|
60
|
+
const dshInstall = findDshInstallDir();
|
|
61
|
+
const anchors = [
|
|
62
|
+
dshInstall !== null ? join(dshInstall, 'package.json') : null,
|
|
63
|
+
join(profileDir, 'package.json'),
|
|
64
|
+
];
|
|
65
|
+
for (const anchor of anchors) {
|
|
66
|
+
if (anchor === null)
|
|
67
|
+
continue;
|
|
68
|
+
let paths = [];
|
|
69
|
+
try {
|
|
70
|
+
paths = createRequire(anchor).resolve.paths(name) ?? [];
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
for (const searchPath of paths) {
|
|
76
|
+
const candidate = join(searchPath, name);
|
|
77
|
+
if (existsSync(join(candidate, 'package.json')))
|
|
78
|
+
return join(candidate, 'package.json');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Read each bundle's declared ordering rules from its package manifest
|
|
85
|
+
* (`dsh.bundle.order.{before,after}` — a list of bundle package names).
|
|
86
|
+
* Unresolvable packages and missing declarations contribute nothing.
|
|
87
|
+
*/
|
|
88
|
+
export function readBundleRules(profileDir) {
|
|
89
|
+
const { bundles } = readBundleStack(profileDir);
|
|
90
|
+
const rules = [];
|
|
91
|
+
for (const name of bundles) {
|
|
92
|
+
const packageJson = resolveBundlePackageJson(profileDir, name);
|
|
93
|
+
if (packageJson === null)
|
|
94
|
+
continue;
|
|
95
|
+
try {
|
|
96
|
+
const manifest = JSON.parse(readFileSync(packageJson, 'utf8'));
|
|
97
|
+
const order = manifest.dsh?.bundle?.order;
|
|
98
|
+
if (order === null || typeof order !== 'object' || Array.isArray(order))
|
|
99
|
+
continue;
|
|
100
|
+
const listOf = (value) => Array.isArray(value)
|
|
101
|
+
? value.filter((item) => typeof item === 'string')
|
|
102
|
+
: [];
|
|
103
|
+
const rule = {
|
|
104
|
+
name,
|
|
105
|
+
after: listOf(order.after),
|
|
106
|
+
before: listOf(order.before),
|
|
107
|
+
};
|
|
108
|
+
if (rule.after.length > 0 || rule.before.length > 0)
|
|
109
|
+
rules.push(rule);
|
|
110
|
+
}
|
|
111
|
+
catch { /* package unreadable — no rule */ }
|
|
112
|
+
}
|
|
113
|
+
return rules;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Check a bundle order against the declared before/after rules. Rules naming
|
|
117
|
+
* bundles outside `order` are ignored (a rule for a not-yet-installed bundle
|
|
118
|
+
* must not block the current stack).
|
|
119
|
+
* @returns every violated rule with a readable reason; [] when all hold.
|
|
120
|
+
*/
|
|
121
|
+
export function validateOrder(bundleNames, rules) {
|
|
122
|
+
const position = new Map(bundleNames.map((name, index) => [name, index]));
|
|
123
|
+
const conflicts = [];
|
|
124
|
+
for (const rule of rules) {
|
|
125
|
+
const pos = position.get(rule.name);
|
|
126
|
+
if (pos === undefined)
|
|
127
|
+
continue;
|
|
128
|
+
for (const other of rule.after) {
|
|
129
|
+
const otherPos = position.get(other);
|
|
130
|
+
if (otherPos === undefined)
|
|
131
|
+
continue;
|
|
132
|
+
if (otherPos >= pos) {
|
|
133
|
+
conflicts.push({
|
|
134
|
+
name: rule.name,
|
|
135
|
+
reason: `must load after ${other}, but ${other} is currently before/equal (position ${otherPos} vs ${pos})`,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
for (const other of rule.before) {
|
|
140
|
+
const otherPos = position.get(other);
|
|
141
|
+
if (otherPos === undefined)
|
|
142
|
+
continue;
|
|
143
|
+
if (otherPos <= pos) {
|
|
144
|
+
conflicts.push({
|
|
145
|
+
name: rule.name,
|
|
146
|
+
reason: `must load before ${other}, but ${other} is currently after/equal (position ${otherPos} vs ${pos})`,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return conflicts;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Merge a community-bundle permutation into the full stack. Official in-box
|
|
155
|
+
* bundles keep their EXACT positions (never moved); community bundles are
|
|
156
|
+
* replaced by `newOrder` in order of appearance. Pure — nothing is written.
|
|
157
|
+
* @returns the merged full stack, or the rejection reason when `newOrder` is
|
|
158
|
+
* not a permutation of the community bundles (duplicates, additions,
|
|
159
|
+
* omissions, official names).
|
|
160
|
+
*/
|
|
161
|
+
export function mergeOrder(bundles, newOrder) {
|
|
162
|
+
const communitySet = new Set(bundles.filter(name => !INBOX_BUNDLES.has(name)));
|
|
163
|
+
if (new Set(newOrder).size !== newOrder.length) {
|
|
164
|
+
return { ok: false, error: 'duplicate bundle names in the new order / 新顺序包含重复的 bundle' };
|
|
165
|
+
}
|
|
166
|
+
if (newOrder.length !== communitySet.size) {
|
|
167
|
+
return { ok: false, error: 'the new order must contain exactly the current community bundles / 新顺序必须恰好包含全部社区 bundle' };
|
|
168
|
+
}
|
|
169
|
+
for (const name of newOrder) {
|
|
170
|
+
if (!communitySet.has(name)) {
|
|
171
|
+
return { ok: false, error: `${name} is not a reorderable community bundle / ${name} 不是可排序的社区 bundle` };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const merged = [...bundles];
|
|
175
|
+
let cursor = 0;
|
|
176
|
+
for (let index = 0; index < merged.length; index += 1) {
|
|
177
|
+
const name = merged[index];
|
|
178
|
+
if (name === undefined || INBOX_BUNDLES.has(name))
|
|
179
|
+
continue;
|
|
180
|
+
merged[index] = newOrder[cursor];
|
|
181
|
+
cursor += 1;
|
|
182
|
+
}
|
|
183
|
+
return { ok: true, bundles: merged };
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Topologically sort the community bundles by their before/after rules — the
|
|
187
|
+
* "auto-fix" counterpart to validateOrder. Returns null when no declared rule
|
|
188
|
+
* applies to the current stack (nothing to suggest). With rules, Kahn's
|
|
189
|
+
* algorithm breaks ties by the CURRENT order: unconstrained bundles keep
|
|
190
|
+
* their current relative order and constrained bundles move only as far as
|
|
191
|
+
* the rules require — the suggestion is the minimal change that satisfies
|
|
192
|
+
* every rule, never an arbitrary canonical rewrite of a hand-picked order
|
|
193
|
+
* (issue #125 review).
|
|
194
|
+
* @returns the suggested community order, null when there are no rules, or a
|
|
195
|
+
* cycle report when the constraints cannot be satisfied (references to
|
|
196
|
+
* unlisted bundles ignored).
|
|
197
|
+
*/
|
|
198
|
+
export function suggestOrder(bundleNames, rules) {
|
|
199
|
+
const names = bundleNames.filter(name => !INBOX_BUNDLES.has(name));
|
|
200
|
+
const inOrder = new Set(names);
|
|
201
|
+
const active = rules.filter(rule => inOrder.has(rule.name));
|
|
202
|
+
// No rule applies to the current stack — nothing to suggest.
|
|
203
|
+
if (active.length === 0)
|
|
204
|
+
return null;
|
|
205
|
+
const position = new Map(names.map((name, index) => [name, index]));
|
|
206
|
+
// Constraint: "a must load before b" (from a.before or b.after) → edge a → b.
|
|
207
|
+
const beforeOf = new Map(); // name → bundles that must come after it
|
|
208
|
+
const deps = new Map(); // name → bundles that must come before it
|
|
209
|
+
for (const name of names) {
|
|
210
|
+
beforeOf.set(name, new Set());
|
|
211
|
+
deps.set(name, new Set());
|
|
212
|
+
}
|
|
213
|
+
const addEdge = (a, b) => {
|
|
214
|
+
if (!inOrder.has(a) || !inOrder.has(b) || a === b)
|
|
215
|
+
return;
|
|
216
|
+
beforeOf.get(a)?.add(b);
|
|
217
|
+
deps.get(b)?.add(a);
|
|
218
|
+
};
|
|
219
|
+
for (const rule of active) {
|
|
220
|
+
for (const other of rule.before)
|
|
221
|
+
addEdge(rule.name, other);
|
|
222
|
+
for (const other of rule.after)
|
|
223
|
+
addEdge(other, rule.name);
|
|
224
|
+
}
|
|
225
|
+
const remaining = new Map();
|
|
226
|
+
for (const [name, depsOf] of deps)
|
|
227
|
+
remaining.set(name, new Set(depsOf));
|
|
228
|
+
const ready = names.filter(name => (remaining.get(name)?.size ?? 0) === 0);
|
|
229
|
+
const ordered = [];
|
|
230
|
+
while (ready.length > 0) {
|
|
231
|
+
// Minimal-change tie-break: among the ready bundles, prefer the one that
|
|
232
|
+
// comes FIRST in the current order. Bundles the rules do not constrain
|
|
233
|
+
// therefore keep their current relative order; constrained bundles move
|
|
234
|
+
// only as far as the rules require (issue #125 review).
|
|
235
|
+
let best = 0;
|
|
236
|
+
for (let i = 1; i < ready.length; i += 1) {
|
|
237
|
+
const a = ready[i];
|
|
238
|
+
const b = ready[best];
|
|
239
|
+
if (a !== undefined && b !== undefined && (position.get(a) ?? 0) < (position.get(b) ?? 0))
|
|
240
|
+
best = i;
|
|
241
|
+
}
|
|
242
|
+
const name = ready.splice(best, 1)[0];
|
|
243
|
+
if (name === undefined)
|
|
244
|
+
break;
|
|
245
|
+
ordered.push(name);
|
|
246
|
+
for (const dependent of beforeOf.get(name) ?? []) {
|
|
247
|
+
const depsOf = remaining.get(dependent);
|
|
248
|
+
if (depsOf === undefined)
|
|
249
|
+
continue;
|
|
250
|
+
depsOf.delete(name);
|
|
251
|
+
if (depsOf.size === 0 && !ordered.includes(dependent) && !ready.includes(dependent))
|
|
252
|
+
ready.push(dependent);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (ordered.length < names.length) {
|
|
256
|
+
return { ok: false, cycle: names.filter(name => !ordered.includes(name)) };
|
|
257
|
+
}
|
|
258
|
+
return { ok: true, order: ordered };
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Apply a new community-bundle order to the profile manifest. The official
|
|
262
|
+
* in-box bundles keep their exact positions; `newOrder` must be a permutation
|
|
263
|
+
* of the current community bundles (no duplicates, no additions, no
|
|
264
|
+
* omissions). On any failure the manifest is left untouched.
|
|
265
|
+
* @returns the new full stack on success, or an error description.
|
|
266
|
+
*/
|
|
267
|
+
export function applyBundleOrder(profileDir, newOrder) {
|
|
268
|
+
const { bundles } = readBundleStack(profileDir);
|
|
269
|
+
const merged = mergeOrder(bundles, newOrder);
|
|
270
|
+
if (!merged.ok)
|
|
271
|
+
return merged;
|
|
272
|
+
try {
|
|
273
|
+
const manifestPath = join(profileDir, 'package.json');
|
|
274
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
275
|
+
manifest.dsh ??= {};
|
|
276
|
+
manifest.dsh.profile ??= {};
|
|
277
|
+
manifest.dsh.profile.bundles = merged.bundles;
|
|
278
|
+
writeFileAtomic(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
279
|
+
return merged;
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
283
|
+
}
|
|
284
|
+
}
|