open source is social v-it.org
1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 sol pbc
3
4import { DEFAULT_EXPLORE_URL } from '../lib/constants.js';
5import { readBeaconSet } 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 return `${days}d ago`;
19}
20
21function resolveUrl(opts) {
22 return opts.exploreUrl || process.env.VIT_EXPLORE_URL || DEFAULT_EXPLORE_URL;
23}
24
25function unavailableMessage(baseUrl) {
26 try {
27 return `${new URL(baseUrl).host} is unavailable. try 'vit explore caps --beacon .' for network-wide discovery.`;
28 } catch {
29 return `${baseUrl} is unavailable. try 'vit explore caps --beacon .' for network-wide discovery.`;
30 }
31}
32
33export default function register(program) {
34 program
35 .command('inbox')
36 .description('Show caps addressed to your project beacon (project-centric view)')
37 .option('--kind <kind>', 'Filter by cap kind (e.g. request)')
38 .option('--sort <sort>', 'Sort order: recent (default) or want-vouches', 'recent')
39 .option('--limit <n>', 'Limit number of caps')
40 .option('--json', 'Output as JSON')
41 .option('--explore-url <url>', 'Explore API base URL')
42 .action(async (opts) => {
43 try {
44 const beaconSet = readBeaconSet();
45 if (beaconSet.size === 0) {
46 const msg = "no beacon set — run 'vit init' first (inbox requires a project beacon)";
47 if (opts.json) {
48 jsonError(msg);
49 return;
50 }
51 console.error(msg);
52 process.exitCode = 1;
53 return;
54 }
55
56 const beacon = [...beaconSet].join(',');
57 const baseUrl = resolveUrl(opts);
58
59 let data;
60 try {
61 const url = new URL('/api/caps', baseUrl);
62 url.searchParams.set('beacon', beacon);
63 if (opts.kind) url.searchParams.set('kind', opts.kind);
64 if (opts.sort === 'want-vouches') url.searchParams.set('sort', 'want-vouches');
65 if (opts.limit) url.searchParams.set('limit', opts.limit);
66
67 const res = await fetch(url);
68 if (!res.ok) throw new Error(`explore API returned ${res.status}`);
69 data = await res.json();
70 } catch (err) {
71 const msg = errorMessage(err);
72 const finalMsg = msg.startsWith('explore API returned ')
73 ? msg
74 : unavailableMessage(baseUrl);
75 if (opts.json) {
76 jsonError(finalMsg);
77 return;
78 }
79 console.error(finalMsg);
80 console.error("fallback: try 'vit explore caps --beacon .' to query the explore index directly.");
81 process.exitCode = 1;
82 return;
83 }
84
85 if (opts.json) {
86 jsonOk({ caps: data.caps || [], cursor: data.cursor || null });
87 return;
88 }
89
90 const caps = data.caps || [];
91 const beaconDisplay = [...beaconSet][0];
92
93 console.log(`${brand} inbox — ${beaconDisplay}`);
94 console.log('');
95
96 if (caps.length === 0) {
97 const kindFilter = opts.kind ? ` (kind: ${opts.kind})` : '';
98 console.log(`no caps found${kindFilter}.`);
99 if (opts.kind) {
100 console.log(`hint: to request a feature, run 'vit ship --kind request --beacon <url>'`);
101 }
102 return;
103 }
104
105 for (const cap of caps) {
106 const wantCount = cap.want_vouch_count ?? 0;
107 const wantStr = wantCount === 1 ? '1 want' : `${wantCount} wants`;
108 const age = cap.created_at ? timeAgo(cap.created_at) : '';
109 const handle = cap.handle ? `@${cap.handle}` : cap.did || 'unknown';
110 const kind = cap.kind || (cap.record_json ? (() => { try { return JSON.parse(cap.record_json).kind || ''; } catch { return ''; } })() : '');
111
112 console.log(` ${cap.ref} ${handle} ${wantStr} ${age}`);
113 if (cap.title) console.log(` ${cap.title}${kind ? ` [${kind}]` : ''}`);
114 if (cap.description) console.log(` ${cap.description}`);
115 console.log('');
116 }
117
118 const kindNote = opts.kind ? ` ${opts.kind}` : '';
119 console.log(`${caps.length} open${kindNote} cap${caps.length === 1 ? '' : 's'}`);
120 console.log(`tip: 'vit vouch <ref> --kind want' to signal demand`);
121 console.log(` 'vit ship --recap <ref>' to ship an implementation`);
122 } catch (err) {
123 if (opts.json) {
124 jsonError(err);
125 return;
126 }
127 console.error(formatError(err, { verbose: false }));
128 process.exitCode = 1;
129 }
130 });
131}