jettypod 3.0.1

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.
Files changed (122) hide show
  1. package/.claude/PROTECT_SKILLS.md +28 -0
  2. package/.claude/settings.json +24 -0
  3. package/.claude/settings.local.json +16 -0
  4. package/.claude/skills/epic-discover/SKILL.md +262 -0
  5. package/.claude/skills/feature-discover/SKILL.md +393 -0
  6. package/.claude/skills/speed-mode/SKILL.md +364 -0
  7. package/.claude/skills/stable-mode/SKILL.md +591 -0
  8. package/.github/workflows/test-safety.yml +85 -0
  9. package/README.md +25 -0
  10. package/SPEED-STABLE-AUDIT.md +853 -0
  11. package/SYSTEM-BEHAVIOR.md +1241 -0
  12. package/TEST_SAFETY_AUDIT.md +314 -0
  13. package/TEST_SAFETY_IMPLEMENTATION.md +97 -0
  14. package/cucumber.js +8 -0
  15. package/docs/COMMAND_REFERENCE.md +903 -0
  16. package/docs/DECISIONS.md +68 -0
  17. package/docs/README.md +48 -0
  18. package/docs/STANDARDS-SYSTEM-DOCUMENTATION.md +374 -0
  19. package/docs/TEST-REWRITE-PLAN.md +261 -0
  20. package/docs/ai-test-writing-requirements.md +219 -0
  21. package/docs/claude-code-skills.md +607 -0
  22. package/docs/core-jettypod-methodology/comprehensive-jettypod-methodology.md +582 -0
  23. package/docs/core-jettypod-methodology/deprecated/jettypod-comprehensive-standards.md +1222 -0
  24. package/docs/core-jettypod-methodology/deprecated/jettypod-operating-guide.md +3399 -0
  25. package/docs/core-jettypod-methodology/deprecated/jettypod-technical-checklist.md +1325 -0
  26. package/docs/core-jettypod-methodology/deprecated/jettypod-vibe-coding-framework.md +1544 -0
  27. package/docs/core-jettypod-methodology/deprecated/prompt-engineering-guide.md +320 -0
  28. package/docs/core-jettypod-methodology/deprecated/vibe-coding-cheatsheet (1).md +516 -0
  29. package/docs/core-jettypod-methodology/deprecated/vibe-coding-framework.md +1544 -0
  30. package/docs/features/jettypod-standards-explained.md +543 -0
  31. package/docs/features/standards-inventory.md +257 -0
  32. package/docs/gap-analysis-current-vs-comprehensive-methodology.md +939 -0
  33. package/docs/jettypod-system-overview.md +409 -0
  34. package/features/auto-generate-production-chores.feature +14 -0
  35. package/features/claude-md-protection/steps.js +487 -0
  36. package/features/decisions/index.js +490 -0
  37. package/features/decisions/index.test.js +208 -0
  38. package/features/git-hooks/git-hooks.feature +30 -0
  39. package/features/git-hooks/index.js +93 -0
  40. package/features/git-hooks/index.test.js +137 -0
  41. package/features/git-hooks/post-commit +56 -0
  42. package/features/git-hooks/post-merge +47 -0
  43. package/features/git-hooks/pre-commit +28 -0
  44. package/features/git-hooks/simple-steps.js +53 -0
  45. package/features/git-hooks/simple-test.feature +10 -0
  46. package/features/git-hooks/steps.js +196 -0
  47. package/features/jettypod-update-command.feature +46 -0
  48. package/features/mode-prompts/index.js +95 -0
  49. package/features/mode-prompts/simple-steps.js +44 -0
  50. package/features/mode-prompts/simple-test.feature +9 -0
  51. package/features/mode-prompts/validation.test.js +120 -0
  52. package/features/refactor-mode/steps.js +217 -0
  53. package/features/refactor-mode.feature +49 -0
  54. package/features/skills-update/index.test.js +216 -0
  55. package/features/step_definitions/auto-generate-production-chores.steps.js +162 -0
  56. package/features/step_definitions/terminal-logo.steps.js +145 -0
  57. package/features/step_definitions/update-command.steps.js +183 -0
  58. package/features/terminal-logo/index.js +39 -0
  59. package/features/terminal-logo/terminal-logo.feature +30 -0
  60. package/features/update-command/index.js +181 -0
  61. package/features/update-command/index.test.js +225 -0
  62. package/features/work-commands/bug-workflow-display.feature +22 -0
  63. package/features/work-commands/index.js +311 -0
  64. package/features/work-commands/simple-steps.js +69 -0
  65. package/features/work-commands/stable-tests.feature +57 -0
  66. package/features/work-commands/steps.js +1120 -0
  67. package/features/work-commands/validation.test.js +88 -0
  68. package/features/work-commands/work-commands.feature +13 -0
  69. package/features/work-tracking/discovery-validation.test.js +228 -0
  70. package/features/work-tracking/index.js +1511 -0
  71. package/features/work-tracking/mode-required.feature +112 -0
  72. package/features/work-tracking/phase-tracking.test.js +482 -0
  73. package/features/work-tracking/prototype-tracking.test.js +485 -0
  74. package/features/work-tracking/tree-view.test.js +310 -0
  75. package/features/work-tracking/work-set-mode.feature +71 -0
  76. package/features/work-tracking/work-start-mode.feature +88 -0
  77. package/full-test.txt +0 -0
  78. package/install.sh +89 -0
  79. package/jettypod.js +1640 -0
  80. package/lib/bug-workflow.js +94 -0
  81. package/lib/bug-workflow.test.js +177 -0
  82. package/lib/claudemd.js +130 -0
  83. package/lib/claudemd.test.js +195 -0
  84. package/lib/comprehensive-standards-full.json +1778 -0
  85. package/lib/config.js +181 -0
  86. package/lib/config.test.js +511 -0
  87. package/lib/constants.js +107 -0
  88. package/lib/constants.test.js +164 -0
  89. package/lib/current-work.js +130 -0
  90. package/lib/current-work.test.js +146 -0
  91. package/lib/database-project-config.test.js +107 -0
  92. package/lib/database.js +256 -0
  93. package/lib/database.test.js +106 -0
  94. package/lib/decisions-generator.js +102 -0
  95. package/lib/decisions-generator.test.js +457 -0
  96. package/lib/decisions-helpers.js +119 -0
  97. package/lib/decisions-helpers.test.js +310 -0
  98. package/lib/discovery-checkpoint.js +83 -0
  99. package/lib/docs-generator.js +280 -0
  100. package/lib/external-checklist.js +177 -0
  101. package/lib/git.js +142 -0
  102. package/lib/git.test.js +145 -0
  103. package/lib/logo.js +3 -0
  104. package/lib/migrations/001-epic-to-parent.js +24 -0
  105. package/lib/migrations/002-default-work-item-modes.js +37 -0
  106. package/lib/migrations/002-default-work-item-modes.test.js +351 -0
  107. package/lib/migrations/003-epic-discovery-fields.js +52 -0
  108. package/lib/migrations/004-discovery-decisions-table.js +32 -0
  109. package/lib/migrations/005-migrate-decision-data.js +62 -0
  110. package/lib/migrations/006-feature-phase-field.js +61 -0
  111. package/lib/migrations/007-prototype-tracking.js +38 -0
  112. package/lib/migrations/008-scenario-file-field.js +24 -0
  113. package/lib/migrations/index.js +74 -0
  114. package/lib/production-helpers.js +69 -0
  115. package/lib/project-state.test.js +92 -0
  116. package/lib/test-helpers.js +184 -0
  117. package/lib/test-helpers.test.js +255 -0
  118. package/package.json +36 -0
  119. package/prototypes/test/index.html +1 -0
  120. package/setup-dist-repo.sh +68 -0
  121. package/test-safety-check.sh +80 -0
  122. package/work-item-tracking-plan.md +199 -0
