claudeup 4.17.0 → 4.18.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 +1 -1
- package/src/__tests__/alias-parser.test.ts +317 -0
- package/src/__tests__/alias-shell-writer.test.ts +661 -0
- package/src/__tests__/alias-store.test.ts +86 -0
- package/src/__tests__/gitignore-fixer.test.ts +64 -1
- package/src/__tests__/gitignore-prerun.test.ts +2 -2
- package/src/__tests__/gitignore-service.test.ts +42 -0
- package/src/__tests__/marketplaces.test.ts +40 -0
- package/src/__tests__/plugin-manager-fallback.test.ts +120 -0
- package/src/__tests__/useGitignoreModal.test.ts +2 -2
- package/src/data/alias-flags.js +196 -0
- package/src/data/alias-flags.ts +291 -0
- package/src/data/gitignore-reasons.js +97 -0
- package/src/data/gitignore-reasons.ts +103 -0
- package/src/data/marketplaces.js +5 -3
- package/src/data/marketplaces.ts +5 -4
- package/src/services/alias-settings.js +51 -0
- package/src/services/alias-settings.ts +63 -0
- package/src/services/alias-shell-writer.js +764 -0
- package/src/services/alias-shell-writer.ts +873 -0
- package/src/services/alias-store.js +77 -0
- package/src/services/alias-store.ts +112 -0
- package/src/services/gitignore-fixer.js +70 -10
- package/src/services/gitignore-fixer.ts +76 -9
- package/src/services/gitignore-prerun.js +3 -3
- package/src/services/gitignore-prerun.ts +3 -3
- package/src/services/gitignore-service.js +20 -2
- package/src/services/gitignore-service.ts +23 -1
- package/src/services/marketplace-fetcher.js +96 -0
- package/src/services/marketplace-fetcher.ts +137 -0
- package/src/services/plugin-manager.js +6 -59
- package/src/services/plugin-manager.ts +16 -91
- package/src/services/skillsmp-client.js +29 -9
- package/src/services/skillsmp-client.ts +38 -8
- package/src/types/gitignore.ts +1 -1
- package/src/ui/App.js +10 -4
- package/src/ui/App.tsx +9 -3
- package/src/ui/components/TabBar.js +2 -1
- package/src/ui/components/TabBar.tsx +2 -1
- package/src/ui/components/layout/FooterHints.js +29 -0
- package/src/ui/components/layout/FooterHints.tsx +52 -0
- package/src/ui/components/layout/ScreenLayout.js +2 -1
- package/src/ui/components/layout/ScreenLayout.tsx +12 -3
- package/src/ui/components/layout/index.js +1 -0
- package/src/ui/components/layout/index.ts +5 -0
- package/src/ui/components/modals/SelectModal.js +8 -1
- package/src/ui/components/modals/SelectModal.tsx +12 -1
- package/src/ui/hooks/useGitignoreModal.js +7 -8
- package/src/ui/hooks/useGitignoreModal.ts +8 -9
- package/src/ui/renderers/gitignoreRenderers.js +36 -23
- package/src/ui/renderers/gitignoreRenderers.tsx +50 -41
- package/src/ui/screens/AliasScreen.js +1008 -0
- package/src/ui/screens/AliasScreen.tsx +1402 -0
- package/src/ui/screens/CliToolsScreen.js +6 -1
- package/src/ui/screens/CliToolsScreen.tsx +6 -1
- package/src/ui/screens/EnvVarsScreen.js +6 -1
- package/src/ui/screens/EnvVarsScreen.tsx +6 -1
- package/src/ui/screens/GitignoreScreen.js +189 -88
- package/src/ui/screens/GitignoreScreen.tsx +312 -132
- package/src/ui/screens/McpRegistryScreen.js +13 -2
- package/src/ui/screens/McpRegistryScreen.tsx +13 -2
- package/src/ui/screens/McpScreen.js +6 -1
- package/src/ui/screens/McpScreen.tsx +6 -1
- package/src/ui/screens/ModelSelectorScreen.js +8 -2
- package/src/ui/screens/ModelSelectorScreen.tsx +8 -2
- package/src/ui/screens/PluginsScreen.js +13 -2
- package/src/ui/screens/PluginsScreen.tsx +13 -2
- package/src/ui/screens/ProfilesScreen.js +8 -1
- package/src/ui/screens/ProfilesScreen.tsx +8 -1
- package/src/ui/screens/SkillsScreen.js +21 -4
- package/src/ui/screens/SkillsScreen.tsx +39 -5
- package/src/ui/screens/StatusLineScreen.js +7 -1
- package/src/ui/screens/StatusLineScreen.tsx +7 -1
- package/src/ui/screens/index.js +1 -0
- package/src/ui/screens/index.ts +1 -0
- package/src/ui/state/types.ts +4 -2
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marketplace.json fetcher with offline fallback.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `plugin-manager.ts` so the fetch + fallback logic can be
|
|
5
|
+
* unit-tested without dragging in claude-settings.ts's module-level
|
|
6
|
+
* `os.homedir()` constants (which contaminate the test suite when
|
|
7
|
+
* imported via plugin-manager.ts).
|
|
8
|
+
*/
|
|
9
|
+
import { isValidGitHubRepo } from "../utils/string-utils.js";
|
|
10
|
+
// Session-level cache for fetched marketplace data (no TTL - persists until explicit refresh)
|
|
11
|
+
const marketplaceCache = new Map();
|
|
12
|
+
export function clearMarketplaceCache() {
|
|
13
|
+
marketplaceCache.clear();
|
|
14
|
+
}
|
|
15
|
+
export async function fetchMarketplacePlugins(marketplaceName, repo) {
|
|
16
|
+
// Check cache first - session-level, no TTL
|
|
17
|
+
const cached = marketplaceCache.get(marketplaceName);
|
|
18
|
+
if (cached) {
|
|
19
|
+
return cached;
|
|
20
|
+
}
|
|
21
|
+
// Validate repo format to prevent SSRF
|
|
22
|
+
if (!isValidGitHubRepo(repo)) {
|
|
23
|
+
console.error(`Invalid GitHub repo format: ${repo}`);
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
// Fetch marketplace.json from GitHub
|
|
28
|
+
const url = `https://raw.githubusercontent.com/${repo}/main/.claude-plugin/marketplace.json`;
|
|
29
|
+
const response = await fetch(url, {
|
|
30
|
+
signal: AbortSignal.timeout(10000), // 10s timeout
|
|
31
|
+
});
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
console.error(`Failed to fetch marketplace: ${response.status}`);
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
// Validate content-type
|
|
37
|
+
const contentType = response.headers.get("content-type");
|
|
38
|
+
if (contentType &&
|
|
39
|
+
!contentType.includes("application/json") &&
|
|
40
|
+
!contentType.includes("text/plain")) {
|
|
41
|
+
console.error(`Invalid content-type for marketplace: ${contentType}`);
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
const data = (await response.json());
|
|
45
|
+
const plugins = [];
|
|
46
|
+
if (data.plugins && Array.isArray(data.plugins)) {
|
|
47
|
+
for (const plugin of data.plugins) {
|
|
48
|
+
plugins.push({
|
|
49
|
+
name: plugin.name,
|
|
50
|
+
version: plugin.version || null,
|
|
51
|
+
description: plugin.description || "",
|
|
52
|
+
category: plugin.category,
|
|
53
|
+
author: plugin.author,
|
|
54
|
+
homepage: plugin.homepage,
|
|
55
|
+
tags: plugin.tags,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Cache the result (session-level)
|
|
60
|
+
marketplaceCache.set(marketplaceName, plugins);
|
|
61
|
+
return plugins;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
console.error(`Error fetching marketplace ${marketplaceName}:`, error);
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Resolve marketplace plugins with offline fallback.
|
|
70
|
+
*
|
|
71
|
+
* Calls `fetchMarketplacePlugins` (which hits GitHub) and, when it returns
|
|
72
|
+
* empty AND a local marketplace cache exists for the same name, falls back
|
|
73
|
+
* to the cached plugin manifest. This prevents every installed plugin from
|
|
74
|
+
* being flagged `isOrphaned: true` (and rendered "deprecated") whenever the
|
|
75
|
+
* machine can't reach `raw.githubusercontent.com` — the local cache already
|
|
76
|
+
* has the data, it just wasn't being consulted as a fallback.
|
|
77
|
+
*
|
|
78
|
+
* The two shapes are nearly identical; we drop a couple of LocalMarketplacePlugin
|
|
79
|
+
* fields (lspServers, agents, etc.) that MarketplacePlugin doesn't carry
|
|
80
|
+
* since the caller doesn't read them at this layer.
|
|
81
|
+
*/
|
|
82
|
+
export async function resolveMarketplacePlugins(mpName, repo, localMarketplaces) {
|
|
83
|
+
const remote = await fetchMarketplacePlugins(mpName, repo);
|
|
84
|
+
if (remote.length > 0)
|
|
85
|
+
return remote;
|
|
86
|
+
const local = localMarketplaces.get(mpName);
|
|
87
|
+
if (!local || local.plugins.length === 0)
|
|
88
|
+
return remote;
|
|
89
|
+
return local.plugins.map((p) => ({
|
|
90
|
+
name: p.name,
|
|
91
|
+
version: p.version,
|
|
92
|
+
description: p.description,
|
|
93
|
+
category: p.category,
|
|
94
|
+
author: p.author,
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marketplace.json fetcher with offline fallback.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `plugin-manager.ts` so the fetch + fallback logic can be
|
|
5
|
+
* unit-tested without dragging in claude-settings.ts's module-level
|
|
6
|
+
* `os.homedir()` constants (which contaminate the test suite when
|
|
7
|
+
* imported via plugin-manager.ts).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { isValidGitHubRepo } from "../utils/string-utils.js";
|
|
11
|
+
import type { LocalMarketplace } from "./local-marketplace.js";
|
|
12
|
+
|
|
13
|
+
export interface MarketplacePlugin {
|
|
14
|
+
name: string;
|
|
15
|
+
version: string | null;
|
|
16
|
+
description: string;
|
|
17
|
+
category?: string;
|
|
18
|
+
author?: { name: string; email?: string };
|
|
19
|
+
homepage?: string;
|
|
20
|
+
tags?: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Session-level cache for fetched marketplace data (no TTL - persists until explicit refresh)
|
|
24
|
+
const marketplaceCache = new Map<string, MarketplacePlugin[]>();
|
|
25
|
+
|
|
26
|
+
export function clearMarketplaceCache(): void {
|
|
27
|
+
marketplaceCache.clear();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function fetchMarketplacePlugins(
|
|
31
|
+
marketplaceName: string,
|
|
32
|
+
repo: string,
|
|
33
|
+
): Promise<MarketplacePlugin[]> {
|
|
34
|
+
// Check cache first - session-level, no TTL
|
|
35
|
+
const cached = marketplaceCache.get(marketplaceName);
|
|
36
|
+
if (cached) {
|
|
37
|
+
return cached;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Validate repo format to prevent SSRF
|
|
41
|
+
if (!isValidGitHubRepo(repo)) {
|
|
42
|
+
console.error(`Invalid GitHub repo format: ${repo}`);
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
// Fetch marketplace.json from GitHub
|
|
48
|
+
const url = `https://raw.githubusercontent.com/${repo}/main/.claude-plugin/marketplace.json`;
|
|
49
|
+
const response = await fetch(url, {
|
|
50
|
+
signal: AbortSignal.timeout(10000), // 10s timeout
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (!response.ok) {
|
|
54
|
+
console.error(`Failed to fetch marketplace: ${response.status}`);
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Validate content-type
|
|
59
|
+
const contentType = response.headers.get("content-type");
|
|
60
|
+
if (
|
|
61
|
+
contentType &&
|
|
62
|
+
!contentType.includes("application/json") &&
|
|
63
|
+
!contentType.includes("text/plain")
|
|
64
|
+
) {
|
|
65
|
+
console.error(`Invalid content-type for marketplace: ${contentType}`);
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface RawPlugin {
|
|
70
|
+
name: string;
|
|
71
|
+
version?: string | null;
|
|
72
|
+
description?: string;
|
|
73
|
+
category?: string;
|
|
74
|
+
author?: { name: string; email?: string };
|
|
75
|
+
homepage?: string;
|
|
76
|
+
tags?: string[];
|
|
77
|
+
}
|
|
78
|
+
const data = (await response.json()) as { plugins?: RawPlugin[] };
|
|
79
|
+
const plugins: MarketplacePlugin[] = [];
|
|
80
|
+
|
|
81
|
+
if (data.plugins && Array.isArray(data.plugins)) {
|
|
82
|
+
for (const plugin of data.plugins) {
|
|
83
|
+
plugins.push({
|
|
84
|
+
name: plugin.name,
|
|
85
|
+
version: plugin.version || null,
|
|
86
|
+
description: plugin.description || "",
|
|
87
|
+
category: plugin.category,
|
|
88
|
+
author: plugin.author,
|
|
89
|
+
homepage: plugin.homepage,
|
|
90
|
+
tags: plugin.tags,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Cache the result (session-level)
|
|
96
|
+
marketplaceCache.set(marketplaceName, plugins);
|
|
97
|
+
|
|
98
|
+
return plugins;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.error(`Error fetching marketplace ${marketplaceName}:`, error);
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Resolve marketplace plugins with offline fallback.
|
|
107
|
+
*
|
|
108
|
+
* Calls `fetchMarketplacePlugins` (which hits GitHub) and, when it returns
|
|
109
|
+
* empty AND a local marketplace cache exists for the same name, falls back
|
|
110
|
+
* to the cached plugin manifest. This prevents every installed plugin from
|
|
111
|
+
* being flagged `isOrphaned: true` (and rendered "deprecated") whenever the
|
|
112
|
+
* machine can't reach `raw.githubusercontent.com` — the local cache already
|
|
113
|
+
* has the data, it just wasn't being consulted as a fallback.
|
|
114
|
+
*
|
|
115
|
+
* The two shapes are nearly identical; we drop a couple of LocalMarketplacePlugin
|
|
116
|
+
* fields (lspServers, agents, etc.) that MarketplacePlugin doesn't carry
|
|
117
|
+
* since the caller doesn't read them at this layer.
|
|
118
|
+
*/
|
|
119
|
+
export async function resolveMarketplacePlugins(
|
|
120
|
+
mpName: string,
|
|
121
|
+
repo: string,
|
|
122
|
+
localMarketplaces: Map<string, LocalMarketplace>,
|
|
123
|
+
): Promise<MarketplacePlugin[]> {
|
|
124
|
+
const remote = await fetchMarketplacePlugins(mpName, repo);
|
|
125
|
+
if (remote.length > 0) return remote;
|
|
126
|
+
|
|
127
|
+
const local = localMarketplaces.get(mpName);
|
|
128
|
+
if (!local || local.plugins.length === 0) return remote;
|
|
129
|
+
|
|
130
|
+
return local.plugins.map((p) => ({
|
|
131
|
+
name: p.name,
|
|
132
|
+
version: p.version,
|
|
133
|
+
description: p.description,
|
|
134
|
+
category: p.category,
|
|
135
|
+
author: p.author,
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
@@ -4,64 +4,11 @@ import { execSync } from "node:child_process";
|
|
|
4
4
|
import { getConfiguredMarketplaces, getEnabledPlugins, readSettings, writeSettings, getGlobalConfiguredMarketplaces, getGlobalEnabledPlugins, getGlobalInstalledPluginVersions, getLocalEnabledPlugins, getLocalInstalledPluginVersions, updateInstalledPluginsRegistry, removeFromInstalledPluginsRegistry, } from "./claude-settings.js";
|
|
5
5
|
import { defaultMarketplaces } from "../data/marketplaces.js";
|
|
6
6
|
import { scanLocalMarketplaces, repairAllMarketplaces, } from "./local-marketplace.js";
|
|
7
|
-
import { formatMarketplaceName,
|
|
7
|
+
import { formatMarketplaceName, parsePluginId, } from "../utils/string-utils.js";
|
|
8
|
+
import { clearMarketplaceCache as clearFetcherCache, fetchMarketplacePlugins, resolveMarketplacePlugins, } from "./marketplace-fetcher.js";
|
|
9
|
+
export { fetchMarketplacePlugins, resolveMarketplacePlugins, };
|
|
8
10
|
// Cache for local marketplaces (session-level) - Promise-based to prevent race conditions
|
|
9
11
|
let localMarketplacesPromise = null;
|
|
10
|
-
// Session-level cache for fetched marketplace data (no TTL - persists until explicit refresh)
|
|
11
|
-
const marketplaceCache = new Map();
|
|
12
|
-
export async function fetchMarketplacePlugins(marketplaceName, repo) {
|
|
13
|
-
// Check cache first - session-level, no TTL
|
|
14
|
-
const cached = marketplaceCache.get(marketplaceName);
|
|
15
|
-
if (cached) {
|
|
16
|
-
return cached;
|
|
17
|
-
}
|
|
18
|
-
// Validate repo format to prevent SSRF
|
|
19
|
-
if (!isValidGitHubRepo(repo)) {
|
|
20
|
-
console.error(`Invalid GitHub repo format: ${repo}`);
|
|
21
|
-
return [];
|
|
22
|
-
}
|
|
23
|
-
try {
|
|
24
|
-
// Fetch marketplace.json from GitHub
|
|
25
|
-
const url = `https://raw.githubusercontent.com/${repo}/main/.claude-plugin/marketplace.json`;
|
|
26
|
-
const response = await fetch(url, {
|
|
27
|
-
signal: AbortSignal.timeout(10000), // 10s timeout
|
|
28
|
-
});
|
|
29
|
-
if (!response.ok) {
|
|
30
|
-
console.error(`Failed to fetch marketplace: ${response.status}`);
|
|
31
|
-
return [];
|
|
32
|
-
}
|
|
33
|
-
// Validate content-type
|
|
34
|
-
const contentType = response.headers.get("content-type");
|
|
35
|
-
if (contentType &&
|
|
36
|
-
!contentType.includes("application/json") &&
|
|
37
|
-
!contentType.includes("text/plain")) {
|
|
38
|
-
console.error(`Invalid content-type for marketplace: ${contentType}`);
|
|
39
|
-
return [];
|
|
40
|
-
}
|
|
41
|
-
const data = (await response.json());
|
|
42
|
-
const plugins = [];
|
|
43
|
-
if (data.plugins && Array.isArray(data.plugins)) {
|
|
44
|
-
for (const plugin of data.plugins) {
|
|
45
|
-
plugins.push({
|
|
46
|
-
name: plugin.name,
|
|
47
|
-
version: plugin.version || null,
|
|
48
|
-
description: plugin.description || "",
|
|
49
|
-
category: plugin.category,
|
|
50
|
-
author: plugin.author,
|
|
51
|
-
homepage: plugin.homepage,
|
|
52
|
-
tags: plugin.tags,
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
// Cache the result (session-level)
|
|
57
|
-
marketplaceCache.set(marketplaceName, plugins);
|
|
58
|
-
return plugins;
|
|
59
|
-
}
|
|
60
|
-
catch (error) {
|
|
61
|
-
console.error(`Error fetching marketplace ${marketplaceName}:`, error);
|
|
62
|
-
return [];
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
12
|
export async function getAvailablePlugins(projectPath) {
|
|
66
13
|
const configuredMarketplaces = await getConfiguredMarketplaces(projectPath);
|
|
67
14
|
const enabledPlugins = await getEnabledPlugins(projectPath);
|
|
@@ -111,7 +58,7 @@ export async function getAvailablePlugins(projectPath) {
|
|
|
111
58
|
const marketplace = defaultMarketplaces.find((m) => m.name === mpName);
|
|
112
59
|
if (!marketplace)
|
|
113
60
|
continue;
|
|
114
|
-
const marketplacePlugins = await
|
|
61
|
+
const marketplacePlugins = await resolveMarketplacePlugins(mpName, marketplace.source.repo, localMarketplaces);
|
|
115
62
|
// Auto-sync local cache if remote has plugins the local cache doesn't
|
|
116
63
|
localMarketplaces = await autoSyncIfStale(mpName, marketplacePlugins.map((p) => p.name), localMarketplaces);
|
|
117
64
|
for (const plugin of marketplacePlugins) {
|
|
@@ -265,7 +212,7 @@ export async function getGlobalAvailablePlugins() {
|
|
|
265
212
|
const marketplace = defaultMarketplaces.find((m) => m.name === mpName);
|
|
266
213
|
if (!marketplace)
|
|
267
214
|
continue;
|
|
268
|
-
const marketplacePlugins = await
|
|
215
|
+
const marketplacePlugins = await resolveMarketplacePlugins(mpName, marketplace.source.repo, localMarketplaces);
|
|
269
216
|
// Auto-sync local cache if remote has plugins the local cache doesn't
|
|
270
217
|
localMarketplaces = await autoSyncIfStale(mpName, marketplacePlugins.map((p) => p.name), localMarketplaces);
|
|
271
218
|
for (const plugin of marketplacePlugins) {
|
|
@@ -414,7 +361,7 @@ export async function removeInstalledPluginVersion(pluginId, projectPath) {
|
|
|
414
361
|
}
|
|
415
362
|
// Clear marketplace cache
|
|
416
363
|
export function clearMarketplaceCache() {
|
|
417
|
-
|
|
364
|
+
clearFetcherCache();
|
|
418
365
|
localMarketplacesPromise = null;
|
|
419
366
|
}
|
|
420
367
|
// Get local marketplaces (with Promise-based caching to prevent race conditions)
|
|
@@ -24,9 +24,19 @@ import {
|
|
|
24
24
|
} from "./local-marketplace.js";
|
|
25
25
|
import {
|
|
26
26
|
formatMarketplaceName,
|
|
27
|
-
isValidGitHubRepo,
|
|
28
27
|
parsePluginId,
|
|
29
28
|
} from "../utils/string-utils.js";
|
|
29
|
+
import {
|
|
30
|
+
clearMarketplaceCache as clearFetcherCache,
|
|
31
|
+
fetchMarketplacePlugins,
|
|
32
|
+
resolveMarketplacePlugins,
|
|
33
|
+
type MarketplacePlugin,
|
|
34
|
+
} from "./marketplace-fetcher.js";
|
|
35
|
+
export {
|
|
36
|
+
fetchMarketplacePlugins,
|
|
37
|
+
resolveMarketplacePlugins,
|
|
38
|
+
type MarketplacePlugin,
|
|
39
|
+
};
|
|
30
40
|
// Cache for local marketplaces (session-level) - Promise-based to prevent race conditions
|
|
31
41
|
let localMarketplacesPromise: Promise<Map<string, LocalMarketplace>> | null =
|
|
32
42
|
null;
|
|
@@ -63,93 +73,6 @@ export interface PluginInfo {
|
|
|
63
73
|
isOrphaned?: boolean;
|
|
64
74
|
}
|
|
65
75
|
|
|
66
|
-
export interface MarketplacePlugin {
|
|
67
|
-
name: string;
|
|
68
|
-
version: string | null;
|
|
69
|
-
description: string;
|
|
70
|
-
category?: string;
|
|
71
|
-
author?: { name: string; email?: string };
|
|
72
|
-
homepage?: string;
|
|
73
|
-
tags?: string[];
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Session-level cache for fetched marketplace data (no TTL - persists until explicit refresh)
|
|
77
|
-
const marketplaceCache = new Map<string, MarketplacePlugin[]>();
|
|
78
|
-
|
|
79
|
-
export async function fetchMarketplacePlugins(
|
|
80
|
-
marketplaceName: string,
|
|
81
|
-
repo: string,
|
|
82
|
-
): Promise<MarketplacePlugin[]> {
|
|
83
|
-
// Check cache first - session-level, no TTL
|
|
84
|
-
const cached = marketplaceCache.get(marketplaceName);
|
|
85
|
-
if (cached) {
|
|
86
|
-
return cached;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Validate repo format to prevent SSRF
|
|
90
|
-
if (!isValidGitHubRepo(repo)) {
|
|
91
|
-
console.error(`Invalid GitHub repo format: ${repo}`);
|
|
92
|
-
return [];
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
try {
|
|
96
|
-
// Fetch marketplace.json from GitHub
|
|
97
|
-
const url = `https://raw.githubusercontent.com/${repo}/main/.claude-plugin/marketplace.json`;
|
|
98
|
-
const response = await fetch(url, {
|
|
99
|
-
signal: AbortSignal.timeout(10000), // 10s timeout
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
if (!response.ok) {
|
|
103
|
-
console.error(`Failed to fetch marketplace: ${response.status}`);
|
|
104
|
-
return [];
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Validate content-type
|
|
108
|
-
const contentType = response.headers.get("content-type");
|
|
109
|
-
if (
|
|
110
|
-
contentType &&
|
|
111
|
-
!contentType.includes("application/json") &&
|
|
112
|
-
!contentType.includes("text/plain")
|
|
113
|
-
) {
|
|
114
|
-
console.error(`Invalid content-type for marketplace: ${contentType}`);
|
|
115
|
-
return [];
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
interface RawPlugin {
|
|
119
|
-
name: string;
|
|
120
|
-
version?: string | null;
|
|
121
|
-
description?: string;
|
|
122
|
-
category?: string;
|
|
123
|
-
author?: { name: string; email?: string };
|
|
124
|
-
homepage?: string;
|
|
125
|
-
tags?: string[];
|
|
126
|
-
}
|
|
127
|
-
const data = (await response.json()) as { plugins?: RawPlugin[] };
|
|
128
|
-
const plugins: MarketplacePlugin[] = [];
|
|
129
|
-
|
|
130
|
-
if (data.plugins && Array.isArray(data.plugins)) {
|
|
131
|
-
for (const plugin of data.plugins) {
|
|
132
|
-
plugins.push({
|
|
133
|
-
name: plugin.name,
|
|
134
|
-
version: plugin.version || null,
|
|
135
|
-
description: plugin.description || "",
|
|
136
|
-
category: plugin.category,
|
|
137
|
-
author: plugin.author,
|
|
138
|
-
homepage: plugin.homepage,
|
|
139
|
-
tags: plugin.tags,
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Cache the result (session-level)
|
|
145
|
-
marketplaceCache.set(marketplaceName, plugins);
|
|
146
|
-
|
|
147
|
-
return plugins;
|
|
148
|
-
} catch (error) {
|
|
149
|
-
console.error(`Error fetching marketplace ${marketplaceName}:`, error);
|
|
150
|
-
return [];
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
76
|
|
|
154
77
|
export async function getAvailablePlugins(
|
|
155
78
|
projectPath?: string,
|
|
@@ -213,9 +136,10 @@ export async function getAvailablePlugins(
|
|
|
213
136
|
const marketplace = defaultMarketplaces.find((m) => m.name === mpName);
|
|
214
137
|
if (!marketplace) continue;
|
|
215
138
|
|
|
216
|
-
const marketplacePlugins = await
|
|
139
|
+
const marketplacePlugins = await resolveMarketplacePlugins(
|
|
217
140
|
mpName,
|
|
218
141
|
marketplace.source.repo,
|
|
142
|
+
localMarketplaces,
|
|
219
143
|
);
|
|
220
144
|
|
|
221
145
|
// Auto-sync local cache if remote has plugins the local cache doesn't
|
|
@@ -397,9 +321,10 @@ export async function getGlobalAvailablePlugins(): Promise<PluginInfo[]> {
|
|
|
397
321
|
const marketplace = defaultMarketplaces.find((m) => m.name === mpName);
|
|
398
322
|
if (!marketplace) continue;
|
|
399
323
|
|
|
400
|
-
const marketplacePlugins = await
|
|
324
|
+
const marketplacePlugins = await resolveMarketplacePlugins(
|
|
401
325
|
mpName,
|
|
402
326
|
marketplace.source.repo,
|
|
327
|
+
localMarketplaces,
|
|
403
328
|
);
|
|
404
329
|
|
|
405
330
|
// Auto-sync local cache if remote has plugins the local cache doesn't
|
|
@@ -592,7 +517,7 @@ export async function removeInstalledPluginVersion(
|
|
|
592
517
|
|
|
593
518
|
// Clear marketplace cache
|
|
594
519
|
export function clearMarketplaceCache(): void {
|
|
595
|
-
|
|
520
|
+
clearFetcherCache();
|
|
596
521
|
localMarketplacesPromise = null;
|
|
597
522
|
}
|
|
598
523
|
|
|
@@ -7,10 +7,28 @@
|
|
|
7
7
|
* - GitHub Tree API with ETag caching
|
|
8
8
|
* - Recommended skills list
|
|
9
9
|
*/
|
|
10
|
-
//
|
|
10
|
+
// Deployed `skills` Gen2 Cloud Function (project claudish-6da10, us-central1).
|
|
11
|
+
// Firebase keeps this cloudfunctions.net alias stable for the function.
|
|
11
12
|
const SKILLS_API_BASE = process.env.SKILLS_API_URL || "https://us-central1-claudish-6da10.cloudfunctions.net/skills";
|
|
12
13
|
/**
|
|
13
|
-
*
|
|
14
|
+
* Thrown when the skills API is unreachable or returns a non-OK status.
|
|
15
|
+
* Lets callers distinguish "search service is down" from "no skills matched",
|
|
16
|
+
* which a bare `[]` return cannot express.
|
|
17
|
+
*/
|
|
18
|
+
export class SkillsApiError extends Error {
|
|
19
|
+
status;
|
|
20
|
+
constructor(message, status) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.status = status;
|
|
23
|
+
this.name = "SkillsApiError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Search skills across all sources (SkillsMP keyword + AI search).
|
|
28
|
+
*
|
|
29
|
+
* Throws {@link SkillsApiError} on transport/HTTP failure so the caller can
|
|
30
|
+
* surface a distinct "search unavailable" state. An empty array means the
|
|
31
|
+
* query genuinely matched nothing.
|
|
14
32
|
*/
|
|
15
33
|
export async function searchSkills(query, options) {
|
|
16
34
|
const params = new URLSearchParams({ q: query });
|
|
@@ -18,18 +36,20 @@ export async function searchSkills(query, options) {
|
|
|
18
36
|
params.set("page", String(options.page));
|
|
19
37
|
if (options?.limit)
|
|
20
38
|
params.set("limit", String(options.limit));
|
|
39
|
+
let res;
|
|
21
40
|
try {
|
|
22
|
-
|
|
41
|
+
res = await fetch(`${SKILLS_API_BASE}/search?${params}`, {
|
|
23
42
|
signal: AbortSignal.timeout(12000),
|
|
24
43
|
});
|
|
25
|
-
if (!res.ok)
|
|
26
|
-
return [];
|
|
27
|
-
const data = (await res.json());
|
|
28
|
-
return data.skills || [];
|
|
29
44
|
}
|
|
30
|
-
catch {
|
|
31
|
-
|
|
45
|
+
catch (cause) {
|
|
46
|
+
throw new SkillsApiError(`Skills search request failed: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
47
|
+
}
|
|
48
|
+
if (!res.ok) {
|
|
49
|
+
throw new SkillsApiError(`Skills search returned HTTP ${res.status}`, res.status);
|
|
32
50
|
}
|
|
51
|
+
const data = (await res.json());
|
|
52
|
+
return data.skills || [];
|
|
33
53
|
}
|
|
34
54
|
/**
|
|
35
55
|
* Fetch skills from a specific GitHub repo (via our proxy)
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* - Recommended skills list
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
//
|
|
11
|
+
// Deployed `skills` Gen2 Cloud Function (project claudish-6da10, us-central1).
|
|
12
|
+
// Firebase keeps this cloudfunctions.net alias stable for the function.
|
|
12
13
|
const SKILLS_API_BASE =
|
|
13
14
|
process.env.SKILLS_API_URL || "https://us-central1-claudish-6da10.cloudfunctions.net/skills";
|
|
14
15
|
|
|
@@ -31,7 +32,26 @@ export interface RepoSkillResult {
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
/**
|
|
34
|
-
*
|
|
35
|
+
* Thrown when the skills API is unreachable or returns a non-OK status.
|
|
36
|
+
* Lets callers distinguish "search service is down" from "no skills matched",
|
|
37
|
+
* which a bare `[]` return cannot express.
|
|
38
|
+
*/
|
|
39
|
+
export class SkillsApiError extends Error {
|
|
40
|
+
constructor(
|
|
41
|
+
message: string,
|
|
42
|
+
readonly status?: number,
|
|
43
|
+
) {
|
|
44
|
+
super(message);
|
|
45
|
+
this.name = "SkillsApiError";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Search skills across all sources (SkillsMP keyword + AI search).
|
|
51
|
+
*
|
|
52
|
+
* Throws {@link SkillsApiError} on transport/HTTP failure so the caller can
|
|
53
|
+
* surface a distinct "search unavailable" state. An empty array means the
|
|
54
|
+
* query genuinely matched nothing.
|
|
35
55
|
*/
|
|
36
56
|
export async function searchSkills(
|
|
37
57
|
query: string,
|
|
@@ -41,16 +61,26 @@ export async function searchSkills(
|
|
|
41
61
|
if (options?.page) params.set("page", String(options.page));
|
|
42
62
|
if (options?.limit) params.set("limit", String(options.limit));
|
|
43
63
|
|
|
64
|
+
let res: Response;
|
|
44
65
|
try {
|
|
45
|
-
|
|
66
|
+
res = await fetch(`${SKILLS_API_BASE}/search?${params}`, {
|
|
46
67
|
signal: AbortSignal.timeout(12000),
|
|
47
68
|
});
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return [];
|
|
69
|
+
} catch (cause) {
|
|
70
|
+
throw new SkillsApiError(
|
|
71
|
+
`Skills search request failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
72
|
+
);
|
|
53
73
|
}
|
|
74
|
+
|
|
75
|
+
if (!res.ok) {
|
|
76
|
+
throw new SkillsApiError(
|
|
77
|
+
`Skills search returned HTTP ${res.status}`,
|
|
78
|
+
res.status,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const data = (await res.json()) as { skills?: SkillSearchResult[] };
|
|
83
|
+
return data.skills || [];
|
|
54
84
|
}
|
|
55
85
|
|
|
56
86
|
/**
|
package/src/types/gitignore.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
export type RuleAction = "ignore" | "track";
|
|
8
8
|
|
|
9
|
-
export type Tier = "builtin" | "global" | "project";
|
|
9
|
+
export type Tier = "builtin" | "global" | "project" | "profile";
|
|
10
10
|
|
|
11
11
|
/** A user-authored manifest, deserialized from JSON. */
|
|
12
12
|
export interface GitignoreManifest {
|
package/src/ui/App.js
CHANGED
|
@@ -5,7 +5,7 @@ import fs from "node:fs";
|
|
|
5
5
|
import { AppProvider, useApp, useNavigation, useModal, } from "./state/AppContext.js";
|
|
6
6
|
import { DimensionsProvider, useDimensions, } from "./state/DimensionsContext.js";
|
|
7
7
|
import { ModalContainer } from "./components/modals/index.js";
|
|
8
|
-
import { PluginsScreen, McpScreen, McpRegistryScreen, SettingsScreen, CliToolsScreen, ModelSelectorScreen, ProfilesScreen, SkillsScreen, GitignoreScreen, } from "./screens/index.js";
|
|
8
|
+
import { PluginsScreen, McpScreen, McpRegistryScreen, SettingsScreen, CliToolsScreen, ModelSelectorScreen, ProfilesScreen, SkillsScreen, GitignoreScreen, AliasScreen, } from "./screens/index.js";
|
|
9
9
|
import { checkGitignore } from "../services/gitignore-prerun.js";
|
|
10
10
|
import { loadGitignoreState, applyAllSafeFixes, } from "../services/gitignore-service.js";
|
|
11
11
|
import { useGitignoreModal } from "./hooks/useGitignoreModal.js";
|
|
@@ -45,6 +45,8 @@ function Router() {
|
|
|
45
45
|
return _jsx(SkillsScreen, {});
|
|
46
46
|
case "gitignore":
|
|
47
47
|
return _jsx(GitignoreScreen, {});
|
|
48
|
+
case "alias":
|
|
49
|
+
return _jsx(AliasScreen, {});
|
|
48
50
|
default:
|
|
49
51
|
return _jsx(PluginsScreen, {});
|
|
50
52
|
}
|
|
@@ -86,7 +88,7 @@ function GlobalKeyHandler({ onDebugToggle, onExit, }) {
|
|
|
86
88
|
// Don't handle keys when modal is open or searching
|
|
87
89
|
if (state.modal || state.isSearching)
|
|
88
90
|
return;
|
|
89
|
-
// Global navigation shortcuts (1-
|
|
91
|
+
// Global navigation shortcuts (1-8) - include mcp-registry as it's a sub-screen of mcp
|
|
90
92
|
const isTopLevel = [
|
|
91
93
|
"plugins",
|
|
92
94
|
"mcp",
|
|
@@ -97,6 +99,7 @@ function GlobalKeyHandler({ onDebugToggle, onExit, }) {
|
|
|
97
99
|
"profiles",
|
|
98
100
|
"skills",
|
|
99
101
|
"gitignore",
|
|
102
|
+
"alias",
|
|
100
103
|
].includes(state.currentRoute.screen);
|
|
101
104
|
if (isTopLevel) {
|
|
102
105
|
if (input === "1")
|
|
@@ -113,6 +116,8 @@ function GlobalKeyHandler({ onDebugToggle, onExit, }) {
|
|
|
113
116
|
navigateToScreen("cli-tools");
|
|
114
117
|
else if (input === "7")
|
|
115
118
|
navigateToScreen("gitignore");
|
|
119
|
+
else if (input === "8")
|
|
120
|
+
navigateToScreen("alias");
|
|
116
121
|
// Tab navigation cycling
|
|
117
122
|
if (key.tab) {
|
|
118
123
|
const screens = [
|
|
@@ -123,6 +128,7 @@ function GlobalKeyHandler({ onDebugToggle, onExit, }) {
|
|
|
123
128
|
"profiles",
|
|
124
129
|
"cli-tools",
|
|
125
130
|
"gitignore",
|
|
131
|
+
"alias",
|
|
126
132
|
];
|
|
127
133
|
const currentIndex = screens.indexOf(state.currentRoute.screen);
|
|
128
134
|
if (currentIndex !== -1) {
|
|
@@ -157,8 +163,8 @@ function GlobalKeyHandler({ onDebugToggle, onExit, }) {
|
|
|
157
163
|
? This help
|
|
158
164
|
|
|
159
165
|
Quick Navigation
|
|
160
|
-
1 Plugins 4 Settings 7
|
|
161
|
-
2 Skills 5 Profiles
|
|
166
|
+
1 Plugins 4 Settings 7 Git State
|
|
167
|
+
2 Skills 5 Profiles 8 Alias
|
|
162
168
|
3 MCP Servers 6 CLI Tools
|
|
163
169
|
|
|
164
170
|
Plugin Actions
|