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