claudeup 3.7.2 → 3.8.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/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/types/index.ts +34 -0
- package/src/ui/App.js +17 -18
- package/src/ui/App.tsx +21 -23
- package/src/ui/components/TabBar.js +8 -8
- package/src/ui/components/TabBar.tsx +14 -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/StatusLineScreen.js +2 -2
- package/src/ui/screens/StatusLineScreen.tsx +10 -12
- package/src/ui/screens/index.js +2 -2
- package/src/ui/screens/index.ts +2 -2
- package/src/ui/state/AppContext.js +2 -1
- package/src/ui/state/AppContext.tsx +2 -0
- package/src/ui/state/reducer.js +63 -19
- package/src/ui/state/reducer.ts +68 -19
- package/src/ui/state/types.ts +33 -14
- package/src/utils/clipboard.js +56 -0
- package/src/utils/clipboard.ts +58 -0
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|