@chatcode/cco-market 1.45.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +146 -0
- package/README.zh.md +143 -0
- package/UPDATE-API-V1.md +132 -0
- package/client/client.js +11534 -0
- package/cordis.patch.yml +5 -0
- package/lib/accelerate.js +184 -0
- package/lib/agents.js +36 -0
- package/lib/backup.js +572 -0
- package/lib/catalog-local-match.js +126 -0
- package/lib/catalog-npm.js +111 -0
- package/lib/changelog.js +242 -0
- package/lib/channels.js +64 -0
- package/lib/check.js +1067 -0
- package/lib/compatibility.js +198 -0
- package/lib/diagnostics.js +49 -0
- package/lib/discovery-compatibility.js +260 -0
- package/lib/dsh-cli.js +949 -0
- package/lib/dsh-install.js +108 -0
- package/lib/gist.js +352 -0
- package/lib/groups.js +97 -0
- package/lib/home-paths.js +42 -0
- package/lib/hot.js +519 -0
- package/lib/http.js +39 -0
- package/lib/index.js +97 -0
- package/lib/install.js +387 -0
- package/lib/log.js +197 -0
- package/lib/ndjson.js +154 -0
- package/lib/net.js +98 -0
- package/lib/order.js +284 -0
- package/lib/patch.js +521 -0
- package/lib/pnpm-compat.js +490 -0
- package/lib/presets.js +278 -0
- package/lib/profile.js +911 -0
- package/lib/region-probe.js +93 -0
- package/lib/regions.js +253 -0
- package/lib/registry.js +204 -0
- package/lib/restart.js +373 -0
- package/lib/routes.js +4372 -0
- package/lib/settings.js +109 -0
- package/lib/skill-market/api.js +119 -0
- package/lib/skill-market/config.js +29 -0
- package/lib/skill-market/installer.js +352 -0
- package/lib/skill-market/runtime.js +13 -0
- package/lib/skill-market/tui.js +119 -0
- package/lib/skill-market/types.js +1 -0
- package/lib/skill-market/web-routes.js +123 -0
- package/lib/snapshot.js +500 -0
- package/lib/source-migration.js +54 -0
- package/lib/sources.js +596 -0
- package/lib/store.js +91 -0
- package/lib/themes.js +102 -0
- package/lib/trial.js +116 -0
- package/lib/types/accelerate.d.ts +85 -0
- package/lib/types/agents.d.ts +22 -0
- package/lib/types/backup.d.ts +131 -0
- package/lib/types/catalog-local-match.d.ts +50 -0
- package/lib/types/catalog-npm.d.ts +47 -0
- package/lib/types/changelog.d.ts +102 -0
- package/lib/types/channels.d.ts +56 -0
- package/lib/types/check.d.ts +266 -0
- package/lib/types/compatibility.d.ts +64 -0
- package/lib/types/diagnostics.d.ts +29 -0
- package/lib/types/discovery-compatibility.d.ts +80 -0
- package/lib/types/dsh-cli.d.ts +324 -0
- package/lib/types/dsh-install.d.ts +46 -0
- package/lib/types/gist.d.ts +53 -0
- package/lib/types/groups.d.ts +34 -0
- package/lib/types/home-paths.d.ts +16 -0
- package/lib/types/hot.d.ts +221 -0
- package/lib/types/http.d.ts +12 -0
- package/lib/types/index.d.ts +14 -0
- package/lib/types/install.d.ts +172 -0
- package/lib/types/log.d.ts +43 -0
- package/lib/types/ndjson.d.ts +52 -0
- package/lib/types/net.d.ts +58 -0
- package/lib/types/order.d.ts +100 -0
- package/lib/types/patch.d.ts +129 -0
- package/lib/types/pnpm-compat.d.ts +93 -0
- package/lib/types/presets.d.ts +90 -0
- package/lib/types/profile.d.ts +253 -0
- package/lib/types/region-probe.d.ts +50 -0
- package/lib/types/regions.d.ts +122 -0
- package/lib/types/registry.d.ts +86 -0
- package/lib/types/restart.d.ts +194 -0
- package/lib/types/routes.d.ts +67 -0
- package/lib/types/settings.d.ts +78 -0
- package/lib/types/skill-market/api.d.ts +10 -0
- package/lib/types/skill-market/config.d.ts +3 -0
- package/lib/types/skill-market/installer.d.ts +23 -0
- package/lib/types/skill-market/runtime.d.ts +10 -0
- package/lib/types/skill-market/tui.d.ts +4 -0
- package/lib/types/skill-market/types.d.ts +71 -0
- package/lib/types/skill-market/web-routes.d.ts +19 -0
- package/lib/types/snapshot.d.ts +93 -0
- package/lib/types/source-migration.d.ts +11 -0
- package/lib/types/sources.d.ts +216 -0
- package/lib/types/store.d.ts +30 -0
- package/lib/types/themes.d.ts +40 -0
- package/lib/types/trial.d.ts +61 -0
- package/lib/types/update-api-v1.d.ts +66 -0
- package/lib/types/updates.d.ts +123 -0
- package/lib/types/verify.d.ts +139 -0
- package/lib/update-api-v1.js +215 -0
- package/lib/updates.js +361 -0
- package/lib/verify.js +453 -0
- package/package.json +121 -0
- package/src/accelerate.ts +213 -0
- package/src/agents.ts +43 -0
- package/src/backup.ts +583 -0
- package/src/catalog-local-match.ts +144 -0
- package/src/catalog-npm.ts +120 -0
- package/src/changelog.ts +282 -0
- package/src/channels.ts +70 -0
- package/src/check.ts +1239 -0
- package/src/client/CommentsModal.tsx +119 -0
- package/src/client/Diagnostics.tsx +907 -0
- package/src/client/ErrorBoundary.tsx +111 -0
- package/src/client/InstallToast.tsx +31 -0
- package/src/client/Market.module.css +830 -0
- package/src/client/MarketSection.tsx +5301 -0
- package/src/client/OperationsPanel.tsx +365 -0
- package/src/client/SettingsCard.tsx +621 -0
- package/src/client/SkillMarket.module.css +21 -0
- package/src/client/SkillMarketSection.tsx +162 -0
- package/src/client/comments.ts +54 -0
- package/src/client/globals.d.ts +13 -0
- package/src/client/index.ts +180 -0
- package/src/client/locales.ts +1112 -0
- package/src/client/market-data.ts +1321 -0
- package/src/client/operations.ts +201 -0
- package/src/client/preset-panel.tsx +263 -0
- package/src/client/primitives.d.ts +140 -0
- package/src/client/self-check.ts +147 -0
- package/src/client/snapshot-panel.tsx +244 -0
- package/src/compatibility.ts +237 -0
- package/src/diagnostics.ts +84 -0
- package/src/discovery-compatibility.ts +315 -0
- package/src/dsh-cli.ts +1126 -0
- package/src/dsh-install.ts +118 -0
- package/src/gist.ts +362 -0
- package/src/groups.ts +111 -0
- package/src/home-paths.ts +53 -0
- package/src/hot.ts +628 -0
- package/src/http.ts +41 -0
- package/src/index.ts +128 -0
- package/src/install.ts +420 -0
- package/src/log.ts +206 -0
- package/src/ndjson.ts +185 -0
- package/src/net.ts +106 -0
- package/src/order.ts +303 -0
- package/src/patch.ts +522 -0
- package/src/pnpm-compat.ts +527 -0
- package/src/presets.ts +344 -0
- package/src/profile.ts +940 -0
- package/src/region-probe.ts +97 -0
- package/src/regions.ts +310 -0
- package/src/registry.ts +250 -0
- package/src/restart.ts +396 -0
- package/src/routes.ts +4506 -0
- package/src/settings.ts +141 -0
- package/src/skill-market/api.ts +130 -0
- package/src/skill-market/config.ts +32 -0
- package/src/skill-market/installer.ts +337 -0
- package/src/skill-market/runtime.ts +14 -0
- package/src/skill-market/tui.ts +157 -0
- package/src/skill-market/types.ts +80 -0
- package/src/skill-market/web-routes.ts +132 -0
- package/src/snapshot.ts +532 -0
- package/src/source-migration.ts +61 -0
- package/src/sources.ts +565 -0
- package/src/store.ts +89 -0
- package/src/themes.ts +125 -0
- package/src/trial.ts +156 -0
- package/src/update-api-v1.ts +277 -0
- package/src/updates.ts +400 -0
- package/src/verify.ts +492 -0
package/src/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
|
+
}
|