sisyphi 1.1.7 → 1.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1493 -215
- package/dist/cli.js.map +1 -1
- package/dist/templates/begin.md +22 -0
- package/dist/templates/nvim-tutorial.txt +68 -0
- package/dist/templates/tutorial-demo/CLAUDE.md +14 -0
- package/dist/templates/tutorial-demo/package.json +10 -0
- package/dist/templates/tutorial-demo/server.js +76 -0
- package/dist/templates/tutorial-demo/test.js +48 -0
- package/dist/templates/tutorial-demo/todo.js +35 -0
- package/package.json +1 -1
- package/templates/begin.md +22 -0
- package/templates/nvim-tutorial.txt +68 -0
- package/templates/tutorial-demo/CLAUDE.md +14 -0
- package/templates/tutorial-demo/package.json +10 -0
- package/templates/tutorial-demo/server.js +76 -0
- package/templates/tutorial-demo/test.js +48 -0
- package/templates/tutorial-demo/todo.js +35 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Hand off a task to sisyphus multi-agent orchestration
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
!`sisyphus -h`
|
|
6
|
+
|
|
7
|
+
Run `sisyphus start` with a concise task/goal and optional background context:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
sisyphus start "your task description" -c "background context"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
**Task description** — the goal. Keep it focused: what needs to be built or fixed and what done looks like. This is the persistent objective the orchestrator sees every cycle.
|
|
14
|
+
|
|
15
|
+
**Context (`-c`)** — background info that informs the work but isn't the goal itself: relevant file paths, constraints, specs, adjacent concerns, prior findings. Rendered separately so the orchestrator can reference it without confusing it with the task.
|
|
16
|
+
|
|
17
|
+
**Context should be factual, not diagnostic.** Point to relevant files, areas of the codebase, and constraints — don't speculate on root causes or solutions, which can bias the orchestrator down the wrong path.
|
|
18
|
+
|
|
19
|
+
**Example:**
|
|
20
|
+
```bash
|
|
21
|
+
sisyphus start "Fix the JWT refresh bug — app shows blank screen on token expiry instead of redirecting to login" -c "Auth system lives in src/auth/. Key files: interceptor.ts (HTTP interceptor), token-store.ts (token persistence), refresh.ts (refresh flow). Tests in src/auth/__tests__/. Don't break the logout flow."
|
|
22
|
+
```
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
Welcome to Neovim!
|
|
2
|
+
===================
|
|
3
|
+
|
|
4
|
+
You're looking at this file in nvim. Right now you're in NORMAL MODE.
|
|
5
|
+
That means keys are commands, not text. Let's learn the basics.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
--- MOVING AROUND (Normal Mode) ---
|
|
9
|
+
|
|
10
|
+
Try these now:
|
|
11
|
+
gg Jump to the top of this file
|
|
12
|
+
G Jump to the bottom of this file
|
|
13
|
+
Ctrl+u Scroll up half a page
|
|
14
|
+
Ctrl+d Scroll down half a page
|
|
15
|
+
/Welcome Search for "Welcome" (press Enter, then n for next match)
|
|
16
|
+
|
|
17
|
+
Arrow keys work too, but h/j/k/l are the vim way:
|
|
18
|
+
h = left j = down k = up l = right
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
--- ENTERING INSERT MODE ---
|
|
22
|
+
|
|
23
|
+
Press i right now. You should see -- INSERT -- at the bottom.
|
|
24
|
+
Now you can type normally! Try typing something on the line below:
|
|
25
|
+
|
|
26
|
+
> [type here]
|
|
27
|
+
|
|
28
|
+
Press Esc when you're done to go back to normal mode.
|
|
29
|
+
|
|
30
|
+
Other ways to enter insert mode:
|
|
31
|
+
i Insert at cursor
|
|
32
|
+
a Insert after cursor
|
|
33
|
+
o Open new line below and insert
|
|
34
|
+
O Open new line above and insert
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
--- PRACTICE: EDIT THIS SECTION ---
|
|
38
|
+
|
|
39
|
+
Fix the typos below (hint: navigate to the typo, press i, fix it, press Esc):
|
|
40
|
+
|
|
41
|
+
1. The quikc brown fox jumps over the lazy dog.
|
|
42
|
+
2. Sisyphus is a multi-agnet orchestrator.
|
|
43
|
+
3. Tmux lets you spilt your terminal into panes.
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
--- SAVING AND QUITTING ---
|
|
47
|
+
|
|
48
|
+
You're almost done! Here's how to get out:
|
|
49
|
+
|
|
50
|
+
:w Enter Save the file (write)
|
|
51
|
+
:q Enter Quit (fails if unsaved changes)
|
|
52
|
+
:wq Enter Save and quit
|
|
53
|
+
:q! Enter Quit WITHOUT saving
|
|
54
|
+
ZZ Shortcut for save and quit (Shift+z twice)
|
|
55
|
+
|
|
56
|
+
Try it now: type :wq and press Enter to save your changes and close nvim.
|
|
57
|
+
The pane will close automatically and you'll be back in Claude.
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
--- BONUS (optional) ---
|
|
61
|
+
|
|
62
|
+
u Undo last change
|
|
63
|
+
Ctrl+r Redo
|
|
64
|
+
dd Delete entire line
|
|
65
|
+
yy Copy (yank) entire line
|
|
66
|
+
p Paste below cursor
|
|
67
|
+
:123 Jump to line 123
|
|
68
|
+
* Search for the word under cursor
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Todo App
|
|
2
|
+
|
|
3
|
+
A minimal Node.js todo API. No dependencies — just `node:http` and `node:test`.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
```bash
|
|
7
|
+
node server.js # Start the API on port 3456
|
|
8
|
+
node --test test.js # Run tests
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Structure
|
|
12
|
+
- `todo.js` — In-memory todo store (add, list, get, toggle, delete)
|
|
13
|
+
- `server.js` — HTTP API server
|
|
14
|
+
- `test.js` — Unit tests for the store
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import { addTodo, listTodos, getTodo, toggleTodo, deleteTodo } from './todo.js';
|
|
3
|
+
|
|
4
|
+
const PORT = 3456;
|
|
5
|
+
|
|
6
|
+
function parseBody(req) {
|
|
7
|
+
return new Promise((resolve) => {
|
|
8
|
+
let body = '';
|
|
9
|
+
req.on('data', (chunk) => (body += chunk));
|
|
10
|
+
req.on('end', () => {
|
|
11
|
+
try { resolve(JSON.parse(body)); }
|
|
12
|
+
catch { resolve({}); }
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function json(res, status, data) {
|
|
18
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
19
|
+
res.end(JSON.stringify(data, null, 2));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const server = createServer(async (req, res) => {
|
|
23
|
+
const url = new URL(req.url, `http://localhost:${PORT}`);
|
|
24
|
+
const path = url.pathname;
|
|
25
|
+
|
|
26
|
+
// GET /todos
|
|
27
|
+
if (req.method === 'GET' && path === '/todos') {
|
|
28
|
+
return json(res, 200, listTodos());
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// POST /todos
|
|
32
|
+
if (req.method === 'POST' && path === '/todos') {
|
|
33
|
+
const { title } = await parseBody(req);
|
|
34
|
+
if (!title) return json(res, 400, { error: 'title is required' });
|
|
35
|
+
return json(res, 201, addTodo(title));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// GET /todos/:id
|
|
39
|
+
const match = path.match(/^\/todos\/(\d+)$/);
|
|
40
|
+
if (match) {
|
|
41
|
+
const id = parseInt(match[1]);
|
|
42
|
+
if (req.method === 'GET') {
|
|
43
|
+
const todo = getTodo(id);
|
|
44
|
+
return todo ? json(res, 200, todo) : json(res, 404, { error: 'not found' });
|
|
45
|
+
}
|
|
46
|
+
// PATCH /todos/:id/toggle
|
|
47
|
+
if (req.method === 'PATCH' && path.endsWith('/toggle')) {
|
|
48
|
+
// BUG: this never matches because the regex above doesn't include /toggle
|
|
49
|
+
}
|
|
50
|
+
// DELETE /todos/:id
|
|
51
|
+
if (req.method === 'DELETE') {
|
|
52
|
+
return deleteTodo(id) ? json(res, 200, { deleted: true }) : json(res, 404, { error: 'not found' });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// PATCH /todos/:id/toggle
|
|
57
|
+
const toggleMatch = path.match(/^\/todos\/(\d+)\/toggle$/);
|
|
58
|
+
if (req.method === 'PATCH' && toggleMatch) {
|
|
59
|
+
const id = parseInt(toggleMatch[1]);
|
|
60
|
+
const todo = toggleTodo(id);
|
|
61
|
+
return todo ? json(res, 200, todo) : json(res, 404, { error: 'not found' });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
json(res, 404, { error: 'not found' });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
server.listen(PORT, () => {
|
|
68
|
+
console.log(`Todo API running at http://localhost:${PORT}`);
|
|
69
|
+
console.log('');
|
|
70
|
+
console.log('Endpoints:');
|
|
71
|
+
console.log(' GET /todos List all todos');
|
|
72
|
+
console.log(' POST /todos Create a todo (body: {"title": "..."})');
|
|
73
|
+
console.log(' GET /todos/:id Get a todo');
|
|
74
|
+
console.log(' PATCH /todos/:id/toggle Toggle done');
|
|
75
|
+
console.log(' DELETE /todos/:id Delete a todo');
|
|
76
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, it, beforeEach } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { addTodo, listTodos, getTodo, toggleTodo, deleteTodo, clearAll } from './todo.js';
|
|
4
|
+
|
|
5
|
+
describe('todo store', () => {
|
|
6
|
+
beforeEach(() => clearAll());
|
|
7
|
+
|
|
8
|
+
it('adds a todo', () => {
|
|
9
|
+
const todo = addTodo('Buy milk');
|
|
10
|
+
assert.strictEqual(todo.title, 'Buy milk');
|
|
11
|
+
assert.strictEqual(todo.done, false);
|
|
12
|
+
assert.ok(todo.id);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('lists todos', () => {
|
|
16
|
+
addTodo('First');
|
|
17
|
+
addTodo('Second');
|
|
18
|
+
assert.strictEqual(listTodos().length, 2);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('gets a todo by id', () => {
|
|
22
|
+
const todo = addTodo('Find me');
|
|
23
|
+
assert.deepStrictEqual(getTodo(todo.id), todo);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('returns null for missing todo', () => {
|
|
27
|
+
assert.strictEqual(getTodo(999), null);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('toggles done status', () => {
|
|
31
|
+
const todo = addTodo('Toggle me');
|
|
32
|
+
assert.strictEqual(todo.done, false);
|
|
33
|
+
toggleTodo(todo.id);
|
|
34
|
+
assert.strictEqual(todo.done, true);
|
|
35
|
+
toggleTodo(todo.id);
|
|
36
|
+
assert.strictEqual(todo.done, false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('deletes a todo', () => {
|
|
40
|
+
const todo = addTodo('Delete me');
|
|
41
|
+
assert.strictEqual(deleteTodo(todo.id), true);
|
|
42
|
+
assert.strictEqual(listTodos().length, 0);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('returns false when deleting nonexistent', () => {
|
|
46
|
+
assert.strictEqual(deleteTodo(999), false);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// In-memory todo store
|
|
2
|
+
const todos = [];
|
|
3
|
+
let nextId = 1;
|
|
4
|
+
|
|
5
|
+
export function addTodo(title) {
|
|
6
|
+
const todo = { id: nextId++, title, done: false, createdAt: new Date().toISOString() };
|
|
7
|
+
todos.push(todo);
|
|
8
|
+
return todo;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function listTodos() {
|
|
12
|
+
return [...todos];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getTodo(id) {
|
|
16
|
+
return todos.find((t) => t.id === id) || null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function toggleTodo(id) {
|
|
20
|
+
const todo = getTodo(id);
|
|
21
|
+
if (todo) todo.done = !todo.done;
|
|
22
|
+
return todo;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function deleteTodo(id) {
|
|
26
|
+
const idx = todos.findIndex((t) => t.id === id);
|
|
27
|
+
if (idx === -1) return false;
|
|
28
|
+
todos.splice(idx, 1);
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function clearAll() {
|
|
33
|
+
todos.length = 0;
|
|
34
|
+
nextId = 1;
|
|
35
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Hand off a task to sisyphus multi-agent orchestration
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
!`sisyphus -h`
|
|
6
|
+
|
|
7
|
+
Run `sisyphus start` with a concise task/goal and optional background context:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
sisyphus start "your task description" -c "background context"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
**Task description** — the goal. Keep it focused: what needs to be built or fixed and what done looks like. This is the persistent objective the orchestrator sees every cycle.
|
|
14
|
+
|
|
15
|
+
**Context (`-c`)** — background info that informs the work but isn't the goal itself: relevant file paths, constraints, specs, adjacent concerns, prior findings. Rendered separately so the orchestrator can reference it without confusing it with the task.
|
|
16
|
+
|
|
17
|
+
**Context should be factual, not diagnostic.** Point to relevant files, areas of the codebase, and constraints — don't speculate on root causes or solutions, which can bias the orchestrator down the wrong path.
|
|
18
|
+
|
|
19
|
+
**Example:**
|
|
20
|
+
```bash
|
|
21
|
+
sisyphus start "Fix the JWT refresh bug — app shows blank screen on token expiry instead of redirecting to login" -c "Auth system lives in src/auth/. Key files: interceptor.ts (HTTP interceptor), token-store.ts (token persistence), refresh.ts (refresh flow). Tests in src/auth/__tests__/. Don't break the logout flow."
|
|
22
|
+
```
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
Welcome to Neovim!
|
|
2
|
+
===================
|
|
3
|
+
|
|
4
|
+
You're looking at this file in nvim. Right now you're in NORMAL MODE.
|
|
5
|
+
That means keys are commands, not text. Let's learn the basics.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
--- MOVING AROUND (Normal Mode) ---
|
|
9
|
+
|
|
10
|
+
Try these now:
|
|
11
|
+
gg Jump to the top of this file
|
|
12
|
+
G Jump to the bottom of this file
|
|
13
|
+
Ctrl+u Scroll up half a page
|
|
14
|
+
Ctrl+d Scroll down half a page
|
|
15
|
+
/Welcome Search for "Welcome" (press Enter, then n for next match)
|
|
16
|
+
|
|
17
|
+
Arrow keys work too, but h/j/k/l are the vim way:
|
|
18
|
+
h = left j = down k = up l = right
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
--- ENTERING INSERT MODE ---
|
|
22
|
+
|
|
23
|
+
Press i right now. You should see -- INSERT -- at the bottom.
|
|
24
|
+
Now you can type normally! Try typing something on the line below:
|
|
25
|
+
|
|
26
|
+
> [type here]
|
|
27
|
+
|
|
28
|
+
Press Esc when you're done to go back to normal mode.
|
|
29
|
+
|
|
30
|
+
Other ways to enter insert mode:
|
|
31
|
+
i Insert at cursor
|
|
32
|
+
a Insert after cursor
|
|
33
|
+
o Open new line below and insert
|
|
34
|
+
O Open new line above and insert
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
--- PRACTICE: EDIT THIS SECTION ---
|
|
38
|
+
|
|
39
|
+
Fix the typos below (hint: navigate to the typo, press i, fix it, press Esc):
|
|
40
|
+
|
|
41
|
+
1. The quikc brown fox jumps over the lazy dog.
|
|
42
|
+
2. Sisyphus is a multi-agnet orchestrator.
|
|
43
|
+
3. Tmux lets you spilt your terminal into panes.
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
--- SAVING AND QUITTING ---
|
|
47
|
+
|
|
48
|
+
You're almost done! Here's how to get out:
|
|
49
|
+
|
|
50
|
+
:w Enter Save the file (write)
|
|
51
|
+
:q Enter Quit (fails if unsaved changes)
|
|
52
|
+
:wq Enter Save and quit
|
|
53
|
+
:q! Enter Quit WITHOUT saving
|
|
54
|
+
ZZ Shortcut for save and quit (Shift+z twice)
|
|
55
|
+
|
|
56
|
+
Try it now: type :wq and press Enter to save your changes and close nvim.
|
|
57
|
+
The pane will close automatically and you'll be back in Claude.
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
--- BONUS (optional) ---
|
|
61
|
+
|
|
62
|
+
u Undo last change
|
|
63
|
+
Ctrl+r Redo
|
|
64
|
+
dd Delete entire line
|
|
65
|
+
yy Copy (yank) entire line
|
|
66
|
+
p Paste below cursor
|
|
67
|
+
:123 Jump to line 123
|
|
68
|
+
* Search for the word under cursor
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Todo App
|
|
2
|
+
|
|
3
|
+
A minimal Node.js todo API. No dependencies — just `node:http` and `node:test`.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
```bash
|
|
7
|
+
node server.js # Start the API on port 3456
|
|
8
|
+
node --test test.js # Run tests
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Structure
|
|
12
|
+
- `todo.js` — In-memory todo store (add, list, get, toggle, delete)
|
|
13
|
+
- `server.js` — HTTP API server
|
|
14
|
+
- `test.js` — Unit tests for the store
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import { addTodo, listTodos, getTodo, toggleTodo, deleteTodo } from './todo.js';
|
|
3
|
+
|
|
4
|
+
const PORT = 3456;
|
|
5
|
+
|
|
6
|
+
function parseBody(req) {
|
|
7
|
+
return new Promise((resolve) => {
|
|
8
|
+
let body = '';
|
|
9
|
+
req.on('data', (chunk) => (body += chunk));
|
|
10
|
+
req.on('end', () => {
|
|
11
|
+
try { resolve(JSON.parse(body)); }
|
|
12
|
+
catch { resolve({}); }
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function json(res, status, data) {
|
|
18
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
19
|
+
res.end(JSON.stringify(data, null, 2));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const server = createServer(async (req, res) => {
|
|
23
|
+
const url = new URL(req.url, `http://localhost:${PORT}`);
|
|
24
|
+
const path = url.pathname;
|
|
25
|
+
|
|
26
|
+
// GET /todos
|
|
27
|
+
if (req.method === 'GET' && path === '/todos') {
|
|
28
|
+
return json(res, 200, listTodos());
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// POST /todos
|
|
32
|
+
if (req.method === 'POST' && path === '/todos') {
|
|
33
|
+
const { title } = await parseBody(req);
|
|
34
|
+
if (!title) return json(res, 400, { error: 'title is required' });
|
|
35
|
+
return json(res, 201, addTodo(title));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// GET /todos/:id
|
|
39
|
+
const match = path.match(/^\/todos\/(\d+)$/);
|
|
40
|
+
if (match) {
|
|
41
|
+
const id = parseInt(match[1]);
|
|
42
|
+
if (req.method === 'GET') {
|
|
43
|
+
const todo = getTodo(id);
|
|
44
|
+
return todo ? json(res, 200, todo) : json(res, 404, { error: 'not found' });
|
|
45
|
+
}
|
|
46
|
+
// PATCH /todos/:id/toggle
|
|
47
|
+
if (req.method === 'PATCH' && path.endsWith('/toggle')) {
|
|
48
|
+
// BUG: this never matches because the regex above doesn't include /toggle
|
|
49
|
+
}
|
|
50
|
+
// DELETE /todos/:id
|
|
51
|
+
if (req.method === 'DELETE') {
|
|
52
|
+
return deleteTodo(id) ? json(res, 200, { deleted: true }) : json(res, 404, { error: 'not found' });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// PATCH /todos/:id/toggle
|
|
57
|
+
const toggleMatch = path.match(/^\/todos\/(\d+)\/toggle$/);
|
|
58
|
+
if (req.method === 'PATCH' && toggleMatch) {
|
|
59
|
+
const id = parseInt(toggleMatch[1]);
|
|
60
|
+
const todo = toggleTodo(id);
|
|
61
|
+
return todo ? json(res, 200, todo) : json(res, 404, { error: 'not found' });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
json(res, 404, { error: 'not found' });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
server.listen(PORT, () => {
|
|
68
|
+
console.log(`Todo API running at http://localhost:${PORT}`);
|
|
69
|
+
console.log('');
|
|
70
|
+
console.log('Endpoints:');
|
|
71
|
+
console.log(' GET /todos List all todos');
|
|
72
|
+
console.log(' POST /todos Create a todo (body: {"title": "..."})');
|
|
73
|
+
console.log(' GET /todos/:id Get a todo');
|
|
74
|
+
console.log(' PATCH /todos/:id/toggle Toggle done');
|
|
75
|
+
console.log(' DELETE /todos/:id Delete a todo');
|
|
76
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, it, beforeEach } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { addTodo, listTodos, getTodo, toggleTodo, deleteTodo, clearAll } from './todo.js';
|
|
4
|
+
|
|
5
|
+
describe('todo store', () => {
|
|
6
|
+
beforeEach(() => clearAll());
|
|
7
|
+
|
|
8
|
+
it('adds a todo', () => {
|
|
9
|
+
const todo = addTodo('Buy milk');
|
|
10
|
+
assert.strictEqual(todo.title, 'Buy milk');
|
|
11
|
+
assert.strictEqual(todo.done, false);
|
|
12
|
+
assert.ok(todo.id);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('lists todos', () => {
|
|
16
|
+
addTodo('First');
|
|
17
|
+
addTodo('Second');
|
|
18
|
+
assert.strictEqual(listTodos().length, 2);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('gets a todo by id', () => {
|
|
22
|
+
const todo = addTodo('Find me');
|
|
23
|
+
assert.deepStrictEqual(getTodo(todo.id), todo);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('returns null for missing todo', () => {
|
|
27
|
+
assert.strictEqual(getTodo(999), null);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('toggles done status', () => {
|
|
31
|
+
const todo = addTodo('Toggle me');
|
|
32
|
+
assert.strictEqual(todo.done, false);
|
|
33
|
+
toggleTodo(todo.id);
|
|
34
|
+
assert.strictEqual(todo.done, true);
|
|
35
|
+
toggleTodo(todo.id);
|
|
36
|
+
assert.strictEqual(todo.done, false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('deletes a todo', () => {
|
|
40
|
+
const todo = addTodo('Delete me');
|
|
41
|
+
assert.strictEqual(deleteTodo(todo.id), true);
|
|
42
|
+
assert.strictEqual(listTodos().length, 0);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('returns false when deleting nonexistent', () => {
|
|
46
|
+
assert.strictEqual(deleteTodo(999), false);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// In-memory todo store
|
|
2
|
+
const todos = [];
|
|
3
|
+
let nextId = 1;
|
|
4
|
+
|
|
5
|
+
export function addTodo(title) {
|
|
6
|
+
const todo = { id: nextId++, title, done: false, createdAt: new Date().toISOString() };
|
|
7
|
+
todos.push(todo);
|
|
8
|
+
return todo;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function listTodos() {
|
|
12
|
+
return [...todos];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getTodo(id) {
|
|
16
|
+
return todos.find((t) => t.id === id) || null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function toggleTodo(id) {
|
|
20
|
+
const todo = getTodo(id);
|
|
21
|
+
if (todo) todo.done = !todo.done;
|
|
22
|
+
return todo;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function deleteTodo(id) {
|
|
26
|
+
const idx = todos.findIndex((t) => t.id === id);
|
|
27
|
+
if (idx === -1) return false;
|
|
28
|
+
todos.splice(idx, 1);
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function clearAll() {
|
|
33
|
+
todos.length = 0;
|
|
34
|
+
nextId = 1;
|
|
35
|
+
}
|