@getcirrus/pds 0.3.0 → 0.4.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
@@ -11,6 +11,8 @@ import { resolve } from "node:path";
11
11
  import { CompositeDidDocumentResolver, DohJsonHandleResolver, PlcDidDocumentResolver, WebDidDocumentResolver } from "@atcute/identity-resolver";
12
12
  import pc from "picocolors";
13
13
  import { Client, ClientResponseError, ok } from "@atcute/client";
14
+ import "@atcute/bluesky";
15
+ import "@atcute/atproto";
14
16
  import { getPdsEndpoint } from "@atcute/identity";
15
17
 
16
18
  //#region src/cli/utils/wrangler.ts
@@ -955,7 +957,7 @@ function createAuthHandler(baseUrl, token) {
955
957
  });
956
958
  };
957
959
  }
958
- var PDSClient = class {
960
+ var PDSClient = class PDSClient {
959
961
  client;
960
962
  authToken;
961
963
  constructor(baseUrl, authToken) {
@@ -1201,6 +1203,23 @@ var PDSClient = class {
1201
1203
  }));
1202
1204
  }
1203
1205
  /**
1206
+ * Emit identity event to notify relays to refresh handle verification
1207
+ */
1208
+ async emitIdentity() {
1209
+ const url = new URL("/xrpc/gg.mk.experimental.emitIdentityEvent", this.baseUrl);
1210
+ const headers = {};
1211
+ if (this.authToken) headers["Authorization"] = `Bearer ${this.authToken}`;
1212
+ const res = await fetch(url.toString(), {
1213
+ method: "POST",
1214
+ headers
1215
+ });
1216
+ if (!res.ok) {
1217
+ const errorBody = await res.json().catch(() => ({}));
1218
+ throw new Error(errorBody.message ?? `Request failed: ${res.status}`);
1219
+ }
1220
+ return res.json();
1221
+ }
1222
+ /**
1204
1223
  * Check if the PDS is reachable
1205
1224
  */
1206
1225
  async healthCheck() {
@@ -1313,8 +1332,78 @@ var PDSClient = class {
1313
1332
  }
1314
1333
  }
1315
1334
  /**
1335
+ * Get a record from the repository
1336
+ */
1337
+ async getRecord(repo, collection, rkey) {
1338
+ try {
1339
+ return await ok(this.client.get("com.atproto.repo.getRecord", { params: {
1340
+ repo,
1341
+ collection,
1342
+ rkey
1343
+ } }));
1344
+ } catch (err) {
1345
+ if (err instanceof ClientResponseError && err.status === 404) return null;
1346
+ throw err;
1347
+ }
1348
+ }
1349
+ /**
1350
+ * Create or update a record in the repository
1351
+ */
1352
+ async putRecord(repo, collection, rkey, record) {
1353
+ return ok(this.client.post("com.atproto.repo.putRecord", { input: {
1354
+ repo,
1355
+ collection,
1356
+ rkey,
1357
+ record
1358
+ } }));
1359
+ }
1360
+ /**
1361
+ * Get the user's profile record
1362
+ */
1363
+ async getProfile(did) {
1364
+ const record = await this.getRecord(did, "app.bsky.actor.profile", "self");
1365
+ if (!record) return null;
1366
+ return record.value;
1367
+ }
1368
+ /**
1369
+ * Create or update the user's profile
1370
+ */
1371
+ async putProfile(did, profile) {
1372
+ return this.putRecord(did, "app.bsky.actor.profile", "self", {
1373
+ $type: "app.bsky.actor.profile",
1374
+ ...profile
1375
+ });
1376
+ }
1377
+ static RELAY_URLS = ["https://relay1.us-west.bsky.network", "https://relay1.us-east.bsky.network"];
1378
+ /**
1379
+ * Get relay's view of this PDS host status from a single relay.
1380
+ * Calls com.atproto.sync.getHostStatus on the relay.
1381
+ */
1382
+ async getRelayHostStatus(pdsHostname, relayUrl) {
1383
+ try {
1384
+ const url = new URL("/xrpc/com.atproto.sync.getHostStatus", relayUrl);
1385
+ url.searchParams.set("hostname", pdsHostname);
1386
+ const res = await fetch(url.toString());
1387
+ if (!res.ok) return null;
1388
+ return {
1389
+ ...await res.json(),
1390
+ relay: relayUrl
1391
+ };
1392
+ } catch {
1393
+ return null;
1394
+ }
1395
+ }
1396
+ /**
1397
+ * Get relay status from all known relays.
1398
+ * Returns results from each relay that responds.
1399
+ */
1400
+ async getAllRelayHostStatus(pdsHostname) {
1401
+ return (await Promise.all(PDSClient.RELAY_URLS.map((url) => this.getRelayHostStatus(pdsHostname, url)))).filter((r) => r !== null);
1402
+ }
1403
+ /**
1316
1404
  * Request the relay to crawl this PDS.
1317
1405
  * This notifies the Bluesky relay that the PDS is active and ready for federation.
1406
+ * Uses bsky.network by default (the main relay endpoint).
1318
1407
  */
