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,310 @@
1
+ // Tests for tree view validation and error handling
2
+ // These tests focus on the printTree and getTree functions' error handling
3
+ // rather than full integration tests
4
+
5
+ describe('Tree View - Error Handling', () => {
6
+ describe('printTree validation', () => {
7
+ const originalConsoleLog = console.log;
8
+ let logOutput = [];
9
+
10
+ beforeEach(() => {
11
+ logOutput = [];
12
+ console.log = jest.fn((...args) => {
13
+ logOutput.push(args.join(' '));
14
+ });
15
+ });
16
+
17
+ afterEach(() => {
18
+ console.log = originalConsoleLog;
19
+ });
20
+
21
+ function printTree(items, prefix = '') {
22
+ if (!Array.isArray(items)) {
23
+ throw new Error('Items must be an array');
24
+ }
25
+
26
+ items.forEach((item, index) => {
27
+ if (!item || typeof item !== 'object') {
28
+ return; // Skip invalid items
29
+ }
30
+
31
+ // Print item (simplified for tests)
32
+ console.log(`${prefix}${item.title}`);
33
+
34
+ // Print description if present
35
+ if (item.description) {
36
+ console.log(`${prefix} "${item.description}"`);
37
+ }
38
+
39
+ if (item.children && item.children.length > 0) {
40
+ const newPrefix = prefix + ' ';
41
+ printTree(item.children, newPrefix);
42
+ }
43
+ });
44
+ }
45
+
46
+ test('should throw error for null input', () => {
47
+ expect(() => printTree(null)).toThrow('Items must be an array');
48
+ });
49
+
50
+ test('should throw error for string input', () => {
51
+ expect(() => printTree('not-an-array')).toThrow('Items must be an array');
52
+ });
53
+
54
+ test('should throw error for object input', () => {
55
+ expect(() => printTree({})).toThrow('Items must be an array');
56
+ });
57
+
58
+ test('should not throw for empty array', () => {
59
+ expect(() => printTree([])).not.toThrow();
60
+ });
61
+
62
+ test('should handle single valid item', () => {
63
+ const items = [{
64
+ id: 1,
65
+ title: 'Test',
66
+ type: 'feature',
67
+ status: 'backlog',
68
+ children: []
69
+ }];
70
+ expect(() => printTree(items)).not.toThrow();
71
+ });
72
+
73
+ test('should handle nested items', () => {
74
+ const items = [{
75
+ id: 1,
76
+ title: 'Epic',
77
+ type: 'epic',
78
+ status: 'backlog',
79
+ children: [{
80
+ id: 2,
81
+ title: 'Feature',
82
+ type: 'feature',
83
+ status: 'backlog',
84
+ children: []
85
+ }]
86
+ }];
87
+ expect(() => printTree(items)).not.toThrow();
88
+ });
89
+
90
+ test('should skip null items in array', () => {
91
+ const items = [
92
+ null,
93
+ {
94
+ id: 1,
95
+ title: 'Valid',
96
+ type: 'feature',
97
+ status: 'backlog',
98
+ children: []
99
+ }
100
+ ];
101
+ expect(() => printTree(items)).not.toThrow();
102
+ });
103
+
104
+ test('should skip undefined items in array', () => {
105
+ const items = [
106
+ undefined,
107
+ {
108
+ id: 1,
109
+ title: 'Valid',
110
+ type: 'feature',
111
+ status: 'backlog',
112
+ children: []
113
+ }
114
+ ];
115
+ expect(() => printTree(items)).not.toThrow();
116
+ });
117
+
118
+ test('should handle items without children property', () => {
119
+ const items = [{
120
+ id: 1,
121
+ title: 'Test',
122
+ type: 'feature',
123
+ status: 'backlog'
124
+ // no children property
125
+ }];
126
+ expect(() => printTree(items)).not.toThrow();
127
+ });
128
+
129
+ test('should handle deeply nested structures', () => {
130
+ const items = [{
131
+ id: 1,
132
+ title: 'Level 1',
133
+ children: [{
134
+ id: 2,
135
+ title: 'Level 2',
136
+ children: [{
137
+ id: 3,
138
+ title: 'Level 3',
139
+ children: []
140
+ }]
141
+ }]
142
+ }];
143
+ expect(() => printTree(items)).not.toThrow();
144
+ });
145
+
146
+ test('should display description when present', () => {
147
+ const items = [{
148
+ id: 1,
149
+ title: 'Test Item',
150
+ type: 'feature',
151
+ status: 'backlog',
152
+ description: 'Test description',
153
+ children: []
154
+ }];
155
+ printTree(items);
156
+ expect(logOutput).toContain('Test Item');
157
+ expect(logOutput).toContain(' "Test description"');
158
+ });
159
+
160
+ test('should not break when description is missing', () => {
161
+ const items = [{
162
+ id: 1,
163
+ title: 'No Description',
164
+ type: 'feature',
165
+ status: 'backlog',
166
+ children: []
167
+ }];
168
+ expect(() => printTree(items)).not.toThrow();
169
+ expect(logOutput).toContain('No Description');
170
+ expect(logOutput).not.toContain('""');
171
+ });
172
+
173
+ test('should handle empty string description', () => {
174
+ const items = [{
175
+ id: 1,
176
+ title: 'Empty Description',
177
+ type: 'feature',
178
+ status: 'backlog',
179
+ description: '',
180
+ children: []
181
+ }];
182
+ expect(() => printTree(items)).not.toThrow();
183
+ expect(logOutput).toContain('Empty Description');
184
+ // Empty string is falsy, so description should not display
185
+ expect(logOutput).not.toContain('""');
186
+ });
187
+
188
+ test('should display descriptions for nested items', () => {
189
+ const items = [{
190
+ id: 1,
191
+ title: 'Parent',
192
+ description: 'Parent description',
193
+ children: [{
194
+ id: 2,
195
+ title: 'Child',
196
+ description: 'Child description',
197
+ children: []
198
+ }]
199
+ }];
200
+ printTree(items);
201
+ expect(logOutput).toContain('Parent');
202
+ expect(logOutput).toContain(' "Parent description"');
203
+ expect(logOutput).toContain(' Child');
204
+ expect(logOutput).toContain(' "Child description"');
205
+ });
206
+ });
207
+
208
+ describe('getTree error handling', () => {
209
+ test('should build tree from flat structure', () => {
210
+ const rows = [
211
+ { id: 1, title: 'Epic', type: 'epic', status: 'backlog', parent_id: null, epic_id: 1 },
212
+ { id: 2, title: 'Feature', type: 'feature', status: 'backlog', parent_id: 1, epic_id: 1 }
213
+ ];
214
+
215
+ const itemsById = {};
216
+ const rootItems = [];
217
+
218
+ // Build lookup
219
+ rows.forEach(item => {
220
+ itemsById[item.id] = item;
221
+ item.children = [];
222
+ });
223
+
224
+ // Build tree
225
+ rows.forEach(item => {
226
+ if (item.parent_id && itemsById[item.parent_id]) {
227
+ itemsById[item.parent_id].children.push(item);
228
+ } else if (!item.parent_id) {
229
+ rootItems.push(item);
230
+ }
231
+ });
232
+
233
+ expect(rootItems).toHaveLength(1);
234
+ expect(rootItems[0].id).toBe(1);
235
+ expect(rootItems[0].children).toHaveLength(1);
236
+ expect(rootItems[0].children[0].id).toBe(2);
237
+ });
238
+
239
+ test('should handle empty rows array', () => {
240
+ const rows = [];
241
+ const itemsById = {};
242
+ const rootItems = [];
243
+
244
+ rows.forEach(item => {
245
+ itemsById[item.id] = item;
246
+ item.children = [];
247
+ });
248
+
249
+ rows.forEach(item => {
250
+ if (item.parent_id && itemsById[item.parent_id]) {
251
+ itemsById[item.parent_id].children.push(item);
252
+ } else if (!item.parent_id) {
253
+ rootItems.push(item);
254
+ }
255
+ });
256
+
257
+ expect(rootItems).toEqual([]);
258
+ });
259
+
260
+ test('should handle orphaned items (missing parent)', () => {
261
+ const rows = [
262
+ { id: 1, title: 'Orphan', type: 'feature', status: 'backlog', parent_id: 999, epic_id: null }
263
+ ];
264
+
265
+ const itemsById = {};
266
+ const rootItems = [];
267
+
268
+ rows.forEach(item => {
269
+ itemsById[item.id] = item;
270
+ item.children = [];
271
+ });
272
+
273
+ rows.forEach(item => {
274
+ if (item.parent_id && itemsById[item.parent_id]) {
275
+ itemsById[item.parent_id].children.push(item);
276
+ } else if (!item.parent_id) {
277
+ rootItems.push(item);
278
+ }
279
+ });
280
+
281
+ // Orphaned item (has parent_id but parent doesn't exist) is not added to rootItems
282
+ expect(rootItems).toHaveLength(0);
283
+ });
284
+
285
+ test('should handle multiple root items', () => {
286
+ const rows = [
287
+ { id: 1, title: 'Epic 1', type: 'epic', status: 'backlog', parent_id: null, epic_id: 1 },
288
+ { id: 2, title: 'Epic 2', type: 'epic', status: 'backlog', parent_id: null, epic_id: 2 }
289
+ ];
290
+
291
+ const itemsById = {};
292
+ const rootItems = [];
293
+
294
+ rows.forEach(item => {
295
+ itemsById[item.id] = item;
296
+ item.children = [];
297
+ });
298
+
299
+ rows.forEach(item => {
300
+ if (item.parent_id && itemsById[item.parent_id]) {
301
+ itemsById[item.parent_id].children.push(item);
302
+ } else if (!item.parent_id) {
303
+ rootItems.push(item);
304
+ }
305
+ });
306
+
307
+ expect(rootItems).toHaveLength(2);
308
+ });
309
+ });
310
+ });
@@ -0,0 +1,71 @@
1
+ Feature: Work set-mode updates CLAUDE.md if item is current
2
+ As a developer
3
+ I want to change a work item's mode with jettypod work set-mode
4
+ So I can adjust my approach without recreating the work item
5
+
6
+ Background:
7
+ Given jettypod is initialized
8
+ And CLAUDE.md exists
9
+
10
+ # Basic mode changes
11
+ Scenario: Set mode on current work item updates CLAUDE.md
12
+ Given I create a feature "Test Feature" with mode "speed"
13
+ And I start work on the feature
14
+ And CLAUDE.md mode is set to "speed"
15
+ When I set mode for current item to "stable"
16
+ Then the work item has mode "stable"
17
+ And CLAUDE.md mode is set to "stable"
18
+
19
+ Scenario: Set mode on non-current item does not update CLAUDE.md
20
+ Given I create a feature "Item 1" with mode "speed"
21
+ And I create a feature "Item 2" with mode "discovery"
22
+ And I start work on the feature "Item 1"
23
+ And CLAUDE.md mode is set to "speed"
24
+ When I set mode for item "Item 2" to "stable"
25
+ Then item "Item 2" has mode "stable"
26
+ But CLAUDE.md mode is set to "speed"
27
+
28
+ # Mode validation
29
+ Scenario: Set invalid mode shows error
30
+ Given I create a feature "Test Feature" with mode "speed"
31
+ When I try to set mode to "invalid"
32
+ Then I get error "Invalid mode"
33
+ And the work item still has mode "speed"
34
+
35
+ Scenario: Set mode is case-sensitive
36
+ Given I create a feature "Test Feature" with mode "speed"
37
+ When I try to set mode to "Speed"
38
+ Then I get error "Invalid mode"
39
+
40
+ # All valid modes
41
+ Scenario Outline: Change to all valid modes
42
+ Given I create a feature "Test Feature" with mode "speed"
43
+ And I start work on the feature
44
+ When I set mode for current item to "<new_mode>"
45
+ Then the work item has mode "<new_mode>"
46
+ And CLAUDE.md mode is set to "<new_mode>"
47
+
48
+ Examples:
49
+ | new_mode |
50
+ | discovery |
51
+ | stable |
52
+ | production |
53
+
54
+ # Mode persistence
55
+ Scenario: Mode change persists across work start/stop
56
+ Given I create a feature "Test Feature" with mode "speed"
57
+ And I start work on the feature
58
+ When I set mode for current item to "production"
59
+ And I stop work
60
+ And I start work on the feature
61
+ Then CLAUDE.md mode is set to "production"
62
+
63
+ # Multiple mode changes
64
+ Scenario: Change mode multiple times
65
+ Given I create a feature "Test Feature" with mode "speed"
66
+ And I start work on the feature
67
+ When I set mode for current item to "discovery"
68
+ And I set mode for current item to "stable"
69
+ And I set mode for current item to "production"
70
+ Then the work item has mode "production"
71
+ And CLAUDE.md mode is set to "production"
@@ -0,0 +1,88 @@
1
+ Feature: Work start sets CLAUDE.md mode from work item
2
+ As a developer
3
+ I want jettypod work start to set the CLAUDE.md mode from the work item's mode
4
+ So I automatically work in the correct mode for each work item
5
+
6
+ Background:
7
+ Given jettypod is initialized
8
+ And CLAUDE.md exists
9
+
10
+ # Basic mode inheritance
11
+ Scenario: Start work on feature in speed mode
12
+ Given I create a feature "Speed Feature" with mode "speed"
13
+ When I start work on the feature
14
+ Then CLAUDE.md mode is set to "speed"
15
+
16
+ Scenario: Start work on bug in stable mode
17
+ Given I create a bug "Stable Bug" with mode "stable"
18
+ When I start work on the bug
19
+ Then CLAUDE.md mode is set to "stable"
20
+
21
+ Scenario: Start work on chore inherits parent feature mode
22
+ Given I create a feature "Discovery Feature" with mode "discovery"
23
+ And I create a chore "Child Chore" without mode and parent feature
24
+ When I start work on the chore
25
+ Then CLAUDE.md mode is set to "discovery"
26
+
27
+ Scenario: Start work on feature in production mode
28
+ Given I create a feature "Production Feature" with mode "production"
29
+ When I start work on the feature
30
+ Then CLAUDE.md mode is set to "production"
31
+
32
+ # Mode switching between work items
33
+ Scenario: Switch from speed mode item to stable mode item
34
+ Given I create a feature "Speed Feature" with mode "speed"
35
+ And I create a bug "Stable Bug" with mode "stable"
36
+ And I start work on the feature
37
+ And CLAUDE.md mode is set to "speed"
38
+ When I stop work
39
+ And I start work on the bug
40
+ Then CLAUDE.md mode is set to "stable"
41
+
42
+ Scenario: Start work updates mode even if item already in progress
43
+ Given I create a feature "Test Feature" with mode "discovery"
44
+ And the feature status is "in_progress"
45
+ And CLAUDE.md has mode "speed"
46
+ When I start work on the feature
47
+ Then CLAUDE.md mode is set to "discovery"
48
+
49
+ # Epic handling - epics don't have modes
50
+ Scenario: Start work on epic (NULL mode)
51
+ Given I create an epic "Test Epic" without mode
52
+ When I start work on the epic
53
+ Then CLAUDE.md has no mode line
54
+ And the current work section exists
55
+
56
+ # Default mode from work item creation
57
+ Scenario: Start work on feature with default discovery mode
58
+ Given I create a feature "Default Feature" without mode
59
+ And the work item has mode "discovery"
60
+ When I start work on the feature
61
+ Then CLAUDE.md mode is set to "discovery"
62
+
63
+ # Hierarchical structure
64
+ Scenario: Start work on child item inherits its own mode, not parent's
65
+ Given I create an epic "Test Epic" without mode
66
+ And I create a feature "Speed Feature" with mode "speed" and parent epic
67
+ And I create a bug "Stable Bug" with mode "stable" and parent epic
68
+ When I start work on the speed feature
69
+ Then CLAUDE.md mode is set to "speed"
70
+ When I stop work
71
+ And I start work on the stable bug
72
+ Then CLAUDE.md mode is set to "stable"
73
+
74
+ # Mode persistence
75
+ Scenario: Work item mode persists in database after start
76
+ Given I create a feature "Test Feature" with mode "stable"
77
+ When I start work on the feature
78
+ And I stop work
79
+ Then the work item still has mode "stable"
80
+
81
+ # Edge cases
82
+ Scenario: Starting work multiple times preserves mode
83
+ Given I create a feature "Test Feature" with mode "production"
84
+ When I start work on the feature
85
+ And I stop work
86
+ And I start work on the feature
87
+ Then CLAUDE.md mode is set to "production"
88
+ And the work item still has mode "production"
package/full-test.txt ADDED
File without changes
package/install.sh ADDED
@@ -0,0 +1,89 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ # JettyPod Installer
5
+ # Usage: curl -fsSL https://raw.githubusercontent.com/spangbaryn/jettypod/main/install.sh | bash
6
+
7
+ REPO="spangbaryn/jettypod"
8
+ INSTALL_DIR="/usr/local/bin"
9
+ BINARY_NAME="jettypod"
10
+
11
+ # Colors for output
12
+ RED='\033[0;31m'
13
+ GREEN='\033[0;32m'
14
+ YELLOW='\033[1;33m'
15
+ NC='\033[0m' # No Color
16
+
17
+ # Error handling
18
+ error() {
19
+ echo -e "${RED}Error: $1${NC}" >&2
20
+ exit 1
21
+ }
22
+
23
+ success() {
24
+ echo -e "${GREEN}$1${NC}"
25
+ }
26
+
27
+ info() {
28
+ echo -e "${YELLOW}$1${NC}"
29
+ }
30
+
31
+ # Detect OS
32
+ OS="$(uname -s)"
33
+ case "$OS" in
34
+ Linux*) OS_TYPE=linux;;
35
+ Darwin*) OS_TYPE=macos;;
36
+ *) error "Unsupported operating system: $OS";;
37
+ esac
38
+
39
+ # Detect architecture
40
+ ARCH="$(uname -m)"
41
+ case "$ARCH" in
42
+ x86_64) ARCH_TYPE=x64;;
43
+ arm64) ARCH_TYPE=arm64;;
44
+ aarch64) ARCH_TYPE=arm64;;
45
+ *) error "Unsupported architecture: $ARCH";;
46
+ esac
47
+
48
+ info "Installing JettyPod for $OS_TYPE-$ARCH_TYPE..."
49
+
50
+ # Get latest release
51
+ info "Fetching latest release..."
52
+ LATEST_RELEASE=$(curl -s https://api.github.com/repos/$REPO/releases/latest)
53
+ VERSION=$(echo "$LATEST_RELEASE" | grep '"tag_name":' | sed -E 's/.*"v?([^"]+)".*/\1/')
54
+
55
+ if [ -z "$VERSION" ]; then
56
+ error "Could not fetch latest version"
57
+ fi
58
+
59
+ info "Latest version: $VERSION"
60
+
61
+ # Construct download URL
62
+ DOWNLOAD_URL="https://github.com/$REPO/releases/download/v$VERSION/${BINARY_NAME}-${OS_TYPE}-${ARCH_TYPE}"
63
+
64
+ # Download binary
65
+ info "Downloading JettyPod..."
66
+ TMP_FILE=$(mktemp)
67
+ if ! curl -fsSL "$DOWNLOAD_URL" -o "$TMP_FILE"; then
68
+ rm -f "$TMP_FILE"
69
+ error "Failed to download JettyPod from $DOWNLOAD_URL"
70
+ fi
71
+
72
+ # Make executable
73
+ chmod +x "$TMP_FILE"
74
+
75
+ # Install
76
+ info "Installing to $INSTALL_DIR..."
77
+ if [ -w "$INSTALL_DIR" ]; then
78
+ mv "$TMP_FILE" "$INSTALL_DIR/$BINARY_NAME"
79
+ else
80
+ sudo mv "$TMP_FILE" "$INSTALL_DIR/$BINARY_NAME"
81
+ fi
82
+
83
+ # Verify installation
84
+ if ! command -v $BINARY_NAME &> /dev/null; then
85
+ error "Installation failed - $BINARY_NAME not found in PATH"
86
+ fi
87
+
88
+ success "✓ JettyPod $VERSION installed successfully!"
89
+ success "Run 'jettypod init' to get started"