Compare commits
6 Commits
v1.2.0
...
120b16b56e
| Author | SHA1 | Date | |
|---|---|---|---|
| 120b16b56e | |||
| 4ee62009bb | |||
| b20a066030 | |||
| c8669a3a66 | |||
| 66bc2b3acd | |||
| 41c3945650 |
@@ -1,5 +1,15 @@
|
|||||||
name: Test Action
|
name: Test Action
|
||||||
on: [push, workflow_dispatch]
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- '.gitea/workflows/test.yml'
|
||||||
|
- 'action.yml'
|
||||||
|
- 'src/**'
|
||||||
|
- 'dist/**'
|
||||||
|
- 'package.json'
|
||||||
|
- 'package-lock.json'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
|
|||||||
45
.github/workflows/test.yml
vendored
Normal file
45
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
name: Test Action
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- '.github/workflows/test.yml'
|
||||||
|
- 'action.yml'
|
||||||
|
- 'src/**'
|
||||||
|
- 'dist/**'
|
||||||
|
- 'package.json'
|
||||||
|
- 'package-lock.json'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Go ACME Setup
|
||||||
|
uses: skoszewski/setup-github-release@v1
|
||||||
|
with:
|
||||||
|
repository: 'go-acme/lego'
|
||||||
|
|
||||||
|
- name: Setup Hugo
|
||||||
|
uses: skoszewski/setup-github-release@v1
|
||||||
|
with:
|
||||||
|
repository: 'gohugoio/hugo'
|
||||||
|
file-name: '~hugo_extended_[^a-z]'
|
||||||
|
|
||||||
|
- name: Setup RClone
|
||||||
|
uses: skoszewski/setup-github-release@v1
|
||||||
|
with:
|
||||||
|
repository: 'rclone/rclone'
|
||||||
|
|
||||||
|
- name: Verify Installation
|
||||||
|
run: |
|
||||||
|
echo "Verifying installed tools..."
|
||||||
|
printf "\nGo ACME Lego:\n"
|
||||||
|
lego -v
|
||||||
|
printf "\nHugo:\n"
|
||||||
|
hugo version
|
||||||
|
printf "\nRClone:\n"
|
||||||
|
rclone version
|
||||||
60
README.md
60
README.md
@@ -19,28 +19,36 @@ Add the action to your workflow. Authenticate with `github.token` (default) or a
|
|||||||
|
|
||||||
Install the CLI tool on any destination system with Node.js 24 or newer.
|
Install the CLI tool on any destination system with Node.js 24 or newer.
|
||||||
|
|
||||||
**Using npx (Quick Run):**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npx install-github-release rclone/rclone
|
|
||||||
```
|
|
||||||
|
|
||||||
**Global Installation:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install -g install-github-release
|
|
||||||
install-github-release rclone/rclone
|
|
||||||
```
|
|
||||||
|
|
||||||
**From Source:**
|
**From Source:**
|
||||||
|
|
||||||
|
1. Clone the repository:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/koszewscy/setup-github-release
|
git clone https://github.com/koszewscy/setup-github-release
|
||||||
cd setup-github-release
|
cd setup-github-release
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install dependencies and build the project:
|
||||||
|
|
||||||
|
```bash
|
||||||
npm install
|
npm install
|
||||||
npm run build
|
npm run build
|
||||||
# The CLI is available at dist/cli.js
|
```
|
||||||
node dist/cli.js <repository>
|
|
||||||
|
3. Install the tool locally:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g .
|
||||||
|
```
|
||||||
|
|
||||||
|
After installation, the tool will be available as `install-github-release`. By default, it installs binaries to:
|
||||||
|
|
||||||
|
- Linux/macOS (root): `/usr/local/bin`
|
||||||
|
- Linux/macOS (user): `~/bin` (if exists) or `/usr/local/bin`
|
||||||
|
- Windows: `%LOCALAPPDATA%\bin`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
install-github-release rclone/rclone
|
||||||
```
|
```
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
@@ -71,7 +79,7 @@ For projects with multiple binary versions, you can use a regex pattern (prefixe
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- name: Install Extended Hugo
|
- name: Install Extended Hugo
|
||||||
uses: skoszewski/setup-github-release@v1
|
uses: koszewscy/setup-github-release@v1
|
||||||
with:
|
with:
|
||||||
repository: 'gohugoio/hugo'
|
repository: 'gohugoio/hugo'
|
||||||
file-name: '~hugo_extended_[^a-z]' # Regex to match extended version
|
file-name: '~hugo_extended_[^a-z]' # Regex to match extended version
|
||||||
@@ -83,7 +91,7 @@ If the binary name is different from the repository name, like in the example of
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- name: Install GitHub CLI
|
- name: Install GitHub CLI
|
||||||
uses: skoszewski/setup-github-release@v1
|
uses: koszewscy/setup-github-release@v1
|
||||||
with:
|
with:
|
||||||
repository: 'cli/cli'
|
repository: 'cli/cli'
|
||||||
binary-name: 'gh' # Searches for 'gh' (or 'gh.exe') inside the extracted release
|
binary-name: 'gh' # Searches for 'gh' (or 'gh.exe') inside the extracted release
|
||||||
@@ -94,7 +102,7 @@ If the binary name is different from the repository name, like in the example of
|
|||||||
If you are unsure how the binary is named, use the `debug` flag to list all files in the unpacked asset, or download the asset manually to inspect its structure.
|
If you are unsure how the binary is named, use the `debug` flag to list all files in the unpacked asset, or download the asset manually to inspect its structure.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- uses: skoszewski/setup-github-release@v1
|
- uses: koszewscy/setup-github-release@v1
|
||||||
with:
|
with:
|
||||||
repository: 'owner/repo'
|
repository: 'owner/repo'
|
||||||
debug: true
|
debug: true
|
||||||
@@ -108,10 +116,13 @@ The following inputs are available for the GitHub Action, and as options for the
|
|||||||
- `file-name` (optional): The name or the regex pattern (prefixed with `~`) of the asset file to download from the release.
|
- `file-name` (optional): The name or the regex pattern (prefixed with `~`) of the asset file to download from the release.
|
||||||
- `binary-name` (optional): The name or regex pattern (prefixed with `~`) of the binary to search for within the downloaded asset. Defaults to the repository name.
|
- `binary-name` (optional): The name or regex pattern (prefixed with `~`) of the binary to search for within the downloaded asset. Defaults to the repository name.
|
||||||
- `file-type` (optional, default: 'archive'): The regex pattern to identify the type of the file to be downloaded. There are two predefined keywords:
|
- `file-type` (optional, default: 'archive'): The regex pattern to identify the type of the file to be downloaded. There are two predefined keywords:
|
||||||
|
|
||||||
- 'archive': matches common archive file extensions like .zip, .tar.gz, .tar, .tgz, .7z.
|
- 'archive': matches common archive file extensions like .zip, .tar.gz, .tar, .tgz, .7z.
|
||||||
- 'package': matches common package file extensions like .deb, .rpm, .pkg.
|
- 'package': matches common package file extensions like .deb, .rpm, .pkg.
|
||||||
- or a custom regex pattern can be provided to match specific file types.
|
- or a custom regex pattern can be provided to match specific file types.
|
||||||
- `update-cache` (optional, default: 'false'): When set to 'false', the action will use the cached version of the tool if it is already available. If set to 'true', the action will check the latest release and update the cache if a newer version is found. If set to 'always', it will always download and install, updating the cache regardless.
|
|
||||||
|
- `install-path` (optional, CLI only): Custom installation directory for the CLI tool.
|
||||||
|
- `update-cache` (optional, default: 'false', Action only): When set to 'false', the action will use the cached version of the tool if it is already available. If set to 'true', the action will check the latest release and update the cache if a newer version is found. If set to 'always', it will always download and install, updating the cache regardless.
|
||||||
- `debug` (optional, default: 'false'): When set to `true`, the action will log the contents of the unpacked directory to the console.
|
- `debug` (optional, default: 'false'): When set to `true`, the action will log the contents of the unpacked directory to the console.
|
||||||
- `token` (optional): A GitHub token for authentication, useful for accessing private repositories or increasing rate limits.
|
- `token` (optional): A GitHub token for authentication, useful for accessing private repositories or increasing rate limits.
|
||||||
|
|
||||||
@@ -131,6 +142,7 @@ Options:
|
|||||||
-f, --file-name <name> Asset file name or regex pattern (prefixed with ~)
|
-f, --file-name <name> Asset file name or regex pattern (prefixed with ~)
|
||||||
-b, --binary-name <name> Binary to search for (prefixed with ~ for regex)
|
-b, --binary-name <name> Binary to search for (prefixed with ~ for regex)
|
||||||
-t, --file-type <type> 'archive', 'package', or custom regex (default: archive)
|
-t, --file-type <type> 'archive', 'package', or custom regex (default: archive)
|
||||||
|
-p, --install-path <path> Custom installation directory
|
||||||
-k, --token <token> GitHub token
|
-k, --token <token> GitHub token
|
||||||
-d, --debug Enable debug logging
|
-d, --debug Enable debug logging
|
||||||
-h, --help Show this help message
|
-h, --help Show this help message
|
||||||
@@ -141,13 +153,15 @@ Options:
|
|||||||
The list of assets from the latest release is filtered based on the following rules:
|
The list of assets from the latest release is filtered based on the following rules:
|
||||||
|
|
||||||
1. If neither `file-name` nor `file-type` is provided, the tool defaults to selecting assets that match the following regular expression: `{{SYSTEM}}[_-]{{ARCH}}.*{{EXT_PATTERN}}$`, where:
|
1. If neither `file-name` nor `file-type` is provided, the tool defaults to selecting assets that match the following regular expression: `{{SYSTEM}}[_-]{{ARCH}}.*{{EXT_PATTERN}}$`, where:
|
||||||
- `{{SYSTEM}}` is replaced with the detected operating system regex.
|
|
||||||
- `{{ARCH}}` is replaced with the detected architecture regex.
|
- `{{SYSTEM}}` is replaced with the detected operating system regex.
|
||||||
- `{{EXT_PATTERN}}` is a regex pattern defined by the `file-type` input (defaulting to 'archive' if not specified).
|
- `{{ARCH}}` is replaced with the detected architecture regex.
|
||||||
|
- `{{EXT_PATTERN}}` is a regex pattern defined by the `file-type` input (defaulting to 'archive' if not specified).
|
||||||
|
|
||||||
2. If `file-name` is provided literally, the tool uses it directly to match the asset name by using exact string comparison.
|
2. If `file-name` is provided literally, the tool uses it directly to match the asset name by using exact string comparison.
|
||||||
|
|
||||||
3. If `file-name` is provided as a regex pattern (prefixed with `~`), then:
|
3. If `file-name` is provided as a regex pattern (prefixed with `~`), then:
|
||||||
|
|
||||||
- If the pattern does not end with `$` and does not include any placeholders, the tool appends `.*{{SYSTEM}}[_-]{{ARCH}}.*{{EXT_PATTERN}}$` to the provided pattern.
|
- If the pattern does not end with `$` and does not include any placeholders, the tool appends `.*{{SYSTEM}}[_-]{{ARCH}}.*{{EXT_PATTERN}}$` to the provided pattern.
|
||||||
- If it already ends with `$` or includes all three placeholders, the tool uses it as-is to match the asset name using regex.
|
- If it already ends with `$` or includes all three placeholders, the tool uses it as-is to match the asset name using regex.
|
||||||
- If only `{{SYSTEM}}` and `{{ARCH}}` placeholders are included, the tool appends `.*{{EXT_PATTERN}}$`.
|
- If only `{{SYSTEM}}` and `{{ARCH}}` placeholders are included, the tool appends `.*{{EXT_PATTERN}}$`.
|
||||||
@@ -161,11 +175,13 @@ The list of assets from the latest release is filtered based on the following ru
|
|||||||
7. After download and extraction, the tool recursively searches for the binary specified by `binary-name` (or the repository name). If found, the directory containing the binary is used as the tool directory and added to the PATH (or used for installation). If the binary is not found, the tool fails.
|
7. After download and extraction, the tool recursively searches for the binary specified by `binary-name` (or the repository name). If found, the directory containing the binary is used as the tool directory and added to the PATH (or used for installation). If the binary is not found, the tool fails.
|
||||||
|
|
||||||
8. `{{SYSTEM}}` is replaced with the detected operating system regex:
|
8. `{{SYSTEM}}` is replaced with the detected operating system regex:
|
||||||
|
|
||||||
- For Linux: `linux`.
|
- For Linux: `linux`.
|
||||||
- For MacOS: `(darwin|macos|mac|osx)`.
|
- For MacOS: `(darwin|macos|mac|osx)`.
|
||||||
- For Windows: `(windows|win)`.
|
- For Windows: `(windows|win)`.
|
||||||
|
|
||||||
9. `{{ARCH}}` is replaced with the detected architecture regex:
|
9. `{{ARCH}}` is replaced with the detected architecture regex:
|
||||||
|
|
||||||
- For x64: `(x86_64|x64|amd64)`.
|
- For x64: `(x86_64|x64|amd64)`.
|
||||||
- For arm64: `(aarch64|arm64)`.
|
- For arm64: `(aarch64|arm64)`.
|
||||||
|
|
||||||
|
|||||||
5
dist/cli.js
vendored
5
dist/cli.js
vendored
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
"use strict";var F=Object.create;var N=Object.defineProperty;var H=Object.getOwnPropertyDescriptor;var O=Object.getOwnPropertyNames;var U=Object.getPrototypeOf,X=Object.prototype.hasOwnProperty;var G=(t,e,n,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of O(e))!X.call(t,s)&&s!==n&&N(t,s,{get:()=>e[s],enumerable:!(r=H(e,s))||r.enumerable});return t};var f=(t,e,n)=>(n=t!=null?F(U(t)):{},G(e||!t||!t.__esModule?N(n,"default",{value:t,enumerable:!0}):n,t));var B=require("util"),h=f(require("path")),c=f(require("fs")),P=f(require("os"));var S=f(require("os")),Y={linux:"linux",darwin:"(darwin|macos|mac|osx)",win32:"(windows|win)"},q={x64:"(x86_64|x64|amd64)",arm64:"(aarch64|arm64)"};function _(){let t=S.platform(),e=S.arch();return{system:t,arch:e,systemPattern:Y[t]||t,archPattern:q[e]||e}}function M(t,e,n){let{fileName:r,fileType:s="archive"}=n,o;if(s==="archive"?o="\\.(zip|tar\\.gz|tar|tgz|7z)":s==="package"?o="\\.(deb|rpm|pkg)":o=s,r)if(r.startsWith("~")){let i=r.substring(1),l=i.includes("{{SYSTEM}}"),a=i.includes("{{ARCH}}"),p=i.includes("{{EXT_PATTERN}}"),$=i.endsWith("$");!l&&!a&&!p&&!$?i+=".*{{SYSTEM}}[_-]{{ARCH}}.*{{EXT_PATTERN}}$":l&&a&&!p&&!$&&(i+=".*{{EXT_PATTERN}}$");let m=i.replace(/{{SYSTEM}}/g,e.systemPattern).replace(/{{ARCH}}/g,e.archPattern).replace(/{{EXT_PATTERN}}/g,o),x=new RegExp(m,"i"),g=t.filter(d=>x.test(d.name));if(g.length===0)throw new Error(`No assets matched the regex: ${m}`);if(g.length>1)throw new Error(`Multiple assets matched the criteria: ${g.map(d=>d.name).join(", ")}`);return g[0]}else{let i=t.find(l=>l.name===r);if(!i)throw new Error(`No asset found matching the exact name: ${r}`);return i}else{let i=`${e.systemPattern}[_-]${e.archPattern}.*${o}$`,l=new RegExp(i,"i"),a=t.filter(p=>l.test(p.name));if(a.length===0)throw new Error(`No assets matched the default criteria: ${i}`);if(a.length>1)throw new Error(`Multiple assets matched the default criteria: ${a.map(p=>p.name).join(", ")}`);return a[0]}}var b=f(require("fs")),j=f(require("path"));function k(t,e,n,r){let s=b.readdirSync(t);n&&(r(`Searching for binary in ${t}...`),s.forEach(o=>r(` - ${o}`)));for(let o of s){let i=j.join(t,o);if(b.statSync(i).isDirectory()){let a=k(i,e,n,r);if(a)return a}else{let a=!1;if(e instanceof RegExp?a=e.test(o):(a=o===e,!a&&process.platform==="win32"&&!e.toLowerCase().endsWith(".exe")&&(a=o.toLowerCase()===`${e.toLowerCase()}.exe`)),a)return i}}}async function C(t,e){let n=`https://api.github.com/repos/${t}/releases/latest`,r={Accept:"application/vnd.github.v3+json","User-Agent":"setup-github-release-action"};e&&(r.Authorization=`token ${e}`);let s=await fetch(n,{headers:r});if(!s.ok){let o=await s.text();throw new Error(`Failed to fetch latest release for ${t}: ${s.statusText}. ${o}`)}return await s.json()}async function W(t,e,n){let r={"User-Agent":"setup-github-release-action"};n&&(r.Authorization=`token ${n}`);let s=await fetch(t,{headers:r});if(!s.ok)throw new Error(`Failed to download asset: ${s.statusText}`);let o=await import("fs"),{Readable:i}=await import("stream"),{finished:l}=await import("stream/promises"),a=o.createWriteStream(e);await l(i.fromWeb(s.body).pipe(a))}var E=require("child_process"),u=f(require("path")),y=f(require("fs"));async function L(t,e){let n=u.extname(t).toLowerCase(),r=u.basename(t).toLowerCase();if(y.existsSync(e)||y.mkdirSync(e,{recursive:!0}),r.endsWith(".tar.gz")||r.endsWith(".tgz")||r.endsWith(".tar")){let o=(0,E.spawnSync)("tar",["-xf",t,"-C",e]);if(o.status!==0)throw new Error(`tar failed with status ${o.status}: ${o.stderr.toString()}`)}else if(r.endsWith(".zip"))if(process.platform==="win32"){let s=`Expand-Archive -Path "${t}" -DestinationPath "${e}" -Force`,o=(0,E.spawnSync)("powershell",["-Command",s]);if(o.status!==0)throw new Error(`powershell Expand-Archive failed with status ${o.status}: ${o.stderr.toString()}`)}else{let s=(0,E.spawnSync)("unzip",["-q",t,"-d",e]);if(s.status!==0)throw new Error(`unzip failed with status ${s.status}: ${s.stderr.toString()}`)}else if(r.endsWith(".7z")){let s=(0,E.spawnSync)("7z",["x",t,`-o${e}`,"-y"]);if(s.status!==0)throw new Error(`7z failed with status ${s.status}. Make sure 7z is installed.`)}else{let s=u.join(e,u.basename(t));y.copyFileSync(t,s)}}async function K(){let{values:t,positionals:e}=(0,B.parseArgs)({options:{"file-name":{type:"string",short:"f"},"binary-name":{type:"string",short:"b"},"file-type":{type:"string",short:"t",default:"archive"},token:{type:"string",short:"k"},debug:{type:"boolean",short:"d",default:!1},help:{type:"boolean",short:"h"}},allowPositionals:!0});(t.help||e.length===0)&&(console.log(`
|
"use strict";var O=Object.create;var z=Object.defineProperty;var H=Object.getOwnPropertyDescriptor;var U=Object.getOwnPropertyNames;var D=Object.getPrototypeOf,X=Object.prototype.hasOwnProperty;var Y=(t,e,i,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of U(e))!X.call(t,s)&&s!==i&&z(t,s,{get:()=>e[s],enumerable:!(r=H(e,s))||r.enumerable});return t};var p=(t,e,i)=>(i=t!=null?O(D(t)):{},Y(e||!t||!t.__esModule?z(i,"default",{value:t,enumerable:!0}):i,t));var F=require("util"),f=p(require("path")),l=p(require("fs")),P=p(require("os"));var T=p(require("os")),G={linux:"linux",darwin:"(darwin|macos|mac|osx)",win32:"(windows|win)"},q={x64:"(x86_64|x64|amd64)",arm64:"(aarch64|arm64)"};function M(){let t=T.platform(),e=T.arch();return{system:t,arch:e,systemPattern:G[t]||t,archPattern:q[e]||e}}function j(t,e,i){let{fileName:r,fileType:s="archive"}=i,o;if(s==="archive"?o="\\.(zip|tar\\.gz|tar|tgz|7z)":s==="package"?o="\\.(deb|rpm|pkg)":o=s,r)if(r.startsWith("~")){let a=r.substring(1),c=a.includes("{{SYSTEM}}"),n=a.includes("{{ARCH}}"),m=a.includes("{{EXT_PATTERN}}"),S=a.endsWith("$");!c&&!n&&!m&&!S?a+=".*{{SYSTEM}}[_-]{{ARCH}}.*{{EXT_PATTERN}}$":c&&n&&!m&&!S&&(a+=".*{{EXT_PATTERN}}$");let h=a.replace(/{{SYSTEM}}/g,e.systemPattern).replace(/{{ARCH}}/g,e.archPattern).replace(/{{EXT_PATTERN}}/g,o),b=new RegExp(h,"i"),u=t.filter(w=>b.test(w.name));if(u.length===0)throw new Error(`No assets matched the regex: ${h}`);if(u.length>1)throw new Error(`Multiple assets matched the criteria: ${u.map(w=>w.name).join(", ")}`);return u[0]}else{let a=t.find(c=>c.name===r);if(!a)throw new Error(`No asset found matching the exact name: ${r}`);return a}else{let a=`${e.systemPattern}[_-]${e.archPattern}.*${o}$`,c=new RegExp(a,"i"),n=t.filter(m=>c.test(m.name));if(n.length===0)throw new Error(`No assets matched the default criteria: ${a}`);if(n.length>1)throw new Error(`Multiple assets matched the default criteria: ${n.map(m=>m.name).join(", ")}`);return n[0]}}var A=p(require("fs")),_=p(require("path"));function I(t,e,i,r){let s=A.readdirSync(t);i&&(r(`Searching for binary in ${t}...`),s.forEach(o=>r(` - ${o}`)));for(let o of s){let a=_.join(t,o);if(A.statSync(a).isDirectory()){let n=I(a,e,i,r);if(n)return n}else{let n=!1;if(e instanceof RegExp?n=e.test(o):(n=o===e,!n&&process.platform==="win32"&&!e.toLowerCase().endsWith(".exe")&&(n=o.toLowerCase()===`${e.toLowerCase()}.exe`)),n)return a}}}async function W(t,e){let i=`https://api.github.com/repos/${t}/releases/latest`,r={Accept:"application/vnd.github.v3+json","User-Agent":"setup-github-release-action"};e&&(r.Authorization=`token ${e}`);let s=await fetch(i,{headers:r});if(!s.ok){let o=await s.text();throw new Error(`Failed to fetch latest release for ${t}: ${s.statusText}. ${o}`)}return await s.json()}async function L(t,e,i){let r={"User-Agent":"setup-github-release-action"};i&&(r.Authorization=`token ${i}`);let s=await fetch(t,{headers:r});if(!s.ok)throw new Error(`Failed to download asset: ${s.statusText}`);let o=await import("fs"),{Readable:a}=await import("stream"),{finished:c}=await import("stream/promises"),n=o.createWriteStream(e);await c(a.fromWeb(s.body).pipe(n))}var x=require("child_process"),y=p(require("path")),E=p(require("fs"));async function B(t,e){let i=y.extname(t).toLowerCase(),r=y.basename(t).toLowerCase();if(E.existsSync(e)||E.mkdirSync(e,{recursive:!0}),r.endsWith(".tar.gz")||r.endsWith(".tgz")||r.endsWith(".tar")){let o=(0,x.spawnSync)("tar",["-xf",t,"-C",e]);if(o.status!==0)throw new Error(`tar failed with status ${o.status}: ${o.stderr.toString()}`)}else if(r.endsWith(".zip"))if(process.platform==="win32"){if((0,x.spawnSync)("tar",["-xf",t,"-C",e]).status===0)return;let o=t.replace(/'/g,"''"),a=e.replace(/'/g,"''"),c=`Add-Type -AssemblyName System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::ExtractToDirectory('${o}', '${a}')`;for(let n of["pwsh","powershell"])if((0,x.spawnSync)(n,["-NoProfile","-ExecutionPolicy","Bypass","-Command",c]).status===0)return;throw new Error("Extraction failed: Both tar and PowerShell fallback failed. Make sure your system can extract ZIP files.")}else{let s=(0,x.spawnSync)("unzip",["-q",t,"-d",e]);if(s.status!==0)throw new Error(`unzip failed with status ${s.status}: ${s.stderr.toString()}`)}else if(r.endsWith(".7z")){let s=(0,x.spawnSync)("7z",["x",t,`-o${e}`,"-y"]);if(s.status!==0)throw new Error(`7z failed with status ${s.status}. Make sure 7z is installed.`)}else{let s=y.join(e,y.basename(t));E.copyFileSync(t,s)}}async function Z(){let{values:t,positionals:e}=(0,F.parseArgs)({options:{"file-name":{type:"string",short:"f"},"binary-name":{type:"string",short:"b"},"file-type":{type:"string",short:"t",default:"archive"},"install-path":{type:"string",short:"p"},token:{type:"string",short:"k"},debug:{type:"boolean",short:"d",default:!1},help:{type:"boolean",short:"h"}},allowPositionals:!0});(t.help||e.length===0)&&(console.log(`
|
||||||
Usage: install-github-release [options] <repository>
|
Usage: install-github-release [options] <repository>
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
@@ -9,7 +9,8 @@ Options:
|
|||||||
-f, --file-name <name> Asset file name or regex pattern (prefixed with ~)
|
-f, --file-name <name> Asset file name or regex pattern (prefixed with ~)
|
||||||
-b, --binary-name <name> Binary to search for (prefixed with ~ for regex)
|
-b, --binary-name <name> Binary to search for (prefixed with ~ for regex)
|
||||||
-t, --file-type <type> 'archive', 'package', or custom regex (default: archive)
|
-t, --file-type <type> 'archive', 'package', or custom regex (default: archive)
|
||||||
|
-p, --install-path <path> Custom installation directory
|
||||||
-k, --token <token> GitHub token
|
-k, --token <token> GitHub token
|
||||||
-d, --debug Enable debug logging
|
-d, --debug Enable debug logging
|
||||||
-h, --help Show this help message
|
-h, --help Show this help message
|
||||||
`),process.exit(0));let n=e[0];n||(console.error("Error: Repository is required."),process.exit(1));let r=t["file-name"],s=t["binary-name"],o=t["file-type"],i=!!t.debug,l=t.token||process.env.GITHUB_TOKEN;try{let a=_(),p=n.split("/").pop()||n;console.log(`Fetching latest release for ${n}...`);let $=await C(n,l),m=M($.assets,a,{fileName:r,fileType:o});console.log(`Selected asset: ${m.name}`);let x=c.mkdtempSync(h.join(P.tmpdir(),"setup-gh-release-")),g=h.join(x,m.name);console.log(`Downloading ${m.name}...`),await W(m.browser_download_url,g,l);let d=h.join(x,"extract");console.log(`Extracting ${m.name}...`),await L(g,d);let A=s||p,R;A.startsWith("~")?R=new RegExp(A.substring(1),"i"):R=A;let T=k(d,R,i,console.log);if(!T)throw new Error(`Could not find binary "${A}" in the extracted asset.`);let w;if(process.getuid&&process.getuid()===0)w="/usr/local/bin";else{let z=h.join(P.homedir(),"bin");c.existsSync(z)?w=z:w="/usr/local/bin"}c.existsSync(w)||c.mkdirSync(w,{recursive:!0});let I=h.basename(T),v=h.join(w,I);console.log(`Installing ${I} to ${v}...`),c.copyFileSync(T,v),process.platform!=="win32"&&c.chmodSync(v,"755"),c.rmSync(x,{recursive:!0,force:!0}),console.log("Installation successful!")}catch(a){console.error(`Error: ${a.message}`),process.exit(1)}}K();
|
`),process.exit(0));let i=e[0];i||(console.error("Error: Repository is required."),process.exit(1));let r=t["file-name"],s=t["binary-name"],o=t["file-type"],a=!!t.debug,c=t.token||process.env.GITHUB_TOKEN;try{let n=M(),m=i.split("/").pop()||i;console.log(`Fetching latest release for ${i}...`);let S=await W(i,c),h=j(S.assets,n,{fileName:r,fileType:o});console.log(`Selected asset: ${h.name}`);let b=l.mkdtempSync(f.join(P.tmpdir(),"setup-gh-release-")),u=f.join(b,h.name);console.log(`Downloading ${h.name}...`),await L(h.browser_download_url,u,c);let w=f.join(b,"extract");console.log(`Extracting ${h.name}...`),await B(u,w);let R=s||m,k;R.startsWith("~")?k=new RegExp(R.substring(1),"i"):k=R;let C=I(w,k,a,console.log);if(!C)throw new Error(`Could not find binary "${R}" in the extracted asset.`);let g;if(t["install-path"])g=f.resolve(t["install-path"]);else if(process.platform==="win32"){let d=process.env.LOCALAPPDATA||f.join(P.homedir(),"AppData","Local");g=f.join(d,"bin")}else if(process.getuid&&process.getuid()===0)g="/usr/local/bin";else{let N=f.join(P.homedir(),"bin");l.existsSync(N)?g=N:g="/usr/local/bin"}l.existsSync(g)||l.mkdirSync(g,{recursive:!0});let v=f.basename(C),$=f.join(g,v);console.log(`Installing ${v} to ${$}...`);try{l.copyFileSync(C,$)}catch(d){throw d.code==="EBUSY"?new Error(`The file ${$} is currently in use. Please close any running instances and try again.`):d.code==="EACCES"||d.code==="EPERM"?new Error(`Permission denied while installing to ${$}. Try running with sudo or as administrator, or use -p to specify a custom path.`):d}process.platform!=="win32"&&l.chmodSync($,"755"),l.rmSync(b,{recursive:!0,force:!0}),console.log("Installation successful!")}catch(n){console.error(`Error: ${n.message}`),process.exit(1)}}Z();
|
||||||
|
|||||||
39
src/cli.ts
39
src/cli.ts
@@ -14,6 +14,7 @@ async function run() {
|
|||||||
'file-name': { type: 'string', short: 'f' },
|
'file-name': { type: 'string', short: 'f' },
|
||||||
'binary-name': { type: 'string', short: 'b' },
|
'binary-name': { type: 'string', short: 'b' },
|
||||||
'file-type': { type: 'string', short: 't', default: 'archive' },
|
'file-type': { type: 'string', short: 't', default: 'archive' },
|
||||||
|
'install-path': { type: 'string', short: 'p' },
|
||||||
'token': { type: 'string', short: 'k' },
|
'token': { type: 'string', short: 'k' },
|
||||||
'debug': { type: 'boolean', short: 'd', default: false },
|
'debug': { type: 'boolean', short: 'd', default: false },
|
||||||
'help': { type: 'boolean', short: 'h' }
|
'help': { type: 'boolean', short: 'h' }
|
||||||
@@ -32,6 +33,7 @@ Options:
|
|||||||
-f, --file-name <name> Asset file name or regex pattern (prefixed with ~)
|
-f, --file-name <name> Asset file name or regex pattern (prefixed with ~)
|
||||||
-b, --binary-name <name> Binary to search for (prefixed with ~ for regex)
|
-b, --binary-name <name> Binary to search for (prefixed with ~ for regex)
|
||||||
-t, --file-type <type> 'archive', 'package', or custom regex (default: archive)
|
-t, --file-type <type> 'archive', 'package', or custom regex (default: archive)
|
||||||
|
-p, --install-path <path> Custom installation directory
|
||||||
-k, --token <token> GitHub token
|
-k, --token <token> GitHub token
|
||||||
-d, --debug Enable debug logging
|
-d, --debug Enable debug logging
|
||||||
-h, --help Show this help message
|
-h, --help Show this help message
|
||||||
@@ -88,17 +90,26 @@ Options:
|
|||||||
|
|
||||||
// Determine install directory
|
// Determine install directory
|
||||||
let installDir: string;
|
let installDir: string;
|
||||||
const isRoot = process.getuid && process.getuid() === 0;
|
|
||||||
|
|
||||||
if (isRoot) {
|
if (values['install-path']) {
|
||||||
installDir = '/usr/local/bin';
|
installDir = path.resolve(values['install-path']);
|
||||||
} else {
|
} else {
|
||||||
const homeBin = path.join(os.homedir(), 'bin');
|
if (process.platform === 'win32') {
|
||||||
if (fs.existsSync(homeBin)) {
|
const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
||||||
installDir = homeBin;
|
installDir = path.join(localAppData, 'bin');
|
||||||
} else {
|
} else {
|
||||||
// Fallback or error? Let's use a local bin if possible or /usr/local/bin (might fail)
|
const isRoot = process.getuid && process.getuid() === 0;
|
||||||
installDir = '/usr/local/bin';
|
|
||||||
|
if (isRoot) {
|
||||||
|
installDir = '/usr/local/bin';
|
||||||
|
} else {
|
||||||
|
const homeBin = path.join(os.homedir(), 'bin');
|
||||||
|
if (fs.existsSync(homeBin)) {
|
||||||
|
installDir = homeBin;
|
||||||
|
} else {
|
||||||
|
installDir = '/usr/local/bin';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,7 +121,17 @@ Options:
|
|||||||
const destPath = path.join(installDir, finalName);
|
const destPath = path.join(installDir, finalName);
|
||||||
|
|
||||||
console.log(`Installing ${finalName} to ${destPath}...`);
|
console.log(`Installing ${finalName} to ${destPath}...`);
|
||||||
fs.copyFileSync(binaryPath, destPath);
|
try {
|
||||||
|
fs.copyFileSync(binaryPath, destPath);
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err.code === 'EBUSY') {
|
||||||
|
throw new Error(`The file ${destPath} is currently in use. Please close any running instances and try again.`);
|
||||||
|
}
|
||||||
|
if (err.code === 'EACCES' || err.code === 'EPERM') {
|
||||||
|
throw new Error(`Permission denied while installing to ${destPath}. Try running with sudo or as administrator, or use -p to specify a custom path.`);
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
if (process.platform !== 'win32') {
|
if (process.platform !== 'win32') {
|
||||||
fs.chmodSync(destPath, '755');
|
fs.chmodSync(destPath, '755');
|
||||||
|
|||||||
@@ -18,11 +18,23 @@ export async function extractAsset(filePath: string, destDir: string): Promise<v
|
|||||||
}
|
}
|
||||||
} else if (name.endsWith('.zip')) {
|
} else if (name.endsWith('.zip')) {
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
const command = `Expand-Archive -Path "${filePath}" -DestinationPath "${destDir}" -Force`;
|
// Modern Windows 10/11 has tar that handles zip
|
||||||
const result = spawnSync('powershell', ['-Command', command]);
|
const tarResult = spawnSync('tar', ['-xf', filePath, '-C', destDir]);
|
||||||
if (result.status !== 0) {
|
if (tarResult.status === 0) return;
|
||||||
throw new Error(`powershell Expand-Archive failed with status ${result.status}: ${result.stderr.toString()}`);
|
|
||||||
|
// Fallback: Use .NET ZipFile class to bypass PowerShell module trust issues (Microsoft.PowerShell.Archive)
|
||||||
|
// We escape single quotes for PowerShell.
|
||||||
|
const escapedFilePath = filePath.replace(/'/g, "''");
|
||||||
|
const escapedDestDir = destDir.replace(/'/g, "''");
|
||||||
|
const dotNetCommand = `Add-Type -AssemblyName System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::ExtractToDirectory('${escapedFilePath}', '${escapedDestDir}')`;
|
||||||
|
|
||||||
|
// Try pwsh (PowerShell 7) then powershell (Windows PowerShell)
|
||||||
|
for (const shell of ['pwsh', 'powershell']) {
|
||||||
|
const result = spawnSync(shell, ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', dotNetCommand]);
|
||||||
|
if (result.status === 0) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw new Error(`Extraction failed: Both tar and PowerShell fallback failed. Make sure your system can extract ZIP files.`);
|
||||||
} else {
|
} else {
|
||||||
const result = spawnSync('unzip', ['-q', filePath, '-d', destDir]);
|
const result = spawnSync('unzip', ['-q', filePath, '-d', destDir]);
|
||||||
if (result.status !== 0) {
|
if (result.status !== 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user