opencode-quotas 0.0.1
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 +344 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/src/cli.d.ts +3 -0
- package/dist/src/cli.d.ts.map +1 -0
- package/dist/src/cli.js +42 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/constants.d.ts +9 -0
- package/dist/src/constants.d.ts.map +1 -0
- package/dist/src/constants.js +15 -0
- package/dist/src/constants.js.map +1 -0
- package/dist/src/defaults.d.ts +3 -0
- package/dist/src/defaults.d.ts.map +1 -0
- package/dist/src/defaults.js +52 -0
- package/dist/src/defaults.js.map +1 -0
- package/dist/src/index.d.ts +6 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +265 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/interfaces.d.ts +197 -0
- package/dist/src/interfaces.d.ts.map +1 -0
- package/dist/src/interfaces.js +2 -0
- package/dist/src/interfaces.js.map +1 -0
- package/dist/src/logger.d.ts +14 -0
- package/dist/src/logger.d.ts.map +1 -0
- package/dist/src/logger.js +44 -0
- package/dist/src/logger.js.map +1 -0
- package/dist/src/plugin-state.d.ts +20 -0
- package/dist/src/plugin-state.d.ts.map +1 -0
- package/dist/src/plugin-state.js +53 -0
- package/dist/src/plugin-state.js.map +1 -0
- package/dist/src/providers/antigravity/auth.d.ts +12 -0
- package/dist/src/providers/antigravity/auth.d.ts.map +1 -0
- package/dist/src/providers/antigravity/auth.js +109 -0
- package/dist/src/providers/antigravity/auth.js.map +1 -0
- package/dist/src/providers/antigravity/index.d.ts +2 -0
- package/dist/src/providers/antigravity/index.d.ts.map +1 -0
- package/dist/src/providers/antigravity/index.js +2 -0
- package/dist/src/providers/antigravity/index.js.map +1 -0
- package/dist/src/providers/antigravity/provider.d.ts +34 -0
- package/dist/src/providers/antigravity/provider.d.ts.map +1 -0
- package/dist/src/providers/antigravity/provider.js +216 -0
- package/dist/src/providers/antigravity/provider.js.map +1 -0
- package/dist/src/providers/codex.d.ts +4 -0
- package/dist/src/providers/codex.d.ts.map +1 -0
- package/dist/src/providers/codex.js +242 -0
- package/dist/src/providers/codex.js.map +1 -0
- package/dist/src/providers/github.d.ts +4 -0
- package/dist/src/providers/github.d.ts.map +1 -0
- package/dist/src/providers/github.js +139 -0
- package/dist/src/providers/github.js.map +1 -0
- package/dist/src/quota-cache.d.ts +26 -0
- package/dist/src/quota-cache.d.ts.map +1 -0
- package/dist/src/quota-cache.js +107 -0
- package/dist/src/quota-cache.js.map +1 -0
- package/dist/src/registry.d.ts +3 -0
- package/dist/src/registry.d.ts.map +1 -0
- package/dist/src/registry.js +23 -0
- package/dist/src/registry.js.map +1 -0
- package/dist/src/services/aggregation-service.d.ts +34 -0
- package/dist/src/services/aggregation-service.d.ts.map +1 -0
- package/dist/src/services/aggregation-service.js +89 -0
- package/dist/src/services/aggregation-service.js.map +1 -0
- package/dist/src/services/config-loader.d.ts +25 -0
- package/dist/src/services/config-loader.d.ts.map +1 -0
- package/dist/src/services/config-loader.js +105 -0
- package/dist/src/services/config-loader.js.map +1 -0
- package/dist/src/services/history-service.d.ts +15 -0
- package/dist/src/services/history-service.d.ts.map +1 -0
- package/dist/src/services/history-service.js +99 -0
- package/dist/src/services/history-service.js.map +1 -0
- package/dist/src/services/prediction-engine.d.ts +47 -0
- package/dist/src/services/prediction-engine.d.ts.map +1 -0
- package/dist/src/services/prediction-engine.js +94 -0
- package/dist/src/services/prediction-engine.js.map +1 -0
- package/dist/src/services/quota-service.d.ts +41 -0
- package/dist/src/services/quota-service.d.ts.map +1 -0
- package/dist/src/services/quota-service.js +257 -0
- package/dist/src/services/quota-service.js.map +1 -0
- package/dist/src/tools/quotas.d.ts +11 -0
- package/dist/src/tools/quotas.d.ts.map +1 -0
- package/dist/src/tools/quotas.js +62 -0
- package/dist/src/tools/quotas.js.map +1 -0
- package/dist/src/ui/progress-bar.d.ts +20 -0
- package/dist/src/ui/progress-bar.d.ts.map +1 -0
- package/dist/src/ui/progress-bar.js +150 -0
- package/dist/src/ui/progress-bar.js.map +1 -0
- package/dist/src/ui/quota-table.d.ts +15 -0
- package/dist/src/ui/quota-table.d.ts.map +1 -0
- package/dist/src/ui/quota-table.js +136 -0
- package/dist/src/ui/quota-table.js.map +1 -0
- package/dist/src/utils/debug.d.ts +1 -0
- package/dist/src/utils/debug.d.ts.map +1 -0
- package/dist/src/utils/debug.js +3 -0
- package/dist/src/utils/debug.js.map +1 -0
- package/dist/src/utils/paths.d.ts +7 -0
- package/dist/src/utils/paths.d.ts.map +1 -0
- package/dist/src/utils/paths.js +37 -0
- package/dist/src/utils/paths.js.map +1 -0
- package/dist/src/utils/time.d.ts +3 -0
- package/dist/src/utils/time.d.ts.map +1 -0
- package/dist/src/utils/time.js +38 -0
- package/dist/src/utils/time.js.map +1 -0
- package/dist/src/utils/validation.d.ts +6 -0
- package/dist/src/utils/validation.d.ts.map +1 -0
- package/dist/src/utils/validation.js +66 -0
- package/dist/src/utils/validation.js.map +1 -0
- package/package.json +42 -0
- package/src/cli.ts +53 -0
- package/src/constants.ts +17 -0
- package/src/defaults.ts +53 -0
- package/src/index.ts +338 -0
- package/src/interfaces.ts +258 -0
- package/src/logger.ts +55 -0
- package/src/plugin-state.ts +65 -0
- package/src/providers/antigravity/auth.ts +163 -0
- package/src/providers/antigravity/index.ts +1 -0
- package/src/providers/antigravity/provider.ts +337 -0
- package/src/providers/codex.ts +327 -0
- package/src/providers/github.ts +161 -0
- package/src/quota-cache.ts +157 -0
- package/src/registry.ts +30 -0
- package/src/services/aggregation-service.ts +116 -0
- package/src/services/config-loader.ts +124 -0
- package/src/services/history-service.ts +116 -0
- package/src/services/prediction-engine.ts +133 -0
- package/src/services/quota-service.ts +343 -0
- package/src/tools/quotas.ts +78 -0
- package/src/ui/progress-bar.ts +204 -0
- package/src/ui/quota-table.ts +173 -0
- package/src/utils/debug.ts +1 -0
- package/src/utils/paths.ts +41 -0
- package/src/utils/time.ts +40 -0
- package/src/utils/validation.ts +63 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { type AnsiColor, type ProgressBarConfig, type GradientLevel } from "../interfaces";
|
|
2
|
+
import { isValidNumber, clamp } from "../utils/validation";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_BAR_WIDTH = 20;
|
|
5
|
+
const DEFAULT_FILLED_CHAR = "█";
|
|
6
|
+
const DEFAULT_EMPTY_CHAR = "░";
|
|
7
|
+
|
|
8
|
+
const DEFAULT_GRADIENTS: GradientLevel[] = [
|
|
9
|
+
{ threshold: 0.5, color: "green" },
|
|
10
|
+
{ threshold: 0.8, color: "yellow" },
|
|
11
|
+
{ threshold: 1.0, color: "red" },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const ANSI_CODES: Record<AnsiColor, string> = {
|
|
15
|
+
red: "\x1b[31m",
|
|
16
|
+
green: "\x1b[32m",
|
|
17
|
+
yellow: "\x1b[33m",
|
|
18
|
+
blue: "\x1b[34m",
|
|
19
|
+
magenta: "\x1b[35m",
|
|
20
|
+
cyan: "\x1b[36m",
|
|
21
|
+
white: "\x1b[37m",
|
|
22
|
+
gray: "\x1b[90m",
|
|
23
|
+
bold: "\x1b[1m",
|
|
24
|
+
dim: "\x1b[2m",
|
|
25
|
+
reset: "\x1b[0m",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function shouldUseColor(config?: ProgressBarConfig): boolean {
|
|
29
|
+
// FORCE_COLOR should take precedence when explicitly set
|
|
30
|
+
if (process.env.FORCE_COLOR !== undefined) return true;
|
|
31
|
+
|
|
32
|
+
// Respect explicit no-color flags
|
|
33
|
+
if (process.env.NO_COLOR !== undefined) return false;
|
|
34
|
+
if (process.env.OPENCODE_QUOTAS_NO_COLOR !== undefined) return false;
|
|
35
|
+
|
|
36
|
+
// Respect explicit config request (useful for tests/environments)
|
|
37
|
+
if (config?.color === true) return true;
|
|
38
|
+
|
|
39
|
+
// If not a TTY, generally disable color
|
|
40
|
+
if (!process.stdout.isTTY) return false;
|
|
41
|
+
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function colorize(text: string, color: AnsiColor | undefined, useColor: boolean): string {
|
|
46
|
+
if (!useColor) return text;
|
|
47
|
+
if (!color || color === "reset") return text;
|
|
48
|
+
return `${ANSI_CODES[color]}${text}${ANSI_CODES.reset}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function formatNumber(value: number): string {
|
|
52
|
+
if (!Number.isFinite(value)) return "0";
|
|
53
|
+
if (Number.isInteger(value)) {
|
|
54
|
+
return `${value}`;
|
|
55
|
+
}
|
|
56
|
+
if (Math.abs(value) >= 100) {
|
|
57
|
+
return value.toFixed(1);
|
|
58
|
+
}
|
|
59
|
+
return value.toFixed(1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type RenderQuotaBarParts = {
|
|
63
|
+
labelPart: string;
|
|
64
|
+
bar: string;
|
|
65
|
+
percent: string;
|
|
66
|
+
valuePart: string;
|
|
67
|
+
detailsPart: string;
|
|
68
|
+
statusEmoji: string;
|
|
69
|
+
statusText: string;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export function getQuotaStatusEmoji(
|
|
73
|
+
ratio: number,
|
|
74
|
+
config: ProgressBarConfig = {}
|
|
75
|
+
): string {
|
|
76
|
+
// Default thresholds if not provided
|
|
77
|
+
const gradients = config.gradients || DEFAULT_GRADIENTS;
|
|
78
|
+
|
|
79
|
+
const sorted = [...gradients].sort((a, b) => a.threshold - b.threshold);
|
|
80
|
+
|
|
81
|
+
// Find the matching level
|
|
82
|
+
const match = sorted.find((g) => ratio <= g.threshold);
|
|
83
|
+
const color = match ? match.color : sorted[sorted.length - 1]?.color || "red";
|
|
84
|
+
|
|
85
|
+
switch (color) {
|
|
86
|
+
case "green": return "🟢";
|
|
87
|
+
case "yellow": return "🟡";
|
|
88
|
+
case "red": return "🔴";
|
|
89
|
+
default: return "⚪"; // Grey/Unknown
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function getQuotaStatusText(
|
|
94
|
+
ratio: number,
|
|
95
|
+
config: ProgressBarConfig = {}
|
|
96
|
+
): string {
|
|
97
|
+
// Default thresholds if not provided
|
|
98
|
+
const gradients = config.gradients || DEFAULT_GRADIENTS;
|
|
99
|
+
|
|
100
|
+
const sorted = [...gradients].sort((a, b) => a.threshold - b.threshold);
|
|
101
|
+
|
|
102
|
+
// Find the matching level
|
|
103
|
+
const match = sorted.find((g) => ratio <= g.threshold);
|
|
104
|
+
const color = match ? match.color : sorted[sorted.length - 1]?.color || "red";
|
|
105
|
+
|
|
106
|
+
switch (color) {
|
|
107
|
+
case "green": return "OK "; // Space for alignment
|
|
108
|
+
case "yellow": return "WRN";
|
|
109
|
+
case "red": return "ERR";
|
|
110
|
+
default: return "UNK";
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function renderQuotaBarParts(
|
|
115
|
+
used: number,
|
|
116
|
+
limit: number,
|
|
117
|
+
options: {
|
|
118
|
+
label: string;
|
|
119
|
+
unit: string;
|
|
120
|
+
details?: string;
|
|
121
|
+
config?: ProgressBarConfig;
|
|
122
|
+
},
|
|
123
|
+
): RenderQuotaBarParts {
|
|
124
|
+
const config = options.config || {};
|
|
125
|
+
const width = config.width ?? DEFAULT_BAR_WIDTH;
|
|
126
|
+
const filledChar = config.filledChar ?? DEFAULT_FILLED_CHAR;
|
|
127
|
+
const emptyChar = config.emptyChar ?? DEFAULT_EMPTY_CHAR;
|
|
128
|
+
const showMode = config.show ?? "used";
|
|
129
|
+
const useColor = shouldUseColor(config);
|
|
130
|
+
|
|
131
|
+
// Defensive guards: normalize inputs
|
|
132
|
+
const usedVal = isValidNumber(used) ? Math.max(0, used) : 0;
|
|
133
|
+
const limitVal = isValidNumber(limit) && limit > 0 ? limit : 0;
|
|
134
|
+
|
|
135
|
+
// Calculate value and ratio based on mode
|
|
136
|
+
let displayValue = usedVal;
|
|
137
|
+
let ratio = 0;
|
|
138
|
+
|
|
139
|
+
if (limitVal > 0) {
|
|
140
|
+
if (showMode === "available") {
|
|
141
|
+
displayValue = Math.max(0, limitVal - usedVal);
|
|
142
|
+
}
|
|
143
|
+
ratio = displayValue / limitVal;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Cap visual ratio at 1.0 for the bar filling, but keep actual ratio for color calculation if needed
|
|
147
|
+
const visualRatio = Math.min(Math.max(ratio, 0), 1);
|
|
148
|
+
|
|
149
|
+
const filledLen = Math.round(width * visualRatio);
|
|
150
|
+
const emptyLen = Math.max(0, width - filledLen);
|
|
151
|
+
|
|
152
|
+
// Determine Color
|
|
153
|
+
let barColor: AnsiColor = "reset";
|
|
154
|
+
let statusColor: AnsiColor = "reset";
|
|
155
|
+
|
|
156
|
+
if (config.gradients && config.gradients.length > 0) {
|
|
157
|
+
// Sort gradients by threshold to ensure correct evaluation
|
|
158
|
+
const sorted = [...config.gradients].sort(
|
|
159
|
+
(a, b) => a.threshold - b.threshold,
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
// Find the matching level
|
|
163
|
+
const match = sorted.find((g) => ratio <= g.threshold);
|
|
164
|
+
|
|
165
|
+
if (match) {
|
|
166
|
+
barColor = match.color;
|
|
167
|
+
statusColor = match.color;
|
|
168
|
+
} else {
|
|
169
|
+
// If ratio exceeds all thresholds (e.g. > 100%), use the last defined color (highest severity)
|
|
170
|
+
barColor = sorted[sorted.length - 1].color;
|
|
171
|
+
statusColor = sorted[sorted.length - 1].color;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const filledStr = filledChar.repeat(filledLen);
|
|
176
|
+
const emptyStr = emptyChar.repeat(emptyLen);
|
|
177
|
+
|
|
178
|
+
const bar = `${colorize(filledStr, barColor, useColor)}${emptyStr}`; // Only color filled part? Or empty too? usually just filled.
|
|
179
|
+
|
|
180
|
+
const percentRaw = limitVal > 0 ? `${Math.round(ratio * 100)}%` : "n/a";
|
|
181
|
+
const percentText = percentRaw === "n/a" ? percentRaw : percentRaw.padStart(4);
|
|
182
|
+
const percent = colorize(percentText, barColor, useColor);
|
|
183
|
+
|
|
184
|
+
const valueText = `${formatNumber(displayValue)}/${formatNumber(limitVal)} ${options.unit}`;
|
|
185
|
+
|
|
186
|
+
// Only append colon if label is present and not empty
|
|
187
|
+
const labelPart = options.label ? `${options.label}: ` : "";
|
|
188
|
+
|
|
189
|
+
// Determine Emoji
|
|
190
|
+
const statusEmoji = getQuotaStatusEmoji(ratio, config);
|
|
191
|
+
const statusTextRaw = getQuotaStatusText(ratio, config);
|
|
192
|
+
const statusText = colorize(statusTextRaw, statusColor, useColor);
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
labelPart,
|
|
196
|
+
bar,
|
|
197
|
+
percent,
|
|
198
|
+
valuePart: `(${valueText})`,
|
|
199
|
+
detailsPart: options.details ? ` | ${options.details}` : "",
|
|
200
|
+
statusEmoji,
|
|
201
|
+
statusText,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { renderQuotaBarParts, type RenderQuotaBarParts, colorize } from "./progress-bar";
|
|
2
|
+
import { type ProgressBarConfig, type QuotaData, type QuotaColumn } from "../interfaces";
|
|
3
|
+
import { validateQuotaData } from "../utils/validation";
|
|
4
|
+
|
|
5
|
+
type RenderedQuotaLine = {
|
|
6
|
+
id: string;
|
|
7
|
+
providerName: string;
|
|
8
|
+
line: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const DEFAULT_COLUMNS: QuotaColumn[] = ["status", "name", "percent", "bar", "reset", "ettl"];
|
|
12
|
+
const HEADERS: Record<QuotaColumn, string> = {
|
|
13
|
+
name: "QUOTA NAME",
|
|
14
|
+
bar: "UTILIZATION",
|
|
15
|
+
percent: "USED",
|
|
16
|
+
value: "VALUE",
|
|
17
|
+
reset: "RESET",
|
|
18
|
+
ettl: "ETTL",
|
|
19
|
+
window: "WINDOW",
|
|
20
|
+
info: "INFO",
|
|
21
|
+
status: "ST"
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function renderQuotaTable(
|
|
25
|
+
quotas: QuotaData[],
|
|
26
|
+
options: {
|
|
27
|
+
progressBarConfig?: ProgressBarConfig;
|
|
28
|
+
tableConfig?: { columns?: QuotaColumn[], header?: boolean };
|
|
29
|
+
},
|
|
30
|
+
): RenderedQuotaLine[] {
|
|
31
|
+
if (quotas.length === 0) return [];
|
|
32
|
+
|
|
33
|
+
const columns = options.tableConfig?.columns || DEFAULT_COLUMNS;
|
|
34
|
+
const useColor = options.progressBarConfig?.color ?? false;
|
|
35
|
+
|
|
36
|
+
// 1. Pre-calculate cell data for every row
|
|
37
|
+
const rows = quotas.map((quota) => {
|
|
38
|
+
const validated = validateQuotaData(quota) || quota;
|
|
39
|
+
const isUnlimited = validated.limit === null || validated.limit <= 0;
|
|
40
|
+
|
|
41
|
+
// Render bar parts if limited
|
|
42
|
+
let barParts: RenderQuotaBarParts | null = null;
|
|
43
|
+
if (!isUnlimited) {
|
|
44
|
+
barParts = renderQuotaBarParts(validated.used, validated.limit!, {
|
|
45
|
+
label: "",
|
|
46
|
+
unit: validated.unit,
|
|
47
|
+
config: options.progressBarConfig,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const name = colorize(validated.providerName, "cyan", useColor);
|
|
52
|
+
const status = barParts
|
|
53
|
+
? barParts.statusText
|
|
54
|
+
: (validated.info === "unlimited" ? colorize("OK ", "green", useColor) : colorize("UNK", "gray", useColor));
|
|
55
|
+
|
|
56
|
+
// Strip "resets in " or "resets at " prefix for cleaner table display
|
|
57
|
+
const resetRaw = validated.reset?.replace(/^resets (in|at) /, "") || "";
|
|
58
|
+
const reset = colorize(resetRaw, "gray", useColor);
|
|
59
|
+
|
|
60
|
+
// Remove leading 'in ' if present and strip '(predicted)'
|
|
61
|
+
const ettlRaw = validated.predictedReset?.replace(/^in\s+/i, "").replace(/\(predicted\)/, "").trim() || "-";
|
|
62
|
+
const ettl = colorize(ettlRaw, "gray", useColor);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
quota: validated,
|
|
66
|
+
barParts,
|
|
67
|
+
cells: {
|
|
68
|
+
name,
|
|
69
|
+
bar: barParts ? barParts.bar : (isUnlimited ? colorize("Unlimited", "green", useColor) : ""),
|
|
70
|
+
percent: barParts ? barParts.percent : "",
|
|
71
|
+
value: barParts ? barParts.valuePart : `${validated.used} ${validated.unit}`,
|
|
72
|
+
reset,
|
|
73
|
+
ettl,
|
|
74
|
+
window: validated.window || "",
|
|
75
|
+
info: validated.info || "",
|
|
76
|
+
status,
|
|
77
|
+
} as Record<QuotaColumn, string>
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// 2. Measure widths
|
|
82
|
+
const widths: Record<QuotaColumn, number> = {
|
|
83
|
+
name: 0, bar: 0, percent: 0, value: 0, reset: 0, window: 0, info: 0, status: 0, ettl: 0
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Calculate max widths including headers
|
|
87
|
+
for (const col of columns) {
|
|
88
|
+
widths[col] = Math.max(widths[col], HEADERS[col].length);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (const row of rows) {
|
|
92
|
+
for (const col of columns) {
|
|
93
|
+
const content = row.cells[col];
|
|
94
|
+
const visibleLength = content.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
95
|
+
if (visibleLength > widths[col]) {
|
|
96
|
+
widths[col] = visibleLength;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const outputRows: RenderedQuotaLine[] = [];
|
|
102
|
+
|
|
103
|
+
// 3. Render Header (optional)
|
|
104
|
+
if (options.tableConfig?.header !== false) {
|
|
105
|
+
const headerSegments: string[] = [];
|
|
106
|
+
const separatorSegments: string[] = [];
|
|
107
|
+
|
|
108
|
+
for (const col of columns) {
|
|
109
|
+
const width = widths[col];
|
|
110
|
+
const headerTitle = HEADERS[col];
|
|
111
|
+
// Alignments: same as content
|
|
112
|
+
// percent: right
|
|
113
|
+
// others: left
|
|
114
|
+
|
|
115
|
+
const coloredHeader = colorize(headerTitle, "bold", useColor);
|
|
116
|
+
let segment = "";
|
|
117
|
+
let sep = "";
|
|
118
|
+
|
|
119
|
+
// When padding, we need to consider visible length of the colored string vs stripped
|
|
120
|
+
// But here `headerTitle` is uncolored when calculating padding, so standard pad works
|
|
121
|
+
// provided we apply color AFTER padding, OR pad properly.
|
|
122
|
+
// Actually, padStart/padEnd on the uncolored string, THEN colorize.
|
|
123
|
+
|
|
124
|
+
let paddedTitle = "";
|
|
125
|
+
if (col === "percent") {
|
|
126
|
+
paddedTitle = headerTitle.padStart(width);
|
|
127
|
+
} else {
|
|
128
|
+
paddedTitle = headerTitle.padEnd(width);
|
|
129
|
+
}
|
|
130
|
+
segment = colorize(paddedTitle, "bold", useColor);
|
|
131
|
+
|
|
132
|
+
// Create separator line using hyphens
|
|
133
|
+
sep = colorize("-".repeat(width), "dim", useColor);
|
|
134
|
+
|
|
135
|
+
headerSegments.push(segment);
|
|
136
|
+
separatorSegments.push(sep);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
outputRows.push({ id: "header", providerName: "header", line: headerSegments.join(" ") });
|
|
140
|
+
outputRows.push({ id: "sep", providerName: "sep", line: separatorSegments.join(" ") });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 4. Render lines
|
|
144
|
+
rows.forEach((row) => {
|
|
145
|
+
const segments: string[] = [];
|
|
146
|
+
|
|
147
|
+
for (let i = 0; i < columns.length; i++) {
|
|
148
|
+
const col = columns[i];
|
|
149
|
+
const content = row.cells[col];
|
|
150
|
+
const width = widths[col];
|
|
151
|
+
|
|
152
|
+
let segment = "";
|
|
153
|
+
const visibleLength = content.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
154
|
+
const padding = Math.max(0, width - visibleLength);
|
|
155
|
+
|
|
156
|
+
if (col === "percent") {
|
|
157
|
+
segment = " ".repeat(padding) + content;
|
|
158
|
+
} else {
|
|
159
|
+
segment = content + " ".repeat(padding);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
segments.push(segment);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
outputRows.push({
|
|
166
|
+
id: row.quota.id,
|
|
167
|
+
providerName: row.quota.providerName,
|
|
168
|
+
line: segments.join(" "), // Use 3 spaces separator as in example
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
return outputRows;
|
|
173
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// Legacy helper removed. Use logger.debug directly.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { homedir, platform } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
export function getDataDirectory(): string {
|
|
5
|
+
const home = homedir();
|
|
6
|
+
|
|
7
|
+
switch (platform()) {
|
|
8
|
+
case "win32":
|
|
9
|
+
return process.env.APPDATA
|
|
10
|
+
? join(process.env.APPDATA, "opencode")
|
|
11
|
+
: join(home, "AppData", "Roaming", "opencode");
|
|
12
|
+
case "darwin":
|
|
13
|
+
return join(home, "Library", "Application Support", "opencode");
|
|
14
|
+
default:
|
|
15
|
+
return process.env.XDG_DATA_HOME
|
|
16
|
+
? join(process.env.XDG_DATA_HOME, "opencode")
|
|
17
|
+
: join(home, ".local", "share", "opencode");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getConfigDirectory(): string {
|
|
22
|
+
const home = homedir();
|
|
23
|
+
|
|
24
|
+
switch (platform()) {
|
|
25
|
+
case "win32":
|
|
26
|
+
return process.env.APPDATA
|
|
27
|
+
? join(process.env.APPDATA, "opencode")
|
|
28
|
+
: join(home, "AppData", "Roaming", "opencode");
|
|
29
|
+
case "darwin":
|
|
30
|
+
return join(home, "Library", "Application Support", "opencode");
|
|
31
|
+
default:
|
|
32
|
+
return process.env.XDG_CONFIG_HOME
|
|
33
|
+
? join(process.env.XDG_CONFIG_HOME, "opencode")
|
|
34
|
+
: join(home, ".config", "opencode");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const AUTH_FILE = (): string => join(getDataDirectory(), "auth.json");
|
|
39
|
+
export const HISTORY_FILE = (): string => join(getDataDirectory(), "quota-history.json");
|
|
40
|
+
export const DEBUG_LOG_FILE = (): string => join(getDataDirectory(), "quotas-debug.log");
|
|
41
|
+
export const ANTIGRAVITY_ACCOUNTS_FILE = (): string => join(getConfigDirectory(), "antigravity-accounts.json");
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export function formatRelativeTime(targetDate: Date): string {
|
|
2
|
+
const now = new Date();
|
|
3
|
+
const diffMs = targetDate.getTime() - now.getTime();
|
|
4
|
+
if (diffMs <= 0) return "now";
|
|
5
|
+
|
|
6
|
+
const diffMins = Math.floor(diffMs / (1000 * 60));
|
|
7
|
+
const diffHours = Math.floor(diffMins / 60);
|
|
8
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
9
|
+
const remainingHours = diffHours % 24;
|
|
10
|
+
const remainingMins = diffMins % 60;
|
|
11
|
+
|
|
12
|
+
if (diffDays > 0) {
|
|
13
|
+
return `${diffDays}d ${remainingHours}h`;
|
|
14
|
+
}
|
|
15
|
+
if (diffHours > 0) {
|
|
16
|
+
return `${diffHours}h ${remainingMins}m`;
|
|
17
|
+
}
|
|
18
|
+
return `${diffMins}m`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function formatDurationMs(ms: number): string {
|
|
22
|
+
if (ms <= 0) return "now";
|
|
23
|
+
|
|
24
|
+
const diffMins = Math.floor(ms / (1000 * 60));
|
|
25
|
+
const diffHours = Math.floor(diffMins / 60);
|
|
26
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
27
|
+
const remainingHours = diffHours % 24;
|
|
28
|
+
const remainingMins = diffMins % 60;
|
|
29
|
+
|
|
30
|
+
if (diffDays > 0) {
|
|
31
|
+
return `${diffDays}d ${remainingHours}h`;
|
|
32
|
+
}
|
|
33
|
+
if (diffHours > 0) {
|
|
34
|
+
return `${diffHours}h ${remainingMins}m`;
|
|
35
|
+
}
|
|
36
|
+
if (diffMins > 0) {
|
|
37
|
+
return `${diffMins}m`;
|
|
38
|
+
}
|
|
39
|
+
return "less than 1m";
|
|
40
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { type QuotaData } from "../interfaces";
|
|
2
|
+
|
|
3
|
+
export function isValidNumber(v: unknown): v is number {
|
|
4
|
+
return typeof v === "number" && Number.isFinite(v);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function clamp(value: number, min: number, max?: number): number {
|
|
8
|
+
if (!Number.isFinite(value)) return min;
|
|
9
|
+
if (value < min) return min;
|
|
10
|
+
if (max !== undefined && value > max) return max;
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function validatePollingInterval(v: unknown): number | null {
|
|
15
|
+
if (v === null || v === undefined) return null;
|
|
16
|
+
const n = typeof v === "string" ? Number(v.trim()) : Number(v);
|
|
17
|
+
if (!Number.isFinite(n) || n <= 0) return null;
|
|
18
|
+
return n;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function validateQuotaData(input: unknown): QuotaData | null {
|
|
22
|
+
if (typeof input !== "object" || input === null) return null;
|
|
23
|
+
const q = input as Partial<QuotaData>;
|
|
24
|
+
|
|
25
|
+
if (!q.id || typeof q.id !== "string") return null;
|
|
26
|
+
if (!q.providerName || typeof q.providerName !== "string") return null;
|
|
27
|
+
|
|
28
|
+
let used = Number(q.used ?? 0);
|
|
29
|
+
if (!Number.isFinite(used)) used = 0;
|
|
30
|
+
if (used < 0) used = 0;
|
|
31
|
+
|
|
32
|
+
let limit: number | null = null;
|
|
33
|
+
if (q.limit === null) {
|
|
34
|
+
limit = null;
|
|
35
|
+
} else if (q.limit !== undefined) {
|
|
36
|
+
const n = Number(q.limit);
|
|
37
|
+
if (Number.isFinite(n) && n > 0) {
|
|
38
|
+
limit = n;
|
|
39
|
+
} else {
|
|
40
|
+
limit = null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const unit = typeof q.unit === "string" ? q.unit : "";
|
|
45
|
+
const reset = typeof q.reset === "string" ? q.reset : undefined;
|
|
46
|
+
const predictedReset = typeof q.predictedReset === "string" ? q.predictedReset : undefined;
|
|
47
|
+
const window = typeof q.window === "string" ? q.window : undefined;
|
|
48
|
+
const info = typeof q.info === "string" ? q.info : undefined;
|
|
49
|
+
const details = typeof q.details === "string" ? q.details : undefined;
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
id: q.id,
|
|
53
|
+
providerName: q.providerName,
|
|
54
|
+
used,
|
|
55
|
+
limit,
|
|
56
|
+
unit,
|
|
57
|
+
reset,
|
|
58
|
+
predictedReset,
|
|
59
|
+
window,
|
|
60
|
+
info,
|
|
61
|
+
details,
|
|
62
|
+
};
|
|
63
|
+
}
|