claudeup 4.0.0 → 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.
@@ -16,9 +16,7 @@ import {
16
16
  removeGlobalInstalledPluginVersion,
17
17
  removeLocalInstalledPluginVersion,
18
18
  } from "./claude-settings.js";
19
- import {
20
- removeInstalledPluginVersion,
21
- } from "./plugin-manager.js";
19
+ import { removeInstalledPluginVersion } from "./plugin-manager.js";
22
20
 
23
21
  const execFileAsync = promisify(execFile);
24
22
 
@@ -44,10 +42,7 @@ async function getClaudePath(): Promise<string> {
44
42
  * @param timeoutMs - Timeout in milliseconds (default: 30s)
45
43
  * @returns stdout from the command
46
44
  */
47
- async function execClaude(
48
- args: string[],
49
- timeoutMs = 30000,
50
- ): Promise<string> {
45
+ async function execClaude(args: string[], timeoutMs = 30000): Promise<string> {
51
46
  const claudePath = await getClaudePath();
52
47
  try {
53
48
  const { stdout } = await execFileAsync(claudePath, args, {
@@ -148,6 +143,14 @@ export async function addMarketplace(repo: string): Promise<void> {
148
143
  await execClaude(["plugin", "marketplace", "add", repo], 60000);
149
144
  }
150
145
 
146
+ /**
147
+ * Update marketplace cache by running git pull via Claude CLI
148
+ * Uses longer timeout since this involves git network operations
149
+ */
150
+ export async function updateMarketplace(name: string): Promise<void> {
151
+ await execClaude(["plugin", "marketplace", "update", name], 60000);
152
+ }
153
+
151
154
  /**
152
155
  * Check if the claude CLI is available
153
156
  * @returns true if claude CLI is found in PATH
@@ -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
  }
@@ -5,6 +5,7 @@ import { getConfiguredMarketplaces, getEnabledPlugins, readSettings, writeSettin
5
5
  import { defaultMarketplaces } from "../data/marketplaces.js";
6
6
  import { scanLocalMarketplaces, repairAllMarketplaces, } from "./local-marketplace.js";
7
7
  import { formatMarketplaceName, isValidGitHubRepo, parsePluginId, } from "../utils/string-utils.js";
8
+ import { updateMarketplace } from "./claude-cli.js";
8
9
  // Cache for local marketplaces (session-level) - Promise-based to prevent race conditions
9
10
  let localMarketplacesPromise = null;
10
11
  // Session-level cache for fetched marketplace data (no TTL - persists until explicit refresh)
@@ -104,12 +105,16 @@ export async function getAvailablePlugins(projectPath) {
104
105
  marketplaceNames.add(mp.name);
105
106
  }
106
107
  }
108
+ // Fetch local marketplace caches up front so we can detect stale caches
109
+ let localMarketplaces = await getLocalMarketplaces();
107
110
  // Fetch plugins from each configured marketplace
108
111
  for (const mpName of marketplaceNames) {
109
112
  const marketplace = defaultMarketplaces.find((m) => m.name === mpName);
110
113
  if (!marketplace)
111
114
  continue;
112
115
  const marketplacePlugins = await fetchMarketplacePlugins(mpName, marketplace.source.repo);
116
+ // Auto-sync local cache if remote has plugins the local cache doesn't
117
+ localMarketplaces = await autoSyncIfStale(mpName, marketplacePlugins.map((p) => p.name), localMarketplaces);
113
118
  for (const plugin of marketplacePlugins) {
114
119
  const pluginId = `${plugin.name}@${mpName}`;
115
120
  const installedVersion = installedVersions[pluginId];
@@ -136,8 +141,6 @@ export async function getAvailablePlugins(projectPath) {
136
141
  });
137
142
  }
138
143
  }
139
- // Fetch ALL plugins from local marketplace caches (for marketplaces not in defaults)
140
- const localMarketplaces = await getLocalMarketplaces();
141
144
  for (const [mpName, localMp] of localMarketplaces) {
142
145
  // Skip if already fetched from defaults
143
146
  if (marketplaceNames.has(mpName))
@@ -256,12 +259,16 @@ export async function getGlobalAvailablePlugins() {
256
259
  marketplaceNames.add(mp.name);
257
260
  }
258
261
  }
262
+ // Fetch local marketplace caches up front so we can detect stale caches
263
+ let localMarketplaces = await getLocalMarketplaces();
259
264
  // Fetch plugins from each configured marketplace
260
265
  for (const mpName of marketplaceNames) {
261
266
  const marketplace = defaultMarketplaces.find((m) => m.name === mpName);
262
267
  if (!marketplace)
263
268
  continue;
264
269
  const marketplacePlugins = await fetchMarketplacePlugins(mpName, marketplace.source.repo);
270
+ // Auto-sync local cache if remote has plugins the local cache doesn't
271
+ localMarketplaces = await autoSyncIfStale(mpName, marketplacePlugins.map((p) => p.name), localMarketplaces);
265
272
  for (const plugin of marketplacePlugins) {
266
273
  const pluginId = `${plugin.name}@${mpName}`;
267
274
  const installedVersion = installedVersions[pluginId];
@@ -288,8 +295,6 @@ export async function getGlobalAvailablePlugins() {
288
295
  });
289
296
  }
290
297
  }
291
- // Fetch ALL plugins from local marketplace caches (for marketplaces not in defaults)
292
- const localMarketplaces = await getLocalMarketplaces();
293
298
  for (const [mpName, localMp] of localMarketplaces) {
294
299
  // Skip if already fetched from defaults
295
300
  if (marketplaceNames.has(mpName))
@@ -424,6 +429,37 @@ async function getLocalMarketplaces() {
424
429
  export async function getLocalMarketplacesInfo() {
425
430
  return getLocalMarketplaces();
426
431
  }
432
+ // Track which marketplaces have already been auto-synced this session
433
+ // so we only attempt the update once per marketplace per claudeup run.
434
+ const autoSyncedMarketplaces = new Set();
435
+ /**
436
+ * If the remote manifest lists plugins that aren't in the local cache,
437
+ * the local clone is stale. Silently run `claude plugin marketplace update`
438
+ * to pull the latest, then invalidate the local cache so the next scan
439
+ * picks up the new plugins.
440
+ */
441
+ async function autoSyncIfStale(mpName, remotePluginNames, localMarketplaces) {
442
+ if (autoSyncedMarketplaces.has(mpName))
443
+ return localMarketplaces;
444
+ const localMp = localMarketplaces.get(mpName);
445
+ if (!localMp)
446
+ return localMarketplaces;
447
+ const localNames = new Set(localMp.plugins.map((p) => p.name));
448
+ const hasMissing = remotePluginNames.some((name) => !localNames.has(name));
449
+ if (!hasMissing)
450
+ return localMarketplaces;
451
+ autoSyncedMarketplaces.add(mpName);
452
+ try {
453
+ await updateMarketplace(mpName);
454
+ // Invalidate local cache so re-scan picks up new plugins
455
+ localMarketplacesPromise = null;
456
+ return getLocalMarketplaces();
457
+ }
458
+ catch {
459
+ // Update failed (no network, CLI missing, etc.) — continue with stale data
460
+ return localMarketplaces;
461
+ }
462
+ }
427
463
  /**
428
464
  * Refresh claudeup's internal cache
429
465
  * Note: Marketplace updates should be done via Claude Code's /plugin marketplace update command
@@ -27,6 +27,7 @@ import {
27
27
  isValidGitHubRepo,
28
28
  parsePluginId,
29
29
  } from "../utils/string-utils.js";
30
+ import { updateMarketplace } from "./claude-cli.js";
30
31
 
31
32
  // Cache for local marketplaces (session-level) - Promise-based to prevent race conditions
32
33
  let localMarketplacesPromise: Promise<Map<string, LocalMarketplace>> | null =
@@ -206,6 +207,9 @@ export async function getAvailablePlugins(
206
207
  }
207
208
  }
208
209
 
210
+ // Fetch local marketplace caches up front so we can detect stale caches
211
+ let localMarketplaces = await getLocalMarketplaces();
212
+
209
213
  // Fetch plugins from each configured marketplace
210
214
  for (const mpName of marketplaceNames) {
211
215
  const marketplace = defaultMarketplaces.find((m) => m.name === mpName);
@@ -216,6 +220,13 @@ export async function getAvailablePlugins(
216
220
  marketplace.source.repo,
217
221
  );
218
222
 
223
+ // Auto-sync local cache if remote has plugins the local cache doesn't
224
+ localMarketplaces = await autoSyncIfStale(
225
+ mpName,
226
+ marketplacePlugins.map((p) => p.name),
227
+ localMarketplaces,
228
+ );
229
+
219
230
  for (const plugin of marketplacePlugins) {
220
231
  const pluginId = `${plugin.name}@${mpName}`;
221
232
  const installedVersion = installedVersions[pluginId];
@@ -245,9 +256,6 @@ export async function getAvailablePlugins(
245
256
  }
246
257
  }
247
258
 
248
- // Fetch ALL plugins from local marketplace caches (for marketplaces not in defaults)
249
- const localMarketplaces = await getLocalMarketplaces();
250
-
251
259
  for (const [mpName, localMp] of localMarketplaces) {
252
260
  // Skip if already fetched from defaults
253
261
  if (marketplaceNames.has(mpName)) continue;
@@ -383,6 +391,9 @@ export async function getGlobalAvailablePlugins(): Promise<PluginInfo[]> {
383
391
  }
384
392
  }
385
393
 
394
+ // Fetch local marketplace caches up front so we can detect stale caches
395
+ let localMarketplaces = await getLocalMarketplaces();
396
+
386
397
  // Fetch plugins from each configured marketplace
387
398
  for (const mpName of marketplaceNames) {
388
399
  const marketplace = defaultMarketplaces.find((m) => m.name === mpName);
@@ -393,6 +404,13 @@ export async function getGlobalAvailablePlugins(): Promise<PluginInfo[]> {
393
404
  marketplace.source.repo,
394
405
  );
395
406
 
407
+ // Auto-sync local cache if remote has plugins the local cache doesn't
408
+ localMarketplaces = await autoSyncIfStale(
409
+ mpName,
410
+ marketplacePlugins.map((p) => p.name),
411
+ localMarketplaces,
412
+ );
413
+
396
414
  for (const plugin of marketplacePlugins) {
397
415
  const pluginId = `${plugin.name}@${mpName}`;
398
416
  const installedVersion = installedVersions[pluginId];
@@ -422,9 +440,6 @@ export async function getGlobalAvailablePlugins(): Promise<PluginInfo[]> {
422
440
  }
423
441
  }
424
442
 
425
- // Fetch ALL plugins from local marketplace caches (for marketplaces not in defaults)
426
- const localMarketplaces = await getLocalMarketplaces();
427
-
428
443
  for (const [mpName, localMp] of localMarketplaces) {
429
444
  // Skip if already fetched from defaults
430
445
  if (marketplaceNames.has(mpName)) continue;
@@ -598,6 +613,42 @@ export async function getLocalMarketplacesInfo(): Promise<
598
613
  return getLocalMarketplaces();
599
614
  }
600
615
 
616
+ // Track which marketplaces have already been auto-synced this session
617
+ // so we only attempt the update once per marketplace per claudeup run.
618
+ const autoSyncedMarketplaces = new Set<string>();
619
+
620
+ /**
621
+ * If the remote manifest lists plugins that aren't in the local cache,
622
+ * the local clone is stale. Silently run `claude plugin marketplace update`
623
+ * to pull the latest, then invalidate the local cache so the next scan
624
+ * picks up the new plugins.
625
+ */
626
+ async function autoSyncIfStale(
627
+ mpName: string,
628
+ remotePluginNames: string[],
629
+ localMarketplaces: Map<string, LocalMarketplace>,
630
+ ): Promise<Map<string, LocalMarketplace>> {
631
+ if (autoSyncedMarketplaces.has(mpName)) return localMarketplaces;
632
+
633
+ const localMp = localMarketplaces.get(mpName);
634
+ if (!localMp) return localMarketplaces;
635
+
636
+ const localNames = new Set(localMp.plugins.map((p) => p.name));
637
+ const hasMissing = remotePluginNames.some((name) => !localNames.has(name));
638
+ if (!hasMissing) return localMarketplaces;
639
+
640
+ autoSyncedMarketplaces.add(mpName);
641
+ try {
642
+ await updateMarketplace(mpName);
643
+ // Invalidate local cache so re-scan picks up new plugins
644
+ localMarketplacesPromise = null;
645
+ return getLocalMarketplaces();
646
+ } catch {
647
+ // Update failed (no network, CLI missing, etc.) — continue with stale data
648
+ return localMarketplaces;
649
+ }
650
+ }
651
+
601
652
  export interface RefreshAndRepairResult {
602
653
  refresh: never[];
603
654
  repair: RepairMarketplaceResult[];
@@ -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
+ }
@@ -0,0 +1,23 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
2
+ import { useState, useEffect } from "react";
3
+ /**
4
+ * A scrollable detail panel that renders an array of lines
5
+ * with automatic scroll tracking. When content exceeds maxHeight,
6
+ * it shows a scroll indicator and clips to fit.
7
+ */
8
+ export function ScrollableDetail({ lines, maxHeight, scrollTrigger = 0, }) {
9
+ const [scrollOffset, setScrollOffset] = useState(0);
10
+ // Reset scroll when content changes (new item selected)
11
+ useEffect(() => {
12
+ setScrollOffset(0);
13
+ }, [scrollTrigger]);
14
+ const totalLines = lines.length;
15
+ const visibleLines = Math.max(1, maxHeight - 1); // -1 for scroll indicator
16
+ const canScroll = totalLines > visibleLines;
17
+ const maxOffset = Math.max(0, totalLines - visibleLines);
18
+ const clampedOffset = Math.min(scrollOffset, maxOffset);
19
+ const visibleContent = lines.slice(clampedOffset, clampedOffset + visibleLines);
20
+ const scrollUp = clampedOffset > 0;
21
+ const scrollDown = clampedOffset < maxOffset;
22
+ return (_jsxs("box", { flexDirection: "column", children: [scrollUp && (_jsx("box", { children: _jsxs("text", { fg: "cyan", children: ["\u2191 ", clampedOffset, " more"] }) })), visibleContent, scrollDown && (_jsx("box", { children: _jsxs("text", { fg: "cyan", children: ["\u2193 ", totalLines - clampedOffset - visibleLines, " more"] }) }))] }));
23
+ }
@@ -0,0 +1,55 @@
1
+ import React, { useState, useEffect } from "react";
2
+
3
+ interface ScrollableDetailProps {
4
+ /** Array of content lines to display */
5
+ lines: React.ReactNode[];
6
+ /** Maximum visible height */
7
+ maxHeight: number;
8
+ /** External scroll trigger — changes when list selection changes */
9
+ scrollTrigger?: number;
10
+ }
11
+
12
+ /**
13
+ * A scrollable detail panel that renders an array of lines
14
+ * with automatic scroll tracking. When content exceeds maxHeight,
15
+ * it shows a scroll indicator and clips to fit.
16
+ */
17
+ export function ScrollableDetail({
18
+ lines,
19
+ maxHeight,
20
+ scrollTrigger = 0,
21
+ }: ScrollableDetailProps) {
22
+ const [scrollOffset, setScrollOffset] = useState(0);
23
+
24
+ // Reset scroll when content changes (new item selected)
25
+ useEffect(() => {
26
+ setScrollOffset(0);
27
+ }, [scrollTrigger]);
28
+
29
+ const totalLines = lines.length;
30
+ const visibleLines = Math.max(1, maxHeight - 1); // -1 for scroll indicator
31
+ const canScroll = totalLines > visibleLines;
32
+ const maxOffset = Math.max(0, totalLines - visibleLines);
33
+ const clampedOffset = Math.min(scrollOffset, maxOffset);
34
+
35
+ const visibleContent = lines.slice(clampedOffset, clampedOffset + visibleLines);
36
+
37
+ const scrollUp = clampedOffset > 0;
38
+ const scrollDown = clampedOffset < maxOffset;
39
+
40
+ return (
41
+ <box flexDirection="column">
42
+ {scrollUp && (
43
+ <box>
44
+ <text fg="cyan">↑ {clampedOffset} more</text>
45
+ </box>
46
+ )}
47
+ {visibleContent}
48
+ {scrollDown && (
49
+ <box>
50
+ <text fg="cyan">↓ {totalLines - clampedOffset - visibleLines} more</text>
51
+ </box>
52
+ )}
53
+ </box>
54
+ );
55
+ }
@@ -10,6 +10,6 @@ export function ScreenLayout({ title, subtitle, currentScreen, search, statusLin
10
10
  const fixedHeight = 6 + (hasSearchBar ? 1 : 0);
11
11
  const panelHeight = Math.max(5, dimensions.contentHeight - fixedHeight);
12
12
  const lineWidth = Math.max(10, dimensions.terminalWidth - 4);
13
- return (_jsxs("box", { flexDirection: "column", height: dimensions.contentHeight, children: [_jsx("box", { height: 1, paddingLeft: 1, paddingRight: 1, children: _jsx("text", { fg: "#333333", children: "─".repeat(lineWidth) }) }), _jsx("box", { height: 1, paddingLeft: 1, paddingRight: 1, children: _jsx(TabBar, { currentScreen: currentScreen }) }), _jsx("box", { height: 1, paddingLeft: 1, paddingRight: 1, children: _jsx("text", { fg: "#333333", children: "─".repeat(lineWidth) }) }), _jsxs("box", { height: 1, paddingLeft: 1, paddingRight: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsx("text", { fg: HEADER_COLOR, children: _jsx("strong", { children: title }) }), subtitle && _jsx("text", { fg: "gray", children: subtitle }), !subtitle && statusLine ? statusLine : null] }), hasSearchBar && (_jsx("box", { height: 1, paddingLeft: 1, paddingRight: 1, children: search.isActive ? (_jsxs("text", { children: [_jsx("span", { fg: "green", children: "Filter: " }), _jsx("span", { fg: "white", children: search.query }), _jsx("span", { bg: "white", fg: "black", children: " " })] })) : (_jsxs("text", { children: [_jsx("span", { fg: "green", children: "Filter: " }), _jsx("span", { fg: "yellow", children: search.query }), _jsx("span", { fg: "gray", children: " (Esc to clear)" })] })) })), _jsx("box", { height: 1, paddingLeft: 1, paddingRight: 1, children: _jsx("text", { fg: "#444444", children: "─".repeat(lineWidth) }) }), _jsxs("box", { flexDirection: "row", height: panelHeight, children: [_jsx("box", { flexDirection: "column", width: "49%", height: panelHeight, paddingRight: 1, children: listPanel }), _jsx("box", { flexDirection: "column", width: 1, height: panelHeight, children: _jsx("text", { fg: "#444444", children: "│".repeat(panelHeight) }) }), _jsx("box", { flexDirection: "column", width: "50%", height: panelHeight, paddingLeft: 1, children: detailPanel })] }), _jsx("box", { height: 1, paddingLeft: 1, children: _jsx("text", { fg: "gray", children: footerHints }) })] }));
13
+ return (_jsxs("box", { flexDirection: "column", height: dimensions.contentHeight, children: [_jsx("box", { height: 1, paddingLeft: 1, paddingRight: 1, children: _jsx("text", { fg: "#333333", children: "─".repeat(lineWidth) }) }), _jsx("box", { height: 1, paddingLeft: 1, paddingRight: 1, children: _jsx(TabBar, { currentScreen: currentScreen }) }), _jsx("box", { height: 1, paddingLeft: 1, paddingRight: 1, children: _jsx("text", { fg: "#333333", children: "─".repeat(lineWidth) }) }), _jsxs("box", { height: 1, paddingLeft: 1, paddingRight: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsx("text", { fg: HEADER_COLOR, children: _jsx("strong", { children: title }) }), subtitle && _jsx("text", { fg: "gray", children: subtitle }), !subtitle && statusLine ? statusLine : null] }), hasSearchBar && (_jsx("box", { height: 1, paddingLeft: 1, paddingRight: 1, children: search.isActive ? (_jsxs("text", { children: [_jsx("span", { fg: "green", children: "Filter: " }), _jsx("span", { fg: "white", children: search.query }), _jsx("span", { bg: "white", fg: "black", children: " " })] })) : (_jsxs("text", { children: [_jsx("span", { fg: "green", children: "Filter: " }), _jsx("span", { fg: "yellow", children: search.query }), _jsx("span", { fg: "gray", children: " (Esc to clear)" })] })) })), _jsx("box", { height: 1, paddingLeft: 1, paddingRight: 1, children: _jsx("text", { fg: "#444444", children: "─".repeat(lineWidth) }) }), _jsxs("box", { flexDirection: "row", height: panelHeight, children: [_jsx("box", { flexDirection: "column", width: "49%", height: panelHeight, paddingRight: 1, children: listPanel }), _jsx("box", { flexDirection: "column", width: 1, height: panelHeight, children: _jsx("text", { fg: "#444444", children: "│".repeat(panelHeight) }) }), _jsx("box", { width: "50%", height: panelHeight, paddingLeft: 1, children: _jsx("scrollbox", { height: panelHeight, scrollY: true, scrollX: false, children: _jsx("box", { flexDirection: "column", children: detailPanel }) }) })] }), _jsx("box", { height: 1, paddingLeft: 1, children: _jsx("text", { fg: "gray", children: footerHints }) })] }));
14
14
  }
15
15
  export default ScreenLayout;
@@ -118,14 +118,17 @@ export function ScreenLayout({
118
118
  <text fg="#444444">{"│".repeat(panelHeight)}</text>
119
119
  </box>
120
120
 
121
- {/* Detail panel */}
122
- <box
123
- flexDirection="column"
124
- width="50%"
125
- height={panelHeight}
126
- paddingLeft={1}
127
- >
128
- {detailPanel}
121
+ {/* Detail panel — scrollable */}
122
+ <box width="50%" height={panelHeight} paddingLeft={1}>
123
+ <scrollbox
124
+ height={panelHeight}
125
+ scrollY={true}
126
+ scrollX={false}
127
+ >
128
+ <box flexDirection="column">
129
+ {detailPanel}
130
+ </box>
131
+ </scrollbox>
129
132
  </box>
130
133
  </box>
131
134