claudeup 4.18.0 → 4.22.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/bin/claudeup.js +43 -19
- package/package.json +14 -19
- package/scripts/build-binaries.ts +77 -0
- package/scripts/check-optional-deps.ts +49 -0
- package/scripts/test-isolated.ts +43 -0
- package/src/__tests__/alias-adopt.test.ts +364 -0
- package/src/__tests__/alias-parser.test.ts +92 -0
- package/src/__tests__/alias-shell-writer.test.ts +7 -0
- package/src/__tests__/alias-store.test.ts +77 -0
- package/src/__tests__/cli-router.test.ts +65 -0
- package/src/__tests__/conventions-integration.test.ts +745 -0
- package/src/__tests__/conventions-manager.test.ts +1172 -0
- package/src/__tests__/doctor-bins.test.ts +35 -0
- package/src/__tests__/doctor.test.ts +450 -0
- package/src/__tests__/dual-write-prevention.test.ts +14 -4
- package/src/__tests__/file-locking.test.ts +208 -0
- package/src/__tests__/gitignore-prerun.test.ts +1 -0
- package/src/__tests__/install-command.test.ts +81 -0
- package/src/__tests__/install-plan.test.ts +98 -0
- package/src/__tests__/manifest.test.ts +136 -0
- package/src/__tests__/marketplace-refresh.test.ts +267 -0
- package/src/__tests__/plugin-requires.test.ts +133 -0
- package/src/__tests__/plugin-setup.test.ts +118 -0
- package/src/__tests__/profile-command.test.ts +116 -0
- package/src/__tests__/profile-materializer.test.ts +82 -0
- package/src/__tests__/profile-sync.test.ts +136 -0
- package/src/__tests__/registry-version-resolution.test.ts +74 -0
- package/src/__tests__/resolve-executable.test.ts +42 -0
- package/src/__tests__/resolver.test.ts +218 -0
- package/src/__tests__/symlink-manager.test.ts +99 -0
- package/src/__tests__/toolchain.test.ts +56 -0
- package/src/cli/claude.ts +21 -0
- package/src/cli/doctor.ts +132 -0
- package/src/cli/install.ts +360 -0
- package/src/cli/profile.ts +166 -0
- package/src/cli/router.ts +107 -0
- package/src/cli/update.ts +85 -0
- package/src/data/alias-flags.ts +11 -1
- package/src/data/cli-tools.ts +7 -7
- package/src/data/gitignore-defaults.ts +4 -0
- package/src/data/marketplaces.ts +12 -0
- package/src/data/predefined-profiles.ts +3 -4
- package/src/main.tsx +11 -154
- package/src/prerunner/index.ts +62 -27
- package/src/services/alias-shell-writer.ts +382 -8
- package/src/services/alias-store.ts +60 -0
- package/src/services/claude-cli.ts +19 -22
- package/src/services/claude-settings.ts +99 -26
- package/src/services/conventions-manager.ts +865 -0
- package/src/services/doctor-bins.ts +49 -0
- package/src/services/doctor.ts +509 -0
- package/src/services/install-plan.ts +107 -0
- package/src/services/manifest.ts +166 -0
- package/src/services/marketplace-refresh.ts +173 -0
- package/src/services/plugin-manager.ts +7 -1
- package/src/services/plugin-requires.ts +215 -0
- package/src/services/plugin-setup.ts +61 -4
- package/src/services/plugin-version-check.ts +9 -4
- package/src/services/profile-materializer.ts +61 -0
- package/src/services/profile-sync.ts +150 -0
- package/src/services/resolver.ts +293 -0
- package/src/services/symlink-manager.ts +146 -0
- package/src/services/toolchain.ts +97 -0
- package/src/services/version-check.ts +10 -11
- package/src/types/index.ts +138 -31
- package/src/ui/App.tsx +16 -11
- package/src/ui/components/FlagDetailEditor.tsx +0 -0
- package/src/ui/components/modals/ConfirmModal.tsx +1 -1
- package/src/ui/screens/AliasScreen.tsx +491 -359
- package/src/ui/screens/CliToolsScreen.tsx +11 -3
- package/src/ui/screens/PluginsScreen.tsx +3 -1
- package/src/ui/screens/index.ts +0 -1
- package/src/ui/state/reducer.ts +6 -102
- package/src/ui/state/types.ts +8 -35
- package/src/utils/command-utils.ts +23 -0
- package/src/utils/file-locking.ts +144 -0
- package/src/data/alias-flags.js +0 -196
- package/src/data/cli-tools.js +0 -123
- package/src/data/gitignore-defaults.js +0 -24
- package/src/data/gitignore-reasons.js +0 -97
- package/src/data/gitignore-templates.js +0 -21
- package/src/data/marketplaces.js +0 -138
- package/src/data/mcp-servers.js +0 -509
- package/src/data/predefined-profiles.js +0 -248
- package/src/data/settings-catalog.js +0 -625
- package/src/data/skill-repos.js +0 -160
- package/src/data/statuslines.js +0 -159
- package/src/data/statuslines.ts +0 -188
- package/src/index.js +0 -4
- package/src/index.ts +0 -5
- package/src/main.js +0 -170
- package/src/prerunner/index.js +0 -252
- package/src/services/alias-settings.js +0 -51
- package/src/services/alias-shell-writer.js +0 -764
- package/src/services/alias-store.js +0 -77
- package/src/services/claude-cli.js +0 -189
- package/src/services/claude-runner.js +0 -28
- package/src/services/claude-settings.js +0 -1223
- package/src/services/gitignore-detector.js +0 -155
- package/src/services/gitignore-fixer.js +0 -231
- package/src/services/gitignore-prerun.js +0 -46
- package/src/services/gitignore-resolver.js +0 -143
- package/src/services/gitignore-service.js +0 -117
- package/src/services/local-marketplace.js +0 -339
- package/src/services/marketplace-fetcher.js +0 -96
- package/src/services/marketplace-sync.js +0 -94
- package/src/services/mcp-registry.js +0 -87
- package/src/services/plugin-manager.js +0 -473
- package/src/services/plugin-mcp-config.js +0 -177
- package/src/services/plugin-setup.js +0 -444
- package/src/services/plugin-version-check.js +0 -262
- package/src/services/profiles.js +0 -161
- package/src/services/settings-manager.js +0 -243
- package/src/services/skills-manager.js +0 -393
- package/src/services/skillsmp-client.js +0 -87
- package/src/services/update-cache.js +0 -52
- package/src/services/version-check.js +0 -99
- package/src/types/gitignore.js +0 -6
- package/src/types/index.js +0 -1
- package/src/ui/App.js +0 -346
- package/src/ui/adapters/pluginsAdapter.js +0 -139
- package/src/ui/adapters/settingsAdapter.js +0 -111
- package/src/ui/adapters/skillsAdapter.js +0 -154
- package/src/ui/components/CategoryHeader.js +0 -9
- package/src/ui/components/EmptyFilterState.js +0 -4
- package/src/ui/components/ScopeIndicator.js +0 -30
- package/src/ui/components/ScrollableList.js +0 -36
- package/src/ui/components/SearchInput.js +0 -19
- package/src/ui/components/StyledText.js +0 -39
- package/src/ui/components/TabBar.js +0 -19
- package/src/ui/components/layout/FooterHints.js +0 -29
- package/src/ui/components/layout/Panel.js +0 -6
- package/src/ui/components/layout/ProgressBar.js +0 -14
- package/src/ui/components/layout/ScopeTabs.js +0 -6
- package/src/ui/components/layout/ScreenLayout.js +0 -16
- package/src/ui/components/layout/index.js +0 -5
- package/src/ui/components/modals/ConfirmModal.js +0 -14
- package/src/ui/components/modals/InputModal.js +0 -5
- package/src/ui/components/modals/LoadingModal.js +0 -17
- package/src/ui/components/modals/MessageModal.js +0 -16
- package/src/ui/components/modals/ModalContainer.js +0 -137
- package/src/ui/components/modals/SelectModal.js +0 -18
- package/src/ui/components/modals/VersionMismatchModal.js +0 -36
- package/src/ui/components/modals/index.js +0 -6
- package/src/ui/components/primitives/ActionHints.js +0 -13
- package/src/ui/components/primitives/DetailSection.js +0 -7
- package/src/ui/components/primitives/KeyValueLine.js +0 -8
- package/src/ui/components/primitives/ListCategoryRow.js +0 -8
- package/src/ui/components/primitives/MetaText.js +0 -8
- package/src/ui/components/primitives/ScopeDetail.js +0 -32
- package/src/ui/components/primitives/ScopeSquares.js +0 -11
- package/src/ui/components/primitives/SelectableRow.js +0 -5
- package/src/ui/components/primitives/index.js +0 -8
- package/src/ui/hooks/index.js +0 -4
- package/src/ui/hooks/useAsyncData.js +0 -77
- package/src/ui/hooks/useGitignoreModal.js +0 -74
- package/src/ui/hooks/useKeyboard.js +0 -13
- package/src/ui/hooks/useKeyboardHandler.js +0 -39
- package/src/ui/hooks/useMismatchModal.js +0 -77
- package/src/ui/registry.js +0 -1
- package/src/ui/renderers/cliToolRenderers.js +0 -54
- package/src/ui/renderers/gitignoreRenderers.js +0 -46
- package/src/ui/renderers/mcpRenderers.js +0 -26
- package/src/ui/renderers/pluginRenderers.js +0 -124
- package/src/ui/renderers/profileRenderers.js +0 -177
- package/src/ui/renderers/settingsRenderers.js +0 -73
- package/src/ui/renderers/skillRenderers.js +0 -138
- package/src/ui/screens/AliasScreen.js +0 -1008
- package/src/ui/screens/CliToolsScreen.js +0 -338
- package/src/ui/screens/EnvVarsScreen.js +0 -152
- package/src/ui/screens/GitignoreScreen.js +0 -328
- package/src/ui/screens/McpRegistryScreen.js +0 -238
- package/src/ui/screens/McpScreen.js +0 -176
- package/src/ui/screens/ModelSelectorScreen.js +0 -296
- package/src/ui/screens/ModelSelectorScreen.tsx +0 -444
- package/src/ui/screens/PluginsScreen.js +0 -766
- package/src/ui/screens/ProfilesScreen.js +0 -298
- package/src/ui/screens/SkillsScreen.js +0 -549
- package/src/ui/screens/StatusLineScreen.js +0 -206
- package/src/ui/screens/StatusLineScreen.tsx +0 -416
- package/src/ui/screens/index.js +0 -10
- package/src/ui/state/AnimationContext.js +0 -34
- package/src/ui/state/AppContext.js +0 -162
- package/src/ui/state/DimensionsContext.js +0 -71
- package/src/ui/state/reducer.js +0 -543
- package/src/ui/state/types.js +0 -1
- package/src/ui/theme.js +0 -47
- package/src/utils/clipboard.js +0 -56
- package/src/utils/command-utils.js +0 -19
- package/src/utils/fuzzy-search.js +0 -101
- package/src/utils/string-utils.js +0 -62
|
@@ -1,393 +0,0 @@
|
|
|
1
|
-
import fs from "fs-extra";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import { RECOMMENDED_SKILLS, classifyStarReliability } from "../data/skill-repos.js";
|
|
5
|
-
const SKILLS_API_BASE = "https://us-central1-claudish-6da10.cloudfunctions.net/skills";
|
|
6
|
-
const treeCache = new Map();
|
|
7
|
-
// ─── Path helpers ──────────────────────────────────────────────────────────────
|
|
8
|
-
export function getUserSkillsDir() {
|
|
9
|
-
return path.join(os.homedir(), ".claude", "skills");
|
|
10
|
-
}
|
|
11
|
-
export function getProjectSkillsDir(projectPath) {
|
|
12
|
-
return path.join(projectPath ?? process.cwd(), ".claude", "skills");
|
|
13
|
-
}
|
|
14
|
-
// ─── GitHub Tree API ──────────────────────────────────────────────────────────
|
|
15
|
-
async function fetchGitTree(repo) {
|
|
16
|
-
const cached = treeCache.get(repo);
|
|
17
|
-
const url = `https://api.github.com/repos/${repo}/git/trees/HEAD?recursive=1`;
|
|
18
|
-
const headers = {
|
|
19
|
-
Accept: "application/vnd.github+json",
|
|
20
|
-
"X-GitHub-Api-Version": "2022-11-28",
|
|
21
|
-
};
|
|
22
|
-
if (cached?.etag) {
|
|
23
|
-
headers["If-None-Match"] = cached.etag;
|
|
24
|
-
}
|
|
25
|
-
const token = process.env.GITHUB_TOKEN || process.env.GITHUB_PERSONAL_ACCESS_TOKEN;
|
|
26
|
-
if (token) {
|
|
27
|
-
headers.Authorization = `Bearer ${token}`;
|
|
28
|
-
}
|
|
29
|
-
const response = await fetch(url, {
|
|
30
|
-
headers,
|
|
31
|
-
signal: AbortSignal.timeout(10000),
|
|
32
|
-
});
|
|
33
|
-
if (response.status === 304 && cached) {
|
|
34
|
-
return cached.tree;
|
|
35
|
-
}
|
|
36
|
-
if (response.status === 403 || response.status === 429) {
|
|
37
|
-
const resetHeader = response.headers.get("X-RateLimit-Reset");
|
|
38
|
-
const resetTime = resetHeader
|
|
39
|
-
? new Date(Number(resetHeader) * 1000).toLocaleTimeString()
|
|
40
|
-
: "unknown";
|
|
41
|
-
throw new Error(`GitHub API rate limit exceeded. Resets at ${resetTime}. Set GITHUB_TOKEN to increase limits.`);
|
|
42
|
-
}
|
|
43
|
-
if (!response.ok) {
|
|
44
|
-
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
|
|
45
|
-
}
|
|
46
|
-
const etag = response.headers.get("ETag") || "";
|
|
47
|
-
const tree = (await response.json());
|
|
48
|
-
treeCache.set(repo, { etag, tree, fetchedAt: Date.now() });
|
|
49
|
-
return tree;
|
|
50
|
-
}
|
|
51
|
-
// ─── Skill Set fetching ──────────────────────────────────────────────────────
|
|
52
|
-
/**
|
|
53
|
-
* Fetch all skills from a skill set repo using the GitHub Tree API.
|
|
54
|
-
* Returns SkillInfo[] with installed status marked via disk scan.
|
|
55
|
-
*/
|
|
56
|
-
export async function fetchSkillSetSkills(repo, projectPath) {
|
|
57
|
-
const tree = await fetchGitTree(repo);
|
|
58
|
-
// Find all SKILL.md blobs
|
|
59
|
-
const skillBlobs = tree.tree.filter((entry) => entry.type === "blob" && entry.path.endsWith("/SKILL.md"));
|
|
60
|
-
const userInstalled = await getInstalledSkillNames("user");
|
|
61
|
-
const projectInstalled = await getInstalledSkillNames("project", projectPath);
|
|
62
|
-
const slugify = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
63
|
-
const source = {
|
|
64
|
-
label: repo,
|
|
65
|
-
repo,
|
|
66
|
-
skillsPath: "",
|
|
67
|
-
};
|
|
68
|
-
return skillBlobs.map((blob) => {
|
|
69
|
-
// Derive skill name from parent directory of SKILL.md
|
|
70
|
-
const parts = blob.path.split("/");
|
|
71
|
-
const name = parts[parts.length - 2]; // e.g. "huggingface-datasets"
|
|
72
|
-
const repoPath = blob.path; // e.g. "skills/huggingface-datasets/SKILL.md"
|
|
73
|
-
const slug = slugify(name);
|
|
74
|
-
const isUser = userInstalled.has(slug) || userInstalled.has(name);
|
|
75
|
-
const isProj = projectInstalled.has(slug) || projectInstalled.has(name);
|
|
76
|
-
const installed = isUser || isProj;
|
|
77
|
-
const installedScope = isProj
|
|
78
|
-
? "project"
|
|
79
|
-
: isUser
|
|
80
|
-
? "user"
|
|
81
|
-
: null;
|
|
82
|
-
return {
|
|
83
|
-
id: `${repo}/${blob.path.replace("/SKILL.md", "")}`,
|
|
84
|
-
name,
|
|
85
|
-
source,
|
|
86
|
-
repoPath,
|
|
87
|
-
gitBlobSha: blob.sha,
|
|
88
|
-
frontmatter: null,
|
|
89
|
-
installed,
|
|
90
|
-
installedScope,
|
|
91
|
-
hasUpdate: false,
|
|
92
|
-
description: "",
|
|
93
|
-
};
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
// ─── Frontmatter parser ───────────────────────────────────────────────────────
|
|
97
|
-
function parseYamlFrontmatter(content) {
|
|
98
|
-
const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
99
|
-
if (!frontmatterMatch)
|
|
100
|
-
return {};
|
|
101
|
-
const yaml = frontmatterMatch[1];
|
|
102
|
-
const result = {};
|
|
103
|
-
for (const line of yaml.split("\n")) {
|
|
104
|
-
const colonIdx = line.indexOf(":");
|
|
105
|
-
if (colonIdx === -1)
|
|
106
|
-
continue;
|
|
107
|
-
const key = line.slice(0, colonIdx).trim();
|
|
108
|
-
const rawValue = line.slice(colonIdx + 1).trim();
|
|
109
|
-
if (!key)
|
|
110
|
-
continue;
|
|
111
|
-
// Handle arrays (simple inline: [a, b, c])
|
|
112
|
-
if (rawValue.startsWith("[") && rawValue.endsWith("]")) {
|
|
113
|
-
const items = rawValue
|
|
114
|
-
.slice(1, -1)
|
|
115
|
-
.split(",")
|
|
116
|
-
.map((s) => s.trim().replace(/^["']|["']$/g, ""))
|
|
117
|
-
.filter(Boolean);
|
|
118
|
-
result[key] = items;
|
|
119
|
-
}
|
|
120
|
-
else {
|
|
121
|
-
// Strip quotes
|
|
122
|
-
result[key] = rawValue.replace(/^["']|["']$/g, "");
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
return result;
|
|
126
|
-
}
|
|
127
|
-
export async function fetchSkillFrontmatter(skill) {
|
|
128
|
-
const url = `https://raw.githubusercontent.com/${skill.source.repo}/HEAD/${skill.repoPath}`;
|
|
129
|
-
const response = await fetch(url, {
|
|
130
|
-
signal: AbortSignal.timeout(10000),
|
|
131
|
-
});
|
|
132
|
-
if (!response.ok) {
|
|
133
|
-
return {
|
|
134
|
-
name: skill.name,
|
|
135
|
-
description: "(no description)",
|
|
136
|
-
category: "general",
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
const content = await response.text();
|
|
140
|
-
const parsed = parseYamlFrontmatter(content);
|
|
141
|
-
return {
|
|
142
|
-
name: parsed.name || skill.name,
|
|
143
|
-
description: parsed.description || "(no description)",
|
|
144
|
-
category: parsed.category,
|
|
145
|
-
author: parsed.author,
|
|
146
|
-
version: parsed.version,
|
|
147
|
-
tags: parsed.tags,
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
// ─── Check installation ───────────────────────────────────────────────────────
|
|
151
|
-
export async function getInstalledSkillNames(scope, projectPath) {
|
|
152
|
-
const dir = scope === "user"
|
|
153
|
-
? getUserSkillsDir()
|
|
154
|
-
: getProjectSkillsDir(projectPath);
|
|
155
|
-
const installed = new Set();
|
|
156
|
-
try {
|
|
157
|
-
if (!(await fs.pathExists(dir)))
|
|
158
|
-
return installed;
|
|
159
|
-
const entries = await fs.readdir(dir);
|
|
160
|
-
for (const entry of entries) {
|
|
161
|
-
const skillMd = path.join(dir, entry, "SKILL.md");
|
|
162
|
-
if (await fs.pathExists(skillMd)) {
|
|
163
|
-
installed.add(entry);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
catch {
|
|
168
|
-
// ignore
|
|
169
|
-
}
|
|
170
|
-
return installed;
|
|
171
|
-
}
|
|
172
|
-
// ─── Firebase Skills API ──────────────────────────────────────────────────────
|
|
173
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
174
|
-
function mapApiSkillToSkillInfo(raw) {
|
|
175
|
-
const repo = raw.repo || "unknown";
|
|
176
|
-
const skillPath = raw.skillPath || raw.name || "";
|
|
177
|
-
const source = {
|
|
178
|
-
label: repo,
|
|
179
|
-
repo,
|
|
180
|
-
skillsPath: "",
|
|
181
|
-
};
|
|
182
|
-
const stars = typeof raw.stars === "number" ? raw.stars : undefined;
|
|
183
|
-
return {
|
|
184
|
-
id: `${repo}/${skillPath}`,
|
|
185
|
-
name: raw.name || skillPath,
|
|
186
|
-
description: raw.description || "",
|
|
187
|
-
source,
|
|
188
|
-
repoPath: skillPath ? `${skillPath}/SKILL.md` : "SKILL.md",
|
|
189
|
-
gitBlobSha: "",
|
|
190
|
-
frontmatter: null,
|
|
191
|
-
installed: false,
|
|
192
|
-
installedScope: null,
|
|
193
|
-
hasUpdate: false,
|
|
194
|
-
stars,
|
|
195
|
-
starReliability: classifyStarReliability(repo, stars),
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
export async function fetchPopularSkills(limit = 30) {
|
|
199
|
-
try {
|
|
200
|
-
const res = await fetch(`${SKILLS_API_BASE}/search?q=development&limit=${limit}&sortBy=stars`, { signal: AbortSignal.timeout(10000) });
|
|
201
|
-
if (!res.ok)
|
|
202
|
-
return [];
|
|
203
|
-
const data = (await res.json());
|
|
204
|
-
return (data.skills || []).map(mapApiSkillToSkillInfo);
|
|
205
|
-
}
|
|
206
|
-
catch {
|
|
207
|
-
return [];
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
// ─── Fetch available skills ───────────────────────────────────────────────────
|
|
211
|
-
export async function fetchAvailableSkills(_repos, projectPath) {
|
|
212
|
-
const userInstalled = await getInstalledSkillNames("user");
|
|
213
|
-
const projectInstalled = await getInstalledSkillNames("project", projectPath);
|
|
214
|
-
const slugify = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
215
|
-
const markInstalled = (skill) => {
|
|
216
|
-
const slug = slugify(skill.name);
|
|
217
|
-
const isUserInstalled = userInstalled.has(slug) || userInstalled.has(skill.name);
|
|
218
|
-
const isProjInstalled = projectInstalled.has(slug) || projectInstalled.has(skill.name);
|
|
219
|
-
const installed = isUserInstalled || isProjInstalled;
|
|
220
|
-
const installedScope = isProjInstalled
|
|
221
|
-
? "project"
|
|
222
|
-
: isUserInstalled
|
|
223
|
-
? "user"
|
|
224
|
-
: null;
|
|
225
|
-
return { ...skill, installed, installedScope };
|
|
226
|
-
};
|
|
227
|
-
// 1. Recommended skills from RECOMMENDED_SKILLS constant (no API call)
|
|
228
|
-
const recommendedSkills = RECOMMENDED_SKILLS.map((rec) => {
|
|
229
|
-
const source = {
|
|
230
|
-
label: rec.repo,
|
|
231
|
-
repo: rec.repo,
|
|
232
|
-
skillsPath: "",
|
|
233
|
-
};
|
|
234
|
-
const skill = {
|
|
235
|
-
id: `${rec.repo}/${rec.skillPath}`,
|
|
236
|
-
name: rec.name,
|
|
237
|
-
description: rec.description,
|
|
238
|
-
source,
|
|
239
|
-
repoPath: `${rec.skillPath}/SKILL.md`,
|
|
240
|
-
gitBlobSha: "",
|
|
241
|
-
frontmatter: null,
|
|
242
|
-
installed: false,
|
|
243
|
-
installedScope: null,
|
|
244
|
-
hasUpdate: false,
|
|
245
|
-
isRecommended: true,
|
|
246
|
-
starReliability: classifyStarReliability(rec.repo, rec.stars),
|
|
247
|
-
};
|
|
248
|
-
return markInstalled(skill);
|
|
249
|
-
});
|
|
250
|
-
// 2. Fetch popular skills from Firebase API
|
|
251
|
-
const popular = await fetchPopularSkills(30);
|
|
252
|
-
const popularSkills = popular.map((s) => markInstalled({ ...s, isRecommended: false }));
|
|
253
|
-
// 3. Enrich recommended skills with GitHub repo stars (cached to disk)
|
|
254
|
-
const starsCachePath = path.join(os.homedir(), ".claude", "skill-stars-cache.json");
|
|
255
|
-
let starsCache = {};
|
|
256
|
-
try {
|
|
257
|
-
starsCache = await fs.readJson(starsCachePath);
|
|
258
|
-
}
|
|
259
|
-
catch { /* no cache yet */ }
|
|
260
|
-
const uniqueRepos = [...new Set(recommendedSkills.map((s) => s.source.repo))];
|
|
261
|
-
const repoStars = new Map();
|
|
262
|
-
const cacheMaxAge = 24 * 60 * 60 * 1000; // 24 hours
|
|
263
|
-
let cacheUpdated = false;
|
|
264
|
-
for (const repo of uniqueRepos) {
|
|
265
|
-
const cached = starsCache[repo];
|
|
266
|
-
if (cached && Date.now() - new Date(cached.fetchedAt).getTime() < cacheMaxAge) {
|
|
267
|
-
repoStars.set(repo, cached.stars);
|
|
268
|
-
continue;
|
|
269
|
-
}
|
|
270
|
-
// Try fetching from GitHub (may be rate limited)
|
|
271
|
-
try {
|
|
272
|
-
const res = await fetch(`https://api.github.com/repos/${repo}`, {
|
|
273
|
-
headers: { Accept: "application/vnd.github+json" },
|
|
274
|
-
signal: AbortSignal.timeout(5000),
|
|
275
|
-
});
|
|
276
|
-
if (res.ok) {
|
|
277
|
-
const data = (await res.json());
|
|
278
|
-
if (data.stargazers_count) {
|
|
279
|
-
repoStars.set(repo, data.stargazers_count);
|
|
280
|
-
starsCache[repo] = { stars: data.stargazers_count, fetchedAt: new Date().toISOString() };
|
|
281
|
-
cacheUpdated = true;
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
else if (cached) {
|
|
285
|
-
// Rate limited but have stale cache — use it
|
|
286
|
-
repoStars.set(repo, cached.stars);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
catch {
|
|
290
|
-
if (cached)
|
|
291
|
-
repoStars.set(repo, cached.stars);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
if (cacheUpdated) {
|
|
295
|
-
try {
|
|
296
|
-
await fs.writeJson(starsCachePath, starsCache);
|
|
297
|
-
}
|
|
298
|
-
catch { /* ignore */ }
|
|
299
|
-
}
|
|
300
|
-
for (const rec of recommendedSkills) {
|
|
301
|
-
rec.stars = repoStars.get(rec.source.repo) || rec.stars || undefined;
|
|
302
|
-
}
|
|
303
|
-
// 4. Combine: recommended first, then popular (dedup by name)
|
|
304
|
-
const seen = new Set(recommendedSkills.map((s) => s.name));
|
|
305
|
-
const deduped = popularSkills.filter((s) => !seen.has(s.name));
|
|
306
|
-
return [...recommendedSkills, ...deduped];
|
|
307
|
-
}
|
|
308
|
-
// ─── Install / Uninstall ──────────────────────────────────────────────────────
|
|
309
|
-
export async function installSkill(skill, scope, projectPath) {
|
|
310
|
-
// Try multiple URL patterns — repos structure SKILL.md differently
|
|
311
|
-
const repo = skill.source.repo;
|
|
312
|
-
const repoPath = skill.repoPath.replace(/\/SKILL\.md$/, "");
|
|
313
|
-
const candidates = [
|
|
314
|
-
`https://raw.githubusercontent.com/${repo}/HEAD/${repoPath}/SKILL.md`,
|
|
315
|
-
`https://raw.githubusercontent.com/${repo}/main/${repoPath}/SKILL.md`,
|
|
316
|
-
`https://raw.githubusercontent.com/${repo}/master/${repoPath}/SKILL.md`,
|
|
317
|
-
`https://raw.githubusercontent.com/${repo}/HEAD/SKILL.md`,
|
|
318
|
-
`https://raw.githubusercontent.com/${repo}/main/SKILL.md`,
|
|
319
|
-
];
|
|
320
|
-
let content = null;
|
|
321
|
-
for (const url of candidates) {
|
|
322
|
-
try {
|
|
323
|
-
const response = await fetch(url, { signal: AbortSignal.timeout(8000) });
|
|
324
|
-
if (response.ok) {
|
|
325
|
-
content = await response.text();
|
|
326
|
-
break;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
catch {
|
|
330
|
-
continue;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
if (!content) {
|
|
334
|
-
throw new Error(`Failed to fetch skill: SKILL.md not found in ${repo}/${repoPath}`);
|
|
335
|
-
}
|
|
336
|
-
// Use slug for directory name — display names can have slashes/spaces
|
|
337
|
-
const dirName = skill.name
|
|
338
|
-
.toLowerCase()
|
|
339
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
340
|
-
.replace(/^-|-$/g, "");
|
|
341
|
-
const installDir = scope === "user"
|
|
342
|
-
? path.join(getUserSkillsDir(), dirName)
|
|
343
|
-
: path.join(getProjectSkillsDir(projectPath), dirName);
|
|
344
|
-
await fs.ensureDir(installDir);
|
|
345
|
-
await fs.writeFile(path.join(installDir, "SKILL.md"), content, "utf8");
|
|
346
|
-
}
|
|
347
|
-
export async function uninstallSkill(skillName, scope, projectPath) {
|
|
348
|
-
const dirName = skillName
|
|
349
|
-
.toLowerCase()
|
|
350
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
351
|
-
.replace(/^-|-$/g, "");
|
|
352
|
-
const installDir = scope === "user"
|
|
353
|
-
? path.join(getUserSkillsDir(), dirName)
|
|
354
|
-
: path.join(getProjectSkillsDir(projectPath), dirName);
|
|
355
|
-
const skillMdPath = path.join(installDir, "SKILL.md");
|
|
356
|
-
if (await fs.pathExists(skillMdPath)) {
|
|
357
|
-
await fs.remove(skillMdPath);
|
|
358
|
-
}
|
|
359
|
-
// Try to remove directory if empty
|
|
360
|
-
try {
|
|
361
|
-
await fs.rmdir(installDir);
|
|
362
|
-
}
|
|
363
|
-
catch {
|
|
364
|
-
// Ignore if not empty or doesn't exist
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
// ─── Check installed skills from file system ──────────────────────────────────
|
|
368
|
-
export async function getInstalledSkillsFromFs(projectPath) {
|
|
369
|
-
const result = [];
|
|
370
|
-
const userDir = getUserSkillsDir();
|
|
371
|
-
const projDir = getProjectSkillsDir(projectPath);
|
|
372
|
-
const scopedDirs = [
|
|
373
|
-
[userDir, "user"],
|
|
374
|
-
[projDir, "project"],
|
|
375
|
-
];
|
|
376
|
-
for (const [dir, scope] of scopedDirs) {
|
|
377
|
-
try {
|
|
378
|
-
if (!(await fs.pathExists(dir)))
|
|
379
|
-
continue;
|
|
380
|
-
const entries = await fs.readdir(dir);
|
|
381
|
-
for (const entry of entries) {
|
|
382
|
-
const skillMd = path.join(dir, entry, "SKILL.md");
|
|
383
|
-
if (await fs.pathExists(skillMd)) {
|
|
384
|
-
result.push({ name: entry, scope });
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
catch {
|
|
389
|
-
// ignore
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
return result;
|
|
393
|
-
}
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Skills API client — calls our Firebase aggregator function
|
|
3
|
-
* instead of hitting SkillsMP/GitHub APIs directly.
|
|
4
|
-
*
|
|
5
|
-
* The Firebase function handles:
|
|
6
|
-
* - SkillsMP keyword + AI search (with server-side API key)
|
|
7
|
-
* - GitHub Tree API with ETag caching
|
|
8
|
-
* - Recommended skills list
|
|
9
|
-
*/
|
|
10
|
-
// Deployed `skills` Gen2 Cloud Function (project claudish-6da10, us-central1).
|
|
11
|
-
// Firebase keeps this cloudfunctions.net alias stable for the function.
|
|
12
|
-
const SKILLS_API_BASE = process.env.SKILLS_API_URL || "https://us-central1-claudish-6da10.cloudfunctions.net/skills";
|
|
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.
|
|
32
|
-
*/
|
|
33
|
-
export async function searchSkills(query, options) {
|
|
34
|
-
const params = new URLSearchParams({ q: query });
|
|
35
|
-
if (options?.page)
|
|
36
|
-
params.set("page", String(options.page));
|
|
37
|
-
if (options?.limit)
|
|
38
|
-
params.set("limit", String(options.limit));
|
|
39
|
-
let res;
|
|
40
|
-
try {
|
|
41
|
-
res = await fetch(`${SKILLS_API_BASE}/search?${params}`, {
|
|
42
|
-
signal: AbortSignal.timeout(12000),
|
|
43
|
-
});
|
|
44
|
-
}
|
|
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);
|
|
50
|
-
}
|
|
51
|
-
const data = (await res.json());
|
|
52
|
-
return data.skills || [];
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Fetch skills from a specific GitHub repo (via our proxy)
|
|
56
|
-
*/
|
|
57
|
-
export async function fetchRepoSkills(owner, repo) {
|
|
58
|
-
try {
|
|
59
|
-
const res = await fetch(`${SKILLS_API_BASE}/repo/${owner}/${repo}`, {
|
|
60
|
-
signal: AbortSignal.timeout(10000),
|
|
61
|
-
});
|
|
62
|
-
if (!res.ok)
|
|
63
|
-
return [];
|
|
64
|
-
const data = (await res.json());
|
|
65
|
-
return data.skills || [];
|
|
66
|
-
}
|
|
67
|
-
catch {
|
|
68
|
-
return [];
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* Get recommended skills list (curated, server-side)
|
|
73
|
-
*/
|
|
74
|
-
export async function fetchRecommendedSkills() {
|
|
75
|
-
try {
|
|
76
|
-
const res = await fetch(`${SKILLS_API_BASE}/recommended`, {
|
|
77
|
-
signal: AbortSignal.timeout(5000),
|
|
78
|
-
});
|
|
79
|
-
if (!res.ok)
|
|
80
|
-
return [];
|
|
81
|
-
const data = (await res.json());
|
|
82
|
-
return data.skills || [];
|
|
83
|
-
}
|
|
84
|
-
catch {
|
|
85
|
-
return [];
|
|
86
|
-
}
|
|
87
|
-
}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { promises as fs } from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
5
|
-
export class UpdateCache {
|
|
6
|
-
cachePath;
|
|
7
|
-
constructor() {
|
|
8
|
-
// Cache at ~/.claude/claudeup-cache.json
|
|
9
|
-
this.cachePath = path.join(os.homedir(), ".claude", "claudeup-cache.json");
|
|
10
|
-
}
|
|
11
|
-
async shouldCheckForUpdates() {
|
|
12
|
-
const data = await this.getLastCheck();
|
|
13
|
-
if (!data)
|
|
14
|
-
return true; // No cache
|
|
15
|
-
const lastCheck = new Date(data.lastUpdateCheck);
|
|
16
|
-
const now = new Date();
|
|
17
|
-
const elapsed = now.getTime() - lastCheck.getTime();
|
|
18
|
-
return elapsed >= CACHE_TTL_MS;
|
|
19
|
-
}
|
|
20
|
-
async getLastCheck() {
|
|
21
|
-
try {
|
|
22
|
-
const content = await fs.readFile(this.cachePath, "utf-8");
|
|
23
|
-
const data = JSON.parse(content);
|
|
24
|
-
return data;
|
|
25
|
-
}
|
|
26
|
-
catch (error) {
|
|
27
|
-
// Cache doesn't exist or is corrupted
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
async saveCheck(result) {
|
|
32
|
-
try {
|
|
33
|
-
// Ensure .claude directory exists
|
|
34
|
-
const cacheDir = path.dirname(this.cachePath);
|
|
35
|
-
await fs.mkdir(cacheDir, { recursive: true });
|
|
36
|
-
// Write cache file
|
|
37
|
-
await fs.writeFile(this.cachePath, JSON.stringify(result, null, 2), "utf-8");
|
|
38
|
-
}
|
|
39
|
-
catch (error) {
|
|
40
|
-
// Non-fatal - just log and continue
|
|
41
|
-
console.warn("Warning: Failed to save update cache:", error instanceof Error ? error.message : "Unknown error");
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
async clear() {
|
|
45
|
-
try {
|
|
46
|
-
await fs.unlink(this.cachePath);
|
|
47
|
-
}
|
|
48
|
-
catch (error) {
|
|
49
|
-
// Ignore errors (file might not exist)
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { createRequire } from "node:module";
|
|
2
|
-
import semver from "semver";
|
|
3
|
-
const require = createRequire(import.meta.url);
|
|
4
|
-
// Cache the result for the session
|
|
5
|
-
let cachedResult = null;
|
|
6
|
-
/**
|
|
7
|
-
* Get the current version from package.json
|
|
8
|
-
*/
|
|
9
|
-
export function getCurrentVersion() {
|
|
10
|
-
try {
|
|
11
|
-
const pkg = require("../../package.json");
|
|
12
|
-
return pkg.version;
|
|
13
|
-
}
|
|
14
|
-
catch {
|
|
15
|
-
return "0.0.0";
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* Fetch the latest version from npm registry
|
|
20
|
-
* Uses a short timeout to avoid blocking startup
|
|
21
|
-
*/
|
|
22
|
-
async function fetchLatestVersion() {
|
|
23
|
-
try {
|
|
24
|
-
const controller = new AbortController();
|
|
25
|
-
const timeout = setTimeout(() => controller.abort(), 3000); // 3s timeout
|
|
26
|
-
const response = await fetch("https://registry.npmjs.org/claudeup/latest", {
|
|
27
|
-
signal: controller.signal,
|
|
28
|
-
headers: {
|
|
29
|
-
Accept: "application/json",
|
|
30
|
-
},
|
|
31
|
-
});
|
|
32
|
-
clearTimeout(timeout);
|
|
33
|
-
if (!response.ok) {
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
const data = (await response.json());
|
|
37
|
-
return data.version || null;
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
40
|
-
// Network error, timeout, or parse error - silently fail
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Determine the type of update (major, minor, patch)
|
|
46
|
-
*/
|
|
47
|
-
function getUpdateType(current, latest) {
|
|
48
|
-
const currentParsed = semver.parse(current);
|
|
49
|
-
const latestParsed = semver.parse(latest);
|
|
50
|
-
if (!currentParsed || !latestParsed)
|
|
51
|
-
return undefined;
|
|
52
|
-
if (latestParsed.major > currentParsed.major)
|
|
53
|
-
return "major";
|
|
54
|
-
if (latestParsed.minor > currentParsed.minor)
|
|
55
|
-
return "minor";
|
|
56
|
-
if (latestParsed.patch > currentParsed.patch)
|
|
57
|
-
return "patch";
|
|
58
|
-
return undefined;
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Check if a new version is available
|
|
62
|
-
* Returns cached result if already checked this session
|
|
63
|
-
*/
|
|
64
|
-
export async function checkForUpdates() {
|
|
65
|
-
// Return cached result if available
|
|
66
|
-
if (cachedResult) {
|
|
67
|
-
return cachedResult;
|
|
68
|
-
}
|
|
69
|
-
const currentVersion = getCurrentVersion();
|
|
70
|
-
const latestVersion = await fetchLatestVersion();
|
|
71
|
-
if (!latestVersion) {
|
|
72
|
-
// Couldn't fetch, assume no update
|
|
73
|
-
cachedResult = {
|
|
74
|
-
currentVersion,
|
|
75
|
-
latestVersion: currentVersion,
|
|
76
|
-
updateAvailable: false,
|
|
77
|
-
};
|
|
78
|
-
return cachedResult;
|
|
79
|
-
}
|
|
80
|
-
const updateAvailable = semver.gt(latestVersion, currentVersion);
|
|
81
|
-
cachedResult = {
|
|
82
|
-
currentVersion,
|
|
83
|
-
latestVersion,
|
|
84
|
-
updateAvailable,
|
|
85
|
-
updateType: updateAvailable
|
|
86
|
-
? getUpdateType(currentVersion, latestVersion)
|
|
87
|
-
: undefined,
|
|
88
|
-
};
|
|
89
|
-
return cachedResult;
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* Format update message for display
|
|
93
|
-
*/
|
|
94
|
-
export function formatUpdateMessage(result) {
|
|
95
|
-
if (!result.updateAvailable)
|
|
96
|
-
return "";
|
|
97
|
-
const typeLabel = result.updateType ? ` (${result.updateType})` : "";
|
|
98
|
-
return `Update available: v${result.currentVersion} → v${result.latestVersion}${typeLabel}. Run: npm i -g claudeup`;
|
|
99
|
-
}
|
package/src/types/gitignore.js
DELETED
package/src/types/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|