@rune-kit/rune 2.10.0 → 2.12.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/LICENSE +21 -21
- package/README.md +65 -6
- package/commands/rune.md +168 -168
- package/compiler/__tests__/detect-invariants.test.js +136 -0
- package/compiler/__tests__/doctor-mesh.test.js +229 -0
- package/compiler/__tests__/hook-dispatch.test.js +91 -0
- package/compiler/__tests__/hooks-antigravity.test.js +118 -0
- package/compiler/__tests__/hooks-cursor.test.js +139 -0
- package/compiler/__tests__/hooks-install.test.js +305 -0
- package/compiler/__tests__/hooks-merge.test.js +204 -0
- package/compiler/__tests__/hooks-tiers.test.js +519 -0
- package/compiler/__tests__/hooks-windsurf.test.js +115 -0
- package/compiler/__tests__/inject-claude-md.test.js +152 -0
- package/compiler/__tests__/load-invariants.test.js +408 -0
- package/compiler/__tests__/onboard-invariants.test.js +240 -0
- package/compiler/adapters/hooks/antigravity.js +140 -0
- package/compiler/adapters/hooks/claude.js +166 -0
- package/compiler/adapters/hooks/cursor.js +191 -0
- package/compiler/adapters/hooks/index.js +82 -0
- package/compiler/adapters/hooks/tier-emitter.js +182 -0
- package/compiler/adapters/hooks/windsurf.js +202 -0
- package/compiler/bin/rune.js +196 -6
- package/compiler/commands/hook-dispatch.js +87 -0
- package/compiler/commands/hooks/install.js +120 -0
- package/compiler/commands/hooks/merge.js +211 -0
- package/compiler/commands/hooks/presets.js +116 -0
- package/compiler/commands/hooks/status.js +112 -0
- package/compiler/commands/hooks/tiers.js +221 -0
- package/compiler/commands/hooks/uninstall.js +94 -0
- package/compiler/doctor.js +236 -0
- package/contexts/dev.md +34 -34
- package/contexts/research.md +43 -43
- package/contexts/review.md +55 -55
- package/extensions/ai-ml/PACK.md +88 -88
- package/extensions/ai-ml/skills/ai-agents.md +172 -172
- package/extensions/ai-ml/skills/code-sandbox.md +187 -187
- package/extensions/ai-ml/skills/deep-research.md +146 -146
- package/extensions/ai-ml/skills/embedding-search.md +66 -66
- package/extensions/ai-ml/skills/fine-tuning-guide.md +74 -74
- package/extensions/ai-ml/skills/llm-architect.md +125 -125
- package/extensions/ai-ml/skills/llm-integration.md +64 -64
- package/extensions/ai-ml/skills/prompt-patterns.md +72 -72
- package/extensions/ai-ml/skills/rag-patterns.md +66 -66
- package/extensions/ai-ml/skills/web-extraction.md +114 -114
- package/extensions/analytics/PACK.md +92 -92
- package/extensions/analytics/skills/ab-testing.md +72 -72
- package/extensions/analytics/skills/dashboard-patterns.md +83 -83
- package/extensions/analytics/skills/data-validation.md +68 -68
- package/extensions/analytics/skills/funnel-analysis.md +81 -81
- package/extensions/analytics/skills/sql-patterns.md +57 -57
- package/extensions/analytics/skills/statistical-analysis.md +79 -79
- package/extensions/analytics/skills/tracking-setup.md +71 -71
- package/extensions/backend/PACK.md +104 -104
- package/extensions/backend/skills/api-patterns.md +84 -84
- package/extensions/backend/skills/async-pipeline.md +193 -193
- package/extensions/backend/skills/auth-patterns.md +97 -97
- package/extensions/backend/skills/background-jobs.md +133 -133
- package/extensions/backend/skills/caching-patterns.md +108 -108
- package/extensions/backend/skills/cli-generation.md +133 -133
- package/extensions/backend/skills/database-patterns.md +87 -87
- package/extensions/backend/skills/middleware-patterns.md +104 -104
- package/extensions/chrome-ext/PACK.md +93 -93
- package/extensions/chrome-ext/skills/cws-preflight.md +143 -143
- package/extensions/chrome-ext/skills/cws-publish.md +104 -104
- package/extensions/chrome-ext/skills/ext-ai-integration.md +251 -251
- package/extensions/chrome-ext/skills/ext-messaging.md +139 -139
- package/extensions/chrome-ext/skills/ext-storage.md +133 -133
- package/extensions/chrome-ext/skills/mv3-scaffold.md +164 -164
- package/extensions/content/PACK.md +96 -96
- package/extensions/content/skills/blog-patterns.md +88 -88
- package/extensions/content/skills/cms-integration.md +131 -131
- package/extensions/content/skills/content-scoring.md +107 -107
- package/extensions/content/skills/i18n.md +83 -83
- package/extensions/content/skills/mdx-authoring.md +137 -137
- package/extensions/content/skills/reference.md +1014 -1014
- package/extensions/content/skills/seo-patterns.md +67 -67
- package/extensions/content/skills/video-repurpose.md +153 -153
- package/extensions/devops/PACK.md +101 -101
- package/extensions/devops/skills/chaos-testing.md +67 -67
- package/extensions/devops/skills/ci-cd.md +75 -75
- package/extensions/devops/skills/docker.md +58 -58
- package/extensions/devops/skills/edge-serverless.md +163 -163
- package/extensions/devops/skills/infra-as-code.md +158 -158
- package/extensions/devops/skills/kubernetes.md +110 -110
- package/extensions/devops/skills/monitoring.md +57 -57
- package/extensions/devops/skills/server-setup.md +64 -64
- package/extensions/devops/skills/ssl-domain.md +42 -42
- package/extensions/ecommerce/PACK.md +116 -116
- package/extensions/ecommerce/skills/cart-system.md +79 -79
- package/extensions/ecommerce/skills/inventory-mgmt.md +102 -102
- package/extensions/ecommerce/skills/order-management.md +126 -126
- package/extensions/ecommerce/skills/payment-integration.md +472 -472
- package/extensions/ecommerce/skills/shopify-dev.md +69 -69
- package/extensions/ecommerce/skills/subscription-billing.md +93 -93
- package/extensions/ecommerce/skills/tax-compliance.md +117 -117
- package/extensions/gamedev/PACK.md +142 -142
- package/extensions/gamedev/skills/asset-pipeline.md +74 -74
- package/extensions/gamedev/skills/audio-system.md +129 -129
- package/extensions/gamedev/skills/camera-system.md +87 -87
- package/extensions/gamedev/skills/ecs.md +98 -98
- package/extensions/gamedev/skills/game-loops.md +72 -72
- package/extensions/gamedev/skills/input-system.md +199 -199
- package/extensions/gamedev/skills/multiplayer.md +180 -180
- package/extensions/gamedev/skills/particles.md +105 -105
- package/extensions/gamedev/skills/physics-engine.md +89 -89
- package/extensions/gamedev/skills/scene-management.md +146 -146
- package/extensions/gamedev/skills/threejs-patterns.md +90 -90
- package/extensions/gamedev/skills/webgl.md +71 -71
- package/extensions/mobile/PACK.md +106 -106
- package/extensions/mobile/skills/app-store-connect.md +152 -152
- package/extensions/mobile/skills/app-store-prep.md +66 -66
- package/extensions/mobile/skills/deep-linking.md +109 -109
- package/extensions/mobile/skills/flutter.md +60 -60
- package/extensions/mobile/skills/ios-build-pipeline.md +142 -142
- package/extensions/mobile/skills/native-bridge.md +66 -66
- package/extensions/mobile/skills/ota-updates.md +97 -97
- package/extensions/mobile/skills/push-notifications.md +111 -111
- package/extensions/mobile/skills/react-native.md +82 -82
- package/extensions/saas/PACK.md +116 -116
- package/extensions/saas/skills/billing-integration.md +200 -200
- package/extensions/saas/skills/feature-flags.md +130 -130
- package/extensions/saas/skills/multi-tenant.md +103 -103
- package/extensions/saas/skills/onboarding-flow.md +139 -139
- package/extensions/saas/skills/subscription-flow.md +95 -95
- package/extensions/saas/skills/team-management.md +144 -144
- package/extensions/security/PACK.md +99 -99
- package/extensions/security/skills/api-security.md +140 -140
- package/extensions/security/skills/compliance.md +68 -68
- package/extensions/security/skills/owasp-audit.md +64 -64
- package/extensions/security/skills/pentest-patterns.md +77 -77
- package/extensions/security/skills/secret-mgmt.md +65 -65
- package/extensions/security/skills/supply-chain.md +65 -65
- package/extensions/trading/PACK.md +80 -80
- package/extensions/trading/skills/chart-components.md +55 -55
- package/extensions/trading/skills/experiment-loop.md +125 -125
- package/extensions/trading/skills/fintech-patterns.md +47 -47
- package/extensions/trading/skills/indicator-library.md +58 -58
- package/extensions/trading/skills/quant-analysis.md +111 -111
- package/extensions/trading/skills/realtime-data.md +58 -58
- package/extensions/trading/skills/trade-logic.md +104 -104
- package/extensions/ui/PACK.md +130 -130
- package/extensions/ui/skills/a11y-audit.md +91 -91
- package/extensions/ui/skills/animation-patterns.md +127 -127
- package/extensions/ui/skills/component-patterns.md +100 -100
- package/extensions/ui/skills/design-decision.md +108 -108
- package/extensions/ui/skills/design-system.md +68 -68
- package/extensions/ui/skills/landing-patterns.md +155 -155
- package/extensions/ui/skills/palette-picker.md +173 -173
- package/extensions/ui/skills/react-health.md +90 -90
- package/extensions/ui/skills/type-system.md +125 -125
- package/extensions/ui/skills/web-vitals.md +153 -153
- package/extensions/zalo/PACK.md +145 -145
- package/extensions/zalo/skills/zalo-oa-mcp.md +317 -317
- package/extensions/zalo/skills/zalo-oa-messaging.md +429 -429
- package/extensions/zalo/skills/zalo-oa-setup.md +236 -236
- package/extensions/zalo/skills/zalo-oa-webhook.md +189 -189
- package/extensions/zalo/skills/zalo-personal-messaging.md +194 -194
- package/extensions/zalo/skills/zalo-personal-setup.md +153 -153
- package/extensions/zalo/skills/zalo-rate-guard.md +219 -219
- package/hooks/auto-format/index.cjs +48 -48
- package/hooks/hooks.json +111 -111
- package/hooks/post-session-reflect/index.cjs +189 -189
- package/hooks/pre-compact/index.cjs +95 -95
- package/hooks/run-hook.cmd +1 -1
- package/hooks/secrets-scan/index.cjs +100 -100
- package/hooks/session-start/index.cjs +71 -71
- package/hooks/typecheck/index.cjs +65 -65
- package/package.json +63 -63
- package/references/ui-pro-max-data/LICENSE-UI-PRO-MAX +21 -21
- package/references/ui-pro-max-data/charts.csv +26 -26
- package/references/ui-pro-max-data/colors.csv +161 -161
- package/references/ui-pro-max-data/styles.csv +68 -68
- package/references/ui-pro-max-data/typography.csv +74 -74
- package/references/ui-pro-max-data/ui-reasoning.csv +162 -162
- package/references/ui-pro-max-data/ux-guidelines.csv +99 -99
- package/skills/adversary/SKILL.md +283 -283
- package/skills/asset-creator/SKILL.md +157 -157
- package/skills/audit/SKILL.md +147 -2
- package/skills/autopsy/SKILL.md +335 -335
- package/skills/ba/SKILL.md +85 -1
- package/skills/brainstorm/SKILL.md +380 -342
- package/skills/browser-pilot/SKILL.md +169 -168
- package/skills/constraint-check/SKILL.md +165 -165
- package/skills/context-engine/SKILL.md +408 -404
- package/skills/cook/SKILL.md +917 -863
- package/skills/db/SKILL.md +273 -273
- package/skills/debug/SKILL.md +465 -465
- package/skills/dependency-doctor/SKILL.md +265 -235
- package/skills/deploy/SKILL.md +274 -231
- package/skills/design/DESIGN-REFERENCE.md +365 -365
- package/skills/design/SKILL.md +590 -589
- package/skills/doc-processor/SKILL.md +254 -254
- package/skills/docs/SKILL.md +374 -374
- package/skills/docs-seeker/SKILL.md +178 -177
- package/skills/fix/SKILL.md +332 -330
- package/skills/git/SKILL.md +339 -339
- package/skills/hallucination-guard/SKILL.md +220 -219
- package/skills/incident/SKILL.md +254 -253
- package/skills/integrity-check/SKILL.md +169 -169
- package/skills/journal/SKILL.md +241 -240
- package/skills/launch/SKILL.md +344 -344
- package/skills/logic-guardian/SKILL.md +269 -251
- package/skills/marketing/SKILL.md +351 -289
- package/skills/mcp-builder/SKILL.md +425 -425
- package/skills/neural-memory/SKILL.md +359 -362
- package/skills/onboard/SKILL.md +432 -403
- package/skills/onboard/references/invariants-template.md +76 -0
- package/skills/onboard/scripts/detect-invariants.js +439 -0
- package/skills/onboard/scripts/inject-claude-md.js +150 -0
- package/skills/onboard/scripts/onboard-invariants.js +194 -0
- package/skills/perf/SKILL.md +347 -346
- package/skills/plan/SKILL.md +435 -428
- package/skills/preflight/SKILL.md +415 -415
- package/skills/problem-solver/SKILL.md +380 -284
- package/skills/rescue/SKILL.md +474 -474
- package/skills/research/SKILL.md +4 -0
- package/skills/retro/SKILL.md +3 -1
- package/skills/review/SKILL.md +614 -588
- package/skills/review-intake/SKILL.md +249 -249
- package/skills/safeguard/SKILL.md +200 -200
- package/skills/sast/SKILL.md +190 -190
- package/skills/scaffold/SKILL.md +328 -287
- package/skills/scope-guard/SKILL.md +183 -180
- package/skills/scout/SKILL.md +269 -263
- package/skills/sentinel/SKILL.md +384 -381
- package/skills/sentinel-env/SKILL.md +254 -254
- package/skills/sequential-thinking/SKILL.md +234 -234
- package/skills/session-bridge/SKILL.md +595 -543
- package/skills/session-bridge/scripts/load-invariants.js +397 -0
- package/skills/skill-forge/SKILL.md +581 -581
- package/skills/skill-router/SKILL.md +3 -0
- package/skills/slides/SKILL.md +19 -0
- package/skills/surgeon/SKILL.md +215 -215
- package/skills/team/SKILL.md +557 -537
- package/skills/test/SKILL.md +620 -614
- package/skills/trend-scout/SKILL.md +145 -145
- package/skills/verification/SKILL.md +334 -326
- package/skills/video-creator/SKILL.md +201 -201
- package/skills/watchdog/SKILL.md +168 -168
- package/skills/worktree/SKILL.md +140 -140
|
@@ -1,133 +1,133 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: "cli-generation"
|
|
3
|
-
pack: "@rune/backend"
|
|
4
|
-
description: "Generate production-grade CLI wrappers for backend services — command groups, dual output mode (human + JSON), stateful REPL, session management with undo/redo, and pip/npm-installable packaging."
|
|
5
|
-
model: sonnet
|
|
6
|
-
tools: [Read, Edit, Write, Grep, Glob, Bash]
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# cli-generation
|
|
10
|
-
|
|
11
|
-
Generate production-grade CLI wrappers for backend services — command groups, dual output mode (human + JSON), stateful REPL, session management with undo/redo, and pip/npm-installable packaging.
|
|
12
|
-
|
|
13
|
-
#### Workflow
|
|
14
|
-
|
|
15
|
-
**Step 1 — Analyze backend service surface**
|
|
16
|
-
Map existing API endpoints, service methods, or data models to CLI command groups:
|
|
17
|
-
```typescript
|
|
18
|
-
interface CLICommandGroup {
|
|
19
|
-
name: string; // e.g., 'users', 'orders', 'config'
|
|
20
|
-
source: string; // API route file or service class
|
|
21
|
-
commands: CLICommand[];
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
interface CLICommand {
|
|
25
|
-
name: string; // e.g., 'list', 'create', 'delete'
|
|
26
|
-
sourceMethod: string; // e.g., 'UserService.findAll'
|
|
27
|
-
params: CLIParam[];
|
|
28
|
-
mutating: boolean; // true = needs confirmation/undo support
|
|
29
|
-
}
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
**Step 2 — Design dual output mode**
|
|
33
|
-
Every command MUST support both human-readable and machine-readable output:
|
|
34
|
-
```typescript
|
|
35
|
-
// Human mode (default): tables, colors, formatted text
|
|
36
|
-
function formatHuman(data: any, format: 'table' | 'list' | 'detail'): string {
|
|
37
|
-
if (format === 'table') return formatTable(data, { borders: true, colors: true });
|
|
38
|
-
if (format === 'list') return data.map((d: any) => ` • ${d.name}`).join('\n');
|
|
39
|
-
return JSON.stringify(data, null, 2);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// JSON mode (--json flag): structured output for piping/scripting
|
|
43
|
-
function formatJSON(data: any): string {
|
|
44
|
-
return JSON.stringify(data, null, 2);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Error output follows same dual pattern
|
|
48
|
-
function formatError(error: Error, jsonMode: boolean): string {
|
|
49
|
-
if (jsonMode) return JSON.stringify({ error: error.message, type: error.constructor.name });
|
|
50
|
-
return chalk.red(`Error: ${error.message}`);
|
|
51
|
-
}
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
**Step 3 — Implement session with undo/redo**
|
|
55
|
-
For mutating operations, maintain session state:
|
|
56
|
-
```typescript
|
|
57
|
-
interface CLISession {
|
|
58
|
-
id: string;
|
|
59
|
-
history: SessionSnapshot[];
|
|
60
|
-
undoStack: SessionSnapshot[]; // max 50
|
|
61
|
-
redoStack: SessionSnapshot[];
|
|
62
|
-
modified: boolean;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function snapshot(session: CLISession, action: string): CLISession {
|
|
66
|
-
return {
|
|
67
|
-
...session,
|
|
68
|
-
undoStack: [...session.undoStack.slice(-49), { action, state: deepCopy(session) }],
|
|
69
|
-
redoStack: [], // new action clears redo
|
|
70
|
-
modified: true,
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
**Step 4 — Build REPL mode**
|
|
76
|
-
CLI enters REPL when invoked without subcommand:
|
|
77
|
-
```typescript
|
|
78
|
-
// Click (Python) — invoke_without_command=True enters REPL
|
|
79
|
-
@click.group(invoke_without_command=True)
|
|
80
|
-
@click.pass_context
|
|
81
|
-
def cli(ctx):
|
|
82
|
-
if ctx.invoked_subcommand is None:
|
|
83
|
-
start_repl(ctx)
|
|
84
|
-
|
|
85
|
-
// Commander (Node.js) — detect no args
|
|
86
|
-
if (process.argv.length <= 2) {
|
|
87
|
-
startREPL({ history: '~/.myapp_history', prompt: 'myapp> ' });
|
|
88
|
-
}
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
REPL features: command history (file-persisted), auto-suggest from history, tab completion, colored prompt, help command, status bar showing connection state.
|
|
92
|
-
|
|
93
|
-
**Step 5 — Package for distribution**
|
|
94
|
-
```bash
|
|
95
|
-
# Python: PEP 420 namespace packages for independent installability
|
|
96
|
-
# pyproject.toml or setup.py
|
|
97
|
-
entry_points = {
|
|
98
|
-
'console_scripts': ['myapp = myapp.cli:main'],
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
# Node.js: bin field in package.json
|
|
102
|
-
{
|
|
103
|
-
"bin": { "myapp": "./bin/cli.js" },
|
|
104
|
-
"files": ["bin/", "lib/"]
|
|
105
|
-
}
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
**Step 6 — Verify installation**
|
|
109
|
-
After packaging: install locally (`pip install -e .` or `npm link`), verify binary on PATH (`which myapp`), run `myapp --version`, test `myapp --json` mode, and verify REPL launch.
|
|
110
|
-
|
|
111
|
-
#### Example
|
|
112
|
-
|
|
113
|
-
```python
|
|
114
|
-
# Generated CLI structure for a backend service
|
|
115
|
-
# myapp/
|
|
116
|
-
# ├── cli.py ← Click entry point + REPL
|
|
117
|
-
# ├── commands/
|
|
118
|
-
# │ ├── users.py ← User CRUD commands
|
|
119
|
-
# │ ├── orders.py ← Order management
|
|
120
|
-
# │ └── config.py ← Config operations
|
|
121
|
-
# ├── core/
|
|
122
|
-
# │ ├── session.py ← Session + undo/redo
|
|
123
|
-
# │ └── client.py ← API client wrapper
|
|
124
|
-
# └── utils/
|
|
125
|
-
# ├── output.py ← Dual output (human + JSON)
|
|
126
|
-
# └── repl.py ← REPL with prompt-toolkit
|
|
127
|
-
|
|
128
|
-
# Usage:
|
|
129
|
-
# myapp users list → human-readable table
|
|
130
|
-
# myapp users list --json → JSON output for piping
|
|
131
|
-
# myapp users create --name "Alice" → creates user, snapshots for undo
|
|
132
|
-
# myapp → enters REPL mode
|
|
133
|
-
```
|
|
1
|
+
---
|
|
2
|
+
name: "cli-generation"
|
|
3
|
+
pack: "@rune/backend"
|
|
4
|
+
description: "Generate production-grade CLI wrappers for backend services — command groups, dual output mode (human + JSON), stateful REPL, session management with undo/redo, and pip/npm-installable packaging."
|
|
5
|
+
model: sonnet
|
|
6
|
+
tools: [Read, Edit, Write, Grep, Glob, Bash]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# cli-generation
|
|
10
|
+
|
|
11
|
+
Generate production-grade CLI wrappers for backend services — command groups, dual output mode (human + JSON), stateful REPL, session management with undo/redo, and pip/npm-installable packaging.
|
|
12
|
+
|
|
13
|
+
#### Workflow
|
|
14
|
+
|
|
15
|
+
**Step 1 — Analyze backend service surface**
|
|
16
|
+
Map existing API endpoints, service methods, or data models to CLI command groups:
|
|
17
|
+
```typescript
|
|
18
|
+
interface CLICommandGroup {
|
|
19
|
+
name: string; // e.g., 'users', 'orders', 'config'
|
|
20
|
+
source: string; // API route file or service class
|
|
21
|
+
commands: CLICommand[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface CLICommand {
|
|
25
|
+
name: string; // e.g., 'list', 'create', 'delete'
|
|
26
|
+
sourceMethod: string; // e.g., 'UserService.findAll'
|
|
27
|
+
params: CLIParam[];
|
|
28
|
+
mutating: boolean; // true = needs confirmation/undo support
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**Step 2 — Design dual output mode**
|
|
33
|
+
Every command MUST support both human-readable and machine-readable output:
|
|
34
|
+
```typescript
|
|
35
|
+
// Human mode (default): tables, colors, formatted text
|
|
36
|
+
function formatHuman(data: any, format: 'table' | 'list' | 'detail'): string {
|
|
37
|
+
if (format === 'table') return formatTable(data, { borders: true, colors: true });
|
|
38
|
+
if (format === 'list') return data.map((d: any) => ` • ${d.name}`).join('\n');
|
|
39
|
+
return JSON.stringify(data, null, 2);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// JSON mode (--json flag): structured output for piping/scripting
|
|
43
|
+
function formatJSON(data: any): string {
|
|
44
|
+
return JSON.stringify(data, null, 2);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Error output follows same dual pattern
|
|
48
|
+
function formatError(error: Error, jsonMode: boolean): string {
|
|
49
|
+
if (jsonMode) return JSON.stringify({ error: error.message, type: error.constructor.name });
|
|
50
|
+
return chalk.red(`Error: ${error.message}`);
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Step 3 — Implement session with undo/redo**
|
|
55
|
+
For mutating operations, maintain session state:
|
|
56
|
+
```typescript
|
|
57
|
+
interface CLISession {
|
|
58
|
+
id: string;
|
|
59
|
+
history: SessionSnapshot[];
|
|
60
|
+
undoStack: SessionSnapshot[]; // max 50
|
|
61
|
+
redoStack: SessionSnapshot[];
|
|
62
|
+
modified: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function snapshot(session: CLISession, action: string): CLISession {
|
|
66
|
+
return {
|
|
67
|
+
...session,
|
|
68
|
+
undoStack: [...session.undoStack.slice(-49), { action, state: deepCopy(session) }],
|
|
69
|
+
redoStack: [], // new action clears redo
|
|
70
|
+
modified: true,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Step 4 — Build REPL mode**
|
|
76
|
+
CLI enters REPL when invoked without subcommand:
|
|
77
|
+
```typescript
|
|
78
|
+
// Click (Python) — invoke_without_command=True enters REPL
|
|
79
|
+
@click.group(invoke_without_command=True)
|
|
80
|
+
@click.pass_context
|
|
81
|
+
def cli(ctx):
|
|
82
|
+
if ctx.invoked_subcommand is None:
|
|
83
|
+
start_repl(ctx)
|
|
84
|
+
|
|
85
|
+
// Commander (Node.js) — detect no args
|
|
86
|
+
if (process.argv.length <= 2) {
|
|
87
|
+
startREPL({ history: '~/.myapp_history', prompt: 'myapp> ' });
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
REPL features: command history (file-persisted), auto-suggest from history, tab completion, colored prompt, help command, status bar showing connection state.
|
|
92
|
+
|
|
93
|
+
**Step 5 — Package for distribution**
|
|
94
|
+
```bash
|
|
95
|
+
# Python: PEP 420 namespace packages for independent installability
|
|
96
|
+
# pyproject.toml or setup.py
|
|
97
|
+
entry_points = {
|
|
98
|
+
'console_scripts': ['myapp = myapp.cli:main'],
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# Node.js: bin field in package.json
|
|
102
|
+
{
|
|
103
|
+
"bin": { "myapp": "./bin/cli.js" },
|
|
104
|
+
"files": ["bin/", "lib/"]
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Step 6 — Verify installation**
|
|
109
|
+
After packaging: install locally (`pip install -e .` or `npm link`), verify binary on PATH (`which myapp`), run `myapp --version`, test `myapp --json` mode, and verify REPL launch.
|
|
110
|
+
|
|
111
|
+
#### Example
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
# Generated CLI structure for a backend service
|
|
115
|
+
# myapp/
|
|
116
|
+
# ├── cli.py ← Click entry point + REPL
|
|
117
|
+
# ├── commands/
|
|
118
|
+
# │ ├── users.py ← User CRUD commands
|
|
119
|
+
# │ ├── orders.py ← Order management
|
|
120
|
+
# │ └── config.py ← Config operations
|
|
121
|
+
# ├── core/
|
|
122
|
+
# │ ├── session.py ← Session + undo/redo
|
|
123
|
+
# │ └── client.py ← API client wrapper
|
|
124
|
+
# └── utils/
|
|
125
|
+
# ├── output.py ← Dual output (human + JSON)
|
|
126
|
+
# └── repl.py ← REPL with prompt-toolkit
|
|
127
|
+
|
|
128
|
+
# Usage:
|
|
129
|
+
# myapp users list → human-readable table
|
|
130
|
+
# myapp users list --json → JSON output for piping
|
|
131
|
+
# myapp users create --name "Alice" → creates user, snapshots for undo
|
|
132
|
+
# myapp → enters REPL mode
|
|
133
|
+
```
|
|
@@ -1,87 +1,87 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: "database-patterns"
|
|
3
|
-
pack: "@rune/backend"
|
|
4
|
-
description: "Database design and query patterns — schema design, migrations, indexing strategies, N+1 prevention, soft deletes, read replicas, connection pooling, seeding."
|
|
5
|
-
model: sonnet
|
|
6
|
-
tools: [Read, Edit, Write, Grep, Glob, Bash]
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# database-patterns
|
|
10
|
-
|
|
11
|
-
Database design and query patterns — schema design, migrations, indexing strategies, N+1 prevention, soft deletes, read replicas, connection pooling, seeding.
|
|
12
|
-
|
|
13
|
-
#### Workflow
|
|
14
|
-
|
|
15
|
-
**Step 1 — Detect ORM and query patterns**
|
|
16
|
-
Use Grep to find ORM usage (`prisma.`, `knex(`, `sequelize.`, `typeorm`, `drizzle`, `mongoose.`, `db.query`) and raw SQL strings. Read schema files (`schema.prisma`, `migrations/`, `models/`) to understand the data model.
|
|
17
|
-
|
|
18
|
-
**Step 2 — Detect N+1 and missing indexes**
|
|
19
|
-
Scan for loops containing database calls (a query inside `for`, `map`, `forEach` → N+1). Check foreign key columns for missing indexes. Identify queries with `WHERE` clauses on unindexed columns. Flag each with the specific query and fix.
|
|
20
|
-
|
|
21
|
-
**Step 3 — Emit optimized queries**
|
|
22
|
-
For N+1: emit eager loading (`include`, `populate`, `JOIN`). For missing indexes: emit migration files. For unsafe raw SQL: emit parameterized version. For connection pooling: check pool config and recommend sizing based on max connections.
|
|
23
|
-
|
|
24
|
-
**Step 4 — Soft delete and query scoping**
|
|
25
|
-
Emit soft delete pattern: add `deleted_at TIMESTAMPTZ` column, update all `findMany`/`findUnique` calls to include `WHERE deleted_at IS NULL`. Cascade consideration: soft-delete parent should soft-delete children (emit trigger or application-level cascade). For Prisma: emit a custom extension that injects the filter automatically. Warn about index bloat from soft-deleted rows — add partial index `WHERE deleted_at IS NULL` to keep index lean.
|
|
26
|
-
|
|
27
|
-
**Step 5 — Read replicas, connection pooling, and seeding**
|
|
28
|
-
Read replicas: emit query routing — writes to primary, reads to replica. Handle replication lag: do not read from replica immediately after write in the same request (use primary for the read-after-write). For Prisma: emit `$extends` with read/write client split. Connection pooling deep dive: PgBouncer in transaction mode for serverless (each query gets a connection); Prisma's built-in pool for long-running servers. Pool sizing formula: `connections = (core_count * 2) + effective_spindle_count`. Seeding: emit factory functions using `@faker-js/faker` — deterministic seeds via `faker.seed(42)` for reproducible test data.
|
|
29
|
-
|
|
30
|
-
#### Example
|
|
31
|
-
|
|
32
|
-
```typescript
|
|
33
|
-
// BEFORE: N+1 — one query per post to get author
|
|
34
|
-
const posts = await prisma.post.findMany();
|
|
35
|
-
for (const post of posts) {
|
|
36
|
-
post.author = await prisma.user.findUnique({ where: { id: post.authorId } });
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// AFTER: eager loading, single query with JOIN
|
|
40
|
-
const posts = await prisma.post.findMany({
|
|
41
|
-
include: { author: { select: { id: true, name: true, avatar: true } } },
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
// Migration: missing indexes + soft delete column
|
|
45
|
-
-- Migration: add_indexes_and_soft_delete_to_posts
|
|
46
|
-
ALTER TABLE posts ADD COLUMN deleted_at TIMESTAMPTZ;
|
|
47
|
-
CREATE INDEX idx_posts_author_id ON posts(author_id);
|
|
48
|
-
CREATE INDEX idx_posts_created_at ON posts(created_at DESC);
|
|
49
|
-
CREATE INDEX idx_posts_active ON posts(author_id, created_at DESC) WHERE deleted_at IS NULL;
|
|
50
|
-
|
|
51
|
-
// Prisma soft delete extension (auto-scopes all queries)
|
|
52
|
-
const softDelete = Prisma.defineExtension({
|
|
53
|
-
name: 'softDelete',
|
|
54
|
-
query: {
|
|
55
|
-
$allModels: {
|
|
56
|
-
async findMany({ model, operation, args, query }) {
|
|
57
|
-
args.where = { ...args.where, deletedAt: null };
|
|
58
|
-
return query(args);
|
|
59
|
-
},
|
|
60
|
-
async delete({ model, args, query }) {
|
|
61
|
-
return (query as any)({ ...args, data: { deletedAt: new Date() } } as any);
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
});
|
|
66
|
-
const prisma = new PrismaClient().$extends(softDelete);
|
|
67
|
-
|
|
68
|
-
// Read replica routing with Prisma
|
|
69
|
-
const primaryClient = new PrismaClient({ datasources: { db: { url: PRIMARY_URL } } });
|
|
70
|
-
const replicaClient = new PrismaClient({ datasources: { db: { url: REPLICA_URL } } });
|
|
71
|
-
const db = { write: primaryClient, read: replicaClient };
|
|
72
|
-
// Usage: db.write.user.create(...) vs db.read.user.findMany(...)
|
|
73
|
-
|
|
74
|
-
// Factory seeding
|
|
75
|
-
import { faker } from '@faker-js/faker';
|
|
76
|
-
faker.seed(42); // reproducible
|
|
77
|
-
|
|
78
|
-
const createUserFactory = (overrides = {}) => ({
|
|
79
|
-
id: faker.string.uuid(),
|
|
80
|
-
email: faker.internet.email(),
|
|
81
|
-
name: faker.person.fullName(),
|
|
82
|
-
createdAt: faker.date.past(),
|
|
83
|
-
...overrides,
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
await prisma.user.createMany({ data: Array.from({ length: 50 }, () => createUserFactory()) });
|
|
87
|
-
```
|
|
1
|
+
---
|
|
2
|
+
name: "database-patterns"
|
|
3
|
+
pack: "@rune/backend"
|
|
4
|
+
description: "Database design and query patterns — schema design, migrations, indexing strategies, N+1 prevention, soft deletes, read replicas, connection pooling, seeding."
|
|
5
|
+
model: sonnet
|
|
6
|
+
tools: [Read, Edit, Write, Grep, Glob, Bash]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# database-patterns
|
|
10
|
+
|
|
11
|
+
Database design and query patterns — schema design, migrations, indexing strategies, N+1 prevention, soft deletes, read replicas, connection pooling, seeding.
|
|
12
|
+
|
|
13
|
+
#### Workflow
|
|
14
|
+
|
|
15
|
+
**Step 1 — Detect ORM and query patterns**
|
|
16
|
+
Use Grep to find ORM usage (`prisma.`, `knex(`, `sequelize.`, `typeorm`, `drizzle`, `mongoose.`, `db.query`) and raw SQL strings. Read schema files (`schema.prisma`, `migrations/`, `models/`) to understand the data model.
|
|
17
|
+
|
|
18
|
+
**Step 2 — Detect N+1 and missing indexes**
|
|
19
|
+
Scan for loops containing database calls (a query inside `for`, `map`, `forEach` → N+1). Check foreign key columns for missing indexes. Identify queries with `WHERE` clauses on unindexed columns. Flag each with the specific query and fix.
|
|
20
|
+
|
|
21
|
+
**Step 3 — Emit optimized queries**
|
|
22
|
+
For N+1: emit eager loading (`include`, `populate`, `JOIN`). For missing indexes: emit migration files. For unsafe raw SQL: emit parameterized version. For connection pooling: check pool config and recommend sizing based on max connections.
|
|
23
|
+
|
|
24
|
+
**Step 4 — Soft delete and query scoping**
|
|
25
|
+
Emit soft delete pattern: add `deleted_at TIMESTAMPTZ` column, update all `findMany`/`findUnique` calls to include `WHERE deleted_at IS NULL`. Cascade consideration: soft-delete parent should soft-delete children (emit trigger or application-level cascade). For Prisma: emit a custom extension that injects the filter automatically. Warn about index bloat from soft-deleted rows — add partial index `WHERE deleted_at IS NULL` to keep index lean.
|
|
26
|
+
|
|
27
|
+
**Step 5 — Read replicas, connection pooling, and seeding**
|
|
28
|
+
Read replicas: emit query routing — writes to primary, reads to replica. Handle replication lag: do not read from replica immediately after write in the same request (use primary for the read-after-write). For Prisma: emit `$extends` with read/write client split. Connection pooling deep dive: PgBouncer in transaction mode for serverless (each query gets a connection); Prisma's built-in pool for long-running servers. Pool sizing formula: `connections = (core_count * 2) + effective_spindle_count`. Seeding: emit factory functions using `@faker-js/faker` — deterministic seeds via `faker.seed(42)` for reproducible test data.
|
|
29
|
+
|
|
30
|
+
#### Example
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
// BEFORE: N+1 — one query per post to get author
|
|
34
|
+
const posts = await prisma.post.findMany();
|
|
35
|
+
for (const post of posts) {
|
|
36
|
+
post.author = await prisma.user.findUnique({ where: { id: post.authorId } });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// AFTER: eager loading, single query with JOIN
|
|
40
|
+
const posts = await prisma.post.findMany({
|
|
41
|
+
include: { author: { select: { id: true, name: true, avatar: true } } },
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Migration: missing indexes + soft delete column
|
|
45
|
+
-- Migration: add_indexes_and_soft_delete_to_posts
|
|
46
|
+
ALTER TABLE posts ADD COLUMN deleted_at TIMESTAMPTZ;
|
|
47
|
+
CREATE INDEX idx_posts_author_id ON posts(author_id);
|
|
48
|
+
CREATE INDEX idx_posts_created_at ON posts(created_at DESC);
|
|
49
|
+
CREATE INDEX idx_posts_active ON posts(author_id, created_at DESC) WHERE deleted_at IS NULL;
|
|
50
|
+
|
|
51
|
+
// Prisma soft delete extension (auto-scopes all queries)
|
|
52
|
+
const softDelete = Prisma.defineExtension({
|
|
53
|
+
name: 'softDelete',
|
|
54
|
+
query: {
|
|
55
|
+
$allModels: {
|
|
56
|
+
async findMany({ model, operation, args, query }) {
|
|
57
|
+
args.where = { ...args.where, deletedAt: null };
|
|
58
|
+
return query(args);
|
|
59
|
+
},
|
|
60
|
+
async delete({ model, args, query }) {
|
|
61
|
+
return (query as any)({ ...args, data: { deletedAt: new Date() } } as any);
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
const prisma = new PrismaClient().$extends(softDelete);
|
|
67
|
+
|
|
68
|
+
// Read replica routing with Prisma
|
|
69
|
+
const primaryClient = new PrismaClient({ datasources: { db: { url: PRIMARY_URL } } });
|
|
70
|
+
const replicaClient = new PrismaClient({ datasources: { db: { url: REPLICA_URL } } });
|
|
71
|
+
const db = { write: primaryClient, read: replicaClient };
|
|
72
|
+
// Usage: db.write.user.create(...) vs db.read.user.findMany(...)
|
|
73
|
+
|
|
74
|
+
// Factory seeding
|
|
75
|
+
import { faker } from '@faker-js/faker';
|
|
76
|
+
faker.seed(42); // reproducible
|
|
77
|
+
|
|
78
|
+
const createUserFactory = (overrides = {}) => ({
|
|
79
|
+
id: faker.string.uuid(),
|
|
80
|
+
email: faker.internet.email(),
|
|
81
|
+
name: faker.person.fullName(),
|
|
82
|
+
createdAt: faker.date.past(),
|
|
83
|
+
...overrides,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
await prisma.user.createMany({ data: Array.from({ length: 50 }, () => createUserFactory()) });
|
|
87
|
+
```
|
|
@@ -1,104 +1,104 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: "middleware-patterns"
|
|
3
|
-
pack: "@rune/backend"
|
|
4
|
-
description: "Middleware architecture — request validation, error handling, logging, CORS, compression, graceful shutdown, health checks, request ID tracking."
|
|
5
|
-
model: sonnet
|
|
6
|
-
tools: [Read, Edit, Write, Grep, Glob, Bash]
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# middleware-patterns
|
|
10
|
-
|
|
11
|
-
Middleware architecture — request validation, error handling, logging, CORS, compression, graceful shutdown, health checks, request ID tracking.
|
|
12
|
-
|
|
13
|
-
#### Workflow
|
|
14
|
-
|
|
15
|
-
**Step 1 — Audit middleware stack**
|
|
16
|
-
Read the main server file (app.ts, server.ts, index.ts) to inventory all middleware in registration order. Check for: missing request ID generation, missing structured logging, inconsistent error responses, missing input validation, CORS misconfiguration (`*` in production).
|
|
17
|
-
|
|
18
|
-
**Step 2 — Detect error handling gaps**
|
|
19
|
-
Use Grep to find `catch` blocks, error middleware signatures (`err, req, res, next`), and unhandled promise rejections. Check if errors return consistent format (same envelope for 400, 401, 403, 404, 500). Flag any that leak stack traces or internal details in production.
|
|
20
|
-
|
|
21
|
-
**Step 3 — Emit middleware improvements**
|
|
22
|
-
For each gap, emit the middleware function: request ID (`X-Request-Id` header, UUID per request), structured JSON logger (request method, path, status, duration, request ID), global error handler with consistent envelope, Zod-based request validation middleware.
|
|
23
|
-
|
|
24
|
-
**Step 4 — Compression strategy**
|
|
25
|
-
Emit response compression middleware. Use `brotli` for static assets and pre-compressible responses (better ratio than gzip, supported by all modern clients). Use `gzip` as fallback for older clients. Conditional compression: skip for already-compressed content types (`image/*`, `video/*`, `application/zip`) — compressing these wastes CPU. In Express: use `compression` package with a `filter` function. In Fastify: `@fastify/compress` with `encodings: ['br', 'gzip']`. Minimum size threshold: do not compress responses < 1KB (overhead exceeds benefit).
|
|
26
|
-
|
|
27
|
-
**Step 5 — Graceful shutdown and health checks**
|
|
28
|
-
Graceful shutdown: on `SIGTERM`/`SIGINT`, stop accepting new connections, wait for in-flight requests to complete (timeout 30s), then close DB pools and exit. Emit the shutdown handler for Express (`server.close()`), Fastify (`fastify.close()`), and worker processes. Health check endpoints: `/health/live` (liveness — is the process alive? return 200 always unless process is broken), `/health/ready` (readiness — can it serve traffic? check DB connection, Redis connection, return 503 if dependencies are down). In Kubernetes: map liveness to `livenessProbe`, readiness to `readinessProbe`. Do NOT check external third-party APIs in readiness — only your own dependencies.
|
|
29
|
-
|
|
30
|
-
#### Example
|
|
31
|
-
|
|
32
|
-
```typescript
|
|
33
|
-
// Request ID middleware
|
|
34
|
-
const requestId = (req, res, next) => {
|
|
35
|
-
req.id = req.headers['x-request-id'] || crypto.randomUUID();
|
|
36
|
-
res.setHeader('X-Request-Id', req.id);
|
|
37
|
-
next();
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
// Structured error handler — consistent envelope, no stack leak
|
|
41
|
-
const errorHandler = (err, req, res, _next) => {
|
|
42
|
-
const status = err.status || 500;
|
|
43
|
-
const message = status < 500 ? err.message : 'Internal server error';
|
|
44
|
-
logger.error({ err, requestId: req.id, path: req.path });
|
|
45
|
-
res.status(status).json({
|
|
46
|
-
error: { code: err.code || 'INTERNAL_ERROR', message },
|
|
47
|
-
request_id: req.id,
|
|
48
|
-
});
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
// Zod validation middleware
|
|
52
|
-
const validate = (schema: z.ZodSchema) => (req, res, next) => {
|
|
53
|
-
const result = schema.safeParse({ body: req.body, query: req.query, params: req.params });
|
|
54
|
-
if (!result.success) {
|
|
55
|
-
return res.status(400).json({ error: { code: 'VALIDATION_ERROR', message: 'Invalid request', details: result.error.flatten() } });
|
|
56
|
-
}
|
|
57
|
-
Object.assign(req, result.data);
|
|
58
|
-
next();
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
// Compression with conditional skip (Express)
|
|
62
|
-
import compression from 'compression';
|
|
63
|
-
app.use(compression({
|
|
64
|
-
filter: (req, res) => {
|
|
65
|
-
const contentType = res.getHeader('Content-Type') as string || '';
|
|
66
|
-
if (/image|video|audio|zip|gz|br/.test(contentType)) return false;
|
|
67
|
-
return compression.filter(req, res);
|
|
68
|
-
},
|
|
69
|
-
threshold: 1024, // skip responses < 1KB
|
|
70
|
-
}));
|
|
71
|
-
|
|
72
|
-
// Graceful shutdown
|
|
73
|
-
const gracefulShutdown = async (signal: string) => {
|
|
74
|
-
console.log(`Received ${signal}, shutting down gracefully...`);
|
|
75
|
-
server.close(async () => {
|
|
76
|
-
try {
|
|
77
|
-
await prisma.$disconnect();
|
|
78
|
-
await redis.quit();
|
|
79
|
-
console.log('All connections closed. Exiting.');
|
|
80
|
-
process.exit(0);
|
|
81
|
-
} catch (err) {
|
|
82
|
-
console.error('Error during shutdown:', err);
|
|
83
|
-
process.exit(1);
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
// Force exit after 30s if still not done
|
|
87
|
-
setTimeout(() => { console.error('Forced shutdown after timeout'); process.exit(1); }, 30_000);
|
|
88
|
-
};
|
|
89
|
-
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
90
|
-
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
91
|
-
|
|
92
|
-
// Health check endpoints
|
|
93
|
-
app.get('/health/live', (req, res) => res.json({ status: 'ok' }));
|
|
94
|
-
|
|
95
|
-
app.get('/health/ready', async (req, res) => {
|
|
96
|
-
const checks = await Promise.allSettled([
|
|
97
|
-
prisma.$queryRaw`SELECT 1`, // DB check
|
|
98
|
-
redis.ping(), // Redis check
|
|
99
|
-
]);
|
|
100
|
-
const results = { db: checks[0].status, redis: checks[1].status };
|
|
101
|
-
const allHealthy = checks.every(c => c.status === 'fulfilled');
|
|
102
|
-
res.status(allHealthy ? 200 : 503).json({ status: allHealthy ? 'ready' : 'degraded', checks: results });
|
|
103
|
-
});
|
|
104
|
-
```
|
|
1
|
+
---
|
|
2
|
+
name: "middleware-patterns"
|
|
3
|
+
pack: "@rune/backend"
|
|
4
|
+
description: "Middleware architecture — request validation, error handling, logging, CORS, compression, graceful shutdown, health checks, request ID tracking."
|
|
5
|
+
model: sonnet
|
|
6
|
+
tools: [Read, Edit, Write, Grep, Glob, Bash]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# middleware-patterns
|
|
10
|
+
|
|
11
|
+
Middleware architecture — request validation, error handling, logging, CORS, compression, graceful shutdown, health checks, request ID tracking.
|
|
12
|
+
|
|
13
|
+
#### Workflow
|
|
14
|
+
|
|
15
|
+
**Step 1 — Audit middleware stack**
|
|
16
|
+
Read the main server file (app.ts, server.ts, index.ts) to inventory all middleware in registration order. Check for: missing request ID generation, missing structured logging, inconsistent error responses, missing input validation, CORS misconfiguration (`*` in production).
|
|
17
|
+
|
|
18
|
+
**Step 2 — Detect error handling gaps**
|
|
19
|
+
Use Grep to find `catch` blocks, error middleware signatures (`err, req, res, next`), and unhandled promise rejections. Check if errors return consistent format (same envelope for 400, 401, 403, 404, 500). Flag any that leak stack traces or internal details in production.
|
|
20
|
+
|
|
21
|
+
**Step 3 — Emit middleware improvements**
|
|
22
|
+
For each gap, emit the middleware function: request ID (`X-Request-Id` header, UUID per request), structured JSON logger (request method, path, status, duration, request ID), global error handler with consistent envelope, Zod-based request validation middleware.
|
|
23
|
+
|
|
24
|
+
**Step 4 — Compression strategy**
|
|
25
|
+
Emit response compression middleware. Use `brotli` for static assets and pre-compressible responses (better ratio than gzip, supported by all modern clients). Use `gzip` as fallback for older clients. Conditional compression: skip for already-compressed content types (`image/*`, `video/*`, `application/zip`) — compressing these wastes CPU. In Express: use `compression` package with a `filter` function. In Fastify: `@fastify/compress` with `encodings: ['br', 'gzip']`. Minimum size threshold: do not compress responses < 1KB (overhead exceeds benefit).
|
|
26
|
+
|
|
27
|
+
**Step 5 — Graceful shutdown and health checks**
|
|
28
|
+
Graceful shutdown: on `SIGTERM`/`SIGINT`, stop accepting new connections, wait for in-flight requests to complete (timeout 30s), then close DB pools and exit. Emit the shutdown handler for Express (`server.close()`), Fastify (`fastify.close()`), and worker processes. Health check endpoints: `/health/live` (liveness — is the process alive? return 200 always unless process is broken), `/health/ready` (readiness — can it serve traffic? check DB connection, Redis connection, return 503 if dependencies are down). In Kubernetes: map liveness to `livenessProbe`, readiness to `readinessProbe`. Do NOT check external third-party APIs in readiness — only your own dependencies.
|
|
29
|
+
|
|
30
|
+
#### Example
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
// Request ID middleware
|
|
34
|
+
const requestId = (req, res, next) => {
|
|
35
|
+
req.id = req.headers['x-request-id'] || crypto.randomUUID();
|
|
36
|
+
res.setHeader('X-Request-Id', req.id);
|
|
37
|
+
next();
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Structured error handler — consistent envelope, no stack leak
|
|
41
|
+
const errorHandler = (err, req, res, _next) => {
|
|
42
|
+
const status = err.status || 500;
|
|
43
|
+
const message = status < 500 ? err.message : 'Internal server error';
|
|
44
|
+
logger.error({ err, requestId: req.id, path: req.path });
|
|
45
|
+
res.status(status).json({
|
|
46
|
+
error: { code: err.code || 'INTERNAL_ERROR', message },
|
|
47
|
+
request_id: req.id,
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Zod validation middleware
|
|
52
|
+
const validate = (schema: z.ZodSchema) => (req, res, next) => {
|
|
53
|
+
const result = schema.safeParse({ body: req.body, query: req.query, params: req.params });
|
|
54
|
+
if (!result.success) {
|
|
55
|
+
return res.status(400).json({ error: { code: 'VALIDATION_ERROR', message: 'Invalid request', details: result.error.flatten() } });
|
|
56
|
+
}
|
|
57
|
+
Object.assign(req, result.data);
|
|
58
|
+
next();
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Compression with conditional skip (Express)
|
|
62
|
+
import compression from 'compression';
|
|
63
|
+
app.use(compression({
|
|
64
|
+
filter: (req, res) => {
|
|
65
|
+
const contentType = res.getHeader('Content-Type') as string || '';
|
|
66
|
+
if (/image|video|audio|zip|gz|br/.test(contentType)) return false;
|
|
67
|
+
return compression.filter(req, res);
|
|
68
|
+
},
|
|
69
|
+
threshold: 1024, // skip responses < 1KB
|
|
70
|
+
}));
|
|
71
|
+
|
|
72
|
+
// Graceful shutdown
|
|
73
|
+
const gracefulShutdown = async (signal: string) => {
|
|
74
|
+
console.log(`Received ${signal}, shutting down gracefully...`);
|
|
75
|
+
server.close(async () => {
|
|
76
|
+
try {
|
|
77
|
+
await prisma.$disconnect();
|
|
78
|
+
await redis.quit();
|
|
79
|
+
console.log('All connections closed. Exiting.');
|
|
80
|
+
process.exit(0);
|
|
81
|
+
} catch (err) {
|
|
82
|
+
console.error('Error during shutdown:', err);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
// Force exit after 30s if still not done
|
|
87
|
+
setTimeout(() => { console.error('Forced shutdown after timeout'); process.exit(1); }, 30_000);
|
|
88
|
+
};
|
|
89
|
+
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
90
|
+
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
91
|
+
|
|
92
|
+
// Health check endpoints
|
|
93
|
+
app.get('/health/live', (req, res) => res.json({ status: 'ok' }));
|
|
94
|
+
|
|
95
|
+
app.get('/health/ready', async (req, res) => {
|
|
96
|
+
const checks = await Promise.allSettled([
|
|
97
|
+
prisma.$queryRaw`SELECT 1`, // DB check
|
|
98
|
+
redis.ping(), // Redis check
|
|
99
|
+
]);
|
|
100
|
+
const results = { db: checks[0].status, redis: checks[1].status };
|
|
101
|
+
const allHealthy = checks.every(c => c.status === 'fulfilled');
|
|
102
|
+
res.status(allHealthy ? 200 : 503).json({ status: allHealthy ? 'ready' : 'degraded', checks: results });
|
|
103
|
+
});
|
|
104
|
+
```
|