gh2ide
Version:
Native messaging host for GitHub to IDE Chrome extension - opens GitHub repos and files directly in your IDE
485 lines (414 loc) โข 18.2 kB
JavaScript
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { writeFileSync, mkdirSync, chmodSync, readFileSync, cpSync, rmSync, existsSync, readdirSync } from 'node:fs';
import { homedir, platform } from 'node:os';
import { createInterface } from 'node:readline';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Read version from package.json
const packageJson = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf8'));
const VERSION = packageJson.version;
const OFFICIAL_EXTENSION_ID = 'nnjlhioijmplllcdjlecbkihcfdcnldb';
function showHelp() {
console.log(`
GitHub to IDE Native Host Installer v${VERSION}
This installer sets up the native messaging host that allows the GitHub to IDE
extension to communicate with your local IDEs.
USAGE:
npx gh2ide [OPTIONS]
OPTIONS:
--extension-id <id> Override the official extension ID. For development only.
--uninstall Remove the native host.
--help, -h Show this help message.
--version, -v Show the version number.
EXAMPLES:
# Install the native host (recommended)
npx gh2ide
# Uninstall the native host
npx gh2ide --uninstall
# Install for a development version of the extension
npx gh2ide --extension-id <your-development-id>
`);
}
function showVersion() {
console.log(`gh2ide v${VERSION}`);
}
function detectExtensions() {
const home = homedir();
const os = platform();
const extensions = [];
let extensionDirs = [];
if (os === 'darwin') {
extensionDirs = [
['Chrome', join(home, 'Library', 'Application Support', 'Google', 'Chrome', 'Default', 'Extensions')],
['Arc', join(home, 'Library', 'Application Support', 'Arc', 'User Data', 'Default', 'Extensions')],
['Chrome Beta', join(home, 'Library', 'Application Support', 'Google', 'Chrome Beta', 'Default', 'Extensions')],
['Chromium', join(home, 'Library', 'Application Support', 'Chromium', 'Default', 'Extensions')],
['Brave', join(home, 'Library', 'Application Support', 'BraveSoftware', 'Brave-Browser', 'Default', 'Extensions')],
['Edge', join(home, 'Library', 'Application Support', 'Microsoft Edge', 'Default', 'Extensions')],
];
} else if (os === 'linux') {
extensionDirs = [
['Chrome', join(home, '.config', 'google-chrome', 'Default', 'Extensions')],
['Chromium', join(home, '.config', 'chromium', 'Default', 'Extensions')],
['Brave', join(home, '.config', 'BraveSoftware', 'Brave-Browser', 'Default', 'Extensions')],
];
} else if (os === 'win32') {
const appData = process.env.LOCALAPPDATA || join(home, 'AppData', 'Local');
extensionDirs = [
['Chrome', join(appData, 'Google', 'Chrome', 'User Data', 'Default', 'Extensions')],
['Edge', join(appData, 'Microsoft', 'Edge', 'User Data', 'Default', 'Extensions')],
['Brave', join(appData, 'BraveSoftware', 'Brave-Browser', 'User Data', 'Default', 'Extensions')],
];
}
for (const [browserName, dir] of extensionDirs) {
if (!existsSync(dir)) continue;
try {
const extensionIds = readdirSync(dir);
for (const extensionId of extensionIds) {
// Chrome extension IDs are 32 characters, lowercase letters a-p
if (/^[a-p]{32}$/.test(extensionId)) {
const extensionPath = join(dir, extensionId);
// Try to find manifest.json in version subdirectories
try {
const versions = readdirSync(extensionPath);
for (const version of versions) {
const manifestPath = join(extensionPath, version, 'manifest.json');
if (existsSync(manifestPath)) {
try {
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
const name = manifest.name || 'Unknown Extension';
// Check if it's the GitHub to IDE extension
if (name.toLowerCase().includes('github') && (
name.toLowerCase().includes('ide') ||
name.toLowerCase().includes('vscode') ||
name.toLowerCase().includes('editor')
)) {
extensions.push({
id: extensionId,
name: name,
browser: browserName,
version: manifest.version || 'unknown'
});
break; // Found it, no need to check other versions
}
} catch (err) {
// Couldn't read/parse manifest, skip
}
}
}
} catch (err) {
// Couldn't read extension directory, skip
}
}
}
} catch (err) {
// Couldn't read extensions directory, skip this browser
}
}
return extensions;
}
async function promptForExtensionId() {
console.log('\n๐ GitHub to IDE - Native Host Installation\n');
// Try to auto-detect extensions
console.log('๐ Scanning for GitHub to IDE extension...\n');
const detectedExtensions = detectExtensions();
if (detectedExtensions.length > 0) {
console.log('โ
Found GitHub to IDE extension(s):\n');
detectedExtensions.forEach((ext, index) => {
console.log(` ${index + 1}. ${ext.name} (v${ext.version})`);
console.log(` Browser: ${ext.browser}`);
console.log(` ID: ${ext.id}\n`);
});
const rl = createInterface({
input: process.stdin,
output: process.stdout
});
if (detectedExtensions.length === 1) {
// Only one extension found, ask if they want to use it
return new Promise((resolve) => {
rl.question(`Use this extension? (Y/n): `, (answer) => {
rl.close();
const normalized = answer.trim().toLowerCase();
if (normalized === '' || normalized === 'y' || normalized === 'yes') {
resolve(detectedExtensions[0].id);
} else {
console.log('\n');
resolve(manualExtensionIdPrompt());
}
});
});
} else {
// Multiple extensions found, let user choose
return new Promise((resolve) => {
rl.question(`Select extension (1-${detectedExtensions.length}), or press Enter to enter manually: `, (answer) => {
rl.close();
const choice = parseInt(answer.trim());
if (choice >= 1 && choice <= detectedExtensions.length) {
resolve(detectedExtensions[choice - 1].id);
} else if (answer.trim() === '') {
console.log('\n');
resolve(manualExtensionIdPrompt());
} else {
console.log('\nโ Invalid selection.\n');
resolve(manualExtensionIdPrompt());
}
});
});
}
} else {
console.log('โน๏ธ No GitHub to IDE extension detected automatically.\n');
console.log('๐ก Note: Unpacked extensions (loaded via "Load unpacked") cannot be');
console.log(' auto-detected. Please enter your extension ID manually.\n');
return manualExtensionIdPrompt();
}
}
async function manualExtensionIdPrompt() {
const rl = createInterface({
input: process.stdin,
output: process.stdout
});
console.log('To find your extension ID manually:');
console.log(' 1. Open chrome://extensions in your browser');
console.log(' 2. Enable "Developer mode" (toggle in top-right corner)');
console.log(' 3. Find the "GitHub to IDE" extension');
console.log(' 4. Copy the ID shown below the extension name');
console.log(' (It looks like: haekngngecedekgjbeoijeaapjkmblgp)\n');
return new Promise((resolve) => {
rl.question('Enter your Chrome extension ID: ', (answer) => {
rl.close();
resolve(answer.trim());
});
});
}
function validateExtensionId(extensionId) {
// Chrome extension IDs are 32 characters, lowercase letters a-p
const isValid = /^[a-p]{32}$/.test(extensionId);
if (!isValid) {
console.error('\nโ Invalid extension ID format.');
console.error(' Extension IDs should be 32 characters (a-p only).');
console.error(' Example: haekngngecedekgjbeoijeaapjkmblgp\n');
return false;
}
return true;
}
async function install(extensionId) {
if (!extensionId) {
console.error('\nโ Error: Extension ID is required\n');
showHelp();
process.exit(1);
}
if (!validateExtensionId(extensionId)) {
process.exit(1);
}
console.log('\n๐ Installing GitHub to IDE native host...\n');
const home = homedir();
const installDir = join(home, '.github-to-ide', 'native-host');
const hostPath = join(installDir, 'index.js');
const runScriptPath = join(installDir, 'run.sh');
const packageJsonPath = join(installDir, 'package.json');
try {
// Create directory
console.log('๐ Creating installation directory...');
mkdirSync(installDir, { recursive: true });
// Copy files
console.log('๐ฆ Copying native host files...');
const indexJsSource = join(__dirname, 'index.js');
const runShSource = join(__dirname, 'run.sh');
const packageJsonSource = join(__dirname, 'package.json');
cpSync(indexJsSource, hostPath);
cpSync(runShSource, runScriptPath);
cpSync(packageJsonSource, packageJsonPath);
chmodSync(runScriptPath, 0o755);
// Create manifest
console.log('๐ Creating native messaging manifest...');
const manifestName = 'com.lovelesslabs.vscodeopener.json';
let manifestDir;
const os = platform();
if (os === 'darwin') {
// macOS - try multiple browser locations
const browsers = [
['Chrome', join(home, 'Library', 'Application Support', 'Google', 'Chrome', 'NativeMessagingHosts')],
['Arc', join(home, 'Library', 'Application Support', 'Arc', 'User Data', 'NativeMessagingHosts')],
['Chrome Beta', join(home, 'Library', 'Application Support', 'Google', 'Chrome Beta', 'NativeMessagingHosts')],
['Chromium', join(home, 'Library', 'Application Support', 'Chromium', 'NativeMessagingHosts')],
['Brave', join(home, 'Library', 'Application Support', 'BraveSoftware', 'Brave-Browser', 'NativeMessagingHosts')],
['Edge', join(home, 'Library', 'Application Support', 'Microsoft Edge', 'NativeMessagingHosts')]
];
manifestDir = browsers[0][1]; // Default to Chrome
// Install to all found browsers
for (const [browserName, dir] of browsers) {
try {
mkdirSync(dir, { recursive: true });
const manifestPath = join(dir, manifestName);
const manifest = {
name: 'com.lovelesslabs.vscodeopener',
description: 'GitHub to IDE native messaging host',
path: runScriptPath,
type: 'stdio',
allowed_origins: [`chrome-extension://${extensionId}/`]
};
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
console.log(` โ Installed for ${browserName}`);
} catch (err) {
// Browser not installed, skip silently
}
}
} else if (os === 'linux') {
// Linux
const browsers = [
['Chrome', join(home, '.config', 'google-chrome', 'NativeMessagingHosts')],
['Chromium', join(home, '.config', 'chromium', 'NativeMessagingHosts')],
['Brave', join(home, '.config', 'BraveSoftware', 'Brave-Browser', 'NativeMessagingHosts')]
];
manifestDir = browsers[0][1]; // Default to Chrome
for (const [browserName, dir] of browsers) {
try {
mkdirSync(dir, { recursive: true });
const manifestPath = join(dir, manifestName);
const manifest = {
name: 'com.lovelesslabs.vscodeopener',
description: 'GitHub to IDE native messaging host',
path: runScriptPath,
type: 'stdio',
allowed_origins: [`chrome-extension://${extensionId}/`]
};
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
console.log(` โ Installed for ${browserName}`);
} catch (err) {
// Browser not installed, skip silently
}
}
} else if (os === 'win32') {
console.log('\nโ ๏ธ Windows Installation');
console.log('Windows requires registry entries. Please run as administrator:');
console.log('\nRegistry path: HKEY_CURRENT_USER\\Software\\Google\\Chrome\\NativeMessagingHosts\\com.lovelesslabs.vscodeopener');
console.log(`Value: "${join(installDir, manifestName)}"`);
console.log('\nFor more details, see: https://developer.chrome.com/docs/apps/nativeMessaging/\n');
process.exit(1);
} else {
console.error(`\nโ Unsupported platform: ${os}`);
console.error('Supported platforms: macOS, Linux, Windows\n');
process.exit(1);
}
console.log('\nโ
Installation complete!\n');
console.log(` Native host: ${hostPath}`);
console.log(` Manifest: ${manifestDir}\n`);
console.log('๐ Please restart your browser for changes to take effect.\n');
console.log('๐งช Test the connection from the extension options page.\n');
} catch (error) {
console.error('\nโ Installation failed:', error.message);
console.error('\nPlease report issues at: https://github.com/justinloveless/github-to-ide/issues\n');
process.exit(1);
}
}
async function uninstall() {
console.log('\n๐๏ธ Uninstalling GitHub to IDE native host...\n');
const home = homedir();
const installDir = join(home, '.github-to-ide');
const manifestName = 'com.lovelesslabs.vscodeopener.json';
let filesRemoved = 0;
let errors = [];
try {
// Remove installation directory
if (existsSync(installDir)) {
console.log('๐ Removing installation directory...');
rmSync(installDir, { recursive: true, force: true });
console.log(` โ Removed ${installDir}`);
filesRemoved++;
} else {
console.log(' โน๏ธ Installation directory not found (already removed)');
}
// Remove manifests from all browsers
console.log('๐ Removing native messaging manifests...');
const os = platform();
let manifestDirs = [];
if (os === 'darwin') {
manifestDirs = [
['Chrome', join(home, 'Library', 'Application Support', 'Google', 'Chrome', 'NativeMessagingHosts')],
['Arc', join(home, 'Library', 'Application Support', 'Arc', 'User Data', 'NativeMessagingHosts')],
['Chrome Beta', join(home, 'Library', 'Application Support', 'Google', 'Chrome Beta', 'NativeMessagingHosts')],
['Chromium', join(home, 'Library', 'Application Support', 'Chromium', 'NativeMessagingHosts')],
['Brave', join(home, 'Library', 'Application Support', 'BraveSoftware', 'Brave-Browser', 'NativeMessagingHosts')],
['Edge', join(home, 'Library', 'Application Support', 'Microsoft Edge', 'NativeMessagingHosts')]
];
} else if (os === 'linux') {
manifestDirs = [
['Chrome', join(home, '.config', 'google-chrome', 'NativeMessagingHosts')],
['Chromium', join(home, '.config', 'chromium', 'NativeMessagingHosts')],
['Brave', join(home, '.config', 'BraveSoftware', 'Brave-Browser', 'NativeMessagingHosts')]
];
} else if (os === 'win32') {
console.log(' โ ๏ธ Windows: You may need to manually remove registry entries');
console.log(' Registry path: HKEY_CURRENT_USER\\Software\\Google\\Chrome\\NativeMessagingHosts\\com.lovelesslabs.vscodeopener\n');
}
for (const [browserName, dir] of manifestDirs) {
const manifestPath = join(dir, manifestName);
if (existsSync(manifestPath)) {
try {
rmSync(manifestPath, { force: true });
console.log(` โ Removed ${browserName} manifest`);
filesRemoved++;
} catch (err) {
errors.push(`Failed to remove ${browserName} manifest: ${err.message}`);
console.log(` โ Failed to remove ${browserName} manifest`);
}
}
}
if (filesRemoved === 0 && errors.length === 0) {
console.log('\nโ ๏ธ No installation found. Nothing to uninstall.\n');
console.log('The native host may have already been uninstalled, or was never installed.\n');
return;
}
console.log('\nโ
Uninstallation complete!\n');
if (filesRemoved > 0) {
console.log(` Removed ${filesRemoved} file(s)/directory(ies)\n`);
}
if (errors.length > 0) {
console.log('โ ๏ธ Some errors occurred:\n');
errors.forEach(err => console.log(` โข ${err}`));
console.log('');
}
console.log('๐ Please restart your browser for changes to take effect.\n');
} catch (error) {
console.error('\nโ Uninstallation failed:', error.message);
console.error('\nPlease report issues at: https://github.com/justinloveless/github-to-ide/issues\n');
process.exit(1);
}
}
// Main CLI logic
async function main() {
const args = process.argv.slice(2);
// Handle help
if (args.includes('--help') || args.includes('-h')) {
showHelp();
process.exit(0);
}
// Handle version
if (args.includes('--version') || args.includes('-v')) {
showVersion();
process.exit(0);
}
// Handle uninstall
if (args.includes('--uninstall')) {
await uninstall();
process.exit(0);
}
// Get extension ID from args, or use the official one as a default
const extensionIdIndex = args.indexOf('--extension-id');
let extensionId = OFFICIAL_EXTENSION_ID;
if (extensionIdIndex !== -1) {
if (args.length > extensionIdIndex + 1 && !args[extensionIdIndex + 1].startsWith('--')) {
extensionId = args[extensionIdIndex + 1];
} else {
console.error('\nโ Error: --extension-id flag requires a value.\n');
process.exit(1);
}
}
await install(extensionId);
}
main().catch((error) => {
console.error('\nโ Unexpected error:', error.message);
process.exit(1);
});