bamboo-coding 0.1.0__py3-none-any.whl

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 (62) hide show
  1. agent/__init__.py +0 -0
  2. agent/bamboo_git_agent/__init__.py +0 -0
  3. agent/bamboo_git_agent/capabilities/__init__.py +0 -0
  4. agent/bamboo_git_agent/capabilities/repo.py +189 -0
  5. agent/bamboo_git_agent/config.py +76 -0
  6. agent/bamboo_git_agent/controller_client.py +73 -0
  7. agent/bamboo_git_agent/journal.py +209 -0
  8. agent/bamboo_git_agent/main.py +164 -0
  9. agent/bamboo_git_agent/protocol.py +21 -0
  10. agent/bamboo_git_agent/status.py +59 -0
  11. api/__init__.py +2 -0
  12. api/commit.py +82 -0
  13. api/repository.py +592 -0
  14. bamboo_coding/__init__.py +3 -0
  15. bamboo_coding/client/__init__.py +3 -0
  16. bamboo_coding/client/capabilities/__init__.py +1 -0
  17. bamboo_coding/client/capabilities/repo.py +279 -0
  18. bamboo_coding/client/config.py +179 -0
  19. bamboo_coding/client/controller_client.py +112 -0
  20. bamboo_coding/client/journal.py +1 -0
  21. bamboo_coding/client/main.py +425 -0
  22. bamboo_coding/client/status.py +59 -0
  23. bamboo_coding/client/terminal_runtime.py +251 -0
  24. bamboo_coding/server/__init__.py +3 -0
  25. bamboo_coding/server/core/__init__.py +1 -0
  26. bamboo_coding/server/core/config.py +1 -0
  27. bamboo_coding/server/main.py +39 -0
  28. bamboo_coding/shared/__init__.py +39 -0
  29. bamboo_coding/shared/protocol.py +1 -0
  30. bamboo_coding-0.1.0.dist-info/METADATA +284 -0
  31. bamboo_coding-0.1.0.dist-info/RECORD +62 -0
  32. bamboo_coding-0.1.0.dist-info/WHEEL +5 -0
  33. bamboo_coding-0.1.0.dist-info/entry_points.txt +3 -0
  34. bamboo_coding-0.1.0.dist-info/top_level.txt +6 -0
  35. controller/__init__.py +0 -0
  36. controller/app/__init__.py +0 -0
  37. controller/app/api/__init__.py +0 -0
  38. controller/app/api/agents/__init__.py +3 -0
  39. controller/app/api/agents/ws.py +133 -0
  40. controller/app/api/public/__init__.py +11 -0
  41. controller/app/api/public/repos.py +207 -0
  42. controller/app/api/public/terminals.py +64 -0
  43. controller/app/core/__init__.py +0 -0
  44. controller/app/core/config.py +20 -0
  45. controller/app/core/errors.py +14 -0
  46. controller/app/db/__init__.py +0 -0
  47. controller/app/db/models.py +42 -0
  48. controller/app/db/session.py +74 -0
  49. controller/app/main.py +71 -0
  50. controller/app/schemas/__init__.py +23 -0
  51. controller/app/schemas/agent_messages.py +21 -0
  52. controller/app/schemas/public.py +3 -0
  53. controller/app/services/__init__.py +0 -0
  54. controller/app/services/agents.py +108 -0
  55. controller/app/services/registrations.py +17 -0
  56. controller/app/services/repositories.py +85 -0
  57. controller/app/services/router.py +140 -0
  58. controller/app/services/tasks.py +104 -0
  59. controller/app/services/terminals.py +148 -0
  60. git_utils.py +2238 -0
  61. shared/__init__.py +39 -0
  62. shared/protocol.py +253 -0