1319
1408
  async requestCrawl(pdsHostname, relayUrl = "https://bsky.network") {
1320
1409
  try {
@@ -1672,24 +1761,261 @@ function showNextSteps(pm, sourceDomain) {
1672
1761
  ]), "Almost there!");
1673
1762
  }
1674
1763
 
1764
+ //#endregion
1765
+ //#region src/cli/utils/checks.ts
1766
+ /**
1767
+ * Check that handle resolves to the expected DID
1768
+ */
1769
+ async function checkHandleResolution(handle, expectedDid) {
1770
+ const resolvedDid = await resolveHandleToDid(handle);
1771
+ if (!resolvedDid) return {
1772
+ ok: false,
1773
+ message: `@${handle} does not resolve to any DID`,
1774
+ detail: "Update your DNS TXT record or .well-known/atproto-did file"
1775
+ };
1776
+ if (resolvedDid !== expectedDid) return {
1777
+ ok: false,
1778
+ message: `@${handle} resolves to wrong DID`,
1779
+ detail: `Expected: ${expectedDid}\n Got: ${resolvedDid}`
1780
+ };
1781
+ return {
1782
+ ok: true,
1783
+ message: `@${handle} → ${expectedDid.slice(0, 24)}...`
1784
+ };
1785
+ }
1786
+ /**
1787
+ * Check that handle resolves via DNS and/or HTTP, returning both methods
1788
+ */
1789
+ async function checkHandleResolutionDetailed(client, handle, expectedDid) {
1790
+ const [httpDid, dnsDid] = await Promise.all([client.checkHandleViaHttp(handle), client.checkHandleViaDns(handle)]);
1791
+ const httpValid = httpDid === expectedDid;
1792
+ const dnsValid = dnsDid === expectedDid;
1793
+ const methods = [];
1794
+ if (dnsValid) methods.push("DNS");
1795
+ if (httpValid) methods.push("HTTP");
1796
+ return {
1797
+ ok: httpValid || dnsValid,
1798
+ httpDid,
1799
+ dnsDid,
1800
+ methods
1801
+ };
1802
+ }
1803
+ /**
1804
+ * Check that DID document points to the expected PDS endpoint
1805
+ */
1806
+ async function checkDidDocument(did, expectedPdsUrl) {
1807
+ const didDoc = await new DidResolver().resolve(did);
1808
+ const expectedEndpoint = expectedPdsUrl.replace(/\/$/, "");
1809
+ if (!didDoc) return {
1810
+ ok: false,
1811
+ message: `Could not resolve DID document for ${did}`,
1812
+ detail: "Make sure your DID is published to the PLC directory or did:web endpoint"
1813
+ };
1814
+ const pdsService = didDoc.service?.find((s) => {
1815
+ return (Array.isArray(s.type) ? s.type : [s.type]).includes("AtprotoPersonalDataServer") || s.id === "#atproto_pds";
1816
+ });
1817
+ if (!pdsService?.serviceEndpoint) return {
1818
+ ok: false,
1819
+ message: "DID document has no PDS service endpoint",
1820
+ detail: "Update your DID document to include an AtprotoPersonalDataServer service"
1821
+ };
1822
+ const actualEndpoint = pdsService.serviceEndpoint.replace(/\/$/, "");
1823
+ if (actualEndpoint !== expectedEndpoint) return {
1824
+ ok: false,
1825
+ message: "DID document points to different PDS",
1826
+ detail: `Expected: ${expectedEndpoint}\n Got: ${actualEndpoint}`
1827
+ };
1828
+ return {
1829
+ ok: true,
1830
+ message: `PDS endpoint → ${expectedEndpoint}`
1831
+ };
1832
+ }
1833
+ /**
1834
+ * Check that DID resolves and returns the PDS endpoint (simpler version using PDSClient)
1835
+ */
1836
+ async function checkDidResolution(client, did, expectedPdsHostname) {
1837
+ const resolved = await client.resolveDid(did);
1838
+ const resolveMethod = did.startsWith("did:plc:") ? "plc.directory" : did.startsWith("did:web:") ? "/.well-known/did.json" : "unknown";
1839
+ if (!resolved.pdsEndpoint) return {
1840
+ ok: false,
1841
+ pdsEndpoint: null,
1842
+ resolveMethod
1843
+ };
1844
+ const expectedEndpoint = `https://${expectedPdsHostname}`;
1845
+ return {
1846
+ ok: resolved.pdsEndpoint === expectedEndpoint || resolved.pdsEndpoint === expectedPdsHostname,
1847
+ pdsEndpoint: resolved.pdsEndpoint,
1848
+ resolveMethod
1849
+ };
1850
+ }
1851
+ /**
1852
+ * Check that all blobs are imported
1853
+ */
1854
+ function checkBlobsImported(status) {
1855
+ const missingBlobs = status.expectedBlobs - status.importedBlobs;
1856
+ if (missingBlobs > 0) return {
1857
+ ok: false,
1858
+ message: `${missingBlobs} blob${missingBlobs === 1 ? "" : "s"} missing`,
1859
+ detail: "Run 'pds migrate' to import missing blobs before activating"
1860
+ };
1861
+ return {
1862
+ ok: true,
1863
+ message: `${status.importedBlobs}/${status.expectedBlobs} blobs imported`
1864
+ };
1865
+ }
1866
+ /**
1867
+ * Check that repository data exists and is properly initialised
1868
+ */
1869
+ function checkRepoInitialised(status) {
1870
+ if (!status.repoCommit) return {
1871
+ ok: false,
1872
+ message: "No repo data imported",
1873
+ detail: "Run 'pds migrate' to import your repository first"
1874
+ };
1875
+ if (status.indexedRecords === 0) return {
1876
+ ok: false,
1877
+ message: "Repository has no indexed records",
1878
+ detail: "Run 'pds migrate' to import your repository"
1879
+ };
1880
+ return {
1881
+ ok: true,
1882
+ message: `${status.repoBlocks.toLocaleString()} blocks, ${status.indexedRecords.toLocaleString()} records`
1883
+ };
1884
+ }
1885
+ /**
1886
+ * Check that repo is complete for activation (combines blob and repo checks)
1887
+ */
1888
+ function checkRepoComplete(status) {
1889
+ const repoCheck = checkRepoInitialised(status);
1890
+ if (!repoCheck.ok) return repoCheck;
1891
+ if (status.expectedBlobs > 0) {
1892
+ const blobCheck = checkBlobsImported(status);
1893
+ if (!blobCheck.ok) return blobCheck;
1894
+ }
1895
+ return {
1896
+ ok: true,
1897
+ message: `${status.repoBlocks} blocks, ${status.importedBlobs} blobs`
1898
+ };
1899
+ }
1900
+ /**
1901
+ * Check if profile is indexed by AppView
1902
+ */
1903
+ async function checkAppViewIndexing(client, did) {
1904
+ if (!await client.checkAppViewIndexing(did)) return {
1905
+ ok: false,
1906
+ message: "Profile not found on AppView",
1907
+ detail: "This may be normal for new accounts"
1908
+ };
1909
+ return {
1910
+ ok: true,
1911
+ message: "Profile indexed by AppView"
1912
+ };
1913
+ }
1914
+
1675
1915
  //#endregion
