1use std::path::{Path, PathBuf};
2
3use anyhow::anyhow;
4use clap::{Arg, Command};
5use rquickjs::CatchResultExt;
6
7use crate::{
8 attach_client::client_main,
9 daemon::{
10 lockfile, socket::connect_client_socket, spawn::spawn_server_daemon,
11 },
12 dekit::{rpc_client::rpc_request, server::run_server},
13 js::js_vm::JsVm,
14 lualib::init_std,
15 protocol::{CltToSrv, DkRequest, DkResponse, SrvToClt},
16};
17
18fn print_task_list(resp: DkResponse) {
19 match resp {
20 DkResponse::TaskList(tasks) => {
21 if tasks.is_empty() {
22 println!("No tasks.");
23 } else {
24 for t in &tasks {
25 println!("{}\t{}", t.path, t.status);
26 }
27 }
28 }
29 DkResponse::Error(e) => eprintln!("Error: {}", e),
30 _ => eprintln!("Unexpected response"),
31 }
32}
33
34pub async fn dekit_main() -> anyhow::Result<()> {
35 println!("* Welcome to dekit — playground for future features *\n");
36
37 let cmd = clap::command!()
38 .subcommands([
39 Command::new("attach"),
40 Command::new("up"),
41 Command::new("down"),
42 Command::new("spawn")
43 .about("Create and start a new task at the given path")
44 .arg(
45 Arg::new("path")
46 .long("path")
47 .required(true)
48 .help("Task path (e.g. /services/web)"),
49 )
50 .arg(
51 Arg::new("cwd")
52 .long("cwd")
53 .help("Working directory for the process"),
54 )
55 .arg(
56 Arg::new("cmd")
57 .required(true)
58 .num_args(1..)
59 .last(true)
60 .help("Command to run"),
61 ),
62 Command::new("ls")
63 .about("List tasks")
64 .arg(Arg::new("glob").help("Optional glob pattern")),
65 Command::new("start")
66 .about("Start a stopped task")
67 .arg(Arg::new("path").required(true).help("Task path")),
68 Command::new("stop")
69 .about("Gracefully stop a task")
70 .arg(Arg::new("path").required(true).help("Task path")),
71 Command::new("kill")
72 .about("Force kill a task")
73 .arg(Arg::new("path").required(true).help("Task path")),
74 Command::new("restart")
75 .about("Restart a task")
76 .arg(Arg::new("path").required(true).help("Task path")),
77 Command::new("screen")
78 .about("Print the current screen of a task")
79 .arg(Arg::new("path").required(true).help("Task path")),
80 Command::new("server").subcommands([
81 Command::new("run")
82 .arg(
83 Arg::new("dir")
84 .long("dir")
85 .required(true)
86 .help("Working directory this daemon manages"),
87 )
88 .arg(
89 Arg::new("log-level")
90 .long("log-level")
91 .help("Diagnostic log level (off|error|warn|info|debug|trace, or env_logger spec). Falls back to $DK_LOG, $RUST_LOG, then 'error' (release) or 'trace' (debug)."),
92 ),
93 Command::new("start")
94 .about("Start the daemon for the current directory"),
95 Command::new("stop").about("Stop the daemon for the current directory"),
96 Command::new("status")
97 .about("Show daemon status for the current directory"),
98 Command::new("list").about("List all daemons on this machine"),
99 Command::new("clean").about("Remove stale lock files"),
100 ]),
101 ])
102 .arg(
103 Arg::new("files")
104 .action(clap::ArgAction::Append)
105 .trailing_var_arg(true),
106 );
107 let matches = cmd.get_matches();
108
109 match matches.subcommand() {
110 Some(("attach", _sub_m)) => {
111 let working_dir = std::env::current_dir()?;
112 let (sender, receiver) =
113 connect_client_socket::<CltToSrv, SrvToClt>(&working_dir, false)
114 .await?;
115 client_main(sender, receiver).await?;
116 }
117 Some(("spawn", sub_m)) => {
118 let working_dir = std::env::current_dir()?;
119 let path = sub_m.get_one::<String>("path").unwrap().clone();
120 let cwd = sub_m.get_one::<String>("cwd").cloned();
121 let cmd: Vec<String> =
122 sub_m.get_many::<String>("cmd").unwrap().cloned().collect();
123 let resp =
124 rpc_request(&working_dir, DkRequest::Spawn { path, cmd, cwd }, true)
125 .await?;
126 match resp {
127 DkResponse::Ok => println!("Spawned."),
128 DkResponse::Error(e) => eprintln!("Error: {}", e),
129 _ => eprintln!("Unexpected response"),
130 }
131 }
132 Some(("ls", sub_m)) => {
133 let working_dir = std::env::current_dir()?;
134 let glob = sub_m.get_one::<String>("glob").cloned();
135 let resp =
136 rpc_request(&working_dir, DkRequest::Ls { glob }, false).await?;
137 print_task_list(resp);
138 }
139 Some(("start", sub_m)) => {
140 let working_dir = std::env::current_dir()?;
141 let path = sub_m.get_one::<String>("path").unwrap().clone();
142 let resp =
143 rpc_request(&working_dir, DkRequest::Start { path }, true).await?;
144 match resp {
145 DkResponse::Ok => println!("Started."),
146 DkResponse::Error(e) => eprintln!("Error: {}", e),
147 _ => eprintln!("Unexpected response"),
148 }
149 }
150 Some(("stop", sub_m)) => {
151 let working_dir = std::env::current_dir()?;
152 let path = sub_m.get_one::<String>("path").unwrap().clone();
153 let resp =
154 rpc_request(&working_dir, DkRequest::Stop { path }, false).await?;
155 match resp {
156 DkResponse::Ok => println!("Stopped."),
157 DkResponse::Error(e) => eprintln!("Error: {}", e),
158 _ => eprintln!("Unexpected response"),
159 }
160 }
161 Some(("kill", sub_m)) => {
162 let working_dir = std::env::current_dir()?;
163 let path = sub_m.get_one::<String>("path").unwrap().clone();
164 let resp =
165 rpc_request(&working_dir, DkRequest::Kill { path }, false).await?;
166 match resp {
167 DkResponse::Ok => println!("Killed."),
168 DkResponse::Error(e) => eprintln!("Error: {}", e),
169 _ => eprintln!("Unexpected response"),
170 }
171 }
172 Some(("restart", sub_m)) => {
173 let working_dir = std::env::current_dir()?;
174 let path = sub_m.get_one::<String>("path").unwrap().clone();
175 let resp =
176 rpc_request(&working_dir, DkRequest::Restart { path }, true).await?;
177 match resp {
178 DkResponse::Ok => println!("Restarted."),
179 DkResponse::Error(e) => eprintln!("Error: {}", e),
180 _ => eprintln!("Unexpected response"),
181 }
182 }
183 Some(("screen", sub_m)) => {
184 let working_dir = std::env::current_dir()?;
185 let path = sub_m.get_one::<String>("path").unwrap().clone();
186 let resp =
187 rpc_request(&working_dir, DkRequest::Screen { path }, false).await?;
188 match resp {
189 DkResponse::Screen(Some(content)) => {
190 print!("{}", content);
191 print!("\x1b[0m\n");
193 }
194 DkResponse::Screen(None) => {
195 eprintln!("No screen content for this task.");
196 }
197 DkResponse::Error(e) => eprintln!("Error: {}", e),
198 _ => eprintln!("Unexpected response"),
199 }
200 }
201 Some(("up", _sub_m)) => {
202 let working_dir = std::env::current_dir()?;
203 let resp =
204 rpc_request(&working_dir, DkRequest::Ls { glob: None }, true).await?;
205 print_task_list(resp);
206 }
207 Some(("down", _sub_m)) => {
208 let working_dir = std::env::current_dir()?;
209 lockfile::stop_daemon(&working_dir)?;
210 println!("Daemon stopped.");
211 }
212 Some(("server", sub_m)) => match sub_m.subcommand() {
213 Some(("run", run_m)) => {
214 let dir = run_m.get_one::<String>("dir").unwrap();
215 let log_level =
216 run_m.get_one::<String>("log-level").map(String::as_str);
217 run_server(PathBuf::from(dir), log_level).await?;
218 }
219 Some(("start", _sub_m)) => {
220 let working_dir = std::env::current_dir()?;
221 if let Some(info) = lockfile::get_daemon_status(&working_dir)? {
223 if info.is_running {
224 println!("Daemon already running (pid={}).", info.contents.pid);
225 return Ok(());
226 }
227 lockfile::cleanup_stale(&working_dir)?;
229 }
230 spawn_server_daemon(&working_dir)?;
231 println!("Daemon started for {}.", working_dir.display());
232 }
233 Some(("stop", _sub_m)) => {
234 let working_dir = std::env::current_dir()?;
235 lockfile::stop_daemon(&working_dir)?;
236 println!("Daemon stopped.");
237 }
238 Some(("status", _sub_m)) => {
239 let working_dir = std::env::current_dir()?;
240 match lockfile::get_daemon_status(&working_dir)? {
241 Some(info) => {
242 let status = if info.is_running { "running" } else { "stale" };
243 println!(
244 "[{}] pid={} socket={} version={}",
245 status,
246 info.contents.pid,
247 info.contents.socket,
248 info.contents.version,
249 );
250 }
251 None => {
252 println!("No daemon for this directory.");
253 }
254 }
255 }
256 Some(("list", _sub_m)) => {
257 let daemons = lockfile::list_daemons()?;
258 if daemons.is_empty() {
259 println!("No daemons found.");
260 } else {
261 for d in &daemons {
262 let status = if d.is_running { "running" } else { "stale" };
263 println!(
264 "[{}] pid={} dir={} socket={} version={}",
265 status,
266 d.contents.pid,
267 d.contents.working_dir,
268 d.contents.socket,
269 d.contents.version,
270 );
271 }
272 }
273 }
274 Some(("clean", _sub_m)) => {
275 let count = lockfile::cleanup_all_stale()?;
276 println!("Removed {} stale lock file(s).", count);
277 }
278 _ => {
279 println!("Expected more arguments after `dk server`");
280 }
281 },
282 Some((arg, _sub_m)) => {
283 println!("Unknown: {}", arg);
284 }
285 None => {
286 let paths = matches
287 .get_many::<String>("files")
288 .map(|p| p.collect::<Vec<_>>())
289 .unwrap_or_default();
290
291 if let Some(first) = paths.first() {
292 if first.ends_with(".lua") {
294 let src = std::fs::read_to_string(first)?;
295
296 let lua = mlua::Lua::new();
297 let cancel = tokio_util::sync::CancellationToken::new();
298 lua.set_app_data(cancel.clone());
299 lua
300 .globals()
301 .set("std", init_std(&lua).map_err(|e| anyhow!("{}", e))?)
302 .map_err(|e| anyhow!("{}", e))?;
303
304 let chunk = lua.load(src.clone());
305 let f: mlua::Function = chunk.eval().map_err(|e| anyhow!("{}", e))?;
306 let r = f
307 .call_async::<mlua::Value>(())
308 .await
309 .map_err(|e| anyhow!("{}", e))?;
310 println!("-> {:?}", r);
311 cancel.cancel();
312 }
313 else if first.ends_with(".js") {
315 let src = std::fs::read_to_string(first)?;
316
317 let vm = JsVm::new().await?;
318 let root =
319 vm.eval_file(Path::new("dekit.js"), src.as_bytes()).await?;
320
321 let r: anyhow::Result<()> =
322 rquickjs::async_with!(vm.context => |ctx| {
323 run_module_main(&ctx, &root).await
324 })
325 .await;
326 r?;
327 }
328 } else {
329 let working_dir = std::env::current_dir()?;
332 let (sender, receiver) =
333 connect_client_socket::<CltToSrv, SrvToClt>(&working_dir, true)
334 .await?;
335 client_main(sender, receiver).await?;
336 }
337 }
338 }
339
340 Ok(())
341}
342
343async fn run_module_main(
344 ctx: &rquickjs::Ctx<'_>,
345 root: &rquickjs::Persistent<rquickjs::Object<'static>>,
346) -> anyhow::Result<()> {
347 let m = map_js_error(
348 ctx,
349 root.clone().restore(ctx),
350 "Failed to restore module namespace",
351 )?;
352 let main = map_js_error(
353 ctx,
354 m.get::<_, rquickjs::Value>("main"),
355 "Failed to read exported `main`",
356 )?;
357
358 let val = match main.type_of() {
359 rquickjs::Type::Constructor => map_js_error(
360 ctx,
361 main
362 .into_constructor()
363 .expect("Type checked as constructor")
364 .call::<_, rquickjs::Value>(()),
365 "Error while calling exported constructor `main`",
366 )?,
367 rquickjs::Type::Function => map_js_error(
368 ctx,
369 main
370 .into_function()
371 .expect("Type checked as function")
372 .call(()),
373 "Error while calling exported function `main`",
374 )?,
375 t => anyhow::bail!("Exported `main` is not a function ({}).", t.as_str()),
376 };
377
378 let val = if let Some(promise) = val.clone().into_promise() {
379 map_js_error(
380 ctx,
381 promise.into_future::<rquickjs::Value<'_>>().await,
382 "Unhandled rejection in exported `main`",
383 )?
384 } else {
385 val
386 };
387
388 println!("-> {:?}", val);
389 Ok(())
390}
391
392fn map_js_error<T>(
393 ctx: &rquickjs::Ctx<'_>,
394 result: rquickjs::Result<T>,
395 scope: &str,
396) -> anyhow::Result<T> {
397 result.catch(ctx).map_err(|err| anyhow!("{scope}:\n{err}"))
398}