swbase.bend

cswap-bend-cli@0.1.0.0 · 255 lines · 50 definitions

1import Base
2import cswap-bend-cli@0.1.0.0/json.bend as Json
3import cswap-bend-cli@0.1.0.0/store.bend as Store
4import cswap-bend-cli@0.1.0.0/store/base.bend as SBase
5import cswap-bend-cli@0.1.0.0/store/ops.bend as SOps
6import cswap-bend-cli@0.1.0.0/store/seq.bend as SSeq
7
8# Switch, part 1: error codes, results, and the edits to ~/.claude.json.
9
10# Error codes, by the reference's exception class.
11
12def e_config() -> U32:
13  1
14
15def e_validation() -> U32:
16  2
17
18def e_not_found() -> U32:
19  3
20
21# A switch that did not stick: a keychain login overrode the files.
22def e_overridden() -> U32:
23  20
24
25def error_type(+code: U32) -> String:
26  SBase.sel.s(U32.is_eq(code, 1), "ConfigError",
27  SBase.sel.s(U32.is_eq(code, 2), "ValidationError",
28  SBase.sel.s(U32.is_eq(code, 3), "AccountNotFoundError",
29  SBase.sel.s(U32.is_eq(code, 4), "SwitchError",
30  SBase.sel.s(U32.is_eq(code, 5), "CredentialReadError",
31  SBase.sel.s(U32.is_eq(code, 20), "live-login-overridden", "Error"))))))
32
33# oauthAccount replaced in place (or appended), every other key kept in order.
34def merge_oauth(+cfg: Json.Json, +oauth: Json.Json) -> Json.Json:
35  Json.set("oauthAccount", oauth, cfg)
36
37def approved_form(+key: String) -> String:
38  String.drop(String.trim(key), Nat.sub(String.length(String.trim(key)), 20n))
39
40def has_text(+xs: Json.Json, +t: String) -> Bool:
41  match xs:
42    case Json.JCons{Json.JStr{s}, rest}:
43      String.eq(s, t) || has_text(rest, t)
44    case Json.JCons{h, rest}:
45      has_text(rest, t)
46    case _:
47      False{}
48
49def snoc(+xs: Json.Json, +x: Json.Json) -> Json.Json:
50  match xs:
51    case Json.JCons{h, t}:
52      Json.JCons{h, snoc(t, x)}
53    case _:
54      Json.JCons{x, Json.JNil{}}
55
56def approved_list(+responses: Json.Json) -> Json.Json:
57  SSeq.unwrap_arr(SSeq.get_or("approved", responses, Json.JNull{}))
58
59def with_rejected(+has: Bool, +r: Json.Json) -> Json.Json:
60  match has:
61    case True{}:
62      r
63    case False{}:
64      Json.set("rejected", Json.JArr{Json.JNil{}}, r)
65
66def responses2(+j: Json.Json) -> Json.Json:
67  match j:
68    case Json.JObj{f}:
69      Json.JObj{f}
70    case _:
71      Json.JObj{Json.JNil{}}
72
73def responses_of(+cfg: Json.Json) -> Json.Json:
74  responses2(SSeq.get_or("customApiKeyResponses", cfg, Json.JNull{}))
75
76def add_approved(+items: Json.Json, +key20: String) -> Json.Json:
77  Store.sel.j(has_text(items, key20), items, snoc(items, Json.JStr{key20}))
78
79def is_some_json(+m: Maybe<&2, Json.Json>) -> Bool:
80  match m:
81    case Some{x}:
82      True{}
83    case None{}:
84      False{}
85
86def managed_responses(+responses: Json.Json, +key20: String) -> Json.Json:
87  with_rejected(is_some_json(Json.get("rejected", responses)), Json.set("approved", Json.JArr{add_approved(approved_list(responses), key20)}, responses))
88
89# A managed API key goes in as primaryApiKey and is approved by its last 20
90# characters (credentials.py _write_managed_credentials, file backend).
91def set_managed(+cfg: Json.Json, +key: String) -> Json.Json:
92  Json.set("primaryApiKey", Json.JStr{String.trim(key)},
93    Json.set("customApiKeyResponses", managed_responses(responses_of(cfg), approved_form(key)), cfg))
94
95def activate2(+api_key: Bool, +cfg: Json.Json, +oauth: Json.Json, +target_creds: String) -> Json.Json:
96  match api_key:
97    case True{}:
98      merge_oauth(set_managed(cfg, target_creds), oauth)
99    case False{}:
100      merge_oauth(Json.del("primaryApiKey", cfg), oauth)
101
102# The live config after activating a target: its oauthAccount, and primaryApiKey
103# either set (target is an API key) or dropped (target is OAuth).
104def activate_config(+cfg: Json.Json, +oauth: Json.Json, +target_creds: String) -> Json.Json:
105  activate2(SBase.looks_like_api_key(target_creds), cfg, oauth, target_creds)
106
107def num_json(+m: Maybe<&2, String>) -> Json.Json:
108  match m:
109    case Some{n}:
110      Json.JNum{n}
111    case None{}:
112      Json.JNull{}
113
114def ref(+num: Maybe<&2, String>, +email: String) -> Json.Json:
115  Json.JObj{Json.JField{"number", num_json(num), Json.JField{"email", Json.JStr{email}, Json.JNil{}}}}
116
117def strs_json(+xs: List<&2, String>) -> List<&2, Json.Json>:
118  match xs:
119    case Nil{}:
120      []
121    case Con{h, t}:
122      Json.JStr{h} <> strs_json(t)
123
124def result(+switched: Bool, +from: Json.Json, +to: Json.Json, +strategy: String, +reason: String, +message: String, +warnings: List<&2, String>) -> Json.Json:
125  Json.JObj{Json.JField{"schemaVersion", Json.JNum{"1"}, Json.JField{"switched", Json.JBool{switched}, Json.JField{"from", from,
126    Json.JField{"to", to, Json.JField{"strategy", Json.JStr{strategy}, Json.JField{"reason", Json.JStr{reason}, Json.JField{"message", Json.JStr{message},
127    Json.JField{"warnings", Json.arr(strs_json(warnings)), Json.JNil{}}}}}}}}}}
128
129# {"schemaVersion": 1, "error": {"type", "message"}}
130def error_envelope(+code: U32, +msg: String) -> Json.Json:
131  Json.JObj{Json.JField{"schemaVersion", Json.JNum{"1"}, Json.JField{"error",
132    Json.JObj{Json.JField{"type", Json.JStr{error_type(code)}, Json.JField{"message", Json.JStr{msg}, Json.JNil{}}}}, Json.JNil{}}}}
133
134def ref_text(+r: Json.Json) -> String:
135  "Account-" ++ SOps.field_json(r, "number") ++ " (" ++ SSeq.field_str(r, "email") ++ ")"
136
137def same_ref(+a: Json.Json, +b: Json.Json) -> Bool:
138  String.eq(SOps.field_json(a, "number"), SOps.field_json(b, "number")) && String.eq(SSeq.field_str(a, "email"), SSeq.field_str(b, "email"))
139
140def switched_msg(+sw: Bool, +to: Json.Json) -> String:
141  SBase.sel.s(sw, "Switched to ", "Already on ") ++ ref_text(to)
142
143def result_of_op4(+sw: Bool, +f: Json.Json, +t: Json.Json, +strategy: String, +warns: List<&2, String>) -> Json.Json:
144  result(sw, f, t, strategy, SBase.sel.s(sw, "switched", "already-active"), switched_msg(sw, t), warns)
145
146def texts_chain(+items: Json.Json) -> List<&2, String>:
147  match items:
148    case Json.JCons{h, t}:
149      SSeq.str_or(Json.as_string(h), "") <> texts_chain(t)
150    case _:
151      []
152
153def texts_of(+w: Json.Json) -> List<&2, String>:
154  match w:
155    case Json.JArr{items}:
156      texts_chain(items)
157    case _:
158      []
159
160def result_of_op3(+f: Json.Json, +t: Json.Json, +strategy: String, +extra: List<&2, String>, +op: Json.Json) -> Json.Json:
161  result_of_op4(Bool.not(same_ref(f, t)), f, t, strategy, List.append(&2, String, extra, texts_of(SSeq.get_or("warnings", op, Json.JNull{}))))
162
163def result_of_op2(+from: Maybe<&2, Json.Json>, +to: Maybe<&2, Json.Json>, +strategy: String, +extra: List<&2, String>, +op: Json.Json) -> Json.Json:
164  result_of_op3(SSeq.get_or("from", op, Json.JNull{}), SSeq.get_or("to", op, Json.JNull{}), strategy, extra, op)
165
166# `_switch_result_from_op`: the op is {"from", "to", "warnings"}.
167def result_of_op(+op: Json.Json, +strategy: String, +extra: List<&2, String>) -> Json.Json:
168  result_of_op2(Json.get("from", op), Json.get("to", op), strategy, extra, op)
169
170def text_or(+m: Maybe<&2, String>) -> String:
171  SSeq.str_or(m, "")
172
173def unwrap_json(+m: Maybe<&2, Json.Json>) -> Json.Json:
174  match m:
175    case Some{j}:
176      j
177    case None{}:
178      Json.JNull{}
179
180def is_some_str(+m: Maybe<&2, String>) -> Bool:
181  match m:
182    case Some{x}:
183      True{}
184    case None{}:
185      False{}
186
187def from_ref(+email: String, +slot: Maybe<&2, String>) -> Json.Json:
188  Store.sel.j(String.is_empty(email), Json.JNull{}, ref(slot, email))
189
190def op_json(+from: Json.Json, +to: Json.Json, +warns: List<&2, String>, +org: String) -> Json.Json:
191  Json.JObj{Json.JField{"from", from, Json.JField{"to", to, Json.JField{"warnings", Json.arr(strs_json(warns)),
192    Json.JField{"targetOrg", Json.JStr{org}, Json.JNil{}}}}}}
193
194def slot_strs(+xs: List<&2, U32>) -> List<&2, String>:
195  match xs:
196    case Nil{}:
197      []
198    case Con{h, t}:
199      U32.show(h) <> slot_strs(t)
200
201# The slots listed in `sequence`, in order, as slot-key strings.
202def slots_of(+seq: Json.Json) -> List<&2, String>:
203  slot_strs(SSeq.seq_nums(seq))
204
205def sel_ms(+c: Bool, +x: Maybe<&2, String>, +y: Maybe<&2, String>) -> Maybe<&2, String>:
206  match c:
207    case True{}:
208      x
209    case False{}:
210      y
211
212def override_msg(+target: String, +temail: String, +observed: String) -> String:
213  "live-login-overridden: after switching to Account-" ++ target ++ " (" ++ temail ++ ") the live login reads "
214    ++ SBase.sel.s(String.is_empty(observed), "no account", observed)
215    ++ "; a Claude Code keychain login is overriding cswap; remove the 'Claude Code-credentials' keychain item"
216
217def noop(+strategy: String, +reason: String, +message: String, +to: Json.Json, +warns: List<&2, String>) -> Json.Json:
218  result(False{}, to, to, strategy, reason, message, warns)
219
220def forced2(+rewrite: Bool, +r: Json.Json) -> Json.Json:
221  Store.sel.j(rewrite, Json.set("message", Json.JStr{"Activated " ++ ref_text(SSeq.get_or("to", r, Json.JNull{})) ++ " from stored backup"},
222    Json.set("reason", Json.JStr{"activated"}, r)), r)
223
224def is_true(+m: Maybe<&2, Json.Json>) -> Bool:
225  match m:
226    case Some{Json.JBool{True{}}}:
227      True{}
228    case _:
229      False{}
230
231def forced_result(+force: Bool, +r: Json.Json) -> Json.Json:
232  forced2(force && Bool.not(is_true(Json.get("switched", r))), r)
233
234def sel_ls(+c: Bool, +x: List<&2, String>, +y: List<&2, String>) -> List<&2, String>:
235  match c:
236    case True{}:
237      x
238    case False{}:
239      y
240
241# A JSON number as its text; None for anything else and for 0 (falsy).
242def num_str(+m: Maybe<&2, Json.Json>) -> Maybe<&2, String>:
243  match m:
244    case Some{Json.JNum{t}}:
245      sel_ms(String.eq(t, "0"), None{}, Some{t})
246    case _:
247      None{}
248
249def sel_s(+c: Bool, +x: String, +y: String) -> String:
250  SBase.sel.s(c, x, y)
251
252# `--model` limits ride on the result: {"models": [...], "modelSource": "cli"}.
253def with_models(+r: Json.Json, +models: List<&2, String>, +source: String) -> Json.Json:
254  Store.sel.j(List.is_empty(&2, String, models), r,
255    Json.set("modelSource", Json.JStr{source}, Json.set("models", Json.arr(strs_json(models)), r)))