api/repository.py ADDED
@@ -0,0 +1,592 @@
1
+ """
2
+ Repository API Endpoints
3
+
4
+ Provides endpoints for repository file tree, diff, status, and branch management operations.
5
+ """
6
+
7
+ from sanic import Blueprint, response
8
+ from sanic.exceptions import InvalidUsage, ServerError, NotFound, SanicException
9
+
10
+ import git_utils
11
+ from git import InvalidGitRepositoryError
12
+
13
+
14
+ # Custom exception for conflict responses (HTTP 409)
15
+ class Conflict(SanicException):
16
+ status_code = 409
17
+
18
+
19
+ repository_bp = Blueprint("repository", url_prefix="/api/repo")
20
+
21
+
22
+ @repository_bp.route("/tree")
23
+ async def get_tree(request):
24
+ """
25
+ Get repository file tree.
26
+
27
+ Query params:
28
+ all: "true" to get all files, otherwise only changed files (default: false)
29
+ no_cache: "true" to bypass cache and get fresh data (default: false)
30
+
31
+ Returns:
32
+ JSON with file tree structure and file statuses
33
+ """
34
+ repo_path = request.ctx.repo_path
35
+
36
+ if not git_utils.is_valid_repository(repo_path):
37
+ raise InvalidUsage(f"Not a valid Git repository: {repo_path}")
38
+
39
+ # Check if user wants all files or just changed files
40
+ show_all = request.args.get("all", "false").lower() == "true"
41
+ changed_only = not show_all
42
+
43
+ # Check if cache should be bypassed
44
+ no_cache = request.args.get("no_cache", "false").lower() == "true"
45
+ if no_cache:
46
+ git_utils.clear_cache()
47
+
48
+ tree_data = git_utils.get_file_tree(repo_path, changed_only=changed_only)
49
+ return response.json(tree_data)
50
+
51
+
52
+ @repository_bp.route("/browse")
53
+ async def browse_directory(request):
54
+ """
55
+ Browse contents of a specific directory within the repository.
56
+
57
+ Query params:
58
+ path: Directory path relative to repository root (required)
59
+ Use "/" for root directory
60
+
61
+ Returns:
62
+ JSON with directory contents including files and subdirectories
63
+ """
64
+ repo_path = request.ctx.repo_path
65
+ dir_path = request.args.get("path")
66
+
67
+ if not dir_path and dir_path != "":
68
+ raise InvalidUsage("Missing 'path' parameter")
69
+
70
+ if not git_utils.is_valid_repository(repo_path):
71
+ raise InvalidUsage(f"Not a valid Git repository: {repo_path}")
72
+
73
+ try:
74
+ directory_data = git_utils.browse_directory(repo_path, dir_path)
75
+ return response.json(directory_data)
76
+ except ValueError as e:
77
+ # Path traversal or invalid path
78
+ raise InvalidUsage(str(e))
79
+ except FileNotFoundError as e:
80
+ raise NotFound(str(e))
81
+ except git_utils.GitPermissionError as e:
82
+ raise ServerError(str(e))
83
+ except Exception as e:
84
+ raise ServerError(f"Failed to browse directory: {str(e)}")
85
+
86
+
87
+ @repository_bp.route("/diff")
88
+ async def get_diff(request):
89
+ """
90
+ Get diff for a specific file.
91
+
92
+ Query params:
93
+ file: File path relative to repository root (required)
94
+ staged: "true" to get staged diff, otherwise unstaged (optional)
95
+ context: Number of context lines (default 3, optional)
96
+
97
+ Returns:
98
+ JSON with diff information
99
+ """
100
+ repo_path = request.ctx.repo_path
101
+ file_path = request.args.get("file")
102
+
103
+ if not file_path:
104
+ raise InvalidUsage("Missing 'file' parameter")
105
+
106
+ if not git_utils.is_valid_repository(repo_path):
107
+ raise InvalidUsage(f"Not a valid Git repository: {repo_path}")
108
+
109
+ staged = request.args.get("staged", "false").lower() == "true"
110
+ context = int(request.args.get("context", "3"))
111
+
112
+ diff_data = git_utils.get_file_diff(repo_path, file_path, staged, context)
113
+ return response.json(diff_data)
114
+
115
+
116
+ @repository_bp.route("/status")
117
+ async def get_status(request):
118
+ """
119
+ Get repository status.
120
+
121
+ Returns:
122
+ JSON with current branch, modified/added/deleted files, and ahead/behind counts
123
+ """
124
+ repo_path = request.ctx.repo_path
125
+
126
+ if not git_utils.is_valid_repository(repo_path):
127
+ raise InvalidUsage(f"Not a valid Git repository: {repo_path}")
128
+
129
+ status_data = git_utils.get_repo_status(repo_path)
130
+ return response.json(status_data)
131
+
132
+
133
+ @repository_bp.route("/branches")
134
+ async def get_branches(request):
135
+ """
136
+ Get all repository branches.
137
+
138
+ Query params:
139
+ type: Filter by 'local' or 'remote' (optional, default: all)
140
+
141
+ Returns:
142
+ JSON with local and remote branches
143
+ """
144
+ repo_path = request.ctx.repo_path
145
+
146
+ if not git_utils.is_valid_repository(repo_path):
147
+ raise InvalidUsage(f"Not a valid Git repository: {repo_path}")
148
+
149
+ try:
150
+ branches = git_utils.list_branches(repo_path)
151
+
152
+ # Filter by type if requested
153
+ branch_type = request.args.get("type")
154
+ if branch_type == "local":
155
+ branches = {'local': branches['local'], 'current': branches['current']}
156
+ elif branch_type == "remote":
157
+ branches = {'remote': branches['remote']}
158
+
159
+ return response.json(branches)
160
+
161
+ except git_utils.GitPermissionError as e:
162
+ raise ServerError(str(e))
163
+ except Exception as e:
164
+ raise ServerError(f"Failed to list branches: {str(e)}")
165
+
166
+
167
+ @repository_bp.route("/branches", methods=["POST"])
168
+ async def create_branch(request):
169
+ """
170
+ Create a new branch.
171
+
172
+ Request body:
173
+ {
174
+ "name": str (required),
175
+ "start_point": str (optional, default: HEAD)
176
+ }
177
+
178
+ Returns:
179
+ JSON with new branch details (HTTP 201)
180
+ """
181
+ repo_path = request.ctx.repo_path
182
+
183
+ if not git_utils.is_valid_repository(repo_path):
184
+ raise InvalidUsage(f"Not a valid Git repository: {repo_path}")
185
+
186
+ # Validate request body
187
+ if not request.json:
188
+ raise InvalidUsage("Request body required")
189
+
190
+ branch_name = request.json.get("name")
191
+ if not branch_name:
192
+ raise InvalidUsage("Missing 'name' in request body")
193
+
194
+ start_point = request.json.get("start_point")
195
+
196
+ try:
197
+ branch_data = git_utils.create_branch(repo_path, branch_name, start_point)
198
+ return response.json(branch_data, status=201)
199
+
200
+ except ValueError as e:
201
+ if "already exists" in str(e):
202
+ raise Conflict(str(e))
203
+ else:
204
+ raise InvalidUsage(str(e))
205
+ except git_utils.GitPermissionError as e:
206
+ raise ServerError(str(e))
207
+ except Exception as e:
208
+ raise ServerError(f"Failed to create branch: {str(e)}")
209
+
210
+
211
+ @repository_bp.route("/branches/checkout", methods=["PUT"])
212
+ async def checkout_branch(request):
213
+ """
214
+ Switch to a different branch.
215
+
216
+ Request body:
217
+ {
218
+ "name": str (required)
219
+ }
220
+
221
+ Returns:
222
+ JSON with new branch details and repository status
223
+ """
224
+ repo_path = request.ctx.repo_path
225
+
226
+ if not git_utils.is_valid_repository(repo_path):
227
+ raise InvalidUsage(f"Not a valid Git repository: {repo_path}")
228
+
229
+ # Validate request body
230
+ if not request.json:
231
+ raise InvalidUsage("Request body required")
232
+
233
+ branch_name = request.json.get("name")
234
+ if not branch_name:
235
+ raise InvalidUsage("Missing 'name' in request body")
236
+
237
+ try:
238
+ status_data = git_utils.switch_branch(repo_path, branch_name)
239
+ return response.json(status_data)
240
+
241
+ except ValueError as e:
242
+ raise NotFound(str(e))
243
+ except git_utils.GitPermissionError as e:
244
+ if "Uncommitted changes" in str(e):
245
+ raise Conflict(str(e))
246
+ else:
247
+ raise ServerError(str(e))
248
+ except Exception as e:
249
+ raise ServerError(f"Failed to switch branch: {str(e)}")
250
+
251
+
252
+ @repository_bp.route("/branches/<branch_name:path>", methods=["DELETE"])
253
+ async def delete_branch(request, branch_name):
254
+ """
255
+ Delete a branch.
256
+
257
+ Path params:
258
+ branch_name: Name of branch to delete
259
+
260
+ Query params:
261
+ force: "true" to delete even if unmerged (optional, default: false)
262
+
263
+ Returns:
264
+ HTTP 204 No Content on success
265
+ """
266
+ repo_path = request.ctx.repo_path
267
+
268
+ if not git_utils.is_valid_repository(repo_path):
269
+ raise InvalidUsage(f"Not a valid Git repository: {repo_path}")
270
+
271
+ force = request.args.get("force", "false").lower() == "true"
272
+
273
+ try:
274
+ git_utils.delete_branch(repo_path, branch_name, force)
275
+ return response.empty(status=204)
276
+
277
+ except ValueError as e:
278
+ if "Cannot delete current branch" in str(e):
279
+ raise InvalidUsage(str(e))
280
+ elif "not found" in str(e).lower():
281
+ raise NotFound(str(e))
282
+ elif "unmerged commits" in str(e):
283
+ raise Conflict(str(e))
284
+ else:
285
+ raise InvalidUsage(str(e))
286
+ except git_utils.GitPermissionError as e:
287
+ raise ServerError(str(e))
288
+ except Exception as e:
289
+ raise ServerError(f"Failed to delete branch: {str(e)}")
290
+
291
+
292
+ @repository_bp.route("/branches/merge", methods=["POST"])
293
+ async def merge_branch(request):
294
+ """
295
+ Merge a branch into the current branch.
296
+
297
+ Request body:
298
+ {
299
+ "source": str (required),
300
+ "message": str (optional)
301
+ }
302
+
303
+ Returns:
304
+ JSON with merge details
305
+ """
306
+ repo_path = request.ctx.repo_path
307
+
308
+ if not git_utils.is_valid_repository(repo_path):
309
+ raise InvalidUsage(f"Not a valid Git repository: {repo_path}")
310
+
311
+ # Validate request body
312
+ if not request.json:
313
+ raise InvalidUsage("Request body required")
314
+
315
+ source_branch = request.json.get("source")
316
+ if not source_branch:
317
+ raise InvalidUsage("Missing 'source' in request body")
318
+
319
+ merge_message = request.json.get("message")
320
+
321
+ try:
322
+ merge_data = git_utils.merge_branch(repo_path, source_branch, merge_message)
323
+ return response.json(merge_data)
324
+
325
+ except ValueError as e:
326
+ raise NotFound(str(e))
327
+ except git_utils.GitConflictError as e:
328
+ raise Conflict(str(e))
329
+ except git_utils.GitPermissionError as e:
330
+ raise ServerError(str(e))
331
+ except Exception as e:
332
+ raise ServerError(f"Failed to merge branch: {str(e)}")
333
+
334
+
335
+ @repository_bp.route("/file/content", methods=["GET"])
336
+ async def get_file_content(request):
337
+ """
338
+ Get file content for viewing in code editor.
339
+
340
+ Query params:
341
+ file_path: Path to file relative to repository root (required)
342
+
343
+ Returns:
344
+ JSON with file content and metadata:
345
+ {
346
+ 'file_path': str,
347
+ 'content': str (if text file),
348
+ 'encoding': str,
349
+ 'size': int,
350
+ 'mime_type': str,
351
+ 'is_binary': bool,
352
+ 'message': str (if binary or too large)
353
+ }
354
+ """
355
+ repo_path = request.ctx.repo_path
356
+ file_path = request.args.get("file_path")
357
+
358
+ if not file_path:
359
+ raise InvalidUsage("Missing 'file_path' parameter")
360
+
361
+ if not git_utils.is_valid_repository(repo_path):
362
+ raise InvalidUsage(f"Not a valid Git repository: {repo_path}")
363
+
364
+ try:
365
+ file_data = git_utils.read_file_content(repo_path, file_path)
366
+ return response.json(file_data)
367
+
368
+ except ValueError as e:
369
+ # Invalid path or path traversal
370
+ if "traversal" in str(e).lower():
371
+ raise InvalidUsage(str(e))
372
+ elif "outside repository" in str(e):
373
+ raise ServerError(str(e), status_code=403)
374
+ else:
375
+ raise InvalidUsage(str(e))
376
+ except FileNotFoundError as e:
377
+ raise NotFound(str(e))
378
+ except git_utils.GitPermissionError as e:
379
+ raise ServerError(str(e), status_code=500)
380
+ except Exception as e:
381
+ raise ServerError(f"Failed to read file content: {str(e)}")
382
+
383
+
384
+ @repository_bp.route("/file/content", methods=["PUT"])
385
+ async def save_file_content(request):
386
+ """
387
+ Save file content from code editor.
388
+
389
+ Query params:
390
+ file_path: Path to file relative to repository root (required)
391
+
392
+ JSON body:
393
+ {
394
+ 'content': str (file content to save),
395
+ 'encoding': str (optional, default: utf-8)
396
+ }
397
+
398
+ Returns:
399
+ JSON with save confirmation:
400
+ {
401
+ 'file_path': str,
402
+ 'size': int,
403
+ 'message': str
404
+ }
405
+ """
406
+ repo_path = request.ctx.repo_path
407
+ file_path = request.args.get("file_path")
408
+
409
+ if not file_path:
410
+ raise InvalidUsage("Missing 'file_path' parameter")
411
+
412
+ if not git_utils.is_valid_repository(repo_path):
413
+ raise InvalidUsage(f"Not a valid Git repository: {repo_path}")
414
+
415
+ # Get request body
416
+ try:
417
+ body = request.json
418
+ if not body:
419
+ raise InvalidUsage("Missing request body")
420
+
421
+ content = body.get('content')
422
+ if content is None:
423
+ raise InvalidUsage("Missing 'content' in request body")
424
+
425
+ encoding = body.get('encoding', 'utf-8')
426
+
427
+ except Exception as e:
428
+ raise InvalidUsage(f"Invalid request body: {str(e)}")
429
+
430
+ try:
431
+ result = git_utils.write_file_content(repo_path, file_path, content, encoding)
432
+ return response.json(result)
433
+
434
+ except ValueError as e:
435
+ # Invalid path or path traversal
436
+ if "traversal" in str(e).lower():
437
+ raise InvalidUsage(str(e))
438
+ elif "outside repository" in str(e):
439
+ raise ServerError(str(e), status_code=403)
440
+ else:
441
+ raise InvalidUsage(str(e))
442
+ except git_utils.GitPermissionError as e:
443
+ raise ServerError(str(e), status_code=500)
444
+ except Exception as e:
445
+ raise ServerError(f"Failed to save file content: {str(e)}")
446
+
447
+
448
+ @repository_bp.route("/stage", methods=["POST"])
449
+ async def stage_files(request):
450
+ """
451
+ Stage files for commit.
452
+
453
+ Request body:
454
+ {
455
+ "file_path": str (optional, for single file),
456
+ "stage_all": bool (optional, to stage all changed files)
457
+ }
458
+
459
+ Returns:
460
+ JSON with staging result
461
+ """
462
+ repo_path = request.ctx.repo_path
463
+
464
+ if not git_utils.is_valid_repository(repo_path):
465
+ raise InvalidUsage(f"Not a valid Git repository: {repo_path}")
466
+
467
+ # Validate request body
468
+ if not request.json:
469
+ raise InvalidUsage("Request body required")
470
+
471
+ file_path = request.json.get("file_path")
472
+ stage_all = request.json.get("stage_all", False)
473
+
474
+ if not file_path and not stage_all:
475
+ raise InvalidUsage("Missing 'file_path' or 'stage_all' in request body")
476
+
477
+ try:
478
+ if stage_all:
479
+ result = git_utils.stage_all_files(repo_path)
480
+ else:
481
+ result = git_utils.stage_file(repo_path, file_path)
482
+
483
+ # Clear cache to ensure fresh data on next request
484
+ git_utils.clear_cache()
485
+
486
+ return response.json(result)
487
+
488
+ except FileNotFoundError as e:
489
+ raise NotFound(str(e))
490
+ except ValueError as e:
491
+ raise InvalidUsage(str(e))
492
+ except git_utils.GitPermissionError as e:
493
+ raise ServerError(str(e))
494
+ except Exception as e:
495
+ raise ServerError(f"Failed to stage files: {str(e)}")
496
+
497
+
498
+ @repository_bp.route("/unstage", methods=["POST"])
499
+ async def unstage_files(request):
500
+ """
501
+ Remove files from staging area.
502
+
503
+ Request body:
504
+ {
505
+ "file_path": str (required)
506
+ }
507
+
508
+ Returns:
509
+ JSON with unstaging result
510
+ """
511
+ repo_path = request.ctx.repo_path
512
+
513
+ if not git_utils.is_valid_repository(repo_path):
514
+ raise InvalidUsage(f"Not a valid Git repository: {repo_path}")
515
+
516
+ # Validate request body
517
+ if not request.json:
518
+ raise InvalidUsage("Request body required")
519
+
520
+ file_path = request.json.get("file_path")
521
+ if not file_path:
522
+ raise InvalidUsage("Missing 'file_path' in request body")
523
+
524
+ try:
525
+ result = git_utils.unstage_file(repo_path, file_path)
526
+
527
+ # Clear cache to ensure fresh data on next request
528
+ git_utils.clear_cache()
529
+
530
+ return response.json(result)
531
+
532
+ except ValueError as e:
533
+ raise InvalidUsage(str(e))
534
+ except git_utils.GitPermissionError as e:
535
+ raise ServerError(str(e))
536
+ except Exception as e:
537
+ raise ServerError(f"Failed to unstage file: {str(e)}")
538
+
539
+
540
+ @repository_bp.route("/commit", methods=["POST"])
541
+ async def create_commit(request):
542
+ """
543
+ Create a commit with staged changes.
544
+
545
+ Request body:
546
+ {
547
+ "message": str (required),
548
+ "author_name": str (optional),
549
+ "author_email": str (optional)
550
+ }
551
+
552
+ Returns:
553
+ JSON with commit details (HTTP 201)
554
+ """
555
+ repo_path = request.ctx.repo_path
556
+
557
+ if not git_utils.is_valid_repository(repo_path):
558
+ raise InvalidUsage(f"Not a valid Git repository: {repo_path}")
559
+
560
+ # Validate request body
561
+ if not request.json:
562
+ raise InvalidUsage("Request body required")
563
+
564
+ message = request.json.get("message")
565
+ if not message:
566
+ raise InvalidUsage("Missing 'message' in request body")
567
+
568
+ author_name = request.json.get("author_name")
569
+ author_email = request.json.get("author_email")
570
+
571
+ try:
572
+ result = git_utils.create_commit(repo_path, message, author_name, author_email)
573
+
574
+ # Clear cache to ensure fresh data on next request
575
+ git_utils.clear_cache()
576
+
577
+ return response.json(result, status=201)
578
+
579
+ except ValueError as e:
580
+ # Empty message, too long, no staged changes, invalid email
581
+ raise InvalidUsage(str(e))
582
+ except git_utils.GitPermissionError as e:
583
+ if "locked" in str(e).lower():
584
+ raise Conflict(str(e))
585
+ else:
586
+ raise ServerError(str(e))
587
+ except git_utils.GitCommandError as e:
588
+ # Commit hook failure
589
+ raise ServerError(str(e))
590
+ except Exception as e:
591
+ raise ServerError(f"Failed to create commit: {str(e)}")
592
+
@@ -0,0 +1,3 @@
1
+ """Bamboo Coding package."""
2
+
3
+ __all__ = ["client", "server", "shared"]
@@ -0,0 +1,3 @@
1
+ from .config import AgentConfig, RepositoryConfig, load_config
2
+
3
+ __all__ = ["AgentConfig", "RepositoryConfig", "load_config"]
@@ -0,0 +1 @@
1
+ """Client capability handlers."""