更新大量 tsx 原始文件; 已经迁移 login panel; 部分 (#121)

* style(B1-1): 格式化 ink/buddy/cli/context/screens/tasks/services/keybindings/state (43 files)

纯格式化:移除分号、React Compiler import、import 多行展开。
修复了 Box.tsx 和 ScrollBox.tsx 中无效的 global.d.ts import。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-2): 格式化 commands (79 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-3): 格式化 components/messages,permissions,mcp,sandbox,shell (104 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-4): 格式化 components/PromptInput,FeedbackSurvey,tasks,agents,skills,design-system,wizard (73 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-5): 格式化 components其余 + hooks + tools (232 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-6): 格式化 main/entrypoints/utils/moreright (21 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: 更新 README,新增 Run.ps1/TODO.md,删除 V6.md

- README.md: 大幅重写,更详细版本历史和配置示例
- Run.ps1: 新增 Windows 启动脚本
- TODO.md: 新增包完成清单
- V6.md: 删除(架构重构规划已不适用)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: 修复以前的问题

* fix: 修复 login 面板的问题

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
claude-code-best
2026-04-04 23:24:27 +08:00
committed by GitHub
parent 02694918b5
commit 5b1a52b8e0
559 changed files with 103807 additions and 101817 deletions

View File

@@ -1,31 +1,39 @@
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';
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;
};
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,
@@ -37,90 +45,100 @@ export function AddMarketplace({
setResult,
setViewState,
onAddComplete,
cliMode = false
cliMode = false,
}: Props): React.ReactNode {
const hasAttemptedAutoAdd = useRef(false);
const [isLoading, setLoading] = useState(false);
const [progressMessage, setProgressMessage] = useState<string>('');
const hasAttemptedAutoAdd = useRef(false)
const [isLoading, setLoading] = useState(false)
const [progressMessage, setProgressMessage] = useState<string>('')
const handleAdd = async () => {
const input = inputValue.trim();
const input = inputValue.trim()
if (!input) {
setError('Please enter a marketplace source');
return;
setError('Please enter a marketplace source')
return
}
const parsed = await parseMarketplaceInput(input);
const parsed = await parseMarketplaceInput(input)
if (!parsed) {
setError('Invalid marketplace source format. Try: owner/repo, https://..., or ./path');
return;
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(parsed.error)
return
}
setError(null);
setError(null)
try {
setLoading(true);
setProgressMessage('');
const {
name,
resolvedSource
} = await addMarketplaceSource(parsed, message => {
setProgressMessage(message);
});
saveMarketplaceToSettings(name, {
source: resolvedSource
});
clearAllCaches();
let sourceType = parsed.source;
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;
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
});
source_type:
sourceType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
if (onAddComplete) {
await onAddComplete();
await onAddComplete()
}
setProgressMessage('');
setLoading(false);
setProgressMessage('')
setLoading(false)
if (cliMode) {
// In CLI mode, set result to trigger completion
setResult(`Successfully added marketplace: ${name}`);
setResult(`Successfully added marketplace: ${name}`)
} else {
// In interactive mode, switch to browse view
setViewState({
type: 'browse-marketplace',
targetMarketplace: name
});
setViewState({ type: 'browse-marketplace', targetMarketplace: name })
}
} catch (err) {
const error = toError(err);
logError(error);
setError(error.message);
setProgressMessage('');
setLoading(false);
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}`);
setResult(`Error: ${error.message}`)
} else {
setResult(null);
setResult(null)
}
}
};
}
// Auto-add if inputValue is provided
useEffect(() => {
if (inputValue && !hasAttemptedAutoAdd.current && !error && !result) {
hasAttemptedAutoAdd.current = true;
void handleAdd();
hasAttemptedAutoAdd.current = true
void handleAdd()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
// biome-ignore lint/correctness/useExhaustiveDependencies: intentional
}, []); // Only run once on mount
}, []) // Only run once on mount
return <Box flexDirection="column">
return (
<Box flexDirection="column">
<Box flexDirection="column" paddingX={1} borderStyle="round">
<Box marginBottom={1}>
<Text bold>Add Marketplace</Text>
@@ -133,29 +151,50 @@ export function AddMarketplace({
<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 />
<TextInput
value={inputValue}
onChange={setInputValue}
onSubmit={handleAdd}
columns={80}
cursorOffset={cursorOffset}
onChangeCursorOffset={setCursorOffset}
focus
showCursor
/>
</Box>
</Box>
{isLoading && <Box marginTop={1}>
{isLoading && (
<Box marginTop={1}>
<Spinner />
<Text>
{progressMessage || 'Adding marketplace to configuration…'}
</Text>
</Box>}
{error && <Box marginTop={1}>
</Box>
)}
{error && (
<Box marginTop={1}>
<Text color="error">{error}</Text>
</Box>}
{result && <Box marginTop={1}>
</Box>
)}
{result && (
<Box marginTop={1}>
<Text>{result}</Text>
</Box>}
</Box>
)}
</Box>
<Box marginLeft={3}>
<Text dimColor italic>
<Byline>
<KeyboardShortcutHint shortcut="Enter" action="add" />
<ConfigurableShortcutHint action="confirm:no" context="Settings" fallback="Esc" description="cancel" />
<ConfigurableShortcutHint
action="confirm:no"
context="Settings"
fallback="Esc"
description="cancel"
/>
</Byline>
</Text>
</Box>
</Box>;
</Box>
)
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,123 +1,140 @@
import { getPluginErrorMessage, type PluginError } from '../../types/plugin.js';
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}`;
return `${error.component} path not found: ${error.path}`
case 'git-auth-failed':
return `Git ${error.authType.toUpperCase()} authentication failed for ${error.gitUrl}`;
return `Git ${error.authType.toUpperCase()} authentication failed for ${error.gitUrl}`
case 'git-timeout':
return `Git ${error.operation} timed out for ${error.gitUrl}`;
return `Git ${error.operation} timed out for ${error.gitUrl}`
case 'network-error':
return `Network error accessing ${error.url}${error.details ? `: ${error.details}` : ''}`;
return `Network error accessing ${error.url}${error.details ? `: ${error.details}` : ''}`
case 'manifest-parse-error':
return `Failed to parse manifest at ${error.manifestPath}: ${error.parseError}`;
return `Failed to parse manifest at ${error.manifestPath}: ${error.parseError}`
case 'manifest-validation-error':
return `Invalid manifest at ${error.manifestPath}: ${error.validationErrors.join(', ')}`;
return `Invalid manifest at ${error.manifestPath}: ${error.validationErrors.join(', ')}`
case 'plugin-not-found':
return `Plugin "${error.pluginId}" not found in marketplace "${error.marketplace}"`;
return `Plugin "${error.pluginId}" not found in marketplace "${error.marketplace}"`
case 'marketplace-not-found':
return `Marketplace "${error.marketplace}" not found`;
return `Marketplace "${error.marketplace}" not found`
case 'marketplace-load-failed':
return `Failed to load marketplace "${error.marketplace}": ${error.reason}`;
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}`;
}
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}`;
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}`;
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}`;
return `Failed to download MCPB from ${error.url}: ${error.reason}`
case 'mcpb-extract-failed':
return `Failed to extract MCPB ${error.mcpbPath}: ${error.reason}`;
return `Failed to extract MCPB ${error.mcpbPath}: ${error.reason}`
case 'mcpb-invalid-manifest':
return `MCPB manifest invalid at ${error.mcpbPath}: ${error.validationError}`;
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`;
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`;
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}`;
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}`;
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'}`;
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`;
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}`;
return `LSP server "${error.serverName}" ${error.method} failed: ${error.error}`
case 'plugin-cache-miss':
return `Plugin "${error.plugin}" not cached at ${error.installPath}`;
return `Plugin "${error.plugin}" not cached at ${error.installPath}`
case 'generic-error':
return error.error;
return error.error
}
const _exhaustive: never = error;
return getPluginErrorMessage(_exhaustive);
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';
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';
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';
return 'Check your internet connection and try again'
case 'manifest-parse-error':
return 'Check manifest file syntax in the plugin directory';
return 'Check manifest file syntax in the plugin directory'
case 'manifest-validation-error':
return 'Check manifest file follows the required schema';
return 'Check manifest file follows the required schema'
case 'plugin-not-found':
return `Plugin may not exist in marketplace "${error.marketplace}"`;
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';
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`;
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';
return 'Check hooks.json file syntax and structure'
case 'component-load-failed':
return `Check ${error.component} directory structure and file permissions`;
return `Check ${error.component} directory structure and file permissions`
case 'mcpb-download-failed':
return 'Check your internet connection and URL accessibility';
return 'Check your internet connection and URL accessibility'
case 'mcpb-extract-failed':
return 'Verify the MCPB file is valid and not corrupted';
return 'Verify the MCPB file is valid and not corrupted'
case 'mcpb-invalid-manifest':
return 'Contact the plugin author about the 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 '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';
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}"`;
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';
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';
return 'Check LSP server logs with --debug for details'
case 'plugin-cache-miss':
return 'Run /plugins to refresh the plugin cache';
return 'Run /plugins to refresh the plugin cache'
case 'marketplace-load-failed':
case 'generic-error':
return null;
return null
}
const _exhaustive: never = error;
return null;
const _exhaustive: never = error
return null
}

