@token-dashboard/codex-usage-uploader 0.1.5 → 0.1.7

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/src/install.js DELETED
@@ -1,156 +0,0 @@
1
- import fs from 'node:fs';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
- import { spawnSync } from 'node:child_process';
5
- import { fileURLToPath } from 'node:url';
6
- import { CLI_NAME } from './constants.js';
7
- import { saveRuntimeConfig } from './runtime-config.js';
8
-
9
- const SHELL_CONFIG_MARKER = '# Added by codex-usage-uploader';
10
- const SHELL_CONFIG_FILES = [
11
- path.join(os.homedir(), '.zshrc'),
12
- path.join(os.homedir(), '.bash_profile'),
13
- ];
14
-
15
- export function getPackageVersion(startUrl = import.meta.url) {
16
- const root = findPackageRoot(startUrl);
17
- const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
18
- return pkg.version ?? 'unknown';
19
- }
20
-
21
- export function findPackageRoot(startUrl = import.meta.url) {
22
- let current = path.dirname(fileURLToPath(startUrl));
23
- while (true) {
24
- const candidate = path.join(current, 'package.json');
25
- if (fs.existsSync(candidate)) {
26
- return current;
27
- }
28
- const parent = path.dirname(current);
29
- if (parent === current) {
30
- throw new Error('package.json not found from current runtime');
31
- }
32
- current = parent;
33
- }
34
- }
35
-
36
- export function resolveSelfPackageSpec(packageRoot, explicitSpec) {
37
- if (explicitSpec) return explicitSpec;
38
- return `file:${packageRoot}`;
39
- }
40
-
41
- export function installCurrentPackage(runtime, { packageRoot, packageSpec, nodePath = process.execPath } = {}) {
42
- const resolvedSpec = resolveSelfPackageSpec(packageRoot, packageSpec);
43
- const appRoot = runtime.appRoot;
44
- const stagingDir = path.join(appRoot, `.staging-${Date.now()}`);
45
- fs.mkdirSync(stagingDir, { recursive: true });
46
- fs.writeFileSync(
47
- path.join(stagingDir, 'package.json'),
48
- `${JSON.stringify({ private: true, name: 'codex-usage-uploader-runtime' }, null, 2)}\n`,
49
- );
50
-
51
- const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
52
- const result = spawnSync(
53
- npmCommand,
54
- ['install', '--no-save', '--omit=dev', '--no-package-lock', resolvedSpec],
55
- {
56
- cwd: stagingDir,
57
- stdio: 'inherit',
58
- env: { ...process.env, npm_config_fund: 'false', npm_config_audit: 'false' },
59
- },
60
- );
61
- if (result.error) {
62
- throw new Error(`failed to execute ${npmCommand}: ${result.error.message}`);
63
- }
64
- if (result.status !== 0) {
65
- throw new Error(`npm install failed for ${resolvedSpec}`);
66
- }
67
-
68
- const sourcePkg = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
69
- const installedPackageRoot = path.join(stagingDir, 'node_modules', ...String(sourcePkg.name).split('/'));
70
- const installedPkg = JSON.parse(fs.readFileSync(path.join(installedPackageRoot, 'package.json'), 'utf8'));
71
- const binField = installedPkg.bin;
72
- const binRel = typeof binField === 'string'
73
- ? binField
74
- : binField?.[CLI_NAME] ?? Object.values(binField ?? {})[0];
75
- if (!binRel) {
76
- throw new Error('installed package does not expose the expected CLI binary');
77
- }
78
-
79
- const currentDir = runtime.currentAppDir;
80
- fs.rmSync(currentDir, { recursive: true, force: true });
81
- fs.mkdirSync(path.dirname(currentDir), { recursive: true });
82
- fs.renameSync(stagingDir, currentDir);
83
-
84
- runtime.nodePath = nodePath;
85
- runtime.packageSpec = resolvedSpec;
86
- runtime.entryFile = path.join(currentDir, 'node_modules', ...String(sourcePkg.name).split('/'), binRel);
87
- writeLocalWrapper(runtime);
88
- ensureHomeBinLink(runtime);
89
- saveRuntimeConfig(runtime);
90
- return runtime;
91
- }
92
-
93
- export function writeLocalWrapper(runtime) {
94
- fs.mkdirSync(runtime.localBinDir, { recursive: true });
95
- const script = `#!/bin/sh
96
- set -eu
97
- CONFIG_FILE="\${CODEX_USAGE_UPLOADER_CONFIG:-${runtime.configFile}}"
98
- exec "${runtime.nodePath}" "${runtime.entryFile}" --config-file "$CONFIG_FILE" "$@"
99
- `;
100
- fs.writeFileSync(runtime.localBinPath, script, { mode: 0o755 });
101
- fs.chmodSync(runtime.localBinPath, 0o755);
102
- }
103
-
104
- export function ensureHomeBinLink(runtime) {
105
- fs.mkdirSync(path.dirname(runtime.homeBinLink), { recursive: true });
106
- try {
107
- if (fs.existsSync(runtime.homeBinLink) || fs.lstatSync(runtime.homeBinLink).isSymbolicLink()) {
108
- fs.rmSync(runtime.homeBinLink, { force: true });
109
- }
110
- } catch {
111
- // ignore
112
- }
113
- fs.symlinkSync(runtime.localBinPath, runtime.homeBinLink);
114
- }
115
-
116
- export function ensurePathInShellConfigs(binDir) {
117
- const exportLine = `export PATH="${binDir}:$PATH"`;
118
- for (const configFile of SHELL_CONFIG_FILES) {
119
- try {
120
- if (!fs.existsSync(configFile)) continue;
121
- const content = fs.readFileSync(configFile, 'utf8');
122
- if (content.includes(SHELL_CONFIG_MARKER)) continue;
123
- const prefix = content.length > 0 && !content.endsWith('\n') ? '\n' : '';
124
- fs.appendFileSync(configFile, `${prefix}\n${SHELL_CONFIG_MARKER}\n${exportLine}\n`);
125
- } catch {
126
- // best-effort, ignore errors
127
- }
128
- }
129
- }
130
-
131
- export function removePathFromShellConfigs() {
132
- for (const configFile of SHELL_CONFIG_FILES) {
133
- try {
134
- if (!fs.existsSync(configFile)) continue;
135
- const content = fs.readFileSync(configFile, 'utf8');
136
- if (!content.includes(SHELL_CONFIG_MARKER)) continue;
137
- const lines = content.split('\n');
138
- const filtered = [];
139
- for (let i = 0; i < lines.length; i++) {
140
- if (lines[i] === SHELL_CONFIG_MARKER) {
141
- if (i + 1 < lines.length && lines[i + 1].startsWith('export PATH=')) {
142
- i += 1;
143
- }
144
- if (filtered.length > 0 && filtered[filtered.length - 1] === '') {
145
- filtered.pop();
146
- }
147
- continue;
148
- }
149
- filtered.push(lines[i]);
150
- }
151
- fs.writeFileSync(configFile, filtered.join('\n'));
152
- } catch {
153
- // best-effort, ignore errors
154
- }
155
- }
156
- }
package/src/launchd.js DELETED
@@ -1,170 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { spawnSync } from 'node:child_process';
4
-
5
- export function buildLaunchdPlist(runtime) {
6
- const args = [
7
- runtime.nodePath,
8
- runtime.entryFile,
9
- '--config-file',
10
- runtime.configFile,
11
- 'run',
12
- ];
13
- return `<?xml version="1.0" encoding="UTF-8"?>
14
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
15
- <plist version="1.0">
16
- <dict>
17
- <key>Label</key>
18
- <string>${escapeXml(runtime.launchdLabel)}</string>
19
- <key>ProgramArguments</key>
20
- <array>
21
- ${args.map((item) => ` <string>${escapeXml(item)}</string>`).join('\n')}
22
- </array>
23
- <key>RunAtLoad</key>
24
- <true/>
25
- <key>KeepAlive</key>
26
- <true/>
27
- <key>ProcessType</key>
28
- <string>Background</string>
29
- <key>WorkingDirectory</key>
30
- <string>${escapeXml(runtime.installRoot)}</string>
31
- <key>StandardOutPath</key>
32
- <string>${escapeXml(runtime.stdoutLogPath)}</string>
33
- <key>StandardErrorPath</key>
34
- <string>${escapeXml(runtime.stderrLogPath)}</string>
35
- <key>EnvironmentVariables</key>
36
- <dict>
37
- <key>NODE_NO_WARNINGS</key>
38
- <string>1</string>
39
- </dict>
40
- </dict>
41
- </plist>
42
- `;
43
- }
44
-
45
- function escapeXml(value) {
46
- return String(value)
47
- .replaceAll('&', '&amp;')
48
- .replaceAll('<', '&lt;')
49
- .replaceAll('>', '&gt;')
50
- .replaceAll('"', '&quot;')
51
- .replaceAll("'", '&apos;');
52
- }
53
-
54
- export function parseLaunchctlPrint(output) {
55
- const state = output.match(/^\s*state = ([^\n]+)$/m)?.[1]?.trim() ?? null;
56
- const pidRaw = output.match(/^\s*pid = (\d+)$/m)?.[1] ?? null;
57
- const exitRaw = output.match(/^\s*last exit code = (-?\d+)$/m)?.[1] ?? null;
58
- return {
59
- loaded: true,
60
- running: state === 'running' || pidRaw != null,
61
- pid: pidRaw ? Number(pidRaw) : null,
62
- state,
63
- lastExitCode: exitRaw ? Number(exitRaw) : null,
64
- };
65
- }
66
-
67
- export class LaunchdServiceManager {
68
- constructor(runtime) {
69
- this.runtime = runtime;
70
- }
71
-
72
- ensureMacos() {
73
- if (process.platform !== 'darwin') {
74
- throw new Error('launchd management is only supported on macOS.');
75
- }
76
- }
77
-
78
- domainTarget() {
79
- return `gui/${process.getuid()}`;
80
- }
81
-
82
- serviceTarget() {
83
- return `${this.domainTarget()}/${this.runtime.launchdLabel}`;
84
- }
85
-
86
- run(args, { check = true } = {}) {
87
- const result = spawnSync(args[0], args.slice(1), { encoding: 'utf8' });
88
- if (check && result.status !== 0) {
89
- throw new Error(result.stderr?.trim() || result.stdout?.trim() || `command failed: ${args.join(' ')}`);
90
- }
91
- return result;
92
- }
93
-
94
- ensurePlist() {
95
- this.ensureMacos();
96
- if (!this.runtime.entryFile || !fs.existsSync(this.runtime.entryFile)) {
97
- throw new Error(`installed runtime entry not found: ${this.runtime.entryFile || '(empty)'}`);
98
- }
99
- const plist = buildLaunchdPlist(this.runtime);
100
- fs.mkdirSync(path.dirname(this.runtime.plistPath), { recursive: true });
101
- fs.mkdirSync(path.dirname(this.runtime.stdoutLogPath), { recursive: true });
102
- fs.writeFileSync(this.runtime.plistPath, plist);
103
- this.syncLaunchAgent(plist);
104
- }
105
-
106
- syncLaunchAgent(plist) {
107
- const launchAgentPath = this.runtime.launchAgentPath;
108
- if (!launchAgentPath) {
109
- return;
110
- }
111
- if (this.runtime.autoStartOnLogin) {
112
- fs.mkdirSync(path.dirname(launchAgentPath), { recursive: true });
113
- fs.writeFileSync(launchAgentPath, plist);
114
- return;
115
- }
116
- if (fs.existsSync(launchAgentPath)) {
117
- fs.rmSync(launchAgentPath, { force: true });
118
- }
119
- }
120
-
121
- start() {
122
- this.ensurePlist();
123
- this.stop();
124
- this.run(['launchctl', 'bootstrap', this.domainTarget(), this.runtime.plistPath]);
125
- }
126
-
127
- stop() {
128
- this.ensureMacos();
129
- this.run(['launchctl', 'bootout', this.serviceTarget()], { check: false });
130
- }
131
-
132
- restart() {
133
- this.start();
134
- }
135
-
136
- status() {
137
- this.ensureMacos();
138
- const result = this.run(['launchctl', 'print', this.serviceTarget()], { check: false });
139
- if (result.status !== 0) {
140
- return {
141
- loaded: false,
142
- running: false,
143
- pid: null,
144
- state: null,
145
- lastExitCode: null,
146
- };
147
- }
148
- return parseLaunchctlPrint(result.stdout);
149
- }
150
-
151
- uninstall() {
152
- this.stop();
153
- if (fs.existsSync(this.runtime.plistPath)) {
154
- fs.unlinkSync(this.runtime.plistPath);
155
- }
156
- if (this.runtime.launchAgentPath && fs.existsSync(this.runtime.launchAgentPath)) {
157
- fs.unlinkSync(this.runtime.launchAgentPath);
158
- }
159
- if (fs.existsSync(this.runtime.homeBinLink) && fs.lstatSync(this.runtime.homeBinLink).isSymbolicLink()) {
160
- try {
161
- const target = fs.realpathSync.native(this.runtime.homeBinLink);
162
- if (target === this.runtime.localBinPath) {
163
- fs.unlinkSync(this.runtime.homeBinLink);
164
- }
165
- } catch {
166
- fs.unlinkSync(this.runtime.homeBinLink);
167
- }
168
- }
169
- }
170
- }
@@ -1,342 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { createInterface } from 'node:readline';
4
- import {
5
- ARCHIVED_SESSIONS_SOURCE_ROOT,
6
- SESSIONS_SOURCE_ROOT,
7
- } from './constants.js';
8
- import { RolloutParser } from './parser.js';
9
-
10
- const LOCAL_USAGE_COLLECTOR = Object.freeze({ collectorId: 'local-usage' });
11
- const VALID_PERIODS = new Set(['today', '7d', '30d', 'all']);
12
- const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
13
-
14
- export function getLocalTimeZone() {
15
- return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
16
- }
17
-
18
- export function resolveUsageQuery({
19
- period,
20
- from,
21
- to,
22
- now = Date.now(),
23
- timeZone = getLocalTimeZone(),
24
- } = {}) {
25
- if (period && (from || to)) {
26
- throw new Error('--period cannot be combined with --from or --to');
27
- }
28
-
29
- if (period && !VALID_PERIODS.has(period)) {
30
- throw new Error(`--period must be one of: today, 7d, 30d, all`);
31
- }
32
-
33
- const today = formatDateInTimeZone(now, timeZone);
34
-
35
- if (period === 'today') {
36
- return {
37
- mode: 'today',
38
- from: today,
39
- to: today,
40
- trendFrom: today,
41
- trendTo: today,
42
- };
43
- }
44
-
45
- if (period === '7d') {
46
- return {
47
- mode: '7d',
48
- from: addDays(today, -6),
49
- to: today,
50
- trendFrom: addDays(today, -6),
51
- trendTo: today,
52
- };
53
- }
54
-
55
- if (period === '30d') {
56
- return {
57
- mode: '30d',
58
- from: addDays(today, -29),
59
- to: today,
60
- trendFrom: addDays(today, -29),
61
- trendTo: today,
62
- };
63
- }
64
-
65
- if (period === 'all' || (!period && !from && !to)) {
66
- return {
67
- mode: 'all',
68
- from: null,
69
- to: null,
70
- trendFrom: addDays(today, -29),
71
- trendTo: today,
72
- };
73
- }
74
-
75
- const normalizedFrom = from == null ? null : normalizeDateString(from, '--from');
76
- const normalizedTo = to == null ? null : normalizeDateString(to, '--to');
77
-
78
- if (normalizedFrom && normalizedTo && normalizedFrom > normalizedTo) {
79
- throw new Error('--from cannot be later than --to');
80
- }
81
-
82
- return {
83
- mode: 'custom',
84
- from: normalizedFrom,
85
- to: normalizedTo,
86
- trendFrom: normalizedFrom,
87
- trendTo: normalizedTo,
88
- };
89
- }
90
-
91
- export async function collectLocalUsage({
92
- sessionsDir,
93
- period,
94
- from,
95
- to,
96
- now = Date.now(),
97
- timeZone = getLocalTimeZone(),
98
- includeArchived = true,
99
- } = {}) {
100
- const range = resolveUsageQuery({ period, from, to, now, timeZone });
101
- const archivedSessionsDir = path.join(
102
- path.dirname(sessionsDir),
103
- ARCHIVED_SESSIONS_SOURCE_ROOT,
104
- );
105
-
106
- const roots = [
107
- { sourceRoot: SESSIONS_SOURCE_ROOT, dir: sessionsDir },
108
- ];
109
- if (includeArchived) {
110
- roots.push({
111
- sourceRoot: ARCHIVED_SESSIONS_SOURCE_ROOT,
112
- dir: archivedSessionsDir,
113
- });
114
- }
115
-
116
- const entries = roots.flatMap((root) =>
117
- iterRolloutFiles(root.dir).map((filePath) => ({
118
- ...root,
119
- filePath,
120
- relpath: path.relative(root.dir, filePath).split(path.sep).join('/'),
121
- })),
122
- );
123
-
124
- entries.sort((left, right) => left.filePath.localeCompare(right.filePath));
125
-
126
- const summary = emptyUsageRow();
127
- const dayMap = new Map();
128
- const trendMap = new Map();
129
- const modelMap = new Map();
130
- let tokenEventCount = 0;
131
-
132
- for (const entry of entries) {
133
- const parser = new RolloutParser(
134
- LOCAL_USAGE_COLLECTOR,
135
- `${entry.sourceRoot}/${entry.relpath}`,
136
- );
137
- const input = fs.createReadStream(entry.filePath, { encoding: 'utf8' });
138
- const rl = createInterface({ input, crlfDelay: Infinity });
139
- let lineNo = 0;
140
-
141
- try {
142
- for await (const line of rl) {
143
- lineNo += 1;
144
- if (!line) continue;
145
- const parsed = parser.processLine(lineNo, line);
146
- for (const event of parsed.events) {
147
- const eventDate = formatDateInTimeZone(event.timestamp, timeZone);
148
- if (!isWithinDateRange(eventDate, range.from, range.to)) {
149
- continue;
150
- }
151
-
152
- const usage = usageFromEvent(event);
153
- tokenEventCount += 1;
154
- accumulateUsage(summary, usage);
155
-
156
- const currentDay = dayMap.get(eventDate) ?? {
157
- date: eventDate,
158
- eventCount: 0,
159
- models: new Set(),
160
- ...emptyUsageRow(),
161
- };
162
- currentDay.eventCount += 1;
163
- currentDay.models.add(usage.model);
164
- accumulateUsage(currentDay, usage);
165
- dayMap.set(eventDate, currentDay);
166
-
167
- const currentModel = modelMap.get(usage.model) ?? {
168
- model: usage.model,
169
- eventCount: 0,
170
- ...emptyUsageRow(),
171
- };
172
- currentModel.eventCount += 1;
173
- accumulateUsage(currentModel, usage);
174
- modelMap.set(usage.model, currentModel);
175
-
176
- if (isWithinDateRange(eventDate, range.trendFrom, range.trendTo)) {
177
- const currentDate = trendMap.get(eventDate) ?? {
178
- date: eventDate,
179
- eventCount: 0,
180
- ...emptyUsageRow(),
181
- };
182
- currentDate.eventCount += 1;
183
- accumulateUsage(currentDate, usage);
184
- trendMap.set(eventDate, currentDate);
185
- }
186
- }
187
- }
188
- } finally {
189
- rl.close();
190
- }
191
- }
192
-
193
- const days = [...dayMap.values()]
194
- .sort((left, right) => left.date.localeCompare(right.date))
195
- .map((row) => ({
196
- ...row,
197
- models: [...row.models].sort((left, right) => left.localeCompare(right)),
198
- }));
199
- const trend = materializeTrendRows(range, trendMap);
200
- const byModel = [...modelMap.values()].sort((left, right) => {
201
- if (right.totalTokens !== left.totalTokens) {
202
- return right.totalTokens - left.totalTokens;
203
- }
204
- return left.model.localeCompare(right.model);
205
- });
206
-
207
- return {
208
- scope: 'local-machine',
209
- timezone: timeZone,
210
- range,
211
- sources: {
212
- sessionsDir,
213
- archivedSessionsDir,
214
- includeArchived,
215
- },
216
- filesScanned: entries.length,
217
- tokenEventCount,
218
- summary,
219
- days,
220
- trend,
221
- byModel,
222
- };
223
- }
224
-
225
- function iterRolloutFiles(rootDir) {
226
- if (!fs.existsSync(rootDir)) return [];
227
- const files = [];
228
- const walk = (dir) => {
229
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
230
- const fullPath = path.join(dir, entry.name);
231
- if (entry.isDirectory()) {
232
- walk(fullPath);
233
- } else if (entry.isFile() && /^rollout-.*\.jsonl$/.test(entry.name)) {
234
- files.push(fullPath);
235
- }
236
- }
237
- };
238
- walk(rootDir);
239
- return files;
240
- }
241
-
242
- function emptyUsageRow() {
243
- return {
244
- inputTokens: 0,
245
- cachedInputTokens: 0,
246
- outputTokens: 0,
247
- reasoningOutputTokens: 0,
248
- totalTokens: 0,
249
- };
250
- }
251
-
252
- function materializeTrendRows(range, trendMap) {
253
- const rows = [...trendMap.values()].sort((left, right) =>
254
- left.date.localeCompare(right.date),
255
- );
256
- if (rows.length === 0) {
257
- return [];
258
- }
259
- if (!range.trendFrom || !range.trendTo || range.trendFrom > range.trendTo) {
260
- return rows;
261
- }
262
- const materialized = [];
263
- for (let date = range.trendFrom; date <= range.trendTo; date = addDays(date, 1)) {
264
- materialized.push(
265
- trendMap.get(date) ?? {
266
- date,
267
- eventCount: 0,
268
- ...emptyUsageRow(),
269
- },
270
- );
271
- }
272
- return materialized;
273
- }
274
-
275
- function usageFromEvent(event) {
276
- return {
277
- model: event.model ?? '(unknown)',
278
- inputTokens: numberOrZero(event.lastInputTokens),
279
- cachedInputTokens: numberOrZero(event.lastCachedInputTokens),
280
- outputTokens: numberOrZero(event.lastOutputTokens),
281
- reasoningOutputTokens: numberOrZero(event.lastReasoningOutputTokens),
282
- totalTokens: numberOrZero(event.lastTotalTokens),
283
- };
284
- }
285
-
286
- function accumulateUsage(target, usage) {
287
- target.inputTokens += usage.inputTokens;
288
- target.cachedInputTokens += usage.cachedInputTokens;
289
- target.outputTokens += usage.outputTokens;
290
- target.reasoningOutputTokens += usage.reasoningOutputTokens;
291
- target.totalTokens += usage.totalTokens;
292
- }
293
-
294
- function numberOrZero(value) {
295
- return typeof value === 'number' && Number.isFinite(value) ? value : 0;
296
- }
297
-
298
- function formatDateInTimeZone(timestampMs, timeZone) {
299
- const parts = new Intl.DateTimeFormat('en-US', {
300
- timeZone,
301
- year: 'numeric',
302
- month: '2-digit',
303
- day: '2-digit',
304
- }).formatToParts(new Date(timestampMs));
305
- const values = Object.fromEntries(parts.map((part) => [part.type, part.value]));
306
- return `${values.year}-${values.month}-${values.day}`;
307
- }
308
-
309
- function normalizeDateString(value, label) {
310
- if (!DATE_RE.test(String(value))) {
311
- throw new Error(`${label} must be in YYYY-MM-DD format`);
312
- }
313
- const [yearRaw, monthRaw, dayRaw] = String(value).split('-');
314
- const year = Number(yearRaw);
315
- const month = Number(monthRaw);
316
- const day = Number(dayRaw);
317
- const normalized = new Date(Date.UTC(year, month - 1, day));
318
- if (
319
- Number.isNaN(normalized.getTime()) ||
320
- normalized.getUTCFullYear() !== year ||
321
- normalized.getUTCMonth() !== month - 1 ||
322
- normalized.getUTCDate() !== day
323
- ) {
324
- throw new Error(`${label} must be a valid calendar date`);
325
- }
326
- return `${yearRaw}-${monthRaw}-${dayRaw}`;
327
- }
328
-
329
- function addDays(dateString, deltaDays) {
330
- const [year, month, day] = dateString.split('-').map(Number);
331
- const shifted = new Date(Date.UTC(year, month - 1, day + deltaDays));
332
- const nextYear = shifted.getUTCFullYear();
333
- const nextMonth = String(shifted.getUTCMonth() + 1).padStart(2, '0');
334
- const nextDay = String(shifted.getUTCDate()).padStart(2, '0');
335
- return `${nextYear}-${nextMonth}-${nextDay}`;
336
- }
337
-
338
- function isWithinDateRange(date, from, to) {
339
- if (from && date < from) return false;
340
- if (to && date > to) return false;
341
- return true;
342
- }