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';
9import { formatError } from '../lib/error-format.js';
10
11export default function register(program) {
12 program
13 .command('follow')
14 .argument('<handle>', 'Handle to follow (e.g. alice.bsky.social)')
15 .description('Add an account to this project\'s following list')
16 .option('-v, --verbose', 'Show step-by-step details')
17 .option('--json', 'Output as JSON')
18 .option('--did <did>', 'DID to use for handle resolution')
19 .action(async (handle, opts) => {
20 try {
21 const { verbose } = opts;
22 const vlog = opts.json ? (...a) => console.error(...a) : console.log;
23 handle = handle.replace(/^@/, '');
24
25 if (opts.json && !(opts.did || loadConfig().did)) {
26 jsonError('no DID configured', "run 'vit login <handle>' first");
27 return;
28 }
29 const did = requireDid(opts);
30 if (!did) return;
31 if (verbose) vlog(`[verbose] DID: ${did}`);
32
33 const list = readFollowing();
34 if (list.some(e => e.handle === handle)) {
35 if (opts.json) {
36 jsonError(`already following ${handle}`);
37 return;
38 }
39 console.error(`already following ${handle}`);
40 process.exitCode = 1;
41 return;
42 }
43
44 const { agent } = await restoreAgent(did);
45 if (verbose) vlog('[verbose] session restored');
46
47 const resolved = await agent.resolveHandle({ handle });
48 const targetDid = resolved.data.did;
49 if (verbose) vlog(`[verbose] resolved ${handle} to ${targetDid}`);
50
51 list.push({ handle, did: targetDid, followedAt: new Date().toISOString() });
52 writeFollowing(list);
53 if (opts.json) {
54 jsonOk({ handle, did: targetDid, followedAt: list[list.length - 1].followedAt });
55 return;
56 }
57 console.log(`${mark} following ${handle} (${targetDid})`);
58 } catch (err) {
59 if (opts.json) {
60 jsonError(err);
61 return;
62 }
63 console.error(formatError(err, { verbose: opts.verbose }));
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 if (opts.json) {
101 jsonError(err);
102 return;
103 }
104 console.error(formatError(err, { verbose: opts.verbose }));
105 process.exitCode = 1;
106 }
107 });
108
109 program
110 .command('following')
111 .description('List accounts in this project\'s following list')
112 .option('-v, --verbose', 'Show step-by-step details')
113 .option('--json', 'Output as JSON')
114 .action(async (opts) => {
115 try {
116 const list = readFollowing();
117 if (list.length === 0) {
118 if (opts.json) {
119 jsonOk({ following: [], hint: "run 'vit follow <handle>' to add accounts" });
120 return;
121 }
122 console.log('not following anyone yet.');
123 console.log('');
124 console.log("run 'vit follow <handle>' to start seeing what people are shipping.");
125 console.log("try 'vit scan' to discover active publishers on the network.");
126 return;
127 }
128 if (opts.json) {
129 jsonOk({ following: list });
130 return;
131 }
132 for (const e of list) {
133 console.log(`${e.handle} (${e.did})`);
134 }
135 } catch (err) {
136 if (opts.json) {
137 jsonError(err);
138 return;
139 }
140 console.error(formatError(err, { verbose: opts.verbose }));
141 process.exitCode = 1;
142 }
143 });
144}