claudeup 4.6.1 → 4.8.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 +1 -1
- package/src/__tests__/gap-fill-versions.test.ts +382 -0
- package/src/data/settings-catalog.js +2 -7
- package/src/data/settings-catalog.ts +2 -7
- package/src/opentui.d.ts +7 -2
- package/src/prerunner/index.js +31 -17
- package/src/prerunner/index.ts +35 -18
- package/src/services/claude-settings.js +74 -0
- package/src/services/claude-settings.ts +92 -0
- package/src/services/plugin-manager.js +13 -16
- package/src/services/plugin-manager.ts +17 -16
- package/src/services/settings-manager.js +84 -5
- package/src/services/settings-manager.ts +86 -5
- package/src/ui/adapters/settingsAdapter.js +8 -8
- package/src/ui/adapters/settingsAdapter.ts +8 -8
- package/src/ui/components/TabBar.js +1 -23
- package/src/ui/components/TabBar.tsx +1 -26
- package/src/ui/components/modals/ConfirmModal.js +1 -1
- package/src/ui/components/modals/ConfirmModal.tsx +17 -16
- package/src/ui/components/modals/InputModal.js +2 -13
- package/src/ui/components/modals/InputModal.tsx +21 -24
- package/src/ui/components/modals/LoadingModal.js +1 -1
- package/src/ui/components/modals/LoadingModal.tsx +6 -6
- package/src/ui/components/modals/MessageModal.js +4 -4
- package/src/ui/components/modals/MessageModal.tsx +13 -13
- package/src/ui/components/modals/ModalContainer.js +25 -2
- package/src/ui/components/modals/ModalContainer.tsx +25 -2
- package/src/ui/components/modals/SelectModal.js +3 -4
- package/src/ui/components/modals/SelectModal.tsx +18 -15
- package/src/ui/renderers/settingsRenderers.js +1 -1
- package/src/ui/renderers/settingsRenderers.tsx +5 -3
- package/src/ui/screens/CliToolsScreen.js +2 -2
- package/src/ui/screens/CliToolsScreen.tsx +3 -1
- package/src/ui/screens/EnvVarsScreen.js +27 -10
- package/src/ui/screens/EnvVarsScreen.tsx +33 -16
- package/src/ui/screens/McpRegistryScreen.js +2 -2
- package/src/ui/screens/McpRegistryScreen.tsx +3 -1
- package/src/ui/screens/McpScreen.js +1 -1
- package/src/ui/screens/McpScreen.tsx +2 -1
- package/src/ui/screens/ModelSelectorScreen.js +2 -2
- package/src/ui/screens/ModelSelectorScreen.tsx +3 -2
- package/src/ui/screens/ProfilesScreen.js +1 -1
- package/src/ui/screens/ProfilesScreen.tsx +2 -1
- package/src/ui/screens/StatusLineScreen.js +1 -1
- package/src/ui/screens/StatusLineScreen.tsx +2 -1
- package/src/ui/state/DimensionsContext.js +2 -2
- package/src/ui/state/DimensionsContext.tsx +3 -3
- package/src/ui/components/ScrollableDetail.js +0 -23
- package/src/ui/components/ScrollableDetail.tsx +0 -55
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "fs-extra";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import os from "node:os";
|
|
4
|
+
import semver from "semver";
|
|
4
5
|
import { parsePluginId } from "../utils/string-utils.js";
|
|
5
6
|
const CLAUDE_DIR = ".claude";
|
|
6
7
|
const SETTINGS_FILE = "settings.json";
|
|
@@ -377,6 +378,79 @@ export async function saveGlobalInstalledPluginVersion(pluginId, version) {
|
|
|
377
378
|
// Update registry for user scope
|
|
378
379
|
await updateInstalledPluginsRegistry(pluginId, version, "user");
|
|
379
380
|
}
|
|
381
|
+
/**
|
|
382
|
+
* Gap-fill: ensure global installedPluginVersions is at least as high as
|
|
383
|
+
* project and local scopes. When a plugin is updated at project scope,
|
|
384
|
+
* global may lag behind, causing Claude Code to resolve stale cache paths.
|
|
385
|
+
*
|
|
386
|
+
* @returns Array of { pluginId, oldVersion, newVersion } for plugins that were bumped
|
|
387
|
+
*/
|
|
388
|
+
export async function gapFillInstalledPluginVersions(projectPath) {
|
|
389
|
+
const globalVersions = await getGlobalInstalledPluginVersions();
|
|
390
|
+
// Collect versions from project and local scopes
|
|
391
|
+
const allVersions = {};
|
|
392
|
+
// Start with global versions
|
|
393
|
+
for (const [id, ver] of Object.entries(globalVersions)) {
|
|
394
|
+
allVersions[id] = [ver];
|
|
395
|
+
}
|
|
396
|
+
// Add project scope versions
|
|
397
|
+
if (projectPath) {
|
|
398
|
+
try {
|
|
399
|
+
const settings = await readSettings(projectPath);
|
|
400
|
+
for (const [id, ver] of Object.entries(settings.installedPluginVersions || {})) {
|
|
401
|
+
if (!allVersions[id])
|
|
402
|
+
allVersions[id] = [];
|
|
403
|
+
allVersions[id].push(ver);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
catch {
|
|
407
|
+
/* skip unreadable */
|
|
408
|
+
}
|
|
409
|
+
// Add local scope versions
|
|
410
|
+
try {
|
|
411
|
+
const localSettings = await readLocalSettings(projectPath);
|
|
412
|
+
for (const [id, ver] of Object.entries(localSettings.installedPluginVersions || {})) {
|
|
413
|
+
if (!allVersions[id])
|
|
414
|
+
allVersions[id] = [];
|
|
415
|
+
allVersions[id].push(ver);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
catch {
|
|
419
|
+
/* skip unreadable */
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
// Find plugins where global needs bumping
|
|
423
|
+
const bumped = [];
|
|
424
|
+
for (const [pluginId, versions] of Object.entries(allVersions)) {
|
|
425
|
+
// Filter to valid semver versions and sort descending
|
|
426
|
+
const valid = versions.filter((v) => semver.valid(v));
|
|
427
|
+
if (valid.length === 0)
|
|
428
|
+
continue;
|
|
429
|
+
const highest = valid.sort((a, b) => semver.rcompare(a, b))[0];
|
|
430
|
+
const globalVer = globalVersions[pluginId];
|
|
431
|
+
// If global is missing, invalid semver, or lower than the highest, bump it
|
|
432
|
+
if (!globalVer ||
|
|
433
|
+
!semver.valid(globalVer) ||
|
|
434
|
+
semver.lt(globalVer, highest)) {
|
|
435
|
+
bumped.push({
|
|
436
|
+
pluginId,
|
|
437
|
+
oldVersion: globalVer || "(missing)",
|
|
438
|
+
newVersion: highest,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
// Apply bumps in a single write
|
|
443
|
+
if (bumped.length > 0) {
|
|
444
|
+
const settings = await readGlobalSettings();
|
|
445
|
+
settings.installedPluginVersions =
|
|
446
|
+
settings.installedPluginVersions || {};
|
|
447
|
+
for (const { pluginId, newVersion } of bumped) {
|
|
448
|
+
settings.installedPluginVersions[pluginId] = newVersion;
|
|
449
|
+
}
|
|
450
|
+
await writeGlobalSettings(settings);
|
|
451
|
+
}
|
|
452
|
+
return bumped;
|
|
453
|
+
}
|
|
380
454
|
export async function removeGlobalInstalledPluginVersion(pluginId) {
|
|
381
455
|
const settings = await readGlobalSettings();
|
|
382
456
|
if (settings.installedPluginVersions) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "fs-extra";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import os from "node:os";
|
|
4
|
+
import semver from "semver";
|
|
4
5
|
import type {
|
|
5
6
|
ClaudeSettings,
|
|
6
7
|
ClaudeLocalSettings,
|
|
@@ -544,6 +545,97 @@ export async function saveGlobalInstalledPluginVersion(
|
|
|
544
545
|
await updateInstalledPluginsRegistry(pluginId, version, "user");
|
|
545
546
|
}
|
|
546
547
|
|
|
548
|
+
/**
|
|
549
|
+
* Gap-fill: ensure global installedPluginVersions is at least as high as
|
|
550
|
+
* project and local scopes. When a plugin is updated at project scope,
|
|
551
|
+
* global may lag behind, causing Claude Code to resolve stale cache paths.
|
|
552
|
+
*
|
|
553
|
+
* @returns Array of { pluginId, oldVersion, newVersion } for plugins that were bumped
|
|
554
|
+
*/
|
|
555
|
+
export async function gapFillInstalledPluginVersions(
|
|
556
|
+
projectPath?: string,
|
|
557
|
+
): Promise<Array<{ pluginId: string; oldVersion: string; newVersion: string }>> {
|
|
558
|
+
const globalVersions = await getGlobalInstalledPluginVersions();
|
|
559
|
+
|
|
560
|
+
// Collect versions from project and local scopes
|
|
561
|
+
const allVersions: Record<string, string[]> = {};
|
|
562
|
+
|
|
563
|
+
// Start with global versions
|
|
564
|
+
for (const [id, ver] of Object.entries(globalVersions)) {
|
|
565
|
+
allVersions[id] = [ver];
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Add project scope versions
|
|
569
|
+
if (projectPath) {
|
|
570
|
+
try {
|
|
571
|
+
const settings = await readSettings(projectPath);
|
|
572
|
+
for (const [id, ver] of Object.entries(
|
|
573
|
+
settings.installedPluginVersions || {},
|
|
574
|
+
)) {
|
|
575
|
+
if (!allVersions[id]) allVersions[id] = [];
|
|
576
|
+
allVersions[id].push(ver);
|
|
577
|
+
}
|
|
578
|
+
} catch {
|
|
579
|
+
/* skip unreadable */
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Add local scope versions
|
|
583
|
+
try {
|
|
584
|
+
const localSettings = await readLocalSettings(projectPath);
|
|
585
|
+
for (const [id, ver] of Object.entries(
|
|
586
|
+
localSettings.installedPluginVersions || {},
|
|
587
|
+
)) {
|
|
588
|
+
if (!allVersions[id]) allVersions[id] = [];
|
|
589
|
+
allVersions[id].push(ver);
|
|
590
|
+
}
|
|
591
|
+
} catch {
|
|
592
|
+
/* skip unreadable */
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Find plugins where global needs bumping
|
|
597
|
+
const bumped: Array<{
|
|
598
|
+
pluginId: string;
|
|
599
|
+
oldVersion: string;
|
|
600
|
+
newVersion: string;
|
|
601
|
+
}> = [];
|
|
602
|
+
|
|
603
|
+
for (const [pluginId, versions] of Object.entries(allVersions)) {
|
|
604
|
+
// Filter to valid semver versions and sort descending
|
|
605
|
+
const valid = versions.filter((v) => semver.valid(v));
|
|
606
|
+
if (valid.length === 0) continue;
|
|
607
|
+
|
|
608
|
+
const highest = valid.sort((a, b) => semver.rcompare(a, b))[0];
|
|
609
|
+
const globalVer = globalVersions[pluginId];
|
|
610
|
+
|
|
611
|
+
// If global is missing, invalid semver, or lower than the highest, bump it
|
|
612
|
+
if (
|
|
613
|
+
!globalVer ||
|
|
614
|
+
!semver.valid(globalVer) ||
|
|
615
|
+
semver.lt(globalVer, highest)
|
|
616
|
+
) {
|
|
617
|
+
bumped.push({
|
|
618
|
+
pluginId,
|
|
619
|
+
oldVersion: globalVer || "(missing)",
|
|
620
|
+
newVersion: highest,
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Apply bumps in a single write
|
|
626
|
+
if (bumped.length > 0) {
|
|
627
|
+
const settings = await readGlobalSettings();
|
|
628
|
+
settings.installedPluginVersions =
|
|
629
|
+
settings.installedPluginVersions || {};
|
|
630
|
+
for (const { pluginId, newVersion } of bumped) {
|
|
631
|
+
settings.installedPluginVersions[pluginId] = newVersion;
|
|
632
|
+
}
|
|
633
|
+
await writeGlobalSettings(settings);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
return bumped;
|
|
637
|
+
}
|
|
638
|
+
|
|
547
639
|
export async function removeGlobalInstalledPluginVersion(
|
|
548
640
|
pluginId: string,
|
|
549
641
|
): Promise<void> {
|
|
@@ -5,7 +5,6 @@ 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";
|
|
9
8
|
// Cache for local marketplaces (session-level) - Promise-based to prevent race conditions
|
|
10
9
|
let localMarketplacesPromise = null;
|
|
11
10
|
// Session-level cache for fetched marketplace data (no TTL - persists until explicit refresh)
|
|
@@ -434,9 +433,14 @@ export async function getLocalMarketplacesInfo() {
|
|
|
434
433
|
const autoSyncedMarketplaces = new Set();
|
|
435
434
|
/**
|
|
436
435
|
* If the remote manifest lists plugins that aren't in the local cache,
|
|
437
|
-
* the local clone is stale.
|
|
438
|
-
*
|
|
439
|
-
*
|
|
436
|
+
* the local clone is stale. Log a warning but do NOT trigger a marketplace
|
|
437
|
+
* update from claudeup.
|
|
438
|
+
*
|
|
439
|
+
* Previously this called `updateMarketplace()` which invokes Claude Code's
|
|
440
|
+
* `cacheMarketplaceFromGit()` — a non-atomic delete-then-clone that can
|
|
441
|
+
* permanently delete the marketplace directory on clone failure.
|
|
442
|
+
* Claude Code's own background autoupdate handles marketplace refreshing
|
|
443
|
+
* after session start. See: ai-docs/plugin-marketplace-bug-investigation.md
|
|
440
444
|
*/
|
|
441
445
|
async function autoSyncIfStale(mpName, remotePluginNames, localMarketplaces) {
|
|
442
446
|
if (autoSyncedMarketplaces.has(mpName))
|
|
@@ -445,20 +449,13 @@ async function autoSyncIfStale(mpName, remotePluginNames, localMarketplaces) {
|
|
|
445
449
|
if (!localMp)
|
|
446
450
|
return localMarketplaces;
|
|
447
451
|
const localNames = new Set(localMp.plugins.map((p) => p.name));
|
|
448
|
-
const
|
|
449
|
-
if (
|
|
452
|
+
const missingPlugins = remotePluginNames.filter((name) => !localNames.has(name));
|
|
453
|
+
if (missingPlugins.length === 0)
|
|
450
454
|
return localMarketplaces;
|
|
451
455
|
autoSyncedMarketplaces.add(mpName);
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|
-
}
|
|
456
|
+
// Log stale state but don't trigger update — Claude Code handles refresh
|
|
457
|
+
console.log(`ℹ Marketplace ${mpName} is stale (missing: ${missingPlugins.join(", ")}). Claude Code will auto-update on next session start.`);
|
|
458
|
+
return localMarketplaces;
|
|
462
459
|
}
|
|
463
460
|
/**
|
|
464
461
|
* Refresh claudeup's internal cache
|
|
@@ -27,8 +27,6 @@ import {
|
|
|
27
27
|
isValidGitHubRepo,
|
|
28
28
|
parsePluginId,
|
|
29
29
|
} from "../utils/string-utils.js";
|
|
30
|
-
import { updateMarketplace } from "./claude-cli.js";
|
|
31
|
-
|
|
32
30
|
// Cache for local marketplaces (session-level) - Promise-based to prevent race conditions
|
|
33
31
|
let localMarketplacesPromise: Promise<Map<string, LocalMarketplace>> | null =
|
|
34
32
|
null;
|
|
@@ -619,9 +617,14 @@ const autoSyncedMarketplaces = new Set<string>();
|
|
|
619
617
|
|
|
620
618
|
/**
|
|
621
619
|
* If the remote manifest lists plugins that aren't in the local cache,
|
|
622
|
-
* the local clone is stale.
|
|
623
|
-
*
|
|
624
|
-
*
|
|
620
|
+
* the local clone is stale. Log a warning but do NOT trigger a marketplace
|
|
621
|
+
* update from claudeup.
|
|
622
|
+
*
|
|
623
|
+
* Previously this called `updateMarketplace()` which invokes Claude Code's
|
|
624
|
+
* `cacheMarketplaceFromGit()` — a non-atomic delete-then-clone that can
|
|
625
|
+
* permanently delete the marketplace directory on clone failure.
|
|
626
|
+
* Claude Code's own background autoupdate handles marketplace refreshing
|
|
627
|
+
* after session start. See: ai-docs/plugin-marketplace-bug-investigation.md
|
|
625
628
|
*/
|
|
626
629
|
async function autoSyncIfStale(
|
|
627
630
|
mpName: string,
|
|
@@ -634,19 +637,17 @@ async function autoSyncIfStale(
|
|
|
634
637
|
if (!localMp) return localMarketplaces;
|
|
635
638
|
|
|
636
639
|
const localNames = new Set(localMp.plugins.map((p) => p.name));
|
|
637
|
-
const
|
|
638
|
-
|
|
640
|
+
const missingPlugins = remotePluginNames.filter(
|
|
641
|
+
(name) => !localNames.has(name),
|
|
642
|
+
);
|
|
643
|
+
if (missingPlugins.length === 0) return localMarketplaces;
|
|
639
644
|
|
|
640
645
|
autoSyncedMarketplaces.add(mpName);
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
} catch {
|
|
647
|
-
// Update failed (no network, CLI missing, etc.) — continue with stale data
|
|
648
|
-
return localMarketplaces;
|
|
649
|
-
}
|
|
646
|
+
// Log stale state but don't trigger update — Claude Code handles refresh
|
|
647
|
+
console.log(
|
|
648
|
+
`ℹ Marketplace ${mpName} is stale (missing: ${missingPlugins.join(", ")}). Claude Code will auto-update on next session start.`,
|
|
649
|
+
);
|
|
650
|
+
return localMarketplaces;
|
|
650
651
|
}
|
|
651
652
|
|
|
652
653
|
export interface RefreshAndRepairResult {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
1
3
|
import { readSettings, writeSettings, readGlobalSettings, writeGlobalSettings, } from "./claude-settings.js";
|
|
2
4
|
/** Read the current value of a setting from the given scope */
|
|
3
5
|
export async function readSettingValue(setting, scope, projectPath) {
|
|
@@ -62,11 +64,6 @@ export async function writeSettingValue(setting, value, scope, projectPath) {
|
|
|
62
64
|
}
|
|
63
65
|
}
|
|
64
66
|
else if (setting.storage.type === "attribution-text") {
|
|
65
|
-
const attr = settings.attribution;
|
|
66
|
-
// If attribution is explicitly disabled ({ commit: "", pr: "" }), do not overwrite it
|
|
67
|
-
if (attr && attr.commit === "" && attr.pr === "") {
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
67
|
if (value && value.trim().length > 0) {
|
|
71
68
|
// Write custom text: commit gets a Co-Authored-By trailer + the text; pr gets the text
|
|
72
69
|
settings.attribution = {
|
|
@@ -162,3 +159,85 @@ export async function readAllSettingsBothScopes(catalog, projectPath) {
|
|
|
162
159
|
}
|
|
163
160
|
return result;
|
|
164
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* Discover available output styles from installed plugins.
|
|
164
|
+
* Scans enabled plugins for outputStyles entries and *-output-style plugin names.
|
|
165
|
+
* Returns options suitable for a select setting.
|
|
166
|
+
*/
|
|
167
|
+
export async function discoverOutputStyles(projectPath) {
|
|
168
|
+
const styles = [
|
|
169
|
+
{ label: "Default", value: "" },
|
|
170
|
+
];
|
|
171
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || "";
|
|
172
|
+
const cacheDir = path.join(homeDir, ".claude", "plugins", "cache");
|
|
173
|
+
// Collect enabled plugins from both scopes
|
|
174
|
+
const enabledPlugins = new Set();
|
|
175
|
+
try {
|
|
176
|
+
const userSettings = await readGlobalSettings();
|
|
177
|
+
for (const [k, v] of Object.entries(userSettings.enabledPlugins || {})) {
|
|
178
|
+
if (v)
|
|
179
|
+
enabledPlugins.add(k);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch { }
|
|
183
|
+
try {
|
|
184
|
+
const projSettings = await readSettings(projectPath);
|
|
185
|
+
for (const [k, v] of Object.entries(projSettings.enabledPlugins || {})) {
|
|
186
|
+
if (v)
|
|
187
|
+
enabledPlugins.add(k);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch { }
|
|
191
|
+
const seen = new Set();
|
|
192
|
+
for (const pluginId of enabledPlugins) {
|
|
193
|
+
const [pluginName, marketplace] = pluginId.split("@");
|
|
194
|
+
// Check if this is a dedicated output-style plugin (e.g. "explanatory-output-style")
|
|
195
|
+
if (pluginName.endsWith("-output-style")) {
|
|
196
|
+
const styleName = pluginName
|
|
197
|
+
.replace(/-output-style$/, "")
|
|
198
|
+
.split("-")
|
|
199
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
200
|
+
.join(" ");
|
|
201
|
+
if (!seen.has(styleName)) {
|
|
202
|
+
seen.add(styleName);
|
|
203
|
+
styles.push({ label: styleName, value: styleName });
|
|
204
|
+
}
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
// Check plugin.json for outputStyles entries
|
|
208
|
+
if (!marketplace)
|
|
209
|
+
continue;
|
|
210
|
+
const pluginDir = path.join(cacheDir, marketplace, pluginName);
|
|
211
|
+
try {
|
|
212
|
+
const versions = fs.readdirSync(pluginDir).sort();
|
|
213
|
+
const latest = versions[versions.length - 1];
|
|
214
|
+
if (!latest)
|
|
215
|
+
continue;
|
|
216
|
+
const manifestPath = path.join(pluginDir, latest, "plugin.json");
|
|
217
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
218
|
+
const outputStyles = manifest.outputStyles;
|
|
219
|
+
if (!outputStyles?.length)
|
|
220
|
+
continue;
|
|
221
|
+
for (const stylePath of outputStyles) {
|
|
222
|
+
const fullPath = path.join(pluginDir, latest, stylePath);
|
|
223
|
+
try {
|
|
224
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
225
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
226
|
+
if (fmMatch) {
|
|
227
|
+
const nameMatch = fmMatch[1].match(/^name:\s*(.+)$/m);
|
|
228
|
+
if (nameMatch) {
|
|
229
|
+
const name = nameMatch[1].trim();
|
|
230
|
+
if (!seen.has(name)) {
|
|
231
|
+
seen.add(name);
|
|
232
|
+
styles.push({ label: name, value: name });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
catch { }
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
catch { }
|
|
241
|
+
}
|
|
242
|
+
return styles;
|
|
243
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
1
3
|
import {
|
|
2
4
|
readSettings,
|
|
3
5
|
writeSettings,
|
|
@@ -80,11 +82,6 @@ export async function writeSettingValue(
|
|
|
80
82
|
delete (settings as any).attribution;
|
|
81
83
|
}
|
|
82
84
|
} else if (setting.storage.type === "attribution-text") {
|
|
83
|
-
const attr = (settings as any).attribution;
|
|
84
|
-
// If attribution is explicitly disabled ({ commit: "", pr: "" }), do not overwrite it
|
|
85
|
-
if (attr && attr.commit === "" && attr.pr === "") {
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
85
|
if (value && value.trim().length > 0) {
|
|
89
86
|
// Write custom text: commit gets a Co-Authored-By trailer + the text; pr gets the text
|
|
90
87
|
(settings as any).attribution = {
|
|
@@ -188,3 +185,87 @@ export async function readAllSettingsBothScopes(
|
|
|
188
185
|
}
|
|
189
186
|
return result;
|
|
190
187
|
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Discover available output styles from installed plugins.
|
|
191
|
+
* Scans enabled plugins for outputStyles entries and *-output-style plugin names.
|
|
192
|
+
* Returns options suitable for a select setting.
|
|
193
|
+
*/
|
|
194
|
+
export async function discoverOutputStyles(
|
|
195
|
+
projectPath?: string,
|
|
196
|
+
): Promise<Array<{ label: string; value: string }>> {
|
|
197
|
+
const styles: Array<{ label: string; value: string }> = [
|
|
198
|
+
{ label: "Default", value: "" },
|
|
199
|
+
];
|
|
200
|
+
|
|
201
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || "";
|
|
202
|
+
const cacheDir = path.join(homeDir, ".claude", "plugins", "cache");
|
|
203
|
+
|
|
204
|
+
// Collect enabled plugins from both scopes
|
|
205
|
+
const enabledPlugins = new Set<string>();
|
|
206
|
+
try {
|
|
207
|
+
const userSettings = await readGlobalSettings();
|
|
208
|
+
for (const [k, v] of Object.entries((userSettings as any).enabledPlugins || {})) {
|
|
209
|
+
if (v) enabledPlugins.add(k);
|
|
210
|
+
}
|
|
211
|
+
} catch {}
|
|
212
|
+
try {
|
|
213
|
+
const projSettings = await readSettings(projectPath);
|
|
214
|
+
for (const [k, v] of Object.entries((projSettings as any).enabledPlugins || {})) {
|
|
215
|
+
if (v) enabledPlugins.add(k);
|
|
216
|
+
}
|
|
217
|
+
} catch {}
|
|
218
|
+
|
|
219
|
+
const seen = new Set<string>();
|
|
220
|
+
|
|
221
|
+
for (const pluginId of enabledPlugins) {
|
|
222
|
+
const [pluginName, marketplace] = pluginId.split("@");
|
|
223
|
+
|
|
224
|
+
// Check if this is a dedicated output-style plugin (e.g. "explanatory-output-style")
|
|
225
|
+
if (pluginName.endsWith("-output-style")) {
|
|
226
|
+
const styleName = pluginName
|
|
227
|
+
.replace(/-output-style$/, "")
|
|
228
|
+
.split("-")
|
|
229
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
230
|
+
.join(" ");
|
|
231
|
+
if (!seen.has(styleName)) {
|
|
232
|
+
seen.add(styleName);
|
|
233
|
+
styles.push({ label: styleName, value: styleName });
|
|
234
|
+
}
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Check plugin.json for outputStyles entries
|
|
239
|
+
if (!marketplace) continue;
|
|
240
|
+
const pluginDir = path.join(cacheDir, marketplace, pluginName);
|
|
241
|
+
try {
|
|
242
|
+
const versions = fs.readdirSync(pluginDir).sort();
|
|
243
|
+
const latest = versions[versions.length - 1];
|
|
244
|
+
if (!latest) continue;
|
|
245
|
+
const manifestPath = path.join(pluginDir, latest, "plugin.json");
|
|
246
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
247
|
+
const outputStyles = manifest.outputStyles as string[] | undefined;
|
|
248
|
+
if (!outputStyles?.length) continue;
|
|
249
|
+
|
|
250
|
+
for (const stylePath of outputStyles) {
|
|
251
|
+
const fullPath = path.join(pluginDir, latest, stylePath);
|
|
252
|
+
try {
|
|
253
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
254
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
255
|
+
if (fmMatch) {
|
|
256
|
+
const nameMatch = fmMatch[1].match(/^name:\s*(.+)$/m);
|
|
257
|
+
if (nameMatch) {
|
|
258
|
+
const name = nameMatch[1].trim();
|
|
259
|
+
if (!seen.has(name)) {
|
|
260
|
+
seen.add(name);
|
|
261
|
+
styles.push({ label: name, value: name });
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
} catch {}
|
|
266
|
+
}
|
|
267
|
+
} catch {}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return styles;
|
|
271
|
+
}
|
|
@@ -52,12 +52,12 @@ export function getEffectiveValue(scoped) {
|
|
|
52
52
|
}
|
|
53
53
|
export function formatValue(setting, value) {
|
|
54
54
|
if (value === undefined || value === "") {
|
|
55
|
-
if (setting.defaultValue !== undefined) {
|
|
56
|
-
return setting.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
if (setting.type === "boolean" && setting.defaultValue !== undefined) {
|
|
56
|
+
return setting.defaultValue === "true" ? "on" : "off";
|
|
57
|
+
}
|
|
58
|
+
if (setting.type === "select" && setting.defaultValue !== undefined) {
|
|
59
|
+
const opt = setting.options?.find((o) => o.value === setting.defaultValue);
|
|
60
|
+
return opt ? opt.label : setting.defaultValue;
|
|
61
61
|
}
|
|
62
62
|
return "—";
|
|
63
63
|
}
|
|
@@ -68,8 +68,8 @@ export function formatValue(setting, value) {
|
|
|
68
68
|
const opt = setting.options.find((o) => o.value === value);
|
|
69
69
|
return opt ? opt.label : value;
|
|
70
70
|
}
|
|
71
|
-
if (value.length >
|
|
72
|
-
return value.slice(0,
|
|
71
|
+
if (value.length > 12) {
|
|
72
|
+
return value.slice(0, 12) + "…";
|
|
73
73
|
}
|
|
74
74
|
return value;
|
|
75
75
|
}
|
|
@@ -94,12 +94,12 @@ export function formatValue(
|
|
|
94
94
|
value: string | undefined,
|
|
95
95
|
): string {
|
|
96
96
|
if (value === undefined || value === "") {
|
|
97
|
-
if (setting.defaultValue !== undefined) {
|
|
98
|
-
return setting.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
97
|
+
if (setting.type === "boolean" && setting.defaultValue !== undefined) {
|
|
98
|
+
return setting.defaultValue === "true" ? "on" : "off";
|
|
99
|
+
}
|
|
100
|
+
if (setting.type === "select" && setting.defaultValue !== undefined) {
|
|
101
|
+
const opt = setting.options?.find((o) => o.value === setting.defaultValue);
|
|
102
|
+
return opt ? opt.label : setting.defaultValue;
|
|
103
103
|
}
|
|
104
104
|
return "—";
|
|
105
105
|
}
|
|
@@ -113,8 +113,8 @@ export function formatValue(
|
|
|
113
113
|
return opt ? opt.label : value;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
if (value.length >
|
|
117
|
-
return value.slice(0,
|
|
116
|
+
if (value.length > 12) {
|
|
117
|
+
return value.slice(0, 12) + "…";
|
|
118
118
|
}
|
|
119
119
|
return value;
|
|
120
120
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
|
|
2
|
-
import { useKeyboardHandler } from "../hooks/useKeyboardHandler";
|
|
3
2
|
const TABS = [
|
|
4
3
|
{ key: "1", label: "Plugins", screen: "plugins" },
|
|
5
4
|
{ key: "2", label: "Skills", screen: "skills" },
|
|
@@ -8,28 +7,7 @@ const TABS = [
|
|
|
8
7
|
{ key: "5", label: "Profiles", screen: "profiles" },
|
|
9
8
|
{ key: "6", label: "CLI", screen: "cli-tools" },
|
|
10
9
|
];
|
|
11
|
-
export function TabBar({ currentScreen
|
|
12
|
-
// Handle number key shortcuts (1-5)
|
|
13
|
-
useKeyboardHandler((input, key) => {
|
|
14
|
-
if (!onTabChange)
|
|
15
|
-
return;
|
|
16
|
-
// Number keys 1-5
|
|
17
|
-
const tabIndex = Number.parseInt(input, 10);
|
|
18
|
-
if (tabIndex >= 1 && tabIndex <= TABS.length) {
|
|
19
|
-
const tab = TABS[tabIndex - 1];
|
|
20
|
-
if (tab && tab.screen !== currentScreen) {
|
|
21
|
-
onTabChange(tab.screen);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
// Tab key to cycle through tabs
|
|
25
|
-
if (key.tab) {
|
|
26
|
-
const currentIndex = TABS.findIndex((t) => t.screen === currentScreen);
|
|
27
|
-
const nextIndex = key.shift
|
|
28
|
-
? (currentIndex - 1 + TABS.length) % TABS.length
|
|
29
|
-
: (currentIndex + 1) % TABS.length;
|
|
30
|
-
onTabChange(TABS[nextIndex].screen);
|
|
31
|
-
}
|
|
32
|
-
});
|
|
10
|
+
export function TabBar({ currentScreen }) {
|
|
33
11
|
return (_jsx("box", { flexDirection: "row", gap: 0, children: TABS.map((tab, index) => {
|
|
34
12
|
const isSelected = tab.screen === currentScreen;
|
|
35
13
|
const isLast = index === TABS.length - 1;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { useKeyboardHandler } from "../hooks/useKeyboardHandler";
|
|
3
2
|
import type { Screen } from "../state/types.js";
|
|
4
3
|
|
|
5
4
|
interface Tab {
|
|
@@ -19,33 +18,9 @@ const TABS: Tab[] = [
|
|
|
19
18
|
|
|
20
19
|
interface TabBarProps {
|
|
21
20
|
currentScreen: Screen;
|
|
22
|
-
onTabChange?: (screen: Screen) => void;
|
|
23
21
|
}
|
|
24
22
|
|
|
25
|
-
export function TabBar({ currentScreen
|
|
26
|
-
// Handle number key shortcuts (1-5)
|
|
27
|
-
useKeyboardHandler((input, key) => {
|
|
28
|
-
if (!onTabChange) return;
|
|
29
|
-
|
|
30
|
-
// Number keys 1-5
|
|
31
|
-
const tabIndex = Number.parseInt(input, 10);
|
|
32
|
-
if (tabIndex >= 1 && tabIndex <= TABS.length) {
|
|
33
|
-
const tab = TABS[tabIndex - 1];
|
|
34
|
-
if (tab && tab.screen !== currentScreen) {
|
|
35
|
-
onTabChange(tab.screen);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Tab key to cycle through tabs
|
|
40
|
-
if (key.tab) {
|
|
41
|
-
const currentIndex = TABS.findIndex((t) => t.screen === currentScreen);
|
|
42
|
-
const nextIndex = key.shift
|
|
43
|
-
? (currentIndex - 1 + TABS.length) % TABS.length
|
|
44
|
-
: (currentIndex + 1) % TABS.length;
|
|
45
|
-
onTabChange(TABS[nextIndex].screen);
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
|
|
23
|
+
export function TabBar({ currentScreen }: TabBarProps) {
|
|
49
24
|
return (
|
|
50
25
|
<box flexDirection="row" gap={0}>
|
|
51
26
|
{TABS.map((tab, index) => {
|
|
@@ -9,6 +9,6 @@ export function ConfirmModal({ title, message, onConfirm, onCancel, }) {
|
|
|
9
9
|
onCancel();
|
|
10
10
|
}
|
|
11
11
|
});
|
|
12
|
-
return (_jsxs("box", { flexDirection: "column", border: true, borderStyle: "rounded", borderColor: "
|
|
12
|
+
return (_jsxs("box", { flexDirection: "column", border: true, borderStyle: "rounded", borderColor: "#525252", backgroundColor: "#1C1C1E", paddingLeft: 3, paddingRight: 3, paddingTop: 1, paddingBottom: 1, width: 60, children: [_jsx("box", { marginBottom: 1, children: _jsx("text", { fg: "#EDEDED", children: _jsx("strong", { children: title }) }) }), _jsx("box", { marginBottom: 1, children: _jsx("text", { fg: "#A1A1AA", children: message }) }), _jsxs("box", { marginTop: 1, children: [_jsx("text", { fg: "#71717A", children: "Press " }), _jsx("text", { fg: "#EDEDED", children: "Y" }), _jsx("text", { fg: "#71717A", children: " to confirm \u2022 " }), _jsx("text", { fg: "#EDEDED", children: "N" }), _jsx("text", { fg: "#71717A", children: " to cancel" })] })] }));
|
|
13
13
|
}
|
|
14
14
|
export default ConfirmModal;
|