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 * 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}