From ccbf3300dc0ff60a70a00816bcc8ea7bfea5e33b Mon Sep 17 00:00:00 2001 From: Mark Bennett Date: Mon, 9 Feb 2026 09:03:33 -0700 Subject: [PATCH] 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 --- src/utils/prompts.ts | 38 +++++++++++++++- tests/utils/prompts.test.ts | 89 ++++++++++++++++++++++++++++++++++++- 2 files changed, 125 insertions(+), 2 deletions(-) diff --git a/src/utils/prompts.ts b/src/utils/prompts.ts index 62a095d..9df6dca 100644 --- a/src/utils/prompts.ts +++ b/src/utils/prompts.ts @@ -1,4 +1,4 @@ -import { input, password } from '@inquirer/prompts'; +import { confirm, input, password, select } from '@inquirer/prompts'; import { safeValidateIdentifier } from './validation.js'; /** @@ -54,3 +54,39 @@ export async function promptForLogin(): Promise<{ password: passwordValue, }; } + +/** + * Prompt user to select a Git remote when multiple tangled remotes exist + * + * @param remotes - Array of available remotes with name and URL + * @returns Selected remote name + */ +export async function promptForRemoteSelection( + remotes: Array<{ name: string; url: string }> +): Promise { + const choices = remotes.map((remote) => ({ + name: `${remote.name} (${remote.url})`, + value: remote.name, + })); + + // Default to "origin" if present + const defaultValue = remotes.find((r) => r.name === 'origin')?.name; + + return await select({ + message: 'Multiple tangled.org remotes found. Which one would you like to use?', + choices, + default: defaultValue, + }); +} + +/** + * Prompt user whether to save remote selection to config + * + * @returns true if user wants to save + */ +export async function promptToSaveRemote(): Promise { + return await confirm({ + message: 'Save this remote selection for this repository? (saves to .tangledrc)', + default: false, + }); +} diff --git a/tests/utils/prompts.test.ts b/tests/utils/prompts.test.ts index 8f6ada2..d708c3c 100644 --- a/tests/utils/prompts.test.ts +++ b/tests/utils/prompts.test.ts @@ -1,20 +1,32 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { promptForIdentifier, promptForLogin, promptForPassword } from '../../src/utils/prompts.js'; +import { + promptForIdentifier, + promptForLogin, + promptForPassword, + promptForRemoteSelection, + promptToSaveRemote, +} from '../../src/utils/prompts.js'; // Mock @inquirer/prompts vi.mock('@inquirer/prompts', () => ({ + confirm: vi.fn(), input: vi.fn(), password: vi.fn(), + select: vi.fn(), })); describe('Prompts', () => { let mockInput: ReturnType; let mockPassword: ReturnType; + let mockSelect: ReturnType; + let mockConfirm: ReturnType; beforeEach(async () => { const inquirer = await import('@inquirer/prompts'); mockInput = vi.mocked(inquirer.input); mockPassword = vi.mocked(inquirer.password); + mockSelect = vi.mocked(inquirer.select); + mockConfirm = vi.mocked(inquirer.confirm); vi.clearAllMocks(); }); @@ -103,4 +115,79 @@ describe('Prompts', () => { expect(mockPassword).toHaveBeenCalledOnce(); }); }); + + describe('promptForRemoteSelection', () => { + it('should prompt user to select from multiple remotes', async () => { + const remotes = [ + { name: 'origin', url: 'git@tangled.org:did:plc:abc123/repo.git' }, + { name: 'upstream', url: 'git@tangled.org:did:plc:xyz789/repo.git' }, + ]; + + mockSelect.mockResolvedValue('upstream'); + + const result = await promptForRemoteSelection(remotes); + + expect(result).toBe('upstream'); + expect(mockSelect).toHaveBeenCalledWith({ + message: 'Multiple tangled.org remotes found. Which one would you like to use?', + choices: [ + { name: 'origin (git@tangled.org:did:plc:abc123/repo.git)', value: 'origin' }, + { name: 'upstream (git@tangled.org:did:plc:xyz789/repo.git)', value: 'upstream' }, + ], + default: 'origin', + }); + }); + + it('should default to "origin" if present', async () => { + const remotes = [ + { name: 'upstream', url: 'git@tangled.org:did:plc:abc123/repo.git' }, + { name: 'origin', url: 'git@tangled.org:did:plc:xyz789/repo.git' }, + ]; + + mockSelect.mockResolvedValue('origin'); + + await promptForRemoteSelection(remotes); + + const call = mockSelect.mock.calls[0]?.[0]; + expect(call?.default).toBe('origin'); + }); + + it('should not have default if "origin" not present', async () => { + const remotes = [ + { name: 'upstream', url: 'git@tangled.org:did:plc:abc123/repo.git' }, + { name: 'fork', url: 'git@tangled.org:did:plc:xyz789/repo.git' }, + ]; + + mockSelect.mockResolvedValue('upstream'); + + await promptForRemoteSelection(remotes); + + const call = mockSelect.mock.calls[0]?.[0]; + expect(call?.default).toBeUndefined(); + }); + }); + + describe('promptToSaveRemote', () => { + it('should prompt user to save remote selection', async () => { + mockConfirm.mockResolvedValue(true); + + const result = await promptToSaveRemote(); + + expect(result).toBe(true); + expect(mockConfirm).toHaveBeenCalledWith({ + message: 'Save this remote selection for this repository? (saves to .tangledrc)', + default: false, + }); + }); + + it('should default to false', async () => { + mockConfirm.mockResolvedValue(false); + + const result = await promptToSaveRemote(); + + expect(result).toBe(false); + const call = mockConfirm.mock.calls[0]?.[0]; + expect(call?.default).toBe(false); + }); + }); }); -- 2.54.0