@basictech/cli
Version:
Basic CLI for creating & managing your projects
358 lines • 18.3 kB
JavaScript
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