claudeup 4.0.1 → 4.1.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudeup",
3
- "version": "4.0.1",
3
+ "version": "4.1.0",
4
4
  "description": "TUI tool for managing Claude Code plugins, MCPs, and configuration",
5
5
  "type": "module",
6
6
  "main": "src/main.tsx",
@@ -18,8 +18,7 @@ export const cliTools: CliTool[] = [
18
18
  "TUI tool for managing Claude Code plugins, MCPs, and configuration",
19
19
  installCommand: "npm install -g claudeup",
20
20
  checkCommand: "claudeup --version",
21
- website:
22
- "https://github.com/MadAppGang/magus/tree/main/tools/claudeup",
21
+ website: "https://github.com/MadAppGang/magus/tree/main/tools/claudeup",
23
22
  category: "ai-coding",
24
23
  packageManager: "npm",
25
24
  packageName: "claudeup",
@@ -70,7 +70,8 @@ export function getAllMarketplaces(localMarketplaces) {
70
70
  const canonical = deprecatedMarketplaces[name];
71
71
  if (canonical) {
72
72
  // If canonical already in the map or in defaults, skip this entry
73
- if (all.has(canonical) || defaultMarketplaces.some((m) => m.name === canonical)) {
73
+ if (all.has(canonical) ||
74
+ defaultMarketplaces.some((m) => m.name === canonical)) {
74
75
  continue;
75
76
  }
76
77
  }
@@ -85,7 +86,10 @@ export function getAllMarketplaces(localMarketplaces) {
85
86
  name,
86
87
  // Prefer default displayName over stale local clone data
87
88
  displayName: defaultMp?.displayName || local.name || formatMarketplaceName(name),
88
- source: { source: "github", repo: defaultMp?.source.repo || local.gitRepo || "" },
89
+ source: {
90
+ source: "github",
91
+ repo: defaultMp?.source.repo || local.gitRepo || "",
92
+ },
89
93
  description: defaultMp?.description || local.description || "",
90
94
  official: defaultMp?.official ?? repo.toLowerCase().includes("anthropics/"),
91
95
  featured: defaultMp?.featured,
@@ -85,7 +85,10 @@ export function getAllMarketplaces(
85
85
  const canonical = deprecatedMarketplaces[name];
86
86
  if (canonical) {
87
87
  // If canonical already in the map or in defaults, skip this entry
88
- if (all.has(canonical) || defaultMarketplaces.some((m) => m.name === canonical)) {
88
+ if (
89
+ all.has(canonical) ||
90
+ defaultMarketplaces.some((m) => m.name === canonical)
91
+ ) {
89
92
  continue;
90
93
  }
91
94
  }
@@ -100,8 +103,12 @@ export function getAllMarketplaces(
100
103
  all.set(name, {
101
104
  name,
102
105
  // Prefer default displayName over stale local clone data
103
- displayName: defaultMp?.displayName || local.name || formatMarketplaceName(name),
104
- source: { source: "github" as const, repo: defaultMp?.source.repo || local.gitRepo || "" },
106
+ displayName:
107
+ defaultMp?.displayName || local.name || formatMarketplaceName(name),
108
+ source: {
109
+ source: "github" as const,
110
+ repo: defaultMp?.source.repo || local.gitRepo || "",
111
+ },
105
112
  description: defaultMp?.description || local.description || "",
106
113
  official:
107
114
  defaultMp?.official ?? repo.toLowerCase().includes("anthropics/"),
@@ -4,7 +4,7 @@ import os from "node:os";
4
4
  import { UpdateCache } from "../services/update-cache.js";
5
5
  import { getAvailablePlugins, clearMarketplaceCache, } from "../services/plugin-manager.js";
6
6
  import { runClaude } from "../services/claude-runner.js";
7
- import { recoverMarketplaceSettings, migrateMarketplaceRename, getGlobalEnabledPlugins, getEnabledPlugins, getLocalEnabledPlugins, saveGlobalInstalledPluginVersion, } from "../services/claude-settings.js";
7
+ import { recoverMarketplaceSettings, migrateMarketplaceRename, getGlobalEnabledPlugins, getEnabledPlugins, getLocalEnabledPlugins, saveGlobalInstalledPluginVersion, readGlobalSettings, writeGlobalSettings, } from "../services/claude-settings.js";
8
8
  import { parsePluginId } from "../utils/string-utils.js";
9
9
  import { defaultMarketplaces } from "../data/marketplaces.js";
10
10
  import { updatePlugin, addMarketplace, isClaudeAvailable, } from "../services/claude-cli.js";
@@ -22,20 +22,26 @@ async function getReferencedMarketplaces(projectPath) {
22
22
  for (const id of Object.keys(global))
23
23
  allPluginIds.add(id);
24
24
  }
25
- catch { /* skip if unreadable */ }
25
+ catch {
26
+ /* skip if unreadable */
27
+ }
26
28
  if (projectPath) {
27
29
  try {
28
30
  const project = await getEnabledPlugins(projectPath);
29
31
  for (const id of Object.keys(project))
30
32
  allPluginIds.add(id);
31
33
  }
32
- catch { /* skip if unreadable */ }
34
+ catch {
35
+ /* skip if unreadable */
36
+ }
33
37
  try {
34
38
  const local = await getLocalEnabledPlugins(projectPath);
35
39
  for (const id of Object.keys(local))
36
40
  allPluginIds.add(id);
37
41
  }
38
- catch { /* skip if unreadable */ }
42
+ catch {
43
+ /* skip if unreadable */
44
+ }
39
45
  }
40
46
  // Parse marketplace names from plugin IDs
41
47
  for (const pluginId of allPluginIds) {
@@ -73,6 +79,57 @@ async function autoAddMissingMarketplaces(projectPath) {
73
79
  }
74
80
  return added;
75
81
  }
82
+ const CONTINUITY_PLUGIN_SENTINEL = "tmux-claude-continuity";
83
+ const CONTINUITY_PLUGIN_SCRIPT = path.join(os.homedir(), ".tmux", "plugins", "tmux-claude-continuity", "scripts", "on_session_start.sh");
84
+ /**
85
+ * Ensure tmux-claude-continuity Claude Code hooks are present in global settings.
86
+ * If the tmux plugin is installed but the hooks are missing, they are appended.
87
+ * Returns a description of what was added, or null if nothing changed.
88
+ */
89
+ async function ensureTmuxContinuityHooks() {
90
+ // Plugin not installed — nothing to do
91
+ if (!(await fs.pathExists(CONTINUITY_PLUGIN_SCRIPT))) {
92
+ return null;
93
+ }
94
+ const settings = await readGlobalSettings();
95
+ // Check if hooks are already configured by looking for the sentinel string
96
+ const existingHooks = settings.hooks ?? {};
97
+ for (const groups of Object.values(existingHooks)) {
98
+ for (const group of groups) {
99
+ for (const hook of group.hooks) {
100
+ if (hook.command.includes(CONTINUITY_PLUGIN_SENTINEL)) {
101
+ return null; // Already configured
102
+ }
103
+ }
104
+ }
105
+ }
106
+ // Append hooks, preserving any existing entries in SessionStart and Stop
107
+ const sessionStartGroups = existingHooks["SessionStart"] ?? [];
108
+ const stopGroups = existingHooks["Stop"] ?? [];
109
+ sessionStartGroups.push({
110
+ hooks: [
111
+ {
112
+ type: "command",
113
+ command: "bash ~/.tmux/plugins/tmux-claude-continuity/scripts/on_session_start.sh",
114
+ },
115
+ ],
116
+ });
117
+ stopGroups.push({
118
+ hooks: [
119
+ {
120
+ type: "command",
121
+ command: "bash ~/.tmux/plugins/tmux-claude-continuity/scripts/on_stop.sh",
122
+ },
123
+ ],
124
+ });
125
+ settings.hooks = {
126
+ ...existingHooks,
127
+ SessionStart: sessionStartGroups,
128
+ Stop: stopGroups,
129
+ };
130
+ await writeGlobalSettings(settings);
131
+ return "SessionStart + Stop hooks";
132
+ }
76
133
  /**
77
134
  * Prerun orchestration: Check for updates, apply them, then run claude
78
135
  * @param claudeArgs - Arguments to pass to claude CLI
@@ -84,9 +141,11 @@ export async function prerunClaude(claudeArgs, options = {}) {
84
141
  try {
85
142
  // STEP 0: Migrate old marketplace names → magus (idempotent, no-ops if already migrated)
86
143
  const migration = await migrateMarketplaceRename();
87
- const migTotal = migration.projectMigrated + migration.globalMigrated
88
- + migration.localMigrated + migration.registryMigrated
89
- + (migration.knownMarketplacesMigrated ? 1 : 0);
144
+ const migTotal = migration.projectMigrated +
145
+ migration.globalMigrated +
146
+ migration.localMigrated +
147
+ migration.registryMigrated +
148
+ (migration.knownMarketplacesMigrated ? 1 : 0);
90
149
  if (migTotal > 0) {
91
150
  console.log(`✓ Migrated ${migTotal} plugin reference(s) → magus`);
92
151
  }
@@ -99,6 +158,11 @@ export async function prerunClaude(claudeArgs, options = {}) {
99
158
  console.log(`✓ Auto-added marketplace(s): ${addedMarketplaces.join(", ")}`);
100
159
  }
101
160
  }
161
+ // STEP 0.6: Ensure tmux-claude-continuity hooks are configured
162
+ const addedHooks = await ensureTmuxContinuityHooks();
163
+ if (addedHooks) {
164
+ console.log(`✓ Added tmux-claude-continuity hooks to ~/.claude/settings.json`);
165
+ }
102
166
  // STEP 1: Check if we should update (time-based cache, or forced)
103
167
  const shouldUpdate = options.force || (await cache.shouldCheckForUpdates());
104
168
  if (options.force) {
@@ -14,6 +14,8 @@ import {
14
14
  getEnabledPlugins,
15
15
  getLocalEnabledPlugins,
16
16
  saveGlobalInstalledPluginVersion,
17
+ readGlobalSettings,
18
+ writeGlobalSettings,
17
19
  } from "../services/claude-settings.js";
18
20
  import { parsePluginId } from "../utils/string-utils.js";
19
21
  import { defaultMarketplaces } from "../data/marketplaces.js";
@@ -49,18 +51,24 @@ async function getReferencedMarketplaces(
49
51
  try {
50
52
  const global = await getGlobalEnabledPlugins();
51
53
  for (const id of Object.keys(global)) allPluginIds.add(id);
52
- } catch { /* skip if unreadable */ }
54
+ } catch {
55
+ /* skip if unreadable */
56
+ }
53
57
 
54
58
  if (projectPath) {
55
59
  try {
56
60
  const project = await getEnabledPlugins(projectPath);
57
61
  for (const id of Object.keys(project)) allPluginIds.add(id);
58
- } catch { /* skip if unreadable */ }
62
+ } catch {
63
+ /* skip if unreadable */
64
+ }
59
65
 
60
66
  try {
61
67
  const local = await getLocalEnabledPlugins(projectPath);
62
68
  for (const id of Object.keys(local)) allPluginIds.add(id);
63
- } catch { /* skip if unreadable */ }
69
+ } catch {
70
+ /* skip if unreadable */
71
+ }
64
72
  }
65
73
 
66
74
  // Parse marketplace names from plugin IDs
@@ -108,6 +116,75 @@ async function autoAddMissingMarketplaces(
108
116
  return added;
109
117
  }
110
118
 
119
+ const CONTINUITY_PLUGIN_SENTINEL = "tmux-claude-continuity";
120
+ const CONTINUITY_PLUGIN_SCRIPT = path.join(
121
+ os.homedir(),
122
+ ".tmux",
123
+ "plugins",
124
+ "tmux-claude-continuity",
125
+ "scripts",
126
+ "on_session_start.sh",
127
+ );
128
+
129
+ /**
130
+ * Ensure tmux-claude-continuity Claude Code hooks are present in global settings.
131
+ * If the tmux plugin is installed but the hooks are missing, they are appended.
132
+ * Returns a description of what was added, or null if nothing changed.
133
+ */
134
+ async function ensureTmuxContinuityHooks(): Promise<string | null> {
135
+ // Plugin not installed — nothing to do
136
+ if (!(await fs.pathExists(CONTINUITY_PLUGIN_SCRIPT))) {
137
+ return null;
138
+ }
139
+
140
+ const settings = await readGlobalSettings();
141
+
142
+ // Check if hooks are already configured by looking for the sentinel string
143
+ const existingHooks = settings.hooks ?? {};
144
+ for (const groups of Object.values(existingHooks)) {
145
+ for (const group of groups) {
146
+ for (const hook of group.hooks) {
147
+ if (hook.command.includes(CONTINUITY_PLUGIN_SENTINEL)) {
148
+ return null; // Already configured
149
+ }
150
+ }
151
+ }
152
+ }
153
+
154
+ // Append hooks, preserving any existing entries in SessionStart and Stop
155
+ const sessionStartGroups = existingHooks["SessionStart"] ?? [];
156
+ const stopGroups = existingHooks["Stop"] ?? [];
157
+
158
+ sessionStartGroups.push({
159
+ hooks: [
160
+ {
161
+ type: "command",
162
+ command:
163
+ "bash ~/.tmux/plugins/tmux-claude-continuity/scripts/on_session_start.sh",
164
+ },
165
+ ],
166
+ });
167
+
168
+ stopGroups.push({
169
+ hooks: [
170
+ {
171
+ type: "command",
172
+ command:
173
+ "bash ~/.tmux/plugins/tmux-claude-continuity/scripts/on_stop.sh",
174
+ },
175
+ ],
176
+ });
177
+
178
+ settings.hooks = {
179
+ ...existingHooks,
180
+ SessionStart: sessionStartGroups,
181
+ Stop: stopGroups,
182
+ };
183
+
184
+ await writeGlobalSettings(settings);
185
+ return "SessionStart + Stop hooks";
186
+ }
187
+
111
188
  /**
112
189
  * Prerun orchestration: Check for updates, apply them, then run claude
113
190
  * @param claudeArgs - Arguments to pass to claude CLI
@@ -123,9 +200,12 @@ export async function prerunClaude(
123
200
  try {
124
201
  // STEP 0: Migrate old marketplace names → magus (idempotent, no-ops if already migrated)
125
202
  const migration = await migrateMarketplaceRename();
126
- const migTotal = migration.projectMigrated + migration.globalMigrated
127
- + migration.localMigrated + migration.registryMigrated
128
- + (migration.knownMarketplacesMigrated ? 1 : 0);
203
+ const migTotal =
204
+ migration.projectMigrated +
205
+ migration.globalMigrated +
206
+ migration.localMigrated +
207
+ migration.registryMigrated +
208
+ (migration.knownMarketplacesMigrated ? 1 : 0);
129
209
  if (migTotal > 0) {
130
210
  console.log(`✓ Migrated ${migTotal} plugin reference(s) → magus`);
131
211
  }
@@ -142,6 +222,14 @@ export async function prerunClaude(
142
222
  }
143
223
  }
144
224
 
225
+ // STEP 0.6: Ensure tmux-claude-continuity hooks are configured
226
+ const addedHooks = await ensureTmuxContinuityHooks();
227
+ if (addedHooks) {
228
+ console.log(
229
+ `✓ Added tmux-claude-continuity hooks to ~/.claude/settings.json`,
230
+ );
231
+ }
232
+
145
233
  // STEP 1: Check if we should update (time-based cache, or forced)
146
234
  const shouldUpdate = options.force || (await cache.shouldCheckForUpdates());
147
235
 
@@ -498,7 +498,9 @@ export async function migrateMarketplaceRename(projectPath) {
498
498
  result.projectMigrated = count;
499
499
  }
500
500
  }
501
- catch { /* skip if unreadable */ }
501
+ catch {
502
+ /* skip if unreadable */
503
+ }
502
504
  // 2. Global settings
503
505
  try {
504
506
  const settings = await readGlobalSettings();
@@ -508,7 +510,9 @@ export async function migrateMarketplaceRename(projectPath) {
508
510
  result.globalMigrated = count;
509
511
  }
510
512
  }
511
- catch { /* skip if unreadable */ }
513
+ catch {
514
+ /* skip if unreadable */
515
+ }
512
516
  // 3. Local settings (settings.local.json)
513
517
  try {
514
518
  const local = await readLocalSettings(projectPath);
@@ -528,7 +532,9 @@ export async function migrateMarketplaceRename(projectPath) {
528
532
  result.localMigrated = localCount;
529
533
  }
530
534
  }
531
- catch { /* skip if unreadable */ }
535
+ catch {
536
+ /* skip if unreadable */
537
+ }
532
538
  // 4. known_marketplaces.json — rename old keys + physical directory cleanup
533
539
  const pluginsDir = path.join(os.homedir(), ".claude", "plugins", "marketplaces");
534
540
  const newDir = path.join(pluginsDir, NEW_MARKETPLACE_NAME);
@@ -553,8 +559,7 @@ export async function migrateMarketplaceRename(projectPath) {
553
559
  }
554
560
  // Ensure installLocation doesn't reference old directory names
555
561
  if (known[NEW_MARKETPLACE_NAME]?.installLocation?.includes(oldName)) {
556
- known[NEW_MARKETPLACE_NAME].installLocation =
557
- known[NEW_MARKETPLACE_NAME].installLocation.replace(oldName, NEW_MARKETPLACE_NAME);
562
+ known[NEW_MARKETPLACE_NAME].installLocation = known[NEW_MARKETPLACE_NAME].installLocation.replace(oldName, NEW_MARKETPLACE_NAME);
558
563
  knownModified = true;
559
564
  }
560
565
  }
@@ -563,7 +568,9 @@ export async function migrateMarketplaceRename(projectPath) {
563
568
  result.knownMarketplacesMigrated = true;
564
569
  }
565
570
  }
566
- catch { /* skip if unreadable */ }
571
+ catch {
572
+ /* skip if unreadable */
573
+ }
567
574
  // 4b. Rename/remove old physical directories (runs even if keys were already migrated)
568
575
  for (const oldName of OLD_MARKETPLACE_NAMES) {
569
576
  const oldDir = path.join(pluginsDir, oldName);
@@ -578,24 +585,32 @@ export async function migrateMarketplaceRename(projectPath) {
578
585
  }
579
586
  }
580
587
  }
581
- catch { /* non-fatal: directory cleanup is best-effort */ }
588
+ catch {
589
+ /* non-fatal: directory cleanup is best-effort */
590
+ }
582
591
  }