View File

@@ -1,13 +1,18 @@
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';
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';
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.
@@ -21,336 +26,189 @@ import type { PluginOptionSchema, PluginOptionValues } from '../../utils/plugins
*
* Exported for unit testing.
*/
export function buildFinalValues(fields: string[], collected: Record<string, string>, configSchema: PluginOptionSchema, initialValues: PluginOptionValues | undefined): PluginOptionValues {
const finalValues: PluginOptionValues = {};
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;
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;
if (value.trim() === '') continue
const num = Number(value)
finalValues[fieldKey] = Number.isNaN(num) ? value : num
} else if (schema?.type === 'boolean') {
finalValues[fieldKey] = isEnvTruthy(value);
finalValues[fieldKey] = isEnvTruthy(value)
} else {
finalValues[fieldKey] = value;
finalValues[fieldKey] = value
}
}
return finalValues;
return finalValues
}
type Props = {
title: string;
subtitle: string;
configSchema: PluginOptionSchema;
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,
initialValues?: PluginOptionValues
onSave: (config: PluginOptionValues) => void
onCancel: () => void
}
export function PluginOptionsDialog({
title,
subtitle,
configSchema,
initialValues,
onSave,
onCancel,
}: Props): React.ReactNode {
const fields = Object.keys(configSchema)
// Prepopulate from initialValues but skip sensitive fields — we don't
// want to echo secrets back into the text buffer.
const initialFor = useCallback(
(key: string): string => {
if (configSchema[key]?.sensitive === true) return ''
const v = initialValues?.[key]
return v === undefined ? '' : String(v)
},
[configSchema, initialValues],
)
const [currentFieldIndex, setCurrentFieldIndex] = useState(0)
const [values, setValues] = useState<Record<string, string>>({})
const [currentInput, setCurrentInput] = useState(() =>
fields[0] ? initialFor(fields[0]) : '',
)
const currentField = fields[currentFieldIndex]
const fieldSchema = currentField ? configSchema[currentField] : null
// Use Settings context so 'n' key doesn't cancel (allows typing 'n' in input).
// isCancelActive={false} on Dialog keeps its own confirm:no out of the way.
useKeybinding('confirm:no', onCancel, { context: 'Settings' })
// Tab to next field
const handleNextField = useCallback(() => {
if (currentFieldIndex < fields.length - 1 && currentField) {
setValues(prev => ({ ...prev, [currentField]: currentInput }))
setCurrentFieldIndex(prev => prev + 1)
const nextKey = fields[currentFieldIndex + 1]
setCurrentInput(nextKey ? initialFor(nextKey) : '')
}
}, [currentFieldIndex, fields, currentField, currentInput, initialFor])
// Enter to save current field and move to next, or save all if last
const handleConfirm = useCallback(() => {
if (!currentField) return
const newValues = { ...values, [currentField]: currentInput }
if (currentFieldIndex === fields.length - 1) {
onSave(buildFinalValues(fields, newValues, configSchema, initialValues))
} else {
// Move to next field
setValues(newValues)
setCurrentFieldIndex(prev => prev + 1)
const nextKey = fields[currentFieldIndex + 1]
setCurrentInput(nextKey ? initialFor(nextKey) : '')
}
}, [
currentField,
values,
currentInput,
currentFieldIndex,
fields,
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);
initialFor,
initialValues,
])
useKeybindings(
{
'confirm:nextField': handleNextField,
'confirm:yes': handleConfirm,
},
{ context: 'Confirmation' },
)
// Character input handling (backspace, typing)
useInput((char, key) => {
// Backspace
if (key.backspace || key.delete) {
setCurrentInput(prev => prev.slice(0, -1))
return
}
// Regular character input
if (char && !key.ctrl && !key.meta && !key.tab && !key.return) {
setCurrentInput(prev => prev + char)
}
})
if (!fieldSchema || !currentField) {
return null;
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;
const isSensitive = fieldSchema.sensitive === true
const isRequired = fieldSchema.required === true
const displayValue = isSensitive
? '*'.repeat(stringWidth(currentInput))
: currentInput
return (
<Dialog
title={title}
subtitle={subtitle}
onCancel={onCancel}
isCancelActive={false}
>
<Box flexDirection="column">
<Text bold={true}>
{fieldSchema.title || currentField}
{isRequired && <Text color="error"> *</Text>}
</Text>
{fieldSchema.description && (
<Text dimColor={true}>{fieldSchema.description}</Text>
)}
<Box marginTop={1}>
<Text>{figures.pointerSmall} </Text>
<Text>{displayValue}</Text>
<Text></Text>
</Box>
</Box>
<Box flexDirection="column">
<Text dimColor={true}>
Field {currentFieldIndex + 1} of {fields.length}
</Text>
{currentFieldIndex < fields.length - 1 && (
<Text dimColor={true}>
Tab: Next field · Enter: Save and continue
</Text>
)}
{currentFieldIndex === fields.length - 1 && (
<Text dimColor={true}>Enter: Save configuration</Text>
)}
</Box>
</Dialog>
)
}

