open source is social v-it.org
0
fork

Configure Feed

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

feat(explore): add cap and skill detail commands, bare explore stats

Files changed: src/cmd/explore.js, test/explore.test.js. No new files, no new dependencies. make test already verified (302 pass, 0 fail).

+248 -35
+196 -35
src/cmd/explore.js
··· 18 18 } 19 19 } 20 20 21 + function timeAgo(iso) { 22 + const seconds = Math.floor((Date.now() - new Date(iso).getTime()) / 1000); 23 + if (seconds < 60) return 'just now'; 24 + const minutes = Math.floor(seconds / 60); 25 + if (minutes < 60) return `${minutes} minute${minutes !== 1 ? 's' : ''} ago`; 26 + const hours = Math.floor(minutes / 60); 27 + if (hours < 24) return `${hours} hour${hours !== 1 ? 's' : ''} ago`; 28 + const days = Math.floor(hours / 24); 29 + if (days < 30) return `${days} day${days !== 1 ? 's' : ''} ago`; 30 + const months = Math.floor(days / 30); 31 + return `${months} month${months !== 1 ? 's' : ''} ago`; 32 + } 33 + 34 + function mergeExploreOpts(opts, command) { 35 + return { 36 + ...(command?.parent?.opts?.() || {}), 37 + ...(command?.opts?.() || opts || {}), 38 + }; 39 + } 40 + 41 + async function fetchAndShowStats(opts) { 42 + const baseUrl = resolveUrl(opts); 43 + 44 + try { 45 + const url = new URL('/api/stats', baseUrl); 46 + const res = await fetch(url); 47 + if (!res.ok) throw new Error(`explore API returned ${res.status}`); 48 + const data = await res.json(); 49 + 50 + if (opts.json) { 51 + jsonOk(data); 52 + return; 53 + } 54 + 55 + console.log(`${brand} explore stats`); 56 + console.log(` caps: ${data.total_caps} skills: ${data.total_skills}`); 57 + console.log(` vouches: ${data.total_vouches} beacons: ${data.total_beacons}`); 58 + console.log(` active dids: ${data.active_dids} skill publishers: ${data.skill_publishers}`); 59 + } catch (err) { 60 + const msg = err instanceof Error ? err.message : String(err); 61 + const finalMsg = msg.startsWith('explore API returned ') 62 + ? msg 63 + : unavailableMessage(baseUrl); 64 + if (opts.json) { 65 + jsonError(finalMsg); 66 + return; 67 + } 68 + console.error(finalMsg); 69 + process.exitCode = 1; 70 + } 71 + } 72 + 21 73 export default function register(program) { 22 74 const explore = program 23 75 .command('explore') 24 - .description('Query the explore index for caps, skills, beacons, vouches, and stats'); 76 + .description('Query the explore index for caps, skills, beacons, vouches, and stats') 77 + .option('--json', 'Output as JSON') 78 + .option('--explore-url <url>', 'Explore API base URL') 79 + .action(async (opts) => { 80 + await fetchAndShowStats(opts); 81 + }); 82 + 83 + explore 84 + .command('cap') 85 + .argument('<ref>', 'Cap ref to look up') 86 + .description('Show details for a single cap') 87 + .option('--beacon <beacon>', 'Scope lookup to a beacon') 88 + .option('--json', 'Output as JSON') 89 + .option('--explore-url <url>', 'Explore API base URL') 90 + .action(async (ref, opts, command) => { 91 + opts = mergeExploreOpts(opts, command); 92 + const baseUrl = resolveUrl(opts); 93 + 94 + try { 95 + const url = new URL('/api/cap', baseUrl); 96 + url.searchParams.set('ref', ref); 97 + if (opts.beacon) url.searchParams.set('beacon', opts.beacon); 98 + 99 + const res = await fetch(url); 100 + if (!res.ok) throw new Error(`explore API returned ${res.status}`); 101 + const data = await res.json(); 102 + 103 + if (!data.cap) { 104 + const msg = `no cap found with ref '${ref}'`; 105 + if (opts.json) { 106 + jsonError(msg); 107 + return; 108 + } 109 + console.error(msg); 110 + process.exitCode = 1; 111 + return; 112 + } 113 + 114 + if (opts.json) { 115 + jsonOk(data.cap); 116 + return; 117 + } 118 + 119 + const record = JSON.parse(data.cap.record_json); 120 + console.log(`${brand} explore cap`); 121 + console.log(` ${data.cap.title} [${record.kind}]`); 122 + console.log(` ${data.cap.description}`); 123 + console.log(); 124 + console.log(` beacon: ${data.cap.beacon}`); 125 + console.log(` author: @${data.cap.handle}`); 126 + console.log(` ref: ${data.cap.ref}`); 127 + console.log(` posted: ${timeAgo(data.cap.created_at)}`); 128 + if (record.text) { 129 + console.log(); 130 + console.log(` ${record.text}`); 131 + } 132 + console.log(); 133 + console.log(` vouches: ${data.cap.vouch_count}`); 134 + console.log(); 135 + console.log(` vit vet ${data.cap.ref} - inspect before adopting`); 136 + console.log(` vit remix ${data.cap.ref} - remix this cap`); 137 + } catch (err) { 138 + const msg = err instanceof Error ? err.message : String(err); 139 + const finalMsg = msg.startsWith('explore API returned ') 140 + ? msg 141 + : unavailableMessage(baseUrl); 142 + if (opts.json) { 143 + jsonError(finalMsg); 144 + return; 145 + } 146 + console.error(finalMsg); 147 + process.exitCode = 1; 148 + } 149 + }); 150 + 151 + explore 152 + .command('skill') 153 + .argument('<name>', 'Skill name to look up') 154 + .description('Show details for a single skill') 155 + .option('--json', 'Output as JSON') 156 + .option('--explore-url <url>', 'Explore API base URL') 157 + .action(async (name, opts, command) => { 158 + opts = mergeExploreOpts(opts, command); 159 + const baseUrl = resolveUrl(opts); 160 + 161 + try { 162 + const url = new URL('/api/skill', baseUrl); 163 + url.searchParams.set('name', name); 164 + 165 + const res = await fetch(url); 166 + if (!res.ok) throw new Error(`explore API returned ${res.status}`); 167 + const data = await res.json(); 168 + 169 + if (!data.skill) { 170 + const msg = `no skill found with name '${name}'`; 171 + if (opts.json) { 172 + jsonError(msg); 173 + return; 174 + } 175 + console.error(msg); 176 + process.exitCode = 1; 177 + return; 178 + } 179 + 180 + if (opts.json) { 181 + jsonOk(data.skill); 182 + return; 183 + } 184 + 185 + const record = JSON.parse(data.skill.record_json); 186 + console.log(`${brand} explore skill`); 187 + console.log(` /${data.skill.name} v${data.skill.version}`); 188 + console.log(` ${data.skill.description}`); 189 + console.log(); 190 + console.log(` author: @${data.skill.handle}`); 191 + if (record.license) console.log(` license: ${record.license}`); 192 + if (data.skill.tags) console.log(` tags: ${data.skill.tags}`); 193 + console.log(` vouches: ${data.skill.vouch_count}`); 194 + console.log(); 195 + console.log(` vit learn skill-${data.skill.name} - install this skill`); 196 + } catch (err) { 197 + const msg = err instanceof Error ? err.message : String(err); 198 + const finalMsg = msg.startsWith('explore API returned ') 199 + ? msg 200 + : unavailableMessage(baseUrl); 201 + if (opts.json) { 202 + jsonError(finalMsg); 203 + return; 204 + } 205 + console.error(finalMsg); 206 + process.exitCode = 1; 207 + } 208 + }); 25 209 26 210 explore 27 211 .command('caps') ··· 31 215 .option('--cursor <id>', 'Pagination cursor') 32 216 .option('--json', 'Output as JSON') 33 217 .option('--explore-url <url>', 'Explore API base URL') 34 - .action(async (opts) => { 218 + .action(async (opts, command) => { 219 + opts = mergeExploreOpts(opts, command); 35 220 const baseUrl = resolveUrl(opts); 36 221 37 222 try { ··· 100 285 .option('--cursor <id>', 'Pagination cursor') 101 286 .option('--json', 'Output as JSON') 102 287 .option('--explore-url <url>', 'Explore API base URL') 103 - .action(async (opts) => { 288 + .action(async (opts, command) => { 289 + opts = mergeExploreOpts(opts, command); 104 290 const baseUrl = resolveUrl(opts); 105 291 106 292 try { ··· 150 336 .description('List active beacons from the explore index') 151 337 .option('--json', 'Output as JSON') 152 338 .option('--explore-url <url>', 'Explore API base URL') 153 - .action(async (opts) => { 339 + .action(async (opts, command) => { 340 + opts = mergeExploreOpts(opts, command); 154 341 const baseUrl = resolveUrl(opts); 155 342 156 343 try { ··· 196 383 .option('--beacon <beacon>', 'Filter ref lookup by beacon') 197 384 .option('--json', 'Output as JSON') 198 385 .option('--explore-url <url>', 'Explore API base URL') 199 - .action(async (opts) => { 386 + .action(async (opts, command) => { 387 + opts = mergeExploreOpts(opts, command); 200 388 const baseUrl = resolveUrl(opts); 201 389 202 390 try { ··· 279 467 .description('Show network-wide stats from the explore index') 280 468 .option('--json', 'Output as JSON') 281 469 .option('--explore-url <url>', 'Explore API base URL') 282 - .action(async (opts) => { 283 - const baseUrl = resolveUrl(opts); 284 - 285 - try { 286 - const url = new URL('/api/stats', baseUrl); 287 - const res = await fetch(url); 288 - if (!res.ok) throw new Error(`explore API returned ${res.status}`); 289 - const data = await res.json(); 290 - 291 - if (opts.json) { 292 - jsonOk(data); 293 - return; 294 - } 295 - 296 - console.log(`${brand} explore stats`); 297 - console.log(` caps: ${data.total_caps} skills: ${data.total_skills}`); 298 - console.log(` vouches: ${data.total_vouches} beacons: ${data.total_beacons}`); 299 - console.log(` active dids: ${data.active_dids} skill publishers: ${data.skill_publishers}`); 300 - } catch (err) { 301 - const msg = err instanceof Error ? err.message : String(err); 302 - const finalMsg = msg.startsWith('explore API returned ') 303 - ? msg 304 - : unavailableMessage(baseUrl); 305 - if (opts.json) { 306 - jsonError(finalMsg); 307 - return; 308 - } 309 - console.error(finalMsg); 310 - process.exitCode = 1; 311 - } 470 + .action(async (opts, command) => { 471 + opts = mergeExploreOpts(opts, command); 472 + await fetchAndShowStats(opts); 312 473 }); 313 474 }
+52
test/explore.test.js
··· 19 19 expect(typeof data.total_caps).toBe('number'); 20 20 }); 21 21 22 + test('cap detail returns JSON', () => { 23 + const result = run('explore cap network-content-seeding --json', '/tmp'); 24 + expect(result.exitCode).toBe(0); 25 + const data = JSON.parse(result.stdout); 26 + expect(data.ok).toBe(true); 27 + expect(data.title).toBe('Network Content Seeding'); 28 + expect(data.ref).toBe('network-content-seeding'); 29 + }); 30 + 31 + test('cap detail not found', () => { 32 + const result = run('explore cap nonexistent --json', '/tmp'); 33 + expect(result.exitCode).not.toBe(0); 34 + const data = JSON.parse(result.stdout); 35 + expect(data.ok).toBe(false); 36 + expect(data.error).toContain('no cap found'); 37 + }); 38 + 39 + test('cap detail with beacon', () => { 40 + const result = run( 41 + 'explore cap network-content-seeding --beacon vit:github.com/solpbc/vit --json', 42 + '/tmp', 43 + ); 44 + expect(result.exitCode).toBe(0); 45 + const data = JSON.parse(result.stdout); 46 + expect(data.ok).toBe(true); 47 + expect(data.beacon).toBe('vit:github.com/solpbc/vit'); 48 + }); 49 + 50 + test('skill detail returns JSON', () => { 51 + const result = run('explore skill atproto-records --json', '/tmp'); 52 + expect(result.exitCode).toBe(0); 53 + const data = JSON.parse(result.stdout); 54 + expect(data.ok).toBe(true); 55 + expect(data.name).toBe('atproto-records'); 56 + }); 57 + 58 + test('skill detail not found', () => { 59 + const result = run('explore skill nonexistent --json', '/tmp'); 60 + expect(result.exitCode).not.toBe(0); 61 + const data = JSON.parse(result.stdout); 62 + expect(data.ok).toBe(false); 63 + expect(data.error).toContain('no skill found'); 64 + }); 65 + 66 + test('bare explore returns stats JSON', () => { 67 + const result = run('explore --json', '/tmp'); 68 + expect(result.exitCode).toBe(0); 69 + const data = JSON.parse(result.stdout); 70 + expect(data.ok).toBe(true); 71 + expect(typeof data.total_caps).toBe('number'); 72 + }); 73 + 22 74 test('caps returns JSON', () => { 23 75 const result = run('explore caps --json --limit 2', '/tmp'); 24 76 expect(result.exitCode).toBe(0);