saving current implementation of the action using the new pypi stats site

This commit is contained in:
Luke Hagar
2025-08-14 21:22:40 -05:00
parent 5aa7ed039e
commit 589919d91d
58 changed files with 20647 additions and 3177 deletions

20
.dev.env Normal file
View File

@@ -0,0 +1,20 @@
# NPM packages
INPUT_NPM-PACKAGES=sailpoint-api-client
# GitHub repositories
INPUT_GITHUB-REPOSITORIES=sailpoint-oss/sailpoint-cli
# PyPI packages
INPUT_PYPI-PACKAGES=sailpoint
# PowerShell modules
INPUT_POWERSHELL-MODULES=PSSailPoint,PSSailpoint.V3,PSSailpoint.Beta,PSSailpoint.V2024,PSSailpoint.V2025
# Go modules
INPUT_GO-MODULES=github.com/sailpoint-oss/golang-sdk/v2
# Output configuration
INPUT_JSON-OUTPUT-PATH=stats.json
INPUT_UPDATE-README=true
INPUT_COMMIT-MESSAGE=chore: update usage statistics
INPUT_README-PATH=Test-Readme.md

40
.github/workflows/stats.yml vendored Normal file
View File

@@ -0,0 +1,40 @@
name: Collect Usage Stats
on:
schedule:
- cron: '0 0 * * 1' # Every Monday at midnight UTC
workflow_dispatch: # Allow manual triggering
jobs:
stats:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- name: Install dependencies
run: bun install
- name: Run collection script
run: bun run scripts/collect.ts
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Configure Git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Commit and push changes
run: |
git add README.md output/
git commit -m "chore: update usage statistics" || echo "No changes to commit"
git push

177
ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,177 @@
# Usage Statistics - Simplified Architecture
This document describes the new simplified architecture for the usage statistics tracker.
## 🏗️ Architecture Overview
The new system is organized into clear, modular components:
```
project-root/
├── collectors/ # Platform-specific data collectors
│ ├── types.ts # Shared interfaces
│ ├── github.ts # GitHub repository stats
│ ├── npm.ts # NPM package stats
│ ├── pypi.ts # PyPI package stats
│ ├── homebrew.ts # Homebrew formula stats
│ ├── powershell.ts # PowerShell module stats
│ └── [removed]
├── core/ # Core orchestration logic
│ ├── runner.ts # Main collection orchestrator
│ ├── registry.ts # Collector registry
│ ├── summarize.ts # Markdown table generation
│ ├── update-readme.ts # README section replacement
│ ├── write-output.ts # JSON file writing
│ └── utils.ts # Shared utilities
├── config/
│ └── sources.json # Configuration of what to track
├── output/ # Generated output files
├── scripts/
│ └── collect.ts # Main collection script
└── .github/workflows/
└── stats.yml # GitHub Action workflow
```
## 🔧 Key Components
### 1. Collectors (`collectors/`)
Each collector is a simple function that takes a source name and returns a `MetricResult`:
```typescript
export interface MetricResult {
platform: string;
name: string;
timestamp: string;
metrics: Record<string, number | string | null>;
error?: string;
}
```
### 2. Registry (`core/registry.ts`)
The registry manages all collectors and provides a unified interface:
```typescript
export const collectors = {
github: { collect: collectGithubMetrics, batched: true },
npm: { collect: collectNpmMetrics, batched: false },
// ...
};
```
### 3. Runner (`core/runner.ts`)
The main orchestrator that:
- Loads sources from `config/sources.json`
- Groups sources by platform
- Handles batching for supported platforms
- Manages errors gracefully
### 4. Summarizer (`core/summarize.ts`)
Converts raw metrics into a human-readable markdown table for the README.
### 5. README Updater (`core/update-readme.ts`)
Replaces a marked section in the README with the generated statistics.
## 📊 Configuration
Sources are configured in `config/sources.json`:
```json
{
"sources": [
{
"platform": "npm",
"name": "express"
},
{
"platform": "github",
"name": "facebook/react"
}
]
}
```
## 🚀 Usage
### Local Development
```bash
# Install dependencies
bun install
# Run collection
bun run collect
# Run collection and update README
bun run collect:readme
```
### GitHub Actions
The workflow in `.github/workflows/stats.yml` runs every Monday and:
1. Runs the collection script
2. Updates the README with new statistics
3. Commits and pushes the changes
## 📈 Adding New Platforms
To add a new platform:
1. Create a new collector in `collectors/`:
```typescript
export const collectNewPlatformMetrics: MetricCollector = {
async collect(source: string): Promise<MetricResult> {
// Implementation
}
};
```
2. Register it in `core/registry.ts`:
```typescript
import { collectNewPlatformMetrics } from '../collectors/newplatform';
export const collectors = {
// ... existing collectors
newplatform: { collect: collectNewPlatformMetrics, batched: false }
};
```
3. Add sources to `config/sources.json`
## 🔄 Output Files
The system generates several output files in the `output/` directory:
- `latest.json` - Complete collection results
- `results.json` - Just the metrics array
- `summary.md` - Human-readable summary
- `backup-{timestamp}.json` - Timestamped backups
## 🎯 Benefits of the New Architecture
1. **Simplicity**: Each component has a single responsibility
2. **Reliability**: Graceful error handling and retries
3. **Extensibility**: Easy to add new platforms
4. **Maintainability**: Clear separation of concerns
5. **Testability**: Pure functions with clear interfaces
## 🔧 Environment Variables
- `GITHUB_TOKEN`: For GitHub API access (optional)
- `GITHUB_ACTIONS`: Set to 'true' in GitHub Actions context
## 📝 README Integration
The system looks for these markers in the README:
```markdown
<!-- {{UsageStats}} -->
[This section will be auto-updated]
<!-- {{endUsageStats}} -->
```
Everything between these markers will be replaced with the generated statistics table.

107
CLAUDE.md Normal file
View File

