commit 3cec32b9e6097c72da26cd711202380d1efc8a9a Author: christian-westerkam Date: Tue Oct 20 23:49:20 2026 +0200 Initial commit: demo todo CLI with Claude Code setup Co-Authored-By: Claude Opus 5.5 diff --git a/.claude/agents/test-writer.md b/.claude/agents/test-writer.md new file mode 100644 index 0000000..96fae0b --- /dev/null +++ b/.claude/agents/test-writer.md @@ -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/`. diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..83b43b2 --- /dev/null +++ b/.claude/settings.json @@ -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" + } + ] + } + ] + } +} diff --git a/.claude/skills/changelog/SKILL.md b/.claude/skills/changelog/SKILL.md new file mode 100644 index 0000000..d010938 --- /dev/null +++ b/.claude/skills/changelog/SKILL.md @@ -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 `. +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. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..285cf65 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +todos.json +.DS_Store + +# Personal Claude Code overrides (not shared) +.claude/settings.local.json +CLAUDE.local.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..2117ff2 --- /dev/null +++ b/CLAUDE.md @@ -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`. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ce35e50 --- /dev/null +++ b/README.md @@ -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 diff --git a/package.json b/package.json new file mode 100644 index 0000000..150c342 --- /dev/null +++ b/package.json @@ -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" +} diff --git a/src/cli.js b/src/cli.js new file mode 100755 index 0000000..90ffe77 --- /dev/null +++ b/src/cli.js @@ -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 | done | rm ]`); + process.exit(1); +} + +save(FILE, todos); +console.log(format(todos)); diff --git a/src/todo.js b/src/todo.js new file mode 100644 index 0000000..efb8343 --- /dev/null +++ b/src/todo.js @@ -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"); +} diff --git a/test/todo.test.js b/test/todo.test.js new file mode 100644 index 0000000..01687e7 --- /dev/null +++ b/test/todo.test.js @@ -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."); +});