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
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
// Command Registry — single source of truth for all CLI commands.
|
|
2
|
+
// Drives both autocomplete suggestions and executor dispatch.
|
|
3
|
+
export const COMMAND_REGISTRY = [
|
|
4
|
+
// ─── GLOBAL ───────────────────────────────────────────────────────────────
|
|
5
|
+
{
|
|
6
|
+
command: '/back',
|
|
7
|
+
args: [],
|
|
8
|
+
screens: ['global'],
|
|
9
|
+
description: 'Go back to previous screen',
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
command: '/home',
|
|
13
|
+
args: [],
|
|
14
|
+
screens: ['global'],
|
|
15
|
+
description: 'Jump to Folders screen',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
command: '/folders',
|
|
19
|
+
args: [],
|
|
20
|
+
screens: ['global'],
|
|
21
|
+
description: 'Navigate to Folders screen',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
command: '/dashboard',
|
|
25
|
+
args: [],
|
|
26
|
+
screens: ['global'],
|
|
27
|
+
description: 'Navigate to dashboard (score + streaks + weekly graph)',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
command: '/help',
|
|
31
|
+
args: [],
|
|
32
|
+
screens: ['global'],
|
|
33
|
+
description: 'Show available commands for current screen',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
command: '/logout',
|
|
37
|
+
args: [],
|
|
38
|
+
screens: ['global'],
|
|
39
|
+
description: 'Log out of current CLI session',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
command: '/whoami',
|
|
43
|
+
args: [],
|
|
44
|
+
screens: ['global'],
|
|
45
|
+
description: 'Show logged-in user + current streak info',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
command: '/tags',
|
|
49
|
+
args: [],
|
|
50
|
+
screens: ['global'],
|
|
51
|
+
description: 'List all tags used across all lists',
|
|
52
|
+
},
|
|
53
|
+
// ─── FOLDERS SCREEN ────────────────────────────────────────────────────────
|
|
54
|
+
{
|
|
55
|
+
command: '/new folder',
|
|
56
|
+
args: [{ name: 'folder-name', type: 'text', placeholder: 'folder name' }],
|
|
57
|
+
screens: ['folders'],
|
|
58
|
+
description: 'Create a new folder',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
command: '/edit folder',
|
|
62
|
+
args: [
|
|
63
|
+
{ name: 'folder-name', type: 'text', placeholder: 'existing folder name' },
|
|
64
|
+
{ name: 'new-folder-name', type: 'text', placeholder: 'new name' },
|
|
65
|
+
],
|
|
66
|
+
screens: ['folders'],
|
|
67
|
+
description: 'Rename a folder',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
command: '/delete folder',
|
|
71
|
+
args: [{ name: 'folder-name', type: 'text', placeholder: 'folder name' }],
|
|
72
|
+
screens: ['folders'],
|
|
73
|
+
description: 'Delete a folder',
|
|
74
|
+
},
|
|
75
|
+
// ─── LISTS SCREEN ──────────────────────────────────────────────────────────
|
|
76
|
+
{
|
|
77
|
+
command: '/new list',
|
|
78
|
+
args: [{ name: 'list-name', type: 'text', placeholder: 'list name' }],
|
|
79
|
+
screens: ['lists'],
|
|
80
|
+
description: 'Create a new list',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
command: '/edit list',
|
|
84
|
+
args: [
|
|
85
|
+
{ name: 'list-name', type: 'text', placeholder: 'existing list name' },
|
|
86
|
+
{ name: 'new-list-name', type: 'text', placeholder: 'new name' },
|
|
87
|
+
],
|
|
88
|
+
screens: ['lists'],
|
|
89
|
+
description: 'Rename a list',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
command: '/delete list',
|
|
93
|
+
args: [{ name: 'list-name', type: 'text', placeholder: 'list name' }],
|
|
94
|
+
screens: ['lists'],
|
|
95
|
+
description: 'Delete a list',
|
|
96
|
+
},
|
|
97
|
+
// ─── NODES SCREEN ─────────────────────────────────────────────────────────
|
|
98
|
+
{
|
|
99
|
+
command: '/add node',
|
|
100
|
+
args: [{ name: 'node-title', type: 'text', placeholder: 'node title' }],
|
|
101
|
+
screens: ['nodes'],
|
|
102
|
+
description: 'Create a new root node',
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
command: '/add sub-node',
|
|
106
|
+
args: [{ name: 'node-title', type: 'text', placeholder: '[parent-index] sub-node title' }],
|
|
107
|
+
screens: ['nodes'],
|
|
108
|
+
description: 'Add sub-node under selected node (or specify parent index)',
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
command: '/done',
|
|
112
|
+
args: [{ name: 'index-or-title', type: 'index-or-title', placeholder: '1 or "task title"' }],
|
|
113
|
+
screens: ['nodes'],
|
|
114
|
+
description: 'Shorthand: set node status to DONE',
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
command: '/delete node',
|
|
118
|
+
args: [{ name: 'index-or-title', type: 'index-or-title', placeholder: '1 or "task title"' }],
|
|
119
|
+
screens: ['nodes'],
|
|
120
|
+
description: 'Delete a node',
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
command: '/move node',
|
|
124
|
+
args: [
|
|
125
|
+
{ name: 'index-or-title', type: 'index-or-title', placeholder: '1 or "task title"' },
|
|
126
|
+
{ name: 'list-name', type: 'text', placeholder: 'destination list name' },
|
|
127
|
+
],
|
|
128
|
+
screens: ['nodes'],
|
|
129
|
+
description: 'Move node to another list',
|
|
130
|
+
},
|
|
131
|
+
// /edit node ... <property>
|
|
132
|
+
{
|
|
133
|
+
command: '/edit node ... title',
|
|
134
|
+
args: [
|
|
135
|
+
{ name: 'index-or-title', type: 'index-or-title', placeholder: '1 or "task title"' },
|
|
136
|
+
{ name: 'new-title', type: 'text', placeholder: 'new title' },
|
|
137
|
+
],
|
|
138
|
+
screens: ['nodes'],
|
|
139
|
+
description: 'Edit node title',
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
command: '/edit node ... position',
|
|
143
|
+
args: [
|
|
144
|
+
{ name: 'index-or-title', type: 'index-or-title', placeholder: '1 or "task title"' },
|
|
145
|
+
{ name: 'new-index', type: 'number', placeholder: '2' },
|
|
146
|
+
],
|
|
147
|
+
screens: ['nodes'],
|
|
148
|
+
description: 'Reorder node',
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
command: '/edit node ... status',
|
|
152
|
+
args: [
|
|
153
|
+
{ name: 'index-or-title', type: 'index-or-title', placeholder: '1 or "task title"' },
|
|
154
|
+
{ name: 'status', type: 'status', placeholder: 'NOT-DONE | IN-PROGRESS | DONE' },
|
|
155
|
+
],
|
|
156
|
+
screens: ['nodes'],
|
|
157
|
+
description: 'Change node status',
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
command: '/edit node ... priority',
|
|
161
|
+
args: [
|
|
162
|
+
{ name: 'index-or-title', type: 'index-or-title', placeholder: '1 or "task title"' },
|
|
163
|
+
{ name: 'priority', type: 'priority', placeholder: 'LOW | MEDIUM | HIGH' },
|
|
164
|
+
],
|
|
165
|
+
screens: ['nodes'],
|
|
166
|
+
description: 'Change node priority',
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
command: '/edit node ... start-date',
|
|
170
|
+
args: [
|
|
171
|
+
{ name: 'index-or-title', type: 'index-or-title', placeholder: '1 or "task title"' },
|
|
172
|
+
{ name: 'start-date', type: 'date', placeholder: 'DD-MM-YYYY' },
|
|
173
|
+
],
|
|
174
|
+
screens: ['nodes'],
|
|
175
|
+
description: 'Edit start date',
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
command: '/edit node ... start-time',
|
|
179
|
+
args: [
|
|
180
|
+
{ name: 'index-or-title', type: 'index-or-title', placeholder: '1 or "task title"' },
|
|
181
|
+
{ name: 'start-time', type: 'time', placeholder: 'HH:MM AM/PM' },
|
|
182
|
+
],
|
|
183
|
+
screens: ['nodes'],
|
|
184
|
+
description: 'Edit start time',
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
command: '/edit node ... end-date',
|
|
188
|
+
args: [
|
|
189
|
+
{ name: 'index-or-title', type: 'index-or-title', placeholder: '1 or "task title"' },
|
|
190
|
+
{ name: 'end-date', type: 'date', placeholder: 'DD-MM-YYYY' },
|
|
191
|
+
],
|
|
192
|
+
screens: ['nodes'],
|
|
193
|
+
description: 'Edit end date',
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
command: '/edit node ... end-time',
|
|
197
|
+
args: [
|
|
198
|
+
{ name: 'index-or-title', type: 'index-or-title', placeholder: '1 or "task title"' },
|
|
199
|
+
{ name: 'end-time', type: 'time', placeholder: 'HH:MM AM/PM' },
|
|
200
|
+
],
|
|
201
|
+
screens: ['nodes'],
|
|
202
|
+
description: 'Edit end time',
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
command: '/edit node ... tag',
|
|
206
|
+
args: [
|
|
207
|
+
{ name: 'index-or-title', type: 'index-or-title', placeholder: '1 or "task title"' },
|
|
208
|
+
{ name: 'tag-name', type: 'text', placeholder: 'tag name' },
|
|
209
|
+
],
|
|
210
|
+
screens: ['nodes'],
|
|
211
|
+
description: 'Edit node tag',
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
command: '/edit node ... note',
|
|
215
|
+
args: [
|
|
216
|
+
{ name: 'index-or-title', type: 'index-or-title', placeholder: '1 or "task title"' },
|
|
217
|
+
{ name: 'new-note', type: 'text', placeholder: 'note text' },
|
|
218
|
+
],
|
|
219
|
+
screens: ['nodes'],
|
|
220
|
+
description: 'Edit note',
|
|
221
|
+
},
|
|
222
|
+
// /add node ... <property>
|
|
223
|
+
{
|
|
224
|
+
command: '/add node ... start-date',
|
|
225
|
+
args: [
|
|
226
|
+
{ name: 'index-or-title', type: 'index-or-title', placeholder: '1 or "task title"' },
|
|
227
|
+
{ name: 'start-date', type: 'date', placeholder: 'DD-MM-YYYY' },
|
|
228
|
+
],
|
|
229
|
+
screens: ['nodes'],
|
|
230
|
+
description: 'Add start date',
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
command: '/add node ... start-time',
|
|
234
|
+
args: [
|
|
235
|
+
{ name: 'index-or-title', type: 'index-or-title', placeholder: '1 or "task title"' },
|
|
236
|
+
{ name: 'start-time', type: 'time', placeholder: 'HH:MM AM/PM' },
|
|
237
|
+
],
|
|
238
|
+
screens: ['nodes'],
|
|
239
|
+
description: 'Add start time (default 12:00 AM)',
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
command: '/add node ... end-date',
|
|
243
|
+
args: [
|
|
244
|
+
{ name: 'index-or-title', type: 'index-or-title', placeholder: '1 or "task title"' },
|
|
245
|
+
{ name: 'end-date', type: 'date', placeholder: 'DD-MM-YYYY' },
|
|
246
|
+
],
|
|
247
|
+
screens: ['nodes'],
|
|
248
|
+
description: 'Add end date',
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
command: '/add node ... end-time',
|
|
252
|
+
args: [
|
|
253
|
+
{ name: 'index-or-title', type: 'index-or-title', placeholder: '1 or "task title"' },
|
|
254
|
+
{ name: 'end-time', type: 'time', placeholder: 'HH:MM AM/PM' },
|
|
255
|
+
],
|
|
256
|
+
screens: ['nodes'],
|
|
257
|
+
description: 'Add end time (default 12:00 AM)',
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
command: '/add node ... tag',
|
|
261
|
+
args: [
|
|
262
|
+
{ name: 'index-or-title', type: 'index-or-title', placeholder: '1 or "task title"' },
|
|
263
|
+
{ name: 'tag-name', type: 'text', placeholder: 'tag name' },
|
|
264
|
+
],
|
|
265
|
+
screens: ['nodes'],
|
|
266
|
+
description: 'Add tag to node',
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
command: '/add node ... note',
|
|
270
|
+
args: [
|
|
271
|
+
{ name: 'index-or-title', type: 'index-or-title', placeholder: '1 or "task title"' },
|
|
272
|
+
{ name: 'note-text', type: 'text', placeholder: 'note text' },
|
|
273
|
+
],
|
|
274
|
+
screens: ['nodes'],
|
|
275
|
+
description: 'Add note',
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
command: '/add node ... status',
|
|
279
|
+
args: [
|
|
280
|
+
{ name: 'index-or-title', type: 'index-or-title', placeholder: '1 or "task title"' },
|
|
281
|
+
{ name: 'status', type: 'status', placeholder: 'NOT-DONE | IN-PROGRESS | DONE' },
|
|
282
|
+
],
|
|
283
|
+
screens: ['nodes'],
|
|
284
|
+
description: 'Set status',
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
command: '/add node ... priority',
|
|
288
|
+
args: [
|
|
289
|
+
{ name: 'index-or-title', type: 'index-or-title', placeholder: '1 or "task title"' },
|
|
290
|
+
{ name: 'priority', type: 'priority', placeholder: 'LOW | MEDIUM | HIGH' },
|
|
291
|
+
],
|
|
292
|
+
screens: ['nodes'],
|
|
293
|
+
description: 'Set priority',
|
|
294
|
+
},
|
|
295
|
+
// /delete node ... <property>
|
|
296
|
+
{
|
|
297
|
+
command: '/delete node ... start-date',
|
|
298
|
+
args: [{ name: 'index-or-title', type: 'index-or-title', placeholder: '1 or "task title"' }],
|
|
299
|
+
screens: ['nodes'],
|
|
300
|
+
description: 'Remove start date',
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
command: '/delete node ... start-time',
|
|
304
|
+
args: [{ name: 'index-or-title', type: 'index-or-title', placeholder: '1 or "task title"' }],
|
|
305
|
+
screens: ['nodes'],
|
|
306
|
+
description: 'Remove start time',
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
command: '/delete node ... end-date',
|
|
310
|
+
args: [{ name: 'index-or-title', type: 'index-or-title', placeholder: '1 or "task title"' }],
|
|
311
|
+
screens: ['nodes'],
|
|
312
|
+
description: 'Remove end date',
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
command: '/delete node ... end-time',
|
|
316
|
+
args: [{ name: 'index-or-title', type: 'index-or-title', placeholder: '1 or "task title"' }],
|
|
317
|
+
screens: ['nodes'],
|
|
318
|
+
description: 'Remove end time',
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
command: '/delete node ... tag',
|
|
322
|
+
args: [
|
|
323
|
+
{ name: 'index-or-title', type: 'index-or-title', placeholder: '1 or "task title"' },
|
|
324
|
+
{ name: 'tag-name', type: 'text', placeholder: 'tag name' },
|
|
325
|
+
],
|
|
326
|
+
screens: ['nodes'],
|
|
327
|
+
description: 'Remove tag from node',
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
command: '/delete node ... note',
|
|
331
|
+
args: [{ name: 'index-or-title', type: 'index-or-title', placeholder: '1 or "task title"' }],
|
|
332
|
+
screens: ['nodes'],
|
|
333
|
+
description: 'Remove note',
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
command: '/delete node ... status',
|
|
337
|
+
args: [{ name: 'index-or-title', type: 'index-or-title', placeholder: '1 or "task title"' }],
|
|
338
|
+
screens: ['nodes'],
|
|
339
|
+
description: 'Remove status',
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
command: '/delete node ... priority',
|
|
343
|
+
args: [{ name: 'index-or-title', type: 'index-or-title', placeholder: '1 or "task title"' }],
|
|
344
|
+
screens: ['nodes'],
|
|
345
|
+
description: 'Remove priority',
|
|
346
|
+
},
|
|
347
|
+
];
|
|
348
|
+
/** Returns commands valid for a given screen (global always included). */
|
|
349
|
+
export function getCommandsForScreen(screen) {
|
|
350
|
+
return COMMAND_REGISTRY.filter((c) => c.screens.includes('global') || c.screens.includes(screen));
|
|
351
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// resolver.ts — Parses the current input string and returns stage + suggestions.
|
|
2
|
+
// This is the core brain of the autocomplete system.
|
|
3
|
+
import { COMMAND_REGISTRY, getCommandsForScreen } from './registry.js';
|
|
4
|
+
import { assignNumbers } from '../utils/numbering.js';
|
|
5
|
+
/** DFS flatten for suggestions — walks the full nested tree. */
|
|
6
|
+
function flattenForSuggestions(nodes) {
|
|
7
|
+
const result = [];
|
|
8
|
+
function dfs(children) {
|
|
9
|
+
const sorted = [...children].sort((a, b) => a.position - b.position);
|
|
10
|
+
for (const node of sorted) {
|
|
11
|
+
result.push(node);
|
|
12
|
+
if ((node.children ?? []).length > 0)
|
|
13
|
+
dfs(node.children);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
dfs(nodes);
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
19
|
+
// The ordered list of sub-properties for "/edit node ..." style commands.
|
|
20
|
+
const NODE_PROPERTIES = [
|
|
21
|
+
'title',
|
|
22
|
+
'start-date',
|
|
23
|
+
'start-time',
|
|
24
|
+
'end-date',
|
|
25
|
+
'end-time',
|
|
26
|
+
'tag',
|
|
27
|
+
'note',
|
|
28
|
+
'status',
|
|
29
|
+
'priority',
|
|
30
|
+
'position',
|
|
31
|
+
];
|
|
32
|
+
const STATUS_VALUES = ['NOT-DONE', 'IN-PROGRESS', 'DONE'];
|
|
33
|
+
const PRIORITY_VALUES = ['LOW', 'MEDIUM', 'HIGH'];
|
|
34
|
+
/** Fuzzy prefix match — input must be a prefix of label (case-insensitive). */
|
|
35
|
+
function prefixMatch(input, label) {
|
|
36
|
+
return label.toLowerCase().startsWith(input.toLowerCase());
|
|
37
|
+
}
|
|
38
|
+
/** Find which registered command best matches a fully-typed command prefix. */
|
|
39
|
+
function matchingCommands(input, screen) {
|
|
40
|
+
const all = getCommandsForScreen(screen);
|
|
41
|
+
return all.filter((c) => prefixMatch(input, c.command));
|
|
42
|
+
}
|
|
43
|
+
export function resolve(input, screen, currentNodes) {
|
|
44
|
+
// ── Stage 0: no slash, no overlay ─────────────────────────────────────────
|
|
45
|
+
if (!input.startsWith('/')) {
|
|
46
|
+
return { stage: 'command', suggestions: [], filledTokens: [] };
|
|
47
|
+
}
|
|
48
|
+
const trimmed = input.trimEnd();
|
|
49
|
+
// ─── Stage 1: command selection ───────────────────────────────────────────
|
|
50
|
+
// We're still typing the command name — no trailing space yet after the
|
|
51
|
+
// command, or no command matched yet.
|
|
52
|
+
const allForScreen = getCommandsForScreen(screen);
|
|
53
|
+
// Check if the current input has moved past Stage 1 by seeing if the input
|
|
54
|
+
// exactly matches a registered command prefix + a space.
|
|
55
|
+
const matchedCmd = COMMAND_REGISTRY.find((c) => trimmed.toLowerCase().startsWith(c.command.toLowerCase()) && input.length > c.command.length);
|
|
56
|
+
if (!matchedCmd) {
|
|
57
|
+
// Still typing the command — show matching commands as suggestions.
|
|
58
|
+
const matches = allForScreen.filter((c) => c.command.toLowerCase().startsWith(trimmed.toLowerCase()));
|
|
59
|
+
return {
|
|
60
|
+
stage: 'command',
|
|
61
|
+
filledTokens: [],
|
|
62
|
+
suggestions: matches.map((c) => ({
|
|
63
|
+
label: c.command,
|
|
64
|
+
fillValue: c.command + ' ',
|
|
65
|
+
hint: c.description,
|
|
66
|
+
})),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
// ─── We have a matched command. Now determine which arg we're filling. ────
|
|
70
|
+
// Strip the command prefix + trailing space.
|
|
71
|
+
const afterCmd = input.slice(matchedCmd.command.length + 1);
|
|
72
|
+
const args = matchedCmd.args;
|
|
73
|
+
// ─── Commands that need a node-ref as first arg ──────────────────────────
|
|
74
|
+
const needsNodeRef = args.length > 0 && args[0].type === 'index-or-title';
|
|
75
|
+
if (needsNodeRef) {
|
|
76
|
+
// Stage 2: node-ref input
|
|
77
|
+
const nodeRefInput = afterCmd;
|
|
78
|
+
const propertyStart = nodeRefInput.indexOf(' ');
|
|
79
|
+
if (propertyStart === -1) {
|
|
80
|
+
// User is still typing the node ref
|
|
81
|
+
const query = nodeRefInput.toLowerCase();
|
|
82
|
+
// Build hierarchical number map and flatten the tree for display
|
|
83
|
+
const numberMap = assignNumbers(currentNodes);
|
|
84
|
+
const flatNodes = flattenForSuggestions(currentNodes);
|
|
85
|
+
const matchedNodes = flatNodes.filter((n) => {
|
|
86
|
+
if (!nodeRefInput)
|
|
87
|
+
return true;
|
|
88
|
+
const num = numberMap.get(n.id) ?? '';
|
|
89
|
+
return n.title.toLowerCase().includes(query) || num.startsWith(query);
|
|
90
|
+
});
|
|
91
|
+
if (nodeRefInput.length > 0 && matchedNodes.length === 0) {
|
|
92
|
+
return {
|
|
93
|
+
stage: 'node-ref',
|
|
94
|
+
filledTokens: [matchedCmd.command],
|
|
95
|
+
suggestions: [{ label: '✕ No match found', fillValue: '', isNoMatch: true }],
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
stage: 'node-ref',
|
|
100
|
+
filledTokens: [matchedCmd.command],
|
|
101
|
+
suggestions: matchedNodes.map((n) => {
|
|
102
|
+
const num = numberMap.get(n.id) ?? '?';
|
|
103
|
+
return {
|
|
104
|
+
label: `${num}. ${n.title}`,
|
|
105
|
+
fillValue: num + ' ',
|
|
106
|
+
hint: n.status,
|
|
107
|
+
};
|
|
108
|
+
}),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
// Node ref is filled. Now figure out which property arg we're on.
|
|
112
|
+
const nodeRef = nodeRefInput.slice(0, propertyStart);
|
|
113
|
+
const afterNodeRef = nodeRefInput.slice(propertyStart + 1);
|
|
114
|
+
const filledTokens = [matchedCmd.command, nodeRef];
|
|
115
|
+
// If the command has exactly 2 args (node-ref + value), go to value-hint
|
|
116
|
+
if (args.length === 2) {
|
|
117
|
+
const valueArg = args[1];
|
|
118
|
+
const valueInput = afterNodeRef;
|
|
119
|
+
// Stage 3 for value typing
|
|
120
|
+
if (valueArg.type === 'status') {
|
|
121
|
+
const matches = STATUS_VALUES.filter((v) => prefixMatch(valueInput, v));
|
|
122
|
+
return {
|
|
123
|
+
stage: 'value-hint',
|
|
124
|
+
filledTokens,
|
|
125
|
+
suggestions: matches.map((v) => ({
|
|
126
|
+
label: v,
|
|
127
|
+
fillValue: v + ' ',
|
|
128
|
+
hint: valueArg.placeholder,
|
|
129
|
+
})),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
if (valueArg.type === 'priority') {
|
|
133
|
+
const matches = PRIORITY_VALUES.filter((v) => prefixMatch(valueInput, v));
|
|
134
|
+
return {
|
|
135
|
+
stage: 'value-hint',
|
|
136
|
+
filledTokens,
|
|
137
|
+
suggestions: matches.map((v) => ({
|
|
138
|
+
label: v,
|
|
139
|
+
fillValue: v + ' ',
|
|
140
|
+
hint: valueArg.placeholder,
|
|
141
|
+
})),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
// Date / time / text / number — just show format hint
|
|
145
|
+
return {
|
|
146
|
+
stage: 'value-hint',
|
|
147
|
+
filledTokens,
|
|
148
|
+
suggestions: [{ label: valueArg.placeholder, fillValue: '', hint: valueArg.placeholder }],
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
// Command has only 1 arg (just node-ref, no further arg) — nothing more to suggest
|
|
152
|
+
return { stage: 'value-hint', filledTokens, suggestions: [] };
|
|
153
|
+
}
|
|
154
|
+
// ─── Commands without node-ref (folder/list commands + global) ───────────
|
|
155
|
+
// If command has args and no node-ref, show property suggestions.
|
|
156
|
+
if (args.length > 0) {
|
|
157
|
+
const firstArg = args[0];
|
|
158
|
+
const valueInput = afterCmd;
|
|
159
|
+
if (firstArg.type === 'status') {
|
|
160
|
+
const matches = STATUS_VALUES.filter((v) => prefixMatch(valueInput, v));
|
|
161
|
+
return {
|
|
162
|
+
stage: 'value-hint',
|
|
163
|
+
filledTokens: [matchedCmd.command],
|
|
164
|
+
suggestions: matches.map((v) => ({ label: v, fillValue: v, hint: firstArg.placeholder })),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
if (firstArg.type === 'priority') {
|
|
168
|
+
const matches = PRIORITY_VALUES.filter((v) => prefixMatch(valueInput, v));
|
|
169
|
+
return {
|
|
170
|
+
stage: 'value-hint',
|
|
171
|
+
filledTokens: [matchedCmd.command],
|
|
172
|
+
suggestions: matches.map((v) => ({ label: v, fillValue: v, hint: firstArg.placeholder })),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
// text / number — show format hint
|
|
176
|
+
return {
|
|
177
|
+
stage: 'value-hint',
|
|
178
|
+
filledTokens: [matchedCmd.command],
|
|
179
|
+
suggestions: [{ label: firstArg.placeholder, fillValue: '', hint: firstArg.placeholder }],
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
// No args needed — command is complete.
|
|
183
|
+
return { stage: 'command', filledTokens: [matchedCmd.command], suggestions: [] };
|
|
184
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import Conf from 'conf';
|
|
3
|
+
const store = new Conf({ projectName: 'stratanodex' });
|
|
4
|
+
const DEFAULT_API_URL = 'https://api.stratanodex.online';
|
|
5
|
+
let runtimeApiUrl;
|
|
6
|
+
export function getConfig() {
|
|
7
|
+
const apiUrl = runtimeApiUrl ?? process.env['STRATANODEX_API_URL'] ?? store.get('apiUrl') ?? DEFAULT_API_URL;
|
|
8
|
+
const verbose = process.env['STRATANODEX_VERBOSE'] === 'true';
|
|
9
|
+
return { apiUrl, verbose };
|
|
10
|
+
}
|
|
11
|
+
export function setRuntimeApiUrl(url) {
|
|
12
|
+
runtimeApiUrl = url;
|
|
13
|
+
}
|
|
14
|
+
export function saveApiUrl(url) {
|
|
15
|
+
store.set('apiUrl', url);
|
|
16
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { program } from 'commander';
|
|
3
|
+
import { createRequire } from 'module';
|
|
4
|
+
import { setRuntimeApiUrl } from './config.js';
|
|
5
|
+
import { runLogin } from './commands/login.js';
|
|
6
|
+
import { runLogout } from './commands/logout.js';
|
|
7
|
+
import { runConfig } from './commands/config.js';
|
|
8
|
+
import { runList } from './commands/list.js';
|
|
9
|
+
import { runAdd } from './commands/add.js';
|
|
10
|
+
import { runDone } from './commands/done.js';
|
|
11
|
+
const require = createRequire(import.meta.url);
|
|
12
|
+
const pkg = require('../package.json');
|
|
13
|
+
program
|
|
14
|
+
.name('stratanodex')
|
|
15
|
+
.description('CLI-first productivity and task management system')
|
|
16
|
+
.version(pkg.version);
|
|
17
|
+
program.option('--api-url <url>', 'Override API base URL').hook('preAction', (thisCommand) => {
|
|
18
|
+
const opts = thisCommand.opts();
|
|
19
|
+
if (opts.apiUrl)
|
|
20
|
+
setRuntimeApiUrl(opts.apiUrl);
|
|
21
|
+
});
|
|
22
|
+
program
|
|
23
|
+
.command('login')
|
|
24
|
+
.description('Log in to your StrataNodex account')
|
|
25
|
+
.action(async () => {
|
|
26
|
+
await runLogin();
|
|
27
|
+
});
|
|
28
|
+
program
|
|
29
|
+
.command('logout')
|
|
30
|
+
.description('Log out and clear stored credentials')
|
|
31
|
+
.action(() => {
|
|
32
|
+
runLogout();
|
|
33
|
+
});
|
|
34
|
+
const configCmd = program.command('config').description('Manage CLI configuration');
|
|
35
|
+
configCmd
|
|
36
|
+
.command('list')
|
|
37
|
+
.description('Show all config values')
|
|
38
|
+
.action(() => runConfig('list'));
|
|
39
|
+
configCmd
|
|
40
|
+
.command('get <key>')
|
|
41
|
+
.description('Get a config value')
|
|
42
|
+
.action((key) => runConfig('get', key));
|
|
43
|
+
configCmd
|
|
44
|
+
.command('set <key> <value>')
|
|
45
|
+
.description('Set a config value')
|
|
46
|
+
.action((key, value) => runConfig('set', key, value));
|
|
47
|
+
program
|
|
48
|
+
.command('list')
|
|
49
|
+
.description('List folders, lists, and tasks')
|
|
50
|
+
.option('-d, --depth <number>', 'Depth of nodes to show (0=none, 1=top-level, 2=two levels)', '0')
|
|
51
|
+
.action(async (options) => {
|
|
52
|
+
await runList({ depth: parseInt(options.depth ?? '0', 10) });
|
|
53
|
+
});
|
|
54
|
+
program
|
|
55
|
+
.command('add <title>')
|
|
56
|
+
.description('Add a new task')
|
|
57
|
+
.option('--list <listId>', 'List ID to add the task to')
|
|
58
|
+
.option('--parent <number>', 'Parent node number (e.g. 1.2) to add as sub-task')
|
|
59
|
+
.action(async (title, options) => {
|
|
60
|
+
await runAdd(title, options);
|
|
61
|
+
});
|
|
62
|
+
program
|
|
63
|
+
.command('done <number>')
|
|
64
|
+
.description('Mark a task as done by its number')
|
|
65
|
+
.option('--list <listId>', 'List ID (optional, uses last active list if omitted)')
|
|
66
|
+
.action(async (number, options) => {
|
|
67
|
+
await runDone(number, options);
|
|
68
|
+
});
|
|
69
|
+
if (process.argv.length === 2) {
|
|
70
|
+
const { render } = await import('ink');
|
|
71
|
+
const React = (await import('react')).default;
|
|
72
|
+
const { App } = await import('./tui/App.js');
|
|
73
|
+
const { waitUntilExit } = render(React.createElement(App));
|
|
74
|
+
await waitUntilExit();
|
|
75
|
+
process.exit(0);
|
|
76
|
+
}
|
|
77
|
+
program.parse();
|