@@ -0,0 +1,107 @@
---
Default to using Bun instead of Node.js.
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
- Use `bun test` instead of `jest` or `vitest`
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
- Bun automatically loads .env, so don't use dotenv.
## APIs
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
- `Bun.redis` for Redis. Don't use `ioredis`.
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
- `WebSocket` is built-in. Don't use `ws`.
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
- Bun.$`ls` instead of execa.
## Testing
Use `bun test` to run tests.
```ts#index.test.ts
import { test, expect } from "bun:test";
test("hello world", () => {
expect(1).toBe(1);
});
```
## Frontend
Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
Server:
```ts#index.ts
import index from "./index.html"
Bun.serve({
routes: {
"/": index,
"/api/users/:id": {
GET: (req) => {
return new Response(JSON.stringify({ id: req.params.id }));
},
},
},
// optional websocket support
websocket: {
open: (ws) => {
ws.send("Hello, world!");
},
message: (ws, message) => {
ws.send(message);
},
close: (ws) => {
// handle close
}
},
development: {
hmr: true,
console: true,
}
})
```
HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
```html#index.html
<html>
<body>
<h1>Hello, world!</h1>
<script type="module" src="./frontend.tsx"></script>
</body>
</html>
```
With the following `frontend.tsx`:
```tsx#frontend.tsx
import React from "react";
// import .css files directly and it works
import './index.css';
import { createRoot } from "react-dom/client";
const root = createRoot(document.body);
export default function Frontend() {
return <h1>Hello, world!</h1>;
}
root.render(<Frontend />);
```
Then, run index.ts
```sh
bun --hot ./index.ts
```
For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`.

View File

@@ -1,107 +0,0 @@
# Quick Installation Guide
## 🚀 Install the Usage Statistics Tracker
### Basic Installation
Add this to your GitHub Actions workflow:
```yaml
- name: Usage Statistics Tracker
uses: LukeHagar/usage-statistics@v1
with:
npm-packages: 'lodash,axios'
github-repositories: 'microsoft/vscode,facebook/react'
pypi-packages: 'requests,numpy'
homebrew-formulas: 'git,node'
powershell-modules: 'PowerShellGet,PSReadLine'
postman-collections: '12345,67890'
go-modules: 'github.com/gin-gonic/gin,github.com/go-chi/chi'
json-output-path: 'stats.json'
update-readme: 'true'
github-token: ${{ secrets.GITHUB_TOKEN }}
```
### Complete Example Workflow
```yaml
name: Update Usage Statistics
on:
schedule:
- cron: '0 0 * * *' # Daily at midnight
workflow_dispatch:
jobs:
update-stats:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Usage Statistics Tracker
uses: LukeHagar/usage-statistics@v1
with:
npm-packages: 'lodash,axios'
github-repositories: 'microsoft/vscode,facebook/react'
pypi-packages: 'requests,numpy'
homebrew-formulas: 'git,node'
powershell-modules: 'PowerShellGet,PSReadLine'
postman-collections: '12345,67890'
go-modules: 'github.com/gin-gonic/gin,github.com/go-chi/chi'
json-output-path: 'stats.json'
csv-output-path: 'stats.csv'
report-output-path: 'docs/usage-report.md'
update-readme: 'true'
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Commit and push changes
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add stats.json stats.csv docs/usage-report.md README.md
git commit -m "chore: update usage statistics [skip ci]" || echo "No changes to commit"
git push
```
### README Integration
Add these markers to your README.md for automatic updates:
```markdown
<!-- USAGE_STATS_START -->
## 📊 Usage Statistics
Last updated: 2025-07-29T18:53:52.619Z
### Summary
- **Total Downloads**: 414,533
- **Unique Packages**: 8
- **Platforms Tracked**: npm, pypi, homebrew, go
### Platform Totals
- **HOMEBREW**: 380,163 downloads (2 packages)
- **NPM**: 34,311 downloads (2 packages)
- **GO**: 33 downloads (2 packages)
### Top Packages
1. **node** (homebrew) - 226,882 downloads
2. **git** (homebrew) - 153,281 downloads
3. **axios** (npm) - 18,397 downloads
4. **lodash** (npm) - 15,914 downloads
5. **github.com/go-chi/chi** (go) - 33 downloads
<!-- USAGE_STATS_END -->
```
## 📚 Documentation
- **Full Documentation**: [README.md](README.md)
- **Examples**: [examples/basic-usage.yml](examples/basic-usage.yml)
- **Repository**: [https://github.com/LukeHagar/usage-statistics](https://github.com/LukeHagar/usage-statistics)
## 🤝 Support
- **Issues**: [GitHub Issues](https://github.com/LukeHagar/usage-statistics/issues)
- **Discussions**: [GitHub Discussions](https://github.com/LukeHagar/usage-statistics/discussions)
- **Documentation**: [README.md](README.md)

View File

@@ -1,260 +0,0 @@
# Publishing to GitHub Marketplace
This guide walks you through the process of publishing the Usage Statistics Tracker to the GitHub Marketplace.
## Prerequisites
1. **GitHub Account**: You need a GitHub account with a verified email
2. **Repository**: This repository should be public
3. **GitHub Actions**: Actions must be enabled on your repository
## Step 1: Prepare Your Repository
### 1.1 Build the Action
```bash
# Install dependencies
bun install
# Build the action for distribution
bun run action:build
# Verify the build
ls -la dist/
```
### 1.2 Commit the Built Files
```bash
# Add the built files
git add dist/
# Commit with a descriptive message
git commit -m "build: add action distribution files for v1.0.0"
# Push to main branch
git push origin main
```
### 1.3 Create a Release
```bash
# Create and push a tag
git tag v1.0.0
git push origin v1.0.0
# Or create a release via GitHub UI:
# 1. Go to your repository
# 2. Click "Releases" on the right
# 3. Click "Create a new release"
# 4. Choose the tag v1.0.0
# 5. Add release notes
# 6. Publish release
```
## Step 2: Publish to Marketplace
### 2.1 Access the Publishing Interface
1. Go to your repository on GitHub
2. Click on the **Actions** tab
3. Look for a banner that says "Publish this Action to the GitHub Marketplace"
4. Click **Publish this Action**
### 2.2 Fill in Action Details
#### Basic Information
- **Action name**: `usage-statistics-tracker`
- **Description**: `Track download statistics across multiple platforms (NPM, GitHub, PyPI, Homebrew, PowerShell, Postman, Go)`
- **Repository**: `LukeHagar/usage-statistics`
- **Category**: Choose `Data` or `Utilities`
- **Icon**: Upload a relevant icon (512x512px PNG recommended)
- **Color**: Choose a brand color (e.g., `#0366d6` for blue)
#### Detailed Description
Use the content from the main README.md file, focusing on:
- Features and capabilities
- Usage examples
- Configuration options
- Supported platforms
#### Keywords
Add relevant keywords:
- `statistics`
- `analytics`
- `downloads`
- `npm`
- `github`
- `pypi`
- `homebrew`
- `powershell`
- `postman`
- `go`
- `tracking`
- `usage`
### 2.3 Marketplace Listing
#### Action Name
- **Marketplace name**: `Usage Statistics Tracker`
- **Description**: `Comprehensive GitHub Action for tracking download statistics across multiple platforms with configurable outputs and README integration`
- **Repository**: `LukeHagar/usage-statistics`
#### Categories
- **Primary category**: `Data`
- **Secondary category**: `Utilities`
#### Pricing
- **Pricing model**: Free
- **License**: MIT
## Step 3: Version Management
### 3.1 Semantic Versioning
Follow semantic versioning for releases:
- **Major** (1.0.0): Breaking changes
- **Minor** (1.1.0): New features, backward compatible
- **Patch** (1.0.1): Bug fixes
### 3.2 Release Process
For each new version:
```bash
# 1. Update version in package.json
# 2. Update CHANGELOG.md
# 3. Build the action
bun run action:build
# 4. Commit changes
git add .
git commit -m "feat: release v1.1.0"
# 5. Create and push tag
git tag v1.1.0
git push origin v1.1.0
# 6. Create GitHub release
# Go to GitHub and create a release for the new tag
```
### 3.3 Changelog
Maintain a `CHANGELOG.md` file:
```markdown
# Changelog
## [1.1.0] - 2025-01-XX
### Added
- New platform support for X
- Enhanced error handling
- Additional configuration options
### Changed
- Improved performance for large datasets
- Updated dependencies
### Fixed
- Bug fix for Y platform
- Resolved issue with Z feature
## [1.0.0] - 2025-01-XX
### Added
- Initial release
- Support for NPM, GitHub, PyPI, Homebrew, PowerShell, Postman, Go
- Configurable outputs (JSON, CSV, human-readable)
- README integration
- Preview mode
```
## Step 4: Marketing and Documentation
### 4.1 README Optimization
Ensure your README includes:
- Clear installation instructions
- Multiple usage examples
- Configuration documentation
- Troubleshooting section
- Contributing guidelines
### 4.2 Examples Repository
Consider creating a separate repository with examples:
- Basic usage workflows
- Advanced configurations
- Custom integrations
- Troubleshooting guides
### 4.3 Social Media
Promote your action on:
- GitHub Discussions
- Reddit (r/github, r/devops)
- Twitter/X with relevant hashtags
- LinkedIn for professional audience
## Step 5: Maintenance
### 5.1 Monitoring
- Monitor GitHub Issues for user feedback
- Track download statistics
- Respond to questions and bug reports
- Update documentation as needed
### 5.2 Updates
Regular maintenance tasks:
- Update dependencies
- Fix security vulnerabilities
- Add new platform support
- Improve performance
- Enhance documentation
### 5.3 Community Engagement
- Respond to issues promptly
- Help users with configuration
- Accept and review pull requests
- Maintain a welcoming community
## Troubleshooting
### Common Issues
1. **Action not found**: Ensure the action is properly built and tagged
2. **Build failures**: Check that all dependencies are included
3. **Permission issues**: Verify GitHub token permissions
4. **Rate limiting**: Implement proper rate limiting in the action
### Support
- GitHub Issues: For bug reports and feature requests
- GitHub Discussions: For questions and community support
- Documentation: Comprehensive README and examples
## Success Metrics
Track these metrics to measure success:
- **Downloads**: Number of action downloads
- **Stars**: Repository stars
- **Forks**: Repository forks
- **Issues**: User engagement and feedback
- **Usage**: Number of repositories using the action
## Legal Considerations
- **License**: MIT License (included)
- **Privacy**: No personal data collection
- **Terms of Service**: Follow GitHub's terms
- **Attribution**: Credit original authors if applicable
## Resources
- [GitHub Actions Documentation](https://docs.github.com/en/actions)
- [GitHub Marketplace Guidelines](https://docs.github.com/en/developers/github-marketplace)
- [Action Metadata Syntax](https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions)
- [Publishing Actions](https://docs.github.com/en/actions/creating-actions/publishing-actions-in-github-marketplace)

155
README.md
View File

@@ -1,19 +1,99 @@
# Usage Statistics Tracker
A comprehensive GitHub Action for tracking download statistics across multiple platforms (NPM, GitHub, PyPI, Homebrew, PowerShell, Postman, Go) with configurable outputs and README integration.
A comprehensive GitHub Action for tracking download statistics across multiple platforms (NPM, GitHub, PyPI, Homebrew, PowerShell) with configurable outputs and README integration.
## 🚀 Features
- 📊 **Multi-Platform Tracking**: NPM, GitHub, PyPI, Homebrew, PowerShell, Postman, Go
- 📊 **Multi-Platform Tracking**: NPM, GitHub, PyPI, Homebrew, PowerShell
- 🎭 **Preview Mode**: Test with mock data without external API calls
- 📄 **Flexible Outputs**: JSON, CSV, and human-readable reports
- 📝 **README Integration**: Auto-update README with statistics
- ⚙️ **Configurable**: Custom configurations via JSON or preset modes
- 🔄 **GitHub Actions Ready**: Built for CI/CD workflows
- 🧪 **Comprehensive Testing**: Full test suite with Bun
- 🐍 **Enhanced PyPI Integration**: Uses PyPI Stats API for comprehensive download statistics
- 📦 **Enhanced NPM Integration**: Bundle size analysis and dependency metrics
- 🐙 **Enhanced GitHub Integration**: Traffic insights and release downloads
- 💻 **Enhanced PowerShell Integration**: Module analytics and function counts
- 🔧 **Enhanced Go Integration**: Version analysis and GitHub integration
## 📦 Enhanced Platform Integrations
### 🐍 PyPI Statistics
Uses an external PyPI Stats API (via BigQuery replication) for comprehensive download statistics:
- **Download Metrics**: Monthly, weekly, daily download counts
- **Python Version Breakdown**: Downloads by Python version adoption
- **Platform Analysis**: Downloads by OS (Windows, Linux, macOS)
- **Trend Analysis**: Growth rates and time series data
- **API Integration**: Serves precomputed and on-demand results from a BigQuery-backed service
### 📦 NPM Statistics
Enhanced with bundle analysis and dependency metrics:
- **Download Statistics**: Daily, weekly, monthly, and yearly downloads
- **Bundle Analysis**: Bundle size, gzip size, dependency count
- **Dependency Metrics**: Total dependencies, dev dependencies, peer dependencies
- **Package Analytics**: Version count, package age, maintainer count
### 🐙 GitHub Statistics
Comprehensive repository analytics with traffic insights:
- **Repository Metrics**: Stars, forks, watchers, open issues
- **Traffic Analytics**: Views, unique visitors, clone statistics
- **Release Downloads**: Total and latest release download counts
- **Activity Metrics**: Repository age, last activity, release count
### 💻 PowerShell Statistics
Enhanced module analytics with detailed download tracking:
- **Download Metrics**: Total downloads across all versions with version-by-version breakdown
- **Version Analysis**: Latest version downloads, version count, release dates
- **Combined Charts**: Multi-module charts with different colors for each module
- **Time Series Data**: Downloads over time and cumulative download trends
- **Top Versions**: Bar charts showing top performing versions across all modules
- **Metadata**: Author, company, tags, package size, PowerShell version requirements
<!-- Go module tracking removed -->
## 📦 Installation
### PyPI Statistics Source
The PyPI collector now uses an external PyPI Stats API instead of querying BigQuery directly.
#### Option 1: Service Account (Recommended for GitHub Actions)
1. **Create a Google Cloud Project** (if you don't have one)
2. **Enable the BigQuery API** in your Google Cloud Console
3. **Create a Service Account**:
- Go to IAM & Admin > Service Accounts
- Click "Create Service Account"
- Give it a name like "pypi-stats-collector"
- Grant "BigQuery User" role
4. **Create and download a JSON key**:
- Click on your service account
- Go to "Keys" tab
- Click "Add Key" > "Create new key" > "JSON"
- Download the JSON file
5. **Add the service account key as a GitHub secret**:
- In your GitHub repository, go to Settings > Secrets and variables > Actions
- Create a new secret named `GOOGLE_CLOUD_CREDENTIALS`
- Paste the entire contents of the downloaded JSON file
#### Option 2: Application Default Credentials (Local Development)
For local development, you can use Application Default Credentials:
```bash
# Install Google Cloud CLI
curl https://sdk.cloud.google.com | bash
exec -l $SHELL
# Authenticate
gcloud auth application-default login
```
#### Environment Variables
- `PYPI_STATS_BASE_URL` (optional): Base URL for the PyPI Stats API. Default is `https://pypistats.dev`.
### As a GitHub Action
```yaml
@@ -26,12 +106,14 @@ A comprehensive GitHub Action for tracking download statistics across multiple p
homebrew-formulas: 'git,node'
powershell-modules: 'PowerShellGet,PSReadLine'
postman-collections: '12345,67890'
go-modules: 'github.com/gin-gonic/gin,github.com/go-chi/chi'
# go-modules removed
json-output-path: 'stats.json'
csv-output-path: 'stats.csv'
report-output-path: 'report.md'
update-readme: 'true'
github-token: ${{ secrets.GITHUB_TOKEN }}
# env:
# PYPI_STATS_BASE_URL: https://your-host
```
### Local Development
@@ -50,6 +132,51 @@ bun preview
bun test
```
<!-- {{UsageStats}} -->
## 📊 Usage Statistics
Last updated: 2025-07-31T16:09:10.951Z
**Summary:**
- **Total Sources**: 26
- **Platforms**: npm, github, pypi, powershell, go
- **Total Monthly Downloads**: 4640.4M
- **Total Stars**: 1103.1K
- **Total Forks**: 234.0K
## 📦 Package Statistics
| Platform | Name | Downloads (Monthly) | Downloads (Total) | Stars | Forks | Enhanced Metrics |
|---|---|---|---|---|---|---|
| NPM | express | 196.7M | 1884.3M | — | — | Bundle: 568.4KB, Age: 5327 days, Versions: 283 |
| NPM | react | 179.1M | 1632.6M | — | — | Bundle: 7.4KB, Age: 5026 days, Versions: 2423 |
| NPM | lodash | 347.7M | 3194.1M | — | — | Bundle: 69.8KB, Age: 4846 days, Versions: 114 |
| NPM | axios | 286.2M | 2968.9M | — | — | Bundle: 36.0KB, Age: 3988 days, Versions: 116 |
| NPM | moment | 108.3M | 1154.0M | — | — | Bundle: 294.9KB, Age: 5035 days, Versions: 76 |
| NPM | vue | 28.8M | 304.2M | — | — | Bundle: 126.0KB, Age: 4254 days, Versions: 538 |
| GitHub | facebook/react | — | — | 237.7K | 49.0K | Watchers: 237.7K, Releases: 30 |
| GitHub | microsoft/vscode | — | — | 175.2K | 34.1K | Watchers: 175.2K, Releases: 30 |
| GitHub | vercel/next.js | — | — | 133.5K | 29.0K | Watchers: 133.5K, Releases: 30 |
| GitHub | vuejs/vue | — | — | 209.2K | 33.7K | Watchers: 209.2K, Releases: 30 |
| GitHub | tensorflow/tensorflow | — | — | 191.0K | 74.8K | Watchers: 191.0K, Releases: 30 |
| PyPI | requests | 1423.9M | 716.0M | — | — | Python breakdown, Platform breakdown |
| PyPI | numpy | 899.7M | 451.0M | — | — | Python breakdown, Platform breakdown |
| PyPI | django | 48.9M | 24.5M | — | — | Python breakdown, Platform breakdown |
| PyPI | flask | 226.5M | 113.2M | — | — | Python breakdown, Platform breakdown |
| PyPI | pandas | 709.0M | 356.4M | — | — | Python breakdown, Platform breakdown |
| PyPI | matplotlib | 185.3M | 92.8M | — | — | Python breakdown, Platform breakdown |
| PowerShell | PowerShellGet | — | — | — | — | Versions: 81 |
| PowerShell | PSReadLine | — | — | — | — | Versions: 46 |
| PowerShell | Pester | — | — | — | — | Versions: 100 |
| PowerShell | PSScriptAnalyzer | — | — | — | — | Versions: 37 |
| PowerShell | dbatools | — | — | — | — | Versions: 100 |
<!-- Go rows removed -->
<!-- {{endUsageStats}} -->
## 🔧 Configuration
### Input Parameters
@@ -62,7 +189,7 @@ bun test
| `homebrew-formulas` | Comma-separated list of Homebrew formulas | No | (empty) |
| `powershell-modules` | Comma-separated list of PowerShell modules | No | (empty) |
| `postman-collections` | Comma-separated list of Postman collection IDs | No | (empty) |
| `go-modules` | Comma-separated list of Go modules | No | (empty) |
<!-- go-modules input removed -->
| `json-output-path` | Path for JSON output | No | `stats.json` |
| `csv-output-path` | Path for CSV output | No | (empty) |
| `report-output-path` | Path for human-readable report | No | (empty) |
@@ -102,7 +229,9 @@ bun test
homebrew-formulas: 'git,node'
powershell-modules: 'PowerShellGet,PSReadLine'
postman-collections: '12345,67890'
go-modules: 'github.com/gin-gonic/gin,github.com/go-chi/chi'
# go-modules removed
# env:
# PYPI_STATS_BASE_URL: https://your-host
```
### Outputs
@@ -145,10 +274,12 @@ jobs:
homebrew-formulas: 'git,node'
powershell-modules: 'PowerShellGet,PSReadLine'
postman-collections: '12345,67890'
go-modules: 'github.com/gin-gonic/gin,github.com/go-chi/chi'
# go-modules removed
json-output-path: 'stats.json'
update-readme: 'true'
github-token: ${{ secrets.GITHUB_TOKEN }}
env:
GOOGLE_CLOUD_CREDENTIALS: ${{ secrets.GOOGLE_CLOUD_CREDENTIALS }}
- name: Commit and push changes
run: |
@@ -171,7 +302,7 @@ jobs:
homebrew-formulas: 'git,node'
powershell-modules: 'PowerShellGet,PSReadLine'
postman-collections: '12345,67890'
go-modules: 'github.com/gin-gonic/gin,github.com/go-chi/chi'
# go-modules removed
json-output-path: 'data/stats.json'
csv-output-path: 'data/stats.csv'
report-output-path: 'docs/usage-report.md'
@@ -180,6 +311,8 @@ jobs:
github-token: ${{ secrets.GITHUB_TOKEN }}
postman-api-key: ${{ secrets.POSTMAN_API_KEY }}
commit-message: 'feat: update usage statistics with detailed report'
env:
GOOGLE_CLOUD_CREDENTIALS: ${{ secrets.GOOGLE_CLOUD_CREDENTIALS }}
```
### Preview Mode for Testing
@@ -206,7 +339,7 @@ jobs:
github-repositories: 'microsoft/vscode,facebook/react'
powershell-modules: 'PowerShellGet'
postman-collections: '12345'
go-modules: 'github.com/gin-gonic/gin'
# go-modules removed
json-output-path: 'stats.json'
- name: Use Statistics Data
@@ -222,7 +355,7 @@ jobs:
To enable automatic README updates, add these markers to your README.md:
```markdown
<!-- USAGE_STATS_START -->
<!-- METRICS_START -->
## 📊 Usage Statistics
Last updated: 2025-07-29T18:53:52.619Z
@@ -243,7 +376,7 @@ Last updated: 2025-07-29T18:53:52.619Z
3. **axios** (npm) - 18,397 downloads
4. **lodash** (npm) - 15,914 downloads
5. **github.com/go-chi/chi** (go) - 33 downloads
<!-- USAGE_STATS_END -->
<!-- METRICS_END -->
```
## 🔧 Development
@@ -346,7 +479,7 @@ git push origin v1.1.0
- **Homebrew**: Formula installation statistics
- **PowerShell**: Module download statistics
- **Postman**: Collection fork/download statistics
- **Go**: Module proxy statistics
<!-- Go platform removed -->
## 🤝 Contributing

134
Test-Readme.md Normal file
View File

@@ -0,0 +1,134 @@
<!-- METRICS_START -->
# Usage Statistics
Last updated: 8/14/2025, 9:11:29 PM
Below are stats from artifacts tracked across NPM, GitHub, PyPI and PowerShell.
### NPM (JavaScript/TypeScript):
| Package | Downloads | Monthly Downloads | Weekly Downloads | Daily Downloads |
| --- | --- | --- | --- | --- |
| sailpoint-api-client | 16,740 | 1,308 | 272 | 39 |
| **Total** | **16,740** | **1,308** | **272** | **39** | | | | |
![./charts/npm/sailpoint-api-client-new-downloads-by-month.svg](./charts/npm/sailpoint-api-client-new-downloads-by-month.svg)
![./charts/npm/sailpoint-api-client-cumulative-downloads.svg](./charts/npm/sailpoint-api-client-cumulative-downloads.svg)
### GitHub:
| Repository | Stars | Forks | Watchers | Open Issues | Closed Issues | Total Issues | Release Downloads | Releases | Latest Release | Language |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| sailpoint-oss/sailpoint-cli | 35 | 24 | 9 | 5 | 35 | 40 | 10,013 | 31 | 2.2.5 | Go |
| **Total** | **35** | **24** | **9** | **5** | **35** | **40** | **10,013** | **31** | | |
#### Repository Details:
**sailpoint-oss/sailpoint-cli**:
- Last Activity: 34 days ago
- Repository Age: 1,120 days
- Release Count: 31
- Total Release Downloads: 10,013
- Latest Release: 2.2.5
- Latest Release Downloads: 746
- Views: 484
- Unique Visitors: 160
- Clones: 18
![./charts/github/sailpoint-oss-sailpoint-cli-release-downloads.svg](./charts/github/sailpoint-oss-sailpoint-cli-release-downloads.svg)
![./charts/github/sailpoint-oss-sailpoint-cli-cumulative-release-downloads.svg](./charts/github/sailpoint-oss-sailpoint-cli-cumulative-release-downloads.svg)
![./charts/github/sailpoint-oss-sailpoint-cli-top-release-downloads.svg](./charts/github/sailpoint-oss-sailpoint-cli-top-release-downloads.svg)
### PyPI (Python):
| Package | Total Downloads | Monthly Downloads | Weekly Downloads | Daily Downloads | Version |
| --- | --- | --- | --- | --- | --- |
| sailpoint | 21,614 | 10,108 | 2,894 | 468 | 1.3.8 |
| **Total** | **21,614** | **10,108** | **2,894** | **468** | | |
#### Package Details:
**sailpoint**:
- Version: 1.3.8
- Released: 2025-07-29
- Popular system: Linux
- Popular installer: pip
- Releases: 29
- OS Usage Breakdown
- other: 1782
- Darwin: 67
- Windows: 87
- Linux: 9570
- Python Version Breakdown
- python2: 1
- python3: 9690
![./charts/pypi/sailpoint-pypi-overall.svg](./charts/pypi/sailpoint-pypi-overall.svg)
![./charts/pypi/sailpoint-pypi-python-major.svg](./charts/pypi/sailpoint-pypi-python-major.svg)
![./charts/pypi/sailpoint-pypi-python-minor.svg](./charts/pypi/sailpoint-pypi-python-minor.svg)
![./charts/pypi/sailpoint-pypi-installer.svg](./charts/pypi/sailpoint-pypi-installer.svg)
![./charts/pypi/sailpoint-pypi-system.svg](./charts/pypi/sailpoint-pypi-system.svg)
### PowerShell:
| Module | Total Downloads | Latest Version | Version Downloads | Versions | Last Updated |
| --- | --- | --- | --- | --- | --- |
| PSSailPoint | 20,447 | 1.6.6 | 111 | 32 | 8/14/2025 |
| PSSailpoint.V3 | 11,721 | 1.6.6 | 111 | 19 | 8/14/2025 |
| PSSailpoint.Beta | 12,049 | 1.6.6 | 110 | 19 | 8/14/2025 |
| PSSailpoint.V2024 | 11,716 | 1.6.6 | 106 | 19 | 8/14/2025 |
| PSSailpoint.V2025 | 1,009 | 1.6.6 | 102 | 8 | 8/14/2025 |
| **Total** | **56,942** | | | **97** | |
#### PowerShell Module Details:
**PSSailPoint**:
- Total Downloads: 20,447
- Latest Version: 1.6.6
- Latest Version Downloads: 111
- Version Count: 32
- Last Updated: 8/14/2025
- Package Size: 13618 KB
**PSSailpoint.V3**:
- Total Downloads: 11,721
- Latest Version: 1.6.6
- Latest Version Downloads: 111
- Version Count: 19
- Last Updated: 8/14/2025
- Package Size: 1023 KB
**PSSailpoint.Beta**:
- Total Downloads: 12,049
- Latest Version: 1.6.6
- Latest Version Downloads: 110
- Version Count: 19
- Last Updated: 8/14/2025
- Package Size: 1526 KB
**PSSailpoint.V2024**:
- Total Downloads: 11,716
- Latest Version: 1.6.6
- Latest Version Downloads: 106
- Version Count: 19
- Last Updated: 8/14/2025
- Package Size: 1881 KB
**PSSailpoint.V2025**:
- Total Downloads: 1,009
- Latest Version: 1.6.6
- Latest Version Downloads: 102
- Version Count: 8
- Last Updated: 8/14/2025
- Package Size: 1923 KB
![./charts/powershell/powershell-combined-downloads.svg](./charts/powershell/powershell-combined-downloads.svg)
![./charts/powershell/powershell-cumulative-downloads.svg](./charts/powershell/powershell-cumulative-downloads.svg)
<!-- METRICS_END -->

View File

@@ -1,5 +1,5 @@
name: 'Usage Statistics Tracker'
description: 'Track download statistics across multiple platforms (NPM, GitHub, PyPI, Homebrew, PowerShell, Postman, Go)'
description: 'Track download statistics across multiple platforms (NPM, GitHub, PyPI, PowerShell)'
author: 'LukeHagar'
inputs:
@@ -21,45 +21,23 @@ inputs:
required: false
default: ''
# Homebrew Configuration
homebrew-formulas:
description: 'Comma-separated list of Homebrew formulas to track'
required: false
default: ''
# PowerShell Configuration
powershell-modules:
description: 'Comma-separated list of PowerShell modules to track'
required: false
default: ''
# Postman Configuration
postman-collections:
description: 'Comma-separated list of Postman collection IDs to track'
required: false
default: ''
# Go Configuration
go-modules:
description: 'Comma-separated list of Go modules to track'
required: false
default: ''
# Go tracking removed
# Output paths
json-output-path:
description: 'Path for JSON output file'
required: false
default: 'stats.json'
csv-output-path:
description: 'Path for CSV output file'
readme-path:
description: 'Path to README file to update'
required: false
default: ''
report-output-path:
description: 'Path for human-readable report file'
required: false
default: ''
default: 'README.md'
# README update
update-readme:
@@ -67,50 +45,22 @@ inputs:
required: false
default: 'true'
readme-path:
description: 'Path to README file to update'
required: false
default: 'README.md'
# GitHub integration
github-token:
description: 'GitHub token for API access and commits'
required: false
default: '${{ github.token }}'
# Postman integration
postman-api-key:
description: 'Postman API key for collection statistics'
required: false
# Commit settings
commit-message:
description: 'Commit message for changes'
required: false
default: 'chore: update usage statistics [skip ci]'
# Preview mode
preview-mode:
description: 'Run in preview mode with mock data (no external API calls)'
required: false
default: 'false'
default: 'chore: update usage statistics'
outputs:
json-output:
description: 'Path to the generated JSON file'
csv-output:
description: 'Path to the generated CSV file'
report-output:
description: 'Path to the generated report file'
total-downloads:
description: 'Total downloads across all platforms'
unique-packages:
description: 'Number of unique packages tracked'
platforms-tracked:
description: 'Comma-separated list of platforms tracked'

453
bun.lock
View File

@@ -5,11 +5,20 @@
"name": "usage-statistics",
"dependencies": {
"@actions/core": "1.11.1",
"@actions/github": "6.0.1",
"@google-cloud/bigquery": "^7.0.0",
"@octokit/graphql": "^7.0.0",
"@octokit/plugin-retry": "^7.0.0",
"@octokit/plugin-throttling": "^7.0.0",
"@octokit/rest": "22.0.0",
"chart.js": "^4.5.0",
"chartjs-adapter-moment": "1.0.1",
"fast-xml-parser": "5.2.5",
"moment": "2.30.1",
"skia-canvas": "^2.0.2",
},
"devDependencies": {
"@types/bun": "latest",
"@types/node": "^20.0.0",
"bun-types": "1.2.19",
"typescript": "^5.0.0",
@@ -21,54 +30,336 @@
"@actions/exec": ["@actions/exec@1.1.1", "", { "dependencies": { "@actions/io": "^1.0.1" } }, "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w=="],
"@actions/github": ["@actions/github@6.0.1", "", { "dependencies": { "@actions/http-client": "^2.2.0", "@octokit/core": "^5.0.1", "@octokit/plugin-paginate-rest": "^9.2.2", "@octokit/plugin-rest-endpoint-methods": "^10.4.0", "@octokit/request": "^8.4.1", "@octokit/request-error": "^5.1.1", "undici": "^5.28.5" } }, "sha512-xbZVcaqD4XnQAe35qSQqskb3SqIAfRyLBrHMd/8TuL7hJSz2QtbDwnNM8zWx4zO5l2fnGtseNE3MbEvD7BxVMw=="],
"@actions/http-client": ["@actions/http-client@2.2.3", "", { "dependencies": { "tunnel": "^0.0.6", "undici": "^5.25.4" } }, "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA=="],
"@actions/io": ["@actions/io@1.1.3", "", {}, "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="],
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
"@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="],
"@google-cloud/bigquery": ["@google-cloud/bigquery@7.9.4", "", { "dependencies": { "@google-cloud/common": "^5.0.0", "@google-cloud/paginator": "^5.0.2", "@google-cloud/precise-date": "^4.0.0", "@google-cloud/promisify": "4.0.0", "arrify": "^2.0.1", "big.js": "^6.0.0", "duplexify": "^4.0.0", "extend": "^3.0.2", "is": "^3.3.0", "stream-events": "^1.0.5", "uuid": "^9.0.0" } }, "sha512-C7jeI+9lnCDYK3cRDujcBsPgiwshWKn/f0BiaJmClplfyosCLfWE83iGQ0eKH113UZzjR9c9q7aZQg0nU388sw=="],
"@octokit/core": ["@octokit/core@7.0.3", "", { "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.1", "@octokit/request": "^10.0.2", "@octokit/request-error": "^7.0.0", "@octokit/types": "^14.0.0", "before-after-hook": "^4.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-oNXsh2ywth5aowwIa7RKtawnkdH6LgU1ztfP9AIUCQCvzysB+WeU8o2kyyosDPwBZutPpjZDKPQGIzzrfTWweQ=="],
"@google-cloud/common": ["@google-cloud/common@5.0.2", "", { "dependencies": { "@google-cloud/projectify": "^4.0.0", "@google-cloud/promisify": "^4.0.0", "arrify": "^2.0.1", "duplexify": "^4.1.1", "extend": "^3.0.2", "google-auth-library": "^9.0.0", "html-entities": "^2.5.2", "retry-request": "^7.0.0", "teeny-request": "^9.0.0" } }, "sha512-V7bmBKYQyu0eVG2BFejuUjlBt+zrya6vtsKdY+JxMM/dNntPF41vZ9+LhOshEUH01zOHEqBSvI7Dad7ZS6aUeA=="],
"@octokit/endpoint": ["@octokit/endpoint@11.0.0", "", { "dependencies": { "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ=="],
"@google-cloud/paginator": ["@google-cloud/paginator@5.0.2", "", { "dependencies": { "arrify": "^2.0.0", "extend": "^3.0.2" } }, "sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg=="],
"@octokit/graphql": ["@octokit/graphql@9.0.1", "", { "dependencies": { "@octokit/request": "^10.0.2", "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-j1nQNU1ZxNFx2ZtKmL4sMrs4egy5h65OMDmSbVyuCzjOcwsHq6EaYjOTGXPQxgfiN8dJ4CriYHk6zF050WEULg=="],
"@google-cloud/precise-date": ["@google-cloud/precise-date@4.0.0", "", {}, "sha512-1TUx3KdaU3cN7nfCdNf+UVqA/PSX29Cjcox3fZZBtINlRrXVTmUkQnCKv2MbBUbCopbK4olAT1IHl76uZyCiVA=="],
"@octokit/openapi-types": ["@octokit/openapi-types@25.1.0", "", {}, "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA=="],
"@google-cloud/projectify": ["@google-cloud/projectify@4.0.0", "", {}, "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA=="],
"@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@13.1.1", "", { "dependencies": { "@octokit/types": "^14.1.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-q9iQGlZlxAVNRN2jDNskJW/Cafy7/XE52wjZ5TTvyhyOD904Cvx//DNyoO3J/MXJ0ve3rPoNWKEg5iZrisQSuw=="],
"@google-cloud/promisify": ["@google-cloud/promisify@4.0.0", "", {}, "sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g=="],
"@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="],
"@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="],
"@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
"@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="],
"@mapbox/node-pre-gyp": ["@mapbox/node-pre-gyp@1.0.11", "", { "dependencies": { "detect-libc": "^2.0.0", "https-proxy-agent": "^5.0.0", "make-dir": "^3.1.0", "node-fetch": "^2.6.7", "nopt": "^5.0.0", "npmlog": "^5.0.1", "rimraf": "^3.0.2", "semver": "^7.3.5", "tar": "^6.1.11" }, "bin": { "node-pre-gyp": "bin/node-pre-gyp" } }, "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ=="],
"@octokit/auth-token": ["@octokit/auth-token@4.0.0", "", {}, "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA=="],
"@octokit/core": ["@octokit/core@5.2.2", "", { "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", "@octokit/request": "^8.4.1", "@octokit/request-error": "^5.1.1", "@octokit/types": "^13.0.0", "before-after-hook": "^2.2.0", "universal-user-agent": "^6.0.0" } }, "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg=="],
"@octokit/endpoint": ["@octokit/endpoint@9.0.6", "", { "dependencies": { "@octokit/types": "^13.1.0", "universal-user-agent": "^6.0.0" } }, "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw=="],
"@octokit/graphql": ["@octokit/graphql@7.1.1", "", { "dependencies": { "@octokit/request": "^8.4.1", "@octokit/types": "^13.0.0", "universal-user-agent": "^6.0.0" } }, "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g=="],
"@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="],
"@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@9.2.2", "", { "dependencies": { "@octokit/types": "^12.6.0" }, "peerDependencies": { "@octokit/core": "5" } }, "sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ=="],
"@octokit/plugin-request-log": ["@octokit/plugin-request-log@6.0.0", "", { "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q=="],
"@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@16.0.0", "", { "dependencies": { "@octokit/types": "^14.1.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-kJVUQk6/dx/gRNLWUnAWKFs1kVPn5O5CYZyssyEoNYaFedqZxsfYs7DwI3d67hGz4qOwaJ1dpm07hOAD1BXx6g=="],
"@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@10.4.1", "", { "dependencies": { "@octokit/types": "^12.6.0" }, "peerDependencies": { "@octokit/core": "5" } }, "sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg=="],
"@octokit/plugin-retry": ["@octokit/plugin-retry@7.2.1", "", { "dependencies": { "@octokit/request-error": "^6.1.8", "@octokit/types": "^14.0.0", "bottleneck": "^2.15.3" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-wUc3gv0D6vNHpGxSaR3FlqJpTXGWgqmk607N9L3LvPL4QjaxDgX/1nY2mGpT37Khn+nlIXdljczkRnNdTTV3/A=="],
"@octokit/plugin-throttling": ["@octokit/plugin-throttling@7.0.0", "", { "dependencies": { "@octokit/types": "^11.0.0", "bottleneck": "^2.15.3" }, "peerDependencies": { "@octokit/core": "^5.0.0" } }, "sha512-KL2k/d0uANc8XqP5S64YcNFCudR3F5AaKO39XWdUtlJIjT9Ni79ekWJ6Kj5xvAw87udkOMEPcVf9xEge2+ahew=="],
"@octokit/request": ["@octokit/request@10.0.3", "", { "dependencies": { "@octokit/endpoint": "^11.0.0", "@octokit/request-error": "^7.0.0", "@octokit/types": "^14.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA=="],
"@octokit/request": ["@octokit/request@8.4.1", "", { "dependencies": { "@octokit/endpoint": "^9.0.6", "@octokit/request-error": "^5.1.1", "@octokit/types": "^13.1.0", "universal-user-agent": "^6.0.0" } }, "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw=="],
"@octokit/request-error": ["@octokit/request-error@6.1.8", "", { "dependencies": { "@octokit/types": "^14.0.0" } }, "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ=="],
"@octokit/request-error": ["@octokit/request-error@5.1.1", "", { "dependencies": { "@octokit/types": "^13.1.0", "deprecation": "^2.0.0", "once": "^1.4.0" } }, "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g=="],
"@octokit/rest": ["@octokit/rest@22.0.0", "", { "dependencies": { "@octokit/core": "^7.0.2", "@octokit/plugin-paginate-rest": "^13.0.1", "@octokit/plugin-request-log": "^6.0.0", "@octokit/plugin-rest-endpoint-methods": "^16.0.0" } }, "sha512-z6tmTu9BTnw51jYGulxrlernpsQYXpui1RK21vmXn8yF5bp6iX16yfTtJYGK5Mh1qDkvDOmp2n8sRMcQmR8jiA=="],
"@octokit/types": ["@octokit/types@14.1.0", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="],
"@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="],
"@tootallnate/once": ["@tootallnate/once@2.0.0", "", {}, "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A=="],
"@types/bun": ["@types/bun@1.2.19", "", { "dependencies": { "bun-types": "1.2.19" } }, "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg=="],
"@types/caseless": ["@types/caseless@0.12.5", "", {}, "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg=="],
"@types/node": ["@types/node@20.19.9", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw=="],
"@types/react": ["@types/react@19.1.9", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA=="],
"before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="],
"@types/request": ["@types/request@2.48.13", "", { "dependencies": { "@types/caseless": "*", "@types/node": "*", "@types/tough-cookie": "*", "form-data": "^2.5.5" } }, "sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg=="],
"@types/tough-cookie": ["@types/tough-cookie@4.0.5", "", {}, "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA=="],
"abbrev": ["abbrev@1.1.1", "", {}, "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="],
"agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
"aproba": ["aproba@2.1.0", "", {}, "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew=="],
"are-we-there-yet": ["are-we-there-yet@2.0.0", "", { "dependencies": { "delegates": "^1.0.0", "readable-stream": "^3.6.0" } }, "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw=="],
"arrify": ["arrify@2.0.1", "", {}, "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug=="],
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"before-after-hook": ["before-after-hook@2.2.3", "", {}, "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="],
"big.js": ["big.js@6.2.2", "", {}, "sha512-y/ie+Faknx7sZA5MfGA2xKlu0GDv8RWrXGsmlteyJQ2lvoKv9GBK/fpRMc2qlSoBAgNxrixICFCBefIq8WCQpQ=="],
"bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
"bottleneck": ["bottleneck@2.19.5", "", {}, "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw=="],
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
"bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
"cargo-cp-artifact": ["cargo-cp-artifact@0.1.9", "", { "bin": { "cargo-cp-artifact": "bin/cargo-cp-artifact.js" } }, "sha512-6F+UYzTaGB+awsTXg0uSJA1/b/B3DDJzpKVRu0UmyI7DmNeaAl2RFHuTGIN6fEgpadRxoXGb7gbC1xo4C3IdyA=="],
"chart.js": ["chart.js@4.5.0", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ=="],
"chartjs-adapter-moment": ["chartjs-adapter-moment@1.0.1", "", { "peerDependencies": { "chart.js": ">=3.0.0", "moment": "^2.10.2" } }, "sha512-Uz+nTX/GxocuqXpGylxK19YG4R3OSVf8326D+HwSTsNw1LgzyIGRo+Qujwro1wy6X+soNSnfj5t2vZ+r6EaDmA=="],
"chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"color-support": ["color-support@1.1.3", "", { "bin": { "color-support": "bin.js" } }, "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg=="],
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
"console-control-strings": ["console-control-strings@1.1.0", "", {}, "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
"decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
"delegates": ["delegates@1.0.0", "", {}, "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ=="],
"deprecation": ["deprecation@2.3.1", "", {}, "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="],
"detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"duplexify": ["duplexify@4.1.3", "", { "dependencies": { "end-of-stream": "^1.4.1", "inherits": "^2.0.3", "readable-stream": "^3.1.1", "stream-shift": "^1.0.2" } }, "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA=="],
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
"fast-content-type-parse": ["fast-content-type-parse@3.0.0", "", {}, "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg=="],
"fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="],
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
"form-data": ["form-data@2.5.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.35", "safe-buffer": "^5.2.1" } }, "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A=="],
"fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="],
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"gauge": ["gauge@3.0.2", "", { "dependencies": { "aproba": "^1.0.3 || ^2.0.0", "color-support": "^1.1.2", "console-control-strings": "^1.0.0", "has-unicode": "^2.0.1", "object-assign": "^4.1.1", "signal-exit": "^3.0.0", "string-width": "^4.2.3", "strip-ansi": "^6.0.1", "wide-align": "^1.1.2" } }, "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q=="],
"gaxios": ["gaxios@6.7.1", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "is-stream": "^2.0.0", "node-fetch": "^2.6.9", "uuid": "^9.0.1" } }, "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ=="],
"gcp-metadata": ["gcp-metadata@6.1.1", "", { "dependencies": { "gaxios": "^6.1.1", "google-logging-utils": "^0.0.2", "json-bigint": "^1.0.0" } }, "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"glob": ["glob@11.0.3", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.0.3", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA=="],
"google-auth-library": ["google-auth-library@9.15.1", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^6.1.1", "gcp-metadata": "^6.1.0", "gtoken": "^7.0.0", "jws": "^4.0.0" } }, "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng=="],
"google-logging-utils": ["google-logging-utils@0.0.2", "", {}, "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"gtoken": ["gtoken@7.1.0", "", { "dependencies": { "gaxios": "^6.0.0", "jws": "^4.0.0" } }, "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw=="],
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
"has-unicode": ["has-unicode@2.0.1", "", {}, "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ=="],
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"html-entities": ["html-entities@2.6.0", "", {}, "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ=="],
"http-proxy-agent": ["http-proxy-agent@5.0.0", "", { "dependencies": { "@tootallnate/once": "2", "agent-base": "6", "debug": "4" } }, "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w=="],
"https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="],
"inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"is": ["is@3.3.2", "", {}, "sha512-a2xr4E3s1PjDS8ORcGgXpWx6V+liNs+O3JRD2mb9aeugD7rtkkZ0zgLdYgw0tWsKhsdiezGYptSiMlVazCBTuQ=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"jackspeak": ["jackspeak@4.1.1", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" } }, "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ=="],
"json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="],
"jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="],
"jws": ["jws@4.0.0", "", { "dependencies": { "jwa": "^2.0.0", "safe-buffer": "^5.0.1" } }, "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg=="],
"lru-cache": ["lru-cache@11.1.0", "", {}, "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A=="],
"make-dir": ["make-dir@3.1.0", "", { "dependencies": { "semver": "^6.0.0" } }, "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="],
"minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="],
"minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
"minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="],
"mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
"moment": ["moment@2.30.1", "", {}, "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
"nopt": ["nopt@5.0.0", "", { "dependencies": { "abbrev": "1" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ=="],
"npmlog": ["npmlog@5.0.1", "", { "dependencies": { "are-we-there-yet": "^2.0.0", "console-control-strings": "^1.1.0", "gauge": "^3.0.0", "set-blocking": "^2.0.0" } }, "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
"parenthesis": ["parenthesis@3.1.8", "", {}, "sha512-KF/U8tk54BgQewkJPvB4s/US3VQY68BRDpH638+7O/n58TpnwiwnOtGIOsT2/i+M78s61BBpeC83STB88d8sqw=="],
"path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="],
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"path-scurry": ["path-scurry@2.0.0", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg=="],
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
"retry-request": ["retry-request@7.0.2", "", { "dependencies": { "@types/request": "^2.48.8", "extend": "^3.0.2", "teeny-request": "^9.0.0" } }, "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w=="],
"rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
"set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="],
"simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="],
"skia-canvas": ["skia-canvas@2.0.2", "", { "dependencies": { "@mapbox/node-pre-gyp": "^1.0.11", "cargo-cp-artifact": "^0.1", "glob": "^11.0.0", "path-browserify": "^1.0.1", "simple-get": "^4.0.1", "string-split-by": "^1.0.0" } }, "sha512-LUa7P41NRNoCWhvPyX4aKP5SpeWDXmWYbonCt4FfkEdTuSssxpvYiK5Y69B0MudDR6LVNt9RBwpZfuCRpVSbbw=="],
"stream-events": ["stream-events@1.0.5", "", { "dependencies": { "stubs": "^3.0.0" } }, "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg=="],
"stream-shift": ["stream-shift@1.0.3", "", {}, "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ=="],
"string-split-by": ["string-split-by@1.0.0", "", { "dependencies": { "parenthesis": "^3.1.5" } }, "sha512-KaJKY+hfpzNyet/emP81PJA9hTVSfxNLS9SFTWxdCnnW1/zOOwiV248+EfoX7IQFcBaOp4G5YE6xTJMF+pLg6A=="],
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"strnum": ["strnum@2.1.1", "", {}, "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw=="],
"stubs": ["stubs@3.0.0", "", {}, "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw=="],
"tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="],
"teeny-request": ["teeny-request@9.0.0", "", { "dependencies": { "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", "node-fetch": "^2.6.9", "stream-events": "^1.0.5", "uuid": "^9.0.0" } }, "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g=="],
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
"tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="],
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
@@ -77,14 +368,148 @@
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"universal-user-agent": ["universal-user-agent@7.0.3", "", {}, "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A=="],
"universal-user-agent": ["universal-user-agent@6.0.1", "", {}, "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ=="],
"@octokit/core/@octokit/request-error": ["@octokit/request-error@7.0.0", "", { "dependencies": { "@octokit/types": "^14.0.0" } }, "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"wide-align": ["wide-align@1.1.5", "", { "dependencies": { "string-width": "^1.0.2 || 2 || 3 || 4" } }, "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg=="],
"wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
"wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
"@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
"@octokit/plugin-paginate-rest/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="],
"@octokit/plugin-request-log/@octokit/core": ["@octokit/core@7.0.3", "", { "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.1", "@octokit/request": "^10.0.2", "@octokit/request-error": "^7.0.0", "@octokit/types": "^14.0.0", "before-after-hook": "^4.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-oNXsh2ywth5aowwIa7RKtawnkdH6LgU1ztfP9AIUCQCvzysB+WeU8o2kyyosDPwBZutPpjZDKPQGIzzrfTWweQ=="],
"@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="],
"@octokit/plugin-retry/@octokit/core": ["@octokit/core@7.0.3", "", { "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.1", "@octokit/request": "^10.0.2", "@octokit/request-error": "^7.0.0", "@octokit/types": "^14.0.0", "before-after-hook": "^4.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-oNXsh2ywth5aowwIa7RKtawnkdH6LgU1ztfP9AIUCQCvzysB+WeU8o2kyyosDPwBZutPpjZDKPQGIzzrfTWweQ=="],
"@octokit/plugin-retry/@octokit/request-error": ["@octokit/request-error@6.1.8", "", { "dependencies": { "@octokit/types": "^14.0.0" } }, "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ=="],
"@octokit/plugin-retry/@octokit/types": ["@octokit/types@14.1.0", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="],
"@octokit/plugin-throttling/@octokit/types": ["@octokit/types@11.1.0", "", { "dependencies": { "@octokit/openapi-types": "^18.0.0" } }, "sha512-Fz0+7GyLm/bHt8fwEqgvRBWwIV1S6wRRyq+V6exRKLVWaKGsuy6H9QFYeBVDV7rK6fO3XwHgQOPxv+cLj2zpXQ=="],
"@octokit/request/@octokit/request-error": ["@octokit/request-error@7.0.0", "", { "dependencies": { "@octokit/types": "^14.0.0" } }, "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg=="],
"@octokit/rest/@octokit/core": ["@octokit/core@7.0.3", "", { "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.1", "@octokit/request": "^10.0.2", "@octokit/request-error": "^7.0.0", "@octokit/types": "^14.0.0", "before-after-hook": "^4.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-oNXsh2ywth5aowwIa7RKtawnkdH6LgU1ztfP9AIUCQCvzysB+WeU8o2kyyosDPwBZutPpjZDKPQGIzzrfTWweQ=="],
"@octokit/rest/@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@13.1.1", "", { "dependencies": { "@octokit/types": "^14.1.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-q9iQGlZlxAVNRN2jDNskJW/Cafy7/XE52wjZ5TTvyhyOD904Cvx//DNyoO3J/MXJ0ve3rPoNWKEg5iZrisQSuw=="],
"@octokit/rest/@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@16.0.0", "", { "dependencies": { "@octokit/types": "^14.1.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-kJVUQk6/dx/gRNLWUnAWKFs1kVPn5O5CYZyssyEoNYaFedqZxsfYs7DwI3d67hGz4qOwaJ1dpm07hOAD1BXx6g=="],
"fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"gauge/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
"gaxios/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
"make-dir/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
"tar/minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="],
"wrap-ansi/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
"wrap-ansi/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
"wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
"@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
"@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="],
"@octokit/plugin-request-log/@octokit/core/@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="],
"@octokit/plugin-request-log/@octokit/core/@octokit/graphql": ["@octokit/graphql@9.0.1", "", { "dependencies": { "@octokit/request": "^10.0.2", "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-j1nQNU1ZxNFx2ZtKmL4sMrs4egy5h65OMDmSbVyuCzjOcwsHq6EaYjOTGXPQxgfiN8dJ4CriYHk6zF050WEULg=="],
"@octokit/plugin-request-log/@octokit/core/@octokit/request": ["@octokit/request@10.0.3", "", { "dependencies": { "@octokit/endpoint": "^11.0.0", "@octokit/request-error": "^7.0.0", "@octokit/types": "^14.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA=="],
"@octokit/plugin-request-log/@octokit/core/@octokit/request-error": ["@octokit/request-error@7.0.0", "", { "dependencies": { "@octokit/types": "^14.0.0" } }, "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg=="],
"@octokit/plugin-request-log/@octokit/core/@octokit/types": ["@octokit/types@14.1.0", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="],
"@octokit/plugin-request-log/@octokit/core/before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="],
"@octokit/plugin-request-log/@octokit/core/universal-user-agent": ["universal-user-agent@7.0.3", "", {}, "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A=="],
"@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="],
"@octokit/plugin-retry/@octokit/core/@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="],
"@octokit/plugin-retry/@octokit/core/@octokit/graphql": ["@octokit/graphql@9.0.1", "", { "dependencies": { "@octokit/request": "^10.0.2", "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-j1nQNU1ZxNFx2ZtKmL4sMrs4egy5h65OMDmSbVyuCzjOcwsHq6EaYjOTGXPQxgfiN8dJ4CriYHk6zF050WEULg=="],
"@octokit/plugin-retry/@octokit/core/@octokit/request": ["@octokit/request@10.0.3", "", { "dependencies": { "@octokit/endpoint": "^11.0.0", "@octokit/request-error": "^7.0.0", "@octokit/types": "^14.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA=="],
"@octokit/plugin-retry/@octokit/core/@octokit/request-error": ["@octokit/request-error@7.0.0", "", { "dependencies": { "@octokit/types": "^14.0.0" } }, "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg=="],
"@octokit/plugin-retry/@octokit/core/before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="],
"@octokit/plugin-retry/@octokit/core/universal-user-agent": ["universal-user-agent@7.0.3", "", {}, "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A=="],
"@octokit/plugin-retry/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@25.1.0", "", {}, "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA=="],
"@octokit/plugin-throttling/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@18.1.1", "", {}, "sha512-VRaeH8nCDtF5aXWnjPuEMIYf1itK/s3JYyJcWFJT8X9pSNnBtriDf7wlEWsGuhPLl4QIH4xM8fqTXDwJ3Mu6sw=="],
"@octokit/rest/@octokit/core/@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="],
"@octokit/rest/@octokit/core/@octokit/graphql": ["@octokit/graphql@9.0.1", "", { "dependencies": { "@octokit/request": "^10.0.2", "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-j1nQNU1ZxNFx2ZtKmL4sMrs4egy5h65OMDmSbVyuCzjOcwsHq6EaYjOTGXPQxgfiN8dJ4CriYHk6zF050WEULg=="],
"@octokit/rest/@octokit/core/@octokit/request": ["@octokit/request@10.0.3", "", { "dependencies": { "@octokit/endpoint": "^11.0.0", "@octokit/request-error": "^7.0.0", "@octokit/types": "^14.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA=="],
"@octokit/rest/@octokit/core/@octokit/request-error": ["@octokit/request-error@7.0.0", "", { "dependencies": { "@octokit/types": "^14.0.0" } }, "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg=="],
"@octokit/rest/@octokit/core/@octokit/types": ["@octokit/types@14.1.0", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="],
"@octokit/rest/@octokit/core/before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="],
"@octokit/rest/@octokit/core/universal-user-agent": ["universal-user-agent@7.0.3", "", {}, "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A=="],
"@octokit/rest/@octokit/plugin-paginate-rest/@octokit/types": ["@octokit/types@14.1.0", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="],
"@octokit/rest/@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@14.1.0", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="],
"gaxios/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
"rimraf/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"wrap-ansi/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
"wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
"@octokit/plugin-request-log/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.0", "", { "dependencies": { "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ=="],
"@octokit/plugin-request-log/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@25.1.0", "", {}, "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA=="],
"@octokit/plugin-retry/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.0", "", { "dependencies": { "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ=="],
"@octokit/rest/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.0", "", { "dependencies": { "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ=="],
"@octokit/rest/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@25.1.0", "", {}, "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA=="],
"@octokit/rest/@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@25.1.0", "", {}, "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA=="],
"@octokit/rest/@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@25.1.0", "", {}, "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA=="],
}
}

View File

@@ -1,14 +1,11 @@
[runtime]
telemetry = false
[install]
# Use exact versions for reproducible builds
exact = true
[install.scopes]
# Configure any scoped packages if needed
[test]
# Test configuration
preload = ["./src/test-setup.ts"]
[run]
# Runtime configuration
bun = true

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 334 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 182 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 121 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 334 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 327 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 801 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 842 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.6 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 575 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 79 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 565 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 117 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.6 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 76 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 875 KiB

121
dist/action.js vendored

File diff suppressed because one or more lines are too long

80
dist/index.js vendored
View File

@@ -1,80 +0,0 @@
#!/usr/bin/env bun
// @bun
// src/index.ts
class UsageStatistics {
data = [];
addUsage(userId, action, metadata) {
const record = {
timestamp: new Date,
userId,
action,
metadata
};
this.data.push(record);
}
getAllData() {
return [...this.data];
}
getUserData(userId) {
return this.data.filter((record) => record.userId === userId);
}
getActionData(action) {
return this.data.filter((record) => record.action === action);
}
getStatistics() {
if (this.data.length === 0) {
return {
totalRecords: 0,
uniqueUsers: 0,
uniqueActions: 0,
timeRange: null
};
}
const uniqueUsers = new Set(this.data.map((record) => record.userId)).size;
const uniqueActions = new Set(this.data.map((record) => record.action)).size;
const timestamps = this.data.map((record) => record.timestamp);
const start = new Date(Math.min(...timestamps.map((t) => t.getTime())));
const end = new Date(Math.max(...timestamps.map((t) => t.getTime())));
return {
totalRecords: this.data.length,
uniqueUsers,
uniqueActions,
timeRange: { start, end }
};
}
}
async function main() {
console.log(`\uD83D\uDE80 Usage Statistics Script Starting...
`);
const stats = new UsageStatistics;
stats.addUsage("user1", "login", { browser: "chrome" });
stats.addUsage("user2", "login", { browser: "firefox" });
stats.addUsage("user1", "view_page", { page: "/dashboard" });
stats.addUsage("user3", "login", { browser: "safari" });
stats.addUsage("user2", "logout");
const summary = stats.getStatistics();
console.log("\uD83D\uDCCA Usage Statistics Summary:");
console.log(`Total Records: ${summary.totalRecords}`);
console.log(`Unique Users: ${summary.uniqueUsers}`);
console.log(`Unique Actions: ${summary.uniqueActions}`);
if (summary.timeRange) {
console.log(`Time Range: ${summary.timeRange.start.toISOString()} to ${summary.timeRange.end.toISOString()}`);
}
console.log(`
\uD83D\uDC65 User Data:`);
const userData = stats.getUserData("user1");
console.log(`User1 actions: ${userData.map((d) => d.action).join(", ")}`);
console.log(`
\uD83C\uDFAF Action Data:`);
const loginData = stats.getActionData("login");
console.log(`Login events: ${loginData.length}`);
console.log(`
\u2705 Script completed successfully!`);
}
if (import.meta.main) {
main().catch(console.error);
}
export {
UsageStatistics
};

View File

@@ -1,39 +0,0 @@
name: Update Usage Statistics
on:
schedule:
- cron: '0 0 * * *' # Daily at midnight
workflow_dispatch: # Allow manual triggering
jobs:
update-stats:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Usage Statistics Tracker
uses: LukeHagar/usage-statistics@v1
with:
npm-packages: 'lodash,axios'
github-repositories: 'microsoft/vscode,facebook/react'
pypi-packages: 'requests,numpy'
homebrew-formulas: 'git,node'
powershell-modules: 'PowerShellGet,PSReadLine'
postman-collections: '12345,67890'
go-modules: 'github.com/gin-gonic/gin,github.com/go-chi/chi'
json-output-path: 'stats.json'
csv-output-path: 'stats.csv'
report-output-path: 'docs/usage-report.md'
update-readme: 'true'
readme-path: 'README.md'
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Commit and push changes
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add stats.json stats.csv docs/usage-report.md README.md
git commit -m "chore: update usage statistics [skip ci]" || echo "No changes to commit"
git push

View File

@@ -1,59 +0,0 @@
name: Usage Statistics with Input-Based Configuration
on:
schedule:
- cron: '0 0 * * *' # Daily at midnight
workflow_dispatch:
jobs:
update-stats:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Usage Statistics Tracker
uses: LukeHagar/usage-statistics@v1
with:
# NPM packages to track
npm-packages: 'lodash,axios,react,vue'
# GitHub repositories to track
github-repositories: 'microsoft/vscode,facebook/react,vercel/next.js'
# PyPI packages to track
pypi-packages: 'requests,numpy,pandas'
# Homebrew formulas to track
homebrew-formulas: 'git,node,postgresql'
# PowerShell modules to track
powershell-modules: 'PowerShellGet,PSReadLine'
# Go modules to track
go-modules: 'github.com/gin-gonic/gin,github.com/go-chi/chi'
# Output configuration
json-output-path: 'data/stats.json'
csv-output-path: 'data/stats.csv'
report-output-path: 'docs/usage-report.md'
# README integration
update-readme: 'true'
readme-path: 'README.md'
# API tokens
github-token: ${{ secrets.GITHUB_TOKEN }}
postman-api-key: ${{ secrets.POSTMAN_API_KEY }}
# Commit settings
commit-message: 'feat: update usage statistics with detailed report'
- name: Commit and push changes
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add data/stats.json data/stats.csv docs/usage-report.md README.md
git commit -m "chore: update usage statistics [skip ci]" || echo "No changes to commit"
git push

View File

@@ -9,24 +9,29 @@
"homepage": "https://github.com/LukeHagar/usage-statistics#readme",
"main": "src/index.ts",
"scripts": {
"start": "bun run src/index.ts",
"preview": "bun run src/index.ts --preview",
"dev": "bun --watch src/index.ts",
"build": "bun build src/index.ts --outdir dist",
"test": "bun test",
"action:build": "bun build src/action.ts --outdir dist --target node --minify",
"action:test": "bun test && bun run action:build"
"dev": "bun --watch --env-file=.dev.env --env-file=.env run src/action.ts",
"test:pypi": "bun run test-pypi-bigquery.ts",
"test:pypi-detailed": "bun run test-pypi-detailed.ts"
},
"devDependencies": {
"@types/bun": "latest",
"@types/node": "^20.0.0",
"bun-types": "1.2.19",
"typescript": "^5.0.0"
},
"dependencies": {
"@actions/core": "1.11.1",
"@octokit/rest": "22.0.0",
"@actions/github": "6.0.1",
"@google-cloud/bigquery": "^7.0.0",
"@octokit/graphql": "^7.0.0",
"@octokit/plugin-retry": "^7.0.0",
"@octokit/plugin-throttling": "^7.0.0"
"@octokit/plugin-throttling": "^7.0.0",
"@octokit/rest": "22.0.0",
"chart.js": "^4.5.0",
"chartjs-adapter-moment": "1.0.1",
"fast-xml-parser": "5.2.5",
"moment": "2.30.1",
"skia-canvas": "^2.0.2"
},
"engines": {
"bun": ">=1.0.0"
@@ -47,5 +52,6 @@
"usage"
],
"author": "LukeHagar",
"license": "MIT"
"license": "MIT",
"private": true
}

View File

@@ -1,206 +1,112 @@
#!/usr/bin/env bun
import * as core from '@actions/core';
import * as fs from 'fs/promises';
import * as path from 'path';
import { UsageStatisticsManager } from './index';
import type { TrackingConfig } from './types';
import * as core from '@actions/core'
import { collectNpmBatch } from './collectors/npm'
import { collectGithubBatch } from './collectors/github'
import { collectPowerShellBatch } from './collectors/powershell'
import { collectPypiBatch } from './collectors/pypi'
import type { MetricResult } from './collectors/types'
import { getInputs, updateRepositoryReadme } from './utils'
import { writeFile } from 'fs/promises'
async function run() {
try {
const {
npmPackages,
githubRepositories,
pypiPackages,
powershellModules,
jsonOutputPath,
updateReadme,
commitMessage,
readmePath,
} = getInputs()
// Debug logs are only output if the `ACTIONS_STEP_DEBUG` secret is true
core.debug(`NPM Packages: ${npmPackages.join(', ')}`)
core.debug(`GitHub Repositories: ${githubRepositories.join(', ')}`)
core.debug(`PyPI Packages: ${pypiPackages.join(', ')}`)
core.debug(`PowerShell Modules: ${powershellModules.join(', ')}`)
core.debug(``)
core.debug(`JSON Output Path: ${jsonOutputPath}`)
core.debug(`Update README: ${updateReadme}`)
core.debug(`Commit Message: ${commitMessage}`)
// Track which platforms are being used
const platformsTracked: string[] = []
if (npmPackages.length > 0) platformsTracked.push('NPM')
if (githubRepositories.length > 0) platformsTracked.push('GitHub')
if (pypiPackages.length > 0) platformsTracked.push('PyPI')
if (powershellModules.length > 0) platformsTracked.push('PowerShell')
core.debug(`Platforms to track: ${platformsTracked.join(', ')}`)
core.info(`Successfully configured usage statistics tracker for ${platformsTracked.length} platforms`)
const metricPromises: Promise<MetricResult[]>[] = []
const metrics: MetricResult[] = []
for (const platform of platformsTracked) {
core.info(`Collecting ${platform} metrics...`)
switch (platform) {
case 'NPM':
console.log(`Collecting NPM metrics for ${npmPackages.join(', ')}`)
console.time(`Collecting NPM metrics`)
metricPromises.push(collectNpmBatch(npmPackages).then(results => {
console.timeEnd(`Collecting NPM metrics`)
return results
}))
break
case 'GitHub':
console.log(`Collecting GitHub metrics for ${githubRepositories.join(', ')}`)
console.time(`Collecting GitHub metrics`)
metricPromises.push(collectGithubBatch(githubRepositories).then(results => {
console.timeEnd(`Collecting GitHub metrics`)
return results
}))
break
case 'PyPI':
console.log(`Collecting PyPI metrics for ${pypiPackages.join(', ')}`)
console.time(`Collecting PyPI metrics`)
metricPromises.push(collectPypiBatch(pypiPackages).then(results => {
console.timeEnd(`Collecting PyPI metrics`)
return results
}))
break
case 'PowerShell':
console.log(`Collecting PowerShell metrics for ${powershellModules.join(', ')}`)
console.time(`Collecting PowerShell metrics`)
metricPromises.push(collectPowerShellBatch(powershellModules).then(results => {
console.timeEnd(`Collecting PowerShell metrics`)
return results
}))
break
}
}
console.log('All metrics collecting started')
const metricResults = await Promise.all(metricPromises)
metrics.push(...metricResults.flat())
console.log('All metrics collecting completed')
if (updateReadme) {
console.log('Updating repository readme...')
await updateRepositoryReadme(metrics, readmePath)
}
console.log('Repository readme updated')
// Persist full result set to JSON for downstream consumption
try {
// Get inputs
const npmPackages = core.getInput('npm-packages');
const githubRepositories = core.getInput('github-repositories');
const pypiPackages = core.getInput('pypi-packages');
const homebrewFormulas = core.getInput('homebrew-formulas');
const powershellModules = core.getInput('powershell-modules');
const postmanCollections = core.getInput('postman-collections');
const goModules = core.getInput('go-modules');
const jsonOutputPath = core.getInput('json-output-path');
const csvOutputPath = core.getInput('csv-output-path');
const reportOutputPath = core.getInput('report-output-path');
const updateReadme = core.getInput('update-readme') === 'true';
const readmePath = core.getInput('readme-path');
const githubToken = core.getInput('github-token');
const postmanApiKey = core.getInput('postman-api-key');
const commitMessage = core.getInput('commit-message');
const previewMode = core.getInput('preview-mode') === 'true';
// Set environment variables
if (githubToken) {
process.env.GITHUB_TOKEN = githubToken;
core.info('✅ Using custom GitHub token for authentication');
} else if (process.env.GITHUB_TOKEN) {
// Use the default GitHub token if no custom token provided
core.info('✅ Using default GitHub token from environment');
} else {
core.warning('⚠️ No GitHub token provided. Some API calls may be rate limited.');
await writeFile(jsonOutputPath, JSON.stringify(metrics, null, 2), 'utf8')
core.setOutput('json-output', jsonOutputPath)
console.log(`Wrote metrics JSON to ${jsonOutputPath}`)
} catch (writeErr) {
console.warn(`Failed to write metrics JSON to ${jsonOutputPath}:`, writeErr)
}
if (postmanApiKey) {
process.env.POSTMAN_API_KEY = postmanApiKey;
}
// Build configuration from inputs
const trackingConfig: TrackingConfig = {
enableLogging: true,
updateInterval: 60 * 60 * 1000, // 1 hour
npmPackages: npmPackages ? npmPackages.split(',').map(p => p.trim()).filter(p => p) : [],
githubRepos: githubRepositories ? githubRepositories.split(',').map(r => r.trim()).filter(r => r) : [],
pythonPackages: pypiPackages ? pypiPackages.split(',').map(p => p.trim()).filter(p => p) : [],
homebrewPackages: homebrewFormulas ? homebrewFormulas.split(',').map(f => f.trim()).filter(f => f) : [],
powershellModules: powershellModules ? powershellModules.split(',').map(m => m.trim()).filter(m => m) : [],
postmanCollections: postmanCollections ? postmanCollections.split(',').map(c => c.trim()).filter(c => c) : [],
goModules: goModules ? goModules.split(',').map(m => m.trim()).filter(m => m) : []
};
// Validate that at least one platform has packages configured
const totalPackages = (trackingConfig.npmPackages?.length || 0) +
(trackingConfig.githubRepos?.length || 0) +
(trackingConfig.pythonPackages?.length || 0) +
(trackingConfig.homebrewPackages?.length || 0) +
(trackingConfig.powershellModules?.length || 0) +
(trackingConfig.postmanCollections?.length || 0) +
(trackingConfig.goModules?.length || 0);
if (totalPackages === 0 && !previewMode) {
core.warning('No packages configured for tracking. Consider adding packages to track or enabling preview mode.');
}
// Create manager
const manager = new UsageStatisticsManager(trackingConfig);
// Generate report
let report;
if (previewMode) {
core.info('🎭 Running in preview mode with mock data...');
report = await manager.generatePreviewReport();
} else {
core.info('📊 Generating comprehensive usage statistics report...');
report = await manager.generateComprehensiveReport();
}
// Display report
await manager.displayReport(report);
// Write JSON output
if (jsonOutputPath) {
const jsonContent = JSON.stringify(report, null, 2);
await fs.writeFile(jsonOutputPath, jsonContent);
core.info(`📄 JSON report written to ${jsonOutputPath}`);
core.setOutput('json-output', jsonOutputPath);
}
// Write CSV output
if (csvOutputPath) {
const csvReport = await manager.exportReport('csv');
await fs.writeFile(csvOutputPath, csvReport);
core.info(`📊 CSV report written to ${csvOutputPath}`);
core.setOutput('csv-output', csvOutputPath);
}
// Write human-readable report
if (reportOutputPath) {
const reportContent = await generateHumanReadableReport(report);
await fs.writeFile(reportOutputPath, reportContent);
core.info(`📋 Human-readable report written to ${reportOutputPath}`);
core.setOutput('report-output', reportOutputPath);
}
// Update README if requested
if (updateReadme && readmePath) {
try {
await updateReadmeWithStats(report, readmePath);
core.info(`📝 README updated at ${readmePath}`);
} catch (error) {
core.warning(`Failed to update README: ${error}`);
}
}
// Set outputs
core.setOutput('total-downloads', report.totalDownloads.toString());
core.setOutput('unique-packages', report.uniquePackages.toString());
core.setOutput('platforms-tracked', report.platforms.join(','));
core.info('✅ Usage Statistics Tracker completed successfully!');
} catch (error) {
core.setFailed(`Action failed: ${error}`);
}
core.setOutput('commit-message', commitMessage)
} catch (error) {
// Fail the workflow run if an error occurs
if (error instanceof Error) core.setFailed(error.message)
}
async function generateHumanReadableReport(report: any): Promise<string> {
let content = '# Usage Statistics Summary\n\n';
content += `Generated on: ${new Date().toISOString()}\n\n`;
// Overall Summary
content += '## Overall Summary\n\n';
content += `- **Total Downloads**: ${report.totalDownloads.toLocaleString()}\n`;
content += `- **Unique Packages**: ${report.uniquePackages}\n`;
content += `- **Platforms Tracked**: ${report.platforms.join(', ')}\n\n`;
// Platform Totals
content += '## Platform Totals\n\n';
for (const [platform, data] of Object.entries(report.platformBreakdown)) {
content += `### ${platform.toUpperCase()}\n`;
content += `- **Downloads**: ${data.totalDownloads.toLocaleString()}\n`;
content += `- **Packages**: ${data.uniquePackages}\n\n`;
}
// Package Rankings
content += '## Package Rankings\n\n';
report.topPackages.forEach((pkg: any, index: number) => {
content += `${index + 1}. **${pkg.name}** (${pkg.platform}) - ${pkg.downloads.toLocaleString()} downloads\n`;
});
return content;
}
async function updateReadmeWithStats(report: any, readmePath: string) {
const STATS_MARKER_START = '<!-- USAGE_STATS_START -->';
const STATS_MARKER_END = '<!-- USAGE_STATS_END -->';
try {
const readmeContent = await fs.readFile(readmePath, 'utf-8');
const statsSection = `
## 📊 Usage Statistics
Last updated: ${new Date().toISOString()}
### Summary
- **Total Downloads**: ${report.totalDownloads.toLocaleString()}
- **Unique Packages**: ${report.uniquePackages}
- **Platforms Tracked**: ${report.platforms.join(', ')}
### Platform Totals
${Object.entries(report.platformBreakdown).map(([platform, data]: [string, any]) =>
`- **${platform.toUpperCase()}**: ${data.totalDownloads.toLocaleString()} downloads (${data.uniquePackages} packages)`
).join('\n')}
### Top Packages
${report.topPackages.map((pkg: any, index: number) =>
`${index + 1}. **${pkg.name}** (${pkg.platform}) - ${pkg.downloads.toLocaleString()} downloads`
).join('\n')}
`;
const startMarker = readmeContent.indexOf(STATS_MARKER_START);
const endMarker = readmeContent.indexOf(STATS_MARKER_END);
if (startMarker !== -1 && endMarker !== -1) {
const beforeStats = readmeContent.substring(0, startMarker + STATS_MARKER_START.length);
const afterStats = readmeContent.substring(endMarker);
const updatedContent = beforeStats + statsSection + afterStats;
await fs.writeFile(readmePath, updatedContent);
} else {
core.warning(`Stats markers not found in README. Please add ${STATS_MARKER_START} and ${STATS_MARKER_END} markers.`);
}
} catch (error) {
throw new Error(`Failed to update README: ${error}`);
}
}
// Run the action
if (import.meta.main) {
run();
}

View File

@@ -1,289 +0,0 @@
/**
* Download Statistics Aggregator
* Combines and analyzes statistics from all platform trackers
*/
import type { BaseDownloadStats, TrackingConfig } from './types';
import NpmTracker from './trackers/npm';
import GitHubTracker from './trackers/github';
import PyPiTracker from './trackers/pypi';
import HomebrewTracker from './trackers/homebrew';
import PowerShellTracker from './trackers/powershell';
import PostmanTracker from './trackers/postman';
import GoTracker from './trackers/go';
export interface AggregatedStats {
totalDownloads: number;
uniquePackages: number;
platforms: string[];
platformBreakdown: Record<string, {
totalDownloads: number;
uniquePackages: number;
packages: string[];
}>;
topPackages: Array<{
name: string;
platform: string;
downloads: number;
}>;
}
export class DownloadStatsAggregator {
private trackers: Map<string, any> = new Map();
private config: TrackingConfig;
constructor(config: TrackingConfig) {
this.config = config;
this.initializeTrackers();
}
private initializeTrackers() {
// Initialize all trackers
this.trackers.set('npm', new NpmTracker());
this.trackers.set('github', new GitHubTracker(process.env.GITHUB_TOKEN));
this.trackers.set('pypi', new PyPiTracker());
this.trackers.set('homebrew', new HomebrewTracker(process.env.GITHUB_TOKEN));
this.trackers.set('powershell', new PowerShellTracker());
this.trackers.set('postman', new PostmanTracker(process.env.POSTMAN_API_KEY));
this.trackers.set('go', new GoTracker(process.env.GITHUB_TOKEN));
}
async aggregateStats(stats: BaseDownloadStats[]): Promise<AggregatedStats> {
const platformBreakdown: Record<string, {
totalDownloads: number;
uniquePackages: number;
packages: string[];
}> = {};
const packageMap = new Map<string, { downloads: number; platform: string }>();
let totalDownloads = 0;
let uniquePackages = 0;
const platforms = new Set<string>();
// Process each stat
for (const stat of stats) {
totalDownloads += stat.downloadCount;
platforms.add(stat.platform);
// Track package downloads
const packageKey = `${stat.platform}:${stat.packageName}`;
const existing = packageMap.get(packageKey);
if (existing) {
existing.downloads += stat.downloadCount;
} else {
packageMap.set(packageKey, { downloads: stat.downloadCount, platform: stat.platform });
uniquePackages++;
}
// Update platform breakdown
if (!platformBreakdown[stat.platform]) {
platformBreakdown[stat.platform] = {
totalDownloads: 0,
uniquePackages: 0,
packages: []
};
}
platformBreakdown[stat.platform].totalDownloads += stat.downloadCount;
if (!platformBreakdown[stat.platform].packages.includes(stat.packageName)) {
platformBreakdown[stat.platform].packages.push(stat.packageName);
platformBreakdown[stat.platform].uniquePackages++;
}
}
// Get top packages
const topPackages = Array.from(packageMap.entries())
.map(([key, data]) => ({
name: key.split(':')[1],
platform: data.platform,
downloads: data.downloads
}))
.sort((a, b) => b.downloads - a.downloads);
return {
totalDownloads,
uniquePackages,
platforms: Array.from(platforms),
platformBreakdown,
topPackages
};
}
async collectAllStats(): Promise<BaseDownloadStats[]> {
const allStats: BaseDownloadStats[] = [];
// Collect NPM stats
if (this.config.npmPackages) {
const npmPromises = this.config.npmPackages.map(async packageName => {
try {
const npmTracker = this.trackers.get('npm');
const stats = await npmTracker.getDownloadStats(packageName);
return stats;
} catch (error) {
console.error(`Error collecting NPM stats for ${packageName}:`, error);
return [];
}
});
const npmResults = await Promise.all(npmPromises);
npmResults.forEach(stats => allStats.push(...stats));
}
// Collect GitHub stats (Octokit plugins handle rate limiting)
if (this.config.githubRepos) {
const githubPromises = this.config.githubRepos.map(async repo => {
try {
const githubTracker = this.trackers.get('github');
const stats = await githubTracker.getDownloadStats(repo);
return stats;
} catch (error) {
console.error(`Error collecting GitHub stats for ${repo}:`, error);
return [];
}
});
const githubResults = await Promise.all(githubPromises);
githubResults.forEach(stats => allStats.push(...stats));
}
// Collect PyPI stats
if (this.config.pythonPackages) {
const pypiPromises = this.config.pythonPackages.map(async packageName => {
try {
const pypiTracker = this.trackers.get('pypi');
const stats = await pypiTracker.getDownloadStats(packageName);
return stats;
} catch (error) {
console.error(`Error collecting PyPI stats for ${packageName}:`, error);
return [];
}
});
const pypiResults = await Promise.all(pypiPromises);
pypiResults.forEach(stats => allStats.push(...stats));
}
// Collect Homebrew stats
if (this.config.homebrewPackages) {
const homebrewPromises = this.config.homebrewPackages.map(async packageName => {
try {
const homebrewTracker = this.trackers.get('homebrew');
const stats = await homebrewTracker.getDownloadStats(packageName);
return stats;
} catch (error) {
console.error(`Error collecting Homebrew stats for ${packageName}:`, error);
return [];
}
});
const homebrewResults = await Promise.all(homebrewPromises);
homebrewResults.forEach(stats => allStats.push(...stats));
}
// Collect PowerShell stats
if (this.config.powershellModules) {
const powershellPromises = this.config.powershellModules.map(async moduleName => {
try {
const powershellTracker = this.trackers.get('powershell');
const stats = await powershellTracker.getDownloadStats(moduleName);
return stats;
} catch (error) {
console.error(`Error collecting PowerShell stats for ${moduleName}:`, error);
return [];
}
});
const powershellResults = await Promise.all(powershellPromises);
powershellResults.forEach(stats => allStats.push(...stats));
}
// Collect Postman stats
if (this.config.postmanCollections) {
const postmanPromises = this.config.postmanCollections.map(async collectionId => {
try {
const postmanTracker = this.trackers.get('postman');
const stats = await postmanTracker.getDownloadStats(collectionId);
return stats;
} catch (error) {
console.error(`Error collecting Postman stats for ${collectionId}:`, error);
return [];
}
});
const postmanResults = await Promise.all(postmanPromises);
postmanResults.forEach(stats => allStats.push(...stats));
}
// Collect Go stats
if (this.config.goModules) {
const goPromises = this.config.goModules.map(async moduleName => {
try {
const goTracker = this.trackers.get('go');
const stats = await goTracker.getDownloadStats(moduleName);
return stats;
} catch (error) {
console.error(`Error collecting Go stats for ${moduleName}:`, error);
return [];
}
});
const goResults = await Promise.all(goPromises);
goResults.forEach(stats => allStats.push(...stats));
}
return allStats;
}
async generateReport(): Promise<AggregatedStats> {
const allStats = await this.collectAllStats();
return this.aggregateStats(allStats);
}
async getPlatformStats(platform: string): Promise<BaseDownloadStats[]> {
const tracker = this.trackers.get(platform);
if (!tracker) {
throw new Error(`Unknown platform: ${platform}`);
}
const allStats: BaseDownloadStats[] = [];
const packages = this.getPackagesForPlatform(platform);
const promises = packages.map(async packageName => {
try {
const stats = await tracker.getDownloadStats(packageName);
return stats;
} catch (error) {
console.error(`Error collecting ${platform} stats for ${packageName}:`, error);
return [];
}
});
const results = await Promise.all(promises);
results.forEach(stats => allStats.push(...stats));
return allStats;
}
private getPackagesForPlatform(platform: string): string[] {
switch (platform) {
case 'npm':
return this.config.npmPackages || [];
case 'github':
return this.config.githubRepos || [];
case 'pypi':
return this.config.pythonPackages || [];
case 'homebrew':
return this.config.homebrewPackages || [];
case 'powershell':
return this.config.powershellModules || [];
case 'postman':
return this.config.postmanCollections || [];
case 'go':
return this.config.goModules || [];
default:
return [];
}
}
}
export default DownloadStatsAggregator;

366
src/collectors/github.ts Normal file
View File

@@ -0,0 +1,366 @@
/**
* GitHub repository statistics collector with enhanced metrics using Octokit SDK and GraphQL
*/
import { Octokit } from '@octokit/rest';
import { graphql } from '@octokit/graphql';
import type { MetricResult } from './types';
const PlatformSettings = {
name: 'GitHub',
}
// GraphQL query for basic repository data (without releases)
const REPOSITORY_BASIC_QUERY = `
query RepositoryBasicData($owner: String!, $name: String!) {
repository(owner: $owner, name: $name) {
id
name
description
homepageUrl
stargazerCount
forkCount
watchers {
totalCount
}
openIssues: issues(states: OPEN) {
totalCount
}
closedIssues: issues(states: CLOSED) {
totalCount
}
primaryLanguage {
name
}
diskUsage
createdAt
updatedAt
pushedAt
defaultBranchRef {
name
}
repositoryTopics(first: 10) {
nodes {
topic {
name
}
}
}
licenseInfo {
name
spdxId
}
}
}
`;
// GraphQL query for releases with download data
const RELEASES_QUERY = `
query RepositoryReleases($owner: String!, $name: String!, $first: Int!) {
repository(owner: $owner, name: $name) {
releases(first: $first, orderBy: {field: CREATED_AT, direction: DESC}) {
nodes {
id
tagName
name
description
createdAt
publishedAt
releaseAssets(first: 100) {
nodes {
id
name
size
downloadCount
downloadUrl
}
}
}
}
}
}
`;
// Interface for basic repository data
interface GraphQLRepositoryBasicResponse {
repository: {
id: string;
name: string;
description: string | null;
homepageUrl: string | null;
stargazerCount: number;
forkCount: number;
watchers: {
totalCount: number;
};
openIssues: {
totalCount: number;
};
closedIssues: {
totalCount: number;
};
primaryLanguage: {
name: string;
} | null;
diskUsage: number;
createdAt: string;
updatedAt: string;
pushedAt: string;
defaultBranchRef: {
name: string;
} | null;
repositoryTopics: {
nodes: Array<{
topic: {
name: string;
};
}>;
};
licenseInfo: {
name: string;
spdxId: string;
} | null;
} | null;
}
// Interface for releases data
interface GraphQLReleasesResponse {
repository: {
releases: {
nodes: Array<{
id: string;
tagName: string;
name: string | null;
description: string | null;
createdAt: string;
publishedAt: string | null;
releaseAssets: {
nodes: Array<{
id: string;
name: string;
size: number;
downloadCount: number;
downloadUrl: string;
} | null>;
};
} | null>;
};
} | null;
}
export async function collectGithub(repository: string): Promise<MetricResult> {
try {
const [owner, repo] = repository.split('/');
if (!owner || !repo) {
throw new Error(`Invalid repository format: ${repository}. Expected "owner/repo"`);
}
// Initialize Octokit for REST API calls
const token = process.env.GITHUB_TOKEN || process.env.INPUT_GITHUB_TOKEN || '';
const octokit = new Octokit({
auth: token,
userAgent: 'usage-statistics-tracker'
});
if (!token) {
console.warn('No GitHub token provided. Using unauthenticated requests (rate limited).');
}
// Step 1: Fetch basic repository data using GraphQL
let graphqlData: any = null;
try {
const graphqlClient = graphql.defaults({
headers: {
authorization: token ? `token ${token}` : undefined,
},
});
// Fetch basic repository data (without releases)
const basicResponse = await graphqlClient<GraphQLRepositoryBasicResponse>(REPOSITORY_BASIC_QUERY, {
owner,
name: repo
});
if (basicResponse.repository) {
graphqlData = basicResponse.repository;
}
} catch (error) {
console.warn(`Could not fetch GitHub GraphQL basic data for ${repository}:`, error);
}
// Step 2: Fetch releases data separately using GraphQL
let totalReleaseDownloads = 0;
let latestReleaseDownloads = 0;
let releaseCount = 0;
let downloadRange = [];
let latestRelease = null;
try {
const graphqlClient = graphql.defaults({
headers: {
authorization: token ? `token ${token}` : undefined,
},
});
// Fetch releases data
const releasesResponse = await graphqlClient<GraphQLReleasesResponse>(RELEASES_QUERY, {
owner,
name: repo,
first: 100
});
if (releasesResponse.repository?.releases?.nodes) {
const releases = releasesResponse.repository.releases.nodes.filter(Boolean);
releaseCount = releases.length;
for (const release of releases) {
let releaseDownloads = 0;
if (release?.releaseAssets?.nodes) {
for (const asset of release.releaseAssets.nodes) {
if (asset) {
releaseDownloads += asset.downloadCount || 0;
}
}
}
totalReleaseDownloads += releaseDownloads;
// Latest release is the first one in the list
if (release && release === releases[0]) {
latestReleaseDownloads = releaseDownloads;
latestRelease = release.tagName;
}
// Add to download range with proper date format for charts
if (release?.publishedAt) {
downloadRange.push({
day: release.publishedAt,
downloads: releaseDownloads,
tagName: release.tagName
});
}
}
}
} catch (error) {
console.warn(`Could not fetch GitHub GraphQL releases data for ${repository}:`, error);
}
// Fallback to REST API if GraphQL fails or for additional data
let restData: any = null;
try {
const { data: repoData } = await octokit.repos.get({
owner,
repo
});
restData = repoData;
} catch (error) {
console.warn(`Could not fetch GitHub REST data for ${repository}:`, error);
}
// Use the best available data (GraphQL preferred, REST as fallback)
const finalData = graphqlData || restData;
if (!finalData) {
throw new Error('Could not fetch repository data from either GraphQL or REST API');
}
// Get traffic statistics using REST API (requires authentication)
let viewsCount = 0;
let uniqueVisitors = 0;
let clonesCount = 0;
if (token) {
try {
// Get views data
const { data: viewsData } = await octokit.repos.getViews({
owner,
repo
});
if (viewsData) {
viewsCount = viewsData.count || 0;
uniqueVisitors = viewsData.uniques || 0;
}
// Get clones data
const { data: clonesData } = await octokit.repos.getClones({
owner,
repo
});
if (clonesData) {
clonesCount = clonesData.count || 0;
}
} catch (error) {
console.warn(`Could not fetch GitHub traffic data for ${repository}:`, error);
}
}
// Calculate repository age
let repositoryAge = 0;
if (finalData.createdAt) {
const created = new Date(finalData.createdAt);
const now = new Date();
repositoryAge = Math.floor((now.getTime() - created.getTime()) / (1000 * 60 * 60 * 24)); // days
}
// Calculate activity metrics
let lastActivity = 0;
if (finalData.pushedAt) {
const pushed = new Date(finalData.pushedAt);
const now = new Date();
lastActivity = Math.floor((now.getTime() - pushed.getTime()) / (1000 * 60 * 60 * 24)); // days
}
return {
platform: PlatformSettings.name,
name: repository,
timestamp: new Date().toISOString(),
metrics: {
stars: finalData.stargazerCount || finalData.stargazers_count || 0,
forks: finalData.forkCount || finalData.forks_count || 0,
watchers: finalData.watchers?.totalCount || finalData.watchers_count || 0,
totalIssues: finalData.openIssues?.totalCount + finalData.closedIssues?.totalCount || 0,
openIssues: finalData.openIssues?.totalCount || 0,
closedIssues: finalData.closedIssues?.totalCount || 0,
language: finalData.primaryLanguage?.name || finalData.language || null,
size: finalData.diskUsage || finalData.size || null,
repositoryAge,
lastActivity,
releaseCount,
totalReleaseDownloads,
latestReleaseDownloads,
viewsCount,
uniqueVisitors,
latestRelease,
clonesCount,
topics: finalData.repositoryTopics?.nodes?.length || finalData.topics?.length || 0,
license: finalData.licenseInfo?.name || finalData.license?.name || null,
defaultBranch: finalData.defaultBranchRef?.name || finalData.default_branch || null,
downloadsTotal: totalReleaseDownloads || 0,
downloadRange,
}
};
} catch (error) {
return {
platform: PlatformSettings.name,
name: repository,
timestamp: new Date().toISOString(),
metrics: {},
error: error instanceof Error ? error.message : String(error)
};
}
}
export async function collectGithubBatch(repositories: string[]): Promise<MetricResult[]> {
const results: Promise<MetricResult>[] = [];
for (const repo of repositories) {
results.push(collectGithub(repo));
}
return Promise.all(results);
}

154
src/collectors/npm.ts Normal file
View File

@@ -0,0 +1,154 @@
/**
* NPM package statistics collector with enhanced metrics
*/
import type { MetricResult } from './types';
const PlatformSettings = {
name: 'NPM',
}
interface NpmPackageInfo {
name: string;
version: string;
description?: string;
homepage?: string;
repository?: { url: string };
maintainers?: Array<{ name: string; email: string }>;
'dist-tags'?: Record<string, string>;
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
peerDependencies?: Record<string, string>;
time?: Record<string, string>;
versions?: Record<string, any>;
}
interface NpmDownloadStats {
downloads: number;
start: string;
end: string;
package: string;
}
const BASE_URL = 'https://api.npmjs.org/downloads/range';
const CHUNK_DAYS = 540; // 18 months max per request
const START_DATE = new Date('2015-01-10'); // Earliest NPM data
function formatDate(date: Date): string {
return date.toISOString().split('T')[0];
}
function addDays(date: Date, days: number): Date {
const result = new Date(date);
result.setDate(result.getDate() + days);
return result;
}
async function fetchChunk(start: Date, end: Date, packageName: string): Promise<{ day: string; downloads: number }[]> {
const url = `${BASE_URL}/${formatDate(start)}:${formatDate(end)}/${packageName}`;
const res = await fetch(url);
if (!res.ok) {
throw new Error(`Failed to fetch data: ${res.status} ${res.statusText}`);
}
const json = await res.json();
return json.downloads;
}
async function getFullDownloadHistory(packageName: string, startDate: Date): Promise<{ day: string; downloads: number }[]> {
const today = new Date();
let currentStart = new Date(startDate);
let allDownloads: { day: string; downloads: number }[] = [];
while (currentStart < today) {
const currentEnd = addDays(currentStart, CHUNK_DAYS - 1);
const end = currentEnd > today ? today : currentEnd;
console.log(`Fetching ${formatDate(currentStart)} to ${formatDate(end)}...`);
const chunk = await fetchChunk(currentStart, end, packageName);
allDownloads = allDownloads.concat(chunk);
currentStart = addDays(end, 1); // move to next chunk
}
return Array.from(new Set(allDownloads));
}
export async function collectNpm(packageName: string): Promise<MetricResult> {
try {
// Get package info from npm registry
const packageUrl = `https://registry.npmjs.org/${packageName}`;
const packageResponse = await fetch(packageUrl);
const packageData: NpmPackageInfo = await packageResponse.json();
// Get download statistics
let downloadsMonthly
let downloadsWeekly
let downloadsDaily
try {
// Monthly downloads
const monthlyUrl = `https://api.npmjs.org/downloads/point/last-month/${packageName}`;
const monthlyResponse = await fetch(monthlyUrl);
const monthlyData: NpmDownloadStats = await monthlyResponse.json();
downloadsMonthly = monthlyData.downloads || null;
} catch (error) {
console.warn(`Could not fetch NPM monthly downloads for ${packageName}:`, error);
}
try {
// Weekly downloads
const weeklyUrl = `https://api.npmjs.org/downloads/point/last-week/${packageName}`;
const weeklyResponse = await fetch(weeklyUrl);
const weeklyData: NpmDownloadStats = await weeklyResponse.json();
downloadsWeekly = weeklyData.downloads || null;
} catch (error) {
console.warn(`Could not fetch NPM weekly downloads for ${packageName}:`, error);
}
try {
// Daily downloads
const dailyUrl = `https://api.npmjs.org/downloads/point/last-day/${packageName}`;
const dailyResponse = await fetch(dailyUrl);
const dailyData: NpmDownloadStats = await dailyResponse.json();
downloadsDaily = dailyData.downloads || null;
} catch (error) {
console.warn(`Could not fetch NPM daily downloads for ${packageName}:`, error);
}
const downloadsRange = await getFullDownloadHistory(packageName, new Date(packageData.time?.created || START_DATE))
const downloadsTotal = downloadsRange.reduce((acc, curr) => acc + curr.downloads, 0)
return {
platform: PlatformSettings.name,
name: packageName,
timestamp: new Date().toISOString(),
metrics: {
downloadsTotal,
downloadsMonthly,
downloadsWeekly,
downloadsDaily,
downloadsRange,
}
};
} catch (error) {
return {
platform: PlatformSettings.name,
name: packageName,
timestamp: new Date().toISOString(),
metrics: {},
error: error instanceof Error ? error.message : String(error)
};
}
}
export async function collectNpmBatch(packageNames: string[]): Promise<MetricResult[]> {
const resultPromises: Promise<MetricResult>[] = []
for (const packageName of packageNames) {
resultPromises.push(collectNpm(packageName))
}
return Promise.all(resultPromises)
}

View File

@@ -0,0 +1,363 @@
/**
* PowerShell Gallery module statistics collector with enhanced metrics
*/
import type { MetricResult } from './types';
import { XMLParser } from 'fast-xml-parser';
const PlatformSettings = {
name: 'PowerShell',
}
const BASE_URL = 'https://www.powershellgallery.com/api/v2/';
const parser = new XMLParser({
ignoreAttributes: false,
attributeNamePrefix: '',
});
export interface PowerShellGalleryEntryArray {
"?xml": {
version: string;
encoding: string;
};
feed: {
id: string;
title: string;
updated: string;
link: {
rel: string;
href: string;
};
entry: Entry[];
};
}
export interface PowerShellGalleryEntry {
'?xml': {
version: string;
encoding: string;
};
entry: Entry;
}
export interface Entry {
id: string;
category: {
term: string;
scheme: string;
};
link: Array<{
rel: string;
href: string;
}>;
title: {
'#text': string;
type: string;
};
updated: string;
author: {
name: string;
};
content: {
type: string;
src: string;
};
'm:properties': MProperties;
// Namespace declarations
'xml:base': string;
xmlns: string;
'xmlns:d': string;
'xmlns:m': string;
'xmlns:georss': string;
'xmlns:gml': string;
}
export interface MProperties {
'd:Id': string;
'd:Version': string;
'd:NormalizedVersion': string;
'd:Authors': string;
'd:Copyright': string;
'd:Created': EdmDateTime;
'd:Dependencies': string;
'd:Description': string;
'd:DownloadCount': EdmInt32;
'd:GalleryDetailsUrl': string;
'd:IconUrl'?: EdmNull;
'd:IsLatestVersion': EdmBoolean;
'd:IsAbsoluteLatestVersion': EdmBoolean;
'd:IsPrerelease': EdmBoolean;
'd:Language'?: EdmNull;
'd:LastUpdated': EdmDateTime;
'd:Published': EdmDateTime;
'd:PackageHash': string;
'd:PackageHashAlgorithm': string;
'd:PackageSize': EdmInt64;
'd:ProjectUrl'?: EdmNull;
'd:ReportAbuseUrl': string;
'd:ReleaseNotes'?: EdmNull;
'd:RequireLicenseAcceptance': EdmBoolean;
'd:Summary'?: EdmNull;
'd:Tags': string;
'd:Title'?: EdmNull;
'd:VersionDownloadCount': EdmInt32;
'd:MinClientVersion'?: EdmNull;
'd:LastEdited'?: EdmNull;
'd:LicenseUrl'?: EdmNull;
'd:LicenseNames'?: EdmNull;
'd:LicenseReportUrl'?: EdmNull;
'd:ItemType': string;
'd:FileList': string;
'd:GUID': string;
'd:PowerShellVersion': number;
'd:PowerShellHostVersion'?: EdmNull;
'd:DotNetFrameworkVersion'?: EdmNull;
'd:CLRVersion'?: EdmNull;
'd:ProcessorArchitecture'?: EdmNull;
'd:CompanyName': string;
'd:Owners': string;
}
export interface EdmDateTime {
'#text': string;
'm:type': 'Edm.DateTime';
}
export interface EdmInt32 {
'#text': number;
'm:type': 'Edm.Int32';
}
export interface EdmInt64 {
'#text': number;
'm:type': 'Edm.Int64';
}
export interface EdmBoolean {
'#text': boolean;
'm:type': 'Edm.Boolean';
}
export interface EdmNull {
'm:null': 'true';
}
type ParsedModuleEntry = {
id: string;
name: string;
version: string;
normalizedVersion: string;
authors: string;
description: string;
downloadCount: number;
versionDownloadCount: number;
published: Date;
lastUpdated: Date;
created: Date;
isLatest: boolean;
isPrerelease: boolean;
projectUrl?: string | null;
reportAbuseUrl: string;
galleryDetailsUrl: string;
packageSize: number;
companyName: string;
owners: string;
};
function parsePowerShellGalleryEntry(entry: Entry): ParsedModuleEntry {
const props = entry["m:properties"]
const getText = (field: any): string =>
field?.["#text"];
const isTrue = (field: any): boolean =>
field?.["#text"] === true;
const getNumber = (field: any): number => field?.["#text"]
const getDate = (field: any): Date => {
const dateText = getText(field);
if (!dateText || dateText === '') {
return new Date(0); // Return epoch date for invalid dates
}
return new Date(dateText);
};
return {
id: entry.id,
name: props["d:Id"],
version: props["d:Version"],
normalizedVersion: props["d:NormalizedVersion"],
authors: props["d:Authors"],
description: props["d:Description"],
downloadCount: getNumber(props["d:DownloadCount"]),
versionDownloadCount: getNumber(props["d:VersionDownloadCount"]),
published: getDate(props["d:Published"]),
lastUpdated: getDate(props["d:LastUpdated"]),
created: getDate(props["d:Created"]),
isLatest: isTrue(props["d:IsLatestVersion"]),
isPrerelease: isTrue(props["d:IsPrerelease"]),
projectUrl: getText(props["d:ProjectUrl"]) ?? undefined,
reportAbuseUrl: props["d:ReportAbuseUrl"],
galleryDetailsUrl: props["d:GalleryDetailsUrl"],
packageSize: getNumber(props["d:PackageSize"]),
companyName: props["d:CompanyName"],
owners: props["d:Owners"],
};
}
/**
* Fetches all versions of a package.
* Equivalent to: FindPackagesById()?id='PackageName'
*/
export async function findPackagesById(id: string) {
const url = `${BASE_URL}FindPackagesById()?id='${encodeURIComponent(id)}'`;
const res = await fetch(url);
const xml = await res.text();
const json = parser.parse(xml) as PowerShellGalleryEntryArray;
return json.feed.entry ?? [];
}
/**
* Fetches metadata for a specific version of a package.
* Equivalent to: Packages(Id='Name',Version='x.y.z')
*/
export async function getPackageVersionInfo(id: string, version: string) {
const url = `${BASE_URL}Packages(Id='${encodeURIComponent(id)}',Version='${encodeURIComponent(version)}')`;
const res = await fetch(url);
const xml = await res.text();
const json = parser.parse(xml) as PowerShellGalleryEntry;
return json.entry
}
/**
* Searches the PowerShell Gallery with a search term.
* Equivalent to: Search()?searchTerm='term'&includePrerelease=false
*/
export async function searchPackages(searchTerm: string, includePrerelease = false) {
const url = `${BASE_URL}Search()?searchTerm='${encodeURIComponent(
searchTerm
)}'&includePrerelease=${includePrerelease.toString()}`;
const res = await fetch(url);
const xml = await res.text();
const json = parser.parse(xml);
return json.feed?.entry ?? [];
}
/**
* Sum total download count for all versions of a package.
*/
export async function getTotalDownloadCount(id: string): Promise<number> {
const entries = await findPackagesById(id);
const versions = Array.isArray(entries) ? entries : [entries];
return versions.reduce((sum, entry) => {
const count = (entry as any)['m:properties']?.['d:DownloadCount']?.['#text']
return sum + count;
}, 0);
}
export async function collectPowerShell(moduleName: string): Promise<MetricResult> {
try {
// Get all versions of the package
const allVersions = await findPackagesById(moduleName);
if (!allVersions || allVersions.length === 0) {
throw new Error(`Module ${moduleName} not found`);
}
const versions: ParsedModuleEntry[] = []
for (const version of allVersions) {
const parsedVersion = parsePowerShellGalleryEntry(version)
versions.push(parsedVersion)
}
// Sort versions by published date (newest first)
const sortedVersions = versions.sort((a, b) => b.published.getTime() - a.published.getTime())
let downloadsTotal = 0;
let latestVersionDownloads = 0;
let downloadRange: Array<{day: string, downloads: number, version: string}> = [];
let latestVersion = '';
let latestVersionDate = '';
// Process each version
for (const version of sortedVersions) {
// Use Created date if Published date is invalid (1900-01-01)
const effectiveDate = version.published.getTime() === new Date('1900-01-01T00:00:00').getTime()
? version.created
: version.published;
// Skip versions with invalid dates
if (effectiveDate.getTime() === 0) {
continue;
}
downloadsTotal += version.versionDownloadCount;
// Track latest version downloads
if (version.isLatest) {
latestVersionDownloads = version.versionDownloadCount;
latestVersion = version.version;
latestVersionDate = effectiveDate.toISOString();
}
const rangeEntry = {
day: effectiveDate.toISOString(),
downloads: version.versionDownloadCount,
version: version.version
}
// Add to download range for charts
downloadRange.push(rangeEntry);
}
// Get latest version metadata
const latestModuleData = sortedVersions[0];
const result: MetricResult = {
platform: PlatformSettings.name,
name: moduleName,
timestamp: new Date().toISOString(),
metrics: {
downloadsTotal,
downloadsRange: downloadRange,
latestVersionDownloads,
latestVersion,
latestVersionDate,
versionCount: versions.length,
lastUpdated: latestModuleData.lastUpdated.toISOString(),
// Additional metadata
authors: latestModuleData.authors,
description: latestModuleData.description,
projectUrl: latestModuleData.projectUrl,
packageSize: latestModuleData.packageSize,
companyName: latestModuleData.companyName,
owners: latestModuleData.owners,
}
};
return result;
} catch (error) {
console.warn('Error collecting PowerShell module:', error);
return {
platform: PlatformSettings.name,
name: moduleName,
timestamp: new Date().toISOString(),
error: error instanceof Error ? error.message : String(error),
};
}
}
export async function collectPowerShellBatch(moduleNames: string[]): Promise<MetricResult[]> {
const resultPromises: Promise<MetricResult>[] = []
for (const moduleName of moduleNames) {
resultPromises.push(collectPowerShell(moduleName))
}
return Promise.all(resultPromises)
}

193
src/collectors/pypi.ts Normal file
View File

@@ -0,0 +1,193 @@
/**
* PyPI package statistics collector using external PyPI Stats API
*/
import type { MetricResult } from './types';
const PlatformSettings = {
name: 'PyPI',
}
interface PyPIPackageInfo {
info: {
name: string;
version: string;
summary?: string;
description?: string;
home_page?: string;
author?: string;
author_email?: string;
license?: string;
requires_python?: string;
project_urls?: Record<string, string>;
};
releases: Record<string, Array<{
filename: string;
url: string;
size: number;
upload_time: string;
file_type: string;
python_version?: string;
}>>;
}
// (no BigQuery historical metrics; all data comes from the external API)
// External PyPI Stats API base URL
const PYPI_STATS_BASE_URL = process.env.PYPI_STATS_BASE_URL || 'https://pypistats.dev'
function normalizePackageName(name: string) {
return name.replace(/[._]/g, '-').toLowerCase()
}
async function fetchJson<T>(url: string): Promise<T> {
const res = await fetch(url)
if (!res.ok) throw new Error(`Request failed ${res.status}: ${url}`)
return res.json() as Promise<T>
}
export async function collectPypi(packageName: string): Promise<MetricResult> {
const normalized = normalizePackageName(packageName)
try {
// Package metadata
const packageDataPromise = fetchJson<PyPIPackageInfo>(`https://pypi.org/pypi/${normalized}/json`)
// External API calls
type RecentResp = { package: string; type: string; data: { last_day?: number; last_week?: number; last_month?: number } }
type SeriesResp = { package: string; type: string; data: { date: string; category: string; downloads: number }[] }
type SummaryResp = { package: string; type: string; totals: { overall: number; system?: Record<string, number>; python_major?: Record<string, number>; python_minor?: Record<string, number> } }
const recentPromise = fetchJson<RecentResp>(`${PYPI_STATS_BASE_URL}/api/packages/${normalized}/recent`)
const summaryPromise = fetchJson<SummaryResp>(`${PYPI_STATS_BASE_URL}/api/packages/${normalized}/summary`)
const overallPromise = fetchJson<SeriesResp>(`${PYPI_STATS_BASE_URL}/api/packages/${normalized}/overall`)
const pythonMajorPromise = fetchJson<SeriesResp>(`${PYPI_STATS_BASE_URL}/api/packages/${normalized}/python_major`)
const pythonMinorPromise = fetchJson<SeriesResp>(`${PYPI_STATS_BASE_URL}/api/packages/${normalized}/python_minor`)
const systemPromise = fetchJson<SeriesResp>(`${PYPI_STATS_BASE_URL}/api/packages/${normalized}/system`)
const installerPromise = fetchJson<SeriesResp>(`${PYPI_STATS_BASE_URL}/api/packages/${normalized}/installer`)
type ChartResp = { package: string; type: string; chartType: string; title: string; labels: string[]; datasets: Array<{ label: string; data: number[] }> }
const overallChartPromise = fetchJson<ChartResp>(`${PYPI_STATS_BASE_URL}/api/packages/${normalized}/chart/overall?format=json`)
const pythonMajorChartPromise = fetchJson<ChartResp>(`${PYPI_STATS_BASE_URL}/api/packages/${normalized}/chart/python_major?format=json`)
const pythonMinorChartPromise = fetchJson<ChartResp>(`${PYPI_STATS_BASE_URL}/api/packages/${normalized}/chart/python_minor?format=json`)
const systemChartPromise = fetchJson<ChartResp>(`${PYPI_STATS_BASE_URL}/api/packages/${normalized}/chart/system?format=json`)
const installerChartPromise = fetchJson<ChartResp>(`${PYPI_STATS_BASE_URL}/api/packages/${normalized}/chart/installer?format=json`)
const [packageData, recent, summary, overall, pythonMajor, pythonMinor, system, installer, overallChart, pythonMajorChart, pythonMinorChart, systemChart, installerChart] = await Promise.all([
packageDataPromise,
recentPromise,
summaryPromise,
overallPromise,
pythonMajorPromise,
pythonMinorPromise,
systemPromise,
installerPromise,
overallChartPromise,
pythonMajorChartPromise,
pythonMinorChartPromise,
systemChartPromise,
installerChartPromise,
])
// All time-series and breakdowns are provided by the external API
const overallSeries = (overall.data || []).filter(p => p.category === 'without_mirrors')
const systemBreakdown = summary.totals?.system || null
const pythonVersionBreakdown = summary.totals?.python_major
? Object.fromEntries(Object.entries(summary.totals.python_major).filter(([k]) => /^\d+$/.test(k)).map(([k, v]) => [`python${k}`, v as number]))
: null
const pythonMinorBreakdown = summary.totals?.python_minor
? Object.fromEntries(Object.entries(summary.totals.python_minor).filter(([k]) => /^\d+(?:\.\d+)?$/.test(k)).map(([k, v]) => [`python${k}`, v as number]))
: null
// Derive popular system and installer from totals/series
let popularSystem: string | undefined
if (systemBreakdown && Object.keys(systemBreakdown).length > 0) {
popularSystem = Object.entries(systemBreakdown).sort((a, b) => (b[1] as number) - (a[1] as number))[0]?.[0]
} else if (system.data && system.data.length > 0) {
const totals: Record<string, number> = {}
for (const p of system.data) totals[p.category] = (totals[p.category] || 0) + p.downloads
popularSystem = Object.entries(totals).sort((a, b) => b[1] - a[1])[0]?.[0]
}
let popularInstaller: string | undefined
if (installer.data && installer.data.length > 0) {
const totals: Record<string, number> = {}
for (const p of installer.data) totals[p.category] = (totals[p.category] || 0) + p.downloads
popularInstaller = Object.entries(totals).sort((a, b) => b[1] - a[1])[0]?.[0]
}
// Latest release date for current version
let latestReleaseDate: string | undefined
try {
const currentVersion = packageData.info?.version
const files = currentVersion ? (packageData.releases?.[currentVersion] || []) : []
const latestUpload = files.reduce<string | undefined>((max, f) => {
const t = f.upload_time
if (!t) return max
if (!max) return t
return new Date(t) > new Date(max) ? t : max
}, undefined)
if (latestUpload) {
const d = new Date(latestUpload)
if (!isNaN(d.getTime())) latestReleaseDate = d.toISOString().slice(0, 10)
}
} catch {}
return {
platform: PlatformSettings.name,
name: packageName,
timestamp: new Date().toISOString(),
metrics: {
downloadsTotal: summary.totals?.overall,
downloadsMonthly: recent.data?.last_month,
downloadsWeekly: recent.data?.last_week,
downloadsDaily: recent.data?.last_day,
version: packageData.info?.version,
latestReleaseDate,
description: packageData.info?.summary,
homepage: packageData.info?.home_page,
author: packageData.info?.author,
license: packageData.info?.license,
requiresPython: packageData.info?.requires_python,
releases: Object.keys(packageData.releases || {}).length,
downloadsRange: overallSeries.map(p => ({ day: p.date, downloads: p.downloads })),
overallSeries,
pythonMajorSeries: (pythonMajor.data || []).filter(p => p.category?.toLowerCase?.() !== 'unknown'),
pythonMinorSeries: (pythonMinor.data || []).filter(p => p.category?.toLowerCase?.() !== 'unknown'),
systemSeries: system.data || [],
installerSeries: installer.data || [],
popularSystem,
popularInstaller,
// Server-prepared chart JSON (preferred for rendering)
overallChart,
pythonMajorChart: { ...pythonMajorChart, datasets: (pythonMajorChart.datasets || []).filter(ds => !/unknown/i.test(ds.label)) },
pythonMinorChart: { ...pythonMinorChart, datasets: (pythonMinorChart.datasets || []).filter(ds => !/unknown/i.test(ds.label)) },
systemChart,
installerChart,
pythonVersionBreakdown,
pythonMinorBreakdown,
systemBreakdown,
}
};
} catch (error) {
return {
platform: PlatformSettings.name,
name: packageName,
timestamp: new Date().toISOString(),
metrics: {},
error: error instanceof Error ? error.message : String(error)
};
}
}
export async function collectPypiBatch(packageNames: string[]): Promise<MetricResult[]> {
const results: Promise<MetricResult>[] = []
for (const packageName of packageNames) {
results.push(collectPypi(packageName))
}
return Promise.all(results)
}

44
src/collectors/types.ts Normal file
View File

@@ -0,0 +1,44 @@
/**
* Core types for the simplified usage statistics system
*/
export interface MetricResult {
platform: string;
name: string;
timestamp: string;
metrics?: {
stars?: number;
forks?: number;
watchers?: number;
openIssues?: number;
totalReleaseDownloads?: number;
downloadsTotal?: number;
downloadsRange?: {
day: string;
downloads: number;
}[];
} & Record<string, any>;
error?: string;
}
export interface MetricCollector {
collect(source: string): Promise<MetricResult>;
collectBatch?(sources: string[]): Promise<MetricResult[]>;
}
export interface CollectorConfig {
collect: MetricCollector;
batched?: boolean;
}
export interface SourceConfig {
platform: string;
name: string;
options?: Record<string, any>;
}
export interface CollectionResult {
results: MetricResult[];
summary: string;
timestamp: string;
}

View File

@@ -1,52 +0,0 @@
import { describe, it, expect, beforeEach } from "bun:test";
import GitHubTracker from "./trackers/github";
describe("GitHubTracker", () => {
let tracker: GitHubTracker;
beforeEach(() => {
// Use a test token or no token for testing
tracker = new GitHubTracker(process.env.GITHUB_TOKEN || undefined);
});
describe("Rate Limiting", () => {
it("should handle rate limiting gracefully", async () => {
// Test with a popular repository that might hit rate limits
const stats = await tracker.getDownloadStats('microsoft/vscode');
// Should return an array (even if empty due to rate limiting)
expect(Array.isArray(stats)).toBe(true);
}, 15000); // 15 second timeout for rate limit handling
it("should get package info", async () => {
try {
const info = await tracker.getPackageInfo('microsoft/vscode');
expect(info).toBeDefined();
expect(info.name).toBe('vscode');
expect(info.full_name).toBe('microsoft/vscode');
} catch (error) {
// If rate limited, that's expected behavior
console.log('Rate limited during test (expected):', error);
expect(error).toBeDefined();
}
}, 15000);
it("should get latest version", async () => {
try {
const version = await tracker.getLatestVersion('microsoft/vscode');
// Should return a version string or null
expect(version === null || typeof version === 'string').toBe(true);
} catch (error) {
// If rate limited, that's expected behavior
console.log('Rate limited during test (expected):', error);
expect(error).toBeDefined();
}
}, 15000);
});
describe("Configuration", () => {
it("should have correct name", () => {
expect(tracker.name).toBe('github');
});
});
});

View File

@@ -1,104 +0,0 @@
import { describe, it, expect, beforeEach } from "bun:test";
import { UsageStatisticsManager } from "./index";
import type { TrackingConfig } from "./types";
describe("UsageStatisticsManager", () => {
let manager: UsageStatisticsManager;
beforeEach(() => {
const config: TrackingConfig = {
enableLogging: false, // Disable logging for tests
updateInterval: 60 * 60 * 1000,
npmPackages: ['lodash'], // Reduce to single package for faster tests
githubRepos: ['microsoft/vscode'], // Reduce to single repo
pythonPackages: ['requests'], // Reduce to single package
homebrewPackages: ['git'], // Reduce to single package
powershellModules: ['PowerShellGet'],
postmanCollections: [],
goModules: ['github.com/go-chi/chi'] // Reduce to single module
};
manager = new UsageStatisticsManager(config);
});
describe("generateComprehensiveReport", () => {
it("should generate a report with aggregated stats", async () => {
const report = await manager.generateComprehensiveReport();
expect(report).toBeDefined();
expect(report.totalDownloads).toBeGreaterThanOrEqual(0);
expect(report.uniquePackages).toBeGreaterThanOrEqual(0);
expect(Array.isArray(report.platforms)).toBe(true);
expect(Array.isArray(report.topPackages)).toBe(true);
expect(report.topPackages.length).toBeGreaterThan(0);
expect(typeof report.platformBreakdown).toBe('object');
}, 10000); // 10 second timeout
});
describe("getPlatformReport", () => {
it("should generate a report for a specific platform", async () => {
const report = await manager.getPlatformReport('npm');
expect(report).toBeDefined();
expect(report.totalDownloads).toBeGreaterThanOrEqual(0);
expect(Array.isArray(report.platforms)).toBe(true);
});
});
describe("exportReport", () => {
it("should export JSON report", async () => {
const jsonReport = await manager.exportReport('json');
expect(jsonReport).toBeDefined();
expect(typeof jsonReport).toBe('string');
// Should be valid JSON
const parsed = JSON.parse(jsonReport);
expect(parsed).toBeDefined();
}, 10000); // 10 second timeout
it("should export CSV report", async () => {
const csvReport = await manager.exportReport('csv');
expect(csvReport).toBeDefined();
expect(typeof csvReport).toBe('string');
expect(csvReport.includes('Platform,Package,Downloads')).toBe(true);
}, 10000); // 10 second timeout
});
describe("getLastUpdateTime", () => {
it("should return null initially", () => {
const lastUpdate = manager.getLastUpdateTime();
expect(lastUpdate).toBeNull();
});
it("should return update time after generating report", async () => {
await manager.generateComprehensiveReport();
const lastUpdate = manager.getLastUpdateTime();
expect(lastUpdate).not.toBeNull();
expect(lastUpdate).toBeInstanceOf(Date);
});
});
});
describe("Configuration", () => {
it("should create valid config", () => {
const config: TrackingConfig = {
enableLogging: true,
updateInterval: 60 * 60 * 1000,
npmPackages: ['lodash', 'axios'],
githubRepos: ['microsoft/vscode'],
pythonPackages: ['requests'],
homebrewPackages: ['git'],
powershellModules: ['PowerShellGet'],
postmanCollections: [],
goModules: ['github.com/gin-gonic/gin']
};
expect(config).toBeDefined();
expect(config.enableLogging).toBe(true);
expect(config.updateInterval).toBe(60 * 60 * 1000); // 1 hour
expect(config.npmPackages).toContain('lodash');
expect(config.githubRepos).toContain('microsoft/vscode');
});
});

View File

@@ -1,262 +0,0 @@
#!/usr/bin/env bun
import type { TrackingConfig } from './types';
import { DownloadStatsAggregator } from './aggregator';
import type { AggregatedStats } from './aggregator';
import { promises as fs } from 'fs';
import path from 'path';
import { execSync } from 'node:child_process';
/**
* Usage Statistics Tracker
*
* Tracks download statistics across multiple platforms:
* - NPM packages
* - PyPI packages
* - Homebrew formulas
* - PowerShell modules
* - Postman collections
* - Go modules
* - GitHub releases
*/
const STATS_FILE = 'stats.json';
const README_FILE = 'README.md';
const STATS_MARKER_START = '<!-- USAGE_STATS_START -->';
const STATS_MARKER_END = '<!-- USAGE_STATS_END -->';
async function writeStatsFile(stats: AggregatedStats, filePath = STATS_FILE) {
await fs.writeFile(filePath, JSON.stringify(stats, null, 2));
console.log(`📄 Stats written to ${filePath}`);
}
async function updateReadmeWithStats(stats: AggregatedStats, readmePath = README_FILE) {
try {
const readmeContent = await fs.readFile(readmePath, 'utf-8');
const statsSection = `
## 📊 Usage Statistics
Last updated: ${new Date().toISOString()}
### Summary
- **Total Downloads**: ${stats.totalDownloads.toLocaleString()}
- **Unique Packages**: ${stats.uniquePackages}
- **Platforms Tracked**: ${stats.platforms.join(', ')}
### Top Packages
${stats.topPackages.map((pkg, index) =>
`${index + 1}. **${pkg.name}** (${pkg.platform}) - ${pkg.downloads.toLocaleString()} downloads`
).join('\n')}
`;
const startMarker = readmeContent.indexOf(STATS_MARKER_START);
const endMarker = readmeContent.indexOf(STATS_MARKER_END);
if (startMarker !== -1 && endMarker !== -1) {
const beforeStats = readmeContent.substring(0, startMarker + STATS_MARKER_START.length);
const afterStats = readmeContent.substring(endMarker);
const updatedContent = beforeStats + statsSection + afterStats;
await fs.writeFile(readmePath, updatedContent);
console.log(`📝 README updated with stats`);
} else {
console.warn(`⚠️ Stats markers not found in README. Please add ${STATS_MARKER_START} and ${STATS_MARKER_END} markers.`);
}
} catch (error) {
console.error('❌ Error updating README:', error);
}
}
async function gitCommitAndPush(files: string[], message: string) {
execSync(`git config user.name "github-actions[bot]"`);
execSync(`git config user.email "github-actions[bot]@users.noreply.github.com"`);
execSync(`git add ${files.join(' ')}`);
execSync(`git commit -m "${message}" || echo 'No changes to commit.'`);
execSync(`git push`);
}
class UsageStatisticsManager {
private aggregator: DownloadStatsAggregator;
private lastUpdateTime: Date | null = null;
constructor(config: TrackingConfig) {
this.aggregator = new DownloadStatsAggregator(config);
}
async generateComprehensiveReport(): Promise<AggregatedStats> {
console.log('📊 Generating comprehensive usage statistics report...\n');
const stats = await this.aggregator.collectAllStats();
const report = this.aggregator.aggregateStats(stats);
this.lastUpdateTime = new Date();
return report;
}
async getPlatformReport(platform: string): Promise<AggregatedStats> {
console.log(`📊 Generating ${platform} platform report...\n`);
const stats = await this.aggregator.getPlatformStats(platform);
const report = this.aggregator.aggregateStats(stats);
this.lastUpdateTime = new Date();
return report;
}
async exportReport(format: 'json' | 'csv' = 'json'): Promise<string> {
const report = await this.generateComprehensiveReport();
if (format === 'csv') {
const csvHeader = 'Platform,Package,Downloads\n';
const csvRows = report.topPackages.map(pkg =>
`${pkg.platform},${pkg.name},${pkg.downloads}`
).join('\n');
return csvHeader + csvRows;
}
return JSON.stringify(report, null, 2);
}
getLastUpdateTime(): Date | null {
return this.lastUpdateTime;
}
async displayReport(report: AggregatedStats) {
console.log('📊 Usage Statistics Summary');
console.log('==================================================\n');
// Overall Summary
console.log('📈 Overall Summary:');
console.log(`Total Downloads: ${report.totalDownloads.toLocaleString()}`);
console.log(`Unique Packages: ${report.uniquePackages}`);
console.log(`Platforms Tracked: ${report.platforms.join(', ')}\n`);
// Platform Totals
console.log('🏗️ Platform Totals:');
for (const [platform, data] of Object.entries(report.platformBreakdown)) {
console.log(` ${platform.toUpperCase()}: ${data.totalDownloads.toLocaleString()} downloads (${data.uniquePackages} packages)`);
}
console.log('');
// Package Rankings
console.log('🏆 Package Rankings:');
report.topPackages.forEach((pkg, index) => {
console.log(` ${index + 1}. ${pkg.name} (${pkg.platform}) - ${pkg.downloads.toLocaleString()} downloads`);
});
console.log('==================================================');
}
async generatePreviewReport(): Promise<AggregatedStats> {
console.log('🎭 Generating preview report with mock data...\n');
// Create mock data for preview
const mockStats = [
{
platform: 'npm',
packageName: 'lodash',
downloadCount: 1500000,
metadata: { version: '4.17.21' }
},
{
platform: 'npm',
packageName: 'axios',
downloadCount: 800000,
metadata: { version: '1.6.0' }
},
{
platform: 'github',
packageName: 'microsoft/vscode',
downloadCount: 500000,
metadata: { release: 'v1.85.0' }
},
{
platform: 'pypi',
packageName: 'requests',
downloadCount: 300000,
metadata: { version: '2.31.0' }
},
{
platform: 'homebrew',
packageName: 'git',
downloadCount: 250000,
metadata: { version: '2.43.0' }
},
{
platform: 'powershell',
packageName: 'PowerShellGet',
downloadCount: 120000,
metadata: { version: '2.2.5' }
},
{
platform: 'postman',
packageName: 'Postman Collection',
downloadCount: 75000,
metadata: { collectionId: '12345' }
},
{
platform: 'go',
packageName: 'github.com/gin-gonic/gin',
downloadCount: 45000,
metadata: { version: 'v1.9.1' }
}
];
const report = this.aggregator.aggregateStats(mockStats);
this.lastUpdateTime = new Date();
return report;
}
}
async function main() {
console.log('🚀 Usage Statistics Tracker Starting...\n');
// Create a default configuration for CLI usage
const defaultConfig: TrackingConfig = {
enableLogging: true,
updateInterval: 60 * 60 * 1000, // 1 hour
npmPackages: ['lodash', 'axios'],
githubRepos: ['microsoft/vscode', 'facebook/react'],
pythonPackages: ['requests', 'numpy'],
homebrewPackages: ['git', 'node'],
powershellModules: ['PowerShellGet'],
postmanCollections: [],
goModules: ['github.com/gin-gonic/gin', 'github.com/go-chi/chi']
};
const manager = new UsageStatisticsManager(defaultConfig);
try {
// Check for preview mode
const isPreview = process.argv.includes('--preview') || process.argv.includes('-p');
let report: AggregatedStats;
if (isPreview) {
report = await manager.generatePreviewReport();
} else {
report = await manager.generateComprehensiveReport();
}
await manager.displayReport(report);
const jsonReport = await manager.exportReport('json');
console.log('\n📄 JSON Report:');
console.log(jsonReport);
// Only write files and commit if not in preview mode and running in GitHub Actions
if (!isPreview && (process.env.GITHUB_ACTIONS === 'true' || process.argv.includes('--action'))) {
await writeStatsFile(report);
await updateReadmeWithStats(report);
await gitCommitAndPush([STATS_FILE, README_FILE], 'chore: update usage statistics [skip ci]');
console.log('✅ Stats written, README updated, and changes pushed.');
} else if (isPreview) {
console.log('\n🎭 Preview mode - no files written or commits made');
}
console.log('\n✅ Script completed successfully!');
} catch (error) {
console.error('❌ Error during execution:', error);
process.exit(1);
}
}
// Run the main function if this file is executed directly
if (import.meta.main) {
main();
}
export { UsageStatisticsManager };

321
src/summaries/github.ts Normal file
View File

@@ -0,0 +1,321 @@
import { mkdirSync, writeFileSync } from "fs"
import type { MetricResult } from "../collectors/types"
import { Chart, registerables } from 'chart.js';
import { Canvas } from 'skia-canvas';
import { semver } from "bun";
// Register all Chart.js controllers
Chart.register(...registerables);
export function formatGitHubSummary(summary: string, platformMetrics: MetricResult[]): string {
let totalStars = 0
let totalForks = 0
let totalWatchers = 0
let totalIssues = 0
let totalOpenIssues = 0
let totalClosedIssues = 0
let totalDownloads = 0
let totalReleases = 0
summary += `| Repository | Stars | Forks | Watchers | Open Issues | Closed Issues | Total Issues | Release Downloads | Releases | Latest Release | Language |\n`
summary += `| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |\n`
for (const metric of platformMetrics) {
const stars = metric.metrics?.stars || 0
const forks = metric.metrics?.forks || 0
const watchers = metric.metrics?.watchers || 0
const issues = metric.metrics?.totalIssues || 0
const openIssues = metric.metrics?.openIssues || 0
const closedIssues = metric.metrics?.closedIssues || 0
const downloads = metric.metrics?.totalReleaseDownloads || 0
const releases = metric.metrics?.releaseCount || 0
const latestRelease = metric.metrics?.latestRelease || 'N/A'
const language = metric.metrics?.language || 'N/A'
totalStars += stars
totalForks += forks
totalWatchers += watchers
totalIssues += issues
totalOpenIssues += openIssues
totalClosedIssues += closedIssues
totalDownloads += downloads
totalReleases += releases
summary += `| ${metric.name} | ${stars.toLocaleString()} | ${forks.toLocaleString()} | ${watchers.toLocaleString()} | ${openIssues.toLocaleString()} | ${closedIssues.toLocaleString()} | ${issues.toLocaleString()} | ${downloads.toLocaleString()} | ${releases.toLocaleString()} | ${latestRelease} | ${language} |\n`
}
summary += `| **Total** | **${totalStars.toLocaleString()}** | **${totalForks.toLocaleString()}** | **${totalWatchers.toLocaleString()}** | **${totalOpenIssues.toLocaleString()}** | **${totalClosedIssues.toLocaleString()}** | **${totalIssues.toLocaleString()}** | **${totalDownloads.toLocaleString()}** | **${totalReleases.toLocaleString()}** | | |\n`
return summary
}
export async function addRepoDetails(summary: string, metrics: MetricResult[]) {
summary += `#### Repository Details:\n\n`
for (const metric of metrics) {
summary += `**${metric.name}**:\n`
summary += `- Last Activity: ${metric.metrics?.lastActivity?.toLocaleString() || 0} days ago\n`
summary += `- Repository Age: ${metric.metrics?.repositoryAge?.toLocaleString() || 0} days\n`
summary += `- Release Count: ${metric.metrics?.releaseCount?.toLocaleString() || 0}\n`
summary += `- Total Release Downloads: ${metric.metrics?.totalReleaseDownloads?.toLocaleString() || 0}\n`
summary += `- Latest Release: ${metric.metrics?.latestRelease || 'N/A'}\n`
summary += `- Latest Release Downloads: ${metric.metrics?.latestReleaseDownloads?.toLocaleString() || 0}\n`
summary += `- Views: ${metric.metrics?.viewsCount?.toLocaleString() || 0}\n`
summary += `- Unique Visitors: ${metric.metrics?.uniqueVisitors?.toLocaleString() || 0}\n`
summary += `- Clones: ${metric.metrics?.clonesCount?.toLocaleString() || 0}\n`
summary += `\n`
}
summary += `\n\n`
const chatOutputPath = './charts/github'
mkdirSync(chatOutputPath, { recursive: true })
const svgOutputPathList = await createGitHubReleaseChart(metrics, chatOutputPath)
for (const svgOutputPath of svgOutputPathList) {
summary += `![${svgOutputPath}](${svgOutputPath})\n`
}
return summary
}
export async function createGitHubReleaseChart(platformMetrics: MetricResult[], outputPath: string) {
const svgOutputPathList = []
for (const metric of platformMetrics) {
// Only create charts if there's download data
if (metric.metrics?.downloadRange && metric.metrics.downloadRange.length > 0) {
const svgOutputPath = await createDownloadsPerReleaseChart(metric, outputPath)
svgOutputPathList.push(svgOutputPath)
const svgOutputPathCumulative = await createCumulativeDownloadsChart(metric, outputPath)
svgOutputPathList.push(svgOutputPathCumulative)
const svgOutputPathReleases = await createReleaseDownloadsChart(metric, outputPath)
svgOutputPathList.push(svgOutputPathReleases)
}
}
return svgOutputPathList
}
function groupByReleaseCumulative(releaseRange: { day: string, downloads: number, tagName?: string }[]){
const releases: Record<string, {downloads: number, tagName: string}> = {}
for (const release of releaseRange.sort((a, b) => {
return semver.order(a.tagName || '0.0.0', b.tagName || '0.0.0')
})) {
if (!release.tagName) {
continue
}
if (!releases[release.tagName]) {
releases[release.tagName] = {downloads: release.downloads, tagName: release.tagName || ''}
} else {
releases[release.tagName].downloads += release.downloads
}
}
let cumulativeDownloads = 0
for (const release of Object.keys(releases).sort((a, b) => {
return semver.order(a, b)
})) {
cumulativeDownloads += releases[release].downloads
releases[release].downloads = cumulativeDownloads
}
return releases
}
export async function createDownloadsPerReleaseChart(metric: MetricResult, outputPath: string): Promise<string> {
const downloadsRange = metric.metrics?.downloadRange || []
const svgOutputPath = `${outputPath}/${metric.name.replace('/', '-')}-release-downloads.svg`
const sortedReleases = downloadsRange.sort((a: { tagName?: string }, b: { tagName?: string }) => {
return semver.order(a.tagName || '0.0.0', b.tagName || '0.0.0')
})
const canvas = new Canvas(1000, 800);
const chart = new Chart(
canvas as any,
{
type: 'bar',
data: {
labels: sortedReleases.map((release: { tagName?: string }) => release.tagName),
datasets: [{
label: `${metric.name} Release Downloads`,
data: sortedReleases.map((release: { downloads: number }) => release.downloads),
backgroundColor: 'rgba(54, 162, 235, 0.8)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 1,
}]
},
options: {
responsive: true,
plugins: {
title: {
display: true,
text: `${metric.name} - Release Downloads`,
font: {
size: 16
}
},
legend: {
display: true
}
},
scales: {
x: {
title: {
display: true,
text: 'Release'
}
},
y: {
title: {
display: true,
text: 'Downloads'
},
beginAtZero: true
}
}
}
}
);
const svgBuffer = await canvas.toBuffer('svg', { matte: 'white' });
writeFileSync(svgOutputPath, svgBuffer);
chart.destroy();
return svgOutputPath
}
export async function createCumulativeDownloadsChart(metric: MetricResult, outputPath: string): Promise<string> {
const downloadsRange = metric.metrics?.downloadRange || []
const svgOutputPath = `${outputPath}/${metric.name.replace('/', '-')}-cumulative-release-downloads.svg`
const groupedDownloads = groupByReleaseCumulative(downloadsRange)
// Sort months chronologically
const semVerSortedReleases = Object.keys(groupedDownloads).sort((a, b) => {
return semver.order(a, b)
})
const canvas = new Canvas(1000, 800);
const chart = new Chart(
canvas as any,
{
type: 'line',
data: {
labels: semVerSortedReleases,
datasets: [{
label: `${metric.name} Cumulative Downloads`,
data: semVerSortedReleases.map(release => groupedDownloads[release].downloads),
backgroundColor: 'rgba(75, 192, 192, 0.2)',
borderColor: 'rgba(75, 192, 192, 1)',
borderWidth: 3,
fill: true,
tension: 0.1
}]
},
options: {
responsive: true,
plugins: {
title: {
display: true,
text: `${metric.name} - Cumulative Release Downloads`,
font: {
size: 16
}
},
legend: {
display: true
}
},
scales: {
x: {
title: {
display: true,
text: 'Release'
}
},
y: {
title: {
display: true,
text: 'Downloads'
},
beginAtZero: true
}
}
}
}
);
const svgBuffer = await canvas.toBuffer('svg', { matte: 'white' });
writeFileSync(svgOutputPath, svgBuffer);
chart.destroy();
return svgOutputPath
}
export async function createReleaseDownloadsChart(metric: MetricResult, outputPath: string): Promise<string> {
const downloadsRange = metric.metrics?.downloadRange || []
const svgOutputPath = `${outputPath}/${metric.name.replace('/', '-')}-top-release-downloads.svg`
// Sort releases by date (newest first for display)
const sortedReleases = downloadsRange
.filter((release: { tagName?: string; downloads: number; day: string }) => release.tagName && release.downloads > 0)
.sort((a: { downloads: number }, b: { downloads: number }) => b.downloads - a.downloads)
.slice(0, 10) // Show top 10 releases
.sort((a: { tagName?: string }, b: { tagName?: string }) => semver.order(a.tagName || '0.0.0', b.tagName || '0.0.0'))
if (sortedReleases.length === 0) {
// Return empty chart if no releases
return svgOutputPath
}
const canvas = new Canvas(1200, 800);
const chart = new Chart(
canvas as any,
{
type: 'bar',
data: {
labels: sortedReleases.map((release: { tagName?: string }) => release.tagName),
datasets: [{
label: `${metric.name} Release Downloads`,
data: sortedReleases.map((release: { downloads: number }) => release.downloads),
backgroundColor: 'rgba(255, 99, 132, 0.8)',
borderColor: 'rgba(255, 99, 132, 1)',
borderWidth: 1,
}]
},
options: {
responsive: true,
plugins: {
title: {
display: true,
text: `${metric.name} - Top Release Downloads`,
font: {
size: 16
}
},
legend: {
display: true
}
},
scales: {
x: {
title: {
display: true,
text: 'Release Tag'
}
},
y: {
title: {
display: true,
text: 'Downloads'
},
beginAtZero: true
}
}
}
}
);
const svgBuffer = await canvas.toBuffer('svg', { matte: 'white' });
writeFileSync(svgOutputPath, svgBuffer);
chart.destroy();
return svgOutputPath
}

201
src/summaries/npm.ts Normal file
View File

@@ -0,0 +1,201 @@
import { mkdirSync, writeFileSync } from 'node:fs';
import type { MetricResult } from "../collectors/types";
import { Chart, registerables } from 'chart.js';
import { Canvas } from 'skia-canvas';
// Register all Chart.js controllers
Chart.register(...registerables);
export function formatNpmSummary(summary: string, platformMetrics: MetricResult[]): string {
let totalDownloads = 0
let totalMonthlyDownloads = 0
let totalWeeklyDownloads = 0
let totalDailyDownloads = 0
summary += `| Package | Downloads | Monthly Downloads | Weekly Downloads | Daily Downloads |\n`
summary += `| --- | --- | --- | --- | --- |\n`
for (const metric of platformMetrics) {
const downloads = metric.metrics?.downloadsTotal || 0
const monthlyDownloads = metric.metrics?.downloadsMonthly || 0
const weeklyDownloads = metric.metrics?.downloadsWeekly || 0
const dailyDownloads = metric.metrics?.downloadsDaily || 0
totalDownloads += downloads
totalMonthlyDownloads += monthlyDownloads
totalWeeklyDownloads += weeklyDownloads
totalDailyDownloads += dailyDownloads
summary += `| ${metric.name} | ${downloads.toLocaleString()} | ${monthlyDownloads.toLocaleString()} | ${weeklyDownloads.toLocaleString()} | ${dailyDownloads.toLocaleString()} |\n`
}
summary += `| **Total** | **${totalDownloads.toLocaleString()}** | **${totalMonthlyDownloads.toLocaleString()}** | **${totalWeeklyDownloads.toLocaleString()}** | **${totalDailyDownloads.toLocaleString()}** | | | | |\n`
return summary
}
// Convert a list of dates into a list of Months
function groupByMonth(dateRange: { day: string, downloads: number }[]) {
const months: Record<string, number> = {}
for (const range of dateRange) {
const month = new Date(range.day).toLocaleDateString('en-US', { month: 'short', year: '2-digit' })
if (!months[month]) {
months[month] = range.downloads
} else {
months[month] += range.downloads
}
}
return months
}
function groupByMonthCumulative(dateRange: { day: string, downloads: number }[]){
const months: Record<string, number> = {}
for (const range of dateRange) {
const month = new Date(range.day).toLocaleDateString('en-US', { month: 'short', year: '2-digit' })
if (!months[month]) {
months[month] = range.downloads
} else {
months[month] += range.downloads
}
}
let cumulativeDownloads = 0
for (const month in months) {
cumulativeDownloads += months[month]
months[month] = cumulativeDownloads
}
return months
}
export async function createDownloadsPerMonthChart(metric: MetricResult, outputPath: string): Promise<string> {
const downloadsRange = metric.metrics?.downloadsRange || []
const svgOutputPath = `${outputPath}/${metric.name}-new-downloads-by-month.svg`
const groupedDownloads = groupByMonth(downloadsRange)
const canvas = new Canvas(1000, 800);
const chart = new Chart(
canvas as any,
{
type: 'line',
data: {
labels: Object.keys(groupedDownloads),
datasets: [{
label: metric.name,
data: Object.values(groupedDownloads),
backgroundColor: 'rgba(75, 192, 192, 0.2)',
borderColor: 'rgba(75, 192, 192, 1)',
borderWidth: 3,
fill: true,
tension: 0.1
}]
},
options: {
scales: {
x: {
time: {
unit: 'month',
displayFormats: {
month: 'MMM DD'
}
},
title: {
display: true,
text: 'Date'
}
},
y: {
title: {
display: true,
text: 'Downloads per month'
},
}
}
}
}
);
const svgBuffer = await canvas.toBuffer('svg', { matte: 'white' });
writeFileSync(svgOutputPath, svgBuffer);
chart.destroy();
return svgOutputPath
}
export async function createCumulativeDownloadsChart(metric: MetricResult, outputPath: string): Promise<string> {
const downloadsRange = metric.metrics?.downloadsRange || []
const svgOutputPath = `${outputPath}/${metric.name}-cumulative-downloads.svg`
const groupedDownloads = groupByMonthCumulative(downloadsRange)
const canvas = new Canvas(1000, 800);
const chart = new Chart(
canvas as any,
{
type: 'line',
data: {
labels: Object.keys(groupedDownloads),
datasets: [{
label: metric.name,
data: Object.values(groupedDownloads),
backgroundColor: 'rgba(75, 192, 192, 0.2)',
borderColor: 'rgba(75, 192, 192, 1)',
borderWidth: 3,
fill: true,
tension: 0.1
}]
},
options: {
scales: {
x: {
time: {
unit: 'month',
displayFormats: {
month: 'MMM DD'
}
},
title: {
display: true,
text: 'Date'
}
},
y: {
title: {
display: true,
text: 'Downloads per month'
},
}
}
}
}
);
const svgBuffer = await canvas.toBuffer('svg', { matte: 'white' });
writeFileSync(svgOutputPath, svgBuffer);
chart.destroy();
return svgOutputPath
}
export async function createNpmChart(platformMetrics: MetricResult[], outputPath: string) {
const svgOutputPathList = []
for (const metric of platformMetrics) {
const svgOutputPath = await createDownloadsPerMonthChart(metric, outputPath)
svgOutputPathList.push(svgOutputPath)
const svgOutputPathCumulative = await createCumulativeDownloadsChart(metric, outputPath)
svgOutputPathList.push(svgOutputPathCumulative)
}
return svgOutputPathList
}
export async function addNpmDetails(summary: string, platformMetrics: MetricResult[]): Promise<string> {
const outputPath = './charts/npm'
mkdirSync(outputPath, { recursive: true })
const svgOutputPathList = await createNpmChart(platformMetrics, outputPath)
for (const svgOutputPath of svgOutputPathList) {
summary += `![${svgOutputPath}](${svgOutputPath})\n`
}
return summary
}

288
src/summaries/powershell.ts Normal file
View File

@@ -0,0 +1,288 @@
import { mkdirSync, writeFileSync } from "fs"
import type { MetricResult } from "../collectors/types"
import { Chart, registerables } from 'chart.js';
import { Canvas } from 'skia-canvas';
// Register all Chart.js controllers
Chart.register(...registerables);
export function formatPowerShellSummary(summary: string, platformMetrics: MetricResult[]): string {
let platformDownloadTotal = 0
let totalVersions = 0
summary += `| Module | Total Downloads | Latest Version | Version Downloads | Versions | Last Updated |\n`
summary += `| --- | --- | --- | --- | --- | --- |\n`
for (const metric of platformMetrics) {
const lastUpdated = metric.metrics?.lastUpdated ? new Date(metric.metrics.lastUpdated).toLocaleDateString() : 'N/A'
const latestVersion = metric.metrics?.latestVersion || 'N/A'
const latestVersionDownloads = metric.metrics?.latestVersionDownloads || 0
const versionCount = metric.metrics?.versionCount || 0
summary += `| ${metric.name} | ${metric.metrics?.downloadsTotal?.toLocaleString() || 0} | ${latestVersion} | ${latestVersionDownloads.toLocaleString()} | ${versionCount} | ${lastUpdated} |\n`
platformDownloadTotal += metric.metrics?.downloadsTotal || 0
totalVersions += versionCount
}
summary += `| **Total** | **${platformDownloadTotal.toLocaleString()}** | | | **${totalVersions}** | |\n`
return summary
}
export async function addPowerShellDetails(summary: string, platformMetrics: MetricResult[]): Promise<string> {
summary += `#### PowerShell Module Details:\n\n`
for (const metric of platformMetrics) {
summary += `**${metric.name}**:\n`
summary += `- Total Downloads: ${metric.metrics?.downloadsTotal?.toLocaleString() || 0}\n`
summary += `- Latest Version: ${metric.metrics?.latestVersion || 'N/A'}\n`
summary += `- Latest Version Downloads: ${metric.metrics?.latestVersionDownloads?.toLocaleString() || 0}\n`
summary += `- Version Count: ${metric.metrics?.versionCount || 0}\n`
summary += `- Last Updated: ${metric.metrics?.lastUpdated ? new Date(metric.metrics.lastUpdated).toLocaleDateString() : 'N/A'}\n`
summary += `- Package Size: ${metric.metrics?.packageSize ? `${Math.round(metric.metrics.packageSize / 1024)} KB` : 'N/A'}\n`
summary += `\n`
}
summary += `\n\n`
const chartOutputPath = './charts/powershell'
mkdirSync(chartOutputPath, { recursive: true })
const svgOutputPathList = await createPowerShellCharts(platformMetrics, chartOutputPath)
for (const svgOutputPath of svgOutputPathList) {
summary += `![${svgOutputPath}](${svgOutputPath})\n`
}
return summary
}
export async function createPowerShellCharts(platformMetrics: MetricResult[], outputPath: string) {
const svgOutputPathList = []
// Only create charts if there's download data
const metricsWithData = platformMetrics.filter(metric =>
metric.metrics?.downloadsRange && metric.metrics.downloadsRange.length > 0
);
if (metricsWithData.length > 0) {
const svgOutputPath = await createCombinedDownloadsChart(metricsWithData, outputPath)
svgOutputPathList.push(svgOutputPath)
const svgOutputPathCumulative = await createCombinedCumulativeDownloadsChart(metricsWithData, outputPath)
svgOutputPathList.push(svgOutputPathCumulative)
}
return svgOutputPathList
}
// Color palette for different modules
const colors = [
'rgba(54, 162, 235, 0.8)',
'rgba(255, 99, 132, 0.8)',
'rgba(75, 192, 192, 0.8)',
'rgba(255, 205, 86, 0.8)',
'rgba(153, 102, 255, 0.8)',
'rgba(255, 159, 64, 0.8)',
'rgba(199, 199, 199, 0.8)',
'rgba(83, 102, 255, 0.8)',
'rgba(255, 99, 132, 0.8)',
'rgba(54, 162, 235, 0.8)'
];
const borderColors = [
'rgba(54, 162, 235, 1)',
'rgba(255, 99, 132, 1)',
'rgba(75, 192, 192, 1)',
'rgba(255, 205, 86, 1)',
'rgba(153, 102, 255, 1)',
'rgba(255, 159, 64, 1)',
'rgba(199, 199, 199, 1)',
'rgba(83, 102, 255, 1)',
'rgba(255, 99, 132, 1)',
'rgba(54, 162, 235, 1)'
];
export async function createCombinedDownloadsChart(metrics: MetricResult[], outputPath: string): Promise<string> {
const svgOutputPath = `${outputPath}/powershell-combined-downloads.svg`
// Get all unique dates across all modules for the x-axis
const allDates = new Set<string>();
for (const metric of metrics) {
const downloadsRange = metric.metrics?.downloadsRange || [];
for (const download of downloadsRange) {
allDates.add(download.day);
}
}
// Sort all dates chronologically
const sortedAllDates = Array.from(allDates).sort((a, b) =>
new Date(a).getTime() - new Date(b).getTime()
);
// Create datasets for each module (one line per module)
const data = []
for (const metric of metrics) {
const downloadsRange = metric.metrics?.downloadsRange || [];
for (const date of sortedAllDates) {
const downloads = downloadsRange.filter(d => d.day === date).reduce((sum, d) => sum + d.downloads, 0);
data.push(downloads);
}
}
const labels = sortedAllDates.map(date =>
new Date(date).toLocaleDateString('en-US', {
month: 'short',
year: '2-digit',
day: 'numeric'
})
);
const canvas = new Canvas(1200, 800);
const chart = new Chart(
canvas as any,
{
type: 'line',
data: {
labels: labels,
datasets: [{
data,
backgroundColor: 'rgba(54, 162, 235, 0.2)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 3,
fill: true,
tension: 0.1
}]
},
options: {
responsive: true,
plugins: {
title: {
display: true,
text: 'PowerShell Modules - Downloads Over Time',
font: {
size: 16
}
},
legend: {
display: true
}
},
scales: {
x: {
title: {
display: true,
text: 'Release Date'
}
},
y: {
type: 'linear',
title: {
display: true,
text: 'Downloads'
},
beginAtZero: true,
ticks: {
stepSize: 1000
}
}
}
}
}
);
const svgBuffer = await canvas.toBuffer('svg', { matte: 'white' });
writeFileSync(svgOutputPath, svgBuffer);
chart.destroy();
return svgOutputPath
}
export async function createCombinedCumulativeDownloadsChart(metrics: MetricResult[], outputPath: string): Promise<string> {
const svgOutputPath = `${outputPath}/powershell-cumulative-downloads.svg`
// Get all unique dates across all modules for the x-axis
const allDates = new Set<string>();
for (const metric of metrics) {
const downloadsRange = metric.metrics?.downloadsRange || [];
for (const download of downloadsRange) {
allDates.add(download.day);
}
}
// Sort all dates chronologically
const sortedAllDates = Array.from(allDates).sort((a, b) =>
new Date(a).getTime() - new Date(b).getTime()
);
const labels = sortedAllDates.map(date =>
new Date(date).toLocaleDateString('en-US', {
month: 'short',
year: '2-digit',
day: 'numeric'
})
);
const data = []
let runningTotal = 0
for (const date of sortedAllDates) {
const downloads = metrics.reduce((sum, metric) => sum + (metric.metrics?.downloadsRange?.find(d => d.day === date)?.downloads || 0), 0);
runningTotal += downloads
data.push(runningTotal);
}
const canvas = new Canvas(1200, 800);
const chart = new Chart(
canvas as any,
{
type: 'line',
data: {
labels: labels,
datasets: [{
label: 'Cumulative Downloads',
data,
backgroundColor: 'rgba(54, 162, 235, 0.2)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 3,
fill: true,
tension: 0.1
}]
},
options: {
responsive: true,
plugins: {
title: {
display: true,
text: 'PowerShell Modules - Cumulative Downloads',
font: {
size: 16
}
},
legend: {
display: true
}
},
scales: {
x: {
title: {
display: true,
text: 'Release Date'
}
},
y: {
title: {
display: true,
text: 'Cumulative Downloads'
},
beginAtZero: true,
ticks: {
stepSize: 5000
}
}
}
}
}
);
const svgBuffer = await canvas.toBuffer('svg', { matte: 'white' });
writeFileSync(svgOutputPath, svgBuffer);
chart.destroy();
return svgOutputPath
}

397
src/summaries/pypi.ts Normal file
View File

@@ -0,0 +1,397 @@
import { mkdirSync, writeFileSync } from 'node:fs'
import type { MetricResult } from "../collectors/types"
import { Chart, registerables } from 'chart.js'
import { Canvas } from 'skia-canvas'
Chart.register(...registerables)
export function formatPypiSummary(summary: string, platformMetrics: MetricResult[]): string {
summary += `| Package | Total Downloads | Monthly Downloads | Weekly Downloads | Daily Downloads | Version |\n`
summary += `| --- | --- | --- | --- | --- | --- |\n`
for (const metric of platformMetrics) {
summary += `| ${metric.name} | ${metric.metrics?.downloadsTotal?.toLocaleString() || 0} | ${metric.metrics?.downloadsMonthly?.toLocaleString() || 0} | ${metric.metrics?.downloadsWeekly?.toLocaleString() || 0} | ${metric.metrics?.downloadsDaily?.toLocaleString() || 0} | ${metric.metrics?.version || 'N/A'} |\n`
}
summary += `| **Total** | **${platformMetrics.reduce((sum, m) => sum + (m.metrics?.downloadsTotal || 0), 0).toLocaleString()}** | **${platformMetrics.reduce((sum, m) => sum + (m.metrics?.downloadsMonthly || 0), 0).toLocaleString()}** | **${platformMetrics.reduce((sum, m) => sum + (m.metrics?.downloadsWeekly || 0), 0).toLocaleString()}** | **${platformMetrics.reduce((sum, m) => sum + (m.metrics?.downloadsDaily || 0), 0).toLocaleString()}** | | |\n`
return summary
}
function toIsoMonth(dateStr: string) {
// input expected YYYY-MM-DD; fallback to Date parse if needed
const iso = dateStr?.slice(0, 7)
if (iso && /\d{4}-\d{2}/.test(iso)) return iso
const d = new Date(dateStr)
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
return `${y}-${m}`
}
function displayMonthLabel(isoMonth: string) {
const [y, m] = isoMonth.split('-')
const d = new Date(Number(y), Number(m) - 1, 1)
return d.toLocaleDateString('en-US', { month: 'short', year: '2-digit' })
}
function aggregateMonthlyTotals(points: { date: string, downloads: number }[]) {
const totals: Record<string, number> = {}
for (const p of points) {
const iso = toIsoMonth(p.date)
totals[iso] = (totals[iso] || 0) + p.downloads
}
const labelsIso = Object.keys(totals).sort()
const labels = labelsIso.map(displayMonthLabel)
const data = labelsIso.map(l => totals[l])
return { labels, data }
}
function aggregateMonthlyByCategory(points: { date: string, category: string, downloads: number }[]) {
const labelIsoSet = new Set<string>()
const categoryMap: Record<string, Record<string, number>> = {}
for (const p of points) {
const iso = toIsoMonth(p.date)
labelIsoSet.add(iso)
if (!categoryMap[p.category]) categoryMap[p.category] = {}
categoryMap[p.category][iso] = (categoryMap[p.category][iso] || 0) + p.downloads
}
const labelsIso = Array.from(labelIsoSet).sort()
const labels = labelsIso.map(displayMonthLabel)
return { labelsIso, labels, categoryMap }
}
async function createOverallDownloadsChart(metric: MetricResult, outputPath: string) {
// Prefer server-prepared chart JSON if present
const server = metric.metrics?.overallChart as { labels?: string[], datasets?: { label: string, data: number[] }[] } | undefined
let labels: string[]
let datasets: { label: string, data: number[], borderColor?: string, backgroundColor?: string, borderWidth?: number, fill?: boolean, tension?: number }[]
if (server && server.labels && server.labels.length && server.datasets && server.datasets.length) {
labels = server.labels
const colorFor = (label?: string, idx?: number) => {
const l = (label || '').toLowerCase()
if (l.includes('without')) return { stroke: '#2563eb', fill: '#2563eb33' } // blue
if (l.includes('with')) return { stroke: '#64748b', fill: '#64748b33' } // slate
const palette = ['#2563eb', '#16a34a', '#f59e0b', '#ef4444', '#7c3aed']
const i = idx ?? 0
return { stroke: palette[i % palette.length], fill: palette[i % palette.length] + '33' }
}
datasets = server.datasets.map((ds, i) => {
const c = colorFor(ds.label, i)
return {
...ds,
borderColor: c.stroke,
backgroundColor: c.fill,
borderWidth: 3,
fill: true,
tension: 0.1,
}
})
} else {
const series = (metric.metrics?.overallSeries as { date: string, category: string, downloads: number }[] | undefined) || []
const agg = aggregateMonthlyTotals(series.map(p => ({ date: p.date, downloads: p.downloads })))
labels = agg.labels
datasets = [{
label: `${metric.name} downloads per month`,
data: agg.data,
backgroundColor: 'rgba(54, 162, 235, 0.2)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 3,
fill: true,
tension: 0.1
}]
}
const canvas = new Canvas(1000, 800)
const chart = new Chart(canvas as any, {
type: 'line',
data: { labels, datasets },
options: {
plugins: {
legend: { display: true, position: 'bottom' },
title: { display: true, text: `${metric.name} overall downloads` }
},
scales: {
x: { title: { display: true, text: 'Month' } },
y: { title: { display: true, text: 'Downloads' } }
}
}
})
const svgBuffer = await canvas.toBuffer('svg', { matte: 'white' })
const svgPath = `${outputPath}/${metric.name}-pypi-overall.svg`
writeFileSync(svgPath, svgBuffer)
chart.destroy()
return svgPath
}
// Time-series: Python major over time (line)
async function createPythonMajorChart(metric: MetricResult, outputPath: string) {
// Prefer server chart JSON if present
const server = metric.metrics?.pythonMajorChart as { labels?: string[], datasets?: { label: string, data: number[] }[] } | undefined
let labels: string[]
let datasets: { label: string, data: number[], borderColor?: string, backgroundColor?: string, borderWidth?: number, fill?: boolean }[]
if (server && server.labels && server.labels.length && server.datasets && server.datasets.length) {
const palette = ['#2563eb', '#16a34a', '#f59e0b', '#ef4444', '#7c3aed', '#0891b2', '#dc2626', '#0ea5e9']
labels = server.labels
datasets = server.datasets
.filter(ds => !/unknown/i.test(ds.label))
.map((ds, idx) => ({
...ds,
borderColor: palette[idx % palette.length],
backgroundColor: palette[idx % palette.length] + '33',
borderWidth: 2,
fill: false,
}))
} else {
const points = (metric.metrics?.pythonMajorSeries as { date: string, category: string, downloads: number }[] | undefined) || []
const { labelsIso, labels: lbls, categoryMap } = aggregateMonthlyByCategory(points)
labels = lbls
const sortedCategories = Object.keys(categoryMap).filter(k => !/unknown/i.test(k)).sort((a, b) => Number(a) - Number(b))
const palette = ['#2563eb', '#16a34a', '#f59e0b', '#ef4444', '#7c3aed', '#0891b2', '#dc2626', '#0ea5e9']
datasets = sortedCategories.map((category, idx) => ({
label: `Python ${category}`,
data: labelsIso.map(l => categoryMap[category][l] || 0),
borderColor: palette[idx % palette.length],
backgroundColor: palette[idx % palette.length] + '33',
borderWidth: 2,
fill: false,
}))
}
const canvas = new Canvas(1000, 800)
const chart = new Chart(canvas as any, {
type: 'line',
data: { labels, datasets },
options: {
plugins: {
legend: { position: 'bottom' },
title: { display: true, text: `${metric.name} downloads by Python major version` }
},
scales: {
x: { title: { display: true, text: 'Month' } },
y: { title: { display: true, text: 'Downloads' } }
}
}
})
const svgBuffer = await canvas.toBuffer('svg', { matte: 'white' })
const svgPath = `${outputPath}/${metric.name}-pypi-python-major.svg`
writeFileSync(svgPath, svgBuffer)
chart.destroy()
return svgPath
}
// Time-series: Python minor over time (line)
async function createPythonMinorChart(metric: MetricResult, outputPath: string) {
// Prefer server chart JSON if present
const server = metric.metrics?.pythonMinorChart as { labels?: string[], datasets?: { label: string, data: number[] }[] } | undefined
let labels: string[]
let datasets: { label: string, data: number[], borderColor?: string, backgroundColor?: string, borderWidth?: number, fill?: boolean }[]
if (server && server.labels && server.labels.length && server.datasets && server.datasets.length) {
const palette = ['#1d4ed8', '#059669', '#d97706', '#dc2626', '#6d28d9', '#0e7490', '#b91c1c', '#0284c7']
labels = server.labels
datasets = server.datasets
.filter(ds => !/unknown/i.test(ds.label))
.map((ds, idx) => ({
...ds,
borderColor: palette[idx % palette.length],
backgroundColor: palette[idx % palette.length] + '33',
borderWidth: 2,
fill: false,
}))
} else {
const points = (metric.metrics?.pythonMinorSeries as { date: string, category: string, downloads: number }[] | undefined) || []
const { labelsIso, labels: lbls, categoryMap } = aggregateMonthlyByCategory(points)
labels = lbls
const sortedCategories = Object.keys(categoryMap).filter(k => !/unknown/i.test(k)).sort((a, b) => a.localeCompare(b, undefined, { numeric: true }))
const palette = ['#1d4ed8', '#059669', '#d97706', '#dc2626', '#6d28d9', '#0e7490', '#b91c1c', '#0284c7']
datasets = sortedCategories.map((category, idx) => ({
label: `Python ${category}`,
data: labelsIso.map(l => categoryMap[category][l] || 0),
borderColor: palette[idx % palette.length],
backgroundColor: palette[idx % palette.length] + '33',
borderWidth: 2,
fill: false,
}))
}
const canvas = new Canvas(1000, 800)
const chart = new Chart(canvas as any, {
type: 'line',
data: { labels, datasets },
options: {
plugins: {
legend: { position: 'bottom' },
title: { display: true, text: `${metric.name} downloads by Python minor version` }
},
scales: {
x: { title: { display: true, text: 'Month' } },
y: { title: { display: true, text: 'Downloads' } }
}
}
})
const svgBuffer = await canvas.toBuffer('svg', { matte: 'white' })
const svgPath = `${outputPath}/${metric.name}-pypi-python-minor.svg`
writeFileSync(svgPath, svgBuffer)
chart.destroy()
return svgPath
}
// Time-series: Installer over time (line) - prefer server JSON
async function createInstallerChart(metric: MetricResult, outputPath: string) {
const server = metric.metrics?.installerChart as { labels?: string[], datasets?: { label: string, data: number[] }[] } | undefined
let labels: string[]
let datasets: { label: string, data: number[], borderColor?: string, backgroundColor?: string, borderWidth?: number, fill?: boolean }[]
if (server && server.labels && server.labels.length && server.datasets && server.datasets.length) {
const palette = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#a78bfa', '#22d3ee']
labels = server.labels
datasets = server.datasets.map((ds, idx) => ({
...ds,
borderColor: palette[idx % palette.length],
backgroundColor: palette[idx % palette.length] + '33',
borderWidth: 2,
fill: false,
}))
} else {
const points = (metric.metrics?.installerSeries as { date: string, category: string, downloads: number }[] | undefined) || []
const { labelsIso, labels: lbls, categoryMap } = aggregateMonthlyByCategory(points)
labels = lbls
const categories = Object.keys(categoryMap)
const palette = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#a78bfa', '#22d3ee']
datasets = categories.map((category, idx) => ({
label: category,
data: labelsIso.map(l => categoryMap[category][l] || 0),
borderColor: palette[idx % palette.length],
backgroundColor: palette[idx % palette.length] + '33',
borderWidth: 2,
fill: false,
}))
}
const canvas = new Canvas(1000, 800)
const chart = new Chart(canvas as any, {
type: 'line',
data: { labels, datasets },
options: {
plugins: {
legend: { position: 'bottom' },
title: { display: true, text: `${metric.name} downloads by installer` }
},
scales: {
x: { title: { display: true, text: 'Month' } },
y: { title: { display: true, text: 'Downloads' } }
}
}
})
const svgBuffer = await canvas.toBuffer('svg', { matte: 'white' })
const svgPath = `${outputPath}/${metric.name}-pypi-installer.svg`
writeFileSync(svgPath, svgBuffer)
chart.destroy()
return svgPath
}
// Time-series: System over time (line) - prefer server JSON
async function createSystemChart(metric: MetricResult, outputPath: string) {
const server = metric.metrics?.systemChart as { labels?: string[], datasets?: { label: string, data: number[] }[] } | undefined
let labels: string[]
let datasets: { label: string, data: number[], borderColor?: string, backgroundColor?: string, borderWidth?: number, fill?: boolean }[]
if (server && server.labels && server.labels.length && server.datasets && server.datasets.length) {
const palette = ['#0ea5e9', '#22c55e', '#f97316', '#e11d48', '#8b5cf6', '#06b6d4']
labels = server.labels
datasets = server.datasets.map((ds, idx) => ({
...ds,
borderColor: palette[idx % palette.length],
backgroundColor: palette[idx % palette.length] + '33',
borderWidth: 2,
fill: false,
}))
} else {
const points = (metric.metrics?.systemSeries as { date: string, category: string, downloads: number }[] | undefined) || []
const { labelsIso, labels: lbls, categoryMap } = aggregateMonthlyByCategory(points)
labels = lbls
const sortedCategories = Object.keys(categoryMap).sort()
const palette = ['#0ea5e9', '#22c55e', '#f97316', '#e11d48', '#8b5cf6', '#06b6d4']
datasets = sortedCategories.map((category, idx) => ({
label: category,
data: labelsIso.map(l => categoryMap[category][l] || 0),
borderColor: palette[idx % palette.length],
backgroundColor: palette[idx % palette.length] + '33',
borderWidth: 2,
fill: false,
}))
}
const canvas = new Canvas(1000, 800)
const chart = new Chart(canvas as any, {
type: 'line',
data: { labels, datasets },
options: {
plugins: {
legend: { position: 'bottom' },
title: { display: true, text: `${metric.name} downloads by OS` }
},
scales: {
x: { title: { display: true, text: 'Month' } },
y: { title: { display: true, text: 'Downloads' } }
}
}
})
const svgBuffer = await canvas.toBuffer('svg', { matte: 'white' })
const svgPath = `${outputPath}/${metric.name}-pypi-system.svg`
writeFileSync(svgPath, svgBuffer)
chart.destroy()
return svgPath
}
// Removed static bar chart generators per request
async function createPypiCharts(metrics: MetricResult[], basePath: string) {
const outputPaths: string[] = []
for (const metric of metrics) {
const packagePath = `${basePath}`
mkdirSync(packagePath, { recursive: true })
const overall = await createOverallDownloadsChart(metric, packagePath)
outputPaths.push(overall)
const pythonMajor = await createPythonMajorChart(metric, packagePath)
outputPaths.push(pythonMajor)
const pythonMinor = await createPythonMinorChart(metric, packagePath)
outputPaths.push(pythonMinor)
const installer = await createInstallerChart(metric, packagePath)
outputPaths.push(installer)
const system = await createSystemChart(metric, packagePath)
outputPaths.push(system)
// static bar charts removed
}
return outputPaths
}
export function addPypiDetails(summary: string, metrics: MetricResult[]): string {
summary += `#### Package Details:\n\n`
for (const metric of metrics) {
summary += `**${metric.name}**:\n`
summary += `- Version: ${metric.metrics?.version || 'N/A'}\n`
if (metric.metrics?.latestReleaseDate) summary += `- Released: ${metric.metrics.latestReleaseDate}\n`
if (metric.metrics?.popularSystem) summary += `- Popular system: ${metric.metrics.popularSystem}\n`
if (metric.metrics?.popularInstaller) summary += `- Popular installer: ${metric.metrics.popularInstaller}\n`
summary += `- Releases: ${metric.metrics?.releases || 0}\n`
if (metric.metrics?.systemBreakdown) {
summary += `- OS Usage Breakdown \n`
for (const [key, value] of Object.entries(metric.metrics?.systemBreakdown)) {
summary += ` - ${key}: ${value}\n`
}
}
if (metric.metrics?.pythonVersionBreakdown) {
summary += `- Python Version Breakdown \n`
for (const [key, value] of Object.entries(metric.metrics?.pythonVersionBreakdown)) {
summary += ` - ${key}: ${value}\n`
}
}
}
return summary
}
export async function addPypiCharts(summary: string, platformMetrics: MetricResult[]): Promise<string> {
const outputPath = './charts/pypi'
mkdirSync(outputPath, { recursive: true })
summary += `\n\n`
const svgPaths = await createPypiCharts(platformMetrics, outputPath)
for (const p of svgPaths) {
summary += `![${p}](${p})\n`
}
return summary
}

View File

@@ -1,29 +0,0 @@
/**
* Test setup file for Bun
* This file is preloaded before running tests
*/
// Global test utilities can be defined here
(globalThis as any).testUtils = {
createMockUsageData: (userId: string, action: string) => ({
timestamp: new Date(),
userId,
action,
metadata: { test: true }
})
};
// Extend global types
declare global {
var testUtils: {
createMockUsageData: (userId: string, action: string) => {
timestamp: Date;
userId: string;
action: string;
metadata: { test: boolean };
};
};
}
// Export to make this a module
export {};

View File

@@ -1,201 +0,0 @@
/**
* GitHub Release Download Tracker
* Uses GitHub API with Octokit to fetch release download statistics
*/
import type { BaseDownloadStats, PlatformTracker } from '../types';
import { Octokit } from '@octokit/rest';
import { retry } from '@octokit/plugin-retry';
import { throttling } from '@octokit/plugin-throttling';
export interface GitHubDownloadStats extends BaseDownloadStats {
platform: 'github';
repository: string;
releaseId: number;
releaseName: string;
releaseTag: string;
assetName?: string;
assetId?: number;
}
export interface GitHubReleaseInfo {
id: number;
name: string | null;
tag_name: string;
published_at: string | null;
assets: Array<{
id: number;
name: string;
download_count: number;
size: number;
content_type: string;
}>;
body?: string | null;
draft: boolean;
prerelease: boolean;
}
export interface GitHubRepositoryInfo {
id: number;
name: string;
full_name: string;
description?: string | null;
html_url: string;
stargazers_count: number;
forks_count: number;
language?: string | null;
created_at: string;
updated_at: string;
}
export class GitHubTracker implements PlatformTracker {
name = 'github';
private token?: string;
private octokit: Octokit | null = null;
constructor(token?: string) {
this.token = token || process.env.GITHUB_TOKEN;
this.initializeOctokit();
}
private async initializeOctokit() {
// Create Octokit with retry and throttling plugins
const MyOctokit = Octokit.plugin(retry, throttling);
this.octokit = new MyOctokit({
auth: this.token,
userAgent: 'usage-statistics-tracker',
timeZone: 'UTC',
baseUrl: 'https://api.github.com',
log: {
debug: () => {},
info: () => {},
warn: console.warn,
error: console.error
},
throttle: {
onRateLimit: (retryAfter: number, options: any) => {
console.warn(`Rate limit hit for ${options.request.url}, retrying after ${retryAfter} seconds`);
return true; // Retry after the specified time
},
onSecondaryRateLimit: (retryAfter: number, options: any) => {
console.warn(`Secondary rate limit hit for ${options.request.url}, retrying after ${retryAfter} seconds`);
return true; // Retry after the specified time
}
},
retry: {
doNotRetry: [400, 401, 403, 404, 422], // Don't retry on these status codes
enabled: true
}
});
}
async getDownloadStats(repository: string, options?: {
releaseTag?: string;
assetName?: string;
startDate?: Date;
endDate?: Date;
}): Promise<GitHubDownloadStats[]> {
try {
const [owner, repo] = repository.split('/');
if (!owner || !repo) {
throw new Error(`Invalid repository format: ${repository}. Expected format: owner/repo`);
}
const releases = await this.getReleases(owner, repo);
const stats: GitHubDownloadStats[] = [];
for (const release of releases) {
// Filter by release tag if specified
if (options?.releaseTag && release.tag_name !== options.releaseTag) {
continue;
}
for (const asset of release.assets) {
// Filter by asset name if specified
if (options?.assetName && asset.name !== options.assetName) {
continue;
}
stats.push({
platform: 'github',
packageName: repository,
repository,
releaseId: release.id,
releaseName: release.name || 'Unknown',
releaseTag: release.tag_name,
assetName: asset.name,
assetId: asset.id,
downloadCount: asset.download_count,
metadata: {
assetSize: asset.size,
contentType: asset.content_type,
isDraft: release.draft,
isPrerelease: release.prerelease
}
});
}
}
return stats;
} catch (error) {
console.error(`Error fetching GitHub stats for ${repository}:`, error);
return [];
}
}
async getLatestVersion(repository: string): Promise<string | null> {
try {
const [owner, repo] = repository.split('/');
const releases = await this.getReleases(owner, repo);
// Get the latest non-draft, non-prerelease
const latestRelease = releases.find(r => !r.draft && !r.prerelease);
return latestRelease?.tag_name || null;
} catch (error) {
console.error(`Error fetching latest version for ${repository}:`, error);
return null;
}
}
async getPackageInfo(repository: string): Promise<GitHubRepositoryInfo> {
const [owner, repo] = repository.split('/');
if (!this.octokit) {
throw new Error('Octokit not initialized');
}
try {
const response = await this.octokit.repos.get({
owner,
repo
});
return response.data;
} catch (error: any) {
console.error(`Error fetching repository info for ${repository}:`, error);
throw error;
}
}
private async getReleases(owner: string, repo: string): Promise<GitHubReleaseInfo[]> {
if (!this.octokit) {
throw new Error('Octokit not initialized');
}
try {
const response = await this.octokit.repos.listReleases({
owner,
repo,
per_page: 100
});
return response.data;
} catch (error: any) {
console.error(`Error fetching releases for ${owner}/${repo}:`, error);
throw error;
}
}
}
export default GitHubTracker;

View File

@@ -1,298 +0,0 @@
/**
* Go Module Download Tracker
* Uses Go module proxy and GitHub API to fetch download statistics
*/
import type { BaseDownloadStats, PlatformTracker } from '../types';
export interface GoDownloadStats extends BaseDownloadStats {
platform: 'go';
moduleName: string;
version: string;
goVersion: string;
downloadCount: number;
publishedDate: Date;
goModHash: string;
}
export interface GoModuleInfo {
Path: string;
Version: string;
Time: string;
Main: boolean;
GoMod: string;
GoVersion: string;
Retracted: boolean;
RetractedReason?: string;
}
export interface GoModuleVersions {
Path: string;
Versions: string[];
Time: Record<string, string>;
Origin: Record<string, any>;
}
export interface GoModuleZipInfo {
Path: string;
Version: string;
Mod: GoModuleInfo;
Zip: {
Hash: string;
Size: number;
};
}
export class GoTracker implements PlatformTracker {
name = 'go';
private proxyUrl = 'https://proxy.golang.org';
private githubToken?: string;
constructor(githubToken?: string) {
this.githubToken = githubToken;
}
async getDownloadStats(moduleName: string, options?: {
version?: string;
startDate?: Date;
endDate?: Date;
}): Promise<GoDownloadStats[]> {
try {
const versions = await this.getModuleVersions(moduleName);
const stats: GoDownloadStats[] = [];
for (const version of versions.Versions) {
if (options?.version && version !== options.version) {
continue;
}
const moduleInfo = await this.getModuleInfo(moduleName, version);
const publishedDate = new Date(moduleInfo.Time);
// Filter by date range if specified
if (options?.startDate && publishedDate < options.startDate) {
continue;
}
if (options?.endDate && publishedDate > options.endDate) {
continue;
}
// Get download count (Go doesn't provide direct download stats)
// We'll use GitHub stars/forks as a proxy for popularity
const downloadCount = await this.getEstimatedDownloads(moduleName, version);
stats.push({
platform: 'go',
packageName: moduleName,
moduleName,
version,
goVersion: moduleInfo.GoVersion,
downloadCount,
publishedDate,
goModHash: moduleInfo.GoMod,
metadata: {
isMain: moduleInfo.Main,
isRetracted: moduleInfo.Retracted,
retractedReason: moduleInfo.RetractedReason,
goModHash: moduleInfo.GoMod
}
});
}
return stats;
} catch (error) {
console.error(`Error fetching Go stats for ${moduleName}:`, error);
return [];
}
}
async getLatestVersion(moduleName: string): Promise<string | null> {
try {
const versions = await this.getModuleVersions(moduleName);
return versions.Versions[versions.Versions.length - 1] || null;
} catch (error) {
console.error(`Error fetching latest version for ${moduleName}:`, error);
return null;
}
}
async getPackageInfo(moduleName: string): Promise<GoModuleInfo> {
const latestVersion = await this.getLatestVersion(moduleName);
if (!latestVersion) {
throw new Error(`No versions found for module ${moduleName}`);
}
return this.getModuleInfo(moduleName, latestVersion);
}
async getModuleVersions(moduleName: string): Promise<GoModuleVersions> {
const response = await fetch(`${this.proxyUrl}/${moduleName}/@v/list`);
if (!response.ok) {
throw new Error(`Failed to fetch versions for ${moduleName}`);
}
const versions = await response.text();
const versionList = versions.trim().split('\n').filter(v => v);
// Get time information for each version
const timeInfo: Record<string, string> = {};
for (const version of versionList) {
const timeResponse = await fetch(`${this.proxyUrl}/${moduleName}/@v/${version}.info`);
if (timeResponse.ok) {
const timeData = await timeResponse.json();
timeInfo[version] = timeData.Time;
}
}
return {
Path: moduleName,
Versions: versionList,
Time: timeInfo,
Origin: {}
};
}
async getModuleInfo(moduleName: string, version: string): Promise<GoModuleInfo> {
const response = await fetch(`${this.proxyUrl}/${moduleName}/@v/${version}.info`);
if (!response.ok) {
throw new Error(`Failed to fetch module info for ${moduleName}@${version}`);
}
return response.json();
}
async getModuleZip(moduleName: string, version: string): Promise<GoModuleZipInfo> {
const response = await fetch(`${this.proxyUrl}/${moduleName}/@v/${version}.zip`);
if (!response.ok) {
throw new Error(`Failed to fetch module zip for ${moduleName}@${version}`);
}
// Get the mod info
const modInfo = await this.getModuleInfo(moduleName, version);
return {
Path: moduleName,
Version: version,
Mod: modInfo,
Zip: {
Hash: '', // Would need to calculate hash from response
Size: parseInt(response.headers.get('content-length') || '0')
}
};
}
private async getEstimatedDownloads(moduleName: string, version: string): Promise<number> {
try {
// Try to get GitHub repository info if it's a GitHub module
if (moduleName.includes('github.com')) {
const repoPath = moduleName.replace('github.com/', '');
const response = await fetch(`https://api.github.com/repos/${repoPath}`, {
headers: this.githubToken ? {
'Authorization': `token ${this.githubToken}`,
'Accept': 'application/vnd.github.v3+json'
} : {
'Accept': 'application/vnd.github.v3+json'
}
});
if (response.ok) {
const repoData = await response.json();
// Use stars + forks as a rough estimate of popularity
return (repoData.stargazers_count || 0) + (repoData.forks_count || 0);
}
}
// Fallback: use a simple heuristic based on version age
const moduleInfo = await this.getModuleInfo(moduleName, version);
const ageInDays = (Date.now() - new Date(moduleInfo.Time).getTime()) / (1000 * 60 * 60 * 24);
return Math.max(1, Math.floor(100 / (ageInDays + 1))); // More downloads for newer versions
} catch (error) {
console.error(`Error estimating downloads for ${moduleName}@${version}:`, error);
return 1;
}
}
async searchModules(query: string): Promise<{
modules: Array<{
path: string;
version: string;
time: string;
}>;
}> {
try {
// Go doesn't have a built-in search API, but we can search GitHub for Go modules
const response = await fetch(`https://api.github.com/search/repositories?q=${query}+language:go&sort=stars&order=desc`, {
headers: this.githubToken ? {
'Authorization': `token ${this.githubToken}`,
'Accept': 'application/vnd.github.v3+json'
} : {
'Accept': 'application/vnd.github.v3+json'
}
});
if (!response.ok) {
throw new Error('Failed to search Go modules');
}
const data = await response.json();
return {
modules: data.items.map((repo: any) => ({
path: `github.com/${repo.full_name}`,
version: 'latest',
time: repo.created_at
}))
};
} catch (error) {
console.error('Error searching Go modules:', error);
return { modules: [] };
}
}
async getModuleAnalytics(moduleName: string): Promise<{
totalVersions: number;
latestVersion: string;
firstPublished: Date;
lastPublished: Date;
estimatedTotalDownloads: number;
}> {
try {
const versions = await this.getModuleVersions(moduleName);
const latestVersion = versions.Versions[versions.Versions.length - 1];
let firstPublished = new Date();
let lastPublished = new Date(0);
let totalDownloads = 0;
for (const version of versions.Versions) {
const moduleInfo = await this.getModuleInfo(moduleName, version);
const publishedDate = new Date(moduleInfo.Time);
if (publishedDate < firstPublished) {
firstPublished = publishedDate;
}
if (publishedDate > lastPublished) {
lastPublished = publishedDate;
}
totalDownloads += await this.getEstimatedDownloads(moduleName, version);
}
return {
totalVersions: versions.Versions.length,
latestVersion: latestVersion || '',
firstPublished,
lastPublished,
estimatedTotalDownloads: totalDownloads
};
} catch (error) {
console.error(`Error fetching analytics for ${moduleName}:`, error);
return {
totalVersions: 0,
latestVersion: '',
firstPublished: new Date(),
lastPublished: new Date(),
estimatedTotalDownloads: 0
};
}
}
}
export default GoTracker;

View File

@@ -1,199 +0,0 @@
/**
* Homebrew Package Download Tracker
* Uses Homebrew API and GitHub API to fetch download statistics
*/
import type { BaseDownloadStats, PlatformTracker } from '../types';
export interface HomebrewDownloadStats extends BaseDownloadStats {
platform: 'homebrew';
formulaName: string;
tapName: string;
version: string;
installCount: number;
analyticsData?: {
installEvents: number;
buildErrors: number;
osVersion: string;
rubyVersion: string;
};
}
export interface HomebrewFormulaInfo {
name: string;
full_name: string;
desc?: string;
homepage?: string;
version: string;
installed: number[];
dependencies: string[];
conflicts: string[];
caveats?: string;
analytics?: {
install: {
'30d': Record<string, number>;
'90d': Record<string, number>;
'365d': Record<string, number>;
};
};
}
export interface HomebrewTapInfo {
name: string;
full_name: string;
description?: string;
homepage?: string;
url: string;
clone_url: string;
default_branch: string;
stargazers_count: number;
forks_count: number;
}
export class HomebrewTracker implements PlatformTracker {
name = 'homebrew';
private baseUrl = 'https://formulae.brew.sh/api';
private githubToken?: string;
constructor(githubToken?: string) {
this.githubToken = githubToken;
}
async getDownloadStats(formulaName: string, options?: {
tapName?: string;
period?: '30d' | '90d' | '365d';
startDate?: Date;
endDate?: Date;
}): Promise<HomebrewDownloadStats[]> {
try {
const formulaInfo = await this.getPackageInfo(formulaName);
const stats: HomebrewDownloadStats[] = [];
// Get analytics data if available
if (formulaInfo.analytics?.install) {
const period = options?.period || '30d';
const analytics = formulaInfo.analytics.install[period];
if (analytics) {
const totalInstalls = Object.values(analytics).reduce((sum, count) => sum + count, 0);
stats.push({
platform: 'homebrew',
packageName: formulaName,
formulaName,
tapName: this.getTapName(formulaName),
version: formulaInfo.version,
installCount: totalInstalls,
downloadCount: totalInstalls, // For compatibility with BaseDownloadStats
metadata: {
analyticsPeriod: period,
analyticsData: analytics,
dependencies: formulaInfo.dependencies,
conflicts: formulaInfo.conflicts
}
});
}
}
// If no analytics available, create a basic stat entry
if (stats.length === 0) {
stats.push({
platform: 'homebrew',
packageName: formulaName,
formulaName,
tapName: this.getTapName(formulaName),
version: formulaInfo.version,
installCount: formulaInfo.installed.length,
downloadCount: formulaInfo.installed.length,
metadata: {
installedVersions: formulaInfo.installed,
dependencies: formulaInfo.dependencies,
conflicts: formulaInfo.conflicts
}
});
}
return stats;
} catch (error) {
console.error(`Error fetching Homebrew stats for ${formulaName}:`, error);
return [];
}
}
async getLatestVersion(formulaName: string): Promise<string | null> {
try {
const formulaInfo = await this.getPackageInfo(formulaName);
return formulaInfo.version || null;
} catch (error) {
console.error(`Error fetching latest version for ${formulaName}:`, error);
return null;
}
}
async getPackageInfo(formulaName: string): Promise<HomebrewFormulaInfo> {
const response = await fetch(`${this.baseUrl}/formula/${formulaName}.json`);
if (!response.ok) {
throw new Error(`Failed to fetch formula info for ${formulaName}`);
}
return response.json();
}
async getTapInfo(tapName: string): Promise<HomebrewTapInfo> {
// Homebrew taps are GitHub repositories
const response = await fetch(`https://api.github.com/repos/Homebrew/${tapName}`, {
headers: this.githubToken ? {
'Authorization': `token ${this.githubToken}`,
'Accept': 'application/vnd.github.v3+json'
} : {
'Accept': 'application/vnd.github.v3+json'
}
});
if (!response.ok) {
throw new Error(`Failed to fetch tap info for ${tapName}`);
}
return response.json();
}
async getAllFormulae(): Promise<string[]> {
try {
const response = await fetch(`${this.baseUrl}/formula.json`);
if (!response.ok) {
throw new Error('Failed to fetch all formulae');
}
const formulae = await response.json();
return formulae.map((formula: any) => formula.name);
} catch (error) {
console.error('Error fetching all formulae:', error);
return [];
}
}
async getAnalytics(formulaName: string, period: '30d' | '90d' | '365d' = '30d'): Promise<{
date: string;
installs: number;
}[]> {
try {
const response = await fetch(`${this.baseUrl}/analytics/install/${period}/${formulaName}.json`);
if (!response.ok) {
throw new Error(`Failed to fetch analytics for ${formulaName}`);
}
const data = await response.json();
return data.analytics || [];
} catch (error) {
console.error(`Error fetching analytics for ${formulaName}:`, error);
return [];
}
}
private getTapName(formulaName: string): string {
// Most formulae are in the homebrew/core tap
// This is a simplified implementation
return 'homebrew/core';
}
}
export default HomebrewTracker;

View File

@@ -1,109 +0,0 @@
/**
* NPM Package Download Tracker
* Uses the npm registry API to fetch download statistics
*/
import type { BaseDownloadStats, PlatformTracker } from '../types';
export interface NpmDownloadStats extends BaseDownloadStats {
platform: 'npm';
registry: string;
distTags?: Record<string, string>;
dependencies?: Record<string, string>;
}
export interface NpmPackageInfo {
name: string;
version: string;
description?: string;
homepage?: string;
repository?: {
type: string;
url: string;
};
distTags: Record<string, string>;
time: Record<string, string>;
versions: Record<string, any>;
}
export class NpmTracker implements PlatformTracker {
name = 'npm';
private baseUrl = 'https://registry.npmjs.org';
async getDownloadStats(packageName: string, options?: {
period?: 'daily' | 'weekly' | 'monthly' | 'total';
startDate?: Date;
endDate?: Date;
}): Promise<NpmDownloadStats[]> {
try {
// Get package info
const packageInfo = await this.getPackageInfo(packageName);
// Get download stats from npm registry
const stats = await this.fetchDownloadStats(packageName, options);
return stats.map(stat => ({
...stat,
platform: 'npm' as const,
registry: this.baseUrl,
distTags: packageInfo.distTags,
dependencies: packageInfo.versions[packageInfo.distTags?.latest]?.dependencies
}));
} catch (error) {
console.error(`Error fetching NPM stats for ${packageName}:`, error);
return [];
}
}
async getLatestVersion(packageName: string): Promise<string | null> {
try {
const packageInfo = await this.getPackageInfo(packageName);
return packageInfo.distTags?.latest || null;
} catch (error) {
console.error(`Error fetching latest version for ${packageName}:`, error);
return null;
}
}
async getPackageInfo(packageName: string): Promise<NpmPackageInfo> {
const response = await fetch(`${this.baseUrl}/${packageName}`);
if (!response.ok) {
throw new Error(`Failed to fetch package info for ${packageName}`);
}
return response.json();
}
private async fetchDownloadStats(packageName: string, options?: {
period?: 'daily' | 'weekly' | 'monthly' | 'total';
startDate?: Date;
endDate?: Date;
}): Promise<NpmDownloadStats[]> {
// Note: NPM registry doesn't provide direct download stats via API
// This would typically require using npm-stat.com or similar services
// For now, we'll return a placeholder structure
const now = new Date();
const stats: NpmDownloadStats[] = [];
// Simulate daily stats for the last 30 days
for (let i = 0; i < 30; i++) {
const date = new Date(now);
date.setDate(date.getDate() - i);
stats.push({
platform: 'npm',
packageName,
downloadCount: Math.floor(Math.random() * 1000) + 100, // Simulated data
registry: this.baseUrl,
metadata: {
source: 'npm-registry',
simulated: true
}
});
}
return stats;
}
}
export default NpmTracker;

View File

@@ -1,286 +0,0 @@
/**
* Postman Collection Download/Fork Tracker
* Uses Postman API to fetch collection statistics
*/
import type { BaseDownloadStats, PlatformTracker } from '../types';
export interface PostmanDownloadStats extends BaseDownloadStats {
platform: 'postman';
collectionId: string;
collectionName: string;
version: string;
forkCount: number;
downloadCount: number;
viewCount: number;
author: string;
publishedDate: Date;
}
export interface PostmanCollectionInfo {
id: string;
name: string;
description?: string;
version: string;
author: {
id: string;
name: string;
username: string;
};
publishedAt: string;
updatedAt: string;
forkCount: number;
downloadCount: number;
viewCount: number;
schema: string;
info: {
name: string;
description?: string;
version: string;
schema: string;
};
item: any[];
variable: any[];
}
export interface PostmanWorkspaceInfo {
id: string;
name: string;
type: 'personal' | 'team' | 'private';
description?: string;
collections: PostmanCollectionInfo[];
}
export class PostmanTracker implements PlatformTracker {
name = 'postman';
private baseUrl = 'https://api.getpostman.com';
private apiKey?: string;
constructor(apiKey?: string) {
this.apiKey = apiKey;
}
async getDownloadStats(collectionId: string, options?: {
version?: string;
startDate?: Date;
endDate?: Date;
}): Promise<PostmanDownloadStats[]> {
try {
const collectionInfo = await this.getPackageInfo(collectionId);
const stats: PostmanDownloadStats[] = [];
// Get collection versions if available
const versions = await this.getCollectionVersions(collectionId);
for (const version of versions) {
if (options?.version && version.version !== options.version) {
continue;
}
const publishedDate = new Date(version.publishedAt);
// Filter by date range if specified
if (options?.startDate && publishedDate < options.startDate) {
continue;
}
if (options?.endDate && publishedDate > options.endDate) {
continue;
}
stats.push({
platform: 'postman',
packageName: collectionId,
collectionId,
collectionName: version.name,
version: version.version,
forkCount: version.forkCount || 0,
downloadCount: version.downloadCount || 0,
viewCount: version.viewCount || 0,
author: version.author?.name || 'Unknown',
publishedDate,
metadata: {
authorId: version.author?.id,
authorUsername: version.author?.username,
schema: version.schema,
itemCount: version.item?.length || 0
}
});
}
return stats;
} catch (error) {
console.error(`Error fetching Postman stats for ${collectionId}:`, error);
return [];
}
}
async getLatestVersion(collectionId: string): Promise<string | null> {
try {
const collectionInfo = await this.getPackageInfo(collectionId);
return collectionInfo.version || null;
} catch (error) {
console.error(`Error fetching latest version for ${collectionId}:`, error);
return null;
}
}
async getPackageInfo(collectionId: string): Promise<PostmanCollectionInfo> {
const headers: Record<string, string> = {
'Accept': 'application/json'
};
if (this.apiKey) {
headers['X-API-Key'] = this.apiKey;
}
const response = await fetch(`${this.baseUrl}/collections/${collectionId}`, {
headers
});
if (!response.ok) {
throw new Error(`Failed to fetch collection info for ${collectionId}`);
}
return response.json();
}
async getCollectionVersions(collectionId: string): Promise<PostmanCollectionInfo[]> {
try {
const headers: Record<string, string> = {
'Accept': 'application/json'
};
if (this.apiKey) {
headers['X-API-Key'] = this.apiKey;
}
const response = await fetch(`${this.baseUrl}/collections/${collectionId}/versions`, {
headers
});
if (!response.ok) {
throw new Error(`Failed to fetch versions for ${collectionId}`);
}
return response.json();
} catch (error) {
console.error(`Error fetching versions for ${collectionId}:`, error);
return [];
}
}
async searchCollections(query: string, options?: {
workspaceId?: string;
limit?: number;
offset?: number;
}): Promise<{
collections: PostmanCollectionInfo[];
totalCount: number;
}> {
try {
const headers: Record<string, string> = {
'Accept': 'application/json'
};
if (this.apiKey) {
headers['X-API-Key'] = this.apiKey;
}
const params = new URLSearchParams({
q: query,
limit: (options?.limit || 50).toString(),
offset: (options?.offset || 0).toString()
});
if (options?.workspaceId) {
params.set('workspace', options.workspaceId);
}
const response = await fetch(`${this.baseUrl}/search?${params}`, {
headers
});
if (!response.ok) {
throw new Error('Failed to search collections');
}
return response.json();
} catch (error) {
console.error('Error searching collections:', error);
return {
collections: [],
totalCount: 0
};
}
}
async getWorkspaceCollections(workspaceId: string): Promise<PostmanCollectionInfo[]> {
try {
const headers: Record<string, string> = {
'Accept': 'application/json'
};
if (this.apiKey) {
headers['X-API-Key'] = this.apiKey;
}
const response = await fetch(`${this.baseUrl}/workspaces/${workspaceId}/collections`, {
headers
});
if (!response.ok) {
throw new Error(`Failed to fetch workspace collections for ${workspaceId}`);
}
return response.json();
} catch (error) {
console.error(`Error fetching workspace collections for ${workspaceId}:`, error);
return [];
}
}
async getCollectionAnalytics(collectionId: string): Promise<{
totalDownloads: number;
totalForks: number;
totalViews: number;
downloadsByVersion: Record<string, number>;
forksByVersion: Record<string, number>;
}> {
try {
const versions = await this.getCollectionVersions(collectionId);
const downloadsByVersion: Record<string, number> = {};
const forksByVersion: Record<string, number> = {};
let totalDownloads = 0;
let totalForks = 0;
let totalViews = 0;
for (const version of versions) {
downloadsByVersion[version.version] = version.downloadCount || 0;
forksByVersion[version.version] = version.forkCount || 0;
totalDownloads += version.downloadCount || 0;
totalForks += version.forkCount || 0;
totalViews += version.viewCount || 0;
}
return {
totalDownloads,
totalForks,
totalViews,
downloadsByVersion,
forksByVersion
};
} catch (error) {
console.error(`Error fetching analytics for ${collectionId}:`, error);
return {
totalDownloads: 0,
totalForks: 0,
totalViews: 0,
downloadsByVersion: {},
forksByVersion: {}
};
}
}
}
export default PostmanTracker;

View File

@@ -1,212 +0,0 @@
/**
* PowerShell Gallery Module Download Tracker
* Uses PowerShell Gallery API to fetch download statistics
*/
import type { BaseDownloadStats, PlatformTracker } from '../types';
export interface PowerShellDownloadStats extends BaseDownloadStats {
platform: 'powershell';
moduleName: string;
version: string;
author: string;
description?: string;
tags: string[];
downloadCount: number;
publishedDate: Date;
}
export interface PowerShellModuleInfo {
Id: string;
Version: string;
Title: string;
Author: string;
Description: string;
Tags: string[];
PublishedDate: string;
UpdatedDate: string;
DownloadCount: number;
IsLatestVersion: boolean;
Dependencies: Array<{
id: string;
version: string;
}>;
PowerShellVersion: string;
ProjectUri?: string;
LicenseUri?: string;
IconUri?: string;
ReleaseNotes?: string;
}
export interface PowerShellSearchResult {
TotalCount: number;
Results: PowerShellModuleInfo[];
}
export class PowerShellTracker implements PlatformTracker {
name = 'powershell';
private baseUrl = 'https://www.powershellgallery.com/api/v2';
async getDownloadStats(moduleName: string, options?: {
version?: string;
startDate?: Date;
endDate?: Date;
}): Promise<PowerShellDownloadStats[]> {
try {
const moduleInfo = await this.getPackageInfo(moduleName);
const stats: PowerShellDownloadStats[] = [];
// Get all versions of the module
const allVersions = await this.getAllVersions(moduleName);
for (const version of allVersions) {
if (options?.version && version.Version !== options.version) {
continue;
}
const publishedDate = new Date(version.PublishedDate);
// Filter by date range if specified
if (options?.startDate && publishedDate < options.startDate) {
continue;
}
if (options?.endDate && publishedDate > options.endDate) {
continue;
}
stats.push({
platform: 'powershell',
packageName: moduleName,
moduleName,
version: version.Version,
author: version.Author,
description: version.Description,
tags: version.Tags,
downloadCount: version.DownloadCount,
publishedDate,
metadata: {
isLatestVersion: version.IsLatestVersion,
dependencies: version.Dependencies,
powershellVersion: version.PowerShellVersion,
projectUri: version.ProjectUri,
licenseUri: version.LicenseUri,
releaseNotes: version.ReleaseNotes
}
});
}
return stats;
} catch (error) {
console.error(`Error fetching PowerShell stats for ${moduleName}:`, error);
return [];
}
}
async getLatestVersion(moduleName: string): Promise<string | null> {
try {
const moduleInfo = await this.getPackageInfo(moduleName);
return moduleInfo.Version || null;
} catch (error) {
console.error(`Error fetching latest version for ${moduleName}:`, error);
return null;
}
}
async getPackageInfo(moduleName: string): Promise<PowerShellModuleInfo> {
const response = await fetch(`${this.baseUrl}/package/${moduleName}`);
if (!response.ok) {
throw new Error(`Failed to fetch module info for ${moduleName}`);
}
return response.json();
}
async getAllVersions(moduleName: string): Promise<PowerShellModuleInfo[]> {
try {
const response = await fetch(`${this.baseUrl}/package/${moduleName}/versions`);
if (!response.ok) {
throw new Error(`Failed to fetch versions for ${moduleName}`);
}
return response.json();
} catch (error) {
console.error(`Error fetching versions for ${moduleName}:`, error);
return [];
}
}
async searchModules(query: string, options?: {
includePrerelease?: boolean;
skip?: number;
take?: number;
}): Promise<PowerShellSearchResult> {
const params = new URLSearchParams({
$filter: `IsLatestVersion eq true`,
$orderby: 'DownloadCount desc',
$skip: (options?.skip || 0).toString(),
$top: (options?.take || 50).toString()
});
if (query) {
params.set('$filter', `${params.get('$filter')} and substringof('${query}', Id)`);
}
const response = await fetch(`${this.baseUrl}/search?${params}`);
if (!response.ok) {
throw new Error('Failed to search modules');
}
return response.json();
}
async getPopularModules(limit: number = 50): Promise<PowerShellModuleInfo[]> {
try {
const searchResult = await this.searchModules('', { take: limit });
return searchResult.Results;
} catch (error) {
console.error('Error fetching popular modules:', error);
return [];
}
}
async getModuleAnalytics(moduleName: string): Promise<{
totalDownloads: number;
downloadsByVersion: Record<string, number>;
downloadsByDate: Array<{
date: string;
downloads: number;
}>;
}> {
try {
const allVersions = await this.getAllVersions(moduleName);
const downloadsByVersion: Record<string, number> = {};
let totalDownloads = 0;
for (const version of allVersions) {
downloadsByVersion[version.Version] = version.DownloadCount;
totalDownloads += version.DownloadCount;
}
// Note: PowerShell Gallery doesn't provide detailed time-based analytics
// This is a simplified implementation
const downloadsByDate = allVersions.map(version => ({
date: version.PublishedDate,
downloads: version.DownloadCount
}));
return {
totalDownloads,
downloadsByVersion,
downloadsByDate
};
} catch (error) {
console.error(`Error fetching analytics for ${moduleName}:`, error);
return {
totalDownloads: 0,
downloadsByVersion: {},
downloadsByDate: []
};
}
}
}
export default PowerShellTracker;

View File

@@ -1,146 +0,0 @@
/**
* PyPI Package Download Tracker
* Uses PyPI JSON API to fetch download statistics
*/
import type { BaseDownloadStats, PlatformTracker } from '../types';
export interface PyPiDownloadStats extends BaseDownloadStats {
platform: 'pypi';
packageName: string;
version: string;
fileType: string;
pythonVersion?: string;
uploadTime: Date;
}
export interface PyPiPackageInfo {
info: {
name: string;
version: string;
summary?: string;
description?: string;
home_page?: string;
author?: string;
author_email?: string;
license?: string;
requires_python?: string;
project_urls?: Record<string, string>;
};
releases: Record<string, Array<{
filename: string;
url: string;
size: number;
upload_time: string;
file_type: string;
python_version?: string;
download_count?: number;
}>>;
urls: Array<{
filename: string;
url: string;
size: number;
upload_time: string;
file_type: string;
python_version?: string;
download_count?: number;
}>;
}
export class PyPiTracker implements PlatformTracker {
name = 'pypi';
private baseUrl = 'https://pypi.org/pypi';
async getDownloadStats(packageName: string, options?: {
version?: string;
fileType?: string;
startDate?: Date;
endDate?: Date;
}): Promise<PyPiDownloadStats[]> {
try {
const packageInfo = await this.getPackageInfo(packageName);
const stats: PyPiDownloadStats[] = [];
// Process releases
for (const [version, files] of Object.entries(packageInfo.releases)) {
if (options?.version && version !== options.version) {
continue;
}
for (const file of files) {
if (options?.fileType && file.file_type !== options.fileType) {
continue;
}
// Filter by date range if specified
const uploadTime = new Date(file.upload_time);
if (options?.startDate && uploadTime < options.startDate) {
continue;
}
if (options?.endDate && uploadTime > options.endDate) {
continue;
}
stats.push({
platform: 'pypi',
packageName,
version,
fileType: file.file_type,
pythonVersion: file.python_version,
uploadTime,
downloadCount: file.download_count || 0,
metadata: {
filename: file.filename,
fileSize: file.size,
url: file.url
}
});
}
}
return stats;
} catch (error) {
console.error(`Error fetching PyPI stats for ${packageName}:`, error);
return [];
}
}
async getLatestVersion(packageName: string): Promise<string | null> {
try {
const packageInfo = await this.getPackageInfo(packageName);
return packageInfo.info.version || null;
} catch (error) {
console.error(`Error fetching latest version for ${packageName}:`, error);
return null;
}
}
async getPackageInfo(packageName: string): Promise<PyPiPackageInfo> {
const response = await fetch(`${this.baseUrl}/${packageName}/json`);
if (!response.ok) {
throw new Error(`Failed to fetch package info for ${packageName}`);
}
return response.json();
}
async getDownloadCounts(packageName: string, period: 'day' | 'week' | 'month' = 'month'): Promise<{
date: string;
downloads: number;
}[]> {
try {
// PyPI provides download stats via a separate endpoint
const response = await fetch(`https://pypi.org/pypi/${packageName}/stats/${period}`);
if (!response.ok) {
throw new Error(`Failed to fetch download stats for ${packageName}`);
}
const data = await response.json();
return data.data || [];
} catch (error) {
console.error(`Error fetching download counts for ${packageName}:`, error);
return [];
}
}
}
export default PyPiTracker;

View File

@@ -1,30 +0,0 @@
/**
* Shared types for usage statistics tracking across all platforms
*/
export interface BaseDownloadStats {
platform: string;
packageName: string;
version?: string;
downloadCount: number;
metadata?: Record<string, any>;
}
export interface PlatformTracker {
name: string;
getDownloadStats(packageName: string, options?: any): Promise<BaseDownloadStats[]>;
getLatestVersion(packageName: string): Promise<string | null>;
getPackageInfo(packageName: string): Promise<any>;
}
export interface TrackingConfig {
npmPackages?: string[];
goModules?: string[];
pythonPackages?: string[];
powershellModules?: string[];
homebrewPackages?: string[];
githubRepos?: string[];
postmanCollections?: string[];
updateInterval?: number; // in milliseconds
enableLogging?: boolean;
}

148
src/utils.ts Normal file
View File

@@ -0,0 +1,148 @@
import * as core from '@actions/core';
import { CategoryScale, Chart, LinearScale, LineController, LineElement, PointElement, BarController, BarElement } from 'chart.js';
import { readFile, writeFile } from 'fs/promises';
import { writeFileSync } from 'node:fs';
import { Canvas } from 'skia-canvas';
import type { MetricResult } from "./collectors/types";
import { addRepoDetails, formatGitHubSummary } from './summaries/github';
import { addNpmDetails, formatNpmSummary } from './summaries/npm';
import { formatPowerShellSummary, addPowerShellDetails } from './summaries/powershell';
import { addPypiDetails, addPypiCharts, formatPypiSummary } from './summaries/pypi';
Chart.register([
CategoryScale,
LineController,
LineElement,
LinearScale,
PointElement,
BarController,
BarElement
]);
/**
* Parse comma-separated inputs into arrays
* @param input - The input string to parse
* @returns An array of trimmed, non-empty items
*/
function parseCommaSeparatedInputs(input: string) {
return input ? input.split(',').map(item => item.trim()).filter(item => item) : []
}
export function getInputs() {
// Get all inputs from action.yml
const npmPackages = core.getInput('npm-packages')
const githubRepositories = core.getInput('github-repositories')
const pypiPackages = core.getInput('pypi-packages')
const powershellModules = core.getInput('powershell-modules')
const jsonOutputPath = core.getInput('json-output-path')
const updateReadme = core.getBooleanInput('update-readme')
const commitMessage = core.getInput('commit-message')
const readmePath = core.getInput('readme-path')
return {
npmPackages: parseCommaSeparatedInputs(npmPackages),
githubRepositories: parseCommaSeparatedInputs(githubRepositories),
pypiPackages: parseCommaSeparatedInputs(pypiPackages),
powershellModules: parseCommaSeparatedInputs(powershellModules),
jsonOutputPath,
updateReadme,
commitMessage,
readmePath,
}
}
const MetricsPlaceHolderRegex = /<!-- METRICS_START -->[\s\S]*<!-- METRICS_END -->/
function formatSummary(summary: string) {
return `<!-- METRICS_START -->\n${summary}\n<!-- METRICS_END -->`
}
const PlatformMap = {
"NPM": "JavaScript/TypeScript",
"PyPI": "Python",
"PowerShell Gallery": undefined,
"GitHub": undefined,
}
export async function createSummary(metrics: MetricResult[]) {
const platforms = metrics.map(metric => metric.platform).filter((value, index, self) => self.indexOf(value) === index)
console.log(platforms)
console.log(metrics)
let summary = `# Usage Statistics
Last updated: ${new Date().toLocaleString()}
Below are stats from artifacts tracked across ${platforms.slice(0, -1).join(', ')} and ${platforms.slice(-1)}.
`
for (const platform of platforms) {
const platformMetrics = metrics.filter(metric => metric.platform === platform)
const platformLanguage = PlatformMap[platform as keyof typeof PlatformMap]
summary += `### ${platform}${platformLanguage ? ` (${platformLanguage})` : ''}: \n\n`
switch (platform) {
case "NPM":
summary = formatNpmSummary(summary, platformMetrics)
break;
case "GitHub":
summary = formatGitHubSummary(summary, platformMetrics)
break;
case "PyPI":
summary = formatPypiSummary(summary, platformMetrics)
break;
case "PowerShell":
summary = formatPowerShellSummary(summary, platformMetrics)
break;
default:
let platformDownloadTotal = 0
summary += `| Package | Downloads |\n`
summary += `| --- | --- |\n`
for (const metric of platformMetrics) {
summary += `| ${metric.name} | ${metric.metrics?.downloadCount?.toLocaleString() || 0} |\n`
platformDownloadTotal += metric.metrics?.downloadCount || 0
}
summary += `| **Total** | **${platformDownloadTotal.toLocaleString()}** |\n`
break;
}
summary += `\n`
// Add detailed information for each platform
switch (platform) {
case "GitHub":
summary = await addRepoDetails(summary, platformMetrics)
break;
case "PyPI":
summary = addPypiDetails(summary, platformMetrics)
summary = await addPypiCharts(summary, platformMetrics)
break;
case "NPM":
summary = await addNpmDetails(summary, platformMetrics)
break;
case "PowerShell":
summary = await addPowerShellDetails(summary, platformMetrics)
break;
default:
break;
}
summary += '\n'
}
return summary
}
export async function updateRepositoryReadme(metrics: MetricResult[], readmePath: string) {
const currentReadme = await readFile(readmePath, 'utf8')
const summary = await createSummary(metrics)
const updatedReadme = currentReadme.replace(MetricsPlaceHolderRegex, formatSummary(summary))
await writeFile(readmePath, updatedReadme)
}

8028
stats.json Normal file

File diff suppressed because it is too large Load Diff

83
summary.md Normal file
View File

@@ -0,0 +1,83 @@
## 📊 Usage Statistics
Last updated: 2025-07-31T16:09:10.951Z
**Summary:**
- **Total Sources**: 26
- **Platforms**: npm, github, pypi, powershell, go
- **Total Monthly Downloads**: 4640.4M
- **Total Stars**: 1103.1K
- **Total Forks**: 234.0K
## 📦 Package Statistics
| Platform | Name | Downloads (Monthly) | Downloads (Total) | Stars | Forks | Enhanced Metrics |
|---|---|---|---|---|---|---|
| NPM | express | 196.7M | 1884.3M | — | — | Bundle: 568.4KB, Age: 5327 days, Versions: 283 |
| NPM | react | 179.1M | 1632.6M | — | — | Bundle: 7.4KB, Age: 5026 days, Versions: 2423 |
| NPM | lodash | 347.7M | 3194.1M | — | — | Bundle: 69.8KB, Age: 4846 days, Versions: 114 |
| NPM | axios | 286.2M | 2968.9M | — | — | Bundle: 36.0KB, Age: 3988 days, Versions: 116 |
| NPM | moment | 108.3M | 1154.0M | — | — | Bundle: 294.9KB, Age: 5035 days, Versions: 76 |
| NPM | vue | 28.8M | 304.2M | — | — | Bundle: 126.0KB, Age: 4254 days, Versions: 538 |
| GitHub | facebook/react | — | — | 237.7K | 49.0K | Watchers: 237.7K, Releases: 30 |
| GitHub | microsoft/vscode | — | — | 175.2K | 34.1K | Watchers: 175.2K, Releases: 30 |
| GitHub | vercel/next.js | — | — | 133.5K | 29.0K | Watchers: 133.5K, Releases: 30 |
| GitHub | vuejs/vue | — | — | 209.2K | 33.7K | Watchers: 209.2K, Releases: 30 |
| GitHub | tensorflow/tensorflow | — | — | 191.0K | 74.8K | Watchers: 191.0K, Releases: 30 |
| PyPI | requests | 1423.9M | 716.0M | — | — | Python breakdown, Platform breakdown |
| PyPI | numpy | 899.7M | 451.0M | — | — | Python breakdown, Platform breakdown |
| PyPI | django | 48.9M | 24.5M | — | — | Python breakdown, Platform breakdown |
| PyPI | flask | 226.5M | 113.2M | — | — | Python breakdown, Platform breakdown |
| PyPI | pandas | 709.0M | 356.4M | — | — | Python breakdown, Platform breakdown |
| PyPI | matplotlib | 185.3M | 92.8M | — | — | Python breakdown, Platform breakdown |
| PowerShell | PowerShellGet | — | — | — | — | Versions: 81 |
| PowerShell | PSReadLine | — | — | — | — | Versions: 46 |
| PowerShell | Pester | — | — | — | — | Versions: 100 |
| PowerShell | PSScriptAnalyzer | — | — | — | — | Versions: 37 |
| PowerShell | dbatools | — | — | — | — | Versions: 100 |
| Go | github.com/gin-gonic/gin | — | — | 83.4K | 8.3K | Versions: 26, Watchers: 83.4K, Releases: 27 |
| Go | github.com/go-chi/chi | — | — | 20.2K | 1.0K | Versions: 33, Watchers: 20.2K, Releases: 30 |
| Go | github.com/gorilla/mux | — | — | 21.5K | 1.9K | Versions: 14, Watchers: 21.5K, Releases: 15 |
| Go | github.com/labstack/echo | — | — | 31.3K | 2.3K | Versions: 62, Watchers: 31.3K, Releases: 30 |
## 🚀 Platform Summaries
### NPM Enhanced Metrics
- **Total Monthly Downloads**: 1146.9M
- **Total Stars**: 0
- **Total Forks**: 0
**Recent Test Results (6 packages):**
- **Enhanced Metrics**: 3 types available
### GITHUB Enhanced Metrics
- **Total Monthly Downloads**: 0
- **Total Stars**: 946.6K
- **Total Forks**: 220.6K
**Recent Test Results (5 packages):**
- **Enhanced Metrics**: 4 types available
### PYPI Enhanced Metrics
- **Total Monthly Downloads**: 3493.4M
- **Total Stars**: 0
- **Total Forks**: 0
**Recent Test Results (6 packages):**
- **Enhanced Metrics**: 2 types available
### POWERSHELL Enhanced Metrics
- **Total Monthly Downloads**: 0
- **Total Stars**: 0
- **Total Forks**: 0
**Recent Test Results (5 packages):**
- **Enhanced Metrics**: 1 types available
### GO Enhanced Metrics
- **Total Monthly Downloads**: 0
- **Total Stars**: 156.5K
- **Total Forks**: 13.5K
**Recent Test Results (4 packages):**
- **Enhanced Metrics**: 5 types available

1029
yarn.lock Normal file

File diff suppressed because it is too large Load Diff