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.
Files changed (150) hide show
  1. package/README.md +22 -0
  2. package/bin/claudeup.js +6 -1
  3. package/package.json +6 -6
  4. package/src/__tests__/alias-adopt.test.ts +331 -329
  5. package/src/__tests__/alias-parser.test.ts +384 -376
  6. package/src/__tests__/alias-shell-writer.test.ts +638 -625
  7. package/src/__tests__/alias-store.test.ts +143 -143
  8. package/src/__tests__/catalog-cache-store.test.ts +265 -265
  9. package/src/__tests__/catalog-notice.test.ts +123 -123
  10. package/src/__tests__/cli-apply-seams.test.ts +12 -8
  11. package/src/__tests__/cli-router.test.ts +132 -3
  12. package/src/__tests__/cli-update-view.test.ts +27 -8
  13. package/src/__tests__/community-staleness.test.ts +0 -2
  14. package/src/__tests__/component-badge.test.ts +4 -4
  15. package/src/__tests__/content-drift.test.ts +5 -1
  16. package/src/__tests__/conventions-integration.test.ts +774 -689
  17. package/src/__tests__/conventions-manager.test.ts +1216 -1152
  18. package/src/__tests__/doctor-bins.test.ts +14 -6
  19. package/src/__tests__/doctor.test.ts +438 -425
  20. package/src/__tests__/dual-write-prevention.test.ts +277 -282
  21. package/src/__tests__/enabled-not-installed.test.ts +71 -63
  22. package/src/__tests__/file-locking.test.ts +196 -196
  23. package/src/__tests__/gap-fill-versions.test.ts +318 -311
  24. package/src/__tests__/github-budget.test.ts +170 -164
  25. package/src/__tests__/gitignore-detector.test.ts +160 -152
  26. package/src/__tests__/gitignore-fixer.test.ts +285 -247
  27. package/src/__tests__/gitignore-prerun.test.ts +57 -57
  28. package/src/__tests__/gitignore-resolver.test.ts +173 -146
  29. package/src/__tests__/gitignore-service.test.ts +121 -119
  30. package/src/__tests__/install-command.test.ts +3 -3
  31. package/src/__tests__/install-plan.test.ts +13 -7
  32. package/src/__tests__/manifest.test.ts +16 -5
  33. package/src/__tests__/markdown-renderer.test.ts +0 -1
  34. package/src/__tests__/marketplace-badge.test.ts +111 -90
  35. package/src/__tests__/marketplace-version.test.ts +64 -61
  36. package/src/__tests__/plugin-manager-fallback.test.ts +249 -239
  37. package/src/__tests__/plugin-requires.test.ts +18 -4
  38. package/src/__tests__/plugin-setup.test.ts +12 -12
  39. package/src/__tests__/profile-command.test.ts +1 -7
  40. package/src/__tests__/profile-materializer.test.ts +31 -8
  41. package/src/__tests__/profile-sync.test.ts +23 -5
  42. package/src/__tests__/registry-version-resolution.test.ts +61 -47
  43. package/src/__tests__/resolve-executable.test.ts +1 -1
  44. package/src/__tests__/resolver.test.ts +40 -15
  45. package/src/__tests__/scope-action.test.ts +52 -50
  46. package/src/__tests__/scope-squares.test.tsx +4 -5
  47. package/src/__tests__/skill-install-files.test.ts +111 -103
  48. package/src/__tests__/symlink-manager.test.ts +27 -10
  49. package/src/__tests__/theme-env.test.ts +281 -0
  50. package/src/__tests__/theme-resolve.test.ts +380 -0
  51. package/src/__tests__/update-apply.test.ts +1 -3
  52. package/src/__tests__/useGitignoreModal.test.ts +69 -61
  53. package/src/__tests__/version-snapshot.test.ts +181 -165
  54. package/src/cli/ansi.ts +23 -15
  55. package/src/cli/doctor.ts +10 -9
  56. package/src/cli/install.ts +3 -3
  57. package/src/cli/profile.ts +5 -4
  58. package/src/cli/prompt.ts +0 -1
  59. package/src/cli/router.ts +69 -2
  60. package/src/cli/update-view.ts +11 -6
  61. package/src/cli/update.ts +9 -4
  62. package/src/cli/upgrade.ts +6 -4
  63. package/src/data/alias-flags.ts +260 -260
  64. package/src/data/gitignore-defaults.ts +20 -20
  65. package/src/data/gitignore-reasons.ts +86 -92
  66. package/src/data/gitignore-templates.ts +14 -14
  67. package/src/data/predefined-profiles.ts +8 -9
  68. package/src/data/skill-repos.ts +43 -35
  69. package/src/main.tsx +29 -6
  70. package/src/opentui.d.ts +1 -3
  71. package/src/prerunner/index.ts +7 -8
  72. package/src/services/alias-settings.ts +20 -23
  73. package/src/services/alias-shell-writer.ts +800 -803
  74. package/src/services/alias-store.ts +77 -77
  75. package/src/services/claude-settings.ts +27 -18
  76. package/src/services/content-drift.ts +13 -3
  77. package/src/services/conventions-manager.ts +692 -617
  78. package/src/services/doctor.ts +464 -403
  79. package/src/services/gitignore-detector.ts +113 -103
  80. package/src/services/gitignore-fixer.ts +204 -189
  81. package/src/services/gitignore-prerun.ts +26 -26
  82. package/src/services/gitignore-resolver.ts +162 -144
  83. package/src/services/gitignore-service.ts +120 -117
  84. package/src/services/install-plan.ts +4 -4
  85. package/src/services/manifest.ts +1 -1
  86. package/src/services/marketplace-catalog-git.ts +5 -2
  87. package/src/services/marketplace-fetcher.ts +241 -235
  88. package/src/services/plugin-manager.ts +17 -20
  89. package/src/services/plugin-mcp-config.ts +2 -2
  90. package/src/services/plugin-requires.ts +9 -6
  91. package/src/services/plugin-setup.ts +7 -9
  92. package/src/services/plugin-version-check.ts +4 -4
  93. package/src/services/profile-materializer.ts +9 -5
  94. package/src/services/profile-sync.ts +12 -4
  95. package/src/services/profiles.ts +3 -3
  96. package/src/services/resolver.ts +10 -6
  97. package/src/services/settings-manager.ts +82 -46
  98. package/src/services/skills-manager.ts +11 -9
  99. package/src/services/symlink-manager.ts +2 -5
  100. package/src/services/toolchain.ts +3 -3
  101. package/src/services/update-cache.ts +1 -1
  102. package/src/services/update-engine.ts +4 -4
  103. package/src/services/update-plan.ts +9 -11
  104. package/src/services/version-snapshot.ts +88 -88
  105. package/src/types/gitignore.ts +28 -28
  106. package/src/ui/App.tsx +36 -36
  107. package/src/ui/adapters/settingsAdapter.ts +2 -2
  108. package/src/ui/components/CategoryHeader.tsx +1 -1
  109. package/src/ui/components/FlagDetailEditor.tsx +0 -0
  110. package/src/ui/components/ScopeIndicator.tsx +3 -1
  111. package/src/ui/components/ScrollableList.tsx +2 -1
  112. package/src/ui/components/StyledText.tsx +1 -1
  113. package/src/ui/components/layout/FooterHints.tsx +5 -3
  114. package/src/ui/components/layout/Panel.tsx +2 -2
  115. package/src/ui/components/layout/ScreenLayout.tsx +5 -4
  116. package/src/ui/components/modals/InputModal.tsx +7 -1
  117. package/src/ui/components/modals/ModalContainer.tsx +3 -3
  118. package/src/ui/components/modals/SelectModal.tsx +1 -1
  119. package/src/ui/components/primitives/DetailSection.tsx +1 -1
  120. package/src/ui/components/primitives/KeyValueLine.tsx +1 -1
  121. package/src/ui/components/primitives/ScopeSquares.tsx +2 -2
  122. package/src/ui/hooks/index.ts +0 -1
  123. package/src/ui/hooks/useGitignoreModal.ts +1 -5
  124. package/src/ui/hooks/useMismatchModal.ts +4 -10
  125. package/src/ui/renderers/cliToolRenderers.tsx +2 -2
  126. package/src/ui/renderers/gitignoreRenderers.tsx +2 -2
  127. package/src/ui/renderers/mcpRenderers.tsx +1 -1
  128. package/src/ui/renderers/pluginRenderers.tsx +3 -9
  129. package/src/ui/renderers/profileRenderers.tsx +9 -8
  130. package/src/ui/renderers/settingsRenderers.tsx +4 -4
  131. package/src/ui/renderers/skillRenderers.tsx +14 -14
  132. package/src/ui/screens/AliasScreen.tsx +31 -34
  133. package/src/ui/screens/CliToolsScreen.tsx +14 -13
  134. package/src/ui/screens/EnvVarsScreen.tsx +8 -8
  135. package/src/ui/screens/GitignoreScreen.tsx +15 -14
  136. package/src/ui/screens/McpRegistryScreen.tsx +10 -7
  137. package/src/ui/screens/McpScreen.tsx +10 -10
  138. package/src/ui/screens/PluginsScreen.tsx +17 -21
  139. package/src/ui/screens/ProfilesScreen.tsx +25 -24
  140. package/src/ui/screens/SkillsScreen.tsx +16 -14
  141. package/src/ui/state/AppContext.tsx +4 -8
  142. package/src/ui/state/DimensionsContext.tsx +3 -2
  143. package/src/ui/theme-mode.ts +20 -38
  144. package/src/ui/theme-resolve.ts +148 -0
  145. package/src/utils/clipboard.ts +5 -5
  146. package/src/utils/command-utils.ts +2 -2
  147. package/src/utils/config-dir.ts +1 -3
  148. package/src/utils/file-locking.ts +88 -86
  149. package/src/ui/hooks/useAsyncData.ts +0 -127
  150. package/src/ui/state/AnimationContext.tsx +0 -76
