@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/bin/Dockerfile ADDED
@@ -0,0 +1,65 @@
1
+ # Demon - Testing Tools Container
2
+ FROM node:20-slim
3
+
4
+ LABEL maintainer="Yanis"
5
+ LABEL description="Demon - Automated testing tools for web applications"
6
+
7
+ # Install system dependencies for Playwright
8
+ RUN apt-get update && apt-get install -y --no-install-recommends \
9
+ libnss3 \
10
+ libnspr4 \
11
+ libatk1.0-0 \
12
+ libatk-bridge2.0-0 \
13
+ libcups6 \
14
+ libdrm2 \
15
+ libxkbcommon0 \
16
+ libxcomposite1 \
17
+ libxdamage1 \
18
+ libxfixes3 \
19
+ libxrandr2 \
20
+ libgbm1 \
21
+ libpango-1.0-0 \
22
+ libcairo2 \
23
+ libasound2 \
24
+ libatspi2.0-0 \
25
+ libxshmfence1 \
26
+ && rm -rf /var/lib/apt/lists/*
27
+
28
+ # Install Playwright browsers
29
+ RUN npx playwright install --with-deps chromium
30
+
31
+ # Install testing tools globally
32
+ RUN npm install -g \
33
+ vitest \
34
+ @vitest/ui \
35
+ @playwright/test \
36
+ @vitejs/plugin-react \
37
+ @testing-library/react \
38
+ @testing-library/jest-dom \
39
+ @testing-library/user-event \
40
+ happy-dom
41
+
42
+ # Install k6 for performance testing
43
+ ARG TARGETARCH=amd64
44
+ RUN ARCH=$([ "$TARGETARCH" = "arm64" ] && echo "arm64" || echo "amd64") && \
45
+ wget -q "https://github.com/grafana/k6/releases/download/v0.52.0/k6-v0.52.0-linux-${ARCH}.tar.gz" -O /tmp/k6.tar.gz && \
46
+ tar -xzf /tmp/k6.tar.gz -C /usr/local/bin && \
47
+ rm /tmp/k6.tar.gz
48
+
49
+ # Install additional tools
50
+ RUN npm install -g \
51
+ supertest \
52
+ msw \
53
+ @prisma/cli
54
+
55
+ # Create non-root user for security
56
+ RUN useradd -m -u 1000 -s /bin/bash demon && \
57
+ mkdir -p /app && \
58
+ chown -R demon:demon /app
59
+
60
+ # Switch to non-root user
61
+ USER demon
62
+ WORKDIR /app
63
+
64
+ # Keep container alive for docker exec
65
+ CMD ["sleep", "infinity"]
package/bin/cli.js ADDED
@@ -0,0 +1,455 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { execSync } = require('child_process');
4
+ const readline = require('readline');
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+
8
+ // --- Config ---
9
+ const CONFIG = {
10
+ IMAGE: 'demon-tools',
11
+ CONTAINER: 'demon-tools',
12
+ PROMPT_SRC: path.join(__dirname, '..', 'prompts', 'EXECUTE.md'),
13
+ // Cross-platform home directory
14
+ PROMPT_DEST: path.join(
15
+ process.env.HOME ||
16
+ process.env.USERPROFILE ||
17
+ path.join(process.env.HOMEDRIVE, process.env.HOMEPATH),
18
+ '.demon',
19
+ 'EXECUTE.md'
20
+ ),
21
+ PROJECT_DIR: process.cwd(),
22
+ DOCKERFILE: path.join(__dirname, 'Dockerfile')
23
+ };
24
+
25
+ // --- Colors ---
26
+ const green = (s) => `\x1b[32m${s}\x1b[0m`;
27
+ const red = (s) => `\x1b[31m${s}\x1b[0m`;
28
+ const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
29
+ const bold = (s) => `\x1b[1m${s}\x1b[0m`;
30
+ const dim = (s) => `\x1b[2m${s}\x1b[0m`;
31
+ const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
32
+ const blue = (s) => `\x1b[34m${s}\x1b[0m`;
33
+ const magenta = (s) => `\x1b[35m${s}\x1b[0m`;
34
+
35
+ // --- Utilities ---
36
+ function run(cmd, { silent = false, timeout = 60000 } = {}) {
37
+ try {
38
+ return execSync(cmd, { encoding: 'utf-8', stdio: 'pipe', timeout }).trim();
39
+ } catch (error) {
40
+ if (!silent && error.status !== null) {
41
+ console.error(dim(` [debug] Command exited with code ${error.status}`));
42
+ }
43
+ return null;
44
+ }
45
+ }
46
+
47
+ function fail(msg) {
48
+ console.error(`\n ${red('✗')} ${msg}\n`);
49
+ process.exit(1);
50
+ }
51
+
52
+ function ask(question) {
53
+ return new Promise((resolve) => {
54
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
55
+ rl.question(question, (answer) => {
56
+ rl.close();
57
+ resolve(answer.trim().toLowerCase());
58
+ });
59
+ });
60
+ }
61
+
62
+ // --- Project Markers ---
63
+ const PROJECT_MARKERS = [
64
+ 'package.json',
65
+ 'requirements.txt',
66
+ 'pyproject.toml',
67
+ 'Pipfile',
68
+ 'go.mod',
69
+ 'pom.xml',
70
+ 'build.gradle',
71
+ 'Gemfile',
72
+ 'composer.json',
73
+ 'Cargo.toml'
74
+ ];
75
+
76
+ function hasProjectFiles() {
77
+ const cwd = path.resolve(process.cwd());
78
+ try {
79
+ const realCwd = fs.realpathSync(cwd);
80
+ if (realCwd !== cwd) {
81
+ console.warn(yellow(' ⚠ Symbolic link detected in path, using real path'));
82
+ }
83
+ } catch {
84
+ // realpathSync failed, continue with cwd
85
+ }
86
+ return PROJECT_MARKERS.some((f) => fs.existsSync(path.join(cwd, f)));
87
+ }
88
+
89
+ // --- Detection Import ---
90
+ function runDetection(projectDir) {
91
+ const detectorPath = path.join(__dirname, '..', 'agents', 'detector.js');
92
+
93
+ if (!fs.existsSync(detectorPath)) {
94
+ // Fallback if detector doesn't exist yet
95
+ return {
96
+ framework: 'Unknown',
97
+ language: 'JavaScript/TypeScript',
98
+ testRunner: 'Vitest',
99
+ database: null,
100
+ existingTests: 0,
101
+ coverage: null,
102
+ dependencies: [],
103
+ target: 'http://localhost:3000'
104
+ };
105
+ }
106
+
107
+ try {
108
+ // Execute detector as a module
109
+ const detector = require(detectorPath);
110
+ return detector.analyze(projectDir);
111
+ } catch (e) {
112
+ console.log(dim(` [debug] Detection error: ${e.message}`));
113
+ return {
114
+ framework: 'Unknown',
115
+ language: 'JavaScript/TypeScript',
116
+ testRunner: 'Vitest',
117
+ database: null,
118
+ existingTests: 0,
119
+ coverage: null,
120
+ dependencies: [],
121
+ target: 'http://localhost:3000'
122
+ };
123
+ }
124
+ }
125
+
126
+ // --- Prompt Generation ---
127
+ function generatePrompt(context) {
128
+ const promptSrc = CONFIG.PROMPT_SRC;
129
+
130
+ if (!fs.existsSync(promptSrc)) {
131
+ return generateFallbackPrompt(context);
132
+ }
133
+
134
+ const basePrompt = fs.readFileSync(promptSrc, 'utf-8');
135
+
136
+ // Build context block
137
+ const contextBlock = buildContextBlock(context);
138
+
139
+ return contextBlock + '\n' + basePrompt;
140
+ }
141
+
142
+ function buildContextBlock(context) {
143
+ const lines = [
144
+ '> **DETECTED CONTEXT**',
145
+ ];
146
+
147
+ if (context.framework) {
148
+ lines.push(`> Framework: ${context.framework}`);
149
+ }
150
+ if (context.language) {
151
+ lines.push(`> Language: ${context.language}`);
152
+ }
153
+ if (context.testRunner) {
154
+ lines.push(`> Test Runner: ${context.testRunner}`);
155
+ }
156
+ if (context.database) {
157
+ lines.push(`> Database: ${context.database.type || 'detected'}`);
158
+ lines.push(`> DB Connection: ${context.database.connection || 'DATABASE_URL'}`);
159
+ lines.push(`> Test Strategy: Transaction rollback (do not modify real data)`);
160
+ } else {
161
+ lines.push(`> Database: none detected`);
162
+ }
163
+ lines.push(`> Existing Tests: ${context.existingTests || 0} found`);
164
+ if (context.coverage) {
165
+ lines.push(`> Current Coverage: ${context.coverage}`);
166
+ }
167
+ if (context.dependencies && context.dependencies.length > 0) {
168
+ lines.push(`> Key Dependencies: ${context.dependencies.join(', ')}`);
169
+ }
170
+ lines.push(`> Target: ${context.target || 'http://localhost:3000'}`);
171
+
172
+ lines.push('');
173
+ lines.push('> **IMPORTANT**:');
174
+ lines.push('> - Use this detected context. Do not re-detect.');
175
+ lines.push('> - Always read source code before generating tests.');
176
+ lines.push('> - Run tests to verify they work before declaring success.');
177
+ if (context.database) {
178
+ lines.push('> - Use transaction rollback for DB tests - never modify real data.');
179
+ }
180
+ lines.push('');
181
+ lines.push('> **WORKFLOW**:');
182
+ lines.push('> 1. Read ~/.demon/EXECUTE.md for full instructions');
183
+ lines.push('> 2. Generate tests following the detected patterns');
184
+ lines.push('> 3. Run tests via Docker container');
185
+ lines.push('> 4. Fix failures iteratively until all pass');
186
+ lines.push('> 5. Generate final report');
187
+ lines.push('');
188
+
189
+ return lines.join('\n');
190
+ }
191
+
192
+ function generateFallbackPrompt(context) {
193
+ return `# Demon — Automated Testing Process
194
+
195
+ > **DETECTED CONTEXT**
196
+ > Framework: ${context.framework || 'Unknown'}
197
+ > Language: ${context.language || 'JavaScript/TypeScript'}
198
+ > Test Runner: ${context.testRunner || 'Vitest'}
199
+ > Database: ${context.database?.type || 'none'}
200
+ > Target: ${context.target || 'http://localhost:3000'}
201
+
202
+ ## Instructions
203
+
204
+ This is the automated testing agent. Follow these steps:
205
+
206
+ 1. **Read the project structure** - Understand the framework and patterns
207
+ 2. **Generate unit tests** - For components, hooks, and utilities
208
+ 3. **Generate integration tests** - For API routes and database operations
209
+ 4. **Generate E2E tests** - For critical user flows
210
+ 5. **Run performance tests** - API load testing and DB query analysis
211
+ 6. **Analyze dependencies** - Check for efficiency patterns
212
+
213
+ Always run tests to verify they work. When a test fails, analyze and fix before proceeding.
214
+
215
+ ## Tool Execution
216
+
217
+ All test tools run inside the Demon Docker container:
218
+
219
+ \`\`\`bash
220
+ docker exec demon-tools <command>
221
+ \`\`\`
222
+
223
+ ### Available Commands
224
+
225
+ | Task | Command |
226
+ |------|---------|
227
+ | Unit tests | \`docker exec demon-tools npm test\` |
228
+ | E2E tests | \`docker exec demon-tools npx playwright test\` |
229
+ | Performance | \`docker exec demon-tools k6 run tests/performance/load.js\` |
230
+
231
+ ## Phase 1 - Unit Tests
232
+
233
+ Generate tests for:
234
+ - Components (render, props, events, edge cases)
235
+ - Hooks (state updates, return values, cleanup)
236
+ - Utils (pure functions, validators)
237
+
238
+ Template:
239
+ \`\`\`typescript
240
+ import { render, screen } from '@testing-library/react';
241
+ import { describe, it, expect, vi } from 'vitest';
242
+ import { Component } from '@/components/Component';
243
+
244
+ describe('Component', () => {
245
+ it('should render', () => {
246
+ render(<Component />);
247
+ expect(screen.getByRole('button')).toBeInTheDocument();
248
+ });
249
+ });
250
+ \`\`\`
251
+
252
+ ## Phase 2 - Integration Tests
253
+
254
+ For API routes and database operations:
255
+ \`\`\`typescript
256
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
257
+ import { db } from '@test/db';
258
+
259
+ describe('API Integration', () => {
260
+ beforeEach(async () => {
261
+ await db.begin();
262
+ });
263
+
264
+ afterEach(async () => {
265
+ await db.rollback();
266
+ });
267
+
268
+ it('should create resource', async () => {
269
+ const result = await db.resource.create({ data: { name: 'test' } });
270
+ expect(result).toHaveProperty('id');
271
+ });
272
+ });
273
+ \`\`\`
274
+
275
+ ## Phase 3 - E2E Tests
276
+
277
+ Use Playwright for user flows:
278
+ \`\`\`typescript
279
+ import { test, expect } from '@playwright/test';
280
+
281
+ test('user journey', async ({ page }) => {
282
+ await page.goto('/login');
283
+ await page.fill('input[name="email"]', 'test@example.com');
284
+ await page.click('button[type="submit"]');
285
+ await expect(page).toHaveURL('/dashboard');
286
+ });
287
+ \`\`\`
288
+
289
+ ## Fix Loop
290
+
291
+ When tests fail:
292
+ 1. Analyze the error
293
+ 2. Determine if it's a test issue or code bug
294
+ 3. Apply fix
295
+ 4. Re-test
296
+
297
+ ## Completion
298
+
299
+ Report:
300
+ \`\`\`
301
+ ✓ Unit Tests: X created, Y passing
302
+ ✓ Integration: X created, Y passing
303
+ ✓ E2E: X created, Y passing
304
+ ✓ Performance: API p95 = Xms
305
+ \`\`\`
306
+ `;
307
+ }
308
+
309
+ // --- Main Function ---
310
+ async function main() {
311
+ console.log('');
312
+ console.log(bold(magenta(' ╔═══════════════════════════════════════╗')));
313
+ console.log(bold(magenta(' ║ Demon ║')));
314
+ console.log(bold(magenta(' ║ AI-Powered Test Generation ║')));
315
+ console.log(bold(magenta(' ╚═══════════════════════════════════════╝')));
316
+ console.log('');
317
+ console.log(dim(' Automated testing toolkit for web applications'));
318
+ console.log('');
319
+
320
+ // Check if we're in a project directory
321
+ if (!hasProjectFiles()) {
322
+ console.log(yellow(' ⚠ No project markers found.'));
323
+ console.log(yellow(' Please run from a project directory with package.json or equivalent.'));
324
+ console.log('');
325
+ const answer = await ask(' Continue anyway? ' + dim('(Y/n)') + ' ');
326
+ if (answer === 'n' || answer === 'no') {
327
+ process.exit(0);
328
+ }
329
+ }
330
+
331
+ // --- Step 1: Check Docker ---
332
+ console.log(` ${dim('→')} Checking Docker...`);
333
+ if (run('docker info', { silent: true }) === null) {
334
+ fail(`Docker is not running.
335
+
336
+ Start Docker Desktop (or the Docker daemon) and try again.
337
+
338
+ Install Docker: ${cyan('https://docs.docker.com/get-docker/')}`);
339
+ }
340
+ console.log(` ${green('✓')} Docker is running`);
341
+
342
+ // --- Step 2: Build image if missing ---
343
+ const imageExists = run(`docker images -q ${CONFIG.IMAGE}`);
344
+
345
+ if (!imageExists) {
346
+ console.log('');
347
+ console.log(` ${yellow('◆')} The testing toolkit needs to be installed (~500 MB Docker image).`);
348
+ console.log(` ${dim('This only happens once.')}`);
349
+ console.log('');
350
+ const answer = await ask(` Install it now? ${dim('(Y/n)')} `);
351
+ if (answer === 'n' || answer === 'no') {
352
+ console.log('');
353
+ console.log(dim(' No problem. Run npx @oalacea/demon again when you\'re ready.'));
354
+ console.log('');
355
+ process.exit(0);
356
+ });
357
+ console.log('');
358
+ console.log(` ${yellow('→')} Building testing toolkit...`);
359
+ console.log(dim(' This may take 2-3 minutes on first run...'));
360
+ console.log('');
361
+ try {
362
+ execSync(
363
+ `docker build -t ${CONFIG.IMAGE} -f "${CONFIG.DOCKERFILE}" "${path.dirname(CONFIG.DOCKERFILE)}"`,
364
+ { stdio: 'inherit', timeout: 600000 }
365
+ );
366
+ } catch {
367
+ fail(`Failed to build the testing toolkit image.
368
+
369
+ Try manually:
370
+ ${cyan(`docker build -t ${CONFIG.IMAGE} -f "${CONFIG.DOCKERFILE}" "${path.dirname(CONFIG.DOCKERFILE)}"`)}`);
371
+ }
372
+ console.log('');
373
+ console.log(` ${green('✓')} Testing toolkit installed`);
374
+ } else {
375
+ console.log(` ${green('✓')} Testing toolkit ready`);
376
+ }
377
+
378
+ // --- Step 3: Start container if not running ---
379
+ const containerRunning = run(
380
+ `docker ps --filter "name=^${CONFIG.CONTAINER}$" --format "{{.Names}}"`
381
+ );
382
+
383
+ if (containerRunning === CONFIG.CONTAINER) {
384
+ console.log(` ${green('✓')} Toolkit container running (${bold(CONFIG.CONTAINER)})`);
385
+ } else {
386
+ const containerExists = run(
387
+ `docker ps -a --filter "name=^${CONFIG.CONTAINER}$" --format "{{.Names}}"`
388
+ );
389
+
390
+ if (containerExists === CONFIG.CONTAINER) {
391
+ process.stdout.write(` ${yellow('→')} Starting toolkit container...`);
392
+ if (run(`docker start ${CONFIG.CONTAINER}`, { timeout: 30000 }) === null) {
393
+ console.log('');
394
+ fail(`Failed to start container.
395
+
396
+ Try manually:
397
+ ${cyan(`docker start ${CONFIG.CONTAINER}`)}`);
398
+ }
399
+ console.log(` ${green('done')}`);
400
+ } else {
401
+ const isLinux = process.platform === 'linux';
402
+ const networkFlag = isLinux ? '--network=host' : '';
403
+
404
+ process.stdout.write(` ${yellow('→')} Creating toolkit container (${CONFIG.CONTAINER})...`);
405
+ const runCmd = `docker run -d --name ${CONFIG.CONTAINER} ${networkFlag} ${CONFIG.IMAGE}`.replace(/\s+/g, ' ');
406
+ if (run(runCmd, { timeout: 30000 }) === null) {
407
+ console.log('');
408
+ fail(`Failed to create container.
409
+
410
+ Try manually:
411
+ ${cyan(runCmd)}`);
412
+ }
413
+ console.log(` ${green('done')}`);
414
+ }
415
+ console.log(` ${green('✓')} Toolkit container running (${bold(CONFIG.CONTAINER)})`);
416
+ }
417
+
418
+ // --- Step 4: Run detection ---
419
+ console.log(` ${dim('→')} Analyzing project...`);
420
+ const context = runDetection(CONFIG.PROJECT_DIR);
421
+
422
+ // --- Step 5: Create demon directory and prompt ---
423
+ const demonDir = path.dirname(CONFIG.PROMPT_DEST);
424
+ if (!fs.existsSync(demonDir)) {
425
+ fs.mkdirSync(demonDir, { recursive: true });
426
+ }
427
+
428
+ const prompt = generatePrompt(context);
429
+ fs.writeFileSync(CONFIG.PROMPT_DEST, prompt);
430
+ console.log(` ${green('✓')} Prompt installed to ${bold('~/.demon/EXECUTE.md')}`);
431
+
432
+ // --- Step 6: Print summary ---
433
+ console.log('');
434
+ console.log(bold(' Detected Configuration:'));
435
+ console.log(` Framework: ${cyan(context.framework || 'Unknown')}`);
436
+ console.log(` Language: ${cyan(context.language || 'JavaScript/TypeScript')}`);
437
+ console.log(` Test Runner: ${cyan(context.testRunner || 'Vitest')}`);
438
+ if (context.database) {
439
+ console.log(` Database: ${cyan(context.database.type)}`);
440
+ console.log(` Connection: ${cyan(context.database.connection)}`);
441
+ }
442
+ console.log(` Existing: ${cyan((context.existingTests || 0) + ' tests')}`);
443
+ console.log(` Target: ${cyan(context.target || 'http://localhost:3000')}`);
444
+ console.log('');
445
+
446
+ // --- Step 7: Print instructions ---
447
+ console.log(bold(' Ready!') + ' Open your AI agent from your project directory and paste:');
448
+ console.log('');
449
+ console.log(` ${cyan(`Read ~/.demon/EXECUTE.md and start the testing process`)}`);
450
+ console.log('');
451
+ console.log(dim(' Works with Claude Code, Cursor, Windsurf, Aider, Codex...'));
452
+ console.log('');
453
+ }
454
+
455
+ main();