k33g/demo-mini-me-editorpublic Fork
⑂ bf66a66
Commits
⬇ Clone ▾
git clone https://git.rickub.com/k33g/demo-mini-me-editor.git
git clone ssh://git@rickub.com/k33g/demo-mini-me-editor.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

🛟 Updated.

k33g committed 2026-10-13T18:53:44+02:00 Browse files
bf66a66
added .gitignore +1 -0
new file mode 100644
@@ -0,0 +1 @@
1+blog
new file mode 100644
@@ -0,0 +1 @@
1+blog
added .vscode/extensions.json +9 -0
new file mode 100644
@@ -0,0 +1,9 @@
1+{
2+ "recommendations": [
3+ "ms-azuretools.vscode-docker",
4+ "pkief.material-icon-theme",
5+ "pkief.material-product-icons",
6+ "aaron-bond.better-comments",
7+ "bierner.markdown-mermaid",
8+ ]
9+}
\ No newline at end of file
new file mode 100644
@@ -0,0 +1,9 @@
1+{
2+ "recommendations": [
3+ "ms-azuretools.vscode-docker",
4+ "pkief.material-icon-theme",
5+ "pkief.material-product-icons",
6+ "aaron-bond.better-comments",
7+ "bierner.markdown-mermaid",
8+ ]
9+}
\ No newline at end of file\ No newline at end of file
added .vscode/settings.json +63 -0
new file mode 100644
@@ -0,0 +1,63 @@
1+{
2+ "workbench.iconTheme": "material-icon-theme",
3+ "workbench.colorTheme": "Light Modern",
4+ "editor.fontSize": 14,
5+ "terminal.integrated.fontSize": 16,
6+ "editor.insertSpaces": true,
7+ "editor.tabSize": 4,
8+ "editor.detectIndentation": true,
9+ "files.autoSave": "afterDelay",
10+ "files.autoSaveDelay": 1000,
11+ // "editor.defaultFormatter": "esbenp.prettier-vscode",
12+ "editor.formatOnSave": true,
13+ "workbench.tree.indent": 20,
14+ //"workbench.activityBar.location": "top",
15+ "workbench.editor.showTabs": "multiple",
16+ "window.zoomLevel": 0.0,
17+ "[markdown]": {
18+ "editor.unicodeHighlight.ambiguousCharacters": false,
19+ "editor.unicodeHighlight.invisibleCharacters": false,
20+ "diffEditor.ignoreTrimWhitespace": false,
21+ "editor.fontWeight": "normal",
22+ "editor.fontFamily": "'Droid Sans Mono', 'monospace', monospace",
23+ "editor.fontSize": 14,
24+ "editor.wordWrap": "on",
25+ "editor.quickSuggestions": {
26+ "comments": "off",
27+ "strings": "off",
28+ "other": "off"
29+ }
30+ },
31+ "markdown.preview.fontSize": 14,
32+ // "workbench.editorAssociations": {
33+ // "*.md": "vscode.markdown.preview.editor"
34+ // },
35+ "markdown.marp.html": "all",
36+ "[dockerfile]": {
37+ "editor.fontSize": 14
38+ },
39+ "[dockercompose]": {
40+ "editor.fontSize": 14
41+ },
42+ "[json]": {
43+ "editor.fontSize": 14
44+ },
45+ "[yaml]": {
46+ "editor.fontSize": 14
47+ },
48+ "[go]": {
49+ "editor.fontSize": 14,
50+ "editor.codeActionsOnSave": {
51+ "source.organizeImports": "explicit"
52+ }
53+ },
54+ "go.lintTool": "golangci-lint",
55+ "go.lintOnSave": "package",
56+ "go.formatTool": "goimports",
57+ "go.useLanguageServer": true,
58+ "gopls": {
59+ "ui.semanticTokens": true,
60+ "ui.completion.usePlaceholders": true
61+ },
62+ "issuespec.groupBy": "milestone"
63+}
\ No newline at end of file
new file mode 100644
@@ -0,0 +1,63 @@
1+{
2+ "workbench.iconTheme": "material-icon-theme",
3+ "workbench.colorTheme": "Light Modern",
4+ "editor.fontSize": 14,
5+ "terminal.integrated.fontSize": 16,
6+ "editor.insertSpaces": true,
7+ "editor.tabSize": 4,
8+ "editor.detectIndentation": true,
9+ "files.autoSave": "afterDelay",
10+ "files.autoSaveDelay": 1000,
11+ // "editor.defaultFormatter": "esbenp.prettier-vscode",
12+ "editor.formatOnSave": true,
13+ "workbench.tree.indent": 20,
14+ //"workbench.activityBar.location": "top",
15+ "workbench.editor.showTabs": "multiple",
16+ "window.zoomLevel": 0.0,
17+ "[markdown]": {
18+ "editor.unicodeHighlight.ambiguousCharacters": false,
19+ "editor.unicodeHighlight.invisibleCharacters": false,
20+ "diffEditor.ignoreTrimWhitespace": false,
21+ "editor.fontWeight": "normal",
22+ "editor.fontFamily": "'Droid Sans Mono', 'monospace', monospace",
23+ "editor.fontSize": 14,
24+ "editor.wordWrap": "on",
25+ "editor.quickSuggestions": {
26+ "comments": "off",
27+ "strings": "off",
28+ "other": "off"
29+ }
30+ },
31+ "markdown.preview.fontSize": 14,
32+ // "workbench.editorAssociations": {
33+ // "*.md": "vscode.markdown.preview.editor"
34+ // },
35+ "markdown.marp.html": "all",
36+ "[dockerfile]": {
37+ "editor.fontSize": 14
38+ },
39+ "[dockercompose]": {
40+ "editor.fontSize": 14
41+ },
42+ "[json]": {
43+ "editor.fontSize": 14
44+ },
45+ "[yaml]": {
46+ "editor.fontSize": 14
47+ },
48+ "[go]": {
49+ "editor.fontSize": 14,
50+ "editor.codeActionsOnSave": {
51+ "source.organizeImports": "explicit"
52+ }
53+ },
54+ "go.lintTool": "golangci-lint",
55+ "go.lintOnSave": "package",
56+ "go.formatTool": "goimports",
57+ "go.useLanguageServer": true,
58+ "gopls": {
59+ "ui.semanticTokens": true,
60+ "ui.completion.usePlaceholders": true
61+ },
62+ "issuespec.groupBy": "milestone"
63+}
\ No newline at end of file\ No newline at end of file
added README.md +22 -0
new file mode 100644
@@ -0,0 +1,22 @@
1+# Mini Me Editor demo
2+
3+
4+```bash
5+mkdir demo
6+cd demo
7+```
8+
9+```bash
10+git clone --depth 1 --filter=blob:none --sparse https://git.rickub.com/bots-garden/squadron.git /tmp/squadron &&
11+git -C /tmp/squadron sparse-checkout set little-coder &&
12+cp -a /tmp/squadron/little-coder/. . &&
13+rm -rf /tmp/squadron
14+```
15+
16+
17+```bash
18+sbx run -d --name mm-little-coder -p 8080:3000 docker.io/k33g/mini-me-editor-kit:0.0.2 \
19+--kit docker.io/k33g/go-toolchain-kit:0.0.2
20+```
21+
22+open the editor on: http://localhost:8080
new file mode 100644
@@ -0,0 +1,22 @@
1+# Mini Me Editor demo
2+
3+
4+```bash
5+mkdir demo
6+cd demo
7+```
8+
9+```bash
10+git clone --depth 1 --filter=blob:none --sparse https://git.rickub.com/bots-garden/squadron.git /tmp/squadron &&
11+git -C /tmp/squadron sparse-checkout set little-coder &&
12+cp -a /tmp/squadron/little-coder/. . &&
13+rm -rf /tmp/squadron
14+```
15+
16+
17+```bash
18+sbx run -d --name mm-little-coder -p 8080:3000 docker.io/k33g/mini-me-editor-kit:0.0.2 \
19+--kit docker.io/k33g/go-toolchain-kit:0.0.2
20+```
21+
22+open the editor on: http://localhost:8080
added demo/.gitignore +7 -0
new file mode 100644
@@ -0,0 +1,7 @@
1+# Apps generated by the agent: one directory each, next to skills/.
2+/*/
3+!/skills/
4+# Files a small model sometimes writes here instead of inside the app.
5+/*.go
6+/*.html
7+/go.mod
new file mode 100644
@@ -0,0 +1,7 @@
1+# Apps generated by the agent: one directory each, next to skills/.
2+/*/
3+!/skills/
4+# Files a small model sometimes writes here instead of inside the app.
5+/*.go
6+/*.html
7+/go.mod
added demo/AGENTS.md +19 -0
new file mode 100644
@@ -0,0 +1,19 @@
1+# Little Coder workspace — rules for generated apps
2+
3+## Stack
4+- Go, standard library only: `net/http`, `html/template`, `embed`, `log/slog`. No framework, no router library, no database driver unless asked.
5+- htmx 2 from the CDN: `<script src="https://unpkg.com/htmx.org@2.0.4"></script>`.
6+- Pico CSS for a responsive page with no CSS to write: `<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">`, plus `<meta name="viewport" content="width=device-width, initial-scale=1">`.
7+- Data in memory (a slice or map behind a `sync.Mutex`) unless the user asks for storage.
8+
9+## Modern Go
10+- Routing with method and path patterns: `mux.HandleFunc("POST /items/{id}/toggle", h)`, read with `r.PathValue("id")`.
11+- Templates and static files embedded with `//go:embed`.
12+- `http.Server` with `ReadHeaderTimeout`; graceful shutdown with `signal.NotifyContext`.
13+- Every error checked; `slog` for logs; port from the `PORT` environment variable, default `8080`.
14+
15+## Layout of an app
16+Each app is a NEW directory here, named in kebab-case: `go.mod`, `main.go`, `app.go` (texts and fields), `appinfo.go`, `store.go`, `handlers.go`, `templates.go`, `handlers_test.go`, `templates/index.html`, `check.sh`, `README.md`. A new app starts from the tested starter of the `go-htmx-app` skill.
17+
18+## Done means
19+`sh check.sh` in the app's directory ends with `CHECK OK`: gofmt, go vet and go test pass, and the running app answered `curl` on `/` and on an htmx route.
new file mode 100644
@@ -0,0 +1,19 @@
1+# Little Coder workspace — rules for generated apps
2+
3+## Stack
4+- Go, standard library only: `net/http`, `html/template`, `embed`, `log/slog`. No framework, no router library, no database driver unless asked.
5+- htmx 2 from the CDN: `<script src="https://unpkg.com/htmx.org@2.0.4"></script>`.
6+- Pico CSS for a responsive page with no CSS to write: `<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">`, plus `<meta name="viewport" content="width=device-width, initial-scale=1">`.
7+- Data in memory (a slice or map behind a `sync.Mutex`) unless the user asks for storage.
8+
9+## Modern Go
10+- Routing with method and path patterns: `mux.HandleFunc("POST /items/{id}/toggle", h)`, read with `r.PathValue("id")`.
11+- Templates and static files embedded with `//go:embed`.
12+- `http.Server` with `ReadHeaderTimeout`; graceful shutdown with `signal.NotifyContext`.
13+- Every error checked; `slog` for logs; port from the `PORT` environment variable, default `8080`.
14+
15+## Layout of an app
16+Each app is a NEW directory here, named in kebab-case: `go.mod`, `main.go`, `app.go` (texts and fields), `appinfo.go`, `store.go`, `handlers.go`, `templates.go`, `handlers_test.go`, `templates/index.html`, `check.sh`, `README.md`. A new app starts from the tested starter of the `go-htmx-app` skill.
17+
18+## Done means
19+`sh check.sh` in the app's directory ends with `CHECK OK`: gofmt, go vet and go test pass, and the running app answered `curl` on `/` and on an htmx route.
added demo/README.agent.md +96 -0
new file mode 100644
@@ -0,0 +1,96 @@
1+# Little Coder — a Go + htmx agent that fits on a MacBook Air
2+
3+An `mm` agent specialised in **modern Go + htmx** web applications, built for a small local model: **JetBrains Mellum2 12B-A2.5B** (Q4_K_M) served by **llmman**, on a machine with **32 GB of RAM**.
4+
5+Ask it *"Generate a sample application in modern Golang, responsive web and HTMX"* (or the same in French): it creates the application, checks it (gofmt, vet, tests, start-up, curl requests) and tells you how to run it.
6+
7+| | |
8+|---|---|
9+| Model | `hf.co/jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M` — MoE, 12.1B parameters, ~2.5B active per token, no reasoning |
10+| Server | llmman (OpenAI API under `/v1`) |
11+| Tools | `bash`, `read_file` / `write_file` / `edit_file`, `read_skill` — nothing else |
12+| Skills | `go-htmx-app`, `go-htmx-subject-app`, `run-and-check`, `htmx-patterns` |
13+| Project rules | `AGENTS.md` (stack, modern Go, definition of done) |
14+
15+## Prerequisites
16+
17+- **llmman** and the model:
18+
19+ ```bash
20+ llmman pull hf.co/jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M
21+ ```
22+
23+### Copie the content of the `little-coder` directory from the repository to the current directory
24+
25+```bash
26+git clone --depth 1 --filter=blob:none --sparse https://git.rickub.com/bots-garden/squadron.git /tmp/squadron &&
27+git -C /tmp/squadron sparse-checkout set little-coder &&
28+cp -a /tmp/squadron/little-coder/. . &&
29+rm -rf /tmp/squadron
30+```
31+
32+## Run
33+
34+**From this directory**: the skills (`skills/`) and `AGENTS.md` are read from the directory `mm` starts in, and each generated application is created there, in its own sub-directory.
35+
36+```bash
37+cd demos/little-coder
38+RELEASE=v0.0.1
39+REPOSITORY=git+https://git.rickub.com/bots-garden/mini-me-kit.git
40+sbx run --name mm-demo-remote "${REPOSITORY}#ref=${RELEASE}&dir=mini-me"
41+```
42+
43+## Examples
44+
45+### 1. A sample application
46+
47+```text
48+Generate a sample application in modern Golang, responsive web and HTMX
49+```
50+
51+### 2. An application about a given subject
52+
53+```text
54+Create a small Go and HTMX web application to manage my library: books with a title and an author, mark a book as read, delete, search.
55+```
56+
57+### 3. In a conversation
58+
59+```
60+> Generate a sample Go + htmx application
61+> Check htmx-tasks
62+> What is the difference between hx-swap="outerHTML" and "innerHTML"?
63+> How do I refresh a counter after every add with htmx?
64+> In htmx-tasks, add a counter of remaining tasks ← unreliable, see Limits
65+```
66+
67+## Notable settings (`agent.little-coder.yaml`)
68+
69+| Key | Value | Why |
70+|---|---|---|
71+| `contextWindow` | 32768 | Matches `LLMMAN_CONTEXT_LENGTH`; if llmman serves more, mm simply compresses earlier. |
72+| `context.threshold` | 60 | Compression around ~20k tokens, before the model loses track. |
73+| `sampling.temperature` | 0.2 | Precise without being greedy (0.0 makes small models loop on long tool calls). |
74+| `sampling.max_tokens` | 8192 | One whole file per `write_file`, without letting a runaway reply fill the window. |
75+| `maxOutput` | 8000 | `go test` output fits without flooding a 32k window. |
76+| `bashTimeout` / `watchdogTimeout` | 2m | A first `go build` of `net/http` on an Air. |
77+| `agentsMd.maxChars` | 4000 | `AGENTS.md` goes out with every request: a guard rail if it grows. |
78+
79+## Limits
80+
81+- **Changing an existing application is not reliable with this model.** Out of 3 attempts at adding a counter: once the files were written outside the application (without the `htmx-tasks/` prefix) with an answer claiming success, once 40 turns without a change, once a broken build. `htmx-patterns` does hold the exact pattern (verified). A skill dedicated to changes (`go-htmx-change`) was tried then removed: it did no better, and its mere presence in the catalogue made one generation in three fail and drew htmx questions to it. 2.5B active parameters are not enough to chain several code files without a mistake. For this work: always check with `sh check.sh` yourself, go one small step per message, or switch to a bigger model (for instance the configuration in `../mini-mes/gopher/`).
82+- The starter covers a **list of items** (main text + text fields + a checkbox), which covers many sample applications. Several entities, relations or persistence require changing the code — same limit as above.
83+- Extra fields are free text, not validated; data is in memory, lost when the server stops.
84+- Run it from this directory (relative paths to the skills and the starter). The `.gitignore` here ignores generated applications and the files a model might write at the root by mistake.
85+
86+## Files
87+
88+| File | Role |
89+|---|---|
90+| `agent.little-coder.yaml` | The agent's configuration, commented. |
91+| `AGENTS.md` | The workspace rules, appended to the system prompt. |
92+| `README.md` | This document in French. |
93+| `skills/go-htmx-app/` | Create a sample application; holds the `starter/`. |
94+| `skills/go-htmx-subject-app/` | Create an application about a given subject (adapt `app.go`). |
95+| `skills/run-and-check/` | Check, run, diagnose and fix. |
96+| `skills/htmx-patterns/` | htmx 2 reference, HTML side and Go handler side. |
new file mode 100644
@@ -0,0 +1,96 @@
1+# Little Coder — a Go + htmx agent that fits on a MacBook Air
2+
3+An `mm` agent specialised in **modern Go + htmx** web applications, built for a small local model: **JetBrains Mellum2 12B-A2.5B** (Q4_K_M) served by **llmman**, on a machine with **32 GB of RAM**.
4+
5+Ask it *"Generate a sample application in modern Golang, responsive web and HTMX"* (or the same in French): it creates the application, checks it (gofmt, vet, tests, start-up, curl requests) and tells you how to run it.
6+
7+| | |
8+|---|---|
9+| Model | `hf.co/jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M` — MoE, 12.1B parameters, ~2.5B active per token, no reasoning |
10+| Server | llmman (OpenAI API under `/v1`) |
11+| Tools | `bash`, `read_file` / `write_file` / `edit_file`, `read_skill` — nothing else |
12+| Skills | `go-htmx-app`, `go-htmx-subject-app`, `run-and-check`, `htmx-patterns` |
13+| Project rules | `AGENTS.md` (stack, modern Go, definition of done) |
14+
15+## Prerequisites
16+
17+- **llmman** and the model:
18+
19+ ```bash
20+ llmman pull hf.co/jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M
21+ ```
22+
23+### Copie the content of the `little-coder` directory from the repository to the current directory
24+
25+```bash
26+git clone --depth 1 --filter=blob:none --sparse https://git.rickub.com/bots-garden/squadron.git /tmp/squadron &&
27+git -C /tmp/squadron sparse-checkout set little-coder &&
28+cp -a /tmp/squadron/little-coder/. . &&
29+rm -rf /tmp/squadron
30+```
31+
32+## Run
33+
34+**From this directory**: the skills (`skills/`) and `AGENTS.md` are read from the directory `mm` starts in, and each generated application is created there, in its own sub-directory.
35+
36+```bash
37+cd demos/little-coder
38+RELEASE=v0.0.1
39+REPOSITORY=git+https://git.rickub.com/bots-garden/mini-me-kit.git
40+sbx run --name mm-demo-remote "${REPOSITORY}#ref=${RELEASE}&dir=mini-me"
41+```
42+
43+## Examples
44+
45+### 1. A sample application
46+
47+```text
48+Generate a sample application in modern Golang, responsive web and HTMX
49+```
50+
51+### 2. An application about a given subject
52+
53+```text
54+Create a small Go and HTMX web application to manage my library: books with a title and an author, mark a book as read, delete, search.
55+```
56+
57+### 3. In a conversation
58+
59+```
60+> Generate a sample Go + htmx application
61+> Check htmx-tasks
62+> What is the difference between hx-swap="outerHTML" and "innerHTML"?
63+> How do I refresh a counter after every add with htmx?
64+> In htmx-tasks, add a counter of remaining tasks ← unreliable, see Limits
65+```
66+
67+## Notable settings (`agent.little-coder.yaml`)
68+
69+| Key | Value | Why |
70+|---|---|---|
71+| `contextWindow` | 32768 | Matches `LLMMAN_CONTEXT_LENGTH`; if llmman serves more, mm simply compresses earlier. |
72+| `context.threshold` | 60 | Compression around ~20k tokens, before the model loses track. |
73+| `sampling.temperature` | 0.2 | Precise without being greedy (0.0 makes small models loop on long tool calls). |
74+| `sampling.max_tokens` | 8192 | One whole file per `write_file`, without letting a runaway reply fill the window. |
75+| `maxOutput` | 8000 | `go test` output fits without flooding a 32k window. |
76+| `bashTimeout` / `watchdogTimeout` | 2m | A first `go build` of `net/http` on an Air. |
77+| `agentsMd.maxChars` | 4000 | `AGENTS.md` goes out with every request: a guard rail if it grows. |
78+
79+## Limits
80+
81+- **Changing an existing application is not reliable with this model.** Out of 3 attempts at adding a counter: once the files were written outside the application (without the `htmx-tasks/` prefix) with an answer claiming success, once 40 turns without a change, once a broken build. `htmx-patterns` does hold the exact pattern (verified). A skill dedicated to changes (`go-htmx-change`) was tried then removed: it did no better, and its mere presence in the catalogue made one generation in three fail and drew htmx questions to it. 2.5B active parameters are not enough to chain several code files without a mistake. For this work: always check with `sh check.sh` yourself, go one small step per message, or switch to a bigger model (for instance the configuration in `../mini-mes/gopher/`).
82+- The starter covers a **list of items** (main text + text fields + a checkbox), which covers many sample applications. Several entities, relations or persistence require changing the code — same limit as above.
83+- Extra fields are free text, not validated; data is in memory, lost when the server stops.
84+- Run it from this directory (relative paths to the skills and the starter). The `.gitignore` here ignores generated applications and the files a model might write at the root by mistake.
85+
86+## Files
87+
88+| File | Role |
89+|---|---|
90+| `agent.little-coder.yaml` | The agent's configuration, commented. |
91+| `AGENTS.md` | The workspace rules, appended to the system prompt. |
92+| `README.md` | This document in French. |
93+| `skills/go-htmx-app/` | Create a sample application; holds the `starter/`. |
94+| `skills/go-htmx-subject-app/` | Create an application about a given subject (adapt `app.go`). |
95+| `skills/run-and-check/` | Check, run, diagnose and fix. |
96+| `skills/htmx-patterns/` | htmx 2 reference, HTML side and Go handler side. |
added demo/agent.little-coder.yaml +169 -0
new file mode 100644
@@ -0,0 +1,169 @@
1+# Little Coder — a small Go + htmx web developer that fits on a MacBook Air.
2+#
3+# Model: JetBrains Mellum2 12B-A2.5B Instruct, Q4_K_M, served by llmman.
4+# - a mixture of experts: 12.1B parameters, ~2.5B active per token, so it
5+# answers at the speed of a 2–3B model;
6+# - no reasoning (its chat template has no thinking block): every token
7+# is code or a tool call;
8+# - trained on 8,192 tokens, stretched to 131,072 with YaRN: it is at its
9+# best on SHORT prompts. Everything below is sized for that.
10+#
11+# Memory on a 32 GB MacBook Air (Q4_K_M, f16 KV cache):
12+# weights ≈ 7.3 GB + KV cache ≈ 57 KB per token
13+# → 32k tokens ≈ 1.8 GB → ~9.5 GB in all (recommended)
14+# → 131k tokens ≈ 7.5 GB → ~15 GB in all (llmman's default for this model)
15+# So start llmman with a 32k window, which also leaves room for the browser,
16+# `go build` and the app being tested:
17+#
18+# llmman pull hf.co/jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M
19+# LLMMAN_CONTEXT_LENGTH=32768 llmman serve
20+#
21+# Run it FROM THIS DIRECTORY: the skills (skills/) and the project rules
22+# (AGENTS.md) are read from the directory mm is started in, and the apps it
23+# generates land here, one sub-directory each.
24+#
25+# cd demos/little-coder
26+# ../../mm -config agent.little-coder.yaml
27+#
28+# Every key is optional; reference: docs/en/reference/configuration.md.
29+
30+# ─── Model and server ────────────────────────────────────────────────────────
31+
32+provider: llmman
33+model: hf.co/jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M
34+
35+baseUrl: http://127.0.0.1:17434/v1
36+fallback: http://host.docker.internal:17434/v1
37+
38+# The window mm budgets for, whatever llmman serves: compression starts at
39+# `context.threshold` % of it. 32k matches LLMMAN_CONTEXT_LENGTH above; if
40+# llmman serves more, mm simply compresses earlier — never set it higher than
41+# what llmman serves.
42+contextWindow: 32768
43+
44+# ─── Generation ──────────────────────────────────────────────────────────────
45+
46+sampling:
47+ # Code wants precision. Not 0.0: greedy decoding is what sends a small
48+ # model into repetition loops on long tool calls.
49+ temperature: 0.2
50+ top_p: 0.9
51+
52+ # ONE reply = one tool call. A whole file in one write_file (main.go,
53+ # a template) is 1–3k tokens; 8192 leaves margin without letting a runaway
54+ # reply fill the window.
55+ max_tokens: 8192
56+
57+ # One file, one command at a time.
58+ parallel_tool_calls: false
59+
60+# Mellum does not reason; nothing to show.
61+showThinking: false
62+
63+# ─── Timeouts ────────────────────────────────────────────────────────────────
64+
65+# Loading ~7 GB on the first question, then prompts of 10–20k tokens.
66+firstTokenTimeout: 3m
67+
68+# A first `go build` / `go test` downloads nothing (standard library only)
69+# but compiles net/http: under a minute on an M-series Air.
70+bashTimeout: 2m
71+
72+# Counts while a bash command runs: at least bashTimeout.
73+watchdogTimeout: 2m
74+
75+# ─── Agent loop ──────────────────────────────────────────────────────────────
76+
77+# A sample app is ~6 files + build + test + run + check + fixes.
78+maxTurns: 40
79+
80+# go vet / go test failures are short; 8000 characters keep a whole report
81+# without filling a 32k window with logs.
82+maxOutput: 8000
83+
84+previewLines: 15
85+displayCommands: true
86+
87+# ─── Tools ───────────────────────────────────────────────────────────────────
88+
89+# true: no question before each command or file write — generating an app is
90+# a dozen of them. Set false (or AGENT_YOLO=false) to approve each one.
91+yolo: true
92+
93+bashTool: true
94+editTools: true
95+
96+# Every tool description is sent with every request: fewer tools = a shorter
97+# prompt and fewer wrong picks for a small model. The skills' numbered steps
98+# are the plan; no web access; no gopls (Mellum cannot compose lsp calls
99+# reliably — measured on rename — and gopls costs memory).
100+planTool: false
101+fetchContentTool: false
102+
103+# Four short procedures, relative to the directory mm starts in; the
104+# starter app (and its check.sh) lives in skills/go-htmx-app/starter.
105+skillsDir: skills
106+
107+# The rules of this workspace (Go version, stack, layout), appended to the
108+# system prompt. Kept short: it is sent with every request.
109+agentsMd:
110+ enabled: true
111+ path: AGENTS.md
112+ # Guard rail: a file grown past ~1,000 tokens is skipped, not sent.
113+ maxChars: 4000
114+
115+# ─── Memory ──────────────────────────────────────────────────────────────────
116+
117+sessions:
118+ enabled: true
119+ dir: .mm/sessions
120+
121+context:
122+ enabled: true
123+ # 60 % of 32k ≈ 19.6k tokens: compress before the prompt gets long enough
124+ # for the model to lose track of the files it wrote.
125+ threshold: 60
126+ maxMessages: 60
127+ keepLastTurns: 2
128+ summaryMaxTokens: 1200
129+ showStats: true
130+
131+# ─── Identity ────────────────────────────────────────────────────────────────
132+
133+acp:
134+ name: little-coder
135+ title: Little Coder (Go + htmx)
136+
137+# Short on purpose: measured on this model, a longer prompt costs more in
138+# tool-call mistakes than it gives in guidance. The details live in the
139+# skills, loaded only when needed.
140+system: |
141+ You are Little Coder, a Go and htmx web developer working in a terminal,
142+ in the current directory.
143+
144+ TOOLS
145+ - `read_skill` loads a procedure. Any request to create a web app, add a
146+ page or an htmx interaction, or run and check an app has one: call
147+ `read_skill` FIRST, then follow its steps in order.
148+ - `write_file` creates a file: ONE complete file per call.
149+ - `read_file` then `edit_file` change an existing file; `old` is copied
150+ exactly from what read_file returned.
151+ - `bash` runs commands: go mod, gofmt, go vet, go build, go test,
152+ curl. Each call is a new shell: start with `cd <dir> &&`.
153+
154+ PATHS
155+ The current directory never changes: a `cd` lasts one bash command. File
156+ paths always start with the app's directory: `htmx-tasks/main.go`, never
157+ `main.go`.
158+
159+ RULES
160+ - Facts come from tools: never say the code builds, the tests pass or a
161+ page works without the command output in this answer.
162+ - When a command fails, read the error, fix the file it names, run the
163+ command again. Never delete a file to make an error go away.
164+ - Never start a server with `go run .` in bash: it never returns. An
165+ app's `check.sh` starts, queries and stops it.
166+ - Write files with tools, never as code blocks in your answer.
167+ - Answer in the language the user wrote in — French if they wrote French,
168+ English if they wrote English — briefly: the files created, the commands
169+ run and their result, how to start the app.
new file mode 100644
@@ -0,0 +1,169 @@
1+# Little Coder — a small Go + htmx web developer that fits on a MacBook Air.
2+#
3+# Model: JetBrains Mellum2 12B-A2.5B Instruct, Q4_K_M, served by llmman.
4+# - a mixture of experts: 12.1B parameters, ~2.5B active per token, so it
5+# answers at the speed of a 2–3B model;
6+# - no reasoning (its chat template has no thinking block): every token
7+# is code or a tool call;
8+# - trained on 8,192 tokens, stretched to 131,072 with YaRN: it is at its
9+# best on SHORT prompts. Everything below is sized for that.
10+#
11+# Memory on a 32 GB MacBook Air (Q4_K_M, f16 KV cache):
12+# weights ≈ 7.3 GB + KV cache ≈ 57 KB per token
13+# → 32k tokens ≈ 1.8 GB → ~9.5 GB in all (recommended)
14+# → 131k tokens ≈ 7.5 GB → ~15 GB in all (llmman's default for this model)
15+# So start llmman with a 32k window, which also leaves room for the browser,
16+# `go build` and the app being tested:
17+#
18+# llmman pull hf.co/jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M
19+# LLMMAN_CONTEXT_LENGTH=32768 llmman serve
20+#
21+# Run it FROM THIS DIRECTORY: the skills (skills/) and the project rules
22+# (AGENTS.md) are read from the directory mm is started in, and the apps it
23+# generates land here, one sub-directory each.
24+#
25+# cd demos/little-coder
26+# ../../mm -config agent.little-coder.yaml
27+#
28+# Every key is optional; reference: docs/en/reference/configuration.md.
29+
30+# ─── Model and server ────────────────────────────────────────────────────────
31+
32+provider: llmman
33+model: hf.co/jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M
34+
35+baseUrl: http://127.0.0.1:17434/v1
36+fallback: http://host.docker.internal:17434/v1
37+
38+# The window mm budgets for, whatever llmman serves: compression starts at
39+# `context.threshold` % of it. 32k matches LLMMAN_CONTEXT_LENGTH above; if
40+# llmman serves more, mm simply compresses earlier — never set it higher than
41+# what llmman serves.
42+contextWindow: 32768
43+
44+# ─── Generation ──────────────────────────────────────────────────────────────
45+
46+sampling:
47+ # Code wants precision. Not 0.0: greedy decoding is what sends a small
48+ # model into repetition loops on long tool calls.
49+ temperature: 0.2
50+ top_p: 0.9
51+
52+ # ONE reply = one tool call. A whole file in one write_file (main.go,
53+ # a template) is 1–3k tokens; 8192 leaves margin without letting a runaway
54+ # reply fill the window.
55+ max_tokens: 8192
56+
57+ # One file, one command at a time.
58+ parallel_tool_calls: false
59+
60+# Mellum does not reason; nothing to show.
61+showThinking: false
62+
63+# ─── Timeouts ────────────────────────────────────────────────────────────────
64+
65+# Loading ~7 GB on the first question, then prompts of 10–20k tokens.
66+firstTokenTimeout: 3m
67+
68+# A first `go build` / `go test` downloads nothing (standard library only)
69+# but compiles net/http: under a minute on an M-series Air.
70+bashTimeout: 2m
71+
72+# Counts while a bash command runs: at least bashTimeout.
73+watchdogTimeout: 2m
74+
75+# ─── Agent loop ──────────────────────────────────────────────────────────────
76+
77+# A sample app is ~6 files + build + test + run + check + fixes.
78+maxTurns: 40
79+
80+# go vet / go test failures are short; 8000 characters keep a whole report
81+# without filling a 32k window with logs.
82+maxOutput: 8000
83+
84+previewLines: 15
85+displayCommands: true
86+
87+# ─── Tools ───────────────────────────────────────────────────────────────────
88+
89+# true: no question before each command or file write — generating an app is
90+# a dozen of them. Set false (or AGENT_YOLO=false) to approve each one.
91+yolo: true
92+
93+bashTool: true
94+editTools: true
95+
96+# Every tool description is sent with every request: fewer tools = a shorter
97+# prompt and fewer wrong picks for a small model. The skills' numbered steps
98+# are the plan; no web access; no gopls (Mellum cannot compose lsp calls
99+# reliably — measured on rename — and gopls costs memory).
100+planTool: false
101+fetchContentTool: false
102+
103+# Four short procedures, relative to the directory mm starts in; the
104+# starter app (and its check.sh) lives in skills/go-htmx-app/starter.
105+skillsDir: skills
106+
107+# The rules of this workspace (Go version, stack, layout), appended to the
108+# system prompt. Kept short: it is sent with every request.
109+agentsMd:
110+ enabled: true
111+ path: AGENTS.md
112+ # Guard rail: a file grown past ~1,000 tokens is skipped, not sent.
113+ maxChars: 4000
114+
115+# ─── Memory ──────────────────────────────────────────────────────────────────
116+
117+sessions:
118+ enabled: true
119+ dir: .mm/sessions
120+
121+context:
122+ enabled: true
123+ # 60 % of 32k ≈ 19.6k tokens: compress before the prompt gets long enough
124+ # for the model to lose track of the files it wrote.
125+ threshold: 60
126+ maxMessages: 60
127+ keepLastTurns: 2
128+ summaryMaxTokens: 1200
129+ showStats: true
130+
131+# ─── Identity ────────────────────────────────────────────────────────────────
132+
133+acp:
134+ name: little-coder
135+ title: Little Coder (Go + htmx)
136+
137+# Short on purpose: measured on this model, a longer prompt costs more in
138+# tool-call mistakes than it gives in guidance. The details live in the
139+# skills, loaded only when needed.
140+system: |
141+ You are Little Coder, a Go and htmx web developer working in a terminal,
142+ in the current directory.
143+
144+ TOOLS
145+ - `read_skill` loads a procedure. Any request to create a web app, add a
146+ page or an htmx interaction, or run and check an app has one: call
147+ `read_skill` FIRST, then follow its steps in order.
148+ - `write_file` creates a file: ONE complete file per call.
149+ - `read_file` then `edit_file` change an existing file; `old` is copied
150+ exactly from what read_file returned.
151+ - `bash` runs commands: go mod, gofmt, go vet, go build, go test,
152+ curl. Each call is a new shell: start with `cd <dir> &&`.
153+
154+ PATHS
155+ The current directory never changes: a `cd` lasts one bash command. File
156+ paths always start with the app's directory: `htmx-tasks/main.go`, never
157+ `main.go`.
158+
159+ RULES
160+ - Facts come from tools: never say the code builds, the tests pass or a
161+ page works without the command output in this answer.
162+ - When a command fails, read the error, fix the file it names, run the
163+ command again. Never delete a file to make an error go away.
164+ - Never start a server with `go run .` in bash: it never returns. An
165+ app's `check.sh` starts, queries and stops it.
166+ - Write files with tools, never as code blocks in your answer.
167+ - Answer in the language the user wrote in — French if they wrote French,
168+ English if they wrote English — briefly: the files created, the commands
169+ run and their result, how to start the app.
added demo/skills/go-htmx-app/SKILL.md +37 -0
new file mode 100644
@@ -0,0 +1,37 @@
1+---
2+name: go-htmx-app
3+description: Create a sample (demo, example) web application in modern Go with htmx and a responsive page, when the user gives no particular subject — or asks for a todo / task list.
4+keywords: exemple, démo, sample, demo, todo, tâches, génère, application
5+---
6+
7+# Create a sample Go + htmx web app
8+
9+Every file path starts with the app directory: `<name>/README.md` — never `README.md` alone.
10+
11+## 1. Name
12+
13+From the request, kebab-case. Nothing given: `htmx-tasks`.
14+
15+## 2. Copy the starter
16+
17+The starter is a COMPLETE app, already tested: a task list with add, mark done, delete, live search, a responsive page, graceful shutdown, a table-driven test of every route, and `check.sh`. Its texts and fields are in `app.go`. It IS the app: do not read or change its files. Call your `bash` tool with this command:
18+
19+ mkdir -p <name> && cp -rn skills/go-htmx-app/starter/. <name>/ && cd <name> && go mod edit -module <name> && ls -R
20+
21+## 3. Check
22+
23+Call your `bash` tool with this command:
24+
25+ cd <name> && sh check.sh
26+
27+It formats, vets, tests, starts the app, queries it and stops it. It ends with `CHECK OK`. Anything else: read the `run-and-check` skill.
28+
29+## 4. README
30+
31+The starter has an English `<name>/README.md`. Request not in English: one `write_file` call on `<name>/README.md` with it translated into the user's language: what the app does, the stack (Go standard library, htmx 2, Pico CSS), the routes, how to run it (`go run .` then http://localhost:8080), test it (`go test ./...`), check it (`sh check.sh`), and that `app.go` holds the texts and fields.
32+
33+Routes: `GET /` page · `POST /items` add (fragment `item`) · `GET /items/search?q=` filter (fragment `items`) · `POST /items/{id}/toggle` done/undone (fragment `item`) · `DELETE /items/{id}` remove (empty answer).
34+
35+## 5. Answer
36+
37+A few lines in the user's language — not the README again: the files, the routes, the `CHECK OK` result, and how to start it: `cd <name> && go run .`, then open http://localhost:8080.
new file mode 100644
@@ -0,0 +1,37 @@
1+---
2+name: go-htmx-app
3+description: Create a sample (demo, example) web application in modern Go with htmx and a responsive page, when the user gives no particular subject — or asks for a todo / task list.
4+keywords: exemple, démo, sample, demo, todo, tâches, génère, application
5+---
6+
7+# Create a sample Go + htmx web app
8+
9+Every file path starts with the app directory: `<name>/README.md` — never `README.md` alone.
10+
11+## 1. Name
12+
13+From the request, kebab-case. Nothing given: `htmx-tasks`.
14+
15+## 2. Copy the starter
16+
17+The starter is a COMPLETE app, already tested: a task list with add, mark done, delete, live search, a responsive page, graceful shutdown, a table-driven test of every route, and `check.sh`. Its texts and fields are in `app.go`. It IS the app: do not read or change its files. Call your `bash` tool with this command:
18+
19+ mkdir -p <name> && cp -rn skills/go-htmx-app/starter/. <name>/ && cd <name> && go mod edit -module <name> && ls -R
20+
21+## 3. Check
22+
23+Call your `bash` tool with this command:
24+
25+ cd <name> && sh check.sh
26+
27+It formats, vets, tests, starts the app, queries it and stops it. It ends with `CHECK OK`. Anything else: read the `run-and-check` skill.
28+
29+## 4. README
30+
31+The starter has an English `<name>/README.md`. Request not in English: one `write_file` call on `<name>/README.md` with it translated into the user's language: what the app does, the stack (Go standard library, htmx 2, Pico CSS), the routes, how to run it (`go run .` then http://localhost:8080), test it (`go test ./...`), check it (`sh check.sh`), and that `app.go` holds the texts and fields.
32+
33+Routes: `GET /` page · `POST /items` add (fragment `item`) · `GET /items/search?q=` filter (fragment `items`) · `POST /items/{id}/toggle` done/undone (fragment `item`) · `DELETE /items/{id}` remove (empty answer).
34+
35+## 5. Answer
36+
37+A few lines in the user's language — not the README again: the files, the routes, the `CHECK OK` result, and how to start it: `cd <name> && go run .`, then open http://localhost:8080.
added demo/skills/go-htmx-app/starter/README.md +33 -0
new file mode 100644
@@ -0,0 +1,33 @@
1+# htmx list app
2+
3+A small web application in modern Go with htmx: a list you can add to, tick, delete from and search live, on a responsive page. Every action swaps one HTML fragment — no JSON, no JavaScript to write.
4+
5+## Stack
6+
7+- Go standard library only: `net/http` (method + path routing), `html/template`, `embed`, `log/slog`.
8+- htmx 2 and Pico CSS from a CDN.
9+- Data in memory, behind a `sync.Mutex` (lost when the server stops).
10+
11+## Run
12+
13+ go run . # http://localhost:8080
14+ PORT=9000 go run . # another port
15+
16+## Test and check
17+
18+ go test ./...
19+ sh check.sh # gofmt, vet, tests, then starts the app, queries it with curl, stops it: ends with CHECK OK
20+
21+## Routes
22+
23+| Route | Answer |
24+|---|---|
25+| `GET /` | the page |
26+| `POST /items` | add an item — fragment `item` |
27+| `GET /items/search?q=` | filter — fragment `items` |
28+| `POST /items/{id}/toggle` | done / not done — fragment `item` |
29+| `DELETE /items/{id}` | remove — empty answer, htmx removes the line |
30+
31+## Adapt it
32+
33+`app.go` holds the texts, the extra fields and the start-up data: the page, the handlers and the tests all read it.
new file mode 100644
@@ -0,0 +1,33 @@
1+# htmx list app
2+
3+A small web application in modern Go with htmx: a list you can add to, tick, delete from and search live, on a responsive page. Every action swaps one HTML fragment — no JSON, no JavaScript to write.
4+
5+## Stack
6+
7+- Go standard library only: `net/http` (method + path routing), `html/template`, `embed`, `log/slog`.
8+- htmx 2 and Pico CSS from a CDN.
9+- Data in memory, behind a `sync.Mutex` (lost when the server stops).
10+
11+## Run
12+
13+ go run . # http://localhost:8080
14+ PORT=9000 go run . # another port
15+
16+## Test and check
17+
18+ go test ./...
19+ sh check.sh # gofmt, vet, tests, then starts the app, queries it with curl, stops it: ends with CHECK OK
20+
21+## Routes
22+
23+| Route | Answer |
24+|---|---|
25+| `GET /` | the page |
26+| `POST /items` | add an item — fragment `item` |
27+| `GET /items/search?q=` | filter — fragment `items` |
28+| `POST /items/{id}/toggle` | done / not done — fragment `item` |
29+| `DELETE /items/{id}` | remove — empty answer, htmx removes the line |
30+
31+## Adapt it
32+
33+`app.go` holds the texts, the extra fields and the start-up data: the page, the handlers and the tests all read it.
added demo/skills/go-htmx-app/starter/app.go +19 -0
new file mode 100644
@@ -0,0 +1,19 @@
1+package main
2+
3+// app describes what this list holds. To adapt the app to another subject,
4+// rewrite only this file: the page, the handlers and the tests all read it.
5+// The types are in appinfo.go.
6+var app = appInfo{
7+ Lang: "en",
8+ Title: "htmx tasks",
9+ TitleLabel: "Task",
10+ Fields: []string{},
11+ DoneLabel: "Done",
12+ AddLabel: "Add",
13+ SearchLabel: "Search",
14+ DeleteConfirm: "Delete this task?",
15+ Seeds: []seed{
16+ {Title: "Read the htmx docs"},
17+ {Title: "Write a Go handler"},
18+ },
19+}
new file mode 100644
@@ -0,0 +1,19 @@
1+package main
2+
3+// app describes what this list holds. To adapt the app to another subject,
4+// rewrite only this file: the page, the handlers and the tests all read it.
5+// The types are in appinfo.go.
6+var app = appInfo{
7+ Lang: "en",
8+ Title: "htmx tasks",
9+ TitleLabel: "Task",
10+ Fields: []string{},
11+ DoneLabel: "Done",
12+ AddLabel: "Add",
13+ SearchLabel: "Search",
14+ DeleteConfirm: "Delete this task?",
15+ Seeds: []seed{
16+ {Title: "Read the htmx docs"},
17+ {Title: "Write a Go handler"},
18+ },
19+}
added demo/skills/go-htmx-app/starter/appinfo.go +20 -0
new file mode 100644
@@ -0,0 +1,20 @@
1+package main
2+
3+// appInfo holds the texts of the page and the shape of an item.
4+type appInfo struct {
5+ Lang string // html lang attribute: "en", "fr"…
6+ Title string // page title and heading
7+ TitleLabel string // label of the main text of an item
8+ Fields []string // labels of the extra text fields, e.g. {"Author", "Year"}
9+ DoneLabel string // label of the done checkbox: "Done", "Read"…; "" = no checkbox
10+ AddLabel string // text of the add button
11+ SearchLabel string // placeholder of the search box
12+ DeleteConfirm string // question asked before a delete
13+ Seeds []seed // items present at start-up
14+}
15+
16+// seed is an item present at start-up: one value per entry of Fields.
17+type seed struct {
18+ Title string
19+ Fields []string
20+}
new file mode 100644
@@ -0,0 +1,20 @@
1+package main
2+
3+// appInfo holds the texts of the page and the shape of an item.
4+type appInfo struct {
5+ Lang string // html lang attribute: "en", "fr"…
6+ Title string // page title and heading
7+ TitleLabel string // label of the main text of an item
8+ Fields []string // labels of the extra text fields, e.g. {"Author", "Year"}
9+ DoneLabel string // label of the done checkbox: "Done", "Read"…; "" = no checkbox
10+ AddLabel string // text of the add button
11+ SearchLabel string // placeholder of the search box
12+ DeleteConfirm string // question asked before a delete
13+ Seeds []seed // items present at start-up
14+}
15+
16+// seed is an item present at start-up: one value per entry of Fields.
17+type seed struct {
18+ Title string
19+ Fields []string
20+}
added demo/skills/go-htmx-app/starter/check.sh +47 -0
new file mode 100644
@@ -0,0 +1,47 @@
1+#!/bin/sh
2+# check.sh — format (gofmt -w), vet, test, then build the app, start it in the
3+# background, query it like a browser and like htmx, and stop it.
4+# Usage: sh check.sh [port] (default port 8099)
5+# Prints "CHECK OK" at the end when everything passed.
6+port="${1:-8099}"
7+bin="$(mktemp -d)/app"
8+
9+echo "== gofmt"
10+unformatted="$(gofmt -l .)"
11+if [ -n "$unformatted" ]; then
12+ gofmt -w . && echo "formatted: $unformatted"
13+fi
14+echo "== go vet"
15+go vet ./... || exit 1
16+echo "== go test"
17+go test ./... 2>&1 | tail -30
18+go test ./... >/dev/null 2>&1 || exit 1
19+echo "== go build"
20+go build -o "$bin" . || exit 1
21+
22+echo "== run on port $port"
23+if curl -s -o /dev/null "localhost:$port/"; then
24+ echo "CHECK FAILED: port $port is already used by another server — run: sh check.sh 8098"; exit 1
25+fi
26+PORT="$port" "$bin" > "$bin.log" 2>&1 &
27+app=$!
28+sleep 2
29+if ! kill -0 "$app" 2>/dev/null; then
30+ echo "CHECK FAILED: the app stopped at start-up:"; cat "$bin.log"; exit 1
31+fi
32+page="$(curl -s -o /dev/null -w '%{http_code}' "localhost:$port/")"
33+title="$(curl -s "localhost:$port/" | sed -n 's:.*<title>\(.*\)</title>.*:\1:p')"
34+echo "GET / -> $page, page title: $title"
35+echo "POST /items (htmx) ->"
36+fragment="$(curl -s -X POST -H 'HX-Request: true' -d 'title=check' "localhost:$port/items")"
37+echo "$fragment" | head -5
38+kill "$app" 2>/dev/null
39+wait "$app" 2>/dev/null
40+echo "== server log"
41+cat "$bin.log"
42+
43+[ "$page" = "200" ] || { echo "CHECK FAILED: GET / answered $page"; exit 1; }
44+case "$fragment" in
45+ *"<li"*) echo "CHECK OK" ;;
46+ *) echo "CHECK FAILED: POST /items did not answer an <li> fragment"; exit 1 ;;
47+esac
new file mode 100644
@@ -0,0 +1,47 @@
1+#!/bin/sh
2+# check.sh — format (gofmt -w), vet, test, then build the app, start it in the
3+# background, query it like a browser and like htmx, and stop it.
4+# Usage: sh check.sh [port] (default port 8099)
5+# Prints "CHECK OK" at the end when everything passed.
6+port="${1:-8099}"
7+bin="$(mktemp -d)/app"
8+
9+echo "== gofmt"
10+unformatted="$(gofmt -l .)"
11+if [ -n "$unformatted" ]; then
12+ gofmt -w . && echo "formatted: $unformatted"
13+fi
14+echo "== go vet"
15+go vet ./... || exit 1
16+echo "== go test"
17+go test ./... 2>&1 | tail -30
18+go test ./... >/dev/null 2>&1 || exit 1
19+echo "== go build"
20+go build -o "$bin" . || exit 1
21+
22+echo "== run on port $port"
23+if curl -s -o /dev/null "localhost:$port/"; then
24+ echo "CHECK FAILED: port $port is already used by another server — run: sh check.sh 8098"; exit 1
25+fi
26+PORT="$port" "$bin" > "$bin.log" 2>&1 &
27+app=$!
28+sleep 2
29+if ! kill -0 "$app" 2>/dev/null; then
30+ echo "CHECK FAILED: the app stopped at start-up:"; cat "$bin.log"; exit 1
31+fi
32+page="$(curl -s -o /dev/null -w '%{http_code}' "localhost:$port/")"
33+title="$(curl -s "localhost:$port/" | sed -n 's:.*<title>\(.*\)</title>.*:\1:p')"
34+echo "GET / -> $page, page title: $title"
35+echo "POST /items (htmx) ->"
36+fragment="$(curl -s -X POST -H 'HX-Request: true' -d 'title=check' "localhost:$port/items")"
37+echo "$fragment" | head -5
38+kill "$app" 2>/dev/null
39+wait "$app" 2>/dev/null
40+echo "== server log"
41+cat "$bin.log"
42+
43+[ "$page" = "200" ] || { echo "CHECK FAILED: GET / answered $page"; exit 1; }
44+case "$fragment" in
45+ *"<li"*) echo "CHECK OK" ;;
46+ *) echo "CHECK FAILED: POST /items did not answer an <li> fragment"; exit 1 ;;
47+esac
added demo/skills/go-htmx-app/starter/go.mod +3 -0
new file mode 100644
@@ -0,0 +1,3 @@
1+module htmx-app
2+
3+go 1.22
new file mode 100644
@@ -0,0 +1,3 @@
1+module htmx-app
2+
3+go 1.22
added demo/skills/go-htmx-app/starter/handlers.go +81 -0
new file mode 100644
@@ -0,0 +1,81 @@
1+package main
2+
3+import (
4+ "html/template"
5+ "log/slog"
6+ "net/http"
7+ "strconv"
8+ "strings"
9+)
10+
11+type handlers struct {
12+ store *store
13+ tmpl *template.Template
14+}
15+
16+func (h *handlers) routes() http.Handler {
17+ mux := http.NewServeMux()
18+ mux.HandleFunc("GET /{$}", h.index)
19+ mux.HandleFunc("POST /items", h.create)
20+ mux.HandleFunc("GET /items/search", h.search)
21+ mux.HandleFunc("POST /items/{id}/toggle", h.toggle)
22+ mux.HandleFunc("DELETE /items/{id}", h.delete)
23+ return mux
24+}
25+
26+// render writes a template: "index.html" for the page, "items" or "item" for htmx fragments.
27+func (h *handlers) render(w http.ResponseWriter, name string, data any) {
28+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
29+ if err := h.tmpl.ExecuteTemplate(w, name, data); err != nil {
30+ slog.Error("render", "template", name, "err", err)
31+ }
32+}
33+
34+func (h *handlers) index(w http.ResponseWriter, r *http.Request) {
35+ h.render(w, "index.html", map[string]any{"Items": h.store.List()})
36+}
37+
38+func (h *handlers) create(w http.ResponseWriter, r *http.Request) {
39+ title := strings.TrimSpace(r.FormValue("title"))
40+ if title == "" {
41+ http.Error(w, "title is required", http.StatusBadRequest)
42+ return
43+ }
44+ fields := make([]string, len(app.Fields))
45+ for i := range app.Fields {
46+ fields[i] = strings.TrimSpace(r.FormValue("field" + strconv.Itoa(i)))
47+ }
48+ h.render(w, "item", h.store.Add(title, fields))
49+}
50+
51+func (h *handlers) search(w http.ResponseWriter, r *http.Request) {
52+ h.render(w, "items", h.store.Search(r.URL.Query().Get("q")))
53+}
54+
55+func (h *handlers) toggle(w http.ResponseWriter, r *http.Request) {
56+ id, err := strconv.Atoi(r.PathValue("id"))
57+ if err != nil {
58+ http.Error(w, "bad id", http.StatusBadRequest)
59+ return
60+ }
61+ it, ok := h.store.Toggle(id)
62+ if !ok {
63+ http.NotFound(w, r)
64+ return
65+ }
66+ h.render(w, "item", it)
67+}
68+
69+// delete answers 200 with an empty body: htmx swaps the <li> for nothing.
70+func (h *handlers) delete(w http.ResponseWriter, r *http.Request) {
71+ id, err := strconv.Atoi(r.PathValue("id"))
72+ if err != nil {
73+ http.Error(w, "bad id", http.StatusBadRequest)
74+ return
75+ }
76+ if !h.store.Delete(id) {
77+ http.NotFound(w, r)
78+ return
79+ }
80+ w.WriteHeader(http.StatusOK)
81+}
new file mode 100644
@@ -0,0 +1,81 @@
1+package main
2+
3+import (
4+ "html/template"
5+ "log/slog"
6+ "net/http"
7+ "strconv"
8+ "strings"
9+)
10+
11+type handlers struct {
12+ store *store
13+ tmpl *template.Template
14+}
15+
16+func (h *handlers) routes() http.Handler {
17+ mux := http.NewServeMux()
18+ mux.HandleFunc("GET /{$}", h.index)
19+ mux.HandleFunc("POST /items", h.create)
20+ mux.HandleFunc("GET /items/search", h.search)
21+ mux.HandleFunc("POST /items/{id}/toggle", h.toggle)
22+ mux.HandleFunc("DELETE /items/{id}", h.delete)
23+ return mux
24+}
25+
26+// render writes a template: "index.html" for the page, "items" or "item" for htmx fragments.
27+func (h *handlers) render(w http.ResponseWriter, name string, data any) {
28+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
29+ if err := h.tmpl.ExecuteTemplate(w, name, data); err != nil {
30+ slog.Error("render", "template", name, "err", err)
31+ }
32+}
33+
34+func (h *handlers) index(w http.ResponseWriter, r *http.Request) {
35+ h.render(w, "index.html", map[string]any{"Items": h.store.List()})
36+}
37+
38+func (h *handlers) create(w http.ResponseWriter, r *http.Request) {
39+ title := strings.TrimSpace(r.FormValue("title"))
40+ if title == "" {
41+ http.Error(w, "title is required", http.StatusBadRequest)
42+ return
43+ }
44+ fields := make([]string, len(app.Fields))
45+ for i := range app.Fields {
46+ fields[i] = strings.TrimSpace(r.FormValue("field" + strconv.Itoa(i)))
47+ }
48+ h.render(w, "item", h.store.Add(title, fields))
49+}
50+
51+func (h *handlers) search(w http.ResponseWriter, r *http.Request) {
52+ h.render(w, "items", h.store.Search(r.URL.Query().Get("q")))
53+}
54+
55+func (h *handlers) toggle(w http.ResponseWriter, r *http.Request) {
56+ id, err := strconv.Atoi(r.PathValue("id"))
57+ if err != nil {
58+ http.Error(w, "bad id", http.StatusBadRequest)
59+ return
60+ }
61+ it, ok := h.store.Toggle(id)
62+ if !ok {
63+ http.NotFound(w, r)
64+ return
65+ }
66+ h.render(w, "item", it)
67+}
68+
69+// delete answers 200 with an empty body: htmx swaps the <li> for nothing.
70+func (h *handlers) delete(w http.ResponseWriter, r *http.Request) {
71+ id, err := strconv.Atoi(r.PathValue("id"))
72+ if err != nil {
73+ http.Error(w, "bad id", http.StatusBadRequest)
74+ return
75+ }
76+ if !h.store.Delete(id) {
77+ http.NotFound(w, r)
78+ return
79+ }
80+ w.WriteHeader(http.StatusOK)
81+}
added demo/skills/go-htmx-app/starter/handlers_test.go +80 -0
new file mode 100644
@@ -0,0 +1,80 @@
1+package main
2+
3+import (
4+ "html/template"
5+ "net/http"
6+ "net/http/httptest"
7+ "net/url"
8+ "strconv"
9+ "strings"
10+ "testing"
11+)
12+
13+func newTestServer(t *testing.T) http.Handler {
14+ t.Helper()
15+ h := &handlers{store: newStore(), tmpl: parseTemplates()}
16+ return h.routes()
17+}
18+
19+// The expected texts come from app.go, so these tests keep passing when the
20+// app is adapted to another subject.
21+func TestRoutes(t *testing.T) {
22+ if len(app.Seeds) == 0 {
23+ t.Fatal("app.Seeds must hold at least one item")
24+ }
25+ created := url.Values{"title": {"New entry"}}
26+ for i := range app.Fields {
27+ created.Set("field"+strconv.Itoa(i), "value "+strconv.Itoa(i))
28+ }
29+ first := app.Seeds[0].Title
30+
31+ tests := []struct {
32+ name string
33+ method string
34+ path string
35+ form url.Values
36+ wantCode int
37+ wantBody string
38+ }{
39+ {"page", "GET", "/", nil, http.StatusOK, "htmx.org"},
40+ {"page title", "GET", "/", nil, http.StatusOK, template.HTMLEscapeString(app.Title)},
41+ {"create", "POST", "/items", created, http.StatusOK, "New entry"},
42+ {"create empty", "POST", "/items", url.Values{"title": {" "}}, http.StatusBadRequest, ""},
43+ {"search", "GET", "/items/search?q=" + url.QueryEscape(first), nil, http.StatusOK, template.HTMLEscapeString(first)},
44+ {"toggle", "POST", "/items/1/toggle", nil, http.StatusOK, `class="done"`},
45+ {"toggle unknown", "POST", "/items/99/toggle", nil, http.StatusNotFound, ""},
46+ {"delete", "DELETE", "/items/1", nil, http.StatusOK, ""},
47+ {"delete unknown", "DELETE", "/items/99", nil, http.StatusNotFound, ""},
48+ }
49+ for _, tt := range tests {
50+ t.Run(tt.name, func(t *testing.T) {
51+ srv := newTestServer(t)
52+ req := httptest.NewRequest(tt.method, tt.path, strings.NewReader(tt.form.Encode()))
53+ if tt.form != nil {
54+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
55+ }
56+ rec := httptest.NewRecorder()
57+ srv.ServeHTTP(rec, req)
58+ if rec.Code != tt.wantCode {
59+ t.Fatalf("status = %d, want %d", rec.Code, tt.wantCode)
60+ }
61+ if !strings.Contains(rec.Body.String(), tt.wantBody) {
62+ t.Errorf("body does not contain %q:\n%s", tt.wantBody, rec.Body)
63+ }
64+ })
65+ }
66+}
67+
68+func TestCreateKeepsFields(t *testing.T) {
69+ if len(app.Fields) == 0 {
70+ t.Skip("no extra fields in app.go")
71+ }
72+ form := url.Values{"title": {"With fields"}, "field0": {"first field value"}}
73+ req := httptest.NewRequest("POST", "/items", strings.NewReader(form.Encode()))
74+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
75+ rec := httptest.NewRecorder()
76+ newTestServer(t).ServeHTTP(rec, req)
77+ if !strings.Contains(rec.Body.String(), "first field value") {
78+ t.Errorf("the fragment does not show the field:\n%s", rec.Body)
79+ }
80+}
new file mode 100644
@@ -0,0 +1,80 @@
1+package main
2+
3+import (
4+ "html/template"
5+ "net/http"
6+ "net/http/httptest"
7+ "net/url"
8+ "strconv"
9+ "strings"
10+ "testing"
11+)
12+
13+func newTestServer(t *testing.T) http.Handler {
14+ t.Helper()
15+ h := &handlers{store: newStore(), tmpl: parseTemplates()}
16+ return h.routes()
17+}
18+
19+// The expected texts come from app.go, so these tests keep passing when the
20+// app is adapted to another subject.
21+func TestRoutes(t *testing.T) {
22+ if len(app.Seeds) == 0 {
23+ t.Fatal("app.Seeds must hold at least one item")
24+ }
25+ created := url.Values{"title": {"New entry"}}
26+ for i := range app.Fields {
27+ created.Set("field"+strconv.Itoa(i), "value "+strconv.Itoa(i))
28+ }
29+ first := app.Seeds[0].Title
30+
31+ tests := []struct {
32+ name string
33+ method string
34+ path string
35+ form url.Values
36+ wantCode int
37+ wantBody string
38+ }{
39+ {"page", "GET", "/", nil, http.StatusOK, "htmx.org"},
40+ {"page title", "GET", "/", nil, http.StatusOK, template.HTMLEscapeString(app.Title)},
41+ {"create", "POST", "/items", created, http.StatusOK, "New entry"},
42+ {"create empty", "POST", "/items", url.Values{"title": {" "}}, http.StatusBadRequest, ""},
43+ {"search", "GET", "/items/search?q=" + url.QueryEscape(first), nil, http.StatusOK, template.HTMLEscapeString(first)},
44+ {"toggle", "POST", "/items/1/toggle", nil, http.StatusOK, `class="done"`},
45+ {"toggle unknown", "POST", "/items/99/toggle", nil, http.StatusNotFound, ""},
46+ {"delete", "DELETE", "/items/1", nil, http.StatusOK, ""},
47+ {"delete unknown", "DELETE", "/items/99", nil, http.StatusNotFound, ""},
48+ }
49+ for _, tt := range tests {
50+ t.Run(tt.name, func(t *testing.T) {
51+ srv := newTestServer(t)
52+ req := httptest.NewRequest(tt.method, tt.path, strings.NewReader(tt.form.Encode()))
53+ if tt.form != nil {
54+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
55+ }
56+ rec := httptest.NewRecorder()
57+ srv.ServeHTTP(rec, req)
58+ if rec.Code != tt.wantCode {
59+ t.Fatalf("status = %d, want %d", rec.Code, tt.wantCode)
60+ }
61+ if !strings.Contains(rec.Body.String(), tt.wantBody) {
62+ t.Errorf("body does not contain %q:\n%s", tt.wantBody, rec.Body)
63+ }
64+ })
65+ }
66+}
67+
68+func TestCreateKeepsFields(t *testing.T) {
69+ if len(app.Fields) == 0 {
70+ t.Skip("no extra fields in app.go")
71+ }
72+ form := url.Values{"title": {"With fields"}, "field0": {"first field value"}}
73+ req := httptest.NewRequest("POST", "/items", strings.NewReader(form.Encode()))
74+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
75+ rec := httptest.NewRecorder()
76+ newTestServer(t).ServeHTTP(rec, req)
77+ if !strings.Contains(rec.Body.String(), "first field value") {
78+ t.Errorf("the fragment does not show the field:\n%s", rec.Body)
79+ }
80+}
added demo/skills/go-htmx-app/starter/main.go +45 -0
new file mode 100644
@@ -0,0 +1,45 @@
1+package main
2+
3+import (
4+ "context"
5+ "errors"
6+ "log/slog"
7+ "net/http"
8+ "os"
9+ "os/signal"
10+ "syscall"
11+ "time"
12+)
13+
14+func main() {
15+ h := &handlers{store: newStore(), tmpl: parseTemplates()}
16+
17+ port := os.Getenv("PORT")
18+ if port == "" {
19+ port = "8080"
20+ }
21+ srv := &http.Server{
22+ Addr: ":" + port,
23+ Handler: h.routes(),
24+ ReadHeaderTimeout: 5 * time.Second,
25+ }
26+
27+ ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
28+ defer stop()
29+
30+ go func() {
31+ slog.Info("listening", "url", "http://localhost:"+port)
32+ if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
33+ slog.Error("server stopped", "err", err)
34+ stop()
35+ }
36+ }()
37+
38+ <-ctx.Done()
39+ shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
40+ defer cancel()
41+ if err := srv.Shutdown(shutdownCtx); err != nil {
42+ slog.Error("shutdown", "err", err)
43+ }
44+ slog.Info("bye")
45+}
new file mode 100644
@@ -0,0 +1,45 @@
1+package main
2+
3+import (
4+ "context"
5+ "errors"
6+ "log/slog"
7+ "net/http"
8+ "os"
9+ "os/signal"
10+ "syscall"
11+ "time"
12+)
13+
14+func main() {
15+ h := &handlers{store: newStore(), tmpl: parseTemplates()}
16+
17+ port := os.Getenv("PORT")
18+ if port == "" {
19+ port = "8080"
20+ }
21+ srv := &http.Server{
22+ Addr: ":" + port,
23+ Handler: h.routes(),
24+ ReadHeaderTimeout: 5 * time.Second,
25+ }
26+
27+ ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
28+ defer stop()
29+
30+ go func() {
31+ slog.Info("listening", "url", "http://localhost:"+port)
32+ if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
33+ slog.Error("server stopped", "err", err)
34+ stop()
35+ }
36+ }()
37+
38+ <-ctx.Done()
39+ shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
40+ defer cancel()
41+ if err := srv.Shutdown(shutdownCtx); err != nil {
42+ slog.Error("shutdown", "err", err)
43+ }
44+ slog.Info("bye")
45+}
added demo/skills/go-htmx-app/starter/store.go +82 -0
new file mode 100644
@@ -0,0 +1,82 @@
1+package main
2+
3+import (
4+ "strings"
5+ "sync"
6+)
7+
8+// Item is one entry of the list. Fields holds one value per app.Fields label.
9+type Item struct {
10+ ID int
11+ Title string
12+ Fields []string
13+ Done bool
14+}
15+
16+// store keeps the items in memory, safe for concurrent handlers.
17+type store struct {
18+ mu sync.Mutex
19+ items []Item
20+ nextID int
21+}
22+
23+func newStore() *store {
24+ s := &store{nextID: 1}
25+ for _, sd := range app.Seeds {
26+ s.Add(sd.Title, sd.Fields)
27+ }
28+ return s
29+}
30+
31+func (s *store) List() []Item {
32+ s.mu.Lock()
33+ defer s.mu.Unlock()
34+ return append([]Item(nil), s.items...)
35+}
36+
37+func (s *store) Add(title string, fields []string) Item {
38+ s.mu.Lock()
39+ defer s.mu.Unlock()
40+ it := Item{ID: s.nextID, Title: title, Fields: fields}
41+ s.nextID++
42+ s.items = append(s.items, it)
43+ return it
44+}
45+
46+// Toggle flips Done and returns the updated item; ok is false for an unknown id.
47+func (s *store) Toggle(id int) (Item, bool) {
48+ s.mu.Lock()
49+ defer s.mu.Unlock()
50+ for i := range s.items {
51+ if s.items[i].ID == id {
52+ s.items[i].Done = !s.items[i].Done
53+ return s.items[i], true
54+ }
55+ }
56+ return Item{}, false
57+}
58+
59+func (s *store) Delete(id int) bool {
60+ s.mu.Lock()
61+ defer s.mu.Unlock()
62+ for i := range s.items {
63+ if s.items[i].ID == id {
64+ s.items = append(s.items[:i], s.items[i+1:]...)
65+ return true
66+ }
67+ }
68+ return false
69+}
70+
71+// Search returns the items whose title or fields contain q, ignoring case.
72+func (s *store) Search(q string) []Item {
73+ q = strings.ToLower(strings.TrimSpace(q))
74+ var found []Item
75+ for _, it := range s.List() {
76+ text := strings.ToLower(it.Title + " " + strings.Join(it.Fields, " "))
77+ if strings.Contains(text, q) {
78+ found = append(found, it)
79+ }
80+ }
81+ return found
82+}
new file mode 100644
@@ -0,0 +1,82 @@
1+package main
2+
3+import (
4+ "strings"
5+ "sync"
6+)
7+
8+// Item is one entry of the list. Fields holds one value per app.Fields label.
9+type Item struct {
10+ ID int
11+ Title string
12+ Fields []string
13+ Done bool
14+}
15+
16+// store keeps the items in memory, safe for concurrent handlers.
17+type store struct {
18+ mu sync.Mutex
19+ items []Item
20+ nextID int
21+}
22+
23+func newStore() *store {
24+ s := &store{nextID: 1}
25+ for _, sd := range app.Seeds {
26+ s.Add(sd.Title, sd.Fields)
27+ }
28+ return s
29+}
30+
31+func (s *store) List() []Item {
32+ s.mu.Lock()
33+ defer s.mu.Unlock()
34+ return append([]Item(nil), s.items...)
35+}
36+
37+func (s *store) Add(title string, fields []string) Item {
38+ s.mu.Lock()
39+ defer s.mu.Unlock()
40+ it := Item{ID: s.nextID, Title: title, Fields: fields}
41+ s.nextID++
42+ s.items = append(s.items, it)
43+ return it
44+}
45+
46+// Toggle flips Done and returns the updated item; ok is false for an unknown id.
47+func (s *store) Toggle(id int) (Item, bool) {
48+ s.mu.Lock()
49+ defer s.mu.Unlock()
50+ for i := range s.items {
51+ if s.items[i].ID == id {
52+ s.items[i].Done = !s.items[i].Done
53+ return s.items[i], true
54+ }
55+ }
56+ return Item{}, false
57+}
58+
59+func (s *store) Delete(id int) bool {
60+ s.mu.Lock()
61+ defer s.mu.Unlock()
62+ for i := range s.items {
63+ if s.items[i].ID == id {
64+ s.items = append(s.items[:i], s.items[i+1:]...)
65+ return true
66+ }
67+ }
68+ return false
69+}
70+
71+// Search returns the items whose title or fields contain q, ignoring case.
72+func (s *store) Search(q string) []Item {
73+ q = strings.ToLower(strings.TrimSpace(q))
74+ var found []Item
75+ for _, it := range s.List() {
76+ text := strings.ToLower(it.Title + " " + strings.Join(it.Fields, " "))
77+ if strings.Contains(text, q) {
78+ found = append(found, it)
79+ }
80+ }
81+ return found
82+}
added demo/skills/go-htmx-app/starter/templates.go +16 -0
new file mode 100644
@@ -0,0 +1,16 @@
1+package main
2+
3+import (
4+ "embed"
5+ "html/template"
6+)
7+
8+//go:embed templates/*.html
9+var templatesFS embed.FS
10+
11+// parseTemplates loads the page and its fragments; `app` gives them the
12+// texts of app.go.
13+func parseTemplates() *template.Template {
14+ funcs := template.FuncMap{"app": func() appInfo { return app }}
15+ return template.Must(template.New("").Funcs(funcs).ParseFS(templatesFS, "templates/*.html"))
16+}
new file mode 100644
@@ -0,0 +1,16 @@
1+package main
2+
3+import (
4+ "embed"
5+ "html/template"
6+)
7+
8+//go:embed templates/*.html
9+var templatesFS embed.FS
10+
11+// parseTemplates loads the page and its fragments; `app` gives them the
12+// texts of app.go.
13+func parseTemplates() *template.Template {
14+ funcs := template.FuncMap{"app": func() appInfo { return app }}
15+ return template.Must(template.New("").Funcs(funcs).ParseFS(templatesFS, "templates/*.html"))
16+}
added demo/skills/go-htmx-app/starter/templates/index.html +40 -0
new file mode 100644
@@ -0,0 +1,40 @@
1+<!doctype html>
2+<html lang="{{(app).Lang}}">
3+<head>
4+ <meta charset="utf-8">
5+ <meta name="viewport" content="width=device-width, initial-scale=1">
6+ <title>{{(app).Title}}</title>
7+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
8+ <script src="https://unpkg.com/htmx.org@2.0.4"></script>
9+ <style>
10+ #items li { display: flex; flex-wrap: wrap; align-items: center; gap: .75rem; }
11+ #items li.done span { text-decoration: line-through; opacity: .6; }
12+ #items li button { margin-left: auto; width: auto; padding: .25rem .75rem; }
13+ @media (max-width: 576px) { form fieldset[role=group] { flex-direction: column; } }
14+ </style>
15+</head>
16+<body>
17+ <main class="container">
18+ <h1>{{(app).Title}}</h1>
19+ <form hx-post="/items" hx-target="#items" hx-swap="beforeend" hx-on::after-request="this.reset()">
20+ <fieldset role="group">
21+ <input name="title" required placeholder="{{(app).TitleLabel}}" aria-label="{{(app).TitleLabel}}">
22+ {{range $i, $label := (app).Fields}}<input name="field{{$i}}" placeholder="{{$label}}" aria-label="{{$label}}">
23+ {{end}}<button>{{(app).AddLabel}}</button>
24+ </fieldset>
25+ </form>
26+ <input type="search" name="q" placeholder="{{(app).SearchLabel}}" aria-label="{{(app).SearchLabel}}"
27+ hx-get="/items/search" hx-trigger="input changed delay:300ms, search" hx-target="#items">
28+ <ul id="items">{{template "items" .Items}}</ul>
29+ </main>
30+</body>
31+</html>
32+
33+{{define "items"}}{{range .}}{{template "item" .}}{{end}}{{end}}
34+
35+{{define "item"}}<li id="item-{{.ID}}"{{if .Done}} class="done"{{end}}>
36+ {{if (app).DoneLabel}}<input type="checkbox" aria-label="{{(app).DoneLabel}}" title="{{(app).DoneLabel}}" hx-post="/items/{{.ID}}/toggle" hx-target="#item-{{.ID}}" hx-swap="outerHTML"{{if .Done}} checked{{end}}>{{end}}
37+ <span>{{.Title}}</span>
38+ {{range .Fields}}{{if .}}<small>{{.}}</small>{{end}}{{end}}
39+ <button class="secondary outline" hx-delete="/items/{{.ID}}" hx-target="#item-{{.ID}}" hx-swap="outerHTML" hx-confirm="{{(app).DeleteConfirm}}">✕</button>
40+</li>{{end}}
new file mode 100644
@@ -0,0 +1,40 @@
1+<!doctype html>
2+<html lang="{{(app).Lang}}">
3+<head>
4+ <meta charset="utf-8">
5+ <meta name="viewport" content="width=device-width, initial-scale=1">
6+ <title>{{(app).Title}}</title>
7+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
8+ <script src="https://unpkg.com/htmx.org@2.0.4"></script>
9+ <style>
10+ #items li { display: flex; flex-wrap: wrap; align-items: center; gap: .75rem; }
11+ #items li.done span { text-decoration: line-through; opacity: .6; }
12+ #items li button { margin-left: auto; width: auto; padding: .25rem .75rem; }
13+ @media (max-width: 576px) { form fieldset[role=group] { flex-direction: column; } }
14+ </style>
15+</head>
16+<body>
17+ <main class="container">
18+ <h1>{{(app).Title}}</h1>
19+ <form hx-post="/items" hx-target="#items" hx-swap="beforeend" hx-on::after-request="this.reset()">
20+ <fieldset role="group">
21+ <input name="title" required placeholder="{{(app).TitleLabel}}" aria-label="{{(app).TitleLabel}}">
22+ {{range $i, $label := (app).Fields}}<input name="field{{$i}}" placeholder="{{$label}}" aria-label="{{$label}}">
23+ {{end}}<button>{{(app).AddLabel}}</button>
24+ </fieldset>
25+ </form>
26+ <input type="search" name="q" placeholder="{{(app).SearchLabel}}" aria-label="{{(app).SearchLabel}}"
27+ hx-get="/items/search" hx-trigger="input changed delay:300ms, search" hx-target="#items">
28+ <ul id="items">{{template "items" .Items}}</ul>
29+ </main>
30+</body>
31+</html>
32+
33+{{define "items"}}{{range .}}{{template "item" .}}{{end}}{{end}}
34+
35+{{define "item"}}<li id="item-{{.ID}}"{{if .Done}} class="done"{{end}}>
36+ {{if (app).DoneLabel}}<input type="checkbox" aria-label="{{(app).DoneLabel}}" title="{{(app).DoneLabel}}" hx-post="/items/{{.ID}}/toggle" hx-target="#item-{{.ID}}" hx-swap="outerHTML"{{if .Done}} checked{{end}}>{{end}}
37+ <span>{{.Title}}</span>
38+ {{range .Fields}}{{if .}}<small>{{.}}</small>{{end}}{{end}}
39+ <button class="secondary outline" hx-delete="/items/{{.ID}}" hx-target="#item-{{.ID}}" hx-swap="outerHTML" hx-confirm="{{(app).DeleteConfirm}}">✕</button>
40+</li>{{end}}
added demo/skills/go-htmx-subject-app/SKILL.md +58 -0
new file mode 100644
@@ -0,0 +1,58 @@
1+---
2+name: go-htmx-subject-app
3+description: Create a web application in modern Go with htmx about a given subject (books, contacts, recipes, movies…) with its own fields, from the tested starter.
4+keywords: gérer, manage, livres, contacts, recettes, champs, fields, liste
5+---
6+
7+# Create a Go + htmx web app about a subject
8+
9+Every file path starts with the app directory: `<name>/app.go` — never `app.go` alone.
10+
11+## 1. Name
12+
13+`<name>`: the subject in kebab-case (`book-library`).
14+
15+## 2. Copy the starter
16+
17+The starter is a complete, tested list app: add, mark done, delete, live search, a responsive page. Call your `bash` tool with this command:
18+
19+ mkdir -p <name> && cp -rn skills/go-htmx-app/starter/. <name>/ && cd <name> && go mod edit -module <name>
20+
21+## 3. Describe the subject in `<name>/app.go` — the ONLY Go file to change
22+
23+The page, the handlers and the tests all read `app.go`: do not touch the other files, do not read `app.go`. Call `write_file` (not edit_file) on `<name>/app.go` with the WHOLE file, texts in the language of the user's request. Example for « des livres avec un titre et un auteur, marquer comme lu », in French:
24+
25+ package main
26+
27+ var app = appInfo{
28+ Lang: "fr",
29+ Title: "Ma bibliothèque",
30+ TitleLabel: "Titre",
31+ Fields: []string{"Auteur"},
32+ DoneLabel: "Lu",
33+ AddLabel: "Ajouter",
34+ SearchLabel: "Rechercher",
35+ DeleteConfirm: "Supprimer ce livre ?",
36+ Seeds: []seed{
37+ {Title: "L'Étranger", Fields: []string{"Albert Camus"}},
38+ {Title: "Dune", Fields: []string{"Frank Herbert"}},
39+ },
40+ }
41+
42+Rules: `Title` is the main text of an item; `Fields` lists the OTHER things the user named (author, year, email…), and every seed has one value per field, in the same order; `DoneLabel` names the checkbox (read, bought, watched, done) — `""` when the subject has no such state (contacts): the checkbox disappears. Everything the user asked fits in `app.go`: after this ONE write_file, go straight to step 4.
43+
44+## 4. Check
45+
46+Call your `bash` tool with this command:
47+
48+ cd <name> && sh check.sh
49+
50+It ends with `CHECK OK`. Anything else: read the `run-and-check` skill, fix `app.go`, run it again. It also prints the page title: it must be YOUR title, not `htmx tasks` — otherwise `app.go` was not written: write it again.
51+
52+## 5. README
53+
54+One `write_file` call on `<name>/README.md` (it replaces the starter's), in the language of the user's request: what the app does, the fields, the stack (Go standard library, htmx 2, Pico CSS), how to run it (`go run .` then http://localhost:8080), test it (`go test ./...`), check it (`sh check.sh`), and that `app.go` holds the texts and fields.
55+
56+## 6. Answer
57+
58+A few lines in the user's language — not the README again: what the app holds, the `CHECK OK` result, how to start it: `cd <name> && go run .`, then open http://localhost:8080.
new file mode 100644
@@ -0,0 +1,58 @@
1+---
2+name: go-htmx-subject-app
3+description: Create a web application in modern Go with htmx about a given subject (books, contacts, recipes, movies…) with its own fields, from the tested starter.
4+keywords: gérer, manage, livres, contacts, recettes, champs, fields, liste
5+---
6+
7+# Create a Go + htmx web app about a subject
8+
9+Every file path starts with the app directory: `<name>/app.go` — never `app.go` alone.
10+
11+## 1. Name
12+
13+`<name>`: the subject in kebab-case (`book-library`).
14+
15+## 2. Copy the starter
16+
17+The starter is a complete, tested list app: add, mark done, delete, live search, a responsive page. Call your `bash` tool with this command:
18+
19+ mkdir -p <name> && cp -rn skills/go-htmx-app/starter/. <name>/ && cd <name> && go mod edit -module <name>
20+
21+## 3. Describe the subject in `<name>/app.go` — the ONLY Go file to change
22+
23+The page, the handlers and the tests all read `app.go`: do not touch the other files, do not read `app.go`. Call `write_file` (not edit_file) on `<name>/app.go` with the WHOLE file, texts in the language of the user's request. Example for « des livres avec un titre et un auteur, marquer comme lu », in French:
24+
25+ package main
26+
27+ var app = appInfo{
28+ Lang: "fr",
29+ Title: "Ma bibliothèque",
30+ TitleLabel: "Titre",
31+ Fields: []string{"Auteur"},
32+ DoneLabel: "Lu",
33+ AddLabel: "Ajouter",
34+ SearchLabel: "Rechercher",
35+ DeleteConfirm: "Supprimer ce livre ?",
36+ Seeds: []seed{
37+ {Title: "L'Étranger", Fields: []string{"Albert Camus"}},
38+ {Title: "Dune", Fields: []string{"Frank Herbert"}},
39+ },
40+ }
41+
42+Rules: `Title` is the main text of an item; `Fields` lists the OTHER things the user named (author, year, email…), and every seed has one value per field, in the same order; `DoneLabel` names the checkbox (read, bought, watched, done) — `""` when the subject has no such state (contacts): the checkbox disappears. Everything the user asked fits in `app.go`: after this ONE write_file, go straight to step 4.
43+
44+## 4. Check
45+
46+Call your `bash` tool with this command:
47+
48+ cd <name> && sh check.sh
49+
50+It ends with `CHECK OK`. Anything else: read the `run-and-check` skill, fix `app.go`, run it again. It also prints the page title: it must be YOUR title, not `htmx tasks` — otherwise `app.go` was not written: write it again.
51+
52+## 5. README
53+
54+One `write_file` call on `<name>/README.md` (it replaces the starter's), in the language of the user's request: what the app does, the fields, the stack (Go standard library, htmx 2, Pico CSS), how to run it (`go run .` then http://localhost:8080), test it (`go test ./...`), check it (`sh check.sh`), and that `app.go` holds the texts and fields.
55+
56+## 6. Answer
57+
58+A few lines in the user's language — not the README again: what the app holds, the `CHECK OK` result, how to start it: `cd <name> && go run .`, then open http://localhost:8080.
added demo/skills/htmx-patterns/SKILL.md +77 -0
new file mode 100644
@@ -0,0 +1,77 @@
1+---
2+name: htmx-patterns
3+description: Reference of the htmx 2 attributes and the Go handler side for the usual interactions — create, toggle, delete, live search, inline edit, partial pages.
4+keywords: htmx, hx-post, hx-get, hx-swap, hx-target, fragment, partial, search
5+---
6+
7+# htmx patterns with Go templates
8+
9+The server answers HTML, never JSON. A full page for a normal request, a fragment (`{{define "item"}}`) for an htmx request.
10+
11+## Attributes
12+
13+| Attribute | Use |
14+|---|---|
15+| `hx-get`, `hx-post`, `hx-put`, `hx-delete` | request sent on the element's natural event (click, submit, change) |
16+| `hx-target="#items"` | element that receives the answer (default: the element itself) |
17+| `hx-swap` | `innerHTML` (default), `outerHTML` (replace the element), `beforeend` (append), `afterbegin`, `delete` |
18+| `hx-trigger="input changed delay:300ms"` | live search; `"load"` loads once at page load |
19+| `hx-confirm="Delete?"` | browser confirmation first |
20+| `hx-on::after-request="this.reset()"` | clear a form after it was sent |
21+| `hx-indicator="#spin"` | element shown while a request runs (`class="htmx-indicator"`) |
22+
23+## Create — append the new item
24+
25+```html
26+<form hx-post="/items" hx-target="#items" hx-swap="beforeend" hx-on::after-request="this.reset()">
27+ <input name="title" required placeholder="New item">
28+ <button>Add</button>
29+</form>
30+<ul id="items">{{template "items" .Items}}</ul>
31+```
32+Handler: validate, add, `ExecuteTemplate(w, "item", it)`.
33+
34+## Toggle or edit — replace one item
35+
36+```html
37+{{define "item"}}<li id="item-{{.ID}}">
38+ <input type="checkbox" hx-post="/items/{{.ID}}/toggle" hx-target="#item-{{.ID}}" hx-swap="outerHTML" {{if .Done}}checked{{end}}>
39+ {{.Title}}
40+ <button hx-delete="/items/{{.ID}}" hx-target="#item-{{.ID}}" hx-swap="outerHTML" hx-confirm="Delete?">✕</button>
41+</li>{{end}}
42+```
43+Toggle handler: answer the updated `"item"`. Delete handler: `w.WriteHeader(http.StatusOK)` with an EMPTY body — htmx replaces the `<li>` with nothing. (htmx ignores a 204 answer: use 200.)
44+
45+## Live search — replace the list
46+
47+```html
48+<input type="search" name="q" hx-get="/items/search" hx-trigger="input changed delay:300ms, search" hx-target="#items">
49+```
50+Handler: `q := r.URL.Query().Get("q")`, answer `"items"` with the filtered slice.
51+
52+## Refresh another part of the page (a counter, a total)
53+
54+Every change announces itself; the counter reloads itself when it hears it.
55+
56+- `handlers.go`, in `routes()`: `mux.HandleFunc("GET /items/count", h.count)`, and wrap the mux so every non-GET request sends the event:
57+
58+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
59+ if r.Method != http.MethodGet {
60+ w.Header().Set("HX-Trigger", "items-changed")
61+ }
62+ mux.ServeHTTP(w, r)
63+ })
64+
65+- the handler: `h.render(w, "count", h.store.Remaining())` — `Remaining()` is a store method counting the items not done.
66+- the page: `<p id="count" hx-get="/items/count" hx-trigger="load, items-changed from:body">…</p>` and the fragment `{{define "count"}}{{.}} remaining{{end}}`.
67+
68+## One route, two answers
69+
70+`r.Header.Get("HX-Request") == "true"` → the fragment; otherwise the full page. Use it when a link can be opened directly too.
71+
72+## Rules
73+
74+- `html/template` escapes values: never build HTML with `fmt.Sprintf`.
75+- `{{template "items" .Items}}` passes the data on; `{{template "items"}}` alone passes nothing.
76+- Every `hx-target` id exists in the page, and fragments keep the same id as the element they replace.
77+- DELETE and PUT bodies: read ids from the path (`r.PathValue("id")`), not from a form.
new file mode 100644
@@ -0,0 +1,77 @@
1+---
2+name: htmx-patterns
3+description: Reference of the htmx 2 attributes and the Go handler side for the usual interactions — create, toggle, delete, live search, inline edit, partial pages.
4+keywords: htmx, hx-post, hx-get, hx-swap, hx-target, fragment, partial, search
5+---
6+
7+# htmx patterns with Go templates
8+
9+The server answers HTML, never JSON. A full page for a normal request, a fragment (`{{define "item"}}`) for an htmx request.
10+
11+## Attributes
12+
13+| Attribute | Use |
14+|---|---|
15+| `hx-get`, `hx-post`, `hx-put`, `hx-delete` | request sent on the element's natural event (click, submit, change) |
16+| `hx-target="#items"` | element that receives the answer (default: the element itself) |
17+| `hx-swap` | `innerHTML` (default), `outerHTML` (replace the element), `beforeend` (append), `afterbegin`, `delete` |
18+| `hx-trigger="input changed delay:300ms"` | live search; `"load"` loads once at page load |
19+| `hx-confirm="Delete?"` | browser confirmation first |
20+| `hx-on::after-request="this.reset()"` | clear a form after it was sent |
21+| `hx-indicator="#spin"` | element shown while a request runs (`class="htmx-indicator"`) |
22+
23+## Create — append the new item
24+
25+```html
26+<form hx-post="/items" hx-target="#items" hx-swap="beforeend" hx-on::after-request="this.reset()">
27+ <input name="title" required placeholder="New item">
28+ <button>Add</button>
29+</form>
30+<ul id="items">{{template "items" .Items}}</ul>
31+```
32+Handler: validate, add, `ExecuteTemplate(w, "item", it)`.
33+
34+## Toggle or edit — replace one item
35+
36+```html
37+{{define "item"}}<li id="item-{{.ID}}">
38+ <input type="checkbox" hx-post="/items/{{.ID}}/toggle" hx-target="#item-{{.ID}}" hx-swap="outerHTML" {{if .Done}}checked{{end}}>
39+ {{.Title}}
40+ <button hx-delete="/items/{{.ID}}" hx-target="#item-{{.ID}}" hx-swap="outerHTML" hx-confirm="Delete?">✕</button>
41+</li>{{end}}
42+```
43+Toggle handler: answer the updated `"item"`. Delete handler: `w.WriteHeader(http.StatusOK)` with an EMPTY body — htmx replaces the `<li>` with nothing. (htmx ignores a 204 answer: use 200.)
44+
45+## Live search — replace the list
46+
47+```html
48+<input type="search" name="q" hx-get="/items/search" hx-trigger="input changed delay:300ms, search" hx-target="#items">
49+```
50+Handler: `q := r.URL.Query().Get("q")`, answer `"items"` with the filtered slice.
51+
52+## Refresh another part of the page (a counter, a total)
53+
54+Every change announces itself; the counter reloads itself when it hears it.
55+
56+- `handlers.go`, in `routes()`: `mux.HandleFunc("GET /items/count", h.count)`, and wrap the mux so every non-GET request sends the event:
57+
58+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
59+ if r.Method != http.MethodGet {
60+ w.Header().Set("HX-Trigger", "items-changed")
61+ }
62+ mux.ServeHTTP(w, r)
63+ })
64+
65+- the handler: `h.render(w, "count", h.store.Remaining())` — `Remaining()` is a store method counting the items not done.
66+- the page: `<p id="count" hx-get="/items/count" hx-trigger="load, items-changed from:body">…</p>` and the fragment `{{define "count"}}{{.}} remaining{{end}}`.
67+
68+## One route, two answers
69+
70+`r.Header.Get("HX-Request") == "true"` → the fragment; otherwise the full page. Use it when a link can be opened directly too.
71+
72+## Rules
73+
74+- `html/template` escapes values: never build HTML with `fmt.Sprintf`.
75+- `{{template "items" .Items}}` passes the data on; `{{template "items"}}` alone passes nothing.
76+- Every `hx-target` id exists in the page, and fragments keep the same id as the element they replace.
77+- DELETE and PUT bodies: read ids from the path (`r.PathValue("id")`), not from a form.
added demo/skills/run-and-check/SKILL.md +38 -0
new file mode 100644
@@ -0,0 +1,38 @@
1+---
2+name: run-and-check
3+description: Check a Go + htmx web app and fix what fails — gofmt, go vet, go test, then start it, query it with curl and stop it.
4+keywords: vérifie, teste, corrige, erreur, lance, check, fix, curl
5+---
6+
7+# Check and fix a Go + htmx web app
8+
9+`<dir>` is the app's directory.
10+
11+## 1. Run the check
12+
13+If `<dir>/check.sh` exists (apps made from the starter), call your `bash` tool with this command:
14+
15+ cd <dir> && sh check.sh
16+
17+Otherwise, call your `bash` tool with this command:
18+
19+ cd <dir> && gofmt -l . && go vet ./... && go test ./... 2>&1 | tail -30
20+
21+## 2. Read what failed — the section printed last
22+
23+- `formatted: <file>`: check.sh fixed the formatting itself — nothing to do.
24+- In an app made from the starter, texts and fields live in `app.go`: every seed needs one value per entry of `Fields`.
25+- `file.go:12:5: …` (vet, build or test): `read_file` that file, fix that line with `edit_file`.
26+- `--- FAIL: TestRoutes/<case>`: the test case named; decide whether the code or the test is wrong — the test describes what the user asked.
27+- `GET / -> 500` or a template error in the server log: `read_file` `<dir>/templates/index.html`, fix the `{{…}}` the log names (a field that does not exist, a `{{define}}` without `{{end}}`).
28+- `GET / -> 000`: the app did not start, the server log says why; `address already in use` → run `sh check.sh 8098`.
29+
30+Fix ONE cause, run the check again. Never delete a file or a test to make an error go away.
31+
32+## 3. Never
33+
34+`go run .` with your bash tool: a server never returns and the command times out. `check.sh` starts and stops it for you.
35+
36+## 4. Report
37+
38+Quote the last line of the check (`CHECK OK`, or `ok` for go test).
new file mode 100644
@@ -0,0 +1,38 @@
1+---
2+name: run-and-check
3+description: Check a Go + htmx web app and fix what fails — gofmt, go vet, go test, then start it, query it with curl and stop it.
4+keywords: vérifie, teste, corrige, erreur, lance, check, fix, curl
5+---
6+
7+# Check and fix a Go + htmx web app
8+
9+`<dir>` is the app's directory.
10+
11+## 1. Run the check
12+
13+If `<dir>/check.sh` exists (apps made from the starter), call your `bash` tool with this command:
14+
15+ cd <dir> && sh check.sh
16+
17+Otherwise, call your `bash` tool with this command:
18+
19+ cd <dir> && gofmt -l . && go vet ./... && go test ./... 2>&1 | tail -30
20+
21+## 2. Read what failed — the section printed last
22+
23+- `formatted: <file>`: check.sh fixed the formatting itself — nothing to do.
24+- In an app made from the starter, texts and fields live in `app.go`: every seed needs one value per entry of `Fields`.
25+- `file.go:12:5: …` (vet, build or test): `read_file` that file, fix that line with `edit_file`.
26+- `--- FAIL: TestRoutes/<case>`: the test case named; decide whether the code or the test is wrong — the test describes what the user asked.
27+- `GET / -> 500` or a template error in the server log: `read_file` `<dir>/templates/index.html`, fix the `{{…}}` the log names (a field that does not exist, a `{{define}}` without `{{end}}`).
28+- `GET / -> 000`: the app did not start, the server log says why; `address already in use` → run `sh check.sh 8098`.
29+
30+Fix ONE cause, run the check again. Never delete a file or a test to make an error go away.
31+
32+## 3. Never
33+
34+`go run .` with your bash tool: a server never returns and the command times out. `check.sh` starts and stops it for you.
35+
36+## 4. Report
37+
38+Quote the last line of the check (`CHECK OK`, or `ok` for go test).
added git.sh +136 -0
new file mode 100755
@@ -0,0 +1,136 @@
1+#!/bin/bash
2+message=""
3+case $1 in
4+
5+ # 🎨: art
6+ art)
7+ message="Improve structure / format of the code"
8+ emoji="🎨"
9+ ;;
10+
11+ # 🐛: bug
12+ bug|fix)
13+ message="Fix a bug"
14+ emoji="🐛"
15+ ;;
16+
17+ # 🤓: geek
18+ human|human-fixed)
19+ message="Human Fixed"
20+ emoji="🤓"
21+ ;;
22+
23+ # 🤖: robot
24+ ai|ai-generated)
25+ message="AI generated"
26+ emoji="🤖"
27+ ;;
28+
29+ # ✨: sparkles
30+ sparkles|feature)
31+ message="Introduce new feature(s)"
32+ emoji="✨"
33+ ;;
34+
35+ # 🧩: jigsaw
36+ jigsaw|example|examples|demo|demos)
37+ message="Introduce new example(s)"
38+ emoji="🧩"
39+ ;;
40+
41+
42+ # 📝: memo
43+ memo|doc|documentation)
44+ message="Add or update documentation"
45+ emoji="📝"
46+ ;;
47+
48+ # 🌸: cherry_blossom
49+ gardening|garden|clean|cleaning)
50+ message="Gardening"
51+ emoji="🌸"
52+ ;;
53+
54+ # 🚀: rocket
55+ rocket|deploy)
56+ message="Deploy stuff"
57+ emoji="🚀"
58+ ;;
59+
60+ # 🎉: tada
61+ tada|first)
62+ message="Begin a project"
63+ emoji="🎉"
64+ ;;
65+
66+ # 🚧: construction
67+ construction|wip)
68+ message="Work in progress"
69+ emoji="🚧"
70+ ;;
71+
72+ # 📦️: package
73+ package|build)
74+ message="Add or update compiled files or packages"
75+ emoji="📦️"
76+ ;;
77+
78+ # 📦️: package
79+ release)
80+ message="Create a release"
81+ emoji="📦️"
82+ ;;
83+
84+ # 👽️: alien
85+ alien|api)
86+ message="Update code due to external API changes"
87+ emoji="👽️"
88+ ;;
89+
90+ # 🐳: whale
91+ docker|container)
92+ message="Docker"
93+ emoji="🐳"
94+ ;;
95+
96+ # 🍊: tangerine
97+ gitpod|gitpodify)
98+ message="Gitpodify"
99+ emoji="🍊"
100+ ;;
101+
102+ # 🧪: test tube
103+ alembic|experiments|experiment|xp)
104+ message="Perform experiments"
105+ emoji="🧪"
106+ ;;
107+
108+ # ✅: check mark
109+ test|tests|testing)
110+ message="Add or update tests"
111+ emoji="✅"
112+ ;;
113+
114+ # 💾: floppy-disk
115+ save)
116+ message="Saved"
117+ emoji="💾"
118+ ;;
119+
120+ *)
121+ message="Updated"
122+ emoji="🛟"
123+ ;;
124+
125+esac
126+
127+find . -name '.DS_Store' -type f -delete
128+
129+if [ -z "$2" ]
130+then
131+ # empty
132+ git add .; git commit -m "$emoji $message."; git push
133+else
134+ # not empty
135+ git add .; git commit -m "$emoji $message: $2"; git push
136+fi
new file mode 100755
@@ -0,0 +1,136 @@
1+#!/bin/bash
2+message=""
3+case $1 in
4+
5+ # 🎨: art
6+ art)
7+ message="Improve structure / format of the code"
8+ emoji="🎨"
9+ ;;
10+
11+ # 🐛: bug
12+ bug|fix)
13+ message="Fix a bug"
14+ emoji="🐛"
15+ ;;
16+
17+ # 🤓: geek
18+ human|human-fixed)
19+ message="Human Fixed"
20+ emoji="🤓"
21+ ;;
22+
23+ # 🤖: robot
24+ ai|ai-generated)
25+ message="AI generated"
26+ emoji="🤖"
27+ ;;
28+
29+ # ✨: sparkles
30+ sparkles|feature)
31+ message="Introduce new feature(s)"
32+ emoji="✨"
33+ ;;
34+
35+ # 🧩: jigsaw
36+ jigsaw|example|examples|demo|demos)
37+ message="Introduce new example(s)"
38+ emoji="🧩"
39+ ;;
40+
41+
42+ # 📝: memo
43+ memo|doc|documentation)
44+ message="Add or update documentation"
45+ emoji="📝"
46+ ;;
47+
48+ # 🌸: cherry_blossom
49+ gardening|garden|clean|cleaning)
50+ message="Gardening"
51+ emoji="🌸"
52+ ;;
53+
54+ # 🚀: rocket
55+ rocket|deploy)
56+ message="Deploy stuff"
57+ emoji="🚀"
58+ ;;
59+
60+ # 🎉: tada
61+ tada|first)
62+ message="Begin a project"
63+ emoji="🎉"
64+ ;;
65+
66+ # 🚧: construction
67+ construction|wip)
68+ message="Work in progress"
69+ emoji="🚧"
70+ ;;
71+
72+ # 📦️: package
73+ package|build)
74+ message="Add or update compiled files or packages"
75+ emoji="📦️"
76+ ;;
77+
78+ # 📦️: package
79+ release)
80+ message="Create a release"
81+ emoji="📦️"
82+ ;;
83+
84+ # 👽️: alien
85+ alien|api)
86+ message="Update code due to external API changes"
87+ emoji="👽️"
88+ ;;
89+
90+ # 🐳: whale
91+ docker|container)
92+ message="Docker"
93+ emoji="🐳"
94+ ;;
95+
96+ # 🍊: tangerine
97+ gitpod|gitpodify)
98+ message="Gitpodify"
99+ emoji="🍊"
100+ ;;
101+
102+ # 🧪: test tube
103+ alembic|experiments|experiment|xp)
104+ message="Perform experiments"
105+ emoji="🧪"
106+ ;;
107+
108+ # ✅: check mark
109+ test|tests|testing)
110+ message="Add or update tests"
111+ emoji="✅"
112+ ;;
113+
114+ # 💾: floppy-disk
115+ save)
116+ message="Saved"
117+ emoji="💾"
118+ ;;
119+
120+ *)
121+ message="Updated"
122+ emoji="🛟"
123+ ;;
124+
125+esac
126+
127+find . -name '.DS_Store' -type f -delete
128+
129+if [ -z "$2" ]
130+then
131+ # empty
132+ git add .; git commit -m "$emoji $message."; git push
133+else
134+ # not empty
135+ git add .; git commit -m "$emoji $message: $2"; git push
136+fi