Skip to content

Commit e148ed9

Browse files
committed
Implemented HatchboxManager to manage the lifecycle of Hatchboxes. Fixes #41.
1 parent bbd0f24 commit e148ed9

14 files changed

+1016
-542
lines changed

src/commands/start.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,25 @@ import { GitHubService } from '../lib/GitHubService.js'
55
// Mock the GitHubService
66
vi.mock('../lib/GitHubService.js')
77

8+
// Mock the HatchboxManager and its dependencies
9+
vi.mock('../lib/HatchboxManager.js', () => ({
10+
HatchboxManager: vi.fn(() => ({
11+
createHatchbox: vi.fn().mockResolvedValue({
12+
id: 'test-hatchbox-123',
13+
path: '/test/path',
14+
branch: 'test-branch',
15+
type: 'issue',
16+
identifier: 123,
17+
port: 3123,
18+
createdAt: new Date(),
19+
githubData: null,
20+
}),
21+
})),
22+
}))
23+
vi.mock('../lib/GitWorktreeManager.js')
24+
vi.mock('../lib/EnvironmentManager.js')
25+
vi.mock('../lib/ClaudeContextManager.js')
26+
827
// Mock the logger to prevent console output during tests
928
vi.mock('../utils/logger.js', () => ({
1029
logger: {
@@ -14,6 +33,13 @@ vi.mock('../utils/logger.js', () => ({
1433
debug: vi.fn(),
1534
success: vi.fn(),
1635
},
36+
createLogger: vi.fn(() => ({
37+
info: vi.fn(),
38+
error: vi.fn(),
39+
warn: vi.fn(),
40+
debug: vi.fn(),
41+
success: vi.fn(),
42+
})),
1743
}))
1844

1945
describe('StartCommand', () => {

src/commands/start.ts

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { logger } from '../utils/logger.js'
22
import { GitHubService } from '../lib/GitHubService.js'
3+
import { HatchboxManager } from '../lib/HatchboxManager.js'
4+
import { GitWorktreeManager } from '../lib/GitWorktreeManager.js'
5+
import { EnvironmentManager } from '../lib/EnvironmentManager.js'
6+
import { ClaudeContextManager } from '../lib/ClaudeContextManager.js'
37
import type { StartOptions } from '../types/index.js'
48

59
export interface StartCommandInput {
@@ -16,9 +20,18 @@ export interface ParsedInput {
1620

1721
export class StartCommand {
1822
private gitHubService: GitHubService
23+
private hatchboxManager: HatchboxManager
1924

20-
constructor(gitHubService?: GitHubService) {
25+
constructor(gitHubService?: GitHubService, hatchboxManager?: HatchboxManager) {
2126
this.gitHubService = gitHubService ?? new GitHubService()
27+
this.hatchboxManager =
28+
hatchboxManager ??
29+
new HatchboxManager(
30+
new GitWorktreeManager(),
31+
this.gitHubService,
32+
new EnvironmentManager(),
33+
new ClaudeContextManager()
34+
)
2235
}
2336

2437
/**
@@ -32,15 +45,31 @@ export class StartCommand {
3245
// Step 2: Validate based on type
3346
await this.validateInput(parsed)
3447

35-
// Step 3: Log success and prepare for next steps (HatchboxManager)
48+
// Step 3: Log success and create hatchbox
3649
logger.info(`✅ Validated input: ${this.formatParsedInput(parsed)}`)
3750

38-
// TODO: Issue #41 - Create hatchbox using HatchboxManager
39-
// The HatchboxManager will orchestrate the creation of the workspace
40-
// including worktree setup, environment configuration, and Claude integration
41-
logger.warn(
42-
'Hatchbox creation not yet implemented (requires Issue #41 - HatchboxManager)'
43-
)
51+
// Step 4: Create hatchbox using HatchboxManager
52+
const identifier =
53+
parsed.type === 'branch'
54+
? parsed.branchName ?? ''
55+
: parsed.number ?? 0
56+
57+
const hatchbox = await this.hatchboxManager.createHatchbox({
58+
type: parsed.type,
59+
identifier,
60+
originalInput: parsed.originalInput,
61+
options: {
62+
...(input.options.urgent !== undefined && { urgent: input.options.urgent }),
63+
skipClaude: !input.options.claude,
64+
},
65+
})
66+
67+
logger.success(`✅ Created hatchbox: ${hatchbox.id} at ${hatchbox.path}`)
68+
logger.info(` Branch: ${hatchbox.branch}`)
69+
logger.info(` Port: ${hatchbox.port}`)
70+
if (hatchbox.githubData?.title) {
71+
logger.info(` Title: ${hatchbox.githubData.title}`)
72+
}
4473
} catch (error) {
4574
if (error instanceof Error) {
4675
logger.error(`❌ ${error.message}`)

src/lib/EnvironmentManager.integration.test.ts

Lines changed: 23 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,11 @@ describe('EnvironmentManager integration', () => {
2222
const envPath = path.join(testDir, '.env')
2323

2424
// Create .env file
25-
const result1 = await manager.setEnvVar(envPath, 'DATABASE_URL', 'postgres://localhost/db')
26-
expect(result1.success).toBe(true)
25+
await manager.setEnvVar(envPath, 'DATABASE_URL', 'postgres://localhost/db')
2726

2827
// Add more variables
29-
const result2 = await manager.setEnvVar(envPath, 'API_KEY', 'test-key-123')
30-
expect(result2.success).toBe(true)
31-
32-
const result3 = await manager.setEnvVar(envPath, 'NODE_ENV', 'development')
33-
expect(result3.success).toBe(true)
28+
await manager.setEnvVar(envPath, 'API_KEY', 'test-key-123')
29+
await manager.setEnvVar(envPath, 'NODE_ENV', 'development')
3430

3531
// Set port for workspace
3632
const port = await manager.setPortForWorkspace(envPath, 42)
@@ -77,8 +73,7 @@ NODE_ENV="development"`
7773
await fs.writeFile(envPath, initialContent, 'utf8')
7874

7975
// Update a value
80-
const result = await manager.setEnvVar(envPath, 'API_KEY', 'new-key')
81-
expect(result.success).toBe(true)
76+
await manager.setEnvVar(envPath, 'API_KEY', 'new-key')
8277

8378
// Read the file content directly
8479
const fileContent = await fs.readFile(envPath, 'utf8')
@@ -105,24 +100,24 @@ NODE_ENV="development"`
105100
await manager.setEnvVar(envPath, 'KEY2', 'value2')
106101

107102
// Update with backup
108-
const result = await manager.setEnvVar(envPath, 'KEY1', 'new-value', true)
109-
expect(result.success).toBe(true)
110-
expect(result.backupPath).toBeDefined()
103+
const backupPath = await manager.setEnvVar(envPath, 'KEY1', 'new-value', true)
104+
expect(backupPath).toBeDefined()
105+
expect(typeof backupPath).toBe('string')
111106

112107
// Verify backup exists
113-
const backupExists = await fs.pathExists(result.backupPath!)
108+
const backupExists = await fs.pathExists(backupPath as string)
114109
expect(backupExists).toBe(true)
115110

116111
// Verify backup contains old value
117-
const backupContent = await fs.readFile(result.backupPath!, 'utf8')
112+
const backupContent = await fs.readFile(backupPath as string, 'utf8')
118113
expect(backupContent).toContain('KEY1="value1"')
119114

120115
// Verify main file has new value
121116
const mainContent = await manager.readEnvFile(envPath)
122117
expect(mainContent.get('KEY1')).toBe('new-value')
123118

124119
// Recovery: restore from backup
125-
await manager.copyEnvFile(result.backupPath!, envPath)
120+
await manager.copyEnvFile(backupPath as string, envPath)
126121
const restoredContent = await manager.readEnvFile(envPath)
127122
expect(restoredContent.get('KEY1')).toBe('value1')
128123
})
@@ -139,8 +134,7 @@ NODE_ENV="development"`
139134
]
140135

141136
for (const [key, value] of specialValues) {
142-
const result = await manager.setEnvVar(envPath, key, value)
143-
expect(result.success).toBe(true)
137+
await manager.setEnvVar(envPath, key, value)
144138
}
145139

146140
// Read back and verify
@@ -183,12 +177,10 @@ NODE_ENV="development"`
183177
it('should handle missing directories gracefully', async () => {
184178
const envPath = path.join(testDir, 'nonexistent', 'directory', '.env')
185179

186-
// Should fail because parent directory doesn't exist
187-
const result = await manager.setEnvVar(envPath, 'KEY', 'value')
188-
189-
expect(result.success).toBe(false)
190-
expect(result.error).toBeDefined()
191-
expect(result.error).toContain('no such file or directory')
180+
// Should throw because parent directory doesn't exist
181+
await expect(
182+
manager.setEnvVar(envPath, 'KEY', 'value')
183+
).rejects.toThrow('no such file or directory')
192184
})
193185

194186
it('should handle read-only files', async () => {
@@ -197,10 +189,10 @@ NODE_ENV="development"`
197189
await fs.writeFile(envPath, 'KEY="value"', 'utf8')
198190
await fs.chmod(envPath, 0o444) // Read-only
199191

200-
const result = await manager.setEnvVar(envPath, 'KEY', 'new-value')
201-
202-
expect(result.success).toBe(false)
203-
expect(result.error).toBeDefined()
192+
// Should throw because file is read-only
193+
await expect(
194+
manager.setEnvVar(envPath, 'KEY', 'new-value')
195+
).rejects.toThrow('permission denied')
204196

205197
// Cleanup
206198
await fs.chmod(envPath, 0o644)
@@ -231,23 +223,20 @@ _VALID_KEY="value"`
231223
const envPath = path.join(testDir, '.env')
232224

233225
// Test case 1: Create new file
234-
const result1 = await manager.setEnvVar(envPath, 'KEY', 'value')
235-
expect(result1.success).toBe(true)
226+
await manager.setEnvVar(envPath, 'KEY', 'value')
236227

237228
let content = await fs.readFile(envPath, 'utf8')
238229
expect(content).toBe('KEY="value"')
239230

240231
// Test case 2: Add variable to existing file
241-
const result2 = await manager.setEnvVar(envPath, 'KEY2', 'value2')
242-
expect(result2.success).toBe(true)
232+
await manager.setEnvVar(envPath, 'KEY2', 'value2')
243233

244234
content = await fs.readFile(envPath, 'utf8')
245235
expect(content).toContain('KEY="value"')
246236
expect(content).toContain('KEY2="value2"')
247237

248238
// Test case 3: Update existing variable
249-
const result3 = await manager.setEnvVar(envPath, 'KEY', 'new-value')
250-
expect(result3.success).toBe(true)
239+
await manager.setEnvVar(envPath, 'KEY', 'new-value')
251240

252241
content = await fs.readFile(envPath, 'utf8')
253242
expect(content).toContain('KEY="new-value"')
@@ -270,8 +259,7 @@ _VALID_KEY="value"`
270259

271260
// The bash script uses: escaped_value="${var_value//\"/\\\"}"
272261
const valueWithQuotes = 'value with "quotes" inside'
273-
const result = await manager.setEnvVar(envPath, 'KEY', valueWithQuotes)
274-
expect(result.success).toBe(true)
262+
await manager.setEnvVar(envPath, 'KEY', valueWithQuotes)
275263

276264
const fileContent = await fs.readFile(envPath, 'utf8')
277265
expect(fileContent).toBe('KEY="value with \\"quotes\\" inside"')

0 commit comments

Comments
 (0)