claudeup 4.19.0 → 4.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/bin/claudeup.js +43 -19
  2. package/package.json +14 -19
  3. package/scripts/build-binaries.ts +77 -0
  4. package/scripts/check-optional-deps.ts +49 -0
  5. package/scripts/test-isolated.ts +43 -0
  6. package/src/__tests__/cli-router.test.ts +65 -0
  7. package/src/__tests__/conventions-integration.test.ts +745 -0
  8. package/src/__tests__/conventions-manager.test.ts +1172 -0
  9. package/src/__tests__/doctor-bins.test.ts +35 -0
  10. package/src/__tests__/doctor.test.ts +450 -0
  11. package/src/__tests__/dual-write-prevention.test.ts +14 -4
  12. package/src/__tests__/file-locking.test.ts +208 -0
  13. package/src/__tests__/gitignore-prerun.test.ts +1 -0
  14. package/src/__tests__/install-command.test.ts +81 -0
  15. package/src/__tests__/install-plan.test.ts +98 -0
  16. package/src/__tests__/manifest.test.ts +136 -0
  17. package/src/__tests__/marketplace-refresh.test.ts +267 -0
  18. package/src/__tests__/plugin-requires.test.ts +133 -0
  19. package/src/__tests__/plugin-setup.test.ts +7 -0
  20. package/src/__tests__/profile-command.test.ts +116 -0
  21. package/src/__tests__/profile-materializer.test.ts +82 -0
  22. package/src/__tests__/profile-sync.test.ts +136 -0
  23. package/src/__tests__/registry-version-resolution.test.ts +74 -0
  24. package/src/__tests__/resolve-executable.test.ts +42 -0
  25. package/src/__tests__/resolver.test.ts +218 -0
  26. package/src/__tests__/symlink-manager.test.ts +99 -0
  27. package/src/__tests__/toolchain.test.ts +56 -0
  28. package/src/cli/claude.ts +21 -0
  29. package/src/cli/doctor.ts +132 -0
  30. package/src/cli/install.ts +360 -0
  31. package/src/cli/profile.ts +166 -0
  32. package/src/cli/router.ts +107 -0
  33. package/src/cli/update.ts +85 -0
  34. package/src/data/cli-tools.ts +7 -7
  35. package/src/data/gitignore-defaults.ts +4 -0
  36. package/src/data/marketplaces.ts +12 -0
  37. package/src/data/predefined-profiles.ts +3 -4
  38. package/src/main.tsx +11 -154
  39. package/src/prerunner/index.ts +62 -27
  40. package/src/services/claude-cli.ts +19 -22
  41. package/src/services/claude-settings.ts +99 -26
  42. package/src/services/conventions-manager.ts +865 -0
  43. package/src/services/doctor-bins.ts +49 -0
  44. package/src/services/doctor.ts +509 -0
  45. package/src/services/install-plan.ts +107 -0
  46. package/src/services/manifest.ts +166 -0
  47. package/src/services/marketplace-refresh.ts +173 -0
  48. package/src/services/plugin-manager.ts +7 -1
  49. package/src/services/plugin-requires.ts +215 -0
  50. package/src/services/plugin-version-check.ts +9 -4
  51. package/src/services/profile-materializer.ts +61 -0
  52. package/src/services/profile-sync.ts +150 -0
  53. package/src/services/resolver.ts +293 -0
  54. package/src/services/symlink-manager.ts +146 -0
  55. package/src/services/toolchain.ts +97 -0
  56. package/src/services/version-check.ts +10 -11
  57. package/src/types/index.ts +138 -31
  58. package/src/ui/App.tsx +0 -4
  59. package/src/ui/screens/CliToolsScreen.tsx +11 -3
  60. package/src/ui/screens/index.ts +0 -1
  61. package/src/ui/state/reducer.ts +0 -101
  62. package/src/ui/state/types.ts +0 -35
  63. package/src/utils/command-utils.ts +23 -0
  64. package/src/utils/file-locking.ts +144 -0
  65. package/src/data/alias-flags.js +0 -205
  66. package/src/data/cli-tools.js +0 -123
  67. package/src/data/gitignore-defaults.js +0 -24
  68. package/src/data/gitignore-reasons.js +0 -97
  69. package/src/data/gitignore-templates.js +0 -21
  70. package/src/data/marketplaces.js +0 -138
  71. package/src/data/mcp-servers.js +0 -509
  72. package/src/data/predefined-profiles.js +0 -248
  73. package/src/data/settings-catalog.js +0 -625
  74. package/src/data/skill-repos.js +0 -160
  75. package/src/data/statuslines.js +0 -159
  76. package/src/data/statuslines.ts +0 -188
  77. package/src/index.js +0 -4
  78. package/src/index.ts +0 -5
  79. package/src/main.js +0 -170
  80. package/src/prerunner/index.js +0 -252
  81. package/src/services/alias-settings.js +0 -51
  82. package/src/services/alias-shell-writer.js +0 -1018
  83. package/src/services/alias-store.js +0 -129
  84. package/src/services/claude-cli.js +0 -189
  85. package/src/services/claude-runner.js +0 -28
  86. package/src/services/claude-settings.js +0 -1223
  87. package/src/services/gitignore-detector.js +0 -155
  88. package/src/services/gitignore-fixer.js +0 -231
  89. package/src/services/gitignore-prerun.js +0 -46
  90. package/src/services/gitignore-resolver.js +0 -143
  91. package/src/services/gitignore-service.js +0 -117
  92. package/src/services/local-marketplace.js +0 -339
  93. package/src/services/marketplace-fetcher.js +0 -96
  94. package/src/services/marketplace-sync.js +0 -94
  95. package/src/services/mcp-registry.js +0 -87
  96. package/src/services/plugin-manager.js +0 -473
  97. package/src/services/plugin-mcp-config.js +0 -177
  98. package/src/services/plugin-setup.js +0 -499
  99. package/src/services/plugin-version-check.js +0 -262
  100. package/src/services/profiles.js +0 -161
  101. package/src/services/settings-manager.js +0 -243
  102. package/src/services/skills-manager.js +0 -393
  103. package/src/services/skillsmp-client.js +0 -87
  104. package/src/services/update-cache.js +0 -52
  105. package/src/services/version-check.js +0 -99
  106. package/src/types/gitignore.js +0 -6
  107. package/src/types/index.js +0 -1
  108. package/src/ui/App.js +0 -355
  109. package/src/ui/adapters/pluginsAdapter.js +0 -139
  110. package/src/ui/adapters/settingsAdapter.js +0 -111
  111. package/src/ui/adapters/skillsAdapter.js +0 -154
  112. package/src/ui/components/CategoryHeader.js +0 -9
  113. package/src/ui/components/EmptyFilterState.js +0 -4
  114. package/src/ui/components/FlagDetailEditor.js +0 -0
  115. package/src/ui/components/ScopeIndicator.js +0 -30
  116. package/src/ui/components/ScrollableList.js +0 -36
  117. package/src/ui/components/SearchInput.js +0 -19
  118. package/src/ui/components/StyledText.js +0 -39
  119. package/src/ui/components/TabBar.js +0 -19
  120. package/src/ui/components/layout/FooterHints.js +0 -29
  121. package/src/ui/components/layout/Panel.js +0 -6
  122. package/src/ui/components/layout/ProgressBar.js +0 -14
  123. package/src/ui/components/layout/ScopeTabs.js +0 -6
  124. package/src/ui/components/layout/ScreenLayout.js +0 -16
  125. package/src/ui/components/layout/index.js +0 -5
  126. package/src/ui/components/modals/ConfirmModal.js +0 -14
  127. package/src/ui/components/modals/InputModal.js +0 -5
  128. package/src/ui/components/modals/LoadingModal.js +0 -17
  129. package/src/ui/components/modals/MessageModal.js +0 -16
  130. package/src/ui/components/modals/ModalContainer.js +0 -137
  131. package/src/ui/components/modals/SelectModal.js +0 -18
  132. package/src/ui/components/modals/VersionMismatchModal.js +0 -36
  133. package/src/ui/components/modals/index.js +0 -6
  134. package/src/ui/components/primitives/ActionHints.js +0 -13
  135. package/src/ui/components/primitives/DetailSection.js +0 -7
  136. package/src/ui/components/primitives/KeyValueLine.js +0 -8
  137. package/src/ui/components/primitives/ListCategoryRow.js +0 -8
  138. package/src/ui/components/primitives/MetaText.js +0 -8
  139. package/src/ui/components/primitives/ScopeDetail.js +0 -32
  140. package/src/ui/components/primitives/ScopeSquares.js +0 -11
  141. package/src/ui/components/primitives/SelectableRow.js +0 -5
  142. package/src/ui/components/primitives/index.js +0 -8
  143. package/src/ui/hooks/index.js +0 -4
  144. package/src/ui/hooks/useAsyncData.js +0 -77
  145. package/src/ui/hooks/useGitignoreModal.js +0 -74
  146. package/src/ui/hooks/useKeyboard.js +0 -13
  147. package/src/ui/hooks/useKeyboardHandler.js +0 -39
  148. package/src/ui/hooks/useMismatchModal.js +0 -77
  149. package/src/ui/registry.js +0 -1
  150. package/src/ui/renderers/cliToolRenderers.js +0 -54
  151. package/src/ui/renderers/gitignoreRenderers.js +0 -46
  152. package/src/ui/renderers/mcpRenderers.js +0 -26
  153. package/src/ui/renderers/pluginRenderers.js +0 -124
  154. package/src/ui/renderers/profileRenderers.js +0 -177
  155. package/src/ui/renderers/settingsRenderers.js +0 -73
  156. package/src/ui/renderers/skillRenderers.js +0 -138
  157. package/src/ui/screens/AliasScreen.js +0 -1111
  158. package/src/ui/screens/CliToolsScreen.js +0 -338
  159. package/src/ui/screens/EnvVarsScreen.js +0 -152
  160. package/src/ui/screens/GitignoreScreen.js +0 -328
  161. package/src/ui/screens/McpRegistryScreen.js +0 -238
  162. package/src/ui/screens/McpScreen.js +0 -176
  163. package/src/ui/screens/ModelSelectorScreen.js +0 -296
  164. package/src/ui/screens/ModelSelectorScreen.tsx +0 -444
  165. package/src/ui/screens/PluginsScreen.js +0 -769
  166. package/src/ui/screens/ProfilesScreen.js +0 -298
  167. package/src/ui/screens/SkillsScreen.js +0 -549
  168. package/src/ui/screens/StatusLineScreen.js +0 -206
  169. package/src/ui/screens/StatusLineScreen.tsx +0 -416
  170. package/src/ui/screens/index.js +0 -10
  171. package/src/ui/state/AnimationContext.js +0 -34
  172. package/src/ui/state/AppContext.js +0 -162
  173. package/src/ui/state/DimensionsContext.js +0 -71
  174. package/src/ui/state/reducer.js +0 -547
  175. package/src/ui/state/types.js +0 -1
  176. package/src/ui/theme.js +0 -47
  177. package/src/utils/clipboard.js +0 -56
  178. package/src/utils/command-utils.js +0 -19
  179. package/src/utils/fuzzy-search.js +0 -101
  180. package/src/utils/string-utils.js +0 -62
