open source is social v-it.org
0
fork

Configure Feed

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

Merge branch 'hopper-g7iktjl7-explore-detail-cmds'

# Conflicts:
# src/cmd/explore.js

+187 -123
+135 -123
src/cmd/explore.js
··· 33 33 } 34 34 } 35 35 36 - async function runStats(opts) { 36 + function mergeExploreOpts(opts, command) { 37 + return { 38 + ...(command?.parent?.opts?.() || {}), 39 + ...(command?.opts?.() || opts || {}), 40 + }; 41 + } 42 + 43 + async function fetchAndShowStats(opts) { 37 44 const baseUrl = resolveUrl(opts); 38 45 try { 39 46 const url = new URL('/api/stats', baseUrl); ··· 67 74 export default function register(program) { 68 75 const explore = program 69 76 .command('explore') 70 - .description('Query the explore index for caps, skills, beacons, vouches, and stats'); 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 + }); 71 83 72 84 explore 73 - .command('caps') 74 - .description('List recent caps from the explore index') 75 - .option('--beacon <beacon>', 'Filter by beacon') 76 - .option('--limit <n>', 'Limit number of caps') 77 - .option('--cursor <id>', 'Pagination cursor') 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') 78 89 .option('--json', 'Output as JSON') 79 90 .option('--explore-url <url>', 'Explore API base URL') 80 - .action(async (opts) => { 91 + .action(async (ref, opts, command) => { 92 + opts = mergeExploreOpts(opts, command); 81 93 const baseUrl = resolveUrl(opts); 82 94 83 95 try { 84 - let beacon = opts.beacon; 85 - if (beacon === '.') { 86 - const config = readProjectConfig(); 87 - const beacons = [config.beacon, config.secondaryBeacon].filter(Boolean); 88 - if (beacons.length === 0) { 89 - const msg = "no beacon set — run 'vit init' first"; 90 - if (opts.json) { 91 - jsonError(msg); 92 - return; 93 - } 94 - console.error(msg); 95 - process.exitCode = 1; 96 - return; 97 - } 98 - beacon = beacons.join(','); 99 - } 100 - 101 - const url = new URL('/api/caps', baseUrl); 102 - if (beacon) url.searchParams.set('beacon', beacon); 103 - if (opts.limit) url.searchParams.set('limit', opts.limit); 104 - if (opts.cursor) url.searchParams.set('cursor', opts.cursor); 96 + const url = new URL('/api/cap', baseUrl); 97 + url.searchParams.set('ref', ref); 98 + if (opts.beacon) url.searchParams.set('beacon', opts.beacon); 105 99 106 100 const res = await fetch(url); 107 101 if (!res.ok) throw new Error(`explore API returned ${res.status}`); 108 102 const data = await res.json(); 109 103 110 - if (opts.json) { 111 - jsonOk({ caps: data.caps, cursor: data.cursor }); 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 112 return; 113 113 } 114 114 115 - console.log(`${brand} explore caps`); 116 - if (!data.caps?.length) { 117 - console.log('no caps found.'); 115 + if (opts.json) { 116 + jsonOk(data.cap); 118 117 return; 119 118 } 120 119 121 - for (const cap of data.caps) { 122 - console.log(` ${cap.title} (${cap.ref})`); 123 - console.log(` @${cap.handle} ${cap.beacon}`); 124 - console.log(` ${cap.description}`); 125 - } 126 - if (data.cursor) { 127 - console.log(`\nnext: --cursor ${data.cursor}`); 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}`); 128 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`); 129 138 } catch (err) { 130 139 const msg = err instanceof Error ? err.message : String(err); 131 140 const finalMsg = msg.startsWith('explore API returned ') ··· 141 150 }); 142 151 143 152 explore 144 - .command('cap') 145 - .description('Show details for a single cap') 146 - .argument('<ref>', 'Cap ref slug') 147 - .option('--beacon <beacon>', 'Scope lookup to a specific beacon') 153 + .command('skill') 154 + .argument('<name>', 'Skill name to look up') 155 + .description('Show details for a single skill') 148 156 .option('--json', 'Output as JSON') 149 157 .option('--explore-url <url>', 'Explore API base URL') 150 - .action(async (ref, opts) => { 158 + .action(async (name, opts, command) => { 159 + opts = mergeExploreOpts(opts, command); 151 160 const baseUrl = resolveUrl(opts); 152 161 153 162 try { 154 - const url = new URL('/api/cap', baseUrl); 155 - url.searchParams.set('ref', ref); 156 - if (opts.beacon) url.searchParams.set('beacon', opts.beacon); 163 + const url = new URL('/api/skill', baseUrl); 164 + url.searchParams.set('name', name); 157 165 158 166 const res = await fetch(url); 159 167 if (!res.ok) throw new Error(`explore API returned ${res.status}`); 160 168 const data = await res.json(); 161 169 162 - if (!data.cap) { 163 - const msg = `no cap found with ref '${ref}'`; 170 + if (!data.skill) { 171 + const msg = `no skill found with name '${name}'`; 164 172 if (opts.json) { 165 173 jsonError(msg); 166 174 return; ··· 171 179 } 172 180 173 181 if (opts.json) { 174 - jsonOk(data); 182 + jsonOk(data.skill); 175 183 return; 176 184 } 177 185 178 - const cap = data.cap; 179 - let record = {}; 180 - try { record = JSON.parse(cap.record_json); } catch {} 181 - 182 - console.log(`${brand} explore cap`); 183 - const kindTag = record.kind ? ` [${record.kind}]` : ''; 184 - console.log(` ${cap.title}${kindTag}`); 185 - console.log(` ${cap.description}`); 186 - if (cap.beacon) console.log(` beacon: ${cap.beacon}`); 187 - console.log(` author: @${cap.handle}`); 188 - console.log(` ref: ${cap.ref}`); 189 - console.log(` shipped: ${timeAgo(cap.created_at)}`); 190 - if (record.text) { 191 - console.log(''); 192 - console.log(` ${record.text}`); 193 - } 194 - console.log(''); 195 - console.log(` vouches: ${cap.vouch_count}`); 196 - console.log(''); 197 - console.log(' commands:'); 198 - console.log(` vit vouch ${cap.ref}`); 199 - console.log(` vit vet ${cap.ref}`); 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`); 200 197 } catch (err) { 201 198 const msg = err instanceof Error ? err.message : String(err); 202 199 const finalMsg = msg.startsWith('explore API returned ') ··· 212 209 }); 213 210 214 211 explore 215 - .command('skills') 216 - .description('List published skills from the explore index') 217 - .option('--tag <tag>', 'Filter by tag') 218 - .option('--limit <n>', 'Limit number of skills') 212 + .command('caps') 213 + .description('List recent caps from the explore index') 214 + .option('--beacon <beacon>', 'Filter by beacon') 215 + .option('--limit <n>', 'Limit number of caps') 219 216 .option('--cursor <id>', 'Pagination cursor') 220 217 .option('--json', 'Output as JSON') 221 218 .option('--explore-url <url>', 'Explore API base URL') 222 - .action(async (opts) => { 219 + .action(async (opts, command) => { 220 + opts = mergeExploreOpts(opts, command); 223 221 const baseUrl = resolveUrl(opts); 224 222 225 223 try { 226 - const url = new URL('/api/skills', baseUrl); 227 - if (opts.tag) url.searchParams.set('tag', opts.tag); 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); 228 243 if (opts.limit) url.searchParams.set('limit', opts.limit); 229 244 if (opts.cursor) url.searchParams.set('cursor', opts.cursor); 230 245 ··· 233 248 const data = await res.json(); 234 249 235 250 if (opts.json) { 236 - jsonOk({ skills: data.skills, cursor: data.cursor }); 251 + jsonOk({ caps: data.caps, cursor: data.cursor }); 237 252 return; 238 253 } 239 254 240 - console.log(`${brand} explore skills`); 241 - if (!data.skills?.length) { 242 - console.log('no skills found.'); 255 + console.log(`${brand} explore caps`); 256 + if (!data.caps?.length) { 257 + console.log('no caps found.'); 243 258 return; 244 259 } 245 260 246 - for (const skill of data.skills) { 247 - console.log(` ${skill.name} v${skill.version} (${skill.ref})`); 248 - console.log(` @${skill.handle} ${skill.description}`); 261 + for (const cap of data.caps) { 262 + console.log(` ${cap.title} (${cap.ref})`); 263 + console.log(` @${cap.handle} ${cap.beacon}`); 264 + console.log(` ${cap.description}`); 249 265 } 250 266 if (data.cursor) { 251 267 console.log(`\nnext: --cursor ${data.cursor}`); ··· 265 281 }); 266 282 267 283 explore 268 - .command('skill') 269 - .description('Show details for a single skill') 270 - .argument('<name>', 'Skill name') 284 + .command('skills') 285 + .description('List published skills from the explore index') 286 + .option('--tag <tag>', 'Filter by tag') 287 + .option('--limit <n>', 'Limit number of skills') 288 + .option('--cursor <id>', 'Pagination cursor') 271 289 .option('--json', 'Output as JSON') 272 290 .option('--explore-url <url>', 'Explore API base URL') 273 - .action(async (name, opts) => { 291 + .action(async (opts, command) => { 292 + opts = mergeExploreOpts(opts, command); 274 293 const baseUrl = resolveUrl(opts); 275 294 276 295 try { 277 - const url = new URL('/api/skill', baseUrl); 278 - url.searchParams.set('name', name); 296 + const url = new URL('/api/skills', baseUrl); 297 + if (opts.tag) url.searchParams.set('tag', opts.tag); 298 + if (opts.limit) url.searchParams.set('limit', opts.limit); 299 + if (opts.cursor) url.searchParams.set('cursor', opts.cursor); 279 300 280 301 const res = await fetch(url); 281 302 if (!res.ok) throw new Error(`explore API returned ${res.status}`); 282 303 const data = await res.json(); 283 304 284 - if (!data.skill) { 285 - const msg = `no skill found with name '${name}'`; 286 - if (opts.json) { 287 - jsonError(msg); 288 - return; 289 - } 290 - console.error(msg); 291 - process.exitCode = 1; 305 + if (opts.json) { 306 + jsonOk({ skills: data.skills, cursor: data.cursor }); 292 307 return; 293 308 } 294 309 295 - if (opts.json) { 296 - jsonOk(data); 310 + console.log(`${brand} explore skills`); 311 + if (!data.skills?.length) { 312 + console.log('no skills found.'); 297 313 return; 298 314 } 299 315 300 - const skill = data.skill; 301 - let record = {}; 302 - try { record = JSON.parse(skill.record_json); } catch {} 303 - 304 - console.log(`${brand} explore skill`); 305 - console.log(` ${skill.name} v${skill.version}`); 306 - console.log(` ${skill.description}`); 307 - console.log(` author: @${skill.handle}`); 308 - if (record.license) console.log(` license: ${record.license}`); 309 - if (skill.tags) console.log(` tags: ${skill.tags}`); 310 - console.log(''); 311 - console.log(` vouches: ${skill.vouch_count}`); 312 - console.log(''); 313 - console.log(' commands:'); 314 - console.log(` vit learn skill-${skill.name}`); 315 - console.log(` vit adopt <beacon> skill-${skill.name}`); 316 + for (const skill of data.skills) { 317 + console.log(` ${skill.name} v${skill.version} (${skill.ref})`); 318 + console.log(` @${skill.handle} ${skill.description}`); 319 + } 320 + if (data.cursor) { 321 + console.log(`\nnext: --cursor ${data.cursor}`); 322 + } 316 323 } catch (err) { 317 324 const msg = err instanceof Error ? err.message : String(err); 318 325 const finalMsg = msg.startsWith('explore API returned ') ··· 332 339 .description('List active beacons from the explore index') 333 340 .option('--json', 'Output as JSON') 334 341 .option('--explore-url <url>', 'Explore API base URL') 335 - .action(async (opts) => { 342 + .action(async (opts, command) => { 343 + opts = mergeExploreOpts(opts, command); 336 344 const baseUrl = resolveUrl(opts); 337 345 338 346 try { ··· 378 386 .option('--beacon <beacon>', 'Filter ref lookup by beacon') 379 387 .option('--json', 'Output as JSON') 380 388 .option('--explore-url <url>', 'Explore API base URL') 381 - .action(async (opts) => { 389 + .action(async (opts, command) => { 390 + opts = mergeExploreOpts(opts, command); 382 391 const baseUrl = resolveUrl(opts); 383 392 384 393 try { ··· 461 470 .description('Show network-wide stats from the explore index') 462 471 .option('--json', 'Output as JSON') 463 472 .option('--explore-url <url>', 'Explore API base URL') 464 - .action(async (opts) => runStats(opts)); 473 + .action(async (opts, command) => { 474 + opts = mergeExploreOpts(opts, command); 475 + await fetchAndShowStats(opts); 476 + }); 465 477 }
+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);