From f3b18dc54a8b466be5dd6087a1b84ddea0ee7e3a Mon Sep 17 00:00:00 2001
From: "D. Moonfire" <d.moonfire@moonfire.us>
Date: Mon, 5 Sep 2022 16:41:27 -0500
Subject: [PATCH 1/8] feat: initial commit

---
 .config/dotnet-tools.json |  18 ++++++
 .editorconfig             | 122 +++++++++++++++++++++++++++++++++++
 .envrc                    |   2 +
 .gitignore                |  25 ++++++++
 .prettierignore           |   8 +++
 .woodpecker.yml           |  36 +++++++++++
 CODE-OF-CONDUCT.md        | 132 ++++++++++++++++++++++++++++++++++++++
 LICENSE.txt               |  21 ++++++
 NuGet.Config              |  16 +++++
 flake.lock                |  42 ++++++++++++
 flake.nix                 |  24 +++++++
 lefthook.yml              |  22 +++++++
 scripts/README.md         |  23 +++++++
 scripts/build.sh          |   7 ++
 scripts/format.sh         |   4 ++
 scripts/release.sh        |  48 ++++++++++++++
 scripts/setup.sh          |  28 ++++++++
 scripts/test.sh           |   8 +++
 src/README.md             |   1 +
 tests/README.md           |   1 +
 20 files changed, 588 insertions(+)
 create mode 100644 .config/dotnet-tools.json
 create mode 100644 .editorconfig
 create mode 100644 .envrc
 create mode 100644 .gitignore
 create mode 100644 .prettierignore
 create mode 100644 .woodpecker.yml
 create mode 100644 CODE-OF-CONDUCT.md
 create mode 100644 LICENSE.txt
 create mode 100644 NuGet.Config
 create mode 100644 flake.lock
 create mode 100644 flake.nix
 create mode 100644 lefthook.yml
 create mode 100644 scripts/README.md
 create mode 100755 scripts/build.sh
 create mode 100755 scripts/format.sh
 create mode 100755 scripts/release.sh
 create mode 100755 scripts/setup.sh
 create mode 100755 scripts/test.sh
 create mode 100644 src/README.md
 create mode 100644 tests/README.md

diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
new file mode 100644
index 0000000..7720a5a
--- /dev/null
+++ b/.config/dotnet-tools.json
@@ -0,0 +1,18 @@
+{
+  "version": 1,
+  "isRoot": true,
+  "tools": {
+    "gitversion.tool": {
+      "version": "5.9.0",
+      "commands": [
+        "dotnet-gitversion"
+      ]
+    },
+    "dotnet-reportgenerator-globaltool": {
+      "version": "5.1.3",
+      "commands": [
+        "reportgenerator"
+      ]
+    }
+  }
+}
\ No newline at end of file
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..819c8e1
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,122 @@
+# EditorConfig is awesome: https://EditorConfig.org
+
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+indent_style = space
+indent_size = 4
+
+# Microsoft .NET properties
+csharp_new_line_before_members_in_object_initializers = false
+csharp_preferred_modifier_order = public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion
+csharp_space_after_cast = false
+csharp_style_var_elsewhere = false:hint
+csharp_style_var_for_built_in_types = false:hint
+csharp_style_var_when_type_is_apparent = true:hint
+csharp_preserve_single_line_statements = false
+csharp_preserve_single_line_blocks = true
+dotnet_style_predefined_type_for_locals_parameters_members = true:hint
+dotnet_style_predefined_type_for_member_access = true:hint
+dotnet_style_qualification_for_event = true:hint
+dotnet_style_qualification_for_field = true:hint
+dotnet_style_qualification_for_method = true:hint
+dotnet_style_qualification_for_property = true:hint
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:hint
+
+# ReSharper properties
+resharper_alignment_tab_fill_style = optimal_fill
+resharper_apply_on_completion = true
+resharper_blank_lines_after_control_transfer_statements = 1
+resharper_blank_lines_around_single_line_auto_property = 1
+resharper_blank_lines_around_single_line_property = 1
+resharper_blank_lines_before_single_line_comment = 1
+resharper_blank_lines_between_using_groups = 1
+resharper_braces_for_for = required
+resharper_braces_for_foreach = required
+resharper_braces_for_ifelse = required
+resharper_braces_for_while = required
+resharper_can_use_global_alias = false
+resharper_csharp_blank_lines_around_single_line_field = 1
+resharper_csharp_blank_lines_around_single_line_invocable = 1
+resharper_csharp_indent_style = tab
+resharper_csharp_insert_final_newline = true
+resharper_csharp_keep_blank_lines_in_code = 1
+resharper_csharp_keep_blank_lines_in_declarations = 1
+resharper_csharp_new_line_before_while = true
+resharper_csharp_use_indent_from_vs = false
+resharper_csharp_wrap_arguments_style = chop_if_long
+resharper_csharp_wrap_extends_list_style = chop_if_long
+resharper_csharp_wrap_parameters_style = chop_if_long
+resharper_css_insert_final_newline = false
+resharper_enforce_line_ending_style = true
+resharper_html_insert_final_newline = false
+resharper_indent_nested_fixed_stmt = true
+resharper_js_indent_style = tab
+resharper_js_insert_final_newline = true
+resharper_js_keep_blank_lines_in_code = 1
+resharper_js_stick_comment = false
+resharper_js_use_indent_from_vs = false
+resharper_js_wrap_before_binary_opsign = true
+resharper_js_wrap_chained_method_calls = chop_if_long
+resharper_keep_blank_lines_between_declarations = 1
+resharper_min_blank_lines_after_imports = 1
+resharper_place_attribute_on_same_line = False
+resharper_place_constructor_initializer_on_same_line = false
+resharper_place_type_constraints_on_same_line = false
+resharper_protobuf_insert_final_newline = false
+resharper_qualified_using_at_nested_scope = true
+resharper_resx_insert_final_newline = false
+resharper_space_within_single_line_array_initializer_braces = true
+resharper_use_indents_from_main_language_in_file = false
+resharper_vb_insert_final_newline = false
+resharper_wrap_after_declaration_lpar = true
+resharper_wrap_after_invocation_lpar = true
+resharper_wrap_before_extends_colon = true
+resharper_wrap_before_first_type_parameter_constraint = true
+resharper_wrap_before_type_parameter_langle = true
+resharper_xmldoc_indent_child_elements = ZeroIndent
+resharper_xmldoc_indent_text = ZeroIndent
+resharper_xmldoc_insert_final_newline = false
+resharper_xml_insert_final_newline = false
+
+# ReSharper inspection severities
+resharper_check_namespace_highlighting = none
+resharper_convert_to_auto_property_highlighting = none
+resharper_localizable_element_highlighting = none
+resharper_redundant_comma_in_attribute_list_highlighting = none
+resharper_redundant_comma_in_enum_declaration_highlighting = none
+resharper_redundant_comma_in_initializer_highlighting = none
+resharper_string_compare_to_is_culture_specific_highlighting = none
+resharper_string_index_of_is_culture_specific_1_highlighting = none
+resharper_use_null_propagation_highlighting = none
+resharper_use_object_or_collection_initializer_highlighting = hint
+resharper_use_string_interpolation_highlighting = hint
+
+# Matches the exact files either package.json or .travis.yml
+[{package.json,.travis.yml}]
+indent_style = space
+indent_size = 2
+tab_width = 2
+
+[*.{cs,js,json,jsx,proto,resjson,ts,tsx}]
+indent_style = space
+indent_size = 4
+tab_width = 4
+
+[*.{asax,ascx,aspx,cshtml,css,htm,html,master,razor,skin,vb,xaml,xamlx,xoml}]
+indent_style = space
+indent_size = 4
+tab_width = 4
+
+[*.{appxmanifest,build,config,csproj,dbml,discomap,dtd,jsproj,lsproj,njsproj,nuspec,proj,props,resw,resx,StyleCop,targets,tasks,vbproj,xml,xsd}]
+indent_style = space
+indent_size = 2
+tab_width = 2
+
+[*.proto]
+indent_style = space
+indent_size = 2
+tab_width = 2
diff --git a/.envrc b/.envrc
new file mode 100644
index 0000000..5816063
--- /dev/null
+++ b/.envrc
@@ -0,0 +1,2 @@
+export PATH=$PWD/scripts:$PATH
+use flake || use nix
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..53987a3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,25 @@
+launchSettings.json
+
+*~
+*.user
+Directory.Build.props
+
+obj/
+[Bb]in/
+.vs/
+.vscode/
+.idea/
+_ReSharper.Caches/
+node_modules/
+
+# NixOS
+.direnv/
+
+# Tests and Coverage
+coverage
+TestResults/
+tests/artifacts/
+
+# Lefthook
+.lefthook-local/
+lefthook-local.yml
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000..f1a0e24
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,8 @@
+*~
+flake.*
+
+node_modules/
+.direnv/
+.config/
+obj/
+bin/
diff --git a/.woodpecker.yml b/.woodpecker.yml
new file mode 100644
index 0000000..3f25d63
--- /dev/null
+++ b/.woodpecker.yml
@@ -0,0 +1,36 @@
+clone:
+    git:
+        image: woodpeckerci/plugin-git
+        settings:
+            tags: true
+
+pipeline:
+    build:
+        image: registry.gitlab.com/dmoonfire/nix-flake-docker:latest
+        commands:
+            - nix develop --command scripts/build.sh
+        when:
+            event: [push, pull_request, tag]
+            tag: v*
+
+    test:
+        image: registry.gitlab.com/dmoonfire/nix-flake-docker:latest
+        commands:
+            - nix develop --command scripts/test.sh
+        when:
+            event: [push, pull_request]
+        #paths:
+        #    - ./**/*test-result.xml
+        #    - ./coverage/Cobertura.xml
+        #    - ./coverage/Summary.*
+        #    - ./**/*.nupkg
+
+    release-main:
+        image: registry.gitlab.com/dmoonfire/nix-flake-docker:latest
+        commands:
+            - nix develop --command scripts/release.sh
+        secrets:
+            - gitea_token
+        when:
+            event: push
+            branch: main
diff --git a/CODE-OF-CONDUCT.md b/CODE-OF-CONDUCT.md
new file mode 100644
index 0000000..5aa66ee
--- /dev/null
+++ b/CODE-OF-CONDUCT.md
@@ -0,0 +1,132 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, caste, color, religion, or sexual
+identity and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment for our
+community include:
+
+-   Demonstrating empathy and kindness toward other people
+-   Being respectful of differing opinions, viewpoints, and experiences
+-   Giving and gracefully accepting constructive feedback
+-   Accepting responsibility and apologizing to those affected by our mistakes,
+    and learning from the experience
+-   Focusing on what is best not just for us as individuals, but for the overall
+    community
+
+Examples of unacceptable behavior include:
+
+-   The use of sexualized language or imagery, and sexual attention or advances of
+    any kind
+-   Trolling, insulting or derogatory comments, and personal or political attacks
+-   Public or private harassment
+-   Publishing others' private information, such as a physical or email address,
+    without their explicit permission
+-   Other conduct which could reasonably be considered inappropriate in a
+    professional setting
+
+## Enforcement Responsibilities
+
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
+
+Community leaders have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for moderation
+decisions when appropriate.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official e-mail address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported to the community leaders responsible for enforcement at
+[INSERT CONTACT METHOD].
+All complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
+
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series of
+actions.
+
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or permanent
+ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior, harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within the
+community.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.1, available at
+[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
+
+Community Impact Guidelines were inspired by
+[Mozilla's code of conduct enforcement ladder][mozilla coc].
+
+For answers to common questions about this code of conduct, see the FAQ at
+[https://www.contributor-covenant.org/faq][faq]. Translations are available at
+[https://www.contributor-covenant.org/translations][translations].
+
+[homepage]: https://www.contributor-covenant.org
+[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
+[mozilla coc]: https://github.com/mozilla/diversity
+[faq]: https://www.contributor-covenant.org/faq
+[translations]: https://www.contributor-covenant.org/translations
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..02b160f
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) Moonfire Games
+
+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.
diff --git a/NuGet.Config b/NuGet.Config
new file mode 100644
index 0000000..ff47af7
--- /dev/null
+++ b/NuGet.Config
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<configuration>
+    <packageSources>
+    <clear />
+    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
+    <add key="mfgames.com" value="https://src.mfgames.com/api/packages/mfgames-cil/nuget/index.json" protocolVersion="3" />
+  </packageSources>
+  <packageSourceMapping>
+    <packageSource key="nuget.org">
+      <package pattern="*" />
+    </packageSource>
+    <packageSource key="mfgames.com">
+      <package pattern="MfGames.*" />
+    </packageSource>
+  </packageSourceMapping>
+</configuration>
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 0000000..b67b61d
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,42 @@
+{
+  "nodes": {
+    "flake-utils": {
+      "locked": {
+        "lastModified": 1659877975,
+        "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
+        "type": "github"
+      },
+      "original": {
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "type": "github"
+      }
+    },
+    "nixpkgs": {
+      "locked": {
+        "lastModified": 1662019588,
+        "narHash": "sha256-oPEjHKGGVbBXqwwL+UjsveJzghWiWV0n9ogo1X6l4cw=",
+        "owner": "NixOS",
+        "repo": "nixpkgs",
+        "rev": "2da64a81275b68fdad38af669afeda43d401e94b",
+        "type": "github"
+      },
+      "original": {
+        "id": "nixpkgs",
+        "ref": "nixos-unstable",
+        "type": "indirect"
+      }
+    },
+    "root": {
+      "inputs": {
+        "flake-utils": "flake-utils",
+        "nixpkgs": "nixpkgs"
+      }
+    }
+  },
+  "root": "root",
+  "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000..3de25d3
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,24 @@
+{
+  description = "A .NET core library for easily building CLI tools";
+
+  inputs = {
+    nixpkgs.url = "nixpkgs/nixos-unstable";
+    flake-utils.url = "github:numtide/flake-utils";
+  };
+
+  outputs = { self, nixpkgs, flake-utils }:
+    flake-utils.lib.eachDefaultSystem (system:
+      let pkgs = nixpkgs.legacyPackages.${system};
+      in {
+        devShell = pkgs.mkShell {
+          buildInputs = [
+            pkgs.dotnet-sdk
+            pkgs.lefthook
+            pkgs.convco
+            pkgs.nodePackages.prettier
+            pkgs.nixfmt
+            pkgs.jq
+          ];
+        };
+      });
+}
diff --git a/lefthook.yml b/lefthook.yml
new file mode 100644
index 0000000..43a0f2b
--- /dev/null
+++ b/lefthook.yml
@@ -0,0 +1,22 @@
+pre-commit:
+    parallel: true
+    commands:
+        dotnet-format:
+            glob: "*.cs"
+            run: dotnet format
+        prettier:
+            run: prettier . --write --loglevel warn
+        nixfmt:
+            run: nixfmt flake.nix
+
+commit-msg:
+    commands:
+        commit-check:
+            run: convco check -n 1
+
+skip_output:
+    - meta # Skips lefthook version printing
+    - summary # Skips summary block (successful and failed steps) printing
+    - success # Skips successful steps printing
+    - failure # Skips failed steps printing
+    - execution # Skips printing successfully executed commands and their output (but still prints failed executions)
diff --git a/scripts/README.md b/scripts/README.md
new file mode 100644
index 0000000..f357938
--- /dev/null
+++ b/scripts/README.md
@@ -0,0 +1,23 @@
+# Scripts Directory
+
+This directory contains the basic scripts for working with the library.
+
+## `setup.sh`
+
+This verifies the environment is correct and makes sure everything is configured.
+
+## `build.sh`
+
+This builds the project and creates the binaries in debug mode.
+
+## `test.sh`
+
+This runs any required tests.
+
+## `format.sh`
+
+This is used to format the code base using our standards. It matches the commands in the `lefthook` pre-commit hook.
+
+## `release.sh`
+
+Intended to run in a CI environment, this creates a NuGet package and publishes it.
diff --git a/scripts/build.sh b/scripts/build.sh
new file mode 100755
index 0000000..66f32c5
--- /dev/null
+++ b/scripts/build.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env sh
+
+cd $(dirname $0)/..
+./scripts/setup.sh || exit 1
+
+echo "$(basename $0): building project"
+dotnet build
diff --git a/scripts/format.sh b/scripts/format.sh
new file mode 100755
index 0000000..7b31af0
--- /dev/null
+++ b/scripts/format.sh
@@ -0,0 +1,4 @@
+#!/usr/bin/env sh
+
+cd $(dirname $0)/..
+lefthook run pre-commit
diff --git a/scripts/release.sh b/scripts/release.sh
new file mode 100755
index 0000000..6e27208
--- /dev/null
+++ b/scripts/release.sh
@@ -0,0 +1,48 @@
+#!/usr/bin/env sh
+
+cd $(dirname $0)/..
+./scripts/setup.sh || exit 1
+
+# Verify the input.
+if [ "x$GITEA_TOKEN" = "x" ]
+then
+    echo "the environment variable GITEA_TOKEN is not defined"
+    exit 1
+fi
+
+# Clean up everything from the previous runs.
+echo "$(basename $0): cleaning project"
+dotnet clean
+
+# Version the file based on the Git repository.
+echo "$(basename $0): setting project version"
+(cd src && dotnet dotnet-gitversion /updateprojectfiles)
+SEMVER="v$(dotnet gitversion /output json | jq -r .SemVer)"
+
+if [ "x$SEMVER" = "x" ]
+then
+  echo "$(basename $0): cannot figure out the semantic version"
+  exit 1
+fi
+
+# Build to pick up the new version.
+echo "$(basename $0): building project $SEMVER"
+dotnet build || exit 1
+
+# Create and publish the NuGet packages.
+echo "$(basename $0): registering NuGet source"
+dotnet nuget add source --name mfgames --username dmoonfire --password $GITEA_TOKEN https://src.mfgames.com/api/packages/mfgames-cil/nuget/index.json --store-password-in-clear-text || exit 1
+
+echo "$(basename $0): publishing NuGet package"
+dotnet pack --include-symbols --include-source || exit 1
+dotnet nuget push --source mfgames src/*/bin/Debug/*.nupkg || exit 1
+
+# Tag and push, but only if we don't have a tag.
+if ! git tag | grep $SEMVER >& /dev/null
+then
+  echo "$(basename $0): tagging and pushing"
+  git tag $SEMVER
+  git push origin $SEMVER
+else
+  echo "$(basename $0): not tagging, already exists"
+fi
diff --git a/scripts/setup.sh b/scripts/setup.sh
new file mode 100755
index 0000000..3ee77ae
--- /dev/null
+++ b/scripts/setup.sh
@@ -0,0 +1,28 @@
+#!/usr/bin/env sh
+
+# Normalize our environment.
+cd $(dirname $0)/..
+
+# Make sure we have the needed executables installed.
+for e in dotnet lefthook prettier nixfmt
+do
+  if ! which $e >& /dev/null
+  then
+      echo "Cannot find '$e' in the path"
+      exit 1
+  fi
+done
+
+# Make sure we have lefthook is installed.
+if [ ! -f .git/hooks/pre-commit ]
+then
+  echo "$(basename $0): installing lefthook"
+  lefthook install
+fi
+
+# Make sure our tools are installed.
+echo "$(basename $0): install .NET tools"
+dotnet tool restore
+
+# Everything is good.
+exit 0
diff --git a/scripts/test.sh b/scripts/test.sh
new file mode 100755
index 0000000..bb106ee
--- /dev/null
+++ b/scripts/test.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env sh
+
+cd $(dirname $0)/..
+./scripts/setup.sh || exit 1
+
+dotnet test --test-adapter-path:. --logger:"junit;LogFilePath=../artifacts/{assembly}-test-result.xml;MethodFormat=Default;FailureBodyFormat=Verbose" --collect:"XPlat Code Coverage"
+dotnet tool run reportgenerator -reports:tests/*/TestResults/*/coverage.cobertura.xml -targetdir:./coverage "-reporttypes:Cobertura;TextSummary"
+grep "Line coverage" coverage/Summary.txt
diff --git a/src/README.md b/src/README.md
new file mode 100644
index 0000000..cbaf845
--- /dev/null
+++ b/src/README.md
@@ -0,0 +1 @@
+# Source Directories
diff --git a/tests/README.md b/tests/README.md
new file mode 100644
index 0000000..6756246
--- /dev/null
+++ b/tests/README.md
@@ -0,0 +1 @@
+# Test Projects

From 4093d9c0cbbeaea20adcea5022e03e82e08d843a Mon Sep 17 00:00:00 2001
From: "D. Moonfire" <d.moonfire@moonfire.us>
Date: Mon, 5 Sep 2022 16:56:41 -0500
Subject: [PATCH 2/8] fix(template): trying a tag push

---
 scripts/release.sh | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/scripts/release.sh b/scripts/release.sh
index 6e27208..e8a7094 100755
--- a/scripts/release.sh
+++ b/scripts/release.sh
@@ -35,14 +35,16 @@ dotnet nuget add source --name mfgames --username dmoonfire --password $GITEA_TO
 
 echo "$(basename $0): publishing NuGet package"
 dotnet pack --include-symbols --include-source || exit 1
-dotnet nuget push --source mfgames src/*/bin/Debug/*.nupkg || exit 1
+dotnet nuget push --source mfgames src/*/bin/Debug/*nupkg || exit 1
 
 # Tag and push, but only if we don't have a tag.
 if ! git tag | grep $SEMVER >& /dev/null
 then
   echo "$(basename $0): tagging and pushing"
+  git remote add publish https://dmoonfire:$GITEA_TOKEN@src.mfgames.com/mfgames-cil/$(basename $(git config --get remote.origin.url))
   git tag $SEMVER
-  git push origin $SEMVER
+  git push publish $SEMVER || exit 1
+  git remote remove publish
 else
   echo "$(basename $0): not tagging, already exists"
 fi

From 294d9c0a38b792e57e1b731b475e7432ae4d98e7 Mon Sep 17 00:00:00 2001
From: "D. Moonfire" <d.moonfire@moonfire.us>
Date: Mon, 5 Sep 2022 17:05:52 -0500
Subject: [PATCH 3/8] fix(release): changing NuGet publish and pack

---
 scripts/release.sh | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/scripts/release.sh b/scripts/release.sh
index e8a7094..db55a27 100755
--- a/scripts/release.sh
+++ b/scripts/release.sh
@@ -31,11 +31,12 @@ dotnet build || exit 1
 
 # Create and publish the NuGet packages.
 echo "$(basename $0): registering NuGet source"
-dotnet nuget add source --name mfgames --username dmoonfire --password $GITEA_TOKEN https://src.mfgames.com/api/packages/mfgames-cil/nuget/index.json --store-password-in-clear-text || exit 1
+dotnet nuget remove source mfgames.publish
+dotnet nuget add source --name mfgames.publish --username dmoonfire --password $GITEA_TOKEN https://src.mfgames.com/api/packages/mfgames-cil/nuget/index.json --store-password-in-clear-text || exit 1
 
 echo "$(basename $0): publishing NuGet package"
-dotnet pack --include-symbols --include-source || exit 1
-dotnet nuget push --source mfgames src/*/bin/Debug/*nupkg || exit 1
+dotnet pack -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg || exit 1
+dotnet nuget push --source mfgames.publish src/*/bin/Debug/*nupkg || exit 1
 
 # Tag and push, but only if we don't have a tag.
 if ! git tag | grep $SEMVER >& /dev/null

From 0603603be06cd529f76055c379c6610a338efa93 Mon Sep 17 00:00:00 2001
From: "D. Moonfire" <d.moonfire@moonfire.us>
Date: Mon, 5 Sep 2022 17:15:21 -0500
Subject: [PATCH 4/8] chore: tweaking NuGet publish process

---
 scripts/release.sh | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/scripts/release.sh b/scripts/release.sh
index db55a27..5024656 100755
--- a/scripts/release.sh
+++ b/scripts/release.sh
@@ -31,12 +31,13 @@ dotnet build || exit 1
 
 # Create and publish the NuGet packages.
 echo "$(basename $0): registering NuGet source"
-dotnet nuget remove source mfgames.publish
-dotnet nuget add source --name mfgames.publish --username dmoonfire --password $GITEA_TOKEN https://src.mfgames.com/api/packages/mfgames-cil/nuget/index.json --store-password-in-clear-text || exit 1
+dotnet nuget remove source publish >& /dev/null
+dotnet nuget add source --name publish --username dmoonfire --password $GITEA_TOKEN https://src.mfgames.com/api/packages/mfgames-cil/nuget/index.json --store-password-in-clear-text || exit 1
 
 echo "$(basename $0): publishing NuGet package"
 dotnet pack -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg || exit 1
-dotnet nuget push --source mfgames.publish src/*/bin/Debug/*nupkg || exit 1
+dotnet nuget push --source publish src/*/bin/Debug/*nupkg || exit 1
+dotnet nuget remove source publish >& /dev/null
 
 # Tag and push, but only if we don't have a tag.
 if ! git tag | grep $SEMVER >& /dev/null

From 962e1f0d11ce9b533718d8d0850fd79f9ed91f4f Mon Sep 17 00:00:00 2001
From: "D. Moonfire" <d.moonfire@moonfire.us>
Date: Mon, 5 Sep 2022 17:25:28 -0500
Subject: [PATCH 5/8] chore(release): removing same source as in NuGet.config

---
 scripts/release.sh | 11 +++++------
 1 file changed, 5 insertions(+), 6 deletions(-)

diff --git a/scripts/release.sh b/scripts/release.sh
index 5024656..996c268 100755
--- a/scripts/release.sh
+++ b/scripts/release.sh
@@ -30,14 +30,13 @@ echo "$(basename $0): building project $SEMVER"
 dotnet build || exit 1
 
 # Create and publish the NuGet packages.
-echo "$(basename $0): registering NuGet source"
-dotnet nuget remove source publish >& /dev/null
-dotnet nuget add source --name publish --username dmoonfire --password $GITEA_TOKEN https://src.mfgames.com/api/packages/mfgames-cil/nuget/index.json --store-password-in-clear-text || exit 1
+echo "$(basename $0): creating NuGet packages"
+dotnet pack -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg || exit 1
 
 echo "$(basename $0): publishing NuGet package"
-dotnet pack -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg || exit 1
-dotnet nuget push --source publish src/*/bin/Debug/*nupkg || exit 1
-dotnet nuget remove source publish >& /dev/null
+dotnet nuget remove source mfgames.com >& /dev/null
+dotnet nuget add source --name mfgames.com --username dmoonfire --password $GITEA_TOKEN https://src.mfgames.com/api/packages/mfgames-cil/nuget/index.json --store-password-in-clear-text || exit 1
+dotnet nuget push --source mfgames.com src/*/bin/Debug/*nupkg || exit 1
 
 # Tag and push, but only if we don't have a tag.
 if ! git tag | grep $SEMVER >& /dev/null

From 152428b235f5edec2d848d97f1574b456976b044 Mon Sep 17 00:00:00 2001
From: "D. Moonfire" <d.moonfire@moonfire.us>
Date: Mon, 5 Sep 2022 17:28:14 -0500
Subject: [PATCH 6/8] chore: added an update-template script

---
 scripts/update-template.sh | 4 ++++
 1 file changed, 4 insertions(+)
 create mode 100755 scripts/update-template.sh

diff --git a/scripts/update-template.sh b/scripts/update-template.sh
new file mode 100755
index 0000000..2d80710
--- /dev/null
+++ b/scripts/update-template.sh
@@ -0,0 +1,4 @@
+#!/usr/bin/env sh
+
+cd $(dirname $0)/..
+git pull template main --no-rebase

From aa1eae40e0cc8550143b8a539c7830be688c24e6 Mon Sep 17 00:00:00 2001
From: "D. Moonfire" <d.moonfire@moonfire.us>
Date: Mon, 5 Sep 2022 17:44:50 -0500
Subject: [PATCH 7/8] chore(release): modifying NuGet publishing

---
 scripts/release.sh | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/scripts/release.sh b/scripts/release.sh
index 996c268..94ed900 100755
--- a/scripts/release.sh
+++ b/scripts/release.sh
@@ -36,7 +36,7 @@ dotnet pack -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg || exit 1
 echo "$(basename $0): publishing NuGet package"
 dotnet nuget remove source mfgames.com >& /dev/null
 dotnet nuget add source --name mfgames.com --username dmoonfire --password $GITEA_TOKEN https://src.mfgames.com/api/packages/mfgames-cil/nuget/index.json --store-password-in-clear-text || exit 1
-dotnet nuget push --source mfgames.com src/*/bin/Debug/*nupkg || exit 1
+dotnet nuget push --skip-duplicate --source mfgames.com src/*/bin/Debug/*.nupkg || exit 1
 
 # Tag and push, but only if we don't have a tag.
 if ! git tag | grep $SEMVER >& /dev/null

From 16880cb36e748cd195e5ad85c4100e4209cc594c Mon Sep 17 00:00:00 2001
From: "D. Moonfire" <d.moonfire@moonfire.us>
Date: Mon, 5 Sep 2022 22:23:39 -0500
Subject: [PATCH 8/8] chore: making tests more intelligent

---
 scripts/test.sh | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)

diff --git a/scripts/test.sh b/scripts/test.sh
index bb106ee..7587fb1 100755
--- a/scripts/test.sh
+++ b/scripts/test.sh
@@ -1,8 +1,12 @@
 #!/usr/bin/env sh
 
 cd $(dirname $0)/..
-./scripts/setup.sh || exit 1
 
-dotnet test --test-adapter-path:. --logger:"junit;LogFilePath=../artifacts/{assembly}-test-result.xml;MethodFormat=Default;FailureBodyFormat=Verbose" --collect:"XPlat Code Coverage"
-dotnet tool run reportgenerator -reports:tests/*/TestResults/*/coverage.cobertura.xml -targetdir:./coverage "-reporttypes:Cobertura;TextSummary"
-grep "Line coverage" coverage/Summary.txt
+if [ -f ./tests/*/*.csproj ]
+then
+  ./scripts/setup.sh || exit 1
+
+  dotnet test --test-adapter-path:. --logger:"junit;LogFilePath=../artifacts/{assembly}-test-result.xml;MethodFormat=Default;FailureBodyFormat=Verbose" --collect:"XPlat Code Coverage"
+  dotnet tool run reportgenerator -reports:tests/*/TestResults/*/coverage.cobertura.xml -targetdir:./coverage "-reporttypes:Cobertura;TextSummary"
+  grep "Line coverage" coverage/Summary.txt
+fi