diff --git a/.copier-answers.yml b/.copier-answers.yml
new file mode 100644
index 0000000..798ea84
--- /dev/null
+++ b/.copier-answers.yml
@@ -0,0 +1,23 @@
+# Changes here will be overwritten by Copier
+_commit: 1.5.0
+_src_path: gh:pawamoy/copier-uv
+author_email: dev@pawamoy.fr
+author_fullname: Timothée Mazzucotelli
+author_username: pawamoy
+copyright_date: '2023'
+copyright_holder: Timothée Mazzucotelli
+copyright_holder_email: dev@pawamoy.fr
+copyright_license: ISC License
+insiders: true
+insiders_email: insiders@pawamoy.fr
+insiders_repository_name: insiders-project
+project_description: Manage your Insiders projects.
+project_name: insiders
+public_release: true
+python_package_command_line_name: insiders
+python_package_distribution_name: insiders
+python_package_import_name: insiders
+repository_name: insiders-project
+repository_namespace: pawamoy
+repository_provider: github.com
+
diff --git a/.envrc b/.envrc
new file mode 100644
index 0000000..f9d77ee
--- /dev/null
+++ b/.envrc
@@ -0,0 +1 @@
+PATH_add scripts
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..a502284
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,5 @@
+github: pawamoy
+ko_fi: pawamoy
+polar: pawamoy
+custom:
+- https://www.paypal.me/pawamoy
diff --git a/.github/ISSUE_TEMPLATE/1-bug.md b/.github/ISSUE_TEMPLATE/1-bug.md
new file mode 100644
index 0000000..3ad8fa3
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/1-bug.md
@@ -0,0 +1,61 @@
+---
+name: Bug report
+about: Create a bug report to help us improve.
+title: "bug: "
+labels: unconfirmed
+assignees: [pawamoy]
+---
+
+### Description of the bug
+
+
+### To Reproduce
+
+
+```
+WRITE MRE / INSTRUCTIONS HERE
+```
+
+### Full traceback
+
+
+Full traceback
+
+```python
+PASTE TRACEBACK HERE
+```
+
+
+
+### Expected behavior
+
+
+### Environment information
+
+
+```bash
+insiders --debug-info # | xclip -selection clipboard
+```
+
+PASTE MARKDOWN OUTPUT HERE
+
+### Additional context
+
diff --git a/.github/ISSUE_TEMPLATE/2-feature.md b/.github/ISSUE_TEMPLATE/2-feature.md
new file mode 100644
index 0000000..2df98fb
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/2-feature.md
@@ -0,0 +1,19 @@
+---
+name: Feature request
+about: Suggest an idea for this project.
+title: "feature: "
+labels: feature
+assignees: pawamoy
+---
+
+### Is your feature request related to a problem? Please describe.
+
+
+### Describe the solution you'd like
+
+
+### Describe alternatives you've considered
+
+
+### Additional context
+
diff --git a/.github/ISSUE_TEMPLATE/3-docs.md b/.github/ISSUE_TEMPLATE/3-docs.md
new file mode 100644
index 0000000..92ac8ec
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/3-docs.md
@@ -0,0 +1,16 @@
+---
+name: Documentation update
+about: Point at unclear, missing or outdated documentation.
+title: "docs: "
+labels: docs
+assignees: pawamoy
+---
+
+### Is something unclear, missing or outdated in our documentation?
+
+
+### Relevant code snippets
+
+
+### Link to the relevant documentation section
+
diff --git a/.github/ISSUE_TEMPLATE/4-change.md b/.github/ISSUE_TEMPLATE/4-change.md
new file mode 100644
index 0000000..dc9a8f1
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/4-change.md
@@ -0,0 +1,18 @@
+---
+name: Change request
+about: Suggest any other kind of change for this project.
+title: "change: "
+assignees: pawamoy
+---
+
+### Is your change request related to a problem? Please describe.
+
+
+### Describe the solution you'd like
+
+
+### Describe alternatives you've considered
+
+
+### Additional context
+
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..45e3514
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,5 @@
+blank_issues_enabled: false
+contact_links:
+- name: I have a question / I need help
+ url: https://github.com/pawamoy/insiders-project/discussions/new?category=q-a
+ about: Ask and answer questions in the Discussions tab.
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 6e0db04..7b98c96 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,8 +1,126 @@
+name: ci
on:
- push:
+ push:
+ pull_request:
+ branches:
+ - main
+
+defaults:
+ run:
+ shell: bash
+
+env:
+ LANG: en_US.utf-8
+ LC_ALL: en_US.utf-8
+ PYTHONIOENCODING: UTF-8
+ PYTHON_VERSIONS: ""
+
jobs:
- all:
- runs-on: ubuntu-latest
- steps:
- - run: echo "Nothing to do!"
+
+ quality:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Fetch all tags
+ run: git fetch --depth=1 --tags
+
+ - name: Setup Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.12"
+
+ - name: Setup uv
+ uses: astral-sh/setup-uv@v3
+ with:
+ enable-cache: true
+ cache-dependency-glob: pyproject.toml
+
+ - name: Install dependencies
+ run: make setup
+
+ - name: Check if the documentation builds correctly
+ run: make check-docs
+
+ - name: Check the code quality
+ run: make check-quality
+
+ - name: Check if the code is correctly typed
+ run: make check-types
+
+ - name: Check for breaking changes in the API
+ run: make check-api
+
+ exclude-test-jobs:
+ runs-on: ubuntu-latest
+ outputs:
+ jobs: ${{ steps.exclude-jobs.outputs.jobs }}
+ steps:
+ - id: exclude-jobs
+ run: |
+ if ${{ github.repository_owner == 'pawamoy-insiders' }}; then
+ echo 'jobs=[
+ {"os": "macos-latest"},
+ {"os": "windows-latest"},
+ {"python-version": "3.11"},
+ {"python-version": "3.12"},
+ {"python-version": "3.13"},
+ {"python-version": "3.14"}
+ ]' | tr -d '[:space:]' >> $GITHUB_OUTPUT
+ else
+ echo 'jobs=[
+ {"os": "macos-latest", "resolution": "lowest-direct"},
+ {"os": "windows-latest", "resolution": "lowest-direct"}
+ ]' | tr -d '[:space:]' >> $GITHUB_OUTPUT
+ fi
+
+ tests:
+
+ needs: exclude-test-jobs
+ strategy:
+ matrix:
+ os:
+ - ubuntu-latest
+ - macos-latest
+ - windows-latest
+ python-version:
+ - "3.10"
+ - "3.11"
+ - "3.12"
+ - "3.13"
+ - "3.14"
+ resolution:
+ - highest
+ - lowest-direct
+ exclude: ${{ fromJSON(needs.exclude-test-jobs.outputs.jobs) }}
+ runs-on: ${{ matrix.os }}
+ continue-on-error: ${{ matrix.python-version == '3.14' }}
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ matrix.python-version }}
+ allow-prereleases: true
+
+ - name: Setup uv
+ uses: astral-sh/setup-uv@v3
+ with:
+ enable-cache: true
+ cache-dependency-glob: pyproject.toml
+ cache-suffix: py${{ matrix.python-version }}
+
+ - name: Install dependencies
+ env:
+ UV_RESOLUTION: ${{ matrix.resolution }}
+ run: make setup
+
+ - name: Run the test suite
+ run: make test
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..fd10f63
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,43 @@
+name: release
+
+on: push
+permissions:
+ contents: write
+
+jobs:
+ release:
+ runs-on: ubuntu-latest
+ if: startsWith(github.ref, 'refs/tags/')
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ - name: Fetch all tags
+ run: git fetch --depth=1 --tags
+ - name: Setup Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.12"
+ - name: Setup uv
+ uses: astral-sh/setup-uv@v3
+ - name: Build dists
+ if: github.repository_owner == 'pawamoy-insiders'
+ run: uv tool run --from build pyproject-build
+ - name: Upload dists artifact
+ uses: actions/upload-artifact@v4
+ if: github.repository_owner == 'pawamoy-insiders'
+ with:
+ name: insiders-insiders
+ path: ./dist/*
+ - name: Prepare release notes
+ if: github.repository_owner != 'pawamoy-insiders'
+ run: uv tool run git-changelog --release-notes > release-notes.md
+ - name: Create release with assets
+ uses: softprops/action-gh-release@v2
+ if: github.repository_owner == 'pawamoy-insiders'
+ with:
+ files: ./dist/*
+ - name: Create release
+ uses: softprops/action-gh-release@v2
+ if: github.repository_owner != 'pawamoy-insiders'
+ with:
+ body_path: release-notes.md
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..9fea047
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,25 @@
+# editors
+.idea/
+.vscode/
+
+# python
+*.egg-info/
+*.py[cod]
+.venv/
+.venvs/
+/build/
+/dist/
+
+# tools
+.coverage*
+/.pdm-build/
+/htmlcov/
+/site/
+uv.lock
+
+# cache
+.cache/
+.pytest_cache/
+.mypy_cache/
+.ruff_cache/
+__pycache__/
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..a87281b
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,8 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
+and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
+
+
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..255e0ee
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,133 @@
+# 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
+dev@pawamoy.fr.
+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/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..d9c1b3c
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,150 @@
+# Contributing
+
+Contributions are welcome, and they are greatly appreciated!
+Every little bit helps, and credit will always be given.
+
+## Environment setup
+
+Nothing easier!
+
+Fork and clone the repository, then:
+
+```bash
+cd insiders
+make setup
+```
+
+> NOTE:
+> If it fails for some reason,
+> you'll need to install
+> [uv](https://github.com/astral-sh/uv)
+> manually.
+>
+> You can install it with:
+>
+> ```bash
+> curl -LsSf https://astral.sh/uv/install.sh | sh
+> ```
+>
+> Now you can try running `make setup` again,
+> or simply `uv sync`.
+
+You now have the dependencies installed.
+
+You can run the application with `make run insiders [ARGS...]`.
+
+Run `make help` to see all the available actions!
+
+## Tasks
+
+The entry-point to run commands and tasks is the `make` Python script,
+located in the `scripts` directory. Try running `make` to show the available commands and tasks.
+The *commands* do not need the Python dependencies to be installed,
+while the *tasks* do.
+The cross-platform tasks are written in Python, thanks to [duty](https://github.com/pawamoy/duty).
+
+If you work in VSCode, we provide
+[an action to configure VSCode](https://pawamoy.github.io/copier-uv/work/#vscode-setup)
+for the project.
+
+## Development
+
+As usual:
+
+1. create a new branch: `git switch -c feature-or-bugfix-name`
+1. edit the code and/or the documentation
+
+**Before committing:**
+
+1. run `make format` to auto-format the code
+1. run `make check` to check everything (fix any warning)
+1. run `make test` to run the tests (fix any issue)
+1. if you updated the documentation or the project dependencies:
+ 1. run `make docs`
+ 1. go to http://localhost:8000 and check that everything looks good
+1. follow our [commit message convention](#commit-message-convention)
+
+If you are unsure about how to fix or ignore a warning,
+just let the continuous integration fail,
+and we will help you during review.
+
+Don't bother updating the changelog, we will take care of this.
+
+## Commit message convention
+
+Commit messages must follow our convention based on the
+[Angular style](https://gist.github.com/stephenparish/9941e89d80e2bc58a153#format-of-the-commit-message)
+or the [Karma convention](https://karma-runner.github.io/4.0/dev/git-commit-msg.html):
+
+```
+[(scope)]: Subject
+
+[Body]
+```
+
+**Subject and body must be valid Markdown.**
+Subject must have proper casing (uppercase for first letter
+if it makes sense), but no dot at the end, and no punctuation
+in general.
+
+Scope and body are optional. Type can be:
+
+- `build`: About packaging, building wheels, etc.
+- `chore`: About packaging or repo/files management.
+- `ci`: About Continuous Integration.
+- `deps`: Dependencies update.
+- `docs`: About documentation.
+- `feat`: New feature.
+- `fix`: Bug fix.
+- `perf`: About performance.
+- `refactor`: Changes that are not features or bug fixes.
+- `style`: A change in code style/format.
+- `tests`: About tests.
+
+If you write a body, please add trailers at the end
+(for example issues and PR references, or co-authors),
+without relying on GitHub's flavored Markdown:
+
+```
+Body.
+
+Issue #10: https://github.com/namespace/project/issues/10
+Related to PR namespace/other-project#15: https://github.com/namespace/other-project/pull/15
+```
+
+These "trailers" must appear at the end of the body,
+without any blank lines between them. The trailer title
+can contain any character except colons `:`.
+We expect a full URI for each trailer, not just GitHub autolinks
+(for example, full GitHub URLs for commits and issues,
+not the hash or the #issue-number).
+
+We do not enforce a line length on commit messages summary and body,
+but please avoid very long summaries, and very long lines in the body,
+unless they are part of code blocks that must not be wrapped.
+
+## Pull requests guidelines
+
+Link to any related issue in the Pull Request message.
+
+During the review, we recommend using fixups:
+
+```bash
+# SHA is the SHA of the commit you want to fix
+git commit --fixup=SHA
+```
+
+Once all the changes are approved, you can squash your commits:
+
+```bash
+git rebase -i --autosquash main
+```
+
+And force-push:
+
+```bash
+git push -f
+```
+
+If this seems all too complicated, you can push or force-push each new commit,
+and we will squash them ourselves if needed, before merging.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..18d0bf3
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,15 @@
+ISC License
+
+Copyright (c) 2023, Timothée Mazzucotelli
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..5e88121
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,28 @@
+# If you have `direnv` loaded in your shell, and allow it in the repository,
+# the `make` command will point at the `scripts/make` shell script.
+# This Makefile is just here to allow auto-completion in the terminal.
+
+actions = \
+ allrun \
+ changelog \
+ check \
+ check-api \
+ check-docs \
+ check-quality \
+ check-types \
+ clean \
+ coverage \
+ docs \
+ docs-deploy \
+ format \
+ help \
+ multirun \
+ release \
+ run \
+ setup \
+ test \
+ vscode
+
+.PHONY: $(actions)
+$(actions):
+ @python scripts/make "$@"
diff --git a/README.md b/README.md
index 744f576..59e9975 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,21 @@
# insiders
+[](https://github.com/pawamoy/insiders-project/actions?query=workflow%3Aci)
+[](https://pawamoy.github.io/insiders-project/)
+[](https://pypi.org/project/insiders/)
+[](https://gitpod.io/#https://github.com/pawamoy/insiders-project)
+[](https://app.gitter.im/#/room/#insiders-project:gitter.im)
+
Manage your Insiders projects.
-This project is currently available to [sponsors](https://github.com/sponsors/pawamoy) only.
-See https://pawamoy.github.io/insiders/insiders.
+## Installation
+
+```bash
+pip install insiders
+```
+
+With [`uv`](https://docs.astral.sh/uv/):
+
+```bash
+uv tool install insiders
+```
diff --git a/config/coverage.ini b/config/coverage.ini
new file mode 100644
index 0000000..b56a286
--- /dev/null
+++ b/config/coverage.ini
@@ -0,0 +1,25 @@
+[coverage:run]
+branch = true
+parallel = true
+source =
+ src/
+ tests/
+
+[coverage:paths]
+equivalent =
+ src/
+ .venv/lib/*/site-packages/
+ .venvs/*/lib/*/site-packages/
+
+[coverage:report]
+precision = 2
+omit =
+ src/*/__init__.py
+ src/*/__main__.py
+ tests/__init__.py
+exclude_lines =
+ pragma: no cover
+ if TYPE_CHECKING
+
+[coverage:json]
+output = htmlcov/coverage.json
diff --git a/config/git-changelog.toml b/config/git-changelog.toml
new file mode 100644
index 0000000..57114e0
--- /dev/null
+++ b/config/git-changelog.toml
@@ -0,0 +1,9 @@
+bump = "auto"
+convention = "angular"
+in-place = true
+output = "CHANGELOG.md"
+parse-refs = false
+parse-trailers = true
+sections = ["build", "deps", "feat", "fix", "refactor"]
+template = "keepachangelog"
+versioning = "pep440"
diff --git a/config/mypy.ini b/config/mypy.ini
new file mode 100644
index 0000000..a93c77a
--- /dev/null
+++ b/config/mypy.ini
@@ -0,0 +1,5 @@
+[mypy]
+ignore_missing_imports = true
+exclude = tests/fixtures/|insiders.py
+warn_unused_ignores = true
+show_error_codes = true
diff --git a/config/pytest.ini b/config/pytest.ini
new file mode 100644
index 0000000..75bad9c
--- /dev/null
+++ b/config/pytest.ini
@@ -0,0 +1,21 @@
+[pytest]
+python_files =
+ test_*.py
+addopts =
+ --cov
+ --cov-config config/coverage.ini
+testpaths =
+ tests
+
+# action:message_regex:warning_class:module_regex:line
+filterwarnings =
+ error
+ # TODO: remove once pytest-xdist 4 is released
+ ignore:.*rsyncdir:DeprecationWarning:xdist
+ # TODO: remove once copier 8.3.0+ is released
+ ignore:.*FieldValidationInfo:DeprecationWarning:pydantic_core
+ ignore:.*Implicit:DeprecationWarning:twine
+ ignore:.*Popen.__del__:pytest.PytestUnraisableExceptionWarning:_pytest
+ ignore:.*io.FileIO:pytest.PytestUnraisableExceptionWarning:_pytest
+ # TODO: remove once docstring-parser 0.15+ is released
+ ignore:.*ast:DeprecationWarning:docstring_parser
diff --git a/config/ruff.toml b/config/ruff.toml
new file mode 100644
index 0000000..d525813
--- /dev/null
+++ b/config/ruff.toml
@@ -0,0 +1,86 @@
+target-version = "py39"
+line-length = 120
+
+[lint]
+exclude = [
+ "tests/fixtures/*.py",
+]
+select = [
+ "A", "ANN", "ARG",
+ "B", "BLE",
+ "C", "C4",
+ "COM",
+ "D", "DTZ",
+ "E", "ERA", "EXE",
+ "F", "FBT",
+ "G",
+ "I", "ICN", "INP", "ISC",
+ "N",
+ "PGH", "PIE", "PL", "PLC", "PLE", "PLR", "PLW", "PT", "PYI",
+ "Q",
+ "RUF", "RSE", "RET",
+ "S", "SIM", "SLF",
+ "T", "T10", "T20", "TCH", "TID", "TRY",
+ "UP",
+ "W",
+ "YTT",
+]
+ignore = [
+ "A001", # Variable is shadowing a Python builtin
+ "ANN101", # Missing type annotation for self
+ "ANN102", # Missing type annotation for cls
+ "ANN204", # Missing return type annotation for special method __str__
+ "ANN401", # Dynamically typed expressions (typing.Any) are disallowed
+ "ARG005", # Unused lambda argument
+ "C901", # Too complex
+ "D105", # Missing docstring in magic method
+ "D411", # Missing blank line before section header
+ "D412", # No blank lines allowed between a section header and its content
+ "D417", # Missing argument description in the docstring
+ "E501", # Line too long
+ "ERA001", # Commented out code
+ "G004", # Logging statement uses f-string
+ "PLR0911", # Too many return statements
+ "PLR0912", # Too many branches
+ "PLR0913", # Too many arguments to function call
+ "PLR0915", # Too many statements
+ "SLF001", # Private member accessed
+ "TRY003", # Avoid specifying long messages outside the exception class
+]
+
+[lint.per-file-ignores]
+"src/*/cli.py" = [
+ "T201", # Print statement
+]
+"src/*/debug.py" = [
+ "T201", # Print statement
+]
+"scripts/*.py" = [
+ "INP001", # File is part of an implicit namespace package
+ "T201", # Print statement
+]
+"tests/*.py" = [
+ "ARG005", # Unused lambda argument
+ "FBT001", # Boolean positional arg in function definition
+ "PLR2004", # Magic value used in comparison
+ "S101", # Use of assert detected
+]
+
+[lint.flake8-quotes]
+docstring-quotes = "double"
+
+[lint.flake8-tidy-imports]
+ban-relative-imports = "all"
+
+[lint.isort]
+known-first-party = ["insiders"]
+
+[lint.pydocstyle]
+convention = "google"
+
+[format]
+exclude = [
+ "tests/fixtures/*.py",
+]
+docstring-code-format = true
+docstring-code-line-length = 80
diff --git a/config/vscode/launch.json b/config/vscode/launch.json
new file mode 100644
index 0000000..e328838
--- /dev/null
+++ b/config/vscode/launch.json
@@ -0,0 +1,47 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "python (current file)",
+ "type": "debugpy",
+ "request": "launch",
+ "program": "${file}",
+ "console": "integratedTerminal",
+ "justMyCode": false
+ },
+ {
+ "name": "docs",
+ "type": "debugpy",
+ "request": "launch",
+ "module": "mkdocs",
+ "justMyCode": false,
+ "args": [
+ "serve",
+ "-v"
+ ]
+ },
+ {
+ "name": "test",
+ "type": "debugpy",
+ "request": "launch",
+ "module": "pytest",
+ "justMyCode": false,
+ "args": [
+ "-c=config/pytest.ini",
+ "-vvv",
+ "--no-cov",
+ "--dist=no",
+ "tests",
+ "-k=${input:tests_selection}"
+ ]
+ }
+ ],
+ "inputs": [
+ {
+ "id": "tests_selection",
+ "type": "promptString",
+ "description": "Tests selection",
+ "default": ""
+ }
+ ]
+}
\ No newline at end of file
diff --git a/config/vscode/settings.json b/config/vscode/settings.json
new file mode 100644
index 0000000..949856d
--- /dev/null
+++ b/config/vscode/settings.json
@@ -0,0 +1,33 @@
+{
+ "files.watcherExclude": {
+ "**/.venv*/**": true,
+ "**/.venvs*/**": true,
+ "**/venv*/**": true
+ },
+ "mypy-type-checker.args": [
+ "--config-file=config/mypy.ini"
+ ],
+ "python.testing.unittestEnabled": false,
+ "python.testing.pytestEnabled": true,
+ "python.testing.pytestArgs": [
+ "--config-file=config/pytest.ini"
+ ],
+ "ruff.enable": true,
+ "ruff.format.args": [
+ "--config=config/ruff.toml"
+ ],
+ "ruff.lint.args": [
+ "--config=config/ruff.toml"
+ ],
+ "yaml.schemas": {
+ "https://squidfunk.github.io/mkdocs-material/schema.json": "mkdocs.yml"
+ },
+ "yaml.customTags": [
+ "!ENV scalar",
+ "!ENV sequence",
+ "!relative scalar",
+ "tag:yaml.org,2002:python/name:materialx.emoji.to_svg",
+ "tag:yaml.org,2002:python/name:materialx.emoji.twemoji",
+ "tag:yaml.org,2002:python/name:pymdownx.superfences.fence_code_format"
+ ]
+}
\ No newline at end of file
diff --git a/config/vscode/tasks.json b/config/vscode/tasks.json
new file mode 100644
index 0000000..73145ee
--- /dev/null
+++ b/config/vscode/tasks.json
@@ -0,0 +1,97 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "changelog",
+ "type": "process",
+ "command": "scripts/make",
+ "args": ["changelog"]
+ },
+ {
+ "label": "check",
+ "type": "process",
+ "command": "scripts/make",
+ "args": ["check"]
+ },
+ {
+ "label": "check-quality",
+ "type": "process",
+ "command": "scripts/make",
+ "args": ["check-quality"]
+ },
+ {
+ "label": "check-types",
+ "type": "process",
+ "command": "scripts/make",
+ "args": ["check-types"]
+ },
+ {
+ "label": "check-docs",
+ "type": "process",
+ "command": "scripts/make",
+ "args": ["check-docs"]
+ },
+ {
+ "label": "check-api",
+ "type": "process",
+ "command": "scripts/make",
+ "args": ["check-api"]
+ },
+ {
+ "label": "clean",
+ "type": "process",
+ "command": "scripts/make",
+ "args": ["clean"]
+ },
+ {
+ "label": "docs",
+ "type": "process",
+ "command": "scripts/make",
+ "args": ["docs"]
+ },
+ {
+ "label": "docs-deploy",
+ "type": "process",
+ "command": "scripts/make",
+ "args": ["docs-deploy"]
+ },
+ {
+ "label": "format",
+ "type": "process",
+ "command": "scripts/make",
+ "args": ["format"]
+ },
+ {
+ "label": "release",
+ "type": "process",
+ "command": "scripts/make",
+ "args": ["release", "${input:version}"]
+ },
+ {
+ "label": "setup",
+ "type": "process",
+ "command": "scripts/make",
+ "args": ["setup"]
+ },
+ {
+ "label": "test",
+ "type": "process",
+ "command": "scripts/make",
+ "args": ["test", "coverage"],
+ "group": "test"
+ },
+ {
+ "label": "vscode",
+ "type": "process",
+ "command": "scripts/make",
+ "args": ["vscode"]
+ }
+ ],
+ "inputs": [
+ {
+ "id": "version",
+ "type": "promptString",
+ "description": "Version"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/docs/.overrides/main.html b/docs/.overrides/main.html
new file mode 100644
index 0000000..1e95685
--- /dev/null
+++ b/docs/.overrides/main.html
@@ -0,0 +1,20 @@
+{% extends "base.html" %}
+
+{% block announce %}
+
+ Fund this project through
+ sponsorship
+
+ {% include ".icons/octicons/heart-fill-16.svg" %}
+ —
+
+ Follow
+ @pawamoy on
+
+
+ {% include ".icons/fontawesome/brands/mastodon.svg" %}
+
+ Fosstodon
+
+ for updates
+{% endblock %}
diff --git a/docs/.overrides/partials/comments.html b/docs/.overrides/partials/comments.html
new file mode 100644
index 0000000..8b181ea
--- /dev/null
+++ b/docs/.overrides/partials/comments.html
@@ -0,0 +1,57 @@
+
+
+
+
Feedback
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/changelog.md b/docs/changelog.md
new file mode 100644
index 0000000..786b75d
--- /dev/null
+++ b/docs/changelog.md
@@ -0,0 +1 @@
+--8<-- "CHANGELOG.md"
diff --git a/docs/code_of_conduct.md b/docs/code_of_conduct.md
new file mode 100644
index 0000000..01f2ea2
--- /dev/null
+++ b/docs/code_of_conduct.md
@@ -0,0 +1 @@
+--8<-- "CODE_OF_CONDUCT.md"
diff --git a/docs/contributing.md b/docs/contributing.md
new file mode 100644
index 0000000..ea38c9b
--- /dev/null
+++ b/docs/contributing.md
@@ -0,0 +1 @@
+--8<-- "CONTRIBUTING.md"
diff --git a/docs/credits.md b/docs/credits.md
new file mode 100644
index 0000000..f758db8
--- /dev/null
+++ b/docs/credits.md
@@ -0,0 +1,10 @@
+---
+hide:
+- toc
+---
+
+
+```python exec="yes"
+--8<-- "scripts/gen_credits.py"
+```
+
diff --git a/docs/css/insiders.css b/docs/css/insiders.css
new file mode 100644
index 0000000..e7b9c74
--- /dev/null
+++ b/docs/css/insiders.css
@@ -0,0 +1,124 @@
+@keyframes heart {
+
+ 0%,
+ 40%,
+ 80%,
+ 100% {
+ transform: scale(1);
+ }
+
+ 20%,
+ 60% {
+ transform: scale(1.15);
+ }
+}
+
+@keyframes vibrate {
+ 0%, 2%, 4%, 6%, 8%, 10%, 12%, 14%, 16%, 18% {
+ -webkit-transform: translate3d(-2px, 0, 0);
+ transform: translate3d(-2px, 0, 0);
+ }
+ 1%, 3%, 5%, 7%, 9%, 11%, 13%, 15%, 17%, 19% {
+ -webkit-transform: translate3d(2px, 0, 0);
+ transform: translate3d(2px, 0, 0);
+ }
+ 20%, 100% {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+.heart {
+ color: #e91e63;
+}
+
+.pulse {
+ animation: heart 1000ms infinite;
+}
+
+.vibrate {
+ animation: vibrate 2000ms infinite;
+}
+
+.new-feature svg {
+ fill: var(--md-accent-fg-color) !important;
+}
+
+a.insiders {
+ color: #e91e63;
+}
+
+.sponsorship-list {
+ width: 100%;
+}
+
+.sponsorship-item {
+ border-radius: 100%;
+ display: inline-block;
+ height: 1.6rem;
+ margin: 0.1rem;
+ overflow: hidden;
+ width: 1.6rem;
+}
+
+.sponsorship-item:focus, .sponsorship-item:hover {
+ transform: scale(1.1);
+}
+
+.sponsorship-item img {
+ filter: grayscale(100%) opacity(75%);
+ height: auto;
+ width: 100%;
+}
+
+.sponsorship-item:focus img, .sponsorship-item:hover img {
+ filter: grayscale(0);
+}
+
+.sponsorship-item.private {
+ background: var(--md-default-fg-color--lightest);
+ color: var(--md-default-fg-color);
+ font-size: .6rem;
+ font-weight: 700;
+ line-height: 1.6rem;
+ text-align: center;
+}
+
+.mastodon {
+ color: #897ff8;
+ border-radius: 100%;
+ box-shadow: inset 0 0 0 .05rem currentcolor;
+ display: inline-block;
+ height: 1.2rem !important;
+ padding: .25rem;
+ transition: all .25s;
+ vertical-align: bottom !important;
+ width: 1.2rem;
+}
+
+.premium-sponsors {
+ text-align: center;
+}
+
+#silver-sponsors img {
+ height: 140px;
+}
+
+#bronze-sponsors img {
+ height: 140px;
+}
+
+#bronze-sponsors p {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+}
+
+#bronze-sponsors a {
+ display: block;
+ flex-shrink: 0;
+}
+
+.sponsors-total {
+ font-weight: bold;
+}
\ No newline at end of file
diff --git a/docs/css/material.css b/docs/css/material.css
new file mode 100644
index 0000000..9e8c14a
--- /dev/null
+++ b/docs/css/material.css
@@ -0,0 +1,4 @@
+/* More space at the bottom of the page. */
+.md-main__inner {
+ margin-bottom: 1.5rem;
+}
diff --git a/docs/css/mkdocstrings.css b/docs/css/mkdocstrings.css
new file mode 100644
index 0000000..88c7357
--- /dev/null
+++ b/docs/css/mkdocstrings.css
@@ -0,0 +1,27 @@
+/* Indentation. */
+div.doc-contents:not(.first) {
+ padding-left: 25px;
+ border-left: .05rem solid var(--md-typeset-table-color);
+}
+
+/* Mark external links as such. */
+a.external::after,
+a.autorefs-external::after {
+ /* https://primer.style/octicons/arrow-up-right-24 */
+ mask-image: url('data:image/svg+xml,');
+ -webkit-mask-image: url('data:image/svg+xml,');
+ content: ' ';
+
+ display: inline-block;
+ vertical-align: middle;
+ position: relative;
+
+ height: 1em;
+ width: 1em;
+ background-color: currentColor;
+}
+
+a.external:hover::after,
+a.autorefs-external:hover::after {
+ background-color: var(--md-accent-fg-color);
+}
\ No newline at end of file
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 0000000..1d24c9e
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,56 @@
+---
+hide:
+- feedback
+---
+
+--8<-- "README.md"
+
+## Usage
+
+```python exec="1" idprefix=""
+import cappa
+from cappa.base import collect
+from cappa.help import generate_arg_groups
+
+from insiders.cli import CommandMain
+
+
+def render_parser(command: cappa.Command, title: str, heading_level: int = 3) -> str:
+ """Render the parser help documents as a string."""
+
+ result = [f"{'#' * heading_level} **`{title}`**\n"]
+ if command.help:
+ result.append(f"> {command.help}\n")
+ if command.description:
+ result.append(f"{command.description}\n")
+
+ for group, args in sorted(generate_arg_groups(command)):
+ result.append(f"*{group.name.title()}*\n")
+ for arg in args:
+ if isinstance(arg, cappa.Subcommand):
+ for option in arg.options.values():
+ result.append(
+ render_parser(option, option.real_name(), heading_level + 1)
+ )
+ continue
+
+ opts = [f"`{opt}`" for opt in arg.names()]
+ if not opts:
+ line = f"- `{arg.field_name}`"
+ else:
+ line = f"- {', '.join(opts)}"
+
+ line += f" `{arg.value_name.upper()}`" if arg.num_args else ""
+ line += f": {arg.help}"
+ if arg.default is not cappa.Arg.default:
+ default = str(arg.default)
+ line += f" Default: `{default}`."
+ result.append(line)
+ result.append("")
+
+ return "\n".join(result)
+
+
+command = collect(CommandMain, help=False, completion=False)
+print(render_parser(command, "insiders"))
+```
diff --git a/docs/insiders/changelog.md b/docs/insiders/changelog.md
new file mode 100644
index 0000000..83409a4
--- /dev/null
+++ b/docs/insiders/changelog.md
@@ -0,0 +1,16 @@
+# Changelog
+
+## insiders Insiders
+
+### 1.0.2 May 25, 2024 { id="1.0.2" }
+
+- Output complete skeleton for public project, not just README and workflow
+- Use copier-uv template for my own projects
+
+### 1.0.1 November 23, 2023 { id="1.0.1" }
+
+- Fix bug in commands (pop `help` flag)
+
+### 1.0.0 October 29, 2023 { id="1.0.0" }
+
+- Release first Insiders version
diff --git a/docs/insiders/goals.yml b/docs/insiders/goals.yml
new file mode 100644
index 0000000..936b18c
--- /dev/null
+++ b/docs/insiders/goals.yml
@@ -0,0 +1,16 @@
+goals:
+ 500:
+ name: PlasmaVac User Guide
+ features: []
+ 1000:
+ name: GraviFridge Fluid Renewal
+ features:
+ - name: "[Project] Manage your Insiders projects"
+ ref: /
+ since: 2023/10/29
+ 1500:
+ name: HyperLamp Navigation Tips
+ features: []
+ 2000:
+ name: FusionDrive Ejection Configuration
+ features: []
diff --git a/docs/insiders/index.md b/docs/insiders/index.md
new file mode 100644
index 0000000..5ca88e4
--- /dev/null
+++ b/docs/insiders/index.md
@@ -0,0 +1,239 @@
+# Insiders
+
+*insiders* follows the **sponsorware** release strategy, which means
+that new features are first exclusively released to sponsors as part of
+[Insiders][insiders]. Read on to learn [what sponsorships achieve][sponsorship],
+[how to become a sponsor][sponsors] to get access to Insiders,
+and [what's in it for you][features]!
+
+## What is Insiders?
+
+*insiders Insiders* is a private fork of *insiders*, hosted as
+a private GitHub repository. Almost[^1] [all new features][features]
+are developed as part of this fork, which means that they are immediately
+available to all eligible sponsors, as they are made collaborators of this
+repository.
+
+ [^1]:
+ In general, every new feature is first exclusively released to sponsors, but
+ sometimes upstream dependencies enhance
+ existing features that must be supported by *insiders*.
+
+Every feature is tied to a [funding goal][funding] in monthly subscriptions. When a
+funding goal is hit, the features that are tied to it are merged back into
+*insiders* and released for general availability, making them available
+to all users. Bugfixes are always released in tandem.
+
+Sponsorships start as low as [**$10 a month**][sponsors].[^2]
+
+ [^2]:
+ Note that $10 a month is the minimum amount to become eligible for
+ Insiders. While GitHub Sponsors also allows to sponsor lower amounts or
+ one-time amounts, those can't be granted access to Insiders due to
+ technical reasons. Such contributions are still very much welcome as
+ they help ensuring the project's sustainability.
+
+
+## What sponsorships achieve
+
+Sponsorships make this project sustainable, as they buy the maintainers of this
+project time – a very scarce resource – which is spent on the development of new
+features, bug fixing, stability improvement, issue triage and general support.
+The biggest bottleneck in Open Source is time.[^3]
+
+ [^3]:
+ Making an Open Source project sustainable is exceptionally hard: maintainers
+ burn out, projects are abandoned. That's not great and very unpredictable.
+ The sponsorware model ensures that if you decide to use *insiders*,
+ you can be sure that bugs are fixed quickly and new features are added
+ regularly.
+
+If you're unsure if you should sponsor this project, check out the list of
+[completed funding goals][goals completed] to learn whether you're already using features that
+were developed with the help of sponsorships. You're most likely using at least
+a handful of them, [thanks to our awesome sponsors][sponsors]!
+
+## What's in it for me?
+
+```python exec="1" session="insiders"
+data_source = "docs/insiders/goals.yml"
+```
+
+
+```python exec="1" session="insiders" idprefix=""
+--8<-- "scripts/insiders.py"
+
+if unreleased_features:
+ print(
+ "The moment you [become a sponsor](#how-to-become-a-sponsor), you'll get **immediate "
+ f"access to {len(unreleased_features)} additional features** that you can start using right away, and "
+ "which are currently exclusively available to sponsors:\n"
+ )
+
+ for feature in unreleased_features:
+ feature.render(badge=True)
+
+ print(
+ "\n\nThese are just the features related to this project. "
+ "[See the complete feature list on the author's main Insiders page](https://pawamoy.github.io/insiders/#whats-in-it-for-me)."
+ )
+else:
+ print(
+ "The moment you [become a sponsor](#how-to-become-a-sponsor), you'll get immediate "
+ "access to all released features that you can start using right away, and "
+ "which are exclusively available to sponsors. At this moment, there are no "
+ "Insiders features for this project, but checkout the [next funding goals](#goals) "
+ "to see what's coming, as well as **[the feature list for all Insiders projects](https://pawamoy.github.io/insiders/#whats-in-it-for-me).**"
+ )
+```
+
+
+## How to become a sponsor
+
+Thanks for your interest in sponsoring! In order to become an eligible sponsor
+with your GitHub account, visit [pawamoy's sponsor profile][github sponsor profile],
+and complete a sponsorship of **$10 a month or more**.
+You can use your individual or organization GitHub account for sponsoring.
+
+Sponsorships lower than $10 a month are also very much appreciated, and useful.
+They won't grant you access to Insiders, but they will be counted towards reaching sponsorship goals.
+*Every* sponsorship helps us implementing new features and releasing them to the public.
+
+**Important**: If you're sponsoring **[@pawamoy][github sponsor profile]**
+through a GitHub organization, please send a short email
+to insiders@pawamoy.fr with the name of your
+organization and the GitHub account of the individual
+that should be added as a collaborator.[^4]
+
+You can cancel your sponsorship anytime.[^5]
+
+ [^4]:
+ It's currently not possible to grant access to each member of an
+ organization, as GitHub only allows for adding users. Thus, after
+ sponsoring, please send an email to insiders@pawamoy.fr, stating which
+ account should become a collaborator of the Insiders repository. We're
+ working on a solution which will make access to organizations much simpler.
+ To ensure that access is not tied to a particular individual GitHub account,
+ create a bot account (i.e. a GitHub account that is not tied to a specific
+ individual), and use this account for the sponsoring. After being added to
+ the list of collaborators, the bot account can create a private fork of the
+ private Insiders GitHub repository, and grant access to all members of the
+ organizations.
+
+ [^5]:
+ If you cancel your sponsorship, GitHub schedules a cancellation request
+ which will become effective at the end of the billing cycle. This means
+ that even though you cancel your sponsorship, you will keep your access to
+ Insiders as long as your cancellation isn't effective. All charges are
+ processed by GitHub through Stripe. As we don't receive any information
+ regarding your payment, and GitHub doesn't offer refunds, sponsorships are
+ non-refundable.
+
+
+[:octicons-heart-fill-24:{ .pulse } Join our awesome sponsors](https://github.com/sponsors/pawamoy){ .md-button .md-button--primary }
+
+
+
+
+
+
+
+
+
+
+
+
+ If you sponsor publicly, you're automatically added here with a link to
+ your profile and avatar to show your support for *insiders*.
+ Alternatively, if you wish to keep your sponsorship private, you'll be a
+ silent +1. You can select visibility during checkout and change it
+ afterwards.
+
+
+## Funding
+
+### Goals
+
+The following section lists all funding goals. Each goal contains a list of
+features prefixed with a checkmark symbol, denoting whether a feature is
+:octicons-check-circle-fill-24:{ style="color: #00e676" } already available or
+:octicons-check-circle-fill-24:{ style="color: var(--md-default-fg-color--lightest)" } planned,
+but not yet implemented. When the funding goal is hit,
+the features are released for general availability.
+
+```python exec="1" session="insiders" idprefix=""
+for goal in goals.values():
+ if not goal.complete:
+ goal.render()
+```
+
+### Goals completed
+
+This section lists all funding goals that were previously completed, which means
+that those features were part of Insiders, but are now generally available and
+can be used by all users.
+
+```python exec="1" session="insiders" idprefix=""
+for goal in goals.values():
+ if goal.complete:
+ goal.render()
+```
+
+## Frequently asked questions
+
+### Compatibility
+
+> We're building an open source project and want to allow outside collaborators
+to use *insiders* locally without having access to Insiders.
+Is this still possible?
+
+Yes. Insiders is compatible with *insiders*. Almost all new features
+and configuration options are either backward-compatible or implemented behind
+feature flags. Most Insiders features enhance the overall experience,
+though while these features add value for the users of your project, they
+shouldn't be necessary for previewing when making changes to content.
+
+### Payment
+
+> We don't want to pay for sponsorship every month. Are there any other options?
+
+Yes. You can sponsor on a yearly basis by [switching your GitHub account to a
+yearly billing cycle][billing cycle]. If for some reason you cannot do that, you
+could also create a dedicated GitHub account with a yearly billing cycle, which
+you only use for sponsoring (some sponsors already do that).
+
+If you have any problems or further questions, please reach out to insiders@pawamoy.fr.
+
+### Terms
+
+> Are we allowed to use Insiders under the same terms and conditions as
+*insiders*?
+
+Yes. Whether you're an individual or a company, you may use *insiders
+Insiders* precisely under the same terms as *insiders*, which are given
+by the [ISC License][license]. However, we kindly ask you to respect our
+**fair use policy**:
+
+- Please **don't distribute the source code** of Insiders. You may freely use
+ it for public, private or commercial projects, privately fork or mirror it,
+ but please don't make the source code public, as it would counteract the
+ sponsorware strategy.
+
+- If you cancel your subscription, you're automatically removed as a
+ collaborator and will miss out on all future updates of Insiders. However, you
+ may **use the latest version** that's available to you **as long as you like**.
+ Just remember that [GitHub deletes private forks][private forks].
+
+[insiders]: #what-is-insiders
+[sponsorship]: #what-sponsorships-achieve
+[sponsors]: #how-to-become-a-sponsor
+[features]: #whats-in-it-for-me
+[funding]: #funding
+[goals completed]: #goals-completed
+[github sponsor profile]: https://github.com/sponsors/pawamoy
+[billing cycle]: https://docs.github.com/en/github/setting-up-and-managing-billing-and-payments-on-github/changing-the-duration-of-your-billing-cycle
+[license]: ../license.md
+[private forks]: https://docs.github.com/en/github/setting-up-and-managing-your-github-user-account/removing-a-collaborator-from-a-personal-repository
+
+
+
diff --git a/docs/insiders/installation.md b/docs/insiders/installation.md
new file mode 100644
index 0000000..140b61b
--- /dev/null
+++ b/docs/insiders/installation.md
@@ -0,0 +1,88 @@
+---
+title: Getting started with Insiders
+---
+
+# Getting started with Insiders
+
+*insiders Insiders* is a compatible drop-in replacement for *insiders*,
+and can be installed similarly using `pip` or `git`.
+Note that in order to access the Insiders repository,
+you need to [become an eligible sponsor] of @pawamoy on GitHub.
+
+ [become an eligible sponsor]: index.md#how-to-become-a-sponsor
+
+## Installation
+
+### with PyPI Insiders
+
+[PyPI Insiders](https://pawamoy.github.io/pypi-insiders/)
+is a tool that helps you keep up-to-date versions
+of Insiders projects in the PyPI index of your choice
+(self-hosted, Google registry, Artifactory, etc.).
+
+See [how to install it](https://pawamoy.github.io/pypi-insiders/#installation)
+and [how to use it](https://pawamoy.github.io/pypi-insiders/#usage).
+
+**We kindly ask that you do not upload the distributions to public registries,
+as it is against our [Terms of use](index.md#terms).**
+
+### with pip (ssh/https)
+
+*insiders Insiders* can be installed with `pip` [using SSH][using ssh]:
+
+```bash
+pip install git+ssh://git@github.com/pawamoy-insiders/insiders.git
+```
+
+ [using ssh]: https://docs.github.com/en/authentication/connecting-to-github-with-ssh
+
+Or using HTTPS:
+
+```bash
+pip install git+https://${GH_TOKEN}@github.com/pawamoy-insiders/insiders.git
+```
+
+>? NOTE: **How to get a GitHub personal access token**
+> The `GH_TOKEN` environment variable is a GitHub token.
+> It can be obtained by creating a [personal access token] for
+> your GitHub account. It will give you access to the Insiders repository,
+> programmatically, from the command line or GitHub Actions workflows:
+>
+> 1. Go to https://github.com/settings/tokens
+> 2. Click on [Generate a new token]
+> 3. Enter a name and select the [`repo`][scopes] scope
+> 4. Generate the token and store it in a safe place
+>
+> [personal access token]: https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token
+> [Generate a new token]: https://github.com/settings/tokens/new
+> [scopes]: https://docs.github.com/en/developers/apps/scopes-for-oauth-apps#available-scopes
+>
+> Note that the personal access
+> token must be kept secret at all times, as it allows the owner to access your
+> private repositories.
+
+### with Git
+
+Of course, you can use *insiders Insiders* directly using Git:
+
+```
+git clone git@github.com:pawamoy-insiders/insiders
+```
+
+When cloning with Git, the package must be installed:
+
+```
+pip install -e insiders
+```
+
+## Upgrading
+
+When upgrading Insiders, you should always check the version of *insiders*
+which makes up the first part of the version qualifier. For example, a version like
+`8.x.x.4.x.x` means that Insiders `4.x.x` is currently based on `8.x.x`.
+
+If the major version increased, it's a good idea to consult the [changelog]
+and go through the steps to ensure your configuration is up to date and
+all necessary changes have been made.
+
+ [changelog]: ./changelog.md
diff --git a/docs/js/feedback.js b/docs/js/feedback.js
new file mode 100644
index 0000000..f97321a
--- /dev/null
+++ b/docs/js/feedback.js
@@ -0,0 +1,14 @@
+const feedback = document.forms.feedback;
+feedback.hidden = false;
+
+feedback.addEventListener("submit", function(ev) {
+ ev.preventDefault();
+ const commentElement = document.getElementById("feedback");
+ commentElement.style.display = "block";
+ feedback.firstElementChild.disabled = true;
+ const data = ev.submitter.getAttribute("data-md-value");
+ const note = feedback.querySelector(".md-feedback__note [data-md-value='" + data + "']");
+ if (note) {
+ note.hidden = false;
+ }
+})
diff --git a/docs/js/insiders.js b/docs/js/insiders.js
new file mode 100644
index 0000000..8bb6848
--- /dev/null
+++ b/docs/js/insiders.js
@@ -0,0 +1,74 @@
+function humanReadableAmount(amount) {
+ const strAmount = String(amount);
+ if (strAmount.length >= 4) {
+ return `${strAmount.slice(0, strAmount.length - 3)},${strAmount.slice(-3)}`;
+ }
+ return strAmount;
+}
+
+function getJSON(url, callback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open('GET', url, true);
+ xhr.responseType = 'json';
+ xhr.onload = function () {
+ var status = xhr.status;
+ if (status === 200) {
+ callback(null, xhr.response);
+ } else {
+ callback(status, xhr.response);
+ }
+ };
+ xhr.send();
+}
+
+function updatePremiumSponsors(dataURL, rank) {
+ let capRank = rank.charAt(0).toUpperCase() + rank.slice(1);
+ getJSON(dataURL + `/sponsors${capRank}.json`, function (err, sponsors) {
+ const sponsorsDiv = document.getElementById(`${rank}-sponsors`);
+ if (sponsors.length > 0) {
+ let html = '';
+ html += `${capRank} sponsors
`
+ sponsors.forEach(function (sponsor) {
+ html += `
+
+
+
+ `
+ });
+ html += '