feat: build
This commit is contained in:
commit
f90eee85d8
1906 files changed
+514339
No files matched your search
@@ -0,0 +1,162 @@
|
||||
import * as React from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js';
|
||||
import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.js';
|
||||
import { Byline } from '../../components/design-system/Byline.js';
|
||||
import { KeyboardShortcutHint } from '../../components/design-system/KeyboardShortcutHint.js';
|
||||
import { Spinner } from '../../components/Spinner.js';
|
||||
import TextInput from '../../components/TextInput.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { toError } from '../../utils/errors.js';
|
||||
import { logError } from '../../utils/log.js';
|
||||
import { clearAllCaches } from '../../utils/plugins/cacheUtils.js';
|
||||
import { addMarketplaceSource, saveMarketplaceToSettings } from '../../utils/plugins/marketplaceManager.js';
|
||||
import { parseMarketplaceInput } from '../../utils/plugins/parseMarketplaceInput.js';
|
||||
import type { ViewState } from './types.js';
|
||||
type Props = {
|
||||
inputValue: string;
|
||||
setInputValue: (value: string) => void;
|
||||
cursorOffset: number;
|
||||
setCursorOffset: (offset: number) => void;
|
||||
error: string | null;
|
||||
setError: (error: string | null) => void;
|
||||
result: string | null;
|
||||
setResult: (result: string | null) => void;
|
||||
setViewState: (state: ViewState) => void;
|
||||
onAddComplete?: () => void | Promise<void>;
|
||||
cliMode?: boolean;
|
||||
};
|
||||
export function AddMarketplace({
|
||||
inputValue,
|
||||
setInputValue,
|
||||
cursorOffset,
|
||||
setCursorOffset,
|
||||
error,
|
||||
setError,
|
||||
result,
|
||||
setResult,
|
||||
setViewState,
|
||||
onAddComplete,
|
||||
cliMode = false
|
||||
}: Props): React.ReactNode {
|
||||
const hasAttemptedAutoAdd = useRef(false);
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
const [progressMessage, setProgressMessage] = useState<string>('');
|
||||
const handleAdd = async () => {
|
||||
const input = inputValue.trim();
|
||||
if (!input) {
|
||||
setError('Please enter a marketplace source');
|
||||
return;
|
||||
}
|
||||
const parsed = await parseMarketplaceInput(input);
|
||||
if (!parsed) {
|
||||
setError('Invalid marketplace source format. Try: owner/repo, https://..., or ./path');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if parseMarketplaceInput returned an error
|
||||
if ('error' in parsed) {
|
||||
setError(parsed.error);
|
||||
return;
|
||||
}
|
||||
setError(null);
|
||||
try {
|
||||
setLoading(true);
|
||||
setProgressMessage('');
|
||||
const {
|
||||
name,
|
||||
resolvedSource
|
||||
} = await addMarketplaceSource(parsed, message => {
|
||||
setProgressMessage(message);
|
||||
});
|
||||
saveMarketplaceToSettings(name, {
|
||||
source: resolvedSource
|
||||
});
|
||||
clearAllCaches();
|
||||
let sourceType = parsed.source;
|
||||
if (parsed.source === 'github') {
|
||||
sourceType = parsed.repo as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS;
|
||||
}
|
||||
logEvent('tengu_marketplace_added', {
|
||||
source_type: sourceType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
if (onAddComplete) {
|
||||
await onAddComplete();
|
||||
}
|
||||
setProgressMessage('');
|
||||
setLoading(false);
|
||||
if (cliMode) {
|
||||
// In CLI mode, set result to trigger completion
|
||||
setResult(`Successfully added marketplace: ${name}`);
|
||||
} else {
|
||||
// In interactive mode, switch to browse view
|
||||
setViewState({
|
||||
type: 'browse-marketplace',
|
||||
targetMarketplace: name
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
const error = toError(err);
|
||||
logError(error);
|
||||
setError(error.message);
|
||||
setProgressMessage('');
|
||||
setLoading(false);
|
||||
if (cliMode) {
|
||||
// In CLI mode, set result with error to trigger completion
|
||||
setResult(`Error: ${error.message}`);
|
||||
} else {
|
||||
setResult(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Auto-add if inputValue is provided
|
||||
useEffect(() => {
|
||||
if (inputValue && !hasAttemptedAutoAdd.current && !error && !result) {
|
||||
hasAttemptedAutoAdd.current = true;
|
||||
void handleAdd();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: intentional
|
||||
}, []); // Only run once on mount
|
||||
|
||||
return <Box flexDirection="column">
|
||||
<Box flexDirection="column" paddingX={1} borderStyle="round">
|
||||
<Box marginBottom={1}>
|
||||
<Text bold>Add Marketplace</Text>
|
||||
</Box>
|
||||
<Box flexDirection="column">
|
||||
<Text>Enter marketplace source:</Text>
|
||||
<Text dimColor>Examples:</Text>
|
||||
<Text dimColor> · owner/repo (GitHub)</Text>
|
||||
<Text dimColor> · git@github.com:owner/repo.git (SSH)</Text>
|
||||
<Text dimColor> · https://example.com/marketplace.json</Text>
|
||||
<Text dimColor> · ./path/to/marketplace</Text>
|
||||
<Box marginTop={1}>
|
||||
<TextInput value={inputValue} onChange={setInputValue} onSubmit={handleAdd} columns={80} cursorOffset={cursorOffset} onChangeCursorOffset={setCursorOffset} focus showCursor />
|
||||
</Box>
|
||||
</Box>
|
||||
{isLoading && <Box marginTop={1}>
|
||||
<Spinner />
|
||||
<Text>
|
||||
{progressMessage || 'Adding marketplace to configuration…'}
|
||||
</Text>
|
||||
</Box>}
|
||||
{error && <Box marginTop={1}>
|
||||
<Text color="error">{error}</Text>
|
||||
</Box>}
|
||||
{result && <Box marginTop={1}>
|
||||
<Text>{result}</Text>
|
||||
</Box>}
|
||||
</Box>
|
||||
<Box marginLeft={3}>
|
||||
<Text dimColor italic>
|
||||
<Byline>
|
||||
<KeyboardShortcutHint shortcut="Enter" action="add" />
|
||||
<ConfigurableShortcutHint action="confirm:no" context="Settings" fallback="Esc" description="cancel" />
|
||||
</Byline>
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>;
|
||||
}
|
||||
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsInVzZUVmZmVjdCIsInVzZVJlZiIsInVzZVN0YXRlIiwiQW5hbHl0aWNzTWV0YWRhdGFfSV9WRVJJRklFRF9USElTX0lTX05PVF9DT0RFX09SX0ZJTEVQQVRIUyIsImxvZ0V2ZW50IiwiQ29uZmlndXJhYmxlU2hvcnRjdXRIaW50IiwiQnlsaW5lIiwiS2V5Ym9hcmRTaG9ydGN1dEhpbnQiLCJTcGlubmVyIiwiVGV4dElucHV0IiwiQm94IiwiVGV4dCIsInRvRXJyb3IiLCJsb2dFcnJvciIsImNsZWFyQWxsQ2FjaGVzIiwiYWRkTWFya2V0cGxhY2VTb3VyY2UiLCJzYXZlTWFya2V0cGxhY2VUb1NldHRpbmdzIiwicGFyc2VNYXJrZXRwbGFjZUlucHV0IiwiVmlld1N0YXRlIiwiUHJvcHMiLCJpbnB1dFZhbHVlIiwic2V0SW5wdXRWYWx1ZSIsInZhbHVlIiwiY3Vyc29yT2Zmc2V0Iiwic2V0Q3Vyc29yT2Zmc2V0Iiwib2Zmc2V0IiwiZXJyb3IiLCJzZXRFcnJvciIsInJlc3VsdCIsInNldFJlc3VsdCIsInNldFZpZXdTdGF0ZSIsInN0YXRlIiwib25BZGRDb21wbGV0ZSIsIlByb21pc2UiLCJjbGlNb2RlIiwiQWRkTWFya2V0cGxhY2UiLCJSZWFjdE5vZGUiLCJoYXNBdHRlbXB0ZWRBdXRvQWRkIiwiaXNMb2FkaW5nIiwic2V0TG9hZGluZyIsInByb2dyZXNzTWVzc2FnZSIsInNldFByb2dyZXNzTWVzc2FnZSIsImhhbmRsZUFkZCIsImlucHV0IiwidHJpbSIsInBhcnNlZCIsIm5hbWUiLCJyZXNvbHZlZFNvdXJjZSIsIm1lc3NhZ2UiLCJzb3VyY2UiLCJzb3VyY2VUeXBlIiwicmVwbyIsInNvdXJjZV90eXBlIiwidHlwZSIsInRhcmdldE1hcmtldHBsYWNlIiwiZXJyIiwiY3VycmVudCJdLCJzb3VyY2VzIjpbIkFkZE1hcmtldHBsYWNlLnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgKiBhcyBSZWFjdCBmcm9tICdyZWFjdCdcbmltcG9ydCB7IHVzZUVmZmVjdCwgdXNlUmVmLCB1c2VTdGF0ZSB9IGZyb20gJ3JlYWN0J1xuaW1wb3J0IHtcbiAgdHlwZSBBbmFseXRpY3NNZXRhZGF0YV9JX1ZFUklGSUVEX1RISVNfSVNfTk9UX0NPREVfT1JfRklMRVBBVEhTLFxuICBsb2dFdmVudCxcbn0gZnJvbSAnc3JjL3NlcnZpY2VzL2FuYWx5dGljcy9pbmRleC5qcydcbmltcG9ydCB7IENvbmZpZ3VyYWJsZVNob3J0Y3V0SGludCB9IGZyb20gJy4uLy4uL2NvbXBvbmVudHMvQ29uZmlndXJhYmxlU2hvcnRjdXRIaW50LmpzJ1xuaW1wb3J0IHsgQnlsaW5lIH0gZnJvbSAnLi4vLi4vY29tcG9uZW50cy9kZXNpZ24tc3lzdGVtL0J5bGluZS5qcydcbmltcG9ydCB7IEtleWJvYXJkU2hvcnRjdXRIaW50IH0gZnJvbSAnLi4vLi4vY29tcG9uZW50cy9kZXNpZ24tc3lzdGVtL0tleWJvYXJkU2hvcnRjdXRIaW50LmpzJ1xuaW1wb3J0IHsgU3Bpbm5lciB9IGZyb20gJy4uLy4uL2NvbXBvbmVudHMvU3Bpbm5lci5qcydcbmltcG9ydCBUZXh0SW5wdXQgZnJvbSAnLi4vLi4vY29tcG9uZW50cy9UZXh0SW5wdXQuanMnXG5pbXBvcnQgeyBCb3gsIFRleHQgfSBmcm9tICcuLi8uLi9pbmsuanMnXG5pbXBvcnQgeyB0b0Vycm9yIH0gZnJvbSAnLi4vLi4vdXRpbHMvZXJyb3JzLmpzJ1xuaW1wb3J0IHsgbG9nRXJyb3IgfSBmcm9tICcuLi8uLi91dGlscy9sb2cuanMnXG5pbXBvcnQgeyBjbGVhckFsbENhY2hlcyB9IGZyb20gJy4uLy4uL3V0aWxzL3BsdWdpbnMvY2FjaGVVdGlscy5qcydcbmltcG9ydCB7XG4gIGFkZE1hcmtldHBsYWNlU291cmNlLFxuICBzYXZlTWFya2V0cGxhY2VUb1NldHRpbmdzLFxufSBmcm9tICcuLi8uLi91dGlscy9wbHVnaW5zL21hcmtldHBsYWNlTWFuYWdlci5qcydcbmltcG9ydCB7IHBhcnNlTWFya2V0cGxhY2VJbnB1dCB9IGZyb20gJy4uLy4uL3V0aWxzL3BsdWdpbnMvcGFyc2VNYXJrZXRwbGFjZUlucHV0LmpzJ1xuaW1wb3J0IHR5cGUgeyBWaWV3U3RhdGUgfSBmcm9tICcuL3R5cGVzLmpzJ1xuXG50eXBlIFByb3BzID0ge1xuICBpbnB1dFZhbHVlOiBzdHJpbmdcbiAgc2V0SW5wdXRWYWx1ZTogKHZhbHVlOiBzdHJpbmcpID0+IHZvaWRcbiAgY3Vyc29yT2Zmc2V0OiBudW1iZXJcbiAgc2V0Q3Vyc29yT2Zmc2V0OiAob2Zmc2V0OiBudW1iZXIpID0+IHZvaWRcbiAgZXJyb3I6IHN0cmluZyB8IG51bGxcbiAgc2V0RXJyb3I6IChlcnJvcjogc3RyaW5nIHwgbnVsbCkgPT4gdm9pZFxuICByZXN1bHQ6IHN0cmluZyB8IG51bGxcbiAgc2V0UmVzdWx0OiAocmVzdWx0OiBzdHJpbmcgfCBudWxsKSA9PiB2b2lkXG4gIHNldFZpZXdTdGF0ZTogKHN0YXRlOiBWaWV3U3RhdGUpID0+IHZvaWRcbiAgb25BZGRDb21wbGV0ZT86ICgpID0+IHZvaWQgfCBQcm9taXNlPHZvaWQ+XG4gIGNsaU1vZGU/OiBib29sZWFuXG59XG5cbmV4cG9ydCBmdW5jdGlvbiBBZGRNYXJrZXRwbGFjZSh7XG4gIGlucHV0VmFsdWUsXG4gIHNldElucHV0VmFsdWUsXG4gIGN1cnNvck9mZnNldCxcbiAgc2V0Q3Vyc29yT2Zmc2V0LFxuICBlcnJvcixcbiAgc2V0RXJyb3IsXG4gIHJlc3VsdCxcbiAgc2V0UmVzdWx0LFxuICBzZXRWaWV3U3RhdGUsXG4gIG9uQWRkQ29tcGxldGUsXG4gIGNsaU1vZGUgPSBmYWxzZSxcbn06IFByb3BzKTogUmVhY3QuUmVhY3ROb2RlIHtcbiAgY29uc3QgaGFzQXR0ZW1wdGVkQXV0b0FkZCA9IHVzZVJlZihmYWxzZSlcbiAgY29uc3QgW2lzTG9hZGluZywgc2V0TG9hZGluZ10gPSB1c2VTdGF0ZShmYWxzZSlcbiAgY29uc3QgW3Byb2dyZXNzTWVzc2FnZSwgc2V0UHJvZ3Jlc3NNZXNzYWdlXSA9IHVzZVN0YXRlPHN0cmluZz4oJycpXG5cbiAgY29uc3QgaGFuZGxlQWRkID0gYXN5bmMgKCkgPT4ge1xuICAgIGNvbnN0IGlucHV0ID0gaW5wdXRWYWx1ZS50cmltKClcbiAgICBpZiAoIWlucHV0KSB7XG4gICAgICBzZXRFcnJvcignUGxlYXNlIGVudGVyIGEgbWFya2V0cGxhY2Ugc291cmNlJylcbiAgICAgIHJldHVyblxuICAgIH1cblxuICAgIGNvbnN0IHBhcnNlZCA9IGF3YWl0IHBhcnNlTWFya2V0cGxhY2VJbnB1dChpbnB1dClcbiAgICBpZiAoIXBhcnNlZCkge1xuICAgICAgc2V0RXJyb3IoXG4gICAgICAgICdJbnZhbGlkIG1hcmtldHBsYWNlIHNvdXJjZSBmb3JtYXQuIFRyeTogb3duZXIvcmVwbywgaHR0cHM6Ly8uLi4sIG9yIC4vcGF0aCcsXG4gICAgICApXG4gICAgICByZXR1cm5cbiAgICB9XG5cbiAgICAvLyBDaGVjayBpZiBwYXJzZU1hcmtldHBsYWNlSW5wdXQgcmV0dXJuZWQgYW4gZXJyb3JcbiAgICBpZiAoJ2Vycm9yJyBpbiBwYXJzZWQpIHtcbiAgICAgIHNldEVycm9yKHBhcnNlZC5lcnJvcilcbiAgICAgIHJldHVyblxuICAgIH1cblxuICAgIHNldEVycm9yKG51bGwpXG5cbiAgICB0cnkge1xuICAgICAgc2V0TG9hZGluZyh0cnVlKVxuICAgICAgc2V0UHJvZ3Jlc3NNZXNzYWdlKCcnKVxuICAgICAgY29uc3QgeyBuYW1lLCByZXNvbHZlZFNvdXJjZSB9ID0gYXdhaXQgYWRkTWFya2V0cGxhY2VTb3VyY2UoXG4gICAgICAgIHBhcnNlZCxcbiAgICAgICAgbWVzc2FnZSA9PiB7XG4gICAgICAgICAgc2V0UHJvZ3Jlc3NNZXNzYWdlKG1lc3NhZ2UpXG4gICAgICAgIH0sXG4gICAgICApXG4gICAgICBzYXZlTWFya2V0cGxhY2VUb1NldHRpbmdzKG5hbWUsIHsgc291cmNlOiByZXNvbHZlZFNvdXJjZSB9KVxuICAgICAgY2xlYXJBbGxDYWNoZXMoKVxuXG4gICAgICBsZXQgc291cmNlVHlwZSA9IHBhcnNlZC5zb3VyY2VcbiAgICAgIGlmIChwYXJzZWQuc291cmNlID09PSAnZ2l0aHViJykge1xuICAgICAgICBzb3VyY2VUeXBlID1cbiAgICAgICAgICBwYXJzZWQucmVwbyBhcyBBbmF
Line truncated
|
||||
@@ -0,0 +1,802 @@
|
||||
import figures from 'figures';
|
||||
import * as React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.js';
|
||||
import { Byline } from '../../components/design-system/Byline.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { useKeybinding, useKeybindings } from '../../keybindings/useKeybinding.js';
|
||||
import type { LoadedPlugin } from '../../types/plugin.js';
|
||||
import { count } from '../../utils/array.js';
|
||||
import { openBrowser } from '../../utils/browser.js';
|
||||
import { logForDebugging } from '../../utils/debug.js';
|
||||
import { errorMessage } from '../../utils/errors.js';
|
||||
import { clearAllCaches } from '../../utils/plugins/cacheUtils.js';
|
||||
import { formatInstallCount, getInstallCounts } from '../../utils/plugins/installCounts.js';
|
||||
import { isPluginGloballyInstalled, isPluginInstalled } from '../../utils/plugins/installedPluginsManager.js';
|
||||
import { createPluginId, formatFailureDetails, formatMarketplaceLoadingErrors, getMarketplaceSourceDisplay, loadMarketplacesWithGracefulDegradation } from '../../utils/plugins/marketplaceHelpers.js';
|
||||
import { getMarketplace, loadKnownMarketplacesConfig } from '../../utils/plugins/marketplaceManager.js';
|
||||
import { OFFICIAL_MARKETPLACE_NAME } from '../../utils/plugins/officialMarketplace.js';
|
||||
import { installPluginFromMarketplace } from '../../utils/plugins/pluginInstallationHelpers.js';
|
||||
import { isPluginBlockedByPolicy } from '../../utils/plugins/pluginPolicy.js';
|
||||
import { plural } from '../../utils/stringUtils.js';
|
||||
import { truncateToWidth } from '../../utils/truncate.js';
|
||||
import { findPluginOptionsTarget, PluginOptionsFlow } from './PluginOptionsFlow.js';
|
||||
import { PluginTrustWarning } from './PluginTrustWarning.js';
|
||||
import { buildPluginDetailsMenuOptions, extractGitHubRepo, type InstallablePlugin, PluginSelectionKeyHint } from './pluginDetailsHelpers.js';
|
||||
import type { ViewState as ParentViewState } from './types.js';
|
||||
import { usePagination } from './usePagination.js';
|
||||
type Props = {
|
||||
error: string | null;
|
||||
setError: (error: string | null) => void;
|
||||
result: string | null;
|
||||
setResult: (result: string | null) => void;
|
||||
setViewState: (state: ParentViewState) => void;
|
||||
onInstallComplete?: () => void | Promise<void>;
|
||||
targetMarketplace?: string;
|
||||
targetPlugin?: string;
|
||||
};
|
||||
type ViewState = 'marketplace-list' | 'plugin-list' | 'plugin-details' | {
|
||||
type: 'plugin-options';
|
||||
plugin: LoadedPlugin;
|
||||
pluginId: string;
|
||||
};
|
||||
type MarketplaceInfo = {
|
||||
name: string;
|
||||
totalPlugins: number;
|
||||
installedCount: number;
|
||||
source?: string;
|
||||
};
|
||||
export function BrowseMarketplace({
|
||||
error,
|
||||
setError,
|
||||
result: _result,
|
||||
setResult,
|
||||
setViewState: setParentViewState,
|
||||
onInstallComplete,
|
||||
targetMarketplace,
|
||||
targetPlugin
|
||||
}: Props): React.ReactNode {
|
||||
// View state
|
||||
const [viewState, setViewState] = useState<ViewState>('marketplace-list');
|
||||
const [selectedMarketplace, setSelectedMarketplace] = useState<string | null>(null);
|
||||
const [selectedPlugin, setSelectedPlugin] = useState<InstallablePlugin | null>(null);
|
||||
|
||||
// Data state
|
||||
const [marketplaces, setMarketplaces] = useState<MarketplaceInfo[]>([]);
|
||||
const [availablePlugins, setAvailablePlugins] = useState<InstallablePlugin[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [installCounts, setInstallCounts] = useState<Map<string, number> | null>(null);
|
||||
|
||||
// Selection state
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const [selectedForInstall, setSelectedForInstall] = useState<Set<string>>(new Set());
|
||||
const [installingPlugins, setInstallingPlugins] = useState<Set<string>>(new Set());
|
||||
|
||||
// Pagination for plugin list (continuous scrolling)
|
||||
const pagination = usePagination<InstallablePlugin>({
|
||||
totalItems: availablePlugins.length,
|
||||
selectedIndex
|
||||
});
|
||||
|
||||
// Details view state
|
||||
const [detailsMenuIndex, setDetailsMenuIndex] = useState(0);
|
||||
const [isInstalling, setIsInstalling] = useState(false);
|
||||
const [installError, setInstallError] = useState<string | null>(null);
|
||||
|
||||
// Warning state for non-critical errors (e.g., some marketplaces failed to load)
|
||||
const [warning, setWarning] = useState<string | null>(null);
|
||||
|
||||
// Handle escape to go back - viewState-dependent navigation
|
||||
const handleBack = React.useCallback(() => {
|
||||
if (viewState === 'plugin-list') {
|
||||
// If navigated directly to a specific marketplace via targetMarketplace,
|
||||
// go back to manage-marketplaces showing that marketplace's details
|
||||
if (targetMarketplace) {
|
||||
setParentViewState({
|
||||
type: 'manage-marketplaces',
|
||||
targetMarketplace
|
||||
});
|
||||
} else if (marketplaces.length === 1) {
|
||||
// If there's only one marketplace, skip the marketplace-list view
|
||||
// since we auto-navigated past it on load
|
||||
setParentViewState({
|
||||
type: 'menu'
|
||||
});
|
||||
} else {
|
||||
setViewState('marketplace-list');
|
||||
setSelectedMarketplace(null);
|
||||
setSelectedForInstall(new Set());
|
||||
}
|
||||
} else if (viewState === 'plugin-details') {
|
||||
setViewState('plugin-list');
|
||||
setSelectedPlugin(null);
|
||||
} else {
|
||||
// At root level (marketplace-list), exit the plugin menu
|
||||
setParentViewState({
|
||||
type: 'menu'
|
||||
});
|
||||
}
|
||||
}, [viewState, targetMarketplace, setParentViewState, marketplaces.length]);
|
||||
useKeybinding('confirm:no', handleBack, {
|
||||
context: 'Confirmation'
|
||||
});
|
||||
|
||||
// Load marketplaces and count installed plugins
|
||||
useEffect(() => {
|
||||
async function loadMarketplaceData() {
|
||||
try {
|
||||
const config = await loadKnownMarketplacesConfig();
|
||||
|
||||
// Load marketplaces with graceful degradation
|
||||
const {
|
||||
marketplaces: marketplaces_0,
|
||||
failures
|
||||
} = await loadMarketplacesWithGracefulDegradation(config);
|
||||
const marketplaceInfos: MarketplaceInfo[] = [];
|
||||
for (const {
|
||||
name,
|
||||
config: marketplaceConfig,
|
||||
data: marketplace
|
||||
} of marketplaces_0) {
|
||||
if (marketplace) {
|
||||
// Count how many plugins from this marketplace are installed
|
||||
const installedFromThisMarketplace = count(marketplace.plugins, plugin => isPluginInstalled(createPluginId(plugin.name, name)));
|
||||
marketplaceInfos.push({
|
||||
name,
|
||||
totalPlugins: marketplace.plugins.length,
|
||||
installedCount: installedFromThisMarketplace,
|
||||
source: getMarketplaceSourceDisplay(marketplaceConfig.source)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sort so claude-plugin-directory is always first
|
||||
marketplaceInfos.sort((a, b) => {
|
||||
if (a.name === 'claude-plugin-directory') return -1;
|
||||
if (b.name === 'claude-plugin-directory') return 1;
|
||||
return 0;
|
||||
});
|
||||
setMarketplaces(marketplaceInfos);
|
||||
|
||||
// Handle marketplace loading errors/warnings
|
||||
const successCount = count(marketplaces_0, m => m.data !== null);
|
||||
const errorResult = formatMarketplaceLoadingErrors(failures, successCount);
|
||||
if (errorResult) {
|
||||
if (errorResult.type === 'warning') {
|
||||
setWarning(errorResult.message + '. Showing available marketplaces.');
|
||||
} else {
|
||||
throw new Error(errorResult.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Skip marketplace selection if there's only one marketplace
|
||||
if (marketplaceInfos.length === 1 && !targetMarketplace && !targetPlugin) {
|
||||
const singleMarketplace = marketplaceInfos[0];
|
||||
if (singleMarketplace) {
|
||||
setSelectedMarketplace(singleMarketplace.name);
|
||||
setViewState('plugin-list');
|
||||
}
|
||||
}
|
||||
|
||||
// Handle targetMarketplace and targetPlugin after marketplaces are loaded
|
||||
if (targetPlugin) {
|
||||
// Search for the plugin across all marketplaces
|
||||
let foundPlugin: InstallablePlugin | null = null;
|
||||
let foundMarketplace: string | null = null;
|
||||
for (const [name_0] of Object.entries(config)) {
|
||||
const marketplace_0 = await getMarketplace(name_0);
|
||||
if (marketplace_0) {
|
||||
const plugin_0 = marketplace_0.plugins.find(p => p.name === targetPlugin);
|
||||
if (plugin_0) {
|
||||
const pluginId = createPluginId(plugin_0.name, name_0);
|
||||
foundPlugin = {
|
||||
entry: plugin_0,
|
||||
marketplaceName: name_0,
|
||||
pluginId,
|
||||
// isPluginGloballyInstalled: only block when user/managed scope
|
||||
// exists (nothing to add). Project/local-scope installs don't
|
||||
// block — user may want to promote to user scope (gh-29997).
|
||||
isInstalled: isPluginGloballyInstalled(pluginId)
|
||||
};
|
||||
foundMarketplace = name_0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (foundPlugin && foundMarketplace) {
|
||||
// Block only on global (user/managed) install — project/local scope
|
||||
// means the user might still want to add a user-scope entry so the
|
||||
// plugin is available in other projects (gh-29997, gh-29240, gh-29392).
|
||||
// The plugin-details view offers all three scope options; the backend
|
||||
// (installPluginOp → addInstalledPlugin) already supports multiple
|
||||
// scope entries per plugin.
|
||||
const pluginId_0 = foundPlugin.pluginId;
|
||||
const globallyInstalled = isPluginGloballyInstalled(pluginId_0);
|
||||
if (globallyInstalled) {
|
||||
setError(`Plugin '${pluginId_0}' is already installed globally. Use '/plugin' to manage existing plugins.`);
|
||||
} else {
|
||||
// Navigate to the plugin details view
|
||||
setSelectedMarketplace(foundMarketplace);
|
||||
setSelectedPlugin(foundPlugin);
|
||||
setViewState('plugin-details');
|
||||
}
|
||||
} else {
|
||||
setError(`Plugin "${targetPlugin}" not found in any marketplace`);
|
||||
}
|
||||
} else if (targetMarketplace) {
|
||||
// Navigate directly to the specified marketplace
|
||||
const marketplaceExists = marketplaceInfos.some(m_0 => m_0.name === targetMarketplace);
|
||||
if (marketplaceExists) {
|
||||
setSelectedMarketplace(targetMarketplace);
|
||||
setViewState('plugin-list');
|
||||
} else {
|
||||
setError(`Marketplace "${targetMarketplace}" not found`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to load marketplaces');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
void loadMarketplaceData();
|
||||
}, [setError, targetMarketplace, targetPlugin]);
|
||||
|
||||
// Load plugins when a marketplace is selected
|
||||
useEffect(() => {
|
||||
if (!selectedMarketplace) return;
|
||||
let cancelled = false;
|
||||
async function loadPluginsForMarketplace(marketplaceName: string) {
|
||||
setLoading(true);
|
||||
try {
|
||||
const marketplace_1 = await getMarketplace(marketplaceName);
|
||||
if (cancelled) return;
|
||||
if (!marketplace_1) {
|
||||
throw new Error(`Failed to load marketplace: ${marketplaceName}`);
|
||||
}
|
||||
|
||||
// Filter out already installed plugins
|
||||
const installablePlugins: InstallablePlugin[] = [];
|
||||
for (const entry of marketplace_1.plugins) {
|
||||
const pluginId_1 = createPluginId(entry.name, marketplaceName);
|
||||
if (isPluginBlockedByPolicy(pluginId_1)) continue;
|
||||
installablePlugins.push({
|
||||
entry,
|
||||
marketplaceName: marketplaceName,
|
||||
pluginId: pluginId_1,
|
||||
// Only mark as "installed" when globally scoped (user/managed).
|
||||
// Project/local installs don't block — user can add user scope
|
||||
// via the plugin-details view (gh-29997).
|
||||
isInstalled: isPluginGloballyInstalled(pluginId_1)
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch install counts and sort by popularity
|
||||
try {
|
||||
const counts = await getInstallCounts();
|
||||
if (cancelled) return;
|
||||
setInstallCounts(counts);
|
||||
if (counts) {
|
||||
// Sort by install count (descending), then alphabetically
|
||||
installablePlugins.sort((a_1, b_1) => {
|
||||
const countA = counts.get(a_1.pluginId) ?? 0;
|
||||
const countB = counts.get(b_1.pluginId) ?? 0;
|
||||
if (countA !== countB) return countB - countA;
|
||||
return a_1.entry.name.localeCompare(b_1.entry.name);
|
||||
});
|
||||
} else {
|
||||
// No counts available - sort alphabetically
|
||||
installablePlugins.sort((a_2, b_2) => a_2.entry.name.localeCompare(b_2.entry.name));
|
||||
}
|
||||
} catch (error_0) {
|
||||
if (cancelled) return;
|
||||
// Log the error, then gracefully degrade to alphabetical sort
|
||||
logForDebugging(`Failed to fetch install counts: ${errorMessage(error_0)}`);
|
||||
installablePlugins.sort((a_0, b_0) => a_0.entry.name.localeCompare(b_0.entry.name));
|
||||
}
|
||||
setAvailablePlugins(installablePlugins);
|
||||
setSelectedIndex(0);
|
||||
setSelectedForInstall(new Set());
|
||||
} catch (err_0) {
|
||||
if (cancelled) return;
|
||||
setError(err_0 instanceof Error ? err_0.message : 'Failed to load plugins');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
void loadPluginsForMarketplace(selectedMarketplace);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [selectedMarketplace, setError]);
|
||||
|
||||
// Install selected plugins
|
||||
const installSelectedPlugins = async () => {
|
||||
if (selectedForInstall.size === 0) return;
|
||||
const pluginsToInstall = availablePlugins.filter(p_0 => selectedForInstall.has(p_0.pluginId));
|
||||
setInstallingPlugins(new Set(pluginsToInstall.map(p_1 => p_1.pluginId)));
|
||||
let successCount_0 = 0;
|
||||
let failureCount = 0;
|
||||
const newFailedPlugins: Array<{
|
||||
name: string;
|
||||
reason: string;
|
||||
}> = [];
|
||||
for (const plugin_1 of pluginsToInstall) {
|
||||
const result = await installPluginFromMarketplace({
|
||||
pluginId: plugin_1.pluginId,
|
||||
entry: plugin_1.entry,
|
||||
marketplaceName: plugin_1.marketplaceName,
|
||||
scope: 'user'
|
||||
});
|
||||
if (result.success) {
|
||||
successCount_0++;
|
||||
} else {
|
||||
failureCount++;
|
||||
newFailedPlugins.push({
|
||||
name: plugin_1.entry.name,
|
||||
reason: result.error
|
||||
});
|
||||
}
|
||||
}
|
||||
setInstallingPlugins(new Set());
|
||||
setSelectedForInstall(new Set());
|
||||
clearAllCaches();
|
||||
|
||||
// Handle installation results
|
||||
if (failureCount === 0) {
|
||||
// All succeeded
|
||||
const message = `✓ Installed ${successCount_0} ${plural(successCount_0, 'plugin')}. ` + `Run /reload-plugins to activate.`;
|
||||
setResult(message);
|
||||
} else if (successCount_0 === 0) {
|
||||
// All failed - show error with reasons
|
||||
setError(`Failed to install: ${formatFailureDetails(newFailedPlugins, true)}`);
|
||||
} else {
|
||||
// Mixed results - show partial success
|
||||
const message_0 = `✓ Installed ${successCount_0} of ${successCount_0 + failureCount} plugins. ` + `Failed: ${formatFailureDetails(newFailedPlugins, false)}. ` + `Run /reload-plugins to activate successfully installed plugins.`;
|
||||
setResult(message_0);
|
||||
}
|
||||
|
||||
// Handle completion callback and navigation
|
||||
if (successCount_0 > 0) {
|
||||
if (onInstallComplete) {
|
||||
await onInstallComplete();
|
||||
}
|
||||
}
|
||||
setParentViewState({
|
||||
type: 'menu'
|
||||
});
|
||||
};
|
||||
|
||||
// Install single plugin from details view
|
||||
const handleSinglePluginInstall = async (plugin_2: InstallablePlugin, scope: 'user' | 'project' | 'local' = 'user') => {
|
||||
setIsInstalling(true);
|
||||
setInstallError(null);
|
||||
const result_0 = await installPluginFromMarketplace({
|
||||
pluginId: plugin_2.pluginId,
|
||||
entry: plugin_2.entry,
|
||||
marketplaceName: plugin_2.marketplaceName,
|
||||
scope
|
||||
});
|
||||
if (result_0.success) {
|
||||
const loaded = await findPluginOptionsTarget(plugin_2.pluginId);
|
||||
if (loaded) {
|
||||
setIsInstalling(false);
|
||||
setViewState({
|
||||
type: 'plugin-options',
|
||||
plugin: loaded,
|
||||
pluginId: plugin_2.pluginId
|
||||
});
|
||||
return;
|
||||
}
|
||||
setResult(result_0.message);
|
||||
if (onInstallComplete) {
|
||||
await onInstallComplete();
|
||||
}
|
||||
setParentViewState({
|
||||
type: 'menu'
|
||||
});
|
||||
} else {
|
||||
setIsInstalling(false);
|
||||
setInstallError(result_0.error);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle error state
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
setResult(error);
|
||||
}
|
||||
}, [error, setResult]);
|
||||
|
||||
// Marketplace-list navigation
|
||||
useKeybindings({
|
||||
'select:previous': () => {
|
||||
if (selectedIndex > 0) {
|
||||
setSelectedIndex(selectedIndex - 1);
|
||||
}
|
||||
},
|
||||
'select:next': () => {
|
||||
if (selectedIndex < marketplaces.length - 1) {
|
||||
setSelectedIndex(selectedIndex + 1);
|
||||
}
|
||||
},
|
||||
'select:accept': () => {
|
||||
const marketplace_2 = marketplaces[selectedIndex];
|
||||
if (marketplace_2) {
|
||||
setSelectedMarketplace(marketplace_2.name);
|
||||
setViewState('plugin-list');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
context: 'Select',
|
||||
isActive: viewState === 'marketplace-list'
|
||||
});
|
||||
|
||||
// Plugin-list navigation
|
||||
useKeybindings({
|
||||
'select:previous': () => {
|
||||
if (selectedIndex > 0) {
|
||||
pagination.handleSelectionChange(selectedIndex - 1, setSelectedIndex);
|
||||
}
|
||||
},
|
||||
'select:next': () => {
|
||||
if (selectedIndex < availablePlugins.length - 1) {
|
||||
pagination.handleSelectionChange(selectedIndex + 1, setSelectedIndex);
|
||||
}
|
||||
},
|
||||
'select:accept': () => {
|
||||
if (selectedIndex === availablePlugins.length && selectedForInstall.size > 0) {
|
||||
void installSelectedPlugins();
|
||||
} else if (selectedIndex < availablePlugins.length) {
|
||||
const plugin_3 = availablePlugins[selectedIndex];
|
||||
if (plugin_3) {
|
||||
if (plugin_3.isInstalled) {
|
||||
setParentViewState({
|
||||
type: 'manage-plugins',
|
||||
targetPlugin: plugin_3.entry.name,
|
||||
targetMarketplace: plugin_3.marketplaceName
|
||||
});
|
||||
} else {
|
||||
setSelectedPlugin(plugin_3);
|
||||
setViewState('plugin-details');
|
||||
setDetailsMenuIndex(0);
|
||||
setInstallError(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, {
|
||||
context: 'Select',
|
||||
isActive: viewState === 'plugin-list'
|
||||
});
|
||||
useKeybindings({
|
||||
'plugin:toggle': () => {
|
||||
if (selectedIndex < availablePlugins.length) {
|
||||
const plugin_4 = availablePlugins[selectedIndex];
|
||||
if (plugin_4 && !plugin_4.isInstalled) {
|
||||
const newSelection = new Set(selectedForInstall);
|
||||
if (newSelection.has(plugin_4.pluginId)) {
|
||||
newSelection.delete(plugin_4.pluginId);
|
||||
} else {
|
||||
newSelection.add(plugin_4.pluginId);
|
||||
}
|
||||
setSelectedForInstall(newSelection);
|
||||
}
|
||||
}
|
||||
},
|
||||
'plugin:install': () => {
|
||||
if (selectedForInstall.size > 0) {
|
||||
void installSelectedPlugins();
|
||||
}
|
||||
}
|
||||
}, {
|
||||
context: 'Plugin',
|
||||
isActive: viewState === 'plugin-list'
|
||||
});
|
||||
|
||||
// Plugin-details navigation
|
||||
const detailsMenuOptions = React.useMemo(() => {
|
||||
if (!selectedPlugin) return [];
|
||||
const hasHomepage = selectedPlugin.entry.homepage;
|
||||
const githubRepo = extractGitHubRepo(selectedPlugin);
|
||||
return buildPluginDetailsMenuOptions(hasHomepage, githubRepo);
|
||||
}, [selectedPlugin]);
|
||||
useKeybindings({
|
||||
'select:previous': () => {
|
||||
if (detailsMenuIndex > 0) {
|
||||
setDetailsMenuIndex(detailsMenuIndex - 1);
|
||||
}
|
||||
},
|
||||
'select:next': () => {
|
||||
if (detailsMenuIndex < detailsMenuOptions.length - 1) {
|
||||
setDetailsMenuIndex(detailsMenuIndex + 1);
|
||||
}
|
||||
},
|
||||
'select:accept': () => {
|
||||
if (!selectedPlugin) return;
|
||||
const action = detailsMenuOptions[detailsMenuIndex]?.action;
|
||||
const hasHomepage_0 = selectedPlugin.entry.homepage;
|
||||
const githubRepo_0 = extractGitHubRepo(selectedPlugin);
|
||||
if (action === 'install-user') {
|
||||
void handleSinglePluginInstall(selectedPlugin, 'user');
|
||||
} else if (action === 'install-project') {
|
||||
void handleSinglePluginInstall(selectedPlugin, 'project');
|
||||
} else if (action === 'install-local') {
|
||||
void handleSinglePluginInstall(selectedPlugin, 'local');
|
||||
} else if (action === 'homepage' && hasHomepage_0) {
|
||||
void openBrowser(hasHomepage_0);
|
||||
} else if (action === 'github' && githubRepo_0) {
|
||||
void openBrowser(`https://github.com/${githubRepo_0}`);
|
||||
} else if (action === 'back') {
|
||||
setViewState('plugin-list');
|
||||
setSelectedPlugin(null);
|
||||
}
|
||||
}
|
||||
}, {
|
||||
context: 'Select',
|
||||
isActive: viewState === 'plugin-details' && !!selectedPlugin
|
||||
});
|
||||
if (typeof viewState === 'object' && viewState.type === 'plugin-options') {
|
||||
const {
|
||||
plugin: plugin_5,
|
||||
pluginId: pluginId_2
|
||||
} = viewState;
|
||||
function finish(msg: string): void {
|
||||
setResult(msg);
|
||||
if (onInstallComplete) {
|
||||
void onInstallComplete();
|
||||
}
|
||||
setParentViewState({
|
||||
type: 'menu'
|
||||
});
|
||||
}
|
||||
return <PluginOptionsFlow plugin={plugin_5} pluginId={pluginId_2} onDone={(outcome, detail) => {
|
||||
switch (outcome) {
|
||||
case 'configured':
|
||||
finish(`✓ Installed and configured ${plugin_5.name}. Run /reload-plugins to apply.`);
|
||||
break;
|
||||
case 'skipped':
|
||||
finish(`✓ Installed ${plugin_5.name}. Run /reload-plugins to apply.`);
|
||||
break;
|
||||
case 'error':
|
||||
finish(`Installed but failed to save config: ${detail}`);
|
||||
break;
|
||||
}
|
||||
}} />;
|
||||
}
|
||||
|
||||
// Loading state
|
||||
if (loading) {
|
||||
return <Text>Loading…</Text>;
|
||||
}
|
||||
|
||||
// Error state
|
||||
if (error) {
|
||||
return <Text color="error">{error}</Text>;
|
||||
}
|
||||
|
||||
// Marketplace selection view
|
||||
if (viewState === 'marketplace-list') {
|
||||
if (marketplaces.length === 0) {
|
||||
return <Box flexDirection="column">
|
||||
<Box marginBottom={1}>
|
||||
<Text bold>Select marketplace</Text>
|
||||
</Box>
|
||||
<Text>No marketplaces configured.</Text>
|
||||
<Text dimColor>
|
||||
Add a marketplace first using {"'Add marketplace'"}.
|
||||
</Text>
|
||||
<Box marginTop={1} paddingLeft={1}>
|
||||
<Text dimColor>
|
||||
<ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="go back" />
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>;
|
||||
}
|
||||
return <Box flexDirection="column">
|
||||
<Box marginBottom={1}>
|
||||
<Text bold>Select marketplace</Text>
|
||||
</Box>
|
||||
|
||||
{/* Warning banner for marketplace load failures */}
|
||||
{warning && <Box marginBottom={1} flexDirection="column">
|
||||
<Text color="warning">
|
||||
{figures.warning} {warning}
|
||||
</Text>
|
||||
</Box>}
|
||||
{marketplaces.map((marketplace_3, index) => <Box key={marketplace_3.name} flexDirection="column" marginBottom={index < marketplaces.length - 1 ? 1 : 0}>
|
||||
<Box>
|
||||
<Text color={selectedIndex === index ? 'suggestion' : undefined}>
|
||||
{selectedIndex === index ? figures.pointer : ' '}{' '}
|
||||
{marketplace_3.name}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box marginLeft={2}>
|
||||
<Text dimColor>
|
||||
{marketplace_3.totalPlugins}{' '}
|
||||
{plural(marketplace_3.totalPlugins, 'plugin')} available
|
||||
{marketplace_3.installedCount > 0 && ` · ${marketplace_3.installedCount} already installed`}
|
||||
{marketplace_3.source && ` · ${marketplace_3.source}`}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>)}
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor italic>
|
||||
<Byline>
|
||||
<ConfigurableShortcutHint action="select:accept" context="Select" fallback="Enter" description="select" />
|
||||
<ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="go back" />
|
||||
</Byline>
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>;
|
||||
}
|
||||
|
||||
// Plugin details view
|
||||
if (viewState === 'plugin-details' && selectedPlugin) {
|
||||
const hasHomepage_1 = selectedPlugin.entry.homepage;
|
||||
const githubRepo_1 = extractGitHubRepo(selectedPlugin);
|
||||
const menuOptions = buildPluginDetailsMenuOptions(hasHomepage_1, githubRepo_1);
|
||||
return <Box flexDirection="column">
|
||||
<Box marginBottom={1}>
|
||||
<Text bold>Plugin Details</Text>
|
||||
</Box>
|
||||
|
||||
{/* Plugin metadata */}
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text bold>{selectedPlugin.entry.name}</Text>
|
||||
{selectedPlugin.entry.version && <Text dimColor>Version: {selectedPlugin.entry.version}</Text>}
|
||||
{selectedPlugin.entry.description && <Box marginTop={1}>
|
||||
<Text>{selectedPlugin.entry.description}</Text>
|
||||
</Box>}
|
||||
{selectedPlugin.entry.author && <Box marginTop={1}>
|
||||
<Text dimColor>
|
||||
By:{' '}
|
||||
{typeof selectedPlugin.entry.author === 'string' ? selectedPlugin.entry.author : selectedPlugin.entry.author.name}
|
||||
</Text>
|
||||
</Box>}
|
||||
</Box>
|
||||
|
||||
{/* What will be installed */}
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text bold>Will install:</Text>
|
||||
{selectedPlugin.entry.commands && <Text dimColor>
|
||||
· Commands:{' '}
|
||||
{Array.isArray(selectedPlugin.entry.commands) ? selectedPlugin.entry.commands.join(', ') : Object.keys(selectedPlugin.entry.commands).join(', ')}
|
||||
</Text>}
|
||||
{selectedPlugin.entry.agents && <Text dimColor>
|
||||
· Agents:{' '}
|
||||
{Array.isArray(selectedPlugin.entry.agents) ? selectedPlugin.entry.agents.join(', ') : Object.keys(selectedPlugin.entry.agents).join(', ')}
|
||||
</Text>}
|
||||
{selectedPlugin.entry.hooks && <Text dimColor>
|
||||
· Hooks: {Object.keys(selectedPlugin.entry.hooks).join(', ')}
|
||||
</Text>}
|
||||
{selectedPlugin.entry.mcpServers && <Text dimColor>
|
||||
· MCP Servers:{' '}
|
||||
{Array.isArray(selectedPlugin.entry.mcpServers) ? selectedPlugin.entry.mcpServers.join(', ') : typeof selectedPlugin.entry.mcpServers === 'object' ? Object.keys(selectedPlugin.entry.mcpServers).join(', ') : 'configured'}
|
||||
</Text>}
|
||||
{!selectedPlugin.entry.commands && !selectedPlugin.entry.agents && !selectedPlugin.entry.hooks && !selectedPlugin.entry.mcpServers && <>
|
||||
{typeof selectedPlugin.entry.source === 'object' && 'source' in selectedPlugin.entry.source && (selectedPlugin.entry.source.source === 'github' || selectedPlugin.entry.source.source === 'url' || selectedPlugin.entry.source.source === 'npm' || selectedPlugin.entry.source.source === 'pip') ? <Text dimColor>
|
||||
· Component summary not available for remote plugin
|
||||
</Text> :
|
||||
// TODO: Actually scan local plugin directories to show real components
|
||||
// This would require accessing the filesystem to check for:
|
||||
// - commands/ directory and list files
|
||||
// - agents/ directory and list files
|
||||
// - hooks/ directory and list files
|
||||
// - .mcp.json or mcp-servers.json files
|
||||
<Text dimColor>
|
||||
· Components will be discovered at installation
|
||||
</Text>}
|
||||
</>}
|
||||
</Box>
|
||||
|
||||
<PluginTrustWarning />
|
||||
|
||||
{/* Error message */}
|
||||
{installError && <Box marginBottom={1}>
|
||||
<Text color="error">Error: {installError}</Text>
|
||||
</Box>}
|
||||
|
||||
{/* Menu options */}
|
||||
<Box flexDirection="column">
|
||||
{menuOptions.map((option, index_0) => <Box key={option.action}>
|
||||
{detailsMenuIndex === index_0 && <Text>{'> '}</Text>}
|
||||
{detailsMenuIndex !== index_0 && <Text>{' '}</Text>}
|
||||
<Text bold={detailsMenuIndex === index_0}>
|
||||
{isInstalling && option.action === 'install' ? 'Installing…' : option.label}
|
||||
</Text>
|
||||
</Box>)}
|
||||
</Box>
|
||||
|
||||
<Box marginTop={1} paddingLeft={1}>
|
||||
<Text dimColor>
|
||||
<Byline>
|
||||
<ConfigurableShortcutHint action="select:accept" context="Select" fallback="Enter" description="select" />
|
||||
<ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="back" />
|
||||
</Byline>
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>;
|
||||
}
|
||||
|
||||
// Plugin installation view
|
||||
if (availablePlugins.length === 0) {
|
||||
return <Box flexDirection="column">
|
||||
<Box marginBottom={1}>
|
||||
<Text bold>Install plugins</Text>
|
||||
</Box>
|
||||
<Text dimColor>No new plugins available to install.</Text>
|
||||
<Text dimColor>
|
||||
All plugins from this marketplace are already installed.
|
||||
</Text>
|
||||
<Box marginLeft={3}>
|
||||
<Text dimColor italic>
|
||||
<ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="go back" />
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>;
|
||||
}
|
||||
|
||||
// Get visible plugins from pagination
|
||||
const visiblePlugins = pagination.getVisibleItems(availablePlugins);
|
||||
return <Box flexDirection="column">
|
||||
<Box marginBottom={1}>
|
||||
<Text bold>Install Plugins</Text>
|
||||
</Box>
|
||||
|
||||
{/* Scroll up indicator */}
|
||||
{pagination.scrollPosition.canScrollUp && <Box>
|
||||
<Text dimColor> {figures.arrowUp} more above</Text>
|
||||
</Box>}
|
||||
|
||||
{/* Plugin list */}
|
||||
{visiblePlugins.map((plugin_6, visibleIndex) => {
|
||||
const actualIndex = pagination.toActualIndex(visibleIndex);
|
||||
const isSelected = selectedIndex === actualIndex;
|
||||
const isSelectedForInstall = selectedForInstall.has(plugin_6.pluginId);
|
||||
const isInstalling_0 = installingPlugins.has(plugin_6.pluginId);
|
||||
const isLast = visibleIndex === visiblePlugins.length - 1;
|
||||
return <Box key={plugin_6.pluginId} flexDirection="column" marginBottom={isLast && !error ? 0 : 1}>
|
||||
<Box>
|
||||
<Text color={isSelected ? 'suggestion' : undefined}>
|
||||
{isSelected ? figures.pointer : ' '}{' '}
|
||||
</Text>
|
||||
<Text color={plugin_6.isInstalled ? 'success' : undefined}>
|
||||
{plugin_6.isInstalled ? figures.tick : isInstalling_0 ? figures.ellipsis : isSelectedForInstall ? figures.radioOn : figures.radioOff}{' '}
|
||||
{plugin_6.entry.name}
|
||||
{plugin_6.entry.category && <Text dimColor> [{plugin_6.entry.category}]</Text>}
|
||||
{plugin_6.entry.tags?.includes('community-managed') && <Text dimColor> [Community Managed]</Text>}
|
||||
{plugin_6.isInstalled && <Text dimColor> (installed)</Text>}
|
||||
{installCounts && selectedMarketplace === OFFICIAL_MARKETPLACE_NAME && <Text dimColor>
|
||||
{' · '}
|
||||
{formatInstallCount(installCounts.get(plugin_6.pluginId) ?? 0)}{' '}
|
||||
installs
|
||||
</Text>}
|
||||
</Text>
|
||||
</Box>
|
||||
{plugin_6.entry.description && <Box marginLeft={4}>
|
||||
<Text dimColor>
|
||||
{truncateToWidth(plugin_6.entry.description, 60)}
|
||||
</Text>
|
||||
{plugin_6.entry.version && <Text dimColor> · v{plugin_6.entry.version}</Text>}
|
||||
</Box>}
|
||||
</Box>;
|
||||
})}
|
||||
|
||||
{/* Scroll down indicator */}
|
||||
{pagination.scrollPosition.canScrollDown && <Box>
|
||||
<Text dimColor> {figures.arrowDown} more below</Text>
|
||||
</Box>}
|
||||
|
||||
{/* Error messages shown in the UI */}
|
||||
{error && <Box marginTop={1}>
|
||||
<Text color="error">
|
||||
{figures.cross} {error}
|
||||
</Text>
|
||||
</Box>}
|
||||
|
||||
<PluginSelectionKeyHint hasSelection={selectedForInstall.size > 0} />
|
||||
</Box>;
|
||||
}
|
||||
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJmaWd1cmVzIiwiUmVhY3QiLCJ1c2VFZmZlY3QiLCJ1c2VTdGF0ZSIsIkNvbmZpZ3VyYWJsZVNob3J0Y3V0SGludCIsIkJ5bGluZSIsIkJveCIsIlRleHQiLCJ1c2VLZXliaW5kaW5nIiwidXNlS2V5YmluZGluZ3MiLCJMb2FkZWRQbHVnaW4iLCJjb3VudCIsIm9wZW5Ccm93c2VyIiwibG9nRm9yRGVidWdnaW5nIiwiZXJyb3JNZXNzYWdlIiwiY2xlYXJBbGxDYWNoZXMiLCJmb3JtYXRJbnN0YWxsQ291bnQiLCJnZXRJbnN0YWxsQ291bnRzIiwiaXNQbHVnaW5HbG9iYWxseUluc3RhbGxlZCIsImlzUGx1Z2luSW5zdGFsbGVkIiwiY3JlYXRlUGx1Z2luSWQiLCJmb3JtYXRGYWlsdXJlRGV0YWlscyIsImZvcm1hdE1hcmtldHBsYWNlTG9hZGluZ0Vycm9ycyIsImdldE1hcmtldHBsYWNlU291cmNlRGlzcGxheSIsImxvYWRNYXJrZXRwbGFjZXNXaXRoR3JhY2VmdWxEZWdyYWRhdGlvbiIsImdldE1hcmtldHBsYWNlIiwibG9hZEtub3duTWFya2V0cGxhY2VzQ29uZmlnIiwiT0ZGSUNJQUxfTUFSS0VUUExBQ0VfTkFNRSIsImluc3RhbGxQbHVnaW5Gcm9tTWFya2V0cGxhY2UiLCJpc1BsdWdpbkJsb2NrZWRCeVBvbGljeSIsInBsdXJhbCIsInRydW5jYXRlVG9XaWR0aCIsImZpbmRQbHVnaW5PcHRpb25zVGFyZ2V0IiwiUGx1Z2luT3B0aW9uc0Zsb3ciLCJQbHVnaW5UcnVzdFdhcm5pbmciLCJidWlsZFBsdWdpbkRldGFpbHNNZW51T3B0aW9ucyIsImV4dHJhY3RHaXRIdWJSZXBvIiwiSW5zdGFsbGFibGVQbHVnaW4iLCJQbHVnaW5TZWxlY3Rpb25LZXlIaW50IiwiVmlld1N0YXRlIiwiUGFyZW50Vmlld1N0YXRlIiwidXNlUGFnaW5hdGlvbiIsIlByb3BzIiwiZXJyb3IiLCJzZXRFcnJvciIsInJlc3VsdCIsInNldFJlc3VsdCIsInNldFZpZXdTdGF0ZSIsInN0YXRlIiwib25JbnN0YWxsQ29tcGxldGUiLCJQcm9taXNlIiwidGFyZ2V0TWFya2V0cGxhY2UiLCJ0YXJnZXRQbHVnaW4iLCJ0eXBlIiwicGx1Z2luIiwicGx1Z2luSWQiLCJNYXJrZXRwbGFjZUluZm8iLCJuYW1lIiwidG90YWxQbHVnaW5zIiwiaW5zdGFsbGVkQ291bnQiLCJzb3VyY2UiLCJCcm93c2VNYXJrZXRwbGFjZSIsIl9yZXN1bHQiLCJzZXRQYXJlbnRWaWV3U3RhdGUiLCJSZWFjdE5vZGUiLCJ2aWV3U3RhdGUiLCJzZWxlY3RlZE1hcmtldHBsYWNlIiwic2V0U2VsZWN0ZWRNYXJrZXRwbGFjZSIsInNlbGVjdGVkUGx1Z2luIiwic2V0U2VsZWN0ZWRQbHVnaW4iLCJtYXJrZXRwbGFjZXMiLCJzZXRNYXJrZXRwbGFjZXMiLCJhdmFpbGFibGVQbHVnaW5zIiwic2V0QXZhaWxhYmxlUGx1Z2lucyIsImxvYWRpbmciLCJzZXRMb2FkaW5nIiwiaW5zdGFsbENvdW50cyIsInNldEluc3RhbGxDb3VudHMiLCJNYXAiLCJzZWxlY3RlZEluZGV4Iiwic2V0U2VsZWN0ZWRJbmRleCIsInNlbGVjdGVkRm9ySW5zdGFsbCIsInNldFNlbGVjdGVkRm9ySW5zdGFsbCIsIlNldCIsImluc3RhbGxpbmdQbHVnaW5zIiwic2V0SW5zdGFsbGluZ1BsdWdpbnMiLCJwYWdpbmF0aW9uIiwidG90YWxJdGVtcyIsImxlbmd0aCIsImRldGFpbHNNZW51SW5kZXgiLCJzZXREZXRhaWxzTWVudUluZGV4IiwiaXNJbnN0YWxsaW5nIiwic2V0SXNJbnN0YWxsaW5nIiwiaW5zdGFsbEVycm9yIiwic2V0SW5zdGFsbEVycm9yIiwid2FybmluZyIsInNldFdhcm5pbmciLCJoYW5kbGVCYWNrIiwidXNlQ2FsbGJhY2siLCJjb250ZXh0IiwibG9hZE1hcmtldHBsYWNlRGF0YSIsImNvbmZpZyIsImZhaWx1cmVzIiwibWFya2V0cGxhY2VJbmZvcyIsIm1hcmtldHBsYWNlQ29uZmlnIiwiZGF0YSIsIm1hcmtldHBsYWNlIiwiaW5zdGFsbGVkRnJvbVRoaXNNYXJrZXRwbGFjZSIsInBsdWdpbnMiLCJwdXNoIiwic29ydCIsImEiLCJiIiwic3VjY2Vzc0NvdW50IiwibSIsImVycm9yUmVzdWx0IiwibWVzc2FnZSIsIkVycm9yIiwic2luZ2xlTWFya2V0cGxhY2UiLCJmb3VuZFBsdWdpbiIsImZvdW5kTWFya2V0cGxhY2UiLCJPYmplY3QiLCJlbnRyaWVzIiwiZmluZCIsInAiLCJlbnRyeSIsIm1hcmtldHBsYWNlTmFtZSIsImlzSW5zdGFsbGVkIiwiZ2xvYmFsbHlJbnN0YWxsZWQiLCJtYXJrZXRwbGFjZUV4aXN0cyIsInNvbWUiLCJlcnIiLCJjYW5jZWxsZWQiLCJsb2FkUGx1Z2luc0Zvck1hcmtldHBsYWNlIiwiaW5zdGFsbGFibGVQbHVnaW5zIiwiY291bnRzIiwiY291bnRBIiwiZ2V0IiwiY291bnRCIiwibG9jYWxlQ29tcGFyZSIsImluc3RhbGxTZWxlY3RlZFBsdWdpbnMiLCJzaXplIiwicGx1Z2luc1RvSW5zdGFsbCIsImZpbHRlciIsImhhcyIsIm1hcCIsImZhaWx1cmVDb3VudCIsIm5ld0ZhaWxlZFBsdWdpbnMiLCJBcnJheSIsInJlYXNvbiIsInNjb3BlIiwic3VjY2VzcyIsImhhbmRsZVNpbmdsZVBsdWdpbkluc3RhbGwiLCJsb2FkZWQiLCJzZWxlY3Q6cHJldmlvdXMiLCJzZWxlY3Q6bmV4dCIsInNlbGVjdDphY2NlcHQiLCJpc0FjdGl2ZSIsImhhbmRsZVNlbGVjdGlvbkNoYW5nZSIsInBsdWdpbjp0b2dnbGUiLCJuZXdTZWxlY3Rpb24iLCJkZWxldGUiLCJhZGQiLCJwbHVnaW46aW5zdGFsbCIsImRldGFpbHNNZW51T3B0aW9ucyIsInVzZU1lbW8iLCJoYXNIb21lcGFnZSIsImhvbWVwYWdlIiwiZ2l0aHViUmVwbyIsImFjdGlvbiIsImZpbmlzaCIsIm1zZyIsIm91dGNvbWUiLCJkZXRhaWwiLCJpbmRleCIsInVuZGVmaW5lZCIsInBvaW50ZXIiLCJtZW51T3B0aW9ucyIsInZlcnNpb24iLCJkZXNjcmlwdGlvbiIsImF1dGhvciIsImNvbW1hbmRzIiwiaXNBcnJheSIsImpvaW4iLCJrZXlzIiwiYWdlbnRzIiwiaG9va3MiLCJtY3BTZXJ2ZXJzIiwib3B0aW9uIiwibGFiZWwiLCJ2aXNpYmxlUGx1Z2lucyIsImdldFZpc2libGVJdGVtcyIsInNjcm9sbFBvc2l0aW9uIiwiY2FuU2Nyb2xsVXAiLCJhcnJvd1VwIiwidmlzaWJsZUluZGV4IiwiYWN0dWFsSW5kZXgiLCJ0b0FjdHVhbEluZGV4IiwiaXNTZWxlY3RlZCIsImlzU2VsZWN0ZWRGb3JJbnN0YWxsIiwiaXNMYXN0IiwidGljayIsImVsbGlwc2lzIiwicmFkaW9PbiIsInJhZGlvT2ZmIiwiY2F0ZWdvcnkiLCJ0YWdzIiwiaW5jbHVkZXMiLCJjYW5TY3JvbGxEb3duIiwiYXJyb3dEb3duIiwiY3Jvc3MiXSwic291cmNlcyI6WyJCcm93c2VNYXJrZXRwbGFjZS50c3giXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IGZpZ3VyZXMgZnJvbSAnZmlndXJlcydcbmltcG9ydCAqIGFzIFJlYWN0IGZyb20gJ3JlYWN0J1xuaW1wb3J0IHsgdXNlRWZmZWN0LCB1c2VTdGF0ZSB9IGZyb20gJ3JlYWN0J1xuaW1wb3J0IHsgQ29uZmlndXJhYmxlU2hvcnRjdXRIaW50IH0gZnJvbSAnLi4vLi4vY29tcG9uZW50cy9Db25maWd1cmFibGVTaG9ydGN1dEhpbnQuanMnXG5pbXBvcnQgeyBCeWxpbmUgfSBmcm9tICcuLi8uLi9jb21wb25lbnRzL2Rlc2lnbi1zeXN0ZW0vQnlsaW5lLmpzJ1xuaW1wb3J0IHsgQm94LCBUZXh0IH0gZnJvbSAnLi4vLi4vaW5rLmpzJ1xuaW1wb3J0IHtcbiAgdXNlS2V5YmluZGluZyxcbiAgdXNlS2V5YmluZGluZ3MsXG59IGZyb20gJy4uLy4uL2tleWJpbmRpbmdzL3VzZUtleWJpbmRpbmcuanMnXG5pbXBvcnQgdHlwZSB7IExvYWRlZFBsdWdpbiB9IGZyb20gJy4uLy4uL3R5cGVzL3BsdWdpbi5qcydcbmltcG9ydCB7IGNvdW50IH0gZnJvbSAnLi4vLi4vdXRpbHMvYXJyYXkuanMnXG5pbXBvcnQgeyBvcGVuQnJvd3NlciB9IGZyb20gJy4uLy4uL3V0aWxzL2Jyb3dzZXIuanMnXG5pbXBvcnQgeyBsb2dGb3JEZWJ1Z2dpbmcgfSBmcm9
Line truncated
|
||||
@@ -0,0 +1,781 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import figures from 'figures';
|
||||
import * as React from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.js';
|
||||
import { Byline } from '../../components/design-system/Byline.js';
|
||||
import { SearchBox } from '../../components/SearchBox.js';
|
||||
import { useSearchInput } from '../../hooks/useSearchInput.js';
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
|
||||
// eslint-disable-next-line custom-rules/prefer-use-keybindings -- useInput needed for raw search mode text input
|
||||
import { Box, Text, useInput, useTerminalFocus } from '../../ink.js';
|
||||
import { useKeybinding, useKeybindings } from '../../keybindings/useKeybinding.js';
|
||||
import type { LoadedPlugin } from '../../types/plugin.js';
|
||||
import { count } from '../../utils/array.js';
|
||||
import { openBrowser } from '../../utils/browser.js';
|
||||
import { logForDebugging } from '../../utils/debug.js';
|
||||
import { errorMessage } from '../../utils/errors.js';
|
||||
import { clearAllCaches } from '../../utils/plugins/cacheUtils.js';
|
||||
import { formatInstallCount, getInstallCounts } from '../../utils/plugins/installCounts.js';
|
||||
import { isPluginGloballyInstalled } from '../../utils/plugins/installedPluginsManager.js';
|
||||
import { createPluginId, detectEmptyMarketplaceReason, type EmptyMarketplaceReason, formatFailureDetails, formatMarketplaceLoadingErrors, loadMarketplacesWithGracefulDegradation } from '../../utils/plugins/marketplaceHelpers.js';
|
||||
import { loadKnownMarketplacesConfig } from '../../utils/plugins/marketplaceManager.js';
|
||||
import { OFFICIAL_MARKETPLACE_NAME } from '../../utils/plugins/officialMarketplace.js';
|
||||
import { installPluginFromMarketplace } from '../../utils/plugins/pluginInstallationHelpers.js';
|
||||
import { isPluginBlockedByPolicy } from '../../utils/plugins/pluginPolicy.js';
|
||||
import { plural } from '../../utils/stringUtils.js';
|
||||
import { truncateToWidth } from '../../utils/truncate.js';
|
||||
import { findPluginOptionsTarget, PluginOptionsFlow } from './PluginOptionsFlow.js';
|
||||
import { PluginTrustWarning } from './PluginTrustWarning.js';
|
||||
import { buildPluginDetailsMenuOptions, extractGitHubRepo, type InstallablePlugin } from './pluginDetailsHelpers.js';
|
||||
import type { ViewState as ParentViewState } from './types.js';
|
||||
import { usePagination } from './usePagination.js';
|
||||
type Props = {
|
||||
error: string | null;
|
||||
setError: (error: string | null) => void;
|
||||
result: string | null;
|
||||
setResult: (result: string | null) => void;
|
||||
setViewState: (state: ParentViewState) => void;
|
||||
onInstallComplete?: () => void | Promise<void>;
|
||||
onSearchModeChange?: (isActive: boolean) => void;
|
||||
targetPlugin?: string;
|
||||
};
|
||||
type ViewState = 'plugin-list' | 'plugin-details' | {
|
||||
type: 'plugin-options';
|
||||
plugin: LoadedPlugin;
|
||||
pluginId: string;
|
||||
};
|
||||
export function DiscoverPlugins({
|
||||
error,
|
||||
setError,
|
||||
result: _result,
|
||||
setResult,
|
||||
setViewState: setParentViewState,
|
||||
onInstallComplete,
|
||||
onSearchModeChange,
|
||||
targetPlugin
|
||||
}: Props): React.ReactNode {
|
||||
// View state
|
||||
const [viewState, setViewState] = useState<ViewState>('plugin-list');
|
||||
const [selectedPlugin, setSelectedPlugin] = useState<InstallablePlugin | null>(null);
|
||||
|
||||
// Data state
|
||||
const [availablePlugins, setAvailablePlugins] = useState<InstallablePlugin[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [installCounts, setInstallCounts] = useState<Map<string, number> | null>(null);
|
||||
|
||||
// Search state
|
||||
const [isSearchMode, setIsSearchModeRaw] = useState(false);
|
||||
const setIsSearchMode = useCallback((active: boolean) => {
|
||||
setIsSearchModeRaw(active);
|
||||
onSearchModeChange?.(active);
|
||||
}, [onSearchModeChange]);
|
||||
const {
|
||||
query: searchQuery,
|
||||
setQuery: setSearchQuery,
|
||||
cursorOffset: searchCursorOffset
|
||||
} = useSearchInput({
|
||||
isActive: viewState === 'plugin-list' && isSearchMode && !loading,
|
||||
onExit: () => {
|
||||
setIsSearchMode(false);
|
||||
}
|
||||
});
|
||||
const isTerminalFocused = useTerminalFocus();
|
||||
const {
|
||||
columns: terminalWidth
|
||||
} = useTerminalSize();
|
||||
|
||||
// Filter plugins based on search query
|
||||
const filteredPlugins = useMemo(() => {
|
||||
if (!searchQuery) return availablePlugins;
|
||||
const lowerQuery = searchQuery.toLowerCase();
|
||||
return availablePlugins.filter(plugin => plugin.entry.name.toLowerCase().includes(lowerQuery) || plugin.entry.description?.toLowerCase().includes(lowerQuery) || plugin.marketplaceName.toLowerCase().includes(lowerQuery));
|
||||
}, [availablePlugins, searchQuery]);
|
||||
|
||||
// Selection state
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const [selectedForInstall, setSelectedForInstall] = useState<Set<string>>(new Set());
|
||||
const [installingPlugins, setInstallingPlugins] = useState<Set<string>>(new Set());
|
||||
|
||||
// Pagination for plugin list (continuous scrolling)
|
||||
const pagination = usePagination<InstallablePlugin>({
|
||||
totalItems: filteredPlugins.length,
|
||||
selectedIndex
|
||||
});
|
||||
|
||||
// Reset selection when search query changes
|
||||
useEffect(() => {
|
||||
setSelectedIndex(0);
|
||||
}, [searchQuery]);
|
||||
|
||||
// Details view state
|
||||
const [detailsMenuIndex, setDetailsMenuIndex] = useState(0);
|
||||
const [isInstalling, setIsInstalling] = useState(false);
|
||||
const [installError, setInstallError] = useState<string | null>(null);
|
||||
|
||||
// Warning state for non-critical errors
|
||||
const [warning, setWarning] = useState<string | null>(null);
|
||||
|
||||
// Empty state reason
|
||||
const [emptyReason, setEmptyReason] = useState<EmptyMarketplaceReason | null>(null);
|
||||
|
||||
// Load all plugins from all marketplaces
|
||||
useEffect(() => {
|
||||
async function loadAllPlugins() {
|
||||
try {
|
||||
const config = await loadKnownMarketplacesConfig();
|
||||
|
||||
// Load marketplaces with graceful degradation
|
||||
const {
|
||||
marketplaces,
|
||||
failures
|
||||
} = await loadMarketplacesWithGracefulDegradation(config);
|
||||
|
||||
// Collect all plugins from all marketplaces
|
||||
const allPlugins: InstallablePlugin[] = [];
|
||||
for (const {
|
||||
name,
|
||||
data: marketplace
|
||||
} of marketplaces) {
|
||||
if (marketplace) {
|
||||
for (const entry of marketplace.plugins) {
|
||||
const pluginId = createPluginId(entry.name, name);
|
||||
allPlugins.push({
|
||||
entry,
|
||||
marketplaceName: name,
|
||||
pluginId,
|
||||
// Only block when globally installed (user/managed scope).
|
||||
// Project/local-scope installs don't block — user may want to
|
||||
// promote to user scope so it's available everywhere (gh-29997).
|
||||
isInstalled: isPluginGloballyInstalled(pluginId)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out installed and policy-blocked plugins
|
||||
const uninstalledPlugins = allPlugins.filter(p => !p.isInstalled && !isPluginBlockedByPolicy(p.pluginId));
|
||||
|
||||
// Fetch install counts and sort by popularity
|
||||
try {
|
||||
const counts = await getInstallCounts();
|
||||
setInstallCounts(counts);
|
||||
if (counts) {
|
||||
// Sort by install count (descending), then alphabetically
|
||||
uninstalledPlugins.sort((a_0, b_0) => {
|
||||
const countA = counts.get(a_0.pluginId) ?? 0;
|
||||
const countB = counts.get(b_0.pluginId) ?? 0;
|
||||
if (countA !== countB) return countB - countA;
|
||||
return a_0.entry.name.localeCompare(b_0.entry.name);
|
||||
});
|
||||
} else {
|
||||
// No counts available - sort alphabetically
|
||||
uninstalledPlugins.sort((a_1, b_1) => a_1.entry.name.localeCompare(b_1.entry.name));
|
||||
}
|
||||
} catch (error_0) {
|
||||
// Log the error, then gracefully degrade to alphabetical sort
|
||||
logForDebugging(`Failed to fetch install counts: ${errorMessage(error_0)}`);
|
||||
uninstalledPlugins.sort((a, b) => a.entry.name.localeCompare(b.entry.name));
|
||||
}
|
||||
setAvailablePlugins(uninstalledPlugins);
|
||||
|
||||
// Detect empty reason if no plugins available
|
||||
const configuredCount = Object.keys(config).length;
|
||||
if (uninstalledPlugins.length === 0) {
|
||||
const reason = await detectEmptyMarketplaceReason({
|
||||
configuredMarketplaceCount: configuredCount,
|
||||
failedMarketplaceCount: failures.length
|
||||
});
|
||||
setEmptyReason(reason);
|
||||
}
|
||||
|
||||
// Handle marketplace loading errors/warnings
|
||||
const successCount = count(marketplaces, m => m.data !== null);
|
||||
const errorResult = formatMarketplaceLoadingErrors(failures, successCount);
|
||||
if (errorResult) {
|
||||
if (errorResult.type === 'warning') {
|
||||
setWarning(errorResult.message + '. Showing available plugins.');
|
||||
} else {
|
||||
throw new Error(errorResult.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle targetPlugin - navigate directly to plugin details
|
||||
// Search in allPlugins (before filtering) to handle installed plugins gracefully
|
||||
if (targetPlugin) {
|
||||
const foundPlugin = allPlugins.find(p_0 => p_0.entry.name === targetPlugin);
|
||||
if (foundPlugin) {
|
||||
if (foundPlugin.isInstalled) {
|
||||
setError(`Plugin '${foundPlugin.pluginId}' is already installed. Use '/plugin' to manage existing plugins.`);
|
||||
} else {
|
||||
setSelectedPlugin(foundPlugin);
|
||||
setViewState('plugin-details');
|
||||
}
|
||||
} else {
|
||||
setError(`Plugin "${targetPlugin}" not found in any marketplace`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to load plugins');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
void loadAllPlugins();
|
||||
}, [setError, targetPlugin]);
|
||||
|
||||
// Install selected plugins
|
||||
const installSelectedPlugins = async () => {
|
||||
if (selectedForInstall.size === 0) return;
|
||||
const pluginsToInstall = availablePlugins.filter(p_1 => selectedForInstall.has(p_1.pluginId));
|
||||
setInstallingPlugins(new Set(pluginsToInstall.map(p_2 => p_2.pluginId)));
|
||||
let successCount_0 = 0;
|
||||
let failureCount = 0;
|
||||
const newFailedPlugins: Array<{
|
||||
name: string;
|
||||
reason: string;
|
||||
}> = [];
|
||||
for (const plugin_0 of pluginsToInstall) {
|
||||
const result = await installPluginFromMarketplace({
|
||||
pluginId: plugin_0.pluginId,
|
||||
entry: plugin_0.entry,
|
||||
marketplaceName: plugin_0.marketplaceName,
|
||||
scope: 'user'
|
||||
});
|
||||
if (result.success) {
|
||||
successCount_0++;
|
||||
} else {
|
||||
failureCount++;
|
||||
newFailedPlugins.push({
|
||||
name: plugin_0.entry.name,
|
||||
reason: result.error
|
||||
});
|
||||
}
|
||||
}
|
||||
setInstallingPlugins(new Set());
|
||||
setSelectedForInstall(new Set());
|
||||
clearAllCaches();
|
||||
|
||||
// Handle installation results
|
||||
if (failureCount === 0) {
|
||||
const message = `✓ Installed ${successCount_0} ${plural(successCount_0, 'plugin')}. ` + `Run /reload-plugins to activate.`;
|
||||
setResult(message);
|
||||
} else if (successCount_0 === 0) {
|
||||
setError(`Failed to install: ${formatFailureDetails(newFailedPlugins, true)}`);
|
||||
} else {
|
||||
const message_0 = `✓ Installed ${successCount_0} of ${successCount_0 + failureCount} plugins. ` + `Failed: ${formatFailureDetails(newFailedPlugins, false)}. ` + `Run /reload-plugins to activate successfully installed plugins.`;
|
||||
setResult(message_0);
|
||||
}
|
||||
if (successCount_0 > 0) {
|
||||
if (onInstallComplete) {
|
||||
await onInstallComplete();
|
||||
}
|
||||
}
|
||||
setParentViewState({
|
||||
type: 'menu'
|
||||
});
|
||||
};
|
||||
|
||||
// Install single plugin from details view
|
||||
const handleSinglePluginInstall = async (plugin_1: InstallablePlugin, scope: 'user' | 'project' | 'local' = 'user') => {
|
||||
setIsInstalling(true);
|
||||
setInstallError(null);
|
||||
const result_0 = await installPluginFromMarketplace({
|
||||
pluginId: plugin_1.pluginId,
|
||||
entry: plugin_1.entry,
|
||||
marketplaceName: plugin_1.marketplaceName,
|
||||
scope
|
||||
});
|
||||
if (result_0.success) {
|
||||
const loaded = await findPluginOptionsTarget(plugin_1.pluginId);
|
||||
if (loaded) {
|
||||
setIsInstalling(false);
|
||||
setViewState({
|
||||
type: 'plugin-options',
|
||||
plugin: loaded,
|
||||
pluginId: plugin_1.pluginId
|
||||
});
|
||||
return;
|
||||
}
|
||||
setResult(result_0.message);
|
||||
if (onInstallComplete) {
|
||||
await onInstallComplete();
|
||||
}
|
||||
setParentViewState({
|
||||
type: 'menu'
|
||||
});
|
||||
} else {
|
||||
setIsInstalling(false);
|
||||
setInstallError(result_0.error);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle error state
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
setResult(error);
|
||||
}
|
||||
}, [error, setResult]);
|
||||
|
||||
// Escape in plugin-details view - go back to plugin-list
|
||||
useKeybinding('confirm:no', () => {
|
||||
setViewState('plugin-list');
|
||||
setSelectedPlugin(null);
|
||||
}, {
|
||||
context: 'Confirmation',
|
||||
isActive: viewState === 'plugin-details'
|
||||
});
|
||||
|
||||
// Escape in plugin-list view (not search mode) - exit to parent menu
|
||||
useKeybinding('confirm:no', () => {
|
||||
setParentViewState({
|
||||
type: 'menu'
|
||||
});
|
||||
}, {
|
||||
context: 'Confirmation',
|
||||
isActive: viewState === 'plugin-list' && !isSearchMode
|
||||
});
|
||||
|
||||
// Handle entering search mode (non-escape keys)
|
||||
useInput((input, _key) => {
|
||||
const keyIsNotCtrlOrMeta = !_key.ctrl && !_key.meta;
|
||||
if (!isSearchMode) {
|
||||
// Enter search mode with '/' or any printable character
|
||||
if (input === '/' && keyIsNotCtrlOrMeta) {
|
||||
setIsSearchMode(true);
|
||||
setSearchQuery('');
|
||||
} else if (keyIsNotCtrlOrMeta && input.length > 0 && !/^\s+$/.test(input) &&
|
||||
// Don't enter search mode for navigation keys
|
||||
input !== 'j' && input !== 'k' && input !== 'i') {
|
||||
setIsSearchMode(true);
|
||||
setSearchQuery(input);
|
||||
}
|
||||
}
|
||||
}, {
|
||||
isActive: viewState === 'plugin-list' && !loading
|
||||
});
|
||||
|
||||
// Plugin-list navigation (non-search mode)
|
||||
useKeybindings({
|
||||
'select:previous': () => {
|
||||
if (selectedIndex === 0) {
|
||||
setIsSearchMode(true);
|
||||
} else {
|
||||
pagination.handleSelectionChange(selectedIndex - 1, setSelectedIndex);
|
||||
}
|
||||
},
|
||||
'select:next': () => {
|
||||
if (selectedIndex < filteredPlugins.length - 1) {
|
||||
pagination.handleSelectionChange(selectedIndex + 1, setSelectedIndex);
|
||||
}
|
||||
},
|
||||
'select:accept': () => {
|
||||
if (selectedIndex === filteredPlugins.length && selectedForInstall.size > 0) {
|
||||
void installSelectedPlugins();
|
||||
} else if (selectedIndex < filteredPlugins.length) {
|
||||
const plugin_2 = filteredPlugins[selectedIndex];
|
||||
if (plugin_2) {
|
||||
if (plugin_2.isInstalled) {
|
||||
setParentViewState({
|
||||
type: 'manage-plugins',
|
||||
targetPlugin: plugin_2.entry.name,
|
||||
targetMarketplace: plugin_2.marketplaceName
|
||||
});
|
||||
} else {
|
||||
setSelectedPlugin(plugin_2);
|
||||
setViewState('plugin-details');
|
||||
setDetailsMenuIndex(0);
|
||||
setInstallError(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, {
|
||||
context: 'Select',
|
||||
isActive: viewState === 'plugin-list' && !isSearchMode
|
||||
});
|
||||
useKeybindings({
|
||||
'plugin:toggle': () => {
|
||||
if (selectedIndex < filteredPlugins.length) {
|
||||
const plugin_3 = filteredPlugins[selectedIndex];
|
||||
if (plugin_3 && !plugin_3.isInstalled) {
|
||||
const newSelection = new Set(selectedForInstall);
|
||||
if (newSelection.has(plugin_3.pluginId)) {
|
||||
newSelection.delete(plugin_3.pluginId);
|
||||
} else {
|
||||
newSelection.add(plugin_3.pluginId);
|
||||
}
|
||||
setSelectedForInstall(newSelection);
|
||||
}
|
||||
}
|
||||
},
|
||||
'plugin:install': () => {
|
||||
if (selectedForInstall.size > 0) {
|
||||
void installSelectedPlugins();
|
||||
}
|
||||
}
|
||||
}, {
|
||||
context: 'Plugin',
|
||||
isActive: viewState === 'plugin-list' && !isSearchMode
|
||||
});
|
||||
|
||||
// Plugin-details navigation
|
||||
const detailsMenuOptions = React.useMemo(() => {
|
||||
if (!selectedPlugin) return [];
|
||||
const hasHomepage = selectedPlugin.entry.homepage;
|
||||
const githubRepo = extractGitHubRepo(selectedPlugin);
|
||||
return buildPluginDetailsMenuOptions(hasHomepage, githubRepo);
|
||||
}, [selectedPlugin]);
|
||||
useKeybindings({
|
||||
'select:previous': () => {
|
||||
if (detailsMenuIndex > 0) {
|
||||
setDetailsMenuIndex(detailsMenuIndex - 1);
|
||||
}
|
||||
},
|
||||
'select:next': () => {
|
||||
if (detailsMenuIndex < detailsMenuOptions.length - 1) {
|
||||
setDetailsMenuIndex(detailsMenuIndex + 1);
|
||||
}
|
||||
},
|
||||
'select:accept': () => {
|
||||
if (!selectedPlugin) return;
|
||||
const action = detailsMenuOptions[detailsMenuIndex]?.action;
|
||||
const hasHomepage_0 = selectedPlugin.entry.homepage;
|
||||
const githubRepo_0 = extractGitHubRepo(selectedPlugin);
|
||||
if (action === 'install-user') {
|
||||
void handleSinglePluginInstall(selectedPlugin, 'user');
|
||||
} else if (action === 'install-project') {
|
||||
void handleSinglePluginInstall(selectedPlugin, 'project');
|
||||
} else if (action === 'install-local') {
|
||||
void handleSinglePluginInstall(selectedPlugin, 'local');
|
||||
} else if (action === 'homepage' && hasHomepage_0) {
|
||||
void openBrowser(hasHomepage_0);
|
||||
} else if (action === 'github' && githubRepo_0) {
|
||||
void openBrowser(`https://github.com/${githubRepo_0}`);
|
||||
} else if (action === 'back') {
|
||||
setViewState('plugin-list');
|
||||
setSelectedPlugin(null);
|
||||
}
|
||||
}
|
||||
}, {
|
||||
context: 'Select',
|
||||
isActive: viewState === 'plugin-details' && !!selectedPlugin
|
||||
});
|
||||
if (typeof viewState === 'object' && viewState.type === 'plugin-options') {
|
||||
const {
|
||||
plugin: plugin_4,
|
||||
pluginId: pluginId_0
|
||||
} = viewState;
|
||||
function finish(msg: string): void {
|
||||
setResult(msg);
|
||||
if (onInstallComplete) {
|
||||
void onInstallComplete();
|
||||
}
|
||||
setParentViewState({
|
||||
type: 'menu'
|
||||
});
|
||||
}
|
||||
return <PluginOptionsFlow plugin={plugin_4} pluginId={pluginId_0} onDone={(outcome, detail) => {
|
||||
switch (outcome) {
|
||||
case 'configured':
|
||||
finish(`✓ Installed and configured ${plugin_4.name}. Run /reload-plugins to apply.`);
|
||||
break;
|
||||
case 'skipped':
|
||||
finish(`✓ Installed ${plugin_4.name}. Run /reload-plugins to apply.`);
|
||||
break;
|
||||
case 'error':
|
||||
finish(`Installed but failed to save config: ${detail}`);
|
||||
break;
|
||||
}
|
||||
}} />;
|
||||
}
|
||||
|
||||
// Loading state
|
||||
if (loading) {
|
||||
return <Text>Loading…</Text>;
|
||||
}
|
||||
|
||||
// Error state
|
||||
if (error) {
|
||||
return <Text color="error">{error}</Text>;
|
||||
}
|
||||
|
||||
// Plugin details view
|
||||
if (viewState === 'plugin-details' && selectedPlugin) {
|
||||
const hasHomepage_1 = selectedPlugin.entry.homepage;
|
||||
const githubRepo_1 = extractGitHubRepo(selectedPlugin);
|
||||
const menuOptions = buildPluginDetailsMenuOptions(hasHomepage_1, githubRepo_1);
|
||||
return <Box flexDirection="column">
|
||||
<Box marginBottom={1}>
|
||||
<Text bold>Plugin details</Text>
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text bold>{selectedPlugin.entry.name}</Text>
|
||||
<Text dimColor>from {selectedPlugin.marketplaceName}</Text>
|
||||
{selectedPlugin.entry.version && <Text dimColor>Version: {selectedPlugin.entry.version}</Text>}
|
||||
{selectedPlugin.entry.description && <Box marginTop={1}>
|
||||
<Text>{selectedPlugin.entry.description}</Text>
|
||||
</Box>}
|
||||
{selectedPlugin.entry.author && <Box marginTop={1}>
|
||||
<Text dimColor>
|
||||
By:{' '}
|
||||
{typeof selectedPlugin.entry.author === 'string' ? selectedPlugin.entry.author : selectedPlugin.entry.author.name}
|
||||
</Text>
|
||||
</Box>}
|
||||
</Box>
|
||||
|
||||
<PluginTrustWarning />
|
||||
|
||||
{installError && <Box marginBottom={1}>
|
||||
<Text color="error">Error: {installError}</Text>
|
||||
</Box>}
|
||||
|
||||
<Box flexDirection="column">
|
||||
{menuOptions.map((option, index) => <Box key={option.action}>
|
||||
{detailsMenuIndex === index && <Text>{'> '}</Text>}
|
||||
{detailsMenuIndex !== index && <Text>{' '}</Text>}
|
||||
<Text bold={detailsMenuIndex === index}>
|
||||
{isInstalling && option.action.startsWith('install-') ? 'Installing…' : option.label}
|
||||
</Text>
|
||||
</Box>)}
|
||||
</Box>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>
|
||||
<Byline>
|
||||
<ConfigurableShortcutHint action="select:accept" context="Select" fallback="Enter" description="select" />
|
||||
<ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="back" />
|
||||
</Byline>
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>;
|
||||
}
|
||||
|
||||
// Empty state
|
||||
if (availablePlugins.length === 0) {
|
||||
return <Box flexDirection="column">
|
||||
<Box marginBottom={1}>
|
||||
<Text bold>Discover plugins</Text>
|
||||
</Box>
|
||||
<EmptyStateMessage reason={emptyReason} />
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor italic>
|
||||
Esc to go back
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>;
|
||||
}
|
||||
|
||||
// Get visible plugins from pagination
|
||||
const visiblePlugins = pagination.getVisibleItems(filteredPlugins);
|
||||
return <Box flexDirection="column">
|
||||
<Box>
|
||||
<Text bold>Discover plugins</Text>
|
||||
{pagination.needsPagination && <Text dimColor>
|
||||
{' '}
|
||||
({pagination.scrollPosition.current}/
|
||||
{pagination.scrollPosition.total})
|
||||
</Text>}
|
||||
</Box>
|
||||
|
||||
{/* Search box */}
|
||||
<Box marginBottom={1}>
|
||||
<SearchBox query={searchQuery} isFocused={isSearchMode} isTerminalFocused={isTerminalFocused} width={terminalWidth - 4} cursorOffset={searchCursorOffset} />
|
||||
</Box>
|
||||
|
||||
{/* Warning banner */}
|
||||
{warning && <Box marginBottom={1}>
|
||||
<Text color="warning">
|
||||
{figures.warning} {warning}
|
||||
</Text>
|
||||
</Box>}
|
||||
|
||||
{/* No search results */}
|
||||
{filteredPlugins.length === 0 && searchQuery && <Box marginBottom={1}>
|
||||
<Text dimColor>No plugins match "{searchQuery}"</Text>
|
||||
</Box>}
|
||||
|
||||
{/* Scroll up indicator */}
|
||||
{pagination.scrollPosition.canScrollUp && <Box>
|
||||
<Text dimColor> {figures.arrowUp} more above</Text>
|
||||
</Box>}
|
||||
|
||||
{/* Plugin list - use startIndex in key to force re-render on scroll */}
|
||||
{visiblePlugins.map((plugin_5, visibleIndex) => {
|
||||
const actualIndex = pagination.toActualIndex(visibleIndex);
|
||||
const isSelected = selectedIndex === actualIndex;
|
||||
const isSelectedForInstall = selectedForInstall.has(plugin_5.pluginId);
|
||||
const isInstallingThis = installingPlugins.has(plugin_5.pluginId);
|
||||
const isLast = visibleIndex === visiblePlugins.length - 1;
|
||||
return <Box key={`${pagination.startIndex}-${plugin_5.pluginId}`} flexDirection="column" marginBottom={isLast && !error ? 0 : 1}>
|
||||
<Box>
|
||||
<Text color={isSelected && !isSearchMode ? 'suggestion' : undefined}>
|
||||
{isSelected && !isSearchMode ? figures.pointer : ' '}{' '}
|
||||
</Text>
|
||||
<Text>
|
||||
{isInstallingThis ? figures.ellipsis : isSelectedForInstall ? figures.radioOn : figures.radioOff}{' '}
|
||||
{plugin_5.entry.name}
|
||||
<Text dimColor> · {plugin_5.marketplaceName}</Text>
|
||||
{plugin_5.entry.tags?.includes('community-managed') && <Text dimColor> [Community Managed]</Text>}
|
||||
{installCounts && plugin_5.marketplaceName === OFFICIAL_MARKETPLACE_NAME && <Text dimColor>
|
||||
{' · '}
|
||||
{formatInstallCount(installCounts.get(plugin_5.pluginId) ?? 0)}{' '}
|
||||
installs
|
||||
</Text>}
|
||||
</Text>
|
||||
</Box>
|
||||
{plugin_5.entry.description && <Box marginLeft={4}>
|
||||
<Text dimColor>
|
||||
{truncateToWidth(plugin_5.entry.description, 60)}
|
||||
</Text>
|
||||
</Box>}
|
||||
</Box>;
|
||||
})}
|
||||
|
||||
{/* Scroll down indicator */}
|
||||
{pagination.scrollPosition.canScrollDown && <Box>
|
||||
<Text dimColor> {figures.arrowDown} more below</Text>
|
||||
</Box>}
|
||||
|
||||
{/* Error messages */}
|
||||
{error && <Box marginTop={1}>
|
||||
<Text color="error">
|
||||
{figures.cross} {error}
|
||||
</Text>
|
||||
</Box>}
|
||||
|
||||
<DiscoverPluginsKeyHint hasSelection={selectedForInstall.size > 0} canToggle={selectedIndex < filteredPlugins.length && !filteredPlugins[selectedIndex]?.isInstalled} />
|
||||
</Box>;
|
||||
}
|
||||
function DiscoverPluginsKeyHint(t0) {
|
||||
const $ = _c(10);
|
||||
const {
|
||||
hasSelection,
|
||||
canToggle
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] !== hasSelection) {
|
||||
t1 = hasSelection && <ConfigurableShortcutHint action="plugin:install" context="Plugin" fallback="i" description="install" bold={true} />;
|
||||
$[0] = hasSelection;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
let t2;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <Text>type to search</Text>;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
let t3;
|
||||
if ($[3] !== canToggle) {
|
||||
t3 = canToggle && <ConfigurableShortcutHint action="plugin:toggle" context="Plugin" fallback="Space" description="toggle" />;
|
||||
$[3] = canToggle;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
let t4;
|
||||
let t5;
|
||||
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = <ConfigurableShortcutHint action="select:accept" context="Select" fallback="Enter" description="details" />;
|
||||
t5 = <ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="back" />;
|
||||
$[5] = t4;
|
||||
$[6] = t5;
|
||||
} else {
|
||||
t4 = $[5];
|
||||
t5 = $[6];
|
||||
}
|
||||
let t6;
|
||||
if ($[7] !== t1 || $[8] !== t3) {
|
||||
t6 = <Box marginTop={1}><Text dimColor={true} italic={true}><Byline>{t1}{t2}{t3}{t4}{t5}</Byline></Text></Box>;
|
||||
$[7] = t1;
|
||||
$[8] = t3;
|
||||
$[9] = t6;
|
||||
} else {
|
||||
t6 = $[9];
|
||||
}
|
||||
return t6;
|
||||
}
|
||||
|
||||
/**
|
||||
* Context-aware empty state message for the Discover screen
|
||||
*/
|
||||
function EmptyStateMessage(t0) {
|
||||
const $ = _c(6);
|
||||
const {
|
||||
reason
|
||||
} = t0;
|
||||
switch (reason) {
|
||||
case "git-not-installed":
|
||||
{
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <><Text dimColor={true}>Git is required to install marketplaces.</Text><Text dimColor={true}>Please install git and restart Claude Code.</Text></>;
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
case "all-blocked-by-policy":
|
||||
{
|
||||
let t1;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <><Text dimColor={true}>Your organization policy does not allow any external marketplaces.</Text><Text dimColor={true}>Contact your administrator.</Text></>;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
case "policy-restricts-sources":
|
||||
{
|
||||
let t1;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <><Text dimColor={true}>Your organization restricts which marketplaces can be added.</Text><Text dimColor={true}>Switch to the Marketplaces tab to view allowed sources.</Text></>;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
case "all-marketplaces-failed":
|
||||
{
|
||||
let t1;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <><Text dimColor={true}>Failed to load marketplace data.</Text><Text dimColor={true}>Check your network connection.</Text></>;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
case "all-plugins-installed":
|
||||
{
|
||||
let t1;
|
||||
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <><Text dimColor={true}>All available plugins are already installed.</Text><Text dimColor={true}>Check for new plugins later or add more marketplaces.</Text></>;
|
||||
$[4] = t1;
|
||||
} else {
|
||||
t1 = $[4];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
case "no-marketplaces-configured":
|
||||
default:
|
||||
{
|
||||
let t1;
|
||||
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <><Text dimColor={true}>No plugins available.</Text><Text dimColor={true}>Add a marketplace first using the Marketplaces tab.</Text></>;
|
||||
$[5] = t1;
|
||||
} else {
|
||||
t1 = $[5];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJmaWd1cmVzIiwiUmVhY3QiLCJ1c2VDYWxsYmFjayIsInVzZUVmZmVjdCIsInVzZU1lbW8iLCJ1c2VTdGF0ZSIsIkNvbmZpZ3VyYWJsZVNob3J0Y3V0SGludCIsIkJ5bGluZSIsIlNlYXJjaEJveCIsInVzZVNlYXJjaElucHV0IiwidXNlVGVybWluYWxTaXplIiwiQm94IiwiVGV4dCIsInVzZUlucHV0IiwidXNlVGVybWluYWxGb2N1cyIsInVzZUtleWJpbmRpbmciLCJ1c2VLZXliaW5kaW5ncyIsIkxvYWRlZFBsdWdpbiIsImNvdW50Iiwib3BlbkJyb3dzZXIiLCJsb2dGb3JEZWJ1Z2dpbmciLCJlcnJvck1lc3NhZ2UiLCJjbGVhckFsbENhY2hlcyIsImZvcm1hdEluc3RhbGxDb3VudCIsImdldEluc3RhbGxDb3VudHMiLCJpc1BsdWdpbkdsb2JhbGx5SW5zdGFsbGVkIiwiY3JlYXRlUGx1Z2luSWQiLCJkZXRlY3RFbXB0eU1hcmtldHBsYWNlUmVhc29uIiwiRW1wdHlNYXJrZXRwbGFjZVJlYXNvbiIsImZvcm1hdEZhaWx1cmVEZXRhaWxzIiwiZm9ybWF0TWFya2V0cGxhY2VMb2FkaW5nRXJyb3JzIiwibG9hZE1hcmtldHBsYWNlc1dpdGhHcmFjZWZ1bERlZ3JhZGF0aW9uIiwibG9hZEtub3duTWFya2V0cGxhY2VzQ29uZmlnIiwiT0ZGSUNJQUxfTUFSS0VUUExBQ0VfTkFNRSIsImluc3RhbGxQbHVnaW5Gcm9tTWFya2V0cGxhY2UiLCJpc1BsdWdpbkJsb2NrZWRCeVBvbGljeSIsInBsdXJhbCIsInRydW5jYXRlVG9XaWR0aCIsImZpbmRQbHVnaW5PcHRpb25zVGFyZ2V0IiwiUGx1Z2luT3B0aW9uc0Zsb3ciLCJQbHVnaW5UcnVzdFdhcm5pbmciLCJidWlsZFBsdWdpbkRldGFpbHNNZW51T3B0aW9ucyIsImV4dHJhY3RHaXRIdWJSZXBvIiwiSW5zdGFsbGFibGVQbHVnaW4iLCJWaWV3U3RhdGUiLCJQYXJlbnRWaWV3U3RhdGUiLCJ1c2VQYWdpbmF0aW9uIiwiUHJvcHMiLCJlcnJvciIsInNldEVycm9yIiwicmVzdWx0Iiwic2V0UmVzdWx0Iiwic2V0Vmlld1N0YXRlIiwic3RhdGUiLCJvbkluc3RhbGxDb21wbGV0ZSIsIlByb21pc2UiLCJvblNlYXJjaE1vZGVDaGFuZ2UiLCJpc0FjdGl2ZSIsInRhcmdldFBsdWdpbiIsInR5cGUiLCJwbHVnaW4iLCJwbHVnaW5JZCIsIkRpc2NvdmVyUGx1Z2lucyIsIl9yZXN1bHQiLCJzZXRQYXJlbnRWaWV3U3RhdGUiLCJSZWFjdE5vZGUiLCJ2aWV3U3RhdGUiLCJzZWxlY3RlZFBsdWdpbiIsInNldFNlbGVjdGVkUGx1Z2luIiwiYXZhaWxhYmxlUGx1Z2lucyIsInNldEF2YWlsYWJsZVBsdWdpbnMiLCJsb2FkaW5nIiwic2V0TG9hZGluZyIsImluc3RhbGxDb3VudHMiLCJzZXRJbnN0YWxsQ291bnRzIiwiTWFwIiwiaXNTZWFyY2hNb2RlIiwic2V0SXNTZWFyY2hNb2RlUmF3Iiwic2V0SXNTZWFyY2hNb2RlIiwiYWN0aXZlIiwicXVlcnkiLCJzZWFyY2hRdWVyeSIsInNldFF1ZXJ5Iiwic2V0U2VhcmNoUXVlcnkiLCJjdXJzb3JPZmZzZXQiLCJzZWFyY2hDdXJzb3JPZmZzZXQiLCJvbkV4aXQiLCJpc1Rlcm1pbmFsRm9jdXNlZCIsImNvbHVtbnMiLCJ0ZXJtaW5hbFdpZHRoIiwiZmlsdGVyZWRQbHVnaW5zIiwibG93ZXJRdWVyeSIsInRvTG93ZXJDYXNlIiwiZmlsdGVyIiwiZW50cnkiLCJuYW1lIiwiaW5jbHVkZXMiLCJkZXNjcmlwdGlvbiIsIm1hcmtldHBsYWNlTmFtZSIsInNlbGVjdGVkSW5kZXgiLCJzZXRTZWxlY3RlZEluZGV4Iiwic2VsZWN0ZWRGb3JJbnN0YWxsIiwic2V0U2VsZWN0ZWRGb3JJbnN0YWxsIiwiU2V0IiwiaW5zdGFsbGluZ1BsdWdpbnMiLCJzZXRJbnN0YWxsaW5nUGx1Z2lucyIsInBhZ2luYXRpb24iLCJ0b3RhbEl0ZW1zIiwibGVuZ3RoIiwiZGV0YWlsc01lbnVJbmRleCIsInNldERldGFpbHNNZW51SW5kZXgiLCJpc0luc3RhbGxpbmciLCJzZXRJc0luc3RhbGxpbmciLCJpbnN0YWxsRXJyb3IiLCJzZXRJbnN0YWxsRXJyb3IiLCJ3YXJuaW5nIiwic2V0V2FybmluZyIsImVtcHR5UmVhc29uIiwic2V0RW1wdHlSZWFzb24iLCJsb2FkQWxsUGx1Z2lucyIsImNvbmZpZyIsIm1hcmtldHBsYWNlcyIsImZhaWx1cmVzIiwiYWxsUGx1Z2lucyIsImRhdGEiLCJtYXJrZXRwbGFjZSIsInBsdWdpbnMiLCJwdXNoIiwiaXNJbnN0YWxsZWQiLCJ1bmluc3RhbGxlZFBsdWdpbnMiLCJwIiwiY291bnRzIiwic29ydCIsImEiLCJiIiwiY291bnRBIiwiZ2V0IiwiY291bnRCIiwibG9jYWxlQ29tcGFyZSIsImNvbmZpZ3VyZWRDb3VudCIsIk9iamVjdCIsImtleXMiLCJyZWFzb24iLCJjb25maWd1cmVkTWFya2V0cGxhY2VDb3VudCIsImZhaWxlZE1hcmtldHBsYWNlQ291bnQiLCJzdWNjZXNzQ291bnQiLCJtIiwiZXJyb3JSZXN1bHQiLCJtZXNzYWdlIiwiRXJyb3IiLCJmb3VuZFBsdWdpbiIsImZpbmQiLCJlcnIiLCJpbnN0YWxsU2VsZWN0ZWRQbHVnaW5zIiwic2l6ZSIsInBsdWdpbnNUb0luc3RhbGwiLCJoYXMiLCJtYXAiLCJmYWlsdXJlQ291bnQiLCJuZXdGYWlsZWRQbHVnaW5zIiwiQXJyYXkiLCJzY29wZSIsInN1Y2Nlc3MiLCJoYW5kbGVTaW5nbGVQbHVnaW5JbnN0YWxsIiwibG9hZGVkIiwiY29udGV4dCIsImlucHV0IiwiX2tleSIsImtleUlzTm90Q3RybE9yTWV0YSIsImN0cmwiLCJtZXRhIiwidGVzdCIsInNlbGVjdDpwcmV2aW91cyIsImhhbmRsZVNlbGVjdGlvbkNoYW5nZSIsInNlbGVjdDpuZXh0Iiwic2VsZWN0OmFjY2VwdCIsInRhcmdldE1hcmtldHBsYWNlIiwicGx1Z2luOnRvZ2dsZSIsIm5ld1NlbGVjdGlvbiIsImRlbGV0ZSIsImFkZCIsInBsdWdpbjppbnN0YWxsIiwiZGV0YWlsc01lbnVPcHRpb25zIiwiaGFzSG9tZXBhZ2UiLCJob21lcGFnZSIsImdpdGh1YlJlcG8iLCJhY3Rpb24iLCJmaW5pc2giLCJtc2ciLCJvdXRjb21lIiwiZGV0YWlsIiwibWVudU9wdGlvbnMiLCJ2ZXJzaW9uIiwiYXV0aG9yIiwib3B0aW9uIiwiaW5kZXgiLCJzdGFydHNXaXRoIiwibGFiZWwiLCJ2aXNpYmxlUGx1Z2lucyIsImdldFZpc2libGVJdGVtcyIsIm5lZWRzUGFnaW5hdGlvbiIsInNjcm9sbFBvc2l0aW9uIiwiY3VycmVudCIsInRvdGFsIiwiY2FuU2Nyb2xsVXAiLCJhcnJvd1VwIiwidmlzaWJsZUluZGV4IiwiYWN0dWFsSW5kZXgiLCJ0b0FjdHVhbEluZGV4IiwiaXNTZWxlY3RlZCIsImlzU2VsZWN0ZWRGb3JJbnN0YWxsIiwiaXNJbnN0YWxsaW5nVGhpcyIsImlzTGFzdCIsInN0YXJ0SW5kZXgiLCJ1bmRlZmluZWQiLCJwb2ludGVyIiwiZWxsaXBzaXMiLCJyYWRpb09uIiwicmFkaW9PZmYiLCJ0YWdzIiwiY2FuU2Nyb2xsRG93biIsImFycm93RG93biIsImNyb3NzIiwiRGlzY292ZXJQbHVnaW5zS2V5SGludCIsInQwIiwiJCIsIl9jIiwiaGFzU2VsZWN0aW9uIiwiY2FuVG9nZ2xlIiwidDEiLCJ0MiIsIlN5bWJvbCIsImZvciIsInQzIiwidDQiLCJ0NSIsInQ2IiwiRW1wdHlTdGF0ZU1lc3NhZ2UiXSwic291cmNlcyI6WyJEaXNjb3ZlclBsdWdpbnMudHN4Il0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCBmaWd1cmVzIGZyb20gJ2ZpZ3VyZXMnXG5pbXBvcnQgKiBhcyBSZWFjdCBmcm9tICdyZWFjdCdcbmltcG9ydCB7IHVzZUNhbGxiYWNrLCB1c2VFZmZlY3QsIHVzZU1lbW8sIHVzZVN0YXRlIH0gZnJvbSAncmVhY3QnXG5pbXBvcnQgeyBDb25maWd1cmFibGVTaG9ydGN1dEhpbnQgfSBmcm9tICcuLi8uLi9jb21wb25lbnRzL0NvbmZpZ3VyYWJsZVNob3J0Y3V0SGludC5qcydcbmltcG9ydCB7IEJ5bGluZSB9IGZyb20gJy4uLy4uL2NvbXBvbmVudHMvZGVzaWduLXN5c3RlbS9CeWxpbmUuanMnXG5pbXBvcnQgeyBTZWFyY2hCb3ggfSBmcm9tICcuLi8uLi9
Line truncated
|
||||
@@ -0,0 +1,838 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import figures from 'figures';
|
||||
import * as React from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js';
|
||||
import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.js';
|
||||
import { Byline } from '../../components/design-system/Byline.js';
|
||||
import { KeyboardShortcutHint } from '../../components/design-system/KeyboardShortcutHint.js';
|
||||
// eslint-disable-next-line custom-rules/prefer-use-keybindings -- useInput needed for marketplace-specific u/r shortcuts and y/n confirmation not in keybinding schema
|
||||
import { Box, Text, useInput } from '../../ink.js';
|
||||
import { useKeybinding, useKeybindings } from '../../keybindings/useKeybinding.js';
|
||||
import type { LoadedPlugin } from '../../types/plugin.js';
|
||||
import { count } from '../../utils/array.js';
|
||||
import { shouldSkipPluginAutoupdate } from '../../utils/config.js';
|
||||
import { errorMessage } from '../../utils/errors.js';
|
||||
import { clearAllCaches } from '../../utils/plugins/cacheUtils.js';
|
||||
import { createPluginId, formatMarketplaceLoadingErrors, getMarketplaceSourceDisplay, loadMarketplacesWithGracefulDegradation } from '../../utils/plugins/marketplaceHelpers.js';
|
||||
import { loadKnownMarketplacesConfig, refreshMarketplace, removeMarketplaceSource, setMarketplaceAutoUpdate } from '../../utils/plugins/marketplaceManager.js';
|
||||
import { updatePluginsForMarketplaces } from '../../utils/plugins/pluginAutoupdate.js';
|
||||
import { loadAllPlugins } from '../../utils/plugins/pluginLoader.js';
|
||||
import { isMarketplaceAutoUpdate } from '../../utils/plugins/schemas.js';
|
||||
import { getSettingsForSource, updateSettingsForSource } from '../../utils/settings/settings.js';
|
||||
import { plural } from '../../utils/stringUtils.js';
|
||||
import type { ViewState } from './types.js';
|
||||
type Props = {
|
||||
setViewState: (state: ViewState) => void;
|
||||
error?: string | null;
|
||||
setError?: (error: string | null) => void;
|
||||
setResult: (result: string | null) => void;
|
||||
exitState: {
|
||||
pending: boolean;
|
||||
keyName: 'Ctrl-C' | 'Ctrl-D' | null;
|
||||
};
|
||||
onManageComplete?: () => void | Promise<void>;
|
||||
targetMarketplace?: string;
|
||||
action?: 'update' | 'remove';
|
||||
};
|
||||
type MarketplaceState = {
|
||||
name: string;
|
||||
source: string;
|
||||
lastUpdated?: string;
|
||||
pluginCount?: number;
|
||||
installedPlugins?: LoadedPlugin[];
|
||||
pendingUpdate?: boolean;
|
||||
pendingRemove?: boolean;
|
||||
autoUpdate?: boolean;
|
||||
};
|
||||
type InternalViewState = 'list' | 'details' | 'confirm-remove';
|
||||
export function ManageMarketplaces({
|
||||
setViewState,
|
||||
error,
|
||||
setError,
|
||||
setResult,
|
||||
exitState,
|
||||
onManageComplete,
|
||||
targetMarketplace,
|
||||
action
|
||||
}: Props): React.ReactNode {
|
||||
const [marketplaceStates, setMarketplaceStates] = useState<MarketplaceState[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [processError, setProcessError] = useState<string | null>(null);
|
||||
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||||
const [progressMessage, setProgressMessage] = useState<string | null>(null);
|
||||
const [internalView, setInternalView] = useState<InternalViewState>('list');
|
||||
const [selectedMarketplace, setSelectedMarketplace] = useState<MarketplaceState | null>(null);
|
||||
const [detailsMenuIndex, setDetailsMenuIndex] = useState(0);
|
||||
const hasAttemptedAutoAction = useRef(false);
|
||||
|
||||
// Load marketplaces and their installed plugins
|
||||
useEffect(() => {
|
||||
async function loadMarketplaces() {
|
||||
try {
|
||||
const config = await loadKnownMarketplacesConfig();
|
||||
const {
|
||||
enabled,
|
||||
disabled
|
||||
} = await loadAllPlugins();
|
||||
const allPlugins = [...enabled, ...disabled];
|
||||
|
||||
// Load marketplaces with graceful degradation
|
||||
const {
|
||||
marketplaces,
|
||||
failures
|
||||
} = await loadMarketplacesWithGracefulDegradation(config);
|
||||
const states: MarketplaceState[] = [];
|
||||
for (const {
|
||||
name,
|
||||
config: entry,
|
||||
data: marketplace
|
||||
} of marketplaces) {
|
||||
// Get all plugins installed from this marketplace
|
||||
const installedFromMarketplace = allPlugins.filter(plugin => plugin.source.endsWith(`@${name}`));
|
||||
states.push({
|
||||
name,
|
||||
source: getMarketplaceSourceDisplay(entry.source),
|
||||
lastUpdated: entry.lastUpdated,
|
||||
pluginCount: marketplace?.plugins.length,
|
||||
installedPlugins: installedFromMarketplace,
|
||||
pendingUpdate: false,
|
||||
pendingRemove: false,
|
||||
autoUpdate: isMarketplaceAutoUpdate(name, entry)
|
||||
});
|
||||
}
|
||||
|
||||
// Sort: claude-plugin-directory first, then alphabetically
|
||||
states.sort((a, b) => {
|
||||
if (a.name === 'claude-plugin-directory') return -1;
|
||||
if (b.name === 'claude-plugin-directory') return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
setMarketplaceStates(states);
|
||||
|
||||
// Handle marketplace loading errors/warnings
|
||||
const successCount = count(marketplaces, m => m.data !== null);
|
||||
const errorResult = formatMarketplaceLoadingErrors(failures, successCount);
|
||||
if (errorResult) {
|
||||
if (errorResult.type === 'warning') {
|
||||
setProcessError(errorResult.message);
|
||||
} else {
|
||||
throw new Error(errorResult.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-execute if target and action provided
|
||||
if (targetMarketplace && !hasAttemptedAutoAction.current && !error) {
|
||||
hasAttemptedAutoAction.current = true;
|
||||
const targetIndex = states.findIndex(s => s.name === targetMarketplace);
|
||||
if (targetIndex >= 0) {
|
||||
const targetState = states[targetIndex];
|
||||
if (action) {
|
||||
// Mark the action as pending and execute
|
||||
setSelectedIndex(targetIndex + 1); // +1 because "Add Marketplace" is at index 0
|
||||
const newStates = [...states];
|
||||
if (action === 'update') {
|
||||
newStates[targetIndex]!.pendingUpdate = true;
|
||||
} else if (action === 'remove') {
|
||||
newStates[targetIndex]!.pendingRemove = true;
|
||||
}
|
||||
setMarketplaceStates(newStates);
|
||||
// Apply the change immediately
|
||||
setTimeout(applyChanges, 100, newStates);
|
||||
} else if (targetState) {
|
||||
// No action - just show the details view for this marketplace
|
||||
setSelectedIndex(targetIndex + 1); // +1 because "Add Marketplace" is at index 0
|
||||
setSelectedMarketplace(targetState);
|
||||
setInternalView('details');
|
||||
}
|
||||
} else if (setError) {
|
||||
setError(`Marketplace not found: ${targetMarketplace}`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (setError) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to load marketplaces');
|
||||
}
|
||||
setProcessError(err instanceof Error ? err.message : 'Failed to load marketplaces');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
void loadMarketplaces();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: intentional
|
||||
}, [targetMarketplace, action, error]);
|
||||
|
||||
// Check if there are any pending changes
|
||||
const hasPendingChanges = () => {
|
||||
return marketplaceStates.some(state => state.pendingUpdate || state.pendingRemove);
|
||||
};
|
||||
|
||||
// Get count of pending operations
|
||||
const getPendingCounts = () => {
|
||||
const updateCount = count(marketplaceStates, s => s.pendingUpdate);
|
||||
const removeCount = count(marketplaceStates, s => s.pendingRemove);
|
||||
return {
|
||||
updateCount,
|
||||
removeCount
|
||||
};
|
||||
};
|
||||
|
||||
// Apply all pending changes
|
||||
const applyChanges = async (states?: MarketplaceState[]) => {
|
||||
const statesToProcess = states || marketplaceStates;
|
||||
const wasInDetailsView = internalView === 'details';
|
||||
setIsProcessing(true);
|
||||
setProcessError(null);
|
||||
setSuccessMessage(null);
|
||||
setProgressMessage(null);
|
||||
try {
|
||||
const settings = getSettingsForSource('userSettings');
|
||||
let updatedCount = 0;
|
||||
let removedCount = 0;
|
||||
const refreshedMarketplaces = new Set<string>();
|
||||
for (const state of statesToProcess) {
|
||||
// Handle remove
|
||||
if (state.pendingRemove) {
|
||||
// First uninstall all plugins from this marketplace
|
||||
if (state.installedPlugins && state.installedPlugins.length > 0) {
|
||||
const newEnabledPlugins = {
|
||||
...settings?.enabledPlugins
|
||||
};
|
||||
for (const plugin of state.installedPlugins) {
|
||||
const pluginId = createPluginId(plugin.name, state.name);
|
||||
// Mark as disabled/uninstalled
|
||||
newEnabledPlugins[pluginId] = false;
|
||||
}
|
||||
updateSettingsForSource('userSettings', {
|
||||
enabledPlugins: newEnabledPlugins
|
||||
});
|
||||
}
|
||||
|
||||
// Then remove the marketplace
|
||||
await removeMarketplaceSource(state.name);
|
||||
removedCount++;
|
||||
logEvent('tengu_marketplace_removed', {
|
||||
marketplace_name: state.name as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
plugins_uninstalled: state.installedPlugins?.length || 0
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle update
|
||||
if (state.pendingUpdate) {
|
||||
// Refresh individual marketplace for efficiency with progress reporting
|
||||
await refreshMarketplace(state.name, (message: string) => {
|
||||
setProgressMessage(message);
|
||||
});
|
||||
updatedCount++;
|
||||
refreshedMarketplaces.add(state.name.toLowerCase());
|
||||
logEvent('tengu_marketplace_updated', {
|
||||
marketplace_name: state.name as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// After marketplace clones are refreshed, bump installed plugins from
|
||||
// those marketplaces to the new version. Without this, the loader's
|
||||
// cache-on-miss (copyPluginToVersionedCache) creates the new version
|
||||
// dir on the next loadAllPlugins() call, but installed_plugins.json
|
||||
// stays on the old version — so cleanupOrphanedPluginVersionsInBackground
|
||||
// stamps the NEW dir with .orphaned_at on the next startup. See #29512.
|
||||
// updatePluginOp (called inside the helper) is what actually writes
|
||||
// installed_plugins.json via updateInstallationPathOnDisk.
|
||||
let updatedPluginCount = 0;
|
||||
if (refreshedMarketplaces.size > 0) {
|
||||
const updatedPluginIds = await updatePluginsForMarketplaces(refreshedMarketplaces);
|
||||
updatedPluginCount = updatedPluginIds.length;
|
||||
}
|
||||
|
||||
// Clear caches after changes
|
||||
clearAllCaches();
|
||||
|
||||
// Call completion callback
|
||||
if (onManageComplete) {
|
||||
await onManageComplete();
|
||||
}
|
||||
|
||||
// Reload marketplace data to show updated timestamps
|
||||
const config = await loadKnownMarketplacesConfig();
|
||||
const {
|
||||
enabled,
|
||||
disabled
|
||||
} = await loadAllPlugins();
|
||||
const allPlugins = [...enabled, ...disabled];
|
||||
const {
|
||||
marketplaces
|
||||
} = await loadMarketplacesWithGracefulDegradation(config);
|
||||
const newStates: MarketplaceState[] = [];
|
||||
for (const {
|
||||
name,
|
||||
config: entry,
|
||||
data: marketplace
|
||||
} of marketplaces) {
|
||||
const installedFromMarketplace = allPlugins.filter(plugin => plugin.source.endsWith(`@${name}`));
|
||||
newStates.push({
|
||||
name,
|
||||
source: getMarketplaceSourceDisplay(entry.source),
|
||||
lastUpdated: entry.lastUpdated,
|
||||
pluginCount: marketplace?.plugins.length,
|
||||
installedPlugins: installedFromMarketplace,
|
||||
pendingUpdate: false,
|
||||
pendingRemove: false,
|
||||
autoUpdate: isMarketplaceAutoUpdate(name, entry)
|
||||
});
|
||||
}
|
||||
|
||||
// Sort: claude-plugin-directory first, then alphabetically
|
||||
newStates.sort((a, b) => {
|
||||
if (a.name === 'claude-plugin-directory') return -1;
|
||||
if (b.name === 'claude-plugin-directory') return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
setMarketplaceStates(newStates);
|
||||
|
||||
// Update selected marketplace reference with fresh data
|
||||
if (wasInDetailsView && selectedMarketplace) {
|
||||
const updatedMarketplace = newStates.find(s => s.name === selectedMarketplace.name);
|
||||
if (updatedMarketplace) {
|
||||
setSelectedMarketplace(updatedMarketplace);
|
||||
}
|
||||
}
|
||||
|
||||
// Build success message
|
||||
const actions: string[] = [];
|
||||
if (updatedCount > 0) {
|
||||
const pluginPart = updatedPluginCount > 0 ? ` (${updatedPluginCount} ${plural(updatedPluginCount, 'plugin')} bumped)` : '';
|
||||
actions.push(`Updated ${updatedCount} ${plural(updatedCount, 'marketplace')}${pluginPart}`);
|
||||
}
|
||||
if (removedCount > 0) {
|
||||
actions.push(`Removed ${removedCount} ${plural(removedCount, 'marketplace')}`);
|
||||
}
|
||||
if (actions.length > 0) {
|
||||
const successMsg = `${figures.tick} ${actions.join(', ')}`;
|
||||
// If we were in details view, stay there and show success
|
||||
if (wasInDetailsView) {
|
||||
setSuccessMessage(successMsg);
|
||||
} else {
|
||||
// Otherwise show result and exit to menu
|
||||
setResult(successMsg);
|
||||
setTimeout(setViewState, 2000, {
|
||||
type: 'menu' as const
|
||||
});
|
||||
}
|
||||
} else if (!wasInDetailsView) {
|
||||
setViewState({
|
||||
type: 'menu'
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMsg = errorMessage(err);
|
||||
setProcessError(errorMsg);
|
||||
if (setError) {
|
||||
setError(errorMsg);
|
||||
}
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
setProgressMessage(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle confirming marketplace removal
|
||||
const confirmRemove = async () => {
|
||||
if (!selectedMarketplace) return;
|
||||
|
||||
// Mark for removal and apply
|
||||
const newStates = marketplaceStates.map(state => state.name === selectedMarketplace.name ? {
|
||||
...state,
|
||||
pendingRemove: true
|
||||
} : state);
|
||||
setMarketplaceStates(newStates);
|
||||
await applyChanges(newStates);
|
||||
};
|
||||
|
||||
// Build menu options for details view
|
||||
const buildDetailsMenuOptions = (marketplace: MarketplaceState | null): Array<{
|
||||
label: string;
|
||||
secondaryLabel?: string;
|
||||
value: string;
|
||||
}> => {
|
||||
if (!marketplace) return [];
|
||||
const options: Array<{
|
||||
label: string;
|
||||
secondaryLabel?: string;
|
||||
value: string;
|
||||
}> = [{
|
||||
label: `Browse plugins (${marketplace.pluginCount ?? 0})`,
|
||||
value: 'browse'
|
||||
}, {
|
||||
label: 'Update marketplace',
|
||||
secondaryLabel: marketplace.lastUpdated ? `(last updated ${new Date(marketplace.lastUpdated).toLocaleDateString()})` : undefined,
|
||||
value: 'update'
|
||||
}];
|
||||
|
||||
// Only show auto-update toggle if auto-updater is not globally disabled
|
||||
if (!shouldSkipPluginAutoupdate()) {
|
||||
options.push({
|
||||
label: marketplace.autoUpdate ? 'Disable auto-update' : 'Enable auto-update',
|
||||
value: 'toggle-auto-update'
|
||||
});
|
||||
}
|
||||
options.push({
|
||||
label: 'Remove marketplace',
|
||||
value: 'remove'
|
||||
});
|
||||
return options;
|
||||
};
|
||||
|
||||
// Handle toggling auto-update for a marketplace
|
||||
const handleToggleAutoUpdate = async (marketplace: MarketplaceState) => {
|
||||
const newAutoUpdate = !marketplace.autoUpdate;
|
||||
try {
|
||||
await setMarketplaceAutoUpdate(marketplace.name, newAutoUpdate);
|
||||
|
||||
// Update local state
|
||||
setMarketplaceStates(prev => prev.map(state => state.name === marketplace.name ? {
|
||||
...state,
|
||||
autoUpdate: newAutoUpdate
|
||||
} : state));
|
||||
|
||||
// Update selected marketplace reference
|
||||
setSelectedMarketplace(prev => prev ? {
|
||||
...prev,
|
||||
autoUpdate: newAutoUpdate
|
||||
} : prev);
|
||||
} catch (err) {
|
||||
setProcessError(err instanceof Error ? err.message : 'Failed to update setting');
|
||||
}
|
||||
};
|
||||
|
||||
// Escape in details or confirm-remove view - go back to list
|
||||
useKeybinding('confirm:no', () => {
|
||||
setInternalView('list');
|
||||
setDetailsMenuIndex(0);
|
||||
}, {
|
||||
context: 'Confirmation',
|
||||
isActive: !isProcessing && (internalView === 'details' || internalView === 'confirm-remove')
|
||||
});
|
||||
|
||||
// Escape in list view with pending changes - clear pending changes
|
||||
useKeybinding('confirm:no', () => {
|
||||
setMarketplaceStates(prev => prev.map(state => ({
|
||||
...state,
|
||||
pendingUpdate: false,
|
||||
pendingRemove: false
|
||||
})));
|
||||
setSelectedIndex(0);
|
||||
}, {
|
||||
context: 'Confirmation',
|
||||
isActive: !isProcessing && internalView === 'list' && hasPendingChanges()
|
||||
});
|
||||
|
||||
// Escape in list view without pending changes - exit to parent menu
|
||||
useKeybinding('confirm:no', () => {
|
||||
setViewState({
|
||||
type: 'menu'
|
||||
});
|
||||
}, {
|
||||
context: 'Confirmation',
|
||||
isActive: !isProcessing && internalView === 'list' && !hasPendingChanges()
|
||||
});
|
||||
|
||||
// List view — navigation (up/down/enter via configurable keybindings)
|
||||
useKeybindings({
|
||||
'select:previous': () => setSelectedIndex(prev => Math.max(0, prev - 1)),
|
||||
'select:next': () => {
|
||||
const totalItems = marketplaceStates.length + 1;
|
||||
setSelectedIndex(prev => Math.min(totalItems - 1, prev + 1));
|
||||
},
|
||||
'select:accept': () => {
|
||||
const marketplaceIndex = selectedIndex - 1;
|
||||
if (selectedIndex === 0) {
|
||||
setViewState({
|
||||
type: 'add-marketplace'
|
||||
});
|
||||
} else if (hasPendingChanges()) {
|
||||
void applyChanges();
|
||||
} else {
|
||||
const marketplace = marketplaceStates[marketplaceIndex];
|
||||
if (marketplace) {
|
||||
setSelectedMarketplace(marketplace);
|
||||
setInternalView('details');
|
||||
setDetailsMenuIndex(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, {
|
||||
context: 'Select',
|
||||
isActive: !isProcessing && internalView === 'list'
|
||||
});
|
||||
|
||||
// List view — marketplace-specific actions (u/r shortcuts)
|
||||
useInput(input => {
|
||||
const marketplaceIndex = selectedIndex - 1;
|
||||
if ((input === 'u' || input === 'U') && marketplaceIndex >= 0) {
|
||||
setMarketplaceStates(prev => prev.map((state, idx) => idx === marketplaceIndex ? {
|
||||
...state,
|
||||
pendingUpdate: !state.pendingUpdate,
|
||||
pendingRemove: state.pendingUpdate ? state.pendingRemove : false
|
||||
} : state));
|
||||
} else if ((input === 'r' || input === 'R') && marketplaceIndex >= 0) {
|
||||
const marketplace = marketplaceStates[marketplaceIndex];
|
||||
if (marketplace) {
|
||||
setSelectedMarketplace(marketplace);
|
||||
setInternalView('confirm-remove');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
isActive: !isProcessing && internalView === 'list'
|
||||
});
|
||||
|
||||
// Details view — navigation
|
||||
useKeybindings({
|
||||
'select:previous': () => setDetailsMenuIndex(prev => Math.max(0, prev - 1)),
|
||||
'select:next': () => {
|
||||
const menuOptions = buildDetailsMenuOptions(selectedMarketplace);
|
||||
setDetailsMenuIndex(prev => Math.min(menuOptions.length - 1, prev + 1));
|
||||
},
|
||||
'select:accept': () => {
|
||||
if (!selectedMarketplace) return;
|
||||
const menuOptions = buildDetailsMenuOptions(selectedMarketplace);
|
||||
const selectedOption = menuOptions[detailsMenuIndex];
|
||||
if (selectedOption?.value === 'browse') {
|
||||
setViewState({
|
||||
type: 'browse-marketplace',
|
||||
targetMarketplace: selectedMarketplace.name
|
||||
});
|
||||
} else if (selectedOption?.value === 'update') {
|
||||
const newStates = marketplaceStates.map(state => state.name === selectedMarketplace.name ? {
|
||||
...state,
|
||||
pendingUpdate: true
|
||||
} : state);
|
||||
setMarketplaceStates(newStates);
|
||||
void applyChanges(newStates);
|
||||
} else if (selectedOption?.value === 'toggle-auto-update') {
|
||||
void handleToggleAutoUpdate(selectedMarketplace);
|
||||
} else if (selectedOption?.value === 'remove') {
|
||||
setInternalView('confirm-remove');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
context: 'Select',
|
||||
isActive: !isProcessing && internalView === 'details'
|
||||
});
|
||||
|
||||
// Confirm-remove view — y/n input
|
||||
useInput(input => {
|
||||
if (input === 'y' || input === 'Y') {
|
||||
void confirmRemove();
|
||||
} else if (input === 'n' || input === 'N') {
|
||||
setInternalView('list');
|
||||
setSelectedMarketplace(null);
|
||||
}
|
||||
}, {
|
||||
isActive: !isProcessing && internalView === 'confirm-remove'
|
||||
});
|
||||
if (loading) {
|
||||
return <Text>Loading marketplaces…</Text>;
|
||||
}
|
||||
if (marketplaceStates.length === 0) {
|
||||
return <Box flexDirection="column">
|
||||
<Box marginBottom={1}>
|
||||
<Text bold>Manage marketplaces</Text>
|
||||
</Box>
|
||||
|
||||
{/* Add Marketplace option */}
|
||||
<Box flexDirection="row" gap={1}>
|
||||
<Text color="suggestion">{figures.pointer} +</Text>
|
||||
<Text bold color="suggestion">
|
||||
Add Marketplace
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box marginLeft={3}>
|
||||
<Text dimColor italic>
|
||||
{exitState.pending ? <>Press {exitState.keyName} again to go back</> : <Byline>
|
||||
<ConfigurableShortcutHint action="select:accept" context="Select" fallback="Enter" description="select" />
|
||||
<ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="go back" />
|
||||
</Byline>}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>;
|
||||
}
|
||||
|
||||
// Show confirmation dialog
|
||||
if (internalView === 'confirm-remove' && selectedMarketplace) {
|
||||
const pluginCount = selectedMarketplace.installedPlugins?.length || 0;
|
||||
return <Box flexDirection="column">
|
||||
<Text bold color="warning">
|
||||
Remove marketplace <Text italic>{selectedMarketplace.name}</Text>?
|
||||
</Text>
|
||||
<Box flexDirection="column">
|
||||
{pluginCount > 0 && <Box marginTop={1}>
|
||||
<Text color="warning">
|
||||
This will also uninstall {pluginCount}{' '}
|
||||
{plural(pluginCount, 'plugin')} from this marketplace:
|
||||
</Text>
|
||||
</Box>}
|
||||
{selectedMarketplace.installedPlugins && selectedMarketplace.installedPlugins.length > 0 && <Box flexDirection="column" marginTop={1} marginLeft={2}>
|
||||
{selectedMarketplace.installedPlugins.map(plugin => <Text key={plugin.name} dimColor>
|
||||
• {plugin.name}
|
||||
</Text>)}
|
||||
</Box>}
|
||||
<Box marginTop={1}>
|
||||
<Text>
|
||||
Press <Text bold>y</Text> to confirm or <Text bold>n</Text> to
|
||||
cancel
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>;
|
||||
}
|
||||
|
||||
// Show marketplace details
|
||||
if (internalView === 'details' && selectedMarketplace) {
|
||||
// Check if this marketplace is currently being processed
|
||||
// Check pendingUpdate first so we show updating state immediately when user presses Enter
|
||||
const isUpdating = selectedMarketplace.pendingUpdate || isProcessing;
|
||||
const menuOptions = buildDetailsMenuOptions(selectedMarketplace);
|
||||
return <Box flexDirection="column">
|
||||
<Text bold>{selectedMarketplace.name}</Text>
|
||||
<Text dimColor>{selectedMarketplace.source}</Text>
|
||||
<Box marginTop={1}>
|
||||
<Text>
|
||||
{selectedMarketplace.pluginCount || 0} available{' '}
|
||||
{plural(selectedMarketplace.pluginCount || 0, 'plugin')}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{/* Installed plugins section */}
|
||||
{selectedMarketplace.installedPlugins && selectedMarketplace.installedPlugins.length > 0 && <Box flexDirection="column" marginTop={1}>
|
||||
<Text bold>
|
||||
Installed plugins ({selectedMarketplace.installedPlugins.length}
|
||||
):
|
||||
</Text>
|
||||
<Box flexDirection="column" marginLeft={1}>
|
||||
{selectedMarketplace.installedPlugins.map(plugin => <Box key={plugin.name} flexDirection="row" gap={1}>
|
||||
<Text>{figures.bullet}</Text>
|
||||
<Box flexDirection="column">
|
||||
<Text>{plugin.name}</Text>
|
||||
<Text dimColor>{plugin.manifest.description}</Text>
|
||||
</Box>
|
||||
</Box>)}
|
||||
</Box>
|
||||
</Box>}
|
||||
|
||||
{/* Processing indicator */}
|
||||
{isUpdating && <Box marginTop={1} flexDirection="column">
|
||||
<Text color="claude">Updating marketplace…</Text>
|
||||
{progressMessage && <Text dimColor>{progressMessage}</Text>}
|
||||
</Box>}
|
||||
|
||||
{/* Success message */}
|
||||
{!isUpdating && successMessage && <Box marginTop={1}>
|
||||
<Text color="claude">{successMessage}</Text>
|
||||
</Box>}
|
||||
|
||||
{/* Error message */}
|
||||
{!isUpdating && processError && <Box marginTop={1}>
|
||||
<Text color="error">{processError}</Text>
|
||||
</Box>}
|
||||
|
||||
{/* Menu options */}
|
||||
{!isUpdating && <Box flexDirection="column" marginTop={1}>
|
||||
{menuOptions.map((option, idx) => {
|
||||
if (!option) return null;
|
||||
const isSelected = idx === detailsMenuIndex;
|
||||
return <Box key={option.value}>
|
||||
<Text color={isSelected ? 'suggestion' : undefined}>
|
||||
{isSelected ? figures.pointer : ' '} {option.label}
|
||||
</Text>
|
||||
{option.secondaryLabel && <Text dimColor> {option.secondaryLabel}</Text>}
|
||||
</Box>;
|
||||
})}
|
||||
</Box>}
|
||||
|
||||
{/* Show explanatory text at the bottom when auto-update is enabled */}
|
||||
{!isUpdating && !shouldSkipPluginAutoupdate() && selectedMarketplace.autoUpdate && <Box marginTop={1}>
|
||||
<Text dimColor>
|
||||
Auto-update enabled. Claude Code will automatically update this
|
||||
marketplace and its installed plugins.
|
||||
</Text>
|
||||
</Box>}
|
||||
|
||||
<Box marginLeft={3}>
|
||||
<Text dimColor italic>
|
||||
{isUpdating ? <>Please wait…</> : <Byline>
|
||||
<ConfigurableShortcutHint action="select:accept" context="Select" fallback="Enter" description="select" />
|
||||
<ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="go back" />
|
||||
</Byline>}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>;
|
||||
}
|
||||
|
||||
// Show marketplace list
|
||||
const {
|
||||
updateCount,
|
||||
removeCount
|
||||
} = getPendingCounts();
|
||||
return <Box flexDirection="column">
|
||||
<Box marginBottom={1}>
|
||||
<Text bold>Manage marketplaces</Text>
|
||||
</Box>
|
||||
|
||||
{/* Add Marketplace option */}
|
||||
<Box flexDirection="row" gap={1} marginBottom={1}>
|
||||
<Text color={selectedIndex === 0 ? 'suggestion' : undefined}>
|
||||
{selectedIndex === 0 ? figures.pointer : ' '} +
|
||||
</Text>
|
||||
<Text bold color={selectedIndex === 0 ? 'suggestion' : undefined}>
|
||||
Add Marketplace
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{/* Marketplace list */}
|
||||
<Box flexDirection="column">
|
||||
{marketplaceStates.map((state, idx) => {
|
||||
const isSelected = idx + 1 === selectedIndex; // +1 because Add Marketplace is at index 0
|
||||
|
||||
// Build status indicators
|
||||
const indicators: string[] = [];
|
||||
if (state.pendingUpdate) indicators.push('UPDATE');
|
||||
if (state.pendingRemove) indicators.push('REMOVE');
|
||||
return <Box key={state.name} flexDirection="row" gap={1} marginBottom={1}>
|
||||
<Text color={isSelected ? 'suggestion' : undefined}>
|
||||
{isSelected ? figures.pointer : ' '}{' '}
|
||||
{state.pendingRemove ? figures.cross : figures.bullet}
|
||||
</Text>
|
||||
<Box flexDirection="column" flexGrow={1}>
|
||||
<Box flexDirection="row" gap={1}>
|
||||
<Text bold strikethrough={state.pendingRemove} dimColor={state.pendingRemove}>
|
||||
{state.name === 'claude-plugins-official' && <Text color="claude">✻ </Text>}
|
||||
{state.name}
|
||||
{state.name === 'claude-plugins-official' && <Text color="claude"> ✻</Text>}
|
||||
</Text>
|
||||
{indicators.length > 0 && <Text color="warning">[{indicators.join(', ')}]</Text>}
|
||||
</Box>
|
||||
<Text dimColor>{state.source}</Text>
|
||||
<Text dimColor>
|
||||
{state.pluginCount !== undefined && <>{state.pluginCount} available</>}
|
||||
{state.installedPlugins && state.installedPlugins.length > 0 && <> • {state.installedPlugins.length} installed</>}
|
||||
{state.lastUpdated && <>
|
||||
{' '}
|
||||
• Updated{' '}
|
||||
{new Date(state.lastUpdated).toLocaleDateString()}
|
||||
</>}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>;
|
||||
})}
|
||||
</Box>
|
||||
|
||||
{/* Pending changes summary */}
|
||||
{hasPendingChanges() && <Box marginTop={1} flexDirection="column">
|
||||
<Text>
|
||||
<Text bold>Pending changes:</Text>{' '}
|
||||
<Text dimColor>Enter to apply</Text>
|
||||
</Text>
|
||||
{updateCount > 0 && <Text>
|
||||
• Update {updateCount} {plural(updateCount, 'marketplace')}
|
||||
</Text>}
|
||||
{removeCount > 0 && <Text color="warning">
|
||||
• Remove {removeCount} {plural(removeCount, 'marketplace')}
|
||||
</Text>}
|
||||
</Box>}
|
||||
|
||||
{/* Processing indicator */}
|
||||
{isProcessing && <Box marginTop={1}>
|
||||
<Text color="claude">Processing changes…</Text>
|
||||
</Box>}
|
||||
|
||||
{/* Error display */}
|
||||
{processError && <Box marginTop={1}>
|
||||
<Text color="error">{processError}</Text>
|
||||
</Box>}
|
||||
|
||||
<ManageMarketplacesKeyHints exitState={exitState} hasPendingActions={hasPendingChanges()} />
|
||||
</Box>;
|
||||
}
|
||||
type ManageMarketplacesKeyHintsProps = {
|
||||
exitState: Props['exitState'];
|
||||
hasPendingActions: boolean;
|
||||
};
|
||||
function ManageMarketplacesKeyHints(t0) {
|
||||
const $ = _c(18);
|
||||
const {
|
||||
exitState,
|
||||
hasPendingActions
|
||||
} = t0;
|
||||
if (exitState.pending) {
|
||||
let t1;
|
||||
if ($[0] !== exitState.keyName) {
|
||||
t1 = <Box marginTop={1}><Text dimColor={true} italic={true}>Press {exitState.keyName} again to go back</Text></Box>;
|
||||
$[0] = exitState.keyName;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
let t1;
|
||||
if ($[2] !== hasPendingActions) {
|
||||
t1 = hasPendingActions && <ConfigurableShortcutHint action="select:accept" context="Select" fallback="Enter" description="apply changes" />;
|
||||
$[2] = hasPendingActions;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
let t2;
|
||||
if ($[4] !== hasPendingActions) {
|
||||
t2 = !hasPendingActions && <ConfigurableShortcutHint action="select:accept" context="Select" fallback="Enter" description="select" />;
|
||||
$[4] = hasPendingActions;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
}
|
||||
let t3;
|
||||
if ($[6] !== hasPendingActions) {
|
||||
t3 = !hasPendingActions && <KeyboardShortcutHint shortcut="u" action="update" />;
|
||||
$[6] = hasPendingActions;
|
||||
$[7] = t3;
|
||||
} else {
|
||||
t3 = $[7];
|
||||
}
|
||||
let t4;
|
||||
if ($[8] !== hasPendingActions) {
|
||||
t4 = !hasPendingActions && <KeyboardShortcutHint shortcut="r" action="remove" />;
|
||||
$[8] = hasPendingActions;
|
||||
$[9] = t4;
|
||||
} else {
|
||||
t4 = $[9];
|
||||
}
|
||||
const t5 = hasPendingActions ? "cancel" : "go back";
|
||||
let t6;
|
||||
if ($[10] !== t5) {
|
||||
t6 = <ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description={t5} />;
|
||||
$[10] = t5;
|
||||
$[11] = t6;
|
||||
} else {
|
||||
t6 = $[11];
|
||||
}
|
||||
let t7;
|
||||
if ($[12] !== t1 || $[13] !== t2 || $[14] !== t3 || $[15] !== t4 || $[16] !== t6) {
|
||||
t7 = <Box marginTop={1}><Text dimColor={true} italic={true}><Byline>{t1}{t2}{t3}{t4}{t6}</Byline></Text></Box>;
|
||||
$[12] = t1;
|
||||
$[13] = t2;
|
||||
$[14] = t3;
|
||||
$[15] = t4;
|
||||
$[16] = t6;
|
||||
$[17] = t7;
|
||||
} else {
|
||||
t7 = $[17];
|
||||
}
|
||||
return t7;
|
||||
}
|
||||
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJmaWd1cmVzIiwiUmVhY3QiLCJ1c2VFZmZlY3QiLCJ1c2VSZWYiLCJ1c2VTdGF0ZSIsIkFuYWx5dGljc01ldGFkYXRhX0lfVkVSSUZJRURfVEhJU19JU19OT1RfQ09ERV9PUl9GSUxFUEFUSFMiLCJsb2dFdmVudCIsIkNvbmZpZ3VyYWJsZVNob3J0Y3V0SGludCIsIkJ5bGluZSIsIktleWJvYXJkU2hvcnRjdXRIaW50IiwiQm94IiwiVGV4dCIsInVzZUlucHV0IiwidXNlS2V5YmluZGluZyIsInVzZUtleWJpbmRpbmdzIiwiTG9hZGVkUGx1Z2luIiwiY291bnQiLCJzaG91bGRTa2lwUGx1Z2luQXV0b3VwZGF0ZSIsImVycm9yTWVzc2FnZSIsImNsZWFyQWxsQ2FjaGVzIiwiY3JlYXRlUGx1Z2luSWQiLCJmb3JtYXRNYXJrZXRwbGFjZUxvYWRpbmdFcnJvcnMiLCJnZXRNYXJrZXRwbGFjZVNvdXJjZURpc3BsYXkiLCJsb2FkTWFya2V0cGxhY2VzV2l0aEdyYWNlZnVsRGVncmFkYXRpb24iLCJsb2FkS25vd25NYXJrZXRwbGFjZXNDb25maWciLCJyZWZyZXNoTWFya2V0cGxhY2UiLCJyZW1vdmVNYXJrZXRwbGFjZVNvdXJjZSIsInNldE1hcmtldHBsYWNlQXV0b1VwZGF0ZSIsInVwZGF0ZVBsdWdpbnNGb3JNYXJrZXRwbGFjZXMiLCJsb2FkQWxsUGx1Z2lucyIsImlzTWFya2V0cGxhY2VBdXRvVXBkYXRlIiwiZ2V0U2V0dGluZ3NGb3JTb3VyY2UiLCJ1cGRhdGVTZXR0aW5nc0ZvclNvdXJjZSIsInBsdXJhbCIsIlZpZXdTdGF0ZSIsIlByb3BzIiwic2V0Vmlld1N0YXRlIiwic3RhdGUiLCJlcnJvciIsInNldEVycm9yIiwic2V0UmVzdWx0IiwicmVzdWx0IiwiZXhpdFN0YXRlIiwicGVuZGluZyIsImtleU5hbWUiLCJvbk1hbmFnZUNvbXBsZXRlIiwiUHJvbWlzZSIsInRhcmdldE1hcmtldHBsYWNlIiwiYWN0aW9uIiwiTWFya2V0cGxhY2VTdGF0ZSIsIm5hbWUiLCJzb3VyY2UiLCJsYXN0VXBkYXRlZCIsInBsdWdpbkNvdW50IiwiaW5zdGFsbGVkUGx1Z2lucyIsInBlbmRpbmdVcGRhdGUiLCJwZW5kaW5nUmVtb3ZlIiwiYXV0b1VwZGF0ZSIsIkludGVybmFsVmlld1N0YXRlIiwiTWFuYWdlTWFya2V0cGxhY2VzIiwiUmVhY3ROb2RlIiwibWFya2V0cGxhY2VTdGF0ZXMiLCJzZXRNYXJrZXRwbGFjZVN0YXRlcyIsImxvYWRpbmciLCJzZXRMb2FkaW5nIiwic2VsZWN0ZWRJbmRleCIsInNldFNlbGVjdGVkSW5kZXgiLCJpc1Byb2Nlc3NpbmciLCJzZXRJc1Byb2Nlc3NpbmciLCJwcm9jZXNzRXJyb3IiLCJzZXRQcm9jZXNzRXJyb3IiLCJzdWNjZXNzTWVzc2FnZSIsInNldFN1Y2Nlc3NNZXNzYWdlIiwicHJvZ3Jlc3NNZXNzYWdlIiwic2V0UHJvZ3Jlc3NNZXNzYWdlIiwiaW50ZXJuYWxWaWV3Iiwic2V0SW50ZXJuYWxWaWV3Iiwic2VsZWN0ZWRNYXJrZXRwbGFjZSIsInNldFNlbGVjdGVkTWFya2V0cGxhY2UiLCJkZXRhaWxzTWVudUluZGV4Iiwic2V0RGV0YWlsc01lbnVJbmRleCIsImhhc0F0dGVtcHRlZEF1dG9BY3Rpb24iLCJsb2FkTWFya2V0cGxhY2VzIiwiY29uZmlnIiwiZW5hYmxlZCIsImRpc2FibGVkIiwiYWxsUGx1Z2lucyIsIm1hcmtldHBsYWNlcyIsImZhaWx1cmVzIiwic3RhdGVzIiwiZW50cnkiLCJkYXRhIiwibWFya2V0cGxhY2UiLCJpbnN0YWxsZWRGcm9tTWFya2V0cGxhY2UiLCJmaWx0ZXIiLCJwbHVnaW4iLCJlbmRzV2l0aCIsInB1c2giLCJwbHVnaW5zIiwibGVuZ3RoIiwic29ydCIsImEiLCJiIiwibG9jYWxlQ29tcGFyZSIsInN1Y2Nlc3NDb3VudCIsIm0iLCJlcnJvclJlc3VsdCIsInR5cGUiLCJtZXNzYWdlIiwiRXJyb3IiLCJjdXJyZW50IiwidGFyZ2V0SW5kZXgiLCJmaW5kSW5kZXgiLCJzIiwidGFyZ2V0U3RhdGUiLCJuZXdTdGF0ZXMiLCJzZXRUaW1lb3V0IiwiYXBwbHlDaGFuZ2VzIiwiZXJyIiwiaGFzUGVuZGluZ0NoYW5nZXMiLCJzb21lIiwiZ2V0UGVuZGluZ0NvdW50cyIsInVwZGF0ZUNvdW50IiwicmVtb3ZlQ291bnQiLCJzdGF0ZXNUb1Byb2Nlc3MiLCJ3YXNJbkRldGFpbHNWaWV3Iiwic2V0dGluZ3MiLCJ1cGRhdGVkQ291bnQiLCJyZW1vdmVkQ291bnQiLCJyZWZyZXNoZWRNYXJrZXRwbGFjZXMiLCJTZXQiLCJuZXdFbmFibGVkUGx1Z2lucyIsImVuYWJsZWRQbHVnaW5zIiwicGx1Z2luSWQiLCJtYXJrZXRwbGFjZV9uYW1lIiwicGx1Z2luc191bmluc3RhbGxlZCIsImFkZCIsInRvTG93ZXJDYXNlIiwidXBkYXRlZFBsdWdpbkNvdW50Iiwic2l6ZSIsInVwZGF0ZWRQbHVnaW5JZHMiLCJ1cGRhdGVkTWFya2V0cGxhY2UiLCJmaW5kIiwiYWN0aW9ucyIsInBsdWdpblBhcnQiLCJzdWNjZXNzTXNnIiwidGljayIsImpvaW4iLCJjb25zdCIsImVycm9yTXNnIiwiY29uZmlybVJlbW92ZSIsIm1hcCIsImJ1aWxkRGV0YWlsc01lbnVPcHRpb25zIiwiQXJyYXkiLCJsYWJlbCIsInNlY29uZGFyeUxhYmVsIiwidmFsdWUiLCJvcHRpb25zIiwiRGF0ZSIsInRvTG9jYWxlRGF0ZVN0cmluZyIsInVuZGVmaW5lZCIsImhhbmRsZVRvZ2dsZUF1dG9VcGRhdGUiLCJuZXdBdXRvVXBkYXRlIiwicHJldiIsImNvbnRleHQiLCJpc0FjdGl2ZSIsInNlbGVjdDpwcmV2aW91cyIsIk1hdGgiLCJtYXgiLCJzZWxlY3Q6bmV4dCIsInRvdGFsSXRlbXMiLCJtaW4iLCJzZWxlY3Q6YWNjZXB0IiwibWFya2V0cGxhY2VJbmRleCIsImlucHV0IiwiaWR4IiwibWVudU9wdGlvbnMiLCJzZWxlY3RlZE9wdGlvbiIsInBvaW50ZXIiLCJpc1VwZGF0aW5nIiwiYnVsbGV0IiwibWFuaWZlc3QiLCJkZXNjcmlwdGlvbiIsIm9wdGlvbiIsImlzU2VsZWN0ZWQiLCJpbmRpY2F0b3JzIiwiY3Jvc3MiLCJNYW5hZ2VNYXJrZXRwbGFjZXNLZXlIaW50c1Byb3BzIiwiaGFzUGVuZGluZ0FjdGlvbnMiLCJNYW5hZ2VNYXJrZXRwbGFjZXNLZXlIaW50cyIsInQwIiwiJCIsIl9jIiwidDEiLCJ0MiIsInQzIiwidDQiLCJ0NSIsInQ2IiwidDciXSwic291cmNlcyI6WyJNYW5hZ2VNYXJrZXRwbGFjZXMudHN4Il0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCBmaWd1cmVzIGZyb20gJ2ZpZ3VyZXMnXG5pbXBvcnQgKiBhcyBSZWFjdCBmcm9tICdyZWFjdCdcbmltcG9ydCB7IHVzZUVmZmVjdCwgdXNlUmVmLCB1c2VTdGF0ZSB9IGZyb20gJ3JlYWN0J1xuaW1wb3J0IHtcbiAgdHlwZSBBbmFseXRpY3NNZXRhZGF0YV9JX1ZFUklGSUVEX1RISVNfSVNfTk9UX0NPREVfT1JfRklMRVBBVEhTLFxuICBsb2dFdmVudCxcbn0gZnJvbSAnc3JjL3NlcnZpY2VzL2FuYWx5dGljcy9pbmRleC5qcydcbmltcG9ydCB7IENvbmZpZ3VyYWJsZVNob3J0Y3V0SGludCB9IGZyb20gJy4uLy4uL2NvbXBvbmVudHMvQ29uZmlndXJhYmxlU2hvcnRjdXRIaW50LmpzJ1xuaW1wb3J0IHsgQnlsaW5lIH0gZnJvbSAnLi4vLi4vY29tcG9uZW50cy9kZXNpZ24tc3lzdGVtL0J5bGluZS5qcydcbmltcG9ydCB7IEtleWJvYXJkU2hvcnRjdXRIaW50IH0gZnJvbSAnLi4vLi4vY29tcG9uZW50cy9kZXNpZ24tc3lzdGVtL0tleWJvYXJkU2hvcnRjdXRIaW50LmpzJ1xuLy8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIGN1c3RvbS1ydWxlcy9wcmVmZXItdXNlLWtleWJpbmRpbmdzIC0tIHVzZUlucHV0IG5lZWRlZCBmb3IgbWFya2V0cGxhY2Utc3BlY2lmaWMgdS9yIHNob3J0Y3V0cyBhbmQgeS9uIGNvbmZpcm1hdGlvbiBub3QgaW4ga2V5YmluZGluZyBzY2hlbWFcbmltcG9ydCB7IEJveCwgVGV4dCwgdXNlSW5wdXQgfSBmcm9tICcuLi8uLi9pbmsuanMnXG5pbXBvcnQge1xuICB1c2VLZXliaW5kaW5nLFxuICB1c2VLZXliaW5kaW5ncyxcbn0gZnJvbSAnLi4vLi4va2V
Line truncated
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,124 @@
|
||||
import { getPluginErrorMessage, type PluginError } from '../../types/plugin.js';
|
||||
export function formatErrorMessage(error: PluginError): string {
|
||||
switch (error.type) {
|
||||
case 'path-not-found':
|
||||
return `${error.component} path not found: ${error.path}`;
|
||||
case 'git-auth-failed':
|
||||
return `Git ${error.authType.toUpperCase()} authentication failed for ${error.gitUrl}`;
|
||||
case 'git-timeout':
|
||||
return `Git ${error.operation} timed out for ${error.gitUrl}`;
|
||||
case 'network-error':
|
||||
return `Network error accessing ${error.url}${error.details ? `: ${error.details}` : ''}`;
|
||||
case 'manifest-parse-error':
|
||||
return `Failed to parse manifest at ${error.manifestPath}: ${error.parseError}`;
|
||||
case 'manifest-validation-error':
|
||||
return `Invalid manifest at ${error.manifestPath}: ${error.validationErrors.join(', ')}`;
|
||||
case 'plugin-not-found':
|
||||
return `Plugin "${error.pluginId}" not found in marketplace "${error.marketplace}"`;
|
||||
case 'marketplace-not-found':
|
||||
return `Marketplace "${error.marketplace}" not found`;
|
||||
case 'marketplace-load-failed':
|
||||
return `Failed to load marketplace "${error.marketplace}": ${error.reason}`;
|
||||
case 'mcp-config-invalid':
|
||||
return `Invalid MCP server config for "${error.serverName}": ${error.validationError}`;
|
||||
case 'mcp-server-suppressed-duplicate':
|
||||
{
|
||||
const dup = error.duplicateOf.startsWith('plugin:') ? `server provided by plugin "${error.duplicateOf.split(':')[1] ?? '?'}"` : `already-configured "${error.duplicateOf}"`;
|
||||
return `MCP server "${error.serverName}" skipped — same command/URL as ${dup}`;
|
||||
}
|
||||
case 'hook-load-failed':
|
||||
return `Failed to load hooks from ${error.hookPath}: ${error.reason}`;
|
||||
case 'component-load-failed':
|
||||
return `Failed to load ${error.component} from ${error.path}: ${error.reason}`;
|
||||
case 'mcpb-download-failed':
|
||||
return `Failed to download MCPB from ${error.url}: ${error.reason}`;
|
||||
case 'mcpb-extract-failed':
|
||||
return `Failed to extract MCPB ${error.mcpbPath}: ${error.reason}`;
|
||||
case 'mcpb-invalid-manifest':
|
||||
return `MCPB manifest invalid at ${error.mcpbPath}: ${error.validationError}`;
|
||||
case 'marketplace-blocked-by-policy':
|
||||
return error.blockedByBlocklist ? `Marketplace "${error.marketplace}" is blocked by enterprise policy` : `Marketplace "${error.marketplace}" is not in the allowed marketplace list`;
|
||||
case 'dependency-unsatisfied':
|
||||
return error.reason === 'not-enabled' ? `Dependency "${error.dependency}" is disabled` : `Dependency "${error.dependency}" is not installed`;
|
||||
case 'lsp-config-invalid':
|
||||
return `Invalid LSP server config for "${error.serverName}": ${error.validationError}`;
|
||||
case 'lsp-server-start-failed':
|
||||
return `LSP server "${error.serverName}" failed to start: ${error.reason}`;
|
||||
case 'lsp-server-crashed':
|
||||
return error.signal ? `LSP server "${error.serverName}" crashed with signal ${error.signal}` : `LSP server "${error.serverName}" crashed with exit code ${error.exitCode ?? 'unknown'}`;
|
||||
case 'lsp-request-timeout':
|
||||
return `LSP server "${error.serverName}" timed out on ${error.method} after ${error.timeoutMs}ms`;
|
||||
case 'lsp-request-failed':
|
||||
return `LSP server "${error.serverName}" ${error.method} failed: ${error.error}`;
|
||||
case 'plugin-cache-miss':
|
||||
return `Plugin "${error.plugin}" not cached at ${error.installPath}`;
|
||||
case 'generic-error':
|
||||
return error.error;
|
||||
}
|
||||
const _exhaustive: never = error;
|
||||
return getPluginErrorMessage(_exhaustive);
|
||||
}
|
||||
export function getErrorGuidance(error: PluginError): string | null {
|
||||
switch (error.type) {
|
||||
case 'path-not-found':
|
||||
return 'Check that the path in your manifest or marketplace config is correct';
|
||||
case 'git-auth-failed':
|
||||
return error.authType === 'ssh' ? 'Configure SSH keys or use HTTPS URL instead' : 'Configure credentials or use SSH URL instead';
|
||||
case 'git-timeout':
|
||||
case 'network-error':
|
||||
return 'Check your internet connection and try again';
|
||||
case 'manifest-parse-error':
|
||||
return 'Check manifest file syntax in the plugin directory';
|
||||
case 'manifest-validation-error':
|
||||
return 'Check manifest file follows the required schema';
|
||||
case 'plugin-not-found':
|
||||
return `Plugin may not exist in marketplace "${error.marketplace}"`;
|
||||
case 'marketplace-not-found':
|
||||
return error.availableMarketplaces.length > 0 ? `Available marketplaces: ${error.availableMarketplaces.join(', ')}` : 'Add the marketplace first using /plugin marketplace add';
|
||||
case 'mcp-config-invalid':
|
||||
return 'Check MCP server configuration in .mcp.json or manifest';
|
||||
case 'mcp-server-suppressed-duplicate':
|
||||
{
|
||||
// duplicateOf is "plugin:name:srv" when another plugin won dedup —
|
||||
// users can't remove plugin-provided servers from their MCP config,
|
||||
// so point them at the winning plugin instead.
|
||||
if (error.duplicateOf.startsWith('plugin:')) {
|
||||
const winningPlugin = error.duplicateOf.split(':')[1] ?? 'the other plugin';
|
||||
return `Disable plugin "${winningPlugin}" if you want this plugin's version instead`;
|
||||
}
|
||||
return `Remove "${error.duplicateOf}" from your MCP config if you want the plugin's version instead`;
|
||||
}
|
||||
case 'hook-load-failed':
|
||||
return 'Check hooks.json file syntax and structure';
|
||||
case 'component-load-failed':
|
||||
return `Check ${error.component} directory structure and file permissions`;
|
||||
case 'mcpb-download-failed':
|
||||
return 'Check your internet connection and URL accessibility';
|
||||
case 'mcpb-extract-failed':
|
||||
return 'Verify the MCPB file is valid and not corrupted';
|
||||
case 'mcpb-invalid-manifest':
|
||||
return 'Contact the plugin author about the invalid manifest';
|
||||
case 'marketplace-blocked-by-policy':
|
||||
if (error.blockedByBlocklist) {
|
||||
return 'This marketplace source is explicitly blocked by your administrator';
|
||||
}
|
||||
return error.allowedSources.length > 0 ? `Allowed sources: ${error.allowedSources.join(', ')}` : 'Contact your administrator to configure allowed marketplace sources';
|
||||
case 'dependency-unsatisfied':
|
||||
return error.reason === 'not-enabled' ? `Enable "${error.dependency}" or uninstall "${error.plugin}"` : `Install "${error.dependency}" or uninstall "${error.plugin}"`;
|
||||
case 'lsp-config-invalid':
|
||||
return 'Check LSP server configuration in the plugin manifest';
|
||||
case 'lsp-server-start-failed':
|
||||
case 'lsp-server-crashed':
|
||||
case 'lsp-request-timeout':
|
||||
case 'lsp-request-failed':
|
||||
return 'Check LSP server logs with --debug for details';
|
||||
case 'plugin-cache-miss':
|
||||
return 'Run /plugins to refresh the plugin cache';
|
||||
case 'marketplace-load-failed':
|
||||
case 'generic-error':
|
||||
return null;
|
||||
}
|
||||
const _exhaustive: never = error;
|
||||
return null;
|
||||
}
|
||||
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJnZXRQbHVnaW5FcnJvck1lc3NhZ2UiLCJQbHVnaW5FcnJvciIsImZvcm1hdEVycm9yTWVzc2FnZSIsImVycm9yIiwidHlwZSIsImNvbXBvbmVudCIsInBhdGgiLCJhdXRoVHlwZSIsInRvVXBwZXJDYXNlIiwiZ2l0VXJsIiwib3BlcmF0aW9uIiwidXJsIiwiZGV0YWlscyIsIm1hbmlmZXN0UGF0aCIsInBhcnNlRXJyb3IiLCJ2YWxpZGF0aW9uRXJyb3JzIiwiam9pbiIsInBsdWdpbklkIiwibWFya2V0cGxhY2UiLCJyZWFzb24iLCJzZXJ2ZXJOYW1lIiwidmFsaWRhdGlvbkVycm9yIiwiZHVwIiwiZHVwbGljYXRlT2YiLCJzdGFydHNXaXRoIiwic3BsaXQiLCJob29rUGF0aCIsIm1jcGJQYXRoIiwiYmxvY2tlZEJ5QmxvY2tsaXN0IiwiZGVwZW5kZW5jeSIsInNpZ25hbCIsImV4aXRDb2RlIiwibWV0aG9kIiwidGltZW91dE1zIiwicGx1Z2luIiwiaW5zdGFsbFBhdGgiLCJfZXhoYXVzdGl2ZSIsImdldEVycm9yR3VpZGFuY2UiLCJhdmFpbGFibGVNYXJrZXRwbGFjZXMiLCJsZW5ndGgiLCJ3aW5uaW5nUGx1Z2luIiwiYWxsb3dlZFNvdXJjZXMiXSwic291cmNlcyI6WyJQbHVnaW5FcnJvcnMudHN4Il0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IGdldFBsdWdpbkVycm9yTWVzc2FnZSwgdHlwZSBQbHVnaW5FcnJvciB9IGZyb20gJy4uLy4uL3R5cGVzL3BsdWdpbi5qcydcblxuZXhwb3J0IGZ1bmN0aW9uIGZvcm1hdEVycm9yTWVzc2FnZShlcnJvcjogUGx1Z2luRXJyb3IpOiBzdHJpbmcge1xuICBzd2l0Y2ggKGVycm9yLnR5cGUpIHtcbiAgICBjYXNlICdwYXRoLW5vdC1mb3VuZCc6XG4gICAgICByZXR1cm4gYCR7ZXJyb3IuY29tcG9uZW50fSBwYXRoIG5vdCBmb3VuZDogJHtlcnJvci5wYXRofWBcbiAgICBjYXNlICdnaXQtYXV0aC1mYWlsZWQnOlxuICAgICAgcmV0dXJuIGBHaXQgJHtlcnJvci5hdXRoVHlwZS50b1VwcGVyQ2FzZSgpfSBhdXRoZW50aWNhdGlvbiBmYWlsZWQgZm9yICR7ZXJyb3IuZ2l0VXJsfWBcbiAgICBjYXNlICdnaXQtdGltZW91dCc6XG4gICAgICByZXR1cm4gYEdpdCAke2Vycm9yLm9wZXJhdGlvbn0gdGltZWQgb3V0IGZvciAke2Vycm9yLmdpdFVybH1gXG4gICAgY2FzZSAnbmV0d29yay1lcnJvcic6XG4gICAgICByZXR1cm4gYE5ldHdvcmsgZXJyb3IgYWNjZXNzaW5nICR7ZXJyb3IudXJsfSR7ZXJyb3IuZGV0YWlscyA/IGA6ICR7ZXJyb3IuZGV0YWlsc31gIDogJyd9YFxuICAgIGNhc2UgJ21hbmlmZXN0LXBhcnNlLWVycm9yJzpcbiAgICAgIHJldHVybiBgRmFpbGVkIHRvIHBhcnNlIG1hbmlmZXN0IGF0ICR7ZXJyb3IubWFuaWZlc3RQYXRofTogJHtlcnJvci5wYXJzZUVycm9yfWBcbiAgICBjYXNlICdtYW5pZmVzdC12YWxpZGF0aW9uLWVycm9yJzpcbiAgICAgIHJldHVybiBgSW52YWxpZCBtYW5pZmVzdCBhdCAke2Vycm9yLm1hbmlmZXN0UGF0aH06ICR7ZXJyb3IudmFsaWRhdGlvbkVycm9ycy5qb2luKCcsICcpfWBcbiAgICBjYXNlICdwbHVnaW4tbm90LWZvdW5kJzpcbiAgICAgIHJldHVybiBgUGx1Z2luIFwiJHtlcnJvci5wbHVnaW5JZH1cIiBub3QgZm91bmQgaW4gbWFya2V0cGxhY2UgXCIke2Vycm9yLm1hcmtldHBsYWNlfVwiYFxuICAgIGNhc2UgJ21hcmtldHBsYWNlLW5vdC1mb3VuZCc6XG4gICAgICByZXR1cm4gYE1hcmtldHBsYWNlIFwiJHtlcnJvci5tYXJrZXRwbGFjZX1cIiBub3QgZm91bmRgXG4gICAgY2FzZSAnbWFya2V0cGxhY2UtbG9hZC1mYWlsZWQnOlxuICAgICAgcmV0dXJuIGBGYWlsZWQgdG8gbG9hZCBtYXJrZXRwbGFjZSBcIiR7ZXJyb3IubWFya2V0cGxhY2V9XCI6ICR7ZXJyb3IucmVhc29ufWBcbiAgICBjYXNlICdtY3AtY29uZmlnLWludmFsaWQnOlxuICAgICAgcmV0dXJuIGBJbnZhbGlkIE1DUCBzZXJ2ZXIgY29uZmlnIGZvciBcIiR7ZXJyb3Iuc2VydmVyTmFtZX1cIjogJHtlcnJvci52YWxpZGF0aW9uRXJyb3J9YFxuICAgIGNhc2UgJ21jcC1zZXJ2ZXItc3VwcHJlc3NlZC1kdXBsaWNhdGUnOiB7XG4gICAgICBjb25zdCBkdXAgPSBlcnJvci5kdXBsaWNhdGVPZi5zdGFydHNXaXRoKCdwbHVnaW46JylcbiAgICAgICAgPyBgc2VydmVyIHByb3ZpZGVkIGJ5IHBsdWdpbiBcIiR7ZXJyb3IuZHVwbGljYXRlT2Yuc3BsaXQoJzonKVsxXSA/PyAnPyd9XCJgXG4gICAgICAgIDogYGFscmVhZHktY29uZmlndXJlZCBcIiR7ZXJyb3IuZHVwbGljYXRlT2Z9XCJgXG4gICAgICByZXR1cm4gYE1DUCBzZXJ2ZXIgXCIke2Vycm9yLnNlcnZlck5hbWV9XCIgc2tpcHBlZCDigJQgc2FtZSBjb21tYW5kL1VSTCBhcyAke2R1cH1gXG4gICAgfVxuICAgIGNhc2UgJ2hvb2stbG9hZC1mYWlsZWQnOlxuICAgICAgcmV0dXJuIGBGYWlsZWQgdG8gbG9hZCBob29rcyBmcm9tICR7ZXJyb3IuaG9va1BhdGh9OiAke2Vycm9yLnJlYXNvbn1gXG4gICAgY2FzZSAnY29tcG9uZW50LWxvYWQtZmFpbGVkJzpcbiAgICAgIHJldHVybiBgRmFpbGVkIHRvIGxvYWQgJHtlcnJvci5jb21wb25lbnR9IGZyb20gJHtlcnJvci5wYXRofTogJHtlcnJvci5yZWFzb259YFxuICAgIGNhc2UgJ21jcGItZG93bmxvYWQtZmFpbGVkJzpcbiAgICAgIHJldHVybiBgRmFpbGVkIHRvIGRvd25sb2FkIE1DUEIgZnJvbSAke2Vycm9yLnVybH06ICR7ZXJyb3IucmVhc29ufWBcbiAgICBjYXNlICdtY3BiLWV4dHJhY3QtZmFpbGVkJzpcbiAgICAgIHJldHVybiBgRmFpbGVkIHRvIGV4dHJhY3QgTUNQQiAke2Vycm9yLm1jcGJQYXRofTogJHtlcnJvci5yZWFzb259YFxuICAgIGNhc2UgJ21jcGItaW52YWxpZC1tYW5pZmVzdCc6XG4gICAgICByZXR1cm4gYE1DUEIgbWFuaWZlc3QgaW52YWxpZCBhdCAke2Vycm9yLm1jcGJQYXRofTogJHtlcnJvci52YWxpZGF0aW9uRXJyb3J9YFxuICAgIGNhc2UgJ21hcmtldHBsYWNlLWJsb2NrZWQtYnktcG9saWN5JzpcbiAgICAgIHJldHVybiBlcnJvci5ibG9ja2VkQnlCbG9ja2xpc3RcbiAgICAgICAgPyBgTWFya2V0cGxhY2UgXCIke2Vycm9yLm1hcmtldHBsYWNlfVwiIGlzIGJsb2NrZWQgYnkgZW50ZXJwcmlzZSBwb2xpY3lgXG4gICAgICAgIDogYE1hcmtldHBsYWNlIFwiJHtlcnJvci5tYXJrZXRwbGFjZX1cIiBpcyBub3QgaW4gdGhlIGFsbG93ZWQgbWFya2V0cGxhY2UgbGlzdGBcbiAgICBjYXNlICdkZXBlbmRlbmN5LXVuc2F0aXNmaWVkJzpcbiAgICAgIHJldHVybiBlcnJvci5yZWFzb24gPT09ICdub3QtZW5hYmxlZCdcbiAgICAgICAgPyBgRGVwZW5kZW5jeSBcIiR7ZXJyb3IuZGVwZW5kZW5jeX1cIiBpcyBkaXNhYmxlZGBcbiAgICAgICAgOiBgRGVwZW5kZW5jeSBcIiR7ZXJyb3IuZGVwZW5kZW5jeX1cIiBpcyBub3QgaW5zdGFsbGVkYFxuICAgIGNhc2UgJ2xzcC1jb25maWctaW52YWxpZCc6XG4gICAgICByZXR1cm4gYEludmFsaWQgTFNQIHNlcnZlciBjb25maWcgZm9yIFwiJHtlcnJvci5zZXJ2ZXJOYW1lfVwiOiAke2Vycm9yLnZhbGlkYXRpb25FcnJvcn1gXG4gICAgY2FzZSAnbHNwLXNlcnZlci1zdGFydC1mYWlsZWQnOlxuICAgICAgcmV0dXJuIGBMU1Agc2VydmVyIFwiJHtlcnJvci5zZXJ2ZXJOYW1lfVwiIGZhaWxlZCB0byBzdGFydDogJHtlcnJvci5yZWFzb259YFxuICAgIGNhc2UgJ2xzcC1zZXJ2ZXItY3Jhc2hlZCc6XG4gICAgICByZXR1cm4gZXJyb3Iuc2lnbmFsXG4gICAgICAgID8gYExTUCBzZXJ2ZXIgXCIke2Vycm9yLnNlcnZlck5hbWV9XCIgY3Jhc2hlZCB3aXRoIHNpZ25hbCAke2Vycm9yLnNpZ25hbH1
Line truncated
|
||||
@@ -0,0 +1,357 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import figures from 'figures';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { Dialog } from '../../components/design-system/Dialog.js';
|
||||
import { stringWidth } from '../../ink/stringWidth.js';
|
||||
// eslint-disable-next-line custom-rules/prefer-use-keybindings -- raw text input for config dialog
|
||||
import { Box, Text, useInput } from '../../ink.js';
|
||||
import { useKeybinding, useKeybindings } from '../../keybindings/useKeybinding.js';
|
||||
import { isEnvTruthy } from '../../utils/envUtils.js';
|
||||
import type { PluginOptionSchema, PluginOptionValues } from '../../utils/plugins/pluginOptionsStorage.js';
|
||||
|
||||
/**
|
||||
* Build the onSave payload from collected string inputs.
|
||||
*
|
||||
* Sensitive fields are never prepopulated in the text buffer (security), so
|
||||
* by the time the user reaches the last field every sensitive field they
|
||||
* stepped through contains '' in collected. To avoid silently wiping saved
|
||||
* secrets on reconfigure: if a sensitive field is '' AND initialValues has
|
||||
* a value for it, OMIT the key entirely. savePluginOptions only writes keys
|
||||
* it receives, so omitting = keep existing.
|
||||
*
|
||||
* Exported for unit testing.
|
||||
*/
|
||||
export function buildFinalValues(fields: string[], collected: Record<string, string>, configSchema: PluginOptionSchema, initialValues: PluginOptionValues | undefined): PluginOptionValues {
|
||||
const finalValues: PluginOptionValues = {};
|
||||
for (const fieldKey of fields) {
|
||||
const schema = configSchema[fieldKey];
|
||||
const value = collected[fieldKey] ?? '';
|
||||
if (schema?.sensitive === true && value === '' && initialValues?.[fieldKey] !== undefined) {
|
||||
continue;
|
||||
}
|
||||
if (schema?.type === 'number') {
|
||||
// Number('') returns 0, not NaN — omit blank number inputs so
|
||||
// validateUserConfig's required check actually catches them.
|
||||
if (value.trim() === '') continue;
|
||||
const num = Number(value);
|
||||
finalValues[fieldKey] = Number.isNaN(num) ? value : num;
|
||||
} else if (schema?.type === 'boolean') {
|
||||
finalValues[fieldKey] = isEnvTruthy(value);
|
||||
} else {
|
||||
finalValues[fieldKey] = value;
|
||||
}
|
||||
}
|
||||
return finalValues;
|
||||
}
|
||||
type Props = {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
configSchema: PluginOptionSchema;
|
||||
/** Pre-fill fields when reconfiguring. Sensitive fields are not prepopulated. */
|
||||
initialValues?: PluginOptionValues;
|
||||
onSave: (config: PluginOptionValues) => void;
|
||||
onCancel: () => void;
|
||||
};
|
||||
export function PluginOptionsDialog(t0) {
|
||||
const $ = _c(70);
|
||||
const {
|
||||
title,
|
||||
subtitle,
|
||||
configSchema,
|
||||
initialValues,
|
||||
onSave,
|
||||
onCancel
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] !== configSchema) {
|
||||
t1 = Object.keys(configSchema);
|
||||
$[0] = configSchema;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const fields = t1;
|
||||
let t2;
|
||||
if ($[2] !== configSchema || $[3] !== initialValues) {
|
||||
t2 = key => {
|
||||
if (configSchema[key]?.sensitive === true) {
|
||||
return "";
|
||||
}
|
||||
const v = initialValues?.[key];
|
||||
return v === undefined ? "" : String(v);
|
||||
};
|
||||
$[2] = configSchema;
|
||||
$[3] = initialValues;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
const initialFor = t2;
|
||||
const [currentFieldIndex, setCurrentFieldIndex] = useState(0);
|
||||
let t3;
|
||||
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = {};
|
||||
$[5] = t3;
|
||||
} else {
|
||||
t3 = $[5];
|
||||
}
|
||||
const [values, setValues] = useState(t3);
|
||||
let t4;
|
||||
if ($[6] !== fields[0] || $[7] !== initialFor) {
|
||||
t4 = () => fields[0] ? initialFor(fields[0]) : "";
|
||||
$[6] = fields[0];
|
||||
$[7] = initialFor;
|
||||
$[8] = t4;
|
||||
} else {
|
||||
t4 = $[8];
|
||||
}
|
||||
const [currentInput, setCurrentInput] = useState(t4);
|
||||
const currentField = fields[currentFieldIndex];
|
||||
const fieldSchema = currentField ? configSchema[currentField] : null;
|
||||
let t5;
|
||||
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = {
|
||||
context: "Settings"
|
||||
};
|
||||
$[9] = t5;
|
||||
} else {
|
||||
t5 = $[9];
|
||||
}
|
||||
useKeybinding("confirm:no", onCancel, t5);
|
||||
let t6;
|
||||
if ($[10] !== currentField || $[11] !== currentFieldIndex || $[12] !== currentInput || $[13] !== fields || $[14] !== initialFor) {
|
||||
t6 = () => {
|
||||
if (currentFieldIndex < fields.length - 1 && currentField) {
|
||||
setValues(prev => ({
|
||||
...prev,
|
||||
[currentField]: currentInput
|
||||
}));
|
||||
setCurrentFieldIndex(_temp);
|
||||
const nextKey = fields[currentFieldIndex + 1];
|
||||
setCurrentInput(nextKey ? initialFor(nextKey) : "");
|
||||
}
|
||||
};
|
||||
$[10] = currentField;
|
||||
$[11] = currentFieldIndex;
|
||||
$[12] = currentInput;
|
||||
$[13] = fields;
|
||||
$[14] = initialFor;
|
||||
$[15] = t6;
|
||||
} else {
|
||||
t6 = $[15];
|
||||
}
|
||||
const handleNextField = t6;
|
||||
let t7;
|
||||
if ($[16] !== configSchema || $[17] !== currentField || $[18] !== currentFieldIndex || $[19] !== currentInput || $[20] !== fields || $[21] !== initialFor || $[22] !== initialValues || $[23] !== onSave || $[24] !== values) {
|
||||
t7 = () => {
|
||||
if (!currentField) {
|
||||
return;
|
||||
}
|
||||
const newValues = {
|
||||
...values,
|
||||
[currentField]: currentInput
|
||||
};
|
||||
if (currentFieldIndex === fields.length - 1) {
|
||||
onSave(buildFinalValues(fields, newValues, configSchema, initialValues));
|
||||
} else {
|
||||
setValues(newValues);
|
||||
setCurrentFieldIndex(_temp2);
|
||||
const nextKey_0 = fields[currentFieldIndex + 1];
|
||||
setCurrentInput(nextKey_0 ? initialFor(nextKey_0) : "");
|
||||
}
|
||||
};
|
||||
$[16] = configSchema;
|
||||
$[17] = currentField;
|
||||
$[18] = currentFieldIndex;
|
||||
$[19] = currentInput;
|
||||
$[20] = fields;
|
||||
$[21] = initialFor;
|
||||
$[22] = initialValues;
|
||||
$[23] = onSave;
|
||||
$[24] = values;
|
||||
$[25] = t7;
|
||||
} else {
|
||||
t7 = $[25];
|
||||
}
|
||||
const handleConfirm = t7;
|
||||
let t8;
|
||||
if ($[26] !== handleConfirm || $[27] !== handleNextField) {
|
||||
t8 = {
|
||||
"confirm:nextField": handleNextField,
|
||||
"confirm:yes": handleConfirm
|
||||
};
|
||||
$[26] = handleConfirm;
|
||||
$[27] = handleNextField;
|
||||
$[28] = t8;
|
||||
} else {
|
||||
t8 = $[28];
|
||||
}
|
||||
let t9;
|
||||
if ($[29] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t9 = {
|
||||
context: "Confirmation"
|
||||
};
|
||||
$[29] = t9;
|
||||
} else {
|
||||
t9 = $[29];
|
||||
}
|
||||
useKeybindings(t8, t9);
|
||||
let t10;
|
||||
if ($[30] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t10 = (char, key_0) => {
|
||||
if (key_0.backspace || key_0.delete) {
|
||||
setCurrentInput(_temp3);
|
||||
return;
|
||||
}
|
||||
if (char && !key_0.ctrl && !key_0.meta && !key_0.tab && !key_0.return) {
|
||||
setCurrentInput(prev_3 => prev_3 + char);
|
||||
}
|
||||
};
|
||||
$[30] = t10;
|
||||
} else {
|
||||
t10 = $[30];
|
||||
}
|
||||
useInput(t10);
|
||||
if (!fieldSchema || !currentField) {
|
||||
return null;
|
||||
}
|
||||
const isSensitive = fieldSchema.sensitive === true;
|
||||
const isRequired = fieldSchema.required === true;
|
||||
let t11;
|
||||
if ($[31] !== currentInput || $[32] !== isSensitive) {
|
||||
t11 = isSensitive ? "*".repeat(stringWidth(currentInput)) : currentInput;
|
||||
$[31] = currentInput;
|
||||
$[32] = isSensitive;
|
||||
$[33] = t11;
|
||||
} else {
|
||||
t11 = $[33];
|
||||
}
|
||||
const displayValue = t11;
|
||||
const t12 = fieldSchema.title || currentField;
|
||||
let t13;
|
||||
if ($[34] !== isRequired) {
|
||||
t13 = isRequired && <Text color="error"> *</Text>;
|
||||
$[34] = isRequired;
|
||||
$[35] = t13;
|
||||
} else {
|
||||
t13 = $[35];
|
||||
}
|
||||
let t14;
|
||||
if ($[36] !== t12 || $[37] !== t13) {
|
||||
t14 = <Text bold={true}>{t12}{t13}</Text>;
|
||||
$[36] = t12;
|
||||
$[37] = t13;
|
||||
$[38] = t14;
|
||||
} else {
|
||||
t14 = $[38];
|
||||
}
|
||||
let t15;
|
||||
if ($[39] !== fieldSchema.description) {
|
||||
t15 = fieldSchema.description && <Text dimColor={true}>{fieldSchema.description}</Text>;
|
||||
$[39] = fieldSchema.description;
|
||||
$[40] = t15;
|
||||
} else {
|
||||
t15 = $[40];
|
||||
}
|
||||
let t16;
|
||||
if ($[41] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t16 = <Text>{figures.pointerSmall} </Text>;
|
||||
$[41] = t16;
|
||||
} else {
|
||||
t16 = $[41];
|
||||
}
|
||||
let t17;
|
||||
if ($[42] !== displayValue) {
|
||||
t17 = <Text>{displayValue}</Text>;
|
||||
$[42] = displayValue;
|
||||
$[43] = t17;
|
||||
} else {
|
||||
t17 = $[43];
|
||||
}
|
||||
let t18;
|
||||
if ($[44] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t18 = <Text>█</Text>;
|
||||
$[44] = t18;
|
||||
} else {
|
||||
t18 = $[44];
|
||||
}
|
||||
let t19;
|
||||
if ($[45] !== t17) {
|
||||
t19 = <Box marginTop={1}>{t16}{t17}{t18}</Box>;
|
||||
$[45] = t17;
|
||||
$[46] = t19;
|
||||
} else {
|
||||
t19 = $[46];
|
||||
}
|
||||
let t20;
|
||||
if ($[47] !== t14 || $[48] !== t15 || $[49] !== t19) {
|
||||
t20 = <Box flexDirection="column">{t14}{t15}{t19}</Box>;
|
||||
$[47] = t14;
|
||||
$[48] = t15;
|
||||
$[49] = t19;
|
||||
$[50] = t20;
|
||||
} else {
|
||||
t20 = $[50];
|
||||
}
|
||||
const t21 = currentFieldIndex + 1;
|
||||
let t22;
|
||||
if ($[51] !== fields.length || $[52] !== t21) {
|
||||
t22 = <Text dimColor={true}>Field {t21} of {fields.length}</Text>;
|
||||
$[51] = fields.length;
|
||||
$[52] = t21;
|
||||
$[53] = t22;
|
||||
} else {
|
||||
t22 = $[53];
|
||||
}
|
||||
let t23;
|
||||
if ($[54] !== currentFieldIndex || $[55] !== fields.length) {
|
||||
t23 = currentFieldIndex < fields.length - 1 && <Text dimColor={true}>Tab: Next field · Enter: Save and continue</Text>;
|
||||
$[54] = currentFieldIndex;
|
||||
$[55] = fields.length;
|
||||
$[56] = t23;
|
||||
} else {
|
||||
t23 = $[56];
|
||||
}
|
||||
let t24;
|
||||
if ($[57] !== currentFieldIndex || $[58] !== fields.length) {
|
||||
t24 = currentFieldIndex === fields.length - 1 && <Text dimColor={true}>Enter: Save configuration</Text>;
|
||||
$[57] = currentFieldIndex;
|
||||
$[58] = fields.length;
|
||||
$[59] = t24;
|
||||
} else {
|
||||
t24 = $[59];
|
||||
}
|
||||
let t25;
|
||||
if ($[60] !== t22 || $[61] !== t23 || $[62] !== t24) {
|
||||
t25 = <Box flexDirection="column">{t22}{t23}{t24}</Box>;
|
||||
$[60] = t22;
|
||||
$[61] = t23;
|
||||
$[62] = t24;
|
||||
$[63] = t25;
|
||||
} else {
|
||||
t25 = $[63];
|
||||
}
|
||||
let t26;
|
||||
if ($[64] !== onCancel || $[65] !== subtitle || $[66] !== t20 || $[67] !== t25 || $[68] !== title) {
|
||||
t26 = <Dialog title={title} subtitle={subtitle} onCancel={onCancel} isCancelActive={false}>{t20}{t25}</Dialog>;
|
||||
$[64] = onCancel;
|
||||
$[65] = subtitle;
|
||||
$[66] = t20;
|
||||
$[67] = t25;
|
||||
$[68] = title;
|
||||
$[69] = t26;
|
||||
} else {
|
||||
t26 = $[69];
|
||||
}
|
||||
return t26;
|
||||
}
|
||||
function _temp3(prev_2) {
|
||||
return prev_2.slice(0, -1);
|
||||
}
|
||||
function _temp2(prev_1) {
|
||||
return prev_1 + 1;
|
||||
}
|
||||
function _temp(prev_0) {
|
||||
return prev_0 + 1;
|
||||
}
|
||||
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJmaWd1cmVzIiwiUmVhY3QiLCJ1c2VDYWxsYmFjayIsInVzZVN0YXRlIiwiRGlhbG9nIiwic3RyaW5nV2lkdGgiLCJCb3giLCJUZXh0IiwidXNlSW5wdXQiLCJ1c2VLZXliaW5kaW5nIiwidXNlS2V5YmluZGluZ3MiLCJpc0VudlRydXRoeSIsIlBsdWdpbk9wdGlvblNjaGVtYSIsIlBsdWdpbk9wdGlvblZhbHVlcyIsImJ1aWxkRmluYWxWYWx1ZXMiLCJmaWVsZHMiLCJjb2xsZWN0ZWQiLCJSZWNvcmQiLCJjb25maWdTY2hlbWEiLCJpbml0aWFsVmFsdWVzIiwiZmluYWxWYWx1ZXMiLCJmaWVsZEtleSIsInNjaGVtYSIsInZhbHVlIiwic2Vuc2l0aXZlIiwidW5kZWZpbmVkIiwidHlwZSIsInRyaW0iLCJudW0iLCJOdW1iZXIiLCJpc05hTiIsIlByb3BzIiwidGl0bGUiLCJzdWJ0aXRsZSIsIm9uU2F2ZSIsImNvbmZpZyIsIm9uQ2FuY2VsIiwiUGx1Z2luT3B0aW9uc0RpYWxvZyIsInQwIiwiJCIsIl9jIiwidDEiLCJPYmplY3QiLCJrZXlzIiwidDIiLCJrZXkiLCJ2IiwiU3RyaW5nIiwiaW5pdGlhbEZvciIsImN1cnJlbnRGaWVsZEluZGV4Iiwic2V0Q3VycmVudEZpZWxkSW5kZXgiLCJ0MyIsIlN5bWJvbCIsImZvciIsInZhbHVlcyIsInNldFZhbHVlcyIsInQ0IiwiY3VycmVudElucHV0Iiwic2V0Q3VycmVudElucHV0IiwiY3VycmVudEZpZWxkIiwiZmllbGRTY2hlbWEiLCJ0NSIsImNvbnRleHQiLCJ0NiIsImxlbmd0aCIsInByZXYiLCJfdGVtcCIsIm5leHRLZXkiLCJoYW5kbGVOZXh0RmllbGQiLCJ0NyIsIm5ld1ZhbHVlcyIsIl90ZW1wMiIsIm5leHRLZXlfMCIsImhhbmRsZUNvbmZpcm0iLCJ0OCIsInQ5IiwidDEwIiwiY2hhciIsImtleV8wIiwiYmFja3NwYWNlIiwiZGVsZXRlIiwiX3RlbXAzIiwiY3RybCIsIm1ldGEiLCJ0YWIiLCJyZXR1cm4iLCJwcmV2XzMiLCJpc1NlbnNpdGl2ZSIsImlzUmVxdWlyZWQiLCJyZXF1aXJlZCIsInQxMSIsInJlcGVhdCIsImRpc3BsYXlWYWx1ZSIsInQxMiIsInQxMyIsInQxNCIsInQxNSIsImRlc2NyaXB0aW9uIiwidDE2IiwicG9pbnRlclNtYWxsIiwidDE3IiwidDE4IiwidDE5IiwidDIwIiwidDIxIiwidDIyIiwidDIzIiwidDI0IiwidDI1IiwidDI2IiwicHJldl8yIiwic2xpY2UiLCJwcmV2XzEiLCJwcmV2XzAiXSwic291cmNlcyI6WyJQbHVnaW5PcHRpb25zRGlhbG9nLnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgZmlndXJlcyBmcm9tICdmaWd1cmVzJ1xuaW1wb3J0IFJlYWN0LCB7IHVzZUNhbGxiYWNrLCB1c2VTdGF0ZSB9IGZyb20gJ3JlYWN0J1xuaW1wb3J0IHsgRGlhbG9nIH0gZnJvbSAnLi4vLi4vY29tcG9uZW50cy9kZXNpZ24tc3lzdGVtL0RpYWxvZy5qcydcbmltcG9ydCB7IHN0cmluZ1dpZHRoIH0gZnJvbSAnLi4vLi4vaW5rL3N0cmluZ1dpZHRoLmpzJ1xuLy8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIGN1c3RvbS1ydWxlcy9wcmVmZXItdXNlLWtleWJpbmRpbmdzIC0tIHJhdyB0ZXh0IGlucHV0IGZvciBjb25maWcgZGlhbG9nXG5pbXBvcnQgeyBCb3gsIFRleHQsIHVzZUlucHV0IH0gZnJvbSAnLi4vLi4vaW5rLmpzJ1xuaW1wb3J0IHtcbiAgdXNlS2V5YmluZGluZyxcbiAgdXNlS2V5YmluZGluZ3MsXG59IGZyb20gJy4uLy4uL2tleWJpbmRpbmdzL3VzZUtleWJpbmRpbmcuanMnXG5pbXBvcnQgeyBpc0VudlRydXRoeSB9IGZyb20gJy4uLy4uL3V0aWxzL2VudlV0aWxzLmpzJ1xuaW1wb3J0IHR5cGUge1xuICBQbHVnaW5PcHRpb25TY2hlbWEsXG4gIFBsdWdpbk9wdGlvblZhbHVlcyxcbn0gZnJvbSAnLi4vLi4vdXRpbHMvcGx1Z2lucy9wbHVnaW5PcHRpb25zU3RvcmFnZS5qcydcblxuLyoqXG4gKiBCdWlsZCB0aGUgb25TYXZlIHBheWxvYWQgZnJvbSBjb2xsZWN0ZWQgc3RyaW5nIGlucHV0cy5cbiAqXG4gKiBTZW5zaXRpdmUgZmllbGRzIGFyZSBuZXZlciBwcmVwb3B1bGF0ZWQgaW4gdGhlIHRleHQgYnVmZmVyIChzZWN1cml0eSksIHNvXG4gKiBieSB0aGUgdGltZSB0aGUgdXNlciByZWFjaGVzIHRoZSBsYXN0IGZpZWxkIGV2ZXJ5IHNlbnNpdGl2ZSBmaWVsZCB0aGV5XG4gKiBzdGVwcGVkIHRocm91Z2ggY29udGFpbnMgJycgaW4gY29sbGVjdGVkLiBUbyBhdm9pZCBzaWxlbnRseSB3aXBpbmcgc2F2ZWRcbiAqIHNlY3JldHMgb24gcmVjb25maWd1cmU6IGlmIGEgc2Vuc2l0aXZlIGZpZWxkIGlzICcnIEFORCBpbml0aWFsVmFsdWVzIGhhc1xuICogYSB2YWx1ZSBmb3IgaXQsIE9NSVQgdGhlIGtleSBlbnRpcmVseS4gc2F2ZVBsdWdpbk9wdGlvbnMgb25seSB3cml0ZXMga2V5c1xuICogaXQgcmVjZWl2ZXMsIHNvIG9taXR0aW5nID0ga2VlcCBleGlzdGluZy5cbiAqXG4gKiBFeHBvcnRlZCBmb3IgdW5pdCB0ZXN0aW5nLlxuICovXG5leHBvcnQgZnVuY3Rpb24gYnVpbGRGaW5hbFZhbHVlcyhcbiAgZmllbGRzOiBzdHJpbmdbXSxcbiAgY29sbGVjdGVkOiBSZWNvcmQ8c3RyaW5nLCBzdHJpbmc+LFxuICBjb25maWdTY2hlbWE6IFBsdWdpbk9wdGlvblNjaGVtYSxcbiAgaW5pdGlhbFZhbHVlczogUGx1Z2luT3B0aW9uVmFsdWVzIHwgdW5kZWZpbmVkLFxuKTogUGx1Z2luT3B0aW9uVmFsdWVzIHtcbiAgY29uc3QgZmluYWxWYWx1ZXM6IFBsdWdpbk9wdGlvblZhbHVlcyA9IHt9XG4gIGZvciAoY29uc3QgZmllbGRLZXkgb2YgZmllbGRzKSB7XG4gICAgY29uc3Qgc2NoZW1hID0gY29uZmlnU2NoZW1hW2ZpZWxkS2V5XVxuICAgIGNvbnN0IHZhbHVlID0gY29sbGVjdGVkW2ZpZWxkS2V5XSA/PyAnJ1xuXG4gICAgaWYgKFxuICAgICAgc2NoZW1hPy5zZW5zaXRpdmUgPT09IHRydWUgJiZcbiAgICAgIHZhbHVlID09PSAnJyAmJlxuICAgICAgaW5pdGlhbFZhbHVlcz8uW2ZpZWxkS2V5XSAhPT0gdW5kZWZpbmVkXG4gICAgKSB7XG4gICAgICBjb250aW51ZVxuICAgIH1cblxuICAgIGlmIChzY2hlbWE/LnR5cGUgPT09ICdudW1iZXInKSB7XG4gICAgICAvLyBOdW1iZXIoJycpIHJldHVybnMgMCwgbm90IE5hTiDigJQgb21pdCBibGFuayBudW1iZXIgaW5wdXRzIHNvXG4gICAgICAvLyB2YWxpZGF0ZVVzZXJDb25maWcncyByZXF1aXJlZCBjaGVjayBhY3R1YWxseSBjYXRjaGVzIHRoZW0uXG4gICAgICBpZiAodmFsdWUudHJpbSgpID09PSAnJykgY29udGludWVcbiAgICAgIGNvbnN0IG51bSA9IE51bWJlcih2YWx1ZSlcbiAgICAgIGZpbmFsVmFsdWVzW2ZpZWxkS2V5XSA9IE51bWJlci5pc05hTihudW0pID8gdmFsdWUgOiBudW1cbiAgICB9IGVsc2UgaWYgKHNjaGVtYT8udHlwZSA9PT0gJ2Jvb2xlYW4nKSB7XG4gICAgICBmaW5hbFZhbHVlc1tmaWVsZEtleV0gPSBpc0VudlRydXRoeSh2YWx1ZSlcbiAgICB9IGVsc2Uge1xuICAgICAgZmluYWxWYWx1ZXNbZmllbGRLZXldID0gdmFsdWVcbiAgICB9XG4gIH1cbiAgcmV0dXJuIGZpbmFsVmFsdWVzXG59XG5cbnR5cGUgUHJvcHMgPSB7XG4gIHRpdGxlOiBzdHJpbmdcbiAgc3VidGl0bGU6IHN0cmluZ1xuICBjb25maWdTY2hlbWE6IFBsdWdpbk9wdGlvblNjaGVtYVxuICAvKiogUHJlLWZpbGwgZmllbGRzIHdoZW4gcmVjb25maWd1cmluZy4gU2Vuc2l0aXZlIGZpZWxkcyBhcmUgbm90IHByZXBvcHVsYXRlZC4gKi9cbiAgaW5pdGlhbFZhbHVlcz86IFBsdWdpbk9wdGlvblZhbHVlc1xuICBvblNhdmU6IChjb25maWc6IFBsdWdpbk9wdGlvblZhbHVlcykgPT4gdm9pZFxuICBvbkNhbmNlbDogKCkgPT4gdm9pZFxufVxuXG5leHBvcnQgZnVuY3Rpb24gUGx1Z2luT3B0aW9
Line truncated
|
||||
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* Post-install/post-enable config prompt.
|
||||
*
|
||||
* Given a LoadedPlugin, checks both the top-level manifest.userConfig and the
|
||||
* channel-specific userConfig. Walks PluginOptionsDialog through each
|
||||
* unconfigured item, saving via the appropriate storage function. Calls
|
||||
* onDone('skipped') immediately if nothing needs filling.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import type { LoadedPlugin } from '../../types/plugin.js';
|
||||
import { errorMessage } from '../../utils/errors.js';
|
||||
import { loadMcpServerUserConfig, saveMcpServerUserConfig } from '../../utils/plugins/mcpbHandler.js';
|
||||
import { getUnconfiguredChannels, type UnconfiguredChannel } from '../../utils/plugins/mcpPluginIntegration.js';
|
||||
import { loadAllPlugins } from '../../utils/plugins/pluginLoader.js';
|
||||
import { getUnconfiguredOptions, loadPluginOptions, type PluginOptionSchema, type PluginOptionValues, savePluginOptions } from '../../utils/plugins/pluginOptionsStorage.js';
|
||||
import { PluginOptionsDialog } from './PluginOptionsDialog.js';
|
||||
|
||||
/**
|
||||
* Post-install lookup: return the LoadedPlugin for the just-installed
|
||||
* pluginId so the caller can divert to PluginOptionsFlow. Returns undefined
|
||||
* if the plugin somehow didn't make it into the fresh load — callers treat
|
||||
* undefined as "carry on closing."
|
||||
*
|
||||
* Install should have cleared caches already; loadAllPlugins reads fresh.
|
||||
*/
|
||||
export async function findPluginOptionsTarget(pluginId: string): Promise<LoadedPlugin | undefined> {
|
||||
const {
|
||||
enabled,
|
||||
disabled
|
||||
} = await loadAllPlugins();
|
||||
return [...enabled, ...disabled].find(p => p.repository === pluginId || p.source === pluginId);
|
||||
}
|
||||
|
||||
/**
|
||||
* A single dialog step in the walk. Top-level options and channels both
|
||||
* collapse to this shape — the only difference is which save function runs.
|
||||
*/
|
||||
type ConfigStep = {
|
||||
key: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
schema: PluginOptionSchema;
|
||||
/** Returns any already-saved values so PluginOptionsDialog can pre-fill and
|
||||
* skip unchanged sensitive fields on reconfigure. */
|
||||
load: () => PluginOptionValues | undefined;
|
||||
save: (values: PluginOptionValues) => void;
|
||||
};
|
||||
type Props = {
|
||||
plugin: LoadedPlugin;
|
||||
/** `name@marketplace` — the savePluginOptions / saveMcpServerUserConfig key. */
|
||||
pluginId: string;
|
||||
/**
|
||||
* `configured` = user filled all fields. `skipped` = nothing needed
|
||||
* configuring, or user hit cancel. `error` = save threw.
|
||||
*/
|
||||
onDone: (outcome: 'configured' | 'skipped' | 'error', detail?: string) => void;
|
||||
};
|
||||
export function PluginOptionsFlow({
|
||||
plugin,
|
||||
pluginId,
|
||||
onDone
|
||||
}: Props): React.ReactNode {
|
||||
// Build the step list once at mount. Re-calling after a save would drop the
|
||||
// item we just configured.
|
||||
const [steps] = React.useState<ConfigStep[]>(() => {
|
||||
const result: ConfigStep[] = [];
|
||||
|
||||
// Top-level manifest.userConfig
|
||||
const unconfigured = getUnconfiguredOptions(plugin);
|
||||
if (Object.keys(unconfigured).length > 0) {
|
||||
result.push({
|
||||
key: 'top-level',
|
||||
title: `Configure ${plugin.name}`,
|
||||
subtitle: 'Plugin options',
|
||||
schema: unconfigured,
|
||||
load: () => loadPluginOptions(pluginId),
|
||||
save: values => savePluginOptions(pluginId, values, plugin.manifest.userConfig!)
|
||||
});
|
||||
}
|
||||
|
||||
// Per-channel userConfig (assistant-mode channels)
|
||||
const channels: UnconfiguredChannel[] = getUnconfiguredChannels(plugin);
|
||||
for (const channel of channels) {
|
||||
result.push({
|
||||
key: `channel:${channel.server}`,
|
||||
title: `Configure ${channel.displayName}`,
|
||||
subtitle: `Plugin: ${plugin.name}`,
|
||||
schema: channel.configSchema,
|
||||
load: () => loadMcpServerUserConfig(pluginId, channel.server) ?? undefined,
|
||||
save: values_0 => saveMcpServerUserConfig(pluginId, channel.server, values_0, channel.configSchema)
|
||||
});
|
||||
}
|
||||
return result;
|
||||
});
|
||||
const [index, setIndex] = React.useState(0);
|
||||
|
||||
// Latest-ref: lets the effect close over the current onDone without
|
||||
// re-running when the parent re-renders.
|
||||
const onDoneRef = React.useRef(onDone);
|
||||
onDoneRef.current = onDone;
|
||||
|
||||
// Nothing to configure → tell the caller and render nothing. Effect,
|
||||
// not inline call: calling setState in the parent during our render
|
||||
// is a React rules-of-hooks violation.
|
||||
React.useEffect(() => {
|
||||
if (steps.length === 0) {
|
||||
onDoneRef.current('skipped');
|
||||
}
|
||||
}, [steps.length]);
|
||||
if (steps.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const current = steps[index]!;
|
||||
function handleSave(values_1: PluginOptionValues): void {
|
||||
try {
|
||||
current.save(values_1);
|
||||
} catch (err) {
|
||||
onDone('error', errorMessage(err));
|
||||
return;
|
||||
}
|
||||
const next = index + 1;
|
||||
if (next < steps.length) {
|
||||
setIndex(next);
|
||||
} else {
|
||||
onDone('configured');
|
||||
}
|
||||
}
|
||||
|
||||
// key forces a remount when advancing to the next step — React would
|
||||
// otherwise reuse the instance and carry PluginOptionsDialog's
|
||||
// internal useState (field index, typed values) over.
|
||||
return <PluginOptionsDialog key={current.key} title={current.title} subtitle={current.subtitle} configSchema={current.schema} initialValues={current.load()} onSave={handleSave} onCancel={() => onDone('skipped')} />;
|
||||
}
|
||||
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsIkxvYWRlZFBsdWdpbiIsImVycm9yTWVzc2FnZSIsImxvYWRNY3BTZXJ2ZXJVc2VyQ29uZmlnIiwic2F2ZU1jcFNlcnZlclVzZXJDb25maWciLCJnZXRVbmNvbmZpZ3VyZWRDaGFubmVscyIsIlVuY29uZmlndXJlZENoYW5uZWwiLCJsb2FkQWxsUGx1Z2lucyIsImdldFVuY29uZmlndXJlZE9wdGlvbnMiLCJsb2FkUGx1Z2luT3B0aW9ucyIsIlBsdWdpbk9wdGlvblNjaGVtYSIsIlBsdWdpbk9wdGlvblZhbHVlcyIsInNhdmVQbHVnaW5PcHRpb25zIiwiUGx1Z2luT3B0aW9uc0RpYWxvZyIsImZpbmRQbHVnaW5PcHRpb25zVGFyZ2V0IiwicGx1Z2luSWQiLCJQcm9taXNlIiwiZW5hYmxlZCIsImRpc2FibGVkIiwiZmluZCIsInAiLCJyZXBvc2l0b3J5Iiwic291cmNlIiwiQ29uZmlnU3RlcCIsImtleSIsInRpdGxlIiwic3VidGl0bGUiLCJzY2hlbWEiLCJsb2FkIiwic2F2ZSIsInZhbHVlcyIsIlByb3BzIiwicGx1Z2luIiwib25Eb25lIiwib3V0Y29tZSIsImRldGFpbCIsIlBsdWdpbk9wdGlvbnNGbG93IiwiUmVhY3ROb2RlIiwic3RlcHMiLCJ1c2VTdGF0ZSIsInJlc3VsdCIsInVuY29uZmlndXJlZCIsIk9iamVjdCIsImtleXMiLCJsZW5ndGgiLCJwdXNoIiwibmFtZSIsIm1hbmlmZXN0IiwidXNlckNvbmZpZyIsImNoYW5uZWxzIiwiY2hhbm5lbCIsInNlcnZlciIsImRpc3BsYXlOYW1lIiwiY29uZmlnU2NoZW1hIiwidW5kZWZpbmVkIiwiaW5kZXgiLCJzZXRJbmRleCIsIm9uRG9uZVJlZiIsInVzZVJlZiIsImN1cnJlbnQiLCJ1c2VFZmZlY3QiLCJoYW5kbGVTYXZlIiwiZXJyIiwibmV4dCJdLCJzb3VyY2VzIjpbIlBsdWdpbk9wdGlvbnNGbG93LnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyIvKipcbiAqIFBvc3QtaW5zdGFsbC9wb3N0LWVuYWJsZSBjb25maWcgcHJvbXB0LlxuICpcbiAqIEdpdmVuIGEgTG9hZGVkUGx1Z2luLCBjaGVja3MgYm90aCB0aGUgdG9wLWxldmVsIG1hbmlmZXN0LnVzZXJDb25maWcgYW5kIHRoZVxuICogY2hhbm5lbC1zcGVjaWZpYyB1c2VyQ29uZmlnLiBXYWxrcyBQbHVnaW5PcHRpb25zRGlhbG9nIHRocm91Z2ggZWFjaFxuICogdW5jb25maWd1cmVkIGl0ZW0sIHNhdmluZyB2aWEgdGhlIGFwcHJvcHJpYXRlIHN0b3JhZ2UgZnVuY3Rpb24uIENhbGxzXG4gKiBvbkRvbmUoJ3NraXBwZWQnKSBpbW1lZGlhdGVseSBpZiBub3RoaW5nIG5lZWRzIGZpbGxpbmcuXG4gKi9cblxuaW1wb3J0ICogYXMgUmVhY3QgZnJvbSAncmVhY3QnXG5pbXBvcnQgdHlwZSB7IExvYWRlZFBsdWdpbiB9IGZyb20gJy4uLy4uL3R5cGVzL3BsdWdpbi5qcydcbmltcG9ydCB7IGVycm9yTWVzc2FnZSB9IGZyb20gJy4uLy4uL3V0aWxzL2Vycm9ycy5qcydcbmltcG9ydCB7XG4gIGxvYWRNY3BTZXJ2ZXJVc2VyQ29uZmlnLFxuICBzYXZlTWNwU2VydmVyVXNlckNvbmZpZyxcbn0gZnJvbSAnLi4vLi4vdXRpbHMvcGx1Z2lucy9tY3BiSGFuZGxlci5qcydcbmltcG9ydCB7XG4gIGdldFVuY29uZmlndXJlZENoYW5uZWxzLFxuICB0eXBlIFVuY29uZmlndXJlZENoYW5uZWwsXG59IGZyb20gJy4uLy4uL3V0aWxzL3BsdWdpbnMvbWNwUGx1Z2luSW50ZWdyYXRpb24uanMnXG5pbXBvcnQgeyBsb2FkQWxsUGx1Z2lucyB9IGZyb20gJy4uLy4uL3V0aWxzL3BsdWdpbnMvcGx1Z2luTG9hZGVyLmpzJ1xuaW1wb3J0IHtcbiAgZ2V0VW5jb25maWd1cmVkT3B0aW9ucyxcbiAgbG9hZFBsdWdpbk9wdGlvbnMsXG4gIHR5cGUgUGx1Z2luT3B0aW9uU2NoZW1hLFxuICB0eXBlIFBsdWdpbk9wdGlvblZhbHVlcyxcbiAgc2F2ZVBsdWdpbk9wdGlvbnMsXG59IGZyb20gJy4uLy4uL3V0aWxzL3BsdWdpbnMvcGx1Z2luT3B0aW9uc1N0b3JhZ2UuanMnXG5pbXBvcnQgeyBQbHVnaW5PcHRpb25zRGlhbG9nIH0gZnJvbSAnLi9QbHVnaW5PcHRpb25zRGlhbG9nLmpzJ1xuXG4vKipcbiAqIFBvc3QtaW5zdGFsbCBsb29rdXA6IHJldHVybiB0aGUgTG9hZGVkUGx1Z2luIGZvciB0aGUganVzdC1pbnN0YWxsZWRcbiAqIHBsdWdpbklkIHNvIHRoZSBjYWxsZXIgY2FuIGRpdmVydCB0byBQbHVnaW5PcHRpb25zRmxvdy4gUmV0dXJucyB1bmRlZmluZWRcbiAqIGlmIHRoZSBwbHVnaW4gc29tZWhvdyBkaWRuJ3QgbWFrZSBpdCBpbnRvIHRoZSBmcmVzaCBsb2FkIOKAlCBjYWxsZXJzIHRyZWF0XG4gKiB1bmRlZmluZWQgYXMgXCJjYXJyeSBvbiBjbG9zaW5nLlwiXG4gKlxuICogSW5zdGFsbCBzaG91bGQgaGF2ZSBjbGVhcmVkIGNhY2hlcyBhbHJlYWR5OyBsb2FkQWxsUGx1Z2lucyByZWFkcyBmcmVzaC5cbiAqL1xuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGZpbmRQbHVnaW5PcHRpb25zVGFyZ2V0KFxuICBwbHVnaW5JZDogc3RyaW5nLFxuKTogUHJvbWlzZTxMb2FkZWRQbHVnaW4gfCB1bmRlZmluZWQ+IHtcbiAgY29uc3QgeyBlbmFibGVkLCBkaXNhYmxlZCB9ID0gYXdhaXQgbG9hZEFsbFBsdWdpbnMoKVxuICByZXR1cm4gWy4uLmVuYWJsZWQsIC4uLmRpc2FibGVkXS5maW5kKFxuICAgIHAgPT4gcC5yZXBvc2l0b3J5ID09PSBwbHVnaW5JZCB8fCBwLnNvdXJjZSA9PT0gcGx1Z2luSWQsXG4gIClcbn1cblxuLyoqXG4gKiBBIHNpbmdsZSBkaWFsb2cgc3RlcCBpbiB0aGUgd2Fsay4gVG9wLWxldmVsIG9wdGlvbnMgYW5kIGNoYW5uZWxzIGJvdGhcbiAqIGNvbGxhcHNlIHRvIHRoaXMgc2hhcGUg4oCUIHRoZSBvbmx5IGRpZmZlcmVuY2UgaXMgd2hpY2ggc2F2ZSBmdW5jdGlvbiBydW5zLlxuICovXG50eXBlIENvbmZpZ1N0ZXAgPSB7XG4gIGtleTogc3RyaW5nXG4gIHRpdGxlOiBzdHJpbmdcbiAgc3VidGl0bGU6IHN0cmluZ1xuICBzY2hlbWE6IFBsdWdpbk9wdGlvblNjaGVtYVxuICAvKiogUmV0dXJucyBhbnkgYWxyZWFkeS1zYXZlZCB2YWx1ZXMgc28gUGx1Z2luT3B0aW9uc0RpYWxvZyBjYW4gcHJlLWZpbGwgYW5kXG4gICAqICBza2lwIHVuY2hhbmdlZCBzZW5zaXRpdmUgZmllbGRzIG9uIHJlY29uZmlndXJlLiAqL1xuICBsb2FkOiAoKSA9PiBQbHVnaW5PcHRpb25WYWx1ZXMgfCB1bmRlZmluZWRcbiAgc2F2ZTogKHZhbHVlczogUGx1Z2luT3B0aW9uVmFsdWVzKSA9PiB2b2lkXG59XG5cbnR5cGUgUHJvcHMgPSB7XG4gIHBsdWdpbjogTG9hZGVkUGx1Z2luXG4gIC8qKiBgbmFtZUBtYXJrZXRwbGFjZWAg4oCUIHRoZSBzYXZlUGx1Z2luT3B0aW9ucyAvIHNhdmVNY3BTZXJ2ZXJVc2VyQ29uZmlnIGtleS4gKi9cbiAgcGx1Z2luSWQ6IHN0cmluZ1xuICAvKipcbiAgICogYGNvbmZpZ3VyZWRgID0gdXNlciBmaWxsZWQgYWxsIGZpZWxkcy4gYHNraXBwZWRgID0gbm90aGluZyBuZWVkZWRcbiAgICogY29uZmlndXJpbmcsIG9yIHVzZXIgaGl0IGNhbmNlbC4gYGVycm9yYCA9IHNhdmUgdGhyZXcuXG4gICAqL1xuICBvbkRvbmU6IChvdXRjb21lOiAnY29uZmlndXJlZCcgfCAnc2tpcHBlZCcgfCAnZXJyb3InLCBkZXRhaWw/OiBzdHJpbmcpID0+IHZvaWRcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIFBsdWdpbk9wdGlvbnNGbG93KHtcbiAgcGx1Z2luLFxuICBwbHVnaW5JZCxcbiAgb25Eb25lLFxufTogUHJvcHMpOiBSZWFjdC5SZWFjdE5vZGUge1xuICAvLyBCdWlsZCB0aGUgc3RlcCBsaXN0IG9uY2UgYXQgbW91bnQuIFJlLWNhbGxpbmcgYWZ0ZXIgYSBzYXZlIHdvdWxkIGRyb3AgdGhlXG4gIC8vIGl0ZW0gd2UganVzdCBjb25maWd1cmVkLlxuICBjb25zdCBbc3RlcHNdID0gUmVhY3QudXNlU3RhdGU8Q29uZmlnU3RlcFt
Line truncated
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,32 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import figures from 'figures';
|
||||
import * as React from 'react';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { getPluginTrustMessage } from '../../utils/plugins/marketplaceHelpers.js';
|
||||
export function PluginTrustWarning() {
|
||||
const $ = _c(3);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = getPluginTrustMessage();
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const customMessage = t0;
|
||||
let t1;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <Text color="claude">{figures.warning} </Text>;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
let t2;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <Box marginBottom={1}>{t1}<Text dimColor={true} italic={true}>Make sure you trust a plugin before installing, updating, or using it. Anthropic does not control what MCP servers, files, or other software are included in plugins and cannot verify that they will work as intended or that they won't change. See each plugin's homepage for more information.{customMessage ? ` ${customMessage}` : ""}</Text></Box>;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJmaWd1cmVzIiwiUmVhY3QiLCJCb3giLCJUZXh0IiwiZ2V0UGx1Z2luVHJ1c3RNZXNzYWdlIiwiUGx1Z2luVHJ1c3RXYXJuaW5nIiwiJCIsIl9jIiwidDAiLCJTeW1ib2wiLCJmb3IiLCJjdXN0b21NZXNzYWdlIiwidDEiLCJ3YXJuaW5nIiwidDIiXSwic291cmNlcyI6WyJQbHVnaW5UcnVzdFdhcm5pbmcudHN4Il0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCBmaWd1cmVzIGZyb20gJ2ZpZ3VyZXMnXG5pbXBvcnQgKiBhcyBSZWFjdCBmcm9tICdyZWFjdCdcbmltcG9ydCB7IEJveCwgVGV4dCB9IGZyb20gJy4uLy4uL2luay5qcydcbmltcG9ydCB7IGdldFBsdWdpblRydXN0TWVzc2FnZSB9IGZyb20gJy4uLy4uL3V0aWxzL3BsdWdpbnMvbWFya2V0cGxhY2VIZWxwZXJzLmpzJ1xuXG5leHBvcnQgZnVuY3Rpb24gUGx1Z2luVHJ1c3RXYXJuaW5nKCk6IFJlYWN0LlJlYWN0Tm9kZSB7XG4gIGNvbnN0IGN1c3RvbU1lc3NhZ2UgPSBnZXRQbHVnaW5UcnVzdE1lc3NhZ2UoKVxuICByZXR1cm4gKFxuICAgIDxCb3ggbWFyZ2luQm90dG9tPXsxfT5cbiAgICAgIDxUZXh0IGNvbG9yPVwiY2xhdWRlXCI+e2ZpZ3VyZXMud2FybmluZ30gPC9UZXh0PlxuICAgICAgPFRleHQgZGltQ29sb3IgaXRhbGljPlxuICAgICAgICBNYWtlIHN1cmUgeW91IHRydXN0IGEgcGx1Z2luIGJlZm9yZSBpbnN0YWxsaW5nLCB1cGRhdGluZywgb3IgdXNpbmcgaXQuXG4gICAgICAgIEFudGhyb3BpYyBkb2VzIG5vdCBjb250cm9sIHdoYXQgTUNQIHNlcnZlcnMsIGZpbGVzLCBvciBvdGhlciBzb2Z0d2FyZVxuICAgICAgICBhcmUgaW5jbHVkZWQgaW4gcGx1Z2lucyBhbmQgY2Fubm90IHZlcmlmeSB0aGF0IHRoZXkgd2lsbCB3b3JrIGFzXG4gICAgICAgIGludGVuZGVkIG9yIHRoYXQgdGhleSB3b24mYXBvczt0IGNoYW5nZS4gU2VlIGVhY2ggcGx1Z2luJmFwb3M7cyBob21lcGFnZVxuICAgICAgICBmb3IgbW9yZSBpbmZvcm1hdGlvbi57Y3VzdG9tTWVzc2FnZSA/IGAgJHtjdXN0b21NZXNzYWdlfWAgOiAnJ31cbiAgICAgIDwvVGV4dD5cbiAgICA8L0JveD5cbiAgKVxufVxuIl0sIm1hcHBpbmdzIjoiO0FBQUEsT0FBT0EsT0FBTyxNQUFNLFNBQVM7QUFDN0IsT0FBTyxLQUFLQyxLQUFLLE1BQU0sT0FBTztBQUM5QixTQUFTQyxHQUFHLEVBQUVDLElBQUksUUFBUSxjQUFjO0FBQ3hDLFNBQVNDLHFCQUFxQixRQUFRLDJDQUEyQztBQUVqRixPQUFPLFNBQUFDLG1CQUFBO0VBQUEsTUFBQUMsQ0FBQSxHQUFBQyxFQUFBO0VBQUEsSUFBQUMsRUFBQTtFQUFBLElBQUFGLENBQUEsUUFBQUcsTUFBQSxDQUFBQyxHQUFBO0lBQ2lCRixFQUFBLEdBQUFKLHFCQUFxQixDQUFDLENBQUM7SUFBQUUsQ0FBQSxNQUFBRSxFQUFBO0VBQUE7SUFBQUEsRUFBQSxHQUFBRixDQUFBO0VBQUE7RUFBN0MsTUFBQUssYUFBQSxHQUFzQkgsRUFBdUI7RUFBQSxJQUFBSSxFQUFBO0VBQUEsSUFBQU4sQ0FBQSxRQUFBRyxNQUFBLENBQUFDLEdBQUE7SUFHekNFLEVBQUEsSUFBQyxJQUFJLENBQU8sS0FBUSxDQUFSLFFBQVEsQ0FBRSxDQUFBWixPQUFPLENBQUFhLE9BQU8sQ0FBRSxDQUFDLEVBQXRDLElBQUksQ0FBeUM7SUFBQVAsQ0FBQSxNQUFBTSxFQUFBO0VBQUE7SUFBQUEsRUFBQSxHQUFBTixDQUFBO0VBQUE7RUFBQSxJQUFBUSxFQUFBO0VBQUEsSUFBQVIsQ0FBQSxRQUFBRyxNQUFBLENBQUFDLEdBQUE7SUFEaERJLEVBQUEsSUFBQyxHQUFHLENBQWUsWUFBQyxDQUFELEdBQUMsQ0FDbEIsQ0FBQUYsRUFBNkMsQ0FDN0MsQ0FBQyxJQUFJLENBQUMsUUFBUSxDQUFSLEtBQU8sQ0FBQyxDQUFDLE1BQU0sQ0FBTixLQUFLLENBQUMsQ0FBQyxrU0FLRSxDQUFBRCxhQUFhLEdBQWIsSUFBb0JBLGFBQWEsRUFBTyxHQUF4QyxFQUF1QyxDQUMvRCxFQU5DLElBQUksQ0FPUCxFQVRDLEdBQUcsQ0FTRTtJQUFBTCxDQUFBLE1BQUFRLEVBQUE7RUFBQTtJQUFBQSxFQUFBLEdBQUFSLENBQUE7RUFBQTtFQUFBLE9BVE5RLEVBU007QUFBQSIsImlnbm9yZUxpc3QiOltdfQ==
|
||||
@@ -0,0 +1,565 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import figures from 'figures';
|
||||
import * as React from 'react';
|
||||
import { Box, color, Text, useTheme } from '../../ink.js';
|
||||
import { plural } from '../../utils/stringUtils.js';
|
||||
import type { UnifiedInstalledItem } from './unifiedTypes.js';
|
||||
type Props = {
|
||||
item: UnifiedInstalledItem;
|
||||
isSelected: boolean;
|
||||
};
|
||||
export function UnifiedInstalledCell(t0) {
|
||||
const $ = _c(142);
|
||||
const {
|
||||
item,
|
||||
isSelected
|
||||
} = t0;
|
||||
const [theme] = useTheme();
|
||||
if (item.type === "plugin") {
|
||||
let statusIcon;
|
||||
let statusText;
|
||||
if (item.pendingToggle) {
|
||||
let t1;
|
||||
if ($[0] !== theme) {
|
||||
t1 = color("suggestion", theme)(figures.arrowRight);
|
||||
$[0] = theme;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
statusIcon = t1;
|
||||
statusText = item.pendingToggle === "will-enable" ? "will enable" : "will disable";
|
||||
} else {
|
||||
if (item.errorCount > 0) {
|
||||
let t1;
|
||||
if ($[2] !== theme) {
|
||||
t1 = color("error", theme)(figures.cross);
|
||||
$[2] = theme;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
statusIcon = t1;
|
||||
const t2 = item.errorCount;
|
||||
let t3;
|
||||
if ($[4] !== item.errorCount) {
|
||||
t3 = plural(item.errorCount, "error");
|
||||
$[4] = item.errorCount;
|
||||
$[5] = t3;
|
||||
} else {
|
||||
t3 = $[5];
|
||||
}
|
||||
statusText = `${t2} ${t3}`;
|
||||
} else {
|
||||
if (!item.isEnabled) {
|
||||
let t1;
|
||||
if ($[6] !== theme) {
|
||||
t1 = color("inactive", theme)(figures.radioOff);
|
||||
$[6] = theme;
|
||||
$[7] = t1;
|
||||
} else {
|
||||
t1 = $[7];
|
||||
}
|
||||
statusIcon = t1;
|
||||
statusText = "disabled";
|
||||
} else {
|
||||
let t1;
|
||||
if ($[8] !== theme) {
|
||||
t1 = color("success", theme)(figures.tick);
|
||||
$[8] = theme;
|
||||
$[9] = t1;
|
||||
} else {
|
||||
t1 = $[9];
|
||||
}
|
||||
statusIcon = t1;
|
||||
statusText = "enabled";
|
||||
}
|
||||
}
|
||||
}
|
||||
const t1 = isSelected ? "suggestion" : undefined;
|
||||
const t2 = isSelected ? `${figures.pointer} ` : " ";
|
||||
let t3;
|
||||
if ($[10] !== t1 || $[11] !== t2) {
|
||||
t3 = <Text color={t1}>{t2}</Text>;
|
||||
$[10] = t1;
|
||||
$[11] = t2;
|
||||
$[12] = t3;
|
||||
} else {
|
||||
t3 = $[12];
|
||||
}
|
||||
const t4 = isSelected ? "suggestion" : undefined;
|
||||
let t5;
|
||||
if ($[13] !== item.name || $[14] !== t4) {
|
||||
t5 = <Text color={t4}>{item.name}</Text>;
|
||||
$[13] = item.name;
|
||||
$[14] = t4;
|
||||
$[15] = t5;
|
||||
} else {
|
||||
t5 = $[15];
|
||||
}
|
||||
const t6 = !isSelected;
|
||||
let t7;
|
||||
if ($[16] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t7 = <Text backgroundColor="userMessageBackground">Plugin</Text>;
|
||||
$[16] = t7;
|
||||
} else {
|
||||
t7 = $[16];
|
||||
}
|
||||
let t8;
|
||||
if ($[17] !== t6) {
|
||||
t8 = <Text dimColor={t6}>{" "}{t7}</Text>;
|
||||
$[17] = t6;
|
||||
$[18] = t8;
|
||||
} else {
|
||||
t8 = $[18];
|
||||
}
|
||||
let t9;
|
||||
if ($[19] !== item.marketplace) {
|
||||
t9 = <Text dimColor={true}> · {item.marketplace}</Text>;
|
||||
$[19] = item.marketplace;
|
||||
$[20] = t9;
|
||||
} else {
|
||||
t9 = $[20];
|
||||
}
|
||||
const t10 = !isSelected;
|
||||
let t11;
|
||||
if ($[21] !== statusIcon || $[22] !== t10) {
|
||||
t11 = <Text dimColor={t10}> · {statusIcon} </Text>;
|
||||
$[21] = statusIcon;
|
||||
$[22] = t10;
|
||||
$[23] = t11;
|
||||
} else {
|
||||
t11 = $[23];
|
||||
}
|
||||
const t12 = !isSelected;
|
||||
let t13;
|
||||
if ($[24] !== statusText || $[25] !== t12) {
|
||||
t13 = <Text dimColor={t12}>{statusText}</Text>;
|
||||
$[24] = statusText;
|
||||
$[25] = t12;
|
||||
$[26] = t13;
|
||||
} else {
|
||||
t13 = $[26];
|
||||
}
|
||||
let t14;
|
||||
if ($[27] !== t11 || $[28] !== t13 || $[29] !== t3 || $[30] !== t5 || $[31] !== t8 || $[32] !== t9) {
|
||||
t14 = <Box>{t3}{t5}{t8}{t9}{t11}{t13}</Box>;
|
||||
$[27] = t11;
|
||||
$[28] = t13;
|
||||
$[29] = t3;
|
||||
$[30] = t5;
|
||||
$[31] = t8;
|
||||
$[32] = t9;
|
||||
$[33] = t14;
|
||||
} else {
|
||||
t14 = $[33];
|
||||
}
|
||||
return t14;
|
||||
}
|
||||
if (item.type === "flagged-plugin") {
|
||||
let t1;
|
||||
if ($[34] !== theme) {
|
||||
t1 = color("warning", theme)(figures.warning);
|
||||
$[34] = theme;
|
||||
$[35] = t1;
|
||||
} else {
|
||||
t1 = $[35];
|
||||
}
|
||||
const statusIcon_0 = t1;
|
||||
const t2 = isSelected ? "suggestion" : undefined;
|
||||
const t3 = isSelected ? `${figures.pointer} ` : " ";
|
||||
let t4;
|
||||
if ($[36] !== t2 || $[37] !== t3) {
|
||||
t4 = <Text color={t2}>{t3}</Text>;
|
||||
$[36] = t2;
|
||||
$[37] = t3;
|
||||
$[38] = t4;
|
||||
} else {
|
||||
t4 = $[38];
|
||||
}
|
||||
const t5 = isSelected ? "suggestion" : undefined;
|
||||
let t6;
|
||||
if ($[39] !== item.name || $[40] !== t5) {
|
||||
t6 = <Text color={t5}>{item.name}</Text>;
|
||||
$[39] = item.name;
|
||||
$[40] = t5;
|
||||
$[41] = t6;
|
||||
} else {
|
||||
t6 = $[41];
|
||||
}
|
||||
const t7 = !isSelected;
|
||||
let t8;
|
||||
if ($[42] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t8 = <Text backgroundColor="userMessageBackground">Plugin</Text>;
|
||||
$[42] = t8;
|
||||
} else {
|
||||
t8 = $[42];
|
||||
}
|
||||
let t9;
|
||||
if ($[43] !== t7) {
|
||||
t9 = <Text dimColor={t7}>{" "}{t8}</Text>;
|
||||
$[43] = t7;
|
||||
$[44] = t9;
|
||||
} else {
|
||||
t9 = $[44];
|
||||
}
|
||||
let t10;
|
||||
if ($[45] !== item.marketplace) {
|
||||
t10 = <Text dimColor={true}> · {item.marketplace}</Text>;
|
||||
$[45] = item.marketplace;
|
||||
$[46] = t10;
|
||||
} else {
|
||||
t10 = $[46];
|
||||
}
|
||||
const t11 = !isSelected;
|
||||
let t12;
|
||||
if ($[47] !== statusIcon_0 || $[48] !== t11) {
|
||||
t12 = <Text dimColor={t11}> · {statusIcon_0} </Text>;
|
||||
$[47] = statusIcon_0;
|
||||
$[48] = t11;
|
||||
$[49] = t12;
|
||||
} else {
|
||||
t12 = $[49];
|
||||
}
|
||||
const t13 = !isSelected;
|
||||
let t14;
|
||||
if ($[50] !== t13) {
|
||||
t14 = <Text dimColor={t13}>removed</Text>;
|
||||
$[50] = t13;
|
||||
$[51] = t14;
|
||||
} else {
|
||||
t14 = $[51];
|
||||
}
|
||||
let t15;
|
||||
if ($[52] !== t10 || $[53] !== t12 || $[54] !== t14 || $[55] !== t4 || $[56] !== t6 || $[57] !== t9) {
|
||||
t15 = <Box>{t4}{t6}{t9}{t10}{t12}{t14}</Box>;
|
||||
$[52] = t10;
|
||||
$[53] = t12;
|
||||
$[54] = t14;
|
||||
$[55] = t4;
|
||||
$[56] = t6;
|
||||
$[57] = t9;
|
||||
$[58] = t15;
|
||||
} else {
|
||||
t15 = $[58];
|
||||
}
|
||||
return t15;
|
||||
}
|
||||
if (item.type === "failed-plugin") {
|
||||
let t1;
|
||||
if ($[59] !== theme) {
|
||||
t1 = color("error", theme)(figures.cross);
|
||||
$[59] = theme;
|
||||
$[60] = t1;
|
||||
} else {
|
||||
t1 = $[60];
|
||||
}
|
||||
const statusIcon_1 = t1;
|
||||
const t2 = item.errorCount;
|
||||
let t3;
|
||||
if ($[61] !== item.errorCount) {
|
||||
t3 = plural(item.errorCount, "error");
|
||||
$[61] = item.errorCount;
|
||||
$[62] = t3;
|
||||
} else {
|
||||
t3 = $[62];
|
||||
}
|
||||
const statusText_0 = `failed to load · ${t2} ${t3}`;
|
||||
const t4 = isSelected ? "suggestion" : undefined;
|
||||
const t5 = isSelected ? `${figures.pointer} ` : " ";
|
||||
let t6;
|
||||
if ($[63] !== t4 || $[64] !== t5) {
|
||||
t6 = <Text color={t4}>{t5}</Text>;
|
||||
$[63] = t4;
|
||||
$[64] = t5;
|
||||
$[65] = t6;
|
||||
} else {
|
||||
t6 = $[65];
|
||||
}
|
||||
const t7 = isSelected ? "suggestion" : undefined;
|
||||
let t8;
|
||||
if ($[66] !== item.name || $[67] !== t7) {
|
||||
t8 = <Text color={t7}>{item.name}</Text>;
|
||||
$[66] = item.name;
|
||||
$[67] = t7;
|
||||
$[68] = t8;
|
||||
} else {
|
||||
t8 = $[68];
|
||||
}
|
||||
const t9 = !isSelected;
|
||||
let t10;
|
||||
if ($[69] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t10 = <Text backgroundColor="userMessageBackground">Plugin</Text>;
|
||||
$[69] = t10;
|
||||
} else {
|
||||
t10 = $[69];
|
||||
}
|
||||
let t11;
|
||||
if ($[70] !== t9) {
|
||||
t11 = <Text dimColor={t9}>{" "}{t10}</Text>;
|
||||
$[70] = t9;
|
||||
$[71] = t11;
|
||||
} else {
|
||||
t11 = $[71];
|
||||
}
|
||||
let t12;
|
||||
if ($[72] !== item.marketplace) {
|
||||
t12 = <Text dimColor={true}> · {item.marketplace}</Text>;
|
||||
$[72] = item.marketplace;
|
||||
$[73] = t12;
|
||||
} else {
|
||||
t12 = $[73];
|
||||
}
|
||||
const t13 = !isSelected;
|
||||
let t14;
|
||||
if ($[74] !== statusIcon_1 || $[75] !== t13) {
|
||||
t14 = <Text dimColor={t13}> · {statusIcon_1} </Text>;
|
||||
$[74] = statusIcon_1;
|
||||
$[75] = t13;
|
||||
$[76] = t14;
|
||||
} else {
|
||||
t14 = $[76];
|
||||
}
|
||||
const t15 = !isSelected;
|
||||
let t16;
|
||||
if ($[77] !== statusText_0 || $[78] !== t15) {
|
||||
t16 = <Text dimColor={t15}>{statusText_0}</Text>;
|
||||
$[77] = statusText_0;
|
||||
$[78] = t15;
|
||||
$[79] = t16;
|
||||
} else {
|
||||
t16 = $[79];
|
||||
}
|
||||
let t17;
|
||||
if ($[80] !== t11 || $[81] !== t12 || $[82] !== t14 || $[83] !== t16 || $[84] !== t6 || $[85] !== t8) {
|
||||
t17 = <Box>{t6}{t8}{t11}{t12}{t14}{t16}</Box>;
|
||||
$[80] = t11;
|
||||
$[81] = t12;
|
||||
$[82] = t14;
|
||||
$[83] = t16;
|
||||
$[84] = t6;
|
||||
$[85] = t8;
|
||||
$[86] = t17;
|
||||
} else {
|
||||
t17 = $[86];
|
||||
}
|
||||
return t17;
|
||||
}
|
||||
let statusIcon_2;
|
||||
let statusText_1;
|
||||
if (item.status === "connected") {
|
||||
let t1;
|
||||
if ($[87] !== theme) {
|
||||
t1 = color("success", theme)(figures.tick);
|
||||
$[87] = theme;
|
||||
$[88] = t1;
|
||||
} else {
|
||||
t1 = $[88];
|
||||
}
|
||||
statusIcon_2 = t1;
|
||||
statusText_1 = "connected";
|
||||
} else {
|
||||
if (item.status === "disabled") {
|
||||
let t1;
|
||||
if ($[89] !== theme) {
|
||||
t1 = color("inactive", theme)(figures.radioOff);
|
||||
$[89] = theme;
|
||||
$[90] = t1;
|
||||
} else {
|
||||
t1 = $[90];
|
||||
}
|
||||
statusIcon_2 = t1;
|
||||
statusText_1 = "disabled";
|
||||
} else {
|
||||
if (item.status === "pending") {
|
||||
let t1;
|
||||
if ($[91] !== theme) {
|
||||
t1 = color("inactive", theme)(figures.radioOff);
|
||||
$[91] = theme;
|
||||
$[92] = t1;
|
||||
} else {
|
||||
t1 = $[92];
|
||||
}
|
||||
statusIcon_2 = t1;
|
||||
statusText_1 = "connecting\u2026";
|
||||
} else {
|
||||
if (item.status === "needs-auth") {
|
||||
let t1;
|
||||
if ($[93] !== theme) {
|
||||
t1 = color("warning", theme)(figures.triangleUpOutline);
|
||||
$[93] = theme;
|
||||
$[94] = t1;
|
||||
} else {
|
||||
t1 = $[94];
|
||||
}
|
||||
statusIcon_2 = t1;
|
||||
statusText_1 = "Enter to auth";
|
||||
} else {
|
||||
let t1;
|
||||
if ($[95] !== theme) {
|
||||
t1 = color("error", theme)(figures.cross);
|
||||
$[95] = theme;
|
||||
$[96] = t1;
|
||||
} else {
|
||||
t1 = $[96];
|
||||
}
|
||||
statusIcon_2 = t1;
|
||||
statusText_1 = "failed";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (item.indented) {
|
||||
const t1 = isSelected ? "suggestion" : undefined;
|
||||
const t2 = isSelected ? `${figures.pointer} ` : " ";
|
||||
let t3;
|
||||
if ($[97] !== t1 || $[98] !== t2) {
|
||||
t3 = <Text color={t1}>{t2}</Text>;
|
||||
$[97] = t1;
|
||||
$[98] = t2;
|
||||
$[99] = t3;
|
||||
} else {
|
||||
t3 = $[99];
|
||||
}
|
||||
const t4 = !isSelected;
|
||||
let t5;
|
||||
if ($[100] !== t4) {
|
||||
t5 = <Text dimColor={t4}>└ </Text>;
|
||||
$[100] = t4;
|
||||
$[101] = t5;
|
||||
} else {
|
||||
t5 = $[101];
|
||||
}
|
||||
const t6 = isSelected ? "suggestion" : undefined;
|
||||
let t7;
|
||||
if ($[102] !== item.name || $[103] !== t6) {
|
||||
t7 = <Text color={t6}>{item.name}</Text>;
|
||||
$[102] = item.name;
|
||||
$[103] = t6;
|
||||
$[104] = t7;
|
||||
} else {
|
||||
t7 = $[104];
|
||||
}
|
||||
const t8 = !isSelected;
|
||||
let t9;
|
||||
if ($[105] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t9 = <Text backgroundColor="userMessageBackground">MCP</Text>;
|
||||
$[105] = t9;
|
||||
} else {
|
||||
t9 = $[105];
|
||||
}
|
||||
let t10;
|
||||
if ($[106] !== t8) {
|
||||
t10 = <Text dimColor={t8}>{" "}{t9}</Text>;
|
||||
$[106] = t8;
|
||||
$[107] = t10;
|
||||
} else {
|
||||
t10 = $[107];
|
||||
}
|
||||
const t11 = !isSelected;
|
||||
let t12;
|
||||
if ($[108] !== statusIcon_2 || $[109] !== t11) {
|
||||
t12 = <Text dimColor={t11}> · {statusIcon_2} </Text>;
|
||||
$[108] = statusIcon_2;
|
||||
$[109] = t11;
|
||||
$[110] = t12;
|
||||
} else {
|
||||
t12 = $[110];
|
||||
}
|
||||
const t13 = !isSelected;
|
||||
let t14;
|
||||
if ($[111] !== statusText_1 || $[112] !== t13) {
|
||||
t14 = <Text dimColor={t13}>{statusText_1}</Text>;
|
||||
$[111] = statusText_1;
|
||||
$[112] = t13;
|
||||
$[113] = t14;
|
||||
} else {
|
||||
t14 = $[113];
|
||||
}
|
||||
let t15;
|
||||
if ($[114] !== t10 || $[115] !== t12 || $[116] !== t14 || $[117] !== t3 || $[118] !== t5 || $[119] !== t7) {
|
||||
t15 = <Box>{t3}{t5}{t7}{t10}{t12}{t14}</Box>;
|
||||
$[114] = t10;
|
||||
$[115] = t12;
|
||||
$[116] = t14;
|
||||
$[117] = t3;
|
||||
$[118] = t5;
|
||||
$[119] = t7;
|
||||
$[120] = t15;
|
||||
} else {
|
||||
t15 = $[120];
|
||||
}
|
||||
return t15;
|
||||
}
|
||||
const t1 = isSelected ? "suggestion" : undefined;
|
||||
const t2 = isSelected ? `${figures.pointer} ` : " ";
|
||||
let t3;
|
||||
if ($[121] !== t1 || $[122] !== t2) {
|
||||
t3 = <Text color={t1}>{t2}</Text>;
|
||||
$[121] = t1;
|
||||
$[122] = t2;
|
||||
$[123] = t3;
|
||||
} else {
|
||||
t3 = $[123];
|
||||
}
|
||||
const t4 = isSelected ? "suggestion" : undefined;
|
||||
let t5;
|
||||
if ($[124] !== item.name || $[125] !== t4) {
|
||||
t5 = <Text color={t4}>{item.name}</Text>;
|
||||
$[124] = item.name;
|
||||
$[125] = t4;
|
||||
$[126] = t5;
|
||||
} else {
|
||||
t5 = $[126];
|
||||
}
|
||||
const t6 = !isSelected;
|
||||
let t7;
|
||||
if ($[127] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t7 = <Text backgroundColor="userMessageBackground">MCP</Text>;
|
||||
$[127] = t7;
|
||||
} else {
|
||||
t7 = $[127];
|
||||
}
|
||||
let t8;
|
||||
if ($[128] !== t6) {
|
||||
t8 = <Text dimColor={t6}>{" "}{t7}</Text>;
|
||||
$[128] = t6;
|
||||
$[129] = t8;
|
||||
} else {
|
||||
t8 = $[129];
|
||||
}
|
||||
const t9 = !isSelected;
|
||||
let t10;
|
||||
if ($[130] !== statusIcon_2 || $[131] !== t9) {
|
||||
t10 = <Text dimColor={t9}> · {statusIcon_2} </Text>;
|
||||
$[130] = statusIcon_2;
|
||||
$[131] = t9;
|
||||
$[132] = t10;
|
||||
} else {
|
||||
t10 = $[132];
|
||||
}
|
||||
const t11 = !isSelected;
|
||||
let t12;
|
||||
if ($[133] !== statusText_1 || $[134] !== t11) {
|
||||
t12 = <Text dimColor={t11}>{statusText_1}</Text>;
|
||||
$[133] = statusText_1;
|
||||
$[134] = t11;
|
||||
$[135] = t12;
|
||||
} else {
|
||||
t12 = $[135];
|
||||
}
|
||||
let t13;
|
||||
if ($[136] !== t10 || $[137] !== t12 || $[138] !== t3 || $[139] !== t5 || $[140] !== t8) {
|
||||
t13 = <Box>{t3}{t5}{t8}{t10}{t12}</Box>;
|
||||
$[136] = t10;
|
||||
$[137] = t12;
|
||||
$[138] = t3;
|
||||
$[139] = t5;
|
||||
$[140] = t8;
|
||||
$[141] = t13;
|
||||
} else {
|
||||
t13 = $[141];
|
||||
}
|
||||
return t13;
|
||||
}
|
||||
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJmaWd1cmVzIiwiUmVhY3QiLCJCb3giLCJjb2xvciIsIlRleHQiLCJ1c2VUaGVtZSIsInBsdXJhbCIsIlVuaWZpZWRJbnN0YWxsZWRJdGVtIiwiUHJvcHMiLCJpdGVtIiwiaXNTZWxlY3RlZCIsIlVuaWZpZWRJbnN0YWxsZWRDZWxsIiwidDAiLCIkIiwiX2MiLCJ0aGVtZSIsInR5cGUiLCJzdGF0dXNJY29uIiwic3RhdHVzVGV4dCIsInBlbmRpbmdUb2dnbGUiLCJ0MSIsImFycm93UmlnaHQiLCJlcnJvckNvdW50IiwiY3Jvc3MiLCJ0MiIsInQzIiwiaXNFbmFibGVkIiwicmFkaW9PZmYiLCJ0aWNrIiwidW5kZWZpbmVkIiwicG9pbnRlciIsInQ0IiwidDUiLCJuYW1lIiwidDYiLCJ0NyIsIlN5bWJvbCIsImZvciIsInQ4IiwidDkiLCJtYXJrZXRwbGFjZSIsInQxMCIsInQxMSIsInQxMiIsInQxMyIsInQxNCIsIndhcm5pbmciLCJzdGF0dXNJY29uXzAiLCJ0MTUiLCJzdGF0dXNJY29uXzEiLCJzdGF0dXNUZXh0XzAiLCJ0MTYiLCJ0MTciLCJzdGF0dXMiLCJ0cmlhbmdsZVVwT3V0bGluZSIsImluZGVudGVkIiwic3RhdHVzSWNvbl8yIiwic3RhdHVzVGV4dF8xIl0sInNvdXJjZXMiOlsiVW5pZmllZEluc3RhbGxlZENlbGwudHN4Il0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCBmaWd1cmVzIGZyb20gJ2ZpZ3VyZXMnXG5pbXBvcnQgKiBhcyBSZWFjdCBmcm9tICdyZWFjdCdcbmltcG9ydCB7IEJveCwgY29sb3IsIFRleHQsIHVzZVRoZW1lIH0gZnJvbSAnLi4vLi4vaW5rLmpzJ1xuaW1wb3J0IHsgcGx1cmFsIH0gZnJvbSAnLi4vLi4vdXRpbHMvc3RyaW5nVXRpbHMuanMnXG5pbXBvcnQgdHlwZSB7IFVuaWZpZWRJbnN0YWxsZWRJdGVtIH0gZnJvbSAnLi91bmlmaWVkVHlwZXMuanMnXG5cbnR5cGUgUHJvcHMgPSB7XG4gIGl0ZW06IFVuaWZpZWRJbnN0YWxsZWRJdGVtXG4gIGlzU2VsZWN0ZWQ6IGJvb2xlYW5cbn1cblxuZXhwb3J0IGZ1bmN0aW9uIFVuaWZpZWRJbnN0YWxsZWRDZWxsKHtcbiAgaXRlbSxcbiAgaXNTZWxlY3RlZCxcbn06IFByb3BzKTogUmVhY3QuUmVhY3ROb2RlIHtcbiAgY29uc3QgW3RoZW1lXSA9IHVzZVRoZW1lKClcblxuICBpZiAoaXRlbS50eXBlID09PSAncGx1Z2luJykge1xuICAgIC8vIFN0YXR1cyBpY29uIGFuZCB0ZXh0XG4gICAgbGV0IHN0YXR1c0ljb246IHN0cmluZ1xuICAgIGxldCBzdGF0dXNUZXh0OiBzdHJpbmdcblxuICAgIC8vIFNob3cgcGVuZGluZyB0b2dnbGUgc3RhdHVzIGlmIHNldCwgb3RoZXJ3aXNlIHNob3cgY3VycmVudCBzdGF0dXNcbiAgICBpZiAoaXRlbS5wZW5kaW5nVG9nZ2xlKSB7XG4gICAgICBzdGF0dXNJY29uID0gY29sb3IoJ3N1Z2dlc3Rpb24nLCB0aGVtZSkoZmlndXJlcy5hcnJvd1JpZ2h0KVxuICAgICAgc3RhdHVzVGV4dCA9XG4gICAgICAgIGl0ZW0ucGVuZGluZ1RvZ2dsZSA9PT0gJ3dpbGwtZW5hYmxlJyA/ICd3aWxsIGVuYWJsZScgOiAnd2lsbCBkaXNhYmxlJ1xuICAgIH0gZWxzZSBpZiAoaXRlbS5lcnJvckNvdW50ID4gMCkge1xuICAgICAgc3RhdHVzSWNvbiA9IGNvbG9yKCdlcnJvcicsIHRoZW1lKShmaWd1cmVzLmNyb3NzKVxuICAgICAgc3RhdHVzVGV4dCA9IGAke2l0ZW0uZXJyb3JDb3VudH0gJHtwbHVyYWwoaXRlbS5lcnJvckNvdW50LCAnZXJyb3InKX1gXG4gICAgfSBlbHNlIGlmICghaXRlbS5pc0VuYWJsZWQpIHtcbiAgICAgIHN0YXR1c0ljb24gPSBjb2xvcignaW5hY3RpdmUnLCB0aGVtZSkoZmlndXJlcy5yYWRpb09mZilcbiAgICAgIHN0YXR1c1RleHQgPSAnZGlzYWJsZWQnXG4gICAgfSBlbHNlIHtcbiAgICAgIHN0YXR1c0ljb24gPSBjb2xvcignc3VjY2VzcycsIHRoZW1lKShmaWd1cmVzLnRpY2spXG4gICAgICBzdGF0dXNUZXh0ID0gJ2VuYWJsZWQnXG4gICAgfVxuXG4gICAgcmV0dXJuIChcbiAgICAgIDxCb3g+XG4gICAgICAgIDxUZXh0IGNvbG9yPXtpc1NlbGVjdGVkID8gJ3N1Z2dlc3Rpb24nIDogdW5kZWZpbmVkfT5cbiAgICAgICAgICB7aXNTZWxlY3RlZCA/IGAke2ZpZ3VyZXMucG9pbnRlcn0gYCA6ICcgICd9XG4gICAgICAgIDwvVGV4dD5cbiAgICAgICAgPFRleHQgY29sb3I9e2lzU2VsZWN0ZWQgPyAnc3VnZ2VzdGlvbicgOiB1bmRlZmluZWR9PntpdGVtLm5hbWV9PC9UZXh0PlxuICAgICAgICA8VGV4dCBkaW1Db2xvcj17IWlzU2VsZWN0ZWR9PlxuICAgICAgICAgIHsnICd9XG4gICAgICAgICAgPFRleHQgYmFja2dyb3VuZENvbG9yPVwidXNlck1lc3NhZ2VCYWNrZ3JvdW5kXCI+UGx1Z2luPC9UZXh0PlxuICAgICAgICA8L1RleHQ+XG4gICAgICAgIDxUZXh0IGRpbUNvbG9yPiDCtyB7aXRlbS5tYXJrZXRwbGFjZX08L1RleHQ+XG4gICAgICAgIDxUZXh0IGRpbUNvbG9yPXshaXNTZWxlY3RlZH0+IMK3IHtzdGF0dXNJY29ufSA8L1RleHQ+XG4gICAgICAgIDxUZXh0IGRpbUNvbG9yPXshaXNTZWxlY3RlZH0+e3N0YXR1c1RleHR9PC9UZXh0PlxuICAgICAgPC9Cb3g+XG4gICAgKVxuICB9XG5cbiAgaWYgKGl0ZW0udHlwZSA9PT0gJ2ZsYWdnZWQtcGx1Z2luJykge1xuICAgIGNvbnN0IHN0YXR1c0ljb24gPSBjb2xvcignd2FybmluZycsIHRoZW1lKShmaWd1cmVzLndhcm5pbmcpXG5cbiAgICByZXR1cm4gKFxuICAgICAgPEJveD5cbiAgICAgICAgPFRleHQgY29sb3I9e2lzU2VsZWN0ZWQgPyAnc3VnZ2VzdGlvbicgOiB1bmRlZmluZWR9PlxuICAgICAgICAgIHtpc1NlbGVjdGVkID8gYCR7ZmlndXJlcy5wb2ludGVyfSBgIDogJyAgJ31cbiAgICAgICAgPC9UZXh0PlxuICAgICAgICA8VGV4dCBjb2xvcj17aXNTZWxlY3RlZCA/ICdzdWdnZXN0aW9uJyA6IHVuZGVmaW5lZH0+e2l0ZW0ubmFtZX08L1RleHQ+XG4gICAgICAgIDxUZXh0IGRpbUNvbG9yPXshaXNTZWxlY3RlZH0+XG4gICAgICAgICAgeycgJ31cbiAgICAgICAgICA8VGV4dCBiYWNrZ3JvdW5kQ29sb3I9XCJ1c2VyTWVzc2FnZUJhY2tncm91bmRcIj5QbHVnaW48L1RleHQ+XG4gICAgICAgIDwvVGV4dD5cbiAgICAgICAgPFRleHQgZGltQ29sb3I+IMK3IHtpdGVtLm1hcmtldHBsYWNlfTwvVGV4dD5cbiAgICAgICAgPFRleHQgZGltQ29sb3I9eyFpc1NlbGVjdGVkfT4gwrcge3N0YXR1c0ljb259IDwvVGV4dD5cbiAgICAgICAgPFRleHQgZGltQ29sb3I9eyFpc1NlbGVjdGVkfT5yZW1vdmVkPC9UZXh0PlxuICAgICAgPC9Cb3g+XG4gICAgKVxuICB9XG5cbiAgaWYgKGl0ZW0udHlwZSA9PT0gJ2ZhaWxlZC1wbHVnaW4nKSB7XG4gICAgY29uc3Qgc3RhdHVzSWNvbiA9IGNvbG9yKCdlcnJvcicsIHRoZW1lKShmaWd1cmVzLmNyb3NzKVxuICAgIGNvbnN0IHN0YXR1c1RleHQgPSBgZmFpbGVkIHRvIGxvYWQgwrcgJHtpdGVtLmVycm9yQ291bnR9ICR7cGx1cmFsKGl0ZW0uZXJyb3JDb3VudCwgJ2Vycm9yJyl9YFxuXG4gICAgcmV0dXJuIChcbiAgICAgIDxCb3g+XG4gICAgICAgIDxUZXh0IGNvbG9yPXtpc1NlbGVjdGVkID8gJ3N1Z2dlc3Rpb24nIDogdW5kZWZpbmVkfT5cbiAgICAgICAgICB7aXNTZWxlY3RlZCA/IGAke2ZpZ3VyZXMucG9pbnRlcn0gYCA6ICcgICd9XG4gICAgICAgIDwvVGV4dD5cbiAgICAgICAgPFRleHQgY29sb3I9e2lzU2VsZWN0ZWQgPyAnc3VnZ2VzdGlvbicgOiB1bmRlZmluZWR9PntpdGVtLm5hbWV9PC9UZXh0PlxuICAgICAgICA8VGV4dCBkaW1Db2xvcj17IWlzU2VsZWN0ZWR9PlxuICAgICAgICAgIHsnICd9XG4gICAgICAgICAgPFRleHQgYmFja2dyb3VuZENvbG9yPVwidXNlck1lc3NhZ2VCYWNrZ3J
Line truncated
|
||||
@@ -0,0 +1,98 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import figures from 'figures';
|
||||
import * as React from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { errorMessage } from '../../utils/errors.js';
|
||||
import { logError } from '../../utils/log.js';
|
||||
import { validateManifest } from '../../utils/plugins/validatePlugin.js';
|
||||
import { plural } from '../../utils/stringUtils.js';
|
||||
type Props = {
|
||||
onComplete: (result?: string) => void;
|
||||
path?: string;
|
||||
};
|
||||
export function ValidatePlugin(t0) {
|
||||
const $ = _c(5);
|
||||
const {
|
||||
onComplete,
|
||||
path
|
||||
} = t0;
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== onComplete || $[1] !== path) {
|
||||
t1 = () => {
|
||||
const runValidation = async function runValidation() {
|
||||
if (!path) {
|
||||
onComplete("Usage: /plugin validate <path>\n\nValidate a plugin or marketplace manifest file or directory.\n\nExamples:\n /plugin validate .claude-plugin/plugin.json\n /plugin validate /path/to/plugin-directory\n /plugin validate .\n\nWhen given a directory, automatically validates .claude-plugin/marketplace.json\nor .claude-plugin/plugin.json (prefers marketplace if both exist).\n\nOr from the command line:\n claude plugin validate <path>");
|
||||
return;
|
||||
}
|
||||
;
|
||||
try {
|
||||
const result = await validateManifest(path);
|
||||
let output = "";
|
||||
output = output + `Validating ${result.fileType} manifest: ${result.filePath}\n\n`;
|
||||
output;
|
||||
if (result.errors.length > 0) {
|
||||
output = output + `${figures.cross} Found ${result.errors.length} ${plural(result.errors.length, "error")}:\n\n`;
|
||||
output;
|
||||
result.errors.forEach(error_0 => {
|
||||
output = output + ` ${figures.pointer} ${error_0.path}: ${error_0.message}\n`;
|
||||
output;
|
||||
});
|
||||
output = output + "\n";
|
||||
output;
|
||||
}
|
||||
if (result.warnings.length > 0) {
|
||||
output = output + `${figures.warning} Found ${result.warnings.length} ${plural(result.warnings.length, "warning")}:\n\n`;
|
||||
output;
|
||||
result.warnings.forEach(warning => {
|
||||
output = output + ` ${figures.pointer} ${warning.path}: ${warning.message}\n`;
|
||||
output;
|
||||
});
|
||||
output = output + "\n";
|
||||
output;
|
||||
}
|
||||
if (result.success) {
|
||||
if (result.warnings.length > 0) {
|
||||
output = output + `${figures.tick} Validation passed with warnings\n`;
|
||||
output;
|
||||
} else {
|
||||
output = output + `${figures.tick} Validation passed\n`;
|
||||
output;
|
||||
}
|
||||
process.exitCode = 0;
|
||||
} else {
|
||||
output = output + `${figures.cross} Validation failed\n`;
|
||||
output;
|
||||
process.exitCode = 1;
|
||||
}
|
||||
onComplete(output);
|
||||
} catch (t3) {
|
||||
const error = t3;
|
||||
process.exitCode = 2;
|
||||
logError(error);
|
||||
onComplete(`${figures.cross} Unexpected error during validation: ${errorMessage(error)}`);
|
||||
}
|
||||
};
|
||||
runValidation();
|
||||
};
|
||||
t2 = [onComplete, path];
|
||||
$[0] = onComplete;
|
||||
$[1] = path;
|
||||
$[2] = t1;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
t2 = $[3];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = <Box flexDirection="column"><Text>Running validation...</Text></Box>;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJmaWd1cmVzIiwiUmVhY3QiLCJ1c2VFZmZlY3QiLCJCb3giLCJUZXh0IiwiZXJyb3JNZXNzYWdlIiwibG9nRXJyb3IiLCJ2YWxpZGF0ZU1hbmlmZXN0IiwicGx1cmFsIiwiUHJvcHMiLCJvbkNvbXBsZXRlIiwicmVzdWx0IiwicGF0aCIsIlZhbGlkYXRlUGx1Z2luIiwidDAiLCIkIiwiX2MiLCJ0MSIsInQyIiwicnVuVmFsaWRhdGlvbiIsIm91dHB1dCIsImZpbGVUeXBlIiwiZmlsZVBhdGgiLCJlcnJvcnMiLCJsZW5ndGgiLCJjcm9zcyIsImZvckVhY2giLCJlcnJvcl8wIiwicG9pbnRlciIsImVycm9yIiwibWVzc2FnZSIsIndhcm5pbmdzIiwid2FybmluZyIsInN1Y2Nlc3MiLCJ0aWNrIiwicHJvY2VzcyIsImV4aXRDb2RlIiwidDMiLCJTeW1ib2wiLCJmb3IiXSwic291cmNlcyI6WyJWYWxpZGF0ZVBsdWdpbi50c3giXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IGZpZ3VyZXMgZnJvbSAnZmlndXJlcydcbmltcG9ydCAqIGFzIFJlYWN0IGZyb20gJ3JlYWN0J1xuaW1wb3J0IHsgdXNlRWZmZWN0IH0gZnJvbSAncmVhY3QnXG5pbXBvcnQgeyBCb3gsIFRleHQgfSBmcm9tICcuLi8uLi9pbmsuanMnXG5pbXBvcnQgeyBlcnJvck1lc3NhZ2UgfSBmcm9tICcuLi8uLi91dGlscy9lcnJvcnMuanMnXG5pbXBvcnQgeyBsb2dFcnJvciB9IGZyb20gJy4uLy4uL3V0aWxzL2xvZy5qcydcbmltcG9ydCB7IHZhbGlkYXRlTWFuaWZlc3QgfSBmcm9tICcuLi8uLi91dGlscy9wbHVnaW5zL3ZhbGlkYXRlUGx1Z2luLmpzJ1xuaW1wb3J0IHsgcGx1cmFsIH0gZnJvbSAnLi4vLi4vdXRpbHMvc3RyaW5nVXRpbHMuanMnXG5cbnR5cGUgUHJvcHMgPSB7XG4gIG9uQ29tcGxldGU6IChyZXN1bHQ/OiBzdHJpbmcpID0+IHZvaWRcbiAgcGF0aD86IHN0cmluZ1xufVxuXG5leHBvcnQgZnVuY3Rpb24gVmFsaWRhdGVQbHVnaW4oeyBvbkNvbXBsZXRlLCBwYXRoIH06IFByb3BzKTogUmVhY3QuUmVhY3ROb2RlIHtcbiAgdXNlRWZmZWN0KCgpID0+IHtcbiAgICBhc3luYyBmdW5jdGlvbiBydW5WYWxpZGF0aW9uKCkge1xuICAgICAgLy8gSWYgbm8gcGF0aCBwcm92aWRlZCwgc2hvdyB1c2FnZVxuICAgICAgaWYgKCFwYXRoKSB7XG4gICAgICAgIG9uQ29tcGxldGUoXG4gICAgICAgICAgJ1VzYWdlOiAvcGx1Z2luIHZhbGlkYXRlIDxwYXRoPlxcblxcbicgK1xuICAgICAgICAgICAgJ1ZhbGlkYXRlIGEgcGx1Z2luIG9yIG1hcmtldHBsYWNlIG1hbmlmZXN0IGZpbGUgb3IgZGlyZWN0b3J5LlxcblxcbicgK1xuICAgICAgICAgICAgJ0V4YW1wbGVzOlxcbicgK1xuICAgICAgICAgICAgJyAgL3BsdWdpbiB2YWxpZGF0ZSAuY2xhdWRlLXBsdWdpbi9wbHVnaW4uanNvblxcbicgK1xuICAgICAgICAgICAgJyAgL3BsdWdpbiB2YWxpZGF0ZSAvcGF0aC90by9wbHVnaW4tZGlyZWN0b3J5XFxuJyArXG4gICAgICAgICAgICAnICAvcGx1Z2luIHZhbGlkYXRlIC5cXG5cXG4nICtcbiAgICAgICAgICAgICdXaGVuIGdpdmVuIGEgZGlyZWN0b3J5LCBhdXRvbWF0aWNhbGx5IHZhbGlkYXRlcyAuY2xhdWRlLXBsdWdpbi9tYXJrZXRwbGFjZS5qc29uXFxuJyArXG4gICAgICAgICAgICAnb3IgLmNsYXVkZS1wbHVnaW4vcGx1Z2luLmpzb24gKHByZWZlcnMgbWFya2V0cGxhY2UgaWYgYm90aCBleGlzdCkuXFxuXFxuJyArXG4gICAgICAgICAgICAnT3IgZnJvbSB0aGUgY29tbWFuZCBsaW5lOlxcbicgK1xuICAgICAgICAgICAgJyAgY2xhdWRlIHBsdWdpbiB2YWxpZGF0ZSA8cGF0aD4nLFxuICAgICAgICApXG4gICAgICAgIHJldHVyblxuICAgICAgfVxuXG4gICAgICB0cnkge1xuICAgICAgICBjb25zdCByZXN1bHQgPSBhd2FpdCB2YWxpZGF0ZU1hbmlmZXN0KHBhdGgpXG5cbiAgICAgICAgbGV0IG91dHB1dCA9ICcnXG5cbiAgICAgICAgLy8gQWRkIGhlYWRlclxuICAgICAgICBvdXRwdXQgKz0gYFZhbGlkYXRpbmcgJHtyZXN1bHQuZmlsZVR5cGV9IG1hbmlmZXN0OiAke3Jlc3VsdC5maWxlUGF0aH1cXG5cXG5gXG5cbiAgICAgICAgLy8gU2hvdyBlcnJvcnNcbiAgICAgICAgaWYgKHJlc3VsdC5lcnJvcnMubGVuZ3RoID4gMCkge1xuICAgICAgICAgIG91dHB1dCArPSBgJHtmaWd1cmVzLmNyb3NzfSBGb3VuZCAke3Jlc3VsdC5lcnJvcnMubGVuZ3RofSAke3BsdXJhbChyZXN1bHQuZXJyb3JzLmxlbmd0aCwgJ2Vycm9yJyl9OlxcblxcbmBcblxuICAgICAgICAgIHJlc3VsdC5lcnJvcnMuZm9yRWFjaChlcnJvciA9PiB7XG4gICAgICAgICAgICBvdXRwdXQgKz0gYCAgJHtmaWd1cmVzLnBvaW50ZXJ9ICR7ZXJyb3IucGF0aH06ICR7ZXJyb3IubWVzc2FnZX1cXG5gXG4gICAgICAgICAgfSlcblxuICAgICAgICAgIG91dHB1dCArPSAnXFxuJ1xuICAgICAgICB9XG5cbiAgICAgICAgLy8gU2hvdyB3YXJuaW5nc1xuICAgICAgICBpZiAocmVzdWx0Lndhcm5pbmdzLmxlbmd0aCA+IDApIHtcbiAgICAgICAgICBvdXRwdXQgKz0gYCR7ZmlndXJlcy53YXJuaW5nfSBGb3VuZCAke3Jlc3VsdC53YXJuaW5ncy5sZW5ndGh9ICR7cGx1cmFsKHJlc3VsdC53YXJuaW5ncy5sZW5ndGgsICd3YXJuaW5nJyl9OlxcblxcbmBcblxuICAgICAgICAgIHJlc3VsdC53YXJuaW5ncy5mb3JFYWNoKHdhcm5pbmcgPT4ge1xuICAgICAgICAgICAgb3V0cHV0ICs9IGAgICR7ZmlndXJlcy5wb2ludGVyfSAke3dhcm5pbmcucGF0aH06ICR7d2FybmluZy5tZXNzYWdlfVxcbmBcbiAgICAgICAgICB9KVxuXG4gICAgICAgICAgb3V0cHV0ICs9ICdcXG4nXG4gICAgICAgIH1cblxuICAgICAgICAvLyBTaG93IHN1Y2Nlc3Mgb3IgZmFpbHVyZVxuICAgICAgICBpZiAocmVzdWx0LnN1Y2Nlc3MpIHtcbiAgICAgICAgICBpZiAocmVzdWx0Lndhcm5pbmdzLmxlbmd0aCA+IDApIHtcbiAgICAgICAgICAgIG91dHB1dCArPSBgJHtmaWd1cmVzLnRpY2t9IFZhbGlkYXRpb24gcGFzc2VkIHdpdGggd2FybmluZ3NcXG5gXG4gICAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICAgIG91dHB1dCArPSBgJHtmaWd1cmVzLnRpY2t9IFZhbGlkYXRpb24gcGFzc2VkXFxuYFxuICAgICAgICAgIH1cblxuICAgICAgICAgIC8vIEV4aXQgd2l0aCBjb2RlIDAgKHN1Y2Nlc3MpXG4gICAgICAgICAgcHJvY2Vzcy5leGl0Q29kZSA9IDBcbiAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICBvdXRwdXQgKz0gYCR7ZmlndXJlcy5jcm9zc30gVmFsaWRhdGlvbiBmYWlsZWRcXG5gXG5cbiAgICAgICAgICAvLyBFeGl0IHdpdGggY29kZSAxICh2YWxpZGF0aW9uIGZhaWx1cmUpXG4gICAgICAgICAgcHJvY2Vzcy5leGl0Q29kZSA9IDFcbiAgICAgICAgfVxuXG4gICAgICAgIG9uQ29tcGxldGUob3V0cHV0KVxuICAgICAgfSBjYXRjaCAoZXJyb3IpIHtcbiAgICAgICAgLy8gRXhpdCB3aXRoIGNvZGUgMiAodW5leHBlY3RlZCBlcnJvcilcbiAgICAgICAgcHJvY2Vzcy5leGl0Q29kZSA9IDJcblxuICAgICAgICBsb2dFcnJvcihlcnJvcilcblxuICAgICAgICBvbkNvbXBsZXRlKFxuICAgICAgICAgIGAke2ZpZ3VyZXMuY3Jvc3N9IFVuZXhwZWN0ZWQgZXJyb3IgZHVyaW5nIHZhbGlkYXRpb246ICR7ZXJyb3JNZXNzYWdlKGVycm9yKX1gLFxuICAgICAgICApXG4gICAgICB9XG4gICAgfVxuXG4gICAgdm9pZCBydW5WYWxpZGF0aW9uKClcbiAgfSwgW29uQ29tcGxldGUsIHBhdGhdKVxuXG4gIHJldHVybiAoXG4gICAgPEJveCBmbGV4RGlyZWN0aW9uPVwiY29sdW1uXCI+XG4gICAgICA8VGV4dD5SdW5uaW5nIHZ
Line truncated
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { Command } from '../../commands.js';
|
||||
const plugin = {
|
||||
type: 'local-jsx',
|
||||
name: 'plugin',
|
||||
aliases: ['plugins', 'marketplace'],
|
||||
description: 'Manage Claude Code plugins',
|
||||
immediate: true,
|
||||
load: () => import('./plugin.js')
|
||||
} satisfies Command;
|
||||
export default plugin;
|
||||
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJDb21tYW5kIiwicGx1Z2luIiwidHlwZSIsIm5hbWUiLCJhbGlhc2VzIiwiZGVzY3JpcHRpb24iLCJpbW1lZGlhdGUiLCJsb2FkIl0sInNvdXJjZXMiOlsiaW5kZXgudHN4Il0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB0eXBlIHsgQ29tbWFuZCB9IGZyb20gJy4uLy4uL2NvbW1hbmRzLmpzJ1xuXG5jb25zdCBwbHVnaW4gPSB7XG4gIHR5cGU6ICdsb2NhbC1qc3gnLFxuICBuYW1lOiAncGx1Z2luJyxcbiAgYWxpYXNlczogWydwbHVnaW5zJywgJ21hcmtldHBsYWNlJ10sXG4gIGRlc2NyaXB0aW9uOiAnTWFuYWdlIENsYXVkZSBDb2RlIHBsdWdpbnMnLFxuICBpbW1lZGlhdGU6IHRydWUsXG4gIGxvYWQ6ICgpID0+IGltcG9ydCgnLi9wbHVnaW4uanMnKSxcbn0gc2F0aXNmaWVzIENvbW1hbmRcblxuZXhwb3J0IGRlZmF1bHQgcGx1Z2luXG4iXSwibWFwcGluZ3MiOiJBQUFBLGNBQWNBLE9BQU8sUUFBUSxtQkFBbUI7QUFFaEQsTUFBTUMsTUFBTSxHQUFHO0VBQ2JDLElBQUksRUFBRSxXQUFXO0VBQ2pCQyxJQUFJLEVBQUUsUUFBUTtFQUNkQyxPQUFPLEVBQUUsQ0FBQyxTQUFTLEVBQUUsYUFBYSxDQUFDO0VBQ25DQyxXQUFXLEVBQUUsNEJBQTRCO0VBQ3pDQyxTQUFTLEVBQUUsSUFBSTtFQUNmQyxJQUFJLEVBQUVBLENBQUEsS0FBTSxNQUFNLENBQUMsYUFBYTtBQUNsQyxDQUFDLFdBQVdQLE9BQU87QUFFbkIsZUFBZUMsTUFBTSIsImlnbm9yZUxpc3QiOltdfQ==
|
||||
@@ -0,0 +1,103 @@
|
||||
// Parse plugin subcommand arguments into structured commands
|
||||
export type ParsedCommand =
|
||||
| { type: 'menu' }
|
||||
| { type: 'help' }
|
||||
| { type: 'install'; marketplace?: string; plugin?: string }
|
||||
| { type: 'manage' }
|
||||
| { type: 'uninstall'; plugin?: string }
|
||||
| { type: 'enable'; plugin?: string }
|
||||
| { type: 'disable'; plugin?: string }
|
||||
| { type: 'validate'; path?: string }
|
||||
| {
|
||||
type: 'marketplace'
|
||||
action?: 'add' | 'remove' | 'update' | 'list'
|
||||
target?: string
|
||||
}
|
||||
|
||||
export function parsePluginArgs(args?: string): ParsedCommand {
|
||||
if (!args) {
|
||||
return { type: 'menu' }
|
||||
}
|
||||
|
||||
const parts = args.trim().split(/\s+/)
|
||||
const command = parts[0]?.toLowerCase()
|
||||
|
||||
switch (command) {
|
||||
case 'help':
|
||||
case '--help':
|
||||
case '-h':
|
||||
return { type: 'help' }
|
||||
|
||||
case 'install':
|
||||
case 'i': {
|
||||
const target = parts[1]
|
||||
if (!target) {
|
||||
return { type: 'install' }
|
||||
}
|
||||
|
||||
// Check if it's in format plugin@marketplace
|
||||
if (target.includes('@')) {
|
||||
const [plugin, marketplace] = target.split('@')
|
||||
return { type: 'install', plugin, marketplace }
|
||||
}
|
||||
|
||||
// Check if the target looks like a marketplace (URL or path)
|
||||
const isMarketplace =
|
||||
target.startsWith('http://') ||
|
||||
target.startsWith('https://') ||
|
||||
target.startsWith('file://') ||
|
||||
target.includes('/') ||
|
||||
target.includes('\\')
|
||||
|
||||
if (isMarketplace) {
|
||||
// This is a marketplace URL/path, no plugin specified
|
||||
return { type: 'install', marketplace: target }
|
||||
}
|
||||
|
||||
// Otherwise treat it as a plugin name
|
||||
return { type: 'install', plugin: target }
|
||||
}
|
||||
|
||||
case 'manage':
|
||||
return { type: 'manage' }
|
||||
|
||||
case 'uninstall':
|
||||
return { type: 'uninstall', plugin: parts[1] }
|
||||
|
||||
case 'enable':
|
||||
return { type: 'enable', plugin: parts[1] }
|
||||
|
||||
case 'disable':
|
||||
return { type: 'disable', plugin: parts[1] }
|
||||
|
||||
case 'validate': {
|
||||
const target = parts.slice(1).join(' ').trim()
|
||||
return { type: 'validate', path: target || undefined }
|
||||
}
|
||||
|
||||
case 'marketplace':
|
||||
case 'market': {
|
||||
const action = parts[1]?.toLowerCase()
|
||||
const target = parts.slice(2).join(' ')
|
||||
|
||||
switch (action) {
|
||||
case 'add':
|
||||
return { type: 'marketplace', action: 'add', target }
|
||||
case 'remove':
|
||||
case 'rm':
|
||||
return { type: 'marketplace', action: 'remove', target }
|
||||
case 'update':
|
||||
return { type: 'marketplace', action: 'update', target }
|
||||
case 'list':
|
||||
return { type: 'marketplace', action: 'list' }
|
||||
default:
|
||||
// No action specified, show marketplace menu
|
||||
return { type: 'marketplace' }
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
// Unknown command, show menu
|
||||
return { type: 'menu' }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
import { PluginSettings } from './PluginSettings.js';
|
||||
export async function call(onDone: LocalJSXCommandOnDone, _context: unknown, args?: string): Promise<React.ReactNode> {
|
||||
return <PluginSettings onComplete={onDone} args={args} />;
|
||||
}
|
||||
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsIkxvY2FsSlNYQ29tbWFuZE9uRG9uZSIsIlBsdWdpblNldHRpbmdzIiwiY2FsbCIsIm9uRG9uZSIsIl9jb250ZXh0IiwiYXJncyIsIlByb21pc2UiLCJSZWFjdE5vZGUiXSwic291cmNlcyI6WyJwbHVnaW4udHN4Il0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCAqIGFzIFJlYWN0IGZyb20gJ3JlYWN0J1xuaW1wb3J0IHR5cGUgeyBMb2NhbEpTWENvbW1hbmRPbkRvbmUgfSBmcm9tICcuLi8uLi90eXBlcy9jb21tYW5kLmpzJ1xuaW1wb3J0IHsgUGx1Z2luU2V0dGluZ3MgfSBmcm9tICcuL1BsdWdpblNldHRpbmdzLmpzJ1xuXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gY2FsbChcbiAgb25Eb25lOiBMb2NhbEpTWENvbW1hbmRPbkRvbmUsXG4gIF9jb250ZXh0OiB1bmtub3duLFxuICBhcmdzPzogc3RyaW5nLFxuKTogUHJvbWlzZTxSZWFjdC5SZWFjdE5vZGU+IHtcbiAgcmV0dXJuIDxQbHVnaW5TZXR0aW5ncyBvbkNvbXBsZXRlPXtvbkRvbmV9IGFyZ3M9e2FyZ3N9IC8+XG59XG4iXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBS0EsS0FBSyxNQUFNLE9BQU87QUFDOUIsY0FBY0MscUJBQXFCLFFBQVEsd0JBQXdCO0FBQ25FLFNBQVNDLGNBQWMsUUFBUSxxQkFBcUI7QUFFcEQsT0FBTyxlQUFlQyxJQUFJQSxDQUN4QkMsTUFBTSxFQUFFSCxxQkFBcUIsRUFDN0JJLFFBQVEsRUFBRSxPQUFPLEVBQ2pCQyxJQUFhLENBQVIsRUFBRSxNQUFNLENBQ2QsRUFBRUMsT0FBTyxDQUFDUCxLQUFLLENBQUNRLFNBQVMsQ0FBQyxDQUFDO0VBQzFCLE9BQU8sQ0FBQyxjQUFjLENBQUMsVUFBVSxDQUFDLENBQUNKLE1BQU0sQ0FBQyxDQUFDLElBQUksQ0FBQyxDQUFDRSxJQUFJLENBQUMsR0FBRztBQUMzRCIsImlnbm9yZUxpc3QiOltdfQ==
|
||||
@@ -0,0 +1,117 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
/**
|
||||
* Shared helper functions and types for plugin details views
|
||||
*
|
||||
* Used by both DiscoverPlugins and BrowseMarketplace components.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.js';
|
||||
import { Byline } from '../../components/design-system/Byline.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import type { PluginMarketplaceEntry } from '../../utils/plugins/schemas.js';
|
||||
|
||||
/**
|
||||
* Represents a plugin available for installation from a marketplace
|
||||
*/
|
||||
export type InstallablePlugin = {
|
||||
entry: PluginMarketplaceEntry;
|
||||
marketplaceName: string;
|
||||
pluginId: string;
|
||||
isInstalled: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Menu option for plugin details view
|
||||
*/
|
||||
export type PluginDetailsMenuOption = {
|
||||
label: string;
|
||||
action: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract GitHub repo info from a plugin's source
|
||||
*/
|
||||
export function extractGitHubRepo(plugin: InstallablePlugin): string | null {
|
||||
const isGitHub = plugin.entry.source && typeof plugin.entry.source === 'object' && 'source' in plugin.entry.source && plugin.entry.source.source === 'github';
|
||||
if (isGitHub && typeof plugin.entry.source === 'object' && 'repo' in plugin.entry.source) {
|
||||
return plugin.entry.source.repo;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build menu options for plugin details view with scoped installation options
|
||||
*/
|
||||
export function buildPluginDetailsMenuOptions(hasHomepage: string | undefined, githubRepo: string | null): PluginDetailsMenuOption[] {
|
||||
const options: PluginDetailsMenuOption[] = [{
|
||||
label: 'Install for you (user scope)',
|
||||
action: 'install-user'
|
||||
}, {
|
||||
label: 'Install for all collaborators on this repository (project scope)',
|
||||
action: 'install-project'
|
||||
}, {
|
||||
label: 'Install for you, in this repo only (local scope)',
|
||||
action: 'install-local'
|
||||
}];
|
||||
if (hasHomepage) {
|
||||
options.push({
|
||||
label: 'Open homepage',
|
||||
action: 'homepage'
|
||||
});
|
||||
}
|
||||
if (githubRepo) {
|
||||
options.push({
|
||||
label: 'View on GitHub',
|
||||
action: 'github'
|
||||
});
|
||||
}
|
||||
options.push({
|
||||
label: 'Back to plugin list',
|
||||
action: 'back'
|
||||
});
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Key hint component for plugin selection screens
|
||||
*/
|
||||
export function PluginSelectionKeyHint(t0) {
|
||||
const $ = _c(7);
|
||||
const {
|
||||
hasSelection
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] !== hasSelection) {
|
||||
t1 = hasSelection && <ConfigurableShortcutHint action="plugin:install" context="Plugin" fallback="i" description="install" bold={true} />;
|
||||
$[0] = hasSelection;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
let t2;
|
||||
let t3;
|
||||
let t4;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <ConfigurableShortcutHint action="plugin:toggle" context="Plugin" fallback="Space" description="toggle" />;
|
||||
t3 = <ConfigurableShortcutHint action="select:accept" context="Select" fallback="Enter" description="details" />;
|
||||
t4 = <ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="back" />;
|
||||
$[2] = t2;
|
||||
$[3] = t3;
|
||||
$[4] = t4;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
t3 = $[3];
|
||||
t4 = $[4];
|
||||
}
|
||||
let t5;
|
||||
if ($[5] !== t1) {
|
||||
t5 = <Box marginTop={1}><Text dimColor={true} italic={true}><Byline>{t1}{t2}{t3}{t4}</Byline></Text></Box>;
|
||||
$[5] = t1;
|
||||
$[6] = t5;
|
||||
} else {
|
||||
t5 = $[6];
|
||||
}
|
||||
return t5;
|
||||
}
|
||||
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsIkNvbmZpZ3VyYWJsZVNob3J0Y3V0SGludCIsIkJ5bGluZSIsIkJveCIsIlRleHQiLCJQbHVnaW5NYXJrZXRwbGFjZUVudHJ5IiwiSW5zdGFsbGFibGVQbHVnaW4iLCJlbnRyeSIsIm1hcmtldHBsYWNlTmFtZSIsInBsdWdpbklkIiwiaXNJbnN0YWxsZWQiLCJQbHVnaW5EZXRhaWxzTWVudU9wdGlvbiIsImxhYmVsIiwiYWN0aW9uIiwiZXh0cmFjdEdpdEh1YlJlcG8iLCJwbHVnaW4iLCJpc0dpdEh1YiIsInNvdXJjZSIsInJlcG8iLCJidWlsZFBsdWdpbkRldGFpbHNNZW51T3B0aW9ucyIsImhhc0hvbWVwYWdlIiwiZ2l0aHViUmVwbyIsIm9wdGlvbnMiLCJwdXNoIiwiUGx1Z2luU2VsZWN0aW9uS2V5SGludCIsInQwIiwiJCIsIl9jIiwiaGFzU2VsZWN0aW9uIiwidDEiLCJ0MiIsInQzIiwidDQiLCJTeW1ib2wiLCJmb3IiLCJ0NSJdLCJzb3VyY2VzIjpbInBsdWdpbkRldGFpbHNIZWxwZXJzLnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyIvKipcbiAqIFNoYXJlZCBoZWxwZXIgZnVuY3Rpb25zIGFuZCB0eXBlcyBmb3IgcGx1Z2luIGRldGFpbHMgdmlld3NcbiAqXG4gKiBVc2VkIGJ5IGJvdGggRGlzY292ZXJQbHVnaW5zIGFuZCBCcm93c2VNYXJrZXRwbGFjZSBjb21wb25lbnRzLlxuICovXG5cbmltcG9ydCAqIGFzIFJlYWN0IGZyb20gJ3JlYWN0J1xuaW1wb3J0IHsgQ29uZmlndXJhYmxlU2hvcnRjdXRIaW50IH0gZnJvbSAnLi4vLi4vY29tcG9uZW50cy9Db25maWd1cmFibGVTaG9ydGN1dEhpbnQuanMnXG5pbXBvcnQgeyBCeWxpbmUgfSBmcm9tICcuLi8uLi9jb21wb25lbnRzL2Rlc2lnbi1zeXN0ZW0vQnlsaW5lLmpzJ1xuaW1wb3J0IHsgQm94LCBUZXh0IH0gZnJvbSAnLi4vLi4vaW5rLmpzJ1xuaW1wb3J0IHR5cGUgeyBQbHVnaW5NYXJrZXRwbGFjZUVudHJ5IH0gZnJvbSAnLi4vLi4vdXRpbHMvcGx1Z2lucy9zY2hlbWFzLmpzJ1xuXG4vKipcbiAqIFJlcHJlc2VudHMgYSBwbHVnaW4gYXZhaWxhYmxlIGZvciBpbnN0YWxsYXRpb24gZnJvbSBhIG1hcmtldHBsYWNlXG4gKi9cbmV4cG9ydCB0eXBlIEluc3RhbGxhYmxlUGx1Z2luID0ge1xuICBlbnRyeTogUGx1Z2luTWFya2V0cGxhY2VFbnRyeVxuICBtYXJrZXRwbGFjZU5hbWU6IHN0cmluZ1xuICBwbHVnaW5JZDogc3RyaW5nXG4gIGlzSW5zdGFsbGVkOiBib29sZWFuXG59XG5cbi8qKlxuICogTWVudSBvcHRpb24gZm9yIHBsdWdpbiBkZXRhaWxzIHZpZXdcbiAqL1xuZXhwb3J0IHR5cGUgUGx1Z2luRGV0YWlsc01lbnVPcHRpb24gPSB7XG4gIGxhYmVsOiBzdHJpbmdcbiAgYWN0aW9uOiBzdHJpbmdcbn1cblxuLyoqXG4gKiBFeHRyYWN0IEdpdEh1YiByZXBvIGluZm8gZnJvbSBhIHBsdWdpbidzIHNvdXJjZVxuICovXG5leHBvcnQgZnVuY3Rpb24gZXh0cmFjdEdpdEh1YlJlcG8ocGx1Z2luOiBJbnN0YWxsYWJsZVBsdWdpbik6IHN0cmluZyB8IG51bGwge1xuICBjb25zdCBpc0dpdEh1YiA9XG4gICAgcGx1Z2luLmVudHJ5LnNvdXJjZSAmJlxuICAgIHR5cGVvZiBwbHVnaW4uZW50cnkuc291cmNlID09PSAnb2JqZWN0JyAmJlxuICAgICdzb3VyY2UnIGluIHBsdWdpbi5lbnRyeS5zb3VyY2UgJiZcbiAgICBwbHVnaW4uZW50cnkuc291cmNlLnNvdXJjZSA9PT0gJ2dpdGh1YidcblxuICBpZiAoXG4gICAgaXNHaXRIdWIgJiZcbiAgICB0eXBlb2YgcGx1Z2luLmVudHJ5LnNvdXJjZSA9PT0gJ29iamVjdCcgJiZcbiAgICAncmVwbycgaW4gcGx1Z2luLmVudHJ5LnNvdXJjZVxuICApIHtcbiAgICByZXR1cm4gcGx1Z2luLmVudHJ5LnNvdXJjZS5yZXBvXG4gIH1cblxuICByZXR1cm4gbnVsbFxufVxuXG4vKipcbiAqIEJ1aWxkIG1lbnUgb3B0aW9ucyBmb3IgcGx1Z2luIGRldGFpbHMgdmlldyB3aXRoIHNjb3BlZCBpbnN0YWxsYXRpb24gb3B0aW9uc1xuICovXG5leHBvcnQgZnVuY3Rpb24gYnVpbGRQbHVnaW5EZXRhaWxzTWVudU9wdGlvbnMoXG4gIGhhc0hvbWVwYWdlOiBzdHJpbmcgfCB1bmRlZmluZWQsXG4gIGdpdGh1YlJlcG86IHN0cmluZyB8IG51bGwsXG4pOiBQbHVnaW5EZXRhaWxzTWVudU9wdGlvbltdIHtcbiAgY29uc3Qgb3B0aW9uczogUGx1Z2luRGV0YWlsc01lbnVPcHRpb25bXSA9IFtcbiAgICB7IGxhYmVsOiAnSW5zdGFsbCBmb3IgeW91ICh1c2VyIHNjb3BlKScsIGFjdGlvbjogJ2luc3RhbGwtdXNlcicgfSxcbiAgICB7XG4gICAgICBsYWJlbDogJ0luc3RhbGwgZm9yIGFsbCBjb2xsYWJvcmF0b3JzIG9uIHRoaXMgcmVwb3NpdG9yeSAocHJvamVjdCBzY29wZSknLFxuICAgICAgYWN0aW9uOiAnaW5zdGFsbC1wcm9qZWN0JyxcbiAgICB9LFxuICAgIHtcbiAgICAgIGxhYmVsOiAnSW5zdGFsbCBmb3IgeW91LCBpbiB0aGlzIHJlcG8gb25seSAobG9jYWwgc2NvcGUpJyxcbiAgICAgIGFjdGlvbjogJ2luc3RhbGwtbG9jYWwnLFxuICAgIH0sXG4gIF1cbiAgaWYgKGhhc0hvbWVwYWdlKSB7XG4gICAgb3B0aW9ucy5wdXNoKHsgbGFiZWw6ICdPcGVuIGhvbWVwYWdlJywgYWN0aW9uOiAnaG9tZXBhZ2UnIH0pXG4gIH1cbiAgaWYgKGdpdGh1YlJlcG8pIHtcbiAgICBvcHRpb25zLnB1c2goeyBsYWJlbDogJ1ZpZXcgb24gR2l0SHViJywgYWN0aW9uOiAnZ2l0aHViJyB9KVxuICB9XG4gIG9wdGlvbnMucHVzaCh7IGxhYmVsOiAnQmFjayB0byBwbHVnaW4gbGlzdCcsIGFjdGlvbjogJ2JhY2snIH0pXG4gIHJldHVybiBvcHRpb25zXG59XG5cbi8qKlxuICogS2V5IGhpbnQgY29tcG9uZW50IGZvciBwbHVnaW4gc2VsZWN0aW9uIHNjcmVlbnNcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIFBsdWdpblNlbGVjdGlvbktleUhpbnQoe1xuICBoYXNTZWxlY3Rpb24sXG59OiB7XG4gIGhhc1NlbGVjdGlvbjogYm9vbGVhblxufSk6IFJlYWN0LlJlYWN0Tm9kZSB7XG4gIHJldHVybiAoXG4gICAgPEJveCBtYXJnaW5Ub3A9ezF9PlxuICAgICAgPFRleHQgZGltQ29sb3IgaXRhbGljPlxuICAgICAgICA8QnlsaW5lPlxuICAgICAgICAgIHtoYXNTZWxlY3Rpb24gJiYgKFxuICAgICAgICAgICAgPENvbmZpZ3VyYWJsZVNob3J0Y3V0SGludFxuICAgICAgICAgICAgICBhY3Rpb249XCJwbHVnaW46aW5zdGFsbFwiXG4gICAgICAgICAgICAgIGNvbnRleHQ9XCJQbHVnaW5cIlxuICAgICAgICAgICAgICBmYWxsYmFjaz1cImlcIlxuICAgICAgICAgICAgICBkZXNjcmlwdGlvbj1cImluc3RhbGxcIlxuICAgICAgICAgICAgICBib2xkXG4gICAgICAgICAgICAvPlxuICAgICAgICAgICl9XG4gICAgICAgICAgPENvbmZpZ3VyYWJsZVNob3J0Y3V0SGludFxuICAgICAgICAgICAgYWN0aW9uPVwicGx1Z2luOnRvZ2dsZVwiXG4gICAgICAgICAgICBjb250ZXh0PVwiUGx1Z2luXCJcbiAgICAgICAgICAgIGZhbGxiYWNrPVwiU3BhY2VcIlxuICAgICAgICAgICAgZGVzY3JpcHRpb249XCJ0b2dnbGVcIlxuICAgICAgICAgIC8+XG4gICAgICAgICAgPENvbmZpZ3VyYWJsZVNob3J0Y3V0SGludFxuICAgICAgICAgICAgYWN0aW9uPVwic2VsZWN0OmFjY2VwdFwiXG4gICAgICAgICAgICBjb250ZXh0PVwiU2VsZWN0XCJcbiAgICAgICAgICAgIGZhbGxiYWNrPVwiRW50ZXJcIlxuICAgICAgICAgICAgZGVzY3JpcHRpb249XCJkZXRhaWxzXCJcbiAgICAgICAgICAvPlxuICAgICAgICAgIDxDb25maWd1cmFibGVTaG9ydGN1dEhpbnRcbiAgICAgICAgICAgIGFjdGlvbj1cImNvbmZpcm06bm9cIlxuICAgICAgICAgICAgY29udGV4dD1cIkNvbmZpcm1hdGl
Line truncated
|
||||
@@ -0,0 +1,171 @@
|
||||
import { useCallback, useMemo, useRef } from 'react'
|
||||
|
||||
const DEFAULT_MAX_VISIBLE = 5
|
||||
|
||||
type UsePaginationOptions = {
|
||||
totalItems: number
|
||||
maxVisible?: number
|
||||
selectedIndex?: number
|
||||
}
|
||||
|
||||
type UsePaginationResult<T> = {
|
||||
// For backwards compatibility with page-based terminology
|
||||
currentPage: number
|
||||
totalPages: number
|
||||
startIndex: number
|
||||
endIndex: number
|
||||
needsPagination: boolean
|
||||
pageSize: number
|
||||
// Get visible slice of items
|
||||
getVisibleItems: (items: T[]) => T[]
|
||||
// Convert visible index to actual index
|
||||
toActualIndex: (visibleIndex: number) => number
|
||||
// Check if actual index is visible
|
||||
isOnCurrentPage: (actualIndex: number) => boolean
|
||||
// Navigation (kept for API compatibility)
|
||||
goToPage: (page: number) => void
|
||||
nextPage: () => void
|
||||
prevPage: () => void
|
||||
// Handle selection - just updates the index, scrolling is automatic
|
||||
handleSelectionChange: (
|
||||
newIndex: number,
|
||||
setSelectedIndex: (index: number) => void,
|
||||
) => void
|
||||
// Page navigation - returns false for continuous scrolling (not needed)
|
||||
handlePageNavigation: (
|
||||
direction: 'left' | 'right',
|
||||
setSelectedIndex: (index: number) => void,
|
||||
) => boolean
|
||||
// Scroll position info for UI display
|
||||
scrollPosition: {
|
||||
current: number
|
||||
total: number
|
||||
canScrollUp: boolean
|
||||
canScrollDown: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export function usePagination<T>({
|
||||
totalItems,
|
||||
maxVisible = DEFAULT_MAX_VISIBLE,
|
||||
selectedIndex = 0,
|
||||
}: UsePaginationOptions): UsePaginationResult<T> {
|
||||
const needsPagination = totalItems > maxVisible
|
||||
|
||||
// Use a ref to track the previous scroll offset for smooth scrolling
|
||||
const scrollOffsetRef = useRef(0)
|
||||
|
||||
// Compute the scroll offset based on selectedIndex
|
||||
// This ensures the selected item is always visible
|
||||
const scrollOffset = useMemo(() => {
|
||||
if (!needsPagination) return 0
|
||||
|
||||
const prevOffset = scrollOffsetRef.current
|
||||
|
||||
// If selected item is above the visible window, scroll up
|
||||
if (selectedIndex < prevOffset) {
|
||||
scrollOffsetRef.current = selectedIndex
|
||||
return selectedIndex
|
||||
}
|
||||
|
||||
// If selected item is below the visible window, scroll down
|
||||
if (selectedIndex >= prevOffset + maxVisible) {
|
||||
const newOffset = selectedIndex - maxVisible + 1
|
||||
scrollOffsetRef.current = newOffset
|
||||
return newOffset
|
||||
}
|
||||
|
||||
// Selected item is within visible window, keep current offset
|
||||
// But ensure offset is still valid
|
||||
const maxOffset = Math.max(0, totalItems - maxVisible)
|
||||
const clampedOffset = Math.min(prevOffset, maxOffset)
|
||||
scrollOffsetRef.current = clampedOffset
|
||||
return clampedOffset
|
||||
}, [selectedIndex, maxVisible, needsPagination, totalItems])
|
||||
|
||||
const startIndex = scrollOffset
|
||||
const endIndex = Math.min(scrollOffset + maxVisible, totalItems)
|
||||
|
||||
const getVisibleItems = useCallback(
|
||||
(items: T[]): T[] => {
|
||||
if (!needsPagination) return items
|
||||
return items.slice(startIndex, endIndex)
|
||||
},
|
||||
[needsPagination, startIndex, endIndex],
|
||||
)
|
||||
|
||||
const toActualIndex = useCallback(
|
||||
(visibleIndex: number): number => {
|
||||
return startIndex + visibleIndex
|
||||
},
|
||||
[startIndex],
|
||||
)
|
||||
|
||||
const isOnCurrentPage = useCallback(
|
||||
(actualIndex: number): boolean => {
|
||||
return actualIndex >= startIndex && actualIndex < endIndex
|
||||
},
|
||||
[startIndex, endIndex],
|
||||
)
|
||||
|
||||
// These are mostly no-ops for continuous scrolling but kept for API compatibility
|
||||
const goToPage = useCallback((_page: number) => {
|
||||
// No-op - scrolling is controlled by selectedIndex
|
||||
}, [])
|
||||
|
||||
const nextPage = useCallback(() => {
|
||||
// No-op - scrolling is controlled by selectedIndex
|
||||
}, [])
|
||||
|
||||
const prevPage = useCallback(() => {
|
||||
// No-op - scrolling is controlled by selectedIndex
|
||||
}, [])
|
||||
|
||||
// Simple selection handler - just updates the index
|
||||
// Scrolling happens automatically via the useMemo above
|
||||
const handleSelectionChange = useCallback(
|
||||
(newIndex: number, setSelectedIndex: (index: number) => void) => {
|
||||
const clampedIndex = Math.max(0, Math.min(newIndex, totalItems - 1))
|
||||
setSelectedIndex(clampedIndex)
|
||||
},
|
||||
[totalItems],
|
||||
)
|
||||
|
||||
// Page navigation - disabled for continuous scrolling
|
||||
const handlePageNavigation = useCallback(
|
||||
(
|
||||
_direction: 'left' | 'right',
|
||||
_setSelectedIndex: (index: number) => void,
|
||||
): boolean => {
|
||||
return false
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
// Calculate page-like values for backwards compatibility
|
||||
const totalPages = Math.max(1, Math.ceil(totalItems / maxVisible))
|
||||
const currentPage = Math.floor(scrollOffset / maxVisible)
|
||||
|
||||
return {
|
||||
currentPage,
|
||||
totalPages,
|
||||
startIndex,
|
||||
endIndex,
|
||||
needsPagination,
|
||||
pageSize: maxVisible,
|
||||
getVisibleItems,
|
||||
toActualIndex,
|
||||
isOnCurrentPage,
|
||||
goToPage,
|
||||
nextPage,
|
||||
prevPage,
|
||||
handleSelectionChange,
|
||||
handlePageNavigation,
|
||||
scrollPosition: {
|
||||
current: selectedIndex + 1,
|
||||
total: totalItems,
|
||||
canScrollUp: scrollOffset > 0,
|
||||
canScrollDown: scrollOffset + maxVisible < totalItems,
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user