Initial push
All checks were successful
Build / Build (push) Successful in 39s

This commit is contained in:
marked 2025-04-05 18:27:50 +02:00
commit e517b193e5
20 changed files with 1237 additions and 0 deletions

View 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
View file

@ -0,0 +1,2 @@
**/bin
**/obj

7
.luaurc Normal file
View file

@ -0,0 +1,7 @@
{
"languageMode": "strict",
"aliases": {
"lune": "~/.lune/.typedefs/0.8.9",
"pkg": "lune/pkg"
}
}

22
CSSharpTemplate.sln Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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

View 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

View 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

View 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
View 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
View 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
View 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
View 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
View 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!");
}
}

View 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
View 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