1 import Base 2 import ./setload.bend as Setload 3 import ./collectrun.bend as Collectrun 4 import ./pollrow.bend as Pollrow 5 import ./store/io.bend as SIo 6 import ./store/base.bend as SBase 7 import ./store/seq.bend as SSeq 8 import ./json.bend as Json 9 import ./clock.bend as Clock 10 import ./act.bend as Act 11 import ./cmd.bend as Cmd 12 import ./view.bend as View 13 import ./collect.bend as Collect 14 import ./store.bend as Store 15 import ./render.bend as Render 16 import ./warn.bend as Warn 17 import ./clibase.bend as Clibase 18 19 def rows_json(as: List<&2, View.Acct>, +models: List<&2, String>, +now: U32, +off: U32) -> List<&2, Json.Json>: 20 match as: 21 case Nil{}: 22 [] 23 case Con{h, t}: 24 View.account_row(h, models, now, off) <> rows_json(t, models, now, off) 25 26 def active.pick(active: Bool, +num: U32, rest: Json.Json) -> Json.Json: 27 match active: 28 case True{}: 29 Json.JNum{U32.show(num)} 30 case False{}: 31 rest 32 33 def active_num(as: List<&2, View.Acct>) -> Json.Json: 34 match as: 35 case Nil{}: 36 Json.JNull{} 37 case Con{View.Acct{num, e, on, ou, active, al, dis, lg, sen, row}, t}: 38 active.pick(active, num, active_num(t)) 39 40 def strs_json(xs: List<&2, String>) -> List<&2, Json.Json>: 41 match xs: 42 case Nil{}: 43 [] 44 case Con{h, t}: 45 Json.JStr{h} <> strs_json(t) 46 47 def with_warn(+k: String, xs: List<&2, String>, j: Json.Json) -> Json.Json: 48 match xs: 49 case Nil{}: 50 j 51 case Con{h, t}: 52 Json.set(k, Json.arr(strs_json(h <> t)), j) 53 54 def list_payload(+as: List<&2, View.Acct>, models: List<&2, String>, +now: U32, +off: U32, dups: List<&2, String>, locks: List<&2, String>) -> Json.Json: 55 with_warn("lockstepUsageWarnings", locks, with_warn("duplicateAccountWarnings", dups, 56 Json.of_pairs([("schemaVersion", Json.JNum{"1"}), ("activeAccountNumber", active_num(as)), ("accounts", Json.arr(rows_json(as, models, now, off)))]))) 57 58 def lines.measured(has: Bool, +lg: Json.Json, +fetched: Maybe<&2, U32>, +now: U32, +off: U32, le: Maybe<&2, String>) -> List<&2, String>: 59 match has: 60 case True{}: 61 Render.measured(lg, fetched, Pollrow.age_of(fetched, now), now, off) 62 case False{}: 63 Render.unavailable(le) 64 65 def acct_lines2(sentinel: Maybe<&2, String>, +row: Pollrow.Row, +now: U32, +off: U32) -> List<&2, String>: 66 match sentinel row: 67 case Some{s} Pollrow.Row{fetched, lg, la, fl, le, bu, np, iv, l429, st, sfp, cu, hu}: 68 Render.sentinel_lines(s, lg, fetched, Pollrow.age_of(fetched, now), off) 69 case None{} Pollrow.Row{+fetched, lg, la, fl, le, bu, np, iv, l429, st, sfp, cu, hu}: 70 lines.measured(View.is_obj(lg), lg, fetched, now, off, le) 71 72 def acct_lines(+a: View.Acct, +now: U32, +off: U32) -> List<&2, String>: 73 match a: 74 case View.Acct{num, email, org_name, org_uuid, active, alias, disabled, login, sentinel, row}: 75 acct_lines2(sentinel, row, now, off) 76 77 def tag_of(+org_name: String) -> String: 78 Clock.sel.s(String.is_empty(org_name), "personal", org_name) 79 80 def head_line(+a: View.Acct) -> String: 81 match a: 82 case View.Acct{num, email, org_name, org_uuid, active, alias, disabled, login, sentinel, row}: 83 " " ++ U32.show(num) ++ ": " ++ Clock.sel.s(String.is_empty(alias), email, alias ++ " (" ++ email ++ ")") ++ " [" ++ tag_of(org_name) ++ "]" 84 ++ Clock.sel.s(active, " (active)", "") ++ Clock.sel.s(disabled, " (disabled)", "") 85 86 def indent5(ls: List<&2, String>) -> String: 87 match ls: 88 case Nil{}: 89 "" 90 case Con{h, t}: 91 " " ++ h ++ "\n" ++ indent5(t) 92 93 def acct_block(+a: View.Acct, +now: U32, +off: U32) -> String: 94 head_line(a) ++ "\n" ++ indent5(acct_lines(a, now, off)) 95 96 def blocks(as: List<&2, View.Acct>, +now: U32, +off: U32) -> String: 97 match as: 98 case Nil{}: 99 "" 100 case Con{h, Nil{}}: 101 acct_block(h, now, off) 102 case Con{h, Con{h2, t2}}: 103 acct_block(h, now, off) ++ "\n" ++ blocks(h2 <> t2, now, off) 104 105 def warn_lines(xs: List<&2, String>) -> String: 106 match xs: 107 case Nil{}: 108 "" 109 case Con{h, t}: 110 "Warning: " ++ h ++ "\n" ++ warn_lines(t) 111 112 def warn_gap(xs: List<&2, String>) -> String: 113 match xs: 114 case Nil{}: 115 "" 116 case Con{h, t}: 117 "\n" ++ warn_lines(h <> t) 118 119 def warn_block(dups: List<&2, String>, locks: List<&2, String>) -> String: 120 warn_gap(List.append(&2, String, dups, locks)) 121 122 def list_text(as: List<&2, View.Acct>, +now: U32, +off: U32, dups: List<&2, String>, locks: List<&2, String>) -> String: 123 "Accounts:\n" ++ blocks(as, now, off) ++ warn_block(dups, locks) 124 125 def list_out(+json: Bool, as: List<&2, View.Acct>, models: List<&2, String>, +now: U32, +off: U32, dups: List<&2, String>, locks: List<&2, String>) -> Cmd.Out: 126 match json: 127 case True{}: 128 Cmd.doc(list_payload(as, models, now, off, dups, locks)) 129 case False{}: 130 Cmd.Out{0, list_text(as, now, off, dups, locks), ""} 131 132 def empty_list(+json: Bool) -> Cmd.Out: 133 match json: 134 case True{}: 135 Cmd.doc(Json.of_pairs([("schemaVersion", Json.JNum{"1"}), ("activeAccountNumber", Json.JNull{}), ("accounts", Json.arr([]))])) 136 case False{}: 137 Cmd.ok("No accounts are managed yet.") 138 139 def settings_of(+ctx: SBase.Ctx) -> Act.of(Json.Json): 140 Act.lift(Json.Json, Setload.load(SBase.root_of(ctx))) 141 142 # One usage pass over `infos` with the models and threshold of the settings. 143 def usage_pass(+ctx: SBase.Ctx, infos: List<&2, Collect.Info>, tm: Clibase.Tm, +cfg: Json.Json) -> Act.of(List<&2, View.Acct>): 144 match tm: 145 case Clibase.Tm{now, off, tick}: 146 Collectrun.collect(ctx, infos, now, Setload.model(cfg), Setload.threshold_tenths(cfg), tick) 147 148 def list_finish(+json: Bool, +infos: List<&2, Collect.Info>, +accts: List<&2, View.Acct>, +seq: Json.Json, +cfg: Json.Json, tm: Clibase.Tm) -> Cmd.Out: 149 match tm: 150 case Clibase.Tm{+now, off, tick}: 151 list_out(json, accts, Setload.model(cfg), now, off, Warn.duplicates(infos, seq), Warn.lockstep(accts, Setload.model(cfg), now)) 152 153 def list_run(+ctx: SBase.Ctx, +seq: Json.Json, +json: Bool) -> Act.of(Cmd.Out): 154 do Clibase.ActM: 155 +tm : Clibase.Tm <- Clibase.times() 156 +cfg : Json.Json <- settings_of(ctx) 157 +infos : List<&2, Collect.Info> <- Collect.gather(ctx, seq) 158 +accts : List<&2, View.Acct> <- usage_pass(ctx, infos, tm, cfg) 159 return list_finish(json, infos, accts, seq, cfg, tm) 160 161 def list_seq(m: Maybe<&2, Json.Json>, +ctx: SBase.Ctx, +json: Bool) -> Act.of(Cmd.Out): 162 match m: 163 case None{}: 164 Clibase.done(empty_list(json)) 165 case Some{seq}: 166 list_run(ctx, seq, json) 167 168 def list_cmd(+ctx: SBase.Ctx, +json: Bool) -> Act.of(Cmd.Out): 169 Act.bind(Maybe<&2, Json.Json>, Cmd.Out, SIo.read_seq(SBase.root_of(ctx)), m => list_seq(m, ctx, json)) 170 171 def unmanaged(+json: Bool, +email: String) -> Cmd.Out: 172 match json: 173 case True{}: 174 Cmd.doc(Json.of_pairs([("schemaVersion", Json.JNum{"1"}), ("active", Json.of_pairs([("email", Json.JStr{email}), ("managed", Json.JBool{False{}})]))])) 175 case False{}: 176 Cmd.ok("Status: " ++ email ++ " (not managed)") 177 178 def logged_out(+json: Bool) -> Cmd.Out: 179 match json: 180 case True{}: 181 Cmd.doc(Json.of_pairs([("schemaVersion", Json.JNum{"1"}), ("active", Json.JNull{})])) 182 case False{}: 183 Cmd.ok("Status: No active Claude account") 184 185 def only.pick(a: Bool, i: Collect.Info, rest: List<&2, Collect.Info>) -> List<&2, Collect.Info>: 186 match a: 187 case True{}: 188 i <> rest 189 case False{}: 190 rest 191 192 def info_active(+i: Collect.Info) -> Bool: 193 match i: 194 case Collect.Info{num, email, org_name, org_uuid, alias, disabled, active, creds}: 195 active 196 197 def active_only(xs: List<&2, Collect.Info>) -> List<&2, Collect.Info>: 198 match xs: 199 case Nil{}: 200 [] 201 case Con{+i, t}: 202 only.pick(info_active(i), i, active_only(t)) 203 204 def status_row(+a: View.Acct, models: List<&2, String>, +now: U32, +off: U32) -> Json.Json: 205 Json.set("managed", Json.JBool{True{}}, Json.del("loginExpiresAt", Json.del("disabled", Json.del("active", View.account_row(a, models, now, off))))) 206 207 def total_of(+seq: Json.Json) -> Json.Json: 208 Json.JNum{Nat.show(List.length(&2, String, Json.keys(Maybe.default(&2, Json.Json, Json.get("accounts", seq), Json.JNull{}))))} 209 210 def total_text(j: Json.Json) -> String: 211 match j: 212 case Json.JNum{t}: 213 t 214 case _: 215 "0" 216 217 def indent2(ls: List<&2, String>) -> String: 218 match ls: 219 case Nil{}: 220 "" 221 case Con{h, t}: 222 " " ++ h ++ "\n" ++ indent2(t) 223 224 def status_text(+a: View.Acct, +total: Json.Json, +now: U32, +off: U32) -> String: 225 match a: 226 case View.Acct{num, email, org_name, org_uuid, active, alias, disabled, login, sentinel, row}: 227 "Status: Account-" ++ U32.show(num) ++ " (" ++ email ++ " [" ++ tag_of(org_name) ++ "])\n Total managed accounts: " ++ total_text(total) ++ "\n" 228 ++ indent2(acct_lines(a, now, off)) 229 230 def status_out2(+json: Bool, +a: View.Acct, models: List<&2, String>, +seq: Json.Json, +now: U32, +off: U32) -> Cmd.Out: 231 match json: 232 case True{}: 233 Cmd.doc(Json.of_pairs([("schemaVersion", Json.JNum{"1"}), ("active", status_row(a, models, now, off)), ("totalManagedAccounts", total_of(seq))])) 234 case False{}: 235 Cmd.Out{0, status_text(a, total_of(seq), now, off), ""} 236 237 def status_out(+json: Bool, as: List<&2, View.Acct>, models: List<&2, String>, +seq: Json.Json, +now: U32, +off: U32) -> Cmd.Out: 238 match as: 239 case Nil{}: 240 Cmd.fail("the active account has no record") 241 case Con{+a, t}: 242 status_out2(json, a, models, seq, now, off) 243 244 def status_finish(+json: Bool, accts: List<&2, View.Acct>, +seq: Json.Json, +cfg: Json.Json, tm: Clibase.Tm) -> Cmd.Out: 245 match tm: 246 case Clibase.Tm{now, off, tick}: 247 status_out(json, accts, Setload.model(cfg), seq, now, off) 248 249 def status_run(+ctx: SBase.Ctx, +seq: Json.Json, +json: Bool) -> Act.of(Cmd.Out): 250 do Clibase.ActM: 251 +tm : Clibase.Tm <- Clibase.times() 252 +cfg : Json.Json <- settings_of(ctx) 253 infos : List<&2, Collect.Info> <- Collect.gather(ctx, seq) 254 accts : List<&2, View.Acct> <- usage_pass(ctx, active_only(infos), tm, cfg) 255 return status_finish(json, accts, seq, cfg, tm) 256 257 def status_slot(m: Maybe<&2, String>, +ctx: SBase.Ctx, +seq: Json.Json, +json: Bool, +email: String) -> Act.of(Cmd.Out): 258 match m: 259 case None{}: 260 Clibase.done(unmanaged(json, email)) 261 case Some{slot}: 262 status_run(ctx, seq, json) 263 264 def status_seq(m: Maybe<&2, Json.Json>, +ctx: SBase.Ctx, +json: Bool, +id: Store.Ident) -> Act.of(Cmd.Out): 265 match m: 266 case None{}: 267 Clibase.done(unmanaged(json, Store.id_email(id))) 268 case Some{+seq}: 269 status_slot(SSeq.find_slot(seq, Store.id_email(id), Store.id_org(id)), ctx, seq, json, Store.id_email(id)) 270 271 def status_ident.m(c: Bool, +id: Store.Ident, +ctx: SBase.Ctx, +json: Bool) -> Act.of(Cmd.Out): 272 match c: 273 case True{}: 274 Clibase.done(logged_out(json)) 275 case False{}: 276 Act.bind(Maybe<&2, Json.Json>, Cmd.Out, SIo.read_seq(SBase.root_of(ctx)), m => status_seq(m, ctx, json, id)) 277 278 def status_ident(+id: Store.Ident, +ctx: SBase.Ctx, +json: Bool) -> Act.of(Cmd.Out): 279 status_ident.m(String.is_empty(Store.id_email(id)), id, ctx, json) 280 def status_cmd(+ctx: SBase.Ctx, +json: Bool) -> Act.of(Cmd.Out): 281 Act.bind(Store.Ident, Cmd.Out, Act.lift(Store.Ident, Collect.live_ident(ctx)), id => status_ident(id, ctx, json))