import { createLogger } from "../utils/logger/index.js";
import { init_logger } from "../utils/logger.js";
import { init_paths, resolveBundledExtensionsDir, resolveExtensionsDir as resolveExtensionsDir$1 } from "../config/paths.js";
import { MAX_EXTENSION_STORE_ZIP_BYTES } from "./store-zip-limits.js";
import { dirname, isAbsolute, join, resolve, sep } from "path";
import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "fs";
import { execSync } from "child_process";
import { tmpdir } from "os";
import AdmZip from "adm-zip";
//#region src/extensions/install.ts
/**
* Extension Installation Module
* Supports installing from npm packages, local directories, and xopc-store zips
* Supports three-tier storage: workspace, global, bundled
*/
init_paths();
init_logger();
const log = createLogger("ExtensionInstall");
const NPM_INSTALL_ENV = process.env.XOPC_EXTENSION_NPM_INSTALL?.trim().toLowerCase();
function extractExecSyncOutput(error) {
	if (!error || typeof error !== "object") return "";
	const o = error;
	const read = (x) => {
		if (x == null) return "";
		return Buffer.isBuffer(x) ? x.toString("utf8") : x;
	};
	const stderr = read(o.stderr).trim();
	const stdout = read(o.stdout).trim();
	if (stderr) return stderr;
	if (stdout) return stdout;
	return typeof o.message === "string" ? o.message.trim() : "";
}
function truncateOutput(text, max = 4500) {
	const t = text.trim();
	if (t.length <= max) return t;
	return `…${t.slice(-max)}`;
}
/**
* Install production `node_modules` for an unpacked extension (package.json dependencies).
* Uses pnpm when `pnpm-lock.yaml` is present, otherwise npm. Set `XOPC_EXTENSION_NPM_INSTALL=skip`
* to skip (extensions must ship runnable code or you install deps manually).
*/
function installExtensionProdDependencies(extensionDir) {
	if (NPM_INSTALL_ENV === "skip" || NPM_INSTALL_ENV === "0" || NPM_INSTALL_ENV === "false") {
		log.warn({
			extensionDir,
			XOPC_EXTENSION_NPM_INSTALL: process.env.XOPC_EXTENSION_NPM_INSTALL
		}, "Skipping extension dependency install (XOPC_EXTENSION_NPM_INSTALL)");
		return { ok: true };
	}
	const execOpts = {
		cwd: extensionDir,
		timeout: 3e5,
		encoding: "utf-8",
		stdio: [
			"ignore",
			"pipe",
			"pipe"
		],
		maxBuffer: 20 * 1024 * 1024,
		env: process.env,
		...process.platform === "win32" ? { shell: process.env.ComSpec && process.env.ComSpec.trim() || "cmd.exe" } : {}
	};
	const run = (label, command) => {
		try {
			execSync(command, execOpts);
			return { ok: true };
		} catch (err) {
			const out = truncateOutput(extractExecSyncOutput(err));
			return {
				ok: false,
				error: `${label} install failed.${label === "npm" ? " Check that Node.js/npm is on PATH, the registry is reachable, and peer/engine rules allow install." : " Check that pnpm is on PATH or remove pnpm-lock.yaml to use npm instead."}${out ? `\n\n${out}` : ""}`
			};
		}
	};
	if (existsSync(join(extensionDir, "pnpm-lock.yaml"))) {
		const pnpm = run("pnpm", "pnpm install --prod --no-frozen-lockfile");
		if (pnpm.ok) return pnpm;
		const npmFallback = run("npm", "npm install --omit=dev --no-audit --no-fund");
		if (npmFallback.ok) {
			log.warn({ extensionDir }, "pnpm failed; extension dependencies installed with npm instead");
			return { ok: true };
		}
		return npmFallback;
	}
	return run("npm", "npm install --omit=dev --no-audit --no-fund");
}
function isSafeZipPath(name) {
	if (!name) return false;
	const normalized = name.replace(/\\/g, "/");
	if (normalized.includes("..")) return false;
	if (normalized.startsWith("/") || /^\w:/.test(normalized)) return false;
	for (const p of normalized.split("/")) if (p === "..") return false;
	return true;
}
function isIgnorableZipEntry(name) {
	const n = name.replace(/\\/g, "/");
	if (n.startsWith("__MACOSX/")) return true;
	const segments = n.split("/").filter(Boolean);
	for (const s of segments) {
		if (s === ".DS_Store" || s === "Thumbs.db" || s === "desktop.ini") return true;
		if (s.startsWith("._")) return true;
	}
	return false;
}
function inferExtensionStripPrefix(primaryManifestPath) {
	const norm = primaryManifestPath.replace(/\\/g, "/");
	const lower = norm.toLowerCase();
	const suff = "xopc.extension.json";
	if (lower === suff) return "";
	if (!lower.endsWith(suff)) return "";
	return norm.slice(0, norm.length - 19);
}
/** Read `id` from the shallowest xopc.extension.json in a store zip (for --force / preflight). */
function peekExtensionIdFromStoreZip(buffer) {
	if (buffer.length > 10485760) return void 0;
	let zip;
	try {
		zip = new AdmZip(buffer);
	} catch {
		return;
	}
	const safeEntries = zip.getEntries().filter((e) => !e.isDirectory && e.entryName && !isIgnorableZipEntry(e.entryName)).filter((e) => isSafeZipPath(e.entryName));
	const shallow = safeEntries.map((e) => e.entryName.replace(/\\/g, "/")).filter((n) => /(^|\/)xopc\.extension\.json$/i.test(n)).filter((n) => n.split("/").filter(Boolean).length <= 2);
	if (shallow.length === 0) return void 0;
	shallow.sort((a, b) => a.length - b.length);
	const path = shallow[0];
	const entry = safeEntries.find((e) => e.entryName.replace(/\\/g, "/") === path);
	if (!entry) return void 0;
	try {
		const raw = entry.getData().toString("utf8");
		const m = JSON.parse(raw);
		const id = typeof m.id === "string" ? m.id.trim() : "";
		if (!id || id.includes("/") || id.includes("\\")) return void 0;
		return id;
	} catch {
		return;
	}
}
/**
* Install an extension from an xopc-store zip buffer (layout: manifest at archive root
* or under a single top-level folder).
*/
async function installExtensionFromStoreZip(buffer, extensionsDir) {
	if (buffer.length > 10485760) return {
		ok: false,
		error: `Extension zip exceeds maximum size (${MAX_EXTENSION_STORE_ZIP_BYTES} bytes)`
	};
	const safeEntries = new AdmZip(buffer).getEntries().filter((e) => !e.isDirectory && e.entryName && !isIgnorableZipEntry(e.entryName)).filter((e) => isSafeZipPath(e.entryName));
	if (safeEntries.length === 0) return {
		ok: false,
		error: "Zip is empty or invalid"
	};
	const names = safeEntries.map((e) => e.entryName.replace(/\\/g, "/"));
	const manifestPaths = names.filter((n) => /(^|\/)xopc\.extension\.json$/i.test(n));
	if (manifestPaths.length === 0) return {
		ok: false,
		error: "Zip must contain xopc.extension.json"
	};
	const shallow = manifestPaths.filter((n) => n.split("/").filter(Boolean).length <= 2);
	if (shallow.length === 0) return {
		ok: false,
		error: "xopc.extension.json is nested too deeply; use a zip with manifest at archive root or one folder (e.g. my-ext/xopc.extension.json)"
	};
	shallow.sort((a, b) => a.length - b.length);
	const stripPrefix = inferExtensionStripPrefix(shallow[0]);
	if (stripPrefix) {
		const prefixNorm = stripPrefix.replace(/\\/g, "/");
		const outside = names.filter((n) => !n.startsWith(prefixNorm) && !isIgnorableZipEntry(n));
		if (outside.length > 0) return {
			ok: false,
			error: `Invalid zip: expected all files under "${prefixNorm.replace(/\/$/, "")}/", but found "${outside[0]}".`
		};
	}
	const tmpRoot = mkdtempSync(join(tmpdir(), "xopc-ext-zip-"));
	const destResolved = resolve(tmpRoot);
	try {
		for (const e of safeEntries) {
			const norm = e.entryName.replace(/\\/g, "/");
			let rel;
			if (stripPrefix) {
				const prefixNorm = stripPrefix.replace(/\\/g, "/");
				if (!norm.startsWith(prefixNorm)) return {
					ok: false,
					error: `Zip entry outside extension prefix "${prefixNorm.replace(/\/$/, "")}": ${norm}`
				};
				rel = norm.slice(prefixNorm.length).replace(/^\//, "");
			} else rel = norm;
			if (!rel || rel.includes("..")) return {
				ok: false,
				error: `Refusing unsafe zip entry path: ${e.entryName}`
			};
			const targetPath = join(tmpRoot, rel);
			const resolvedTarget = resolve(targetPath);
			if (!resolvedTarget.startsWith(destResolved + sep) && resolvedTarget !== destResolved) return {
				ok: false,
				error: `Refusing unsafe zip path (possible traversal): ${e.entryName}`
			};
			mkdirSync(dirname(targetPath), { recursive: true });
			writeFileSync(targetPath, e.getData());
		}
		if (!existsSync(join(tmpRoot, "xopc.extension.json"))) return {
			ok: false,
			error: "Extracted content is missing xopc.extension.json"
		};
		return await installFromDirectory(tmpRoot, extensionsDir);
	} finally {
		try {
			rmSync(tmpRoot, {
				recursive: true,
				force: true
			});
		} catch {}
	}
}
/**
* Resolve target extensions directory based on options
*/
function resolveExtensionsDir(workspaceDir, global = false) {
	if (global) {
		const globalDir = resolveExtensionsDir$1();
		mkdirSync(globalDir, { recursive: true });
		return globalDir;
	}
	return join(workspaceDir, ".extensions");
}
/**
* Install extension from npm package
*/
async function installFromNpm(packageSpec, extensionsDir, timeoutMs = 12e4) {
	const tmpDir = join(tmpdir(), `xopc-install-${Date.now()}`);
	try {
		console.log(`📦 Downloading ${packageSpec} from npm...`);
		mkdirSync(tmpDir, { recursive: true });
		const packedFile = execSync(`npm pack ${packageSpec} --pack-destination ${tmpDir}`, {
			timeout: timeoutMs,
			encoding: "utf-8",
			stdio: "pipe"
		}).trim().split("\n").pop()?.trim();
		if (!packedFile) return {
			ok: false,
			error: "Failed to download package from npm"
		};
		const archivePath = join(tmpDir, packedFile);
		console.log(`📂 Extracting ${packedFile}...`);
		execSync(`tar -xzf ${archivePath} -C ${tmpDir}`, {
			timeout: 3e4,
			stdio: "pipe"
		});
		return await installFromDirectory(join(tmpDir, "package"), extensionsDir);
	} catch (error) {
		return {
			ok: false,
			error: `Failed to install from npm: ${error instanceof Error ? error.message : String(error)}`
		};
	} finally {
		try {
			rmSync(tmpDir, {
				recursive: true,
				force: true
			});
		} catch {}
	}
}
/**
* Install extension from local directory
*/
async function installFromLocal(localPath, extensionsDir) {
	const sourceDir = isAbsolute(localPath) ? localPath : resolve(process.cwd(), localPath);
	if (!existsSync(sourceDir)) return {
		ok: false,
		error: `Directory not found: ${sourceDir}`
	};
	console.log(`📂 Installing from local directory: ${sourceDir}...`);
	return await installFromDirectory(sourceDir, extensionsDir);
}
/**
* Install extension from extracted directory
*/
async function installFromDirectory(sourceDir, extensionsDir) {
	const manifestPath = join(sourceDir, "xopc.extension.json");
	const packagePath = join(sourceDir, "package.json");
	let manifest = null;
	let packageJson = null;
	if (existsSync(manifestPath)) try {
		manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
	} catch {
		return {
			ok: false,
			error: "Invalid xopc.extension.json manifest file"
		};
	}
	if (existsSync(packagePath)) try {
		packageJson = JSON.parse(readFileSync(packagePath, "utf-8"));
	} catch {}
	const extensionId = manifest?.id || packageJson?.name;
	if (!extensionId) return {
		ok: false,
		error: "Extension must have an id in xopc.extension.json or name in package.json"
	};
	if (extensionId.includes("/") || extensionId.includes("\\")) return {
		ok: false,
		error: "Extension ID cannot contain path separators"
	};
	const targetDir = join(extensionsDir, extensionId);
	if (existsSync(targetDir)) return {
		ok: false,
		error: `Extension already exists at ${targetDir}. Use update instead.`
	};
	const mainFile = manifest?.main || "index.js";
	if (!existsSync(join(sourceDir, mainFile))) return {
		ok: false,
		error: `Main entry not found: ${mainFile}`
	};
	console.log(`📋 Extension: ${manifest?.name || extensionId} (${extensionId})`);
	if (manifest?.version || packageJson?.version) console.log(`🔖 Version: ${manifest?.version || packageJson?.version}`);
	mkdirSync(targetDir, { recursive: true });
	console.log(`📂 Copying files to ${targetDir}...`);
	cpSync(sourceDir, targetDir, {
		recursive: true,
		force: true
	});
	if (packageJson?.dependencies && Object.keys(packageJson.dependencies).length > 0) {
		console.log(`📦 Installing dependencies...`);
		const depResult = installExtensionProdDependencies(targetDir);
		if (depResult.ok === false) {
			rmSync(targetDir, {
				recursive: true,
				force: true
			});
			return {
				ok: false,
				error: depResult.error
			};
		}
	}
	const origin = extensionsDir.includes(".xopc/extensions") ? "global" : "workspace";
	console.log(`✅ Extension ${extensionId} installed successfully!`);
	console.log(`\nTo enable the extension, add to your config:`);
	console.log(`  extensions:`);
	console.log(`    enabled: [${extensionId}]`);
	console.log(`    ${extensionId}:`);
	console.log(`      # your extension options here\n`);
	return {
		ok: true,
		extensionId,
		targetDir,
		origin
	};
}
/**
* Remove installed extension from all tiers
*/
function removeExtension(extensionId, workspaceDir) {
	const workspaceExtension = join(join(workspaceDir, ".extensions"), extensionId);
	if (existsSync(workspaceExtension)) try {
		rmSync(workspaceExtension, {
			recursive: true,
			force: true
		});
		return {
			ok: true,
			removedFrom: "workspace"
		};
	} catch (error) {
		return {
			ok: false,
			error: `Failed to remove from workspace: ${error instanceof Error ? error.message : String(error)}`
		};
	}
	const globalExtension = join(resolveExtensionsDir$1(), extensionId);
	if (existsSync(globalExtension)) try {
		rmSync(globalExtension, {
			recursive: true,
			force: true
		});
		return {
			ok: true,
			removedFrom: "global"
		};
	} catch (error) {
		return {
			ok: false,
			error: `Failed to remove from global: ${error instanceof Error ? error.message : String(error)}`
		};
	}
	return {
		ok: false,
		error: `Extension not found: ${extensionId}`
	};
}
/**
* List installed extensions from all tiers
*/
function listAllExtensions(workspaceDir) {
	const extensions = [];
	const seen = /* @__PURE__ */ new Set();
	const workspaceExtensionsDir = join(workspaceDir, ".extensions");
	if (existsSync(workspaceExtensionsDir)) for (const entry of readdirSync(workspaceExtensionsDir, { withFileTypes: true })) {
		if (!entry.isDirectory()) continue;
		const extensionDir = join(workspaceExtensionsDir, entry.name);
		const manifest = readManifest(extensionDir);
		if (manifest) {
			seen.add(entry.name);
			extensions.push({
				id: entry.name,
				name: manifest.name,
				version: manifest.version,
				path: extensionDir,
				origin: "workspace"
			});
		}
	}
	const globalDir = resolveExtensionsDir$1();
	if (existsSync(globalDir)) for (const entry of readdirSync(globalDir, { withFileTypes: true })) {
		if (!entry.isDirectory()) continue;
		if (seen.has(entry.name)) continue;
		const extensionDir = join(globalDir, entry.name);
		const manifest = readManifest(extensionDir);
		if (manifest) {
			seen.add(entry.name);
			extensions.push({
				id: entry.name,
				name: manifest.name,
				version: manifest.version,
				path: extensionDir,
				origin: "global"
			});
		}
	}
	const bundledDir = resolveBundledExtensionsDir();
	if (bundledDir && existsSync(bundledDir)) for (const entry of readdirSync(bundledDir, { withFileTypes: true })) {
		if (!entry.isDirectory()) continue;
		if (seen.has(entry.name)) continue;
		const extensionDir = join(bundledDir, entry.name);
		const manifest = readManifest(extensionDir);
		if (manifest) extensions.push({
			id: entry.name,
			name: manifest.name,
			version: manifest.version,
			path: extensionDir,
			origin: "bundled"
		});
	}
	return extensions;
}
function readManifest(extensionDir) {
	const manifestPath = join(extensionDir, "xopc.extension.json");
	if (!existsSync(manifestPath)) {
		const packagePath = join(extensionDir, "package.json");
		if (existsSync(packagePath)) try {
			const pkg = JSON.parse(readFileSync(packagePath, "utf-8"));
			return {
				id: pkg.name,
				name: pkg.xopc?.extension?.name || pkg.name,
				version: pkg.version
			};
		} catch {
			return null;
		}
		return null;
	}
	try {
		return JSON.parse(readFileSync(manifestPath, "utf-8"));
	} catch {
		return null;
	}
}
//#endregion
export { installExtensionFromStoreZip, installFromLocal, installFromNpm, listAllExtensions, peekExtensionIdFromStoreZip, removeExtension, resolveExtensionsDir };

//# sourceMappingURL=install.js.map