redminectl 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -0
- package/bin/redmine +18 -0
- package/bin/redmine.exe +0 -0
- package/package.json +41 -0
- package/scripts/download.js +142 -0
- package/scripts/install.ps1 +177 -0
- package/scripts/install.sh +150 -0
package/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Redmine CLI
|
|
2
|
+
|
|
3
|
+
AI Agent 友好的 Redmine 命令行工具。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
### 方式一:一键安装(推荐)
|
|
8
|
+
|
|
9
|
+
**macOS / Linux:**
|
|
10
|
+
```bash
|
|
11
|
+
curl -fsSL https://raw.githubusercontent.com/largeoliu/redmine-cli/main/scripts/install.sh | sh
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
**Windows (PowerShell):**
|
|
15
|
+
```powershell
|
|
16
|
+
irm https://raw.githubusercontent.com/largeoliu/redmine-cli/main/scripts/install.ps1 | iex
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### 方式二:npm 安装
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install -g rmcli
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### 方式三:手动下载
|
|
26
|
+
|
|
27
|
+
从 [GitHub Releases](https://github.com/largeoliu/redmine-cli/releases) 下载对应平台的预编译二进制。
|
|
28
|
+
|
|
29
|
+
### 方式四:从源码构建
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
go install github.com/largeoliu/redmine-cli@latest
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## 快速开始
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# 登录
|
|
39
|
+
redmine login
|
|
40
|
+
|
|
41
|
+
# 列出 Issues
|
|
42
|
+
redmine issue list
|
|
43
|
+
|
|
44
|
+
# 创建 Issue
|
|
45
|
+
redmine issue create --project 1 --subject "Bug report"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## 文档
|
|
49
|
+
|
|
50
|
+
- [设计文档](docs/specs/2026-04-12-redmine-cli-design.md)
|
package/bin/redmine
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { spawn } = require('child_process');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
|
|
7
|
+
const binDir = path.join(__dirname, '..', 'bin');
|
|
8
|
+
const binaryName = os.platform() === 'win32' ? 'redmine.exe' : 'redmine';
|
|
9
|
+
const binaryPath = path.join(binDir, binaryName);
|
|
10
|
+
|
|
11
|
+
const child = spawn(binaryPath, process.argv.slice(2), {
|
|
12
|
+
stdio: 'inherit',
|
|
13
|
+
env: process.env
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
child.on('exit', (code) => {
|
|
17
|
+
process.exit(code || 0);
|
|
18
|
+
});
|
package/bin/redmine.exe
ADDED
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "redminectl",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "AI Agent friendly Redmine CLI tool",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"redmine": "./bin/redmine"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"postinstall": "node scripts/download.js"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/largeoliu/redmine-cli.git"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"redmine",
|
|
18
|
+
"cli",
|
|
19
|
+
"issue-tracker",
|
|
20
|
+
"project-management",
|
|
21
|
+
"ai-agent"
|
|
22
|
+
],
|
|
23
|
+
"author": "largeoliu",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/largeoliu/redmine-cli/issues"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/largeoliu/redmine-cli#readme",
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=14"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"bin",
|
|
34
|
+
"scripts"
|
|
35
|
+
],
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"axios": "^1.6.0",
|
|
38
|
+
"unzipper": "^0.10.14",
|
|
39
|
+
"tar": "^6.2.0"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const https = require('https');
|
|
4
|
+
const http = require('http');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { pipeline } = require('stream');
|
|
8
|
+
const { promisify } = require('util');
|
|
9
|
+
const streamPipeline = promisify(pipeline);
|
|
10
|
+
|
|
11
|
+
const REPO = 'largeoliu/redmine-cli';
|
|
12
|
+
const BINARY_NAME = 'redmine';
|
|
13
|
+
|
|
14
|
+
function getPlatform() {
|
|
15
|
+
switch (process.platform) {
|
|
16
|
+
case 'win32': return 'windows';
|
|
17
|
+
case 'darwin': return 'darwin';
|
|
18
|
+
case 'linux': return 'linux';
|
|
19
|
+
default: throw new Error(`Unsupported platform: ${process.platform}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getArch() {
|
|
24
|
+
switch (process.arch) {
|
|
25
|
+
case 'x64': return 'amd64';
|
|
26
|
+
case 'arm64': return 'arm64';
|
|
27
|
+
default: throw new Error(`Unsupported architecture: ${process.arch}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function fetchJson(url) {
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
https.get(url, { headers: { 'User-Agent': 'redmine-cli-npm' } }, (res) => {
|
|
34
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
35
|
+
return fetchJson(res.headers.location).then(resolve).catch(reject);
|
|
36
|
+
}
|
|
37
|
+
if (res.statusCode !== 200) {
|
|
38
|
+
return reject(new Error(`HTTP ${res.statusCode}`));
|
|
39
|
+
}
|
|
40
|
+
let data = '';
|
|
41
|
+
res.on('data', chunk => data += chunk);
|
|
42
|
+
res.on('end', () => {
|
|
43
|
+
try {
|
|
44
|
+
resolve(JSON.parse(data));
|
|
45
|
+
} catch (e) {
|
|
46
|
+
reject(e);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}).on('error', reject);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function getLatestVersion() {
|
|
54
|
+
const releases = await fetchJson(`https://api.github.com/repos/${REPO}/releases/latest`);
|
|
55
|
+
return releases.tag_name;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function downloadFile(url, dest) {
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
const protocol = url.startsWith('https') ? https : http;
|
|
61
|
+
protocol.get(url, (res) => {
|
|
62
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
63
|
+
return downloadFile(res.headers.location, dest).then(resolve).catch(reject);
|
|
64
|
+
}
|
|
65
|
+
if (res.statusCode !== 200) {
|
|
66
|
+
return reject(new Error(`HTTP ${res.statusCode}`));
|
|
67
|
+
}
|
|
68
|
+
const file = fs.createWriteStream(dest);
|
|
69
|
+
streamPipeline(res, file).then(resolve).catch(reject);
|
|
70
|
+
}).on('error', reject);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function extractArchive(archivePath, destDir, isZip) {
|
|
75
|
+
const targetDir = path.dirname(destDir);
|
|
76
|
+
|
|
77
|
+
if (isZip) {
|
|
78
|
+
const unzipper = require('unzipper');
|
|
79
|
+
await new Promise((resolve, reject) => {
|
|
80
|
+
fs.createReadStream(archivePath)
|
|
81
|
+
.pipe(unzipper.Extract({ path: targetDir }))
|
|
82
|
+
.on('close', resolve)
|
|
83
|
+
.on('error', reject);
|
|
84
|
+
});
|
|
85
|
+
} else {
|
|
86
|
+
const tar = require('tar');
|
|
87
|
+
await tar.x({ file: archivePath, cwd: targetDir });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function main() {
|
|
92
|
+
const binDir = path.join(__dirname, '..', 'bin');
|
|
93
|
+
const platform = getPlatform();
|
|
94
|
+
const arch = getArch();
|
|
95
|
+
|
|
96
|
+
console.log(`[INFO] Installing redmine-cli for ${platform}/${arch}...`);
|
|
97
|
+
|
|
98
|
+
let version;
|
|
99
|
+
try {
|
|
100
|
+
version = await getLatestVersion();
|
|
101
|
+
console.log(`[INFO] Latest version: ${version}`);
|
|
102
|
+
} catch (e) {
|
|
103
|
+
console.error('[ERROR] Failed to get latest version:', e.message);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const ext = platform === 'windows' ? 'zip' : 'tar.gz';
|
|
108
|
+
const archiveName = `${BINARY_NAME}_${version.replace('v', '')}_${platform}_${arch}.${ext}`;
|
|
109
|
+
const downloadUrl = `https://github.com/${REPO}/releases/download/${version}/${archiveName}`;
|
|
110
|
+
|
|
111
|
+
const tmpDir = fs.mkdtempSync(path.join(require('os').tmpdir(), 'redmine-cli-'));
|
|
112
|
+
const archivePath = path.join(tmpDir, archiveName);
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
console.log(`[INFO] Downloading ${archiveName}...`);
|
|
116
|
+
await downloadFile(downloadUrl, archivePath);
|
|
117
|
+
|
|
118
|
+
console.log('[INFO] Extracting...');
|
|
119
|
+
await extractArchive(archivePath, binDir, platform === 'windows');
|
|
120
|
+
|
|
121
|
+
const binaryName = platform === 'windows' ? `${BINARY_NAME}.exe` : BINARY_NAME;
|
|
122
|
+
const extractedPath = path.join(tmpDir, binaryName);
|
|
123
|
+
const destPath = path.join(binDir, binaryName);
|
|
124
|
+
|
|
125
|
+
if (fs.existsSync(extractedPath)) {
|
|
126
|
+
fs.renameSync(extractedPath, destPath);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (platform !== 'windows') {
|
|
130
|
+
fs.chmodSync(destPath, 0o755);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
console.log('[INFO] Successfully installed redmine-cli');
|
|
134
|
+
} catch (e) {
|
|
135
|
+
console.error('[ERROR] Installation failed:', e.message);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
} finally {
|
|
138
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
main();
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
param(
|
|
2
|
+
[string]$InstallDir = "",
|
|
3
|
+
[string]$Version = ""
|
|
4
|
+
)
|
|
5
|
+
|
|
6
|
+
$ErrorActionPreference = "Stop"
|
|
7
|
+
|
|
8
|
+
$REPO = "largeoliu/redmine-cli"
|
|
9
|
+
$BINARY_NAME = "redmine"
|
|
10
|
+
|
|
11
|
+
if (-not $InstallDir) {
|
|
12
|
+
$InstallDir = Join-Path $env:USERPROFILE ".local\bin"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function Write-Info {
|
|
16
|
+
param([string]$Message)
|
|
17
|
+
Write-Host "[INFO] " -ForegroundColor Green -NoNewline
|
|
18
|
+
Write-Host $Message
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function Write-Warn {
|
|
22
|
+
param([string]$Message)
|
|
23
|
+
Write-Host "[WARN] " -ForegroundColor Yellow -NoNewline
|
|
24
|
+
Write-Host $Message
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function Write-Error-Exit {
|
|
28
|
+
param([string]$Message)
|
|
29
|
+
Write-Host "[ERROR] " -ForegroundColor Red -NoNewline
|
|
30
|
+
Write-Host $Message
|
|
31
|
+
exit 1
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function Get-OS {
|
|
35
|
+
if ($IsWindows -or ($env:OS -match "Windows")) {
|
|
36
|
+
return "windows"
|
|
37
|
+
} elseif ($IsMacOS) {
|
|
38
|
+
return "darwin"
|
|
39
|
+
} elseif ($IsLinux) {
|
|
40
|
+
return "linux"
|
|
41
|
+
}
|
|
42
|
+
Write-Error-Exit "Unsupported OS"
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function Get-Arch {
|
|
46
|
+
$arch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture
|
|
47
|
+
switch ($arch) {
|
|
48
|
+
"X64" { return "amd64" }
|
|
49
|
+
"Arm64" { return "arm64" }
|
|
50
|
+
default { Write-Error-Exit "Unsupported architecture: $arch" }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function Get-LatestVersion {
|
|
55
|
+
$latestUrl = "https://github.com/$REPO/releases/latest"
|
|
56
|
+
try {
|
|
57
|
+
$response = Invoke-WebRequest -Uri $latestUrl -Method Head -MaximumRedirection 0 -ErrorAction SilentlyContinue
|
|
58
|
+
} catch {
|
|
59
|
+
$response = $_.Exception.Response
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if ($response.Headers.Location) {
|
|
63
|
+
$location = $response.Headers.Location.ToString()
|
|
64
|
+
$version = $location.Split("/")[-1]
|
|
65
|
+
return $version
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
Write-Error-Exit "Failed to get latest version"
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function Download-Binary {
|
|
72
|
+
param(
|
|
73
|
+
[string]$Version,
|
|
74
|
+
[string]$OS,
|
|
75
|
+
[string]$Arch
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
$archiveName = "${BINARY_NAME}_$($Version.Substring(1))_${OS}_${Arch}.zip"
|
|
79
|
+
$downloadUrl = "https://github.com/$REPO/releases/download/$Version/$archiveName"
|
|
80
|
+
|
|
81
|
+
Write-Info "Downloading $archiveName..."
|
|
82
|
+
|
|
83
|
+
$tmpDir = New-TemporaryDirectory
|
|
84
|
+
$archivePath = Join-Path $tmpDir $archiveName
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
Invoke-WebRequest -Uri $downloadUrl -OutFile $archivePath -UseBasicParsing
|
|
88
|
+
} catch {
|
|
89
|
+
Write-Error-Exit "Failed to download $archiveName : $_"
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
Write-Info "Extracting..."
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
Expand-Archive -Path $archivePath -DestinationPath $tmpDir -Force
|
|
96
|
+
} catch {
|
|
97
|
+
Write-Error-Exit "Failed to extract archive: $_"
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return $tmpDir
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function New-TemporaryDirectory {
|
|
104
|
+
$tmpPath = [System.IO.Path]::GetTempPath()
|
|
105
|
+
$tmpDir = [System.IO.Path]::Combine($tmpPath, [System.IO.Path]::GetRandomFileName())
|
|
106
|
+
New-Item -ItemType Directory -Path $tmpDir | Out-Null
|
|
107
|
+
return $tmpDir
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function Install-Binary {
|
|
111
|
+
param([string]$TmpDir)
|
|
112
|
+
|
|
113
|
+
if (-not (Test-Path $InstallDir)) {
|
|
114
|
+
Write-Info "Creating install directory: $InstallDir"
|
|
115
|
+
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
$binaryPath = Join-Path $TmpDir "$BINARY_NAME.exe"
|
|
119
|
+
|
|
120
|
+
if (-not (Test-Path $binaryPath)) {
|
|
121
|
+
$binaryPath = Join-Path $TmpDir $BINARY_NAME
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (-not (Test-Path $binaryPath)) {
|
|
125
|
+
Write-Error-Exit "Binary not found in archive"
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
$destPath = Join-Path $InstallDir "$BINARY_NAME.exe"
|
|
129
|
+
Move-Item -Path $binaryPath -Destination $destPath -Force
|
|
130
|
+
|
|
131
|
+
Remove-Item -Path $TmpDir -Recurse -Force -ErrorAction SilentlyContinue
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function Test-PathInEnv {
|
|
135
|
+
$pathDirs = $env:PATH -split ";"
|
|
136
|
+
return $pathDirs -contains $InstallDir
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function Add-ToPath {
|
|
140
|
+
$currentPath = [Environment]::GetEnvironmentVariable("Path", "User")
|
|
141
|
+
if ($currentPath -notlike "*$InstallDir*") {
|
|
142
|
+
$newPath = "$currentPath;$InstallDir"
|
|
143
|
+
[Environment]::SetEnvironmentVariable("Path", $newPath, "User")
|
|
144
|
+
Write-Info "Added $InstallDir to user PATH"
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
$os = Get-OS
|
|
149
|
+
$arch = Get-Arch
|
|
150
|
+
|
|
151
|
+
if (-not $Version) {
|
|
152
|
+
$Version = Get-LatestVersion
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
Write-Info "Installing $BINARY_NAME..."
|
|
156
|
+
Write-Info "OS: $os, Arch: $arch, Version: $Version"
|
|
157
|
+
|
|
158
|
+
$tmpDir = Download-Binary -Version $Version -OS $os -Arch $arch
|
|
159
|
+
Install-Binary -TmpDir $tmpDir
|
|
160
|
+
|
|
161
|
+
Write-Info "Successfully installed $BINARY_NAME to $InstallDir"
|
|
162
|
+
|
|
163
|
+
if (-not (Test-PathInEnv)) {
|
|
164
|
+
Write-Host ""
|
|
165
|
+
Write-Warn "$InstallDir is not in your PATH"
|
|
166
|
+
Write-Host ""
|
|
167
|
+
|
|
168
|
+
$addToPath = Read-Host "Would you like to add it to your PATH? (Y/n)"
|
|
169
|
+
if ($addToPath -ne "n" -and $addToPath -ne "N") {
|
|
170
|
+
Add-ToPath
|
|
171
|
+
Write-Host ""
|
|
172
|
+
Write-Info "Please restart your terminal or run: `$env:Path = [System.Environment]::GetEnvironmentVariable('Path','User')"
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
Write-Host ""
|
|
177
|
+
Write-Info "Run '$BINARY_NAME --help' to get started"
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
|
|
3
|
+
set -e
|
|
4
|
+
|
|
5
|
+
REPO="largeoliu/redmine-cli"
|
|
6
|
+
BINARY_NAME="redmine"
|
|
7
|
+
INSTALL_DIR="${INSTALL_DIR:-$HOME/.local/bin}"
|
|
8
|
+
|
|
9
|
+
GREEN='\033[0;32m'
|
|
10
|
+
RED='\033[0;31m'
|
|
11
|
+
YELLOW='\033[1;33m'
|
|
12
|
+
NC='\033[0m'
|
|
13
|
+
|
|
14
|
+
info() {
|
|
15
|
+
echo "${GREEN}[INFO]${NC} $1"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
warn() {
|
|
19
|
+
echo "${YELLOW}[WARN]${NC} $1"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
error() {
|
|
23
|
+
echo "${RED}[ERROR]${NC} $1"
|
|
24
|
+
exit 1
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
detect_os() {
|
|
28
|
+
case "$(uname -s)" in
|
|
29
|
+
Darwin*) echo "darwin" ;;
|
|
30
|
+
Linux*) echo "linux" ;;
|
|
31
|
+
CYGWIN*|MINGW*|MSYS*) echo "windows" ;;
|
|
32
|
+
*) error "Unsupported OS: $(uname -s)" ;;
|
|
33
|
+
esac
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
detect_arch() {
|
|
37
|
+
case "$(uname -m)" in
|
|
38
|
+
x86_64|amd64) echo "amd64" ;;
|
|
39
|
+
arm64|aarch64) echo "arm64" ;;
|
|
40
|
+
*) error "Unsupported architecture: $(uname -m)" ;;
|
|
41
|
+
esac
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get_latest_version() {
|
|
45
|
+
latest_url="https://github.com/${REPO}/releases/latest"
|
|
46
|
+
version=$(curl -sI "$latest_url" | grep -i "location:" | sed 's/.*\/tag\/\(.*\)/\1/' | tr -d '\r\n')
|
|
47
|
+
if [ -z "$version" ]; then
|
|
48
|
+
error "Failed to get latest version"
|
|
49
|
+
fi
|
|
50
|
+
echo "$version"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
download_binary() {
|
|
54
|
+
version="$1"
|
|
55
|
+
os="$2"
|
|
56
|
+
arch="$3"
|
|
57
|
+
|
|
58
|
+
if [ "$os" = "windows" ]; then
|
|
59
|
+
archive_name="${BINARY_NAME}_${version#v}_${os}_${arch}.zip"
|
|
60
|
+
else
|
|
61
|
+
archive_name="${BINARY_NAME}_${version#v}_${os}_${arch}.tar.gz"
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
download_url="https://github.com/${REPO}/releases/download/${version}/${archive_name}"
|
|
65
|
+
|
|
66
|
+
info "Downloading ${archive_name}..."
|
|
67
|
+
|
|
68
|
+
tmp_dir=$(mktemp -d)
|
|
69
|
+
archive_path="${tmp_dir}/${archive_name}"
|
|
70
|
+
|
|
71
|
+
if ! curl -fsSL "$download_url" -o "$archive_path"; then
|
|
72
|
+
error "Failed to download ${archive_name}"
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
info "Extracting..."
|
|
76
|
+
|
|
77
|
+
if [ "$os" = "windows" ]; then
|
|
78
|
+
if ! unzip -q "$archive_path" -d "$tmp_dir"; then
|
|
79
|
+
error "Failed to extract archive"
|
|
80
|
+
fi
|
|
81
|
+
else
|
|
82
|
+
if ! tar -xzf "$archive_path" -C "$tmp_dir"; then
|
|
83
|
+
error "Failed to extract archive"
|
|
84
|
+
fi
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
echo "$tmp_dir"
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
install_binary() {
|
|
91
|
+
tmp_dir="$1"
|
|
92
|
+
|
|
93
|
+
if [ ! -d "$INSTALL_DIR" ]; then
|
|
94
|
+
info "Creating install directory: $INSTALL_DIR"
|
|
95
|
+
mkdir -p "$INSTALL_DIR"
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
binary_path="${tmp_dir}/${BINARY_NAME}"
|
|
99
|
+
|
|
100
|
+
if [ ! -f "$binary_path" ]; then
|
|
101
|
+
error "Binary not found in archive"
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
chmod +x "$binary_path"
|
|
105
|
+
mv "$binary_path" "${INSTALL_DIR}/${BINARY_NAME}"
|
|
106
|
+
|
|
107
|
+
rm -rf "$tmp_dir"
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
check_path() {
|
|
111
|
+
case ":$PATH:" in
|
|
112
|
+
*":$INSTALL_DIR:"*)
|
|
113
|
+
return 0
|
|
114
|
+
;;
|
|
115
|
+
*)
|
|
116
|
+
return 1
|
|
117
|
+
;;
|
|
118
|
+
esac
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
main() {
|
|
122
|
+
info "Installing ${BINARY_NAME}..."
|
|
123
|
+
|
|
124
|
+
os=$(detect_os)
|
|
125
|
+
arch=$(detect_arch)
|
|
126
|
+
version=$(get_latest_version)
|
|
127
|
+
|
|
128
|
+
info "OS: ${os}, Arch: ${arch}, Version: ${version}"
|
|
129
|
+
|
|
130
|
+
tmp_dir=$(download_binary "$version" "$os" "$arch")
|
|
131
|
+
install_binary "$tmp_dir"
|
|
132
|
+
|
|
133
|
+
info "Successfully installed ${BINARY_NAME} to ${INSTALL_DIR}"
|
|
134
|
+
|
|
135
|
+
if ! check_path; then
|
|
136
|
+
echo ""
|
|
137
|
+
warn "${INSTALL_DIR} is not in your PATH"
|
|
138
|
+
echo ""
|
|
139
|
+
echo "Add the following to your shell profile (~/.bashrc, ~/.zshrc, etc.):"
|
|
140
|
+
echo ""
|
|
141
|
+
echo " export PATH=\"\$PATH:${INSTALL_DIR}\""
|
|
142
|
+
echo ""
|
|
143
|
+
echo "Then restart your shell or run: source ~/.bashrc (or ~/.zshrc)"
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
echo ""
|
|
147
|
+
info "Run '${BINARY_NAME} --help' to get started"
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
main "$@"
|