tledger 0.3.0 → 0.4.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 +283 -155
- package/bin/token-ledger-cache-data.mjs +492 -0
- package/bin/token-ledger-cache-image.mjs +7 -1147
- package/bin/token-ledger-cache-sections.mjs +848 -0
- package/bin/token-ledger-cost-terminal.mjs +234 -0
- package/bin/token-ledger-image-layout.mjs +20 -0
- package/bin/token-ledger-image-primitives.mjs +192 -0
- package/bin/token-ledger-report-data.mjs +1159 -0
- package/bin/token-ledger-source-status.mjs +31 -0
- package/bin/token-ledger-terminal.mjs +245 -69
- package/bin/token-ledger-trend-image.mjs +2039 -1599
- package/bin/token-ledger-trend-terminal.mjs +273 -150
- package/bin/token-ledger-trend.mjs +262 -210
- package/bin/token-ledger-tui.mjs +180 -51
- package/bin/token-ledger.mjs +747 -194
- package/docs/durable-ledger-operations.md +198 -0
- package/docs/release-notes-0.4.0.md +41 -0
- package/docs/token-ledger-report-7-day.png +0 -0
- package/lib/token-ledger-calendar.mjs +225 -0
- package/lib/token-ledger-collection.mjs +100 -0
- package/lib/token-ledger-importer.mjs +3334 -480
- package/lib/token-ledger-labels.mjs +66 -0
- package/lib/token-ledger-ledger.mjs +6056 -0
- package/lib/token-ledger-quota-contract.mjs +38 -0
- package/lib/token-ledger-range-analysis.mjs +120 -0
- package/lib/token-ledger-rates.mjs +330 -0
- package/lib/token-ledger-snapshot.mjs +336 -35
- package/lib/token-ledger-terminal-text.mjs +11 -0
- package/lib/token-ledger-usage.mjs +339 -33
- package/package.json +13 -10
- package/bin/token-ledger-rates.mjs +0 -65
package/bin/token-ledger-tui.mjs
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { emitKeypressEvents } from "node:readline";
|
|
3
|
+
|
|
1
4
|
import { renderFullscreen, SCREEN_BASE } from "./token-ledger-terminal.mjs";
|
|
2
5
|
import { actionFor } from "./token-ledger-controls.mjs";
|
|
3
6
|
|
|
@@ -9,19 +12,29 @@ const HIDE_CURSOR = "\u001b[?25l";
|
|
|
9
12
|
const SHOW_CURSOR = "\u001b[?25h";
|
|
10
13
|
const CLEAR_SCREEN = "\u001b[2J\u001b[H";
|
|
11
14
|
const RESET = "\u001b[0m";
|
|
15
|
+
const SUPPORTED_SIGNALS = ["SIGINT", "SIGHUP", "SIGTERM"];
|
|
16
|
+
const SIGNAL_EXIT_CODES = {
|
|
17
|
+
SIGINT: 2,
|
|
18
|
+
SIGHUP: 1,
|
|
19
|
+
SIGTERM: 15,
|
|
20
|
+
};
|
|
12
21
|
|
|
13
|
-
export function startInteractive(view
|
|
22
|
+
export function startInteractive(view, {
|
|
23
|
+
stdin = process.stdin,
|
|
24
|
+
stdout = process.stdout,
|
|
25
|
+
signalTarget = process,
|
|
26
|
+
render = renderFullscreen,
|
|
27
|
+
} = {}) {
|
|
14
28
|
const {
|
|
15
29
|
options,
|
|
16
30
|
snapshot,
|
|
17
31
|
snapshotFreshness,
|
|
32
|
+
sourceStatus,
|
|
18
33
|
bounds,
|
|
19
34
|
events,
|
|
20
35
|
rows,
|
|
21
36
|
allRows,
|
|
22
37
|
} = view;
|
|
23
|
-
const stdin = process.stdin;
|
|
24
|
-
const stdout = process.stdout;
|
|
25
38
|
// Interactive mode needs a raw-mode-capable terminal on both ends. Capture
|
|
26
39
|
// the capability once here; the handlers below rely on it unconditionally.
|
|
27
40
|
const setRawMode =
|
|
@@ -33,63 +46,179 @@ export function startInteractive(view) {
|
|
|
33
46
|
return new Promise((resolve, reject) => {
|
|
34
47
|
let selectedIndex = 0;
|
|
35
48
|
let closed = false;
|
|
49
|
+
let rawModeTouched = false;
|
|
50
|
+
let streamFlowTouched = false;
|
|
51
|
+
let terminalStateTouched = false;
|
|
52
|
+
const previousRawMode = stdin.isRaw === true;
|
|
53
|
+
const previousFlowing = stdin.readableFlowing;
|
|
54
|
+
const registrations = [];
|
|
55
|
+
// Decode complete keys across stream chunks without attaching readline's
|
|
56
|
+
// internal listeners to the caller-owned terminal stream.
|
|
57
|
+
const keyInput = new EventEmitter();
|
|
58
|
+
emitKeypressEvents(keyInput, { escapeCodeTimeout: 50 });
|
|
59
|
+
|
|
60
|
+
const registerListener = (target, event, handler, method = "on") => {
|
|
61
|
+
const registration = { target, event, handler };
|
|
62
|
+
registrations.push(registration);
|
|
63
|
+
target[method](event, handler);
|
|
64
|
+
};
|
|
36
65
|
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
snapshotFreshness,
|
|
44
|
-
bounds,
|
|
45
|
-
events,
|
|
46
|
-
rows,
|
|
47
|
-
allRows,
|
|
48
|
-
width,
|
|
49
|
-
height,
|
|
50
|
-
});
|
|
51
|
-
stdout.write(`${SCREEN_BASE}${CLEAR_SCREEN}${screen}`);
|
|
66
|
+
const attemptCleanup = (cleanupErrors, action) => {
|
|
67
|
+
try {
|
|
68
|
+
action();
|
|
69
|
+
} catch (error) {
|
|
70
|
+
cleanupErrors.push(error);
|
|
71
|
+
}
|
|
52
72
|
};
|
|
53
73
|
|
|
54
|
-
|
|
74
|
+
function teardown(
|
|
75
|
+
error = null,
|
|
76
|
+
afterRestore = null,
|
|
77
|
+
{ preserveSignalGuards = false } = {},
|
|
78
|
+
) {
|
|
55
79
|
if (closed) return;
|
|
56
80
|
closed = true;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
81
|
+
const cleanupErrors = [];
|
|
82
|
+
let restorationPending = false;
|
|
83
|
+
let settled = false;
|
|
84
|
+
const signalGuards = preserveSignalGuards
|
|
85
|
+
? registrations.filter(({ target, event }) =>
|
|
86
|
+
target === signalTarget && SUPPORTED_SIGNALS.includes(event))
|
|
87
|
+
: [];
|
|
88
|
+
const signalGuardSet = new Set(signalGuards);
|
|
89
|
+
const settle = () => {
|
|
90
|
+
if (settled) return;
|
|
91
|
+
settled = true;
|
|
92
|
+
for (const { target, event, handler } of signalGuards) {
|
|
93
|
+
attemptCleanup(cleanupErrors, () => target.off(event, handler));
|
|
94
|
+
}
|
|
95
|
+
if (afterRestore) afterRestore();
|
|
96
|
+
if (error !== null) reject(error);
|
|
97
|
+
else if (cleanupErrors.length > 0) reject(cleanupErrors[0]);
|
|
98
|
+
else resolve();
|
|
99
|
+
};
|
|
66
100
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
101
|
+
const activeRegistrations = registrations.splice(0);
|
|
102
|
+
for (const registration of activeRegistrations) {
|
|
103
|
+
const { target, event, handler } = registration;
|
|
104
|
+
if (signalGuardSet.has(registration)) {
|
|
105
|
+
registrations.push(registration);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
attemptCleanup(cleanupErrors, () => target.off(event, handler));
|
|
73
109
|
}
|
|
74
|
-
if (
|
|
75
|
-
|
|
76
|
-
draw();
|
|
77
|
-
return;
|
|
110
|
+
if (rawModeTouched) {
|
|
111
|
+
attemptCleanup(cleanupErrors, () => setRawMode(previousRawMode));
|
|
78
112
|
}
|
|
79
|
-
if (
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
113
|
+
if (streamFlowTouched) {
|
|
114
|
+
attemptCleanup(cleanupErrors, () => {
|
|
115
|
+
if (previousFlowing === true) stdin.resume();
|
|
116
|
+
else stdin.pause();
|
|
117
|
+
});
|
|
83
118
|
}
|
|
84
|
-
|
|
119
|
+
if (terminalStateTouched) {
|
|
120
|
+
attemptCleanup(cleanupErrors, () => {
|
|
121
|
+
const restoration = `${RESET}${SHOW_CURSOR}${EXIT_ALT_SCREEN}`;
|
|
122
|
+
if (afterRestore) {
|
|
123
|
+
restorationPending = true;
|
|
124
|
+
stdout.write(restoration, settle);
|
|
125
|
+
} else {
|
|
126
|
+
stdout.write(restoration);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!restorationPending) settle();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function draw() {
|
|
135
|
+
if (closed) return;
|
|
136
|
+
try {
|
|
137
|
+
const width = Math.max(40, stdout.columns || 120);
|
|
138
|
+
const height = Math.max(12, stdout.rows || 32);
|
|
139
|
+
const screen = render({
|
|
140
|
+
options: { ...options, forceColor: true, selectedIndex },
|
|
141
|
+
snapshot,
|
|
142
|
+
snapshotFreshness,
|
|
143
|
+
sourceStatus,
|
|
144
|
+
bounds,
|
|
145
|
+
events,
|
|
146
|
+
rows,
|
|
147
|
+
allRows,
|
|
148
|
+
width,
|
|
149
|
+
height,
|
|
150
|
+
});
|
|
151
|
+
stdout.write(`${SCREEN_BASE}${CLEAR_SCREEN}${screen}`);
|
|
152
|
+
} catch (drawError) {
|
|
153
|
+
teardown(drawError);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
85
156
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
157
|
+
function onSignal(signal) {
|
|
158
|
+
if (closed) return;
|
|
159
|
+
const exitCode = 128 + SIGNAL_EXIT_CODES[signal];
|
|
160
|
+
signalTarget.exitCode = exitCode;
|
|
161
|
+
teardown(null, () => {
|
|
162
|
+
if (signalTarget.pid) signalTarget.kill(signalTarget.pid, signal);
|
|
163
|
+
}, { preserveSignalGuards: true });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function onStreamError(streamError) {
|
|
167
|
+
teardown(streamError ?? new Error("Interactive stream failed."));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function onKeypress(_text, key) {
|
|
171
|
+
if (closed) return;
|
|
172
|
+
try {
|
|
173
|
+
const action = actionFor(key.sequence);
|
|
174
|
+
if (action === "quit") {
|
|
175
|
+
teardown();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (action === "up") {
|
|
179
|
+
selectedIndex = Math.max(0, selectedIndex - 1);
|
|
180
|
+
draw();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (action === "down") {
|
|
184
|
+
selectedIndex = Math.min(Math.max(0, rows.length - 1), selectedIndex + 1);
|
|
185
|
+
draw();
|
|
186
|
+
}
|
|
187
|
+
} catch (inputError) {
|
|
188
|
+
teardown(inputError);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function onData(input) {
|
|
193
|
+
if (closed) return;
|
|
194
|
+
try {
|
|
195
|
+
keyInput.emit("data", Buffer.isBuffer(input) ? input : Buffer.from(String(input)));
|
|
196
|
+
} catch (inputError) {
|
|
197
|
+
teardown(inputError);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
registerListener(stdin, "error", onStreamError);
|
|
203
|
+
registerListener(stdout, "error", onStreamError);
|
|
204
|
+
for (const signal of SUPPORTED_SIGNALS) {
|
|
205
|
+
// Keep a live guard through deferred terminal restoration so a second
|
|
206
|
+
// signal cannot reach Node's default termination path mid-cleanup.
|
|
207
|
+
registerListener(signalTarget, signal, () => onSignal(signal));
|
|
208
|
+
}
|
|
209
|
+
rawModeTouched = true;
|
|
210
|
+
setRawMode(true);
|
|
211
|
+
stdin.setEncoding("utf8");
|
|
212
|
+
streamFlowTouched = true;
|
|
213
|
+
stdin.resume();
|
|
214
|
+
registerListener(keyInput, "keypress", onKeypress);
|
|
215
|
+
registerListener(stdin, "data", onData);
|
|
216
|
+
registerListener(stdout, "resize", draw);
|
|
217
|
+
terminalStateTouched = true;
|
|
218
|
+
stdout.write(`${ENTER_ALT_SCREEN}${SCREEN_BASE}${HIDE_CURSOR}${CLEAR_SCREEN}`);
|
|
219
|
+
draw();
|
|
220
|
+
} catch (setupError) {
|
|
221
|
+
teardown(setupError);
|
|
222
|
+
}
|
|
94
223
|
});
|
|
95
224
|
}
|