saving current implementation of the action using the new pypi stats site
20
.dev.env
Normal 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
@@ -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
@@ -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
@@ -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`.
|
||||
107
INSTALLATION.md
@@ -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)
|
||||
260
PUBLISHING.md
@@ -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
@@ -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
@@ -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** | | | | |
|
||||
|
||||

|
||||

|
||||
|
||||
### 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
|
||||
|
||||
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
### 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
|
||||
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
### 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
|
||||
|
||||
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
<!-- METRICS_END -->
|
||||
62
action.yml
@@ -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
@@ -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=="],
|
||||
}
|
||||
}
|
||||
|
||||
11
bunfig.toml
@@ -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
|
||||
|
After Width: | Height: | Size: 334 KiB |
554
charts/github/sailpoint-oss-sailpoint-cli-release-downloads.svg
Normal file
|
After Width: | Height: | Size: 182 KiB |
|
After Width: | Height: | Size: 121 KiB |
428
charts/npm/sailpoint-api-client-cumulative-downloads.svg
Normal file
|
After Width: | Height: | Size: 334 KiB |
428
charts/npm/sailpoint-api-client-new-downloads-by-month.svg
Normal file
|
After Width: | Height: | Size: 327 KiB |
681
charts/powershell/powershell-combined-downloads.svg
Normal file
|
After Width: | Height: | Size: 801 KiB |
729
charts/powershell/powershell-cumulative-downloads.svg
Normal file
|
After Width: | Height: | Size: 842 KiB |
1093
charts/pypi/sailpoint-pypi-installer.svg
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
509
charts/pypi/sailpoint-pypi-overall.svg
Normal file
|
After Width: | Height: | Size: 575 KiB |
127
charts/pypi/sailpoint-pypi-python-major-totals.svg
Normal file
|
After Width: | Height: | Size: 79 KiB |
505
charts/pypi/sailpoint-pypi-python-major.svg
Normal file
|
After Width: | Height: | Size: 565 KiB |
217
charts/pypi/sailpoint-pypi-python-minor-totals.svg
Normal file
|
After Width: | Height: | Size: 117 KiB |
1093
charts/pypi/sailpoint-pypi-python-minor.svg
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
155
charts/pypi/sailpoint-pypi-system-totals.svg
Normal file
|
After Width: | Height: | Size: 76 KiB |
673
charts/pypi/sailpoint-pypi-system.svg
Normal file
|
After Width: | Height: | Size: 875 KiB |
121
dist/action.js
vendored
80
dist/index.js
vendored
@@ -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
|
||||
};
|
||||
@@ -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
|
||||
@@ -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
|
||||
26
package.json
@@ -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
|
||||
}
|
||||
306
src/action.ts
@@ -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();
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
363
src/collectors/powershell.ts
Normal 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
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
262
src/index.ts
@@ -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
@@ -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 += `\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
@@ -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 += `\n`
|
||||
}
|
||||
return summary
|
||||
}
|
||||
288
src/summaries/powershell.ts
Normal 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 += `\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
@@ -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 += `\n`
|
||||
}
|
||||
return summary
|
||||
}
|
||||
@@ -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 {};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
@@ -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
83
summary.md
Normal 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
|
||||