@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
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Restart-free installs: mount a freshly installed plugin into the running
|
|
3
|
+
* composition through a market-owned Include subtree.
|
|
4
|
+
*
|
|
5
|
+
* Durable state stays with the profile's `dsh.profile.bundles` (reconciled by
|
|
6
|
+
* the dsh CLI at install time), so the next boot loads the plugin through the
|
|
7
|
+
* normal bundle layer. The subtree here exists only for the current process:
|
|
8
|
+
* its input files live under `<profile>/.dsh-market/` and are wiped on every
|
|
9
|
+
* boot, so a crash can never leave a file that collides with the bundle layer
|
|
10
|
+
* (inserting an id the bundle layer also inserts is a hard boot failure).
|
|
11
|
+
* `state.json` in the same directory is the market's own durable state
|
|
12
|
+
* (disable list + custom groups) and deliberately survives the wipe.
|
|
13
|
+
*
|
|
14
|
+
* The Include subclass suppresses `write()` — the loader otherwise persists
|
|
15
|
+
* tree changes back to the file it read (see dsh's agent-presets PresetTree
|
|
16
|
+
* for the in-tree precedent).
|
|
17
|
+
*/
|
|
18
|
+
import { type Channel } from './channels.ts';
|
|
19
|
+
import { type Region } from './regions.ts';
|
|
20
|
+
interface HotRow {
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
}
|
|
24
|
+
interface PluginHandle {
|
|
25
|
+
await(): Promise<unknown>;
|
|
26
|
+
dispose(): Promise<unknown> | void;
|
|
27
|
+
}
|
|
28
|
+
interface HotContext {
|
|
29
|
+
plugin(plugin: unknown, config: unknown): PluginHandle;
|
|
30
|
+
logger?: {
|
|
31
|
+
info?(message: string): void;
|
|
32
|
+
warn(message: string): void;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Insert rows of a plugin's bundle patch, or null when the patch contains
|
|
37
|
+
* anything beyond plain `id`/`name` insert rows (config blocks, disables,
|
|
38
|
+
* expressions) — those compositions fall back to restart activation.
|
|
39
|
+
*/
|
|
40
|
+
export declare function parseSimplePatch(patchText: string): HotRow[] | null;
|
|
41
|
+
/**
|
|
42
|
+
* Wipe leftover hot-mount inputs; call once when the market host starts.
|
|
43
|
+
* `state.json` (disable choices + groups) deliberately survives.
|
|
44
|
+
*/
|
|
45
|
+
export declare function cleanHotDir(profileDir: string): void;
|
|
46
|
+
/** Persisted market state: the generic disable list plus custom groups. */
|
|
47
|
+
export interface MarketState {
|
|
48
|
+
/** Plugins the user switched off; replayed at every boot. */
|
|
49
|
+
disabled: Set<string>;
|
|
50
|
+
/** User-defined plugin groups: group name → member package names. */
|
|
51
|
+
groups: Record<string, string[]>;
|
|
52
|
+
/** Display order of group names; "ungrouped" is implicit and never listed. */
|
|
53
|
+
groupOrder: string[];
|
|
54
|
+
/**
|
|
55
|
+
* The user's own one-line note per installed plugin (#347).
|
|
56
|
+
*
|
|
57
|
+
* A catalog description answers "what is this", written by its author for
|
|
58
|
+
* strangers and often in a language the reader did not pick. It cannot
|
|
59
|
+
* answer "why did I install this" — which is the question someone with
|
|
60
|
+
* forty plugins is actually asking. So a note REPLACES the description on
|
|
61
|
+
* that row, and the original stays one click away.
|
|
62
|
+
*
|
|
63
|
+
* Local state like the disable list and the groups beside it: never sent
|
|
64
|
+
* anywhere, and carried by a backup because it is part of how this profile
|
|
65
|
+
* is set up.
|
|
66
|
+
*
|
|
67
|
+
* Optional on the way IN: several callers build a state object from the
|
|
68
|
+
* few fields they own and hand it to writeMarketState. Requiring this one
|
|
69
|
+
* would make every such call a silent way to erase every note — the exact
|
|
70
|
+
* shape of #339, where a partial snapshot dropped a field nobody was
|
|
71
|
+
* thinking about. Omitting it means "leave them alone" instead.
|
|
72
|
+
*/
|
|
73
|
+
notes?: Record<string, string>;
|
|
74
|
+
/**
|
|
75
|
+
* The release channel the user PICKED, absent until they pick one.
|
|
76
|
+
*
|
|
77
|
+
* Absent is not the same as 'stable': with no choice on record the channel
|
|
78
|
+
* is derived from the running build, so installing a prerelease by hand
|
|
79
|
+
* puts you on the beta channel without a second step. Once chosen, the
|
|
80
|
+
* choice is the answer — including "stable" while a beta is running, which
|
|
81
|
+
* is how someone gets back off the channel.
|
|
82
|
+
*/
|
|
83
|
+
channel?: Channel;
|
|
84
|
+
/**
|
|
85
|
+
* The download region in force, absent until something has decided one.
|
|
86
|
+
*
|
|
87
|
+
* Absent means "nobody has decided yet", which is what triggers the
|
|
88
|
+
* one-time network probe. Once a value is here — whether the probe wrote
|
|
89
|
+
* it or the user picked it — no further probing happens, so the market
|
|
90
|
+
* does not silently change routes between runs.
|
|
91
|
+
*/
|
|
92
|
+
region?: Region;
|
|
93
|
+
/**
|
|
94
|
+
* Whether `region` was chosen by the probe rather than by the user.
|
|
95
|
+
*
|
|
96
|
+
* Only drives a one-time notice explaining why the market picked what it
|
|
97
|
+
* picked. A user who never learns a route was chosen for them has no way
|
|
98
|
+
* to know the setting exists, and no reason to look for it when something
|
|
99
|
+
* downloads oddly.
|
|
100
|
+
*/
|
|
101
|
+
regionAuto?: boolean;
|
|
102
|
+
/**
|
|
103
|
+
* Catalog entry URLs the user bookmarked for later install (#414).
|
|
104
|
+
* Keys are registry `url` strings, not package names — favorites are a
|
|
105
|
+
* pre-install list, unlike groups/notes which target installed packages.
|
|
106
|
+
*/
|
|
107
|
+
favorites?: string[];
|
|
108
|
+
/** User-supplied HTTPS prefix used when the built-in GitHub routes fail. */
|
|
109
|
+
githubProxy?: string;
|
|
110
|
+
}
|
|
111
|
+
/** Upper bound on bookmarked catalog URLs kept in state.json (#414). */
|
|
112
|
+
export declare const MAX_FAVORITES = 500;
|
|
113
|
+
/**
|
|
114
|
+
* Read the whole market state. Legacy `disabledSkins` (the pre-#60
|
|
115
|
+
* theme-only key) still loads; every new write uses the generic `disabled`
|
|
116
|
+
* key (#60).
|
|
117
|
+
*/
|
|
118
|
+
/** A note is a label, not a document: one line, bounded so state.json cannot
|
|
119
|
+
* grow without limit from a paste. */
|
|
120
|
+
export declare const MAX_NOTE = 200;
|
|
121
|
+
export declare function readMarketState(profileDir: string): MarketState;
|
|
122
|
+
/**
|
|
123
|
+
* Persist the whole market state.
|
|
124
|
+
*
|
|
125
|
+
* Every field a caller does not carry forward is taken from disk rather than
|
|
126
|
+
* dropped. Several callers legitimately know about only one part of the
|
|
127
|
+
* state — `writeMarketState(dir, { disabled, groups, groupOrder })` appears
|
|
128
|
+
* at five call sites in routes.ts — and before #435 that shape silently
|
|
129
|
+
* erased whatever else the user had chosen:
|
|
130
|
+
*
|
|
131
|
+
* - `channel` and `region` had no fallback at all, so toggling any plugin
|
|
132
|
+
* threw away the user's update channel and download region. Both are
|
|
133
|
+
* deliberate choices made through the settings card, and neither has a
|
|
134
|
+
* "clear it" path: once picked they only ever move to another value. So
|
|
135
|
+
* an absent one always means "the caller has nothing to say", never
|
|
136
|
+
* "the user unchose it".
|
|
137
|
+
* `notes` keeps its original rule — an explicit object wins, including an
|
|
138
|
+
* empty one, because deleting the last note has to be expressible. What made
|
|
139
|
+
* #435 lose notes was not this function but a caller: the note route wrote
|
|
140
|
+
* through a fresh read while the long-lived `marketState` in routes.ts still
|
|
141
|
+
* carried `notes: {}` from boot, and the next write from that object put the
|
|
142
|
+
* empty one back. The fix for that belongs at the call site, where the two
|
|
143
|
+
* copies are, not here — see the note route.
|
|
144
|
+
*
|
|
145
|
+
* Reading before writing costs one small JSON parse on an operation that is
|
|
146
|
+
* already doing filesystem work, and it is what makes "this function writes
|
|
147
|
+
* the whole document" safe for callers that only hold part of it.
|
|
148
|
+
*/
|
|
149
|
+
export declare function writeMarketState(profileDir: string, state: MarketState): void;
|
|
150
|
+
/** Plugins the user switched off; skipped by the boot re-mount. */
|
|
151
|
+
export declare function readDisabled(profileDir: string): Set<string>;
|
|
152
|
+
/** Persist just the disable list, preserving groups and order. */
|
|
153
|
+
export declare function writeDisabled(profileDir: string, disabled: Set<string>): void;
|
|
154
|
+
/** @deprecated theme-specific alias — kept for pre-#60 callers. */
|
|
155
|
+
export declare function readDisabledThemes(profileDir: string): Set<string>;
|
|
156
|
+
/** @deprecated theme-specific alias — kept for pre-#60 callers. */
|
|
157
|
+
export declare function writeDisabledThemes(profileDir: string, disabled: Set<string>): void;
|
|
158
|
+
/** Package names currently live through a market hot mount (patch or shim). */
|
|
159
|
+
export declare function listHotMounts(): string[];
|
|
160
|
+
/** Outcome of one hot-mount attempt; `reason` explains non-`ok` results. */
|
|
161
|
+
export interface HotMountResult {
|
|
162
|
+
ok: boolean;
|
|
163
|
+
/** Bilingual reason shown to the user instead of a bare restart banner. */
|
|
164
|
+
reason: string | null;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Dispose a plugin hot-mounted earlier in this session, removing it from the
|
|
168
|
+
* running composition immediately.
|
|
169
|
+
* @param packageName - package to unmount.
|
|
170
|
+
* @returns true when a live hot mount was found and disposed.
|
|
171
|
+
*/
|
|
172
|
+
export declare function hotUnmount(packageName: string): Promise<boolean>;
|
|
173
|
+
/**
|
|
174
|
+
* Mount `packageName` (just installed into the profile) into the running
|
|
175
|
+
* composition.
|
|
176
|
+
* @param ctx - market host context; the subtree unwinds with the market's fiber.
|
|
177
|
+
* @param profileDir - profile the package was installed into.
|
|
178
|
+
* @param packageName - installed package to activate.
|
|
179
|
+
* @returns whether the plugin is live without a restart, plus the reason
|
|
180
|
+
* when it is not (P0-2: the UI must distinguish "restart will fix it" from
|
|
181
|
+
* "this package can never hot-mount").
|
|
182
|
+
*/
|
|
183
|
+
export declare function hotMount(ctx: HotContext, profileDir: string, packageName: string): Promise<HotMountResult>;
|
|
184
|
+
/**
|
|
185
|
+
* Mount every installed client-only package (`dsh.client` without
|
|
186
|
+
* `dsh.bundle`) at market startup. The bundle reconcile skips these packages
|
|
187
|
+
* entirely, so without the market's shim their client bundles are unreachable
|
|
188
|
+
* in every boot — this is what makes them behave like normal plugins.
|
|
189
|
+
* @returns names that were mounted.
|
|
190
|
+
*/
|
|
191
|
+
export declare function mountClientOnlyDeps(ctx: HotContext, profileDir: string): Promise<string[]>;
|
|
192
|
+
/**
|
|
193
|
+
* Row ids and package names the user's own patch layer (cordis.patch.yml)
|
|
194
|
+
* already contains. Line-wise scan on purpose: the file may hold structures
|
|
195
|
+
* the market's strict patch parser rejects, but any mention of a row id or
|
|
196
|
+
* package name is enough to know the user manages it (#58).
|
|
197
|
+
*/
|
|
198
|
+
export declare function readUserPatchControls(profileDir: string): {
|
|
199
|
+
ids: Set<string>;
|
|
200
|
+
names: Set<string>;
|
|
201
|
+
};
|
|
202
|
+
/**
|
|
203
|
+
* Whether the user patch layer manages `name` — matched by exact package
|
|
204
|
+
* name or by the plugin-manager row-id convention (strip the leading @,
|
|
205
|
+
* non-alphanumerics to '-', lowercase).
|
|
206
|
+
*/
|
|
207
|
+
export declare function patchLayerManages(controls: {
|
|
208
|
+
ids: Set<string>;
|
|
209
|
+
names: Set<string>;
|
|
210
|
+
}, name: string): boolean;
|
|
211
|
+
/**
|
|
212
|
+
* Delete the market's own state directory.
|
|
213
|
+
*
|
|
214
|
+
* `cleanHotDir` wipes the ephemeral hot-mount inputs on every boot but
|
|
215
|
+
* deliberately preserves `state.json` — the disable list and custom groups
|
|
216
|
+
* are the user's durable choices. Uninstalling the market is the one moment
|
|
217
|
+
* where removing them is the right thing, and only when the user asked.
|
|
218
|
+
* @returns true when a directory was there to remove.
|
|
219
|
+
*/
|
|
220
|
+
export declare function purgeMarketState(profileDir: string): boolean;
|
|
221
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal HTTP helpers shared by every market route: JSON serialization,
|
|
3
|
+
* same-origin enforcement for mutating endpoints, and a size-capped JSON
|
|
4
|
+
* body reader.
|
|
5
|
+
*/
|
|
6
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
7
|
+
/** Write a JSON payload with no-store caching. */
|
|
8
|
+
export declare function sendJson(response: ServerResponse, status: number, payload: unknown): void;
|
|
9
|
+
/** True when the request's Origin matches its Host — required on every POST route. */
|
|
10
|
+
export declare function sameOrigin(request: IncomingMessage): boolean;
|
|
11
|
+
/** Read and parse a JSON request body, rejecting anything over 4 KiB. */
|
|
12
|
+
export declare function readJsonBody(request: IncomingMessage, maxBytes?: number): Promise<unknown>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-market host entry: mounts the market's HTTP routes once the profile
|
|
3
|
+
* composes the webServer and shell services.
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
6
|
+
import { type MarketConfig } from './routes.ts';
|
|
7
|
+
import type { SkillMarketConfig } from './skill-market/types.ts';
|
|
8
|
+
export declare const name = "dsh-market";
|
|
9
|
+
/** Optional cordis.yml configuration; profile defaults to `web`. */
|
|
10
|
+
export type Config = Partial<Pick<MarketConfig, 'profile' | 'allowRestart' | 'maxSnapshots'>> & {
|
|
11
|
+
/** Shared Web and terminal skill-market settings. */
|
|
12
|
+
readonly skillMarket?: SkillMarketConfig;
|
|
13
|
+
};
|
|
14
|
+
export declare function apply(ctx: Context, config?: Config): void;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install orchestration: collection-repo retargeting, post-install
|
|
3
|
+
* validation that keeps broken pieces from bricking the next boot, and
|
|
4
|
+
* update staleness detection. Every function takes the plugin runner as a
|
|
5
|
+
* parameter so tests can substitute a recording fake.
|
|
6
|
+
*/
|
|
7
|
+
import type { InstallResult, PluginRunner } from './dsh-cli.ts';
|
|
8
|
+
/** One-shot bypass for pnpm's fresh-release hold; scoped to a single command. */
|
|
9
|
+
export declare const RELEASE_AGE_OVERRIDE = "--config.minimumReleaseAge=0";
|
|
10
|
+
/**
|
|
11
|
+
* Longer per-request fetch timeout for one retried command. pnpm's default
|
|
12
|
+
* 60-second limit aborts large tarball downloads (github: sources fetch the
|
|
13
|
+
* WHOLE repo even for a `#path:` subdirectory plugin) on slow networks; a
|
|
14
|
+
* plain retry fails again at the same limit, so the recovery re-runs with
|
|
15
|
+
* this override once. Scoped to a single command like RELEASE_AGE_OVERRIDE.
|
|
16
|
+
*/
|
|
17
|
+
export declare const FETCH_TIMEOUT_OVERRIDE = "--config.fetchTimeout=600000";
|
|
18
|
+
/**
|
|
19
|
+
* Stop pnpm downloading a plugin's peer dependencies (#289 by @00080000).
|
|
20
|
+
*
|
|
21
|
+
* The last resort for a peer that cannot be downloaded because it does not
|
|
22
|
+
* exist on any registry: the dsh runtime injects several `@deepseek-ai/*`
|
|
23
|
+
* packages and never publishes them, and since pnpm 8 `auto-install-peers`
|
|
24
|
+
* defaults on, so pnpm walks the peer list and 404s on one.
|
|
25
|
+
*
|
|
26
|
+
* Only on the retry, never by default. Turning it off wholesale would also
|
|
27
|
+
* stop pnpm installing the peers a plugin legitimately needs from npm, and
|
|
28
|
+
* that failure would surface much later — as a missing module at runtime
|
|
29
|
+
* rather than a clear error at install time. Narrow beats early here.
|
|
30
|
+
*
|
|
31
|
+
* Verified against pnpm 10.29.3: `peerDependencyRules.ignoreMissing` does
|
|
32
|
+
* NOT prevent the fetch (it only silences the warning), so this flag is the
|
|
33
|
+
* only lever that actually works.
|
|
34
|
+
*/
|
|
35
|
+
export declare const AUTO_INSTALL_PEERS_OFF = "--config.auto-install-peers=false";
|
|
36
|
+
/**
|
|
37
|
+
* Whether an unresolvable package is a host peer pnpm went looking for on
|
|
38
|
+
* its own, rather than something the profile actually asks for.
|
|
39
|
+
*
|
|
40
|
+
* The same 404 means two different things and wants two different answers.
|
|
41
|
+
* A `@deepseek-ai/*` package that IS in the profile manifest is a ghost
|
|
42
|
+
* entry — left by an earlier failed operation, or hand-added — and the user
|
|
43
|
+
* has to remove that line; retrying would only fail again. One that is NOT
|
|
44
|
+
* in the manifest was never asked for by anybody: pnpm reached it by walking
|
|
45
|
+
* an installed plugin's peerDependencies, which in this ecosystem name what
|
|
46
|
+
* the runtime provides rather than what npm carries.
|
|
47
|
+
*
|
|
48
|
+
* Reading the manifest is what separates them, so this cannot live in the
|
|
49
|
+
* pure classifier.
|
|
50
|
+
*/
|
|
51
|
+
export declare function isUnpublishedHostPeer(pkg: string | undefined, profile: string, explicitDir?: string): boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Run one plugin command with automatic recovery from three known pnpm traps:
|
|
54
|
+
*
|
|
55
|
+
* - pnpm-major drift (#20 bug 2): a modules directory built by a different
|
|
56
|
+
* pnpm major fails mutation; pnpm's documented remedy is one `install` to
|
|
57
|
+
* recreate it — do that silently and retry the original command once.
|
|
58
|
+
* - release-age lockfile lock (#39): once a too-young release is in the
|
|
59
|
+
* lockfile, pnpm 11 rejects EVERY later add/remove during verification —
|
|
60
|
+
* retry once with the one-shot minimumReleaseAge bypass (safe: the young
|
|
61
|
+
* package is already installed; the bypass only lets pnpm touch the
|
|
62
|
+
* lockfile again).
|
|
63
|
+
* - per-request fetch timeout: large tarballs (github: sources fetch the
|
|
64
|
+
* whole repo even for a `#path:` subdirectory) on slow networks blow
|
|
65
|
+
* pnpm's default 60-second limit; a plain retry fails again at the same
|
|
66
|
+
* limit, so retry once with a longer fetchTimeout.
|
|
67
|
+
*
|
|
68
|
+
* Any recognized failure that survives gets its bilingual explanation
|
|
69
|
+
* appended to stderr so the UI shows an actionable message instead of a
|
|
70
|
+
* wall of text (#20 bug 3). Cancelled runs are never recovered.
|
|
71
|
+
*/
|
|
72
|
+
export declare function withHoistRecovery(run: PluginRunner, profile: string, pluginArgs: string[], profileDirectory?: string): Promise<InstallResult>;
|
|
73
|
+
/**
|
|
74
|
+
* Whether pnpm never started at all, so the profile cannot have been touched.
|
|
75
|
+
*
|
|
76
|
+
* Worth its own question because the update route answers a failed run by
|
|
77
|
+
* reinstalling the previous build and reporting loudly when it cannot verify
|
|
78
|
+
* that (#502 by @Ztyss): three updates in a row told the user their profile
|
|
79
|
+
* might be broken and to inspect it before restarting, when in fact nothing
|
|
80
|
+
* had been written — the command line could not launch pnpm, so package.json
|
|
81
|
+
* and node_modules were exactly as they had been.
|
|
82
|
+
* @param result - the failed run.
|
|
83
|
+
* @returns true when the failure happened before pnpm could run.
|
|
84
|
+
*/
|
|
85
|
+
export declare function pnpmNeverStarted(result: InstallResult): boolean;
|
|
86
|
+
/**
|
|
87
|
+
* The most specific description of a failed run available, for logs.
|
|
88
|
+
*
|
|
89
|
+
* pnpm's structured error beats the stderr tail whenever there is one — see
|
|
90
|
+
* withHoistRecovery above for why the tail is nearly worthless here.
|
|
91
|
+
*/
|
|
92
|
+
export declare function failureDetail(result: InstallResult, limit?: number): string;
|
|
93
|
+
/**
|
|
94
|
+
* Some registry entries point at collection repos whose actual plugin lives
|
|
95
|
+
* in a subdirectory — the root has no package.json (or a workspace root with
|
|
96
|
+
* no dsh surface), and pnpm installs the bare fileset with exit 0. Detect
|
|
97
|
+
* that junk install, drop it, and re-add each plugin subdirectory through
|
|
98
|
+
* pnpm's `#path:` selector (#18).
|
|
99
|
+
* @returns overall success (true when nothing needed retargeting).
|
|
100
|
+
*/
|
|
101
|
+
export declare function retargetCollections(run: PluginRunner, profile: string, before: Set<string>, target: string, explicitDir?: string): Promise<boolean>;
|
|
102
|
+
/**
|
|
103
|
+
* Fake-success guard (#18): validate every package the install added. A
|
|
104
|
+
* piece without a dsh manifest or without its declared entry artifact
|
|
105
|
+
* (source-only checkout, build blocked by pnpm allowBuilds) would brick the
|
|
106
|
+
* next boot, so it is removed on the spot.
|
|
107
|
+
*
|
|
108
|
+
* Since #122 this also covers duplicate loader entry ids: cordis refuses to
|
|
109
|
+
* load a tree containing two entries with one id, so a TUI bundle landing in
|
|
110
|
+
* a web profile (both declare `id: storage`) leaves DSH unable to START —
|
|
111
|
+
* an error naming neither plugin, from which the market's own page is
|
|
112
|
+
* unreachable. Such a package is removed like any other bricking piece.
|
|
113
|
+
* @returns names added by this run, names kept, names removed as broken,
|
|
114
|
+
* and the id conflicts found. `added` is reported separately from `keep`
|
|
115
|
+
* because an EMPTY `added` is a different failure from "everything added was
|
|
116
|
+
* unloadable": it means the install reported success without touching the
|
|
117
|
+
* profile at all, which is a broken plugin-command channel rather than
|
|
118
|
+
* anything wrong with the plugin (#258).
|
|
119
|
+
*/
|
|
120
|
+
export declare function validateAddedPlugins(run: PluginRunner, profile: string, before: Set<string>, explicitDir?: string): Promise<{
|
|
121
|
+
added: string[];
|
|
122
|
+
keep: string[];
|
|
123
|
+
removedBroken: string[];
|
|
124
|
+
conflicts: {
|
|
125
|
+
name: string;
|
|
126
|
+
id: string;
|
|
127
|
+
owner: string;
|
|
128
|
+
}[];
|
|
129
|
+
}>;
|
|
130
|
+
/**
|
|
131
|
+
* Group flat `{id, owner}` conflict hits by the installed plugin that owns
|
|
132
|
+
* them. What the user has to decide is which PLUGINS to uninstall, not which
|
|
133
|
+
* ids to resolve, so one row per owner is the unit the market renders and
|
|
134
|
+
* acts on. Flattening the other way (one row per id) also misattributes when
|
|
135
|
+
* a candidate clashes with several installed plugins at once.
|
|
136
|
+
* @param conflicts flat hits as returned by {@link validateAddedPlugins}.
|
|
137
|
+
* @returns one entry per owner, owners and ids both in first-seen order.
|
|
138
|
+
*/
|
|
139
|
+
export declare function groupConflictsByOwner(conflicts: readonly {
|
|
140
|
+
id: string;
|
|
141
|
+
owner: string;
|
|
142
|
+
}[]): {
|
|
143
|
+
owner: string;
|
|
144
|
+
ids: string[];
|
|
145
|
+
}[];
|
|
146
|
+
/**
|
|
147
|
+
* Whether a clean-exit update actually changed nothing — pnpm's
|
|
148
|
+
* minimumReleaseAge silently keeps the old version and exits 0 when the new
|
|
149
|
+
* release is "too young" (#13, #22), so a clean exit alone does not mean the
|
|
150
|
+
* update happened.
|
|
151
|
+
*/
|
|
152
|
+
export declare function isStaleUpdate(check: {
|
|
153
|
+
isGit: boolean;
|
|
154
|
+
beforeVersion: string | null;
|
|
155
|
+
afterVersion: string | null;
|
|
156
|
+
beforeCommit: string | null;
|
|
157
|
+
afterCommit: string | null;
|
|
158
|
+
}): boolean;
|
|
159
|
+
/**
|
|
160
|
+
* The package pnpm's fetcher refused to prepare because its build script is
|
|
161
|
+
* not allowlisted — `The git-hosted package "name@2.8.0" needs to execute
|
|
162
|
+
* build scripts but is not in the "allowBuilds" allowlist.` Null when the
|
|
163
|
+
* output is not this failure. Unlike ignored-builds, the package is NOT in
|
|
164
|
+
* node_modules yet (the fetcher rejects before materialization, #68).
|
|
165
|
+
*/
|
|
166
|
+
export declare function parsePrepareNotAllowed(stdout: string, stderr: string): string | null;
|
|
167
|
+
/**
|
|
168
|
+
* Package names pnpm reported as having their build scripts ignored
|
|
169
|
+
* ("Ignored build scripts: esbuild, koffi."). Empty when none.
|
|
170
|
+
* (#6 by @qichuang321.)
|
|
171
|
+
*/
|
|
172
|
+
export declare function parseIgnoredBuilds(stdout: string, stderr: string): string[];
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event log for issue reports: what the market did and how it failed,
|
|
3
|
+
* exportable as plain text from `/dsh-market/logs`.
|
|
4
|
+
*
|
|
5
|
+
* Privacy: entries are sanitized on write — the home directory collapses to
|
|
6
|
+
* `~`, and common credential shapes (API keys, GitHub/npm tokens, bearer
|
|
7
|
+
* headers) are masked. The in-memory buffer dies with the process and holds
|
|
8
|
+
* at most {@link MAX_ENTRIES} entries; a process that also configures a
|
|
9
|
+
* persistent sink appends every event there, capped at
|
|
10
|
+
* {@link PERSISTENT_MAX_BYTES}, because the failures worth reporting most —
|
|
11
|
+
* the ones that only appear after a restart — used to take their story with
|
|
12
|
+
* them when the process died (#341).
|
|
13
|
+
*/
|
|
14
|
+
export type LogLevel = 'info' | 'warn' | 'error';
|
|
15
|
+
/**
|
|
16
|
+
* Append one event, sanitized and truncated.
|
|
17
|
+
* @param level - severity for the export listing.
|
|
18
|
+
* @param event - short machine-ish event name (e.g. `install`, `hot-mount`).
|
|
19
|
+
* @param detail - free-form context; credentials and home paths are masked.
|
|
20
|
+
*/
|
|
21
|
+
export declare function logEvent(level: LogLevel, event: string, detail: string): void;
|
|
22
|
+
/**
|
|
23
|
+
* Append events to a profile-owned file, or stop doing so.
|
|
24
|
+
*
|
|
25
|
+
* Called once per mount with `<profile>/.dsh-market/log.ndjson` and with
|
|
26
|
+
* `null` on dispose. An oversized file is trimmed to its newest half on
|
|
27
|
+
* configure, so one long-lived profile cannot grow it without bound.
|
|
28
|
+
* @param file - the sink file, or null to disable persistence.
|
|
29
|
+
*/
|
|
30
|
+
export declare function configurePersistentLog(file: string | null): void;
|
|
31
|
+
/**
|
|
32
|
+
* The newest persisted lines, for the export's prior-session section.
|
|
33
|
+
* @param file - the sink file to read.
|
|
34
|
+
* @param maxLines - how many trailing lines to return.
|
|
35
|
+
* @returns parsed-or-raw lines, newest last; empty when nothing is readable.
|
|
36
|
+
*/
|
|
37
|
+
export declare function readPersistentLog(file: string, maxLines?: number): string[];
|
|
38
|
+
/**
|
|
39
|
+
* The export document for bug reports.
|
|
40
|
+
* @param header - environment lines to prepend (version, platform — no paths).
|
|
41
|
+
* @returns plain text, newest entry last.
|
|
42
|
+
*/
|
|
43
|
+
export declare function exportLogs(header: Record<string, string>, snapshot?: string[], priorSessions?: string[]): string;
|
|
@@ -0,0 +1,52 @@
|
|
|
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 type ProgressPhase = 'resolving' | 'downloading' | 'linking' | 'building' | null;
|
|
21
|
+
export interface ProgressSnapshot {
|
|
22
|
+
phase: ProgressPhase;
|
|
23
|
+
/** Distinct packages resolved/fetched so far. */
|
|
24
|
+
done: number;
|
|
25
|
+
/** Package-count total; pnpm 11's stream carries no per-run total. */
|
|
26
|
+
total: number | null;
|
|
27
|
+
/** Package being fetched/linked right now (its `packageId`), or null. */
|
|
28
|
+
currentPackage: string | null;
|
|
29
|
+
/** Bytes downloaded for the current package, when known. */
|
|
30
|
+
downloaded: number | null;
|
|
31
|
+
/** Total bytes for the current package, when known. */
|
|
32
|
+
size: number | null;
|
|
33
|
+
/** True once any structured pnpm event was parsed (ndjson works). */
|
|
34
|
+
seen: boolean;
|
|
35
|
+
/** Last fatal error message carried by the stream, if any. */
|
|
36
|
+
error: string | null;
|
|
37
|
+
/**
|
|
38
|
+
* pnpm's own error CODE from that same event (`ERR_PNPM_…`), when it
|
|
39
|
+
* carried one. The code is the part that classifies reliably: the message
|
|
40
|
+
* is prose and gets reworded between pnpm releases (#244).
|
|
41
|
+
*/
|
|
42
|
+
errorCode: string | null;
|
|
43
|
+
/** Package names pnpm reported as having ignored build scripts. */
|
|
44
|
+
ignoredBuilds: string[];
|
|
45
|
+
}
|
|
46
|
+
export declare function emptyProgress(): ProgressSnapshot;
|
|
47
|
+
export interface ProgressTracker {
|
|
48
|
+
readonly snapshot: ProgressSnapshot;
|
|
49
|
+
feed(line: string): void;
|
|
50
|
+
reset(): void;
|
|
51
|
+
}
|
|
52
|
+
export declare function createProgressTracker(): ProgressTracker;
|
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
/**
|
|
23
|
+
* The proxy this process would use for the catalog, if any.
|
|
24
|
+
*
|
|
25
|
+
* The standard variables mirror `EnvHttpProxyAgent`'s own resolution
|
|
26
|
+
* deliberately, rather than picking the order that reads best, because the
|
|
27
|
+
* same answer does two jobs: it decides whether to route through undici at
|
|
28
|
+
* all, and it is what the failure message CLAIMS was tried. A helper that
|
|
29
|
+
* named a proxy undici would not have used would put a false statement in
|
|
30
|
+
* every bug report. `npm_config_*` is an additional source on top of that:
|
|
31
|
+
* npm holds its proxy in its own config namespace (a machine set up with
|
|
32
|
+
* `npm config set proxy` has the proxy in `npm_config_proxy` and nowhere
|
|
33
|
+
* else), and undici does not read it — so it is resolved here and handed to
|
|
34
|
+
* the agent explicitly in `marketFetch`.
|
|
35
|
+
*
|
|
36
|
+
* Three details are undici's, not ours (env-http-proxy-agent.js):
|
|
37
|
+
* - lowercase wins over uppercase (`https_proxy ?? HTTPS_PROXY`)
|
|
38
|
+
* - an https request falls back to the http proxy when no https one is set
|
|
39
|
+
* - the value is tested for truthiness, so `HTTPS_PROXY=` — which is how
|
|
40
|
+
* people turn a proxy off — falls through instead of masking HTTP_PROXY
|
|
41
|
+
*
|
|
42
|
+
* Blank-is-unset is ours, and only widens that last one: undici would hand a
|
|
43
|
+
* whitespace-only value to `new URL()` and throw out of the constructor.
|
|
44
|
+
* Scheme-less host:port values are also common in Windows proxy fields and
|
|
45
|
+
* npm config; proxy agents require URLs, so those default to `http://`.
|
|
46
|
+
*/
|
|
47
|
+
export declare function configuredProxy(): string | null;
|
|
48
|
+
/**
|
|
49
|
+
* Fetch through the proxy this machine is configured to use.
|
|
50
|
+
*
|
|
51
|
+
* Falls back to the global fetch when no proxy is set, which keeps the
|
|
52
|
+
* ordinary case on the runtime's own path rather than routing it through a
|
|
53
|
+
* second HTTP stack for no reason.
|
|
54
|
+
*/
|
|
55
|
+
export declare function marketFetch(url: string, init?: {
|
|
56
|
+
signal?: AbortSignal;
|
|
57
|
+
headers?: Record<string, string>;
|
|
58
|
+
}): Promise<Response>;
|