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
|
# 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
|
## 🚀 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
|
- 🎭 **Preview Mode**: Test with mock data without external API calls
|
||||||
- 📄 **Flexible Outputs**: JSON, CSV, and human-readable reports
|
- 📄 **Flexible Outputs**: JSON, CSV, and human-readable reports
|
||||||
- 📝 **README Integration**: Auto-update README with statistics
|
- 📝 **README Integration**: Auto-update README with statistics
|
||||||
- ⚙️ **Configurable**: Custom configurations via JSON or preset modes
|
- ⚙️ **Configurable**: Custom configurations via JSON or preset modes
|
||||||
- 🔄 **GitHub Actions Ready**: Built for CI/CD workflows
|
- 🔄 **GitHub Actions Ready**: Built for CI/CD workflows
|
||||||
- 🧪 **Comprehensive Testing**: Full test suite with Bun
|
- 🧪 **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
|
## 📦 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
|
### As a GitHub Action
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -26,12 +106,14 @@ A comprehensive GitHub Action for tracking download statistics across multiple p
|
|||||||
homebrew-formulas: 'git,node'
|
homebrew-formulas: 'git,node'
|
||||||
powershell-modules: 'PowerShellGet,PSReadLine'
|
powershell-modules: 'PowerShellGet,PSReadLine'
|
||||||
postman-collections: '12345,67890'
|
postman-collections: '12345,67890'
|
||||||
go-modules: 'github.com/gin-gonic/gin,github.com/go-chi/chi'
|
# go-modules removed
|
||||||
json-output-path: 'stats.json'
|
json-output-path: 'stats.json'
|
||||||
csv-output-path: 'stats.csv'
|
csv-output-path: 'stats.csv'
|
||||||
report-output-path: 'report.md'
|
report-output-path: 'report.md'
|
||||||
update-readme: 'true'
|
update-readme: 'true'
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
# env:
|
||||||
|
# PYPI_STATS_BASE_URL: https://your-host
|
||||||
```
|
```
|
||||||
|
|
||||||
### Local Development
|
### Local Development
|
||||||
@@ -50,6 +132,51 @@ bun preview
|
|||||||
bun test
|
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
|
## 🔧 Configuration
|
||||||
|
|
||||||
### Input Parameters
|
### Input Parameters
|
||||||
@@ -62,7 +189,7 @@ bun test
|
|||||||
| `homebrew-formulas` | Comma-separated list of Homebrew formulas | No | (empty) |
|
| `homebrew-formulas` | Comma-separated list of Homebrew formulas | No | (empty) |
|
||||||
| `powershell-modules` | Comma-separated list of PowerShell modules | No | (empty) |
|
| `powershell-modules` | Comma-separated list of PowerShell modules | No | (empty) |
|
||||||
| `postman-collections` | Comma-separated list of Postman collection IDs | 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` |
|
| `json-output-path` | Path for JSON output | No | `stats.json` |
|
||||||
| `csv-output-path` | Path for CSV output | No | (empty) |
|
| `csv-output-path` | Path for CSV output | No | (empty) |
|
||||||
| `report-output-path` | Path for human-readable report | No | (empty) |
|
| `report-output-path` | Path for human-readable report | No | (empty) |
|
||||||
@@ -102,7 +229,9 @@ bun test
|
|||||||
homebrew-formulas: 'git,node'
|
homebrew-formulas: 'git,node'
|
||||||
powershell-modules: 'PowerShellGet,PSReadLine'
|
powershell-modules: 'PowerShellGet,PSReadLine'
|
||||||
postman-collections: '12345,67890'
|
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
|
### Outputs
|
||||||
@@ -145,10 +274,12 @@ jobs:
|
|||||||
homebrew-formulas: 'git,node'
|
homebrew-formulas: 'git,node'
|
||||||
powershell-modules: 'PowerShellGet,PSReadLine'
|
powershell-modules: 'PowerShellGet,PSReadLine'
|
||||||
postman-collections: '12345,67890'
|
postman-collections: '12345,67890'
|
||||||
go-modules: 'github.com/gin-gonic/gin,github.com/go-chi/chi'
|
# go-modules removed
|
||||||
json-output-path: 'stats.json'
|
json-output-path: 'stats.json'
|
||||||
update-readme: 'true'
|
update-readme: 'true'
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
env:
|
||||||
|
GOOGLE_CLOUD_CREDENTIALS: ${{ secrets.GOOGLE_CLOUD_CREDENTIALS }}
|
||||||
|
|
||||||
- name: Commit and push changes
|
- name: Commit and push changes
|
||||||
run: |
|
run: |
|
||||||
@@ -171,7 +302,7 @@ jobs:
|
|||||||
homebrew-formulas: 'git,node'
|
homebrew-formulas: 'git,node'
|
||||||
powershell-modules: 'PowerShellGet,PSReadLine'
|
powershell-modules: 'PowerShellGet,PSReadLine'
|
||||||
postman-collections: '12345,67890'
|
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'
|
json-output-path: 'data/stats.json'
|
||||||
csv-output-path: 'data/stats.csv'
|
csv-output-path: 'data/stats.csv'
|
||||||
report-output-path: 'docs/usage-report.md'
|
report-output-path: 'docs/usage-report.md'
|
||||||
@@ -180,6 +311,8 @@ jobs:
|
|||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
postman-api-key: ${{ secrets.POSTMAN_API_KEY }}
|
postman-api-key: ${{ secrets.POSTMAN_API_KEY }}
|
||||||
commit-message: 'feat: update usage statistics with detailed report'
|
commit-message: 'feat: update usage statistics with detailed report'
|
||||||
|
env:
|
||||||
|
GOOGLE_CLOUD_CREDENTIALS: ${{ secrets.GOOGLE_CLOUD_CREDENTIALS }}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Preview Mode for Testing
|
### Preview Mode for Testing
|
||||||
@@ -206,7 +339,7 @@ jobs:
|
|||||||
github-repositories: 'microsoft/vscode,facebook/react'
|
github-repositories: 'microsoft/vscode,facebook/react'
|
||||||
powershell-modules: 'PowerShellGet'
|
powershell-modules: 'PowerShellGet'
|
||||||
postman-collections: '12345'
|
postman-collections: '12345'
|
||||||
go-modules: 'github.com/gin-gonic/gin'
|
# go-modules removed
|
||||||
json-output-path: 'stats.json'
|
json-output-path: 'stats.json'
|
||||||
|
|
||||||
- name: Use Statistics Data
|
- name: Use Statistics Data
|
||||||
@@ -222,7 +355,7 @@ jobs:
|
|||||||
To enable automatic README updates, add these markers to your README.md:
|
To enable automatic README updates, add these markers to your README.md:
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
<!-- USAGE_STATS_START -->
|
<!-- METRICS_START -->
|
||||||
## 📊 Usage Statistics
|
## 📊 Usage Statistics
|
||||||
|
|
||||||
Last updated: 2025-07-29T18:53:52.619Z
|
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
|
3. **axios** (npm) - 18,397 downloads
|
||||||
4. **lodash** (npm) - 15,914 downloads
|
4. **lodash** (npm) - 15,914 downloads
|
||||||
5. **github.com/go-chi/chi** (go) - 33 downloads
|
5. **github.com/go-chi/chi** (go) - 33 downloads
|
||||||
<!-- USAGE_STATS_END -->
|
<!-- METRICS_END -->
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🔧 Development
|
## 🔧 Development
|
||||||
@@ -346,7 +479,7 @@ git push origin v1.1.0
|
|||||||
- **Homebrew**: Formula installation statistics
|
- **Homebrew**: Formula installation statistics
|
||||||
- **PowerShell**: Module download statistics
|
- **PowerShell**: Module download statistics
|
||||||
- **Postman**: Collection fork/download statistics
|
- **Postman**: Collection fork/download statistics
|
||||||
- **Go**: Module proxy statistics
|
<!-- Go platform removed -->
|
||||||
|
|
||||||
## 🤝 Contributing
|
## 🤝 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'
|
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'
|
author: 'LukeHagar'
|
||||||
|
|
||||||
inputs:
|
inputs:
|
||||||
@@ -21,45 +21,23 @@ inputs:
|
|||||||
required: false
|
required: false
|
||||||
default: ''
|
default: ''
|
||||||
|
|
||||||
# Homebrew Configuration
|
|
||||||
homebrew-formulas:
|
|
||||||
description: 'Comma-separated list of Homebrew formulas to track'
|
|
||||||
required: false
|
|
||||||
default: ''
|
|
||||||
|
|
||||||
# PowerShell Configuration
|
# PowerShell Configuration
|
||||||
powershell-modules:
|
powershell-modules:
|
||||||
description: 'Comma-separated list of PowerShell modules to track'
|
description: 'Comma-separated list of PowerShell modules to track'
|
||||||
required: false
|
required: false
|
||||||
default: ''
|
default: ''
|
||||||
|
|
||||||
# Postman Configuration
|
# Go tracking removed
|
||||||
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: ''
|
|
||||||
|
|
||||||
# Output paths
|
# Output paths
|
||||||
json-output-path:
|
json-output-path:
|
||||||
description: 'Path for JSON output file'
|
description: 'Path for JSON output file'
|
||||||
required: false
|
required: false
|
||||||
default: 'stats.json'
|
default: 'stats.json'
|
||||||
|
readme-path:
|
||||||
csv-output-path:
|
description: 'Path to README file to update'
|
||||||
description: 'Path for CSV output file'
|
|
||||||
required: false
|
required: false
|
||||||
default: ''
|
default: 'README.md'
|
||||||
|
|
||||||
report-output-path:
|
|
||||||
description: 'Path for human-readable report file'
|
|
||||||
required: false
|
|
||||||
default: ''
|
|
||||||
|
|
||||||
# README update
|
# README update
|
||||||
update-readme:
|
update-readme:
|
||||||
@@ -67,50 +45,22 @@ inputs:
|
|||||||
required: false
|
required: false
|
||||||
default: 'true'
|
default: 'true'
|
||||||
|
|
||||||
readme-path:
|
|
||||||
description: 'Path to README file to update'
|
|
||||||
required: false
|
|
||||||
default: 'README.md'
|
|
||||||
|
|
||||||
# GitHub integration
|
# GitHub integration
|
||||||
github-token:
|
github-token:
|
||||||
description: 'GitHub token for API access and commits'
|
description: 'GitHub token for API access and commits'
|
||||||
required: false
|
required: false
|
||||||
default: '${{ github.token }}'
|
default: '${{ github.token }}'
|
||||||
|
|
||||||
# Postman integration
|
|
||||||
postman-api-key:
|
|
||||||
description: 'Postman API key for collection statistics'
|
|
||||||
required: false
|
|
||||||
|
|
||||||
# Commit settings
|
# Commit settings
|
||||||
commit-message:
|
commit-message:
|
||||||
description: 'Commit message for changes'
|
description: 'Commit message for changes'
|
||||||
required: false
|
required: false
|
||||||
default: 'chore: update usage statistics [skip ci]'
|
default: 'chore: update usage statistics'
|
||||||
|
|
||||||
# Preview mode
|
|
||||||
preview-mode:
|
|
||||||
description: 'Run in preview mode with mock data (no external API calls)'
|
|
||||||
required: false
|
|
||||||
default: 'false'
|
|
||||||
|
|
||||||
outputs:
|
outputs:
|
||||||
json-output:
|
json-output:
|
||||||
description: 'Path to the generated JSON file'
|
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:
|
platforms-tracked:
|
||||||
description: 'Comma-separated list of platforms tracked'
|
description: 'Comma-separated list of platforms tracked'
|
||||||
|
|
||||||
|
|||||||
453
bun.lock
@@ -5,11 +5,20 @@
|
|||||||
"name": "usage-statistics",
|
"name": "usage-statistics",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "1.11.1",
|
"@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-retry": "^7.0.0",
|
||||||
"@octokit/plugin-throttling": "^7.0.0",
|
"@octokit/plugin-throttling": "^7.0.0",
|
||||||
"@octokit/rest": "22.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": {
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest",
|
||||||
"@types/node": "^20.0.0",
|
"@types/node": "^20.0.0",
|
||||||
"bun-types": "1.2.19",
|
"bun-types": "1.2.19",
|
||||||
"typescript": "^5.0.0",
|
"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/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/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=="],
|
"@actions/io": ["@actions/io@1.1.3", "", {}, "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="],
|
||||||
|
|
||||||
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
|
"@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-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-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/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/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/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=="],
|
"@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=="],
|
"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=="],
|
"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=="],
|
"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-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=="],
|
"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=="],
|
"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=="],
|
"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/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/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]
|
[install]
|
||||||
# Use exact versions for reproducible builds
|
# Use exact versions for reproducible builds
|
||||||
exact = true
|
exact = true
|
||||||
|
|
||||||
[install.scopes]
|
|
||||||
# Configure any scoped packages if needed
|
|
||||||
|
|
||||||
[test]
|
|
||||||
# Test configuration
|
|
||||||
preload = ["./src/test-setup.ts"]
|
|
||||||
|
|
||||||
[run]
|
[run]
|
||||||
# Runtime configuration
|
# Runtime configuration
|
||||||
bun = true
|
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",
|
"homepage": "https://github.com/LukeHagar/usage-statistics#readme",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "bun run src/index.ts",
|
"dev": "bun --watch --env-file=.dev.env --env-file=.env run src/action.ts",
|
||||||
"preview": "bun run src/index.ts --preview",
|
"test:pypi": "bun run test-pypi-bigquery.ts",
|
||||||
"dev": "bun --watch src/index.ts",
|
"test:pypi-detailed": "bun run test-pypi-detailed.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"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest",
|
||||||
"@types/node": "^20.0.0",
|
"@types/node": "^20.0.0",
|
||||||
"bun-types": "1.2.19",
|
"bun-types": "1.2.19",
|
||||||
"typescript": "^5.0.0"
|
"typescript": "^5.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "1.11.1",
|
"@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-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": {
|
"engines": {
|
||||||
"bun": ">=1.0.0"
|
"bun": ">=1.0.0"
|
||||||
@@ -47,5 +52,6 @@
|
|||||||
"usage"
|
"usage"
|
||||||
],
|
],
|
||||||
"author": "LukeHagar",
|
"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 core from '@actions/core';
|
import { collectNpmBatch } from './collectors/npm'
|
||||||
import * as fs from 'fs/promises';
|
import { collectGithubBatch } from './collectors/github'
|
||||||
import * as path from 'path';
|
import { collectPowerShellBatch } from './collectors/powershell'
|
||||||
import { UsageStatisticsManager } from './index';
|
import { collectPypiBatch } from './collectors/pypi'
|
||||||
import type { TrackingConfig } from './types';
|
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 {
|
try {
|
||||||
// Get inputs
|
await writeFile(jsonOutputPath, JSON.stringify(metrics, null, 2), 'utf8')
|
||||||
const npmPackages = core.getInput('npm-packages');
|
core.setOutput('json-output', jsonOutputPath)
|
||||||
const githubRepositories = core.getInput('github-repositories');
|
console.log(`Wrote metrics JSON to ${jsonOutputPath}`)
|
||||||
const pypiPackages = core.getInput('pypi-packages');
|
} catch (writeErr) {
|
||||||
const homebrewFormulas = core.getInput('homebrew-formulas');
|
console.warn(`Failed to write metrics JSON to ${jsonOutputPath}:`, writeErr)
|
||||||
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.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (postmanApiKey) {
|
core.setOutput('commit-message', commitMessage)
|
||||||
process.env.POSTMAN_API_KEY = postmanApiKey;
|
} catch (error) {
|
||||||
}
|
// Fail the workflow run if an error occurs
|
||||||
|
if (error instanceof Error) core.setFailed(error.message)
|
||||||
// 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}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||