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 173 lines 5.0 kB view raw
1/** 2 * Configuration management for Tangled CLI 3 * Handles loading and saving configuration with proper precedence: 4 * 1. TANGLED_REMOTE environment variable 5 * 2. Local config (.tangledrc in current directory or Git root) 6 * 3. User config (~/.tangledrc or ~/.config/tangled/config) 7 * 4. System config (/etc/tangledrc) 8 */ 9 10import { mkdir, unlink, writeFile } from 'node:fs/promises'; 11import { homedir } from 'node:os'; 12import { dirname, join } from 'node:path'; 13import { cosmiconfig } from 'cosmiconfig'; 14import { simpleGit } from 'simple-git'; 15 16export interface TangledConfig { 17 remote?: string; 18} 19 20const MODULE_NAME = 'tangled'; 21 22/** 23 * Get the Git root directory for the current working directory 24 * @param cwd - Current working directory 25 * @returns Git root path or null if not in a Git repository 26 */ 27async function getGitRoot(cwd: string = process.cwd()): Promise<string | null> { 28 try { 29 const git = simpleGit(cwd); 30 const isRepo = await git.checkIsRepo(); 31 if (!isRepo) { 32 return null; 33 } 34 const root = await git.revparse(['--show-toplevel']); 35 return root.trim(); 36 } catch { 37 return null; 38 } 39} 40 41/** 42 * Load configuration with proper precedence 43 * Checks: env var > local config > user config > system config 44 * @param cwd - Current working directory (defaults to process.cwd()) 45 * @returns Configuration object 46 */ 47export async function loadConfig(cwd: string = process.cwd()): Promise<TangledConfig> { 48 // Check environment variable first 49 if (process.env.TANGLED_REMOTE) { 50 return { remote: process.env.TANGLED_REMOTE }; 51 } 52 53 try { 54 const explorer = cosmiconfig(MODULE_NAME); 55 56 // For local config, search from Git root if in a Git repo 57 const gitRoot = await getGitRoot(cwd); 58 const searchFrom = gitRoot || cwd; 59 60 const result = await explorer.search(searchFrom); 61 62 if (result && !result.isEmpty) { 63 return result.config as TangledConfig; 64 } 65 } catch (error) { 66 // Log warning but continue with empty config 67 console.warn( 68 `Warning: Failed to load config: ${error instanceof Error ? error.message : 'Unknown error'}` 69 ); 70 } 71 72 return {}; 73} 74 75/** 76 * Get the configured remote name for the current context 77 * Returns null if no config found 78 * @param cwd - Current working directory 79 * @returns Remote name or null 80 */ 81export async function getConfiguredRemote(cwd: string = process.cwd()): Promise<string | null> { 82 const config = await loadConfig(cwd); 83 return config.remote || null; 84} 85 86/** 87 * Set the remote name in local config (.tangledrc in Git root) 88 * @param remoteName - Name of the remote to use 89 * @param cwd - Current working directory 90 */ 91export async function setLocalRemote( 92 remoteName: string, 93 cwd: string = process.cwd() 94): Promise<void> { 95 const gitRoot = await getGitRoot(cwd); 96 97 if (!gitRoot) { 98 throw new Error('Not in a Git repository. Cannot set local config.'); 99 } 100 101 const configPath = join(gitRoot, '.tangledrc'); 102 const config: TangledConfig = { remote: remoteName }; 103 104 try { 105 await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf-8'); 106 } catch (error) { 107 throw new Error( 108 `Failed to write local config: ${error instanceof Error ? error.message : 'Unknown error'}` 109 ); 110 } 111} 112 113/** 114 * Set the remote name in user config (~/.tangledrc) 115 * @param remoteName - Name of the remote to use 116 */ 117export async function setUserRemote(remoteName: string): Promise<void> { 118 const configPath = join(homedir(), '.tangledrc'); 119 const config: TangledConfig = { remote: remoteName }; 120 121 try { 122 // Ensure directory exists 123 await mkdir(dirname(configPath), { recursive: true }); 124 await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf-8'); 125 } catch (error) { 126 throw new Error( 127 `Failed to write user config: ${error instanceof Error ? error.message : 'Unknown error'}` 128 ); 129 } 130} 131 132/** 133 * Clear configured remote from local config 134 * @param cwd - Current working directory 135 */ 136export async function clearLocalRemote(cwd: string = process.cwd()): Promise<void> { 137 const gitRoot = await getGitRoot(cwd); 138 139 if (!gitRoot) { 140 throw new Error('Not in a Git repository. Cannot clear local config.'); 141 } 142 143 const configPath = join(gitRoot, '.tangledrc'); 144 145 try { 146 await unlink(configPath); 147 } catch (error) { 148 // If file doesn't exist, that's fine 149 if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { 150 throw new Error( 151 `Failed to delete local config: ${error instanceof Error ? error.message : 'Unknown error'}` 152 ); 153 } 154 } 155} 156 157/** 158 * Clear configured remote from user config 159 */ 160export async function clearUserRemote(): Promise<void> { 161 const configPath = join(homedir(), '.tangledrc'); 162 163 try { 164 await unlink(configPath); 165 } catch (error) { 166 // If file doesn't exist, that's fine 167 if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { 168 throw new Error( 169 `Failed to delete user config: ${error instanceof Error ? error.message : 'Unknown error'}` 170 ); 171 } 172 } 173}