rebrand to "luna"

Ondřej Hruška 2 hours ago
parent ccdbf1307c
commit 6308e4ae87
  1. 66
      Cargo.lock
  2. 2
      Cargo.toml
  3. 24
      README.md
  4. 8
      docs/README.md
  5. 2
      docs/concurrency.md
  6. 10
      docs/fs.md
  7. 2
      docs/math.md
  8. 4
      docs/os.md
  9. 16
      docs/require.md
  10. 2
      docs/table.md
  11. 2
      lua/demo/weather.lua
  12. 4
      lua/stubs/README.md
  13. 2
      lua/stubs/os.lua
  14. 4
      lua/stubs/require.lua
  15. 2
      src/lua_tests/http_tests.rs
  16. 2
      src/lua_tests/require_tests.rs
  17. 10
      src/main.rs
  18. 4
      src/repl.rs
  19. 7
      src/stdlib/fs/store.rs
  20. 2
      src/stdlib/require.rs

66
Cargo.lock generated

@ -2,39 +2,6 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "a"
version = "0.1.0"
dependencies = [
"base64",
"chrono",
"clap",
"colored",
"cookie_store",
"crossterm",
"digest_auth",
"dotenvy",
"env_logger",
"form_urlencoded",
"futures",
"inquire",
"log",
"mlua",
"publicsuffix",
"reqwest",
"reqwest_cookie_store",
"rusqlite",
"rustix",
"rustls",
"rustyline",
"serde",
"serde_json",
"tempfile",
"thiserror",
"tiny_http",
"tokio",
]
[[package]]
name = "ahash"
version = "0.8.12"
@ -1279,6 +1246,39 @@ dependencies = [
"which",
]
[[package]]
name = "luna"
version = "0.1.0"
dependencies = [
"base64",
"chrono",
"clap",
"colored",
"cookie_store",
"crossterm",
"digest_auth",
"dotenvy",
"env_logger",
"form_urlencoded",
"futures",
"inquire",
"log",
"mlua",
"publicsuffix",
"reqwest",
"reqwest_cookie_store",
"rusqlite",
"rustix",
"rustls",
"rustyline",
"serde",
"serde_json",
"tempfile",
"thiserror",
"tiny_http",
"tokio",
]
[[package]]
name = "md-5"
version = "0.10.6"

@ -1,5 +1,5 @@
[package]
name = "a"
name = "luna"
version = "0.1.0"
edition = "2024"
publish = false

