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! :)
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}