WIP: A simple cli for daily tangled use cases and AI integration. This is for my personal use right now, but happy if others get mileage from it! :)

Add prompts for remote selection and config persistence

Extend prompts module with:
- promptForRemoteSelection(): Interactive picker for Git remotes
with default to "origin" if present
- promptToSaveRemote(): Confirmation prompt to persist remote choice
to .tangledrc config file

These prompts are used by context resolution when multiple tangled.org
remotes exist and user input is needed.

Includes comprehensive test coverage (10 tests total, 5 new tests).

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

markbennett.ca ccbf3300 398c2888

verified
+125 -2
+37 -1
src/utils/prompts.ts
··· 1 - import { input, password } from '@inquirer/prompts'; 1 + import { confirm, input, password, select } from '@inquirer/prompts'; 2 2 import { safeValidateIdentifier } from './validation.js'; 3 3 4 4 /** ··· 54 54 password: passwordValue, 55 55 }; 56 56 } 57 + 58 + /** 59 + * Prompt user to select a Git remote when multiple tangled remotes exist 60 + * 61 + * @param remotes - Array of available remotes with name and URL 62 + * @returns Selected remote name 63 + */ 64 + export async function promptForRemoteSelection( 65 + remotes: Array<{ name: string; url: string }> 66 + ): Promise<string> { 67 + const choices = remotes.map((remote) => ({ 68 + name: `${remote.name} (${remote.url})`, 69 + value: remote.name, 70 + })); 71 + 72 + // Default to "origin" if present 73 + const defaultValue = remotes.find((r) => r.name === 'origin')?.name; 74 + 75 + return await select({ 76 + message: 'Multiple tangled.org remotes found. Which one would you like to use?', 77 + choices, 78 + default: defaultValue, 79 + }); 80 + } 81 + 82 + /** 83 + * Prompt user whether to save remote selection to config 84 + * 85 + * @returns true if user wants to save 86 + */ 87 + export async function promptToSaveRemote(): Promise<boolean> { 88 + return await confirm({ 89 + message: 'Save this remote selection for this repository? (saves to .tangledrc)', 90 + default: false, 91 + }); 92 + }
+88 -1
tests/utils/prompts.test.ts
··· 1 1 import { beforeEach, describe, expect, it, vi } from 'vitest'; 2 - import { promptForIdentifier, promptForLogin, promptForPassword } from '../../src/utils/prompts.js'; 2 + import { 3 + promptForIdentifier, 4 + promptForLogin, 5 + promptForPassword, 6 + promptForRemoteSelection, 7 + promptToSaveRemote, 8 + } from '../../src/utils/prompts.js'; 3 9 4 10 // Mock @inquirer/prompts 5 11 vi.mock('@inquirer/prompts', () => ({ 12 + confirm: vi.fn(), 6 13 input: vi.fn(), 7 14 password: vi.fn(), 15 + select: vi.fn(), 8 16 })); 9 17 10 18 describe('Prompts', () => { 11 19 let mockInput: ReturnType<typeof vi.fn>; 12 20 let mockPassword: ReturnType<typeof vi.fn>; 21 + let mockSelect: ReturnType<typeof vi.fn>; 22 + let mockConfirm: ReturnType<typeof vi.fn>; 13 23 14 24 beforeEach(async () => { 15 25 const inquirer = await import('@inquirer/prompts'); 16 26 mockInput = vi.mocked(inquirer.input); 17 27 mockPassword = vi.mocked(inquirer.password); 28 + mockSelect = vi.mocked(inquirer.select); 29 + mockConfirm = vi.mocked(inquirer.confirm); 18 30 vi.clearAllMocks(); 19 31 }); 20 32 ··· 101 113 }); 102 114 expect(mockInput).toHaveBeenCalledOnce(); 103 115 expect(mockPassword).toHaveBeenCalledOnce(); 116 + }); 117 + }); 118 + 119 + describe('promptForRemoteSelection', () => { 120 + it('should prompt user to select from multiple remotes', async () => { 121 + const remotes = [ 122 + { name: 'origin', url: 'git@tangled.org:did:plc:abc123/repo.git' }, 123 + { name: 'upstream', url: 'git@tangled.org:did:plc:xyz789/repo.git' }, 124 + ]; 125 + 126 + mockSelect.mockResolvedValue('upstream'); 127 + 128 + const result = await promptForRemoteSelection(remotes); 129 + 130 + expect(result).toBe('upstream'); 131 + expect(mockSelect).toHaveBeenCalledWith({ 132 + message: 'Multiple tangled.org remotes found. Which one would you like to use?', 133 + choices: [ 134 + { name: 'origin (git@tangled.org:did:plc:abc123/repo.git)', value: 'origin' }, 135 + { name: 'upstream (git@tangled.org:did:plc:xyz789/repo.git)', value: 'upstream' }, 136 + ], 137 + default: 'origin', 138 + }); 139 + }); 140 + 141 + it('should default to "origin" if present', async () => { 142 + const remotes = [ 143 + { name: 'upstream', url: 'git@tangled.org:did:plc:abc123/repo.git' }, 144 + { name: 'origin', url: 'git@tangled.org:did:plc:xyz789/repo.git' }, 145 + ]; 146 + 147 + mockSelect.mockResolvedValue('origin'); 148 + 149 + await promptForRemoteSelection(remotes); 150 + 151 + const call = mockSelect.mock.calls[0]?.[0]; 152 + expect(call?.default).toBe('origin'); 153 + }); 154 + 155 + it('should not have default if "origin" not present', async () => { 156 + const remotes = [ 157 + { name: 'upstream', url: 'git@tangled.org:did:plc:abc123/repo.git' }, 158 + { name: 'fork', url: 'git@tangled.org:did:plc:xyz789/repo.git' }, 159 + ]; 160 + 161 + mockSelect.mockResolvedValue('upstream'); 162 + 163 + await promptForRemoteSelection(remotes); 164 + 165 + const call = mockSelect.mock.calls[0]?.[0]; 166 + expect(call?.default).toBeUndefined(); 167 + }); 168 + }); 169 + 170 + describe('promptToSaveRemote', () => { 171 + it('should prompt user to save remote selection', async () => { 172 + mockConfirm.mockResolvedValue(true); 173 + 174 + const result = await promptToSaveRemote(); 175 + 176 + expect(result).toBe(true); 177 + expect(mockConfirm).toHaveBeenCalledWith({ 178 + message: 'Save this remote selection for this repository? (saves to .tangledrc)', 179 + default: false, 180 + }); 181 + }); 182 + 183 + it('should default to false', async () => { 184 + mockConfirm.mockResolvedValue(false); 185 + 186 + const result = await promptToSaveRemote(); 187 + 188 + expect(result).toBe(false); 189 + const call = mockConfirm.mock.calls[0]?.[0]; 190 + expect(call?.default).toBe(false); 104 191 }); 105 192 }); 106 193 });