-
Notifications
You must be signed in to change notification settings - Fork 0
chore: sync core lib and CLAUDE.md from agent-core #33
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,160 @@ | ||
| 'use strict'; | ||
|
|
||
| /** | ||
| * Shared HTTP + archive helpers used by both binary resolvers | ||
| * (`lib/binary/index.js` for `agent-analyzer`, `lib/embed/binary.js` | ||
| * for `agent-analyzer-embed`). | ||
| * | ||
| * Extracted to keep the two resolvers from drifting on HTTP redirect | ||
| * handling, GitHub auth, and archive extraction details — a single | ||
| * fix to e.g. the timeout policy or the redirect cap lands once and | ||
| * applies to both binaries. | ||
| * | ||
| * @module lib/binary/shared-helpers | ||
| */ | ||
|
|
||
| const fs = require('fs'); | ||
| const path = require('path'); | ||
| const os = require('os'); | ||
| const https = require('https'); | ||
| const cp = require('child_process'); | ||
|
|
||
| const DEFAULT_DOWNLOAD_TIMEOUT_MS = 30000; | ||
| const MAX_REDIRECTS = 5; | ||
|
|
||
| /** | ||
| * Fetch a URL into an in-memory Buffer following up to 5 redirects. | ||
| * | ||
| * Honors `GITHUB_TOKEN` / `GH_TOKEN` for authenticated requests | ||
| * (raises rate limit, lets private-repo asset URLs work). Stalled | ||
| * connections are killed by the per-request timeout — without this | ||
| * a stuck socket would hang the process indefinitely. | ||
| * | ||
| * @param {string} url | ||
| * @param {Object} [options] | ||
| * @param {string} [options.userAgent='agent-sh/binary-resolver'] | ||
| * @param {number} [options.timeoutMs=30000] - per-request timeout | ||
| * @returns {Promise<Buffer>} | ||
| */ | ||
| function downloadToBuffer(url, options) { | ||
| const opts = options || {}; | ||
| const userAgent = opts.userAgent || 'agent-sh/binary-resolver'; | ||
| const timeoutMs = opts.timeoutMs || DEFAULT_DOWNLOAD_TIMEOUT_MS; | ||
|
|
||
| return new Promise(function (resolve, reject) { | ||
| const ghToken = process.env.GITHUB_TOKEN || process.env.GH_TOKEN; | ||
|
|
||
| function request(reqUrl, redirectCount) { | ||
| if (redirectCount > MAX_REDIRECTS) { | ||
| reject(new Error('Too many redirects fetching from ' + url)); | ||
| return; | ||
| } | ||
| const headers = { | ||
| 'User-Agent': userAgent, | ||
| 'Accept': 'application/octet-stream' | ||
| }; | ||
| if (ghToken) headers['Authorization'] = 'Bearer ' + ghToken; | ||
|
|
||
| const req = https.get(reqUrl, { headers: headers, timeout: timeoutMs }, function (res) { | ||
| const sc = res.statusCode; | ||
| if (sc === 301 || sc === 302 || sc === 307 || sc === 308) { | ||
| res.resume(); | ||
| var loc = res.headers.location; | ||
| if (loc && !loc.startsWith('https://')) { | ||
| reject(new Error('Refusing non-HTTPS redirect to ' + loc)); | ||
| return; | ||
| } | ||
| request(loc, redirectCount + 1); | ||
| return; | ||
| } | ||
| if (sc !== 200) { | ||
| res.resume(); | ||
| const hint = sc === 403 ? ' (rate limited - set GITHUB_TOKEN env var)' : ''; | ||
| reject(new Error('HTTP ' + sc + hint + ' fetching ' + reqUrl)); | ||
| return; | ||
| } | ||
| const chunks = []; | ||
| res.on('data', function (chunk) { chunks.push(chunk); }); | ||
| res.on('end', function () { resolve(Buffer.concat(chunks)); }); | ||
| res.on('error', reject); | ||
| }); | ||
| req.on('error', reject); | ||
| req.on('timeout', function () { | ||
| req.destroy(); | ||
| reject(new Error('Timeout (' + timeoutMs + 'ms) fetching ' + reqUrl)); | ||
| }); | ||
| } | ||
|
|
||
| request(url, 0); | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Extract a `.tar.gz` Buffer into `destDir` using the system `tar`. | ||
| * Available on Linux, macOS, and Windows (built into recent Win10/11). | ||
| * | ||
| * @param {Buffer} buf | ||
| * @param {string} destDir | ||
| * @returns {Promise<void>} | ||
| */ | ||
| function extractTarGz(buf, destDir) { | ||
| return new Promise(function (resolve, reject) { | ||
| const tarDest = process.platform === 'win32' ? destDir.replace(/\\/g, '/') : destDir; | ||
| const tar = cp.spawn('tar', ['xz', '-C', tarDest], { | ||
| stdio: ['pipe', 'pipe', 'pipe'] | ||
| }); | ||
| let stderr = ''; | ||
| tar.stderr.on('data', function (d) { stderr += d; }); | ||
| tar.stdin.write(buf); | ||
| tar.stdin.end(); | ||
| tar.on('close', function (code) { | ||
| if (code !== 0) { | ||
| reject(new Error('tar extraction failed (code ' + code + '): ' + stderr)); | ||
| } else { | ||
| resolve(); | ||
| } | ||
| }); | ||
| tar.on('error', reject); | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Extract a `.zip` Buffer into `destDir` using PowerShell's | ||
| * `Expand-Archive` (Windows-only). | ||
| * | ||
| * @param {Buffer} buf | ||
| * @param {string} destDir | ||
| * @param {string} binaryName - used as the temp-dir prefix | ||
| * @returns {Promise<void>} | ||
| */ | ||
| function extractZip(buf, destDir, binaryName) { | ||
| return new Promise(function (resolve, reject) { | ||
| var tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), binaryName + '-')); | ||
| var tmpZip = path.join(tmpDir, 'archive.zip'); | ||
| fs.writeFileSync(tmpZip, buf); | ||
| var ps = cp.spawn( | ||
| 'powershell', | ||
| ['-NoProfile', '-NonInteractive', '-Command', | ||
| 'Expand-Archive', '-Path', tmpZip, '-DestinationPath', destDir, '-Force'], | ||
| { stdio: ['ignore', 'pipe', 'pipe'] } | ||
| ); | ||
| var stderr = ''; | ||
| ps.stderr.on('data', function (d) { stderr += d; }); | ||
| ps.on('close', function (code) { | ||
| try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (e) { /* ignore */ } | ||
| if (code !== 0) { | ||
| reject(new Error('zip extraction failed (code ' + code + '): ' + stderr)); | ||
| } else { | ||
| resolve(); | ||
| } | ||
| }); | ||
| ps.on('error', reject); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the PowerShell process fails to spawn (e.g., if ps.on('error', function (err) {
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (e) { /* ignore */ }
reject(err);
}); |
||
| }); | ||
| } | ||
|
|
||
| module.exports = { | ||
| downloadToBuffer, | ||
| extractTarGz, | ||
| extractZip, | ||
| DEFAULT_DOWNLOAD_TIMEOUT_MS | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,171 @@ | ||
| /** | ||
| * Repo map cache management | ||
| * | ||
| * @module lib/repo-intel/cache | ||
| */ | ||
|
|
||
| 'use strict'; | ||
|
|
||
| const fs = require('fs'); | ||
| const path = require('path'); | ||
| const { getStateDirPath } = require('../platform/state-dir'); | ||
| const { writeJsonAtomic, writeFileAtomic } = require('../utils/atomic-write'); | ||
|
|
||
| const MAP_FILENAME = 'repo-map.json'; | ||
| const STALE_FILENAME = 'repo-map.stale'; | ||
| const INTEL_FILENAME = 'repo-intel.json'; | ||
|
|
||
| /** | ||
| * Get repo-map path (the converted view artifact). | ||
| * @param {string} basePath - Repository root | ||
| * @returns {string} | ||
| */ | ||
| function getMapPath(basePath) { | ||
| return path.join(getStateDirPath(basePath), MAP_FILENAME); | ||
| } | ||
|
|
||
| /** | ||
| * Get the RAW repo-intel.json path (the binary's native artifact). | ||
| * The embed submodule (orchestrator.js) feeds this to the embed binary's | ||
| * --map-file. In the standalone layout cache.getPath returned repo-intel.json; | ||
| * the fold renamed the converted-view accessor to getMapPath, so getPath keeps | ||
| * its original raw-artifact meaning. Mirrors index.js getIntelMapPath. | ||
| * @param {string} basePath - Repository root | ||
| * @returns {string} | ||
| */ | ||
| function getPath(basePath) { | ||
| return path.join(getStateDirPath(basePath), INTEL_FILENAME); | ||
| } | ||
|
|
||
| /** | ||
| * Get stale marker path | ||
| * @param {string} basePath - Repository root | ||
| * @returns {string} | ||
| */ | ||
| function getStalePath(basePath) { | ||
| return path.join(getStateDirPath(basePath), STALE_FILENAME); | ||
| } | ||
|
|
||
| /** | ||
| * Ensure state directory exists | ||
| * @param {string} basePath - Repository root | ||
| * @returns {string} | ||
| */ | ||
| function ensureStateDir(basePath) { | ||
| const stateDir = getStateDirPath(basePath); | ||
| if (!fs.existsSync(stateDir)) { | ||
| fs.mkdirSync(stateDir, { recursive: true }); | ||
| } | ||
| return stateDir; | ||
| } | ||
|
|
||
| /** | ||
| * Load repo-map from cache | ||
| * @param {string} basePath - Repository root | ||
| * @returns {Object|null} | ||
| */ | ||
| function load(basePath) { | ||
| const mapPath = getMapPath(basePath); | ||
| if (!fs.existsSync(mapPath)) return null; | ||
|
|
||
| try { | ||
| const raw = fs.readFileSync(mapPath, 'utf8'); | ||
| return JSON.parse(raw); | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Save repo-map to cache | ||
| * @param {string} basePath - Repository root | ||
| * @param {Object} map - Map object | ||
| */ | ||
| function save(basePath, map) { | ||
| ensureStateDir(basePath); | ||
| const mapPath = getMapPath(basePath); | ||
|
|
||
| const output = { | ||
| ...map, | ||
| updated: new Date().toISOString() | ||
| }; | ||
|
|
||
| writeJsonAtomic(mapPath, output); | ||
|
|
||
| // Clear stale marker if present | ||
| clearStale(basePath); | ||
| } | ||
|
|
||
| /** | ||
| * Check if repo-map exists | ||
| * @param {string} basePath - Repository root | ||
| * @returns {boolean} | ||
| */ | ||
| function exists(basePath) { | ||
| return fs.existsSync(getMapPath(basePath)); | ||
| } | ||
|
|
||
| /** | ||
| * Mark repo-map as stale | ||
| * @param {string} basePath - Repository root | ||
| */ | ||
| function markStale(basePath) { | ||
| ensureStateDir(basePath); | ||
| writeFileAtomic(getStalePath(basePath), new Date().toISOString()); | ||
| } | ||
|
|
||
| /** | ||
| * Clear stale marker | ||
| * @param {string} basePath - Repository root | ||
| */ | ||
| function clearStale(basePath) { | ||
| const stalePath = getStalePath(basePath); | ||
| if (fs.existsSync(stalePath)) { | ||
| fs.unlinkSync(stalePath); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Check if stale marker exists | ||
| * @param {string} basePath - Repository root | ||
| * @returns {boolean} | ||
| */ | ||
| function isMarkedStale(basePath) { | ||
| return fs.existsSync(getStalePath(basePath)); | ||
| } | ||
|
|
||
| /** | ||
| * Get basic status summary | ||
| * @param {string} basePath - Repository root | ||
| * @returns {Object|null} | ||
| */ | ||
| function getStatus(basePath) { | ||
| const map = load(basePath); | ||
| if (!map) return null; | ||
|
|
||
| return { | ||
| generated: map.generated, | ||
| updated: map.updated, | ||
| commit: map.git?.commit, | ||
| branch: map.git?.branch, | ||
| files: Object.keys(map.files || {}).length, | ||
| symbols: map.stats?.totalSymbols || 0, | ||
| languages: map.project?.languages || [] | ||
| }; | ||
| } | ||
|
|
||
| module.exports = { | ||
| load, | ||
| save, | ||
| exists, | ||
| getStatus, | ||
| getMapPath, | ||
| // getPath -> raw repo-intel.json (embed orchestrator); getStateDirPath | ||
| // re-exported for embed/preference.js. Both delegate to platform/state-dir | ||
| // and match the names the standalone cache exposed. | ||
| getPath, | ||
| getStateDirPath, | ||
| markStale, | ||
| clearStale, | ||
| isMarkedStale | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the redirect response does not contain a
Locationheader,locwill beundefined. Passingundefinedtorequest(loc, ...)will causehttps.get(undefined, ...)to throw aTypeErrorsynchronously. Since this synchronous throw occurs inside an asynchronous callback, it will not be caught by the Promise executor and will result in an unhandled exception that crashes the Node.js process. Always validate thatlocis a non-empty string before proceeding.