@ -1,18 +1,18 @@
# `a`
# Luna
`a` embeds a Lua interpreter and adds a standard library and IO access, so a script can reach the outside world — today an HTTP client, SQLite and interactive terminal prompts, with more of the surface below planned — without additional setup.
Luna embeds a Lua interpreter and adds a standard library and IO access, so a script can reach the outside world — today an HTTP client, SQLite and interactive terminal prompts, with more of the surface below planned — without additional setup.
```sh
a file.lua # run a script
a # start an interactive REPL
luna file.lua # run a script
luna # start an interactive REPL
```
Run `a` with a script path to execute it, or with no arguments to start an
Run `luna` with a script path to execute it, or with no arguments to start an
interactive REPL.
## The REPL
Running `a` with no arguments starts a read-eval-print loop in the same
Running `luna` with no arguments starts a read-eval-print loop in the same
sandboxed environment used for scripts, with the full standard library
available. It is convenient for trying out the library or exploring data.
@ -23,7 +23,7 @@ available. It is convenient for trying out the library or exploring data.
- Async calls such as `os.sleep`, `http.get`, and `task.join` suspend and resume
just as they do in a script — each entry runs on the async executor.
- Line editing and history are provided, with history persisted to
`~/.a_history`. Press Ctrl-D to exit (`os.exit` is not available in the
`~/.luna_history`. Press Ctrl-D to exit (`os.exit` is not available in the
sandbox).
Each entry is compiled as its own chunk, so a `local` declared on one line is
@ -32,13 +32,13 @@ across entries.
## What it is
`a` is not a Lua implementation; it embeds [Lua 5.5](https://www.lua.org/) (via [`mlua`](https://github.com/mlua-rs/mlua)) and adds the parts stock Lua omits. Plain Lua ships almost no standard library and no way to reach the outside world without C modules and a build toolchain. `a` provides a standard library written partly in Rust and partly in Lua, together with IO access.
Luna is not a Lua implementation; it embeds [Lua 5.5](https://www.lua.org/) (via [`mlua`](https://github.com/mlua-rs/mlua)) and adds the parts stock Lua omits. Plain Lua ships almost no standard library and no way to reach the outside world without C modules and a build toolchain. Luna provides a standard library written partly in Rust and partly in Lua, together with IO access.
The result is a single binary that runs a `.lua` file. There is no package manager, build step, or project scaffolding.
## The standard library
The standard library is the main purpose of `a`. Available today:
The standard library is the main purpose of Luna. Available today:
- **Networking** — HTTP client (WebSocket and MQTT are planned).
- **SQLite** — access to embedded SQLite databases.
@ -57,15 +57,15 @@ The intent is for the common case to be a single call rather than a multi-step s
- Embeds Lua 5.5 via `mlua`, on an async core ([`tokio`](https://tokio.rs/)) so IO-heavy scripts don't block.
- A single self-contained binary. No LuaRocks, no make, no virtualenv.
- Scripts run in a **sandboxed** environment modeled on [Luau's safe environment](https://luau.org/sandbox): `io`, `package`, and `debug` are not loaded; `os` is trimmed to its time functions; the bytecode/chunk-loading escape hatches (`load`, `loadfile`, `dofile`, `string.dump`) are removed. The library `a` provides is the supported way to reach the outside world.
- Scripts run in a **sandboxed** environment modeled on [Luau's safe environment](https://luau.org/sandbox): `io`, `package`, and `debug` are not loaded; `os` is trimmed to its time functions; the bytecode/chunk-loading escape hatches (`load`, `loadfile`, `dofile`, `string.dump`) are removed. The library Luna provides is the supported way to reach the outside world.
- A leading `#!` shebang line is skipped (after an optional UTF-8 BOM), so scripts can be made executable directly. Script files need not be valid UTF-8 — Lua source is byte-oriented.
- Scripts can be split into files with a sandboxed [`require`](docs/require.md): modules are looked up next to the main script (symlinks resolved, so an executable script symlinked into `~/.local/bin` finds its files) and in `$A_INCLUDE_PATHS`, which can also be set in a `.env` beside the script.
- Scripts can be split into files with a sandboxed [`require`](docs/require.md): modules are looked up next to the main script (symlinks resolved, so an executable script symlinked into `~/.local/bin` finds its files) and in `$LUNA_INCLUDE_PATHS`, which can also be set in a `.env` beside the script.
## Building
```sh
cargo build --release
# binary at target/release/a
# binary at target/release/luna
cargo run -- lua/test.lua # run a script during development
```

@ -1,6 +1,6 @@
# `a` standard library
# Luna standard library
Reference docs for the library `a` adds on top of Lua 5.5. Every module below is
Reference docs for the library Luna adds on top of Lua 5.5. Every module below is
a **global**, present in every script with no setup (`require`-ing one by name
also works and returns the same table).
@ -23,6 +23,6 @@ also works and returns the same table).
| Page | What it covers |
|------|----------------|
| [Concurrency](concurrency.md) | How async calls suspend, and running work in parallel with `task.join`. |
| [Modules & `require`](require.md) | Splitting scripts into files: search roots, `A_INCLUDE_PATHS`, `.env`, caching, LSP setup. |
| [Modules & `require`](require.md) | Splitting scripts into files: search roots, `LUNA_INCLUDE_PATHS`, `.env`, caching, LSP setup. |
See the top-level [README](../README.md) for what `a` is and how to build it.
See the top-level [README](../README.md) for what Luna is and how to build it.

@ -1,6 +1,6 @@
# Concurrency
`a` runs every script on an async core ([tokio](https://tokio.rs/)). The async
Luna runs every script on an async core ([tokio](https://tokio.rs/)). The async
stdlib calls — `os.sleep`, the `http` client, `sqlite` queries — don't block the
OS thread while they wait: they suspend and let other work run. This page covers
how that interacts with Lua coroutines and how to run several pieces of work at

@ -105,12 +105,12 @@ script:
- Read and write are tracked separately; each is approved, denied, or not
decided yet.
When an operation needs a decision that isn't recorded, `a` asks on the
When an operation needs a decision that isn't recorded, Luna asks on the
terminal (stderr, so it shows even with stdout piped) — one keypress, no
Enter:
```
a: permission request
luna: permission request
script: /home/me/bin/deploy.lua
wants: read access to directory /home/me/project
[y] allow once [n] deny once [A] allow always [N] never allow:
@ -126,8 +126,8 @@ a: permission request
### `access.json`
Recorded answers live in `$XDG_CONFIG_HOME/a/access.json` (usually
`~/.config/a/access.json`), keyed by the script's canonical path — moving or
Recorded answers live in `$XDG_CONFIG_HOME/luna/access.json` (usually
`~/.config/luna/access.json`), keyed by the script's canonical path — moving or
renaming a script starts it with a clean slate. The file is plain JSON and
safe to hand-edit; each flag is `true` (approved), `false` (denied), or
`null` (not decided yet):
@ -144,7 +144,7 @@ safe to hand-edit; each flag is `true` (approved), `false` (denied), or
}
```
Concurrent `a` processes update it safely (the write cycle holds a file
Concurrent Luna processes update it safely (the write cycle holds a file
lock and replaces the file atomically).
The **REPL** prompts the same way, but its answers — including `A`/`N` —

@ -1,6 +1,6 @@
# `math`
`a` keeps Lua's standard `math` library intact and adds a handful of functions
Luna keeps Lua's standard `math` library intact and adds a handful of functions
that come up constantly in everyday scripting: rounding, clamping, sign, finiteness,
and linear rescaling. The additions live on the same global `math` table, so
`math.floor` and `math.round` sit side by side.

@ -1,9 +1,9 @@
# `os`
`a` runs scripts in a sandbox, so the standard `os` table is trimmed to its
Luna runs scripts in a sandbox, so the standard `os` table is trimmed to its
safe, time-related functions — `os.time`, `os.clock`, `os.date`,
`os.difftime`, `os.getenv` — while the parts that touch the system
(`os.execute`, `os.remove`, `os.exit`, …) are removed. To that trimmed table `a`
(`os.execute`, `os.remove`, `os.exit`, …) are removed. To that trimmed table Luna
adds two functions: a high-resolution clock and an async sleep.
```lua

@ -1,6 +1,6 @@
# Modules & `require`
`a` provides a sandboxed `require` for splitting a script into multiple files.
Luna provides a sandboxed `require` for splitting a script into multiple files.
It follows stock Lua's conventions — dot-separated module names, one execution
per module, cached results — so editors and the Lua language server understand
the imports without special setup.
@ -27,7 +27,7 @@ Names are resolved against a fixed list of directories, in order:
so a script symlinked into `~/.local/bin` still loads modules (and its
`.env`, below) from next to the real file. In the REPL, the current working
directory is used instead.
2. **Each entry of `$A_INCLUDE_PATHS`**, colon-separated like `$PATH`.
2. **Each entry of `$LUNA_INCLUDE_PATHS`**, colon-separated like `$PATH`.
In every root, two candidates are tried (the Lua language server's default
templates):
@ -43,14 +43,14 @@ lists every path that was tried, in the stock Lua format.
## `.env`
Before resolving anything, `a` loads a `.env` file from the main script's
Before resolving anything, Luna loads a `.env` file from the main script's
real directory (the CWD in the REPL), if one exists. Variables already set in
the real environment take precedence. This is the intended place to set
`A_INCLUDE_PATHS` for a project:
`LUNA_INCLUDE_PATHS` for a project:
```
# myproject/.env
A_INCLUDE_PATHS=/home/me/lua-libs:/opt/shared-lua
LUNA_INCLUDE_PATHS=/home/me/lua-libs:/opt/shared-lua
```
Since the script's symlink is resolved first, `myproject/main.lua` symlinked
@ -93,15 +93,15 @@ across `require` boundaries:
}
```
List the same directories in `workspace.library` as in `A_INCLUDE_PATHS`;
List the same directories in `workspace.library` as in `LUNA_INCLUDE_PATHS`;
modules under the script's own directory are found via the workspace root.
## Example layout
```
myproject/
├── main.lua -- #!/usr/bin/env a; symlinked to ~/.local/bin/mytool
├── .env -- A_INCLUDE_PATHS=...
├── main.lua -- #!/usr/bin/env luna; symlinked to ~/.local/bin/mytool
├── .env -- LUNA_INCLUDE_PATHS=...
├── .luarc.json
└── lib/
├── helper.lua -- require "lib.helper"

@ -1,6 +1,6 @@
# `table`
`a` keeps Lua's standard `table` library (`insert`, `remove`, `concat`, `sort`,
Luna keeps Lua's standard `table` library (`insert`, `remove`, `concat`, `sort`,
`pack`, `unpack`) and adds the higher-order and collection helpers stock Lua
leaves out: map/filter/reduce, merge and deep-copy, aggregates, lookups, and a
read-only proxy. The additions live on the same global `table`.

@ -1,4 +1,4 @@
-- Weather forecast demo — a small app that exercises most of the `a` stdlib:
-- Weather forecast demo — a small app that exercises most of the Luna stdlib:
--
-- tui prompts (text w/ suggestions, select, confirm), styled markup,
-- custom presets, escape for untrusted text in markup, message &

@ -1,6 +1,6 @@
# Lua stubs for the `a` runtime environment
# Lua stubs for the Luna runtime environment
`---@meta` type definitions of the **Rust-native** parts of the `a` stdlib, for
`---@meta` type definitions of the **Rust-native** parts of the Luna stdlib, for
[lua-language-server](https://github.com/LuaLS/lua-language-server) (completion,
hover docs, static checking). Definitions only — these files are never executed
and are not part of the runtime.

@ -1,6 +1,6 @@
---@meta
---@diagnostic disable: missing-return
-- Type stubs for the `a` additions to the sandboxed `os` table.
-- Type stubs for the Luna additions to the sandboxed `os` table.
--
-- The sandbox trims `os` to its time-related functions — os.time, os.clock,
-- os.date, os.difftime, os.getenv — plus the two additions below. The

@ -1,12 +1,12 @@
---@meta
---@diagnostic disable: missing-return
-- Type stub for the sandboxed `require`. The stock `package` library does not
-- exist in `a` (it is disabled in .luarc.json, which also removes the stock
-- exist in Luna (it is disabled in .luarc.json, which also removes the stock
-- `require` definition — this stub replaces it). Reference: docs/require.md
--
-- Names are dot-separated (`require "lib.helper"` → lib/helper.lua or
-- lib/helper/init.lua), resolved against the main script's directory and each
-- entry of $A_INCLUDE_PATHS. Each module runs once; the result is cached.
-- entry of $LUNA_INCLUDE_PATHS. Each module runs once; the result is cached.
---Load a module. Returns the module's value and, unlike stock Lua's loader
---data, the resolved file path as the second value.

@ -133,7 +133,7 @@ async fn test_get_response_fields() {
// The default User-Agent goes out unless overridden.
let reqs = server.requests();
assert!(reqs[0].header("user-agent").unwrap().starts_with("a/"), "{reqs:?}");
assert!(reqs[0].header("user-agent").unwrap().starts_with("luna/"), "{reqs:?}");
}
#[tokio::test]

@ -254,7 +254,7 @@ async fn test_require_module_error_propagates_and_is_not_cached() {
#[tokio::test]
async fn test_require_module_with_shebang_and_bom() {
let root = TempRoot::new("shebang");
root.write("sh.lua", "\u{FEFF}#!/usr/bin/env a\nreturn 7");
root.write("sh.lua", "\u{FEFF}#!/usr/bin/env luna\nreturn 7");
let lua = lua_with_roots(&[&root]);
let got: i64 = lua.load(r#"return require "sh""#).eval_async().await.unwrap();

@ -101,7 +101,7 @@ async fn run() -> Result<(), Box<dyn std::error::Error>> {
// to the real file.
let realpath = tokio::fs::canonicalize(&filepath)
.await
.map_err(|e| format!("a: cannot open {}: {e}", filepath.display()))?;
.map_err(|e| format!("luna: cannot open {}: {e}", filepath.display()))?;
let script_dir = realpath.parent().unwrap_or(Path::new("/"));
setup_require(&lua, script_dir)?;
// Exposed to scripts as fs.getScriptDir() — the base for bundled assets.
@ -116,7 +116,7 @@ async fn run() -> Result<(), Box<dyn std::error::Error>> {
// byte strings, and stock Lua runs files that aren't valid UTF-8.
let source = tokio::fs::read(&realpath)
.await
.map_err(|e| format!("a: cannot read {}: {e}", filepath.display()))?;
.map_err(|e| format!("luna: cannot read {}: {e}", filepath.display()))?;
lua.load(stdlib::require::prepare_chunk(&source))
// "@" marks the chunk name as a file path in error messages / tracebacks
@ -130,17 +130,17 @@ async fn run() -> Result<(), Box<dyn std::error::Error>> {
/// Prepare module loading rooted at `dir` — the real directory of the main
/// script, or the CWD in the REPL. Loads `dir/.env` into the process
/// environment (variables already set in the real environment win), then
/// installs `require` searching `dir` followed by the `$A_INCLUDE_PATHS`
/// installs `require` searching `dir` followed by the `$LUNA_INCLUDE_PATHS`
/// entries (colon-separated, like `$PATH`).
fn setup_require(lua: &mlua::Lua, dir: &Path) -> Result<(), Box<dyn std::error::Error>> {
match dotenvy::from_path(dir.join(".env")) {
Ok(()) => {}
Err(dotenvy::Error::Io(e)) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => return Err(format!("a: cannot load {}: {e}", dir.join(".env").display()).into()),
Err(e) => return Err(format!("luna: cannot load {}: {e}", dir.join(".env").display()).into()),
}
let mut roots = vec![dir.to_path_buf()];
if let Some(paths) = std::env::var_os("A_INCLUDE_PATHS") {
if let Some(paths) = std::env::var_os("LUNA_INCLUDE_PATHS") {
roots.extend(std::env::split_paths(&paths).filter(|p| !p.as_os_str().is_empty()));
}
stdlib::require::install(lua, roots)?;

@ -69,7 +69,7 @@ pub(crate) async fn run(lua: &Lua) -> Result<(), Box<dyn std::error::Error>> {
let mut editor = DefaultEditor::new()?;
let history_path = std::env::var_os("HOME").map(|home| {
let mut p = std::path::PathBuf::from(home);
p.push(".a_history");
p.push(".luna_history");
p
});
if let Some(path) = &history_path {
@ -78,7 +78,7 @@ pub(crate) async fn run(lua: &Lua) -> Result<(), Box<dyn std::error::Error>> {
}
println!(
"a {} — interactive Lua. Ctrl-D to exit.",
"luna {} — interactive Lua. Ctrl-D to exit.",
env!("CARGO_PKG_VERSION")
);

@ -3,7 +3,7 @@
//! Grants are keyed by the script's canonical path, then by canonical
//! directory, each carrying a tri-state per direction: `null` (not decided),
//! `true` (approved), `false` (denied). The file is hand-editable; [`persist`]
//! keeps concurrent `a` processes from losing each other's updates by holding
//! keeps concurrent Luna processes from losing each other's updates by holding
//! an exclusive `flock` on a sidecar lock file across its read-merge-write
//! cycle, and replaces the store with `rename()` so readers never see a
//! half-written file.
@ -17,7 +17,7 @@ use serde::{Deserialize, Serialize};
/// Config directory name under `$XDG_CONFIG_HOME`. A literal, not
/// `CARGO_PKG_NAME`: renaming the binary silently abandoning every recorded
/// grant should be an explicit decision made here.
pub(crate) const APP_NAME: &str = "a";
pub(crate) const APP_NAME: &str = "luna";
/// Tri-state access recorded for one directory. `None` = not decided yet,
/// `Some(true)` = approved, `Some(false)` = denied — serialized as JSON
@ -61,7 +61,8 @@ impl Default for AccessStore {
}
}
/// `$XDG_CONFIG_HOME/a/access.json`, falling back to `~/.config/a/access.json`.
/// `$XDG_CONFIG_HOME/luna/access.json`, falling back to
/// `~/.config/luna/access.json`.
/// Per the XDG spec a non-absolute `$XDG_CONFIG_HOME` is ignored. `None` when
/// no absolute base can be found — permissions then live for the session only.
pub(crate) fn default_store_path() -> Option<PathBuf> {

@ -3,7 +3,7 @@
//! Module names use stock Lua dot notation (`require "db.migrations"`) and are
//! resolved against a fixed list of root directories — the main script's real
//! directory (the CWD in the REPL) followed by the entries of
//! `$A_INCLUDE_PATHS` — through the standard `?.lua` / `?/init.lua` templates.
//! `$LUNA_INCLUDE_PATHS` — through the standard `?.lua` / `?/init.lua` templates.
//! Because a name may only contain identifier-like segments, a module can
//! never name a file outside the roots.
//!

Loading…
Cancel
Save