UNPKG

@basictech/cli

Version:

Basic CLI for creating & managing your projects

358 lines 18.3 kB
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; import { useState, useEffect } from 'react'; import { Box, Text, useInput } from 'ink'; import { ApiClient } from '../lib/api.js'; import { generateSlug } from '../lib/platform.js'; import { CONFIG_TEMPLATES } from '../lib/config-templates.js'; import { Spinner } from './Spinner.js'; import { TeamForm } from './TeamForm.js'; export function InitForm({ onSuccess, onCancel, initialData }) { const [state, setState] = useState({ step: initialData?.source ? (initialData.source === 'new' ? 'project-details' : 'existing-selection') : 'source', source: initialData?.source || null, projectName: initialData?.projectName || '', projectSlug: initialData?.projectName ? generateSlug(initialData.projectName) : '', selectedTeamId: null, selectedProjectId: initialData?.projectId || null, configTemplate: initialData?.configTemplate || null, availableTeams: [], availableProjects: [], isLoading: false, error: null }); const [selectedOptionIndex, setSelectedOptionIndex] = useState(0); const [showTeamForm, setShowTeamForm] = useState(false); // Load initial data useEffect(() => { async function loadData() { setState(prev => ({ ...prev, isLoading: true, error: null })); try { const apiClient = ApiClient.getInstance(); const [teams, projects] = await Promise.all([ apiClient.getTeams(), apiClient.getProjects() ]); setState(prev => ({ ...prev, availableTeams: teams, availableProjects: projects, isLoading: false, selectedTeamId: teams.length > 0 ? teams[0].id : null })); } catch (error) { setState(prev => ({ ...prev, isLoading: false, error: error instanceof Error ? error.message : 'Failed to load data' })); } } loadData(); }, []); // Auto-generate slug when project name changes useEffect(() => { if (state.projectName.trim()) { const newSlug = generateSlug(state.projectName); setState(prev => ({ ...prev, projectSlug: newSlug })); } }, [state.projectName]); useInput((input, key) => { if (showTeamForm) { return; // Let TeamForm handle input } if (key.escape) { if (state.step === 'source') { onCancel(); } else { // Go back to previous step goToPreviousStep(); } return; } if (state.step === 'project-details') { handleProjectDetailsInput(input, key); } else if (state.step === 'source' || state.step === 'team-selection' || state.step === 'existing-selection' || state.step === 'config-template' || state.step === 'confirmation') { handleSelectionInput(input, key); } }); const handleProjectDetailsInput = (input, key) => { if (key.return) { if (state.projectName.trim()) { setState(prev => ({ ...prev, step: 'team-selection' })); setSelectedOptionIndex(0); } return; } if (key.backspace || key.delete) { setState(prev => ({ ...prev, projectName: prev.projectName.slice(0, -1) })); return; } if (input && input.length === 1) { setState(prev => ({ ...prev, projectName: prev.projectName + input })); } }; const handleSelectionInput = (input, key) => { const options = getOptionsForCurrentStep(); if (key.upArrow) { setSelectedOptionIndex(prev => prev > 0 ? prev - 1 : options.length - 1); return; } if (key.downArrow) { setSelectedOptionIndex(prev => prev < options.length - 1 ? prev + 1 : 0); return; } if (key.return) { handleOptionSelection(); } }; const getOptionsForCurrentStep = () => { switch (state.step) { case 'source': return [ { label: 'Create new project', value: 'new' }, { label: 'Import existing project', value: 'existing' } ]; case 'team-selection': const teamOptions = state.availableTeams.map(team => ({ label: `${team.name} (${team.slug})`, value: team.id })); teamOptions.push({ label: 'Create new team...', value: 'new' }); return teamOptions; case 'existing-selection': return state.availableProjects.map(project => ({ label: `${project.name} (${project.team_name || 'Unknown team'})`, value: project.id })); case 'config-template': return Object.entries(CONFIG_TEMPLATES).map(([key, template]) => ({ label: `${template.name} - ${template.description}`, value: key })); case 'confirmation': return [ { label: state.source === 'new' ? 'Yes, create project' : 'Yes, import project', value: 'confirm' }, { label: 'No, go back', value: 'back' } ]; default: return []; } }; const handleOptionSelection = () => { const options = getOptionsForCurrentStep(); const selectedOption = options[selectedOptionIndex]; switch (state.step) { case 'source': setState(prev => ({ ...prev, source: selectedOption.value, step: selectedOption.value === 'new' ? 'project-details' : 'existing-selection' })); setSelectedOptionIndex(0); break; case 'team-selection': if (selectedOption.value === 'new') { setShowTeamForm(true); } else { setState(prev => ({ ...prev, selectedTeamId: selectedOption.value, step: 'config-template' })); setSelectedOptionIndex(0); } break; case 'existing-selection': setState(prev => ({ ...prev, selectedProjectId: selectedOption.value, step: 'config-template' })); setSelectedOptionIndex(0); break; case 'config-template': setState(prev => ({ ...prev, configTemplate: selectedOption.value, step: 'confirmation' })); setSelectedOptionIndex(0); break; case 'confirmation': if (selectedOption.value === 'confirm') { handleSubmit(); } else { goToPreviousStep(); } break; } }; const goToPreviousStep = () => { switch (state.step) { case 'project-details': setState(prev => ({ ...prev, step: 'source' })); break; case 'team-selection': setState(prev => ({ ...prev, step: 'project-details' })); break; case 'existing-selection': setState(prev => ({ ...prev, step: 'source' })); break; case 'config-template': setState(prev => ({ ...prev, step: state.source === 'new' ? 'team-selection' : 'existing-selection' })); break; case 'confirmation': setState(prev => ({ ...prev, step: 'config-template' })); break; } setSelectedOptionIndex(0); }; const handleTeamCreated = async (teamData) => { setState(prev => ({ ...prev, isLoading: true })); try { const apiClient = ApiClient.getInstance(); const newTeam = await apiClient.createTeam(teamData.teamName, teamData.teamSlug); setState(prev => ({ ...prev, availableTeams: [...prev.availableTeams, newTeam], selectedTeamId: newTeam.id, step: 'config-template', isLoading: false })); setShowTeamForm(false); setSelectedOptionIndex(0); } catch (error) { setState(prev => ({ ...prev, isLoading: false, error: error instanceof Error ? error.message : 'Failed to create team' })); } }; const handleSubmit = async () => { setState(prev => ({ ...prev, isLoading: true, error: null })); try { const apiClient = ApiClient.getInstance(); let projectId; let projectName; if (state.source === 'new') { if (!state.selectedTeamId || !state.configTemplate) { throw new Error('Missing required data for project creation'); } const project = await apiClient.createProjectWithTeam(state.projectName, state.projectSlug, state.selectedTeamId); projectId = project.id; projectName = project.name; } else { if (!state.selectedProjectId) { throw new Error('No project selected'); } const project = await apiClient.getProject(state.selectedProjectId); projectId = project.id; projectName = project.name; } // Create config file let configPath = null; if (state.configTemplate && state.configTemplate !== 'none') { const { createConfigFile } = await import('../lib/config-templates'); configPath = await createConfigFile(state.configTemplate, projectId, projectName); // After creating the config file, try to pull the latest schema if it exists try { const remoteSchema = await apiClient.getProjectSchema(projectId); // Check if remote schema exists and is different from the default if (remoteSchema && remoteSchema.version > 0) { // Update the config file with the remote schema const { saveSchemaToConfig } = await import('../lib/schema'); await saveSchemaToConfig(remoteSchema); } } catch (error) { // If fetching/updating schema fails, we don't want to fail the entire init // Just log the error and continue - the config file was still created successfully console.warn('Failed to pull latest schema during init:', error); } } onSuccess({ projectId, projectName, configPath }); } catch (error) { setState(prev => ({ ...prev, isLoading: false, error: error instanceof Error ? error.message : 'Failed to create project' })); } }; if (showTeamForm) { return (_jsx(TeamForm, { title: "Create New Team", onSubmit: handleTeamCreated, onCancel: () => setShowTeamForm(false) })); } if (state.isLoading) { return _jsx(Spinner, { text: "Loading..." }); } if (state.error) { return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "red", children: ["Error: ", state.error] }), _jsx(Text, { color: "gray", children: "Press Esc to go back" })] })); } return (_jsx(Box, { flexDirection: "column", padding: 1, children: renderCurrentStep() })); function renderCurrentStep() { const stepNumber = getStepNumber(); const totalSteps = getTotalSteps(); switch (state.step) { case 'source': return (_jsxs(_Fragment, { children: [_jsxs(Text, { bold: true, color: "blue", children: ["Project Setup (", stepNumber, "/", totalSteps, ")"] }), _jsx(Box, { marginTop: 1, marginBottom: 2, children: _jsx(Text, { children: "How would you like to proceed?" }) }), renderOptions(), _jsx(Box, { marginTop: 2, children: _jsx(Text, { color: "gray", children: "\u2191/\u2193 select \u2022 enter to continue \u2022 esc to cancel" }) })] })); case 'project-details': return (_jsxs(_Fragment, { children: [_jsxs(Text, { bold: true, color: "blue", children: ["Create New Project (", stepNumber, "/", totalSteps, ")"] }), _jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsxs(Text, { color: "blue", children: ['>', " Project Name:"] }) }), _jsx(Box, { marginLeft: 2, marginBottom: 1, children: _jsxs(Text, { children: [state.projectName, _jsx(Text, { backgroundColor: "white", color: "black", children: "\u2588" })] }) }), state.projectSlug && (_jsxs(Box, { marginBottom: 1, children: [_jsx(Box, { children: _jsx(Text, { color: "gray", children: "\u2713 Project Slug (auto-generated):" }) }), _jsx(Box, { marginLeft: 2, children: _jsx(Text, { children: state.projectSlug }) })] })), _jsx(Box, { marginTop: 2, children: _jsx(Text, { color: "gray", children: state.projectName.trim() ? 'Enter to continue • esc to go back' : 'Type project name • esc to go back' }) })] })); case 'team-selection': return (_jsxs(_Fragment, { children: [_jsxs(Text, { bold: true, color: "blue", children: ["Create New Project (", stepNumber, "/", totalSteps, ")"] }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: "gray", children: ["\u2713 Project Name: ", state.projectName] }), _jsxs(Text, { color: "gray", children: ["\u2713 Project Slug: ", state.projectSlug] })] }), _jsx(Box, { marginTop: 1, marginBottom: 2, children: _jsx(Text, { children: "Select Team:" }) }), renderOptions(), _jsx(Box, { marginTop: 2, children: _jsx(Text, { color: "gray", children: "\u2191/\u2193 select \u2022 enter to continue \u2022 esc to go back" }) })] })); case 'existing-selection': return (_jsxs(_Fragment, { children: [_jsxs(Text, { bold: true, color: "blue", children: ["Import Existing Project (", stepNumber, "/", totalSteps, ")"] }), _jsx(Box, { marginTop: 1, marginBottom: 2, children: _jsx(Text, { children: "Select Project:" }) }), renderOptions(), _jsx(Box, { marginTop: 2, children: _jsx(Text, { color: "gray", children: "\u2191/\u2193 select \u2022 enter to continue \u2022 esc to go back" }) })] })); case 'config-template': return (_jsxs(_Fragment, { children: [_jsxs(Text, { bold: true, color: "blue", children: ["Configuration Setup (", stepNumber, "/", totalSteps, ")"] }), _jsx(Box, { marginTop: 1, marginBottom: 2, children: _jsx(Text, { children: "Choose config template:" }) }), renderOptions(), _jsx(Box, { marginTop: 2, children: _jsx(Text, { color: "gray", children: "\u2191/\u2193 select \u2022 enter to continue \u2022 esc to go back" }) })] })); case 'confirmation': return (_jsxs(_Fragment, { children: [_jsxs(Text, { bold: true, color: "blue", children: ["Ready to ", state.source === 'new' ? 'Create' : 'Import', " (", stepNumber, "/", totalSteps, ")"] }), _jsxs(Box, { marginTop: 1, marginBottom: 2, flexDirection: "column", children: [state.source === 'new' ? (_jsxs(_Fragment, { children: [_jsxs(Text, { children: ["\u2713 Project: ", state.projectName] }), _jsxs(Text, { children: ["\u2713 Team: ", getSelectedTeamName()] })] })) : (_jsxs(Text, { children: ["\u2713 Project: ", getSelectedProjectName()] })), _jsxs(Text, { children: ["\u2713 Config: ", getSelectedTemplateName()] }), state.configTemplate !== 'none' && (_jsxs(Text, { children: ["\u2713 Location: ./", CONFIG_TEMPLATES[state.configTemplate].filename] }))] }), renderOptions(), _jsx(Box, { marginTop: 2, children: _jsx(Text, { color: "gray", children: "\u2191/\u2193 select \u2022 enter to confirm \u2022 esc to go back" }) })] })); default: return _jsx(Text, { children: "Unknown step" }); } } function renderOptions() { const options = getOptionsForCurrentStep(); return (_jsx(Box, { flexDirection: "column", children: options.map((option, index) => (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: index === selectedOptionIndex ? 'blue' : 'white', children: [index === selectedOptionIndex ? '●' : '○', " ", option.label] }) }, option.value))) })); } function getStepNumber() { const stepOrder = ['source', 'project-details', 'team-selection', 'existing-selection', 'config-template', 'confirmation']; return stepOrder.indexOf(state.step) + 1; } function getTotalSteps() { return state.source === 'new' ? 5 : 4; } function getSelectedTeamName() { const team = state.availableTeams.find(t => t.id === state.selectedTeamId); return team ? team.name : 'Unknown'; } function getSelectedProjectName() { const project = state.availableProjects.find(p => p.id === state.selectedProjectId); return project ? project.name : 'Unknown'; } function getSelectedTemplateName() { return state.configTemplate ? CONFIG_TEMPLATES[state.configTemplate].name : 'None'; } } //# sourceMappingURL=InitForm.js.map