@getcirrus/pds 0.2.5 → 0.3.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,9 +8,10 @@ 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 { resolve } from "node:path";
11
- import { AtprotoDohHandleResolver } from "@atproto-labs/handle-resolver";
12
- import { check, didDocument, getPdsEndpoint } from "@atproto/common-web";
11
+ import { CompositeDidDocumentResolver, DohJsonHandleResolver, PlcDidDocumentResolver, WebDidDocumentResolver } from "@atcute/identity-resolver";
13
12
  import pc from "picocolors";
13
+ import { Client, ClientResponseError, ok } from "@atcute/client";
14
+ import { getPdsEndpoint } from "@atcute/identity";
14
15
 
15
16
  //#region src/cli/utils/wrangler.ts
16
17
  /**
@@ -77,6 +78,85 @@ async function setSecret(name, value) {
77
78
  child.on("error", reject);
78
79
  });
79
80
  }
81
+ /**
82
+ * Get account_id from wrangler config
83
+ */
84
+ function getAccountId() {
85
+ const { rawConfig } = experimental_readRawConfig({});
86
+ return rawConfig.account_id;
87
+ }
88
+ /**
89
+ * Set account_id in wrangler config
90
+ */
91
+ function setAccountId(accountId) {
92
+ const { configPath } = experimental_readRawConfig({});
93
+ if (!configPath) throw new Error("No wrangler config found");
94
+ experimental_patchConfig(configPath, { account_id: accountId });
95
+ }
96
+ /**
97
+ * Set custom domain routes in wrangler config
98
+ */
99
+ function setCustomDomains(domains) {
100
+ const { configPath } = experimental_readRawConfig({});
101
+ if (!configPath) throw new Error("No wrangler config found");
102
+ experimental_patchConfig(configPath, { routes: domains.map((pattern) => ({
103
+ pattern,
104
+ custom_domain: true
105
+ })) });
106
+ }
107
+ /**
108
+ * Detect available Cloudflare accounts by running wrangler whoami.
109
+ * Returns array of accounts if multiple found, null if single account or already configured.
110
+ */
111
+ async function detectCloudflareAccounts() {
112
+ if (getAccountId()) return null;
113
+ const { stdout, stderr } = await runWranglerWithOutput(["whoami"]);
114
+ const output = stdout + stderr;
115
+ const accounts = [];
116
+ const regex = /│\s*([^│]+?)\s*│\s*([a-f0-9]{32})\s*│/g;
117
+ let match;
118
+ while ((match = regex.exec(output)) !== null) {
119
+ const name = match[1]?.trim();
120
+ const id = match[2];
121
+ if (name && id && name !== "Account Name") accounts.push({
122
+ name,
123
+ id
124
+ });
125
+ }
126
+ return accounts.length > 1 ? accounts : null;
127
+ }
128
+ /**
129
+ * Run a wrangler command and capture output
130
+ */
131
+ function runWranglerWithOutput(args) {
132
+ return new Promise((resolve$1) => {
133
+ const child = spawn("wrangler", args, { stdio: [
134
+ "pipe",
135
+ "pipe",
136
+ "pipe"
137
+ ] });
138
+ let stdout = "";
139
+ let stderr = "";
140
+ child.stdout?.on("data", (data) => {
141
+ stdout += data.toString();
142
+ });
143
+ child.stderr?.on("data", (data) => {
144
+ stderr += data.toString();
145
+ });
146
+ child.on("close", () => {
147
+ resolve$1({
148
+ stdout,
149
+ stderr
150
+ });
151
+ });
152
+ child.on("error", () => {
153
+ resolve$1({
154
+ stdout,
155
+ stderr
156
+ });
157
+ });
158
+ });
159
+ }
80
160
 
81
161
  //#endregion
82
162
  //#region src/cli/utils/dotenv.ts
@@ -395,13 +475,32 @@ function getDomain(url) {
395
475
  return url;
396
476
  }
397
477
  }
478
+ /**
479
+ * Detect which package manager is being used based on npm_config_user_agent
480
+ */
481
+ function detectPackageManager() {
482
+ const userAgent = process.env.npm_config_user_agent || "";
483
+ if (userAgent.startsWith("yarn")) return "yarn";
484
+ if (userAgent.startsWith("pnpm")) return "pnpm";
485
+ if (userAgent.startsWith("bun")) return "bun";
486
+ return "npm";
487
+ }
488
+ /**
489
+ * Format a command for the detected package manager
490
+ * npm always needs "run" for scripts, pnpm/yarn/bun can use shorthand
491
+ * except for "deploy" which conflicts with pnpm's built-in deploy command
492
+ */
493
+ function formatCommand(pm, ...args) {
494
+ if (pm === "npm" || args[0] === "deploy") return `${pm} run ${args.join(" ")}`;
495
+ return `${pm} ${args.join(" ")}`;
496
+ }
398
497
 
399
498
  //#endregion
400
499
  //#region src/cli/utils/handle-resolver.ts
401
500
  /**
402
501
  * Utilities for resolving AT Protocol handles to DIDs
403
502
  */
404
- const resolver = new AtprotoDohHandleResolver({ dohEndpoint: "https://cloudflare-dns.com/dns-query" });
503
+ const resolver = new DohJsonHandleResolver({ dohUrl: "https://cloudflare-dns.com/dns-query" });
405
504
  /**
406
505
  * Resolve a handle to a DID using the AT Protocol handle resolution methods.
407
506
  * Uses DNS-over-HTTPS via Cloudflare for DNS resolution.
@@ -409,7 +508,7 @@ const resolver = new AtprotoDohHandleResolver({ dohEndpoint: "https://cloudflare
409
508
  async function resolveHandleToDid(handle) {
410
509
  try {
411
510
  return await resolver.resolve(handle, { signal: AbortSignal.timeout(1e4) });
412
- } catch (err) {
511
+ } catch {
413
512
  return null;
414
513
  }
415
514
  }
@@ -419,20 +518,31 @@ async function resolveHandleToDid(handle) {
419
518
  /**
420
519
  * DID resolution for Cloudflare Workers
421
520
  *
422
- * We can't use @atproto/identity directly because it uses `redirect: "error"`
423
- * which Cloudflare Workers doesn't support. This is a simple implementation
424
- * that's compatible with Workers.
521
+ * Uses @atcute/identity-resolver which is already Workers-compatible
522
+ * (uses redirect: "manual" internally).
425
523
  */
426
524
  const PLC_DIRECTORY = "https://plc.directory";
427
525
  const TIMEOUT_MS = 3e3;
