From 8f7035eefc713b235438c615bf07a64aee423160 Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Thu, 1 Jan 2026 21:25:04 +0000 Subject: [PATCH] fix: improve CLI logic and copy --- .changeset/pink-ducks-judge.md | 6 + demos/pds/.env.example | 26 -- packages/create-pds/src/index.ts | 13 +- .../templates/pds-worker/env.example | 31 +- .../templates/pds-worker/wrangler.jsonc | 4 +- packages/pds/src/cli/commands/init.ts | 265 +++++++----------- packages/pds/src/cli/commands/migrate.ts | 68 +++-- packages/pds/src/cli/utils/cli-helpers.ts | 47 +++- 8 files changed, 200 insertions(+), 260 deletions(-) create mode 100644 .changeset/pink-ducks-judge.md delete mode 100644 demos/pds/.env.example diff --git a/.changeset/pink-ducks-judge.md b/.changeset/pink-ducks-judge.md new file mode 100644 index 00000000..2107018e --- /dev/null +++ b/.changeset/pink-ducks-judge.md @@ -0,0 +1,6 @@ +--- +"create-pds": patch +"@ascorbic/pds": patch +--- + +Improvements to CLI prompts and logic diff --git a/demos/pds/.env.example b/demos/pds/.env.example deleted file mode 100644 index a488f29f..00000000 --- a/demos/pds/.env.example +++ /dev/null @@ -1,26 +0,0 @@ -# Example environment variables for local development -# Copy this to .dev.vars and fill in your values - -# Public hostname of your PDS -PDS_HOSTNAME=demo-pds.example.com - -# Your account's DID (usually did:web:{PDS_HOSTNAME}) -DID=did:web:demo-pds.example.com - -# Your account's handle -HANDLE=alice.demo-pds.example.com - -# Bearer token for write operations (generate a random string) -AUTH_TOKEN=your-secret-token-here - -# Private key for signing commits (secp256k1 JWK) -SIGNING_KEY={"kty":"EC","crv":"secp256k1","x":"...","d":"..."} - -# Public key for DID document (multibase encoded) -SIGNING_KEY_PUBLIC=zQ3sh... - -# Secret for signing session JWTs (generate a random string) -JWT_SECRET=your-jwt-secret-here - -# Optional: Bcrypt hash of account password (for app login) -# PASSWORD_HASH=$2a$10$... diff --git a/packages/create-pds/src/index.ts b/packages/create-pds/src/index.ts index b59e35c8..e8d794d5 100644 --- a/packages/create-pds/src/index.ts +++ b/packages/create-pds/src/index.ts @@ -106,11 +106,12 @@ async function getLatestPdsVersion(): Promise { throw new Error(`Failed to fetch: ${response.status}`); } const data = (await response.json()) as { version: string }; - return `^${data.version}`; - } catch { - // Fallback to a known version if fetch fails - return "^0.2.0"; - } + if (data.version) { + return data.version; + } + } catch {} + // Fallback to a known version if fetch fails + return "^0.2.0"; } const main = defineCommand({ @@ -253,7 +254,7 @@ const main = defineCommand({ // Replace placeholders in package.json await replaceInFile(join(targetDir, "package.json"), { name: projectName, - pdsVersion, + pdsVersion: `^${pdsVersion}`, }); spinner.stop("Template copied"); diff --git a/packages/create-pds/templates/pds-worker/env.example b/packages/create-pds/templates/pds-worker/env.example index 5ca950f6..2328035c 100644 --- a/packages/create-pds/templates/pds-worker/env.example +++ b/packages/create-pds/templates/pds-worker/env.example @@ -1,27 +1,14 @@ -# Example environment variables for local development -# Copy this to .dev.vars and fill in your values -# Or run `pnpm pds init --local` to generate automatically +# Environment variables for local development +# Run `pnpm pds init` to generate this file automatically -# Public hostname of your PDS -PDS_HOSTNAME=demo-pds.example.com - -# Your account's DID (usually did:web:{PDS_HOSTNAME}) -DID=did:web:demo-pds.example.com - -# Your account's handle -HANDLE=alice.demo-pds.example.com - -# Bearer token for write operations (generate a random string) -AUTH_TOKEN=your-secret-token-here +# Bearer token for write operations +AUTH_TOKEN= # Private key for signing commits (secp256k1 JWK) -SIGNING_KEY={"kty":"EC","crv":"secp256k1","x":"...","d":"..."} - -# Public key for DID document (multibase encoded) -SIGNING_KEY_PUBLIC=zQ3sh... +SIGNING_KEY= -# Secret for signing session JWTs (generate a random string) -JWT_SECRET=your-jwt-secret-here +# Secret for signing session JWTs +JWT_SECRET= -# Optional: Bcrypt hash of account password (for app login) -# PASSWORD_HASH=$2a$10$... +# Bcrypt hash of account password (for app login) +PASSWORD_HASH= diff --git a/packages/create-pds/templates/pds-worker/wrangler.jsonc b/packages/create-pds/templates/pds-worker/wrangler.jsonc index f3411844..730b563a 100644 --- a/packages/create-pds/templates/pds-worker/wrangler.jsonc +++ b/packages/create-pds/templates/pds-worker/wrangler.jsonc @@ -1,7 +1,7 @@ { // AT Protocol PDS on Cloudflare Workers // Run `pnpm pds init` to configure, or `pnpm pds init --production` to deploy secrets - "name": "atproto-pds", + "name": "my-pds", "main": "src/index.ts", "compatibility_date": "2025-12-02", "compatibility_flags": [ @@ -47,4 +47,4 @@ // - SIGNING_KEY: Private signing key (secp256k1 JWK) // - JWT_SECRET: Secret for signing session JWTs // - PASSWORD_HASH: Bcrypt hash of account password (for Bluesky app login) -} +} \ No newline at end of file diff --git a/packages/pds/src/cli/commands/init.ts b/packages/pds/src/cli/commands/init.ts index 50e0605b..38f9fa1a 100644 --- a/packages/pds/src/cli/commands/init.ts +++ b/packages/pds/src/cli/commands/init.ts @@ -11,6 +11,11 @@ import { setWorkerName, type SecretName, } from "../utils/wrangler.js"; +import { + promptText, + promptConfirm, + promptSelect, +} from "../utils/cli-helpers.js"; /** * Slugify a handle to create a worker name @@ -24,6 +29,34 @@ function slugifyHandle(handle: string): string { .replace(/^-|-$/g, "") + "-pds" ); } + +const defaultWorkerName = "my-pds"; + +/** + * Prompt for worker name with validation + */ +async function promptWorkerName( + handle: string, + currentWorkerName: string | undefined, +): Promise { + // Use current worker name if it exists and is not the default + const placeholder = + currentWorkerName && currentWorkerName !== defaultWorkerName + ? currentWorkerName + : // Otherwise, generate from handle + slugifyHandle(handle); + return promptText({ + message: "Cloudflare Worker name:", + placeholder, + initialValue: placeholder, + validate: (v) => { + if (!v) return "Worker name is required"; + if (!/^[a-z0-9-]+$/.test(v)) + return "Worker name can only contain lowercase letters, numbers, and hyphens"; + return undefined; + }, + }); +} import { readDevVars } from "../utils/dotenv.js"; import { generateSigningKeypair, @@ -77,8 +110,7 @@ export const initCommand = defineCommand({ args: { production: { type: "boolean", - description: - "Deploy secrets to Cloudflare (prompts to reuse .dev.vars values)", + description: "Deploy secrets to Cloudflare?", default: false, }, }, @@ -87,10 +119,9 @@ export const initCommand = defineCommand({ const isProduction = args.production; if (isProduction) { - p.log.info("Production mode: secrets will be deployed via wrangler"); - } else { - p.log.info("Let's set up your new home in the Atmosphere!"); + p.log.info("Production mode: secrets will be deployed to Cloudflare"); } + p.log.info("Let's set up your new home in the Atmosphere!"); // Get current config from both sources const wranglerVars = getVars(); @@ -100,14 +131,10 @@ export const initCommand = defineCommand({ const currentVars = { ...devVars, ...wranglerVars }; // Ask if migrating an existing account - const isMigrating = await p.confirm({ - message: "Are you migrating an existing Bluesky account? 🦋", + const isMigrating = await promptConfirm({ + message: "Are you migrating an existing Bluesky/ATProto account?", initialValue: false, }); - if (p.isCancel(isMigrating)) { - p.cancel("Setup cancelled"); - process.exit(0); - } let did: string; let handle: string; @@ -120,13 +147,13 @@ export const initCommand = defineCommand({ if (isMigrating) { p.log.info("Time to pack your bags! 🧳"); p.log.info( - "Your account will be inactive until you've moved your data over.", + "Your new account will be inactive until you've ready to go live.", ); // Fallback hosted domains - will be updated from source PDS if possible let hostedDomains = [".bsky.social", ".bsky.network", ".bsky.team"]; - const isHostedHandle = (h: string) => - hostedDomains.some((domain) => h.endsWith(domain)); + const isHostedHandle = (h: string | null) => + hostedDomains.some((domain) => h?.endsWith(domain)); // Loop to allow retry on failed handle resolution (max 3 attempts) let resolvedDid: string | null = null; @@ -137,16 +164,12 @@ export const initCommand = defineCommand({ while (!resolvedDid && attempts < MAX_ATTEMPTS) { attempts++; // Get current handle to look up DID - const currentHandle = await p.text({ + const currentHandle = await promptText({ message: "Your current Bluesky/ATProto handle:", placeholder: "example.bsky.social", validate: (v) => (!v ? "Handle is required" : undefined), }); - if (p.isCancel(currentHandle)) { - p.cancel("Cancelled"); - process.exit(0); - } - existingHandle = currentHandle as string; + existingHandle = currentHandle; // Resolve handle to DID const spinner = p.spinner(); @@ -157,20 +180,16 @@ export const initCommand = defineCommand({ spinner.stop("Not found"); p.log.error(`Failed to resolve handle "${currentHandle}"`); - const action = await p.select({ + const action = await promptSelect({ message: "What would you like to do?", options: [ - { value: "retry", label: "Try a different handle" }, - { value: "manual", label: "Enter DID manually" }, + { value: "retry" as const, label: "Try a different handle" }, + { value: "manual" as const, label: "Enter DID manually" }, ], }); - if (p.isCancel(action)) { - p.cancel("Cancelled"); - process.exit(0); - } if (action === "manual") { - const manualDid = await p.text({ + resolvedDid = await promptText({ message: "Enter your DID:", placeholder: "did:plc:...", validate: (v) => { @@ -179,11 +198,6 @@ export const initCommand = defineCommand({ return undefined; }, }); - if (p.isCancel(manualDid)) { - p.cancel("Cancelled"); - process.exit(0); - } - resolvedDid = manualDid as string; } // If action === "retry", loop continues with fresh handle prompt } else { @@ -215,10 +229,10 @@ export const initCommand = defineCommand({ // Ignore errors, use fallback domains } spinner.stop(`Found you! ${resolvedDid}`); - if (isHostedHandle(existingHandle!)) { + if (isHostedHandle(existingHandle)) { // Show the actual hosted domain they're on const theirDomain = hostedDomains.find((d) => - existingHandle!.endsWith(d), + existingHandle?.endsWith(d), ); const domainExample = theirDomain ? `*${theirDomain}` @@ -247,7 +261,7 @@ export const initCommand = defineCommand({ ? existingHandle : currentVars.HANDLE || ""; - handle = (await p.text({ + handle = await promptText({ message: "New account handle (must be a domain you control):", placeholder: "example.com", initialValue: defaultHandle, @@ -258,41 +272,17 @@ export const initCommand = defineCommand({ } return undefined; }, - })) as string; - if (p.isCancel(handle)) { - p.cancel("Cancelled"); - process.exit(0); - } + }); // Prompt for PDS hostname - default to handle if it looks like a good PDS domain - hostname = (await p.text({ + hostname = await promptText({ message: "Domain where you'll deploy your PDS:", placeholder: handle, initialValue: currentVars.PDS_HOSTNAME || handle, validate: (v) => (!v ? "Hostname is required" : undefined), - })) as string; - if (p.isCancel(hostname)) { - p.cancel("Cancelled"); - process.exit(0); - } + }); - // Prompt for worker name - const defaultWorkerName = currentWorkerName || slugifyHandle(handle); - workerName = (await p.text({ - message: "Cloudflare Worker name:", - placeholder: defaultWorkerName, - initialValue: defaultWorkerName, - validate: (v) => { - if (!v) return "Worker name is required"; - if (!/^[a-z0-9-]+$/.test(v)) - return "Worker name can only contain lowercase letters, numbers, and hyphens"; - return undefined; - }, - })) as string; - if (p.isCancel(workerName)) { - p.cancel("Cancelled"); - process.exit(0); - } + workerName = await promptWorkerName(handle, currentWorkerName); // Set to deactivated initially for migration initialActive = "false"; @@ -301,63 +291,37 @@ export const initCommand = defineCommand({ p.log.info("A fresh start in the Atmosphere! ✨"); // Prompt for hostname - hostname = (await p.text({ + hostname = await promptText({ message: "Domain where you'll deploy your PDS:", placeholder: "pds.example.com", initialValue: currentVars.PDS_HOSTNAME || "", validate: (v) => (!v ? "Hostname is required" : undefined), - })) as string; - if (p.isCancel(hostname)) { - p.cancel("Cancelled"); - process.exit(0); - } + }); // Prompt for handle - default to hostname for simplicity - handle = (await p.text({ + handle = await promptText({ message: "Account handle:", placeholder: hostname, initialValue: currentVars.HANDLE || hostname, validate: (v) => (!v ? "Handle is required" : undefined), - })) as string; - if (p.isCancel(handle)) { - p.cancel("Cancelled"); - process.exit(0); - } + }); // Prompt for DID const didDefault = "did:web:" + hostname; - did = (await p.text({ + did = await promptText({ message: "Account DID:", placeholder: didDefault, initialValue: currentVars.DID || didDefault, validate: (v) => { - if (!v) return "DID is required"; - if (!v.startsWith("did:")) return "DID must start with did:"; + if (!v) { + return "DID is required"; + } + if (!v.startsWith("did:")) return "DID must start with 'did:'"; return undefined; }, - })) as string; - if (p.isCancel(did)) { - p.cancel("Cancelled"); - process.exit(0); - } + }); - // Prompt for worker name - const defaultWorkerName = currentWorkerName || slugifyHandle(handle); - workerName = (await p.text({ - message: "Cloudflare Worker name:", - placeholder: defaultWorkerName, - initialValue: defaultWorkerName, - validate: (v) => { - if (!v) return "Worker name is required"; - if (!/^[a-z0-9-]+$/.test(v)) - return "Worker name can only contain lowercase letters, numbers, and hyphens"; - return undefined; - }, - })) as string; - if (p.isCancel(workerName)) { - p.cancel("Cancelled"); - process.exit(0); - } + workerName = await promptWorkerName(handle, currentWorkerName); // Active by default for new accounts initialActive = "true"; @@ -399,77 +363,52 @@ export const initCommand = defineCommand({ const spinner = p.spinner(); - // In production mode, we may reuse secrets from .dev.vars - // Otherwise, we always generate fresh values - let authToken: string; - let signingKey: string; - let signingKeyPublic: string; - let jwtSecret: string; - let passwordHash: string; - - if (isProduction) { - // For each secret, ask if we should reuse from .dev.vars - authToken = await getOrGenerateSecret("AUTH_TOKEN", devVars, async () => { + const authToken = await getOrGenerateSecret( + "AUTH_TOKEN", + devVars, + async () => { spinner.start("Generating auth token..."); const token = generateAuthToken(); spinner.stop("Auth token generated"); return token; - }); + }, + ); - signingKey = await getOrGenerateSecret( - "SIGNING_KEY", - devVars, - async () => { - spinner.start("Generating signing keypair..."); - const { privateKey } = await generateSigningKeypair(); - spinner.stop("Signing keypair generated"); - return privateKey; - }, - ); + const signingKey = await getOrGenerateSecret( + "SIGNING_KEY", + devVars, + async () => { + spinner.start("Generating signing keypair..."); + const { privateKey } = await generateSigningKeypair(); + spinner.stop("Signing keypair generated"); + return privateKey; + }, + ); - // Derive public key from the signing key we're using - signingKeyPublic = await derivePublicKey(signingKey); + const signingKeyPublic = await derivePublicKey(signingKey); - jwtSecret = await getOrGenerateSecret("JWT_SECRET", devVars, async () => { + const jwtSecret = await getOrGenerateSecret( + "JWT_SECRET", + devVars, + async () => { spinner.start("Generating JWT secret..."); const secret = generateJwtSecret(); spinner.stop("JWT secret generated"); return secret; - }); + }, + ); - passwordHash = await getOrGenerateSecret( - "PASSWORD_HASH", - devVars, - async () => { - const password = await promptPassword(handle); - spinner.start("Hashing password..."); - const hash = await hashPassword(password); - spinner.stop("Password hashed"); - return hash; - }, - ); - } else { - // Local mode: always prompt for password and generate fresh secrets - const password = await promptPassword(handle); - - spinner.start("Hashing password..."); - passwordHash = await hashPassword(password); - spinner.stop("Password hashed"); - - spinner.start("Generating JWT secret..."); - jwtSecret = generateJwtSecret(); - spinner.stop("JWT secret generated"); - - spinner.start("Generating auth token..."); - authToken = generateAuthToken(); - spinner.stop("Auth token generated"); - - spinner.start("Generating signing keypair..."); - const keypair = await generateSigningKeypair(); - signingKey = keypair.privateKey; - signingKeyPublic = keypair.publicKey; - spinner.stop("Signing keypair generated"); - } + const passwordHash = await getOrGenerateSecret( + "PASSWORD_HASH", + devVars, + async () => { + const password = await promptPassword(handle); + spinner.start("Hashing password..."); + const hash = await hashPassword(password); + spinner.stop("Password hashed"); + return hash; + }, + ); // Always set public vars and worker name in wrangler.jsonc spinner.start("Updating wrangler.jsonc..."); @@ -589,11 +528,7 @@ async function getOrGenerateSecret( message: `Use ${name} from .dev.vars?`, initialValue: true, }); - if (p.isCancel(useExisting)) { - p.cancel("Cancelled"); - process.exit(0); - } - if (useExisting) { + if (useExisting === true) { return devVars[name]; } } diff --git a/packages/pds/src/cli/commands/migrate.ts b/packages/pds/src/cli/commands/migrate.ts index af1d0bcf..e372eb25 100644 --- a/packages/pds/src/cli/commands/migrate.ts +++ b/packages/pds/src/cli/commands/migrate.ts @@ -61,7 +61,8 @@ export const migrateCommand = defineCommand({ }, }, async run({ args }) { - const pm = detectPackageManager(); + const packageManager = detectPackageManager(); + const pm = packageManager === "npm" ? "npm run" : packageManager; const isDev = args.dev; // Get target URL @@ -147,6 +148,9 @@ export const migrateCommand = defineCommand({ spinner.start("Checking account status..."); + const isBlueskyPds = sourceDomain.endsWith(".bsky.network"); + const pdsDisplayName = isBlueskyPds ? "bsky.social" : sourceDomain; + let status; try { status = await targetClient.getAccountStatus(); @@ -165,7 +169,7 @@ export const migrateCommand = defineCommand({ if (status.active) { p.log.error("Cannot reset: account is active"); p.log.info("The --clean flag only works on deactivated accounts."); - p.log.info("Your account is already live in the Atmosphere."); + p.log.info("Your account is already live"); p.log.info(""); p.log.info("If you need to re-import, first deactivate:"); p.log.info(" pnpm pds deactivate"); @@ -182,7 +186,7 @@ export const migrateCommand = defineCommand({ ` • ${formatNumber(status.importedBlobs)} imported images`, " • All blob tracking data", "", - bold(`Your data on ${sourceDomain} is NOT affected.`), + bold(`Your data on ${pdsDisplayName} is NOT affected.`), "You'll need to re-import everything.", ]), "⚠️ Reset Migration Data", @@ -220,13 +224,13 @@ export const migrateCommand = defineCommand({ } if (status.active) { - p.log.warn("Your account is already active in the Atmosphere!"); + p.log.warn(`Your account is already active at ${targetDomain}!`); p.log.info("No migration needed - your PDS is live."); - p.outro("All good! 🦋"); + p.outro("All good!"); return; } - spinner.start(`Fetching your account details from ${sourceDomain}...`); + spinner.start(`Fetching your account details from ${pdsDisplayName}...`); const sourceClient = new PDSClient(sourcePdsUrl); try { @@ -266,13 +270,13 @@ export const migrateCommand = defineCommand({ `@${handle} (${did.slice(0, 20)}...)`, "", "✓ Repository imported", - `◐ Images: ${formatNumber(status.importedBlobs)}/${formatNumber(status.expectedBlobs)} transferred`, + `◐ Media: ${formatNumber(status.importedBlobs)}/${formatNumber(status.expectedBlobs)} images and videos transferred`, ].join("\n"), "Migration Progress", ); const continueTransfer = await p.confirm({ - message: "Continue transferring images?", + message: "Continue transferring images and video?", initialValue: true, }); @@ -283,17 +287,15 @@ export const migrateCommand = defineCommand({ } else if (needsRepoImport) { // Fresh migration p.log.info("Time to pack your bags!"); - p.log.info( - "Let's move your Bluesky account to its new home in the Atmosphere.", - ); + p.log.info("Let's clone your account to its new home in the Atmosphere."); const statsLines = profileStats ? [ ` 📝 ${formatNumber(profileStats.postsCount)} posts`, ` 👥 ${formatNumber(profileStats.followsCount)} follows`, - ` ...plus all your images, likes, and blocks`, + ` ...plus all your images, likes and preferences`, ] - : [` 📝 Posts, follows, images, likes, and blocks`]; + : [` 📝 Posts, follows, images, likes and preferences`]; p.note( brightNote([ @@ -309,11 +311,11 @@ export const migrateCommand = defineCommand({ ); p.log.info( - "This will copy your data - nothing is changed or deleted on Bluesky.", + `This will copy your data - nothing is changed or deleted on your current PDS.`, ); const proceed = await p.confirm({ - message: "Ready to start packing?", + message: "Ready to start moving?", initialValue: true, }); @@ -323,18 +325,14 @@ export const migrateCommand = defineCommand({ } } else { // Already complete - p.log.success("All packed and moved! 🦋"); - showNextSteps(pm, sourceDomain); + p.log.success("Everything looks good!"); + showNextSteps(pm, pdsDisplayName); p.outro("Welcome to your new home in the Atmosphere! 🦋"); return; } - const isBlueskyPds = sourceDomain.endsWith(".bsky.network"); - const passwordPrompt = isBlueskyPds - ? "Your current Bluesky password:" - : `Your ${sourceDomain} password:`; const password = await p.password({ - message: passwordPrompt, + message: `Your password for ${pdsDisplayName}:`, }); if (p.isCancel(password)) { @@ -342,9 +340,7 @@ export const migrateCommand = defineCommand({ process.exit(0); } - spinner.start( - `Logging in to ${isBlueskyPds ? "Bluesky" : sourceDomain}...`, - ); + spinner.start(`Logging in to ${pdsDisplayName}...`); try { const session = await sourceClient.createSession(did, password); sourceClient.setAuthToken(session.accessJwt); @@ -363,7 +359,7 @@ export const migrateCommand = defineCommand({ } if (needsRepoImport) { - spinner.start("Packing your repository..."); + spinner.start(`Exporting your repository from ${pdsDisplayName}...`); let carBytes: Uint8Array; try { carBytes = await sourceClient.getRepo(did); @@ -379,7 +375,7 @@ export const migrateCommand = defineCommand({ process.exit(1); } - spinner.start(`Unpacking at ${targetDomain}...`); + spinner.start(`Importing to ${targetDomain}...`); try { await targetClient.importRepo(carBytes); spinner.stop("Repository imported"); @@ -401,7 +397,9 @@ export const migrateCommand = defineCommand({ const preferences = await sourceClient.getPreferences(); if (preferences.length > 0) { await targetClient.putPreferences(preferences); - spinner.stop(`Migrated ${preferences.length} preference${preferences.length === 1 ? "" : "s"}`); + spinner.stop( + `Migrated ${preferences.length} preference${preferences.length === 1 ? "" : "s"}`, + ); } else { spinner.stop("No preferences to migrate"); } @@ -437,7 +435,7 @@ export const migrateCommand = defineCommand({ countCursor = page.cursor; } while (countCursor); - spinner.message(`Transferring images ${progressBar(0, totalBlobs)}`); + spinner.message(`Transferring media:\n${progressBar(0, totalBlobs)}`); do { const page = await targetClient.listMissingBlobs(100, cursor); @@ -452,13 +450,13 @@ export const migrateCommand = defineCommand({ await targetClient.uploadBlob(bytes, mimeType); synced++; spinner.message( - `Transferring images ${progressBar(synced, totalBlobs)}`, + `Transferring media:\n${progressBar(synced, totalBlobs)}`, ); } catch (err) { synced++; failedBlobs.push(blob.cid); spinner.message( - `Transferring images ${progressBar(synced, totalBlobs)}`, + `Transferring media:\n${progressBar(synced, totalBlobs)}`, ); } } @@ -466,11 +464,11 @@ export const migrateCommand = defineCommand({ if (failedBlobs.length > 0) { spinner.stop( - `Transferred ${formatNumber(synced - failedBlobs.length)} images (${failedBlobs.length} failed)`, + `Transferred ${formatNumber(synced - failedBlobs.length)} images and videos (${failedBlobs.length} failed)`, ); p.log.warn(`Run 'pds migrate' again to retry failed transfers.`); } else { - spinner.stop(`Transferred ${formatNumber(synced)} images`); + spinner.stop(`Transferred ${formatNumber(synced)} images and videos`); } } @@ -482,7 +480,7 @@ export const migrateCommand = defineCommand({ finalStatus.importedBlobs >= finalStatus.expectedBlobs; if (allBlobsSynced) { - p.log.success("All packed and moved! 🦋"); + p.log.success("All packed and moved!"); } else { p.log.warn( `Migration partially complete. ${finalStatus.expectedBlobs - finalStatus.importedBlobs} images remaining.`, @@ -490,7 +488,7 @@ export const migrateCommand = defineCommand({ p.log.info("Run 'pds migrate' again to continue."); } - showNextSteps(pm, sourceDomain); + showNextSteps(pm, pdsDisplayName); p.outro("Welcome to your new home in the Atmosphere! 🦋"); }, }); diff --git a/packages/pds/src/cli/utils/cli-helpers.ts b/packages/pds/src/cli/utils/cli-helpers.ts index e64efcd9..e16ee6ce 100644 --- a/packages/pds/src/cli/utils/cli-helpers.ts +++ b/packages/pds/src/cli/utils/cli-helpers.ts @@ -1,15 +1,54 @@ /** * Shared CLI utilities for PDS commands */ +import * as p from "@clack/prompts"; +import type { TextOptions, ConfirmOptions, SelectOptions } from "@clack/prompts"; /** - * Get target PDS URL based on mode + * Prompt for text input, exiting on cancel + */ +export async function promptText(options: TextOptions): Promise { + const result = await p.text(options); + if (p.isCancel(result)) { + p.cancel("Cancelled"); + process.exit(0); + } + return result as string; +} + +/** + * Prompt for confirmation, exiting on cancel + */ +export async function promptConfirm(options: ConfirmOptions): Promise { + const result = await p.confirm(options); + if (p.isCancel(result)) { + p.cancel("Cancelled"); + process.exit(0); + } + return result; +} + +/** + * Prompt for selection, exiting on cancel */ -export function getTargetUrl(isDev: boolean, pdsHostname: string | undefined): string { - const LOCAL_PDS_URL = "http://localhost:5173"; +export async function promptSelect(options: SelectOptions): Promise { + const result = await p.select(options); + if (p.isCancel(result)) { + p.cancel("Cancelled"); + process.exit(0); + } + return result as V; +} +/** + * Get target PDS URL based on mode + */ +export function getTargetUrl( + isDev: boolean, + pdsHostname: string | undefined, +): string { if (isDev) { - return LOCAL_PDS_URL; + return `http://localhost:${process.env.PORT ? (parseInt(process.env.PORT) ?? "5173") : "5173"}`; } if (!pdsHostname) { throw new Error("PDS_HOSTNAME not configured in wrangler.jsonc");