claudeup 3.7.2 → 3.9.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/data/settings-catalog.js +612 -0
- package/src/data/settings-catalog.ts +689 -0
- package/src/data/skill-repos.js +86 -0
- package/src/data/skill-repos.ts +97 -0
- package/src/services/plugin-manager.js +2 -0
- package/src/services/plugin-manager.ts +3 -0
- package/src/services/profiles.js +161 -0
- package/src/services/profiles.ts +225 -0
- package/src/services/settings-manager.js +108 -0
- package/src/services/settings-manager.ts +140 -0
- package/src/services/skills-manager.js +239 -0
- package/src/services/skills-manager.ts +328 -0
- package/src/services/skillsmp-client.js +67 -0
- package/src/services/skillsmp-client.ts +89 -0
- package/src/types/index.ts +101 -1
- package/src/ui/App.js +23 -18
- package/src/ui/App.tsx +27 -23
- package/src/ui/components/TabBar.js +9 -8
- package/src/ui/components/TabBar.tsx +15 -19
- package/src/ui/components/layout/ScreenLayout.js +8 -14
- package/src/ui/components/layout/ScreenLayout.tsx +51 -58
- package/src/ui/components/modals/ModalContainer.js +43 -11
- package/src/ui/components/modals/ModalContainer.tsx +44 -12
- package/src/ui/components/modals/SelectModal.js +4 -18
- package/src/ui/components/modals/SelectModal.tsx +10 -21
- package/src/ui/screens/CliToolsScreen.js +2 -2
- package/src/ui/screens/CliToolsScreen.tsx +8 -8
- package/src/ui/screens/EnvVarsScreen.js +248 -116
- package/src/ui/screens/EnvVarsScreen.tsx +419 -184
- package/src/ui/screens/McpRegistryScreen.tsx +18 -6
- package/src/ui/screens/McpScreen.js +1 -1
- package/src/ui/screens/McpScreen.tsx +15 -5
- package/src/ui/screens/ModelSelectorScreen.js +3 -5
- package/src/ui/screens/ModelSelectorScreen.tsx +12 -16
- package/src/ui/screens/PluginsScreen.js +154 -66
- package/src/ui/screens/PluginsScreen.tsx +280 -97
- package/src/ui/screens/ProfilesScreen.js +255 -0
- package/src/ui/screens/ProfilesScreen.tsx +487 -0
- package/src/ui/screens/SkillsScreen.js +325 -0
- package/src/ui/screens/SkillsScreen.tsx +574 -0
- package/src/ui/screens/StatusLineScreen.js +2 -2
- package/src/ui/screens/StatusLineScreen.tsx +10 -12
- package/src/ui/screens/index.js +3 -2
- package/src/ui/screens/index.ts +3 -2
- package/src/ui/state/AppContext.js +2 -1
- package/src/ui/state/AppContext.tsx +2 -0
- package/src/ui/state/reducer.js +151 -19
- package/src/ui/state/reducer.ts +167 -19
- package/src/ui/state/types.ts +58 -14
- package/src/utils/clipboard.js +56 -0
- package/src/utils/clipboard.ts +58 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { readSettings, writeSettings, readGlobalSettings, writeGlobalSettings, } from "./claude-settings.js";
|
|
2
|
+
/** Read the current value of a setting from the given scope */
|
|
3
|
+
export async function readSettingValue(setting, scope, projectPath) {
|
|
4
|
+
const settings = scope === "user"
|
|
5
|
+
? await readGlobalSettings()
|
|
6
|
+
: await readSettings(projectPath);
|
|
7
|
+
if (setting.storage.type === "env") {
|
|
8
|
+
const env = settings.env;
|
|
9
|
+
return env?.[setting.storage.key];
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
// Direct settings key (supports dot notation like "permissions.defaultMode")
|
|
13
|
+
const keys = setting.storage.key.split(".");
|
|
14
|
+
let value = settings;
|
|
15
|
+
for (const k of keys) {
|
|
16
|
+
value = value?.[k];
|
|
17
|
+
}
|
|
18
|
+
return value !== undefined && value !== null ? String(value) : undefined;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** Write a setting value to the given scope */
|
|
22
|
+
export async function writeSettingValue(setting, value, scope, projectPath) {
|
|
23
|
+
const settings = scope === "user"
|
|
24
|
+
? await readGlobalSettings()
|
|
25
|
+
: await readSettings(projectPath);
|
|
26
|
+
if (setting.storage.type === "env") {
|
|
27
|
+
// Write to the "env" block in settings.json
|
|
28
|
+
settings.env = settings.env || {};
|
|
29
|
+
if (value === undefined || value === "" || value === setting.defaultValue) {
|
|
30
|
+
delete settings.env[setting.storage.key];
|
|
31
|
+
// Clean up empty env block
|
|
32
|
+
if (Object.keys(settings.env).length === 0) {
|
|
33
|
+
delete settings.env;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
settings.env[setting.storage.key] = value;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
// Direct settings key (supports dot notation)
|
|
42
|
+
const keys = setting.storage.key.split(".");
|
|
43
|
+
if (keys.length === 1) {
|
|
44
|
+
if (value === undefined || value === "") {
|
|
45
|
+
delete settings[keys[0]];
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
// Try to preserve type: boolean, number, or string
|
|
49
|
+
settings[keys[0]] = parseSettingValue(value, setting.type);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
// Nested key like "permissions.defaultMode"
|
|
54
|
+
let obj = settings;
|
|
55
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
56
|
+
obj[keys[i]] = obj[keys[i]] || {};
|
|
57
|
+
obj = obj[keys[i]];
|
|
58
|
+
}
|
|
59
|
+
const lastKey = keys[keys.length - 1];
|
|
60
|
+
if (value === undefined || value === "") {
|
|
61
|
+
delete obj[lastKey];
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
obj[lastKey] = parseSettingValue(value, setting.type);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (scope === "user") {
|
|
69
|
+
await writeGlobalSettings(settings);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
await writeSettings(settings, projectPath);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function parseSettingValue(value, type) {
|
|
76
|
+
if (type === "boolean") {
|
|
77
|
+
return value === "true" || value === "1";
|
|
78
|
+
}
|
|
79
|
+
// Try number
|
|
80
|
+
const num = Number(value);
|
|
81
|
+
if (!Number.isNaN(num) && value.trim() !== "") {
|
|
82
|
+
return value; // Keep as string for env vars
|
|
83
|
+
}
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
/** Read all setting values for the catalog */
|
|
87
|
+
export async function readAllSettings(catalog, scope, projectPath) {
|
|
88
|
+
const values = new Map();
|
|
89
|
+
for (const setting of catalog) {
|
|
90
|
+
values.set(setting.id, await readSettingValue(setting, scope, projectPath));
|
|
91
|
+
}
|
|
92
|
+
return values;
|
|
93
|
+
}
|
|
94
|
+
/** Read all setting values from both scopes simultaneously */
|
|
95
|
+
export async function readAllSettingsBothScopes(catalog, projectPath) {
|
|
96
|
+
const result = new Map();
|
|
97
|
+
const [userValues, projectValues] = await Promise.all([
|
|
98
|
+
readAllSettings(catalog, "user", projectPath),
|
|
99
|
+
readAllSettings(catalog, "project", projectPath),
|
|
100
|
+
]);
|
|
101
|
+
for (const setting of catalog) {
|
|
102
|
+
result.set(setting.id, {
|
|
103
|
+
user: userValues.get(setting.id),
|
|
104
|
+
project: projectValues.get(setting.id),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readSettings,
|
|
3
|
+
writeSettings,
|
|
4
|
+
readGlobalSettings,
|
|
5
|
+
writeGlobalSettings,
|
|
6
|
+
} from "./claude-settings.js";
|
|
7
|
+
import type { SettingDefinition } from "../data/settings-catalog.js";
|
|
8
|
+
|
|
9
|
+
export type SettingScope = "user" | "project";
|
|
10
|
+
|
|
11
|
+
/** Read the current value of a setting from the given scope */
|
|
12
|
+
export async function readSettingValue(
|
|
13
|
+
setting: SettingDefinition,
|
|
14
|
+
scope: SettingScope,
|
|
15
|
+
projectPath?: string,
|
|
16
|
+
): Promise<string | undefined> {
|
|
17
|
+
const settings =
|
|
18
|
+
scope === "user"
|
|
19
|
+
? await readGlobalSettings()
|
|
20
|
+
: await readSettings(projectPath);
|
|
21
|
+
|
|
22
|
+
if (setting.storage.type === "env") {
|
|
23
|
+
const env = (settings as any).env as Record<string, string> | undefined;
|
|
24
|
+
return env?.[setting.storage.key];
|
|
25
|
+
} else {
|
|
26
|
+
// Direct settings key (supports dot notation like "permissions.defaultMode")
|
|
27
|
+
const keys = setting.storage.key.split(".");
|
|
28
|
+
let value: any = settings;
|
|
29
|
+
for (const k of keys) {
|
|
30
|
+
value = value?.[k];
|
|
31
|
+
}
|
|
32
|
+
return value !== undefined && value !== null ? String(value) : undefined;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Write a setting value to the given scope */
|
|
37
|
+
export async function writeSettingValue(
|
|
38
|
+
setting: SettingDefinition,
|
|
39
|
+
value: string | undefined,
|
|
40
|
+
scope: SettingScope,
|
|
41
|
+
projectPath?: string,
|
|
42
|
+
): Promise<void> {
|
|
43
|
+
const settings =
|
|
44
|
+
scope === "user"
|
|
45
|
+
? await readGlobalSettings()
|
|
46
|
+
: await readSettings(projectPath);
|
|
47
|
+
|
|
48
|
+
if (setting.storage.type === "env") {
|
|
49
|
+
// Write to the "env" block in settings.json
|
|
50
|
+
(settings as any).env = (settings as any).env || {};
|
|
51
|
+
if (value === undefined || value === "" || value === setting.defaultValue) {
|
|
52
|
+
delete (settings as any).env[setting.storage.key];
|
|
53
|
+
// Clean up empty env block
|
|
54
|
+
if (Object.keys((settings as any).env).length === 0) {
|
|
55
|
+
delete (settings as any).env;
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
(settings as any).env[setting.storage.key] = value;
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
// Direct settings key (supports dot notation)
|
|
62
|
+
const keys = setting.storage.key.split(".");
|
|
63
|
+
if (keys.length === 1) {
|
|
64
|
+
if (value === undefined || value === "") {
|
|
65
|
+
delete (settings as any)[keys[0]];
|
|
66
|
+
} else {
|
|
67
|
+
// Try to preserve type: boolean, number, or string
|
|
68
|
+
(settings as any)[keys[0]] = parseSettingValue(value, setting.type);
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
// Nested key like "permissions.defaultMode"
|
|
72
|
+
let obj: any = settings;
|
|
73
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
74
|
+
obj[keys[i]] = obj[keys[i]] || {};
|
|
75
|
+
obj = obj[keys[i]];
|
|
76
|
+
}
|
|
77
|
+
const lastKey = keys[keys.length - 1];
|
|
78
|
+
if (value === undefined || value === "") {
|
|
79
|
+
delete obj[lastKey];
|
|
80
|
+
} else {
|
|
81
|
+
obj[lastKey] = parseSettingValue(value, setting.type);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (scope === "user") {
|
|
87
|
+
await writeGlobalSettings(settings);
|
|
88
|
+
} else {
|
|
89
|
+
await writeSettings(settings, projectPath);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function parseSettingValue(value: string, type: string): any {
|
|
94
|
+
if (type === "boolean") {
|
|
95
|
+
return value === "true" || value === "1";
|
|
96
|
+
}
|
|
97
|
+
// Try number
|
|
98
|
+
const num = Number(value);
|
|
99
|
+
if (!Number.isNaN(num) && value.trim() !== "") {
|
|
100
|
+
return value; // Keep as string for env vars
|
|
101
|
+
}
|
|
102
|
+
return value;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Read all setting values for the catalog */
|
|
106
|
+
export async function readAllSettings(
|
|
107
|
+
catalog: SettingDefinition[],
|
|
108
|
+
scope: SettingScope,
|
|
109
|
+
projectPath?: string,
|
|
110
|
+
): Promise<Map<string, string | undefined>> {
|
|
111
|
+
const values = new Map<string, string | undefined>();
|
|
112
|
+
for (const setting of catalog) {
|
|
113
|
+
values.set(setting.id, await readSettingValue(setting, scope, projectPath));
|
|
114
|
+
}
|
|
115
|
+
return values;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface ScopedSettingValues {
|
|
119
|
+
user: string | undefined;
|
|
120
|
+
project: string | undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Read all setting values from both scopes simultaneously */
|
|
124
|
+
export async function readAllSettingsBothScopes(
|
|
125
|
+
catalog: SettingDefinition[],
|
|
126
|
+
projectPath?: string,
|
|
127
|
+
): Promise<Map<string, ScopedSettingValues>> {
|
|
128
|
+
const result = new Map<string, ScopedSettingValues>();
|
|
129
|
+
const [userValues, projectValues] = await Promise.all([
|
|
130
|
+
readAllSettings(catalog, "user", projectPath),
|
|
131
|
+
readAllSettings(catalog, "project", projectPath),
|
|
132
|
+
]);
|
|
133
|
+
for (const setting of catalog) {
|
|
134
|
+
result.set(setting.id, {
|
|
135
|
+
user: userValues.get(setting.id),
|
|
136
|
+
project: projectValues.get(setting.id),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
const treeCache = new Map();
|
|
5
|
+
// ─── Path helpers ──────────────────────────────────────────────────────────────
|
|
6
|
+
export function getUserSkillsDir() {
|
|
7
|
+
return path.join(os.homedir(), ".claude", "skills");
|
|
8
|
+
}
|
|
9
|
+
export function getProjectSkillsDir(projectPath) {
|
|
10
|
+
return path.join(projectPath ?? process.cwd(), ".claude", "skills");
|
|
11
|
+
}
|
|
12
|
+
// ─── GitHub Tree API ──────────────────────────────────────────────────────────
|
|
13
|
+
async function fetchGitTree(repo) {
|
|
14
|
+
const cached = treeCache.get(repo);
|
|
15
|
+
const url = `https://api.github.com/repos/${repo}/git/trees/HEAD?recursive=1`;
|
|
16
|
+
const headers = {
|
|
17
|
+
Accept: "application/vnd.github+json",
|
|
18
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
19
|
+
};
|
|
20
|
+
if (cached?.etag) {
|
|
21
|
+
headers["If-None-Match"] = cached.etag;
|
|
22
|
+
}
|
|
23
|
+
const token = process.env.GITHUB_TOKEN || process.env.GITHUB_PERSONAL_ACCESS_TOKEN;
|
|
24
|
+
if (token) {
|
|
25
|
+
headers.Authorization = `Bearer ${token}`;
|
|
26
|
+
}
|
|
27
|
+
const response = await fetch(url, {
|
|
28
|
+
headers,
|
|
29
|
+
signal: AbortSignal.timeout(10000),
|
|
30
|
+
});
|
|
31
|
+
if (response.status === 304 && cached) {
|
|
32
|
+
return cached.tree;
|
|
33
|
+
}
|
|
34
|
+
if (response.status === 403 || response.status === 429) {
|
|
35
|
+
const resetHeader = response.headers.get("X-RateLimit-Reset");
|
|
36
|
+
const resetTime = resetHeader
|
|
37
|
+
? new Date(Number(resetHeader) * 1000).toLocaleTimeString()
|
|
38
|
+
: "unknown";
|
|
39
|
+
throw new Error(`GitHub API rate limit exceeded. Resets at ${resetTime}. Set GITHUB_TOKEN to increase limits.`);
|
|
40
|
+
}
|
|
41
|
+
if (!response.ok) {
|
|
42
|
+
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
|
|
43
|
+
}
|
|
44
|
+
const etag = response.headers.get("ETag") || "";
|
|
45
|
+
const tree = (await response.json());
|
|
46
|
+
treeCache.set(repo, { etag, tree, fetchedAt: Date.now() });
|
|
47
|
+
return tree;
|
|
48
|
+
}
|
|
49
|
+
// ─── Frontmatter parser ───────────────────────────────────────────────────────
|
|
50
|
+
function parseYamlFrontmatter(content) {
|
|
51
|
+
const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
52
|
+
if (!frontmatterMatch)
|
|
53
|
+
return {};
|
|
54
|
+
const yaml = frontmatterMatch[1];
|
|
55
|
+
const result = {};
|
|
56
|
+
for (const line of yaml.split("\n")) {
|
|
57
|
+
const colonIdx = line.indexOf(":");
|
|
58
|
+
if (colonIdx === -1)
|
|
59
|
+
continue;
|
|
60
|
+
const key = line.slice(0, colonIdx).trim();
|
|
61
|
+
const rawValue = line.slice(colonIdx + 1).trim();
|
|
62
|
+
if (!key)
|
|
63
|
+
continue;
|
|
64
|
+
// Handle arrays (simple inline: [a, b, c])
|
|
65
|
+
if (rawValue.startsWith("[") && rawValue.endsWith("]")) {
|
|
66
|
+
const items = rawValue
|
|
67
|
+
.slice(1, -1)
|
|
68
|
+
.split(",")
|
|
69
|
+
.map((s) => s.trim().replace(/^["']|["']$/g, ""))
|
|
70
|
+
.filter(Boolean);
|
|
71
|
+
result[key] = items;
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
// Strip quotes
|
|
75
|
+
result[key] = rawValue.replace(/^["']|["']$/g, "");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
export async function fetchSkillFrontmatter(skill) {
|
|
81
|
+
const url = `https://raw.githubusercontent.com/${skill.source.repo}/HEAD/${skill.repoPath}`;
|
|
82
|
+
const response = await fetch(url, {
|
|
83
|
+
signal: AbortSignal.timeout(10000),
|
|
84
|
+
});
|
|
85
|
+
if (!response.ok) {
|
|
86
|
+
return {
|
|
87
|
+
name: skill.name,
|
|
88
|
+
description: "(no description)",
|
|
89
|
+
category: "general",
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const content = await response.text();
|
|
93
|
+
const parsed = parseYamlFrontmatter(content);
|
|
94
|
+
return {
|
|
95
|
+
name: parsed.name || skill.name,
|
|
96
|
+
description: parsed.description || "(no description)",
|
|
97
|
+
category: parsed.category,
|
|
98
|
+
author: parsed.author,
|
|
99
|
+
version: parsed.version,
|
|
100
|
+
tags: parsed.tags,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
// ─── Check installation ───────────────────────────────────────────────────────
|
|
104
|
+
export async function getInstalledSkillNames(scope, projectPath) {
|
|
105
|
+
const dir = scope === "user"
|
|
106
|
+
? getUserSkillsDir()
|
|
107
|
+
: getProjectSkillsDir(projectPath);
|
|
108
|
+
const installed = new Set();
|
|
109
|
+
try {
|
|
110
|
+
if (!(await fs.pathExists(dir)))
|
|
111
|
+
return installed;
|
|
112
|
+
const entries = await fs.readdir(dir);
|
|
113
|
+
for (const entry of entries) {
|
|
114
|
+
const skillMd = path.join(dir, entry, "SKILL.md");
|
|
115
|
+
if (await fs.pathExists(skillMd)) {
|
|
116
|
+
installed.add(entry);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// ignore
|
|
122
|
+
}
|
|
123
|
+
return installed;
|
|
124
|
+
}
|
|
125
|
+
// ─── Fetch available skills ───────────────────────────────────────────────────
|
|
126
|
+
export async function fetchAvailableSkills(repos, projectPath) {
|
|
127
|
+
const userInstalled = await getInstalledSkillNames("user");
|
|
128
|
+
const projectInstalled = await getInstalledSkillNames("project", projectPath);
|
|
129
|
+
const skills = [];
|
|
130
|
+
for (const source of repos) {
|
|
131
|
+
let tree;
|
|
132
|
+
try {
|
|
133
|
+
tree = await fetchGitTree(source.repo);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// Skip this repo on error
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
// Filter for SKILL.md files under skillsPath
|
|
140
|
+
const prefix = source.skillsPath ? `${source.skillsPath}/` : "";
|
|
141
|
+
for (const item of tree.tree) {
|
|
142
|
+
if (item.type !== "blob")
|
|
143
|
+
continue;
|
|
144
|
+
if (!item.path.endsWith("/SKILL.md"))
|
|
145
|
+
continue;
|
|
146
|
+
if (prefix && !item.path.startsWith(prefix))
|
|
147
|
+
continue;
|
|
148
|
+
// Extract skill name: second-to-last segment of path
|
|
149
|
+
const parts = item.path.split("/");
|
|
150
|
+
if (parts.length < 2)
|
|
151
|
+
continue;
|
|
152
|
+
const skillName = parts[parts.length - 2];
|
|
153
|
+
// Validate name (prevent traversal)
|
|
154
|
+
if (!/^[a-z0-9][a-z0-9-_]*$/i.test(skillName))
|
|
155
|
+
continue;
|
|
156
|
+
const isUserInstalled = userInstalled.has(skillName);
|
|
157
|
+
const isProjInstalled = projectInstalled.has(skillName);
|
|
158
|
+
const installed = isUserInstalled || isProjInstalled;
|
|
159
|
+
const installedScope = isProjInstalled
|
|
160
|
+
? "project"
|
|
161
|
+
: isUserInstalled
|
|
162
|
+
? "user"
|
|
163
|
+
: null;
|
|
164
|
+
skills.push({
|
|
165
|
+
id: `${source.repo}/${item.path}`,
|
|
166
|
+
name: skillName,
|
|
167
|
+
source,
|
|
168
|
+
repoPath: item.path,
|
|
169
|
+
gitBlobSha: item.sha,
|
|
170
|
+
frontmatter: null,
|
|
171
|
+
installed,
|
|
172
|
+
installedScope,
|
|
173
|
+
hasUpdate: false,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// Sort by name within each repo
|
|
178
|
+
skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
179
|
+
return skills;
|
|
180
|
+
}
|
|
181
|
+
// ─── Install / Uninstall ──────────────────────────────────────────────────────
|
|
182
|
+
export async function installSkill(skill, scope, projectPath) {
|
|
183
|
+
const url = `https://raw.githubusercontent.com/${skill.source.repo}/HEAD/${skill.repoPath}`;
|
|
184
|
+
const response = await fetch(url, {
|
|
185
|
+
signal: AbortSignal.timeout(15000),
|
|
186
|
+
});
|
|
187
|
+
if (!response.ok) {
|
|
188
|
+
throw new Error(`Failed to fetch skill: ${response.status} ${response.statusText}`);
|
|
189
|
+
}
|
|
190
|
+
const content = await response.text();
|
|
191
|
+
const installDir = scope === "user"
|
|
192
|
+
? path.join(getUserSkillsDir(), skill.name)
|
|
193
|
+
: path.join(getProjectSkillsDir(projectPath), skill.name);
|
|
194
|
+
await fs.ensureDir(installDir);
|
|
195
|
+
await fs.writeFile(path.join(installDir, "SKILL.md"), content, "utf8");
|
|
196
|
+
}
|
|
197
|
+
export async function uninstallSkill(skillName, scope, projectPath) {
|
|
198
|
+
const installDir = scope === "user"
|
|
199
|
+
? path.join(getUserSkillsDir(), skillName)
|
|
200
|
+
: path.join(getProjectSkillsDir(projectPath), skillName);
|
|
201
|
+
const skillMdPath = path.join(installDir, "SKILL.md");
|
|
202
|
+
if (await fs.pathExists(skillMdPath)) {
|
|
203
|
+
await fs.remove(skillMdPath);
|
|
204
|
+
}
|
|
205
|
+
// Try to remove directory if empty
|
|
206
|
+
try {
|
|
207
|
+
await fs.rmdir(installDir);
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
// Ignore if not empty or doesn't exist
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// ─── Check installed skills from file system ──────────────────────────────────
|
|
214
|
+
export async function getInstalledSkillsFromFs(projectPath) {
|
|
215
|
+
const result = [];
|
|
216
|
+
const userDir = getUserSkillsDir();
|
|
217
|
+
const projDir = getProjectSkillsDir(projectPath);
|
|
218
|
+
const scopedDirs = [
|
|
219
|
+
[userDir, "user"],
|
|
220
|
+
[projDir, "project"],
|
|
221
|
+
];
|
|
222
|
+
for (const [dir, scope] of scopedDirs) {
|
|
223
|
+
try {
|
|
224
|
+
if (!(await fs.pathExists(dir)))
|
|
225
|
+
continue;
|
|
226
|
+
const entries = await fs.readdir(dir);
|
|
227
|
+
for (const entry of entries) {
|
|
228
|
+
const skillMd = path.join(dir, entry, "SKILL.md");
|
|
229
|
+
if (await fs.pathExists(skillMd)) {
|
|
230
|
+
result.push({ name: entry, scope });
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
// ignore
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return result;
|
|
239
|
+
}
|