Files
shivammittal274 565ce18eba feat: add npm/npx distribution for BrowserOS CLI (#618)
* feat(cli): skip self-update prompts for package manager installs

Checks BROWSEROS_INSTALL_METHOD env var (npm, brew) and skips automatic
update checks. Users should use their package manager's update mechanism.
FormatNotice now shows the appropriate upgrade command based on install method.

* feat(cli): add npm bin wrapper for browseros-cli

* feat(cli): add npm postinstall script to download platform binary

Downloads the correct platform binary from GitHub releases during npm
install, verifies SHA256 checksums, and extracts to .binary directory.

* feat(cli): add npm package metadata and README

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: move npm package files to correct monorepo path

The bin wrapper and postinstall were created at apps/cli/npm/ instead of
packages/browseros-agent/apps/cli/npm/. Moves them to the correct location.

* style: use node: protocol for builtin module imports

* feat(cli): add Makefile npm targets and release workflow npm publish step

Adds npm-version and npm-publish Makefile targets for version sync.
Adds Node.js setup and npm publish step to the release workflow.
Adds npm/npx install instructions to release notes template.

* fix(cli): fail on missing checksum entry and limit redirect depth

- Abort if checksums.txt downloaded but archive entry is missing
- Warn if checksums.txt itself failed to download
- Cap redirect depth at 5 to prevent stack overflow on circular redirects

* fix(cli): match install.sh checksum behavior — warn instead of abort

The existing shell installer (install.sh) warns and continues when the
checksum entry is missing from checksums.txt. Match that behavior in the
npm postinstall to avoid unnecessary install failures. Both files come
from the same GitHub release, so the checksum is a corruption check,
not a strong security boundary.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 22:30:58 +05:30

143 lines
4.3 KiB
JavaScript

const https = require('node:https')
const http = require('node:http')
const fs = require('node:fs')
const path = require('node:path')
const { execSync } = require('node:child_process')
const { createHash } = require('node:crypto')
const VERSION = require('../package.json').version
const GITHUB_RELEASE_BASE = `https://github.com/browseros-ai/BrowserOS/releases/download/browseros-cli-v${VERSION}`
const BINARY_DIR = path.join(__dirname, '..', '.binary')
const EXT = process.platform === 'win32' ? '.exe' : ''
const BINARY_PATH = path.join(BINARY_DIR, `browseros-cli${EXT}`)
if (process.env.CI && !process.env.BROWSEROS_NPM_FORCE) {
process.exit(0)
}
const PLATFORM_MAP = { darwin: 'darwin', linux: 'linux', win32: 'windows' }
const ARCH_MAP = { x64: 'amd64', arm64: 'arm64' }
const platform = PLATFORM_MAP[process.platform]
const arch = ARCH_MAP[process.arch]
if (!platform || !arch) {
console.error(
`browseros-cli: unsupported platform ${process.platform}/${process.arch}`,
)
process.exit(1)
}
const isWindows = platform === 'windows'
const archiveExt = isWindows ? 'zip' : 'tar.gz'
const archiveName = `browseros-cli_${VERSION}_${platform}_${arch}.${archiveExt}`
const archiveURL = `${GITHUB_RELEASE_BASE}/${archiveName}`
const checksumURL = `${GITHUB_RELEASE_BASE}/checksums.txt`
const MAX_REDIRECTS = 5
function download(url, redirects = 0) {
return new Promise((resolve, reject) => {
if (redirects > MAX_REDIRECTS) {
return reject(new Error(`Too many redirects for ${url}`))
}
const client = url.startsWith('https') ? https : http
client
.get(url, { headers: { 'User-Agent': 'browseros-cli-npm' } }, (res) => {
if (
res.statusCode >= 300 &&
res.statusCode < 400 &&
res.headers.location
) {
return download(res.headers.location, redirects + 1).then(
resolve,
reject,
)
}
if (res.statusCode !== 200) {
return reject(new Error(`HTTP ${res.statusCode} for ${url}`))
}
const chunks = []
res.on('data', (chunk) => chunks.push(chunk))
res.on('end', () => resolve(Buffer.concat(chunks)))
res.on('error', reject)
})
.on('error', reject)
})
}
async function main() {
console.log(
`browseros-cli: downloading v${VERSION} for ${platform}/${arch}...`,
)
const [archiveBuffer, checksumBuffer] = await Promise.all([
download(archiveURL),
download(checksumURL).catch(() => null),
])
if (checksumBuffer) {
const checksumText = checksumBuffer.toString('utf-8')
const expectedLine = checksumText
.split('\n')
.find((l) => l.includes(archiveName))
if (expectedLine) {
const expected = expectedLine.split(/\s+/)[0]
const actual = createHash('sha256').update(archiveBuffer).digest('hex')
if (actual !== expected) {
console.error(
`browseros-cli: checksum mismatch!\n expected: ${expected}\n got: ${actual}`,
)
process.exit(1)
}
console.log('browseros-cli: checksum verified.')
} else {
console.warn(
'browseros-cli: warning: checksum entry not found in checksums.txt, skipping verification.',
)
}
} else {
console.warn(
'browseros-cli: warning: could not fetch checksums.txt, skipping verification.',
)
}
fs.mkdirSync(BINARY_DIR, { recursive: true })
const tmpArchive = path.join(BINARY_DIR, archiveName)
fs.writeFileSync(tmpArchive, archiveBuffer)
if (isWindows) {
execSync(
`powershell -Command "Expand-Archive -Force -Path '${tmpArchive}' -DestinationPath '${BINARY_DIR}'"`,
{ stdio: 'inherit' },
)
} else {
execSync(`tar -xzf "${tmpArchive}" -C "${BINARY_DIR}"`, {
stdio: 'inherit',
})
}
fs.unlinkSync(tmpArchive)
if (!fs.existsSync(BINARY_PATH)) {
console.error(
`browseros-cli: binary not found after extraction at ${BINARY_PATH}`,
)
process.exit(1)
}
if (!isWindows) {
fs.chmodSync(BINARY_PATH, 0o755)
}
console.log(`browseros-cli: installed v${VERSION} successfully.`)
}
main().catch((err) => {
console.error(`browseros-cli: installation failed: ${err.message}`)
console.error(
'You can install manually: curl -fsSL https://cdn.browseros.com/cli/install.sh | bash',
)
process.exit(1)
})