"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.RegistryClient = exports.DEFAULT_REGISTRY_URL = exports.MODULES_DIRNAME = exports.MANIFEST_FILENAME = void 0;
exports.parsePackageSpec = parsePackageSpec;
const crypto = __importStar(require("crypto"));
const fs = __importStar(require("fs"));
const fsp = __importStar(require("fs/promises"));
const os = __importStar(require("os"));
const path = __importStar(require("path"));
const promises_1 = require("stream/promises");
const axios_1 = __importStar(require("axios"));
const form_data_1 = __importDefault(require("form-data"));
const smol_toml_1 = require("smol-toml");
const tar = __importStar(require("tar"));
exports.MANIFEST_FILENAME = "nexus.toml";
exports.MODULES_DIRNAME = ".modules";
exports.DEFAULT_REGISTRY_URL = "http://localhost:8080";
const NAME_RE = /^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/;
const VERSION_RE = /^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?$/;
/**
 * Registry & package-management client for Nexus workspaces.
 */
class RegistryClient {
    registryUrl;
    timeoutMs;
    constructor(options = {}) {
        this.registryUrl = (options.registryUrl ?? exports.DEFAULT_REGISTRY_URL).replace(/\/+$/, "");
        this.timeoutMs = options.timeoutMs ?? 60_000;
    }
    get baseUrl() {
        return this.registryUrl;
    }
    /** Read and validate `nexus.toml` from a workspace directory. */
    async readManifest(workspaceRoot) {
        const manifestPath = path.join(workspaceRoot, exports.MANIFEST_FILENAME);
        let raw;
        try {
            raw = await fsp.readFile(manifestPath, "utf8");
        }
        catch (err) {
            throw new Error(`unable to read ${exports.MANIFEST_FILENAME}: ${errorMessage(err)}`);
        }
        let parsed;
        try {
            parsed = (0, smol_toml_1.parse)(raw);
        }
        catch (err) {
            throw new Error(`invalid ${exports.MANIFEST_FILENAME}: ${errorMessage(err)}`);
        }
        const manifest = normalizeManifest(parsed);
        validateManifest(manifest);
        return manifest;
    }
    /** Write a manifest to the workspace root. */
    async writeManifest(workspaceRoot, manifest) {
        validateManifest(manifest);
        const payload = {
            name: manifest.name,
            version: manifest.version,
            author: manifest.author,
        };
        if (manifest.description) {
            payload.description = manifest.description;
        }
        payload.dependencies = manifest.dependencies ?? {};
        const content = (0, smol_toml_1.stringify)(payload);
        await fsp.writeFile(path.join(workspaceRoot, exports.MANIFEST_FILENAME), content.endsWith("\n") ? content : `${content}\n`, "utf8");
    }
    /**
     * Compress a workspace directory into a `.tar.gz` archive.
     * Skips `.git`, `.modules`, and the destination archive itself.
     */
    async createArchive(workspaceRoot, destArchivePath) {
        const absDest = path.resolve(destArchivePath);
        await fsp.mkdir(path.dirname(absDest), { recursive: true });
        const skipTopLevel = new Set([".git", ".modules", ".nex", "node_modules"]);
        await tar.c({
            gzip: true,
            file: absDest,
            cwd: workspaceRoot,
            filter: (p) => {
                const normalized = p.replace(/\\/g, "/").replace(/^\.\//, "");
                if (normalized === "." || normalized === "") {
                    return true;
                }
                const top = normalized.split("/")[0] ?? "";
                if (skipTopLevel.has(top)) {
                    return false;
                }
                if (path.resolve(workspaceRoot, normalized) === absDest) {
                    return false;
                }
                return true;
            },
        }, ["."]);
    }
    /** Compute the hex-encoded SHA-256 checksum of a file. */
    async sha256File(filePath) {
        const hash = crypto.createHash("sha256");
        const stream = fs.createReadStream(filePath);
        await (0, promises_1.pipeline)(stream, hash);
        return hash.digest("hex");
    }
    /** Query package metadata from the registry. */
    async getPackage(name, version) {
        if (!name) {
            throw new Error("package name is required");
        }
        const url = new URL(`${this.registryUrl}/api/v1/packages/${encodeURIComponent(name)}`);
        if (version) {
            url.searchParams.set("version", version);
        }
        try {
            const response = await axios_1.default.get(url.toString(), {
                timeout: this.timeoutMs,
                headers: { Accept: "application/json" },
                validateStatus: () => true,
            });
            if (response.status === 404) {
                throw new Error(`package "${name}" not found in registry`);
            }
            if (response.status < 200 || response.status >= 300) {
                throw new Error(`registry returned ${response.status}: ${stringifyBody(response.data)}`);
            }
            const info = response.data;
            if (!info || typeof info !== "object") {
                throw new Error("registry returned an invalid package payload");
            }
            return {
                name: info.name || name,
                version: info.version,
                author: info.author ?? "",
                checksum: info.checksum,
                filename: info.filename,
                url: info.url,
            };
        }
        catch (err) {
            if (err instanceof Error && err.message.startsWith("package ")) {
                throw err;
            }
            throw new Error(`query registry failed: ${errorMessage(err)}`);
        }
    }
    /** Download a package archive to `destPath`. */
    async downloadPackage(name, version, destPath) {
        const url = new URL(`${this.registryUrl}/api/v1/packages/${encodeURIComponent(name)}/download`);
        if (version) {
            url.searchParams.set("version", version);
        }
        await fsp.mkdir(path.dirname(destPath), { recursive: true });
        try {
            const response = await axios_1.default.get(url.toString(), {
                timeout: this.timeoutMs,
                responseType: "stream",
                validateStatus: () => true,
            });
            if (response.status === 404) {
                throw new Error(`package "${name}" not found in registry`);
            }
            if (response.status < 200 || response.status >= 300) {
                throw new Error(`registry returned ${response.status} during download`);
            }
            await (0, promises_1.pipeline)(response.data, fs.createWriteStream(destPath));
        }
        catch (err) {
            if (err instanceof Error && err.message.startsWith("package ")) {
                throw err;
            }
            throw new Error(`download failed: ${errorMessage(err)}`);
        }
    }
    /**
     * Bundle the workspace, checksum it, and multipart-upload to the registry.
     */
    async publish(workspaceRoot) {
        const manifest = await this.readManifest(workspaceRoot);
        const tmpDir = await fsp.mkdtemp(path.join(os.tmpdir(), "nexus-publish-"));
        const archiveName = `${manifest.name}-${manifest.version}.tar.gz`;
        const archivePath = path.join(tmpDir, archiveName);
        try {
            await this.createArchive(workspaceRoot, archivePath);
            const checksum = await this.sha256File(archivePath);
            const form = new form_data_1.default();
            form.append("name", manifest.name);
            form.append("version", manifest.version);
            form.append("author", manifest.author);
            form.append("checksum", checksum);
            form.append("file", fs.createReadStream(archivePath), {
                filename: archiveName,
                contentType: "application/gzip",
            });
            const response = await axios_1.default.post(`${this.registryUrl}/api/v1/packages`, form, {
                timeout: this.timeoutMs,
                headers: {
                    ...form.getHeaders(),
                    Accept: "application/json",
                },
                maxBodyLength: Infinity,
                maxContentLength: Infinity,
                validateStatus: () => true,
            });
            if (response.status < 200 || response.status >= 300) {
                throw new Error(`publish failed (${response.status}): ${stringifyBody(response.data)}`);
            }
            const body = response.data;
            if (typeof body === "string") {
                return {
                    package: {
                        name: manifest.name,
                        version: manifest.version,
                        author: manifest.author,
                        checksum,
                        filename: archiveName,
                    },
                    message: body.trim(),
                };
            }
            return {
                package: {
                    name: body.package?.name ?? manifest.name,
                    version: body.package?.version ?? manifest.version,
                    author: body.package?.author ?? manifest.author,
                    checksum: body.package?.checksum ?? checksum,
                    filename: body.package?.filename ?? archiveName,
                },
                message: body.message,
            };
        }
        finally {
            await fsp.rm(tmpDir, { recursive: true, force: true });
        }
    }
    /**
     * Download a package, verify its checksum, and extract into `.modules/<name>`.
     */
    async install(workspaceRoot, packageSpec) {
        const { name, version: requestedVersion } = parsePackageSpec(packageSpec);
        if (!name) {
            throw new Error("invalid package name");
        }
        const info = await this.getPackage(name, requestedVersion);
        const version = requestedVersion || info.version;
        if (!info.checksum) {
            throw new Error(`registry response for ${name}@${version} is missing checksum`);
        }
        const tmpDir = await fsp.mkdtemp(path.join(os.tmpdir(), "nexus-install-"));
        const archiveName = info.filename || `${name}-${version}.tar.gz`;
        const archivePath = path.join(tmpDir, path.basename(archiveName));
        try {
            await this.downloadPackage(name, version, archivePath);
            const actual = await this.sha256File(archivePath);
            if (actual.toLowerCase() !== info.checksum.toLowerCase()) {
                throw new Error(`checksum mismatch for ${name}@${version}: expected ${info.checksum}, got ${actual}`);
            }
            const destination = path.join(workspaceRoot, exports.MODULES_DIRNAME, name);
            await fsp.rm(destination, { recursive: true, force: true });
            await fsp.mkdir(destination, { recursive: true });
            await tar.x({
                file: archivePath,
                cwd: destination,
                strict: true,
            });
            const manifestPath = path.join(workspaceRoot, exports.MANIFEST_FILENAME);
            try {
                await fsp.access(manifestPath);
                const manifest = await this.readManifest(workspaceRoot);
                manifest.dependencies = {
                    ...(manifest.dependencies ?? {}),
                    [name]: version,
                };
                await this.writeManifest(workspaceRoot, manifest);
            }
            catch {
                // No local manifest — install still succeeds into .modules/
            }
            return { name, version, destination };
        }
        finally {
            await fsp.rm(tmpDir, { recursive: true, force: true });
        }
    }
}
exports.RegistryClient = RegistryClient;
function parsePackageSpec(spec) {
    const trimmed = spec.trim();
    if (!trimmed) {
        return { name: "" };
    }
    const at = trimmed.lastIndexOf("@");
    if (at > 0) {
        return { name: trimmed.slice(0, at), version: trimmed.slice(at + 1) };
    }
    return { name: trimmed };
}
function normalizeManifest(value) {
    if (!value || typeof value !== "object") {
        throw new Error("manifest root must be a table");
    }
    const obj = value;
    const depsRaw = obj.dependencies;
    let dependencies;
    if (depsRaw && typeof depsRaw === "object") {
        dependencies = {};
        for (const [k, v] of Object.entries(depsRaw)) {
            dependencies[k] = String(v);
        }
    }
    return {
        name: String(obj.name ?? ""),
        version: String(obj.version ?? ""),
        author: String(obj.author ?? ""),
        description: obj.description !== undefined ? String(obj.description) : undefined,
        dependencies,
    };
}
function validateManifest(manifest) {
    if (!manifest.name.trim()) {
        throw new Error("manifest field 'name' is required");
    }
    if (!NAME_RE.test(manifest.name)) {
        throw new Error(`invalid package name "${manifest.name}": must start with a letter and contain only letters, digits, '_' or '-'`);
    }
    if (!manifest.version.trim()) {
        throw new Error("manifest field 'version' is required");
    }
    if (!VERSION_RE.test(manifest.version)) {
        throw new Error(`invalid version "${manifest.version}": expected semver like 1.2.3`);
    }
    if (!manifest.author.trim()) {
        throw new Error("manifest field 'author' is required");
    }
    for (const [dep, ver] of Object.entries(manifest.dependencies ?? {})) {
        if (!NAME_RE.test(dep)) {
            throw new Error(`invalid dependency name "${dep}"`);
        }
        if (!String(ver).trim()) {
            throw new Error(`dependency "${dep}" has empty version`);
        }
    }
}
function errorMessage(err) {
    if (err instanceof axios_1.AxiosError) {
        return err.message;
    }
    if (err instanceof Error) {
        return err.message;
    }
    return String(err);
}
function stringifyBody(data) {
    if (typeof data === "string") {
        return data.trim();
    }
    try {
        return JSON.stringify(data);
    }
    catch {
        return String(data);
    }
}
//# sourceMappingURL=client.js.map