1use std::sync::Arc;
4
5use sim_kernel::{
6 AbiVersion, Args, CORE_FUNCTION_CLASS_ID, Callable, ClassRef, Cx, Error, Export, Expr, Lib,
7 LibManifest, LibTarget, Linker, LoadCx, Object, ObjectCompat, Result, Symbol, Value, Version,
8};
9
10pub struct AtelierCliLib;
12
13pub struct BrowseCliLib;
15
16impl Lib for AtelierCliLib {
17 fn manifest(&self) -> LibManifest {
18 cli_manifest("atelier", "cli/main/atelier")
19 }
20
21 fn load(&self, cx: &mut LoadCx, linker: &mut Linker<'_>) -> Result<()> {
22 register_cli_entrypoint(cx, linker, "atelier")
23 }
24}
25
26impl Lib for BrowseCliLib {
27 fn manifest(&self) -> LibManifest {
28 cli_manifest("browse", "cli/main/browse")
29 }
30
31 fn load(&self, cx: &mut LoadCx, linker: &mut Linker<'_>) -> Result<()> {
32 register_cli_entrypoint(cx, linker, "browse")
33 }
34}
35
36fn cli_manifest(id: &str, entrypoint: &str) -> LibManifest {
37 LibManifest {
38 id: Symbol::new(id),
39 version: Version(env!("CARGO_PKG_VERSION").to_owned()),
40 abi: AbiVersion { major: 0, minor: 1 },
41 target: LibTarget::HostRegistered,
42 requires: Vec::new(),
43 capabilities: Vec::new(),
44 exports: vec![Export::Function {
45 symbol: symbol_from_slash(entrypoint),
46 function_id: None,
47 }],
48 }
49}
50
51fn register_cli_entrypoint(
52 cx: &mut LoadCx,
53 linker: &mut Linker<'_>,
54 verb: &'static str,
55) -> Result<()> {
56 linker.function_value(
57 Symbol::qualified("cli", format!("main/{verb}")),
58 cx.factory()
59 .opaque(Arc::new(WebShellCliEntrypoint { verb }))?,
60 )?;
61 Ok(())
62}
63
64#[derive(Clone)]
65struct WebShellCliEntrypoint {
66 verb: &'static str,
67}
68
69impl Object for WebShellCliEntrypoint {
70 fn display(&self, _cx: &mut Cx) -> Result<String> {
71 Ok(format!("#<function cli/main/{}>", self.verb))
72 }
73
74 fn as_any(&self) -> &dyn std::any::Any {
75 self
76 }
77}
78
79impl ObjectCompat for WebShellCliEntrypoint {
80 fn class(&self, cx: &mut Cx) -> Result<ClassRef> {
81 if let Some(value) = cx
82 .registry()
83 .class_by_symbol(&Symbol::qualified("core", "Function"))
84 {
85 return Ok(value.clone());
86 }
87 cx.factory().class_stub(
88 CORE_FUNCTION_CLASS_ID,
89 Symbol::qualified("core", "Function"),
90 )
91 }
92
93 fn as_callable(&self) -> Option<&dyn Callable> {
94 Some(self)
95 }
96}
97
98impl Callable for WebShellCliEntrypoint {
99 fn call(&self, cx: &mut Cx, args: Args) -> Result<Value> {
100 verify_cli_envelope(cx, &args, self.verb)?;
101 cx.factory().bool(true)
102 }
103}
104
105fn verify_cli_envelope(cx: &mut Cx, args: &Args, verb: &str) -> Result<()> {
106 let envelope = args
107 .values()
108 .first()
109 .ok_or_else(|| Error::Eval(format!("cli/main/{verb} expects a CLI envelope")))?;
110 let envelope_verb = envelope_string_field(cx, envelope, "verb")?;
111 if envelope_verb != verb {
112 return Err(Error::Eval(format!(
113 "cli/main/{verb} received verb {envelope_verb}"
114 )));
115 }
116 let payload_args = envelope_args(cx, envelope)?;
117 if payload_args.first().map(String::as_str) != Some(verb) {
118 return Err(Error::Eval(format!(
119 "cli/main/{verb} expects the first payload argument to be {verb}"
120 )));
121 }
122 Ok(())
123}
124
125fn envelope_string_field(cx: &mut Cx, envelope: &Value, field: &str) -> Result<String> {
126 let Some(table) = envelope.object().as_table_impl() else {
127 return Err(Error::Eval("CLI envelope is not a table".to_owned()));
128 };
129 match table.get(cx, Symbol::new(field))?.object().as_expr(cx)? {
130 Expr::String(text) => Ok(text),
131 Expr::Nil => Err(Error::Eval(format!("CLI envelope field {field} is nil"))),
132 other => Err(Error::Eval(format!(
133 "CLI envelope field {field} is not a string: {other:?}"
134 ))),
135 }
136}
137
138fn envelope_args(cx: &mut Cx, envelope: &Value) -> Result<Vec<String>> {
139 let Some(table) = envelope.object().as_table_impl() else {
140 return Err(Error::Eval("CLI envelope is not a table".to_owned()));
141 };
142 let value = table.get(cx, Symbol::new("args"))?;
143 let Some(list) = value.object().as_list() else {
144 return Err(Error::Eval(
145 "CLI envelope field args is not a list".to_owned(),
146 ));
147 };
148 list.to_vec(cx, Some(64))?
149 .into_iter()
150 .map(|value| match value.object().as_expr(cx)? {
151 Expr::String(text) => Ok(text),
152 other => Err(Error::Eval(format!(
153 "CLI payload argument is not a string: {other:?}"
154 ))),
155 })
156 .collect()
157}
158
159fn symbol_from_slash(text: &str) -> Symbol {
160 match text.split_once('/') {
161 Some((head, tail)) => Symbol::qualified(head, tail),
162 None => Symbol::new(text),
163 }
164}