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 204 lines 6.1 kB view raw
1import { mkdir, readFile, unlink, writeFile } from 'node:fs/promises'; 2import { homedir } from 'node:os'; 3import { dirname, join } from 'node:path'; 4import { Command } from 'commander'; 5import { simpleGit } from 'simple-git'; 6import { loadConfig, type TangledConfig } from '../lib/config.js'; 7 8/** 9 * Get Git root directory 10 */ 11async function getGitRoot(cwd: string = process.cwd()): Promise<string | null> { 12 try { 13 const git = simpleGit(cwd); 14 const isRepo = await git.checkIsRepo(); 15 if (!isRepo) { 16 return null; 17 } 18 const root = await git.revparse(['--show-toplevel']); 19 return root.trim(); 20 } catch { 21 return null; 22 } 23} 24 25/** 26 * Set a config value 27 */ 28async function setConfigValue(key: string, value: string, global: boolean): Promise<void> { 29 const configPath = global 30 ? join(homedir(), '.tangledrc') 31 : join((await getGitRoot()) || process.cwd(), '.tangledrc'); 32 33 if (!global) { 34 const gitRoot = await getGitRoot(); 35 if (!gitRoot) { 36 throw new Error('Not in a Git repository. Use --global or run from a Git repository.'); 37 } 38 } 39 40 // Load existing config 41 let config: TangledConfig = {}; 42 try { 43 const content = await readFile(configPath, 'utf-8'); 44 config = JSON.parse(content); 45 } catch { 46 // Config doesn't exist yet, start with empty object 47 } 48 49 // Set the value 50 config[key as keyof TangledConfig] = value as never; 51 52 // Write updated config 53 await mkdir(dirname(configPath), { recursive: true }); 54 await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf-8'); 55} 56 57/** 58 * Unset a config value 59 */ 60async function unsetConfigValue(key: string, global: boolean): Promise<void> { 61 const configPath = global 62 ? join(homedir(), '.tangledrc') 63 : join((await getGitRoot()) || process.cwd(), '.tangledrc'); 64 65 if (!global) { 66 const gitRoot = await getGitRoot(); 67 if (!gitRoot) { 68 throw new Error('Not in a Git repository. Use --global or run from a Git repository.'); 69 } 70 } 71 72 // Load existing config 73 let config: TangledConfig = {}; 74 try { 75 const content = await readFile(configPath, 'utf-8'); 76 config = JSON.parse(content); 77 } catch { 78 // Config doesn't exist, nothing to unset 79 return; 80 } 81 82 // Remove the key 83 delete config[key as keyof TangledConfig]; 84 85 // If config is now empty, delete the file 86 if (Object.keys(config).length === 0) { 87 try { 88 await unlink(configPath); 89 } catch { 90 // File might not exist 91 } 92 } else { 93 // Write updated config 94 await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf-8'); 95 } 96} 97 98/** 99 * Available configuration keys with their descriptions 100 */ 101const AVAILABLE_KEYS: Record<string, string> = { 102 remote: 'Default Git remote to use when multiple tangled.org remotes exist', 103}; 104 105/** 106 * Create the config command for managing Tangled CLI configuration 107 */ 108export function createConfigCommand(): Command { 109 const config = new Command('config'); 110 config.description('Manage Tangled CLI configuration'); 111 112 // List available config keys 113 config 114 .command('list') 115 .description('List all available configuration keys') 116 .action(async () => { 117 try { 118 const cfg = await loadConfig(); 119 120 console.log('Available configuration keys:\n'); 121 for (const [key, description] of Object.entries(AVAILABLE_KEYS)) { 122 const value = cfg[key as keyof TangledConfig]; 123 const status = value ? `"${value}"` : '(not set)'; 124 console.log(` ${key}`); 125 console.log(` ${description}`); 126 console.log(` Current value: ${status}\n`); 127 } 128 } catch (error) { 129 console.error( 130 `Failed to list config: ${error instanceof Error ? error.message : 'Unknown error'}` 131 ); 132 process.exit(1); 133 } 134 }); 135 136 // Get current config 137 config 138 .command('get [key]') 139 .description('Get configuration value (defaults to all)') 140 .action(async (key?: string) => { 141 try { 142 const cfg = await loadConfig(); 143 144 if (!key) { 145 // Show all config values 146 const keys = Object.keys(cfg) as Array<keyof TangledConfig>; 147 if (keys.length === 0) { 148 console.log('No configuration set'); 149 return; 150 } 151 for (const k of keys) { 152 console.log(`${k} = ${cfg[k] || '(not set)'}`); 153 } 154 } else { 155 // Show specific key 156 const value = cfg[key as keyof TangledConfig]; 157 console.log(`${key} = ${value || '(not set)'}`); 158 } 159 } catch (error) { 160 console.error( 161 `Failed to get config: ${error instanceof Error ? error.message : 'Unknown error'}` 162 ); 163 process.exit(1); 164 } 165 }); 166 167 // Set config value 168 config 169 .command('set <key> <value>') 170 .option('-g, --global', 'Save to user config instead of local') 171 .description('Set a configuration value') 172 .action(async (key: string, value: string, options: { global?: boolean }) => { 173 try { 174 await setConfigValue(key, value, options.global ?? false); 175 const scope = options.global ? 'user config (~/.tangledrc)' : 'local config (.tangledrc)'; 176 console.log(`✓ Set ${key} to "${value}" in ${scope}`); 177 } catch (error) { 178 console.error( 179 `Failed to set config: ${error instanceof Error ? error.message : 'Unknown error'}` 180 ); 181 process.exit(1); 182 } 183 }); 184 185 // Unset config value 186 config 187 .command('unset <key>') 188 .option('-g, --global', 'Clear from user config instead of local') 189 .description('Clear a configuration value') 190 .action(async (key: string, options: { global?: boolean }) => { 191 try { 192 await unsetConfigValue(key, options.global ?? false); 193 const scope = options.global ? 'user config' : 'local config'; 194 console.log(`✓ Cleared ${key} from ${scope}`); 195 } catch (error) { 196 console.error( 197 `Failed to clear config: ${error instanceof Error ? error.message : 'Unknown error'}` 198 ); 199 process.exit(1); 200 } 201 }); 202 203 return config; 204}