From 27c19700764442252c6d85aaa8800e7dffd2f760 Mon Sep 17 00:00:00 2001 From: Sid Date: Tue, 23 Jun 2026 12:08:15 +0530 Subject: [PATCH] feat: add cli offline bundle script (#8192) --- scripts/bundle-cli.js | 125 ++++++++++++++++++++++++++++++++++++++++++ scripts/bundle-cli.sh | 6 ++ 2 files changed, 131 insertions(+) create mode 100755 scripts/bundle-cli.js create mode 100755 scripts/bundle-cli.sh diff --git a/scripts/bundle-cli.js b/scripts/bundle-cli.js new file mode 100755 index 000000000..80267dc9b --- /dev/null +++ b/scripts/bundle-cli.js @@ -0,0 +1,125 @@ +#!/usr/bin/env node +'use strict'; + +// pack a self-contained tarball of @usebruno/cli that installs offline +// - npm pack all workspace packages into tarballs/ +// - stage cli without node_modules, patch package.json to use local tarballs +// - npm install in stage, then npm pack the whole thing + +const { execSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); +const os = require('os'); + +const ROOT = path.resolve(__dirname, '..'); +const CLI_DIR = path.join(ROOT, 'packages', 'bruno-cli'); +const OUT_DIR = path.join(ROOT, 'dist', 'cli-bundle'); +const TARBALLS = path.join(OUT_DIR, 'tarballs'); + +const log = msg => process.stdout.write(`> ${msg}\n`); +const run = (cmd, cwd) => execSync(cmd, { cwd, stdio: 'pipe', encoding: 'utf8' }).trim(); + +function copyDir(src, dst) { + fs.mkdirSync(dst, { recursive: true }); + for (const entry of fs.readdirSync(src, { withFileTypes: true })) { + if (entry.name === 'node_modules') continue; + const s = path.join(src, entry.name); + const d = path.join(dst, entry.name); + if (entry.isDirectory()) { + copyDir(s, d); + } else { + fs.copyFileSync(s, d); + fs.chmodSync(d, fs.statSync(s).mode); + } + } +} + +const rootPkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')); + +const workspaces = {}; +for (const wsPath of rootPkg.workspaces) { + try { + const dir = path.join(ROOT, wsPath); + const pkgFile = path.join(dir, 'package.json'); + if (!fs.existsSync(pkgFile)) continue; + const pkg = JSON.parse(fs.readFileSync(pkgFile, 'utf8')); + workspaces[pkg.name] = { dir, version: pkg.version }; + } catch (err) { + log(` skip ${wsPath} (${err.message.split('\n')[0]})`); + } +} + +fs.rmSync(OUT_DIR, { recursive: true, force: true }); +fs.mkdirSync(TARBALLS, { recursive: true }); + +log('packing workspace packages...'); +const tarballMap = {}; + +for (const [name, { dir }] of Object.entries(workspaces)) { + try { + const { version } = workspaces[name]; + const filename = `${name.replace(/^@/, '').replace('/', '-')}-${version}.tgz`; + run(`npm pack --pack-destination "${TARBALLS}"`, dir); + tarballMap[name] = filename; + log(` ${name} -> ${filename}`); + } catch (err) { + console.error(err); + log(` skip ${name} (${err.message.split('\n')[0]})`); + } +} + +log('staging cli...'); +const stage = fs.mkdtempSync(path.join(os.tmpdir(), 'bruno-cli-bundle-')); + +const cleanup = () => { + try { fs.rmSync(stage, { recursive: true, force: true }); } catch {} +}; +process.on('exit', cleanup); +process.on('SIGINT', () => { cleanup(); process.exit(1); }); +process.on('SIGTERM', () => { cleanup(); process.exit(1); }); +process.on('uncaughtException', () => { cleanup(); process.exit(1); }); +process.on('unhandledRejection', () => { cleanup(); process.exit(1); }); + +copyDir(CLI_DIR, stage); + +const stageTarballs = path.join(stage, 'tarballs'); +fs.mkdirSync(stageTarballs, { recursive: true }); +for (const tarball of Object.values(tarballMap)) { + fs.copyFileSync(path.join(TARBALLS, tarball), path.join(stageTarballs, tarball)); +} + +const pkg = JSON.parse(fs.readFileSync(path.join(stage, 'package.json'), 'utf8')); + +for (const dep of Object.keys(pkg.dependencies || {})) { + if (tarballMap[dep]) { + pkg.dependencies[dep] = `file:./tarballs/${tarballMap[dep]}`; + } +} + +const tarballOverrides = {}; +for (const [name, tarball] of Object.entries(tarballMap)) { + tarballOverrides[name] = `file:./tarballs/${tarball}`; +} +pkg.overrides = { ...(rootPkg.overrides || {}), ...(pkg.overrides || {}), ...tarballOverrides }; + +pkg.bundledDependencies = Object.keys(pkg.dependencies || {}); +pkg.files = [...(pkg.files || []), 'tarballs', 'node_modules']; + +fs.writeFileSync(path.join(stage, 'package.json'), JSON.stringify(pkg, null, 2) + '\n'); + +log('installing...'); +execSync('npm install --ignore-scripts --no-audit --no-fund', { + cwd: stage, + stdio: 'inherit', +}); + +log('packing bundle...'); +run(`npm pack --pack-destination "${OUT_DIR}"`, stage); +const packed = fs.readdirSync(OUT_DIR).find(f => f.endsWith('.tgz')); +if (!packed) throw new Error('no .tgz found after npm pack'); +const finalPath = path.join(OUT_DIR, packed); + +log(''); +log(`done: ${finalPath}`); +log(` npm install -g "${finalPath}"`); +log(` npm install -g --prefer-offline "${finalPath}"`); diff --git a/scripts/bundle-cli.sh b/scripts/bundle-cli.sh new file mode 100755 index 000000000..77ab26e70 --- /dev/null +++ b/scripts/bundle-cli.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" + +node "${SCRIPT_DIR}/bundle-cli.js"