View File

@@ -7,14 +7,26 @@
* 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';
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
@@ -24,12 +36,13 @@ import { PluginOptionsDialog } from './PluginOptionsDialog.js';
*
* 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);
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,
)
}
/**
@@ -37,37 +50,39 @@ export async function findPluginOptionsTarget(pluginId: string): Promise<LoadedP
* collapse to this shape — the only difference is which save function runs.
*/
type ConfigStep = {
key: string;
title: string;
subtitle: string;
schema: PluginOptionSchema;
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;
};
load: () => PluginOptionValues | undefined
save: (values: PluginOptionValues) => void
}
type Props = {
plugin: LoadedPlugin;
plugin: LoadedPlugin
/** `name@marketplace` — the savePluginOptions / saveMcpServerUserConfig key. */
pluginId: string;
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;
};
onDone: (outcome: 'configured' | 'skipped' | 'error', detail?: string) => void
}
export function PluginOptionsFlow({
plugin,
pluginId,
onDone
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[] = [];
const result: ConfigStep[] = []
// Top-level manifest.userConfig
const unconfigured = getUnconfiguredOptions(plugin);
const unconfigured = getUnconfiguredOptions(plugin)
if (Object.keys(unconfigured).length > 0) {
result.push({
key: 'top-level',
@@ -75,60 +90,83 @@ export function PluginOptionsFlow({
subtitle: 'Plugin options',
schema: unconfigured,
load: () => loadPluginOptions(pluginId),
save: values => savePluginOptions(pluginId, values, plugin.manifest.userConfig!)
});
save: values =>
savePluginOptions(pluginId, values, plugin.manifest.userConfig!),
})
}
// Per-channel userConfig (assistant-mode channels)
const channels: UnconfiguredChannel[] = getUnconfiguredChannels(plugin);
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)
});
load: () =>
loadMcpServerUserConfig(pluginId, channel.server) ?? undefined,
save: values =>
saveMcpServerUserConfig(
pluginId,
channel.server,
values,
channel.configSchema,
),
})
}
return result;
});
const [index, setIndex] = React.useState(0);
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;
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');
onDoneRef.current('skipped')
}
}, [steps.length]);
}, [steps.length])
if (steps.length === 0) {
return null;
return null
}
const current = steps[index]!;
function handleSave(values_1: PluginOptionValues): void {
const current = steps[index]!
function handleSave(values: PluginOptionValues): void {
try {
current.save(values_1);
current.save(values)
} catch (err) {
onDone('error', errorMessage(err));
return;
onDone('error', errorMessage(err))
return
}
const next = index + 1;
const next = index + 1
if (next < steps.length) {
setIndex(next);
setIndex(next)
} else {
onDone('configured');
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')} />;
return (
<PluginOptionsDialog
key={current.key}
title={current.title}
subtitle={current.subtitle}
configSchema={current.schema}
initialValues={current.load()}
onSave={handleSave}
onCancel={() => onDone('skipped')}
/>
)
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,31 +1,20 @@
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;
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(): React.ReactNode {
const customMessage = getPluginTrustMessage()
return (
<Box marginBottom={1}>
<Text color="claude">{figures.warning} </Text>
<Text dimColor italic>
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&apos;t change. See each plugin&apos;s homepage
for more information.{customMessage ? ` ${customMessage}` : ''}
</Text>
</Box>
)
}

View File

@@ -1,564 +1,151 @@
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';
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;
item: UnifiedInstalledItem
isSelected: boolean
}
export function UnifiedInstalledCell({
item,
isSelected,
}: Props): React.ReactNode {
const [theme] = useTheme()
if (item.type === 'plugin') {
// Status icon and text
let statusIcon: string
let statusText: string
// Show pending toggle status if set, otherwise show current status
if (item.pendingToggle) {
statusIcon = color('suggestion', theme)(figures.arrowRight)
statusText =
item.pendingToggle === 'will-enable' ? 'will enable' : 'will disable'
} else if (item.errorCount > 0) {
statusIcon = color('error', theme)(figures.cross)
statusText = `${item.errorCount} ${plural(item.errorCount, 'error')}`
} else if (!item.isEnabled) {
statusIcon = color('inactive', theme)(figures.radioOff)
statusText = 'disabled'
} else {
statusIcon = color('success', theme)(figures.tick)
statusText = 'enabled'
}
return (
<Box>
<Text color={isSelected ? 'suggestion' : undefined}>
{isSelected ? `${figures.pointer} ` : ' '}
</Text>
<Text color={isSelected ? 'suggestion' : undefined}>{item.name}</Text>
<Text dimColor={!isSelected}>
{' '}
<Text backgroundColor="userMessageBackground">Plugin</Text>
</Text>
<Text dimColor> · {item.marketplace}</Text>
<Text dimColor={!isSelected}> · {statusIcon} </Text>
<Text dimColor={!isSelected}>{statusText}</Text>
</Box>
)
}
if (item.type === 'flagged-plugin') {
const statusIcon = color('warning', theme)(figures.warning)
return (
<Box>
<Text color={isSelected ? 'suggestion' : undefined}>
{isSelected ? `${figures.pointer} ` : ' '}
</Text>
<Text color={isSelected ? 'suggestion' : undefined}>{item.name}</Text>
<Text dimColor={!isSelected}>
{' '}
<Text backgroundColor="userMessageBackground">Plugin</Text>
</Text>
<Text dimColor> · {item.marketplace}</Text>
<Text dimColor={!isSelected}> · {statusIcon} </Text>
<Text dimColor={!isSelected}>removed</Text>
</Box>
)
}
if (item.type === 'failed-plugin') {
const statusIcon = color('error', theme)(figures.cross)
const statusText = `failed to load · ${item.errorCount} ${plural(item.errorCount, 'error')}`
return (
<Box>
<Text color={isSelected ? 'suggestion' : undefined}>
{isSelected ? `${figures.pointer} ` : ' '}
</Text>
<Text color={isSelected ? 'suggestion' : undefined}>{item.name}</Text>
<Text dimColor={!isSelected}>
{' '}
<Text backgroundColor="userMessageBackground">Plugin</Text>
</Text>
<Text dimColor> · {item.marketplace}</Text>
<Text dimColor={!isSelected}> · {statusIcon} </Text>
<Text dimColor={!isSelected}>{statusText}</Text>
</Box>
)
}
// MCP server
let statusIcon: string
let statusText: string
if (item.status === 'connected') {
statusIcon = color('success', theme)(figures.tick)
statusText = 'connected'
} else if (item.status === 'disabled') {
statusIcon = color('inactive', theme)(figures.radioOff)
statusText = 'disabled'
} else if (item.status === 'pending') {
statusIcon = color('inactive', theme)(figures.radioOff)
statusText = 'connecting…'
} else if (item.status === 'needs-auth') {
statusIcon = color('warning', theme)(figures.triangleUpOutline)
statusText = 'Enter to auth'
} else {
statusIcon = color('error', theme)(figures.cross)
statusText = 'failed'
}
// Indented MCPs (child of a plugin)
if (item.indented) {
return (
<Box>
<Text color={isSelected ? 'suggestion' : undefined}>
{isSelected ? `${figures.pointer} ` : ' '}
</Text>
<Text dimColor={!isSelected}> </Text>
<Text color={isSelected ? 'suggestion' : undefined}>{item.name}</Text>
<Text dimColor={!isSelected}>
{' '}
<Text backgroundColor="userMessageBackground">MCP</Text>
</Text>
<Text dimColor={!isSelected}> · {statusIcon} </Text>
<Text dimColor={!isSelected}>{statusText}</Text>
</Box>
)
}
return (
<Box>
<Text color={isSelected ? 'suggestion' : undefined}>
{isSelected ? `${figures.pointer} ` : ' '}
</Text>
<Text color={isSelected ? 'suggestion' : undefined}>{item.name}</Text>
<Text dimColor={!isSelected}>
{' '}
<Text backgroundColor="userMessageBackground">MCP</Text>
</Text>
<Text dimColor={!isSelected}> · {statusIcon} </Text>
<Text dimColor={!isSelected}>{statusText}</Text>
</Box>
)
}

View File

@@ -1,97 +1,103 @@
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';
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;
onComplete: (result?: string) => void
path?: string
}
export function ValidatePlugin({ onComplete, path }: Props): React.ReactNode {
useEffect(() => {
async function runValidation() {
// If no path provided, show usage
if (!path) {
onComplete(
'Usage: /plugin validate <path>\n\n' +
'Validate a plugin or marketplace manifest file or directory.\n\n' +
'Examples:\n' +
' /plugin validate .claude-plugin/plugin.json\n' +
' /plugin validate /path/to/plugin-directory\n' +
' /plugin validate .\n\n' +
'When given a directory, automatically validates .claude-plugin/marketplace.json\n' +
'or .claude-plugin/plugin.json (prefers marketplace if both exist).\n\n' +
'Or from the command line:\n' +
' claude plugin validate <path>',
)
return
}
try {
const result = await validateManifest(path)
let output = ''
// Add header
output += `Validating ${result.fileType} manifest: ${result.filePath}\n\n`
// Show errors
if (result.errors.length > 0) {
output += `${figures.cross} Found ${result.errors.length} ${plural(result.errors.length, 'error')}:\n\n`
result.errors.forEach(error => {
output += ` ${figures.pointer} ${error.path}: ${error.message}\n`
})
output += '\n'
}
// Show warnings
if (result.warnings.length > 0) {
output += `${figures.warning} Found ${result.warnings.length} ${plural(result.warnings.length, 'warning')}:\n\n`
result.warnings.forEach(warning => {
output += ` ${figures.pointer} ${warning.path}: ${warning.message}\n`
})
output += '\n'
}
// Show success or failure
if (result.success) {
if (result.warnings.length > 0) {
output += `${figures.tick} Validation passed with warnings\n`
} else {
output += `${figures.tick} Validation passed\n`
}
// Exit with code 0 (success)
process.exitCode = 0
} else {
output += `${figures.cross} Validation failed\n`
// Exit with code 1 (validation failure)
process.exitCode = 1
}
onComplete(output)
} catch (error) {
// Exit with code 2 (unexpected error)
process.exitCode = 2
logError(error)
onComplete(
`${figures.cross} Unexpected error during validation: ${errorMessage(error)}`,
)
}
}
void runValidation()
}, [onComplete, path])
return (
<Box flexDirection="column">
<Text>Running validation...</Text>
</Box>
)
}

View File

@@ -1,10 +1,12 @@
import type { Command } from '../../commands.js';
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;
load: () => import('./plugin.js'),
} satisfies Command
export default plugin

