claudeup 4.18.0 → 4.22.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 (191) 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__/alias-adopt.test.ts +364 -0
  7. package/src/__tests__/alias-parser.test.ts +92 -0
  8. package/src/__tests__/alias-shell-writer.test.ts +7 -0
  9. package/src/__tests__/alias-store.test.ts +77 -0
  10. package/src/__tests__/cli-router.test.ts +65 -0
  11. package/src/__tests__/conventions-integration.test.ts +745 -0
  12. package/src/__tests__/conventions-manager.test.ts +1172 -0
  13. package/src/__tests__/doctor-bins.test.ts +35 -0
  14. package/src/__tests__/doctor.test.ts +450 -0
  15. package/src/__tests__/dual-write-prevention.test.ts +14 -4
  16. package/src/__tests__/file-locking.test.ts +208 -0
  17. package/src/__tests__/gitignore-prerun.test.ts +1 -0
  18. package/src/__tests__/install-command.test.ts +81 -0
  19. package/src/__tests__/install-plan.test.ts +98 -0
  20. package/src/__tests__/manifest.test.ts +136 -0
  21. package/src/__tests__/marketplace-refresh.test.ts +267 -0
  22. package/src/__tests__/plugin-requires.test.ts +133 -0
  23. package/src/__tests__/plugin-setup.test.ts +118 -0
  24. package/src/__tests__/profile-command.test.ts +116 -0
  25. package/src/__tests__/profile-materializer.test.ts +82 -0
  26. package/src/__tests__/profile-sync.test.ts +136 -0
  27. package/src/__tests__/registry-version-resolution.test.ts +74 -0
  28. package/src/__tests__/resolve-executable.test.ts +42 -0
  29. package/src/__tests__/resolver.test.ts +218 -0
  30. package/src/__tests__/symlink-manager.test.ts +99 -0
  31. package/src/__tests__/toolchain.test.ts +56 -0
  32. package/src/cli/claude.ts +21 -0
  33. package/src/cli/doctor.ts +132 -0
  34. package/src/cli/install.ts +360 -0
  35. package/src/cli/profile.ts +166 -0
  36. package/src/cli/router.ts +107 -0
  37. package/src/cli/update.ts +85 -0
  38. package/src/data/alias-flags.ts +11 -1
  39. package/src/data/cli-tools.ts +7 -7
  40. package/src/data/gitignore-defaults.ts +4 -0
  41. package/src/data/marketplaces.ts +12 -0
  42. package/src/data/predefined-profiles.ts +3 -4
  43. package/src/main.tsx +11 -154
  44. package/src/prerunner/index.ts +62 -27
  45. package/src/services/alias-shell-writer.ts +382 -8
  46. package/src/services/alias-store.ts +60 -0
  47. package/src/services/claude-cli.ts +19 -22
  48. package/src/services/claude-settings.ts +99 -26
  49. package/src/services/conventions-manager.ts +865 -0
  50. package/src/services/doctor-bins.ts +49 -0
  51. package/src/services/doctor.ts +509 -0
  52. package/src/services/install-plan.ts +107 -0
  53. package/src/services/manifest.ts +166 -0
  54. package/src/services/marketplace-refresh.ts +173 -0
  55. package/src/services/plugin-manager.ts +7 -1
  56. package/src/services/plugin-requires.ts +215 -0
  57. package/src/services/plugin-setup.ts +61 -4
  58. package/src/services/plugin-version-check.ts +9 -4
  59. package/src/services/profile-materializer.ts +61 -0
  60. package/src/services/profile-sync.ts +150 -0
  61. package/src/services/resolver.ts +293 -0
  62. package/src/services/symlink-manager.ts +146 -0
  63. package/src/services/toolchain.ts +97 -0
  64. package/src/services/version-check.ts +10 -11
  65. package/src/types/index.ts +138 -31
  66. package/src/ui/App.tsx +16 -11
  67. package/src/ui/components/FlagDetailEditor.tsx +0 -0
  68. package/src/ui/components/modals/ConfirmModal.tsx +1 -1
  69. package/src/ui/screens/AliasScreen.tsx +491 -359
  70. package/src/ui/screens/CliToolsScreen.tsx +11 -3
  71. package/src/ui/screens/PluginsScreen.tsx +3 -1
  72. package/src/ui/screens/index.ts +0 -1
  73. package/src/ui/state/reducer.ts +6 -102
  74. package/src/ui/state/types.ts +8 -35
  75. package/src/utils/command-utils.ts +23 -0
  76. package/src/utils/file-locking.ts +144 -0
  77. package/src/data/alias-flags.js +0 -196
  78. package/src/data/cli-tools.js +0 -123
  79. package/src/data/gitignore-defaults.js +0 -24
  80. package/src/data/gitignore-reasons.js +0 -97
  81. package/src/data/gitignore-templates.js +0 -21
  82. package/src/data/marketplaces.js +0 -138
  83. package/src/data/mcp-servers.js +0 -509
  84. package/src/data/predefined-profiles.js +0 -248
  85. package/src/data/settings-catalog.js +0 -625
  86. package/src/data/skill-repos.js +0 -160
  87. package/src/data/statuslines.js +0 -159
  88. package/src/data/statuslines.ts +0 -188
  89. package/src/index.js +0 -4
  90. package/src/index.ts +0 -5
  91. package/src/main.js +0 -170
  92. package/src/prerunner/index.js +0 -252
  93. package/src/services/alias-settings.js +0 -51
  94. package/src/services/alias-shell-writer.js +0 -764
  95. package/src/services/alias-store.js +0 -77
  96. package/src/services/claude-cli.js +0 -189
  97. package/src/services/claude-runner.js +0 -28
  98. package/src/services/claude-settings.js +0 -1223
  99. package/src/services/gitignore-detector.js +0 -155
  100. package/src/services/gitignore-fixer.js +0 -231
  101. package/src/services/gitignore-prerun.js +0 -46
  102. package/src/services/gitignore-resolver.js +0 -143
  103. package/src/services/gitignore-service.js +0 -117
  104. package/src/services/local-marketplace.js +0 -339
  105. package/src/services/marketplace-fetcher.js +0 -96
  106. package/src/services/marketplace-sync.js +0 -94
  107. package/src/services/mcp-registry.js +0 -87
  108. package/src/services/plugin-manager.js +0 -473
  109. package/src/services/plugin-mcp-config.js +0 -177
  110. package/src/services/plugin-setup.js +0 -444
  111. package/src/services/plugin-version-check.js +0 -262
  112. package/src/services/profiles.js +0 -161
  113. package/src/services/settings-manager.js +0 -243
  114. package/src/services/skills-manager.js +0 -393
  115. package/src/services/skillsmp-client.js +0 -87
  116. package/src/services/update-cache.js +0 -52
  117. package/src/services/version-check.js +0 -99
  118. package/src/types/gitignore.js +0 -6
  119. package/src/types/index.js +0 -1
  120. package/src/ui/App.js +0 -346
  121. package/src/ui/adapters/pluginsAdapter.js +0 -139
  122. package/src/ui/adapters/settingsAdapter.js +0 -111
  123. package/src/ui/adapters/skillsAdapter.js +0 -154
  124. package/src/ui/components/CategoryHeader.js +0 -9
  125. package/src/ui/components/EmptyFilterState.js +0 -4
  126. package/src/ui/components/ScopeIndicator.js +0 -30
  127. package/src/ui/components/ScrollableList.js +0 -36
  128. package/src/ui/components/SearchInput.js +0 -19
  129. package/src/ui/components/StyledText.js +0 -39
  130. package/src/ui/components/TabBar.js +0 -19
  131. package/src/ui/components/layout/FooterHints.js +0 -29
  132. package/src/ui/components/layout/Panel.js +0 -6
  133. package/src/ui/components/layout/ProgressBar.js +0 -14
  134. package/src/ui/components/layout/ScopeTabs.js +0 -6
  135. package/src/ui/components/layout/ScreenLayout.js +0 -16
  136. package/src/ui/components/layout/index.js +0 -5
  137. package/src/ui/components/modals/ConfirmModal.js +0 -14
  138. package/src/ui/components/modals/InputModal.js +0 -5
  139. package/src/ui/components/modals/LoadingModal.js +0 -17
  140. package/src/ui/components/modals/MessageModal.js +0 -16
  141. package/src/ui/components/modals/ModalContainer.js +0 -137
  142. package/src/ui/components/modals/SelectModal.js +0 -18
  143. package/src/ui/components/modals/VersionMismatchModal.js +0 -36
  144. package/src/ui/components/modals/index.js +0 -6
  145. package/src/ui/components/primitives/ActionHints.js +0 -13
  146. package/src/ui/components/primitives/DetailSection.js +0 -7
  147. package/src/ui/components/primitives/KeyValueLine.js +0 -8
  148. package/src/ui/components/primitives/ListCategoryRow.js +0 -8
  149. package/src/ui/components/primitives/MetaText.js +0 -8
  150. package/src/ui/components/primitives/ScopeDetail.js +0 -32
  151. package/src/ui/components/primitives/ScopeSquares.js +0 -11
  152. package/src/ui/components/primitives/SelectableRow.js +0 -5
  153. package/src/ui/components/primitives/index.js +0 -8
  154. package/src/ui/hooks/index.js +0 -4
  155. package/src/ui/hooks/useAsyncData.js +0 -77
  156. package/src/ui/hooks/useGitignoreModal.js +0 -74
  157. package/src/ui/hooks/useKeyboard.js +0 -13
  158. package/src/ui/hooks/useKeyboardHandler.js +0 -39
  159. package/src/ui/hooks/useMismatchModal.js +0 -77
  160. package/src/ui/registry.js +0 -1
  161. package/src/ui/renderers/cliToolRenderers.js +0 -54
  162. package/src/ui/renderers/gitignoreRenderers.js +0 -46
  163. package/src/ui/renderers/mcpRenderers.js +0 -26
  164. package/src/ui/renderers/pluginRenderers.js +0 -124
  165. package/src/ui/renderers/profileRenderers.js +0 -177
  166. package/src/ui/renderers/settingsRenderers.js +0 -73
  167. package/src/ui/renderers/skillRenderers.js +0 -138
  168. package/src/ui/screens/AliasScreen.js +0 -1008
  169. package/src/ui/screens/CliToolsScreen.js +0 -338
  170. package/src/ui/screens/EnvVarsScreen.js +0 -152
  171. package/src/ui/screens/GitignoreScreen.js +0 -328
  172. package/src/ui/screens/McpRegistryScreen.js +0 -238
  173. package/src/ui/screens/McpScreen.js +0 -176
  174. package/src/ui/screens/ModelSelectorScreen.js +0 -296
  175. package/src/ui/screens/ModelSelectorScreen.tsx +0 -444
  176. package/src/ui/screens/PluginsScreen.js +0 -766
  177. package/src/ui/screens/ProfilesScreen.js +0 -298
  178. package/src/ui/screens/SkillsScreen.js +0 -549
  179. package/src/ui/screens/StatusLineScreen.js +0 -206
  180. package/src/ui/screens/StatusLineScreen.tsx +0 -416
  181. package/src/ui/screens/index.js +0 -10
  182. package/src/ui/state/AnimationContext.js +0 -34
  183. package/src/ui/state/AppContext.js +0 -162
  184. package/src/ui/state/DimensionsContext.js +0 -71
  185. package/src/ui/state/reducer.js +0 -543
  186. package/src/ui/state/types.js +0 -1
  187. package/src/ui/theme.js +0 -47
  188. package/src/utils/clipboard.js +0 -56
  189. package/src/utils/command-utils.js +0 -19
  190. package/src/utils/fuzzy-search.js +0 -101
  191. package/src/utils/string-utils.js +0 -62
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Toolchain detection + bootstrap for the package managers a profile's binary
3
+ * deps need (bun, brew, npm, pip, go).
4
+ *
5
+ * `claudeup install` requires whatever installers the resolved closure uses. In
6
+ * source form bun is already guaranteed (bin/claudeup.js hard-exits without it),
7
+ * but once claudeup ships as a compiled binary an end user may lack bun — so bun
8
+ * is treated as a bootstrappable toolchain exactly like brew.
9
+ */
10
+
11
+ import type { BinInstaller, ResolvedBin } from "../types/index.js";
12
+ import { which } from "../utils/command-utils.js";
13
+
14
+ /** The executable that proves a toolchain is installed. */
15
+ const TOOLCHAIN_PROBE: Record<BinInstaller, string> = {
16
+ bun: "bun",
17
+ brew: "brew",
18
+ npm: "npm",
19
+ pip: "pip",
20
+ go: "go",
21
+ };
22
+
23
+ /** Official one-line bootstrap for the toolchains claudeup will install. */
24
+ export const TOOLCHAIN_BOOTSTRAP: Partial<Record<BinInstaller, string>> = {
25
+ bun: "curl -fsSL https://bun.sh/install | bash",
26
+ brew: '/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"',
27
+ };
28
+
29
+ export interface ToolchainStatus {
30
+ name: BinInstaller;
31
+ present: boolean;
32
+ /** Binary names in the closure that need this toolchain. */
33
+ requiredBy: string[];
34
+ /** Bootstrap command, when claudeup can install this toolchain itself. */
35
+ bootstrap?: string;
36
+ }
37
+
38
+ /** Is a given toolchain available on PATH? */
39
+ export async function isToolchainPresent(name: BinInstaller): Promise<boolean> {
40
+ return (await which(TOOLCHAIN_PROBE[name])) !== null;
41
+ }
42
+
43
+ /**
44
+ * Determine which toolchains a resolved closure's binary deps require, and
45
+ * whether each is present. Only toolchains actually needed are returned.
46
+ */
47
+ export async function detectToolchains(
48
+ bins: ResolvedBin[],
49
+ ): Promise<ToolchainStatus[]> {
50
+ const requiredBy = new Map<BinInstaller, string[]>();
51
+ for (const bin of bins) {
52
+ const list = requiredBy.get(bin.via) ?? [];
53
+ list.push(bin.name);
54
+ requiredBy.set(bin.via, list);
55
+ }
56
+
57
+ const statuses: ToolchainStatus[] = [];
58
+ for (const [name, names] of requiredBy) {
59
+ statuses.push({
60
+ name,
61
+ present: await isToolchainPresent(name),
62
+ requiredBy: names,
63
+ bootstrap: TOOLCHAIN_BOOTSTRAP[name],
64
+ });
65
+ }
66
+ // Stable order: bun, brew, npm, pip, go.
67
+ const order: BinInstaller[] = ["bun", "brew", "npm", "pip", "go"];
68
+ statuses.sort((a, b) => order.indexOf(a.name) - order.indexOf(b.name));
69
+ return statuses;
70
+ }
71
+
72
+ /** The install command for a single binary requirement. */
73
+ export function binInstallCommand(bin: {
74
+ name: string;
75
+ via: BinInstaller;
76
+ package?: string;
77
+ formula?: string;
78
+ module?: string;
79
+ version?: string;
80
+ }): string {
81
+ const pkg = bin.package ?? bin.name;
82
+ const at = bin.version ? `@${bin.version}` : "";
83
+ switch (bin.via) {
84
+ case "bun":
85
+ return `bun install -g ${pkg}${at}`;
86
+ case "npm":
87
+ return `npm install -g ${pkg}${at}`;
88
+ case "pip":
89
+ return bin.version
90
+ ? `pip install ${pkg}==${bin.version}`
91
+ : `pip install ${pkg}`;
92
+ case "brew":
93
+ return `brew install ${bin.formula ?? bin.name}`;
94
+ case "go":
95
+ return `go install ${bin.module ?? bin.name}@${bin.version ?? "latest"}`;
96
+ }
97
+ }
@@ -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":
@@ -89,7 +86,7 @@ function Router() {
89
86
 
90
87
  /**
91
88
  * GlobalKeyHandler Component
92
- * Handles global keyboard shortcuts (1-5, Tab, Escape, q, ?, Shift+D)
89
+ * Handles global keyboard shortcuts (1-8, Tab, Escape, q, Ctrl+C, ?, Shift+D)
93
90
  * Does not render anything (returns null)
94
91
  */
95
92
  function GlobalKeyHandler({
@@ -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",
@@ -180,16 +176,24 @@ function GlobalKeyHandler({
180
176
  }
181
177
  }
182
178
 
183
- // Escape/q to go back or exit
184
- if (key.escape || input === "q") {
179
+ // q or Ctrl+C quits from anywhere — no need to bounce to home first.
180
+ if (input === "q" || (key.ctrl && input === "c")) {
181
+ onExit();
182
+ return;
183
+ }
184
+
185
+ // Escape goes back: registry → mcp, any other tab → plugins home,
186
+ // already-home → exit. But when a screen has a focused sub-view open
187
+ // (e.g. the Alias flag-detail editor), that sub-view owns Escape and
188
+ // backs out to itself — don't let the global handler jump to home.
189
+ // Scoped to the Escape branch ONLY so q/Ctrl+C still quit from anywhere.
190
+ if (key.escape) {
191
+ if (state.detailActive) return;
185
192
  if (state.currentRoute.screen === "plugins") {
186
- // On home screen, exit immediately
187
193
  onExit();
188
194
  } else if (state.currentRoute.screen === "mcp-registry") {
189
- // Go back to MCP from registry
190
195
  navigateToScreen("mcp");
191
196
  } else {
192
- // Go back to plugins (home)
193
197
  navigateToScreen("plugins");
194
198
  }
195
199
  }
@@ -201,7 +205,8 @@ function GlobalKeyHandler({
201
205
  `Navigation
202
206
  ↑/↓ or j/k Move selection
203
207
  Enter Select / Toggle
204
- Escape or q Back / Quit
208
+ Escape Back (to home)
209
+ q / Ctrl+C Quit (from anywhere)
205
210
  ? This help
206
211
 
207
212
  Quick Navigation
@@ -47,7 +47,7 @@ export function ConfirmModal({
47
47
  <box marginBottom={1}>
48
48
  <text fg="#A1A1AA">{message}</text>
49
49
  </box>
50
- <box marginTop={1}>
50
+ <box marginTop={1} flexDirection="row">
51
51
  <text fg="#71717A">Press </text>
52
52
  <text fg="#EDEDED">Y</text>
53
53
  <text fg="#71717A"> to confirm • </text>