logwork-cli 0.0.0-stage → 1.0.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 (91) hide show
  1. package/LICENSE +11 -0
  2. package/README.md +287 -2
  3. package/RELEASE.md +89 -0
  4. package/bin/logwork.mjs +101 -0
  5. package/commands/auth.mjs +227 -0
  6. package/commands/delete.mjs +69 -0
  7. package/commands/doctor.mjs +24 -0
  8. package/commands/edit.mjs +88 -0
  9. package/commands/history.mjs +62 -0
  10. package/commands/query.mjs +52 -0
  11. package/commands/quick.mjs +163 -0
  12. package/commands/reconcile.mjs +55 -0
  13. package/commands/release.mjs +77 -0
  14. package/commands/reminder.mjs +172 -0
  15. package/commands/setup.mjs +504 -0
  16. package/commands/tui.mjs +50 -0
  17. package/commands/update.mjs +161 -0
  18. package/docs/advanced.md +334 -0
  19. package/docs/mcp-setup.md +347 -0
  20. package/docs/operations-guide.md +587 -0
  21. package/docs/recipes.md +259 -0
  22. package/docs/security.md +151 -0
  23. package/examples/mcp/antigravity.mcp_config.json +8 -0
  24. package/examples/mcp/codex.config.toml +5 -0
  25. package/examples/mcp/copilot.mcp.json +9 -0
  26. package/lib/api-auth.mjs +1259 -0
  27. package/lib/api.mjs +1422 -0
  28. package/lib/apply-ledger.mjs +227 -0
  29. package/lib/atomic-file.mjs +24 -0
  30. package/lib/auth-diagnostics.mjs +113 -0
  31. package/lib/auth-errors.mjs +22 -0
  32. package/lib/auth-redaction.mjs +78 -0
  33. package/lib/auth-terminal.mjs +97 -0
  34. package/lib/auth.mjs +323 -0
  35. package/lib/batch-parser.mjs +145 -0
  36. package/lib/batch-workflow.mjs +842 -0
  37. package/lib/config.mjs +191 -0
  38. package/lib/credential-store.mjs +130 -0
  39. package/lib/diagnostics.mjs +307 -0
  40. package/lib/entrypoint.mjs +14 -0
  41. package/lib/file-lock.mjs +44 -0
  42. package/lib/http.mjs +125 -0
  43. package/lib/jira-api.mjs +258 -0
  44. package/lib/jira-auth.mjs +203 -0
  45. package/lib/jira-workflow.mjs +523 -0
  46. package/lib/keychain.mjs +105 -0
  47. package/lib/legacy-migration.mjs +66 -0
  48. package/lib/list-navigation.mjs +19 -0
  49. package/lib/logwork-config.mjs +199 -0
  50. package/lib/logwork-f03-enrich.mjs +41 -0
  51. package/lib/macos.mjs +33 -0
  52. package/lib/manual-apply-state.mjs +35 -0
  53. package/lib/manual-commands.mjs +294 -0
  54. package/lib/manual-drafts.mjs +154 -0
  55. package/lib/manual-ink-app.mjs +1654 -0
  56. package/lib/manual-ink-auth.mjs +349 -0
  57. package/lib/manual-ink-ui.mjs +675 -0
  58. package/lib/manual-input.mjs +21 -0
  59. package/lib/manual-logwork-wizard.mjs +310 -0
  60. package/lib/manual-mcp.mjs +187 -0
  61. package/lib/manual-repl.mjs +1037 -0
  62. package/lib/manual-targets.mjs +78 -0
  63. package/lib/mcp-server.mjs +793 -0
  64. package/lib/os-notification.mjs +105 -0
  65. package/lib/package-info.mjs +17 -0
  66. package/lib/paths.mjs +68 -0
  67. package/lib/project-identity.mjs +66 -0
  68. package/lib/project-mapping-workflow.mjs +309 -0
  69. package/lib/project-resolver.mjs +373 -0
  70. package/lib/query-workflow.mjs +553 -0
  71. package/lib/reconciliation-workflow.mjs +446 -0
  72. package/lib/release-doctor.mjs +448 -0
  73. package/lib/reminder-config.mjs +71 -0
  74. package/lib/reminder-scheduler.mjs +301 -0
  75. package/lib/reminder-service.mjs +118 -0
  76. package/lib/reminder-state.mjs +38 -0
  77. package/lib/reminder-workflow.mjs +299 -0
  78. package/lib/ro-delete-args.mjs +38 -0
  79. package/lib/ro-delete-workflow.mjs +115 -0
  80. package/lib/ro-edit-args.mjs +142 -0
  81. package/lib/ro-edit-workflow.mjs +632 -0
  82. package/lib/ro-entry-reader.mjs +56 -0
  83. package/lib/ro-resubmit-workflow.mjs +134 -0
  84. package/lib/terminal-auth-prompts.mjs +120 -0
  85. package/lib/ui.mjs +133 -0
  86. package/lib/update-service.mjs +350 -0
  87. package/lib/util.mjs +66 -0
  88. package/lib/windows-credential-manager.mjs +260 -0
  89. package/lib/worklog-task-resolver.mjs +113 -0
  90. package/package.json +72 -4
  91. package/setup.sh +22 -0
