@ascorbic/pds 0.2.0 → 0.2.2
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 +188 -211
- package/dist/index.js +38 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -339,6 +339,63 @@ const secretCommand = defineCommand({
|
|
|
339
339
|
}
|
|
340
340
|
});
|
|
341
341
|
|
|
342
|
+
//#endregion
|
|
343
|
+
//#region src/cli/utils/cli-helpers.ts
|
|
344
|
+
/**
|
|
345
|
+
* Shared CLI utilities for PDS commands
|
|
346
|
+
*/
|
|
347
|
+
/**
|
|
348
|
+
* Prompt for text input, exiting on cancel
|
|
349
|
+
*/
|
|
350
|
+
async function promptText(options) {
|
|
351
|
+
const result = await p.text(options);
|
|
352
|
+
if (p.isCancel(result)) {
|
|
353
|
+
p.cancel("Cancelled");
|
|
354
|
+
process.exit(0);
|
|
355
|
+
}
|
|
356
|
+
return result;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Prompt for confirmation, exiting on cancel
|
|
360
|
+
*/
|
|
361
|
+
async function promptConfirm(options) {
|
|
362
|
+
const result = await p.confirm(options);
|
|
363
|
+
if (p.isCancel(result)) {
|
|
364
|
+
p.cancel("Cancelled");
|
|
365
|
+
process.exit(0);
|
|
366
|
+
}
|
|
367
|
+
return result;
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Prompt for selection, exiting on cancel
|
|
371
|
+
*/
|
|
372
|
+
async function promptSelect(options) {
|
|
373
|
+
const result = await p.select(options);
|
|
374
|
+
if (p.isCancel(result)) {
|
|
375
|
+
p.cancel("Cancelled");
|
|
376
|
+
process.exit(0);
|
|
377
|
+
}
|
|
378
|
+
return result;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Get target PDS URL based on mode
|
|
382
|
+
*/
|
|
383
|
+
function getTargetUrl(isDev, pdsHostname) {
|
|
384
|
+
if (isDev) return `http://localhost:${process.env.PORT ? parseInt(process.env.PORT) ?? "5173" : "5173"}`;
|
|
385
|
+
if (!pdsHostname) throw new Error("PDS_HOSTNAME not configured in wrangler.jsonc");
|
|
386
|
+
return `https://${pdsHostname}`;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Extract domain from URL
|
|
390
|
+
*/
|
|
391
|
+
function getDomain(url) {
|
|
392
|
+
try {
|
|
393
|
+
return new URL(url).hostname;
|
|
394
|
+
} catch {
|
|
395
|
+
return url;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
342
399
|
//#endregion
|
|
343
400
|
//#region src/cli/utils/handle-resolver.ts
|
|
344
401
|
/**
|
|
@@ -456,6 +513,22 @@ var DidResolver = class {
|
|
|
456
513
|
function slugifyHandle(handle) {
|
|
457
514
|
return handle.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") + "-pds";
|
|
458
515
|
}
|
|
516
|
+
const defaultWorkerName = "my-pds";
|
|
517
|
+
/**
|
|
518
|
+
* Prompt for worker name with validation
|
|
519
|
+
*/
|
|
520
|
+
async function promptWorkerName(handle, currentWorkerName) {
|
|
521
|
+
const placeholder = currentWorkerName && currentWorkerName !== defaultWorkerName ? currentWorkerName : slugifyHandle(handle);
|
|
522
|
+
return promptText({
|
|
523
|
+
message: "Cloudflare Worker name:",
|
|
524
|
+
placeholder,
|
|
525
|
+
initialValue: placeholder,
|
|
526
|
+
validate: (v) => {
|
|
527
|
+
if (!v) return "Worker name is required";
|
|
528
|
+
if (!/^[a-z0-9-]+$/.test(v)) return "Worker name can only contain lowercase letters, numbers, and hyphens";
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
}
|
|
459
532
|
/**
|
|
460
533
|
* Run wrangler types to regenerate TypeScript types
|
|
461
534
|
*/
|
|
@@ -486,28 +559,24 @@ const initCommand = defineCommand({
|
|
|
486
559
|
},
|
|
487
560
|
args: { production: {
|
|
488
561
|
type: "boolean",
|
|
489
|
-
description: "Deploy secrets to Cloudflare
|
|
562
|
+
description: "Deploy secrets to Cloudflare?",
|
|
490
563
|
default: false
|
|
491
564
|
} },
|
|
492
565
|
async run({ args }) {
|
|
493
566
|
p.intro("🦋 PDS Setup");
|
|
494
567
|
const isProduction = args.production;
|
|
495
|
-
if (isProduction) p.log.info("Production mode: secrets will be deployed
|
|
496
|
-
|
|
568
|
+
if (isProduction) p.log.info("Production mode: secrets will be deployed to Cloudflare");
|
|
569
|
+
p.log.info("Let's set up your new home in the Atmosphere!");
|
|
497
570
|
const wranglerVars = getVars();
|
|
498
571
|
const devVars = readDevVars();
|
|
499
572
|
const currentVars = {
|
|
500
573
|
...devVars,
|
|
501
574
|
...wranglerVars
|
|
502
575
|
};
|
|
503
|
-
const isMigrating = await
|
|
504
|
-
message: "Are you migrating an existing Bluesky account?
|
|
576
|
+
const isMigrating = await promptConfirm({
|
|
577
|
+
message: "Are you migrating an existing Bluesky/ATProto account?",
|
|
505
578
|
initialValue: false
|
|
506
579
|
});
|
|
507
|
-
if (p.isCancel(isMigrating)) {
|
|
508
|
-
p.cancel("Setup cancelled");
|
|
509
|
-
process.exit(0);
|
|
510
|
-
}
|
|
511
580
|
let did;
|
|
512
581
|
let handle;
|
|
513
582
|
let hostname;
|
|
@@ -516,28 +585,24 @@ const initCommand = defineCommand({
|
|
|
516
585
|
const currentWorkerName = getWorkerName();
|
|
517
586
|
if (isMigrating) {
|
|
518
587
|
p.log.info("Time to pack your bags! 🧳");
|
|
519
|
-
p.log.info("Your account will be inactive until you'
|
|
588
|
+
p.log.info("Your new account will be inactive until you're ready to go live.");
|
|
520
589
|
let hostedDomains = [
|
|
521
590
|
".bsky.social",
|
|
522
591
|
".bsky.network",
|
|
523
592
|
".bsky.team"
|
|
524
593
|
];
|
|
525
|
-
const isHostedHandle = (h) => hostedDomains.some((domain) => h
|
|
594
|
+
const isHostedHandle = (h) => hostedDomains.some((domain) => h?.endsWith(domain));
|
|
526
595
|
let resolvedDid = null;
|
|
527
596
|
let existingHandle = null;
|
|
528
597
|
let attempts = 0;
|
|
529
598
|
const MAX_ATTEMPTS = 3;
|
|
530
599
|
while (!resolvedDid && attempts < MAX_ATTEMPTS) {
|
|
531
600
|
attempts++;
|
|
532
|
-
const currentHandle = await
|
|
601
|
+
const currentHandle = await promptText({
|
|
533
602
|
message: "Your current Bluesky/ATProto handle:",
|
|
534
603
|
placeholder: "example.bsky.social",
|
|
535
604
|
validate: (v) => !v ? "Handle is required" : void 0
|
|
536
605
|
});
|
|
537
|
-
if (p.isCancel(currentHandle)) {
|
|
538
|
-
p.cancel("Cancelled");
|
|
539
|
-
process.exit(0);
|
|
540
|
-
}
|
|
541
606
|
existingHandle = currentHandle;
|
|
542
607
|
const spinner$1 = p.spinner();
|
|
543
608
|
spinner$1.start("Finding you in the Atmosphere...");
|
|
@@ -545,7 +610,7 @@ const initCommand = defineCommand({
|
|
|
545
610
|
if (!resolvedDid) {
|
|
546
611
|
spinner$1.stop("Not found");
|
|
547
612
|
p.log.error(`Failed to resolve handle "${currentHandle}"`);
|
|
548
|
-
|
|
613
|
+
if (await promptSelect({
|
|
549
614
|
message: "What would you like to do?",
|
|
550
615
|
options: [{
|
|
551
616
|
value: "retry",
|
|
@@ -554,26 +619,14 @@ const initCommand = defineCommand({
|
|
|
554
619
|
value: "manual",
|
|
555
620
|
label: "Enter DID manually"
|
|
556
621
|
}]
|
|
557
|
-
})
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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);
|
|
622
|
+
}) === "manual") resolvedDid = await promptText({
|
|
623
|
+
message: "Enter your DID:",
|
|
624
|
+
placeholder: "did:plc:...",
|
|
625
|
+
validate: (v) => {
|
|
626
|
+
if (!v) return "DID is required";
|
|
627
|
+
if (!v.startsWith("did:")) return "DID must start with did:";
|
|
574
628
|
}
|
|
575
|
-
|
|
576
|
-
}
|
|
629
|
+
});
|
|
577
630
|
} else {
|
|
578
631
|
try {
|
|
579
632
|
const pdsService = (await new DidResolver().resolve(resolvedDid))?.service?.find((s) => s.type === "AtprotoPersonalDataServer" || s.id === "#atproto_pds");
|
|
@@ -587,7 +640,7 @@ const initCommand = defineCommand({
|
|
|
587
640
|
} catch {}
|
|
588
641
|
spinner$1.stop(`Found you! ${resolvedDid}`);
|
|
589
642
|
if (isHostedHandle(existingHandle)) {
|
|
590
|
-
const theirDomain = hostedDomains.find((d) => existingHandle
|
|
643
|
+
const theirDomain = hostedDomains.find((d) => existingHandle?.endsWith(d));
|
|
591
644
|
const domainExample = theirDomain ? `*${theirDomain}` : "*.bsky.social";
|
|
592
645
|
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
646
|
}
|
|
@@ -604,95 +657,48 @@ const initCommand = defineCommand({
|
|
|
604
657
|
}
|
|
605
658
|
}
|
|
606
659
|
did = resolvedDid;
|
|
607
|
-
|
|
608
|
-
handle = await p.text({
|
|
660
|
+
handle = await promptText({
|
|
609
661
|
message: "New account handle (must be a domain you control):",
|
|
610
662
|
placeholder: "example.com",
|
|
611
|
-
initialValue:
|
|
663
|
+
initialValue: existingHandle && !isHostedHandle(existingHandle) ? existingHandle : currentVars.HANDLE || "",
|
|
612
664
|
validate: (v) => {
|
|
613
665
|
if (!v) return "Handle is required";
|
|
614
666
|
if (isHostedHandle(v)) return "You need a custom domain - hosted handles like *.bsky.social won't work";
|
|
615
667
|
}
|
|
616
668
|
});
|
|
617
|
-
|
|
618
|
-
p.cancel("Cancelled");
|
|
619
|
-
process.exit(0);
|
|
620
|
-
}
|
|
621
|
-
hostname = await p.text({
|
|
669
|
+
hostname = await promptText({
|
|
622
670
|
message: "Domain where you'll deploy your PDS:",
|
|
623
671
|
placeholder: handle,
|
|
624
672
|
initialValue: currentVars.PDS_HOSTNAME || handle,
|
|
625
673
|
validate: (v) => !v ? "Hostname is required" : void 0
|
|
626
674
|
});
|
|
627
|
-
|
|
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
|
-
}
|
|
675
|
+
workerName = await promptWorkerName(handle, currentWorkerName);
|
|
645
676
|
initialActive = "false";
|
|
646
677
|
} else {
|
|
647
678
|
p.log.info("A fresh start in the Atmosphere! ✨");
|
|
648
|
-
hostname = await
|
|
679
|
+
hostname = await promptText({
|
|
649
680
|
message: "Domain where you'll deploy your PDS:",
|
|
650
681
|
placeholder: "pds.example.com",
|
|
651
682
|
initialValue: currentVars.PDS_HOSTNAME || "",
|
|
652
683
|
validate: (v) => !v ? "Hostname is required" : void 0
|
|
653
684
|
});
|
|
654
|
-
|
|
655
|
-
p.cancel("Cancelled");
|
|
656
|
-
process.exit(0);
|
|
657
|
-
}
|
|
658
|
-
handle = await p.text({
|
|
685
|
+
handle = await promptText({
|
|
659
686
|
message: "Account handle:",
|
|
660
687
|
placeholder: hostname,
|
|
661
688
|
initialValue: currentVars.HANDLE || hostname,
|
|
662
689
|
validate: (v) => !v ? "Handle is required" : void 0
|
|
663
690
|
});
|
|
664
|
-
if (p.isCancel(handle)) {
|
|
665
|
-
p.cancel("Cancelled");
|
|
666
|
-
process.exit(0);
|
|
667
|
-
}
|
|
668
691
|
const didDefault = "did:web:" + hostname;
|
|
669
|
-
did = await
|
|
692
|
+
did = await promptText({
|
|
670
693
|
message: "Account DID:",
|
|
671
694
|
placeholder: didDefault,
|
|
672
695
|
initialValue: currentVars.DID || didDefault,
|
|
673
696
|
validate: (v) => {
|
|
674
697
|
if (!v) return "DID is required";
|
|
675
|
-
if (!v.startsWith("did:")) return "DID must start with did:";
|
|
698
|
+
if (!v.startsWith("did:")) return "DID must start with 'did:'";
|
|
676
699
|
}
|
|
677
700
|
});
|
|
678
|
-
|
|
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
|
-
}
|
|
701
|
+
workerName = await promptWorkerName(handle, currentWorkerName);
|
|
696
702
|
initialActive = "true";
|
|
697
703
|
if (handle === hostname) p.note([
|
|
698
704
|
"Your handle matches your PDS hostname, so your PDS will",
|
|
@@ -719,55 +725,32 @@ const initCommand = defineCommand({
|
|
|
719
725
|
].join("\n"), "Identity Setup 🪪");
|
|
720
726
|
}
|
|
721
727
|
const spinner = p.spinner();
|
|
722
|
-
|
|
723
|
-
let signingKey;
|
|
724
|
-
let signingKeyPublic;
|
|
725
|
-
let jwtSecret;
|
|
726
|
-
let passwordHash;
|
|
727
|
-
if (isProduction) {
|
|
728
|
-
authToken = await getOrGenerateSecret("AUTH_TOKEN", devVars, async () => {
|
|
729
|
-
spinner.start("Generating auth token...");
|
|
730
|
-
const token = generateAuthToken();
|
|
731
|
-
spinner.stop("Auth token generated");
|
|
732
|
-
return token;
|
|
733
|
-
});
|
|
734
|
-
signingKey = await getOrGenerateSecret("SIGNING_KEY", devVars, async () => {
|
|
735
|
-
spinner.start("Generating signing keypair...");
|
|
736
|
-
const { privateKey } = await generateSigningKeypair();
|
|
737
|
-
spinner.stop("Signing keypair generated");
|
|
738
|
-
return privateKey;
|
|
739
|
-
});
|
|
740
|
-
signingKeyPublic = await derivePublicKey(signingKey);
|
|
741
|
-
jwtSecret = await getOrGenerateSecret("JWT_SECRET", devVars, async () => {
|
|
742
|
-
spinner.start("Generating JWT secret...");
|
|
743
|
-
const secret = generateJwtSecret();
|
|
744
|
-
spinner.stop("JWT secret generated");
|
|
745
|
-
return secret;
|
|
746
|
-
});
|
|
747
|
-
passwordHash = await getOrGenerateSecret("PASSWORD_HASH", devVars, async () => {
|
|
748
|
-
const password = await promptPassword(handle);
|
|
749
|
-
spinner.start("Hashing password...");
|
|
750
|
-
const hash = await hashPassword(password);
|
|
751
|
-
spinner.stop("Password hashed");
|
|
752
|
-
return hash;
|
|
753
|
-
});
|
|
754
|
-
} else {
|
|
755
|
-
const password = await promptPassword(handle);
|
|
756
|
-
spinner.start("Hashing password...");
|
|
757
|
-
passwordHash = await hashPassword(password);
|
|
758
|
-
spinner.stop("Password hashed");
|
|
759
|
-
spinner.start("Generating JWT secret...");
|
|
760
|
-
jwtSecret = generateJwtSecret();
|
|
761
|
-
spinner.stop("JWT secret generated");
|
|
728
|
+
const authToken = await getOrGenerateSecret("AUTH_TOKEN", devVars, async () => {
|
|
762
729
|
spinner.start("Generating auth token...");
|
|
763
|
-
|
|
730
|
+
const token = generateAuthToken();
|
|
764
731
|
spinner.stop("Auth token generated");
|
|
732
|
+
return token;
|
|
733
|
+
});
|
|
734
|
+
const signingKey = await getOrGenerateSecret("SIGNING_KEY", devVars, async () => {
|
|
765
735
|
spinner.start("Generating signing keypair...");
|
|
766
|
-
const
|
|
767
|
-
signingKey = keypair.privateKey;
|
|
768
|
-
signingKeyPublic = keypair.publicKey;
|
|
736
|
+
const { privateKey } = await generateSigningKeypair();
|
|
769
737
|
spinner.stop("Signing keypair generated");
|
|
770
|
-
|
|
738
|
+
return privateKey;
|
|
739
|
+
});
|
|
740
|
+
const signingKeyPublic = await derivePublicKey(signingKey);
|
|
741
|
+
const jwtSecret = await getOrGenerateSecret("JWT_SECRET", devVars, async () => {
|
|
742
|
+
spinner.start("Generating JWT secret...");
|
|
743
|
+
const secret = generateJwtSecret();
|
|
744
|
+
spinner.stop("JWT secret generated");
|
|
745
|
+
return secret;
|
|
746
|
+
});
|
|
747
|
+
const passwordHash = await getOrGenerateSecret("PASSWORD_HASH", devVars, async () => {
|
|
748
|
+
const password = await promptPassword(handle);
|
|
749
|
+
spinner.start("Hashing password...");
|
|
750
|
+
const hash = await hashPassword(password);
|
|
751
|
+
spinner.stop("Password hashed");
|
|
752
|
+
return hash;
|
|
753
|
+
});
|
|
771
754
|
spinner.start("Updating wrangler.jsonc...");
|
|
772
755
|
setWorkerName(workerName);
|
|
773
756
|
setVars({
|
|
@@ -844,15 +827,10 @@ const initCommand = defineCommand({
|
|
|
844
827
|
*/
|
|
845
828
|
async function getOrGenerateSecret(name, devVars, generate) {
|
|
846
829
|
if (devVars[name]) {
|
|
847
|
-
|
|
830
|
+
if (await p.confirm({
|
|
848
831
|
message: `Use ${name} from .dev.vars?`,
|
|
849
832
|
initialValue: true
|
|
850
|
-
});
|
|
851
|
-
if (p.isCancel(useExisting)) {
|
|
852
|
-
p.cancel("Cancelled");
|
|
853
|
-
process.exit(0);
|
|
854
|
-
}
|
|
855
|
-
if (useExisting) return devVars[name];
|
|
833
|
+
}) === true) return devVars[name];
|
|
856
834
|
}
|
|
857
835
|
return generate();
|
|
858
836
|
}
|
|
@@ -981,6 +959,21 @@ var PDSClient = class {
|
|
|
981
959
|
return this.xrpc("GET", "com.atproto.sync.listBlobs", { params });
|
|
982
960
|
}
|
|
983
961
|
/**
|
|
962
|
+
* Get user preferences
|
|
963
|
+
*/
|
|
964
|
+
async getPreferences() {
|
|
965
|
+
return (await this.xrpc("GET", "app.bsky.actor.getPreferences", { auth: true })).preferences;
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* Update user preferences
|
|
969
|
+
*/
|
|
970
|
+
async putPreferences(preferences) {
|
|
971
|
+
await this.xrpc("POST", "app.bsky.actor.putPreferences", {
|
|
972
|
+
body: { preferences },
|
|
973
|
+
auth: true
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
984
977
|
* Get account status including migration progress
|
|
985
978
|
*/
|
|
986
979
|
async getAccountStatus() {
|
|
@@ -1048,31 +1041,6 @@ var PDSClient = class {
|
|
|
1048
1041
|
}
|
|
1049
1042
|
};
|
|
1050
1043
|
|
|
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
1044
|
//#endregion
|
|
1077
1045
|
//#region src/cli/commands/migrate.ts
|
|
1078
1046
|
function detectPackageManager() {
|
|
@@ -1083,11 +1051,10 @@ function detectPackageManager() {
|
|
|
1083
1051
|
return "npm";
|
|
1084
1052
|
}
|
|
1085
1053
|
const brightNote$1 = (lines) => lines.map((l) => `\x1b[0m${l}`).join("\n");
|
|
1086
|
-
const bold$1 = (text) => pc.bold(text);
|
|
1087
1054
|
/**
|
|
1088
1055
|
* Format number with commas
|
|
1089
1056
|
*/
|
|
1090
|
-
function
|
|
1057
|
+
function num(n) {
|
|
1091
1058
|
return n.toLocaleString();
|
|
1092
1059
|
}
|
|
1093
1060
|
/**
|
|
@@ -1116,7 +1083,8 @@ const migrateCommand = defineCommand({
|
|
|
1116
1083
|
}
|
|
1117
1084
|
},
|
|
1118
1085
|
async run({ args }) {
|
|
1119
|
-
const
|
|
1086
|
+
const packageManager = detectPackageManager();
|
|
1087
|
+
const pm = packageManager === "npm" ? "npm run" : packageManager;
|
|
1120
1088
|
const isDev = args.dev;
|
|
1121
1089
|
const vars = getVars();
|
|
1122
1090
|
let targetUrl;
|
|
@@ -1183,6 +1151,7 @@ const migrateCommand = defineCommand({
|
|
|
1183
1151
|
const sourceDomain = getDomain(sourcePdsUrl);
|
|
1184
1152
|
spinner.stop(`Found your account at ${sourceDomain}`);
|
|
1185
1153
|
spinner.start("Checking account status...");
|
|
1154
|
+
const pdsDisplayName = sourceDomain.endsWith(".bsky.network") ? "bsky.social" : sourceDomain;
|
|
1186
1155
|
let status;
|
|
1187
1156
|
try {
|
|
1188
1157
|
status = await targetClient.getAccountStatus();
|
|
@@ -1197,7 +1166,7 @@ const migrateCommand = defineCommand({
|
|
|
1197
1166
|
if (status.active) {
|
|
1198
1167
|
p.log.error("Cannot reset: account is active");
|
|
1199
1168
|
p.log.info("The --clean flag only works on deactivated accounts.");
|
|
1200
|
-
p.log.info("Your account is already live
|
|
1169
|
+
p.log.info("Your account is already live");
|
|
1201
1170
|
p.log.info("");
|
|
1202
1171
|
p.log.info("If you need to re-import, first deactivate:");
|
|
1203
1172
|
p.log.info(" pnpm pds deactivate");
|
|
@@ -1205,13 +1174,13 @@ const migrateCommand = defineCommand({
|
|
|
1205
1174
|
process.exit(1);
|
|
1206
1175
|
}
|
|
1207
1176
|
p.note(brightNote$1([
|
|
1208
|
-
bold
|
|
1177
|
+
pc.bold("This will permanently delete from your new PDS:"),
|
|
1209
1178
|
"",
|
|
1210
|
-
` • ${
|
|
1211
|
-
` • ${
|
|
1179
|
+
` • ${num(status.repoBlocks)} repository blocks`,
|
|
1180
|
+
` • ${num(status.importedBlobs)} imported images`,
|
|
1212
1181
|
" • All blob tracking data",
|
|
1213
1182
|
"",
|
|
1214
|
-
bold
|
|
1183
|
+
pc.bold(`Your data on ${pdsDisplayName} is NOT affected.`),
|
|
1215
1184
|
"You'll need to re-import everything."
|
|
1216
1185
|
]), "⚠️ Reset Migration Data");
|
|
1217
1186
|
const confirmReset = await p.confirm({
|
|
@@ -1225,7 +1194,7 @@ const migrateCommand = defineCommand({
|
|
|
1225
1194
|
spinner.start("Resetting migration state...");
|
|
1226
1195
|
try {
|
|
1227
1196
|
const result = await targetClient.resetMigration();
|
|
1228
|
-
spinner.stop(`Deleted ${
|
|
1197
|
+
spinner.stop(`Deleted ${num(result.blocksDeleted)} blocks, ${num(result.blobsCleared)} blobs`);
|
|
1229
1198
|
} catch (err) {
|
|
1230
1199
|
spinner.stop("Reset failed");
|
|
1231
1200
|
p.log.error(err instanceof Error ? err.message : "Could not reset migration");
|
|
@@ -1236,12 +1205,12 @@ const migrateCommand = defineCommand({
|
|
|
1236
1205
|
status = await targetClient.getAccountStatus();
|
|
1237
1206
|
}
|
|
1238
1207
|
if (status.active) {
|
|
1239
|
-
p.log.warn(
|
|
1208
|
+
p.log.warn(`Your account is already active at ${targetDomain}!`);
|
|
1240
1209
|
p.log.info("No migration needed - your PDS is live.");
|
|
1241
|
-
p.outro("All good!
|
|
1210
|
+
p.outro("All good!");
|
|
1242
1211
|
return;
|
|
1243
1212
|
}
|
|
1244
|
-
spinner.start(`Fetching your account details from ${
|
|
1213
|
+
spinner.start(`Fetching your account details from ${pdsDisplayName}...`);
|
|
1245
1214
|
const sourceClient = new PDSClient(sourcePdsUrl);
|
|
1246
1215
|
try {
|
|
1247
1216
|
await sourceClient.describeRepo(did);
|
|
@@ -1262,10 +1231,10 @@ const migrateCommand = defineCommand({
|
|
|
1262
1231
|
`@${handle} (${did.slice(0, 20)}...)`,
|
|
1263
1232
|
"",
|
|
1264
1233
|
"✓ Repository imported",
|
|
1265
|
-
`◐
|
|
1234
|
+
`◐ Media: ${num(status.importedBlobs)}/${num(status.expectedBlobs)} images and videos transferred`
|
|
1266
1235
|
].join("\n"), "Migration Progress");
|
|
1267
1236
|
const continueTransfer = await p.confirm({
|
|
1268
|
-
message: "Continue transferring images?",
|
|
1237
|
+
message: "Continue transferring images and video?",
|
|
1269
1238
|
initialValue: true
|
|
1270
1239
|
});
|
|
1271
1240
|
if (p.isCancel(continueTransfer) || !continueTransfer) {
|
|
@@ -1274,14 +1243,14 @@ const migrateCommand = defineCommand({
|
|
|
1274
1243
|
}
|
|
1275
1244
|
} else if (needsRepoImport) {
|
|
1276
1245
|
p.log.info("Time to pack your bags!");
|
|
1277
|
-
p.log.info("Let's
|
|
1246
|
+
p.log.info("Let's clone your account to its new home in the Atmosphere.");
|
|
1278
1247
|
const statsLines = profileStats ? [
|
|
1279
|
-
` 📝 ${
|
|
1280
|
-
` 👥 ${
|
|
1281
|
-
` ...plus all your images, likes
|
|
1282
|
-
] : [` 📝 Posts, follows, images, likes
|
|
1248
|
+
` 📝 ${num(profileStats.postsCount)} posts`,
|
|
1249
|
+
` 👥 ${num(profileStats.followsCount)} follows`,
|
|
1250
|
+
` ...plus all your images, likes and preferences`
|
|
1251
|
+
] : [` 📝 Posts, follows, images, likes and preferences`];
|
|
1283
1252
|
p.note(brightNote$1([
|
|
1284
|
-
bold
|
|
1253
|
+
pc.bold(`@${handle}`) + ` (${did.slice(0, 20)}...)`,
|
|
1285
1254
|
"",
|
|
1286
1255
|
`Currently at: ${sourceDomain}`,
|
|
1287
1256
|
`Moving to: ${targetDomain}`,
|
|
@@ -1289,9 +1258,9 @@ const migrateCommand = defineCommand({
|
|
|
1289
1258
|
"What you're bringing:",
|
|
1290
1259
|
...statsLines
|
|
1291
1260
|
]), "Your Bluesky Account 🦋");
|
|
1292
|
-
p.log.info(
|
|
1261
|
+
p.log.info(`This will copy your data - nothing is changed or deleted on your current PDS.`);
|
|
1293
1262
|
const proceed = await p.confirm({
|
|
1294
|
-
message: "Ready to start
|
|
1263
|
+
message: "Ready to start moving?",
|
|
1295
1264
|
initialValue: true
|
|
1296
1265
|
});
|
|
1297
1266
|
if (p.isCancel(proceed) || !proceed) {
|
|
@@ -1299,19 +1268,17 @@ const migrateCommand = defineCommand({
|
|
|
1299
1268
|
process.exit(0);
|
|
1300
1269
|
}
|
|
1301
1270
|
} else {
|
|
1302
|
-
p.log.success("
|
|
1303
|
-
showNextSteps(pm,
|
|
1271
|
+
p.log.success("Everything looks good!");
|
|
1272
|
+
showNextSteps(pm, pdsDisplayName);
|
|
1304
1273
|
p.outro("Welcome to your new home in the Atmosphere! 🦋");
|
|
1305
1274
|
return;
|
|
1306
1275
|
}
|
|
1307
|
-
const
|
|
1308
|
-
const passwordPrompt = isBlueskyPds ? "Your current Bluesky password:" : `Your ${sourceDomain} password:`;
|
|
1309
|
-
const password = await p.password({ message: passwordPrompt });
|
|
1276
|
+
const password = await p.password({ message: `Your password for ${pdsDisplayName}:` });
|
|
1310
1277
|
if (p.isCancel(password)) {
|
|
1311
1278
|
p.cancel("Migration cancelled.");
|
|
1312
1279
|
process.exit(0);
|
|
1313
1280
|
}
|
|
1314
|
-
spinner.start(`Logging in to ${
|
|
1281
|
+
spinner.start(`Logging in to ${pdsDisplayName}...`);
|
|
1315
1282
|
try {
|
|
1316
1283
|
const session = await sourceClient.createSession(did, password);
|
|
1317
1284
|
sourceClient.setAuthToken(session.accessJwt);
|
|
@@ -1324,7 +1291,7 @@ const migrateCommand = defineCommand({
|
|
|
1324
1291
|
process.exit(1);
|
|
1325
1292
|
}
|
|
1326
1293
|
if (needsRepoImport) {
|
|
1327
|
-
spinner.start(
|
|
1294
|
+
spinner.start(`Exporting your repository from ${pdsDisplayName}...`);
|
|
1328
1295
|
let carBytes;
|
|
1329
1296
|
try {
|
|
1330
1297
|
carBytes = await sourceClient.getRepo(did);
|
|
@@ -1335,7 +1302,7 @@ const migrateCommand = defineCommand({
|
|
|
1335
1302
|
p.outro("Migration cancelled.");
|
|
1336
1303
|
process.exit(1);
|
|
1337
1304
|
}
|
|
1338
|
-
spinner.start(`
|
|
1305
|
+
spinner.start(`Importing to ${targetDomain}...`);
|
|
1339
1306
|
try {
|
|
1340
1307
|
await targetClient.importRepo(carBytes);
|
|
1341
1308
|
spinner.stop("Repository imported");
|
|
@@ -1347,6 +1314,16 @@ const migrateCommand = defineCommand({
|
|
|
1347
1314
|
}
|
|
1348
1315
|
status = await targetClient.getAccountStatus();
|
|
1349
1316
|
}
|
|
1317
|
+
spinner.start("Migrating your preferences...");
|
|
1318
|
+
try {
|
|
1319
|
+
const preferences = await sourceClient.getPreferences();
|
|
1320
|
+
if (preferences.length > 0) {
|
|
1321
|
+
await targetClient.putPreferences(preferences);
|
|
1322
|
+
spinner.stop(`Migrated ${preferences.length} preference${preferences.length === 1 ? "" : "s"}`);
|
|
1323
|
+
} else spinner.stop("No preferences to migrate");
|
|
1324
|
+
} catch (err) {
|
|
1325
|
+
spinner.stop("Skipped preferences (not available)");
|
|
1326
|
+
}
|
|
1350
1327
|
if (status.expectedBlobs - status.importedBlobs > 0) {
|
|
1351
1328
|
let synced = 0;
|
|
1352
1329
|
let totalBlobs = 0;
|
|
@@ -1366,7 +1343,7 @@ const migrateCommand = defineCommand({
|
|
|
1366
1343
|
totalBlobs += page.blobs.length;
|
|
1367
1344
|
countCursor = page.cursor;
|
|
1368
1345
|
} while (countCursor);
|
|
1369
|
-
spinner.message(`Transferring
|
|
1346
|
+
spinner.message(`Transferring media ${progressBar(0, totalBlobs)}`);
|
|
1370
1347
|
do {
|
|
1371
1348
|
const page = await targetClient.listMissingBlobs(100, cursor);
|
|
1372
1349
|
cursor = page.cursor;
|
|
@@ -1374,40 +1351,40 @@ const migrateCommand = defineCommand({
|
|
|
1374
1351
|
const { bytes, mimeType } = await sourceClient.getBlob(did, blob.cid);
|
|
1375
1352
|
await targetClient.uploadBlob(bytes, mimeType);
|
|
1376
1353
|
synced++;
|
|
1377
|
-
spinner.message(`Transferring
|
|
1354
|
+
spinner.message(`Transferring media ${progressBar(synced, totalBlobs)}`);
|
|
1378
1355
|
} catch (err) {
|
|
1379
1356
|
synced++;
|
|
1380
1357
|
failedBlobs.push(blob.cid);
|
|
1381
|
-
spinner.message(`Transferring
|
|
1358
|
+
spinner.message(`Transferring media ${progressBar(synced, totalBlobs)}`);
|
|
1382
1359
|
}
|
|
1383
1360
|
} while (cursor);
|
|
1384
1361
|
if (failedBlobs.length > 0) {
|
|
1385
|
-
spinner.stop(`Transferred ${
|
|
1362
|
+
spinner.stop(`Transferred ${num(synced - failedBlobs.length)} images and videos (${failedBlobs.length} failed)`);
|
|
1386
1363
|
p.log.warn(`Run 'pds migrate' again to retry failed transfers.`);
|
|
1387
|
-
} else spinner.stop(`Transferred ${
|
|
1364
|
+
} else spinner.stop(`Transferred ${num(synced)} images and videos`);
|
|
1388
1365
|
}
|
|
1389
1366
|
spinner.start("Verifying migration...");
|
|
1390
1367
|
const finalStatus = await targetClient.getAccountStatus();
|
|
1391
1368
|
spinner.stop("Verification complete");
|
|
1392
|
-
if (finalStatus.importedBlobs >= finalStatus.expectedBlobs) p.log.success("All packed and moved!
|
|
1369
|
+
if (finalStatus.importedBlobs >= finalStatus.expectedBlobs) p.log.success("All packed and moved!");
|
|
1393
1370
|
else {
|
|
1394
1371
|
p.log.warn(`Migration partially complete. ${finalStatus.expectedBlobs - finalStatus.importedBlobs} images remaining.`);
|
|
1395
1372
|
p.log.info("Run 'pds migrate' again to continue.");
|
|
1396
1373
|
}
|
|
1397
|
-
showNextSteps(pm,
|
|
1374
|
+
showNextSteps(pm, pdsDisplayName);
|
|
1398
1375
|
p.outro("Welcome to your new home in the Atmosphere! 🦋");
|
|
1399
1376
|
}
|
|
1400
1377
|
});
|
|
1401
1378
|
function showNextSteps(pm, sourceDomain) {
|
|
1402
1379
|
p.note(brightNote$1([
|
|
1403
|
-
bold
|
|
1404
|
-
"Two more steps to go live
|
|
1380
|
+
pc.bold("Your data is safe in your new PDS."),
|
|
1381
|
+
"Two more steps to go live:",
|
|
1405
1382
|
"",
|
|
1406
|
-
bold
|
|
1383
|
+
pc.bold("1. Update your identity"),
|
|
1407
1384
|
" Tell the network where you live now.",
|
|
1408
1385
|
` (Requires email verification from ${sourceDomain})`,
|
|
1409
1386
|
"",
|
|
1410
|
-
bold
|
|
1387
|
+
pc.bold("2. Flip the switch"),
|
|
1411
1388
|
` ${pm} pds activate`,
|
|
1412
1389
|
"",
|
|
1413
1390
|
"Docs: https://atproto.com/guides/account-migration"
|