UNPKG

@nanocollective/nanocoder

Version:

A local-first CLI coding agent that brings the power of agentic coding tools like Claude Code and Gemini CLI to local models or controlled APIs like OpenRouter

254 lines 13.7 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { spawnSync } from 'node:child_process'; import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, } from 'node:fs'; import { dirname, join } from 'node:path'; import { Box, Text, useFocus, useInput } from 'ink'; import SelectInput from 'ink-select-input'; import Spinner from 'ink-spinner'; import { useEffect, useState } from 'react'; import { TitledBoxWithPreferences } from '../components/ui/titled-box.js'; import { colors } from '../config/index.js'; import { getConfigPath } from '../config/paths.js'; import { useResponsiveTerminal } from '../hooks/useTerminalWidth.js'; import { logError, logInfo } from '../utils/message-queue.js'; import { LocationStep } from './steps/location-step.js'; import { ProviderStep } from './steps/provider-step.js'; import { buildProviderConfigObject } from './validation.js'; export function ProviderWizard({ projectDir, onComplete, onCancel, }) { const [step, setStep] = useState('location'); const [providerConfigPath, setProviderConfigPath] = useState(''); const [providers, setProviders] = useState([]); const [error, setError] = useState(null); const { boxWidth, isNarrow } = useResponsiveTerminal(); // Capture focus to ensure keyboard handling works properly useFocus({ autoFocus: true, id: 'config-wizard' }); // Load existing config if editing useEffect(() => { if (!providerConfigPath) { return; } // Use a microtask to defer state updates void Promise.resolve().then(() => { try { let loadedProviders = []; // Try to load providers from agents.config.json if (existsSync(providerConfigPath)) { try { const providerContent = readFileSync(providerConfigPath, 'utf-8'); const providerConfig = JSON.parse(providerContent); loadedProviders = providerConfig.nanocoder?.providers || []; } catch (err) { logError('Failed to load provider configuration', true, { context: { providerConfigPath }, error: err instanceof Error ? err.message : String(err), }); } } setProviders(loadedProviders); } catch (err) { logError('Failed to load existing configuration', true, { context: { providerConfigPath }, error: err instanceof Error ? err.message : String(err), }); } }); }, [providerConfigPath]); const handleLocationComplete = (location) => { // Determine the base directory based on location const baseDir = location === 'project' ? process.cwd() : getConfigPath(); setProviderConfigPath(join(baseDir, 'agents.config.json')); setStep('providers'); }; const handleProvidersComplete = (newProviders) => { setProviders(newProviders); setStep('summary'); }; const handleSave = () => { setStep('saving'); setError(null); try { // Build and save provider config if (providers.length > 0) { const providerConfig = buildProviderConfigObject(providers); const providerDir = dirname(providerConfigPath); if (!existsSync(providerDir)) { mkdirSync(providerDir, { recursive: true }); } writeFileSync(providerConfigPath, JSON.stringify(providerConfig, null, 2), 'utf-8'); } setStep('complete'); // Don't auto-complete - wait for user to press Enter } catch (err) { setError(err instanceof Error ? err.message : 'Failed to save configuration'); setStep('summary'); } }; const handleAddProviders = () => { setStep('providers'); }; const handleCancel = () => { if (onCancel) { onCancel(); } }; const handleDeleteConfig = () => { setStep('confirm-delete'); }; const handleConfirmDelete = () => { try { if (existsSync(providerConfigPath)) { unlinkSync(providerConfigPath); logInfo(`Deleted configuration file: ${providerConfigPath}`); } // Call onComplete to trigger reload onComplete(providerConfigPath); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to delete configuration'); setStep('providers'); } }; const openInEditor = () => { try { // Save current progress if (providers.length > 0) { const providerConfig = buildProviderConfigObject(providers); const providerDir = dirname(providerConfigPath); if (!existsSync(providerDir)) { mkdirSync(providerDir, { recursive: true }); } writeFileSync(providerConfigPath, JSON.stringify(providerConfig, null, 2), 'utf-8'); } // Detect editor (respect $EDITOR or $VISUAL environment variables) // Fall back to nano on Unix/Mac (much friendlier than vi!) // On Windows, use notepad const editor = process.env.EDITOR || process.env.VISUAL || (process.platform === 'win32' ? 'notepad' : 'nano'); // Show cursor and restore terminal for editor process.stdout.write('\x1B[?25h'); // Show cursor process.stdin.setRawMode?.(false); // Disable raw mode // Open provider config in editor const result = spawnSync(editor, [providerConfigPath], { stdio: 'inherit', // Give editor full control of terminal }); // Restore terminal state after editor closes process.stdin.setRawMode?.(true); // Re-enable raw mode process.stdout.write('\x1B[?25l'); // Hide cursor (Ink will manage it) if (result.status === 0) { // Reload config to get updated values let loadedProviders = []; // Reload provider config if (existsSync(providerConfigPath)) { try { const editedContent = readFileSync(providerConfigPath, 'utf-8'); const editedConfig = JSON.parse(editedContent); loadedProviders = editedConfig.nanocoder?.providers || []; } catch (parseErr) { setError(parseErr instanceof Error ? `Invalid JSON: ${parseErr.message}` : 'Failed to parse edited configuration'); setStep('summary'); return; } } setProviders(loadedProviders); // Return to summary to review changes setStep('summary'); setError(null); } else { setError('Editor exited with an error. Changes may not be saved.'); setStep('summary'); } } catch (err) { // Restore terminal state on error process.stdin.setRawMode?.(true); process.stdout.write('\x1B[?25l'); setError(err instanceof Error ? `Failed to open editor: ${err.message}` : 'Failed to open editor'); setStep('summary'); } }; // Handle global keyboard shortcuts useInput((input, key) => { // In complete step, wait for Enter to finish if (step === 'complete' && key.return) { onComplete(providerConfigPath); return; } // Escape - cancel/exit wizard completely if (key.escape) { if (onCancel) { onCancel(); } return; } // Ctrl+E to open editor (available after location is chosen) if (key.ctrl && input === 'e' && providerConfigPath && (step === 'providers' || step === 'summary')) { openInEditor(); } }); const renderStep = () => { switch (step) { case 'location': { return (_jsx(LocationStep, { projectDir: projectDir, onComplete: handleLocationComplete, onBack: onCancel })); } case 'providers': { return (_jsx(ProviderStep, { existingProviders: providers, onComplete: handleProvidersComplete, onBack: () => setStep('location'), onDelete: handleDeleteConfig, configExists: existsSync(providerConfigPath) })); } case 'summary': { return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: colors.primary, children: "Configuration Summary" }) }), _jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.secondary, children: "Config file:" }), _jsx(Text, { color: colors.success, children: providerConfigPath })] }), providers.length > 0 ? (_jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [_jsxs(Text, { color: colors.secondary, children: ["Providers (", providers.length, "):"] }), providers.map((provider, index) => (_jsxs(Text, { color: colors.success, children: ["\u2022 ", provider.name] }, index)))] })) : (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: colors.warning, children: "No providers configured" }) })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.secondary, children: "\u2022 Enter: Save configuration" }), _jsx(Text, { color: colors.secondary, children: "\u2022 Shift+Tab: Go back" }), _jsx(Text, { color: colors.secondary, children: "\u2022 Esc: Cancel" })] })] })); } case 'confirm-delete': { const deleteOptions = [ { label: 'Yes, delete the file', value: 'yes' }, { label: 'No, go back', value: 'no' }, ]; return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: colors.error, children: "Delete Configuration?" }) }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { children: ["Are you sure you want to delete", ' ', _jsx(Text, { color: colors.warning, children: providerConfigPath }), "?"] }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: "This action cannot be undone." }) }), _jsx(SelectInput, { items: deleteOptions, onSelect: (item) => { if (item.value === 'yes') { handleConfirmDelete(); } else { setStep('providers'); } } })] })); } case 'saving': { return (_jsx(Box, { flexDirection: "column", children: _jsx(Box, { children: _jsxs(Text, { color: colors.success, children: [_jsx(Spinner, { type: "dots" }), " Saving configuration..."] }) }) })); } case 'complete': { return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: colors.success, bold: true, children: "\u2713 Configuration saved!" }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: "Saved to:" }) }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { dimColor: true, children: [" ", providerConfigPath] }) }), _jsx(Box, { children: _jsx(Text, { color: colors.secondary, children: "Press Enter to continue" }) })] })); } default: { return null; } } }; return (_jsxs(TitledBoxWithPreferences, { title: "Provider Wizard", width: boxWidth, borderColor: colors.primary, paddingX: 2, paddingY: 1, flexDirection: "column", marginBottom: 1, children: [error && (_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: colors.error, children: ["Error: ", error] }) })), renderStep(), (step === 'location' || step === 'providers' || step === 'summary') && (isNarrow ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.secondary, children: "Esc: Exit wizard" }), _jsx(Text, { color: colors.secondary, children: "Shift+Tab: Go back" }), providerConfigPath && (_jsx(Text, { color: colors.secondary, children: "Ctrl+E: Edit manually" }))] })) : (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.secondary, children: ["Esc: Exit wizard | Shift+Tab: Go back", providerConfigPath && ' | Ctrl+E: Edit manually'] }) }))), step === 'summary' && (_jsx(SummaryStepActions, { onSave: handleSave, onAddProviders: handleAddProviders, onCancel: handleCancel }))] })); } function SummaryStepActions({ onSave, onAddProviders, onCancel, }) { useInput((_input, key) => { if (key.shift && key.tab) { onAddProviders(); } else if (key.return) { onSave(); } else if (key.escape) { onCancel(); } }); return null; } //# sourceMappingURL=provider-wizard.js.map