@@ -56,17 +56,15 @@ function scopeVersions(
56
56
  info: PluginInfo | undefined,
57
57
  ): Array<[PluginScope, string]> {
58
58
  if (!info) return [];
59
- return (
60
- [
61
- ["user", info.userScope],
62
- ["project", info.projectScope],
63
- ["local", info.localScope],
64
- ] as const
65
- )
66
- .filter(([, status]) => !!status?.version)
67
- .map(
68
- ([scope, status]) => [scope, status!.version!] as [PluginScope, string],
69
- );
59
+ const out: Array<[PluginScope, string]> = [];
60
+ for (const [scope, status] of [
61
+ ["user", info.userScope],
62
+ ["project", info.projectScope],
63
+ ["local", info.localScope],
64
+ ] as const) {
65
+ if (status?.version) out.push([scope, status.version]);
66
+ }
67
+ return out;
70
68
  }
71
69
 
72
70
  /** The highest version installed across scopes, or null. */
@@ -15,8 +15,8 @@
15
15
  */
16
16
 
17
17
  import { promises as fs } from "node:fs";
18
- import path from "node:path";
19
18
  import os from "node:os";
19
+ import path from "node:path";
20
20
 
21
21
  /**
22
22
  * Sentinel recorded by Claude Code for a plugin that is installed but whose
@@ -27,41 +27,41 @@ export const UNKNOWN_VERSION = "0.0.0";
27
27
 
28
28
  /** True when a version string carries no usable information. */
