#[cfg(not(target_family = "wasm"))]
mod clipboard;
#[cfg(feature = "quic")]
mod quic;
mod repl;
#[cfg(not(target_family = "wasm"))]
mod tui;
const USAGE: &str = "\
ikigai — resource-resolution REPL
usage:
ikigai start the interactive REPL (full-screen on a terminal)
ikigai --plain force the line REPL (also used automatically when piped)
ikigai --demo mount the interactive runbook (urn:runbook:*); off by default
ikigai --connect [<target>] attach the REPL to a kernel server (a Unix path, or quic://host:port)
ikigai serve [<target>] run a kernel server (a Unix socket path, or quic://addr to bind)
ikigai cert generate create the pinned QUIC certificates in your config dir
ikigai -c '<command>' ... run command(s) non-interactively, then exit
ikigai -h | --help show this help
QUIC: --server-cert/--server-key name the server's identity, --client-cert/--client-key the client's
inside the REPL: source, describe, help, quit (type `help` for details)";
use ikigai_engine::Engine;
#[derive(Default)]
struct Certs {
server_cert: Option<String>,
server_key: Option<String>,
client_cert: Option<String>,
client_key: Option<String>,
}
enum Mode {
Repl(ReplArgs),
Serve {
target: Option<String>,
certs: Certs,
},
CertGenerate {
force: bool,
},
}
#[derive(Default)]
struct ReplArgs {
plain: bool,
demo: bool,
commands: Vec<String>,
connect: Option<Option<String>>,
certs: Certs,
}
fn is_quic(target: &str) -> bool {
target.starts_with("quic://")
}
fn cert_flag(
arg: &str,
argv: &mut impl Iterator<Item = String>,
certs: &mut Certs,
) -> Result<bool, String> {
let slot = match arg {
"--server-cert" => &mut certs.server_cert,
"--server-key" => &mut certs.server_key,
"--client-cert" => &mut certs.client_cert,
"--client-key" => &mut certs.client_key,
_ => return Ok(false),
};
*slot = Some(
argv.next()
.ok_or_else(|| format!("{arg} requires a path"))?,
);
Ok(true)
}
fn parse_args() -> Result<Option<Mode>, String> {
let mut argv = std::env::args().skip(1).peekable();
if argv.peek().map(String::as_str) == Some("cert") {
argv.next();
return match argv.next().as_deref() {
Some("generate") => {
let mut force = false;
for arg in argv {
match arg.as_str() {
"--force" => force = true,
other => {
return Err(format!("unknown argument after `cert generate`: {other}"))
}
}
}
Ok(Some(Mode::CertGenerate { force }))
}
Some(other) => Err(format!("unknown `cert` subcommand: {other}")),
None => Err("usage: `ikigai cert generate`".to_string()),
};
}
if argv.peek().map(String::as_str) == Some("serve") {
argv.next();
let mut target = None;
let mut certs = Certs::default();
while let Some(arg) = argv.next() {
if cert_flag(&arg, &mut argv, &mut certs)? {
continue;
}
if arg.starts_with('-') {
return Err(format!("unknown argument: {arg}"));
} else if target.is_none() {
target = Some(arg);
} else {
return Err(format!("unexpected argument after `serve`: {arg}"));
}
}
return Ok(Some(Mode::Serve { target, certs }));
}
let mut repl = ReplArgs::default();
while let Some(arg) = argv.next() {
if cert_flag(&arg, &mut argv, &mut repl.certs)? {
continue;
}
match arg.as_str() {
"-h" | "--help" => return Ok(None),
"--plain" => repl.plain = true,
"--demo" => repl.demo = true,
"--connect" => {
let target = match argv.peek() {
Some(next) if !next.starts_with('-') => argv.next(),
_ => None,
};
repl.connect = Some(target);
}
"-c" | "--command" => {
let command = argv
.next()
.ok_or_else(|| format!("{arg} requires a command argument"))?;
repl.commands.push(command);
}
other => return Err(format!("unknown argument: {other}")),
}
}
Ok(Some(Mode::Repl(repl)))
}
#[cfg(feature = "embedded")]
fn main() {
let mode = match parse_args() {
Ok(Some(mode)) => mode,
Ok(None) => {
println!("{USAGE}");
return;
}
Err(e) => {
eprintln!("ikigai: {e}\n\n{USAGE}");
std::process::exit(2);
}
};
match mode {
Mode::CertGenerate { force } => cert_generate(force),
Mode::Serve { target, certs } => match target.as_deref() {
Some(t) if is_quic(t) => serve_quic(t, &certs),
_ => serve_ipc(target),
},
Mode::Repl(args) => {
if args.demo {
ikigai_embedded::demo_flag().store(true, std::sync::atomic::Ordering::SeqCst);
}
let engine = build_engine(args.connect, &args.certs).unwrap_or_else(|e| {
eprintln!("ikigai: {e}");
std::process::exit(1);
});
run_repl(engine, args.plain, &args.commands);
}
}
}
#[cfg(feature = "embedded")]
fn with_profiles(engine: Engine) -> Engine {
engine.define_cap_profile("freebusy", ["urn:cap:personal:calendar:read:freebusy"]);
let root = ikigai_embedded::file_root();
let root = root.display();
let read = format!("urn:cap:fs:read:{root}");
let write = format!("urn:cap:fs:write:{root}");
let delete = format!("urn:cap:fs:delete:{root}");
engine.define_cap_profile("read-only", [read.clone()]);
engine.define_cap_profile("read", [read.clone()]);
engine.define_cap_profile("write", [read.clone(), write.clone()]);
engine.define_cap_profile("delete", [read.clone(), write, delete]);
engine.define_cap_profile(
"agent",
["urn:cap:personal:calendar:read:freebusy".to_string(), read],
);
engine
}
#[cfg(feature = "embedded")]
fn build_engine(connect: Option<Option<String>>, certs: &Certs) -> Result<Engine, String> {
match connect {
None => Ok(with_profiles(
Engine::new(ikigai_embedded::watched_kernel())
.with_spawner(std::sync::Arc::new(ikigai_embedded::scheduler())),
)),
Some(target) => match target.as_deref() {
Some(t) if is_quic(t) => connect_quic(t, certs),
_ => connect_ipc(target),
},
}
}
#[cfg(feature = "embedded")]
fn run_repl(engine: Engine, plain: bool, commands: &[String]) {
if !commands.is_empty() {
std::process::exit(repl::run_commands(engine, commands));
}
#[cfg(not(target_family = "wasm"))]
{
use std::io::IsTerminal;
if !plain && std::io::stdin().is_terminal() && std::io::stdout().is_terminal() {
if let Err(e) = tui::run(engine, ikigai_engine::config::keybindings()) {
eprintln!("ikigai: tui error: {e}");
std::process::exit(1);
}
return;
}
repl::run(engine);
}
#[cfg(target_family = "wasm")]
repl::run(engine);
}
#[cfg(all(feature = "embedded", feature = "quic"))]
fn cert_generate(force: bool) -> ! {
match quic::generate(force) {
Ok(dir) => {
println!(
"wrote server.{{crt,key}} and client.{{crt,key}} to {}",
dir.display()
);
println!(
"to attach a client on another machine, copy client.crt, client.key, and \
server.crt there."
);
std::process::exit(0);
}
Err(e) => {
eprintln!("ikigai: {e}");
std::process::exit(1);
}
}
}
#[cfg(all(feature = "embedded", not(feature = "quic")))]
fn cert_generate(_force: bool) -> ! {
eprintln!("ikigai: `cert generate` needs the `quic` feature");
std::process::exit(1);
}
#[cfg(all(feature = "embedded", feature = "quic"))]
fn serve_quic(target: &str, certs: &Certs) -> ! {
let result = (|| -> Result<(), String> {
let addr = quic::parse_addr(target)?;
let identity = quic::server_identity(certs)?;
let trusted = quic::trusted_client_certs(certs)?;
let root = ikigai_embedded::file_root();
let minter: std::sync::Arc<dyn Fn(&str) -> ikigai_quic::Session + Send + Sync> =
std::sync::Arc::new(move |id: &str| {
let segment = root.join(id);
let _ = std::fs::create_dir_all(&segment); let seg = segment.display();
ikigai_quic::Session {
capability: ikigai_core::Capability::root().attenuate([
format!("urn:cap:fs:read:{seg}"),
format!("urn:cap:fs:write:{seg}"),
format!("urn:cap:fs:delete:{seg}"),
]),
file_segment: id.to_string(),
}
});
eprintln!(
"ikigai: serving on {target} (per-client workspaces; {} trusted client cert(s)) (Ctrl-C to stop)",
trusted.len()
);
ikigai_quic::serve(
ikigai_embedded::kernel_for("Remote (QUIC)"),
addr,
&identity,
&trusted,
minter,
)
.map_err(|e| e.to_string())
})();
match result {
Ok(()) => std::process::exit(0),
Err(e) => {
eprintln!("ikigai: {e}");
std::process::exit(1);
}
}
}
#[cfg(all(feature = "embedded", feature = "quic"))]
fn connect_quic(target: &str, certs: &Certs) -> Result<Engine, String> {
let addr = quic::parse_addr(target)?;
let identity = quic::client_identity(certs)?;
let trusted = quic::trusted_server_cert(certs)?;
let resolver = ikigai_quic::connect(addr, &identity, &trusted)
.map_err(|e| format!("connect {target}: {e}"))?;
Ok(with_profiles(Engine::new(resolver)))
}
#[cfg(all(feature = "embedded", not(feature = "quic")))]
fn serve_quic(_target: &str, _certs: &Certs) -> ! {
eprintln!("ikigai: `quic://` needs the `quic` feature");
std::process::exit(1);
}
#[cfg(all(feature = "embedded", not(feature = "quic")))]
fn connect_quic(_target: &str, _certs: &Certs) -> Result<Engine, String> {
Err("`quic://` needs the `quic` feature".to_string())
}
#[cfg(all(feature = "embedded", feature = "ipc", unix))]
fn serve_ipc(path: Option<String>) -> ! {
let socket = ipc_socket(path);
eprintln!("ikigai: serving on {} (Ctrl-C to stop)", socket.display());
match ikigai_ipc::serve(ikigai_embedded::trusted_kernel_for("Remote (IPC)"), &socket) {
Ok(()) => std::process::exit(0),
Err(e) => {
eprintln!("ikigai: serve error: {e}");
std::process::exit(1);
}
}
}
#[cfg(all(feature = "embedded", feature = "ipc", unix))]
fn connect_ipc(path: Option<String>) -> Result<Engine, String> {
let socket = ipc_socket(path);
let resolver =
ikigai_ipc::connect(&socket).map_err(|e| format!("connect {}: {e}", socket.display()))?;
Ok(with_profiles(Engine::new(resolver)))
}
#[cfg(all(feature = "embedded", feature = "ipc", unix))]
fn ipc_socket(path: Option<String>) -> std::path::PathBuf {
path.map(std::path::PathBuf::from)
.or_else(ikigai_ipc::default_socket_path)
.unwrap_or_else(|| {
eprintln!("ikigai: no socket path given and no runtime directory to default to");
std::process::exit(2);
})
}
#[cfg(all(feature = "embedded", not(all(feature = "ipc", unix))))]
fn serve_ipc(_path: Option<String>) -> ! {
eprintln!("ikigai: a Unix-socket server needs the `ipc` feature on a Unix platform");
std::process::exit(1);
}
#[cfg(all(feature = "embedded", not(all(feature = "ipc", unix))))]
fn connect_ipc(_path: Option<String>) -> Result<Engine, String> {
Err("attaching to a Unix socket needs the `ipc` feature on a Unix platform".to_string())
}
#[cfg(not(feature = "embedded"))]
fn main() {
eprintln!(
"ikigai {}: built without a transport. Rebuild with a transport feature, e.g. `--features embedded`.",
env!("CARGO_PKG_VERSION")
);
std::process::exit(1);
}