Synced from monorepo
Changes: - Non-blocking coding-data sharing upsell banner - Consolidate remediation in Doctor - Auto mode defers fail-closed gate asks to the classifier - Coalesce marketplace list fetches - Allow removing a marketplace source by name - Contain hung git marketplace sources (timeouts, non-blocking refresh, unbrick modal) - Label failed workspace RPCs with error_kind - Drop redundant explicit tonic/prost deps from xai-grok-shell - Report real exit codes for completed background shells - Narrow the date-rollover reminder to date-bearing templates - Wire toolOverrides through the session and agent - Security: Bash(git:*) allowlist matches whole command chain by prefix - Split prompt-trigger telemetry and record classifier provenance - Raise connectors-manager timeout to 60s - Auto classifier honors recorded approvals for repeat actions - Apply doctor fixes in the TUI - Auto-mode classifier timeouts prompt instead of silently denying - Scope subagent completion drains to the owning session - Add the toolOverrides wire types - Set client_identifier=grok-agent-sdk - Accept both spellings of the workspace-teleport kill switch - Persist one-shot occurrence journal - Stop turns that poll the exact same tool call 16x in a row - Copy compaction checkpoint files when forking sessions - Auto-focus permission prompt from scrollback - Esc cancels the running turn in non-vim and minimal modes - List Ctrl+Z undo and redo in keyboard shortcuts - Out-of-process macOS mic capture - Show active auth mode on session-info - Install the npm binary under $GROK_HOME - Remove hover/click dead zones between dashboard items - Route startup warnings to doctor - Document [feedback.user] author identity config - Extend bang command timeout - Close combine-queued edit-hold race - Integrate relocation recovery - Expose privacy notice rollout flag - Break harness discovery ref cycle so connections can idle-evict - Shift/Alt+Enter inserts newline when editing a queued prompt - Gate project Claude permissions on folder trust - Echo response.create.event_id on response.created - Toast when session creation fails from disk full - Add shared test process lifecycle - Enable dynamic workflows by default - Add relocation transaction state machine - Add shared test sandbox - Surface auth failures on model-switch compact - Persist durable scheduler expiry - Confirm before removing extensions-modal items - Re-run compact and prompt after login when compact hit expired auth - Recap sends hosted tools under backend search
This commit is contained in:
parent
3af4d5d398
commit
a5727c5960
482 changed files with 37627 additions and 13402 deletions
|
|
@ -1,17 +1,15 @@
|
|||
#!/usr/bin/env node
|
||||
// Thin trampoline: resolves the grok binary from the matching per-platform
|
||||
// optional dependency package and execs it.
|
||||
// Thin trampoline: resolves the grok binary and execs it.
|
||||
//
|
||||
// Falls back to bootstrapping the canonical ~/.grok/bin/grok-<version> symlink
|
||||
// layout if postinstall hasn't run (e.g. npx, or postinstall failure).
|
||||
// Resolution order:
|
||||
// 1. $GROK_HOME/bin/grok — canonical versioned symlink (installed by postinstall.js)
|
||||
// 2. bootstrap it from the per-platform @xai-official/grok-<platform> package,
|
||||
// decompressing the brotli payload straight into $GROK_HOME/bin
|
||||
// 3. last resort (no resolvable version or an unwritable home): decompress the
|
||||
// payload in place under node_modules and exec that
|
||||
//
|
||||
// Binary location strategy (in priority order):
|
||||
// 1. ~/.grok/bin/grok — canonical versioned symlink (postinstall.js)
|
||||
// 2. @xai-official/grok-<platform>/bin/grok[.exe] — decompressed sibling
|
||||
// 3. @xai-official/grok-<platform>/bin/grok[.exe].br — brotli-compressed
|
||||
//
|
||||
// Per-platform binaries are shipped brotli-compressed to stay well under
|
||||
// npm's ~200 MB tarball ceiling. See sibling packages @xai-official/grok-*.
|
||||
// Per-platform binaries ship brotli-compressed to stay under npm's ~200 MB
|
||||
// tarball ceiling. See sibling packages @xai-official/grok-*.
|
||||
const { spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
|
@ -22,7 +20,14 @@ const pkgName = '@xai-official/grok';
|
|||
const IS_WINDOWS = process.platform === 'win32';
|
||||
const EXE = IS_WINDOWS ? '.exe' : '';
|
||||
const BIN_NAME = `grok${EXE}`;
|
||||
const CANONICAL_DIR = path.join(os.homedir(), '.grok', 'bin');
|
||||
// $GROK_HOME/bin (else ~/.grok/bin), matching the Rust grok_home(), including
|
||||
// its canonicalized-home default (so a symlinked $HOME resolves the same way).
|
||||
function defaultGrokHome() {
|
||||
const home = os.homedir();
|
||||
try { return path.join(fs.realpathSync(home), '.grok'); } catch { return path.join(home, '.grok'); }
|
||||
}
|
||||
const GROK_HOME = process.env.GROK_HOME ?? defaultGrokHome();
|
||||
const CANONICAL_DIR = path.join(GROK_HOME, 'bin');
|
||||
const CANONICAL_PATH = path.join(CANONICAL_DIR, BIN_NAME);
|
||||
|
||||
function readLocalVersion() {
|
||||
|
|
@ -41,53 +46,63 @@ function resolvePlatformPackageDir() {
|
|||
}
|
||||
}
|
||||
|
||||
// Decompress a brotli-compressed binary to a sibling path. Atomic via tmp+rename.
|
||||
function decompressBrotli(brPath, outPath) {
|
||||
const compressed = fs.readFileSync(brPath);
|
||||
const decompressed = zlib.brotliDecompressSync(compressed);
|
||||
const tmp = outPath + `.tmp.${process.pid}`;
|
||||
fs.writeFileSync(tmp, decompressed);
|
||||
if (!IS_WINDOWS) fs.chmodSync(tmp, 0o755);
|
||||
try { fs.renameSync(tmp, outPath); } catch {}
|
||||
function writeVendorBinary(brPath, rawPath, destPath) {
|
||||
const tmp = destPath + `.tmp.${process.pid}`;
|
||||
try {
|
||||
if (fs.existsSync(brPath)) {
|
||||
fs.writeFileSync(tmp, zlib.brotliDecompressSync(fs.readFileSync(brPath)));
|
||||
} else if (fs.existsSync(rawPath)) {
|
||||
fs.copyFileSync(rawPath, tmp);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
if (!IS_WINDOWS) fs.chmodSync(tmp, 0o755);
|
||||
fs.renameSync(tmp, destPath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
} finally {
|
||||
try { fs.unlinkSync(tmp); } catch {}
|
||||
}
|
||||
}
|
||||
|
||||
// Bootstrap the canonical versioned-symlink layout from a source binary.
|
||||
// Returns the canonical path on success, or the source path on failure.
|
||||
function bootstrapCanonical(sourceBinPath, version) {
|
||||
function swapCanonical(versionedName, versionedPath) {
|
||||
if (!IS_WINDOWS) {
|
||||
const tmpLink = CANONICAL_PATH + `.link.${process.pid}`;
|
||||
try { fs.unlinkSync(tmpLink); } catch {}
|
||||
fs.symlinkSync(versionedName, tmpLink);
|
||||
fs.renameSync(tmpLink, CANONICAL_PATH);
|
||||
return;
|
||||
}
|
||||
const oldPath = CANONICAL_PATH + '.old';
|
||||
try { fs.unlinkSync(oldPath); } catch {}
|
||||
try {
|
||||
try { fs.unlinkSync(CANONICAL_PATH); } catch {}
|
||||
fs.copyFileSync(versionedPath, CANONICAL_PATH);
|
||||
} catch {
|
||||
fs.renameSync(CANONICAL_PATH, oldPath);
|
||||
try {
|
||||
fs.copyFileSync(versionedPath, CANONICAL_PATH);
|
||||
} catch {
|
||||
try { fs.renameSync(oldPath, CANONICAL_PATH); } catch {}
|
||||
throw new Error('locked');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function bootstrapCanonical(brPath, rawPath, version) {
|
||||
try {
|
||||
fs.mkdirSync(CANONICAL_DIR, { recursive: true });
|
||||
const versionedName = `grok-${version}${EXE}`;
|
||||
const versionedPath = path.join(CANONICAL_DIR, versionedName);
|
||||
if (!fs.existsSync(versionedPath)) {
|
||||
const tmpPath = versionedPath + `.tmp.${process.pid}`;
|
||||
fs.copyFileSync(sourceBinPath, tmpPath);
|
||||
if (!IS_WINDOWS) fs.chmodSync(tmpPath, 0o755);
|
||||
fs.renameSync(tmpPath, versionedPath);
|
||||
if (!fs.existsSync(versionedPath) && !writeVendorBinary(brPath, rawPath, versionedPath)) {
|
||||
return null;
|
||||
}
|
||||
if (IS_WINDOWS) {
|
||||
const oldPath = CANONICAL_PATH + '.old';
|
||||
try { fs.unlinkSync(oldPath); } catch {}
|
||||
try {
|
||||
try { fs.unlinkSync(CANONICAL_PATH); } catch {}
|
||||
fs.copyFileSync(versionedPath, CANONICAL_PATH);
|
||||
} catch {
|
||||
fs.renameSync(CANONICAL_PATH, oldPath);
|
||||
try {
|
||||
fs.copyFileSync(versionedPath, CANONICAL_PATH);
|
||||
} catch {
|
||||
try { fs.renameSync(oldPath, CANONICAL_PATH); } catch {}
|
||||
throw new Error('locked');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const tmpLink = CANONICAL_PATH + `.link.${process.pid}`;
|
||||
try { fs.unlinkSync(tmpLink); } catch {}
|
||||
fs.symlinkSync(versionedName, tmpLink);
|
||||
fs.renameSync(tmpLink, CANONICAL_PATH);
|
||||
}
|
||||
return CANONICAL_PATH;
|
||||
swapCanonical(versionedName, versionedPath);
|
||||
// null on a broken wire-up so the caller falls back to in-place launch.
|
||||
return fs.existsSync(CANONICAL_PATH) ? CANONICAL_PATH : null;
|
||||
} catch {
|
||||
return sourceBinPath;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -105,22 +120,20 @@ function resolveBinary() {
|
|||
|
||||
const rawPath = path.join(platformDir, 'bin', BIN_NAME);
|
||||
const brPath = rawPath + '.br';
|
||||
const version = readLocalVersion();
|
||||
|
||||
// Decompress on first use if needed (atomic via tmp+rename).
|
||||
if (!fs.existsSync(rawPath)) {
|
||||
if (fs.existsSync(brPath)) {
|
||||
decompressBrotli(brPath, rawPath);
|
||||
}
|
||||
// Prefer the canonical layout, decompressing straight into CANONICAL_DIR so
|
||||
// no second uncompressed copy lands under node_modules.
|
||||
if (version) {
|
||||
const bootstrapped = bootstrapCanonical(brPath, rawPath, version);
|
||||
if (bootstrapped) return bootstrapped;
|
||||
}
|
||||
if (!fs.existsSync(rawPath)) {
|
||||
|
||||
// Fallback (unresolved version or unwritable home): materialize in place.
|
||||
if (!fs.existsSync(rawPath) && !writeVendorBinary(brPath, rawPath, rawPath)) {
|
||||
console.error(`${pkgName}: missing binary at ${rawPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const version = readLocalVersion();
|
||||
if (version) {
|
||||
return bootstrapCanonical(rawPath, version);
|
||||
}
|
||||
return rawPath;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,15 @@ const zlib = require('zlib');
|
|||
const { execSync } = require('child_process');
|
||||
const TOML = require('@iarna/toml');
|
||||
|
||||
const CANONICAL_DIR = path.join(os.homedir(), '.grok', 'bin');
|
||||
// $GROK_HOME (else ~/.grok), matching the Rust grok_home() including its
|
||||
// canonicalized-home default. Lets fleets relocate the binary off a slow $HOME
|
||||
// (NFS); old code hardcoded os.homedir().
|
||||
function defaultGrokHome() {
|
||||
const home = os.homedir();
|
||||
try { return path.join(fs.realpathSync(home), '.grok'); } catch { return path.join(home, '.grok'); }
|
||||
}
|
||||
const GROK_HOME = process.env.GROK_HOME ?? defaultGrokHome();
|
||||
const CANONICAL_DIR = path.join(GROK_HOME, 'bin');
|
||||
|
||||
const key = `${process.platform}-${process.arch}`;
|
||||
const SUPPORTED = new Set([
|
||||
|
|
@ -57,43 +65,39 @@ const EXE = IS_WINDOWS ? '.exe' : '';
|
|||
|
||||
fs.mkdirSync(CANONICAL_DIR, { recursive: true });
|
||||
|
||||
// Install a vendored binary: versioned filename + symlink (Unix) or copy (Windows).
|
||||
// Binaries are shipped brotli-compressed in the per-platform npm tarball to keep
|
||||
// each sub-package well under npm's ~200 MB tarball limit. This function
|
||||
// decompresses them before installing into the canonical layout.
|
||||
function writeVendorBinary(brPath, rawPath, destPath) {
|
||||
const tmp = destPath + `.tmp.${process.pid}`;
|
||||
try {
|
||||
if (fs.existsSync(brPath)) {
|
||||
fs.writeFileSync(tmp, zlib.brotliDecompressSync(fs.readFileSync(brPath)));
|
||||
} else if (fs.existsSync(rawPath)) {
|
||||
fs.copyFileSync(rawPath, tmp);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
if (!IS_WINDOWS) fs.chmodSync(tmp, 0o755);
|
||||
fs.renameSync(tmp, destPath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
} finally {
|
||||
try { fs.unlinkSync(tmp); } catch {}
|
||||
}
|
||||
}
|
||||
|
||||
function installBinary(binName, sourceDir, vendorSubpath) {
|
||||
const brPath = path.join(sourceDir, 'bin', vendorSubpath + '.br');
|
||||
const rawPath = path.join(sourceDir, 'bin', vendorSubpath);
|
||||
let vendoredBinPath;
|
||||
if (fs.existsSync(brPath)) {
|
||||
const compressed = fs.readFileSync(brPath);
|
||||
const decompressed = zlib.brotliDecompressSync(compressed);
|
||||
vendoredBinPath = rawPath;
|
||||
fs.writeFileSync(vendoredBinPath, decompressed);
|
||||
if (!IS_WINDOWS) fs.chmodSync(vendoredBinPath, 0o755);
|
||||
try { fs.unlinkSync(brPath); } catch {}
|
||||
} else if (fs.existsSync(rawPath)) {
|
||||
vendoredBinPath = rawPath;
|
||||
} else {
|
||||
console.error(`@xai-official/grok: missing binary at ${brPath}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const versionedName = `${binName}-${version}${EXE}`;
|
||||
const versionedPath = path.join(CANONICAL_DIR, versionedName);
|
||||
const canonicalName = `${binName}${EXE}`;
|
||||
const canonicalPath = path.join(CANONICAL_DIR, canonicalName);
|
||||
|
||||
// Only copy if this exact version isn't already installed.
|
||||
if (!fs.existsSync(versionedPath)) {
|
||||
const tmpPath = versionedPath + `.tmp.${process.pid}`;
|
||||
try {
|
||||
fs.copyFileSync(vendoredBinPath, tmpPath);
|
||||
if (!IS_WINDOWS) fs.chmodSync(tmpPath, 0o755);
|
||||
fs.renameSync(tmpPath, versionedPath);
|
||||
} finally {
|
||||
try { fs.unlinkSync(tmpPath); } catch {}
|
||||
}
|
||||
// Skip if this exact version is already installed.
|
||||
if (!fs.existsSync(versionedPath) && !writeVendorBinary(brPath, rawPath, versionedPath)) {
|
||||
console.error(`@xai-official/grok: missing binary at ${brPath}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (IS_WINDOWS) {
|
||||
|
|
@ -128,10 +132,28 @@ function installBinary(binName, sourceDir, vendorSubpath) {
|
|||
fs.renameSync(tmpLink, canonicalPath);
|
||||
}
|
||||
|
||||
// Don't report a broken wire-up as success.
|
||||
if (!fs.existsSync(canonicalPath)) {
|
||||
console.error(`@xai-official/grok: ${canonicalName} did not resolve after install`);
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(`${binName} ${version} installed to ${canonicalPath} -> ${versionedName}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Comparator: sort "<prefix>X.Y.Z" filenames by version, newest first.
|
||||
function byVersionDescending(prefix) {
|
||||
return (a, b) => {
|
||||
const pa = a.slice(prefix.length).split('.').map(Number);
|
||||
const pb = b.slice(prefix.length).split('.').map(Number);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if ((pa[i] || 0) !== (pb[i] || 0)) return (pb[i] || 0) - (pa[i] || 0);
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
}
|
||||
|
||||
// Best-effort cleanup of old versioned binaries for a given binary name.
|
||||
// Keeps the current version and the previous one (in case a process is still
|
||||
// running the old binary and hasn't fully loaded all pages yet).
|
||||
|
|
@ -149,14 +171,7 @@ function cleanupOldVersions(binName) {
|
|||
const suffix = e.slice(prefix.length);
|
||||
return /^\d/.test(suffix);
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const pa = a.slice(prefix.length).split('.').map(Number);
|
||||
const pb = b.slice(prefix.length).split('.').map(Number);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if ((pa[i] || 0) !== (pb[i] || 0)) return (pb[i] || 0) - (pa[i] || 0);
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
.sort(byVersionDescending(prefix));
|
||||
for (const old of versionedBinaries.slice(1)) {
|
||||
try { fs.unlinkSync(path.join(CANONICAL_DIR, old)); } catch {}
|
||||
}
|
||||
|
|
@ -176,7 +191,7 @@ cleanupOldVersions('grok');
|
|||
cleanupOldVersions('grok-pager');
|
||||
|
||||
// Write installer config
|
||||
const configDir = path.join(os.homedir(), '.grok');
|
||||
const configDir = GROK_HOME;
|
||||
const configPath = path.join(configDir, 'config.toml');
|
||||
let obj = {};
|
||||
try { obj = TOML.parse(fs.readFileSync(configPath, 'utf8')); } catch { }
|
||||
|
|
@ -208,7 +223,7 @@ const GROK_PATH = path.join(CANONICAL_DIR, `grok${EXE}`);
|
|||
if (process.env.GROK_INSTALL_COMPLETIONS === '1' && !IS_WINDOWS) {
|
||||
try {
|
||||
const { spawnSync } = require('child_process');
|
||||
const completionsDir = path.join(os.homedir(), '.grok', 'completions');
|
||||
const completionsDir = path.join(GROK_HOME, 'completions');
|
||||
const bashPath = path.join(completionsDir, 'bash', 'grok.bash');
|
||||
const zshPath = path.join(completionsDir, 'zsh', '_grok');
|
||||
fs.mkdirSync(path.dirname(bashPath), { recursive: true });
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const zlib = require('zlib');
|
||||
const assert = require('assert');
|
||||
|
||||
let passed = 0;
|
||||
|
|
@ -36,14 +37,16 @@ function cleanup(dir) {
|
|||
|
||||
// ─── Extracted logic (mirrors postinstall.js and bin/grok exactly) ─────
|
||||
|
||||
/** Semver-aware descending sort for "grok-X.Y.Z" filenames. */
|
||||
function semverSortDescending(a, b) {
|
||||
const pa = a.slice(5).split('.').map(Number);
|
||||
const pb = b.slice(5).split('.').map(Number);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if ((pa[i] || 0) !== (pb[i] || 0)) return (pb[i] || 0) - (pa[i] || 0);
|
||||
}
|
||||
return 0;
|
||||
/** Comparator: sort "<prefix>X.Y.Z" filenames by version, newest first. */
|
||||
function byVersionDescending(prefix) {
|
||||
return (a, b) => {
|
||||
const pa = a.slice(prefix.length).split('.').map(Number);
|
||||
const pb = b.slice(prefix.length).split('.').map(Number);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if ((pa[i] || 0) !== (pb[i] || 0)) return (pb[i] || 0) - (pa[i] || 0);
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
}
|
||||
|
||||
/** Install a versioned binary + atomic symlink (same as postinstall.js). */
|
||||
|
|
@ -78,7 +81,7 @@ function cleanupOldVersions(canonicalDir, currentVersionedName) {
|
|||
const entries = fs.readdirSync(canonicalDir);
|
||||
const versionedBinaries = entries
|
||||
.filter(e => e.startsWith('grok-') && !e.includes('.tmp.') && !e.includes('.link.') && e !== currentVersionedName)
|
||||
.sort(semverSortDescending);
|
||||
.sort(byVersionDescending('grok-'));
|
||||
// Keep the most recent old version, remove anything older.
|
||||
for (const old of versionedBinaries.slice(1)) {
|
||||
try { fs.unlinkSync(path.join(canonicalDir, old)); } catch {}
|
||||
|
|
@ -86,6 +89,60 @@ function cleanupOldVersions(canonicalDir, currentVersionedName) {
|
|||
return versionedBinaries;
|
||||
}
|
||||
|
||||
/** Grok bin dir resolution (mirrors postinstall.js and bin/grok). */
|
||||
function resolveGrokBinDir(env, homedir) {
|
||||
const grokHome = env.GROK_HOME ?? path.join(homedir, '.grok');
|
||||
return path.join(grokHome, 'bin');
|
||||
}
|
||||
|
||||
/** Materialize the vendored binary at destPath (mirrors writeVendorBinary). */
|
||||
function writeVendorBinary(brPath, rawPath, destPath) {
|
||||
const tmp = destPath + `.tmp.${process.pid}`;
|
||||
try {
|
||||
if (fs.existsSync(brPath)) {
|
||||
fs.writeFileSync(tmp, zlib.brotliDecompressSync(fs.readFileSync(brPath)));
|
||||
} else if (fs.existsSync(rawPath)) {
|
||||
fs.copyFileSync(rawPath, tmp);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
fs.chmodSync(tmp, 0o755);
|
||||
fs.renameSync(tmp, destPath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
} finally {
|
||||
try { fs.unlinkSync(tmp); } catch {}
|
||||
}
|
||||
}
|
||||
|
||||
/** Decompress a brotli payload into the canonical dir (mirrors installBinary). */
|
||||
function installBinaryFromBrotli(brPath, version, canonicalDir) {
|
||||
fs.mkdirSync(canonicalDir, { recursive: true });
|
||||
const versionedName = `grok-${version}`;
|
||||
const versionedPath = path.join(canonicalDir, versionedName);
|
||||
const canonicalPath = path.join(canonicalDir, 'grok');
|
||||
|
||||
if (!fs.existsSync(versionedPath)) {
|
||||
const tmpPath = versionedPath + `.tmp.${process.pid}`;
|
||||
try {
|
||||
const decompressed = zlib.brotliDecompressSync(fs.readFileSync(brPath));
|
||||
fs.writeFileSync(tmpPath, decompressed);
|
||||
fs.chmodSync(tmpPath, 0o755);
|
||||
fs.renameSync(tmpPath, versionedPath);
|
||||
} finally {
|
||||
try { fs.unlinkSync(tmpPath); } catch {}
|
||||
}
|
||||
}
|
||||
|
||||
const tmpLink = canonicalPath + `.link.${process.pid}`;
|
||||
try { fs.unlinkSync(tmpLink); } catch {}
|
||||
fs.symlinkSync(versionedName, tmpLink);
|
||||
fs.renameSync(tmpLink, canonicalPath);
|
||||
|
||||
return { canonicalPath, versionedPath, versionedName };
|
||||
}
|
||||
|
||||
/** Bootstrap canonical from vendored (same as bin/grok trampoline). */
|
||||
function bootstrapCanonical(vendoredBinPath, version, canonicalDir) {
|
||||
const canonicalPath = path.join(canonicalDir, 'grok');
|
||||
|
|
@ -458,9 +515,9 @@ test('semver sort: minor version boundary (0.1.x vs 0.2.x)', () => {
|
|||
}
|
||||
});
|
||||
|
||||
test('semverSortDescending: unit test comparator directly', () => {
|
||||
test('byVersionDescending: unit test comparator directly', () => {
|
||||
const input = ['grok-0.1.9', 'grok-0.1.10', 'grok-0.1.2', 'grok-1.0.0', 'grok-0.2.0'];
|
||||
const sorted = [...input].sort(semverSortDescending);
|
||||
const sorted = [...input].sort(byVersionDescending('grok-'));
|
||||
assert.deepStrictEqual(sorted, [
|
||||
'grok-1.0.0',
|
||||
'grok-0.2.0',
|
||||
|
|
@ -674,14 +731,7 @@ function cleanupOldVersionsNamed(canonicalDir, binName, version) {
|
|||
const suffix = e.slice(prefix.length);
|
||||
return /^\d/.test(suffix);
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const pa = a.slice(prefix.length).split('.').map(Number);
|
||||
const pb = b.slice(prefix.length).split('.').map(Number);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if ((pa[i] || 0) !== (pb[i] || 0)) return (pb[i] || 0) - (pa[i] || 0);
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
.sort(byVersionDescending(prefix));
|
||||
for (const old of versionedBinaries.slice(1)) {
|
||||
try { fs.unlinkSync(path.join(canonicalDir, old)); } catch {}
|
||||
}
|
||||
|
|
@ -977,6 +1027,58 @@ test('canonical pager from non-npm install is preserved on Linux', () => {
|
|||
}
|
||||
});
|
||||
|
||||
console.log('\ngrok home + brotli install tests\n');
|
||||
|
||||
test('resolveGrokBinDir honors $GROK_HOME, else falls back to <home>/.grok/bin', () => {
|
||||
assert.strictEqual(
|
||||
resolveGrokBinDir({ GROK_HOME: '/fast/local/.grok' }, '/home/alice'),
|
||||
path.join('/fast/local/.grok', 'bin'),
|
||||
);
|
||||
assert.strictEqual(
|
||||
resolveGrokBinDir({}, '/home/alice'),
|
||||
path.join('/home/alice', '.grok', 'bin'),
|
||||
);
|
||||
assert.strictEqual(resolveGrokBinDir({ GROK_HOME: '' }, '/home/alice'), path.join('', 'bin'));
|
||||
});
|
||||
|
||||
test('writeVendorBinary returns false (not true) when the destination cannot be written', () => {
|
||||
const dir = makeTmpDir();
|
||||
try {
|
||||
const brPath = path.join(dir, 'grok.br');
|
||||
fs.writeFileSync(brPath, zlib.brotliCompressSync(Buffer.from('binary')));
|
||||
|
||||
// A non-empty directory at destPath makes the final rename fail.
|
||||
const dest = path.join(dir, 'dest');
|
||||
fs.mkdirSync(dest);
|
||||
fs.writeFileSync(path.join(dest, 'child'), 'x');
|
||||
|
||||
assert.strictEqual(writeVendorBinary(brPath, path.join(dir, 'raw'), dest), false);
|
||||
assert.ok(!fs.existsSync(`${dest}.tmp.${process.pid}`), 'temp file is cleaned up on failure');
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('decompresses brotli into the canonical dir without duplicating into node_modules', () => {
|
||||
const dir = makeTmpDir();
|
||||
try {
|
||||
const vendorBin = path.join(dir, 'node_modules', 'bin');
|
||||
fs.mkdirSync(vendorBin, { recursive: true });
|
||||
const brPath = path.join(vendorBin, 'grok.br');
|
||||
fs.writeFileSync(brPath, zlib.brotliCompressSync(Buffer.from('native-binary-bytes')));
|
||||
|
||||
const binDir = path.join(dir, '.grok', 'bin');
|
||||
const result = installBinaryFromBrotli(brPath, '0.1.220', binDir);
|
||||
|
||||
assert.ok(fs.lstatSync(result.canonicalPath).isSymbolicLink());
|
||||
assert.strictEqual(fs.readFileSync(result.canonicalPath, 'utf8'), 'native-binary-bytes');
|
||||
assert.ok(!fs.existsSync(path.join(vendorBin, 'grok')), 'no uncompressed binary in node_modules');
|
||||
assert.ok(fs.existsSync(brPath), 'compressed .br payload is preserved');
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
// ─── Summary ───────────────────────────────────────────────────────────
|
||||
|
||||
console.log(`\n${passed} passed, ${failed} failed`);
|
||||
|
|
|
|||
Loading…
Reference in a new issue