switch.bend
cswap-bend-cli@0.1.0.0 · 271 lines · 41 definitions
1import Base
2import cswap-bend-cli@0.1.0.0/json.bend as Json
3import cswap-bend-cli@0.1.0.0/act.bend as Act
4import cswap-bend-cli@0.1.0.0/store.bend as Store
5import cswap-bend-cli@0.1.0.0/swbase.bend as B
6import cswap-bend-cli@0.1.0.0/swpick.bend as P
7import cswap-bend-cli@0.1.0.0/swlive.bend as L
8import cswap-bend-cli@0.1.0.0/swrun.bend as R
9import cswap-bend-cli@0.1.0.0/store/base.bend as SBase
10import cswap-bend-cli@0.1.0.0/store/io.bend as SIo
11import cswap-bend-cli@0.1.0.0/store/ops.bend as SOps
12import cswap-bend-cli@0.1.0.0/store/seq.bend as SSeq
13
14
15
16
17
18def ActM(-A: Type) -> Type:
19 Act.of(A)
20
21def ActM.pure(-A: Type, x: A) -> Act.of(A):
22 Act.pure(A, x)
23
24def ActM.bind(-A: Type, -B: Type, m: Act.of(A), f: A -> Act.of(B)) -> Act.of(B):
25 Act.bind(A, B, m, f)
26
27def direct4(+already: Bool, +ctx: SBase.Ctx, +n: String, +force: Bool, +opts: L.Opts, +email: String) -> Act.of(Json.Json):
28 match already:
29 case True{}:
30 Act.pure(Json.Json, B.noop("direct", "already-active", "Already on Account-" ++ n ++ " (" ++ email ++ ")", B.ref(Some{n}, email), []))
31 case False{}:
32 do ActM<Json.Json>:
33 op : Json.Json <- R.do_switch(ctx, n, force, opts)
34 return B.forced_result(force, B.result_of_op(op, "direct", []))
35
36def direct3(+ctx: SBase.Ctx, +seq: Json.Json, +n: String, +force: Bool, +opts: L.Opts, +rec: Maybe<&2, Json.Json>) -> Act.of(Json.Json):
37 match rec:
38 case None{}:
39 Act.fail(Json.Json, B.e_not_found(), "Account-" ++ n ++ " does not exist")
40 case Some{r}:
41 do ActM<Json.Json>:
42 l : L.Live <- L.read_live(ctx)
43 direct4(Bool.not(force) && String.eq(SSeq.str_or(L.lv_slot(l, seq), ""), n), ctx, n, force, opts, SSeq.field_str(r, "email"))
44
45def direct2(+ctx: SBase.Ctx, +seq: Json.Json, +ident: String, +force: Bool, +opts: L.Opts, +r: SSeq.Resolved) -> Act.of(Json.Json):
46 match r:
47 case SSeq.RNone{}:
48 Act.fail(Json.Json, B.e_not_found(), "No account found with identifier: " ++ ident)
49 case SSeq.RAmb{msg}:
50 Act.fail(Json.Json, B.e_config(), msg)
51 case SSeq.RSlot{n}:
52 direct3(ctx, seq, n, force, opts, SSeq.slot_record(seq, n))
53
54def direct1(+ctx: SBase.Ctx, +ident: String, +force: Bool, +opts: L.Opts, +m: Maybe<&2, Json.Json>) -> Act.of(Json.Json):
55 match m:
56 case None{}:
57 Act.fail(Json.Json, B.e_config(), "No accounts are managed yet")
58 case Some{seq}:
59 do ActM<Json.Json>:
60 Act.guard(SOps.valid_ident(seq, ident), B.e_validation(), "Invalid account identifier: " ++ ident)
61 direct2(ctx, seq, ident, force, opts, SSeq.resolve(seq, ident))
62
63
64
65
66
67def switch_to(+ctx: SBase.Ctx, +ident: String, +force: Bool, +opts: L.Opts) -> Act.of(Json.Json):
68 do ActM<Json.Json>:
69 m : Maybe<&2, Json.Json> <- SIo.read_seq(SBase.root_of(ctx))
70 direct1(ctx, ident, force, opts, m)
71
72def is_disabled(+rec: Maybe<&2, Json.Json>) -> Bool:
73 match rec:
74 case None{}:
75 False{}
76 case Some{r}:
77 B.is_true(Json.get("disabled", r))
78
79def cand_of2(+slot: String, +rec: Maybe<&2, Json.Json>, +c: String, +cfg: Maybe<&2, String>, +usage: Json.Json, +models: List<&2, String>) -> P.Cand:
80 P.Cand{slot, is_disabled(rec), SBase.nonempty(c) && SBase.nonempty(B.text_or(cfg)), P.headroom(P.usage_row(usage, slot), models)}
81
82def cand_of(+root: String, +seq: Json.Json, +usage: Json.Json, +models: List<&2, String>, +slot: String) -> Act.of(P.Cand):
83 do ActM<P.Cand>:
84 c : String <- SIo.read_cred(root, slot, SIo.slot_email(seq, slot))
85 cfg : Maybe<&2, String> <- L.read_text_opt(SBase.config_path(root, slot, SIo.slot_email(seq, slot)))
86 return cand_of2(slot, SSeq.slot_record(seq, slot), c, cfg, usage, models)
87
88def cands_of(+root: String, +seq: Json.Json, +usage: Json.Json, +models: List<&2, String>, +slots: List<&2, String>) -> Act.of(List<&2, P.Cand>):
89 match slots:
90 case Nil{}:
91 Act.pure(List<&2, P.Cand>, [])
92 case Con{s, t}:
93 do ActM<List<&2, P.Cand>>:
94 c : P.Cand <- cand_of(root, seq, usage, models, s)
95 rest : List<&2, P.Cand> <- cands_of(root, seq, usage, models, t)
96 return c <> rest
97
98def drop_slot_str(+x: String, +xs: List<&2, String>) -> List<&2, String>:
99 match xs:
100 case Nil{}:
101 []
102 case Con{h, t}:
103 B.sel_ls(String.eq(h, x), drop_slot_str(x, t), h <> drop_slot_str(x, t))
104
105def usable(+cs: List<&2, P.Cand>) -> List<&2, P.Cand>:
106 match cs:
107 case Nil{}:
108 []
109 case Con{P.Cand{s, d, sw, h}, t}:
110 P.sel_lc(Bool.not(d) && sw, P.Cand{s, d, sw, h} <> usable(t), usable(t))
111
112def limits_label(+models: List<&2, String>) -> String:
113 SBase.sel.s(List.is_empty(&2, String, models), "5h/7d limit", "usage limits")
114
115def stay_msg(+cur: String) -> String:
116 "staying on Account-" ++ cur ++ "."
117
118def note_pick(+note: String, +cur: String, +curref: Json.Json, +models: List<&2, String>) -> Json.Json:
119 Store.sel.j(String.eq(note, "current-unavailable"), B.noop("best", "usage-unavailable", "Current account usage is unavailable — " ++ stay_msg(cur), curref, []),
120 Store.sel.j(String.eq(note, "no-comparison"), B.noop("best", "usage-unavailable", "No other account has usage data to compare — " ++ stay_msg(cur), curref, []),
121 Store.sel.j(String.eq(note, "incomplete-comparison"), B.noop("best", "usage-unavailable", "No account with known usage has more remaining quota; some usage is unavailable — " ++ stay_msg(cur), curref, []),
122 Store.sel.j(String.eq(note, "stay"), B.noop("best", "already-best", "Already on the account with the most remaining quota (Account-" ++ cur ++ ").", curref, []),
123 B.noop("best", "candidates-exhausted", "All accounts are at their " ++ limits_label(models) ++ " — " ++ stay_msg(cur), curref, [])))))
124
125def note_result(+note: String, +cur: String, +curref: Json.Json, +models: List<&2, String>) -> Json.Json:
126 note_pick(note, cur, curref, models)
127
128def best_out(o: P.BestOut, +ctx: SBase.Ctx, +cur: String, +curref: Json.Json, +models: List<&2, String>, +opts: L.Opts, fallthrough: Act.of(Json.Json)) -> Act.of(Json.Json):
129 match o:
130 case P.BPick{slot}:
131 do ActM<Json.Json>:
132 op : Json.Json <- R.do_switch(ctx, slot, False{}, opts)
133 return B.result_of_op(op, "best", [])
134 case P.BNote{+note}:
135 Act.cond(Json.Json, String.eq(note, "none"), fallthrough, Act.pure(Json.Json, note_result(note, cur, curref, models)))
136
137def best_pick(+usage: Json.Json, +cur: String, +models: List<&2, String>, +others: List<&2, P.Cand>) -> P.BestOut:
138 P.select_best(P.headroom(P.usage_row(usage, cur), models), usable(others))
139
140def best_run(+ctx: SBase.Ctx, +root: String, +seq: Json.Json, +cur: String, +curref: Json.Json, +usage: Json.Json, +models: List<&2, String>, +opts: L.Opts, fallthrough: Act.of(Json.Json)) -> Act.of(Json.Json):
141 do ActM<Json.Json>:
142 others : List<&2, P.Cand> <- cands_of(root, seq, usage, models, drop_slot_str(cur, B.slots_of(seq)))
143 best_out(best_pick(usage, cur, models, others), ctx, cur, curref, models, opts, fallthrough)
144
145def rotate_do(+ctx: SBase.Ctx, +slot: String, +label: String, +warns: List<&2, String>, +opts: L.Opts) -> Act.of(Json.Json):
146 do ActM<Json.Json>:
147 op : Json.Json <- R.do_switch(ctx, slot, False{}, opts)
148 return B.result_of_op(op, label, warns)
149
150def rotate_out(+s: P.Sel, +ctx: SBase.Ctx, +cur: String, +curref: Json.Json, +label: String, +models: List<&2, String>, +opts: L.Opts) -> Act.of(Json.Json):
151 match s:
152 case P.SelOk{slot, warns}:
153 Act.cond(Json.Json, String.eq(slot, cur),
154 Act.pure(Json.Json, B.noop(label, "already-active", "Already on " ++ B.ref_text(curref), curref, warns)),
155 rotate_do(ctx, slot, label, warns, opts))
156 case P.SelNone{warns, exhausted}:
157 Act.pure(Json.Json, Store.sel.j(exhausted,
158 B.noop(label, "candidates-exhausted", "All other accounts are at their " ++ limits_label(models) ++ " — " ++ stay_msg(cur), curref, warns),
159 B.noop(label, "no-valid-target", "No other accounts have valid stored credentials/config.", curref, warns)))
160
161def rotate_run2(+anchor: Maybe<&2, String>, +ctx: SBase.Ctx, +root: String, +seq: Json.Json, +cur: String, +curref: Json.Json, +usage: Json.Json, +label: String, +models: List<&2, String>, +opts: L.Opts) -> Act.of(Json.Json):
162 do ActM<Json.Json>:
163 cs : List<&2, P.Cand> <- cands_of(root, seq, usage, models, P.rotated(B.slots_of(seq), anchor))
164 rotate_out(P.select_next(cs, String.eq(label, "next-available")), ctx, cur, curref, label, models, opts)
165
166def rotate_run(+ctx: SBase.Ctx, +root: String, +seq: Json.Json, +cur: String, +curref: Json.Json, +usage: Json.Json, +label: String, +models: List<&2, String>, +opts: L.Opts) -> Act.of(Json.Json):
167 rotate_run2(B.sel_ms(String.eq(label, "next-available"), Some{cur}, B.num_str(Json.get("activeAccountNumber", seq))), ctx, root, seq, cur, curref, usage, label, models, opts)
168
169def auto4(+strategy: String, +ctx: SBase.Ctx, +root: String, +seq: Json.Json, +cur: String, +curref: Json.Json, +usage: Json.Json, +label: String, +models: List<&2, String>, +opts: L.Opts) -> Act.of(Json.Json):
170 Act.cond(Json.Json, String.eq(strategy, "best"),
171 best_run(ctx, root, seq, cur, curref, usage, models, opts, rotate_run(ctx, root, seq, cur, curref, usage, label, models, opts)),
172 rotate_run(ctx, root, seq, cur, curref, usage, label, models, opts))
173
174def usage_json(+u: Maybe<&2, String>) -> Json.Json:
175 Store.sel.j(B.is_some_json(L.parse_text(u)), B.unwrap_json(L.parse_text(u)), Json.JNull{})
176
177def auto3(+strategy: String, +ctx: SBase.Ctx, +seq: Json.Json, +cur: String, +email: String, +label: String, +models: List<&2, String>, +opts: L.Opts) -> Act.of(Json.Json):
178 do ActM<Json.Json>:
179 +u : Maybe<&2, String> <- L.read_text_opt(SBase.root_of(ctx) ++ "/cache/usage.json")
180 auto4(strategy, ctx, SBase.root_of(ctx), seq, cur, B.ref(Some{cur}, email), usage_json(u), label, models, opts)
181
182def auto2(+strategy: String, +ctx: SBase.Ctx, +seq: Json.Json, +l: L.Live, +cur: Maybe<&2, String>, +label: String, +models: List<&2, String>, +opts: L.Opts) -> Act.of(Json.Json):
183 match cur:
184 case None{}:
185 Act.pure(Json.Json, B.noop(label, "unmanaged-account", "Active account is not managed; run cswap --add-account", B.ref(None{}, L.lv_email(l)), []))
186 case Some{c}:
187 Act.cond(Json.Json, Nat.is_lt(List.length(&2, String, B.slots_of(seq)), 2n),
188 Act.pure(Json.Json, B.noop(label, "only-one-account", "Only one account is managed. Add more accounts to switch between.", B.ref(Some{c}, L.lv_email(l)), [])),
189 auto3(strategy, ctx, seq, c, L.lv_email(l), label, models, opts))
190
191def preferred2(+active: Maybe<&2, String>, +slots: List<&2, String>) -> Maybe<&2, String>:
192 match active:
193 case Some{a}:
194 Some{a}
195 case None{}:
196 List.head(&2, String, slots)
197
198
199def preferred(+seq: Json.Json) -> Maybe<&2, String>:
200 preferred2(B.num_str(Json.get("activeAccountNumber", seq)), B.slots_of(seq))
201
202def reason_of(+c: P.Cand) -> String:
203 match c:
204 case P.Cand{s, d, sw, h}:
205 SBase.sel.s(d, "(disabled)", "(no stored credentials/config)")
206
207def any_switchable(+cs: List<&2, P.Cand>) -> Bool:
208 match cs:
209 case Nil{}:
210 False{}
211 case Con{P.Cand{s, d, sw, h}, t}:
212 sw || any_switchable(t)
213
214def fallback_out(+s: P.Sel, +all_cs: List<&2, P.Cand>, +ctx: SBase.Ctx, +warn: String, +label: String, +opts: L.Opts) -> Act.of(Json.Json):
215 match s:
216 case P.SelOk{slot, w}:
217 rotate_do(ctx, slot, label, [warn], opts)
218 case P.SelNone{w, e}:
219 Act.fail(Json.Json, B.e_config(), SBase.sel.s(any_switchable(all_cs), "No accounts remain in rotation. Re-enable one with: cswap enable <num|email>", "No managed accounts have valid stored credentials/config. Re-add a slot with: cswap --add-account --slot <number>"))
220
221def logged_out3(+c: P.Cand, +rest: List<&2, P.Cand>, +ctx: SBase.Ctx, +label: String, +opts: L.Opts) -> Act.of(Json.Json):
222 match c:
223 case P.Cand{slot, d, sw, h}:
224 Act.cond(Json.Json, Bool.not(d) && sw,
225 rotate_do(ctx, slot, label, [], opts),
226 fallback_out(P.select_next(rest, False{}), Con{c, rest}, ctx, "Skipped Account-" ++ slot ++ " " ++ reason_of(c), label, opts))
227
228def logged_out2(+cs: List<&2, P.Cand>, +ctx: SBase.Ctx, +label: String, +opts: L.Opts) -> Act.of(Json.Json):
229 match cs:
230 case Nil{}:
231 Act.fail(Json.Json, B.e_config(), "No accounts are managed yet")
232 case Con{c, rest}:
233 logged_out3(c, rest, ctx, label, opts)
234
235def logged_out4(+m: Maybe<&2, String>, +ctx: SBase.Ctx, +seq: Json.Json, +label: String, +opts: L.Opts) -> Act.of(Json.Json):
236 match m:
237 case None{}:
238 Act.fail(Json.Json, B.e_config(), "No accounts are managed yet")
239 case Some{t}:
240 do ActM<Json.Json>:
241 cs : List<&2, P.Cand> <- cands_of(SBase.root_of(ctx), seq, Json.JNull{}, [], Con{t, drop_slot_str(t, B.slots_of(seq))})
242 logged_out2(cs, ctx, label, opts)
243
244def logged_out(+ctx: SBase.Ctx, +seq: Json.Json, +label: String, +opts: L.Opts) -> Act.of(Json.Json):
245 logged_out4(preferred(seq), ctx, seq, label, opts)
246
247def auto_branch(+strategy: String, +ctx: SBase.Ctx, +seq: Json.Json, +l: L.Live, +label: String, +models: List<&2, String>, +opts: L.Opts) -> Act.of(Json.Json):
248 Act.cond(Json.Json, String.is_empty(L.lv_email(l)), logged_out(ctx, seq, label, opts), auto2(strategy, ctx, seq, l, L.lv_slot(l, seq), label, models, opts))
249
250def auto1(+strategy: String, +ctx: SBase.Ctx, +label: String, +models: List<&2, String>, +opts: L.Opts, +m: Maybe<&2, Json.Json>) -> Act.of(Json.Json):
251 match m:
252 case None{}:
253 Act.fail(Json.Json, B.e_config(), "No accounts are managed yet")
254 case Some{seq}:
255 do ActM<Json.Json>:
256 +l : L.Live <- L.read_live(ctx)
257 auto_branch(strategy, ctx, seq, l, label, models, opts)
258
259
260
261
262
263
264
265
266def switch_auto(+ctx: SBase.Ctx, +strategy: String, +models: List<&2, String>, +opts: L.Opts) -> Act.of(Json.Json):
267 do ActM<Json.Json>:
268 m : Maybe<&2, Json.Json> <- SIo.read_seq(SBase.root_of(ctx))
269 auto1(strategy, ctx, B.sel_s(String.eq(strategy, "best") || String.eq(strategy, "next-available"), strategy, "rotation"), B.sel_ls(String.eq(strategy, "best") || String.eq(strategy, "next-available"), models, []), opts, m)
270
271