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:
12
npm-package/.npmignore
Normal file
12
npm-package/.npmignore
Normal 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
21
npm-package/LICENSE
Normal 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
42
npm-package/README.md
Normal 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
54
npm-package/bin/gt.js
Normal 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
51
npm-package/package.json
Normal 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"
|
||||
]
|
||||
}
|
||||
209
npm-package/scripts/postinstall.js
Normal file
209
npm-package/scripts/postinstall.js
Normal 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');
|
||||
}
|
||||
53
npm-package/scripts/test.js
Normal file
53
npm-package/scripts/test.js
Normal 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);
|
||||
Reference in New Issue
Block a user