profcmd.bend

cswap-bend-cli@0.1.0.0 · 349 lines · 55 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/cmd.bend as Cmd
5import cswap-bend-cli@0.1.0.0/fs.bend as Fs
6import cswap-bend-cli@0.1.0.0/setload.bend as Setload
7import cswap-bend-cli@0.1.0.0/setwrite.bend as Setwrite
8import cswap-bend-cli@0.1.0.0/setcmd.bend as Setcmd
9import cswap-bend-cli@0.1.0.0/profile.bend as P
10import cswap-bend-cli@0.1.0.0/policy/safe.bend as SF
11import cswap-bend-cli@0.1.0.0/daemon/config.bend as Cfg
12import cswap-bend-cli@0.1.0.0/profview.bend as PV
13
14# `cswap profile list|show [NAME]|use NAME|set NAME KEY VALUE|remove NAME`,
15# each with `--json`. The profiles live in settings.json (src/profile.bend);
16# a write changes only `profiles` / `active_profile` and keeps every other key.
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
27# ----------------------------------------------------------------- commands
28
29# What a command settled on: an answer without writing, or the settings to
30# write and the answer to give once they are.
31type Plan2 is Data:
32  Answer{out: Cmd.Out}
33  Wr{raw: Json.Json, out: Cmd.Out}
34
35def doc(pairs: List<(String & Json.Json)>) -> Cmd.Out:
36  Cmd.doc(Json.of_pairs(Con{("schemaVersion", Json.JNum{"1"}), pairs}))
37
38def err(+json: Bool, +msg: String) -> Cmd.Out:
39  Cmd.failed(json, "ConfigError", msg)
40
41def unknown_msg(+name: String) -> String:
42  "unknown profile '" ++ name ++ "'; `cswap profile list` shows the profiles"
43
44def active_json(+m: Maybe<&2, String>) -> Maybe<&2, Json.Json>:
45  match m:
46    case Some{s}:
47      Some{Json.JStr{s}}
48    case None{}:
49      None{}
50
51def list_doc(+raw: Json.Json) -> Cmd.Out:
52  doc([("active", PV.opt_json(active_json(P.active(raw)))), ("profiles", Json.arr(PV.prof_jsons(raw, Cfg.from_settings(raw), P.names(raw))))])
53
54def list_out(+json: Bool, +raw: Json.Json) -> Cmd.Out:
55  match json:
56    case True{}:
57      list_doc(raw)
58    case False{}:
59      Cmd.ok(PV.table(PV.rows_of(raw, Cfg.from_settings(raw), P.names(raw))))
60
61def yes_no(+b: Bool) -> String:
62  match b:
63    case True{}:
64      "yes"
65    case False{}:
66      "no"
67
68def show_text2(+r: PV.Row) -> String:
69  match r:
70    case PV.Row{m, n, s, t, ms, c, i, src}:
71      "name:       " ++ n ++ "\nstrategy:   " ++ s ++ "\nthreshold:  " ++ t ++ "\nmodels:     " ++ ms ++ "\ncooldown_s: " ++ c ++ "\ninterval_s: " ++ i ++ "\nsource:     " ++ src ++ "\nactive:     " ++ yes_no(String.eq(m, "*"))
72
73def show_text(+raw: Json.Json, +cfg: Cfg.Cfg, +p: P.Prof) -> String:
74  show_text2(PV.row_of(raw, cfg, p))
75
76def show_out(+json: Bool, +raw: Json.Json, +p: P.Prof) -> Cmd.Out:
77  match json:
78    case True{}:
79      doc([("profile", PV.prof_json(raw, Cfg.from_settings(raw), p))])
80    case False{}:
81      Cmd.ok(show_text(raw, Cfg.from_settings(raw), p))
82
83def show_found(+json: Bool, +raw: Json.Json, +name: String, +m: Maybe<&2, P.Prof>) -> Cmd.Out:
84  match m:
85    case None{}:
86      err(json, unknown_msg(name))
87    case Some{p}:
88      show_out(json, raw, p)
89
90def show_name(+raw: Json.Json, +n: Maybe<&2, String>) -> String:
91  match n:
92    case Some{x}:
93      x
94    case None{}:
95      Maybe.default(&2, String, P.active(raw), "default")
96
97def show_out2(+json: Bool, +raw: Json.Json, +name: String) -> Cmd.Out:
98  show_found(json, raw, name, P.find(raw, name))
99
100def run.list(+root: String, +json: Bool) -> IO(Cmd.Out):
101  IO.bind(Json.Json, Cmd.Out, Setload.load_raw(root), raw => IO.pure(Cmd.Out, list_out(json, raw)))
102
103def show_run(+json: Bool, +n: Maybe<&2, String>, +raw: Json.Json) -> IO(Cmd.Out):
104  IO.pure(Cmd.Out, show_out2(json, raw, show_name(raw, n)))
105
106def run.show(+root: String, +json: Bool, +n: Maybe<&2, String>) -> IO(Cmd.Out):
107  IO.bind(Json.Json, Cmd.Out, Setload.load_raw(root), raw => show_run(json, n, raw))
108
109# ---- use
110
111def use_ok(+json: Bool, +raw: Json.Json, +name: String, +p: P.Prof) -> Cmd.Out:
112  match json:
113    case True{}:
114      doc([("activeProfile", Json.JStr{name}), ("profile", PV.prof_json(raw, Cfg.from_settings(raw), p))])
115    case False{}:
116      Cmd.ok("active profile: " ++ name ++ " (" ++ SF.name(P.algo_base(p)) ++ ")")
117
118def use_plan2(+m: Maybe<&2, P.Prof>, +json: Bool, +raw: Json.Json, +name: String) -> Plan2:
119  match m:
120    case None{}:
121      Answer{err(json, unknown_msg(name))}
122    case Some{p}:
123      Wr{P.use(raw, name), use_ok(json, P.use(raw, name), name, p)}
124
125def use_plan(+json: Bool, +raw: Json.Json, +name: String) -> Plan2:
126  use_plan2(P.find(raw, name), json, raw, name)
127
128# ---- set
129
130def set_ok(+json: Bool, +raw: Json.Json, +name: String, +key: String, +value: String, +p: P.Prof) -> Cmd.Out:
131  match json:
132    case True{}:
133      doc([("profile", PV.prof_json(raw, Cfg.from_settings(raw), p))])
134    case False{}:
135      Cmd.ok("profile " ++ name ++ ": " ++ key ++ " = " ++ value)
136
137def set_plan3(+u: P.Upd, +json: Bool, +raw: Json.Json, +name: String, +key: String, +value: String) -> Plan2:
138  match u:
139    case P.UBad{m}:
140      Answer{err(json, m)}
141    case P.UOk{p}:
142      Wr{P.store(raw, p), set_ok(json, P.store(raw, p), name, key, value, p)}
143
144def set_base(+m: Maybe<&2, P.Prof>, +name: String) -> P.Prof:
145  Maybe.default(&2, P.Prof, m, P.bare(name, SF.ABest{}))
146
147def set_plan2(+valid: Bool, +json: Bool, +raw: Json.Json, +name: String, +key: String, +value: String) -> Plan2:
148  match valid:
149    case False{}:
150      Answer{err(json, "invalid profile name '" ++ name ++ "': use letters, digits, '-' and '_', starting with a letter or digit")}
151    case True{}:
152      set_plan3(P.update(set_base(P.find(raw, name), name), key, value), json, raw, name, key, value)
153
154def set_plan1(+is_default: Bool, +json: Bool, +raw: Json.Json, +name: String, +key: String, +value: String) -> Plan2:
155  match is_default:
156    case True{}:
157      Answer{err(json, "profile 'default' follows the autoswitch settings and cannot be changed; create another profile, or use `cswap config set`")}
158    case False{}:
159      set_plan2(P.valid_name(name), json, raw, name, key, value)
160
161def set_plan(+json: Bool, +raw: Json.Json, +name: String, +key: String, +value: String) -> Plan2:
162  set_plan1(String.eq(name, "default"), json, raw, name, key, value)
163
164# ---- remove
165
166def gone_note(+was_active: Bool) -> String:
167  match was_active:
168    case True{}:
169      "\nactive profile cleared (back to default)"
170    case False{}:
171      ""
172
173def gone_ok(+json: Bool, +raw: Json.Json, +name: String, +was_active: Bool) -> Cmd.Out:
174  match json:
175    case True{}:
176      doc([("removed", Json.JStr{name}), ("activeProfile", PV.opt_json(active_json(P.active(raw))))])
177    case False{}:
178      Cmd.ok("removed profile " ++ name ++ gone_note(was_active))
179
180def gone_missing(+builtin: Bool, +name: String) -> String:
181  match builtin:
182    case True{}:
183      "profile '" ++ name ++ "' is built in and has no stored override to remove"
184    case False{}:
185      unknown_msg(name)
186
187def gone_plan3(+stored: Bool, +json: Bool, +raw: Json.Json, +name: String) -> Plan2:
188  match stored:
189    case True{}:
190      Wr{P.remove(raw, name), gone_ok(json, P.remove(raw, name), name, String.eq(Maybe.default(&2, String, P.active(raw), ""), name))}
191    case False{}:
192      Answer{err(json, gone_missing(P.is_builtin(name), name))}
193
194def gone_plan1(+is_default: Bool, +json: Bool, +raw: Json.Json, +name: String) -> Plan2:
195  match is_default:
196    case True{}:
197      Answer{err(json, "profile 'default' is built in and cannot be removed")}
198    case False{}:
199      gone_plan3(P.has_stored(raw, name), json, raw, name)
200
201def gone_plan(+json: Bool, +raw: Json.Json, +name: String) -> Plan2:
202  gone_plan1(String.eq(name, "default"), json, raw, name)
203
204# ---- running a plan
205
206def finish(+root: String, +w: Plan2) -> Act.of(Cmd.Out):
207  match w:
208    case Answer{o}:
209      Act.pure(Cmd.Out, o)
210    case Wr{raw, o}:
211      do ActM<Cmd.Out>:
212        u : Unit <- Setcmd.write_settings(root, raw)
213        Act.pure(Cmd.Out, o)
214
215def edit(+root: String, +plan: Plan2) -> IO(Cmd.Out):
216  IO.bind(Fs.Res(Cmd.Out), Cmd.Out, finish(root, plan), r => IO.pure(Cmd.Out, Setcmd.fin(r)))
217
218def use.act(+root: String, +json: Bool, +name: String) -> Act.of(Plan2):
219  do ActM<Plan2>:
220    +raw : Json.Json <- Setcmd.load_raw_w(root)
221    Act.pure(Plan2, use_plan(json, raw, name))
222
223def set.act(+root: String, +json: Bool, +name: String, +key: String, +value: String) -> Act.of(Plan2):
224  do ActM<Plan2>:
225    +raw : Json.Json <- Setcmd.load_raw_w(root)
226    Act.pure(Plan2, set_plan(json, raw, name, key, value))
227
228def gone.act(+root: String, +json: Bool, +name: String) -> Act.of(Plan2):
229  do ActM<Plan2>:
230    +raw : Json.Json <- Setcmd.load_raw_w(root)
231    Act.pure(Plan2, gone_plan(json, raw, name))
232
233# Read settings.json strictly (a torn file is an error, never replaced), plan, write.
234def planned(+root: String, r: Fs.Res(Plan2)) -> IO(Cmd.Out):
235  match r:
236    case Fail{e}:
237      IO.pure(Cmd.Out, Cmd.fail(Setcmd.err.msg(e)))
238    case Done{w}:
239      edit(root, w)
240
241def run.use(+root: String, +json: Bool, +name: String) -> IO(Cmd.Out):
242  IO.bind(Fs.Res(Plan2), Cmd.Out, use.act(root, json, name), r => planned(root, r))
243
244def run.set(+root: String, +json: Bool, +name: String, +key: String, +value: String) -> IO(Cmd.Out):
245  IO.bind(Fs.Res(Plan2), Cmd.Out, set.act(root, json, name, key, value), r => planned(root, r))
246
247def run.gone(+root: String, +json: Bool, +name: String) -> IO(Cmd.Out):
248  IO.bind(Fs.Res(Plan2), Cmd.Out, gone.act(root, json, name), r => planned(root, r))
249
250# ----------------------------------------------------------------- argv
251
252type Plan is Data:
253  PList{}
254  PShow{name: Maybe<&2, String>}
255  PUse{name: String}
256  PSet{name: String, key: String, value: String}
257  PRemove{name: String}
258  PBad{msg: String}
259
260def join_sp(xs: List<String>) -> String:
261  match xs:
262    case Nil{}:
263      ""
264    case Con{h, Nil{}}:
265      h
266    case Con{h, t}:
267      h ++ " " ++ join_sp(t)
268
269def plan.none(rest: List<String>, +p: Plan) -> Plan:
270  match rest:
271    case Nil{}:
272      p
273    case o:
274      PBad{"unrecognized arguments: " ++ join_sp(rest)}
275
276def plan.show(rest: List<String>) -> Plan:
277  match rest:
278    case Nil{}:
279      PShow{None{}}
280    case Con{n, t}:
281      plan.none(t, PShow{Some{n}})
282
283def plan.use(rest: List<String>) -> Plan:
284  match rest:
285    case Nil{}:
286      PBad{"the following arguments are required: NAME"}
287    case Con{n, t}:
288      plan.none(t, PUse{n})
289
290def plan.gone(rest: List<String>) -> Plan:
291  match rest:
292    case Nil{}:
293      PBad{"the following arguments are required: NAME"}
294    case Con{n, t}:
295      plan.none(t, PRemove{n})
296
297def plan.set(rest: List<String>) -> Plan:
298  match rest:
299    case Nil{}:
300      PBad{"the following arguments are required: NAME, KEY, VALUE"}
301    case Con{n, Nil{}}:
302      PBad{"the following arguments are required: KEY, VALUE"}
303    case Con{n, Con{k, Nil{}}}:
304      PBad{"the following arguments are required: VALUE"}
305    case Con{n, Con{k, Con{v, t}}}:
306      plan.none(t, PSet{n, k, v})
307
308def plan.verb(+l: Bool, +sh: Bool, +u: Bool, +st: Bool, +dl: Bool, +a: String, rest: List<String>) -> Plan:
309  match l sh u st dl:
310    case True{} _ _ _ _:
311      plan.none(rest, PList{})
312    case False{} True{} _ _ _:
313      plan.show(rest)
314    case False{} False{} True{} _ _:
315      plan.use(rest)
316    case False{} False{} False{} True{} _:
317      plan.set(rest)
318    case False{} False{} False{} False{} True{}:
319      plan.gone(rest)
320    case _ _ _ _ _:
321      PBad{"argument action: invalid choice: '" ++ a ++ "' (choose from 'list', 'show', 'use', 'set', 'remove')"}
322
323# `profile [list | show [NAME] | use NAME | set NAME KEY VALUE | remove NAME]`, flags removed.
324def plan.of(args: List<String>) -> Plan:
325  match args:
326    case Nil{}:
327      PList{}
328    case Con{+a, rest}:
329      plan.verb(String.eq(a, "list") || String.eq(a, "ls"), String.eq(a, "show"), String.eq(a, "use"), String.eq(a, "set"), String.eq(a, "remove"), a, rest)
330
331def run(+root: String, +p: Plan, +json: Bool) -> IO(Cmd.Out):
332  match p:
333    case PList{}:
334      run.list(root, json)
335    case PShow{n}:
336      run.show(root, json, n)
337    case PUse{n}:
338      run.use(root, json, n)
339    case PSet{n, k, v}:
340      run.set(root, json, n, k, v)
341    case PRemove{n}:
342      run.gone(root, json, n)
343    case PBad{msg}:
344      IO.pure(Cmd.Out, Cmd.Out{2, "", "cswap profile: error: " ++ msg ++ "\n"})
345
346# `cswap profile ARGS...`: `args` are the words after `profile` with the
347# `--json` / `--debug` flags already taken off, `json` the --json flag.
348def profile(+root: String, args: List<String>, +json: Bool) -> IO(Cmd.Out):
349  run(root, plan.of(args), json)