@chatcode/cco-market 1.45.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +146 -0
  3. package/README.zh.md +143 -0
  4. package/UPDATE-API-V1.md +132 -0
  5. package/client/client.js +11534 -0
  6. package/cordis.patch.yml +5 -0
  7. package/lib/accelerate.js +184 -0
  8. package/lib/agents.js +36 -0
  9. package/lib/backup.js +572 -0
  10. package/lib/catalog-local-match.js +126 -0
  11. package/lib/catalog-npm.js +111 -0
  12. package/lib/changelog.js +242 -0
  13. package/lib/channels.js +64 -0
  14. package/lib/check.js +1067 -0
  15. package/lib/compatibility.js +198 -0
  16. package/lib/diagnostics.js +49 -0
  17. package/lib/discovery-compatibility.js +260 -0
  18. package/lib/dsh-cli.js +949 -0
  19. package/lib/dsh-install.js +108 -0
  20. package/lib/gist.js +352 -0
  21. package/lib/groups.js +97 -0
  22. package/lib/home-paths.js +42 -0
  23. package/lib/hot.js +519 -0
  24. package/lib/http.js +39 -0
  25. package/lib/index.js +97 -0
  26. package/lib/install.js +387 -0
  27. package/lib/log.js +197 -0
  28. package/lib/ndjson.js +154 -0
  29. package/lib/net.js +98 -0
  30. package/lib/order.js +284 -0
  31. package/lib/patch.js +521 -0
  32. package/lib/pnpm-compat.js +490 -0
  33. package/lib/presets.js +278 -0
  34. package/lib/profile.js +911 -0
  35. package/lib/region-probe.js +93 -0
  36. package/lib/regions.js +253 -0
  37. package/lib/registry.js +204 -0
  38. package/lib/restart.js +373 -0
  39. package/lib/routes.js +4372 -0
  40. package/lib/settings.js +109 -0
  41. package/lib/skill-market/api.js +119 -0
  42. package/lib/skill-market/config.js +29 -0
  43. package/lib/skill-market/installer.js +352 -0
  44. package/lib/skill-market/runtime.js +13 -0
  45. package/lib/skill-market/tui.js +119 -0
  46. package/lib/skill-market/types.js +1 -0
  47. package/lib/skill-market/web-routes.js +123 -0
  48. package/lib/snapshot.js +500 -0
  49. package/lib/source-migration.js +54 -0
  50. package/lib/sources.js +596 -0
  51. package/lib/store.js +91 -0
  52. package/lib/themes.js +102 -0
  53. package/lib/trial.js +116 -0
  54. package/lib/types/accelerate.d.ts +85 -0
  55. package/lib/types/agents.d.ts +22 -0
  56. package/lib/types/backup.d.ts +131 -0
  57. package/lib/types/catalog-local-match.d.ts +50 -0
  58. package/lib/types/catalog-npm.d.ts +47 -0
  59. package/lib/types/changelog.d.ts +102 -0
  60. package/lib/types/channels.d.ts +56 -0
  61. package/lib/types/check.d.ts +266 -0
  62. package/lib/types/compatibility.d.ts +64 -0
  63. package/lib/types/diagnostics.d.ts +29 -0
  64. package/lib/types/discovery-compatibility.d.ts +80 -0
  65. package/lib/types/dsh-cli.d.ts +324 -0
  66. package/lib/types/dsh-install.d.ts +46 -0
  67. package/lib/types/gist.d.ts +53 -0
  68. package/lib/types/groups.d.ts +34 -0
  69. package/lib/types/home-paths.d.ts +16 -0
  70. package/lib/types/hot.d.ts +221 -0
  71. package/lib/types/http.d.ts +12 -0
  72. package/lib/types/index.d.ts +14 -0
  73. package/lib/types/install.d.ts +172 -0
  74. package/lib/types/log.d.ts +43 -0
  75. package/lib/types/ndjson.d.ts +52 -0
  76. package/lib/types/net.d.ts +58 -0
  77. package/lib/types/order.d.ts +100 -0
  78. package/lib/types/patch.d.ts +129 -0
  79. package/lib/types/pnpm-compat.d.ts +93 -0
  80. package/lib/types/presets.d.ts +90 -0
  81. package/lib/types/profile.d.ts +253 -0
  82. package/lib/types/region-probe.d.ts +50 -0
  83. package/lib/types/regions.d.ts +122 -0
  84. package/lib/types/registry.d.ts +86 -0
  85. package/lib/types/restart.d.ts +194 -0
  86. package/lib/types/routes.d.ts +67 -0
  87. package/lib/types/settings.d.ts +78 -0
  88. package/lib/types/skill-market/api.d.ts +10 -0
  89. package/lib/types/skill-market/config.d.ts +3 -0
  90. package/lib/types/skill-market/installer.d.ts +23 -0
  91. package/lib/types/skill-market/runtime.d.ts +10 -0
  92. package/lib/types/skill-market/tui.d.ts +4 -0
  93. package/lib/types/skill-market/types.d.ts +71 -0
  94. package/lib/types/skill-market/web-routes.d.ts +19 -0
  95. package/lib/types/snapshot.d.ts +93 -0
  96. package/lib/types/source-migration.d.ts +11 -0
  97. package/lib/types/sources.d.ts +216 -0
  98. package/lib/types/store.d.ts +30 -0
  99. package/lib/types/themes.d.ts +40 -0
  100. package/lib/types/trial.d.ts +61 -0
  101. package/lib/types/update-api-v1.d.ts +66 -0
  102. package/lib/types/updates.d.ts +123 -0
  103. package/lib/types/verify.d.ts +139 -0
  104. package/lib/update-api-v1.js +215 -0
  105. package/lib/updates.js +361 -0
  106. package/lib/verify.js +453 -0
  107. package/package.json +121 -0
  108. package/src/accelerate.ts +213 -0
  109. package/src/agents.ts +43 -0
  110. package/src/backup.ts +583 -0
  111. package/src/catalog-local-match.ts +144 -0
  112. package/src/catalog-npm.ts +120 -0
  113. package/src/changelog.ts +282 -0
  114. package/src/channels.ts +70 -0
  115. package/src/check.ts +1239 -0
  116. package/src/client/CommentsModal.tsx +119 -0
  117. package/src/client/Diagnostics.tsx +907 -0
  118. package/src/client/ErrorBoundary.tsx +111 -0
  119. package/src/client/InstallToast.tsx +31 -0
  120. package/src/client/Market.module.css +830 -0
  121. package/src/client/MarketSection.tsx +5301 -0
  122. package/src/client/OperationsPanel.tsx +365 -0
  123. package/src/client/SettingsCard.tsx +621 -0
  124. package/src/client/SkillMarket.module.css +21 -0
  125. package/src/client/SkillMarketSection.tsx +162 -0
  126. package/src/client/comments.ts +54 -0
  127. package/src/client/globals.d.ts +13 -0
  128. package/src/client/index.ts +180 -0
  129. package/src/client/locales.ts +1112 -0
  130. package/src/client/market-data.ts +1321 -0
  131. package/src/client/operations.ts +201 -0
  132. package/src/client/preset-panel.tsx +263 -0
  133. package/src/client/primitives.d.ts +140 -0
  134. package/src/client/self-check.ts +147 -0
  135. package/src/client/snapshot-panel.tsx +244 -0
  136. package/src/compatibility.ts +237 -0
  137. package/src/diagnostics.ts +84 -0
  138. package/src/discovery-compatibility.ts +315 -0
  139. package/src/dsh-cli.ts +1126 -0
  140. package/src/dsh-install.ts +118 -0
  141. package/src/gist.ts +362 -0
  142. package/src/groups.ts +111 -0
  143. package/src/home-paths.ts +53 -0
  144. package/src/hot.ts +628 -0
  145. package/src/http.ts +41 -0
  146. package/src/index.ts +128 -0
  147. package/src/install.ts +420 -0
  148. package/src/log.ts +206 -0
  149. package/src/ndjson.ts +185 -0
  150. package/src/net.ts +106 -0
  151. package/src/order.ts +303 -0
  152. package/src/patch.ts +522 -0
  153. package/src/pnpm-compat.ts +527 -0
  154. package/src/presets.ts +344 -0
  155. package/src/profile.ts +940 -0
  156. package/src/region-probe.ts +97 -0
  157. package/src/regions.ts +310 -0
  158. package/src/registry.ts +250 -0
  159. package/src/restart.ts +396 -0
  160. package/src/routes.ts +4506 -0
  161. package/src/settings.ts +141 -0
  162. package/src/skill-market/api.ts +130 -0
  163. package/src/skill-market/config.ts +32 -0
  164. package/src/skill-market/installer.ts +337 -0
  165. package/src/skill-market/runtime.ts +14 -0
  166. package/src/skill-market/tui.ts +157 -0
  167. package/src/skill-market/types.ts +80 -0
  168. package/src/skill-market/web-routes.ts +132 -0
  169. package/src/snapshot.ts +532 -0
  170. package/src/source-migration.ts +61 -0
  171. package/src/sources.ts +565 -0
  172. package/src/store.ts +89 -0
  173. package/src/themes.ts +125 -0
  174. package/src/trial.ts +156 -0
  175. package/src/update-api-v1.ts +277 -0
  176. package/src/updates.ts +400 -0
  177. package/src/verify.ts +492 -0
