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! :)
at main 165 lines 4.8 kB view raw
1/** 2 * Repository context resolution for Tangled CLI 3 * Automatically infers repository context from Git remotes 4 */ 5 6import { simpleGit } from 'simple-git'; 7import { isTangledRemote, parseTangledRemote } from '../utils/git.js'; 8import { promptForRemoteSelection, promptToSaveRemote } from '../utils/prompts.js'; 9import { getConfiguredRemote, setLocalRemote } from './config.js'; 10 11export interface RepositoryContext { 12 owner: string; // Owner identifier - DID (e.g., "did:plc:...") or handle (e.g., "markbennett.ca") 13 ownerType: 'did' | 'handle'; // Type of owner identifier 14 name: string; // Repository name (e.g., "tangled-cli") 15 remoteName: string; // Git remote name (e.g., "origin") 16 remoteUrl: string; // Full remote URL 17 protocol: 'ssh' | 'https'; // Protocol used by remote 18} 19 20/** 21 * Get all tangled.org remotes from the current Git repository 22 * 23 * @param cwd - Current working directory 24 * @returns Array of repository contexts 25 */ 26export async function getTangledRemotes(cwd: string = process.cwd()): Promise<RepositoryContext[]> { 27 try { 28 const git = simpleGit(cwd); 29 30 // Check if in a Git repository 31 const isRepo = await git.checkIsRepo(); 32 if (!isRepo) { 33 return []; 34 } 35 36 // Get all remotes with URLs 37 const remotes = await git.getRemotes(true); 38 39 // Filter and parse tangled.org remotes 40 const tangledRemotes: RepositoryContext[] = []; 41 42 for (const remote of remotes) { 43 if (!remote.refs.fetch || !isTangledRemote(remote.refs.fetch)) { 44 continue; 45 } 46 47 const parsed = parseTangledRemote(remote.refs.fetch); 48 if (!parsed) { 49 console.warn(`Warning: Invalid tangled.org remote URL: ${remote.refs.fetch}`); 50 continue; 51 } 52 53 tangledRemotes.push({ 54 owner: parsed.owner, 55 ownerType: parsed.ownerType, 56 name: parsed.name, 57 remoteName: remote.name, 58 remoteUrl: remote.refs.fetch, 59 protocol: parsed.protocol, 60 }); 61 } 62 63 return tangledRemotes; 64 } catch { 65 // Git errors - return empty array 66 return []; 67 } 68} 69 70/** 71 * Prompt user to select a remote when multiple tangled remotes exist 72 * 73 * @param remotes - Array of repository contexts 74 * @returns Selected repository context 75 */ 76export async function promptForRemote(remotes: RepositoryContext[]): Promise<RepositoryContext> { 77 if (remotes.length === 0) { 78 throw new Error('No remotes available to select from'); 79 } 80 81 if (remotes.length === 1) { 82 return remotes[0]; 83 } 84 85 // Convert to format expected by prompt 86 const remoteChoices = remotes.map((r) => ({ 87 name: r.remoteName, 88 url: r.remoteUrl, 89 })); 90 91 const selectedName = await promptForRemoteSelection(remoteChoices); 92 93 const selected = remotes.find((r) => r.remoteName === selectedName); 94 if (!selected) { 95 throw new Error(`Selected remote "${selectedName}" not found`); 96 } 97 98 return selected; 99} 100 101/** 102 * Get repository context from the current working directory 103 * Looks for Git remotes pointing to tangled.org 104 * 105 * @param cwd - Current working directory (defaults to process.cwd()) 106 * @returns Repository context or null if not in a tangled repo 107 */ 108export async function getCurrentRepoContext( 109 cwd: string = process.cwd() 110): Promise<RepositoryContext | null> { 111 // Get all tangled remotes 112 const remotes = await getTangledRemotes(cwd); 113 114 // No tangled remotes found 115 if (remotes.length === 0) { 116 return null; 117 } 118 119 // Single remote - use it 120 if (remotes.length === 1) { 121 return remotes[0]; 122 } 123 124 // Multiple remotes - check config first 125 const configuredRemote = await getConfiguredRemote(cwd); 126 127 if (configuredRemote) { 128 // Check if configured remote exists and is a tangled remote 129 const matchingRemote = remotes.find((r) => r.remoteName === configuredRemote); 130 131 if (matchingRemote) { 132 return matchingRemote; 133 } 134 135 // Configured remote doesn't exist or isn't a tangled remote 136 console.warn( 137 `Warning: Configured remote "${configuredRemote}" not found or is not a tangled.org remote. Continuing with heuristics.` 138 ); 139 } 140 141 // Check for "origin" remote 142 const originRemote = remotes.find((r) => r.remoteName === 'origin'); 143 if (originRemote) { 144 return originRemote; 145 } 146 147 // Prompt user to select 148 const selected = await promptForRemote(remotes); 149 150 // Ask if user wants to save selection 151 const shouldSave = await promptToSaveRemote(); 152 if (shouldSave) { 153 try { 154 await setLocalRemote(selected.remoteName, cwd); 155 console.log(`✓ Saved remote "${selected.remoteName}" to local config\n`); 156 } catch (error) { 157 console.warn( 158 `Warning: Failed to save config: ${error instanceof Error ? error.message : 'Unknown error'}` 159 ); 160 // Don't block command execution if config save fails 161 } 162 } 163 164 return selected; 165}