cnpmcore
Copilot instructions738 starsChanged 6 days agoMITView on GitHub
cnpmcore is a TypeScript-based private NPM registry implementation built with Egg.js framework. It provides enterprise-grade package management with support for MySQL/PostgreSQL databases, Redis caching, and optional Elasticsearch. ALWAYS reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the information here. The project uses Vite+ (the vp CLI) as the unified toolchain. Lint/format config lives in vite.config.ts (lint and fmt blocks) — there are no standalone .oxlintrc.json/.oxfmtrc.json files (running…
- Reads credentials
- Installs packages
We split this copilot-instructions.md into the rules it gives an agent
24 rules. About 0 are common, 9 are distinctive, and 15 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈9
Distinctive · estimate
This author's own choices. Worth a look.
15
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 47
Use strict TypeScript with comprehensive type definitions
- Very specific: line 48
Avoid any types - use proper typing or unknown
- Very specific: line 50
Use ES modules (import/export) syntax throughout
- Very specific: line 54
Test files use .test.ts suffix
- Very specific: line 55
Use @eggjs/mock for mocking and testing
- Very specific: line 57
Use assert from node:assert/strict for assertions
- Distinctive: line 161
NO HTTP concerns, NO direct database access
- Very specific: line 175
Use EntityUtil.defaultData(data, 'idField') for ID/timestamp defaults
- Distinctive: line 256
When changing a Model, update all 3 locations:
- Distinctive: line 357
Test failures may occur in CI environment.
- Distinctive: line 361
Write tests for all new features: - No feature is complete without tests
- Very specific: line 363
Mock external dependencies: - Use mock() from @eggjs/mock
- Very specific: line 364
Use realistic test data: - Create through TestUtil helper methods
- Very specific: line 490
Use @LifecycleInit() decorator for adapter initialization hooks.
- Very specific: line 512
Add SQL migrations: sql/mysql/*.sql AND sql/postgresql/*.sql
- Distinctive: line 514
Run database migration scripts
- Distinctive: line 519
Keep entities pure (no infrastructure dependencies)
- Very specific: line 523
Follow naming: findX, saveX, removeX, listXs
- Very specific: line 529
Use @SingletonProto() for service lifecycle
- Very specific: line 534
Add HTTP method decorators: @HTTPMethod(), @HTTPBody(), etc.
- Very specific: line 537
Add tests: - Create test file: test/path/matching/source/YourFile.test.ts
- Very specific: line 540
Commit: - Use semantic commit messages (feat/fix/chore/docs/test)
- Distinctive: line 673
Always account for these timings when setting timeouts for automated processes.
- Distinctive: line 677
Use conventional commit format for all commits:
≈ “Common” is an estimate: at least 1 in 5 of the 3,669 Copilot instructions files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
cnpmcore - Private NPM Registry for Enterprise
cnpmcore is a TypeScript-based private NPM registry implementation built with Egg.js framework. It provides enterprise-grade package management with support for MySQL/PostgreSQL databases, Redis caching, and optional Elasticsearch.
ALWAYS reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the information here.
Code Style and Conventions
Linting and Formatting
The project uses Vite+ (the vp CLI) as the unified toolchain. Lint/format config lives in vite.config.ts (lint and fmt blocks) — there are no standalone .oxlintrc.json/.oxfmtrc.json files (running oxlint/oxfmt directly would NOT pick up project rules; always go through vp).
- Linter: Oxlint via
vp lint(fast Rust-based linter) with type-aware checking - Formatter: Oxfmt via
vp fmt(sole formatter, no Prettier) - Unified check:
vp checkruns format + lint + type-check in one pass (vp check --fixauto-fixes) — the preferred validation loop - Pre-commit hooks: Vite+ git hooks installed by
vp config(thepreparescript);.vite-hooks/pre-commitrunsvp staged, which appliesvp check --fixto staged files (replaces the old Husky + lint-staged setup)
Code Style Rules (configured in vite.config.ts):
// fmt block
{
"singleQuote": true, // Use single quotes
"printWidth": 120 // 120 character line width
// 2-space indentation and ES5 trailing commas are oxfmt defaults
}
// lint block (rules)
{
"max-params": ["error", 6], // Maximum 6 function parameters
"no-console": "warn", // Warn on console usage
"import/no-anonymous-default-export": "error"
}Linting Commands:
npm run lint # Check for linting errors (= vp lint)
npm run lint:fix # Auto-fix linting issues (= vp lint --fix)
npm run typecheck # TypeScript type checking without build (tsc --noEmit)
vp check # Format + lint + type-check in one pass (add --fix to auto-fix)TypeScript Conventions
- Use strict TypeScript with comprehensive type definitions
- Avoid
anytypes - use proper typing orunknown - Export types and interfaces for reusability
- Use ES modules (
import/export) syntax throughout
Testing Conventions
- Test files use
.test.tssuffix - Use
@eggjs/mockfor mocking and testing - Tests organized to mirror source structure in
test/directory - Use
assertfromnode:assert/strictfor assertions - Mock external dependencies using
mock()from@eggjs/mock
Test Infrastructure:
test/.setup.ts- Global beforeEach/afterEach hookstest/TestUtil.ts- Comprehensive test utilitiestest/fixtures/- Mock data and responses
Key TestUtil Methods:
TestUtil.createUser(options)- Create test user with auth tokensTestUtil.createPackage(options)- Create full package in systemTestUtil.getFullPackage(options)- Get mock package JSONTestUtil.truncateDatabase()- Clear all tables between testsTestUtil.query(sql)- Execute raw SQL (MySQL/PostgreSQL)
Mocking Patterns:
// Config mocking
mock(app.config.cnpmcore, 'propertyName', newValue);
// HTTP mocking
app.mockHttpclient('https://example.com/path', 'GET', {
data: await TestUtil.readFixturesFile('fixture.json'),
persist: false,
});
// Log assertions
app.mockLog();
await someOperation();
app.expectLog(/pattern/);Getting DI Objects in Tests:
const service = await app.getEggObject(PackageManagerService);Scheduled Task Testing:
await app.runSchedule(SyncPackageWorkerPath);Test Naming Pattern:
describe('test/path/to/SourceFile.test.ts', () => {
describe('[HTTP_METHOD /api/path] functionName()', () => {
it('should handle expected behavior', async () => {
// Test implementation
});
});
});Domain-Driven Design (DDD) Architecture
cnpmcore follows Domain-Driven Design principles with clear separation of concerns:
Layer Architecture (Dependency Flow)
Controller (HTTP Interface Layer)
↓ depends on
Service (Business Logic Layer)
↓ depends on
Repository (Data Access Layer)
↓ depends on
Model (ORM/Database Layer)
Entity (Domain Models - no dependencies, pure business logic)
Common (Utilities and Adapters - available to all layers)Layer Responsibilities
Controller Layer (app/port/controller/):
- HTTP request/response handling
- Request validation using
@eggjs/typebox-validate - User authentication and authorization
- NO business logic - delegate to Services
- Inheritance:
YourController extends AbstractController extends MiddlewareController - Auto-applied middlewares:
AlwaysAuth,Tracing,ErrorHandler
Key AbstractController methods:
ensurePublishAccess(ctx, fullname)- Authorization check for package publishgetPackageEntity(scope, name)- Fetch package with error handlingsetCDNHeaders(ctx)- Set cache control headersgetAllowSync(ctx)- Check if sync should be triggered
Service Layer (app/core/service/):
- Core business logic implementation
- Orchestration of multiple repositories and entities
- Transaction management
- Publish domain events via
EventBus - Use
@SingletonProto({ accessLevel: AccessLevel.PUBLIC })scope - Define command interfaces (e.g.,
PublishPackageCmd) for complex operations - NO HTTP concerns, NO direct database access
Repository Layer (app/repository/):
- Data access and persistence
- CRUD operations on Models
- Query building and optimization
- NO business logic
Entity Layer (app/core/entity/):
- Domain models with business behavior
- Pure business logic (no infrastructure dependencies)
- Factory pattern:
static create(data)method on each entity - Use
EntityUtil.defaultData(data, 'idField')for ID/timestamp defaults - Type guards for union types (e.g.,
isGranularToken()) EasyData<T, Id>type helper for optional create-time fields
Model Layer (app/repository/model/):
- ORM definitions using Leoric
- Database schema mapping
- Table and column definitions
- NO business logic
Model-Entity Bridge (app/repository/util/):
ModelConvertor.convertEntityToModel(entity, ModelClass)- Persist entityModelConvertor.convertModelToEntity(model, EntityClass)- Load entity- Uses
@EntityProperty()decorator for complex nested property mapping ModelMetadataUtilfor reflection-based property discovery
Key Decorators & Annotations
HTTP Layer (from @eggjs/tegg):
@HTTPController()- Marks class as HTTP controller@HTTPMethod({ path, method })- Route definition@HTTPBody()/@HTTPQuery()/@HTTPParam()- Request data injection with typebox validation@HTTPContext() ctx: Context- Inject Egg.js context@Middleware(MiddlewareClass)- Apply middleware to controller
Dependency Injection:
@Inject()- Field-level dependency injection@SingletonProto({ accessLevel: AccessLevel.PUBLIC })- Singleton service scope@ContextProto()- Request-scoped service (e.g.,UserRoleManager)
ORM Layer (from leoric):
@Model()- Marks class as ORM model@Attribute(DataTypes.TYPE, options)- Column definition@EntityProperty('nested.path')- Maps model field to entity property
Lifecycle:
@LifecycleInit()- Post-construction initialization hook
Repository Method Naming Convention
ALWAYS follow these naming patterns:
findSomething- Query a single model/entitysaveSomething- Save (create or update) a modelremoveSomething- Delete a modellistSomethings- Query multiple models (use plural)
Request Validation Trilogy
ALWAYS validate requests in this exact order:
Request Parameter Validation - First line of defense
// Use @eggjs/typebox-validate for type-safe validation // See app/port/typebox.ts for examplesUser Authentication & Token Permissions
// Token roles: 'read' | 'publish' | 'setting' const authorizedUser = await this.userRoleManager.requiredAuthorizedUser(ctx, 'publish');- Resource Authorization - Prevent horizontal privilege escalation
// Example: Ensure user is package maintainer await this.userRoleManager.requiredPackageMaintainer(pkg, authorizedUser); // Or use convenience method const { pkg } = await this.ensurePublishAccess(ctx, fullname);
Modifying Database Models
When changing a Model, update all 3 locations:
- SQL migration files:
sql/mysql/*.sqlANDsql/postgresql/*.sql - ORM Model:
app/repository/model/*.ts - Domain Entity:
app/core/entity/*.ts
NEVER auto-generate SQL migrations - manual review is required for safety.
Prerequisites and Environment Setup
- Node.js:
^22.18.0 || ^24.11.0 || ^26.0.0 - Database: MySQL 5.7+ or PostgreSQL 17+ (SQLite support in progress)
- Cache: Redis 6+
- Optional: Elasticsearch 8.x for enhanced search capabilities
Working Effectively
Bootstrap and Build
# Install dependencies (takes ~2 minutes)
npm install
# Copy environment configuration
cp .env.example .env
# Lint code (very fast, <1 second)
npm run lint
# Fix linting issues
npm run lint:fix
# Build TypeScript (takes ~6 seconds)
npm run tsc
# Production build (takes ~6 seconds)
npm run tsc:prodDatabase Setup - MySQL (Recommended for Development)
# Start MySQL + Redis services via Docker (takes ~1 minute to pull images initially)
docker compose -f docker-compose.yml up -d
# Verify services are running
docker compose ps
# Initialize database (takes <2 seconds)
CNPMCORE_DATABASE_NAME=cnpmcore bash ./prepare-database-mysql.sh
# For tests, create test database
mysql -h 127.0.0.1 -P 3306 -u root -e "CREATE DATABASE cnpmcore_unittest;"Database Setup - PostgreSQL (Alternative)
# Start PostgreSQL + Redis services via Docker
docker compose -f docker-compose-postgres.yml up -d
# Initialize database (takes <1 second)
CNPMCORE_DATABASE_NAME=cnpmcore bash ./prepare-database-postgresql.shDevelopment Server
# MySQL development server (starts in ~20 seconds)
npm run dev
# Server runs on http://127.0.0.1:7001
# PostgreSQL development server
npm run dev:postgresql
# Server runs on http://127.0.0.1:7001Testing
# Run full test suite with MySQL - NEVER CANCEL: Takes 4+ minutes. Set timeout to 10+ minutes.
npm run test
# Run full test suite with PostgreSQL - NEVER CANCEL: Takes 4+ minutes. Set timeout to 10+ minutes.
npm run test:postgresql
# Run single test file (for faster iteration, takes ~12 seconds)
npm run test:local test/common/CryptoUtil.test.ts
# Test coverage with MySQL - NEVER CANCEL: Takes 5+ minutes. Set timeout to 15+ minutes.
npm run cov
# Test coverage with PostgreSQL - NEVER CANCEL: Takes 5+ minutes. Set timeout to 15+ minutes.
npm run cov:postgresqlCRITICAL TESTING NOTES:
- NEVER CANCEL build or test commands - they may take 4-15 minutes to complete
- Individual test files run much faster (~12 seconds) for development iteration
- Full test suite processes 100+ test files and requires database initialization
- Test failures may occur in CI environment; use individual test files for validation
Testing Philosophy:
- Write tests for all new features - No feature is complete without tests
- Test at the right layer - Controller tests for HTTP, Service tests for business logic
- Mock external dependencies - Use
mock()from@eggjs/mock - Use realistic test data - Create through
TestUtilhelper methods - Clean up after tests - Database is reset between test files
- Test both success and failure cases - Error paths are equally important
Common Test Patterns:
import { app, mock } from '@eggjs/mock/bootstrap';
import { TestUtil } from '../../../test/TestUtil';
describe('test/path/to/YourController.test.ts', () => {
describe('[GET /api/endpoint] methodName()', () => {
it('should return expected result', async () => {
// Setup
const { authorization } = await TestUtil.createUser();
// Execute
const res = await app.httpRequest().get('/api/endpoint').set('authorization', authorization).expect(200);
// Assert
assert.equal(res.body.someField, expectedValue);
});
it('should handle unauthorized access', async () => {
const res = await app.httpRequest().get('/api/endpoint').expect(401);
assert.equal(res.body.error, '[UNAUTHORIZED] Login first');
});
});
});Production Commands
# CI pipeline commands - NEVER CANCEL: Takes 5+ minutes. Set timeout to 15+ minutes.
npm run ci # MySQL CI (includes lint, test, coverage, build)
npm run ci:postgresql # PostgreSQL CI
# Production start/stop
npm run start # Start as daemon
npm run stop # Stop daemon
npm run start:foreground # Start in foreground for debuggingValidation Scenarios
ALWAYS manually validate changes by running through these scenarios:
Basic API Validation
# Start development server
npm run dev
# Test registry root endpoint
curl http://127.0.0.1:7001
# Should return JSON with app metadata and stats
# Test authentication endpoint
curl http://127.0.0.1:7001/-/whoami
# Should return authentication error (expected when not logged in)
# Test package listing (initially empty)
curl http://127.0.0.1:7001/-/allAdmin User Setup and Package Publishing
# Register admin user (cnpmcore_admin) - requires allowPublicRegistration=true in config
npm login --registry=http://127.0.0.1:7001
# Verify login
npm whoami --registry=http://127.0.0.1:7001
# Test package publishing
npm publish --registry=http://127.0.0.1:7001Architecture and Navigation
Project Structure
app/
├── common/ # Global utilities and adapters
│ ├── adapter/ # External service adapters (NpmRegistry, Binary, etc.)
│ └── enum/ # Shared enumerations
├── core/ # Business logic layer
│ ├── entity/ # Core domain models
│ ├── event/ # Event handlers and async processing
│ ├── service/ # Core business services
│ └── util/ # Internal utilities
├── port/ # Interface layer
│ ├── controller/ # HTTP controllers
│ ├── middleware/ # Express middleware
│ ├── schedule/ # Background job schedulers
│ └── webauth/ # WebAuth integration
├── repository/ # Data access layer
│ ├── model/ # ORM models
│ └── util/ # Repository utilities
└── infra/ # Infrastructure adaptersKey Services and Controllers
- PackageController: Main package CRUD operations
- PackageManagerService: Core package management business logic
- BinarySyncerService: Binary package synchronization
- ChangesStreamService: NPM registry change stream processing
- UserController: User authentication and profile management
Infrastructure Adapters (app/infra/)
Enterprise customization layer for PaaS integration. cnpmcore provides default implementations, but enterprises should implement their own based on their infrastructure:
- NFSClientAdapter: File storage abstraction (local/S3/OSS)
- QueueAdapter: Message queue integration
- AuthAdapter: Authentication system integration
- BinaryAdapter: Binary package storage adapter
- SearchAdapter: Elasticsearch integration
- CacheAdapter: Redis caching with distributed locking
These adapters allow cnpmcore to integrate with different cloud providers and enterprise systems without modifying core business logic.
Use @LifecycleInit() decorator for adapter initialization hooks.
Configuration Files
config/config.default.ts: Main application configurationconfig/database.ts: Database connection settingsconfig/binaries.ts: Binary package mirror configurations.env: Environment-specific variablestsconfig.json: TypeScript compilation settingstsconfig.prod.json: Production build settings
Common Development Tasks
Adding New Features
ALWAYS follow this workflow:
- Plan the change - Identify which layers need modification
- Run linter -
npm run lint:fixto establish clean baseline Bottom-up implementation - Build from data layer up to controller:
a. Model Layer (if new data structure needed):
- Add SQL migrations:
sql/mysql/*.sqlANDsql/postgresql/*.sql - Create Model:
app/repository/model/YourModel.ts - Run database migration scripts
b. Entity Layer (domain models):
- Create Entity:
app/core/entity/YourEntity.ts - Implement business logic and behavior
- Keep entities pure (no infrastructure dependencies)
c. Repository Layer (data access):
- Create Repository:
app/repository/YourRepository.ts - Follow naming:
findX,saveX,removeX,listXs - Inject dependencies using
@Inject()
d. Service Layer (business logic):
- Create Service:
app/core/service/YourService.ts - Orchestrate repositories and entities
- Use
@SingletonProto()for service lifecycle
e. Controller Layer (HTTP endpoints):
- Create Controller:
app/port/controller/YourController.ts - Extend
AbstractController - Add HTTP method decorators:
@HTTPMethod(),@HTTPBody(), etc. - Implement 3-step validation (params → auth → authorization)
- Add SQL migrations:
- Add tests - Create test file:
test/path/matching/source/YourFile.test.ts - Lint and test -
npm run lint:fix && npm run test:local test/your/test.test.ts - Type check -
npm run typecheck - Commit - Use semantic commit messages (feat/fix/chore/docs/test)
Example Controller Implementation:
import { AbstractController } from './AbstractController';
import { HTTPController, HTTPMethod, HTTPQuery, Inject } from 'egg';
@HTTPController()
export class YourController extends AbstractController {
@Inject()
private readonly yourService: YourService;
@HTTPMethod({ path: '/api/path', method: 'GET' })
async yourMethod(@HTTPQuery() params: YourQueryType) {
// 1. Validate params (done by @HTTPQuery with typebox)
// 2. Authenticate user
const user = await this.userRoleManager.requiredAuthorizedUser(this.ctx, 'read');
// 3. Authorize resource access (if needed)
// 4. Delegate to service
return await this.yourService.doSomething(params);
}
}Database Migrations
- SQL files are in
sql/mysql/andsql/postgresql/ - Migration scripts automatically run during database preparation
- NEVER modify existing migration files - only add new ones
Background Jobs
- Schedulers are in
app/port/schedule/ - Include sync workers, cleanup tasks, and stream processors
- Jobs run automatically when development server starts
Troubleshooting
Database Connection Issues
# Check if services are running
docker compose ps
# Reset MySQL environment
docker compose -f docker-compose.yml down
docker compose -f docker-compose.yml up -d
CNPMCORE_DATABASE_NAME=cnpmcore bash ./prepare-database-mysql.sh
# Reset PostgreSQL environment
docker compose -f docker-compose-postgres.yml down
docker compose -f docker-compose-postgres.yml up -d
CNPMCORE_DATABASE_NAME=cnpmcore bash ./prepare-database-postgresql.shBuild Issues
# Clean and rebuild
npm run clean
npm run tsc
# Check TypeScript configuration
npx tsc --noEmitTest Issues
# Create missing test database
mysql -h 127.0.0.1 -P 3306 -u root -e "CREATE DATABASE cnpmcore_unittest;"
# Run single test for debugging
npm run test:local test/common/CryptoUtil.test.tsCI/CD Integration
The project uses GitHub Actions with workflows in .github/workflows/:
nodejs.yml: Main CI pipeline with MySQL, PostgreSQL, and Elasticsearch testing- Multiple Node.js versions tested: 22, 24, 26
- CRITICAL: CI jobs include long-running tests that can take 15+ minutes per database type
Pre-commit Validation
ALWAYS run before committing:
npm run lint:fix # Fix linting issues
npm run tsc # Verify TypeScript compilation
npm run test:local test/path/to/relevant.test.ts # Run relevant testsDocker Support
Development Environments
docker-compose.yml: MySQL + Redis + phpMyAdmindocker-compose-postgres.yml: PostgreSQL + Redis + pgAdmindocker-compose-es.yml: Elasticsearch integration
Production Images
# Build Alpine image
npm run images:alpine
# Build Debian image
npm run images:debianExternal Dependencies
- Database: MySQL 9.x or PostgreSQL 17+
- Cache: Redis 6+
- Search: Elasticsearch 8.x (optional)
- Storage: Local filesystem or S3-compatible storage
- Framework: Egg.js with extensive TypeScript integration
Performance Notes
Command execution times (for timeout planning):
- Startup Time: ~20 seconds for development server
- Build Time: ~6 seconds for TypeScript compilation
- Test Time: 4-15 minutes for full suite (database dependent)
- Individual Test: ~12 seconds for single test file
- Package Installation: ~2 minutes for npm install
- Database Init: <2 seconds for either MySQL or PostgreSQL
- Linting: <1 second (
vp lint, oxlint-based, very fast)
Always account for these timings when setting timeouts for automated processes.
Semantic Commit Messages
Use conventional commit format for all commits:
feat:- New featuresfix:- Bug fixesdocs:- Documentation changeschore:- Maintenance taskstest:- Test additions or modificationsrefactor:- Code refactoringperf:- Performance improvements
Examples:
feat: add support for GitHub binary mirroring
fix: resolve authentication token expiration issue
docs: update API documentation for sync endpoints
test: add tests for package publication workflowWhere files are headingNew Copilot instructions files have more rules about branches and pull requests, and fewer about code style.
Of the Copilot instructions files we read, the 2,297 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.
In this repositoryOne other file it gives its agents.
- CLAUDE.mdCLAUDE.md
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

