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.
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 +38 -13
  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 +183 -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 +1 -2
  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
@@ -1,9 +1,9 @@
1
1
  import { useCallback } from "react";
2
- import { useApp } from "../state/AppContext.js";
3
2
  import {
4
- fixAllPluginVersionMismatches,
5
3
  type VersionMismatchInfo,
4
+ fixAllPluginVersionMismatches,
6
5
  } from "../../services/plugin-version-check.js";
6
+ import { useApp } from "../state/AppContext.js";
7
7
 
8
8
  /**
9
9
  * Shared hook for showing the plugin version mismatch modal.
@@ -52,11 +52,7 @@ export function useMismatchModal() {
52
52
  modal: {
53
53
  type: "message",
54
54
  title: "Plugin Versions Aligned",
55
- message:
56
- `Updated ${totalUpdated} project entr${totalUpdated === 1 ? "y" : "ies"} in ` +
57
- `installed_plugins.json.\n\n` +
58
- `Restart Claude Code for the new versions to load. ` +
59
- `The running session still has the old plugin code in memory.`,
55
+ message: `Updated ${totalUpdated} project entr${totalUpdated === 1 ? "y" : "ies"} in installed_plugins.json.\n\nRestart Claude Code for the new versions to load. The running session still has the old plugin code in memory.`,
60
56
  variant: "success",
61
57
  onDismiss: dismiss,
62
58
  },
@@ -68,9 +64,7 @@ export function useMismatchModal() {
68
64
  modal: {
69
65
  type: "message",
70
66
  title: "Fix Failed",
71
- message:
72
- `Could not align plugin versions: ${error instanceof Error ? error.message : String(error)}\n\n` +
73
- `Try running claudeup again, or manually update the plugin in each project.`,
67
+ message: `Could not align plugin versions: ${error instanceof Error ? error.message : String(error)}\n\nTry running claudeup again, or manually update the plugin in each project.`,
74
68
  variant: "error",
75
69
  onDismiss: dismiss,
76
70
  },
@@ -1,4 +1,4 @@
1
- import React from "react";
1
+ import type React from "react";
2
2
  import type { CliTool } from "../../data/cli-tools.js";
3
3
  import {
4
4
  type InstallMethod,
@@ -7,7 +7,7 @@ import {
7
7
  } from "../../services/cli-tool-commands.js";
8
8
  import type { Command } from "../../utils/command-utils.js";
9
9
  import { formatCommand } from "../../utils/command-utils.js";
10
- import { theme, type UiColor } from "../theme.js";
10
+ import { type UiColor, theme } from "../theme.js";
11
11
 
12
12
  // ─── Helpers ────────────────────────────────────────────────────────────────
13
13
 
@@ -1,4 +1,4 @@
1
- import React from "react";
1
+ import type React from "react";
2
2
  import type { ResolvedRule, Violation } from "../../types/gitignore.js";
3
3
  import { theme } from "../theme.js";
4
4
 
@@ -39,7 +39,7 @@ const MARKER_NONE = " ";
39
39
  const MAX_PATH_LEN = 22;
40
40
 
41
41
  function truncatePath(p: string): string {
42
- return p.length > MAX_PATH_LEN ? p.slice(0, MAX_PATH_LEN - 1) + "…" : p;
42
+ return p.length > MAX_PATH_LEN ? `${p.slice(0, MAX_PATH_LEN - 1)}…` : p;
43
43
  }
44
44
 
45
45
  export function renderRuleRow(
@@ -1,4 +1,4 @@
1
- import React from "react";
1
+ import type React from "react";
2
2
  import type { McpServer } from "../../types/index.js";
3
3
  import { theme } from "../theme.js";
4
4
 
@@ -724,18 +724,15 @@ const COMPONENT_NOUN: Record<PluginComponentKind, string> = {
724
724
  /**
725
725
  * The type badge: one glyph in a padded, washed cell.
726
726
  *
727
- * Three states, and each exists for a reason.
727
+ * Two states, and each exists for a reason.
728
728
  *
729
729
  * On a *selected* row the whole line is painted in the selection colour, so the
730
730
  * badge drops its own fill — a chip of a third colour inside an inverted row is
731
731
  * the noisiest thing on the screen, and the selection already says where you
732
732
  * are.
733
733
  *
734
- * When the terminal never answered the theme query there is no fill either.
735
- * `componentBadge()` returns null there by design; painting a light wash on an
736
- * unknown page risks a bright band across every row of a long list.
737
- *
738
- * Otherwise: a wash of the kind's hue, quiet enough to sit on hundreds of rows.
734
+ * Otherwise: a wash of the kind's hue for the resolved mode, quiet enough to
735
+ * sit on hundreds of rows.
739
736
  */
