From a233d60969bca767240749317606e91c819d6461 Mon Sep 17 00:00:00 2001
From: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com>
Date: Wed, 27 Aug 2025 15:50:54 +1000
Subject: [PATCH 1/2] Implemented lpop ask
---
CLAUDE.md | 2 +
bun.lock | 33 +++++++++++
package.json | 1 +
src/ask-messages.ts | 52 ++++++++++++++++++
src/cli.ts | 110 ++++++++++++++++++++++++++++++++-----
tests/ask-messages.test.ts | 77 ++++++++++++++++++++++++++
tests/cli.test.ts | 79 ++++++++++++++++++++++++++
7 files changed, 340 insertions(+), 14 deletions(-)
create mode 100644 src/ask-messages.ts
create mode 100644 tests/ask-messages.test.ts
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..2f83f0a 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,62 @@ describe('LpopCLI', () => {
});
});
});
+
+ describe('ask command', () => {
+ const mockedClipboardy = vi.mocked(clipboardy);
+ const mockedGetOrCreateDeviceKey = vi.mocked(getOrCreateDeviceKey);
+ const mockedFormatAskMessage = vi.mocked(formatAskMessage);
+
+ beforeEach(() => {
+ 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();
+ });
+ });
});
From 6d77f00b7f76b814514a67d6f357de7a34fc3f5f Mon Sep 17 00:00:00 2001
From: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com>
Date: Wed, 27 Aug 2025 16:22:24 +1000
Subject: [PATCH 2/2] Corrected tests
---
tests/cli.test.ts | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/tests/cli.test.ts b/tests/cli.test.ts
index 2f83f0a..c7ba4d9 100644
--- a/tests/cli.test.ts
+++ b/tests/cli.test.ts
@@ -987,6 +987,19 @@ describe('LpopCLI', () => {
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',