/**
 * File-based local DWN discovery for CLI and native apps.
 *
 * When `electrobun-dwn` (or any local DWN server) starts, it writes a
 * well-known file (`~/.enbox/dwn.json`) containing the DWN endpoint URL
 * and the server PID. CLI tools and native apps read this file to discover
 * the local DWN without port probing.
 *
 * The filesystem operations are abstracted behind {@link DiscoveryFileFs}
 * so the module can be tested without touching the real filesystem, and
 * adapted to runtimes that provide different I/O primitives.
 *
 * @see https://github.com/enboxorg/enbox/issues/587
 * @module
 */
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
import { normalizeBaseUrl } from './local-dwn.js';
// ─── Default Node/Bun filesystem implementation ──────────────────
/**
 * Creates a {@link DiscoveryFileFs} backed by Node.js / Bun built-in
 * modules. Returns `undefined` in environments where `node:fs/promises`
 * or `node:os` are not available (e.g. browsers).
 */
export function createNodeDiscoveryFileFs() {
    try {
        // Dynamic require avoids hard dependency on Node built-ins so bundlers
        // can tree-shake this path away in browser builds.
        const nodeRequire = require;
        const fs = nodeRequire('node:fs/promises');
        const path = nodeRequire('node:path');
        const os = nodeRequire('node:os');
        return {
            readFile(filePath) {
                return __awaiter(this, void 0, void 0, function* () {
                    try {
                        return yield fs.readFile(filePath, 'utf-8');
                    }
                    catch (_a) {
                        return null;
                    }
                });
            },
            writeFile(filePath, contents) {
                return __awaiter(this, void 0, void 0, function* () {
                    yield fs.mkdir(path.dirname(filePath), { recursive: true });
                    // mode 0o600: owner read/write only — the file contains the PID
                    // and endpoint of a local DWN server.
                    yield fs.writeFile(filePath, contents, { encoding: 'utf-8', mode: 0o600 });
                });
            },
            removeFile(filePath) {
                return __awaiter(this, void 0, void 0, function* () {
                    try {
                        yield fs.unlink(filePath);
                    }
                    catch (_a) {
                        // Ignore ENOENT — the file was already gone.
                    }
                });
            },
            isProcessAlive(pid) {
                try {
                    process.kill(pid, 0);
                    return true;
                }
                catch (_a) {
                    return false;
                }
            },
            homedir() {
                return os.homedir();
            },
        };
    }
    catch (_a) {
        return undefined;
    }
}
// ─── Constants ────────────────────────────────────────────────────
/**
 * Directory under the user's home where the discovery file lives.
 * Shared with the `electrobun-dwn` app and other Enbox tooling.
 */
export const DISCOVERY_DIR = '.enbox';
/** Filename of the discovery file. */
export const DISCOVERY_FILENAME = 'dwn.json';
// ─── DwnDiscoveryFile ────────────────────────────────────────────
/**
 * Reads, writes, and validates the `~/.enbox/dwn.json` discovery file.
 *
 * This is the **file-based discovery channel** for CLI and native apps.
 * It is complementary to the `dwn://connect` browser redirect flow.
 *
 * @example Reading the discovery file
 * ```ts
 * const discoveryFile = new DwnDiscoveryFile();
 * const record = await discoveryFile.read();
 *
 * if (record) {
 *   console.log(`Local DWN at ${record.endpoint}`);
 * }
 * ```
 *
 * @example Writing the discovery file (from electrobun-dwn)
 * ```ts
 * const discoveryFile = new DwnDiscoveryFile();
 * await discoveryFile.write({
 *   endpoint : 'http://127.0.0.1:55557',
 *   pid      : process.pid,
 * });
 * ```
 */
