File size: 7,798 Bytes
2b64d42 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 | // Logger must be imported first to patch log functions before other modules use them
import './dashboard/logger.js';
import { initAuth, isAuthenticated, saveAccountsSync } from './auth.js';
import { startLanguageServer, waitForReady, isLanguageServerRunning, stopLanguageServer, cleanupOrphanLanguageServers } from './langserver.js';
import { startServer } from './server.js';
import { config, log } from './config.js';
import { existsSync, mkdirSync, readdirSync, rmSync } from 'fs';
import { tmpdir } from 'os';
import { execSync } from 'child_process';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
import { VERSION, BRAND } from './version.js';
import { abortActiveSse } from './sse-registry.js';
import { startQuietWindowAutoUpdate, stopQuietWindowAutoUpdate } from './dashboard/quiet-window-updater.js';
export { VERSION, BRAND };
function workspaceBase() {
const tmpDir = process.env.TEMP || process.env.TMP || tmpdir();
const suffix = process.env.HOSTNAME ? `-${process.env.HOSTNAME}` : '';
return join(tmpDir, `windsurf-workspace${suffix}`);
}
function resetWorkspace() {
// Wipe the workspace on every startup. If we don't, files created by
// previous chat sessions (e.g. Claude "editing" config.yaml/lru_cache.py
// via the baked-in Cascade tool prompts) persist and pollute the next
// request β the model sees them at session init and starts narrating
// edits to files the caller never mentioned.
//
// Using Node fs APIs instead of an `execSync('mkdir -p ... && rm -rf')`
// shell pipeline so this is correct on Windows, macOS, and Linux without
// depending on a POSIX shell.
const wsBase = workspaceBase();
try {
mkdirSync(wsBase, { recursive: true });
for (const name of readdirSync(wsBase)) {
try { rmSync(join(wsBase, name), { recursive: true, force: true }); } catch {}
}
} catch {}
try {
mkdirSync(join('/opt/windsurf/data', 'db'), { recursive: true });
} catch {}
}
async function main() {
const banner = `
_ _ _ _ __ _ ____ ___
| | | (_) | | / _| / \\ | _ \\_ _|
| | | |_ _ __ __| |___ _ _ _ __ _| |_ / _ \\ | |_) | |
| |/\\| | | '_ \\ / _\` / __| | | | '__|_ _|/ ___ \\| __/| |
\\ /\\ / | | | | (_| \\__ \\ |_| | | |_| /_/ \\_\\_| |___|
\\/ \\/|_|_| |_|\\__,_|___/\\__,_|_|
${BRAND} v${VERSION}
`;
console.log(banner);
console.log(` OpenAI-compatible proxy for Windsurf β by dwgx1337\n`);
// Start language server binary.
// Auto-install if missing β users repeatedly miss the manual install step
// and open "request crashes" issues (see #18), so we just do it ourselves.
// Skipped on Windows (LS is Linux-only) and when install-ls.sh isn't present.
const binaryPath = config.lsBinaryPath;
if (!existsSync(binaryPath) && process.platform === 'win32') {
log.warn('Windows detected: the Language Server binary is Linux/macOS only.');
log.warn('Options: (1) Use Docker (see docker-compose.yml), (2) Use WSL2, or');
log.warn('(3) Point LS_BINARY_PATH to a Windsurf desktop app language_server binary.');
}
if (!existsSync(binaryPath) && process.platform !== 'win32') {
const scriptPath = (() => {
try {
const here = dirname(fileURLToPath(import.meta.url));
return join(here, '..', 'install-ls.sh');
} catch { return null; }
})();
if (scriptPath && existsSync(scriptPath)) {
log.info(`Language server binary missing at ${binaryPath}`);
log.info(`Auto-installing via ${scriptPath} β this runs once.`);
try {
execSync(`bash "${scriptPath}"`, {
stdio: 'inherit',
env: { ...process.env, LS_INSTALL_PATH: binaryPath },
});
log.info('Language server binary installed.');
} catch (err) {
log.error(`Auto-install failed: ${err.message}`);
log.error('Run manually: bash install-ls.sh (or set LS_BINARY_PATH to point at an existing binary)');
}
}
}
if (existsSync(binaryPath)) {
resetWorkspace();
// v2.0.85 (#127 123cek): kill any leftover language_server_linux_x64
// processes from prior runs (PM2 SIGKILL / dashboard self-update via
// process.exit() / earlier crash) before we start ours. Otherwise
// they keep their LS pool ports occupied and accumulate over self-
// update cycles. Setting WINDSURFAPI_SKIP_LS_CLEANUP=1 disables
// (e.g. multi-WindsurfAPI on a shared host).
if (process.env.WINDSURFAPI_SKIP_LS_CLEANUP !== '1') {
try {
const r = cleanupOrphanLanguageServers();
if (r.killed > 0) log.info(`LS cleanup: scanned ${r.scanned} candidate(s), killed ${r.killed} orphan(s)`);
} catch (e) { log.warn(`LS cleanup error (non-fatal): ${e.message}`); }
}
await startLanguageServer({
binaryPath,
port: config.lsPort,
apiServerUrl: config.codeiumApiUrl,
});
try {
await waitForReady(30000);
// v2.0.93: if default LS started but proxy-LS crashed, give the
// manage child (port 42101) a moment to restart before syncing models.
log.info('LS ready β fetching model catalog');
} catch (err) {
log.error(`Language server failed to start: ${err.message}`);
log.error('Chat completions will not work without the language server.');
log.error('Run: bash install-ls.sh (now uses Windsurf desktop LS, not stale Exafunction)');
}
} else {
log.warn(`Language server binary not found at ${binaryPath}`);
log.warn('Install it with: download Windsurf Linux tarball and extract language_server_linux_x64');
}
// Init auth pool
await initAuth();
if (!isAuthenticated()) {
log.warn('No accounts configured. Add via:');
log.warn(' POST /auth/login {"token":"..."}');
log.warn(' POST /auth/login {"api_key":"..."}');
}
const server = startServer();
// v2.0.67 (#112) β quiet-window auto-update watcher. No-op until the
// operator flips experimental.autoUpdateQuietWindow on (default off).
// Runs alongside the HTTP server; ticks every minute, polls the request
// ring + cooldown gates, fires runDockerSelfUpdate during real lulls.
try { startQuietWindowAutoUpdate(); } catch (e) { log.warn(`quiet-window: failed to start: ${e.message}`); }
let shuttingDown = false;
const shutdown = (signal) => {
if (shuttingDown) return;
shuttingDown = true;
const inflight = server.getActiveRequests?.() ?? '?';
log.info(`${signal} received β draining ${inflight} in-flight requests (up to 30s)...`);
const abortedSse = abortActiveSse('server shutting down');
if (abortedSse) log.warn(`Aborted ${abortedSse} active SSE stream(s): server shutting down`);
if (typeof server.closeIdleConnections === 'function') server.closeIdleConnections();
server.close(() => {
log.info('HTTP server closed, flushing state + stopping language server');
// Persist any in-memory account updates (capability probes, error
// counts, rate-limit cooldowns) before PM2 restarts us. Debounced
// saves would otherwise be killed by the exit below.
try { saveAccountsSync(); } catch {}
try { stopQuietWindowAutoUpdate(); } catch {}
try { stopLanguageServer(); } catch {}
process.exit(0);
});
setTimeout(() => {
log.warn('Drain timeout, forcing exit');
try { saveAccountsSync(); } catch {}
try { stopQuietWindowAutoUpdate(); } catch {}
try { stopLanguageServer(); } catch {}
process.exit(0);
}, 30_000);
};
process.on('SIGINT', () => shutdown('SIGINT'));
process.on('SIGTERM', () => shutdown('SIGTERM'));
}
main().catch(err => { console.error('Fatal:', err); process.exit(1); });
|