eidoncore 3.7.4 → 3.7.6

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.
Files changed (2) hide show
  1. package/install.js +70 -28
  2. package/package.json +1 -1
package/install.js CHANGED
@@ -110,41 +110,77 @@ async function fetchAndVerifyManifest(url) {
110
110
 
111
111
  function downloadFile(url, dest) {
112
112
  return new Promise((resolve, reject) => {
113
- const file = fs.createWriteStream(dest);
114
- function follow(href) {
113
+ // Follow redirects BEFORE opening the write stream — GitHub releases always
114
+ // 302-redirect to objects.githubusercontent.com. Creating the WriteStream
115
+ // before resolving redirects and then calling file.destroy() on redirect
116
+ // causes the subsequent pipe to write to a destroyed stream (0 bytes, silent).
117
+ function followRedirects(href, hops) {
118
+ if (hops > 5) return reject(new Error('Too many redirects downloading binary'));
119
+ if (!href.startsWith('https://')) return reject(new Error(`Refusing non-HTTPS redirect: ${href}`));
115
120
  const req = https.get(href, { timeout: 120000 }, res => {
116
121
  if (res.statusCode === 301 || res.statusCode === 302) {
117
- file.destroy();
118
- return follow(res.headers.location);
122
+ res.resume(); // drain response body before following redirect
123
+ const loc = res.headers.location || '';
124
+ return followRedirects(loc, hops + 1);
119
125
  }
120
126
  if (res.statusCode !== 200) {
121
- file.destroy();
122
- fs.unlink(dest, () => {});
123
- return reject(new Error(`HTTP ${res.statusCode} downloading binary`));
127
+ res.resume();
128
+ return reject(new Error(`HTTP ${res.statusCode} downloading binary from ${href}`));
124
129
  }
125
- const total = parseInt(res.headers['content-length'] || '0', 10);
126
- let received = 0;
127
- let lastPct = -1;
130
+ // Final URL resolved now create the write stream.
131
+ // Do NOT use res.pipe(file) here: adding a 'data' listener puts the
132
+ // stream in flowing mode before pipe is registered, causing chunks to
133
+ // be consumed by the progress listener but not written to the file.
134
+ // Manual write() with backpressure is the only reliable approach on Windows.
135
+ const total = parseInt(res.headers['content-length'] || '0', 10);
136
+ let received = 0;
137
+ let lastPct = -1;
138
+ const file = fs.createWriteStream(dest);
139
+
140
+ file.on('error', err => {
141
+ res.destroy();
142
+ try { fs.unlinkSync(dest); } catch {}
143
+ reject(new Error(`File write error: ${err.message}`));
144
+ });
145
+
146
+ res.on('error', err => {
147
+ file.destroy();
148
+ try { fs.unlinkSync(dest); } catch {}
149
+ reject(new Error(`Download stream error: ${err.message}`));
150
+ });
151
+
128
152
  res.on('data', chunk => {
129
153
  received += chunk.length;
154
+ // Backpressure: pause the network stream if the disk is behind
155
+ const ok = file.write(chunk);
156
+ if (!ok) res.pause();
157
+
130
158
  if (total > 0) {
131
- const pct = Math.floor((received / total) * 20); // 20-char bar
159
+ const pct = Math.floor((received / total) * 20);
132
160
  if (pct !== lastPct) {
133
161
  lastPct = pct;
134
162
  const bar = '█'.repeat(pct) + '░'.repeat(20 - pct);
135
- process.stdout.write(`\r [${bar}] ${Math.floor(received / 1024)}KB`);
163
+ const mb = (received / 1024 / 1024).toFixed(1);
164
+ const tot = (total / 1024 / 1024).toFixed(1);
165
+ process.stdout.write(`\r [${bar}] ${mb} / ${tot} MB`);
136
166
  }
167
+ } else {
168
+ process.stdout.write(`\r Downloading... ${(received / 1024).toFixed(0)} KB`);
137
169
  }
138
170
  });
139
- res.pipe(file);
140
- res.on('end', () => { process.stdout.write('\n'); });
141
- file.on('finish', () => file.close(resolve));
142
- file.on('error', err => { fs.unlink(dest, () => {}); reject(err); });
171
+
172
+ // Resume the readable when the writable drains (backpressure relief)
173
+ file.on('drain', () => res.resume());
174
+
175
+ res.on('end', () => {
176
+ process.stdout.write('\n');
177
+ file.end(() => resolve());
178
+ });
143
179
  });
144
- req.on('error', err => { fs.unlink(dest, () => {}); reject(err); });
180
+ req.on('error', err => { try { fs.unlinkSync(dest); } catch {} reject(err); });
145
181
  req.on('timeout', () => { req.destroy(); reject(new Error('Download timed out')); });
146
182
  }
147
- follow(url);
183
+ followRedirects(url, 0);
148
184
  });
149
185
  }
150
186
 
@@ -161,15 +197,20 @@ function sha256File(filePath) {
161
197
 
162
198
  // ── Main ─────────────────────────────────────────────────────────────────
163
199
  async function main() {
164
- // Skip if binary already exists and matches (e.g. re-running postinstall)
200
+ // If binary already exists, check if it matches the npm package version.
201
+ // If versions match → skip. If outdated or broken → re-download.
165
202
  if (fs.existsSync(BINARY_PATH)) {
166
203
  try {
167
- execFileSync(BINARY_PATH, ['--version'], { stdio: 'ignore' });
168
- console.log(' Eidon binary already installed and working.');
169
- return;
204
+ const installedVersion = execFileSync(BINARY_PATH, ['--version'], { encoding: 'utf8' }).trim();
205
+ const pkgVersion = require('./package.json').version;
206
+ if (installedVersion === pkgVersion) {
207
+ console.log(` Eidon ${pkgVersion} already installed.`);
208
+ return;
209
+ }
210
+ console.log(` Upgrading Eidon ${installedVersion} → ${pkgVersion}...`);
170
211
  } catch {
171
212
  // Binary exists but broken — re-download
172
- fs.unlinkSync(BINARY_PATH);
213
+ try { fs.unlinkSync(BINARY_PATH); } catch {}
173
214
  }
174
215
  }
175
216
 
@@ -178,11 +219,12 @@ async function main() {
178
219
  const platform = getPlatform();
179
220
  console.log(` Platform : ${platform}`);
180
221
 
181
- // Allow pinning: EIDON_VERSION=3.2.0 npm install -g eidon
182
- const requestedVersion = process.env.EIDON_VERSION || '';
183
- const manifestUrl = requestedVersion
184
- ? `${RELEASES_BASE}/${requestedVersion}.json`
185
- : `${RELEASES_BASE}/latest.json`;
222
+ // Always download the exact version matching this npm package no race condition
223
+ // with latest.json (which GitHub Pages may not have updated yet when npm was published).
224
+ // Allow override: EIDON_VERSION=3.2.0 npm install -g eidoncore
225
+ const npmVersion = require('./package.json').version;
226
+ const requestedVersion = process.env.EIDON_VERSION || npmVersion;
227
+ const manifestUrl = `${RELEASES_BASE}/${requestedVersion}.json`;
186
228
 
187
229
  console.log(' Fetching release manifest...');
188
230
  const manifest = await fetchAndVerifyManifest(manifestUrl);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eidoncore",
3
- "version": "3.7.4",
3
+ "version": "3.7.6",
4
4
  "description": "Your AI finally knows your whole codebase. Structural graph intelligence for every AI tool you use.",
5
5
  "keywords": [
6
6
  "ai",