@kroszborg/sugi 0.3.1 → 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 CHANGED
@@ -7,7 +7,7 @@
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:
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 build` if no release exists yet (local / dev install).
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 { execSync, spawnSync } = require('child_process');
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
- const file = fs.createWriteStream(dest);
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
- file.close();
45
- const newFile = fs.createWriteStream(dest);
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
- file.close();
50
- return reject(new Error(`HTTP ${res.statusCode} for ${u}`));
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', () => { file.close(); resolve(); });
54
- }).on('error', (e) => { file.close(); reject(e); });
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
- // If we're inside an npm-installed package, try go install as a fallback
73
- console.log('sugi: building from source via `go install`…');
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 === 0) {
81
- // Find the installed binary in GOPATH/bin or GOBIN
82
- const goEnv = spawnSync('go', ['env', 'GOBIN', 'GOPATH'], { stdio: 'pipe' });
83
- if (goEnv.status === 0) {
84
- const [gobin, gopath] = goEnv.stdout.toString().trim().split('\n');
85
- const candidates = [
86
- gobin && path.join(gobin.trim(), binName),
87
- gopath && path.join(gopath.trim(), 'bin', binName),
88
- ].filter(Boolean);
89
-
90
- for (const src of candidates) {
91
- if (fs.existsSync(src)) {
92
- fs.copyFileSync(src, binDest);
93
- fs.chmodSync(binDest, 0o755);
94
- console.log(`sugi: installed to ${binDest}`);
95
- return true;
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 ${e.message}`);
147
+ console.error(`sugi: download failed - ${e.message}`);
120
148
  }
121
149
 
122
150
  if (!downloadOk) {
123
- console.log(`sugi: falling back to go install…`);
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(`sugi: install failed. Install manually:`);
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
- if (ext === '.tar.gz') {
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 ${e.message}`);
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 });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kroszborg/sugi",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Terminal UI git client — PRs, AI commits, interactive rebase, bisect, worktrees",
5
5
  "keywords": [
6
6
  "git",