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.
- agent/__init__.py +0 -0
- agent/bamboo_git_agent/__init__.py +0 -0
- agent/bamboo_git_agent/capabilities/__init__.py +0 -0
- agent/bamboo_git_agent/capabilities/repo.py +189 -0
- agent/bamboo_git_agent/config.py +76 -0
- agent/bamboo_git_agent/controller_client.py +73 -0
- agent/bamboo_git_agent/journal.py +209 -0
- agent/bamboo_git_agent/main.py +164 -0
- agent/bamboo_git_agent/protocol.py +21 -0
- agent/bamboo_git_agent/status.py +59 -0
- api/__init__.py +2 -0
- api/commit.py +82 -0
- api/repository.py +592 -0
- bamboo_coding/__init__.py +3 -0
- bamboo_coding/client/__init__.py +3 -0
- bamboo_coding/client/capabilities/__init__.py +1 -0
- bamboo_coding/client/capabilities/repo.py +279 -0
- bamboo_coding/client/config.py +179 -0
- bamboo_coding/client/controller_client.py +112 -0
- bamboo_coding/client/journal.py +1 -0
- bamboo_coding/client/main.py +425 -0
- bamboo_coding/client/status.py +59 -0
- bamboo_coding/client/terminal_runtime.py +251 -0
- bamboo_coding/server/__init__.py +3 -0
- bamboo_coding/server/core/__init__.py +1 -0
- bamboo_coding/server/core/config.py +1 -0
- bamboo_coding/server/main.py +39 -0
- bamboo_coding/shared/__init__.py +39 -0
- bamboo_coding/shared/protocol.py +1 -0
- bamboo_coding-0.1.0.dist-info/METADATA +284 -0
- bamboo_coding-0.1.0.dist-info/RECORD +62 -0
- bamboo_coding-0.1.0.dist-info/WHEEL +5 -0
- bamboo_coding-0.1.0.dist-info/entry_points.txt +3 -0
- bamboo_coding-0.1.0.dist-info/top_level.txt +6 -0
- controller/__init__.py +0 -0
- controller/app/__init__.py +0 -0
- controller/app/api/__init__.py +0 -0
- controller/app/api/agents/__init__.py +3 -0
- controller/app/api/agents/ws.py +133 -0
- controller/app/api/public/__init__.py +11 -0
- controller/app/api/public/repos.py +207 -0
- controller/app/api/public/terminals.py +64 -0
- controller/app/core/__init__.py +0 -0
- controller/app/core/config.py +20 -0
- controller/app/core/errors.py +14 -0
- controller/app/db/__init__.py +0 -0
- controller/app/db/models.py +42 -0
- controller/app/db/session.py +74 -0
- controller/app/main.py +71 -0
- controller/app/schemas/__init__.py +23 -0
- controller/app/schemas/agent_messages.py +21 -0
- controller/app/schemas/public.py +3 -0
- controller/app/services/__init__.py +0 -0
- controller/app/services/agents.py +108 -0
- controller/app/services/registrations.py +17 -0
- controller/app/services/repositories.py +85 -0
- controller/app/services/router.py +140 -0
- controller/app/services/tasks.py +104 -0
- controller/app/services/terminals.py +148 -0
- git_utils.py +2238 -0
- shared/__init__.py +39 -0
- shared/protocol.py +253 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import inspect
|
|
5
|
+
import shlex
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Awaitable, Callable
|
|
9
|
+
|
|
10
|
+
from bamboo_ssh import TerminalSession, stream_terminal_output
|
|
11
|
+
|
|
12
|
+
from bamboo_coding.shared.protocol import (
|
|
13
|
+
TerminalClosedMessage,
|
|
14
|
+
TerminalHeartbeatMessage,
|
|
15
|
+
TerminalInputMessage,
|
|
16
|
+
TerminalOpenMessage,
|
|
17
|
+
TerminalOutputMessage,
|
|
18
|
+
TerminalStatusMessage,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
MessageCallback = Callable[[Any], Awaitable[None] | None]
|
|
23
|
+
DEFAULT_OUTPUT_FLUSH_INTERVAL = 0.05
|
|
24
|
+
DEFAULT_HEARTBEAT_INTERVAL = 10.0
|
|
25
|
+
MAX_PENDING_OUTPUT_CHARS = 262144
|
|
26
|
+
TRUNCATED_OUTPUT_NOTICE = "\r\n[terminal output truncated while relay caught up]\r\n"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def build_initial_cwd_command(path: str) -> str:
|
|
30
|
+
if not path:
|
|
31
|
+
return ''
|
|
32
|
+
target = Path(path).expanduser()
|
|
33
|
+
resolved = target.resolve(strict=False)
|
|
34
|
+
working_dir = resolved if resolved.is_dir() else resolved.parent
|
|
35
|
+
return f"cd {shlex.quote(str(working_dir))}\n"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class ActiveTerminalSession:
|
|
40
|
+
terminal_id: str
|
|
41
|
+
session: TerminalSession
|
|
42
|
+
reader_task: asyncio.Task | None = None
|
|
43
|
+
output_task: asyncio.Task | None = None
|
|
44
|
+
heartbeat_task: asyncio.Task | None = None
|
|
45
|
+
output_event: asyncio.Event = field(default_factory=asyncio.Event)
|
|
46
|
+
pending_output: list[str] = field(default_factory=list)
|
|
47
|
+
pending_output_chars: int = 0
|
|
48
|
+
output_truncated: bool = False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ClientTerminalRuntime:
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
*,
|
|
55
|
+
send_status: MessageCallback,
|
|
56
|
+
send_output: MessageCallback,
|
|
57
|
+
send_closed: MessageCallback,
|
|
58
|
+
send_heartbeat: MessageCallback | None = None,
|
|
59
|
+
output_flush_interval: float = DEFAULT_OUTPUT_FLUSH_INTERVAL,
|
|
60
|
+
heartbeat_interval: float = DEFAULT_HEARTBEAT_INTERVAL,
|
|
61
|
+
max_pending_output_chars: int = MAX_PENDING_OUTPUT_CHARS,
|
|
62
|
+
):
|
|
63
|
+
self.send_status = send_status
|
|
64
|
+
self.send_output = send_output
|
|
65
|
+
self.send_closed = send_closed
|
|
66
|
+
self.send_heartbeat = send_heartbeat or (lambda _message: None)
|
|
67
|
+
self.output_flush_interval = output_flush_interval
|
|
68
|
+
self.heartbeat_interval = heartbeat_interval
|
|
69
|
+
self.max_pending_output_chars = max_pending_output_chars
|
|
70
|
+
self.sessions: dict[str, ActiveTerminalSession] = {}
|
|
71
|
+
|
|
72
|
+
async def open_terminal(self, message: TerminalOpenMessage) -> None:
|
|
73
|
+
await self.close_terminal(message.terminal_id, reason='replaced', emit_closed=False)
|
|
74
|
+
await self._emit(
|
|
75
|
+
self.send_status,
|
|
76
|
+
TerminalStatusMessage(terminal_id=message.terminal_id, status='starting'),
|
|
77
|
+
)
|
|
78
|
+
session = TerminalSession.start(cols=message.cols, rows=message.rows, command=build_initial_cwd_command(message.path))
|
|
79
|
+
active = ActiveTerminalSession(
|
|
80
|
+
terminal_id=message.terminal_id,
|
|
81
|
+
session=session,
|
|
82
|
+
)
|
|
83
|
+
self.sessions[message.terminal_id] = active
|
|
84
|
+
active.output_task = asyncio.create_task(
|
|
85
|
+
self._flush_output_loop(message.terminal_id),
|
|
86
|
+
name=f'terminal-output:{message.terminal_id}',
|
|
87
|
+
)
|
|
88
|
+
active.heartbeat_task = asyncio.create_task(
|
|
89
|
+
self._heartbeat_loop(message.terminal_id),
|
|
90
|
+
name=f'terminal-heartbeat:{message.terminal_id}',
|
|
91
|
+
)
|
|
92
|
+
active.reader_task = asyncio.create_task(
|
|
93
|
+
self._stream_terminal(message.terminal_id, session),
|
|
94
|
+
name=f'terminal:{message.terminal_id}',
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
async def input_terminal(self, message: TerminalInputMessage) -> None:
|
|
98
|
+
active = self.sessions.get(message.terminal_id)
|
|
99
|
+
if active is None:
|
|
100
|
+
return
|
|
101
|
+
try:
|
|
102
|
+
active.session.write(message.data)
|
|
103
|
+
except Exception as exc:
|
|
104
|
+
await self._emit(
|
|
105
|
+
self.send_status,
|
|
106
|
+
TerminalStatusMessage(terminal_id=message.terminal_id, status='error', message=str(exc)),
|
|
107
|
+
)
|
|
108
|
+
await self.close_terminal(message.terminal_id, reason='write_failed')
|
|
109
|
+
|
|
110
|
+
async def resize_terminal(self, terminal_id: str, *, cols: int, rows: int) -> None:
|
|
111
|
+
active = self.sessions.get(terminal_id)
|
|
112
|
+
if active is None:
|
|
113
|
+
return
|
|
114
|
+
try:
|
|
115
|
+
active.session.resize(cols, rows)
|
|
116
|
+
except Exception as exc:
|
|
117
|
+
await self._emit(
|
|
118
|
+
self.send_status,
|
|
119
|
+
TerminalStatusMessage(terminal_id=terminal_id, status='error', message=str(exc)),
|
|
120
|
+
)
|
|
121
|
+
await self.close_terminal(terminal_id, reason='resize_failed')
|
|
122
|
+
|
|
123
|
+
async def close_terminal(
|
|
124
|
+
self,
|
|
125
|
+
terminal_id: str,
|
|
126
|
+
*,
|
|
127
|
+
reason: str = 'closed',
|
|
128
|
+
emit_closed: bool = True,
|
|
129
|
+
exit_code: int | None = None,
|
|
130
|
+
) -> None:
|
|
131
|
+
active = self.sessions.pop(terminal_id, None)
|
|
132
|
+
if active is None:
|
|
133
|
+
return
|
|
134
|
+
active.session.close()
|
|
135
|
+
current_task = asyncio.current_task()
|
|
136
|
+
for task in (active.reader_task, active.output_task, active.heartbeat_task):
|
|
137
|
+
if task is None or task is current_task:
|
|
138
|
+
continue
|
|
139
|
+
task.cancel()
|
|
140
|
+
try:
|
|
141
|
+
await task
|
|
142
|
+
except asyncio.CancelledError:
|
|
143
|
+
pass
|
|
144
|
+
await self._flush_pending_output(active)
|
|
145
|
+
if emit_closed:
|
|
146
|
+
if exit_code is None:
|
|
147
|
+
exit_code = getattr(getattr(active.session, 'process', None), 'returncode', None)
|
|
148
|
+
await self._emit(
|
|
149
|
+
self.send_closed,
|
|
150
|
+
TerminalClosedMessage(terminal_id=terminal_id, reason=reason, exit_code=exit_code),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
async def close_all(self, reason: str = 'shutdown') -> None:
|
|
154
|
+
for terminal_id in list(self.sessions):
|
|
155
|
+
await self.close_terminal(terminal_id, reason=reason)
|
|
156
|
+
|
|
157
|
+
async def _stream_terminal(self, terminal_id: str, session: TerminalSession) -> None:
|
|
158
|
+
async def send_json(payload: dict) -> None:
|
|
159
|
+
message_type = payload.get('type')
|
|
160
|
+
if message_type == 'ready':
|
|
161
|
+
await self._emit(self.send_status, TerminalStatusMessage(terminal_id=terminal_id, status='ready'))
|
|
162
|
+
elif message_type == 'cmd':
|
|
163
|
+
self._queue_output(terminal_id, str(payload.get('data') or ''))
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
await stream_terminal_output(session, send_json)
|
|
167
|
+
except asyncio.CancelledError:
|
|
168
|
+
raise
|
|
169
|
+
except Exception as exc:
|
|
170
|
+
if terminal_id in self.sessions:
|
|
171
|
+
await self._emit(
|
|
172
|
+
self.send_status,
|
|
173
|
+
TerminalStatusMessage(terminal_id=terminal_id, status='error', message=str(exc)),
|
|
174
|
+
)
|
|
175
|
+
finally:
|
|
176
|
+
active = self.sessions.get(terminal_id)
|
|
177
|
+
if active is not None:
|
|
178
|
+
exit_code = getattr(getattr(active.session, 'process', None), 'returncode', None)
|
|
179
|
+
await self.close_terminal(terminal_id, reason='session_ended', exit_code=exit_code)
|
|
180
|
+
|
|
181
|
+
def _queue_output(self, terminal_id: str, data: str) -> None:
|
|
182
|
+
if not data:
|
|
183
|
+
return
|
|
184
|
+
active = self.sessions.get(terminal_id)
|
|
185
|
+
if active is None:
|
|
186
|
+
return
|
|
187
|
+
active.pending_output.append(data)
|
|
188
|
+
active.pending_output_chars += len(data)
|
|
189
|
+
while active.pending_output_chars > self.max_pending_output_chars and active.pending_output:
|
|
190
|
+
removed = active.pending_output.pop(0)
|
|
191
|
+
active.pending_output_chars -= len(removed)
|
|
192
|
+
active.output_truncated = True
|
|
193
|
+
active.output_event.set()
|
|
194
|
+
|
|
195
|
+
async def _flush_output_loop(self, terminal_id: str) -> None:
|
|
196
|
+
try:
|
|
197
|
+
while True:
|
|
198
|
+
active = self.sessions.get(terminal_id)
|
|
199
|
+
if active is None:
|
|
200
|
+
return
|
|
201
|
+
await active.output_event.wait()
|
|
202
|
+
await asyncio.sleep(self.output_flush_interval)
|
|
203
|
+
active = self.sessions.get(terminal_id)
|
|
204
|
+
if active is None:
|
|
205
|
+
return
|
|
206
|
+
await self._flush_pending_output(active)
|
|
207
|
+
except asyncio.CancelledError:
|
|
208
|
+
raise
|
|
209
|
+
|
|
210
|
+
async def _flush_pending_output(self, active: ActiveTerminalSession) -> None:
|
|
211
|
+
if not active.pending_output and not active.output_truncated:
|
|
212
|
+
active.output_event.clear()
|
|
213
|
+
return
|
|
214
|
+
payload = ''.join(active.pending_output)
|
|
215
|
+
active.pending_output.clear()
|
|
216
|
+
active.pending_output_chars = 0
|
|
217
|
+
if active.output_truncated:
|
|
218
|
+
payload = f'{TRUNCATED_OUTPUT_NOTICE}{payload}'
|
|
219
|
+
active.output_truncated = False
|
|
220
|
+
active.output_event.clear()
|
|
221
|
+
if payload:
|
|
222
|
+
await self._emit(
|
|
223
|
+
self.send_output,
|
|
224
|
+
TerminalOutputMessage(terminal_id=active.terminal_id, data=payload),
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
async def _heartbeat_loop(self, terminal_id: str) -> None:
|
|
228
|
+
try:
|
|
229
|
+
while True:
|
|
230
|
+
await asyncio.sleep(self.heartbeat_interval)
|
|
231
|
+
active = self.sessions.get(terminal_id)
|
|
232
|
+
if active is None:
|
|
233
|
+
return
|
|
234
|
+
process = getattr(active.session, 'process', None)
|
|
235
|
+
poll = getattr(process, 'poll', None)
|
|
236
|
+
exit_code = poll() if callable(poll) else getattr(process, 'returncode', None)
|
|
237
|
+
process_alive = exit_code is None
|
|
238
|
+
await self._emit(
|
|
239
|
+
self.send_heartbeat,
|
|
240
|
+
TerminalHeartbeatMessage(terminal_id=terminal_id, process_alive=process_alive),
|
|
241
|
+
)
|
|
242
|
+
if not process_alive:
|
|
243
|
+
await self.close_terminal(terminal_id, reason='session_ended', exit_code=exit_code)
|
|
244
|
+
return
|
|
245
|
+
except asyncio.CancelledError:
|
|
246
|
+
raise
|
|
247
|
+
|
|
248
|
+
async def _emit(self, callback: MessageCallback, message: Any) -> None:
|
|
249
|
+
result = callback(message)
|
|
250
|
+
if inspect.isawaitable(result):
|
|
251
|
+
await result
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Server configuration helpers."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from controller.app.core.config import * # noqa: F401,F403
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
import uvicorn
|
|
7
|
+
from dotenv import load_dotenv
|
|
8
|
+
|
|
9
|
+
from controller.app.main import app, create_app
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
load_dotenv()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main() -> None:
|
|
16
|
+
parser = argparse.ArgumentParser(description="Bamboo Coding")
|
|
17
|
+
parser.add_argument("--dev", action="store_true", help="Run in development mode with auto-reload")
|
|
18
|
+
parser.add_argument("--port", type=int, help="Port to run on")
|
|
19
|
+
parser.add_argument("--host", help="Host to bind to")
|
|
20
|
+
args = parser.parse_args()
|
|
21
|
+
|
|
22
|
+
host = args.host or os.getenv("HOST", "0.0.0.0")
|
|
23
|
+
port = args.port or int(os.getenv("PORT", 8100))
|
|
24
|
+
reload_enabled = bool(args.dev)
|
|
25
|
+
|
|
26
|
+
uvicorn.run(
|
|
27
|
+
"bamboo_coding.server.main:app",
|
|
28
|
+
host=host,
|
|
29
|
+
port=port,
|
|
30
|
+
reload=reload_enabled,
|
|
31
|
+
factory=False,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
__all__ = ["app", "create_app", "main"]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
if __name__ == "__main__":
|
|
39
|
+
main()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from .protocol import (
|
|
2
|
+
AgentRegisterMessage,
|
|
3
|
+
CapabilityDescriptor,
|
|
4
|
+
ClientInfo,
|
|
5
|
+
ClientStatusMessage,
|
|
6
|
+
PublicRepositorySummary,
|
|
7
|
+
PublicRootSummary,
|
|
8
|
+
RepositoryDescriptor,
|
|
9
|
+
RootDescriptor,
|
|
10
|
+
RuntimeHealth,
|
|
11
|
+
TaskMessage,
|
|
12
|
+
TerminalCloseMessage,
|
|
13
|
+
TerminalClosedMessage,
|
|
14
|
+
TerminalInputMessage,
|
|
15
|
+
TerminalOpenMessage,
|
|
16
|
+
TerminalOutputMessage,
|
|
17
|
+
TerminalResizeMessage,
|
|
18
|
+
TerminalStatusMessage,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"AgentRegisterMessage",
|
|
23
|
+
"CapabilityDescriptor",
|
|
24
|
+
"ClientInfo",
|
|
25
|
+
"ClientStatusMessage",
|
|
26
|
+
"PublicRepositorySummary",
|
|
27
|
+
"PublicRootSummary",
|
|
28
|
+
"RepositoryDescriptor",
|
|
29
|
+
"RootDescriptor",
|
|
30
|
+
"RuntimeHealth",
|
|
31
|
+
"TaskMessage",
|
|
32
|
+
"TerminalCloseMessage",
|
|
33
|
+
"TerminalClosedMessage",
|
|
34
|
+
"TerminalInputMessage",
|
|
35
|
+
"TerminalOpenMessage",
|
|
36
|
+
"TerminalOutputMessage",
|
|
37
|
+
"TerminalResizeMessage",
|
|
38
|
+
"TerminalStatusMessage",
|
|
39
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from shared.protocol import * # noqa: F401,F403
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bamboo-coding
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Bamboo Coding controller and client
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: fastapi==0.115.0
|
|
8
|
+
Requires-Dist: uvicorn==0.30.6
|
|
9
|
+
Requires-Dist: gitpython==3.1.40
|
|
10
|
+
Requires-Dist: python-dotenv==1.0.0
|
|
11
|
+
Requires-Dist: websockets==13.1
|
|
12
|
+
Requires-Dist: httpx==0.27.2
|
|
13
|
+
Requires-Dist: bamboo-ssh
|
|
14
|
+
|
|
15
|
+
# Bamboo Coding
|
|
16
|
+
|
|
17
|
+
Bamboo Coding is a FastAPI-based workspace and Git review system with an outbound Python client and a browser UI.
|
|
18
|
+
|
|
19
|
+
## Architecture
|
|
20
|
+
|
|
21
|
+
- `bamboo_coding/server/`: packaged server entrypoint and public app surface
|
|
22
|
+
- `bamboo_coding/client/`: packaged outbound client runtime
|
|
23
|
+
- `controller/app/`: controller internals used by the packaged server entrypoint
|
|
24
|
+
- `shared/`: legacy shared protocol implementation backing the new namespace
|
|
25
|
+
- `static/`: browser UI assets
|
|
26
|
+
|
|
27
|
+
The controller never reads arbitrary local paths itself. Clients connect outward, advertise allowed roots, wait for approval, and then handle tasks for paths inside those roots.
|
|
28
|
+
|
|
29
|
+
## Features
|
|
30
|
+
|
|
31
|
+
- Web approval for pending clients
|
|
32
|
+
- Client-first workspace selection in the UI
|
|
33
|
+
- Direct folder browsing, including non-Git folders
|
|
34
|
+
- Automatic Git mode when the selected path is inside a repository
|
|
35
|
+
- Changed-files-first tree, diff viewer, commit history, branches, stage/unstage, and commit
|
|
36
|
+
- Durable client token storage and task journal
|
|
37
|
+
- Multi-terminal browser dialogs bound to the selected client and path
|
|
38
|
+
|
|
39
|
+
## Requirements
|
|
40
|
+
|
|
41
|
+
- Python 3.12+
|
|
42
|
+
- Git 2.x+
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
python3 -m venv .venv
|
|
48
|
+
. .venv/bin/activate
|
|
49
|
+
pip install -e .
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Client Config
|
|
53
|
+
|
|
54
|
+
The client reads `~/.bamboo-coding/bamboo-coding.toml` by default.
|
|
55
|
+
|
|
56
|
+
Quick start:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
bamboo-coding setup
|
|
60
|
+
bamboo-coding run
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`setup` creates the config file if it is missing. By default it uses the current working directory as the first allowed root. You can override the generated values:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
bamboo-coding setup --root /work/project-a --root /work/project-b --client-id worker-1 --hostname devbox
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Example generated config:
|
|
70
|
+
|
|
71
|
+
```toml
|
|
72
|
+
[client]
|
|
73
|
+
controller_url = "ws://127.0.0.1:8100/ws/agents"
|
|
74
|
+
client_id = "worker-1"
|
|
75
|
+
hostname = "devbox"
|
|
76
|
+
allowed_roots = ["/work/project-a", "/work/project-b"]
|
|
77
|
+
max_concurrent_tasks = 1
|
|
78
|
+
|
|
79
|
+
[storage]
|
|
80
|
+
token_path = "~/.bamboo-coding/token.json"
|
|
81
|
+
journal_path = "~/.bamboo-coding/journal.db"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Environment variables still override TOML values. Existing `AGENT_*` overrides remain supported during the transition.
|
|
85
|
+
|
|
86
|
+
## Makefile Commands
|
|
87
|
+
|
|
88
|
+
The repository now includes a root `Makefile` as the quick command hub.
|
|
89
|
+
|
|
90
|
+
Common targets:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
make help # list available commands
|
|
94
|
+
make dev # run the server locally
|
|
95
|
+
make start # start the server with docker compose
|
|
96
|
+
make stop # stop the docker compose stack
|
|
97
|
+
make restart # restart the docker compose stack
|
|
98
|
+
make logs # follow server logs
|
|
99
|
+
make ps # show compose service state
|
|
100
|
+
make test # run the full pytest suite
|
|
101
|
+
make client-install # install editable package plus build/upload tooling
|
|
102
|
+
make client-clean # remove build artifacts and Python cache files
|
|
103
|
+
make client-build # build wheel and sdist into dist/
|
|
104
|
+
make client-reinstall # clean and reinstall the editable package
|
|
105
|
+
make client-deploy # publish dist/* to real PyPI
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`client-deploy` publishes to the real PyPI index. It prompts for a PyPI deploy
|
|
109
|
+
token when one is missing:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
make client-deploy
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
You can also provide the token through the environment for non-interactive runs:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
export PYPI_TOKEN=<pypi-token>
|
|
119
|
+
make client-deploy
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The Makefile maps `PYPI_TOKEN` to Twine's token auth internally by setting
|
|
123
|
+
`TWINE_USERNAME=__token__` and `TWINE_PASSWORD=<pypi-token>`.
|
|
124
|
+
|
|
125
|
+
## Docker Folder
|
|
126
|
+
|
|
127
|
+
The server Docker assets live under `docker/`:
|
|
128
|
+
|
|
129
|
+
```text
|
|
130
|
+
docker/
|
|
131
|
+
compose.server.yml
|
|
132
|
+
server.Dockerfile
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Run Locally
|
|
136
|
+
|
|
137
|
+
### 1. Start the server
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
bamboo-coding-server --dev --host 127.0.0.1 --port 8100
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The legacy launcher also still works:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
.venv/bin/python app.py --dev --host 127.0.0.1 --port 8100
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### 2. Setup and run a client
|
|
150
|
+
|
|
151
|
+
First write the default config:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
bamboo-coding setup
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Then start the long-running client:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
bamboo-coding run
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Bare `bamboo-coding` is an alias for `bamboo-coding run`.
|
|
164
|
+
|
|
165
|
+
For one-off value overrides you can still use environment variables, but `run` still expects a config file to exist:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
CONTROLLER_URL=ws://127.0.0.1:8100/ws/agents AGENT_CLIENT_ID=worker-1 AGENT_ALLOWED_ROOTS=/work/project-a:/work/project-b bamboo-coding run
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
The module entrypoint is also available:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
.venv/bin/python -m bamboo_coding.client.main run
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### 3. Approve the client in the web UI
|
|
178
|
+
|
|
179
|
+
Open:
|
|
180
|
+
|
|
181
|
+
```text
|
|
182
|
+
http://127.0.0.1:8100/
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
The start page shows:
|
|
186
|
+
|
|
187
|
+
- approved clients first, with live root counts
|
|
188
|
+
- pending clients with an `Approve` button
|
|
189
|
+
- roots and immediate children only after you pick a specific client
|
|
190
|
+
|
|
191
|
+
Once approved, the client stores its token under `~/.bamboo-coding/` by default and reconnects automatically on the next start.
|
|
192
|
+
|
|
193
|
+
### 4. Open terminals from the web UI
|
|
194
|
+
|
|
195
|
+
After you pick a client and workspace, use the `Terminal` button in the header.
|
|
196
|
+
|
|
197
|
+
Terminal behavior:
|
|
198
|
+
|
|
199
|
+
- each terminal attaches to the currently selected client
|
|
200
|
+
- the initial shell directory uses the current selected path
|
|
201
|
+
- you can open multiple terminals in the same page
|
|
202
|
+
- windows can be dragged, resized, and maximized inside the page
|
|
203
|
+
- `Hide` moves a terminal into the bottom-right dock; click the dock item to restore it
|
|
204
|
+
- the header `Layout` control supports `Hidden`, `1`, `1|2`, `1|2|3`, and `2x2` presets
|
|
205
|
+
- applying `Hidden` docks every open terminal without closing the sessions
|
|
206
|
+
- applying a layout restores all open terminals, fills the preset slots by open order, and docks overflow terminals
|
|
207
|
+
- with an active layout, new terminals join the preset only while free slots remain; after that they open as floating windows
|
|
208
|
+
- the main page `Shortcuts` button opens browser-local terminal shortcut settings
|
|
209
|
+
- default terminal shortcuts are `Ctrl+Down` for `Hidden` and `Ctrl+Up` for `2x2`
|
|
210
|
+
- leaving or refreshing the page while any terminal windows remain open triggers the browser unload warning
|
|
211
|
+
|
|
212
|
+
There is also a standalone terminal page that reuses the same component:
|
|
213
|
+
|
|
214
|
+
```text
|
|
215
|
+
http://127.0.0.1:8100/terminal?agent_id=<agent-id>&path=/absolute/path
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### 5. Open a specific workspace directly
|
|
219
|
+
|
|
220
|
+
When multiple clients may advertise the same path, include both the client and the path:
|
|
221
|
+
|
|
222
|
+
```text
|
|
223
|
+
http://127.0.0.1:8100/?agent_id=<agent-id>&path=/absolute/path/inside/an/approved/root
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Path-only links still work when the owner is unambiguous:
|
|
227
|
+
|
|
228
|
+
```text
|
|
229
|
+
http://127.0.0.1:8100/?path=/absolute/path/inside/an/approved/root
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
If the path is inside a Git repository, Git actions are enabled. If it is only a normal folder, the UI still shows the file tree and editor.
|
|
233
|
+
|
|
234
|
+
## Public API
|
|
235
|
+
|
|
236
|
+
### Discovery and approval
|
|
237
|
+
|
|
238
|
+
- `GET /api/health`
|
|
239
|
+
- `GET /api/agents` (includes live `roots` for connected approved clients)
|
|
240
|
+
- `POST /api/agents/{agent_id}/approve`
|
|
241
|
+
- `GET /api/roots`
|
|
242
|
+
|
|
243
|
+
### Filesystem
|
|
244
|
+
|
|
245
|
+
- `GET /api/fs/tree?path=...&agent_id=...`
|
|
246
|
+
- `GET /api/fs/browse?path=...&agent_id=...`
|
|
247
|
+
- `GET /api/fs/file?path=...&agent_id=...`
|
|
248
|
+
- `PUT /api/fs/file?path=...&agent_id=...`
|
|
249
|
+
|
|
250
|
+
### Git
|
|
251
|
+
|
|
252
|
+
- `GET /api/git/status?path=...&agent_id=...`
|
|
253
|
+
- `GET /api/git/diff?path=...&file=...&agent_id=...`
|
|
254
|
+
- `GET /api/git/commits?path=...&agent_id=...`
|
|
255
|
+
- `GET /api/git/commit/{commit_hash}?path=...&agent_id=...`
|
|
256
|
+
- `POST /api/git/stage?path=...&agent_id=...`
|
|
257
|
+
- `POST /api/git/unstage?path=...&agent_id=...`
|
|
258
|
+
- `GET /api/git/branches?path=...&agent_id=...`
|
|
259
|
+
- `POST /api/git/branches?path=...&agent_id=...`
|
|
260
|
+
- `PUT /api/git/branches/checkout?path=...&agent_id=...`
|
|
261
|
+
- `DELETE /api/git/branches/{name}?path=...&agent_id=...`
|
|
262
|
+
- `POST /api/git/branches/merge?path=...&agent_id=...`
|
|
263
|
+
- `POST /api/git/commit?path=...&agent_id=...`
|
|
264
|
+
|
|
265
|
+
### Client transport
|
|
266
|
+
|
|
267
|
+
- `WS /ws/agents`
|
|
268
|
+
- `WS /ws/terminal?agent_id=...&path=...&terminal_id=...&cols=...&rows=...`
|
|
269
|
+
|
|
270
|
+
## Testing
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
.venv/bin/python -m pytest -q
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Repository Layout
|
|
277
|
+
|
|
278
|
+
- `app.py`: legacy server launcher pointing at `bamboo_coding.server.main`
|
|
279
|
+
- `bamboo_coding/server/`: packaged server entrypoint
|
|
280
|
+
- `bamboo_coding/client/`: packaged client runtime
|
|
281
|
+
- `controller/app/`: controller internals
|
|
282
|
+
- `static/`: SPA assets
|
|
283
|
+
- `docker/`: server Docker Compose and Dockerfile assets
|
|
284
|
+
- `tests/`: controller, client, protocol, and Git utility tests
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
git_utils.py,sha256=y1NsxtvsY7gOXjym-sVld6b4Qw5CQ-OdqzlqhBVY9eY,82025
|
|
2
|
+
agent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
agent/bamboo_git_agent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
agent/bamboo_git_agent/config.py,sha256=cDzCRi2Tz8H71-cK6gW8f4m6BlqOWT2Du-le0pDcE08,2454
|
|
5
|
+
agent/bamboo_git_agent/controller_client.py,sha256=qPMp-JEoCbSxMR5OH114QQ9ZjIi134jWU47n5ro7M3w,2467
|
|
6
|
+
agent/bamboo_git_agent/journal.py,sha256=xIK4aBYrYwqLddZ3LAX4tuY6ugjUPFxb5TP_Fkrqog8,8504
|
|
7
|
+
agent/bamboo_git_agent/main.py,sha256=svI0KmXWOBtybc7Gk1UviECg4XCEPbFF0MQgUwIx88o,7338
|
|
8
|
+
agent/bamboo_git_agent/protocol.py,sha256=MPSxQVhkw__IIqsqeS-j46OsUHjvWT_WJH4qnE42lcA,445
|
|
9
|
+
agent/bamboo_git_agent/status.py,sha256=QhCgmhDx6sLpsWHD516LGirBJMxDDFFfHjLdg5BHbJk,2532
|
|
10
|
+
agent/bamboo_git_agent/capabilities/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
agent/bamboo_git_agent/capabilities/repo.py,sha256=HJrkmcEGgnAZ7uXW2Yky3INno4DG-Vhesa9qO5JPM2w,7271
|
|
12
|
+
api/__init__.py,sha256=UMv3OaonM4UouYy8NRtLWNj5Hh1IE-SEdV0jMTMya10,35
|
|
13
|
+
api/commit.py,sha256=IGS9O5_A22cZ6dBWR0mYqOUgT4HQl3lt2mC5jWiE8po,2343
|
|
14
|
+
api/repository.py,sha256=HiGwG-Bjz_Toy4sUGgKLopkAexYD2QbvinPOgRNowvo,17763
|
|
15
|
+
bamboo_coding/__init__.py,sha256=46h_Uj5gdx8oYtLJDZMgSyA-APe5YE7vLfgyQaCb32o,71
|
|
16
|
+
bamboo_coding/client/__init__.py,sha256=3wip8Z6F-mf1YUXavSPFM1jBu3i3NXU1Yp1ymaHrpq8,125
|
|
17
|
+
bamboo_coding/client/config.py,sha256=3NQ7HzmOy1Y-rjgDAUbyKJ1RCxpOylWR8ZY5boxUFFU,5953
|
|
18
|
+
bamboo_coding/client/controller_client.py,sha256=UvhbOZpszNy8soOkoCevCuSbDDpwjDNzZTdVAVbQ5qo,4262
|
|
19
|
+
bamboo_coding/client/journal.py,sha256=uZJesILPzOYKTJJaJAWN5_e_h3ONp1Nl7EVbYtfhHuA,64
|
|
20
|
+
bamboo_coding/client/main.py,sha256=osxHMu3UJGAvmrJmhS1Jd4yKuggUiqn-iCc3RjvBfXs,17697
|
|
21
|
+
bamboo_coding/client/status.py,sha256=xX2Ofjmuf5B9y7NsdG9djxNoH1DOw_NWr0gjK_GPw6c,2546
|
|
22
|
+
bamboo_coding/client/terminal_runtime.py,sha256=_mWWKOUcizqgwLXYhZAxI_LPW0bzTXcd03b58ZYi6yA,9916
|
|
23
|
+
bamboo_coding/client/capabilities/__init__.py,sha256=B7qxGhwcbKpRuE2jiZDa85BzYRCFyTEGWK_TWfRvFWQ,34
|
|
24
|
+
bamboo_coding/client/capabilities/repo.py,sha256=02FhJGdnT2Jpd9whfKfxzrQvNt9xwty1hm42hwmzLYw,10768
|
|
25
|
+
bamboo_coding/server/__init__.py,sha256=T-PUHHEg8K4GY9IkpxyN4-28eD01KU2W1JfgNqMHT8U,81
|
|
26
|
+
bamboo_coding/server/main.py,sha256=85P-AAzjiGKfSHBtTlMfDYq9-rUTuV0xf7TGBhITSGQ,903
|
|
27
|
+
bamboo_coding/server/core/__init__.py,sha256=Utes-Fcxa2vnzgU3ZCimYF7Y78PrzswLkPpNtQpUHF0,36
|
|
28
|
+
bamboo_coding/server/core/config.py,sha256=RhQF4gYFtfC6k_fm33nKQgwJgE0GZYn-OV81u9oOJHY,60
|
|
29
|
+
bamboo_coding/shared/__init__.py,sha256=2hU3m0yWwYevIoOEJqkoumqVGW4CGrQIYHE_q1ftpbk,899
|
|
30
|
+
bamboo_coding/shared/protocol.py,sha256=qnbT6fMKp-d7-e_sFq5QPfG0gPTrxuAAuczLFZuTenc,49
|
|
31
|
+
controller/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
32
|
+
controller/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
33
|
+
controller/app/main.py,sha256=2TAQewTF65lBxKHUimmBT6mnIE0ZRdl9kuHkVUM6fGM,2841
|
|
34
|
+
controller/app/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
35
|
+
controller/app/api/agents/__init__.py,sha256=-GnMriR-KQnvpEZYjyDNFEtMHV5yXpfJmBQ4bAXWn-w,45
|
|
36
|
+
controller/app/api/agents/ws.py,sha256=VQN_1D5QezbjIaMXiCXv1UxoCeEHTz3QIKuCfhtV4hA,5411
|
|
37
|
+
controller/app/api/public/__init__.py,sha256=jLNHGmfLCF4sFk01LQ0yUCYyPDiSiyyjtkmRu8Qysmc,244
|
|
38
|
+
controller/app/api/public/repos.py,sha256=lsRQFNFEo5koiezskzLalTuv19YQO_EdWVG9pLJg-Wg,9087
|
|
39
|
+
controller/app/api/public/terminals.py,sha256=n0QOHw2WT1ozqKh_LqUHysDVZ9VeHG3zMvoZA20w0wY,2365
|
|
40
|
+
controller/app/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
41
|
+
controller/app/core/config.py,sha256=cWBLY43Y49eBuFRWSBvjueXBpTkMDIUyBEqyl1-FHFc,560
|
|
42
|
+
controller/app/core/errors.py,sha256=D6KFR3llcvaSDEdJTZtRZh7ONREEfUlGDLVt1q4YW0M,404
|
|
43
|
+
controller/app/db/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
44
|
+
controller/app/db/models.py,sha256=woNGECTOCIwrAT9HSLLHXNZVTejc3Cd_hBg_-6keZtw,722
|
|
45
|
+
controller/app/db/session.py,sha256=DHGrcYmrCLb7b_0IBmxuaJUAA03yRnWrvHl_V0K_H3o,2759
|
|
46
|
+
controller/app/schemas/__init__.py,sha256=-G-eagWihlL-tJxVe6hKt_v94dAx3swVKd91DLxyXIQ,520
|
|
47
|
+
controller/app/schemas/agent_messages.py,sha256=MPSxQVhkw__IIqsqeS-j46OsUHjvWT_WJH4qnE42lcA,445
|
|
48
|
+
controller/app/schemas/public.py,sha256=etpMQUTTaLIw33f2rI_XAVhRfl398P7GyUlwLBzUyiU,131
|
|
49
|
+
controller/app/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
50
|
+
controller/app/services/agents.py,sha256=ocPa2Bcv5WatyTPTiYAjWGDJfIC52C9y3S3i643wfA4,4841
|
|
51
|
+
controller/app/services/registrations.py,sha256=rzc_Wv-YczoNSRE3XpDCLLQLhfP9Hyb9c_npublhcWA,742
|
|
52
|
+
controller/app/services/repositories.py,sha256=soQkGdkXQz0r2ZcutJ3SNAlty1XBJhsP-nqfL4rodQs,3659
|
|
53
|
+
controller/app/services/router.py,sha256=_Nx48mvusDiQm9Fj_a8hrC0BZevkMn6rUNoLw3ga2SQ,5406
|
|
54
|
+
controller/app/services/tasks.py,sha256=mevkv9ofQWARuSres9PLgP6jKX0_-maW9tS_4C4iyUk,4673
|
|
55
|
+
controller/app/services/terminals.py,sha256=jYDqF9Eo9gQaI8teBdUziBKPYzyl5iwDq0K6LaRWxHY,5890
|
|
56
|
+
shared/__init__.py,sha256=2hU3m0yWwYevIoOEJqkoumqVGW4CGrQIYHE_q1ftpbk,899
|
|
57
|
+
shared/protocol.py,sha256=7sTnV0tHGoRND6SjbTrASE1HEuLG3TJ-CF4l5QqRxm0,6925
|
|
58
|
+
bamboo_coding-0.1.0.dist-info/METADATA,sha256=2y_tje1EBwqmtLfoazs4SvaA78q2_5-YzPPTnR4EI7M,8493
|
|
59
|
+
bamboo_coding-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
60
|
+
bamboo_coding-0.1.0.dist-info/entry_points.txt,sha256=LYwgfzzCTbCjBo3iImfUbBYSjXf2NkscFv6FA4IxHvQ,119
|
|
61
|
+
bamboo_coding-0.1.0.dist-info/top_level.txt,sha256=oWwVpBlYKI-qdAPqf9XWLBCjLfKxN2W_D3gNJo8_pVo,52
|
|
62
|
+
bamboo_coding-0.1.0.dist-info/RECORD,,
|