@deriv-com/shiftai-cli
Version:
A comprehensive AI code detection and analysis CLI tool for tracking AI-generated code in projects
223 lines (194 loc) • 6.63 kB
JavaScript
const fs = require('fs-extra');
const path = require('path');
const { execSync } = require('child_process');
const chalk = require('chalk');
const inquirer = require('inquirer');
const { getAllRemotes } = require('../utils/git-utils');
/**
* Remote Selector - Manages main remote selection per project
*/
class RemoteSelector {
constructor(options = {}) {
this.projectRoot = options.projectRoot || process.cwd();
this.projectHash = options.projectHash;
this.shiftaiDir = path.join(require('os').homedir(), '.shiftai');
}
/**
* Get the path for storing remote selection for this project
*/
getRemoteConfigPath() {
if (this.projectHash) {
return path.join(this.shiftaiDir, this.projectHash, 'remote-config.json');
}
// Fallback to project-local config
return path.join(this.projectRoot, '.shiftai-remote-config.json');
}
/**
* Load the selected remote for this project
*/
async getSelectedRemote() {
try {
const configPath = this.getRemoteConfigPath();
if (await fs.pathExists(configPath)) {
const config = await fs.readJson(configPath);
return config.selectedRemote || null;
}
} catch (error) {
// Config doesn't exist or is invalid
}
return null;
}
/**
* Save the selected remote for this project
*/
async saveSelectedRemote(remoteName, remoteInfo) {
try {
const configPath = this.getRemoteConfigPath();
await fs.ensureDir(path.dirname(configPath));
const config = {
selectedRemote: remoteName,
remoteInfo: remoteInfo,
selectedAt: new Date().toISOString(),
projectPath: this.projectRoot
};
await fs.writeJson(configPath, config, { spaces: 2 });
return true;
} catch (error) {
console.warn(chalk.yellow(`⚠ Could not save remote selection: ${error.message}`));
return false;
}
}
/**
* Get all available remotes with enhanced information
*/
getAvailableRemotes() {
const remotes = getAllRemotes();
const remoteList = [];
for (const [name, info] of Object.entries(remotes)) {
remoteList.push({
name,
url: info.url,
organization: info.organization,
repository: info.repository,
displayName: `${name} (${info.organization}/${info.repository})`
});
}
return remoteList;
}
/**
* Check if remote selection is needed
*/
async shouldPromptForRemote() {
const selectedRemote = await this.getSelectedRemote();
if (selectedRemote) {
// Check if the selected remote still exists in git
const remotes = getAllRemotes();
if (remotes[selectedRemote]) {
return false; // Already have valid selection
} else {
// Selected remote no longer exists, clear the invalid selection
console.warn(`Warning: Selected remote '${selectedRemote}' no longer exists. Clearing selection.`);
await this.resetRemoteSelection();
}
}
// Check if we have multiple remotes (use actual git remotes, not cached)
const actualRemotes = getAllRemotes();
return Object.keys(actualRemotes).length > 1;
}
/**
* Prompt user to select main remote for diff comparisons using interactive arrow keys
*/
async promptForRemoteSelection() {
const availableRemotes = this.getAvailableRemotes();
if (availableRemotes.length === 0) {
throw new Error('No git remotes found');
}
if (availableRemotes.length === 1) {
// Only one remote, use it automatically
const remote = availableRemotes[0];
await this.saveSelectedRemote(remote.name, remote);
return remote.name;
}
console.log();
console.log(chalk.blue('🔄 Multiple Git Remotes Detected'));
console.log(chalk.blue('═══════════════════════════════'));
console.log(chalk.yellow('ShiftAI needs to know which remote to use for diff comparisons.'));
console.log(chalk.gray('This selection will be saved for this project.'));
console.log();
// Create choices for inquirer with enhanced display
const choices = availableRemotes.map(remote => ({
name: `${chalk.cyan(remote.name)} ${chalk.gray(`(${remote.organization}/${remote.repository})`)}`,
value: remote,
short: remote.name
}));
try {
const answers = await inquirer.prompt([
{
type: 'list',
name: 'selectedRemote',
message: 'Select the main remote for diff comparisons:',
choices: choices,
pageSize: 10,
loop: false
}
]);
const selectedRemote = answers.selectedRemote;
console.log();
console.log(chalk.green(`✅ Selected: ${selectedRemote.displayName}`));
// Save the selection
const saved = await this.saveSelectedRemote(selectedRemote.name, selectedRemote);
if (saved) {
console.log(chalk.green('✅ Remote selection saved for this project'));
}
return selectedRemote.name;
} catch (error) {
if (error.name === 'ExitPromptError') {
throw new Error('Remote selection cancelled');
}
throw error;
}
}
/**
* Get the main remote for diff comparisons (with auto-selection if needed)
*/
async getMainRemote() {
// Check if we already have a selection
const selectedRemote = await this.getSelectedRemote();
if (selectedRemote) {
const remotes = getAllRemotes();
if (remotes[selectedRemote]) {
return selectedRemote;
}
}
// Check if we need to prompt
const shouldPrompt = await this.shouldPromptForRemote();
if (shouldPrompt) {
return await this.promptForRemoteSelection();
}
// Single remote or no remotes - use default logic
const availableRemotes = this.getAvailableRemotes();
if (availableRemotes.length === 1) {
const remote = availableRemotes[0];
await this.saveSelectedRemote(remote.name, remote);
return remote.name;
}
// Fallback to origin
return 'origin';
}
/**
* Reset remote selection for this project
*/
async resetRemoteSelection() {
try {
const configPath = this.getRemoteConfigPath();
if (await fs.pathExists(configPath)) {
await fs.remove(configPath);
return true;
}
} catch (error) {
console.warn(chalk.yellow(`⚠ Could not reset remote selection: ${error.message}`));
}
return false;
}
}
module.exports = RemoteSelector;