@jlmarcos99/todos 0.0.0-stage → 1.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 +139 -2
- package/dist/app/src/app.js +39 -0
- package/dist/app/src/application/operation-queue.js +9 -0
- package/dist/app/src/application/task-service.js +172 -0
- package/dist/app/src/cli/format.js +6 -0
- package/dist/app/src/cli/prompts.js +15 -0
- package/dist/app/src/cli.js +174 -0
- package/dist/app/src/config/config.js +25 -0
- package/dist/app/src/config/paths.js +95 -0
- package/dist/app/src/model/activity.js +1 -0
- package/dist/app/src/model/daily.js +34 -0
- package/dist/app/src/model/task.js +1 -0
- package/dist/app/src/model/vault.js +1 -0
- package/dist/app/src/storage/atomic-file.js +15 -0
- package/dist/app/src/storage/record-codec.js +57 -0
- package/dist/app/src/storage/store.js +187 -0
- package/dist/app/src/sync/git-client.js +50 -0
- package/dist/app/src/sync/git-repository.js +291 -0
- package/dist/app/src/sync/sync-service.js +97 -0
- package/dist/app/src/tui/demo.js +93 -0
- package/dist/app/src/tui/screen.js +325 -0
- package/dist/app/src/tui/session.js +167 -0
- package/dist/app/src/tui/theme.js +17 -0
- package/dist/app/src/tui/view.js +279 -0
- package/dist/index.js +3 -0
- package/package.json +55 -4
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
|
|
2
|
+
function color(code, text) {
|
|
3
|
+
if (process.env.NO_COLOR !== undefined)
|
|
4
|
+
return text;
|
|
5
|
+
return `\x1b[${code}m${text}\x1b[0m`;
|
|
6
|
+
}
|
|
7
|
+
export const theme = {
|
|
8
|
+
accent: (text) => color('38;5;115', text),
|
|
9
|
+
muted: (text) => color('38;5;245', text),
|
|
10
|
+
bold: (text) => color('1', text),
|
|
11
|
+
selected: (text) => color('48;5;236;38;5;115', text),
|
|
12
|
+
};
|
|
13
|
+
export function padLine(text, width) {
|
|
14
|
+
const clipped = truncateToWidth(text, Math.max(0, width));
|
|
15
|
+
const padding = ' '.repeat(Math.max(0, width - visibleWidth(clipped)));
|
|
16
|
+
return clipped + padding;
|
|
17
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { truncateToWidth, wrapTextWithAnsi } from '@earendil-works/pi-tui';
|
|
2
|
+
import { padLine, theme } from "./theme.js";
|
|
3
|
+
const MIN_COLUMNS = 40;
|
|
4
|
+
const MIN_ROWS = 17;
|
|
5
|
+
export const SIDEBAR_BREAKPOINT = 85;
|
|
6
|
+
const SIDEBAR_WIDTH = 23;
|
|
7
|
+
const COLUMN_SEPARATOR_WIDTH = 3;
|
|
8
|
+
const FOOTER_ROWS = 3;
|
|
9
|
+
const WORKSPACE_HEADER_ROWS = 8;
|
|
10
|
+
const TASK_DETAIL_ROWS = 5;
|
|
11
|
+
const ACTIVITY_HEADER_ROWS = 4;
|
|
12
|
+
const DAILY_SCROLL_HINT_ROWS = 1;
|
|
13
|
+
const STATUS_LABELS = {
|
|
14
|
+
todo: 'To do',
|
|
15
|
+
'in-progress': 'In progress',
|
|
16
|
+
done: 'Done',
|
|
17
|
+
};
|
|
18
|
+
const ACTIVITY_LABELS = {
|
|
19
|
+
started: 'Started', completed: 'Completed', reopened: 'Reopened', note: 'Progress note',
|
|
20
|
+
};
|
|
21
|
+
const STATUS_SYMBOLS = {
|
|
22
|
+
todo: '○', 'in-progress': '◐', done: '✓',
|
|
23
|
+
};
|
|
24
|
+
const VIEW_LABELS = {
|
|
25
|
+
current: 'Tasks', daily: 'Daily', history: 'History',
|
|
26
|
+
};
|
|
27
|
+
export function mainColumnWidth(width) {
|
|
28
|
+
return width >= SIDEBAR_BREAKPOINT
|
|
29
|
+
? width - SIDEBAR_WIDTH - COLUMN_SEPARATOR_WIDTH
|
|
30
|
+
: width;
|
|
31
|
+
}
|
|
32
|
+
export function activityViewportHeight(terminalRows) {
|
|
33
|
+
return Math.max(1, terminalRows - 1 - FOOTER_ROWS - ACTIVITY_HEADER_ROWS);
|
|
34
|
+
}
|
|
35
|
+
export function dailyViewportHeight(terminalRows) {
|
|
36
|
+
return Math.max(1, terminalRows - 1 - FOOTER_ROWS - WORKSPACE_HEADER_ROWS - DAILY_SCROLL_HINT_ROWS);
|
|
37
|
+
}
|
|
38
|
+
export function renderScreen(session, options) {
|
|
39
|
+
const { width, rows, showHelp, inputLine } = options;
|
|
40
|
+
const height = Math.max(1, rows - 1);
|
|
41
|
+
if (width < MIN_COLUMNS || rows < MIN_ROWS) {
|
|
42
|
+
return [' TODOS', '', ' Resize your terminal to 40 × 17.', ' q quit']
|
|
43
|
+
.slice(0, height)
|
|
44
|
+
.map(line => truncateToWidth(line, width));
|
|
45
|
+
}
|
|
46
|
+
const mainWidth = mainColumnWidth(width);
|
|
47
|
+
const contentHeight = height - FOOTER_ROWS;
|
|
48
|
+
let content;
|
|
49
|
+
if (showHelp) {
|
|
50
|
+
content = renderHelp();
|
|
51
|
+
}
|
|
52
|
+
else if (options.showActivity) {
|
|
53
|
+
content = renderActivity(session, mainWidth, contentHeight, options.activityOffset ?? 0, options.activityTaskId);
|
|
54
|
+
}
|
|
55
|
+
else if (session.view === 'daily') {
|
|
56
|
+
content = renderDailyWorkspace(session, mainWidth, rows, options.dailyOffset ?? 0);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
content = renderWorkspace(session, mainWidth, contentHeight);
|
|
60
|
+
}
|
|
61
|
+
const body = content.slice(0, contentHeight);
|
|
62
|
+
while (body.length < contentHeight)
|
|
63
|
+
body.push('');
|
|
64
|
+
body.push(...renderFooter(session, inputLine, options.showActivity));
|
|
65
|
+
if (width < SIDEBAR_BREAKPOINT)
|
|
66
|
+
return body.map(line => truncateToWidth(line, width));
|
|
67
|
+
const sidebar = renderSidebar(session);
|
|
68
|
+
const separator = ` ${theme.muted('│')} `;
|
|
69
|
+
return body.map((line, index) => {
|
|
70
|
+
const left = padLine(sidebar[index] ?? '', SIDEBAR_WIDTH);
|
|
71
|
+
return left + separator + padLine(line, mainWidth);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
function renderWorkspace(session, width, height) {
|
|
75
|
+
const lines = renderHeader(session, width);
|
|
76
|
+
const listHeight = Math.max(1, height - WORKSPACE_HEADER_ROWS - TASK_DETAIL_ROWS);
|
|
77
|
+
const pageStart = Math.max(0, session.selected - listHeight + 1);
|
|
78
|
+
const tasks = session.tasks;
|
|
79
|
+
const page = tasks.slice(pageStart, pageStart + listHeight);
|
|
80
|
+
if (!tasks.length)
|
|
81
|
+
lines.push('', theme.muted(emptyMessage(session)));
|
|
82
|
+
page.forEach((task, index) => {
|
|
83
|
+
const taskIndex = pageStart + index;
|
|
84
|
+
lines.push(renderTaskRow(task.name, task.status, taskIndex === session.selected, width));
|
|
85
|
+
});
|
|
86
|
+
while (lines.length < WORKSPACE_HEADER_ROWS + listHeight)
|
|
87
|
+
lines.push('');
|
|
88
|
+
lines.push(` ${theme.muted('─'.repeat(width - 2))}`);
|
|
89
|
+
lines.push(...renderTaskDetails(session, width - 4));
|
|
90
|
+
return lines;
|
|
91
|
+
}
|
|
92
|
+
/** Task row offsets let keyboard navigation follow variable-height note lists. */
|
|
93
|
+
export function dailyListLayout(session, width) {
|
|
94
|
+
const lines = [];
|
|
95
|
+
const taskRows = [];
|
|
96
|
+
session.dailyItems.forEach((item, index) => {
|
|
97
|
+
taskRows.push(lines.length);
|
|
98
|
+
lines.push(renderTaskRow(item.name, item.status, index === session.selected, width));
|
|
99
|
+
const notes = item.entries.filter(entry => entry.note);
|
|
100
|
+
for (const entry of notes) {
|
|
101
|
+
const date = entry.date.toLocaleDateString('en-GB', { day: '2-digit', month: 'short' });
|
|
102
|
+
const time = entry.date.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' });
|
|
103
|
+
const prefix = ` • ${date} · ${time} `;
|
|
104
|
+
const wrapped = wrapTextWithAnsi(entry.note, Math.max(1, width - prefix.length - 1));
|
|
105
|
+
wrapped.forEach((line, lineIndex) => {
|
|
106
|
+
const indent = lineIndex === 0 ? prefix : ' '.repeat(prefix.length);
|
|
107
|
+
lines.push(theme.muted(indent + line));
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
if (!notes.length)
|
|
111
|
+
lines.push(theme.muted(' No notes for this day.'));
|
|
112
|
+
lines.push('');
|
|
113
|
+
});
|
|
114
|
+
return { lines, taskRows };
|
|
115
|
+
}
|
|
116
|
+
function renderDailyWorkspace(session, width, rows, offset) {
|
|
117
|
+
const header = renderHeader(session, width);
|
|
118
|
+
const { lines } = dailyListLayout(session, width);
|
|
119
|
+
const viewportHeight = dailyViewportHeight(rows);
|
|
120
|
+
const start = Math.min(offset, Math.max(0, lines.length - viewportHeight));
|
|
121
|
+
const page = lines.slice(start, start + viewportHeight);
|
|
122
|
+
if (!lines.length)
|
|
123
|
+
page.push(theme.muted(emptyMessage(session)));
|
|
124
|
+
while (page.length < viewportHeight)
|
|
125
|
+
page.push('');
|
|
126
|
+
const directions = [
|
|
127
|
+
...(start > 0 ? ['↑ More above'] : []),
|
|
128
|
+
...(start + viewportHeight < lines.length ? ['↓ More below'] : []),
|
|
129
|
+
];
|
|
130
|
+
const hint = directions.length ? ` ${directions.join(' · ')} · Ctrl+d/u scroll` : '';
|
|
131
|
+
return [...header, ...page, theme.muted(hint)];
|
|
132
|
+
}
|
|
133
|
+
function renderHeader(session, width) {
|
|
134
|
+
const { vault, view } = session;
|
|
135
|
+
const title = `${vault.name} / ${VIEW_LABELS[view]}`;
|
|
136
|
+
const currentCount = vault.tasks.filter(task => !session.isInHistory(task)).length;
|
|
137
|
+
const historyCount = vault.tasks.length - currentCount;
|
|
138
|
+
const tabs = [
|
|
139
|
+
renderTab(`1 Tasks ${currentCount}`, view === 'current'),
|
|
140
|
+
renderTab('2 Daily', view === 'daily'),
|
|
141
|
+
renderTab(`3 History ${historyCount}`, view === 'history'),
|
|
142
|
+
].join(' ');
|
|
143
|
+
let subtitle = vault.description;
|
|
144
|
+
let context = session.syncMessage;
|
|
145
|
+
if (view === 'daily') {
|
|
146
|
+
const date = session.dailyDate.toLocaleDateString('en-US', {
|
|
147
|
+
weekday: 'short', month: 'short', day: 'numeric', year: 'numeric',
|
|
148
|
+
});
|
|
149
|
+
const previousDay = new Date(session.dailyDate);
|
|
150
|
+
previousDay.setDate(previousDay.getDate() - 1);
|
|
151
|
+
const startDate = previousDay.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
152
|
+
const completed = session.dailyItems.filter(item => item.status === 'done').length;
|
|
153
|
+
const ongoing = session.dailyItems.filter(item => item.status === 'in-progress').length;
|
|
154
|
+
const noteCount = session.dailyItems.reduce((count, item) => count + item.entries.filter(entry => entry.note).length, 0);
|
|
155
|
+
subtitle = `${startDate} – ${date} · [ / ] change window · t today`;
|
|
156
|
+
context = `${session.syncMessage} · ${completed} done · ${ongoing} in progress · ${noteCount} notes`;
|
|
157
|
+
}
|
|
158
|
+
return [
|
|
159
|
+
` ${theme.accent(theme.bold('◈ TODOS'))} ${theme.muted('/ your work, day by day')}`,
|
|
160
|
+
` ${theme.muted(context)}`,
|
|
161
|
+
'',
|
|
162
|
+
` ${theme.bold(title)}`,
|
|
163
|
+
` ${theme.muted(subtitle)}`,
|
|
164
|
+
'',
|
|
165
|
+
` ${tabs}`,
|
|
166
|
+
` ${theme.muted('─'.repeat(width - 2))}`,
|
|
167
|
+
];
|
|
168
|
+
}
|
|
169
|
+
function renderTab(label, active) {
|
|
170
|
+
return active ? theme.accent(theme.bold(label)) : theme.muted(label);
|
|
171
|
+
}
|
|
172
|
+
function renderTaskRow(name, status, selected, width) {
|
|
173
|
+
const cursor = selected ? '›' : ' ';
|
|
174
|
+
const label = `${STATUS_SYMBOLS[status]} ${STATUS_LABELS[status].padEnd(11)}`;
|
|
175
|
+
const row = ` ${cursor} ${label} ${truncateToWidth(name, width - 20)}`;
|
|
176
|
+
if (selected)
|
|
177
|
+
return theme.selected(padLine(row, width - 1));
|
|
178
|
+
return status === 'done' ? theme.muted(row) : row;
|
|
179
|
+
}
|
|
180
|
+
function renderTaskDetails(session, width) {
|
|
181
|
+
const task = session.selectedTask;
|
|
182
|
+
if (!task)
|
|
183
|
+
return [];
|
|
184
|
+
const description = task.description ?? 'Press n to record a progress note.';
|
|
185
|
+
const hint = 's: start · n: progress note · space: complete/reopen';
|
|
186
|
+
return [
|
|
187
|
+
` ${theme.bold(truncateToWidth(task.name, width))}`,
|
|
188
|
+
...wrapTextWithAnsi(description, width).slice(0, 2).map(line => ` ${theme.muted(line)}`),
|
|
189
|
+
` ${theme.accent(`${STATUS_LABELS[task.status]} · ${hint}`)}`,
|
|
190
|
+
];
|
|
191
|
+
}
|
|
192
|
+
function emptyMessage(session) {
|
|
193
|
+
if (session.query)
|
|
194
|
+
return ' No matches. Press Esc to clear the search.';
|
|
195
|
+
if (session.view === 'history')
|
|
196
|
+
return ' No archived tasks yet.';
|
|
197
|
+
if (session.view === 'daily')
|
|
198
|
+
return ' No updates on this day. Try [ or ] to browse.';
|
|
199
|
+
return ' All clear. Press a to add a task.';
|
|
200
|
+
}
|
|
201
|
+
function renderSidebar(session) {
|
|
202
|
+
const lines = ['', '', '', ` ${theme.muted('VAULTS')}`, ''];
|
|
203
|
+
session.vaults.forEach((vault, index) => {
|
|
204
|
+
const pending = vault.tasks.filter(task => task.status !== 'done').length;
|
|
205
|
+
const label = index === session.vaultIndex
|
|
206
|
+
? theme.accent(theme.bold(` ▸ ${vault.name}`))
|
|
207
|
+
: theme.muted(` ${vault.name}`);
|
|
208
|
+
lines.push(label, theme.muted(` ${pending} open tasks`), '');
|
|
209
|
+
});
|
|
210
|
+
lines.push('', theme.muted(' Tab / ⇧Tab switch · V new vault'), '', theme.muted(' ──────────────────'), theme.muted(` ${session.syncMessage}`));
|
|
211
|
+
return lines;
|
|
212
|
+
}
|
|
213
|
+
function renderHelp() {
|
|
214
|
+
return [
|
|
215
|
+
' ◈ TODOS / KEYBOARD SHORTCUTS', '',
|
|
216
|
+
' j/k · gg/G Move · first/last task',
|
|
217
|
+
' Tab / ⇧Tab Switch vault',
|
|
218
|
+
' h/l · 1/2/3 Tasks / Daily / History',
|
|
219
|
+
' Space Complete / reopen task',
|
|
220
|
+
' s Start working on a task',
|
|
221
|
+
' n Write a progress note',
|
|
222
|
+
' Enter Read task activity',
|
|
223
|
+
' [ / ] Previous / next two-day window',
|
|
224
|
+
' Ctrl+d/u Scroll daily notes',
|
|
225
|
+
' t Today’s daily',
|
|
226
|
+
' d Delete task; press d again to confirm',
|
|
227
|
+
' a · V · / Add task · vault · search',
|
|
228
|
+
' r Sync, or link a Git remote if local-only',
|
|
229
|
+
' Esc · ? Back / clear · help',
|
|
230
|
+
' q / Ctrl+C Quit', '',
|
|
231
|
+
' Daily keeps progress by local calendar date.',
|
|
232
|
+
' Completed tasks move to History after 24 h.',
|
|
233
|
+
].map(theme.muted);
|
|
234
|
+
}
|
|
235
|
+
export function taskActivityLines(session, width, taskId = session.selectedTask?.id) {
|
|
236
|
+
const task = session.vault.tasks.find(task => task.id === taskId);
|
|
237
|
+
if (!task)
|
|
238
|
+
return [];
|
|
239
|
+
const entries = session.view === 'daily'
|
|
240
|
+
? session.dailyItems.find(item => item.task.id === task.id)?.entries ?? []
|
|
241
|
+
: session.vault.activity.filter(entry => entry.taskId === task.id);
|
|
242
|
+
return entries.flatMap(entry => renderActivityEntry(entry, Math.max(1, width - 4)));
|
|
243
|
+
}
|
|
244
|
+
function renderActivityEntry(entry, width) {
|
|
245
|
+
const date = entry.date.toLocaleString('en-US', {
|
|
246
|
+
month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false,
|
|
247
|
+
});
|
|
248
|
+
const heading = ` ${date} · ${ACTIVITY_LABELS[entry.kind]}`;
|
|
249
|
+
const note = entry.note ? wrapTextWithAnsi(entry.note, width).map(line => ` ${line}`) : [];
|
|
250
|
+
return [theme.accent(heading), ...note, ''];
|
|
251
|
+
}
|
|
252
|
+
function renderActivity(session, width, height, offset, taskId) {
|
|
253
|
+
const task = session.vault.tasks.find(task => task.id === taskId);
|
|
254
|
+
const title = task?.name ?? 'Task activity';
|
|
255
|
+
const allLines = taskActivityLines(session, width, taskId);
|
|
256
|
+
const availableRows = Math.max(1, height - ACTIVITY_HEADER_ROWS);
|
|
257
|
+
const start = Math.min(offset, Math.max(0, allLines.length - availableRows));
|
|
258
|
+
const count = `${Math.min(start + 1, allLines.length)}–${Math.min(start + availableRows, allLines.length)} / ${allLines.length}`;
|
|
259
|
+
return [
|
|
260
|
+
` ${theme.bold(truncateToWidth(title, width - 3))}`,
|
|
261
|
+
` ${theme.muted(`ACTIVITY · ${count} · j/k scroll · Esc back`)}`,
|
|
262
|
+
` ${theme.muted('─'.repeat(width - 2))}`,
|
|
263
|
+
'',
|
|
264
|
+
...(allLines.length ? allLines.slice(start, start + availableRows) : [theme.muted(' No activity yet. Press Esc, then n to add a note.')]),
|
|
265
|
+
];
|
|
266
|
+
}
|
|
267
|
+
function renderFooter(session, inputLine, showActivity) {
|
|
268
|
+
const message = session.query
|
|
269
|
+
? ` ${theme.accent(`/ ${session.query}`)} ${theme.muted('· Esc clear')}`
|
|
270
|
+
: ` ${theme.muted(session.message)}`;
|
|
271
|
+
if (showActivity) {
|
|
272
|
+
return [message, ' j/k scroll · gg/G first/last update', ' Esc / Enter back · q quit'];
|
|
273
|
+
}
|
|
274
|
+
return [
|
|
275
|
+
inputLine ?? message,
|
|
276
|
+
` ${theme.accent('j/k')} move ${theme.accent('Tab')} vault ${theme.accent('h/l')} views ${theme.accent('space')} done`,
|
|
277
|
+
` ${theme.accent('s')} start ${theme.accent('n')} note ${theme.accent('d')} delete ${theme.accent('V')} vault ${theme.accent('r')} sync ${theme.accent('?')} help`,
|
|
278
|
+
];
|
|
279
|
+
}
|
package/dist/index.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,57 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jlmarcos99/todos",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "A keyboard-first task manager and daily work log for the terminal",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/jlmarcos99/todos.git"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/jlmarcos99/todos#readme",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/jlmarcos99/todos/issues"
|
|
12
|
+
},
|
|
13
|
+
"author": "jlmarcos99",
|
|
14
|
+
"bin": {
|
|
15
|
+
"todos": "dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist/index.js",
|
|
19
|
+
"dist/app/src/",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public",
|
|
24
|
+
"provenance": true
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"start": "npm run build && node dist/index.js",
|
|
28
|
+
"dev": "npm run build && node dist/index.js",
|
|
29
|
+
"init": "npm run build && node dist/index.js init",
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"build": "tsc && node scripts/prepare-bin.mjs",
|
|
32
|
+
"start:prod": "node dist/index.js",
|
|
33
|
+
"test": "npm run build && node --test dist/app/tests/*.test.js",
|
|
34
|
+
"changeset": "changeset",
|
|
35
|
+
"release": "npm run build && changeset publish"
|
|
36
|
+
},
|
|
37
|
+
"keywords": [
|
|
38
|
+
"todo",
|
|
39
|
+
"task-manager",
|
|
40
|
+
"daily-log",
|
|
41
|
+
"terminal",
|
|
42
|
+
"tui"
|
|
43
|
+
],
|
|
44
|
+
"license": "ISC",
|
|
45
|
+
"type": "module",
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=22.19.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/node": "22.20.0",
|
|
51
|
+
"typescript": "5.9.3",
|
|
52
|
+
"@changesets/cli": "^2.31.0"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"@earendil-works/pi-tui": "0.83.0"
|
|
56
|
+
}
|
|
57
|
+
}
|