Shows today's GitHub Actions spend beside the spinner, warns under the prompt when the month is on pace to exceed the plan's included usage, and adds /spend…

The complete skill library that Cure Consulting Group uses to build apps, platforms, and products. These skills encode our standards, frameworks, and processes — so every project ships with the same level of rigor.
Now available as a Claude Code Plugin — install once, get auto-updates across all projects.
ProductEngineeringSkills/
├── .claude-plugin/ # Plugin manifest
│ └── plugin.json
├── skills/{domain}/ # 80 skills, organized by domain (engineering/platform/product/business/marketing/security/legal)
│ ├── sdlc/
│ ├── android-feature-scaffold/
│ ├── incident-response/ # NEW
│ ├── accessibility-audit/ # NEW
│ ├── performance-review/ # NEW
│ ├── database-architect/ # NEW
│ ├── infrastructure-scaffold/ # NEW
│ ├── project-bootstrap/
│ ├── e2e-testing/
│ ├── test-accounts/
│ ├── uat/
│ ├── compliance-architect/
│ ├── data-migration/
│ ├── feature-flags/
│ ├── release-management/
│ ├── observability/
│ ├── client-handoff/
│ ├── llmops/
│ ├── disaster-recovery/
│ ├── dora-metrics/
│ ├── design-system/
│ ├── client-communication/
│ ├── i18n/
│ ├── notification-architect/
│ ├── offline-first/
│ ├── chaos-engineering/
│ ├── edge-computing/
│ ├── finops/
│ ├── micro-frontends/
│ ├── growth-engineering/
│ ├── green-software/
│ ├── proposal-generator/
│ ├── api-gateway/
│ ├── ... (75 total — see docs/OVERVIEW.md for full inventory)
│ └── legal-doc-scaffold/
├── agents/ # 35 custom subagent definitions
├── personas/ # 4 cross-domain engagement archetypes
│ ├── code-reviewer.md # Security + quality review agent
│ ├── project-bootstrapper.md # New project setup agent
│ ├── test-runner.md # Execute test suites, report coverage
│ ├── pr-reviewer.md # Automated PR diff review
│ ├── refactor-assistant.md # Safe refactoring with test validation
│ ├── ci-debugger.md # Diagnose failed CI/CD runs
│ ├── release-coordinator.md # Version bump, changelog, deploy validation
│ ├── doc-generator.md # API docs, ADRs, changelogs from code
│ ├── codebase-explainer.md # Onboarding — explain architecture, trace flows
│ ├── migration-validator.md # Database migration safety checks
│ ├── deployment-validator.md # Pre-deployment checklist validation
│ ├── dependency-auditor.md # Vulnerability and outdated package audit
│ ├── api-validator.md # OpenAPI spec and contract validation
│ ├── product-analyst.md # Feature adoption, analytics instrumentation
│ ├── ux-researcher.md # Usability analysis, friction mapping
│ ├── roadmap-strategist.md # RICE scoring, dependency mapping, roadmaps
│ ├── competitive-intel.md # Feature matrices, positioning, moat analysis
│ ├── content-strategist.md # Editorial calendars, SEO, content briefs
│ ├── campaign-analyst.md # Attribution, funnel analysis, channel ROI
│ ├── brand-guardian.md # Voice/tone, visual identity, microcopy audit
│ ├── growth-analyst.md # Activation, retention, viral mechanics
│ ├── financial-analyst.md # Revenue forecasts, unit economics, scenarios
│ ├── market-intelligence.md # TAM/SAM/SOM, trends, market timing
│ ├── investor-relations.md # Board updates, KPIs, fundraising narratives
│ ├── contract-reviewer.md # SOW/contract risk, terms, IP review
│ ├── data-analyst.md # Schema exploration, queries, data quality
│ ├── metrics-dashboard.md # KPI definitions, SLOs, dashboard wireframes
│ ├── ab-test-analyst.md # Experiment design, statistical analysis
│ ├── qa-engineer.md # Test planning, edge cases, regression, quality gates
│ ├── accessibility-checker.md # WCAG 2.2 automated compliance
│ └── firebase-security-auditor.md # Firestore rules and Functions audit
├── hooks/ # Multi-layer automated enforcement
│ └── hooks.json # Command + Prompt hooks (9 event types)
├── rules/ # 11 path-specific coding standards
│ ├── android.md # Loads for *.kt files
│ ├── ios.md # Loads for *.swift files
│ ├── web.md # Loads for *.ts/*.tsx files
│ ├── firebase.md # Loads for functions/**
│ ├── python.md # Loads for *.py files
│ ├── go.md # Loads for *.go files
│ ├── rust.md # Loads for *.rs files
│ ├── sql.md # Loads for *.sql, migrations/**
│ ├── docker.md # Loads for Dockerfile, *.dockerfile
│ ├── terraform.md # Loads for *.tf, *.tfvars
│ └── cicd.md # Loads for .github/workflows/**
├── output-styles/ # 9 custom output formatting styles
│ ├── prd/ # Product docs (PRDs, GTM, research)
│ ├── code-generation/ # Code scaffolds and implementations
│ ├── financial-analysis/ # Cost models, SaaS metrics
│ ├── audit-report/ # Audits, reviews, compliance
│ ├── api-specification/ # OpenAPI specs, endpoint docs
│ ├── architecture-decision/ # ADRs, RFCs, trade-off matrices
│ ├── runbook/ # Incident runbooks, DR procedures
│ ├── test-plan/ # Test plans, coverage reports
│ └── monitoring-alert/ # Alert definitions, thresholds
├── .mcp.json # MCP server configs (GitHub, Sentry, Firestore, PostgreSQL)
├── .lsp.json # LSP server configs (TypeScript, Python/Pyright)
├── marketplace.json # Plugin marketplace manifest
├── settings.json # Default permission rules
├── claude-commands/ # Legacy format (backwards compat, 64 files)
├── gemini skills/ # Google Gemini skills (.skill ZIP)
├── CLAUDE.md # Project instructions (Claude)
├── GEMINI.md # Project instructions (Gemini CLI)
├── AGENT-GUIDE.md # How to structure prompts for agents & skills
├── setup.sh # Setup script for Antigravity & other projects
└── README.md
Install the plugin as an npm package from GitHub Packages. This is the easiest way to keep all your projects up to date.
1. Authenticate with GitHub Packages (one-time setup):
# Create a Personal Access Token (PAT) with read:packages scope at
# https://github.com/settings/tokens, then:
npm login --scope=@cure-consulting-group --registry=https://npm.pkg.github.com
Or add to your project's .npmrc:
@cure-consulting-group:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
2. Install in your project:
npm install @cure-consulting-group/product-engineering-skills
The postinstall script automatically:
~/.claude/plugins/ProductEngineeringSkills~/.claude/settings.jsonAll 80 skills, 39 agents, 4 personas, hooks, rules, and output styles are immediately available.
3. Enable auto-updates with Dependabot (recommended):
Add .github/dependabot.yml to your project (or run setup.sh which does this automatically):
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"
allow:
- dependency-name: "@cure-consulting-group/product-engineering-skills"
labels:
- "dependencies"
- "skills-update"
commit-message:
prefix: "chore"
include: "scope"
Dependabot will open a PR in your project whenever a new version is published. Merge it and every agent on that project gets the updated skills.
4. Manual update:
npm update @cure-consulting-group/product-engineering-skills
# Load the plugin during a session
claude --plugin-dir /path/to/ProductEngineeringSkills
# Or for development/testing
claude --plugin-dir ./ProductEngineeringSkills
Once loaded, all skills are available as namespaced commands:
/cure-product-engineering:sdlc
/cure-product-engineering:feature-audit
/cure-product-engineering:android-feature-scaffold
/cure-product-engineering:incident-response
/cure-product-engineering:accessibility-audit
Hooks, agents, rules, output styles, and MCP servers are all included automatically.
The fastest way to onboard any project:
# From the target project directory
/path/to/ProductEngineeringSkills/setup.sh
# Or specify the project path
/path/to/ProductEngineeringSkills/setup.sh /path/to/antigravity-app
# Install globally for ALL projects
/path/to/ProductEngineeringSkills/setup.sh --global
# Legacy mode (just copy skills, no hooks/agents)
/path/to/ProductEngineeringSkills/setup.sh --legacy
The setup script will:
~/.claude/plugins/.claude/settings.local.json to .gitignore# Add the Cure Consulting marketplace
claude marketplace add https://github.com/Cure-Consulting-Group/ProductEngineeringSkills/marketplace.json
# Install the plugin
claude plugin install cure-product-engineering
Copy the claude-commands/ files into your project's .claude/commands/ directory:
cp claude-commands/*.md /path/to/your/project/.claude/commands/
Then use them as slash commands:
/sdlc — Generate SDLC artifacts
/android-feature-scaffold — Scaffold an Android feature module
/feature-audit — Audit a completed feature
Import the .skill files from gemini skills/ into your Gemini workspace. Each .skill file is a ZIP archive containing:
SKILL.md — The main skill definitionreferences/ — Supporting documents and templates| Skill | What It Does | Auto-Invoked? |
|---|---|---|
| product-manager | OKRs, roadmaps, RICE prioritization, feature briefs | Yes |
| product-design | Apple HIG, Material Design 3, design tokens, accessibility-first | Yes |
| market-research | TAM/SAM/SOM, competitive analysis, ICP definition (read-only) | Yes |
| go-to-market | GTM plans, launch strategy, channel selection, growth playbooks | Yes |
| product-marketing | Brand strategy, messaging frameworks, campaigns | Yes |
| customer-onboarding | Activation flows, empty states, email sequences, retention | Yes |
| seo-content-engine | Technical SEO, structured data, content strategy | Yes |
| Skill | What It Does | Auto-Invoked? |
|---|---|---|
| sdlc | PRDs, ADRs, RFCs, Epics, Stories, Task specs — full SDLC | Yes |
| android-feature-scaffold | Clean Architecture Android scaffolding (MVI, Compose, Hilt) | Yes |
| ios-architect | Swift/SwiftUI Clean Architecture, MVVM, structured concurrency | Yes |
| nextjs-feature-scaffold | App Router, Server/Client components, Tailwind patterns | Yes |
| firebase-architect | Firestore schema, security rules, Cloud Functions | Yes |
| api-architect | REST/GraphQL design, versioning, auth, rate limiting | Yes |
| api-gateway | API gateway and BFF layers, rate limiting, GraphQL federation | Yes |
| stripe-integration | Stripe payments + subscriptions via Firebase Functions | Yes |
| ai-feature-builder | LLM integration, RAG pipelines, prompt engineering | Yes |
| llmops | LLM operationalization — prompt versioning, eval pipelines, cost optimization, guardrails | Yes |
| database-architect | Schema design, migrations, indexing for Firestore/PostgreSQL/SQLite | Yes |
| data-migration | ETL pipelines, zero-downtime cutover, validation, rollback strategies | Yes |
| infrastructure-scaffold | Cloud infra configs for Firebase, GCP, Vercel, Docker | Yes |
| edge-computing | Edge functions, CDN strategies, cache invalidation, edge middleware | Yes |
| micro-frontends | Module federation, monorepo management, independent deployments | Yes |
| offline-first | Offline-first architecture, sync strategies, conflict resolution, optimistic UI | Yes |
| i18n | Internationalization — string extraction, RTL, locale-aware formatting, translation workflows | Yes |
| notification-architect | Push (FCM/APNs), in-app messaging, email, preference management | Yes |
| Skill | What It Does | Auto-Invoked? |
|---|---|---|
| feature-audit | 5-phase post-completion audit with scored gap report | Yes (read-only, forked) |
| testing-strategy | Testing pyramid, platform standards, coverage rules | Yes |
| e2e-testing | E2E test suites with page objects, visual regression, CI integration | Yes |
| test-accounts | Test user personas, seed data scripts, environment credentials | Yes |
| uat | UAT plans, acceptance criteria checklists, go/no-go release gates | Yes |
| security-review | OWASP checklist, auth/data/API/mobile/web security | Yes (read-only, forked) |
| compliance-architect | HIPAA, COPPA, GDPR, PCI compliance frameworks, consent flows, audit trails | Yes |
| accessibility-audit | WCAG 2.2 compliance, screen readers, inclusive design | Yes (read-only, forked) |
| performance-review | Performance budgets, load testing, optimization strategies | Yes |
| chaos-engineering | Resilience testing, failure injection, graceful degradation, game days | Yes |
| green-software | Sustainable software practices, carbon-aware computing, energy efficiency | Yes |
| Skill | What It Does | Auto-Invoked? |
|---|---|---|
| project-bootstrap | Bootstrap repo with CLAUDE.md + STATE.md via codebase inspection and developer interview | Yes |
| project-manager | Sprint planning, RACI, risk registers, retrospectives | Yes |
| ci-cd-pipeline | GitHub Actions, build/test/deploy, environments, secrets | Yes |
| release-management | App store submissions, staged rollouts, versioning, ASO, changelogs | Yes |
| feature-flags | Progressive rollouts, A/B testing, kill switches, experimentation frameworks | Yes |
| observability | Structured logging, distributed tracing, alerting, SLO/SLI, dashboards | Yes |
| dora-metrics | DORA and SPACE metrics — deployment frequency, lead time, MTTR, developer experience | Yes |
| analytics-implementation | Event taxonomy, tracking plans, funnels, dashboards | Yes |
| incident-response | Runbooks, severity classification, post-mortems, escalation | Yes |
| disaster-recovery | DR and business continuity — RTO/RPO, backup strategies, failover, DR testing | Yes |
| growth-engineering | Activation funnels, referral programs, lifecycle automation, PLG patterns | Yes |
| design-system | Design tokens, component libraries, Storybook/Catalog, cross-platform consistency | Yes |
| Skill | What It Does | Auto-Invoked? |
|---|---|---|
| engineering-cost-model | Project estimates, infrastructure costs, build vs buy | Yes (read-only) |
| saas-financial-model | Unit economics, MRR/ARR, pricing tiers, break-even | Yes (read-only) |
| finops | Cloud cost optimization, budget alerts, resource right-sizing, FinOps practices | Yes |
| investor-reporting | Investor updates, board decks, portfolio financials, cap table, runway modeling | Yes |
| fundraising-materials | Pitch decks, data rooms, investor updates, cap table scenarios, fundraising pipeline | Yes |
| burn-rate-tracker | Burn rates, runway scenarios, break-even analysis, cash flow projections | Yes |
| legal-doc-scaffold | ToS, Privacy Policy, SOW, NDA scaffolds | No (manual only) |
| Skill | What It Does | Auto-Invoked? |
|---|---|---|
| portfolio-registry | Product portfolio registry — single source of truth for all products, stacks, teams, stages | Yes |
| technology-radar | ThoughtWorks-style technology radar — Adopt/Trial/Assess/Hold across the portfolio | Yes |
| Skill | What It Does | Auto-Invoked? |
|---|---|---|
| client-handoff | Handoff packages, runbooks, credential transfers, maintenance SLAs, knowledge transfer | Yes |
| client-communication | Sprint demo scripts, stakeholder updates, risk escalation, executive summaries | Yes |
| proposal-generator | Consulting proposals, SOWs, milestone pricing, engagement structure | No (manual only) |
| Skill | What It Does | Auto-Invoked? |
|---|---|---|
| android-design-expert | Material Design 3 — dynamic color, component tokens, adaptive layouts, motion, Compose patterns | Yes |
| ios-design-expert | Apple HIG — SF Symbols, Dynamic Type, navigation patterns, SwiftUI components | Yes |
| web-design-expert | Responsive design, CSS architecture, design tokens, container queries, accessibility-first, Tailwind | Yes |
| stitch-design | AI-native UI design via Stitch MCP — vibe design, mockups, screen generation, design tokens, component export | Yes |
The library ships a standard maintenance loop (self-provisioned to .claude/loop.md on first session start — run bare /loop to use it), Recurring Mode sections in the goal-shaped skills (finops, burn-rate-tracker, investor-reporting, security-review, and others), and copy-paste cloud-routine recipes in docs/AUTOMATION.md. Library upkeep cadence: docs/MAINTENANCE.md. Mechanism selection and unattended-run guardrails: /cure-product-engineering:engagement-automation.
The plugin ships command and prompt hooks across 9 event types: SessionStart, PreCompact, PostCompact, ConfigChange, PostToolUseFailure, UserPromptSubmit, PreToolUse, Stop, SubagentStop. Highlights: a Stop-hook quality gate (blocks "done" without verification), a PreToolUse static security guard on skill/agent/persona files, and a ConfigChange audit trigger when skill files change mid-session.
New in v4.0: Hooks now suggest and auto-trigger agents based on context. Every code edit, test run, deployment, and PR action recommends the most relevant agent(s).
| Hook | Event | What It Does | Agent Integration |
|---|---|---|---|
| Welcome | SessionStart | Confirms plugin loaded with counts; full inventory stays in docs/OVERVIEW.md | Points to inventory |
| Git status | SessionStart | Reports current branch, uncommitted changes, last commit | — |
| Dependency check | SessionStart | Detects outdated packages | Suggests dependency-auditor |
| Code edit advisor | PostToolUse (Edit/Write) | Context-aware suggestions based on file type (.kt, .swift, .ts, .sql, .tf, etc.) | Suggests code-reviewer, test-runner, brand-guardian, migration-validator |
| Command advisor | PostToolUse (Bash) | Post-action guidance for tests, installs, deploys, PRs, releases | Suggests ci-debugger, dependency-auditor, pr-reviewer, release-coordinator |
| Failure recovery | PostToolUseFailure | Diagnoses failure type and suggests fix approach | Auto-suggests ci-debugger, deployment-validator, dependency-auditor |
| Destructive prompt guard | UserPromptSubmit | Detects destructive operations in prompts | Blocks and confirms |
| Protected files | PreToolUse (Edit/Write) | Blocks edits to .env, lock files, credentials, tfstate | — |
| Dangerous commands | PreToolUse (Bash) | Blocks force push, destructive rm, DROP TABLE, prod deploys | — |
| Context re-injection | PreCompact | Re-injects all 80 skills, 39 agents, 4 personas, and Cure standards | Full inventory preserved |
| Post-compact restore | PostCompact | Confirms context restored with agent availability | — |
| Subagent start banner | SubagentStart | Announces agent with role, standards, and companion agents | Lists companion agents |
| Subagent completion | SubagentStop | Suggests follow-up agents (test-runner, code-reviewer, pr-reviewer) | Agent chaining |
| Task quality check | TaskCompleted | Validates tests, security, docs, brand consistency | Suggests test-runner, code-reviewer, doc-generator, brand-guardian |
| Hook | Event | What It Does | Agent Integration |
|---|---|---|---|
| Code quality gate | PreToolUse (Edit/Write) | Haiku validates: no secrets, no debug logs, no disabled tests, no any types | — |
| Deployment safety | PreToolUse (Bash) | Haiku validates: blocks production deployments outside CI/CD | — |
| Intent classifier | UserPromptSubmit | Haiku classifies prompt intent and suggests the most relevant agent(s) from all 30 | Maps prompts → agents with confidence scores |
| Hook | Event | What It Does | Agent Integration |
|---|---|---|---|
| Completion validator | Stop | Validates: tests for new code, security review for sensitive changes, rollback for migrations, docs for features, brand consistency for UI, analytics for events, API contracts | Suggests specific agents for each gap found |
Pre-configured MCP servers in .mcp.json:
| Server | Type | What It Does |
|---|---|---|
| GitHub | HTTP | PR management, issue tracking, code search |
| Sentry | HTTP | Error monitoring, issue tracking, release health |
| Firestore | stdio | Direct database queries, schema inspection |
| PostgreSQL | stdio | Database queries, schema inspection, migrations |
Pre-configured LSP servers in .lsp.json:
| Server | Language | What It Provides |
|---|---|---|
| TypeScript | .ts, .tsx, .js | Type checking, auto-imports, refactoring, go-to-definition |
| Python (Pyright) | *.py | Static type analysis, import resolution, error diagnostics |
Custom output formatting for different artifact types:
| Style | Used By | Key Rules |
|---|---|---|
| prd | Product skills (PRDs, GTM, research) | Numbered sections, decision matrices, executive summaries |
| code-generation | Engineering skills (scaffolds) | File tree first, dependency order, complete runnable code |
| financial-analysis | Business skills (costs, models) | ASCII tables, explicit assumptions, sensitivity analysis |
| audit-report | Quality skills (audits, reviews) | Severity scoring, checklists, remediation with effort estimates |
| api-specification | API design skills | OpenAPI 3.0 blocks, endpoint tables, request/response examples |
| architecture-decision | ADR and RFC skills | Context/decision/consequences format, trade-off matrices |
| runbook | Incident response, disaster recovery | Numbered steps, command blocks, decision trees, escalation paths |
| test-plan | Testing strategy, QA skills | Coverage tables, test case templates, pass/fail criteria |
| monitoring-alert | Observability, incident response | Alert definition tables, threshold rationale, runbook links |
| Agent | Purpose | Tools | Auto-Triggered By |
|---|---|---|---|
| code-reviewer | Security + quality review against Cure standards | Read-only | Stop hook, SubagentStop |
| project-bootstrapper | Set up new projects with correct architecture |
hooks/register.ts 83 lines1import type { On } from 'claude-code'
2
3import { bandText, parseUsage, reportText, summarize, warningText, type Spend } from './spend'
4
5/**
6 * cure-spend-band: today's GitHub Actions spend beside the spinner, a warning
7 * line under the prompt when the month is on pace to exceed the plan's
8 * included usage, and /spend for the detail.
9 *
10 * Reads billing through the `gh` CLI the person is already logged into
11 * (`gh api organizations/{org}/settings/billing/usage`), every 15 minutes and
12 * on /spend. No token is stored by the mod.
13 *
14 * Settings, as environment variables:
15 * CURE_SPEND_ORG the organization (default Cure-Consulting-Group)
16 * CURE_SPEND_ALLOWANCE monthly included Actions usage in USD (default 300)
17 *
18 * Fails quiet: if `gh` is missing, logged out or the API refuses, the band
19 * shows nothing and /spend says why.
20 */
21
22const REFRESH_MS = 15 * 60 * 1000
23const DEFAULT_ORG = 'Cure-Consulting-Group'
24const DEFAULT_ALLOWANCE = 300
25
26let latest: { spend: Spend; fetchedAt: string; org: string } | undefined
27let lastError: string | undefined
28let timer: { cancel: () => void } | undefined
29
30export function register(on: On) {
31 on('session.start', async ($, e, next) => {
32 await $.command.register({ name: 'spend', description: "Show this month's GitHub Actions spend against the plan's included usage" })
33 timer?.cancel()
34 timer = $.clock.every(REFRESH_MS, () => void refresh($))
35 void refresh($)
36 return next(e)
37 })
38
39 on('session.end', async ($, e, next) => {
40 timer?.cancel()
41 timer = undefined
42 return next(e)
43 })
44
45 on('command.run', { command: 'spend' }, async $ => {
46 await refresh($)
47 if (!latest) return { text: `cure-spend-band: no billing data (${lastError ?? 'not fetched yet'})` }
48 const report = reportText(latest.spend, latest.fetchedAt, latest.org)
49 return {
50 text: lastError ? `${report}\n Latest refresh FAILED: ${lastError} (numbers above are from ${latest.fetchedAt})` : report,
51 }
52 })
53
54 on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
55 if (!latest) return next(e)
56 const suffix = typeof e.props?.suffix === 'string' ? e.props.suffix : ''
57 return next({ ...e, props: { ...e.props, suffix: `${suffix} · ${bandText(latest.spend)}` } })
58 })
59}
60
61async function settings($: any): Promise<{ org: string; allowance: number }> {
62 const org = (await $.env.get('CURE_SPEND_ORG')) || DEFAULT_ORG
63 const raw = Number(await $.env.get('CURE_SPEND_ALLOWANCE'))
64 return { org, allowance: Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_ALLOWANCE }
65}
66
67async function refresh($: any): Promise<void> {
68 try {
69 const { org, allowance } = await settings($)
70 const now = new Date(await $.clock.now())
71 const url = `organizations/${org}/settings/billing/usage?year=${now.getUTCFullYear()}&month=${now.getUTCMonth() + 1}`
72 const res = await $.process.run(['gh', 'api', url], { timeoutMs: 60000 })
73 if (res.exitCode !== 0) throw new Error((res.stderr || 'gh api failed').trim().split('\n')[0])
74 const spend = summarize(parseUsage(res.stdout), now, allowance)
75 latest = { spend, fetchedAt: now.toISOString().slice(11, 16) + ' UTC', org }
76 lastError = undefined
77 $.ui.status(warningText(spend))
78 } catch (err) {
79 lastError = err instanceof Error ? err.message : String(err)
80 }
81 $.ui.invalidate('ui.render')
82}
83hooks/spend.ts 141 lines1/**
2 * GitHub billing arithmetic for the spend band. Pure: the usage items in, the
3 * numbers the band shows out.
4 *
5 * The source is `GET /organizations/{org}/settings/billing/usage?year&month`
6 * (enhanced billing platform): one item per day × product × SKU × repo, with
7 * `grossAmount` (list price), `discountAmount` (what the plan's included
8 * usage absorbed) and `netAmount` (what is billed). Dates are UTC days.
9 */
10
11export type UsageItem = {
12 date: string
13 product: string
14 sku: string
15 quantity: number
16 unitType: string
17 grossAmount: number
18 discountAmount: number
19 netAmount: number
20 repositoryName?: string
21}
22
23export type Spend = {
24 /** UTC day the numbers are for, YYYY-MM-DD. */
25 today: string
26 /** Gross Actions spend today (list price, before included usage). */
27 actionsToday: number
28 /** Gross Actions spend month to date. */
29 actionsMonth: number
30 /** What is actually billed month to date, every product (seats included). */
31 billedMonth: number
32 /**
33 * Gross Actions spend projected to month end: month to date, plus the rest
34 * of the month at the average of the last (up to) three complete days. Falls
35 * back to month-to-date pace before the month has a complete day.
36 */
37 projectedActions: number
38 /** The daily pace the projection uses for the rest of the month. */
39 recentDailyPace: number
40 /** How many complete days that pace averages (0 = month-to-date pace). */
41 paceDays: number
42 /** The monthly included Actions allowance, in dollars. */
43 allowance: number
44 /** The allowance spread evenly over the month's days. */
45 dailyAllowance: number
46 /** Projected Actions gross above the allowance (0 when inside it). */
47 projectedOverage: number
48 /** The repo with the most gross Actions spend today, and its amount. */
49 topRepoToday?: { repo: string; amount: number }
50}
51
52const round2 = (n: number) => Math.round(n * 100) / 100
53
54/** Days in the UTC month of `now`. */
55export function daysInMonth(now: Date): number {
56 return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 0)).getUTCDate()
57}
58
59/** The band's numbers from one month's usage items, as of `now`. */
60export function summarize(items: readonly UsageItem[], now: Date, allowance: number): Spend {
61 const today = now.toISOString().slice(0, 10)
62 let actionsToday = 0
63 let actionsMonth = 0
64 let billedMonth = 0
65 const byRepoToday = new Map<string, number>()
66 const byDay = new Map<string, number>()
67 for (const i of items) {
68 billedMonth += i.netAmount || 0
69 if (i.product !== 'actions') continue
70 actionsMonth += i.grossAmount || 0
71 const day = i.date.slice(0, 10)
72 byDay.set(day, (byDay.get(day) ?? 0) + (i.grossAmount || 0))
73 if (day === today) {
74 actionsToday += i.grossAmount || 0
75 const repo = i.repositoryName || '(org)'
76 byRepoToday.set(repo, (byRepoToday.get(repo) ?? 0) + (i.grossAmount || 0))
77 }
78 }
79 const days = daysInMonth(now)
80 // Elapsed days include today's fraction, so the projection does not jump at midnight.
81 const elapsed = now.getUTCDate() - 1 + (now.getUTCHours() * 60 + now.getUTCMinutes()) / 1440
82 // The last three complete days of this month (days with no usage count as $0).
83 const recent: number[] = []
84 for (let d = now.getUTCDate() - 1; d >= 1 && recent.length < 3; d--) {
85 const key = `${today.slice(0, 8)}${String(d).padStart(2, '0')}`
86 recent.push(byDay.get(key) ?? 0)
87 }
88 const remaining = Math.max(0, days - elapsed)
89 const mtdPace = elapsed > 0.25 ? actionsMonth / elapsed : 0
90 const recentDailyPace = recent.length > 0 ? recent.reduce((a, b) => a + b, 0) / recent.length : mtdPace
91 const projectedActions = recent.length > 0 || elapsed > 0.25 ? actionsMonth + recentDailyPace * remaining : actionsMonth
92 const top = [...byRepoToday.entries()].sort((a, b) => b[1] - a[1])[0]
93 return {
94 today,
95 actionsToday: round2(actionsToday),
96 actionsMonth: round2(actionsMonth),
97 billedMonth: round2(billedMonth),
98 projectedActions: round2(projectedActions),
99 recentDailyPace: round2(recentDailyPace),
100 paceDays: recent.length,
101 allowance,
102 dailyAllowance: round2(allowance / days),
103 projectedOverage: round2(Math.max(0, projectedActions - allowance)),
104 topRepoToday: top && top[1] > 0 ? { repo: top[0], amount: round2(top[1]) } : undefined,
105 }
106}
107
108const usd = (n: number) => `$${n.toFixed(2)}`
109
110/** The short suffix beside the spinner. */
111export function bandText(s: Spend): string {
112 const over = s.projectedOverage > 0 ? ' ▲' : ''
113 return `Actions ${usd(s.actionsToday)} today · ${usd(s.dailyAllowance)}/day covered${over}`
114}
115
116/** The persistent warning line, or undefined when the month is on pace. */
117export function warningText(s: Spend): string | undefined {
118 if (s.projectedOverage <= 0) return undefined
119 return `GitHub Actions headed for ${usd(s.projectedActions)} this month at the recent ${usd(s.recentDailyPace)}/day, ${usd(s.projectedOverage)} over the ${usd(s.allowance)} included. /spend for detail.`
120}
121
122/** The /spend report. */
123export function reportText(s: Spend, fetchedAt: string, org: string): string {
124 return [
125 `GitHub spend for ${org} (as of ${fetchedAt})`,
126 ` Actions today ${usd(s.actionsToday)} (plan covers ~${usd(s.dailyAllowance)}/day)`,
127 ` Actions this month ${usd(s.actionsMonth)} (plan covers ${usd(s.allowance)})`,
128 ` Projected month end ${usd(s.projectedActions)} ${s.projectedOverage > 0 ? `→ ${usd(s.projectedOverage)} over` : '→ inside the allowance'}`,
129 ` (month to date + ${s.paceDays > 0 ? `last ${s.paceDays} full day${s.paceDays > 1 ? 's' : ''}' average, ${usd(s.recentDailyPace)}/day` : `month-to-date pace, ${usd(s.recentDailyPace)}/day`}, for the rest of the month)`,
130 ` Billed so far ${usd(s.billedMonth)} (every product, seats included)`,
131 s.topRepoToday ? ` Top repo today ${s.topRepoToday.repo} ${usd(s.topRepoToday.amount)}` : ' Top repo today none',
132 ].join('\n')
133}
134
135/** Usage items from the API's JSON text; throws on anything else. */
136export function parseUsage(json: string): UsageItem[] {
137 const body = JSON.parse(json) as { usageItems?: unknown }
138 if (!Array.isArray(body.usageItems)) throw new Error('no usageItems in the billing response')
139 return body.usageItems as UsageItem[]
140}
141