29
29
  export function isKnownVersion(v: string | undefined): v is string {
30
- return !!v && v !== UNKNOWN_VERSION;
30
+ return !!v && v !== UNKNOWN_VERSION;
31
31
  }
32
32
 
33
33
  export type PluginChangeKind = "installed" | "updated";
34
34
 
35
35
  export interface PluginChange {
36
- pluginId: string;
37
- kind: PluginChangeKind;
38
- /** Previous version. Only set for "updated", and always a known version. */
39
- from?: string;
40
- /** Current version. May be UNKNOWN_VERSION — callers must not render it raw. */
41
- to: string;
36
+ pluginId: string;
37
+ kind: PluginChangeKind;
38
+ /** Previous version. Only set for "updated", and always a known version. */
39
+ from?: string;
40
+ /** Current version. May be UNKNOWN_VERSION — callers must not render it raw. */
41
+ to: string;
42
42
  }
43
43
 
44
44
  interface SnapshotFile {
45
- /**
46
- * Legacy flat baseline: pluginId -> version, with no record of which project
47
- * it was observed from. Kept only so an existing file can be migrated.
48
- */
49
- seen?: Record<string, string>;
50
- /**
51
- * projectPath -> (pluginId -> installedVersion) as of the last time claudeup
52
- * rendered that project.
53
- *
54
- * Versions are per-project (installed_plugins.json keys installs by
55
- * projectPath), but this file is global. A single flat map therefore let two
56
- * projects legitimately on different versions overwrite each other's
57
- * baseline, and every differing plugin was badged "updated" on the next
58
- * render — a change that never happened. Keying by project removes the
59
- * cross-talk entirely.
60
- */
61
- byProject?: Record<string, Record<string, string>>;
62
- /** Set on the run that first created the baseline. */
63
- seededAt: string;
64
- updatedAt: string;
45
+ /**
46
+ * Legacy flat baseline: pluginId -> version, with no record of which project
47
+ * it was observed from. Kept only so an existing file can be migrated.
48
+ */
49
+ seen?: Record<string, string>;
50
+ /**
51
+ * projectPath -> (pluginId -> installedVersion) as of the last time claudeup
52
+ * rendered that project.
53
+ *
54
+ * Versions are per-project (installed_plugins.json keys installs by
55
+ * projectPath), but this file is global. A single flat map therefore let two
56
+ * projects legitimately on different versions overwrite each other's
57
+ * baseline, and every differing plugin was badged "updated" on the next
58
+ * render — a change that never happened. Keying by project removes the
59
+ * cross-talk entirely.
60
+ */
61
+ byProject?: Record<string, Record<string, string>>;
62
+ /** Set on the run that first created the baseline. */
63
+ seededAt: string;
64
+ updatedAt: string;
65
65
  }
