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.
Files changed (44) hide show
  1. package/package.json +1 -1
  2. package/src/data/settings-catalog.js +612 -0
  3. package/src/data/settings-catalog.ts +689 -0
  4. package/src/services/plugin-manager.js +2 -0
  5. package/src/services/plugin-manager.ts +3 -0
  6. package/src/services/profiles.js +161 -0
  7. package/src/services/profiles.ts +225 -0
  8. package/src/services/settings-manager.js +108 -0
  9. package/src/services/settings-manager.ts +140 -0
  10. package/src/types/index.ts +34 -0
  11. package/src/ui/App.js +17 -18
  12. package/src/ui/App.tsx +21 -23
  13. package/src/ui/components/TabBar.js +8 -8
  14. package/src/ui/components/TabBar.tsx +14 -19
  15. package/src/ui/components/layout/ScreenLayout.js +8 -14
  16. package/src/ui/components/layout/ScreenLayout.tsx +51 -58
  17. package/src/ui/components/modals/ModalContainer.js +43 -11
  18. package/src/ui/components/modals/ModalContainer.tsx +44 -12
  19. package/src/ui/components/modals/SelectModal.js +4 -18
  20. package/src/ui/components/modals/SelectModal.tsx +10 -21
  21. package/src/ui/screens/CliToolsScreen.js +2 -2
  22. package/src/ui/screens/CliToolsScreen.tsx +8 -8
  23. package/src/ui/screens/EnvVarsScreen.js +248 -116
  24. package/src/ui/screens/EnvVarsScreen.tsx +419 -184
  25. package/src/ui/screens/McpRegistryScreen.tsx +18 -6
  26. package/src/ui/screens/McpScreen.js +1 -1
  27. package/src/ui/screens/McpScreen.tsx +15 -5
  28. package/src/ui/screens/ModelSelectorScreen.js +3 -5
  29. package/src/ui/screens/ModelSelectorScreen.tsx +12 -16
  30. package/src/ui/screens/PluginsScreen.js +154 -66
  31. package/src/ui/screens/PluginsScreen.tsx +280 -97
  32. package/src/ui/screens/ProfilesScreen.js +255 -0
  33. package/src/ui/screens/ProfilesScreen.tsx +487 -0
  34. package/src/ui/screens/StatusLineScreen.js +2 -2
  35. package/src/ui/screens/StatusLineScreen.tsx +10 -12
  36. package/src/ui/screens/index.js +2 -2
  37. package/src/ui/screens/index.ts +2 -2
  38. package/src/ui/state/AppContext.js +2 -1
  39. package/src/ui/state/AppContext.tsx +2 -0
  40. package/src/ui/state/reducer.js +63 -19
  41. package/src/ui/state/reducer.ts +68 -19
  42. package/src/ui/state/types.ts +33 -14
  43. package/src/utils/clipboard.js +56 -0
  44. 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
+ }