'use strict'; const promises = require('node:fs/promises'); const node_fs = require('node:fs'); const tar = require('tar'); const pathe = require('pathe'); const defu = require('defu'); const nypm = require('nypm'); const node_stream = require('node:stream'); const node_child_process = require('node:child_process'); const node_os = require('node:os'); const node_util = require('node:util'); const proxy = require('node-fetch-native/proxy'); async function download(url, filePath, options = {}) { const infoPath = filePath + ".json"; const info = JSON.parse( await promises.readFile(infoPath, "utf8").catch(() => "{}") ); const headResponse = await sendFetch(url, { method: "HEAD", headers: options.headers }).catch(() => void 0); const etag = headResponse?.headers.get("etag"); if (info.etag === etag && node_fs.existsSync(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 = node_fs.createWriteStream(filePath); await node_util.promisify(node_stream.pipeline)(response.body, stream); await promises.writeFile(infoPath, JSON.stringify(info), "utf8"); } const inputRegex = /^(?[\w.-]+\/[\w.-]+)(?[^#]+)?(?#[\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 (process.env.DEBUG) { console.debug("[giget]", ...args); } } async function sendFetch(url, options = {}) { if (options.headers?.["sec-fetch-mode"]) { options.mode = options.headers["sec-fetch-mode"]; } const res = await proxy.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() { return process.env.XDG_CACHE_HOME ? pathe.resolve(process.env.XDG_CACHE_HOME, "giget") : pathe.resolve(node_os.homedir(), ".cache/giget"); } 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 (process.env.SHELL) { return process.env.SHELL; } if (process.platform === "win32") { return "cmd.exe"; } return "/bin/bash"; } function startShell(cwd) { cwd = pathe.resolve(cwd); const shell = currentShell(); console.info( `(experimental) Opening shell in ${pathe.relative(process.cwd(), cwd)}...` ); node_child_process.spawnSync(shell, [], { cwd, shell: true, stdio: "inherit" }); } const http = async (input, options) => { if (input.endsWith(".json")) { return await _httpJSON(input, options); } const url = new URL(input); let name = pathe.basename(url.pathname); try { const head = await sendFetch(url.href, { method: "HEAD", validateStatus: true, headers: { authorization: options.auth ? `Bearer ${options.auth}` : void 0 } }); 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}` : void 0 } }; }; const _httpJSON = async (input, options) => { const result = await sendFetch(input, { validateStatus: true, headers: { authorization: options.auth ? `Bearer ${options.auth}` : void 0 } }); 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; }; const github = (input, options) => { const parsed = parseGitURI(input); const githubAPIURL = process.env.GIGET_GITHUB_URL || "https://api.github.com"; return { name: parsed.repo.replace("/", "-"), version: parsed.ref, subdir: parsed.subdir, headers: { Authorization: options.auth ? `Bearer ${options.auth}` : void 0, 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}` }; }; const gitlab = (input, options) => { const parsed = parseGitURI(input); const gitlab2 = process.env.GIGET_GITLAB_URL || "https://gitlab.com"; return { name: parsed.repo.replace("/", "-"), version: parsed.ref, subdir: parsed.subdir, headers: { authorization: options.auth ? `Bearer ${options.auth}` : void 0, // https://gitlab.com/gitlab-org/gitlab/-/commit/50c11f278d18fe1f3fb12eb595067216bb58ade2 "sec-fetch-mode": "same-origin" }, url: `${gitlab2}/${parsed.repo}/tree/${parsed.ref}${parsed.subdir}`, tar: `${gitlab2}/${parsed.repo}/-/archive/${parsed.ref}.tar.gz` }; }; const 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}` : void 0 }, url: `https://bitbucket.com/${parsed.repo}/src/${parsed.ref}${parsed.subdir}`, tar: `https://bitbucket.org/${parsed.repo}/get/${parsed.ref}.tar.gz` }; }; const 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}` : void 0 }, 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` }; }; const providers = { http, https: http, github, gh: github, gitlab, bitbucket, sourcehut }; const DEFAULT_REGISTRY = "https://raw.githubusercontent.com/unjs/giget/main/templates"; const registryProvider = (registryEndpoint = DEFAULT_REGISTRY, options = {}) => { return async (input) => { const start = Date.now(); const registryURL = `${registryEndpoint}/${input}.json`; const result = await sendFetch(registryURL, { headers: { authorization: options.auth ? `Bearer ${options.auth}` : void 0 } }); 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; }; }; const sourceProtoRe = /^([\w-.]+):/; async function downloadTemplate(input, options = {}) { options = defu.defu( { registry: process.env.GIGET_REGISTRY, auth: process.env.GIGET_AUTH }, options ); const registry = options.registry === false ? void 0 : registryProvider(options.registry, { auth: options.auth }); let providerName = options.provider || (registry ? "registry" : "github"); let source = input; const sourceProvierMatch = input.match(sourceProtoRe); if (sourceProvierMatch) { providerName = sourceProvierMatch[1]; source = input.slice(sourceProvierMatch[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 = pathe.resolve( cacheDirectory(), providerName, template.name ); const tarPath = pathe.resolve( temporaryDirectory, (template.version || template.name) + ".tar.gz" ); if (options.preferOffline && node_fs.existsSync(tarPath)) { options.offline = true; } if (!options.offline) { await promises.mkdir(pathe.dirname(tarPath), { recursive: true }); const s2 = Date.now(); await download(template.tar, tarPath, { headers: { Authorization: options.auth ? `Bearer ${options.auth}` : void 0, ...normalizeHeaders(template.headers) } }).catch((error) => { if (!node_fs.existsSync(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 (!node_fs.existsSync(tarPath)) { throw new Error( `Tarball not found: ${tarPath} (offline: ${options.offline})` ); } const cwd = pathe.resolve(options.cwd || "."); const extractPath = pathe.resolve(cwd, options.dir || template.defaultDir); if (options.forceClean) { await promises.rm(extractPath, { recursive: true, force: true }); } if (!options.force && node_fs.existsSync(extractPath) && node_fs.readdirSync(extractPath).length > 0) { throw new Error(`Destination ${extractPath} already exists.`); } await promises.mkdir(extractPath, { recursive: true }); const s = Date.now(); const subdir = template.subdir?.replace(/^\//, "") || ""; await tar.extract({ 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 = ""; } } } }); debug(`Extracted to ${extractPath} in ${Date.now() - s}ms`); if (options.install) { debug("Installing dependencies..."); await nypm.installDependencies({ cwd: extractPath, silent: options.silent }); } return { ...template, source, dir: extractPath }; } exports.downloadTemplate = downloadTemplate; exports.registryProvider = registryProvider; exports.startShell = startShell;