@@ -0,0 +1,69 @@
1
+ // Production readiness helpers
2
+ const { getDb } = require('./database');
3
+
4
+ /**
5
+ * Get all features that are in stable mode and ready for production elevation
6
+ * @returns {Promise<Array>} Array of feature objects with id, title, description, etc.
7
+ */
8
+ function getStableFeatures() {
9
+ const db = getDb();
10
+
11
+ return new Promise((resolve, reject) => {
12
+ db.all(`
13
+ SELECT *
14
+ FROM work_items
15
+ WHERE type = 'feature'
16
+ AND mode = 'stable'
17
+ AND phase = 'implementation'
18
+ ORDER BY id ASC
19
+ `, [], (err, rows) => {
20
+ if (err) {
21
+ return reject(new Error(`Failed to fetch stable features: ${err.message}`));
22
+ }
23
+ resolve(rows || []);
24
+ });
25
+ });
26
+ }
27
+
28
+ /**
29
+ * Create production chores for a given feature
30
+ * @param {Object} feature - Feature object with id and title
31
+ * @param {number} epicId - Epic ID to group chores under
32
+ * @returns {Promise<Array>} Array of created chore IDs
33
+ */
34
+ async function createProductionChoresForFeature(feature, epicId) {
35
+ const { create } = require('../features/work-tracking');
36
+
37
+ const chores = [
38
+ {
39
+ title: `Add security hardening to ${feature.title}`,
40
+ description: `Add authentication/authorization checks, input sanitization, encryption, and attack protection (XSS, CSRF, injection) for ${feature.title}`
41
+ },
42
+ {
43
+ title: `Add scale testing to ${feature.title}`,
44
+ description: `Add performance optimization, load testing (100+ concurrent users), caching strategy, and monitoring for ${feature.title}`
45
+ },
46
+ {
47
+ title: `Add compliance requirements to ${feature.title}`,
48
+ description: `Add GDPR/HIPAA/SOC2 compliance, audit trails, data retention policies, and regulatory requirements for ${feature.title}`
49
+ }
50
+ ];
51
+
52
+ // Create all chores (chores don't have modes - they inherit context from parent)
53
+ const choreIds = [];
54
+ for (const chore of chores) {
55
+ try {
56
+ const choreId = await create('chore', chore.title, chore.description, epicId, null, false);
57
+ choreIds.push(choreId);
58
+ } catch (err) {
59
+ throw new Error(`Failed to create production chore: ${err.message}`);
60
+ }
61
+ }
62
+
63
+ return choreIds;
64
+ }
65
+
66
+ module.exports = {
67
+ getStableFeatures,
68
+ createProductionChoresForFeature
69
+ };
@@ -0,0 +1,92 @@
1
+ const config = require('./config');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ describe('Project State', () => {
6
+ const testDir = path.join('/tmp', 'jettypod-project-state-test-' + Date.now());
7
+ const testConfigPath = path.join(testDir, 'config.json');
8
+ const originalPath = config.path;
9
+
10
+ beforeEach(() => {
11
+ config.path = testConfigPath;
12
+ // SAFETY: Only delete if testDir is in /tmp
13
+ if (fs.existsSync(testDir) && testDir.startsWith('/tmp/')) {
14
+ fs.rmSync(testDir, { recursive: true });
15
+ }
16
+ fs.mkdirSync(testDir, { recursive: true });
17
+ });
18
+
19
+ afterEach(() => {
20
+ config.path = originalPath;
21
+ // SAFETY: Only delete if testDir is in /tmp
22
+ if (fs.existsSync(testDir) && testDir.startsWith('/tmp/')) {
23
+ fs.rmSync(testDir, { recursive: true });
24
+ }
25
+ });
26
+
27
+ describe('isValidProjectState', () => {
28
+ it('should accept "internal" as valid', () => {
29
+ expect(config.isValidProjectState('internal')).toBe(true);
30
+ });
31
+
32
+ it('should accept "external" as valid', () => {
33
+ expect(config.isValidProjectState('external')).toBe(true);
34
+ });
35
+
36
+ it('should reject invalid values', () => {
37
+ expect(config.isValidProjectState('production')).toBe(false);
38
+ expect(config.isValidProjectState('invalid')).toBe(false);
39
+ expect(config.isValidProjectState('')).toBe(false);
40
+ });
41
+ });
42
+
43
+ describe('read', () => {
44
+ it('should default project_state to "internal" if not exists', () => {
45
+ const data = config.read();
46
+ expect(data.project_state).toBe('internal');
47
+ });
48
+
49
+ it('should default project_state to "internal" if invalid value in file', () => {
50
+ fs.mkdirSync(testDir, { recursive: true });
51
+ fs.writeFileSync(testConfigPath, JSON.stringify({ name: 'test', project_state: 'invalid' }));
52
+
53
+ const data = config.read();
54
+ expect(data.project_state).toBe('internal');
55
+ });
56
+
57
+ it('should preserve valid project_state from file', () => {
58
+ fs.mkdirSync(testDir, { recursive: true });
59
+ fs.writeFileSync(testConfigPath, JSON.stringify({ name: 'test', project_state: 'external' }));
60
+
61
+ const data = config.read();
62
+ expect(data.project_state).toBe('external');
63
+ });
64
+ });
65
+
66
+ describe('update', () => {
67
+ it('should update project_state to external', () => {
68
+ config.write({ name: 'test', project_state: 'internal' });
69
+ config.update({ project_state: 'external' });
70
+
71
+ const data = config.read();
72
+ expect(data.project_state).toBe('external');
73
+ });
74
+
75
+ it('should reject invalid project_state', () => {
76
+ config.write({ name: 'test', project_state: 'internal' });
77
+
78
+ expect(() => {
79
+ config.update({ project_state: 'invalid' });
80
+ }).toThrow('Invalid project_state: invalid');
81
+ });
82
+
83
+ it('should allow updating other fields without affecting project_state', () => {
84
+ config.write({ name: 'test', project_state: 'internal' });
85
+ config.update({ description: 'Test project' });
86
+
87
+ const data = config.read();
88
+ expect(data.project_state).toBe('internal');
89
+ expect(data.description).toBe('Test project');
90
+ });
91
+ });
92
+ });
@@ -0,0 +1,184 @@
1
+ const { execSync } = require('child_process');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const os = require('os');
5
+
6
+ /**
7
+ * Creates an isolated test directory for testing
8
+ * @returns {Object} Test context with testDir and originalCwd
9
+ * @throws {Error} If temporary directory cannot be created
10
+ */
11
+ function createTestEnvironment() {
12
+ const originalCwd = process.cwd();
13
+
14
+ let testDir;
15
+ try {
16
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jettypod-test-'));
17
+ } catch (err) {
18
+ throw new Error(`Failed to create test directory: ${err.message}`);
19
+ }
20
+
21
+ return {
22
+ originalCwd,
23
+ testDir,
24
+ cleanup: () => {
25
+ try {
26
+ process.chdir(originalCwd);
27
+ } catch (err) {
28
+ // Directory may have been deleted, ignore
29
+ }
30
+ if (fs.existsSync(testDir)) {
31
+ try {
32
+ fs.rmSync(testDir, { recursive: true, force: true });
33
+ } catch (err) {
34
+ console.warn(`Warning: Failed to cleanup test directory: ${err.message}`);
35
+ }
36
+ }
37
+ }
38
+ };
39
+ }
40
+
41
+ /**
42
+ * Copies directory recursively, excluding test files
43
+ * @param {string} src - Source directory
44
+ * @param {string} dest - Destination directory
45
+ * @throws {Error} If src doesn't exist, is not a directory, or copy fails
46
+ */
47
+ function copyDirRecursive(src, dest) {
48
+ if (!src || typeof src !== 'string') {
49
+ throw new Error('Source path must be a non-empty string');
50
+ }
51
+ if (!dest || typeof dest !== 'string') {
52
+ throw new Error('Destination path must be a non-empty string');
53
+ }
54
+
55
+ if (!fs.existsSync(src)) {
56
+ throw new Error(`Source directory does not exist: ${src}`);
57
+ }
58
+
59
+ const srcStat = fs.statSync(src);
60
+ if (!srcStat.isDirectory()) {
61
+ throw new Error(`Source is not a directory: ${src}`);
62
+ }
63
+
64
+ try {
65
+ if (!fs.existsSync(dest)) {
66
+ fs.mkdirSync(dest, { recursive: true });
67
+ }
68
+
69
+ fs.readdirSync(src).forEach(item => {
70
+ const srcPath = path.join(src, item);
71
+ const destPath = path.join(dest, item);
72
+
73
+ if (fs.statSync(srcPath).isDirectory()) {
74
+ copyDirRecursive(srcPath, destPath);
75
+ } else {
76
+ // Skip test files when copying
77
+ if (!item.endsWith('.test.js')) {
78
+ fs.copyFileSync(srcPath, destPath);
79
+ }
80
+ }
81
+ });
82
+ } catch (err) {
83
+ throw new Error(`Failed to copy directory from ${src} to ${dest}: ${err.message}`);
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Runs a JettyPod command in test environment
89
+ * @param {string} cmd - Command to run (e.g., 'jettypod init')
90
+ * @param {Object} context - Test context with originalCwd
91
+ * @returns {string} Command output
92
+ * @throws {Error} If cmd is invalid or context is missing
93
+ */
94
+ function runCommand(cmd, context) {
95
+ if (!cmd || typeof cmd !== 'string') {
96
+ throw new Error('Command must be a non-empty string');
97
+ }
98
+ if (!context || typeof context !== 'object') {
99
+ throw new Error('Context must be an object');
100
+ }
101
+
102
+ try {
103
+ const productionJettypodPath = context.originalCwd ?
104
+ path.join(context.originalCwd, 'jettypod.js') :
105
+ path.join(process.cwd(), '..', '..', 'jettypod.js');
106
+
107
+ if (!fs.existsSync(productionJettypodPath)) {
108
+ throw new Error(`JettyPod script not found at: ${productionJettypodPath}`);
109
+ }
110
+
111
+ const fullCmd = cmd.replace(/^jettypod/, `node ${productionJettypodPath}`);
112
+ return execSync(fullCmd, {
113
+ encoding: 'utf-8',
114
+ stdio: 'pipe'
115
+ });
116
+ } catch (error) {
117
+ return error.stdout || error.message;
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Reads config.json from .jettypod directory
123
+ * @returns {Object|null} Config object or null if not found
124
+ * @throws {Error} If JSON is corrupted or file cannot be read
125
+ */
126
+ function readConfig() {
127
+ const configPath = '.jettypod/config.json';
128
+
129
+ if (!fs.existsSync(configPath)) {
130
+ return null;
131
+ }
132
+
133
+ try {
134
+ const content = fs.readFileSync(configPath, 'utf-8');
135
+ return JSON.parse(content);
136
+ } catch (err) {
137
+ if (err instanceof SyntaxError) {
138
+ throw new Error(`Config file contains invalid JSON: ${err.message}`);
139
+ }
140
+ throw new Error(`Failed to read config file: ${err.message}`);
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Reads CLAUDE.md from current directory
146
+ * @returns {string} CLAUDE.md content or empty string
147
+ * @throws {Error} If file exists but cannot be read
148
+ */
149
+ function readClaude() {
150
+ if (!fs.existsSync('CLAUDE.md')) {
151
+ return '';
152
+ }
153
+
154
+ try {
155
+ return fs.readFileSync('CLAUDE.md', 'utf-8');
156
+ } catch (err) {
157
+ throw new Error(`Failed to read CLAUDE.md: ${err.message}`);
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Initializes tracking for created files/directories
163
+ * @param {Object} context - Test context to initialize
164
+ * @throws {Error} If context is invalid
165
+ */
166
+ function initializeTracking(context) {
167
+ if (!context || typeof context !== 'object') {
168
+ throw new Error('Context must be an object');
169
+ }
170
+
171
+ context.createdFiles = [];
172
+ context.createdDirs = [];
173
+ context.modifiedFiles = new Map();
174
+ context.createdTestDirs = [];
175
+ }
176
+
177
+ module.exports = {
178
+ createTestEnvironment,
179
+ copyDirRecursive,
180
+ runCommand,
181
+ readConfig,
182
+ readClaude,
183
+ initializeTracking
184
+ };
@@ -0,0 +1,255 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const {
5
+ createTestEnvironment,
6
+ copyDirRecursive,
7
+ runCommand,
8
+ readConfig,
9
+ readClaude,
10
+ initializeTracking
11
+ } = require('./test-helpers');
12
+
13
+ describe('Test Helpers Module', () => {
14
+ describe('createTestEnvironment()', () => {
15
+ test('should create isolated test directory', () => {
16
+ const env = createTestEnvironment();
17
+
18
+ expect(env.testDir).toBeTruthy();
19
+ expect(fs.existsSync(env.testDir)).toBe(true);
20
+ expect(env.testDir).toContain('jettypod-test-');
21
+ expect(env.originalCwd).toBeTruthy();
22
+
23
+ env.cleanup();
24
+ });
25
+
26
+ test('should provide cleanup function', () => {
27
+ const env = createTestEnvironment();
28
+ const testDir = env.testDir;
29
+
30
+ expect(fs.existsSync(testDir)).toBe(true);
31
+ env.cleanup();
32
+ expect(fs.existsSync(testDir)).toBe(false);
33
+ });
34
+
35
+ test('should handle cleanup when directory already deleted', () => {
36
+ const env = createTestEnvironment();
37
+ fs.rmSync(env.testDir, { recursive: true });
38
+
39
+ expect(() => env.cleanup()).not.toThrow();
40
+ });
41
+
42
+ test('should restore original working directory on cleanup', () => {
43
+ const originalCwd = process.cwd();
44
+ const env = createTestEnvironment();
45
+
46
+ process.chdir(env.testDir);
47
+ env.cleanup();
48
+
49
+ expect(process.cwd()).toBe(originalCwd);
50
+ });
51
+ });
52
+
53
+ describe('copyDirRecursive()', () => {
54
+ let testEnv;
55
+
56
+ beforeEach(() => {
57
+ testEnv = createTestEnvironment();
58
+ process.chdir(testEnv.testDir);
59
+ });
60
+
61
+ afterEach(() => {
62
+ testEnv.cleanup();
63
+ });
64
+
65
+ test('should copy directory recursively', () => {
66
+ const srcDir = path.join(testEnv.testDir, 'src');
67
+ const destDir = path.join(testEnv.testDir, 'dest');
68
+
69
+ fs.mkdirSync(srcDir);
70
+ fs.writeFileSync(path.join(srcDir, 'file1.js'), 'content1');
71
+ fs.writeFileSync(path.join(srcDir, 'file2.js'), 'content2');
72
+
73
+ copyDirRecursive(srcDir, destDir);
74
+
75
+ expect(fs.existsSync(path.join(destDir, 'file1.js'))).toBe(true);
76
+ expect(fs.existsSync(path.join(destDir, 'file2.js'))).toBe(true);
77
+ expect(fs.readFileSync(path.join(destDir, 'file1.js'), 'utf-8')).toBe('content1');
78
+ });
79
+
80
+ test('should skip .test.js files when copying', () => {
81
+ const srcDir = path.join(testEnv.testDir, 'src');
82
+ const destDir = path.join(testEnv.testDir, 'dest');
83
+
84
+ fs.mkdirSync(srcDir);
85
+ fs.writeFileSync(path.join(srcDir, 'file.js'), 'content');
86
+ fs.writeFileSync(path.join(srcDir, 'file.test.js'), 'test content');
87
+
88
+ copyDirRecursive(srcDir, destDir);
89
+
90
+ expect(fs.existsSync(path.join(destDir, 'file.js'))).toBe(true);
91
+ expect(fs.existsSync(path.join(destDir, 'file.test.js'))).toBe(false);
92
+ });
93
+
94
+ test('should copy nested directories', () => {
95
+ const srcDir = path.join(testEnv.testDir, 'src');
96
+ const destDir = path.join(testEnv.testDir, 'dest');
97
+ const nestedDir = path.join(srcDir, 'nested');
98
+
99
+ fs.mkdirSync(srcDir);
100
+ fs.mkdirSync(nestedDir);
101
+ fs.writeFileSync(path.join(nestedDir, 'file.js'), 'nested content');
102
+
103
+ copyDirRecursive(srcDir, destDir);
104
+
105
+ expect(fs.existsSync(path.join(destDir, 'nested', 'file.js'))).toBe(true);
106
+ expect(fs.readFileSync(path.join(destDir, 'nested', 'file.js'), 'utf-8')).toBe('nested content');
107
+ });
108
+
109
+ test('should create destination directory if it does not exist', () => {
110
+ const srcDir = path.join(testEnv.testDir, 'src');
111
+ const destDir = path.join(testEnv.testDir, 'dest');
112
+
113
+ fs.mkdirSync(srcDir);
114
+ fs.writeFileSync(path.join(srcDir, 'file.js'), 'content');
115
+
116
+ expect(fs.existsSync(destDir)).toBe(false);
117
+ copyDirRecursive(srcDir, destDir);
118
+ expect(fs.existsSync(destDir)).toBe(true);
119
+ });
120
+
121
+ test('should throw error for non-existent source directory', () => {
122
+ const srcDir = path.join(testEnv.testDir, 'nonexistent');
123
+ const destDir = path.join(testEnv.testDir, 'dest');
124
+
125
+ expect(() => copyDirRecursive(srcDir, destDir)).toThrow('Source directory does not exist');
126
+ });
127
+
128
+ test('should throw error for invalid source path', () => {
129
+ expect(() => copyDirRecursive(null, 'dest')).toThrow('Source path must be a non-empty string');
130
+ expect(() => copyDirRecursive('', 'dest')).toThrow('Source path must be a non-empty string');
131
+ });
132
+
133
+ test('should throw error for invalid destination path', () => {
134
+ const srcDir = path.join(testEnv.testDir, 'src');
135
+ fs.mkdirSync(srcDir);
136
+
137
+ expect(() => copyDirRecursive(srcDir, null)).toThrow('Destination path must be a non-empty string');
138
+ expect(() => copyDirRecursive(srcDir, '')).toThrow('Destination path must be a non-empty string');
139
+ });
140
+
141
+ test('should throw error if source is not a directory', () => {
142
+ const srcFile = path.join(testEnv.testDir, 'file.js');
143
+ fs.writeFileSync(srcFile, 'content');
144
+
145
+ expect(() => copyDirRecursive(srcFile, 'dest')).toThrow('Source is not a directory');
146
+ });
147
+ });
148
+
149
+ describe('runCommand()', () => {
150
+ let testEnv;
151
+
152
+ beforeEach(() => {
153
+ testEnv = createTestEnvironment();
154
+ });
155
+
156
+ afterEach(() => {
157
+ testEnv.cleanup();
158
+ });
159
+
160
+ test('should throw error for invalid command', () => {
161
+ expect(() => runCommand(null, testEnv)).toThrow('Command must be a non-empty string');
162
+ expect(() => runCommand('', testEnv)).toThrow('Command must be a non-empty string');
163
+ });
164
+
165
+ test('should throw error for invalid context', () => {
166
+ expect(() => runCommand('jettypod --version', null)).toThrow('Context must be an object');
167
+ expect(() => runCommand('jettypod --version', 'string')).toThrow('Context must be an object');
168
+ });
169
+
170
+ test('should return error message if jettypod.js not found', () => {
171
+ const invalidContext = { originalCwd: '/nonexistent/path' };
172
+ const result = runCommand('jettypod --version', invalidContext);
173
+ expect(result).toContain('JettyPod script not found');
174
+ });
175
+ });
176
+
177
+ describe('readConfig()', () => {
178
+ let testEnv;
179
+
180
+ beforeEach(() => {
181
+ testEnv = createTestEnvironment();
182
+ process.chdir(testEnv.testDir);
183
+ });
184
+
185
+ afterEach(() => {
186
+ testEnv.cleanup();
187
+ });
188
+
189
+ test('should return null if config file does not exist', () => {
190
+ expect(readConfig()).toBeNull();
191
+ });
192
+
193
+ test('should read config.json successfully', () => {
194
+ const jettypodDir = path.join(testEnv.testDir, '.jettypod');
195
+ fs.mkdirSync(jettypodDir);
196
+ fs.writeFileSync(
197
+ path.join(jettypodDir, 'config.json'),
198
+ JSON.stringify({ mode: 'speed', stage: 'starting' })
199
+ );
200
+
201
+ const config = readConfig();
202
+ expect(config).toEqual({ mode: 'speed', stage: 'starting' });
203
+ });
204
+
205
+ test('should throw error for corrupted JSON', () => {
206
+ const jettypodDir = path.join(testEnv.testDir, '.jettypod');
207
+ fs.mkdirSync(jettypodDir);
208
+ fs.writeFileSync(path.join(jettypodDir, 'config.json'), 'not valid json');
209
+
210
+ expect(() => readConfig()).toThrow('Config file contains invalid JSON');
211
+ });
212
+ });
213
+
214
+ describe('readClaude()', () => {
215
+ let testEnv;
216
+
217
+ beforeEach(() => {
218
+ testEnv = createTestEnvironment();
219
+ process.chdir(testEnv.testDir);
220
+ });
221
+
222
+ afterEach(() => {
223
+ testEnv.cleanup();
224
+ });
225
+
226
+ test('should return empty string if CLAUDE.md does not exist', () => {
227
+ expect(readClaude()).toBe('');
228
+ });
229
+
230
+ test('should read CLAUDE.md successfully', () => {
231
+ fs.writeFileSync('CLAUDE.md', '# Test CLAUDE.md');
232
+
233
+ const content = readClaude();
234
+ expect(content).toBe('# Test CLAUDE.md');
235
+ });
236
+ });
237
+
238
+ describe('initializeTracking()', () => {
239
+ test('should initialize tracking properties on context', () => {
240
+ const context = {};
241
+ initializeTracking(context);
242
+
243
+ expect(context.createdFiles).toEqual([]);
244
+ expect(context.createdDirs).toEqual([]);
245
+ expect(context.modifiedFiles).toBeInstanceOf(Map);
246
+ expect(context.createdTestDirs).toEqual([]);
247
+ });
248
+
249
+ test('should throw error for invalid context', () => {
250
+ expect(() => initializeTracking(null)).toThrow('Context must be an object');
251
+ expect(() => initializeTracking('string')).toThrow('Context must be an object');
252
+ expect(() => initializeTracking(undefined)).toThrow('Context must be an object');
253
+ });
254
+ });
255
+ });
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "jettypod",
3
+ "version": "3.0.1",
4
+ "description": "Simplified development mode manager",
5
+ "main": "jettypod.js",
6
+ "bin": {
7
+ "jettypod": "./jettypod.js"
8
+ },
9
+ "scripts": {
10
+ "test": "npm run test:unit && npm run test:bdd && npm run test:cleanup",
11
+ "test:bdd": "NODE_ENV=test cucumber-js",
12
+ "test:unit": "NODE_ENV=test jest",
13
+ "test:unit:watch": "NODE_ENV=test jest --watch",
14
+ "test:cleanup": "node -e \"const fs = require('fs'); const path = require('path'); const testDb = path.join(process.cwd(), '.jettypod', 'test-work.db'); if (fs.existsSync(testDb)) { fs.unlinkSync(testDb); console.log('✅ Cleaned up test database'); }\""
15
+ },
16
+ "devDependencies": {
17
+ "@cucumber/cucumber": "^10.0.1",
18
+ "chai": "^6.0.1",
19
+ "jest": "^30.1.3"
20
+ },
21
+ "jest": {
22
+ "testEnvironment": "node",
23
+ "testMatch": [
24
+ "**/__tests__/**/*.js",
25
+ "**/*.test.js"
26
+ ],
27
+ "testPathIgnorePatterns": [
28
+ "/node_modules/"
29
+ ],
30
+ "collectCoverage": false,
31
+ "coverageDirectory": "coverage"
32
+ },
33
+ "dependencies": {
34
+ "sqlite3": "^5.1.7"
35
+ }
36
+ }
@@ -0,0 +1 @@
1
+ test