export class DwnDiscoveryFile {
    /**
     * @param fs - Filesystem adapter. Defaults to Node/Bun built-ins.
     * @param filePath - Override the discovery file path (mainly for testing).
     * @throws If no filesystem adapter is available (e.g. in a browser).
     */
    constructor(fs, filePath) {
        const resolvedFs = fs !== null && fs !== void 0 ? fs : createNodeDiscoveryFileFs();
        if (!resolvedFs) {
            throw new Error('DwnDiscoveryFile: No filesystem adapter available. ' +
                'Provide a DiscoveryFileFs implementation or run in Node.js / Bun.');
        }
        this._fs = resolvedFs;
        if (filePath) {
            this._filePath = filePath;
        }
        else {
            // Dynamic require so we can resolve the path at construction time.
            const nodeRequire = require;
            const path = nodeRequire('node:path');
            this._filePath = path.join(resolvedFs.homedir(), DISCOVERY_DIR, DISCOVERY_FILENAME);
        }
    }
    /** The absolute path of the discovery file. */
    get path() {
        return this._filePath;
    }
    /**
     * Read and validate the discovery file.
     *
     * Returns the parsed {@link DwnDiscoveryRecord} if:
     * 1. The file exists and contains valid JSON.
     * 2. The `endpoint` is a non-empty string.
     * 3. The `pid` is a positive integer whose process is still alive.
     *
     * Returns `undefined` in all other cases (missing file, parse error,
     * stale PID). Stale files are automatically removed.
     */
    read() {
        return __awaiter(this, void 0, void 0, function* () {
            const raw = yield this._fs.readFile(this._filePath);
            if (raw === null) {
                return undefined;
            }
            let parsed;
            try {
                parsed = JSON.parse(raw);
            }
            catch (_a) {
                // Corrupted file — remove it.
                yield this._fs.removeFile(this._filePath);
                return undefined;
            }
            if (!isValidRecord(parsed)) {
                yield this._fs.removeFile(this._filePath);
                return undefined;
            }
            // Check that the server process is still alive.
            if (!this._fs.isProcessAlive(parsed.pid)) {
                yield this._fs.removeFile(this._filePath);
                return undefined;
            }
            const result = {
                endpoint: normalizeBaseUrl(parsed.endpoint),
                pid: parsed.pid,
            };
            if (parsed.capabilities !== undefined) {
                result.capabilities = parsed.capabilities;
            }
            return result;
        });
    }
    /**
     * Write the discovery file. Creates the `~/.enbox/` directory if needed.
     *
     * @param record - The endpoint and PID to persist.
     */
    write(record) {
        return __awaiter(this, void 0, void 0, function* () {
            const serialized = {
                endpoint: normalizeBaseUrl(record.endpoint),
                pid: record.pid,
            };
            if (record.capabilities !== undefined && record.capabilities.length > 0) {
                serialized.capabilities = record.capabilities;
            }
            const json = JSON.stringify(serialized, null, 2);
            yield this._fs.writeFile(this._filePath, json);
        });
    }
    /**
     * Remove the discovery file. Does not throw if the file is already gone.
     */
    remove() {
        return __awaiter(this, void 0, void 0, function* () {
            yield this._fs.removeFile(this._filePath);
        });
    }
}
// ─── Internal helpers ─────────────────────────────────────────────
/** Type guard for a valid {@link DwnDiscoveryRecord}. */
function isValidRecord(value) {
    if (typeof value !== 'object' || value === null) {
        return false;
    }
    const record = value;
    if (typeof record.endpoint !== 'string' || record.endpoint.length === 0) {
        return false;
    }
    if (typeof record.pid !== 'number' || !Number.isInteger(record.pid) || record.pid <= 0) {
        return false;
    }
    // `capabilities` is optional, but when present must be a string array.
    if (record.capabilities !== undefined) {
        if (!Array.isArray(record.capabilities)) {
            return false;
        }
        if (!record.capabilities.every((item) => typeof item === 'string')) {
            return false;
        }
    }
    return true;
}
//# sourceMappingURL=dwn-discovery-file.js.map