opencode-token-tracker 1.6.2 → 1.6.3
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/dist/bin/opencode-tokens.js +131 -125
- package/package.json +1 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { join } from "path";
|
|
5
|
-
import {
|
|
2
|
+
import { closeSync, copyFileSync, existsSync, mkdirSync, openSync, readFileSync, readSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { BUILTIN_PRICING, DEFAULT_CONFIG, findModelConfigPricing, formatCost, formatTokens, getStartOfDay, getStartOfMonth, getStartOfWeek, validateConfig } from "../lib/shared.js";
|
|
6
6
|
const CONFIG_DIR = join(homedir(), ".config", "opencode");
|
|
7
7
|
const CONFIG_FILE = join(CONFIG_DIR, "token-tracker.json");
|
|
8
8
|
const LOG_FILE = join(CONFIG_DIR, "logs", "token-tracker", "tokens.jsonl");
|
|
@@ -10,15 +10,15 @@ const LOG_FILE = join(CONFIG_DIR, "logs", "token-tracker", "tokens.jsonl");
|
|
|
10
10
|
// Helpers
|
|
11
11
|
// ============================================================================
|
|
12
12
|
function padRight(str, len) {
|
|
13
|
-
return str.length >= len ? str : str
|
|
13
|
+
return str.length >= len ? str : `${str}${" ".repeat(len - str.length)}`;
|
|
14
14
|
}
|
|
15
15
|
function padLeft(str, len) {
|
|
16
|
-
return str.length >= len ? str : " ".repeat(len - str.length)
|
|
16
|
+
return str.length >= len ? str : `${" ".repeat(len - str.length)}${str}`;
|
|
17
17
|
}
|
|
18
18
|
function truncateSessionId(sessionId) {
|
|
19
19
|
if (!sessionId)
|
|
20
20
|
return "unknown";
|
|
21
|
-
return sessionId.length > 16 ? sessionId.slice(0, 14)
|
|
21
|
+
return sessionId.length > 16 ? `${sessionId.slice(0, 14)}…` : sessionId;
|
|
22
22
|
}
|
|
23
23
|
function parseArgs(args) {
|
|
24
24
|
const positional = [];
|
|
@@ -70,9 +70,6 @@ function flagValue(flags, name) {
|
|
|
70
70
|
const v = flags.get(name);
|
|
71
71
|
return typeof v === "string" ? v : undefined;
|
|
72
72
|
}
|
|
73
|
-
function flagBool(flags, name) {
|
|
74
|
-
return flags.has(name);
|
|
75
|
-
}
|
|
76
73
|
// ============================================================================
|
|
77
74
|
// Data Loading
|
|
78
75
|
// ============================================================================
|
|
@@ -178,10 +175,11 @@ function groupBy(entries, keyFn) {
|
|
|
178
175
|
const groups = new Map();
|
|
179
176
|
for (const e of entries) {
|
|
180
177
|
const key = keyFn(e);
|
|
181
|
-
|
|
182
|
-
|
|
178
|
+
let stats = groups.get(key);
|
|
179
|
+
if (!stats) {
|
|
180
|
+
stats = createEmptyStats();
|
|
181
|
+
groups.set(key, stats);
|
|
183
182
|
}
|
|
184
|
-
const stats = groups.get(key);
|
|
185
183
|
stats.input += e.input ?? 0;
|
|
186
184
|
stats.output += e.output ?? 0;
|
|
187
185
|
stats.reasoning += e.reasoning ?? 0;
|
|
@@ -277,7 +275,6 @@ function cmdStats(period, breakdown) {
|
|
|
277
275
|
since = getStartOfMonth(now);
|
|
278
276
|
title = "This Month's Usage";
|
|
279
277
|
break;
|
|
280
|
-
case "all":
|
|
281
278
|
default:
|
|
282
279
|
since = undefined;
|
|
283
280
|
title = "All-Time Usage";
|
|
@@ -337,15 +334,15 @@ function cmdPricing() {
|
|
|
337
334
|
const p = BUILTIN_PRICING[model];
|
|
338
335
|
if (!p)
|
|
339
336
|
continue;
|
|
340
|
-
const overridden = config.models
|
|
341
|
-
console.log(` ${padRight(model
|
|
337
|
+
const overridden = config.models[model] ? " *" : "";
|
|
338
|
+
console.log(` ${padRight(`${model}${overridden}`, modelWidth)} ${padLeft(`$${p.input.toString()}`, priceWidth)} ${padLeft(`$${p.output.toString()}`, priceWidth)} ${padLeft(p.cacheRead ? `$${p.cacheRead.toString()}` : "-", priceWidth)} ${padLeft(p.cacheWrite ? `$${p.cacheWrite.toString()}` : "-", priceWidth)}`);
|
|
342
339
|
}
|
|
343
340
|
console.log();
|
|
344
341
|
}
|
|
345
342
|
console.log(` Default (unknown models)`);
|
|
346
343
|
console.log(` ${"-".repeat(modelWidth + priceWidth * 4 + 12)}`);
|
|
347
|
-
const def = BUILTIN_PRICING
|
|
348
|
-
console.log(` ${padRight("_default", modelWidth)} ${padLeft(
|
|
344
|
+
const def = BUILTIN_PRICING._default;
|
|
345
|
+
console.log(` ${padRight("_default", modelWidth)} ${padLeft(`$${def.input.toString()}`, priceWidth)} ${padLeft(`$${def.output.toString()}`, priceWidth)} ${padLeft("-", priceWidth)} ${padLeft("-", priceWidth)}`);
|
|
349
346
|
console.log();
|
|
350
347
|
if (Object.keys(config.models || {}).length > 0) {
|
|
351
348
|
console.log(` * = overridden in config`);
|
|
@@ -363,10 +360,11 @@ function cmdModels() {
|
|
|
363
360
|
const model = e.model ?? "unknown";
|
|
364
361
|
const provider = e.provider ?? "unknown";
|
|
365
362
|
const key = `${model}|${provider}`;
|
|
366
|
-
|
|
367
|
-
|
|
363
|
+
let info = modelProviders.get(key);
|
|
364
|
+
if (!info) {
|
|
365
|
+
info = { provider, count: 0, lastUsed: 0 };
|
|
366
|
+
modelProviders.set(key, info);
|
|
368
367
|
}
|
|
369
|
-
const info = modelProviders.get(key);
|
|
370
368
|
info.count++;
|
|
371
369
|
info.lastUsed = Math.max(info.lastUsed, e._ts);
|
|
372
370
|
}
|
|
@@ -569,7 +567,7 @@ function cmdConfig(positional) {
|
|
|
569
567
|
return;
|
|
570
568
|
}
|
|
571
569
|
}
|
|
572
|
-
applyConfigSet(key, value
|
|
570
|
+
applyConfigSet(key, value);
|
|
573
571
|
console.log(`\n Set ${key} = ${JSON.stringify(value)}\n`);
|
|
574
572
|
return;
|
|
575
573
|
}
|
|
@@ -584,7 +582,7 @@ function cmdConfig(positional) {
|
|
|
584
582
|
console.log(`\n Unknown key: ${key}\n Available: ${Object.keys(SETTABLE_KEYS).join(", ")}\n`);
|
|
585
583
|
return;
|
|
586
584
|
}
|
|
587
|
-
applyConfigUnset(key
|
|
585
|
+
applyConfigUnset(key);
|
|
588
586
|
console.log(`\n Unset ${key} (reverted to default)\n`);
|
|
589
587
|
return;
|
|
590
588
|
}
|
|
@@ -604,7 +602,7 @@ function cmdConfig(positional) {
|
|
|
604
602
|
if (existsSync(CONFIG_FILE)) {
|
|
605
603
|
console.log(` Contents:`);
|
|
606
604
|
console.log(` ${"-".repeat(60)}`);
|
|
607
|
-
console.log(JSON.stringify(config, null, 2).split("\n").map(l =>
|
|
605
|
+
console.log(JSON.stringify(config, null, 2).split("\n").map(l => ` ${l}`).join("\n"));
|
|
608
606
|
console.log();
|
|
609
607
|
}
|
|
610
608
|
console.log(` Commands:`);
|
|
@@ -648,7 +646,7 @@ function resolveConfigKey(config, key) {
|
|
|
648
646
|
}
|
|
649
647
|
return obj[spec.path[spec.path.length - 1]] ?? spec.default;
|
|
650
648
|
}
|
|
651
|
-
function applyConfigSet(key, value
|
|
649
|
+
function applyConfigSet(key, value) {
|
|
652
650
|
const spec = SETTABLE_KEYS[key];
|
|
653
651
|
const fullConfig = loadOrInitConfig();
|
|
654
652
|
let obj = fullConfig;
|
|
@@ -660,7 +658,7 @@ function applyConfigSet(key, value, config) {
|
|
|
660
658
|
obj[spec.path[spec.path.length - 1]] = value;
|
|
661
659
|
saveConfig(fullConfig);
|
|
662
660
|
}
|
|
663
|
-
function applyConfigUnset(key
|
|
661
|
+
function applyConfigUnset(key) {
|
|
664
662
|
const spec = SETTABLE_KEYS[key];
|
|
665
663
|
const fullConfig = loadOrInitConfig();
|
|
666
664
|
let obj = fullConfig;
|
|
@@ -687,9 +685,9 @@ function saveConfig(raw) {
|
|
|
687
685
|
mkdirSync(dir, { recursive: true });
|
|
688
686
|
}
|
|
689
687
|
if (existsSync(CONFIG_FILE)) {
|
|
690
|
-
copyFileSync(CONFIG_FILE, CONFIG_FILE
|
|
688
|
+
copyFileSync(CONFIG_FILE, `${CONFIG_FILE}.bak`);
|
|
691
689
|
}
|
|
692
|
-
writeFileSync(CONFIG_FILE, JSON.stringify(raw, null, 2)
|
|
690
|
+
writeFileSync(CONFIG_FILE, `${JSON.stringify(raw, null, 2)}\n`);
|
|
693
691
|
}
|
|
694
692
|
// ============================================================================
|
|
695
693
|
// Export
|
|
@@ -738,7 +736,7 @@ function cmdExport(flags) {
|
|
|
738
736
|
e.cacheWrite ?? 0,
|
|
739
737
|
e.cost ?? 0,
|
|
740
738
|
].map(csvEscape).join(","));
|
|
741
|
-
output = [headers.join(","), ...rows].join("\n")
|
|
739
|
+
output = `${[headers.join(","), ...rows].join("\n")}\n`;
|
|
742
740
|
}
|
|
743
741
|
if (outputFile) {
|
|
744
742
|
writeFileSync(outputFile, output);
|
|
@@ -901,9 +899,74 @@ function cmdHelp() {
|
|
|
901
899
|
opencode-tokens config get toast.enabled # Check if toast is enabled
|
|
902
900
|
`);
|
|
903
901
|
}
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
902
|
+
function getTrendValue(point, metric) {
|
|
903
|
+
return metric === "tokens" ? point.tokens : metric === "messages" ? point.messages : point.cost;
|
|
904
|
+
}
|
|
905
|
+
function formatTrendValue(value, metric) {
|
|
906
|
+
return metric === "tokens" ? formatTokens(value) : metric === "messages" ? String(Math.round(value)) : formatCost(value);
|
|
907
|
+
}
|
|
908
|
+
function metricLabel(metric) {
|
|
909
|
+
return metric === "tokens" ? "Token Trend" : metric === "messages" ? "Message Trend" : "Cost Trend";
|
|
910
|
+
}
|
|
911
|
+
function createTrendRows(height, width, gridRows) {
|
|
912
|
+
const rows = [];
|
|
913
|
+
for (let row = 0; row < height; row++) {
|
|
914
|
+
const grid = gridRows.has(row);
|
|
915
|
+
rows.push(Array.from({ length: width }, (_, x) => grid && x % 2 === 0 ? "·" : " "));
|
|
916
|
+
}
|
|
917
|
+
return rows;
|
|
918
|
+
}
|
|
919
|
+
function drawTrendSegment(rows, from, to) {
|
|
920
|
+
const startX = Math.min(from.x, to.x);
|
|
921
|
+
const endX = Math.max(from.x, to.x);
|
|
922
|
+
const slope = to.y - from.y;
|
|
923
|
+
for (let x = startX; x <= endX; x++) {
|
|
924
|
+
const t = x === from.x ? 0 : (x - from.x) / (to.x - from.x);
|
|
925
|
+
const y = Math.round(from.y + slope * t);
|
|
926
|
+
const char = slope > 0 ? "╱" : slope < 0 ? "╲" : "─";
|
|
927
|
+
rows[y][x] = char;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
function drawTrendArea(rows, points) {
|
|
931
|
+
for (let i = 1; i < points.length; i++) {
|
|
932
|
+
const from = points[i - 1];
|
|
933
|
+
const to = points[i];
|
|
934
|
+
const startX = Math.min(from.x, to.x);
|
|
935
|
+
const endX = Math.max(from.x, to.x);
|
|
936
|
+
for (let x = startX; x <= endX; x++) {
|
|
937
|
+
const t = x === from.x ? 0 : (x - from.x) / (to.x - from.x);
|
|
938
|
+
const y = Math.round(from.y + (to.y - from.y) * t);
|
|
939
|
+
for (let fillY = 0; fillY < y; fillY++) {
|
|
940
|
+
if (rows[fillY][x] === " " || rows[fillY][x] === "·") {
|
|
941
|
+
rows[fillY][x] = "░";
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
function drawTrendPoints(rows, points) {
|
|
948
|
+
for (const point of points) {
|
|
949
|
+
rows[point.y][point.x] = "●";
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
function buildTrendXAxis(points, chartPoints, chartWidth) {
|
|
953
|
+
const labelChars = Array.from({ length: chartWidth }, () => " ");
|
|
954
|
+
const labelStep = Math.max(1, Math.ceil(points.length / 6));
|
|
955
|
+
for (let i = 0; i < chartPoints.length; i++) {
|
|
956
|
+
if (i % labelStep !== 0 && i !== chartPoints.length - 1)
|
|
957
|
+
continue;
|
|
958
|
+
const date = new Date(points[i][0]);
|
|
959
|
+
const label = `${date.getMonth() + 1}/${date.getDate()}`;
|
|
960
|
+
const start = Math.min(Math.max(0, chartPoints[i].x - Math.floor(label.length / 2)), Math.max(0, chartWidth - label.length));
|
|
961
|
+
const hasSpace = labelChars.slice(Math.max(0, start - 1), Math.min(chartWidth, start + label.length + 1)).every((c) => c === " ");
|
|
962
|
+
if (!hasSpace)
|
|
963
|
+
continue;
|
|
964
|
+
for (let j = 0; j < label.length; j++) {
|
|
965
|
+
labelChars[start + j] = label[j];
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
return labelChars.join("");
|
|
969
|
+
}
|
|
907
970
|
function cmdTrend(flags) {
|
|
908
971
|
const days = parseInt(String(flagValue(flags, "days") ?? "30"), 10);
|
|
909
972
|
const metric = flagValue(flags, "metric") ?? "cost";
|
|
@@ -914,14 +977,14 @@ function cmdTrend(flags) {
|
|
|
914
977
|
console.log(`\n (no data in period)\n`);
|
|
915
978
|
return;
|
|
916
979
|
}
|
|
917
|
-
// Aggregate by day
|
|
918
980
|
const dayMap = new Map();
|
|
919
981
|
for (const e of entries) {
|
|
920
982
|
const dayStart = getStartOfDay(new Date(e._ts));
|
|
921
|
-
|
|
922
|
-
|
|
983
|
+
let d = dayMap.get(dayStart);
|
|
984
|
+
if (!d) {
|
|
985
|
+
d = { cost: 0, tokens: 0, messages: 0 };
|
|
986
|
+
dayMap.set(dayStart, d);
|
|
923
987
|
}
|
|
924
|
-
const d = dayMap.get(dayStart);
|
|
925
988
|
d.cost += e.cost ?? 0;
|
|
926
989
|
d.tokens += (e.input ?? 0) + (e.output ?? 0) + (e.reasoning ?? 0);
|
|
927
990
|
d.messages += 1;
|
|
@@ -930,116 +993,60 @@ function cmdTrend(flags) {
|
|
|
930
993
|
if (sorted.length < 2) {
|
|
931
994
|
const only = sorted[0];
|
|
932
995
|
if (only) {
|
|
933
|
-
const v =
|
|
996
|
+
const v = formatTrendValue(getTrendValue(only[1], metric), metric);
|
|
934
997
|
console.log(`\n ${new Date(only[0]).toISOString().slice(0, 10)}: ${v}\n`);
|
|
935
998
|
}
|
|
936
999
|
return;
|
|
937
1000
|
}
|
|
938
|
-
const values = sorted.map(([, d]) =>
|
|
1001
|
+
const values = sorted.map(([, d]) => getTrendValue(d, metric));
|
|
939
1002
|
const maxVal = Math.max(...values, 1);
|
|
1003
|
+
const minVal = Math.min(...values);
|
|
1004
|
+
const totalVal = values.reduce((sum, value) => sum + value, 0);
|
|
1005
|
+
const avgVal = totalVal / values.length;
|
|
1006
|
+
const deltaVal = values[values.length - 1] - values[0];
|
|
940
1007
|
const H = Math.max(5, Math.min(Math.floor(width / 3), 20));
|
|
941
1008
|
if (width < 35) {
|
|
942
1009
|
// Fallback: simple sparkline
|
|
943
1010
|
const chars = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
|
|
944
1011
|
const spark = values.map(v => chars[Math.min(Math.floor((v / maxVal) * 7), 7)]).join("");
|
|
945
|
-
console.log(`\n ${spark}\n`);
|
|
1012
|
+
console.log(`\n ${metricLabel(metric)} ${spark}\n`);
|
|
946
1013
|
return;
|
|
947
1014
|
}
|
|
948
|
-
// Build chart — pixel-based rendering
|
|
949
|
-
const cols = values.map((v) => ({ value: v, y: Math.round((v / maxVal) * (H - 1)) }));
|
|
950
1015
|
const chartWidth = Math.max(width - 12, 20);
|
|
951
|
-
// Map data points to pixel x-positions (evenly spaced across chartWidth)
|
|
952
|
-
const px = [];
|
|
953
|
-
const py = [];
|
|
954
|
-
for (let i = 0; i < cols.length; i++) {
|
|
955
|
-
px.push(cols.length === 1 ? Math.floor(chartWidth / 2) : Math.round((i / (cols.length - 1)) * (chartWidth - 1)));
|
|
956
|
-
py.push(cols[i].y);
|
|
957
|
-
}
|
|
958
1016
|
const yLabelStep = Math.max(1, Math.floor(H / 5));
|
|
1017
|
+
const gridRows = new Set();
|
|
1018
|
+
for (let row = 0; row < H; row++) {
|
|
1019
|
+
if (row === 0 || row === H - 1 || (H - 1 - row) % yLabelStep === 0) {
|
|
1020
|
+
gridRows.add(row);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
const chartPoints = values.map((value, i) => ({
|
|
1024
|
+
x: values.length === 1 ? Math.floor(chartWidth / 2) : Math.round((i / (values.length - 1)) * (chartWidth - 1)),
|
|
1025
|
+
y: Math.round((value / maxVal) * (H - 1)),
|
|
1026
|
+
}));
|
|
1027
|
+
const rows = createTrendRows(H, chartWidth, gridRows);
|
|
1028
|
+
drawTrendArea(rows, chartPoints);
|
|
1029
|
+
for (let i = 1; i < chartPoints.length; i++) {
|
|
1030
|
+
drawTrendSegment(rows, chartPoints[i - 1], chartPoints[i]);
|
|
1031
|
+
}
|
|
1032
|
+
drawTrendPoints(rows, chartPoints);
|
|
959
1033
|
const lines = [];
|
|
1034
|
+
lines.push(`${metricLabel(metric)} · ${sorted.length} days · peak ${formatTrendValue(maxVal, metric)} · avg ${formatTrendValue(avgVal, metric)} · Δ ${deltaVal >= 0 ? "+" : ""}${formatTrendValue(deltaVal, metric)}`);
|
|
1035
|
+
lines.push(`range ${formatTrendValue(minVal, metric)} → ${formatTrendValue(maxVal, metric)}`);
|
|
960
1036
|
for (let row = H - 1; row >= 0; row--) {
|
|
961
|
-
let line = "";
|
|
962
1037
|
const valAtRow = (row / (H - 1)) * maxVal;
|
|
963
|
-
const label =
|
|
964
|
-
?
|
|
1038
|
+
const label = gridRows.has(row)
|
|
1039
|
+
? formatTrendValue(valAtRow, metric)
|
|
965
1040
|
: "";
|
|
966
|
-
line
|
|
967
|
-
line += row === 0 ? " ┼" : " ┤";
|
|
968
|
-
// Build a sparse array of characters at specific x-positions for this row
|
|
969
|
-
const chars = [];
|
|
970
|
-
// Data points that land on this row
|
|
971
|
-
for (let i = 0; i < px.length; i++) {
|
|
972
|
-
if (py[i] === row) {
|
|
973
|
-
if (cols.length === 1) {
|
|
974
|
-
chars.push({ x: px[i], c: "─" });
|
|
975
|
-
}
|
|
976
|
-
else {
|
|
977
|
-
const prevSlope = i > 0 ? py[i] - py[i - 1] : 0;
|
|
978
|
-
const nextSlope = i < px.length - 1 ? py[i + 1] - py[i] : 0;
|
|
979
|
-
let c = "─";
|
|
980
|
-
if (i === 0)
|
|
981
|
-
c = nextSlope > 0 ? "╭" : nextSlope < 0 ? "╰" : "─";
|
|
982
|
-
else if (i === px.length - 1)
|
|
983
|
-
c = prevSlope > 0 ? "╮" : prevSlope < 0 ? "╯" : "─";
|
|
984
|
-
else if (prevSlope > 0 && nextSlope > 0)
|
|
985
|
-
c = "╭";
|
|
986
|
-
else if (prevSlope < 0 && nextSlope < 0)
|
|
987
|
-
c = "╰";
|
|
988
|
-
else if (prevSlope > 0 && nextSlope < 0)
|
|
989
|
-
c = "╮";
|
|
990
|
-
else if (prevSlope < 0 && nextSlope > 0)
|
|
991
|
-
c = "╯";
|
|
992
|
-
chars.push({ x: px[i], c });
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
// Line segments crossing this row (exact x of intersection)
|
|
997
|
-
for (let i = 1; i < px.length; i++) {
|
|
998
|
-
const y0 = py[i - 1], y1 = py[i];
|
|
999
|
-
// Skip if segment doesn't cross this row
|
|
1000
|
-
if ((y0 <= row && y1 <= row) || (y0 >= row && y1 >= row))
|
|
1001
|
-
continue;
|
|
1002
|
-
if (y0 === y1)
|
|
1003
|
-
continue;
|
|
1004
|
-
const t = (row - y0) / (y1 - y0);
|
|
1005
|
-
const cx = Math.round(px[i - 1] + t * (px[i] - px[i - 1]));
|
|
1006
|
-
const slope = y1 - y0;
|
|
1007
|
-
chars.push({ x: cx, c: slope > 0 ? "╱" : "╲" });
|
|
1008
|
-
}
|
|
1009
|
-
// Render the row: sort chars by x and fill gaps with spaces
|
|
1010
|
-
chars.sort((a, b) => a.x - b.x);
|
|
1011
|
-
let prevX = 0;
|
|
1012
|
-
for (const { x, c } of chars) {
|
|
1013
|
-
while (prevX < x) {
|
|
1014
|
-
line += " ";
|
|
1015
|
-
prevX++;
|
|
1016
|
-
}
|
|
1017
|
-
line += c;
|
|
1018
|
-
prevX = x + 1;
|
|
1019
|
-
}
|
|
1041
|
+
const line = `${padLeft(label, 9)}${row === 0 ? " ┼" : " ┤"}${rows[row].join("")}`;
|
|
1020
1042
|
lines.push(line);
|
|
1021
1043
|
}
|
|
1022
|
-
|
|
1023
|
-
let axis = " ".repeat(9) + " └";
|
|
1024
|
-
axis += "─".repeat(chartWidth);
|
|
1044
|
+
const axis = `${" ".repeat(9)} └${"─".repeat(chartWidth)}`;
|
|
1025
1045
|
lines.push(axis);
|
|
1026
|
-
|
|
1027
|
-
const labelStep = Math.max(1, Math.ceil(sorted.length / 6));
|
|
1028
|
-
let xLabels = " ".repeat(11);
|
|
1029
|
-
for (let i = 0; i < px.length; i++) {
|
|
1030
|
-
if (i % labelStep === 0 || i === px.length - 1) {
|
|
1031
|
-
const d = new Date(sorted[i][0]);
|
|
1032
|
-
const ds = `${d.getMonth() + 1}/${d.getDate()}`;
|
|
1033
|
-
const pos = px[i] + 0;
|
|
1034
|
-
while (xLabels.length - 11 < pos)
|
|
1035
|
-
xLabels += " ";
|
|
1036
|
-
xLabels += ds;
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
lines.push(xLabels);
|
|
1046
|
+
lines.push(`${" ".repeat(11)}${buildTrendXAxis(sorted, chartPoints, chartWidth)}`);
|
|
1040
1047
|
console.log();
|
|
1041
1048
|
for (const l of lines)
|
|
1042
|
-
console.log(
|
|
1049
|
+
console.log(` ${l}`);
|
|
1043
1050
|
console.log();
|
|
1044
1051
|
}
|
|
1045
1052
|
// ============================================================================
|
|
@@ -1075,8 +1082,7 @@ function main() {
|
|
|
1075
1082
|
}
|
|
1076
1083
|
// Default: stats
|
|
1077
1084
|
let period = "all";
|
|
1078
|
-
|
|
1079
|
-
breakdown = flagValue(parsed.flags, "by") || (parsed.flags.has("b") ? String(parsed.flags.get("b")) : undefined);
|
|
1085
|
+
const breakdown = flagValue(parsed.flags, "by") || (parsed.flags.has("b") ? String(parsed.flags.get("b")) : undefined);
|
|
1080
1086
|
for (const p of ["today", "week", "month", "all"]) {
|
|
1081
1087
|
if (parsed.positional.includes(p)) {
|
|
1082
1088
|
period = p;
|
package/package.json
CHANGED