refactor: reorganize, add package (opencode, osgrep, beads), add ai agent shell
This commit is contained in:
parent
7e0a207778
commit
a0f0f39399
58 changed files with 7867 additions and 42 deletions
212
shells/README.md
Normal file
212
shells/README.md
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
# Development Shells
|
||||
|
||||
This directory contains modular Nix devShell configurations for various programming languages and use cases.
|
||||
|
||||
## Available Shells
|
||||
|
||||
> **Note:** When in the dotfiles directory, use `.#shell-name`. From other directories, use the full path `~/Development/dotfiles#shell-name`.
|
||||
|
||||
### `default`
|
||||
Basic shell with git and nixfmt for dotfiles development.
|
||||
|
||||
```bash
|
||||
nix develop # or: nix develop .#default
|
||||
```
|
||||
|
||||
**Includes:**
|
||||
- Git
|
||||
- nixfmt-classic
|
||||
- uv (for `uvx` commands)
|
||||
|
||||
**MCP helper:**
|
||||
- `mcp-nixos` alias pins UV caches to `.cache/` so `uvx mcp-nixos` works without extra setup; useful before launching Codex.
|
||||
|
||||
### `python-uv`
|
||||
Python development with UV package manager.
|
||||
|
||||
```bash
|
||||
nix develop .#python-uv
|
||||
```
|
||||
|
||||
**Includes:**
|
||||
- Python 3.12
|
||||
- UV (fast Python package manager)
|
||||
- Ruff (linter/formatter)
|
||||
- Pyright (type checker)
|
||||
|
||||
### `ai-notebook`
|
||||
Notebook-focused ML/AI environment with HuggingFace tooling and JupyterLab.
|
||||
|
||||
```bash
|
||||
nix develop .#ai-notebook
|
||||
```
|
||||
|
||||
**Includes:**
|
||||
- Python 3.12 with JupyterLab, NumPy/Pandas/SciPy stack
|
||||
- HuggingFace `transformers`, `datasets`, `accelerate`, `tokenizers`
|
||||
- Vision tooling: OpenCV, scikit-image, Pillow, albumentations, Altair/Plotly
|
||||
- Optional CPU PyTorch (Linux hosts) plus torchvision/torchaudio
|
||||
- UV, Ruff, Pyright, Git LFS
|
||||
|
||||
**HuggingFace caches:**
|
||||
- Models: `~/.cache/huggingface/hub`
|
||||
- Transformers: `~/.cache/huggingface/transformers`
|
||||
|
||||
**Example usage:**
|
||||
```bash
|
||||
# Launch JupyterLab with AI helpers
|
||||
jupyter lab
|
||||
|
||||
# Download a model or dataset
|
||||
huggingface-cli download meta-llama/Llama-3.2-1B
|
||||
python -c "from datasets import load_dataset; load_dataset('ag_news')"
|
||||
|
||||
# Fine-tune with Accelerate
|
||||
python -m accelerate.commands.launch train.py
|
||||
```
|
||||
|
||||
### `go`
|
||||
Go development environment.
|
||||
|
||||
```bash
|
||||
nix develop .#go
|
||||
```
|
||||
|
||||
**Includes:**
|
||||
- Go compiler
|
||||
- gopls (language server)
|
||||
- golangci-lint
|
||||
- delve (debugger)
|
||||
|
||||
### `web-bun`
|
||||
Modern web development with Bun runtime.
|
||||
|
||||
```bash
|
||||
nix develop .#web-bun
|
||||
```
|
||||
|
||||
**Includes:**
|
||||
- Bun
|
||||
- Node.js 20
|
||||
- Yarn
|
||||
- esbuild
|
||||
- vscode-langservers-extracted
|
||||
|
||||
### `rust`
|
||||
Rust development environment.
|
||||
|
||||
```bash
|
||||
nix develop .#rust
|
||||
```
|
||||
|
||||
**Includes:**
|
||||
- Cargo & rustc
|
||||
- rust-analyzer
|
||||
- rustfmt
|
||||
- clippy
|
||||
|
||||
## Usage from Any Directory
|
||||
|
||||
### Option 1: Direct Path Reference
|
||||
Use the full path to your dotfiles:
|
||||
|
||||
```bash
|
||||
cd ~/my-project
|
||||
nix develop ~/Development/dotfiles#ai-notebook
|
||||
```
|
||||
|
||||
### Option 2: Flake Registry Alias (Recommended)
|
||||
|
||||
**One-time setup:**
|
||||
```bash
|
||||
nix registry add dotfiles ~/Development/dotfiles
|
||||
|
||||
# Verify it worked
|
||||
nix registry list | grep dotfiles
|
||||
```
|
||||
|
||||
**Then from anywhere:**
|
||||
```bash
|
||||
cd ~/any-project
|
||||
nix develop dotfiles#ai-notebook
|
||||
```
|
||||
|
||||
Benefits: Shorter commands, cleaner paths, easy to remember!
|
||||
|
||||
### Option 3: Auto-loading with direnv (Best for per-project)
|
||||
|
||||
**Recommended: Combine registry + direnv for cleanest setup:**
|
||||
|
||||
```bash
|
||||
# One-time: Set up registry (if not done already)
|
||||
nix registry add dotfiles ~/Development/dotfiles
|
||||
|
||||
# In each project:
|
||||
cd ~/my-ml-project
|
||||
echo "use flake dotfiles#ai-notebook" > .envrc
|
||||
direnv allow
|
||||
```
|
||||
|
||||
**Alternative: Without registry (using full path):**
|
||||
```bash
|
||||
cd ~/my-ml-project
|
||||
echo "use flake ~/Development/dotfiles#ai-notebook" > .envrc
|
||||
direnv allow
|
||||
```
|
||||
|
||||
Now the shell automatically activates when you `cd` into that directory!
|
||||
|
||||
### Option 4: Project-specific flake.nix
|
||||
|
||||
Create a `flake.nix` in your project that imports the shell:
|
||||
|
||||
```nix
|
||||
{
|
||||
inputs = {
|
||||
dotfiles.url = "path:/Users/rasyidanakbar/Development/dotfiles";
|
||||
};
|
||||
|
||||
outputs = { self, dotfiles }:
|
||||
let
|
||||
system = "aarch64-darwin";
|
||||
in {
|
||||
devShells.${system}.default = dotfiles.devShells.${system}.ai-notebook;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Then simply:
|
||||
```bash
|
||||
cd ~/my-project
|
||||
nix develop
|
||||
```
|
||||
|
||||
### Quick Reference
|
||||
|
||||
| Method | Command | Pros |
|
||||
|--------|---------|------|
|
||||
| Direct path | `nix develop ~/Development/dotfiles#ai-notebook` | Simple, no setup |
|
||||
| Registry | `nix develop dotfiles#ai-notebook` | Clean, short commands |
|
||||
| direnv + registry | `echo "use flake dotfiles#ai-notebook" > .envrc` | Auto-loads, clean |
|
||||
| Project flake | `nix develop` | Shareable, version-controlled |
|
||||
|
||||
## Adding New Shells
|
||||
|
||||
1. Create a new file in `devshells/` (e.g., `devshells/java.nix`)
|
||||
2. Follow the pattern:
|
||||
```nix
|
||||
{ pkgs, ... }:
|
||||
|
||||
pkgs.mkShell {
|
||||
name = "java";
|
||||
packages = with pkgs; [ jdk maven ];
|
||||
shellHook = ''
|
||||
echo "Java development shell ready"
|
||||
'';
|
||||
}
|
||||
```
|
||||
3. Add it to `flake.nix` imports:
|
||||
```nix
|
||||
java = importShell "java";
|
||||
```
|
||||
4. Test: `nix flake check`
|
||||
98
shells/ai-agent.nix
Normal file
98
shells/ai-agent.nix
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
{ pkgs, lib, customPkgs, ... }:
|
||||
|
||||
let
|
||||
# Get Beads package from custom packages
|
||||
beadsPkg = customPkgs.beads;
|
||||
|
||||
# Helper script to show Beads project status
|
||||
beadsStatus = pkgs.writeShellScriptBin "beads-status" ''
|
||||
if [ ! -d ".beads" ]; then
|
||||
echo "Not a Beads project (no .beads directory found)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== Beads Project Status ==="
|
||||
echo ""
|
||||
echo "Ready tasks:"
|
||||
${beadsPkg}/bin/bd ready --json | ${pkgs.jq}/bin/jq -r '.[] | " - [\(.id)] \(.title) (\(.type))"'
|
||||
echo ""
|
||||
echo "In Progress:"
|
||||
${beadsPkg}/bin/bd list --status in-progress --json | ${pkgs.jq}/bin/jq -r '.[] | " - [\(.id)] \(.title)"'
|
||||
echo ""
|
||||
echo "For full details, run: bd list --json | jq"
|
||||
'';
|
||||
|
||||
# Helper script to generate full project context for AI agents
|
||||
beadsContext = pkgs.writeShellScriptBin "beads-context" ''
|
||||
if [ ! -d ".beads" ]; then
|
||||
echo "Not a Beads project (no .beads directory found)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== Beads Project Context ==="
|
||||
echo ""
|
||||
echo "All issues:"
|
||||
${beadsPkg}/bin/bd list --json | ${pkgs.jq}/bin/jq '.'
|
||||
echo ""
|
||||
echo "Dependency tree (first ready task):"
|
||||
ready_id=$(${beadsPkg}/bin/bd ready --json | ${pkgs.jq}/bin/jq -r '.[0].id // empty')
|
||||
if [ -n "$ready_id" ]; then
|
||||
${beadsPkg}/bin/bd dep tree "$ready_id"
|
||||
else
|
||||
echo "No ready tasks"
|
||||
fi
|
||||
'';
|
||||
|
||||
in pkgs.mkShell {
|
||||
name = "ai-agent";
|
||||
|
||||
packages = with pkgs; [
|
||||
# Custom packages
|
||||
customPkgs.beads
|
||||
customPkgs.osgrep
|
||||
|
||||
# Version control
|
||||
git
|
||||
jujutsu
|
||||
|
||||
# Code search and navigation
|
||||
ripgrep
|
||||
ast-grep
|
||||
fzf
|
||||
|
||||
# JSON processing
|
||||
jq
|
||||
|
||||
# Helper scripts
|
||||
beadsStatus
|
||||
beadsContext
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
# Add home-manager binaries to PATH (claude-code, codex, etc.)
|
||||
if [ -d "$HOME/.nix-profile/bin" ]; then
|
||||
export PATH="$HOME/.nix-profile/bin:$PATH"
|
||||
fi
|
||||
|
||||
echo "AI Agent development shell loaded"
|
||||
echo ""
|
||||
echo "Available tools:"
|
||||
echo " bd - Beads CLI (issue tracker)"
|
||||
echo " beads-status - Show project status summary"
|
||||
echo " beads-context - Generate full context for AI"
|
||||
echo " osgrep - Semantic code search"
|
||||
echo " jj/git - Version control"
|
||||
echo " rg/ast-grep - Code search"
|
||||
echo ""
|
||||
echo "Quick start:"
|
||||
echo " bd init - Initialize Beads in current project"
|
||||
echo " bd create - Create new issue"
|
||||
echo " bd ready - Show ready tasks"
|
||||
echo " bd --help - Full command reference"
|
||||
echo ""
|
||||
echo "Nushell helpers (if in nushell):"
|
||||
echo " bd-init - Safe init with git checks"
|
||||
echo " bd-ready - View ready tasks (structured)"
|
||||
echo " bd-sync - Sync database with git"
|
||||
'';
|
||||
}
|
||||
54
shells/ai-notebook.nix
Normal file
54
shells/ai-notebook.nix
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
{ pkgs, lib, python ? pkgs.python312, ... }:
|
||||
|
||||
let
|
||||
pythonWithAI = python.withPackages (ps:
|
||||
let
|
||||
basePackages = with ps; [
|
||||
accelerate
|
||||
albumentations
|
||||
altair
|
||||
datasets
|
||||
evaluate
|
||||
huggingface-hub
|
||||
ipykernel
|
||||
jupyterlab
|
||||
matplotlib
|
||||
numpy
|
||||
opencv4
|
||||
pandas
|
||||
pillow
|
||||
plotly
|
||||
scipy
|
||||
scikit-image
|
||||
scikit-learn
|
||||
seaborn
|
||||
sentencepiece
|
||||
tokenizers
|
||||
transformers
|
||||
];
|
||||
linuxOnlyPackages = lib.optionals pkgs.stdenv.isLinux
|
||||
(with ps; [ torch torchaudio torchvision ]);
|
||||
in basePackages ++ linuxOnlyPackages);
|
||||
in pkgs.mkShell {
|
||||
name = "ai-notebook";
|
||||
packages = with pkgs; [ pythonWithAI uv git-lfs ruff pyright ];
|
||||
|
||||
shellHook = ''
|
||||
export UV_PYTHON="${pythonWithAI}/bin/python3"
|
||||
export UV_LINK_MODE=copy
|
||||
|
||||
export PYTHONNOUSERSITE=1
|
||||
export HF_HOME="''${XDG_CACHE_HOME:-$HOME/.cache}/huggingface"
|
||||
export HF_HUB_CACHE="$HF_HOME/hub"
|
||||
export TRANSFORMERS_CACHE="$HF_HOME/transformers"
|
||||
export JUPYTER_CONFIG_DIR="''${XDG_CONFIG_HOME:-$HOME/.config}/jupyter"
|
||||
|
||||
mkdir -p "$HF_HOME" "$JUPYTER_CONFIG_DIR"
|
||||
|
||||
echo "AI notebook shell ready"
|
||||
echo "Python: ${python.version}"
|
||||
echo "HuggingFace cache: $HF_HOME"
|
||||
echo "Launch JupyterLab: jupyter lab"
|
||||
echo "Launch classic notebook: jupyter notebook"
|
||||
'';
|
||||
}
|
||||
120
shells/default.nix
Normal file
120
shells/default.nix
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
{ pkgs, ... }:
|
||||
|
||||
let
|
||||
# MCP wrapper script that ensures Python is available for GUI apps
|
||||
mcpNixosWrapper = pkgs.writeShellScriptBin "mcp-nixos-wrapper" ''
|
||||
export PATH="${pkgs.python3}/bin:${pkgs.uv}/bin:$PATH"
|
||||
exec ${pkgs.uv}/bin/uvx mcp-nixos "$@"
|
||||
'';
|
||||
|
||||
# Generate MCP configs for GUI applications
|
||||
mcpConfigForGUI = pkgs.writeText "mcp_settings.json" (builtins.toJSON {
|
||||
mcpServers = {
|
||||
nixos = {
|
||||
type = "stdio";
|
||||
command = "${mcpNixosWrapper}/bin/mcp-nixos-wrapper";
|
||||
args = [ ];
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
# Script to install MCP configs for Claude Code and Codex
|
||||
installMcpConfigs = pkgs.writeShellScriptBin "install-mcp-configs" ''
|
||||
set -e
|
||||
echo "Installing MCP configurations..."
|
||||
|
||||
mkdir -p "$HOME/.config/claude-code"
|
||||
mkdir -p "$HOME/.config/codex"
|
||||
|
||||
cp ${mcpConfigForGUI} "$HOME/.config/claude-code/mcp_settings.json"
|
||||
cp ${mcpConfigForGUI} "$HOME/.config/codex/mcp_settings.json"
|
||||
|
||||
echo "✓ Installed MCP config to ~/.config/claude-code/mcp_settings.json"
|
||||
echo "✓ Installed MCP config to ~/.config/codex/mcp_settings.json"
|
||||
echo ""
|
||||
echo "Restart Claude Code and Codex to apply changes."
|
||||
'';
|
||||
|
||||
# Helper that builds the wrapped Neovim from the flake and runs it with repo-scoped XDG dirs
|
||||
previewNvim = pkgs.writeShellScriptBin "preview-nvim" ''
|
||||
set -euo pipefail
|
||||
|
||||
root="''${DOTFILES_ROOT:-$PWD}"
|
||||
cache_base="$root/.cache"
|
||||
nvim_cache="$cache_base/nvim-preview"
|
||||
xdg_state="$cache_base/xdg-state"
|
||||
xdg_data="$cache_base/xdg-data"
|
||||
xdg_config="$root/modules/home/rsydn/programs"
|
||||
|
||||
mkdir -p "$nvim_cache" "$xdg_state" "$xdg_data"
|
||||
|
||||
attr="''${NVIM_ATTR:-}"
|
||||
if [ "$#" -gt 0 ]; then
|
||||
attr="$1"
|
||||
shift
|
||||
fi
|
||||
|
||||
if [ -z "$attr" ]; then
|
||||
case "$(uname -s)" in
|
||||
Darwin)
|
||||
attr=".#darwinConfigurations.macbook-pro.config.home-manager.users.rasyidanakbar.programs.neovim.finalPackage"
|
||||
;;
|
||||
Linux)
|
||||
attr=".#nixosConfigurations.dev-vm.config.home-manager.users.rasyidanakbar.programs.neovim.finalPackage"
|
||||
;;
|
||||
*)
|
||||
echo "error: unsupported system; pass flake attribute as first argument or set NVIM_ATTR" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
out_link="$nvim_cache/result"
|
||||
|
||||
echo "Building Neovim package: $attr"
|
||||
XDG_CACHE_HOME="$nvim_cache" nix build "$attr" --out-link "$out_link"
|
||||
|
||||
echo "Launching Neovim..."
|
||||
XDG_CACHE_HOME="$nvim_cache" \
|
||||
XDG_STATE_HOME="$xdg_state" \
|
||||
XDG_DATA_HOME="$xdg_data" \
|
||||
XDG_CONFIG_HOME="$xdg_config" \
|
||||
NVIM_APPNAME="nvim" \
|
||||
"$out_link/bin/nvim" "$@"
|
||||
'';
|
||||
|
||||
in pkgs.mkShell {
|
||||
name = "default";
|
||||
packages = with pkgs; [
|
||||
git
|
||||
nix
|
||||
nixfmt-classic
|
||||
uv
|
||||
python3
|
||||
mcpNixosWrapper
|
||||
installMcpConfigs
|
||||
previewNvim
|
||||
];
|
||||
shellHook = ''
|
||||
export DOTFILES_ROOT="$PWD"
|
||||
export XDG_CACHE_HOME="$DOTFILES_ROOT/.cache"
|
||||
export UV_CACHE_DIR="$XDG_CACHE_HOME/uv"
|
||||
export UV_TOOL_HOME="$XDG_CACHE_HOME/uv-tools"
|
||||
export XDG_DATA_HOME="$DOTFILES_ROOT/.cache/xdg-data"
|
||||
export UV_PYTHON_DOWNLOADS=never
|
||||
export MCP_CONFIG_PATH="$DOTFILES_ROOT/.mcp.json"
|
||||
|
||||
mkdir -p "$XDG_CACHE_HOME" "$UV_CACHE_DIR" "$UV_TOOL_HOME" "$XDG_DATA_HOME"
|
||||
|
||||
alias mcp-nixos='XDG_CACHE_HOME="$XDG_CACHE_HOME" XDG_DATA_HOME="$XDG_DATA_HOME" UV_CACHE_DIR="$UV_CACHE_DIR" UV_TOOL_HOME="$UV_TOOL_HOME" UV_PYTHON_DOWNLOADS="$UV_PYTHON_DOWNLOADS" uvx mcp-nixos'
|
||||
|
||||
echo "Loaded default shell for dotfiles development"
|
||||
echo "MCP helper alias ready: mcp-nixos"
|
||||
echo ""
|
||||
echo "To install MCP configs for Claude Code and Codex, run:"
|
||||
echo " install-mcp-configs"
|
||||
echo ""
|
||||
echo "To preview the Neovim config without switching, run:"
|
||||
echo " preview-nvim [flakeAttr] [-- extra args]"
|
||||
'';
|
||||
}
|
||||
13
shells/go.nix
Normal file
13
shells/go.nix
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{ pkgs, ... }:
|
||||
|
||||
pkgs.mkShell {
|
||||
name = "go";
|
||||
packages = with pkgs; [ go gopls golangci-lint delve git ];
|
||||
shellHook = ''
|
||||
mkdir -p "$PWD/.cache/go" "$PWD/.cache/gomod"
|
||||
export GOPATH="$PWD/.cache/go"
|
||||
export GOMODCACHE="$PWD/.cache/gomod"
|
||||
echo "Go toolchain shell ready"
|
||||
echo "Go: ${pkgs.go.version}"
|
||||
'';
|
||||
}
|
||||
31
shells/jupyter-notebook.nix
Normal file
31
shells/jupyter-notebook.nix
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
{ pkgs, python ? pkgs.python312, ... }:
|
||||
|
||||
let
|
||||
# Python environment with notebook tooling and core data-science libs
|
||||
pythonWithNotebook = python.withPackages (ps:
|
||||
with ps; [
|
||||
altair
|
||||
ipykernel
|
||||
jupyterlab
|
||||
matplotlib
|
||||
numpy
|
||||
pandas
|
||||
polars
|
||||
plotly
|
||||
scipy
|
||||
scikit-learn
|
||||
seaborn
|
||||
]);
|
||||
in pkgs.mkShell {
|
||||
name = "jupyter-notebook";
|
||||
packages = [ pythonWithNotebook pkgs.ruff pkgs.pyright pkgs.git ];
|
||||
shellHook = ''
|
||||
export PYTHONNOUSERSITE=1
|
||||
export JUPYTER_CONFIG_DIR="''${XDG_CONFIG_HOME:-$HOME/.config}/jupyter"
|
||||
mkdir -p "$JUPYTER_CONFIG_DIR"
|
||||
echo "Jupyter notebook shell ready"
|
||||
echo "Python: ${python.version}"
|
||||
echo "Launch notebook: jupyter lab"
|
||||
echo "Launch classic: jupyter notebook"
|
||||
'';
|
||||
}
|
||||
12
shells/python-uv.nix
Normal file
12
shells/python-uv.nix
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{ pkgs, python ? pkgs.python312, ... }:
|
||||
|
||||
pkgs.mkShell {
|
||||
name = "python-uv";
|
||||
packages = [ python pkgs.uv pkgs.ruff pkgs.pyright ];
|
||||
shellHook = ''
|
||||
export UV_PYTHON="${python}/bin/python3"
|
||||
export UV_LINK_MODE=copy
|
||||
echo "Python (uv) shell ready"
|
||||
echo "Python: ${python.version}"
|
||||
'';
|
||||
}
|
||||
12
shells/rust.nix
Normal file
12
shells/rust.nix
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{ pkgs, ... }:
|
||||
|
||||
pkgs.mkShell {
|
||||
name = "rust";
|
||||
packages = with pkgs; [ cargo rustc rust-analyzer rustfmt clippy ];
|
||||
shellHook = ''
|
||||
mkdir -p "$PWD/.cache/cargo"
|
||||
export CARGO_HOME="$PWD/.cache/cargo"
|
||||
echo "Rust toolchain shell ready"
|
||||
echo "Rust: ${pkgs.rustc.version}"
|
||||
'';
|
||||
}
|
||||
21
shells/web-bun.nix
Normal file
21
shells/web-bun.nix
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{ pkgs, ... }:
|
||||
|
||||
pkgs.mkShell {
|
||||
name = "web-bun";
|
||||
packages = with pkgs; [
|
||||
bun
|
||||
nodejs_20
|
||||
yarn
|
||||
esbuild
|
||||
vscode-langservers-extracted
|
||||
];
|
||||
shellHook = ''
|
||||
mkdir -p "$PWD/.cache/bun"
|
||||
export BUN_INSTALL_CACHE="$PWD/.cache/bun"
|
||||
ccusage() { bunx ccusage "$@"; }
|
||||
export -f ccusage
|
||||
echo "Bun/Node web shell ready"
|
||||
echo "Bun: ${pkgs.bun.version}"
|
||||
echo "Node: ${pkgs.nodejs_20.version}"
|
||||
'';
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue