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,86 @@
|
|
|
1
|
+
export const RECOMMENDED_SKILLS = [
|
|
2
|
+
{
|
|
3
|
+
name: "Find Skills",
|
|
4
|
+
repo: "vercel-labs/skills",
|
|
5
|
+
skillPath: "find-skills",
|
|
6
|
+
description: "Discover and install new skills from the ecosystem",
|
|
7
|
+
category: "search",
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
name: "React Best Practices",
|
|
11
|
+
repo: "vercel-labs/agent-skills",
|
|
12
|
+
skillPath: "vercel-react-best-practices",
|
|
13
|
+
description: "Modern React patterns and Vercel deployment guidelines",
|
|
14
|
+
category: "frontend",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: "Web Design Guidelines",
|
|
18
|
+
repo: "vercel-labs/agent-skills",
|
|
19
|
+
skillPath: "web-design-guidelines",
|
|
20
|
+
description: "UI/UX design principles and web standards",
|
|
21
|
+
category: "design",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: "Remotion Best Practices",
|
|
25
|
+
repo: "remotion-dev/skills",
|
|
26
|
+
skillPath: "remotion-best-practices",
|
|
27
|
+
description: "Programmatic video creation with Remotion",
|
|
28
|
+
category: "media",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: "UI/UX Pro Max",
|
|
32
|
+
repo: "nextlevelbuilder/ui-ux-pro-max-skill",
|
|
33
|
+
skillPath: "ui-ux-pro-max",
|
|
34
|
+
description: "Advanced UI/UX design and implementation patterns",
|
|
35
|
+
category: "design",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: "ElevenLabs TTS",
|
|
39
|
+
repo: "inferen-sh/skills",
|
|
40
|
+
skillPath: "elevenlabs-tts",
|
|
41
|
+
description: "Text-to-speech with ElevenLabs API integration",
|
|
42
|
+
category: "media",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "Audit Website",
|
|
46
|
+
repo: "squirrelscan/skills",
|
|
47
|
+
skillPath: "audit-website",
|
|
48
|
+
description: "Security and quality auditing for web applications",
|
|
49
|
+
category: "security",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: "Systematic Debugging",
|
|
53
|
+
repo: "obra/superpowers",
|
|
54
|
+
skillPath: "systematic-debugging",
|
|
55
|
+
description: "Structured debugging methodology with root cause analysis",
|
|
56
|
+
category: "debugging",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "shadcn/ui",
|
|
60
|
+
repo: "shadcn/ui",
|
|
61
|
+
skillPath: "shadcn",
|
|
62
|
+
description: "shadcn/ui component library patterns and usage",
|
|
63
|
+
category: "frontend",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: "Neon Postgres",
|
|
67
|
+
repo: "neondatabase/agent-skills",
|
|
68
|
+
skillPath: "neon-postgres",
|
|
69
|
+
description: "Neon serverless Postgres setup and best practices",
|
|
70
|
+
category: "database",
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: "Neon Serverless",
|
|
74
|
+
repo: "neondatabase/ai-rules",
|
|
75
|
+
skillPath: "neon-serverless",
|
|
76
|
+
description: "Serverless database patterns with Neon",
|
|
77
|
+
category: "database",
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
export const DEFAULT_SKILL_REPOS = [
|
|
81
|
+
{
|
|
82
|
+
label: "vercel-labs/agent-skills",
|
|
83
|
+
repo: "vercel-labs/agent-skills",
|
|
84
|
+
skillsPath: "skills",
|
|
85
|
+
},
|
|
86
|
+
];
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { SkillSource } from "../types/index.js";
|
|
2
|
+
|
|
3
|
+
export interface RecommendedSkill {
|
|
4
|
+
name: string;
|
|
5
|
+
repo: string;
|
|
6
|
+
skillPath: string;
|
|
7
|
+
description: string;
|
|
8
|
+
category: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const RECOMMENDED_SKILLS: RecommendedSkill[] = [
|
|
12
|
+
{
|
|
13
|
+
name: "Find Skills",
|
|
14
|
+
repo: "vercel-labs/skills",
|
|
15
|
+
skillPath: "find-skills",
|
|
16
|
+
description: "Discover and install new skills from the ecosystem",
|
|
17
|
+
category: "search",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: "React Best Practices",
|
|
21
|
+
repo: "vercel-labs/agent-skills",
|
|
22
|
+
skillPath: "vercel-react-best-practices",
|
|
23
|
+
description: "Modern React patterns and Vercel deployment guidelines",
|
|
24
|
+
category: "frontend",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: "Web Design Guidelines",
|
|
28
|
+
repo: "vercel-labs/agent-skills",
|
|
29
|
+
skillPath: "web-design-guidelines",
|
|
30
|
+
description: "UI/UX design principles and web standards",
|
|
31
|
+
category: "design",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: "Remotion Best Practices",
|
|
35
|
+
repo: "remotion-dev/skills",
|
|
36
|
+
skillPath: "remotion-best-practices",
|
|
37
|
+
description: "Programmatic video creation with Remotion",
|
|
38
|
+
category: "media",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "UI/UX Pro Max",
|
|
42
|
+
repo: "nextlevelbuilder/ui-ux-pro-max-skill",
|
|
43
|
+
skillPath: "ui-ux-pro-max",
|
|
44
|
+
description: "Advanced UI/UX design and implementation patterns",
|
|
45
|
+
category: "design",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: "ElevenLabs TTS",
|
|
49
|
+
repo: "inferen-sh/skills",
|
|
50
|
+
skillPath: "elevenlabs-tts",
|
|
51
|
+
description: "Text-to-speech with ElevenLabs API integration",
|
|
52
|
+
category: "media",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "Audit Website",
|
|
56
|
+
repo: "squirrelscan/skills",
|
|
57
|
+
skillPath: "audit-website",
|
|
58
|
+
description: "Security and quality auditing for web applications",
|
|
59
|
+
category: "security",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: "Systematic Debugging",
|
|
63
|
+
repo: "obra/superpowers",
|
|
64
|
+
skillPath: "systematic-debugging",
|
|
65
|
+
description: "Structured debugging methodology with root cause analysis",
|
|
66
|
+
category: "debugging",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: "shadcn/ui",
|
|
70
|
+
repo: "shadcn/ui",
|
|
71
|
+
skillPath: "shadcn",
|
|
72
|
+
description: "shadcn/ui component library patterns and usage",
|
|
73
|
+
category: "frontend",
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: "Neon Postgres",
|
|
77
|
+
repo: "neondatabase/agent-skills",
|
|
78
|
+
skillPath: "neon-postgres",
|
|
79
|
+
description: "Neon serverless Postgres setup and best practices",
|
|
80
|
+
category: "database",
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: "Neon Serverless",
|
|
84
|
+
repo: "neondatabase/ai-rules",
|
|
85
|
+
skillPath: "neon-serverless",
|
|
86
|
+
description: "Serverless database patterns with Neon",
|
|
87
|
+
category: "database",
|
|
88
|
+
},
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
export const DEFAULT_SKILL_REPOS: SkillSource[] = [
|
|
92
|
+
{
|
|
93
|
+
label: "vercel-labs/agent-skills",
|
|
94
|
+
repo: "vercel-labs/agent-skills",
|
|
95
|
+
skillsPath: "skills",
|
|
96
|
+
},
|
|
97
|
+
];
|
|
@@ -207,6 +207,7 @@ export async function getAvailablePlugins(projectPath) {
|
|
|
207
207
|
enabled: isEnabled,
|
|
208
208
|
installedVersion: installedVersion,
|
|
209
209
|
hasUpdate,
|
|
210
|
+
isOrphaned: true,
|
|
210
211
|
...scopeStatus,
|
|
211
212
|
});
|
|
212
213
|
}
|
|
@@ -359,6 +360,7 @@ export async function getGlobalAvailablePlugins() {
|
|
|
359
360
|
...scopeStatus,
|
|
360
361
|
installedVersion: installedVersion,
|
|
361
362
|
hasUpdate,
|
|
363
|
+
isOrphaned: true,
|
|
362
364
|
});
|
|
363
365
|
}
|
|
364
366
|
return plugins;
|
|
@@ -61,6 +61,7 @@ export interface PluginInfo {
|
|
|
61
61
|
skills?: string[];
|
|
62
62
|
mcpServers?: string[];
|
|
63
63
|
lspServers?: Record<string, unknown>;
|
|
64
|
+
isOrphaned?: boolean;
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
export interface MarketplacePlugin {
|
|
@@ -323,6 +324,7 @@ export async function getAvailablePlugins(
|
|
|
323
324
|
enabled: isEnabled,
|
|
324
325
|
installedVersion: installedVersion,
|
|
325
326
|
hasUpdate,
|
|
327
|
+
isOrphaned: true,
|
|
326
328
|
...scopeStatus,
|
|
327
329
|
});
|
|
328
330
|
}
|
|
@@ -500,6 +502,7 @@ export async function getGlobalAvailablePlugins(): Promise<PluginInfo[]> {
|
|
|
500
502
|
...scopeStatus,
|
|
501
503
|
installedVersion: installedVersion,
|
|
502
504
|
hasUpdate,
|
|
505
|
+
isOrphaned: true,
|
|
503
506
|
});
|
|
504
507
|
}
|
|
505
508
|
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
const PROFILES_FILE = "profiles.json";
|
|
5
|
+
// ─── Path helpers ──────────────────────────────────────────────────────────────
|
|
6
|
+
export function getUserProfilesPath() {
|
|
7
|
+
return path.join(os.homedir(), ".claude", PROFILES_FILE);
|
|
8
|
+
}
|
|
9
|
+
export function getProjectProfilesPath(projectPath) {
|
|
10
|
+
const base = projectPath ?? process.cwd();
|
|
11
|
+
return path.join(base, ".claude", PROFILES_FILE);
|
|
12
|
+
}
|
|
13
|
+
// ─── Low-level read/write ──────────────────────────────────────────────────────
|
|
14
|
+
export async function readProfiles(scope, projectPath) {
|
|
15
|
+
const filePath = scope === "user"
|
|
16
|
+
? getUserProfilesPath()
|
|
17
|
+
: getProjectProfilesPath(projectPath);
|
|
18
|
+
try {
|
|
19
|
+
if (await fs.pathExists(filePath)) {
|
|
20
|
+
const data = await fs.readJson(filePath);
|
|
21
|
+
if (data.version && data.profiles)
|
|
22
|
+
return data;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// fall through to default
|
|
27
|
+
}
|
|
28
|
+
return { version: 1, profiles: {} };
|
|
29
|
+
}
|
|
30
|
+
async function writeProfiles(scope, data, projectPath) {
|
|
31
|
+
const filePath = scope === "user"
|
|
32
|
+
? getUserProfilesPath()
|
|
33
|
+
: getProjectProfilesPath(projectPath);
|
|
34
|
+
await fs.ensureDir(path.dirname(filePath));
|
|
35
|
+
await fs.writeJson(filePath, data, { spaces: 2 });
|
|
36
|
+
}
|
|
37
|
+
// ─── CRUD ──────────────────────────────────────────────────────────────────────
|
|
38
|
+
/** Return all profiles from both scopes, merged into a flat list */
|
|
39
|
+
export async function listProfiles(projectPath) {
|
|
40
|
+
const [user, project] = await Promise.all([
|
|
41
|
+
readProfiles("user"),
|
|
42
|
+
readProfiles("project", projectPath),
|
|
43
|
+
]);
|
|
44
|
+
const entries = [];
|
|
45
|
+
for (const [id, p] of Object.entries(user.profiles)) {
|
|
46
|
+
entries.push({ id, scope: "user", ...p });
|
|
47
|
+
}
|
|
48
|
+
for (const [id, p] of Object.entries(project.profiles)) {
|
|
49
|
+
entries.push({ id, scope: "project", ...p });
|
|
50
|
+
}
|
|
51
|
+
// Sort: user profiles first, then project, each by updatedAt desc
|
|
52
|
+
entries.sort((a, b) => {
|
|
53
|
+
if (a.scope !== b.scope)
|
|
54
|
+
return a.scope === "user" ? -1 : 1;
|
|
55
|
+
return b.updatedAt.localeCompare(a.updatedAt);
|
|
56
|
+
});
|
|
57
|
+
return entries;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Save (create or overwrite) a profile.
|
|
61
|
+
* Returns the generated profile ID.
|
|
62
|
+
*/
|
|
63
|
+
export async function saveProfile(name, plugins, scope, projectPath) {
|
|
64
|
+
const data = await readProfiles(scope, projectPath);
|
|
65
|
+
const id = generateUniqueId(name, data.profiles);
|
|
66
|
+
const now = new Date().toISOString();
|
|
67
|
+
data.profiles[id] = {
|
|
68
|
+
name,
|
|
69
|
+
plugins,
|
|
70
|
+
createdAt: now,
|
|
71
|
+
updatedAt: now,
|
|
72
|
+
};
|
|
73
|
+
await writeProfiles(scope, data, projectPath);
|
|
74
|
+
return id;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Apply a profile: replaces enabledPlugins in the target settings scope.
|
|
78
|
+
* targetScope is where the settings will be written, independent of where
|
|
79
|
+
* the profile is stored.
|
|
80
|
+
*/
|
|
81
|
+
export async function applyProfile(profileId, profileScope, targetScope, projectPath) {
|
|
82
|
+
const data = await readProfiles(profileScope, projectPath);
|
|
83
|
+
const profile = data.profiles[profileId];
|
|
84
|
+
if (!profile)
|
|
85
|
+
throw new Error(`Profile "${profileId}" not found`);
|
|
86
|
+
// Import settings functions dynamically to avoid circular deps
|
|
87
|
+
const { readSettings, writeSettings, readGlobalSettings, writeGlobalSettings, } = await import("./claude-settings.js");
|
|
88
|
+
if (targetScope === "user") {
|
|
89
|
+
const settings = await readGlobalSettings();
|
|
90
|
+
settings.enabledPlugins = { ...profile.plugins };
|
|
91
|
+
await writeGlobalSettings(settings);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
// project or local both write to project settings.json
|
|
95
|
+
const settings = await readSettings(projectPath);
|
|
96
|
+
settings.enabledPlugins = { ...profile.plugins };
|
|
97
|
+
await writeSettings(settings, projectPath);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** Rename a profile in-place (preserves id, createdAt) */
|
|
101
|
+
export async function renameProfile(profileId, newName, scope, projectPath) {
|
|
102
|
+
const data = await readProfiles(scope, projectPath);
|
|
103
|
+
if (!data.profiles[profileId]) {
|
|
104
|
+
throw new Error(`Profile "${profileId}" not found in ${scope} scope`);
|
|
105
|
+
}
|
|
106
|
+
data.profiles[profileId].name = newName;
|
|
107
|
+
data.profiles[profileId].updatedAt = new Date().toISOString();
|
|
108
|
+
await writeProfiles(scope, data, projectPath);
|
|
109
|
+
}
|
|
110
|
+
export async function deleteProfile(profileId, scope, projectPath) {
|
|
111
|
+
const data = await readProfiles(scope, projectPath);
|
|
112
|
+
if (!data.profiles[profileId]) {
|
|
113
|
+
throw new Error(`Profile "${profileId}" not found in ${scope} scope`);
|
|
114
|
+
}
|
|
115
|
+
delete data.profiles[profileId];
|
|
116
|
+
await writeProfiles(scope, data, projectPath);
|
|
117
|
+
}
|
|
118
|
+
// ─── Import / Export ───────────────────────────────────────────────────────────
|
|
119
|
+
/** Serialize a profile to a compact JSON string for clipboard sharing */
|
|
120
|
+
export async function exportProfileToJson(profileId, scope, projectPath) {
|
|
121
|
+
const data = await readProfiles(scope, projectPath);
|
|
122
|
+
const profile = data.profiles[profileId];
|
|
123
|
+
if (!profile)
|
|
124
|
+
throw new Error(`Profile "${profileId}" not found`);
|
|
125
|
+
const entry = { id: profileId, scope, ...profile };
|
|
126
|
+
return JSON.stringify(entry, null, 2);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Parse a clipboard JSON string and save into the specified scope.
|
|
130
|
+
* Returns the saved profile id.
|
|
131
|
+
*/
|
|
132
|
+
export async function importProfileFromJson(json, targetScope, projectPath) {
|
|
133
|
+
let entry;
|
|
134
|
+
try {
|
|
135
|
+
entry = JSON.parse(json);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
throw new Error("Invalid JSON — could not parse clipboard content");
|
|
139
|
+
}
|
|
140
|
+
if (!entry.name || typeof entry.plugins !== "object") {
|
|
141
|
+
throw new Error("Invalid profile format — expected { name, plugins }");
|
|
142
|
+
}
|
|
143
|
+
return saveProfile(entry.name, entry.plugins, targetScope, projectPath);
|
|
144
|
+
}
|
|
145
|
+
// ─── Internal helpers ──────────────────────────────────────────────────────────
|
|
146
|
+
function slugify(name) {
|
|
147
|
+
return name
|
|
148
|
+
.toLowerCase()
|
|
149
|
+
.trim()
|
|
150
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
151
|
+
.replace(/^-|-$/g, "");
|
|
152
|
+
}
|
|
153
|
+
function generateUniqueId(name, existing) {
|
|
154
|
+
const base = slugify(name) || "profile";
|
|
155
|
+
if (!existing[base])
|
|
156
|
+
return base;
|
|
157
|
+
let n = 2;
|
|
158
|
+
while (existing[`${base}-${n}`])
|
|
159
|
+
n++;
|
|
160
|
+
return `${base}-${n}`;
|
|
161
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import type { Profile, ProfilesFile, ProfileEntry } from "../types/index.js";
|
|
5
|
+
|
|
6
|
+
const PROFILES_FILE = "profiles.json";
|
|
7
|
+
|
|
8
|
+
// ─── Path helpers ──────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
export function getUserProfilesPath(): string {
|
|
11
|
+
return path.join(os.homedir(), ".claude", PROFILES_FILE);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getProjectProfilesPath(projectPath?: string): string {
|
|
15
|
+
const base = projectPath ?? process.cwd();
|
|
16
|
+
return path.join(base, ".claude", PROFILES_FILE);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ─── Low-level read/write ──────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
export async function readProfiles(
|
|
22
|
+
scope: "user" | "project",
|
|
23
|
+
projectPath?: string,
|
|
24
|
+
): Promise<ProfilesFile> {
|
|
25
|
+
const filePath =
|
|
26
|
+
scope === "user"
|
|
27
|
+
? getUserProfilesPath()
|
|
28
|
+
: getProjectProfilesPath(projectPath);
|
|
29
|
+
try {
|
|
30
|
+
if (await fs.pathExists(filePath)) {
|
|
31
|
+
const data = await fs.readJson(filePath);
|
|
32
|
+
if (data.version && data.profiles) return data as ProfilesFile;
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
// fall through to default
|
|
36
|
+
}
|
|
37
|
+
return { version: 1, profiles: {} };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function writeProfiles(
|
|
41
|
+
scope: "user" | "project",
|
|
42
|
+
data: ProfilesFile,
|
|
43
|
+
projectPath?: string,
|
|
44
|
+
): Promise<void> {
|
|
45
|
+
const filePath =
|
|
46
|
+
scope === "user"
|
|
47
|
+
? getUserProfilesPath()
|
|
48
|
+
: getProjectProfilesPath(projectPath);
|
|
49
|
+
await fs.ensureDir(path.dirname(filePath));
|
|
50
|
+
await fs.writeJson(filePath, data, { spaces: 2 });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ─── CRUD ──────────────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
/** Return all profiles from both scopes, merged into a flat list */
|
|
56
|
+
export async function listProfiles(
|
|
57
|
+
projectPath?: string,
|
|
58
|
+
): Promise<ProfileEntry[]> {
|
|
59
|
+
const [user, project] = await Promise.all([
|
|
60
|
+
readProfiles("user"),
|
|
61
|
+
readProfiles("project", projectPath),
|
|
62
|
+
]);
|
|
63
|
+
const entries: ProfileEntry[] = [];
|
|
64
|
+
for (const [id, p] of Object.entries(user.profiles)) {
|
|
65
|
+
entries.push({ id, scope: "user", ...p });
|
|
66
|
+
}
|
|
67
|
+
for (const [id, p] of Object.entries(project.profiles)) {
|
|
68
|
+
entries.push({ id, scope: "project", ...p });
|
|
69
|
+
}
|
|
70
|
+
// Sort: user profiles first, then project, each by updatedAt desc
|
|
71
|
+
entries.sort((a, b) => {
|
|
72
|
+
if (a.scope !== b.scope) return a.scope === "user" ? -1 : 1;
|
|
73
|
+
return b.updatedAt.localeCompare(a.updatedAt);
|
|
74
|
+
});
|
|
75
|
+
return entries;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Save (create or overwrite) a profile.
|
|
80
|
+
* Returns the generated profile ID.
|
|
81
|
+
*/
|
|
82
|
+
export async function saveProfile(
|
|
83
|
+
name: string,
|
|
84
|
+
plugins: Record<string, boolean>,
|
|
85
|
+
scope: "user" | "project",
|
|
86
|
+
projectPath?: string,
|
|
87
|
+
): Promise<string> {
|
|
88
|
+
const data = await readProfiles(scope, projectPath);
|
|
89
|
+
const id = generateUniqueId(name, data.profiles);
|
|
90
|
+
const now = new Date().toISOString();
|
|
91
|
+
data.profiles[id] = {
|
|
92
|
+
name,
|
|
93
|
+
plugins,
|
|
94
|
+
createdAt: now,
|
|
95
|
+
updatedAt: now,
|
|
96
|
+
};
|
|
97
|
+
await writeProfiles(scope, data, projectPath);
|
|
98
|
+
return id;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Apply a profile: replaces enabledPlugins in the target settings scope.
|
|
103
|
+
* targetScope is where the settings will be written, independent of where
|
|
104
|
+
* the profile is stored.
|
|
105
|
+
*/
|
|
106
|
+
export async function applyProfile(
|
|
107
|
+
profileId: string,
|
|
108
|
+
profileScope: "user" | "project",
|
|
109
|
+
targetScope: "user" | "project" | "local",
|
|
110
|
+
projectPath?: string,
|
|
111
|
+
): Promise<void> {
|
|
112
|
+
const data = await readProfiles(profileScope, projectPath);
|
|
113
|
+
const profile = data.profiles[profileId];
|
|
114
|
+
if (!profile) throw new Error(`Profile "${profileId}" not found`);
|
|
115
|
+
|
|
116
|
+
// Import settings functions dynamically to avoid circular deps
|
|
117
|
+
const {
|
|
118
|
+
readSettings,
|
|
119
|
+
writeSettings,
|
|
120
|
+
readGlobalSettings,
|
|
121
|
+
writeGlobalSettings,
|
|
122
|
+
} = await import("./claude-settings.js");
|
|
123
|
+
|
|
124
|
+
if (targetScope === "user") {
|
|
125
|
+
const settings = await readGlobalSettings();
|
|
126
|
+
settings.enabledPlugins = { ...profile.plugins };
|
|
127
|
+
await writeGlobalSettings(settings);
|
|
128
|
+
} else {
|
|
129
|
+
// project or local both write to project settings.json
|
|
130
|
+
const settings = await readSettings(projectPath);
|
|
131
|
+
settings.enabledPlugins = { ...profile.plugins };
|
|
132
|
+
await writeSettings(settings, projectPath);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Rename a profile in-place (preserves id, createdAt) */
|
|
137
|
+
export async function renameProfile(
|
|
138
|
+
profileId: string,
|
|
139
|
+
newName: string,
|
|
140
|
+
scope: "user" | "project",
|
|
141
|
+
projectPath?: string,
|
|
142
|
+
): Promise<void> {
|
|
143
|
+
const data = await readProfiles(scope, projectPath);
|
|
144
|
+
if (!data.profiles[profileId]) {
|
|
145
|
+
throw new Error(`Profile "${profileId}" not found in ${scope} scope`);
|
|
146
|
+
}
|
|
147
|
+
data.profiles[profileId].name = newName;
|
|
148
|
+
data.profiles[profileId].updatedAt = new Date().toISOString();
|
|
149
|
+
await writeProfiles(scope, data, projectPath);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function deleteProfile(
|
|
153
|
+
profileId: string,
|
|
154
|
+
scope: "user" | "project",
|
|
155
|
+
projectPath?: string,
|
|
156
|
+
): Promise<void> {
|
|
157
|
+
const data = await readProfiles(scope, projectPath);
|
|
158
|
+
if (!data.profiles[profileId]) {
|
|
159
|
+
throw new Error(`Profile "${profileId}" not found in ${scope} scope`);
|
|
160
|
+
}
|
|
161
|
+
delete data.profiles[profileId];
|
|
162
|
+
await writeProfiles(scope, data, projectPath);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ─── Import / Export ───────────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
/** Serialize a profile to a compact JSON string for clipboard sharing */
|
|
168
|
+
export async function exportProfileToJson(
|
|
169
|
+
profileId: string,
|
|
170
|
+
scope: "user" | "project",
|
|
171
|
+
projectPath?: string,
|
|
172
|
+
): Promise<string> {
|
|
173
|
+
const data = await readProfiles(scope, projectPath);
|
|
174
|
+
const profile = data.profiles[profileId];
|
|
175
|
+
if (!profile) throw new Error(`Profile "${profileId}" not found`);
|
|
176
|
+
const entry: ProfileEntry = { id: profileId, scope, ...profile };
|
|
177
|
+
return JSON.stringify(entry, null, 2);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Parse a clipboard JSON string and save into the specified scope.
|
|
182
|
+
* Returns the saved profile id.
|
|
183
|
+
*/
|
|
184
|
+
export async function importProfileFromJson(
|
|
185
|
+
json: string,
|
|
186
|
+
targetScope: "user" | "project",
|
|
187
|
+
projectPath?: string,
|
|
188
|
+
): Promise<string> {
|
|
189
|
+
let entry: Partial<ProfileEntry>;
|
|
190
|
+
try {
|
|
191
|
+
entry = JSON.parse(json) as Partial<ProfileEntry>;
|
|
192
|
+
} catch {
|
|
193
|
+
throw new Error("Invalid JSON — could not parse clipboard content");
|
|
194
|
+
}
|
|
195
|
+
if (!entry.name || typeof entry.plugins !== "object") {
|
|
196
|
+
throw new Error("Invalid profile format — expected { name, plugins }");
|
|
197
|
+
}
|
|
198
|
+
return saveProfile(
|
|
199
|
+
entry.name,
|
|
200
|
+
entry.plugins as Record<string, boolean>,
|
|
201
|
+
targetScope,
|
|
202
|
+
projectPath,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ─── Internal helpers ──────────────────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
function slugify(name: string): string {
|
|
209
|
+
return name
|
|
210
|
+
.toLowerCase()
|
|
211
|
+
.trim()
|
|
212
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
213
|
+
.replace(/^-|-$/g, "");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function generateUniqueId(
|
|
217
|
+
name: string,
|
|
218
|
+
existing: Record<string, unknown>,
|
|
219
|
+
): string {
|
|
220
|
+
const base = slugify(name) || "profile";
|
|
221
|
+
if (!existing[base]) return base;
|
|
222
|
+
let n = 2;
|
|
223
|
+
while (existing[`${base}-${n}`]) n++;
|
|
224
|
+
return `${base}-${n}`;
|
|
225
|
+
}
|