@@ -0,0 +1,675 @@
1
+ import React, { useState } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ import TextInput from 'ink-text-input';
4
+ import chalk from 'chalk';
5
+ import {
6
+ COMMAND_SUGGESTION_LIMIT,
7
+ getCommandSuggestions,
8
+ MANUAL_COMMANDS,
9
+ TASK_COMMANDS
10
+ } from './manual-commands.mjs';
11
+ import { formatManualDraftLabel } from './manual-drafts.mjs';
12
+ import { MCP_CLIENTS } from './manual-mcp.mjs';
13
+ import {
14
+ nextIndex,
15
+ previousIndex
16
+ } from './list-navigation.mjs';
17
+ import {
18
+ sameProjectIdentity
19
+ } from './project-identity.mjs';
20
+
21
+ const h = React.createElement;
22
+
23
+ export { sameProjectIdentity };
24
+
25
+ export function HeaderBar({ cwd }) {
26
+ return h(Box, {
27
+ flexDirection: 'column',
28
+ borderStyle: 'round',
29
+ borderColor: 'cyan',
30
+ paddingX: 1
31
+ },
32
+ h(Text, { bold: true }, 'Logwork'),
33
+ h(Text, { color: 'gray' }, `cwd: ${cwd}`));
34
+ }
35
+
36
+ export function StatusBar({ preview, dryRun, wizard, authPrompt }) {
37
+ const previewText = preview
38
+ ? `preview: ${preview.status} · ${preview.entries?.length || 0} entries`
39
+ : 'preview: none';
40
+ const targetText = wizard?.target ? ` · target: ${wizard.target}` : '';
41
+ const mode = authPrompt ? `mode: auth-${authPrompt.step}` : wizard?.step ? `mode: ${wizard.step.replace('_', '-')}${targetText}` : 'mode: command';
42
+ const dryRunText = dryRun ? ' · DRY RUN' : '';
43
+ const hint = authPrompt ? 'Enter auth details here' : wizard?.step === 'edit_tasks' ? 'Type / for task commands' : 'Type / for commands';
44
+
45
+ return h(Box, {
46
+ borderStyle: 'single',
47
+ borderColor: 'gray',
48
+ paddingX: 1
49
+ },
50
+ h(Text, { color: authPrompt ? 'cyan' : wizard?.step ? 'yellow' : 'gray' }, `${mode} · ${previewText}${dryRunText} · ${hint}`));
51
+ }
52
+
53
+ export function TargetPicker({
54
+ title = 'Pick target',
55
+ targets = [],
56
+ selectedIndex = 0,
57
+ onChange,
58
+ onSelect,
59
+ onCancel
60
+ }) {
61
+ useInput((input, key) => {
62
+ if (key.upArrow) {
63
+ onChange(previousIndex(selectedIndex, targets.length));
64
+ return;
65
+ }
66
+ if (key.downArrow) {
67
+ onChange(nextIndex(selectedIndex, targets.length));
68
+ return;
69
+ }
70
+ if (key.return) {
71
+ onSelect(targets[selectedIndex]);
72
+ return;
73
+ }
74
+ if (key.escape && typeof onCancel === 'function') {
75
+ onCancel();
76
+ }
77
+ });
78
+
79
+ return h(SelectionList, {
80
+ title,
81
+ items: targets.map((target) => ({
82
+ key: target.key,
83
+ label: `${target.label} - ${target.description}`
84
+ })),
85
+ selectedIndex
86
+ });
87
+ }
88
+
89
+ export function CurrentPanel({ panel = {} }) {
90
+ return h(Box, {
91
+ flexDirection: 'column',
92
+ borderStyle: 'single',
93
+ borderColor: panelColor(panel.kind),
94
+ paddingX: 1,
95
+ minHeight: 5
96
+ },
97
+ h(Text, {
98
+ bold: true,
99
+ color: panelColor(panel.kind)
100
+ }, panel.title || 'Current'),
101
+ h(Text, {
102
+ color: panel.kind === 'error' ? 'red' : undefined
103
+ }, panel.text || 'Type / for commands.'));
104
+ }
105
+
106
+ export function OutputPanel({ items = [] }) {
107
+ const lastItem = items.at(-1);
108
+ return h(CurrentPanel, {
109
+ panel: lastItem
110
+ ? {
111
+ kind: lastItem.kind,
112
+ title: 'Current',
113
+ text: lastItem.text
114
+ }
115
+ : {
116
+ kind: 'idle',
117
+ title: 'Current',
118
+ text: 'Type / for commands.'
119
+ }
120
+ });
121
+ }
122
+
123
+ export function CommandInput({
124
+ value,
125
+ onChange,
126
+ onSubmit,
127
+ selectedIndex,
128
+ setSelectedIndex,
129
+ mode = 'command',
130
+ onEscape
131
+ }) {
132
+ const [autocompleteVersion, setAutocompleteVersion] = useState(0);
133
+ const [completedCommandValue, setCompletedCommandValue] = useState('');
134
+ const suggestions = mode === 'task' ? getCommandSuggestions(value, TASK_COMMANDS) : mode === 'command' ? getCommandSuggestions(value) : [];
135
+ const showSuggestions = suggestions.length > 0 && value !== completedCommandValue;
136
+
137
+ function completeSelectedCommand() {
138
+ const completed = completeCommandValue(value, suggestions, selectedIndex);
139
+ if (!completed) {
140
+ return false;
141
+ }
142
+
143
+ onChange(completed);
144
+ setCompletedCommandValue(completed);
145
+ setAutocompleteVersion((current) => current + 1);
146
+ return true;
147
+ }
148
+
149
+ function handleInputChange(nextValue) {
150
+ setCompletedCommandValue('');
151
+ onChange(nextValue);
152
+ }
153
+
154
+ function handleSubmit(submittedValue) {
155
+ if (completeSelectedCommand()) {
156
+ return;
157
+ }
158
+ onSubmit(submittedValue);
159
+ }
160
+
161
+ useInput((input, key) => {
162
+ if (key.escape && typeof onEscape === 'function') {
163
+ onEscape();
164
+ return;
165
+ }
166
+
167
+ if (!showSuggestions) {
168
+ return;
169
+ }
170
+
171
+ if (key.upArrow) {
172
+ setSelectedIndex((current) => previousIndex(current, suggestions.length));
173
+ return;
174
+ }
175
+
176
+ if (key.downArrow) {
177
+ setSelectedIndex((current) => nextIndex(current, suggestions.length));
178
+ return;
179
+ }
180
+
181
+ if (key.tab) {
182
+ completeSelectedCommand();
183
+ }
184
+ });
185
+
186
+ const isTaskMode = mode === 'task' || mode === 'task-edit';
187
+ return h(Box, {
188
+ flexDirection: 'column'
189
+ },
190
+ h(Box, null,
191
+ h(Text, { color: isTaskMode ? 'yellow' : 'green' }, isTaskMode ? 'task › ' : 'logwork › '),
192
+ h(TextInput, {
193
+ key: commandInputKey(mode, autocompleteVersion),
194
+ value,
195
+ onChange: handleInputChange,
196
+ onSubmit: handleSubmit,
197
+ placeholder: mode === 'task'
198
+ ? '+2 Feature Development | check ui/ux · / for task commands · empty Enter applies'
199
+ : mode === 'task-edit'
200
+ ? '+2 replacement task'
201
+ : 'Type / for commands'
202
+ })),
203
+ showSuggestions ? h(SlashMenu, {
204
+ suggestions,
205
+ selectedIndex
206
+ }) : null);
207
+ }
208
+
209
+ export function commandInputKey(mode, autocompleteVersion = 0) {
210
+ return `${mode}:${autocompleteVersion}`;
211
+ }
212
+
213
+ export function completeCommandValue(value, suggestions = [], selectedIndex = 0) {
214
+ const selected = suggestions[selectedIndex] || suggestions[0];
215
+ const trimmed = String(value || '').trim();
216
+ if (!selected || !trimmed || trimmed === selected.name || !selected.name.startsWith(trimmed)) {
217
+ return null;
218
+ }
219
+
220
+ return selected.name;
221
+ }
222
+
223
+ export function DatePicker({
224
+ options = [],
225
+ selectedIndex = 0,
226
+ onChange,
227
+ onSelect
228
+ }) {
229
+ useInput((input, key) => {
230
+ if (key.upArrow) {
231
+ onChange(previousIndex(selectedIndex, options.length));
232
+ return;
233
+ }
234
+ if (key.downArrow) {
235
+ onChange(nextIndex(selectedIndex, options.length));
236
+ return;
237
+ }
238
+ if (key.return) {
239
+ onSelect(options[selectedIndex]);
240
+ }
241
+ });
242
+
243
+ return h(SelectionList, {
244
+ title: 'Pick date',
245
+ items: options.map((option) => ({
246
+ key: option.date,
247
+ label: `${option.label}${option.isToday ? ' · today' : ''}`
248
+ })),
249
+ selectedIndex
250
+ });
251
+ }
252
+
253
+ export function ProjectPicker({
254
+ options = [],
255
+ selectedIndex = 0,
256
+ onChange,
257
+ onSelect
258
+ }) {
259
+ useInput((input, key) => {
260
+ if (key.upArrow) {
261
+ onChange(previousIndex(selectedIndex, options.length));
262
+ return;
263
+ }
264
+ if (key.downArrow) {
265
+ onChange(nextIndex(selectedIndex, options.length));
266
+ return;
267
+ }
268
+ if (key.return) {
269
+ onSelect(options[selectedIndex]);
270
+ }
271
+ });
272
+
273
+ return h(SelectionList, {
274
+ title: 'Pick project',
275
+ items: options.map((option) => ({
276
+ key: String(option.projectMemberId ?? option.projectId ?? option.projectName),
277
+ label: option.label
278
+ })),
279
+ selectedIndex
280
+ });
281
+ }
282
+
283
+ export function DraftPicker({
284
+ drafts = [],
285
+ selectedIndex = 0,
286
+ includeStartNew = false,
287
+ onChange,
288
+ onResume,
289
+ onDelete,
290
+ onStartNew,
291
+ onCancel
292
+ }) {
293
+ const items = includeStartNew
294
+ ? [{ type: 'new', key: '__new__', label: 'Start new logwork session' }, ...drafts.map((draft) => ({
295
+ type: 'draft',
296
+ key: draft.id,
297
+ draft,
298
+ label: formatManualDraftLabel(draft)
299
+ }))]
300
+ : drafts.map((draft) => ({
301
+ type: 'draft',
302
+ key: draft.id,
303
+ draft,
304
+ label: formatManualDraftLabel(draft)
305
+ }));
306
+
307
+ useInput((input, key) => {
308
+ if (!items.length) {
309
+ if (key.escape) {
310
+ onCancel();
311
+ }
312
+ return;
313
+ }
314
+ if (key.upArrow) {
315
+ onChange(previousIndex(selectedIndex, items.length));
316
+ return;
317
+ }
318
+ if (key.downArrow) {
319
+ onChange(nextIndex(selectedIndex, items.length));
320
+ return;
321
+ }
322
+ if (key.escape) {
323
+ onCancel();
324
+ return;
325
+ }
326
+ const selected = items[selectedIndex];
327
+ if (key.return) {
328
+ if (selected?.type === 'new') {
329
+ onStartNew();
330
+ } else if (selected?.draft) {
331
+ onResume(selected.draft);
332
+ }
333
+ return;
334
+ }
335
+ if (String(input || '').toLowerCase() === 'd' && selected?.draft) {
336
+ onDelete(selected.draft);
337
+ }
338
+ });
339
+
340
+ if (!items.length) {
341
+ return h(Box, {
342
+ borderStyle: 'single',
343
+ borderColor: 'yellow',
344
+ paddingX: 1
345
+ }, h(Text, { color: 'yellow' }, 'No saved drafts. Esc returns.'));
346
+ }
347
+
348
+ return h(SelectionList, {
349
+ title: 'Saved drafts · Enter resume/start · d delete · Esc back',
350
+ items,
351
+ selectedIndex
352
+ });
353
+ }
354
+
355
+ export function McpClientPicker({
356
+ clients = MCP_CLIENTS,
357
+ selectedIndex = 0,
358
+ onChange,
359
+ onSelect,
360
+ onCancel
361
+ }) {
362
+ useInput((input, key) => {
363
+ if (key.upArrow) {
364
+ onChange(previousIndex(selectedIndex, clients.length));
365
+ return;
366
+ }
367
+ if (key.downArrow) {
368
+ onChange(nextIndex(selectedIndex, clients.length));
369
+ return;
370
+ }
371
+ if (key.return) {
372
+ onSelect(clients[selectedIndex]);
373
+ return;
374
+ }
375
+ if (key.escape) {
376
+ onCancel();
377
+ }
378
+ });
379
+
380
+ return h(SelectionList, {
381
+ title: 'Pick MCP client',
382
+ items: clients.map((client) => ({
383
+ key: client.key,
384
+ label: `${client.label} - ${client.format}`
385
+ })),
386
+ selectedIndex
387
+ });
388
+ }
389
+
390
+ export function TaskEditPicker({
391
+ tasks = [],
392
+ selectedIndex = 0,
393
+ onChange,
394
+ onSelect,
395
+ onCancel
396
+ }) {
397
+ useInput((input, key) => {
398
+ if (key.upArrow) {
399
+ onChange(previousIndex(selectedIndex, tasks.length));
400
+ return;
401
+ }
402
+ if (key.downArrow) {
403
+ onChange(nextIndex(selectedIndex, tasks.length));
404
+ return;
405
+ }
406
+ if (key.return) {
407
+ onSelect(selectedIndex);
408
+ return;
409
+ }
410
+ if (key.escape) {
411
+ onCancel();
412
+ }
413
+ });
414
+
415
+ return h(Box, {
416
+ flexDirection: 'column',
417
+ borderStyle: 'single',
418
+ borderColor: 'yellow',
419
+ paddingX: 1
420
+ },
421
+ h(Text, { color: 'yellow' }, 'Edit task · ↑/↓ select · Enter replace · Esc cancel'),
422
+ tasks.map((task, index) => h(Text, {
423
+ key: `${index}-${task.taskName}`,
424
+ color: index === selectedIndex ? 'yellow' : 'gray'
425
+ }, `${index === selectedIndex ? '›' : ' '} ${index + 1}. +${formatTaskHours(task.hours)} ${task.taskName}`)));
426
+ }
427
+
428
+ export function TaskRemovePicker({
429
+ tasks = [],
430
+ selectedIndex = 0,
431
+ selectedIndexes = [],
432
+ onChange,
433
+ onToggle,
434
+ onSubmit,
435
+ onCancel
436
+ }) {
437
+ const [commandBuffer, setCommandBuffer] = useState('');
438
+
439
+ useInput((input, key) => {
440
+ if (key.upArrow) {
441
+ onChange(previousIndex(selectedIndex, tasks.length));
442
+ return;
443
+ }
444
+ if (key.downArrow) {
445
+ onChange(nextIndex(selectedIndex, tasks.length));
446
+ return;
447
+ }
448
+ if (input === ' ' || key.space) {
449
+ onToggle(selectedIndex);
450
+ return;
451
+ }
452
+ if (key.return) {
453
+ onSubmit(selectedIndexes);
454
+ return;
455
+ }
456
+ if (key.escape) {
457
+ onCancel();
458
+ return;
459
+ }
460
+
461
+ const nextBuffer = `${commandBuffer}${input || ''}`;
462
+ if ('/cancel'.startsWith(nextBuffer)) {
463
+ setCommandBuffer(nextBuffer);
464
+ if (nextBuffer === '/cancel') {
465
+ onCancel();
466
+ }
467
+ } else {
468
+ setCommandBuffer(input === '/' ? '/' : '');
469
+ }
470
+ });
471
+
472
+ return h(Box, {
473
+ flexDirection: 'column',
474
+ borderStyle: 'single',
475
+ borderColor: 'yellow',
476
+ paddingX: 1
477
+ },
478
+ h(Text, { color: 'yellow' }, 'Remove tasks · Space select · Enter confirm · Esc cancel'),
479
+ tasks.map((task, index) => {
480
+ const selected = selectedIndexes.includes(index);
481
+ return h(Text, {
482
+ key: `${index}-${task.taskName}`,
483
+ color: index === selectedIndex ? 'yellow' : selected ? 'cyan' : 'gray'
484
+ }, `${index === selectedIndex ? '›' : ' '} ${selected ? '[x]' : '[ ]'} ${index + 1}. +${formatTaskHours(task.hours)} ${task.taskName}`);
485
+ }));
486
+ }
487
+
488
+ export function SelectionList({ title, items = [], selectedIndex = 0 }) {
489
+ return h(Box, {
490
+ flexDirection: 'column',
491
+ borderStyle: 'single',
492
+ borderColor: 'cyan',
493
+ paddingX: 1
494
+ },
495
+ h(Text, { color: 'cyan' }, `${title} · ↑/↓ then Enter`),
496
+ items.map((item, index) => h(Text, {
497
+ key: item.key,
498
+ color: index === selectedIndex ? 'cyan' : 'gray'
499
+ }, `${index === selectedIndex ? '›' : ' '} ${item.label}`)));
500
+ }
501
+
502
+ export function SlashMenu({ suggestions = [], selectedIndex = 0 }) {
503
+ return h(Box, {
504
+ flexDirection: 'column',
505
+ borderStyle: 'single',
506
+ borderColor: 'cyan',
507
+ paddingX: 1
508
+ },
509
+ suggestions.slice(0, COMMAND_SUGGESTION_LIMIT).map((command, index) => h(Text, {
510
+ key: command.name,
511
+ color: index === selectedIndex ? 'cyan' : 'gray'
512
+ }, `${index === selectedIndex ? '›' : ' '} ${command.name.padEnd(12)} ${command.description}`)));
513
+ }
514
+
515
+ export function ConfirmDialog({
516
+ message,
517
+ initialValue = true,
518
+ onResolve
519
+ }) {
520
+ useInput((input, key) => {
521
+ if (key.return) {
522
+ onResolve(initialValue);
523
+ return;
524
+ }
525
+
526
+ const normalized = String(input || '').toLowerCase();
527
+ if (normalized === 'y') {
528
+ onResolve(true);
529
+ return;
530
+ }
531
+ if (normalized === 'n' || key.escape) {
532
+ onResolve(false);
533
+ }
534
+ });
535
+
536
+ return h(Box, {
537
+ borderStyle: 'round',
538
+ borderColor: 'yellow',
539
+ paddingX: 1
540
+ },
541
+ h(Text, { color: 'yellow' }, `${message} ${initialValue ? '[Y/n]' : '[y/N]'}`));
542
+ }
543
+
544
+ export function LoadingLine({ message }) {
545
+ return h(Text, { color: 'yellow' }, `⏳ ${message}`);
546
+ }
547
+
548
+ export function ProjectChart({ lines = [] }) {
549
+ return h(Box, {
550
+ flexDirection: 'column'
551
+ }, lines.map((line, index) => h(Text, {
552
+ key: index,
553
+ color: chartLineColor(line)
554
+ }, line)));
555
+ }
556
+
557
+ export function loadingMessageForCommand(command) {
558
+ if (command.type === 'query') {
559
+ return 'Fetching Resource Optimiser timesheet...';
560
+ }
561
+ if (command.type === 'projects') {
562
+ return 'Fetching Resource Optimiser projects and weekly timesheet...';
563
+ }
564
+ if (command.type === 'reconcile') {
565
+ return 'Comparing Resource Optimiser and Jira worklogs...';
566
+ }
567
+ if (command.type === 'logwork') {
568
+ return 'Building logwork preview...';
569
+ }
570
+ if (command.type === 'apply') {
571
+ return 'Submitting logwork...';
572
+ }
573
+ if (command.type === 'edit_ro_logwork') {
574
+ return 'Checking and updating Resource Optimiser logwork...';
575
+ }
576
+ if (command.type === 'map') {
577
+ return 'Updating project mapping...';
578
+ }
579
+ if (command.type === 'auth') {
580
+ return 'Authenticating Resource Optimiser...';
581
+ }
582
+ if (command.type === 'diagnostics') {
583
+ return 'Writing diagnostics report...';
584
+ }
585
+ if (command.type === 'mcp') {
586
+ return '';
587
+ }
588
+ return '';
589
+ }
590
+
591
+ export function panelTitleForCommand(command) {
592
+ if (command.type === 'query') {
593
+ return 'Query Logwork';
594
+ }
595
+ if (command.type === 'projects') {
596
+ return 'Projects';
597
+ }
598
+ if (command.type === 'reconcile') {
599
+ return 'RO / Jira Reconciliation';
600
+ }
601
+ if (command.type === 'logwork') {
602
+ return 'Logwork Preview';
603
+ }
604
+ if (command.type === 'apply') {
605
+ return 'Apply Logwork';
606
+ }
607
+ if (command.type === 'edit_ro_logwork') {
608
+ return 'Edit RO Logwork';
609
+ }
610
+ if (command.type === 'map') {
611
+ return 'Project Mapping';
612
+ }
613
+ if (command.type === 'auth') {
614
+ return 'Authentication';
615
+ }
616
+ if (command.type === 'status') {
617
+ return 'Auth Status';
618
+ }
619
+ if (command.type === 'diagnostics') {
620
+ return 'Diagnostics';
621
+ }
622
+ if (command.type === 'mcp') {
623
+ return 'MCP Setup';
624
+ }
625
+ return 'Command';
626
+ }
627
+
628
+ export function panelColor(kind) {
629
+ if (kind === 'error') {
630
+ return 'red';
631
+ }
632
+ if (kind === 'success') {
633
+ return 'green';
634
+ }
635
+ if (kind === 'logwork' || kind === 'auth') {
636
+ return 'cyan';
637
+ }
638
+ return 'gray';
639
+ }
640
+
641
+ export function formatTaskHours(value) {
642
+ const number = Number(value || 0);
643
+ return Number.isInteger(number) ? String(number) : String(Number(number.toFixed(2))).replace(/\.0$/, '');
644
+ }
645
+
646
+ export function chartLineColor(line) {
647
+ if (/over|unbooked/i.test(line)) {
648
+ return 'yellow';
649
+ }
650
+ if (line.includes('█')) {
651
+ return 'green';
652
+ }
653
+ return undefined;
654
+ }
655
+
656
+ export function renderProjectChartText(lines = []) {
657
+ return lines.map((line) => {
658
+ if (/over|unbooked/i.test(line)) {
659
+ return chalk.yellow(line);
660
+ }
661
+ if (line.includes('█')) {
662
+ return chalk.green(line);
663
+ }
664
+ return line;
665
+ }).join('\n');
666
+ }
667
+
668
+ export function commandItems() {
669
+ return MANUAL_COMMANDS
670
+ .filter((command) => command.hidden !== true)
671
+ .map((command) => ({
672
+ label: `${command.name} ${command.description}`,
673
+ value: command.name
674
+ }));
675
+ }
@@ -0,0 +1,21 @@
1
+ import { createInterface } from 'node:readline/promises';
2
+
3
+ export function createManualLineReader({
4
+ input,
5
+ output
6
+ } = {}) {
7
+ const rl = createInterface({
8
+ input,
9
+ output,
10
+ terminal: Boolean(input?.isTTY && output?.isTTY)
11
+ });
12
+
13
+ return {
14
+ readLine(prompt = 'logwork> ') {
15
+ return rl.question(prompt);
16
+ },
17
+ close() {
18
+ rl.close();
19
+ }
20
+ };
21
+ }