66
66
 
67
67
  /**
@@ -72,24 +72,24 @@ interface SnapshotFile {
72
72
  * That is not hypothetical: it happened while building the tests for this fix.
73
73
  */
74
74
  function snapshotPath(): string {
75
- const configDir =
76
- process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude");
77
- return path.join(configDir, "claudeup-version-snapshot.json");
75
+ const configDir =
76
+ process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude");
77
+ return path.join(configDir, "claudeup-version-snapshot.json");
78
78
  }
79
79
 
80
80
  async function read(): Promise<SnapshotFile | null> {
81
- try {
82
- const raw = await fs.readFile(snapshotPath(), "utf-8");
83
- const data = JSON.parse(raw) as SnapshotFile;
84
- if (!data) return null;
85
- const hasFlat = typeof data.seen === "object" && data.seen !== null;
86
- const hasByProject =
87
- typeof data.byProject === "object" && data.byProject !== null;
88
- if (!hasFlat && !hasByProject) return null;
89
- return data;
90
- } catch {
91
- return null; // absent or corrupt — treated as "no baseline yet"
92
- }
81
+ try {
82
+ const raw = await fs.readFile(snapshotPath(), "utf-8");
83
+ const data = JSON.parse(raw) as SnapshotFile;
84
+ if (!data) return null;
85
+ const hasFlat = typeof data.seen === "object" && data.seen !== null;
86
+ const hasByProject =
87
+ typeof data.byProject === "object" && data.byProject !== null;
88
+ if (!hasFlat && !hasByProject) return null;
89
+ return data;
90
+ } catch {
91
+ return null; // absent or corrupt — treated as "no baseline yet"
92
+ }
93
93
  }
94
94
 
95
95
  /**
@@ -111,30 +111,30 @@ async function read(): Promise<SnapshotFile | null> {
111
111
  * A downgrade IS reported: rolling back is a real change worth surfacing.
112
112
  */
113
113
  export function diffVersions(
114
- previous: Record<string, string> | null,
115
- current: Record<string, string>,
114
+ previous: Record<string, string> | null,
115
+ current: Record<string, string>,
116
116
  ): PluginChange[] {
117
- if (!previous) return [];
117
+ if (!previous) return [];
118
118
 
119
- const changes: PluginChange[] = [];
120
- for (const [pluginId, to] of Object.entries(current)) {
121
- const from = previous[pluginId];
119
+ const changes: PluginChange[] = [];
120
+ for (const [pluginId, to] of Object.entries(current)) {
121
+ const from = previous[pluginId];
122
122
 
123
- // Never seen before → newly installed, not an update. There is no
124
- // meaningful "from", and `to` may legitimately be UNKNOWN_VERSION.
125
- if (from === undefined) {
126
- changes.push({ pluginId, kind: "installed", to });
127
- continue;
128
- }
123
+ // Never seen before → newly installed, not an update. There is no
124
+ // meaningful "from", and `to` may legitimately be UNKNOWN_VERSION.
125
+ if (from === undefined) {
126
+ changes.push({ pluginId, kind: "installed", to });
127
+ continue;
128
+ }
129
129
 
130
- if (from === to) continue;
130
+ if (from === to) continue;
131
131
 
132
- // One side carries no real version: we learned or lost it, nothing moved.
133
- if (!isKnownVersion(from) || !isKnownVersion(to)) continue;
132
+ // One side carries no real version: we learned or lost it, nothing moved.
133
+ if (!isKnownVersion(from) || !isKnownVersion(to)) continue;
134
134
 
135
- changes.push({ pluginId, kind: "updated", from, to });
136
- }
137
- return changes;
135
+ changes.push({ pluginId, kind: "updated", from, to });
136
+ }
137
+ return changes;
138
138
  }
139
139
 
140
140
  /**
@@ -149,16 +149,16 @@ export function diffVersions(
149
149
  * asks first, which is the closest thing to the truth it contains.
150
150
  */