583
592
  // 4c. Update git remote URL in the marketplace clone (old → new repo)
584
593
  try {
585
594
  if (await fs.pathExists(path.join(newDir, ".git"))) {
586
595
  const { execSync } = await import("node:child_process");
587
596
  const remote = execSync("git remote get-url origin", {
588
- cwd: newDir, encoding: "utf-8", timeout: 5000,
597
+ cwd: newDir,
598
+ encoding: "utf-8",
599
+ timeout: 5000,
589
600
  }).trim();
590
601
  if (remote.includes("claude-code") && remote.includes("MadAppGang")) {
591
602
  const newRemote = remote.replace("claude-code", NEW_MARKETPLACE_NAME);
592
603
  execSync(`git remote set-url origin "${newRemote}"`, {
593
- cwd: newDir, encoding: "utf-8", timeout: 5000,
604
+ cwd: newDir,
605
+ encoding: "utf-8",
606
+ timeout: 5000,
594
607
  });
595
608
  }
596
609
  }
597
610
  }
598
- catch { /* non-fatal: git remote update is best-effort */ }
611
+ catch {
612
+ /* non-fatal: git remote update is best-effort */
613
+ }
599
614
  // 5. installed_plugins.json — rename plugin ID keys
600
615
  try {
601
616
  const registry = await readInstalledPluginsRegistry();
@@ -621,7 +636,9 @@ export async function migrateMarketplaceRename(projectPath) {
621
636
  result.registryMigrated = regCount;
622
637
  }
623
638
  }
