claudeup 4.19.0 → 4.23.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 (182) hide show
  1. package/bin/claudeup.js +43 -19
  2. package/package.json +14 -19
  3. package/scripts/build-binaries.ts +77 -0
  4. package/scripts/check-optional-deps.ts +49 -0
  5. package/scripts/test-isolated.ts +43 -0
  6. package/src/__tests__/cli-router.test.ts +65 -0
  7. package/src/__tests__/conventions-integration.test.ts +745 -0
  8. package/src/__tests__/conventions-manager.test.ts +1172 -0
  9. package/src/__tests__/doctor-bins.test.ts +35 -0
  10. package/src/__tests__/doctor.test.ts +450 -0
  11. package/src/__tests__/dual-write-prevention.test.ts +14 -4
  12. package/src/__tests__/file-locking.test.ts +208 -0
  13. package/src/__tests__/gitignore-prerun.test.ts +1 -0
  14. package/src/__tests__/install-command.test.ts +81 -0
  15. package/src/__tests__/install-plan.test.ts +98 -0
  16. package/src/__tests__/manifest.test.ts +136 -0
  17. package/src/__tests__/marketplace-badge.test.ts +91 -0
  18. package/src/__tests__/marketplace-refresh.test.ts +267 -0
  19. package/src/__tests__/plugin-requires.test.ts +133 -0
  20. package/src/__tests__/plugin-setup.test.ts +7 -0
  21. package/src/__tests__/profile-command.test.ts +116 -0
  22. package/src/__tests__/profile-materializer.test.ts +82 -0
  23. package/src/__tests__/profile-sync.test.ts +136 -0
  24. package/src/__tests__/registry-version-resolution.test.ts +74 -0
  25. package/src/__tests__/resolve-executable.test.ts +42 -0
  26. package/src/__tests__/resolver.test.ts +218 -0
  27. package/src/__tests__/symlink-manager.test.ts +99 -0
  28. package/src/__tests__/toolchain.test.ts +56 -0
  29. package/src/cli/claude.ts +21 -0
  30. package/src/cli/doctor.ts +132 -0
  31. package/src/cli/install.ts +360 -0
  32. package/src/cli/profile.ts +166 -0
  33. package/src/cli/router.ts +107 -0
  34. package/src/cli/update.ts +85 -0
  35. package/src/data/cli-tools.ts +7 -7
  36. package/src/data/gitignore-defaults.ts +4 -0
  37. package/src/data/marketplaces.ts +12 -0
  38. package/src/data/predefined-profiles.ts +3 -4
  39. package/src/main.tsx +11 -154
  40. package/src/prerunner/index.ts +62 -27
  41. package/src/services/claude-cli.ts +19 -22
  42. package/src/services/claude-settings.ts +99 -26
  43. package/src/services/conventions-manager.ts +865 -0
  44. package/src/services/doctor-bins.ts +49 -0
  45. package/src/services/doctor.ts +509 -0
  46. package/src/services/install-plan.ts +107 -0
  47. package/src/services/manifest.ts +166 -0
  48. package/src/services/marketplace-refresh.ts +173 -0
  49. package/src/services/plugin-manager.ts +7 -1
  50. package/src/services/plugin-requires.ts +215 -0
  51. package/src/services/plugin-version-check.ts +9 -4
  52. package/src/services/profile-materializer.ts +61 -0
  53. package/src/services/profile-sync.ts +150 -0
  54. package/src/services/resolver.ts +293 -0
  55. package/src/services/symlink-manager.ts +146 -0
  56. package/src/services/toolchain.ts +97 -0
  57. package/src/services/version-check.ts +10 -11
  58. package/src/types/index.ts +138 -31
  59. package/src/ui/App.tsx +0 -4
  60. package/src/ui/adapters/pluginsAdapter.ts +13 -0
  61. package/src/ui/screens/CliToolsScreen.tsx +11 -3
  62. package/src/ui/screens/index.ts +0 -1
  63. package/src/ui/state/reducer.ts +0 -101
  64. package/src/ui/state/types.ts +0 -35
  65. package/src/utils/command-utils.ts +23 -0
  66. package/src/utils/file-locking.ts +144 -0
  67. package/src/data/alias-flags.js +0 -205
  68. package/src/data/cli-tools.js +0 -123
  69. package/src/data/gitignore-defaults.js +0 -24
  70. package/src/data/gitignore-reasons.js +0 -97
  71. package/src/data/gitignore-templates.js +0 -21
  72. package/src/data/marketplaces.js +0 -138
  73. package/src/data/mcp-servers.js +0 -509
  74. package/src/data/predefined-profiles.js +0 -248
  75. package/src/data/settings-catalog.js +0 -625
  76. package/src/data/skill-repos.js +0 -160
  77. package/src/data/statuslines.js +0 -159
  78. package/src/data/statuslines.ts +0 -188
  79. package/src/index.js +0 -4
  80. package/src/index.ts +0 -5
  81. package/src/main.js +0 -170
  82. package/src/prerunner/index.js +0 -252
  83. package/src/services/alias-settings.js +0 -51
  84. package/src/services/alias-shell-writer.js +0 -1018
  85. package/src/services/alias-store.js +0 -129
  86. package/src/services/claude-cli.js +0 -189
  87. package/src/services/claude-runner.js +0 -28
  88. package/src/services/claude-settings.js +0 -1223
  89. package/src/services/gitignore-detector.js +0 -155
  90. package/src/services/gitignore-fixer.js +0 -231
  91. package/src/services/gitignore-prerun.js +0 -46
  92. package/src/services/gitignore-resolver.js +0 -143
  93. package/src/services/gitignore-service.js +0 -117
  94. package/src/services/local-marketplace.js +0 -339
  95. package/src/services/marketplace-fetcher.js +0 -96
  96. package/src/services/marketplace-sync.js +0 -94
  97. package/src/services/mcp-registry.js +0 -87
  98. package/src/services/plugin-manager.js +0 -473
  99. package/src/services/plugin-mcp-config.js +0 -177
  100. package/src/services/plugin-setup.js +0 -499
  101. package/src/services/plugin-version-check.js +0 -262
  102. package/src/services/profiles.js +0 -161
  103. package/src/services/settings-manager.js +0 -243
  104. package/src/services/skills-manager.js +0 -393
  105. package/src/services/skillsmp-client.js +0 -87
  106. package/src/services/update-cache.js +0 -52
  107. package/src/services/version-check.js +0 -99
  108. package/src/types/gitignore.js +0 -6
  109. package/src/types/index.js +0 -1
  110. package/src/ui/App.js +0 -355
  111. package/src/ui/adapters/pluginsAdapter.js +0 -139
  112. package/src/ui/adapters/settingsAdapter.js +0 -111
  113. package/src/ui/adapters/skillsAdapter.js +0 -154
  114. package/src/ui/components/CategoryHeader.js +0 -9
  115. package/src/ui/components/EmptyFilterState.js +0 -4
  116. package/src/ui/components/FlagDetailEditor.js +0 -0
  117. package/src/ui/components/ScopeIndicator.js +0 -30
  118. package/src/ui/components/ScrollableList.js +0 -36
  119. package/src/ui/components/SearchInput.js +0 -19
  120. package/src/ui/components/StyledText.js +0 -39
  121. package/src/ui/components/TabBar.js +0 -19
  122. package/src/ui/components/layout/FooterHints.js +0 -29
  123. package/src/ui/components/layout/Panel.js +0 -6
  124. package/src/ui/components/layout/ProgressBar.js +0 -14
  125. package/src/ui/components/layout/ScopeTabs.js +0 -6
  126. package/src/ui/components/layout/ScreenLayout.js +0 -16
  127. package/src/ui/components/layout/index.js +0 -5
  128. package/src/ui/components/modals/ConfirmModal.js +0 -14
  129. package/src/ui/components/modals/InputModal.js +0 -5
  130. package/src/ui/components/modals/LoadingModal.js +0 -17
  131. package/src/ui/components/modals/MessageModal.js +0 -16
  132. package/src/ui/components/modals/ModalContainer.js +0 -137
  133. package/src/ui/components/modals/SelectModal.js +0 -18
  134. package/src/ui/components/modals/VersionMismatchModal.js +0 -36
  135. package/src/ui/components/modals/index.js +0 -6
  136. package/src/ui/components/primitives/ActionHints.js +0 -13
  137. package/src/ui/components/primitives/DetailSection.js +0 -7
  138. package/src/ui/components/primitives/KeyValueLine.js +0 -8
  139. package/src/ui/components/primitives/ListCategoryRow.js +0 -8
  140. package/src/ui/components/primitives/MetaText.js +0 -8
  141. package/src/ui/components/primitives/ScopeDetail.js +0 -32
  142. package/src/ui/components/primitives/ScopeSquares.js +0 -11
  143. package/src/ui/components/primitives/SelectableRow.js +0 -5
  144. package/src/ui/components/primitives/index.js +0 -8
  145. package/src/ui/hooks/index.js +0 -4
  146. package/src/ui/hooks/useAsyncData.js +0 -77
  147. package/src/ui/hooks/useGitignoreModal.js +0 -74
  148. package/src/ui/hooks/useKeyboard.js +0 -13
  149. package/src/ui/hooks/useKeyboardHandler.js +0 -39
  150. package/src/ui/hooks/useMismatchModal.js +0 -77
  151. package/src/ui/registry.js +0 -1
  152. package/src/ui/renderers/cliToolRenderers.js +0 -54
  153. package/src/ui/renderers/gitignoreRenderers.js +0 -46
  154. package/src/ui/renderers/mcpRenderers.js +0 -26
  155. package/src/ui/renderers/pluginRenderers.js +0 -124
  156. package/src/ui/renderers/profileRenderers.js +0 -177
  157. package/src/ui/renderers/settingsRenderers.js +0 -73
  158. package/src/ui/renderers/skillRenderers.js +0 -138
  159. package/src/ui/screens/AliasScreen.js +0 -1111
  160. package/src/ui/screens/CliToolsScreen.js +0 -338
  161. package/src/ui/screens/EnvVarsScreen.js +0 -152
  162. package/src/ui/screens/GitignoreScreen.js +0 -328
  163. package/src/ui/screens/McpRegistryScreen.js +0 -238
  164. package/src/ui/screens/McpScreen.js +0 -176
  165. package/src/ui/screens/ModelSelectorScreen.js +0 -296
  166. package/src/ui/screens/ModelSelectorScreen.tsx +0 -444
  167. package/src/ui/screens/PluginsScreen.js +0 -769
  168. package/src/ui/screens/ProfilesScreen.js +0 -298
  169. package/src/ui/screens/SkillsScreen.js +0 -549
  170. package/src/ui/screens/StatusLineScreen.js +0 -206
  171. package/src/ui/screens/StatusLineScreen.tsx +0 -416
  172. package/src/ui/screens/index.js +0 -10
  173. package/src/ui/state/AnimationContext.js +0 -34
  174. package/src/ui/state/AppContext.js +0 -162
  175. package/src/ui/state/DimensionsContext.js +0 -71
  176. package/src/ui/state/reducer.js +0 -547
  177. package/src/ui/state/types.js +0 -1
  178. package/src/ui/theme.js +0 -47
  179. package/src/utils/clipboard.js +0 -56
  180. package/src/utils/command-utils.js +0 -19
  181. package/src/utils/fuzzy-search.js +0 -101
  182. package/src/utils/string-utils.js +0 -62