151
151
  export async function loadSeenVersions(
152
- projectPath: string,
152
+ projectPath: string,
153
153
  ): Promise<Record<string, string> | null> {
154
- const data = await read();
155
- if (!data) return null;
156
- const scoped = data.byProject?.[projectPath];
157
- if (scoped) return scoped;
158
- // Legacy flat baseline, project unknown — use it once, then it is replaced
159
- // by per-project entries on save.
160
- if (data.seen && !data.byProject) return data.seen;
161
- return null;
154
+ const data = await read();
155
+ if (!data) return null;
156
+ const scoped = data.byProject?.[projectPath];
157
+ if (scoped) return scoped;
158
+ // Legacy flat baseline, project unknown — use it once, then it is replaced
159
+ // by per-project entries on save.
160
+ if (data.seen && !data.byProject) return data.seen;
161
+ return null;
162
162
  }
163
163
 
164
164
  /**
@@ -167,23 +167,23 @@ export async function loadSeenVersions(
167
167
  * shows again next launch.
168
168
  */
169
169
  export async function saveSeenVersions(
170
- projectPath: string,
171
- current: Record<string, string>,
170
+ projectPath: string,
171
+ current: Record<string, string>,
172
172
  ): Promise<void> {
173
- try {
174
- const file = snapshotPath();
175
- await fs.mkdir(path.dirname(file), { recursive: true });
176
- const existing = await read();
177
- const now = new Date().toISOString();
178
- const payload: SnapshotFile = {
179
- // Other projects' baselines are preserved — writing only this project's
180
- // slot is the entire point of the fix.
181
- byProject: { ...(existing?.byProject ?? {}), [projectPath]: current },
182
- seededAt: existing?.seededAt ?? now,
183
- updatedAt: now,
184
- };
185
- await fs.writeFile(file, JSON.stringify(payload, null, 2), "utf-8");
186
- } catch {
187
- // ignore
188
- }
173
+ try {
174
+ const file = snapshotPath();
175
+ await fs.mkdir(path.dirname(file), { recursive: true });
176
+ const existing = await read();
177
+ const now = new Date().toISOString();
178
+ const payload: SnapshotFile = {
179
+ // Other projects' baselines are preserved — writing only this project's
180
+ // slot is the entire point of the fix.
181
+ byProject: { ...(existing?.byProject ?? {}), [projectPath]: current },
182
+ seededAt: existing?.seededAt ?? now,
183
+ updatedAt: now,
184
+ };
185
+ await fs.writeFile(file, JSON.stringify(payload, null, 2), "utf-8");
186
+ } catch {
187
+ // ignore
188
+ }
189
189
  }
@@ -10,31 +10,31 @@ export type Tier = "builtin" | "global" | "project" | "profile";
10
10
 
11
11
  /** A user-authored manifest, deserialized from JSON. */
12
12
  export interface GitignoreManifest {
13
- /** Paths or globs that must be excluded from git. */
14
- ignore: string[];
15
- /** Paths that must remain tracked even if a parent is in ignore. */
16
- track: string[];
17
- /** Optional template names to merge in (e.g. "common-dev"). */
18
- extends?: string[];
13
+ /** Paths or globs that must be excluded from git. */
14
+ ignore: string[];
15
+ /** Paths that must remain tracked even if a parent is in ignore. */
16
+ track: string[];
17
+ /** Optional template names to merge in (e.g. "common-dev"). */
18
+ extends?: string[];
19
19
  }
20
20
 
21
21
  /** A single rule after tier resolution. */
22
22
  export interface ResolvedRule {
23
- pattern: string;
24
- action: RuleAction;
25
- source: Tier;
23
+ pattern: string;
24
+ action: RuleAction;
25
+ source: Tier;
26
26
  }
27
27
 
28
28
  /** When two tiers disagree on a pattern, the higher-precedence tier wins. */
29
29
  export interface ResolvedConflict {
30
- pattern: string;
31
- winner: Tier;
32
- losers: Array<{ tier: Tier; action: RuleAction }>;
30
+ pattern: string;
31
+ winner: Tier;
32
+ losers: Array<{ tier: Tier; action: RuleAction }>;
33
33
  }
34
34
 
35
35
  export interface ResolvedManifest {
36
- rules: ResolvedRule[];
37
- conflicts: ResolvedConflict[];
36
+ rules: ResolvedRule[];
37
+ conflicts: ResolvedConflict[];
38
38
  }
39
39
 
40
40
  /**
@@ -49,20 +49,20 @@ export interface ResolvedManifest {
49
49
  * covers it (and the path isn't currently tracked)
50
50
  */
51
51
  export type ViolationKind =
52
- | "tracked-but-should-ignore"
53
- | "ignored-but-should-track"
54
- | "untracked-and-should-track"
55
- | "missing-from-gitignore";
52
+ | "tracked-but-should-ignore"
53
+ | "ignored-but-should-track"
54
+ | "untracked-and-should-track"
55
+ | "missing-from-gitignore";
56
56
 
