diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..47ee722 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,29 @@ +name: Documentation +on: + push: + branches: + - master + - main +permissions: + contents: read + pages: write + id-token: write +jobs: + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - uses: actions/configure-pages@v6 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + with: + python-version: 3.x + - run: pip install zensical + - run: zensical build --clean + - uses: actions/upload-pages-artifact@v5 + with: + path: site + - uses: actions/deploy-pages@v5 + id: deployment diff --git a/README.md b/README.md index bfb07d7..84e1434 100644 --- a/README.md +++ b/README.md @@ -11,14 +11,10 @@ It supports multiple deployment types: ## Installation -```bash -pip install deploy -``` - With [`uv`](https://docs.astral.sh/uv/): ```bash -uv tool install deploy +uv tool install https://github.com/trobz/deploy.py.git ``` On remote server where applications will be deployed: diff --git a/docs/commands/configure.md b/docs/commands/configure.md new file mode 100644 index 0000000..20174fa --- /dev/null +++ b/docs/commands/configure.md @@ -0,0 +1,91 @@ +# deploy configure + +Clone a repository, set up the application environment, and install a +systemd user unit on the target host. + +## Signature + +```bash +deploy [--config FILE] configure [] [] \ + [--type odoo|python|service] [-p PORT] [--force] +``` + +## Arguments + +| Argument | Required without config | Description | +|----------|------------------------|-------------| +| `instance_name` | Always | Logical name for the instance | +| `ssh_host` | If not in config | SSH target, or `localhost` / omit for local execution | +| `repo_url` | If not in config | Git repository URL | + +## Options + +| Option | Default | Description | +|--------|---------|-------------| +| `--type` | auto | Deployment type: `odoo`, `python`, or `service` | +| `-p`, `--port` | — | SSH port on the remote host | +| `--force` | `False` | Re-run steps 3–4 even if the instance directory already exists | + +## Steps + +1. **Connect** — open SSH connection if `ssh_host` is set and is not `localhost`. +2. **Clone repository** — clone `repo_url` into `~/`. + - If the directory already exists without `--force`: abort with exit 1. + - With `--force`: skip clone, proceed to step 3. +3. **Set up environment** + + === "odoo" + + ```bash + odoo-venv create --project-dir ~/ --preset project + odoo-addons-path ~/ + ``` + + === "python" + + ```bash + uv venv .venv + uv pip install -r requirements.txt # if requirements.txt exists + uv sync # if pyproject.toml exists + ``` + + === "service" + + Runs the `build` command defined in `deploy.yml`. + +4. **Install systemd unit** — render the bundled template and register it: + + ```bash + mkdir -p ~/.config/systemd/user/ + # write ~/.service + loginctl enable-linger + systemctl --user daemon-reload + systemctl --user enable --now + ``` + +## Exit codes + +| Condition | Exit code | +|-----------|-----------| +| All steps succeeded | 0 | +| SSH connection failed | 1 | +| Repository already exists (without `--force`) | 1 | +| Git clone failed | 1 | +| Virtual environment step failed | 1 | +| Template rendering / write failed | 1 | + +## Examples + +```bash +# Auto-detect type from prefix, read config from deploy.yml +deploy configure odoo-myproject-production + +# Explicit SSH host and repo +deploy configure service-api-staging deploy@host.example.com git@github.com:org/api.git + +# Custom SSH port +deploy configure odoo-myproject-staging -p 2222 + +# Re-run environment setup without re-cloning +deploy configure odoo-myproject-production --force +``` diff --git a/docs/commands/index.md b/docs/commands/index.md new file mode 100644 index 0000000..ffbf56e --- /dev/null +++ b/docs/commands/index.md @@ -0,0 +1,28 @@ +# Commands + +`deploy` provides three commands. All commands accept global options: + +| Option | Default | Description | +|--------|---------|-------------| +| `--config FILE` | `deploy.yml` | Path to the configuration file (resolved locally) | +| `--verbose` | `False` | Print each remote command and its output as it runs | + +## Overview + +| Command | Purpose | +|---------|---------| +| [`configure`](configure.md) | Clone repo, set up environment, install systemd unit | +| [`update`](update.md) | Pull latest code, sync deps, restart service | +| [`status`](status.md) | Show git info and systemd unit state | + +## Global usage + +```bash +deploy [--config FILE] [--verbose] [args...] +``` + +Example using a custom config path: + +```bash +deploy --config /etc/deploy/myserver.yml update odoo-myproject-production +``` diff --git a/docs/commands/status.md b/docs/commands/status.md new file mode 100644 index 0000000..1be51e6 --- /dev/null +++ b/docs/commands/status.md @@ -0,0 +1,61 @@ +# deploy status + +Show the current state of a deployment instance: git info and systemd unit status. + +## Signature + +```bash +deploy [--config FILE] status [] [-p PORT] +``` + +## Arguments + +| Argument | Required without config | Description | +|----------|------------------------|-------------| +| `instance_name` | Always | Name of the previously configured instance | +| `ssh_host` | If not in config | SSH target, or `localhost` / omit for local execution | + +## Options + +| Option | Default | Description | +|--------|---------|-------------| +| `-p`, `--port` | — | SSH port on the remote host | + +## Output + +``` +Instance: odoo-myproject-production +Remote: git@github.com:org/repo.git +Branch: main (abc1234) +Unit: active (running) since 2026-03-09 08:12:03 +``` + +## Steps + +1. **Connect** — open SSH connection if `ssh_host` is not `localhost`. +2. **Git info** — inside `~/`: + - `git remote get-url origin` → remote URL + - `git rev-parse --abbrev-ref HEAD` → branch + - `git rev-parse --short HEAD` → commit hash +3. **Unit status** — via `systemctl --user show`. + +## Exit codes + +| Condition | Exit code | +|-----------|-----------| +| All steps succeeded | 0 | +| SSH connection failed | 1 | +| Instance directory not found | 1 | + +## Examples + +```bash +# Read ssh_host from deploy.yml +deploy status odoo-myproject-production + +# Explicit host +deploy status odoo-myproject-production deploy@myserver.example.com + +# Custom SSH port +deploy status odoo-myproject-production -p 2222 +``` diff --git a/docs/commands/update.md b/docs/commands/update.md new file mode 100644 index 0000000..85b8532 --- /dev/null +++ b/docs/commands/update.md @@ -0,0 +1,96 @@ +# deploy update + +Pull the latest code, update dependencies, and restart the service on the +target host. + +## Signature + +```bash +deploy [--config FILE] update [] \ + [--type odoo|python|service] [-p PORT] [--db DATABASE] [--ignore-hooks] +``` + +## Arguments + +| Argument | Required without config | Description | +|----------|------------------------|-------------| +| `instance_name` | Always | Name of the previously configured instance | +| `ssh_host` | If not in config | SSH target, or `localhost` / omit for local execution | + +## Options + +| Option | Default | Description | +|--------|---------|-------------| +| `--type` | auto | Deployment type: `odoo`, `python`, or `service` | +| `-p`, `--port` | — | SSH port on the remote host | +| `--db` | `` | Override target database name (Odoo only) | +| `--ignore-hooks` | `False` | Skip all hook execution | + +## Steps + +1. **Connect** — open SSH connection if `ssh_host` is not `localhost`. +2. **`pre-update` hooks** — non-blocking. +3. **`pre-update-required` hooks** — abort with exit 1 on failure; runs `pre-update-fail` first. +4. **`pre-update-success` or `pre-update-fail`** — based on steps 2–3 outcome. +5. **Pull latest code** — `git pull` inside `~/`. +6. **Update dependencies / rebuild** + + === "odoo" + ```bash + odoo-venv update .venv --backup --yes + ``` + + === "python" + ```bash + uv pip install -r requirements.txt # if requirements.txt exists + uv sync # if pyproject.toml exists + ``` + + === "service" + Runs the `build` command from `deploy.yml`. + +7. **Apply changes** + + === "odoo" + ```bash + ~//.venv/bin/click-odoo-upgrade -d + systemctl --user restart + ``` + + === "python / service" + ```bash + systemctl --user restart + ``` + +8. **`post-update` hooks**, then `post-update-success` or `post-update-fail`. + +## Exit codes + +| Condition | Exit code | +|-----------|-----------| +| All steps succeeded | 0 | +| SSH connection failed | 1 | +| Instance directory not found | 1 | +| `pre-update-required` hook failed | 1 | +| `git pull` failed | 1 | +| Dependency update failed | 1 | +| Upgrade / restart command failed | 1 | + +## Examples + +```bash +# Standard update — reads all values from deploy.yml +deploy update odoo-myproject-production + +# Override database name +deploy update odoo-myproject-production --db myproject_alt + +# Custom SSH port +deploy update odoo-myproject-staging -p 2222 + +# Skip hooks for emergency deploys +deploy update odoo-myproject-production --ignore-hooks + +# Verbose output to see every remote command +deploy --verbose update odoo-myproject-production +``` diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..b67cbeb --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,92 @@ +# Configuration + +`deploy.yml` is a YAML file where each top-level key is an `instance_name`. +It centralises per-instance defaults so commands can be invoked with only the +instance name. + +```bash +deploy update my-project # reads all values from deploy.yml +deploy update my-project --db alt # overrides only the db +``` + +## Precedence + +Highest → lowest: + +1. **CLI argument** +2. **`deploy.yml` value** +3. **Built-in default** + +## Full schema + +```yaml +# deploy.yml + +odoo-myproject-production: + # SSH connection + ssh_host: deploy@myserver.example.com # omit or set "localhost" for local deployment + ssh_port: 22 # optional; defaults to SSH default (22) + + # Repository + repo_url: git@github.com:org/repo.git # used by `configure` + + # Deployment type + # type: odoo # auto-detected from "odoo-" prefix; can be overridden + + # Odoo only + db: myproject # defaults to instance_name if omitted + + # python / service only + exec_start: myapp.main:app # module path for python; verbatim for service + build: npm ci && npm run build # service only + + # Hooks (used by `update`) + hooks: + pre-update: + - ./scripts/check_disk_space.sh + pre-update-required: + - ./scripts/run_tests.sh + pre-update-success: + - ./scripts/notify_slack.sh "Pre-checks passed" + pre-update-fail: + - ./scripts/notify_slack.sh "Pre-checks failed" + post-update: + - ./scripts/smoke_test.sh + post-update-success: + - ./scripts/notify_slack.sh "Update succeeded" + post-update-fail: + - ./scripts/notify_slack.sh "Update failed" +``` + +## Options reference + +| Key | Type | Commands | Description | +|-----|------|----------|-------------| +| `ssh_host` | string | all | SSH target. Omit or `localhost` for local execution. | +| `ssh_port` | integer | all | SSH port on the remote host. | +| `repo_url` | string | `configure` | Git repository URL. | +| `type` | string | all | Deployment type: `odoo`, `python`, or `service`. | +| `db` | string | `update` | Target database name (Odoo only). | +| `exec_start` | string | `configure` | Entry point for python/service systemd unit. | +| `build` | string | `configure`, `update` | Build command for `service` type. | +| `hooks` | mapping | `update` | Lifecycle hooks — see [Hooks](hooks.md). | + +## Multiple instances + +A single `deploy.yml` can hold configuration for any number of instances: + +```yaml +odoo-myproject-staging: + ssh_host: deploy@staging.example.com + repo_url: git@github.com:org/repo.git + +odoo-myproject-production: + ssh_host: deploy@prod.example.com + repo_url: git@github.com:org/repo.git + db: myproject_prod +``` + +!!! tip + `deploy.yml` is resolved **locally** — on the machine running `deploy`, not on + the remote host. Keep it outside your project repository in a private + configuration directory. diff --git a/docs/demo.gif b/docs/demo.gif new file mode 100644 index 0000000..574b927 Binary files /dev/null and b/docs/demo.gif differ diff --git a/docs/demo.tape b/docs/demo.tape new file mode 100644 index 0000000..98a0f0f --- /dev/null +++ b/docs/demo.tape @@ -0,0 +1,90 @@ +# deploy terminal demo +# Showcases: installation, configure/update/status command help + +# ── Output ──────────────────────────────────────────────────────────────────── +Output ./docs/demo.gif + +# ── Dependencies ────────────────────────────────────────────────────────────── +Require deploy + +# ── Settings ────────────────────────────────────────────────────────────────── +Set Shell bash +Set TypingSpeed 25ms +Set Width 2300 +Set Height 1600 +Set FontSize 49 +Set FontFamily "JetBrains Mono, Fira Code, monospace" +Set Theme { "name": "deploy", "background": "#111111", "foreground": "#f5f5f5", "cursor": "#ea580c", "black": "#2a2a2a", "red": "#ef4444", "green": "#22c55e", "yellow": "#eab308", "blue": "#3b82f6", "magenta": "#d946ef", "cyan": "#06b6d4", "white": "#f5f5f5", "brightBlack": "#52525b", "brightRed": "#f87171", "brightGreen": "#4ade80", "brightYellow": "#facc15", "brightBlue": "#60a5fa", "brightMagenta": "#e879f9", "brightCyan": "#22d3ee", "brightWhite": "#ffffff" } +Set WindowBar Colorful +Set WindowBarSize 60 +Set BorderRadius 15 +Set Margin 30 +Set Padding 40 +Set LineHeight 1.3 + +# ── Scene 1: Installation ───────────────────────────────────────────────────── +Hide +Type "export PS1='\[\033[38;2;249;115;22m\]❯\[\033[0m\] '" +Enter +Type "clear" +Enter +Show + +Sleep 500ms +Type "uv tool install https://github.com/trobz/deploy.py.git" +Sleep 400ms +Enter + +Sleep 6s + +# ── Scene 2: Top-level help ─────────────────────────────────────────────────── +Hide +Type "clear" +Enter +Show + +Sleep 500ms +Type "deploy --help" +Sleep 400ms +Enter + +Sleep 6s + +# ── Scene 3: configure --help ───────────────────────────────────────────────── +Hide +Type "clear" +Enter +Show + +Sleep 500ms +Type "deploy configure --help" +Sleep 400ms +Enter + +Sleep 6s + +# ── Scene 4: update --help ──────────────────────────────────────────────────── +Hide +Type "clear" +Enter +Show + +Sleep 500ms +Type "deploy update --help" +Sleep 400ms +Enter + +Sleep 6s + +# ── Scene 5: status --help ──────────────────────────────────────────────────── +Hide +Type "clear" +Enter +Show + +Sleep 500ms +Type "deploy status --help" +Sleep 400ms +Enter + +Sleep 6s diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..6aff26d --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,73 @@ +# Getting Started + +## Installation + +Install `deploy` with [`uv`](https://docs.astral.sh/uv/) (recommended): + +```bash +uv tool install https://github.com/trobz/deploy.py.git +``` + +Or with pip: + +```bash +pip install https://github.com/trobz/deploy.py.git +``` + +## Remote host requirements + +The following tools must be pre-installed on the **target host** before using `deploy`: + +| Tool | Required for | +|------|-------------| +| `git` | All deployment types | +| `odoo-venv` | `odoo` deployments | +| `odoo-addons-path` | `odoo` deployments | +| `git-aggregator` | `odoo` deployments | +| `uv` | `python` deployments | +| `click-odoo-upgrade` | `odoo` upgrades | +| `systemd` (user) | All — service management | + +Install Trobz tools on the remote host: + +```bash +uv tool install odoo-venv +uv tool install odoo-addons-path +uv tool install git-aggregator +``` + +## Quickstart + +**1. Create a `deploy.yml`** alongside your deployment scripts: + +```yaml +odoo-myproject-production: + ssh_host: deploy@myserver.example.com + ssh_port: 22 + repo_url: git@github.com:myorg/myproject.git +``` + +**2. Configure the instance** (clone repo + set up venv + install systemd unit): + +```bash +deploy configure odoo-myproject-production +``` + +**3. Update the instance** (pull + sync venv + upgrade + restart): + +```bash +deploy update odoo-myproject-production +``` + +**4. Check status**: + +```bash +deploy status odoo-myproject-production +``` + +## Next steps + +- [Instance Names](instance-names.md) — understand the naming convention +- [Configuration](configuration.md) — full `deploy.yml` reference +- [Commands](commands/index.md) — detailed command documentation +- [Hooks](hooks.md) — automate pre/post update tasks diff --git a/docs/hooks.md b/docs/hooks.md new file mode 100644 index 0000000..fb9fd38 --- /dev/null +++ b/docs/hooks.md @@ -0,0 +1,65 @@ +# Hooks + +Hooks are shell commands defined in `deploy.yml` under a `hooks` key for a given +instance. They are executed remotely on `ssh_host` from the `~/` +working directory during the `update` command. + +## Hook types + +| Hook | When it runs | Blocks update on failure? | +|------|-------------|--------------------------| +| `pre-update` | Before any update step | No | +| `pre-update-required` | After `pre-update`; failure aborts the update | **Yes** | +| `pre-update-success` | After pre-update phase, only if all hooks succeeded | No | +| `pre-update-fail` | After pre-update phase, only if any hook failed | No | +| `post-update` | After update completes (success or failure) | No | +| `post-update-success` | After update, only if it succeeded | No | +| `post-update-fail` | After update, only if it failed | No | + +## Configuration + +```yaml +# deploy.yml +odoo-myproject-production: + ssh_host: deploy@myserver.example.com + hooks: + pre-update: + - ./scripts/check_disk_space.sh + - ./scripts/notify_slack.sh "Update starting" + pre-update-required: + - ./scripts/run_tests.sh # update is aborted if this fails + pre-update-success: + - ./scripts/notify_slack.sh "Pre-checks passed" + pre-update-fail: + - ./scripts/notify_slack.sh "Pre-checks failed" + post-update: + - ./scripts/smoke_test.sh + post-update-success: + - ./scripts/notify_slack.sh "Update succeeded" + post-update-fail: + - ./scripts/notify_slack.sh "Update failed" +``` + +## Execution order + +```mermaid +graph TD + A[Start update] --> B[pre-update] + B --> C[pre-update-required] + C -->|success| D[pre-update-success] + C -->|failure| E[pre-update-fail] + E --> Z[Abort with exit 1] + D --> F[git pull + deps + restart] + F -->|success| G[post-update] + F -->|failure| G + G -->|update succeeded| H[post-update-success] + G -->|update failed| I[post-update-fail] +``` + +## Skipping hooks + +Pass `--ignore-hooks` to skip all hook execution: + +```bash +deploy update odoo-myproject-production --ignore-hooks +``` diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..d39c509 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,5 @@ +--- +template: landing.html +--- + +# Home diff --git a/docs/instance-names.md b/docs/instance-names.md new file mode 100644 index 0000000..e11301a --- /dev/null +++ b/docs/instance-names.md @@ -0,0 +1,57 @@ +# Instance Names + +Instance names follow a structured convention that encodes the deployment type, +project, environment, and an optional qualifier. + +## Format + +``` +--[-] +``` + +| Segment | Values / Notes | +|---------|----------------| +| `type_prefix` | Always first. `odoo`, `openerp` → Odoo · `service` → Python | +| `project_slug` | Everything between prefix and environment. May contain hyphens. | +| `environment` | `integration`, `staging`, `production`, `hotfix`, `debug`, `demo` | +| `suffix` | Optional. Short qualifier after the environment: `-02`, `-eu`, `-vn` | + +## Examples + +``` +odoo-myproject-production +odoo-myproject-staging-02 +odoo-my-cool-project-production +openerp-legacy-integration +service-myapi-production-eu +service-worker-staging +``` + +## Parsing algorithm + +Because the slug may contain hyphens, the name is parsed from both ends: + +1. **Prefix** — everything before the first `-`. +2. **Suffix** — if the last segment does not match a known environment, treat it as a suffix and strip it. +3. **Environment** — the last remaining segment, which must be a known value. +4. **Slug** — all segments between prefix and environment, joined with `-`. + +## Type auto-detection + +When `--type` is not provided and no `type` key exists in `deploy.yml`, +the prefix determines the deployment type automatically: + +| Prefix | Detected type | +|--------|---------------| +| `odoo-`, `openerp-` | `odoo` | +| `service-` | `python` | +| *(anything else)* | error — `--type` is required | + +!!! note + The `service` deployment type (non-Python) is never auto-detected. + It must be set explicitly via `--type service` or `type: service` in `deploy.yml`. + +## Database name (Odoo) + +For `odoo` deployments the database name defaults to the full `instance_name`. +Override it with `--db` or the `db` key in `deploy.yml`. diff --git a/docs/javascripts/hero-scene.js b/docs/javascripts/hero-scene.js new file mode 100644 index 0000000..5ebc263 --- /dev/null +++ b/docs/javascripts/hero-scene.js @@ -0,0 +1,163 @@ +/** + * Flowing Wave Field — abstract background animation using Three.js. + * + * A grid of particles undulates using layered sine waves, creating an + * organic, ocean-like surface. Particles are orange (#F97316), connections + * are white with low opacity. Mouse movement warps the wave locally. + */ + +;(function () { + let animationId = null; + let renderer = null; + let mouseMoveHandler = null; + let resizeHandler = null; + + function teardown() { + if (animationId) { cancelAnimationFrame(animationId); animationId = null; } + if (renderer) { renderer.dispose(); renderer = null; } + if (mouseMoveHandler) { document.removeEventListener('mousemove', mouseMoveHandler); mouseMoveHandler = null; } + if (resizeHandler) { window.removeEventListener('resize', resizeHandler); resizeHandler = null; } + } + + function init() { + teardown(); + const canvas = document.getElementById('hero-canvas'); + if (!canvas) return; + + const scene = new THREE.Scene(); + renderer = new THREE.WebGLRenderer({ canvas, alpha: true, antialias: true }); + renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); + + const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.set(0, 120, 200); + camera.lookAt(0, 0, 0); + + const COLS = 50, ROWS = 50, SPACING = 8; + const PARTICLE_COUNT = COLS * ROWS; + const offsetX = ((COLS - 1) * SPACING) / 2; + const offsetZ = ((ROWS - 1) * SPACING) / 2; + + const geometry = new THREE.BufferGeometry(); + const positions = new Float32Array(PARTICLE_COUNT * 3); + for (let row = 0; row < ROWS; row++) { + for (let col = 0; col < COLS; col++) { + const i = (row * COLS + col) * 3; + positions[i] = col * SPACING - offsetX; + positions[i + 1] = 0; + positions[i + 2] = row * SPACING - offsetZ; + } + } + geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); + + function createCircleTexture() { + const c = document.createElement('canvas'); + c.width = 32; c.height = 32; + const ctx = c.getContext('2d'); + const grad = ctx.createRadialGradient(16, 16, 0, 16, 16, 16); + grad.addColorStop(0, 'rgba(255,255,255,1)'); + grad.addColorStop(0.4, 'rgba(255,255,255,0.8)'); + grad.addColorStop(1, 'rgba(255,255,255,0)'); + ctx.fillStyle = grad; + ctx.fillRect(0, 0, 32, 32); + const tex = new THREE.Texture(c); + tex.needsUpdate = true; + return tex; + } + + const points = new THREE.Points(geometry, new THREE.PointsMaterial({ + color: 0xF97316, size: 2.5, + map: createCircleTexture(), + transparent: true, alphaTest: 0.01, opacity: 0.9, + depthWrite: false, blending: THREE.AdditiveBlending + })); + scene.add(points); + + const maxLines = (COLS - 1) * ROWS + COLS * (ROWS - 1); + const linePositions = new Float32Array(maxLines * 6); + const lineGeometry = new THREE.BufferGeometry(); + lineGeometry.setAttribute('position', + new THREE.BufferAttribute(linePositions, 3).setUsage(THREE.DynamicDrawUsage)); + const lines = new THREE.LineSegments(lineGeometry, + new THREE.LineBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.08, depthWrite: false })); + scene.add(lines); + + const mouse = { x: 9999, y: 9999 }; + mouseMoveHandler = (e) => { + const rect = canvas.parentNode.getBoundingClientRect(); + mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1; + mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1; + }; + document.addEventListener('mousemove', mouseMoveHandler); + + resizeHandler = () => { + const container = canvas.parentNode; + if (!container) return; + renderer.setSize(container.clientWidth, container.clientHeight); + camera.aspect = container.clientWidth / container.clientHeight; + camera.updateProjectionMatrix(); + }; + window.addEventListener('resize', resizeHandler); + resizeHandler(); + + const clock = new THREE.Clock(); + function animate() { + animationId = requestAnimationFrame(animate); + const t = clock.getElapsedTime(); + const pos = points.geometry.attributes.position.array; + + const mouseVec = new THREE.Vector3(mouse.x, mouse.y, 0.5); + mouseVec.unproject(camera); + const dir = mouseVec.sub(camera.position).normalize(); + const dist = -camera.position.y / dir.y; + const mouseWorld = camera.position.clone().add(dir.multiplyScalar(dist)); + + for (let row = 0; row < ROWS; row++) { + for (let col = 0; col < COLS; col++) { + const i = (row * COLS + col) * 3; + const x = pos[i], z = pos[i + 2]; + let y = Math.sin(x * 0.04 + t * 0.8) * 12 + + Math.sin(z * 0.06 + t * 1.2) * 8 + + Math.sin((x + z) * 0.05 + t * 0.6) * 5; + const dx = x - mouseWorld.x, dz = z - mouseWorld.z; + const mouseDist = Math.sqrt(dx * dx + dz * dz); + if (mouseDist < 60) y += (1 - mouseDist / 60) * 20; + pos[i + 1] = y; + } + } + points.geometry.attributes.position.needsUpdate = true; + + const lnPos = lines.geometry.attributes.position.array; + let li = 0; + for (let row = 0; row < ROWS; row++) { + for (let col = 0; col < COLS; col++) { + const i = (row * COLS + col) * 3; + if (col < COLS - 1) { + const j = (row * COLS + col + 1) * 3; + lnPos[li++]=pos[i]; lnPos[li++]=pos[i+1]; lnPos[li++]=pos[i+2]; + lnPos[li++]=pos[j]; lnPos[li++]=pos[j+1]; lnPos[li++]=pos[j+2]; + } + if (row < ROWS - 1) { + const j = ((row + 1) * COLS + col) * 3; + lnPos[li++]=pos[i]; lnPos[li++]=pos[i+1]; lnPos[li++]=pos[i+2]; + lnPos[li++]=pos[j]; lnPos[li++]=pos[j+1]; lnPos[li++]=pos[j+2]; + } + } + } + lines.geometry.attributes.position.needsUpdate = true; + lines.geometry.setDrawRange(0, li / 3); + + scene.rotation.y += 0.0008; + renderer.render(scene, camera); + } + animate(); + } + + function safeInit() { + if (typeof THREE === 'undefined') return; + init(); + } + + if (typeof document$ !== 'undefined') { document$.subscribe(safeInit); } + if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', safeInit); } + else { safeInit(); } +})(); diff --git a/docs/markdown.md b/docs/markdown.md new file mode 100644 index 0000000..a4ad61f --- /dev/null +++ b/docs/markdown.md @@ -0,0 +1,96 @@ +--- +icon: simple/markdown +--- + +# Markdown in 5min + +## Headers +``` +# H1 Header +## H2 Header +### H3 Header +#### H4 Header +##### H5 Header +###### H6 Header +``` + +## Text formatting +``` +**bold text** +*italic text* +***bold and italic*** +~~strikethrough~~ +`inline code` +``` + +## Links and images +``` +[Link text](https://example.com) +[Link with title](https://example.com "Hover title") +![Alt text](image.jpg) +![Image with title](image.jpg "Image title") +``` + +## Lists +``` +Unordered: +- Item 1 +- Item 2 + - Nested item + +Ordered: +1. First item +2. Second item +3. Third item +``` + +## Blockquotes +``` +> This is a blockquote +> Multiple lines +>> Nested quote +``` + +## Code blocks +```` +```javascript +function hello() { + console.log("Hello, world!"); +} +``` +```` + +## Tables +``` +| Header 1 | Header 2 | Header 3 | +|----------|----------|----------| +| Row 1 | Data | Data | +| Row 2 | Data | Data | +``` + +## Horizontal rule +``` +--- +or +*** +or +___ +``` + +## Task lists +``` +- [x] Completed task +- [ ] Incomplete task +- [ ] Another task +``` + +## Escaping characters +``` +Use backslash to escape: \* \_ \# \` +``` + +## Line breaks +``` + +Use a blank line for a new paragraph. +``` diff --git a/docs/stylesheets/hero.css b/docs/stylesheets/hero.css new file mode 100644 index 0000000..4b4db9e --- /dev/null +++ b/docs/stylesheets/hero.css @@ -0,0 +1,158 @@ +@import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap"); + +:root { + --ov-bg: #111111; + --ov-text: #f5f5f5; + --ov-text-muted: #a3a3a3; + --ov-accent: #f97316; + --ov-accent-glow: rgba(249, 115, 22, 0.4); + --ov-primary: #ea580c; + --ov-surface: rgba(255, 255, 255, 0.06); + --ov-border: rgba(255, 255, 255, 0.12); + --ov-font-mono: "JetBrains Mono", "Fira Code", monospace; + --ov-font-sans: "IBM Plex Sans", system-ui, sans-serif; + --ov-container-width: 1440px; + --ov-split-ratio: minmax(0, 1fr) minmax(0, 2fr); +} + +body:has(.ov-landing), +body:has(.ov-landing) .md-main__inner, +body:has(.ov-landing) .md-main, +body:has(.ov-landing) .md-content, +body:has(.ov-landing) .md-content__inner { + margin: 0 !important; padding: 0 !important; + max-width: none !important; + background: var(--ov-bg) !important; + height: 100vh; overflow: hidden; +} + +.ov-landing { + position: relative; display: flex; + align-items: center; justify-content: center; + width: 100%; height: 100vh; + background: var(--ov-bg); color: var(--ov-text); overflow: hidden; +} + +.ov-landing__canvas { + position: absolute; inset: 0; + width: 100%; height: 100%; + z-index: 0; opacity: 0.6; pointer-events: none; +} + +.ov-split { + position: relative; z-index: 1; + display: grid; grid-template-columns: var(--ov-split-ratio); + align-items: center; gap: 4rem; + width: 100%; max-width: var(--ov-container-width); padding: 0 4rem; +} + +.ov-split__copy { + display: flex; flex-direction: column; + align-items: flex-start; gap: 1.5rem; +} + +.ov-split__demo { + display: flex; align-items: center; justify-content: center; +} + +.ov-hero__badge { + display: inline-flex; align-items: center; gap: 0.5rem; + padding: 0.5rem 1.25rem; + background: var(--ov-surface); border: 1px solid var(--ov-border); + border-radius: 999px; font-family: var(--ov-font-mono); + font-size: 0.8rem; color: var(--ov-text-muted); + backdrop-filter: blur(8px); + animation: ov-fadeDown 0.6s ease-out both; +} +.ov-hero__prompt { color: var(--ov-accent); font-weight: 600; } +.ov-hero__cursor { + display: inline-block; width: 2px; height: 1em; + background: var(--ov-accent); animation: ov-blink 1s step-end infinite; +} + +.ov-hero__title { margin: 0 !important; animation: ov-fadeUp 0.7s ease-out 0.2s both; } +.ov-hero__line { + display: block; font-family: var(--ov-font-mono); + font-weight: 700; letter-spacing: -0.03em; line-height: 1.1 !important; +} +.ov-hero__line--1 { + font-size: clamp(1.8rem, 3vw, 3rem) !important; + background: linear-gradient(135deg, #fff 0%, #f97316 50%, #ea580c 100%); + -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; +} + +.ov-hero__subtitle { + font-family: var(--ov-font-sans); font-size: clamp(0.95rem, 2vw, 1.15rem); + font-weight: 300; line-height: 1.6; color: var(--ov-text-muted); margin: 0; + animation: ov-fadeUp 0.7s ease-out 0.4s both; +} +.ov-hero__accent { color: var(--ov-accent); font-weight: 500; } + +.ov-hero__actions { + display: flex; gap: 1rem; flex-wrap: wrap; + animation: ov-fadeUp 0.7s ease-out 0.6s both; +} + +.ov-btn { + display: inline-flex; align-items: center; gap: 0.5rem; + padding: 0.75rem 1.75rem; font-family: var(--ov-font-mono); + font-weight: 500; font-size: 0.9rem; + text-decoration: none; border-radius: 8px; cursor: pointer; + transition: all 0.2s ease-out; +} +.ov-btn--primary { + background: var(--ov-primary); color: #fff !important; + border: 1px solid transparent; box-shadow: 0 0 20px rgba(249, 115, 22, 0.3); +} +.ov-btn--primary:hover { background: #c2410c; box-shadow: 0 0 30px rgba(249, 115, 22, 0.5); transform: translateY(-2px); } +.ov-btn--ghost { background: transparent; color: var(--ov-text) !important; border: 1px solid var(--ov-border); } +.ov-btn--ghost:hover { background: var(--ov-surface); border-color: var(--ov-text-muted); transform: translateY(-2px); } + +.ov-terminal-frame { + width: 100%; border-radius: 12px; + background: #1a1a1a; border: 1px solid var(--ov-border); + box-shadow: 0 32px 64px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.04), 0 0 60px var(--ov-accent-glow); + overflow: hidden; transition: box-shadow 0.3s ease; +} +.ov-terminal-frame:hover { + box-shadow: 0 40px 80px rgba(0,0,0,0.7), 0 0 0 1px rgba(255,255,255,0.06), 0 0 90px rgba(249,115,22,0.55); +} +.ov-terminal-frame__body { display: block; line-height: 0; background: #0d0d0d; } +.ov-terminal-frame__gif { display: block; width: 100%; height: auto; } + +.ov-hero__features { + display: flex; gap: 2rem; flex-wrap: wrap; margin-top: 0.5rem; + animation: ov-fadeUp 0.7s ease-out 0.8s both; +} +.ov-feature { display: flex; align-items: center; gap: 0.5rem; font-family: var(--ov-font-sans); font-size: 0.875rem; color: var(--ov-text-muted); } +.ov-feature__icon { color: var(--ov-accent); flex-shrink: 0; } + +@keyframes ov-fadeDown { from { opacity: 0; transform: translateY(-16px); } to { opacity: 1; transform: translateY(0); } } +@keyframes ov-fadeUp { from { opacity: 0; transform: translateY( 16px); } to { opacity: 1; transform: translateY(0); } } +@keyframes ov-blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } } + +@media (prefers-reduced-motion: reduce) { + .ov-hero__badge, .ov-hero__title, .ov-hero__subtitle, + .ov-hero__actions, .ov-hero__features, .ov-split__demo { animation: none !important; } + .ov-hero__cursor { animation: none !important; opacity: 1; } + .ov-btn:hover { transform: none; } +} + +@media screen and (max-width: 60rem) { + .ov-split { grid-template-columns: 1fr; gap: 2.5rem; padding: 2rem; justify-items: center; overflow-y: auto; max-height: 100vh; } + .ov-split__copy { align-items: center; text-align: center; } + .ov-hero__actions { justify-content: center; } + .ov-hero__line--1 { font-size: clamp(2.5rem, 10vw, 4rem) !important; } + .ov-terminal-frame { max-width: 600px; } + body:has(.ov-landing), body:has(.ov-landing) .md-main__inner, + body:has(.ov-landing) .md-main, body:has(.ov-landing) .md-content, + body:has(.ov-landing) .md-content__inner { overflow: auto; height: auto; } +} + +@media screen and (max-width: 30rem) { + .ov-split { padding: 1.5rem 1.25rem; gap: 2rem; } + .ov-hero__badge { font-size: 0.75rem; padding: 0.4rem 1rem; } + .ov-hero__actions { flex-direction: column; width: 100%; } + .ov-btn { justify-content: center; width: 100%; } + .ov-terminal-frame { border-radius: 8px; } +} diff --git a/docs/stylesheets/site.css b/docs/stylesheets/site.css new file mode 100644 index 0000000..3b82ab2 --- /dev/null +++ b/docs/stylesheets/site.css @@ -0,0 +1,4 @@ +.md-header__button.md-logo, +.md-header__topic { + display: none; +} diff --git a/overrides/landing.html b/overrides/landing.html new file mode 100644 index 0000000..c5a08b0 --- /dev/null +++ b/overrides/landing.html @@ -0,0 +1,93 @@ +{% extends "base.html" %} + +{# Hide default MkDocs chrome — we own the full viewport #} +{% block header %}{% endblock %} +{% block footer %}{% endblock %} +{% block tabs %}{% endblock %} +{% block site_nav %}{% endblock %} + +{% block content %} +
+ + {# ── Three.js particle canvas (background layer) ── #} + + + {# ── Split layout: left = copy, right = demo/screenshot ── #} +
+ + {# ── LEFT — hero copy ── #} +
+ +
+ $ + uv tool install https://github.com/trobz/deploy.py.git + +
+ +

+ deploy +

+ +

+ Deploy and manage applications on remote servers over SSH.
+ Odoo, Python, and service deployments +

+ +
+ + + + + SSH & local execution + + + + + + systemd user units + + + + + + Lifecycle hooks + +
+ + + +
+ + {# ── RIGHT — demo / screenshot ── #} +
+ +
+ +
+
+{% endblock %} diff --git a/overrides/main.html b/overrides/main.html new file mode 100644 index 0000000..34b75d7 --- /dev/null +++ b/overrides/main.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} + +{% block analytics %} + + + +{% endblock %} diff --git a/pyproject.toml b/pyproject.toml index ac1a08a..1cb456e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,7 @@ dev = [ "ruff>=0.11.5", "pytest>=7.2.0", "python-semantic-release>=10.5.3", + "zensical>=0.0.33", ] [tool.pytest.ini_options] diff --git a/uv.lock b/uv.lock index 9301165..9e476c2 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.10" [[package]] @@ -151,6 +151,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "deepmerge" +version = "2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/3a/b0ba594708f1ad0bc735884b3ad854d3ca3bdc1d741e56e40bbda6263499/deepmerge-2.0.tar.gz", hash = "sha256:5c3d86081fbebd04dd5de03626a0607b809a98fb6ccba5770b62466fe940ff20", size = 19890, upload-time = "2024-08-30T05:31:50.308Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/82/e5d2c1c67d19841e9edc74954c827444ae826978499bde3dfc1d007c8c11/deepmerge-2.0-py3-none-any.whl", hash = "sha256:6de9ce507115cff0bed95ff0ce9ecc31088ef50cbdf09bc90a09349a318b3d00", size = 13475, upload-time = "2024-08-30T05:31:48.659Z" }, +] + [[package]] name = "deploy" version = "0.5.0" @@ -173,6 +182,7 @@ dev = [ { name = "python-semantic-release" }, { name = "ruff" }, { name = "ty" }, + { name = "zensical" }, ] [package.metadata] @@ -191,6 +201,7 @@ dev = [ { name = "python-semantic-release", specifier = ">=10.5.3" }, { name = "ruff", specifier = ">=0.11.5" }, { name = "ty", specifier = ">=0.0.1a16" }, + { name = "zensical", specifier = ">=0.0.33" }, ] [[package]] @@ -316,6 +327,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] +[[package]] +name = "markdown" +version = "3.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, +] + [[package]] name = "markdown-it-py" version = "4.0.0" @@ -609,11 +629,24 @@ wheels = [ [[package]] name = "pygments" -version = "2.19.2" +version = "2.20.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pymdown-extensions" +version = "10.21.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/08/f1c908c581fd11913da4711ea7ba32c0eee40b0190000996bb863b0c9349/pymdown_extensions-10.21.2.tar.gz", hash = "sha256:c3f55a5b8a1d0edf6699e35dcbea71d978d34ff3fa79f3d807b8a5b3fa90fbdc", size = 853922, upload-time = "2026-03-29T15:01:55.233Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl", hash = "sha256:5c0fd2a2bea14eb39af8ff284f1066d898ab2187d81b889b75d46d4348c01638", size = 268901, upload-time = "2026-03-29T15:01:53.244Z" }, ] [[package]] @@ -1074,3 +1107,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5c/99/79f17046cf67e4a95b9987ea129632ba8bcec0bc81f3fb3d19bdb0bd60cd/wrapt-2.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:72aaa9d0d8e4ed0e2e98019cea47a21f823c9dd4b43c7b77bba6679ffcca6a00", size = 60554, upload-time = "2026-03-06T02:53:14.132Z" }, { url = "https://files.pythonhosted.org/packages/1a/c7/8528ac2dfa2c1e6708f647df7ae144ead13f0a31146f43c7264b4942bf12/wrapt-2.1.2-py3-none-any.whl", hash = "sha256:b8fd6fa2b2c4e7621808f8c62e8317f4aae56e59721ad933bac5239d913cf0e8", size = 43993, upload-time = "2026-03-06T02:53:12.905Z" }, ] + +[[package]] +name = "zensical" +version = "0.0.33" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "deepmerge" }, + { name = "markdown" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "pyyaml" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/c2/dea4b86dc1ca2a7b55414017f12cfb12b5cfdf3a1ed7c77a04c271eb523b/zensical-0.0.33.tar.gz", hash = "sha256:05209cb4f80185c533e0d37c25d084ddc2050e3d5a4dd1b1812961c2ee0c3380", size = 3892278, upload-time = "2026-04-14T11:08:19.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/5f/45d5200405420a9d8ac91cf9e7826622ea12f3198e8e6ac4ffb481eb53bf/zensical-0.0.33-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f658e3c241cfbb560bd8811116a9486cff7e04d7d5aed73569dd533c74187450", size = 12416748, upload-time = "2026-04-14T11:07:43.246Z" }, + { url = "https://files.pythonhosted.org/packages/33/1e/aadaf31d6e4d20419ecedaf0b1c804e359ec23dcdb44c8d2bf6d8407080c/zensical-0.0.33-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:f9813ac3256c28e2e2f1ba5c9fab1b4bca62bbe0e0f8e85ac22d33b068b1b08a", size = 12293372, upload-time = "2026-04-14T11:07:46.569Z" }, + { url = "https://files.pythonhosted.org/packages/db/e5/838be8451ea8b2aecec39fbec3971060fc705e17f5741249740d9b6a6824/zensical-0.0.33-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3bad7ac71028769c5d1f3f84f448dbb7352db28d77095d1b40a8d1b0aa34ec30", size = 12659832, upload-time = "2026-04-14T11:07:50.754Z" }, + { url = "https://files.pythonhosted.org/packages/1e/5c/dd957d7c83efc13a70a6058d4190a3afcf29942aefb391120bca5466347d/zensical-0.0.33-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:06bb039daf044547c9400a52f9493b3cd486ba9baef3324fdcffd2e26e61105f", size = 12603847, upload-time = "2026-04-14T11:07:53.698Z" }, + { url = "https://files.pythonhosted.org/packages/b7/99/dd6ccc392ece1f34fb20ea339a01717badbbeb2fba1d4f3019a5028d0bcc/zensical-0.0.33-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:260238062b3139ece0edab93f4dbe7a12923453091f5aa580dfd73e799388076", size = 12956236, upload-time = "2026-04-14T11:07:56.728Z" }, + { url = "https://files.pythonhosted.org/packages/f4/76/e0a1b884eadf6afa7e2d56c90c268eec36836ac27e96ef250c0129e55417/zensical-0.0.33-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7dff0f4afda7b8586bc4ab2a5684bce5b282232dd4e0cad3be4c73fedd264425", size = 12701944, upload-time = "2026-04-14T11:07:59.928Z" }, + { url = "https://files.pythonhosted.org/packages/38/38/e1ff13461e406864fa2b23fc828822659a7dbac5c79398f724d17f088540/zensical-0.0.33-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:207b4d81b208d75b97dc7bd318804550b886a3e852ef67429ef0e6b9442839d1", size = 12835444, upload-time = "2026-04-14T11:08:02.998Z" }, + { url = "https://files.pythonhosted.org/packages/41/04/7d24d52d6903fc5c511633afe8b5716fef19da09685327665cc127f61648/zensical-0.0.33-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:06d2f57f7bc8cc8fd904386020ea1365eebc411e8698a871e9525c885abca574", size = 12878419, upload-time = "2026-04-14T11:08:06.054Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ec/87fc9e360c694ab006363c7834639eccafd0d26a487cd63dd609bd68f36a/zensical-0.0.33-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:c2851b82d83aa0b2ae4f8e99731cfeedeecebfa04e6b3fc4d375deca629fa240", size = 13022474, upload-time = "2026-04-14T11:08:09.007Z" }, + { url = "https://files.pythonhosted.org/packages/10/b3/0bf174ab6ceedb31d9af462073b5339c894b2084a27d42cb9f0906050d76/zensical-0.0.33-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:90daaf512b0429d7b9147ad5e6085b455d24803eff18b508aed738ca65444683", size = 12975233, upload-time = "2026-04-14T11:08:12.535Z" }, + { url = "https://files.pythonhosted.org/packages/a9/27/7cc3c2d284698647f60f3b823e0101e619c87edf158d47ee11bf4bfb6228/zensical-0.0.33-cp310-abi3-win32.whl", hash = "sha256:2701820597fe19361a12371129927c58c19633dcaa5f6986d610dce58cecd8c4", size = 12012664, upload-time = "2026-04-14T11:08:14.977Z" }, + { url = "https://files.pythonhosted.org/packages/25/0b/6be5c2fdaf9f1600577e7ba5e235d86b72a26f6af389efb146f978f76ac3/zensical-0.0.33-cp310-abi3-win_amd64.whl", hash = "sha256:a5a0911b4247708a55951b74c459f4d5faec5daaf287d23a2e1f0d96be1e647f", size = 12206255, upload-time = "2026-04-14T11:08:17.375Z" }, +] diff --git a/zensical.toml b/zensical.toml new file mode 100644 index 0000000..e1e2db0 --- /dev/null +++ b/zensical.toml @@ -0,0 +1,98 @@ +[project] +site_name = "deploy" +site_description = "CLI tool for deploying and managing applications on remote servers over SSH." +site_author = "Trobz" +site_url = "https://trobz.github.io/deploy.py/" +docs_dir = "docs" +site_dir = "site" +copyright = "Copyright © 2026 Trobz" + +extra_css = ["stylesheets/hero.css", "stylesheets/site.css"] + +[[project.extra_javascript]] +path = "https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js" +defer = true + +[[project.extra_javascript]] +path = "javascripts/hero-scene.js" +defer = true + +nav = [ + { "Home" = "index.md" }, + { "Getting Started" = "getting-started.md" }, + { "Instance Names" = "instance-names.md" }, + { "Configuration" = "configuration.md" }, + { "Commands" = [ + { "Overview" = "commands/index.md" }, + { "configure" = "commands/configure.md" }, + { "update" = "commands/update.md" }, + { "status" = "commands/status.md" }, + ]}, + { "Hooks" = "hooks.md" }, +] + +[project.theme] +name = "modern" +custom_dir = "overrides" +language = "en" +features = [ + "announce.dismiss", + "content.code.annotate", + "content.code.copy", + "content.code.select", + "content.footnote.tooltips", + "content.tabs.link", + "content.tooltips", + "navigation.footer", + "navigation.indexes", + "navigation.instant", + "navigation.instant.prefetch", + "navigation.path", + "navigation.sections", + "navigation.top", + "navigation.tracking", + "search.highlight", +] + +[[project.theme.palette]] +scheme = "default" +toggle.icon = "lucide/sun" +toggle.name = "Switch to dark mode" + +[[project.theme.palette]] +scheme = "slate" +toggle.icon = "lucide/moon" +toggle.name = "Switch to light mode" + +[project.markdown_extensions.abbr] +[project.markdown_extensions.admonition] +[project.markdown_extensions.attr_list] +[project.markdown_extensions.def_list] +[project.markdown_extensions.footnotes] +[project.markdown_extensions.md_in_html] +[project.markdown_extensions.toc] +permalink = true +[project.markdown_extensions.pymdownx.betterem] +[project.markdown_extensions.pymdownx.caret] +[project.markdown_extensions.pymdownx.details] +[project.markdown_extensions.pymdownx.emoji] +emoji_generator = "zensical.extensions.emoji.to_svg" +emoji_index = "zensical.extensions.emoji.twemoji" +[project.markdown_extensions.pymdownx.highlight] +anchor_linenums = true +line_spans = "__span" +pygments_lang_class = true +[project.markdown_extensions.pymdownx.inlinehilite] +[project.markdown_extensions.pymdownx.keys] +[project.markdown_extensions.pymdownx.mark] +[project.markdown_extensions.pymdownx.smartsymbols] +[project.markdown_extensions.pymdownx.superfences] +custom_fences = [ + { name = "mermaid", class = "mermaid", format = "pymdownx.superfences.fence_code_format" } +] +[project.markdown_extensions.pymdownx.tabbed] +alternate_style = true +combine_header_slug = true +[project.markdown_extensions.pymdownx.tasklist] +custom_checkbox = true +[project.markdown_extensions.pymdownx.tilde]