claudeup 6.2.0 → 6.3.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/README.md +22 -0
- package/bin/claudeup.js +6 -1
- package/package.json +6 -6
- package/src/__tests__/alias-adopt.test.ts +331 -329
- package/src/__tests__/alias-parser.test.ts +384 -376
- package/src/__tests__/alias-shell-writer.test.ts +638 -625
- package/src/__tests__/alias-store.test.ts +143 -143
- package/src/__tests__/catalog-cache-store.test.ts +265 -265
- package/src/__tests__/catalog-notice.test.ts +123 -123
- package/src/__tests__/cli-apply-seams.test.ts +12 -8
- package/src/__tests__/cli-router.test.ts +132 -3
- package/src/__tests__/cli-update-view.test.ts +27 -8
- package/src/__tests__/community-staleness.test.ts +0 -2
- package/src/__tests__/component-badge.test.ts +4 -4
- package/src/__tests__/content-drift.test.ts +5 -1
- package/src/__tests__/conventions-integration.test.ts +774 -689
- package/src/__tests__/conventions-manager.test.ts +1216 -1152
- package/src/__tests__/doctor-bins.test.ts +14 -6
- package/src/__tests__/doctor.test.ts +438 -425
- package/src/__tests__/dual-write-prevention.test.ts +277 -282
- package/src/__tests__/enabled-not-installed.test.ts +71 -63
- package/src/__tests__/file-locking.test.ts +196 -196
- package/src/__tests__/gap-fill-versions.test.ts +318 -311
- package/src/__tests__/github-budget.test.ts +170 -164
- package/src/__tests__/gitignore-detector.test.ts +160 -152
- package/src/__tests__/gitignore-fixer.test.ts +285 -247
- package/src/__tests__/gitignore-prerun.test.ts +57 -57
- package/src/__tests__/gitignore-resolver.test.ts +173 -146
- package/src/__tests__/gitignore-service.test.ts +121 -119
- package/src/__tests__/go-module.test.ts +43 -0
- package/src/__tests__/install-command.test.ts +3 -3
- package/src/__tests__/install-plan.test.ts +13 -7
- package/src/__tests__/manifest.test.ts +16 -5
- package/src/__tests__/markdown-renderer.test.ts +0 -1
- package/src/__tests__/marketplace-badge.test.ts +111 -90
- package/src/__tests__/marketplace-version.test.ts +64 -61
- package/src/__tests__/plugin-manager-fallback.test.ts +249 -239
- package/src/__tests__/plugin-requires.test.ts +55 -1
- package/src/__tests__/plugin-setup.test.ts +111 -9
- package/src/__tests__/profile-command.test.ts +1 -7
- package/src/__tests__/profile-materializer.test.ts +31 -8
- package/src/__tests__/profile-sync.test.ts +23 -5
- package/src/__tests__/registry-version-resolution.test.ts +61 -47
- package/src/__tests__/resolve-executable.test.ts +1 -1
- package/src/__tests__/resolver.test.ts +38 -13
- package/src/__tests__/scope-action.test.ts +52 -50
- package/src/__tests__/scope-squares.test.tsx +4 -5
- package/src/__tests__/skill-install-files.test.ts +111 -103
- package/src/__tests__/symlink-manager.test.ts +27 -10
- package/src/__tests__/theme-env.test.ts +281 -0
- package/src/__tests__/theme-resolve.test.ts +380 -0
- package/src/__tests__/update-apply.test.ts +1 -3
- package/src/__tests__/useGitignoreModal.test.ts +69 -61
- package/src/__tests__/version-snapshot.test.ts +183 -165
- package/src/cli/ansi.ts +23 -15
- package/src/cli/doctor.ts +10 -9
- package/src/cli/install.ts +3 -3
- package/src/cli/profile.ts +5 -4
- package/src/cli/prompt.ts +0 -1
- package/src/cli/router.ts +69 -2
- package/src/cli/update-view.ts +11 -6
- package/src/cli/update.ts +9 -4
- package/src/cli/upgrade.ts +6 -4
- package/src/data/alias-flags.ts +260 -260
- package/src/data/gitignore-defaults.ts +20 -20
- package/src/data/gitignore-reasons.ts +86 -92
- package/src/data/gitignore-templates.ts +14 -14
- package/src/data/predefined-profiles.ts +1 -2
- package/src/data/skill-repos.ts +43 -35
- package/src/main.tsx +29 -6
- package/src/opentui.d.ts +1 -3
- package/src/prerunner/index.ts +7 -8
- package/src/services/alias-settings.ts +20 -23
- package/src/services/alias-shell-writer.ts +800 -803
- package/src/services/alias-store.ts +77 -77
- package/src/services/claude-settings.ts +27 -18
- package/src/services/content-drift.ts +14 -4
- package/src/services/conventions-manager.ts +692 -617
- package/src/services/doctor.ts +464 -403
- package/src/services/gitignore-detector.ts +113 -103
- package/src/services/gitignore-fixer.ts +204 -189
- package/src/services/gitignore-prerun.ts +26 -26
- package/src/services/gitignore-resolver.ts +162 -144
- package/src/services/gitignore-service.ts +120 -117
- package/src/services/install-plan.ts +4 -4
- package/src/services/local-marketplace.ts +7 -5
- package/src/services/manifest.ts +1 -1
- package/src/services/marketplace-catalog-git.ts +5 -2
- package/src/services/marketplace-fetcher.ts +241 -235
- package/src/services/plugin-manager.ts +17 -20
- package/src/services/plugin-mcp-config.ts +2 -2
- package/src/services/plugin-requires.ts +16 -8
- package/src/services/plugin-setup.ts +32 -15
- package/src/services/plugin-version-check.ts +4 -4
- package/src/services/profile-materializer.ts +9 -5
- package/src/services/profile-sync.ts +12 -4
- package/src/services/profiles.ts +3 -3
- package/src/services/resolver.ts +10 -6
- package/src/services/settings-manager.ts +82 -46
- package/src/services/skills-manager.ts +11 -9
- package/src/services/symlink-manager.ts +2 -5
- package/src/services/toolchain.ts +3 -3
- package/src/services/update-cache.ts +1 -1
- package/src/services/update-engine.ts +4 -4
- package/src/services/update-plan.ts +9 -11
- package/src/services/version-snapshot.ts +88 -88
- package/src/types/gitignore.ts +28 -28
- package/src/ui/App.tsx +36 -36
- package/src/ui/adapters/settingsAdapter.ts +2 -2
- package/src/ui/components/CategoryHeader.tsx +1 -1
- package/src/ui/components/FlagDetailEditor.tsx +0 -0
- package/src/ui/components/ScopeIndicator.tsx +3 -1
- package/src/ui/components/ScrollableList.tsx +2 -1
- package/src/ui/components/StyledText.tsx +1 -1
- package/src/ui/components/layout/FooterHints.tsx +5 -3
- package/src/ui/components/layout/Panel.tsx +2 -2
- package/src/ui/components/layout/ScreenLayout.tsx +5 -4
- package/src/ui/components/modals/InputModal.tsx +7 -1
- package/src/ui/components/modals/ModalContainer.tsx +3 -3
- package/src/ui/components/modals/SelectModal.tsx +1 -1
- package/src/ui/components/primitives/DetailSection.tsx +1 -1
- package/src/ui/components/primitives/KeyValueLine.tsx +1 -1
- package/src/ui/components/primitives/ScopeSquares.tsx +2 -2
- package/src/ui/hooks/index.ts +0 -1
- package/src/ui/hooks/useGitignoreModal.ts +1 -5
- package/src/ui/hooks/useMismatchModal.ts +4 -10
- package/src/ui/renderers/cliToolRenderers.tsx +2 -2
- package/src/ui/renderers/gitignoreRenderers.tsx +2 -2
- package/src/ui/renderers/mcpRenderers.tsx +1 -1
- package/src/ui/renderers/pluginRenderers.tsx +3 -9
- package/src/ui/renderers/profileRenderers.tsx +9 -8
- package/src/ui/renderers/settingsRenderers.tsx +4 -4
- package/src/ui/renderers/skillRenderers.tsx +14 -14
- package/src/ui/screens/AliasScreen.tsx +31 -34
- package/src/ui/screens/CliToolsScreen.tsx +14 -13
- package/src/ui/screens/EnvVarsScreen.tsx +8 -8
- package/src/ui/screens/GitignoreScreen.tsx +15 -14
- package/src/ui/screens/McpRegistryScreen.tsx +10 -7
- package/src/ui/screens/McpScreen.tsx +10 -10
- package/src/ui/screens/PluginsScreen.tsx +17 -21
- package/src/ui/screens/ProfilesScreen.tsx +25 -24
- package/src/ui/screens/SkillsScreen.tsx +16 -14
- package/src/ui/state/AppContext.tsx +4 -8
- package/src/ui/state/DimensionsContext.tsx +3 -2
- package/src/ui/theme-mode.ts +20 -38
- package/src/ui/theme-resolve.ts +148 -0
- package/src/utils/clipboard.ts +5 -5
- package/src/utils/command-utils.ts +2 -2
- package/src/utils/config-dir.ts +1 -3
- package/src/utils/file-locking.ts +88 -86
- package/src/utils/go-module.ts +36 -0
- package/src/ui/hooks/useAsyncData.ts +0 -127
- package/src/ui/state/AnimationContext.tsx +0 -76
package/src/services/manifest.ts
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* structural subset of a v2 manifest entry, so migration is a version bump.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import fs from "fs-extra";
|
|
11
10
|
import path from "node:path";
|
|
11
|
+
import fs from "fs-extra";
|
|
12
12
|
import type { ProfileManifest, ProfileManifestEntry } from "../types/index.js";
|
|
13
13
|
|
|
14
14
|
const PROFILES_FILE = "profiles.json";
|
|
@@ -32,8 +32,8 @@ import fs from "node:fs";
|
|
|
32
32
|
import path from "node:path";
|
|
33
33
|
import { claudeConfigDirOrNull } from "../utils/config-dir.js";
|
|
34
34
|
import {
|
|
35
|
-
normalizeCatalogJson,
|
|
36
35
|
type MarketplacePlugin,
|
|
36
|
+
normalizeCatalogJson,
|
|
37
37
|
} from "./marketplace-catalog.js";
|
|
38
38
|
|
|
39
39
|
/** Network reach; a wedged fetch must never hold up the screen. */
|
|
@@ -160,7 +160,10 @@ export async function readCatalogFromRemote(
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
try {
|
|
163
|
-
const plugins = normalizeCatalogJson(
|
|
163
|
+
const plugins = normalizeCatalogJson(
|
|
164
|
+
marketplaceName,
|
|
165
|
+
JSON.parse(shown.out),
|
|
166
|
+
);
|
|
164
167
|
catalogCache.set(marketplaceName, plugins);
|
|
165
168
|
return plugins;
|
|
166
169
|
} catch {
|
|
@@ -9,35 +9,35 @@
|
|
|
9
9
|
|
|
10
10
|
import { isValidGitHubRepo } from "../utils/string-utils.js";
|
|
11
11
|
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
clearCachedCatalogs,
|
|
13
|
+
readCachedCatalog,
|
|
14
|
+
writeCachedCatalog,
|
|
15
15
|
} from "./catalog-cache-store.js";
|
|
16
16
|
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
type GitHubHost,
|
|
18
|
+
cooldownFor,
|
|
19
|
+
describeCooldown,
|
|
20
|
+
hydrateGitHubBudget,
|
|
21
|
+
recordRateLimit,
|
|
22
|
+
recordSuccess,
|
|
23
23
|
} from "./github-budget.js";
|
|
24
24
|
import type { LocalMarketplace } from "./local-marketplace.js";
|
|
25
25
|
import {
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
clearGitCatalogCache,
|
|
27
|
+
readCatalogFromRemote,
|
|
28
28
|
} from "./marketplace-catalog-git.js";
|
|
29
29
|
import {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
type MarketplacePlugin,
|
|
31
|
+
clearMarketplaceVersionCache,
|
|
32
|
+
normalizeCatalogJson,
|
|
33
33
|
} from "./marketplace-catalog.js";
|
|
34
34
|
|
|
35
35
|
// Re-exported: these used to live here, and plugin-manager plus several screens
|
|
36
36
|
// import them from this module.
|
|
37
37
|
export {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
getMarketplaceVersion,
|
|
39
|
+
normalizeCatalogJson,
|
|
40
|
+
type MarketplacePlugin,
|
|
41
41
|
} from "./marketplace-catalog.js";
|
|
42
42
|
|
|
43
43
|
// Session-level cache for fetched marketplace data (no TTL - persists until explicit refresh)
|
|
@@ -45,31 +45,31 @@ const marketplaceCache = new Map<string, MarketplacePlugin[]>();
|
|
|
45
45
|
|
|
46
46
|
/** Why a catalog fetch did not produce an authoritative plugin list. */
|
|
47
47
|
export type MarketplaceFetchFailureKind =
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
48
|
+
| "rate-limited"
|
|
49
|
+
| "http"
|
|
50
|
+
| "timeout"
|
|
51
|
+
| "network"
|
|
52
|
+
| "invalid-repo"
|
|
53
|
+
| "invalid-content";
|
|
54
54
|
|
|
55
55
|
export interface MarketplaceFetchFailure {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
56
|
+
marketplace: string;
|
|
57
|
+
kind: MarketplaceFetchFailureKind;
|
|
58
|
+
httpStatus?: number;
|
|
59
|
+
/** Short, user-facing reason — rendered verbatim in the TUI. */
|
|
60
|
+
detail: string;
|
|
61
|
+
/**
|
|
62
|
+
* Wall-clock ms when a retry is due, for rate limits. Lets the UI count down
|
|
63
|
+
* and retry itself instead of telling the user to try again "later" and leaving
|
|
64
|
+
* them to guess how much later.
|
|
65
|
+
*/
|
|
66
|
+
retryAt?: number;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
export interface MarketplaceFetchResult {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
plugins: MarketplacePlugin[];
|
|
71
|
+
/** Present exactly when `plugins` is not an authoritative remote answer. */
|
|
72
|
+
failure?: MarketplaceFetchFailure;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
/**
|
|
@@ -88,17 +88,17 @@ export type CatalogSource = "remote" | "remote-git" | "local-clone" | "none";
|
|
|
88
88
|
|
|
89
89
|
/** Sources whose versions may be reported to the user as current. */
|
|
90
90
|
export function isAuthoritative(source: CatalogSource): boolean {
|
|
91
|
-
|
|
91
|
+
return source === "remote" || source === "remote-git";
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
export interface MarketplaceResolution {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
95
|
+
plugins: MarketplacePlugin[];
|
|
96
|
+
source: CatalogSource;
|
|
97
|
+
/**
|
|
98
|
+
* The remote failure, if any. Set even when `source` is `local-clone` — the
|
|
99
|
+
* fallback succeeded, but the versions in it are not known to be current.
|
|
100
|
+
*/
|
|
101
|
+
failure?: MarketplaceFetchFailure;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
/**
|
|
@@ -114,19 +114,22 @@ export interface MarketplaceResolution {
|
|
|
114
114
|
* freezes the catalog for a whole session — which would be the same
|
|
115
115
|
* silent-staleness trap this module exists to close.
|
|
116
116
|
*/
|
|
117
|
-
const failureCache = new Map<
|
|
117
|
+
const failureCache = new Map<
|
|
118
|
+
string,
|
|
119
|
+
{ failure: MarketplaceFetchFailure; at: number }
|
|
120
|
+
>();
|
|
118
121
|
const NEGATIVE_TTL_MS = 60_000;
|
|
119
122
|
|
|
120
123
|
/** Monotonic clock, so a system clock change cannot expire the cache early. */
|
|
121
124
|
function now(): number {
|
|
122
|
-
|
|
125
|
+
return performance.now();
|
|
123
126
|
}
|
|
124
127
|
|
|
125
128
|
export function clearMarketplaceCache(): void {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
129
|
+
marketplaceCache.clear();
|
|
130
|
+
clearMarketplaceVersionCache();
|
|
131
|
+
clearGitCatalogCache();
|
|
132
|
+
failureCache.clear();
|
|
130
133
|
}
|
|
131
134
|
|
|
132
135
|
/**
|
|
@@ -141,7 +144,7 @@ export function clearMarketplaceCache(): void {
|
|
|
141
144
|
* and refetches by itself when the window opens.
|
|
142
145
|
*/
|
|
143
146
|
export async function clearPersistedCatalogs(): Promise<void> {
|
|
144
|
-
|
|
147
|
+
await clearCachedCatalogs();
|
|
145
148
|
}
|
|
146
149
|
|
|
147
150
|
/**
|
|
@@ -151,30 +154,30 @@ export async function clearPersistedCatalogs(): Promise<void> {
|
|
|
151
154
|
* only proof that the versions on screen were actually checked against GitHub.
|
|
152
155
|
*/
|
|
153
156
|
export function getMarketplaceFetchFailures(): MarketplaceFetchFailure[] {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
157
|
+
const out: MarketplaceFetchFailure[] = [];
|
|
158
|
+
for (const [, entry] of failureCache) {
|
|
159
|
+
if (now() - entry.at < NEGATIVE_TTL_MS) out.push(entry.failure);
|
|
160
|
+
}
|
|
161
|
+
return out;
|
|
159
162
|
}
|
|
160
163
|
|
|
161
164
|
/** Record a failure and return it as the result, so callers cannot ignore it. */
|
|
162
165
|
function fail(
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
166
|
+
marketplace: string,
|
|
167
|
+
kind: MarketplaceFetchFailureKind,
|
|
168
|
+
detail: string,
|
|
169
|
+
httpStatus?: number,
|
|
170
|
+
retryAt?: number,
|
|
168
171
|
): MarketplaceFetchResult {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
172
|
+
const failure: MarketplaceFetchFailure = {
|
|
173
|
+
marketplace,
|
|
174
|
+
kind,
|
|
175
|
+
detail,
|
|
176
|
+
httpStatus,
|
|
177
|
+
retryAt,
|
|
178
|
+
};
|
|
179
|
+
failureCache.set(marketplace, { failure, at: now() });
|
|
180
|
+
return { plugins: [], failure };
|
|
178
181
|
}
|
|
179
182
|
|
|
180
183
|
/**
|
|
@@ -185,19 +188,19 @@ function fail(
|
|
|
185
188
|
* right now", the second is usually "this machine has no route at all".
|
|
186
189
|
*/
|
|
187
190
|
function classifyThrown(
|
|
188
|
-
|
|
189
|
-
|
|
191
|
+
marketplace: string,
|
|
192
|
+
error: unknown,
|
|
190
193
|
): MarketplaceFetchResult {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
194
|
+
const name = (error as { name?: string } | null)?.name;
|
|
195
|
+
if (name === "TimeoutError" || name === "AbortError") {
|
|
196
|
+
return fail(
|
|
197
|
+
marketplace,
|
|
198
|
+
"timeout",
|
|
199
|
+
`no response in ${FETCH_TIMEOUT_MS / 1000}s`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
203
|
+
return fail(marketplace, "network", message);
|
|
201
204
|
}
|
|
202
205
|
|
|
203
206
|
const FETCH_TIMEOUT_MS = 10_000;
|
|
@@ -213,110 +216,113 @@ const FETCH_TIMEOUT_MS = 10_000;
|
|
|
213
216
|
* one. A failure must be a value the caller has to look at.
|
|
214
217
|
*/
|
|
215
218
|
export async function fetchMarketplacePlugins(
|
|
216
|
-
|
|
217
|
-
|
|
219
|
+
marketplaceName: string,
|
|
220
|
+
repo: string,
|
|
218
221
|
): Promise<MarketplaceFetchResult> {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
222
|
+
// Check cache first - session-level, no TTL
|
|
223
|
+
const cached = marketplaceCache.get(marketplaceName);
|
|
224
|
+
if (cached) {
|
|
225
|
+
return { plugins: cached };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// A fresh failure is reused rather than re-attempted: see `failureCache`.
|
|
229
|
+
const priorFailure = failureCache.get(marketplaceName);
|
|
230
|
+
if (priorFailure && now() - priorFailure.at < NEGATIVE_TTL_MS) {
|
|
231
|
+
return { plugins: [], failure: priorFailure.failure };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Validate repo format to prevent SSRF
|
|
235
|
+
if (!isValidGitHubRepo(repo)) {
|
|
236
|
+
return fail(
|
|
237
|
+
marketplaceName,
|
|
238
|
+
"invalid-repo",
|
|
239
|
+
`not a valid GitHub repo: "${repo}"`,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const host: GitHubHost = "raw.githubusercontent.com";
|
|
244
|
+
|
|
245
|
+
// Cooldowns recorded by a PREVIOUS launch count too. Without this the gate is
|
|
246
|
+
// per-process, and a TUI's process lives for one screen session.
|
|
247
|
+
await hydrateGitHubBudget();
|
|
248
|
+
|
|
249
|
+
// Already known to be rate-limited: do not spend an attempt to rediscover it.
|
|
250
|
+
// The budget is per-host and shared with every other service, so six
|
|
251
|
+
// marketplaces used to trip the same limit six times over — and on some
|
|
252
|
+
// endpoints a rejected request extends the penalty.
|
|
253
|
+
const cooling = cooldownFor(host);
|
|
254
|
+
if (cooling) {
|
|
255
|
+
return fail(
|
|
256
|
+
marketplaceName,
|
|
257
|
+
"rate-limited",
|
|
258
|
+
describeCooldown(cooling),
|
|
259
|
+
429,
|
|
260
|
+
cooling.until,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
// Fetch marketplace.json from GitHub
|
|
266
|
+
const url = `https://raw.githubusercontent.com/${repo}/main/.claude-plugin/marketplace.json`;
|
|
267
|
+
const response = await fetch(url, {
|
|
268
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
if (!response.ok) {
|
|
272
|
+
// 429/403 are the rate-limit shapes GitHub returns for unauthenticated
|
|
273
|
+
// traffic. Called out separately because the fix is "wait, or
|
|
274
|
+
// authenticate", not "check your network".
|
|
275
|
+
const rateLimited = response.status === 429 || response.status === 403;
|
|
276
|
+
if (rateLimited) {
|
|
277
|
+
// Hand the response headers over: api.github.com states when the window
|
|
278
|
+
// reopens, raw.githubusercontent.com states nothing, and the budget
|
|
279
|
+
// module is what knows the difference.
|
|
280
|
+
const cooldown = recordRateLimit(host, response.headers);
|
|
281
|
+
return fail(
|
|
282
|
+
marketplaceName,
|
|
283
|
+
"rate-limited",
|
|
284
|
+
describeCooldown(cooldown),
|
|
285
|
+
response.status,
|
|
286
|
+
cooldown.until,
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
return fail(
|
|
290
|
+
marketplaceName,
|
|
291
|
+
"http",
|
|
292
|
+
`HTTP ${response.status} ${response.statusText}`.trim(),
|
|
293
|
+
response.status,
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Validate content-type
|
|
298
|
+
const contentType = response.headers.get("content-type");
|
|
299
|
+
if (
|
|
300
|
+
contentType &&
|
|
301
|
+
!contentType.includes("application/json") &&
|
|
302
|
+
!contentType.includes("text/plain")
|
|
303
|
+
) {
|
|
304
|
+
return fail(
|
|
305
|
+
marketplaceName,
|
|
306
|
+
"invalid-content",
|
|
307
|
+
`unexpected content-type "${contentType}"`,
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const plugins = normalizeCatalogJson(
|
|
312
|
+
marketplaceName,
|
|
313
|
+
await response.json(),
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
// Cache the result (session-level)
|
|
317
|
+
marketplaceCache.set(marketplaceName, plugins);
|
|
318
|
+
// A success retires any recorded failure, so the banner clears itself.
|
|
319
|
+
failureCache.delete(marketplaceName);
|
|
320
|
+
recordSuccess(host);
|
|
321
|
+
|
|
322
|
+
return { plugins };
|
|
323
|
+
} catch (error) {
|
|
324
|
+
return classifyThrown(marketplaceName, error);
|
|
325
|
+
}
|
|
320
326
|
}
|
|
321
327
|
|
|
322
328
|
/**
|
|
@@ -341,54 +347,54 @@ export async function fetchMarketplacePlugins(
|
|
|
341
347
|
* reporting a guess as a fact.
|
|
342
348
|
*/
|
|
343
349
|
export async function resolveMarketplacePlugins(
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
350
|
+
mpName: string,
|
|
351
|
+
repo: string,
|
|
352
|
+
localMarketplaces: Map<string, LocalMarketplace>,
|
|
347
353
|
): Promise<MarketplaceResolution> {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
354
|
+
// Disk cache first, so a relaunch costs no network at all. Measured before this
|
|
355
|
+
// existed: every process spent six requests and ~13s re-fetching catalogs it had
|
|
356
|
+
// already read minutes earlier.
|
|
357
|
+
const cached = await readCachedCatalog(mpName);
|
|
358
|
+
if (cached) {
|
|
359
|
+
return {
|
|
360
|
+
plugins: cached.plugins,
|
|
361
|
+
source: cached.source as CatalogSource,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const remote = await fetchMarketplacePlugins(mpName, repo);
|
|
366
|
+
if (remote.plugins.length > 0) {
|
|
367
|
+
void writeCachedCatalog(mpName, remote.plugins, "remote");
|
|
368
|
+
return { plugins: remote.plugins, source: "remote" };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// HTTP failed — most often rate-limited. Before falling back to the pinned
|
|
372
|
+
// working tree, ask git for upstream's catalog. `git fetch` moves only remote
|
|
373
|
+
// refs, so this respects `autoUpdate: false` (the install source stays pinned)
|
|
374
|
+
// while still answering "what version exists upstream". It is also on a
|
|
375
|
+
// different transport, so a raw.githubusercontent.com rate limit does not
|
|
376
|
+
// affect it.
|
|
377
|
+
const viaGit = await readCatalogFromRemote(mpName);
|
|
378
|
+
if (viaGit && viaGit.length > 0) {
|
|
379
|
+
void writeCachedCatalog(mpName, viaGit, "remote-git");
|
|
380
|
+
return { plugins: viaGit, source: "remote-git", failure: remote.failure };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const local = localMarketplaces.get(mpName);
|
|
384
|
+
if (!local || local.plugins.length === 0) {
|
|
385
|
+
return { plugins: [], source: "none", failure: remote.failure };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
plugins: local.plugins.map((p) => ({
|
|
390
|
+
name: p.name,
|
|
391
|
+
version: p.version,
|
|
392
|
+
description: p.description,
|
|
393
|
+
category: p.category,
|
|
394
|
+
author: p.author,
|
|
395
|
+
releases: p.releases,
|
|
396
|
+
})),
|
|
397
|
+
source: "local-clone",
|
|
398
|
+
failure: remote.failure,
|
|
399
|
+
};
|
|
394
400
|
}
|