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, KeyboardShortcutHint } from '@anthropic/ink' import { Spinner } from '../../components/Spinner.js' import TextInput from '../../components/TextInput.js' import { Box, Text } from '@anthropic/ink' 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 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('') 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 }, []) // Only run once on mount return ( Add Marketplace Enter marketplace source: Examples: · owner/repo (GitHub) · git@github.com:owner/repo.git (SSH) · https://example.com/marketplace.json · ./path/to/marketplace {isLoading && ( {progressMessage || 'Adding marketplace to configuration…'} )} {error && ( {error} )} {result && ( {result} )} ) }