From bc7e8f403416c6830e3971bea0bd55cd0a1f60e9 Mon Sep 17 00:00:00 2001 From: seanseannery Date: Fri, 6 Mar 2026 18:52:29 -0800 Subject: [PATCH 1/2] feat: add npm GitHub repo install support (Issue#11) --- README.md | 7 +++++ install/install_test.sh | 25 +++++++++++++++++ install/npm_install.js | 60 +++++++++++++++++++++++++++++++++++++++++ package.json | 21 +++++++++++++++ 4 files changed, 113 insertions(+) create mode 100644 install/npm_install.js create mode 100644 package.json diff --git a/README.md b/README.md index c1861a5..c54a5d6 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,13 @@ ``` After tapping, `brew upgrade seanseannery/opsfile/opsfile` keeps `ops` up to date. + ### npm (MacOS / Linux) + + ```bash + npm install -g github:seanseannery/opsfile + ``` + Requires Node.js ≥ 14. Downloads the correct platform binary from the latest GitHub release on install. + ### curl (MacOS / Linux) ```bash diff --git a/install/install_test.sh b/install/install_test.sh index f005c69..c436baa 100644 --- a/install/install_test.sh +++ b/install/install_test.sh @@ -10,6 +10,7 @@ FAIL=0 BREW_TAPPED=false BREW_INSTALLED=false +NPM_INSTALLED=false CURL_TMP_DIR="" @@ -29,6 +30,9 @@ cleanup() { if [ "$BREW_TAPPED" = true ]; then brew untap seanseannery/opsfile 2>/dev/null && echo " brew untap: done" || echo " brew untap: skipped" fi + if [ "$NPM_INSTALLED" = true ]; then + npm uninstall -g opsfile 2>/dev/null && echo " npm uninstall: done" || echo " npm uninstall: skipped" + fi } trap cleanup EXIT @@ -76,6 +80,27 @@ else fi fi +# ── npm install test ───────────────────────────────────────────────────────── + +echo "" +echo "=== npm install test ===" + +if ! command -v npm > /dev/null 2>&1; then + echo " SKIP: npm not found" +else + if npm install -g github:seanseannery/opsfile --no-fund --no-audit 2>&1; then + NPM_INSTALLED=true + NPM_OPS="$(npm config get prefix)/bin/ops" + if "$NPM_OPS" --version > /dev/null 2>&1; then + pass "ops installed via npm and responds to --version" + else + fail "ops installed via npm but --version failed" + fi + else + fail "npm install github:seanseannery/opsfile failed" + fi +fi + # ── summary ────────────────────────────────────────────────────────────────── echo "" diff --git a/install/npm_install.js b/install/npm_install.js new file mode 100644 index 0000000..0d8f703 --- /dev/null +++ b/install/npm_install.js @@ -0,0 +1,60 @@ +#!/usr/bin/env node +'use strict'; + +const https = require('https'); +const fs = require('fs'); +const path = require('path'); + +const REPO = 'seanseannery/opsfile'; +const BIN_DIR = path.join(__dirname, '..', 'bin'); +const BIN_PATH = path.join(BIN_DIR, 'ops'); + +function platformAssetPrefix() { + switch (process.platform) { + case 'darwin': return 'ops_darwin_v'; + case 'linux': return 'ops_unix_v'; + default: + console.error(`Unsupported platform: ${process.platform}`); + process.exit(1); + } +} + +function get(url) { + return new Promise((resolve, reject) => { + https.get(url, { headers: { 'User-Agent': 'opsfile-npm-installer' } }, (res) => { + if (res.statusCode === 301 || res.statusCode === 302) { + return get(res.headers.location).then(resolve).catch(reject); + } + 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() { + const prefix = platformAssetPrefix(); + + console.log(`Fetching latest release from github.com/${REPO} ...`); + const apiData = await get(`https://api.github.com/repos/${REPO}/releases/latest`); + const release = JSON.parse(apiData.toString()); + + const asset = release.assets.find(a => a.name.startsWith(prefix)); + if (!asset) { + console.error(`No release asset found matching '${prefix}'`); + process.exit(1); + } + + console.log(`Downloading ops ${release.tag_name} for ${process.platform} ...`); + const binary = await get(asset.browser_download_url); + + fs.mkdirSync(BIN_DIR, { recursive: true }); + fs.writeFileSync(BIN_PATH, binary, { mode: 0o755 }); + console.log(`Installed ops ${release.tag_name}`); +} + +main().catch(err => { + console.error('Install failed:', err.message); + process.exit(1); +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..6262c08 --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "opsfile", + "version": "0.8.1", + "description": "Like make/Makefile but for live operations commands", + "homepage": "https://github.com/seanseannery/opsfile", + "repository": { + "type": "git", + "url": "https://github.com/seanseannery/opsfile.git" + }, + "license": "MIT", + "bin": { + "ops": "bin/ops" + }, + "scripts": { + "postinstall": "node install/npm_install.js" + }, + "os": ["darwin", "linux"], + "engines": { + "node": ">=14" + } +} From 88d5e5e7def5e70a4a808f29352c932710cb1e17 Mon Sep 17 00:00:00 2001 From: seanseannery Date: Fri, 6 Mar 2026 19:01:42 -0800 Subject: [PATCH 2/2] docs: clarify npm upgrade and uninstall commands in README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c54a5d6..5414650 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ npm install -g github:seanseannery/opsfile ``` Requires Node.js ≥ 14. Downloads the correct platform binary from the latest GitHub release on install. + To upgrade, re-run the same command. To uninstall: `npm uninstall -g opsfile`. ### curl (MacOS / Linux)