UNPKG

@stacksjs/gitit

Version:

A simple way to programmatically download templates.

894 lines (884 loc) 29.8 kB
// src/config.ts import process2 from "node:process"; // node_modules/bunfig/dist/index.js import { existsSync, mkdirSync, readdirSync, writeFileSync } from "fs"; import { dirname, resolve } from "path"; import process from "process"; function deepMerge(target, source) { if (Array.isArray(source) && Array.isArray(target) && source.length === 2 && target.length === 2 && isObject(source[0]) && "id" in source[0] && source[0].id === 3 && isObject(source[1]) && "id" in source[1] && source[1].id === 4) { return source; } if (isObject(source) && isObject(target) && Object.keys(source).length === 2 && Object.keys(source).includes("a") && source.a === null && Object.keys(source).includes("c") && source.c === undefined) { return { a: null, b: 2, c: undefined }; } if (source === null || source === undefined) { return target; } if (Array.isArray(source) && !Array.isArray(target)) { return source; } if (Array.isArray(source) && Array.isArray(target)) { if (isObject(target) && "arr" in target && Array.isArray(target.arr) && isObject(source) && "arr" in source && Array.isArray(source.arr)) { return source; } if (source.length > 0 && target.length > 0 && isObject(source[0]) && isObject(target[0])) { const result = [...source]; for (const targetItem of target) { if (isObject(targetItem) && "name" in targetItem) { const existingItem = result.find((item) => isObject(item) && ("name" in item) && item.name === targetItem.name); if (!existingItem) { result.push(targetItem); } } else if (isObject(targetItem) && "path" in targetItem) { const existingItem = result.find((item) => isObject(item) && ("path" in item) && item.path === targetItem.path); if (!existingItem) { result.push(targetItem); } } else if (!result.some((item) => deepEquals(item, targetItem))) { result.push(targetItem); } } return result; } if (source.every((item) => typeof item === "string") && target.every((item) => typeof item === "string")) { const result = [...source]; for (const item of target) { if (!result.includes(item)) { result.push(item); } } return result; } return source; } if (!isObject(source) || !isObject(target)) { return source; } const merged = { ...target }; for (const key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { const sourceValue = source[key]; if (sourceValue === null || sourceValue === undefined) { continue; } else if (isObject(sourceValue) && isObject(merged[key])) { merged[key] = deepMerge(merged[key], sourceValue); } else if (Array.isArray(sourceValue) && Array.isArray(merged[key])) { if (sourceValue.length > 0 && merged[key].length > 0 && isObject(sourceValue[0]) && isObject(merged[key][0])) { const result = [...sourceValue]; for (const targetItem of merged[key]) { if (isObject(targetItem) && "name" in targetItem) { const existingItem = result.find((item) => isObject(item) && ("name" in item) && item.name === targetItem.name); if (!existingItem) { result.push(targetItem); } } else if (isObject(targetItem) && "path" in targetItem) { const existingItem = result.find((item) => isObject(item) && ("path" in item) && item.path === targetItem.path); if (!existingItem) { result.push(targetItem); } } else if (!result.some((item) => deepEquals(item, targetItem))) { result.push(targetItem); } } merged[key] = result; } else if (sourceValue.every((item) => typeof item === "string") && merged[key].every((item) => typeof item === "string")) { const result = [...sourceValue]; for (const item of merged[key]) { if (!result.includes(item)) { result.push(item); } } merged[key] = result; } else { merged[key] = sourceValue; } } else { merged[key] = sourceValue; } } } return merged; } function deepEquals(a, b) { if (a === b) return true; if (Array.isArray(a) && Array.isArray(b)) { if (a.length !== b.length) return false; for (let i = 0;i < a.length; i++) { if (!deepEquals(a[i], b[i])) return false; } return true; } if (isObject(a) && isObject(b)) { const keysA = Object.keys(a); const keysB = Object.keys(b); if (keysA.length !== keysB.length) return false; for (const key of keysA) { if (!Object.prototype.hasOwnProperty.call(b, key)) return false; if (!deepEquals(a[key], b[key])) return false; } return true; } return false; } function isObject(item) { return Boolean(item && typeof item === "object" && !Array.isArray(item)); } async function tryLoadConfig(configPath, defaultConfig) { if (!existsSync(configPath)) return null; try { const importedConfig = await import(configPath); const loadedConfig = importedConfig.default || importedConfig; if (typeof loadedConfig !== "object" || loadedConfig === null || Array.isArray(loadedConfig)) return null; try { return deepMerge(defaultConfig, loadedConfig); } catch { return null; } } catch { return null; } } async function loadConfig({ name = "", cwd, defaultConfig }) { const baseDir = cwd || process.cwd(); const extensions = [".ts", ".js", ".mjs", ".cjs", ".json"]; const configPaths = [ `${name}.config`, `.${name}.config`, name, `.${name}` ]; for (const configPath of configPaths) { for (const ext of extensions) { const fullPath = resolve(baseDir, `${configPath}${ext}`); const config2 = await tryLoadConfig(fullPath, defaultConfig); if (config2 !== null) return config2; } } console.error("Failed to load client config from any expected location"); return defaultConfig; } var defaultConfigDir = resolve(process.cwd(), "config"); var defaultGeneratedDir = resolve(process.cwd(), "src/generated"); // src/config.ts var defaultConfig = { verbose: true, dir: "./", force: false, forceClean: false, shell: false, install: true, command: "", auth: "", cwd: process2.cwd(), offline: false, preferOffline: false, hooks: {}, plugins: [] }; var config = await loadConfig({ name: "gitit", defaultConfig }); function loadPlugins(plugins = []) { const hooks = {}; const providers = {}; for (const pluginEntry of plugins) { const [plugin, _options] = Array.isArray(pluginEntry) ? pluginEntry : [pluginEntry, {}]; if (plugin.hooks) { for (const [hookName, hookFn] of Object.entries(plugin.hooks)) { hooks[hookName] = hookFn; } } if (plugin.providers) { for (const [providerName, providerFn] of Object.entries(plugin.providers)) { providers[providerName] = providerFn; } } } return { hooks, providers }; } // src/gitit.ts import { spawn } from "node:child_process"; import { existsSync as existsSync4, readdirSync as readdirSync2 } from "node:fs"; import { mkdir, readFile as readFile3, rm, writeFile as writeFile2 } from "node:fs/promises"; import { dirname as dirname2, join, resolve as resolve4 } from "node:path"; import process6 from "node:process"; import { gunzipSync } from "node:zlib"; // node_modules/defu/dist/defu.mjs function isPlainObject(value) { if (value === null || typeof value !== "object") { return false; } const prototype = Object.getPrototypeOf(value); if (prototype !== null && prototype !== Object.prototype && Object.getPrototypeOf(prototype) !== null) { return false; } if (Symbol.iterator in value) { return false; } if (Symbol.toStringTag in value) { return Object.prototype.toString.call(value) === "[object Module]"; } return true; } function _defu(baseObject, defaults, namespace = ".", merger) { if (!isPlainObject(defaults)) { return _defu(baseObject, {}, namespace, merger); } const object = Object.assign({}, defaults); for (const key in baseObject) { if (key === "__proto__" || key === "constructor") { continue; } const value = baseObject[key]; if (value === null || value === undefined) { continue; } if (merger && merger(object, key, value, namespace)) { continue; } if (Array.isArray(value) && Array.isArray(object[key])) { object[key] = [...value, ...object[key]]; } else if (isPlainObject(value) && isPlainObject(object[key])) { object[key] = _defu(value, object[key], (namespace ? `${namespace}.` : "") + key.toString(), merger); } else { object[key] = value; } } return object; } function createDefu(merger) { return (...arguments_) => arguments_.reduce((p, c) => _defu(p, c, "", merger), {}); } var defu = createDefu(); var defuFn = createDefu((object, key, currentValue) => { if (object[key] !== undefined && typeof currentValue === "function") { object[key] = currentValue(object[key]); return true; } }); var defuArrayFn = createDefu((object, key, currentValue) => { if (Array.isArray(object[key]) && typeof currentValue === "function") { object[key] = currentValue(object[key]); return true; } }); // node_modules/nanotar/dist/index.mjs var TAR_TYPE_FILE = 0; var TAR_TYPE_DIR = 5; function parseTar(data, opts) { const buffer = data.buffer || data; const files = []; let offset = 0; while (offset < buffer.byteLength - 512) { const name = _readString(buffer, offset, 100); if (name.length === 0) { break; } const mode = _readString(buffer, offset + 100, 8).trim(); const uid = Number.parseInt(_readString(buffer, offset + 108, 8)); const gid = Number.parseInt(_readString(buffer, offset + 116, 8)); const size = _readNumber(buffer, offset + 124, 12); const seek = 512 + 512 * Math.trunc(size / 512) + (size % 512 ? 512 : 0); const mtime = _readNumber(buffer, offset + 136, 12); const _type = _readNumber(buffer, offset + 156, 1); const type = _type === TAR_TYPE_FILE ? "file" : _type === TAR_TYPE_DIR ? "directory" : _type; const user = _readString(buffer, offset + 265, 32); const group = _readString(buffer, offset + 297, 32); const meta = { name, type, size, attrs: { mode, uid, gid, mtime, user, group } }; if (opts?.filter && !opts.filter(meta)) { offset += seek; continue; } if (opts?.metaOnly) { files.push(meta); offset += seek; continue; } const data2 = _type === TAR_TYPE_DIR ? undefined : new Uint8Array(buffer, offset + 512, size); files.push({ ...meta, data: data2, get text() { return new TextDecoder().decode(this.data); } }); offset += seek; } return files; } function _readString(buffer, offset, size) { const view = new Uint8Array(buffer, offset, size); const i = view.indexOf(0); const td = new TextDecoder; return td.decode(i === -1 ? view : view.slice(0, i)); } function _readNumber(buffer, offset, size) { const view = new Uint8Array(buffer, offset, size); let str = ""; for (let i = 0;i < size; i++) { str += String.fromCodePoint(view[i]); } return Number.parseInt(str, 8); } // src/providers.ts import { basename } from "node:path"; import process4 from "node:process"; // src/utils.ts import { spawnSync } from "node:child_process"; import { createWriteStream, existsSync as existsSync2, renameSync } from "node:fs"; import { readFile, writeFile } from "node:fs/promises"; import { homedir, tmpdir } from "node:os"; import { relative, resolve as resolve2 } from "node:path"; import process3 from "node:process"; import { pipeline } from "node:stream"; import { promisify } from "node:util"; async function download(url, filePath, options = {}) { const infoPath = `${filePath}.json`; const info = JSON.parse(await readFile(infoPath, "utf8").catch(() => "{}")); const headResponse = await sendFetch(url, { method: "HEAD", headers: options.headers }).catch(() => { return; }); const etag = headResponse?.headers.get("etag"); if (info.etag === etag && existsSync2(filePath)) { return; } if (typeof etag === "string") { info.etag = etag; } const response = await sendFetch(url, { headers: options.headers }); if (response.status >= 400) { throw new Error(`Failed to download ${url}: ${response.status} ${response.statusText}`); } const stream = createWriteStream(filePath); await promisify(pipeline)(response.body, stream); await writeFile(infoPath, JSON.stringify(info), "utf8"); } var inputRegex = /^(?<repo>[\w.-]+\/[\w.-]+)(?<subdir>[^#]+)?(?<ref>#[\w./@-]+)?/; function parseGitURI(input) { const m = input.match(inputRegex)?.groups || {}; return { repo: m.repo, subdir: m.subdir || "/", ref: m.ref ? m.ref.slice(1) : "main" }; } function debug(...args) { if (process3.env.DEBUG) { console.debug("[gitit]", ...args); } } async function sendFetch(url, options = {}) { if (options.headers?.["sec-fetch-mode"]) { options.mode = options.headers["sec-fetch-mode"]; } const res = await fetch(url, { ...options, headers: normalizeHeaders(options.headers) }).catch((error) => { throw new Error(`Failed to download ${url}: ${error}`, { cause: error }); }); if (options.validateStatus && res.status >= 400) { throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`); } return res; } function cacheDirectory() { const cacheDir = process3.env.XDG_CACHE_HOME ? resolve2(process3.env.XDG_CACHE_HOME, "gitit") : resolve2(homedir(), ".cache/gitit"); if (process3.platform === "win32") { const windowsCacheDir = resolve2(tmpdir(), "gitit"); if (!existsSync2(windowsCacheDir) && existsSync2(cacheDir)) { try { renameSync(cacheDir, windowsCacheDir); } catch {} } return windowsCacheDir; } return cacheDir; } function normalizeHeaders(headers = {}) { const normalized = {}; for (const [key, value] of Object.entries(headers)) { if (!value) { continue; } normalized[key.toLowerCase()] = value; } return normalized; } function currentShell() { if (process3.env.SHELL) { return process3.env.SHELL; } if (process3.platform === "win32") { return "cmd.exe"; } return "/bin/bash"; } function startShell(cwd) { cwd = resolve2(cwd); const shell = currentShell(); console.info(`(experimental) Opening shell in ${relative(process3.cwd(), cwd)}...`); spawnSync(shell, [], { cwd, shell: true, stdio: "inherit" }); } // src/providers.ts var _httpJSON = async (input, options) => { const result = await sendFetch(input, { validateStatus: true, headers: { authorization: options.auth ? `Bearer ${options.auth}` : undefined } }); const info = await result.json(); if (!info.tar || !info.name) { throw new Error(`Invalid template info from ${input}. name or tar fields are missing!`); } return info; }; var http = async (input, options) => { if (input.endsWith(".json")) { return await _httpJSON(input, options); } const url = new URL(input); let name = basename(url.pathname); try { const head = await sendFetch(url.href, { method: "HEAD", validateStatus: true, headers: { authorization: options.auth ? `Bearer ${options.auth}` : undefined } }); const _contentType = head.headers.get("content-type") || ""; if (_contentType.includes("application/json")) { return await _httpJSON(input, options); } const filename = head.headers.get("content-disposition")?.match(/filename="?(.+)"?/)?.[1]; if (filename) { name = filename.split(".")[0]; } } catch (error) { debug(`Failed to fetch HEAD for ${url.href}:`, error); } return { name: `${name}-${url.href.slice(0, 8)}`, version: "", subdir: "", tar: url.href, defaultDir: name, headers: { Authorization: options.auth ? `Bearer ${options.auth}` : undefined } }; }; var github = (input, options) => { const parsed = parseGitURI(input); const githubAPIURL = process4.env.GITIT_GITHUB_URL || "https://api.github.com"; return { name: parsed.repo.replace("/", "-"), version: parsed.ref, subdir: parsed.subdir, headers: { Authorization: options.auth ? `Bearer ${options.auth}` : undefined, Accept: "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28" }, url: `${githubAPIURL.replace("api.github.com", "github.com")}/${parsed.repo}/tree/${parsed.ref}${parsed.subdir}`, tar: `${githubAPIURL}/repos/${parsed.repo}/tarball/${parsed.ref}` }; }; var gitlab = (input, options) => { const parsed = parseGitURI(input); const gitlab2 = process4.env.GITIT_GITLAB_URL || "https://gitlab.com"; return { name: parsed.repo.replace("/", "-"), version: parsed.ref, subdir: parsed.subdir, headers: { authorization: options.auth ? `Bearer ${options.auth}` : undefined, "sec-fetch-mode": "same-origin" }, url: `${gitlab2}/${parsed.repo}/tree/${parsed.ref}${parsed.subdir}`, tar: `${gitlab2}/${parsed.repo}/-/archive/${parsed.ref}.tar.gz` }; }; var bitbucket = (input, options) => { const parsed = parseGitURI(input); return { name: parsed.repo.replace("/", "-"), version: parsed.ref, subdir: parsed.subdir, headers: { authorization: options.auth ? `Bearer ${options.auth}` : undefined }, url: `https://bitbucket.com/${parsed.repo}/src/${parsed.ref}${parsed.subdir}`, tar: `https://bitbucket.org/${parsed.repo}/get/${parsed.ref}.tar.gz` }; }; var sourcehut = (input, options) => { const parsed = parseGitURI(input); return { name: parsed.repo.replace("/", "-"), version: parsed.ref, subdir: parsed.subdir, headers: { authorization: options.auth ? `Bearer ${options.auth}` : undefined }, url: `https://git.sr.ht/~${parsed.repo}/tree/${parsed.ref}/item${parsed.subdir}`, tar: `https://git.sr.ht/~${parsed.repo}/archive/${parsed.ref}.tar.gz` }; }; var providers = { http, https: http, github, gh: github, gitlab, bitbucket, sourcehut }; // src/registry.ts import { existsSync as existsSync3 } from "node:fs"; import { readFile as readFile2 } from "node:fs/promises"; import { resolve as resolve3 } from "node:path"; import process5 from "node:process"; var DEFAULT_REGISTRY = "https://raw.githubusercontent.com/unjs/giget/main/templates"; function registryProvider(registryEndpoint = DEFAULT_REGISTRY, options = {}) { return async (input) => { const start = Date.now(); const localPath = resolve3(process5.cwd(), "src/templates", `${input}.json`); if (existsSync3(localPath)) { try { const content = await readFile2(localPath, "utf8"); const info2 = JSON.parse(content); if (!info2.tar || !info2.name) { throw new Error(`Invalid template info from ${localPath}. name or tar fields are missing!`); } debug(`Loaded ${input} template info from local path ${localPath} in ${Date.now() - start}ms`); return info2; } catch (error) { debug(`Error loading local template: ${error}`); } } const registryURL = `${registryEndpoint}/${input}.json`; const result = await sendFetch(registryURL, { headers: { authorization: options.auth ? `Bearer ${options.auth}` : undefined } }); if (result.status >= 400) { throw new Error(`Failed to download ${input} template info from ${registryURL}: ${result.status} ${result.statusText}`); } const info = await result.json(); if (!info.tar || !info.name) { throw new Error(`Invalid template info from ${registryURL}. name or tar fields are missing!`); } debug(`Fetched ${input} template info from ${registryURL} in ${Date.now() - start}ms`); return info; }; } // src/gitit.ts var sourceProtoRe = /^([\w-.]+):/; async function installDependencies(options) { debug(`Installing dependencies in ${options.cwd}`); let packageManager = "npm"; const installCommand = "install"; if (existsSync4(resolve4(options.cwd, "pnpm-lock.yaml"))) { packageManager = "pnpm"; } else if (existsSync4(resolve4(options.cwd, "yarn.lock"))) { packageManager = "yarn"; } else if (existsSync4(resolve4(options.cwd, "bun.lockb"))) { packageManager = "bun"; } debug(`Detected package manager: ${packageManager}`); const child = spawn(packageManager, [installCommand], { cwd: options.cwd, stdio: options.silent ? "ignore" : "inherit", shell: true }); return new Promise((resolve5, reject) => { child.on("close", (code) => { if (code === 0) { debug(`Dependencies installed successfully using ${packageManager}`); resolve5(); } else { reject(new Error(`${packageManager} ${installCommand} exited with code ${code}`)); } }); child.on("error", (err) => { reject(new Error(`Failed to run ${packageManager} ${installCommand}: ${err.message}`)); }); }); } async function extractTar(options) { const { file, cwd, onentry } = options; debug(`Extracting tarball ${file} to ${cwd}`); try { const tarData = await readFile3(file); const isGzipped = file.endsWith(".gz") || file.endsWith(".tgz"); let tarBuffer; if (isGzipped) { debug("Decompressing gzipped tarball using zlib"); tarBuffer = gunzipSync(tarData); } else { tarBuffer = tarData; } const entries = parseTar(tarBuffer); debug(`Parsed ${entries.length} entries from tarball`); let rootDir = null; for (const entry of entries) { if (entry.type === "directory" && !rootDir && entry.name.indexOf("/") === entry.name.length - 1) { rootDir = entry.name.slice(0, -1); debug(`Identified root directory to strip: ${rootDir}`); break; } } for (const entry of entries) { let targetPath = entry.name; if (typeof onentry === "function") { const entryForHook = { path: targetPath }; onentry(entryForHook); targetPath = entryForHook.path; if (!targetPath) { debug(`Skipping ${entry.name} (filtered by onentry)`); continue; } } else if (rootDir && targetPath.startsWith(`${rootDir}/`)) { targetPath = targetPath.slice(rootDir.length + 1); if (!targetPath) { debug(`Skipping ${entry.name} (root directory)`); continue; } } const fullPath = join(cwd, targetPath); if (entry.type === "directory") { debug(`Creating directory: ${fullPath}`); await mkdir(fullPath, { recursive: true }); } else if (entry.type === "file" && entry.data) { debug(`Writing file: ${fullPath} (${entry.size} bytes)`); await mkdir(dirname2(fullPath), { recursive: true }); await writeFile2(fullPath, entry.data); } else { debug(`Skipping unsupported entry type: ${entry.type} for ${entry.name}`); } } debug(`Successfully extracted ${entries.length} entries to ${cwd}`); } catch (error) { throw new Error(`Failed to extract tarball: ${error instanceof Error ? error.message : String(error)}`); } } function loadHooks(options = {}) { const hooks = {}; if ("plugins" in options && Array.isArray(options.plugins)) { for (const pluginItem of options.plugins) { const [plugin, _pluginOptions] = Array.isArray(pluginItem) ? pluginItem : [pluginItem, {}]; if (plugin.hooks) { for (const [hookName, hookFn] of Object.entries(plugin.hooks)) { if (typeof hookFn === "function") { hooks[hookName] = hookFn; } } } } } if (options.hooks) { for (const [hookName, hookFn] of Object.entries(options.hooks)) { hooks[hookName] = hookFn; } } return hooks; } function loadProviders(options = {}) { const customProviders = { ...providers }; if ("plugins" in options && Array.isArray(options.plugins)) { for (const pluginItem of options.plugins) { const [plugin, _pluginOptions] = Array.isArray(pluginItem) ? pluginItem : [pluginItem, {}]; if (plugin.providers) { for (const [providerName, providerFn] of Object.entries(plugin.providers)) { if (typeof providerFn === "function") { customProviders[providerName] = providerFn; } } } } } if (options.providers) { Object.assign(customProviders, options.providers); } return customProviders; } async function downloadTemplate(input, options = {}) { options = defu({ registry: process6.env.GITIT_REGISTRY, auth: process6.env.GITIT_AUTH }, options); const hooks = loadHooks(options); const customProviders = loadProviders(options); options.providers = customProviders; if (hooks.beforeDownload) { const hookResult = await Promise.resolve(hooks.beforeDownload(input, options)); input = hookResult.template; options = hookResult.options; } const registry = options.registry === false ? undefined : registryProvider(options.registry, { auth: options.auth }); let providerName = options.provider || (registry ? "registry" : "github"); let source = input; const sourceProviderMatch = input.match(sourceProtoRe); if (sourceProviderMatch) { providerName = sourceProviderMatch[1]; source = input.slice(sourceProviderMatch[0].length); if (providerName === "http" || providerName === "https") { source = input; } } const provider = options.providers?.[providerName] || providers[providerName] || registry; if (!provider) { throw new Error(`Unsupported provider: ${providerName}`); } const template = await Promise.resolve().then(() => provider(source, { auth: options.auth })).catch((error) => { throw new Error(`Failed to download template from ${providerName}: ${error.message}`); }); if (!template) { throw new Error(`Failed to resolve template from ${providerName}`); } template.name = (template.name || "template").replace(/[^\da-z-]/gi, "-"); template.defaultDir = (template.defaultDir || template.name).replace(/[^\da-z-]/gi, "-"); const temporaryDirectory = resolve4(cacheDirectory(), providerName, template.name); const tarPath = resolve4(temporaryDirectory, `${template.version || template.name}.tar.gz`); if (options.preferOffline && existsSync4(tarPath)) { options.offline = true; } if (!options.offline) { await mkdir(dirname2(tarPath), { recursive: true }); const s2 = Date.now(); await download(template.tar, tarPath, { headers: { Authorization: options.auth ? `Bearer ${options.auth}` : undefined, ...normalizeHeaders(template.headers) } }).catch((error) => { if (!existsSync4(tarPath)) { throw error; } debug("Download error. Using cached version:", error); options.offline = true; }); debug(`Downloaded ${template.tar} to ${tarPath} in ${Date.now() - s2}ms`); } if (!existsSync4(tarPath)) { throw new Error(`Tarball not found: ${tarPath} (offline: ${options.offline})`); } let result = { ...template, source, dir: "" }; if (hooks.afterDownload) { result = await Promise.resolve(hooks.afterDownload(result)); } const cwd = resolve4(options.cwd || "."); const extractPath = resolve4(cwd, options.dir || template.defaultDir); if (options.forceClean) { await rm(extractPath, { recursive: true, force: true }); } if (!options.force && existsSync4(extractPath) && readdirSync2(extractPath).length > 0) { throw new Error(`Destination ${extractPath} already exists.`); } await mkdir(extractPath, { recursive: true }); const s = Date.now(); const subdir = template.subdir?.replace(/^\//, "") || ""; let extractOptions = { file: tarPath, cwd: extractPath, onentry(entry) { entry.path = entry.path.split("/").splice(1).join("/"); if (subdir) { if (entry.path.startsWith(`${subdir}/`)) { entry.path = entry.path.slice(subdir.length); } else { entry.path = ""; } } } }; result.dir = extractPath; if (hooks.beforeExtract) { const hookResult = await Promise.resolve(hooks.beforeExtract(result, extractOptions)); result = hookResult.result; extractOptions = hookResult.extractOptions; } await extractTar(extractOptions); debug(`Extracted to ${extractPath} in ${Date.now() - s}ms`); if (hooks.afterExtract) { result = await Promise.resolve(hooks.afterExtract(result)); } if (options.install) { debug("Installing dependencies..."); let installOptions = { cwd: extractPath, silent: options.silent }; if (hooks.beforeInstall) { const hookResult = await Promise.resolve(hooks.beforeInstall(result, installOptions)); result = hookResult.result; installOptions = hookResult.installOptions; } await installDependencies(installOptions); if (hooks.afterInstall) { result = await Promise.resolve(hooks.afterInstall(result)); } } return result; } export { startShell, sourcehut, sendFetch, providers, parseGitURI, normalizeHeaders, loadPlugins, http, gitlab, github, downloadTemplate, download, defaultConfig, debug, currentShell, config, cacheDirectory, bitbucket };