Initial commit: demo todo CLI with Claude Code setup
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
10 files changed
+193
No files matched your search
@@ -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/`.
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
node_modules/
|
||||||
|
todos.json
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Personal Claude Code overrides (not shared)
|
||||||
|
.claude/settings.local.json
|
||||||
|
CLAUDE.local.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`.
|
||||||
@@ -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
|
||||||
@@ -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
@@ -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
@@ -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");
|
||||||
|
}
|
||||||
@@ -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.");
|
||||||
|
});
|
||||||
Reference in new issue
Block a user