@@ -1,262 +0,0 @@
1
- /**
2
- * Plugin version mismatch detection and fix.
3
- *
4
- * Claude Code's plugin loader (pluginLoader.ts:2051) takes the FIRST entry
5
- * from installed_plugins.json for each plugin ID:
6
- *
7
- * const installEntry = installedPluginsData.plugins[pluginId]?.[0]
8
- *
9
- * This means the first project to install a plugin determines which version
10
- * loads for ALL projects. When different projects install different versions,
11
- * only the one at index [0] takes effect.
12
- *
13
- * Bug report: https://github.com/anthropics/claude-code/issues/45997
14
- */
15
- import { getEnabledPlugins, getGlobalEnabledPlugins, getLocalEnabledPlugins, readInstalledPluginsRegistry, writeInstalledPluginsRegistry, } from "./claude-settings.js";
16
- const BUG_REPORT_URL = "https://github.com/anthropics/claude-code/issues/45997";
17
- // ── Detection ────────────────────────────────────────────────────────────────
18
- /**
19
- * Check for plugin version mismatches caused by the [0] index bug.
20
- *
21
- * For each enabled plugin in the current project:
22
- * 1. Reads the plugin's entry array from installed_plugins.json
23
- * 2. Finds the entry matching the current project
24
- * 3. Checks if entries[0] has a DIFFERENT version than the current project's entry
25
- * 4. If yes: this project will load the wrong version
26
- *
27
- * @param projectPath - The current project path to check
28
- * @returns Array of mismatch info for each affected plugin
29
- */
30
- export async function checkPluginVersionMismatches(projectPath) {
31
- const registry = await readInstalledPluginsRegistry();
32
- const enabledPluginIds = await collectEnabledPluginIds(projectPath);
33
- const mismatches = [];
34
- for (const pluginId of enabledPluginIds) {
35
- const entries = registry.plugins[pluginId];
36
- if (!entries || entries.length < 2)
37
- continue;
38
- // Find the entry for the current project
39
- const currentEntry = entries.find((e) => e.scope === "project" &&
40
- normalizePath(e.projectPath) === normalizePath(projectPath));
41
- if (!currentEntry)
42
- continue;
43
- const firstEntry = entries[0];
44
- // Compare the version at [0] with the current project's version
45
- if (firstEntry.version !== currentEntry.version) {
46
- mismatches.push({
47
- pluginId,
48
- firstEntryVersion: firstEntry.version,
49
- firstEntryProject: firstEntry.projectPath,
50
- currentProjectVersion: currentEntry.version,
51
- allEntries: entries,
52
- });
53
- }
54
- }
55
- return mismatches;
56
- }
57
- /**
58
- * Check a single plugin for version mismatch (used after plugin update).
59
- *
60
- * @param pluginId - The plugin ID to check
61
- * @param projectPath - The current project path
62
- * @returns Mismatch info or null if no mismatch
63
- */
64
- export async function checkSinglePluginMismatch(pluginId, projectPath) {
65
- const registry = await readInstalledPluginsRegistry();
66
- const entries = registry.plugins[pluginId];
67
- if (!entries || entries.length < 2)
68
- return null;
69
- const currentEntry = entries.find((e) => e.scope === "project" &&
70
- normalizePath(e.projectPath) === normalizePath(projectPath));
71
- if (!currentEntry)
72
- return null;
73
- const firstEntry = entries[0];
74
- if (firstEntry.version === currentEntry.version)
75
- return null;
76
- return {
77
- pluginId,
78
- firstEntryVersion: firstEntry.version,
79
- firstEntryProject: firstEntry.projectPath,
80
- currentProjectVersion: currentEntry.version,
81
- allEntries: entries,
82
- };
83
- }
84
- // ── Fix ──────────────────────────────────────────────────────────────────────
85
- /**
86
- * Fix a version mismatch by updating ALL entries for a plugin to use
87
- * the target version. This is a JSON-only operation that rewrites
88
- * installPath and version in each entry.
89
- *
90
- * IMPORTANT: This modifies installed_plugins.json which is Claude Code-owned.
91
- * Only call after explicit user consent.
92
- *
93
- * @param pluginId - The plugin ID to fix
94
- * @param targetVersion - The version to normalize all entries to
95
- * @returns Number of updated entries and which projects were affected
96
- */
97
- export async function fixPluginVersionMismatch(pluginId, targetVersion) {
98
- const registry = await readInstalledPluginsRegistry();
99
- const entries = registry.plugins[pluginId];
100
- if (!entries || entries.length === 0) {
101
- return { updated: 0, projects: [] };
102
- }
103
- // Find an existing entry with the target version to get the correct installPath
104
- const templateEntry = entries.find((e) => e.version === targetVersion);
105
- if (!templateEntry) {
106
- return { updated: 0, projects: [] };
107
- }
108
- const targetInstallPath = templateEntry.installPath;
109
- const updated = [];
110
- for (const entry of entries) {
111
- if (entry.version !== targetVersion) {
112
- entry.version = targetVersion;
113
- entry.installPath = targetInstallPath;
114
- entry.lastUpdated = new Date().toISOString();
115
- updated.push(entry.projectPath || "(global)");
116
- }
117
- }
118
- if (updated.length > 0) {
119
- await writeInstalledPluginsRegistry(registry);
120
- }
121
- return { updated: updated.length, projects: updated };
122
- }
123
- /**
124
- * Fix all version mismatches by normalizing each plugin to its latest version.
125
- *
126
- * @param mismatches - Array of mismatches from checkPluginVersionMismatches
127
- * @returns Map of pluginId to FixResult
128
- */
129
- export async function fixAllPluginVersionMismatches(mismatches) {
130
- const results = new Map();
131
- // Read registry once, apply all fixes, write once
132
- const registry = await readInstalledPluginsRegistry();
133
- for (const mismatch of mismatches) {
134
- const entries = registry.plugins[mismatch.pluginId];
135
- if (!entries || entries.length === 0) {
136
- results.set(mismatch.pluginId, { updated: 0, projects: [] });
137
- continue;
138
- }
139
- // Use the current project's version as the target (the version the user expects)
140
- const targetVersion = mismatch.currentProjectVersion;
141
- const templateEntry = entries.find((e) => e.version === targetVersion);
142
- if (!templateEntry) {
143
- results.set(mismatch.pluginId, { updated: 0, projects: [] });
144
- continue;
145
- }
146
- const targetInstallPath = templateEntry.installPath;
147
- const updated = [];
148
- for (const entry of entries) {
149
- if (entry.version !== targetVersion) {
150
- entry.version = targetVersion;
151
- entry.installPath = targetInstallPath;
152
- entry.lastUpdated = new Date().toISOString();
153
- updated.push(entry.projectPath || "(global)");
154
- }
155
- }
156
- results.set(mismatch.pluginId, {
157
- updated: updated.length,
158
- projects: updated,
159
- });
160
- }
161
- await writeInstalledPluginsRegistry(registry);
162
- return results;
163
- }
164
- // ── Formatting ───────────────────────────────────────────────────────────────
165
- /**
166
- * Format mismatch info for console output (prerunner).
167
- */
168
- export function formatMismatchWarning(mismatches) {
169
- const lines = [];
170
- lines.push("WARNING: Plugin version mismatch detected (Claude Code bug #45997)");
171
- lines.push("Claude Code loads the first entry from installed_plugins.json for ALL projects.");
172
- lines.push("The following plugins will load a different version than what this project has installed:\n");
173
- for (const m of mismatches) {
174
- const firstProject = m.firstEntryProject
175
- ? shortenPath(m.firstEntryProject)
176
- : "(global)";
177
- lines.push(` ${m.pluginId}:`);
178
- lines.push(` Loading v${m.firstEntryVersion} (from ${firstProject})`);
179
- lines.push(` Expected v${m.currentProjectVersion} (this project)`);
180
- }
181
- lines.push("");
182
- lines.push(`Bug report: ${BUG_REPORT_URL}`);
183
- lines.push("Run claudeup to fix — it can align all projects to the same version.");
184
- return lines.join("\n");
185
- }
186
- /**
187
- * Format mismatch info for TUI modal display.
188
- * Returns a multi-line string suitable for a modal message.
189
- */
190
- export function formatMismatchModal(mismatches) {
191
- const lines = [];
192
- lines.push("Your plugins will not work correctly in this project.");
193
- lines.push("");
194
- lines.push("Due to a Claude Code bug, the plugin loader ignores");
195
- lines.push("per-project versions and loads an older version instead.");
196
- lines.push("Commands, MCP tools, and skills will be outdated.");
197
- lines.push("");
198
- for (const m of mismatches) {
199
- const name = m.pluginId.split("@")[0];
200
- lines.push(` ${name}: v${m.firstEntryVersion} loaded, expected v${m.currentProjectVersion}`);
201
- }
202
- lines.push("");
203
- lines.push("Fix: align all projects to the same version.");
204
- lines.push("Details: github.com/anthropics/claude-code/issues/45997");
205
- return lines.join("\n");
206
- }
207
- // ── Helpers ──────────────────────────────────────────────────────────────────
208
- /**
209
- * Collect all enabled plugin IDs across all scopes for a project.
210
- */
211
- async function collectEnabledPluginIds(projectPath) {
212
- const pluginIds = new Set();
213
- try {
214
- const global = await getGlobalEnabledPlugins();
215
- for (const [id, enabled] of Object.entries(global)) {
216
- if (enabled)
217
- pluginIds.add(id);
218
- }
219
- }
220
- catch {
221
- /* skip unreadable */
222
- }
223
- try {
224
- const project = await getEnabledPlugins(projectPath);
225
- for (const [id, enabled] of Object.entries(project)) {
226
- if (enabled)
227
- pluginIds.add(id);
228
- }
229
- }
230
- catch {
231
- /* skip unreadable */
232
- }
233
- try {
234
- const local = await getLocalEnabledPlugins(projectPath);
235
- for (const [id, enabled] of Object.entries(local)) {
236
- if (enabled)
237
- pluginIds.add(id);
238
- }
239
- }
240
- catch {
241
- /* skip unreadable */
242
- }
243
- return pluginIds;
244
- }
245
- /**
246
- * Normalize a path for comparison (resolve and trailing-slash normalize).
247
- */
248
- function normalizePath(p) {
249
- if (!p)
250
- return "";
251
- return p.replace(/\/+$/, "");
252
- }
253
- /**
254
- * Shorten a path for display by replacing homedir with ~.
255
- */
256
- function shortenPath(p) {
257
- const home = process.env.HOME || process.env.USERPROFILE || "";
258
- if (home && p.startsWith(home)) {
259
- return `~${p.slice(home.length)}`;
260
- }
261
- return p;
262
- }
@@ -1,161 +0,0 @@
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
- }
@@ -1,243 +0,0 @@
1
- import * as fs from "fs";
2
- import * as path from "path";
3
- import { readSettings, writeSettings, readGlobalSettings, writeGlobalSettings, } from "./claude-settings.js";
4
- /** Read the current value of a setting from the given scope */
5
- export async function readSettingValue(setting, scope, projectPath) {
6
- const settings = scope === "user"
7
- ? await readGlobalSettings()
8
- : await readSettings(projectPath);
9
- if (setting.storage.type === "attribution") {
10
- // Attribution is an object: { commit: "", pr: "" } means disabled
11
- const attr = settings.attribution;
12
- if (attr && attr.commit === "" && attr.pr === "") {
13
- return "false";
14
- }
15
- return undefined; // default (enabled)
16
- }
17
- else if (setting.storage.type === "attribution-text") {
18
- // Custom attribution text: read from attribution.commit, strip the Co-Authored-By trailer prefix
19
- const attr = settings.attribution;
20
- if (!attr || (attr.commit === "" && attr.pr === "")) {
21
- // Attribution is disabled or not set — no custom text stored
22
- return undefined;
23
- }
24
- const commit = attr.commit;
25
- if (!commit) {
26
- return undefined;
27
- }
28
- // The commit value is "Co-Authored-By: Magus <magus@madappgang.com>\n\n{customText}"
29
- // Strip the trailer prefix if present
30
- const trailerPrefix = "Co-Authored-By: Magus <magus@madappgang.com>\n\n";
31
- if (commit.startsWith(trailerPrefix)) {
32
- const text = commit.slice(trailerPrefix.length);
33
- return text.length > 0 ? text : undefined;
34
- }
35
- // If no trailer prefix, the value is the raw text (or Claude default — return undefined)
36
- return undefined;
37
- }
38
- else if (setting.storage.type === "env") {
39
- const env = settings.env;
40
- return env?.[setting.storage.key];
41
- }
42
- else {
43
- // Direct settings key (supports dot notation like "permissions.defaultMode")
44
- const keys = setting.storage.key.split(".");
45
- let value = settings;
46
- for (const k of keys) {
47
- value = value?.[k];
48
- }
49
- return value !== undefined && value !== null ? String(value) : undefined;
50
- }
51
- }
52
- /** Write a setting value to the given scope */
53
- export async function writeSettingValue(setting, value, scope, projectPath) {
54
- const settings = scope === "user"
55
- ? await readGlobalSettings()
56
- : await readSettings(projectPath);
57
- if (setting.storage.type === "attribution") {
58
- // Boolean toggle: "false" -> write { commit: "", pr: "" }, "true"/undefined -> delete key
59
- if (value === "false") {
60
- settings.attribution = { commit: "", pr: "" };
61
- }
62
- else {
63
- delete settings.attribution;
64
- }
65
- }
66
- else if (setting.storage.type === "attribution-text") {
67
- if (value && value.trim().length > 0) {
68
- // Write custom text: commit gets a Co-Authored-By trailer + the text; pr gets the text
69
- settings.attribution = {
70
- commit: `Co-Authored-By: Magus <magus@madappgang.com>\n\n${value}`,
71
- pr: value,
72
- };
73
- }
74
- else {
75
- // Empty value: remove attribution key entirely (revert to Claude defaults)
76
- delete settings.attribution;
77
- }
78
- }
79
- else if (setting.storage.type === "env") {
80
- // Write to the "env" block in settings.json
81
- settings.env = settings.env || {};
82
- if (value === undefined || value === "" || value === setting.defaultValue) {
83
- delete settings.env[setting.storage.key];
84
- // Clean up empty env block
85
- if (Object.keys(settings.env).length === 0) {
86
- delete settings.env;
87
- }
88
- }
89
- else {
90
- settings.env[setting.storage.key] = value;
91
- }
92
- }
93
- else {
94
- // Direct settings key (supports dot notation)
95
- const keys = setting.storage.key.split(".");
96
- if (keys.length === 1) {
97
- if (value === undefined || value === "") {
98
- delete settings[keys[0]];
99
- }
100
- else {
101
- // Try to preserve type: boolean, number, or string
102
- settings[keys[0]] = parseSettingValue(value, setting.type);
103
- }
104
- }
105
- else {
106
- // Nested key like "permissions.defaultMode"
107
- let obj = settings;
108
- for (let i = 0; i < keys.length - 1; i++) {
109
- obj[keys[i]] = obj[keys[i]] || {};
110
- obj = obj[keys[i]];
111
- }
112
- const lastKey = keys[keys.length - 1];
113
- if (value === undefined || value === "") {
114
- delete obj[lastKey];
115
- }
116
- else {
117
- obj[lastKey] = parseSettingValue(value, setting.type);
118
- }
119
- }
120
- }
121
- if (scope === "user") {
122
- await writeGlobalSettings(settings);
123
- }
124
- else {
125
- await writeSettings(settings, projectPath);
126
- }
127
- }
128
- function parseSettingValue(value, type) {
129
- if (type === "boolean") {
130
- return value === "true" || value === "1";
131
- }
132
- // Try number
133
- const num = Number(value);
134
- if (!Number.isNaN(num) && value.trim() !== "") {
135
- return value; // Keep as string for env vars
136
- }
137
- return value;
138
- }
139
- /** Read all setting values for the catalog */
140
- export async function readAllSettings(catalog, scope, projectPath) {
141
- const values = new Map();
142
- for (const setting of catalog) {
143
- values.set(setting.id, await readSettingValue(setting, scope, projectPath));
144
- }
145
- return values;
146
- }
147
- /** Read all setting values from both scopes simultaneously */
148
- export async function readAllSettingsBothScopes(catalog, projectPath) {
149
- const result = new Map();
150
- const [userValues, projectValues] = await Promise.all([
151
- readAllSettings(catalog, "user", projectPath),
152
- readAllSettings(catalog, "project", projectPath),
153
- ]);
154
- for (const setting of catalog) {
155
- result.set(setting.id, {
156
- user: userValues.get(setting.id),
157
- project: projectValues.get(setting.id),
158
- });
159
- }
160
- return result;
161
- }
162
- /**
163
- * Discover available output styles from installed plugins.
164
- * Scans enabled plugins for outputStyles entries and *-output-style plugin names.
165
- * Returns options suitable for a select setting.
166
- */
167
- export async function discoverOutputStyles(projectPath) {
168
- const styles = [
169
- { label: "Default", value: "" },
170
- ];
171
- const homeDir = process.env.HOME || process.env.USERPROFILE || "";
172
- const cacheDir = path.join(homeDir, ".claude", "plugins", "cache");
173
- // Collect enabled plugins from both scopes
174
- const enabledPlugins = new Set();
175
- try {
176
- const userSettings = await readGlobalSettings();
177
- for (const [k, v] of Object.entries(userSettings.enabledPlugins || {})) {
178
- if (v)
179
- enabledPlugins.add(k);
180
- }
181
- }
182
- catch { }
183
- try {
184
- const projSettings = await readSettings(projectPath);
185
- for (const [k, v] of Object.entries(projSettings.enabledPlugins || {})) {
186
- if (v)
187
- enabledPlugins.add(k);
188
- }
189
- }
190
- catch { }
191
- const seen = new Set();
192
- for (const pluginId of enabledPlugins) {
193
- const [pluginName, marketplace] = pluginId.split("@");
194
- // Check if this is a dedicated output-style plugin (e.g. "explanatory-output-style")
195
- if (pluginName.endsWith("-output-style")) {
196
- const styleName = pluginName
197
- .replace(/-output-style$/, "")
198
- .split("-")
199
- .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
200
- .join(" ");
201
- if (!seen.has(styleName)) {
202
- seen.add(styleName);
203
- styles.push({ label: styleName, value: styleName });
204
- }
205
- continue;
206
- }
207
- // Check plugin.json for outputStyles entries
208
- if (!marketplace)
209
- continue;
210
- const pluginDir = path.join(cacheDir, marketplace, pluginName);
211
- try {
212
- const versions = fs.readdirSync(pluginDir).sort();
213
- const latest = versions[versions.length - 1];
214
- if (!latest)
215
- continue;
216
- const manifestPath = path.join(pluginDir, latest, "plugin.json");
217
- const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
218
- const outputStyles = manifest.outputStyles;
219
- if (!outputStyles?.length)
220
- continue;
221
- for (const stylePath of outputStyles) {
222
- const fullPath = path.join(pluginDir, latest, stylePath);
223
- try {
224
- const content = fs.readFileSync(fullPath, "utf-8");
225
- const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
226
- if (fmMatch) {
227
- const nameMatch = fmMatch[1].match(/^name:\s*(.+)$/m);
228
- if (nameMatch) {
229
- const name = nameMatch[1].trim();
230
- if (!seen.has(name)) {
231
- seen.add(name);
232
- styles.push({ label: name, value: name });
233
- }
234
- }
235
- }
236
- }
237
- catch { }
238
- }
239
- }
240
- catch { }
241
- }
242
- return styles;
243
- }