@oalacea/demon 1.0.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/CHANGELOG.md ADDED
@@ -0,0 +1,38 @@
1
+ # Demon - Changelog
2
+
3
+ ## [1.0.0-alpha] - 2025-02-07
4
+
5
+ ### Added
6
+ - Initial release
7
+ - Auto-detection for frameworks (Next.js, Remix, SvelteKit, Vite, etc.)
8
+ - Database detection (Prisma, Drizzle, Neon, Supabase, local Postgres)
9
+ - Unit test generation (components, hooks, utils)
10
+ - Integration test templates (API routes, DB with transaction rollback)
11
+ - E2E test templates (Playwright)
12
+ - Performance test templates (k6)
13
+ - Dependency efficiency analysis (TanStack Router, React Query, Prisma, Zustand, React Compiler)
14
+ - Docker container with all testing tools
15
+ - Cross-platform support (Linux, macOS, Windows)
16
+
17
+ ### Features
18
+ - **Unit Tests**: Component, hook, and utility testing with Vitest
19
+ - **Integration Tests**: API route and database testing with Prisma
20
+ - **E2E Tests**: User flow testing with Playwright
21
+ - **Performance Tests**: API load testing with k6
22
+ - **Analysis**: Dependency pattern analysis and recommendations
23
+ - **Remediation**: Automatic test failure categorization and fix suggestions
24
+
25
+ ### Tools Included
26
+ - Vitest
27
+ - @testing-library/react
28
+ - Playwright
29
+ - k6
30
+ - supertest
31
+ - MSW
32
+ - @prisma/cli
33
+
34
+ ### Documentation
35
+ - Comprehensive prompt system for AI agent
36
+ - Test templates for all test types
37
+ - Fix engine for common test failures
38
+ - Performance analysis and reporting
package/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ # Demon - License
2
+
3
+ MIT License
4
+
5
+ Copyright (c) 2025 Yanis
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,103 @@
1
+ # Demon
2
+
3
+ AI-powered automated test generation and remediation for web applications.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ # From your project directory
9
+ npx @oalacea/demon
10
+ ```
11
+
12
+ First run installs the testing toolkit (~500 MB Docker image, takes 2-3 minutes).
13
+
14
+ ## What You Need
15
+
16
+ - **Docker** - [Install](https://docs.docker.com/get-docker/)
17
+ - **AI coding agent** - Claude Code, Cursor, Windsurf, Aider, Codex...
18
+
19
+ ## Features
20
+
21
+ | Category | Features |
22
+ |----------|----------|
23
+ | **Unit Tests** | Components, hooks, utils, validators, stores |
24
+ | **Integration Tests** | API routes, database operations (with transaction rollback) |
25
+ | **E2E Tests** | User flows, form interactions, navigation (Playwright) |
26
+ | **Performance Tests** | API load testing (k6), DB query benchmarks |
27
+ | **Dependency Analysis** | TanStack Router, React Query, Prisma, Zustand, React Compiler |
28
+
29
+ ## How It Works
30
+
31
+ 1. **Analyze** - Auto-detects your framework, database, and existing tests
32
+ 2. **Generate** - Creates tests based on your code patterns
33
+ 3. **Execute** - Runs tests inside Docker container
34
+ 4. **Fix** - Analyzes failures and applies corrections
35
+ 5. **Report** - Provides comprehensive coverage and performance report
36
+
37
+ ## Included Tools
38
+
39
+ The Docker toolkit includes:
40
+
41
+ | Category | Tools |
42
+ |----------|-------|
43
+ | Testing | Vitest, @testing-library/react, happy-dom |
44
+ | E2E | Playwright (Chromium) |
45
+ | Performance | k6 |
46
+ | Utilities | supertest, MSW, @prisma/cli |
47
+
48
+ ## Output Example
49
+
50
+ ```
51
+ ✓ Unit Tests: 45 created, 42 passing, 3 fixed
52
+ ✓ Integration: 12 created, 12 passing
53
+ ✓ E2E: 8 created, 7 passing, 1 requires manual review
54
+ ✓ Performance: API p95 = 145ms (PASS)
55
+ ✓ Dependencies: 3 improvements suggested
56
+
57
+ ## Summary
58
+ Total Tests: 245
59
+ Passing: 238
60
+ Failing: 2 (requires manual review)
61
+ Coverage: 84%
62
+ ```
63
+
64
+ ## Safety
65
+
66
+ - Always use transaction rollback for database tests
67
+ - Never modify production data
68
+ - Git integration for safe rollbacks
69
+ - Non-destructive testing modes available
70
+
71
+ ## Troubleshooting
72
+
73
+ ### Rebuild toolkit image
74
+
75
+ ```bash
76
+ docker rm -f demon-tools
77
+ docker rmi demon-tools
78
+ npx @oalacea/demon
79
+ ```
80
+
81
+ ### Run specific test
82
+
83
+ ```bash
84
+ docker exec demon-tools npm test -- Button.test.ts
85
+ ```
86
+
87
+ ### Debug test
88
+
89
+ ```bash
90
+ docker exec demon-tools npm test -- Button.test.ts --reporter=verbose
91
+ ```
92
+
93
+ ## Related
94
+
95
+ - **Guardian** - Security testing package: `npx @oalacea/guardian`
96
+
97
+ ## License
98
+
99
+ MIT - Use at your own risk.
100
+
101
+ ## Credits
102
+
103
+ Inspired by the need for comprehensive automated testing in modern web development.
@@ -0,0 +1,366 @@
1
+ /**
2
+ * Demon - Dependency Efficiency Analyzer
3
+ *
4
+ * Analyzes codebase for dependency usage patterns and inefficiencies:
5
+ * - TanStack Router patterns
6
+ * - React Query usage
7
+ * - Prisma query patterns
8
+ * - Zustand store patterns
9
+ * - React Compiler readiness
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ /**
16
+ * Find files matching a pattern
17
+ */
18
+ function findFiles(dir, pattern, excludeDirs = ['node_modules', '.next', 'dist', 'build']) {
19
+ const files = [];
20
+
21
+ function traverse(currentDir) {
22
+ if (!fs.existsSync(currentDir)) return;
23
+
24
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
25
+
26
+ for (const entry of entries) {
27
+ if (entry.isDirectory()) {
28
+ if (excludeDirs.includes(entry.name)) continue;
29
+ traverse(path.join(currentDir, entry.name));
30
+ } else if (entry.isFile() && entry.name.match(pattern)) {
31
+ files.push(path.join(currentDir, entry.name));
32
+ }
33
+ }
34
+ }
35
+
36
+ traverse(dir);
37
+ return files;
38
+ }
39
+
40
+ /**
41
+ * Read file content
42
+ */
43
+ function readFile(filePath) {
44
+ try {
45
+ return fs.readFileSync(filePath, 'utf-8');
46
+ } catch {
47
+ return '';
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Analyze TanStack Router usage
53
+ */
54
+ function analyzeTanStackRouter(projectDir) {
55
+ const findings = {
56
+ good: [],
57
+ issues: [],
58
+ recommendations: [],
59
+ };
60
+
61
+ const routeFiles = findFiles(path.join(projectDir, 'src'), /routes/);
62
+
63
+ for (const file of routeFiles) {
64
+ const content = readFile(file);
65
+
66
+ // Check for typed routes
67
+ if (content.includes('useParams') || content.includes('$')) {
68
+ findings.good.push(`Typed params in ${path.relative(projectDir, file)}`);
69
+ }
70
+
71
+ // Check for loaders
72
+ if (content.includes('loader:') || content.includes('loaderBefore')) {
73
+ findings.good.push(`Data loader in ${path.relative(projectDir, file)}`);
74
+ } else if (content.includes('useQuery') || content.includes('useFetch')) {
75
+ findings.issues.push(`Missing loader in ${path.relative(projectDir, file)} - data fetching without loader`);
76
+ }
77
+
78
+ // Check for error boundaries
79
+ if (content.includes('loader:') && !content.includes('errorComponent') && !content.includes('ErrorBoundary')) {
80
+ findings.issues.push(`Missing error boundary in ${path.relative(projectDir, file)}`);
81
+ findings.recommendations.push(`Add errorComponent to route in ${path.relative(projectDir, file)}`);
82
+ }
83
+ }
84
+
85
+ // Check navigation for prefetching
86
+ const componentFiles = findFiles(path.join(projectDir, 'src'), /.*\.(tsx|jsx)$/);
87
+ for (const file of componentFiles) {
88
+ const content = readFile(file);
89
+ if (content.includes('<Link') && !content.includes('prefetch=') && !content.includes('prefetchIntent')) {
90
+ findings.issues.push(`Link prefetching not enabled in ${path.relative(projectDir, file)}`);
91
+ }
92
+ }
93
+
94
+ return findings;
95
+ }
96
+
97
+ /**
98
+ * Analyze React Query usage
99
+ */
100
+ function analyzeReactQuery(projectDir) {
101
+ const findings = {
102
+ good: [],
103
+ issues: [],
104
+ recommendations: [],
105
+ };
106
+
107
+ const hookFiles = findFiles(path.join(projectDir, 'src'), /hooks/);
108
+ const componentFiles = findFiles(path.join(projectDir, 'src'), /.*\.(tsx|jsx)$/);
109
+ const allFiles = [...hookFiles, ...componentFiles];
110
+
111
+ for (const file of allFiles) {
112
+ const content = readFile(file);
113
+
114
+ if (!content.includes('useQuery') && !content.includes('useMutation')) continue;
115
+
116
+ // Check for array cache keys
117
+ if (content.includes('useQuery(') || content.includes('useInfiniteQuery(')) {
118
+ const hasArrayKey = /\['/.test(content) || /queryKey:\s*\[/.test(content);
119
+ if (hasArrayKey) {
120
+ findings.good.push(`Array-based cache keys in ${path.relative(projectDir, file)}`);
121
+ } else {
122
+ findings.issues.push(`Non-array cache keys in ${path.relative(projectDir, file)}`);
123
+ findings.recommendations.push(`Use array-based cache keys in ${path.relative(projectDir, file)}`);
124
+ }
125
+ }
126
+
127
+ // Check for staleTime
128
+ if (content.includes('useQuery(') && !content.includes('staleTime')) {
129
+ findings.issues.push(`Missing staleTime in ${path.relative(projectDir, file)}`);
130
+ findings.recommendations.push(`Add staleTime to queries in ${path.relative(projectDir, file)}`);
131
+ }
132
+
133
+ // Check for mutation invalidation
134
+ if (content.includes('useMutation(')) {
135
+ if (content.includes('invalidateQueries')) {
136
+ findings.good.push(`Proper invalidation in ${path.relative(projectDir, file)}`);
137
+ } else {
138
+ findings.issues.push(`Missing invalidation in mutation at ${path.relative(projectDir, file)}`);
139
+ }
140
+ }
141
+ }
142
+
143
+ return findings;
144
+ }
145
+
146
+ /**
147
+ * Analyze Prisma usage
148
+ */
149
+ function analyzePrisma(projectDir) {
150
+ const findings = {
151
+ good: [],
152
+ issues: [],
153
+ recommendations: [],
154
+ };
155
+
156
+ const libFiles = findFiles(path.join(projectDir, 'src'), /.*\.(ts|js)$/);
157
+
158
+ for (const file of libFiles) {
159
+ const content = readFile(file);
160
+
161
+ if (!content.includes('prisma.')) continue;
162
+
163
+ // Check for select usage
164
+ if (content.includes('prisma.') && content.includes('findMany')) {
165
+ if (content.includes('select:')) {
166
+ findings.good.push(`Using select in ${path.relative(projectDir, file)}`);
167
+ } else if (content.includes('.findMany().then')) {
168
+ findings.issues.push(`Not using select in ${path.relative(projectDir, file)} - returning full objects`);
169
+ findings.recommendations.push(`Add select to prisma queries in ${path.relative(projectDir, file)}`);
170
+ }
171
+ }
172
+
173
+ // Check for potential N+1
174
+ const lines = content.split('\n');
175
+ for (let i = 0; i < lines.length; i++) {
176
+ if (lines[i].includes('findMany') || lines[i].includes('findFirst')) {
177
+ // Check next 10 lines for forEach with prisma query
178
+ for (let j = i + 1; j < Math.min(i + 10, lines.length); j++) {
179
+ if (lines[j].includes('forEach') && lines[j].includes('prisma.')) {
180
+ findings.issues.push(`Potential N+1 query in ${path.relative(projectDir, file)}:${i + 1}`);
181
+ findings.recommendations.push(`Use include or separate query with WHERE in ${path.relative(projectDir, file)}`);
182
+ break;
183
+ }
184
+ }
185
+ }
186
+ }
187
+ }
188
+
189
+ // Check schema for indexes
190
+ const schemaPath = path.join(projectDir, 'prisma', 'schema.prisma');
191
+ if (fs.existsSync(schemaPath)) {
192
+ const schema = readFile(schemaPath);
193
+ const models = schema.matchAll(/model\s+(\w+)\s*{([^}]+)}/g);
194
+
195
+ for (const modelMatch of models) {
196
+ const modelName = modelMatch[1];
197
+ const body = modelMatch[2];
198
+
199
+ if (body.includes('email') && !body.includes('@@index') && !body.includes('@@unique')) {
200
+ findings.recommendations.push(`Add index on ${modelName}.email for faster lookups`);
201
+ }
202
+ }
203
+ }
204
+
205
+ return findings;
206
+ }
207
+
208
+ /**
209
+ * Analyze Zustand usage
210
+ */
211
+ function analyzeZustand(projectDir) {
212
+ const findings = {
213
+ good: [],
214
+ issues: [],
215
+ recommendations: [],
216
+ };
217
+
218
+ const storeFiles = findFiles(path.join(projectDir, 'src'), /store|stores/);
219
+ const componentFiles = findFiles(path.join(projectDir, 'src'), /.*\.(tsx|jsx)$/);
220
+
221
+ for (const file of storeFiles) {
222
+ const content = readFile(file);
223
+
224
+ // Check store size
225
+ const lines = content.split('\n').length;
226
+ if (lines > 500) {
227
+ findings.issues.push(`Large store file ${path.relative(projectDir, file)} (${lines} lines)`);
228
+ findings.recommendations.push(`Consider splitting ${path.basename(file)} into multiple stores`);
229
+ }
230
+ }
231
+
232
+ for (const file of componentFiles) {
233
+ const content = readFile(file);
234
+
235
+ if (!content.includes('useStore')) continue;
236
+
237
+ // Check for full-store subscriptions
238
+ if (content.includes('useStore()') || content.match(/useStore\(\s*state\s*=>\s*state/)) {
239
+ findings.issues.push(`Full-store subscription in ${path.relative(projectDir, file)}`);
240
+ findings.recommendations.push(`Use selectors for specific fields in ${path.relative(projectDir, file)}`);
241
+ }
242
+ }
243
+
244
+ return findings;
245
+ }
246
+
247
+ /**
248
+ * Analyze React Compiler readiness
249
+ */
250
+ function analyzeReactCompilerReadiness(projectDir) {
251
+ const findings = {
252
+ good: [],
253
+ issues: [],
254
+ recommendations: [],
255
+ };
256
+
257
+ const componentFiles = findFiles(path.join(projectDir, 'src'), /.*\.(tsx|jsx)$/);
258
+
259
+ for (const file of componentFiles) {
260
+ const content = readFile(file);
261
+
262
+ // Check for simple useMemo that can be removed
263
+ const simpleMemo = content.match(/useMemo\(\(\)\s*=>\s*([^,]+),\s*\[[^\]]*\]\)/g);
264
+ if (simpleMemo) {
265
+ for (const memo of simpleMemo) {
266
+ const value = memo.match(/=>\s*(.+),/)?.[1];
267
+ if (value && !value.includes('()') && !value.includes('function')) {
268
+ findings.recommendations.push(`Remove simple useMemo in ${path.basename(file)} - React Compiler will handle this`);
269
+ }
270
+ }
271
+ }
272
+
273
+ // Check for useCallback dependencies
274
+ const useCallbacks = content.matchAll(/useCallback\([^)]+\)/g);
275
+ for (const callback of useCallbacks) {
276
+ const deps = callback[0].match(/\[([^\]]*)\]/)?.[1];
277
+ if (deps && deps.trim() === '') {
278
+ findings.issues.push(`useCallback with empty deps in ${path.basename(file)}`);
279
+ }
280
+ }
281
+
282
+ // Check for large inline objects
283
+ const largeObjects = content.match(/{{[\s\S]{200,}}}/g);
284
+ if (largeObjects) {
285
+ findings.issues.push(`Large inline object in ${path.basename(file)} - move outside component`);
286
+ findings.recommendations.push(`Extract large objects to constants in ${path.basename(file)}`);
287
+ }
288
+ }
289
+
290
+ return findings;
291
+ }
292
+
293
+ /**
294
+ * Analyze bundle optimization
295
+ */
296
+ function analyzeBundleOptimization(projectDir) {
297
+ const findings = {
298
+ good: [],
299
+ issues: [],
300
+ recommendations: [],
301
+ };
302
+
303
+ const files = findFiles(path.join(projectDir, 'src'), /.*\.(ts|tsx|js|jsx)$/);
304
+
305
+ for (const file of files) {
306
+ const content = readFile(file);
307
+
308
+ // Check for namespace imports
309
+ if (content.includes('* as ')) {
310
+ findings.issues.push(`Namespace import in ${path.basename(file)}`);
311
+ findings.recommendations.push(`Use named imports for better tree-shaking in ${path.basename(file)}`);
312
+ }
313
+
314
+ // Check for large library imports
315
+ const largeLibs = ['monaco-editor', 'codemirror', 'pdfjs-dist', 'fabric'];
316
+ for (const lib of largeLibs) {
317
+ if (content.includes(`from '${lib}'`) || content.includes(`from "${lib}"`)) {
318
+ if (!content.includes('dynamic(') && !content.includes('React.lazy')) {
319
+ findings.recommendations.push(`Use dynamic import for ${lib} in ${path.basename(file)}`);
320
+ }
321
+ }
322
+ }
323
+ }
324
+
325
+ // Check package.json for duplicate dependencies
326
+ const pkgPath = path.join(projectDir, 'package.json');
327
+ if (fs.existsSync(pkgPath)) {
328
+ const pkg = JSON.parse(readFile(pkgPath));
329
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
330
+
331
+ for (const [name, version] of Object.entries(deps)) {
332
+ if (name.startsWith('@types/')) {
333
+ const mainPkg = name.substring(6);
334
+ if (mainPkg in deps) {
335
+ findings.good.push(`Types package for ${mainPkg} found`);
336
+ }
337
+ }
338
+ }
339
+ }
340
+
341
+ return findings;
342
+ }
343
+
344
+ /**
345
+ * Run full analysis
346
+ */
347
+ function analyze(projectDir) {
348
+ return {
349
+ tanStackRouter: analyzeTanStackRouter(projectDir),
350
+ reactQuery: analyzeReactQuery(projectDir),
351
+ prisma: analyzePrisma(projectDir),
352
+ zustand: analyzeZustand(projectDir),
353
+ reactCompiler: analyzeReactCompilerReadiness(projectDir),
354
+ bundleOptimization: analyzeBundleOptimization(projectDir),
355
+ };
356
+ }
357
+
358
+ module.exports = {
359
+ analyze,
360
+ analyzeTanStackRouter,
361
+ analyzeReactQuery,
362
+ analyzePrisma,
363
+ analyzeZustand,
364
+ analyzeReactCompilerReadiness,
365
+ analyzeBundleOptimization,
366
+ };