/**
 * OpenCode Server Manager (Singleton)
 *
 * Manages the lifecycle of a single shared OpenCode server process.
 * Multiple sessions share one server; ref counting shuts it down
 * when the last session releases.
 *
 * @module
 */
// ============================================================================
// Server Manager
// ============================================================================
/** Default model for OpenCode provider */
const OPENCODE_DEFAULT_MODEL = 'opencode/minimax-m2.5-free';
/**
 * Manages a single shared OpenCode server instance.
 *
 * - `acquire()` starts or reuses the server, increments ref count
 * - `release()` decrements ref count, stops server at zero
 * - Concurrent acquire() calls are coalesced into a single startup
 */
class OpenCodeServerManager {
    client = null;
    server = null;
    refCount = 0;
    startPromise = null;
    async acquire(config) {
        this.refCount++;
        if (this.client) {
            return this.client;
        }
        // Coalesce concurrent startup requests
        if (this.startPromise) {
            return this.startPromise;
        }
        this.startPromise = this.startServer(config);
        try {
            const client = await this.startPromise;
            return client;
        }
        catch (error) {
            this.refCount--;
            this.startPromise = null;
            throw error;
        }
    }
    release() {
        this.refCount = Math.max(0, this.refCount - 1);
        if (this.refCount === 0) {
            this.shutdown();
        }
    }
    shutdown() {
        if (this.server) {
            this.server.close();
            this.server = null;
        }
        this.client = null;
        this.startPromise = null;
        this.refCount = 0;
    }
    /**
     * List all available models from OpenCode providers.
     * Uses provider.list() to get the full catalog (not just configured providers).
     * Each model ID is formatted as `providerID/modelID`.
     * @returns Flattened array of models with composite IDs
     */
    async listModels(config) {
        const client = await this.acquire(config);
        try {
            const response = await client.provider.list();
            if (!response.data?.all) {
                // Fallback to config.providers() if provider.list() is not available
                return this.listModelsFromConfig(client);
            }
            const models = [];
            for (const provider of response.data.all) {
                if (!provider.models)
                    continue;
                // Handle models as either a Record or an Array
                const entries = Array.isArray(provider.models)
                    ? provider.models.map((m) => [m.id, m])
                    : Object.entries(provider.models);
                for (const [modelKey, model] of entries) {
                    // Format ID as providerID/modelID
                    const id = `${provider.id}/${modelKey}`;
                    models.push({
                        id,
                        displayName: model.name || modelKey,
                        description: undefined,
                        providerName: provider.name || provider.id,
                        ...(id === OPENCODE_DEFAULT_MODEL ? { isDefault: true } : {}),
                    });
                }
            }
            return models;
        }
        finally {
            this.release();
        }
    }
    /**
     * Fallback: list models from config.providers() (only configured providers).
     */
    async listModelsFromConfig(client) {
        const response = await client.config.providers();
        if (!response.data?.providers) {
            return [];
        }
        const models = [];
        for (const provider of response.data.providers) {
            if (!provider.models)
                continue;
            const entries = Array.isArray(provider.models)
                ? provider.models.map((m) => [m.id, m])
                : Object.entries(provider.models);
            for (const [modelKey, model] of entries) {
                const id = `${provider.id}/${modelKey}`;
                models.push({
                    id,
                    displayName: model.name || modelKey,
                    description: undefined,
                    providerName: provider.name || provider.id,
                    ...(id === OPENCODE_DEFAULT_MODEL ? { isDefault: true } : {}),
                });
            }
        }
        return models;
    }
    async startServer(config) {
        const createOpencode = await this.loadSDK();
        const env = {
            ...process.env,
            OPENCODE_PERMISSION: JSON.stringify({ '*': 'allow' }),
            OPENCODE_CLIENT: 'stoneforge',
        };
        if (config?.stoneforgeRoot) {
            env.STONEFORGE_ROOT = config.stoneforgeRoot;
        }
        const result = await createOpencode({
            port: config?.port ?? 0,
            cwd: config?.cwd,
            env,
        });
        // The SDK returns richer types; we extract what we need
        this.client = result.client;
        this.server = result.server;
        this.startPromise = null;
        return this.client;
    }
    async loadSDK() {
        try {
            const sdk = await import('@opencode-ai/sdk');
            return sdk.createOpencode;
        }
        catch {
            throw new Error('OpenCode SDK is not installed. Install it with: npm install @opencode-ai/sdk');
        }
    }
}
/** Singleton instance */
export const serverManager = new OpenCodeServerManager();
//# sourceMappingURL=server-manager.js.map