claudeup 6.2.1 → 6.3.2
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 +40 -15
- 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 +181 -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 +8 -9
- 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
|
@@ -16,17 +16,6 @@ import {
|
|
|
16
16
|
setMcpEnvVar,
|
|
17
17
|
} from "../../services/claude-settings.js";
|
|
18
18
|
import { clearContentDriftCache } from "../../services/content-drift.js";
|
|
19
|
-
// The one update implementation, shared with `claudeup update`. See the header
|
|
20
|
-
// of update-engine.ts for what drifted while the TUI had its own loop.
|
|
21
|
-
import {
|
|
22
|
-
type ApplyReporter,
|
|
23
|
-
REAL_PLUGIN_DEPS,
|
|
24
|
-
applyPlugins,
|
|
25
|
-
pluginNeedsWork,
|
|
26
|
-
installPluginInScope,
|
|
27
|
-
updatePluginInScope,
|
|
28
|
-
} from "../../services/update-engine.js";
|
|
29
|
-
import { planPluginUpdates } from "../../services/update-plan.js";
|
|
30
19
|
import {
|
|
31
20
|
type MarketplaceFetchFailure,
|
|
32
21
|
type PluginInfo,
|
|
@@ -58,6 +47,17 @@ import {
|
|
|
58
47
|
fetchPluginContents,
|
|
59
48
|
fetchPluginSkillDetail,
|
|
60
49
|
} from "../../services/skills-manager.js";
|
|
50
|
+
// The one update implementation, shared with `claudeup update`. See the header
|
|
51
|
+
// of update-engine.ts for what drifted while the TUI had its own loop.
|
|
52
|
+
import {
|
|
53
|
+
type ApplyReporter,
|
|
54
|
+
REAL_PLUGIN_DEPS,
|
|
55
|
+
applyPlugins,
|
|
56
|
+
installPluginInScope,
|
|
57
|
+
pluginNeedsWork,
|
|
58
|
+
updatePluginInScope,
|
|
59
|
+
} from "../../services/update-engine.js";
|
|
60
|
+
import { planPluginUpdates } from "../../services/update-plan.js";
|
|
61
61
|
import {
|
|
62
62
|
diffVersions,
|
|
63
63
|
loadSeenVersions,
|
|
@@ -184,6 +184,7 @@ export function PluginsScreen() {
|
|
|
184
184
|
}, [dispatch, state.projectPath]);
|
|
185
185
|
|
|
186
186
|
// Load data on mount or when dataRefreshVersion changes
|
|
187
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: dataRefreshVersion is the external refresh trigger; bumping it must re-run the fetch even though the body never reads it
|
|
187
188
|
useEffect(() => {
|
|
188
189
|
fetchData();
|
|
189
190
|
}, [fetchData, state.dataRefreshVersion]);
|
|
@@ -725,7 +726,7 @@ export function PluginsScreen() {
|
|
|
725
726
|
if (value === null) {
|
|
726
727
|
await modal.message(
|
|
727
728
|
"Configuration Incomplete",
|
|
728
|
-
|
|
729
|
+
"Skipped remaining configuration.\nYou can configure these later in Environment Variables (press 4).",
|
|
729
730
|
"info",
|
|
730
731
|
);
|
|
731
732
|
return true;
|
|
@@ -821,9 +822,7 @@ export function PluginsScreen() {
|
|
|
821
822
|
} else {
|
|
822
823
|
await modal.message(
|
|
823
824
|
`Remove ${mp.displayName}?`,
|
|
824
|
-
`To remove this marketplace, run in Claude Code:\n\n
|
|
825
|
-
` /plugin marketplace remove ${mp.name}\n\n` +
|
|
826
|
-
`After removing, refresh claudeup with 'r' to update the display.`,
|
|
825
|
+
`To remove this marketplace, run in Claude Code:\n\n /plugin marketplace remove ${mp.name}\n\nAfter removing, refresh claudeup with 'r' to update the display.`,
|
|
827
826
|
"info",
|
|
828
827
|
);
|
|
829
828
|
}
|
|
@@ -835,8 +834,7 @@ export function PluginsScreen() {
|
|
|
835
834
|
const repo = mp.source.repo || mp.name;
|
|
836
835
|
const wantAdd = await modal.confirm(
|
|
837
836
|
`Add ${mp.displayName}?`,
|
|
838
|
-
`Clone ${repo} and register it as a marketplace?\n\
|
|
839
|
-
`Auto-update is enabled by default.`,
|
|
837
|
+
`Clone ${repo} and register it as a marketplace?\n\nAuto-update is enabled by default.`,
|
|
840
838
|
);
|
|
841
839
|
if (wantAdd) {
|
|
842
840
|
try {
|
|
@@ -845,9 +843,7 @@ export function PluginsScreen() {
|
|
|
845
843
|
} catch (e) {
|
|
846
844
|
await modal.message(
|
|
847
845
|
"Could not add marketplace",
|
|
848
|
-
`${(e as Error).message}\n\n
|
|
849
|
-
`Run it yourself if this persists:\n\n` +
|
|
850
|
-
` claude plugin marketplace add ${repo}`,
|
|
846
|
+
`${(e as Error).message}\n\nRun it yourself if this persists:\n\n claude plugin marketplace add ${repo}`,
|
|
851
847
|
"error",
|
|
852
848
|
);
|
|
853
849
|
}
|
|
@@ -897,7 +893,7 @@ export function PluginsScreen() {
|
|
|
897
893
|
|
|
898
894
|
const scopeValue = await modal.select(
|
|
899
895
|
plugin.name,
|
|
900
|
-
|
|
896
|
+
"Select scope to toggle:",
|
|
901
897
|
scopeOptions,
|
|
902
898
|
);
|
|
903
899
|
if (scopeValue === null) return;
|
|
@@ -1,34 +1,34 @@
|
|
|
1
1
|
import React, { useEffect, useCallback } from "react";
|
|
2
|
+
import { ScreenLayout } from "../components/layout/index.js";
|
|
3
|
+
import { useKeyboard } from "../hooks/useKeyboard.js";
|
|
2
4
|
import { useApp, useModal } from "../state/AppContext.js";
|
|
3
5
|
import { useDimensions } from "../state/DimensionsContext.js";
|
|
4
|
-
import { useKeyboard } from "../hooks/useKeyboard.js";
|
|
5
|
-
import { ScreenLayout } from "../components/layout/index.js";
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
|
-
|
|
8
|
+
PREDEFINED_PROFILES,
|
|
9
|
+
type PredefinedProfile,
|
|
10
|
+
} from "../../data/predefined-profiles.js";
|
|
11
|
+
import { readSettings, writeSettings } from "../../services/claude-settings.js";
|
|
12
|
+
import {
|
|
9
13
|
applyProfile,
|
|
10
|
-
renameProfile,
|
|
11
14
|
deleteProfile,
|
|
12
15
|
exportProfileToJson,
|
|
13
16
|
importProfileFromJson,
|
|
17
|
+
listProfiles,
|
|
18
|
+
renameProfile,
|
|
14
19
|
} from "../../services/profiles.js";
|
|
15
|
-
import { readSettings, writeSettings } from "../../services/claude-settings.js";
|
|
16
20
|
import {
|
|
17
|
-
writeClipboard,
|
|
18
|
-
readClipboard,
|
|
19
21
|
ClipboardUnavailableError,
|
|
22
|
+
readClipboard,
|
|
23
|
+
writeClipboard,
|
|
20
24
|
} from "../../utils/clipboard.js";
|
|
25
|
+
import { ScrollableList } from "../components/ScrollableList.js";
|
|
21
26
|
import {
|
|
22
|
-
|
|
23
|
-
type PredefinedProfile,
|
|
24
|
-
} from "../../data/predefined-profiles.js";
|
|
25
|
-
import {
|
|
27
|
+
type ProfileListItem,
|
|
26
28
|
buildProfileListItems,
|
|
27
|
-
renderProfileRow,
|
|
28
29
|
renderProfileDetail,
|
|
29
|
-
|
|
30
|
+
renderProfileRow,
|
|
30
31
|
} from "../renderers/profileRenderers.js";
|
|
31
|
-
import { ScrollableList } from "../components/ScrollableList.js";
|
|
32
32
|
import { theme } from "../theme.js";
|
|
33
33
|
|
|
34
34
|
export function ProfilesScreen() {
|
|
@@ -50,6 +50,7 @@ export function ProfilesScreen() {
|
|
|
50
50
|
}
|
|
51
51
|
}, [dispatch, state.projectPath]);
|
|
52
52
|
|
|
53
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: dataRefreshVersion is the external refresh trigger; bumping it must re-run the fetch even though the body never reads it
|
|
53
54
|
useEffect(() => {
|
|
54
55
|
fetchData();
|
|
55
56
|
}, [fetchData, state.dataRefreshVersion]);
|
|
@@ -63,28 +64,29 @@ export function ProfilesScreen() {
|
|
|
63
64
|
const selectedItem: ProfileListItem | undefined =
|
|
64
65
|
allItems[profilesState.selectedIndex];
|
|
65
66
|
|
|
66
|
-
const isNavigable = (item: ProfileListItem) =>
|
|
67
|
+
const isNavigable = (item: ProfileListItem | undefined) =>
|
|
68
|
+
item !== undefined && item.kind !== "header";
|
|
67
69
|
|
|
68
70
|
useKeyboard((event) => {
|
|
69
71
|
if (state.isSearching || state.modal) return;
|
|
70
72
|
|
|
71
73
|
if (event.name === "up" || event.name === "k") {
|
|
72
74
|
let newIndex = profilesState.selectedIndex - 1;
|
|
73
|
-
while (newIndex > 0 && !isNavigable(allItems[newIndex]
|
|
75
|
+
while (newIndex > 0 && !isNavigable(allItems[newIndex])) {
|
|
74
76
|
newIndex--;
|
|
75
77
|
}
|
|
76
|
-
if (newIndex >= 0 && isNavigable(allItems[newIndex]
|
|
78
|
+
if (newIndex >= 0 && isNavigable(allItems[newIndex])) {
|
|
77
79
|
dispatch({ type: "PROFILES_SELECT", index: newIndex });
|
|
78
80
|
}
|
|
79
81
|
} else if (event.name === "down" || event.name === "j") {
|
|
80
82
|
let newIndex = profilesState.selectedIndex + 1;
|
|
81
83
|
while (
|
|
82
84
|
newIndex < allItems.length - 1 &&
|
|
83
|
-
!isNavigable(allItems[newIndex]
|
|
85
|
+
!isNavigable(allItems[newIndex])
|
|
84
86
|
) {
|
|
85
87
|
newIndex++;
|
|
86
88
|
}
|
|
87
|
-
if (newIndex < allItems.length && isNavigable(allItems[newIndex]
|
|
89
|
+
if (newIndex < allItems.length && isNavigable(allItems[newIndex])) {
|
|
88
90
|
dispatch({ type: "PROFILES_SELECT", index: newIndex });
|
|
89
91
|
}
|
|
90
92
|
} else if (event.name === "enter" || event.name === "a") {
|
|
@@ -133,13 +135,13 @@ export function ProfilesScreen() {
|
|
|
133
135
|
const envMap = value as Record<string, string>;
|
|
134
136
|
const existing = settings as Record<string, unknown>;
|
|
135
137
|
const existingEnv =
|
|
136
|
-
(existing
|
|
138
|
+
(existing.env as Record<string, string> | undefined) ?? {};
|
|
137
139
|
for (const [envKey, envVal] of Object.entries(envMap)) {
|
|
138
140
|
if (!existingEnv[envKey]) {
|
|
139
141
|
existingEnv[envKey] = envVal;
|
|
140
142
|
}
|
|
141
143
|
}
|
|
142
|
-
(settings as Record<string, unknown>)
|
|
144
|
+
(settings as Record<string, unknown>).env = existingEnv;
|
|
143
145
|
} else {
|
|
144
146
|
const settingsMap = settings as Record<string, unknown>;
|
|
145
147
|
if (settingsMap[key] === undefined) {
|
|
@@ -304,7 +306,7 @@ export function ProfilesScreen() {
|
|
|
304
306
|
await writeClipboard(json);
|
|
305
307
|
await modal.message(
|
|
306
308
|
"Copied",
|
|
307
|
-
|
|
309
|
+
"Profile JSON copied to clipboard.\nShare it with teammates to import.",
|
|
308
310
|
"success",
|
|
309
311
|
);
|
|
310
312
|
} catch (err) {
|
|
@@ -397,8 +399,7 @@ export function ProfilesScreen() {
|
|
|
397
399
|
? firstNavigableIndex
|
|
398
400
|
: profilesState.selectedIndex;
|
|
399
401
|
|
|
400
|
-
const loadingStatus =
|
|
401
|
-
profilesState.profiles.status === "loading" ? true : false;
|
|
402
|
+
const loadingStatus = profilesState.profiles.status === "loading";
|
|
402
403
|
const errorMessage =
|
|
403
404
|
profilesState.profiles.status === "error"
|
|
404
405
|
? profilesState.profiles.error.message
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fs from "fs-extra";
|
|
1
4
|
import React, {
|
|
2
5
|
useEffect,
|
|
3
6
|
useCallback,
|
|
@@ -5,15 +8,16 @@ import React, {
|
|
|
5
8
|
useState,
|
|
6
9
|
useRef,
|
|
7
10
|
} from "react";
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import fs from "fs-extra";
|
|
11
|
+
import { ScreenLayout } from "../components/layout/index.js";
|
|
12
|
+
import { useKeyboard } from "../hooks/useKeyboard.js";
|
|
11
13
|
import { useApp, useModal } from "../state/AppContext.js";
|
|
12
14
|
import { useDimensions } from "../state/DimensionsContext.js";
|
|
13
|
-
import { useKeyboard } from "../hooks/useKeyboard.js";
|
|
14
|
-
import { ScreenLayout } from "../components/layout/index.js";
|
|
15
15
|
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
DEFAULT_SKILL_REPOS,
|
|
18
|
+
type RecommendedSkill,
|
|
19
|
+
classifyStarReliability,
|
|
20
|
+
} from "../../data/skill-repos.js";
|
|
17
21
|
import {
|
|
18
22
|
fetchAvailableSkills,
|
|
19
23
|
fetchSkillFrontmatter,
|
|
@@ -25,11 +29,6 @@ import {
|
|
|
25
29
|
fetchCuratedSkillsCatalog,
|
|
26
30
|
searchSkills,
|
|
27
31
|
} from "../../services/skillsmp-client.js";
|
|
28
|
-
import {
|
|
29
|
-
DEFAULT_SKILL_REPOS,
|
|
30
|
-
classifyStarReliability,
|
|
31
|
-
type RecommendedSkill,
|
|
32
|
-
} from "../../data/skill-repos.js";
|
|
33
32
|
import type {
|
|
34
33
|
SkillInfo,
|
|
35
34
|
SkillSetInfo,
|
|
@@ -37,11 +36,12 @@ import type {
|
|
|
37
36
|
} from "../../types/index.js";
|
|
38
37
|
import { buildSkillBrowserItems } from "../adapters/skillsAdapter.js";
|
|
39
38
|
import type { SkillBrowserItem } from "../adapters/skillsAdapter.js";
|
|
39
|
+
import { EmptyFilterState } from "../components/EmptyFilterState.js";
|
|
40
|
+
import { ScrollableList } from "../components/ScrollableList.js";
|
|
40
41
|
import {
|
|
41
|
-
renderSkillRow,
|
|
42
42
|
renderSkillDetail,
|
|
43
|
+
renderSkillRow,
|
|
43
44
|
} from "../renderers/skillRenderers.js";
|
|
44
|
-
import { ScrollableList } from "../components/ScrollableList.js";
|
|
45
45
|
import { theme } from "../theme.js";
|
|
46
46
|
|
|
47
47
|
export function SkillsScreen() {
|
|
@@ -95,6 +95,7 @@ export function SkillsScreen() {
|
|
|
95
95
|
}
|
|
96
96
|
}, [dispatch, state.projectPath]);
|
|
97
97
|
|
|
98
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: dataRefreshVersion is the external refresh trigger; bumping it must re-run the fetch even though the body never reads it
|
|
98
99
|
useEffect(() => {
|
|
99
100
|
fetchData();
|
|
100
101
|
}, [fetchData, state.dataRefreshVersion]);
|
|
@@ -208,6 +209,7 @@ export function SkillsScreen() {
|
|
|
208
209
|
setInstalledFromDisk({ user, project });
|
|
209
210
|
}, [state.projectPath]);
|
|
210
211
|
|
|
212
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: dataRefreshVersion is the external refresh trigger; bumping it must re-scan the disk even though the body never reads it
|
|
211
213
|
useEffect(() => {
|
|
212
214
|
scanDisk();
|
|
213
215
|
}, [scanDisk, state.dataRefreshVersion]);
|
|
@@ -520,7 +522,7 @@ export function SkillsScreen() {
|
|
|
520
522
|
}
|
|
521
523
|
})
|
|
522
524
|
.catch(() => {});
|
|
523
|
-
}, [selectedSkill
|
|
525
|
+
}, [selectedSkill, isSkillSetChild, dispatch]);
|
|
524
526
|
|
|
525
527
|
// ── Action handlers ───────────────────────────────────────────────────────
|
|
526
528
|
|
|
@@ -1,16 +1,12 @@
|
|
|
1
|
-
import React
|
|
2
|
-
|
|
3
|
-
useContext,
|
|
4
|
-
useReducer,
|
|
5
|
-
type ReactNode,
|
|
6
|
-
} from "react";
|
|
1
|
+
import type React from "react";
|
|
2
|
+
import { type ReactNode, createContext, useContext, useReducer } from "react";
|
|
7
3
|
import { appReducer, initialState } from "./reducer.js";
|
|
8
4
|
import type {
|
|
9
|
-
AppState,
|
|
10
5
|
AppAction,
|
|
11
|
-
|
|
6
|
+
AppState,
|
|
12
7
|
ModalState,
|
|
13
8
|
ProgressState,
|
|
9
|
+
Route,
|
|
14
10
|
} from "./types.js";
|
|
15
11
|
|
|
16
12
|
// ============================================================================
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import { useOnResize, useRenderer } from "@opentui/react";
|
|
2
|
+
import type React from "react";
|
|
3
|
+
import { createContext, useContext, useEffect, useState } from "react";
|
|
3
4
|
|
|
4
5
|
interface Dimensions {
|
|
5
6
|
/** Terminal width */
|
package/src/ui/theme-mode.ts
CHANGED
|
@@ -13,26 +13,27 @@ import type { ThemeMode } from "@opentui/core";
|
|
|
13
13
|
* expressed as "the page, 8% toward the foreground". OpenTUI's alpha blending
|
|
14
14
|
* lives only in its 3d module, not the text renderer.
|
|
15
15
|
*
|
|
16
|
-
* So we
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
16
|
+
* So we resolve, once at startup, through `theme-resolve.ts`: flag, then
|
|
17
|
+
* `CLAUDEUP_THEME`, then `TERM_THEME`, then the bounded OSC probe, then
|
|
18
|
+
* `COLORFGBG`, then dark. **There is no unknown.** Resolution always yields a
|
|
19
|
+
* mode; when nothing answered it is dark, because that is the user's stated
|
|
20
|
+
* default, not because dark was detected.
|
|
20
21
|
*/
|
|
21
22
|
|
|
22
|
-
let detected: ThemeMode
|
|
23
|
+
let detected: ThemeMode = "dark";
|
|
23
24
|
|
|
24
|
-
/** Set once at startup from `
|
|
25
|
-
export function setThemeMode(mode: ThemeMode
|
|
25
|
+
/** Set once at startup from `resolveTheme()`. */
|
|
26
|
+
export function setThemeMode(mode: ThemeMode): void {
|
|
26
27
|
detected = mode;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
export function getThemeMode(): ThemeMode
|
|
30
|
+
export function getThemeMode(): ThemeMode {
|
|
30
31
|
return detected;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
/** Test seam —
|
|
34
|
+
/** Test seam — return to the pre-resolution default between cases. */
|
|
34
35
|
export function resetThemeMode(): void {
|
|
35
|
-
detected =
|
|
36
|
+
detected = "dark";
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
/**
|
|
@@ -46,8 +47,8 @@ export function resetThemeMode(): void {
|
|
|
46
47
|
*
|
|
47
48
|
* An unlit segment should recede toward the page, and "toward the page" is the
|
|
48
49
|
* one thing a fixed colour cannot express: a pale fill that sits quietly on cream
|
|
49
|
-
* is a bright block on near-black. Hence the two values, chosen per
|
|
50
|
-
*
|
|
50
|
+
* is a bright block on near-black. Hence the two values, chosen per resolved
|
|
51
|
+
* mode — this is the only place claudeup asks.
|
|
51
52
|
*/
|
|
52
53
|
export const SCOPE_OFF_FILL = {
|
|
53
54
|
/** Just off a light page — a filled but quiet segment. */
|
|
@@ -56,20 +57,10 @@ export const SCOPE_OFF_FILL = {
|
|
|
56
57
|
dark: "#3A3A40",
|
|
57
58
|
} as const;
|
|
58
59
|
|
|
59
|
-
/**
|
|
60
|
-
* Neutral fallback when the terminal never answered the OSC query. A mid grey is
|
|
61
|
-
* visible on both pages — not as quiet as the tuned values, but never invisible
|
|
62
|
-
* and never a bright block. Failing to a wrong extreme would be worse than
|
|
63
|
-
* failing to something merely unremarkable.
|
|
64
|
-
*/
|
|
65
|
-
export const SCOPE_OFF_FALLBACK = "#9CA3AF";
|
|
66
|
-
|
|
67
60
|
/** Fill colour for an unlit scope segment. Always a colour — never undefined,
|
|
68
61
|
* because the segment is a filled block whatever the terminal turns out to be. */
|
|
69
62
|
export function scopeOffFill(): string {
|
|
70
|
-
|
|
71
|
-
if (detected === "dark") return SCOPE_OFF_FILL.dark;
|
|
72
|
-
return SCOPE_OFF_FALLBACK;
|
|
63
|
+
return SCOPE_OFF_FILL[detected];
|
|
73
64
|
}
|
|
74
65
|
|
|
75
66
|
/**
|
|
@@ -122,19 +113,10 @@ export const CONTRAST_REFERENCE_KEYS = {
|
|
|
122
113
|
dark: "#1C1C1E",
|
|
123
114
|
} as const;
|
|
124
115
|
|
|
125
|
-
/**
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
* rather than a guess, and here the wrong guess paints a wash of the wrong end
|
|
132
|
-
* behind every row in the list.
|
|
133
|
-
*/
|
|
134
|
-
export function componentBadge(
|
|
135
|
-
kind: ComponentBadgeKind,
|
|
136
|
-
): { bg: string; fg: string } | null {
|
|
137
|
-
if (detected === "light") return COMPONENT_BADGE[kind].light;
|
|
138
|
-
if (detected === "dark") return COMPONENT_BADGE[kind].dark;
|
|
139
|
-
return null;
|
|
116
|
+
/** Chip colours for one component kind, for the resolved mode. */
|
|
117
|
+
export function componentBadge(kind: ComponentBadgeKind): {
|
|
118
|
+
bg: string;
|
|
119
|
+
fg: string;
|
|
120
|
+
} {
|
|
121
|
+
return COMPONENT_BADGE[kind][detected];
|
|
140
122
|
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { ThemeMode } from "@opentui/core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Terminal theme resolution — the one order claudeup follows, decided once at
|
|
5
|
+
* process start, first answer wins:
|
|
6
|
+
*
|
|
7
|
+
* 1. `--theme` flag
|
|
8
|
+
* 2. `CLAUDEUP_THEME`
|
|
9
|
+
* 3. `TERM_THEME`
|
|
10
|
+
* 4. OSC 10/11 probe (bounded, interactive tty only)
|
|
11
|
+
* 5. `COLORFGBG`
|
|
12
|
+
* 6. dark
|
|
13
|
+
*
|
|
14
|
+
* A step with no answer says nothing and the next one runs; nothing before
|
|
15
|
+
* step 6 ever asserts dark. Every theme word is compared exactly — the byte
|
|
16
|
+
* strings `light` and `dark`, lowercase, untrimmed — because the same test has
|
|
17
|
+
* to run in a POSIX `case` and a tmux format, and only the exact comparison is
|
|
18
|
+
* free in all three.
|
|
19
|
+
*
|
|
20
|
+
* This module is pure by design: no `process` access except as a *default
|
|
21
|
+
* argument* to `snapshotThemeEnv`, no I/O, no renderer. Everything it needs is
|
|
22
|
+
* passed in, so the whole contract runs in a test without a terminal.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export type ThemeSource =
|
|
26
|
+
| "flag"
|
|
27
|
+
| "variable"
|
|
28
|
+
| "TERM_THEME"
|
|
29
|
+
| "osc11"
|
|
30
|
+
| "COLORFGBG"
|
|
31
|
+
| "default";
|
|
32
|
+
|
|
33
|
+
export interface ThemeResolution {
|
|
34
|
+
/** Never null — when nothing answered, it is dark by decision. */
|
|
35
|
+
mode: ThemeMode;
|
|
36
|
+
source: ThemeSource;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The three variables the resolver reads, captured BEFORE .env is merged. */
|
|
40
|
+
export interface ThemeEnv {
|
|
41
|
+
TERM_THEME?: string;
|
|
42
|
+
CLAUDEUP_THEME?: string;
|
|
43
|
+
COLORFGBG?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ResolveThemeInput {
|
|
47
|
+
/** Already validated by the router: only "light" | "dark" | undefined arrive here. */
|
|
48
|
+
flag?: ThemeMode;
|
|
49
|
+
env: ThemeEnv;
|
|
50
|
+
/** stdin AND stdout are TTYs. Decided by the caller; the resolver never touches process. */
|
|
51
|
+
isInteractive: boolean;
|
|
52
|
+
/** Bounded OSC 10/11 probe. Called at most once, and only when step 4 is reached. */
|
|
53
|
+
probe: () => Promise<ThemeMode | null>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Exact match on "light" | "dark"; everything else is no opinion. */
|
|
57
|
+
export function parseThemeWord(value: string | undefined): ThemeMode | null {
|
|
58
|
+
if (value === "light") return "light";
|
|
59
|
+
if (value === "dark") return "dark";
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The literal background fields that answer — the same set as the statusline's `case`. */
|
|
64
|
+
const LIGHT_BACKGROUNDS: ReadonlySet<string> = new Set(["7", "15"]);
|
|
65
|
+
const DARK_BACKGROUNDS: ReadonlySet<string> = new Set([
|
|
66
|
+
"0",
|
|
67
|
+
"1",
|
|
68
|
+
"2",
|
|
69
|
+
"3",
|
|
70
|
+
"4",
|
|
71
|
+
"5",
|
|
72
|
+
"6",
|
|
73
|
+
"8",
|
|
74
|
+
"9",
|
|
75
|
+
"10",
|
|
76
|
+
"11",
|
|
77
|
+
"12",
|
|
78
|
+
"13",
|
|
79
|
+
"14",
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Last ";"-field is the background: 7|15 → light, 0-6|8-14 → dark, else null.
|
|
84
|
+
*
|
|
85
|
+
* Exact string membership, not numeric parsing: the statusline's
|
|
86
|
+
* `parse_colorfgbg` is a POSIX `case` over the literal words, so `07`, `015`
|
|
87
|
+
* and `00` are no opinion there, and that literal set is the contract the two
|
|
88
|
+
* tools share. `Number("07")` would make claudeup say light where the
|
|
89
|
+
* statusline says nothing.
|
|
90
|
+
*
|
|
91
|
+
* A value without a `;` is not `<fg>;<bg>` at all — a lone `7` is no opinion,
|
|
92
|
+
* matching the statusline's `case "$1" in *\;*)` guard so the two tools agree.
|
|
93
|
+
*/
|
|
94
|
+
export function parseColorFgBg(value: string | undefined): ThemeMode | null {
|
|
95
|
+
if (value === undefined || !value.includes(";")) return null;
|
|
96
|
+
const bg = value.slice(value.lastIndexOf(";") + 1);
|
|
97
|
+
if (LIGHT_BACKGROUNDS.has(bg)) return "light";
|
|
98
|
+
if (DARK_BACKGROUNDS.has(bg)) return "dark";
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Copy the three variables out of `env` — call this before loadProjectDotenv.
|
|
104
|
+
*
|
|
105
|
+
* Copies, never references: the returned object must not observe later writes
|
|
106
|
+
* to `process.env`, which is exactly what a merged `.env` would be.
|
|
107
|
+
*/
|
|
108
|
+
export function snapshotThemeEnv(
|
|
109
|
+
env: Readonly<Record<string, string | undefined>> = process.env,
|
|
110
|
+
): ThemeEnv {
|
|
111
|
+
const snapshot: ThemeEnv = {};
|
|
112
|
+
if (env.TERM_THEME !== undefined) snapshot.TERM_THEME = env.TERM_THEME;
|
|
113
|
+
if (env.CLAUDEUP_THEME !== undefined)
|
|
114
|
+
snapshot.CLAUDEUP_THEME = env.CLAUDEUP_THEME;
|
|
115
|
+
if (env.COLORFGBG !== undefined) snapshot.COLORFGBG = env.COLORFGBG;
|
|
116
|
+
return snapshot;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function resolveTheme(
|
|
120
|
+
input: ResolveThemeInput,
|
|
121
|
+
): Promise<ThemeResolution> {
|
|
122
|
+
if (input.flag !== undefined) {
|
|
123
|
+
return { mode: input.flag, source: "flag" };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const variable = parseThemeWord(input.env.CLAUDEUP_THEME);
|
|
127
|
+
if (variable !== null) return { mode: variable, source: "variable" };
|
|
128
|
+
|
|
129
|
+
const termTheme = parseThemeWord(input.env.TERM_THEME);
|
|
130
|
+
if (termTheme !== null) return { mode: termTheme, source: "TERM_THEME" };
|
|
131
|
+
|
|
132
|
+
// Steps 1-3 answered nothing, so this is the only path that probes. Not
|
|
133
|
+
// reaching here is what keeps first paint undelayed when a word answered.
|
|
134
|
+
if (input.isInteractive) {
|
|
135
|
+
let probed: ThemeMode | null = null;
|
|
136
|
+
try {
|
|
137
|
+
probed = await input.probe();
|
|
138
|
+
} catch {
|
|
139
|
+
probed = null;
|
|
140
|
+
}
|
|
141
|
+
if (probed !== null) return { mode: probed, source: "osc11" };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const colorFgBg = parseColorFgBg(input.env.COLORFGBG);
|
|
145
|
+
if (colorFgBg !== null) return { mode: colorFgBg, source: "COLORFGBG" };
|
|
146
|
+
|
|
147
|
+
return { mode: "dark", source: "default" };
|
|
148
|
+
}
|
package/src/utils/clipboard.ts
CHANGED
|
@@ -27,13 +27,13 @@ export async function writeClipboard(text: string): Promise<void> {
|
|
|
27
27
|
export async function readClipboard(): Promise<string> {
|
|
28
28
|
if (process.platform === "darwin") {
|
|
29
29
|
return execSync("pbpaste").toString();
|
|
30
|
-
}
|
|
30
|
+
}
|
|
31
|
+
if (process.platform === "linux") {
|
|
31
32
|
return execSync("xclip -selection clipboard -o").toString();
|
|
32
|
-
} else {
|
|
33
|
-
throw new ClipboardUnavailableError(
|
|
34
|
-
`Clipboard not supported on platform: ${process.platform}`,
|
|
35
|
-
);
|
|
36
33
|
}
|
|
34
|
+
throw new ClipboardUnavailableError(
|
|
35
|
+
`Clipboard not supported on platform: ${process.platform}`,
|
|
36
|
+
);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
export class ClipboardUnavailableError extends Error {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
-
import {
|
|
2
|
+
import { constants, accessSync, statSync } from "node:fs";
|
|
3
3
|
import { access, realpath } from "node:fs/promises";
|
|
4
|
-
import { accessSync, constants, statSync } from "node:fs";
|
|
5
4
|
import { join } from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
6
|
|
|
7
7
|
const execFileAsync = promisify(execFile);
|
|
8
8
|
|
package/src/utils/config-dir.ts
CHANGED
|
@@ -41,7 +41,5 @@ export function claudeConfigDirOrNull(): string | null {
|
|
|
41
41
|
|
|
42
42
|
/** The Claude config directory, falling back to `~/.claude` even under test. */
|
|
43
43
|
export function requireClaudeConfigDir(): string {
|
|
44
|
-
return (
|
|
45
|
-
process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude")
|
|
46
|
-
);
|
|
44
|
+
return process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude");
|
|
47
45
|
}
|