@kroszborg/sugi 0.3.0 → 0.3.2
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 +8 -8
- package/bin.js +1 -1
- package/install.js +70 -53
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
> A terminal UI git client — GitHub/GitLab PRs, AI commit messages, interactive rebase, bisect, worktrees, multi-account management, and more.
|
|
4
4
|
|
|
5
|
-

|
|
5
|
+

|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
|
-
npm install -g sugi
|
|
10
|
+
npm install -g @kroszborg/sugi
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
Or via Homebrew:
|
|
@@ -47,7 +47,7 @@ sugi version # print version
|
|
|
47
47
|
| Interactive Rebase | `i` | Reorder/squash/fixup/drop commits |
|
|
48
48
|
| Conflict Resolver | auto | Opens on conflicted files — pick ours/theirs |
|
|
49
49
|
| File History | `L` | Commits touching the selected file |
|
|
50
|
-
| Command Palette | `ctrl+p` | Fuzzy search all actions |
|
|
50
|
+
| Command Palette | `ctrl+p`/`alt+p` | Fuzzy search all actions |
|
|
51
51
|
| Help | `?` | Scrollable keybinding reference |
|
|
52
52
|
| Settings | `O` | Edit config in-app, saved instantly |
|
|
53
53
|
|
|
@@ -71,7 +71,7 @@ sugi version # print version
|
|
|
71
71
|
| `a` | stage all |
|
|
72
72
|
| `d` | discard (with confirmation) |
|
|
73
73
|
| `c` | commit form |
|
|
74
|
-
| `
|
|
74
|
+
| `ctrl+a` | amend HEAD |
|
|
75
75
|
| `P` / `p` | push / pull |
|
|
76
76
|
| `F` | force push with-lease |
|
|
77
77
|
| `Z` | stash all changes |
|
|
@@ -102,7 +102,7 @@ sugi version # print version
|
|
|
102
102
|
| `g` | toggle graph |
|
|
103
103
|
| `b` | blame at this commit |
|
|
104
104
|
| `R` | reflog |
|
|
105
|
-
| `
|
|
105
|
+
| `ctrl+a` | amend HEAD |
|
|
106
106
|
|
|
107
107
|
### Diff
|
|
108
108
|
| Key | Action |
|
|
@@ -111,9 +111,9 @@ sugi version # print version
|
|
|
111
111
|
| `space` | stage hunk |
|
|
112
112
|
| `u` | unstage hunk |
|
|
113
113
|
| `s` | toggle staged/unstaged |
|
|
114
|
-
| `
|
|
114
|
+
| `ctrl+i` | AI-summarise diff |
|
|
115
115
|
|
|
116
|
-
### Accounts (`A`)
|
|
116
|
+
### Accounts (`A` — from any panel)
|
|
117
117
|
| Key | Action |
|
|
118
118
|
|-----|--------|
|
|
119
119
|
| `tab` | GitHub / GitLab tab |
|
|
@@ -127,7 +127,7 @@ sugi version # print version
|
|
|
127
127
|
|-----|--------|
|
|
128
128
|
| `c` | commit form |
|
|
129
129
|
| `ctrl+g` / `alt+g` | AI-generate commit message |
|
|
130
|
-
| `ctrl+p` | command palette |
|
|
130
|
+
| `ctrl+p` / `alt+p` | command palette |
|
|
131
131
|
| `O` | settings |
|
|
132
132
|
| `?` | help |
|
|
133
133
|
| `q` | quit |
|
package/bin.js
CHANGED
|
@@ -10,7 +10,7 @@ const binPath = path.join(__dirname, 'bin', binName);
|
|
|
10
10
|
|
|
11
11
|
if (!fs.existsSync(binPath)) {
|
|
12
12
|
console.error('sugi: binary not found at ' + binPath);
|
|
13
|
-
console.error('Try reinstalling: npm install -g sugi');
|
|
13
|
+
console.error('Try reinstalling: npm install -g @kroszborg/sugi');
|
|
14
14
|
process.exit(1);
|
|
15
15
|
}
|
|
16
16
|
|
package/install.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// npm binary wrapper for sugi
|
|
3
3
|
// Downloads the correct pre-built binary from GitHub Releases on postinstall.
|
|
4
|
-
// Falls back to `go
|
|
4
|
+
// Falls back to `go install` if no release binary exists yet.
|
|
5
5
|
|
|
6
6
|
'use strict';
|
|
7
7
|
|
|
@@ -9,7 +9,7 @@ const https = require('https');
|
|
|
9
9
|
const fs = require('fs');
|
|
10
10
|
const path = require('path');
|
|
11
11
|
const os = require('os');
|
|
12
|
-
const {
|
|
12
|
+
const { spawnSync } = require('child_process');
|
|
13
13
|
|
|
14
14
|
const VERSION = require('./package.json').version;
|
|
15
15
|
const REPO = 'Kroszborg/sugi';
|
|
@@ -35,72 +35,100 @@ function platformInfo() {
|
|
|
35
35
|
return { osName, cpu, ext, binName };
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
// Download url to dest, following up to 10 redirects.
|
|
39
|
+
// Only opens the WriteStream once we have a confirmed 200 response.
|
|
38
40
|
function downloadFile(url, dest) {
|
|
39
41
|
return new Promise((resolve, reject) => {
|
|
40
|
-
|
|
42
|
+
let redirects = 0;
|
|
43
|
+
|
|
41
44
|
const get = (u) => {
|
|
45
|
+
if (redirects > 10) return reject(new Error('Too many redirects'));
|
|
42
46
|
https.get(u, { headers: { 'User-Agent': 'sugi-npm-installer' } }, (res) => {
|
|
43
|
-
if (res.statusCode === 301 || res.statusCode === 302
|
|
44
|
-
|
|
45
|
-
|
|
47
|
+
if (res.statusCode === 301 || res.statusCode === 302 ||
|
|
48
|
+
res.statusCode === 307 || res.statusCode === 308) {
|
|
49
|
+
redirects++;
|
|
50
|
+
res.resume(); // drain so socket can be reused
|
|
46
51
|
return get(res.headers.location);
|
|
47
52
|
}
|
|
48
53
|
if (res.statusCode !== 200) {
|
|
49
|
-
|
|
50
|
-
return reject(new Error(`HTTP ${res.statusCode}
|
|
54
|
+
res.resume();
|
|
55
|
+
return reject(new Error(`HTTP ${res.statusCode} downloading ${u}`));
|
|
51
56
|
}
|
|
57
|
+
// Only create the file once we have a real 200 response
|
|
58
|
+
const file = fs.createWriteStream(dest);
|
|
52
59
|
res.pipe(file);
|
|
53
|
-
file.on('finish', () =>
|
|
54
|
-
|
|
60
|
+
file.on('finish', () => file.close(resolve));
|
|
61
|
+
file.on('error', (e) => { fs.unlink(dest, () => {}); reject(e); });
|
|
62
|
+
res.on('error', (e) => { file.close(); fs.unlink(dest, () => {}); reject(e); });
|
|
63
|
+
}).on('error', reject);
|
|
55
64
|
};
|
|
65
|
+
|
|
56
66
|
get(url);
|
|
57
67
|
});
|
|
58
68
|
}
|
|
59
69
|
|
|
60
70
|
function tryGoBuild() {
|
|
61
|
-
// Check if go is available
|
|
62
71
|
const goCheck = spawnSync('go', ['version'], { stdio: 'pipe' });
|
|
63
|
-
if (goCheck.status !== 0)
|
|
64
|
-
return false;
|
|
65
|
-
}
|
|
72
|
+
if (goCheck.status !== 0) return false;
|
|
66
73
|
|
|
67
74
|
if (!fs.existsSync(BIN_DIR)) fs.mkdirSync(BIN_DIR, { recursive: true });
|
|
68
75
|
|
|
69
76
|
const binName = process.platform === 'win32' ? 'sugi.exe' : 'sugi';
|
|
70
77
|
const binDest = path.join(BIN_DIR, binName);
|
|
71
78
|
|
|
72
|
-
|
|
73
|
-
|
|
79
|
+
console.log('sugi: building from source via `go install`...');
|
|
80
|
+
// Embed the version string via ldflags so `sugi version` shows the right value
|
|
81
|
+
const ldflags = `-X main.version=${VERSION}`;
|
|
74
82
|
const result = spawnSync(
|
|
75
83
|
'go',
|
|
76
|
-
['install', `github.com/${REPO}/cmd/sugi@latest`],
|
|
84
|
+
['install', `-ldflags=${ldflags}`, `github.com/${REPO}/cmd/sugi@latest`],
|
|
77
85
|
{ stdio: 'inherit', env: process.env }
|
|
78
86
|
);
|
|
79
87
|
|
|
80
|
-
if (result.status
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
88
|
+
if (result.status !== 0) return false;
|
|
89
|
+
|
|
90
|
+
// Locate the installed binary in GOBIN or GOPATH/bin
|
|
91
|
+
const goEnv = spawnSync('go', ['env', 'GOBIN', 'GOPATH'], { stdio: 'pipe' });
|
|
92
|
+
if (goEnv.status !== 0) return false;
|
|
93
|
+
|
|
94
|
+
const [gobin, gopath] = goEnv.stdout.toString().trim().split('\n');
|
|
95
|
+
const candidates = [
|
|
96
|
+
gobin && path.join(gobin.trim(), binName),
|
|
97
|
+
gopath && path.join(gopath.trim(), 'bin', binName),
|
|
98
|
+
].filter(Boolean);
|
|
99
|
+
|
|
100
|
+
for (const src of candidates) {
|
|
101
|
+
if (fs.existsSync(src)) {
|
|
102
|
+
fs.copyFileSync(src, binDest);
|
|
103
|
+
fs.chmodSync(binDest, 0o755);
|
|
104
|
+
console.log(`sugi: installed to ${binDest}`);
|
|
105
|
+
return true;
|
|
98
106
|
}
|
|
99
107
|
}
|
|
100
108
|
|
|
101
109
|
return false;
|
|
102
110
|
}
|
|
103
111
|
|
|
112
|
+
function extractArchive(archivePath, ext, binName) {
|
|
113
|
+
if (!fs.existsSync(BIN_DIR)) fs.mkdirSync(BIN_DIR, { recursive: true });
|
|
114
|
+
|
|
115
|
+
if (ext === '.tar.gz') {
|
|
116
|
+
// Use array args to avoid shell injection
|
|
117
|
+
const result = spawnSync('tar', ['-xzf', archivePath, '-C', BIN_DIR, binName], { stdio: 'inherit' });
|
|
118
|
+
if (result.status !== 0) throw new Error('tar extraction failed');
|
|
119
|
+
} else {
|
|
120
|
+
// Windows: use PowerShell Expand-Archive
|
|
121
|
+
const ps = `Expand-Archive -Path '${archivePath}' -DestinationPath '${BIN_DIR}' -Force`;
|
|
122
|
+
const result = spawnSync('powershell', ['-NoProfile', '-Command', ps], { stdio: 'inherit' });
|
|
123
|
+
if (result.status !== 0) throw new Error('Expand-Archive failed');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const binDest = path.join(BIN_DIR, binName);
|
|
127
|
+
if (!fs.existsSync(binDest)) throw new Error(`Binary not found after extraction: ${binDest}`);
|
|
128
|
+
fs.chmodSync(binDest, 0o755);
|
|
129
|
+
return binDest;
|
|
130
|
+
}
|
|
131
|
+
|
|
104
132
|
async function main() {
|
|
105
133
|
const { osName, cpu, ext, binName } = platformInfo();
|
|
106
134
|
|
|
@@ -109,45 +137,34 @@ async function main() {
|
|
|
109
137
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sugi-'));
|
|
110
138
|
const archivePath = path.join(tmpDir, assetName);
|
|
111
139
|
|
|
112
|
-
console.log(`sugi: downloading ${assetName}
|
|
140
|
+
console.log(`sugi: downloading ${assetName}...`);
|
|
113
141
|
|
|
114
142
|
let downloadOk = false;
|
|
115
143
|
try {
|
|
116
144
|
await downloadFile(url, archivePath);
|
|
117
145
|
downloadOk = true;
|
|
118
146
|
} catch (e) {
|
|
119
|
-
console.error(`sugi: download failed
|
|
147
|
+
console.error(`sugi: download failed - ${e.message}`);
|
|
120
148
|
}
|
|
121
149
|
|
|
122
150
|
if (!downloadOk) {
|
|
123
|
-
console.log(
|
|
151
|
+
console.log('sugi: falling back to go install...');
|
|
124
152
|
const ok = tryGoBuild();
|
|
153
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
125
154
|
if (!ok) {
|
|
126
|
-
console.error(
|
|
155
|
+
console.error('sugi: install failed. Install manually:');
|
|
127
156
|
console.error(` go install github.com/${REPO}/cmd/sugi@latest`);
|
|
128
157
|
console.error(` or download from: https://github.com/${REPO}/releases`);
|
|
129
158
|
process.exit(1);
|
|
130
159
|
}
|
|
131
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
132
160
|
return;
|
|
133
161
|
}
|
|
134
162
|
|
|
135
|
-
if (!fs.existsSync(BIN_DIR)) fs.mkdirSync(BIN_DIR, { recursive: true });
|
|
136
|
-
const binDest = path.join(BIN_DIR, binName);
|
|
137
|
-
|
|
138
163
|
try {
|
|
139
|
-
|
|
140
|
-
execSync(`tar -xzf "${archivePath}" -C "${BIN_DIR}" sugi`, { stdio: 'inherit' });
|
|
141
|
-
} else {
|
|
142
|
-
execSync(
|
|
143
|
-
`powershell -Command "Expand-Archive -Path '${archivePath}' -DestinationPath '${BIN_DIR}' -Force"`,
|
|
144
|
-
{ stdio: 'inherit' }
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
fs.chmodSync(binDest, 0o755);
|
|
164
|
+
const binDest = extractArchive(archivePath, ext, binName);
|
|
148
165
|
console.log(`sugi: installed to ${binDest}`);
|
|
149
166
|
} catch (e) {
|
|
150
|
-
console.error(`sugi: extraction failed
|
|
167
|
+
console.error(`sugi: extraction failed - ${e.message}`);
|
|
151
168
|
process.exit(1);
|
|
152
169
|
} finally {
|
|
153
170
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|