624
- catch { /* skip if unreadable */ }
639
+ catch {
640
+ /* skip if unreadable */
641
+ }
625
642
  return result;
626
643
  }
627
644
  /**
@@ -732,7 +732,9 @@ export async function migrateMarketplaceRename(
732
732
  await writeSettings(settings, projectPath);
733
733
  result.projectMigrated = count;
734
734
  }
735
- } catch { /* skip if unreadable */ }
735
+ } catch {
736
+ /* skip if unreadable */
737
+ }
736
738
 
737
739
  // 2. Global settings
738
740
  try {
@@ -742,24 +744,39 @@ export async function migrateMarketplaceRename(
742
744
  await writeGlobalSettings(settings);
743
745
  result.globalMigrated = count;
744
746
  }
745
- } catch { /* skip if unreadable */ }
747
+ } catch {
748
+ /* skip if unreadable */
749
+ }
746
750
 
747
751
  // 3. Local settings (settings.local.json)
748
752
  try {
749
753
  const local = await readLocalSettings(projectPath);
750
754
  let localCount = 0;
751
755
  const [ep, epCount] = migratePluginKeys(local.enabledPlugins);
752
- if (epCount > 0) { local.enabledPlugins = ep; localCount += epCount; }
756
+ if (epCount > 0) {
757
+ local.enabledPlugins = ep;
758
+ localCount += epCount;
759
+ }
753
760
  const [iv, ivCount] = migratePluginKeys(local.installedPluginVersions);
754
- if (ivCount > 0) { local.installedPluginVersions = iv; localCount += ivCount; }
761
+ if (ivCount > 0) {
762
+ local.installedPluginVersions = iv;
763
+ localCount += ivCount;
764
+ }
755
765
  if (localCount > 0) {
756
766
  await writeLocalSettings(local, projectPath);
757
767
  result.localMigrated = localCount;
758
768
  }
759
- } catch { /* skip if unreadable */ }
769
+ } catch {
770
+ /* skip if unreadable */
771
+ }
760
772
 