1676
1916
  //#region src/cli/commands/activate.ts
1677
1917
  /**
1678
1918
  * Activate account command - enables writes after migration
1679
1919
  */
1920
+ /**
1921
+ * Run pre-activation checks
1922
+ */
1923
+ async function runChecks(handle, did, pdsUrl, status) {
1924
+ const checks = [];
1925
+ p.log.step("Checking handle resolution...");
1926
+ const handleResult = await checkHandleResolution(handle, did);
1927
+ checks.push({
1928
+ name: "Handle",
1929
+ ...handleResult
1930
+ });
1931
+ p.log.step("Checking DID document...");
1932
+ const didResult = await checkDidDocument(did, pdsUrl);
1933
+ checks.push({
1934
+ name: "DID",
1935
+ ...didResult
1936
+ });
1937
+ p.log.step("Checking repo status...");
1938
+ const repoResult = checkRepoComplete(status);
1939
+ checks.push({
1940
+ name: "Repo",
1941
+ ...repoResult
1942
+ });
1943
+ return checks;
1944
+ }
1945
+ function logCheck(check) {
1946
+ const icon = check.ok ? pc.green("✓") : pc.red("✗");
1947
+ const name = pc.bold(check.name.padEnd(8));
1948
+ console.log(` ${icon} ${name} ${check.message}`);
1949
+ if (check.detail && !check.ok) for (const line of check.detail.split("\n")) console.log(` ${pc.dim(line)}`);
1950
+ }
1951
+ /**
1952
+ * Prompt user to create a profile if one doesn't exist
1953
+ */
1954
+ async function promptCreateProfile(client, did, handle) {
1955
+ const spinner = p.spinner();
1956
+ spinner.start("Checking profile...");
1957
+ const existingProfile = await client.getProfile(did);
1958
+ spinner.stop(existingProfile ? "Profile found" : "No profile found");
1959
+ if (!existingProfile) {
1960
+ const createProfile = await p.confirm({
1961
+ message: "Create a profile? (recommended for visibility on the network)",
1962
+ initialValue: true
1963
+ });
1964
+ if (p.isCancel(createProfile)) {
1965
+ p.cancel("Cancelled.");
1966
+ process.exit(0);
1967
+ }
1968
+ if (createProfile) {
1969
+ const displayName = await promptText({
1970
+ message: "Display name:",
1971
+ placeholder: handle || "Your Name",
1972
+ validate: (v) => {
1973
+ if (v && v.length > 64) return "Display name must be 64 characters or less";
1974
+ }
1975
+ });
1976
+ const description = await promptText({
1977
+ message: "Bio (optional):",
1978
+ placeholder: "Tell us about yourself",
1979
+ validate: (v) => {
1980
+ if (v && v.length > 256) return "Bio must be 256 characters or less";
1981
+ }
1982
+ });
1983
+ spinner.start("Creating profile...");
1984
+ try {
1985
+ await client.putProfile(did, {
1986
+ displayName: displayName || void 0,
1987
+ description: description || void 0
1988
+ });
1989
+ spinner.stop("Profile created!");
1990
+ } catch (err) {
1991
+ spinner.stop("Failed to create profile");
1992
+ p.log.warn(err instanceof Error ? err.message : "Could not create profile");
1993
+ }
1994
+ }
1995
+ }
1996
+ }
1680
1997
  const activateCommand = defineCommand({
1681
1998
  meta: {
1682
1999
  name: "activate",
1683
2000
  description: "Activate your account to enable writes and go live"
1684
2001
  },
1685
- args: { dev: {
1686
- type: "boolean",
1687
- description: "Target local development server instead of production",
1688
- default: false
1689
- } },
2002
+ args: {
2003
+ dev: {
2004
+ type: "boolean",
2005
+ description: "Target local development server instead of production",
2006
+ default: false
2007
+ },
2008
+ yes: {
2009
+ type: "boolean",
2010
+ alias: "y",
2011
+ description: "Skip confirmation prompts",
2012
+ default: false
2013
+ }
2014
+ },
1690
2015
  async run({ args }) {
1691
2016
  const pm = detectPackageManager();
1692
2017
  const isDev = args.dev;
2018
+ const skipConfirm = args.yes;
1693
2019
  p.intro("🦋 Activate Account");
1694
2020
  const vars = getVars();
1695
2021
  let targetUrl;
@@ -1708,11 +2034,17 @@ const activateCommand = defineCommand({
1708
2034
  };
1709
2035
  const authToken = config.AUTH_TOKEN;
1710
2036
  const handle = config.HANDLE;
2037
+ const did = config.DID;
1711
2038
  if (!authToken) {
1712
2039
  p.log.error("No AUTH_TOKEN found. Run 'pds init' first.");
1713
2040
  p.outro("Activation cancelled.");
1714
2041
  process.exit(1);
1715
2042
  }
2043
+ if (!handle || !did) {
2044
+ p.log.error("No HANDLE or DID found. Run 'pds init' first.");
2045
+ p.outro("Activation cancelled.");
2046
+ process.exit(1);
2047
+ }
1716
2048
  const client = new PDSClient(targetUrl, authToken);
1717
2049
  const spinner = p.spinner();
1718
2050
  spinner.start(`Checking PDS at ${targetDomain}...`);
@@ -1729,6 +2061,7 @@ const activateCommand = defineCommand({
1729
2061
  spinner.stop("Account status retrieved");
1730
2062
  if (status.active) {
1731
2063
  p.log.info("Your account is already active.");
2064
+ await promptCreateProfile(client, did, handle);
1732
2065
  const pdsHostname$1 = config.PDS_HOSTNAME;
1733
2066
  if (pdsHostname$1 && !isDev) {
1734
2067
  const pingRelay = await p.confirm({
@@ -1740,29 +2073,44 @@ const activateCommand = defineCommand({
1740
2073
  process.exit(0);
1741
2074
  }
1742
2075
  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");
2076
+ spinner.start("Requesting crawl from relay...");
2077
+ if (await client.requestCrawl(pdsHostname$1)) spinner.stop("Crawl requested");
2078
+ else spinner.stop("Could not request crawl");
1746
2079
  }
1747
2080
  }
1748
2081
  p.outro("All good!");
1749
2082
  return;
1750
2083
  }
1751
- p.note([
1752
- `@${handle || "your-handle"}`,
1753
- "",
1754
- "This will enable writes and make your account live.",
1755
- "Make sure you've:",
1756
- " ✓ Updated your DID document to point here",
1757
- " ✓ Completed email verification (if required)"
1758
- ].join("\n"), "Ready to go live?");
1759
- const confirm = await p.confirm({
1760
- message: "Activate account?",
1761
- initialValue: true
1762
- });
1763
- if (p.isCancel(confirm) || !confirm) {
1764
- p.cancel("Activation cancelled.");
1765
- process.exit(0);
2084
+ p.log.info("");
2085
+ p.log.info(pc.bold("Pre-activation checks:"));
2086
+ const checks = await runChecks(handle, did, targetUrl, status);
2087
+ console.log("");
2088
+ for (const check of checks) logCheck(check);
2089
+ console.log("");
2090
+ const hasFailures = checks.some((c) => !c.ok);
2091
+ if (hasFailures) {
2092
+ p.log.warn(pc.yellow("Some checks failed. Activating now may cause issues."));
2093
+ p.log.info("");
2094
+ if (skipConfirm) p.log.info("Proceeding anyway (--yes flag)");
2095
+ else {
2096
+ const proceed = await p.confirm({
2097
+ message: "Proceed with activation anyway?",
2098
+ initialValue: false
2099
+ });
2100
+ if (p.isCancel(proceed) || !proceed) {
2101
+ p.cancel("Activation cancelled. Fix the issues above and try again.");
2102
+ process.exit(0);
2103
+ }
2104
+ }
2105
+ } else if (!skipConfirm) {
2106
+ const confirm = await p.confirm({
2107
+ message: "Activate account?",
2108
+ initialValue: true
2109
+ });
2110
+ if (p.isCancel(confirm) || !confirm) {
2111
+ p.cancel("Activation cancelled.");
2112
+ process.exit(0);
2113
+ }
1766
2114
  }
1767
2115
  spinner.start("Activating account...");
1768
2116
  try {
@@ -1774,17 +2122,54 @@ const activateCommand = defineCommand({
1774
2122
  p.outro("Activation failed.");
1775
2123
  process.exit(1);
1776
2124
  }
2125
+ await promptCreateProfile(client, did, handle);
2126
+ spinner.start("Verifying activation...");
2127
+ if (!(await client.getAccountStatus()).active) {
2128
+ spinner.stop("Verification failed");
2129
+ p.log.error("Account was activated but is not showing as active.");
2130
+ p.log.info("Try running 'pds status' to check the current state.");
2131
+ p.outro("Activation may have failed.");
2132
+ process.exit(1);
2133
+ }
2134
+ spinner.stop("Account is active");
1777
2135
  const pdsHostname = config.PDS_HOSTNAME;
1778
2136
  if (pdsHostname && !isDev) {
1779
- spinner.start("Notifying relay...");
1780
- if (await client.requestCrawl(pdsHostname)) spinner.stop("Relay notified");
2137
+ spinner.start("Requesting crawl from relay...");
2138
+ if (await client.requestCrawl(pdsHostname)) spinner.stop("Crawl requested");
1781
2139
  else {
1782
- spinner.stop("Could not notify relay");
1783
- p.log.warn("Run 'pds activate' again later to retry notifying the relay.");
2140
+ spinner.stop("Could not request crawl");
2141
+ p.log.warn("Run 'pds activate' again later to retry requesting a crawl.");
1784
2142
  }
1785
2143
  }
1786
2144
  p.log.success("Welcome to the Atmosphere! 🦋");
1787
2145
  p.log.info("Your account is now live and accepting writes.");
2146
+ if (!hasFailures) {
2147
+ p.log.info("");
2148
+ let shouldEmit = skipConfirm;
2149
+ if (!skipConfirm) {
2150
+ const emitConfirm = await p.confirm({
2151
+ message: "Emit identity event to notify relays?",
2152
+ initialValue: true
2153
+ });
2154
+ shouldEmit = !p.isCancel(emitConfirm) && emitConfirm;
2155
+ }
2156
+ if (shouldEmit) {
2157
+ spinner.start("Emitting identity event...");
2158
+ try {
2159
+ const result = await client.emitIdentity();
2160
+ spinner.stop(`Identity event emitted (seq: ${result.seq})`);
2161
+ } catch (err) {
2162
+ spinner.stop("Failed to emit identity event");
2163
+ p.log.warn(err instanceof Error ? err.message : "Could not emit identity");
2164
+ p.log.info("You can try again later with: pds emit-identity");
2165
+ }
2166
+ } else p.log.info("To notify relays later, run: pds emit-identity");
2167
+ } else {
2168
+ p.log.info("");
2169
+ p.log.info("Some checks failed, so identity was not emitted automatically.");
2170
+ p.log.info("Once your handle and DID are configured correctly, run:");
2171
+ p.log.info(" pds emit-identity");
2172
+ }
1788
2173
  p.outro("All set!");
1789
2174
  }
1790
2175
  });
@@ -1796,19 +2181,47 @@ const activateCommand = defineCommand({
1796
2181
  */
1797
2182
  const brightNote = (lines) => lines.map((l) => `\x1b[0m${l}`).join("\n");
1798
2183
  const bold = (text) => pc.bold(text);
2184
+ /**
2185
+ * Run identity verification checks - for deactivate, we just inform the user
2186
+ */
2187
+ async function runIdentityChecks(handle, did, pdsUrl) {
2188
+ const checks = [];
2189
+ p.log.step("Checking handle resolution...");
2190
+ const handleResult = await checkHandleResolution(handle, did);
2191
+ checks.push({
2192
+ name: "Handle resolution",
2193
+ ...handleResult
2194
+ });
2195
+ p.log.step("Checking DID document...");
2196
+ const didResult = await checkDidDocument(did, pdsUrl);
2197
+ checks.push({
2198
+ name: "DID document",
2199
+ ...didResult
2200
+ });
2201
+ return checks;
2202
+ }
1799
2203
  const deactivateCommand = defineCommand({
1800
2204
  meta: {
1801
2205
  name: "deactivate",
1802
2206
  description: "Deactivate your account to enable re-import"
1803
2207
  },
1804
- args: { dev: {
1805
- type: "boolean",
1806
- description: "Target local development server instead of production",
1807
- default: false
1808
- } },
2208
+ args: {
2209
+ dev: {
2210
+ type: "boolean",
2211
+ description: "Target local development server instead of production",
2212
+ default: false
2213
+ },
2214
+ yes: {
2215
+ type: "boolean",
2216
+ alias: "y",
2217
+ description: "Skip confirmation prompts",
2218
+ default: false
2219
+ }
2220
+ },
1809
2221
  async run({ args }) {
1810
2222
  const pm = detectPackageManager();
1811
2223
  const isDev = args.dev;
2224
+ const skipConfirm = args.yes;
1812
2225
  p.intro("🦋 Deactivate Account");
1813
2226
  const vars = getVars();
1814
2227
  let targetUrl;
@@ -1827,11 +2240,17 @@ const deactivateCommand = defineCommand({
1827
2240
  };
1828
2241
  const authToken = config.AUTH_TOKEN;
1829
2242
  const handle = config.HANDLE;
2243
+ const did = config.DID;
1830
2244
  if (!authToken) {
1831
2245
  p.log.error("No AUTH_TOKEN found. Run 'pds init' first.");
1832
2246
  p.outro("Deactivation cancelled.");
1833
2247
  process.exit(1);
1834
2248
  }
2249
+ if (!handle || !did) {
2250
+ p.log.error("No HANDLE or DID found. Run 'pds init' first.");
2251
+ p.outro("Deactivation cancelled.");
2252
+ process.exit(1);
2253
+ }
1835
2254
  const client = new PDSClient(targetUrl, authToken);
1836
2255
  const spinner = p.spinner();
1837
2256
  spinner.start(`Checking PDS at ${targetDomain}...`);
@@ -1852,8 +2271,17 @@ const deactivateCommand = defineCommand({
1852
2271
  p.outro("Already deactivated.");
1853
2272
  return;
1854
2273
  }
2274
+ p.log.info("");
2275
+ p.log.info(pc.bold("Current identity status:"));
2276
+ const checks = await runIdentityChecks(handle, did, targetUrl);
2277
+ for (const check of checks) if (check.ok) p.log.success(`${check.name}: ${check.message}`);
2278
+ else {
2279
+ p.log.warn(`${check.name}: ${check.message}`);
2280
+ if (check.detail) p.log.info(` ${check.detail}`);
2281
+ }
2282
+ p.log.info("");
1855
2283
  p.note(brightNote([
1856
- bold(`⚠️ WARNING: This will disable writes for @${handle || "your-handle"}`),
2284
+ bold(`⚠️ WARNING: This will disable writes for @${handle}`),
1857
2285
  "",
1858
2286
  "Your account will:",
1859
2287
  " • Stop accepting new posts, follows, and other writes",
@@ -1862,13 +2290,15 @@ const deactivateCommand = defineCommand({
1862
2290
  "",
1863
2291
  bold("Only deactivate if you need to re-import your data.")
1864
2292
  ]), "Deactivate Account");
1865
- const confirm = await p.confirm({
1866
- message: "Are you sure you want to deactivate?",
1867
- initialValue: false
1868
- });
1869
- if (p.isCancel(confirm) || !confirm) {
1870
- p.cancel("Deactivation cancelled.");
1871
- process.exit(0);
2293
+ if (!skipConfirm) {
2294
+ const confirm = await p.confirm({
2295
+ message: "Are you sure you want to deactivate?",
2296
+ initialValue: false
2297
+ });
2298
+ if (p.isCancel(confirm) || !confirm) {
2299
+ p.cancel("Deactivation cancelled.");
2300
+ process.exit(0);
2301
+ }
1872
2302
  }
1873
2303
  spinner.start("Deactivating account...");
1874
2304
  try {
@@ -1965,14 +2395,15 @@ const statusCommand = defineCommand({
1965
2395
  }
1966
2396
  console.log();
1967
2397
  console.log(pc.bold("Repository"));
1968
- if (status.repoCommit && status.indexedRecords > 0) {
2398
+ const repoCheck = checkRepoInitialised(status);
2399
+ if (repoCheck.ok) {
1969
2400
  const shortCid = status.repoCommit.slice(0, 12) + "..." + status.repoCommit.slice(-4);
1970
2401
  const shortRev = status.repoRev ? status.repoRev.slice(0, 8) + "..." : "none";
1971
2402
  console.log(` ${CHECK} Initialized: ${pc.dim(shortCid)} (rev: ${shortRev})`);
1972
- console.log(` ${INFO} ${status.repoBlocks.toLocaleString()} blocks, ${status.indexedRecords.toLocaleString()} records`);
2403
+ console.log(` ${INFO} ${repoCheck.message}`);
1973
2404
  } else {
1974
- console.log(` ${WARN} Repository empty (no records)`);
1975
- console.log(pc.dim(" Run 'pds migrate' to import from another PDS"));
2405
+ console.log(` ${WARN} ${repoCheck.message}`);
2406
+ if (repoCheck.detail) console.log(pc.dim(` ${repoCheck.detail}`));
1976
2407
  hasWarnings = true;
1977
2408
  }
1978
2409
  console.log();
@@ -1983,18 +2414,14 @@ const statusCommand = defineCommand({
1983
2414
  }
1984
2415
  if (handle) console.log(` ${INFO} Handle: ${pc.cyan(`@${handle}`)}`);
1985
2416
  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
- }
2417
+ const didCheck = await checkDidResolution(client, did, pdsHostname);
2418
+ if (didCheck.ok) console.log(` ${CHECK} DID resolves to this PDS (via ${didCheck.resolveMethod})`);
2419
+ else if (didCheck.pdsEndpoint) {
2420
+ console.log(` ${CROSS} DID resolves to different PDS`);
2421
+ console.log(pc.dim(` Resolved via: ${didCheck.resolveMethod}`));
2422
+ console.log(pc.dim(` Expected: https://${pdsHostname}`));
2423
+ console.log(pc.dim(` Got: ${didCheck.pdsEndpoint}`));
2424
+ hasErrors = true;
1998
2425
  } else {
1999
2426
  console.log(` ${WARN} Could not resolve DID`);
2000
2427
  if (did.startsWith("did:plc:")) console.log(pc.dim(" Check plc.directory or update DID document"));
@@ -2006,19 +2433,13 @@ const statusCommand = defineCommand({
2006
2433
  hasWarnings = true;
2007
2434
  }
2008
2435
  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) {
2436
+ const handleCheck = await checkHandleResolutionDetailed(client, handle, did);
2437
+ if (handleCheck.ok) console.log(` ${CHECK} Handle verified via ${handleCheck.methods.join(" + ")}`);
2438
+ else if (handleCheck.httpDid || handleCheck.dnsDid) {
2018
2439
  console.log(` ${CROSS} Handle resolves to different DID`);
2019
2440
  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}`));
2441
+ if (handleCheck.httpDid) console.log(pc.dim(` HTTP well-known: ${handleCheck.httpDid}`));
2442
+ if (handleCheck.dnsDid) console.log(pc.dim(` DNS TXT: ${handleCheck.dnsDid}`));
2022
2443
  hasErrors = true;
2023
2444
  } else {
2024
2445
  console.log(` ${WARN} Handle not resolving`);
@@ -2030,7 +2451,8 @@ const statusCommand = defineCommand({
2030
2451
  console.log();
2031
2452
  if (status.expectedBlobs > 0) {
2032
2453
  console.log(pc.bold("Blobs"));
2033
- if (status.importedBlobs === status.expectedBlobs) console.log(` ${CHECK} ${status.importedBlobs}/${status.expectedBlobs} blobs imported`);
2454
+ const blobCheck = checkBlobsImported(status);
2455
+ if (blobCheck.ok) console.log(` ${CHECK} ${blobCheck.message}`);
2034
2456
  else {
2035
2457
  const missing = status.expectedBlobs - status.importedBlobs;
2036
2458
  console.log(` ${WARN} ${status.importedBlobs}/${status.expectedBlobs} blobs imported (${missing} missing)`);
@@ -2039,11 +2461,33 @@ const statusCommand = defineCommand({
2039
2461
  console.log();
2040
2462
  }
2041
2463
  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;
2464
+ if (did) {
2465
+ const appViewCheck = await checkAppViewIndexing(client, did);
2466
+ if (appViewCheck.ok) console.log(` ${CHECK} ${appViewCheck.message}`);
2467
+ else {
2468
+ console.log(` ${WARN} ${appViewCheck.message}`);
2469
+ if (appViewCheck.detail) console.log(pc.dim(` ${appViewCheck.detail}`));
2470
+ hasWarnings = true;
2471
+ }
2472
+ }
2473
+ if (pdsHostname) {
2474
+ const relayStatuses = await client.getAllRelayHostStatus(pdsHostname);
2475
+ const hasActiveRelay = relayStatuses.some((r) => r.status === "active");
2476
+ const hasBannedRelay = relayStatuses.some((r) => r.status === "banned");
2477
+ const needsCrawl = relayStatuses.length === 0 || relayStatuses.every((r) => r.status === "idle" || r.status === "offline");
2478
+ if (relayStatuses.length === 0) console.log(` ${WARN} No relays have crawled this PDS yet`);
2479
+ else for (const relayStatus of relayStatuses) {
2480
+ const relayName = relayStatus.relay.includes("us-west") ? "us-west" : "us-east";
2481
+ const statusIcon = relayStatus.status === "active" ? CHECK : relayStatus.status === "banned" ? CROSS : WARN;
2482
+ console.log(` ${statusIcon} Relay ${relayName}: ${relayStatus.status}${relayStatus.accountCount !== void 0 ? ` (${relayStatus.accountCount} accounts, seq: ${relayStatus.seq ?? "none"})` : ""}`);
2483
+ }
2484
+ if (hasBannedRelay && !hasActiveRelay) {
2485
+ console.log(` ${CROSS} PDS is banned from all relays`);
2486
+ hasErrors = true;
2487
+ } else if (needsCrawl) {
2488
+ console.log(pc.dim(" Run 'pds activate' or 'pds emit-identity' to request a crawl"));
2489
+ hasWarnings = true;
2490
+ }
2047
2491
  }
2048
2492
  try {
2049
2493
  const firehose = await client.getFirehoseStatus();
@@ -2068,6 +2512,80 @@ const statusCommand = defineCommand({
2068
2512
  }
2069
2513
  });
2070
2514
 
2515
+ //#endregion
2516
+ //#region src/cli/commands/emit-identity.ts
2517
+ /**
2518
+ * Emit identity command - notifies relays to refresh handle verification
2519
+ */
2520
+ const emitIdentityCommand = defineCommand({
2521
+ meta: {
2522
+ name: "emit-identity",
2523
+ description: "Emit an identity event to notify relays to refresh handle verification"
2524
+ },
2525
+ args: { dev: {
2526
+ type: "boolean",
2527
+ description: "Target local development server instead of production",
2528
+ default: false
2529
+ } },
2530
+ async run({ args }) {
2531
+ const isDev = args.dev;
2532
+ p.intro("🦋 Emit Identity Event");
2533
+ const vars = getVars();
2534
+ let targetUrl;
2535
+ try {
2536
+ targetUrl = getTargetUrl(isDev, vars.PDS_HOSTNAME);
2537
+ } catch (err) {
2538
+ p.log.error(err instanceof Error ? err.message : "Configuration error");
2539
+ p.log.info("Run 'pds init' first to configure your PDS.");
2540
+ process.exit(1);
2541
+ }
2542
+ const targetDomain = getDomain(targetUrl);
2543
+ const wranglerVars = getVars();
2544
+ const config = {
2545
+ ...readDevVars(),
2546
+ ...wranglerVars
2547
+ };
2548
+ const authToken = config.AUTH_TOKEN;
2549
+ if (!authToken) {
2550
+ p.log.error("No AUTH_TOKEN found. Run 'pds init' first.");
2551
+ p.outro("Cancelled.");
2552
+ process.exit(1);
2553
+ }
2554
+ const client = new PDSClient(targetUrl, authToken);
2555
+ const spinner = p.spinner();
2556
+ spinner.start(`Checking PDS at ${targetDomain}...`);
2557
+ if (!await client.healthCheck()) {
2558
+ spinner.stop(`PDS not responding at ${targetDomain}`);
2559
+ p.log.error(`Your PDS isn't responding at ${targetUrl}`);
2560
+ if (!isDev) p.log.info("Make sure your worker is deployed: wrangler deploy");
2561
+ p.outro("Cancelled.");
2562
+ process.exit(1);
2563
+ }
2564
+ spinner.stop(`Connected to ${targetDomain}`);
2565
+ spinner.start("Emitting identity event...");
2566
+ try {
2567
+ const result = await client.emitIdentity();
2568
+ spinner.stop(`Identity event emitted (seq: ${result.seq})`);
2569
+ } catch (err) {
2570
+ spinner.stop("Failed to emit identity event");
2571
+ p.log.error(err instanceof Error ? err.message : "Could not emit identity event");
2572
+ p.outro("Failed.");
2573
+ process.exit(1);
2574
+ }
2575
+ const pdsHostname = config.PDS_HOSTNAME;
2576
+ if (pdsHostname && !isDev) {
2577
+ spinner.start("Requesting crawl from relay...");
2578
+ if (await client.requestCrawl(pdsHostname)) spinner.stop("Crawl requested");
2579
+ else {
2580
+ spinner.stop("Could not request crawl");
2581
+ p.log.warn("The relay may not pick up the identity change immediately.");
2582
+ }
2583
+ }
2584
+ p.log.success("Relays have been notified to refresh your handle verification.");
2585
+ p.outro("Done!");
2586
+ }
2587
+ });
2588
+
2071
2589
  //#endregion
2072
2590
  //#region src/cli/index.ts
2073
2591
  /**
@@ -2085,7 +2603,8 @@ runMain(defineCommand({
2085
2603
  migrate: migrateCommand,
2086
2604
  activate: activateCommand,
2087
2605
  deactivate: deactivateCommand,
2088
- status: statusCommand
2606
+ status: statusCommand,
2607
+ "emit-identity": emitIdentityCommand
2089
2608
  }
2090
2609
  }));
2091
2610