@harveyrandall/bsky-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/README.md +229 -0
- package/dist/index.js +1620 -0
- package/dist/index.js.map +1 -0
- package/package.json +67 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1620 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/config.ts
|
|
7
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
8
|
+
import { homedir } from "os";
|
|
9
|
+
import { join, dirname } from "path";
|
|
10
|
+
import { existsSync } from "fs";
|
|
11
|
+
import { readdirSync } from "fs";
|
|
12
|
+
function configDir() {
|
|
13
|
+
return join(homedir(), ".config");
|
|
14
|
+
}
|
|
15
|
+
function bskyDir() {
|
|
16
|
+
return join(configDir(), "bsky");
|
|
17
|
+
}
|
|
18
|
+
function configPath(profile) {
|
|
19
|
+
if (profile) {
|
|
20
|
+
return join(bskyDir(), `config-${profile}.json`);
|
|
21
|
+
}
|
|
22
|
+
return join(bskyDir(), "config.json");
|
|
23
|
+
}
|
|
24
|
+
function authPath(handle, prefix = "") {
|
|
25
|
+
return join(bskyDir(), `${prefix}${handle}.auth`);
|
|
26
|
+
}
|
|
27
|
+
async function loadConfig(profile) {
|
|
28
|
+
if (profile === "?") {
|
|
29
|
+
listProfiles();
|
|
30
|
+
process.exit(0);
|
|
31
|
+
}
|
|
32
|
+
const fp = configPath(profile);
|
|
33
|
+
await mkdir(dirname(fp), { recursive: true });
|
|
34
|
+
let cfg = {
|
|
35
|
+
host: "https://bsky.social",
|
|
36
|
+
bgs: "https://bsky.network",
|
|
37
|
+
handle: "",
|
|
38
|
+
password: ""
|
|
39
|
+
};
|
|
40
|
+
try {
|
|
41
|
+
const data = await readFile(fp, "utf-8");
|
|
42
|
+
const fileCfg = JSON.parse(data);
|
|
43
|
+
cfg = { ...cfg, ...fileCfg };
|
|
44
|
+
} catch {
|
|
45
|
+
}
|
|
46
|
+
if (process.env.BSKY_HANDLE) cfg.handle = process.env.BSKY_HANDLE;
|
|
47
|
+
if (process.env.BSKY_PASSWORD) cfg.password = process.env.BSKY_PASSWORD;
|
|
48
|
+
if (process.env.BSKY_HOST) cfg.host = process.env.BSKY_HOST;
|
|
49
|
+
if (process.env.BSKY_BGS) cfg.bgs = process.env.BSKY_BGS;
|
|
50
|
+
if (!cfg.handle || !cfg.password) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
"No credentials found. Run 'bsky login' or set BSKY_HANDLE and BSKY_PASSWORD."
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return cfg;
|
|
56
|
+
}
|
|
57
|
+
async function saveConfig(config, profile) {
|
|
58
|
+
const fp = configPath(profile);
|
|
59
|
+
await mkdir(dirname(fp), { recursive: true });
|
|
60
|
+
await writeFile(fp, JSON.stringify(config, null, " ") + "\n", {
|
|
61
|
+
mode: 420
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function listProfiles() {
|
|
65
|
+
const dir = bskyDir();
|
|
66
|
+
if (!existsSync(dir)) return;
|
|
67
|
+
const files = readdirSync(dir);
|
|
68
|
+
for (const file of files) {
|
|
69
|
+
if (file.startsWith("config-") && file.endsWith(".json")) {
|
|
70
|
+
const name = file.slice(7, -5);
|
|
71
|
+
console.log(name);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/client.ts
|
|
77
|
+
import { AtpAgent } from "@atproto/api";
|
|
78
|
+
|
|
79
|
+
// src/auth.ts
|
|
80
|
+
import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
81
|
+
import { createInterface } from "readline/promises";
|
|
82
|
+
import { createWriteStream } from "fs";
|
|
83
|
+
import { stdin, stdout, stderr } from "process";
|
|
84
|
+
async function readAuth(handle, prefix = "") {
|
|
85
|
+
try {
|
|
86
|
+
const fp = authPath(handle, prefix);
|
|
87
|
+
const data = await readFile2(fp, "utf-8");
|
|
88
|
+
return JSON.parse(data);
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async function writeAuth(auth, handle, prefix = "") {
|
|
94
|
+
const fp = authPath(handle, prefix);
|
|
95
|
+
await writeFile2(fp, JSON.stringify(auth, null, " ") + "\n", {
|
|
96
|
+
mode: 384
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
async function promptPassword() {
|
|
100
|
+
if (stdin.isTTY) {
|
|
101
|
+
stderr.write("Password: ");
|
|
102
|
+
const muted = createWriteStream("/dev/null");
|
|
103
|
+
const rl2 = createInterface({ input: stdin, output: muted });
|
|
104
|
+
const password2 = await rl2.question("");
|
|
105
|
+
rl2.close();
|
|
106
|
+
stderr.write("\n");
|
|
107
|
+
return password2.trim();
|
|
108
|
+
}
|
|
109
|
+
const rl = createInterface({ input: stdin });
|
|
110
|
+
const password = await rl.question("");
|
|
111
|
+
rl.close();
|
|
112
|
+
return password.trim();
|
|
113
|
+
}
|
|
114
|
+
async function prompt2FA() {
|
|
115
|
+
stderr.write(
|
|
116
|
+
"2FA is enabled. A sign-in code has been sent to your email.\nEnter the code: "
|
|
117
|
+
);
|
|
118
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
119
|
+
const code = await rl.question("");
|
|
120
|
+
rl.close();
|
|
121
|
+
return code.trim();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/client.ts
|
|
125
|
+
async function createClient(config, prefix = "") {
|
|
126
|
+
const agent = new AtpAgent({ service: config.host });
|
|
127
|
+
const auth = await readAuth(config.handle, prefix);
|
|
128
|
+
if (auth) {
|
|
129
|
+
try {
|
|
130
|
+
await agent.resumeSession({
|
|
131
|
+
did: auth.did,
|
|
132
|
+
handle: auth.handle,
|
|
133
|
+
accessJwt: auth.refreshJwt,
|
|
134
|
+
refreshJwt: auth.refreshJwt,
|
|
135
|
+
active: true
|
|
136
|
+
});
|
|
137
|
+
const refreshed = await agent.com.atproto.server.refreshSession(
|
|
138
|
+
void 0,
|
|
139
|
+
{
|
|
140
|
+
headers: {
|
|
141
|
+
authorization: `Bearer ${auth.refreshJwt}`
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
);
|
|
145
|
+
await agent.resumeSession({
|
|
146
|
+
did: refreshed.data.did,
|
|
147
|
+
handle: refreshed.data.handle,
|
|
148
|
+
accessJwt: refreshed.data.accessJwt,
|
|
149
|
+
refreshJwt: refreshed.data.refreshJwt,
|
|
150
|
+
active: true
|
|
151
|
+
});
|
|
152
|
+
await writeAuth(
|
|
153
|
+
{
|
|
154
|
+
did: refreshed.data.did,
|
|
155
|
+
handle: refreshed.data.handle,
|
|
156
|
+
accessJwt: refreshed.data.accessJwt,
|
|
157
|
+
refreshJwt: refreshed.data.refreshJwt
|
|
158
|
+
},
|
|
159
|
+
config.handle,
|
|
160
|
+
prefix
|
|
161
|
+
);
|
|
162
|
+
return agent;
|
|
163
|
+
} catch {
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
const loginResponse = await agent.login({
|
|
168
|
+
identifier: config.handle,
|
|
169
|
+
password: config.password
|
|
170
|
+
});
|
|
171
|
+
await writeAuth(
|
|
172
|
+
{
|
|
173
|
+
did: loginResponse.data.did,
|
|
174
|
+
handle: loginResponse.data.handle,
|
|
175
|
+
accessJwt: loginResponse.data.accessJwt,
|
|
176
|
+
refreshJwt: loginResponse.data.refreshJwt
|
|
177
|
+
},
|
|
178
|
+
config.handle,
|
|
179
|
+
prefix
|
|
180
|
+
);
|
|
181
|
+
return agent;
|
|
182
|
+
} catch (err) {
|
|
183
|
+
if (err instanceof Error && err.message.includes("AuthFactorTokenRequired")) {
|
|
184
|
+
const token = await prompt2FA();
|
|
185
|
+
const loginResponse = await agent.login({
|
|
186
|
+
identifier: config.handle,
|
|
187
|
+
password: config.password,
|
|
188
|
+
authFactorToken: token
|
|
189
|
+
});
|
|
190
|
+
await writeAuth(
|
|
191
|
+
{
|
|
192
|
+
did: loginResponse.data.did,
|
|
193
|
+
handle: loginResponse.data.handle,
|
|
194
|
+
accessJwt: loginResponse.data.accessJwt,
|
|
195
|
+
refreshJwt: loginResponse.data.refreshJwt
|
|
196
|
+
},
|
|
197
|
+
config.handle,
|
|
198
|
+
prefix
|
|
199
|
+
);
|
|
200
|
+
return agent;
|
|
201
|
+
}
|
|
202
|
+
throw new Error(`Cannot create session: ${err}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/commands/login.ts
|
|
207
|
+
function registerLogin(program2) {
|
|
208
|
+
program2.command("login").description("Login to Bluesky").argument("<handle>", "Your Bluesky handle").argument("[password]", "Your app password (prompts if omitted)").option("--host <url>", "PDS host URL", "https://bsky.social").option("--bgs <url>", "BGS host URL", "https://bsky.network").action(
|
|
209
|
+
async (handle, password, opts) => {
|
|
210
|
+
const resolvedPassword = password ?? process.env.BSKY_PASSWORD ?? await promptPassword();
|
|
211
|
+
const profile = program2.opts().profile;
|
|
212
|
+
const config = {
|
|
213
|
+
host: opts.host,
|
|
214
|
+
bgs: opts.bgs,
|
|
215
|
+
handle,
|
|
216
|
+
password: resolvedPassword
|
|
217
|
+
};
|
|
218
|
+
await saveConfig(config, profile);
|
|
219
|
+
}
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// src/commands/timeline.ts
|
|
224
|
+
import chalk2 from "chalk";
|
|
225
|
+
import WebSocket from "ws";
|
|
226
|
+
|
|
227
|
+
// src/lib/format.ts
|
|
228
|
+
import chalk from "chalk";
|
|
229
|
+
function printPost(post) {
|
|
230
|
+
const rec = post.record;
|
|
231
|
+
process.stdout.write(chalk.redBright(post.author.handle));
|
|
232
|
+
process.stdout.write(` [${post.author.displayName ?? ""}]`);
|
|
233
|
+
console.log(` (${formatTime(rec.createdAt)})`);
|
|
234
|
+
console.log(rec.text);
|
|
235
|
+
if (post.embed) {
|
|
236
|
+
const images = post.embed;
|
|
237
|
+
if (images.images) {
|
|
238
|
+
for (const img of images.images) {
|
|
239
|
+
console.log(` {${img.fullsize}}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
console.log(
|
|
244
|
+
` \u{1F44D}(${post.likeCount ?? 0})\u26A1(${post.repostCount ?? 0})\u21A9\uFE0F (${post.replyCount ?? 0})`
|
|
245
|
+
);
|
|
246
|
+
if (rec.reply?.parent) {
|
|
247
|
+
process.stdout.write(" > ");
|
|
248
|
+
console.log(chalk.blue(rec.reply.parent.uri));
|
|
249
|
+
}
|
|
250
|
+
process.stdout.write(" - ");
|
|
251
|
+
console.log(chalk.blue(post.uri));
|
|
252
|
+
console.log();
|
|
253
|
+
}
|
|
254
|
+
function printActor(actor) {
|
|
255
|
+
process.stdout.write(chalk.redBright(actor.handle));
|
|
256
|
+
process.stdout.write(` [${actor.displayName ?? ""}] `);
|
|
257
|
+
console.log(chalk.blue(actor.did));
|
|
258
|
+
}
|
|
259
|
+
function formatTime(dateStr) {
|
|
260
|
+
const date = new Date(dateStr);
|
|
261
|
+
return date.toISOString();
|
|
262
|
+
}
|
|
263
|
+
function printStreamPost(did, handle, text, rkey, collection) {
|
|
264
|
+
const author = handle ?? did;
|
|
265
|
+
process.stdout.write(chalk.redBright(author));
|
|
266
|
+
console.log(`: ${text}`);
|
|
267
|
+
console.log(` at://${did}/${collection}/${rkey}`);
|
|
268
|
+
console.log();
|
|
269
|
+
}
|
|
270
|
+
function outputJson(data) {
|
|
271
|
+
console.log(JSON.stringify(data));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// src/commands/timeline.ts
|
|
275
|
+
var DEFAULT_JETSTREAM = "wss://jetstream1.us-east.bsky.network/subscribe";
|
|
276
|
+
function registerTimeline(program2) {
|
|
277
|
+
program2.command("timeline").alias("tl").description("Show timeline").option("-H, --handle <handle>", "User handle").option("-n <count>", "Number of items", "30").action(async (opts) => {
|
|
278
|
+
const agent = await getClient(program2);
|
|
279
|
+
const json = isJson(program2);
|
|
280
|
+
const n = parseInt(opts.n, 10);
|
|
281
|
+
let feed = [];
|
|
282
|
+
let cursor;
|
|
283
|
+
while (true) {
|
|
284
|
+
if (opts.handle) {
|
|
285
|
+
const handle = opts.handle === "self" ? agent.session.did : opts.handle;
|
|
286
|
+
const resp = await agent.getAuthorFeed({
|
|
287
|
+
actor: handle,
|
|
288
|
+
cursor,
|
|
289
|
+
limit: n
|
|
290
|
+
});
|
|
291
|
+
feed.push(...resp.data.feed);
|
|
292
|
+
cursor = resp.data.cursor;
|
|
293
|
+
} else {
|
|
294
|
+
const resp = await agent.getTimeline({
|
|
295
|
+
cursor,
|
|
296
|
+
limit: n
|
|
297
|
+
});
|
|
298
|
+
feed.push(...resp.data.feed);
|
|
299
|
+
cursor = resp.data.cursor;
|
|
300
|
+
}
|
|
301
|
+
if (!cursor || feed.length > n) break;
|
|
302
|
+
}
|
|
303
|
+
feed.sort((a, b) => {
|
|
304
|
+
const ta = new Date(
|
|
305
|
+
a.post.record.createdAt
|
|
306
|
+
).getTime();
|
|
307
|
+
const tb = new Date(
|
|
308
|
+
b.post.record.createdAt
|
|
309
|
+
).getTime();
|
|
310
|
+
return ta - tb;
|
|
311
|
+
});
|
|
312
|
+
if (feed.length > n) {
|
|
313
|
+
feed = feed.slice(feed.length - n);
|
|
314
|
+
}
|
|
315
|
+
if (json) {
|
|
316
|
+
for (const p of feed) outputJson(p);
|
|
317
|
+
} else {
|
|
318
|
+
for (const p of feed) printPost(p.post);
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
function registerStream(program2) {
|
|
323
|
+
program2.command("stream").description("Stream live posts from the network").option("--cursor <cursor>", "Resume cursor (Unix microseconds)").option("-H, --handle <handle>", "Filter to a specific user").option("--pattern <regex>", "Filter post text by regex").option("--pattern-flags <flags>", "Regex flags for --pattern", "gi").option("--jetstream <url>", "Override Jetstream endpoint").action(
|
|
324
|
+
async (opts) => {
|
|
325
|
+
const json = isJson(program2);
|
|
326
|
+
const VALID_FLAGS = /* @__PURE__ */ new Set(["g", "i", "m", "s", "u", "v", "d", "y"]);
|
|
327
|
+
if (!opts.pattern && opts.patternFlags !== "gi") {
|
|
328
|
+
program2.error(`${chalk2.bgRed.white("Fatal error:")} --pattern-flags requires --pattern`);
|
|
329
|
+
}
|
|
330
|
+
if (opts.pattern) {
|
|
331
|
+
let flagChars = opts.patternFlags.split("");
|
|
332
|
+
const unknown = flagChars.filter((f) => !VALID_FLAGS.has(f));
|
|
333
|
+
if (unknown.length > 0) {
|
|
334
|
+
program2.error(`unknown regex flag(s): ${unknown.join(", ")}`);
|
|
335
|
+
}
|
|
336
|
+
const uniqueFlags = new Set(flagChars);
|
|
337
|
+
if (uniqueFlags.size !== flagChars.length) {
|
|
338
|
+
const dupes = flagChars.filter((f, i) => flagChars.indexOf(f) !== i);
|
|
339
|
+
console.warn(
|
|
340
|
+
`
|
|
341
|
+
${chalk2.bgYellow.black("Warning:")} duplicate regex flag(s) removed: ${[...new Set(dupes)].join(", ")}`
|
|
342
|
+
);
|
|
343
|
+
flagChars = [...uniqueFlags];
|
|
344
|
+
opts.patternFlags = flagChars.join("");
|
|
345
|
+
}
|
|
346
|
+
if (flagChars.includes("u") && flagChars.includes("v")) {
|
|
347
|
+
program2.error(`${chalk2.bgRed.white("Fatal error:")}regex flags u and v cannot be used together`);
|
|
348
|
+
}
|
|
349
|
+
if (flagChars.includes("y") && flagChars.includes("g")) {
|
|
350
|
+
console.warn(`${chalk2.bgYellow.black("Warning:")} sticky flag (y) makes global flag (g) meaningless`);
|
|
351
|
+
}
|
|
352
|
+
if (flagChars.includes("u") && flagChars.includes("d")) {
|
|
353
|
+
console.warn(`${chalk2.bgYellow.black("Warning:")} unicode (u) with hasIndices (d) is valid but rarely needed`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
const wsUrl = new URL(opts.jetstream ?? DEFAULT_JETSTREAM);
|
|
357
|
+
wsUrl.searchParams.set("wantedCollections", "app.bsky.feed.post");
|
|
358
|
+
let filterHandle = null;
|
|
359
|
+
if (opts.handle) {
|
|
360
|
+
const agent = await getClient(program2);
|
|
361
|
+
if (opts.handle === "self") {
|
|
362
|
+
wsUrl.searchParams.set("wantedDids", agent.session.did);
|
|
363
|
+
filterHandle = agent.session.handle;
|
|
364
|
+
} else if (opts.handle.startsWith("did:")) {
|
|
365
|
+
wsUrl.searchParams.set("wantedDids", opts.handle);
|
|
366
|
+
} else {
|
|
367
|
+
const profile = await agent.getProfile({ actor: opts.handle });
|
|
368
|
+
wsUrl.searchParams.set("wantedDids", profile.data.did);
|
|
369
|
+
filterHandle = opts.handle;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (opts.cursor) {
|
|
373
|
+
wsUrl.searchParams.set("cursor", opts.cursor);
|
|
374
|
+
}
|
|
375
|
+
let re = null;
|
|
376
|
+
if (opts.pattern) {
|
|
377
|
+
try {
|
|
378
|
+
re = new RegExp(opts.pattern, opts.patternFlags);
|
|
379
|
+
} catch (err) {
|
|
380
|
+
program2.error(`invalid regex pattern: ${err.message}`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
let lastCursor = "";
|
|
384
|
+
const ws = new WebSocket(wsUrl.toString());
|
|
385
|
+
process.on("SIGINT", () => {
|
|
386
|
+
if (lastCursor) {
|
|
387
|
+
console.error(`
|
|
388
|
+
Cursor: ${lastCursor}`);
|
|
389
|
+
}
|
|
390
|
+
ws.close();
|
|
391
|
+
process.exit(0);
|
|
392
|
+
});
|
|
393
|
+
ws.on("message", (data) => {
|
|
394
|
+
try {
|
|
395
|
+
const event = JSON.parse(data.toString());
|
|
396
|
+
lastCursor = String(event.time_us);
|
|
397
|
+
if (event.kind !== "commit") return;
|
|
398
|
+
const commit = event.commit;
|
|
399
|
+
if (commit.operation !== "create") return;
|
|
400
|
+
if (commit.collection !== "app.bsky.feed.post") return;
|
|
401
|
+
const text = commit.record?.text;
|
|
402
|
+
if (!text) return;
|
|
403
|
+
if (re && !re.test(text)) return;
|
|
404
|
+
if (json) {
|
|
405
|
+
outputJson(event);
|
|
406
|
+
} else {
|
|
407
|
+
printStreamPost(
|
|
408
|
+
event.did,
|
|
409
|
+
filterHandle,
|
|
410
|
+
text,
|
|
411
|
+
commit.rkey,
|
|
412
|
+
commit.collection
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
} catch {
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
ws.on("error", (err) => {
|
|
419
|
+
console.error("WebSocket error:", err.message);
|
|
420
|
+
process.exit(1);
|
|
421
|
+
});
|
|
422
|
+
ws.on("close", () => {
|
|
423
|
+
if (lastCursor) {
|
|
424
|
+
console.error(`Cursor: ${lastCursor}`);
|
|
425
|
+
}
|
|
426
|
+
process.exit(0);
|
|
427
|
+
});
|
|
428
|
+
await new Promise(() => {
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// src/commands/thread.ts
|
|
435
|
+
function normalizeUri(uri) {
|
|
436
|
+
if (!uri.startsWith("at://did:plc:")) {
|
|
437
|
+
return "at://did:plc:" + uri;
|
|
438
|
+
}
|
|
439
|
+
return uri;
|
|
440
|
+
}
|
|
441
|
+
function registerThread(program2) {
|
|
442
|
+
program2.command("thread").description("Show thread").argument("<uri>", "Post URI").option("-n <count>", "Number of items", "30").action(async (uri, opts) => {
|
|
443
|
+
const agent = await getClient(program2);
|
|
444
|
+
const json = isJson(program2);
|
|
445
|
+
const depth = parseInt(opts.n, 10);
|
|
446
|
+
const resp = await agent.getPostThread({
|
|
447
|
+
uri: normalizeUri(uri),
|
|
448
|
+
depth
|
|
449
|
+
});
|
|
450
|
+
const thread = resp.data.thread;
|
|
451
|
+
if (json) {
|
|
452
|
+
outputJson(thread);
|
|
453
|
+
if (thread.replies) {
|
|
454
|
+
for (const r of thread.replies) outputJson(r);
|
|
455
|
+
}
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
const replies = thread.replies ?? [];
|
|
459
|
+
replies.reverse();
|
|
460
|
+
printPost(thread.post);
|
|
461
|
+
for (const r of replies) {
|
|
462
|
+
if (r.post) printPost(r.post);
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// src/commands/post.ts
|
|
468
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
469
|
+
import { basename } from "path";
|
|
470
|
+
import { stdin as stdin2 } from "process";
|
|
471
|
+
import { load as cheerioLoad } from "cheerio";
|
|
472
|
+
|
|
473
|
+
// src/lib/extract.ts
|
|
474
|
+
var URL_PATTERN = /https?:\/\/[-A-Za-z0-9+&@#/%?=~_|!:,.;()]+/g;
|
|
475
|
+
var MENTION_PATTERN = /@[a-zA-Z0-9.]+/g;
|
|
476
|
+
var TAG_PATTERN = /\B#\S+/g;
|
|
477
|
+
function byteLength(str) {
|
|
478
|
+
return new TextEncoder().encode(str).byteLength;
|
|
479
|
+
}
|
|
480
|
+
function extractWithByteOffsets(text, pattern, stripPrefix) {
|
|
481
|
+
const results = [];
|
|
482
|
+
let match;
|
|
483
|
+
pattern.lastIndex = 0;
|
|
484
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
485
|
+
const before = text.slice(0, match.index);
|
|
486
|
+
const matchText = match[0];
|
|
487
|
+
const start = byteLength(before);
|
|
488
|
+
const end = start + byteLength(matchText);
|
|
489
|
+
const entryText = stripPrefix ? matchText.replace(new RegExp(`^${stripPrefix}`), "") : matchText;
|
|
490
|
+
results.push({ start, end, text: entryText });
|
|
491
|
+
}
|
|
492
|
+
return results;
|
|
493
|
+
}
|
|
494
|
+
function extractLinks(text) {
|
|
495
|
+
return extractWithByteOffsets(text, URL_PATTERN);
|
|
496
|
+
}
|
|
497
|
+
function extractMentions(text) {
|
|
498
|
+
return extractWithByteOffsets(text, MENTION_PATTERN, "@");
|
|
499
|
+
}
|
|
500
|
+
function extractTags(text) {
|
|
501
|
+
return extractWithByteOffsets(text, TAG_PATTERN, "#");
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// src/commands/post.ts
|
|
505
|
+
async function readStdin() {
|
|
506
|
+
const chunks = [];
|
|
507
|
+
for await (const chunk of stdin2) {
|
|
508
|
+
chunks.push(chunk);
|
|
509
|
+
}
|
|
510
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
511
|
+
}
|
|
512
|
+
function detectMimeType(data) {
|
|
513
|
+
if (data[0] === 255 && data[1] === 216) return "image/jpeg";
|
|
514
|
+
if (data[0] === 137 && data[1] === 80 && data[2] === 78 && data[3] === 71)
|
|
515
|
+
return "image/png";
|
|
516
|
+
if (data[0] === 71 && data[1] === 73 && data[2] === 70)
|
|
517
|
+
return "image/gif";
|
|
518
|
+
if (data[0] === 82 && data[1] === 73 && data[2] === 70 && data[3] === 70)
|
|
519
|
+
return "image/webp";
|
|
520
|
+
if (data[4] === 102 && data[5] === 116 && data[6] === 121)
|
|
521
|
+
return "video/mp4";
|
|
522
|
+
return "application/octet-stream";
|
|
523
|
+
}
|
|
524
|
+
async function buildFacets(agent, text) {
|
|
525
|
+
const facets = [];
|
|
526
|
+
for (const entry of extractLinks(text)) {
|
|
527
|
+
facets.push({
|
|
528
|
+
index: { byteStart: entry.start, byteEnd: entry.end },
|
|
529
|
+
features: [
|
|
530
|
+
{ $type: "app.bsky.richtext.facet#link", uri: entry.text }
|
|
531
|
+
]
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
for (const entry of extractMentions(text)) {
|
|
535
|
+
try {
|
|
536
|
+
const profile = await agent.getProfile({ actor: entry.text });
|
|
537
|
+
facets.push({
|
|
538
|
+
index: { byteStart: entry.start, byteEnd: entry.end },
|
|
539
|
+
features: [
|
|
540
|
+
{ $type: "app.bsky.richtext.facet#mention", did: profile.data.did }
|
|
541
|
+
]
|
|
542
|
+
});
|
|
543
|
+
} catch {
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
for (const entry of extractTags(text)) {
|
|
547
|
+
facets.push({
|
|
548
|
+
index: { byteStart: entry.start, byteEnd: entry.end },
|
|
549
|
+
features: [
|
|
550
|
+
{ $type: "app.bsky.richtext.facet#tag", tag: entry.text }
|
|
551
|
+
]
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
return facets;
|
|
555
|
+
}
|
|
556
|
+
async function fetchLinkCard(agent, url) {
|
|
557
|
+
try {
|
|
558
|
+
const resp = await fetch(url);
|
|
559
|
+
if (!resp.ok) return null;
|
|
560
|
+
const html = await resp.text();
|
|
561
|
+
const $ = cheerioLoad(html);
|
|
562
|
+
let title = $("title").text();
|
|
563
|
+
let description = $('meta[property="description"]').attr("content") ?? "";
|
|
564
|
+
const imgURL = $('meta[property="og:image"]').attr("content");
|
|
565
|
+
if (!title) title = $('meta[property="og:title"]').attr("content") ?? url;
|
|
566
|
+
if (!description)
|
|
567
|
+
description = $('meta[property="og:description"]').attr("content") ?? url;
|
|
568
|
+
const external = {
|
|
569
|
+
uri: url,
|
|
570
|
+
title,
|
|
571
|
+
description
|
|
572
|
+
};
|
|
573
|
+
if (imgURL) {
|
|
574
|
+
try {
|
|
575
|
+
const imgResp = await fetch(imgURL);
|
|
576
|
+
if (imgResp.ok) {
|
|
577
|
+
const imgData = new Uint8Array(await imgResp.arrayBuffer());
|
|
578
|
+
const uploadResp = await agent.uploadBlob(imgData, {
|
|
579
|
+
encoding: detectMimeType(imgData)
|
|
580
|
+
});
|
|
581
|
+
external.thumb = uploadResp.data.blob;
|
|
582
|
+
}
|
|
583
|
+
} catch {
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return { $type: "app.bsky.embed.external", external };
|
|
587
|
+
} catch {
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
async function createPost(agent, text, opts) {
|
|
592
|
+
const facets = await buildFacets(agent, text);
|
|
593
|
+
const post = {
|
|
594
|
+
$type: "app.bsky.feed.post",
|
|
595
|
+
text,
|
|
596
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
597
|
+
facets: facets.length > 0 ? facets : void 0
|
|
598
|
+
};
|
|
599
|
+
if (opts.reply) {
|
|
600
|
+
post.reply = {
|
|
601
|
+
root: opts.replyRoot ?? opts.reply,
|
|
602
|
+
parent: opts.reply
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
if (opts.images && opts.images.length > 0) {
|
|
606
|
+
const images = [];
|
|
607
|
+
for (let i = 0; i < opts.images.length; i++) {
|
|
608
|
+
const data = await readFile3(opts.images[i]);
|
|
609
|
+
const uploadResp = await agent.uploadBlob(data, {
|
|
610
|
+
encoding: detectMimeType(data)
|
|
611
|
+
});
|
|
612
|
+
images.push({
|
|
613
|
+
alt: opts.imageAlts?.[i] ?? basename(opts.images[i]),
|
|
614
|
+
image: uploadResp.data.blob
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
post.embed = { $type: "app.bsky.embed.images", images };
|
|
618
|
+
}
|
|
619
|
+
if (opts.video) {
|
|
620
|
+
const data = await readFile3(opts.video);
|
|
621
|
+
const uploadResp = await agent.uploadBlob(data, {
|
|
622
|
+
encoding: detectMimeType(data)
|
|
623
|
+
});
|
|
624
|
+
post.embed = {
|
|
625
|
+
$type: "app.bsky.embed.video",
|
|
626
|
+
video: uploadResp.data.blob,
|
|
627
|
+
alt: opts.videoAlt ?? basename(opts.video)
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
if (opts.quote) {
|
|
631
|
+
post.embed = {
|
|
632
|
+
$type: "app.bsky.embed.record",
|
|
633
|
+
record: opts.quote
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
if (!post.embed) {
|
|
637
|
+
const links = extractLinks(text);
|
|
638
|
+
if (links.length > 0) {
|
|
639
|
+
const card = await fetchLinkCard(agent, links[0].text);
|
|
640
|
+
if (card) post.embed = card;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
const resp = await agent.post(post);
|
|
644
|
+
return resp.uri;
|
|
645
|
+
}
|
|
646
|
+
function registerPost(program2) {
|
|
647
|
+
program2.command("post").description("Create a new post").argument("[text...]", "Post text").option("--stdin", "Read text from stdin").option("-i, --image <files...>", "Image files to attach").option("--image-alt <alts...>", "Alt text for images").option("--video <file>", "Video file to attach").option("--video-alt <alt>", "Alt text for video").action(
|
|
648
|
+
async (textParts, opts) => {
|
|
649
|
+
let text = textParts.join(" ");
|
|
650
|
+
if (opts.stdin) {
|
|
651
|
+
text = await readStdin();
|
|
652
|
+
}
|
|
653
|
+
if (!text.trim()) {
|
|
654
|
+
console.error("Error: post text is required");
|
|
655
|
+
process.exit(1);
|
|
656
|
+
}
|
|
657
|
+
const agent = await getClient(program2);
|
|
658
|
+
const uri = await createPost(agent, text, {
|
|
659
|
+
images: opts.image,
|
|
660
|
+
imageAlts: opts.imageAlt,
|
|
661
|
+
video: opts.video,
|
|
662
|
+
videoAlt: opts.videoAlt
|
|
663
|
+
});
|
|
664
|
+
console.log(uri);
|
|
665
|
+
}
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
function registerReply(program2) {
|
|
669
|
+
program2.command("reply").description("Reply to a post").argument("<uri>", "URI of the post to reply to").argument("<text...>", "Reply text").action(async (uri, textParts) => {
|
|
670
|
+
const text = textParts.join(" ");
|
|
671
|
+
const agent = await getClient(program2);
|
|
672
|
+
const atUri = uri.startsWith("at://") ? uri : `at://did:plc:${uri}`;
|
|
673
|
+
const parts = atUri.split("/");
|
|
674
|
+
const rkey = parts[parts.length - 1];
|
|
675
|
+
const collection = parts[parts.length - 2];
|
|
676
|
+
const did = parts[2];
|
|
677
|
+
const record = await agent.com.atproto.repo.getRecord({
|
|
678
|
+
repo: did,
|
|
679
|
+
collection,
|
|
680
|
+
rkey
|
|
681
|
+
});
|
|
682
|
+
const parent = {
|
|
683
|
+
uri: record.data.uri,
|
|
684
|
+
cid: record.data.cid
|
|
685
|
+
};
|
|
686
|
+
const parentPost = record.data.value;
|
|
687
|
+
const root = parentPost.reply?.root ?? parent;
|
|
688
|
+
const postUri = await createPost(agent, text, {
|
|
689
|
+
reply: parent,
|
|
690
|
+
replyRoot: root
|
|
691
|
+
});
|
|
692
|
+
console.log(postUri);
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
function registerQuote(program2) {
|
|
696
|
+
program2.command("quote").description("Quote a post").argument("<uri>", "URI of the post to quote").argument("<text...>", "Quote text").action(async (uri, textParts) => {
|
|
697
|
+
const text = textParts.join(" ");
|
|
698
|
+
const agent = await getClient(program2);
|
|
699
|
+
const atUri = uri.startsWith("at://") ? uri : `at://did:plc:${uri}`;
|
|
700
|
+
const parts = atUri.split("/");
|
|
701
|
+
const rkey = parts[parts.length - 1];
|
|
702
|
+
const collection = parts[parts.length - 2];
|
|
703
|
+
const did = parts[2];
|
|
704
|
+
const record = await agent.com.atproto.repo.getRecord({
|
|
705
|
+
repo: did,
|
|
706
|
+
collection,
|
|
707
|
+
rkey
|
|
708
|
+
});
|
|
709
|
+
const postUri = await createPost(agent, text, {
|
|
710
|
+
quote: {
|
|
711
|
+
uri: record.data.uri,
|
|
712
|
+
cid: record.data.cid
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
console.log(postUri);
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// src/commands/delete.ts
|
|
720
|
+
function registerDelete(program2) {
|
|
721
|
+
program2.command("delete").description("Delete a post").argument("<uri...>", "Post URI(s) to delete").action(async (uris) => {
|
|
722
|
+
const agent = await getClient(program2);
|
|
723
|
+
for (const uri of uris) {
|
|
724
|
+
const atUri = uri.startsWith("at://") ? uri : `at://did:plc:${uri}`;
|
|
725
|
+
const parts = atUri.split("/");
|
|
726
|
+
if (parts.length < 3) {
|
|
727
|
+
console.error(`Invalid post URI: ${uri}`);
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
const rkey = parts[parts.length - 1];
|
|
731
|
+
const collection = parts[parts.length - 2];
|
|
732
|
+
await agent.com.atproto.repo.deleteRecord({
|
|
733
|
+
repo: agent.session.did,
|
|
734
|
+
collection,
|
|
735
|
+
rkey
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// src/commands/like.ts
|
|
742
|
+
import chalk3 from "chalk";
|
|
743
|
+
function registerLike(program2) {
|
|
744
|
+
program2.command("like").description("Like a post").argument("<uri...>", "Post URI(s) to like").action(async (uris) => {
|
|
745
|
+
const agent = await getClient(program2);
|
|
746
|
+
for (const uri of uris) {
|
|
747
|
+
const atUri = uri.startsWith("at://") ? uri : `at://did:plc:${uri}`;
|
|
748
|
+
const parts = atUri.split("/");
|
|
749
|
+
const rkey = parts[parts.length - 1];
|
|
750
|
+
const collection = parts[parts.length - 2];
|
|
751
|
+
const did = parts[2];
|
|
752
|
+
const record = await agent.com.atproto.repo.getRecord({
|
|
753
|
+
repo: did,
|
|
754
|
+
collection,
|
|
755
|
+
rkey
|
|
756
|
+
});
|
|
757
|
+
const resp = await agent.like(record.data.uri, record.data.cid);
|
|
758
|
+
console.log(resp.uri);
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
function registerLikes(program2) {
|
|
763
|
+
program2.command("likes").description("Show likes on a post").argument("<uri>", "Post URI").action(async (uri) => {
|
|
764
|
+
const agent = await getClient(program2);
|
|
765
|
+
const json = isJson(program2);
|
|
766
|
+
const atUri = uri.startsWith("at://") ? uri : `at://did:plc:${uri}`;
|
|
767
|
+
const parts = atUri.split("/");
|
|
768
|
+
const rkey = parts[parts.length - 1];
|
|
769
|
+
const collection = parts[parts.length - 2];
|
|
770
|
+
const did = parts[2];
|
|
771
|
+
const record = await agent.com.atproto.repo.getRecord({
|
|
772
|
+
repo: did,
|
|
773
|
+
collection,
|
|
774
|
+
rkey
|
|
775
|
+
});
|
|
776
|
+
const resp = await agent.getLikes({
|
|
777
|
+
uri: record.data.uri,
|
|
778
|
+
cid: record.data.cid,
|
|
779
|
+
limit: 50
|
|
780
|
+
});
|
|
781
|
+
if (json) {
|
|
782
|
+
for (const v of resp.data.likes) outputJson(v);
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
for (const v of resp.data.likes) {
|
|
786
|
+
process.stdout.write("\u{1F44D} ");
|
|
787
|
+
process.stdout.write(chalk3.redBright(v.actor.handle));
|
|
788
|
+
process.stdout.write(` [${v.actor.displayName ?? ""}]`);
|
|
789
|
+
console.log(` (${v.createdAt})`);
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// src/commands/repost.ts
|
|
795
|
+
import chalk4 from "chalk";
|
|
796
|
+
function registerRepost(program2) {
|
|
797
|
+
program2.command("repost").description("Repost a post").argument("<uri...>", "Post URI(s) to repost").action(async (uris) => {
|
|
798
|
+
const agent = await getClient(program2);
|
|
799
|
+
for (const uri of uris) {
|
|
800
|
+
const atUri = uri.startsWith("at://") ? uri : `at://did:plc:${uri}`;
|
|
801
|
+
const parts = atUri.split("/");
|
|
802
|
+
const rkey = parts[parts.length - 1];
|
|
803
|
+
const collection = parts[parts.length - 2];
|
|
804
|
+
const did = parts[2];
|
|
805
|
+
const record = await agent.com.atproto.repo.getRecord({
|
|
806
|
+
repo: did,
|
|
807
|
+
collection,
|
|
808
|
+
rkey
|
|
809
|
+
});
|
|
810
|
+
const resp = await agent.repost(record.data.uri, record.data.cid);
|
|
811
|
+
console.log(resp.uri);
|
|
812
|
+
}
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
function registerReposts(program2) {
|
|
816
|
+
program2.command("reposts").description("Show reposts of a post").argument("<uri>", "Post URI").action(async (uri) => {
|
|
817
|
+
const agent = await getClient(program2);
|
|
818
|
+
const json = isJson(program2);
|
|
819
|
+
const atUri = uri.startsWith("at://") ? uri : `at://did:plc:${uri}`;
|
|
820
|
+
const parts = atUri.split("/");
|
|
821
|
+
const rkey = parts[parts.length - 1];
|
|
822
|
+
const collection = parts[parts.length - 2];
|
|
823
|
+
const did = parts[2];
|
|
824
|
+
const record = await agent.com.atproto.repo.getRecord({
|
|
825
|
+
repo: did,
|
|
826
|
+
collection,
|
|
827
|
+
rkey
|
|
828
|
+
});
|
|
829
|
+
const resp = await agent.getRepostedBy({
|
|
830
|
+
uri: record.data.uri,
|
|
831
|
+
cid: record.data.cid,
|
|
832
|
+
limit: 50
|
|
833
|
+
});
|
|
834
|
+
if (json) {
|
|
835
|
+
for (const r of resp.data.repostedBy) outputJson(r);
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
for (const r of resp.data.repostedBy) {
|
|
839
|
+
process.stdout.write("\u26A1 ");
|
|
840
|
+
process.stdout.write(chalk4.redBright(r.handle));
|
|
841
|
+
console.log(` [${r.displayName ?? ""}]`);
|
|
842
|
+
}
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// src/commands/social.ts
|
|
847
|
+
async function resolveDid(agent, handleOrDid) {
|
|
848
|
+
if (handleOrDid.startsWith("did:")) return handleOrDid;
|
|
849
|
+
const profile = await agent.getProfile({ actor: handleOrDid });
|
|
850
|
+
return profile.data.did;
|
|
851
|
+
}
|
|
852
|
+
function registerFollow(program2) {
|
|
853
|
+
program2.command("follow").description("Follow user(s)").argument("<handles...>", "Handle(s) to follow").action(async (handles) => {
|
|
854
|
+
const agent = await getClient(program2);
|
|
855
|
+
for (const handle of handles) {
|
|
856
|
+
const profile = await agent.getProfile({ actor: handle });
|
|
857
|
+
const resp = await agent.follow(profile.data.did);
|
|
858
|
+
console.log(resp.uri);
|
|
859
|
+
}
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
function registerUnfollow(program2) {
|
|
863
|
+
program2.command("unfollow").description("Unfollow user(s)").argument("<handles...>", "Handle(s) to unfollow").action(async (handles) => {
|
|
864
|
+
const agent = await getClient(program2);
|
|
865
|
+
for (const handle of handles) {
|
|
866
|
+
const profile = await agent.getProfile({ actor: handle });
|
|
867
|
+
const followUri = profile.data.viewer?.following;
|
|
868
|
+
if (!followUri) continue;
|
|
869
|
+
const parts = followUri.split("/");
|
|
870
|
+
const rkey = parts[parts.length - 1];
|
|
871
|
+
const collection = parts[parts.length - 2];
|
|
872
|
+
console.log(followUri);
|
|
873
|
+
await agent.com.atproto.repo.deleteRecord({
|
|
874
|
+
repo: agent.session.did,
|
|
875
|
+
collection,
|
|
876
|
+
rkey
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
function registerFollows(program2) {
|
|
882
|
+
program2.command("follows").description("Show follows").option("-H, --handle <handle>", "User handle").action(async (opts) => {
|
|
883
|
+
const agent = await getClient(program2);
|
|
884
|
+
const json = isJson(program2);
|
|
885
|
+
const handle = opts.handle ?? agent.session.handle;
|
|
886
|
+
let cursor;
|
|
887
|
+
while (true) {
|
|
888
|
+
const resp = await agent.getFollows({
|
|
889
|
+
actor: handle,
|
|
890
|
+
cursor,
|
|
891
|
+
limit: 100
|
|
892
|
+
});
|
|
893
|
+
if (json) {
|
|
894
|
+
for (const f of resp.data.follows) outputJson(f);
|
|
895
|
+
} else {
|
|
896
|
+
for (const f of resp.data.follows) printActor(f);
|
|
897
|
+
}
|
|
898
|
+
cursor = resp.data.cursor;
|
|
899
|
+
if (!cursor) break;
|
|
900
|
+
}
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
function registerFollowers(program2) {
|
|
904
|
+
program2.command("followers").description("Show followers").option("-H, --handle <handle>", "User handle").action(async (opts) => {
|
|
905
|
+
const agent = await getClient(program2);
|
|
906
|
+
const json = isJson(program2);
|
|
907
|
+
const handle = opts.handle ?? agent.session.handle;
|
|
908
|
+
let cursor;
|
|
909
|
+
while (true) {
|
|
910
|
+
const resp = await agent.getFollowers({
|
|
911
|
+
actor: handle,
|
|
912
|
+
cursor,
|
|
913
|
+
limit: 100
|
|
914
|
+
});
|
|
915
|
+
if (json) {
|
|
916
|
+
for (const f of resp.data.followers) outputJson(f);
|
|
917
|
+
} else {
|
|
918
|
+
for (const f of resp.data.followers) printActor(f);
|
|
919
|
+
}
|
|
920
|
+
cursor = resp.data.cursor;
|
|
921
|
+
if (!cursor) break;
|
|
922
|
+
}
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
function registerBlock(program2) {
|
|
926
|
+
program2.command("block").description("Block user(s)").argument("<handles...>", "Handle(s) or DID(s) to block").action(async (handles) => {
|
|
927
|
+
const agent = await getClient(program2);
|
|
928
|
+
for (const handle of handles) {
|
|
929
|
+
const did = await resolveDid(agent, handle);
|
|
930
|
+
const resp = await agent.com.atproto.repo.createRecord({
|
|
931
|
+
repo: agent.session.did,
|
|
932
|
+
collection: "app.bsky.graph.block",
|
|
933
|
+
record: {
|
|
934
|
+
$type: "app.bsky.graph.block",
|
|
935
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
936
|
+
subject: did
|
|
937
|
+
}
|
|
938
|
+
});
|
|
939
|
+
console.log(resp.data.uri);
|
|
940
|
+
}
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
function registerUnblock(program2) {
|
|
944
|
+
program2.command("unblock").description("Unblock user(s)").argument("<handles...>", "Handle(s) to unblock").action(async (handles) => {
|
|
945
|
+
const agent = await getClient(program2);
|
|
946
|
+
for (const handle of handles) {
|
|
947
|
+
const profile = await agent.getProfile({ actor: handle });
|
|
948
|
+
const blockUri = profile.data.viewer?.blocking;
|
|
949
|
+
if (!blockUri) continue;
|
|
950
|
+
const parts = blockUri.split("/");
|
|
951
|
+
const rkey = parts[parts.length - 1];
|
|
952
|
+
const collection = parts[parts.length - 2];
|
|
953
|
+
console.log(blockUri);
|
|
954
|
+
await agent.com.atproto.repo.deleteRecord({
|
|
955
|
+
repo: agent.session.did,
|
|
956
|
+
collection,
|
|
957
|
+
rkey
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
function registerBlocks(program2) {
|
|
963
|
+
program2.command("blocks").description("Show blocked users").option("-H, --handle <handle>", "User handle").action(async () => {
|
|
964
|
+
const agent = await getClient(program2);
|
|
965
|
+
const json = isJson(program2);
|
|
966
|
+
let cursor;
|
|
967
|
+
while (true) {
|
|
968
|
+
const resp = await agent.app.bsky.graph.getBlocks({
|
|
969
|
+
cursor,
|
|
970
|
+
limit: 100
|
|
971
|
+
});
|
|
972
|
+
if (json) {
|
|
973
|
+
for (const b of resp.data.blocks) outputJson(b);
|
|
974
|
+
} else {
|
|
975
|
+
for (const b of resp.data.blocks) printActor(b);
|
|
976
|
+
}
|
|
977
|
+
cursor = resp.data.cursor;
|
|
978
|
+
if (!cursor) break;
|
|
979
|
+
}
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
function registerMute(program2) {
|
|
983
|
+
program2.command("mute").description("Mute user(s)").argument("<handles...>", "Handle(s) or DID(s) to mute").action(async (handles) => {
|
|
984
|
+
const agent = await getClient(program2);
|
|
985
|
+
for (const handle of handles) {
|
|
986
|
+
const did = await resolveDid(agent, handle);
|
|
987
|
+
await agent.mute(did);
|
|
988
|
+
}
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// src/commands/search.ts
|
|
993
|
+
function registerSearch(program2) {
|
|
994
|
+
program2.command("search").description("Search posts").argument("<terms...>", "Search terms").option("-n <count>", "Number of results", "100").action(async (terms, opts) => {
|
|
995
|
+
const agent = await getClient(program2);
|
|
996
|
+
const json = isJson(program2);
|
|
997
|
+
const n = parseInt(opts.n, 10);
|
|
998
|
+
const query = terms.join(" ");
|
|
999
|
+
let results = [];
|
|
1000
|
+
let cursor;
|
|
1001
|
+
while (true) {
|
|
1002
|
+
const resp = await agent.app.bsky.feed.searchPosts({
|
|
1003
|
+
q: query,
|
|
1004
|
+
cursor,
|
|
1005
|
+
limit: 100
|
|
1006
|
+
});
|
|
1007
|
+
results.push(...resp.data.posts);
|
|
1008
|
+
cursor = resp.data.cursor;
|
|
1009
|
+
if (!cursor || results.length > n) break;
|
|
1010
|
+
}
|
|
1011
|
+
results.sort((a, b) => {
|
|
1012
|
+
const ta = new Date(
|
|
1013
|
+
a.record.createdAt
|
|
1014
|
+
).getTime();
|
|
1015
|
+
const tb = new Date(
|
|
1016
|
+
b.record.createdAt
|
|
1017
|
+
).getTime();
|
|
1018
|
+
return ta - tb;
|
|
1019
|
+
});
|
|
1020
|
+
if (results.length > n) {
|
|
1021
|
+
results = results.slice(results.length - n);
|
|
1022
|
+
}
|
|
1023
|
+
if (json) {
|
|
1024
|
+
for (const p of results) outputJson(p);
|
|
1025
|
+
} else {
|
|
1026
|
+
for (const p of results) printPost(p);
|
|
1027
|
+
}
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
function registerSearchUsers(program2) {
|
|
1031
|
+
program2.command("search-users").description("Search for users").argument("<terms...>", "Search terms").option("-n <count>", "Number of results", "100").action(async (terms, opts) => {
|
|
1032
|
+
const agent = await getClient(program2);
|
|
1033
|
+
const n = parseInt(opts.n, 10);
|
|
1034
|
+
const query = terms.join(" ");
|
|
1035
|
+
const resp = await agent.searchActors({
|
|
1036
|
+
term: query,
|
|
1037
|
+
limit: n
|
|
1038
|
+
});
|
|
1039
|
+
for (const actor of resp.data.actors) {
|
|
1040
|
+
outputJson(actor);
|
|
1041
|
+
}
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// src/commands/profile.ts
|
|
1046
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
1047
|
+
function detectMimeType2(data) {
|
|
1048
|
+
if (data[0] === 255 && data[1] === 216) return "image/jpeg";
|
|
1049
|
+
if (data[0] === 137 && data[1] === 80 && data[2] === 78 && data[3] === 71)
|
|
1050
|
+
return "image/png";
|
|
1051
|
+
if (data[0] === 71 && data[1] === 73 && data[2] === 70)
|
|
1052
|
+
return "image/gif";
|
|
1053
|
+
return "application/octet-stream";
|
|
1054
|
+
}
|
|
1055
|
+
function registerProfile(program2) {
|
|
1056
|
+
program2.command("profile").description("Show profile").option("-H, --handle <handle>", "User handle").action(async (opts) => {
|
|
1057
|
+
const agent = await getClient(program2);
|
|
1058
|
+
const json = isJson(program2);
|
|
1059
|
+
const handle = opts.handle ?? agent.session.handle;
|
|
1060
|
+
const resp = await agent.getProfile({ actor: handle });
|
|
1061
|
+
const profile = resp.data;
|
|
1062
|
+
if (json) {
|
|
1063
|
+
outputJson(profile);
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
console.log(`Did: ${profile.did}`);
|
|
1067
|
+
console.log(`Handle: ${profile.handle}`);
|
|
1068
|
+
console.log(`DisplayName: ${profile.displayName ?? ""}`);
|
|
1069
|
+
console.log(`Description: ${profile.description ?? ""}`);
|
|
1070
|
+
console.log(`Follows: ${profile.followsCount ?? 0}`);
|
|
1071
|
+
console.log(`Followers: ${profile.followersCount ?? 0}`);
|
|
1072
|
+
console.log(`Avatar: ${profile.avatar ?? ""}`);
|
|
1073
|
+
console.log(`Banner: ${profile.banner ?? ""}`);
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
function registerProfileUpdate(program2) {
|
|
1077
|
+
program2.command("profile-update").description("Update profile").argument("[displayname]", "Display name").argument("[description]", "Description").option("--avatar <file>", "Avatar image file").option("--banner <file>", "Banner image file").action(
|
|
1078
|
+
async (displayName, description, opts) => {
|
|
1079
|
+
if (!displayName && !description && !opts.avatar && !opts.banner) {
|
|
1080
|
+
console.error("Error: provide at least one field to update");
|
|
1081
|
+
process.exit(1);
|
|
1082
|
+
}
|
|
1083
|
+
const agent = await getClient(program2);
|
|
1084
|
+
const current = await agent.getProfile({
|
|
1085
|
+
actor: agent.session.handle
|
|
1086
|
+
});
|
|
1087
|
+
const name = displayName ?? current.data.displayName;
|
|
1088
|
+
const desc = description ?? current.data.description;
|
|
1089
|
+
let avatar = void 0;
|
|
1090
|
+
if (opts.avatar) {
|
|
1091
|
+
const data = await readFile4(opts.avatar);
|
|
1092
|
+
const uploadResp = await agent.uploadBlob(data, {
|
|
1093
|
+
encoding: detectMimeType2(data)
|
|
1094
|
+
});
|
|
1095
|
+
avatar = uploadResp.data.blob;
|
|
1096
|
+
}
|
|
1097
|
+
let banner = void 0;
|
|
1098
|
+
if (opts.banner) {
|
|
1099
|
+
const data = await readFile4(opts.banner);
|
|
1100
|
+
const uploadResp = await agent.uploadBlob(data, {
|
|
1101
|
+
encoding: detectMimeType2(data)
|
|
1102
|
+
});
|
|
1103
|
+
banner = uploadResp.data.blob;
|
|
1104
|
+
}
|
|
1105
|
+
const currentRecord = await agent.com.atproto.repo.getRecord({
|
|
1106
|
+
repo: agent.session.did,
|
|
1107
|
+
collection: "app.bsky.actor.profile",
|
|
1108
|
+
rkey: "self"
|
|
1109
|
+
});
|
|
1110
|
+
await agent.com.atproto.repo.putRecord({
|
|
1111
|
+
repo: agent.session.did,
|
|
1112
|
+
collection: "app.bsky.actor.profile",
|
|
1113
|
+
rkey: "self",
|
|
1114
|
+
record: {
|
|
1115
|
+
$type: "app.bsky.actor.profile",
|
|
1116
|
+
displayName: name,
|
|
1117
|
+
description: desc,
|
|
1118
|
+
...avatar ? { avatar } : {},
|
|
1119
|
+
...banner ? { banner } : {}
|
|
1120
|
+
},
|
|
1121
|
+
swapRecord: currentRecord.data.cid
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
);
|
|
1125
|
+
}
|
|
1126
|
+
function registerSession(program2) {
|
|
1127
|
+
program2.command("session").description("Show session info").action(async () => {
|
|
1128
|
+
const agent = await getClient(program2);
|
|
1129
|
+
const json = isJson(program2);
|
|
1130
|
+
const resp = await agent.com.atproto.server.getSession();
|
|
1131
|
+
const session = resp.data;
|
|
1132
|
+
if (json) {
|
|
1133
|
+
outputJson(session);
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
console.log(`Did: ${session.did}`);
|
|
1137
|
+
console.log(`Email: ${session.email ?? ""}`);
|
|
1138
|
+
console.log(`Handle: ${session.handle}`);
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// src/commands/notification.ts
|
|
1143
|
+
import chalk5 from "chalk";
|
|
1144
|
+
function registerNotifs(program2) {
|
|
1145
|
+
program2.command("notifs").alias("notification").description("Show notifications").option("-a, --all", "Show all (including read)").action(async (opts) => {
|
|
1146
|
+
const agent = await getClient(program2);
|
|
1147
|
+
const json = isJson(program2);
|
|
1148
|
+
const resp = await agent.listNotifications({ limit: 50 });
|
|
1149
|
+
const notifs = resp.data.notifications;
|
|
1150
|
+
if (json) {
|
|
1151
|
+
for (const n of notifs) outputJson(n);
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
for (const n of notifs) {
|
|
1155
|
+
if (!opts.all && n.isRead) continue;
|
|
1156
|
+
process.stdout.write(chalk5.redBright(n.author.handle));
|
|
1157
|
+
process.stdout.write(` [${n.author.displayName ?? ""}] `);
|
|
1158
|
+
console.log(chalk5.blue(n.author.did));
|
|
1159
|
+
const record = n.record;
|
|
1160
|
+
switch (record.$type) {
|
|
1161
|
+
case "app.bsky.feed.post":
|
|
1162
|
+
console.log(` ${n.reason} to ${n.uri}`);
|
|
1163
|
+
break;
|
|
1164
|
+
case "app.bsky.feed.repost": {
|
|
1165
|
+
const repost = record;
|
|
1166
|
+
console.log(` reposted ${repost.subject.uri}`);
|
|
1167
|
+
break;
|
|
1168
|
+
}
|
|
1169
|
+
case "app.bsky.feed.like": {
|
|
1170
|
+
const like = record;
|
|
1171
|
+
console.log(` liked ${like.subject.uri}`);
|
|
1172
|
+
break;
|
|
1173
|
+
}
|
|
1174
|
+
case "app.bsky.graph.follow":
|
|
1175
|
+
console.log(" followed you");
|
|
1176
|
+
break;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
await agent.app.bsky.notification.updateSeen({
|
|
1180
|
+
seenAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1181
|
+
});
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// src/commands/moderation.ts
|
|
1186
|
+
async function resolveDid2(agent, handleOrDid) {
|
|
1187
|
+
if (handleOrDid.startsWith("did:")) return handleOrDid;
|
|
1188
|
+
const profile = await agent.getProfile({ actor: handleOrDid });
|
|
1189
|
+
return profile.data.did;
|
|
1190
|
+
}
|
|
1191
|
+
function registerReport(program2) {
|
|
1192
|
+
program2.command("report").description("Report a user").argument("<handle>", "Handle or DID to report").option("--comment <text>", "Report comment").action(async (handle, opts) => {
|
|
1193
|
+
const agent = await getClient(program2);
|
|
1194
|
+
const did = await resolveDid2(agent, handle);
|
|
1195
|
+
const resp = await agent.com.atproto.moderation.createReport({
|
|
1196
|
+
reasonType: "com.atproto.moderation.defs#reasonSpam",
|
|
1197
|
+
subject: {
|
|
1198
|
+
$type: "com.atproto.admin.defs#repoRef",
|
|
1199
|
+
did
|
|
1200
|
+
},
|
|
1201
|
+
reason: opts.comment
|
|
1202
|
+
});
|
|
1203
|
+
console.log("Report created successfully:", JSON.stringify(resp.data));
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
function registerModList(program2) {
|
|
1207
|
+
program2.command("mod-list").description("Create a moderation list with user(s)").argument("<handles...>", "Handle(s) or DID(s) to add").option("--name <name>", "List name", "NewList").option("--desc <description>", "List description", "").action(
|
|
1208
|
+
async (handles, opts) => {
|
|
1209
|
+
const agent = await getClient(program2);
|
|
1210
|
+
const listResp = await agent.com.atproto.repo.createRecord({
|
|
1211
|
+
repo: agent.session.did,
|
|
1212
|
+
collection: "app.bsky.graph.list",
|
|
1213
|
+
record: {
|
|
1214
|
+
$type: "app.bsky.graph.list",
|
|
1215
|
+
name: opts.name,
|
|
1216
|
+
purpose: "app.bsky.graph.defs#modlist",
|
|
1217
|
+
description: opts.desc,
|
|
1218
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1219
|
+
}
|
|
1220
|
+
});
|
|
1221
|
+
console.log("List created successfully. URI:", listResp.data.uri);
|
|
1222
|
+
for (const handle of handles) {
|
|
1223
|
+
const did = await resolveDid2(agent, handle);
|
|
1224
|
+
await agent.com.atproto.repo.createRecord({
|
|
1225
|
+
repo: agent.session.did,
|
|
1226
|
+
collection: "app.bsky.graph.listitem",
|
|
1227
|
+
record: {
|
|
1228
|
+
$type: "app.bsky.graph.listitem",
|
|
1229
|
+
subject: did,
|
|
1230
|
+
list: listResp.data.uri,
|
|
1231
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1232
|
+
}
|
|
1233
|
+
});
|
|
1234
|
+
console.log("User added to moderation list successfully.");
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// src/commands/bookmark.ts
|
|
1241
|
+
import { AppBskyFeedDefs } from "@atproto/api";
|
|
1242
|
+
function registerBookmarks(program2) {
|
|
1243
|
+
const bookmarks = program2.command("bookmarks").description("Manage bookmarks");
|
|
1244
|
+
bookmarks.command("create").description("Bookmark a post").argument("<uri...>", "Post URI(s) to bookmark").action(async (uris) => {
|
|
1245
|
+
const agent = await getClient(program2);
|
|
1246
|
+
for (const uri of uris) {
|
|
1247
|
+
const atUri = uri.startsWith("at://") ? uri : `at://did:plc:${uri}`;
|
|
1248
|
+
const parts = atUri.split("/");
|
|
1249
|
+
const rkey = parts[parts.length - 1];
|
|
1250
|
+
const collection = parts[parts.length - 2];
|
|
1251
|
+
const did = parts[2];
|
|
1252
|
+
const record = await agent.com.atproto.repo.getRecord({
|
|
1253
|
+
repo: did,
|
|
1254
|
+
collection,
|
|
1255
|
+
rkey
|
|
1256
|
+
});
|
|
1257
|
+
await agent.app.bsky.bookmark.createBookmark({
|
|
1258
|
+
uri: record.data.uri,
|
|
1259
|
+
cid: record.data.cid
|
|
1260
|
+
});
|
|
1261
|
+
console.log(record.data.uri);
|
|
1262
|
+
}
|
|
1263
|
+
});
|
|
1264
|
+
bookmarks.command("delete").description("Remove a bookmark").argument("<uri...>", "Post URI(s) to unbookmark").action(async (uris) => {
|
|
1265
|
+
const agent = await getClient(program2);
|
|
1266
|
+
for (const uri of uris) {
|
|
1267
|
+
const atUri = uri.startsWith("at://") ? uri : `at://did:plc:${uri}`;
|
|
1268
|
+
await agent.app.bsky.bookmark.deleteBookmark({ uri: atUri });
|
|
1269
|
+
console.log(atUri);
|
|
1270
|
+
}
|
|
1271
|
+
});
|
|
1272
|
+
bookmarks.command("get").description("List bookmarked posts").option("-n, --count <number>", "Number of bookmarks to show", "50").action(async (opts) => {
|
|
1273
|
+
const agent = await getClient(program2);
|
|
1274
|
+
const json = isJson(program2);
|
|
1275
|
+
const limit = parseInt(opts.count, 10);
|
|
1276
|
+
let cursor;
|
|
1277
|
+
let remaining = limit;
|
|
1278
|
+
do {
|
|
1279
|
+
const resp = await agent.app.bsky.bookmark.getBookmarks({
|
|
1280
|
+
limit: Math.min(remaining, 50),
|
|
1281
|
+
cursor
|
|
1282
|
+
});
|
|
1283
|
+
for (const bookmark of resp.data.bookmarks) {
|
|
1284
|
+
if (json) {
|
|
1285
|
+
outputJson(bookmark);
|
|
1286
|
+
} else if (AppBskyFeedDefs.isPostView(bookmark.item)) {
|
|
1287
|
+
printPost(bookmark.item);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
remaining -= resp.data.bookmarks.length;
|
|
1291
|
+
cursor = resp.data.cursor;
|
|
1292
|
+
} while (cursor && remaining > 0);
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
// src/commands/password.ts
|
|
1297
|
+
function registerAppPassword(program2) {
|
|
1298
|
+
const appPw = program2.command("app-password").description("Manage app passwords");
|
|
1299
|
+
appPw.command("list").description("List app passwords").action(async () => {
|
|
1300
|
+
const agent = await getClient(program2);
|
|
1301
|
+
const json = isJson(program2);
|
|
1302
|
+
const resp = await agent.com.atproto.server.listAppPasswords();
|
|
1303
|
+
const passwords = resp.data.passwords;
|
|
1304
|
+
if (json) {
|
|
1305
|
+
for (const pw of passwords) outputJson(pw);
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
for (const pw of passwords) {
|
|
1309
|
+
console.log(`${pw.name} (${pw.createdAt})`);
|
|
1310
|
+
}
|
|
1311
|
+
});
|
|
1312
|
+
appPw.command("add").description("Create an app password").argument("<name>", "Password name").action(async (name) => {
|
|
1313
|
+
const agent = await getClient(program2);
|
|
1314
|
+
const json = isJson(program2);
|
|
1315
|
+
const resp = await agent.com.atproto.server.createAppPassword({
|
|
1316
|
+
name
|
|
1317
|
+
});
|
|
1318
|
+
if (json) {
|
|
1319
|
+
outputJson(resp.data);
|
|
1320
|
+
} else {
|
|
1321
|
+
console.log(`${resp.data.name}: ${resp.data.password}`);
|
|
1322
|
+
}
|
|
1323
|
+
});
|
|
1324
|
+
appPw.command("revoke").description("Revoke an app password").argument("<name>", "Password name").action(async (name) => {
|
|
1325
|
+
const agent = await getClient(program2);
|
|
1326
|
+
await agent.com.atproto.server.revokeAppPassword({ name });
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// src/commands/invite.ts
|
|
1331
|
+
import chalk6 from "chalk";
|
|
1332
|
+
function registerInviteCodes(program2) {
|
|
1333
|
+
program2.command("invite-codes").description("Show invite codes").option("--used", "Show used codes too").action(async (opts) => {
|
|
1334
|
+
const agent = await getClient(program2);
|
|
1335
|
+
const json = isJson(program2);
|
|
1336
|
+
const resp = await agent.com.atproto.server.getAccountInviteCodes({
|
|
1337
|
+
includeUsed: opts.used ?? false
|
|
1338
|
+
});
|
|
1339
|
+
const codes = resp.data.codes;
|
|
1340
|
+
if (json) {
|
|
1341
|
+
for (const c of codes) outputJson(c);
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
for (const c of codes) {
|
|
1345
|
+
if (c.uses.length >= c.available) {
|
|
1346
|
+
console.log(chalk6.magentaBright(`${c.code} (used)`));
|
|
1347
|
+
} else {
|
|
1348
|
+
console.log(c.code);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
// src/commands/completions.ts
|
|
1355
|
+
function getCommands(program2) {
|
|
1356
|
+
return program2.commands.map((cmd) => ({
|
|
1357
|
+
name: cmd.name(),
|
|
1358
|
+
aliases: cmd.aliases(),
|
|
1359
|
+
description: cmd.description(),
|
|
1360
|
+
options: cmd.options.map((opt) => ({
|
|
1361
|
+
short: opt.short?.replace(/^-/, "") || void 0,
|
|
1362
|
+
long: opt.long?.replace(/^--/, "") || void 0,
|
|
1363
|
+
description: opt.description
|
|
1364
|
+
}))
|
|
1365
|
+
}));
|
|
1366
|
+
}
|
|
1367
|
+
function getGlobalOptions(program2) {
|
|
1368
|
+
return program2.options.map((opt) => ({
|
|
1369
|
+
short: opt.short?.replace(/^-/, "") || void 0,
|
|
1370
|
+
long: opt.long?.replace(/^--/, "") || void 0,
|
|
1371
|
+
description: opt.description
|
|
1372
|
+
}));
|
|
1373
|
+
}
|
|
1374
|
+
function optionFlag(opt) {
|
|
1375
|
+
return opt.long ? `--${opt.long}` : `-${opt.short}`;
|
|
1376
|
+
}
|
|
1377
|
+
function generateBash(program2) {
|
|
1378
|
+
const commands = getCommands(program2);
|
|
1379
|
+
const globalOpts = getGlobalOptions(program2);
|
|
1380
|
+
const cmdNames = commands.flatMap((c) => [c.name, ...c.aliases]).join(" ");
|
|
1381
|
+
const globalFlags = globalOpts.map((o) => optionFlag(o)).join(" ");
|
|
1382
|
+
const caseClauses = commands.map((cmd) => {
|
|
1383
|
+
const names = [cmd.name, ...cmd.aliases];
|
|
1384
|
+
const opts = cmd.options.map((o) => optionFlag(o)).join(" ");
|
|
1385
|
+
return names.map((n) => ` ${n}) opts="${opts}" ;;`).join("\n");
|
|
1386
|
+
}).join("\n");
|
|
1387
|
+
const subcommandClauses = commands.filter((cmd) => {
|
|
1388
|
+
const programCmd = program2.commands.find((c) => c.name() === cmd.name);
|
|
1389
|
+
return programCmd && programCmd.commands.length > 0;
|
|
1390
|
+
}).map((cmd) => {
|
|
1391
|
+
const programCmd = program2.commands.find((c) => c.name() === cmd.name);
|
|
1392
|
+
const subNames = programCmd.commands.map((sub) => sub.name()).join(" ");
|
|
1393
|
+
return ` ${cmd.name}) opts="${subNames}" ;;`;
|
|
1394
|
+
}).join("\n");
|
|
1395
|
+
return `#!/bin/bash
|
|
1396
|
+
|
|
1397
|
+
_bsky_completions() {
|
|
1398
|
+
local cur prev commands global_opts opts
|
|
1399
|
+
COMPREPLY=()
|
|
1400
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
1401
|
+
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
1402
|
+
commands="${cmdNames}"
|
|
1403
|
+
global_opts="${globalFlags} --help --version"
|
|
1404
|
+
|
|
1405
|
+
if [[ \${COMP_CWORD} -eq 1 ]]; then
|
|
1406
|
+
COMPREPLY=( $(compgen -W "\${commands} \${global_opts}" -- "\${cur}") )
|
|
1407
|
+
return 0
|
|
1408
|
+
fi
|
|
1409
|
+
|
|
1410
|
+
case "\${COMP_WORDS[1]}" in
|
|
1411
|
+
${caseClauses}
|
|
1412
|
+
${subcommandClauses}
|
|
1413
|
+
*) opts="" ;;
|
|
1414
|
+
esac
|
|
1415
|
+
|
|
1416
|
+
COMPREPLY=( $(compgen -W "\${opts} --help" -- "\${cur}") )
|
|
1417
|
+
return 0
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
complete -F _bsky_completions bsky
|
|
1421
|
+
`;
|
|
1422
|
+
}
|
|
1423
|
+
function generateZsh(program2) {
|
|
1424
|
+
const commands = getCommands(program2);
|
|
1425
|
+
const globalOpts = getGlobalOptions(program2);
|
|
1426
|
+
const cmdList = commands.map((c) => {
|
|
1427
|
+
const desc = c.description.replace(/'/g, "'\\''");
|
|
1428
|
+
return ` '${c.name}:${desc}'`;
|
|
1429
|
+
}).join("\n");
|
|
1430
|
+
const globalOptsList = globalOpts.map((o) => {
|
|
1431
|
+
const desc = o.description.replace(/'/g, "'\\''");
|
|
1432
|
+
return ` '${optionFlag(o)}[${desc}]'`;
|
|
1433
|
+
}).join("\n");
|
|
1434
|
+
const subcmdFunctions = commands.map((cmd) => {
|
|
1435
|
+
const opts = cmd.options.map((o) => {
|
|
1436
|
+
const desc = o.description.replace(/'/g, "'\\''");
|
|
1437
|
+
return ` '${optionFlag(o)}[${desc}]'`;
|
|
1438
|
+
}).join("\n");
|
|
1439
|
+
const programCmd = program2.commands.find((c) => c.name() === cmd.name);
|
|
1440
|
+
const subCommands = programCmd?.commands ?? [];
|
|
1441
|
+
if (subCommands.length > 0) {
|
|
1442
|
+
const subList = subCommands.map((sub) => {
|
|
1443
|
+
const desc = sub.description().replace(/'/g, "'\\''");
|
|
1444
|
+
return ` '${sub.name()}:${desc}'`;
|
|
1445
|
+
}).join("\n");
|
|
1446
|
+
return ` ${cmd.name})
|
|
1447
|
+
local -a subcommands
|
|
1448
|
+
subcommands=(
|
|
1449
|
+
${subList}
|
|
1450
|
+
)
|
|
1451
|
+
_describe 'subcommand' subcommands
|
|
1452
|
+
;;`;
|
|
1453
|
+
}
|
|
1454
|
+
if (!opts) return ` ${cmd.name}) ;;`;
|
|
1455
|
+
return ` ${cmd.name})
|
|
1456
|
+
_arguments \\
|
|
1457
|
+
${opts}
|
|
1458
|
+
;;`;
|
|
1459
|
+
}).join("\n");
|
|
1460
|
+
return `#compdef bsky
|
|
1461
|
+
|
|
1462
|
+
_bsky() {
|
|
1463
|
+
local -a commands
|
|
1464
|
+
commands=(
|
|
1465
|
+
${cmdList}
|
|
1466
|
+
)
|
|
1467
|
+
|
|
1468
|
+
_arguments \\
|
|
1469
|
+
${globalOptsList} \\
|
|
1470
|
+
'1:command:->command' \\
|
|
1471
|
+
'*::arg:->args'
|
|
1472
|
+
|
|
1473
|
+
case $state in
|
|
1474
|
+
command)
|
|
1475
|
+
_describe 'command' commands
|
|
1476
|
+
;;
|
|
1477
|
+
args)
|
|
1478
|
+
case $words[1] in
|
|
1479
|
+
${subcmdFunctions}
|
|
1480
|
+
esac
|
|
1481
|
+
;;
|
|
1482
|
+
esac
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
_bsky "$@"
|
|
1486
|
+
`;
|
|
1487
|
+
}
|
|
1488
|
+
function generateFish(program2) {
|
|
1489
|
+
const commands = getCommands(program2);
|
|
1490
|
+
const globalOpts = getGlobalOptions(program2);
|
|
1491
|
+
const lines = [
|
|
1492
|
+
"# Fish completions for bsky",
|
|
1493
|
+
"# Generated by: bsky completions fish",
|
|
1494
|
+
"",
|
|
1495
|
+
"# Disable file completions by default",
|
|
1496
|
+
"complete -c bsky -f",
|
|
1497
|
+
""
|
|
1498
|
+
];
|
|
1499
|
+
for (const opt of globalOpts) {
|
|
1500
|
+
const desc = opt.description.replace(/'/g, "\\'");
|
|
1501
|
+
const parts = [`complete -c bsky -n '__fish_use_subcommand'`];
|
|
1502
|
+
if (opt.long) parts.push(`-l ${opt.long}`);
|
|
1503
|
+
if (opt.short) parts.push(`-s ${opt.short}`);
|
|
1504
|
+
parts.push(`-d '${desc}'`);
|
|
1505
|
+
lines.push(parts.join(" "));
|
|
1506
|
+
}
|
|
1507
|
+
lines.push("");
|
|
1508
|
+
for (const cmd of commands) {
|
|
1509
|
+
const desc = cmd.description.replace(/'/g, "\\'");
|
|
1510
|
+
lines.push(
|
|
1511
|
+
`complete -c bsky -n '__fish_use_subcommand' -a ${cmd.name} -d '${desc}'`
|
|
1512
|
+
);
|
|
1513
|
+
for (const alias of cmd.aliases) {
|
|
1514
|
+
lines.push(
|
|
1515
|
+
`complete -c bsky -n '__fish_use_subcommand' -a ${alias} -d '${desc}'`
|
|
1516
|
+
);
|
|
1517
|
+
}
|
|
1518
|
+
for (const opt of cmd.options) {
|
|
1519
|
+
const optDesc = opt.description.replace(/'/g, "\\'");
|
|
1520
|
+
const parts = [`complete -c bsky -n '__fish_seen_subcommand_from ${cmd.name}'`];
|
|
1521
|
+
if (opt.long) parts.push(`-l ${opt.long}`);
|
|
1522
|
+
if (opt.short) parts.push(`-s ${opt.short}`);
|
|
1523
|
+
parts.push(`-d '${optDesc}'`);
|
|
1524
|
+
lines.push(parts.join(" "));
|
|
1525
|
+
}
|
|
1526
|
+
const programCmd = program2.commands.find((c) => c.name() === cmd.name);
|
|
1527
|
+
if (programCmd && programCmd.commands.length > 0) {
|
|
1528
|
+
for (const sub of programCmd.commands) {
|
|
1529
|
+
const subDesc = sub.description().replace(/'/g, "\\'");
|
|
1530
|
+
lines.push(
|
|
1531
|
+
`complete -c bsky -n '__fish_seen_subcommand_from ${cmd.name}' -a ${sub.name()} -d '${subDesc}'`
|
|
1532
|
+
);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
lines.push("");
|
|
1537
|
+
return lines.join("\n");
|
|
1538
|
+
}
|
|
1539
|
+
function registerCompletions(program2) {
|
|
1540
|
+
program2.command("completions").description("Generate shell completion script").argument("<shell>", "Shell type: bash, zsh, or fish").action((shell) => {
|
|
1541
|
+
switch (shell) {
|
|
1542
|
+
case "bash":
|
|
1543
|
+
process.stdout.write(generateBash(program2));
|
|
1544
|
+
break;
|
|
1545
|
+
case "zsh":
|
|
1546
|
+
process.stdout.write(generateZsh(program2));
|
|
1547
|
+
break;
|
|
1548
|
+
case "fish":
|
|
1549
|
+
process.stdout.write(generateFish(program2));
|
|
1550
|
+
break;
|
|
1551
|
+
default:
|
|
1552
|
+
console.error(
|
|
1553
|
+
`Unknown shell: ${shell}. Supported: bash, zsh, fish`
|
|
1554
|
+
);
|
|
1555
|
+
process.exit(1);
|
|
1556
|
+
}
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
// src/index.ts
|
|
1561
|
+
var program = new Command();
|
|
1562
|
+
program.name("bsky").description("A CLI client for Bluesky").version("1.0.0").option("--json", "Output as JSON").option("-p, --profile <name>", "Profile name").option("-v, --verbose", "Verbose output");
|
|
1563
|
+
function resolveProfile(program2) {
|
|
1564
|
+
return program2.opts().profile ?? process.env.BSKY_PROFILE;
|
|
1565
|
+
}
|
|
1566
|
+
async function getClient(program2) {
|
|
1567
|
+
const profile = resolveProfile(program2);
|
|
1568
|
+
const config = await loadConfig(profile);
|
|
1569
|
+
const prefix = profile ? `${profile}-` : "";
|
|
1570
|
+
return createClient(config, prefix);
|
|
1571
|
+
}
|
|
1572
|
+
async function getConfig(program2) {
|
|
1573
|
+
const profile = resolveProfile(program2);
|
|
1574
|
+
return loadConfig(profile);
|
|
1575
|
+
}
|
|
1576
|
+
function isJson(program2) {
|
|
1577
|
+
return program2.opts().json === true;
|
|
1578
|
+
}
|
|
1579
|
+
registerLogin(program);
|
|
1580
|
+
registerTimeline(program);
|
|
1581
|
+
registerStream(program);
|
|
1582
|
+
registerThread(program);
|
|
1583
|
+
registerPost(program);
|
|
1584
|
+
registerReply(program);
|
|
1585
|
+
registerQuote(program);
|
|
1586
|
+
registerDelete(program);
|
|
1587
|
+
registerLike(program);
|
|
1588
|
+
registerLikes(program);
|
|
1589
|
+
registerRepost(program);
|
|
1590
|
+
registerReposts(program);
|
|
1591
|
+
registerFollow(program);
|
|
1592
|
+
registerUnfollow(program);
|
|
1593
|
+
registerFollows(program);
|
|
1594
|
+
registerFollowers(program);
|
|
1595
|
+
registerBlock(program);
|
|
1596
|
+
registerUnblock(program);
|
|
1597
|
+
registerBlocks(program);
|
|
1598
|
+
registerMute(program);
|
|
1599
|
+
registerSearch(program);
|
|
1600
|
+
registerSearchUsers(program);
|
|
1601
|
+
registerProfile(program);
|
|
1602
|
+
registerProfileUpdate(program);
|
|
1603
|
+
registerSession(program);
|
|
1604
|
+
registerNotifs(program);
|
|
1605
|
+
registerReport(program);
|
|
1606
|
+
registerModList(program);
|
|
1607
|
+
registerBookmarks(program);
|
|
1608
|
+
registerAppPassword(program);
|
|
1609
|
+
registerInviteCodes(program);
|
|
1610
|
+
registerCompletions(program);
|
|
1611
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
1612
|
+
console.error(err.message ?? err);
|
|
1613
|
+
process.exit(1);
|
|
1614
|
+
});
|
|
1615
|
+
export {
|
|
1616
|
+
getClient,
|
|
1617
|
+
getConfig,
|
|
1618
|
+
isJson
|
|
1619
|
+
};
|
|
1620
|
+
//# sourceMappingURL=index.js.map
|