526
+ /**
527
+ * Wrapper that always uses globalThis.fetch so it can be mocked in tests.
528
+ * @atcute resolvers capture the fetch reference at construction time,
529
+ * so we need this indirection to allow test mocking.
530
+ */
531
+ const stubbableFetch = (input, init) => globalThis.fetch(input, init);
428
532
  var DidResolver = class {
429
- plcUrl;
533
+ resolver;
430
534
  timeout;
431
535
  cache;
432
536
  constructor(opts = {}) {
433
- this.plcUrl = opts.plcUrl ?? PLC_DIRECTORY;
434
537
  this.timeout = opts.timeout ?? TIMEOUT_MS;
435
538
  this.cache = opts.didCache;
539
+ this.resolver = new CompositeDidDocumentResolver({ methods: {
540
+ plc: new PlcDidDocumentResolver({
541
+ apiUrl: opts.plcUrl ?? PLC_DIRECTORY,
542
+ fetch: stubbableFetch
543
+ }),
544
+ web: new WebDidDocumentResolver({ fetch: stubbableFetch })
545
+ } });
436
546
  }
437
547
  async resolve(did) {
438
548
  if (this.cache) {
@@ -448,57 +558,18 @@ var DidResolver = class {
448
558
  return doc;
449
559
  }
450
560
  async resolveNoCache(did) {
451
- if (did.startsWith("did:web:")) return this.resolveDidWeb(did);
452
- if (did.startsWith("did:plc:")) return this.resolveDidPlc(did);
453
- throw new Error(`Unsupported DID method: ${did}`);
454
- }
455
- async resolveDidWeb(did) {
456
- const parts = did.split(":").slice(2);
457
- if (parts.length === 0) throw new Error(`Invalid did:web format: ${did}`);
458
- if (parts.length > 1) throw new Error(`Unsupported did:web with path: ${did}`);
459
- const domain = decodeURIComponent(parts[0]);
460
- const url = new URL(`https://${domain}/.well-known/did.json`);
461
- if (url.hostname === "localhost") url.protocol = "http:";
462
561
  const controller = new AbortController();
463
562
  const timeoutId = setTimeout(() => controller.abort(), this.timeout);
464
563
  try {
465
- const res = await fetch(url.toString(), {
466
- signal: controller.signal,
467
- redirect: "manual",
468
- headers: { accept: "application/did+ld+json,application/json" }
469
- });
470
- if (res.status >= 300 && res.status < 400) return null;
471
- if (!res.ok) return null;
472
- const doc = await res.json();
473
- return this.validateDidDoc(did, doc);
474
- } finally {
475
- clearTimeout(timeoutId);
476
- }
477
- }
478
- async resolveDidPlc(did) {
479
- const url = new URL(`/${encodeURIComponent(did)}`, this.plcUrl);
480
- const controller = new AbortController();
481
- const timeoutId = setTimeout(() => controller.abort(), this.timeout);
482
- try {
483
- const res = await fetch(url.toString(), {
484
- signal: controller.signal,
485
- redirect: "manual",
486
- headers: { accept: "application/did+ld+json,application/json" }
487
- });
488
- if (res.status >= 300 && res.status < 400) return null;
489
- if (res.status === 404) return null;
490
- if (!res.ok) throw new Error(`PLC directory error: ${res.status} ${res.statusText}`);
491
- const doc = await res.json();
492
- return this.validateDidDoc(did, doc);
564
+ const doc = await this.resolver.resolve(did, { signal: controller.signal });
565
+ if (doc.id !== did) return null;
566
+ return doc;
567
+ } catch {
568
+ return null;
493
569
  } finally {
494
570
  clearTimeout(timeoutId);
495
571
  }
496
572
  }
497
- validateDidDoc(did, doc) {
498
- if (!check.is(doc, didDocument)) return null;
499
- if (doc.id !== did) return null;
500
- return doc;
501
- }
502
573
  };
503
574
 
504
575
  //#endregion
@@ -530,6 +601,31 @@ async function promptWorkerName(handle, currentWorkerName) {
530
601
  });
531
602
  }
532
603
  /**
604
+ * Ensure a Cloudflare account_id is configured.
605
+ * If multiple accounts detected, prompts user to select one.
606
+ */
607
+ async function ensureAccountConfigured() {
608
+ const spinner = p.spinner();
609
+ spinner.start("Checking Cloudflare account...");
610
+ const accounts = await detectCloudflareAccounts();
611
+ if (accounts === null) {
612
+ spinner.stop("Cloudflare account configured");
613
+ return;
614
+ }
615
+ spinner.stop(`Found ${accounts.length} Cloudflare accounts`);
616
+ const selectedId = await promptSelect({
617
+ message: "Select your Cloudflare account:",
618
+ options: accounts.map((acc) => ({
619
+ value: acc.id,
620
+ label: acc.name,
621
+ hint: acc.id.slice(0, 8) + "..."
622
+ }))
623
+ });
624
+ setAccountId(selectedId);
625
+ const selectedName = accounts.find((a) => a.id === selectedId)?.name;
626
+ p.log.success(`Account "${selectedName}" saved to wrangler.jsonc`);
627
+ }
628
+ /**
533
629
  * Run wrangler types to regenerate TypeScript types
534
630
  */
