UNPKG

@basictech/cli

Version:

Basic CLI for creating & managing your projects

366 lines 18.7 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, saveSchemaToConfig } from '../lib/schema.js'; import { isOnline } from '../lib/platform.js'; import { MESSAGES } from '../lib/constants.js'; function PullApp() { const [state, setState] = React.useState({ phase: 'checking', error: null }); const [selectedOption, setSelectedOption] = React.useState('yes'); React.useEffect(() => { async function checkPullStatus() { 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 analyzePullAction(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 pull status' }); } } checkPullStatus(); }, []); // 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') { handlePull(); } else { setState(prev => ({ ...prev, phase: 'no-action' })); } } else if (key.escape || input === 'q') { setState(prev => ({ ...prev, phase: 'no-action' })); } } }); const handlePull = async () => { setState(prev => ({ ...prev, phase: 'pulling' })); try { if (!state.statusResult) { throw new Error('No status result available'); } // Re-fetch the latest data for pulling const localConfig = await readSchemaFromConfig(); if (!localConfig) { throw new Error('Local schema not found'); } const apiClient = ApiClient.getInstance(); const remoteSchema = await apiClient.getProjectSchema(localConfig.projectId); if (!remoteSchema) { throw new Error('Remote schema not found'); } // Save the remote schema to local config const filePath = await saveSchemaToConfig(remoteSchema); setState({ phase: 'success', error: null, pullResult: { projectId: localConfig.projectId, oldVersion: localConfig.schema.version || 0, newVersion: remoteSchema.version || 0, filePath } }); } catch (error) { setState({ phase: 'error', error: error instanceof Error ? error.message : 'Failed to pull schema' }); } }; if (state.phase === 'checking') { return _jsx(Spinner, { text: "Checking pull status..." }); } if (state.phase === 'pulling') { return _jsx(Spinner, { text: "Pulling latest schema..." }); } 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.pullResult) { // Exit immediately on success setTimeout(() => process.exit(0), 0); return _jsx(PullSuccessDisplay, { result: state.pullResult }); } if (state.phase === 'confirming' && state.statusResult) { return (_jsx(PullConfirmationDialog, { 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(PullStatusDisplay, { result: state.statusResult }); } return _jsx(Text, { children: "Unknown state" }); } async function analyzePullAction(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 'behind': return { ...baseResult, status: 'behind', message: [ 'Your local schema is behind the remote version.', 'Pull the latest changes?' ], needsConfirmation: true, confirmationTitle: 'Pull Remote Schema', confirmationMessage: 'This will update your local schema to the latest version.' }; case 'equal': // For version 0, assume it's current (no conflicts possible for first version) if (comparison.localVersion === 0 && comparison.remoteVersion === 0) { return { ...baseResult, status: 'current', message: [ 'Schema is up to date!', 'No pull needed.' ] }; } // For same non-zero versions, check for actual content differences try { const comparisonResult = await apiClient.compareSchema(localConfig.schema); if (comparisonResult.valid) { // Schemas match - truly up to date return { ...baseResult, status: 'current', message: [ 'Schema is up to date!', 'No pull needed.' ] }; } else { // Same version but different content - conflict detected return { ...baseResult, status: 'conflict', message: [ 'Schema conflicts detected!', 'Your local schema differs from the remote schema at the same version.', 'Pull the remote version to override local changes?' ], needsConfirmation: true, confirmationTitle: 'Override Local Changes', confirmationMessage: 'This will replace your local schema with the remote version.' }; } } catch (error) { // If comparison fails, assume current to be safe return { ...baseResult, status: 'current', message: [ 'Schema is up to date!', 'No pull needed.', '(Unable to verify schema content - assuming current)' ] }; } case 'ahead': return { ...baseResult, status: 'ahead', message: [ 'Your local schema is ahead of the remote version.', 'Did you mean to push instead?', 'Use \'basic push\' to publish your changes.' ] }; default: return { ...baseResult, status: 'current', message: [ 'Schema is up to date!', 'No pull needed.' ] }; } } function PullConfirmationDialog({ statusResult, selectedOption }) { const getStatusIcon = () => { switch (statusResult.status) { case 'behind': return '⬇️'; case 'conflict': return '⚠️'; default: return '📥'; } }; const getStatusText = () => { switch (statusResult.status) { case 'behind': return 'Schema is out of date'; case 'conflict': return 'Schema conflicts detected'; default: return 'Schema update available'; } }; const getStatusColor = () => { switch (statusResult.status) { case 'behind': return 'yellow'; case 'conflict': return 'magenta'; 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, pull 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 PullStatusDisplay({ result }) { const getStatusColor = () => { switch (result.status) { case 'current': return 'green'; case 'ahead': return 'blue'; case 'conflict': return 'magenta'; case 'no-schema': return 'gray'; default: return 'white'; } }; const getStatusIcon = () => { switch (result.status) { case 'current': return '✅'; case 'ahead': return '🚀'; case 'conflict': return '⚠️'; case 'no-schema': return '📄'; default: return '❓'; } }; const getStatusDescription = () => { switch (result.status) { case 'current': return 'Schema is up to date'; case 'ahead': return 'Local schema is ahead'; case 'conflict': return 'Schema conflicts detected'; 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 'ahead': return [ 'Run \'basic push\' to publish your changes', 'Or run \'basic status\' for more details' ]; case 'conflict': return [ 'Run \'basic pull\' again to override local changes', 'Or run \'basic status\' to understand the differences', 'Consider backing up your local changes first' ]; 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))) }), 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 PullSuccessDisplay({ result }) { return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "green", children: "\u2705 Schema updated successfully!" }) }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { children: ["Updated: ", 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 Review the updated schema changes" }), _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 PullCommand() { render(_jsx(PullApp, {})); } //# sourceMappingURL=pull.js.map