@getcirrus/pds 0.10.6 → 0.12.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/dist/cli.js CHANGED
@@ -8,14 +8,15 @@ import { spawn } from "node:child_process";
8
8
  import { experimental_patchConfig, experimental_readRawConfig } from "wrangler";
9
9
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
10
10
  import { join, resolve } from "node:path";
11
+ import { writeFile } from "node:fs/promises";
11
12
  import pc from "picocolors";
12
13
  import QRCode from "qrcode";
13
14
  import { Client, ClientResponseError, ok } from "@atcute/client";
14
15
  import "@atcute/bluesky";
15
16
  import "@atcute/atproto";
16
- import { writeFile } from "node:fs/promises";
17
17
  import { CompositeDidDocumentResolver, DohJsonHandleResolver, PlcDidDocumentResolver, WebDidDocumentResolver } from "@atcute/identity-resolver";
18
18
  import { getPdsEndpoint } from "@atcute/identity";
19
+ import { decodeAll } from "@atproto/lex-cbor";
19
20
 
20
21
  //#region src/cli/utils/wrangler.ts
21
22
  /**
@@ -276,159 +277,492 @@ function setDevVar(key, value, dir = process.cwd()) {
276
277
  }
277
278
 
278
279
  //#endregion
279
- //#region src/cli/utils/secrets.ts
280
+ //#region src/cli/utils/cli-helpers.ts
280
281
  /**
281
- * Secret generation and management utilities for PDS CLI
282
+ * Shared CLI utilities for PDS commands
282
283
  */
283
284
  /**
284
- * Generate a new secp256k1 signing keypair
285
+ * Prompt for text input, exiting on cancel
285
286
  */
