diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 2ae2870..988bc7c 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -32,6 +32,9 @@ jobs:
- name: Type Check
run: npm run check-types
+ - name: Documentation up to date
+ run: npm run docs:check
+
- name: Lint
run: npm run lint
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
new file mode 100644
index 0000000..e826e20
--- /dev/null
+++ b/.github/workflows/docs.yml
@@ -0,0 +1,27 @@
+name: Documentation
+
+on:
+ workflow_dispatch:
+ pull_request:
+ push:
+ branches:
+ - main
+ schedule:
+ # Weekly, to catch links to other websites that went dead.
+ - cron: "41 5 * * 1"
+
+permissions:
+ contents: read
+
+jobs:
+ docs:
+ name: Docs
+ permissions:
+ contents: read
+ pages: write
+ id-token: write
+ # The build, the checks and the deployment are shared by all the documentation sites.
+ uses: boxboxjason/hugo-theme/.github/workflows/docs.yml@089035183ff3ce6abed590404e87faf86037af14 # main, same commit as docs/go.mod
+ with:
+ deploy: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }}
+ check-external-links: ${{ github.event_name == 'schedule' }}
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 214b33b..782b995 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -127,6 +127,16 @@ These are the most useful scripts from [package.json](package.json):
- Reference the issue number (e.g. `Fixes #123`).
- Describe what changed and how it was tested.
+## Documentation
+
+The documentation lives in the [`docs`](docs) directory, as Markdown rendered by [Hugo](https://gohugo.io/) (extended edition) with the shared [hugo-theme](https://github.com/boxboxjason/hugo-theme), and is published to from `main`.
+
+- Change the documentation in the same pull request as the code it describes.
+- The [commands](docs/content/docs/commands.md), [settings](docs/content/docs/settings.md) and [achievements](docs/content/docs/achievements.md) pages are generated from `package.json` and the achievement templates: run `npm run docs:generate` after changing them, and commit the result. CI runs `npm run docs:check`, which fails when a generated page is stale.
+- The getting started walkthrough shown in VS Code ([docs/walkthroughs](docs/walkthroughs)) is also rendered on the site: edit it in one place.
+- `hugo server --source docs` serves the site locally with live reload, `hugo --source docs` builds it into `docs/public`.
+- Look and feel (colors, footer, ...) belong to the shared theme, do not restyle a single site.
+
## Coding guidelines
- Keep changes scoped to the issue.
diff --git a/README.md b/README.md
index b1e3b04..99a6ab5 100644
--- a/README.md
+++ b/README.md
@@ -23,131 +23,20 @@ Achievements is a Visual Studio Code extension that allows you to track your pro
- Enable or disable notifications
- Enable or disable any type of listeners for **privacy**
-## How it works
+## Documentation
-- The extension activates on `onStartupFinished` and listens to VS Code and Git events (based on your settings).
-- Progress and achievement state are stored locally in a SQLite database powered by `sql.js` (no server).
-- The UI is a VS Code webview (React) opened via **Achievements: Show**.
+📖 ****
-### Local data & read-only mode
+The documentation covers [installation](https://boxboxjason.github.io/achievements/docs/installation/), [getting started](https://boxboxjason.github.io/achievements/docs/getting-started/), [how it works](https://boxboxjason.github.io/achievements/docs/how-it-works/) (local data, devcontainers, network access), the full list of [achievements](https://boxboxjason.github.io/achievements/docs/achievements/), [commands](https://boxboxjason.github.io/achievements/docs/commands/) and [settings](https://boxboxjason.github.io/achievements/docs/settings/).
-- Data is stored under VS Code's global storage as `achievements.sqlite`.
-- A lock prevents multiple VS Code instances from writing the database at the same time.
- - If another instance is using the DB, the extension runs in **read-only** mode: listeners are disabled and a status bar item indicates the state.
+## Privacy
-### Sharing progress across devcontainers
-
-When VS Code connects to a devcontainer, `achievements.sqlite` is stored inside the container's filesystem, at a path determined by the extension ID: `~/.vscode-server/data/User/globalStorage/boxboxjason.achievements/achievements.sqlite`
-
-That path is deterministic across every devcontainer you connect to, so you can persist and share progress between containers (and across rebuilds) by mounting a host directory or named Docker volume at that exact location in `.devcontainer/devcontainer.json`:
-
-```jsonc
-{
- "mounts": [
- "source=${localEnv:HOME}/.achievements-data,target=/home/vscode/.vscode-server/data/User/globalStorage/boxboxjason.achievements,type=bind"
- ]
-}
-```
-
-Adjust the remote user's home directory in `target` to match your container image (e.g. `/root` instead of `/home/vscode`), and use the same `source` across devcontainer configs to share one database between all of them.
-
-> **Note:** only open one devcontainer at a time against a shared database. The write lock file
-> lives under the container's own temp directory rather than next to the database, so two
-> containers open simultaneously against the same mounted database won't see each other's lock
-> and could both attempt to write at once.
-
-### Network access
-
-By default, this extension makes **no network requests** and works fully offline.
-
-The only exception is the optional `achievements.checkOutdatedExtensions` setting (disabled by default). When enabled, it periodically sends the IDs of your installed (non-builtin) extensions to the VS Marketplace (`marketplace.visualstudio.com`) to check for outdated versions and award related achievements. Enable it only if you're comfortable sharing your installed-extension list with the Marketplace.
-
-## Extension Commands
-
-Several commands are available to interact with the Achievements extension. You can access these commands through the Command Palette (Ctrl+Shift+P) or by using keybindings.
-
-|Command|Description|
-|---|---|
-|`achievements.enable`|Enable or Disable the Achievements extension event tracking (requires a restart)|
-|`achievements.settings`|Open the Achievements configuration page|
-|`achievements.show`|Show the Achievements panel|
-
-## Configuration
-
-The Achievements extension can be configured through the settings. You can access the settings by going to **File > Preferences > Settings** and searching for "Achievements".
-You can also access the settings by using the command `achievements.settings`.
-
-|Setting|Description|
-|---|---|
-|`achievements.enabled`|Enable or disable the Achievements extension event tracking|
-|`achievements.notifications`|Enable or disable notifications for achievements|
-|`achievements.logDirectory`|Select the directory where the log files will be stored|
-|`achievements.logLevel`|Select the log level (info, debug, error)|
-|`achievements.username`|Define your username for the Achievements score display|
-|`achievements.listeners.debug`|Enable or disable debug listeners|
-|`achievements.listeners.git`|Enable or disable git listeners|
-|`achievements.listeners.extensions`|Enable or disable extension listeners|
-|`achievements.checkOutdatedExtensions`|Opt-in: check installed extensions against the VS Marketplace for outdated versions (sends your installed extension IDs over the network; disabled by default)|
-|`achievements.listeners.files`|Enable or disable file listeners|
-|`achievements.listeners.shortcuts`|Enable or disable paste shortcut listeners|
-|`achievements.listeners.tabs`|Enable or disable tab listeners|
-|`achievements.listeners.tasks`|Enable or disable task listeners|
-|`achievements.listeners.time`|Enable or disable time tracking listeners|
-|`achievements.ignore.files`|List of file names ignored by file-based achievements (case-insensitive, for example `package-lock.json` matches `PACKAGE-LOCK.JSON`)|
-|`achievements.ignore.directories`|List of directory names ignored by file-based achievements (case-sensitive, for example `.git` matches only `.git`)|
+Progress is stored locally in a SQLite database, and by default this extension makes **no network requests**. The only exception is the opt-in `achievements.checkOutdatedExtensions` setting, which sends the IDs of your installed extensions to the VS Marketplace to check for outdated versions. Every listener can be turned off individually in the settings.
## Installation
-### From OpenVSX
-
-1. Download and install [Visual Studio Code](https://code.visualstudio.com/).
-2. Download and install the [Achievements Extension VSIX](https://open-vsx.org/extension/boxboxjason/achievements) from the OpenVSX registry.
-3. Open Visual Studio Code. Go to the Extensions view by clicking on the Extensions icon in the Activity Bar on the side of the window or by pressing `Ctrl+Shift+X`.
-4. Click on the three-dot menu icon in the top-right corner of the Extensions view and select "Install from VSIX...".
-5. In the file dialog that appears, navigate to the location where you downloaded the Achievements Extension VSIX file, select it, and click "Open".
-6. Visual Studio Code will install the extension. Once the installation is complete, you may need to reload Visual Studio Code for the extension to be activated. You can do this by clicking the "Reload" button that appears in the Extensions view after installation or by closing and reopening Visual Studio Code.
-7. Run the command `Achievements: Show` from the Command Palette (`Ctrl+Shift+P`) to open the Achievements panel.
-
-### From VSIX
-
-1. Download and install [Visual Studio Code](https://code.visualstudio.com/).
-2. Download and install the [Achievements Extension VSIX](https://github.com/boxboxjason/achievements/releases) from the GitHub releases page.
-3. Open Visual Studio Code. Go to the Extensions view by clicking on the Extensions icon in the Activity Bar on the side of the window or by pressing `Ctrl+Shift+X`.
-4. Click on the three-dot menu icon in the top-right corner of the Extensions view and select "Install from VSIX...".
-5. In the file dialog that appears, navigate to the location where you downloaded the Achievements Extension VSIX file, select it, and click "Open".
-6. Visual Studio Code will install the extension. Once the installation is complete, you may need to reload Visual Studio Code for the extension to be activated. You can do this by clicking the "Reload" button that appears in the Extensions view after installation or by closing and reopening Visual Studio Code.
-7. Run the command `Achievements: Show` from the Command Palette (`Ctrl+Shift+P`) to open the Achievements panel.
+Install it from [Open VSX](https://open-vsx.org/extension/boxboxjason/achievements), or download the VSIX from the [GitHub releases](https://github.com/boxboxjason/achievements/releases) and run **Extensions: Install from VSIX...**. Then run **Achievements: Show** from the Command Palette (`Ctrl+Shift+P`).
## Development
-Prerequisites:
-
-- Node.js `24` (matches CI)
-- npm
-- VS Code
-
-Quick start:
-
-1. Install dependencies: `npm ci`
-2. Open in VS Code: `code .`
-3. Press `F5` (launches the Extension Development Host)
-
-Build system notes:
-
-- The default debug launch runs the VS Code task `watch` (see [.vscode/tasks.json](.vscode/tasks.json)).
- - `watch:esbuild` bundles the extension + webview.
- - `watch:tsc` type-checks in watch mode.
-
-Useful scripts:
-
-|Command|Description|
-|---|---|
-|`npm run compile`|Clean + build Tailwind + bundle extension/webview (dev build)|
-|`npm run build`|Production build (typecheck + lint + minified bundle)|
-|`npm test`|Compile with `tsc` and run extension tests via `@vscode/test-cli`|
-|`npm run test:coverage`|Run tests with NYC coverage output|
-|`npm run lint`|Lint `src/`|
-|`npm run check-types`|Typecheck only (`tsc --noEmit`)|
-|`npm run package`|Build and create a `.vsix` via `vsce`|
-
-Contributing guidelines: see [CONTRIBUTING.md](CONTRIBUTING.md).
+See [CONTRIBUTING.md](CONTRIBUTING.md) to build, test and package the extension, and to work on the documentation.
diff --git a/docs/.gitignore b/docs/.gitignore
new file mode 100644
index 0000000..1c3fa73
--- /dev/null
+++ b/docs/.gitignore
@@ -0,0 +1,3 @@
+public/
+resources/
+.hugo_build.lock
diff --git a/docs/content/_index.md b/docs/content/_index.md
new file mode 100644
index 0000000..4fd9297
--- /dev/null
+++ b/docs/content/_index.md
@@ -0,0 +1,33 @@
+---
+title: Achievements
+layout: hextra-home
+---
+
+{{< hextra/hero-headline >}}
+ Earn achievements while you code
+{{< /hextra/hero-headline >}}
+
+{{< hextra/hero-subtitle >}}
+ A VS Code extension that tracks your coding journey and rewards it, entirely offline.
+{{< /hextra/hero-subtitle >}}
+
+{{< spacer >}}
+
+{{< hextra/hero-button text="Get started" link="docs/installation" >}}
+
+{{< spacer "3rem" >}}
+
+{{< hextra/feature-grid >}}
+ {{< hextra/feature-card
+ title="Nothing to trigger"
+ subtitle="Achievements unlock from what you already do: saving files, committing, switching tabs, running tasks, fixing errors, and more."
+ >}}
+ {{< hextra/feature-card
+ title="Private by default"
+ subtitle="Progress lives in a local SQLite database, no network request is made unless you opt in, and every listener can be turned off."
+ >}}
+ {{< hextra/feature-card
+ title="Your profile"
+ subtitle="Follow your completion percentage, the time spent coding, and the progress towards every achievement in one panel."
+ >}}
+{{< /hextra/feature-grid >}}
diff --git a/docs/content/docs/_index.md b/docs/content/docs/_index.md
new file mode 100644
index 0000000..23a05a4
--- /dev/null
+++ b/docs/content/docs/_index.md
@@ -0,0 +1,37 @@
+---
+title: Documentation
+weight: 1
+---
+
+Achievements is a Visual Studio Code extension that allows you to track your progress in coding, and earn achievements for completing tasks.
+
+
+
+{{< callout type="warning" >}}
+This extension is in beta, may contain bugs and database schema may change in future versions.
+It is recommended to back up your data before updating.
+{{< /callout >}}
+
+## Features
+
+- Track your progress in coding
+- Earn achievements for completing tasks
+- View your achievements in the **Achievements** panel
+- View each achievement's description and requirements
+- View your progress towards each achievement
+- Track your time spent coding
+- Filter achievements by category, progress, and name
+- View your profile and completion percentage
+- Enable or disable notifications
+- Enable or disable any type of listeners for **privacy**
+
+## Where to go next
+
+{{< cards >}}
+ {{< card link="installation" title="Installation" subtitle="From Open VSX or from a VSIX file." >}}
+ {{< card link="getting-started" title="Getting started" subtitle="Open the panel, set your username, choose what gets tracked." >}}
+ {{< card link="how-it-works" title="How it works" subtitle="Local data, read-only mode, devcontainers and network access." >}}
+ {{< card link="achievements" title="Achievements" subtitle="Every achievement and what it takes to unlock it." >}}
+ {{< card link="commands" title="Commands" subtitle="The commands available from the Command Palette." >}}
+ {{< card link="settings" title="Settings" subtitle="Every setting, with its default value." >}}
+{{< /cards >}}
diff --git a/docs/content/docs/achievements.md b/docs/content/docs/achievements.md
new file mode 100644
index 0000000..73f6c1f
--- /dev/null
+++ b/docs/content/docs/achievements.md
@@ -0,0 +1,65 @@
+---
+title: Achievements
+weight: 40
+---
+
+
+
+Every achievement comes in tiers: each tier raises the required value N, and earns more experience.
+
+Rows with `` exist once per tracked language: `ada`, `assembly`, `c`, `c#`, `c++`, `cobol`, `css`, `fortran`, `go`, `haskell`, `html`, `java`, `javascript`, `kotlin`, `less`, `lua`, `markdown`, `matlab`, `objective-c`, `pascal`, `perl`, `php`, `powershell`, `python`, `r`, `react`, `ruby`, `rust`, `scala`, `scss`, `shell`, `sql`, `swift`, `toml`, `typescript`, `vue`, `yaml`.
+
+## Productivity
+
+| Achievements | Group | Requirement | Required N |
+| ------------ | ----- | ----------- | ---------- |
+| Code Monkey 1 … Code Monkey 11 | Lines of Code | Write N lines of code | 1 → 10,000,000,000 (11 tiers) |
+| `` Speaker 1 … `` Speaker 16 | Lines of Code | Write N lines of `` code | 2 → 100,000,000 (16 tiers) |
+| Outstanding Commentator 1 … Outstanding Commentator 16 | Lines of Comments | Write N lines of comments | 2 → 100,000,000 (16 tiers) |
+| Copy Ninja 1 … Copy Ninja 16 | Number of Pastes | Paste N times | 1 → 50,000,000 (16 tiers) |
+| I can fix her 1 … I can fix her 16 | Errors Fixed | Fix N errors | 1 → 50,000,000 (16 tiers) |
+| Shower Avoider 1 … Shower Avoider 14 | time spent | Spend N hours coding | 1 → 1,000,000 (14 tiers) |
+| Tab Hoarder 1 … Tab Hoarder 6 | Number of Simultaneous Tabs | Open N tabs simultaneously | 1 → 100 (6 tiers) |
+| Split Personality 1 … Split Personality 6 | Number of Simultaneous Editor Groups | Open N editor groups simultaneously | 1 → 100 (6 tiers) |
+| Multi-tasker 1 … Multi-tasker 6 | Number of Simultaneous Workspace Folders | Open N workspace folders simultaneously | 1 → 100 (6 tiers) |
+| Remote Worker 1 … Remote Worker 14 | time spent | Spend N hours coding remotely | 1 → 1,000,000 (14 tiers) |
+| Hydra Editing 1 … Hydra Editing 11 | Multi Cursor Sessions | Start multi-cursor editing N times | 1 → 100,000 (11 tiers) |
+| Night Owl 1 … Night Owl 11 | Night Owl Sessions | Start N coding sessions late at night | 1 → 50,000 (11 tiers) |
+| Early Bird 1 … Early Bird 11 | Early Bird Sessions | Start N coding sessions early in the morning | 1 → 50,000 (11 tiers) |
+| Connection Streak 1 … Connection Streak 1000 | Connection Streak | Connect for N consecutive days | 1 → 1,000 (1000 tiers) |
+
+## Git
+
+| Achievements | Group | Requirement | Required N |
+| ------------ | ----- | ----------- | ---------- |
+| Committer 1 … Committer 11 | Commits | Commit N times | 1 → 100,000 (11 tiers) |
+| Friend of the Trees 1 … Friend of the Trees 11 | Branches Created | Create N branches | 1 → 50,000 (11 tiers) |
+| Merger 1 … Merger 11 | Merges and Rebases | Merge or rebase N times | 1 → 50,000 (11 tiers) |
+| Amender 1 … Amender 11 | Amends | Amend N times | 1 → 50,000 (11 tiers) |
+| Product Shipping 1 … Product Shipping 11 | Pushes | Push N times | 1 → 50,000 (11 tiers) |
+
+## VS Code
+
+| Achievements | Group | Requirement | Required N |
+| ------------ | ----- | ----------- | ---------- |
+| Buying Frenzy 1 … Buying Frenzy 6 | Extensions Installed | Install N extensions | 1 → 100 (6 tiers) |
+| Living Dangerously 1 … Living Dangerously 6 | Extensions Outdated | Have N outdated extensions | 1 → 100 (6 tiers) |
+| Pimp my Ride 1 … Pimp my Ride 6 | Themes Installed | Install N themes | 1 → 100 (6 tiers) |
+| Chameleon 1 … Chameleon 9 | Theme Changed | Change theme N times | 1 → 5,000 (9 tiers) |
+| Am I better than everyone ? 1 … Am I better than everyone ? 11 | Debugger Sessions | Start N debugger sessions | 1 → 50,000 (11 tiers) |
+| Breaking Bad 1 … Breaking Bad 11 | Breakpoints | Create N breakpoints | 1 → 100,000 (11 tiers) |
+| Commander 1 … Commander 11 | tasks | Run N vscode tasks (terminal commands) | 1 → 50,000 (11 tiers) |
+| Failure achiever 1 … Failure achiever 11 | tasks | Run N failing vscode tasks or terminal command | 1 → 50,000 (11 tiers) |
+| Works on my Machine 1 … Works on my Machine 11 | tasks | Run N successful vscode tasks or terminal command | 1 → 50,000 (11 tiers) |
+| Portal Opener 1 … Portal Opener 11 | Terminals Opened | Open N terminals | 1 → 50,000 (11 tiers) |
+| Snippets Master 1 … Snippets Master 11 | Code Snippets | Use N code snippets | 1 → 50,000 (11 tiers) |
+
+## Files
+
+| Achievements | Group | Requirement | Required N |
+| ------------ | ----- | ----------- | ---------- |
+| Creator 1 … Creator 16 | Files Created | Create N files | 2 → 100,000,000 (16 tiers) |
+| `` Creator 1 … `` Creator 12 | Files Created | Create N files in `` | 1 → 500,000 (12 tiers) |
+| Please Bro, Just One more Refactor 1 … Please Bro, Just One more Refactor 11 | Files Renamed | Rename or Move N files | 1 → 50,000 (11 tiers) |
+| Shaper 1 … Shaper 11 | Resource Created | Create N directories | 1 → 50,000 (11 tiers) |
+| Destroyer 1 … Destroyer 11 | Resource Deleted | Delete N files or directories | 1 → 50,000 (11 tiers) |
diff --git a/docs/content/docs/commands.md b/docs/content/docs/commands.md
new file mode 100644
index 0000000..453da5d
--- /dev/null
+++ b/docs/content/docs/commands.md
@@ -0,0 +1,15 @@
+---
+title: Commands
+weight: 50
+---
+
+
+
+Run these commands from the Command Palette (`Ctrl+Shift+P` / `Cmd+Shift+P`), or bind them to keyboard shortcuts.
+
+| Command | Identifier |
+| ------- | ---------- |
+| **Achievements: Enable / Disable** | `achievements.enable` |
+| **Achievements: Settings** | `achievements.settings` |
+| **Achievements: Show** | `achievements.show` |
+| **Achievements: Quick Menu** | `achievements.statusBarMenu` |
diff --git a/docs/content/docs/getting-started.md b/docs/content/docs/getting-started.md
new file mode 100644
index 0000000..1bca32b
--- /dev/null
+++ b/docs/content/docs/getting-started.md
@@ -0,0 +1,18 @@
+---
+title: Getting started
+weight: 20
+---
+
+These are the steps of the **Get Started with Achievements** walkthrough, also available inside VS Code from **Help > Welcome**.
+
+{{< walkthrough "getting-started/open-panel" >}}
+
+{{< walkthrough "getting-started/username" >}}
+
+{{< walkthrough "getting-started/configure" >}}
+
+See [settings]({{< relref "settings" >}}) for the full list.
+
+{{< walkthrough "getting-started/first-achievement" >}}
+
+Browse the [achievements]({{< relref "achievements" >}}) to see what there is to unlock.
diff --git a/docs/content/docs/how-it-works.md b/docs/content/docs/how-it-works.md
new file mode 100644
index 0000000..e714117
--- /dev/null
+++ b/docs/content/docs/how-it-works.md
@@ -0,0 +1,40 @@
+---
+title: How it works
+weight: 30
+---
+
+- The extension activates on `onStartupFinished` and listens to VS Code and Git events (based on your settings).
+- Progress and achievement state are stored locally in a SQLite database powered by `sql.js` (no server).
+- The UI is a VS Code webview (React) opened via **Achievements: Show**.
+
+## Local data and read-only mode
+
+- Data is stored under VS Code's global storage as `achievements.sqlite`.
+- A lock prevents multiple VS Code instances from writing the database at the same time.
+ - If another instance is using the DB, the extension runs in **read-only** mode: listeners are disabled and a status bar item indicates the state.
+
+## Sharing progress across devcontainers
+
+When VS Code connects to a devcontainer, `achievements.sqlite` is stored inside the container's filesystem, at a path determined by the extension ID: `~/.vscode-server/data/User/globalStorage/boxboxjason.achievements/achievements.sqlite`
+
+That path is deterministic across every devcontainer you connect to, so you can persist and share progress between containers (and across rebuilds) by mounting a host directory or named Docker volume at that exact location in `.devcontainer/devcontainer.json`:
+
+```jsonc
+{
+ "mounts": [
+ "source=${localEnv:HOME}/.achievements-data,target=/home/vscode/.vscode-server/data/User/globalStorage/boxboxjason.achievements,type=bind"
+ ]
+}
+```
+
+Adjust the remote user's home directory in `target` to match your container image (e.g. `/root` instead of `/home/vscode`), and use the same `source` across devcontainer configs to share one database between all of them.
+
+{{< callout type="warning" >}}
+Only open one devcontainer at a time against a shared database. The write lock file lives under the container's own temp directory rather than next to the database, so two containers open simultaneously against the same mounted database won't see each other's lock and could both attempt to write at once.
+{{< /callout >}}
+
+## Network access
+
+By default, this extension makes **no network requests** and works fully offline.
+
+The only exception is the optional `achievements.checkOutdatedExtensions` [setting]({{< relref "settings" >}}) (disabled by default). When enabled, it periodically sends the IDs of your installed (non-builtin) extensions to the VS Marketplace (`marketplace.visualstudio.com`) to check for outdated versions and award related achievements. Enable it only if you're comfortable sharing your installed-extension list with the Marketplace.
diff --git a/docs/content/docs/installation.md b/docs/content/docs/installation.md
new file mode 100644
index 0000000..347874c
--- /dev/null
+++ b/docs/content/docs/installation.md
@@ -0,0 +1,26 @@
+---
+title: Installation
+weight: 10
+---
+
+## From Open VSX
+
+1. Download and install [Visual Studio Code](https://code.visualstudio.com/).
+2. Download and install the [Achievements Extension VSIX](https://open-vsx.org/extension/boxboxjason/achievements) from the OpenVSX registry.
+3. Open Visual Studio Code. Go to the Extensions view by clicking on the Extensions icon in the Activity Bar on the side of the window or by pressing `Ctrl+Shift+X`.
+4. Click on the three-dot menu icon in the top-right corner of the Extensions view and select "Install from VSIX...".
+5. In the file dialog that appears, navigate to the location where you downloaded the Achievements Extension VSIX file, select it, and click "Open".
+6. Visual Studio Code will install the extension. Once the installation is complete, you may need to reload Visual Studio Code for the extension to be activated. You can do this by clicking the "Reload" button that appears in the Extensions view after installation or by closing and reopening Visual Studio Code.
+7. Run the command `Achievements: Show` from the Command Palette (`Ctrl+Shift+P`) to open the Achievements panel.
+
+## From VSIX
+
+1. Download and install [Visual Studio Code](https://code.visualstudio.com/).
+2. Download and install the [Achievements Extension VSIX](https://github.com/boxboxjason/achievements/releases) from the GitHub releases page.
+3. Open Visual Studio Code. Go to the Extensions view by clicking on the Extensions icon in the Activity Bar on the side of the window or by pressing `Ctrl+Shift+X`.
+4. Click on the three-dot menu icon in the top-right corner of the Extensions view and select "Install from VSIX...".
+5. In the file dialog that appears, navigate to the location where you downloaded the Achievements Extension VSIX file, select it, and click "Open".
+6. Visual Studio Code will install the extension. Once the installation is complete, you may need to reload Visual Studio Code for the extension to be activated. You can do this by clicking the "Reload" button that appears in the Extensions view after installation or by closing and reopening Visual Studio Code.
+7. Run the command `Achievements: Show` from the Command Palette (`Ctrl+Shift+P`) to open the Achievements panel.
+
+Once installed, follow the [getting started]({{< relref "getting-started" >}}) steps.
diff --git a/docs/content/docs/settings.md b/docs/content/docs/settings.md
new file mode 100644
index 0000000..77074b4
--- /dev/null
+++ b/docs/content/docs/settings.md
@@ -0,0 +1,27 @@
+---
+title: Settings
+weight: 60
+---
+
+
+
+Open **File > Preferences > Settings** and search for "Achievements", or run **Achievements: Settings**.
+
+| Setting | Type | Default | Description |
+| ------- | ---- | ------- | ----------- |
+| `achievements.enabled` | boolean | `true` | Enable or disable the Achievements extension. |
+| `achievements.logLevel` | string | `INFO` | Set the log level for the extension. One of `DEBUG`, `INFO`, `WARN`, `ERROR`, `FATAL`. |
+| `achievements.notifications` | boolean | `true` | Enable or disable Achievements extension notifications. |
+| `achievements.logDirectory` | string | `""` | Specify the directory for log files. |
+| `achievements.username` | string | `Guest` | Set your username for the Achievements extension. |
+| `achievements.listeners.debug` | boolean | `true` | Enable or disable debug events listeners for the Achievements extension. |
+| `achievements.listeners.extensions` | boolean | `true` | Enable or disable extensions / themes events listeners for the Achievements extension. This only tracks locally installed/enabled extensions and does not make any network request; see achievements.checkOutdatedExtensions for the opt-in Marketplace check. |
+| `achievements.checkOutdatedExtensions` | boolean | `false` | Opt-in: periodically send the IDs of your installed (non-builtin) extensions to the VS Marketplace (marketplace.visualstudio.com) to check for outdated versions and award related achievements. Disabled by default since this is the only network request made by this extension. |
+| `achievements.listeners.files` | boolean | `true` | Enable or disable file events listeners for the Achievements extension. |
+| `achievements.listeners.git` | boolean | `true` | Enable or disable git events listeners for the Achievements extension. |
+| `achievements.listeners.shortcuts` | boolean | `true` | Enable or disable paste shortcut events listeners for the Achievements extension. |
+| `achievements.listeners.tabs` | boolean | `true` | Enable or disable tabs events listeners for the Achievements extension. |
+| `achievements.listeners.tasks` | boolean | `true` | Enable or disable tasks events listeners for the Achievements extension. |
+| `achievements.listeners.time` | boolean | `true` | Enable or disable time events listeners for the Achievements extension. |
+| `achievements.ignore.files` | array | `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`, `bun.lockb`, `.ds_store`, `thumbs.db` | File names ignored by file-related achievements (case-insensitive). |
+| `achievements.ignore.directories` | array | `.git`, `.svn`, `.hg`, `.jj`, `node_modules` | Directory names ignored by file-related achievements (case-sensitive). |
diff --git a/docs/go.mod b/docs/go.mod
new file mode 100644
index 0000000..03a3359
--- /dev/null
+++ b/docs/go.mod
@@ -0,0 +1,5 @@
+module github.com/boxboxjason/achievements/docs
+
+go 1.26
+
+require github.com/boxboxjason/hugo-theme v0.0.0-20261030223012-089035183ff3 // indirect
diff --git a/docs/go.sum b/docs/go.sum
new file mode 100644
index 0000000..6e2293b
--- /dev/null
+++ b/docs/go.sum
@@ -0,0 +1,2 @@
+github.com/boxboxjason/hugo-theme v0.0.0-20261030223012-089035183ff3 h1:lHhCSzGHFEjxtWJIJcZHBUkNHL910kZPCwA6QTOiNvM=
+github.com/boxboxjason/hugo-theme v0.0.0-20261030223012-089035183ff3/go.mod h1:ebGD0oRHgFK9/q9krUBEOIi6WHj6nixMPEXoGoN5nnk=
diff --git a/docs/hugo.toml b/docs/hugo.toml
new file mode 100644
index 0000000..f33e316
--- /dev/null
+++ b/docs/hugo.toml
@@ -0,0 +1,58 @@
+baseURL = "https://boxboxjason.github.io/achievements/"
+title = "Achievements"
+locale = "en-US"
+
+# "Last updated" dates on pages come from git history, so CI must check out all of it.
+enableGitInfo = true
+enableRobotsTXT = true
+
+# Hugo does not merge `markup` from a theme: keep this block in every site.
+[markup]
+ [markup.highlight]
+ noClasses = false
+
+# Look and feel (colours, favicon, footer, search, ...) come from the shared theme,
+# which is the same for every documentation site. Only project specifics live here.
+[module]
+ [[module.imports]]
+ path = "github.com/boxboxjason/hugo-theme"
+ [[module.mounts]]
+ source = "content"
+ target = "content"
+ [[module.mounts]]
+ source = "layouts"
+ target = "layouts"
+ # The getting started walkthrough shown inside VS Code is rendered on the site too,
+ # from the same files, and so is the panel animation.
+ [[module.mounts]]
+ source = "walkthroughs"
+ target = "assets/walkthroughs"
+ [[module.mounts]]
+ source = "."
+ target = "static/images"
+ files = ["achievements.gif"]
+
+[menu]
+ [[menu.main]]
+ name = "Documentation"
+ pageRef = "/docs"
+ weight = 1
+ [[menu.main]]
+ name = "Open VSX"
+ url = "https://open-vsx.org/extension/BoxBoxJason/achievements"
+ weight = 2
+ [[menu.main]]
+ name = "Search"
+ weight = 3
+ [menu.main.params]
+ type = "search"
+ [[menu.main]]
+ name = "GitHub"
+ weight = 4
+ url = "https://github.com/boxboxjason/achievements"
+ [menu.main.params]
+ icon = "github"
+
+[params]
+ [params.editURL]
+ base = "https://github.com/boxboxjason/achievements/edit/main/docs/content"
diff --git a/docs/layouts/_shortcodes/walkthrough.html b/docs/layouts/_shortcodes/walkthrough.html
new file mode 100644
index 0000000..1542392
--- /dev/null
+++ b/docs/layouts/_shortcodes/walkthrough.html
@@ -0,0 +1,11 @@
+{{- /*
+ Renders a step of the VS Code getting started walkthrough (docs/walkthroughs/.md),
+ so the extension and the site share one source. Its top level heading is demoted to fit
+ under the page title: {{< walkthrough "getting-started/open-panel" >}}
+*/ -}}
+{{- $path := printf "walkthroughs/%s.md" (.Get 0) -}}
+{{- with resources.Get $path -}}
+ {{- .Content | replaceRE `(?m)^# ` "## " | $.Page.RenderString -}}
+{{- else -}}
+ {{- errorf "walkthrough %q not found (%s)" $path $.Position -}}
+{{- end -}}
diff --git a/package.json b/package.json
index fb1a75a..f4a3421 100644
--- a/package.json
+++ b/package.json
@@ -244,7 +244,9 @@
"test:coverage": "npm run clean && rm -rf .nyc_output dist-cov && npx tsc && npx nyc instrument dist dist-cov --include=\"**/*.js\" --exclude=\"**/test/**\" && cp -r dist-cov/* dist/ && node dist/test/runTest.js && npx nyc report --reporter=text-summary --reporter=text",
"clean": "rm -rf dist dist-cov .nyc_output",
"tailwind": "npx tailwindcss -i ./src/views/style/main.css -o ./dist/style/main.css --minify",
- "changelog": "git-cliff -c cliff.toml -o CHANGELOG.md"
+ "changelog": "git-cliff -c cliff.toml -o CHANGELOG.md",
+ "docs:generate": "ts-node scripts/generate-docs.ts",
+ "docs:check": "ts-node scripts/generate-docs.ts --check"
},
"devDependencies": {
"@eslint-react/eslint-plugin": "5.20.8",
@@ -279,7 +281,8 @@
"files": [
"dist",
"assets",
- "docs",
+ "docs/achievements.gif",
+ "docs/walkthroughs",
"README.md",
"CHANGELOG.md",
"LICENSE.md",
diff --git a/scripts/generate-docs.ts b/scripts/generate-docs.ts
new file mode 100644
index 0000000..12f79db
--- /dev/null
+++ b/scripts/generate-docs.ts
@@ -0,0 +1,216 @@
+/**
+ * Generates the reference pages of the documentation site from their source of truth:
+ * - docs/content/docs/commands.md and settings.md from the package.json contributions,
+ * - docs/content/docs/achievements.md from the achievement stacking templates.
+ *
+ * Usage: ts-node scripts/generate-docs.ts [--check]
+ * With --check, nothing is written and the process fails when a committed page is stale.
+ *
+ * @author BoxBoxJason
+ */
+
+import fs from "node:fs";
+import path from "node:path";
+import { StackingTemplates } from "../src/database/model/init/StackingTemplates";
+import { StackingAchievementTemplate } from "../src/database/model/tables/Achievement";
+
+const ROOT = path.resolve(__dirname, "..");
+const PAGES_DIR = path.join(ROOT, "docs", "content", "docs");
+const GENERATED_NOTICE =
+ "";
+
+interface Command {
+ command: string;
+ title: string;
+}
+
+interface Setting {
+ type: string;
+ default?: unknown;
+ description?: string;
+ enum?: string[];
+}
+
+interface PackageJson {
+ contributes: {
+ commands: Command[];
+ configuration: { properties: Record }[];
+ };
+}
+
+const numberFormat = new Intl.NumberFormat("en-US");
+
+/** Escapes a value for a Markdown table cell. */
+function cell(value: string): string {
+ return value.replaceAll("|", "\\|").replaceAll("\n", " ");
+}
+
+function code(value: string): string {
+ return "`" + value + "`";
+}
+
+function page(title: string, weight: number, intro: string, body: string): string {
+ return `---\ntitle: ${title}\nweight: ${weight}\n---\n\n${GENERATED_NOTICE}\n\n${intro}\n\n${body.trimEnd()}\n`;
+}
+
+function commandsPage(pkg: PackageJson): string {
+ const rows = pkg.contributes.commands.map(
+ ({ command, title }) => `| **${cell(title)}** | ${code(command)} |`,
+ );
+
+ return page(
+ "Commands",
+ 50,
+ "Run these commands from the Command Palette (`Ctrl+Shift+P` / `Cmd+Shift+P`), or bind them to keyboard shortcuts.",
+ ["| Command | Identifier |", "| ------- | ---------- |", ...rows].join("\n"),
+ );
+}
+
+function formatDefault(value: unknown): string {
+ if (Array.isArray(value)) {
+ return value.length === 0 ? "`[]`" : value.map((item) => code(String(item))).join(", ");
+ }
+ if (value === undefined) {
+ return "";
+ }
+ return value === "" ? '`""`' : code(String(value));
+}
+
+function settingsPage(pkg: PackageJson): string {
+ const rows = pkg.contributes.configuration.flatMap((section) =>
+ Object.entries(section.properties).map(([name, setting]) => {
+ let description = setting.description ?? "";
+ if (setting.enum) {
+ description += ` One of ${setting.enum.map(code).join(", ")}.`;
+ }
+ return `| ${code(name)} | ${setting.type} | ${formatDefault(setting.default)} | ${cell(description.trim())} |`;
+ }),
+ );
+
+ return page(
+ "Settings",
+ 60,
+ 'Open **File > Preferences > Settings** and search for "Achievements", or run **Achievements: Settings**.',
+ ["| Setting | Type | Default | Description |", "| ------- | ---- | ------- | ----------- |", ...rows].join("\n"),
+ );
+}
+
+/** The language a per-language template variant is about, read from its criteria name suffix. */
+function variantLanguage(template: StackingAchievementTemplate): string {
+ const criteria = template.criterias[0];
+ return criteria.slice(criteria.lastIndexOf("_") + 1);
+}
+
+/** Replaces the criteria names of a description with N (or N1, N2, ... when there are several). */
+function genericDescription(template: StackingAchievementTemplate): string {
+ return template.criterias.reduce(
+ (description, criteria, index) =>
+ description.replace(criteria, template.criterias.length === 1 ? "N" : `N${index + 1}`),
+ template.description,
+ );
+}
+
+function thresholds(template: StackingAchievementTemplate): string {
+ const tiers = template.maxTier - template.minTier + 1;
+ const first = template.criteriasFunctions[0](template.minTier);
+ const last = template.criteriasFunctions[0](template.maxTier);
+ const range = tiers === 1 ? numberFormat.format(first) : `${numberFormat.format(first)} → ${numberFormat.format(last)}`;
+ return `${range} (${tiers} ${tiers === 1 ? "tier" : "tiers"})`;
+}
+
+function titles(title: string, template: StackingAchievementTemplate): string {
+ const tiers = template.maxTier - template.minTier + 1;
+ if (!title.includes("%d")) {
+ return title;
+ }
+ return tiers === 1 ? title.replace("%d", "1") : `${title.replace("%d", "1")} … ${title.replace("%d", String(tiers))}`;
+}
+
+function achievementRows(templates: StackingAchievementTemplate[], languages: Set): string[] {
+ return templates
+ .filter((template) => !template.hidden)
+ .map((template) => {
+ let title = template.title;
+ let description = genericDescription(template);
+ if (template.criterias[0].includes("_")) {
+ const language = variantLanguage(template);
+ languages.add(language);
+ // Whole words only: the "r" language must not match the "r" of "Write".
+ const word = new RegExp(`(?`");
+ description = description.replace(word, "``");
+ }
+ return `| ${cell(titles(title, template))} | ${cell(template.group)} | ${cell(description)} | ${thresholds(template)} |`;
+ });
+}
+
+function achievementsPage(): string {
+ // Same categories, in the same order, as the database initialization (src/database/model/init/init.ts).
+ const categories: [string, Record][] = [
+ ["Productivity", StackingTemplates.productivity],
+ ["Git", StackingTemplates.git],
+ ["VS Code", StackingTemplates.vscode],
+ ["Files", StackingTemplates.files],
+ ];
+ const languages = new Set();
+ const sections: string[] = [];
+
+ for (const [name, namespace] of categories) {
+ const rows: string[] = [];
+ for (const factory of Object.values(namespace)) {
+ if (typeof factory !== "function") {
+ continue;
+ }
+ const result = (factory as () => StackingAchievementTemplate | StackingAchievementTemplate[])();
+ const templates = Array.isArray(result) ? result : [result];
+ // Per-language variants only differ by their language: keep one row for all of them.
+ const unique = new Map();
+ for (const row of achievementRows(templates, languages)) {
+ unique.set(row, row);
+ }
+ rows.push(...unique.values());
+ }
+ sections.push(
+ `## ${name}\n\n| Achievements | Group | Requirement | Required N |\n| ------------ | ----- | ----------- | ---------- |\n${rows.join("\n")}`,
+ );
+ }
+
+ const intro = [
+ "Every achievement comes in tiers: each tier raises the required value N, and earns more experience.",
+ "",
+ `Rows with \`\` exist once per tracked language: ${[...languages].sort().map(code).join(", ")}.`,
+ ].join("\n");
+
+ return page("Achievements", 40, intro, sections.join("\n\n"));
+}
+
+function main(): void {
+ const check = process.argv.includes("--check");
+ const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, "package.json"), "utf8")) as PackageJson;
+ const pages: Record = {
+ "commands.md": commandsPage(pkg),
+ "settings.md": settingsPage(pkg),
+ "achievements.md": achievementsPage(),
+ };
+
+ const stale: string[] = [];
+ for (const [file, content] of Object.entries(pages)) {
+ const target = path.join(PAGES_DIR, file);
+ if (check) {
+ const current = fs.existsSync(target) ? fs.readFileSync(target, "utf8") : "";
+ if (current !== content) {
+ stale.push(path.relative(ROOT, target));
+ }
+ } else {
+ fs.mkdirSync(PAGES_DIR, { recursive: true });
+ fs.writeFileSync(target, content);
+ }
+ }
+
+ if (stale.length > 0) {
+ console.error(`Stale documentation pages: ${stale.join(", ")}. Run \`npm run docs:generate\` and commit the result.`);
+ process.exit(1);
+ }
+}
+
+main();
diff --git a/tsconfig.json b/tsconfig.json
index a1ae384..21941c7 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -15,6 +15,7 @@
"strict": true // Enable all strict type-checking options
},
"exclude": [
- "esbuild.ts"
+ "esbuild.ts",
+ "scripts"
],
}