mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-23 00:41:36 +00:00
562 lines
16 KiB
JavaScript
562 lines
16 KiB
JavaScript
import { spawnSync } from 'node:child_process';
|
|
import {
|
|
cpSync,
|
|
existsSync,
|
|
mkdirSync,
|
|
mkdtempSync,
|
|
readFileSync,
|
|
readdirSync,
|
|
rmSync,
|
|
statSync,
|
|
writeFileSync,
|
|
} from 'node:fs';
|
|
import { createRequire } from 'node:module';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
const workspaceRoot = path.resolve(__dirname, '..', '..');
|
|
const sharedRoot = path.join(workspaceRoot, 'gitnexus-shared');
|
|
const gitnexusRoot = path.join(workspaceRoot, 'gitnexus');
|
|
const gitnexusWebRoot = path.join(workspaceRoot, 'gitnexus-web');
|
|
const gitnexusLockfile = JSON.parse(
|
|
readFileSync(path.join(gitnexusRoot, 'package-lock.json'), 'utf8'),
|
|
);
|
|
const gitnexusPackageJson = JSON.parse(
|
|
readFileSync(path.join(gitnexusRoot, 'package.json'), 'utf8'),
|
|
);
|
|
const gitnexusModuleResolver = createRequire(path.join(gitnexusRoot, 'package.json'));
|
|
const gitnexusServerEntry = path.join(gitnexusRoot, 'dist', 'server', 'api.js');
|
|
const gitnexusCliEntry = path.join(gitnexusRoot, 'dist', 'cli', 'index.js');
|
|
const gitnexusServerPort = 4747;
|
|
const gitnexusWebDevPort = 5173;
|
|
const desktopRendererPort = 5174;
|
|
const shouldCleanupDevPort = process.argv.includes('--cleanup-dev-port');
|
|
|
|
const getNodeModulePackagePath = (packageRoot, packageName) => {
|
|
return path.join(packageRoot, 'node_modules', ...packageName.split('/'));
|
|
};
|
|
|
|
const getNodeModulePackageJsonPath = (packageRoot, packageName) => {
|
|
return path.join(packageRoot, 'node_modules', ...packageName.split('/'), 'package.json');
|
|
};
|
|
|
|
const getLockedPackageInstallSpecifier = (packageName) => {
|
|
const lockedPackage = gitnexusLockfile.packages?.[`node_modules/${packageName}`];
|
|
const lockedVersion = lockedPackage?.version;
|
|
|
|
if (!lockedVersion || String(lockedVersion).startsWith('file:')) {
|
|
return null;
|
|
}
|
|
|
|
return lockedVersion;
|
|
};
|
|
|
|
const getDependencySubset = (dependencyMap, packageNames, overrides = {}) => {
|
|
return Object.fromEntries(
|
|
packageNames.map((packageName) => {
|
|
const versionSpecifier =
|
|
overrides[packageName] ??
|
|
getLockedPackageInstallSpecifier(packageName) ??
|
|
dependencyMap?.[packageName];
|
|
|
|
if (!versionSpecifier) {
|
|
throw new Error(`Missing package specifier for ${packageName}.`);
|
|
}
|
|
|
|
return [packageName, versionSpecifier];
|
|
}),
|
|
);
|
|
};
|
|
|
|
const getEntryMtimeMs = (targetPath) => {
|
|
if (!existsSync(targetPath)) {
|
|
return 0;
|
|
}
|
|
|
|
return statSync(targetPath).mtimeMs;
|
|
};
|
|
|
|
const getPathMtimeMs = (targetPath) => {
|
|
if (!existsSync(targetPath)) {
|
|
return 0;
|
|
}
|
|
|
|
const stats = statSync(targetPath);
|
|
|
|
if (!stats.isDirectory()) {
|
|
return stats.mtimeMs;
|
|
}
|
|
|
|
let newestMtimeMs = stats.mtimeMs;
|
|
|
|
for (const entry of readdirSync(targetPath, { withFileTypes: true })) {
|
|
if (entry.name === '.git' || entry.name === 'dist' || entry.name === 'node_modules') {
|
|
continue;
|
|
}
|
|
|
|
newestMtimeMs = Math.max(newestMtimeMs, getPathMtimeMs(path.join(targetPath, entry.name)));
|
|
}
|
|
|
|
return newestMtimeMs;
|
|
};
|
|
|
|
const getNewestMtimeMs = (paths) => {
|
|
return paths.reduce((latestMtimeMs, targetPath) => {
|
|
return Math.max(latestMtimeMs, getPathMtimeMs(targetPath));
|
|
}, 0);
|
|
};
|
|
|
|
const getOldestOutputMtimeMs = (paths) => {
|
|
let oldestMtimeMs = Number.POSITIVE_INFINITY;
|
|
|
|
for (const targetPath of paths) {
|
|
const mtimeMs = getPathMtimeMs(targetPath);
|
|
|
|
if (mtimeMs === 0) {
|
|
return 0;
|
|
}
|
|
|
|
oldestMtimeMs = Math.min(oldestMtimeMs, mtimeMs);
|
|
}
|
|
|
|
return Number.isFinite(oldestMtimeMs) ? oldestMtimeMs : 0;
|
|
};
|
|
|
|
const gitnexusSharedSourceInputs = [
|
|
path.join(sharedRoot, 'src'),
|
|
path.join(sharedRoot, 'package.json'),
|
|
path.join(sharedRoot, 'package-lock.json'),
|
|
path.join(sharedRoot, 'tsconfig.json'),
|
|
];
|
|
|
|
const gitnexusSharedInstallInputs = [
|
|
path.join(sharedRoot, 'package.json'),
|
|
path.join(sharedRoot, 'package-lock.json'),
|
|
path.join(sharedRoot, 'tsconfig.json'),
|
|
];
|
|
|
|
const gitnexusSharedInstallMarkerPaths = [path.join(sharedRoot, 'node_modules', 'typescript')];
|
|
|
|
const gitnexusInstallInputs = [
|
|
path.join(gitnexusRoot, 'package.json'),
|
|
path.join(gitnexusRoot, 'package-lock.json'),
|
|
path.join(sharedRoot, 'package.json'),
|
|
path.join(sharedRoot, 'package-lock.json'),
|
|
];
|
|
|
|
const gitnexusDesktopRuntimeDependencyNames = [
|
|
'@ladybugdb/core',
|
|
'@modelcontextprotocol/sdk',
|
|
'cli-progress',
|
|
'commander',
|
|
'cors',
|
|
'express',
|
|
'glob',
|
|
'graphology',
|
|
'graphology-indices',
|
|
'graphology-utils',
|
|
'ignore',
|
|
'js-yaml',
|
|
'jsonc-parser',
|
|
'lru-cache',
|
|
'mnemonist',
|
|
'pandemonium',
|
|
'uuid',
|
|
];
|
|
const unmanagedGitNexusRuntimeDependencyNames = Object.keys(gitnexusPackageJson.dependencies ?? {})
|
|
.filter((packageName) => !gitnexusDesktopRuntimeDependencyNames.includes(packageName))
|
|
.sort();
|
|
|
|
const gitnexusDesktopBuildDependencyNames = ['@types/node', 'gitnexus-shared', 'typescript'];
|
|
|
|
const gitnexusDesktopRuntimeDependencies = getDependencySubset(
|
|
gitnexusPackageJson.dependencies ?? {},
|
|
gitnexusDesktopRuntimeDependencyNames,
|
|
);
|
|
|
|
const gitnexusDesktopBuildDependencies = getDependencySubset(
|
|
gitnexusPackageJson.devDependencies ?? {},
|
|
gitnexusDesktopBuildDependencyNames,
|
|
{ 'gitnexus-shared': `file:${sharedRoot}` },
|
|
);
|
|
|
|
const gitnexusDesktopRuntimeInstallMarkerPaths = [
|
|
...gitnexusDesktopRuntimeDependencyNames.map((packageName) =>
|
|
getNodeModulePackageJsonPath(gitnexusRoot, packageName),
|
|
),
|
|
path.join(gitnexusRoot, 'node_modules', 'commander', 'index.js'),
|
|
path.join(gitnexusRoot, 'node_modules', '@ladybugdb', 'core', 'lbugjs.node'),
|
|
];
|
|
|
|
const gitnexusDesktopRuntimeResolutionChecks = [
|
|
'commander',
|
|
'@modelcontextprotocol/sdk/server/index.js',
|
|
'@modelcontextprotocol/sdk/server/streamableHttp.js',
|
|
'ignore',
|
|
'js-yaml',
|
|
];
|
|
|
|
const gitnexusDesktopBuildInstallMarkerPaths = gitnexusDesktopBuildDependencyNames.map(
|
|
(packageName) => getNodeModulePackageJsonPath(gitnexusRoot, packageName),
|
|
);
|
|
|
|
const gitnexusRuntimeInputs = [
|
|
path.join(gitnexusRoot, 'src'),
|
|
path.join(gitnexusRoot, 'scripts'),
|
|
path.join(gitnexusRoot, 'package.json'),
|
|
path.join(gitnexusRoot, 'package-lock.json'),
|
|
path.join(gitnexusRoot, 'tsconfig.json'),
|
|
...gitnexusSharedSourceInputs,
|
|
];
|
|
|
|
const gitnexusRuntimeOutputs = [gitnexusServerEntry, gitnexusCliEntry];
|
|
|
|
const isInstallStale = (inputPaths, installMarkerPaths) => {
|
|
if (installMarkerPaths.some((targetPath) => !existsSync(targetPath))) {
|
|
return true;
|
|
}
|
|
|
|
const installMarkerMtimeMs = Math.min(...installMarkerPaths.map(getEntryMtimeMs));
|
|
|
|
return getNewestMtimeMs(inputPaths) > installMarkerMtimeMs;
|
|
};
|
|
|
|
const isGitNexusSharedInstallStale = () => {
|
|
return isInstallStale(gitnexusSharedInstallInputs, gitnexusSharedInstallMarkerPaths);
|
|
};
|
|
|
|
const canResolveGitNexusSpecifier = (specifier) => {
|
|
try {
|
|
gitnexusModuleResolver.resolve(specifier);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const isGitNexusRuntimeInstallStale = () => {
|
|
return (
|
|
isInstallStale(gitnexusInstallInputs, gitnexusDesktopRuntimeInstallMarkerPaths) ||
|
|
gitnexusDesktopRuntimeResolutionChecks.some(
|
|
(specifier) => !canResolveGitNexusSpecifier(specifier),
|
|
)
|
|
);
|
|
};
|
|
|
|
const isGitNexusBuildInstallStale = () => {
|
|
return isInstallStale(gitnexusInstallInputs, gitnexusDesktopBuildInstallMarkerPaths);
|
|
};
|
|
|
|
const isGitNexusBuildStale = () => {
|
|
return getNewestMtimeMs(gitnexusRuntimeInputs) > getOldestOutputMtimeMs(gitnexusRuntimeOutputs);
|
|
};
|
|
|
|
const runNpmAttempt = (args, cwd) => {
|
|
const result = spawnSync('npm', args, {
|
|
cwd,
|
|
stdio: 'inherit',
|
|
shell: process.platform === 'win32',
|
|
});
|
|
|
|
if (result.error) {
|
|
throw result.error;
|
|
}
|
|
|
|
return result.status ?? 1;
|
|
};
|
|
|
|
const runNpm = (args, cwd) => {
|
|
const status = runNpmAttempt(args, cwd);
|
|
|
|
if (status !== 0) {
|
|
process.exit(status);
|
|
}
|
|
};
|
|
|
|
const overlayDependencyPackages = (sourceDirectory, destinationDirectory, dependencyMap) => {
|
|
mkdirSync(destinationDirectory, { recursive: true });
|
|
|
|
for (const packageName of Object.keys(dependencyMap)) {
|
|
const sourcePath = path.join(sourceDirectory, ...packageName.split('/'));
|
|
const destinationPath = path.join(destinationDirectory, ...packageName.split('/'));
|
|
|
|
if (!existsSync(sourcePath)) {
|
|
continue;
|
|
}
|
|
|
|
if (existsSync(destinationPath)) {
|
|
continue;
|
|
}
|
|
|
|
mkdirSync(path.dirname(destinationPath), { recursive: true });
|
|
|
|
try {
|
|
cpSync(sourcePath, destinationPath, {
|
|
dereference: true,
|
|
force: true,
|
|
recursive: true,
|
|
});
|
|
} catch (error) {
|
|
const errorCode = error?.code;
|
|
const isLockedPathError =
|
|
(errorCode === 'EPIPE' || errorCode === 'EBUSY' || errorCode === 'EPERM') &&
|
|
existsSync(destinationPath);
|
|
const isExistingPathError = errorCode === 'EEXIST' && existsSync(destinationPath);
|
|
|
|
if (!isLockedPathError && !isExistingPathError) {
|
|
throw error;
|
|
}
|
|
|
|
console.warn(
|
|
`[gitnexus-desktop] Skipping already-present dependency path during repair: ${destinationPath}`,
|
|
);
|
|
}
|
|
}
|
|
};
|
|
|
|
const replaceDirectGitNexusPackages = (dependencyMap) => {
|
|
for (const packageName of Object.keys(dependencyMap)) {
|
|
rmSync(getNodeModulePackagePath(gitnexusRoot, packageName), {
|
|
force: true,
|
|
recursive: true,
|
|
});
|
|
}
|
|
};
|
|
|
|
const repairGitNexusPackages = (dependencyMap, label) => {
|
|
const repairDirectory = mkdtempSync(path.join(os.tmpdir(), 'gitnexus-desktop-gitnexus-repair-'));
|
|
|
|
try {
|
|
writeFileSync(
|
|
path.join(repairDirectory, 'package.json'),
|
|
JSON.stringify(
|
|
{
|
|
name: 'gitnexus-desktop-gitnexus-repair',
|
|
private: true,
|
|
dependencies: dependencyMap,
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
|
|
console.info(`[gitnexus-desktop] ${label}.`);
|
|
runNpm(['install', '--no-package-lock'], repairDirectory);
|
|
|
|
replaceDirectGitNexusPackages(dependencyMap);
|
|
overlayDependencyPackages(
|
|
path.join(repairDirectory, 'node_modules'),
|
|
path.join(gitnexusRoot, 'node_modules'),
|
|
dependencyMap,
|
|
);
|
|
} finally {
|
|
rmSync(repairDirectory, { force: true, recursive: true });
|
|
}
|
|
};
|
|
|
|
const runCommand = (command, args) => {
|
|
return spawnSync(command, args, {
|
|
encoding: 'utf8',
|
|
shell: false,
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
windowsHide: true,
|
|
});
|
|
};
|
|
|
|
const getPosixCommandLine = (pid) => {
|
|
if (!pid) {
|
|
return '';
|
|
}
|
|
|
|
const lookup = runCommand('ps', ['-p', String(pid), '-o', 'args=']);
|
|
|
|
if (lookup.status !== 0) {
|
|
return '';
|
|
}
|
|
|
|
return lookup.stdout.trim();
|
|
};
|
|
|
|
const findPortOwner = (port) => {
|
|
if (process.platform === 'win32') {
|
|
const lookup = runCommand('powershell.exe', [
|
|
'-NoProfile',
|
|
'-Command',
|
|
[
|
|
`$connection = Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue | Where-Object { $_.State -eq 'Listen' } | Select-Object -First 1;`,
|
|
'if (-not $connection) { return }',
|
|
`$process = Get-CimInstance Win32_Process -Filter \"ProcessId = $($connection.OwningProcess)\";`,
|
|
'$payload = [PSCustomObject]@{',
|
|
' pid = $connection.OwningProcess;',
|
|
' name = $process.Name;',
|
|
' executablePath = $process.ExecutablePath;',
|
|
' commandLine = $process.CommandLine',
|
|
'};',
|
|
'$payload | ConvertTo-Json -Compress',
|
|
].join(' '),
|
|
]);
|
|
|
|
if (lookup.status !== 0 || !lookup.stdout.trim()) {
|
|
return null;
|
|
}
|
|
|
|
return JSON.parse(lookup.stdout.trim());
|
|
}
|
|
|
|
const lookup = runCommand('lsof', ['-nP', `-iTCP:${port}`, '-sTCP:LISTEN', '-Fpc']);
|
|
|
|
if (lookup.status !== 0 || !lookup.stdout.trim()) {
|
|
return null;
|
|
}
|
|
|
|
const owner = { pid: null, name: '', commandLine: '' };
|
|
|
|
for (const line of lookup.stdout.split(/\r?\n/)) {
|
|
if (line.startsWith('p')) {
|
|
owner.pid = Number.parseInt(line.slice(1), 10);
|
|
} else if (line.startsWith('c')) {
|
|
owner.name = line.slice(1);
|
|
}
|
|
}
|
|
|
|
owner.commandLine = getPosixCommandLine(owner.pid);
|
|
|
|
return owner.pid ? owner : null;
|
|
};
|
|
|
|
const killProcessTree = (pid) => {
|
|
if (!pid) {
|
|
return;
|
|
}
|
|
|
|
if (process.platform === 'win32') {
|
|
const result = spawnSync('taskkill', ['/pid', String(pid), '/t', '/f'], {
|
|
stdio: 'inherit',
|
|
windowsHide: true,
|
|
});
|
|
|
|
if ((result.status ?? 1) !== 0) {
|
|
process.exit(result.status ?? 1);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
process.kill(pid, 'SIGTERM');
|
|
};
|
|
|
|
const ensureDesktopRendererPortAvailable = () => {
|
|
const owner = findPortOwner(desktopRendererPort);
|
|
|
|
if (!owner) {
|
|
return;
|
|
}
|
|
|
|
const signature = `${owner.name ?? ''} ${owner.commandLine ?? ''}`.toLowerCase();
|
|
const isStaleElectronViteProcess = signature.includes('electron-vite');
|
|
|
|
if (!isStaleElectronViteProcess) {
|
|
console.error(
|
|
`Port ${desktopRendererPort} is already in use by ${owner.name ?? 'another process'} (PID ${owner.pid}). Stop that process and try again.`,
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
console.info(
|
|
`Stopping stale desktop renderer process on port ${desktopRendererPort} (PID ${owner.pid}).`,
|
|
);
|
|
killProcessTree(owner.pid);
|
|
};
|
|
|
|
const ensureGitNexusWebDevPortAvailable = () => {
|
|
const owner = findPortOwner(gitnexusWebDevPort);
|
|
|
|
if (!owner) {
|
|
return;
|
|
}
|
|
|
|
const signature = `${owner.name ?? ''} ${owner.commandLine ?? ''}`.toLowerCase();
|
|
const isGitNexusWebViteProcess = signature.includes('vite') && signature.includes('gitnexus-web');
|
|
|
|
if (!isGitNexusWebViteProcess) {
|
|
console.error(
|
|
`Port ${gitnexusWebDevPort} is already in use by ${owner.name ?? 'another process'} (PID ${owner.pid}). Stop that process and try again.`,
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
console.info(
|
|
`Stopping stale GitNexus web dev server on port ${gitnexusWebDevPort} (PID ${owner.pid}).`,
|
|
);
|
|
killProcessTree(owner.pid);
|
|
};
|
|
|
|
const ensureGitNexusServerPortAvailable = () => {
|
|
const owner = findPortOwner(gitnexusServerPort);
|
|
|
|
if (!owner) {
|
|
return;
|
|
}
|
|
|
|
const signature = `${owner.name ?? ''} ${owner.commandLine ?? ''}`.toLowerCase();
|
|
const isGitNexusServerProcess =
|
|
(signature.includes('dist\\cli\\index.js') || signature.includes('dist/cli/index.js')) &&
|
|
signature.includes('serve');
|
|
|
|
if (!isGitNexusServerProcess) {
|
|
console.error(
|
|
`Port ${gitnexusServerPort} is already in use by ${owner.name ?? 'another process'} (PID ${owner.pid}). Stop that process and try again.`,
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
console.info(`Stopping stale GitNexus backend on port ${gitnexusServerPort} (PID ${owner.pid}).`);
|
|
killProcessTree(owner.pid);
|
|
};
|
|
|
|
if (shouldCleanupDevPort) {
|
|
ensureGitNexusServerPortAvailable();
|
|
ensureDesktopRendererPortAvailable();
|
|
ensureGitNexusWebDevPortAvailable();
|
|
}
|
|
|
|
if (
|
|
!existsSync(path.join(sharedRoot, 'node_modules', 'typescript')) ||
|
|
isGitNexusSharedInstallStale()
|
|
) {
|
|
console.info('[gitnexus-desktop] Refreshing gitnexus-shared dependencies.');
|
|
runNpm(['ci'], sharedRoot);
|
|
}
|
|
|
|
if (unmanagedGitNexusRuntimeDependencyNames.length > 0) {
|
|
console.warn(
|
|
'[gitnexus-desktop] Runtime repair does not yet manage these direct GitNexus dependencies:\n' +
|
|
unmanagedGitNexusRuntimeDependencyNames.map((packageName) => ` - ${packageName}`).join('\n'),
|
|
);
|
|
}
|
|
|
|
if (isGitNexusRuntimeInstallStale()) {
|
|
repairGitNexusPackages(gitnexusDesktopRuntimeDependencies, 'Repairing GitNexus runtime packages');
|
|
}
|
|
|
|
if (!existsSync(path.join(gitnexusWebRoot, 'node_modules', 'vite'))) {
|
|
runNpm(['ci'], gitnexusWebRoot);
|
|
}
|
|
|
|
if (getOldestOutputMtimeMs(gitnexusRuntimeOutputs) === 0 || isGitNexusBuildStale()) {
|
|
if (isGitNexusBuildInstallStale()) {
|
|
repairGitNexusPackages(
|
|
{
|
|
...gitnexusDesktopRuntimeDependencies,
|
|
...gitnexusDesktopBuildDependencies,
|
|
},
|
|
'Repairing GitNexus build tooling',
|
|
);
|
|
}
|
|
|
|
console.info('[gitnexus-desktop] Rebuilding GitNexus runtime artifacts.');
|
|
runNpm(['run', 'build'], gitnexusRoot);
|
|
}
|