ai-agent-board 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +271 -0
- package/bin/download.js +121 -0
- package/package.json +35 -0
package/bin/cli.js
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* agent-board CLI
|
|
5
|
+
*
|
|
6
|
+
* Lightweight wrapper that downloads, verifies, extracts, and runs
|
|
7
|
+
* the agent-board binary from Cloudflare R2.
|
|
8
|
+
*
|
|
9
|
+
* Usage: npx agent-board [--port <number>] [--no-open]
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync, mkdirSync, rmSync, chmodSync, readFileSync, unlinkSync } from 'node:fs';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { homedir, platform, arch } from 'node:os';
|
|
15
|
+
import { spawn } from 'node:child_process';
|
|
16
|
+
import { R2_BASE_URL, fetchJSON, downloadFile, verifySHA256 } from './download.js';
|
|
17
|
+
|
|
18
|
+
// ── Read version from package.json ─────────────────────────────────
|
|
19
|
+
const pkg = JSON.parse(
|
|
20
|
+
readFileSync(new URL('../package.json', import.meta.url), 'utf-8')
|
|
21
|
+
);
|
|
22
|
+
const VERSION = pkg.version;
|
|
23
|
+
|
|
24
|
+
// ── Platform detection ─────────────────────────────────────────────
|
|
25
|
+
function detectPlatform() {
|
|
26
|
+
const plat = platform();
|
|
27
|
+
const ar = arch();
|
|
28
|
+
|
|
29
|
+
const map = {
|
|
30
|
+
'linux-x64': 'linux-x64',
|
|
31
|
+
'darwin-x64': 'macos-x64',
|
|
32
|
+
'darwin-arm64': 'macos-arm64',
|
|
33
|
+
'win32-x64': 'win-x64',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const key = `${plat}-${ar}`;
|
|
37
|
+
const mapped = map[key];
|
|
38
|
+
|
|
39
|
+
if (!mapped) {
|
|
40
|
+
console.error(`\n Unsupported platform: ${key}`);
|
|
41
|
+
console.error(' Supported: linux-x64, macos-x64, macos-arm64, win-x64\n');
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return mapped;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── Cache management ───────────────────────────────────────────────
|
|
49
|
+
function getCacheDir(platformId) {
|
|
50
|
+
const base = process.env.XDG_CACHE_HOME || join(homedir(), '.cache');
|
|
51
|
+
return join(base, 'agent-board', `v${VERSION}`, platformId);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getBinaryName() {
|
|
55
|
+
return platform() === 'win32' ? 'agent-board.exe' : 'agent-board';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── ZIP extraction ─────────────────────────────────────────────────
|
|
59
|
+
async function extractZip(zipPath, destDir) {
|
|
60
|
+
// adm-zip is the only external dependency
|
|
61
|
+
const AdmZip = (await import('adm-zip')).default;
|
|
62
|
+
const zip = new AdmZip(zipPath);
|
|
63
|
+
zip.extractAllTo(destDir, /* overwrite */ true);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── Download, verify, and extract ──────────────────────────────────
|
|
67
|
+
async function downloadAndExtract(platformId, cacheDir) {
|
|
68
|
+
const tag = `v${VERSION}`;
|
|
69
|
+
const manifestUrl = `${R2_BASE_URL}/binaries/${tag}/manifest.json`;
|
|
70
|
+
const zipUrl = `${R2_BASE_URL}/binaries/${tag}/${platformId}/agent-board.zip`;
|
|
71
|
+
const zipDest = join(cacheDir, 'agent-board.zip');
|
|
72
|
+
|
|
73
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
74
|
+
|
|
75
|
+
console.log(`\n agent-board ${tag}`);
|
|
76
|
+
console.log(` Platform: ${platformId}`);
|
|
77
|
+
console.log(` Cache: ${cacheDir}\n`);
|
|
78
|
+
|
|
79
|
+
// 1. Fetch manifest for checksums
|
|
80
|
+
let manifest;
|
|
81
|
+
try {
|
|
82
|
+
manifest = await fetchJSON(manifestUrl);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
console.error(` Failed to fetch manifest: ${err.message}`);
|
|
85
|
+
console.error(` URL: ${manifestUrl}\n`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const entry = manifest.platforms && manifest.platforms[platformId];
|
|
90
|
+
if (!entry || !entry.sha256) {
|
|
91
|
+
console.error(` No checksum found for platform "${platformId}" in manifest.\n`);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 2. Download ZIP
|
|
96
|
+
try {
|
|
97
|
+
await downloadFile(zipUrl, zipDest, (downloaded, total) => {
|
|
98
|
+
if (total > 0) {
|
|
99
|
+
const pct = Math.round((downloaded / total) * 100);
|
|
100
|
+
const mb = (downloaded / 1024 / 1024).toFixed(1);
|
|
101
|
+
const totalMb = (total / 1024 / 1024).toFixed(1);
|
|
102
|
+
process.stdout.write(`\r Downloading... ${mb} MB / ${totalMb} MB (${pct}%)`);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
process.stdout.write('\n');
|
|
106
|
+
} catch (err) {
|
|
107
|
+
console.error(`\n Failed to download: ${err.message}`);
|
|
108
|
+
console.error(` URL: ${zipUrl}\n`);
|
|
109
|
+
cleanup(zipDest);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 3. Verify SHA-256
|
|
114
|
+
process.stdout.write(' Verifying checksum... ');
|
|
115
|
+
if (!verifySHA256(zipDest, entry.sha256)) {
|
|
116
|
+
console.error('FAILED');
|
|
117
|
+
console.error('\n SHA-256 mismatch — the download may be corrupted or tampered with.');
|
|
118
|
+
console.error(' Try running with --clear-cache and retry.\n');
|
|
119
|
+
cleanup(zipDest);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
console.log('OK');
|
|
123
|
+
|
|
124
|
+
// 4. Extract ZIP
|
|
125
|
+
process.stdout.write(' Extracting... ');
|
|
126
|
+
try {
|
|
127
|
+
await extractZip(zipDest, cacheDir);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
console.error('FAILED');
|
|
130
|
+
console.error(`\n Extraction error: ${err.message}\n`);
|
|
131
|
+
cleanup(zipDest);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
console.log('OK');
|
|
135
|
+
|
|
136
|
+
// 5. Clean up ZIP
|
|
137
|
+
cleanup(zipDest);
|
|
138
|
+
|
|
139
|
+
// 6. Make binary executable on Unix
|
|
140
|
+
if (platform() !== 'win32') {
|
|
141
|
+
const binPath = join(cacheDir, getBinaryName());
|
|
142
|
+
chmodSync(binPath, 0o755);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
console.log(' Download complete.\n');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function cleanup(filePath) {
|
|
149
|
+
try {
|
|
150
|
+
if (existsSync(filePath)) unlinkSync(filePath);
|
|
151
|
+
} catch {
|
|
152
|
+
// Ignore cleanup errors
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── Auto-update check (non-blocking) ──────────────────────────────
|
|
157
|
+
function checkForUpdates() {
|
|
158
|
+
const latestUrl = `${R2_BASE_URL}/latest.json`;
|
|
159
|
+
|
|
160
|
+
fetchJSON(latestUrl)
|
|
161
|
+
.then((data) => {
|
|
162
|
+
if (data && data.version && data.version !== VERSION) {
|
|
163
|
+
console.log(`\n Update available: v${VERSION} -> v${data.version}`);
|
|
164
|
+
console.log(' Run: npx agent-board@latest\n');
|
|
165
|
+
}
|
|
166
|
+
})
|
|
167
|
+
.catch(() => {
|
|
168
|
+
// Silently ignore — this is a non-blocking check
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── Run the binary ─────────────────────────────────────────────────
|
|
173
|
+
function runBinary(cacheDir, args) {
|
|
174
|
+
const binaryPath = join(cacheDir, getBinaryName());
|
|
175
|
+
|
|
176
|
+
const child = spawn(binaryPath, args, {
|
|
177
|
+
cwd: cacheDir,
|
|
178
|
+
stdio: 'inherit',
|
|
179
|
+
env: {
|
|
180
|
+
...process.env,
|
|
181
|
+
__BIN_MODE__: '1',
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
child.on('error', (err) => {
|
|
186
|
+
if (err.code === 'EACCES') {
|
|
187
|
+
console.error('\n Permission denied. Try: chmod +x ' + binaryPath);
|
|
188
|
+
} else if (err.code === 'ENOENT') {
|
|
189
|
+
console.error('\n Binary not found. Cache may be corrupted.');
|
|
190
|
+
console.error(' Try: npx agent-board --clear-cache');
|
|
191
|
+
} else {
|
|
192
|
+
console.error(`\n Failed to start agent-board: ${err.message}`);
|
|
193
|
+
}
|
|
194
|
+
process.exit(1);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
child.on('exit', (code) => {
|
|
198
|
+
process.exit(code ?? 0);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Forward signals to child process
|
|
202
|
+
for (const sig of ['SIGTERM', 'SIGINT']) {
|
|
203
|
+
process.on(sig, () => {
|
|
204
|
+
child.kill(sig);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ── CLI argument parsing ───────────────────────────────────────────
|
|
210
|
+
function parseArgs() {
|
|
211
|
+
const args = process.argv.slice(2);
|
|
212
|
+
|
|
213
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
214
|
+
console.log(`
|
|
215
|
+
agent-board v${VERSION}
|
|
216
|
+
|
|
217
|
+
Usage: npx agent-board [options]
|
|
218
|
+
|
|
219
|
+
Options:
|
|
220
|
+
--port <number> Server port (default: auto-detect)
|
|
221
|
+
--no-open Don't open browser automatically
|
|
222
|
+
--clear-cache Delete cached binary and re-download
|
|
223
|
+
--version, -v Show version
|
|
224
|
+
--help, -h Show this help
|
|
225
|
+
`);
|
|
226
|
+
process.exit(0);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (args.includes('--version') || args.includes('-v')) {
|
|
230
|
+
console.log(`agent-board v${VERSION}`);
|
|
231
|
+
process.exit(0);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (args.includes('--clear-cache')) {
|
|
235
|
+
const base = process.env.XDG_CACHE_HOME || join(homedir(), '.cache');
|
|
236
|
+
const versionCacheDir = join(base, 'agent-board', `v${VERSION}`);
|
|
237
|
+
if (existsSync(versionCacheDir)) {
|
|
238
|
+
rmSync(versionCacheDir, { recursive: true });
|
|
239
|
+
console.log(` Cache cleared: ${versionCacheDir}`);
|
|
240
|
+
} else {
|
|
241
|
+
console.log(' No cache to clear.');
|
|
242
|
+
}
|
|
243
|
+
process.exit(0);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return args;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ── Main ───────────────────────────────────────────────────────────
|
|
250
|
+
async function main() {
|
|
251
|
+
const args = parseArgs();
|
|
252
|
+
const platformId = detectPlatform();
|
|
253
|
+
const cacheDir = getCacheDir(platformId);
|
|
254
|
+
const binaryPath = join(cacheDir, getBinaryName());
|
|
255
|
+
|
|
256
|
+
// Download if not cached
|
|
257
|
+
if (!existsSync(binaryPath)) {
|
|
258
|
+
await downloadAndExtract(platformId, cacheDir);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Non-blocking update check
|
|
262
|
+
checkForUpdates();
|
|
263
|
+
|
|
264
|
+
// Run the binary, forwarding all CLI args
|
|
265
|
+
runBinary(cacheDir, args);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
main().catch((err) => {
|
|
269
|
+
console.error(`\n Unexpected error: ${err.message}\n`);
|
|
270
|
+
process.exit(1);
|
|
271
|
+
});
|
package/bin/download.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Download utilities for agent-board CLI.
|
|
3
|
+
*
|
|
4
|
+
* Handles fetching JSON manifests, downloading files with progress,
|
|
5
|
+
* and verifying SHA-256 checksums. Uses only Node.js built-ins.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { get as httpsGet } from 'node:https';
|
|
9
|
+
import { createWriteStream, readFileSync } from 'node:fs';
|
|
10
|
+
import { createHash } from 'node:crypto';
|
|
11
|
+
|
|
12
|
+
// Will be replaced with the real Cloudflare R2 public bucket URL
|
|
13
|
+
export const R2_BASE_URL = 'https://pub-3e8e5cea43b3427fa24870c7a04e46dd.r2.dev';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Fetch JSON from a URL. Follows redirects (up to 5).
|
|
17
|
+
* @param {string} url
|
|
18
|
+
* @returns {Promise<any>}
|
|
19
|
+
*/
|
|
20
|
+
export function fetchJSON(url) {
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
let redirects = 0;
|
|
23
|
+
|
|
24
|
+
const follow = (targetUrl) => {
|
|
25
|
+
if (redirects++ > 5) {
|
|
26
|
+
return reject(new Error('Too many redirects'));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
httpsGet(targetUrl, (res) => {
|
|
30
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
31
|
+
return follow(res.headers.location);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (res.statusCode !== 200) {
|
|
35
|
+
// Consume response to free the socket
|
|
36
|
+
res.resume();
|
|
37
|
+
return reject(new Error(`HTTP ${res.statusCode} fetching ${url}`));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let data = '';
|
|
41
|
+
res.setEncoding('utf-8');
|
|
42
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
43
|
+
res.on('end', () => {
|
|
44
|
+
try {
|
|
45
|
+
resolve(JSON.parse(data));
|
|
46
|
+
} catch (e) {
|
|
47
|
+
reject(new Error(`Invalid JSON from ${url}: ${e.message}`));
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}).on('error', reject);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
follow(url);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Download a file to disk with optional progress callback. Follows redirects.
|
|
59
|
+
* @param {string} url
|
|
60
|
+
* @param {string} dest Absolute path to write to
|
|
61
|
+
* @param {(downloaded: number, total: number) => void} [onProgress]
|
|
62
|
+
* @returns {Promise<void>}
|
|
63
|
+
*/
|
|
64
|
+
export function downloadFile(url, dest, onProgress) {
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
let redirects = 0;
|
|
67
|
+
|
|
68
|
+
const follow = (targetUrl) => {
|
|
69
|
+
if (redirects++ > 5) {
|
|
70
|
+
return reject(new Error('Too many redirects'));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
httpsGet(targetUrl, (res) => {
|
|
74
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
75
|
+
return follow(res.headers.location);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (res.statusCode !== 200) {
|
|
79
|
+
res.resume();
|
|
80
|
+
return reject(new Error(`HTTP ${res.statusCode} downloading ${url}`));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const totalBytes = parseInt(res.headers['content-length'] || '0', 10);
|
|
84
|
+
let downloadedBytes = 0;
|
|
85
|
+
const file = createWriteStream(dest);
|
|
86
|
+
|
|
87
|
+
res.on('data', (chunk) => {
|
|
88
|
+
downloadedBytes += chunk.length;
|
|
89
|
+
if (onProgress) {
|
|
90
|
+
onProgress(downloadedBytes, totalBytes);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
res.pipe(file);
|
|
95
|
+
|
|
96
|
+
file.on('finish', () => {
|
|
97
|
+
file.close(() => resolve());
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
file.on('error', (err) => {
|
|
101
|
+
file.close();
|
|
102
|
+
reject(err);
|
|
103
|
+
});
|
|
104
|
+
}).on('error', reject);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
follow(url);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Verify that a file matches an expected SHA-256 hex digest.
|
|
113
|
+
* @param {string} filePath
|
|
114
|
+
* @param {string} expectedHash Lowercase hex string
|
|
115
|
+
* @returns {boolean}
|
|
116
|
+
*/
|
|
117
|
+
export function verifySHA256(filePath, expectedHash) {
|
|
118
|
+
const fileBuffer = readFileSync(filePath);
|
|
119
|
+
const actualHash = createHash('sha256').update(fileBuffer).digest('hex');
|
|
120
|
+
return actualHash === expectedHash.toLowerCase();
|
|
121
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ai-agent-board",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Dashboard for managing autonomous AI agent tasks — run with npx agent-board",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ai-agent-board": "bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/"
|
|
11
|
+
],
|
|
12
|
+
"keywords": [
|
|
13
|
+
"ai",
|
|
14
|
+
"agent",
|
|
15
|
+
"dashboard",
|
|
16
|
+
"task-management",
|
|
17
|
+
"llm",
|
|
18
|
+
"automation"
|
|
19
|
+
],
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/ezeoli88/dash-agent.git"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/ezeoli88/dash-agent#readme",
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/ezeoli88/dash-agent/issues"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"adm-zip": "^0.5.16"
|
|
34
|
+
}
|
|
35
|
+
}
|