stratanodex 0.1.0

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.
Files changed (59) hide show
  1. package/README.md +251 -0
  2. package/dist/api/ApiError.js +10 -0
  3. package/dist/api/client.js +96 -0
  4. package/dist/commands/add.js +45 -0
  5. package/dist/commands/config.js +41 -0
  6. package/dist/commands/done.js +39 -0
  7. package/dist/commands/executor.js +457 -0
  8. package/dist/commands/list.js +86 -0
  9. package/dist/commands/login.js +1 -0
  10. package/dist/commands/loginFlow.js +86 -0
  11. package/dist/commands/logout.js +11 -0
  12. package/dist/commands/registry.js +351 -0
  13. package/dist/commands/resolver.js +184 -0
  14. package/dist/config.js +16 -0
  15. package/dist/index.js +77 -0
  16. package/dist/tui/App.js +141 -0
  17. package/dist/tui/components/AutocompleteOverlay.js +22 -0
  18. package/dist/tui/components/BottomBar.js +8 -0
  19. package/dist/tui/components/Breadcrumb.js +6 -0
  20. package/dist/tui/components/CommandInput.js +111 -0
  21. package/dist/tui/components/CommandPalette.js +1 -0
  22. package/dist/tui/components/ErrorBoundary.js +26 -0
  23. package/dist/tui/components/FocusMode.js +1 -0
  24. package/dist/tui/components/FolderItem.js +5 -0
  25. package/dist/tui/components/Header.js +5 -0
  26. package/dist/tui/components/Keybindings.js +5 -0
  27. package/dist/tui/components/ListItem.js +5 -0
  28. package/dist/tui/components/NodeRow.js +14 -0
  29. package/dist/tui/components/PriorityBadge.js +9 -0
  30. package/dist/tui/components/SearchOverlay.js +1 -0
  31. package/dist/tui/components/Spinner.js +23 -0
  32. package/dist/tui/components/StatusBadge.js +9 -0
  33. package/dist/tui/components/SuggestionItem.js +8 -0
  34. package/dist/tui/components/TopBar.js +13 -0
  35. package/dist/tui/components/TreeConnector.js +17 -0
  36. package/dist/tui/hooks/useAuth.js +37 -0
  37. package/dist/tui/hooks/useCommandInput.js +35 -0
  38. package/dist/tui/hooks/useFolders.js +16 -0
  39. package/dist/tui/hooks/useKeymap.js +30 -0
  40. package/dist/tui/hooks/useLists.js +16 -0
  41. package/dist/tui/hooks/useNavigation.js +15 -0
  42. package/dist/tui/hooks/useTree.js +93 -0
  43. package/dist/tui/screens/DailyScreen.js +77 -0
  44. package/dist/tui/screens/DashboardScreen.js +262 -0
  45. package/dist/tui/screens/HomeScreen.js +75 -0
  46. package/dist/tui/screens/ListsScreen.js +73 -0
  47. package/dist/tui/screens/LoginScreen.js +115 -0
  48. package/dist/tui/screens/NodeScreen.js +48 -0
  49. package/dist/tui/screens/TreeScreen.js +182 -0
  50. package/dist/tui/screens/WelcomeScreen.js +83 -0
  51. package/dist/tui/types.js +1 -0
  52. package/dist/types/index.js +1 -0
  53. package/dist/utils/auth.js +11 -0
  54. package/dist/utils/logger.js +32 -0
  55. package/dist/utils/numbering.js +38 -0
  56. package/dist/utils/recents.js +24 -0
  57. package/dist/utils/scoring.js +29 -0
  58. package/dist/utils/tree.js +125 -0
  59. package/package.json +74 -0