761
773
  // 4. known_marketplaces.json — rename old keys + physical directory cleanup
762
- const pluginsDir = path.join(os.homedir(), ".claude", "plugins", "marketplaces");
774
+ const pluginsDir = path.join(
775
+ os.homedir(),
776
+ ".claude",
777
+ "plugins",
778
+ "marketplaces",
779
+ );
763
780
  const newDir = path.join(pluginsDir, NEW_MARKETPLACE_NAME);
764
781
 
765
782
  try {
@@ -785,11 +802,9 @@ export async function migrateMarketplaceRename(
785
802
 
786
803
  // Ensure installLocation doesn't reference old directory names
787
804
  if (known[NEW_MARKETPLACE_NAME]?.installLocation?.includes(oldName)) {
788
- known[NEW_MARKETPLACE_NAME].installLocation =
789
- known[NEW_MARKETPLACE_NAME].installLocation.replace(
790
- oldName,
791
- NEW_MARKETPLACE_NAME,
792
- );
805
+ known[NEW_MARKETPLACE_NAME].installLocation = known[
806
+ NEW_MARKETPLACE_NAME
807
+ ].installLocation.replace(oldName, NEW_MARKETPLACE_NAME);
793
808
  knownModified = true;
794
809
  }
795
810
  }
@@ -798,7 +813,9 @@ export async function migrateMarketplaceRename(
798
813
  await writeKnownMarketplaces(known);
799
814
  result.knownMarketplacesMigrated = true;
800
815
  }
801
- } catch { /* skip if unreadable */ }
816
+ } catch {
817
+ /* skip if unreadable */
818
+ }
802
819
 