@@ -1,7 +1,5 @@
1
- import { createRequire } from "node:module";
2
1
  import semver from "semver";
3
-
4
- const require = createRequire(import.meta.url);
2
+ import pkg from "../../package.json";
5
3
 
6
4
  // Cache the result for the session
7
5
  let cachedResult: VersionCheckResult | null = null;
@@ -14,15 +12,12 @@ export interface VersionCheckResult {
14
12
  }
15
13
 
16
14
  /**
17
- * Get the current version from package.json
15
+ * Get the current version from package.json.
16
+ * Static import so it resolves inside a `bun build --compile` binary — a
17
+ * dynamic require("../../package.json") returns no `.version` there.
18
18
  */
19
19
  export function getCurrentVersion(): string {
20
- try {
21
- const pkg = require("../../package.json");
22
- return pkg.version;
23
- } catch {
24
- return "0.0.0";
25
- }
20
+ return (pkg as { version?: string }).version ?? "0.0.0";
26
21
  }
27
22
 
28
23
  /**
@@ -97,7 +92,11 @@ export async function checkForUpdates(): Promise<VersionCheckResult> {
97
92
  return cachedResult;
98
93
  }
99
94
 
100
- const updateAvailable = semver.gt(latestVersion, currentVersion);
95
+ // Guard: never let a malformed version crash the update check.
96
+ const updateAvailable =
97
+ semver.valid(latestVersion) != null &&
98
+ semver.valid(currentVersion) != null &&
99
+ semver.gt(latestVersion, currentVersion);
101
100
 
102
101
  cachedResult = {
103
102
  currentVersion,
@@ -60,12 +60,6 @@ export interface Plugin {
60
60
  hasUpdate?: boolean;
61
61
  }
62
62
 
63
- export interface StatusLineConfig {
64
- name: string;
65
- description: string;
66
- template: string;
67
- }
68
-
69
63
  export interface MarketplaceSource {
70
64
  source: {
71
65
  source: "github";
@@ -250,6 +244,35 @@ export interface InstalledPluginsRegistry {
250
244
  plugins: Record<string, InstalledPluginEntry[]>;
251
245
  }
252
246
 
247
+ // ─── Plugin Conventions (declarative project hygiene) ───────────────────────────
248
+
249
+ /** Convention declarations for a plugin */
250
+ export interface PluginConventions {
251
+ gitignore?: GitignoreConventions;
252
+ claudemd?: ClaudeMdConventions;
253
+ }
254
+
255
+ /** Gitignore entries a plugin needs */
256
+ export interface GitignoreConventions {
257
+ /** Entries for project .gitignore (team-visible, committed) */
258
+ project?: string[];
259
+ /** Advisory entries for global gitignore (personal, not committed).
260
+ * Resolved via `git config --global core.excludesfile`,
261
+ * falling back to ~/.config/git/ignore if unset. */
262
+ global?: string[];
263
+ }
264
+
265
+ /** CLAUDE.md section injection config */
266
+ export interface ClaudeMdConventions {
267
+ /** Section identifier used in markers: <!-- BEGIN magus:{section} -->
268
+ * MUST equal the plugin's `name` field. */
269
+ section: string;
270
+ /** Path to markdown template, relative to plugin root */
271
+ template: string;
272
+ /** Semantic version of the template content */
273
+ version: string;
274
+ }
275
+
253
276
  // ─── Plugin Profile Types ──────────────────────────────────────────────────────
