# Project
Project is a cross-platform CLI program to execute commands defined in the `Project.yml`. Each command is a Lua script. The main different from [just](https://github.com/casey/just) is Project focus on cross-platform scripting instead of rely on the other tools.
> [!WARNING]
> There is a plan to upgrade to Lua 5.5 so don't write your script in such a way that it is not [compatible](https://www.lua.org/manual/5.5/manual.html#8.1) with it.
## Key features
- Batteries included.
- Small and lightweight.
- Easy to install on Linux, macOS and Windows.
- Single executable with only system dependencies.
- Lua 5.4 as scripting language.
- Non-blocking concurrent execution with Lua thread.
- Simple APIs designed for project automation.
## Why use this instead of Python?
- Project are easy lightweight to install (especially if you already have Rust or on Windows).
- No additional steps to install external dependencies required by your scripts.
- APIs designed for project automation.
- Declarative command.
## Installation
There are 3 ways to install Project:
1. Via cargo (recommended if you have Rust).
2. Via automated script (recommended if you don't have Rust).
3. Manual download.
### Cargo
If you have Rust installed you can use [cargo install](https://doc.rust-lang.org/cargo/commands/cargo-install.html) to install Project:
```sh
cargo install --git https://github.com/ultimaweapon/project.git
```
Try running Project to see if you have Cargo installation directory in the `PATH`:
```sh
project
```
It should output something like:
```
Failed to open Project.yml: No such file or directory (os error 2).
```
If it error with command not found you need to add Cargo installation directory to `PATH` manually. You can find this directory in the outputs of `cargo install` on the above.
### Automated script
TBA
### Manual download
TBA
## Quick start
Create `Project.yml` in the root of your repository with the following content:
```yaml
commands:
build:
description: Build the project
args:
release:
description: Enable optimization
long: release
short: r
type: bool
script: scripts/build.lua
```
Then create `scripts/build.lua` with the following content:
```lua
if args['release'] then
print('Start building release build!')
else
print('Start building debug build!')
end
```
Then run:
```sh
project --help
```
It will outputs something like:
```
Run a command defined in Project.yml
Usage: project <COMMAND>
Commands:
build Build the project
help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help
```
Notice the `build` command that loaded from your `Project.yml`. Run the following command to see how to use your `build` command:
```sh
project build --help
```
It will outputs something like:
```
Build the project
Usage: project build [OPTIONS]
Options:
-r, --release Enable optimization
-h, --help Print help
```
If you run `project build` it will run `scripts/build.lua`, which output the following text to the console:
```
Start building debug build!
```
## Script API
Lua implementation used here is [Tsuki](https://github.com/ultimaweapon/tsuki). There are some differences with vanilla Lua, which you can see in Tsuki's README. The following is a list of additional changes from Project:
- No `pcall`.
- No `os.execute`.
- No `os.exit`.
- No `os.remove`.
### args[name]
A global variable contains all command arguments. If argument `name` does not present it will return `false` for `bool` argument or `nil` for the other type.
### exit(code)
Cause Project process to exit immediately. Unlike `os.exit`, this function always close all to-be-closed variables.
### json.parse(json)
Parse a JSON string and return a corresponding value (e.g. the result will be a table for JSON object).
### os.arch
Architecture of the OS. The value will be one of `aarch64` and `x86_64`.
### os.capture(prog [, ...])
Run `prog` with the remaining arguments as its arguments and return its outputs, which is stdout by default. This does not use OS shell to run `prog`. The returned string will have LF and/or CR at the end removed by default. This function will raise an error by default if `prog` exit with non-zero code. By default, stdin will be a null stream and a non-captured stream will be inherits from Project process. Working directory will be the directory that contains `Project.yml` by default.
All `nil` in the arguments will be removed (e.g. `os.capture('echo', 'abc', nil, 'def')` will invoke `echo` with `abc` and `def` as arguments).
If `prog` is a table the item at index #1 must be the name of program to run and it can contains the following additional items:
#### from
Can be either `stdout`, `stderr` or `both`. If this key does not present it will default to `stdout`. With `both` this function will return a table contains `stdout` and `stderr` fields.
### os.copyfile(src, dst [, mode])
Copy a file from `src` to directory `dst`. This function will **overwrite** file with the same name in `dst`. `mode` can be either:
- `content`: Copy only content, not permissions. This is default if `mode` is absent.
- `all`: Copy both content and permissions. This use [tokio::fs::copy](https://docs.rs/tokio/latest/tokio/fs/fn.copy.html) under the hood.
Returns number of bytes copied.
### os.copyfileas(src, dst [, mode])
Copy a file from `src` to `dst`. This function will **overwrite** the contents of `dst`. Note that `dst` always treat as a destination file, not a destination directory. `mode` can be either:
- `content`: Copy only content, not permissions. This is default if `mode` is absent.
- `all`: Copy both content and permissions. This use [tokio::fs::copy](https://docs.rs/tokio/latest/tokio/fs/fn.copy.html) under the hood.
Returns number of bytes copied.
### os.createdir(path [, ...])
Recursively create a directory and all of its parent components if they are missing. Path will be **joined** together with native path separator to form a path to directory so:
```lua
local r = os.createdir('abc', 'def')
if t[1] then
-- 'abc' does not exists before and was created by the call
end
```
Will result in `abc/def` on *nix and `abc\def` on Windows as a path to create. This use [PathBuf::push](https://doc.rust-lang.org/std/path/struct.PathBuf.html#method.push) to create the path so if any arguments is an absolute path it will **discard** the path that was created by previous arguments.
Returns a table consist of argument number as a key and `boolean` as a value indicated if the component was created by the call (that is, not exists before the call).
### os.kind
Kind of the OS. The value will be one of `linux`, `macos` and `windows`.
### os.removedir(path [, ...])
Remove a directory and its content, which mean it will **always** remove the directory even if the directory is not empty. Path will be **joined** together with native path separator to form a path to directory so:
```lua
os.removedir('abc', 'def')
```
Will result in `abc/def` on *nix and `abc\def` on Windows as a path to remove. This use [PathBuf::push](https://doc.rust-lang.org/std/path/struct.PathBuf.html#method.push) to create the path so if any arguments is an absolute path it will **discard** the path that was created by previous arguments.
### os.run(prog [, ...])
Run `prog` with the remaining arguments as its arguments. Unlike `os.execute`, this does not use OS shell to run `prog`. This function will raise an error by default if `prog` exit with non-zero code. By default, stdin will be a null stream and stdout/stderr will be inherits from Project process and working directory will be the directory that contains `Project.yml`.
All `nil` in the arguments will be removed (e.g. `os.run('echo', 'abc', nil, 'def')` will invoke `echo` with only 2 arguments).
### os.spawn(prog [, ...])
Run `prog` with the remaining arguments as its arguments and return a process object to manipulate it. This does not use OS shell to run `prog`. By default, stdin will be a null stream and a non-captured stream will be inherits from Project process. Working directory will be the directory that contains `Project.yml` by default.
All `nil` in the arguments will be removed (e.g. `os.spawn('echo', 'abc', nil, 'def')` will spawn `echo` with only 2 arguments).
The process object can be a [to-be-closed](https://www.lua.org/manual/5.4/manual.html#3.3.8) variable, which will kill the process when the object goes out of scope. If the variable does not have `close` attribute the process will get killed when the object is freed by Lua GC.
If `prog` is a table the item at index #1 must be the name of program to run and it can contains the following additional items:
#### cwd
Working directory for the process. If this key does not present it will default to the directory that contains `Project.yml`.
#### stdout
Can be either `null`, `inherit` or `pipe`. If this key does not present it will default to `inherit`. For `pipe` the process object will have `stdout` property, which have [read](https://www.lua.org/manual/5.4/manual.html#pdf-file:read) method.
### path.basename(path)
Returns the final component of the path, if there is one. This use [Path::file_name](https://doc.rust-lang.org/std/path/struct.Path.html#method.file_name) under the hood.
### path.dirname(path)
Returns `path` without its final component, if there is one. This use [Path::parent](https://doc.rust-lang.org/std/path/struct.Path.html#method.parent) under the hood.
### path.join(component [, ...])
Returns a joined path components so:
```lua
path.join('abc', 'def')
```
Will result in `abc/def` on *nix and `abc\def` on Windows. This use [PathBuf::push](https://doc.rust-lang.org/std/path/struct.PathBuf.html#method.push) to create the path so if any arguments is an absolute path it will **discard** the path that was created by previous arguments.
### string.capitalize(str [, mode])
Capitalize `str` and return it. `mode` can be either:
- `first`: Capitalize only the first letter. This is default if `mode` is absent.
### Url:new(url)
Create an instance of `Url` class from `url`. This class has the following properties and methods:
#### path
Returns the path for the URL, as a percent-encoded ASCII string.
## Exit code
Project will exit with exit code 0 when all operations completed successfully. The script can use `exit` to exit with a custom exit code. The code 100 and above are reserved for Project use and have the following meaning:
### 100
The script exit with an error. This is runtime error, not compile time.
### 101
Project process was panic. This indicate an underlying bug on Project itself to please report this!
### 102
Project unable to open `Project.yml`.
### 103
Project unable to parse `Project.yml`.
### 104
No action is defined for some commands.
### 105
Project unable to read Lua script for the command.
### 106
Project unable to load Lua script for the command.
### 109
Project unable to setup Tokio.
## Project.yml
### commands
List of commands.
### commands.<command_id>
A command definition.
### commands.<command_id>.description
Description of the command.
### commands.<command_id>.args.<arg_id>.description
Description of the argument.
### commands.<command_id>.args.<arg_id>.long
Long name of the argument (e.g. `help`).
### commands.<command_id>.args.<arg_id>.short
Shot name of the argument (e.g. `h`).
### commands.<command_id>.args.<arg_id>.type
Type of the argument. Can be either `bool` or `string`.
### commands.<command_id>.args.<arg_id>.placeholder
Placeholer of argument's value.
### commands.<command_id>.args.<arg_id>.default
Default value if argument presented but the value is missing.
### commands.<command_id>.script
Path to Lua script to execute when this command is invoked. Path separator always is `/` even on Windows and Project will convert to native path.
## License
This project is licensed under either of
- Apache License, Version 2.0,
- MIT license
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this project by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.