57
57
  export interface Violation {
58
- kind: ViolationKind;
59
- path: string;
60
- /** Which tier asserted the rule that's being violated. */
61
- source: Tier;
62
- /**
63
- * "safe" — fix is purely additive (append a .gitignore line)
64
- * "destructive" — fix mutates git index (`git rm --cached`, `git add`,
65
- * removing existing .gitignore lines)
66
- */
67
- severity: "safe" | "destructive";
58
+ kind: ViolationKind;
59
+ path: string;
60
+ /** Which tier asserted the rule that's being violated. */
61
+ source: Tier;
62
+ /**
63
+ * "safe" — fix is purely additive (append a .gitignore line)
64
+ * "destructive" — fix mutates git index (`git rm --cached`, `git add`,
65
+ * removing existing .gitignore lines)
66
+ */
67
+ severity: "safe" | "destructive";
68
68
  }
package/src/ui/App.tsx CHANGED
@@ -1,55 +1,55 @@
1
- import React, { useEffect, useState } from "react";
2
- import { useRenderer } from "@opentui/react";
3
1
  import fs from "node:fs";
2
+ import { useRenderer } from "@opentui/react";
3
+ import React, { useEffect, useState } from "react";
4
+ import { isClaudeAvailable } from "../services/claude-cli.js";
4
5
  import {
5
- AppProvider,
6
- useApp,
7
- useNavigation,
8
- useModal,
9
- } from "./state/AppContext.js";
10
- import {
11
- DimensionsProvider,
12
- useDimensions,
13
- } from "./state/DimensionsContext.js";
14
- import { ModalContainer } from "./components/modals/index.js";
15
- import {
16
- PluginsScreen,
17
- McpScreen,
18
- McpRegistryScreen,
19
- SettingsScreen,
20
- CliToolsScreen,
21
- ProfilesScreen,
22
- SkillsScreen,
23
- StylesScreen,
24
- GitignoreScreen,
25
- AliasScreen,
26
- } from "./screens/index.js";
27
- import type { Screen } from "./state/types.js";
6
+ migrateMarketplaceRename,
7
+ recoverMarketplaceSettings,
8
+ } from "../services/claude-settings.js";
28
9
  import { checkGitignore } from "../services/gitignore-prerun.js";
29
10
  import {
30
- loadGitignoreState,
31
11
  applyAllSafeFixes,
12
+ loadGitignoreState,
32
13
  } from "../services/gitignore-service.js";
33
- import { useGitignoreModal } from "./hooks/useGitignoreModal.js";
34
14
  import { repairAllMarketplaces } from "../services/local-marketplace.js";
35
- import {
36
- migrateMarketplaceRename,
37
- recoverMarketplaceSettings,
38
- } from "../services/claude-settings.js";
39
15
  import { autoAddMissingMarketplaces } from "../services/marketplace-sync.js";
40
- import { isClaudeAvailable } from "../services/claude-cli.js";
41
16
  import {
42
- checkPluginVersionMismatches,
43
17
  type VersionMismatchInfo,
18
+ checkPluginVersionMismatches,
44
19
  } from "../services/plugin-version-check.js";
45
- import { useMismatchModal } from "./hooks/useMismatchModal.js";
46
20
  import {
21
+ type VersionCheckResult,
47
22
  checkForUpdates,
48
23
  getCurrentVersion,
49
- type VersionCheckResult,
50
24
  } from "../services/version-check.js";
51
- import { useKeyboardHandler } from "./hooks/useKeyboardHandler.js";
52
25
  import { ProgressBar } from "./components/layout/ProgressBar.js";
26
+ import { ModalContainer } from "./components/modals/index.js";
27
+ import { useGitignoreModal } from "./hooks/useGitignoreModal.js";
28
+ import { useKeyboardHandler } from "./hooks/useKeyboardHandler.js";
29
+ import { useMismatchModal } from "./hooks/useMismatchModal.js";
30
+ import {
31
+ AliasScreen,
32
+ CliToolsScreen,
33
+ GitignoreScreen,
34
+ McpRegistryScreen,
35
+ McpScreen,
36
+ PluginsScreen,
37
+ ProfilesScreen,
38
+ SettingsScreen,
39
+ SkillsScreen,
40
+ StylesScreen,
41
+ } from "./screens/index.js";
42
+ import {
43
+ AppProvider,
44
+ useApp,
45
+ useModal,
46
+ useNavigation,
47
+ } from "./state/AppContext.js";
48
+ import {
49
+ DimensionsProvider,
50
+ useDimensions,
51
+ } from "./state/DimensionsContext.js";
52
+ import type { Screen } from "./state/types.js";
53
53
  import { theme } from "./theme.js";
