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
@@ -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();