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