535
631
  function runWranglerTypes() {
@@ -563,6 +659,7 @@ const initCommand = defineCommand({
563
659
  default: false
564
660
  } },
565
661
  async run({ args }) {
662
+ const pm = detectPackageManager();
566
663
  p.intro("🦋 PDS Setup");
567
664
  const isProduction = args.production;
568
665
  if (isProduction) p.log.info("Production mode: secrets will be deployed to Cloudflare");
@@ -674,6 +771,7 @@ const initCommand = defineCommand({
674
771
  });
675
772
  workerName = await promptWorkerName(handle, currentWorkerName);
676
773
  initialActive = "false";
774
+ await ensureAccountConfigured();
677
775
  } else {
678
776
  p.log.info("A fresh start in the Atmosphere! ✨");
679
777
  hostname = await promptText({
@@ -700,6 +798,7 @@ const initCommand = defineCommand({
700
798
  });
701
799
  workerName = await promptWorkerName(handle, currentWorkerName);
702
800
  initialActive = "true";
801
+ await ensureAccountConfigured();
703
802
  if (handle === hostname) p.note([
704
803
  "Your handle matches your PDS hostname, so your PDS will",
705
804
  "automatically handle domain verification for you!",
@@ -760,6 +859,7 @@ const initCommand = defineCommand({
760
859
  SIGNING_KEY_PUBLIC: signingKeyPublic,
761
860
  INITIAL_ACTIVE: initialActive
762
861
  });
862
+ setCustomDomains([hostname]);
763
863
  spinner.stop("wrangler.jsonc updated");
764
864
  const local = !isProduction;
765
865
  if (isProduction) spinner.start("Deploying secrets to Cloudflare...");
@@ -807,19 +907,19 @@ const initCommand = defineCommand({
807
907
  if (isMigrating) p.note([
808
908
  deployedSecrets ? "Deploy your worker and run the migration:" : "Push secrets, deploy, and run the migration:",
809
909
  "",
810
- ...deployedSecrets ? [] : [" pnpm pds init --production", ""],
811
- " wrangler deploy",
812
- " pnpm pds migrate",
910
+ ...deployedSecrets ? [] : [` ${formatCommand(pm, "pds", "init", "--production")}`, ""],
911
+ ` ${formatCommand(pm, "deploy")}`,
912
+ ` ${formatCommand(pm, "pds", "migrate")}`,
813
913
  "",
814
914
  "To test locally first:",
815
- " pnpm dev # in one terminal",
816
- " pnpm pds migrate --dev # in another",
915
+ ` ${formatCommand(pm, "dev")} # in one terminal`,
916
+ ` ${formatCommand(pm, "pds", "migrate", "--dev")} # in another`,
817
917
  "",
818
918
  "Then update your identity and flip the switch! 🦋",
819
919
  " https://atproto.com/guides/account-migration"
820
920
  ].join("\n"), "Next Steps 🧳");
821
- if (deployedSecrets) p.outro("Run 'wrangler deploy' to launch your PDS! 🚀");
822
- else p.outro("Run 'pnpm dev' to start your PDS locally! 🦋");
921
+ if (deployedSecrets) p.outro(`Run '${formatCommand(pm, "deploy")}' to launch your PDS! 🚀`);
922
+ else p.outro(`Run '${formatCommand(pm, "dev")}' to start your PDS locally! 🦋`);
823
923
  }
824
924
  });
825
925
  /**
@@ -837,85 +937,53 @@ async function getOrGenerateSecret(name, devVars, generate) {
837
937
 
838
938
  //#endregion
839
939
  //#region src/cli/utils/pds-client.ts
840
- var PDSClientError = class extends Error {
841
- constructor(status, error, message) {
842
- super(message);
843
- this.status = status;
844
- this.error = error;
845
- this.name = "PDSClientError";
846
- }
847
- };
940
+ /**
941
+ * HTTP client for AT Protocol PDS XRPC endpoints
942
+ * Uses @atcute/client for type-safe XRPC calls
943
+ */
944
+ /**
945
+ * Create a fetch handler that adds optional auth token
946
+ */
947
+ function createAuthHandler(baseUrl, token) {
948
+ return async (pathname, init) => {
949
+ const url = new URL(pathname, baseUrl);
950
+ const headers = new Headers(init.headers);
951
+ if (token) headers.set("Authorization", `Bearer ${token}`);
952
+ return fetch(url, {
953
+ ...init,
954
+ headers
955
+ });
956
+ };
957
+ }
848
958
  var PDSClient = class {
959
+ client;
849
960
  authToken;
850
961
  constructor(baseUrl, authToken) {
851
962
  this.baseUrl = baseUrl;
852
963
  this.authToken = authToken;
964
+ this.client = new Client({ handler: createAuthHandler(baseUrl, authToken) });
853
965
  }
854
966
  /**
855
967
  * Set the auth token for subsequent requests
856
968
  */
857
969
  setAuthToken(token) {
858
970
  this.authToken = token;
859
- }
860
- /**
861
- * Make an XRPC request
862
- */
863
- async xrpc(method, endpoint, options = {}) {
864
- const url = new URL(`/xrpc/${endpoint}`, this.baseUrl);
865
- if (options.params) for (const [key, value] of Object.entries(options.params)) url.searchParams.set(key, value);
866
- const headers = {};
867
- if (options.auth && this.authToken) headers["Authorization"] = `Bearer ${this.authToken}`;
868
- if (options.contentType) headers["Content-Type"] = options.contentType;
869
- else if (options.body && !(options.body instanceof Uint8Array)) headers["Content-Type"] = "application/json";
870
- const res = await fetch(url.toString(), {
871
- method,
872
- headers,
873
- body: options.body ? options.body instanceof Uint8Array ? options.body : JSON.stringify(options.body) : void 0
874
- });
875
- if (!res.ok) {
876
- const errorBody = await res.json().catch(() => ({}));
877
- throw new PDSClientError(res.status, errorBody.error ?? "Unknown", errorBody.message ?? `Request failed: ${res.status}`);
878
- }
879
- if ((res.headers.get("content-type") ?? "").includes("application/json")) return res.json();
880
- return {};
881
- }
882
- /**
883
- * Make a raw request that returns bytes
884
- */
885
- async xrpcBytes(method, endpoint, options = {}) {
886
- const url = new URL(`/xrpc/${endpoint}`, this.baseUrl);
887
- if (options.params) for (const [key, value] of Object.entries(options.params)) url.searchParams.set(key, value);
888
- const headers = {};
889
- if (options.auth && this.authToken) headers["Authorization"] = `Bearer ${this.authToken}`;
890
- if (options.contentType) headers["Content-Type"] = options.contentType;
891
- const res = await fetch(url.toString(), {
892
- method,
893
- headers,
894
- body: options.body
895
- });
896
- if (!res.ok) {
897
- const errorBody = await res.json().catch(() => ({}));
898
- throw new PDSClientError(res.status, errorBody.error ?? "Unknown", errorBody.message ?? `Request failed: ${res.status}`);
899
- }
900
- return {
901
- bytes: new Uint8Array(await res.arrayBuffer()),
902
- mimeType: res.headers.get("content-type") ?? "application/octet-stream"
903
- };
971
+ this.client = new Client({ handler: createAuthHandler(this.baseUrl, token) });
904
972
  }
905
973
  /**
906
974
  * Create a session with identifier and password
907
975
  */
908
976
  async createSession(identifier, password) {
909
- return this.xrpc("POST", "com.atproto.server.createSession", { body: {
977
+ return ok(this.client.post("com.atproto.server.createSession", { input: {
910
978
  identifier,
911
979
  password
912
- } });
980
+ } }));
913
981
  }
914
982
  /**
915
983
  * Get repository description including collections
916
984
  */
917
985
  async describeRepo(did) {
918
- return this.xrpc("GET", "com.atproto.repo.describeRepo", { params: { repo: did } });
986
+ return ok(this.client.get("com.atproto.repo.describeRepo", { params: { repo: did } }));
919
987
  }
920
988
  /**
921
989
  * Get profile stats from AppView (posts, follows, followers counts)
@@ -938,96 +1006,199 @@ var PDSClient = class {
938
1006
  * Export repository as CAR file
939
1007
  */
940
1008
  async getRepo(did) {
941
- const { bytes } = await this.xrpcBytes("GET", "com.atproto.sync.getRepo", { params: { did } });
942
- return bytes;
1009
+ const response = await this.client.get("com.atproto.sync.getRepo", {
1010
+ params: { did },
1011
+ as: "bytes"
1012
+ });
1013
+ if (!response.ok) throw new ClientResponseError({
1014
+ status: response.status,
1015
+ headers: response.headers,
1016
+ data: response.data
1017
+ });
1018
+ return response.data;
943
1019
  }
944
1020
  /**
945
1021
  * Get a blob by CID
946
1022
  */
947
1023
  async getBlob(did, cid) {
948
- return this.xrpcBytes("GET", "com.atproto.sync.getBlob", { params: {
949
- did,
950
- cid
951
- } });
1024
+ const response = await this.client.get("com.atproto.sync.getBlob", {
1025
+ params: {
1026
+ did,
1027
+ cid
1028
+ },
1029
+ as: "bytes"
1030
+ });
1031
+ if (!response.ok) throw new ClientResponseError({
1032
+ status: response.status,
1033
+ headers: response.headers,
1034
+ data: response.data
1035
+ });
1036
+ return {
1037
+ bytes: response.data,
1038
+ mimeType: response.headers.get("content-type") ?? "application/octet-stream"
1039
+ };
952
1040
  }
953
1041
  /**
954
1042
  * List blobs in repository
955
1043
  */
956
1044
  async listBlobs(did, cursor) {
957
- const params = { did };
958
- if (cursor) params.cursor = cursor;
959
- return this.xrpc("GET", "com.atproto.sync.listBlobs", { params });
1045
+ return ok(this.client.get("com.atproto.sync.listBlobs", { params: {
1046
+ did,
1047
+ ...cursor && { cursor }
1048
+ } }));
960
1049
  }
961
1050
  /**
962
1051
  * Get user preferences
963
1052
  */
964
1053
  async getPreferences() {
965
- return (await this.xrpc("GET", "app.bsky.actor.getPreferences", { auth: true })).preferences;
1054
+ return (await ok(this.client.get("app.bsky.actor.getPreferences", { params: {} }))).preferences;
966
1055
  }
967
1056
  /**
968
1057
  * Update user preferences
969
1058
  */
970
1059
  async putPreferences(preferences) {
971
- await this.xrpc("POST", "app.bsky.actor.putPreferences", {
972
- body: { preferences },
973
- auth: true
1060
+ const url = new URL("/xrpc/app.bsky.actor.putPreferences", this.baseUrl);
1061
+ const headers = { "Content-Type": "application/json" };
1062
+ if (this.authToken) headers["Authorization"] = `Bearer ${this.authToken}`;
1063
+ const res = await fetch(url.toString(), {
1064
+ method: "POST",
1065
+ headers,
1066
+ body: JSON.stringify({ preferences })
974
1067
  });
1068
+ if (!res.ok) {
1069
+ const errorBody = await res.json().catch(() => ({}));
1070
+ throw new ClientResponseError({
1071
+ status: res.status,
1072
+ headers: res.headers,
1073
+ data: {
1074
+ error: errorBody.error ?? "Unknown",
1075
+ message: errorBody.message
1076
+ }
1077
+ });
1078
+ }
975
1079
  }
976
1080
  /**
977
1081
  * Get account status including migration progress
978
1082
  */
979
1083
  async getAccountStatus() {
980
- return this.xrpc("GET", "com.atproto.server.getAccountStatus", { auth: true });
1084
+ const url = new URL("/xrpc/com.atproto.server.getAccountStatus", this.baseUrl);
1085
+ const headers = {};
1086
+ if (this.authToken) headers["Authorization"] = `Bearer ${this.authToken}`;
1087
+ const res = await fetch(url.toString(), {
1088
+ method: "GET",
1089
+ headers
1090
+ });
1091
+ if (!res.ok) {
1092
+ const errorBody = await res.json().catch(() => ({}));
1093
+ throw new ClientResponseError({
1094
+ status: res.status,
1095
+ headers: res.headers,
1096
+ data: {
1097
+ error: errorBody.error ?? "Unknown",
1098
+ message: errorBody.message
1099
+ }
1100
+ });
1101
+ }
1102
+ return res.json();
981
1103
  }
982
1104
  /**
983
1105
  * Import repository from CAR file
984
1106
  */
985
1107
  async importRepo(carBytes) {
986
- return this.xrpc("POST", "com.atproto.repo.importRepo", {
987
- body: carBytes,
988
- contentType: "application/vnd.ipld.car",
989
- auth: true
1108
+ const url = new URL("/xrpc/com.atproto.repo.importRepo", this.baseUrl);
1109
+ const headers = { "Content-Type": "application/vnd.ipld.car" };
1110
+ if (this.authToken) headers["Authorization"] = `Bearer ${this.authToken}`;
1111
+ const res = await fetch(url.toString(), {
1112
+ method: "POST",
1113
+ headers,
1114
+ body: carBytes
990
1115
  });
1116
+ if (!res.ok) {
1117
+ const errorBody = await res.json().catch(() => ({}));
1118
+ throw new ClientResponseError({
1119
+ status: res.status,
1120
+ headers: res.headers,
1121
+ data: {
1122
+ error: errorBody.error ?? "Unknown",
1123
+ message: errorBody.message
1124
+ }
1125
+ });
1126
+ }
1127
+ return res.json();
991
1128
  }
992
1129
  /**
993
1130
  * List blobs that are missing (referenced but not imported)
994
1131
  */
995
1132
  async listMissingBlobs(limit, cursor) {
996
- const params = {};
997
- if (limit) params.limit = String(limit);
998
- if (cursor) params.cursor = cursor;
999
- return this.xrpc("GET", "com.atproto.repo.listMissingBlobs", {
1000
- params,
1001
- auth: true
1002
- });
1133
+ return ok(this.client.get("com.atproto.repo.listMissingBlobs", { params: {
1134
+ ...limit && { limit },
1135
+ ...cursor && { cursor }
1136
+ } }));
1003
1137
  }
1004
1138
  /**
1005
1139
  * Upload a blob
1006
1140
  */
1007
1141
  async uploadBlob(bytes, mimeType) {
1008
- return (await this.xrpc("POST", "com.atproto.repo.uploadBlob", {
1009
- body: bytes,
1010
- contentType: mimeType,
1011
- auth: true
1012
- })).blob;
1142
+ const url = new URL("/xrpc/com.atproto.repo.uploadBlob", this.baseUrl);
1143
+ const headers = { "Content-Type": mimeType };
1144
+ if (this.authToken) headers["Authorization"] = `Bearer ${this.authToken}`;
1145
+ const res = await fetch(url.toString(), {
1146
+ method: "POST",
1147
+ headers,
1148
+ body: bytes
1149
+ });
1150
+ if (!res.ok) {
1151
+ const errorBody = await res.json().catch(() => ({}));
1152
+ throw new ClientResponseError({
1153
+ status: res.status,
1154
+ headers: res.headers,
1155
+ data: {
1156
+ error: errorBody.error ?? "Unknown",
1157
+ message: errorBody.message
1158
+ }
1159
+ });
1160
+ }
1161
+ return (await res.json()).blob;
1013
1162
  }
1014
1163
  /**
1015
1164
  * Reset migration state (only works on deactivated accounts)
1165
+ * Custom endpoint - not in standard lexicons
1016
1166
  */
1017
1167
  async resetMigration() {
1018
- return this.xrpc("POST", "gg.mk.experimental.resetMigration", { auth: true });
1168
+ const url = new URL("/xrpc/gg.mk.experimental.resetMigration", this.baseUrl);
1169
+ const headers = {};
1170
+ if (this.authToken) headers["Authorization"] = `Bearer ${this.authToken}`;
1171
+ const res = await fetch(url.toString(), {
1172
+ method: "POST",
1173
+ headers
1174
+ });
1175
+ if (!res.ok) {
1176
+ const errorBody = await res.json().catch(() => ({}));
1177
+ throw new ClientResponseError({
1178
+ status: res.status,
1179
+ headers: res.headers,
1180
+ data: {
1181
+ error: errorBody.error ?? "Unknown",
1182
+ message: errorBody.message
1183
+ }
1184
+ });
1185
+ }
1186
+ return res.json();
1019
1187
  }
1020
1188
  /**
1021
1189
  * Activate account to enable writes
1022
1190
  */
1023
1191
  async activateAccount() {
1024
- await this.xrpc("POST", "com.atproto.server.activateAccount", { auth: true });
1192
+ await ok(this.client.post("com.atproto.server.activateAccount", { as: null }));
1025
1193
  }
1026
1194
  /**
1027
1195
  * Deactivate account to disable writes
1028
1196
  */
1029
1197
  async deactivateAccount() {
1030
- await this.xrpc("POST", "com.atproto.server.deactivateAccount", { auth: true });
1198
+ await ok(this.client.post("com.atproto.server.deactivateAccount", {
1199
+ input: {},
1200
+ as: null
1201
+ }));
1031
1202
  }
1032
1203
  /**
1033
1204
  * Check if the PDS is reachable
@@ -1039,17 +1210,128 @@ var PDSClient = class {
1039
1210
  return false;
1040
1211
  }
1041
1212
  }
1213
+ /**
1214
+ * Get DID document from PDS
1215
+ */
1216
+ async getDidDocument() {
1217
+ const res = await fetch(new URL("/.well-known/did.json", this.baseUrl));
1218
+ if (!res.ok) throw new Error("Failed to fetch DID document");
1219
+ return res.json();
1220
+ }
1221
+ /**
1222
+ * Resolve handle to DID via public API
1223
+ */
1224
+ async resolveHandle(handle) {
1225
+ try {
1226
+ const res = await fetch(`https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handle)}`);
1227
+ if (!res.ok) return null;
1228
+ return (await res.json()).did;
1229
+ } catch {
1230
+ return null;
1231
+ }
1232
+ }
1233
+ /**
1234
+ * Resolve DID to get service endpoints (supports did:plc and did:web)
1235
+ */
1236
+ async resolveDid(did) {
1237
+ try {
1238
+ let doc;
1239
+ if (did.startsWith("did:plc:")) {
1240
+ const res = await fetch(`https://plc.directory/${did}`);
1241
+ if (!res.ok) return { pdsEndpoint: null };
1242
+ doc = await res.json();
1243
+ } else if (did.startsWith("did:web:")) {
1244
+ const hostname = did.slice(8);
1245
+ const res = await fetch(`https://${hostname}/.well-known/did.json`);
1246
+ if (!res.ok) return { pdsEndpoint: null };
1247
+ doc = await res.json();
1248
+ } else return { pdsEndpoint: null };
1249
+ return { pdsEndpoint: (doc.service?.find((s) => s.type === "AtprotoPersonalDataServer"))?.serviceEndpoint ?? null };
1250
+ } catch {
1251
+ return { pdsEndpoint: null };
1252
+ }
1253
+ }
1254
+ /**
1255
+ * Check if profile is indexed by AppView
1256
+ */
1257
+ async checkAppViewIndexing(did) {
1258
+ try {
1259
+ return (await fetch(`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(did)}`)).ok;
1260
+ } catch {
1261
+ return false;
1262
+ }
1263
+ }
1264
+ /**
1265
+ * Get firehose status (subscribers, seq)
1266
+ * Custom endpoint - not in standard lexicons
1267
+ */
1268
+ async getFirehoseStatus() {
1269
+ const url = new URL("/xrpc/gg.mk.experimental.getFirehoseStatus", this.baseUrl);
1270
+ const headers = {};
1271
+ if (this.authToken) headers["Authorization"] = `Bearer ${this.authToken}`;
1272
+ const res = await fetch(url.toString(), {
1273
+ method: "GET",
1274
+ headers
1275
+ });
1276
+ if (!res.ok) {
1277
+ const errorBody = await res.json().catch(() => ({}));
1278
+ throw new ClientResponseError({
1279
+ status: res.status,
1280
+ headers: res.headers,
1281
+ data: {
1282
+ error: errorBody.error ?? "Unknown",
1283
+ message: errorBody.message
1284
+ }
1285
+ });
1286
+ }
1287
+ return res.json();
1288
+ }
1289
+ /**
1290
+ * Check handle verification via HTTP well-known
1291
+ */
1292
+ async checkHandleViaHttp(handle) {
1293
+ try {
1294
+ const res = await fetch(`https://${handle}/.well-known/atproto-did`);
1295
+ if (!res.ok) return null;
1296
+ return (await res.text()).trim() || null;
1297
+ } catch {
1298
+ return null;
1299
+ }
1300
+ }
1301
+ /**
1302
+ * Check handle verification via DNS TXT record (using DNS-over-HTTPS)
1303
+ */
1304
+ async checkHandleViaDns(handle) {
1305
+ try {
1306
+ const res = await fetch(`https://cloudflare-dns.com/dns-query?name=_atproto.${handle}&type=TXT`, { headers: { Accept: "application/dns-json" } });
1307
+ if (!res.ok) return null;
1308
+ const txtRecord = (await res.json()).Answer?.find((a) => a.data?.includes("did="));
1309
+ if (!txtRecord) return null;
1310
+ return txtRecord.data.match(/did=([^\s"]+)/)?.[1] ?? null;
1311
+ } catch {
1312
+ return null;
1313
+ }
1314
+ }
1315
+ /**
1316
+ * Request the relay to crawl this PDS.
1317
+ * This notifies the Bluesky relay that the PDS is active and ready for federation.
1318
+ */
1319
+ async requestCrawl(pdsHostname, relayUrl = "https://bsky.network") {
1320
+ try {
1321
+ const url = new URL("/xrpc/com.atproto.sync.requestCrawl", relayUrl);
1322
+ return (await fetch(url.toString(), {
1323
+ method: "POST",
1324
+ headers: { "Content-Type": "application/json" },
1325
+ body: JSON.stringify({ hostname: pdsHostname })
1326
+ })).ok;
1327
+ } catch {
1328
+ return false;
1329
+ }
1330
+ }
1042
1331
  };
1043
1332
 
1044
1333
  //#endregion
1045
1334
  //#region src/cli/commands/migrate.ts
1046
- function detectPackageManager() {
1047
- const userAgent = process.env.npm_config_user_agent || "";
1048
- if (userAgent.startsWith("yarn")) return "yarn";
1049
- if (userAgent.startsWith("pnpm")) return "pnpm";
1050
- if (userAgent.startsWith("bun")) return "bun";
1051
- return "npm";
1052
- }
1053
1335
  const brightNote$1 = (lines) => lines.map((l) => `\x1b[0m${l}`).join("\n");
1054
1336
  /**
1055
1337
  * Format number with commas
@@ -1083,8 +1365,7 @@ const migrateCommand = defineCommand({
1083
1365
  }
1084
1366
  },
1085
1367
  async run({ args }) {
1086
- const packageManager = detectPackageManager();
1087
- const pm = packageManager === "npm" ? "npm run" : packageManager;
1368
+ const pm = detectPackageManager();
1088
1369
  const isDev = args.dev;
1089
1370
  const vars = getVars();
1090
1371
  let targetUrl;
@@ -1104,11 +1385,11 @@ const migrateCommand = defineCommand({
1104
1385
  spinner.stop(`PDS not responding at ${targetDomain}`);
1105
1386
  if (isDev) {
1106
1387
  p.log.error(`Your local PDS isn't running at ${targetUrl}`);
1107
- p.log.info(`Start it with: ${pm} dev`);
1388
+ p.log.info(`Start it with: ${formatCommand(pm, "dev")}`);
1108
1389
  } else {
1109
1390
  p.log.error(`Your PDS isn't responding at ${targetUrl}`);
1110
- p.log.info("Make sure your worker is deployed: wrangler deploy");
1111
- p.log.info(`Or test locally first: ${pm} pds migrate --dev`);
1391
+ p.log.info(`Make sure your worker is deployed: ${formatCommand(pm, "deploy")}`);
1392
+ p.log.info(`Or test locally first: ${formatCommand(pm, "pds", "migrate", "--dev")}`);
1112
1393
  }
1113
1394
  p.outro("Migration cancelled.");
1114
1395
  process.exit(1);
@@ -1169,7 +1450,7 @@ const migrateCommand = defineCommand({
1169
1450
  p.log.info("Your account is already live");
1170
1451
  p.log.info("");
1171
1452
  p.log.info("If you need to re-import, first deactivate:");
1172
- p.log.info(" pnpm pds deactivate");
1453
+ p.log.info(` ${formatCommand(pm, "pds", "deactivate")}`);
1173
1454
  p.outro("Migration cancelled.");
1174
1455
  process.exit(1);
1175
1456
  }
@@ -1285,7 +1566,7 @@ const migrateCommand = defineCommand({
1285
1566
  spinner.stop("Authenticated successfully");
1286
1567
  } catch (err) {
1287
1568
  spinner.stop("Login failed");
1288
- if (err instanceof PDSClientError) p.log.error(`Authentication failed: ${err.message}`);
1569
+ if (err instanceof ClientResponseError) p.log.error(`Authentication failed: ${err.description ?? err.message}`);
1289
1570
  else p.log.error(err instanceof Error ? err.message : "Authentication failed");
1290
1571
  p.outro("Migration cancelled.");
1291
1572
  process.exit(1);
@@ -1385,7 +1666,7 @@ function showNextSteps(pm, sourceDomain) {
1385
1666
  ` (Requires email verification from ${sourceDomain})`,
1386
1667
  "",
1387
1668
  pc.bold("2. Flip the switch"),
1388
- ` ${pm} pds activate`,
1669
+ ` ${formatCommand(pm, "pds", "activate")}`,
1389
1670
  "",
1390
1671
  "Docs: https://atproto.com/guides/account-migration"
1391
1672
  ]), "Almost there!");
@@ -1407,6 +1688,7 @@ const activateCommand = defineCommand({
1407
1688
  default: false
1408
1689
  } },
1409
1690
  async run({ args }) {
1691
+ const pm = detectPackageManager();
1410
1692
  const isDev = args.dev;
1411
1693
  p.intro("🦋 Activate Account");
1412
1694
  const vars = getVars();
@@ -1437,7 +1719,7 @@ const activateCommand = defineCommand({
1437
1719
  if (!await client.healthCheck()) {
1438
1720
  spinner.stop(`PDS not responding at ${targetDomain}`);
1439
1721
  p.log.error(`Your PDS isn't responding at ${targetUrl}`);
1440
- if (!isDev) p.log.info("Make sure your worker is deployed: wrangler deploy");
1722
+ if (!isDev) p.log.info(`Make sure your worker is deployed: ${formatCommand(pm, "deploy")}`);
1441
1723
  p.outro("Activation cancelled.");
1442
1724
  process.exit(1);
1443
1725
  }
@@ -1446,8 +1728,23 @@ const activateCommand = defineCommand({
1446
1728
  const status = await client.getAccountStatus();
1447
1729
  spinner.stop("Account status retrieved");
1448
1730
  if (status.active) {
1449
- p.log.warn("Your account is already active!");
1450
- p.log.info("No action needed - you're live in the Atmosphere. 🦋");
1731
+ p.log.info("Your account is already active.");
1732
+ const pdsHostname$1 = config.PDS_HOSTNAME;
1733
+ if (pdsHostname$1 && !isDev) {
1734
+ const pingRelay = await p.confirm({
1735
+ message: "Notify the relay? (useful if posts aren't being indexed)",
1736
+ initialValue: false
1737
+ });
1738
+ if (p.isCancel(pingRelay)) {
1739
+ p.cancel("Cancelled.");
1740
+ process.exit(0);
1741
+ }
1742
+ if (pingRelay) {
1743
+ spinner.start("Notifying relay...");
1744
+ if (await client.requestCrawl(pdsHostname$1)) spinner.stop("Relay notified");
1745
+ else spinner.stop("Could not notify relay");
1746
+ }
1747
+ }
1451
1748
  p.outro("All good!");
1452
1749
  return;
1453
1750
  }
@@ -1477,6 +1774,15 @@ const activateCommand = defineCommand({
1477
1774
  p.outro("Activation failed.");
1478
1775
  process.exit(1);
1479
1776
  }
1777
+ const pdsHostname = config.PDS_HOSTNAME;
1778
+ if (pdsHostname && !isDev) {
1779
+ spinner.start("Notifying relay...");
1780
+ if (await client.requestCrawl(pdsHostname)) spinner.stop("Relay notified");
1781
+ else {
1782
+ spinner.stop("Could not notify relay");
1783
+ p.log.warn("Run 'pds activate' again later to retry notifying the relay.");
1784
+ }
1785
+ }
1480
1786
  p.log.success("Welcome to the Atmosphere! 🦋");
1481
1787
  p.log.info("Your account is now live and accepting writes.");
1482
1788
  p.outro("All set!");
@@ -1501,6 +1807,7 @@ const deactivateCommand = defineCommand({
1501
1807
  default: false
1502
1808
  } },
1503
1809
  async run({ args }) {
1810
+ const pm = detectPackageManager();
1504
1811
  const isDev = args.dev;
1505
1812
  p.intro("🦋 Deactivate Account");
1506
1813
  const vars = getVars();
@@ -1531,7 +1838,7 @@ const deactivateCommand = defineCommand({
1531
1838
  if (!await client.healthCheck()) {
1532
1839
  spinner.stop(`PDS not responding at ${targetDomain}`);
1533
1840
  p.log.error(`Your PDS isn't responding at ${targetUrl}`);
1534
- if (!isDev) p.log.info("Make sure your worker is deployed: wrangler deploy");
1841
+ if (!isDev) p.log.info(`Make sure your worker is deployed: ${formatCommand(pm, "deploy")}`);
1535
1842
  p.outro("Deactivation cancelled.");
1536
1843
  process.exit(1);
1537
1844
  }
@@ -1577,14 +1884,190 @@ const deactivateCommand = defineCommand({
1577
1884
  p.log.info("Writes are now disabled.");
1578
1885
  p.log.info("");
1579
1886
  p.log.info("To re-import your data:");
1580
- p.log.info(" pnpm pds migrate --clean");
1887
+ p.log.info(` ${formatCommand(pm, "pds", "migrate", "--clean")}`);
1581
1888
  p.log.info("");
1582
1889
  p.log.info("To re-enable writes:");
1583
- p.log.info(" pnpm pds activate");
1890
+ p.log.info(` ${formatCommand(pm, "pds", "activate")}`);
1584
1891
  p.outro("Deactivated.");
1585
1892
  }
1586
1893
  });
1587
1894
 
1895
+ //#endregion
1896
+ //#region src/cli/commands/status.ts
1897
+ /**
1898
+ * Status command - comprehensive PDS health and configuration check
1899
+ */
1900
+ const CHECK = pc.green("✓");
1901
+ const CROSS = pc.red("✗");
1902
+ const WARN = pc.yellow("!");
1903
+ const INFO = pc.cyan("ℹ");
1904
+ const statusCommand = defineCommand({
1905
+ meta: {
1906
+ name: "status",
1907
+ description: "Check PDS health and configuration"
1908
+ },
1909
+ args: { dev: {
1910
+ type: "boolean",
1911
+ description: "Target local development server instead of production",
1912
+ default: false
1913
+ } },
1914
+ async run({ args }) {
1915
+ const isDev = args.dev;
1916
+ const wranglerVars = getVars();
1917
+ const config = {
1918
+ ...readDevVars(),
1919
+ ...wranglerVars
1920
+ };
1921
+ let targetUrl;
1922
+ try {
1923
+ targetUrl = getTargetUrl(isDev, config.PDS_HOSTNAME);
1924
+ } catch (err) {
1925
+ console.error(pc.red("Error:"), err instanceof Error ? err.message : "Configuration error");
1926
+ console.log(pc.dim("Run 'pds init' first to configure your PDS."));
1927
+ process.exit(1);
1928
+ }
1929
+ const authToken = config.AUTH_TOKEN;
1930
+ const did = config.DID;
1931
+ const handle = config.HANDLE;
1932
+ const pdsHostname = config.PDS_HOSTNAME;
1933
+ if (!authToken) {
1934
+ console.error(pc.red("Error:"), "No AUTH_TOKEN found. Run 'pds init' first.");
1935
+ process.exit(1);
1936
+ }
1937
+ console.log();
1938
+ console.log(pc.bold("PDS Status Check"));
1939
+ console.log("=".repeat(50));
1940
+ console.log(`Endpoint: ${pc.cyan(targetUrl)}`);
1941
+ console.log();
1942
+ const client = new PDSClient(targetUrl, authToken);
1943
+ let hasErrors = false;
1944
+ let hasWarnings = false;
1945
+ console.log(pc.bold("Connectivity"));
1946
+ if (await client.healthCheck()) console.log(` ${CHECK} PDS reachable`);
1947
+ else {
1948
+ console.log(` ${CROSS} PDS not responding`);
1949
+ hasErrors = true;
1950
+ console.log();
1951
+ console.log(pc.red("Cannot continue - PDS is not reachable."));
1952
+ if (!isDev) console.log(pc.dim("Make sure your worker is deployed: wrangler deploy"));
1953
+ process.exit(1);
1954
+ }
1955
+ let status;
1956
+ try {
1957
+ status = await client.getAccountStatus();
1958
+ console.log(` ${CHECK} Account status retrieved`);
1959
+ } catch (err) {
1960
+ console.log(` ${CROSS} Failed to get account status`);
1961
+ hasErrors = true;
1962
+ console.log();
1963
+ console.log(pc.red("Error:"), err instanceof Error ? err.message : "Unknown error");
1964
+ process.exit(1);
1965
+ }
1966
+ console.log();
1967
+ console.log(pc.bold("Repository"));
1968
+ if (status.repoCommit && status.indexedRecords > 0) {
1969
+ const shortCid = status.repoCommit.slice(0, 12) + "..." + status.repoCommit.slice(-4);
1970
+ const shortRev = status.repoRev ? status.repoRev.slice(0, 8) + "..." : "none";
1971
+ console.log(` ${CHECK} Initialized: ${pc.dim(shortCid)} (rev: ${shortRev})`);
1972
+ console.log(` ${INFO} ${status.repoBlocks.toLocaleString()} blocks, ${status.indexedRecords.toLocaleString()} records`);
1973
+ } else {
1974
+ console.log(` ${WARN} Repository empty (no records)`);
1975
+ console.log(pc.dim(" Run 'pds migrate' to import from another PDS"));
1976
+ hasWarnings = true;
1977
+ }
1978
+ console.log();
1979
+ console.log(pc.bold("Identity"));
1980
+ if (did) {
1981
+ const didType = did.startsWith("did:plc:") ? "did:plc" : did.startsWith("did:web:") ? "did:web" : "unknown";
1982
+ console.log(` ${INFO} DID: ${pc.dim(did)} (${didType})`);
1983
+ }
1984
+ if (handle) console.log(` ${INFO} Handle: ${pc.cyan(`@${handle}`)}`);
1985
+ if (did) {
1986
+ const resolved = await client.resolveDid(did);
1987
+ const resolveMethod = did.startsWith("did:plc:") ? "plc.directory" : did.startsWith("did:web:") ? "/.well-known/did.json" : "unknown";
1988
+ if (resolved.pdsEndpoint) {
1989
+ const expectedEndpoint = `https://${pdsHostname}`;
1990
+ if (resolved.pdsEndpoint === expectedEndpoint || resolved.pdsEndpoint === pdsHostname) console.log(` ${CHECK} DID resolves to this PDS (via ${resolveMethod})`);
1991
+ else {
1992
+ console.log(` ${CROSS} DID resolves to different PDS`);
1993
+ console.log(pc.dim(` Resolved via: ${resolveMethod}`));
1994
+ console.log(pc.dim(` Expected: ${expectedEndpoint}`));
1995
+ console.log(pc.dim(` Got: ${resolved.pdsEndpoint}`));
1996
+ hasErrors = true;
1997
+ }
1998
+ } else {
1999
+ console.log(` ${WARN} Could not resolve DID`);
2000
+ if (did.startsWith("did:plc:")) console.log(pc.dim(" Check plc.directory or update DID document"));
2001
+ else if (did.startsWith("did:web:")) console.log(pc.dim(" Ensure /.well-known/did.json is accessible"));
2002
+ hasWarnings = true;
2003
+ }
2004
+ } else {
2005
+ console.log(` ${WARN} DID not configured`);
2006
+ hasWarnings = true;
2007
+ }
2008
+ if (handle) {
2009
+ const [httpDid, dnsDid] = await Promise.all([client.checkHandleViaHttp(handle), client.checkHandleViaDns(handle)]);
2010
+ const httpValid = httpDid === did;
2011
+ const dnsValid = dnsDid === did;
2012
+ if (httpValid || dnsValid) {
2013
+ const methods = [];
2014
+ if (dnsValid) methods.push("DNS");
2015
+ if (httpValid) methods.push("HTTP");
2016
+ console.log(` ${CHECK} Handle verified via ${methods.join(" + ")}`);
2017
+ } else if (httpDid || dnsDid) {
2018
+ console.log(` ${CROSS} Handle resolves to different DID`);
2019
+ console.log(pc.dim(` Expected: ${did}`));
2020
+ if (httpDid) console.log(pc.dim(` HTTP well-known: ${httpDid}`));
2021
+ if (dnsDid) console.log(pc.dim(` DNS TXT: ${dnsDid}`));
2022
+ hasErrors = true;
2023
+ } else {
2024
+ console.log(` ${WARN} Handle not resolving`);
2025
+ if (handle === pdsHostname) console.log(pc.dim(" Ensure /.well-known/atproto-did returns your DID"));
2026
+ else console.log(pc.dim(` Add DNS TXT record: _atproto.${handle} → did=...`));
2027
+ hasWarnings = true;
2028
+ }
2029
+ }
2030
+ console.log();
2031
+ if (status.expectedBlobs > 0) {
2032
+ console.log(pc.bold("Blobs"));
2033
+ if (status.importedBlobs === status.expectedBlobs) console.log(` ${CHECK} ${status.importedBlobs}/${status.expectedBlobs} blobs imported`);
2034
+ else {
2035
+ const missing = status.expectedBlobs - status.importedBlobs;
2036
+ console.log(` ${WARN} ${status.importedBlobs}/${status.expectedBlobs} blobs imported (${missing} missing)`);
2037
+ hasWarnings = true;
2038
+ }
2039
+ console.log();
2040
+ }
2041
+ console.log(pc.bold("Federation"));
2042
+ if (did) if (await client.checkAppViewIndexing(did)) console.log(` ${CHECK} Profile indexed by AppView`);
2043
+ else {
2044
+ console.log(` ${WARN} Profile not found on AppView`);
2045
+ console.log(pc.dim(" This may be normal for new accounts"));
2046
+ hasWarnings = true;
2047
+ }
2048
+ try {
2049
+ const firehose = await client.getFirehoseStatus();
2050
+ console.log(` ${INFO} ${firehose.subscribers} firehose subscriber${firehose.subscribers !== 1 ? "s" : ""}, seq: ${firehose.latestSeq ?? "none"}`);
2051
+ } catch {
2052
+ console.log(` ${pc.dim(" Could not get firehose status")}`);
2053
+ }
2054
+ console.log();
2055
+ console.log(pc.bold("Account"));
2056
+ if (status.active) console.log(` ${CHECK} Active (accepting writes)`);
2057
+ else {
2058
+ console.log(` ${WARN} Deactivated (writes disabled)`);
2059
+ console.log(pc.dim(" Run 'pds activate' when ready to go live"));
2060
+ hasWarnings = true;
2061
+ }
2062
+ console.log();
2063
+ if (hasErrors) {
2064
+ console.log(pc.red(pc.bold("Some checks failed!")));
2065
+ process.exit(1);
2066
+ } else if (hasWarnings) console.log(pc.yellow("All checks passed with warnings."));
2067
+ else console.log(pc.green(pc.bold("All checks passed!")));
2068
+ }
2069
+ });
2070
+
1588
2071
  //#endregion
1589
2072
  //#region src/cli/index.ts
1590
2073
  /**
@@ -1601,7 +2084,8 @@ runMain(defineCommand({
1601
2084
  secret: secretCommand,
1602
2085
  migrate: migrateCommand,
1603
2086
  activate: activateCommand,
1604
- deactivate: deactivateCommand
2087
+ deactivate: deactivateCommand,
2088
+ status: statusCommand
1605
2089
  }
1606
2090
  }));
1607
2091