View File

@@ -1,6 +1,11 @@
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} />;
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} />
}

View File

@@ -1,116 +1,123 @@
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';
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;
};
entry: PluginMarketplaceEntry
marketplaceName: string
pluginId: string
isInstalled: boolean
}
/**
* Menu option for plugin details view
*/
export type PluginDetailsMenuOption = {
label: string;
action: string;
};
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;
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;
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'
}];
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'
});
options.push({ label: 'Open homepage', action: 'homepage' })
}
if (githubRepo) {
options.push({
label: 'View on GitHub',
action: 'github'
});
options.push({ label: 'View on GitHub', action: 'github' })
}
options.push({
label: 'Back to plugin list',
action: 'back'
});
return options;
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;
export function PluginSelectionKeyHint({
hasSelection,
}: {
hasSelection: boolean
}): React.ReactNode {
return (
<Box marginTop={1}>
<Text dimColor italic>
<Byline>
{hasSelection && (
<ConfigurableShortcutHint
action="plugin:install"
context="Plugin"
fallback="i"
description="install"
bold
/>
)}
<ConfigurableShortcutHint
action="plugin:toggle"
context="Plugin"
fallback="Space"
description="toggle"
/>
<ConfigurableShortcutHint
action="select:accept"
context="Select"
fallback="Enter"
description="details"
/>
<ConfigurableShortcutHint
action="confirm:no"
context="Confirmation"
fallback="Esc"
description="back"
/>
</Byline>
</Text>
</Box>
)
}