254
277
 
255
278
  export interface Profile {
@@ -274,35 +297,119 @@ export interface ProfileEntry {
274
297
  scope: "user" | "project";
275
298
  }
276
299
 
277
- // ─── Predefined Profile Types ──────────────────────────────────────────────────
300
+ // NOTE: The built-in profile type lives with its data in
301
+ // data/predefined-profiles.ts (that file declares and exports its own
302
+ // PredefinedProfile). A second, unused definition previously lived here and
303
+ // has been removed to avoid two competing shapes.
304
+
305
+ // ─── Team Profile Manifest (v2, committed .claude/profiles.json) ────────────────
306
+ //
307
+ // The v1 `Profile`/`ProfilesFile` above are the TUI's saved presets (plugins
308
+ // only). The v2 manifest below is the committed, team-shared source of truth:
309
+ // a superset that also carries marketplaces, MCP servers, CLI tools, skills,
310
+ // settings, and env requirements. A v1 profile is a structural subset of a v2
311
+ // entry, so v1 files migrate forward by a version bump alone.
312
+
313
+ /** A marketplace a profile depends on (registered before its plugins install). */
314
+ export interface ProfileMarketplaceRef {
315
+ source: "github" | "directory";
316
+ /** owner/repo — for source: "github" */
317
+ repo?: string;
318
+ /** absolute or repo-relative path — for source: "directory" */
319
+ path?: string;
320
+ }
278
321
 
279
- /** A skill reference for predefined profiles */
280
- export interface PredefinedSkill {
322
+ /** A skill a profile installs (resolved from a GitHub repo + path). */
323
+ export interface ProfileSkillRef {
281
324
  name: string;
282
325
  repo: string;
283
- skillPath: string;
284
- }
285
-
286
- /** Settings that can be configured in a predefined profile */
287
- export interface PredefinedSettings {
288
- effortLevel?: "low" | "medium" | "high";
289
- alwaysThinkingEnabled?: boolean;
290
- model?: "claude-sonnet-4-6" | "claude-opus-4-6";
291
- outputStyle?: "concise" | "explanatory" | "formal";
292
- CLAUDE_CODE_ENABLE_TASKS?: boolean;
293
- CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS?: boolean;
294
- includeGitInstructions?: boolean;
295
- respectGitignore?: boolean;
296
- enableAllProjectMcpServers?: boolean;
326
+ path: string;
297
327
  }
298
328
 
299
- /** A predefined (built-in) profile for claudeup */
300
- export interface PredefinedProfile {
301
- id: string;
329
+ /** Environment variables a profile declares (values live in settings.local.json). */
330
+ export interface ProfileEnvRequirements {
331
+ required?: string[];
332
+ optional?: string[];
333
+ }
334
+
335
+ /**
336
+ * A single team profile in .claude/profiles.json (v2).
337
+ * `plugins` maps a plugin id to a version pin ("1.2.0"), "latest", or `true`
338
+ * (equivalent to "latest" — the shape v1 saved profiles use).
339
+ */
340
+ export interface ProfileManifestEntry {
302
341
  name: string;
303
- description: string;
304
- targetAudience: string;
305
- plugins: Record<string, boolean>;
306
- skills: PredefinedSkill[];
307
- settings: PredefinedSettings;
342
+ /** Optional predefined-profile id to inherit plugins/skills/settings from. */
343
+ extends?: string;
344
+ marketplaces?: Record<string, ProfileMarketplaceRef>;
345
+ plugins?: Record<string, string | boolean>;
346
+ mcpServers?: Record<string, McpServerConfig>;
347
+ /** cliTool name -> version pin | "latest". */
348
+ cliTools?: Record<string, string>;
349
+ skills?: ProfileSkillRef[];
350
+ settings?: Record<string, unknown>;
351
+ env?: ProfileEnvRequirements;
352
+ /** Optional bookkeeping carried over from v1 saved profiles. */
353
+ createdAt?: string;
354
+ updatedAt?: string;
355
+ }
356
+
357
+ /** The .claude/profiles.json manifest (v2). */
358
+ export interface ProfileManifest {
359
+ version: 2;
360
+ /** When true, `install --check` fails on any plugin version drift. */
361
+ strictVersions?: boolean;
362
+ profiles: Record<string, ProfileManifestEntry>;
363
+ }
364
+
365
+ // ─── Plugin binary requirements (plugin.json `requires.bin`) ────────────────────
366
+
367
+ /** How a binary dependency is installed. */
368
+ export type BinInstaller = "bun" | "brew" | "npm" | "pip" | "go";
369
+
370
+ /** A single binary dependency a plugin (or profile) declares. */
371
+ export interface BinRequirement {
372
+ /** The executable name expected on PATH (also the dedup key). */
373
+ name: string;
374
+ via: BinInstaller;
375
+ /** Package id for bun/npm/pip (defaults to `name`). */
376
+ package?: string;
377
+ /** Formula for brew (defaults to `name`). */
378
+ formula?: string;
379
+ /** Module path for `go install` (defaults to `name`). */
380
+ module?: string;
381
+ /** Command that verifies presence, e.g. "tmux -V" (defaults to `<name> --version`). */
382
+ check?: string;
383
+ /** Optional version pin (real for bun/npm/pip; advisory for brew/go). */
384
+ version?: string;
385
+ }
386
+
387
+ /** The `requires` block a plugin may declare in its plugin.json. */
388
+ export interface PluginRequires {
389
+ bin?: BinRequirement[];
390
+ }
391
+
392
+ // ─── Resolved install closure (resolver output) ─────────────────────────────────
393
+
394
+ /** A binary requirement after dedup + pin precedence, with provenance. */
395
+ export interface ResolvedBin extends BinRequirement {
396
+ /** Where this requirement came from, for diagnostics. */
397
+ sources: string[];
398
+ }
399
+
400
+ /**
401
+ * The fully-resolved set of things to install for a profile (or the union of
402
+ * all profiles). Produced by the resolver; consumed by `claudeup install`.
403
+ */
404
+ export interface ResolvedClosure {
405
+ marketplaces: Record<string, ProfileMarketplaceRef>;
406
+ /** pluginId -> version pin | "latest". */
407
+ plugins: Record<string, string>;
408
+ mcpServers: Record<string, McpServerConfig>;
409
+ bins: ResolvedBin[];
410
+ skills: ProfileSkillRef[];
411
+ settings: Record<string, unknown>;
412
+ env: { required: string[]; optional: string[] };
413
+ /** Cross-profile version conflicts detected while unioning profiles. */
414
+ conflicts?: string[];
308
415
  }
package/src/ui/App.tsx CHANGED
@@ -18,7 +18,6 @@ import {
18
18
  McpRegistryScreen,
19
19
  SettingsScreen,
20
20
  CliToolsScreen,
21
- ModelSelectorScreen,
22
21
  ProfilesScreen,
23
22
  SkillsScreen,
24
23
  GitignoreScreen,
@@ -72,8 +71,6 @@ function Router() {
72
71
  return <SettingsScreen />;
73
72
  case "cli-tools":
74
73
  return <CliToolsScreen />;
75
- case "model-selector":
76
- return <ModelSelectorScreen />;
77
74
  case "profiles":
78
75
  return <ProfilesScreen />;
79
76
  case "skills":
@@ -139,7 +136,6 @@ function GlobalKeyHandler({
139
136
  "mcp-registry",
140
137
  "settings",
141
138
  "cli-tools",
142
- "model-selector",
143
139
  "profiles",
144
140
  "skills",
145
141
  "gitignore",
@@ -65,9 +65,22 @@ function categoryStyling(
65
65
  if (mp.official) {
66
66
  return { tone: "yellow", badge: "★ Official" };
67
67
  }
68
+ if (isMadAppGang(mp)) {
69
+ return { tone: "green", badge: "made with ❤️ by MadAppGang" };
70
+ }
68
71
  return { tone: "green", badge: "✓ Added" };
69
72
  }
70
73
 
74
+ /**
75
+ * Our own marketplaces (magus, magus-marketing, magus-alpha) get the byline
76
+ * instead of the generic "Added". Matched on the repo owner rather than a list
77
+ * of names, so a new channel picks it up without a code change. Same idiom the
78
+ * marketplace sort uses to float MadAppGang entries to the top.
79
+ */
80
+ function isMadAppGang(mp: Marketplace): boolean {
81
+ return (mp.source.repo ?? "").toLowerCase().startsWith("madappgang/");
82
+ }
83
+
71
84
  /**
72
85
  * Builds the flat list of items for the PluginsScreen list panel.
73
86
  * Extracted from PluginsScreen so it can be tested independently.
@@ -171,9 +171,17 @@ async function getLatestPipVersion(packageName: string): Promise<string | undefi
171
171
  async function getLatestVersion(
172
172
  tool: import("../../data/cli-tools.js").CliTool,
173
173
  ): Promise<string | undefined> {
174
- return tool.packageManager === "npm"
175
- ? getLatestNpmVersion(tool.packageName)
176
- : getLatestPipVersion(tool.packageName);
174
+ switch (tool.packageManager) {
175
+ // bun packages are published to the npm registry, so `npm view` resolves them.
176
+ case "bun":
177
+ case "npm":
178
+ return getLatestNpmVersion(tool.packageName);
179
+ case "pip":
180
+ return getLatestPipVersion(tool.packageName);
181
+ // brew/go have no uniform remote version query here — skip the check.
182
+ default:
183
+ return undefined;
184
+ }
177
185
  }
178
186
 
179
187
  function compareVersions(v1: string, v2: string): number {
@@ -3,7 +3,6 @@ export { McpScreen } from "./McpScreen.js";
3
3
  export { McpRegistryScreen } from "./McpRegistryScreen.js";
4
4
  export { SettingsScreen } from "./EnvVarsScreen.js";
5
5
  export { CliToolsScreen } from "./CliToolsScreen.js";
6
- export { ModelSelectorScreen } from "./ModelSelectorScreen.js";
7
6
  export { ProfilesScreen } from "./ProfilesScreen.js";
8
7
  export { SkillsScreen } from "./SkillsScreen.js";
9
8
  export { GitignoreScreen } from "./GitignoreScreen.js";
@@ -38,13 +38,6 @@ export const initialState: AppState = {
38
38
  servers: { status: "idle" },
39
39
  },
40
40
 
41
- statusline: {
42
- scope: "project",
43
- selectedIndex: 0,
44
- presets: { status: "idle" },
45
- currentPreset: null,
46
- },
47
-
48
41
  settings: {
49
42
  selectedIndex: 0,
50
43
  values: { status: "idle" },
@@ -55,12 +48,6 @@ export const initialState: AppState = {
55
48
  tools: { status: "idle" },
56
49
  },
57
50
 
58
- modelSelector: {
59
- selectedIndex: 0,
60
- searchQuery: "",
61
- taskSize: "large",
62
- },
63
-
64
51
  profiles: {
65
52
  selectedIndex: 0,
66
53
  profiles: { status: "idle" },
@@ -266,63 +253,6 @@ export function appReducer(state: AppState, action: AppAction): AppState {
266
253
  },
267
254
  };
268
255
 
269
- // =========================================================================
270
- // Status Line Screen
271
- // =========================================================================
272
- case "STATUSLINE_SET_SCOPE":
273
- return {
274
- ...state,
275
- statusline: {
276
- ...state.statusline,
277
- scope: action.scope,
278
- selectedIndex: 0,
279
- presets: { status: "loading" },
280
- },
281
- };
282
-
283
- case "STATUSLINE_TOGGLE_SCOPE":
284
- return appReducer(state, {
285
- type: "STATUSLINE_SET_SCOPE",
286
- scope: state.statusline.scope === "project" ? "global" : "project",
287
- });
288
-
289
- case "STATUSLINE_SELECT":
290
- return {
291
- ...state,
292
- statusline: {
293
- ...state.statusline,
294
- selectedIndex: action.index,
295
- },
296
- };
297
-
298
- case "STATUSLINE_DATA_LOADING":
299
- return {
300
- ...state,
301
- statusline: {
302
- ...state.statusline,
303
- presets: { status: "loading" },
304
- },
305
- };
306
-
307
- case "STATUSLINE_DATA_SUCCESS":
308
- return {
309
- ...state,
310
- statusline: {
311
- ...state.statusline,
312
- presets: { status: "success", data: action.presets },
313
- currentPreset: action.currentPreset,
314
- },
315
- };
316
-
317
- case "STATUSLINE_DATA_ERROR":
318
- return {
319
- ...state,
320
- statusline: {
321
- ...state.statusline,
322
- presets: { status: "error", error: action.error },
323
- },
324
- };
325
-
326
256
  // =========================================================================
327
257
  // Settings Screen
328
258
  // =========================================================================
@@ -417,37 +347,6 @@ export function appReducer(state: AppState, action: AppAction): AppState {
417
347
  };
418
348
  }
419
349
 
420
- // =========================================================================
421
- // Model Selector Screen
422
- // =========================================================================
423
- case "MODEL_SELECTOR_SELECT":
424
- return {
425
- ...state,
426
- modelSelector: {
427
- ...state.modelSelector,
428
- selectedIndex: action.index,
429
- },
430
- };
431
-
432
- case "MODEL_SELECTOR_SET_SEARCH":
433
- return {
434
- ...state,
435
- modelSelector: {
436
- ...state.modelSelector,
437
- searchQuery: action.query,
438
- selectedIndex: 0, // Reset selection when search changes
439
- },
440
- };
441
-
442
- case "MODEL_SELECTOR_SET_TASK_SIZE":
443
- return {
444
- ...state,
445
- modelSelector: {
446
- ...state.modelSelector,
447
- taskSize: action.size,
448
- },
449
- };
450
-
451
350
  // =========================================================================
452
351
  // Profiles Screen
453
352
  // =========================================================================
@@ -1,7 +1,6 @@
1
1
  import type {
2
2
  Marketplace,
3
3
  McpServer,
4
- StatusLineConfig,
5
4
  ProfileEntry,
6
5
  SkillInfo,
7
6
  } from "../../types/index.js";
@@ -18,7 +17,6 @@ export type Screen =
18
17
  | "mcp-registry"
19
18
  | "settings"
20
19
  | "cli-tools"
21
- | "model-selector"
22
20
  | "profiles"
23
21
  | "skills"
24
22
  | "gitignore"
@@ -30,7 +28,6 @@ export type Route =
30
28
  | { screen: "mcp-registry"; query?: string }
31
29
  | { screen: "settings" }
32
30
  | { screen: "cli-tools" }
33
- | { screen: "model-selector" }
34
31
  | { screen: "profiles" }
35
32
  | { screen: "skills" }
36
33
  | { screen: "gitignore" }
@@ -134,13 +131,6 @@ export interface McpRegistryScreenState {
134
131
  servers: AsyncData<McpServer[]>;
135
132
  }
136
133
 
137
- export interface StatusLineScreenState {
138
- scope: "project" | "global";
139
- selectedIndex: number;
140
- presets: AsyncData<StatusLineConfig[]>;
141
- currentPreset: string | null;
142
- }
143
-
144
134
  export interface ScopedSettingValues {
145
135
  user: string | undefined;
146
136
  project: string | undefined;
@@ -171,12 +161,6 @@ export interface CliToolsScreenState {
171
161
  tools: AsyncData<CliTool[]>;
172
162
  }
173
163
 
174
- export interface ModelSelectorScreenState {
175
- selectedIndex: number;
176
- searchQuery: string;
177
- taskSize: "large" | "small";
178
- }
179
-
180
164
  export interface ProfilesScreenState {
181
165
  selectedIndex: number;
182
166
  profiles: AsyncData<ProfileEntry[]>;
@@ -221,10 +205,8 @@ export interface AppState {
221
205
  plugins: PluginsScreenState;
222
206
  mcp: McpScreenState;
223
207
  mcpRegistry: McpRegistryScreenState;
224
- statusline: StatusLineScreenState;
225
208
  settings: SettingsScreenState;
226
209
  cliTools: CliToolsScreenState;
227
- modelSelector: ModelSelectorScreenState;
228
210
  profiles: ProfilesScreenState;
229
211
  skills: SkillsScreenState;
230
212
  }
@@ -270,18 +252,6 @@ export type AppAction =
270
252
  | { type: "MCP_REGISTRY_DATA_SUCCESS"; servers: McpServer[] }
271
253
  | { type: "MCP_REGISTRY_DATA_ERROR"; error: Error }
272
254
 
273
- // Status line screen
274
- | { type: "STATUSLINE_SET_SCOPE"; scope: "project" | "global" }
275
- | { type: "STATUSLINE_TOGGLE_SCOPE" }
276
- | { type: "STATUSLINE_SELECT"; index: number }
277
- | { type: "STATUSLINE_DATA_LOADING" }
278
- | {
279
- type: "STATUSLINE_DATA_SUCCESS";
280
- presets: StatusLineConfig[];
281
- currentPreset: string | null;
282
- }
283
- | { type: "STATUSLINE_DATA_ERROR"; error: Error }
284
-
285
255
  // Settings screen
286
256
  | { type: "SETTINGS_SELECT"; index: number }
287
257
  | { type: "SETTINGS_DATA_LOADING" }
@@ -295,11 +265,6 @@ export type AppAction =
295
265
  | { type: "CLITOOLS_DATA_ERROR"; error: Error }
296
266
  | { type: "CLITOOLS_UPDATE_TOOL"; name: string; updates: Partial<CliTool> }
297
267
 
298
- // Model Selector Screen
299
- | { type: "MODEL_SELECTOR_SELECT"; index: number }
300
- | { type: "MODEL_SELECTOR_SET_SEARCH"; query: string }
301
- | { type: "MODEL_SELECTOR_SET_TASK_SIZE"; size: "large" | "small" }
302
-
303
268
  // Modals
304
269
  | { type: "SHOW_MODAL"; modal: ModalState }
305
270
  | { type: "HIDE_MODAL" }
@@ -1,5 +1,7 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
+ import { access, realpath } from "node:fs/promises";
4
+ import { constants } from "node:fs";
3
5
 
4
6
  const execFileAsync = promisify(execFile);
5
7
 
@@ -18,3 +20,24 @@ export async function which(command: string): Promise<string | null> {
18
20
  return null;
19
21
  }
20
22
  }
23
+
24
+ /**
25
+ * Like {@link which}, but verifies the command actually RESOLVES and is
26
+ * executable — a dangling symlink (e.g. a stale npx-cache link) returns null.
27
+ * `which` alone reports the symlink's own path even when its target is gone,
28
+ * which is exactly how a plugin's binary can look "present" yet fail at runtime.
29
+ * @returns The real (symlink-resolved) executable path, or null.
30
+ */
31
+ export async function resolveExecutable(
32
+ command: string,
33
+ ): Promise<string | null> {
34
+ const p = await which(command);
35
+ if (!p) return null;
36
+ try {
37
+ const real = await realpath(p); // follows symlinks; throws if dangling
38
+ await access(real, constants.X_OK); // must be executable
39
+ return real;
40
+ } catch {
41
+ return null; // dangling link or not executable
42
+ }
43
+ }