From b450c59c69d39194506a9a75d6cfca09cf1ed8c7 Mon Sep 17 00:00:00 2001 From: Martin Date: Tue, 10 Mar 2026 01:35:16 +0100 Subject: [PATCH] monorepo prepare --- .claude/plans/regiojet-dashboard.md | 376 +++++++++++++++++ ccusage | 1 + packages/utils/bun.lock | 220 ++++++++++ packages/utils/package.json | 83 ++++ packages/utils/src/core/diff.ts | 244 +++++++++++ packages/utils/src/core/formatting.ts | 317 ++++++++++++++ packages/utils/src/core/logger.ts | 169 ++++++++ packages/utils/src/core/path.ts | 94 +++++ packages/utils/src/core/rate-limit.ts | 154 +++++++ packages/utils/src/core/storage/index.ts | 2 + packages/utils/src/core/storage/runtime.ts | 46 ++ packages/utils/src/core/storage/storage.ts | 465 +++++++++++++++++++++ packages/utils/src/index.ts | 61 +++ packages/utils/test-import.ts | 29 ++ packages/utils/tsconfig.json | 18 + packages/utils/tsup.config.ts | 22 + src/utils/storage/index.ts | 6 +- tsconfig.json | 4 +- 18 files changed, 2308 insertions(+), 3 deletions(-) create mode 100644 .claude/plans/regiojet-dashboard.md create mode 160000 ccusage create mode 100644 packages/utils/bun.lock create mode 100644 packages/utils/package.json create mode 100644 packages/utils/src/core/diff.ts create mode 100644 packages/utils/src/core/formatting.ts create mode 100644 packages/utils/src/core/logger.ts create mode 100644 packages/utils/src/core/path.ts create mode 100644 packages/utils/src/core/rate-limit.ts create mode 100644 packages/utils/src/core/storage/index.ts create mode 100644 packages/utils/src/core/storage/runtime.ts create mode 100644 packages/utils/src/core/storage/storage.ts create mode 100644 packages/utils/src/index.ts create mode 100644 packages/utils/test-import.ts create mode 100644 packages/utils/tsconfig.json create mode 100644 packages/utils/tsup.config.ts diff --git a/.claude/plans/regiojet-dashboard.md b/.claude/plans/regiojet-dashboard.md new file mode 100644 index 000000000..3e52f0417 --- /dev/null +++ b/.claude/plans/regiojet-dashboard.md @@ -0,0 +1,376 @@ +# Regiojet Travel Dashboard - Implementation Plan + +## Overview + +Create a new "Regiojet" page in the claude-history-dashboard that fetches, caches, and visualizes travel history from the Regiojet API with comprehensive statistics and cyberpunk-styled charts. + +## API Endpoints + +### 1. Tickets API (Primary - Rich Data) +``` +GET https://brn-ybus-pubapi.sa.cz/restapi/tickets?dateFrom=2020-01-01&dateTo=2026-01-17&sortDirection=DESC&limit=100 +``` + +**Returns:** +- `id`, `ticketCode`, `routeId`, `price`, `currency`, `state` +- `seatClassKey` (TRAIN_LOW_COST, etc.) +- `routeSections[].section`: + - `vehicleType` (TRAIN, BUS) + - `line` (code, from, to, lineGroupCode) + - `departureCityName`, `departureStationName`, `departureTime` + - `arrivalCityName`, `arrivalStationName`, `arrivalTime` + - `services[]` (wifi, catering, etc.) + +### 2. Payments API (Secondary - Financial Data) +``` +GET https://brn-ybus-pubapi.sa.cz/restapi/payments?dateFrom=2020-01-01&dateTo=2026-01-17&type=CREDIT&type=DIRECT&limit=100 +``` + +**Returns:** +- `paymentId`, `ticketId`, `amount`, `currency`, `method`, `dateTransaction` +- `description` (ticket purchase, cancellation, catering) + +## Statistics & Visualizations + +### Overview Cards (4 cards, top row) +| Stat | Icon | Description | +|------|------|-------------| +| Total Trips | Train | Count of valid tickets | +| Total Spent | Wallet | Sum of all ticket prices | +| Avg Trip Cost | TrendingUp | Average ticket price | +| Total Travel Time | Clock | Sum of all journey durations | + +### Charts & Visualizations + +#### 1. Trips by Month (Bar Chart) +- X-axis: Months (Jan 2020 - present) +- Y-axis: Number of trips +- Color: Amber (primary neon) +- Hover: Show exact count + total spent that month + +#### 2. Spending Trend (Area Chart) +- X-axis: Months +- Y-axis: Amount in CZK +- Fill: Gradient amber to transparent +- Line: Cyan for cumulative total + +#### 3. Most Traveled Routes (Horizontal Bar Chart) +- Top 10 routes (e.g., "Praha → Brno") +- Show trip count + total spent per route +- Color gradient based on frequency + +#### 4. City Distribution (Dual Pie/Donut Charts) +- Left: Departure cities distribution +- Right: Arrival cities distribution +- Center: Total trips count +- Colors: Amber/Cyan palette + +#### 5. Vehicle Type Distribution (Donut Chart) +- TRAIN vs BUS breakdown +- Show percentage and count + +#### 6. Day of Week Heatmap +- 7 columns (Mon-Sun) +- Show trip frequency per day +- Glow intensity based on count + +#### 7. Time of Day Distribution (Histogram) +- 24 bars (hours 0-23) +- Group departures by hour +- Identify peak travel times + +#### 8. Seat Class Distribution (Stacked Bar) +- LOW_COST vs Standard vs Business (if applicable) +- Show cost savings potential + +#### 9. Payment Method Breakdown (Pie Chart) +- ONLINE_PAYMENT, CREDIT_CARD, ACCOUNT +- Show percentage of each + +#### 10. Travel Calendar Heatmap +- GitHub-style contribution graph +- Each cell = one day +- Color intensity = trip count +- Show full year view + +### Additional Stats (Cards/Metrics) +- **Longest Trip**: Route with max duration +- **Most Expensive Trip**: Highest single ticket price +- **Busiest Month**: Month with most trips +- **Favorite Route**: Most frequently traveled +- **Total Distance**: Estimated km (if calculable from route data) +- **Cancellation Rate**: % of cancelled tickets +- **Peak Travel Hour**: Most common departure time +- **Weekend vs Weekday**: Travel pattern ratio + +## Technical Implementation + +### File Structure +``` +src/claude-history-dashboard/src/ +├── routes/ +│ └── regiojet.tsx # Main page component +├── server/ +│ └── regiojet.ts # Server functions for API + caching +├── components/ +│ └── charts/ # Chart components (if reusable) +│ ├── BarChart.tsx +│ ├── PieChart.tsx +│ ├── AreaChart.tsx +│ └── HeatmapCalendar.tsx +└── types/ + └── regiojet.ts # TypeScript interfaces +``` + +### Server Functions (`src/server/regiojet.ts`) + +```typescript +import { createServerFn } from '@tanstack/react-start' +import { Storage } from '@app/utils/storage' + +const storage = new Storage('regiojet-dashboard') + +// Fetch all tickets with 1-year cache +export const getRegiojetTickets = createServerFn({ method: 'GET' }) + .handler(async () => { + return storage.getFileOrPut( + 'tickets/all.json', + async () => { + // Paginate through all tickets + // Return combined array + }, + '365 days' + ) + }) + +// Fetch all payments with 1-year cache +export const getRegiojetPayments = createServerFn({ method: 'GET' }) + .handler(async () => { + return storage.getFileOrPut( + 'payments/all.json', + async () => { /* fetch and paginate */ }, + '365 days' + ) + }) + +// Compute statistics from cached data +export const getRegiojetStats = createServerFn({ method: 'GET' }) + .handler(async () => { + const [tickets, payments] = await Promise.all([ + getRegiojetTickets(), + getRegiojetPayments() + ]) + return computeStats(tickets, payments) + }) +``` + +### Caching Strategy (IMPORTANT) +- Cache directory: `~/.genesis-tools/regiojet-dashboard/cache/` +- TTL: 365 days (per user requirement) +- Cache keys: + - `tickets/all.json` - ALL tickets in one file + - `payments/all.json` - ALL payments in one file + +**First-time fetch behavior:** +1. Server function checks if cache exists and is valid +2. If NO cache: Paginate through ENTIRE API history (all years) +3. Combine all pages into ONE JSON array +4. Save to cache file +5. Return data + +**Subsequent fetches:** +- Return cached data instantly (no API calls) +- Cache valid for 365 days + +**Manual refresh:** +- "Refresh Data" button on UI +- Calls `forceRefreshData()` server function +- Deletes cache files and re-fetches everything +- Updates cache with fresh data + +### API Pagination (Aggressive - Fetch Everything) +Strategy for fetching ALL historical data: +1. Use large page size: `limit=500` (or max allowed) +2. Start with `offset=0` +3. Loop: fetch page, if `response.length === limit` → continue with `offset += limit` +4. Stop when `response.length < limit` (last page) +5. Combine ALL pages into single array before caching + +```typescript +async function fetchAllPaginated(baseUrl: string, headers: Headers): Promise { + const allData: T[] = [] + let offset = 0 + const limit = 500 // Large batch size + + while (true) { + const url = `${baseUrl}&limit=${limit}&offset=${offset}` + const response = await fetch(url, { headers }) + const page = await response.json() as T[] + + allData.push(...page) + + if (page.length < limit) break // Last page + offset += limit + } + + return allData // Single combined array +} +``` + +**Result: One API session fetches EVERYTHING, then cached for 1 year.** + +### TypeScript Interfaces + +```typescript +interface RegiojetTicket { + id: number + ticketCode: string + price: number + currency: string + state: 'VALID' | 'CANCELLED' | 'USED' + seatClassKey: string + routeSections: Array<{ + section: { + vehicleType: 'TRAIN' | 'BUS' + line: { code: string; from: string; to: string } + departureCityName: string + departureTime: string + arrivalCityName: string + arrivalTime: string + services: string[] + } + }> +} + +interface RegiojetPayment { + paymentId: number + ticketId: number | null + amount: number + currency: string + method: 'ONLINE_PAYMENT' | 'CREDIT_CARD' | 'ACCOUNT' + dateTransaction: string + description: string +} + +interface RegiojetStats { + totalTrips: number + totalSpent: number + avgTripCost: number + totalTravelMinutes: number + tripsByMonth: Record + spendingByMonth: Record + routeCounts: Record + departureCities: Record + arrivalCities: Record + vehicleTypes: Record + dayOfWeekCounts: number[] + hourOfDayCounts: number[] + seatClassCounts: Record + paymentMethods: Record + dailyActivity: Record +} +``` + +### Chart Implementation Approach +Since no chart library is installed, implement charts using: +1. **CSS + Tailwind**: Flexbox-based bars, CSS gradients +2. **SVG**: For pie/donut charts and area charts +3. **CSS Grid**: For calendar heatmap + +This matches the existing stats.tsx approach (div-based bar charts). + +### Route Definition (`src/routes/regiojet.tsx`) + +```typescript +export const Route = createFileRoute('/regiojet')({ + component: RegiojetPage, + loader: async () => { + const stats = await getRegiojetStats() + return { stats } + }, +}) +``` + +### Navigation Update +Add link to Header.tsx: +```tsx +Regiojet +``` + +## UI Design Guidelines (Cyberpunk Theme) + +### Colors +- Primary Neon: `var(--neon-primary)` - Amber #ff9500 +- Secondary Neon: `var(--neon-secondary)` - Cyan #00f0ff +- Background: `var(--bg-primary)` - Deep dark +- Cards: `var(--bg-secondary)` with glass effect + +### Effects to Apply +- `glass-card` class on main containers +- `neon-border` on stat cards +- `gradient-text` on main heading +- Subtle `animate-fade-in-up` on load +- Glow effects on interactive elements + +### Layout +- Max width: 7xl (1280px) +- Grid: 4 cols for overview stats +- 2-column layout for charts on desktop +- Full-width for large visualizations (calendar, trends) + +## Implementation Steps + +1. **Create types file** (`src/types/regiojet.ts`) + - Define all TypeScript interfaces + +2. **Create server functions** (`src/server/regiojet.ts`) + - Implement API fetching with pagination + - Add caching with Storage class + - Create stats computation function + +3. **Create chart components** (`src/components/charts/`) + - BarChart (horizontal and vertical variants) + - PieChart/DonutChart (SVG-based) + - AreaChart (SVG path-based) + - HeatmapCalendar (CSS grid) + +4. **Create main page** (`src/routes/regiojet.tsx`) + - Layout with all stat cards + - Integrate all chart components + - Add refresh data button + - Apply cyberpunk styling + +5. **Update navigation** (`src/components/Header.tsx`) + - Add Regiojet link to nav + +6. **Testing & Polish** + - Test with real API data + - Verify caching works + - Polish animations and responsiveness + +## Critical Files to Modify + +| File | Action | +|------|--------| +| `src/routes/regiojet.tsx` | CREATE - Main page | +| `src/server/regiojet.ts` | CREATE - Server functions | +| `src/types/regiojet.ts` | CREATE - TypeScript types | +| `src/components/Header.tsx` | MODIFY - Add nav link | +| `src/components/charts/*.tsx` | CREATE - Chart components | + +## Verification Plan + +1. **API Access**: Verify bearer token works and data fetches correctly +2. **Caching**: Check `~/.genesis-tools/regiojet-dashboard/cache/` for cached files +3. **Stats Computation**: Console log computed stats before rendering +4. **Chart Rendering**: Verify all charts display with sample data +5. **Responsiveness**: Test on mobile/tablet breakpoints +6. **Theme Consistency**: Compare with existing stats.tsx styling + +## Dependencies + +No new dependencies required. Using: +- Existing TanStack Start infrastructure +- Existing Storage utility from `@app/utils/storage` +- Native SVG for charts +- Tailwind CSS for styling diff --git a/ccusage b/ccusage new file mode 160000 index 000000000..0225b9132 --- /dev/null +++ b/ccusage @@ -0,0 +1 @@ +Subproject commit 0225b9132904ec87b93f3e372426ba0e90c13eb5 diff --git a/packages/utils/bun.lock b/packages/utils/bun.lock new file mode 100644 index 000000000..021ae15c2 --- /dev/null +++ b/packages/utils/bun.lock @@ -0,0 +1,220 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "@genesis-tools/utils", + "devDependencies": { + "tsup": "^8.5.1", + "typescript": "^5.9.3", + }, + "peerDependencies": { + "chalk": "^5.0.0", + "pino": "^9.0.0", + "pino-pretty": "^11.0.0", + }, + "optionalPeers": [ + "chalk", + "pino", + "pino-pretty", + ], + }, + }, + "packages": { + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.56.0", "", { "os": "android", "cpu": "arm" }, "sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.56.0", "", { "os": "android", "cpu": "arm64" }, "sha512-lfbVUbelYqXlYiU/HApNMJzT1E87UPGvzveGg2h0ktUNlOCxKlWuJ9jtfvs1sKHdwU4fzY7Pl8sAl49/XaEk6Q=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.56.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-EgxD1ocWfhoD6xSOeEEwyE7tDvwTgZc8Bss7wCWe+uc7wO8G34HHCUH+Q6cHqJubxIAnQzAsyUsClt0yFLu06w=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.56.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-1vXe1vcMOssb/hOF8iv52A7feWW2xnu+c8BV4t1F//m9QVLTfNVpEdja5ia762j/UEJe2Z1jAmEqZAK42tVW3g=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.56.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-bof7fbIlvqsyv/DtaXSck4VYQ9lPtoWNFCB/JY4snlFuJREXfZnm+Ej6yaCHfQvofJDXLDMTVxWscVSuQvVWUQ=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.56.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KNa6lYHloW+7lTEkYGa37fpvPq+NKG/EHKM8+G/g9WDU7ls4sMqbVRV78J6LdNuVaeeK5WB9/9VAFbKxcbXKYg=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.56.0", "", { "os": "linux", "cpu": "arm" }, "sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.56.0", "", { "os": "linux", "cpu": "arm" }, "sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.56.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.56.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.56.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.56.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.56.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.56.0", "", { "os": "linux", "cpu": "x64" }, "sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.56.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.56.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.56.0", "", { "os": "none", "cpu": "arm64" }, "sha512-LhN/Reh+7F3RCgQIRbgw8ZMwUwyqJM+8pXNT6IIJAqm2IdKkzpCh/V9EdgOMBKuebIrzswqy4ATlrDgiOwbRcQ=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.56.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-kbFsOObXp3LBULg1d3JIUQMa9Kv4UitDmpS+k0tinPBz3watcUiV2/LUDMMucA6pZO3WGE27P7DsfaN54l9ing=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.56.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-vSSgny54D6P4vf2izbtFm/TcWYedw7f8eBrOiGGecyHyQB9q4Kqentjaj8hToe+995nob/Wv48pDqL5a62EWtg=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.56.0", "", { "os": "win32", "cpu": "x64" }, "sha512-FeCnkPCTHQJFbiGG49KjV5YGW/8b9rrXAM2Mz2kiIoktq2qsJxRD5giEMEOD2lPdgs72upzefaUvS+nc8E3UzQ=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.56.0", "", { "os": "win32", "cpu": "x64" }, "sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + + "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], + + "bundle-require": ["bundle-require@5.1.0", "", { "dependencies": { "load-tsconfig": "^0.2.3" }, "peerDependencies": { "esbuild": ">=0.18" } }, "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA=="], + + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], + + "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + + "commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + + "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], + + "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fix-dts-default-cjs-exports": ["fix-dts-default-cjs-exports@1.0.1", "", { "dependencies": { "magic-string": "^0.30.17", "mlly": "^1.7.4", "rollup": "^4.34.8" } }, "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], + + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], + + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "load-tsconfig": ["load-tsconfig@0.2.5", "", {}, "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], + + "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], + + "postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="], + + "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + + "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], + + "rollup": ["rollup@4.56.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.56.0", "@rollup/rollup-android-arm64": "4.56.0", "@rollup/rollup-darwin-arm64": "4.56.0", "@rollup/rollup-darwin-x64": "4.56.0", "@rollup/rollup-freebsd-arm64": "4.56.0", "@rollup/rollup-freebsd-x64": "4.56.0", "@rollup/rollup-linux-arm-gnueabihf": "4.56.0", "@rollup/rollup-linux-arm-musleabihf": "4.56.0", "@rollup/rollup-linux-arm64-gnu": "4.56.0", "@rollup/rollup-linux-arm64-musl": "4.56.0", "@rollup/rollup-linux-loong64-gnu": "4.56.0", "@rollup/rollup-linux-loong64-musl": "4.56.0", "@rollup/rollup-linux-ppc64-gnu": "4.56.0", "@rollup/rollup-linux-ppc64-musl": "4.56.0", "@rollup/rollup-linux-riscv64-gnu": "4.56.0", "@rollup/rollup-linux-riscv64-musl": "4.56.0", "@rollup/rollup-linux-s390x-gnu": "4.56.0", "@rollup/rollup-linux-x64-gnu": "4.56.0", "@rollup/rollup-linux-x64-musl": "4.56.0", "@rollup/rollup-openbsd-x64": "4.56.0", "@rollup/rollup-openharmony-arm64": "4.56.0", "@rollup/rollup-win32-arm64-msvc": "4.56.0", "@rollup/rollup-win32-ia32-msvc": "4.56.0", "@rollup/rollup-win32-x64-gnu": "4.56.0", "@rollup/rollup-win32-x64-msvc": "4.56.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg=="], + + "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], + + "sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="], + + "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], + + "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], + + "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], + + "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], + + "tsup": ["tsup@8.5.1", "", { "dependencies": { "bundle-require": "^5.1.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "consola": "^3.4.0", "debug": "^4.4.0", "esbuild": "^0.27.0", "fix-dts-default-cjs-exports": "^1.0.0", "joycon": "^3.1.1", "picocolors": "^1.1.1", "postcss-load-config": "^6.0.1", "resolve-from": "^5.0.0", "rollup": "^4.34.8", "source-map": "^0.7.6", "sucrase": "^3.35.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.11", "tree-kill": "^1.2.2" }, "peerDependencies": { "@microsoft/api-extractor": "^7.36.0", "@swc/core": "^1", "postcss": "^8.4.12", "typescript": ">=4.5.0" }, "optionalPeers": ["@microsoft/api-extractor", "@swc/core", "postcss", "typescript"], "bin": { "tsup": "dist/cli-default.js", "tsup-node": "dist/cli-node.js" } }, "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], + } +} diff --git a/packages/utils/package.json b/packages/utils/package.json new file mode 100644 index 000000000..948c6f7a7 --- /dev/null +++ b/packages/utils/package.json @@ -0,0 +1,83 @@ +{ + "name": "@genesis-tools/utils", + "version": "0.1.0", + "type": "module", + "description": "Utility library for GenesisTools - storage, formatting, rate limiting, and more", + "author": "Martin", + "license": "MIT", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "./storage": { + "import": "./dist/core/storage/index.js", + "types": "./dist/core/storage/index.d.ts" + }, + "./formatting": { + "import": "./dist/core/formatting.js", + "types": "./dist/core/formatting.d.ts" + }, + "./path": { + "import": "./dist/core/path.js", + "types": "./dist/core/path.d.ts" + }, + "./diff": { + "import": "./dist/core/diff.js", + "types": "./dist/core/diff.d.ts" + }, + "./rate-limit": { + "import": "./dist/core/rate-limit.js", + "types": "./dist/core/rate-limit.d.ts" + }, + "./logger": { + "import": "./dist/core/logger.js", + "types": "./dist/core/logger.d.ts" + } + }, + "files": [ + "dist", + "src" + ], + "scripts": { + "build": "tsup", + "dev": "tsup --watch", + "typecheck": "tsc --noEmit", + "prepublishOnly": "npm run build" + }, + "devDependencies": { + "tsup": "^8.5.1", + "typescript": "^5.9.3" + }, + "peerDependencies": { + "chalk": "^5.0.0", + "pino": "^9.0.0", + "pino-pretty": "^11.0.0" + }, + "peerDependenciesMeta": { + "chalk": { + "optional": true + }, + "pino": { + "optional": true + }, + "pino-pretty": { + "optional": true + } + }, + "keywords": [ + "genesis-tools", + "utilities", + "storage", + "cache", + "formatting", + "rate-limit" + ], + "repository": { + "type": "git", + "url": "https://github.com/your-username/genesis-tools.git" + } +} diff --git a/packages/utils/src/core/diff.ts b/packages/utils/src/core/diff.ts new file mode 100644 index 000000000..7b0840e1a --- /dev/null +++ b/packages/utils/src/core/diff.ts @@ -0,0 +1,244 @@ +import { spawn } from "node:child_process"; +import { unlink, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +/** + * Optional logger interface for DiffUtil + */ +export interface DiffLogger { + info: (msg: string) => void; + warn: (msg: string) => void; + error: (msg: string) => void; +} + +const noopLogger: DiffLogger = { + info: () => {}, + warn: () => {}, + error: () => {}, +}; + +/** + * Color function type for formatting diff output + */ +export interface DiffColorizer { + red: (text: string) => string; + green: (text: string) => string; + cyan: (text: string) => string; + gray: (text: string) => string; + yellow: (text: string) => string; +} + +const noopColorizer: DiffColorizer = { + red: (text) => text, + green: (text) => text, + cyan: (text) => text, + gray: (text) => text, + yellow: (text) => text, +}; + +/** + * Options for DiffUtil + */ +export interface DiffOptions { + logger?: DiffLogger; + colorizer?: DiffColorizer; +} + +/** + * Utility for showing diffs using system diff command + */ +export class DiffUtil { + private logger: DiffLogger; + private colorizer: DiffColorizer; + + constructor(options: DiffOptions = {}) { + this.logger = options.logger || noopLogger; + this.colorizer = options.colorizer || noopColorizer; + } + + /** + * Show diff between two strings using system diff command + * @param oldContent The old content + * @param newContent The new content + * @param oldLabel Label for old content (e.g., "Current") + * @param newLabel Label for new content (e.g., "Incoming") + * @returns Promise that resolves when diff is shown + */ + async showDiff( + oldContent: string, + newContent: string, + oldLabel: string = "old", + newLabel: string = "new" + ): Promise { + // Create temporary files + const tmpDir = tmpdir(); + const oldFile = join(tmpDir, `diff-old-${Date.now()}-${Math.random().toString(36).substring(7)}`); + const newFile = join(tmpDir, `diff-new-${Date.now()}-${Math.random().toString(36).substring(7)}`); + + try { + // Write content to temp files + await writeFile(oldFile, oldContent, "utf-8"); + await writeFile(newFile, newContent, "utf-8"); + + // Run diff command with at least 20 lines of context + return new Promise((resolve) => { + const proc = spawn("diff", ["-U", "20", oldFile, newFile], { + stdio: ["ignore", "pipe", "pipe"], + }); + + let stdout = ""; + let stderr = ""; + + proc.stdout.on("data", (data) => { + stdout += data.toString(); + }); + + proc.stderr.on("data", (data) => { + stderr += data.toString(); + }); + + proc.on("close", (code) => { + // Clean up temp files + Promise.all([unlink(oldFile).catch(() => {}), unlink(newFile).catch(() => {})]).finally(() => { + if (code === 0) { + // No differences + this.logger.info(this.colorizer.gray("No differences found.")); + resolve(); + } else if (code === 1) { + // Differences found - this is expected + const formatted = this.formatDiffOutput(stdout, oldLabel, newLabel); + this.logger.info(formatted); + resolve(); + } else { + // Error (code 2) + if (stderr) { + this.logger.error(this.colorizer.red(`Diff error: ${stderr}`)); + } + // Fallback: show a simple comparison + this.logger.warn( + this.colorizer.yellow("Could not generate diff. Showing content comparison:") + ); + this.logger.info(this.colorizer.red(`--- ${oldLabel}`)); + this.logger.info(oldContent); + this.logger.info(this.colorizer.green(`+++ ${newLabel}`)); + this.logger.info(newContent); + resolve(); + } + }); + }); + + proc.on("error", (error) => { + // Clean up temp files + Promise.all([unlink(oldFile).catch(() => {}), unlink(newFile).catch(() => {})]).finally(() => { + this.logger.error(this.colorizer.red(`Failed to run diff command: ${error.message}`)); + // Fallback: show a simple comparison + this.logger.warn(this.colorizer.yellow("Could not generate diff. Showing content comparison:")); + this.logger.info(this.colorizer.red(`--- ${oldLabel}`)); + this.logger.info(oldContent); + this.logger.info(this.colorizer.green(`+++ ${newLabel}`)); + this.logger.info(newContent); + resolve(); + }); + }); + }); + } catch (error) { + // Clean up temp files on error + await Promise.all([unlink(oldFile).catch(() => {}), unlink(newFile).catch(() => {})]); + this.logger.error(this.colorizer.red(`Failed to create diff: ${(error as Error).message}`)); + // Fallback: show a simple comparison + this.logger.warn(this.colorizer.yellow("Could not generate diff. Showing content comparison:")); + this.logger.info(this.colorizer.red(`--- ${oldLabel}`)); + this.logger.info(oldContent); + this.logger.info(this.colorizer.green(`+++ ${newLabel}`)); + this.logger.info(newContent); + } + } + + /** + * Format diff output with colors + */ + private formatDiffOutput(diffOutput: string, oldLabel: string, newLabel: string): string { + const lines = diffOutput.split("\n"); + let formatted = "\n"; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (line.startsWith("---")) { + formatted += this.colorizer.red(`--- ${oldLabel}\n`); + } else if (line.startsWith("+++")) { + formatted += this.colorizer.green(`+++ ${newLabel}\n`); + } else if (line.startsWith("-")) { + formatted += `${this.colorizer.red(line)}\n`; + } else if (line.startsWith("+")) { + formatted += `${this.colorizer.green(line)}\n`; + } else if (line.startsWith("@")) { + formatted += `${this.colorizer.cyan(line)}\n`; + } else { + formatted += `${line}\n`; + } + } + + return formatted; + } + + /** + * Static showDiff for backwards compatibility + * Usage: DiffUtil.showDiff(oldContent, newContent, oldLabel, newLabel) + */ + static async showDiff( + oldContent: string, + newContent: string, + oldLabel: string = "old", + newLabel: string = "new" + ): Promise { + const util = new DiffUtil(); + return util.showDiff(oldContent, newContent, oldLabel, newLabel); + } + + /** + * Compare two objects and check if they differ in specific fields + * @param oldObj The old object + * @param newObj The new object + * @param fields Fields to compare (defaults to ['args', 'name', 'env']) + * @returns Object with conflict info: { hasConflict: boolean, differences: string[] } + */ + static detectConflicts( + oldObj: Record, + newObj: Record, + fields: string[] = ["args", "name", "env"] + ): { hasConflict: boolean; differences: string[] } { + const differences: string[] = []; + + for (const field of fields) { + const oldValue = oldObj[field]; + const newValue = newObj[field]; + + // Deep comparison using JSON.stringify for simplicity + if (JSON.stringify(oldValue) !== JSON.stringify(newValue)) { + differences.push(field); + } + } + + return { + hasConflict: differences.length > 0, + differences, + }; + } +} + +/** + * Static methods for backwards compatibility + */ +export const showDiff = async ( + oldContent: string, + newContent: string, + oldLabel?: string, + newLabel?: string, + options?: DiffOptions +): Promise => { + const util = new DiffUtil(options); + return util.showDiff(oldContent, newContent, oldLabel, newLabel); +}; + +export const detectConflicts = DiffUtil.detectConflicts; diff --git a/packages/utils/src/core/formatting.ts b/packages/utils/src/core/formatting.ts new file mode 100644 index 000000000..fc12cef86 --- /dev/null +++ b/packages/utils/src/core/formatting.ts @@ -0,0 +1,317 @@ +import { randomBytes } from "node:crypto"; + +// ============================================ +// Session & Token Management +// ============================================ + +/** + * Generate a unique session ID with timestamp and random hex + */ +export function generateSessionId(): string { + const timestamp = new Date().toISOString().replace(/[:.]/g, "-").replace("T", "_").split("Z")[0]; + const random = randomBytes(3).toString("hex"); + return `${timestamp}_${random}`; +} + +/** + * Estimate token count from text (rough ~4 chars per token) + */ +export function estimateTokens(text: string): number { + return Math.ceil(text.length / 4); +} + +/** + * Format token count with K/M suffixes + */ +export function formatTokens(tokens: number): string { + if (tokens >= 1000000) { + return `${(tokens / 1000000).toFixed(1)}M`; + } else if (tokens >= 1000) { + return `${(tokens / 1000).toFixed(1)}K`; + } + return tokens.toString(); +} + +/** + * Format cost as currency + */ +export function formatCost(cost: number): string { + return `$${cost.toFixed(4)}`; +} + +/** + * Format duration in human-readable format + */ +export function formatDuration(ms: number): string { + if (ms < 1000) { + return `${ms}ms`; + } + + const seconds = Math.floor(ms / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + + if (hours > 0) { + return `${hours}h ${minutes % 60}m ${seconds % 60}s`; + } else if (minutes > 0) { + return `${minutes}m ${seconds % 60}s`; + } else { + return `${seconds}s`; + } +} + +/** + * Format file size in human-readable format + */ +export function formatFileSize(bytes: number): string { + const units = ["B", "KB", "MB", "GB", "TB"]; + let size = bytes; + let unitIndex = 0; + + while (size >= 1024 && unitIndex < units.length - 1) { + size /= 1024; + unitIndex++; + } + + return `${size.toFixed(1)} ${units[unitIndex]}`; +} + +// ============================================ +// Text Processing +// ============================================ + +/** + * Truncate text with ellipsis + */ +export function truncateText(text: string, maxLength: number = 100): string { + if (text.length <= maxLength) { + return text; + } + + return `${text.substring(0, maxLength - 3)}...`; +} + +/** + * Sanitize output by removing ANSI codes and control characters + */ +export function sanitizeOutput(text: string, removeANSI: boolean = false): string { + let sanitized = text; + + if (removeANSI) { + // Remove ANSI escape codes + // biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape codes use control chars + sanitized = sanitized.replace(/\x1b\[[0-9;]*m/g, ""); + } + + // Remove other potentially problematic characters + // biome-ignore lint/suspicious/noControlCharactersInRegex: intentionally stripping control chars + sanitized = sanitized.replace(/[\u0000-\u001F\u007F-\u009F]/g, ""); + + return sanitized; +} + +/** + * Safe JSON parsing with fallback + */ +export function parseJSON(text: string, fallback?: T): T | null { + try { + return JSON.parse(text) as T; + } catch { + if (fallback !== undefined) { + return fallback; + } + return null; + } +} + +// ============================================ +// UI/Progress +// ============================================ + +/** + * Create an ASCII progress bar + */ +export function createProgressBar(current: number, total: number, width: number = 40): string { + const percentage = Math.min(1, Math.max(0, current / total)); + const filled = Math.round(width * percentage); + const empty = width - filled; + + const filledBar = "█".repeat(filled); + const emptyBar = "░".repeat(empty); + const percentageText = `${Math.round(percentage * 100)}%`; + + return `[${filledBar}${emptyBar}] ${percentageText}`; +} + +// ============================================ +// Validation +// ============================================ + +/** + * Basic API key validation by provider + */ +export function validateAPIKey(key: string, provider: string): boolean { + const minLengths: Record = { + openai: 20, + anthropic: 20, + google: 20, + groq: 20, + openrouter: 20, + xai: 20, + jinaai: 20, + }; + + const minLength = minLengths[provider] || 20; + + if (key.length < minLength) { + return false; + } + + // Check for common patterns that indicate invalid keys + const invalidPatterns = [/^your_api_key_here$/i, /^sk-test-/i, /^placeholder$/i, /^xxx+/i]; + + if (invalidPatterns.some((pattern) => pattern.test(key))) { + return false; + } + + return true; +} + +/** + * Sanitize filename by removing invalid characters + */ +export function sanitizeFilename(filename: string): string { + // biome-ignore lint/suspicious/noControlCharactersInRegex: intentionally matching invalid filename chars + return filename.replace(/[<>:"/\\|?*\x00-\x1f]/g, "_"); +} + +// ============================================ +// Functional Utilities +// ============================================ + +/** + * Debounce function calls + */ +export function debounce void>( + func: T, + wait: number +): (...args: Parameters) => void { + let timeout: ReturnType; + + return (...args: Parameters) => { + clearTimeout(timeout); + timeout = setTimeout(() => func(...args), wait); + }; +} + +/** + * Throttle function calls + */ +export function throttle void>( + func: T, + limit: number +): (...args: Parameters) => void { + let inThrottle: boolean; + + return (...args: Parameters) => { + if (!inThrottle) { + func(...args); + inThrottle = true; + // biome-ignore lint/suspicious/noAssignInExpressions: standard throttle pattern + setTimeout(() => (inThrottle = false), limit); + } + }; +} + +/** + * Retry with exponential backoff + */ +export function retry(operation: () => Promise, maxAttempts: number = 3, delay: number = 1000): Promise { + return new Promise((resolve, reject) => { + let attempt = 0; + + const tryOperation = async () => { + try { + const result = await operation(); + resolve(result); + } catch (error) { + attempt++; + if (attempt >= maxAttempts) { + reject(error); + } else { + setTimeout(tryOperation, delay * 2 ** (attempt - 1)); + } + } + }; + + tryOperation(); + }); +} + +/** + * Add timeout to a promise + */ +export async function withTimeout(promise: Promise, timeoutMs: number, timeoutError?: Error): Promise { + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => { + reject(timeoutError || new Error(`Operation timed out after ${timeoutMs}ms`)); + }, timeoutMs); + }); + + return Promise.race([promise, timeoutPromise]); +} + +// ============================================ +// Object Utilities +// ============================================ + +/** + * Type guard for objects + */ +export function isObject(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +/** + * Deep merge two objects + */ +export function deepMerge>(target: T, source: Partial): T { + const result = { ...target }; + + for (const key in source) { + if (source[key] && isObject(source[key]) && isObject(result[key])) { + result[key] = deepMerge( + result[key] as Record, + source[key] as Record + ) as T[Extract]; + } else if (source[key] !== undefined) { + result[key] = source[key] as T[Extract]; + } + } + + return result; +} + +// ============================================ +// Environment +// ============================================ + +/** + * Safe environment variable access + */ +export function getEnvVar(name: string, required: boolean = false): string | undefined { + const value = process.env[name]; + + if (required && !value) { + throw new Error(`Required environment variable ${name} is not set`); + } + + return value; +} + +/** + * Generate a safe timestamp for filenames + */ +export function generateTimestamp(): string { + return new Date().toISOString().replace(/[:.]/g, "-"); +} diff --git a/packages/utils/src/core/logger.ts b/packages/utils/src/core/logger.ts new file mode 100644 index 000000000..f261db554 --- /dev/null +++ b/packages/utils/src/core/logger.ts @@ -0,0 +1,169 @@ +/** + * Lightweight logger module + * + * This module provides a simple logging interface that can work with or without pino. + * When pino is available as a peer dependency, it uses pino for logging. + * Otherwise, it falls back to console-based logging. + */ + +export type LogLevel = "trace" | "debug" | "info" | "warn" | "error" | "silent"; + +const LOG_LEVELS: Record = { + trace: 10, + debug: 20, + info: 30, + warn: 40, + error: 50, + silent: 100, +}; + +/** + * Check if log level is enabled + */ +function shouldLog(level: LogLevel, currentLevel: LogLevel): boolean { + return LOG_LEVELS[level] >= LOG_LEVELS[currentLevel]; +} + +/** + * Get log level from environment or arguments + */ +export function getLogLevel(): LogLevel { + if (process.env.LOG_TRACE === "1") { + return "trace"; + } + if (process.env.LOG_DEBUG === "1") { + return "debug"; + } + if (process.env.LOG_SILENT === "1") { + return "silent"; + } + if (process.argv.some((arg) => arg === "-vv") || process.argv.includes("--trace")) { + return "trace"; + } + if (process.argv.includes("-v") || process.argv.includes("--verbose")) { + return "debug"; + } + return "info"; +} + +/** + * Logger options + */ +export interface LoggerOptions { + level?: LogLevel; + prefix?: string; +} + +/** + * Logger interface + */ +export interface Logger { + trace: (...args: unknown[]) => void; + debug: (...args: unknown[]) => void; + info: (...args: unknown[]) => void; + warn: (...args: unknown[]) => void; + error: (...args: unknown[]) => void; + level: LogLevel; +} + +/** + * Create a console-based logger + */ +export function createLogger(options: LoggerOptions = {}): Logger { + const logLevel = options.level || getLogLevel(); + const prefix = options.prefix ? `[${options.prefix}] ` : ""; + const isTerminal = process.stdout.isTTY === true; + + const formatArgs = (args: unknown[]): string => { + return args + .map((arg) => { + if (typeof arg === "string") { + return arg; + } + if (arg instanceof Error) { + return arg.message; + } + try { + return JSON.stringify(arg); + } catch { + return String(arg); + } + }) + .join(" "); + }; + + // Try to import chalk for colors (optional) + const colors: { + gray: (s: string) => string; + yellow: (s: string) => string; + red: (s: string) => string; + } = { + gray: (s) => s, + yellow: (s) => s, + red: (s) => s, + }; + + // Attempt to load chalk if available + try { + // Dynamic import not supported in sync context, + // so we provide plain output by default + // Users can enhance with chalk by passing a colorizer + } catch { + // chalk not available, use plain output + } + + return { + level: logLevel, + trace: (...args: unknown[]) => { + if (shouldLog("trace", logLevel)) { + const msg = prefix + formatArgs(args); + console.log(isTerminal ? colors.gray(msg) : msg); + } + }, + debug: (...args: unknown[]) => { + if (shouldLog("debug", logLevel)) { + const msg = prefix + formatArgs(args); + console.log(isTerminal ? colors.gray(msg) : msg); + } + }, + info: (...args: unknown[]) => { + if (shouldLog("info", logLevel)) { + console.log(prefix + formatArgs(args)); + } + }, + warn: (...args: unknown[]) => { + if (shouldLog("warn", logLevel)) { + const levelPrefix = isTerminal ? colors.yellow("WARN:") : "WARN:"; + console.log(levelPrefix, prefix + formatArgs(args)); + } + }, + error: (...args: unknown[]) => { + if (shouldLog("error", logLevel)) { + const levelPrefix = isTerminal ? colors.red("ERROR:") : "ERROR:"; + console.log(levelPrefix, prefix + formatArgs(args)); + } + }, + }; +} + +/** + * Create a no-op logger (silent) + */ +export function createNoopLogger(): Logger { + const noop = () => {}; + return { + level: "silent", + trace: noop, + debug: noop, + info: noop, + warn: noop, + error: noop, + }; +} + +/** + * Default logger instance + */ +export const logger = createLogger(); + +export default logger; diff --git a/packages/utils/src/core/path.ts b/packages/utils/src/core/path.ts new file mode 100644 index 000000000..5e05588e6 --- /dev/null +++ b/packages/utils/src/core/path.ts @@ -0,0 +1,94 @@ +import os from "node:os"; +import pathUtils from "node:path"; + +/** + * Replaces the home directory with a tilde. + * @param path - The path to tildeify. + * @returns The tildeified path. + */ +export function tildeifyPath(path: string): string { + const homeDir = os.homedir(); + if (path.startsWith(homeDir)) { + return path.replace(homeDir, "~"); + } + return path; +} + +/** + * Resolves a path with tilde to absolute path. + * @param path - The path to resolve. + * @returns The resolved absolute path. + */ +export function resolvePathWithTilde(path: string): string { + if (path.startsWith("~")) { + return path.replace("~", os.homedir()); + } + + return pathUtils.resolve(path); +} + +/** + * Normalizes file path(s) from various formats that MCP tools might receive. + * Handles: + * - Arrays: `["file1.ts", "file2.ts"]` + * - JSON array strings: `'["file1.ts", "file2.ts"]'` + * - Python-style array strings: `"['file1.ts', 'file2.ts']"` + * - Single strings: `"file.ts"` + * + * @param input - The input value which could be a string, array, or array-like string + * @returns An array of file paths/patterns + */ +export function normalizeFilePaths(input: string | string[] | unknown): string[] { + // Already an array + if (Array.isArray(input)) { + return input.filter((item): item is string => typeof item === "string"); + } + + // Not a string, return empty array + if (typeof input !== "string") { + return []; + } + + const trimmed = input.trim(); + + // Check if it looks like an array string (starts with [ and ends with ]) + if (trimmed.startsWith("[") && trimmed.endsWith("]")) { + // Try parsing as JSON first (handles double-quoted strings) + try { + const parsed = JSON.parse(trimmed); + if (Array.isArray(parsed)) { + return parsed.filter((item): item is string => typeof item === "string"); + } + } catch { + // JSON.parse failed, try handling Python-style arrays (single quotes) + try { + // Convert single quotes to double quotes for JSON compatibility + const pythonToJson = trimmed.replace(/'/g, '"'); + const parsed = JSON.parse(pythonToJson); + if (Array.isArray(parsed)) { + return parsed.filter((item): item is string => typeof item === "string"); + } + } catch { + // Both JSON parsing attempts failed, manually parse the array + // Extract content between brackets, handling both single and double quotes + const match = trimmed.match(/\[(.*)\]/s); + if (match) { + const content = match[1].trim(); + if (content) { + // Split by comma and clean up quotes + const items = content + .split(",") + .map((item) => item.trim().replace(/^['"]|['"]$/g, "")) + .filter((item) => item.length > 0); + if (items.length > 0) { + return items; + } + } + } + } + } + } + + // Not an array string, treat as single file path + return [trimmed]; +} diff --git a/packages/utils/src/core/rate-limit.ts b/packages/utils/src/core/rate-limit.ts new file mode 100644 index 000000000..fd28e52a7 --- /dev/null +++ b/packages/utils/src/core/rate-limit.ts @@ -0,0 +1,154 @@ +// Rate limit handling with exponential backoff + +const MAX_RETRIES = 5; +const INITIAL_DELAY_MS = 1000; + +/** + * Optional logger interface for rate limiting + */ +export interface RateLimitLogger { + warn: (msg: string) => void; + error: (msg: string) => void; +} + +const noopLogger: RateLimitLogger = { + warn: () => {}, + error: () => {}, +}; + +/** + * Rate limit error structure + */ +export interface RateLimitError { + status: number; + headers?: { + "x-ratelimit-remaining"?: string; + "x-ratelimit-reset"?: string; + "retry-after"?: string; + }; +} + +/** + * Check if error is a rate limit error + */ +export function isRateLimitError(error: unknown): error is RateLimitError { + if (!error || typeof error !== "object") { + return false; + } + const err = error as Record; + return err.status === 403 || err.status === 429; +} + +/** + * Get delay from rate limit error headers + */ +function getDelayFromHeaders(error: RateLimitError): number | null { + if (!error.headers) { + return null; + } + + // Check retry-after header (seconds) + if (error.headers["retry-after"]) { + return parseInt(error.headers["retry-after"], 10) * 1000; + } + + // Check rate limit reset time + if (error.headers["x-ratelimit-reset"]) { + const resetTime = parseInt(error.headers["x-ratelimit-reset"], 10) * 1000; + const now = Date.now(); + const delay = resetTime - now; + if (delay > 0) { + return Math.min(delay, 60000); // Cap at 60 seconds + } + } + + return null; +} + +/** + * Sleep for given milliseconds + */ +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +/** + * Options for withRetry function + */ +export interface RetryOptions { + onRetry?: (attempt: number, delay: number) => void; + maxRetries?: number; + label?: string; + logger?: RateLimitLogger; + onRequest?: (label: string) => void; + onResponse?: (label: string) => void; +} + +/** + * Execute function with retry on rate limit + */ +export async function withRetry(fn: () => Promise, options: RetryOptions = {}): Promise { + const maxRetries = options.maxRetries ?? MAX_RETRIES; + const logger = options.logger ?? noopLogger; + let lastError: unknown; + + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + if (attempt === 0 && options.label && options.onRequest) { + options.onRequest(options.label); + } + const result = await fn(); + if (options.label && options.onResponse) { + options.onResponse(options.label); + } + return result; + } catch (error) { + lastError = error; + + if (!isRateLimitError(error)) { + throw error; + } + + if (attempt === maxRetries) { + logger.error(`Rate limit: Max retries (${maxRetries}) exceeded`); + throw error; + } + + // Calculate delay + const headerDelay = getDelayFromHeaders(error); + const exponentialDelay = INITIAL_DELAY_MS * 2 ** attempt; + const delay = headerDelay ?? exponentialDelay; + + logger.warn( + `Rate limited. Retrying in ${Math.round(delay / 1000)}s... (attempt ${attempt + 1}/${maxRetries})` + ); + + if (options.onRetry) { + options.onRetry(attempt + 1, delay); + } + + await sleep(delay); + } + } + + throw lastError; +} + +/** + * Create a rate-limited API caller + */ +export function createRateLimitedCaller(minDelayMs: number = 100) { + let lastCallTime = 0; + + return async (fn: () => Promise, options?: RetryOptions): Promise => { + const now = Date.now(); + const timeSinceLastCall = now - lastCallTime; + + if (timeSinceLastCall < minDelayMs) { + await sleep(minDelayMs - timeSinceLastCall); + } + + lastCallTime = Date.now(); + return withRetry(fn, options); + }; +} diff --git a/packages/utils/src/core/storage/index.ts b/packages/utils/src/core/storage/index.ts new file mode 100644 index 000000000..07323fb7a --- /dev/null +++ b/packages/utils/src/core/storage/index.ts @@ -0,0 +1,2 @@ +export { fileExists, readFile, writeFile } from "./runtime"; +export { Storage, type StorageLogger, type TTLString } from "./storage"; diff --git a/packages/utils/src/core/storage/runtime.ts b/packages/utils/src/core/storage/runtime.ts new file mode 100644 index 000000000..6786fcf4d --- /dev/null +++ b/packages/utils/src/core/storage/runtime.ts @@ -0,0 +1,46 @@ +/** + * Runtime abstraction for file operations + * Supports both Bun and Node.js runtimes + */ + +declare const Bun: + | { + file: (path: string) => { text: () => Promise }; + write: (path: string, content: string) => Promise; + } + | undefined; + +const isBun = typeof Bun !== "undefined"; + +/** + * Read file content as text + * Uses Bun.file() in Bun runtime, fs.promises in Node.js + */ +export async function readFile(path: string): Promise { + if (isBun) { + return Bun?.file(path).text(); + } + const { readFile } = await import("node:fs/promises"); + return readFile(path, "utf-8"); +} + +/** + * Write content to file + * Uses Bun.write() in Bun runtime, fs.promises in Node.js + */ +export async function writeFile(path: string, content: string): Promise { + if (isBun) { + await Bun?.write(path, content); + return; + } + const { writeFile } = await import("node:fs/promises"); + await writeFile(path, content, "utf-8"); +} + +/** + * Check if file exists + */ +export function fileExists(path: string): boolean { + const { existsSync } = require("node:fs"); + return existsSync(path); +} diff --git a/packages/utils/src/core/storage/storage.ts b/packages/utils/src/core/storage/storage.ts new file mode 100644 index 000000000..a43acd4a7 --- /dev/null +++ b/packages/utils/src/core/storage/storage.ts @@ -0,0 +1,465 @@ +import { existsSync, mkdirSync, readdirSync, statSync, unlinkSync } from "node:fs"; +import { homedir } from "node:os"; +import { dirname, join } from "node:path"; +import { readFile, writeFile } from "./runtime"; + +/** + * TTL string format: " " or "" + * Units: "second(s)", "minute(s)", "hour(s)", "day(s)", "week(s)" + * Examples: "5 days", "1 hour", "30 minutes", "1 week" + */ +export type TTLString = string; + +/** + * Optional logger interface for Storage class + * If not provided, logging is disabled + */ +export interface StorageLogger { + debug: (msg: string) => void; + error: (msg: string) => void; +} + +const noopLogger: StorageLogger = { + debug: () => {}, + error: () => {}, +}; + +export class Storage { + private toolName: string; + private baseDir: string; + private cacheDir: string; + private configPath: string; + private logger: StorageLogger; + + /** + * Create a Storage instance for a tool + * @param toolName - Name of the tool (e.g., "timely", "ask") + * @param logger - Optional logger instance (defaults to no-op) + */ + constructor(toolName: string, logger: StorageLogger = noopLogger) { + this.toolName = toolName; + this.baseDir = join(homedir(), ".genesis-tools", toolName); + this.cacheDir = join(this.baseDir, "cache"); + this.configPath = join(this.baseDir, "config.json"); + this.logger = logger; + } + + // ============================================ + // Directory Management + // ============================================ + + /** + * Get the base directory for this tool + * @returns Absolute path to ~/.genesis-tools/ + */ + getBaseDir(): string { + return this.baseDir; + } + + /** + * Get the cache directory for this tool + * @returns Absolute path to ~/.genesis-tools//cache + */ + getCacheDir(): string { + return this.cacheDir; + } + + /** + * Get the config file path + * @returns Absolute path to ~/.genesis-tools//config.json + */ + getConfigPath(): string { + return this.configPath; + } + + /** + * Ensure all required directories exist + * Creates: baseDir, cacheDir + */ + async ensureDirs(): Promise { + if (!existsSync(this.baseDir)) { + mkdirSync(this.baseDir, { recursive: true }); + this.logger.debug(`Created directory: ${this.baseDir}`); + } + + if (!existsSync(this.cacheDir)) { + mkdirSync(this.cacheDir, { recursive: true }); + this.logger.debug(`Created directory: ${this.cacheDir}`); + } + } + + // ============================================ + // Config Management + // ============================================ + + /** + * Read the entire config object + * @returns The config object or null if not found + */ + async getConfig(): Promise { + try { + if (!existsSync(this.configPath)) { + return null; + } + const content = await readFile(this.configPath); + return JSON.parse(content) as T; + } catch (error) { + this.logger.error(`Failed to read config: ${error}`); + return null; + } + } + + /** + * Get a specific value from config + * @param key - The config key (supports dot notation: "oauth2.access_token") + * @returns The value or undefined + */ + async getConfigValue(key: string): Promise { + const config = await this.getConfig>(); + if (!config) { + return undefined; + } + + // Support dot notation + const keys = key.split("."); + let value: unknown = config; + for (const k of keys) { + if (value && typeof value === "object" && k in value) { + value = (value as Record)[k]; + } else { + return undefined; + } + } + return value as T; + } + + /** + * Set a value in config (merges with existing config) + * @param key - The config key (supports dot notation) + * @param value - The value to set + */ + async setConfigValue(key: string, value: T): Promise { + await this.ensureDirs(); + const config = (await this.getConfig>()) || {}; + + // Support dot notation for nested keys + const keys = key.split("."); + let current = config; + for (let i = 0; i < keys.length - 1; i++) { + const k = keys[i]; + if (!(k in current) || typeof current[k] !== "object") { + current[k] = {}; + } + current = current[k] as Record; + } + current[keys[keys.length - 1]] = value; + + await writeFile(this.configPath, JSON.stringify(config, null, 2)); + this.logger.debug(`Config updated: ${key}`); + } + + /** + * Set the entire config object + * @param config - The config object to save + */ + async setConfig(config: T): Promise { + await this.ensureDirs(); + await writeFile(this.configPath, JSON.stringify(config, null, 2)); + this.logger.debug(`Config saved`); + } + + /** + * Clear the config (delete config.json) + */ + async clearConfig(): Promise { + try { + if (existsSync(this.configPath)) { + unlinkSync(this.configPath); + this.logger.debug(`Config cleared`); + } + } catch (error) { + this.logger.error(`Failed to clear config: ${error}`); + } + } + + // ============================================ + // Cache Management + // ============================================ + + /** + * Parse TTL string to milliseconds + * @param ttl - TTL string like "5 days", "1 hour", "30 minutes" + * @returns Milliseconds + */ + parseTTL(ttl: TTLString): number { + const match = ttl.trim().match(/^(\d+)\s*(second|minute|hour|day|week)s?$/i); + if (!match) { + throw new Error(`Invalid TTL format: "${ttl}". Use format like "5 days", "1 hour", "30 minutes"`); + } + + const value = parseInt(match[1], 10); + const unit = match[2].toLowerCase(); + + const multipliers: Record = { + second: 1000, + minute: 60 * 1000, + hour: 60 * 60 * 1000, + day: 24 * 60 * 60 * 1000, + week: 7 * 24 * 60 * 60 * 1000, + }; + + return value * multipliers[unit]; + } + + /** + * Get the full path for a cache file + * @param relativePath - Relative path within cache directory + * @returns Absolute path + */ + private getCacheFilePath(relativePath: string): string { + return join(this.cacheDir, relativePath); + } + + /** + * Check if a cache file is expired based on file modification time + * @param filePath - Absolute path to the cache file + * @param ttlMs - TTL in milliseconds + * @returns true if expired or doesn't exist + */ + private isCacheFileExpired(filePath: string, ttlMs: number): boolean { + try { + if (!existsSync(filePath)) { + return true; + } + + const stats = statSync(filePath); + const age = Date.now() - stats.mtimeMs; + return age > ttlMs; + } catch { + return true; + } + } + + /** + * Put a file in the cache (saves as raw JSON, no metadata wrapper) + * Expiration is checked via file modification time + * @param relativePath - Relative path within cache directory + * @param data - Data to cache (will be JSON stringified) + * @param _ttl - TTL string (expiration checked via file mtime) + */ + async putCacheFile(relativePath: string, data: T, _ttl: TTLString): Promise { + await this.ensureDirs(); + const filePath = this.getCacheFilePath(relativePath); + + // Ensure parent directory exists + const dir = dirname(filePath); + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true }); + } + + // Save raw JSON without metadata wrapper + const content = JSON.stringify(data, null, 2); + await writeFile(filePath, content); + this.logger.debug(`Cache written: ${filePath}`); + } + + /** + * Get a file from cache (returns null if not found or expired) + * Expiration is checked based on file modification time + * @param relativePath - Relative path within cache directory + * @param ttl - TTL string to check expiration + * @returns Cached data or null + */ + async getCacheFile(relativePath: string, ttl: TTLString): Promise { + const filePath = this.getCacheFilePath(relativePath); + const ttlMs = this.parseTTL(ttl); + + if (this.isCacheFileExpired(filePath, ttlMs)) { + return null; + } + + try { + const content = await readFile(filePath); + return JSON.parse(content) as T; + } catch { + return null; + } + } + + /** + * Get cached file or fetch and cache it + * @param relativePath - Relative path within cache directory + * @param fetcher - Async function to fetch data if not cached + * @param ttl - TTL string like "5 days" + * @returns Cached or fetched data + */ + async getFileOrPut(relativePath: string, fetcher: () => Promise, ttl: TTLString): Promise { + const filePath = this.getCacheFilePath(relativePath); + + // Try to get from cache first + const cached = await this.getCacheFile(relativePath, ttl); + if (cached !== null) { + this.logger.debug(`Cache hit: ${filePath}`); + return cached; + } + + // Fetch fresh data + this.logger.debug(`Cache miss: ${filePath}, fetching...`); + const data = await fetcher(); + + // Store in cache + await this.putCacheFile(relativePath, data, ttl); + + return data; + } + + /** + * Delete a specific cache file + * @param relativePath - Relative path within cache directory + */ + async deleteCacheFile(relativePath: string): Promise { + const filePath = this.getCacheFilePath(relativePath); + try { + if (existsSync(filePath)) { + unlinkSync(filePath); + this.logger.debug(`Cache deleted: ${relativePath}`); + } + } catch (error) { + this.logger.error(`Failed to delete cache file: ${error}`); + } + } + + /** + * Clear all cache files + */ + async clearCache(): Promise { + try { + const removeDir = (dir: string) => { + if (!existsSync(dir)) { + return; + } + const files = readdirSync(dir, { withFileTypes: true }); + for (const file of files) { + const filePath = join(dir, file.name); + if (file.isDirectory()) { + removeDir(filePath); + } else { + unlinkSync(filePath); + } + } + }; + removeDir(this.cacheDir); + // Recreate empty cache directory + mkdirSync(this.cacheDir, { recursive: true }); + this.logger.debug(`Cache cleared for ${this.toolName}`); + } catch (error) { + this.logger.error(`Failed to clear cache: ${error}`); + } + } + + /** + * List all cache files + * @param absolute - If true, returns absolute paths; if false, returns relative paths (default: true) + * @returns Array of paths (absolute or relative based on parameter) + */ + async listCacheFiles(absolute: boolean = true): Promise { + const files: string[] = []; + + const walkDir = (dir: string, prefix: string = "") => { + if (!existsSync(dir)) { + return; + } + const entries = readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name; + if (entry.isDirectory()) { + walkDir(join(dir, entry.name), relativePath); + } else if (entry.name.endsWith(".json") || entry.name.endsWith(".md")) { + // Return absolute or relative path based on parameter + const path = absolute ? join(this.cacheDir, relativePath) : relativePath; + files.push(path); + } + } + }; + + walkDir(this.cacheDir); + return files; + } + + /** + * Get cache statistics + * @returns Object with count and total size + */ + async getCacheStats(): Promise<{ count: number; totalSizeBytes: number }> { + let count = 0; + let totalSizeBytes = 0; + + const walkDir = (dir: string) => { + if (!existsSync(dir)) { + return; + } + const entries = readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + const filePath = join(dir, entry.name); + if (entry.isDirectory()) { + walkDir(filePath); + } else { + count++; + const stats = statSync(filePath); + totalSizeBytes += stats.size; + } + } + }; + + walkDir(this.cacheDir); + return { count, totalSizeBytes }; + } + + // ============================================ + // Raw File Management (for non-JSON content) + // ============================================ + + /** + * Put a raw file in the cache (for non-JSON content like markdown, text, etc.) + * Expiration is checked via file modification time + * @param relativePath - Relative path within cache directory + * @param content - Raw file content (string) + * @param _ttl - TTL string (expiration checked via file mtime) + */ + async putRawFile(relativePath: string, content: string, _ttl: TTLString): Promise { + await this.ensureDirs(); + const filePath = this.getCacheFilePath(relativePath); + + // Ensure parent directory exists + const dir = dirname(filePath); + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true }); + } + + await writeFile(filePath, content); + this.logger.debug(`Raw file written: ${filePath}`); + } + + /** + * Get a raw file from cache (returns null if not found or expired) + * Expiration is checked based on file modification time + * @param relativePath - Relative path within cache directory + * @param ttl - TTL string to check expiration + * @returns Raw file content or null + */ + async getRawFile(relativePath: string, ttl: TTLString): Promise { + const filePath = this.getCacheFilePath(relativePath); + const ttlMs = this.parseTTL(ttl); + + if (this.isCacheFileExpired(filePath, ttlMs)) { + return null; + } + + try { + const content = await readFile(filePath); + return content; + } catch { + return null; + } + } +} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts new file mode 100644 index 000000000..260255111 --- /dev/null +++ b/packages/utils/src/index.ts @@ -0,0 +1,61 @@ +/** + * @genesis-tools/utils + * + * Utility library for GenesisTools - storage, formatting, rate limiting, and more + */ + +// Diff utilities +export { + type DiffColorizer, + type DiffLogger, + type DiffOptions, + DiffUtil, + detectConflicts, + showDiff, +} from "./core/diff"; +// Formatting utilities +export { + createProgressBar, + debounce, + deepMerge, + estimateTokens, + formatCost, + formatDuration, + formatFileSize, + formatTokens, + generateSessionId, + generateTimestamp, + getEnvVar, + isObject, + parseJSON, + retry, + sanitizeFilename, + sanitizeOutput, + throttle, + truncateText, + validateAPIKey, + withTimeout, +} from "./core/formatting"; +// Logger +export { + createLogger, + createNoopLogger, + getLogLevel, + type Logger, + type LoggerOptions, + type LogLevel, + logger, +} from "./core/logger"; +// Path utilities +export { normalizeFilePaths, resolvePathWithTilde, tildeifyPath } from "./core/path"; +// Rate limiting +export { + createRateLimitedCaller, + isRateLimitError, + type RateLimitError, + type RateLimitLogger, + type RetryOptions, + withRetry, +} from "./core/rate-limit"; +// Storage +export { fileExists, readFile, Storage, type StorageLogger, type TTLString, writeFile } from "./core/storage"; diff --git a/packages/utils/test-import.ts b/packages/utils/test-import.ts new file mode 100644 index 000000000..a166a627a --- /dev/null +++ b/packages/utils/test-import.ts @@ -0,0 +1,29 @@ +// Quick verification test for imports + +import { formatTokens } from "./dist/core/formatting.js"; +import { Storage as StorageSubpath } from "./dist/core/storage/index.js"; +import { createLogger, debounce, formatDuration, Storage } from "./dist/index.js"; + +console.log("Testing imports..."); + +// Test Storage +const storage = new Storage("test-tool"); +console.log("✓ Storage imported:", storage.getBaseDir()); + +// Test formatting +console.log("✓ formatDuration:", formatDuration(125000)); +console.log("✓ formatTokens:", formatTokens(15000)); + +// Test debounce +const debouncedFn = debounce(() => console.log("debounced"), 100); +console.log("✓ debounce works:", typeof debouncedFn); + +// Test logger +const logger = createLogger({ level: "info" }); +console.log("✓ createLogger works:", logger.level); + +// Test subpath import +const storageSubpath = new StorageSubpath("test-subpath"); +console.log("✓ Subpath import works:", storageSubpath.getCacheDir()); + +console.log("\nAll imports verified successfully!"); diff --git a/packages/utils/tsconfig.json b/packages/utils/tsconfig.json new file mode 100644 index 000000000..58eb36788 --- /dev/null +++ b/packages/utils/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src", + "noEmit": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/utils/tsup.config.ts b/packages/utils/tsup.config.ts new file mode 100644 index 000000000..67bbe5a47 --- /dev/null +++ b/packages/utils/tsup.config.ts @@ -0,0 +1,22 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: { + index: "src/index.ts", + "core/storage/index": "src/core/storage/index.ts", + "core/formatting": "src/core/formatting.ts", + "core/path": "src/core/path.ts", + "core/diff": "src/core/diff.ts", + "core/rate-limit": "src/core/rate-limit.ts", + "core/logger": "src/core/logger.ts", + }, + format: ["esm"], + dts: true, + clean: true, + splitting: false, + sourcemap: true, + platform: "neutral", + external: ["chalk", "pino", "pino-pretty"], + // Tree-shake unused exports + treeshake: true, +}); diff --git a/src/utils/storage/index.ts b/src/utils/storage/index.ts index 0d087d301..245acf180 100644 --- a/src/utils/storage/index.ts +++ b/src/utils/storage/index.ts @@ -1,2 +1,4 @@ -export { LockTimeoutError, withFileLock } from "./file-lock"; -export { Storage, type TTLString } from "./storage"; +// Re-export from @genesis-tools/utils package +// This shim maintains backwards compatibility with @app/utils/storage imports + +export { Storage, type StorageLogger, type TTLString } from "@genesis-tools/utils/core/storage/storage"; diff --git a/tsconfig.json b/tsconfig.json index fc5402ba8..898bdbdc7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -34,7 +34,9 @@ // Path aliases "paths": { "@app/*": ["./src/*"], - "@ask/*": ["./src/ask/*"] + "@ask/*": ["./src/ask/*"], + "@genesis-tools/utils": ["./packages/utils/src/index.ts"], + "@genesis-tools/utils/*": ["./packages/utils/src/*"] } }, "include": ["src/**/*", "./test-*.ts"]