generated from marked/CSSharpTemplate
Initial commit
This commit is contained in:
commit
a44fe0dbe0
20 changed files with 1237 additions and 0 deletions
42
.forgejo/workflows/build.yml
Normal file
42
.forgejo/workflows/build.yml
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
name: Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "**.cs"
|
||||||
|
- "**.csproj"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
DOTNET_VERSION: "8.0.407"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build
|
||||||
|
runs-on: docker
|
||||||
|
container:
|
||||||
|
image: ghcr.io/catthehacker/ubuntu:act-24.04
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set Date
|
||||||
|
run: echo "DATE=$(date +'%Y%m%dT%H%M%SZ')" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Setup .NET Core
|
||||||
|
uses: https://github.com/actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: ${{ env.DOTNET_VERSION }}
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: dotnet restore
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: dotnet build --configuration Release --no-restore
|
||||||
|
|
||||||
|
- name: Upload Build Artifact
|
||||||
|
uses: https://git.devmarked.win/actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: build-${{ env.DATE }}.zip
|
||||||
|
path: src/bin/Release/net8.0
|
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
**/bin
|
||||||
|
**/obj
|
7
.luaurc
Normal file
7
.luaurc
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"languageMode": "strict",
|
||||||
|
"aliases": {
|
||||||
|
"lune": "~/.lune/.typedefs/0.8.9",
|
||||||
|
"pkg": "lune/pkg"
|
||||||
|
}
|
||||||
|
}
|
22
CSSharpTemplate.sln
Normal file
22
CSSharpTemplate.sln
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 17
|
||||||
|
VisualStudioVersion = 17.0.31903.59
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSSharpTemplate", "src\CSSharpTemplate.csproj", "{A50E1683-7ED3-4FE7-A752-DAFC22ABBDBD}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{A50E1683-7ED3-4FE7-A752-DAFC22ABBDBD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{A50E1683-7ED3-4FE7-A752-DAFC22ABBDBD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{A50E1683-7ED3-4FE7-A752-DAFC22ABBDBD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{A50E1683-7ED3-4FE7-A752-DAFC22ABBDBD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 marked
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
9
PROJECT-SETUP.md
Normal file
9
PROJECT-SETUP.md
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
## PROJECT SETUP
|
||||||
|
|
||||||
|
To set up the template:
|
||||||
|
1. Make a repository using the template
|
||||||
|
2. `git clone YOUR-REPOSITORY-URL MyPlugin`
|
||||||
|
3. `cd MyPlugin`
|
||||||
|
4. `lune run manager -- setup MyPlugin`
|
||||||
|
|
||||||
|
This file will be deleted afterwards.
|
53
README.md
Normal file
53
README.md
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
<h1 align=center><code>CSSharpTemplate</code></h1>
|
||||||
|
|
||||||
|
<div align=center>
|
||||||
|
<a href="https://git.devmarked.win/marked/CSSharpTemplate/actions?workflow=build.yml"><img src="https://git.devmarked.win/marked/CSSharpTemplate/badges/workflows/build.yml/badge.svg?style=for-the-badge&label=BUILD"></a>
|
||||||
|
<a href="https://git.devmarked.win/marked/CSSharpTemplate/src/branch/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue?style=for-the-badge"></a>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
Description
|
||||||
|
|
||||||
|
# Installation
|
||||||
|
|
||||||
|
1. Download the latest build artifact from [the build action](https://git.devmarked.win/marked/CSSharpTemplate/actions?workflow=build.yml)
|
||||||
|
2. Extract it in `addons/counterstrikesharp/plugins`
|
||||||
|
|
||||||
|
# Development
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
This project uses [Lune](https://github.com/lune-org/lune) to feature a cross-platform helper script.\
|
||||||
|
To install it, fetch the latest binary from [GitHub releases](https://github.com/lune-org/lune/releases/tag/v0.8.9), or install it via cargo:
|
||||||
|
```sh
|
||||||
|
cargo install --locked lune
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
For a basic debug build, run:
|
||||||
|
```sh
|
||||||
|
lune run manager -- build
|
||||||
|
```
|
||||||
|
|
||||||
|
To specify a configuration:
|
||||||
|
```sh
|
||||||
|
lune run manager -- build --configuration CONFIGURATION
|
||||||
|
```
|
||||||
|
|
||||||
|
To replicate the built plugin to a docker container:
|
||||||
|
```sh
|
||||||
|
lune run manager -- build --replicator docker --remote-output container-id:/path/to/plugin
|
||||||
|
```
|
||||||
|
|
||||||
|
To replicate the built plugin to a remote machine:
|
||||||
|
```sh
|
||||||
|
lune run manager -- build --replicator ssh --remote-output username@hostname:/path/to/plugin
|
||||||
|
```
|
||||||
|
|
||||||
|
To replicate the built plugin to a docker container on a remote machine:
|
||||||
|
```sh
|
||||||
|
lune run manager -- build --replicator ssh-docker --remote-output username@hostname->container-id:/path/to/plugin
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also write a custom replicator in [Luau](https://github.com/luau-lang/luau) under `lune/manager/replicators`.
|
46
lune/manager/build.luau
Normal file
46
lune/manager/build.luau
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
--!strict
|
||||||
|
local common = require("./common")
|
||||||
|
local progress_bar = require("@pkg/progress")
|
||||||
|
local result = require("@pkg/result")
|
||||||
|
|
||||||
|
export type ReplicatorData = {
|
||||||
|
remote_output: string,
|
||||||
|
replicator: string,
|
||||||
|
}
|
||||||
|
local function build(configuration: string, replicator_data: ReplicatorData?)
|
||||||
|
local progress = progress_bar.new():with_stage("init", "Initializing"):with_stage("build", "Building project")
|
||||||
|
if replicator_data then
|
||||||
|
progress:with_stage("replicate", "Replicating to remote server")
|
||||||
|
end
|
||||||
|
|
||||||
|
progress:start() -- init
|
||||||
|
|
||||||
|
progress:next_stage() -- build
|
||||||
|
|
||||||
|
local build_result = common.spawn(`dotnet build -c {configuration}`)
|
||||||
|
if not build_result.ok then
|
||||||
|
progress:stop()
|
||||||
|
return result(false, `Failed to build:\n{build_result.err}`)
|
||||||
|
end
|
||||||
|
|
||||||
|
if replicator_data then
|
||||||
|
progress:next_stage() -- replicate
|
||||||
|
|
||||||
|
progress:pause()
|
||||||
|
local replicator: common.Replicator = (require)(`./replicators/{replicator_data.replicator}`)
|
||||||
|
local replication_result = replicator(`src/bin/{configuration}/net8.0`, replicator_data.remote_output)
|
||||||
|
if not replication_result.ok then
|
||||||
|
progress:stop()
|
||||||
|
return result(false, `Failed to replicate build:\n{replication_result.err}`)
|
||||||
|
end
|
||||||
|
progress:unpause()
|
||||||
|
end
|
||||||
|
|
||||||
|
progress:stop() -- finish
|
||||||
|
|
||||||
|
print(build_result.val.stdout)
|
||||||
|
|
||||||
|
return result(true, nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
return build
|
46
lune/manager/common.luau
Normal file
46
lune/manager/common.luau
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
--!strict
|
||||||
|
local process = require("@lune/process")
|
||||||
|
local stdio = require("@lune/stdio")
|
||||||
|
|
||||||
|
local result = require("@pkg/result")
|
||||||
|
|
||||||
|
local function c_print(msg: string)
|
||||||
|
stdio.write(`{stdio.color("blue")}[info]{stdio.color("reset")} {msg}\n`)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function c_warn(msg: string)
|
||||||
|
stdio.write(`{stdio.color("yellow")}[warn]{stdio.color("reset")} {msg}\n`)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function c_error(msg: string)
|
||||||
|
stdio.ewrite(`{stdio.color("red")}[error]{stdio.color("reset")} {msg}\n`)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function c_spawn(command: string): result.Identity<process.SpawnResult>
|
||||||
|
local args = string.split(command, " ")
|
||||||
|
local program = table.remove(args, 1)
|
||||||
|
if not program then
|
||||||
|
return result(false, "Couldn't find program in command.")
|
||||||
|
end
|
||||||
|
|
||||||
|
local spawn_result = process.spawn(program, args)
|
||||||
|
if not spawn_result.ok then
|
||||||
|
return result(false, spawn_result.stderr)
|
||||||
|
end
|
||||||
|
|
||||||
|
return result(true, spawn_result)
|
||||||
|
end
|
||||||
|
|
||||||
|
local common = {
|
||||||
|
default_name = "CSSharpTemplate",
|
||||||
|
|
||||||
|
print = c_print,
|
||||||
|
warn = c_warn,
|
||||||
|
error = c_error,
|
||||||
|
|
||||||
|
spawn = c_spawn,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Replicator = (dist_folder: string, remote_output: string) -> result.Identity<nil>
|
||||||
|
|
||||||
|
return common
|
67
lune/manager/init.luau
Normal file
67
lune/manager/init.luau
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
--!strict
|
||||||
|
local process = require("@lune/process")
|
||||||
|
|
||||||
|
local common = require("./common")
|
||||||
|
local frkcli = require("@pkg/frkcli")
|
||||||
|
|
||||||
|
local cli = frkcli.new_subcommands("build_cli", "CLI for automatic basic C# tasks")
|
||||||
|
|
||||||
|
local command_build = cli:add_subcommand(
|
||||||
|
"build",
|
||||||
|
[[
|
||||||
|
Build the project.
|
||||||
|
|
||||||
|
Additionally, you can use a replicator to replicate your built plugin to:
|
||||||
|
1. A docker container:
|
||||||
|
`lune run manager -- build --replicator docker --remote-output container-id:/path/to/plugin`
|
||||||
|
2. A remote machine:
|
||||||
|
`lune run manager -- build --replicator ssh --remote-output username@hostname:/path/to/plugin`
|
||||||
|
3. A docker container on a remote machine:
|
||||||
|
`lune run manager -- build --replicator docker-ssh --remote-output username@hostname->container-id:/path/to/plugin`
|
||||||
|
|
||||||
|
You can also use a custom replicator, as long as it's located inside `lune/manager/replicators`.
|
||||||
|
]]
|
||||||
|
)
|
||||||
|
command_build:add_option(
|
||||||
|
"configuration",
|
||||||
|
{ help = "The configuration to use for building the project", aliases = { "c" }, default = "Debug" }
|
||||||
|
)
|
||||||
|
command_build:add_option(
|
||||||
|
"remote-output",
|
||||||
|
{ help = "Path on a remote machine to copy the built project to", aliases = { "o" }, default = "" }
|
||||||
|
)
|
||||||
|
command_build:add_option(
|
||||||
|
"replicator",
|
||||||
|
{ help = "Replicator to use for copying builds to a remote machine", aliases = { "r" }, default = "ssh" }
|
||||||
|
)
|
||||||
|
|
||||||
|
local command_setup = cli:add_subcommand("setup", "Setup the project")
|
||||||
|
command_setup:add_positional("name", { help = "Name of the project", default = nil })
|
||||||
|
command_setup:add_flag("force", { help = "Force the setup?", aliases = { "f" } })
|
||||||
|
|
||||||
|
local parsed, err = cli:parse(process.args)
|
||||||
|
if err ~= nil then
|
||||||
|
error(err)
|
||||||
|
end
|
||||||
|
assert(parsed ~= nil)
|
||||||
|
|
||||||
|
local values = parsed.result.values
|
||||||
|
local flags = parsed.result.flags
|
||||||
|
if parsed.command == "build" then
|
||||||
|
local replicator_data = nil
|
||||||
|
if values.remote_output ~= "" then
|
||||||
|
replicator_data = {
|
||||||
|
remote_output = values.remote_output,
|
||||||
|
replicator = values.replicator,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
require("./build")(values.configuration, replicator_data)
|
||||||
|
elseif parsed.command == "setup" then
|
||||||
|
local setup = require("./setup")
|
||||||
|
local result = setup(values.name, flags.force)
|
||||||
|
|
||||||
|
if not result.ok then
|
||||||
|
common.error(result.err)
|
||||||
|
end
|
||||||
|
end
|
16
lune/manager/replicators/docker.luau
Normal file
16
lune/manager/replicators/docker.luau
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
--!strict
|
||||||
|
local process = require("@lune/process")
|
||||||
|
|
||||||
|
local common = require("../common")
|
||||||
|
local result = require("@pkg/result")
|
||||||
|
|
||||||
|
local function docker_replicator(dist_folder: string, remote_output: string): result.Identity<nil>
|
||||||
|
local copy_result = process.spawn("docker", { "cp", "-r", dist_folder, remote_output })
|
||||||
|
if not copy_result.ok then
|
||||||
|
return result(false, `Failed to copy files:\n{copy_result.stderr}`)
|
||||||
|
end
|
||||||
|
|
||||||
|
return result(true, nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
return docker_replicator :: common.Replicator
|
41
lune/manager/replicators/ssh-docker.luau
Normal file
41
lune/manager/replicators/ssh-docker.luau
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
--!strict
|
||||||
|
local process = require("@lune/process")
|
||||||
|
|
||||||
|
local common = require("../common")
|
||||||
|
local result = require("@pkg/result")
|
||||||
|
|
||||||
|
local function ssh_docker_replicator(dist_folder: string, remote_output: string): result.Identity<nil>
|
||||||
|
local split = string.split(remote_output, "->")
|
||||||
|
|
||||||
|
local remote_machine = split[1]
|
||||||
|
if not remote_machine then
|
||||||
|
return result(false, "Couldn't get the remote machine to ssh into.")
|
||||||
|
end
|
||||||
|
|
||||||
|
local docker_path = split[2]
|
||||||
|
if not docker_path then
|
||||||
|
return result(false, "Couldn't get the docker container path for the remote machine.")
|
||||||
|
end
|
||||||
|
|
||||||
|
local ssh_copy_result = process.spawn(
|
||||||
|
"scp",
|
||||||
|
{ "-r", "-O", dist_folder, `{remote_machine}:/tmp/cssharp-replicate` },
|
||||||
|
{ stdio = "forward" }
|
||||||
|
)
|
||||||
|
if not ssh_copy_result.ok then
|
||||||
|
return result(false, `Failed to copy files to the remote machine:\n{ssh_copy_result.stderr}`)
|
||||||
|
end
|
||||||
|
|
||||||
|
local docker_copy_result = process.spawn(
|
||||||
|
"ssh",
|
||||||
|
{ "-t", remote_machine, `"docker cp -r /tmp/cssharp-replicate {docker_path}"` },
|
||||||
|
{ stdio = "forward" }
|
||||||
|
)
|
||||||
|
if not docker_copy_result.ok then
|
||||||
|
return result(false, `Failed to copy files via docker on the remote machine:\n{docker_copy_result.stderr}`)
|
||||||
|
end
|
||||||
|
|
||||||
|
return result(true, nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
return ssh_docker_replicator :: common.Replicator
|
16
lune/manager/replicators/ssh.luau
Normal file
16
lune/manager/replicators/ssh.luau
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
--!strict
|
||||||
|
local process = require("@lune/process")
|
||||||
|
|
||||||
|
local common = require("../common")
|
||||||
|
local result = require("@pkg/result")
|
||||||
|
|
||||||
|
local function ssh_replicator(dist_folder: string, remote_output: string): result.Identity<nil>
|
||||||
|
local copy_result = process.spawn("scp", { "-r", "-O", dist_folder, remote_output }, { stdio = "forward" })
|
||||||
|
if not copy_result.ok then
|
||||||
|
return result(false, `Failed to copy files:\n{copy_result.stderr}`)
|
||||||
|
end
|
||||||
|
|
||||||
|
return result(true, nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
return ssh_replicator :: common.Replicator
|
69
lune/manager/setup.luau
Normal file
69
lune/manager/setup.luau
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
--!strict
|
||||||
|
local fs = require("@lune/fs")
|
||||||
|
|
||||||
|
local common = require("./common")
|
||||||
|
local progress_bar = require("@pkg/progress")
|
||||||
|
local result = require("@pkg/result")
|
||||||
|
|
||||||
|
local function setup(name: string, force: boolean): result.Identity<nil>
|
||||||
|
local progress = progress_bar
|
||||||
|
.new()
|
||||||
|
:with_stage("init", "Initializing")
|
||||||
|
:with_stage("prepare", "Cleaning up defaults")
|
||||||
|
:with_stage("modify", "Replacing default names")
|
||||||
|
:with_stage("generate", "Generating .sln")
|
||||||
|
progress:start() -- init
|
||||||
|
|
||||||
|
if not fs.metadata(`{common.default_name}.sln`).exists and not force then
|
||||||
|
progress:stop()
|
||||||
|
return result(false, "Aborting -- the template is already set up.")
|
||||||
|
end
|
||||||
|
|
||||||
|
progress:next_stage() -- prepare
|
||||||
|
|
||||||
|
fs.removeFile(`{common.default_name}.sln`)
|
||||||
|
fs.removeFile(`PROJECT-SETUP.md`)
|
||||||
|
|
||||||
|
progress:next_stage() -- modify
|
||||||
|
|
||||||
|
fs.move(`src/{common.default_name}.cs`, `src/{name}.cs`)
|
||||||
|
fs.move(`src/{common.default_name}.csproj`, `src/{name}.csproj`)
|
||||||
|
|
||||||
|
do
|
||||||
|
local main_src = fs.readFile(`src/{name}.cs`)
|
||||||
|
local edited_src = string.gsub(main_src, common.default_name, name)
|
||||||
|
fs.writeFile(`src/{name}.cs`, edited_src)
|
||||||
|
end
|
||||||
|
|
||||||
|
progress:next_stage() -- generate
|
||||||
|
|
||||||
|
do
|
||||||
|
local res = common.spawn(`dotnet new sln --name {name}`)
|
||||||
|
if not res.ok then
|
||||||
|
return result(false, `Failed to create new sln:\n{res.err}`)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
do
|
||||||
|
local res = common.spawn("dotnet sln add src")
|
||||||
|
if not res.ok then
|
||||||
|
return result(false, `Failed to add src to sln:\n{res.err}`)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
do
|
||||||
|
local sln_src = fs.readFile(`{name}.sln`)
|
||||||
|
local by_line = string.split(sln_src, "\n")
|
||||||
|
for _ = 1, 3 do
|
||||||
|
table.remove(by_line, 3)
|
||||||
|
end
|
||||||
|
local concated = table.concat(by_line, "\n")
|
||||||
|
fs.writeFile(`{name}.sln`, concated)
|
||||||
|
end
|
||||||
|
|
||||||
|
progress:stop() -- finish
|
||||||
|
|
||||||
|
return result(true, nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
return setup
|
585
lune/pkg/frkcli.luau
Normal file
585
lune/pkg/frkcli.luau
Normal file
|
@ -0,0 +1,585 @@
|
||||||
|
--!nolint LocalShadow
|
||||||
|
|
||||||
|
local process = require("@lune/process")
|
||||||
|
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
type ArgKind = "POSITIONAL" | "FLAG" | "OPTION"
|
||||||
|
type ArgOptions = {
|
||||||
|
help: string?,
|
||||||
|
aliases: { string }?,
|
||||||
|
default: string?, -- if this is not nil, the arg will be optional
|
||||||
|
}
|
||||||
|
|
||||||
|
-- this is what is stored, we guarante non nullity when args are added so this types makes Luau feel better
|
||||||
|
type ArgOptionsSafe = {
|
||||||
|
help: string,
|
||||||
|
aliases: { string },
|
||||||
|
default: string?,
|
||||||
|
}
|
||||||
|
|
||||||
|
type ArgData = {
|
||||||
|
name: string,
|
||||||
|
kind: ArgKind,
|
||||||
|
options: ArgOptionsSafe,
|
||||||
|
}
|
||||||
|
|
||||||
|
type ParseResult = {
|
||||||
|
values: { [string]: string },
|
||||||
|
flags: { [string]: boolean },
|
||||||
|
fwd_args: { string }, -- all args after `--`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubcommandParseResult = {
|
||||||
|
command: string,
|
||||||
|
result: ParseResult,
|
||||||
|
}
|
||||||
|
|
||||||
|
local DEFAULT_OPTIONS: ArgOptionsSafe = {
|
||||||
|
help = "",
|
||||||
|
default = nil,
|
||||||
|
aliases = {},
|
||||||
|
}
|
||||||
|
|
||||||
|
local DEFAULT_helpset = {
|
||||||
|
["-h"] = true,
|
||||||
|
["--help"] = true,
|
||||||
|
}
|
||||||
|
|
||||||
|
local function validate_key_or_error(key: string)
|
||||||
|
if (key:sub(1, 2) == "--" or key:sub(1, 1) == "-") and not key:find(" ") then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
error(`arg key {key} is invalid. Keys must start with either '-' or '--' and may not contain spaces`)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function validate_subcommand_name_or_error(name: string)
|
||||||
|
if name:sub(1, 2) == "--" or name:sub(1, 1) == "-" or name:find(" ") then
|
||||||
|
error(`subcommand name '{name}' is invalid. Name must not start with '-' or '--' and may not contain spaces`)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
type HelpSection = { title: string, lines: { { string } | string }? }
|
||||||
|
local function make_help(sections: { HelpSection }, indent: number?): string
|
||||||
|
local function align_cols(rows: { { string } }, sep: string?): { { string } }
|
||||||
|
local sep = if sep == nil then " " else sep
|
||||||
|
local max_col_lengths: { number } = {}
|
||||||
|
for _, row in rows do
|
||||||
|
for i, s in row do
|
||||||
|
if max_col_lengths[i] == nil or s:len() > max_col_lengths[i] then
|
||||||
|
max_col_lengths[i] = s:len()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local aligned_rows: { { string } } = {}
|
||||||
|
for _, row in rows do
|
||||||
|
local line: { string } = {}
|
||||||
|
for i, col in row do
|
||||||
|
table.insert(line, col)
|
||||||
|
if i < #row then
|
||||||
|
local spacing = sep
|
||||||
|
local diff = max_col_lengths[i] - col:len()
|
||||||
|
if diff > 0 then
|
||||||
|
spacing = string.rep(" ", diff) .. sep
|
||||||
|
end
|
||||||
|
table.insert(line, spacing)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.insert(aligned_rows, line)
|
||||||
|
end
|
||||||
|
return aligned_rows
|
||||||
|
end
|
||||||
|
|
||||||
|
local function append_list<T>(dest: { T }, src: { T })
|
||||||
|
for _, v in src do
|
||||||
|
table.insert(dest, v)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local ind = " "
|
||||||
|
if indent ~= nil then
|
||||||
|
ind = string.rep(" ", indent)
|
||||||
|
end
|
||||||
|
|
||||||
|
local help_lines: { { string } } = {}
|
||||||
|
|
||||||
|
for _, s in sections do
|
||||||
|
table.insert(help_lines, { s.title })
|
||||||
|
if s.lines ~= nil then
|
||||||
|
local section_lines: { { string } } = {}
|
||||||
|
for _, l in s.lines do
|
||||||
|
if typeof(l) == "string" then
|
||||||
|
table.insert(section_lines, { ind, l })
|
||||||
|
else
|
||||||
|
table.insert(section_lines, { ind, table.unpack(l) })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
append_list(help_lines, align_cols(section_lines))
|
||||||
|
end
|
||||||
|
table.insert(help_lines, {})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- remove last empty line
|
||||||
|
table.remove(help_lines)
|
||||||
|
|
||||||
|
local help_text = ""
|
||||||
|
for _, line in help_lines do
|
||||||
|
help_text ..= table.concat(line, " ") .. "\n"
|
||||||
|
end
|
||||||
|
return help_text
|
||||||
|
end
|
||||||
|
-- subcommand cli, must be first positional arg
|
||||||
|
function M.new_subcommands(name, description: string?, helpkeys: { string }?)
|
||||||
|
-- args that trigger print help + abort
|
||||||
|
local helpset: { [string]: boolean } = DEFAULT_helpset
|
||||||
|
if helpkeys ~= nil then
|
||||||
|
helpset = {}
|
||||||
|
for _, k in helpkeys do
|
||||||
|
helpset[k] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local cli = {}
|
||||||
|
cli.name = name
|
||||||
|
cli.description = description
|
||||||
|
cli._helpkeys = helpkeys
|
||||||
|
cli._helpset = helpset
|
||||||
|
cli._subcommands = {}
|
||||||
|
|
||||||
|
function cli:add_subcommand(name: string, description: string?)
|
||||||
|
validate_subcommand_name_or_error(name)
|
||||||
|
local sc = M.new(name, description, cli._helpkeys)
|
||||||
|
cli._subcommands[name] = sc
|
||||||
|
return sc
|
||||||
|
end
|
||||||
|
|
||||||
|
function cli:parse(args: { string }): (SubcommandParseResult?, string?)
|
||||||
|
if #args == 0 then
|
||||||
|
return nil, "insufficient arguments, required at least 1, got 0"
|
||||||
|
end
|
||||||
|
local command = args[1]
|
||||||
|
assert(command ~= nil)
|
||||||
|
|
||||||
|
-- asking for help?
|
||||||
|
if cli._helpset[command] ~= nil then
|
||||||
|
print(cli:help())
|
||||||
|
process.exit(0)
|
||||||
|
end
|
||||||
|
|
||||||
|
local subcommand = cli._subcommands[command]
|
||||||
|
if subcommand == nil then
|
||||||
|
return nil, `'{command} is not a valid subcommand`
|
||||||
|
end
|
||||||
|
|
||||||
|
local sub_args = { table.unpack(args, 2) }
|
||||||
|
local res, err = subcommand:parse(sub_args)
|
||||||
|
if err ~= nil then
|
||||||
|
return nil, err
|
||||||
|
end
|
||||||
|
assert(res ~= nil)
|
||||||
|
|
||||||
|
return {
|
||||||
|
command = command,
|
||||||
|
result = res,
|
||||||
|
}, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function cli:help(indent: number?): string
|
||||||
|
local help_sections: { HelpSection } = {}
|
||||||
|
|
||||||
|
table.insert(help_sections, { title = `usage: {cli.name} <command>` })
|
||||||
|
|
||||||
|
if cli.description ~= nil then
|
||||||
|
table.insert(help_sections, {
|
||||||
|
title = "description:",
|
||||||
|
lines = { { cli.description } },
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
local subcommand_lines = {}
|
||||||
|
for _, c in cli._subcommands do
|
||||||
|
local desc = if c.description then c.description else ""
|
||||||
|
table.insert(subcommand_lines, { c.name, desc })
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(help_sections, {
|
||||||
|
title = "commands:",
|
||||||
|
lines = subcommand_lines,
|
||||||
|
})
|
||||||
|
|
||||||
|
return make_help(help_sections, indent)
|
||||||
|
end
|
||||||
|
|
||||||
|
return cli
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.new(name: string, description: string?, helpkeys: { string }?)
|
||||||
|
-- args that trigger print help + abort
|
||||||
|
local helpset: { [string]: boolean } = DEFAULT_helpset
|
||||||
|
if helpkeys ~= nil then
|
||||||
|
helpset = {}
|
||||||
|
for _, k in helpkeys do
|
||||||
|
helpset[k] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- I'm fairly certain this is not the right pattern for making objects, but it results in single definition + great LSP so I'm happy
|
||||||
|
local cli = {}
|
||||||
|
cli.name = name
|
||||||
|
cli.description = description
|
||||||
|
cli._helpset = helpset
|
||||||
|
cli._positionals = {} :: { ArgData }
|
||||||
|
cli._flags = {} :: { ArgData }
|
||||||
|
cli._options = {} :: { ArgData }
|
||||||
|
cli._argdata_set = {} :: { [string]: ArgData }
|
||||||
|
cli._required_list = {} :: { string }
|
||||||
|
cli._lookup = {} :: { [string]: ArgData }
|
||||||
|
cli._required_positional_count = 0
|
||||||
|
cli._default_result = { values = {}, flags = {} } :: ParseResult
|
||||||
|
|
||||||
|
local function add_arg_lookups(keys: { string }, arg_data: ArgData)
|
||||||
|
for _, k in keys do
|
||||||
|
if cli._helpset[k] ~= nil then
|
||||||
|
error(`key '{k}' is already used as a help key.`)
|
||||||
|
end
|
||||||
|
validate_key_or_error(k)
|
||||||
|
if cli._lookup[k] ~= nil then
|
||||||
|
error(`key '{k}' already exists.`)
|
||||||
|
end
|
||||||
|
cli._lookup[k] = arg_data
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function add_name_error_on_duplicate(name: string, data: ArgData)
|
||||||
|
if cli._argdata_set[name] ~= nil then
|
||||||
|
error(`arg with name {name} already exists`)
|
||||||
|
end
|
||||||
|
cli._argdata_set[name] = data
|
||||||
|
end
|
||||||
|
|
||||||
|
local function make_safe_options(options: ArgOptions?): ArgOptionsSafe
|
||||||
|
if options == nil then
|
||||||
|
return DEFAULT_OPTIONS
|
||||||
|
end
|
||||||
|
assert(options)
|
||||||
|
|
||||||
|
options.aliases = if options.aliases == nil then {} else options.aliases
|
||||||
|
|
||||||
|
return options :: ArgOptionsSafe
|
||||||
|
end
|
||||||
|
|
||||||
|
-- return is guaranteed to start with at least one '-', if none, adds '--'
|
||||||
|
local function to_lookup_name(name: string): string
|
||||||
|
if name:sub(1, 1) == "-" then
|
||||||
|
return name
|
||||||
|
end
|
||||||
|
return "--" .. name
|
||||||
|
end
|
||||||
|
|
||||||
|
-- return is guaranted to not start with '-'
|
||||||
|
local function to_result_name(name: string): string
|
||||||
|
while name:sub(1, 1) == "-" do
|
||||||
|
name = name:sub(2, -1)
|
||||||
|
end
|
||||||
|
return name
|
||||||
|
end
|
||||||
|
|
||||||
|
function cli:add_positional(name: string, options: ArgOptions?)
|
||||||
|
if name:sub(1, 1) == "-" then
|
||||||
|
error(`invalid arg name '{name}', positional cannot start with dashes`)
|
||||||
|
end
|
||||||
|
|
||||||
|
local options = make_safe_options(options)
|
||||||
|
|
||||||
|
local arg_data: ArgData = {
|
||||||
|
name = name,
|
||||||
|
kind = "POSITIONAL",
|
||||||
|
options = options,
|
||||||
|
}
|
||||||
|
add_name_error_on_duplicate(name, arg_data)
|
||||||
|
if options.default == nil then
|
||||||
|
-- check positional required ordering, can't have a required pos after an optional one
|
||||||
|
local last = cli._positionals[#cli._positionals]
|
||||||
|
if last ~= nil and last.options.default ~= nil then
|
||||||
|
error(
|
||||||
|
`{name} is required, but {last.name} is optional. Cannot have required positional after optional positional`
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(cli._required_list, name)
|
||||||
|
else
|
||||||
|
cli._default_result.values[name] = options.default
|
||||||
|
end
|
||||||
|
table.insert(cli._positionals, arg_data)
|
||||||
|
end
|
||||||
|
|
||||||
|
function cli:add_flag(name: string, options: ArgOptions?)
|
||||||
|
if options and options.default ~= nil then
|
||||||
|
-- these are called constants :)
|
||||||
|
error(`flag {name} has non nil default, default value is not supported for flags`)
|
||||||
|
end
|
||||||
|
|
||||||
|
local options = make_safe_options(options)
|
||||||
|
|
||||||
|
local lookup_name = to_lookup_name(name)
|
||||||
|
name = to_result_name(name)
|
||||||
|
for i, v in options.aliases do
|
||||||
|
options.aliases[i] = to_lookup_name(v)
|
||||||
|
end
|
||||||
|
|
||||||
|
local arg_data: ArgData = {
|
||||||
|
name = name,
|
||||||
|
kind = "FLAG",
|
||||||
|
options = options,
|
||||||
|
}
|
||||||
|
add_name_error_on_duplicate(name, arg_data)
|
||||||
|
add_arg_lookups({ lookup_name, table.unpack(options.aliases) }, arg_data)
|
||||||
|
|
||||||
|
if options.default ~= nil then
|
||||||
|
error(`flag {name} has non nil default value, defaults are not supported for flag args`)
|
||||||
|
end
|
||||||
|
|
||||||
|
cli._default_result.flags[name] = false
|
||||||
|
table.insert(cli._flags, arg_data)
|
||||||
|
end
|
||||||
|
|
||||||
|
function cli:add_option(name: string, options: ArgOptions?)
|
||||||
|
local options = make_safe_options(options)
|
||||||
|
|
||||||
|
local lookup_name = to_lookup_name(name)
|
||||||
|
name = to_result_name(name)
|
||||||
|
for i, v in options.aliases do
|
||||||
|
options.aliases[i] = to_lookup_name(v)
|
||||||
|
end
|
||||||
|
|
||||||
|
local arg_data: ArgData = {
|
||||||
|
name = name,
|
||||||
|
kind = "OPTION",
|
||||||
|
options = options,
|
||||||
|
}
|
||||||
|
add_name_error_on_duplicate(name, arg_data)
|
||||||
|
add_arg_lookups({ lookup_name, table.unpack(options.aliases) }, arg_data)
|
||||||
|
if options.default == nil then
|
||||||
|
table.insert(cli._required_list, name)
|
||||||
|
else
|
||||||
|
if options.default == nil then
|
||||||
|
error(`optional arg {name} must have a default value`)
|
||||||
|
end
|
||||||
|
assert(options.default ~= nil)
|
||||||
|
cli._default_result.values[name] = options.default
|
||||||
|
end
|
||||||
|
table.insert(cli._options, arg_data)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- return: data, err - where data is a table, and err is a string
|
||||||
|
-- errors early, first encountered error ends the parse
|
||||||
|
-- if a help key is found will print help text and exit the process
|
||||||
|
function cli:parse(args: { string }): (ParseResult?, string?)
|
||||||
|
local parsed: ParseResult = { values = {}, flags = {}, fwd_args = {} } :: ParseResult
|
||||||
|
|
||||||
|
local positional_idx = 1
|
||||||
|
local skip_next = false -- used for options
|
||||||
|
for i, arg in args do
|
||||||
|
-- asking for help?
|
||||||
|
if cli._helpset[arg] ~= nil then
|
||||||
|
print(cli:help())
|
||||||
|
process.exit(0)
|
||||||
|
end
|
||||||
|
|
||||||
|
if skip_next then
|
||||||
|
skip_next = false
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
|
-- rest of args should be fwd args
|
||||||
|
if arg == "--" then
|
||||||
|
parsed.fwd_args = { table.unpack(args, i + 1) }
|
||||||
|
break
|
||||||
|
end
|
||||||
|
|
||||||
|
local has_dash = arg:sub(1, 1) == "-"
|
||||||
|
local arg_data: ArgData? = nil
|
||||||
|
local value: string? = nil
|
||||||
|
|
||||||
|
if not has_dash then -- positional
|
||||||
|
if positional_idx > #cli._positionals then
|
||||||
|
return nil, `too many positional arguments, expected {#cli._positionals}.`
|
||||||
|
end
|
||||||
|
arg_data = cli._positionals[positional_idx]
|
||||||
|
positional_idx += 1
|
||||||
|
value = arg
|
||||||
|
else -- flag or option
|
||||||
|
arg_data = cli._lookup[arg]
|
||||||
|
end
|
||||||
|
|
||||||
|
if arg_data == nil then
|
||||||
|
return nil, `unknown flag or option: '{arg}'`
|
||||||
|
end
|
||||||
|
assert(arg_data ~= nil)
|
||||||
|
|
||||||
|
-- handle flags early
|
||||||
|
if arg_data.kind == "FLAG" then
|
||||||
|
parsed.flags[arg_data.name] = true
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
|
-- get option value
|
||||||
|
if arg_data.kind == "OPTION" then
|
||||||
|
if i < #args then
|
||||||
|
value = args[i + 1]
|
||||||
|
skip_next = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- resolve positionals and options
|
||||||
|
if value == nil then
|
||||||
|
return nil, `no value provided for option '{arg}'`
|
||||||
|
end
|
||||||
|
assert(value ~= nil)
|
||||||
|
|
||||||
|
parsed.values[arg_data.name] = value
|
||||||
|
end
|
||||||
|
|
||||||
|
-- check all required options are provided
|
||||||
|
for _, name in cli._required_list do
|
||||||
|
if parsed.values[name] == nil and parsed.flags[name] == nil then
|
||||||
|
local kind = cli._argdata_set[name].kind:lower()
|
||||||
|
return nil, `required {kind} arg '{name}' was not found`
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- apply defaults for missing optional
|
||||||
|
for k, v in cli._default_result.flags do
|
||||||
|
if parsed.flags[k] == nil then
|
||||||
|
parsed.flags[k] = v
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for k, v in cli._default_result.values do
|
||||||
|
if parsed.values[k] == nil then
|
||||||
|
parsed.values[k] = v
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return parsed, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function cli:help(indent: number?): string
|
||||||
|
local function align_cols(rows: { { string } }, sep: string?): { { string } }
|
||||||
|
local sep = if sep == nil then " " else sep
|
||||||
|
local max_col_lengths: { number } = {}
|
||||||
|
for _, row in rows do
|
||||||
|
for i, s in row do
|
||||||
|
if max_col_lengths[i] == nil or s:len() > max_col_lengths[i] then
|
||||||
|
max_col_lengths[i] = s:len()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local aligned_rows: { { string } } = {}
|
||||||
|
for _, row in rows do
|
||||||
|
local line: { string } = {}
|
||||||
|
for i, col in row do
|
||||||
|
table.insert(line, col)
|
||||||
|
if i < #row then
|
||||||
|
local spacing = sep
|
||||||
|
local diff = max_col_lengths[i] - col:len()
|
||||||
|
if diff > 0 then
|
||||||
|
spacing = string.rep(" ", diff) .. sep
|
||||||
|
end
|
||||||
|
table.insert(line, spacing)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.insert(aligned_rows, line)
|
||||||
|
end
|
||||||
|
return aligned_rows
|
||||||
|
end
|
||||||
|
|
||||||
|
local help_sections: { HelpSection } = {}
|
||||||
|
|
||||||
|
-- usage
|
||||||
|
local usage = `usage: {cli.name}`
|
||||||
|
if #cli._flags > 0 or #cli._options > 0 then
|
||||||
|
usage ..= " [options]"
|
||||||
|
end
|
||||||
|
for _, arg in cli._positionals do
|
||||||
|
if arg.options.default == nil then
|
||||||
|
usage ..= ` <{arg.name}>`
|
||||||
|
else
|
||||||
|
usage ..= ` [{arg.name}]`
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(help_sections, { title = usage })
|
||||||
|
|
||||||
|
-- description
|
||||||
|
if cli.description ~= nil then
|
||||||
|
table.insert(help_sections, { title = "description:", lines = { cli.description } })
|
||||||
|
end
|
||||||
|
|
||||||
|
local function make_arg_line(arg: ArgData): { string }
|
||||||
|
local keys = arg.name
|
||||||
|
if arg.kind ~= "POSITIONAL" then
|
||||||
|
keys = `--{arg.name}`
|
||||||
|
for _, a in arg.options.aliases do
|
||||||
|
keys ..= `, {a}`
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local reqdef = ""
|
||||||
|
-- optional & default
|
||||||
|
if arg.kind ~= "FLAG" then
|
||||||
|
if arg.options.default == nil then
|
||||||
|
reqdef ..= "[required]"
|
||||||
|
else
|
||||||
|
assert(arg.options.default)
|
||||||
|
reqdef ..= `[default: '{arg.options.default}']`
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local help = ""
|
||||||
|
if arg.options.help then
|
||||||
|
if help ~= "" then
|
||||||
|
help ..= " "
|
||||||
|
end
|
||||||
|
help ..= arg.options.help
|
||||||
|
end
|
||||||
|
|
||||||
|
return { keys, reqdef, help }
|
||||||
|
end
|
||||||
|
|
||||||
|
-- positionals
|
||||||
|
if #cli._positionals > 0 then
|
||||||
|
local positional_lines: { { string } } = {}
|
||||||
|
for _, arg in cli._positionals do
|
||||||
|
table.insert(positional_lines, make_arg_line(arg))
|
||||||
|
end
|
||||||
|
table.insert(help_sections, { title = "positional arguments:", lines = positional_lines })
|
||||||
|
end
|
||||||
|
|
||||||
|
-- flags
|
||||||
|
if #cli._flags > 0 then
|
||||||
|
local flag_lines: { { string } } = {}
|
||||||
|
for _, arg in cli._flags do
|
||||||
|
table.insert(flag_lines, make_arg_line(arg))
|
||||||
|
end
|
||||||
|
table.insert(help_sections, { title = "flags:", lines = flag_lines })
|
||||||
|
end
|
||||||
|
|
||||||
|
-- options
|
||||||
|
if #cli._options > 0 then
|
||||||
|
local option_lines: { { string } } = {}
|
||||||
|
for _, arg in cli._options do
|
||||||
|
table.insert(option_lines, make_arg_line(arg))
|
||||||
|
end
|
||||||
|
table.insert(help_sections, { title = "options:", lines = option_lines })
|
||||||
|
end
|
||||||
|
|
||||||
|
return make_help(help_sections, indent)
|
||||||
|
end
|
||||||
|
|
||||||
|
return cli
|
||||||
|
end
|
||||||
|
|
||||||
|
export type Cli = typeof(M.new(""))
|
||||||
|
export type CliSubcommands = typeof(M.new_subcommands(""))
|
||||||
|
|
||||||
|
return M
|
135
lune/pkg/progress.luau
Normal file
135
lune/pkg/progress.luau
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
--!strict
|
||||||
|
--> Inspired by Rokit's progress bar: https://github.com/rojo-rbx/rokit/blob/a303faf/src/util/progress.rs
|
||||||
|
-- Original: https://github.com/pesde-pkg/tooling/blob/main/toolchainlib/src/utils/progress.luau
|
||||||
|
--[[
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 pesde-pkg
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
]]
|
||||||
|
local stdio = require("@lune/stdio")
|
||||||
|
local task = require("@lune/task")
|
||||||
|
|
||||||
|
local result = require("./result")
|
||||||
|
|
||||||
|
-- FORMAT: {SPINNER} {MESSAGE} {BAR} {STAGE}
|
||||||
|
local SPINNERS = { "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏" }
|
||||||
|
local BAR_COMPONENT = "▇"
|
||||||
|
local MAX_BAR_LENGTH = 30
|
||||||
|
|
||||||
|
local ProgressBar = {}
|
||||||
|
type ProgressBar = {
|
||||||
|
stages: { { tag: string, message: string } },
|
||||||
|
current_stage_index: number,
|
||||||
|
finished: boolean,
|
||||||
|
paused: boolean,
|
||||||
|
thread: thread?,
|
||||||
|
}
|
||||||
|
export type ProgressBarImpl = typeof(setmetatable({} :: ProgressBar, { __index = ProgressBar }))
|
||||||
|
|
||||||
|
function ProgressBar.new(): ProgressBarImpl
|
||||||
|
return setmetatable(
|
||||||
|
{
|
||||||
|
stages = {},
|
||||||
|
current_stage_index = 1,
|
||||||
|
finished = false,
|
||||||
|
} :: ProgressBar,
|
||||||
|
{
|
||||||
|
__index = ProgressBar,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
function ProgressBar.with_stage(self: ProgressBarImpl, tag: string, msg: string): ProgressBarImpl
|
||||||
|
table.insert(self.stages, { tag = tag, message = msg })
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function ProgressBar.start(self: ProgressBarImpl)
|
||||||
|
local BAR_LENGTH = MAX_BAR_LENGTH // #self.stages
|
||||||
|
local TOTAL_BAR_LENGTH = BAR_LENGTH * #self.stages
|
||||||
|
local BAR = string.rep(BAR_COMPONENT, BAR_LENGTH)
|
||||||
|
local MAX_MESSAGE_LENGTH = 0
|
||||||
|
for _, stage in self.stages do
|
||||||
|
local len = #stage.message
|
||||||
|
if len > MAX_MESSAGE_LENGTH then
|
||||||
|
MAX_MESSAGE_LENGTH = len
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
self.thread = task.spawn(function()
|
||||||
|
while true do
|
||||||
|
if self.paused then
|
||||||
|
task.wait(0.1)
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, spinner in SPINNERS do
|
||||||
|
if self.finished then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local stage = self.stages[self.current_stage_index]
|
||||||
|
stdio.ewrite(
|
||||||
|
`\x1b[2K\x1b[0G{stdio.color("cyan")}{spinner} {stage.message}{stdio.color("reset")}{string.rep(
|
||||||
|
" ",
|
||||||
|
MAX_MESSAGE_LENGTH - #stage.message
|
||||||
|
)} [{stdio.style("dim")}{string.rep(BAR, self.current_stage_index)}{string.rep(
|
||||||
|
" ",
|
||||||
|
TOTAL_BAR_LENGTH - (BAR_LENGTH * self.current_stage_index)
|
||||||
|
)}{stdio.style("reset")}] {stdio.style("bold")}{self.current_stage_index} / {#self.stages}{stdio.style(
|
||||||
|
"reset"
|
||||||
|
)}`
|
||||||
|
)
|
||||||
|
|
||||||
|
task.wait(0.1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
function ProgressBar.stop(self: ProgressBarImpl)
|
||||||
|
-- Trigger upvalue, kill thread and clean progress bar remnant
|
||||||
|
self.finished = true
|
||||||
|
stdio.ewrite("\x1b[2K\x1b[0G")
|
||||||
|
end
|
||||||
|
|
||||||
|
function ProgressBar.pause(self: ProgressBarImpl)
|
||||||
|
self.paused = true
|
||||||
|
stdio.ewrite("\x1b[2K\x1b[0G")
|
||||||
|
end
|
||||||
|
|
||||||
|
function ProgressBar.unpause(self: ProgressBarImpl)
|
||||||
|
self.paused = false
|
||||||
|
end
|
||||||
|
|
||||||
|
function ProgressBar.next_stage(self: ProgressBarImpl): result.Identity<nil>
|
||||||
|
local inc = self.current_stage_index + 1
|
||||||
|
if inc > #self.stages then
|
||||||
|
-- TODO: Make this a result
|
||||||
|
self.finished = true
|
||||||
|
return result(false, "OutOfBounds - Attempted to advance past last stage")
|
||||||
|
end
|
||||||
|
|
||||||
|
self.current_stage_index = inc
|
||||||
|
return result(true, nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
return ProgressBar
|
24
lune/pkg/result.luau
Normal file
24
lune/pkg/result.luau
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
--!strict
|
||||||
|
export type Identity<T> = {
|
||||||
|
ok: true,
|
||||||
|
val: T,
|
||||||
|
} | {
|
||||||
|
ok: false,
|
||||||
|
err: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
local function construct<T>(ok: boolean, value: T & string): Identity<T>
|
||||||
|
if ok then
|
||||||
|
return {
|
||||||
|
ok = true,
|
||||||
|
val = value,
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return {
|
||||||
|
ok = false,
|
||||||
|
err = value,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return (construct :: any) :: (<T>(ok: true, value: T) -> Identity<T>) & (<T>(ok: false, value: string) -> Identity<T>)
|
13
src/CSSharpTemplate.cs
Normal file
13
src/CSSharpTemplate.cs
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
using CounterStrikeSharp.API.Core;
|
||||||
|
|
||||||
|
namespace SimleAdmin;
|
||||||
|
public class CSSharpTemplate : BasePlugin
|
||||||
|
{
|
||||||
|
public override string ModuleName => "CSSharpTemplate";
|
||||||
|
public override string ModuleVersion => "0.1.0";
|
||||||
|
|
||||||
|
public override void Load(bool hotReload)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Hello World!");
|
||||||
|
}
|
||||||
|
}
|
13
src/CSSharpTemplate.csproj
Normal file
13
src/CSSharpTemplate.csproj
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="CounterStrikeSharp.API" Version="1.0.316" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
10
stylua.toml
Normal file
10
stylua.toml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
column_width = 120
|
||||||
|
line_endings = "Unix"
|
||||||
|
indent_type = "Spaces"
|
||||||
|
indent_width = 4
|
||||||
|
quote_style = "AutoPreferDouble"
|
||||||
|
call_parentheses = "Input"
|
||||||
|
collapse_simple_statement = "Never"
|
||||||
|
|
||||||
|
[sort_requires]
|
||||||
|
enabled = true
|
Loading…
Reference in a new issue