803
820
  // 4b. Rename/remove old physical directories (runs even if keys were already migrated)
804
821
  for (const oldName of OLD_MARKETPLACE_NAMES) {
@@ -812,7 +829,9 @@ export async function migrateMarketplaceRename(
812
829
  await fs.remove(oldDir);
813
830
  }
814
831
  }
815
- } catch { /* non-fatal: directory cleanup is best-effort */ }
832
+ } catch {
833
+ /* non-fatal: directory cleanup is best-effort */
834
+ }
816
835
  }
817
836
 
818
837
  // 4c. Update git remote URL in the marketplace clone (old → new repo)
@@ -820,16 +839,22 @@ export async function migrateMarketplaceRename(
820
839
  if (await fs.pathExists(path.join(newDir, ".git"))) {
821
840
  const { execSync } = await import("node:child_process");
822
841
  const remote = execSync("git remote get-url origin", {
823
- cwd: newDir, encoding: "utf-8", timeout: 5000,
842
+ cwd: newDir,
843
+ encoding: "utf-8",
844
+ timeout: 5000,
824
845
  }).trim();
825
846
  if (remote.includes("claude-code") && remote.includes("MadAppGang")) {
826
847
  const newRemote = remote.replace("claude-code", NEW_MARKETPLACE_NAME);
827
848
  execSync(`git remote set-url origin "${newRemote}"`, {
828
- cwd: newDir, encoding: "utf-8", timeout: 5000,
849
+ cwd: newDir,
850
+ encoding: "utf-8",
851
+ timeout: 5000,
829
852
  });
830
853
  }
831
854
  }
832
- } catch { /* non-fatal: git remote update is best-effort */ }
855
+ } catch {
856
+ /* non-fatal: git remote update is best-effort */
857
+ }
833
858
 
