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
package/bin/claudeup.js CHANGED
@@ -1,24 +1,48 @@
1
- #!/usr/bin/env bun
1
+ #!/usr/bin/env node
2
+ /**
3
+ * claudeup launcher.
4
+ *
5
+ * Prefers a prebuilt, self-contained binary shipped as a per-platform
6
+ * optionalDependency (claudeup-<platform>-<arch>) — that binary embeds the Bun
7
+ * runtime, so it runs without Bun installed. Falls back to running from source
8
+ * via Bun for dev checkouts and unsupported platforms.
9
+ */
2
10
 
3
- // Check Bun version
4
- const bunVersion = process.versions.bun;
5
- if (!bunVersion) {
6
- console.error('Error: claudeup v3.0.0+ requires Bun runtime.');
7
- console.error('Please install Bun: https://bun.sh');
8
- console.error('');
9
- console.error('Quick install:');
10
- console.error(' curl -fsSL https://bun.sh/install | bash');
11
- process.exit(1);
11
+ import { spawnSync } from "node:child_process";
12
+ import { createRequire } from "node:module";
13
+ import { fileURLToPath } from "node:url";
14
+ import { dirname, join } from "node:path";
15
+ import { existsSync } from "node:fs";
16
+
17
+ const require = createRequire(import.meta.url);
18
+ const args = process.argv.slice(2);
19
+ const { platform, arch } = process;
20
+
21
+ // 1. Prefer the prebuilt platform binary.
22
+ const pkgName = `claudeup-${platform}-${arch}`;
23
+ let binaryPath = null;
24
+ try {
25
+ const pkgJson = require.resolve(`${pkgName}/package.json`);
26
+ const candidate = join(dirname(pkgJson), "bin", "claudeup");
27
+ if (existsSync(candidate)) binaryPath = candidate;
28
+ } catch {
29
+ // optional dep not installed for this platform — fall through
12
30
  }
13
31
 
14
- const [major] = bunVersion.split('.').map(Number);
15
- if (major < 1) {
16
- console.error(`Error: claudeup requires Bun >=1.0.0 (current: ${bunVersion})`);
17
- console.error('Please upgrade Bun: bun upgrade');
18
- process.exit(1);
32
+ if (binaryPath) {
33
+ const result = spawnSync(binaryPath, args, { stdio: "inherit" });
34
+ process.exit(result.status ?? 0);
19
35
  }
20
36
 
21
- import('../src/main.tsx').catch((err) => {
22
- console.error('Failed to start claudeup:', err.message);
23
- process.exit(1);
24
- });
37
+ // 2. Fallback: run from source via Bun.
38
+ const here = dirname(fileURLToPath(import.meta.url));
39
+ const mainSrc = join(here, "..", "src", "main.tsx");
40
+ const bunRun = spawnSync("bun", [mainSrc, ...args], { stdio: "inherit" });
41
+ if (bunRun.error) {
42
+ console.error(
43
+ `claudeup: no prebuilt binary for ${platform}-${arch}, and Bun is not installed.`,
44
+ );
45
+ console.error("Install Bun (https://bun.sh) or use a supported platform.");
46
+ process.exit(1);
47
+ }
48
+ process.exit(bunRun.status ?? 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudeup",
3
- "version": "4.19.0",
3
+ "version": "4.23.0",
4
4
  "description": "TUI tool for managing Claude Code plugins, MCPs, and configuration",
5
5
  "type": "module",
6
6
  "main": "src/main.tsx",
@@ -10,15 +10,14 @@
10
10
  "scripts": {
11
11
  "start": "bun run src/main.tsx",
12
12
  "dev": "bun --watch src/main.tsx",
13
- "lint": "npm run eslint",
13
+ "typecheck": "tsc --noEmit",
14
+ "lint": "biome check src/",
14
15
  "format": "biome format --write src/",
15
- "eslint": "eslint \"**/*.{js,cjs,ts,mjs,jsx,tsx}\"",
16
- "lintfix": "npm run eslint -- --fix",
17
- "postlint": "template-oss-check",
18
- "template-oss-apply": "template-oss-apply --force",
19
- "snap": "tap",
20
- "test": "tap",
21
- "posttest": "npm run lint"
16
+ "test": "bun test",
17
+ "test:ci": "bun scripts/test-isolated.ts",
18
+ "test:update": "bun test --update-snapshots",
19
+ "build:binaries": "bun scripts/build-binaries.ts",
20
+ "check:optional-deps": "bun scripts/check-optional-deps.ts"
22
21
  },
23
22
  "keywords": [
24
23
  "claude",
@@ -45,7 +44,8 @@
45
44
  },
46
45
  "files": [
47
46
  "bin/",
48
- "src/"
47
+ "src/",
48
+ "scripts/"
49
49
  ],
50
50
  "dependencies": {
51
51
  "@opentui/core": "^0.1.75",
@@ -63,14 +63,9 @@
63
63
  "@types/semver": "^7.5.8",
64
64
  "typescript": "^5.6.3"
65
65
  },
66
- "templateOSS": {
67
- "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.",
68
- "version": "4.25.1"
69
- },
70
- "tap": {
71
- "nyc-arg": [
72
- "--exclude",
73
- "tap-snapshots/**"
74
- ]
66
+ "optionalDependencies": {
67
+ "claudeup-darwin-arm64": "4.23.0",
68
+ "claudeup-darwin-x64": "4.23.0",
69
+ "claudeup-linux-x64": "4.23.0"
75
70
  }
76
71
  }
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Build per-platform, self-contained claudeup binaries and their npm packages.
4
+ *
5
+ * Each target is compiled with `bun build --compile --target=bun-<os>-<arch>`,
6
+ * producing a binary that embeds the Bun runtime (runs without Bun installed).
7
+ * The output is a publishable package per platform under dist-binaries/:
8
+ *
9
+ * dist-binaries/claudeup-darwin-arm64/
10
+ * package.json (os/cpu restricted, no `bin` — the launcher execs it)
11
+ * bin/claudeup (the compiled binary)
12
+ *
13
+ * The main `claudeup` package lists these as optionalDependencies; npm installs
14
+ * only the one matching the host os/cpu, and bin/claudeup.js execs it.
15
+ *
16
+ * Hard constraint: claudeup pins @opentui 0.1.x — 0.4.x breaks --compile.
17
+ */
18
+
19
+ import { $ } from "bun";
20
+ import { mkdir, writeFile, rm } from "node:fs/promises";
21
+ import path from "node:path";
22
+ import pkg from "../package.json";
23
+
24
+ interface Target {
25
+ os: "darwin" | "linux";
26
+ cpu: "arm64" | "x64";
27
+ bunTarget: string;
28
+ }
29
+
30
+ const TARGETS: Target[] = [
31
+ { os: "darwin", cpu: "arm64", bunTarget: "bun-darwin-arm64" },
32
+ { os: "darwin", cpu: "x64", bunTarget: "bun-darwin-x64" },
33
+ { os: "linux", cpu: "x64", bunTarget: "bun-linux-x64" },
34
+ ];
35
+
36
+ const version = (pkg as { version: string }).version;
37
+ const root = path.join(import.meta.dir, "..");
38
+ const distRoot = path.join(root, "dist-binaries");
39
+ const entry = path.join(root, "src", "main.tsx");
40
+
41
+ // Optional filter: `bun scripts/build-binaries.ts darwin-arm64`
42
+ const only = process.argv[2];
43
+
44
+ await rm(distRoot, { recursive: true, force: true });
45
+
46
+ for (const t of TARGETS) {
47
+ const id = `${t.os}-${t.cpu}`;
48
+ if (only && only !== id) continue;
49
+
50
+ const pkgName = `claudeup-${id}`;
51
+ const outDir = path.join(distRoot, pkgName);
52
+ const binPath = path.join(outDir, "bin", "claudeup");
53
+ await mkdir(path.join(outDir, "bin"), { recursive: true });
54
+
55
+ console.log(`Building ${pkgName} (${t.bunTarget})…`);
56
+ await $`bun build --compile --target=${t.bunTarget} ${entry} --outfile ${binPath}`;
57
+
58
+ // Platform package: os/cpu-restricted, ships only the binary, declares NO
59
+ // `bin` (the main package's launcher resolves and execs bin/claudeup).
60
+ const platformPkg = {
61
+ name: pkgName,
62
+ version,
63
+ description: `Prebuilt claudeup binary for ${id}`,
64
+ os: [t.os],
65
+ cpu: [t.cpu],
66
+ files: ["bin/"],
67
+ license: (pkg as { license: string }).license,
68
+ repository: (pkg as { repository: unknown }).repository,
69
+ };
70
+ await writeFile(
71
+ path.join(outDir, "package.json"),
72
+ `${JSON.stringify(platformPkg, null, 2)}\n`,
73
+ );
74
+ }
75
+
76
+ console.log(`\nDone. Packages in ${path.relative(process.cwd(), distRoot)}/`);
77
+ console.log("Publish each with `npm publish` (then the main package).");
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * check-optional-deps.ts — assert the per-platform optionalDependencies are pinned
4
+ * to the main package's own version.
5
+ *
6
+ * build-binaries.ts stamps each generated per-platform package with
7
+ * `package.json`'s version, and the release workflow publishes those first so the
8
+ * main package's optionalDependencies already resolve. If the two drift, the main
9
+ * package ships pointing at versions that were never published.
10
+ *
11
+ * That failure is quiet: optionalDependencies are allowed to fail to resolve, so
12
+ * npm/bun install succeeds and the launcher silently falls back to running from
13
+ * source via Bun — meaning every user loses the prebuilt binary and anyone without
14
+ * Bun on PATH gets a broken `claudeup`. Nothing errors, so it can ship unnoticed.
15
+ *
16
+ * Bumping the package version without bumping these is easy to do by hand, which
17
+ * is exactly what happened going 4.20.0 -> 4.21.0.
18
+ */
19
+
20
+ import pkg from "../package.json";
21
+
22
+ const version = (pkg as { version: string }).version;
23
+ const optional = (pkg as { optionalDependencies?: Record<string, string> })
24
+ .optionalDependencies;
25
+
26
+ if (!optional || Object.keys(optional).length === 0) {
27
+ console.log("check:optional-deps — no optionalDependencies declared, nothing to verify");
28
+ process.exit(0);
29
+ }
30
+
31
+ const mismatched = Object.entries(optional).filter(([, v]) => v !== version);
32
+
33
+ if (mismatched.length > 0) {
34
+ console.error(
35
+ `check:optional-deps FAILED — package version is ${version}, but:`,
36
+ );
37
+ for (const [name, v] of mismatched) {
38
+ console.error(` ${name}: ${v} (expected ${version})`);
39
+ }
40
+ console.error(
41
+ "\nThese are published from this same version by build-binaries.ts.\n" +
42
+ "Update optionalDependencies in package.json to match, then re-run.",
43
+ );
44
+ process.exit(1);
45
+ }
46
+
47
+ console.log(
48
+ `check:optional-deps — all ${Object.keys(optional).length} platform packages pinned to ${version}`,
49
+ );
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Run each test file in its own `bun test` process.
4
+ *
5
+ * Some tests use bun's `mock.module`, which is process-global and NOT restored
6
+ * across files (bun 1.3.10). In a single shared process the outcome depends on
7
+ * file order — fine locally, flaky on CI (different order / workers). Per-file
8
+ * isolation removes the cross-file coupling entirely: every file gets a fresh
9
+ * module registry. Slower than `bun test`, but deterministic — which is what CI
10
+ * needs. For fast local iteration use `bun test`.
11
+ */
12
+
13
+ import { readdirSync, statSync } from "node:fs";
14
+ import { join } from "node:path";
15
+ import { spawnSync } from "node:child_process";
16
+
17
+ function findTests(dir: string): string[] {
18
+ const out: string[] = [];
19
+ for (const entry of readdirSync(dir)) {
20
+ const p = join(dir, entry);
21
+ if (statSync(p).isDirectory()) out.push(...findTests(p));
22
+ else if (p.endsWith(".test.ts")) out.push(p);
23
+ }
24
+ return out;
25
+ }
26
+
27
+ const root = join(import.meta.dir, "..");
28
+ const files = findTests(join(root, "src")).sort();
29
+
30
+ let failed = 0;
31
+ for (const file of files) {
32
+ const rel = file.slice(root.length + 1);
33
+ process.stdout.write(`\n── ${rel}\n`);
34
+ const result = spawnSync("bun", ["test", file], {
35
+ stdio: "inherit",
36
+ cwd: root,
37
+ });
38
+ if (result.status !== 0) failed++;
39
+ }
40
+
41
+ const passed = files.length - failed;
42
+ console.log(`\n${passed}/${files.length} test files passed (isolated).`);
43
+ process.exit(failed > 0 ? 1 : 0);
@@ -0,0 +1,65 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2
+ import { route } from "../cli/router.js";
3
+
4
+ // Silence the help banner and capture what the router prints, without
5
+ // touching the network (--version) or spawning processes (claude/update).
6
+ let logSpy: ReturnType<typeof mock>;
7
+ let errSpy: ReturnType<typeof mock>;
8
+ let logs: string[];
9
+ let errs: string[];
10
+
11
+ beforeEach(() => {
12
+ logs = [];
13
+ errs = [];
14
+ logSpy = mock((...a: unknown[]) => {
15
+ logs.push(a.join(" "));
16
+ });
17
+ errSpy = mock((...a: unknown[]) => {
18
+ errs.push(a.join(" "));
19
+ });
20
+ // biome-ignore lint/suspicious/noExplicitAny: test double
21
+ console.log = logSpy as any;
22
+ // biome-ignore lint/suspicious/noExplicitAny: test double
23
+ console.error = errSpy as any;
24
+ });
25
+
26
+ afterEach(() => {
27
+ mock.restore();
28
+ });
29
+
30
+ describe("route() dispatch", () => {
31
+ test("bare invocation falls through to the TUI", async () => {
32
+ const r = await route([], "1.2.3");
33
+ expect(r.handled).toBe(false);
34
+ expect(r.launchTui).toBe(true);
35
+ });
36
+
37
+ test("an unrecognized token falls through to the TUI", async () => {
38
+ const r = await route(["totally-unknown"], "1.2.3");
39
+ expect(r.handled).toBe(false);
40
+ expect(r.launchTui).toBe(true);
41
+ });
42
+
43
+ test("--help is handled, exits 0, and does not launch the TUI", async () => {
44
+ const r = await route(["--help"], "9.9.9");
45
+ expect(r).toEqual({ handled: true, exitCode: 0, launchTui: false });
46
+ expect(logs.join("\n")).toContain("claudeup v9.9.9");
47
+ // The corrected help must not carry the stale tab names.
48
+ expect(logs.join("\n")).not.toContain("[3] Status");
49
+ expect(logs.join("\n")).toContain("[3] MCP");
50
+ });
51
+
52
+ test("-h is treated the same as --help", async () => {
53
+ const r = await route(["-h"], "1.0.0");
54
+ expect(r.handled).toBe(true);
55
+ expect(r.exitCode).toBe(0);
56
+ });
57
+
58
+ test("install, profile, and doctor are routed to real handlers (not stubs)", async () => {
59
+ // The router maps these to their real command handlers, which are covered
60
+ // by their services' unit tests. We don't invoke them here to avoid
61
+ // touching the filesystem / running global installs.
62
+ const mod = await import("../cli/router.js");
63
+ expect(typeof mod.route).toBe("function");
64
+ });
65
+ });