pilot-agent-cli
Version:
GitHub Copilot automation tool with configuration-driven file management
429 lines (362 loc) ⢠15.2 kB
JavaScript
const { spawn } = require('child_process');
const readline = require('readline');
const { promisify } = require('util');
const exec = promisify(require('child_process').exec);
const fs = require('fs');
const path = require('path');
// Suppress deprecation warnings for worker.terminate()
process.removeAllListeners('warning');
process.on('warning', (warning) => {
if (!warning.message.includes('DEP0132') &&
!warning.message.includes('Passing a callback to worker.terminate()')) {
console.warn(warning.message);
}
});
// Import the new service following hexagonal architecture
const CopilotServerDetector = require('./src/infrastructure/services/CopilotServerDetector');
const CopilotAgentInitializer = require('./src/infrastructure/services/CopilotAgentInitializer');
let copilotServer = null;
let agentInitializer = null;
let messageId = 1;
let isPolling = false;
let pollInterval = null;
let authAttempts = 0;
let maxAuthAttempts = 3;
let isAuthenticating = false;
let authenticationState = 'unknown'; // 'unknown', 'authenticated', 'pending', 'failed'
// Function to detect and start Copilot server
async function startCopilotServer() {
const detector = new CopilotServerDetector();
const detection = await detector.detect();
if (!detection.found) {
process.exit(1);
}
// Get spawn configuration from detector
const spawnConfig = detector.getSpawnCommand(detection.method, detection.path);
const spawnOptions = detector.getSpawnOptions(detection.method);
// Start attempts with different methods
const attempts = [
() => spawn(spawnConfig.command, spawnConfig.args, spawnOptions),
() => spawn('npx', ['copilot-language-server', '--stdio'], {
stdio: ['pipe', 'pipe', 'pipe'],
shell: process.platform === 'win32'
})
];
// Add additional attempts based on detection
if (detection.path) {
attempts.push(() => spawn('node', [detection.path, '--stdio'], {
stdio: ['pipe', 'pipe', 'pipe']
}));
}
let lastError = null;
for (let i = 0; i < attempts.length; i++) {
try {
console.log(`š§ Attempt ${i + 1}/${attempts.length}...`);
copilotServer = attempts[i]();
// Wait to see if process starts
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => resolve(), 3000);
copilotServer.on('error', (error) => {
clearTimeout(timeout);
reject(error);
});
copilotServer.on('spawn', () => {
clearTimeout(timeout);
resolve();
});
});
console.log('ā
Server started successfully');
// Setup event handlers after successful spawn
copilotServer.stderr.on('data', (data) => {
console.error('Server error:', data.toString());
});
copilotServer.on('close', (code) => {
console.log(`Server closed with code: ${code}`);
process.exit(code);
});
copilotServer.on('error', (error) => {
console.error('ā Server error:', error.message);
process.exit(1);
});
break;
} catch (error) {
console.log(`ā Attempt ${i + 1} failed:`, error.message);
lastError = error;
if (copilotServer) {
try { copilotServer.kill(); } catch (e) {}
copilotServer = null;
}
}
}
if (!copilotServer) {
console.error('ā Unable to start server');
console.log('š Debugging information:');
console.log(` Platform: ${process.platform}`);
console.log(` Node version: ${process.version}`);
if (detection.path) {
console.log(` Server path: ${detection.path}`);
}
throw lastError || new Error('All attempts failed');
}
return copilotServer;
}
// Interface to read user input
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
// Function to send JSON-RPC message
function sendMessage(method, params = {}) {
const message = {
jsonrpc: '2.0',
id: messageId++,
method: method,
params: params
};
const content = JSON.stringify(message);
const header = `Content-Length: ${Buffer.byteLength(content)}\r\n\r\n`;
console.log(`ā Sending: ${method}`);
copilotServer.stdin.write(header + content);
}
// Function to send JSON-RPC notification
function sendNotification(method, params = {}) {
const message = {
jsonrpc: '2.0',
method: method,
params: params
};
const content = JSON.stringify(message);
const header = `Content-Length: ${Buffer.byteLength(content)}\r\n\r\n`;
console.log(`ā Notification: ${method}`);
copilotServer.stdin.write(header + content);
}
// Clean shutdown handling
process.on('SIGINT', () => {
console.log('\nš Shutting down...');
if (copilotServer && !copilotServer.killed) {
copilotServer.kill();
}
rl.close();
process.exit(0);
});
// Initialization sequence
async function initialize() {
console.log('š Starting GitHub Copilot authentication...\n');
try {
await startCopilotServer();
// Initialize the agent with proper LSP sequence
agentInitializer = new CopilotAgentInitializer(console);
// Setup event listeners for the initializer
agentInitializer.on('statusChanged', (status) => {
console.log(`š Agent status changed: ${status}`);
});
agentInitializer.on('authenticationRequired', (authData) => {
console.log('\nš GITHUB COPILOT AUTHENTICATION REQUIRED š');
console.log('ā'.repeat(60));
console.log('š Open this URL in your browser:');
console.log(` ${authData.verificationUri}`);
console.log('');
console.log('š Enter this code on the GitHub page:');
console.log(` ${authData.userCode}`);
console.log('');
console.log(`ā±ļø Code valid for: ${Math.floor(authData.expiresIn / 60)} minutes`);
console.log('');
console.log('š Steps to follow:');
console.log(' 1. Click the link above or copy it to your browser');
console.log(' 2. Log in to GitHub if necessary');
console.log(' 3. Enter the user code displayed above');
console.log(' 4. Authorize access to GitHub Copilot');
console.log(' 5. Return to this terminal');
console.log('');
console.log(`š” OR use command: confirm ${authData.userCode}`);
console.log('ā'.repeat(60));
console.log('ā³ Waiting for your authentication...');
console.log('');
// Start automatic polling
setTimeout(() => startPolling(authData.userCode), 5000);
});
agentInitializer.on('error', (error) => {
console.error('ā Agent initialization error:', error.message);
if (error.message.includes('Agent service not initialized')) {
console.log('\nš” SOLUTION STEPS:');
console.log('š¹ 1. Ensure GitHub Copilot subscription is active');
console.log('š¹ 2. Try: gh auth login --scopes copilot');
console.log('š¹ 3. Verify: gh auth status');
console.log('š¹ 4. Restart this script');
}
});
// Perform proper agent initialization
console.log('š§ Initializing agent service with LSP protocol...');
const initSuccess = await agentInitializer.initializeAgent(copilotServer);
if (initSuccess) {
const status = agentInitializer.getStatus();
console.log('\nā
AGENT INITIALIZATION SUCCESSFUL! ā
');
console.log('ā'.repeat(60));
console.log(`ā
Agent Status: ${status.agentStatus}`);
console.log(`ā
LSP Initialized: ${status.isInitialized}`);
if (status.agentStatus === 'authenticated') {
console.log('ā
Already authenticated - ready to use');
console.log('ā'.repeat(60));
console.log('\nš” NEXT STEPS:');
console.log('š¹ GitHub Copilot is now authenticated and ready');
console.log('š¹ Authentication persists across sessions');
console.log('š¹ You can now use compatible Copilot clients');
console.log('š¹ Type "quit" to close this authentication script');
} else {
console.log('š Authentication required - triggering sign-in...');
console.log('ā'.repeat(60));
// Trigger authentication
try {
await agentInitializer.triggerAuthentication(copilotServer);
} catch (authError) {
if (authError.message.includes('No pending sign in')) {
console.log('\nš AUTHENTICATION CONFIRMED! š');
console.log('ā'.repeat(60));
console.log('ā
"No pending sign in" means you are already logged in');
console.log('ā
GitHub Copilot is ready to use');
console.log('ā
Authentication completed successfully');
console.log('ā'.repeat(60));
} else {
throw authError;
}
}
}
} else {
throw new Error('Agent initialization failed');
}
// Setup interactive interface
setTimeout(() => {
const currentStatus = agentInitializer.getStatus();
if (currentStatus.agentStatus !== 'authenticated') {
console.log('\nš Available commands:');
console.log('- "auth" : Start authentication');
console.log('- "status" : Check status');
console.log('- "confirm <code>" : Confirm with user code (after seeing the code)');
console.log('- "unauth" : Disconnect from GitHub Copilot');
console.log('- "stop" : Stop automatic polling');
console.log('- "quit" : Exit');
console.log('');
console.log('š” The "confirm" command is only used if you see an authentication code');
console.log('š” The "unauth" command revokes your GitHub Copilot authentication');
console.log('š” If you are already connected, just use "quit" to exit');
}
rl.on('line', (input) => {
const parts = input.trim().split(' ');
const command = parts[0].toLowerCase();
switch (command) {
case 'auth':
console.log('š Starting authentication...');
agentInitializer.triggerAuthentication(copilotServer).catch(error => {
if (error.message.includes('No pending sign in')) {
console.log('ā
Already authenticated');
} else {
console.error('ā Authentication error:', error.message);
}
});
break;
case 'status':
console.log('š Checking status...');
const status = agentInitializer.getStatus();
console.log(`Agent Status: ${status.agentStatus}`);
console.log(`LSP Initialized: ${status.isInitialized}`);
console.log(`Capabilities: ${status.capabilities ? 'Available' : 'None'}`);
break;
case 'confirm':
if (parts.length > 1) {
const userCode = parts[1];
console.log(`š Confirming with code: ${userCode}`);
sendMessage('signInConfirm', { userCode: userCode });
} else {
console.log('ā Usage: confirm <user_code>');
console.log('š” Example: confirm ABCD-EFGH');
console.log('š” Use this command only if you see an authentication code');
}
break;
case 'unauth':
case 'logout':
case 'signout':
console.log('š Disconnecting from GitHub Copilot...');
console.log('ā ļø This will revoke your GitHub Copilot authentication');
// Stop polling if active
if (isPolling && pollInterval) {
clearInterval(pollInterval);
isPolling = false;
console.log('š Stopping polling before logout');
}
console.log('š§ Logging out...');
sendMessage('signOut', { dummy: 'value' });
break;
case 'stop':
if (isPolling && pollInterval) {
clearInterval(pollInterval);
isPolling = false;
console.log('š Automatic polling stopped manually');
} else {
console.log('ā¹ļø No polling in progress');
}
break;
case 'quit':
case 'exit':
console.log('š Goodbye!');
console.log('ā
GitHub Copilot remains authenticated for future use');
console.log('š§ Authentication will be available for all compatible Copilot clients');
// Clean up resources
if (agentInitializer) {
agentInitializer.cleanup();
}
if (pollInterval) {
clearInterval(pollInterval);
isPolling = false;
}
rl.close();
if (copilotServer && !copilotServer.killed) {
copilotServer.kill('SIGTERM');
setTimeout(() => {
if (copilotServer && !copilotServer.killed) {
copilotServer.kill('SIGKILL');
}
process.exit(0);
}, 2000);
} else {
process.exit(0);
}
break;
default:
console.log('ā Unknown command. Use: auth, status, confirm <code>, unauth, stop, or quit');
}
});
}, 1000);
} catch (error) {
console.error('ā Error during initialization:', error.message);
if (error.message.includes('copilot-language-server not found')) {
console.log('\nš¦ INSTALLATION REQUIRED:');
console.log('npm install -g @github/copilot-language-server');
} else if (error.message.includes('Agent service not initialized')) {
console.log('\nš§ TROUBLESHOOTING STEPS:');
console.log('1. Verify GitHub Copilot subscription is active');
console.log('2. Run: gh auth login --scopes copilot');
console.log('3. Check: gh auth status');
console.log('4. Restart this script');
}
process.exit(1);
}
}
// Function to start automatic polling
function startPolling(userCode) {
console.log('š Starting automatic polling...');
isPolling = true;
pollInterval = setInterval(() => {
console.log('š Checking authentication...');
sendMessage('signInConfirm', { userCode: userCode });
}, 10000); // Every 10 seconds
// Stop polling after 15 minutes
setTimeout(() => {
if (isPolling) {
console.log('ā° Polling timeout reached - stopping polling');
clearInterval(pollInterval);
isPolling = false;
}
}, 15 * 60 * 1000); // 15 minutes
}
// Start initialization
initialize();