834
859
  // 5. installed_plugins.json — rename plugin ID keys
835
860
  try {
@@ -837,7 +862,9 @@ export async function migrateMarketplaceRename(
837
862
  let regCount = 0;
838
863
  const newPlugins: typeof registry.plugins = {};
839
864
  for (const [pluginId, entries] of Object.entries(registry.plugins)) {
840
- const oldName = OLD_MARKETPLACE_NAMES.find((n) => pluginId.endsWith(`@${n}`));
865
+ const oldName = OLD_MARKETPLACE_NAMES.find((n) =>
866
+ pluginId.endsWith(`@${n}`),
867
+ );
841
868
  if (oldName) {
842
869
  const pluginName = pluginId.slice(0, pluginId.lastIndexOf("@"));
843
870
  const newKey = `${pluginName}@${NEW_MARKETPLACE_NAME}`;
@@ -854,7 +881,9 @@ export async function migrateMarketplaceRename(
854
881
  await writeInstalledPluginsRegistry(registry);
855
882
  result.registryMigrated = regCount;
856
883
  }
857
- } catch { /* skip if unreadable */ }
884
+ } catch {
885
+ /* skip if unreadable */
886
+ }
858
887
 
859
888
  return result;
860
889
  }
@@ -243,3 +243,36 @@ export interface ProfileEntry {
243
243
  updatedAt: string;
244
244
  scope: "user" | "project";
245
245
  }
246
+
247
+ // ─── Predefined Profile Types ──────────────────────────────────────────────────
248
+
249
+ /** A skill reference for predefined profiles */
250
+ export interface PredefinedSkill {
251
+ name: string;
252
+ repo: string;
253
+ skillPath: string;
254
+ }
255
+
256
+ /** Settings that can be configured in a predefined profile */
257
+ export interface PredefinedSettings {
258
+ effortLevel?: "low" | "medium" | "high";
259
+ alwaysThinkingEnabled?: boolean;
260
+ model?: "claude-sonnet-4-6" | "claude-opus-4-6";
261
+ outputStyle?: "concise" | "explanatory" | "formal";
262
+ CLAUDE_CODE_ENABLE_TASKS?: boolean;
263
+ CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS?: boolean;
264
+ includeGitInstructions?: boolean;
265
+ respectGitignore?: boolean;
266
+ enableAllProjectMcpServers?: boolean;
267
+ }
268
+
269
+ /** A predefined (built-in) profile for claudeup */
270
+ export interface PredefinedProfile {
271
+ id: string;
272
+ name: string;
273
+ description: string;
274
+ targetAudience: string;
275
+ plugins: Record<string, boolean>;
276
+ skills: PredefinedSkill[];
277
+ settings: PredefinedSettings;
278
+ }
@@ -87,11 +87,11 @@ const predefinedRenderer = {
87
87
  const { profile } = item;
88
88
  const pluginCount = profile.magusPlugins.length + profile.anthropicPlugins.length;
89
89
  const skillCount = profile.skills.length;
90
- const label = truncate(`${profile.name} — ${pluginCount} plugins · ${skillCount} skill${skillCount !== 1 ? "s" : ""}`, 45);
90
+ const countStr = `${pluginCount}p ${skillCount}s`;
91
91
  if (isSelected) {
92
- return (_jsxs("text", { bg: "blue", fg: "white", children: [" ", label, " "] }));
92
+ return (_jsxs("text", { bg: theme.selection.bg, fg: theme.selection.fg, children: [" ", profile.name, " ", countStr, " "] }));
93
93
  }
94
- return (_jsxs("text", { children: [_jsx("span", { fg: theme.colors.muted, children: "- " }), _jsx("span", { fg: theme.colors.text, children: label })] }));
94
+ return (_jsxs("text", { children: [_jsxs("span", { fg: "white", children: [" ", profile.name] }), _jsxs("span", { fg: theme.colors.dim, children: [" ", countStr] })] }));
95
95
  },
96
96
  renderDetail: ({ item }) => {
97
97
  const { profile } = item;
@@ -99,7 +99,7 @@ const predefinedRenderer = {
99
99
  const envMap = profile.settings["env"] ?? {};
100
100
  const tasksOn = envMap["CLAUDE_CODE_ENABLE_TASKS"] === "true";
101
101
  const teamsOn = envMap["CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS"] === "true";
102
- return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: theme.colors.info, children: _jsx("strong", { children: profile.name }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: theme.colors.muted, children: profile.description }) }), _jsx("box", { children: _jsx("text", { fg: theme.colors.muted, children: `\nMagus (${profile.magusPlugins.length})` }) }), _jsx("box", { children: _jsx("text", { fg: "#00bfa5", children: profile.magusPlugins.map((p) => ` ■ ${p}`).join("\n") }) }), _jsx("box", { children: _jsx("text", { fg: theme.colors.muted, children: `\nAnthropic (${profile.anthropicPlugins.length})` }) }), _jsx("box", { children: _jsx("text", { fg: "#b39ddb", children: profile.anthropicPlugins.map((p) => ` ■ ${p}`).join("\n") }) }), profile.skills.length > 0 && (_jsxs(_Fragment, { children: [_jsx("box", { children: _jsx("text", { fg: theme.colors.muted, children: `\nSkills (${profile.skills.length})` }) }), _jsx("box", { children: _jsx("text", { fg: "#ffd54f", children: profile.skills.map((s) => ` ■ ${s}`).join("\n") }) })] })), _jsx("box", { children: _jsx("text", { fg: theme.colors.dim, children: `\n${DIVIDER}` }) }), _jsx("box", { children: _jsx("text", { fg: theme.colors.muted, children: [
102
+ return (_jsxs("box", { flexDirection: "column", children: [_jsx("box", { children: _jsx("text", { bg: theme.colors.accent, fg: "white", children: _jsxs("strong", { children: [" ", profile.name, " "] }) }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: theme.colors.muted, children: profile.description }) }), _jsx("box", { children: _jsx("text", { fg: theme.colors.muted, children: `\nMagus (${profile.magusPlugins.length})` }) }), _jsx("box", { children: _jsx("text", { fg: "#00bfa5", children: profile.magusPlugins.map((p) => ` ■ ${p}`).join("\n") }) }), _jsx("box", { children: _jsx("text", { fg: theme.colors.muted, children: `\nAnthropic (${profile.anthropicPlugins.length})` }) }), _jsx("box", { children: _jsx("text", { fg: "#b39ddb", children: profile.anthropicPlugins.map((p) => ` ■ ${p}`).join("\n") }) }), profile.skills.length > 0 && (_jsxs(_Fragment, { children: [_jsx("box", { children: _jsx("text", { fg: theme.colors.muted, children: `\nSkills (${profile.skills.length})` }) }), _jsx("box", { children: _jsx("text", { fg: "#ffd54f", children: profile.skills.map((s) => ` ■ ${s}`).join("\n") }) })] })), _jsx("box", { children: _jsx("text", { fg: theme.colors.dim, children: `\n${DIVIDER}` }) }), _jsx("box", { children: _jsx("text", { fg: theme.colors.muted, children: [
103
103
  ...settingEntries.map(([k, v]) => ` ${humanizeKey(k).padEnd(18)}${humanizeValue(k, v)}`),
104
104
  ...(tasksOn ? [` ${"Tasks".padEnd(18)}on`] : []),
105
105
  ...(teamsOn ? [` ${"Agent Teams".padEnd(18)}on`] : []),
@@ -112,24 +112,20 @@ const predefinedRenderer: ItemRenderer<{ kind: "predefined"; profile: Predefined
112
112
  const pluginCount =
113
113
  profile.magusPlugins.length + profile.anthropicPlugins.length;
114
114
  const skillCount = profile.skills.length;
115
- const label = truncate(
116
- `${profile.name} — ${pluginCount} plugins · ${skillCount} skill${skillCount !== 1 ? "s" : ""}`,
117
- 45,
118
- );
115
+ const countStr = `${pluginCount}p ${skillCount}s`;
119
116
 
120
117
  if (isSelected) {
121
118
  return (
122
- <text bg="blue" fg="white">
123
- {" "}
124
- {label}{" "}
119
+ <text bg={theme.selection.bg} fg={theme.selection.fg}>
120
+ {" "}{profile.name} {countStr}{" "}
125
121
  </text>
126
122
  );
127
123
  }
128
124
 
129
125
  return (
130
126
  <text>
131
- <span fg={theme.colors.muted}>{"- "}</span>
132
- <span fg={theme.colors.text}>{label}</span>
127
+ <span fg="white">{" "}{profile.name}</span>
128
+ <span fg={theme.colors.dim}> {countStr}</span>
133
129
  </text>
134
130
  );
135
131
  },
@@ -146,9 +142,11 @@ const predefinedRenderer: ItemRenderer<{ kind: "predefined"; profile: Predefined
146
142
 
147
143
  return (
148
144
  <box flexDirection="column">
149
- <text fg={theme.colors.info}>
150
- <strong>{profile.name}</strong>
151
- </text>
145
+ <box>
146
+ <text bg={theme.colors.accent} fg="white">
147
+ <strong> {profile.name} </strong>
148
+ </text>
149
+ </box>
152
150
  <box marginTop={1}>
153
151
  <text fg={theme.colors.muted}>{profile.description}</text>
154
152
  </box>