diff --git a/dist/cli.js b/dist/cli.js index bb3b94b..15071a3 100755 --- a/dist/cli.js +++ b/dist/cli.js @@ -24,7 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge )); // src/cli.ts -var path3 = __toESM(require("path")); +var path4 = __toESM(require("path")); var fs3 = __toESM(require("fs")); var os2 = __toESM(require("os")); var import_child_process2 = require("child_process"); @@ -51,97 +51,1865 @@ function getPlatformInfo(overrides) { }; } -// src/core/matcher.ts -function normalizeCustomExtensionPattern(fileType) { - let pattern = fileType; - if (!pattern.endsWith("$")) { - pattern += "$"; - } - if (!pattern.startsWith("\\.")) { - pattern = `\\.${pattern}`; - } - return pattern; -} -function getExtPattern(fileType, system) { - const normalizedType = (fileType || "").toLowerCase(); - if (!normalizedType) { - if (system === "linux") { - return "\\.(deb|rpm|zip|tar\\.gz|tgz)$"; - } - if (system === "darwin" || system === "macos" || system === "mac" || system === "osx") { - return "\\.(pkg|zip|tar\\.gz|tgz)$"; - } - return "\\.(zip|tar\\.gz|tgz)$"; - } - if (normalizedType === "archive") { - return "\\.(zip|tar\\.gz|tgz)$"; - } - if (normalizedType === "package") { - return "\\.(deb|pkg|rpm)$"; - } - const shorthandTypePatterns = { - zip: "\\.(zip)$", - gzip: "\\.(tar\\.gz|tgz)$", - gz: "\\.(tar\\.gz|tgz)$", - tar: "\\.(tar)$", - "tar.gz": "\\.(tar\\.gz)$", - tgz: "\\.(tgz)$", - deb: "\\.(deb)$", - pkg: "\\.(pkg)$", - rpm: "\\.(rpm)$" +// node_modules/balanced-match/dist/esm/index.js +var balanced = (a, b, str) => { + const ma = a instanceof RegExp ? maybeMatch(a, str) : a; + const mb = b instanceof RegExp ? maybeMatch(b, str) : b; + const r = ma !== null && mb != null && range(ma, mb, str); + return r && { + start: r[0], + end: r[1], + pre: str.slice(0, r[0]), + body: str.slice(r[0] + ma.length, r[1]), + post: str.slice(r[1] + mb.length) }; - if (shorthandTypePatterns[normalizedType]) { - return shorthandTypePatterns[normalizedType]; +}; +var maybeMatch = (reg, str) => { + const m = str.match(reg); + return m ? m[0] : null; +}; +var range = (a, b, str) => { + let begs, beg, left, right = void 0, result; + let ai = str.indexOf(a); + let bi = str.indexOf(b, ai + 1); + let i = ai; + if (ai >= 0 && bi > 0) { + if (a === b) { + return [ai, bi]; + } + begs = []; + left = str.length; + while (i >= 0 && !result) { + if (i === ai) { + begs.push(i); + ai = str.indexOf(a, i + 1); + } else if (begs.length === 1) { + const r = begs.pop(); + if (r !== void 0) + result = [r, bi]; + } else { + beg = begs.pop(); + if (beg !== void 0 && beg < left) { + left = beg; + right = bi; + } + bi = str.indexOf(b, i + 1); + } + i = ai < bi && ai >= 0 ? ai : bi; + } + if (begs.length && right !== void 0) { + result = [left, right]; + } } - return normalizeCustomExtensionPattern(fileType || ""); + return result; +}; + +// node_modules/brace-expansion/dist/esm/index.js +var escSlash = "\0SLASH" + Math.random() + "\0"; +var escOpen = "\0OPEN" + Math.random() + "\0"; +var escClose = "\0CLOSE" + Math.random() + "\0"; +var escComma = "\0COMMA" + Math.random() + "\0"; +var escPeriod = "\0PERIOD" + Math.random() + "\0"; +var escSlashPattern = new RegExp(escSlash, "g"); +var escOpenPattern = new RegExp(escOpen, "g"); +var escClosePattern = new RegExp(escClose, "g"); +var escCommaPattern = new RegExp(escComma, "g"); +var escPeriodPattern = new RegExp(escPeriod, "g"); +var slashPattern = /\\\\/g; +var openPattern = /\\{/g; +var closePattern = /\\}/g; +var commaPattern = /\\,/g; +var periodPattern = /\\\./g; +var EXPANSION_MAX = 1e5; +function numeric(str) { + return !isNaN(str) ? parseInt(str, 10) : str.charCodeAt(0); } -function getMatchingAsset(assets, platform2, options) { - const { fileName, fileType } = options; - const extPattern = getExtPattern(fileType, platform2.system); - if (!fileName) { - const pattern = `${platform2.systemPattern}[_-]${platform2.archPattern}.*${extPattern}`; - const regex = new RegExp(pattern, "i"); - const matchingAssets = assets.filter((a) => regex.test(a.name)); - if (matchingAssets.length === 0) { - throw new Error(`No assets matched the default criteria: ${pattern}`); - } - if (matchingAssets.length > 1) { - throw new Error(`Multiple assets matched the default criteria: ${matchingAssets.map((a) => a.name).join(", ")}`); - } - return matchingAssets[0]; - } else if (fileName.startsWith("~")) { - let pattern = fileName.substring(1); - const hasSystem = pattern.includes("{{SYSTEM}}"); - const hasArch = pattern.includes("{{ARCH}}"); - const hasExt = pattern.includes("{{EXT_PATTERN}}"); - const hasEnd = pattern.endsWith("$"); - if (!hasSystem && !hasArch && !hasExt && !hasEnd) { - pattern += `.*{{SYSTEM}}[_-]{{ARCH}}.*{{EXT_PATTERN}}$`; - } else if (hasSystem && hasArch && !hasExt && !hasEnd) { - pattern += `.*{{EXT_PATTERN}}$`; - } - const finalPattern = pattern.replace(/{{SYSTEM}}/g, platform2.systemPattern).replace(/{{ARCH}}/g, platform2.archPattern).replace(/{{EXT_PATTERN}}/g, extPattern); - const regex = new RegExp(finalPattern, "i"); - const matchingAssets = assets.filter((a) => regex.test(a.name)); - if (matchingAssets.length === 0) { - throw new Error(`No assets matched the regex: ${finalPattern}`); - } - if (matchingAssets.length > 1) { - throw new Error(`Multiple assets matched the criteria: ${matchingAssets.map((a) => a.name).join(", ")}`); - } - return matchingAssets[0]; - } else { - const asset = assets.find((a) => a.name === fileName); - if (!asset) { - throw new Error(`No asset found matching the exact name: ${fileName}`); - } - return asset; +function escapeBraces(str) { + return str.replace(slashPattern, escSlash).replace(openPattern, escOpen).replace(closePattern, escClose).replace(commaPattern, escComma).replace(periodPattern, escPeriod); +} +function unescapeBraces(str) { + return str.replace(escSlashPattern, "\\").replace(escOpenPattern, "{").replace(escClosePattern, "}").replace(escCommaPattern, ",").replace(escPeriodPattern, "."); +} +function parseCommaParts(str) { + if (!str) { + return [""]; } + const parts = []; + const m = balanced("{", "}", str); + if (!m) { + return str.split(","); + } + const { pre, body, post } = m; + const p = pre.split(","); + p[p.length - 1] += "{" + body + "}"; + const postParts = parseCommaParts(post); + if (post.length) { + ; + p[p.length - 1] += postParts.shift(); + p.push.apply(p, postParts); + } + parts.push.apply(parts, p); + return parts; +} +function expand(str, options = {}) { + if (!str) { + return []; + } + const { max = EXPANSION_MAX } = options; + if (str.slice(0, 2) === "{}") { + str = "\\{\\}" + str.slice(2); + } + return expand_(escapeBraces(str), max, true).map(unescapeBraces); +} +function embrace(str) { + return "{" + str + "}"; +} +function isPadded(el) { + return /^-?0\d/.test(el); +} +function lte(i, y) { + return i <= y; +} +function gte(i, y) { + return i >= y; +} +function expand_(str, max, isTop) { + const expansions = []; + const m = balanced("{", "}", str); + if (!m) + return [str]; + const pre = m.pre; + const post = m.post.length ? expand_(m.post, max, false) : [""]; + if (/\$$/.test(m.pre)) { + for (let k = 0; k < post.length && k < max; k++) { + const expansion = pre + "{" + m.body + "}" + post[k]; + expansions.push(expansion); + } + } else { + const isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body); + const isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body); + const isSequence = isNumericSequence || isAlphaSequence; + const isOptions = m.body.indexOf(",") >= 0; + if (!isSequence && !isOptions) { + if (m.post.match(/,(?!,).*\}/)) { + str = m.pre + "{" + m.body + escClose + m.post; + return expand_(str, max, true); + } + return [str]; + } + let n; + if (isSequence) { + n = m.body.split(/\.\./); + } else { + n = parseCommaParts(m.body); + if (n.length === 1 && n[0] !== void 0) { + n = expand_(n[0], max, false).map(embrace); + if (n.length === 1) { + return post.map((p) => m.pre + n[0] + p); + } + } + } + let N; + if (isSequence && n[0] !== void 0 && n[1] !== void 0) { + const x = numeric(n[0]); + const y = numeric(n[1]); + const width = Math.max(n[0].length, n[1].length); + let incr = n.length === 3 && n[2] !== void 0 ? Math.max(Math.abs(numeric(n[2])), 1) : 1; + let test = lte; + const reverse = y < x; + if (reverse) { + incr *= -1; + test = gte; + } + const pad = n.some(isPadded); + N = []; + for (let i = x; test(i, y); i += incr) { + let c; + if (isAlphaSequence) { + c = String.fromCharCode(i); + if (c === "\\") { + c = ""; + } + } else { + c = String(i); + if (pad) { + const need = width - c.length; + if (need > 0) { + const z = new Array(need + 1).join("0"); + if (i < 0) { + c = "-" + z + c.slice(1); + } else { + c = z + c; + } + } + } + } + N.push(c); + } + } else { + N = []; + for (let j = 0; j < n.length; j++) { + N.push.apply(N, expand_(n[j], max, false)); + } + } + for (let j = 0; j < N.length; j++) { + for (let k = 0; k < post.length && expansions.length < max; k++) { + const expansion = pre + N[j] + post[k]; + if (!isTop || isSequence || expansion) { + expansions.push(expansion); + } + } + } + } + return expansions; +} + +// node_modules/minimatch/dist/esm/assert-valid-pattern.js +var MAX_PATTERN_LENGTH = 1024 * 64; +var assertValidPattern = (pattern) => { + if (typeof pattern !== "string") { + throw new TypeError("invalid pattern"); + } + if (pattern.length > MAX_PATTERN_LENGTH) { + throw new TypeError("pattern is too long"); + } +}; + +// node_modules/minimatch/dist/esm/brace-expressions.js +var posixClasses = { + "[:alnum:]": ["\\p{L}\\p{Nl}\\p{Nd}", true], + "[:alpha:]": ["\\p{L}\\p{Nl}", true], + "[:ascii:]": ["\\x00-\\x7f", false], + "[:blank:]": ["\\p{Zs}\\t", true], + "[:cntrl:]": ["\\p{Cc}", true], + "[:digit:]": ["\\p{Nd}", true], + "[:graph:]": ["\\p{Z}\\p{C}", true, true], + "[:lower:]": ["\\p{Ll}", true], + "[:print:]": ["\\p{C}", true], + "[:punct:]": ["\\p{P}", true], + "[:space:]": ["\\p{Z}\\t\\r\\n\\v\\f", true], + "[:upper:]": ["\\p{Lu}", true], + "[:word:]": ["\\p{L}\\p{Nl}\\p{Nd}\\p{Pc}", true], + "[:xdigit:]": ["A-Fa-f0-9", false] +}; +var braceEscape = (s) => s.replace(/[[\]\\-]/g, "\\$&"); +var regexpEscape = (s) => s.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); +var rangesToString = (ranges) => ranges.join(""); +var parseClass = (glob, position) => { + const pos = position; + if (glob.charAt(pos) !== "[") { + throw new Error("not in a brace expression"); + } + const ranges = []; + const negs = []; + let i = pos + 1; + let sawStart = false; + let uflag = false; + let escaping = false; + let negate = false; + let endPos = pos; + let rangeStart = ""; + WHILE: while (i < glob.length) { + const c = glob.charAt(i); + if ((c === "!" || c === "^") && i === pos + 1) { + negate = true; + i++; + continue; + } + if (c === "]" && sawStart && !escaping) { + endPos = i + 1; + break; + } + sawStart = true; + if (c === "\\") { + if (!escaping) { + escaping = true; + i++; + continue; + } + } + if (c === "[" && !escaping) { + for (const [cls, [unip, u, neg]] of Object.entries(posixClasses)) { + if (glob.startsWith(cls, i)) { + if (rangeStart) { + return ["$.", false, glob.length - pos, true]; + } + i += cls.length; + if (neg) + negs.push(unip); + else + ranges.push(unip); + uflag = uflag || u; + continue WHILE; + } + } + } + escaping = false; + if (rangeStart) { + if (c > rangeStart) { + ranges.push(braceEscape(rangeStart) + "-" + braceEscape(c)); + } else if (c === rangeStart) { + ranges.push(braceEscape(c)); + } + rangeStart = ""; + i++; + continue; + } + if (glob.startsWith("-]", i + 1)) { + ranges.push(braceEscape(c + "-")); + i += 2; + continue; + } + if (glob.startsWith("-", i + 1)) { + rangeStart = c; + i += 2; + continue; + } + ranges.push(braceEscape(c)); + i++; + } + if (endPos < i) { + return ["", false, 0, false]; + } + if (!ranges.length && !negs.length) { + return ["$.", false, glob.length - pos, true]; + } + if (negs.length === 0 && ranges.length === 1 && /^\\?.$/.test(ranges[0]) && !negate) { + const r = ranges[0].length === 2 ? ranges[0].slice(-1) : ranges[0]; + return [regexpEscape(r), false, endPos - pos, false]; + } + const sranges = "[" + (negate ? "^" : "") + rangesToString(ranges) + "]"; + const snegs = "[" + (negate ? "" : "^") + rangesToString(negs) + "]"; + const comb = ranges.length && negs.length ? "(" + sranges + "|" + snegs + ")" : ranges.length ? sranges : snegs; + return [comb, uflag, endPos - pos, true]; +}; + +// node_modules/minimatch/dist/esm/unescape.js +var unescape = (s, { windowsPathsNoEscape = false, magicalBraces = true } = {}) => { + if (magicalBraces) { + return windowsPathsNoEscape ? s.replace(/\[([^/\\])\]/g, "$1") : s.replace(/((?!\\).|^)\[([^/\\])\]/g, "$1$2").replace(/\\([^/])/g, "$1"); + } + return windowsPathsNoEscape ? s.replace(/\[([^/\\{}])\]/g, "$1") : s.replace(/((?!\\).|^)\[([^/\\{}])\]/g, "$1$2").replace(/\\([^/{}])/g, "$1"); +}; + +// node_modules/minimatch/dist/esm/ast.js +var _a; +var types = /* @__PURE__ */ new Set(["!", "?", "+", "*", "@"]); +var isExtglobType = (c) => types.has(c); +var isExtglobAST = (c) => isExtglobType(c.type); +var adoptionMap = /* @__PURE__ */ new Map([ + ["!", ["@"]], + ["?", ["?", "@"]], + ["@", ["@"]], + ["*", ["*", "+", "?", "@"]], + ["+", ["+", "@"]] +]); +var adoptionWithSpaceMap = /* @__PURE__ */ new Map([ + ["!", ["?"]], + ["@", ["?"]], + ["+", ["?", "*"]] +]); +var adoptionAnyMap = /* @__PURE__ */ new Map([ + ["!", ["?", "@"]], + ["?", ["?", "@"]], + ["@", ["?", "@"]], + ["*", ["*", "+", "?", "@"]], + ["+", ["+", "@", "?", "*"]] +]); +var usurpMap = /* @__PURE__ */ new Map([ + ["!", /* @__PURE__ */ new Map([["!", "@"]])], + [ + "?", + /* @__PURE__ */ new Map([ + ["*", "*"], + ["+", "*"] + ]) + ], + [ + "@", + /* @__PURE__ */ new Map([ + ["!", "!"], + ["?", "?"], + ["@", "@"], + ["*", "*"], + ["+", "+"] + ]) + ], + [ + "+", + /* @__PURE__ */ new Map([ + ["?", "*"], + ["*", "*"] + ]) + ] +]); +var startNoTraversal = "(?!(?:^|/)\\.\\.?(?:$|/))"; +var startNoDot = "(?!\\.)"; +var addPatternStart = /* @__PURE__ */ new Set(["[", "."]); +var justDots = /* @__PURE__ */ new Set(["..", "."]); +var reSpecials = new Set("().*{}+?[]^$\\!"); +var regExpEscape = (s) => s.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); +var qmark = "[^/]"; +var star = qmark + "*?"; +var starNoEmpty = qmark + "+?"; +var ID = 0; +var AST = class { + type; + #root; + #hasMagic; + #uflag = false; + #parts = []; + #parent; + #parentIndex; + #negs; + #filledNegs = false; + #options; + #toString; + // set to true if it's an extglob with no children + // (which really means one child of '') + #emptyExt = false; + id = ++ID; + get depth() { + return (this.#parent?.depth ?? -1) + 1; + } + [/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() { + return { + "@@type": "AST", + id: this.id, + type: this.type, + root: this.#root.id, + parent: this.#parent?.id, + depth: this.depth, + partsLength: this.#parts.length, + parts: this.#parts + }; + } + constructor(type, parent, options = {}) { + this.type = type; + if (type) + this.#hasMagic = true; + this.#parent = parent; + this.#root = this.#parent ? this.#parent.#root : this; + this.#options = this.#root === this ? options : this.#root.#options; + this.#negs = this.#root === this ? [] : this.#root.#negs; + if (type === "!" && !this.#root.#filledNegs) + this.#negs.push(this); + this.#parentIndex = this.#parent ? this.#parent.#parts.length : 0; + } + get hasMagic() { + if (this.#hasMagic !== void 0) + return this.#hasMagic; + for (const p of this.#parts) { + if (typeof p === "string") + continue; + if (p.type || p.hasMagic) + return this.#hasMagic = true; + } + return this.#hasMagic; + } + // reconstructs the pattern + toString() { + return this.#toString !== void 0 ? this.#toString : !this.type ? this.#toString = this.#parts.map((p) => String(p)).join("") : this.#toString = this.type + "(" + this.#parts.map((p) => String(p)).join("|") + ")"; + } + #fillNegs() { + if (this !== this.#root) + throw new Error("should only call on root"); + if (this.#filledNegs) + return this; + this.toString(); + this.#filledNegs = true; + let n; + while (n = this.#negs.pop()) { + if (n.type !== "!") + continue; + let p = n; + let pp = p.#parent; + while (pp) { + for (let i = p.#parentIndex + 1; !pp.type && i < pp.#parts.length; i++) { + for (const part of n.#parts) { + if (typeof part === "string") { + throw new Error("string part in extglob AST??"); + } + part.copyIn(pp.#parts[i]); + } + } + p = pp; + pp = p.#parent; + } + } + return this; + } + push(...parts) { + for (const p of parts) { + if (p === "") + continue; + if (typeof p !== "string" && !(p instanceof _a && p.#parent === this)) { + throw new Error("invalid part: " + p); + } + this.#parts.push(p); + } + } + toJSON() { + const ret = this.type === null ? this.#parts.slice().map((p) => typeof p === "string" ? p : p.toJSON()) : [this.type, ...this.#parts.map((p) => p.toJSON())]; + if (this.isStart() && !this.type) + ret.unshift([]); + if (this.isEnd() && (this === this.#root || this.#root.#filledNegs && this.#parent?.type === "!")) { + ret.push({}); + } + return ret; + } + isStart() { + if (this.#root === this) + return true; + if (!this.#parent?.isStart()) + return false; + if (this.#parentIndex === 0) + return true; + const p = this.#parent; + for (let i = 0; i < this.#parentIndex; i++) { + const pp = p.#parts[i]; + if (!(pp instanceof _a && pp.type === "!")) { + return false; + } + } + return true; + } + isEnd() { + if (this.#root === this) + return true; + if (this.#parent?.type === "!") + return true; + if (!this.#parent?.isEnd()) + return false; + if (!this.type) + return this.#parent?.isEnd(); + const pl = this.#parent ? this.#parent.#parts.length : 0; + return this.#parentIndex === pl - 1; + } + copyIn(part) { + if (typeof part === "string") + this.push(part); + else + this.push(part.clone(this)); + } + clone(parent) { + const c = new _a(this.type, parent); + for (const p of this.#parts) { + c.copyIn(p); + } + return c; + } + static #parseAST(str, ast, pos, opt, extDepth) { + const maxDepth = opt.maxExtglobRecursion ?? 2; + let escaping = false; + let inBrace = false; + let braceStart = -1; + let braceNeg = false; + if (ast.type === null) { + let i2 = pos; + let acc2 = ""; + while (i2 < str.length) { + const c = str.charAt(i2++); + if (escaping || c === "\\") { + escaping = !escaping; + acc2 += c; + continue; + } + if (inBrace) { + if (i2 === braceStart + 1) { + if (c === "^" || c === "!") { + braceNeg = true; + } + } else if (c === "]" && !(i2 === braceStart + 2 && braceNeg)) { + inBrace = false; + } + acc2 += c; + continue; + } else if (c === "[") { + inBrace = true; + braceStart = i2; + braceNeg = false; + acc2 += c; + continue; + } + const doRecurse = !opt.noext && isExtglobType(c) && str.charAt(i2) === "(" && extDepth <= maxDepth; + if (doRecurse) { + ast.push(acc2); + acc2 = ""; + const ext2 = new _a(c, ast); + i2 = _a.#parseAST(str, ext2, i2, opt, extDepth + 1); + ast.push(ext2); + continue; + } + acc2 += c; + } + ast.push(acc2); + return i2; + } + let i = pos + 1; + let part = new _a(null, ast); + const parts = []; + let acc = ""; + while (i < str.length) { + const c = str.charAt(i++); + if (escaping || c === "\\") { + escaping = !escaping; + acc += c; + continue; + } + if (inBrace) { + if (i === braceStart + 1) { + if (c === "^" || c === "!") { + braceNeg = true; + } + } else if (c === "]" && !(i === braceStart + 2 && braceNeg)) { + inBrace = false; + } + acc += c; + continue; + } else if (c === "[") { + inBrace = true; + braceStart = i; + braceNeg = false; + acc += c; + continue; + } + const doRecurse = !opt.noext && isExtglobType(c) && str.charAt(i) === "(" && /* c8 ignore start - the maxDepth is sufficient here */ + (extDepth <= maxDepth || ast && ast.#canAdoptType(c)); + if (doRecurse) { + const depthAdd = ast && ast.#canAdoptType(c) ? 0 : 1; + part.push(acc); + acc = ""; + const ext2 = new _a(c, part); + part.push(ext2); + i = _a.#parseAST(str, ext2, i, opt, extDepth + depthAdd); + continue; + } + if (c === "|") { + part.push(acc); + acc = ""; + parts.push(part); + part = new _a(null, ast); + continue; + } + if (c === ")") { + if (acc === "" && ast.#parts.length === 0) { + ast.#emptyExt = true; + } + part.push(acc); + acc = ""; + ast.push(...parts, part); + return i; + } + acc += c; + } + ast.type = null; + ast.#hasMagic = void 0; + ast.#parts = [str.substring(pos - 1)]; + return i; + } + #canAdoptWithSpace(child) { + return this.#canAdopt(child, adoptionWithSpaceMap); + } + #canAdopt(child, map = adoptionMap) { + if (!child || typeof child !== "object" || child.type !== null || child.#parts.length !== 1 || this.type === null) { + return false; + } + const gc = child.#parts[0]; + if (!gc || typeof gc !== "object" || gc.type === null) { + return false; + } + return this.#canAdoptType(gc.type, map); + } + #canAdoptType(c, map = adoptionAnyMap) { + return !!map.get(this.type)?.includes(c); + } + #adoptWithSpace(child, index) { + const gc = child.#parts[0]; + const blank = new _a(null, gc, this.options); + blank.#parts.push(""); + gc.push(blank); + this.#adopt(child, index); + } + #adopt(child, index) { + const gc = child.#parts[0]; + this.#parts.splice(index, 1, ...gc.#parts); + for (const p of gc.#parts) { + if (typeof p === "object") + p.#parent = this; + } + this.#toString = void 0; + } + #canUsurpType(c) { + const m = usurpMap.get(this.type); + return !!m?.has(c); + } + #canUsurp(child) { + if (!child || typeof child !== "object" || child.type !== null || child.#parts.length !== 1 || this.type === null || this.#parts.length !== 1) { + return false; + } + const gc = child.#parts[0]; + if (!gc || typeof gc !== "object" || gc.type === null) { + return false; + } + return this.#canUsurpType(gc.type); + } + #usurp(child) { + const m = usurpMap.get(this.type); + const gc = child.#parts[0]; + const nt = m?.get(gc.type); + if (!nt) + return false; + this.#parts = gc.#parts; + for (const p of this.#parts) { + if (typeof p === "object") { + p.#parent = this; + } + } + this.type = nt; + this.#toString = void 0; + this.#emptyExt = false; + } + static fromGlob(pattern, options = {}) { + const ast = new _a(null, void 0, options); + _a.#parseAST(pattern, ast, 0, options, 0); + return ast; + } + // returns the regular expression if there's magic, or the unescaped + // string if not. + toMMPattern() { + if (this !== this.#root) + return this.#root.toMMPattern(); + const glob = this.toString(); + const [re, body, hasMagic, uflag] = this.toRegExpSource(); + const anyMagic = hasMagic || this.#hasMagic || this.#options.nocase && !this.#options.nocaseMagicOnly && glob.toUpperCase() !== glob.toLowerCase(); + if (!anyMagic) { + return body; + } + const flags = (this.#options.nocase ? "i" : "") + (uflag ? "u" : ""); + return Object.assign(new RegExp(`^${re}$`, flags), { + _src: re, + _glob: glob + }); + } + get options() { + return this.#options; + } + // returns the string match, the regexp source, whether there's magic + // in the regexp (so a regular expression is required) and whether or + // not the uflag is needed for the regular expression (for posix classes) + // TODO: instead of injecting the start/end at this point, just return + // the BODY of the regexp, along with the start/end portions suitable + // for binding the start/end in either a joined full-path makeRe context + // (where we bind to (^|/), or a standalone matchPart context (where + // we bind to ^, and not /). Otherwise slashes get duped! + // + // In part-matching mode, the start is: + // - if not isStart: nothing + // - if traversal possible, but not allowed: ^(?!\.\.?$) + // - if dots allowed or not possible: ^ + // - if dots possible and not allowed: ^(?!\.) + // end is: + // - if not isEnd(): nothing + // - else: $ + // + // In full-path matching mode, we put the slash at the START of the + // pattern, so start is: + // - if first pattern: same as part-matching mode + // - if not isStart(): nothing + // - if traversal possible, but not allowed: /(?!\.\.?(?:$|/)) + // - if dots allowed or not possible: / + // - if dots possible and not allowed: /(?!\.) + // end is: + // - if last pattern, same as part-matching mode + // - else nothing + // + // Always put the (?:$|/) on negated tails, though, because that has to be + // there to bind the end of the negated pattern portion, and it's easier to + // just stick it in now rather than try to inject it later in the middle of + // the pattern. + // + // We can just always return the same end, and leave it up to the caller + // to know whether it's going to be used joined or in parts. + // And, if the start is adjusted slightly, can do the same there: + // - if not isStart: nothing + // - if traversal possible, but not allowed: (?:/|^)(?!\.\.?$) + // - if dots allowed or not possible: (?:/|^) + // - if dots possible and not allowed: (?:/|^)(?!\.) + // + // But it's better to have a simpler binding without a conditional, for + // performance, so probably better to return both start options. + // + // Then the caller just ignores the end if it's not the first pattern, + // and the start always gets applied. + // + // But that's always going to be $ if it's the ending pattern, or nothing, + // so the caller can just attach $ at the end of the pattern when building. + // + // So the todo is: + // - better detect what kind of start is needed + // - return both flavors of starting pattern + // - attach $ at the end of the pattern when creating the actual RegExp + // + // Ah, but wait, no, that all only applies to the root when the first pattern + // is not an extglob. If the first pattern IS an extglob, then we need all + // that dot prevention biz to live in the extglob portions, because eg + // +(*|.x*) can match .xy but not .yx. + // + // So, return the two flavors if it's #root and the first child is not an + // AST, otherwise leave it to the child AST to handle it, and there, + // use the (?:^|/) style of start binding. + // + // Even simplified further: + // - Since the start for a join is eg /(?!\.) and the start for a part + // is ^(?!\.), we can just prepend (?!\.) to the pattern (either root + // or start or whatever) and prepend ^ or / at the Regexp construction. + toRegExpSource(allowDot) { + const dot = allowDot ?? !!this.#options.dot; + if (this.#root === this) { + this.#flatten(); + this.#fillNegs(); + } + if (!isExtglobAST(this)) { + const noEmpty = this.isStart() && this.isEnd() && !this.#parts.some((s) => typeof s !== "string"); + const src = this.#parts.map((p) => { + const [re, _, hasMagic, uflag] = typeof p === "string" ? _a.#parseGlob(p, this.#hasMagic, noEmpty) : p.toRegExpSource(allowDot); + this.#hasMagic = this.#hasMagic || hasMagic; + this.#uflag = this.#uflag || uflag; + return re; + }).join(""); + let start2 = ""; + if (this.isStart()) { + if (typeof this.#parts[0] === "string") { + const dotTravAllowed = this.#parts.length === 1 && justDots.has(this.#parts[0]); + if (!dotTravAllowed) { + const aps = addPatternStart; + const needNoTrav = ( + // dots are allowed, and the pattern starts with [ or . + dot && aps.has(src.charAt(0)) || // the pattern starts with \., and then [ or . + src.startsWith("\\.") && aps.has(src.charAt(2)) || // the pattern starts with \.\., and then [ or . + src.startsWith("\\.\\.") && aps.has(src.charAt(4)) + ); + const needNoDot = !dot && !allowDot && aps.has(src.charAt(0)); + start2 = needNoTrav ? startNoTraversal : needNoDot ? startNoDot : ""; + } + } + } + let end = ""; + if (this.isEnd() && this.#root.#filledNegs && this.#parent?.type === "!") { + end = "(?:$|\\/)"; + } + const final2 = start2 + src + end; + return [ + final2, + unescape(src), + this.#hasMagic = !!this.#hasMagic, + this.#uflag + ]; + } + const repeated = this.type === "*" || this.type === "+"; + const start = this.type === "!" ? "(?:(?!(?:" : "(?:"; + let body = this.#partsToRegExp(dot); + if (this.isStart() && this.isEnd() && !body && this.type !== "!") { + const s = this.toString(); + const me = this; + me.#parts = [s]; + me.type = null; + me.#hasMagic = void 0; + return [s, unescape(this.toString()), false, false]; + } + let bodyDotAllowed = !repeated || allowDot || dot || !startNoDot ? "" : this.#partsToRegExp(true); + if (bodyDotAllowed === body) { + bodyDotAllowed = ""; + } + if (bodyDotAllowed) { + body = `(?:${body})(?:${bodyDotAllowed})*?`; + } + let final = ""; + if (this.type === "!" && this.#emptyExt) { + final = (this.isStart() && !dot ? startNoDot : "") + starNoEmpty; + } else { + const close = this.type === "!" ? ( + // !() must match something,but !(x) can match '' + "))" + (this.isStart() && !dot && !allowDot ? startNoDot : "") + star + ")" + ) : this.type === "@" ? ")" : this.type === "?" ? ")?" : this.type === "+" && bodyDotAllowed ? ")" : this.type === "*" && bodyDotAllowed ? `)?` : `)${this.type}`; + final = start + body + close; + } + return [ + final, + unescape(body), + this.#hasMagic = !!this.#hasMagic, + this.#uflag + ]; + } + #flatten() { + if (!isExtglobAST(this)) { + for (const p of this.#parts) { + if (typeof p === "object") { + p.#flatten(); + } + } + } else { + let iterations = 0; + let done = false; + do { + done = true; + for (let i = 0; i < this.#parts.length; i++) { + const c = this.#parts[i]; + if (typeof c === "object") { + c.#flatten(); + if (this.#canAdopt(c)) { + done = false; + this.#adopt(c, i); + } else if (this.#canAdoptWithSpace(c)) { + done = false; + this.#adoptWithSpace(c, i); + } else if (this.#canUsurp(c)) { + done = false; + this.#usurp(c); + } + } + } + } while (!done && ++iterations < 10); + } + this.#toString = void 0; + } + #partsToRegExp(dot) { + return this.#parts.map((p) => { + if (typeof p === "string") { + throw new Error("string type in extglob ast??"); + } + const [re, _, _hasMagic, uflag] = p.toRegExpSource(dot); + this.#uflag = this.#uflag || uflag; + return re; + }).filter((p) => !(this.isStart() && this.isEnd()) || !!p).join("|"); + } + static #parseGlob(glob, hasMagic, noEmpty = false) { + let escaping = false; + let re = ""; + let uflag = false; + let inStar = false; + for (let i = 0; i < glob.length; i++) { + const c = glob.charAt(i); + if (escaping) { + escaping = false; + re += (reSpecials.has(c) ? "\\" : "") + c; + continue; + } + if (c === "*") { + if (inStar) + continue; + inStar = true; + re += noEmpty && /^[*]+$/.test(glob) ? starNoEmpty : star; + hasMagic = true; + continue; + } else { + inStar = false; + } + if (c === "\\") { + if (i === glob.length - 1) { + re += "\\\\"; + } else { + escaping = true; + } + continue; + } + if (c === "[") { + const [src, needUflag, consumed, magic] = parseClass(glob, i); + if (consumed) { + re += src; + uflag = uflag || needUflag; + i += consumed - 1; + hasMagic = hasMagic || magic; + continue; + } + } + if (c === "?") { + re += qmark; + hasMagic = true; + continue; + } + re += regExpEscape(c); + } + return [re, unescape(glob), !!hasMagic, uflag]; + } +}; +_a = AST; + +// node_modules/minimatch/dist/esm/escape.js +var escape = (s, { windowsPathsNoEscape = false, magicalBraces = false } = {}) => { + if (magicalBraces) { + return windowsPathsNoEscape ? s.replace(/[?*()[\]{}]/g, "[$&]") : s.replace(/[?*()[\]\\{}]/g, "\\$&"); + } + return windowsPathsNoEscape ? s.replace(/[?*()[\]]/g, "[$&]") : s.replace(/[?*()[\]\\]/g, "\\$&"); +}; + +// node_modules/minimatch/dist/esm/index.js +var minimatch = (p, pattern, options = {}) => { + assertValidPattern(pattern); + if (!options.nocomment && pattern.charAt(0) === "#") { + return false; + } + return new Minimatch(pattern, options).match(p); +}; +var starDotExtRE = /^\*+([^+@!?*[(]*)$/; +var starDotExtTest = (ext2) => (f) => !f.startsWith(".") && f.endsWith(ext2); +var starDotExtTestDot = (ext2) => (f) => f.endsWith(ext2); +var starDotExtTestNocase = (ext2) => { + ext2 = ext2.toLowerCase(); + return (f) => !f.startsWith(".") && f.toLowerCase().endsWith(ext2); +}; +var starDotExtTestNocaseDot = (ext2) => { + ext2 = ext2.toLowerCase(); + return (f) => f.toLowerCase().endsWith(ext2); +}; +var starDotStarRE = /^\*+\.\*+$/; +var starDotStarTest = (f) => !f.startsWith(".") && f.includes("."); +var starDotStarTestDot = (f) => f !== "." && f !== ".." && f.includes("."); +var dotStarRE = /^\.\*+$/; +var dotStarTest = (f) => f !== "." && f !== ".." && f.startsWith("."); +var starRE = /^\*+$/; +var starTest = (f) => f.length !== 0 && !f.startsWith("."); +var starTestDot = (f) => f.length !== 0 && f !== "." && f !== ".."; +var qmarksRE = /^\?+([^+@!?*[(]*)?$/; +var qmarksTestNocase = ([$0, ext2 = ""]) => { + const noext = qmarksTestNoExt([$0]); + if (!ext2) + return noext; + ext2 = ext2.toLowerCase(); + return (f) => noext(f) && f.toLowerCase().endsWith(ext2); +}; +var qmarksTestNocaseDot = ([$0, ext2 = ""]) => { + const noext = qmarksTestNoExtDot([$0]); + if (!ext2) + return noext; + ext2 = ext2.toLowerCase(); + return (f) => noext(f) && f.toLowerCase().endsWith(ext2); +}; +var qmarksTestDot = ([$0, ext2 = ""]) => { + const noext = qmarksTestNoExtDot([$0]); + return !ext2 ? noext : (f) => noext(f) && f.endsWith(ext2); +}; +var qmarksTest = ([$0, ext2 = ""]) => { + const noext = qmarksTestNoExt([$0]); + return !ext2 ? noext : (f) => noext(f) && f.endsWith(ext2); +}; +var qmarksTestNoExt = ([$0]) => { + const len = $0.length; + return (f) => f.length === len && !f.startsWith("."); +}; +var qmarksTestNoExtDot = ([$0]) => { + const len = $0.length; + return (f) => f.length === len && f !== "." && f !== ".."; +}; +var defaultPlatform = typeof process === "object" && process ? typeof process.env === "object" && process.env && process.env.__MINIMATCH_TESTING_PLATFORM__ || process.platform : "posix"; +var path = { + win32: { sep: "\\" }, + posix: { sep: "/" } +}; +var sep = defaultPlatform === "win32" ? path.win32.sep : path.posix.sep; +minimatch.sep = sep; +var GLOBSTAR = /* @__PURE__ */ Symbol("globstar **"); +minimatch.GLOBSTAR = GLOBSTAR; +var qmark2 = "[^/]"; +var star2 = qmark2 + "*?"; +var twoStarDot = "(?:(?!(?:\\/|^)(?:\\.{1,2})($|\\/)).)*?"; +var twoStarNoDot = "(?:(?!(?:\\/|^)\\.).)*?"; +var filter = (pattern, options = {}) => (p) => minimatch(p, pattern, options); +minimatch.filter = filter; +var ext = (a, b = {}) => Object.assign({}, a, b); +var defaults = (def) => { + if (!def || typeof def !== "object" || !Object.keys(def).length) { + return minimatch; + } + const orig = minimatch; + const m = (p, pattern, options = {}) => orig(p, pattern, ext(def, options)); + return Object.assign(m, { + Minimatch: class Minimatch extends orig.Minimatch { + constructor(pattern, options = {}) { + super(pattern, ext(def, options)); + } + static defaults(options) { + return orig.defaults(ext(def, options)).Minimatch; + } + }, + AST: class AST extends orig.AST { + /* c8 ignore start */ + constructor(type, parent, options = {}) { + super(type, parent, ext(def, options)); + } + /* c8 ignore stop */ + static fromGlob(pattern, options = {}) { + return orig.AST.fromGlob(pattern, ext(def, options)); + } + }, + unescape: (s, options = {}) => orig.unescape(s, ext(def, options)), + escape: (s, options = {}) => orig.escape(s, ext(def, options)), + filter: (pattern, options = {}) => orig.filter(pattern, ext(def, options)), + defaults: (options) => orig.defaults(ext(def, options)), + makeRe: (pattern, options = {}) => orig.makeRe(pattern, ext(def, options)), + braceExpand: (pattern, options = {}) => orig.braceExpand(pattern, ext(def, options)), + match: (list, pattern, options = {}) => orig.match(list, pattern, ext(def, options)), + sep: orig.sep, + GLOBSTAR + }); +}; +minimatch.defaults = defaults; +var braceExpand = (pattern, options = {}) => { + assertValidPattern(pattern); + if (options.nobrace || !/\{(?:(?!\{).)*\}/.test(pattern)) { + return [pattern]; + } + return expand(pattern, { max: options.braceExpandMax }); +}; +minimatch.braceExpand = braceExpand; +var makeRe = (pattern, options = {}) => new Minimatch(pattern, options).makeRe(); +minimatch.makeRe = makeRe; +var match = (list, pattern, options = {}) => { + const mm = new Minimatch(pattern, options); + list = list.filter((f) => mm.match(f)); + if (mm.options.nonull && !list.length) { + list.push(pattern); + } + return list; +}; +minimatch.match = match; +var globMagic = /[?*]|[+@!]\(.*?\)|\[|\]/; +var regExpEscape2 = (s) => s.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); +var Minimatch = class { + options; + set; + pattern; + windowsPathsNoEscape; + nonegate; + negate; + comment; + empty; + preserveMultipleSlashes; + partial; + globSet; + globParts; + nocase; + isWindows; + platform; + windowsNoMagicRoot; + maxGlobstarRecursion; + regexp; + constructor(pattern, options = {}) { + assertValidPattern(pattern); + options = options || {}; + this.options = options; + this.maxGlobstarRecursion = options.maxGlobstarRecursion ?? 200; + this.pattern = pattern; + this.platform = options.platform || defaultPlatform; + this.isWindows = this.platform === "win32"; + const awe = "allowWindowsEscape"; + this.windowsPathsNoEscape = !!options.windowsPathsNoEscape || options[awe] === false; + if (this.windowsPathsNoEscape) { + this.pattern = this.pattern.replace(/\\/g, "/"); + } + this.preserveMultipleSlashes = !!options.preserveMultipleSlashes; + this.regexp = null; + this.negate = false; + this.nonegate = !!options.nonegate; + this.comment = false; + this.empty = false; + this.partial = !!options.partial; + this.nocase = !!this.options.nocase; + this.windowsNoMagicRoot = options.windowsNoMagicRoot !== void 0 ? options.windowsNoMagicRoot : !!(this.isWindows && this.nocase); + this.globSet = []; + this.globParts = []; + this.set = []; + this.make(); + } + hasMagic() { + if (this.options.magicalBraces && this.set.length > 1) { + return true; + } + for (const pattern of this.set) { + for (const part of pattern) { + if (typeof part !== "string") + return true; + } + } + return false; + } + debug(..._) { + } + make() { + const pattern = this.pattern; + const options = this.options; + if (!options.nocomment && pattern.charAt(0) === "#") { + this.comment = true; + return; + } + if (!pattern) { + this.empty = true; + return; + } + this.parseNegate(); + this.globSet = [...new Set(this.braceExpand())]; + if (options.debug) { + this.debug = (...args) => console.error(...args); + } + this.debug(this.pattern, this.globSet); + const rawGlobParts = this.globSet.map((s) => this.slashSplit(s)); + this.globParts = this.preprocess(rawGlobParts); + this.debug(this.pattern, this.globParts); + let set = this.globParts.map((s, _, __) => { + if (this.isWindows && this.windowsNoMagicRoot) { + const isUNC = s[0] === "" && s[1] === "" && (s[2] === "?" || !globMagic.test(s[2])) && !globMagic.test(s[3]); + const isDrive = /^[a-z]:/i.test(s[0]); + if (isUNC) { + return [ + ...s.slice(0, 4), + ...s.slice(4).map((ss) => this.parse(ss)) + ]; + } else if (isDrive) { + return [s[0], ...s.slice(1).map((ss) => this.parse(ss))]; + } + } + return s.map((ss) => this.parse(ss)); + }); + this.debug(this.pattern, set); + this.set = set.filter((s) => s.indexOf(false) === -1); + if (this.isWindows) { + for (let i = 0; i < this.set.length; i++) { + const p = this.set[i]; + if (p[0] === "" && p[1] === "" && this.globParts[i][2] === "?" && typeof p[3] === "string" && /^[a-z]:$/i.test(p[3])) { + p[2] = "?"; + } + } + } + this.debug(this.pattern, this.set); + } + // various transforms to equivalent pattern sets that are + // faster to process in a filesystem walk. The goal is to + // eliminate what we can, and push all ** patterns as far + // to the right as possible, even if it increases the number + // of patterns that we have to process. + preprocess(globParts) { + if (this.options.noglobstar) { + for (const partset of globParts) { + for (let j = 0; j < partset.length; j++) { + if (partset[j] === "**") { + partset[j] = "*"; + } + } + } + } + const { optimizationLevel = 1 } = this.options; + if (optimizationLevel >= 2) { + globParts = this.firstPhasePreProcess(globParts); + globParts = this.secondPhasePreProcess(globParts); + } else if (optimizationLevel >= 1) { + globParts = this.levelOneOptimize(globParts); + } else { + globParts = this.adjascentGlobstarOptimize(globParts); + } + return globParts; + } + // just get rid of adjascent ** portions + adjascentGlobstarOptimize(globParts) { + return globParts.map((parts) => { + let gs = -1; + while (-1 !== (gs = parts.indexOf("**", gs + 1))) { + let i = gs; + while (parts[i + 1] === "**") { + i++; + } + if (i !== gs) { + parts.splice(gs, i - gs); + } + } + return parts; + }); + } + // get rid of adjascent ** and resolve .. portions + levelOneOptimize(globParts) { + return globParts.map((parts) => { + parts = parts.reduce((set, part) => { + const prev = set[set.length - 1]; + if (part === "**" && prev === "**") { + return set; + } + if (part === "..") { + if (prev && prev !== ".." && prev !== "." && prev !== "**") { + set.pop(); + return set; + } + } + set.push(part); + return set; + }, []); + return parts.length === 0 ? [""] : parts; + }); + } + levelTwoFileOptimize(parts) { + if (!Array.isArray(parts)) { + parts = this.slashSplit(parts); + } + let didSomething = false; + do { + didSomething = false; + if (!this.preserveMultipleSlashes) { + for (let i = 1; i < parts.length - 1; i++) { + const p = parts[i]; + if (i === 1 && p === "" && parts[0] === "") + continue; + if (p === "." || p === "") { + didSomething = true; + parts.splice(i, 1); + i--; + } + } + if (parts[0] === "." && parts.length === 2 && (parts[1] === "." || parts[1] === "")) { + didSomething = true; + parts.pop(); + } + } + let dd = 0; + while (-1 !== (dd = parts.indexOf("..", dd + 1))) { + const p = parts[dd - 1]; + if (p && p !== "." && p !== ".." && p !== "**" && !(this.isWindows && /^[a-z]:$/i.test(p))) { + didSomething = true; + parts.splice(dd - 1, 2); + dd -= 2; + } + } + } while (didSomething); + return parts.length === 0 ? [""] : parts; + } + // First phase: single-pattern processing + //
is 1 or more portions + //is 1 or more portions + // is any portion other than ., .., '', or ** + //
is . or '' + // + // **/.. is *brutal* for filesystem walking performance, because + // it effectively resets the recursive walk each time it occurs, + // and ** cannot be reduced out by a .. pattern part like a regexp + // or most strings (other than .., ., and '') can be. + // + // /**/..//
/
-> { /..//
/
, /**//
/
} + // // -> /+ // //../
-> /+ // **/**/ -> **/ + // + // **/*/ -> */**/ <== not valid because ** doesn't follow + // this WOULD be allowed if ** did follow symlinks, or * didn't + firstPhasePreProcess(globParts) { + let didSomething = false; + do { + didSomething = false; + for (let parts of globParts) { + let gs = -1; + while (-1 !== (gs = parts.indexOf("**", gs + 1))) { + let gss = gs; + while (parts[gss + 1] === "**") { + gss++; + } + if (gss > gs) { + parts.splice(gs + 1, gss - gs); + } + let next = parts[gs + 1]; + const p = parts[gs + 2]; + const p2 = parts[gs + 3]; + if (next !== "..") + continue; + if (!p || p === "." || p === ".." || !p2 || p2 === "." || p2 === "..") { + continue; + } + didSomething = true; + parts.splice(gs, 1); + const other = parts.slice(0); + other[gs] = "**"; + globParts.push(other); + gs--; + } + if (!this.preserveMultipleSlashes) { + for (let i = 1; i < parts.length - 1; i++) { + const p = parts[i]; + if (i === 1 && p === "" && parts[0] === "") + continue; + if (p === "." || p === "") { + didSomething = true; + parts.splice(i, 1); + i--; + } + } + if (parts[0] === "." && parts.length === 2 && (parts[1] === "." || parts[1] === "")) { + didSomething = true; + parts.pop(); + } + } + let dd = 0; + while (-1 !== (dd = parts.indexOf("..", dd + 1))) { + const p = parts[dd - 1]; + if (p && p !== "." && p !== ".." && p !== "**") { + didSomething = true; + const needDot = dd === 1 && parts[dd + 1] === "**"; + const splin = needDot ? ["."] : []; + parts.splice(dd - 1, 2, ...splin); + if (parts.length === 0) + parts.push(""); + dd -= 2; + } + } + } + } while (didSomething); + return globParts; + } + // second phase: multi-pattern dedupes + // { /*/, //
} -> /*/+ // { /, /} -> /+ // { /**/, /} -> /**/+ // + // { /**/, /**//
} -> /**/+ // ^-- not valid because ** doens't follow symlinks + secondPhasePreProcess(globParts) { + for (let i = 0; i < globParts.length - 1; i++) { + for (let j = i + 1; j < globParts.length; j++) { + const matched = this.partsMatch(globParts[i], globParts[j], !this.preserveMultipleSlashes); + if (matched) { + globParts[i] = []; + globParts[j] = matched; + break; + } + } + } + return globParts.filter((gs) => gs.length); + } + partsMatch(a, b, emptyGSMatch = false) { + let ai = 0; + let bi = 0; + let result = []; + let which = ""; + while (ai < a.length && bi < b.length) { + if (a[ai] === b[bi]) { + result.push(which === "b" ? b[bi] : a[ai]); + ai++; + bi++; + } else if (emptyGSMatch && a[ai] === "**" && b[bi] === a[ai + 1]) { + result.push(a[ai]); + ai++; + } else if (emptyGSMatch && b[bi] === "**" && a[ai] === b[bi + 1]) { + result.push(b[bi]); + bi++; + } else if (a[ai] === "*" && b[bi] && (this.options.dot || !b[bi].startsWith(".")) && b[bi] !== "**") { + if (which === "b") + return false; + which = "a"; + result.push(a[ai]); + ai++; + bi++; + } else if (b[bi] === "*" && a[ai] && (this.options.dot || !a[ai].startsWith(".")) && a[ai] !== "**") { + if (which === "a") + return false; + which = "b"; + result.push(b[bi]); + ai++; + bi++; + } else { + return false; + } + } + return a.length === b.length && result; + } + parseNegate() { + if (this.nonegate) + return; + const pattern = this.pattern; + let negate = false; + let negateOffset = 0; + for (let i = 0; i < pattern.length && pattern.charAt(i) === "!"; i++) { + negate = !negate; + negateOffset++; + } + if (negateOffset) + this.pattern = pattern.slice(negateOffset); + this.negate = negate; + } + // set partial to true to test if, for example, + // "/a/b" matches the start of "/*/b/*/d" + // Partial means, if you run out of file before you run + // out of pattern, then that's fine, as long as all + // the parts match. + matchOne(file, pattern, partial = false) { + let fileStartIndex = 0; + let patternStartIndex = 0; + if (this.isWindows) { + const fileDrive = typeof file[0] === "string" && /^[a-z]:$/i.test(file[0]); + const fileUNC = !fileDrive && file[0] === "" && file[1] === "" && file[2] === "?" && /^[a-z]:$/i.test(file[3]); + const patternDrive = typeof pattern[0] === "string" && /^[a-z]:$/i.test(pattern[0]); + const patternUNC = !patternDrive && pattern[0] === "" && pattern[1] === "" && pattern[2] === "?" && typeof pattern[3] === "string" && /^[a-z]:$/i.test(pattern[3]); + const fdi = fileUNC ? 3 : fileDrive ? 0 : void 0; + const pdi = patternUNC ? 3 : patternDrive ? 0 : void 0; + if (typeof fdi === "number" && typeof pdi === "number") { + const [fd, pd] = [ + file[fdi], + pattern[pdi] + ]; + if (fd.toLowerCase() === pd.toLowerCase()) { + pattern[pdi] = fd; + patternStartIndex = pdi; + fileStartIndex = fdi; + } + } + } + const { optimizationLevel = 1 } = this.options; + if (optimizationLevel >= 2) { + file = this.levelTwoFileOptimize(file); + } + if (pattern.includes(GLOBSTAR)) { + return this.#matchGlobstar(file, pattern, partial, fileStartIndex, patternStartIndex); + } + return this.#matchOne(file, pattern, partial, fileStartIndex, patternStartIndex); + } + #matchGlobstar(file, pattern, partial, fileIndex, patternIndex) { + const firstgs = pattern.indexOf(GLOBSTAR, patternIndex); + const lastgs = pattern.lastIndexOf(GLOBSTAR); + const [head, body, tail] = partial ? [ + pattern.slice(patternIndex, firstgs), + pattern.slice(firstgs + 1), + [] + ] : [ + pattern.slice(patternIndex, firstgs), + pattern.slice(firstgs + 1, lastgs), + pattern.slice(lastgs + 1) + ]; + if (head.length) { + const fileHead = file.slice(fileIndex, fileIndex + head.length); + if (!this.#matchOne(fileHead, head, partial, 0, 0)) { + return false; + } + fileIndex += head.length; + patternIndex += head.length; + } + let fileTailMatch = 0; + if (tail.length) { + if (tail.length + fileIndex > file.length) + return false; + let tailStart = file.length - tail.length; + if (this.#matchOne(file, tail, partial, tailStart, 0)) { + fileTailMatch = tail.length; + } else { + if (file[file.length - 1] !== "" || fileIndex + tail.length === file.length) { + return false; + } + tailStart--; + if (!this.#matchOne(file, tail, partial, tailStart, 0)) { + return false; + } + fileTailMatch = tail.length + 1; + } + } + if (!body.length) { + let sawSome = !!fileTailMatch; + for (let i2 = fileIndex; i2 < file.length - fileTailMatch; i2++) { + const f = String(file[i2]); + sawSome = true; + if (f === "." || f === ".." || !this.options.dot && f.startsWith(".")) { + return false; + } + } + return partial || sawSome; + } + const bodySegments = [[[], 0]]; + let currentBody = bodySegments[0]; + let nonGsParts = 0; + const nonGsPartsSums = [0]; + for (const b of body) { + if (b === GLOBSTAR) { + nonGsPartsSums.push(nonGsParts); + currentBody = [[], 0]; + bodySegments.push(currentBody); + } else { + currentBody[0].push(b); + nonGsParts++; + } + } + let i = bodySegments.length - 1; + const fileLength = file.length - fileTailMatch; + for (const b of bodySegments) { + b[1] = fileLength - (nonGsPartsSums[i--] + b[0].length); + } + return !!this.#matchGlobStarBodySections(file, bodySegments, fileIndex, 0, partial, 0, !!fileTailMatch); + } + // return false for "nope, not matching" + // return null for "not matching, cannot keep trying" + #matchGlobStarBodySections(file, bodySegments, fileIndex, bodyIndex, partial, globStarDepth, sawTail) { + const bs = bodySegments[bodyIndex]; + if (!bs) { + for (let i = fileIndex; i < file.length; i++) { + sawTail = true; + const f = file[i]; + if (f === "." || f === ".." || !this.options.dot && f.startsWith(".")) { + return false; + } + } + return sawTail; + } + const [body, after] = bs; + while (fileIndex <= after) { + const m = this.#matchOne(file.slice(0, fileIndex + body.length), body, partial, fileIndex, 0); + if (m && globStarDepth < this.maxGlobstarRecursion) { + const sub = this.#matchGlobStarBodySections(file, bodySegments, fileIndex + body.length, bodyIndex + 1, partial, globStarDepth + 1, sawTail); + if (sub !== false) { + return sub; + } + } + const f = file[fileIndex]; + if (f === "." || f === ".." || !this.options.dot && f.startsWith(".")) { + return false; + } + fileIndex++; + } + return partial || null; + } + #matchOne(file, pattern, partial, fileIndex, patternIndex) { + let fi; + let pi; + let pl; + let fl; + for (fi = fileIndex, pi = patternIndex, fl = file.length, pl = pattern.length; fi < fl && pi < pl; fi++, pi++) { + this.debug("matchOne loop"); + let p = pattern[pi]; + let f = file[fi]; + this.debug(pattern, p, f); + if (p === false || p === GLOBSTAR) { + return false; + } + let hit; + if (typeof p === "string") { + hit = f === p; + this.debug("string match", p, f, hit); + } else { + hit = p.test(f); + this.debug("pattern match", p, f, hit); + } + if (!hit) + return false; + } + if (fi === fl && pi === pl) { + return true; + } else if (fi === fl) { + return partial; + } else if (pi === pl) { + return fi === fl - 1 && file[fi] === ""; + } else { + throw new Error("wtf?"); + } + } + braceExpand() { + return braceExpand(this.pattern, this.options); + } + parse(pattern) { + assertValidPattern(pattern); + const options = this.options; + if (pattern === "**") + return GLOBSTAR; + if (pattern === "") + return ""; + let m; + let fastTest = null; + if (m = pattern.match(starRE)) { + fastTest = options.dot ? starTestDot : starTest; + } else if (m = pattern.match(starDotExtRE)) { + fastTest = (options.nocase ? options.dot ? starDotExtTestNocaseDot : starDotExtTestNocase : options.dot ? starDotExtTestDot : starDotExtTest)(m[1]); + } else if (m = pattern.match(qmarksRE)) { + fastTest = (options.nocase ? options.dot ? qmarksTestNocaseDot : qmarksTestNocase : options.dot ? qmarksTestDot : qmarksTest)(m); + } else if (m = pattern.match(starDotStarRE)) { + fastTest = options.dot ? starDotStarTestDot : starDotStarTest; + } else if (m = pattern.match(dotStarRE)) { + fastTest = dotStarTest; + } + const re = AST.fromGlob(pattern, this.options).toMMPattern(); + if (fastTest && typeof re === "object") { + Reflect.defineProperty(re, "test", { value: fastTest }); + } + return re; + } + makeRe() { + if (this.regexp || this.regexp === false) + return this.regexp; + const set = this.set; + if (!set.length) { + this.regexp = false; + return this.regexp; + } + const options = this.options; + const twoStar = options.noglobstar ? star2 : options.dot ? twoStarDot : twoStarNoDot; + const flags = new Set(options.nocase ? ["i"] : []); + let re = set.map((pattern) => { + const pp = pattern.map((p) => { + if (p instanceof RegExp) { + for (const f of p.flags.split("")) + flags.add(f); + } + return typeof p === "string" ? regExpEscape2(p) : p === GLOBSTAR ? GLOBSTAR : p._src; + }); + pp.forEach((p, i) => { + const next = pp[i + 1]; + const prev = pp[i - 1]; + if (p !== GLOBSTAR || prev === GLOBSTAR) { + return; + } + if (prev === void 0) { + if (next !== void 0 && next !== GLOBSTAR) { + pp[i + 1] = "(?:\\/|" + twoStar + "\\/)?" + next; + } else { + pp[i] = twoStar; + } + } else if (next === void 0) { + pp[i - 1] = prev + "(?:\\/|\\/" + twoStar + ")?"; + } else if (next !== GLOBSTAR) { + pp[i - 1] = prev + "(?:\\/|\\/" + twoStar + "\\/)" + next; + pp[i + 1] = GLOBSTAR; + } + }); + const filtered = pp.filter((p) => p !== GLOBSTAR); + if (this.partial && filtered.length >= 1) { + const prefixes = []; + for (let i = 1; i <= filtered.length; i++) { + prefixes.push(filtered.slice(0, i).join("/")); + } + return "(?:" + prefixes.join("|") + ")"; + } + return filtered.join("/"); + }).join("|"); + const [open, close] = set.length > 1 ? ["(?:", ")"] : ["", ""]; + re = "^" + open + re + close + "$"; + if (this.partial) { + re = "^(?:\\/|" + open + re.slice(1, -1) + close + ")$"; + } + if (this.negate) + re = "^(?!" + re + ").+$"; + try { + this.regexp = new RegExp(re, [...flags].join("")); + } catch { + this.regexp = false; + } + return this.regexp; + } + slashSplit(p) { + if (this.preserveMultipleSlashes) { + return p.split("/"); + } else if (this.isWindows && /^\/\/[^/]+/.test(p)) { + return ["", ...p.split(/\/+/)]; + } else { + return p.split(/\/+/); + } + } + match(f, partial = this.partial) { + this.debug("match", f, this.pattern); + if (this.comment) { + return false; + } + if (this.empty) { + return f === ""; + } + if (f === "/" && partial) { + return true; + } + const options = this.options; + if (this.isWindows) { + f = f.split("\\").join("/"); + } + const ff = this.slashSplit(f); + this.debug(this.pattern, "split", ff); + const set = this.set; + this.debug(this.pattern, "set", set); + let filename = ff[ff.length - 1]; + if (!filename) { + for (let i = ff.length - 2; !filename && i >= 0; i--) { + filename = ff[i]; + } + } + for (const pattern of set) { + let file = ff; + if (options.matchBase && pattern.length === 1) { + file = [filename]; + } + const hit = this.matchOne(file, pattern, partial); + if (hit) { + if (options.flipNegate) { + return true; + } + return !this.negate; + } + } + if (options.flipNegate) { + return false; + } + return this.negate; + } + static defaults(def) { + return minimatch.defaults(def).Minimatch; + } +}; +minimatch.AST = AST; +minimatch.Minimatch = Minimatch; +minimatch.escape = escape; +minimatch.unescape = unescape; + +// src/core/matcher.ts +var knownFileTypes = { + archive: "*.{zip,tar.gz,tgz}", + package: "*.{deb,pkg,rpm}", + linux: "*.{deb,rpm}", + macos: "*.pkg", + targz: "*.{tgz,tar.gz}" +}; +function filterByRegex(assets, pattern) { + const regex = new RegExp(pattern, "i"); + return assets.filter((asset) => regex.test(asset.name)); +} +function replacePlatformPlaceholders(pattern, platform2) { + return pattern.replace(/{{SYSTEM}}/g, platform2.systemPattern).replace(/{{ARCH}}/g, platform2.archPattern); +} +function getMatchingAsset(assets, platform2, fileName, fileType) { + if (fileName && !fileName.startsWith("~")) { + const exactMatches = assets.filter((asset) => asset.name === fileName); + if (exactMatches.length !== 1) { + throw new Error(`Expected exactly one asset to match the provided filename, matched: ${exactMatches.length}`); + } + return exactMatches[0]; + } + let fileTypeFilteredAssets = assets; + if (fileType) { + if (Object.hasOwn(knownFileTypes, fileType)) { + const fileTypeGlob = knownFileTypes[fileType]; + fileTypeFilteredAssets = assets.filter((asset) => minimatch(asset.name, fileTypeGlob, { nocase: true })); + } else if (fileType.startsWith("~")) { + const fileTypeRegex = `${fileType.substring(1)}$`; + fileTypeFilteredAssets = filterByRegex(assets, fileTypeRegex); + } else { + const extension = fileType.replace(/^\./, ""); + const fileTypeGlob = `*.${extension}`; + fileTypeFilteredAssets = assets.filter((asset) => minimatch(asset.name, fileTypeGlob, { nocase: true })); + } + } + if (fileName && fileName.startsWith("~")) { + const fileNamePattern = replacePlatformPlaceholders(fileName.substring(1), platform2); + const fileNameFilteredAssets = filterByRegex(fileTypeFilteredAssets, fileNamePattern); + if (fileNameFilteredAssets.length !== 1) { + throw new Error(`Expected exactly one asset to match the filename regex, matched: ${fileNameFilteredAssets.length}`); + } + return fileNameFilteredAssets[0]; + } + const defaultPattern = replacePlatformPlaceholders("{{SYSTEM}}[_-]{{ARCH}}", platform2); + const defaultFilteredAssets = filterByRegex(fileTypeFilteredAssets, defaultPattern); + if (defaultFilteredAssets.length !== 1) { + const errorMessage = defaultFilteredAssets.length === 0 ? `No assets matched the default criteria: ${defaultPattern}` : `Multiple assets matched the default criteria: ${defaultFilteredAssets.map((asset) => asset.name).join(", ")}`; + throw new Error(errorMessage); + } + return defaultFilteredAssets[0]; } // src/core/finder.ts var fs = __toESM(require("fs")); -var path = __toESM(require("path")); +var path2 = __toESM(require("path")); function findBinary(dir, pattern, debug, logger) { const items = fs.readdirSync(dir); if (debug) { @@ -149,7 +1917,7 @@ function findBinary(dir, pattern, debug, logger) { items.forEach((item) => logger(` - ${item}`)); } for (const item of items) { - const fullPath = path.join(dir, item); + const fullPath = path2.join(dir, item); const stat = fs.statSync(fullPath); if (stat.isDirectory()) { const found = findBinary(fullPath, pattern, debug, logger); @@ -221,11 +1989,11 @@ async function downloadAsset(url, destPath, token) { // src/core/extractor.ts var import_child_process = require("child_process"); -var path2 = __toESM(require("path")); +var path3 = __toESM(require("path")); var fs2 = __toESM(require("fs")); async function extractAsset(filePath, destDir) { - const ext = path2.extname(filePath).toLowerCase(); - const name = path2.basename(filePath).toLowerCase(); + const ext2 = path3.extname(filePath).toLowerCase(); + const name = path3.basename(filePath).toLowerCase(); if (!fs2.existsSync(destDir)) { fs2.mkdirSync(destDir, { recursive: true }); } @@ -259,7 +2027,7 @@ async function extractAsset(filePath, destDir) { throw new Error(`7z failed with status ${result.status}. Make sure 7z is installed.`); } } else { - const destPath = path2.join(destDir, path2.basename(filePath)); + const destPath = path3.join(destDir, path3.basename(filePath)); fs2.copyFileSync(filePath, destPath); } } @@ -278,7 +2046,8 @@ Options: -a, --app-name Application name (optional, for output messages) -f, --file-name Asset file name or regex pattern (prefixed with ~) -b, --binary-name Binary name (supports source:destination form) - -t, --file-type archive|package|zip|gzip|gz|tar|tar.gz|tgz|deb|pkg|rpm + -t, --file-type Known: archive|package|linux|macos|targz + Or custom: ~ (end-of-string match) or extension (e.g. zip, .tar.gz) -p, --install-path Custom installation directory -o, --output-directory Only download selected asset to the specified directory @@ -352,9 +2121,8 @@ function parseCliArgs(argv) { break; case "-t": case "--file-type": { - const fileType = ensureOptionValue(argv, i, arg).toLowerCase(); - const knownType = /^(archive|package|zip|gzip|gz|tar|tar\.gz|tgz|deb|pkg|rpm)$/i; - if (!knownType.test(fileType)) { + const fileType = ensureOptionValue(argv, i, arg); + if (!fileType.trim()) { throw new Error(`Unknown asset type: ${fileType}`); } opts.fileType = fileType; @@ -403,7 +2171,7 @@ function parseCliArgs(argv) { return opts; } function validateOutputDirectory(outputDirectory) { - const resolvedPath = path3.resolve(outputDirectory); + const resolvedPath = path4.resolve(outputDirectory); if (!fs3.existsSync(resolvedPath) || !fs3.statSync(resolvedPath).isDirectory()) { throw new Error(`Output directory "${resolvedPath}" does not exist.`); } @@ -411,24 +2179,24 @@ function validateOutputDirectory(outputDirectory) { } function getInstallDir(installPath) { if (installPath) { - return path3.resolve(installPath); + return path4.resolve(installPath); } if (process.platform === "win32") { - const localAppData = process.env.LOCALAPPDATA || path3.join(os2.homedir(), "AppData", "Local"); - return path3.join(localAppData, "bin"); + const localAppData = process.env.LOCALAPPDATA || path4.join(os2.homedir(), "AppData", "Local"); + return path4.join(localAppData, "bin"); } const isRoot = process.getuid && process.getuid() === 0; if (isRoot) { return "/usr/local/bin"; } - const homeBin = path3.join(os2.homedir(), "bin"); + const homeBin = path4.join(os2.homedir(), "bin"); if (fs3.existsSync(homeBin)) { return homeBin; } return "/usr/local/bin"; } function installSystemPackage(downloadPath) { - const fileName = path3.basename(downloadPath).toLowerCase(); + const fileName = path4.basename(downloadPath).toLowerCase(); const command = fileName.endsWith(".deb") ? { binary: "dpkg", args: ["-i", downloadPath] } : fileName.endsWith(".pkg") ? { binary: "installer", args: ["-pkg", downloadPath, "-target", "/"] } : fileName.endsWith(".rpm") ? { binary: "rpm", args: ["-i", downloadPath] } : void 0; if (!command) { throw new Error(`Unsupported package type: ${fileName}`); @@ -464,7 +2232,7 @@ async function run() { const rawRelease = await fetchLatestReleaseRaw(repository, token); const outputBase = binaryDestination || toolName; const outputName = `${outputBase}.releases.json`; - const outputPath = options.outputDirectory ? path3.join(validateOutputDirectory(options.outputDirectory), outputName) : outputName; + const outputPath = options.outputDirectory ? path4.join(validateOutputDirectory(options.outputDirectory), outputName) : outputName; fs3.writeFileSync(outputPath, rawRelease, "utf8"); console.log(`Downloaded GitHub releases to ${outputPath}.`); process.exit(0); @@ -475,10 +2243,7 @@ async function run() { }); console.log(`Fetching latest release for ${repository}...`); const release = await fetchLatestRelease(repository, token); - const asset = getMatchingAsset(release.assets, platformInfo, { - fileName: options.fileName, - fileType: options.fileType - }); + const asset = getMatchingAsset(release.assets, platformInfo, options.fileName, options.fileType); const version = release.tag_name.replace(/^v/i, ""); const downloadUrl = asset.browser_download_url; console.log(`Will download '${appName}' version: ${version}`); @@ -488,20 +2253,20 @@ async function run() { } if (options.outputDirectory) { const outputDir = validateOutputDirectory(options.outputDirectory); - const outputPath = path3.join(outputDir, path3.basename(downloadUrl)); + const outputPath = path4.join(outputDir, path4.basename(downloadUrl)); console.log(`Downloading '${appName}' version ${version} to '${outputPath}'...`); await downloadAsset(downloadUrl, outputPath, token); process.exit(0); } - tempDir = fs3.mkdtempSync(path3.join(os2.tmpdir(), "setup-gh-release-")); - const downloadPath = path3.join(tempDir, asset.name); + tempDir = fs3.mkdtempSync(path4.join(os2.tmpdir(), "setup-gh-release-")); + const downloadPath = path4.join(tempDir, asset.name); await downloadAsset(downloadUrl, downloadPath, token); if (/\.(deb|pkg|rpm)$/i.test(asset.name)) { installSystemPackage(downloadPath); console.log("Installation successful!"); process.exit(0); } - const extractDir = path3.join(tempDir, "extract"); + const extractDir = path4.join(tempDir, "extract"); console.log(`Extracting ${asset.name}...`); await extractAsset(downloadPath, extractDir); let binaryPattern; @@ -518,8 +2283,8 @@ async function run() { if (!fs3.existsSync(installDir)) { fs3.mkdirSync(installDir, { recursive: true }); } - const finalName = binaryDestination || path3.basename(binaryPath); - const destPath = path3.join(installDir, finalName); + const finalName = binaryDestination || path4.basename(binaryPath); + const destPath = path4.join(installDir, finalName); console.log(`Installing ${finalName} to ${destPath}...`); try { fs3.copyFileSync(binaryPath, destPath); diff --git a/package-lock.json b/package-lock.json index d2a83de..499d97e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,21 @@ { - "name": "setup-github-release", + "name": "install-github-release", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "setup-github-release", + "name": "install-github-release", "version": "1.0.0", "license": "MIT", "dependencies": { "@actions/core": "^1.11.0", - "@actions/tool-cache": "^2.0.2" + "@actions/tool-cache": "^2.0.2", + "minimatch": "^10.2.5" + }, + "bin": { + "check-github-token": "dist/check-token.js", + "install-github-release": "dist/cli.js" }, "devDependencies": { "@types/node": "^25.0.0", @@ -530,6 +535,27 @@ "undici-types": "~7.16.0" } }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/esbuild": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", @@ -572,6 +598,21 @@ "@esbuild/win32-x64": "0.27.2" } }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", diff --git a/package.json b/package.json index 09e0c46..e42ecc5 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,8 @@ }, "dependencies": { "@actions/core": "^1.11.0", - "@actions/tool-cache": "^2.0.2" + "@actions/tool-cache": "^2.0.2", + "minimatch": "^10.2.5" }, "devDependencies": { "@types/node": "^25.0.0", diff --git a/src/cli.ts b/src/cli.ts index c46a229..501ab4f 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -40,7 +40,8 @@ Options: -a, --app-name Application name (optional, for output messages) -f, --file-name Asset file name or regex pattern (prefixed with ~) -b, --binary-name Binary name (supports source:destination form) - -t, --file-type archive|package|zip|gzip|gz|tar|tar.gz|tgz|deb|pkg|rpm + -t, --file-type Known: archive|package|linux|macos|targz + Or custom: ~ (end-of-string match) or extension (e.g. zip, .tar.gz) -p, --install-path Custom installation directory -o, --output-directory Only download selected asset to the specified directory @@ -118,9 +119,8 @@ function parseCliArgs(argv: string[]): CliOptions { break; case '-t': case '--file-type': { - const fileType = ensureOptionValue(argv, i, arg).toLowerCase(); - const knownType = /^(archive|package|zip|gzip|gz|tar|tar\.gz|tgz|deb|pkg|rpm)$/i; - if (!knownType.test(fileType)) { + const fileType = ensureOptionValue(argv, i, arg); + if (!fileType.trim()) { throw new Error(`Unknown asset type: ${fileType}`); } opts.fileType = fileType; diff --git a/src/core/matcher.ts b/src/core/matcher.ts index 75fe8fc..5938d75 100644 --- a/src/core/matcher.ts +++ b/src/core/matcher.ts @@ -1,116 +1,76 @@ import { PlatformInfo } from './platform'; +import { minimatch } from 'minimatch'; -function normalizeCustomExtensionPattern(fileType: string): string { - let pattern = fileType; +type ReleaseAsset = { name: string; browser_download_url: string }; - if (!pattern.endsWith('$')) { - pattern += '$'; - } +const knownFileTypes: Record = { + archive: '*.{zip,tar.gz,tgz}', + package: '*.{deb,pkg,rpm}', + linux: '*.{deb,rpm}', + macos: '*.pkg', + targz: '*.{tgz,tar.gz}', +}; - if (!pattern.startsWith('\\.')) { - pattern = `\\.${pattern}`; - } - - return pattern; -} - -function getExtPattern(fileType: string | undefined, system: string): string { - const normalizedType = (fileType || '').toLowerCase(); - - if (!normalizedType) { - if (system === 'linux') { - return '\\.(deb|rpm|zip|tar\\.gz|tgz)$'; - } - if (system === 'darwin' || system === 'macos' || system === 'mac' || system === 'osx') { - return '\\.(pkg|zip|tar\\.gz|tgz)$'; - } - return '\\.(zip|tar\\.gz|tgz)$'; - } - - if (normalizedType === 'archive') { - return '\\.(zip|tar\\.gz|tgz)$'; - } - - if (normalizedType === 'package') { - return '\\.(deb|pkg|rpm)$'; - } - - const shorthandTypePatterns: Record = { - zip: '\\.(zip)$', - gzip: '\\.(tar\\.gz|tgz)$', - gz: '\\.(tar\\.gz|tgz)$', - tar: '\\.(tar)$', - 'tar.gz': '\\.(tar\\.gz)$', - tgz: '\\.(tgz)$', - deb: '\\.(deb)$', - pkg: '\\.(pkg)$', - rpm: '\\.(rpm)$' - }; - - if (shorthandTypePatterns[normalizedType]) { - return shorthandTypePatterns[normalizedType]; - } - - return normalizeCustomExtensionPattern(fileType || ''); -} - -function matchFilenameString(re: string, pi: PlatformInfo, extRe: string): string { - const hasSystem = re.includes('{{SYSTEM}}'); - const hasArch = re.includes('{{ARCH}}'); - const hasExt = re.includes('{{EXT_PATTERN}}'); - const hasEnd = re.endsWith('$'); - - const finalRe = (!hasSystem && !hasArch && !hasExt && !hasEnd) - ? `${re}.*{{SYSTEM}}[_-]{{ARCH}}.*{{EXT_PATTERN}}$` - : (hasSystem && hasArch && !hasExt && !hasEnd) - ? `${re}.*{{EXT_PATTERN}}$` - : re; - - return finalRe - .replace(/{{SYSTEM}}/g, pi.systemPattern) - .replace(/{{ARCH}}/g, pi.archPattern) - .replace(/{{EXT_PATTERN}}/g, extRe); -} - -function matchSingleAssetByRegex(assets: any[], pattern: string, noMatchError: string, multipleMatchErrorPrefix: string): any { +function filterByRegex(assets: ReleaseAsset[], pattern: string): ReleaseAsset[] { const regex = new RegExp(pattern, 'i'); - const matchingAssets = assets.filter((a: any) => regex.test(a.name)); - if (matchingAssets.length === 0) { - throw new Error(noMatchError); - } - if (matchingAssets.length > 1) { - throw new Error(`${multipleMatchErrorPrefix}: ${matchingAssets.map((a: any) => a.name).join(', ')}`); - } - return matchingAssets[0]; + return assets.filter((asset) => regex.test(asset.name)); } -export function getMatchingAsset(assets: any[], platform: PlatformInfo, fileName?: string, fileType?: string): any { - const extPattern = getExtPattern(fileType, platform.system); +function replacePlatformPlaceholders(pattern: string, platform: PlatformInfo): string { + return pattern + .replace(/{{SYSTEM}}/g, platform.systemPattern) + .replace(/{{ARCH}}/g, platform.archPattern); +} - if (!fileName || fileName.startsWith('~')) { - // Rule 1 + Rule 3: Regex-based matching rules - const pattern = !fileName - ? `${platform.systemPattern}[_-]${platform.archPattern}.*${extPattern}` - : matchFilenameString(fileName.substring(1), platform, extPattern); - const noMatchError = !fileName - ? `No assets matched the default criteria: ${pattern}` - : `No assets matched the regex: ${pattern}`; - const multipleMatchErrorPrefix = !fileName - ? 'Multiple assets matched the default criteria' - : 'Multiple assets matched the criteria'; - - return matchSingleAssetByRegex( - assets, - pattern, - noMatchError, - multipleMatchErrorPrefix - ); - } else { - // Rule 2: Literal matching rule - const asset = assets.find((a: any) => a.name === fileName); - if (!asset) { - throw new Error(`No asset found matching the exact name: ${fileName}`); +export function getMatchingAsset(assets: ReleaseAsset[], platform: PlatformInfo, fileName?: string, fileType?: string): ReleaseAsset { + // Filename provided as literal string (no ~): exact match. + if (fileName && !fileName.startsWith('~')) { + const exactMatches = assets.filter((asset) => asset.name === fileName); + if (exactMatches.length !== 1) { + throw new Error(`Expected exactly one asset to match the provided filename, matched: ${exactMatches.length}`); } - return asset; + return exactMatches[0]; } + + // Filetype filtering stage (or passthrough when not provided). + let fileTypeFilteredAssets: ReleaseAsset[] = assets; + if (fileType) { + if (Object.hasOwn(knownFileTypes, fileType)) { + // 2. Known fileType key: use predefined glob. + const fileTypeGlob = knownFileTypes[fileType]; + fileTypeFilteredAssets = assets.filter((asset) => minimatch(asset.name, fileTypeGlob, { nocase: true })); + } else if (fileType.startsWith('~')) { + // 3. Custom regex fileType: match regex at end of string. + const fileTypeRegex = `${fileType.substring(1)}$`; + fileTypeFilteredAssets = filterByRegex(assets, fileTypeRegex); + } else { + // 4. Custom extension fileType: treat as plain extension glob. + const extension = fileType.replace(/^\./, ''); + const fileTypeGlob = `*.${extension}`; + fileTypeFilteredAssets = assets.filter((asset) => minimatch(asset.name, fileTypeGlob, { nocase: true })); + } + } + + // 4. Filename provided with ~: platform placeholder expansion and regex filtering. + if (fileName && fileName.startsWith('~')) { + const fileNamePattern = replacePlatformPlaceholders(fileName.substring(1), platform); + const fileNameFilteredAssets = filterByRegex(fileTypeFilteredAssets, fileNamePattern); + if (fileNameFilteredAssets.length !== 1) { + throw new Error(`Expected exactly one asset to match the filename regex, matched: ${fileNameFilteredAssets.length}`); + } + return fileNameFilteredAssets[0]; + } + + // 5. No filename: use default {{SYSTEM}}-{{ARCH}} regex. + const defaultPattern = replacePlatformPlaceholders('{{SYSTEM}}[_-]{{ARCH}}', platform); + const defaultFilteredAssets = filterByRegex(fileTypeFilteredAssets, defaultPattern); + + // 6. Zero or multiple matches are errors. + if (defaultFilteredAssets.length !== 1) { + const errorMessage = defaultFilteredAssets.length === 0 + ? `No assets matched the default criteria: ${defaultPattern}` + : `Multiple assets matched the default criteria: ${defaultFilteredAssets.map((asset) => asset.name).join(', ')}`; + throw new Error(errorMessage); + } + return defaultFilteredAssets[0]; }