#!/usr/bin/env python3 """Build a device-local Surge profile. Never modify the managed source profile.""" import argparse import json import re from pathlib import Path def sections(text): result = {} current = None for line in text.splitlines(): match = re.fullmatch(r'\[([^]]+)\]', line.strip()) if match: current = match[1] result[current] = [] elif current is not None: result[current].append(line) return result def build(base, domains, enabled): if len(domains) != 25 or len(set(domains)) != 25: raise ValueError('FFSB scope changed; re-audit before applying') s = sections(base) for key in ['General', 'Proxy', 'Proxy Group', 'Rule', 'Host']: if key not in s: raise ValueError('Missing section: ' + key) groups = s['Proxy Group'] original = [x for x in groups if re.match(r'^(?:Claude|"?🦀 Claude"?)\s*=', x)] if len(original) != 1: raise ValueError('Expected one production Claude group') if original[0].startswith('Claude'): groups[groups.index(original[0])] = original[0].replace('Claude', '"🦀 Claude"', 1) groups.insert(0, 'Claude = fallback, ' + ('FFSB, ' if enabled else '') + '"🦀 Claude", interval=5') s['Proxy'].insert(0, 'FFSB = direct, interface=utun100, allow-other-interface=false, dns-follow-interface=true, test-url=https://api.ipify.org') # The domain union goes to the Surge fallback unless overridden by the exact FFSB scope. s['Rule'] = [re.sub(r',"?Claude"?(?=,|$)', ',"🦀 Claude"', line) for line in s['Rule']] scope = [('DOMAIN-WILDCARD' if d.startswith('*.') else 'DOMAIN') + ',' + d + ',Claude' for d in domains] outer = 'PROCESS-NAME,/Applications/FFSB.app/Contents/MacOS/ffsb-service,DIRECT' s['Rule'] = [outer] + scope + [line for line in s['Rule'] if line != outer] if enabled: s['Host'] = [d + ' = server:100.70.255.254' for d in domains] + s['Host'] for i, line in enumerate(s['General']): if re.match(r'^always-real-ip\s*=', line): s['General'][i] = line + ', ' + ', '.join(domains) break else: s['General'].append('always-real-ip = ' + ', '.join(domains)) return '# Local Claude overlay; source remains managed. Mode: ' + ('FFSB' if enabled else 'Surge') + '\n\n' + '\n\n'.join('['+key+']\n'+'\n'.join(lines) for key, lines in s.items()) + '\n' def main(): parser = argparse.ArgumentParser() parser.add_argument('base', type=Path) parser.add_argument('domains', type=Path) parser.add_argument('output', type=Path) parser.add_argument('--ffsb', action='store_true') args = parser.parse_args() args.output.write_text(build(args.base.read_text(), json.loads(args.domains.read_text()), args.ffsb)) if __name__ == '__main__': main()