@girardmedia/bootspring 3.3.2 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/agents/accessibility-auditor.md +39 -0
- package/assets/agents/api-designer.md +40 -0
- package/assets/agents/auth-implementer.md +64 -0
- package/assets/agents/bug-hunter.md +42 -0
- package/assets/agents/bundle-analyzer.md +40 -0
- package/assets/agents/cache-optimizer.md +55 -0
- package/assets/agents/changelog-writer.md +55 -0
- package/assets/agents/ci-cd-builder.md +40 -0
- package/assets/agents/code-explainer.md +39 -0
- package/assets/agents/code-reviewer.md +39 -0
- package/assets/agents/cost-optimizer.md +57 -0
- package/assets/agents/cron-scheduler.md +51 -0
- package/assets/agents/data-seeder.md +56 -0
- package/assets/agents/database-architect.md +40 -0
- package/assets/agents/dependency-updater.md +40 -0
- package/assets/agents/deploy-checker.md +40 -0
- package/assets/agents/docker-optimizer.md +40 -0
- package/assets/agents/documentation-writer.md +40 -0
- package/assets/agents/email-builder.md +55 -0
- package/assets/agents/env-setup.md +40 -0
- package/assets/agents/error-handler.md +40 -0
- package/assets/agents/eslint-fixer.md +46 -0
- package/assets/agents/feature-flagger.md +69 -0
- package/assets/agents/git-detective.md +39 -0
- package/assets/agents/graphql-builder.md +60 -0
- package/assets/agents/incident-responder.md +59 -0
- package/assets/agents/log-analyzer.md +39 -0
- package/assets/agents/migration-planner.md +41 -0
- package/assets/agents/monorepo-navigator.md +39 -0
- package/assets/agents/nextjs-expert.md +57 -0
- package/assets/agents/notification-builder.md +56 -0
- package/assets/agents/onboarding-guide.md +39 -0
- package/assets/agents/performance-profiler.md +40 -0
- package/assets/agents/prisma-expert.md +57 -0
- package/assets/agents/rate-limiter.md +58 -0
- package/assets/agents/react-expert.md +58 -0
- package/assets/agents/refactorer.md +42 -0
- package/assets/agents/regex-builder.md +46 -0
- package/assets/agents/release-manager.md +40 -0
- package/assets/agents/s3-manager.md +58 -0
- package/assets/agents/schema-validator.md +40 -0
- package/assets/agents/search-builder.md +62 -0
- package/assets/agents/security-auditor.md +39 -0
- package/assets/agents/sitemap-generator.md +53 -0
- package/assets/agents/stripe-integrator.md +59 -0
- package/assets/agents/tailwind-expert.md +55 -0
- package/assets/agents/tech-debt-tracker.md +39 -0
- package/assets/agents/test-writer.md +42 -0
- package/assets/agents/type-fixer.md +45 -0
- package/assets/agents/webhook-builder.md +54 -0
- package/assets/rules/cpp.md +53 -0
- package/assets/rules/css.md +52 -0
- package/assets/rules/go.md +50 -0
- package/assets/rules/html.md +52 -0
- package/assets/rules/java.md +51 -0
- package/assets/rules/kotlin.md +50 -0
- package/assets/rules/php.md +51 -0
- package/assets/rules/python.md +51 -0
- package/assets/rules/ruby.md +51 -0
- package/assets/rules/rust.md +49 -0
- package/assets/rules/shell.md +52 -0
- package/assets/rules/sql.md +49 -0
- package/assets/rules/swift.md +50 -0
- package/assets/rules/typescript.md +52 -0
- package/assets/rules/yaml-json.md +51 -0
- package/assets/skills/accessibility.md +210 -0
- package/assets/skills/agent-patterns.md +387 -0
- package/assets/skills/ai-integration.md +263 -0
- package/assets/skills/animation-patterns.md +224 -0
- package/assets/skills/api-design.md +218 -0
- package/assets/skills/api-gateway.md +341 -0
- package/assets/skills/api-versioning.md +226 -0
- package/assets/skills/astro-patterns.md +233 -0
- package/assets/skills/auth-patterns.md +248 -0
- package/assets/skills/aws-patterns.md +171 -0
- package/assets/skills/background-jobs.md +162 -0
- package/assets/skills/browser-extensions.md +309 -0
- package/assets/skills/caching-patterns.md +253 -0
- package/assets/skills/ci-cd.md +251 -0
- package/assets/skills/cli-development.md +296 -0
- package/assets/skills/code-review.md +185 -0
- package/assets/skills/cron-patterns.md +327 -0
- package/assets/skills/data-fetching.md +231 -0
- package/assets/skills/database-migrations.md +346 -0
- package/assets/skills/database-patterns.md +219 -0
- package/assets/skills/debugging.md +281 -0
- package/assets/skills/design-system.md +289 -0
- package/assets/skills/django-patterns.md +182 -0
- package/assets/skills/docker-patterns.md +235 -0
- package/assets/skills/e2e-testing.md +287 -0
- package/assets/skills/edge-computing.md +268 -0
- package/assets/skills/electron-patterns.md +266 -0
- package/assets/skills/email-templates.md +206 -0
- package/assets/skills/error-handling.md +265 -0
- package/assets/skills/event-driven.md +232 -0
- package/assets/skills/express-patterns.md +239 -0
- package/assets/skills/fastapi-patterns.md +198 -0
- package/assets/skills/feature-flags.md +212 -0
- package/assets/skills/figma-to-code.md +298 -0
- package/assets/skills/file-upload.md +228 -0
- package/assets/skills/forms-patterns.md +264 -0
- package/assets/skills/gcp-patterns.md +189 -0
- package/assets/skills/git-workflow.md +187 -0
- package/assets/skills/golang-patterns.md +185 -0
- package/assets/skills/graphql-patterns.md +244 -0
- package/assets/skills/i18n-patterns.md +172 -0
- package/assets/skills/image-processing.md +350 -0
- package/assets/skills/java-springboot.md +226 -0
- package/assets/skills/kotlin-patterns.md +207 -0
- package/assets/skills/kubernetes-patterns.md +326 -0
- package/assets/skills/laravel-patterns.md +261 -0
- package/assets/skills/llm-fine-tuning.md +335 -0
- package/assets/skills/load-testing.md +303 -0
- package/assets/skills/logging-observability.md +228 -0
- package/assets/skills/markdown-processing.md +318 -0
- package/assets/skills/mcp-server-patterns.md +292 -0
- package/assets/skills/microservices.md +272 -0
- package/assets/skills/migration-patterns.md +239 -0
- package/assets/skills/mongodb-patterns.md +189 -0
- package/assets/skills/monorepo-patterns.md +287 -0
- package/assets/skills/nextjs-app-router.md +237 -0
- package/assets/skills/notification-patterns.md +348 -0
- package/assets/skills/oauth-patterns.md +246 -0
- package/assets/skills/payment-integration.md +222 -0
- package/assets/skills/pdf-generation.md +307 -0
- package/assets/skills/performance-optimization.md +277 -0
- package/assets/skills/php-patterns.md +210 -0
- package/assets/skills/prisma-patterns.md +241 -0
- package/assets/skills/prompt-engineering.md +193 -0
- package/assets/skills/pwa-patterns.md +247 -0
- package/assets/skills/python-patterns.md +158 -0
- package/assets/skills/python-testing.md +172 -0
- package/assets/skills/queue-patterns.md +295 -0
- package/assets/skills/rag-patterns.md +159 -0
- package/assets/skills/rate-limiting.md +319 -0
- package/assets/skills/react-components.md +201 -0
- package/assets/skills/react-native-patterns.md +299 -0
- package/assets/skills/real-time-patterns.md +181 -0
- package/assets/skills/redis-patterns.md +188 -0
- package/assets/skills/refactoring.md +218 -0
- package/assets/skills/regex-patterns.md +191 -0
- package/assets/skills/remix-patterns.md +262 -0
- package/assets/skills/responsive-design.md +199 -0
- package/assets/skills/ruby-rails-patterns.md +178 -0
- package/assets/skills/rust-patterns.md +211 -0
- package/assets/skills/search-patterns.md +227 -0
- package/assets/skills/security-hardening.md +237 -0
- package/assets/skills/seo-patterns.md +179 -0
- package/assets/skills/serverless-patterns.md +223 -0
- package/assets/skills/sql-optimization.md +154 -0
- package/assets/skills/state-management.md +254 -0
- package/assets/skills/storybook-patterns.md +330 -0
- package/assets/skills/svelte-patterns.md +258 -0
- package/assets/skills/swift-patterns.md +227 -0
- package/assets/skills/tailwind-patterns.md +272 -0
- package/assets/skills/tdd-workflow.md +199 -0
- package/assets/skills/terraform-patterns.md +270 -0
- package/assets/skills/testing-react.md +240 -0
- package/assets/skills/testing-vitest.md +232 -0
- package/assets/skills/typescript-strict.md +159 -0
- package/assets/skills/video-processing.md +340 -0
- package/assets/skills/vue-patterns.md +247 -0
- package/assets/skills/web-workers.md +327 -0
- package/assets/skills/webhooks-patterns.md +283 -0
- package/assets/skills/websocket-patterns.md +306 -0
- package/dist/cli/index.js +941 -958
- package/dist/core/index.d.ts +341 -11
- package/dist/core.js +58 -95
- package/dist/mcp/index.d.ts +33 -1
- package/dist/mcp-server.js +177 -255
- package/package.json +4 -1
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cli-development
|
|
3
|
+
description: Build CLI tools — Commander.js, Inquirer prompts, chalk output, progress bars, configuration, and packaging.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# CLI Development Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
|
|
10
|
+
Apply these patterns when building command-line tools in Node.js. Covers
|
|
11
|
+
argument parsing with Commander, interactive prompts with Inquirer, styled
|
|
12
|
+
output with chalk, progress indicators, configuration management, and
|
|
13
|
+
packaging for distribution via npm or standalone binaries. Use this skill
|
|
14
|
+
when creating developer tools, build scripts, or automation CLIs.
|
|
15
|
+
|
|
16
|
+
## How It Works
|
|
17
|
+
|
|
18
|
+
### 1. Commander.js — Argument Parsing
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import { Command, Option } from 'commander';
|
|
22
|
+
|
|
23
|
+
const program = new Command();
|
|
24
|
+
|
|
25
|
+
program
|
|
26
|
+
.name('mycli')
|
|
27
|
+
.version('1.0.0')
|
|
28
|
+
.description('A developer productivity tool');
|
|
29
|
+
|
|
30
|
+
// Subcommand with options and arguments
|
|
31
|
+
program
|
|
32
|
+
.command('deploy')
|
|
33
|
+
.description('Deploy to environment')
|
|
34
|
+
.argument('<environment>', 'Target environment (staging|production)')
|
|
35
|
+
.option('-f, --force', 'Skip confirmation prompt')
|
|
36
|
+
.option('-t, --tag <tag>', 'Deploy specific tag', 'latest')
|
|
37
|
+
.option('--dry-run', 'Show what would be deployed without deploying')
|
|
38
|
+
.addOption(
|
|
39
|
+
new Option('--strategy <strategy>', 'Deployment strategy')
|
|
40
|
+
.choices(['rolling', 'blue-green', 'canary'])
|
|
41
|
+
.default('rolling')
|
|
42
|
+
)
|
|
43
|
+
.action(async (environment, options) => {
|
|
44
|
+
if (!options.force && environment === 'production') {
|
|
45
|
+
const confirmed = await confirmProduction();
|
|
46
|
+
if (!confirmed) process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
await deploy(environment, options);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Global options
|
|
52
|
+
program
|
|
53
|
+
.option('--json', 'Output as JSON')
|
|
54
|
+
.option('-v, --verbose', 'Verbose logging')
|
|
55
|
+
.hook('preAction', (thisCommand) => {
|
|
56
|
+
if (thisCommand.opts().verbose) {
|
|
57
|
+
setLogLevel('debug');
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
program.parse();
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 2. Inquirer — Interactive Prompts
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
import { input, select, confirm, checkbox, password } from '@inquirer/prompts';
|
|
68
|
+
|
|
69
|
+
async function setupProject(): Promise<ProjectConfig> {
|
|
70
|
+
const name = await input({
|
|
71
|
+
message: 'Project name:',
|
|
72
|
+
default: path.basename(process.cwd()),
|
|
73
|
+
validate: (val) => /^[a-z0-9-]+$/.test(val) || 'Use lowercase letters, numbers, and hyphens',
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const framework = await select({
|
|
77
|
+
message: 'Framework:',
|
|
78
|
+
choices: [
|
|
79
|
+
{ name: 'Next.js', value: 'nextjs', description: 'Full-stack React' },
|
|
80
|
+
{ name: 'Express', value: 'express', description: 'Minimal API server' },
|
|
81
|
+
{ name: 'Fastify', value: 'fastify', description: 'High-performance API' },
|
|
82
|
+
],
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const features = await checkbox({
|
|
86
|
+
message: 'Features:',
|
|
87
|
+
choices: [
|
|
88
|
+
{ name: 'TypeScript', value: 'typescript', checked: true },
|
|
89
|
+
{ name: 'ESLint', value: 'eslint', checked: true },
|
|
90
|
+
{ name: 'Docker', value: 'docker' },
|
|
91
|
+
{ name: 'CI/CD', value: 'ci' },
|
|
92
|
+
],
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const proceed = await confirm({ message: 'Create project?', default: true });
|
|
96
|
+
if (!proceed) process.exit(0);
|
|
97
|
+
|
|
98
|
+
return { name, framework, features };
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### 3. Chalk — Styled Output
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
import chalk from 'chalk';
|
|
106
|
+
|
|
107
|
+
// Status messages
|
|
108
|
+
function logSuccess(msg: string) { console.log(chalk.green('OK') + ' ' + msg); }
|
|
109
|
+
function logError(msg: string) { console.error(chalk.red('ERR') + ' ' + msg); }
|
|
110
|
+
function logWarn(msg: string) { console.log(chalk.yellow('WARN') + ' ' + msg); }
|
|
111
|
+
function logInfo(msg: string) { console.log(chalk.blue('INFO') + ' ' + msg); }
|
|
112
|
+
|
|
113
|
+
// Formatted output
|
|
114
|
+
console.log(chalk.bold.underline('Deployment Summary'));
|
|
115
|
+
console.log(` Environment: ${chalk.cyan('production')}`);
|
|
116
|
+
console.log(` Version: ${chalk.green('v1.2.3')}`);
|
|
117
|
+
console.log(` Status: ${chalk.bgGreen.black(' DEPLOYED ')}`);
|
|
118
|
+
|
|
119
|
+
// Tables (without dependencies)
|
|
120
|
+
function printTable(headers: string[], rows: string[][]) {
|
|
121
|
+
const widths = headers.map((h, i) =>
|
|
122
|
+
Math.max(h.length, ...rows.map(r => r[i]?.length ?? 0))
|
|
123
|
+
);
|
|
124
|
+
const line = widths.map(w => '-'.repeat(w + 2)).join('+');
|
|
125
|
+
|
|
126
|
+
console.log(headers.map((h, i) => ` ${h.padEnd(widths[i])} `).join('|'));
|
|
127
|
+
console.log(line);
|
|
128
|
+
rows.forEach(row => {
|
|
129
|
+
console.log(row.map((c, i) => ` ${(c ?? '').padEnd(widths[i])} `).join('|'));
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### 4. Progress Indicators
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
import ora from 'ora';
|
|
138
|
+
import cliProgress from 'cli-progress';
|
|
139
|
+
|
|
140
|
+
// Spinner for indeterminate operations
|
|
141
|
+
async function withSpinner<T>(message: string, fn: () => Promise<T>): Promise<T> {
|
|
142
|
+
const spinner = ora(message).start();
|
|
143
|
+
try {
|
|
144
|
+
const result = await fn();
|
|
145
|
+
spinner.succeed();
|
|
146
|
+
return result;
|
|
147
|
+
} catch (err) {
|
|
148
|
+
spinner.fail();
|
|
149
|
+
throw err;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
await withSpinner('Deploying...', () => deploy());
|
|
154
|
+
|
|
155
|
+
// Progress bar for determinate operations
|
|
156
|
+
const bar = new cliProgress.SingleBar({
|
|
157
|
+
format: 'Uploading |{bar}| {percentage}% | {value}/{total} files',
|
|
158
|
+
barCompleteChar: '\u2588',
|
|
159
|
+
barIncompleteChar: '\u2591',
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
bar.start(files.length, 0);
|
|
163
|
+
for (const file of files) {
|
|
164
|
+
await upload(file);
|
|
165
|
+
bar.increment();
|
|
166
|
+
}
|
|
167
|
+
bar.stop();
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### 5. Configuration Management
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
import Conf from 'conf';
|
|
174
|
+
|
|
175
|
+
interface Config {
|
|
176
|
+
apiKey: string;
|
|
177
|
+
defaultEnvironment: 'staging' | 'production';
|
|
178
|
+
outputFormat: 'json' | 'table' | 'text';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const config = new Conf<Config>({
|
|
182
|
+
projectName: 'mycli',
|
|
183
|
+
schema: {
|
|
184
|
+
apiKey: { type: 'string', default: '' },
|
|
185
|
+
defaultEnvironment: { type: 'string', enum: ['staging', 'production'], default: 'staging' },
|
|
186
|
+
outputFormat: { type: 'string', enum: ['json', 'table', 'text'], default: 'table' },
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// Config subcommand
|
|
191
|
+
program
|
|
192
|
+
.command('config')
|
|
193
|
+
.description('Manage configuration')
|
|
194
|
+
.command('set')
|
|
195
|
+
.argument('<key>', 'Configuration key')
|
|
196
|
+
.argument('<value>', 'Configuration value')
|
|
197
|
+
.action((key, value) => {
|
|
198
|
+
config.set(key, value);
|
|
199
|
+
logSuccess(`Set ${key} = ${value}`);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
program
|
|
203
|
+
.command('config')
|
|
204
|
+
.command('get')
|
|
205
|
+
.argument('<key>', 'Configuration key')
|
|
206
|
+
.action((key) => {
|
|
207
|
+
const value = config.get(key);
|
|
208
|
+
console.log(value ?? chalk.dim('(not set)'));
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
program
|
|
212
|
+
.command('config')
|
|
213
|
+
.command('path')
|
|
214
|
+
.action(() => console.log(config.path));
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### 6. Error Handling and Exit Codes
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
// Wrap the top-level action to handle errors consistently
|
|
221
|
+
function handleErrors(fn: (...args: any[]) => Promise<void>) {
|
|
222
|
+
return async (...args: any[]) => {
|
|
223
|
+
try {
|
|
224
|
+
await fn(...args);
|
|
225
|
+
} catch (err) {
|
|
226
|
+
if (err instanceof UserError) {
|
|
227
|
+
logError(err.message);
|
|
228
|
+
if (err.hint) logInfo(`Hint: ${err.hint}`);
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
if (err instanceof NetworkError) {
|
|
232
|
+
logError(`Network error: ${err.message}`);
|
|
233
|
+
logInfo('Check your connection and try again');
|
|
234
|
+
process.exit(2);
|
|
235
|
+
}
|
|
236
|
+
// Unexpected error — show stack trace
|
|
237
|
+
console.error(err);
|
|
238
|
+
process.exit(99);
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
program
|
|
244
|
+
.command('deploy')
|
|
245
|
+
.action(handleErrors(async (env, opts) => {
|
|
246
|
+
await deploy(env, opts);
|
|
247
|
+
}));
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### 7. Packaging and Distribution
|
|
251
|
+
|
|
252
|
+
```json
|
|
253
|
+
{
|
|
254
|
+
"name": "mycli",
|
|
255
|
+
"version": "1.0.0",
|
|
256
|
+
"bin": { "mycli": "./dist/index.js" },
|
|
257
|
+
"type": "module",
|
|
258
|
+
"files": ["dist"],
|
|
259
|
+
"engines": { "node": ">=18" },
|
|
260
|
+
"scripts": {
|
|
261
|
+
"build": "tsup src/index.ts --format cjs --target node18 --clean",
|
|
262
|
+
"prepublishOnly": "npm run build"
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
For standalone binaries (no Node.js required):
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
# Using pkg or @vercel/ncc
|
|
271
|
+
npx pkg dist/index.js --targets node18-macos-arm64,node18-linux-x64,node18-win-x64
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Examples
|
|
275
|
+
|
|
276
|
+
| Tool | Purpose | Key Feature |
|
|
277
|
+
|------|---------|-------------|
|
|
278
|
+
| Commander | Argument parsing | Subcommands, options, validation |
|
|
279
|
+
| Inquirer | Interactive prompts | Select, checkbox, confirm |
|
|
280
|
+
| chalk | Styled terminal output | Colors, bold, underline |
|
|
281
|
+
| ora | Spinners | Indeterminate progress |
|
|
282
|
+
| Conf | Config persistence | JSON schema validation |
|
|
283
|
+
| tsup | Bundle for distribution | Single CJS file |
|
|
284
|
+
|
|
285
|
+
## Checklist
|
|
286
|
+
|
|
287
|
+
- [ ] Subcommands organized by domain (`deploy`, `config`, `status`)
|
|
288
|
+
- [ ] All commands support `--json` flag for scripting/piping
|
|
289
|
+
- [ ] Interactive prompts have non-interactive fallbacks via flags
|
|
290
|
+
- [ ] Help text is clear, with examples for complex commands
|
|
291
|
+
- [ ] Exit codes are consistent (0 = success, 1 = user error, 2 = network)
|
|
292
|
+
- [ ] Configuration stored with schema validation
|
|
293
|
+
- [ ] Errors show actionable hints, not raw stack traces
|
|
294
|
+
- [ ] Spinners/progress bars used for operations longer than 1 second
|
|
295
|
+
- [ ] `bin` field in package.json points to built output
|
|
296
|
+
- [ ] Shell completions generated with `program.completions()`
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: code-review
|
|
3
|
+
description: Code review patterns with review checklist, PR sizing, automated checks, constructive feedback, and security review.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Code Review
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
Apply to every pull request before merge. Use the structured checklist to catch bugs, security holes, and maintainability issues that automated tools miss. Also useful for self-review before opening a PR, and for training junior developers on what to look for.
|
|
10
|
+
|
|
11
|
+
## How It Works
|
|
12
|
+
|
|
13
|
+
### Start with the Big Picture
|
|
14
|
+
|
|
15
|
+
Before reading line-by-line, understand what the PR is doing:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
1. Read the PR title and description -- what problem does this solve?
|
|
19
|
+
2. Check the file list -- does the scope match the described change?
|
|
20
|
+
3. Look for unexpected files (config changes, new dependencies, migrations)
|
|
21
|
+
4. Verify the change has tests proportional to its risk
|
|
22
|
+
5. Check if the PR can be split into smaller, focused changes
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
If a PR touches 20+ files across unrelated domains, ask to split it. Large PRs get rubber-stamped; small PRs get real review.
|
|
26
|
+
|
|
27
|
+
### Security Review Checklist
|
|
28
|
+
|
|
29
|
+
Check every PR for these patterns, especially in routes and data handling:
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
// SQL injection -- use parameterized queries
|
|
33
|
+
// BAD: db.query(`SELECT * FROM users WHERE id = '${userId}'`)
|
|
34
|
+
// GOOD:
|
|
35
|
+
const user = await db.query("SELECT * FROM users WHERE id = $1", [userId]);
|
|
36
|
+
|
|
37
|
+
// XSS -- sanitize user content before rendering
|
|
38
|
+
// BAD: <div dangerouslySetInnerHTML={{ __html: userComment }} />
|
|
39
|
+
// GOOD:
|
|
40
|
+
import DOMPurify from "dompurify";
|
|
41
|
+
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userComment) }} />
|
|
42
|
+
|
|
43
|
+
// Auth bypass -- verify ownership, not just authentication
|
|
44
|
+
// BAD: const order = await db.orders.findById(req.params.id);
|
|
45
|
+
// GOOD:
|
|
46
|
+
const order = await db.orders.findOne({
|
|
47
|
+
id: req.params.id,
|
|
48
|
+
userId: req.user.id,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Mass assignment -- whitelist allowed fields
|
|
52
|
+
// BAD: await db.users.update(req.params.id, req.body);
|
|
53
|
+
// GOOD:
|
|
54
|
+
const { name, email } = req.body;
|
|
55
|
+
await db.users.update(req.params.id, { name, email });
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Performance Review
|
|
59
|
+
|
|
60
|
+
Look for patterns that will cause problems at scale:
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
// N+1 queries -- fetch related data in one query
|
|
64
|
+
// BAD:
|
|
65
|
+
for (const order of orders) {
|
|
66
|
+
order.user = await db.users.findById(order.userId);
|
|
67
|
+
}
|
|
68
|
+
// GOOD:
|
|
69
|
+
const userIds = [...new Set(orders.map((o) => o.userId))];
|
|
70
|
+
const users = await db.users.findByIds(userIds);
|
|
71
|
+
const userMap = new Map(users.map((u) => [u.id, u]));
|
|
72
|
+
orders.forEach((o) => (o.user = userMap.get(o.userId)));
|
|
73
|
+
|
|
74
|
+
// Unbounded queries -- always paginate
|
|
75
|
+
// BAD: const allUsers = await db.users.findAll();
|
|
76
|
+
// GOOD: const users = await db.users.findAll({ limit: 50, cursor });
|
|
77
|
+
|
|
78
|
+
// Memory leaks -- event listeners without cleanup
|
|
79
|
+
// BAD: useEffect(() => { window.addEventListener("resize", handler); }, []);
|
|
80
|
+
// GOOD:
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
window.addEventListener("resize", handler);
|
|
83
|
+
return () => window.removeEventListener("resize", handler);
|
|
84
|
+
}, []);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Readability Review
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
- Functions: does the name describe what it does, not how?
|
|
91
|
+
BAD: loopAndFilter() GOOD: getActiveUsers()
|
|
92
|
+
|
|
93
|
+
- Variables: would a new team member understand this?
|
|
94
|
+
BAD: const d = new Date() GOOD: const createdAt = new Date()
|
|
95
|
+
|
|
96
|
+
- Comments: explain WHY, not WHAT
|
|
97
|
+
BAD: // increment counter
|
|
98
|
+
GOOD: // Retry up to 3 times -- the payment gateway has transient failures
|
|
99
|
+
|
|
100
|
+
- Magic numbers: named constants, not inline literals
|
|
101
|
+
BAD: if (retries > 3) GOOD: if (retries > MAX_RETRIES)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Automated Checks as First Pass
|
|
105
|
+
|
|
106
|
+
Set up CI to catch mechanical issues before human review:
|
|
107
|
+
|
|
108
|
+
```yaml
|
|
109
|
+
# .github/workflows/pr-checks.yml
|
|
110
|
+
name: PR Checks
|
|
111
|
+
on: [pull_request]
|
|
112
|
+
jobs:
|
|
113
|
+
checks:
|
|
114
|
+
runs-on: ubuntu-latest
|
|
115
|
+
steps:
|
|
116
|
+
- uses: actions/checkout@v4
|
|
117
|
+
- run: npm ci
|
|
118
|
+
- run: npm run typecheck
|
|
119
|
+
- run: npm run lint
|
|
120
|
+
- run: npm test -- --coverage
|
|
121
|
+
- run: npx audit-ci --moderate
|
|
122
|
+
- uses: danger/danger-js@v1
|
|
123
|
+
env:
|
|
124
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
// dangerfile.ts -- automated PR feedback
|
|
129
|
+
import { danger, warn, fail } from "danger";
|
|
130
|
+
|
|
131
|
+
if (danger.github.pr.body.length < 10) {
|
|
132
|
+
fail("PR description is too short. Explain what and why.");
|
|
133
|
+
}
|
|
134
|
+
if (danger.github.pr.additions > 500) {
|
|
135
|
+
warn("Large PR. Consider splitting into smaller changes.");
|
|
136
|
+
}
|
|
137
|
+
const hasTests = danger.git.created_files.some((f) => f.includes(".test."));
|
|
138
|
+
if (!hasTests && danger.github.pr.additions > 50) {
|
|
139
|
+
warn("No test files added. Is this change covered by existing tests?");
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Constructive Feedback Patterns
|
|
144
|
+
|
|
145
|
+
```markdown
|
|
146
|
+
## Ask, don't demand
|
|
147
|
+
BAD: "This is wrong. Use a Map instead."
|
|
148
|
+
GOOD: "Have you considered using a Map here? It would give O(1)
|
|
149
|
+
lookup instead of the O(n) filter on line 42."
|
|
150
|
+
|
|
151
|
+
## Distinguish blocking from non-blocking
|
|
152
|
+
- "nit: rename `d` to `createdAt`" -> nice to have
|
|
153
|
+
- "issue: this query is missing LIMIT" -> must fix
|
|
154
|
+
- "question: what happens if `user` is null?" -> need clarification
|
|
155
|
+
|
|
156
|
+
## Offer alternatives with code
|
|
157
|
+
"This works, but a reduce() might be clearer:
|
|
158
|
+
```ts
|
|
159
|
+
const byId = items.reduce<Record<string, Item>>(
|
|
160
|
+
(acc, item) => ({ ...acc, [item.id]: item }),
|
|
161
|
+
{}
|
|
162
|
+
);
|
|
163
|
+
```"
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Examples
|
|
167
|
+
|
|
168
|
+
| What You Catch | How to Spot It | Impact |
|
|
169
|
+
|---------------|---------------|--------|
|
|
170
|
+
| SQL injection | String interpolation in queries | Data breach |
|
|
171
|
+
| N+1 query | Loop with await inside | 200ms -> 5s page load |
|
|
172
|
+
| Missing error handling | No try/catch on external calls | Unhandled crash |
|
|
173
|
+
| Auth bypass | findById without userId check | Users access others' data |
|
|
174
|
+
| Memory leak | Event listener without cleanup | App degrades over time |
|
|
175
|
+
| Large PR | 20+ files across domains | Missed bugs, rubber-stamped |
|
|
176
|
+
|
|
177
|
+
## Checklist
|
|
178
|
+
- [ ] PR description explains WHAT and WHY (not just HOW)
|
|
179
|
+
- [ ] Scope is focused -- one concern per PR, under 400 lines changed
|
|
180
|
+
- [ ] No hardcoded secrets, API keys, or credentials
|
|
181
|
+
- [ ] SQL queries use parameterized inputs
|
|
182
|
+
- [ ] User input is validated and sanitized
|
|
183
|
+
- [ ] Database queries have LIMIT and appropriate indexes
|
|
184
|
+
- [ ] Error cases handled, not swallowed silently
|
|
185
|
+
- [ ] No N+1 query patterns
|