From 438d082e306de89cb2a8f452dab8dde94d3a0dbb Mon Sep 17 00:00:00 2001 From: Panger Lkr <73515951+pangerlkr@users.noreply.github.com> Date: Fri, 23 Jan 2026 22:50:59 +0530 Subject: [PATCH 01/29] feat: add cloud infrastructure security skill Add comprehensive cloud and infrastructure security skill covering: - IAM & access control (least privilege, MFA) - Secrets management & rotation - Network security (VPC, firewalls) - Logging & monitoring setup - CI/CD pipeline security - Cloudflare/CDN security - Backup & disaster recovery - Pre-deployment checklist Complements existing security-review skill with cloud-specific guidance. --- .../cloud-infrastructure-security.md | 361 ++++++++++++++++++ 1 file changed, 361 insertions(+) create mode 100644 skills/security-review/cloud-infrastructure-security.md diff --git a/skills/security-review/cloud-infrastructure-security.md b/skills/security-review/cloud-infrastructure-security.md new file mode 100644 index 00000000..24e9ec20 --- /dev/null +++ b/skills/security-review/cloud-infrastructure-security.md @@ -0,0 +1,361 @@ +| name | description | +|------|-------------| +| cloud-infrastructure-security | Use this skill when deploying to cloud platforms, configuring infrastructure, managing IAM policies, setting up logging/monitoring, or implementing CI/CD pipelines. Provides cloud security checklist aligned with best practices. | + +# Cloud & Infrastructure Security Skill + +This skill ensures cloud infrastructure, CI/CD pipelines, and deployment configurations follow security best practices and comply with industry standards. + +## When to Activate + +- Deploying applications to cloud platforms (AWS, Vercel, Railway, Cloudflare) +- Configuring IAM roles and permissions +- Setting up CI/CD pipelines +- Implementing infrastructure as code (Terraform, CloudFormation) +- Configuring logging and monitoring +- Managing secrets in cloud environments +- Setting up CDN and edge security +- Implementing disaster recovery and backup strategies + +## Cloud Security Checklist + +### 1. IAM & Access Control + +#### Principle of Least Privilege + +```yaml +# ✅ CORRECT: Minimal permissions +iam_role: + permissions: + - s3:GetObject # Only read access + - s3:ListBucket + resources: + - arn:aws:s3:::my-bucket/* # Specific bucket only + +# ❌ WRONG: Overly broad permissions +iam_role: + permissions: + - s3:* # All S3 actions + resources: + - "*" # All resources +``` + +#### Multi-Factor Authentication (MFA) + +```bash +# ALWAYS enable MFA for root/admin accounts +aws iam enable-mfa-device \ + --user-name admin \ + --serial-number arn:aws:iam::123456789:mfa/admin \ + --authentication-code1 123456 \ + --authentication-code2 789012 +``` + +#### Verification Steps + +- [ ] No root account usage in production +- [ ] MFA enabled for all privileged accounts +- [ ] Service accounts use roles, not long-lived credentials +- [ ] IAM policies follow least privilege +- [ ] Regular access reviews conducted +- [ ] Unused credentials rotated or removed + +### 2. Secrets Management + +#### Cloud Secrets Managers + +```typescript +// ✅ CORRECT: Use cloud secrets manager +import { SecretsManager } from '@aws-sdk/client-secrets-manager'; + +const client = new SecretsManager({ region: 'us-east-1' }); +const secret = await client.getSecretValue({ SecretId: 'prod/api-key' }); +const apiKey = JSON.parse(secret.SecretString).key; + +// ❌ WRONG: Hardcoded or in environment variables only +const apiKey = process.env.API_KEY; // Not rotated, not audited +``` + +#### Secrets Rotation + +```bash +# Set up automatic rotation for database credentials +aws secretsmanager rotate-secret \ + --secret-id prod/db-password \ + --rotation-lambda-arn arn:aws:lambda:region:account:function:rotate \ + --rotation-rules AutomaticallyAfterDays=30 +``` + +#### Verification Steps + +- [ ] All secrets stored in cloud secrets manager (AWS Secrets Manager, Vercel Secrets) +- [ ] Automatic rotation enabled for database credentials +- [ ] API keys rotated at least quarterly +- [ ] No secrets in code, logs, or error messages +- [ ] Audit logging enabled for secret access + +### 3. Network Security + +#### VPC and Firewall Configuration + +```terraform +# ✅ CORRECT: Restricted security group +resource "aws_security_group" "app" { + name = "app-sg" + + ingress { + from_port = 443 + to_port = 443 + protocol = "tcp" + cidr_blocks = ["10.0.0.0/16"] # Internal VPC only + } + + egress { + from_port = 443 + to_port = 443 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] # Only HTTPS outbound + } +} + +# ❌ WRONG: Open to the internet +resource "aws_security_group" "bad" { + ingress { + from_port = 0 + to_port = 65535 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] # All ports, all IPs! + } +} +``` + +#### Verification Steps + +- [ ] Database not publicly accessible +- [ ] SSH/RDP ports restricted to VPN/bastion only +- [ ] Security groups follow least privilege +- [ ] Network ACLs configured +- [ ] VPC flow logs enabled + +### 4. Logging & Monitoring + +#### CloudWatch/Logging Configuration + +```typescript +// ✅ CORRECT: Comprehensive logging +import { CloudWatchLogsClient, CreateLogStreamCommand } from '@aws-sdk/client-cloudwatch-logs'; + +const logSecurityEvent = async (event: SecurityEvent) => { + await cloudwatch.putLogEvents({ + logGroupName: '/aws/security/events', + logStreamName: 'authentication', + logEvents: [{ + timestamp: Date.now(), + message: JSON.stringify({ + type: event.type, + userId: event.userId, + ip: event.ip, + result: event.result, + // Never log sensitive data + }) + }] + }); +}; +``` + +#### Verification Steps + +- [ ] CloudWatch/logging enabled for all services +- [ ] Failed authentication attempts logged +- [ ] Admin actions audited +- [ ] Log retention configured (90+ days for compliance) +- [ ] Alerts configured for suspicious activity +- [ ] Logs centralized and tamper-proof + +### 5. CI/CD Pipeline Security + +#### Secure Pipeline Configuration + +```yaml +# ✅ CORRECT: Secure GitHub Actions workflow +name: Deploy + +on: + push: + branches: [main] + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + contents: read # Minimal permissions + + steps: + - uses: actions/checkout@v4 + + # Scan for secrets + - name: Secret scanning + uses: trufflesecurity/trufflehog@main + + # Dependency audit + - name: Audit dependencies + run: npm audit --audit-level=high + + # Use OIDC, not long-lived tokens + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: arn:aws:iam::123456789:role/GitHubActionsRole + aws-region: us-east-1 +``` + +#### Supply Chain Security + +```json +// package.json - Use lock files and integrity checks +{ + "scripts": { + "install": "npm ci", // Use ci for reproducible builds + "audit": "npm audit --audit-level=moderate", + "check": "npm outdated" + } +} +``` + +#### Verification Steps + +- [ ] OIDC used instead of long-lived credentials +- [ ] Secrets scanning in pipeline +- [ ] Dependency vulnerability scanning +- [ ] Container image scanning (if applicable) +- [ ] Branch protection rules enforced +- [ ] Code review required before merge +- [ ] Signed commits enforced + +### 6. Cloudflare & CDN Security + +#### Cloudflare Security Configuration + +```typescript +// ✅ CORRECT: Cloudflare Workers with security headers +export default { + async fetch(request: Request): Promise { + const response = await fetch(request); + + // Add security headers + const headers = new Headers(response.headers); + headers.set('X-Frame-Options', 'DENY'); + headers.set('X-Content-Type-Options', 'nosniff'); + headers.set('Referrer-Policy', 'strict-origin-when-cross-origin'); + headers.set('Permissions-Policy', 'geolocation=(), microphone=()'); + + return new Response(response.body, { + status: response.status, + headers + }); + } +}; +``` + +#### WAF Rules + +```bash +# Enable Cloudflare WAF managed rules +# - OWASP Core Ruleset +# - Cloudflare Managed Ruleset +# - Rate limiting rules +# - Bot protection +``` + +#### Verification Steps + +- [ ] WAF enabled with OWASP rules +- [ ] Rate limiting configured +- [ ] Bot protection active +- [ ] DDoS protection enabled +- [ ] Security headers configured +- [ ] SSL/TLS strict mode enabled + +### 7. Backup & Disaster Recovery + +#### Automated Backups + +```terraform +# ✅ CORRECT: Automated RDS backups +resource "aws_db_instance" "main" { + allocated_storage = 20 + engine = "postgres" + + backup_retention_period = 30 # 30 days retention + backup_window = "03:00-04:00" + maintenance_window = "mon:04:00-mon:05:00" + + enabled_cloudwatch_logs_exports = ["postgresql"] + + deletion_protection = true # Prevent accidental deletion +} +``` + +#### Verification Steps + +- [ ] Automated daily backups configured +- [ ] Backup retention meets compliance requirements +- [ ] Point-in-time recovery enabled +- [ ] Backup testing performed quarterly +- [ ] Disaster recovery plan documented +- [ ] RPO and RTO defined and tested + +## Pre-Deployment Cloud Security Checklist + +Before ANY production cloud deployment: + +- [ ] **IAM**: Root account not used, MFA enabled, least privilege policies +- [ ] **Secrets**: All secrets in cloud secrets manager with rotation +- [ ] **Network**: Security groups restricted, no public databases +- [ ] **Logging**: CloudWatch/logging enabled with retention +- [ ] **Monitoring**: Alerts configured for anomalies +- [ ] **CI/CD**: OIDC auth, secrets scanning, dependency audits +- [ ] **CDN/WAF**: Cloudflare WAF enabled with OWASP rules +- [ ] **Encryption**: Data encrypted at rest and in transit +- [ ] **Backups**: Automated backups with tested recovery +- [ ] **Compliance**: GDPR/HIPAA requirements met (if applicable) +- [ ] **Documentation**: Infrastructure documented, runbooks created +- [ ] **Incident Response**: Security incident plan in place + +## Common Cloud Security Misconfigurations + +### S3 Bucket Exposure + +```bash +# ❌ WRONG: Public bucket +aws s3api put-bucket-acl --bucket my-bucket --acl public-read + +# ✅ CORRECT: Private bucket with specific access +aws s3api put-bucket-acl --bucket my-bucket --acl private +aws s3api put-bucket-policy --bucket my-bucket --policy file://policy.json +``` + +### RDS Public Access + +```terraform +# ❌ WRONG +resource "aws_db_instance" "bad" { + publicly_accessible = true # NEVER do this! +} + +# ✅ CORRECT +resource "aws_db_instance" "good" { + publicly_accessible = false + vpc_security_group_ids = [aws_security_group.db.id] +} +``` + +## Resources + +- [AWS Security Best Practices](https://aws.amazon.com/security/best-practices/) +- [CIS AWS Foundations Benchmark](https://www.cisecurity.org/benchmark/amazon_web_services) +- [Cloudflare Security Documentation](https://developers.cloudflare.com/security/) +- [OWASP Cloud Security](https://owasp.org/www-project-cloud-security/) +- [Terraform Security Best Practices](https://www.terraform.io/docs/cloud/guides/recommended-practices/) + +**Remember**: Cloud misconfigurations are the leading cause of data breaches. A single exposed S3 bucket or overly permissive IAM policy can compromise your entire infrastructure. Always follow the principle of least privilege and defense in depth. From 4d98d9f12561cf2115761452f2aa3a4abc464521 Mon Sep 17 00:00:00 2001 From: Pangerkumzuk Longkumer <73515951+pangerlkr@users.noreply.github.com> Date: Mon, 16 Feb 2026 07:10:39 +0530 Subject: [PATCH 02/29] Add Go environment setup step to workflow Added a step to set up the Go environment in GitHub Actions workflow. --- .github/workflows/copilot-setup-steps.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/workflows/copilot-setup-steps.yml diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml new file mode 100644 index 00000000..c57d5abd --- /dev/null +++ b/.github/workflows/copilot-setup-steps.yml @@ -0,0 +1,18 @@ + - name: Setup Go environment + uses: actions/setup-go@v6.2.0 + with: + # The Go version to download (if necessary) and use. Supports semver spec and ranges. Be sure to enclose this option in single quotation marks. + go-version: # optional + # Path to the go.mod, go.work, .go-version, or .tool-versions file. + go-version-file: # optional + # Set this option to true if you want the action to always check for the latest available version that satisfies the version spec + check-latest: # optional + # Used to pull Go distributions from go-versions. Since there's a default, this is typically not supplied by the user. When running this action on github.com, the default value is sufficient. When running on GHES, you can pass a personal access token for github.com if you are experiencing rate limiting. + token: # optional, default is ${{ github.server_url == 'https://github.com' && github.token || '' }} + # Used to specify whether caching is needed. Set to true, if you'd like to enable caching. + cache: # optional, default is true + # Used to specify the path to a dependency file - go.sum + cache-dependency-path: # optional + # Target architecture for Go to use. Examples: x86, x64. Will use system architecture by default. + architecture: # optional + From 2ca903d4c5a5465cd00a2fcf3351ff044d1c98ef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 02:20:28 +0000 Subject: [PATCH 03/29] Initial plan From af24c617bb296572c26d7ee56b6813b0c91dea82 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 02:22:14 +0000 Subject: [PATCH 04/29] Fix all markdownlint errors (MD038, MD058, MD025, MD034) Co-authored-by: pangerlkr <73515951+pangerlkr@users.noreply.github.com> --- commands/multi-execute.md | 2 +- commands/multi-plan.md | 2 +- commands/multi-workflow.md | 2 +- commands/pm2.md | 7 +++++-- skills/nutrient-document-processing/SKILL.md | 2 +- 5 files changed, 9 insertions(+), 6 deletions(-) diff --git a/commands/multi-execute.md b/commands/multi-execute.md index 6e7ac824..cc5c24bc 100644 --- a/commands/multi-execute.md +++ b/commands/multi-execute.md @@ -78,7 +78,7 @@ EOF", ``` **Model Parameter Notes**: -- `{{GEMINI_MODEL_FLAG}}`: When using `--backend gemini`, replace with `--gemini-model gemini-3-pro-preview ` (note trailing space); use empty string for codex +- `{{GEMINI_MODEL_FLAG}}`: When using `--backend gemini`, replace with `--gemini-model gemini-3-pro-preview` (note trailing space); use empty string for codex **Role Prompts**: diff --git a/commands/multi-plan.md b/commands/multi-plan.md index cbc37e0e..947fc953 100644 --- a/commands/multi-plan.md +++ b/commands/multi-plan.md @@ -37,7 +37,7 @@ EOF", ``` **Model Parameter Notes**: -- `{{GEMINI_MODEL_FLAG}}`: When using `--backend gemini`, replace with `--gemini-model gemini-3-pro-preview ` (note trailing space); use empty string for codex +- `{{GEMINI_MODEL_FLAG}}`: When using `--backend gemini`, replace with `--gemini-model gemini-3-pro-preview` (note trailing space); use empty string for codex **Role Prompts**: diff --git a/commands/multi-workflow.md b/commands/multi-workflow.md index bdcdab95..c6e8e4ba 100644 --- a/commands/multi-workflow.md +++ b/commands/multi-workflow.md @@ -66,7 +66,7 @@ EOF", ``` **Model Parameter Notes**: -- `{{GEMINI_MODEL_FLAG}}`: When using `--backend gemini`, replace with `--gemini-model gemini-3-pro-preview ` (note trailing space); use empty string for codex +- `{{GEMINI_MODEL_FLAG}}`: When using `--backend gemini`, replace with `--gemini-model gemini-3-pro-preview` (note trailing space); use empty string for codex **Role Prompts**: diff --git a/commands/pm2.md b/commands/pm2.md index 75d63490..263a0349 100644 --- a/commands/pm2.md +++ b/commands/pm2.md @@ -247,6 +247,7 @@ After all files generated, output: ## PM2 Init Complete **Services:** + | Port | Name | Type | |------|------|------| | {port} | {name} | {type} | @@ -254,10 +255,12 @@ After all files generated, output: **Claude Commands:** /pm2-all, /pm2-all-stop, /pm2-{port}, /pm2-{port}-stop, /pm2-logs, /pm2-status **Terminal Commands:** -# First time (with config file) + +## First time (with config file) + pm2 start ecosystem.config.cjs && pm2 save -# After first time (simplified) +## After first time (simplified) pm2 start all # Start all pm2 stop all # Stop all pm2 restart all # Restart all diff --git a/skills/nutrient-document-processing/SKILL.md b/skills/nutrient-document-processing/SKILL.md index eeb7a34c..8aa64753 100644 --- a/skills/nutrient-document-processing/SKILL.md +++ b/skills/nutrient-document-processing/SKILL.md @@ -9,7 +9,7 @@ Process documents with the [Nutrient DWS Processor API](https://www.nutrient.io/ ## Setup -Get a free API key at **https://dashboard.nutrient.io/sign_up/?product=processor** +Get a free API key at **** ```bash export NUTRIENT_API_KEY="pdf_live_..." From c5de2a7bf704f0542ca1065a3a75b3cd51cf9879 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 02:22:49 +0000 Subject: [PATCH 05/29] Remove misleading comments about trailing spaces Co-authored-by: pangerlkr <73515951+pangerlkr@users.noreply.github.com> --- commands/multi-execute.md | 2 +- commands/multi-plan.md | 2 +- commands/multi-workflow.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/commands/multi-execute.md b/commands/multi-execute.md index cc5c24bc..206cf352 100644 --- a/commands/multi-execute.md +++ b/commands/multi-execute.md @@ -78,7 +78,7 @@ EOF", ``` **Model Parameter Notes**: -- `{{GEMINI_MODEL_FLAG}}`: When using `--backend gemini`, replace with `--gemini-model gemini-3-pro-preview` (note trailing space); use empty string for codex +- `{{GEMINI_MODEL_FLAG}}`: When using `--backend gemini`, replace with `--gemini-model gemini-3-pro-preview`; use empty string for codex **Role Prompts**: diff --git a/commands/multi-plan.md b/commands/multi-plan.md index 947fc953..f33149d3 100644 --- a/commands/multi-plan.md +++ b/commands/multi-plan.md @@ -37,7 +37,7 @@ EOF", ``` **Model Parameter Notes**: -- `{{GEMINI_MODEL_FLAG}}`: When using `--backend gemini`, replace with `--gemini-model gemini-3-pro-preview` (note trailing space); use empty string for codex +- `{{GEMINI_MODEL_FLAG}}`: When using `--backend gemini`, replace with `--gemini-model gemini-3-pro-preview`; use empty string for codex **Role Prompts**: diff --git a/commands/multi-workflow.md b/commands/multi-workflow.md index c6e8e4ba..d8f84abf 100644 --- a/commands/multi-workflow.md +++ b/commands/multi-workflow.md @@ -66,7 +66,7 @@ EOF", ``` **Model Parameter Notes**: -- `{{GEMINI_MODEL_FLAG}}`: When using `--backend gemini`, replace with `--gemini-model gemini-3-pro-preview` (note trailing space); use empty string for codex +- `{{GEMINI_MODEL_FLAG}}`: When using `--backend gemini`, replace with `--gemini-model gemini-3-pro-preview`; use empty string for codex **Role Prompts**: From b1d47b22eaf13c0d8fd4caf8acf6c4d48f0c73e3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 02:37:38 +0000 Subject: [PATCH 06/29] Initial plan From 2be6e09501474a72165a2c91f2a9577e02b11412 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 02:55:40 +0000 Subject: [PATCH 07/29] Initial plan From 2b6ff6b55ef77ebab97cf704e966c3b96d7602ad Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 02:58:12 +0000 Subject: [PATCH 08/29] Initial plan for markdownlint error fixes Co-authored-by: pangerlkr <73515951+pangerlkr@users.noreply.github.com> --- package-lock.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package-lock.json b/package-lock.json index db3ddf53..22265668 100644 --- a/package-lock.json +++ b/package-lock.json @@ -294,6 +294,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -599,6 +600,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -1930,6 +1932,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, From 586637f94c5405335547c6680f3a725cafbb9594 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 03:01:15 +0000 Subject: [PATCH 09/29] Revert unrelated package-lock.json changes Co-authored-by: pangerlkr <73515951+pangerlkr@users.noreply.github.com> --- package-lock.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 22265668..db3ddf53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -294,7 +294,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -600,7 +599,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -1932,7 +1930,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, From d54124afad972bf827aa1971dce1647ad4ae0f6a Mon Sep 17 00:00:00 2001 From: Pangerkumzuk Longkumer <73515951+pangerlkr@users.noreply.github.com> Date: Wed, 18 Feb 2026 07:14:32 +0530 Subject: [PATCH 10/29] fix: remove useless escape characters in regex patterns (no-useless-escape ESLint) --- scripts/lib/package-manager.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/lib/package-manager.js b/scripts/lib/package-manager.js index c12537f0..7eefc3c9 100644 --- a/scripts/lib/package-manager.js +++ b/scripts/lib/package-manager.js @@ -282,7 +282,7 @@ function setProjectPackageManager(pmName, projectDir = process.cwd()) { // Allowed characters in script/binary names: alphanumeric, dash, underscore, dot, slash, @ // This prevents shell metacharacter injection while allowing scoped packages (e.g., @scope/pkg) -const SAFE_NAME_REGEX = /^[@a-zA-Z0-9_.\/-]+$/; +const SAFE_NAME_REGEX = /^[\[@a-zA-Z0-9_./-]+$/; /** * Get the command to run a script @@ -316,7 +316,7 @@ function getRunCommand(script, options = {}) { // Allowed characters in arguments: alphanumeric, whitespace, dashes, dots, slashes, // equals, colons, commas, quotes, @. Rejects shell metacharacters like ; | & ` $ ( ) { } < > ! -const SAFE_ARGS_REGEX = /^[@a-zA-Z0-9\s_.\/:=,'"*+-]+$/; +const SAFE_ARGS_REGEX = /^[@a-zA-Z0-9\s_./:=,'"*+-]+$/; /** * Get the command to execute a package binary From 3d979855595107e1a4dcf464acda01fbd60de6b8 Mon Sep 17 00:00:00 2001 From: Pangerkumzuk Longkumer <73515951+pangerlkr@users.noreply.github.com> Date: Wed, 18 Feb 2026 07:15:53 +0530 Subject: [PATCH 11/29] fix: remove unused execFileSync import (no-unused-vars ESLint) --- tests/hooks/evaluate-session.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/hooks/evaluate-session.test.js b/tests/hooks/evaluate-session.test.js index a5b92e2d..f49bb6a4 100644 --- a/tests/hooks/evaluate-session.test.js +++ b/tests/hooks/evaluate-session.test.js @@ -11,7 +11,7 @@ const assert = require('assert'); const path = require('path'); const fs = require('fs'); const os = require('os'); -const { spawnSync, execFileSync } = require('child_process'); +const { spawnSync } = require('child_process'); const evaluateScript = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'evaluate-session.js'); From 8966282e48d159b561ab52046303bad4af6fee2d Mon Sep 17 00:00:00 2001 From: Pangerkumzuk Longkumer <73515951+pangerlkr@users.noreply.github.com> Date: Wed, 18 Feb 2026 07:18:40 +0530 Subject: [PATCH 12/29] fix: add comments to empty catch blocks (no-empty ESLint) --- tests/hooks/hooks.test.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/hooks/hooks.test.js b/tests/hooks/hooks.test.js index 8e24bac9..e86633e8 100644 --- a/tests/hooks/hooks.test.js +++ b/tests/hooks/hooks.test.js @@ -1324,7 +1324,7 @@ async function runTests() { val = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10); assert.strictEqual(val, 2, 'Second call should write count 2'); } finally { - try { fs.unlinkSync(counterFile); } catch {} + try { fs.unlinkSync(counterFile); } catch { /* ignore */ } } })) passed++; else failed++; @@ -1341,7 +1341,7 @@ async function runTests() { assert.strictEqual(result.code, 0); assert.ok(result.stderr.includes('5 tool calls reached'), 'Should suggest compact at threshold'); } finally { - try { fs.unlinkSync(counterFile); } catch {} + try { fs.unlinkSync(counterFile); } catch { /* ignore */ } } })) passed++; else failed++; @@ -1359,7 +1359,7 @@ async function runTests() { assert.strictEqual(result.code, 0); assert.ok(result.stderr.includes('30 tool calls'), 'Should suggest at threshold + 25n intervals'); } finally { - try { fs.unlinkSync(counterFile); } catch {} + try { fs.unlinkSync(counterFile); } catch { /* ignore */ } } })) passed++; else failed++; @@ -1376,7 +1376,7 @@ async function runTests() { assert.ok(!result.stderr.includes('tool calls reached'), 'Should not suggest below threshold'); assert.ok(!result.stderr.includes('checkpoint'), 'Should not suggest checkpoint'); } finally { - try { fs.unlinkSync(counterFile); } catch {} + try { fs.unlinkSync(counterFile); } catch { /* ignore */ } } })) passed++; else failed++; @@ -1394,7 +1394,7 @@ async function runTests() { const newCount = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10); assert.strictEqual(newCount, 1, 'Should reset to 1 on overflow value'); } finally { - try { fs.unlinkSync(counterFile); } catch {} + try { fs.unlinkSync(counterFile); } catch { /* ignore */ } } })) passed++; else failed++; @@ -1410,7 +1410,7 @@ async function runTests() { const newCount = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10); assert.strictEqual(newCount, 1, 'Should reset to 1 on negative value'); } finally { - try { fs.unlinkSync(counterFile); } catch {} + try { fs.unlinkSync(counterFile); } catch { /* ignore */ } } })) passed++; else failed++; @@ -1426,7 +1426,7 @@ async function runTests() { assert.strictEqual(result.code, 0); assert.ok(result.stderr.includes('50 tool calls reached'), 'Zero threshold should fall back to 50'); } finally { - try { fs.unlinkSync(counterFile); } catch {} + try { fs.unlinkSync(counterFile); } catch { /* ignore */ } } })) passed++; else failed++; @@ -1443,7 +1443,7 @@ async function runTests() { assert.strictEqual(result.code, 0); assert.ok(result.stderr.includes('50 tool calls reached'), 'Should use default threshold of 50'); } finally { - try { fs.unlinkSync(counterFile); } catch {} + try { fs.unlinkSync(counterFile); } catch { /* ignore */ } } })) passed++; else failed++; @@ -1883,7 +1883,7 @@ async function runTests() { assert.strictEqual(result.code, 0); assert.ok(result.stderr.includes('38 tool calls'), 'Should suggest at threshold(13) + 25 = 38'); } finally { - try { fs.unlinkSync(counterFile); } catch {} + try { fs.unlinkSync(counterFile); } catch { /* ignore */ } } })) passed++; else failed++; @@ -1901,7 +1901,7 @@ async function runTests() { assert.strictEqual(result.code, 0); assert.ok(!result.stderr.includes('checkpoint'), 'Should NOT suggest at count=50 with threshold=13'); } finally { - try { fs.unlinkSync(counterFile); } catch {} + try { fs.unlinkSync(counterFile); } catch { /* ignore */ } } })) passed++; else failed++; @@ -1918,7 +1918,7 @@ async function runTests() { const newCount = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10); assert.strictEqual(newCount, 1, 'Should reset to 1 on corrupted file content'); } finally { - try { fs.unlinkSync(counterFile); } catch {} + try { fs.unlinkSync(counterFile); } catch { /* ignore */ } } })) passed++; else failed++; @@ -1935,7 +1935,7 @@ async function runTests() { const newCount = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10); assert.strictEqual(newCount, 1000001, 'Should increment from exactly 1000000'); } finally { - try { fs.unlinkSync(counterFile); } catch {} + try { fs.unlinkSync(counterFile); } catch { /* ignore */ } } })) passed++; else failed++; From 2ad888ca82c6cd064f32179e6ee3a6fe181c0ffc Mon Sep 17 00:00:00 2001 From: Pangerkumzuk Longkumer <73515951+pangerlkr@users.noreply.github.com> Date: Wed, 18 Feb 2026 07:21:58 +0530 Subject: [PATCH 13/29] Refactor console log formatting in tests --- tests/hooks/suggest-compact.test.js | 111 ++++++++++++++++++---------- 1 file changed, 73 insertions(+), 38 deletions(-) diff --git a/tests/hooks/suggest-compact.test.js b/tests/hooks/suggest-compact.test.js index 174dc0ea..5b09631c 100644 --- a/tests/hooks/suggest-compact.test.js +++ b/tests/hooks/suggest-compact.test.js @@ -19,11 +19,11 @@ const compactScript = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'sugg function test(name, fn) { try { fn(); - console.log(` \u2713 ${name}`); + console.log(` \u2713 ${name}`); return true; } catch (err) { - console.log(` \u2717 ${name}`); - console.log(` Error: ${err.message}`); + console.log(` \u2717 ${name}`); + console.log(` Error: ${err.message}`); return false; } } @@ -55,7 +55,9 @@ function getCounterFilePath(sessionId) { } function runTests() { - console.log('\n=== Testing suggest-compact.js ===\n'); + console.log(' +=== Testing suggest-compact.js === +'); let passed = 0; let failed = 0; @@ -66,7 +68,11 @@ function runTests() { // Cleanup helper function cleanupCounter() { - try { fs.unlinkSync(counterFile); } catch {} + try { + fs.unlinkSync(counterFile); + } catch (err) { + // Ignore error + } } // Basic functionality @@ -80,7 +86,8 @@ function runTests() { const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10); assert.strictEqual(count, 1, 'Counter should be 1 after first run'); cleanupCounter(); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('increments counter on subsequent runs', () => { cleanupCounter(); @@ -90,10 +97,12 @@ function runTests() { const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10); assert.strictEqual(count, 3, 'Counter should be 3 after three runs'); cleanupCounter(); - })) passed++; else failed++; + })) passed++; + else failed++; // Threshold suggestion - console.log('\nThreshold suggestion:'); + console.log(' +Threshold suggestion:'); if (test('suggests compact at threshold (COMPACT_THRESHOLD=3)', () => { cleanupCounter(); @@ -106,7 +115,8 @@ function runTests() { `Should suggest compact at threshold. Got stderr: ${result.stderr}` ); cleanupCounter(); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('does NOT suggest compact before threshold', () => { cleanupCounter(); @@ -117,10 +127,12 @@ function runTests() { 'Should NOT suggest compact before threshold' ); cleanupCounter(); - })) passed++; else failed++; + })) passed++; + else failed++; // Interval suggestion (every 25 calls after threshold) - console.log('\nInterval suggestion:'); + console.log(' +Interval suggestion:'); if (test('suggests at threshold + 25 interval', () => { cleanupCounter(); @@ -135,10 +147,12 @@ function runTests() { `Should suggest at threshold+25 interval. Got stderr: ${result.stderr}` ); cleanupCounter(); - })) passed++; else failed++; + })) passed++; + else failed++; // Environment variable handling - console.log('\nEnvironment variable handling:'); + console.log(' +Environment variable handling:'); if (test('uses default threshold (50) when COMPACT_THRESHOLD is not set', () => { cleanupCounter(); @@ -151,7 +165,8 @@ function runTests() { `Should use default threshold of 50. Got stderr: ${result.stderr}` ); cleanupCounter(); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('ignores invalid COMPACT_THRESHOLD (negative)', () => { cleanupCounter(); @@ -163,7 +178,8 @@ function runTests() { `Should fallback to 50 for negative threshold. Got stderr: ${result.stderr}` ); cleanupCounter(); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('ignores non-numeric COMPACT_THRESHOLD', () => { cleanupCounter(); @@ -175,10 +191,12 @@ function runTests() { `Should fallback to 50 for non-numeric threshold. Got stderr: ${result.stderr}` ); cleanupCounter(); - })) passed++; else failed++; + })) passed++; + else failed++; // Corrupted counter file - console.log('\nCorrupted counter file:'); + console.log(' +Corrupted counter file:'); if (test('resets counter on corrupted file content', () => { cleanupCounter(); @@ -189,7 +207,8 @@ function runTests() { const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10); assert.strictEqual(count, 1, 'Should reset to 1 on corrupted file'); cleanupCounter(); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('resets counter on extremely large value', () => { cleanupCounter(); @@ -200,7 +219,8 @@ function runTests() { const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10); assert.strictEqual(count, 1, 'Should reset to 1 for value > 1000000'); cleanupCounter(); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('handles empty counter file', () => { cleanupCounter(); @@ -211,10 +231,12 @@ function runTests() { const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10); assert.strictEqual(count, 1, 'Should start at 1 for empty file'); cleanupCounter(); - })) passed++; else failed++; + })) passed++; + else failed++; // Session isolation - console.log('\nSession isolation:'); + console.log(' +Session isolation:'); if (test('uses separate counter files per session ID', () => { const sessionA = `compact-a-${Date.now()}`; @@ -230,23 +252,27 @@ function runTests() { assert.strictEqual(countA, 2, 'Session A should have count 2'); assert.strictEqual(countB, 1, 'Session B should have count 1'); } finally { - try { fs.unlinkSync(fileA); } catch {} - try { fs.unlinkSync(fileB); } catch {} + try { fs.unlinkSync(fileA); } catch (err) { /* ignore */ } + try { fs.unlinkSync(fileB); } catch (err) { /* ignore */ } } - })) passed++; else failed++; + })) passed++; + else failed++; // Always exits 0 - console.log('\nExit code:'); + console.log(' +Exit code:'); if (test('always exits 0 (never blocks Claude)', () => { cleanupCounter(); const result = runCompact({ CLAUDE_SESSION_ID: testSession }); assert.strictEqual(result.code, 0, 'Should always exit 0'); cleanupCounter(); - })) passed++; else failed++; + })) passed++; + else failed++; // ── Round 29: threshold boundary values ── - console.log('\nThreshold boundary values:'); + console.log(' +Threshold boundary values:'); if (test('rejects COMPACT_THRESHOLD=0 (falls back to 50)', () => { cleanupCounter(); @@ -258,7 +284,8 @@ function runTests() { `Should fallback to 50 for threshold=0. Got stderr: ${result.stderr}` ); cleanupCounter(); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('accepts COMPACT_THRESHOLD=10000 (boundary max)', () => { cleanupCounter(); @@ -270,7 +297,8 @@ function runTests() { `Should accept threshold=10000. Got stderr: ${result.stderr}` ); cleanupCounter(); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('rejects COMPACT_THRESHOLD=10001 (falls back to 50)', () => { cleanupCounter(); @@ -282,7 +310,8 @@ function runTests() { `Should fallback to 50 for threshold=10001. Got stderr: ${result.stderr}` ); cleanupCounter(); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('rejects float COMPACT_THRESHOLD (e.g. 3.5)', () => { cleanupCounter(); @@ -297,7 +326,8 @@ function runTests() { 'Float threshold should be parseInt-ed to 3, no suggestion at count=50' ); cleanupCounter(); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('counter value at exact boundary 1000000 is valid', () => { cleanupCounter(); @@ -307,7 +337,8 @@ function runTests() { const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10); assert.strictEqual(count, 1000000, 'Counter at 1000000 boundary should be valid'); cleanupCounter(); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('counter value at 1000001 is clamped (reset to 1)', () => { cleanupCounter(); @@ -316,14 +347,16 @@ function runTests() { const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10); assert.strictEqual(count, 1, 'Counter > 1000000 should be reset to 1'); cleanupCounter(); - })) passed++; else failed++; + })) passed++; + else failed++; // ── Round 64: default session ID fallback ── - console.log('\nDefault session ID fallback (Round 64):'); + console.log(' +Default session ID fallback (Round 64):'); if (test('uses "default" session ID when CLAUDE_SESSION_ID is empty', () => { const defaultCounterFile = getCounterFilePath('default'); - try { fs.unlinkSync(defaultCounterFile); } catch {} + try { fs.unlinkSync(defaultCounterFile); } catch (err) { /* ignore */ } try { // Pass empty CLAUDE_SESSION_ID — falsy, so script uses 'default' const env = { ...process.env, CLAUDE_SESSION_ID: '' }; @@ -338,12 +371,14 @@ function runTests() { const count = parseInt(fs.readFileSync(defaultCounterFile, 'utf8').trim(), 10); assert.strictEqual(count, 1, 'Counter should be 1 for first run with default session'); } finally { - try { fs.unlinkSync(defaultCounterFile); } catch {} + try { fs.unlinkSync(defaultCounterFile); } catch (err) { /* ignore */ } } - })) passed++; else failed++; + })) passed++; + else failed++; // Summary - console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`); + console.log(` +Results: Passed: ${passed}, Failed: ${failed}`); process.exit(failed > 0 ? 1 : 0); } From 0bf47bbb41c4a2a2b8511368206c7fd67c4a5826 Mon Sep 17 00:00:00 2001 From: Pangerkumzuk Longkumer <73515951+pangerlkr@users.noreply.github.com> Date: Wed, 18 Feb 2026 07:29:16 +0530 Subject: [PATCH 14/29] Update print statement from 'Hfix: update package manager tests and add summaryello' to 'Goodbye' --- tests/lib/package-manager.test.js | 743 ++++++++++++++++++------------ 1 file changed, 450 insertions(+), 293 deletions(-) diff --git a/tests/lib/package-manager.test.js b/tests/lib/package-manager.test.js index a9b32f7b..58818e93 100644 --- a/tests/lib/package-manager.test.js +++ b/tests/lib/package-manager.test.js @@ -16,11 +16,11 @@ const pm = require('../../scripts/lib/package-manager'); function test(name, fn) { try { fn(); - console.log(` ✓ ${name}`); + console.log(` ✓ ${name}`); return true; } catch (err) { - console.log(` ✗ ${name}`); - console.log(` Error: ${err.message}`); + console.log(` ✗ ${name}`); + console.log(` Error: ${err.message}`); return false; } } @@ -39,7 +39,9 @@ function cleanupTestDir(testDir) { // Test suite function runTests() { - console.log('\n=== Testing package-manager.js ===\n'); + console.log(' +=== Testing package-manager.js === +'); let passed = 0; let failed = 0; @@ -52,7 +54,8 @@ function runTests() { assert.ok(pm.PACKAGE_MANAGERS.pnpm, 'Should have pnpm'); assert.ok(pm.PACKAGE_MANAGERS.yarn, 'Should have yarn'); assert.ok(pm.PACKAGE_MANAGERS.bun, 'Should have bun'); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('Each manager has required properties', () => { const requiredProps = ['name', 'lockFile', 'installCmd', 'runCmd', 'execCmd', 'testCmd', 'buildCmd', 'devCmd']; @@ -61,10 +64,12 @@ function runTests() { assert.ok(config[prop], `${name} should have ${prop}`); } } - })) passed++; else failed++; + })) passed++; + else failed++; // detectFromLockFile tests - console.log('\ndetectFromLockFile:'); + console.log(' +detectFromLockFile:'); if (test('detects npm from package-lock.json', () => { const testDir = createTestDir(); @@ -75,7 +80,8 @@ function runTests() { } finally { cleanupTestDir(testDir); } - })) passed++; else failed++; + })) passed++; + else failed++; if (test('detects pnpm from pnpm-lock.yaml', () => { const testDir = createTestDir(); @@ -86,7 +92,8 @@ function runTests() { } finally { cleanupTestDir(testDir); } - })) passed++; else failed++; + })) passed++; + else failed++; if (test('detects yarn from yarn.lock', () => { const testDir = createTestDir(); @@ -97,7 +104,8 @@ function runTests() { } finally { cleanupTestDir(testDir); } - })) passed++; else failed++; + })) passed++; + else failed++; if (test('detects bun from bun.lockb', () => { const testDir = createTestDir(); @@ -108,7 +116,8 @@ function runTests() { } finally { cleanupTestDir(testDir); } - })) passed++; else failed++; + })) passed++; + else failed++; if (test('returns null when no lock file exists', () => { const testDir = createTestDir(); @@ -118,7 +127,8 @@ function runTests() { } finally { cleanupTestDir(testDir); } - })) passed++; else failed++; + })) passed++; + else failed++; if (test('respects detection priority (pnpm > npm)', () => { const testDir = createTestDir(); @@ -132,51 +142,48 @@ function runTests() { } finally { cleanupTestDir(testDir); } - })) passed++; else failed++; + })) passed++; + else failed++; // detectFromPackageJson tests - console.log('\ndetectFromPackageJson:'); + console.log(' +detectFromPackageJson:'); if (test('detects package manager from packageManager field', () => { const testDir = createTestDir(); try { - fs.writeFileSync(path.join(testDir, 'package.json'), JSON.stringify({ - name: 'test', - packageManager: 'pnpm@8.6.0' - })); + fs.writeFileSync(path.join(testDir, 'package.json'), JSON.stringify({ name: 'test', packageManager: 'pnpm@8.6.0' })); const result = pm.detectFromPackageJson(testDir); assert.strictEqual(result, 'pnpm'); } finally { cleanupTestDir(testDir); } - })) passed++; else failed++; + })) passed++; + else failed++; if (test('handles packageManager without version', () => { const testDir = createTestDir(); try { - fs.writeFileSync(path.join(testDir, 'package.json'), JSON.stringify({ - name: 'test', - packageManager: 'yarn' - })); + fs.writeFileSync(path.join(testDir, 'package.json'), JSON.stringify({ name: 'test', packageManager: 'yarn' })); const result = pm.detectFromPackageJson(testDir); assert.strictEqual(result, 'yarn'); } finally { cleanupTestDir(testDir); } - })) passed++; else failed++; + })) passed++; + else failed++; if (test('returns null when no packageManager field', () => { const testDir = createTestDir(); try { - fs.writeFileSync(path.join(testDir, 'package.json'), JSON.stringify({ - name: 'test' - })); + fs.writeFileSync(path.join(testDir, 'package.json'), JSON.stringify({ name: 'test' })); const result = pm.detectFromPackageJson(testDir); assert.strictEqual(result, null); } finally { cleanupTestDir(testDir); } - })) passed++; else failed++; + })) passed++; + else failed++; if (test('returns null when no package.json exists', () => { const testDir = createTestDir(); @@ -186,27 +193,32 @@ function runTests() { } finally { cleanupTestDir(testDir); } - })) passed++; else failed++; + })) passed++; + else failed++; // getAvailablePackageManagers tests - console.log('\ngetAvailablePackageManagers:'); + console.log(' +getAvailablePackageManagers:'); if (test('returns array of available managers', () => { const available = pm.getAvailablePackageManagers(); assert.ok(Array.isArray(available), 'Should return array'); // npm should always be available with Node.js assert.ok(available.includes('npm'), 'npm should be available'); - })) passed++; else failed++; + })) passed++; + else failed++; // getPackageManager tests - console.log('\ngetPackageManager:'); + console.log(' +getPackageManager:'); if (test('returns object with name, config, and source', () => { const result = pm.getPackageManager(); assert.ok(result.name, 'Should have name'); assert.ok(result.config, 'Should have config'); assert.ok(result.source, 'Should have source'); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('respects environment variable', () => { const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER; @@ -222,12 +234,12 @@ function runTests() { delete process.env.CLAUDE_PACKAGE_MANAGER; } } - })) passed++; else failed++; + })) passed++; + else failed++; if (test('detects from lock file in project', () => { const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER; delete process.env.CLAUDE_PACKAGE_MANAGER; - const testDir = createTestDir(); try { fs.writeFileSync(path.join(testDir, 'bun.lockb'), ''); @@ -240,10 +252,12 @@ function runTests() { process.env.CLAUDE_PACKAGE_MANAGER = originalEnv; } } - })) passed++; else failed++; + })) passed++; + else failed++; // getRunCommand tests - console.log('\ngetRunCommand:'); + console.log(' +getRunCommand:'); if (test('returns correct install command', () => { const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER; @@ -258,7 +272,8 @@ function runTests() { delete process.env.CLAUDE_PACKAGE_MANAGER; } } - })) passed++; else failed++; + })) passed++; + else failed++; if (test('returns correct test command', () => { const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER; @@ -273,10 +288,12 @@ function runTests() { delete process.env.CLAUDE_PACKAGE_MANAGER; } } - })) passed++; else failed++; + })) passed++; + else failed++; // getExecCommand tests - console.log('\ngetExecCommand:'); + console.log(' +getExecCommand:'); if (test('returns correct exec command for npm', () => { const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER; @@ -291,7 +308,8 @@ function runTests() { delete process.env.CLAUDE_PACKAGE_MANAGER; } } - })) passed++; else failed++; + })) passed++; + else failed++; if (test('returns correct exec command for pnpm', () => { const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER; @@ -306,10 +324,12 @@ function runTests() { delete process.env.CLAUDE_PACKAGE_MANAGER; } } - })) passed++; else failed++; + })) passed++; + else failed++; // getCommandPattern tests - console.log('\ngetCommandPattern:'); + console.log(' +getCommandPattern:'); if (test('generates pattern for dev command', () => { const pattern = pm.getCommandPattern('dev'); @@ -317,31 +337,35 @@ function runTests() { assert.ok(pattern.includes('pnpm'), 'Should include pnpm'); assert.ok(pattern.includes('yarn dev'), 'Should include yarn'); assert.ok(pattern.includes('bun run dev'), 'Should include bun'); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('pattern matches actual commands', () => { const pattern = pm.getCommandPattern('test'); const regex = new RegExp(pattern); - assert.ok(regex.test('npm test'), 'Should match npm test'); assert.ok(regex.test('pnpm test'), 'Should match pnpm test'); assert.ok(regex.test('yarn test'), 'Should match yarn test'); assert.ok(regex.test('bun test'), 'Should match bun test'); assert.ok(!regex.test('cargo test'), 'Should not match cargo test'); - })) passed++; else failed++; + })) passed++; + else failed++; // getSelectionPrompt tests - console.log('\ngetSelectionPrompt:'); + console.log(' +getSelectionPrompt:'); if (test('returns informative prompt', () => { const prompt = pm.getSelectionPrompt(); assert.ok(prompt.includes('Supported package managers'), 'Should list supported managers'); assert.ok(prompt.includes('CLAUDE_PACKAGE_MANAGER'), 'Should mention env var'); assert.ok(prompt.includes('lock file'), 'Should mention lock file option'); - })) passed++; else failed++; + })) passed++; + else failed++; // setProjectPackageManager tests - console.log('\nsetProjectPackageManager:'); + console.log(' +setProjectPackageManager:'); if (test('sets project package manager', () => { const testDir = createTestDir(); @@ -349,7 +373,6 @@ function runTests() { const result = pm.setProjectPackageManager('pnpm', testDir); assert.strictEqual(result.packageManager, 'pnpm'); assert.ok(result.setAt, 'Should have setAt timestamp'); - // Verify file was created const configPath = path.join(testDir, '.claude', 'package-manager.json'); assert.ok(fs.existsSync(configPath), 'Config file should exist'); @@ -358,25 +381,30 @@ function runTests() { } finally { cleanupTestDir(testDir); } - })) passed++; else failed++; + })) passed++; + else failed++; if (test('rejects unknown package manager', () => { assert.throws(() => { pm.setProjectPackageManager('cargo'); }, /Unknown package manager/); - })) passed++; else failed++; + })) passed++; + else failed++; // setPreferredPackageManager tests - console.log('\nsetPreferredPackageManager:'); + console.log(' +setPreferredPackageManager:'); if (test('rejects unknown package manager', () => { assert.throws(() => { pm.setPreferredPackageManager('pip'); }, /Unknown package manager/); - })) passed++; else failed++; + })) passed++; + else failed++; // detectFromPackageJson edge cases - console.log('\ndetectFromPackageJson (edge cases):'); + console.log(' +detectFromPackageJson (edge cases):'); if (test('handles invalid JSON in package.json', () => { const testDir = createTestDir(); @@ -387,24 +415,24 @@ function runTests() { } finally { cleanupTestDir(testDir); } - })) passed++; else failed++; + })) passed++; + else failed++; if (test('returns null for unknown package manager in packageManager field', () => { const testDir = createTestDir(); try { - fs.writeFileSync(path.join(testDir, 'package.json'), JSON.stringify({ - name: 'test', - packageManager: 'deno@1.0' - })); + fs.writeFileSync(path.join(testDir, 'package.json'), JSON.stringify({ name: 'test', packageManager: 'deno@1.0' })); const result = pm.detectFromPackageJson(testDir); assert.strictEqual(result, null); } finally { cleanupTestDir(testDir); } - })) passed++; else failed++; + })) passed++; + else failed++; // getExecCommand edge cases - console.log('\ngetExecCommand (edge cases):'); + console.log(' +getExecCommand (edge cases):'); if (test('returns exec command without args', () => { const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER; @@ -419,10 +447,12 @@ function runTests() { delete process.env.CLAUDE_PACKAGE_MANAGER; } } - })) passed++; else failed++; + })) passed++; + else failed++; // getRunCommand additional cases - console.log('\ngetRunCommand (additional):'); + console.log(' +getRunCommand (additional):'); if (test('returns correct build command', () => { const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER; @@ -436,7 +466,8 @@ function runTests() { delete process.env.CLAUDE_PACKAGE_MANAGER; } } - })) passed++; else failed++; + })) passed++; + else failed++; if (test('returns correct dev command', () => { const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER; @@ -450,7 +481,8 @@ function runTests() { delete process.env.CLAUDE_PACKAGE_MANAGER; } } - })) passed++; else failed++; + })) passed++; + else failed++; if (test('returns correct custom script command', () => { const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER; @@ -464,21 +496,26 @@ function runTests() { delete process.env.CLAUDE_PACKAGE_MANAGER; } } - })) passed++; else failed++; + })) passed++; + else failed++; // DETECTION_PRIORITY tests - console.log('\nDETECTION_PRIORITY:'); + console.log(' +DETECTION_PRIORITY:'); if (test('has pnpm first', () => { assert.strictEqual(pm.DETECTION_PRIORITY[0], 'pnpm'); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('has npm last', () => { assert.strictEqual(pm.DETECTION_PRIORITY[pm.DETECTION_PRIORITY.length - 1], 'npm'); - })) passed++; else failed++; + })) passed++; + else failed++; // getCommandPattern additional cases - console.log('\ngetCommandPattern (additional):'); + console.log(' +getCommandPattern (additional):'); if (test('generates pattern for install command', () => { const pattern = pm.getCommandPattern('install'); @@ -487,7 +524,8 @@ function runTests() { assert.ok(regex.test('pnpm install'), 'Should match pnpm install'); assert.ok(regex.test('yarn'), 'Should match yarn (install implicit)'); assert.ok(regex.test('bun install'), 'Should match bun install'); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('generates pattern for custom action', () => { const pattern = pm.getCommandPattern('lint'); @@ -496,17 +534,18 @@ function runTests() { assert.ok(regex.test('pnpm lint'), 'Should match pnpm lint'); assert.ok(regex.test('yarn lint'), 'Should match yarn lint'); assert.ok(regex.test('bun run lint'), 'Should match bun run lint'); - })) passed++; else failed++; + })) passed++; + else failed++; // getPackageManager robustness tests - console.log('\ngetPackageManager (robustness):'); + console.log(' +getPackageManager (robustness):'); if (test('falls through on corrupted project config JSON', () => { const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pm-robust-')); const claudeDir = path.join(testDir, '.claude'); fs.mkdirSync(claudeDir, { recursive: true }); fs.writeFileSync(path.join(claudeDir, 'package-manager.json'), '{not valid json!!!'); - const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER; try { delete process.env.CLAUDE_PACKAGE_MANAGER; @@ -520,15 +559,14 @@ function runTests() { } fs.rmSync(testDir, { recursive: true, force: true }); } - })) passed++; else failed++; + })) passed++; + else failed++; if (test('falls through on project config with unknown PM', () => { const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pm-robust-')); const claudeDir = path.join(testDir, '.claude'); fs.mkdirSync(claudeDir, { recursive: true }); - fs.writeFileSync(path.join(claudeDir, 'package-manager.json'), - JSON.stringify({ packageManager: 'nonexistent-pm' })); - + fs.writeFileSync(path.join(claudeDir, 'package-manager.json'), JSON.stringify({ packageManager: 'nonexistent-pm' })); const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER; try { delete process.env.CLAUDE_PACKAGE_MANAGER; @@ -541,26 +579,32 @@ function runTests() { } fs.rmSync(testDir, { recursive: true, force: true }); } - })) passed++; else failed++; + })) passed++; + else failed++; // getRunCommand validation tests - console.log('\ngetRunCommand (validation):'); + console.log(' +getRunCommand (validation):'); if (test('rejects empty script name', () => { assert.throws(() => pm.getRunCommand(''), /non-empty string/); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('rejects null script name', () => { assert.throws(() => pm.getRunCommand(null), /non-empty string/); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('rejects script name with shell metacharacters', () => { assert.throws(() => pm.getRunCommand('test; rm -rf /'), /unsafe characters/); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('rejects script name with backticks', () => { assert.throws(() => pm.getRunCommand('test`whoami`'), /unsafe characters/); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('accepts scoped package names', () => { const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER; @@ -575,22 +619,27 @@ function runTests() { delete process.env.CLAUDE_PACKAGE_MANAGER; } } - })) passed++; else failed++; + })) passed++; + else failed++; // getExecCommand validation tests - console.log('\ngetExecCommand (validation):'); + console.log(' +getExecCommand (validation):'); if (test('rejects empty binary name', () => { assert.throws(() => pm.getExecCommand(''), /non-empty string/); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('rejects null binary name', () => { assert.throws(() => pm.getExecCommand(null), /non-empty string/); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('rejects binary name with shell metacharacters', () => { assert.throws(() => pm.getExecCommand('prettier; cat /etc/passwd'), /unsafe characters/); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('accepts dotted binary names like tsc', () => { const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER; @@ -605,18 +654,18 @@ function runTests() { delete process.env.CLAUDE_PACKAGE_MANAGER; } } - })) passed++; else failed++; + })) passed++; + else failed++; // getPackageManager source detection tests - console.log('\ngetPackageManager (source detection):'); + console.log(' +getPackageManager (source detection):'); if (test('detects from valid project-config (.claude/package-manager.json)', () => { const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pm-projcfg-')); const claudeDir = path.join(testDir, '.claude'); fs.mkdirSync(claudeDir, { recursive: true }); - fs.writeFileSync(path.join(claudeDir, 'package-manager.json'), - JSON.stringify({ packageManager: 'pnpm' })); - + fs.writeFileSync(path.join(claudeDir, 'package-manager.json'), JSON.stringify({ packageManager: 'pnpm' })); const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER; try { delete process.env.CLAUDE_PACKAGE_MANAGER; @@ -629,22 +678,19 @@ function runTests() { } fs.rmSync(testDir, { recursive: true, force: true }); } - })) passed++; else failed++; + })) passed++; + else failed++; if (test('project-config takes priority over package.json', () => { const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pm-priority-')); const claudeDir = path.join(testDir, '.claude'); fs.mkdirSync(claudeDir, { recursive: true }); - // Project config says bun - fs.writeFileSync(path.join(claudeDir, 'package-manager.json'), - JSON.stringify({ packageManager: 'bun' })); + fs.writeFileSync(path.join(claudeDir, 'package-manager.json'), JSON.stringify({ packageManager: 'bun' })); // package.json says yarn - fs.writeFileSync(path.join(testDir, 'package.json'), - JSON.stringify({ packageManager: 'yarn@4.0.0' })); + fs.writeFileSync(path.join(testDir, 'package.json'), JSON.stringify({ packageManager: 'yarn@4.0.0' })); // Lock file says npm fs.writeFileSync(path.join(testDir, 'package-lock.json'), '{}'); - const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER; try { delete process.env.CLAUDE_PACKAGE_MANAGER; @@ -657,16 +703,15 @@ function runTests() { } fs.rmSync(testDir, { recursive: true, force: true }); } - })) passed++; else failed++; + })) passed++; + else failed++; if (test('package.json takes priority over lock file', () => { const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pm-pj-lock-')); // package.json says yarn - fs.writeFileSync(path.join(testDir, 'package.json'), - JSON.stringify({ packageManager: 'yarn@4.0.0' })); + fs.writeFileSync(path.join(testDir, 'package.json'), JSON.stringify({ packageManager: 'yarn@4.0.0' })); // Lock file says npm fs.writeFileSync(path.join(testDir, 'package-lock.json'), '{}'); - const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER; try { delete process.env.CLAUDE_PACKAGE_MANAGER; @@ -679,7 +724,8 @@ function runTests() { } fs.rmSync(testDir, { recursive: true, force: true }); } - })) passed++; else failed++; + })) passed++; + else failed++; if (test('defaults to npm when no config found', () => { const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pm-default-')); @@ -695,10 +741,12 @@ function runTests() { } fs.rmSync(testDir, { recursive: true, force: true }); } - })) passed++; else failed++; + })) passed++; + else failed++; // setPreferredPackageManager success - console.log('\nsetPreferredPackageManager (success):'); + console.log(' +setPreferredPackageManager (success):'); if (test('successfully saves preferred package manager', () => { // This writes to ~/.claude/package-manager.json — read original to restore @@ -717,31 +765,40 @@ function runTests() { if (original) { fs.writeFileSync(configPath, original, 'utf8'); } else { - try { fs.unlinkSync(configPath); } catch {} + try { + fs.unlinkSync(configPath); + } catch (err) { + // ignore + } } } - })) passed++; else failed++; + })) passed++; + else failed++; // getCommandPattern completeness - console.log('\ngetCommandPattern (completeness):'); + console.log(' +getCommandPattern (completeness):'); if (test('generates pattern for test command', () => { const pattern = pm.getCommandPattern('test'); assert.ok(pattern.includes('npm test'), 'Should include npm test'); assert.ok(pattern.includes('pnpm test'), 'Should include pnpm test'); assert.ok(pattern.includes('bun test'), 'Should include bun test'); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('generates pattern for build command', () => { const pattern = pm.getCommandPattern('build'); assert.ok(pattern.includes('npm run build'), 'Should include npm run build'); assert.ok(pattern.includes('yarn build'), 'Should include yarn build'); - })) passed++; else failed++; + })) passed++; + else failed++; // getRunCommand PM-specific format tests - console.log('\ngetRunCommand (PM-specific formats):'); + console.log(' +getRunCommand (PM-specific formats):'); - if (test('pnpm custom script: pnpm