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.
Files changed (76) hide show
  1. package/package.json +1 -1
  2. package/src/__tests__/alias-parser.test.ts +317 -0
  3. package/src/__tests__/alias-shell-writer.test.ts +661 -0
  4. package/src/__tests__/alias-store.test.ts +86 -0
  5. package/src/__tests__/gitignore-fixer.test.ts +64 -1
  6. package/src/__tests__/gitignore-prerun.test.ts +2 -2
  7. package/src/__tests__/gitignore-service.test.ts +42 -0
  8. package/src/__tests__/marketplaces.test.ts +40 -0
  9. package/src/__tests__/plugin-manager-fallback.test.ts +120 -0
  10. package/src/__tests__/useGitignoreModal.test.ts +2 -2
  11. package/src/data/alias-flags.js +196 -0
  12. package/src/data/alias-flags.ts +291 -0
  13. package/src/data/gitignore-reasons.js +97 -0
  14. package/src/data/gitignore-reasons.ts +103 -0
  15. package/src/data/marketplaces.js +5 -3
  16. package/src/data/marketplaces.ts +5 -4
  17. package/src/services/alias-settings.js +51 -0
  18. package/src/services/alias-settings.ts +63 -0
  19. package/src/services/alias-shell-writer.js +764 -0
  20. package/src/services/alias-shell-writer.ts +873 -0
  21. package/src/services/alias-store.js +77 -0
  22. package/src/services/alias-store.ts +112 -0
  23. package/src/services/gitignore-fixer.js +70 -10
  24. package/src/services/gitignore-fixer.ts +76 -9
  25. package/src/services/gitignore-prerun.js +3 -3
  26. package/src/services/gitignore-prerun.ts +3 -3
  27. package/src/services/gitignore-service.js +20 -2
  28. package/src/services/gitignore-service.ts +23 -1
  29. package/src/services/marketplace-fetcher.js +96 -0
  30. package/src/services/marketplace-fetcher.ts +137 -0
  31. package/src/services/plugin-manager.js +6 -59
  32. package/src/services/plugin-manager.ts +16 -91
  33. package/src/services/skillsmp-client.js +29 -9
  34. package/src/services/skillsmp-client.ts +38 -8
  35. package/src/types/gitignore.ts +1 -1
  36. package/src/ui/App.js +10 -4
  37. package/src/ui/App.tsx +9 -3
  38. package/src/ui/components/TabBar.js +2 -1
  39. package/src/ui/components/TabBar.tsx +2 -1
  40. package/src/ui/components/layout/FooterHints.js +29 -0
  41. package/src/ui/components/layout/FooterHints.tsx +52 -0
  42. package/src/ui/components/layout/ScreenLayout.js +2 -1
  43. package/src/ui/components/layout/ScreenLayout.tsx +12 -3
  44. package/src/ui/components/layout/index.js +1 -0
  45. package/src/ui/components/layout/index.ts +5 -0
  46. package/src/ui/components/modals/SelectModal.js +8 -1
  47. package/src/ui/components/modals/SelectModal.tsx +12 -1
  48. package/src/ui/hooks/useGitignoreModal.js +7 -8
  49. package/src/ui/hooks/useGitignoreModal.ts +8 -9
  50. package/src/ui/renderers/gitignoreRenderers.js +36 -23
  51. package/src/ui/renderers/gitignoreRenderers.tsx +50 -41
  52. package/src/ui/screens/AliasScreen.js +1008 -0
  53. package/src/ui/screens/AliasScreen.tsx +1402 -0
  54. package/src/ui/screens/CliToolsScreen.js +6 -1
  55. package/src/ui/screens/CliToolsScreen.tsx +6 -1
  56. package/src/ui/screens/EnvVarsScreen.js +6 -1
  57. package/src/ui/screens/EnvVarsScreen.tsx +6 -1
  58. package/src/ui/screens/GitignoreScreen.js +189 -88
  59. package/src/ui/screens/GitignoreScreen.tsx +312 -132
  60. package/src/ui/screens/McpRegistryScreen.js +13 -2
  61. package/src/ui/screens/McpRegistryScreen.tsx +13 -2
  62. package/src/ui/screens/McpScreen.js +6 -1
  63. package/src/ui/screens/McpScreen.tsx +6 -1
  64. package/src/ui/screens/ModelSelectorScreen.js +8 -2
  65. package/src/ui/screens/ModelSelectorScreen.tsx +8 -2
  66. package/src/ui/screens/PluginsScreen.js +13 -2
  67. package/src/ui/screens/PluginsScreen.tsx +13 -2
  68. package/src/ui/screens/ProfilesScreen.js +8 -1
  69. package/src/ui/screens/ProfilesScreen.tsx +8 -1
  70. package/src/ui/screens/SkillsScreen.js +21 -4
  71. package/src/ui/screens/SkillsScreen.tsx +39 -5
  72. package/src/ui/screens/StatusLineScreen.js +7 -1
  73. package/src/ui/screens/StatusLineScreen.tsx +7 -1
  74. package/src/ui/screens/index.js +1 -0
  75. package/src/ui/screens/index.ts +1 -0
  76. 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, isValidGitHubRepo, parsePluginId, } from "../utils/string-utils.js";
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 fetchMarketplacePlugins(mpName, marketplace.source.repo);
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 fetchMarketplacePlugins(mpName, marketplace.source.repo);
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
- marketplaceCache.clear();
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 fetchMarketplacePlugins(
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 fetchMarketplacePlugins(
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
- marketplaceCache.clear();
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
- // TODO: Update to production URL after Firebase deploy
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
- * Search skills across all sources (SkillsMP keyword + AI search)
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
- const res = await fetch(`${SKILLS_API_BASE}/search?${params}`, {
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
- return [];
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
- // TODO: Update to production URL after Firebase deploy
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
- * Search skills across all sources (SkillsMP keyword + AI search)
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
- const res = await fetch(`${SKILLS_API_BASE}/search?${params}`, {
66
+ res = await fetch(`${SKILLS_API_BASE}/search?${params}`, {
46
67
  signal: AbortSignal.timeout(12000),
47
68
  });
48
- if (!res.ok) return [];
49
- const data = (await res.json()) as { skills: SkillSearchResult[] };
50
- return data.skills || [];
51
- } catch {
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
  /**
@@ -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-7) - include mcp-registry as it's a sub-screen of mcp
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 Gitignore
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