Publish harness and TUI open-source
initial sync from the monorepo
This commit is contained in:
commit
c68e39f604
2734 changed files with 1437016 additions and 0 deletions
4
crates/codegen/xai-grok-pager/npm/grok-darwin-arm64/.gitignore
vendored
Normal file
4
crates/codegen/xai-grok-pager/npm/grok-darwin-arm64/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
bin/*.br
|
||||
bin/grok
|
||||
bin/grok.exe
|
||||
THIRD_PARTY_NOTICES.md
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
# @xai-official/grok-darwin-arm64
|
||||
|
||||
Platform-specific binary for [`@xai-official/grok`](https://www.npmjs.com/package/@xai-official/grok) on darwin-arm64.
|
||||
|
||||
Do not install this package directly. Install the main package instead:
|
||||
|
||||
```sh
|
||||
npm install -g @xai-official/grok
|
||||
```
|
||||
|
||||
The main package will automatically pull the correct binary for your platform via `optionalDependencies`.
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "@xai-official/grok-darwin-arm64",
|
||||
"version": "0.2.0-dev",
|
||||
"description": "darwin-arm64 binary for @xai-official/grok. Do not install directly; install @xai-official/grok instead.",
|
||||
"license": "Apache-2.0",
|
||||
"files": [
|
||||
"bin/",
|
||||
"THIRD_PARTY_NOTICES.md"
|
||||
],
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
4
crates/codegen/xai-grok-pager/npm/grok-darwin-x64/.gitignore
vendored
Normal file
4
crates/codegen/xai-grok-pager/npm/grok-darwin-x64/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
bin/*.br
|
||||
bin/grok
|
||||
bin/grok.exe
|
||||
THIRD_PARTY_NOTICES.md
|
||||
11
crates/codegen/xai-grok-pager/npm/grok-darwin-x64/README.md
Normal file
11
crates/codegen/xai-grok-pager/npm/grok-darwin-x64/README.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# @xai-official/grok-darwin-x64
|
||||
|
||||
Platform-specific binary for [`@xai-official/grok`](https://www.npmjs.com/package/@xai-official/grok) on darwin-x64.
|
||||
|
||||
Do not install this package directly. Install the main package instead:
|
||||
|
||||
```sh
|
||||
npm install -g @xai-official/grok
|
||||
```
|
||||
|
||||
The main package will automatically pull the correct binary for your platform via `optionalDependencies`.
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "@xai-official/grok-darwin-x64",
|
||||
"version": "0.2.0-dev",
|
||||
"description": "darwin-x64 binary for @xai-official/grok. Do not install directly; install @xai-official/grok instead.",
|
||||
"license": "Apache-2.0",
|
||||
"files": [
|
||||
"bin/",
|
||||
"THIRD_PARTY_NOTICES.md"
|
||||
],
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
4
crates/codegen/xai-grok-pager/npm/grok-linux-arm64/.gitignore
vendored
Normal file
4
crates/codegen/xai-grok-pager/npm/grok-linux-arm64/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
bin/*.br
|
||||
bin/grok
|
||||
bin/grok.exe
|
||||
THIRD_PARTY_NOTICES.md
|
||||
11
crates/codegen/xai-grok-pager/npm/grok-linux-arm64/README.md
Normal file
11
crates/codegen/xai-grok-pager/npm/grok-linux-arm64/README.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# @xai-official/grok-linux-arm64
|
||||
|
||||
Platform-specific binary for [`@xai-official/grok`](https://www.npmjs.com/package/@xai-official/grok) on linux-arm64.
|
||||
|
||||
Do not install this package directly. Install the main package instead:
|
||||
|
||||
```sh
|
||||
npm install -g @xai-official/grok
|
||||
```
|
||||
|
||||
The main package will automatically pull the correct binary for your platform via `optionalDependencies`.
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "@xai-official/grok-linux-arm64",
|
||||
"version": "0.2.0-dev",
|
||||
"description": "linux-arm64 binary for @xai-official/grok. Do not install directly; install @xai-official/grok instead.",
|
||||
"license": "Apache-2.0",
|
||||
"files": [
|
||||
"bin/",
|
||||
"THIRD_PARTY_NOTICES.md"
|
||||
],
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
4
crates/codegen/xai-grok-pager/npm/grok-linux-x64/.gitignore
vendored
Normal file
4
crates/codegen/xai-grok-pager/npm/grok-linux-x64/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
bin/*.br
|
||||
bin/grok
|
||||
bin/grok.exe
|
||||
THIRD_PARTY_NOTICES.md
|
||||
11
crates/codegen/xai-grok-pager/npm/grok-linux-x64/README.md
Normal file
11
crates/codegen/xai-grok-pager/npm/grok-linux-x64/README.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# @xai-official/grok-linux-x64
|
||||
|
||||
Platform-specific binary for [`@xai-official/grok`](https://www.npmjs.com/package/@xai-official/grok) on linux-x64.
|
||||
|
||||
Do not install this package directly. Install the main package instead:
|
||||
|
||||
```sh
|
||||
npm install -g @xai-official/grok
|
||||
```
|
||||
|
||||
The main package will automatically pull the correct binary for your platform via `optionalDependencies`.
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "@xai-official/grok-linux-x64",
|
||||
"version": "0.2.0-dev",
|
||||
"description": "linux-x64 binary for @xai-official/grok. Do not install directly; install @xai-official/grok instead.",
|
||||
"license": "Apache-2.0",
|
||||
"files": [
|
||||
"bin/",
|
||||
"THIRD_PARTY_NOTICES.md"
|
||||
],
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
4
crates/codegen/xai-grok-pager/npm/grok-win32-arm64/.gitignore
vendored
Normal file
4
crates/codegen/xai-grok-pager/npm/grok-win32-arm64/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
bin/*.br
|
||||
bin/grok
|
||||
bin/grok.exe
|
||||
THIRD_PARTY_NOTICES.md
|
||||
11
crates/codegen/xai-grok-pager/npm/grok-win32-arm64/README.md
Normal file
11
crates/codegen/xai-grok-pager/npm/grok-win32-arm64/README.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# @xai-official/grok-win32-arm64
|
||||
|
||||
Platform-specific binary for [`@xai-official/grok`](https://www.npmjs.com/package/@xai-official/grok) on win32-arm64.
|
||||
|
||||
Do not install this package directly. Install the main package instead:
|
||||
|
||||
```sh
|
||||
npm install -g @xai-official/grok
|
||||
```
|
||||
|
||||
The main package will automatically pull the correct binary for your platform via `optionalDependencies`.
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "@xai-official/grok-win32-arm64",
|
||||
"version": "0.2.0-dev",
|
||||
"description": "win32-arm64 binary for @xai-official/grok. Do not install directly; install @xai-official/grok instead.",
|
||||
"license": "Apache-2.0",
|
||||
"files": [
|
||||
"bin/",
|
||||
"THIRD_PARTY_NOTICES.md"
|
||||
],
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
4
crates/codegen/xai-grok-pager/npm/grok-win32-x64/.gitignore
vendored
Normal file
4
crates/codegen/xai-grok-pager/npm/grok-win32-x64/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
bin/*.br
|
||||
bin/grok
|
||||
bin/grok.exe
|
||||
THIRD_PARTY_NOTICES.md
|
||||
11
crates/codegen/xai-grok-pager/npm/grok-win32-x64/README.md
Normal file
11
crates/codegen/xai-grok-pager/npm/grok-win32-x64/README.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# @xai-official/grok-win32-x64
|
||||
|
||||
Platform-specific binary for [`@xai-official/grok`](https://www.npmjs.com/package/@xai-official/grok) on win32-x64.
|
||||
|
||||
Do not install this package directly. Install the main package instead:
|
||||
|
||||
```sh
|
||||
npm install -g @xai-official/grok
|
||||
```
|
||||
|
||||
The main package will automatically pull the correct binary for your platform via `optionalDependencies`.
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "@xai-official/grok-win32-x64",
|
||||
"version": "0.2.0-dev",
|
||||
"description": "win32-x64 binary for @xai-official/grok. Do not install directly; install @xai-official/grok instead.",
|
||||
"license": "Apache-2.0",
|
||||
"files": [
|
||||
"bin/",
|
||||
"THIRD_PARTY_NOTICES.md"
|
||||
],
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
61
crates/codegen/xai-grok-pager/npm/grok/README.md
Normal file
61
crates/codegen/xai-grok-pager/npm/grok/README.md
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
# Grok
|
||||
|
||||
Bring Grok into your terminal. Fast, flicker-free CLI built for plans, subagents, and parallel work.
|
||||
|
||||
**[Homepage](https://x.ai/cli)** | **[Documentation](https://docs.x.ai/build/overview)**
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
curl -fsSL https://x.ai/cli/install.sh | bash
|
||||
```
|
||||
|
||||
Or install with npm:
|
||||
|
||||
```bash
|
||||
npm i -g @xai-official/grok
|
||||
```
|
||||
|
||||
## Get Started
|
||||
|
||||
```bash
|
||||
# Launch the interactive TUI
|
||||
grok
|
||||
|
||||
# Run a single task
|
||||
grok -p "Explain this codebase"
|
||||
```
|
||||
|
||||
On first launch, Grok opens your browser to authenticate. For CI or headless environments, use an API key from [console.x.ai](https://console.x.ai):
|
||||
|
||||
```bash
|
||||
export XAI_API_KEY="xai-..."
|
||||
```
|
||||
|
||||
## Update
|
||||
|
||||
```bash
|
||||
grok update
|
||||
```
|
||||
|
||||
Or if installed via npm:
|
||||
|
||||
```bash
|
||||
npm i -g @xai-official/grok@latest
|
||||
```
|
||||
|
||||
## Supported Platforms
|
||||
|
||||
| Platform | Architecture |
|
||||
|---|---|
|
||||
| macOS | Apple Silicon (arm64) |
|
||||
| Linux | x86_64, arm64 |
|
||||
| Windows | x86_64 |
|
||||
|
||||
## Documentation
|
||||
|
||||
For full documentation including configuration, MCP servers, custom models, headless mode, agent mode, and more, visit [docs.x.ai/build/overview](https://docs.x.ai/build/overview).
|
||||
|
||||
## Feedback
|
||||
|
||||
Run `/feedback` inside Grok to report issues or send feedback directly.
|
||||
136
crates/codegen/xai-grok-pager/npm/grok/bin/grok
Executable file
136
crates/codegen/xai-grok-pager/npm/grok/bin/grok
Executable file
|
|
@ -0,0 +1,136 @@
|
|||
#!/usr/bin/env node
|
||||
// Thin trampoline: resolves the grok binary from the matching per-platform
|
||||
// optional dependency package 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).
|
||||
//
|
||||
// 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-*.
|
||||
const { spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const zlib = require('zlib');
|
||||
|
||||
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');
|
||||
const CANONICAL_PATH = path.join(CANONICAL_DIR, BIN_NAME);
|
||||
|
||||
function readLocalVersion() {
|
||||
try { return require('../package.json').version; } catch { return undefined; }
|
||||
}
|
||||
|
||||
// Resolve the per-platform sibling package's directory. Returns null if the
|
||||
// matching optional dependency wasn't installed (unsupported platform, or
|
||||
// npm refused to install optionalDependencies).
|
||||
function resolvePlatformPackageDir() {
|
||||
const platformPkg = `@xai-official/grok-${process.platform}-${process.arch}`;
|
||||
try {
|
||||
return path.dirname(require.resolve(`${platformPkg}/package.json`));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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 (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;
|
||||
} catch {
|
||||
return sourceBinPath;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveBinary() {
|
||||
if (fs.existsSync(CANONICAL_PATH)) return CANONICAL_PATH;
|
||||
|
||||
const platformDir = resolvePlatformPackageDir();
|
||||
if (!platformDir) {
|
||||
console.error(`${pkgName}: no platform binary installed for ${process.platform}-${process.arch}.`);
|
||||
console.error(` Expected sibling package @xai-official/grok-${process.platform}-${process.arch}.`);
|
||||
console.error(` This usually means npm skipped optionalDependencies (e.g. --no-optional)`);
|
||||
console.error(` or the platform is not supported.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const rawPath = path.join(platformDir, 'bin', BIN_NAME);
|
||||
const brPath = rawPath + '.br';
|
||||
|
||||
// Decompress on first use if needed (atomic via tmp+rename).
|
||||
if (!fs.existsSync(rawPath)) {
|
||||
if (fs.existsSync(brPath)) {
|
||||
decompressBrotli(brPath, rawPath);
|
||||
}
|
||||
}
|
||||
if (!fs.existsSync(rawPath)) {
|
||||
console.error(`${pkgName}: missing binary at ${rawPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const version = readLocalVersion();
|
||||
if (version) {
|
||||
return bootstrapCanonical(rawPath, version);
|
||||
}
|
||||
return rawPath;
|
||||
}
|
||||
|
||||
const execPath = resolveBinary();
|
||||
const childEnv = { ...process.env, GROK_MANAGED_BY_NPM: '1' };
|
||||
const child = spawn(execPath, process.argv.slice(2), { stdio: 'inherit', env: childEnv });
|
||||
child.on('exit', (code, signal) => {
|
||||
if (signal) {
|
||||
process.kill(process.pid, signal);
|
||||
} else {
|
||||
process.exit(code ?? 0);
|
||||
}
|
||||
});
|
||||
225
crates/codegen/xai-grok-pager/npm/grok/bin/postinstall.js
Normal file
225
crates/codegen/xai-grok-pager/npm/grok/bin/postinstall.js
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
#!/usr/bin/env node
|
||||
// Runs once after npm install/update. Reads the grok binary from the
|
||||
// matching per-platform optional dependency (@xai-official/grok-<platform>)
|
||||
// and installs it to ~/.grok/bin/ using versioned filenames:
|
||||
//
|
||||
// Unix: grok-<version> + grok (symlink)
|
||||
// Windows: grok-<version>.exe + grok.exe (copy)
|
||||
//
|
||||
// Versioned files ensure running processes are never disrupted on macOS
|
||||
// (replacing a binary that a running process has mmap'd causes SIGKILL
|
||||
// because the kernel can no longer verify the code signature).
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const zlib = require('zlib');
|
||||
const { execSync } = require('child_process');
|
||||
const TOML = require('@iarna/toml');
|
||||
|
||||
const CANONICAL_DIR = path.join(os.homedir(), '.grok', 'bin');
|
||||
|
||||
const key = `${process.platform}-${process.arch}`;
|
||||
const SUPPORTED = new Set([
|
||||
'darwin-arm64',
|
||||
'darwin-x64',
|
||||
'linux-x64',
|
||||
'linux-arm64',
|
||||
'win32-x64',
|
||||
'win32-arm64',
|
||||
]);
|
||||
if (!SUPPORTED.has(key)) {
|
||||
console.error(`@xai-official/grok: unsupported platform ${key}`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Resolve the per-platform sibling package's directory. The matching
|
||||
// optionalDependency is installed by npm based on `os`/`cpu` filters; the
|
||||
// other five are silently skipped. If the matching one is missing, npm was
|
||||
// likely invoked with --no-optional or the platform is unsupported.
|
||||
function resolvePlatformPackageDir() {
|
||||
const platformPkg = `@xai-official/grok-${key}`;
|
||||
try {
|
||||
return path.dirname(require.resolve(`${platformPkg}/package.json`));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
let version;
|
||||
try { version = require('../package.json').version; } catch {}
|
||||
if (!version) {
|
||||
console.error('@xai-official/grok: unable to determine version');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const IS_WINDOWS = process.platform === 'win32';
|
||||
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 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 {}
|
||||
}
|
||||
}
|
||||
|
||||
if (IS_WINDOWS) {
|
||||
// Symlinks need elevation on Windows; copy instead. If the exe is
|
||||
// locked by a running process, rename it aside then retry.
|
||||
const oldPath = canonicalPath + '.old';
|
||||
try { fs.unlinkSync(oldPath); } catch {} // stale backup from prior update
|
||||
try {
|
||||
try { fs.unlinkSync(canonicalPath); } catch {}
|
||||
fs.copyFileSync(versionedPath, canonicalPath);
|
||||
} catch (e) {
|
||||
try {
|
||||
fs.renameSync(canonicalPath, oldPath);
|
||||
try {
|
||||
fs.copyFileSync(versionedPath, canonicalPath);
|
||||
} catch (copyErr) {
|
||||
// Rollback: restore the old binary so the install isn't broken.
|
||||
try { fs.renameSync(oldPath, canonicalPath); } catch {}
|
||||
throw copyErr;
|
||||
}
|
||||
} catch (e2) {
|
||||
console.error(`@xai-official/grok: failed to update ${canonicalPath}: ${e2.message}`);
|
||||
console.error('Close all running grok processes and try again.');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Atomic symlink swap.
|
||||
const tmpLink = canonicalPath + `.link.${process.pid}`;
|
||||
try { fs.unlinkSync(tmpLink); } catch {}
|
||||
fs.symlinkSync(versionedName, tmpLink);
|
||||
fs.renameSync(tmpLink, canonicalPath);
|
||||
}
|
||||
|
||||
console.log(`${binName} ${version} installed to ${canonicalPath} -> ${versionedName}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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).
|
||||
// Uses an exact prefix match + hyphen + digit to avoid grok-* matching grok-pager-*.
|
||||
function cleanupOldVersions(binName) {
|
||||
try {
|
||||
const prefix = `${binName}-`;
|
||||
const currentVersioned = `${binName}-${version}${EXE}`;
|
||||
const entries = fs.readdirSync(CANONICAL_DIR);
|
||||
const versionedBinaries = entries
|
||||
.filter(e => {
|
||||
if (!e.startsWith(prefix)) return false;
|
||||
if (e.includes('.tmp.') || e.includes('.link.')) return false;
|
||||
if (e === currentVersioned) return false;
|
||||
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;
|
||||
});
|
||||
for (const old of versionedBinaries.slice(1)) {
|
||||
try { fs.unlinkSync(path.join(CANONICAL_DIR, old)); } catch {}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const platformDir = resolvePlatformPackageDir();
|
||||
if (!platformDir) {
|
||||
console.error(`@xai-official/grok: platform package @xai-official/grok-${key} not installed.`);
|
||||
console.error(' This usually means npm was invoked with --no-optional, or the install failed.');
|
||||
console.error(' Try: npm install -g @xai-official/grok');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
installBinary('grok', platformDir, `grok${EXE}`);
|
||||
cleanupOldVersions('grok');
|
||||
cleanupOldVersions('grok-pager');
|
||||
|
||||
// Write installer config
|
||||
const configDir = path.join(os.homedir(), '.grok');
|
||||
const configPath = path.join(configDir, 'config.toml');
|
||||
let obj = {};
|
||||
try { obj = TOML.parse(fs.readFileSync(configPath, 'utf8')); } catch { }
|
||||
obj.cli ??= {};
|
||||
obj.cli.installer = 'npm';
|
||||
|
||||
// Persist the npm registry so `grok update` and the trampoline use the same one.
|
||||
const npmRegistry = process.env.GROK_NPM_REGISTRY
|
||||
|| (() => {
|
||||
try {
|
||||
const resolved = execSync(
|
||||
'npm config get @xai-official:registry',
|
||||
{ encoding: 'utf8', timeout: 5000 }
|
||||
).trim();
|
||||
if (resolved && resolved !== 'undefined') return resolved;
|
||||
} catch {}
|
||||
return null;
|
||||
})();
|
||||
|
||||
if (npmRegistry) {
|
||||
obj.cli.npm_registry = npmRegistry;
|
||||
}
|
||||
|
||||
fs.writeFileSync(configPath, TOML.stringify(obj), 'utf8');
|
||||
|
||||
// Shell completions: print setup hints (no silent shell config mutation).
|
||||
// Set GROK_INSTALL_COMPLETIONS=1 to auto-generate to ~/.grok/completions.
|
||||
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 bashPath = path.join(completionsDir, 'bash', 'grok.bash');
|
||||
const zshPath = path.join(completionsDir, 'zsh', '_grok');
|
||||
fs.mkdirSync(path.dirname(bashPath), { recursive: true });
|
||||
fs.mkdirSync(path.dirname(zshPath), { recursive: true });
|
||||
const bashRes = spawnSync(GROK_PATH, ['completions', 'bash'], { encoding: 'utf8' });
|
||||
if (bashRes.status === 0) fs.writeFileSync(bashPath, bashRes.stdout);
|
||||
const zshRes = spawnSync(GROK_PATH, ['completions', 'zsh'], { encoding: 'utf8' });
|
||||
if (zshRes.status === 0) fs.writeFileSync(zshPath, zshRes.stdout);
|
||||
console.log('Completions generated to ~/.grok/completions (bash/zsh)');
|
||||
} catch {}
|
||||
} else if (!IS_WINDOWS) {
|
||||
console.log('Tip: grok completions bash > ~/.local/share/bash-completion/completions/grok');
|
||||
console.log(' grok completions zsh > ~/.zsh/completions/_grok');
|
||||
}
|
||||
41
crates/codegen/xai-grok-pager/npm/grok/package.json
Normal file
41
crates/codegen/xai-grok-pager/npm/grok/package.json
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"name": "@xai-official/grok",
|
||||
"version": "0.1.220-alpha.4",
|
||||
"description": "Bring Grok into your terminal",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"grok": "bin/grok"
|
||||
},
|
||||
"files": [
|
||||
"bin/"
|
||||
],
|
||||
"os": [
|
||||
"darwin",
|
||||
"linux",
|
||||
"win32"
|
||||
],
|
||||
"cpu": [
|
||||
"arm64",
|
||||
"x64"
|
||||
],
|
||||
"scripts": {
|
||||
"postinstall": "node bin/postinstall.js"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"dependencies": {
|
||||
"@iarna/toml": "^3.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@xai-official/grok-darwin-arm64": "0.1.220-alpha.4",
|
||||
"@xai-official/grok-darwin-x64": "0.1.220-alpha.4",
|
||||
"@xai-official/grok-linux-arm64": "0.1.220-alpha.4",
|
||||
"@xai-official/grok-linux-x64": "0.1.220-alpha.4",
|
||||
"@xai-official/grok-win32-arm64": "0.1.220-alpha.4",
|
||||
"@xai-official/grok-win32-x64": "0.1.220-alpha.4"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
#!/usr/bin/env node
|
||||
// Assemble the six per-platform npm packages prior to `npm publish`.
|
||||
//
|
||||
// For each supported (platform, arch) target this:
|
||||
// 1. Brotli-compresses the built binary into `../grok-<platform>/bin/<bin>.br`
|
||||
// 2. Stamps the sub-package's version to match the meta package
|
||||
//
|
||||
// Each per-platform package is its own npm publish target. The meta package
|
||||
// (`@xai-official/grok`) lists all six as `optionalDependencies` pinned to
|
||||
// the same version; npm installs only the one matching the host's
|
||||
// `os` + `cpu` filters.
|
||||
//
|
||||
// Why brotli? npm's tarball ceiling is ~200 MB and the raw grok binary is
|
||||
// 100–150 MB per platform. Brotli at max quality cuts that to 30–40 MB,
|
||||
// leaves plenty of headroom for binary growth, and is decoded by Node's
|
||||
// built-in zlib.brotliDecompressSync (no native deps required).
|
||||
//
|
||||
// Source paths come from environment variables (set in CI) and fall back to
|
||||
// the default cargo target dirs for local testing.
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { promisify } = require('util');
|
||||
const zlib = require('zlib');
|
||||
|
||||
const brotliCompress = promisify(zlib.brotliCompress);
|
||||
|
||||
const xaiRoot = process.env.XAI_ROOT || path.resolve(__dirname, '..', '..', '..', '..', '..');
|
||||
const npmRoot = path.resolve(__dirname, '..', '..');
|
||||
|
||||
const NOTICES_SOURCE = path.resolve(
|
||||
npmRoot, '..', '..', 'xai-grok-tools', 'THIRD_PARTY_NOTICES.md');
|
||||
const NOTICES_NAME = 'THIRD_PARTY_NOTICES.md';
|
||||
|
||||
const META_PKG_JSON = path.resolve(__dirname, '..', 'package.json');
|
||||
const meta = JSON.parse(fs.readFileSync(META_PKG_JSON, 'utf8'));
|
||||
const VERSION = meta.version;
|
||||
|
||||
function ensureDir(p) { fs.mkdirSync(path.dirname(p), { recursive: true }); }
|
||||
|
||||
async function packPlatform({ platform, arch, envVar, defaultSource, binName }) {
|
||||
const pkgDir = path.join(npmRoot, `grok-${platform}-${arch}`);
|
||||
const pkgJsonPath = path.join(pkgDir, 'package.json');
|
||||
|
||||
if (!fs.existsSync(pkgJsonPath)) {
|
||||
console.error(`[assemble] Missing per-platform package at ${pkgDir}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const source = process.env[envVar] || defaultSource;
|
||||
if (!fs.existsSync(source)) {
|
||||
console.error(`[assemble] Missing binary for ${platform}-${arch}: ${source}`);
|
||||
console.error(` Set ${envVar} or build to the default location.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Stamp the sub-package's version to match the meta package.
|
||||
const subPkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
|
||||
subPkg.version = VERSION;
|
||||
fs.writeFileSync(pkgJsonPath, JSON.stringify(subPkg, null, 4) + '\n');
|
||||
|
||||
if (!fs.existsSync(NOTICES_SOURCE)) {
|
||||
console.error(`[assemble] Missing third-party notices file: ${NOTICES_SOURCE}`);
|
||||
return false;
|
||||
}
|
||||
fs.copyFileSync(NOTICES_SOURCE, path.join(pkgDir, NOTICES_NAME));
|
||||
|
||||
// Brotli-compress into the sub-package's bin/.
|
||||
const outBr = path.join(pkgDir, 'bin', `${binName}.br`);
|
||||
ensureDir(outBr);
|
||||
const raw = fs.readFileSync(source);
|
||||
const compressed = await brotliCompress(raw, {
|
||||
params: { [zlib.constants.BROTLI_PARAM_QUALITY]: zlib.constants.BROTLI_MAX_QUALITY },
|
||||
});
|
||||
fs.writeFileSync(outBr, compressed);
|
||||
console.log(
|
||||
`[assemble] grok-${platform}-${arch}@${VERSION}: ` +
|
||||
`${(raw.length / 1048576).toFixed(1)} MB -> ${(compressed.length / 1048576).toFixed(1)} MB ` +
|
||||
`(${path.relative(npmRoot, outBr)})`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const targets = [
|
||||
{
|
||||
platform: 'darwin', arch: 'arm64', binName: 'grok',
|
||||
envVar: 'GROK_DARWIN_ARM64',
|
||||
defaultSource: path.join(xaiRoot, 'target', 'release', 'xai-grok-pager'),
|
||||
},
|
||||
{
|
||||
platform: 'darwin', arch: 'x64', binName: 'grok',
|
||||
envVar: 'GROK_DARWIN_X64',
|
||||
defaultSource: path.join(xaiRoot, 'target', 'x86_64-apple-darwin', 'release', 'xai-grok-pager'),
|
||||
},
|
||||
{
|
||||
platform: 'linux', arch: 'x64', binName: 'grok',
|
||||
envVar: 'GROK_LINUX_X64',
|
||||
defaultSource: path.join(xaiRoot, 'target',
|
||||
'explorer_cross_x86_64-unknown-linux-gnu',
|
||||
'x86_64-unknown-linux-gnu', 'release', 'xai-grok-pager'),
|
||||
},
|
||||
{
|
||||
platform: 'linux', arch: 'arm64', binName: 'grok',
|
||||
envVar: 'GROK_LINUX_ARM64',
|
||||
defaultSource: path.join(xaiRoot, 'target',
|
||||
'explorer_cross_aarch64-unknown-linux-gnu',
|
||||
'aarch64-unknown-linux-gnu', 'release', 'xai-grok-pager'),
|
||||
},
|
||||
{
|
||||
platform: 'win32', arch: 'x64', binName: 'grok.exe',
|
||||
envVar: 'GROK_WIN32_X64',
|
||||
defaultSource: path.join(xaiRoot, 'target', 'x86_64-pc-windows-msvc', 'release', 'xai-grok-pager.exe'),
|
||||
},
|
||||
{
|
||||
platform: 'win32', arch: 'arm64', binName: 'grok.exe',
|
||||
envVar: 'GROK_WIN32_ARM64',
|
||||
defaultSource: path.join(xaiRoot, 'target', 'aarch64-pc-windows-msvc', 'release', 'xai-grok-pager.exe'),
|
||||
},
|
||||
];
|
||||
|
||||
// Compress in parallel — brotliCompress runs on the libuv thread pool so
|
||||
// calls genuinely overlap (set UV_THREADPOOL_SIZE>=6 in CI for full
|
||||
// parallelism; Node's default pool size is 4).
|
||||
const results = await Promise.all(targets.map(packPlatform));
|
||||
const failed = results.filter(r => !r).length;
|
||||
if (failed > 0) {
|
||||
console.error(`[assemble] ${failed} target(s) failed.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`[assemble] All 6 per-platform packages assembled at version ${VERSION}.`);
|
||||
}
|
||||
|
||||
main().catch((err) => { console.error(err); process.exit(1); });
|
||||
|
|
@ -0,0 +1,983 @@
|
|||
#!/usr/bin/env node
|
||||
// Tests for the versioned-binary + symlink installation logic used by
|
||||
// postinstall.js and the bin/grok trampoline.
|
||||
//
|
||||
// Run with: node scripts/test-postinstall.js
|
||||
//
|
||||
// Uses only Node.js built-in modules (no test framework needed).
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const assert = require('assert');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
fn();
|
||||
console.log(` ✓ ${name}`);
|
||||
passed++;
|
||||
} catch (e) {
|
||||
console.error(` ✗ ${name}`);
|
||||
console.error(` ${e.message}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
function makeTmpDir() {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), 'grok-test-'));
|
||||
}
|
||||
|
||||
function cleanup(dir) {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
// ─── 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;
|
||||
}
|
||||
|
||||
/** Install a versioned binary + atomic symlink (same as postinstall.js). */
|
||||
function installVersionedBinary(vendoredBinPath, version, canonicalDir) {
|
||||
const canonicalPath = path.join(canonicalDir, 'grok');
|
||||
fs.mkdirSync(canonicalDir, { recursive: true });
|
||||
|
||||
const versionedName = `grok-${version}`;
|
||||
const versionedPath = path.join(canonicalDir, versionedName);
|
||||
|
||||
if (!fs.existsSync(versionedPath)) {
|
||||
const tmpPath = versionedPath + `.tmp.${process.pid}`;
|
||||
try {
|
||||
fs.copyFileSync(vendoredBinPath, tmpPath);
|
||||
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 };
|
||||
}
|
||||
|
||||
/** Cleanup old versioned binaries (same as postinstall.js). */
|
||||
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);
|
||||
// Keep the most recent old version, remove anything older.
|
||||
for (const old of versionedBinaries.slice(1)) {
|
||||
try { fs.unlinkSync(path.join(canonicalDir, old)); } catch {}
|
||||
}
|
||||
return versionedBinaries;
|
||||
}
|
||||
|
||||
/** Bootstrap canonical from vendored (same as bin/grok trampoline). */
|
||||
function bootstrapCanonical(vendoredBinPath, version, canonicalDir) {
|
||||
const canonicalPath = path.join(canonicalDir, 'grok');
|
||||
try {
|
||||
fs.mkdirSync(canonicalDir, { recursive: true });
|
||||
const versionedName = `grok-${version}`;
|
||||
const versionedPath = path.join(canonicalDir, versionedName);
|
||||
if (!fs.existsSync(versionedPath)) {
|
||||
const tmpPath = versionedPath + `.tmp.${process.pid}`;
|
||||
fs.copyFileSync(vendoredBinPath, tmpPath);
|
||||
fs.chmodSync(tmpPath, 0o755);
|
||||
fs.renameSync(tmpPath, versionedPath);
|
||||
}
|
||||
const tmpLink = canonicalPath + `.link.${process.pid}`;
|
||||
try { fs.unlinkSync(tmpLink); } catch {}
|
||||
fs.symlinkSync(versionedName, tmpLink);
|
||||
fs.renameSync(tmpLink, canonicalPath);
|
||||
return canonicalPath;
|
||||
} catch {
|
||||
return vendoredBinPath;
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// Install + Symlink Tests
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
console.log('install + symlink tests\n');
|
||||
|
||||
test('creates versioned binary and symlink on fresh install', () => {
|
||||
const dir = makeTmpDir();
|
||||
try {
|
||||
const vendored = path.join(dir, 'vendored-grok');
|
||||
fs.writeFileSync(vendored, 'binary-content-v1');
|
||||
|
||||
const binDir = path.join(dir, 'bin');
|
||||
const result = installVersionedBinary(vendored, '0.1.140', binDir);
|
||||
|
||||
// Versioned file should exist
|
||||
assert.ok(fs.existsSync(result.versionedPath), 'versioned binary should exist');
|
||||
assert.strictEqual(fs.readFileSync(result.versionedPath, 'utf8'), 'binary-content-v1');
|
||||
|
||||
// Canonical path should be a symlink
|
||||
const stat = fs.lstatSync(result.canonicalPath);
|
||||
assert.ok(stat.isSymbolicLink(), 'canonical path should be a symlink');
|
||||
|
||||
// Symlink should point to the versioned name (relative)
|
||||
const target = fs.readlinkSync(result.canonicalPath);
|
||||
assert.strictEqual(target, 'grok-0.1.140');
|
||||
|
||||
// Reading through the symlink should return the binary content
|
||||
assert.strictEqual(fs.readFileSync(result.canonicalPath, 'utf8'), 'binary-content-v1');
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('upgrade swaps symlink and preserves old binary', () => {
|
||||
const dir = makeTmpDir();
|
||||
try {
|
||||
const binDir = path.join(dir, 'bin');
|
||||
|
||||
// Install v1
|
||||
const vendored_v1 = path.join(dir, 'vendored-v1');
|
||||
fs.writeFileSync(vendored_v1, 'v1-content');
|
||||
installVersionedBinary(vendored_v1, '0.1.140', binDir);
|
||||
|
||||
// Install v2
|
||||
const vendored_v2 = path.join(dir, 'vendored-v2');
|
||||
fs.writeFileSync(vendored_v2, 'v2-content');
|
||||
const result = installVersionedBinary(vendored_v2, '0.1.141', binDir);
|
||||
|
||||
// Symlink now points to v2
|
||||
assert.strictEqual(fs.readlinkSync(result.canonicalPath), 'grok-0.1.141');
|
||||
assert.strictEqual(fs.readFileSync(result.canonicalPath, 'utf8'), 'v2-content');
|
||||
|
||||
// Old v1 binary MUST still exist on disk (this is the key safety property)
|
||||
const oldBinary = path.join(binDir, 'grok-0.1.140');
|
||||
assert.ok(fs.existsSync(oldBinary), 'old versioned binary must not be deleted');
|
||||
assert.strictEqual(fs.readFileSync(oldBinary, 'utf8'), 'v1-content');
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('idempotent: reinstalling same version does not re-copy', () => {
|
||||
const dir = makeTmpDir();
|
||||
try {
|
||||
const binDir = path.join(dir, 'bin');
|
||||
|
||||
const vendored = path.join(dir, 'vendored');
|
||||
fs.writeFileSync(vendored, 'original');
|
||||
installVersionedBinary(vendored, '0.1.140', binDir);
|
||||
|
||||
// Modify vendored source (simulate npm replacing it)
|
||||
fs.writeFileSync(vendored, 'replaced-by-npm');
|
||||
|
||||
// Re-run postinstall with same version
|
||||
installVersionedBinary(vendored, '0.1.140', binDir);
|
||||
|
||||
// Versioned binary should NOT have been replaced (existsSync guard)
|
||||
const versionedPath = path.join(binDir, 'grok-0.1.140');
|
||||
assert.strictEqual(fs.readFileSync(versionedPath, 'utf8'), 'original');
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('symlink swap is atomic (no intermediate missing state)', () => {
|
||||
const dir = makeTmpDir();
|
||||
try {
|
||||
const binDir = path.join(dir, 'bin');
|
||||
const canonicalPath = path.join(binDir, 'grok');
|
||||
|
||||
const vendored = path.join(dir, 'vendored');
|
||||
fs.writeFileSync(vendored, 'v1');
|
||||
installVersionedBinary(vendored, '0.1.140', binDir);
|
||||
assert.ok(fs.existsSync(canonicalPath), 'should exist after first install');
|
||||
|
||||
// Upgrade
|
||||
fs.writeFileSync(vendored, 'v2');
|
||||
installVersionedBinary(vendored, '0.1.141', binDir);
|
||||
assert.ok(fs.existsSync(canonicalPath), 'should exist after upgrade');
|
||||
|
||||
// No temp files left behind
|
||||
const entries = fs.readdirSync(binDir);
|
||||
const tempFiles = entries.filter(e => e.includes('.tmp.') || e.includes('.link.'));
|
||||
assert.strictEqual(tempFiles.length, 0, `temp files should be cleaned up, found: ${tempFiles}`);
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('handles upgrade from old-style regular file to versioned symlink', () => {
|
||||
const dir = makeTmpDir();
|
||||
try {
|
||||
const binDir = path.join(dir, 'bin');
|
||||
const canonicalPath = path.join(binDir, 'grok');
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
|
||||
// Simulate old installation: grok is a regular file
|
||||
fs.writeFileSync(canonicalPath, 'old-style-binary');
|
||||
assert.ok(!fs.lstatSync(canonicalPath).isSymbolicLink(), 'should be regular file initially');
|
||||
|
||||
// Run new-style install
|
||||
const vendored = path.join(dir, 'vendored');
|
||||
fs.writeFileSync(vendored, 'v2-content');
|
||||
installVersionedBinary(vendored, '0.1.141', binDir);
|
||||
|
||||
// Should now be a symlink
|
||||
assert.ok(fs.lstatSync(canonicalPath).isSymbolicLink(), 'should be symlink after install');
|
||||
assert.strictEqual(fs.readFileSync(canonicalPath, 'utf8'), 'v2-content');
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('handles broken symlink (target deleted externally)', () => {
|
||||
const dir = makeTmpDir();
|
||||
try {
|
||||
const binDir = path.join(dir, 'bin');
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
|
||||
// Create a broken symlink (points to a file that doesn't exist)
|
||||
const canonicalPath = path.join(binDir, 'grok');
|
||||
fs.symlinkSync('grok-0.1.99', canonicalPath);
|
||||
assert.ok(!fs.existsSync(canonicalPath), 'broken symlink should not "exist"');
|
||||
|
||||
// Install should work and fix the broken symlink
|
||||
const vendored = path.join(dir, 'vendored');
|
||||
fs.writeFileSync(vendored, 'fixed-content');
|
||||
const result = installVersionedBinary(vendored, '0.1.141', binDir);
|
||||
|
||||
assert.ok(fs.existsSync(result.canonicalPath), 'symlink should now resolve');
|
||||
assert.strictEqual(fs.readFileSync(result.canonicalPath, 'utf8'), 'fixed-content');
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('three sequential upgrades: v1 -> v2 -> v3 all coexist', () => {
|
||||
const dir = makeTmpDir();
|
||||
try {
|
||||
const binDir = path.join(dir, 'bin');
|
||||
const vendored = path.join(dir, 'vendored');
|
||||
|
||||
fs.writeFileSync(vendored, 'content-v1');
|
||||
installVersionedBinary(vendored, '0.1.1', binDir);
|
||||
|
||||
fs.writeFileSync(vendored, 'content-v2');
|
||||
installVersionedBinary(vendored, '0.1.2', binDir);
|
||||
|
||||
fs.writeFileSync(vendored, 'content-v3');
|
||||
installVersionedBinary(vendored, '0.1.3', binDir);
|
||||
|
||||
// Symlink points to latest
|
||||
assert.strictEqual(fs.readlinkSync(path.join(binDir, 'grok')), 'grok-0.1.3');
|
||||
|
||||
// All three versioned binaries still exist (no cleanup yet)
|
||||
assert.ok(fs.existsSync(path.join(binDir, 'grok-0.1.1')));
|
||||
assert.ok(fs.existsSync(path.join(binDir, 'grok-0.1.2')));
|
||||
assert.ok(fs.existsSync(path.join(binDir, 'grok-0.1.3')));
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('file permissions are preserved (0o755)', () => {
|
||||
const dir = makeTmpDir();
|
||||
try {
|
||||
const binDir = path.join(dir, 'bin');
|
||||
const vendored = path.join(dir, 'vendored');
|
||||
fs.writeFileSync(vendored, 'binary');
|
||||
|
||||
const result = installVersionedBinary(vendored, '0.1.140', binDir);
|
||||
|
||||
const mode = fs.statSync(result.versionedPath).mode & 0o777;
|
||||
assert.strictEqual(mode, 0o755, `expected 0755, got ${mode.toString(8)}`);
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// Cleanup / Semver Sort Tests
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
console.log('\ncleanup + semver sort tests\n');
|
||||
|
||||
test('cleanup keeps N-1 version and removes older ones', () => {
|
||||
const dir = makeTmpDir();
|
||||
try {
|
||||
const binDir = path.join(dir, 'bin');
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
|
||||
// Create three old versioned binaries
|
||||
fs.writeFileSync(path.join(binDir, 'grok-0.1.138'), 'v138');
|
||||
fs.writeFileSync(path.join(binDir, 'grok-0.1.139'), 'v139');
|
||||
fs.writeFileSync(path.join(binDir, 'grok-0.1.140'), 'v140');
|
||||
// grok-0.1.141 is the current version (excluded from cleanup)
|
||||
fs.writeFileSync(path.join(binDir, 'grok-0.1.141'), 'v141');
|
||||
|
||||
cleanupOldVersions(binDir, 'grok-0.1.141');
|
||||
|
||||
assert.ok(fs.existsSync(path.join(binDir, 'grok-0.1.141')), 'current should exist');
|
||||
assert.ok(fs.existsSync(path.join(binDir, 'grok-0.1.140')), 'N-1 should be kept');
|
||||
assert.ok(!fs.existsSync(path.join(binDir, 'grok-0.1.139')), 'N-2 should be removed');
|
||||
assert.ok(!fs.existsSync(path.join(binDir, 'grok-0.1.138')), 'N-3 should be removed');
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('cleanup with only one old version keeps it', () => {
|
||||
const dir = makeTmpDir();
|
||||
try {
|
||||
const binDir = path.join(dir, 'bin');
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
|
||||
fs.writeFileSync(path.join(binDir, 'grok-0.1.140'), 'v140');
|
||||
fs.writeFileSync(path.join(binDir, 'grok-0.1.141'), 'v141');
|
||||
|
||||
cleanupOldVersions(binDir, 'grok-0.1.141');
|
||||
|
||||
assert.ok(fs.existsSync(path.join(binDir, 'grok-0.1.140')), 'single old version should be kept');
|
||||
assert.ok(fs.existsSync(path.join(binDir, 'grok-0.1.141')), 'current should exist');
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('cleanup with no old versions is a no-op', () => {
|
||||
const dir = makeTmpDir();
|
||||
try {
|
||||
const binDir = path.join(dir, 'bin');
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
|
||||
// Only the current version exists
|
||||
fs.writeFileSync(path.join(binDir, 'grok-0.1.141'), 'v141');
|
||||
|
||||
cleanupOldVersions(binDir, 'grok-0.1.141');
|
||||
|
||||
assert.ok(fs.existsSync(path.join(binDir, 'grok-0.1.141')), 'current should still exist');
|
||||
const entries = fs.readdirSync(binDir).filter(e => e.startsWith('grok-'));
|
||||
assert.strictEqual(entries.length, 1, 'should only have current version');
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('cleanup ignores .tmp. and .link. files', () => {
|
||||
const dir = makeTmpDir();
|
||||
try {
|
||||
const binDir = path.join(dir, 'bin');
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
|
||||
fs.writeFileSync(path.join(binDir, 'grok-0.1.141'), 'current');
|
||||
// Leftover temp files from a crashed install
|
||||
fs.writeFileSync(path.join(binDir, 'grok-0.1.140.tmp.12345'), 'crashed-tmp');
|
||||
fs.writeFileSync(path.join(binDir, 'grok.link.12345'), 'crashed-link');
|
||||
|
||||
cleanupOldVersions(binDir, 'grok-0.1.141');
|
||||
|
||||
// Temp files should not be touched by cleanup (they're filtered out)
|
||||
assert.ok(fs.existsSync(path.join(binDir, 'grok-0.1.140.tmp.12345')), 'tmp file should not be touched');
|
||||
assert.ok(fs.existsSync(path.join(binDir, 'grok.link.12345')), 'link file should not be touched');
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('semver sort: 0.1.9 vs 0.1.10 (digit boundary)', () => {
|
||||
// Regression test: lexical sort puts '0.1.9' after '0.1.10' because '9' > '1'
|
||||
const dir = makeTmpDir();
|
||||
try {
|
||||
const binDir = path.join(dir, 'bin');
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
|
||||
fs.writeFileSync(path.join(binDir, 'grok-0.1.8'), 'v8');
|
||||
fs.writeFileSync(path.join(binDir, 'grok-0.1.9'), 'v9');
|
||||
fs.writeFileSync(path.join(binDir, 'grok-0.1.10'), 'v10');
|
||||
fs.writeFileSync(path.join(binDir, 'grok-0.1.11'), 'v11');
|
||||
|
||||
cleanupOldVersions(binDir, 'grok-0.1.11');
|
||||
|
||||
assert.ok(fs.existsSync(path.join(binDir, 'grok-0.1.11')), 'current should exist');
|
||||
assert.ok(fs.existsSync(path.join(binDir, 'grok-0.1.10')), '0.1.10 should be kept (N-1)');
|
||||
assert.ok(!fs.existsSync(path.join(binDir, 'grok-0.1.9')), '0.1.9 should be removed');
|
||||
assert.ok(!fs.existsSync(path.join(binDir, 'grok-0.1.8')), '0.1.8 should be removed');
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('semver sort: major version boundary (0.x vs 1.x)', () => {
|
||||
const dir = makeTmpDir();
|
||||
try {
|
||||
const binDir = path.join(dir, 'bin');
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
|
||||
fs.writeFileSync(path.join(binDir, 'grok-0.9.99'), 'old');
|
||||
fs.writeFileSync(path.join(binDir, 'grok-1.0.0'), 'v1');
|
||||
fs.writeFileSync(path.join(binDir, 'grok-1.0.1'), 'current');
|
||||
|
||||
cleanupOldVersions(binDir, 'grok-1.0.1');
|
||||
|
||||
assert.ok(fs.existsSync(path.join(binDir, 'grok-1.0.0')), '1.0.0 should be kept (N-1)');
|
||||
assert.ok(!fs.existsSync(path.join(binDir, 'grok-0.9.99')), '0.9.99 should be removed');
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('semver sort: minor version boundary (0.1.x vs 0.2.x)', () => {
|
||||
const dir = makeTmpDir();
|
||||
try {
|
||||
const binDir = path.join(dir, 'bin');
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
|
||||
fs.writeFileSync(path.join(binDir, 'grok-0.1.999'), 'old');
|
||||
fs.writeFileSync(path.join(binDir, 'grok-0.2.0'), 'v2');
|
||||
fs.writeFileSync(path.join(binDir, 'grok-0.2.1'), 'current');
|
||||
|
||||
cleanupOldVersions(binDir, 'grok-0.2.1');
|
||||
|
||||
assert.ok(fs.existsSync(path.join(binDir, 'grok-0.2.0')), '0.2.0 should be kept (N-1)');
|
||||
assert.ok(!fs.existsSync(path.join(binDir, 'grok-0.1.999')), '0.1.999 should be removed');
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('semverSortDescending: 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);
|
||||
assert.deepStrictEqual(sorted, [
|
||||
'grok-1.0.0',
|
||||
'grok-0.2.0',
|
||||
'grok-0.1.10',
|
||||
'grok-0.1.9',
|
||||
'grok-0.1.2',
|
||||
]);
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// Bootstrap (trampoline) Tests
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
console.log('\nbootstrap (trampoline) tests\n');
|
||||
|
||||
test('bootstrapCanonical creates versioned binary from vendored', () => {
|
||||
const dir = makeTmpDir();
|
||||
try {
|
||||
const binDir = path.join(dir, 'bin');
|
||||
const vendored = path.join(dir, 'vendored-grok');
|
||||
fs.writeFileSync(vendored, 'vendored-content');
|
||||
|
||||
const result = bootstrapCanonical(vendored, '0.1.140', binDir);
|
||||
|
||||
assert.strictEqual(result, path.join(binDir, 'grok'));
|
||||
assert.ok(fs.existsSync(path.join(binDir, 'grok-0.1.140')), 'versioned binary should exist');
|
||||
assert.ok(fs.lstatSync(result).isSymbolicLink(), 'canonical should be symlink');
|
||||
assert.strictEqual(fs.readFileSync(result, 'utf8'), 'vendored-content');
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('bootstrapCanonical is idempotent', () => {
|
||||
const dir = makeTmpDir();
|
||||
try {
|
||||
const binDir = path.join(dir, 'bin');
|
||||
const vendored = path.join(dir, 'vendored-grok');
|
||||
fs.writeFileSync(vendored, 'original-content');
|
||||
|
||||
bootstrapCanonical(vendored, '0.1.140', binDir);
|
||||
|
||||
// Change vendored content (simulating npm update)
|
||||
fs.writeFileSync(vendored, 'npm-replaced-content');
|
||||
|
||||
// Second bootstrap should not overwrite existing versioned binary
|
||||
const result = bootstrapCanonical(vendored, '0.1.140', binDir);
|
||||
|
||||
assert.strictEqual(
|
||||
fs.readFileSync(path.join(binDir, 'grok-0.1.140'), 'utf8'),
|
||||
'original-content',
|
||||
'should keep original, not npm-replaced version'
|
||||
);
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('bootstrapCanonical returns vendored path on failure', () => {
|
||||
// If the canonical dir can't be created (e.g. permission denied),
|
||||
// bootstrap should gracefully fall back to the vendored binary.
|
||||
const dir = makeTmpDir();
|
||||
try {
|
||||
const vendored = path.join(dir, 'vendored');
|
||||
fs.writeFileSync(vendored, 'fallback');
|
||||
|
||||
// Create a regular file where the dir should be — mkdirSync will fail.
|
||||
const blockerFile = path.join(dir, 'blocked');
|
||||
fs.writeFileSync(blockerFile, 'I am a file, not a directory');
|
||||
const impossibleDir = path.join(blockerFile, 'subdir');
|
||||
|
||||
const result = bootstrapCanonical(vendored, '0.1.140', impossibleDir);
|
||||
|
||||
assert.strictEqual(result, vendored, 'should fall back to vendored path');
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('bootstrapCanonical works when canonical already exists (different version)', () => {
|
||||
const dir = makeTmpDir();
|
||||
try {
|
||||
const binDir = path.join(dir, 'bin');
|
||||
|
||||
// Install v1 via postinstall
|
||||
const vendored1 = path.join(dir, 'vendored-v1');
|
||||
fs.writeFileSync(vendored1, 'v1');
|
||||
installVersionedBinary(vendored1, '0.1.140', binDir);
|
||||
|
||||
// Bootstrap with v2 (simulates trampoline running a newer vendored binary)
|
||||
const vendored2 = path.join(dir, 'vendored-v2');
|
||||
fs.writeFileSync(vendored2, 'v2');
|
||||
const result = bootstrapCanonical(vendored2, '0.1.141', binDir);
|
||||
|
||||
assert.strictEqual(result, path.join(binDir, 'grok'));
|
||||
// Symlink should now point to v2
|
||||
assert.strictEqual(fs.readlinkSync(result), 'grok-0.1.141');
|
||||
// v1 should still exist
|
||||
assert.ok(fs.existsSync(path.join(binDir, 'grok-0.1.140')), 'old version should still exist');
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// End-to-end Scenario Tests
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
console.log('\nend-to-end scenario tests\n');
|
||||
|
||||
test('full lifecycle: install, upgrade, cleanup', () => {
|
||||
const dir = makeTmpDir();
|
||||
try {
|
||||
const binDir = path.join(dir, 'bin');
|
||||
const vendored = path.join(dir, 'vendored');
|
||||
|
||||
// v1: fresh install
|
||||
fs.writeFileSync(vendored, 'v1');
|
||||
installVersionedBinary(vendored, '0.1.140', binDir);
|
||||
|
||||
// v2: upgrade
|
||||
fs.writeFileSync(vendored, 'v2');
|
||||
installVersionedBinary(vendored, '0.1.141', binDir);
|
||||
|
||||
// v3: another upgrade
|
||||
fs.writeFileSync(vendored, 'v3');
|
||||
installVersionedBinary(vendored, '0.1.142', binDir);
|
||||
cleanupOldVersions(binDir, 'grok-0.1.142');
|
||||
|
||||
// Current (v3) + N-1 (v2) should exist; v1 removed
|
||||
assert.ok(fs.existsSync(path.join(binDir, 'grok-0.1.142')), 'v3 should exist');
|
||||
assert.ok(fs.existsSync(path.join(binDir, 'grok-0.1.141')), 'v2 should be kept (N-1)');
|
||||
assert.ok(!fs.existsSync(path.join(binDir, 'grok-0.1.140')), 'v1 should be removed');
|
||||
|
||||
// Canonical symlink points to v3
|
||||
assert.strictEqual(fs.readlinkSync(path.join(binDir, 'grok')), 'grok-0.1.142');
|
||||
assert.strictEqual(fs.readFileSync(path.join(binDir, 'grok'), 'utf8'), 'v3');
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('downgrade: installing older version than current', () => {
|
||||
const dir = makeTmpDir();
|
||||
try {
|
||||
const binDir = path.join(dir, 'bin');
|
||||
const vendored = path.join(dir, 'vendored');
|
||||
|
||||
// Install v2 first
|
||||
fs.writeFileSync(vendored, 'v2');
|
||||
installVersionedBinary(vendored, '0.1.141', binDir);
|
||||
|
||||
// Downgrade to v1
|
||||
fs.writeFileSync(vendored, 'v1');
|
||||
installVersionedBinary(vendored, '0.1.140', binDir);
|
||||
|
||||
// Symlink should now point to v1
|
||||
assert.strictEqual(fs.readlinkSync(path.join(binDir, 'grok')), 'grok-0.1.140');
|
||||
assert.strictEqual(fs.readFileSync(path.join(binDir, 'grok'), 'utf8'), 'v1');
|
||||
|
||||
// v2 should still exist (never delete old binaries during install)
|
||||
assert.ok(fs.existsSync(path.join(binDir, 'grok-0.1.141')), 'v2 should still exist');
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('non-grok files in bin dir are not touched by cleanup', () => {
|
||||
const dir = makeTmpDir();
|
||||
try {
|
||||
const binDir = path.join(dir, 'bin');
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
|
||||
// Non-grok files
|
||||
fs.writeFileSync(path.join(binDir, 'other-tool'), 'should-stay');
|
||||
fs.writeFileSync(path.join(binDir, 'README.md'), 'should-stay');
|
||||
|
||||
// Grok versions
|
||||
fs.writeFileSync(path.join(binDir, 'grok-0.1.138'), 'old1');
|
||||
fs.writeFileSync(path.join(binDir, 'grok-0.1.139'), 'old2');
|
||||
fs.writeFileSync(path.join(binDir, 'grok-0.1.140'), 'current');
|
||||
|
||||
cleanupOldVersions(binDir, 'grok-0.1.140');
|
||||
|
||||
assert.ok(fs.existsSync(path.join(binDir, 'other-tool')), 'non-grok file should not be touched');
|
||||
assert.ok(fs.existsSync(path.join(binDir, 'README.md')), 'non-grok file should not be touched');
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// grok vs grok-pager Isolation Tests
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
console.log('\ngrok vs grok-pager isolation tests\n');
|
||||
|
||||
/**
|
||||
* Cleanup for a named binary (mirrors postinstall.js cleanupOldVersions).
|
||||
* Uses prefix + leading digit to avoid grok-* matching grok-pager-*.
|
||||
*/
|
||||
function cleanupOldVersionsNamed(canonicalDir, binName, version) {
|
||||
const prefix = `${binName}-`;
|
||||
const currentVersioned = `${binName}-${version}`;
|
||||
const entries = fs.readdirSync(canonicalDir);
|
||||
const versionedBinaries = entries
|
||||
.filter(e => {
|
||||
if (!e.startsWith(prefix)) return false;
|
||||
if (e.includes('.tmp.') || e.includes('.link.')) return false;
|
||||
if (e === currentVersioned) return false;
|
||||
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;
|
||||
});
|
||||
for (const old of versionedBinaries.slice(1)) {
|
||||
try { fs.unlinkSync(path.join(canonicalDir, old)); } catch {}
|
||||
}
|
||||
return versionedBinaries;
|
||||
}
|
||||
|
||||
/** Install a named binary (mirrors postinstall.js installBinary). */
|
||||
function installNamedBinary(vendoredBinPath, binName, version, canonicalDir) {
|
||||
fs.mkdirSync(canonicalDir, { recursive: true });
|
||||
const versionedName = `${binName}-${version}`;
|
||||
const versionedPath = path.join(canonicalDir, versionedName);
|
||||
const canonicalPath = path.join(canonicalDir, binName);
|
||||
|
||||
if (!fs.existsSync(versionedPath)) {
|
||||
const tmpPath = versionedPath + `.tmp.${process.pid}`;
|
||||
try {
|
||||
fs.copyFileSync(vendoredBinPath, tmpPath);
|
||||
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 };
|
||||
}
|
||||
|
||||
test('installing both grok and grok-pager creates independent symlinks', () => {
|
||||
const dir = makeTmpDir();
|
||||
try {
|
||||
const binDir = path.join(dir, 'bin');
|
||||
const vendored = path.join(dir, 'vendored');
|
||||
fs.writeFileSync(vendored, 'grok-binary');
|
||||
const vendoredPager = path.join(dir, 'vendored-pager');
|
||||
fs.writeFileSync(vendoredPager, 'pager-binary');
|
||||
|
||||
installNamedBinary(vendored, 'grok', '0.1.141', binDir);
|
||||
installNamedBinary(vendoredPager, 'grok-pager', '0.1.141', binDir);
|
||||
|
||||
// Both symlinks exist and point to correct targets
|
||||
assert.strictEqual(fs.readlinkSync(path.join(binDir, 'grok')), 'grok-0.1.141');
|
||||
assert.strictEqual(fs.readlinkSync(path.join(binDir, 'grok-pager')), 'grok-pager-0.1.141');
|
||||
|
||||
// Both versioned files exist with correct content
|
||||
assert.strictEqual(fs.readFileSync(path.join(binDir, 'grok-0.1.141'), 'utf8'), 'grok-binary');
|
||||
assert.strictEqual(fs.readFileSync(path.join(binDir, 'grok-pager-0.1.141'), 'utf8'), 'pager-binary');
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('cleanup of grok-* does not remove grok-pager-*', () => {
|
||||
const dir = makeTmpDir();
|
||||
try {
|
||||
const binDir = path.join(dir, 'bin');
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
|
||||
// Old grok versions
|
||||
fs.writeFileSync(path.join(binDir, 'grok-0.1.138'), 'old-grok-1');
|
||||
fs.writeFileSync(path.join(binDir, 'grok-0.1.139'), 'old-grok-2');
|
||||
fs.writeFileSync(path.join(binDir, 'grok-0.1.140'), 'old-grok-3');
|
||||
// Current grok
|
||||
fs.writeFileSync(path.join(binDir, 'grok-0.1.141'), 'current-grok');
|
||||
|
||||
// grok-pager versions (should not be touched)
|
||||
fs.writeFileSync(path.join(binDir, 'grok-pager-0.1.138'), 'old-pager-1');
|
||||
fs.writeFileSync(path.join(binDir, 'grok-pager-0.1.139'), 'old-pager-2');
|
||||
fs.writeFileSync(path.join(binDir, 'grok-pager-0.1.141'), 'current-pager');
|
||||
|
||||
cleanupOldVersionsNamed(binDir, 'grok', '0.1.141');
|
||||
|
||||
// grok cleanup: current + N-1 kept, older removed
|
||||
assert.ok(fs.existsSync(path.join(binDir, 'grok-0.1.141')), 'current grok should exist');
|
||||
assert.ok(fs.existsSync(path.join(binDir, 'grok-0.1.140')), 'N-1 grok should be kept');
|
||||
assert.ok(!fs.existsSync(path.join(binDir, 'grok-0.1.139')), 'N-2 grok should be removed');
|
||||
assert.ok(!fs.existsSync(path.join(binDir, 'grok-0.1.138')), 'N-3 grok should be removed');
|
||||
|
||||
// ALL grok-pager versions must be untouched
|
||||
assert.ok(fs.existsSync(path.join(binDir, 'grok-pager-0.1.138')), 'grok-pager-0.1.138 must survive grok cleanup');
|
||||
assert.ok(fs.existsSync(path.join(binDir, 'grok-pager-0.1.139')), 'grok-pager-0.1.139 must survive grok cleanup');
|
||||
assert.ok(fs.existsSync(path.join(binDir, 'grok-pager-0.1.141')), 'grok-pager-0.1.141 must survive grok cleanup');
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('cleanup of grok-pager-* does not remove grok-*', () => {
|
||||
const dir = makeTmpDir();
|
||||
try {
|
||||
const binDir = path.join(dir, 'bin');
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
|
||||
// grok versions (should not be touched)
|
||||
fs.writeFileSync(path.join(binDir, 'grok-0.1.138'), 'old-grok-1');
|
||||
fs.writeFileSync(path.join(binDir, 'grok-0.1.139'), 'old-grok-2');
|
||||
fs.writeFileSync(path.join(binDir, 'grok-0.1.141'), 'current-grok');
|
||||
|
||||
// Old grok-pager versions
|
||||
fs.writeFileSync(path.join(binDir, 'grok-pager-0.1.138'), 'old-pager-1');
|
||||
fs.writeFileSync(path.join(binDir, 'grok-pager-0.1.139'), 'old-pager-2');
|
||||
fs.writeFileSync(path.join(binDir, 'grok-pager-0.1.140'), 'old-pager-3');
|
||||
// Current pager
|
||||
fs.writeFileSync(path.join(binDir, 'grok-pager-0.1.141'), 'current-pager');
|
||||
|
||||
cleanupOldVersionsNamed(binDir, 'grok-pager', '0.1.141');
|
||||
|
||||
// grok-pager cleanup: current + N-1 kept, older removed
|
||||
assert.ok(fs.existsSync(path.join(binDir, 'grok-pager-0.1.141')), 'current pager should exist');
|
||||
assert.ok(fs.existsSync(path.join(binDir, 'grok-pager-0.1.140')), 'N-1 pager should be kept');
|
||||
assert.ok(!fs.existsSync(path.join(binDir, 'grok-pager-0.1.139')), 'N-2 pager should be removed');
|
||||
assert.ok(!fs.existsSync(path.join(binDir, 'grok-pager-0.1.138')), 'N-3 pager should be removed');
|
||||
|
||||
// ALL grok versions must be untouched
|
||||
assert.ok(fs.existsSync(path.join(binDir, 'grok-0.1.138')), 'grok-0.1.138 must survive pager cleanup');
|
||||
assert.ok(fs.existsSync(path.join(binDir, 'grok-0.1.139')), 'grok-0.1.139 must survive pager cleanup');
|
||||
assert.ok(fs.existsSync(path.join(binDir, 'grok-0.1.141')), 'grok-0.1.141 must survive pager cleanup');
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('full dual-binary lifecycle: install, upgrade, cleanup both', () => {
|
||||
const dir = makeTmpDir();
|
||||
try {
|
||||
const binDir = path.join(dir, 'bin');
|
||||
const vendored = path.join(dir, 'vendored');
|
||||
const vendoredPager = path.join(dir, 'vendored-pager');
|
||||
|
||||
// v1
|
||||
fs.writeFileSync(vendored, 'grok-v1');
|
||||
fs.writeFileSync(vendoredPager, 'pager-v1');
|
||||
installNamedBinary(vendored, 'grok', '0.1.140', binDir);
|
||||
installNamedBinary(vendoredPager, 'grok-pager', '0.1.140', binDir);
|
||||
|
||||
// v2
|
||||
fs.writeFileSync(vendored, 'grok-v2');
|
||||
fs.writeFileSync(vendoredPager, 'pager-v2');
|
||||
installNamedBinary(vendored, 'grok', '0.1.141', binDir);
|
||||
installNamedBinary(vendoredPager, 'grok-pager', '0.1.141', binDir);
|
||||
|
||||
// v3
|
||||
fs.writeFileSync(vendored, 'grok-v3');
|
||||
fs.writeFileSync(vendoredPager, 'pager-v3');
|
||||
installNamedBinary(vendored, 'grok', '0.1.142', binDir);
|
||||
installNamedBinary(vendoredPager, 'grok-pager', '0.1.142', binDir);
|
||||
|
||||
// Cleanup both independently
|
||||
cleanupOldVersionsNamed(binDir, 'grok', '0.1.142');
|
||||
cleanupOldVersionsNamed(binDir, 'grok-pager', '0.1.142');
|
||||
|
||||
// Current + N-1 for each
|
||||
assert.ok(fs.existsSync(path.join(binDir, 'grok-0.1.142')));
|
||||
assert.ok(fs.existsSync(path.join(binDir, 'grok-0.1.141')));
|
||||
assert.ok(!fs.existsSync(path.join(binDir, 'grok-0.1.140')));
|
||||
|
||||
assert.ok(fs.existsSync(path.join(binDir, 'grok-pager-0.1.142')));
|
||||
assert.ok(fs.existsSync(path.join(binDir, 'grok-pager-0.1.141')));
|
||||
assert.ok(!fs.existsSync(path.join(binDir, 'grok-pager-0.1.140')));
|
||||
|
||||
// Symlinks correct
|
||||
assert.strictEqual(fs.readlinkSync(path.join(binDir, 'grok')), 'grok-0.1.142');
|
||||
assert.strictEqual(fs.readlinkSync(path.join(binDir, 'grok-pager')), 'grok-pager-0.1.142');
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// macOS-only Pager Platform Split Tests
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
console.log('\nmacOS-only pager platform split tests\n');
|
||||
|
||||
test('grok installs normally regardless of platform key', () => {
|
||||
const dir = makeTmpDir();
|
||||
try {
|
||||
const binDir = path.join(dir, 'bin');
|
||||
const vendored = path.join(dir, 'vendored-grok');
|
||||
fs.writeFileSync(vendored, 'grok-binary');
|
||||
|
||||
for (const platform of ['darwin-arm64', 'linux-x64', 'linux-arm64']) {
|
||||
const result = installNamedBinary(vendored, 'grok', '0.1.150', binDir);
|
||||
assert.ok(fs.existsSync(result.versionedPath), `grok should install for ${platform}`);
|
||||
assert.strictEqual(fs.readlinkSync(result.canonicalPath), 'grok-0.1.150');
|
||||
}
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('grok-pager installs when vendored binary exists (darwin-arm64 path)', () => {
|
||||
const dir = makeTmpDir();
|
||||
try {
|
||||
const binDir = path.join(dir, 'bin');
|
||||
const vendoredPager = path.join(dir, 'vendored-pager');
|
||||
fs.writeFileSync(vendoredPager, 'pager-binary');
|
||||
|
||||
const result = installNamedBinary(vendoredPager, 'grok-pager', '0.1.150', binDir);
|
||||
assert.ok(fs.existsSync(result.versionedPath), 'pager versioned binary should exist');
|
||||
assert.strictEqual(fs.readlinkSync(result.canonicalPath), 'grok-pager-0.1.150');
|
||||
assert.strictEqual(fs.readFileSync(result.canonicalPath, 'utf8'), 'pager-binary');
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('Linux pager vendor files are not required for grok install', () => {
|
||||
const dir = makeTmpDir();
|
||||
try {
|
||||
const binDir = path.join(dir, 'bin');
|
||||
const vendorBase = path.join(dir, 'vendor');
|
||||
|
||||
// Only darwin-arm64 pager exists (mirrors npm tarball)
|
||||
fs.mkdirSync(path.join(vendorBase, 'darwin-arm64'), { recursive: true });
|
||||
fs.writeFileSync(path.join(vendorBase, 'darwin-arm64', 'grok-pager'), 'mac-pager');
|
||||
|
||||
// Linux pager vendor dirs exist but without pager binaries
|
||||
fs.mkdirSync(path.join(vendorBase, 'linux-x64'), { recursive: true });
|
||||
fs.mkdirSync(path.join(vendorBase, 'linux-arm64'), { recursive: true });
|
||||
|
||||
// Verify no Linux pager binaries
|
||||
assert.ok(!fs.existsSync(path.join(vendorBase, 'linux-x64', 'grok-pager')));
|
||||
assert.ok(!fs.existsSync(path.join(vendorBase, 'linux-arm64', 'grok-pager')));
|
||||
|
||||
// grok install should succeed independently
|
||||
const grokVendored = path.join(dir, 'vendored-grok');
|
||||
fs.writeFileSync(grokVendored, 'grok-linux');
|
||||
const result = installNamedBinary(grokVendored, 'grok', '0.1.150', binDir);
|
||||
assert.ok(fs.existsSync(result.versionedPath), 'grok should install without Linux pager');
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('skipping pager install on Linux does not affect grok cleanup', () => {
|
||||
const dir = makeTmpDir();
|
||||
try {
|
||||
const binDir = path.join(dir, 'bin');
|
||||
const vendored = path.join(dir, 'vendored');
|
||||
|
||||
// Install grok across two versions
|
||||
fs.writeFileSync(vendored, 'grok-v1');
|
||||
installNamedBinary(vendored, 'grok', '0.1.149', binDir);
|
||||
fs.writeFileSync(vendored, 'grok-v2');
|
||||
installNamedBinary(vendored, 'grok', '0.1.150', binDir);
|
||||
|
||||
// Simulate Linux: only run grok cleanup, skip pager entirely
|
||||
cleanupOldVersionsNamed(binDir, 'grok', '0.1.150');
|
||||
|
||||
assert.ok(fs.existsSync(path.join(binDir, 'grok-0.1.150')), 'current grok should exist');
|
||||
assert.ok(fs.existsSync(path.join(binDir, 'grok-0.1.149')), 'N-1 grok should be kept');
|
||||
assert.strictEqual(fs.readlinkSync(path.join(binDir, 'grok')), 'grok-0.1.150');
|
||||
|
||||
// No pager files should exist at all
|
||||
const entries = fs.readdirSync(binDir);
|
||||
const pagerEntries = entries.filter(e => e.includes('pager'));
|
||||
assert.strictEqual(pagerEntries.length, 0, 'no pager artifacts on Linux');
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('canonical pager from non-npm install is preserved on Linux', () => {
|
||||
const dir = makeTmpDir();
|
||||
try {
|
||||
const binDir = path.join(dir, 'bin');
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
|
||||
// Simulate pager installed by install-grok.sh (not npm)
|
||||
const pagerVersioned = path.join(binDir, 'grok-pager-0.1.150');
|
||||
fs.writeFileSync(pagerVersioned, 'installer-pager-binary');
|
||||
fs.chmodSync(pagerVersioned, 0o755);
|
||||
const pagerCanonical = path.join(binDir, 'grok-pager');
|
||||
fs.symlinkSync('grok-pager-0.1.150', pagerCanonical);
|
||||
|
||||
// Run grok-only install + cleanup (simulating Linux postinstall)
|
||||
const vendored = path.join(dir, 'vendored');
|
||||
fs.writeFileSync(vendored, 'grok-binary');
|
||||
installNamedBinary(vendored, 'grok', '0.1.150', binDir);
|
||||
cleanupOldVersionsNamed(binDir, 'grok', '0.1.150');
|
||||
|
||||
// Pager installed by other means must be untouched
|
||||
assert.ok(fs.existsSync(pagerCanonical), 'canonical pager should survive');
|
||||
assert.ok(fs.existsSync(pagerVersioned), 'versioned pager should survive');
|
||||
assert.strictEqual(fs.readlinkSync(pagerCanonical), 'grok-pager-0.1.150');
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
// ─── Summary ───────────────────────────────────────────────────────────
|
||||
|
||||
console.log(`\n${passed} passed, ${failed} failed`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
Loading…
Reference in a new issue