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.
- package/README.md +251 -0
- package/dist/api/ApiError.js +10 -0
- package/dist/api/client.js +96 -0
- package/dist/commands/add.js +45 -0
- package/dist/commands/config.js +41 -0
- package/dist/commands/done.js +39 -0
- package/dist/commands/executor.js +457 -0
- package/dist/commands/list.js +86 -0
- package/dist/commands/login.js +1 -0
- package/dist/commands/loginFlow.js +86 -0
- package/dist/commands/logout.js +11 -0
- package/dist/commands/registry.js +351 -0
- package/dist/commands/resolver.js +184 -0
- package/dist/config.js +16 -0
- package/dist/index.js +77 -0
- package/dist/tui/App.js +141 -0
- package/dist/tui/components/AutocompleteOverlay.js +22 -0
- package/dist/tui/components/BottomBar.js +8 -0
- package/dist/tui/components/Breadcrumb.js +6 -0
- package/dist/tui/components/CommandInput.js +111 -0
- package/dist/tui/components/CommandPalette.js +1 -0
- package/dist/tui/components/ErrorBoundary.js +26 -0
- package/dist/tui/components/FocusMode.js +1 -0
- package/dist/tui/components/FolderItem.js +5 -0
- package/dist/tui/components/Header.js +5 -0
- package/dist/tui/components/Keybindings.js +5 -0
- package/dist/tui/components/ListItem.js +5 -0
- package/dist/tui/components/NodeRow.js +14 -0
- package/dist/tui/components/PriorityBadge.js +9 -0
- package/dist/tui/components/SearchOverlay.js +1 -0
- package/dist/tui/components/Spinner.js +23 -0
- package/dist/tui/components/StatusBadge.js +9 -0
- package/dist/tui/components/SuggestionItem.js +8 -0
- package/dist/tui/components/TopBar.js +13 -0
- package/dist/tui/components/TreeConnector.js +17 -0
- package/dist/tui/hooks/useAuth.js +37 -0
- package/dist/tui/hooks/useCommandInput.js +35 -0
- package/dist/tui/hooks/useFolders.js +16 -0
- package/dist/tui/hooks/useKeymap.js +30 -0
- package/dist/tui/hooks/useLists.js +16 -0
- package/dist/tui/hooks/useNavigation.js +15 -0
- package/dist/tui/hooks/useTree.js +93 -0
- package/dist/tui/screens/DailyScreen.js +77 -0
- package/dist/tui/screens/DashboardScreen.js +262 -0
- package/dist/tui/screens/HomeScreen.js +75 -0
- package/dist/tui/screens/ListsScreen.js +73 -0
- package/dist/tui/screens/LoginScreen.js +115 -0
- package/dist/tui/screens/NodeScreen.js +48 -0
- package/dist/tui/screens/TreeScreen.js +182 -0
- package/dist/tui/screens/WelcomeScreen.js +83 -0
- package/dist/tui/types.js +1 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/auth.js +11 -0
- package/dist/utils/logger.js +32 -0
- package/dist/utils/numbering.js +38 -0
- package/dist/utils/recents.js +24 -0
- package/dist/utils/scoring.js +29 -0
- package/dist/utils/tree.js +125 -0
- 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
|
+
[](https://www.npmjs.com/package/stratanodex)
|
|
6
|
+
[](https://nodejs.org)
|
|
7
|
+
[](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,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
|
+
}
|