@chatcode/cco-market 1.45.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +146 -0
  3. package/README.zh.md +143 -0
  4. package/UPDATE-API-V1.md +132 -0
  5. package/client/client.js +11534 -0
  6. package/cordis.patch.yml +5 -0
  7. package/lib/accelerate.js +184 -0
  8. package/lib/agents.js +36 -0
  9. package/lib/backup.js +572 -0
  10. package/lib/catalog-local-match.js +126 -0
  11. package/lib/catalog-npm.js +111 -0
  12. package/lib/changelog.js +242 -0
  13. package/lib/channels.js +64 -0
  14. package/lib/check.js +1067 -0
  15. package/lib/compatibility.js +198 -0
  16. package/lib/diagnostics.js +49 -0
  17. package/lib/discovery-compatibility.js +260 -0
  18. package/lib/dsh-cli.js +949 -0
  19. package/lib/dsh-install.js +108 -0
  20. package/lib/gist.js +352 -0
  21. package/lib/groups.js +97 -0
  22. package/lib/home-paths.js +42 -0
  23. package/lib/hot.js +519 -0
  24. package/lib/http.js +39 -0
  25. package/lib/index.js +97 -0
  26. package/lib/install.js +387 -0
  27. package/lib/log.js +197 -0
  28. package/lib/ndjson.js +154 -0
  29. package/lib/net.js +98 -0
  30. package/lib/order.js +284 -0
  31. package/lib/patch.js +521 -0
  32. package/lib/pnpm-compat.js +490 -0
  33. package/lib/presets.js +278 -0
  34. package/lib/profile.js +911 -0
  35. package/lib/region-probe.js +93 -0
  36. package/lib/regions.js +253 -0
  37. package/lib/registry.js +204 -0
  38. package/lib/restart.js +373 -0
  39. package/lib/routes.js +4372 -0
  40. package/lib/settings.js +109 -0
  41. package/lib/skill-market/api.js +119 -0
  42. package/lib/skill-market/config.js +29 -0
  43. package/lib/skill-market/installer.js +352 -0
  44. package/lib/skill-market/runtime.js +13 -0
  45. package/lib/skill-market/tui.js +119 -0
  46. package/lib/skill-market/types.js +1 -0
  47. package/lib/skill-market/web-routes.js +123 -0
  48. package/lib/snapshot.js +500 -0
  49. package/lib/source-migration.js +54 -0
  50. package/lib/sources.js +596 -0
  51. package/lib/store.js +91 -0
  52. package/lib/themes.js +102 -0
  53. package/lib/trial.js +116 -0
  54. package/lib/types/accelerate.d.ts +85 -0
  55. package/lib/types/agents.d.ts +22 -0
  56. package/lib/types/backup.d.ts +131 -0
  57. package/lib/types/catalog-local-match.d.ts +50 -0
  58. package/lib/types/catalog-npm.d.ts +47 -0
  59. package/lib/types/changelog.d.ts +102 -0
  60. package/lib/types/channels.d.ts +56 -0
  61. package/lib/types/check.d.ts +266 -0
  62. package/lib/types/compatibility.d.ts +64 -0
  63. package/lib/types/diagnostics.d.ts +29 -0
  64. package/lib/types/discovery-compatibility.d.ts +80 -0
  65. package/lib/types/dsh-cli.d.ts +324 -0
  66. package/lib/types/dsh-install.d.ts +46 -0
  67. package/lib/types/gist.d.ts +53 -0
  68. package/lib/types/groups.d.ts +34 -0
  69. package/lib/types/home-paths.d.ts +16 -0
  70. package/lib/types/hot.d.ts +221 -0
  71. package/lib/types/http.d.ts +12 -0
  72. package/lib/types/index.d.ts +14 -0
  73. package/lib/types/install.d.ts +172 -0
  74. package/lib/types/log.d.ts +43 -0
  75. package/lib/types/ndjson.d.ts +52 -0
  76. package/lib/types/net.d.ts +58 -0
  77. package/lib/types/order.d.ts +100 -0
  78. package/lib/types/patch.d.ts +129 -0
  79. package/lib/types/pnpm-compat.d.ts +93 -0
  80. package/lib/types/presets.d.ts +90 -0
  81. package/lib/types/profile.d.ts +253 -0
  82. package/lib/types/region-probe.d.ts +50 -0
  83. package/lib/types/regions.d.ts +122 -0
  84. package/lib/types/registry.d.ts +86 -0
  85. package/lib/types/restart.d.ts +194 -0
  86. package/lib/types/routes.d.ts +67 -0
  87. package/lib/types/settings.d.ts +78 -0
  88. package/lib/types/skill-market/api.d.ts +10 -0
  89. package/lib/types/skill-market/config.d.ts +3 -0
  90. package/lib/types/skill-market/installer.d.ts +23 -0
  91. package/lib/types/skill-market/runtime.d.ts +10 -0
  92. package/lib/types/skill-market/tui.d.ts +4 -0
  93. package/lib/types/skill-market/types.d.ts +71 -0
  94. package/lib/types/skill-market/web-routes.d.ts +19 -0
  95. package/lib/types/snapshot.d.ts +93 -0
  96. package/lib/types/source-migration.d.ts +11 -0
  97. package/lib/types/sources.d.ts +216 -0
  98. package/lib/types/store.d.ts +30 -0
  99. package/lib/types/themes.d.ts +40 -0
  100. package/lib/types/trial.d.ts +61 -0
  101. package/lib/types/update-api-v1.d.ts +66 -0
  102. package/lib/types/updates.d.ts +123 -0
  103. package/lib/types/verify.d.ts +139 -0
  104. package/lib/update-api-v1.js +215 -0
  105. package/lib/updates.js +361 -0
  106. package/lib/verify.js +453 -0
  107. package/package.json +121 -0
  108. package/src/accelerate.ts +213 -0
  109. package/src/agents.ts +43 -0
  110. package/src/backup.ts +583 -0
  111. package/src/catalog-local-match.ts +144 -0
  112. package/src/catalog-npm.ts +120 -0
  113. package/src/changelog.ts +282 -0
  114. package/src/channels.ts +70 -0
  115. package/src/check.ts +1239 -0
  116. package/src/client/CommentsModal.tsx +119 -0
  117. package/src/client/Diagnostics.tsx +907 -0
  118. package/src/client/ErrorBoundary.tsx +111 -0
  119. package/src/client/InstallToast.tsx +31 -0
  120. package/src/client/Market.module.css +830 -0
  121. package/src/client/MarketSection.tsx +5301 -0
  122. package/src/client/OperationsPanel.tsx +365 -0
  123. package/src/client/SettingsCard.tsx +621 -0
  124. package/src/client/SkillMarket.module.css +21 -0
  125. package/src/client/SkillMarketSection.tsx +162 -0
  126. package/src/client/comments.ts +54 -0
  127. package/src/client/globals.d.ts +13 -0
  128. package/src/client/index.ts +180 -0
  129. package/src/client/locales.ts +1112 -0
  130. package/src/client/market-data.ts +1321 -0
  131. package/src/client/operations.ts +201 -0
  132. package/src/client/preset-panel.tsx +263 -0
  133. package/src/client/primitives.d.ts +140 -0
  134. package/src/client/self-check.ts +147 -0
  135. package/src/client/snapshot-panel.tsx +244 -0
  136. package/src/compatibility.ts +237 -0
  137. package/src/diagnostics.ts +84 -0
  138. package/src/discovery-compatibility.ts +315 -0
  139. package/src/dsh-cli.ts +1126 -0
  140. package/src/dsh-install.ts +118 -0
  141. package/src/gist.ts +362 -0
  142. package/src/groups.ts +111 -0
  143. package/src/home-paths.ts +53 -0
  144. package/src/hot.ts +628 -0
  145. package/src/http.ts +41 -0
  146. package/src/index.ts +128 -0
  147. package/src/install.ts +420 -0
  148. package/src/log.ts +206 -0
  149. package/src/ndjson.ts +185 -0
  150. package/src/net.ts +106 -0
  151. package/src/order.ts +303 -0
  152. package/src/patch.ts +522 -0
  153. package/src/pnpm-compat.ts +527 -0
  154. package/src/presets.ts +344 -0
  155. package/src/profile.ts +940 -0
  156. package/src/region-probe.ts +97 -0
  157. package/src/regions.ts +310 -0
  158. package/src/registry.ts +250 -0
  159. package/src/restart.ts +396 -0
  160. package/src/routes.ts +4506 -0
  161. package/src/settings.ts +141 -0
  162. package/src/skill-market/api.ts +130 -0
  163. package/src/skill-market/config.ts +32 -0
  164. package/src/skill-market/installer.ts +337 -0
  165. package/src/skill-market/runtime.ts +14 -0
  166. package/src/skill-market/tui.ts +157 -0
  167. package/src/skill-market/types.ts +80 -0
  168. package/src/skill-market/web-routes.ts +132 -0
  169. package/src/snapshot.ts +532 -0
  170. package/src/source-migration.ts +61 -0
  171. package/src/sources.ts +565 -0
  172. package/src/store.ts +89 -0
  173. package/src/themes.ts +125 -0
  174. package/src/trial.ts +156 -0
  175. package/src/update-api-v1.ts +277 -0
  176. package/src/updates.ts +400 -0
  177. package/src/verify.ts +492 -0
@@ -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>;