claudeup 5.0.0 → 6.0.0
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/package.json +4 -4
- package/src/__tests__/cli-ansi.test.ts +297 -0
- package/src/__tests__/cli-apply-seams.test.ts +233 -0
- package/src/__tests__/cli-live.test.ts +384 -0
- package/src/__tests__/cli-update-view.test.ts +286 -0
- package/src/__tests__/dotenv.test.ts +5 -0
- package/src/__tests__/gitignore-fixer.test.ts +20 -1
- package/src/__tests__/marketplace-refresh.test.ts +63 -0
- package/src/cli/ansi.ts +512 -0
- package/src/cli/bootstrap.ts +0 -6
- package/src/cli/install.ts +4 -1
- package/src/cli/live.ts +374 -0
- package/src/cli/profile.ts +1 -1
- package/src/cli/router.ts +7 -3
- package/src/cli/update-view.ts +438 -0
- package/src/cli/update.ts +457 -129
- package/src/services/doctor-bins.ts +9 -1
- package/src/services/dotenv.ts +9 -4
- package/src/services/marketplace-refresh.ts +39 -1
|
@@ -23,6 +23,12 @@ export interface BinCheckResult {
|
|
|
23
23
|
/**
|
|
24
24
|
* Check each resolved binary for presence on PATH. `isPresent` is injectable
|
|
25
25
|
* for tests; it defaults to a real PATH lookup.
|
|
26
|
+
*
|
|
27
|
+
* The probes are deliberately SERIAL — a PATH walk per binary, each resolving
|
|
28
|
+
* symlinks — so a profile declaring a dozen tools spends real time here. That
|
|
29
|
+
* is what `onProbe` is for: it fires with the name about to be checked, before
|
|
30
|
+
* the await, so a caller can name the binary it is currently blocked on rather
|
|
31
|
+
* than showing a bare spinner.
|
|
26
32
|
*/
|
|
27
33
|
export async function checkBinaries(
|
|
28
34
|
bins: ResolvedBin[],
|
|
@@ -30,9 +36,11 @@ export async function checkBinaries(
|
|
|
30
36
|
// (the tmux-mcp / npx-cache failure mode) reads as MISSING, not present.
|
|
31
37
|
isPresent: (name: string) => Promise<boolean> = async (name) =>
|
|
32
38
|
(await resolveExecutable(name)) !== null,
|
|
39
|
+
onProbe?: (name: string, index: number, total: number) => void,
|
|
33
40
|
): Promise<BinCheckResult[]> {
|
|
34
41
|
const results: BinCheckResult[] = [];
|
|
35
|
-
for (const bin of bins) {
|
|
42
|
+
for (const [index, bin] of bins.entries()) {
|
|
43
|
+
onProbe?.(bin.name, index, bins.length);
|
|
36
44
|
results.push({
|
|
37
45
|
name: bin.name,
|
|
38
46
|
present: await isPresent(bin.name),
|
package/src/services/dotenv.ts
CHANGED
|
@@ -16,9 +16,14 @@ import path from "node:path";
|
|
|
16
16
|
* Two rules this loader keeps that the autoload did not:
|
|
17
17
|
*
|
|
18
18
|
* 1. Only regular files are read. A FIFO, socket or device is skipped, never
|
|
19
|
-
* opened
|
|
20
|
-
*
|
|
21
|
-
* the
|
|
19
|
+
* opened, because opening a pipe BLOCKS until some process opens the other
|
|
20
|
+
* end — with no writer standing by, claudeup hangs on startup instead of
|
|
21
|
+
* running. That is the whole reason. It is NOT that reading would steal
|
|
22
|
+
* another process's secrets: 1Password's pipe re-serves its contents on
|
|
23
|
+
* every open, and `readFileSync` reads it correctly (`Bun.file().text()`
|
|
24
|
+
* is the API that hangs on a FIFO). An earlier version of this comment,
|
|
25
|
+
* the warning text, and the 4.42.1 changelog all claimed the one-reader
|
|
26
|
+
* story; it is wrong, so do not restore it.
|
|
22
27
|
* 2. Nothing here is fatal. A missing, unreadable or malformed .env produces a
|
|
23
28
|
* warning and claudeup carries on. Managing ~/.claude must never depend on
|
|
24
29
|
* the state of whatever directory you happen to be standing in.
|
|
@@ -89,7 +94,7 @@ export function parseDotenv(text: string): Record<string, string> {
|
|
|
89
94
|
/** What a non-regular entry actually is, for a warning a human can act on. */
|
|
90
95
|
function describeKind(info: Stats): string {
|
|
91
96
|
if (info.isFIFO()) {
|
|
92
|
-
return "it is a named pipe;
|
|
97
|
+
return "it is a named pipe; opening one blocks until a writer appears, and claudeup must not hang before it starts";
|
|
93
98
|
}
|
|
94
99
|
if (info.isSocket()) return "it is a socket, not a regular file";
|
|
95
100
|
if (info.isDirectory()) return "it is a directory, not a regular file";
|
|
@@ -130,12 +130,44 @@ export async function fastForwardCloneIfPresent(
|
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Progress taps, so a caller can show what is happening during the pulls.
|
|
135
|
+
*
|
|
136
|
+
* This is the slowest thing on the update path — a git fetch per marketplace,
|
|
137
|
+
* over the network, with a 30s timeout each — and it used to report only after
|
|
138
|
+
* every clone had settled.
|
|
139
|
+
*
|
|
140
|
+
* Both callbacks are purely observational and CANNOT affect the refresh: each
|
|
141
|
+
* is invoked inside a try/catch that discards whatever it throws. `onSettled`
|
|
142
|
+
* in particular runs inside the `Promise.all` fan-out, so an unguarded throw
|
|
143
|
+
* there would reject the batch and turn a display error into a failed refresh —
|
|
144
|
+
* exactly the coupling this function's "never throws" contract exists to
|
|
145
|
+
* prevent.
|
|
146
|
+
*/
|
|
147
|
+
export interface RefreshProgress {
|
|
148
|
+
/** The eligible set, once selection is done and before any pull starts. */
|
|
149
|
+
onStart?: (names: string[]) => void;
|
|
150
|
+
/** One clone settled. Fires in completion order, not in `names` order. */
|
|
151
|
+
onSettled?: (name: string, status: CloneRefresh) => void;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Invoke an observer without letting it affect the caller. */
|
|
155
|
+
function tap(fn: (() => void) | undefined): void {
|
|
156
|
+
if (!fn) return;
|
|
157
|
+
try {
|
|
158
|
+
fn();
|
|
159
|
+
} catch {
|
|
160
|
+
// A progress display is never worth failing a refresh over.
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
133
164
|
/**
|
|
134
165
|
* Non-destructively fast-forward the on-disk clone of every registered,
|
|
135
166
|
* auto-update-enabled, github-sourced marketplace.
|
|
136
167
|
*
|
|
137
168
|
* @param skip marketplaces already refreshed this run (e.g. re-registered ones),
|
|
138
169
|
* so they aren't pulled twice.
|
|
170
|
+
* @param progress optional taps for a live progress display.
|
|
139
171
|
* @returns which marketplaces were refreshed, which failed (clone left intact),
|
|
140
172
|
* and which were skipped. Failures and skips are collected rather than thrown —
|
|
141
173
|
* one unreachable marketplace must not block launching Claude. Pulls run
|
|
@@ -143,6 +175,7 @@ export async function fastForwardCloneIfPresent(
|
|
|
143
175
|
*/
|
|
144
176
|
export async function refreshRegisteredMarketplaces(
|
|
145
177
|
skip: Iterable<string> = [],
|
|
178
|
+
progress: RefreshProgress = {},
|
|
146
179
|
): Promise<RefreshResult> {
|
|
147
180
|
const refreshed: string[] = [];
|
|
148
181
|
const failed: string[] = [];
|
|
@@ -170,8 +203,13 @@ export async function refreshRegisteredMarketplaces(
|
|
|
170
203
|
// Fast-forward every eligible clone in parallel so a slow one doesn't
|
|
171
204
|
// serialize the rest onto the launch path. fastForwardCloneIfPresent never
|
|
172
205
|
// throws, so the batch always resolves.
|
|
206
|
+
tap(() => progress.onStart?.([...eligible]));
|
|
173
207
|
const statuses = await Promise.all(
|
|
174
|
-
eligible.map((name) =>
|
|
208
|
+
eligible.map(async (name) => {
|
|
209
|
+
const status = await fastForwardCloneIfPresent(name);
|
|
210
|
+
tap(() => progress.onSettled?.(name, status));
|
|
211
|
+
return status;
|
|
212
|
+
}),
|
|
175
213
|
);
|
|
176
214
|
|
|
177
215
|
statuses.forEach((status, i) => {
|