54
54
 
55
55
  export const VERSION = getCurrentVersion();
@@ -4,7 +4,7 @@ import {
4
4
  type SettingDefinition,
5
5
  } from "../../data/settings-catalog.js";
6
6
  import type { ScopedSettingValues } from "../../services/settings-manager.js";
7
- import { theme, type UiColor } from "../theme.js";
7
+ import { type UiColor, theme } from "../theme.js";
8
8
 
9
9
  // ─── Item types ────────────────────────────────────────────────────────────────
10
10
 
@@ -117,7 +117,7 @@ export function formatValue(
117
117
  }
118
118
 
119
119
  if (value.length > 12) {
120
- return value.slice(0, 12) + "…";
120
+ return `${value.slice(0, 12)}…`;
121
121
  }
122
122
  return value;
123
123
  }
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { theme, type UiColor } from "../theme.js";
2
+ import { type UiColor, theme } from "../theme.js";
3
3
 
4
4
  interface CategoryHeaderProps {
5
5
  /** Category title */
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { theme, type UiColor } from "../theme.js";
2
+ import { type UiColor, theme } from "../theme.js";
3
3
 
4
4
  /**
5
5
  * Scope indicator for list items.
@@ -49,10 +49,12 @@ export function ScopeIndicator({ user, project, local }: ScopeIndicatorProps) {
49
49
  <text fg={theme.colors.text}>
50
50
  {segments.map((s, i) =>
51
51
  s.bg ? (
52
+ // biome-ignore lint/suspicious/noArrayIndexKey: segments are fixed-position scope squares (user/project/local); the slot is the identity and the list never reorders
52
53
  <span key={i} bg={s.bg} fg={s.fg}>
53
54
  {s.char}
54
55
  </span>
55
56
  ) : (
57
+ // biome-ignore lint/suspicious/noArrayIndexKey: same fixed-slot list as above
56
58
  <span key={i}> </span>
57
59
  ),
58
60
  )}
@@ -1,4 +1,5 @@
1
- import React, { useState, useEffect, useMemo } from "react";
1
+ import type React from "react";
2
+ import { useEffect, useMemo, useState } from "react";
2
3
  import { theme } from "../theme.js";
3
4
 
4
5
  interface ScrollableListProps<T> {
@@ -1,4 +1,4 @@
1
- import React from "react";
1
+ import type React from "react";
2
2
  import { theme } from "../theme.js";
3
3
 
4
4
  interface StyledTextProps {
@@ -30,20 +30,22 @@ export function FooterHints({
30
30
  }: { hints: FooterHint[] }): React.ReactNode {
31
31
  const nodes: React.ReactNode[] = [];
32
32
  hints.forEach((hint, i) => {
33
+ // A hint is identified by its key chord plus label, not its position.
34
+ const id = `${hint.keys.join("")}:${hint.label}`;
33
35
  if (i > 0) {
34
36
  nodes.push(
35
- <span key={`gap-${i}`} fg={LABEL_FG}>
37
+ <span key={`gap-${id}`} fg={LABEL_FG}>
36
38
  {GAP}
37
39
  </span>,
38
40
  );
39
41
  }
40
42
  nodes.push(
41
- <span key={`k-${i}`} bg={KEY_BG} fg={KEY_FG}>
43
+ <span key={`k-${id}`} bg={KEY_BG} fg={KEY_FG}>
42
44
  {` ${hint.keys.join("")} `}
43
45
  </span>,
44
46
  );
45
47
  nodes.push(
46
- <span key={`l-${i}`} fg={LABEL_FG}>
48
+ <span key={`l-${id}`} fg={LABEL_FG}>
47
49
  {` ${hint.label}`}
48
50
  </span>,
49
51
  );
@@ -1,5 +1,5 @@
1
- import React from "react";
2
- import { theme, type UiColor } from "../../theme.js";
1
+ import type React from "react";
2
+ import { type UiColor, theme } from "../../theme.js";
3
3
 
4
4
  interface PanelProps {
5
5
  /** Panel title */
@@ -1,12 +1,13 @@
1
- import React, { useEffect, useRef } from "react";
2
1
  import type { ScrollBoxRenderable } from "@opentui/core";
3
- import { useDimensions } from "../../state/DimensionsContext.js";
2
+ import type React from "react";
3
+ import { useEffect, useRef } from "react";
4
4
  import { useKeyboard } from "../../hooks/useKeyboard.js";
5
5
  import { useApp } from "../../state/AppContext.js";
6
- import { TabBar } from "../TabBar.js";
6
+ import { useDimensions } from "../../state/DimensionsContext.js";
7
7
  import type { Screen } from "../../state/types.js";
8
- import { FooterHints, type FooterHint } from "./FooterHints.js";
9
8
  import { theme } from "../../theme.js";
9
+ import { TabBar } from "../TabBar.js";
10
+ import { type FooterHint, FooterHints } from "./FooterHints.js";
10
11
 
11
12
  interface ScreenLayoutProps {
12
13
  /** Screen title (e.g., "claudeup Plugins") */
@@ -1,3 +1,4 @@
1
+ import type { SubmitEvent } from "@opentui/core";
1
2
  import React from "react";
2
3
  import { theme } from "../../theme.js";
3
4
 
@@ -53,7 +54,12 @@ export function InputModal({
53
54
  >
54
55
  <input
55
56
  value={defaultValue}
56
- onSubmit={onSubmit as any}
57
+ onSubmit={(value: string | SubmitEvent) => {
58
+ // @opentui/react types the prop as an intersection of the core
59
+ // `(event: SubmitEvent) => void` and its own `(value: string) => void`;
60
+ // at runtime the input hands over the string.
61
+ if (typeof value === "string") onSubmit(value);
62
+ }}
57
63
  focused
58
64
  width={50}
59
65
  />
@@ -1,11 +1,11 @@
1
1
  import React, { useState } from "react";
2
- import { useApp } from "../../state/AppContext.js";
3
2
  import { useKeyboard } from "../../hooks/useKeyboard.js";
3
+ import { useApp } from "../../state/AppContext.js";
4
4
  import { ConfirmModal } from "./ConfirmModal.js";
5
5
  import { InputModal } from "./InputModal.js";
6
- import { SelectModal } from "./SelectModal.js";
7
- import { MessageModal } from "./MessageModal.js";
8
6
  import { LoadingModal } from "./LoadingModal.js";
7
+ import { MessageModal } from "./MessageModal.js";
8
+ import { SelectModal } from "./SelectModal.js";
9
9
  import { VersionMismatchModal } from "./VersionMismatchModal.js";
10
10
 
11
11
  /**
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
- import type { SelectOption } from "../../state/types.js";
3
2
  import { useDimensions } from "../../state/DimensionsContext.js";
3
+ import type { SelectOption } from "../../state/types.js";
4
4
  import { theme } from "../../theme.js";
5
5
 
6
6
  interface SelectModalProps {
@@ -1,4 +1,4 @@
1
- import React from "react";
1
+ import type React from "react";
2
2
  import { theme } from "../../theme.js";
3
3
 
4
4
  interface DetailSectionProps {
@@ -1,4 +1,4 @@
1
- import React from "react";
1
+ import type React from "react";
2
2
  import { theme } from "../../theme.js";
3
3
 
4
4
  interface KeyValueLineProps {
@@ -23,8 +23,8 @@ const SEGMENT = "■";
23
23
  * grey as de-emphasised body text. Against a light page that reads as a *dark*
24
24
  * block, giving an unlit segment the same visual weight as a lit one — "non
25
25
  * installed state is dark as well - confusing". The unlit fill now comes from
26
- * `scopeOffFill()`, which steps toward whichever page the terminal actually has;
27
- * see theme-mode.ts for why that one value cannot be fixed.
26
+ * `scopeOffFill()`, which steps toward the page the resolved mode names; see
27
+ * theme-mode.ts for why that one value cannot be fixed.
28
28
  */
29
29
  export function ScopeSquares({
30
30
  user,
@@ -1,4 +1,3 @@
1
- export { useAsyncData, useDebouncedAsyncData } from "./useAsyncData.js";
2
1
  export { useKeyboardHandler } from "./useKeyboardHandler.js";
3
2
  export { useKeyboard } from "./useKeyboard.js";
4
3
  export { useMismatchModal } from "./useMismatchModal.js";
@@ -40,11 +40,7 @@ export function buildGitignoreModal(args: BuildModalArgs): ModalState {
40
40
  return {
41
41
  type: "select",
42
42
  title: "Gitignore violations detected",
43
- message:
44
- `Found ${violationCount} managed gitignore item(s) that do not match ` +
45
- `the current repo state.\n\n` +
46
- `Missing .gitignore entries can be added now. Items that change git ` +
47
- `tracking are reviewed and fixed from the Gitignore tab.`,
43
+ message: `Found ${violationCount} managed gitignore item(s) that do not match the current repo state.\n\nMissing .gitignore entries can be added now. Items that change git tracking are reviewed and fixed from the Gitignore tab.`,
48
44
  options,
49
45
  onSelect: (value: string) => {
50
46
  if (value === "fix-safe") onFixSafe();