open source is social v-it.org
1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 sol pbc
3
4import { requireDid, loadConfig } from '../lib/config.js';
5import { restoreAgent } from '../lib/oauth.js';
6import { readFollowing, writeFollowing } from '../lib/vit-dir.js';
7import { mark } from '../lib/brand.js';
8import { jsonOk, jsonError } from '../lib/json-output.js';
9
10export default function register(program) {
11 program
12 .command('follow')
13 .argument('<handle>', 'Handle to follow (e.g. alice.bsky.social)')
14 .description('Add an account to this project\'s following list')
15 .option('-v, --verbose', 'Show step-by-step details')
16 .option('--json', 'Output as JSON')
17 .option('--did <did>', 'DID to use for handle resolution')
18 .action(async (handle, opts) => {
19 try {
20 const { verbose } = opts;
21 const vlog = opts.json ? (...a) => console.error(...a) : console.log;
22 handle = handle.replace(/^@/, '');
23
24 if (opts.json && !(opts.did || loadConfig().did)) {
25 jsonError('no DID configured', "run 'vit login <handle>' first");
26 return;
27 }
28 const did = requireDid(opts);
29 if (!did) return;
30 if (verbose) vlog(`[verbose] DID: ${did}`);
31
32 const list = readFollowing();
33 if (list.some(e => e.handle === handle)) {
34 if (opts.json) {
35 jsonError(`already following ${handle}`);
36 return;
37 }
38 console.error(`already following ${handle}`);
39 process.exitCode = 1;
40 return;
41 }
42
43 const { agent } = await restoreAgent(did);
44 if (verbose) vlog('[verbose] session restored');
45
46 const resolved = await agent.resolveHandle({ handle });
47 const targetDid = resolved.data.did;
48 if (verbose) vlog(`[verbose] resolved ${handle} to ${targetDid}`);
49
50 list.push({ handle, did: targetDid, followedAt: new Date().toISOString() });
51 writeFollowing(list);
52 if (opts.json) {
53 jsonOk({ handle, did: targetDid, followedAt: list[list.length - 1].followedAt });
54 return;
55 }
56 console.log(`${mark} following ${handle} (${targetDid})`);
57 } catch (err) {
58 const msg = err instanceof Error ? err.message : String(err);
59 if (opts.json) {
60 jsonError(msg);
61 return;
62 }
63 console.error(msg);
64 process.exitCode = 1;
65 }
66 });
67
68 program
69 .command('unfollow')
70 .argument('<handle>', 'Handle to unfollow (e.g. alice.bsky.social)')
71 .description('Remove an account from this project\'s following list')
72 .option('-v, --verbose', 'Show step-by-step details')
73 .option('--json', 'Output as JSON')
74 .action(async (handle, opts) => {
75 try {
76 const { verbose } = opts;
77 const vlog = opts.json ? (...a) => console.error(...a) : console.log;
78 handle = handle.replace(/^@/, '');
79
80 const list = readFollowing();
81 const filtered = list.filter(e => e.handle !== handle);
82 if (filtered.length === list.length) {
83 if (opts.json) {
84 jsonError(`not following ${handle}`);
85 return;
86 }
87 console.error(`not following ${handle}`);
88 process.exitCode = 1;
89 return;
90 }
91
92 writeFollowing(filtered);
93 if (verbose) vlog(`[verbose] removed ${handle} from following list`);
94 if (opts.json) {
95 jsonOk({ handle });
96 return;
97 }
98 console.log(`${mark} unfollowed ${handle}`);
99 } catch (err) {
100 const msg = err instanceof Error ? err.message : String(err);
101 if (opts.json) {
102 jsonError(msg);
103 return;
104 }
105 console.error(msg);
106 process.exitCode = 1;
107 }
108 });
109
110 program
111 .command('following')
112 .description('List accounts in this project\'s following list')
113 .option('-v, --verbose', 'Show step-by-step details')
114 .option('--json', 'Output as JSON')
115 .action(async (opts) => {
116 try {
117 const list = readFollowing();
118 if (list.length === 0) {
119 if (opts.json) {
120 jsonOk({ following: [], hint: "run 'vit follow <handle>' to add accounts" });
121 return;
122 }
123 console.log('not following anyone yet.');
124 console.log('');
125 console.log("run 'vit follow <handle>' to start seeing what people are shipping.");
126 console.log("try 'vit scan' to discover active publishers on the network.");
127 return;
128 }
129 if (opts.json) {
130 jsonOk({ following: list });
131 return;
132 }
133 for (const e of list) {
134 console.log(`${e.handle} (${e.did})`);
135 }
136 } catch (err) {
137 const msg = err instanceof Error ? err.message : String(err);
138 if (opts.json) {
139 jsonError(msg);
140 return;
141 }
142 console.error(msg);
143 process.exitCode = 1;
144 }
145 });
146}