open source is social v-it.org
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 456 lines 15 kB view raw
1// SPDX-License-Identifier: MIT 2// Copyright (c) 2026 sol pbc 3 4import { DEFAULT_EXPLORE_URL } from '../lib/constants.js'; 5import { readProjectConfig } from '../lib/vit-dir.js'; 6import { brand } from '../lib/brand.js'; 7import { jsonOk, jsonError } from '../lib/json-output.js'; 8import { errorMessage, formatError } from '../lib/error-format.js'; 9 10function timeAgo(isoString) { 11 const seconds = Math.floor((Date.now() - new Date(isoString).getTime()) / 1000); 12 if (seconds < 60) return `${seconds}s ago`; 13 const minutes = Math.floor(seconds / 60); 14 if (minutes < 60) return `${minutes}m ago`; 15 const hours = Math.floor(minutes / 60); 16 if (hours < 24) return `${hours}h ago`; 17 const days = Math.floor(hours / 24); 18 if (days < 30) return `${days}d ago`; 19 const months = Math.floor(days / 30); 20 if (months < 12) return `${months}mo ago`; 21 const years = Math.floor(days / 365); 22 return `${years}y ago`; 23} 24 25function resolveUrl(opts) { 26 return opts.exploreUrl || process.env.VIT_EXPLORE_URL || DEFAULT_EXPLORE_URL; 27} 28 29function requestErrorMessage(method, url, err) { 30 const code = err?.cause?.code || err?.code; 31 if (code === 'ECONNREFUSED') return `could not connect to ${url} (refused)`; 32 if (code === 'ENOTFOUND') return `could not resolve ${url}`; 33 if (code === 'ETIMEDOUT' || code === 'UND_ERR_CONNECT_TIMEOUT') { 34 return `timed out connecting to ${url}`; 35 } 36 return `request to ${url} failed: ${errorMessage(err)}`; 37} 38 39async function fetchExploreJson(url) { 40 const requestUrl = url.toString(); 41 try { 42 const res = await fetch(url); 43 if (!res.ok) throw new Error(`GET ${requestUrl} returned ${res.status}`); 44 return await res.json(); 45 } catch (err) { 46 if (err instanceof Error && err.message.startsWith(`GET ${requestUrl} returned `)) { 47 throw err; 48 } 49 throw new Error(requestErrorMessage('GET', requestUrl, err), { cause: err }); 50 } 51} 52 53function mergeExploreOpts(opts, command) { 54 return { 55 ...(command?.parent?.opts?.() || {}), 56 ...(command?.opts?.() || opts || {}), 57 }; 58} 59 60async function fetchAndShowStats(opts) { 61 const baseUrl = resolveUrl(opts); 62 try { 63 const url = new URL('/api/stats', baseUrl); 64 const data = await fetchExploreJson(url); 65 66 if (opts.json) { 67 jsonOk(data); 68 return; 69 } 70 71 console.log(`${brand} explore stats`); 72 console.log(` caps: ${data.total_caps} skills: ${data.total_skills}`); 73 console.log(` vouches: ${data.total_vouches} beacons: ${data.total_beacons}`); 74 console.log(` active dids: ${data.active_dids} skill publishers: ${data.skill_publishers}`); 75 } catch (err) { 76 if (opts.json) { 77 jsonError(err); 78 return; 79 } 80 console.error(formatError(err, { verbose: false })); 81 process.exitCode = 1; 82 } 83} 84 85export default function register(program) { 86 const explore = program 87 .command('explore') 88 .description('Query the explore index for caps, skills, beacons, vouches, and stats') 89 .option('--json', 'Output as JSON') 90 .option('--explore-url <url>', 'Explore API base URL') 91 .action(async (opts) => { 92 await fetchAndShowStats(opts); 93 }); 94 95 explore 96 .command('cap') 97 .argument('<ref>', 'Cap ref to look up') 98 .description('Show details for a single cap') 99 .option('--beacon <beacon>', 'Scope lookup to a beacon') 100 .option('--json', 'Output as JSON') 101 .option('--explore-url <url>', 'Explore API base URL') 102 .action(async (ref, opts, command) => { 103 opts = mergeExploreOpts(opts, command); 104 const baseUrl = resolveUrl(opts); 105 106 try { 107 const url = new URL('/api/cap', baseUrl); 108 url.searchParams.set('ref', ref); 109 if (opts.beacon) url.searchParams.set('beacon', opts.beacon); 110 111 const data = await fetchExploreJson(url); 112 113 if (!data.cap) { 114 const msg = `no cap found with ref '${ref}'`; 115 if (opts.json) { 116 jsonError(msg); 117 return; 118 } 119 console.error(msg); 120 process.exitCode = 1; 121 return; 122 } 123 124 if (opts.json) { 125 jsonOk(data); 126 return; 127 } 128 129 const record = JSON.parse(data.cap.record_json); 130 console.log(`${brand} explore cap`); 131 console.log(` ${data.cap.title} [${record.kind}]`); 132 console.log(` ${data.cap.description}`); 133 console.log(); 134 console.log(` beacon: ${data.cap.beacon}`); 135 console.log(` author: @${data.cap.handle}`); 136 console.log(` ref: ${data.cap.ref}`); 137 console.log(` posted: ${timeAgo(data.cap.created_at)}`); 138 if (record.text) { 139 console.log(); 140 console.log(` ${record.text}`); 141 } 142 console.log(); 143 console.log(` vouches: ${data.cap.vouch_count}`); 144 console.log(); 145 console.log(` vit vet ${data.cap.ref} - inspect before adopting`); 146 console.log(` vit remix ${data.cap.ref} - remix this cap`); 147 } catch (err) { 148 if (opts.json) { 149 jsonError(err); 150 return; 151 } 152 console.error(formatError(err, { verbose: false })); 153 process.exitCode = 1; 154 } 155 }); 156 157 explore 158 .command('skill') 159 .argument('<name>', 'Skill name to look up') 160 .description('Show details for a single skill') 161 .option('--json', 'Output as JSON') 162 .option('--explore-url <url>', 'Explore API base URL') 163 .action(async (name, opts, command) => { 164 opts = mergeExploreOpts(opts, command); 165 const baseUrl = resolveUrl(opts); 166 167 try { 168 const url = new URL('/api/skill', baseUrl); 169 url.searchParams.set('name', name); 170 171 const data = await fetchExploreJson(url); 172 173 if (!data.skill) { 174 const msg = `no skill found with name '${name}'`; 175 if (opts.json) { 176 jsonError(msg); 177 return; 178 } 179 console.error(msg); 180 process.exitCode = 1; 181 return; 182 } 183 184 if (opts.json) { 185 jsonOk(data); 186 return; 187 } 188 189 const record = JSON.parse(data.skill.record_json); 190 console.log(`${brand} explore skill`); 191 console.log(` /${data.skill.name} v${data.skill.version}`); 192 console.log(` ${data.skill.description}`); 193 console.log(); 194 console.log(` author: @${data.skill.handle}`); 195 if (record.license) console.log(` license: ${record.license}`); 196 if (data.skill.tags) console.log(` tags: ${data.skill.tags}`); 197 console.log(` vouches: ${data.skill.vouch_count}`); 198 console.log(); 199 console.log(` vit learn skill-${data.skill.name} - install this skill`); 200 } catch (err) { 201 if (opts.json) { 202 jsonError(err); 203 return; 204 } 205 console.error(formatError(err, { verbose: false })); 206 process.exitCode = 1; 207 } 208 }); 209 210 explore 211 .command('caps') 212 .description('List recent caps from the explore index') 213 .option('--beacon <beacon>', 'Filter by beacon') 214 .option('--kind <kind>', 'Filter by cap kind (e.g. request, feat, fix)') 215 .option('--limit <n>', 'Limit number of caps') 216 .option('--cursor <id>', 'Pagination cursor') 217 .option('--json', 'Output as JSON') 218 .option('--explore-url <url>', 'Explore API base URL') 219 .action(async (opts, command) => { 220 opts = mergeExploreOpts(opts, command); 221 const baseUrl = resolveUrl(opts); 222 223 try { 224 let beacon = opts.beacon; 225 if (beacon === '.') { 226 const config = readProjectConfig(); 227 const beacons = [config.beacon, config.secondaryBeacon].filter(Boolean); 228 if (beacons.length === 0) { 229 const msg = "no beacon set — run 'vit init' first"; 230 if (opts.json) { 231 jsonError(msg); 232 return; 233 } 234 console.error(msg); 235 process.exitCode = 1; 236 return; 237 } 238 beacon = beacons.join(','); 239 } 240 241 const url = new URL('/api/caps', baseUrl); 242 if (beacon) url.searchParams.set('beacon', beacon); 243 if (opts.kind) url.searchParams.set('kind', opts.kind); 244 if (opts.limit) url.searchParams.set('limit', opts.limit); 245 if (opts.cursor) url.searchParams.set('cursor', opts.cursor); 246 247 const data = await fetchExploreJson(url); 248 249 if (opts.json) { 250 jsonOk({ caps: data.caps, cursor: data.cursor }); 251 return; 252 } 253 254 console.log(`${brand} explore caps`); 255 if (!data.caps?.length) { 256 console.log('no caps found.'); 257 console.log('the network is just getting started. ship a cap or skill to be one of the first.'); 258 console.log("try 'vit scan' for real-time discovery — the explore index may still be catching up."); 259 return; 260 } 261 262 for (const cap of data.caps) { 263 console.log(` ${cap.title} (${cap.ref})`); 264 console.log(` @${cap.handle} ${cap.beacon}`); 265 console.log(` ${cap.description}`); 266 } 267 if (data.cursor) { 268 console.log(`\nnext: --cursor ${data.cursor}`); 269 } 270 } catch (err) { 271 if (opts.json) { 272 jsonError(err); 273 return; 274 } 275 console.error(formatError(err, { verbose: false })); 276 process.exitCode = 1; 277 } 278 }); 279 280 explore 281 .command('skills') 282 .description('List published skills from the explore index') 283 .option('--tag <tag>', 'Filter by tag') 284 .option('--limit <n>', 'Limit number of skills') 285 .option('--cursor <id>', 'Pagination cursor') 286 .option('--json', 'Output as JSON') 287 .option('--explore-url <url>', 'Explore API base URL') 288 .action(async (opts, command) => { 289 opts = mergeExploreOpts(opts, command); 290 const baseUrl = resolveUrl(opts); 291 292 try { 293 const url = new URL('/api/skills', baseUrl); 294 if (opts.tag) url.searchParams.set('tag', opts.tag); 295 if (opts.limit) url.searchParams.set('limit', opts.limit); 296 if (opts.cursor) url.searchParams.set('cursor', opts.cursor); 297 298 const data = await fetchExploreJson(url); 299 300 if (opts.json) { 301 jsonOk({ skills: data.skills, cursor: data.cursor }); 302 return; 303 } 304 305 console.log(`${brand} explore skills`); 306 if (!data.skills?.length) { 307 console.log('no skills found.'); 308 console.log('the network is just getting started. ship a cap or skill to be one of the first.'); 309 return; 310 } 311 312 for (const skill of data.skills) { 313 console.log(` ${skill.name} v${skill.version} (${skill.ref})`); 314 console.log(` @${skill.handle} ${skill.description}`); 315 } 316 if (data.cursor) { 317 console.log(`\nnext: --cursor ${data.cursor}`); 318 } 319 } catch (err) { 320 if (opts.json) { 321 jsonError(err); 322 return; 323 } 324 console.error(formatError(err, { verbose: false })); 325 process.exitCode = 1; 326 } 327 }); 328 329 explore 330 .command('beacons') 331 .description('List active beacons from the explore index') 332 .option('--json', 'Output as JSON') 333 .option('--explore-url <url>', 'Explore API base URL') 334 .action(async (opts, command) => { 335 opts = mergeExploreOpts(opts, command); 336 const baseUrl = resolveUrl(opts); 337 338 try { 339 const url = new URL('/api/beacons', baseUrl); 340 const data = await fetchExploreJson(url); 341 342 if (opts.json) { 343 jsonOk({ beacons: data.beacons }); 344 return; 345 } 346 347 console.log(`${brand} explore beacons`); 348 if (!data.beacons?.length) { 349 console.log('no beacons found.'); 350 console.log('the network is just getting started. ship a cap or skill to be one of the first.'); 351 return; 352 } 353 354 for (const beacon of data.beacons) { 355 console.log(` ${beacon.name}`); 356 console.log(` caps: ${beacon.cap_count} vouches: ${beacon.vouch_count} last active: ${beacon.last_activity}`); 357 } 358 } catch (err) { 359 if (opts.json) { 360 jsonError(err); 361 return; 362 } 363 console.error(formatError(err, { verbose: false })); 364 process.exitCode = 1; 365 } 366 }); 367 368 explore 369 .command('vouches') 370 .description('List vouches for a cap from the explore index') 371 .option('--cap <uri>', 'Cap URI') 372 .option('--ref <ref>', 'Cap ref') 373 .option('--beacon <beacon>', 'Filter ref lookup by beacon') 374 .option('--json', 'Output as JSON') 375 .option('--explore-url <url>', 'Explore API base URL') 376 .action(async (opts, command) => { 377 opts = mergeExploreOpts(opts, command); 378 const baseUrl = resolveUrl(opts); 379 380 try { 381 if ((!opts.cap && !opts.ref) || (opts.cap && opts.ref)) { 382 const msg = 'provide --cap <uri> or --ref <ref>'; 383 if (opts.json) { 384 jsonError(msg); 385 return; 386 } 387 console.error(msg); 388 process.exitCode = 1; 389 return; 390 } 391 392 let capUri = opts.cap; 393 if (opts.ref) { 394 const capsUrl = new URL('/api/caps', baseUrl); 395 if (opts.beacon) capsUrl.searchParams.set('beacon', opts.beacon); 396 397 const capsData = await fetchExploreJson(capsUrl); 398 const match = capsData.caps?.find((cap) => cap.ref === opts.ref); 399 400 if (!match) { 401 const msg = `no cap found with ref '${opts.ref}'`; 402 if (opts.json) { 403 jsonError(msg); 404 return; 405 } 406 console.error(msg); 407 process.exitCode = 1; 408 return; 409 } 410 411 capUri = match.uri; 412 } 413 414 const url = new URL('/api/vouches', baseUrl); 415 url.searchParams.set('cap_uri', capUri); 416 417 const data = await fetchExploreJson(url); 418 419 if (opts.json) { 420 jsonOk({ vouches: data.vouches, cap_uri: capUri }); 421 return; 422 } 423 424 console.log(`${brand} explore vouches`); 425 if (!data.vouches?.length) { 426 console.log('no vouches found for this cap.'); 427 return; 428 } 429 430 for (const vouch of data.vouches) { 431 const who = vouch.handle ? `@${vouch.handle}` : (vouch.did || 'unknown'); 432 const createdAt = vouch.created_at || vouch.createdAt || 'unknown'; 433 const ref = vouch.ref || vouch.cap_ref || vouch.cap_uri || ''; 434 console.log(` ${who} ${createdAt}`); 435 if (ref) console.log(` ${ref}`); 436 } 437 } catch (err) { 438 if (opts.json) { 439 jsonError(err); 440 return; 441 } 442 console.error(formatError(err, { verbose: false })); 443 process.exitCode = 1; 444 } 445 }); 446 447 explore 448 .command('stats', { isDefault: true }) 449 .description('Show network-wide stats from the explore index') 450 .option('--json', 'Output as JSON') 451 .option('--explore-url <url>', 'Explore API base URL') 452 .action(async (opts, command) => { 453 opts = mergeExploreOpts(opts, command); 454 await fetchAndShowStats(opts); 455 }); 456}