diff --git a/CLAUDE.md b/CLAUDE.md index 0626412..92247ba 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -82,3 +82,5 @@ The main command `lpop ` intelligently determines the operation: ## Development Guidelines - Use bun to build and install libraries don't use pnpm or npm + +- Add dependencies as dev dependencies instead of direct dependencies using bun as this is a package distributed as a compiled cli \ No newline at end of file diff --git a/bun.lock b/bun.lock index ab51736..d1823a9 100644 --- a/bun.lock +++ b/bun.lock @@ -13,6 +13,7 @@ "@vitest/ui": "^3.2.4", "bs58": "^6.0.0", "chalk": "^5.4.1", + "clipboardy": "^4.0.0", "commander": "^14.0.0", "dotenv": "^17.2.0", "git-url-parse": "^16.1.0", @@ -277,6 +278,8 @@ "check-error": ["check-error@2.1.1", "", {}, "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw=="], + "clipboardy": ["clipboardy@4.0.0", "", { "dependencies": { "execa": "^8.0.1", "is-wsl": "^3.1.0", "is64bit": "^2.0.0" } }, "sha512-5mOlNS0mhX0707P2I0aZ2V/cmHUEO/fL7VFLqszkhUsxt7RwnmrInf/eEQKlf5GzvYeHIjT+Ov1HRfNmymlG0w=="], + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], @@ -303,6 +306,8 @@ "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + "execa": ["execa@8.0.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", "human-signals": "^5.0.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^4.1.0", "strip-final-newline": "^3.0.0" } }, "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg=="], + "expect-type": ["expect-type@1.2.2", "", {}, "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA=="], "fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="], @@ -315,6 +320,8 @@ "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "get-stream": ["get-stream@8.0.1", "", {}, "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA=="], + "get-tsconfig": ["get-tsconfig@4.10.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="], "git-up": ["git-up@8.1.1", "", { "dependencies": { "is-ssh": "^1.4.0", "parse-url": "^9.2.0" } }, "sha512-FDenSF3fVqBYSaJoYy1KSc2wosx0gCvKP+c+PRBht7cAaiCeQlBtfBDX9vgnNOHmdePlSFITVcn4pFfcgNvx3g=="], @@ -329,10 +336,22 @@ "html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], + "human-signals": ["human-signals@5.0.0", "", {}, "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ=="], + + "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], + "is-ssh": ["is-ssh@1.4.1", "", { "dependencies": { "protocols": "^2.0.1" } }, "sha512-JNeu1wQsHjyHgn9NcWTaXq6zWSR6hqE0++zhfZlkFBbScNkyvxCdeV8sRkSBaeLKxmbpR21brail63ACNxJ0Tg=="], + "is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="], + + "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], + + "is64bit": ["is64bit@2.0.0", "", { "dependencies": { "system-architecture": "^0.1.0" } }, "sha512-jv+8jaWCl0g2lSBkNSVXdzfBA0npK1HGC2KtWM9FumFRoGS94g3NbCCLVnCYHLjp4GrW2KZeeSTMo5ddtznmGw=="], + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="], @@ -379,6 +398,10 @@ "make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="], + "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], + + "mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="], + "minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], @@ -389,6 +412,10 @@ "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "npm-run-path": ["npm-run-path@5.3.0", "", { "dependencies": { "path-key": "^4.0.0" } }, "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ=="], + + "onetime": ["onetime@6.0.0", "", { "dependencies": { "mimic-fn": "^4.0.0" } }, "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ=="], + "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], "parse-path": ["parse-path@7.1.0", "", { "dependencies": { "protocols": "^2.0.0" } }, "sha512-EuCycjZtfPcjWk7KTksnJ5xPMvWGA/6i4zrLYhRG0hGvC3GPU/jGUj3Cy+ZR0v30duV3e23R95T1lE2+lsndSw=="], @@ -443,10 +470,14 @@ "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "strip-final-newline": ["strip-final-newline@3.0.0", "", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="], + "strip-literal": ["strip-literal@3.0.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA=="], "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "system-architecture": ["system-architecture@0.1.0", "", {}, "sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA=="], + "test-exclude": ["test-exclude@7.0.1", "", { "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^10.4.1", "minimatch": "^9.0.4" } }, "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg=="], "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], @@ -489,6 +520,8 @@ "happy-dom/@types/node": ["@types/node@20.19.9", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw=="], + "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], + "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], diff --git a/package.json b/package.json index cb93eee..1d5ae6d 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@vitest/ui": "^3.2.4", "bs58": "^6.0.0", "chalk": "^5.4.1", + "clipboardy": "^4.0.0", "commander": "^14.0.0", "dotenv": "^17.2.0", "git-url-parse": "^16.1.0", diff --git a/src/ask-messages.ts b/src/ask-messages.ts new file mode 100644 index 0000000..112fdd8 --- /dev/null +++ b/src/ask-messages.ts @@ -0,0 +1,52 @@ +/** + * Quirky message templates for asking colleagues for environment variables + */ + +const ASK_MESSAGES = [ + "🙏 Help a fellow developer out! I'm missing some environment variables:", + '🚨 SOS! My .env file is feeling a bit empty. Could you help me with:', + '🔑 Secret ingredient missing! Hook me up with these variables:', + '🎭 Playing hide and seek with some env vars. Mind sharing:', + '🔍 Environment variable detective work needed! Please share:', + '💎 Looking for these precious environment gems:', + '🧩 Missing puzzle pieces for my environment! Can you provide:', + '🎪 Step right up and share these magical variables:', + '🏴‍☠️ Ahoy! Seeking treasure (aka environment variables):', + '🌟 Calling all env var wizards! I need these spells:', + '🎯 Target acquired: environment variables. Please deploy:', + '🔮 Crystal ball says I need these variables. Can you share:', + '🎨 Painting my environment, but missing these colors:', + '🚀 Mission control, requesting these variables for launch:', + '🎵 My environment is singing the blues without these vars:', + '🍕 My development environment is hungry for these toppings:', + '🎲 Rolling the dice and hoping you can share these variables:', + '🏆 Champion env var sharer needed! Please provide:', + '🎪 The greatest show on earth needs these environment variables:', + '🔥 My build is fire, but it needs these variables to ignite:', +]; + +/** + * Gets a random quirky message template + */ +export const getRandomAskMessage = (): string => { + const randomIndex = Math.floor(Math.random() * ASK_MESSAGES.length); + return ASK_MESSAGES[randomIndex]; +}; + +/** + * Formats the complete ask message with public key + */ +export const formatAskMessage = ( + publicKey: string, + serviceName: string, + environment?: string, +): string => { + const quirkMessage = getRandomAskMessage(); + const envSuffix = environment ? ` [${environment}]` : ''; + + return `${quirkMessage} + +Repository: ${serviceName}${envSuffix} + +lpop give ${publicKey}`; +}; diff --git a/src/cli.ts b/src/cli.ts index eccdb4f..5aa3393 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,8 +1,10 @@ import { spawn } from 'node:child_process'; import { existsSync } from 'node:fs'; import chalk from 'chalk'; +import clipboardy from 'clipboardy'; import { Command } from 'commander'; import packageJson from '../package.json' with { type: 'json' }; +import { formatAskMessage } from './ask-messages.js'; import { asVariable, type EnvEntry, @@ -14,6 +16,7 @@ import { } from './env-file-parser.js'; import { GitPathResolver, getServicePrefix } from './git-path-resolver.js'; import { KeychainManager } from './keychain-manager.js'; +import { getOrCreateDeviceKey } from './quantum-keys.js'; type Options = { env?: string; @@ -132,6 +135,17 @@ export class LpopCLI { const mergedOptions = { ...globalOptions, ...options }; await this.handleEnv(command, mergedOptions); }); + + this.program + .command('ask') + .description( + 'Generate a message to ask colleagues for missing environment variables', + ) + .action(async (options: CommandOptions) => { + const globalOptions = this.program.opts(); + const mergedOptions = { ...globalOptions, ...options }; + await this.handleAsk(mergedOptions); + }); } private async handleSmartCommand( @@ -182,6 +196,7 @@ export class LpopCLI { options: { env?: string; repo?: string }, ): Promise { const serviceName = await this.getServiceName(options); + const repoDisplayName = await this.getRepositoryDisplayName(options); const keychain = new KeychainManager(serviceName, options.env); try { @@ -215,7 +230,9 @@ export class LpopCLI { await keychain.setEnvironmentVariables(keychainEntries); console.log( - chalk.green(`✓ Added ${entries.length} variables to ${serviceName}`), + chalk.green( + `✓ Added ${entries.length} variables to ${repoDisplayName}`, + ), ); } catch (error) { console.error( @@ -237,12 +254,10 @@ export class LpopCLI { options: GetOptions, ): Promise { const serviceName = await this.getServiceName(options); - let logMsg = `Getting variables for ${serviceName}`; - if (options.repo) { - logMsg += ` for repo ${options.repo}`; - } + const repoDisplayName = await this.getRepositoryDisplayName(options); + let logMsg = `Getting variables for ${repoDisplayName}`; if (options.env) { - logMsg += ` [env: ${options.env}]`; + logMsg += ` [${options.env}]`; } console.log(chalk.blue(logMsg)); const keychain = new KeychainManager(serviceName, options.env); @@ -251,7 +266,7 @@ export class LpopCLI { const variables = await keychain.getEnvironmentVariables(); if (variables.length === 0) { - console.log(chalk.yellow(`No variables found for ${serviceName}`)); + console.log(chalk.yellow(`No variables found for ${repoDisplayName}`)); return; } @@ -347,17 +362,18 @@ export class LpopCLI { options: CommandOptions, ): Promise { const serviceName = await this.getServiceName(options); + const repoDisplayName = await this.getRepositoryDisplayName(options); const keychain = new KeychainManager(serviceName, options.env); try { const removed = await keychain.removeEnvironmentVariable(key); if (removed) { console.log( - chalk.green(`✓ Removed variable ${key} from ${serviceName}`), + chalk.green(`✓ Removed variable ${key} from ${repoDisplayName}`), ); } else { console.log( - chalk.yellow(`Variable ${key} not found in ${serviceName}`), + chalk.yellow(`Variable ${key} not found in ${repoDisplayName}`), ); } } catch (error) { @@ -372,13 +388,14 @@ export class LpopCLI { private async handleClear(options: ClearOptions): Promise { const serviceName = await this.getServiceName(options); + const repoDisplayName = await this.getRepositoryDisplayName(options); const keychain = new KeychainManager(serviceName, options.env); try { if (!options.confirm) { console.log( chalk.yellow( - `This will remove ALL environment variables for ${serviceName}`, + `This will remove ALL environment variables for ${repoDisplayName}`, ), ); console.log(chalk.yellow('Use --confirm to skip this warning')); @@ -386,7 +403,9 @@ export class LpopCLI { } await keychain.clearAllEnvironmentVariables(); - console.log(chalk.green(`✓ Cleared all variables for ${serviceName}`)); + console.log( + chalk.green(`✓ Cleared all variables for ${repoDisplayName}`), + ); } catch (error) { console.error( chalk.red( @@ -418,6 +437,7 @@ export class LpopCLI { options: CommandOptions, ): Promise { const serviceName = await this.getServiceName(options); + const repoDisplayName = await this.getRepositoryDisplayName(options); const keychain = new KeychainManager(serviceName, options.env); try { @@ -450,7 +470,7 @@ export class LpopCLI { const variables = await keychain.getEnvironmentVariables(); if (variables.length === 0) { - console.log(chalk.yellow(`No variables found for ${serviceName}`)); + console.log(chalk.yellow(`No variables found for ${repoDisplayName}`)); if (actualCommand.length === 0) { return; } @@ -458,7 +478,9 @@ export class LpopCLI { // If no command specified, just show what variables would be loaded if (actualCommand.length === 0) { - console.log(chalk.blue(`Environment variables for ${serviceName}:`)); + console.log( + chalk.blue(`Environment variables for ${repoDisplayName}:`), + ); for (const { key, value } of variables) { console.log(`${key}=${value}`); } @@ -473,7 +495,7 @@ export class LpopCLI { console.log( chalk.blue( - `Running "${actualCommand.join(' ')}" with ${variables.length} variables from ${serviceName}`, + `Running "${actualCommand.join(' ')}" with ${variables.length} variables from ${repoDisplayName}`, ), ); @@ -502,6 +524,47 @@ export class LpopCLI { } } + private async handleAsk(options: CommandOptions): Promise { + try { + const deviceKey = getOrCreateDeviceKey(); + + // Get clean repository display name + const repoDisplayName = await this.getRepositoryDisplayName(options); + + console.log( + chalk.blue( + `Generating ask message for ${repoDisplayName}${options.env ? ` [${options.env}]` : ''}`, + ), + ); + + const message = formatAskMessage( + deviceKey.publicKey, + repoDisplayName, + options.env, + ); + + await clipboardy.write(message); + + console.log(chalk.green('✓ Message copied to clipboard!')); + console.log(chalk.gray('\nGenerated message:')); + console.log(chalk.gray('─'.repeat(50))); + console.log(message); + console.log(chalk.gray('─'.repeat(50))); + console.log( + chalk.blue( + '\nShare this message with your colleague to request environment variables!', + ), + ); + } catch (error) { + console.error( + chalk.red( + `Error generating ask message: ${error instanceof Error ? error.message : String(error)}`, + ), + ); + process.exit(1); + } + } + private async getServiceName(options: CommandOptions): Promise { if (options.repo) { return `${getServicePrefix()}${options.repo}`; @@ -510,6 +573,25 @@ export class LpopCLI { return await this.gitResolver.generateServiceNameAsync(); } + private async getRepositoryDisplayName( + options: CommandOptions, + ): Promise { + if (options.repo) { + // If repo is manually specified, show it cleanly + return options.repo; + } + + // Get git information for clean display + const gitInfo = await this.gitResolver.getGitInfo(); + if (gitInfo) { + return gitInfo.full_name; // e.g., "loggipop/lpop" + } + + // Fallback for non-git directories + const dirName = process.cwd().split('/').pop() || 'unknown'; + return `Local: ${dirName}`; + } + public async run(): Promise { await this.program.parseAsync(process.argv); } diff --git a/tests/ask-messages.test.ts b/tests/ask-messages.test.ts new file mode 100644 index 0000000..9c1f2a6 --- /dev/null +++ b/tests/ask-messages.test.ts @@ -0,0 +1,77 @@ +import { describe, expect, it } from 'vitest'; +import { formatAskMessage, getRandomAskMessage } from '../src/ask-messages.js'; + +describe('ask-messages', () => { + describe('getRandomAskMessage', () => { + it('should return a string', () => { + const message = getRandomAskMessage(); + expect(typeof message).toBe('string'); + expect(message.length).toBeGreaterThan(0); + }); + + it('should return different messages on multiple calls', () => { + const messages = new Set(); + + for (let i = 0; i < 50; i++) { + messages.add(getRandomAskMessage()); + } + + expect(messages.size).toBeGreaterThan(1); + }); + + it('should return messages with personality (contain emojis)', () => { + const message = getRandomAskMessage(); + const emojiRegex = + /[\u{1F300}-\u{1F6FF}]|[\u{1F900}-\u{1F9FF}]|[\u{2600}-\u{27FF}]/u; + expect(emojiRegex.test(message)).toBe(true); + }); + }); + + describe('formatAskMessage', () => { + const mockPublicKey = 'test123PublicKey'; + const mockServiceName = 'user/repo'; + + it('should format message with public key and service name', () => { + const message = formatAskMessage(mockPublicKey, mockServiceName); + + expect(message).toContain(mockPublicKey); + expect(message).toContain(mockServiceName); + expect(message).toContain('lpop give'); + expect(message).toContain('Repository:'); + }); + + it('should include environment when provided', () => { + const environment = 'production'; + const message = formatAskMessage( + mockPublicKey, + mockServiceName, + environment, + ); + + expect(message).toContain(`[${environment}]`); + }); + + it('should not include environment section when not provided', () => { + const message = formatAskMessage(mockPublicKey, mockServiceName); + + expect(message).not.toContain('['); + expect(message).not.toContain(']'); + }); + + it('should contain plain text command without formatting', () => { + const message = formatAskMessage(mockPublicKey, mockServiceName); + + expect(message).toContain('lpop give'); + expect(message).not.toContain('`'); + expect(message).not.toContain('```'); + }); + + it('should have proper structure with line breaks', () => { + const message = formatAskMessage(mockPublicKey, mockServiceName); + const lines = message.split('\n'); + + expect(lines.length).toBeGreaterThan(3); + expect(lines.find((line) => line.includes('lpop give'))).toBeTruthy(); + }); + }); +}); diff --git a/tests/cli.test.ts b/tests/cli.test.ts index 99e586b..c7ba4d9 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -1,5 +1,6 @@ import { type ChildProcess, spawn } from 'node:child_process'; import { existsSync } from 'node:fs'; +import clipboardy from 'clipboardy'; import { afterEach, beforeEach, @@ -9,6 +10,7 @@ import { type MockInstance, vi, } from 'vitest'; +import { formatAskMessage } from '../src/ask-messages'; import { LpopCLI } from '../src/cli'; import { asComment, @@ -21,6 +23,7 @@ import { } from '../src/env-file-parser'; import { GitPathResolver, getServicePrefix } from '../src/git-path-resolver'; import { KeychainManager } from '../src/keychain-manager'; +import { getOrCreateDeviceKey } from '../src/quantum-keys'; // Mock modules vi.mock('../src/keychain-manager'); @@ -33,9 +36,27 @@ vi.mock('chalk', () => ({ green: vi.fn((text: string) => text), yellow: vi.fn((text: string) => text), red: vi.fn((text: string) => text), + gray: vi.fn((text: string) => text), }, })); +// Mock clipboardy +vi.mock('clipboardy', () => ({ + default: { + write: vi.fn(), + }, +})); + +// Mock quantum keys +vi.mock('../src/quantum-keys', () => ({ + getOrCreateDeviceKey: vi.fn(), +})); + +// Mock ask messages +vi.mock('../src/ask-messages', () => ({ + formatAskMessage: vi.fn(), +})); + // Mock specific functions from env-file-parser instead of the entire module vi.mock('../src/env-file-parser', async (importOriginal) => { const actual = @@ -959,4 +980,75 @@ describe('LpopCLI', () => { }); }); }); + + describe('ask command', () => { + const mockedClipboardy = vi.mocked(clipboardy); + const mockedGetOrCreateDeviceKey = vi.mocked(getOrCreateDeviceKey); + const mockedFormatAskMessage = vi.mocked(formatAskMessage); + + beforeEach(() => { + // Mock the git resolver to return a service name containing "test-service" + mockGitResolver.generateServiceNameAsync.mockResolvedValue( + 'github.com/user/test-service', + ); + + // Mock getGitInfo to return git info with test-service in the name + mockGitResolver.getGitInfo.mockResolvedValue({ + full_name: 'user/test-service', + owner: { login: 'user' }, + name: 'test-service', + html_url: 'https://github.com/user/test-service', + }); + + mockedGetOrCreateDeviceKey.mockReturnValue({ + publicKey: 'test-public-key-123', + privateKey: 'test-private-key-456', + createdAt: Date.now(), + expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000, + }); + + mockedFormatAskMessage.mockReturnValue( + 'Formatted ask message with test-public-key-123', + ); + mockedClipboardy.write.mockResolvedValue(); + }); + + it('should generate ask message and copy to clipboard', async () => { + process.argv = ['node', 'lpop', 'ask']; + await cli.run(); + + expect(mockedGetOrCreateDeviceKey).toHaveBeenCalled(); + expect(mockedFormatAskMessage).toHaveBeenCalledWith( + 'test-public-key-123', + expect.stringContaining('test-service'), + undefined, + ); + expect(mockedClipboardy.write).toHaveBeenCalledWith( + 'Formatted ask message with test-public-key-123', + ); + expect(consoleLogSpy).toHaveBeenCalledWith( + expect.stringContaining('Message copied to clipboard'), + ); + }); + + it('should include environment in ask message when specified', async () => { + process.argv = ['node', 'lpop', 'ask', '-e', 'production']; + await cli.run(); + + expect(mockedFormatAskMessage).toHaveBeenCalledWith( + 'test-public-key-123', + expect.stringContaining('test-service'), + 'production', + ); + }); + + it('should handle clipboard write errors gracefully', async () => { + mockedClipboardy.write.mockRejectedValue(new Error('Clipboard error')); + + process.argv = ['node', 'lpop', 'ask']; + + // Should exit with error code 1 + await expect(cli.run()).rejects.toThrow(); + }); + }); });