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