740
737
  function componentBadge(
741
738
  kind: PluginComponentKind,
@@ -745,9 +742,6 @@ function componentBadge(
745
742
  if (isSelected) return ` ${icon} `;
746
743
 
747
744
  const chip = badgeColors(kind);
748
- if (!chip) {
749
- return <span fg={COMPONENT_COLOR[kind]}>{` ${icon} `}</span>;
750
- }
751
745
  return <span bg={chip.bg} fg={chip.fg}>{` ${icon} `}</span>;
752
746
  }
753
747
 
@@ -1,8 +1,8 @@
1
- import React from "react";
2
- import type { ItemRenderer } from "../registry.js";
3
- import type { ProfileEntry } from "../../types/index.js";
1
+ import type React from "react";
4
2
  import type { PredefinedProfile } from "../../data/predefined-profiles.js";
5
- import { theme, type UiColor } from "../theme.js";
3
+ import type { ProfileEntry } from "../../types/index.js";
4
+ import type { ItemRenderer } from "../registry.js";
5
+ import { type UiColor, theme } from "../theme.js";
6
6
 
7
7
  // ─── List item types ───────────────────────────────────────────────────────────
8
8
 
@@ -15,7 +15,7 @@ export type ProfileListItem =
15
15
 
16
16
  function truncate(s: string, maxLen: number): string {
17
17
  if (s.length <= maxLen) return s;
18
- return s.slice(0, maxLen - 1) + "\u2026";
18
+ return `${s.slice(0, maxLen - 1)}\u2026`;
19
19
  }
20
20
 
21
21
  export function humanizeValue(_key: string, value: unknown): string {
@@ -143,9 +143,9 @@ const predefinedRenderer: ItemRenderer<{
143
143
  ([k]) => k !== "env",
144
144
  );
145
145
  const envMap =
146
- (profile.settings["env"] as Record<string, string> | undefined) ?? {};
147
- const tasksOn = envMap["CLAUDE_CODE_ENABLE_TASKS"] === "true";
148
- const teamsOn = envMap["CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS"] === "true";
146
+ (profile.settings.env as Record<string, string> | undefined) ?? {};
147
+ const tasksOn = envMap.CLAUDE_CODE_ENABLE_TASKS === "true";
148
+ const teamsOn = envMap.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS === "true";
149
149
 
150
150
  return (
151
151
  <box flexDirection="column">
@@ -298,6 +298,7 @@ const savedRenderer: ItemRenderer<{ kind: "saved"; entry: ProfileEntry }> = {
298
298
  <text fg={theme.colors.warning}> (none)</text>
299
299
  ) : (
300
300
  wrapNames(cleanPlugins).map((line, i) => (
301
+ // biome-ignore lint/suspicious/noArrayIndexKey: word-wrapped lines of a static preview; lines carry no identity and can repeat, so the row number is the only stable key
301
302
  <text key={i} fg={theme.colors.text}>
302
303
  {" "}
303
304
  {line}
@@ -1,18 +1,18 @@
1
- import React from "react";
2
- import type { ItemRenderer } from "../registry.js";
1
+ import type React from "react";
3
2
  import type {
4
3
  SettingsBrowserItem,
5
4
  SettingsCategoryItem,
6
5
  SettingsSettingItem,
7
6
  } from "../adapters/settingsAdapter.js";
8
7
  import {
9
- CATEGORY_LABELS,
10
8
  CATEGORY_BG,
11
9
  CATEGORY_COLOR,
12
10
  CATEGORY_DESCRIPTIONS,
11
+ CATEGORY_LABELS,
13
12
  formatValue,
14
13
  } from "../adapters/settingsAdapter.js";
15
- import { SelectableRow, MetaText } from "../components/primitives/index.js";
14
+ import { MetaText, SelectableRow } from "../components/primitives/index.js";
15
+ import type { ItemRenderer } from "../registry.js";
16
16
  import { theme } from "../theme.js";
17
17
 
18
18
  // ─── Category renderer ─────────────────────────────────────────────────────────
@@ -1,24 +1,24 @@
1
- import React from "react";
2
- import type { ItemRenderer } from "../registry.js";
1
+ import type React from "react";
2
+ import { STAR_RELIABILITY_INFO } from "../../data/skill-repos.js";
3
+ import type { StarReliability } from "../../types/index.js";
3
4
  import type {
4
5
  SkillBrowserItem,
5
6
  SkillCategoryItem,
6
- SkillSkillItem,
7
7
  SkillSetItem,
8
+ SkillSkillItem,
8
9
  } from "../adapters/skillsAdapter.js";
9
- import type { StarReliability } from "../../types/index.js";
10
- import { STAR_RELIABILITY_INFO } from "../../data/skill-repos.js";
11
10
  import {
12
- SelectableRow,
13
- ListCategoryRow,
14
- ScopeSquares,
15
- ScopeDetail,
16
11
  ActionHints,
17
- MetaText,
18
- KeyValueLine,
19
12
  DetailSection,
13
+ KeyValueLine,
14
+ ListCategoryRow,
15
+ MetaText,
16
+ ScopeDetail,
17
+ ScopeSquares,
18
+ SelectableRow,
20
19
  } from "../components/primitives/index.js";
21
- import { theme, type UiColor } from "../theme.js";
20
+ import type { ItemRenderer } from "../registry.js";
21
+ import { type UiColor, theme } from "../theme.js";
22
22
 
23
23
  // ─── Helpers ──────────────────────────────────────────────────────────────────
24
24
 
@@ -92,7 +92,7 @@ const MAX_SKILL_NAME_LEN = 35;
92
92
 
93
93
  function truncateName(name: string): string {
94
94
  return name.length > MAX_SKILL_NAME_LEN
95
- ? name.slice(0, MAX_SKILL_NAME_LEN - 1) + "\u2026"
95
+ ? `${name.slice(0, MAX_SKILL_NAME_LEN - 1)}\u2026`
96
96
  : name;
97
97
  }
98
98
 
@@ -282,7 +282,7 @@ const skillSetRenderer: ItemRenderer<SkillSetItem> = {
282
282
  const arrow = expanded ? "\u25BC" : "\u25B6";
283
283
  const displayName =
284
284
  skillSet.name.length > MAX_SET_NAME_LEN
285
- ? skillSet.name.slice(0, MAX_SET_NAME_LEN - 1) + "\u2026"
285
+ ? `${skillSet.name.slice(0, MAX_SET_NAME_LEN - 1)}\u2026`
286
286
  : skillSet.name;
287
287
 
288
288
  // Count installed child skills
@@ -1,46 +1,41 @@
1
- import React, {
2
- useEffect,
3
- useState,
4
- useCallback,
5
- useMemo,
6
- useRef,
7
- } from "react";
1
+ import { existsSync } from "node:fs";
2
+ import { readFile } from "node:fs/promises";
8
3
  import type { ScrollBoxRenderable } from "@opentui/core";
9
- import { useApp, useModal } from "../state/AppContext.js";
10
- import { useKeyboardHandler } from "../hooks/useKeyboardHandler.js";
11
- import { ScreenLayout, type FooterHint } from "../components/layout/index.js";
12
- import { FlagDetailEditor } from "../components/FlagDetailEditor.js";
4
+ import type React from "react";
5
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
13
6
  import {
14
7
  ALIAS_FLAGS,
15
- FLAG_GROUPS,
16
8
  type AliasFlag,
9
+ FLAG_GROUPS,
17
10
  } from "../../data/alias-flags.js";
18
- import {
19
- defaultAliasConfig,
20
- defaultValueFor,
21
- validateAliasName,
22
- derivedChannelValues,
23
- withoutDerivedChannel,
24
- CHANNELS_FLAG_ID,
25
- DEFAULT_ALIAS_NAME,
26
- type AliasConfig,
27
- type FlagValue,
28
- } from "../../services/alias-store.js";
29
11
  import { loadAliasName, saveAliasName } from "../../services/alias-settings.js";
30
12
  import {
13
+ type AdoptableAlias,
14
+ type FlagValidationIssue,
15
+ type Segment,
16
+ type ShellTarget,
31
17
  detectShells,
18
+ findAdoptableAlias,
19
+ parseAliasFromRc,
32
20
  renderArgs,
33
21
  validateConfig,
34
22
  writeAliasToShell,
35
- parseAliasFromRc,
36
- findAdoptableAlias,
37
- type ShellTarget,
38
- type FlagValidationIssue,
39
- type Segment,
40
- type AdoptableAlias,
41
23
  } from "../../services/alias-shell-writer.js";
42
- import { readFile } from "node:fs/promises";
43
- import { existsSync } from "node:fs";
24
+ import {
25
+ type AliasConfig,
26
+ CHANNELS_FLAG_ID,
27
+ DEFAULT_ALIAS_NAME,
28
+ type FlagValue,
29
+ defaultAliasConfig,
30
+ defaultValueFor,
31
+ derivedChannelValues,
32
+ validateAliasName,
33
+ withoutDerivedChannel,
34
+ } from "../../services/alias-store.js";
35
+ import { FlagDetailEditor } from "../components/FlagDetailEditor.js";
36
+ import { type FooterHint, ScreenLayout } from "../components/layout/index.js";
37
+ import { useKeyboardHandler } from "../hooks/useKeyboardHandler.js";
38
+ import { useApp, useModal } from "../state/AppContext.js";
44
39
  import { theme } from "../theme.js";
45
40
 
46
41
  interface FlagRow {
@@ -463,8 +458,7 @@ export function AliasScreen() {
463
458
  // enough today; recurse defensively in case a chain emerges).
464
459
  const queue = [id];
465
460
  const seen = new Set<string>();
466
- while (queue.length > 0) {
467
- const cur = queue.shift()!;
461
+ for (let cur = queue.shift(); cur !== undefined; cur = queue.shift()) {
468
462
  if (seen.has(cur)) continue;
469
463
  seen.add(cur);
470
464
  for (const dependent of ALIAS_FLAGS) {
@@ -1052,7 +1046,7 @@ function summarizeValue(value: FlagValue): string {
1052
1046
  }
1053
1047
 
1054
1048
  function truncate(s: string, n: number): string {
1055
- return s.length <= n ? s : s.slice(0, n - 1) + "…";
1049
+ return s.length <= n ? s : `${s.slice(0, n - 1)}…`;
1056
1050
  }
1057
1051
 
1058
1052
  /**
@@ -1447,6 +1441,7 @@ function renderPreviewLines(
1447
1441
  ];
1448
1442
  segments.forEach((seg, i) => {
1449
1443
  lines.push(
1444
+ // biome-ignore lint/suspicious/noArrayIndexKey: alias arguments are positional by definition and identical tokens can legitimately repeat; the preview re-renders whole
1450
1445
  <text key={`arg-${i}`} fg={theme.colors.text}>
1451
1446
  {" "}
1452
1447
  {renderPreviewSegment(seg)}
@@ -1472,12 +1467,14 @@ function renderPreviewSegment(seg: Segment): React.ReactNode {
1472
1467
  seg.parts.forEach((p, i) => {
1473
1468
  if (p.kind === "literal") {
1474
1469
  spans.push(
1470
+ // biome-ignore lint/suspicious/noArrayIndexKey: parts of one composite argument are positional text fragments with no identity of their own
1475
1471
  <span key={`p-${i}`} fg={theme.colors.text}>
1476
1472
  {p.text}
1477
1473
  </span>,
1478
1474
  );
1479
1475
  } else {
1480
1476
  spans.push(
1477
+ // biome-ignore lint/suspicious/noArrayIndexKey: same positional fragment list as above
1481
1478
  <span key={`p-${i}`} fg={theme.colors.info}>
1482
1479
  {substitutionToToken(p.posix)}
1483
1480
  </span>,
@@ -1,10 +1,10 @@
1
+ import { exec, execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
1
3
  import React, { useEffect, useCallback, useState, useRef } from "react";
2
- import { exec, execFile } from "child_process";
3
- import { promisify } from "util";
4
+ import { ScreenLayout } from "../components/layout/index.js";
5
+ import { useKeyboard } from "../hooks/useKeyboard.js";
4
6
  import { useApp, useModal } from "../state/AppContext.js";
5
7
  import { useDimensions } from "../state/DimensionsContext.js";
6
- import { useKeyboard } from "../hooks/useKeyboard.js";
7
- import { ScreenLayout } from "../components/layout/index.js";
8
8
 
9
9
  import { cliTools } from "../../data/cli-tools.js";
10
10
  import {
@@ -15,12 +15,12 @@ import {
15
15
  } from "../../services/cli-tool-commands.js";
16
16
  import type { Command } from "../../utils/command-utils.js";
17
17
  import { formatCommand } from "../../utils/command-utils.js";
18
+ import { ScrollableList } from "../components/ScrollableList.js";
18
19
  import {
19
- renderCliToolRow,
20
- renderCliToolDetail,
21
20
  type CliToolStatus,
21
+ renderCliToolDetail,
22
+ renderCliToolRow,
22
23
  } from "../renderers/cliToolRenderers.js";
23
- import { ScrollableList } from "../components/ScrollableList.js";
24
24
  import { theme } from "../theme.js";
25
25
 
26
26
  const execAsync = promisify(exec);
@@ -123,9 +123,10 @@ async function detectInstallMethods(
123
123
  if (method && !methods.includes(method)) methods.push(method);
124
124
  }
125
125
 
126
- if (methods.length === 0)
126
+ const primary = methods[0];
127
+ if (primary === undefined)
127
128
  return { primary: fallback as InstallMethod, all: [] };
128
- return { primary: methods[0]!, all: methods, brewFormula };
129
+ return { primary, all: methods, brewFormula };
129
130
  } catch {
130
131
  return { primary: fallback as InstallMethod, all: [] };
131
132
  }
@@ -212,8 +213,8 @@ async function getLatestVersion(
212
213
  }
213
214
 
214
215
  function compareVersions(v1: string, v2: string): number {
215
- const parts1 = v1.split(/[-.]/).map((p) => parseInt(p, 10) || 0);
216
- const parts2 = v2.split(/[-.]/).map((p) => parseInt(p, 10) || 0);
216
+ const parts1 = v1.split(/[-.]/).map((p) => Number.parseInt(p, 10) || 0);
217
+ const parts2 = v2.split(/[-.]/).map((p) => Number.parseInt(p, 10) || 0);
217
218
  for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
218
219
  const p1 = parts1[i] || 0;
219
220
  const p2 = parts2[i] || 0;
@@ -250,7 +251,7 @@ export function CliToolsScreen() {
250
251
  (index: number, updates: Partial<CliToolStatus>) => {
251
252
  setToolStatuses((prev) => {
252
253
  const newStatuses = [...prev];
253
- newStatuses[index] = { ...newStatuses[index]!, ...updates };
254
+ newStatuses[index] = { ...newStatuses[index], ...updates };
254
255
  cachedToolStatuses = newStatuses;
255
256
  return newStatuses;
256
257
  });
@@ -260,7 +261,7 @@ export function CliToolsScreen() {
260
261
 
261
262
  const fetchVersionInfo = useCallback(async () => {
262
263
  for (let i = 0; i < cliTools.length; i++) {
263
- const tool = cliTools[i]!;
264
+ const tool = cliTools[i];
264
265
 
265
266
  // Run all checks in parallel, then update with all results
266
267
  Promise.all([
@@ -1,23 +1,23 @@
1
1
  import React, { useEffect, useCallback, useMemo } from "react";
2
- import { useApp, useModal } from "../state/AppContext.js";
3
- import { useDimensions } from "../state/DimensionsContext.js";
4
- import { useKeyboard } from "../hooks/useKeyboard.js";
5
- import { ScreenLayout } from "../components/layout/index.js";
6
2
  import { SETTINGS_CATALOG } from "../../data/settings-catalog.js";
7
3
  import {
4
+ discoverOutputStyles,
8
5
  readAllSettingsBothScopes,
9
6
  writeSettingValue,
10
- discoverOutputStyles,
11
7
  } from "../../services/settings-manager.js";
12
8
  import {
13
- buildSettingsBrowserItems,
14
9
  type SettingsBrowserItem,
10
+ buildSettingsBrowserItems,
15
11
  } from "../adapters/settingsAdapter.js";
12
+ import { ScrollableList } from "../components/ScrollableList.js";
13
+ import { ScreenLayout } from "../components/layout/index.js";
14
+ import { useKeyboard } from "../hooks/useKeyboard.js";
16
15
  import {
17
- renderSettingRow,
18
16
  renderSettingDetail,
17
+ renderSettingRow,
19
18
  } from "../renderers/settingsRenderers.js";
20
- import { ScrollableList } from "../components/ScrollableList.js";
19
+ import { useApp, useModal } from "../state/AppContext.js";
20
+ import { useDimensions } from "../state/DimensionsContext.js";
21
21
  import { theme } from "../theme.js";
22
22
 
23
23
  export function SettingsScreen() {
@@ -1,24 +1,25 @@
1
- import React, { useEffect, useState, useCallback, useMemo } from "react";
2
1
  import { spawn } from "node:child_process";
3
2
  import { appendFileSync } from "node:fs";
4
3
  import { useRenderer } from "@opentui/react";
5
- import { useApp, useModal } from "../state/AppContext.js";
6
- import { useKeyboardHandler } from "../hooks/useKeyboardHandler.js";
7
- import { ScreenLayout } from "../components/layout/index.js";
4
+ import type React from "react";
5
+ import { useCallback, useEffect, useMemo, useState } from "react";
6
+ import { getGitignoreRuleReason } from "../../data/gitignore-reasons.js";
7
+ import { BUILTIN_TEMPLATES } from "../../data/gitignore-templates.js";
8
8
  import {
9
- loadGitignoreState,
9
+ type GitignoreState,
10
10
  applyDestructiveFix,
11
- ensureProjectManifestForEdit,
12
11
  applyTemplate,
13
- type GitignoreState,
12
+ ensureProjectManifestForEdit,
13
+ loadGitignoreState,
14
14
  } from "../../services/gitignore-service.js";
15
- import { BUILTIN_TEMPLATES } from "../../data/gitignore-templates.js";
16
- import { getGitignoreRuleReason } from "../../data/gitignore-reasons.js";
15
+ import type { ResolvedRule, Tier, Violation } from "../../types/gitignore.js";
16
+ import { ScreenLayout } from "../components/layout/index.js";
17
+ import { useKeyboardHandler } from "../hooks/useKeyboardHandler.js";
17
18
  import {
18
- renderRuleRow,
19
19
  type RuleRow,
20
+ renderRuleRow,
20
21
  } from "../renderers/gitignoreRenderers.js";
21
- import type { ResolvedRule, Tier, Violation } from "../../types/gitignore.js";
22
+ import { useApp, useModal } from "../state/AppContext.js";
22
23
  import { theme } from "../theme.js";
23
24
 
24
25
  // Clean rows are grouped by the manifest that defined them. Order = display
@@ -229,9 +230,7 @@ export function GitignoreScreen() {
229
230
  : "";
230
231
  await modal.message(
231
232
  "Template applied",
232
- `Wrote ${r.appliedTo}\n\n` +
233
- `Added ${r.added.ignore.length} ignore + ${r.added.track.length} track entries.` +
234
- conflictNote,
233
+ `Wrote ${r.appliedTo}\n\nAdded ${r.added.ignore.length} ignore + ${r.added.track.length} track entries.${conflictNote}`,
235
234
  "success",
236
235
  );
237
236
  } catch (err) {
@@ -436,6 +435,7 @@ function renderDetail(
436
435
  <box flexDirection="column" marginTop={1}>
437
436
  <text fg={theme.colors.warning}>Warnings:</text>
438
437
  {warnings.map((w, i) => (
438
+ // biome-ignore lint/suspicious/noArrayIndexKey: warnings are plain strings that may repeat and are replaced as a whole list; the index is the only stable key
439
439
  <text fg={theme.colors.muted} key={i}>
440
440
  {w}
441
441
  </text>
@@ -552,6 +552,7 @@ function renderWarnings(warnings: string[]): React.ReactNode {
552
552
  <box flexDirection="column" marginTop={1}>
553
553
  <text fg={theme.colors.warning}>Warnings:</text>
554
554
  {warnings.map((w, i) => (
555
+ // biome-ignore lint/suspicious/noArrayIndexKey: warnings are plain strings that may repeat and are replaced as a whole list; the index is the only stable key
555
556
  <text fg={theme.colors.muted} key={i}>
556
557
  {w}
557
558
  </text>
@@ -1,11 +1,11 @@
1
1
  import React, { useEffect, useCallback, useState, useRef } from "react";
2
+ import { ScreenLayout } from "../components/layout/index.js";
3
+ import { useKeyboard } from "../hooks/useKeyboard.js";
2
4
  import { useApp, useModal, useNavigation } 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
- import { searchMcpServers, formatDate } from "../../services/mcp-registry.js";
8
7
  import { addMcpServer, setAllowMcp } from "../../services/claude-settings.js";
8
+ import { formatDate, searchMcpServers } from "../../services/mcp-registry.js";
9
9
  import type { McpRegistryServer, McpServerConfig } from "../../types/index.js";
10
10
  import { ScrollableList } from "../components/ScrollableList.js";
11
11
  import { theme } from "../theme.js";
@@ -43,8 +43,8 @@ function deduplicateServers(servers: McpRegistryServer[]): McpRegistryServer[] {
43
43
  function compareVersions(a: McpRegistryServer, b: McpRegistryServer): number {
44
44
  // Try semver comparison first
45
45
  if (a.version && b.version) {
46
- const aParts = a.version.split(".").map((n) => parseInt(n, 10) || 0);
47
- const bParts = b.version.split(".").map((n) => parseInt(n, 10) || 0);
46
+ const aParts = a.version.split(".").map((n) => Number.parseInt(n, 10) || 0);
47
+ const bParts = b.version.split(".").map((n) => Number.parseInt(n, 10) || 0);
48
48
 
49
49
  for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
50
50
  const aVal = aParts[i] || 0;
@@ -120,14 +120,17 @@ export function McpRegistryScreen() {
120
120
  [loadServers],
121
121
  );
122
122
 
123
+ // Initial load runs once with the query the screen opened with; later edits
124
+ // go through debouncedSearch, so the live query is deliberately not a dep.
125
+ const initialQueryRef = useRef(searchQuery);
123
126
  useEffect(() => {
124
- loadServers(searchQuery);
127
+ loadServers(initialQueryRef.current);
125
128
  return () => {
126
129
  if (searchTimeoutRef.current) {
127
130
  clearTimeout(searchTimeoutRef.current);
128
131
  }
129
132
  };
130
- }, []);
133
+ }, [loadServers]);
131
134
 
132
135
  // Keyboard handling
133
136
  useKeyboard((event) => {
@@ -1,28 +1,28 @@
1
1
  import React, { useEffect, useCallback, useMemo } from "react";
2
+ import { ScreenLayout } from "../components/layout/index.js";
3
+ import { useKeyboard } from "../hooks/useKeyboard.js";
2
4
  import { useApp, useModal, useNavigation } 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
- getMcpServersByCategory,
9
- getCategoryDisplayName,
10
8
  categoryOrder,
9
+ getCategoryDisplayName,
10
+ getMcpServersByCategory,
11
11
  } from "../../data/mcp-servers.js";
12
- import { fetchCuratedMcpServers } from "../../services/mcp-registry.js";
13
12
  import {
14
13
  addMcpServer,
15
- removeMcpServer,
16
- getInstalledMcpServers,
17
14
  getEnabledMcpServers,
15
+ getInstalledMcpServers,
16
+ removeMcpServer,
18
17
  } from "../../services/claude-settings.js";
18
+ import { fetchCuratedMcpServers } from "../../services/mcp-registry.js";
19
19
  import type { McpServer, McpServerConfig } from "../../types/index.js";
20
+ import { ScrollableList } from "../components/ScrollableList.js";
20
21
  import {
21
- renderMcpRow,
22
- renderMcpDetail,
23
22
  type McpListItem,
23
+ renderMcpDetail,
24
+ renderMcpRow,
24
25
  } from "../renderers/mcpRenderers.js";
25
- import { ScrollableList } from "../components/ScrollableList.js";
26
26
 
27
27
  export function McpScreen() {
28
28
  const { state, dispatch } = useApp();