feat: Add GitHub deployment artifacts (homebrew, npm) (gt-ufep6)

- Add .goreleaser.yml for multi-platform binary releases
- Add npm-package/ with postinstall binary download
- Add release.yml workflow for GoReleaser + npm publish + Homebrew tap

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
jack
2026-01-02 00:19:26 -08:00
committed by Steve Yegge
parent 8c7ea8a991
commit 03ffefc962
9 changed files with 783 additions and 0 deletions

12
npm-package/.npmignore Normal file
View File

@@ -0,0 +1,12 @@
# Ignore test files
test/
# Ignore development files
*.md
!README.md
# Ignore any local binaries (downloaded at install time)
bin/gt
bin/gt.exe
bin/*.tar.gz
bin/*.zip

21
npm-package/LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Steve Yegge
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

42
npm-package/README.md Normal file
View File

@@ -0,0 +1,42 @@
# @gastown/gt
Gas Town CLI - multi-agent workspace manager for coordinating AI coding agents.
## Installation
```bash
npm install -g @gastown/gt
```
This will download the appropriate native binary for your platform during installation.
## Usage
```bash
# Check version
gt version
# Initialize a new town
gt init
# View status
gt status
# List rigs
gt rigs
```
## Supported Platforms
- macOS (Intel and Apple Silicon)
- Linux (x64 and ARM64)
- Windows (x64)
## Manual Installation
If npm installation fails, you can download binaries directly from:
https://github.com/steveyegge/gastown/releases
## License
MIT

54
npm-package/bin/gt.js Normal file
View File

@@ -0,0 +1,54 @@
#!/usr/bin/env node
const { spawn } = require('child_process');
const path = require('path');
const os = require('os');
const fs = require('fs');
// Determine the platform-specific binary name
function getBinaryPath() {
const platform = os.platform();
const arch = os.arch();
let binaryName = 'gt';
if (platform === 'win32') {
binaryName = 'gt.exe';
}
// Binary is stored in the package's bin directory
const binaryPath = path.join(__dirname, binaryName);
if (!fs.existsSync(binaryPath)) {
console.error(`Error: gt binary not found at ${binaryPath}`);
console.error('This may indicate that the postinstall script failed to download the binary.');
console.error(`Platform: ${platform}, Architecture: ${arch}`);
process.exit(1);
}
return binaryPath;
}
// Execute the native binary with all arguments passed through
function main() {
const binaryPath = getBinaryPath();
// Spawn the native gt binary with all command-line arguments
const child = spawn(binaryPath, process.argv.slice(2), {
stdio: 'inherit',
env: process.env
});
child.on('error', (err) => {
console.error(`Error executing gt binary: ${err.message}`);
process.exit(1);
});
child.on('exit', (code, signal) => {
if (signal) {
process.exit(1);
}
process.exit(code || 0);
});
}
main();

51
npm-package/package.json Normal file
View File

@@ -0,0 +1,51 @@
{
"name": "@gastown/gt",
"version": "0.1.0",
"description": "Gas Town CLI - multi-agent workspace manager with native binary support",
"main": "bin/gt.js",
"bin": {
"gt": "bin/gt.js"
},
"scripts": {
"postinstall": "node scripts/postinstall.js",
"test": "node scripts/test.js"
},
"keywords": [
"multi-agent",
"workspace-manager",
"ai-agent",
"coding-agent",
"claude",
"git",
"orchestration"
],
"author": "Steve Yegge",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/steveyegge/gastown.git",
"directory": "npm-package"
},
"bugs": {
"url": "https://github.com/steveyegge/gastown/issues"
},
"homepage": "https://github.com/steveyegge/gastown#readme",
"engines": {
"node": ">=14.0.0"
},
"os": [
"darwin",
"linux",
"win32"
],
"cpu": [
"x64",
"arm64"
],
"files": [
"bin/",
"scripts/",
"README.md",
"LICENSE"
]
}

View File

@@ -0,0 +1,209 @@
#!/usr/bin/env node
const https = require('https');
const fs = require('fs');
const path = require('path');
const os = require('os');
const { execSync } = require('child_process');
// Get package version to determine which release to download
const packageJson = require('../package.json');
const VERSION = packageJson.version;
// Determine platform and architecture
function getPlatformInfo() {
const platform = os.platform();
const arch = os.arch();
let platformName;
let archName;
let binaryName = 'gt';
// Map Node.js platform names to GitHub release names
switch (platform) {
case 'darwin':
platformName = 'darwin';
break;
case 'linux':
platformName = 'linux';
break;
case 'win32':
platformName = 'windows';
binaryName = 'gt.exe';
break;
default:
throw new Error(`Unsupported platform: ${platform}`);
}
// Map Node.js arch names to GitHub release names
switch (arch) {
case 'x64':
archName = 'amd64';
break;
case 'arm64':
archName = 'arm64';
break;
default:
throw new Error(`Unsupported architecture: ${arch}`);
}
return { platformName, archName, binaryName };
}
// Download file from URL
function downloadFile(url, dest) {
return new Promise((resolve, reject) => {
console.log(`Downloading from: ${url}`);
const file = fs.createWriteStream(dest);
const request = https.get(url, (response) => {
// Handle redirects
if (response.statusCode === 301 || response.statusCode === 302) {
const redirectUrl = response.headers.location;
console.log(`Following redirect to: ${redirectUrl}`);
downloadFile(redirectUrl, dest).then(resolve).catch(reject);
return;
}
if (response.statusCode !== 200) {
reject(new Error(`Failed to download: HTTP ${response.statusCode}`));
return;
}
response.pipe(file);
file.on('finish', () => {
// Wait for file.close() to complete before resolving
// This is critical on Windows where the file may still be locked
file.close((err) => {
if (err) reject(err);
else resolve();
});
});
});
request.on('error', (err) => {
fs.unlink(dest, () => {});
reject(err);
});
file.on('error', (err) => {
fs.unlink(dest, () => {});
reject(err);
});
});
}
// Extract tar.gz file
function extractTarGz(tarGzPath, destDir, binaryName) {
console.log(`Extracting ${tarGzPath}...`);
try {
// Use tar command to extract
execSync(`tar -xzf "${tarGzPath}" -C "${destDir}"`, { stdio: 'inherit' });
// The binary should now be in destDir
const extractedBinary = path.join(destDir, binaryName);
if (!fs.existsSync(extractedBinary)) {
throw new Error(`Binary not found after extraction: ${extractedBinary}`);
}
// Make executable on Unix-like systems
if (os.platform() !== 'win32') {
fs.chmodSync(extractedBinary, 0o755);
}
console.log(`Binary extracted to: ${extractedBinary}`);
} catch (err) {
throw new Error(`Failed to extract archive: ${err.message}`);
}
}
// Extract zip file (for Windows)
function extractZip(zipPath, destDir, binaryName) {
console.log(`Extracting ${zipPath}...`);
try {
// Use unzip command or powershell on Windows
if (os.platform() === 'win32') {
execSync(`powershell -command "Expand-Archive -Path '${zipPath}' -DestinationPath '${destDir}' -Force"`, { stdio: 'inherit' });
} else {
execSync(`unzip -o "${zipPath}" -d "${destDir}"`, { stdio: 'inherit' });
}
// The binary should now be in destDir
const extractedBinary = path.join(destDir, binaryName);
if (!fs.existsSync(extractedBinary)) {
throw new Error(`Binary not found after extraction: ${extractedBinary}`);
}
console.log(`Binary extracted to: ${extractedBinary}`);
} catch (err) {
throw new Error(`Failed to extract archive: ${err.message}`);
}
}
// Main installation function
async function install() {
try {
const { platformName, archName, binaryName } = getPlatformInfo();
console.log(`Installing gt v${VERSION} for ${platformName}-${archName}...`);
// Construct download URL
// Format: https://github.com/steveyegge/gastown/releases/download/v0.1.0/gastown_0.1.0_darwin_amd64.tar.gz
const releaseVersion = VERSION;
const archiveExt = platformName === 'windows' ? 'zip' : 'tar.gz';
const archiveName = `gastown_${releaseVersion}_${platformName}_${archName}.${archiveExt}`;
const downloadUrl = `https://github.com/steveyegge/gastown/releases/download/v${releaseVersion}/${archiveName}`;
// Determine destination paths
const binDir = path.join(__dirname, '..', 'bin');
const archivePath = path.join(binDir, archiveName);
const binaryPath = path.join(binDir, binaryName);
// Ensure bin directory exists
if (!fs.existsSync(binDir)) {
fs.mkdirSync(binDir, { recursive: true });
}
// Download the archive
console.log(`Downloading gt binary...`);
await downloadFile(downloadUrl, archivePath);
// Extract the archive based on platform
if (platformName === 'windows') {
extractZip(archivePath, binDir, binaryName);
} else {
extractTarGz(archivePath, binDir, binaryName);
}
// Clean up archive
fs.unlinkSync(archivePath);
// Verify the binary works
try {
const output = execSync(`"${binaryPath}" version`, { encoding: 'utf8' });
console.log(`gt installed successfully: ${output.trim()}`);
} catch (err) {
console.warn('Warning: Could not verify binary version');
}
} catch (err) {
console.error(`Error installing gt: ${err.message}`);
console.error('');
console.error('Installation failed. You can try:');
console.error('1. Installing manually from: https://github.com/steveyegge/gastown/releases');
console.error('2. Opening an issue: https://github.com/steveyegge/gastown/issues');
process.exit(1);
}
}
// Run installation if not in CI environment
if (!process.env.CI) {
install();
} else {
console.log('Skipping binary download in CI environment');
}

View File

@@ -0,0 +1,53 @@
#!/usr/bin/env node
const { execSync } = require('child_process');
const path = require('path');
const fs = require('fs');
const os = require('os');
console.log('Running gt npm package tests...\n');
let passed = 0;
let failed = 0;
function test(name, fn) {
try {
fn();
console.log(`[PASS] ${name}`);
passed++;
} catch (err) {
console.log(`[FAIL] ${name}: ${err.message}`);
failed++;
}
}
// Test 1: Check binary exists
test('Binary exists in bin directory', () => {
const binaryName = os.platform() === 'win32' ? 'gt.exe' : 'gt';
const binaryPath = path.join(__dirname, '..', 'bin', binaryName);
if (!fs.existsSync(binaryPath)) {
throw new Error(`Binary not found at ${binaryPath}`);
}
});
// Test 2: Binary is executable (version check)
test('Binary executes and returns version', () => {
const binaryName = os.platform() === 'win32' ? 'gt.exe' : 'gt';
const binaryPath = path.join(__dirname, '..', 'bin', binaryName);
const output = execSync(`"${binaryPath}" version`, { encoding: 'utf8' });
if (!output.includes('gt version')) {
throw new Error(`Unexpected version output: ${output}`);
}
});
// Test 3: Wrapper script exists
test('Wrapper script (gt.js) exists', () => {
const wrapperPath = path.join(__dirname, '..', 'bin', 'gt.js');
if (!fs.existsSync(wrapperPath)) {
throw new Error(`Wrapper not found at ${wrapperPath}`);
}
});
// Summary
console.log(`\n${passed} passed, ${failed} failed`);
process.exit(failed > 0 ? 1 : 0);