sisyphi 1.1.8 → 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 +854 -160
- package/dist/cli.js.map +1 -1
- 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/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,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,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
|
+
}
|