diff --git a/packages/pds/src/cli/utils/cli-helpers.ts b/packages/pds/src/cli/utils/cli-helpers.ts index 3f67b93..1ba734d 100644 --- a/packages/pds/src/cli/utils/cli-helpers.ts +++ b/packages/pds/src/cli/utils/cli-helpers.ts @@ -203,6 +203,56 @@ export async function saveTo1Password( }); } +/** + * Save a password to 1Password as a Login item for bsky.app + */ +export async function savePasswordTo1Password( + password: string, + handle: string, +): Promise<{ success: boolean; itemName?: string; error?: string }> { + const itemName = `Bluesky - @${handle}`; + + return new Promise((resolve) => { + const child = spawn( + "op", + [ + "item", + "create", + "--category", + "Login", + "--title", + itemName, + `username=${handle}`, + `password=${password}`, + "--url=https://bsky.app", + "--tags", + "cirrus,pds,bluesky", + ], + { stdio: ["ignore", "pipe", "pipe"] }, + ); + + let stderr = ""; + child.stderr?.on("data", (data) => { + stderr += data.toString(); + }); + + child.on("error", (err) => { + resolve({ success: false, error: err.message }); + }); + + child.on("close", (code) => { + if (code === 0) { + resolve({ success: true, itemName }); + } else { + resolve({ + success: false, + error: stderr || `1Password CLI exited with code ${code}`, + }); + } + }); + }); +} + export interface RunCommandOptions { /** If true, stream output to stdout/stderr in real-time */ stream?: boolean; diff --git a/packages/pds/src/cli/utils/secrets.ts b/packages/pds/src/cli/utils/secrets.ts index 896d46a..e7dc087 100644 --- a/packages/pds/src/cli/utils/secrets.ts +++ b/packages/pds/src/cli/utils/secrets.ts @@ -7,7 +7,12 @@ import bcrypt from "bcryptjs"; import * as p from "@clack/prompts"; import { setSecret, setVar, type SecretName } from "./wrangler.js"; import { setDevVar } from "./dotenv.js"; -import { promptSelect, copyToClipboard } from "./cli-helpers.js"; +import { + promptSelect, + copyToClipboard, + is1PasswordAvailable, + savePasswordTo1Password, +} from "./cli-helpers.js"; export interface SigningKeypair { privateKey: string; // hex-encoded @@ -78,15 +83,65 @@ export async function promptPassword(handle?: string): Promise { if (method === "generate") { const password = generatePassword(); - p.note(password, "Generated password"); - const copied = await copyToClipboard(password); - if (copied) { - p.log.success("Copied to clipboard"); + const has1Password = await is1PasswordAvailable(); + + type SaveOption = "1password" | "clipboard" | "show"; + const saveOptions: Array<{ + value: SaveOption; + label: string; + hint: string; + }> = []; + + if (has1Password) { + saveOptions.push({ + value: "1password", + label: "Save to 1Password", + hint: "as a bsky.app login", + }); + } + + saveOptions.push( + { value: "clipboard", label: "Copy to clipboard", hint: "paste into password manager" }, + { value: "show", label: "Display it", hint: "shown in terminal" }, + ); + + const saveChoice = await promptSelect({ + message: "Where should we save the password?", + options: saveOptions, + }); + + if (saveChoice === "1password") { + const spinner = p.spinner(); + spinner.start("Saving to 1Password..."); + const result = await savePasswordTo1Password(password, handle ?? ""); + if (result.success) { + spinner.stop("Saved to 1Password"); + p.log.success(`Created: "${result.itemName}"`); + } else { + spinner.stop("Failed to save to 1Password"); + p.log.error(result.error || "Unknown error"); + // Fall back to clipboard + const copied = await copyToClipboard(password); + if (copied) { + p.log.info("Copied to clipboard instead"); + } else { + p.note(password, "Generated password"); + p.log.warn("Save this password somewhere safe!"); + } + } + } else if (saveChoice === "clipboard") { + const copied = await copyToClipboard(password); + if (copied) { + p.log.success("Password generated and copied to clipboard"); + } else { + p.note(password, "Generated password"); + p.log.warn("Could not copy to clipboard — save this password somewhere safe!"); + } } else { - p.log.warn( - "Could not copy to clipboard — save this password somewhere safe!", - ); + p.note(password, "Generated password"); + p.log.warn("Save this password somewhere safe!"); } + return password; }