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.
Files changed (3) hide show
  1. package/bin/cli.js +271 -0
  2. package/bin/download.js +121 -0
  3. 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
+ });
@@ -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
+ }