@ascorbic/pds 0.0.2 → 0.2.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/README.md +1 -6
- package/dist/cli.js +1165 -59
- package/dist/index.d.ts +201 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3826 -752
- package/dist/index.js.map +1 -1
- package/package.json +12 -3
package/dist/cli.js
CHANGED
|
@@ -8,6 +8,9 @@ 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";
|
|
13
|
+
import pc from "picocolors";
|
|
11
14
|
|
|
12
15
|
//#region src/cli/utils/wrangler.ts
|
|
13
16
|
/**
|
|
@@ -37,6 +40,21 @@ function getVars() {
|
|
|
37
40
|
return rawConfig.vars || {};
|
|
38
41
|
}
|
|
39
42
|
/**
|
|
43
|
+
* Get current worker name from wrangler config
|
|
44
|
+
*/
|
|
45
|
+
function getWorkerName() {
|
|
46
|
+
const { rawConfig } = experimental_readRawConfig({});
|
|
47
|
+
return rawConfig.name;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Set worker name in wrangler config
|
|
51
|
+
*/
|
|
52
|
+
function setWorkerName(name) {
|
|
53
|
+
const { configPath } = experimental_readRawConfig({});
|
|
54
|
+
if (!configPath) throw new Error("No wrangler config found");
|
|
55
|
+
experimental_patchConfig(configPath, { name });
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
40
58
|
* Set a secret using wrangler secret put
|
|
41
59
|
*/
|
|
42
60
|
async function setSecret(name, value) {
|
|
@@ -171,24 +189,30 @@ async function hashPassword(password) {
|
|
|
171
189
|
return bcrypt.hash(password, 10);
|
|
172
190
|
}
|
|
173
191
|
/**
|
|
174
|
-
* Prompt for password with confirmation
|
|
192
|
+
* Prompt for password with confirmation (max 3 attempts)
|
|
175
193
|
*/
|
|
176
|
-
async function promptPassword() {
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
p.
|
|
189
|
-
|
|
194
|
+
async function promptPassword(handle) {
|
|
195
|
+
const message = handle ? `Choose a password for @${handle}:` : "Enter password:";
|
|
196
|
+
const MAX_ATTEMPTS = 3;
|
|
197
|
+
let attempts = 0;
|
|
198
|
+
while (attempts < MAX_ATTEMPTS) {
|
|
199
|
+
attempts++;
|
|
200
|
+
const password = await p.password({ message });
|
|
201
|
+
if (p.isCancel(password)) {
|
|
202
|
+
p.cancel("Cancelled");
|
|
203
|
+
process.exit(0);
|
|
204
|
+
}
|
|
205
|
+
const confirm = await p.password({ message: "Confirm password:" });
|
|
206
|
+
if (p.isCancel(confirm)) {
|
|
207
|
+
p.cancel("Cancelled");
|
|
208
|
+
process.exit(0);
|
|
209
|
+
}
|
|
210
|
+
if (password === confirm) return password;
|
|
211
|
+
p.log.error("Passwords do not match. Try again.");
|
|
190
212
|
}
|
|
191
|
-
|
|
213
|
+
p.log.error("Too many failed attempts.");
|
|
214
|
+
p.cancel("Password setup cancelled");
|
|
215
|
+
process.exit(1);
|
|
192
216
|
}
|
|
193
217
|
/**
|
|
194
218
|
* Set a secret value, either locally (.dev.vars) or via wrangler
|
|
@@ -315,12 +339,124 @@ const secretCommand = defineCommand({
|
|
|
315
339
|
}
|
|
316
340
|
});
|
|
317
341
|
|
|
342
|
+
//#endregion
|
|
343
|
+
//#region src/cli/utils/handle-resolver.ts
|
|
344
|
+
/**
|
|
345
|
+
* Utilities for resolving AT Protocol handles to DIDs
|
|
346
|
+
*/
|
|
347
|
+
const resolver = new AtprotoDohHandleResolver({ dohEndpoint: "https://cloudflare-dns.com/dns-query" });
|
|
348
|
+
/**
|
|
349
|
+
* Resolve a handle to a DID using the AT Protocol handle resolution methods.
|
|
350
|
+
* Uses DNS-over-HTTPS via Cloudflare for DNS resolution.
|
|
351
|
+
*/
|
|
352
|
+
async function resolveHandleToDid(handle) {
|
|
353
|
+
try {
|
|
354
|
+
return await resolver.resolve(handle, { signal: AbortSignal.timeout(1e4) });
|
|
355
|
+
} catch (err) {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
//#endregion
|
|
361
|
+
//#region src/did-resolver.ts
|
|
362
|
+
/**
|
|
363
|
+
* DID resolution for Cloudflare Workers
|
|
364
|
+
*
|
|
365
|
+
* We can't use @atproto/identity directly because it uses `redirect: "error"`
|
|
366
|
+
* which Cloudflare Workers doesn't support. This is a simple implementation
|
|
367
|
+
* that's compatible with Workers.
|
|
368
|
+
*/
|
|
369
|
+
const PLC_DIRECTORY = "https://plc.directory";
|
|
370
|
+
const TIMEOUT_MS = 3e3;
|
|
371
|
+
var DidResolver = class {
|
|
372
|
+
plcUrl;
|
|
373
|
+
timeout;
|
|
374
|
+
cache;
|
|
375
|
+
constructor(opts = {}) {
|
|
376
|
+
this.plcUrl = opts.plcUrl ?? PLC_DIRECTORY;
|
|
377
|
+
this.timeout = opts.timeout ?? TIMEOUT_MS;
|
|
378
|
+
this.cache = opts.didCache;
|
|
379
|
+
}
|
|
380
|
+
async resolve(did) {
|
|
381
|
+
if (this.cache) {
|
|
382
|
+
const cached = await this.cache.checkCache(did);
|
|
383
|
+
if (cached && !cached.expired) {
|
|
384
|
+
if (cached.stale) this.cache.refreshCache(did, () => this.resolveNoCache(did), cached);
|
|
385
|
+
return cached.doc;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
const doc = await this.resolveNoCache(did);
|
|
389
|
+
if (doc && this.cache) await this.cache.cacheDid(did, doc);
|
|
390
|
+
else if (!doc && this.cache) await this.cache.clearEntry(did);
|
|
391
|
+
return doc;
|
|
392
|
+
}
|
|
393
|
+
async resolveNoCache(did) {
|
|
394
|
+
if (did.startsWith("did:web:")) return this.resolveDidWeb(did);
|
|
395
|
+
if (did.startsWith("did:plc:")) return this.resolveDidPlc(did);
|
|
396
|
+
throw new Error(`Unsupported DID method: ${did}`);
|
|
397
|
+
}
|
|
398
|
+
async resolveDidWeb(did) {
|
|
399
|
+
const parts = did.split(":").slice(2);
|
|
400
|
+
if (parts.length === 0) throw new Error(`Invalid did:web format: ${did}`);
|
|
401
|
+
if (parts.length > 1) throw new Error(`Unsupported did:web with path: ${did}`);
|
|
402
|
+
const domain = decodeURIComponent(parts[0]);
|
|
403
|
+
const url = new URL(`https://${domain}/.well-known/did.json`);
|
|
404
|
+
if (url.hostname === "localhost") url.protocol = "http:";
|
|
405
|
+
const controller = new AbortController();
|
|
406
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
407
|
+
try {
|
|
408
|
+
const res = await fetch(url.toString(), {
|
|
409
|
+
signal: controller.signal,
|
|
410
|
+
redirect: "manual",
|
|
411
|
+
headers: { accept: "application/did+ld+json,application/json" }
|
|
412
|
+
});
|
|
413
|
+
if (res.status >= 300 && res.status < 400) return null;
|
|
414
|
+
if (!res.ok) return null;
|
|
415
|
+
const doc = await res.json();
|
|
416
|
+
return this.validateDidDoc(did, doc);
|
|
417
|
+
} finally {
|
|
418
|
+
clearTimeout(timeoutId);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
async resolveDidPlc(did) {
|
|
422
|
+
const url = new URL(`/${encodeURIComponent(did)}`, this.plcUrl);
|
|
423
|
+
const controller = new AbortController();
|
|
424
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
425
|
+
try {
|
|
426
|
+
const res = await fetch(url.toString(), {
|
|
427
|
+
signal: controller.signal,
|
|
428
|
+
redirect: "manual",
|
|
429
|
+
headers: { accept: "application/did+ld+json,application/json" }
|
|
430
|
+
});
|
|
431
|
+
if (res.status >= 300 && res.status < 400) return null;
|
|
432
|
+
if (res.status === 404) return null;
|
|
433
|
+
if (!res.ok) throw new Error(`PLC directory error: ${res.status} ${res.statusText}`);
|
|
434
|
+
const doc = await res.json();
|
|
435
|
+
return this.validateDidDoc(did, doc);
|
|
436
|
+
} finally {
|
|
437
|
+
clearTimeout(timeoutId);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
validateDidDoc(did, doc) {
|
|
441
|
+
if (!check.is(doc, didDocument)) return null;
|
|
442
|
+
if (doc.id !== did) return null;
|
|
443
|
+
return doc;
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
|
|
318
447
|
//#endregion
|
|
319
448
|
//#region src/cli/commands/init.ts
|
|
320
449
|
/**
|
|
321
450
|
* Interactive PDS setup wizard
|
|
322
451
|
*/
|
|
323
452
|
/**
|
|
453
|
+
* Slugify a handle to create a worker name
|
|
454
|
+
* e.g., "example.com" -> "example-com-pds"
|
|
455
|
+
*/
|
|
456
|
+
function slugifyHandle(handle) {
|
|
457
|
+
return handle.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") + "-pds";
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
324
460
|
* Run wrangler types to regenerate TypeScript types
|
|
325
461
|
*/
|
|
326
462
|
function runWranglerTypes() {
|
|
@@ -354,48 +490,233 @@ const initCommand = defineCommand({
|
|
|
354
490
|
default: false
|
|
355
491
|
} },
|
|
356
492
|
async run({ args }) {
|
|
357
|
-
p.intro("PDS Setup
|
|
493
|
+
p.intro("🦋 PDS Setup");
|
|
358
494
|
const isProduction = args.production;
|
|
359
495
|
if (isProduction) p.log.info("Production mode: secrets will be deployed via wrangler");
|
|
496
|
+
else p.log.info("Let's set up your new home in the Atmosphere!");
|
|
360
497
|
const wranglerVars = getVars();
|
|
361
498
|
const devVars = readDevVars();
|
|
362
499
|
const currentVars = {
|
|
363
500
|
...devVars,
|
|
364
501
|
...wranglerVars
|
|
365
502
|
};
|
|
366
|
-
const
|
|
367
|
-
message: "
|
|
368
|
-
|
|
369
|
-
initialValue: currentVars.PDS_HOSTNAME || "",
|
|
370
|
-
validate: (v) => !v ? "Hostname is required" : void 0
|
|
503
|
+
const isMigrating = await p.confirm({
|
|
504
|
+
message: "Are you migrating an existing Bluesky account? 🦋",
|
|
505
|
+
initialValue: false
|
|
371
506
|
});
|
|
372
|
-
if (p.isCancel(
|
|
373
|
-
p.cancel("
|
|
507
|
+
if (p.isCancel(isMigrating)) {
|
|
508
|
+
p.cancel("Setup cancelled");
|
|
374
509
|
process.exit(0);
|
|
375
510
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
if (
|
|
383
|
-
p.
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
511
|
+
let did;
|
|
512
|
+
let handle;
|
|
513
|
+
let hostname;
|
|
514
|
+
let workerName;
|
|
515
|
+
let initialActive;
|
|
516
|
+
const currentWorkerName = getWorkerName();
|
|
517
|
+
if (isMigrating) {
|
|
518
|
+
p.log.info("Time to pack your bags! 🧳");
|
|
519
|
+
p.log.info("Your account will be inactive until you've moved your data over.");
|
|
520
|
+
let hostedDomains = [
|
|
521
|
+
".bsky.social",
|
|
522
|
+
".bsky.network",
|
|
523
|
+
".bsky.team"
|
|
524
|
+
];
|
|
525
|
+
const isHostedHandle = (h) => hostedDomains.some((domain) => h.endsWith(domain));
|
|
526
|
+
let resolvedDid = null;
|
|
527
|
+
let existingHandle = null;
|
|
528
|
+
let attempts = 0;
|
|
529
|
+
const MAX_ATTEMPTS = 3;
|
|
530
|
+
while (!resolvedDid && attempts < MAX_ATTEMPTS) {
|
|
531
|
+
attempts++;
|
|
532
|
+
const currentHandle = await p.text({
|
|
533
|
+
message: "Your current Bluesky/ATProto handle:",
|
|
534
|
+
placeholder: "example.bsky.social",
|
|
535
|
+
validate: (v) => !v ? "Handle is required" : void 0
|
|
536
|
+
});
|
|
537
|
+
if (p.isCancel(currentHandle)) {
|
|
538
|
+
p.cancel("Cancelled");
|
|
539
|
+
process.exit(0);
|
|
540
|
+
}
|
|
541
|
+
existingHandle = currentHandle;
|
|
542
|
+
const spinner$1 = p.spinner();
|
|
543
|
+
spinner$1.start("Finding you in the Atmosphere...");
|
|
544
|
+
resolvedDid = await resolveHandleToDid(currentHandle);
|
|
545
|
+
if (!resolvedDid) {
|
|
546
|
+
spinner$1.stop("Not found");
|
|
547
|
+
p.log.error(`Failed to resolve handle "${currentHandle}"`);
|
|
548
|
+
const action = await p.select({
|
|
549
|
+
message: "What would you like to do?",
|
|
550
|
+
options: [{
|
|
551
|
+
value: "retry",
|
|
552
|
+
label: "Try a different handle"
|
|
553
|
+
}, {
|
|
554
|
+
value: "manual",
|
|
555
|
+
label: "Enter DID manually"
|
|
556
|
+
}]
|
|
557
|
+
});
|
|
558
|
+
if (p.isCancel(action)) {
|
|
559
|
+
p.cancel("Cancelled");
|
|
560
|
+
process.exit(0);
|
|
561
|
+
}
|
|
562
|
+
if (action === "manual") {
|
|
563
|
+
const manualDid = await p.text({
|
|
564
|
+
message: "Enter your DID:",
|
|
565
|
+
placeholder: "did:plc:...",
|
|
566
|
+
validate: (v) => {
|
|
567
|
+
if (!v) return "DID is required";
|
|
568
|
+
if (!v.startsWith("did:")) return "DID must start with did:";
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
if (p.isCancel(manualDid)) {
|
|
572
|
+
p.cancel("Cancelled");
|
|
573
|
+
process.exit(0);
|
|
574
|
+
}
|
|
575
|
+
resolvedDid = manualDid;
|
|
576
|
+
}
|
|
577
|
+
} else {
|
|
578
|
+
try {
|
|
579
|
+
const pdsService = (await new DidResolver().resolve(resolvedDid))?.service?.find((s) => s.type === "AtprotoPersonalDataServer" || s.id === "#atproto_pds");
|
|
580
|
+
if (pdsService?.serviceEndpoint) {
|
|
581
|
+
const describeRes = await fetch(`${pdsService.serviceEndpoint}/xrpc/com.atproto.server.describeServer`);
|
|
582
|
+
if (describeRes.ok) {
|
|
583
|
+
const desc = await describeRes.json();
|
|
584
|
+
if (desc.availableUserDomains?.length) hostedDomains = desc.availableUserDomains.map((d) => d.startsWith(".") ? d : `.${d}`);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
} catch {}
|
|
588
|
+
spinner$1.stop(`Found you! ${resolvedDid}`);
|
|
589
|
+
if (isHostedHandle(existingHandle)) {
|
|
590
|
+
const theirDomain = hostedDomains.find((d) => existingHandle.endsWith(d));
|
|
591
|
+
const domainExample = theirDomain ? `*${theirDomain}` : "*.bsky.social";
|
|
592
|
+
p.log.warn(`You'll need a custom domain for your new handle (not ${domainExample}). You can set this up after transferring your data.`);
|
|
593
|
+
}
|
|
594
|
+
if (attempts >= MAX_ATTEMPTS) {
|
|
595
|
+
p.log.error("Unable to resolve handle after 3 attempts.");
|
|
596
|
+
p.log.info("");
|
|
597
|
+
p.log.info("You can:");
|
|
598
|
+
p.log.info(" 1. Double-check your handle spelling");
|
|
599
|
+
p.log.info(" 2. Provide your DID directly if you know it");
|
|
600
|
+
p.log.info(" 3. Run 'pds init' again when ready");
|
|
601
|
+
p.outro("Initialization cancelled.");
|
|
602
|
+
process.exit(1);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
394
605
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
p.
|
|
398
|
-
|
|
606
|
+
did = resolvedDid;
|
|
607
|
+
const defaultHandle = existingHandle && !isHostedHandle(existingHandle) ? existingHandle : currentVars.HANDLE || "";
|
|
608
|
+
handle = await p.text({
|
|
609
|
+
message: "New account handle (must be a domain you control):",
|
|
610
|
+
placeholder: "example.com",
|
|
611
|
+
initialValue: defaultHandle,
|
|
612
|
+
validate: (v) => {
|
|
613
|
+
if (!v) return "Handle is required";
|
|
614
|
+
if (isHostedHandle(v)) return "You need a custom domain - hosted handles like *.bsky.social won't work";
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
if (p.isCancel(handle)) {
|
|
618
|
+
p.cancel("Cancelled");
|
|
619
|
+
process.exit(0);
|
|
620
|
+
}
|
|
621
|
+
hostname = await p.text({
|
|
622
|
+
message: "Domain where you'll deploy your PDS:",
|
|
623
|
+
placeholder: handle,
|
|
624
|
+
initialValue: currentVars.PDS_HOSTNAME || handle,
|
|
625
|
+
validate: (v) => !v ? "Hostname is required" : void 0
|
|
626
|
+
});
|
|
627
|
+
if (p.isCancel(hostname)) {
|
|
628
|
+
p.cancel("Cancelled");
|
|
629
|
+
process.exit(0);
|
|
630
|
+
}
|
|
631
|
+
const defaultWorkerName = currentWorkerName || slugifyHandle(handle);
|
|
632
|
+
workerName = await p.text({
|
|
633
|
+
message: "Cloudflare Worker name:",
|
|
634
|
+
placeholder: defaultWorkerName,
|
|
635
|
+
initialValue: defaultWorkerName,
|
|
636
|
+
validate: (v) => {
|
|
637
|
+
if (!v) return "Worker name is required";
|
|
638
|
+
if (!/^[a-z0-9-]+$/.test(v)) return "Worker name can only contain lowercase letters, numbers, and hyphens";
|
|
639
|
+
}
|
|
640
|
+
});
|
|
641
|
+
if (p.isCancel(workerName)) {
|
|
642
|
+
p.cancel("Cancelled");
|
|
643
|
+
process.exit(0);
|
|
644
|
+
}
|
|
645
|
+
initialActive = "false";
|
|
646
|
+
} else {
|
|
647
|
+
p.log.info("A fresh start in the Atmosphere! ✨");
|
|
648
|
+
hostname = await p.text({
|
|
649
|
+
message: "Domain where you'll deploy your PDS:",
|
|
650
|
+
placeholder: "pds.example.com",
|
|
651
|
+
initialValue: currentVars.PDS_HOSTNAME || "",
|
|
652
|
+
validate: (v) => !v ? "Hostname is required" : void 0
|
|
653
|
+
});
|
|
654
|
+
if (p.isCancel(hostname)) {
|
|
655
|
+
p.cancel("Cancelled");
|
|
656
|
+
process.exit(0);
|
|
657
|
+
}
|
|
658
|
+
handle = await p.text({
|
|
659
|
+
message: "Account handle:",
|
|
660
|
+
placeholder: hostname,
|
|
661
|
+
initialValue: currentVars.HANDLE || hostname,
|
|
662
|
+
validate: (v) => !v ? "Handle is required" : void 0
|
|
663
|
+
});
|
|
664
|
+
if (p.isCancel(handle)) {
|
|
665
|
+
p.cancel("Cancelled");
|
|
666
|
+
process.exit(0);
|
|
667
|
+
}
|
|
668
|
+
const didDefault = "did:web:" + hostname;
|
|
669
|
+
did = await p.text({
|
|
670
|
+
message: "Account DID:",
|
|
671
|
+
placeholder: didDefault,
|
|
672
|
+
initialValue: currentVars.DID || didDefault,
|
|
673
|
+
validate: (v) => {
|
|
674
|
+
if (!v) return "DID is required";
|
|
675
|
+
if (!v.startsWith("did:")) return "DID must start with did:";
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
if (p.isCancel(did)) {
|
|
679
|
+
p.cancel("Cancelled");
|
|
680
|
+
process.exit(0);
|
|
681
|
+
}
|
|
682
|
+
const defaultWorkerName = currentWorkerName || slugifyHandle(handle);
|
|
683
|
+
workerName = await p.text({
|
|
684
|
+
message: "Cloudflare Worker name:",
|
|
685
|
+
placeholder: defaultWorkerName,
|
|
686
|
+
initialValue: defaultWorkerName,
|
|
687
|
+
validate: (v) => {
|
|
688
|
+
if (!v) return "Worker name is required";
|
|
689
|
+
if (!/^[a-z0-9-]+$/.test(v)) return "Worker name can only contain lowercase letters, numbers, and hyphens";
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
if (p.isCancel(workerName)) {
|
|
693
|
+
p.cancel("Cancelled");
|
|
694
|
+
process.exit(0);
|
|
695
|
+
}
|
|
696
|
+
initialActive = "true";
|
|
697
|
+
if (handle === hostname) p.note([
|
|
698
|
+
"Your handle matches your PDS hostname, so your PDS will",
|
|
699
|
+
"automatically handle domain verification for you!",
|
|
700
|
+
"",
|
|
701
|
+
"For did:web, your PDS serves the DID document at:",
|
|
702
|
+
` https://${hostname}/.well-known/did.json`,
|
|
703
|
+
"",
|
|
704
|
+
"For handle verification, it serves:",
|
|
705
|
+
` https://${hostname}/.well-known/atproto-did`,
|
|
706
|
+
"",
|
|
707
|
+
"No additional DNS or hosting setup needed. Easy! 🎉"
|
|
708
|
+
].join("\n"), "Identity Setup 🪪");
|
|
709
|
+
else p.note([
|
|
710
|
+
"For did:web, your PDS will serve the DID document at:",
|
|
711
|
+
` https://${hostname}/.well-known/did.json`,
|
|
712
|
+
"",
|
|
713
|
+
"To verify your handle, create a DNS TXT record:",
|
|
714
|
+
` _atproto.${handle} TXT "did=${did}"`,
|
|
715
|
+
"",
|
|
716
|
+
"Or serve a file at:",
|
|
717
|
+
` https://${handle}/.well-known/atproto-did`,
|
|
718
|
+
` containing: ${did}`
|
|
719
|
+
].join("\n"), "Identity Setup 🪪");
|
|
399
720
|
}
|
|
400
721
|
const spinner = p.spinner();
|
|
401
722
|
let authToken;
|
|
@@ -424,14 +745,14 @@ const initCommand = defineCommand({
|
|
|
424
745
|
return secret;
|
|
425
746
|
});
|
|
426
747
|
passwordHash = await getOrGenerateSecret("PASSWORD_HASH", devVars, async () => {
|
|
427
|
-
const password = await promptPassword();
|
|
748
|
+
const password = await promptPassword(handle);
|
|
428
749
|
spinner.start("Hashing password...");
|
|
429
750
|
const hash = await hashPassword(password);
|
|
430
751
|
spinner.stop("Password hashed");
|
|
431
752
|
return hash;
|
|
432
753
|
});
|
|
433
754
|
} else {
|
|
434
|
-
const password = await promptPassword();
|
|
755
|
+
const password = await promptPassword(handle);
|
|
435
756
|
spinner.start("Hashing password...");
|
|
436
757
|
passwordHash = await hashPassword(password);
|
|
437
758
|
spinner.stop("Password hashed");
|
|
@@ -448,11 +769,13 @@ const initCommand = defineCommand({
|
|
|
448
769
|
spinner.stop("Signing keypair generated");
|
|
449
770
|
}
|
|
450
771
|
spinner.start("Updating wrangler.jsonc...");
|
|
772
|
+
setWorkerName(workerName);
|
|
451
773
|
setVars({
|
|
452
774
|
PDS_HOSTNAME: hostname,
|
|
453
775
|
DID: did,
|
|
454
776
|
HANDLE: handle,
|
|
455
|
-
SIGNING_KEY_PUBLIC: signingKeyPublic
|
|
777
|
+
SIGNING_KEY_PUBLIC: signingKeyPublic,
|
|
778
|
+
INITIAL_ACTIVE: initialActive
|
|
456
779
|
});
|
|
457
780
|
spinner.stop("wrangler.jsonc updated");
|
|
458
781
|
const local = !isProduction;
|
|
@@ -471,20 +794,49 @@ const initCommand = defineCommand({
|
|
|
471
794
|
spinner.stop("Failed to generate types (wrangler types)");
|
|
472
795
|
}
|
|
473
796
|
p.note([
|
|
474
|
-
"
|
|
475
|
-
"",
|
|
476
|
-
" PDS_HOSTNAME: " + hostname,
|
|
797
|
+
" Worker name: " + workerName,
|
|
798
|
+
" PDS hostname: " + hostname,
|
|
477
799
|
" DID: " + did,
|
|
478
|
-
"
|
|
479
|
-
"
|
|
800
|
+
" Handle: " + handle,
|
|
801
|
+
" Public signing key: " + signingKeyPublic.slice(0, 20) + "...",
|
|
480
802
|
"",
|
|
481
|
-
isProduction ? "Secrets deployed to Cloudflare" : "Secrets saved to .dev.vars",
|
|
803
|
+
isProduction ? "Secrets deployed to Cloudflare ☁️" : "Secrets saved to .dev.vars",
|
|
482
804
|
"",
|
|
483
805
|
"Auth token (save this!):",
|
|
484
806
|
" " + authToken
|
|
485
|
-
].join("\n"), "
|
|
486
|
-
|
|
487
|
-
|
|
807
|
+
].join("\n"), "Your New Home 🏠");
|
|
808
|
+
let deployedSecrets = isProduction;
|
|
809
|
+
if (!isProduction) {
|
|
810
|
+
const deployNow = await p.confirm({
|
|
811
|
+
message: "Push secrets to Cloudflare now?",
|
|
812
|
+
initialValue: false
|
|
813
|
+
});
|
|
814
|
+
if (!p.isCancel(deployNow) && deployNow) {
|
|
815
|
+
spinner.start("Deploying secrets to Cloudflare...");
|
|
816
|
+
await setSecretValue("AUTH_TOKEN", authToken, false);
|
|
817
|
+
await setSecretValue("SIGNING_KEY", signingKey, false);
|
|
818
|
+
await setSecretValue("JWT_SECRET", jwtSecret, false);
|
|
819
|
+
await setSecretValue("PASSWORD_HASH", passwordHash, false);
|
|
820
|
+
spinner.stop("Secrets deployed to Cloudflare");
|
|
821
|
+
deployedSecrets = true;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
if (isMigrating) p.note([
|
|
825
|
+
deployedSecrets ? "Deploy your worker and run the migration:" : "Push secrets, deploy, and run the migration:",
|
|
826
|
+
"",
|
|
827
|
+
...deployedSecrets ? [] : [" pnpm pds init --production", ""],
|
|
828
|
+
" wrangler deploy",
|
|
829
|
+
" pnpm pds migrate",
|
|
830
|
+
"",
|
|
831
|
+
"To test locally first:",
|
|
832
|
+
" pnpm dev # in one terminal",
|
|
833
|
+
" pnpm pds migrate --dev # in another",
|
|
834
|
+
"",
|
|
835
|
+
"Then update your identity and flip the switch! 🦋",
|
|
836
|
+
" https://atproto.com/guides/account-migration"
|
|
837
|
+
].join("\n"), "Next Steps 🧳");
|
|
838
|
+
if (deployedSecrets) p.outro("Run 'wrangler deploy' to launch your PDS! 🚀");
|
|
839
|
+
else p.outro("Run 'pnpm dev' to start your PDS locally! 🦋");
|
|
488
840
|
}
|
|
489
841
|
});
|
|
490
842
|
/**
|
|
@@ -505,6 +857,757 @@ async function getOrGenerateSecret(name, devVars, generate) {
|
|
|
505
857
|
return generate();
|
|
506
858
|
}
|
|
507
859
|
|
|
860
|
+
//#endregion
|
|
861
|
+
//#region src/cli/utils/pds-client.ts
|
|
862
|
+
var PDSClientError = class extends Error {
|
|
863
|
+
constructor(status, error, message) {
|
|
864
|
+
super(message);
|
|
865
|
+
this.status = status;
|
|
866
|
+
this.error = error;
|
|
867
|
+
this.name = "PDSClientError";
|
|
868
|
+
}
|
|
869
|
+
};
|
|
870
|
+
var PDSClient = class {
|
|
871
|
+
authToken;
|
|
872
|
+
constructor(baseUrl, authToken) {
|
|
873
|
+
this.baseUrl = baseUrl;
|
|
874
|
+
this.authToken = authToken;
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* Set the auth token for subsequent requests
|
|
878
|
+
*/
|
|
879
|
+
setAuthToken(token) {
|
|
880
|
+
this.authToken = token;
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Make an XRPC request
|
|
884
|
+
*/
|
|
885
|
+
async xrpc(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
|
+
else if (options.body && !(options.body instanceof Uint8Array)) headers["Content-Type"] = "application/json";
|
|
892
|
+
const res = await fetch(url.toString(), {
|
|
893
|
+
method,
|
|
894
|
+
headers,
|
|
895
|
+
body: options.body ? options.body instanceof Uint8Array ? options.body : JSON.stringify(options.body) : void 0
|
|
896
|
+
});
|
|
897
|
+
if (!res.ok) {
|
|
898
|
+
const errorBody = await res.json().catch(() => ({}));
|
|
899
|
+
throw new PDSClientError(res.status, errorBody.error ?? "Unknown", errorBody.message ?? `Request failed: ${res.status}`);
|
|
900
|
+
}
|
|
901
|
+
if ((res.headers.get("content-type") ?? "").includes("application/json")) return res.json();
|
|
902
|
+
return {};
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Make a raw request that returns bytes
|
|
906
|
+
*/
|
|
907
|
+
async xrpcBytes(method, endpoint, options = {}) {
|
|
908
|
+
const url = new URL(`/xrpc/${endpoint}`, this.baseUrl);
|
|
909
|
+
if (options.params) for (const [key, value] of Object.entries(options.params)) url.searchParams.set(key, value);
|
|
910
|
+
const headers = {};
|
|
911
|
+
if (options.auth && this.authToken) headers["Authorization"] = `Bearer ${this.authToken}`;
|
|
912
|
+
if (options.contentType) headers["Content-Type"] = options.contentType;
|
|
913
|
+
const res = await fetch(url.toString(), {
|
|
914
|
+
method,
|
|
915
|
+
headers,
|
|
916
|
+
body: options.body
|
|
917
|
+
});
|
|
918
|
+
if (!res.ok) {
|
|
919
|
+
const errorBody = await res.json().catch(() => ({}));
|
|
920
|
+
throw new PDSClientError(res.status, errorBody.error ?? "Unknown", errorBody.message ?? `Request failed: ${res.status}`);
|
|
921
|
+
}
|
|
922
|
+
return {
|
|
923
|
+
bytes: new Uint8Array(await res.arrayBuffer()),
|
|
924
|
+
mimeType: res.headers.get("content-type") ?? "application/octet-stream"
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Create a session with identifier and password
|
|
929
|
+
*/
|
|
930
|
+
async createSession(identifier, password) {
|
|
931
|
+
return this.xrpc("POST", "com.atproto.server.createSession", { body: {
|
|
932
|
+
identifier,
|
|
933
|
+
password
|
|
934
|
+
} });
|
|
935
|
+
}
|
|
936
|
+
/**
|
|
937
|
+
* Get repository description including collections
|
|
938
|
+
*/
|
|
939
|
+
async describeRepo(did) {
|
|
940
|
+
return this.xrpc("GET", "com.atproto.repo.describeRepo", { params: { repo: did } });
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* Get profile stats from AppView (posts, follows, followers counts)
|
|
944
|
+
*/
|
|
945
|
+
async getProfileStats(did) {
|
|
946
|
+
try {
|
|
947
|
+
const res = await fetch(`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(did)}`);
|
|
948
|
+
if (!res.ok) return null;
|
|
949
|
+
const profile = await res.json();
|
|
950
|
+
return {
|
|
951
|
+
postsCount: profile.postsCount ?? 0,
|
|
952
|
+
followsCount: profile.followsCount ?? 0,
|
|
953
|
+
followersCount: profile.followersCount ?? 0
|
|
954
|
+
};
|
|
955
|
+
} catch {
|
|
956
|
+
return null;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* Export repository as CAR file
|
|
961
|
+
*/
|
|
962
|
+
async getRepo(did) {
|
|
963
|
+
const { bytes } = await this.xrpcBytes("GET", "com.atproto.sync.getRepo", { params: { did } });
|
|
964
|
+
return bytes;
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Get a blob by CID
|
|
968
|
+
*/
|
|
969
|
+
async getBlob(did, cid) {
|
|
970
|
+
return this.xrpcBytes("GET", "com.atproto.sync.getBlob", { params: {
|
|
971
|
+
did,
|
|
972
|
+
cid
|
|
973
|
+
} });
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* List blobs in repository
|
|
977
|
+
*/
|
|
978
|
+
async listBlobs(did, cursor) {
|
|
979
|
+
const params = { did };
|
|
980
|
+
if (cursor) params.cursor = cursor;
|
|
981
|
+
return this.xrpc("GET", "com.atproto.sync.listBlobs", { params });
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Get account status including migration progress
|
|
985
|
+
*/
|
|
986
|
+
async getAccountStatus() {
|
|
987
|
+
return this.xrpc("GET", "com.atproto.server.getAccountStatus", { auth: true });
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* Import repository from CAR file
|
|
991
|
+
*/
|
|
992
|
+
async importRepo(carBytes) {
|
|
993
|
+
return this.xrpc("POST", "com.atproto.repo.importRepo", {
|
|
994
|
+
body: carBytes,
|
|
995
|
+
contentType: "application/vnd.ipld.car",
|
|
996
|
+
auth: true
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* List blobs that are missing (referenced but not imported)
|
|
1001
|
+
*/
|
|
1002
|
+
async listMissingBlobs(limit, cursor) {
|
|
1003
|
+
const params = {};
|
|
1004
|
+
if (limit) params.limit = String(limit);
|
|
1005
|
+
if (cursor) params.cursor = cursor;
|
|
1006
|
+
return this.xrpc("GET", "com.atproto.repo.listMissingBlobs", {
|
|
1007
|
+
params,
|
|
1008
|
+
auth: true
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Upload a blob
|
|
1013
|
+
*/
|
|
1014
|
+
async uploadBlob(bytes, mimeType) {
|
|
1015
|
+
return (await this.xrpc("POST", "com.atproto.repo.uploadBlob", {
|
|
1016
|
+
body: bytes,
|
|
1017
|
+
contentType: mimeType,
|
|
1018
|
+
auth: true
|
|
1019
|
+
})).blob;
|
|
1020
|
+
}
|
|
1021
|
+
/**
|
|
1022
|
+
* Reset migration state (only works on deactivated accounts)
|
|
1023
|
+
*/
|
|
1024
|
+
async resetMigration() {
|
|
1025
|
+
return this.xrpc("POST", "gg.mk.experimental.resetMigration", { auth: true });
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* Activate account to enable writes
|
|
1029
|
+
*/
|
|
1030
|
+
async activateAccount() {
|
|
1031
|
+
await this.xrpc("POST", "com.atproto.server.activateAccount", { auth: true });
|
|
1032
|
+
}
|
|
1033
|
+
/**
|
|
1034
|
+
* Deactivate account to disable writes
|
|
1035
|
+
*/
|
|
1036
|
+
async deactivateAccount() {
|
|
1037
|
+
await this.xrpc("POST", "com.atproto.server.deactivateAccount", { auth: true });
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Check if the PDS is reachable
|
|
1041
|
+
*/
|
|
1042
|
+
async healthCheck() {
|
|
1043
|
+
try {
|
|
1044
|
+
return (await fetch(new URL("/xrpc/_health", this.baseUrl).toString())).ok;
|
|
1045
|
+
} catch {
|
|
1046
|
+
return false;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
};
|
|
1050
|
+
|
|
1051
|
+
//#endregion
|
|
1052
|
+
//#region src/cli/utils/cli-helpers.ts
|
|
1053
|
+
/**
|
|
1054
|
+
* Shared CLI utilities for PDS commands
|
|
1055
|
+
*/
|
|
1056
|
+
/**
|
|
1057
|
+
* Get target PDS URL based on mode
|
|
1058
|
+
*/
|
|
1059
|
+
function getTargetUrl(isDev, pdsHostname) {
|
|
1060
|
+
const LOCAL_PDS_URL = "http://localhost:5173";
|
|
1061
|
+
if (isDev) return LOCAL_PDS_URL;
|
|
1062
|
+
if (!pdsHostname) throw new Error("PDS_HOSTNAME not configured in wrangler.jsonc");
|
|
1063
|
+
return `https://${pdsHostname}`;
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Extract domain from URL
|
|
1067
|
+
*/
|
|
1068
|
+
function getDomain(url) {
|
|
1069
|
+
try {
|
|
1070
|
+
return new URL(url).hostname;
|
|
1071
|
+
} catch {
|
|
1072
|
+
return url;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
//#endregion
|
|
1077
|
+
//#region src/cli/commands/migrate.ts
|
|
1078
|
+
function detectPackageManager() {
|
|
1079
|
+
const userAgent = process.env.npm_config_user_agent || "";
|
|
1080
|
+
if (userAgent.startsWith("yarn")) return "yarn";
|
|
1081
|
+
if (userAgent.startsWith("pnpm")) return "pnpm";
|
|
1082
|
+
if (userAgent.startsWith("bun")) return "bun";
|
|
1083
|
+
return "npm";
|
|
1084
|
+
}
|
|
1085
|
+
const brightNote$1 = (lines) => lines.map((l) => `\x1b[0m${l}`).join("\n");
|
|
1086
|
+
const bold$1 = (text) => pc.bold(text);
|
|
1087
|
+
/**
|
|
1088
|
+
* Format number with commas
|
|
1089
|
+
*/
|
|
1090
|
+
function formatNumber(n) {
|
|
1091
|
+
return n.toLocaleString();
|
|
1092
|
+
}
|
|
1093
|
+
/**
|
|
1094
|
+
* Format bytes to human-readable size
|
|
1095
|
+
*/
|
|
1096
|
+
function formatBytes(bytes) {
|
|
1097
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
1098
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
1099
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
1100
|
+
}
|
|
1101
|
+
const migrateCommand = defineCommand({
|
|
1102
|
+
meta: {
|
|
1103
|
+
name: "migrate",
|
|
1104
|
+
description: "Migrate account from source PDS to your new PDS"
|
|
1105
|
+
},
|
|
1106
|
+
args: {
|
|
1107
|
+
clean: {
|
|
1108
|
+
type: "boolean",
|
|
1109
|
+
description: "Reset migration and start fresh",
|
|
1110
|
+
default: false
|
|
1111
|
+
},
|
|
1112
|
+
dev: {
|
|
1113
|
+
type: "boolean",
|
|
1114
|
+
description: "Target local development server instead of production",
|
|
1115
|
+
default: false
|
|
1116
|
+
}
|
|
1117
|
+
},
|
|
1118
|
+
async run({ args }) {
|
|
1119
|
+
const pm = detectPackageManager();
|
|
1120
|
+
const isDev = args.dev;
|
|
1121
|
+
const vars = getVars();
|
|
1122
|
+
let targetUrl;
|
|
1123
|
+
try {
|
|
1124
|
+
targetUrl = getTargetUrl(isDev, vars.PDS_HOSTNAME);
|
|
1125
|
+
} catch (err) {
|
|
1126
|
+
p.log.error(err instanceof Error ? err.message : "Configuration error");
|
|
1127
|
+
p.log.info("Run 'pds init' first to configure your PDS.");
|
|
1128
|
+
process.exit(1);
|
|
1129
|
+
}
|
|
1130
|
+
const targetDomain = getDomain(targetUrl);
|
|
1131
|
+
p.intro("🦋 PDS Migration");
|
|
1132
|
+
const spinner = p.spinner();
|
|
1133
|
+
spinner.start(`Checking PDS at ${targetDomain}...`);
|
|
1134
|
+
const targetClient = new PDSClient(targetUrl);
|
|
1135
|
+
if (!await targetClient.healthCheck()) {
|
|
1136
|
+
spinner.stop(`PDS not responding at ${targetDomain}`);
|
|
1137
|
+
if (isDev) {
|
|
1138
|
+
p.log.error(`Your local PDS isn't running at ${targetUrl}`);
|
|
1139
|
+
p.log.info(`Start it with: ${pm} dev`);
|
|
1140
|
+
} else {
|
|
1141
|
+
p.log.error(`Your PDS isn't responding at ${targetUrl}`);
|
|
1142
|
+
p.log.info("Make sure your worker is deployed: wrangler deploy");
|
|
1143
|
+
p.log.info(`Or test locally first: ${pm} pds migrate --dev`);
|
|
1144
|
+
}
|
|
1145
|
+
p.outro("Migration cancelled.");
|
|
1146
|
+
process.exit(1);
|
|
1147
|
+
}
|
|
1148
|
+
spinner.stop(`Connected to ${targetDomain}`);
|
|
1149
|
+
const wranglerVars = getVars();
|
|
1150
|
+
const config = {
|
|
1151
|
+
...readDevVars(),
|
|
1152
|
+
...wranglerVars
|
|
1153
|
+
};
|
|
1154
|
+
const did = config.DID;
|
|
1155
|
+
const handle = config.HANDLE;
|
|
1156
|
+
const authToken = config.AUTH_TOKEN;
|
|
1157
|
+
if (!did) {
|
|
1158
|
+
p.log.error("No DID configured. Run 'pds init' first.");
|
|
1159
|
+
p.outro("Migration cancelled.");
|
|
1160
|
+
process.exit(1);
|
|
1161
|
+
}
|
|
1162
|
+
if (!authToken) {
|
|
1163
|
+
p.log.error("No AUTH_TOKEN found. Run 'pds init' first.");
|
|
1164
|
+
p.outro("Migration cancelled.");
|
|
1165
|
+
process.exit(1);
|
|
1166
|
+
}
|
|
1167
|
+
targetClient.setAuthToken(authToken);
|
|
1168
|
+
spinner.start(`Looking up @${handle}...`);
|
|
1169
|
+
const didDoc = await new DidResolver().resolve(did);
|
|
1170
|
+
if (!didDoc) {
|
|
1171
|
+
spinner.stop("Failed to resolve DID");
|
|
1172
|
+
p.log.error(`Could not resolve DID: ${did}`);
|
|
1173
|
+
p.outro("Migration cancelled.");
|
|
1174
|
+
process.exit(1);
|
|
1175
|
+
}
|
|
1176
|
+
const sourcePdsUrl = getPdsEndpoint(didDoc);
|
|
1177
|
+
if (!sourcePdsUrl) {
|
|
1178
|
+
spinner.stop("No PDS found in DID document");
|
|
1179
|
+
p.log.error("Could not find PDS endpoint in DID document");
|
|
1180
|
+
p.outro("Migration cancelled.");
|
|
1181
|
+
process.exit(1);
|
|
1182
|
+
}
|
|
1183
|
+
const sourceDomain = getDomain(sourcePdsUrl);
|
|
1184
|
+
spinner.stop(`Found your account at ${sourceDomain}`);
|
|
1185
|
+
spinner.start("Checking account status...");
|
|
1186
|
+
let status;
|
|
1187
|
+
try {
|
|
1188
|
+
status = await targetClient.getAccountStatus();
|
|
1189
|
+
} catch (err) {
|
|
1190
|
+
spinner.stop("Failed to get account status");
|
|
1191
|
+
p.log.error(err instanceof Error ? err.message : "Could not get account status");
|
|
1192
|
+
p.outro("Migration cancelled.");
|
|
1193
|
+
process.exit(1);
|
|
1194
|
+
}
|
|
1195
|
+
spinner.stop("Account status retrieved");
|
|
1196
|
+
if (args.clean) {
|
|
1197
|
+
if (status.active) {
|
|
1198
|
+
p.log.error("Cannot reset: account is active");
|
|
1199
|
+
p.log.info("The --clean flag only works on deactivated accounts.");
|
|
1200
|
+
p.log.info("Your account is already live in the Atmosphere.");
|
|
1201
|
+
p.log.info("");
|
|
1202
|
+
p.log.info("If you need to re-import, first deactivate:");
|
|
1203
|
+
p.log.info(" pnpm pds deactivate");
|
|
1204
|
+
p.outro("Migration cancelled.");
|
|
1205
|
+
process.exit(1);
|
|
1206
|
+
}
|
|
1207
|
+
p.note(brightNote$1([
|
|
1208
|
+
bold$1("This will permanently delete from your new PDS:"),
|
|
1209
|
+
"",
|
|
1210
|
+
` • ${formatNumber(status.repoBlocks)} repository blocks`,
|
|
1211
|
+
` • ${formatNumber(status.importedBlobs)} imported images`,
|
|
1212
|
+
" • All blob tracking data",
|
|
1213
|
+
"",
|
|
1214
|
+
bold$1(`Your data on ${sourceDomain} is NOT affected.`),
|
|
1215
|
+
"You'll need to re-import everything."
|
|
1216
|
+
]), "⚠️ Reset Migration Data");
|
|
1217
|
+
const confirmReset = await p.confirm({
|
|
1218
|
+
message: "Are you sure you want to delete this data?",
|
|
1219
|
+
initialValue: false
|
|
1220
|
+
});
|
|
1221
|
+
if (p.isCancel(confirmReset) || !confirmReset) {
|
|
1222
|
+
p.cancel("Keeping your data.");
|
|
1223
|
+
process.exit(0);
|
|
1224
|
+
}
|
|
1225
|
+
spinner.start("Resetting migration state...");
|
|
1226
|
+
try {
|
|
1227
|
+
const result = await targetClient.resetMigration();
|
|
1228
|
+
spinner.stop(`Deleted ${formatNumber(result.blocksDeleted)} blocks, ${formatNumber(result.blobsCleared)} blobs`);
|
|
1229
|
+
} catch (err) {
|
|
1230
|
+
spinner.stop("Reset failed");
|
|
1231
|
+
p.log.error(err instanceof Error ? err.message : "Could not reset migration");
|
|
1232
|
+
p.outro("Migration cancelled.");
|
|
1233
|
+
process.exit(1);
|
|
1234
|
+
}
|
|
1235
|
+
p.log.success("Clean slate! Starting fresh migration...");
|
|
1236
|
+
status = await targetClient.getAccountStatus();
|
|
1237
|
+
}
|
|
1238
|
+
if (status.active) {
|
|
1239
|
+
p.log.warn("Your account is already active in the Atmosphere!");
|
|
1240
|
+
p.log.info("No migration needed - your PDS is live.");
|
|
1241
|
+
p.outro("All good! 🦋");
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
spinner.start(`Fetching your account details from ${sourceDomain}...`);
|
|
1245
|
+
const sourceClient = new PDSClient(sourcePdsUrl);
|
|
1246
|
+
try {
|
|
1247
|
+
await sourceClient.describeRepo(did);
|
|
1248
|
+
} catch (err) {
|
|
1249
|
+
spinner.stop("Failed to fetch account details");
|
|
1250
|
+
p.log.error(err instanceof Error ? err.message : "Could not fetch account details from source PDS");
|
|
1251
|
+
p.outro("Migration cancelled.");
|
|
1252
|
+
process.exit(1);
|
|
1253
|
+
}
|
|
1254
|
+
const profileStats = await sourceClient.getProfileStats(did);
|
|
1255
|
+
spinner.stop("Account details fetched");
|
|
1256
|
+
const needsRepoImport = status.repoBlocks === 0 || status.indexedRecords === 0 && status.expectedBlobs === 0;
|
|
1257
|
+
const needsBlobSync = status.expectedBlobs - status.importedBlobs > 0 || needsRepoImport;
|
|
1258
|
+
if (!needsRepoImport && needsBlobSync) {
|
|
1259
|
+
p.log.info("Welcome back!");
|
|
1260
|
+
p.log.info("Looks like you started packing earlier. Let's pick up where we left off.");
|
|
1261
|
+
p.note([
|
|
1262
|
+
`@${handle} (${did.slice(0, 20)}...)`,
|
|
1263
|
+
"",
|
|
1264
|
+
"✓ Repository imported",
|
|
1265
|
+
`◐ Images: ${formatNumber(status.importedBlobs)}/${formatNumber(status.expectedBlobs)} transferred`
|
|
1266
|
+
].join("\n"), "Migration Progress");
|
|
1267
|
+
const continueTransfer = await p.confirm({
|
|
1268
|
+
message: "Continue transferring images?",
|
|
1269
|
+
initialValue: true
|
|
1270
|
+
});
|
|
1271
|
+
if (p.isCancel(continueTransfer) || !continueTransfer) {
|
|
1272
|
+
p.cancel("Migration paused.");
|
|
1273
|
+
process.exit(0);
|
|
1274
|
+
}
|
|
1275
|
+
} else if (needsRepoImport) {
|
|
1276
|
+
p.log.info("Time to pack your bags!");
|
|
1277
|
+
p.log.info("Let's move your Bluesky account to its new home in the Atmosphere.");
|
|
1278
|
+
const statsLines = profileStats ? [
|
|
1279
|
+
` 📝 ${formatNumber(profileStats.postsCount)} posts`,
|
|
1280
|
+
` 👥 ${formatNumber(profileStats.followsCount)} follows`,
|
|
1281
|
+
` ...plus all your images, likes, and blocks`
|
|
1282
|
+
] : [` 📝 Posts, follows, images, likes, and blocks`];
|
|
1283
|
+
p.note(brightNote$1([
|
|
1284
|
+
bold$1(`@${handle}`) + ` (${did.slice(0, 20)}...)`,
|
|
1285
|
+
"",
|
|
1286
|
+
`Currently at: ${sourceDomain}`,
|
|
1287
|
+
`Moving to: ${targetDomain}`,
|
|
1288
|
+
"",
|
|
1289
|
+
"What you're bringing:",
|
|
1290
|
+
...statsLines
|
|
1291
|
+
]), "Your Bluesky Account 🦋");
|
|
1292
|
+
p.log.info("This will copy your data - nothing is changed or deleted on Bluesky.");
|
|
1293
|
+
const proceed = await p.confirm({
|
|
1294
|
+
message: "Ready to start packing?",
|
|
1295
|
+
initialValue: true
|
|
1296
|
+
});
|
|
1297
|
+
if (p.isCancel(proceed) || !proceed) {
|
|
1298
|
+
p.cancel("Migration cancelled.");
|
|
1299
|
+
process.exit(0);
|
|
1300
|
+
}
|
|
1301
|
+
} else {
|
|
1302
|
+
p.log.success("All packed and moved! 🦋");
|
|
1303
|
+
showNextSteps(pm, sourceDomain);
|
|
1304
|
+
p.outro("Welcome to your new home in the Atmosphere! 🦋");
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
const isBlueskyPds = sourceDomain.endsWith(".bsky.network");
|
|
1308
|
+
const passwordPrompt = isBlueskyPds ? "Your current Bluesky password:" : `Your ${sourceDomain} password:`;
|
|
1309
|
+
const password = await p.password({ message: passwordPrompt });
|
|
1310
|
+
if (p.isCancel(password)) {
|
|
1311
|
+
p.cancel("Migration cancelled.");
|
|
1312
|
+
process.exit(0);
|
|
1313
|
+
}
|
|
1314
|
+
spinner.start(`Logging in to ${isBlueskyPds ? "Bluesky" : sourceDomain}...`);
|
|
1315
|
+
try {
|
|
1316
|
+
const session = await sourceClient.createSession(did, password);
|
|
1317
|
+
sourceClient.setAuthToken(session.accessJwt);
|
|
1318
|
+
spinner.stop("Authenticated successfully");
|
|
1319
|
+
} catch (err) {
|
|
1320
|
+
spinner.stop("Login failed");
|
|
1321
|
+
if (err instanceof PDSClientError) p.log.error(`Authentication failed: ${err.message}`);
|
|
1322
|
+
else p.log.error(err instanceof Error ? err.message : "Authentication failed");
|
|
1323
|
+
p.outro("Migration cancelled.");
|
|
1324
|
+
process.exit(1);
|
|
1325
|
+
}
|
|
1326
|
+
if (needsRepoImport) {
|
|
1327
|
+
spinner.start("Packing your repository...");
|
|
1328
|
+
let carBytes;
|
|
1329
|
+
try {
|
|
1330
|
+
carBytes = await sourceClient.getRepo(did);
|
|
1331
|
+
spinner.stop(`Downloaded ${formatBytes(carBytes.length)} from ${sourceDomain}`);
|
|
1332
|
+
} catch (err) {
|
|
1333
|
+
spinner.stop("Export failed");
|
|
1334
|
+
p.log.error(err instanceof Error ? err.message : "Could not export repository");
|
|
1335
|
+
p.outro("Migration cancelled.");
|
|
1336
|
+
process.exit(1);
|
|
1337
|
+
}
|
|
1338
|
+
spinner.start(`Unpacking at ${targetDomain}...`);
|
|
1339
|
+
try {
|
|
1340
|
+
await targetClient.importRepo(carBytes);
|
|
1341
|
+
spinner.stop("Repository imported");
|
|
1342
|
+
} catch (err) {
|
|
1343
|
+
spinner.stop("Import failed");
|
|
1344
|
+
p.log.error(err instanceof Error ? err.message : "Could not import repository");
|
|
1345
|
+
p.outro("Migration cancelled.");
|
|
1346
|
+
process.exit(1);
|
|
1347
|
+
}
|
|
1348
|
+
status = await targetClient.getAccountStatus();
|
|
1349
|
+
}
|
|
1350
|
+
if (status.expectedBlobs - status.importedBlobs > 0) {
|
|
1351
|
+
let synced = 0;
|
|
1352
|
+
let totalBlobs = 0;
|
|
1353
|
+
let cursor;
|
|
1354
|
+
let failedBlobs = [];
|
|
1355
|
+
const progressBar = (current, total) => {
|
|
1356
|
+
const width = 20;
|
|
1357
|
+
const ratio = total > 0 ? Math.min(1, current / total) : 0;
|
|
1358
|
+
const filled = Math.round(ratio * width);
|
|
1359
|
+
const empty = width - filled;
|
|
1360
|
+
return `${"█".repeat(filled)}${"░".repeat(empty)} ${current}/${total}`;
|
|
1361
|
+
};
|
|
1362
|
+
spinner.start("Counting images to transfer...");
|
|
1363
|
+
let countCursor;
|
|
1364
|
+
do {
|
|
1365
|
+
const page = await targetClient.listMissingBlobs(500, countCursor);
|
|
1366
|
+
totalBlobs += page.blobs.length;
|
|
1367
|
+
countCursor = page.cursor;
|
|
1368
|
+
} while (countCursor);
|
|
1369
|
+
spinner.message(`Transferring images ${progressBar(0, totalBlobs)}`);
|
|
1370
|
+
do {
|
|
1371
|
+
const page = await targetClient.listMissingBlobs(100, cursor);
|
|
1372
|
+
cursor = page.cursor;
|
|
1373
|
+
for (const blob of page.blobs) try {
|
|
1374
|
+
const { bytes, mimeType } = await sourceClient.getBlob(did, blob.cid);
|
|
1375
|
+
await targetClient.uploadBlob(bytes, mimeType);
|
|
1376
|
+
synced++;
|
|
1377
|
+
spinner.message(`Transferring images ${progressBar(synced, totalBlobs)}`);
|
|
1378
|
+
} catch (err) {
|
|
1379
|
+
synced++;
|
|
1380
|
+
failedBlobs.push(blob.cid);
|
|
1381
|
+
spinner.message(`Transferring images ${progressBar(synced, totalBlobs)}`);
|
|
1382
|
+
}
|
|
1383
|
+
} while (cursor);
|
|
1384
|
+
if (failedBlobs.length > 0) {
|
|
1385
|
+
spinner.stop(`Transferred ${formatNumber(synced - failedBlobs.length)} images (${failedBlobs.length} failed)`);
|
|
1386
|
+
p.log.warn(`Run 'pds migrate' again to retry failed transfers.`);
|
|
1387
|
+
} else spinner.stop(`Transferred ${formatNumber(synced)} images`);
|
|
1388
|
+
}
|
|
1389
|
+
spinner.start("Verifying migration...");
|
|
1390
|
+
const finalStatus = await targetClient.getAccountStatus();
|
|
1391
|
+
spinner.stop("Verification complete");
|
|
1392
|
+
if (finalStatus.importedBlobs >= finalStatus.expectedBlobs) p.log.success("All packed and moved! 🦋");
|
|
1393
|
+
else {
|
|
1394
|
+
p.log.warn(`Migration partially complete. ${finalStatus.expectedBlobs - finalStatus.importedBlobs} images remaining.`);
|
|
1395
|
+
p.log.info("Run 'pds migrate' again to continue.");
|
|
1396
|
+
}
|
|
1397
|
+
showNextSteps(pm, sourceDomain);
|
|
1398
|
+
p.outro("Welcome to your new home in the Atmosphere! 🦋");
|
|
1399
|
+
}
|
|
1400
|
+
});
|
|
1401
|
+
function showNextSteps(pm, sourceDomain) {
|
|
1402
|
+
p.note(brightNote$1([
|
|
1403
|
+
bold$1("Your data is safe in your new PDS."),
|
|
1404
|
+
"Two more steps to go live in the Atmosphere:",
|
|
1405
|
+
"",
|
|
1406
|
+
bold$1("1. Update your identity"),
|
|
1407
|
+
" Tell the network where you live now.",
|
|
1408
|
+
` (Requires email verification from ${sourceDomain})`,
|
|
1409
|
+
"",
|
|
1410
|
+
bold$1("2. Flip the switch"),
|
|
1411
|
+
` ${pm} pds activate`,
|
|
1412
|
+
"",
|
|
1413
|
+
"Docs: https://atproto.com/guides/account-migration"
|
|
1414
|
+
]), "Almost there!");
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
//#endregion
|
|
1418
|
+
//#region src/cli/commands/activate.ts
|
|
1419
|
+
/**
|
|
1420
|
+
* Activate account command - enables writes after migration
|
|
1421
|
+
*/
|
|
1422
|
+
const activateCommand = defineCommand({
|
|
1423
|
+
meta: {
|
|
1424
|
+
name: "activate",
|
|
1425
|
+
description: "Activate your account to enable writes and go live"
|
|
1426
|
+
},
|
|
1427
|
+
args: { dev: {
|
|
1428
|
+
type: "boolean",
|
|
1429
|
+
description: "Target local development server instead of production",
|
|
1430
|
+
default: false
|
|
1431
|
+
} },
|
|
1432
|
+
async run({ args }) {
|
|
1433
|
+
const isDev = args.dev;
|
|
1434
|
+
p.intro("🦋 Activate Account");
|
|
1435
|
+
const vars = getVars();
|
|
1436
|
+
let targetUrl;
|
|
1437
|
+
try {
|
|
1438
|
+
targetUrl = getTargetUrl(isDev, vars.PDS_HOSTNAME);
|
|
1439
|
+
} catch (err) {
|
|
1440
|
+
p.log.error(err instanceof Error ? err.message : "Configuration error");
|
|
1441
|
+
p.log.info("Run 'pds init' first to configure your PDS.");
|
|
1442
|
+
process.exit(1);
|
|
1443
|
+
}
|
|
1444
|
+
const targetDomain = getDomain(targetUrl);
|
|
1445
|
+
const wranglerVars = getVars();
|
|
1446
|
+
const config = {
|
|
1447
|
+
...readDevVars(),
|
|
1448
|
+
...wranglerVars
|
|
1449
|
+
};
|
|
1450
|
+
const authToken = config.AUTH_TOKEN;
|
|
1451
|
+
const handle = config.HANDLE;
|
|
1452
|
+
if (!authToken) {
|
|
1453
|
+
p.log.error("No AUTH_TOKEN found. Run 'pds init' first.");
|
|
1454
|
+
p.outro("Activation cancelled.");
|
|
1455
|
+
process.exit(1);
|
|
1456
|
+
}
|
|
1457
|
+
const client = new PDSClient(targetUrl, authToken);
|
|
1458
|
+
const spinner = p.spinner();
|
|
1459
|
+
spinner.start(`Checking PDS at ${targetDomain}...`);
|
|
1460
|
+
if (!await client.healthCheck()) {
|
|
1461
|
+
spinner.stop(`PDS not responding at ${targetDomain}`);
|
|
1462
|
+
p.log.error(`Your PDS isn't responding at ${targetUrl}`);
|
|
1463
|
+
if (!isDev) p.log.info("Make sure your worker is deployed: wrangler deploy");
|
|
1464
|
+
p.outro("Activation cancelled.");
|
|
1465
|
+
process.exit(1);
|
|
1466
|
+
}
|
|
1467
|
+
spinner.stop(`Connected to ${targetDomain}`);
|
|
1468
|
+
spinner.start("Checking account status...");
|
|
1469
|
+
const status = await client.getAccountStatus();
|
|
1470
|
+
spinner.stop("Account status retrieved");
|
|
1471
|
+
if (status.active) {
|
|
1472
|
+
p.log.warn("Your account is already active!");
|
|
1473
|
+
p.log.info("No action needed - you're live in the Atmosphere. 🦋");
|
|
1474
|
+
p.outro("All good!");
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
p.note([
|
|
1478
|
+
`@${handle || "your-handle"}`,
|
|
1479
|
+
"",
|
|
1480
|
+
"This will enable writes and make your account live.",
|
|
1481
|
+
"Make sure you've:",
|
|
1482
|
+
" ✓ Updated your DID document to point here",
|
|
1483
|
+
" ✓ Completed email verification (if required)"
|
|
1484
|
+
].join("\n"), "Ready to go live?");
|
|
1485
|
+
const confirm = await p.confirm({
|
|
1486
|
+
message: "Activate account?",
|
|
1487
|
+
initialValue: true
|
|
1488
|
+
});
|
|
1489
|
+
if (p.isCancel(confirm) || !confirm) {
|
|
1490
|
+
p.cancel("Activation cancelled.");
|
|
1491
|
+
process.exit(0);
|
|
1492
|
+
}
|
|
1493
|
+
spinner.start("Activating account...");
|
|
1494
|
+
try {
|
|
1495
|
+
await client.activateAccount();
|
|
1496
|
+
spinner.stop("Account activated!");
|
|
1497
|
+
} catch (err) {
|
|
1498
|
+
spinner.stop("Activation failed");
|
|
1499
|
+
p.log.error(err instanceof Error ? err.message : "Could not activate account");
|
|
1500
|
+
p.outro("Activation failed.");
|
|
1501
|
+
process.exit(1);
|
|
1502
|
+
}
|
|
1503
|
+
p.log.success("Welcome to the Atmosphere! 🦋");
|
|
1504
|
+
p.log.info("Your account is now live and accepting writes.");
|
|
1505
|
+
p.outro("All set!");
|
|
1506
|
+
}
|
|
1507
|
+
});
|
|
1508
|
+
|
|
1509
|
+
//#endregion
|
|
1510
|
+
//#region src/cli/commands/deactivate.ts
|
|
1511
|
+
/**
|
|
1512
|
+
* Deactivate account command - disables writes for re-import
|
|
1513
|
+
*/
|
|
1514
|
+
const brightNote = (lines) => lines.map((l) => `\x1b[0m${l}`).join("\n");
|
|
1515
|
+
const bold = (text) => pc.bold(text);
|
|
1516
|
+
const deactivateCommand = defineCommand({
|
|
1517
|
+
meta: {
|
|
1518
|
+
name: "deactivate",
|
|
1519
|
+
description: "Deactivate your account to enable re-import"
|
|
1520
|
+
},
|
|
1521
|
+
args: { dev: {
|
|
1522
|
+
type: "boolean",
|
|
1523
|
+
description: "Target local development server instead of production",
|
|
1524
|
+
default: false
|
|
1525
|
+
} },
|
|
1526
|
+
async run({ args }) {
|
|
1527
|
+
const isDev = args.dev;
|
|
1528
|
+
p.intro("🦋 Deactivate Account");
|
|
1529
|
+
const vars = getVars();
|
|
1530
|
+
let targetUrl;
|
|
1531
|
+
try {
|
|
1532
|
+
targetUrl = getTargetUrl(isDev, vars.PDS_HOSTNAME);
|
|
1533
|
+
} catch (err) {
|
|
1534
|
+
p.log.error(err instanceof Error ? err.message : "Configuration error");
|
|
1535
|
+
p.log.info("Run 'pds init' first to configure your PDS.");
|
|
1536
|
+
process.exit(1);
|
|
1537
|
+
}
|
|
1538
|
+
const targetDomain = getDomain(targetUrl);
|
|
1539
|
+
const wranglerVars = getVars();
|
|
1540
|
+
const config = {
|
|
1541
|
+
...readDevVars(),
|
|
1542
|
+
...wranglerVars
|
|
1543
|
+
};
|
|
1544
|
+
const authToken = config.AUTH_TOKEN;
|
|
1545
|
+
const handle = config.HANDLE;
|
|
1546
|
+
if (!authToken) {
|
|
1547
|
+
p.log.error("No AUTH_TOKEN found. Run 'pds init' first.");
|
|
1548
|
+
p.outro("Deactivation cancelled.");
|
|
1549
|
+
process.exit(1);
|
|
1550
|
+
}
|
|
1551
|
+
const client = new PDSClient(targetUrl, authToken);
|
|
1552
|
+
const spinner = p.spinner();
|
|
1553
|
+
spinner.start(`Checking PDS at ${targetDomain}...`);
|
|
1554
|
+
if (!await client.healthCheck()) {
|
|
1555
|
+
spinner.stop(`PDS not responding at ${targetDomain}`);
|
|
1556
|
+
p.log.error(`Your PDS isn't responding at ${targetUrl}`);
|
|
1557
|
+
if (!isDev) p.log.info("Make sure your worker is deployed: wrangler deploy");
|
|
1558
|
+
p.outro("Deactivation cancelled.");
|
|
1559
|
+
process.exit(1);
|
|
1560
|
+
}
|
|
1561
|
+
spinner.stop(`Connected to ${targetDomain}`);
|
|
1562
|
+
spinner.start("Checking account status...");
|
|
1563
|
+
const status = await client.getAccountStatus();
|
|
1564
|
+
spinner.stop("Account status retrieved");
|
|
1565
|
+
if (!status.active) {
|
|
1566
|
+
p.log.warn("Your account is already deactivated.");
|
|
1567
|
+
p.log.info("Writes are disabled. Use 'pds activate' to re-enable.");
|
|
1568
|
+
p.outro("Already deactivated.");
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
p.note(brightNote([
|
|
1572
|
+
bold(`⚠️ WARNING: This will disable writes for @${handle || "your-handle"}`),
|
|
1573
|
+
"",
|
|
1574
|
+
"Your account will:",
|
|
1575
|
+
" • Stop accepting new posts, follows, and other writes",
|
|
1576
|
+
" • Remain readable in the Atmosphere",
|
|
1577
|
+
" • Allow you to use 'pds migrate --clean' to re-import",
|
|
1578
|
+
"",
|
|
1579
|
+
bold("Only deactivate if you need to re-import your data.")
|
|
1580
|
+
]), "Deactivate Account");
|
|
1581
|
+
const confirm = await p.confirm({
|
|
1582
|
+
message: "Are you sure you want to deactivate?",
|
|
1583
|
+
initialValue: false
|
|
1584
|
+
});
|
|
1585
|
+
if (p.isCancel(confirm) || !confirm) {
|
|
1586
|
+
p.cancel("Deactivation cancelled.");
|
|
1587
|
+
process.exit(0);
|
|
1588
|
+
}
|
|
1589
|
+
spinner.start("Deactivating account...");
|
|
1590
|
+
try {
|
|
1591
|
+
await client.deactivateAccount();
|
|
1592
|
+
spinner.stop("Account deactivated");
|
|
1593
|
+
} catch (err) {
|
|
1594
|
+
spinner.stop("Deactivation failed");
|
|
1595
|
+
p.log.error(err instanceof Error ? err.message : "Could not deactivate account");
|
|
1596
|
+
p.outro("Deactivation failed.");
|
|
1597
|
+
process.exit(1);
|
|
1598
|
+
}
|
|
1599
|
+
p.log.success("Account deactivated");
|
|
1600
|
+
p.log.info("Writes are now disabled.");
|
|
1601
|
+
p.log.info("");
|
|
1602
|
+
p.log.info("To re-import your data:");
|
|
1603
|
+
p.log.info(" pnpm pds migrate --clean");
|
|
1604
|
+
p.log.info("");
|
|
1605
|
+
p.log.info("To re-enable writes:");
|
|
1606
|
+
p.log.info(" pnpm pds activate");
|
|
1607
|
+
p.outro("Deactivated.");
|
|
1608
|
+
}
|
|
1609
|
+
});
|
|
1610
|
+
|
|
508
1611
|
//#endregion
|
|
509
1612
|
//#region src/cli/index.ts
|
|
510
1613
|
/**
|
|
@@ -518,7 +1621,10 @@ runMain(defineCommand({
|
|
|
518
1621
|
},
|
|
519
1622
|
subCommands: {
|
|
520
1623
|
init: initCommand,
|
|
521
|
-
secret: secretCommand
|
|
1624
|
+
secret: secretCommand,
|
|
1625
|
+
migrate: migrateCommand,
|
|
1626
|
+
activate: activateCommand,
|
|
1627
|
+
deactivate: deactivateCommand
|
|
522
1628
|
}
|
|
523
1629
|
}));
|
|
524
1630
|
|