Initial commit: demo todo CLI with Claude Code setup

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
christianandClaude Opus 5.5 committed 2026-10-20 23:49:20 +02:00
commit 3cec32b9e6
10 files changed
+193

No files matched your search

+11
View File
@@ -0,0 +1,11 @@
---
name: test-writer
description: Writes node:test unit tests for functions in src/ that lack coverage. Use when asked to add or improve tests.
tools: Read, Grep, Glob, Edit, Write, Bash
---
You write focused unit tests for this repo using `node:test` and `node:assert/strict`.
- Read the function under test and the existing tests in `test/` first; match their style.
- Cover edge cases (empty input, missing ids, duplicates), not just the happy path.
- Run `npm test` before finishing. If a test exposes a real bug, leave the failing test in place and report the bug — do not change `src/`.
+30
View File
@@ -0,0 +1,30 @@
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"permissions": {
"allow": [
"Bash(npm test)",
"Bash(npm start:*)",
"Bash(node --test:*)",
"Bash(git status)",
"Bash(git diff:*)",
"Bash(git log:*)"
],
"deny": [
"Bash(git push --force:*)",
"Read(./.env)"
]
},
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "f=$(jq -r '.tool_input.file_path // empty'); case \"$f\" in *.js) node --check \"$f\" || { echo \"Syntax error in $f\" >&2; exit 2; } ;; esac"
}
]
}
]
}
}
+9
View File
@@ -0,0 +1,9 @@
---
name: changelog
description: Draft a changelog entry from the git commits since the last tag (or all commits if there is no tag). Use when the user asks for release notes or a changelog.
---
1. Find the range: `git describe --tags --abbrev=0` — if it fails, use the full history.
2. Read the commits in that range with `git log --oneline <range>`.
3. Group them under **Added**, **Changed**, **Fixed** (skip empty groups), one bullet per user-visible change, in plain language.
4. Print the entry. Only write it to `CHANGELOG.md` if the user asks.
+7
View File
@@ -0,0 +1,7 @@
node_modules/
todos.json
.DS_Store
# Personal Claude Code overrides (not shared)
.claude/settings.local.json
CLAUDE.local.md
+16
View File
@@ -0,0 +1,16 @@
# claude-playground
A tiny zero-dependency Node.js todo CLI, used as a sandbox for trying Claude Code features.
## Commands
- `npm test` — run the test suite (built-in `node:test`, no install needed)
- `npm start -- add "something"` / `npm start -- ls` — run the CLI (data in `todos.json`, override with `TODO_FILE`)
## Conventions
- ES modules, Node >= 20, no third-party dependencies.
- `src/todo.js` holds pure functions; keep I/O in `load`/`save` and `src/cli.js`.
- Every behavior change gets a test in `test/`.
- Commit messages: short imperative subject line.
## Remote
Gitea at code.westerkam.org — `origin` = `git@code.westerkam.org:christian/claude-playground.git`.
+28
View File
@@ -0,0 +1,28 @@
# claude-playground
A tiny todo CLI that exists to try out [Claude Code](https://docs.claude.com/en/docs/claude-code) features.
```sh
npm test
npm start -- add "try Claude Code"
npm start -- done 1
npm start -- ls
```
## What's set up
| File | Claude Code feature |
| --- | --- |
| `CLAUDE.md` | Project memory, loaded into every session |
| `.claude/settings.json` | Shared permissions + a hook that syntax-checks `.js` files after every edit |
| `.claude/skills/changelog/` | A custom skill — run `/changelog` |
| `.claude/agents/test-writer.md` | A custom subagent — "use the test-writer agent to…" |
## Things to try
- *"There's a bug in how ids are assigned after removing a todo — find it and fix it with a test."*
- *"Use the test-writer agent to cover `src/todo.js`."*
- *"Add a `--json` flag to `ls`."* then `/code-review`
- `/changelog` after a few commits
- Press `shift+tab` to cycle permission modes (e.g. plan mode before a bigger change)
- `/memory` to edit CLAUDE.md, `/agents` and `/hooks` to inspect the setup
+13
View File
@@ -0,0 +1,13 @@
{
"name": "claude-playground",
"version": "0.1.0",
"description": "A tiny todo CLI used as a sandbox for trying Claude Code features",
"type": "module",
"bin": { "todo": "./src/cli.js" },
"scripts": {
"start": "node src/cli.js",
"test": "node --test"
},
"engines": { "node": ">=20" },
"license": "MIT"
}
Executable
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env node
import { load, save, add, complete, remove, format } from "./todo.js";
const FILE = process.env.TODO_FILE ?? "todos.json";
const [cmd, ...args] = process.argv.slice(2);
let todos = load(FILE);
switch (cmd) {
case "add":
todos = add(todos, args.join(" "));
break;
case "done":
todos = complete(todos, Number(args[0]));
break;
case "rm":
todos = remove(todos, Number(args[0]));
break;
case "ls":
case undefined:
console.log(format(todos));
process.exit(0);
default:
console.error(`Unknown command: ${cmd}\nUsage: todo [ls | add <text> | done <id> | rm <id>]`);
process.exit(1);
}
save(FILE, todos);
console.log(format(todos));
+28
View File
@@ -0,0 +1,28 @@
import { readFileSync, writeFileSync, existsSync } from "node:fs";
export function load(path) {
if (!existsSync(path)) return [];
return JSON.parse(readFileSync(path, "utf8"));
}
export function save(path, todos) {
writeFileSync(path, JSON.stringify(todos, null, 2) + "\n");
}
export function add(todos, text) {
const id = todos.length + 1;
return [...todos, { id, text, done: false }];
}
export function complete(todos, id) {
return todos.map((t) => (t.id === id ? { ...t, done: true } : t));
}
export function remove(todos, id) {
return todos.filter((t) => t.id !== id);
}
export function format(todos) {
if (todos.length === 0) return "Nothing to do.";
return todos.map((t) => `${t.done ? "[x]" : "[ ]"} ${t.id}. ${t.text}`).join("\n");
}
+23
View File
@@ -0,0 +1,23 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { add, complete, remove, format } from "../src/todo.js";
test("add appends an open todo", () => {
const todos = add([], "buy milk");
assert.deepEqual(todos, [{ id: 1, text: "buy milk", done: false }]);
});
test("complete marks only the matching todo", () => {
const todos = complete(add(add([], "a"), "b"), 2);
assert.equal(todos[0].done, false);
assert.equal(todos[1].done, true);
});
test("remove drops the matching todo", () => {
const todos = remove(add(add([], "a"), "b"), 1);
assert.deepEqual(todos.map((t) => t.text), ["b"]);
});
test("format renders an empty list", () => {
assert.equal(format([]), "Nothing to do.");
});