UNPKG

@basictech/cli

Version:

Basic CLI for creating & managing your projects

403 lines 20.6 kB
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; import React from 'react'; import { render, Box, Text, useInput } from 'ink'; import { Spinner } from '../components/Spinner.js'; import { ApiClient } from '../lib/api.js'; import { AuthService } from '../lib/auth.js'; import { readSchemaFromConfig, compareVersions } from '../lib/schema.js'; import { isOnline } from '../lib/platform.js'; import { MESSAGES } from '../lib/constants.js'; function PushApp() { const [state, setState] = React.useState({ phase: 'checking', error: null }); const [selectedOption, setSelectedOption] = React.useState('yes'); React.useEffect(() => { async function checkPushStatus() { try { // Check if online if (!(await isOnline())) { setState({ phase: 'error', error: MESSAGES.OFFLINE, }); return; } // Check authentication const authService = AuthService.getInstance(); const token = await authService.getToken(); if (!token) { setState({ phase: 'error', error: MESSAGES.LOGGED_OUT, }); return; } // Read local schema const localConfig = await readSchemaFromConfig(); if (!localConfig) { setState({ phase: 'no-action', error: null, statusResult: { status: 'no-schema', projectId: '', localVersion: 0, remoteVersion: 0, message: [ 'No schema found in config files', 'Run \'basic init\' to create a new project or import an existing project' ], needsConfirmation: false, confirmationTitle: '', confirmationMessage: '' } }); return; } // Get remote schema const apiClient = ApiClient.getInstance(); let remoteSchema = null; try { remoteSchema = await apiClient.getProjectSchema(localConfig.projectId); } catch (error) { setState({ phase: 'error', error: `Error fetching remote schema: ${error instanceof Error ? error.message : 'Unknown error'}`, }); return; } // Create empty schema if none exists remotely if (!remoteSchema) { remoteSchema = { project_id: localConfig.projectId, version: 0, tables: {} }; } // Compare versions and determine action const comparison = compareVersions(localConfig.schema, remoteSchema); const result = await analyzePushAction(localConfig, remoteSchema, comparison, apiClient); setState({ phase: result.needsConfirmation ? 'confirming' : 'no-action', error: null, statusResult: result }); } catch (error) { setState({ phase: 'error', error: error instanceof Error ? error.message : 'Failed to check push status' }); } } checkPushStatus(); }, []); // Handle confirmation input (only for interactive states) useInput((input, key) => { if (state.phase === 'confirming') { if (key.upArrow || key.downArrow) { setSelectedOption(prev => prev === 'yes' ? 'no' : 'yes'); } else if (key.return) { if (selectedOption === 'yes') { handlePush(); } else { setState(prev => ({ ...prev, phase: 'no-action' })); } } else if (key.escape || input === 'q') { setState(prev => ({ ...prev, phase: 'no-action' })); } } }); const handlePush = async () => { setState(prev => ({ ...prev, phase: 'pushing' })); try { if (!state.statusResult) { throw new Error('No status result available'); } // Re-fetch the latest data for pushing const localConfig = await readSchemaFromConfig(); if (!localConfig) { throw new Error('Local schema not found'); } const apiClient = ApiClient.getInstance(); // Push the schema await apiClient.pushProjectSchema(localConfig.projectId, localConfig.schema); setState({ phase: 'success', error: null, pushResult: { projectId: localConfig.projectId, oldVersion: state.statusResult.remoteVersion, newVersion: localConfig.schema.version || 0, filePath: localConfig.filePath } }); } catch (error) { setState({ phase: 'error', error: error instanceof Error ? error.message : 'Failed to push schema' }); } }; if (state.phase === 'checking') { return _jsx(Spinner, { text: "Checking push status..." }); } if (state.phase === 'pushing') { return _jsx(Spinner, { text: "Pushing schema to remote..." }); } if (state.phase === 'error') { // Exit immediately with error code setTimeout(() => process.exit(1), 0); return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "red", children: ["Error: ", state.error] }), _jsxs(Box, { flexDirection: "column", marginTop: 1, marginBottom: 1, children: [_jsx(Text, { color: "blue", children: "Next steps:" }), state.error?.includes('offline') || state.error?.includes('network') ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: "\u2022 Check your internet connection" }), _jsx(Text, { color: "gray", children: "\u2022 Try again in a moment" })] })) : state.error?.includes('logged') || state.error?.includes('auth') ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: "\u2022 Run 'basic login' to authenticate" }), _jsx(Text, { color: "gray", children: "\u2022 Ensure you have a valid account" })] })) : state.error?.includes('schema') || state.error?.includes('project') ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: "\u2022 Check if the project ID is correct" }), _jsx(Text, { color: "gray", children: "\u2022 Ensure you have access to this project" }), _jsx(Text, { color: "gray", children: "\u2022 Run 'basic status' for more details" })] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: "\u2022 Try running the command again" }), _jsx(Text, { color: "gray", children: "\u2022 Run 'basic status' to check your project state" }), _jsx(Text, { color: "gray", children: "\u2022 Check the Basic documentation if the issue persists" })] }))] })] })); } if (state.phase === 'success' && state.pushResult) { // Exit immediately on success setTimeout(() => process.exit(0), 0); return _jsx(PushSuccessDisplay, { result: state.pushResult }); } if (state.phase === 'confirming' && state.statusResult) { return (_jsx(PushConfirmationDialog, { statusResult: state.statusResult, selectedOption: selectedOption })); } if (state.phase === 'no-action' && state.statusResult) { // Exit immediately for no-action states setTimeout(() => process.exit(0), 0); return _jsx(PushStatusDisplay, { result: state.statusResult }); } return _jsx(Text, { children: "Unknown state" }); } async function analyzePushAction(localConfig, remoteSchema, comparison, apiClient) { const { projectId } = localConfig; const baseResult = { projectId, localVersion: comparison.localVersion, remoteVersion: comparison.remoteVersion, message: [], needsConfirmation: false, confirmationTitle: '', confirmationMessage: '' }; switch (comparison.status) { case 'ahead': // Validate the local schema since it's ahead try { const validation = await apiClient.validateSchema(localConfig.schema); if (validation.valid === false && validation.errors) { return { ...baseResult, status: 'invalid', message: [ 'Errors found in schema! Please fix:', 'Your local schema has validation errors that must be resolved before pushing.' ], validationErrors: validation.errors }; } return { ...baseResult, status: 'ahead', message: [ 'Your local schema is ahead of the remote version.', 'Push your changes to publish them?' ], needsConfirmation: true, confirmationTitle: 'Push Schema Changes', confirmationMessage: 'This will publish your local schema changes to the remote project.' }; } catch (error) { return { ...baseResult, status: 'invalid', message: [ `Error validating schema: ${error instanceof Error ? error.message : 'Unknown error'}`, 'Please check your schema for syntax errors.' ] }; } case 'equal': // Same version - check for version 0 case or true equality if (comparison.localVersion === 0 && comparison.remoteVersion === 0) { // Both at version 0 - validate and suggest incrementing try { const validation = await apiClient.validateSchema(localConfig.schema); if (validation.valid === false && validation.errors) { return { ...baseResult, status: 'invalid', message: [ 'Errors found in schema! Please fix:' ], validationErrors: validation.errors }; } // Schema is valid but needs version increment return { ...baseResult, status: 'invalid', message: [ 'Schema changes are valid!', 'Please increment your version number to 1', 'and run \'basic push\' if you are ready to publish your changes.' ] }; } catch (error) { return { ...baseResult, status: 'invalid', message: [ `Error validating schema: ${error instanceof Error ? error.message : 'Unknown error'}` ] }; } } // Same non-zero version - check for content differences try { const comparisonResult = await apiClient.compareSchema(localConfig.schema); if (comparisonResult.valid) { // Schemas match - no push needed return { ...baseResult, status: 'current', message: [ 'Schema is up to date!', 'No push needed.' ] }; } else { // Same version but different content - suggest incrementing version return { ...baseResult, status: 'invalid', message: [ 'Your local schema differs from the remote schema.', 'Please increment your version number before pushing changes.' ] }; } } catch (error) { return { ...baseResult, status: 'current', message: [ 'Schema appears to be up to date.', '(Unable to verify schema content - assuming current)' ] }; } case 'behind': return { ...baseResult, status: 'behind', message: [ 'Your local schema is behind the remote version.', 'Did you mean to pull instead?', 'Use \'basic pull\' to get the latest changes.' ] }; default: return { ...baseResult, status: 'current', message: [ 'Schema is up to date!', 'No push needed.' ] }; } } function PushConfirmationDialog({ statusResult, selectedOption }) { const getStatusIcon = () => { switch (statusResult.status) { case 'ahead': return '⬆️'; default: return '📤'; } }; const getStatusText = () => { switch (statusResult.status) { case 'ahead': return 'Ready to push changes'; default: return 'Schema update ready'; } }; const getStatusColor = () => { switch (statusResult.status) { case 'ahead': return 'green'; default: return 'blue'; } }; return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { color: "cyan", children: ["Project ID: ", statusResult.projectId] }), _jsxs(Box, { children: [_jsxs(Text, { color: "gray", children: ["Local version: ", statusResult.localVersion] }), statusResult.remoteVersion > 0 && (_jsxs(Text, { color: "gray", children: [" \u2022 Remote version: ", statusResult.remoteVersion] }))] })] }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: getStatusColor(), children: [getStatusIcon(), " ", getStatusText()] }) }), _jsx(Box, { flexDirection: "column", marginBottom: 2, children: statusResult.message.map((line, index) => (_jsx(Text, { children: line }, index))) }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { children: _jsxs(Text, { color: selectedOption === 'yes' ? 'green' : 'gray', children: [selectedOption === 'yes' ? '❯' : ' ', " Yes, push changes"] }) }), _jsx(Box, { children: _jsxs(Text, { color: selectedOption === 'no' ? 'green' : 'gray', children: [selectedOption === 'no' ? '❯' : ' ', " No, cancel"] }) })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: "Use \u2191\u2193 to navigate, Enter to confirm, Esc to cancel" }) })] })); } function PushStatusDisplay({ result }) { const getStatusColor = () => { switch (result.status) { case 'current': return 'green'; case 'behind': return 'yellow'; case 'invalid': return 'red'; case 'no-schema': return 'gray'; default: return 'white'; } }; const getStatusIcon = () => { switch (result.status) { case 'current': return '✅'; case 'behind': return '⬇️'; case 'invalid': return '❌'; case 'no-schema': return '📄'; default: return '❓'; } }; const getStatusDescription = () => { switch (result.status) { case 'current': return 'Schema is up to date'; case 'behind': return 'Local schema is behind remote'; case 'invalid': return 'Schema has validation errors'; case 'no-schema': return 'No schema file found'; default: return 'Unknown status'; } }; const getNextSteps = () => { switch (result.status) { case 'current': return [ 'Continue working on your project', 'Run \'basic status\' to check for changes', 'Make schema modifications if needed' ]; case 'behind': return [ 'Run \'basic pull\' to get the latest changes', 'Or run \'basic status\' for more details' ]; case 'invalid': return [ 'Fix the validation errors shown below', 'Run \'basic status\' again after fixing errors', 'Review your schema syntax and field definitions' ]; case 'no-schema': return [ 'Run \'basic init\' to create a new project or import an existing project', 'Make sure you\'re in a directory with a basic.config.ts/js file' ]; default: return []; } }; return (_jsxs(Box, { flexDirection: "column", children: [result.projectId && (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { color: "cyan", children: ["Project ID: ", result.projectId] }), _jsxs(Box, { children: [_jsxs(Text, { color: "gray", children: ["Local version: ", result.localVersion] }), result.remoteVersion > 0 && (_jsxs(Text, { color: "gray", children: [" \u2022 Remote version: ", result.remoteVersion] }))] })] })), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: getStatusColor(), children: [getStatusIcon(), " ", getStatusDescription()] }) }), _jsx(Box, { flexDirection: "column", marginBottom: 1, children: result.message.map((line, index) => (_jsx(Text, { children: line }, index))) }), result.validationErrors && result.validationErrors.length > 0 && (_jsx(Box, { flexDirection: "column", marginBottom: 1, children: result.validationErrors.map((error, index) => (_jsxs(Text, { color: "red", children: ["\u2022 ", error.message, " at ", error.instancePath || 'root'] }, index))) })), getNextSteps().length > 0 && (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { color: "blue", children: "Next steps:" }), getNextSteps().map((step, index) => (_jsxs(Text, { color: "gray", children: ["\u2022 ", step] }, index)))] }))] })); } function PushSuccessDisplay({ result }) { return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "green", children: "\u2705 Schema pushed successfully!" }) }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { children: ["Source: ", result.filePath.split('/').pop()] }), _jsxs(Text, { children: ["Version: ", result.oldVersion, " \u2192 ", result.newVersion] }), _jsxs(Text, { children: ["Project: ", result.projectId] })] }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "blue", children: "Next steps:" }), _jsx(Text, { color: "gray", children: "\u2022 Your schema changes are now live" }), _jsx(Text, { color: "gray", children: "\u2022 Continue working on your project" }), _jsx(Text, { color: "gray", children: "\u2022 Run 'basic status' to check your project state" })] })] })); } export async function PushCommand() { render(_jsx(PushApp, {})); } //# sourceMappingURL=push.js.map