package/src/log.ts ADDED
@@ -0,0 +1,206 @@
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
+
15
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
16
+ import { dirname } from 'node:path'
17
+ import { homedir } from 'node:os'
18
+
19
+ export type LogLevel = 'info' | 'warn' | 'error'
20
+
21
+ interface LogEntry {
22
+ at: string
23
+ level: LogLevel
24
+ event: string
25
+ detail: string
26
+ }
27
+
28
+ const MAX_ENTRIES = 200
29
+ const DETAIL_MAX = 600
30
+ const PERSISTENT_MAX_BYTES = 256 * 1024
31
+
32
+ const entries: LogEntry[] = []
33
+ let persistentFile: string | null = null
34
+ /** Bytes in the sink file, tracked so the cap holds without a stat per event. */
35
+ let persistentBytes = 0
36
+
37
+ function sanitize(text: string): string {
38
+ return text
39
+ .replaceAll(homedir(), '~')
40
+ // Log-injection guard: control characters (newlines above all) would
41
+ // forge extra lines in the exported log file. The #98 routes pass
42
+ // user-supplied names into logEvent (bundle-order order entries, trial
43
+ // messages), so strip them at the single choke point (issue #98
44
+ // analysis: log filtering).
45
+ .replace(/[\u0000-\u001f\u007f]/g, '')
46
+ .replace(/sk-[A-Za-z0-9_-]{8,}/g, 'sk-***')
47
+ .replace(/gh[pousr]_[A-Za-z0-9]{16,}/g, 'gh*_***')
48
+ .replace(/npm_[A-Za-z0-9]{16,}/g, 'npm_***')
49
+ .replace(/bearer\s+\S+/gi, 'Bearer ***')
50
+ .replace(/(authorization|token|apikey|api-key|password)(["':=\s]+)\S+/gi, '$1$2***')
51
+ }
52
+
53
+ /**
54
+ * Append one event, sanitized and truncated.
55
+ * @param level - severity for the export listing.
56
+ * @param event - short machine-ish event name (e.g. `install`, `hot-mount`).
57
+ * @param detail - free-form context; credentials and home paths are masked.
58
+ */
59
+ export function logEvent(level: LogLevel, event: string, detail: string): void {
60
+ const entry = {
61
+ at: new Date().toISOString(),
62
+ level,
63
+ event,
64
+ detail: sanitize(detail).slice(0, DETAIL_MAX),
65
+ }
66
+ entries.push(entry)
67
+ if (entries.length > MAX_ENTRIES) entries.splice(0, entries.length - MAX_ENTRIES)
68
+ if (persistentFile === null) return
69
+ try {
70
+ const line = `${JSON.stringify(entry)}\n`
71
+ appendFileSync(persistentFile, line)
72
+ persistentBytes += line.length
73
+ // Trimming only on mount left the cap unenforced for the life of a
74
+ // process: 20k events grew the file to 3.2 MB in a measurement, and a
75
+ // retry loop is exactly the situation that both logs hardest and never
76
+ // restarts. Re-trim in place once the ceiling is crossed.
77
+ if (persistentBytes > PERSISTENT_MAX_BYTES) trimPersistentLog(persistentFile)
78
+ } catch {
79
+ // Only append failures reach this: a read-only or full disk. The
80
+ // in-memory log still serves this session's export; persistence
81
+ // disables itself so one bad write cannot break every future event.
82
+ persistentFile = null
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Rewrite the sink keeping only its newest half, and resync the byte count.
88
+ * @param file - the sink file to trim in place.
89
+ */
90
+ function trimPersistentLog(file: string): void {
91
+ const lines = readFileSync(file, 'utf8').split('\n').filter(line => line !== '')
92
+ const kept: string[] = []
93
+ let bytes = 0
94
+ for (let index = lines.length - 1; index >= 0 && bytes <= PERSISTENT_MAX_BYTES / 2; index -= 1) {
95
+ kept.unshift(`${lines[index]!}\n`)
96
+ bytes += lines[index]!.length + 1
97
+ }
98
+ writeFileSync(file, kept.join(''))
99
+ persistentBytes = bytes
100
+ }
101
+
102
+ /**
103
+ * Append events to a profile-owned file, or stop doing so.
104
+ *
105
+ * Called once per mount with `<profile>/.dsh-market/log.ndjson` and with
106
+ * `null` on dispose. An oversized file is trimmed to its newest half on
107
+ * configure, so one long-lived profile cannot grow it without bound.
108
+ * @param file - the sink file, or null to disable persistence.
109
+ */
110
+ export function configurePersistentLog(file: string | null): void {
111
+ persistentFile = file
112
+ if (file === null) return
113
+ try {
114
+ mkdirSync(dirname(file), { recursive: true })
115
+ persistentBytes = existsSync(file) ? statSync(file).size : 0
116
+ if (persistentBytes <= PERSISTENT_MAX_BYTES) return
117
+ trimPersistentLog(file)
118
+ } catch {
119
+ // Only configure-time filesystem failures reach this (the directory
120
+ // cannot be created, the file cannot be read or rewritten). The market
121
+ // must mount regardless; the session simply runs memory-only.
122
+ persistentFile = null
123
+ }
124
+ }
125
+
126
+ /**
127
+ * The newest persisted lines, for the export's prior-session section.
128
+ * @param file - the sink file to read.
129
+ * @param maxLines - how many trailing lines to return.
130
+ * @returns parsed-or-raw lines, newest last; empty when nothing is readable.
131
+ */
132
+ export function readPersistentLog(file: string, maxLines = 80): string[] {
133
+ try {
134
+ return readFileSync(file, 'utf8').split('\n').filter(line => line !== '').slice(-maxLines)
135
+ } catch {
136
+ // Only a missing or unreadable file reaches this: there are no prior
137
+ // sessions to show, which is the empty answer.
138
+ return []
139
+ }
140
+ }
141
+
142
+ /**
143
+ * How to read the `at` fields, said once at the top.
144
+ *
145
+ * Every line is stamped in UTC and stays that way: this file is written to
146
+ * be attached to an issue, and a maintainer comparing two reporters' logs
147
+ * needs one clock, not each machine's. Localizing the lines would make the
148
+ * artifact worse at the only job it has.
149
+ *
150
+ * But the reader is looking at their own wall clock, and #449 is what that
151
+ * costs when nobody says so — a UTC+8 user saw `07:32` for something they
152
+ * watched happen at `15:32` and could not tell whether the log was skewed
153
+ * or their own machine was. So the offset is stated, with the arithmetic
154
+ * already done: "add 8h" is a fact they can use without converting
155
+ * anything, and it is the one line that turns the timestamps from
156
+ * suspicious into usable.
157
+ * @returns the note, or null when this machine is already on UTC.
158
+ */
159
+ function timezoneNote(now = new Date()): string | null {
160
+ // getTimezoneOffset is minutes to ADD to local to reach UTC, so it is
161
+ // positive west of Greenwich — the opposite sign from how offsets are
162
+ // written. Negate it once here rather than at each use.
163
+ const minutes = -now.getTimezoneOffset()
164
+ if (minutes === 0) return null
165
+ const sign = minutes < 0 ? '-' : '+'
166
+ const abs = Math.abs(minutes)
167
+ const hh = String(Math.floor(abs / 60)).padStart(2, '0')
168
+ const mm = String(abs % 60).padStart(2, '0')
169
+ let zone = ''
170
+ try {
171
+ const named = Intl.DateTimeFormat().resolvedOptions().timeZone
172
+ zone = typeof named === 'string' && named !== '' ? ` (${named})` : ''
173
+ } catch { /* a runtime without full ICU still gets the offset */ }
174
+ const shift = mm === '00' ? `${String(Math.floor(abs / 60))}h` : `${String(Math.floor(abs / 60))}h${mm}m`
175
+ const direction = minutes < 0 ? 'subtract' : 'add'
176
+ return `timestamps: UTC — this machine is UTC${sign}${hh}:${mm}${zone}, so ${direction} ${shift} for local time`
177
+ }
178
+
179
+ /**
180
+ * The export document for bug reports.
181
+ * @param header - environment lines to prepend (version, platform — no paths).
182
+ * @returns plain text, newest entry last.
183
+ */
184
+ export function exportLogs(header: Record<string, string>, snapshot: string[] = [], priorSessions: string[] = []): string {
185
+ const note = timezoneNote()
186
+ const head = [
187
+ ...Object.entries(header).map(([key, value]) => `${key}: ${sanitize(value)}`),
188
+ ...(note === null ? [] : [note]),
189
+ ]
190
+ const lines = entries.map(e => `${e.at} [${e.level}] ${e.event}: ${e.detail}`)
191
+ return [
192
+ '# ChatCode CLI Market log export',
193
+ ...head,
194
+ '',
195
+ // The state of the profile RIGHT NOW, which does not depend on anything
196
+ // having been recorded this session (#341). The buffer dies with the
197
+ // process, so the failures worth reporting most — the ones that only
198
+ // appear after a restart — were exactly the ones whose export said
199
+ // "(no events this session)". This part still answers.
200
+ ...(snapshot.length > 0 ? ['## profile state', ...snapshot.map(line => sanitize(line)), ''] : []),
201
+ ...(priorSessions.length > 0 ? ['## previous sessions (persisted log)', ...priorSessions.map(line => sanitize(line)), ''] : []),
202
+ '## events this session',
203
+ ...(lines.length > 0 ? lines : ['(none — the buffer starts empty on every start)']),
204
+ '',
205
+ ].join('\n')
206
+ }
package/src/ndjson.ts ADDED
@@ -0,0 +1,185 @@
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
+
21
+ export type ProgressPhase = 'resolving' | 'downloading' | 'linking' | 'building' | null
22
+
23
+ export interface ProgressSnapshot {
24
+ phase: ProgressPhase
25
+ /** Distinct packages resolved/fetched so far. */
26
+ done: number
27
+ /** Package-count total; pnpm 11's stream carries no per-run total. */
28
+ total: number | null
29
+ /** Package being fetched/linked right now (its `packageId`), or null. */
30
+ currentPackage: string | null
31
+ /** Bytes downloaded for the current package, when known. */
32
+ downloaded: number | null
33
+ /** Total bytes for the current package, when known. */
34
+ size: number | null
35
+ /** True once any structured pnpm event was parsed (ndjson works). */
36
+ seen: boolean
37
+ /** Last fatal error message carried by the stream, if any. */
38
+ error: string | null
39
+ /**
40
+ * pnpm's own error CODE from that same event (`ERR_PNPM_…`), when it
41
+ * carried one. The code is the part that classifies reliably: the message
42
+ * is prose and gets reworded between pnpm releases (#244).
43
+ */
44
+ errorCode: string | null
45
+ /** Package names pnpm reported as having ignored build scripts. */
46
+ ignoredBuilds: string[]
47
+ }
48
+
49
+ export function emptyProgress(): ProgressSnapshot {
50
+ return {
51
+ phase: null,
52
+ done: 0,
53
+ total: null,
54
+ currentPackage: null,
55
+ downloaded: null,
56
+ size: null,
57
+ seen: false,
58
+ error: null,
59
+ errorCode: null,
60
+ ignoredBuilds: [],
61
+ }
62
+ }
63
+
64
+ export interface ProgressTracker {
65
+ readonly snapshot: ProgressSnapshot
66
+ feed(line: string): void
67
+ reset(): void
68
+ }
69
+
70
+ export function createProgressTracker(): ProgressTracker {
71
+ const snap = emptyProgress()
72
+ const seenPackages = new Set<string>()
73
+
74
+ function dedupe(packageId: unknown): void {
75
+ if (typeof packageId !== 'string' || packageId === '') return
76
+ if (!seenPackages.has(packageId)) {
77
+ seenPackages.add(packageId)
78
+ snap.done += 1
79
+ }
80
+ }
81
+
82
+ function feed(line: string): void {
83
+ let event: unknown
84
+ try {
85
+ event = JSON.parse(line)
86
+ } catch {
87
+ return // human fallback lines are handled by the caller
88
+ }
89
+ if (typeof event !== 'object' || event === null) return
90
+ const msg = event as Record<string, unknown>
91
+ const name = msg.name
92
+ if (typeof name !== 'string') return
93
+
94
+ if (name === 'pnpm:stage') {
95
+ const stage = msg.stage
96
+ if (stage === 'resolution_started') snap.phase = 'resolving'
97
+ else if (stage === 'resolution_done') snap.phase = 'downloading'
98
+ else if (stage === 'importing_started' || stage === 'importing_done') snap.phase = 'linking'
99
+ snap.seen = true
100
+ return
101
+ }
102
+
103
+ if (name === 'pnpm:progress') {
104
+ snap.seen = true
105
+ const status = msg.status
106
+ if (status === 'resolved') {
107
+ if (snap.phase === null) snap.phase = 'resolving'
108
+ dedupe(msg.packageId)
109
+ } else if (status === 'fetched' || status === 'found_in_store') {
110
+ snap.phase = 'downloading'
111
+ snap.currentPackage = typeof msg.packageId === 'string' ? msg.packageId : snap.currentPackage
112
+ dedupe(msg.packageId)
113
+ }
114
+ return
115
+ }
116
+
117
+ if (name === 'pnpm:fetching-progress') {
118
+ snap.seen = true
119
+ snap.phase = 'downloading'
120
+ if (typeof msg.packageId === 'string') snap.currentPackage = msg.packageId
121
+ if (typeof msg.size === 'number') snap.size = msg.size
122
+ if (typeof msg.downloaded === 'number') snap.downloaded = msg.downloaded
123
+ dedupe(msg.packageId)
124
+ return
125
+ }
126
+
127
+ if (name === 'pnpm:lifecycle') {
128
+ snap.seen = true
129
+ snap.phase = 'building'
130
+ const wd = typeof msg.wd === 'string' ? msg.wd : ''
131
+ const dep = typeof msg.depPath === 'string' ? msg.depPath : ''
132
+ const base = wd.split(/[\\/]/).filter(Boolean).pop()
133
+ snap.currentPackage = base ?? (dep !== '' ? dep : snap.currentPackage)
134
+ return
135
+ }
136
+
137
+ if (name === 'pnpm:stats') {
138
+ // added/removed counts land after the import pass — still linking.
139
+ if (msg.added !== undefined || msg.removed !== undefined) snap.phase = 'linking'
140
+ snap.seen = true
141
+ return
142
+ }
143
+
144
+ if (name === 'pnpm:ignored-scripts') {
145
+ snap.seen = true
146
+ if (Array.isArray(msg.packageNames)) {
147
+ for (const pkg of msg.packageNames) {
148
+ // pnpm's ndjson event reports version-qualified names (cloudflared@0.7.3);
149
+ // the approve-builds allowlist keys and node_modules lookups use bare
150
+ // package names, so strip the suffix (same rule as the human-line
151
+ // fallback in install.ts's parseIgnoredBuilds).
152
+ const at = typeof pkg === 'string' ? pkg.lastIndexOf('@') : -1
153
+ const bare = at > 0 ? pkg.slice(0, at) : pkg
154
+ if (typeof bare === 'string' && bare !== '' && !snap.ignoredBuilds.includes(bare)) snap.ignoredBuilds.push(bare)
155
+ }
156
+ }
157
+ return
158
+ }
159
+
160
+ if (name === 'pnpm' && msg.level === 'error') {
161
+ const err = (msg.err ?? {}) as Record<string, unknown>
162
+ const message = typeof err.message === 'string' ? err.message : ''
163
+ // Keep more than the old 400: ERR_PNPM_UNEXPECTED_STORE spends most of
164
+ // its message naming two absolute store paths, and truncating them is
165
+ // exactly the information a user needs to fix it by hand (#244).
166
+ if (message !== '') snap.error = message.slice(0, 2000)
167
+ if (typeof err.code === 'string' && err.code !== '') snap.errorCode = err.code
168
+ return
169
+ }
170
+ }
171
+
172
+ function reset(): void {
173
+ seenPackages.clear()
174
+ const fresh = emptyProgress()
175
+ Object.assign(snap, fresh)
176
+ }
177
+
178
+ return {
179
+ get snapshot(): ProgressSnapshot {
180
+ return { ...snap, ignoredBuilds: [...snap.ignoredBuilds] }
181
+ },
182
+ feed,
183
+ reset,
184
+ }
185
+ }
package/src/net.ts ADDED
@@ -0,0 +1,106 @@
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
+ import { EnvHttpProxyAgent, fetch as undiciFetch } from 'undici'
24
+
25
+ /**
26
+ * The proxy this process would use for the catalog, if any.
27
+ *
28
+ * The standard variables mirror `EnvHttpProxyAgent`'s own resolution
29
+ * deliberately, rather than picking the order that reads best, because the
30
+ * same answer does two jobs: it decides whether to route through undici at
31
+ * all, and it is what the failure message CLAIMS was tried. A helper that
32
+ * named a proxy undici would not have used would put a false statement in
33
+ * every bug report. `npm_config_*` is an additional source on top of that:
34
+ * npm holds its proxy in its own config namespace (a machine set up with
35
+ * `npm config set proxy` has the proxy in `npm_config_proxy` and nowhere
36
+ * else), and undici does not read it — so it is resolved here and handed to
37
+ * the agent explicitly in `marketFetch`.
38
+ *
39
+ * Three details are undici's, not ours (env-http-proxy-agent.js):
40
+ * - lowercase wins over uppercase (`https_proxy ?? HTTPS_PROXY`)
41
+ * - an https request falls back to the http proxy when no https one is set
42
+ * - the value is tested for truthiness, so `HTTPS_PROXY=` — which is how
43
+ * people turn a proxy off — falls through instead of masking HTTP_PROXY
44
+ *
45
+ * Blank-is-unset is ours, and only widens that last one: undici would hand a
46
+ * whitespace-only value to `new URL()` and throw out of the constructor.
47
+ * Scheme-less host:port values are also common in Windows proxy fields and
48
+ * npm config; proxy agents require URLs, so those default to `http://`.
49
+ */
50
+ export function configuredProxy(): string | null {
51
+ const { http, https } = proxyFromEnv()
52
+ return https ?? http
53
+ }
54
+
55
+ /**
56
+ * Proxy URLs resolved from the process environment, standard variables
57
+ * first and npm's own config (`npm_config_https_proxy` / `npm_config_proxy`)
58
+ * as the fallback. npm's config is the second source, not a preference: a
59
+ * standard variable always wins over npm's, and npm's https proxy falls
60
+ * back to npm's http proxy the same way undici's does.
61
+ */
62
+ function proxyFromEnv(): { http: string | null; https: string | null } {
63
+ const pick = (raw: string | undefined): string | null => {
64
+ const value = raw?.trim()
65
+ if (value === undefined || value === '') return null
66
+ return /^[a-z][a-z\d+.-]*:\/\//iu.test(value) ? value : `http://${value}`
67
+ }
68
+ const https =
69
+ pick(process.env.https_proxy ?? process.env.HTTPS_PROXY) ??
70
+ pick(process.env.npm_config_https_proxy)
71
+ const http =
72
+ pick(process.env.http_proxy ?? process.env.HTTP_PROXY) ??
73
+ pick(process.env.npm_config_proxy)
74
+ return { http, https }
75
+ }
76
+
77
+ /**
78
+ * Built once and reused: an agent per request would drop connection reuse,
79
+ * and this one reads NO_PROXY as well, so a host that excludes its own
80
+ * registry mirror keeps being excluded.
81
+ */
82
+ let agent: EnvHttpProxyAgent | null = null
83
+
84
+ /**
85
+ * Fetch through the proxy this machine is configured to use.
86
+ *
87
+ * Falls back to the global fetch when no proxy is set, which keeps the
88
+ * ordinary case on the runtime's own path rather than routing it through a
89
+ * second HTTP stack for no reason.
90
+ */
91
+ export async function marketFetch(
92
+ url: string,
93
+ init?: { signal?: AbortSignal; headers?: Record<string, string> },
94
+ ): Promise<Response> {
95
+ const { http, https } = proxyFromEnv()
96
+ if (http === null && https === null) return await fetch(url, init)
97
+ // Pass the resolved proxies explicitly. EnvHttpProxyAgent itself reads
98
+ // only http(s)_proxy out of the environment, so a proxy that lives in
99
+ // npm_config_* must be handed over directly — otherwise the agent would
100
+ // silently go direct while configuredProxy() claims a proxy was used.
101
+ agent ??= new EnvHttpProxyAgent({
102
+ httpProxy: http ?? undefined,
103
+ httpsProxy: https ?? undefined,
104
+ })
105
+ return await undiciFetch(url, { ...init, dispatcher: agent }) as unknown as Response
106
+ }