// Name: Secrets Manager // Description: Manage secrets in ~/.secrets // Author: Strajk import '@johnlindquist/kit' const SECRETS_PATH = home('.secrets') /** * Parses the secrets file (~/.secrets) and organizes the data into a structured object. * @returns A promise that resolves to an object where keys are secret names and values are objects * containing sections with their corresponding secret values. */ async function parseSecretsFile(): Promise<{ [key: string]: { [section: string]: string } }> { try { const content = await readFile(SECRETS_PATH, 'utf-8') const lines = content.split('\n') const secrets: { [key: string]: { [section: string]: string } } = {} let currentSection = 'default' for (const line of lines) { const trimmedLine = line.trim() // Skip lines starting with '# ===' (section dividers) if (trimmedLine.startsWith('# ===')) { continue } // Update current section if the line starts with '# ' if (trimmedLine.startsWith('# ')) { currentSection = trimmedLine.substring(2).trim() continue } // Process lines starting with 'export ' if (trimmedLine.startsWith('export ')) { const parts = trimmedLine.substring(7).trim().split('=') const key = parts[0].trim() const value = parts.slice(1).join('=').trim() if (!secrets[key]) { secrets[key] = {} } secrets[key][currentSection] = value } } return secrets } catch (error) { console.error('Error parsing secrets file:', error) return {} } } /** * Extracts the keys (secret names) from the parsed secrets object. * @param secrets The parsed secrets object. * @returns An array of strings, each representing a secret name. */ async function getKeyChoices(secrets: { [key: string]: { [section: string]: string } }): Promise<string[]> { return Object.keys(secrets) } /** * Obfuscates a secret value, showing only the first and last 4 characters, and replacing the rest with asterisks. * @param value The secret value to obfuscate. * @returns An obfuscated string representing the secret value. */ function obfuscate(value: string): string { if (value.length <= 16) { return '*'.repeat(value.length) } const visibleLength = 4 const hiddenLength = value.length - 2 * visibleLength return ( value.substring(0, visibleLength) + '*'.repeat(hiddenLength) + value.substring(value.length - visibleLength) ) } /** * Generates an array of choices for selecting a section of a secret, including obfuscated descriptions and active status. * @param secrets The parsed secrets object. * @param key The secret name for which to generate section choices. * @returns A promise that resolves to an array of choice objects, each representing a section. */ async function getSectionChoices( secrets: { [key: string]: { [section: string]: string } }, key: string ): Promise< { name: string; value: string; description: string; active: boolean }[] > { try { const sections = Object.keys(secrets[key]) const content = await readFile(SECRETS_PATH, 'utf-8') return sections.map(section => { const value = secrets[key][section] const exportLine = `export ${key}=${value}` // Check if the export line is commented out by verifying that either the export line is missing, or the last occurance isn't the line in question. const commentedOut = !content.includes(exportLine) || content.indexOf(exportLine) > content.lastIndexOf(`export ${key}=`) return { name: section, value: value, description: obfuscate(value), active: !commentedOut, } }) } catch (error) { console.error('Error getting section choices:', error) return [] } } /** * Copies a value to the clipboard and displays a toast notification. * @param value The string value to copy to the clipboard. */ async function copyToClipboard(value: string): Promise<void> { try { await clipboard.writeText(value) toast('Copied to clipboard') } catch (error) { console.error('Error copying to clipboard:', error) toast('Failed to copy to clipboard') } } /** * Opens the secrets file in an editor, navigating to the line containing the selected secret. * @param key The secret name. * @param section The section of the secret. */ async function openSecretsFile(key: string, section: string): Promise<void> { try { const content = await readFile(SECRETS_PATH, 'utf-8') const value = secrets[key][section] const exportLine = `export ${key}=${value}` // Find the line number of the export line for the selected section const lineNumber = content.split('\n').findIndex(line => line.includes(exportLine)) + 1 await edit(`${SECRETS_PATH}:${lineNumber}`) } catch (error) { console.error('Error opening secrets file:', error) toast('Failed to open secrets file') } } /** * Toggles the comment status of a line in the secrets file. * @param key The secret name. * @param section The section of the secret. */ async function toggleComment(key: string, section: string): Promise<void> { try { let content = await readFile(SECRETS_PATH, 'utf-8') const value = secrets[key][section] const exportLine = `export ${key}=${value}` let lines = content.split('\n') // Find the index of the export line for the selected section let index = lines.findIndex(line => line.includes(exportLine)) // Toggle comment status if the line is found if (index > -1) { if (lines[index].trim().startsWith('#')) { lines[index] = lines[index].trim().substring(1).trimStart() } else { lines[index] = '# ' + lines[index] } } await writeFile(SECRETS_PATH, lines.join('\n')) } catch (error) { console.error('Error toggling comment:', error) toast('Failed to toggle comment') } } // Main script execution try { const secrets = await parseSecretsFile() const keyChoices = await getKeyChoices(secrets) const selectedKey = await arg('Select a key:', keyChoices) const sectionChoices = await getSectionChoices(secrets, selectedKey) const selectedSection = await arg( { placeholder: 'Select a section:', enter: 'Copy to clipboard', preview: (choice: { description: string }) => `<div class="text-sm">${choice.description}</div>`, }, sectionChoices.map(s => ({ name: s.name, value: s.value, description: s.description, active: s.active, })) ) const actions = [ { name: 'Copy to clipboard', onAction: async () => { await copyToClipboard(selectedSection) }, }, { name: 'Open in editor', onAction: async () => { const sectionObj = sectionChoices.find(s => s.value == selectedSection) await openSecretsFile(selectedKey, sectionObj.name) }, }, { name: 'Toggle comment', onAction: async () => { const sectionObj = sectionChoices.find(s => s.value == selectedSection) await toggleComment(selectedKey, sectionObj.name) }, }, ] await micro('Select Action', actions) } catch (error) { console.error('An unexpected error occurred:', error) toast('An unexpected error occurred') }