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 * Git utilities for parsing and validating tangled.org remote URLs
3 */
4
5import { isValidHandle, isValidTangledDid } from './validation.js';
6
7export interface ParsedTangledRemote {
8 owner: string;
9 ownerType: 'did' | 'handle';
10 name: string;
11 protocol: 'ssh' | 'https';
12}
13
14/**
15 * Check if a Git remote URL is a tangled.org URL
16 * @param url - Git remote URL
17 * @returns true if URL points to tangled.org
18 */
19export function isTangledRemote(url: string): boolean {
20 // Match tangled.org in SSH or HTTPS URLs
21 return (
22 url.includes('tangled.org') &&
23 (url.startsWith('git@tangled.org:') ||
24 url.startsWith('ssh://git@tangled.org') ||
25 url.startsWith('https://tangled.org'))
26 );
27}
28
29/**
30 * Parse a tangled.org Git remote URL to extract owner and repo name
31 * @param url - Git remote URL
32 * @returns Parsed remote info or null if not a valid tangled URL
33 */
34export function parseTangledRemote(url: string): ParsedTangledRemote | null {
35 if (!isTangledRemote(url)) {
36 return null;
37 }
38
39 let path: string;
40 let protocol: 'ssh' | 'https';
41
42 // Parse based on protocol
43 if (url.startsWith('https://tangled.org')) {
44 // HTTPS: https://tangled.org/owner/repo
45 protocol = 'https';
46 path = url.replace(/^https:\/\/tangled\.org\//, '');
47 } else if (url.startsWith('ssh://git@tangled.org')) {
48 // SSH with ssh:// prefix: ssh://git@tangled.org/owner/repo.git
49 protocol = 'ssh';
50 path = url.replace(/^ssh:\/\/git@tangled\.org\//, '');
51 } else if (url.startsWith('git@tangled.org:')) {
52 // SSH shorthand: git@tangled.org:owner/repo.git
53 protocol = 'ssh';
54 path = url.replace(/^git@tangled\.org:/, '');
55 } else {
56 return null;
57 }
58
59 // Remove trailing slashes
60 path = path.replace(/\/+$/, '');
61
62 // Remove .git extension if present
63 path = path.replace(/\.git$/, '');
64
65 // Split path into owner and repo name
66 const parts = path.split('/');
67 if (parts.length < 2) {
68 return null;
69 }
70
71 const owner = parts[0];
72 const name = parts[1];
73
74 // Validate that we have both parts
75 if (!owner || !name) {
76 return null;
77 }
78
79 // Determine owner type based on format
80 let ownerType: 'did' | 'handle';
81 if (owner.startsWith('did:plc:')) {
82 ownerType = 'did';
83 // Validate DID format
84 if (!isValidTangledDid(owner)) {
85 return null;
86 }
87 } else {
88 ownerType = 'handle';
89 // Validate handle format
90 if (!isValidHandle(owner)) {
91 return null;
92 }
93 }
94
95 return {
96 owner,
97 ownerType,
98 name,
99 protocol,
100 };
101}