claudeup 6.2.1 → 6.3.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/README.md +22 -0
- package/bin/claudeup.js +6 -1
- package/package.json +6 -6
- package/src/__tests__/alias-adopt.test.ts +331 -329
- package/src/__tests__/alias-parser.test.ts +384 -376
- package/src/__tests__/alias-shell-writer.test.ts +638 -625
- package/src/__tests__/alias-store.test.ts +143 -143
- package/src/__tests__/catalog-cache-store.test.ts +265 -265
- package/src/__tests__/catalog-notice.test.ts +123 -123
- package/src/__tests__/cli-apply-seams.test.ts +12 -8
- package/src/__tests__/cli-router.test.ts +132 -3
- package/src/__tests__/cli-update-view.test.ts +27 -8
- package/src/__tests__/community-staleness.test.ts +0 -2
- package/src/__tests__/component-badge.test.ts +4 -4
- package/src/__tests__/content-drift.test.ts +5 -1
- package/src/__tests__/conventions-integration.test.ts +774 -689
- package/src/__tests__/conventions-manager.test.ts +1216 -1152
- package/src/__tests__/doctor-bins.test.ts +14 -6
- package/src/__tests__/doctor.test.ts +438 -425
- package/src/__tests__/dual-write-prevention.test.ts +277 -282
- package/src/__tests__/enabled-not-installed.test.ts +71 -63
- package/src/__tests__/file-locking.test.ts +196 -196
- package/src/__tests__/gap-fill-versions.test.ts +318 -311
- package/src/__tests__/github-budget.test.ts +170 -164
- package/src/__tests__/gitignore-detector.test.ts +160 -152
- package/src/__tests__/gitignore-fixer.test.ts +285 -247
- package/src/__tests__/gitignore-prerun.test.ts +57 -57
- package/src/__tests__/gitignore-resolver.test.ts +173 -146
- package/src/__tests__/gitignore-service.test.ts +121 -119
- package/src/__tests__/install-command.test.ts +3 -3
- package/src/__tests__/install-plan.test.ts +13 -7
- package/src/__tests__/manifest.test.ts +16 -5
- package/src/__tests__/markdown-renderer.test.ts +0 -1
- package/src/__tests__/marketplace-badge.test.ts +111 -90
- package/src/__tests__/marketplace-version.test.ts +64 -61
- package/src/__tests__/plugin-manager-fallback.test.ts +249 -239
- package/src/__tests__/plugin-requires.test.ts +18 -4
- package/src/__tests__/plugin-setup.test.ts +12 -12
- package/src/__tests__/profile-command.test.ts +1 -7
- package/src/__tests__/profile-materializer.test.ts +31 -8
- package/src/__tests__/profile-sync.test.ts +23 -5
- package/src/__tests__/registry-version-resolution.test.ts +61 -47
- package/src/__tests__/resolve-executable.test.ts +1 -1
- package/src/__tests__/resolver.test.ts +38 -13
- package/src/__tests__/scope-action.test.ts +52 -50
- package/src/__tests__/scope-squares.test.tsx +4 -5
- package/src/__tests__/skill-install-files.test.ts +111 -103
- package/src/__tests__/symlink-manager.test.ts +27 -10
- package/src/__tests__/theme-env.test.ts +281 -0
- package/src/__tests__/theme-resolve.test.ts +380 -0
- package/src/__tests__/update-apply.test.ts +1 -3
- package/src/__tests__/useGitignoreModal.test.ts +69 -61
- package/src/__tests__/version-snapshot.test.ts +183 -165
- package/src/cli/ansi.ts +23 -15
- package/src/cli/doctor.ts +10 -9
- package/src/cli/install.ts +3 -3
- package/src/cli/profile.ts +5 -4
- package/src/cli/prompt.ts +0 -1
- package/src/cli/router.ts +69 -2
- package/src/cli/update-view.ts +11 -6
- package/src/cli/update.ts +9 -4
- package/src/cli/upgrade.ts +6 -4
- package/src/data/alias-flags.ts +260 -260
- package/src/data/gitignore-defaults.ts +20 -20
- package/src/data/gitignore-reasons.ts +86 -92
- package/src/data/gitignore-templates.ts +14 -14
- package/src/data/predefined-profiles.ts +1 -2
- package/src/data/skill-repos.ts +43 -35
- package/src/main.tsx +29 -6
- package/src/opentui.d.ts +1 -3
- package/src/prerunner/index.ts +7 -8
- package/src/services/alias-settings.ts +20 -23
- package/src/services/alias-shell-writer.ts +800 -803
- package/src/services/alias-store.ts +77 -77
- package/src/services/claude-settings.ts +27 -18
- package/src/services/content-drift.ts +13 -3
- package/src/services/conventions-manager.ts +692 -617
- package/src/services/doctor.ts +464 -403
- package/src/services/gitignore-detector.ts +113 -103
- package/src/services/gitignore-fixer.ts +204 -189
- package/src/services/gitignore-prerun.ts +26 -26
- package/src/services/gitignore-resolver.ts +162 -144
- package/src/services/gitignore-service.ts +120 -117
- package/src/services/install-plan.ts +4 -4
- package/src/services/manifest.ts +1 -1
- package/src/services/marketplace-catalog-git.ts +5 -2
- package/src/services/marketplace-fetcher.ts +241 -235
- package/src/services/plugin-manager.ts +17 -20
- package/src/services/plugin-mcp-config.ts +2 -2
- package/src/services/plugin-requires.ts +9 -6
- package/src/services/plugin-setup.ts +7 -9
- package/src/services/plugin-version-check.ts +4 -4
- package/src/services/profile-materializer.ts +9 -5
- package/src/services/profile-sync.ts +12 -4
- package/src/services/profiles.ts +3 -3
- package/src/services/resolver.ts +10 -6
- package/src/services/settings-manager.ts +82 -46
- package/src/services/skills-manager.ts +11 -9
- package/src/services/symlink-manager.ts +2 -5
- package/src/services/toolchain.ts +3 -3
- package/src/services/update-cache.ts +1 -1
- package/src/services/update-engine.ts +4 -4
- package/src/services/update-plan.ts +9 -11
- package/src/services/version-snapshot.ts +88 -88
- package/src/types/gitignore.ts +28 -28
- package/src/ui/App.tsx +36 -36
- package/src/ui/adapters/settingsAdapter.ts +2 -2
- package/src/ui/components/CategoryHeader.tsx +1 -1
- package/src/ui/components/FlagDetailEditor.tsx +0 -0
- package/src/ui/components/ScopeIndicator.tsx +3 -1
- package/src/ui/components/ScrollableList.tsx +2 -1
- package/src/ui/components/StyledText.tsx +1 -1
- package/src/ui/components/layout/FooterHints.tsx +5 -3
- package/src/ui/components/layout/Panel.tsx +2 -2
- package/src/ui/components/layout/ScreenLayout.tsx +5 -4
- package/src/ui/components/modals/InputModal.tsx +7 -1
- package/src/ui/components/modals/ModalContainer.tsx +3 -3
- package/src/ui/components/modals/SelectModal.tsx +1 -1
- package/src/ui/components/primitives/DetailSection.tsx +1 -1
- package/src/ui/components/primitives/KeyValueLine.tsx +1 -1
- package/src/ui/components/primitives/ScopeSquares.tsx +2 -2
- package/src/ui/hooks/index.ts +0 -1
- package/src/ui/hooks/useGitignoreModal.ts +1 -5
- package/src/ui/hooks/useMismatchModal.ts +4 -10
- package/src/ui/renderers/cliToolRenderers.tsx +2 -2
- package/src/ui/renderers/gitignoreRenderers.tsx +2 -2
- package/src/ui/renderers/mcpRenderers.tsx +1 -1
- package/src/ui/renderers/pluginRenderers.tsx +3 -9
- package/src/ui/renderers/profileRenderers.tsx +9 -8
- package/src/ui/renderers/settingsRenderers.tsx +4 -4
- package/src/ui/renderers/skillRenderers.tsx +14 -14
- package/src/ui/screens/AliasScreen.tsx +31 -34
- package/src/ui/screens/CliToolsScreen.tsx +14 -13
- package/src/ui/screens/EnvVarsScreen.tsx +8 -8
- package/src/ui/screens/GitignoreScreen.tsx +15 -14
- package/src/ui/screens/McpRegistryScreen.tsx +10 -7
- package/src/ui/screens/McpScreen.tsx +10 -10
- package/src/ui/screens/PluginsScreen.tsx +17 -21
- package/src/ui/screens/ProfilesScreen.tsx +25 -24
- package/src/ui/screens/SkillsScreen.tsx +16 -14
- package/src/ui/state/AppContext.tsx +4 -8
- package/src/ui/state/DimensionsContext.tsx +3 -2
- package/src/ui/theme-mode.ts +20 -38
- package/src/ui/theme-resolve.ts +148 -0
- package/src/utils/clipboard.ts +5 -5
- package/src/utils/command-utils.ts +2 -2
- package/src/utils/config-dir.ts +1 -3
- package/src/utils/file-locking.ts +88 -86
- package/src/ui/hooks/useAsyncData.ts +0 -127
- package/src/ui/state/AnimationContext.tsx +0 -76
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { defaultMarketplaces } from "../data/marketplaces.js";
|
|
3
|
+
import type {
|
|
4
|
+
InstalledPluginsRegistry,
|
|
5
|
+
PluginRelease,
|
|
6
|
+
} from "../types/index.js";
|
|
7
|
+
import { formatMarketplaceName, parsePluginId } from "../utils/string-utils.js";
|
|
8
|
+
import type { PluginScope } from "./claude-cli.js";
|
|
2
9
|
import {
|
|
3
10
|
getConfiguredMarketplaces,
|
|
4
11
|
getEnabledPlugins,
|
|
5
12
|
getGlobalClaudeDir,
|
|
6
|
-
readSettings,
|
|
7
|
-
writeSettings,
|
|
8
13
|
getGlobalConfiguredMarketplaces,
|
|
9
14
|
getGlobalEnabledPlugins,
|
|
10
15
|
getGlobalInstalledPluginVersions,
|
|
@@ -13,40 +18,32 @@ import {
|
|
|
13
18
|
getProjectInstalledPluginVersions,
|
|
14
19
|
pickRegistryEntry,
|
|
15
20
|
readInstalledPluginsRegistry,
|
|
21
|
+
readSettings,
|
|
22
|
+
removeFromInstalledPluginsRegistry,
|
|
16
23
|
saveGlobalInstalledPluginVersion,
|
|
17
24
|
saveLocalInstalledPluginVersion,
|
|
18
25
|
updateInstalledPluginsRegistry,
|
|
19
|
-
|
|
26
|
+
writeSettings,
|
|
20
27
|
} from "./claude-settings.js";
|
|
21
|
-
import type { PluginScope } from "./claude-cli.js";
|
|
22
28
|
import { hasContentDrift } from "./content-drift.js";
|
|
23
29
|
import { inheritablePaths } from "./git-worktree.js";
|
|
24
|
-
import { defaultMarketplaces } from "../data/marketplaces.js";
|
|
25
|
-
import type {
|
|
26
|
-
InstalledPluginsRegistry,
|
|
27
|
-
PluginRelease,
|
|
28
|
-
} from "../types/index.js";
|
|
29
30
|
import {
|
|
30
|
-
scanLocalMarketplaces,
|
|
31
|
-
repairAllMarketplaces,
|
|
32
31
|
type LocalMarketplace,
|
|
33
32
|
type ProgressCallback,
|
|
34
33
|
type RepairMarketplaceResult,
|
|
34
|
+
repairAllMarketplaces,
|
|
35
|
+
scanLocalMarketplaces,
|
|
35
36
|
} from "./local-marketplace.js";
|
|
36
37
|
import {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
import {
|
|
38
|
+
type CatalogSource,
|
|
39
|
+
type MarketplaceFetchFailure,
|
|
40
|
+
type MarketplacePlugin,
|
|
41
41
|
clearMarketplaceCache as clearFetcherCache,
|
|
42
42
|
clearPersistedCatalogs,
|
|
43
43
|
fetchMarketplacePlugins,
|
|
44
44
|
getMarketplaceFetchFailures,
|
|
45
45
|
isAuthoritative,
|
|
46
46
|
resolveMarketplacePlugins,
|
|
47
|
-
type CatalogSource,
|
|
48
|
-
type MarketplaceFetchFailure,
|
|
49
|
-
type MarketplacePlugin,
|
|
50
47
|
} from "./marketplace-fetcher.js";
|
|
51
48
|
export {
|
|
52
49
|
fetchMarketplacePlugins,
|
|
@@ -224,7 +221,6 @@ export function resolveScopeAction(
|
|
|
224
221
|
return hasUpdate ? "update" : "uninstall";
|
|
225
222
|
}
|
|
226
223
|
|
|
227
|
-
|
|
228
224
|
export async function getAvailablePlugins(
|
|
229
225
|
projectPath?: string,
|
|
230
226
|
): Promise<PluginInfo[]> {
|
|
@@ -728,7 +724,8 @@ async function annotateContentDrift(
|
|
|
728
724
|
plugins.map(async (plugin) => {
|
|
729
725
|
if (plugin.hasUpdate || plugin.isOrphaned) return;
|
|
730
726
|
if (!plugin.installedVersion || !plugin.version) return;
|
|
731
|
-
if (compareVersions(plugin.version, plugin.installedVersion) !== 0)
|
|
727
|
+
if (compareVersions(plugin.version, plugin.installedVersion) !== 0)
|
|
728
|
+
return;
|
|
732
729
|
|
|
733
730
|
const entry = pickRegistryEntry(
|
|
734
731
|
registry.plugins[plugin.id],
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
* bug where a plugin's MCP server silently fails because its binary is missing.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import fs from "fs-extra";
|
|
11
|
-
import path from "node:path";
|
|
12
10
|
import os from "node:os";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import fs from "fs-extra";
|
|
13
13
|
import type {
|
|
14
14
|
BinInstaller,
|
|
15
15
|
BinRequirement,
|
|
@@ -80,9 +80,7 @@ function parseSetup(setup: unknown): BinRequirement[] {
|
|
|
80
80
|
const s = setup as Record<string, unknown>;
|
|
81
81
|
const bins: BinRequirement[] = [];
|
|
82
82
|
|
|
83
|
-
const pkgManager = (
|
|
84
|
-
key: "bun" | "npm" | "pip",
|
|
85
|
-
): void => {
|
|
83
|
+
const pkgManager = (key: "bun" | "npm" | "pip"): void => {
|
|
86
84
|
for (const spec of (s[key] as string[] | undefined) ?? []) {
|
|
87
85
|
if (typeof spec !== "string") continue;
|
|
88
86
|
const { base, version } = splitVersion(spec);
|
|
@@ -116,7 +114,12 @@ function parseSetup(setup: unknown): BinRequirement[] {
|
|
|
116
114
|
}
|
|
117
115
|
for (const spec of (s.brew as string[] | undefined) ?? []) {
|
|
118
116
|
if (typeof spec !== "string") continue;
|
|
119
|
-
bins.push({
|
|
117
|
+
bins.push({
|
|
118
|
+
name: spec,
|
|
119
|
+
via: "brew",
|
|
120
|
+
formula: spec,
|
|
121
|
+
check: `${spec} --version`,
|
|
122
|
+
});
|
|
120
123
|
}
|
|
121
124
|
// `required` names a system tool that must be present; brew is the assumed
|
|
122
125
|
// installer (matches the "system tools via brew" decision).
|
|
@@ -26,12 +26,12 @@
|
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
28
|
import { execFile } from "node:child_process";
|
|
29
|
-
import { promisify } from "node:util";
|
|
30
|
-
import fs from "fs-extra";
|
|
31
|
-
import { access } from "node:fs/promises";
|
|
32
29
|
import { constants as fsConstants } from "node:fs";
|
|
33
|
-
import
|
|
30
|
+
import { access } from "node:fs/promises";
|
|
34
31
|
import os from "node:os";
|
|
32
|
+
import path from "node:path";
|
|
33
|
+
import { promisify } from "node:util";
|
|
34
|
+
import fs from "fs-extra";
|
|
35
35
|
import { which } from "../utils/command-utils.js";
|
|
36
36
|
import { goBinaryName } from "../utils/go-module.js";
|
|
37
37
|
|
|
@@ -800,10 +800,10 @@ export async function getPluginSetupConfig(
|
|
|
800
800
|
|
|
801
801
|
// Find the version directory
|
|
802
802
|
const versions = await fs.readdir(pluginPath);
|
|
803
|
-
if (versions.length === 0) return null;
|
|
804
803
|
|
|
805
804
|
// Use the latest version directory
|
|
806
|
-
const latestVersion = versions.sort().pop()
|
|
805
|
+
const latestVersion = versions.sort().pop();
|
|
806
|
+
if (latestVersion === undefined) return null;
|
|
807
807
|
const manifestPath = path.join(pluginPath, latestVersion, "plugin.json");
|
|
808
808
|
|
|
809
809
|
if (!(await fs.pathExists(manifestPath))) return null;
|
|
@@ -843,9 +843,7 @@ export async function getPluginSetupFromSource(
|
|
|
843
843
|
try {
|
|
844
844
|
const manifest = await fs.readJson(pluginJson);
|
|
845
845
|
return manifest.setup || null;
|
|
846
|
-
} catch {
|
|
847
|
-
continue;
|
|
848
|
-
}
|
|
846
|
+
} catch {}
|
|
849
847
|
}
|
|
850
848
|
}
|
|
851
849
|
|
|
@@ -276,9 +276,7 @@ export function formatMismatchWarning(
|
|
|
276
276
|
* Format mismatch info for TUI modal display.
|
|
277
277
|
* Returns a multi-line string suitable for a modal message.
|
|
278
278
|
*/
|
|
279
|
-
export function formatMismatchModal(
|
|
280
|
-
mismatches: VersionMismatchInfo[],
|
|
281
|
-
): string {
|
|
279
|
+
export function formatMismatchModal(mismatches: VersionMismatchInfo[]): string {
|
|
282
280
|
const lines: string[] = [];
|
|
283
281
|
|
|
284
282
|
lines.push("Your plugins will not work correctly in this project.");
|
|
@@ -290,7 +288,9 @@ export function formatMismatchModal(
|
|
|
290
288
|
|
|
291
289
|
for (const m of mismatches) {
|
|
292
290
|
const name = m.pluginId.split("@")[0];
|
|
293
|
-
lines.push(
|
|
291
|
+
lines.push(
|
|
292
|
+
` ${name}: v${m.firstEntryVersion} loaded, expected v${m.currentProjectVersion}`,
|
|
293
|
+
);
|
|
294
294
|
}
|
|
295
295
|
|
|
296
296
|
lines.push("");
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* and touches no global state, so it is fully testable with a temp dir.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import fs from "fs-extra";
|
|
15
14
|
import path from "node:path";
|
|
15
|
+
import fs from "fs-extra";
|
|
16
16
|
import type { ResolvedClosure } from "../types/index.js";
|
|
17
17
|
import { profileDir } from "./symlink-manager.js";
|
|
18
18
|
|
|
@@ -48,9 +48,9 @@ export function buildProfileSettings(
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/** Build the .mcp.json contents for a profile. */
|
|
51
|
-
export function buildProfileMcp(
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
export function buildProfileMcp(closure: ResolvedClosure): {
|
|
52
|
+
mcpServers: Record<string, unknown>;
|
|
53
|
+
} {
|
|
54
54
|
return { mcpServers: { ...closure.mcpServers } };
|
|
55
55
|
}
|
|
56
56
|
|
|
@@ -73,7 +73,11 @@ async function seedSkillsFromProject(
|
|
|
73
73
|
): Promise<void> {
|
|
74
74
|
if (await fs.pathExists(skillsDir)) return;
|
|
75
75
|
|
|
76
|
-
const projectSkills = path.join(
|
|
76
|
+
const projectSkills = path.join(
|
|
77
|
+
projectPath ?? process.cwd(),
|
|
78
|
+
".claude",
|
|
79
|
+
"skills",
|
|
80
|
+
);
|
|
77
81
|
try {
|
|
78
82
|
// lstat, not pathExists: an existing profile symlink is not a source.
|
|
79
83
|
const stat = await fs.lstat(projectSkills);
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* becomes a reviewable manifest diff instead of silent, throwaway drift.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import fs from "fs-extra";
|
|
12
11
|
import path from "node:path";
|
|
12
|
+
import fs from "fs-extra";
|
|
13
13
|
import type {
|
|
14
14
|
McpServerConfig,
|
|
15
15
|
ProfileManifest,
|
|
@@ -68,7 +68,10 @@ export function computeSync(
|
|
|
68
68
|
const { enabledPlugins: _drop, ...settings } = materializedSettings;
|
|
69
69
|
if (JSON.stringify(settings) !== JSON.stringify(entry.settings ?? {})) {
|
|
70
70
|
if (Object.keys(settings).length > 0) next.settings = settings;
|
|
71
|
-
else
|
|
71
|
+
else {
|
|
72
|
+
// biome-ignore lint/performance/noDelete: the manifest entry is serialised to profiles.json and diffed with JSON.stringify/toStrictEqual; the key must be absent, not undefined
|
|
73
|
+
delete next.settings;
|
|
74
|
+
}
|
|
72
75
|
changes.push({ kind: "settings-changed", detail: "settings updated" });
|
|
73
76
|
}
|
|
74
77
|
|
|
@@ -76,7 +79,10 @@ export function computeSync(
|
|
|
76
79
|
const mcp = materializedMcp.mcpServers ?? {};
|
|
77
80
|
if (JSON.stringify(mcp) !== JSON.stringify(entry.mcpServers ?? {})) {
|
|
78
81
|
if (Object.keys(mcp).length > 0) next.mcpServers = mcp;
|
|
79
|
-
else
|
|
82
|
+
else {
|
|
83
|
+
// biome-ignore lint/performance/noDelete: the manifest entry is serialised to profiles.json and diffed with JSON.stringify/toStrictEqual; the key must be absent, not undefined
|
|
84
|
+
delete next.mcpServers;
|
|
85
|
+
}
|
|
80
86
|
changes.push({ kind: "mcp-changed", detail: "mcp servers updated" });
|
|
81
87
|
}
|
|
82
88
|
|
|
@@ -129,7 +135,9 @@ export async function syncProfile(
|
|
|
129
135
|
const settingsPath = path.join(dir, "settings.json");
|
|
130
136
|
const mcpPath = path.join(dir, "mcp.json");
|
|
131
137
|
if (!(await fs.pathExists(settingsPath))) {
|
|
132
|
-
throw new Error(
|
|
138
|
+
throw new Error(
|
|
139
|
+
`Profile "${name}" isn't materialized (${settingsPath} missing).`,
|
|
140
|
+
);
|
|
133
141
|
}
|
|
134
142
|
|
|
135
143
|
const materializedSettings = await fs.readJson(settingsPath);
|
package/src/services/profiles.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import fs from "fs-extra";
|
|
2
|
-
import path from "node:path";
|
|
3
1
|
import os from "node:os";
|
|
4
|
-
import
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fs from "fs-extra";
|
|
4
|
+
import type { Profile, ProfileEntry, ProfilesFile } from "../types/index.js";
|
|
5
5
|
|
|
6
6
|
const PROFILES_FILE = "profiles.json";
|
|
7
7
|
|
package/src/services/resolver.ts
CHANGED
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import semver from "semver";
|
|
12
|
+
import { cliTools } from "../data/cli-tools.js";
|
|
13
|
+
import { getMarketplaceByName } from "../data/marketplaces.js";
|
|
14
|
+
import { PREDEFINED_PROFILES } from "../data/predefined-profiles.js";
|
|
12
15
|
import type {
|
|
13
16
|
BinRequirement,
|
|
14
17
|
ProfileManifest,
|
|
@@ -16,9 +19,6 @@ import type {
|
|
|
16
19
|
ResolvedBin,
|
|
17
20
|
ResolvedClosure,
|
|
18
21
|
} from "../types/index.js";
|
|
19
|
-
import { cliTools } from "../data/cli-tools.js";
|
|
20
|
-
import { getMarketplaceByName } from "../data/marketplaces.js";
|
|
21
|
-
import { PREDEFINED_PROFILES } from "../data/predefined-profiles.js";
|
|
22
22
|
import { resolvePluginBinRequirements } from "./plugin-requires.js";
|
|
23
23
|
|
|
24
24
|
export interface ResolveOptions {
|
|
@@ -237,17 +237,21 @@ function pickVersion(
|
|
|
237
237
|
* resolved deterministically to the higher pin — NOT by profile order — and
|
|
238
238
|
* collected into `conflicts` so `install`/`profile show` can surface them.
|
|
239
239
|
*/
|
|
240
|
-
function mergeClosures(
|
|
240
|
+
function mergeClosures(
|
|
241
|
+
a: ResolvedClosure,
|
|
242
|
+
b: ResolvedClosure,
|
|
243
|
+
): ResolvedClosure {
|
|
241
244
|
const conflicts = [...(a.conflicts ?? []), ...(b.conflicts ?? [])];
|
|
242
245
|
|
|
243
246
|
// Plugins: deterministic version pick per id.
|
|
244
247
|
const plugins: Record<string, string> = { ...a.plugins };
|
|
245
248
|
for (const [id, ver] of Object.entries(b.plugins)) {
|
|
246
|
-
|
|
249
|
+
const existing = plugins[id];
|
|
250
|
+
if (existing === undefined) {
|
|
247
251
|
plugins[id] = ver;
|
|
248
252
|
continue;
|
|
249
253
|
}
|
|
250
|
-
const { chosen, conflict } = pickVersion(id,
|
|
254
|
+
const { chosen, conflict } = pickVersion(id, existing, ver);
|
|
251
255
|
plugins[id] = chosen;
|
|
252
256
|
if (conflict) conflicts.push(conflict);
|
|
253
257
|
}
|
|
@@ -1,42 +1,74 @@
|
|
|
1
|
-
import * as fs from "fs";
|
|
2
|
-
import * as path from "path";
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { SettingDefinition } from "../data/settings-catalog.js";
|
|
4
|
+
import type { ClaudeSettings } from "../types/index.js";
|
|
3
5
|
import {
|
|
4
|
-
readSettings,
|
|
5
|
-
writeSettings,
|
|
6
6
|
readGlobalSettings,
|
|
7
|
+
readSettings,
|
|
7
8
|
writeGlobalSettings,
|
|
9
|
+
writeSettings,
|
|
8
10
|
} from "./claude-settings.js";
|
|
9
|
-
import type { SettingDefinition } from "../data/settings-catalog.js";
|
|
10
11
|
|
|
11
12
|
export type SettingScope = "user" | "project";
|
|
12
13
|
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
/**
|
|
15
|
+
* settings.json is an open JSON object: the catalog addresses keys that the
|
|
16
|
+
* typed `ClaudeSettings` shape does not enumerate (`attribution`, `env`, any
|
|
17
|
+
* dotted path). Read and write through this record view and narrow each value.
|
|
18
|
+
*/
|
|
19
|
+
type SettingsRecord = ClaudeSettings & Record<string, unknown>;
|
|
20
|
+
|
|
21
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
22
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The `attribution` block as Claude Code stores it. */
|
|
26
|
+
interface Attribution {
|
|
27
|
+
commit?: unknown;
|
|
28
|
+
pr?: unknown;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readAttribution(settings: SettingsRecord): Attribution | undefined {
|
|
32
|
+
const raw = settings.attribution;
|
|
33
|
+
return isRecord(raw) ? raw : undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function loadSettings(
|
|
16
37
|
scope: SettingScope,
|
|
17
38
|
projectPath?: string,
|
|
18
|
-
): Promise<
|
|
39
|
+
): Promise<SettingsRecord> {
|
|
19
40
|
const settings =
|
|
20
41
|
scope === "user"
|
|
21
42
|
? await readGlobalSettings()
|
|
22
43
|
: await readSettings(projectPath);
|
|
44
|
+
return settings as SettingsRecord;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Read the current value of a setting from the given scope */
|
|
48
|
+
export async function readSettingValue(
|
|
49
|
+
setting: SettingDefinition,
|
|
50
|
+
scope: SettingScope,
|
|
51
|
+
projectPath?: string,
|
|
52
|
+
): Promise<string | undefined> {
|
|
53
|
+
const settings = await loadSettings(scope, projectPath);
|
|
23
54
|
|
|
24
55
|
if (setting.storage.type === "attribution") {
|
|
25
56
|
// Attribution is an object: { commit: "", pr: "" } means disabled
|
|
26
|
-
const attr = (settings
|
|
57
|
+
const attr = readAttribution(settings);
|
|
27
58
|
if (attr && attr.commit === "" && attr.pr === "") {
|
|
28
59
|
return "false";
|
|
29
60
|
}
|
|
30
61
|
return undefined; // default (enabled)
|
|
31
|
-
}
|
|
62
|
+
}
|
|
63
|
+
if (setting.storage.type === "attribution-text") {
|
|
32
64
|
// Custom attribution text: read from attribution.commit, strip the Co-Authored-By trailer prefix
|
|
33
|
-
const attr = (settings
|
|
65
|
+
const attr = readAttribution(settings);
|
|
34
66
|
if (!attr || (attr.commit === "" && attr.pr === "")) {
|
|
35
67
|
// Attribution is disabled or not set — no custom text stored
|
|
36
68
|
return undefined;
|
|
37
69
|
}
|
|
38
|
-
const commit
|
|
39
|
-
if (
|
|
70
|
+
const commit = attr.commit;
|
|
71
|
+
if (typeof commit !== "string" || commit === "") {
|
|
40
72
|
return undefined;
|
|
41
73
|
}
|
|
42
74
|
// The commit value is "Co-Authored-By: Magus <magus@madappgang.com>\n\n{customText}"
|
|
@@ -48,18 +80,19 @@ export async function readSettingValue(
|
|
|
48
80
|
}
|
|
49
81
|
// If no trailer prefix, the value is the raw text (or Claude default — return undefined)
|
|
50
82
|
return undefined;
|
|
51
|
-
} else if (setting.storage.type === "env") {
|
|
52
|
-
const env = (settings as any).env as Record<string, string> | undefined;
|
|
53
|
-
return env?.[setting.storage.key];
|
|
54
|
-
} else {
|
|
55
|
-
// Direct settings key (supports dot notation like "permissions.defaultMode")
|
|
56
|
-
const keys = setting.storage.key.split(".");
|
|
57
|
-
let value: any = settings;
|
|
58
|
-
for (const k of keys) {
|
|
59
|
-
value = value?.[k];
|
|
60
|
-
}
|
|
61
|
-
return value !== undefined && value !== null ? String(value) : undefined;
|
|
62
83
|
}
|
|
84
|
+
if (setting.storage.type === "env") {
|
|
85
|
+
const env = settings.env;
|
|
86
|
+
const raw = isRecord(env) ? env[setting.storage.key] : undefined;
|
|
87
|
+
return typeof raw === "string" ? raw : undefined;
|
|
88
|
+
}
|
|
89
|
+
// Direct settings key (supports dot notation like "permissions.defaultMode")
|
|
90
|
+
const keys = setting.storage.key.split(".");
|
|
91
|
+
let value: unknown = settings;
|
|
92
|
+
for (const k of keys) {
|
|
93
|
+
value = isRecord(value) ? value[k] : undefined;
|
|
94
|
+
}
|
|
95
|
+
return value !== undefined && value !== null ? String(value) : undefined;
|
|
63
96
|
}
|
|
64
97
|
|
|
65
98
|
/** Write a setting value to the given scope */
|
|
@@ -69,57 +102,60 @@ export async function writeSettingValue(
|
|
|
69
102
|
scope: SettingScope,
|
|
70
103
|
projectPath?: string,
|
|
71
104
|
): Promise<void> {
|
|
72
|
-
const settings =
|
|
73
|
-
scope === "user"
|
|
74
|
-
? await readGlobalSettings()
|
|
75
|
-
: await readSettings(projectPath);
|
|
105
|
+
const settings = await loadSettings(scope, projectPath);
|
|
76
106
|
|
|
77
107
|
if (setting.storage.type === "attribution") {
|
|
78
108
|
// Boolean toggle: "false" -> write { commit: "", pr: "" }, "true"/undefined -> delete key
|
|
79
109
|
if (value === "false") {
|
|
80
|
-
|
|
110
|
+
settings.attribution = { commit: "", pr: "" };
|
|
81
111
|
} else {
|
|
82
|
-
|
|
112
|
+
// biome-ignore lint/performance/noDelete: the key must be absent from the serialised settings.json; `undefined` would still be listed by Object.keys and defeat the "revert to Claude defaults" contract
|
|
113
|
+
delete settings.attribution;
|
|
83
114
|
}
|
|
84
115
|
} else if (setting.storage.type === "attribution-text") {
|
|
85
116
|
if (value && value.trim().length > 0) {
|
|
86
117
|
// Write custom text: commit gets a Co-Authored-By trailer + the text; pr gets the text
|
|
87
|
-
|
|
118
|
+
settings.attribution = {
|
|
88
119
|
commit: `Co-Authored-By: Magus <magus@madappgang.com>\n\n${value}`,
|
|
89
120
|
pr: value,
|
|
90
121
|
};
|
|
91
122
|
} else {
|
|
92
123
|
// Empty value: remove attribution key entirely (revert to Claude defaults)
|
|
93
|
-
|
|
124
|
+
// biome-ignore lint/performance/noDelete: the key must be absent from the serialised settings.json, not present with an undefined value
|
|
125
|
+
delete settings.attribution;
|
|
94
126
|
}
|
|
95
127
|
} else if (setting.storage.type === "env") {
|
|
96
128
|
// Write to the "env" block in settings.json
|
|
97
|
-
|
|
129
|
+
const env = isRecord(settings.env) ? settings.env : {};
|
|
130
|
+
settings.env = env;
|
|
98
131
|
if (value === undefined || value === "" || value === setting.defaultValue) {
|
|
99
|
-
delete
|
|
132
|
+
delete env[setting.storage.key];
|
|
100
133
|
// Clean up empty env block
|
|
101
|
-
if (Object.keys(
|
|
102
|
-
|
|
134
|
+
if (Object.keys(env).length === 0) {
|
|
135
|
+
// biome-ignore lint/performance/noDelete: an empty env block is removed from the file entirely; assigning undefined would leave the key for Object.keys and JSON readers that check `in`
|
|
136
|
+
delete settings.env;
|
|
103
137
|
}
|
|
104
138
|
} else {
|
|
105
|
-
|
|
139
|
+
env[setting.storage.key] = value;
|
|
106
140
|
}
|
|
107
141
|
} else {
|
|
108
142
|
// Direct settings key (supports dot notation)
|
|
109
143
|
const keys = setting.storage.key.split(".");
|
|
110
144
|
if (keys.length === 1) {
|
|
111
145
|
if (value === undefined || value === "") {
|
|
112
|
-
delete
|
|
146
|
+
delete settings[keys[0]];
|
|
113
147
|
} else {
|
|
114
148
|
// Try to preserve type: boolean, number, or string
|
|
115
|
-
|
|
149
|
+
settings[keys[0]] = parseSettingValue(value, setting.type);
|
|
116
150
|
}
|
|
117
151
|
} else {
|
|
118
152
|
// Nested key like "permissions.defaultMode"
|
|
119
|
-
let obj:
|
|
153
|
+
let obj: Record<string, unknown> = settings;
|
|
120
154
|
for (let i = 0; i < keys.length - 1; i++) {
|
|
121
|
-
|
|
122
|
-
|
|
155
|
+
const existing = obj[keys[i]];
|
|
156
|
+
const next = isRecord(existing) ? existing : {};
|
|
157
|
+
obj[keys[i]] = next;
|
|
158
|
+
obj = next;
|
|
123
159
|
}
|
|
124
160
|
const lastKey = keys[keys.length - 1];
|
|
125
161
|
if (value === undefined || value === "") {
|
|
@@ -137,7 +173,7 @@ export async function writeSettingValue(
|
|
|
137
173
|
}
|
|
138
174
|
}
|
|
139
175
|
|
|
140
|
-
function parseSettingValue(value: string, type: string):
|
|
176
|
+
function parseSettingValue(value: string, type: string): string | boolean {
|
|
141
177
|
if (type === "boolean") {
|
|
142
178
|
return value === "true" || value === "1";
|
|
143
179
|
}
|
|
@@ -205,13 +241,13 @@ export async function discoverOutputStyles(
|
|
|
205
241
|
const enabledPlugins = new Set<string>();
|
|
206
242
|
try {
|
|
207
243
|
const userSettings = await readGlobalSettings();
|
|
208
|
-
for (const [k, v] of Object.entries(
|
|
244
|
+
for (const [k, v] of Object.entries(userSettings.enabledPlugins ?? {})) {
|
|
209
245
|
if (v) enabledPlugins.add(k);
|
|
210
246
|
}
|
|
211
247
|
} catch {}
|
|
212
248
|
try {
|
|
213
249
|
const projSettings = await readSettings(projectPath);
|
|
214
|
-
for (const [k, v] of Object.entries(
|
|
250
|
+
for (const [k, v] of Object.entries(projSettings.enabledPlugins ?? {})) {
|
|
215
251
|
if (v) enabledPlugins.add(k);
|
|
216
252
|
}
|
|
217
253
|
} catch {}
|
|
@@ -751,10 +751,14 @@ export async function getInstalledSkillNames(
|
|
|
751
751
|
|
|
752
752
|
// ─── Firebase Skills API ──────────────────────────────────────────────────────
|
|
753
753
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
754
|
+
function mapApiSkillToSkillInfo(entry: unknown): SkillInfo {
|
|
755
|
+
const raw: Record<string, unknown> =
|
|
756
|
+
typeof entry === "object" && entry !== null
|
|
757
|
+
? (entry as Record<string, unknown>)
|
|
758
|
+
: {};
|
|
759
|
+
const str = (v: unknown): string => (typeof v === "string" ? v : "");
|
|
760
|
+
const repo = str(raw.repo) || "unknown";
|
|
761
|
+
const skillPath = str(raw.skillPath) || str(raw.name) || "";
|
|
758
762
|
const source: SkillSource = {
|
|
759
763
|
label: repo,
|
|
760
764
|
repo,
|
|
@@ -763,8 +767,8 @@ function mapApiSkillToSkillInfo(raw: any): SkillInfo {
|
|
|
763
767
|
const stars = typeof raw.stars === "number" ? raw.stars : undefined;
|
|
764
768
|
return {
|
|
765
769
|
id: `${repo}/${skillPath}`,
|
|
766
|
-
name: (raw.name
|
|
767
|
-
description: (raw.description
|
|
770
|
+
name: str(raw.name) || skillPath,
|
|
771
|
+
description: str(raw.description),
|
|
768
772
|
source,
|
|
769
773
|
repoPath: skillPath ? `${skillPath}/SKILL.md` : "SKILL.md",
|
|
770
774
|
gitBlobSha: "",
|
|
@@ -943,9 +947,7 @@ export async function installSkill(
|
|
|
943
947
|
content = await response.text();
|
|
944
948
|
break;
|
|
945
949
|
}
|
|
946
|
-
} catch {
|
|
947
|
-
continue;
|
|
948
|
-
}
|
|
950
|
+
} catch {}
|
|
949
951
|
}
|
|
950
952
|
|
|
951
953
|
if (!content) {
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
* survive switches untouched.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import fs from "fs-extra";
|
|
18
17
|
import path from "node:path";
|
|
18
|
+
import fs from "fs-extra";
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Paths that must be gitignored once a project uses profiles: the generated
|
|
@@ -113,10 +113,7 @@ async function isProfileLink(
|
|
|
113
113
|
try {
|
|
114
114
|
const stat = await fs.lstat(link);
|
|
115
115
|
if (!stat.isSymbolicLink()) return false;
|
|
116
|
-
const resolved = path.resolve(
|
|
117
|
-
path.dirname(link),
|
|
118
|
-
await fs.readlink(link),
|
|
119
|
-
);
|
|
116
|
+
const resolved = path.resolve(path.dirname(link), await fs.readlink(link));
|
|
120
117
|
return resolved.startsWith(profilesRoot(projectPath) + path.sep);
|
|
121
118
|
} catch {
|
|
122
119
|
return false;
|
|
@@ -127,9 +127,9 @@ let pythonCliMemo: PythonCliInstaller | undefined;
|
|
|
127
127
|
*/
|
|
128
128
|
export function resolvePythonCliInstaller(): PythonCliInstaller {
|
|
129
129
|
if (pythonCliMemo) return pythonCliMemo;
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
130
|
+
const found = PYTHON_CLI_INSTALLERS.find((i) => whichSync(i.probe) !== null);
|
|
131
|
+
const fallback = PYTHON_CLI_INSTALLERS[PYTHON_CLI_INSTALLERS.length - 1];
|
|
132
|
+
pythonCliMemo = found ?? fallback;
|
|
133
133
|
return pythonCliMemo;
|
|
134
134
|
}
|
|
135
135
|
|
|
@@ -30,6 +30,10 @@
|
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
32
|
import path from "node:path";
|
|
33
|
+
import type { ProfileSkillRef, SkillInfo } from "../types/index.js";
|
|
34
|
+
import type { Command } from "../utils/command-utils.js";
|
|
35
|
+
import { formatCommand } from "../utils/command-utils.js";
|
|
36
|
+
import { runCommand } from "../utils/run.js";
|
|
33
37
|
import type { PluginScope } from "./claude-cli.js";
|
|
34
38
|
import { installPlugin, repairPlugin, updatePlugin } from "./claude-cli.js";
|
|
35
39
|
import {
|
|
@@ -39,10 +43,6 @@ import {
|
|
|
39
43
|
import { installSkill } from "./skills-manager.js";
|
|
40
44
|
import { binInstallCommand, binUpgradeCommand } from "./toolchain.js";
|
|
41
45
|
import type { PluginUpdateItem, UpdatePlan } from "./update-plan.js";
|
|
42
|
-
import type { ProfileSkillRef, SkillInfo } from "../types/index.js";
|
|
43
|
-
import type { Command } from "../utils/command-utils.js";
|
|
44
|
-
import { formatCommand } from "../utils/command-utils.js";
|
|
45
|
-
import { runCommand } from "../utils/run.js";
|
|
46
46
|
|
|
47
47
|
// -- results -----------------------------------------------------------------
|
|
48
48
|
|