store/base.bend
cswap-bend-cli@0.1.0.0 · 186 lines · 40 definitions
1import Base
2
3
4
5
6
7
8
9type Ctx is Data:
10 Ctx{home: String, config_home: String, global_base: String}
11
12def ctx_of2(+home: String, +dir: String, +unset: Bool) -> Ctx:
13 match unset:
14 case True{}:
15 Ctx{home, home ++ "/.claude", home}
16 case False{}:
17 Ctx{home, dir, dir}
18
19def ctx_of(+home: String, +claude_config_dir: String) -> Ctx:
20 ctx_of2(home, claude_config_dir, String.is_empty(claude_config_dir))
21
22def root_of(+ctx: Ctx) -> String:
23 match ctx:
24 case Ctx{home, c, g}:
25 home ++ "/.claude-swap-backup"
26
27def live_creds_path(+ctx: Ctx) -> String:
28 match ctx:
29 case Ctx{home, c, g}:
30 c ++ "/.credentials.json"
31
32def global_config_path(+ctx: Ctx) -> String:
33 match ctx:
34 case Ctx{home, c, g}:
35 g ++ "/.claude.json"
36
37def seq_path(+root: String) -> String:
38 root ++ "/sequence.json"
39
40def lock_path(+root: String) -> String:
41 root ++ "/.lock"
42
43def log_path(+root: String) -> String:
44 root ++ "/claude-swap.log"
45
46def creds_dir(+root: String) -> String:
47 root ++ "/credentials"
48
49def configs_dir(+root: String) -> String:
50 root ++ "/configs"
51
52def removed_dir(+root: String) -> String:
53 creds_dir(root) ++ "/.removed"
54
55def removed_configs_dir(+root: String) -> String:
56 configs_dir(root) ++ "/.removed"
57
58def enc_path(+root: String, +slot: String, +email: String) -> String:
59 creds_dir(root) ++ "/.creds-" ++ slot ++ "-" ++ email ++ ".enc"
60
61def prev_path(+root: String, +slot: String, +email: String) -> String:
62 enc_path(root, slot, email) ++ ".prev"
63
64def config_path(+root: String, +slot: String, +email: String) -> String:
65 configs_dir(root) ++ "/.claude-config-" ++ slot ++ "-" ++ email ++ ".json"
66
67
68def removed_enc_path(+root: String, +ts: String, +slot: String, +email: String) -> String:
69 removed_dir(root) ++ "/" ++ ts ++ "-" ++ slot ++ "-" ++ email ++ ".enc"
70
71def removed_config_path(+root: String, +ts: String, +slot: String, +email: String) -> String:
72 removed_configs_dir(root) ++ "/" ++ ts ++ "-" ++ slot ++ "-" ++ email ++ ".json"
73
74
75
76def sel.s(+c: Bool, +x: String, +y: String) -> String:
77 match c:
78 case True{}:
79 x
80 case False{}:
81 y
82
83def strip_chars.pick(+skip: Bool, +c: Char, +rest: String) -> String:
84 match skip:
85 case True{}:
86 rest
87 case False{}:
88 SCon{c, rest}
89
90
91def strip_chars(+s: String, +drop: String) -> String:
92 match s:
93 case SNil{}:
94 SNil{}
95 case SCon{+c, t}:
96 strip_chars.pick(String.contains(drop, SCon{c, SNil{}}), c, strip_chars(t, drop))
97
98
99def compact_ts(+iso: String) -> String:
100 strip_chars(iso, "-:")
101
102
103def log_stamp(+iso: String) -> String:
104 String.take(iso, 10n) ++ " " ++ String.take(String.drop(iso, 11n), 8n) ++ ",000"
105
106def log_line(+iso: String, +level: String, +msg: String) -> String:
107 log_stamp(iso) ++ " - " ++ level ++ " - " ++ msg ++ "\n"
108
109
110
111def is_oauth_token(+t: String) -> Bool:
112 String.starts_with(t, "sk-ant-oat") || String.starts_with(t, "sk-ant-ort")
113
114def not_(+b: Bool) -> Bool:
115 Bool.not(b)
116
117def looks_like_api_key2(+t: String) -> Bool:
118 not_(String.starts_with(t, "{")) && String.starts_with(t, "sk-ant-") && not_(is_oauth_token(t))
119
120
121def looks_like_api_key(+text: String) -> Bool:
122 looks_like_api_key2(String.trim(text))
123
124def email_char(+c: Char) -> Bool:
125 Char.is_digit(c) || Char.is_alpha(c) || Char.is_eq(c, '.') || Char.is_eq(c, '-') || Char.is_eq(c, '_') || Char.is_eq(c, '%') || Char.is_eq(c, '+')
126
127def domain_char(+c: Char) -> Bool:
128 Char.is_digit(c) || Char.is_alpha(c) || Char.is_eq(c, '.') || Char.is_eq(c, '-')
129
130def sel.b(+c: Bool, +x: Bool, +y: Bool) -> Bool:
131 match c:
132 case True{}:
133 x
134 case False{}:
135 y
136
137def all_chars(+s: String, +local: Bool) -> Bool:
138 match s:
139 case SNil{}:
140 True{}
141 case SCon{+c, t}:
142 sel.b(local, email_char(c), domain_char(c)) && all_chars(t, local)
143
144def all_alpha(+s: String) -> Bool:
145 match s:
146 case SNil{}:
147 True{}
148 case SCon{c, t}:
149 Char.is_alpha(c) && all_alpha(t)
150
151def nonempty(+s: String) -> Bool:
152 not_(String.is_empty(s))
153
154def valid_host2(+tld: Maybe<&2, String>, +n: Nat, +chars: Bool) -> Bool:
155 match tld:
156 case None{}:
157 False{}
158 case Some{t}:
159 chars && Nat.is_ge(n, 2n) && all_alpha(t) && Nat.is_ge(String.length(t), 2n)
160
161
162
163def valid_host(+segs: List<&2, String>, +host: String) -> Bool:
164 valid_host2(List.last(&2, String, segs), List.length(&2, String, segs), all_chars(host, False{}))
165
166def valid_email2(+parts: List<&2, String>) -> Bool:
167 match parts:
168 case Con{local, Con{host, Nil{}}}:
169 nonempty(local) && all_chars(local, True{}) && valid_host(String.split(host, '.'), host)
170 case _:
171 False{}
172
173
174def valid_email(+e: String) -> Bool:
175 valid_email2(String.split(e, '@'))
176
177def all_digits(+s: String) -> Bool:
178 match s:
179 case SNil{}:
180 True{}
181 case SCon{c, t}:
182 Char.is_digit(c) && all_digits(t)
183
184
185def alias_ok(+a: String) -> Bool:
186 nonempty(a) && not_(all_digits(a)) && not_(String.starts_with(a, "-"))