2025-12-13 16:34:17 +02:00
|
|
|
import * as vscode from 'vscode';
|
|
|
|
|
import * as os from 'os';
|
2026-01-16 01:22:43 +02:00
|
|
|
import { execSync } from 'child_process';
|
2026-01-09 12:15:09 +02:00
|
|
|
import { createOpencodeServer } from '@opencode-ai/sdk/server';
|
2025-12-13 16:34:17 +02:00
|
|
|
|
2026-01-06 21:31:04 +02:00
|
|
|
const READY_CHECK_TIMEOUT_MS = 30000;
|
2025-12-13 16:34:17 +02:00
|
|
|
|
|
|
|
|
export type ConnectionStatus = 'disconnected' | 'connecting' | 'connected' | 'error';
|
|
|
|
|
|
2025-12-24 22:50:46 +02:00
|
|
|
export type OpenCodeDebugInfo = {
|
|
|
|
|
mode: 'managed' | 'external';
|
|
|
|
|
status: ConnectionStatus;
|
|
|
|
|
lastError?: string;
|
|
|
|
|
workingDirectory: string;
|
|
|
|
|
cliAvailable: boolean;
|
|
|
|
|
cliPath: string | null;
|
|
|
|
|
configuredApiUrl: string | null;
|
|
|
|
|
configuredPort: number | null;
|
|
|
|
|
detectedPort: number | null;
|
|
|
|
|
apiPrefix: string;
|
|
|
|
|
apiPrefixDetected: boolean;
|
|
|
|
|
startCount: number;
|
|
|
|
|
restartCount: number;
|
|
|
|
|
lastStartAt: number | null;
|
|
|
|
|
lastConnectedAt: number | null;
|
|
|
|
|
lastExitCode: number | null;
|
2026-01-09 12:15:09 +02:00
|
|
|
serverUrl: string | null;
|
2026-01-16 14:43:53 +02:00
|
|
|
lastReadyElapsedMs: number | null;
|
|
|
|
|
lastReadyAttempts: number | null;
|
|
|
|
|
lastStartAttempts: number | null;
|
2025-12-24 22:50:46 +02:00
|
|
|
};
|
|
|
|
|
|
2025-12-13 16:34:17 +02:00
|
|
|
export interface OpenCodeManager {
|
|
|
|
|
start(workdir?: string): Promise<void>;
|
|
|
|
|
stop(): Promise<void>;
|
|
|
|
|
restart(): Promise<void>;
|
|
|
|
|
setWorkingDirectory(path: string): Promise<{ success: boolean; restarted: boolean; path: string }>;
|
|
|
|
|
getStatus(): ConnectionStatus;
|
2025-12-24 03:31:07 +02:00
|
|
|
getApiUrl(): string | null;
|
2025-12-13 16:34:17 +02:00
|
|
|
getWorkingDirectory(): string;
|
2026-01-09 18:03:12 +02:00
|
|
|
isCliAvailable(): boolean;
|
2025-12-24 22:50:46 +02:00
|
|
|
getDebugInfo(): OpenCodeDebugInfo;
|
2025-12-13 16:34:17 +02:00
|
|
|
onStatusChange(callback: (status: ConnectionStatus, error?: string) => void): vscode.Disposable;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-09 12:15:09 +02:00
|
|
|
function resolvePortFromUrl(url: string): number | null {
|
2025-12-13 16:34:17 +02:00
|
|
|
try {
|
2026-01-09 12:15:09 +02:00
|
|
|
const parsed = new URL(url);
|
|
|
|
|
return parsed.port ? parseInt(parsed.port, 10) : null;
|
2025-12-13 16:34:17 +02:00
|
|
|
} catch {
|
2025-12-24 03:31:07 +02:00
|
|
|
return null;
|
|
|
|
|
}
|
2025-12-13 16:34:17 +02:00
|
|
|
}
|
|
|
|
|
|
2026-01-16 14:43:53 +02:00
|
|
|
type ReadyResult =
|
|
|
|
|
| { ok: true; baseUrl: string; elapsedMs: number; attempts: number }
|
|
|
|
|
| { ok: false; elapsedMs: number; attempts: number };
|
2026-01-14 15:40:41 +02:00
|
|
|
|
|
|
|
|
function normalizeBaseUrl(value: string): string {
|
|
|
|
|
return value.replace(/\/+$/, '');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getCandidateBaseUrls(serverUrl: string): string[] {
|
|
|
|
|
const normalized = normalizeBaseUrl(serverUrl);
|
|
|
|
|
try {
|
|
|
|
|
const parsed = new URL(normalized);
|
|
|
|
|
const origin = parsed.origin;
|
|
|
|
|
|
|
|
|
|
const candidates: string[] = [];
|
|
|
|
|
const add = (url: string) => {
|
|
|
|
|
const v = normalizeBaseUrl(url);
|
|
|
|
|
if (!candidates.includes(v)) candidates.push(v);
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-16 18:44:11 +02:00
|
|
|
const normalizedPath = parsed.pathname.replace(/\/+$/, '');
|
|
|
|
|
// Prefer plain origin. Only keep SDK url when already root.
|
2026-01-14 15:40:41 +02:00
|
|
|
add(origin);
|
2026-01-16 18:44:11 +02:00
|
|
|
if (normalizedPath === '' || normalizedPath === '/') {
|
|
|
|
|
add(normalized);
|
|
|
|
|
}
|
2026-01-14 15:40:41 +02:00
|
|
|
|
|
|
|
|
return candidates;
|
|
|
|
|
} catch {
|
|
|
|
|
return [normalized];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-17 22:44:09 +02:00
|
|
|
async function waitForReady(serverUrl: string, timeoutMs = 15000): Promise<ReadyResult> {
|
2026-01-09 18:03:12 +02:00
|
|
|
const start = Date.now();
|
2026-01-14 15:40:41 +02:00
|
|
|
const candidates = getCandidateBaseUrls(serverUrl);
|
2026-01-16 14:43:53 +02:00
|
|
|
let attempts = 0;
|
2026-01-14 15:40:41 +02:00
|
|
|
|
2026-01-09 18:03:12 +02:00
|
|
|
while (Date.now() - start < timeoutMs) {
|
2026-01-14 15:40:41 +02:00
|
|
|
for (const baseUrl of candidates) {
|
2026-01-16 14:43:53 +02:00
|
|
|
attempts += 1;
|
2026-01-14 15:40:41 +02:00
|
|
|
try {
|
|
|
|
|
const controller = new AbortController();
|
2026-01-16 18:44:11 +02:00
|
|
|
const timeout = setTimeout(() => controller.abort(), 3000);
|
2026-01-14 15:40:41 +02:00
|
|
|
|
|
|
|
|
// Keep using /config since the UI proxies to it (via /api -> strip prefix).
|
2026-01-15 18:56:14 +02:00
|
|
|
const url = new URL(`${baseUrl}/config`);
|
|
|
|
|
const res = await fetch(url.toString(), {
|
2026-01-14 15:40:41 +02:00
|
|
|
method: 'GET',
|
|
|
|
|
headers: { Accept: 'application/json' },
|
|
|
|
|
signal: controller.signal,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
clearTimeout(timeout);
|
2026-01-16 14:43:53 +02:00
|
|
|
if (res.ok) {
|
|
|
|
|
return { ok: true, baseUrl, elapsedMs: Date.now() - start, attempts };
|
|
|
|
|
}
|
2026-01-14 15:40:41 +02:00
|
|
|
} catch {
|
|
|
|
|
// ignore
|
|
|
|
|
}
|
2026-01-09 18:03:12 +02:00
|
|
|
}
|
2026-01-14 15:40:41 +02:00
|
|
|
|
2026-01-09 18:03:12 +02:00
|
|
|
await new Promise(r => setTimeout(r, 100));
|
|
|
|
|
}
|
2026-01-14 15:40:41 +02:00
|
|
|
|
2026-01-16 14:43:53 +02:00
|
|
|
return { ok: false, elapsedMs: Date.now() - start, attempts };
|
2026-01-09 18:03:12 +02:00
|
|
|
}
|
|
|
|
|
|
2025-12-24 03:31:07 +02:00
|
|
|
export function createOpenCodeManager(_context: vscode.ExtensionContext): OpenCodeManager {
|
2026-01-09 12:15:09 +02:00
|
|
|
// Discard unused parameter - reserved for future use (state persistence, subscriptions)
|
|
|
|
|
void _context;
|
|
|
|
|
let server: { url: string; close: () => void } | null = null;
|
2026-01-14 15:40:41 +02:00
|
|
|
let managedApiUrlOverride: string | null = null;
|
2025-12-13 16:34:17 +02:00
|
|
|
let status: ConnectionStatus = 'disconnected';
|
|
|
|
|
let lastError: string | undefined;
|
|
|
|
|
const listeners = new Set<(status: ConnectionStatus, error?: string) => void>();
|
2026-01-16 14:43:53 +02:00
|
|
|
const workspaceDirectory = (): string =>
|
|
|
|
|
vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || os.homedir();
|
|
|
|
|
let workingDirectory: string = workspaceDirectory();
|
2025-12-24 22:50:46 +02:00
|
|
|
let startCount = 0;
|
|
|
|
|
let restartCount = 0;
|
|
|
|
|
let lastStartAt: number | null = null;
|
|
|
|
|
let lastConnectedAt: number | null = null;
|
|
|
|
|
let lastExitCode: number | null = null;
|
2026-01-16 14:43:53 +02:00
|
|
|
let lastReadyElapsedMs: number | null = null;
|
|
|
|
|
let lastReadyAttempts: number | null = null;
|
|
|
|
|
let lastStartAttempts: number | null = null;
|
2025-12-26 02:29:00 +02:00
|
|
|
|
2025-12-24 03:31:07 +02:00
|
|
|
let detectedPort: number | null = null;
|
2026-01-09 18:03:12 +02:00
|
|
|
let cliMissing = false;
|
2025-12-26 02:29:00 +02:00
|
|
|
|
2026-01-16 01:22:43 +02:00
|
|
|
let pendingOperation: Promise<void> | null = null;
|
|
|
|
|
|
2025-12-13 16:34:17 +02:00
|
|
|
const config = vscode.workspace.getConfiguration('openchamber');
|
|
|
|
|
const configuredApiUrl = config.get<string>('apiUrl') || '';
|
2025-12-24 03:31:07 +02:00
|
|
|
const useConfiguredUrl = configuredApiUrl && configuredApiUrl.trim().length > 0;
|
2025-12-26 02:29:00 +02:00
|
|
|
|
2025-12-24 03:31:07 +02:00
|
|
|
let configuredPort: number | null = null;
|
|
|
|
|
if (useConfiguredUrl) {
|
2025-12-13 16:34:17 +02:00
|
|
|
try {
|
2025-12-24 03:31:07 +02:00
|
|
|
const parsed = new URL(configuredApiUrl);
|
|
|
|
|
if (parsed.port) {
|
|
|
|
|
configuredPort = parseInt(parsed.port, 10);
|
|
|
|
|
}
|
2025-12-13 16:34:17 +02:00
|
|
|
} catch {
|
2026-01-09 12:15:09 +02:00
|
|
|
// Invalid URL
|
2025-12-13 16:34:17 +02:00
|
|
|
}
|
2025-12-24 03:31:07 +02:00
|
|
|
}
|
2025-12-13 16:34:17 +02:00
|
|
|
|
2026-01-09 12:15:09 +02:00
|
|
|
const setStatus = (newStatus: ConnectionStatus, error?: string) => {
|
2025-12-13 16:34:17 +02:00
|
|
|
if (status !== newStatus || lastError !== error) {
|
|
|
|
|
status = newStatus;
|
|
|
|
|
lastError = error;
|
2025-12-24 22:50:46 +02:00
|
|
|
if (newStatus === 'connected') {
|
|
|
|
|
lastConnectedAt = Date.now();
|
|
|
|
|
}
|
2025-12-13 16:34:17 +02:00
|
|
|
listeners.forEach(cb => cb(status, error));
|
|
|
|
|
}
|
2026-01-09 12:15:09 +02:00
|
|
|
};
|
2025-12-24 03:31:07 +02:00
|
|
|
|
2026-01-09 12:15:09 +02:00
|
|
|
const getApiUrl = (): string | null => {
|
2025-12-24 03:31:07 +02:00
|
|
|
if (useConfiguredUrl && configuredApiUrl) {
|
|
|
|
|
return configuredApiUrl.replace(/\/+$/, '');
|
|
|
|
|
}
|
2026-01-14 15:40:41 +02:00
|
|
|
if (managedApiUrlOverride) {
|
|
|
|
|
return managedApiUrlOverride.replace(/\/+$/, '');
|
|
|
|
|
}
|
2026-01-09 12:15:09 +02:00
|
|
|
if (server?.url) {
|
|
|
|
|
return server.url.replace(/\/+$/, '');
|
2025-12-24 03:31:07 +02:00
|
|
|
}
|
2026-01-09 12:15:09 +02:00
|
|
|
if (detectedPort) {
|
2026-01-16 14:43:53 +02:00
|
|
|
return `http://127.0.0.1:${detectedPort}`;
|
2025-12-13 16:34:17 +02:00
|
|
|
}
|
2026-01-09 12:15:09 +02:00
|
|
|
return null;
|
|
|
|
|
};
|
2025-12-13 16:34:17 +02:00
|
|
|
|
2026-01-16 01:22:43 +02:00
|
|
|
async function startInternal(workdir?: string): Promise<void> {
|
2025-12-24 22:50:46 +02:00
|
|
|
startCount += 1;
|
2026-01-16 14:43:53 +02:00
|
|
|
setStatus('connecting');
|
2025-12-24 22:50:46 +02:00
|
|
|
lastStartAt = Date.now();
|
2026-01-16 14:43:53 +02:00
|
|
|
lastStartAttempts = startCount;
|
2025-12-24 22:50:46 +02:00
|
|
|
|
2025-12-13 16:34:17 +02:00
|
|
|
if (typeof workdir === 'string' && workdir.trim().length > 0) {
|
|
|
|
|
workingDirectory = workdir.trim();
|
2026-01-16 14:43:53 +02:00
|
|
|
} else {
|
|
|
|
|
workingDirectory = workspaceDirectory();
|
2025-12-13 16:34:17 +02:00
|
|
|
}
|
|
|
|
|
|
2025-12-24 03:31:07 +02:00
|
|
|
if (useConfiguredUrl && configuredApiUrl) {
|
|
|
|
|
setStatus('connecting');
|
2025-12-13 16:34:17 +02:00
|
|
|
setStatus('connected');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 01:22:43 +02:00
|
|
|
// If server already running, don't spawn another
|
|
|
|
|
if (server) {
|
|
|
|
|
if (status !== 'connected') {
|
|
|
|
|
setStatus('connected');
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-24 03:31:07 +02:00
|
|
|
setStatus('connecting');
|
2026-01-16 01:22:43 +02:00
|
|
|
cliMissing = false;
|
2025-12-24 03:31:07 +02:00
|
|
|
|
|
|
|
|
detectedPort = null;
|
2025-12-24 22:50:46 +02:00
|
|
|
lastExitCode = null;
|
2026-01-14 15:40:41 +02:00
|
|
|
managedApiUrlOverride = null;
|
|
|
|
|
|
2025-12-13 16:34:17 +02:00
|
|
|
try {
|
2026-01-15 18:56:14 +02:00
|
|
|
// SDK spawns `opencode serve` in current process cwd.
|
|
|
|
|
// Some OpenCode endpoints behave differently based on server process cwd,
|
|
|
|
|
// so ensure we start it from the workspace directory.
|
|
|
|
|
const originalCwd = process.cwd();
|
|
|
|
|
try {
|
|
|
|
|
process.chdir(workingDirectory);
|
|
|
|
|
server = await createOpencodeServer({
|
|
|
|
|
hostname: '127.0.0.1',
|
|
|
|
|
port: 0,
|
|
|
|
|
timeout: READY_CHECK_TIMEOUT_MS,
|
|
|
|
|
signal: undefined,
|
|
|
|
|
});
|
|
|
|
|
} finally {
|
|
|
|
|
try {
|
|
|
|
|
process.chdir(originalCwd);
|
|
|
|
|
} catch {
|
|
|
|
|
// ignore
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-09 12:15:09 +02:00
|
|
|
if (server && server.url) {
|
2026-01-15 18:56:14 +02:00
|
|
|
// Validate readiness for the current workspace context.
|
2026-01-17 22:44:09 +02:00
|
|
|
const ready = await waitForReady(server.url, 10000);
|
2026-01-16 14:43:53 +02:00
|
|
|
lastReadyElapsedMs = ready.elapsedMs;
|
|
|
|
|
lastReadyAttempts = ready.attempts;
|
2026-01-14 15:40:41 +02:00
|
|
|
if (ready.ok) {
|
|
|
|
|
managedApiUrlOverride = ready.baseUrl;
|
|
|
|
|
detectedPort = resolvePortFromUrl(ready.baseUrl);
|
2026-01-09 18:03:12 +02:00
|
|
|
setStatus('connected');
|
|
|
|
|
} else {
|
|
|
|
|
try {
|
|
|
|
|
server.close();
|
|
|
|
|
} catch {
|
|
|
|
|
// ignore
|
|
|
|
|
}
|
|
|
|
|
server = null;
|
|
|
|
|
throw new Error('Server started but health check failed');
|
|
|
|
|
}
|
2025-12-13 16:34:17 +02:00
|
|
|
} else {
|
2026-01-09 12:15:09 +02:00
|
|
|
throw new Error('Server started but URL is missing');
|
2025-12-13 16:34:17 +02:00
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
const message = err instanceof Error ? err.message : String(err);
|
2026-01-09 12:15:09 +02:00
|
|
|
|
|
|
|
|
// Check for ENOENT or generic spawn failure which implies CLI missing
|
|
|
|
|
if (message.includes('ENOENT') || message.includes('spawn opencode')) {
|
2026-01-09 18:03:12 +02:00
|
|
|
cliMissing = true;
|
2026-01-09 12:15:09 +02:00
|
|
|
setStatus('error', 'OpenCode CLI not found. Install it or ensure it\'s in PATH.');
|
|
|
|
|
vscode.window.showErrorMessage(
|
|
|
|
|
'OpenCode CLI not found. Please install it or ensure it\'s in PATH.',
|
|
|
|
|
'More Info'
|
|
|
|
|
).then(selection => {
|
|
|
|
|
if (selection === 'More Info') {
|
|
|
|
|
vscode.env.openExternal(vscode.Uri.parse('https://github.com/opencode-ai/opencode'));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
setStatus('error', `Failed to start OpenCode: ${message}`);
|
|
|
|
|
}
|
2025-12-13 16:34:17 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 01:22:43 +02:00
|
|
|
async function stopInternal(): Promise<void> {
|
|
|
|
|
const portToKill = detectedPort;
|
|
|
|
|
|
2026-01-09 12:15:09 +02:00
|
|
|
if (server) {
|
2025-12-13 16:34:17 +02:00
|
|
|
try {
|
2026-01-09 12:15:09 +02:00
|
|
|
server.close();
|
2025-12-13 16:34:17 +02:00
|
|
|
} catch {
|
2026-01-09 12:15:09 +02:00
|
|
|
// Ignore close errors
|
2025-12-13 16:34:17 +02:00
|
|
|
}
|
2026-01-09 12:15:09 +02:00
|
|
|
server = null;
|
2025-12-13 16:34:17 +02:00
|
|
|
}
|
2026-01-15 18:56:14 +02:00
|
|
|
|
2026-01-16 01:22:43 +02:00
|
|
|
// SDK's proc.kill() only kills the Node wrapper, not the actual opencode binary.
|
|
|
|
|
// Kill any process listening on our port to clean up orphaned children.
|
|
|
|
|
if (portToKill) {
|
|
|
|
|
try {
|
2026-01-16 02:38:09 +02:00
|
|
|
const lsofOutput = execSync(`lsof -ti:${portToKill} 2>/dev/null || true`, {
|
|
|
|
|
encoding: 'utf8',
|
2026-01-16 01:22:43 +02:00
|
|
|
timeout: 5000
|
|
|
|
|
});
|
2026-01-16 02:38:09 +02:00
|
|
|
const myPid = process.pid;
|
|
|
|
|
for (const pidStr of lsofOutput.split(/\s+/)) {
|
|
|
|
|
const pid = parseInt(pidStr.trim(), 10);
|
|
|
|
|
if (pid && pid !== myPid) {
|
|
|
|
|
try {
|
|
|
|
|
execSync(`kill -9 ${pid} 2>/dev/null || true`, { stdio: 'ignore', timeout: 2000 });
|
|
|
|
|
} catch {
|
|
|
|
|
// Ignore
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-16 01:22:43 +02:00
|
|
|
} catch {
|
|
|
|
|
// Ignore - process may already be dead
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-15 18:56:14 +02:00
|
|
|
|
2026-01-14 15:40:41 +02:00
|
|
|
managedApiUrlOverride = null;
|
2025-12-24 03:31:07 +02:00
|
|
|
detectedPort = null;
|
2025-12-13 16:34:17 +02:00
|
|
|
setStatus('disconnected');
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 01:22:43 +02:00
|
|
|
async function restartInternal(): Promise<void> {
|
2025-12-24 22:50:46 +02:00
|
|
|
restartCount += 1;
|
2026-01-16 01:22:43 +02:00
|
|
|
await stopInternal();
|
2025-12-24 03:31:07 +02:00
|
|
|
await new Promise(r => setTimeout(r, 250));
|
2026-01-16 01:22:43 +02:00
|
|
|
await startInternal();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function start(workdir?: string): Promise<void> {
|
|
|
|
|
if (pendingOperation) {
|
|
|
|
|
await pendingOperation;
|
|
|
|
|
if (server) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-16 14:43:53 +02:00
|
|
|
lastStartAttempts = 1;
|
2026-01-16 01:22:43 +02:00
|
|
|
pendingOperation = startInternal(workdir);
|
|
|
|
|
try {
|
|
|
|
|
await pendingOperation;
|
|
|
|
|
} finally {
|
|
|
|
|
pendingOperation = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function stop(): Promise<void> {
|
|
|
|
|
if (pendingOperation) {
|
|
|
|
|
await pendingOperation;
|
|
|
|
|
}
|
|
|
|
|
// Check if already stopped
|
|
|
|
|
if (!server) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
pendingOperation = stopInternal();
|
|
|
|
|
try {
|
|
|
|
|
await pendingOperation;
|
|
|
|
|
} finally {
|
|
|
|
|
pendingOperation = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function restart(): Promise<void> {
|
|
|
|
|
if (pendingOperation) {
|
|
|
|
|
await pendingOperation;
|
|
|
|
|
}
|
2026-01-16 14:43:53 +02:00
|
|
|
lastStartAttempts = 1;
|
2026-01-16 01:22:43 +02:00
|
|
|
pendingOperation = restartInternal();
|
|
|
|
|
try {
|
|
|
|
|
await pendingOperation;
|
|
|
|
|
} finally {
|
|
|
|
|
pendingOperation = null;
|
|
|
|
|
}
|
2025-12-13 16:34:17 +02:00
|
|
|
}
|
|
|
|
|
|
2025-12-24 03:31:07 +02:00
|
|
|
async function setWorkingDirectory(newPath: string): Promise<{ success: boolean; restarted: boolean; path: string }> {
|
2026-01-17 22:44:09 +02:00
|
|
|
void newPath;
|
2026-01-16 14:43:53 +02:00
|
|
|
const workspacePath = workspaceDirectory();
|
|
|
|
|
const nextDirectory = workspacePath;
|
|
|
|
|
|
|
|
|
|
if (workingDirectory === nextDirectory) {
|
|
|
|
|
return { success: true, restarted: false, path: nextDirectory };
|
2025-12-24 22:50:46 +02:00
|
|
|
}
|
2026-01-06 21:31:04 +02:00
|
|
|
|
2026-01-16 14:43:53 +02:00
|
|
|
workingDirectory = nextDirectory;
|
2025-12-24 22:50:46 +02:00
|
|
|
|
|
|
|
|
if (useConfiguredUrl && configuredApiUrl) {
|
2026-01-16 14:43:53 +02:00
|
|
|
return { success: true, restarted: false, path: nextDirectory };
|
2025-12-24 22:50:46 +02:00
|
|
|
}
|
|
|
|
|
|
2026-01-16 14:43:53 +02:00
|
|
|
return { success: true, restarted: false, path: nextDirectory };
|
2025-12-13 16:34:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
start,
|
|
|
|
|
stop,
|
|
|
|
|
restart,
|
|
|
|
|
setWorkingDirectory,
|
|
|
|
|
getStatus: () => status,
|
2025-12-24 03:31:07 +02:00
|
|
|
getApiUrl,
|
2025-12-13 16:34:17 +02:00
|
|
|
getWorkingDirectory: () => workingDirectory,
|
2026-01-09 18:03:12 +02:00
|
|
|
isCliAvailable: () => !cliMissing,
|
2025-12-24 22:50:46 +02:00
|
|
|
getDebugInfo: () => ({
|
|
|
|
|
mode: useConfiguredUrl && configuredApiUrl ? 'external' : 'managed',
|
|
|
|
|
status,
|
|
|
|
|
lastError,
|
|
|
|
|
workingDirectory,
|
2026-01-09 18:03:12 +02:00
|
|
|
cliAvailable: !cliMissing,
|
2026-01-09 12:15:09 +02:00
|
|
|
cliPath: null,
|
2025-12-24 22:50:46 +02:00
|
|
|
configuredApiUrl: useConfiguredUrl && configuredApiUrl ? configuredApiUrl.replace(/\/+$/, '') : null,
|
|
|
|
|
configuredPort,
|
|
|
|
|
detectedPort,
|
2026-01-16 14:43:53 +02:00
|
|
|
apiPrefix: '',
|
|
|
|
|
apiPrefixDetected: true,
|
2025-12-24 22:50:46 +02:00
|
|
|
startCount,
|
|
|
|
|
restartCount,
|
|
|
|
|
lastStartAt,
|
|
|
|
|
lastConnectedAt,
|
|
|
|
|
lastExitCode,
|
2026-01-09 12:15:09 +02:00
|
|
|
serverUrl: getApiUrl(),
|
2026-01-16 14:43:53 +02:00
|
|
|
lastReadyElapsedMs,
|
|
|
|
|
lastReadyAttempts,
|
|
|
|
|
lastStartAttempts,
|
2025-12-24 22:50:46 +02:00
|
|
|
}),
|
2025-12-13 16:34:17 +02:00
|
|
|
onStatusChange(callback) {
|
|
|
|
|
listeners.add(callback);
|
|
|
|
|
callback(status, lastError);
|
|
|
|
|
return new vscode.Disposable(() => listeners.delete(callback));
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|