package/README.md ADDED
@@ -0,0 +1,251 @@
1
+ # StrataNodex CLI
2
+
3
+ > A keyboard-driven, terminal-based task manager for power users.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/stratanodex)](https://www.npmjs.com/package/stratanodex)
6
+ [![Node.js >=20](https://img.shields.io/badge/node-%3E%3D20-brightgreen)](https://nodejs.org)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
8
+
9
+ ---
10
+
11
+ ## What is StrataNodex CLI?
12
+
13
+ StrataNodex CLI is the terminal-first interface for **StrataNodex** — a hierarchical task manager built around the idea that tasks live in a **tree**, not a flat list.
14
+
15
+ ```
16
+ ██▀▀ ▀█▀ █▀█ ▄▀█ ▀█▀ ▄▀█ █▄ █ █▀█ █▀▄ █▀▀ ▀▄▀
17
+ ▄██ █ █▀▄ █▀█ █ █▀█ █ ▀█ █▄█ █▄▀ ██▄ █ █ v0.1.0 ● connected
18
+ ──────────────────────────────────────────────────────
19
+ 📁 Folders
20
+
21
+ › my project
22
+ › side hustle
23
+
24
+ [↑↓] navigate [Enter] open [n] new [e] edit [d] delete [q] quit
25
+ ──────────────────────────────────────────────────────
26
+ > type / for commands
27
+ / for commands ↑↓ navigate TAB complete ESC close Enter execute
28
+ ```
29
+
30
+ **3-panel TUI layout:**
31
+
32
+ - **Top** — fixed header with version and login state
33
+ - **Middle** — scrollable content (folders → lists → nodes)
34
+ - **Bottom** — smart command bar with live autocomplete
35
+
36
+ ---
37
+
38
+ ## Quick Setup
39
+
40
+ > **Prerequisites:** [Node.js ≥20](https://nodejs.org) must be installed.
41
+
42
+ ### Install from npm (recommended)
43
+
44
+ ```bash
45
+ npm install -g stratanodex
46
+ stratanodex
47
+ ```
48
+
49
+ ### Run from source
50
+
51
+ ```bash
52
+ # 1. Clone the repo
53
+ git clone https://github.com/pranavdadhe1806/StrataNodex-CLI.git
54
+ cd StrataNodex-CLI
55
+
56
+ # 2. Install dependencies
57
+ npm install
58
+
59
+ # 3. Run the CLI
60
+ npm run dev
61
+ ```
62
+
63
+ On first launch you'll see the login screen. It opens your browser to [stratanodex.online](https://stratanodex.online) for authentication, and once you log in, the CLI picks it up automatically and drops you into the home screen.
64
+
65
+ ---
66
+
67
+ ## How It Works
68
+
69
+ ```
70
+ ┌─────────────┐ ┌──────────────────────┐ ┌────────────────────────┐
71
+ │ CLI (you) │───────▶│ Backend (Render) │◀───────│ Landing Page (Vercel) │
72
+ │ npm run dev│ API │ REST API + Auth │ Auth │ Browser-based login │
73
+ └─────────────┘ └──────────────────────┘ └────────────────────────┘
74
+ ```
75
+
76
+ 1. You run `npm run dev` — the CLI starts in your terminal
77
+ 2. On first use, it opens your browser to the StrataNodex landing page
78
+ 3. You log in (email + password) — the CLI detects the login automatically
79
+ 4. You're now authenticated and can manage folders, lists, and tasks
80
+
81
+ **The backend and landing page are already deployed.** You only need this CLI repo.
82
+
83
+ ---
84
+
85
+ ## Usage
86
+
87
+ ### Navigation
88
+
89
+ | Key | Action |
90
+ | --------- | -------------------------- |
91
+ | `↑` / `↓` | Move cursor |
92
+ | `Enter` | Open selected item |
93
+ | `b` | Go back |
94
+ | `q` | Quit |
95
+ | `/` | Open command bar |
96
+ | `TAB` | Complete autocomplete |
97
+ | `ESC` | Close autocomplete overlay |
98
+
99
+ ### Commands
100
+
101
+ Every command starts with `/`. Type `/` in the command bar to see suggestions.
102
+
103
+ #### Global (available on all screens)
104
+
105
+ | Command | Description |
106
+ | ------------ | ---------------------------- |
107
+ | `/back` | Go back to previous screen |
108
+ | `/home` | Jump to Folders screen |
109
+ | `/folders` | Navigate to Folders screen |
110
+ | `/dashboard` | Score + streak + 7-day chart |
111
+ | `/help` | Show available commands |
112
+ | `/logout` | Log out and clear token |
113
+ | `/whoami` | Show username + streak |
114
+ | `/tags` | List all tags |
115
+
116
+ #### Folders screen
117
+
118
+ | Command | Args | Description |
119
+ | ---------------- | ------------------- | ------------------- |
120
+ | `/new folder` | `name` | Create a new folder |
121
+ | `/edit folder` | `name` → `new-name` | Rename a folder |
122
+ | `/delete folder` | `name` | Delete a folder |
123
+
124
+ #### Lists screen (inside a folder)
125
+
126
+ | Command | Args | Description |
127
+ | -------------- | ------------------- | ----------------- |
128
+ | `/new list` | `name` | Create a new list |
129
+ | `/edit list` | `name` → `new-name` | Rename a list |
130
+ | `/delete list` | `name` | Delete a list |
131
+
132
+ #### Nodes screen (inside a list)
133
+
134
+ | Command | Args | Description |
135
+ | --------------------------- | ------------------------------------- | -------------------------------------- |
136
+ | `/add node` | `title` | Add a new root task |
137
+ | `/add sub-node` | `title` | Add sub-task under selected node |
138
+ | `/add sub-node` | `index title` | Add sub-task under node at index (1.2) |
139
+ | `/done` | `index or title` | Mark a task as DONE |
140
+ | `/delete node` | `index or title` | Delete a task |
141
+ | `/move node` | `ref` → `list-name` | Move to another list |
142
+ | `/edit node ... title` | `ref` → `new title` | Rename a task |
143
+ | `/edit node ... status` | `ref` → `NOT-DONE\|IN-PROGRESS\|DONE` | Change status |
144
+ | `/edit node ... priority` | `ref` → `LOW\|MEDIUM\|HIGH` | Change priority |
145
+ | `/edit node ... start-date` | `ref` → `DD-MM-YYYY` | Set start date |
146
+ | `/edit node ... end-date` | `ref` → `DD-MM-YYYY` | Set end date |
147
+ | `/add node ... tag` | `ref` → `tag-name` | Add a tag |
148
+ | `/add node ... note` | `ref` → `text` | Add a note |
149
+ | `/delete node ... note` | `ref` | Remove note |
150
+
151
+ ---
152
+
153
+ ## Autocomplete
154
+
155
+ Autocomplete is **context-aware** — it knows which screen you're on and which nodes are in the current list.
156
+
157
+ ```
158
+ Stage 1 /edit no → /edit node
159
+ Stage 2 /edit node → 1. Fix login bug 2. Review PR
160
+ Stage 3 /edit node 1 → title / status / priority / start-date ...
161
+ Stage 4 /edit node 1 sta → start-date (DD-MM-YYYY hint)
162
+ ```
163
+
164
+ - `↑`/`↓` to navigate suggestions
165
+ - `TAB` fills the selected suggestion
166
+ - `ESC` closes without clearing input
167
+
168
+ ---
169
+
170
+ ## Example Workflow
171
+
172
+ ```bash
173
+ npm run dev # Launch the CLI
174
+
175
+ # After logging in:
176
+ /new folder Work # Create a folder
177
+ # Press Enter on "Work" to open it
178
+ /new list Sprint 1 # Create a list
179
+ # Press Enter on "Sprint 1"
180
+ /add node Fix login bug # Add tasks
181
+ /add node Review PR
182
+ /add node Write docs
183
+ /done 1 # Mark "Fix login bug" as done
184
+ /edit node 2 priority HIGH # Set priority
185
+ /add node 1 tag frontend # Tag a task
186
+ /dashboard # View your score + streaks
187
+ ```
188
+
189
+ ---
190
+
191
+ ## Environment Variables (optional)
192
+
193
+ These are only needed if you want to override defaults. For normal use, **you don't need any env file.**
194
+
195
+ | Variable | Default | Description |
196
+ | --------------------- | -------------------------------- | ---------------------- |
197
+ | `STRATANODEX_API_URL` | `https://api.stratanodex.online` | Override API base URL |
198
+ | `STRATANODEX_VERBOSE` | `false` | Enable verbose logging |
199
+ | `NO_COLOR` | unset | Disable all colors |
200
+
201
+ To use local overrides, copy the example file:
202
+
203
+ ```bash
204
+ cp .env.example .env
205
+ # Edit .env with your values
206
+ ```
207
+
208
+ ---
209
+
210
+ ## Project Structure
211
+
212
+ ```
213
+ src/
214
+ api/ ← Axios client + ApiError (all HTTP calls)
215
+ commands/ ← Command registry, resolver, executor + individual commands
216
+ tui/ ← Ink React TUI (screens, hooks, components)
217
+ types/ ← Shared TypeScript types
218
+ utils/ ← Numbering, tree, scoring, logger, auth helpers
219
+ config.ts ← Config resolution (env → store → defaults)
220
+ index.ts ← Entry point
221
+ ```
222
+
223
+ ---
224
+
225
+ ## Development Scripts
226
+
227
+ ```bash
228
+ npm run dev # Run with tsx (no build step)
229
+ npm run build # Compile TypeScript to dist/
230
+ npm run test # Run tests (Vitest)
231
+ npm run typecheck # Type-check without emitting
232
+ npm run lint # ESLint
233
+ npm run format # Prettier
234
+ ```
235
+
236
+ ---
237
+
238
+ ## Tech Stack
239
+
240
+ - **Runtime:** Node.js ≥ 20
241
+ - **Language:** TypeScript (ESM)
242
+ - **TUI Framework:** [Ink](https://github.com/vadimdemedes/ink) (React for terminals)
243
+ - **HTTP Client:** Axios
244
+ - **Config:** [Conf](https://github.com/sindresorhus/conf) (persistent local config)
245
+ - **Testing:** Vitest
246
+
247
+ ---
248
+
249
+ ## License
250
+
251
+ MIT © 2024 pranavdadhe1806
@@ -0,0 +1,10 @@
1
+ export class ApiError extends Error {
2
+ statusCode;
3
+ raw;
4
+ constructor(statusCode, message, raw) {
5
+ super(message);
6
+ this.statusCode = statusCode;
7
+ this.raw = raw;
8
+ this.name = 'ApiError';
9
+ }
10
+ }
@@ -0,0 +1,96 @@
1
+ import axios from 'axios';
2
+ import { getConfig } from '../config.js';
3
+ import { getToken, clearToken } from '../utils/auth.js';
4
+ import { logger } from '../utils/logger.js';
5
+ import { ApiError } from './ApiError.js';
6
+ const http = axios.create({
7
+ timeout: 30_000, // 30s — Render free-tier cold starts can take up to ~30s
8
+ });
9
+ http.interceptors.request.use((config) => {
10
+ config.baseURL = getConfig().apiUrl;
11
+ const token = getToken();
12
+ if (token) {
13
+ config.headers.Authorization = `Bearer ${token}`;
14
+ }
15
+ return config;
16
+ });
17
+ http.interceptors.response.use((response) => response, (error) => {
18
+ if (axios.isAxiosError(error) && error.response) {
19
+ const { status, data } = error.response;
20
+ const rawError = data?.error;
21
+ const msg = typeof rawError === 'string'
22
+ ? rawError
23
+ : rawError && typeof rawError === 'object' && 'fieldErrors' in rawError
24
+ ? Object.entries(rawError.fieldErrors)
25
+ .map(([k, v]) => `${k}: ${v.join(', ')}`)
26
+ .join('; ')
27
+ : 'An unexpected error occurred.';
28
+ logger.error(`API ${status}: ${msg}`, { status, url: error.config?.url });
29
+ if (status === 401) {
30
+ clearToken();
31
+ throw new ApiError(401, 'Session expired. Please log in again.', error);
32
+ }
33
+ if (status === 429) {
34
+ throw new ApiError(429, 'Too many requests, please wait a moment.', error);
35
+ }
36
+ if (status >= 500) {
37
+ throw new ApiError(status, typeof rawError === 'string' ? rawError : 'Server error. Try again later.', error);
38
+ }
39
+ throw new ApiError(status, msg, error);
40
+ }
41
+ logger.error('Network error: cannot reach server');
42
+ throw new ApiError(0, 'Cannot reach server. Check your connection.', error);
43
+ });
44
+ // ── Health ────────────────────────────────────────────────────────────────────
45
+ export const healthCheck = () => http.get('/health').then((r) => r.data);
46
+ // ── Auth ──────────────────────────────────────────────────────────────────────
47
+ export const login = (email, password) => http.post('/api/auth/login', { email, password }).then((r) => r.data);
48
+ export const verify2FA = (userId, code) => http
49
+ .post('/api/auth/2fa/verify', { userId, code })
50
+ .then((r) => r.data);
51
+ export const getMe = () => http.get('/api/auth/me').then((r) => r.data);
52
+ // ── Folders ───────────────────────────────────────────────────────────────────
53
+ export const getFolders = () => http.get('/api/folders').then((r) => r.data);
54
+ export const createFolder = (name, position) => http.post('/api/folders', { name, position }).then((r) => r.data);
55
+ export const updateFolder = (id, data) => http.patch(`/api/folders/${id}`, data).then((r) => r.data);
56
+ export const deleteFolder = (id) => http.delete(`/api/folders/${id}`).then(() => undefined);
57
+ // ── Lists ─────────────────────────────────────────────────────────────────────
58
+ export const getLists = (folderId) => http.get(`/api/folders/${folderId}/lists`).then((r) => r.data);
59
+ export const createList = (name, folderId, position) => http.post('/api/lists', { name, folderId, position }).then((r) => r.data);
60
+ export const updateList = (id, data) => http.patch(`/api/lists/${id}`, data).then((r) => r.data);
61
+ export const deleteList = (id) => http.delete(`/api/lists/${id}`).then(() => undefined);
62
+ // ── Nodes ─────────────────────────────────────────────────────────────────────
63
+ export const getNodes = (listId) => http.get(`/api/lists/${listId}/nodes`).then((r) => r.data);
64
+ export const getNode = (id) => http.get(`/api/nodes/${id}`).then((r) => r.data);
65
+ export const createRootNode = (listId, data) => http.post(`/api/lists/${listId}/nodes`, { ...data, listId }).then((r) => r.data);
66
+ export const createChildNode = (parentId, data) => http.post(`/api/nodes/${parentId}/children`, data).then((r) => r.data);
67
+ export const updateNode = (id, data) => http.patch(`/api/nodes/${id}`, data).then((r) => r.data);
68
+ export const moveNode = (id, parentId, position) => http.patch(`/api/nodes/${id}/move`, { parentId, position }).then((r) => r.data);
69
+ export const deleteNode = (id) => http.delete(`/api/nodes/${id}`).then(() => undefined);
70
+ // ── Tags ──────────────────────────────────────────────────────────────────────
71
+ export const getTags = (listId) => http.get('/api/tags', { params: listId ? { listId } : {} }).then((r) => r.data);
72
+ export const createTag = (name, color, listId) => http.post('/api/tags', { name, color, listId }).then((r) => r.data);
73
+ export const updateTag = (id, data) => http.patch(`/api/tags/${id}`, data).then((r) => r.data);
74
+ export const deleteTag = (id) => http.delete(`/api/tags/${id}`).then(() => undefined);
75
+ export const attachTag = (nodeId, tagId) => http.post(`/api/nodes/${nodeId}/tags/${tagId}`).then(() => undefined);
76
+ export const detachTag = (nodeId, tagId) => http.delete(`/api/nodes/${nodeId}/tags/${tagId}`).then(() => undefined);
77
+ // ── Daily ─────────────────────────────────────────────────────────────────────
78
+ export const getDailyToday = () => http.get('/api/daily/today').then((r) => r.data);
79
+ export const getDailyOverdue = () => http.get('/api/daily/overdue').then((r) => r.data);
80
+ export const getDailyScore = (date) => http.get(`/api/daily/${date}`).then((r) => r.data);
81
+ // ── Scores ────────────────────────────────────────────────────────────────────
82
+ export const getScores = (limit, listId) => http.get('/api/scores', { params: { limit, listId } }).then((r) => r.data);
83
+ export const getStreak = () => http.get('/api/scores/streak').then((r) => r.data);
84
+ // ── CLI Session (browser-based auth handshake) ────────────────────────────────
85
+ // Backend mounts these at /api/auth/cli-session (see app.ts)
86
+ export const createCliSession = () => http
87
+ .post('/api/auth/cli-session')
88
+ .then((r) => ({
89
+ code: r.data.code,
90
+ }));
91
+ export const pollCliSession = (code) => http.get(`/api/auth/cli-session/${code}`).then((r) => {
92
+ // Backend returns { pending: true } or { token: string }
93
+ if (r.data.token)
94
+ return { status: 'complete', token: r.data.token };
95
+ return { status: 'pending' };
96
+ });
@@ -0,0 +1,45 @@
1
+ import Conf from 'conf';
2
+ import chalk from 'chalk';
3
+ import { getNodes, createRootNode, createChildNode } from '../api/client.js';
4
+ import { assignNumbers, flattenTree } from '../utils/numbering.js';
5
+ import { ApiError } from '../api/ApiError.js';
6
+ const store = new Conf({ projectName: 'stratanodex' });
7
+ export async function runAdd(title, options) {
8
+ if (!options.list) {
9
+ console.log(chalk.red('✗ --list <listId> is required. Use: stratanodex list to see list IDs.'));
10
+ return;
11
+ }
12
+ const listId = options.list;
13
+ try {
14
+ if (!options.parent) {
15
+ const node = await createRootNode(listId, { title });
16
+ store.set('lastListId', listId);
17
+ console.log(chalk.green(`✓ Added: ${node.title}`));
18
+ return;
19
+ }
20
+ const nodes = await getNodes(listId);
21
+ const flat = flattenTree(nodes);
22
+ const numberMap = assignNumbers(nodes);
23
+ const byNumber = new Map();
24
+ for (const [id, num] of numberMap) {
25
+ byNumber.set(num, id);
26
+ }
27
+ const parentId = byNumber.get(options.parent);
28
+ if (!parentId) {
29
+ console.log(chalk.red(`✗ Node "${options.parent}" not found in list.`));
30
+ return;
31
+ }
32
+ const parentNode = flat.find((n) => n.id === parentId);
33
+ const child = await createChildNode(parentId, { title });
34
+ store.set('lastListId', listId);
35
+ console.log(chalk.green(`✓ Added: ${child.title} (under: ${parentNode?.title ?? parentId})`));
36
+ }
37
+ catch (err) {
38
+ if (err instanceof ApiError) {
39
+ console.log(chalk.red(`✗ ${err.message}`));
40
+ }
41
+ else {
42
+ console.log(chalk.red('✗ Unexpected error.'));
43
+ }
44
+ }
45
+ }
@@ -0,0 +1,41 @@
1
+ import Conf from 'conf';
2
+ import chalk from 'chalk';
3
+ import { getConfig, saveApiUrl } from '../config.js';
4
+ const ALLOWED_KEYS = ['apiUrl', 'verbose'];
5
+ const store = new Conf({ projectName: 'stratanodex' });
6
+ export function runConfig(subcommand, key, value) {
7
+ if (subcommand === 'list') {
8
+ const cfg = getConfig();
9
+ console.log(`apiUrl ${cfg.apiUrl}`);
10
+ console.log(`verbose ${cfg.verbose}`);
11
+ return;
12
+ }
13
+ if (subcommand === 'get') {
14
+ if (!key || !ALLOWED_KEYS.includes(key)) {
15
+ console.log(chalk.red(`✗ Unknown config key: ${key}`));
16
+ return;
17
+ }
18
+ const cfg = getConfig();
19
+ console.log(cfg[key]);
20
+ return;
21
+ }
22
+ if (subcommand === 'set') {
23
+ if (!key || !ALLOWED_KEYS.includes(key)) {
24
+ console.log(chalk.red(`✗ Unknown config key: ${key}`));
25
+ return;
26
+ }
27
+ if (value === undefined) {
28
+ console.log(chalk.red('✗ A value is required.'));
29
+ return;
30
+ }
31
+ if (key === 'apiUrl') {
32
+ saveApiUrl(value);
33
+ }
34
+ else {
35
+ store.set(key, value);
36
+ }
37
+ console.log(chalk.green(`✓ Set ${key} = ${value}`));
38
+ return;
39
+ }
40
+ console.log(chalk.red(`✗ Unknown subcommand: ${subcommand}`));
41
+ }
@@ -0,0 +1,39 @@
1
+ import Conf from 'conf';
2
+ import chalk from 'chalk';
3
+ import { getNodes, updateNode } from '../api/client.js';
4
+ import { assignNumbers } from '../utils/numbering.js';
5
+ import { flattenTree } from '../utils/tree.js';
6
+ import { ApiError } from '../api/ApiError.js';
7
+ const store = new Conf({ projectName: 'stratanodex' });
8
+ export async function runDone(number, options) {
9
+ const listId = options.list ?? store.get('lastListId');
10
+ if (!listId) {
11
+ console.log(chalk.red('✗ No active list. Run: stratanodex list -d 1 first, or use: stratanodex done <number> --list <listId>'));
12
+ return;
13
+ }
14
+ try {
15
+ const nodes = await getNodes(listId);
16
+ const numberMap = assignNumbers(nodes);
17
+ const flat = flattenTree(nodes);
18
+ const byNumber = new Map();
19
+ for (const [id, num] of numberMap) {
20
+ byNumber.set(num, id);
21
+ }
22
+ const nodeId = byNumber.get(number);
23
+ if (!nodeId) {
24
+ console.log(chalk.red(`✗ Node "${number}" not found.`));
25
+ return;
26
+ }
27
+ const updated = await updateNode(nodeId, { status: 'DONE' });
28
+ const found = flat.find((n) => n.id === nodeId);
29
+ console.log(chalk.green(`✓ Done: ${updated.title ?? found?.title}`));
30
+ }
31
+ catch (err) {
32
+ if (err instanceof ApiError) {
33
+ console.log(chalk.red(`✗ ${err.message}`));
34
+ }
35
+ else {
36
+ console.log(chalk.red('✗ Unexpected error.'));
37
+ }
38
+ }
39
+ }