jettypod 4.1.2 → 4.1.4
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/.nvmrc +1 -0
- package/docs/COMPLETE-TESTING-STRATEGY.md +970 -0
- package/docs/DECISIONS.md +10 -12
- package/docs/NODE_VERSION.md +83 -0
- package/docs/TDD-INFRASTRUCTURE-STRATEGY.md +1374 -0
- package/docs/TESTING-FOR-NON-ENGINEERS.md +1588 -0
- package/docs/TESTING-STRATEGY-AUDIT.md +698 -0
- package/hooks/post-checkout +17 -0
- package/hooks/post-merge +17 -0
- package/hooks/pre-commit +30 -0
- package/jettypod.js +259 -120
- package/lib/coverage-tracker.js +218 -0
- package/lib/database.js +2 -0
- package/lib/db-export.js +192 -0
- package/lib/db-import.js +193 -0
- package/lib/external-transition-handler.js +32 -0
- package/lib/git-hook-helpers.js +174 -0
- package/lib/git-root.js +90 -0
- package/lib/infrastructure-chore-generator.js +45 -0
- package/lib/install-hooks.js +52 -0
- package/lib/jettypod-backup.js +238 -0
- package/lib/merge-lock.js +193 -0
- package/lib/migrations/012-add-worktree-path.js +38 -0
- package/lib/migrations/013-worktrees-table.js +86 -0
- package/lib/migrations/014-migrate-worktree-data.js +161 -0
- package/lib/migrations/015-merge-locks-table.js +67 -0
- package/lib/pattern-finder.js +152 -0
- package/lib/process-manager.js +140 -0
- package/lib/production-standards-reader.js +13 -2
- package/lib/production-standards-writer.js +85 -0
- package/lib/skills/feature-planning/dry-run-validator.js +135 -0
- package/lib/skills/feature-planning/validation-formatter.js +160 -0
- package/lib/smart-conflict-detection.js +168 -0
- package/lib/smart-fetch-rebase.js +614 -0
- package/lib/step-definition-parser.js +76 -0
- package/lib/unit-test-generator.js +232 -0
- package/lib/verification-command-generator.js +66 -0
- package/lib/worktree-diagnostics.js +413 -0
- package/lib/worktree-facade.js +174 -0
- package/lib/worktree-manager.js +636 -0
- package/lib/worktree-reconciler.js +429 -0
- package/package.json +30 -3
- package/skills-templates/external-transition/SKILL.md +34 -3
- package/skills-templates/feature-planning/SKILL.md +190 -24
- package/skills-templates/production-mode/SKILL.md +127 -9
- package/skills-templates/speed-mode/SKILL.md +454 -51
- package/skills-templates/stable-mode/SKILL.md +285 -76
- package/.claude/PROTECT_SKILLS.md +0 -28
- package/.claude/settings.json +0 -24
- package/.claude/settings.local.json +0 -16
- package/.claude/skills/epic-planning/SKILL.md +0 -297
- package/.claude/skills/external-transition/SKILL.md +0 -384
- package/.claude/skills/feature-planning/SKILL.md +0 -464
- package/.claude/skills/production-mode/SKILL.md +0 -369
- package/.claude/skills/speed-mode/SKILL.md +0 -481
- package/.claude/skills/stable-mode/SKILL.md +0 -713
- package/.claude/skills.backup-2025-11-10T23-33-09-368Z/epic-planning/SKILL.md +0 -297
- package/.claude/skills.backup-2025-11-10T23-33-09-368Z/feature-planning/SKILL.md +0 -464
- package/.claude/skills.backup-2025-11-10T23-33-09-368Z/speed-mode/SKILL.md +0 -467
- package/.claude/skills.backup-2025-11-10T23-33-09-368Z/stable-mode/SKILL.md +0 -673
- package/.claude/skills.backup-2025-11-11T16-15-10-070Z/epic-discover/SKILL.md +0 -297
- package/.claude/skills.backup-2025-11-11T16-42-43-212Z/epic-planning/SKILL.md +0 -297
- package/.claude/skills.backup-2025-11-11T16-42-43-212Z/feature-planning/SKILL.md +0 -464
- package/.claude/skills.backup-2025-11-11T16-42-43-212Z/speed-mode/SKILL.md +0 -467
- package/.claude/skills.backup-2025-11-11T16-42-43-212Z/stable-mode/SKILL.md +0 -673
- package/.claude/skills.backup-2025-11-11T17-06-09-783Z/epic-planning/SKILL.md +0 -297
- package/.claude/skills.backup-2025-11-11T17-06-09-783Z/feature-planning/SKILL.md +0 -464
- package/.claude/skills.backup-2025-11-11T17-06-09-783Z/speed-mode/SKILL.md +0 -467
- package/.claude/skills.backup-2025-11-11T17-06-09-783Z/stable-mode/SKILL.md +0 -673
- package/.devpod/current-work.json +0 -10
- package/.devpod/work.db +0 -0
- package/.github/workflows/test-safety.yml +0 -85
- package/.jettypod/config.json +0 -5
- package/.jettypod/current-work.json +0 -10
- package/.jettypod/hooks/README.md +0 -77
- package/.jettypod/hooks/protect-claude-md.js +0 -338
- package/.jettypod/test-work.db +0 -0
- package/.jettypod/work.db +0 -0
- package/CLAUDE.md +0 -49
- package/SPEED-STABLE-AUDIT.md +0 -853
- package/SYSTEM-BEHAVIOR.md +0 -2199
- package/TEST_SAFETY_AUDIT.md +0 -314
- package/TEST_SAFETY_IMPLEMENTATION.md +0 -97
- package/cucumber-report.html +0 -45
- package/dist/devpod-linux +0 -0
- package/dist/devpod-macos +0 -0
- package/dist/devpod-win.exe +0 -0
- package/docs/features/jettypod-standards-explained.md +0 -543
- package/docs/features/standards-inventory.md +0 -257
- package/features/auto-generate-production-chores.feature +0 -13
- package/features/backlog-command.feature +0 -26
- package/features/backlog-filtering-production.feature +0 -10
- package/features/claude-md-protection/steps.js +0 -498
- package/features/decisions/index.js +0 -490
- package/features/decisions/index.test.js +0 -208
- package/features/fix-text-wrapping.feature +0 -42
- package/features/git-hooks/git-hooks.feature +0 -30
- package/features/git-hooks/index.js +0 -93
- package/features/git-hooks/index.test.js +0 -137
- package/features/git-hooks/post-commit +0 -56
- package/features/git-hooks/post-merge +0 -47
- package/features/git-hooks/pre-commit +0 -28
- package/features/git-hooks/simple-steps.js +0 -53
- package/features/git-hooks/simple-test.feature +0 -10
- package/features/git-hooks/steps.js +0 -196
- package/features/jettypod-update-command.feature +0 -46
- package/features/mode-prompts/index.js +0 -95
- package/features/mode-prompts/simple-steps.js +0 -44
- package/features/mode-prompts/simple-test.feature +0 -9
- package/features/mode-prompts/validation.test.js +0 -120
- package/features/multiple-claude-instances.feature +0 -121
- package/features/production-mode-skill.feature +0 -121
- package/features/refactor-mode/steps.js +0 -217
- package/features/refactor-mode.feature +0 -49
- package/features/simplify-external-transition.feature +0 -166
- package/features/skills-update/index.test.js +0 -216
- package/features/step_definitions/backlog-command.steps.js +0 -37
- package/features/step_definitions/fix-text-wrapping.steps.js +0 -271
- package/features/step_definitions/multiple-claude-instances.steps.js +0 -621
- package/features/step_definitions/production-mode-skill.steps.js +0 -862
- package/features/step_definitions/simplify-external-transition.steps.js +0 -370
- package/features/step_definitions/terminal-logo.steps.js +0 -145
- package/features/step_definitions/update-command.steps.js +0 -183
- package/features/support/hooks.js +0 -9
- package/features/terminal-logo/index.js +0 -39
- package/features/terminal-logo/terminal-logo.feature +0 -30
- package/features/update-command/index.js +0 -181
- package/features/update-command/index.test.js +0 -225
- package/features/work-commands/bug-workflow-display.feature +0 -22
- package/features/work-commands/index.js +0 -498
- package/features/work-commands/simple-steps.js +0 -69
- package/features/work-commands/stable-tests.feature +0 -57
- package/features/work-commands/steps.js +0 -1174
- package/features/work-commands/validation.test.js +0 -88
- package/features/work-commands/work-commands.feature +0 -13
- package/features/work-tracking/discovery-validation.test.js +0 -228
- package/features/work-tracking/index.js +0 -1921
- package/features/work-tracking/mode-required.feature +0 -112
- package/features/work-tracking/phase-tracking.test.js +0 -482
- package/features/work-tracking/prototype-tracking.test.js +0 -485
- package/features/work-tracking/tree-view.test.js +0 -310
- package/features/work-tracking/work-set-mode.feature +0 -71
- package/features/work-tracking/work-start-mode.feature +0 -88
- package/full-test.txt +0 -0
- package/lib/bug-workflow.test.js +0 -177
- package/lib/claudemd.test.js +0 -195
- package/lib/config.test.js +0 -511
- package/lib/constants.test.js +0 -164
- package/lib/current-work.test.js +0 -146
- package/lib/database-project-config.test.js +0 -111
- package/lib/database.test.js +0 -106
- package/lib/decisions-generator.test.js +0 -457
- package/lib/decisions-helpers.test.js +0 -310
- package/lib/git-coordinator.js +0 -167
- package/lib/git.test.js +0 -145
- package/lib/migrations/002-default-work-item-modes.test.js +0 -351
- package/lib/production-chore-generator.test.js +0 -432
- package/lib/production-context-detector.test.js +0 -277
- package/lib/production-scenario-appender.test.js +0 -235
- package/lib/production-scenario-validator.test.js +0 -246
- package/lib/production-standards-reader.test.js +0 -270
- package/lib/project-state.test.js +0 -92
- package/lib/push-queue.js +0 -417
- package/lib/queue-processor.js +0 -74
- package/lib/test-helpers.js +0 -202
- package/lib/test-helpers.test.js +0 -255
- package/prototypes/2025-01-11-production-mode-autonomous.js +0 -119
- package/prototypes/2025-01-11-production-mode-collaborative.js +0 -166
- package/prototypes/2025-01-11-production-mode-guided.js +0 -217
- package/prototypes/2025-01-11-production-mode-smart-context.js +0 -347
- package/prototypes/2025-01-11-production-standards-example.md +0 -204
- package/prototypes/2025-11-10-backlog-filtering-tree-aware.js +0 -242
- package/prototypes/test/index.html +0 -1
- package/setup-dist-repo.sh +0 -68
- package/test-production-standards-engine.js +0 -130
- package/test-results.json +0 -2195
- package/test-safety-check.sh +0 -80
- package/work-item-tracking-plan.md +0 -199
- /package/{.jettypod/devpod.db → jettypod.db} +0 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
const { exec } = require('child_process');
|
|
2
|
+
const { promisify } = require('util');
|
|
3
|
+
const execPromise = promisify(exec);
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Find all processes with open file handles in the specified directory
|
|
7
|
+
* @param {string} dirPath - Directory path to search for processes
|
|
8
|
+
* @returns {Promise<Array<{pid: number, name: string}>>} Array of process info
|
|
9
|
+
*/
|
|
10
|
+
async function findProcessesInDirectory(dirPath) {
|
|
11
|
+
const processes = [];
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
// Use lsof to find processes with file handles in the directory
|
|
15
|
+
// -t returns PIDs only, +D searches directory recursively
|
|
16
|
+
// 2>/dev/null suppresses errors, || true ensures command doesn't fail
|
|
17
|
+
const lsofCmd = `lsof +D "${dirPath}" -t 2>/dev/null || true`;
|
|
18
|
+
const { stdout } = await execPromise(lsofCmd);
|
|
19
|
+
|
|
20
|
+
const pids = stdout.trim().split('\n').filter(pid => pid);
|
|
21
|
+
|
|
22
|
+
// Get process names for each PID
|
|
23
|
+
for (const pid of pids) {
|
|
24
|
+
if (!pid) continue;
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const psCmd = `ps -p ${pid} -o comm= 2>/dev/null || echo "unknown"`;
|
|
28
|
+
const { stdout: name } = await execPromise(psCmd);
|
|
29
|
+
|
|
30
|
+
processes.push({
|
|
31
|
+
pid: parseInt(pid, 10),
|
|
32
|
+
name: name.trim()
|
|
33
|
+
});
|
|
34
|
+
} catch (err) {
|
|
35
|
+
// Process may have exited, skip it
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
} catch (err) {
|
|
40
|
+
// lsof command failed - log details and continue gracefully
|
|
41
|
+
console.warn('⚠️ Warning: Failed to detect processes in directory');
|
|
42
|
+
console.warn(` Directory: ${dirPath}`);
|
|
43
|
+
|
|
44
|
+
// Provide actionable error details
|
|
45
|
+
if (err.code === 'ENOENT') {
|
|
46
|
+
console.warn(' Reason: lsof command not found');
|
|
47
|
+
console.warn(' Solution: Install lsof (brew install lsof on macOS, apt-get install lsof on Linux)');
|
|
48
|
+
} else if (err.code === 'EACCES') {
|
|
49
|
+
console.warn(' Reason: Permission denied');
|
|
50
|
+
console.warn(' Solution: Run with appropriate permissions or check directory permissions');
|
|
51
|
+
} else if (err.killed) {
|
|
52
|
+
console.warn(' Reason: Command timed out');
|
|
53
|
+
console.warn(' Solution: Directory may have too many files or processes');
|
|
54
|
+
} else {
|
|
55
|
+
console.warn(` Reason: ${err.message}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
console.warn(' Continuing cleanup without process termination...');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return processes;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Terminate a process gracefully with SIGTERM, falling back to SIGKILL if needed
|
|
66
|
+
* @param {number} pid - Process ID to terminate
|
|
67
|
+
* @param {number} timeout - Maximum milliseconds to wait for graceful termination (default: 2000)
|
|
68
|
+
* @returns {Promise<{pid: number, name: string, method: string}>} Termination result
|
|
69
|
+
*/
|
|
70
|
+
async function terminateProcessGracefully(pid, timeout = 2000) {
|
|
71
|
+
let name = 'unknown';
|
|
72
|
+
|
|
73
|
+
// Get process name
|
|
74
|
+
try {
|
|
75
|
+
const { stdout } = await execPromise(`ps -p ${pid} -o comm= 2>/dev/null || echo "unknown"`);
|
|
76
|
+
name = stdout.trim();
|
|
77
|
+
} catch (err) {
|
|
78
|
+
// Process may have exited, name stays "unknown"
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Try graceful termination with SIGTERM
|
|
82
|
+
try {
|
|
83
|
+
process.kill(pid, 'SIGTERM');
|
|
84
|
+
} catch (err) {
|
|
85
|
+
// Handle termination errors
|
|
86
|
+
if (err.code === 'EPERM') {
|
|
87
|
+
console.warn(`⚠️ Warning: Permission denied when terminating process ${pid} (${name})`);
|
|
88
|
+
console.warn(' Reason: Insufficient permissions to terminate this process');
|
|
89
|
+
console.warn(' Solution: Run with elevated permissions (sudo) or check process ownership');
|
|
90
|
+
return { pid, name, method: 'permission-denied' };
|
|
91
|
+
} else if (err.code === 'ESRCH') {
|
|
92
|
+
// Process already dead or not found - this is fine
|
|
93
|
+
return { pid, name, method: 'already-terminated' };
|
|
94
|
+
} else {
|
|
95
|
+
console.warn(`⚠️ Warning: Failed to terminate process ${pid} (${name}): ${err.message}`);
|
|
96
|
+
return { pid, name, method: 'failed' };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Wait for process to exit gracefully
|
|
101
|
+
const startTime = Date.now();
|
|
102
|
+
while (Date.now() - startTime < timeout) {
|
|
103
|
+
try {
|
|
104
|
+
// Check if process still exists (kill with signal 0)
|
|
105
|
+
process.kill(pid, 0);
|
|
106
|
+
|
|
107
|
+
// Still alive, wait a bit longer
|
|
108
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
109
|
+
} catch (err) {
|
|
110
|
+
// Process is dead
|
|
111
|
+
return { pid, name, method: 'graceful' };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Timeout reached, force kill with SIGKILL
|
|
116
|
+
try {
|
|
117
|
+
process.kill(pid, 'SIGKILL');
|
|
118
|
+
} catch (err) {
|
|
119
|
+
// Handle force kill errors
|
|
120
|
+
if (err.code === 'EPERM') {
|
|
121
|
+
console.warn(`⚠️ Warning: Permission denied when force-killing process ${pid} (${name})`);
|
|
122
|
+
console.warn(' Reason: Insufficient permissions to force-kill this process');
|
|
123
|
+
console.warn(' Solution: Run with elevated permissions (sudo) or check process ownership');
|
|
124
|
+
return { pid, name, method: 'permission-denied' };
|
|
125
|
+
} else if (err.code === 'ESRCH') {
|
|
126
|
+
// Process exited during timeout - this is fine
|
|
127
|
+
return { pid, name, method: 'graceful' };
|
|
128
|
+
} else {
|
|
129
|
+
console.warn(`⚠️ Warning: Failed to force-kill process ${pid} (${name}): ${err.message}`);
|
|
130
|
+
return { pid, name, method: 'failed' };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { pid, name, method: 'force-killed' };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = {
|
|
138
|
+
findProcessesInDirectory,
|
|
139
|
+
terminateProcessGracefully
|
|
140
|
+
};
|
|
@@ -11,7 +11,7 @@ async function readStandards() {
|
|
|
11
11
|
|
|
12
12
|
// Check file exists
|
|
13
13
|
if (!fs.existsSync(standardsPath)) {
|
|
14
|
-
throw new Error('Production standards
|
|
14
|
+
throw new Error('Production standards not found');
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
// Read file with specific error handling
|
|
@@ -98,7 +98,18 @@ function validateStandardsFormat(standards) {
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
if (!standard.acceptance) {
|
|
101
|
-
throw new Error(`
|
|
101
|
+
throw new Error(`Invalid standard format: missing acceptance field`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Validate infrastructure-scoped standards have all required fields
|
|
105
|
+
if (standard.scope === 'infrastructure') {
|
|
106
|
+
if (!standard.reasoning) {
|
|
107
|
+
throw new Error(`Invalid standard format: missing reasoning field`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!standard.pattern) {
|
|
111
|
+
throw new Error(`Invalid standard format: missing pattern field`);
|
|
112
|
+
}
|
|
102
113
|
}
|
|
103
114
|
}
|
|
104
115
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Production standards writer - saves production standards to file
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { validateStandardsFormat } = require('./production-standards-reader');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Save production standards to file
|
|
8
|
+
* @param {Object} standards - Production standards object to save
|
|
9
|
+
* @returns {void}
|
|
10
|
+
*/
|
|
11
|
+
function saveStandards(standards) {
|
|
12
|
+
const standardsPath = path.join(process.cwd(), '.jettypod', 'production-standards.json');
|
|
13
|
+
|
|
14
|
+
// Validate standards format before saving
|
|
15
|
+
try {
|
|
16
|
+
validateStandardsFormat(standards);
|
|
17
|
+
} catch (validationErr) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
`Invalid production standards format:\n` +
|
|
20
|
+
` ${validationErr.message}\n` +
|
|
21
|
+
` File: ${standardsPath}`
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Ensure .jettypod directory exists
|
|
26
|
+
ensureJettypodDir();
|
|
27
|
+
|
|
28
|
+
// Write JSON with pretty formatting
|
|
29
|
+
try {
|
|
30
|
+
fs.writeFileSync(standardsPath, JSON.stringify(standards, null, 2), 'utf8');
|
|
31
|
+
} catch (writeErr) {
|
|
32
|
+
// Provide specific error messages based on error code
|
|
33
|
+
let errorMessage = `Failed to save production standards to ${standardsPath}:\n`;
|
|
34
|
+
|
|
35
|
+
if (writeErr.code === 'EACCES') {
|
|
36
|
+
errorMessage += ` Permission denied. Check file permissions:\n`;
|
|
37
|
+
errorMessage += ` chmod 644 ${standardsPath}`;
|
|
38
|
+
} else if (writeErr.code === 'ENOSPC') {
|
|
39
|
+
errorMessage += ` No space left on device. Free up disk space and try again.`;
|
|
40
|
+
} else if (writeErr.code === 'EROFS') {
|
|
41
|
+
errorMessage += ` File system is read-only. Check mount permissions.`;
|
|
42
|
+
} else {
|
|
43
|
+
errorMessage += ` ${writeErr.message}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
throw new Error(errorMessage);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Ensure .jettypod directory exists
|
|
52
|
+
* @returns {void}
|
|
53
|
+
*/
|
|
54
|
+
function ensureJettypodDir() {
|
|
55
|
+
const jettypodDir = path.join(process.cwd(), '.jettypod');
|
|
56
|
+
|
|
57
|
+
if (!fs.existsSync(jettypodDir)) {
|
|
58
|
+
try {
|
|
59
|
+
fs.mkdirSync(jettypodDir, { recursive: true });
|
|
60
|
+
} catch (mkdirErr) {
|
|
61
|
+
// Provide specific error messages based on error code
|
|
62
|
+
let errorMessage = `Failed to create .jettypod directory at ${jettypodDir}:\n`;
|
|
63
|
+
|
|
64
|
+
if (mkdirErr.code === 'EACCES') {
|
|
65
|
+
errorMessage += ` Permission denied. Check parent directory permissions:\n`;
|
|
66
|
+
errorMessage += ` chmod 755 ${path.dirname(jettypodDir)}`;
|
|
67
|
+
} else if (mkdirErr.code === 'ENOSPC') {
|
|
68
|
+
errorMessage += ` No space left on device. Free up disk space and try again.`;
|
|
69
|
+
} else if (mkdirErr.code === 'EROFS') {
|
|
70
|
+
errorMessage += ` File system is read-only. Check mount permissions.`;
|
|
71
|
+
} else if (mkdirErr.code === 'EEXIST') {
|
|
72
|
+
errorMessage += ` Path exists but is not a directory. Remove the file:\n`;
|
|
73
|
+
errorMessage += ` rm ${jettypodDir}`;
|
|
74
|
+
} else {
|
|
75
|
+
errorMessage += ` ${mkdirErr.message}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
throw new Error(errorMessage);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = {
|
|
84
|
+
saveStandards
|
|
85
|
+
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
const { execSync } = require('child_process');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Validates BDD infrastructure by running cucumber dry-run
|
|
6
|
+
* @param {string} featureFilePath - Absolute path to .feature file
|
|
7
|
+
* @returns {Promise<{success: boolean, errors: Array, warnings: Array}>}
|
|
8
|
+
*/
|
|
9
|
+
async function validateBddInfrastructure(featureFilePath) {
|
|
10
|
+
const result = {
|
|
11
|
+
success: false,
|
|
12
|
+
errors: [],
|
|
13
|
+
warnings: []
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
// Run cucumber-js with --dry-run flag
|
|
18
|
+
const output = execSync(
|
|
19
|
+
`npx cucumber-js --dry-run "${featureFilePath}"`,
|
|
20
|
+
{
|
|
21
|
+
cwd: path.dirname(path.dirname(path.dirname(featureFilePath))),
|
|
22
|
+
encoding: 'utf-8',
|
|
23
|
+
timeout: 5000,
|
|
24
|
+
stdio: 'pipe'
|
|
25
|
+
}
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
// If we get here, dry-run passed
|
|
29
|
+
result.success = true;
|
|
30
|
+
|
|
31
|
+
// Check for warnings in output (like deprecation notices)
|
|
32
|
+
if (output.includes('deprecated') || output.includes('warning')) {
|
|
33
|
+
result.warnings.push({
|
|
34
|
+
type: 'deprecation',
|
|
35
|
+
message: output.match(/warning.*/gi)?.[0] || 'Deprecation warning found'
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
} catch (error) {
|
|
40
|
+
// Dry-run failed - parse the error output
|
|
41
|
+
const errorOutput = error.stderr || error.stdout || error.message;
|
|
42
|
+
|
|
43
|
+
result.success = false;
|
|
44
|
+
result.errors = parseValidationErrors(errorOutput, featureFilePath);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Parses cucumber dry-run error output to extract specific issues
|
|
52
|
+
* @param {string} errorOutput - stderr/stdout from cucumber-js
|
|
53
|
+
* @param {string} featureFilePath - Path to feature file for context
|
|
54
|
+
* @returns {Array<{type: string, message: string, details: object}>}
|
|
55
|
+
*/
|
|
56
|
+
function parseValidationErrors(errorOutput, featureFilePath) {
|
|
57
|
+
const errors = [];
|
|
58
|
+
|
|
59
|
+
// Check for undefined steps
|
|
60
|
+
const undefinedStepsMatch = errorOutput.match(/Undefined\. Implement with the following snippet:([\s\S]*?)(?=\n\n|\nScenario:|\nFeature:|$)/g);
|
|
61
|
+
if (undefinedStepsMatch) {
|
|
62
|
+
undefinedStepsMatch.forEach(match => {
|
|
63
|
+
const stepText = match.match(/(?:Given|When|Then|And|But)\('(.+?)'/)?.[1];
|
|
64
|
+
if (stepText) {
|
|
65
|
+
errors.push({
|
|
66
|
+
type: 'undefined_step',
|
|
67
|
+
message: `Undefined step: "${stepText}"`,
|
|
68
|
+
details: {
|
|
69
|
+
stepText,
|
|
70
|
+
file: featureFilePath,
|
|
71
|
+
suggestedImplementation: match
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Check for syntax errors
|
|
79
|
+
const syntaxErrorMatch = errorOutput.match(/Parse error[^\n]*\n[^\n]*/);
|
|
80
|
+
if (syntaxErrorMatch) {
|
|
81
|
+
errors.push({
|
|
82
|
+
type: 'syntax_error',
|
|
83
|
+
message: syntaxErrorMatch[0],
|
|
84
|
+
details: {
|
|
85
|
+
file: featureFilePath
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Check for duplicate step definitions
|
|
91
|
+
const duplicateMatch = errorOutput.match(/Multiple step definitions match:([\s\S]*?)(?=\n\n|$)/g);
|
|
92
|
+
if (duplicateMatch) {
|
|
93
|
+
duplicateMatch.forEach(match => {
|
|
94
|
+
const stepFiles = match.match(/features\/[^\s]+\.js:\d+/g) || [];
|
|
95
|
+
errors.push({
|
|
96
|
+
type: 'duplicate_step',
|
|
97
|
+
message: 'Multiple step definitions match the same pattern',
|
|
98
|
+
details: {
|
|
99
|
+
files: stepFiles,
|
|
100
|
+
fullMessage: match
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Check for missing cucumber installation
|
|
107
|
+
if (errorOutput.includes('cucumber-js: command not found') ||
|
|
108
|
+
errorOutput.includes('Cannot find module')) {
|
|
109
|
+
errors.push({
|
|
110
|
+
type: 'missing_dependency',
|
|
111
|
+
message: 'Cucumber is not installed or not found',
|
|
112
|
+
details: {
|
|
113
|
+
suggestion: 'Run: npm install --save-dev @cucumber/cucumber'
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// If no specific errors found but validation failed, add generic error
|
|
119
|
+
if (errors.length === 0) {
|
|
120
|
+
errors.push({
|
|
121
|
+
type: 'unknown',
|
|
122
|
+
message: 'Validation failed with unknown error',
|
|
123
|
+
details: {
|
|
124
|
+
output: errorOutput.substring(0, 500)
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return errors;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
module.exports = {
|
|
133
|
+
validateBddInfrastructure,
|
|
134
|
+
parseValidationErrors
|
|
135
|
+
};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formats dry-run validation results with colors and actionable guidance
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Formats validation result for display
|
|
9
|
+
* @param {object} result - Result from validateBddInfrastructure()
|
|
10
|
+
* @param {string} featurePath - Path to feature file being validated
|
|
11
|
+
* @returns {string} Formatted output
|
|
12
|
+
*/
|
|
13
|
+
function formatValidationResult(result, featurePath) {
|
|
14
|
+
const lines = [];
|
|
15
|
+
|
|
16
|
+
// Always show the validation header first
|
|
17
|
+
lines.push(chalk.cyan('🧪 Validating BDD infrastructure...'));
|
|
18
|
+
lines.push('');
|
|
19
|
+
|
|
20
|
+
if (result.success) {
|
|
21
|
+
lines.push(chalk.green('✅ BDD infrastructure validated'));
|
|
22
|
+
lines.push(chalk.green('✅ Step definitions match scenario steps'));
|
|
23
|
+
|
|
24
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
25
|
+
lines.push('');
|
|
26
|
+
lines.push(chalk.yellow('⚠️ Warnings:'));
|
|
27
|
+
result.warnings.forEach(warning => {
|
|
28
|
+
lines.push(chalk.yellow(` • ${warning.message}`));
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
} else {
|
|
32
|
+
lines.push(chalk.red('❌ BDD infrastructure validation failed'));
|
|
33
|
+
lines.push('');
|
|
34
|
+
|
|
35
|
+
// Check error types to provide specific messaging
|
|
36
|
+
const hasUndefinedSteps = result.errors.some(e => e.type === 'undefined_step');
|
|
37
|
+
const hasSyntaxErrors = result.errors.some(e => e.type === 'syntax_error');
|
|
38
|
+
|
|
39
|
+
if (hasUndefinedSteps) {
|
|
40
|
+
lines.push(chalk.red('❌ Undefined steps detected:'));
|
|
41
|
+
lines.push('');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (result.errors && result.errors.length > 0) {
|
|
45
|
+
result.errors.forEach(error => {
|
|
46
|
+
const formatted = formatError(error, featurePath);
|
|
47
|
+
lines.push(formatted);
|
|
48
|
+
lines.push('');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Provide specific guidance based on error types
|
|
52
|
+
if (hasUndefinedSteps) {
|
|
53
|
+
lines.push(chalk.yellow('⚠️ Fix step definitions before proceeding'));
|
|
54
|
+
} else if (hasSyntaxErrors) {
|
|
55
|
+
lines.push(chalk.yellow('⚠️ Fix the feature file before proceeding'));
|
|
56
|
+
} else {
|
|
57
|
+
lines.push(chalk.yellow('⚠️ Fix the issues above before proceeding to implementation.'));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return lines.join('\n');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Formats a single validation error with guidance
|
|
67
|
+
* @param {object} error - Error object from parseValidationErrors()
|
|
68
|
+
* @param {string} featurePath - Path to feature file
|
|
69
|
+
* @returns {string} Formatted error message
|
|
70
|
+
*/
|
|
71
|
+
function formatError(error, featurePath) {
|
|
72
|
+
const lines = [];
|
|
73
|
+
|
|
74
|
+
switch (error.type) {
|
|
75
|
+
case 'undefined_step':
|
|
76
|
+
lines.push(chalk.red(` Undefined step: ${chalk.bold(error.details.stepText)}`));
|
|
77
|
+
lines.push(chalk.gray(` File: ${error.details.file}`));
|
|
78
|
+
lines.push('');
|
|
79
|
+
lines.push(chalk.cyan(' 💡 Add this step definition:'));
|
|
80
|
+
lines.push(chalk.gray(indent(extractStepDefinition(error.details.suggestedImplementation), 5)));
|
|
81
|
+
break;
|
|
82
|
+
|
|
83
|
+
case 'syntax_error':
|
|
84
|
+
lines.push(chalk.red(` Syntax error in feature file`));
|
|
85
|
+
lines.push(chalk.gray(` ${error.message}`));
|
|
86
|
+
lines.push('');
|
|
87
|
+
lines.push(chalk.cyan(' 💡 Check your Gherkin syntax:'));
|
|
88
|
+
lines.push(chalk.gray(' • Ensure proper indentation'));
|
|
89
|
+
lines.push(chalk.gray(' • Verify Feature/Scenario keywords are correctly placed'));
|
|
90
|
+
lines.push(chalk.gray(' • Check for missing colons after keywords'));
|
|
91
|
+
break;
|
|
92
|
+
|
|
93
|
+
case 'duplicate_step':
|
|
94
|
+
lines.push(chalk.red(` Duplicate step definitions detected`));
|
|
95
|
+
lines.push(chalk.gray(` ${error.message}`));
|
|
96
|
+
if (error.details.files && error.details.files.length > 0) {
|
|
97
|
+
lines.push('');
|
|
98
|
+
lines.push(chalk.cyan(' 💡 Found in these files:'));
|
|
99
|
+
error.details.files.forEach(file => {
|
|
100
|
+
lines.push(chalk.gray(` • ${file}`));
|
|
101
|
+
});
|
|
102
|
+
lines.push('');
|
|
103
|
+
lines.push(chalk.gray(' Remove or rename one of the duplicate definitions'));
|
|
104
|
+
}
|
|
105
|
+
break;
|
|
106
|
+
|
|
107
|
+
case 'missing_dependency':
|
|
108
|
+
lines.push(chalk.red(` Missing dependency: Cucumber`));
|
|
109
|
+
lines.push(chalk.gray(` ${error.message}`));
|
|
110
|
+
lines.push('');
|
|
111
|
+
lines.push(chalk.cyan(' 💡 Install Cucumber:'));
|
|
112
|
+
lines.push(chalk.gray(` ${error.details.suggestion}`));
|
|
113
|
+
break;
|
|
114
|
+
|
|
115
|
+
case 'unknown':
|
|
116
|
+
default:
|
|
117
|
+
lines.push(chalk.red(` Validation failed: ${error.message}`));
|
|
118
|
+
if (error.details && error.details.output) {
|
|
119
|
+
lines.push('');
|
|
120
|
+
lines.push(chalk.gray(' Error output:'));
|
|
121
|
+
lines.push(chalk.gray(indent(error.details.output, 5)));
|
|
122
|
+
}
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return lines.join('\n');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Extracts step definition code from cucumber suggestion
|
|
131
|
+
* @param {string} suggestion - Cucumber's suggested implementation
|
|
132
|
+
* @returns {string} Clean step definition code
|
|
133
|
+
*/
|
|
134
|
+
function extractStepDefinition(suggestion) {
|
|
135
|
+
// Extract the step definition code (everything after "Implement with the following snippet:")
|
|
136
|
+
const match = suggestion.match(/(?:Given|When|Then|And|But)\([^)]+\)[^{]*\{[^}]*\}/);
|
|
137
|
+
if (match) {
|
|
138
|
+
return match[0];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Fallback: return first few lines of the suggestion
|
|
142
|
+
const lines = suggestion.split('\n').slice(1, 4);
|
|
143
|
+
return lines.join('\n');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Indents text by a given number of spaces
|
|
148
|
+
* @param {string} text - Text to indent
|
|
149
|
+
* @param {number} spaces - Number of spaces to indent
|
|
150
|
+
* @returns {string} Indented text
|
|
151
|
+
*/
|
|
152
|
+
function indent(text, spaces) {
|
|
153
|
+
const prefix = ' '.repeat(spaces);
|
|
154
|
+
return text.split('\n').map(line => prefix + line).join('\n');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
module.exports = {
|
|
158
|
+
formatValidationResult,
|
|
159
|
+
formatError
|
|
160
|
+
};
|