286
- async function generateSigningKeypair() {
287
- const keypair = await Secp256k1Keypair.create({ exportable: true });
288
- return {
289
- privateKey: Buffer.from(await keypair.export()).toString("hex"),
290
- publicKey: keypair.did().replace("did:key:", "")
291
- };
287
+ async function promptText(options) {
288
+ const result = await p.text(options);
289
+ if (p.isCancel(result)) {
290
+ p.cancel("Cancelled");
291
+ process.exit(0);
292
+ }
293
+ return result;
292
294
  }
293
295
  /**
294
- * Derive public key from an existing private key
296
+ * Prompt for confirmation, exiting on cancel
295
297
  */
296
- async function derivePublicKey(privateKeyHex) {
297
- return (await Secp256k1Keypair.import(privateKeyHex)).did().replace("did:key:", "");
298
+ async function promptConfirm(options) {
299
+ const result = await p.confirm(options);
300
+ if (p.isCancel(result)) {
301
+ p.cancel("Cancelled");
302
+ process.exit(0);
303
+ }
304
+ return result;
298
305
  }
299
306
  /**
300
- * Generate a random auth token (base64url, 32 bytes)
307
+ * Prompt for selection, exiting on cancel
301
308
  */
302
- function generateAuthToken() {
303
- return randomBytes(32).toString("base64url");
309
+ async function promptSelect(options) {
310
+ const result = await p.select(options);
311
+ if (p.isCancel(result)) {
312
+ p.cancel("Cancelled");
313
+ process.exit(0);
314
+ }
315
+ return result;
304
316
  }
305
317
  /**
306
- * Generate a random JWT secret (base64, 32 bytes)
318
+ * Get target PDS URL based on mode
307
319
  */
308
- function generateJwtSecret() {
309
- return randomBytes(32).toString("base64");
320
+ function getTargetUrl(isDev, pdsHostname) {
321
+ if (isDev) return `http://localhost:${process.env.PORT ? parseInt(process.env.PORT) ?? "5173" : "5173"}`;
322
+ if (!pdsHostname) throw new Error("PDS_HOSTNAME not configured in wrangler.jsonc");
323
+ return `https://${pdsHostname}`;
310
324
  }
311
325
  /**
312
- * Hash a password using bcrypt
326
+ * Extract domain from URL
313
327
  */
314
- async function hashPassword(password) {
315
- return bcrypt.hash(password, 10);
328
+ function getDomain(url) {
329
+ try {
330
+ return new URL(url).hostname;
331
+ } catch {
332
+ return url;
333
+ }
316
334
  }
317
335
  /**
318
- * Prompt for password with confirmation (max 3 attempts)
336
+ * Detect which package manager is being used based on npm_config_user_agent
319
337
  */
320
- async function promptPassword(handle) {
321
- const message = handle ? `Choose a password for @${handle}:` : "Enter password:";
322
- const MAX_ATTEMPTS = 3;
323
- let attempts = 0;
324
- while (attempts < MAX_ATTEMPTS) {
325
- attempts++;
326
- const password = await p.password({ message });
327
- if (p.isCancel(password)) {
328
- p.cancel("Cancelled");
329
- process.exit(0);
330
- }
331
- const confirm = await p.password({ message: "Confirm password:" });
332
- if (p.isCancel(confirm)) {
333
- p.cancel("Cancelled");
334
- process.exit(0);
335
- }
336
- if (password === confirm) return password;
337
- p.log.error("Passwords do not match. Try again.");
338
- }
339
- p.log.error("Too many failed attempts.");
340
- p.cancel("Password setup cancelled");
341
- process.exit(1);
338
+ function detectPackageManager() {
339
+ const userAgent = process.env.npm_config_user_agent || "";
340
+ if (userAgent.startsWith("yarn")) return "yarn";
341
+ if (userAgent.startsWith("pnpm")) return "pnpm";
342
+ if (userAgent.startsWith("bun")) return "bun";
343
+ return "npm";
342
344
  }
343
345
  /**
344
- * Set a secret value, either locally (.dev.vars) or via wrangler
346
+ * Format a command for the detected package manager
347
+ * npm always needs "run" for scripts, pnpm/yarn/bun can use shorthand
348
+ * except for "deploy" which conflicts with pnpm's built-in deploy command
345
349
  */
346
- async function setSecretValue(name, value, local) {
347
- if (local) setDevVar(name, value);
348
- else await setSecret(name, value);
350
+ function formatCommand(pm, ...args) {
351
+ if (pm === "npm" || args[0] === "deploy") return `${pm} run ${args.join(" ")}`;
352
+ return `${pm} ${args.join(" ")}`;
349
353
  }
350
354
  /**
351
- * Set a public var in wrangler.jsonc
355
+ * Copy text to clipboard using platform-specific command
356
+ * Falls back gracefully if clipboard is unavailable
352
357
  */
353
- function setPublicVar(name, value, local) {
354
- if (local) setDevVar(name, value);
355
- else setVar(name, value);
358
+ async function copyToClipboard(text) {
359
+ const platform = process.platform;
360
+ let cmd;
361
+ let args;
362
+ if (platform === "darwin") {
363
+ cmd = "pbcopy";
364
+ args = [];
365
+ } else if (platform === "linux") {
366
+ cmd = "xclip";
367
+ args = ["-selection", "clipboard"];
368
+ } else if (platform === "win32") {
369
+ cmd = "clip";
370
+ args = [];
371
+ } else return false;
372
+ return new Promise((resolve$1) => {
373
+ const child = spawn(cmd, args, { stdio: [
374
+ "pipe",
375
+ "ignore",
376
+ "ignore"
377
+ ] });
378
+ child.on("error", () => resolve$1(false));
379
+ child.on("close", (code) => resolve$1(code === 0));
380
+ child.stdin?.write(text);
381
+ child.stdin?.end();
382
+ });
356
383
  }
357
-
358
- //#endregion
359
- //#region src/cli/commands/secret/jwt.ts
360
384
  /**
361
- * JWT secret generation command
385
+ * Check if 1Password CLI (op) is available
386
+ * Only checks on POSIX systems (macOS, Linux)
362
387
  */
363
- const jwtCommand = defineCommand({
364
- meta: {
365
- name: "jwt",
366
- description: "Generate and set JWT signing secret"
367
- },
368
- args: { local: {
369
- type: "boolean",
370
- description: "Write to .dev.vars instead of wrangler secrets",
371
- default: false
372
- } },
373
- async run({ args }) {
374
- p.intro("Generate JWT Secret");
375
- const secret = generateJwtSecret();
376
- try {
377
- await setSecretValue("JWT_SECRET", secret, args.local);
378
- p.outro(args.local ? "JWT_SECRET written to .dev.vars" : "Done!");
379
- } catch (error) {
380
- p.log.error(String(error));
381
- process.exit(1);
382
- }
383
- }
384
- });
385
-
386
- //#endregion
387
- //#region src/cli/commands/secret/password.ts
388
+ async function is1PasswordAvailable() {
389
+ if (process.platform === "win32") return false;
390
+ return new Promise((resolve$1) => {
391
+ const child = spawn("which", ["op"], { stdio: [
392
+ "ignore",
393
+ "pipe",
394
+ "ignore"
395
+ ] });
396
+ child.on("error", () => resolve$1(false));
397
+ child.on("close", (code) => resolve$1(code === 0));
398
+ });
399
+ }
388
400
  /**
389
- * Password hash generation command
401
+ * Save a key to 1Password using the CLI
402
+ * Creates a secure note with the signing key
390
403
  */
391
- const passwordCommand = defineCommand({
392
- meta: {
393
- name: "password",
394
- description: "Set account password (stored as bcrypt hash)"
395
- },
396
- args: { local: {
397
- type: "boolean",
398
- description: "Write to .dev.vars instead of wrangler secrets",
399
- default: false
400
- } },
401
- async run({ args }) {
402
- p.intro("Set Account Password");
403
- const password = await promptPassword();
404
- const spinner = p.spinner();
405
- spinner.start("Hashing password...");
406
- const passwordHash = await hashPassword(password);
407
- spinner.stop("Password hashed");
408
- try {
409
- await setSecretValue("PASSWORD_HASH", passwordHash, args.local);
410
- p.outro(args.local ? "PASSWORD_HASH written to .dev.vars" : "Done!");
411
- } catch (error) {
412
- p.log.error(String(error));
413
- process.exit(1);
414
- }
415
- }
416
- });
417
-
418
- //#endregion
419
- //#region src/cli/commands/secret/key.ts
404
+ async function saveTo1Password(key, handle) {
405
+ const itemName = `Cirrus PDS Signing Key - ${handle}`;
406
+ return new Promise((resolve$1) => {
407
+ const child = spawn("op", [
408
+ "item",
409
+ "create",
410
+ "--category",
411
+ "Secure Note",
412
+ "--title",
413
+ itemName,
414
+ `notesPlain=CIRRUS PDS SIGNING KEY\n\nHandle: ${handle}\nCreated: ${(/* @__PURE__ */ new Date()).toISOString()}\n\nWARNING: This key controls your identity!\n\nSIGNING KEY:\n${key}`,
415
+ "--tags",
416
+ "cirrus,pds,signing-key"
417
+ ], { stdio: [
418
+ "ignore",
419
+ "pipe",
420
+ "pipe"
421
+ ] });
422
+ let stderr = "";
423
+ child.stderr?.on("data", (data) => {
424
+ stderr += data.toString();
425
+ });
426
+ child.on("error", (err) => {
427
+ resolve$1({
428
+ success: false,
429
+ error: err.message
430
+ });
431
+ });
432
+ child.on("close", (code) => {
433
+ if (code === 0) resolve$1({
434
+ success: true,
435
+ itemName
436
+ });
437
+ else resolve$1({
438
+ success: false,
439
+ error: stderr || `1Password CLI exited with code ${code}`
440
+ });
441
+ });
442
+ });
443
+ }
420
444
  /**
421
- * Signing key generation command
445
+ * Save a password to 1Password as a Login item for bsky.app
422
446
  */
423
- const keyCommand = defineCommand({
424
- meta: {
425
- name: "key",
426
- description: "Generate and set signing keypair"
427
- },
428
- args: { local: {
429
- type: "boolean",
430
- description: "Write to .dev.vars instead of wrangler secrets/config",
431
- default: false
447
+ async function savePasswordTo1Password(password, handle) {
448
+ const itemName = `Bluesky - @${handle}`;
449
+ return new Promise((resolve$1) => {
450
+ const child = spawn("op", [
451
+ "item",
452
+ "create",
453
+ "--category",
454
+ "Login",
455
+ "--title",
456
+ itemName,
457
+ `username=${handle}`,
458
+ `password=${password}`,
459
+ "--url=https://bsky.app",
460
+ "--tags",
461
+ "cirrus,pds,bluesky"
462
+ ], { stdio: [
463
+ "ignore",
464
+ "pipe",
465
+ "pipe"
466
+ ] });
467
+ let stderr = "";
468
+ child.stderr?.on("data", (data) => {
469
+ stderr += data.toString();
470
+ });
471
+ child.on("error", (err) => {
472
+ resolve$1({
473
+ success: false,
474
+ error: err.message
475
+ });
476
+ });
477
+ child.on("close", (code) => {
478
+ if (code === 0) resolve$1({
479
+ success: true,
480
+ itemName
481
+ });
482
+ else resolve$1({
483
+ success: false,
484
+ error: stderr || `1Password CLI exited with code ${code}`
485
+ });
486
+ });
487
+ });
488
+ }
489
+ /**
490
+ * Run a shell command and return a promise.
491
+ * Captures output and throws on non-zero exit code.
492
+ * Use this for running npm/pnpm/yarn scripts etc.
493
+ */
494
+ function runCommand(cmd, args, options = {}) {
495
+ return new Promise((resolve$1, reject) => {
496
+ const child = spawn(cmd, args, { stdio: options.stream ? "inherit" : "pipe" });
497
+ let output = "";
498
+ if (!options.stream) {
499
+ child.stdout?.on("data", (data) => {
500
+ output += data.toString();
501
+ });
502
+ child.stderr?.on("data", (data) => {
503
+ output += data.toString();
504
+ });
505
+ }
506
+ child.on("close", (code) => {
507
+ if (code === 0) resolve$1();
508
+ else {
509
+ if (output && !options.stream) console.error(output);
510
+ reject(/* @__PURE__ */ new Error(`${cmd} ${args.join(" ")} failed with code ${code}`));
511
+ }
512
+ });
513
+ child.on("error", reject);
514
+ });
515
+ }
516
+ /**
517
+ * Save a key backup file with appropriate warnings
518
+ */
519
+ async function saveKeyBackup(key, handle) {
520
+ const filename = `signing-key-backup-${handle.replace(/[^a-z0-9]/gi, "-")}.txt`;
521
+ const filepath = join(process.cwd(), filename);
522
+ await writeFile(filepath, [
523
+ "=".repeat(60),
524
+ "CIRRUS PDS SIGNING KEY BACKUP",
525
+ "=".repeat(60),
526
+ "",
527
+ `Handle: ${handle}`,
528
+ `Created: ${(/* @__PURE__ */ new Date()).toISOString()}`,
529
+ "",
530
+ "WARNING: This key controls your identity!",
531
+ "- Store this file in a secure location (password manager, encrypted drive)",
532
+ "- Delete this file from your local disk after backing up",
533
+ "- Never share this key with anyone",
534
+ "- If compromised, your identity can be stolen",
535
+ "",
536
+ "=".repeat(60),
537
+ "SIGNING KEY (hex-encoded secp256k1 private key)",
538
+ "=".repeat(60),
539
+ "",
540
+ key,
541
+ "",
542
+ "=".repeat(60)
543
+ ].join("\n"), { mode: 384 });
544
+ return filepath;
545
+ }
546
+
547
+ //#endregion
548
+ //#region src/cli/utils/secrets.ts
549
+ /**
550
+ * Secret generation and management utilities for PDS CLI
551
+ */
552
+ /**
553
+ * Generate a new secp256k1 signing keypair
554
+ */
555
+ async function generateSigningKeypair() {
556
+ const keypair = await Secp256k1Keypair.create({ exportable: true });
557
+ return {
558
+ privateKey: Buffer.from(await keypair.export()).toString("hex"),
559
+ publicKey: keypair.did().replace("did:key:", "")
560
+ };
561
+ }
562
+ /**
563
+ * Derive public key from an existing private key
564
+ */
565
+ async function derivePublicKey(privateKeyHex) {
566
+ return (await Secp256k1Keypair.import(privateKeyHex)).did().replace("did:key:", "");
567
+ }
568
+ /**
569
+ * Generate a random auth token (base64url, 32 bytes)
570
+ */
571
+ function generateAuthToken() {
572
+ return randomBytes(32).toString("base64url");
573
+ }
574
+ /**
575
+ * Generate a random JWT secret (base64, 32 bytes)
576
+ */
577
+ function generateJwtSecret() {
578
+ return randomBytes(32).toString("base64");
579
+ }
580
+ /**
581
+ * Hash a password using bcrypt
582
+ */
583
+ async function hashPassword(password) {
584
+ return bcrypt.hash(password, 10);
585
+ }
586
+ /**
587
+ * Generate a random password (base64url, 24 bytes = 32 chars)
588
+ */
589
+ function generatePassword() {
590
+ return randomBytes(24).toString("base64url");
591
+ }
592
+ /**
593
+ * Prompt for password with confirmation (max 3 attempts),
594
+ * or generate one automatically
595
+ */
596
+ async function promptPassword(handle) {
597
+ if (await promptSelect({
598
+ message: handle ? `Set a password for @${handle}:` : "Set a password:",
599
+ options: [{
600
+ value: "manual",
601
+ label: "Choose a password"
602
+ }, {
603
+ value: "generate",
604
+ label: "Generate one automatically"
605
+ }]
606
+ }) === "generate") {
607
+ const password = generatePassword();
608
+ const has1Password = await is1PasswordAvailable();
609
+ const saveOptions = [];
610
+ if (has1Password) saveOptions.push({
611
+ value: "1password",
612
+ label: "Save to 1Password",
613
+ hint: "as a bsky.app login"
614
+ });
615
+ saveOptions.push({
616
+ value: "clipboard",
617
+ label: "Copy to clipboard",
618
+ hint: "paste into password manager"
619
+ }, {
620
+ value: "show",
621
+ label: "Display it",
622
+ hint: "shown in terminal"
623
+ });
624
+ const saveChoice = await promptSelect({
625
+ message: "Where should we save the password?",
626
+ options: saveOptions
627
+ });
628
+ if (saveChoice === "1password") {
629
+ const spinner = p.spinner();
630
+ spinner.start("Saving to 1Password...");
631
+ const result = await savePasswordTo1Password(password, handle ?? "");
632
+ if (result.success) {
633
+ spinner.stop("Saved to 1Password");
634
+ p.log.success(`Created: "${result.itemName}"`);
635
+ } else {
636
+ spinner.stop("Failed to save to 1Password");
637
+ p.log.error(result.error || "Unknown error");
638
+ if (await copyToClipboard(password)) p.log.info("Copied to clipboard instead");
639
+ else {
640
+ p.note(password, "Generated password");
641
+ p.log.warn("Save this password somewhere safe!");
642
+ }
643
+ }
644
+ } else if (saveChoice === "clipboard") if (await copyToClipboard(password)) p.log.success("Password generated and copied to clipboard");
645
+ else {
646
+ p.note(password, "Generated password");
647
+ p.log.warn("Could not copy to clipboard — save this password somewhere safe!");
648
+ }
649
+ else {
650
+ p.note(password, "Generated password");
651
+ p.log.warn("Save this password somewhere safe!");
652
+ }
653
+ return password;
654
+ }
655
+ const message = handle ? `Choose a password for @${handle}:` : "Enter password:";
656
+ const MAX_ATTEMPTS = 3;
657
+ let attempts = 0;
658
+ while (attempts < MAX_ATTEMPTS) {
659
+ attempts++;
660
+ const password = await p.password({ message });
661
+ if (p.isCancel(password)) {
662
+ p.cancel("Cancelled");
663
+ process.exit(0);
664
+ }
665
+ const confirm = await p.password({ message: "Confirm password:" });
666
+ if (p.isCancel(confirm)) {
667
+ p.cancel("Cancelled");
668
+ process.exit(0);
669
+ }
670
+ if (password === confirm) return password;
671
+ p.log.error("Passwords do not match. Try again.");
672
+ }
673
+ p.log.error("Too many failed attempts.");
674
+ p.cancel("Password setup cancelled");
675
+ process.exit(1);
676
+ }
677
+ /**
678
+ * Set a secret value, either locally (.dev.vars) or via wrangler
679
+ */
680
+ async function setSecretValue(name, value, local) {
681
+ if (local) setDevVar(name, value);
682
+ else await setSecret(name, value);
683
+ }
684
+ /**
685
+ * Set a public var in wrangler.jsonc
686
+ */
687
+ function setPublicVar(name, value, local) {
688
+ if (local) setDevVar(name, value);
689
+ else setVar(name, value);
690
+ }
691
+
692
+ //#endregion
693
+ //#region src/cli/commands/secret/jwt.ts
694
+ /**
695
+ * JWT secret generation command
696
+ */
697
+ const jwtCommand = defineCommand({
698
+ meta: {
699
+ name: "jwt",
700
+ description: "Generate and set JWT signing secret"
701
+ },
702
+ args: { local: {
703
+ type: "boolean",
704
+ description: "Write to .dev.vars instead of wrangler secrets",
705
+ default: false
706
+ } },
707
+ async run({ args }) {
708
+ p.intro("Generate JWT Secret");
709
+ const secret = generateJwtSecret();
710
+ try {
711
+ await setSecretValue("JWT_SECRET", secret, args.local);
712
+ p.outro(args.local ? "JWT_SECRET written to .dev.vars" : "Done!");
713
+ } catch (error) {
714
+ p.log.error(String(error));
715
+ process.exit(1);
716
+ }
717
+ }
718
+ });
719
+
720
+ //#endregion
721
+ //#region src/cli/commands/secret/password.ts
722
+ /**
723
+ * Password hash generation command
724
+ */
725
+ const passwordCommand = defineCommand({
726
+ meta: {
727
+ name: "password",
728
+ description: "Set account password (stored as bcrypt hash)"
729
+ },
730
+ args: { local: {
731
+ type: "boolean",
732
+ description: "Write to .dev.vars instead of wrangler secrets",
733
+ default: false
734
+ } },
735
+ async run({ args }) {
736
+ p.intro("Set Account Password");
737
+ const password = await promptPassword();
738
+ const spinner = p.spinner();
739
+ spinner.start("Hashing password...");
740
+ const passwordHash = await hashPassword(password);
741
+ spinner.stop("Password hashed");
742
+ try {
743
+ await setSecretValue("PASSWORD_HASH", passwordHash, args.local);
744
+ p.outro(args.local ? "PASSWORD_HASH written to .dev.vars" : "Done!");
745
+ } catch (error) {
746
+ p.log.error(String(error));
747
+ process.exit(1);
748
+ }
749
+ }
750
+ });
751
+
752
+ //#endregion
753
+ //#region src/cli/commands/secret/key.ts
754
+ /**
755
+ * Signing key generation command
756
+ */
757
+ const keyCommand = defineCommand({
758
+ meta: {
759
+ name: "key",
760
+ description: "Generate and set signing keypair"
761
+ },
762
+ args: { local: {
763
+ type: "boolean",
764
+ description: "Write to .dev.vars instead of wrangler secrets/config",
765
+ default: false
432
766
  } },
433
767
  async run({ args }) {
434
768
  p.intro("Generate Signing Keypair");
@@ -933,36 +1267,11 @@ var PDSClient = class PDSClient {
933
1267
  */
934
1268
  async listPasskeys() {
935
1269
  const url = new URL("/passkey/list", this.baseUrl);
936
- const headers = {};
937
- if (this.authToken) headers["Authorization"] = `Bearer ${this.authToken}`;
938
- const res = await fetch(url.toString(), {
939
- method: "GET",
940
- headers
941
- });
942
- if (!res.ok) {
943
- const errorBody = await res.json().catch(() => ({}));
944
- throw new ClientResponseError({
945
- status: res.status,
946
- headers: res.headers,
947
- data: {
948
- error: errorBody.error ?? "Unknown",
949
- message: errorBody.message
950
- }
951
- });
952
- }
953
- return res.json();
954
- }
955
- /**
956
- * Delete a passkey by credential ID
957
- */
958
- async deletePasskey(credentialId) {
959
- const url = new URL("/passkey/delete", this.baseUrl);
960
- const headers = { "Content-Type": "application/json" };
1270
+ const headers = {};
961
1271
  if (this.authToken) headers["Authorization"] = `Bearer ${this.authToken}`;
962
1272
  const res = await fetch(url.toString(), {
963
- method: "POST",
964
- headers,
965
- body: JSON.stringify({ id: credentialId })
1273
+ method: "GET",
1274
+ headers
966
1275
  });
967
1276
  if (!res.ok) {
968
1277
  const errorBody = await res.json().catch(() => ({}));
@@ -978,293 +1287,128 @@ var PDSClient = class PDSClient {
978
1287
  return res.json();
979
1288
  }
980
1289
  /**
981
- * Get a migration token for outbound migration.
982
- * This token can be used to migrate to another PDS.
1290
+ * Delete a passkey by credential ID
983
1291
  */
984
- async getMigrationToken() {
985
- const url = new URL("/xrpc/gg.mk.experimental.getMigrationToken", this.baseUrl);
986
- const headers = {};
1292
+ async deletePasskey(credentialId) {
1293
+ const url = new URL("/passkey/delete", this.baseUrl);
1294
+ const headers = { "Content-Type": "application/json" };
987
1295
  if (this.authToken) headers["Authorization"] = `Bearer ${this.authToken}`;
988
1296
  const res = await fetch(url.toString(), {
989
- method: "GET",
990
- headers
991
- });
992
- if (!res.ok) return {
993
- success: false,
994
- error: (await res.json().catch(() => ({}))).message ?? `Request failed: ${res.status}`
995
- };
996
- return {
997
- success: true,
998
- token: (await res.json()).token
999
- };
1000
- }
1001
- static RELAY_URLS = ["https://relay1.us-west.bsky.network", "https://relay1.us-east.bsky.network"];
1002
- /**
1003
- * Get relay's view of this PDS host status from a single relay.
1004
- * Calls com.atproto.sync.getHostStatus on the relay.
1005
- */
1006
- async getRelayHostStatus(pdsHostname, relayUrl) {
1007
- try {
1008
- const url = new URL("/xrpc/com.atproto.sync.getHostStatus", relayUrl);
1009
- url.searchParams.set("hostname", pdsHostname);
1010
- const res = await fetch(url.toString());
1011
- if (!res.ok) return null;
1012
- return {
1013
- ...await res.json(),
1014
- relay: relayUrl
1015
- };
1016
- } catch {
1017
- return null;
1018
- }
1019
- }
1020
- /**
1021
- * Get relay status from all known relays.
1022
- * Returns results from each relay that responds.
1023
- */
1024
- async getAllRelayHostStatus(pdsHostname) {
1025
- return (await Promise.all(PDSClient.RELAY_URLS.map((url) => this.getRelayHostStatus(pdsHostname, url)))).filter((r) => r !== null);
1026
- }
1027
- /**
1028
- * Request the relay to crawl this PDS.
1029
- * This notifies the Bluesky relay that the PDS is active and ready for federation.
1030
- * Uses bsky.network by default (the main relay endpoint).
1031
- */
1032
- async requestCrawl(pdsHostname, relayUrl = "https://bsky.network") {
1033
- try {
1034
- const url = new URL("/xrpc/com.atproto.sync.requestCrawl", relayUrl);
1035
- return (await fetch(url.toString(), {
1036
- method: "POST",
1037
- headers: { "Content-Type": "application/json" },
1038
- body: JSON.stringify({ hostname: pdsHostname })
1039
- })).ok;
1040
- } catch {
1041
- return false;
1042
- }
1043
- }
1044
- };
1045
-
1046
- //#endregion
1047
- //#region src/cli/utils/cli-helpers.ts
1048
- /**
1049
- * Shared CLI utilities for PDS commands
1050
- */
1051
- /**
1052
- * Prompt for text input, exiting on cancel
1053
- */
1054
- async function promptText(options) {
1055
- const result = await p.text(options);
1056
- if (p.isCancel(result)) {
1057
- p.cancel("Cancelled");
1058
- process.exit(0);
1059
- }
1060
- return result;
1061
- }
1062
- /**
1063
- * Prompt for confirmation, exiting on cancel
1064
- */
1065
- async function promptConfirm(options) {
1066
- const result = await p.confirm(options);
1067
- if (p.isCancel(result)) {
1068
- p.cancel("Cancelled");
1069
- process.exit(0);
1070
- }
1071
- return result;
1072
- }
1073
- /**
1074
- * Prompt for selection, exiting on cancel
1075
- */
1076
- async function promptSelect(options) {
1077
- const result = await p.select(options);
1078
- if (p.isCancel(result)) {
1079
- p.cancel("Cancelled");
1080
- process.exit(0);
1081
- }
1082
- return result;
1083
- }
1084
- /**
1085
- * Get target PDS URL based on mode
1086
- */
1087
- function getTargetUrl(isDev, pdsHostname) {
1088
- if (isDev) return `http://localhost:${process.env.PORT ? parseInt(process.env.PORT) ?? "5173" : "5173"}`;
1089
- if (!pdsHostname) throw new Error("PDS_HOSTNAME not configured in wrangler.jsonc");
1090
- return `https://${pdsHostname}`;
1091
- }
1092
- /**
1093
- * Extract domain from URL
1094
- */
1095
- function getDomain(url) {
1096
- try {
1097
- return new URL(url).hostname;
1098
- } catch {
1099
- return url;
1100
- }
1101
- }
1102
- /**
1103
- * Detect which package manager is being used based on npm_config_user_agent
1104
- */
1105
- function detectPackageManager() {
1106
- const userAgent = process.env.npm_config_user_agent || "";
1107
- if (userAgent.startsWith("yarn")) return "yarn";
1108
- if (userAgent.startsWith("pnpm")) return "pnpm";
1109
- if (userAgent.startsWith("bun")) return "bun";
1110
- return "npm";
1111
- }
1112
- /**
1113
- * Format a command for the detected package manager
1114
- * npm always needs "run" for scripts, pnpm/yarn/bun can use shorthand
1115
- * except for "deploy" which conflicts with pnpm's built-in deploy command
1116
- */
1117
- function formatCommand(pm, ...args) {
1118
- if (pm === "npm" || args[0] === "deploy") return `${pm} run ${args.join(" ")}`;
1119
- return `${pm} ${args.join(" ")}`;
1120
- }
1121
- /**
1122
- * Copy text to clipboard using platform-specific command
1123
- * Falls back gracefully if clipboard is unavailable
1124
- */
1125
- async function copyToClipboard(text) {
1126
- const platform = process.platform;
1127
- let cmd;
1128
- let args;
1129
- if (platform === "darwin") {
1130
- cmd = "pbcopy";
1131
- args = [];
1132
- } else if (platform === "linux") {
1133
- cmd = "xclip";
1134
- args = ["-selection", "clipboard"];
1135
- } else if (platform === "win32") {
1136
- cmd = "clip";
1137
- args = [];
1138
- } else return false;
1139
- return new Promise((resolve$1) => {
1140
- const child = spawn(cmd, args, { stdio: [
1141
- "pipe",
1142
- "ignore",
1143
- "ignore"
1144
- ] });
1145
- child.on("error", () => resolve$1(false));
1146
- child.on("close", (code) => resolve$1(code === 0));
1147
- child.stdin?.write(text);
1148
- child.stdin?.end();
1149
- });
1150
- }
1151
- /**
1152
- * Check if 1Password CLI (op) is available
1153
- * Only checks on POSIX systems (macOS, Linux)
1154
- */
1155
- async function is1PasswordAvailable() {
1156
- if (process.platform === "win32") return false;
1157
- return new Promise((resolve$1) => {
1158
- const child = spawn("which", ["op"], { stdio: [
1159
- "ignore",
1160
- "pipe",
1161
- "ignore"
1162
- ] });
1163
- child.on("error", () => resolve$1(false));
1164
- child.on("close", (code) => resolve$1(code === 0));
1165
- });
1166
- }
1167
- /**
1168
- * Save a key to 1Password using the CLI
1169
- * Creates a secure note with the signing key
1170
- */
1171
- async function saveTo1Password(key, handle) {
1172
- const itemName = `Cirrus PDS Signing Key - ${handle}`;
1173
- return new Promise((resolve$1) => {
1174
- const child = spawn("op", [
1175
- "item",
1176
- "create",
1177
- "--category",
1178
- "Secure Note",
1179
- "--title",
1180
- itemName,
1181
- `notesPlain=CIRRUS PDS SIGNING KEY\n\nHandle: ${handle}\nCreated: ${(/* @__PURE__ */ new Date()).toISOString()}\n\nWARNING: This key controls your identity!\n\nSIGNING KEY:\n${key}`,
1182
- "--tags",
1183
- "cirrus,pds,signing-key"
1184
- ], { stdio: [
1185
- "ignore",
1186
- "pipe",
1187
- "pipe"
1188
- ] });
1189
- let stderr = "";
1190
- child.stderr?.on("data", (data) => {
1191
- stderr += data.toString();
1192
- });
1193
- child.on("error", (err) => {
1194
- resolve$1({
1195
- success: false,
1196
- error: err.message
1197
- });
1198
- });
1199
- child.on("close", (code) => {
1200
- if (code === 0) resolve$1({
1201
- success: true,
1202
- itemName
1203
- });
1204
- else resolve$1({
1205
- success: false,
1206
- error: stderr || `1Password CLI exited with code ${code}`
1207
- });
1297
+ method: "POST",
1298
+ headers,
1299
+ body: JSON.stringify({ id: credentialId })
1208
1300
  });
1209
- });
1210
- }
1211
- /**
1212
- * Run a shell command and return a promise.
1213
- * Captures output and throws on non-zero exit code.
1214
- * Use this for running npm/pnpm/yarn scripts etc.
1215
- */
1216
- function runCommand(cmd, args, options = {}) {
1217
- return new Promise((resolve$1, reject) => {
1218
- const child = spawn(cmd, args, { stdio: options.stream ? "inherit" : "pipe" });
1219
- let output = "";
1220
- if (!options.stream) {
1221
- child.stdout?.on("data", (data) => {
1222
- output += data.toString();
1223
- });
1224
- child.stderr?.on("data", (data) => {
1225
- output += data.toString();
1301
+ if (!res.ok) {
1302
+ const errorBody = await res.json().catch(() => ({}));
1303
+ throw new ClientResponseError({
1304
+ status: res.status,
1305
+ headers: res.headers,
1306
+ data: {
1307
+ error: errorBody.error ?? "Unknown",
1308
+ message: errorBody.message
1309
+ }
1226
1310
  });
1227
1311
  }
1228
- child.on("close", (code) => {
1229
- if (code === 0) resolve$1();
1230
- else {
1231
- if (output && !options.stream) console.error(output);
1232
- reject(/* @__PURE__ */ new Error(`${cmd} ${args.join(" ")} failed with code ${code}`));
1233
- }
1312
+ return res.json();
1313
+ }
1314
+ /**
1315
+ * Get a migration token for outbound migration.
1316
+ * This token can be used to migrate to another PDS.
1317
+ */
1318
+ async getMigrationToken() {
1319
+ const url = new URL("/xrpc/gg.mk.experimental.getMigrationToken", this.baseUrl);
1320
+ const headers = {};
1321
+ if (this.authToken) headers["Authorization"] = `Bearer ${this.authToken}`;
1322
+ const res = await fetch(url.toString(), {
1323
+ method: "GET",
1324
+ headers
1234
1325
  });
1235
- child.on("error", reject);
1236
- });
1237
- }
1238
- /**
1239
- * Save a key backup file with appropriate warnings
1240
- */
1241
- async function saveKeyBackup(key, handle) {
1242
- const filename = `signing-key-backup-${handle.replace(/[^a-z0-9]/gi, "-")}.txt`;
1243
- const filepath = join(process.cwd(), filename);
1244
- await writeFile(filepath, [
1245
- "=".repeat(60),
1246
- "CIRRUS PDS SIGNING KEY BACKUP",
1247
- "=".repeat(60),
1248
- "",
1249
- `Handle: ${handle}`,
1250
- `Created: ${(/* @__PURE__ */ new Date()).toISOString()}`,
1251
- "",
1252
- "WARNING: This key controls your identity!",
1253
- "- Store this file in a secure location (password manager, encrypted drive)",
1254
- "- Delete this file from your local disk after backing up",
1255
- "- Never share this key with anyone",
1256
- "- If compromised, your identity can be stolen",
1257
- "",
1258
- "=".repeat(60),
1259
- "SIGNING KEY (hex-encoded secp256k1 private key)",
1260
- "=".repeat(60),
1261
- "",
1262
- key,
1263
- "",
1264
- "=".repeat(60)
1265
- ].join("\n"), { mode: 384 });
1266
- return filepath;
1267
- }
1326
+ if (!res.ok) return {
1327
+ success: false,
1328
+ error: (await res.json().catch(() => ({}))).message ?? `Request failed: ${res.status}`
1329
+ };
1330
+ return {
1331
+ success: true,
1332
+ token: (await res.json()).token
1333
+ };
1334
+ }
1335
+ /**
1336
+ * List notifications (proxied through PDS to AppView)
1337
+ */
1338
+ async listNotifications(limit = 25) {
1339
+ const url = new URL("/xrpc/app.bsky.notification.listNotifications", this.baseUrl);
1340
+ url.searchParams.set("limit", String(limit));
1341
+ const headers = {};
1342
+ if (this.authToken) headers["Authorization"] = `Bearer ${this.authToken}`;
1343
+ const res = await fetch(url.toString(), { headers });
1344
+ if (!res.ok) throw new Error(`Failed to get notifications: ${res.status}`);
1345
+ return res.json();
1346
+ }
1347
+ /**
1348
+ * List repos (for getting PDS rev)
1349
+ */
1350
+ async listRepos() {
1351
+ const url = new URL("/xrpc/com.atproto.sync.listRepos", this.baseUrl);
1352
+ const res = await fetch(url.toString());
1353
+ if (!res.ok) throw new Error(`Failed to list repos: ${res.status}`);
1354
+ return res.json();
1355
+ }
1356
+ /**
1357
+ * List records in a collection
1358
+ */
1359
+ async listRecords(did, collection, limit = 100) {
1360
+ const url = new URL("/xrpc/com.atproto.repo.listRecords", this.baseUrl);
1361
+ url.searchParams.set("repo", did);
1362
+ url.searchParams.set("collection", collection);
1363
+ url.searchParams.set("limit", String(limit));
1364
+ const res = await fetch(url.toString());
1365
+ if (!res.ok) throw new Error(`Failed to list records: ${res.status}`);
1366
+ return res.json();
1367
+ }
1368
+ static RELAY_URLS = ["https://relay1.us-west.bsky.network", "https://relay1.us-east.bsky.network"];
1369
+ /**
1370
+ * Get relay's view of this PDS host status from a single relay.
1371
+ * Calls com.atproto.sync.getHostStatus on the relay.
1372
+ */
1373
+ async getRelayHostStatus(pdsHostname, relayUrl) {
1374
+ try {
1375
+ const url = new URL("/xrpc/com.atproto.sync.getHostStatus", relayUrl);
1376
+ url.searchParams.set("hostname", pdsHostname);
1377
+ const res = await fetch(url.toString());
1378
+ if (!res.ok) return null;
1379
+ return {
1380
+ ...await res.json(),
1381
+ relay: relayUrl
1382
+ };
1383
+ } catch {
1384
+ return null;
1385
+ }
1386
+ }
1387
+ /**
1388
+ * Get relay status from all known relays.
1389
+ * Returns results from each relay that responds.
1390
+ */
1391
+ async getAllRelayHostStatus(pdsHostname) {
1392
+ return (await Promise.all(PDSClient.RELAY_URLS.map((url) => this.getRelayHostStatus(pdsHostname, url)))).filter((r) => r !== null);
1393
+ }
1394
+ /**
1395
+ * Request the relay to crawl this PDS.
1396
+ * This notifies the Bluesky relay that the PDS is active and ready for federation.
1397
+ * Uses bsky.network by default (the main relay endpoint).
1398
+ */
1399
+ async requestCrawl(pdsHostname, relayUrl = "https://bsky.network") {
1400
+ try {
1401
+ const url = new URL("/xrpc/com.atproto.sync.requestCrawl", relayUrl);
1402
+ return (await fetch(url.toString(), {
1403
+ method: "POST",
1404
+ headers: { "Content-Type": "application/json" },
1405
+ body: JSON.stringify({ hostname: pdsHostname })
1406
+ })).ok;
1407
+ } catch {
1408
+ return false;
1409
+ }
1410
+ }
1411
+ };
1268
1412
 
1269
1413
  //#endregion
1270
1414
  //#region src/cli/commands/passkey/add.ts
@@ -3801,7 +3945,8 @@ const statusCommand = defineCommand({
3801
3945
  }
3802
3946
  try {
3803
3947
  const firehose = await client.getFirehoseStatus();
3804
- console.log(` ${INFO} ${firehose.subscribers} firehose subscriber${firehose.subscribers !== 1 ? "s" : ""}, seq: ${firehose.latestSeq ?? "none"}`);
3948
+ const subCount = firehose.subscribers.length;
3949
+ console.log(` ${INFO} ${subCount} firehose subscriber${subCount !== 1 ? "s" : ""}, seq: ${firehose.latestSeq ?? "none"}`);
3805
3950
  } catch {
3806
3951
  console.log(` ${pc.dim(" Could not get firehose status")}`);
3807
3952
  }
@@ -3896,6 +4041,598 @@ const emitIdentityCommand = defineCommand({
3896
4041
  }
3897
4042
  });
3898
4043
 
4044
+ //#endregion
4045
+ //#region src/cli/commands/dashboard.ts
4046
+ /**
4047
+ * Live terminal dashboard for PDS monitoring
4048
+ */
4049
+ function stripAnsi(s) {
4050
+ return s.replace(/\x1b\[[0-9;]*m/g, "");
4051
+ }
4052
+ function visibleLength(s) {
4053
+ return stripAnsi(s).length;
4054
+ }
4055
+ function padRight(s, width) {
4056
+ const pad = width - visibleLength(s);
4057
+ return pad > 0 ? s + " ".repeat(pad) : s;
4058
+ }
4059
+ function truncate(s, width) {
4060
+ if (visibleLength(s) <= width) return s;
4061
+ return stripAnsi(s).slice(0, width - 1) + pc.dim("…");
4062
+ }
4063
+ function enterAltScreen() {
4064
+ process.stdout.write("\x1B[?1049h");
4065
+ }
4066
+ function exitAltScreen() {
4067
+ process.stdout.write("\x1B[?1049l");
4068
+ }
4069
+ function hideCursor() {
4070
+ process.stdout.write("\x1B[?25l");
4071
+ }
4072
+ function showCursor() {
4073
+ process.stdout.write("\x1B[?25h");
4074
+ }
4075
+ function clearScreen() {
4076
+ process.stdout.write("\x1B[2J\x1B[H");
4077
+ }
4078
+ function renderColumns(cols, widths) {
4079
+ const maxRows = Math.max(...cols.map((c) => c.length));
4080
+ const lines = [];
4081
+ for (let i = 0; i < maxRows; i++) {
4082
+ let line = "";
4083
+ for (let j = 0; j < cols.length; j++) {
4084
+ const cell = cols[j][i] ?? "";
4085
+ line += padRight(cell, widths[j]);
4086
+ }
4087
+ lines.push(line);
4088
+ }
4089
+ return lines;
4090
+ }
4091
+ function parseFirehoseMessage(data) {
4092
+ try {
4093
+ const decoded = [...decodeAll(data)];
4094
+ if (decoded.length !== 2) return null;
4095
+ const header = decoded[0];
4096
+ const body = decoded[1];
4097
+ if (!header || header.op !== 1) return null;
4098
+ if (!body || typeof body.seq !== "number") return null;
4099
+ if (header.t === "#commit") return {
4100
+ seq: body.seq,
4101
+ type: "commit",
4102
+ ops: (body.ops ?? []).map((op) => ({
4103
+ action: op.action,
4104
+ path: op.path
4105
+ }))
4106
+ };
4107
+ if (header.t === "#identity") return {
4108
+ seq: body.seq,
4109
+ type: "identity",
4110
+ ops: [],
4111
+ handle: body.handle
4112
+ };
4113
+ return null;
4114
+ } catch {
4115
+ return null;
4116
+ }
4117
+ }
4118
+ const COLLECTION_NAMES = {
4119
+ "app.bsky.feed.post": "posts",
4120
+ "app.bsky.feed.like": "likes",
4121
+ "app.bsky.graph.follow": "follows",
4122
+ "app.bsky.feed.repost": "reposts",
4123
+ "app.bsky.actor.profile": "profile",
4124
+ "app.bsky.graph.block": "blocks",
4125
+ "app.bsky.graph.list": "lists",
4126
+ "app.bsky.graph.listitem": "list items",
4127
+ "app.bsky.feed.generator": "feeds",
4128
+ "app.bsky.feed.threadgate": "threadgates",
4129
+ "app.bsky.graph.starterpack": "starter packs",
4130
+ "chat.bsky.actor.declaration": "chat",
4131
+ "app.bsky.feed.postgate": "postgates",
4132
+ "app.bsky.labeler.service": "labeler"
4133
+ };
4134
+ /** Sort priority for collections (lower = first). Unlisted collections sort alphabetically at the end. */
4135
+ const COLLECTION_ORDER = {
4136
+ "app.bsky.feed.post": 1,
4137
+ "app.bsky.feed.like": 2,
4138
+ "app.bsky.graph.follow": 3,
4139
+ "app.bsky.feed.repost": 4,
4140
+ "app.bsky.graph.list": 5,
4141
+ "app.bsky.feed.generator": 6,
4142
+ "app.bsky.graph.block": 7,
4143
+ "app.bsky.graph.starterpack": 8,
4144
+ "app.bsky.actor.profile": 100
4145
+ };
4146
+ function friendlyName(collection) {
4147
+ return COLLECTION_NAMES[collection] ?? collection.split(".").pop() ?? collection;
4148
+ }
4149
+ /** Shorten IPv6 addresses by collapsing the longest zero run to :: */
4150
+ function shortenIP(ip) {
4151
+ if (!ip.includes(":")) return ip;
4152
+ const parts = ip.split(":");
4153
+ let bestStart = -1;
4154
+ let bestLen = 0;
4155
+ let curStart = -1;
4156
+ let curLen = 0;
4157
+ for (let i = 0; i < parts.length; i++) if (parts[i] === "0" || parts[i] === "0000" || parts[i] === "") {
4158
+ if (curStart === -1) curStart = i;
4159
+ curLen++;
4160
+ } else {
4161
+ if (curLen > bestLen) {
4162
+ bestStart = curStart;
4163
+ bestLen = curLen;
4164
+ }
4165
+ curStart = -1;
4166
+ curLen = 0;
4167
+ }
4168
+ if (curLen > bestLen) {
4169
+ bestStart = curStart;
4170
+ bestLen = curLen;
4171
+ }
4172
+ if (bestLen < 2) return parts.map((p$1) => p$1.replace(/^0+(?=.)/, "")).join(":");
4173
+ const before = parts.slice(0, bestStart).map((p$1) => p$1.replace(/^0+(?=.)/, ""));
4174
+ const after = parts.slice(bestStart + bestLen).map((p$1) => p$1.replace(/^0+(?=.)/, ""));
4175
+ return (before.length ? before.join(":") : "") + "::" + (after.length ? after.join(":") : "");
4176
+ }
4177
+ const REASON_ICON = {
4178
+ like: pc.red("♥"),
4179
+ repost: pc.green("↻"),
4180
+ follow: pc.cyan("+"),
4181
+ mention: pc.yellow("@"),
4182
+ reply: pc.cyan("↩"),
4183
+ quote: pc.yellow("❝"),
4184
+ "starterpack-joined": pc.cyan("★")
4185
+ };
4186
+ const REASON_TEXT = {
4187
+ like: "liked your post",
4188
+ repost: "reposted your post",
4189
+ follow: "followed you",
4190
+ mention: "mentioned you",
4191
+ reply: "replied to you",
4192
+ quote: "quoted your post",
4193
+ "starterpack-joined": "joined your starter pack"
4194
+ };
4195
+ function relativeTime(ts) {
4196
+ const diff = Math.floor((Date.now() - ts) / 1e3);
4197
+ if (diff < 10) return "just now";
4198
+ if (diff < 60) return `${diff}s ago`;
4199
+ if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
4200
+ if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
4201
+ return `${Math.floor(diff / 86400)}d ago`;
4202
+ }
4203
+ const MAX_EVENTS = 100;
4204
+ function createInitialState() {
4205
+ return {
4206
+ collections: [],
4207
+ pdsRev: null,
4208
+ subscribers: 0,
4209
+ latestSeq: null,
4210
+ subscriberDetails: [],
4211
+ events: [],
4212
+ notifications: [],
4213
+ accountActive: false,
4214
+ wsConnected: false,
4215
+ statusMessage: null,
4216
+ statusMessageTimeout: null,
4217
+ handleCheck: null,
4218
+ didCheck: null,
4219
+ relayHostStatus: []
4220
+ };
4221
+ }
4222
+ async function fetchRepo(client, did, state, render) {
4223
+ try {
4224
+ const repo = (await client.listRepos()).repos?.[0];
4225
+ if (repo) state.pdsRev = repo.rev;
4226
+ const collections = (await client.describeRepo(did)).collections ?? [];
4227
+ const results = await Promise.all(collections.map(async (col) => {
4228
+ const data = await client.listRecords(did, col, 100);
4229
+ return {
4230
+ name: col,
4231
+ friendlyName: friendlyName(col),
4232
+ count: data.records?.length ?? 0,
4233
+ hasMore: !!data.cursor
4234
+ };
4235
+ }));
4236
+ results.sort((a, b) => {
4237
+ const oa = COLLECTION_ORDER[a.name] ?? 50;
4238
+ const ob = COLLECTION_ORDER[b.name] ?? 50;
4239
+ if (oa !== ob) return oa - ob;
4240
+ return a.friendlyName.localeCompare(b.friendlyName);
4241
+ });
4242
+ state.collections = results.filter((c) => c.count > 0);
4243
+ render();
4244
+ } catch {}
4245
+ }
4246
+ async function fetchFirehoseStatus(client, state, render) {
4247
+ try {
4248
+ const data = await client.getFirehoseStatus();
4249
+ state.subscribers = data.subscribers?.length ?? 0;
4250
+ state.subscriberDetails = data.subscribers ?? [];
4251
+ if (data.latestSeq != null) state.latestSeq = data.latestSeq;
4252
+ render();
4253
+ } catch {}
4254
+ }
4255
+ async function fetchNotifications(client, state, render) {
4256
+ try {
4257
+ state.notifications = ((await client.listNotifications(25)).notifications ?? []).map((n) => ({
4258
+ time: new Date(n.indexedAt).toLocaleTimeString("en-GB", {
4259
+ hour12: false,
4260
+ hour: "2-digit",
4261
+ minute: "2-digit"
4262
+ }),
4263
+ icon: REASON_ICON[n.reason] ?? "?",
4264
+ author: n.author.displayName || n.author.handle,
4265
+ text: REASON_TEXT[n.reason] ?? n.reason,
4266
+ isRead: n.isRead
4267
+ }));
4268
+ render();
4269
+ } catch {}
4270
+ }
4271
+ async function fetchAccountStatus(client, state, render) {
4272
+ try {
4273
+ state.accountActive = (await client.getAccountStatus()).active;
4274
+ render();
4275
+ } catch {}
4276
+ }
4277
+ async function fetchIdentityChecks(client, handle, did, pdsHostname, state, render) {
4278
+ try {
4279
+ const [handleResult, didResult] = await Promise.all([checkHandleResolutionDetailed(client, handle, did), checkDidResolution(client, did, pdsHostname)]);
4280
+ state.handleCheck = {
4281
+ ok: handleResult.ok,
4282
+ methods: handleResult.methods
4283
+ };
4284
+ state.didCheck = {
4285
+ ok: didResult.ok,
4286
+ pdsEndpoint: didResult.pdsEndpoint
4287
+ };
4288
+ render();
4289
+ } catch {}
4290
+ }
4291
+ async function fetchRelayHostStatus(client, pdsHostname, state, render) {
4292
+ try {
4293
+ state.relayHostStatus = await client.getAllRelayHostStatus(pdsHostname);
4294
+ render();
4295
+ } catch {}
4296
+ }
4297
+ function connectFirehose(targetUrl, state, render, onCommit) {
4298
+ let ws = null;
4299
+ let reconnectTimer = null;
4300
+ let closed = false;
4301
+ function connect() {
4302
+ if (closed) return;
4303
+ try {
4304
+ const url = `${targetUrl.startsWith("https") ? "wss:" : "ws:"}//${targetUrl.replace(/^https?:\/\//, "")}/xrpc/com.atproto.sync.subscribeRepos`;
4305
+ ws = new WebSocket(url);
4306
+ ws.binaryType = "arraybuffer";
4307
+ ws.onopen = () => {
4308
+ state.wsConnected = true;
4309
+ render();
4310
+ };
4311
+ ws.onmessage = (e) => {
4312
+ const event = parseFirehoseMessage(new Uint8Array(e.data));
4313
+ if (!event) return;
4314
+ const time = (/* @__PURE__ */ new Date()).toLocaleTimeString("en-GB", { hour12: false });
4315
+ if (event.type === "identity") state.events.unshift({
4316
+ time,
4317
+ seq: event.seq,
4318
+ action: "identity",
4319
+ path: event.handle ?? ""
4320
+ });
4321
+ else {
4322
+ for (const op of event.ops) state.events.unshift({
4323
+ time,
4324
+ seq: event.seq,
4325
+ action: op.action,
4326
+ path: op.path
4327
+ });
4328
+ onCommit?.();
4329
+ }
4330
+ if (state.events.length > MAX_EVENTS) state.events.length = MAX_EVENTS;
4331
+ render();
4332
+ };
4333
+ ws.onclose = () => {
4334
+ state.wsConnected = false;
4335
+ render();
4336
+ if (!closed) reconnectTimer = setTimeout(connect, 3e3);
4337
+ };
4338
+ ws.onerror = () => {
4339
+ state.wsConnected = false;
4340
+ render();
4341
+ };
4342
+ } catch {}
4343
+ }
4344
+ connect();
4345
+ return { close() {
4346
+ closed = true;
4347
+ if (reconnectTimer) clearTimeout(reconnectTimer);
4348
+ if (ws) {
4349
+ ws.onclose = null;
4350
+ ws.close();
4351
+ }
4352
+ } };
4353
+ }
4354
+ function renderDashboard(state, config) {
4355
+ const cols = process.stdout.columns || 80;
4356
+ const rows = process.stdout.rows || 24;
4357
+ const lines = [];
4358
+ const indent = " ";
4359
+ const accountDot = state.accountActive ? pc.green("●") + " " + pc.green("ACTIVE") : pc.yellow("○") + " " + pc.yellow("INACTIVE");
4360
+ lines.push("");
4361
+ lines.push(`${indent}${pc.bold("☁ CIRRUS")} ${pc.dim("·")} ${pc.cyan(config.hostname)} ${pc.dim("·")} ${pc.dim("v" + config.version)}`);
4362
+ lines.push(`${indent} ${pc.white("@" + config.handle)} ${pc.dim("·")} ${pc.dim(config.did)} ${pc.dim("·")} ${accountDot}`);
4363
+ lines.push("");
4364
+ const colWidth = Math.floor((cols - 6) / 2);
4365
+ const col1 = [pc.dim("REPOSITORY"), ""];
4366
+ if (state.collections.length === 0) col1.push(pc.dim("No records"));
4367
+ else for (const c of state.collections) {
4368
+ const name = c.friendlyName.padEnd(16);
4369
+ const count = String(c.count).padStart(5);
4370
+ const more = c.hasMore ? "+" : " ";
4371
+ col1.push(`${name} ${pc.bold(count)}${more}`);
4372
+ }
4373
+ const col2 = [pc.dim("NETWORK"), ""];
4374
+ if (state.handleCheck) {
4375
+ const icon = state.handleCheck.ok ? pc.green("✓") : pc.red("✗");
4376
+ const methods = state.handleCheck.methods.length > 0 ? pc.dim(` ${state.handleCheck.methods.join(" ")}`) : "";
4377
+ col2.push(`${icon} handle${methods}`);
4378
+ } else col2.push(pc.dim("○ handle checking…"));
4379
+ if (state.didCheck) {
4380
+ const icon = state.didCheck.ok ? pc.green("✓") : pc.red("✗");
4381
+ col2.push(`${icon} did document`);
4382
+ } else col2.push(pc.dim("○ did doc checking…"));
4383
+ col2.push("");
4384
+ const relayStatusColors = {
4385
+ active: pc.green,
4386
+ idle: pc.yellow,
4387
+ offline: pc.red,
4388
+ throttled: pc.red,
4389
+ banned: pc.red
4390
+ };
4391
+ const relayDotColors = {
4392
+ active: pc.green("●"),
4393
+ idle: pc.yellow("●"),
4394
+ offline: pc.red("●"),
4395
+ throttled: pc.red("●"),
4396
+ banned: pc.red("●")
4397
+ };
4398
+ if (state.relayHostStatus.length > 0) for (const relay of state.relayHostStatus) {
4399
+ const name = relay.relay.replace("https://relay1.", "").replace(".bsky.network", "");
4400
+ const colorFn = relayStatusColors[relay.status] ?? pc.dim;
4401
+ const dot = relayDotColors[relay.status] ?? pc.dim("○");
4402
+ col2.push(`${dot} ${name} ${colorFn(relay.status)}`);
4403
+ }
4404
+ else col2.push(pc.dim("○ relay unknown"));
4405
+ col2.push("");
4406
+ const subDot = state.subscribers > 0 ? pc.green("●") : pc.dim("○");
4407
+ col2.push(`${subDot} ${pc.bold(String(state.subscribers))} subscriber${state.subscribers !== 1 ? "s" : ""} ${pc.dim("seq:")} ${state.latestSeq != null ? state.latestSeq : pc.dim("—")}`);
4408
+ if (state.subscriberDetails.length > 0) for (const sub of state.subscriberDetails.slice(0, 3)) {
4409
+ const ip = sub.ip ? shortenIP(sub.ip) : "";
4410
+ col2.push(pc.dim(` ${relativeTime(sub.connectedAt)} cursor: ${sub.cursor} ${ip}`));
4411
+ }
4412
+ const columnLines = renderColumns([col1, col2], [colWidth, colWidth]);
4413
+ for (const line of columnLines) lines.push(indent + line);
4414
+ lines.push("");
4415
+ const remaining = rows - lines.length - 3;
4416
+ const notifHeight = Math.max(3, Math.floor(remaining * .35));
4417
+ const eventsHeight = Math.max(3, remaining - notifHeight);
4418
+ const wsStatusText = state.wsConnected ? "● connected" : "○ disconnected";
4419
+ indent + "";
4420
+ const eventsSuffix = " " + wsStatusText + " ";
4421
+ const eventsSeparator = "─".repeat(Math.max(0, cols - 9 - eventsSuffix.length));
4422
+ const wsStatus = state.wsConnected ? pc.green(wsStatusText) : pc.dim(wsStatusText);
4423
+ lines.push(`${indent}${pc.dim("EVENTS " + eventsSeparator)} ${wsStatus}`);
4424
+ if (state.events.length === 0) {
4425
+ lines.push(`${indent}${pc.dim("Waiting for events…")}`);
4426
+ for (let i = 1; i < eventsHeight - 1; i++) lines.push("");
4427
+ } else {
4428
+ const visibleEvents = state.events.slice(0, eventsHeight - 1);
4429
+ for (const ev of visibleEvents) {
4430
+ const actionColor = {
4431
+ create: pc.green,
4432
+ update: pc.yellow,
4433
+ delete: pc.red,
4434
+ identity: pc.cyan
4435
+ }[ev.action] ?? pc.dim;
4436
+ const line = `${indent}${pc.dim(ev.time)} ${pc.dim("#" + String(ev.seq).padStart(4))} ${actionColor(ev.action.toUpperCase().padEnd(8))} ${ev.path}`;
4437
+ lines.push(truncate(line, cols));
4438
+ }
4439
+ for (let i = visibleEvents.length; i < eventsHeight - 1; i++) lines.push("");
4440
+ }
4441
+ const notifSeparator = "─".repeat(Math.max(0, cols - visibleLength(indent + "NOTIFICATIONS ") - 2));
4442
+ lines.push(`${indent}${pc.dim("NOTIFICATIONS " + notifSeparator)}`);
4443
+ if (state.notifications.length === 0) {
4444
+ lines.push(`${indent}${pc.dim("No notifications yet")}`);
4445
+ for (let i = 1; i < notifHeight - 1; i++) lines.push("");
4446
+ } else {
4447
+ const visibleNotifs = state.notifications.slice(0, notifHeight - 1);
4448
+ for (const n of visibleNotifs) {
4449
+ const readDim = n.isRead ? pc.dim : (s) => s;
4450
+ const line = `${indent}${pc.dim(n.time)} ${n.icon} ${readDim(n.author)} ${readDim(pc.dim(n.text))}`;
4451
+ lines.push(truncate(line, cols));
4452
+ }
4453
+ for (let i = visibleNotifs.length; i < notifHeight - 1; i++) lines.push("");
4454
+ }
4455
+ lines.push("");
4456
+ const keys = [];
4457
+ if (!state.accountActive) keys.push(`${pc.dim("[a]")} activate`);
4458
+ else {
4459
+ keys.push(`${pc.dim("[r]")} crawl`);
4460
+ keys.push(`${pc.dim("[e]")} emit identity`);
4461
+ }
4462
+ keys.push(`${pc.dim("[q]")} quit`);
4463
+ let footer = `${indent}${keys.join(` ${pc.dim("·")} `)}`;
4464
+ if (state.statusMessage) footer += ` ${pc.yellow(state.statusMessage)}`;
4465
+ lines.push(footer);
4466
+ while (lines.length < rows) lines.push("");
4467
+ const output = lines.slice(0, rows).map((l) => padRight(l, cols)).join("\n");
4468
+ process.stdout.write("\x1B[H" + output);
4469
+ }
4470
+ function setStatusMessage(state, message, render, durationMs = 3e3) {
4471
+ if (state.statusMessageTimeout) clearTimeout(state.statusMessageTimeout);
4472
+ state.statusMessage = message;
4473
+ render();
4474
+ state.statusMessageTimeout = setTimeout(() => {
4475
+ state.statusMessage = null;
4476
+ render();
4477
+ }, durationMs);
4478
+ }
4479
+ const dashboardCommand = defineCommand({
4480
+ meta: {
4481
+ name: "dashboard",
4482
+ description: "Live dashboard for PDS monitoring"
4483
+ },
4484
+ args: { dev: {
4485
+ type: "boolean",
4486
+ description: "Target local development server instead of production",
4487
+ default: false
4488
+ } },
4489
+ async run({ args }) {
4490
+ const isDev = args.dev;
4491
+ const wranglerVars = getVars();
4492
+ const config = {
4493
+ ...readDevVars(),
4494
+ ...wranglerVars
4495
+ };
4496
+ let targetUrl;
4497
+ try {
4498
+ targetUrl = getTargetUrl(isDev, config.PDS_HOSTNAME);
4499
+ } catch (err) {
4500
+ console.error(pc.red("Error:"), err instanceof Error ? err.message : "Configuration error");
4501
+ console.log(pc.dim("Run 'pds init' first to configure your PDS."));
4502
+ process.exit(1);
4503
+ }
4504
+ const authToken = config.AUTH_TOKEN;
4505
+ const handle = config.HANDLE ?? "";
4506
+ const did = config.DID ?? "";
4507
+ const pdsHostname = config.PDS_HOSTNAME ?? "";
4508
+ if (!authToken) {
4509
+ console.error(pc.red("Error:"), "No AUTH_TOKEN found. Run 'pds init' first.");
4510
+ process.exit(1);
4511
+ }
4512
+ const client = new PDSClient(targetUrl, authToken);
4513
+ if (!await client.healthCheck()) {
4514
+ console.error(pc.red("Error:"), `PDS not responding at ${targetUrl}`);
4515
+ process.exit(1);
4516
+ }
4517
+ const state = createInitialState();
4518
+ const dashConfig = {
4519
+ hostname: pdsHostname || targetUrl,
4520
+ handle,
4521
+ did,
4522
+ version: "0.10.6"
4523
+ };
4524
+ const render = () => renderDashboard(state, dashConfig);
4525
+ enterAltScreen();
4526
+ hideCursor();
4527
+ clearScreen();
4528
+ const intervals = [];
4529
+ let firehose = null;
4530
+ let repoRefetchTimer = null;
4531
+ function cleanup() {
4532
+ for (const interval of intervals) clearInterval(interval);
4533
+ if (firehose) firehose.close();
4534
+ if (repoRefetchTimer) clearTimeout(repoRefetchTimer);
4535
+ if (state.statusMessageTimeout) clearTimeout(state.statusMessageTimeout);
4536
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
4537
+ showCursor();
4538
+ exitAltScreen();
4539
+ }
4540
+ process.on("SIGINT", () => {
4541
+ cleanup();
4542
+ process.exit(0);
4543
+ });
4544
+ process.on("SIGTERM", () => {
4545
+ cleanup();
4546
+ process.exit(0);
4547
+ });
4548
+ const scheduleRepoRefetch = () => {
4549
+ if (repoRefetchTimer) clearTimeout(repoRefetchTimer);
4550
+ repoRefetchTimer = setTimeout(() => {
4551
+ fetchRepo(client, did, state, render);
4552
+ }, 1e3);
4553
+ };
4554
+ render();
4555
+ await Promise.all([
4556
+ fetchRepo(client, did, state, render),
4557
+ fetchFirehoseStatus(client, state, render),
4558
+ fetchAccountStatus(client, state, render),
4559
+ fetchNotifications(client, state, render),
4560
+ fetchIdentityChecks(client, handle, did, pdsHostname, state, render)
4561
+ ]);
4562
+ await fetchRelayHostStatus(client, pdsHostname, state, render);
4563
+ intervals.push(setInterval(() => fetchRepo(client, did, state, render), 3e4));
4564
+ intervals.push(setInterval(() => fetchRelayHostStatus(client, pdsHostname, state, render), 5e3));
4565
+ intervals.push(setInterval(() => fetchFirehoseStatus(client, state, render), 1e4));
4566
+ intervals.push(setInterval(() => fetchNotifications(client, state, render), 15e3));
4567
+ intervals.push(setInterval(() => fetchAccountStatus(client, state, render), 3e4));
4568
+ intervals.push(setInterval(() => fetchIdentityChecks(client, handle, did, pdsHostname, state, render), 15e3));
4569
+ firehose = connectFirehose(targetUrl, state, render, scheduleRepoRefetch);
4570
+ process.stdout.on("resize", render);
4571
+ if (process.stdin.isTTY) {
4572
+ process.stdin.setRawMode(true);
4573
+ process.stdin.resume();
4574
+ process.stdin.setEncoding("utf8");
4575
+ let activateConfirmTimeout = null;
4576
+ let awaitingActivateConfirm = false;
4577
+ process.stdin.on("data", async (key) => {
4578
+ if (key === "") {
4579
+ cleanup();
4580
+ process.exit(0);
4581
+ }
4582
+ if (key === "q" || key === "Q") {
4583
+ cleanup();
4584
+ process.exit(0);
4585
+ }
4586
+ if (key === "a" || key === "A") {
4587
+ if (state.accountActive) return;
4588
+ if (awaitingActivateConfirm) {
4589
+ awaitingActivateConfirm = false;
4590
+ if (activateConfirmTimeout) clearTimeout(activateConfirmTimeout);
4591
+ setStatusMessage(state, "Activating…", render, 1e4);
4592
+ try {
4593
+ await client.activateAccount();
4594
+ state.accountActive = true;
4595
+ setStatusMessage(state, pc.green("✓ Account activated"), render, 5e3);
4596
+ } catch (err) {
4597
+ setStatusMessage(state, pc.red(`\u2717 ${err instanceof Error ? err.message : "Activation failed"}`), render, 5e3);
4598
+ }
4599
+ } else {
4600
+ awaitingActivateConfirm = true;
4601
+ setStatusMessage(state, "Press [a] again to activate", render, 3e3);
4602
+ activateConfirmTimeout = setTimeout(() => {
4603
+ awaitingActivateConfirm = false;
4604
+ state.statusMessage = null;
4605
+ render();
4606
+ }, 3e3);
4607
+ }
4608
+ return;
4609
+ }
4610
+ if (key === "r" || key === "R") {
4611
+ if (!state.accountActive) return;
4612
+ if (!pdsHostname || isDev) {
4613
+ setStatusMessage(state, pc.yellow("No PDS hostname configured"), render);
4614
+ return;
4615
+ }
4616
+ setStatusMessage(state, "Requesting crawl…", render, 1e4);
4617
+ setStatusMessage(state, await client.requestCrawl(pdsHostname) ? pc.green("✓ Crawl requested") : pc.red("✗ Crawl request failed"), render);
4618
+ return;
4619
+ }
4620
+ if (key === "e" || key === "E") {
4621
+ if (!state.accountActive) return;
4622
+ setStatusMessage(state, "Emitting identity…", render, 1e4);
4623
+ try {
4624
+ const result = await client.emitIdentity();
4625
+ setStatusMessage(state, pc.green(`\u2713 Identity emitted (seq: ${result.seq})`), render);
4626
+ } catch (err) {
4627
+ setStatusMessage(state, pc.red(`\u2717 ${err instanceof Error ? err.message : "Failed"}`), render);
4628
+ }
4629
+ return;
4630
+ }
4631
+ });
4632
+ }
4633
+ }
4634
+ });
4635
+
3899
4636
  //#endregion
3900
4637
  //#region src/cli/index.ts
3901
4638
  /**
@@ -3917,7 +4654,8 @@ runMain(defineCommand({
3917
4654
  activate: activateCommand,
3918
4655
  deactivate: deactivateCommand,
3919
4656
  status: statusCommand,
3920
- "emit-identity": emitIdentityCommand
4657
+ "emit-identity": emitIdentityCommand,
4658
+ dashboard: dashboardCommand
3921
4659
  }
3922
4660
  }));
3923
4661