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,216 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { createTestEnvironment } = require('../../lib/test-helpers');
4
+ const { execSync } = require('child_process');
5
+
6
+ /**
7
+ * End-to-end tests for skills update functionality
8
+ * Tests the BDD scenarios from features/always-update-skills.feature
9
+ */
10
+ describe('Skills Update on JettyPod Init', () => {
11
+ let testEnv;
12
+ let jettypodPath;
13
+ let skillsSourceDir;
14
+
15
+ beforeEach(() => {
16
+ testEnv = createTestEnvironment();
17
+ process.chdir(testEnv.testDir);
18
+
19
+ // Path to jettypod script (relative to test directory)
20
+ jettypodPath = path.join(__dirname, '..', '..', 'jettypod.js');
21
+
22
+ // SAFETY: Create mock skills source directory in /tmp/ test environment, NOT in actual project
23
+ skillsSourceDir = path.join(testEnv.testDir, 'mock-jettypod-skills');
24
+ if (!fs.existsSync(skillsSourceDir)) {
25
+ fs.mkdirSync(skillsSourceDir, { recursive: true });
26
+ }
27
+
28
+ // Create sample skills for testing
29
+ const speedModeSkillDir = path.join(skillsSourceDir, 'speed-mode');
30
+ fs.mkdirSync(speedModeSkillDir, { recursive: true });
31
+ fs.writeFileSync(
32
+ path.join(speedModeSkillDir, 'SKILL.md'),
33
+ '# Speed Mode Skill\nTest content for speed mode skill'
34
+ );
35
+
36
+ const stableModeSkillDir = path.join(skillsSourceDir, 'stable-mode');
37
+ fs.mkdirSync(stableModeSkillDir, { recursive: true });
38
+ fs.writeFileSync(
39
+ path.join(stableModeSkillDir, 'SKILL.md'),
40
+ '# Stable Mode Skill\nTest content for stable mode skill'
41
+ );
42
+
43
+ // Configure jettypod to use mock skills source
44
+ process.env.JETTYPOD_SKILLS_SOURCE_DIR = skillsSourceDir;
45
+ });
46
+
47
+ afterEach(() => {
48
+ // Clean up environment variable
49
+ delete process.env.JETTYPOD_SKILLS_SOURCE_DIR;
50
+ testEnv.cleanup();
51
+ });
52
+
53
+ describe('Scenario: Skills are updated when initializing existing project', () => {
54
+ test('should backup existing skills before updating', () => {
55
+ // Given: I have a project with old .claude/skills/ directory
56
+ const claudeDir = path.join(testEnv.testDir, '.claude');
57
+ const oldSkillsDir = path.join(claudeDir, 'skills');
58
+ fs.mkdirSync(oldSkillsDir, { recursive: true });
59
+
60
+ const oldSkillDir = path.join(oldSkillsDir, 'old-skill');
61
+ fs.mkdirSync(oldSkillDir, { recursive: true });
62
+ fs.writeFileSync(
63
+ path.join(oldSkillDir, 'SKILL.md'),
64
+ '# Old Skill\nThis is an old skill that should be backed up'
65
+ );
66
+
67
+ // When: I run jettypod in the project directory
68
+ execSync(`node ${jettypodPath}`, { stdio: 'pipe', env: { ...process.env } });
69
+
70
+ // Then: old skills are backed up before replacement
71
+ const backupDirs = fs.readdirSync(claudeDir).filter(name => name.startsWith('skills.backup-'));
72
+ expect(backupDirs.length).toBe(1);
73
+
74
+ const backupDir = path.join(claudeDir, backupDirs[0]);
75
+ expect(fs.existsSync(backupDir)).toBe(true);
76
+
77
+ // Verify old skill is in backup
78
+ const backedUpSkillFile = path.join(backupDir, 'old-skill', 'SKILL.md');
79
+ expect(fs.existsSync(backedUpSkillFile)).toBe(true);
80
+ const backedUpContent = fs.readFileSync(backedUpSkillFile, 'utf8');
81
+ expect(backedUpContent).toContain('This is an old skill that should be backed up');
82
+ });
83
+
84
+ test('should replace skills with latest from jettypod', () => {
85
+ // Given: I have a project with old .claude/skills/ directory
86
+ const claudeDir = path.join(testEnv.testDir, '.claude');
87
+ const oldSkillsDir = path.join(claudeDir, 'skills');
88
+ fs.mkdirSync(oldSkillsDir, { recursive: true });
89
+
90
+ const oldSkillDir = path.join(oldSkillsDir, 'old-skill');
91
+ fs.mkdirSync(oldSkillDir, { recursive: true });
92
+ fs.writeFileSync(path.join(oldSkillDir, 'SKILL.md'), '# Old Skill');
93
+
94
+ // When: I run jettypod in the project directory
95
+ execSync(`node ${jettypodPath}`, { stdio: 'pipe', env: { ...process.env } });
96
+
97
+ // Then: .claude/skills/ is replaced with latest skills from jettypod
98
+ const newSkillsDir = path.join(testEnv.testDir, '.claude', 'skills');
99
+ expect(fs.existsSync(newSkillsDir)).toBe(true);
100
+
101
+ // Old skill should be gone
102
+ expect(fs.existsSync(path.join(newSkillsDir, 'old-skill'))).toBe(false);
103
+
104
+ // New skills should be present
105
+ expect(fs.existsSync(path.join(newSkillsDir, 'speed-mode', 'SKILL.md'))).toBe(true);
106
+ expect(fs.existsSync(path.join(newSkillsDir, 'stable-mode', 'SKILL.md'))).toBe(true);
107
+
108
+ // Verify content is from jettypod source
109
+ const speedModeContent = fs.readFileSync(
110
+ path.join(newSkillsDir, 'speed-mode', 'SKILL.md'),
111
+ 'utf8'
112
+ );
113
+ expect(speedModeContent).toContain('Test content for speed mode skill');
114
+ });
115
+
116
+ test('should handle concurrent inits with unique backup names', () => {
117
+ // Given: I have a project with .claude/skills/ directory
118
+ const claudeDir = path.join(testEnv.testDir, '.claude');
119
+ const skillsDir = path.join(claudeDir, 'skills');
120
+ fs.mkdirSync(skillsDir, { recursive: true });
121
+ fs.writeFileSync(path.join(skillsDir, 'test.md'), 'test');
122
+
123
+ // Manually create a backup with current timestamp format
124
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
125
+ const existingBackup = path.join(claudeDir, `skills.backup-${timestamp}`);
126
+ fs.mkdirSync(existingBackup, { recursive: true });
127
+ fs.writeFileSync(path.join(existingBackup, 'existing.md'), 'existing');
128
+
129
+ // Restore skills dir for the actual init
130
+ if (fs.existsSync(skillsDir)) {
131
+ fs.rmSync(skillsDir, { recursive: true, force: true });
132
+ }
133
+ fs.mkdirSync(skillsDir, { recursive: true });
134
+ fs.writeFileSync(path.join(skillsDir, 'test.md'), 'test');
135
+
136
+ // When: I run jettypod (which creates another backup with same timestamp)
137
+ execSync(`node ${jettypodPath}`, { stdio: 'pipe', env: { ...process.env } });
138
+
139
+ // Then: backup directory should have counter appended
140
+ const backupDirs = fs.readdirSync(claudeDir)
141
+ .filter(name => name.startsWith('skills.backup-'))
142
+ .sort();
143
+
144
+ // Should have at least 2 backups (the manual one and the new one)
145
+ expect(backupDirs.length).toBeGreaterThanOrEqual(2);
146
+ });
147
+ });
148
+
149
+ describe('Scenario: Skills are copied when initializing new project', () => {
150
+ test('should create .claude/skills/ with latest skills', () => {
151
+ // Given: I have a new project with no .claude/ directory
152
+ // (testEnv starts clean)
153
+
154
+ // When: I run jettypod to initialize
155
+ execSync(`node ${jettypodPath}`, { stdio: 'pipe', env: { ...process.env } });
156
+
157
+ // Then: .claude/skills/ is created with latest skills from jettypod
158
+ const skillsDir = path.join(testEnv.testDir, '.claude', 'skills');
159
+ expect(fs.existsSync(skillsDir)).toBe(true);
160
+
161
+ // And: all skills are ready to use
162
+ expect(fs.existsSync(path.join(skillsDir, 'speed-mode', 'SKILL.md'))).toBe(true);
163
+ expect(fs.existsSync(path.join(skillsDir, 'stable-mode', 'SKILL.md'))).toBe(true);
164
+
165
+ const speedModeContent = fs.readFileSync(
166
+ path.join(skillsDir, 'speed-mode', 'SKILL.md'),
167
+ 'utf8'
168
+ );
169
+ expect(speedModeContent).toContain('Test content for speed mode skill');
170
+ });
171
+
172
+ test('should not create backup when no existing skills', () => {
173
+ // Given: I have a new project with no .claude/ directory
174
+ // When: I run jettypod to initialize
175
+ execSync(`node ${jettypodPath}`, { stdio: 'pipe', env: { ...process.env } });
176
+
177
+ // Then: no backup should be created
178
+ const claudeDir = path.join(testEnv.testDir, '.claude');
179
+ const backupDirs = fs.readdirSync(claudeDir).filter(name => name.startsWith('skills.backup-'));
180
+ expect(backupDirs.length).toBe(0);
181
+ });
182
+ });
183
+
184
+ describe('Error handling validation', () => {
185
+ test('should handle source directory missing gracefully', () => {
186
+ // Remove skills source directory
187
+ if (fs.existsSync(skillsSourceDir)) {
188
+ fs.rmSync(skillsSourceDir, { recursive: true, force: true });
189
+ }
190
+
191
+ // Should not throw, just warn
192
+ expect(() => {
193
+ execSync(`node ${jettypodPath}`, { stdio: 'pipe', env: { ...process.env } });
194
+ }).not.toThrow();
195
+
196
+ // .claude should still be created
197
+ expect(fs.existsSync(path.join(testEnv.testDir, '.claude'))).toBe(true);
198
+ });
199
+
200
+ test('should handle empty source directory gracefully', () => {
201
+ // Empty the skills source directory
202
+ if (fs.existsSync(skillsSourceDir)) {
203
+ fs.rmSync(skillsSourceDir, { recursive: true, force: true });
204
+ }
205
+ fs.mkdirSync(skillsSourceDir, { recursive: true });
206
+
207
+ // Should not throw, just warn
208
+ expect(() => {
209
+ execSync(`node ${jettypodPath}`, { stdio: 'pipe', env: { ...process.env } });
210
+ }).not.toThrow();
211
+
212
+ // .claude should still be created
213
+ expect(fs.existsSync(path.join(testEnv.testDir, '.claude'))).toBe(true);
214
+ });
215
+ });
216
+ });
@@ -0,0 +1,162 @@
1
+ const { Given, When, Then, AfterAll } = require('@cucumber/cucumber');
2
+ const assert = require('assert');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { execSync } = require('child_process');
6
+
7
+ const testDir = path.join('/tmp', 'jettypod-production-chores-test-' + Date.now());
8
+ const jettypodDir = path.join(testDir, '.jettypod');
9
+ const configPath = path.join(jettypodDir, 'config.json');
10
+ const dbFileName = process.env.NODE_ENV === 'test' ? 'test-work.db' : 'work.db';
11
+ const dbPath = path.join(jettypodDir, dbFileName);
12
+
13
+ let testContext = {};
14
+ let originalDir = process.cwd();
15
+
16
+ // Setup test environment
17
+ async function setupTestEnv() {
18
+ // SAFETY: Only delete if testDir is in /tmp
19
+ if (fs.existsSync(testDir) && testDir.startsWith('/tmp/')) {
20
+ fs.rmSync(testDir, { recursive: true, force: true });
21
+ }
22
+ fs.mkdirSync(testDir, { recursive: true });
23
+ fs.mkdirSync(jettypodDir, { recursive: true });
24
+ process.chdir(testDir);
25
+
26
+ // Initialize git
27
+ execSync('git init', { stdio: 'pipe' });
28
+ execSync('git config user.email "test@test.com"', { stdio: 'pipe' });
29
+ execSync('git config user.name "Test"', { stdio: 'pipe' });
30
+
31
+ // Initialize database
32
+ const { getDb } = require('../../lib/database');
33
+ const { runMigrations } = require('../../lib/migrations');
34
+ const db = getDb();
35
+ await runMigrations(db);
36
+ }
37
+
38
+ // Cleanup test environment
39
+ async function cleanupTestEnv() {
40
+ const { closeDb } = require('../../lib/database');
41
+ const { resetDb } = require('../../lib/database');
42
+ await closeDb();
43
+ resetDb();
44
+
45
+ if (fs.existsSync(testDir)) {
46
+ process.chdir(originalDir);
47
+ fs.rmSync(testDir, { recursive: true, force: true });
48
+ }
49
+ testContext = {};
50
+ }
51
+
52
+ // Given steps
53
+ Given('I have a project with {int} features in stable mode', async function (featureCount) {
54
+ await setupTestEnv();
55
+
56
+ // Create config
57
+ const config = {
58
+ name: 'test-project',
59
+ project_state: 'internal',
60
+ project_discovery: {
61
+ status: 'completed'
62
+ }
63
+ };
64
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
65
+
66
+ // Create features in database
67
+ const sqlite3 = require('sqlite3').verbose();
68
+ const db = new sqlite3.Database(dbPath);
69
+
70
+ for (let i = 1; i <= featureCount; i++) {
71
+ await new Promise((resolve) => {
72
+ db.run(
73
+ `INSERT INTO work_items (id, type, title, status, mode, phase) VALUES (?, 'feature', ?, 'completed', 'stable', 'implementation')`,
74
+ [i, `Feature ${i}`],
75
+ resolve
76
+ );
77
+ });
78
+ }
79
+
80
+ testContext.featureCount = featureCount;
81
+ });
82
+
83
+ Given('the project state is {string}', function (state) {
84
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
85
+ assert.strictEqual(config.project_state, state);
86
+ });
87
+
88
+ // When steps
89
+ When('I transition the project to external', function () {
90
+ try {
91
+ testContext.output = execSync(
92
+ `node ${path.join(originalDir, 'jettypod.js')} project external`,
93
+ { cwd: testDir, encoding: 'utf-8' }
94
+ );
95
+ } catch (err) {
96
+ testContext.error = err.message;
97
+ testContext.output = err.stdout || '';
98
+ }
99
+ });
100
+
101
+ // Then steps
102
+ Then('the project state is set to {string}', function (expectedState) {
103
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
104
+ assert.strictEqual(config.project_state, expectedState);
105
+ });
106
+
107
+ Then('a {string} epic is created', function (epicTitle) {
108
+ const sqlite3 = require('sqlite3').verbose();
109
+ const db = new sqlite3.Database(dbPath);
110
+
111
+ return new Promise((resolve) => {
112
+ db.get(
113
+ `SELECT * FROM work_items WHERE type = 'epic' AND title = ?`,
114
+ [epicTitle],
115
+ (err, row) => {
116
+ assert(row, `Epic "${epicTitle}" should exist`);
117
+ testContext.epicId = row.id;
118
+ resolve();
119
+ }
120
+ );
121
+ });
122
+ });
123
+
124
+ Then('{int} production chores are created for each stable feature', function (choresPerFeature) {
125
+ const sqlite3 = require('sqlite3').verbose();
126
+ const db = new sqlite3.Database(dbPath);
127
+
128
+ return new Promise((resolve) => {
129
+ db.all(
130
+ `SELECT * FROM work_items WHERE type = 'chore' AND parent_id = ?`,
131
+ [testContext.epicId],
132
+ (err, rows) => {
133
+ const expectedTotal = testContext.featureCount * choresPerFeature;
134
+ assert.strictEqual(rows.length, expectedTotal, `Should have ${expectedTotal} production chores`);
135
+ testContext.productionChores = rows;
136
+ resolve();
137
+ }
138
+ );
139
+ });
140
+ });
141
+
142
+ Then('the chores are grouped under the epic', function () {
143
+ const choresUnderEpic = testContext.productionChores.filter(
144
+ chore => chore.parent_id === testContext.epicId
145
+ );
146
+ assert.strictEqual(
147
+ choresUnderEpic.length,
148
+ testContext.productionChores.length,
149
+ 'All production chores should be under the epic'
150
+ );
151
+ });
152
+
153
+ Then('I see a summary of created chores', function () {
154
+ assert(testContext.output.includes('production chores'), 'Output should mention production chores');
155
+ assert(testContext.output.includes('Feature Production Readiness'), 'Output should mention the epic');
156
+ });
157
+
158
+ // Cleanup after all scenarios complete (AfterAll allows async operations)
159
+ AfterAll(async function() {
160
+ const { closeDb } = require('../../lib/database');
161
+ await closeDb();
162
+ });
@@ -0,0 +1,145 @@
1
+ const { Given, When, Then } = require('@cucumber/cucumber');
2
+ const assert = require('assert');
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+
6
+ Given('the logo module exists', function () {
7
+ const logoPath = path.join(__dirname, '../../lib/logo.js');
8
+ assert(fs.existsSync(logoPath), `Logo module should exist at ${logoPath}`);
9
+ this.logoPath = logoPath;
10
+ });
11
+
12
+ When('I import it', function () {
13
+ // Import the logo module
14
+ this.logoModule = require(this.logoPath);
15
+ });
16
+
17
+ Then('it exports a {string} function', function (functionName) {
18
+ assert(this.logoModule, 'Logo module should be imported');
19
+ assert(typeof this.logoModule[functionName] === 'function', `Should export a ${functionName} function`);
20
+ this.exportedFunction = this.logoModule[functionName];
21
+ });
22
+
23
+ When('I call the showLogo function', function () {
24
+ // Capture console.log output
25
+ const originalLog = console.log;
26
+ const logOutput = [];
27
+
28
+ console.log = (...args) => {
29
+ logOutput.push(args.join(' '));
30
+ };
31
+
32
+ try {
33
+ // Call the showLogo function
34
+ const { showLogo } = require('../../lib/logo.js');
35
+ showLogo();
36
+ } finally {
37
+ console.log = originalLog;
38
+ }
39
+
40
+ this.logoOutput = logOutput;
41
+ });
42
+
43
+ Then('it outputs {int} lines of logo art', function (expectedLines) {
44
+ assert(this.logoOutput, 'Logo output should exist');
45
+ // Count total lines across all console.log calls (split by newlines)
46
+ const totalLines = this.logoOutput.reduce((count, output) => {
47
+ return count + output.split('\n').filter(line => line.trim().length > 0).length;
48
+ }, 0);
49
+ assert(totalLines >= expectedLines,
50
+ `Expected at least ${expectedLines} lines but got ${totalLines}`);
51
+ });
52
+
53
+ Then('it includes the version subtitle', function () {
54
+ assert(this.logoOutput, 'Logo output should exist');
55
+ const hasVersionLine = this.logoOutput.some(line =>
56
+ line.includes('version') || line.includes('v') || line.includes('.')
57
+ );
58
+ assert(hasVersionLine, 'Logo should include version subtitle');
59
+ });
60
+
61
+ When('I run jettypod init', function () {
62
+ const { execSync } = require('child_process');
63
+ const jettypodPath = path.join(__dirname, '../../jettypod.js');
64
+ const skillsSourceDir = path.join(__dirname, '../../.claude/skills');
65
+
66
+ try {
67
+ const output = execSync(`node ${jettypodPath} init`, {
68
+ cwd: this.testDir || process.cwd(),
69
+ encoding: 'utf-8',
70
+ env: { ...process.env, JETTYPOD_SKILLS_SOURCE_DIR: skillsSourceDir }
71
+ });
72
+ this.initOutput = output;
73
+ } catch (err) {
74
+ this.initOutput = err.stdout || '';
75
+ this.initError = err;
76
+ }
77
+ });
78
+
79
+ Then('the output contains the unicode gradient logo', function () {
80
+ assert(this.initOutput || this.commandOutput, 'Should have output from jettypod init');
81
+ const output = this.initOutput || this.commandOutput;
82
+
83
+ // Check for DEV or POD in the output
84
+ const hasLogo = output.includes('DEV') || output.includes('POD') ||
85
+ output.includes('█') || output.includes('▓') ||
86
+ output.includes('▒') || output.includes('░');
87
+
88
+ assert(hasLogo, 'Output should contain unicode gradient logo characters');
89
+ });
90
+
91
+ Then('I see the unicode logo', function () {
92
+ assert(this.initOutput || this.commandOutput, 'Should have output from jettypod init');
93
+ const output = this.initOutput || this.commandOutput;
94
+
95
+ const hasLogo = output.includes('DEV') || output.includes('POD') ||
96
+ output.includes('█') || output.includes('▓');
97
+
98
+ assert(hasLogo, 'Should see unicode logo in output');
99
+ });
100
+
101
+ Then('the logo includes {string} and {string} text', function (text1, text2) {
102
+ const output = this.initOutput || this.commandOutput;
103
+ assert(output, 'Should have output');
104
+ // The logo uses box-drawing characters, not literal text
105
+ // Just check that the output has the box characters that form the logo
106
+ const hasBoxChars = output.includes('█') || output.includes('╔') || output.includes('║');
107
+ assert(hasBoxChars, `Logo should include box-drawing characters (forms ${text1} and ${text2})`);
108
+ });
109
+
110
+ Then('the logo uses ANSI color codes', function () {
111
+ const output = this.initOutput || this.commandOutput;
112
+ assert(output, 'Should have output');
113
+
114
+ // Check for ANSI escape codes
115
+ const hasAnsiCodes = output.includes('\x1b[') || output.includes('\u001b[');
116
+ assert(hasAnsiCodes, 'Logo should use ANSI color codes');
117
+ });
118
+
119
+ Then('the .jettypod directory is created', function () {
120
+ const jettypodDir = path.join(this.testDir || process.cwd(), '.jettypod');
121
+ assert(fs.existsSync(jettypodDir), '.jettypod directory should be created');
122
+
123
+ // Verify it contains expected files
124
+ const configPath = path.join(jettypodDir, 'config.json');
125
+ assert(fs.existsSync(configPath), 'config.json should exist in .jettypod directory');
126
+ });
127
+
128
+ Then('git hooks are installed', function () {
129
+ const hooksDir = path.join(this.testDir || process.cwd(), '.git', 'hooks');
130
+
131
+ // Check if .git directory exists (it might not if init was run without git)
132
+ const gitDir = path.join(this.testDir || process.cwd(), '.git');
133
+ if (!fs.existsSync(gitDir)) {
134
+ // Skip this check if not a git repo
135
+ return;
136
+ }
137
+
138
+ // Check for post-commit hook
139
+ const postCommitHook = path.join(hooksDir, 'post-commit');
140
+ assert(fs.existsSync(postCommitHook), 'post-commit hook should be installed');
141
+
142
+ // Check for post-merge hook
143
+ const postMergeHook = path.join(hooksDir, 'post-merge');
144
+ assert(fs.existsSync(postMergeHook), 'post-merge hook should be installed');
145
+ });